Writing and Modifying Delegates

Overview

Now you need to provide the code implementation for overriding the distance covered by the racers. Building a delegate module with the SysML v2 API has never been easier. The code required to get up is small and only needs to be setup once. The <BEE Install Directory>/samples/sysml2/tutorialTortoiseVsHare/part-2 contains the completed delegate module that you can reference at any time.

Prerequisites

Prerequisite Description
Behavior Execution Engine Installation You must have installed Behavior Execution Engine.
Tutorial Project You must start this section with the Behavior Execution Engine simulation project from the previous section.

Instructions

Initial Delegate Module Setup

  1. Create a package called com.agi.mbse.tutorial.tortoisevshare package under the src folder.
  2. Create a Java Class with the name TortoiseVsHareDelegateModuleProvider.
  3. Modify the class signature to implement the com.agi.mbse.sysml2.spi.DelegateModuleProvider interface.
  4. Create an override method for the interface’s provideDelegateModule method. The class should look like the following:
    public class TortoiseVsHareDelegateModuleProvider implements DelegateModuleProvider {
        @Override
        public DelegateModule provideDelegateModule() {
        }
    }

    The delegate module provider just does one thing. It provides the delegate module to Behavior Execution Engine when the system loads. To keep things concise, we are going to define the delegate module in the same file as the provider.

  5. Return a new DelegateProperty for the previously defined provideDelegateModule method.
  6. Create an override method for the DelegateProperty interface’s getIdentity method.
  7. Return a new DelegateIdentity for the previously defined getIdentity method using the following constructor arguments:
    1. UUID.fromString("d547ca5c-3fb7-11f0-ac13-325096b39f47")
    2. "TortoiseVsHare Part 2 Module"
    3. "The delegate module for the Tortoise vs Hare part 2 sample."

The class should now look like the following:

public class TortoiseVsHareDelegateModuleProvider implements DelegateModuleProvider {
    @Override
    public DelegateModule provideDelegateModule() {
        return new DelegateModule() {
            @Override
            public DelegateIdentity getIdentity() {
                return new DelegateIdentity(
                        UUID.fromString("d547ca5c-3fb7-11f0-ac13-325096b39f47"),
                        "TortoiseVsHare Part 2 Module",
                        "The delegate module for the Tortoise vs Hare part 2 sample."
                );
            }
        };
    }
}

The DelegateIdentity is a class that represents the identifying information for a delegate module. The constructor we just used takes in three arguments: the UUID – universally unique identifier, the name of the module, and a short description of what the module does. Keep in mind this identifier should be a different generated UUID for every delegate module. No two modules should share the same UUID.

For the delegate module to compile, we need to add one more required method to the DelegateProperty.

  1. Create an override method for the interface’s registerDelegate method just below the getIdentity method.

At this point the class should look like the following:

public class TortoiseVsHareDelegateModuleProvider implements DelegateModuleProvider {
    @Override
    public DelegateModule provideDelegateModule() {
        return new DelegateModule() {
            @Override
            public DelegateIdentity getIdentity() {
                return new DelegateIdentity(
                        UUID.fromString("d547ca5c-3fb7-11f0-ac13-325096b39f47"),
                        "TortoiseVsHare Part 2 Module",
                        "The delegate module for the Tortoise vs Hare part 2 sample."
                );
            }

            @Override
            public void registerDelegate(CustomCodeRegistry codeRegistry) {

            }
        };
    }
}

That’s all the code Behavior Execution Engine needs to load in the delegate module. The rest of the code will be using the new opt-in system where you can specify specific elements in the model that you want to delegate for.

Finally, for Behavior Execution Engine to see the delegate module, you need to make an addition to the com.agi.mbse.sysml2.spi.DelegateModuleProvider file.

  1. Go to the directory DelegateSource/resources/META-INF/services.
  2. Open the file com.agi.mbse.sysml2.spi.DelegateModuleProvider.
  3. Set the content to: com.agi.mbse.tutorial.tortoisevshare.TortoiseVsHareDelegateModuleProvider.

Now you are ready to build and test to see if Behavior Execution Engine can see your new delegate module.

  1. Run the gradle install task again.
  2. Verify that the delegate module has been placed in the expected directory. By default, it should be <BEE Install Directory>/delegates.
  3. Create a new config file on your computer called TortoiseVsHare Part 2.config.
  4. Copy and paste the snippet below into the config file:
    {
        "configVersion" : "0.1.0",
        "delegateModules" : [
            {
                "identifier" : "d547ca5c-3fb7-11f0-ac13-325096b39f47",
                "name" : "TortoiseVsHare Part 2 Module"
            }
        ],
        "additionalDelegateModuleSearchDirectories" : [],
        "simConfigs" : [
            {
                "name" : "Part-2 Delegate Version",
                "startTime" : "2000-01-01T12:00:00.000000Z",
                "stopTime" : "2000-01-02T12:00:00.000000Z",
                "targetElement" : "TortoiseVsHare Part 2::infamousRaceCase"
            }
        ]
    }

    If you do not have com.agi.mbse.delegateModuleDir property set to the <BEE Install Directory>/delegates directory, then you'll need to add your custom location to the additionalDelegateModuleSearchDirectories list.

  5. Open the TortoiseVsHare Part 2 file for model execution.
  6. Select the configuration file that you created in the previous steps.
  7. Execute the simulation.

Example Execution with Initial Module

If everything worked as expected, you should see a line that says, Executing simulation with delegate modules: [TortoiseVsHare Part 2 Module]. This indicates that Behavior Execution Engine was able to find and load the delegate module. You might have also noticed that there the simulation failed with an exception. Don’t worry, this is expected.

Stubbing out a Scalar Value Feature Accessor

Previously in this tutorial, we had you remove the feature value expression from the Racer’s distanceCovered attribute. Feature value expressions for attribute are normally required for execution but in the case where you want to delegate for them, only default values are allowed.

Since we don't want to use a fixed rate check-in system for keeping track of the distance covered by each racer, we are going to use delegate code to handle this.

  1. Create a new java class file in your delegate module project called DistanceCoveredAccessor and have it implement the ScalarValueFeatureAccessor interface with the generic type set to ScalarQuantityValue.
  2. Create override methods for the two required methods on the ScalarValueFeatureAccessor interface: getCurrentValue and setCurrentValue. It should look like the following:
    public class DistanceCoveredAccessor implements ScalarValueFeatureAccessor<ScalarQuantityValue> {
        @Override
        public List<ScalarQuantityValue> getCurrentValue(@NotNull InstanceContext context) {
        }

        @Override
        public void setCurrentValue(@NotNull List<Object> value, @NotNull InstanceContext context) {
        }
    }
  3. 3. Throw an IllegalStartException for the setCurrentValue method. In this delegate, we will not be allowing the sysml model to modify the value of the distanceCovered attribute. This means that any assignment actions in the model would be unexpected and should throw an error. It should look like the following:
    @Override
    public void setCurrentValue(@NotNull List<Object> value, @NotNull InstanceContext context) {
        throw new IllegalStateException("Setting is not allowed in this delegate.");
    }

Before we get to determining what the current value of the racer’s distanceCovered attribute actually is, we need to come up with a system for keeping track of it.

Registering Delegation Data

In the system we are designing as a part of this tutorial, the racers will change their speeds in response to specific events. Recording a history of how fast the racer was going, how far the racer has traveled, and the time at which the racer’s speed changed will enable us to calculate where the racer actually is at any point in the race. Furthermore, we will be able to calculate at what time intervals the racer will be at specific points of interest in the race. To do this, let’s create a simple structure for keeping track of the racer’s movement history.

  1. Create a new java class file called RacerData.
  2. Set the content of the RacerData class to the following:
    public record RacerData(AbsoluteTime time, ScalarQuantityValue speed, ScalarQuantityValue distanceRan) {
    }

This Java record class will act as our individual data points for keeping track of the state changes in the race. Now we will need something to keep track of those points to tie it together.

  1. Create a new java class file called RacerDataHistory.
  2. Set the contents of the RacerDataHistory class to the following:
    public class RacerDataHistory {
        private final List<RacerData> mHistory = new ArrayList<>();

        public RacerData getLastRecordedRacerData() {
            if (!mHistory.isEmpty()) {
                return mHistory.get(mHistory.size() - 1);
            } else {
                return null;
            }
        }

        public void recordRacerData(AbsoluteTime time, ScalarQuantityValue speed, ScalarQuantityValue distanceRan) {
            mHistory.add(new RacerData(time, speed, distanceRan));
        }
    }

Now we have class capable of recording when the racers speed changes. Behavior Execution Engine provides a way of registering a copy of this class for every instance of an element. If we register this class for the Racer part, then we will ensure that every instance of Racer (and by extension Tortoise and Hare) will have a RacerDataHistory instance created alongside.

  1. Open the TortoiseVsHareDelegateModuleProvider.
  2. Add the following line above the provideDelegateModule method:
    public static final String rootPackageName = "TortoiseVsHare Part 2";

    If you gave the project a different name, change the string to match.

  3. Add the following lines inside the registerDelegate method:
    codeRegistry.registerInstanceDataFor(rootPackageName + "::Racer", RacerDataHistory.class, context -> {
        return new RacerDataHistory();
    });

    codeRegistry.delegateScalarValueForFeature(rootPackageName + "::Racer::distanceCovered", new DistanceCoveredAccessor());

The first call to the codeRegistry is how we ensure that the tortoise and hare parts will have a RacerDataHistory instance that will be available for access later in our delegate code. The second call is telling Behavior Execution Engine that instead of the default implementation, we want to delegate for the Racer::distanceCovered attribute using the DistanceCoveredAccessor class.

Working with units

This model uses ScalarQuantityValues to represent various attributes of the race and racers. To work with these, we need to keep in mind that there are two pieces that we need to work with – a number and a unit. Using Indriya, you have access to an abundance of standard units to use and perform simple unit conversions with to ensure all of our calculations are in the same unit of reference. See this page about working with units.

  1. Create a new java class file called HelperCode.
  2. Add the following static method inside the class for use across the delegate module:
    public static double convertScalarQuantityUnitValue(ScalarQuantityValue value, Unit<?> unit) {
        try {
            UnitConverter converter = value.getRefUnit().getConverterToAny(unit);
            return converter.convert(value.getValue()).doubleValue();
        } catch (IncommensurableException e) {
            throw new RuntimeException(e);
        }
    }
  3. Open the RacerDataHistory class.
  4. Add the following method below the recordRacerData method:
    public ScalarQuantityValue calculateDistanceAtTime(AbsoluteTime time) {
        RacerData lastRecordedRacerData = getLastRecordedRacerData();

        double previousSpeedMetersPerSecond = HelperCode. convertScalarQuantityUnitValue(lastRecordedRacerData.speed(), Units.METRE_PER_SECOND);
        double previousDistanceRanMeters = HelperCode. convertScalarQuantityUnitValue(lastRecordedRacerData.distanceRan(), Units.METRE);

        long deltaTimeSeconds = Duration.between(lastRecordedRacerData.time().toJavaInstant(), time.toJavaInstant()).toSeconds();

        double distanceMeters = previousDistanceRanMeters + previousSpeedMetersPerSecond * deltaTimeSeconds;

        return new ScalarQuantityValue(distanceMeters, Units.METRE);
    }

After making these changes, the delegate module has a reusable way of converting units and calculating where a racer would be at a given future time. Since the accessor has the ability to call on the RacerDataHistory, we are now ready to integrate the two together.

  1. Open the DistanceCoveredAccessor class.
  2. Add the following lines to the getCurrentValue method:
    AbsoluteTime currentTime = context.getTimeService().getCurrentSimulationTime();

    RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
    return List.of(racerDataHistory.calculateDistanceAtTime(currentTime));

At this point the delegate module is in a compilable state again. Install the delegate module and run the simulation again. You will see that there is now a new exception. The log will report that: Exception encountered during implementation of delegate module., along with a null pointer exception because lastRecordedRacerData is null. This makes sense because we haven’t actually recorded any speeds yet.

Delegating for actions

In the TortoiseVsHare Part 2 model, we have effect actions on transitions that don’t currently do anything other than show up in the log when they occur. Let’s start by creating an organized file for the racer.

  1. Create a java class file called RacerCode.
  2. Create two static methods that take in an ActionContext as a parameter. One should be called startRacing (as it is meant to delegate for the startRacing action) and one for stopRacing (for stopRacing, naturally). The RacerCode class should look like the following:
    public class RacerCode {
        public static void startRacing(ActionContext context) {
        }

        public static void stopRacing(ActionContext context) {
        }
    }
  3. Open the TortoiseVsHareDelegateModuleProvider class.
  4. Add the following lines to the end of the registerDelegate method:
    codeRegistry.delegateExecutionOfAction(rootPackageName + "::Racer::startRacing", context -> {
        RacerCode.startRacing(context);
        return DelegationResult.NoAdditionalEvents.INSTANCE;
    });

    codeRegistry.delegateExecutionOfAction(rootPackageName + "::Racer::stopRacing", context -> {
        RacerCode.stopRacing(context);
        return DelegationResult.NoAdditionalEvents.INSTANCE;
    });

These calls to codeRegistry will tell Behavior Execution Engine that whenever the Racer’s startRacing or stopRacing actions would occur, to instead execute the delegated action. You can verify this behavior by installing and running again while looking for the DelegatedActionExecuted log in the trace. Keep in mind that delegating for actions will replace all the original behavior of the action. This means that any owned actions will not occur. This is the reason why we removed those assignment actions from the model earlier.

The delegateExecutionOfAction method expects a return of a DelegationResult. This result is used to tell Behavior Execution Engine if there are any future events that the delegated action should wait on before performing. Dealing with future events is out of scope of this tutorial so we use the singleton NoAdditionalEvents instance to tell Behavior Execution Engine that after performing this delegated step, the action is ready to complete. More information can be found on the delegate module page.

Recording information during the race

With the basic stubs in place, we can now add in the logic for changing the racer’s speed over time.

  1. Open the RacerCode class.
  2. Add the following static helper method for when the racers start running:
    public static void recordRunningStart(ActionContext context, ScalarQuantityValue distanceRan, AbsoluteTime time) {
        RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
        ScalarQuantityValue topSpeed = (ScalarQuantityValue) context.getAccessorService().requestFeature(rootPackageName + "::Racer::topSpeed").getValue().get(0);

        racerDataHistory.recordRacerData(time, topSpeed, distanceRan);
    }
  3. Change the startRacing method to the following:
    public static void startRacing(ActionContext context) {
        AbsoluteTime time = context.getTimeService().getCurrentSimulationTime();

        recordRunningStart(context, new ScalarQuantityValue(0, Units.METRE), time);
    }
  4. Add the following static helper method for when the racers stop running:
    public static void recordRunningStop(ActionContext context, ScalarQuantityValue distanceRan, AbsoluteTime time) {
        RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
        racerDataHistory.recordRacerData(time, new ScalarQuantityValue(0, Units.METRE_PER_SECOND), distanceRan);
    }
  5. Change the stopRacing method to the following:
    public static void stopRacing(ActionContext context) {
        AbsoluteTime time = context.getTimeService().getCurrentSimulationTime();

        FeatureReadWriteAccessor raceFinishedAccessor = context.getAccessorService().requestMutableFeature(rootPackageName + "::Racer::raceFinished");
        raceFinishedAccessor.setValue(List.of(true));

        FeatureReadAccessor distanceCoveredAccessor = context.getAccessorService().requestFeature(rootPackageName + "::Racer::distanceCovered");
        ScalarQuantityValue distanceCovered = (ScalarQuantityValue) distanceCoveredAccessor.getValue().get(0);

        recordRunningStop(context, distanceCovered, time);
    }
  6. Install and execute the simulation again.

After making these basic changes, the racers now mark when they start racing and how fast they are going. We are going to have to add in more logic for the hare since there are additional behaviors at play for napping during the race. However, this change brought the racers another step closer to finishing the race. You will notice that the exception that was raised this time was about not being able to create an evaluator for the distanceCovered attribute. This new exception occurs when you use a delegated scalar value in a change trigger. Behavior Execution Engine needs a way to determine when some predicate of the change event will change from false to true. More information on change events can be found here.

Delegating for a scalar threshold comparison

Normally, the threshold system for ScalarValueFeatureAccessors is defaulted to have no implementation. However, in this case, we will need an implementation for one.

  1. Open the DistanceCoveredAccessor class.
  2. Create an override method for the interface’s canCreateEvaluator method.
  3. Have the method return true in every case. This model doesn’t require any sophisticated checking, and we therefore know we’ll always be able to evaluate the distanceCovered for any given time. The method should look like the following:
    @Override
    public boolean canCreateEvaluator(@NotNull TimeInterval analysisInterval) {
        return true;
    }
  4. Create an override method for the interface’s createEvaluator method.
  5. Return a new ScalarValueFeatureEvaluator for the previously defined method. It should look like the following:
    @Override
    public ScalarValueFeatureEvaluator<ScalarQuantityValue> createEvaluator(@NotNull TimeInterval analysisInterval) {
        return new ScalarValueFeatureEvaluator<ScalarQuantityValue>() {
        }
    }
  6. Create an override method for the ScalarValueFeatureEvaluator interface’s getValue method with the following implementation:
    @Override
    public List<ScalarQuantityValue> getValue(@NotNull InstanceContext context) {
        AbsoluteTime currentTime = context.getTimeService().getCurrentSimulationTime();

        RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
        return List.of(racerDataHistory.calculateDistanceAtTime(currentTime));
    }

    The point of the evaluator is to compare possible futures together until the earliest event is determined.

  7. Create an override method for the ScalarValueFeatureEvaluator interface’s computeSatisfactionIntervals method with the following implementation :
    @Override
    public List<TimeInterval> computeSatisfactionIntervals(
            @NotNull InstanceContext context,
            Object threshold,
            ComparisonOperator comparisonOperator
    ) {
        AbsoluteTime currentTime = context.getTimeService().getCurrentSimulationTime();

        ScalarQuantityValue thresholdDistance = (ScalarQuantityValue) threshold;
        double thresholdDistanceMeters = HelperCode.convertScalarQuantityUnitValue(thresholdDistance, Units.METRE);

        RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
        RacerData lastRecordedRacerData = racerDataHistory.getLastRecordedRacerData();

        double previousSpeedMetersPerSecond = convertScalarQuantityUnitValue(lastRecordedRacerData.speed(), Units.METRE_PER_SECOND);
        double analysisStartDistanceMeters = racerDataHistory.calculateDistanceAtTime(currentTime).getValue().doubleValue();

        double deltaTimeSeconds = Math.ceil((thresholdDistanceMeters - analysisStartDistanceMeters) / previousSpeedMetersPerSecond);
        AbsoluteTime satisfiedTime = currentTime.addTotalSeconds(deltaTimeSeconds);

        List<TimeInterval> satisfiedIntervals = null;
        switch (comparisonOperator) {
            case GREATER_THAN ->
                    satisfiedIntervals = List.of(new TimeInterval(satisfiedTime, analysisInterval.getStop(), false, true));
            case GREATER_THAN_OR_EQUAL ->
                    satisfiedIntervals = List.of(new TimeInterval(satisfiedTime, analysisInterval.getStop()));
            case LESS_THAN ->
                    satisfiedIntervals = List.of(new TimeInterval(analysisInterval.getStart(), satisfiedTime, true, false));
            case LESS_THAN_OR_EQUAL ->
                    satisfiedIntervals = List.of(new TimeInterval(analysisInterval.getStart(), satisfiedTime));
            case EQUAL -> satisfiedIntervals = List.of(new TimeInterval(satisfiedTime, satisfiedTime));
            case NOT_EQUAL -> satisfiedIntervals = List.of(
                    new TimeInterval(analysisInterval.getStart(), satisfiedTime, true, false),
                    new TimeInterval(satisfiedTime, analysisInterval.getStop(), false, true)
            );
        }
        return satisfiedIntervals;
    }

In this method we are gathering historical information on where the racer was, how fast the racer was going, and when this information was recorded from the history. The threshold represents a ScalarQuantityValue distance that we are comparing to. We use the history information to calculate when the racer will have reached that threshold distance. After we calculate when the racer reaches the threshold, we can use the provided comparison operator to contextualize exactly when the comparison is satisfied.

Adding Logic for the Hare

If you run the simulation again now, you can look for the Winner and Loser StateStarted messages. You will see that the hare will win the race and the tortoise loses. This is because the hare is faster than the tortoise, and we are still missing the custom logic for napping.

  1. Create a new java class file called HareCode.
  2. Create a static method called startNapping with the following implementation:
    public static void startNapping(ActionContext context) {
        AbsoluteTime time = context.getTimeService().getCurrentSimulationTime();
        FeatureReadAccessor distanceCoveredAccessor = context.getAccessorService().requestFeature(rootPackageName + "::Racer::distanceCovered");
        ScalarQuantityValue distanceCovered = (ScalarQuantityValue) distanceCoveredAccessor.getValue().get(0);

        FeatureReadWriteAccessor isNappingAccessor = context.getAccessorService().requestMutableFeature(rootPackageName + "::Hare::isNapping");
        isNappingAccessor.setValue(List.of(true));

        RacerCode.recordRunningStop(context, distanceCovered, time);
    }
  3. Create a static method called stopNapping with the following implementation:
    public static void stopNapping(ActionContext context) {
        AbsoluteTime time = context.getTimeService().getCurrentSimulationTime();
        FeatureReadAccessor distanceCoveredAccessor = context.getAccessorService().requestFeature(rootPackageName + "::Racer::distanceCovered");
        ScalarQuantityValue distanceCovered = (ScalarQuantityValue) distanceCoveredAccessor.getValue().get(0);

        FeatureReadWriteAccessor isNappingAccessor = context.getAccessorService().requestMutableFeature(rootPackageName + "::Hare::isNapping");
        isNappingAccessor.setValue(List.of(false));
        FeatureReadWriteAccessor wantsToNapAccessor = context.getAccessorService().requestMutableFeature(rootPackageName + "::Hare::hasTakenANap");
        wantsToNapAccessor.setValue(List.of(true));

        RacerCode.recordRunningStart(context, distanceCovered, time);
    }
  4. Open the TortoiseVsHareDelegateModuleProvider class.
  5. Add the following lines to the end of the registerDelegate method:
    codeRegistry.delegateExecutionOfAction(rootPackageName + "::Hare::startNapping", context -> {
        HareCode.startNapping(context);
        return DelegationResult.NoAdditionalEvents.INSTANCE;
    });

    codeRegistry.delegateExecutionOfAction(rootPackageName + "::Hare::stopNapping", context -> {
        HareCode.stopNapping(context);
        return DelegationResult.NoAdditionalEvents.INSTANCE;
    });

As you can see, the logic from the previous tutorial’s assignment actions is now faithfully recreated in the delegated actions.

Now that the hare’s speed can be zero when resting, we need to add more logic to that threshold evaluator to avoid a division by zero.

  1. Open the DistanceCoveredAccessor class.
  2. Add the following method below the computeSatisfactionIntervals method:
    private boolean isSatisfiedWithCurrentDistance(
            ComparisonOperator comparisonOperator,
            double referenceDistanceMeters,
            double thresholdDistanceMeters
    ) {
        boolean satisfied = false;
        switch (comparisonOperator) {
            case GREATER_THAN -> satisfied = referenceDistanceMeters > thresholdDistanceMeters;
            case GREATER_THAN_OR_EQUAL -> satisfied = referenceDistanceMeters >= thresholdDistanceMeters;
            case LESS_THAN -> satisfied = referenceDistanceMeters < thresholdDistanceMeters;
            case LESS_THAN_OR_EQUAL -> satisfied = referenceDistanceMeters <= thresholdDistanceMeters;
            case EQUAL -> satisfied = referenceDistanceMeters == thresholdDistanceMeters;
            case NOT_EQUAL -> satisfied = referenceDistanceMeters != thresholdDistanceMeters;
        }
        return satisfied;
    }
  3. Add the following lines below the declaration of the analysisStartDistanceMeters variable:
    if (previousSpeedMetersPerSecond == 0.0) {
        boolean satisfied = isSatisfiedWithCurrentDistance(comparisonOperator, analysisStartDistanceMeters, thresholdDistanceMeters);
        if (satisfied) {
            return List.of(analysisInterval);
        } else {
            return List.of();
        }
    }

With these last additions, the delegate module’s logic is complete. Reinstalling the delegate module and running the simulation again will reveal that the tortoise finishes the race before the hare, since the hare cannot make up the distance after the nap.

Adding Logging to the Module

You might find it difficult to sort through all the messages that show up in the trace in the log. To remedy this, you can add in your own custom log messages that will add in more useful and contextualized information for your delegate module during simulation.

  1. Open the HelperCode class.
  2. Add in the following method to the class:
    public static void logHareAndTortoiseComparison(ActionContext context, AbsoluteTime time, ScalarQuantityValue distanceCovered, ContextLogger logger) {
        FeatureReadAccessor tortoiseAccessor = context.getAccessorService().requestFeature(rootPackageName + "::Hare::tortoise");
        InstanceValue tortoise = (InstanceValue) tortoiseAccessor.getValue().get(0);
        RacerDataHistory tortoiseInformation = tortoise.getContext().getDataService().getDelegationData(RacerDataHistory.class);
        ScalarQuantityValue tortoiseDistanceCovered = tortoiseInformation.calculateDistanceAtTime(time);

        double distanceAheadMeters =
            convertScalarQuantityUnitValue(distanceCovered, Units.METRE) - convertScalarQuantityUnitValue(tortoiseDistanceCovered, Units.METRE);
        if (distanceAheadMeters > 0) {
            logger.info("The hare is " + Math.abs(distanceAheadMeters) + " meters behind the tortoise.");
        } else {
            logger.info("The hare is " + distanceAheadMeters + " meters ahead of the tortoise.");
        }
    }

These new log messages will give us a good indication of where the hare is in comparison to the tortoise during the race. Now we can add that call and more diagnostic information to the other methods.

  1. Open the RacerCode class.
  2. Add the following lines to the startRacing method:
    ContextLogger logger = context.getContextLogger();

    String racerName = (String) context.getAccessorService().requestFeature(rootPackageName + "::Racer::name").getValue().get(0);
    logger.info("The " + racerName + " is starting to run the race at: " + time.toIso8601String());

    if (racerName.equals("hare")) {
        HelperCode.logHareAndTortoiseComparison(context, time, new ScalarQuantityValue(0, Units.METRE), logger);
    }
  3. Add the following lines to the stopRacing method:
    ContextLogger logger = context.getContextLogger();

    String racerName = (String) context.getAccessorService().requestFeature(rootPackageName + "::Racer::name").getValue().get(0);
    logger.info("The " + racerName + " stopped running the race at: " + time.toIso8601String());
  4. Open the HareCode class.
  5. Add the following lines to the startNapping method:
    ContextLogger logger = context.getContextLogger();
    HelperCode.logHareAndTortoiseComparison(context, time, distanceCovered, logger);

    logger.info("The hare is taking a nap at: " + time.toIso8601String());
    logger.info("The hare has run " + distanceCovered.getValue() + " " + distanceCovered.getRefUnit().getName() + "s.");
  6. Add the following lines to the stopNapping method:
    ContextLogger logger = context.getContextLogger();
    logger.info("The hare woke up from the nap at: " + time.toIso8601String());
    HelperCode.logHareAndTortoiseComparison(context, time, distanceCovered, logger);
  7. Install and execute the simulation.

Now when you execute the simulation, you will see your custom log messages that Behavior Execution Engine sends to the console window, telling you the exact times the racers start and stop racing, how far they have progressed at each stoppage, and how far the hare is away from the tortoise.

You have now successfully adjusted the previous iteration of the race simulation to use delegates and then modified those delegates to provide custom functionality to meet your needs.


Next Tutorial >