Auto-Implementation

Behavior Execution Engine's auto-implementation system enables you to provide custom Java code to implement behaviors and interactions with external tools only where you need to. This frees you from the burden of having to provide code for every single block, property, and operation in your simulation.

If you do not provide a Java implementation for a block, Behavior Execution Engine will auto-generate an implementation for that block, including its properties and operations, in memory at runtime. It uses public get methods for properties, and returns the appropriate Property type. Operations with return types will return the default value for those return types (null for most types). Operations with no return type will be generated with an empty body.

If you do provide a Java implementation for a block, you can choose which properties and operations you want to implement without having to provide code for the others. You can do this by using abstract classes as discussed below.

Interfaces

Annotating an interface with @AutoDelegateImplementation will instruct Behavior Execution Engine to generate a concrete implementation for it in memory at runtime. This is useful at development time when you want to reference a block in your IDE for which you do not have a corresponding Java delegate.

Figure 1 shows the Passenger interface from the hot air balloon ride example. For this example, assume there are no concrete implementations of Passenger. Since Passenger is annotated with @AutoDelegateImplementation, Behavior Execution Engine will auto-generate a concrete implementation for it in memory at runtime.

package com.agi.moxie.samples.balloonride.structure;

import com.agi.moxie.api.annotations.DelegateFor;
import com.agi.moxie.api.models.Property;
import com.agi.moxie.delegates.base.FixedDuration;
import com.agi.moxie.delegates.base.Iso8601TimeInstant;
import java.lang.Boolean;

@AutoDelegateImplementation
@DelegateFor("Structure::Passenger")
public interface Passenger extends SpatialEntity {

    Property<Iso8601TimeInstant> appointmentTimeProperty();

    Property<FixedDuration> photoDurationProperty();

    Property<Boolean> isSatisfiedWithPhotoProperty();

    Property<Boolean> takingPhotoProperty();

    void embark(SpatialEntity vehicle);

    void disembark();

    void takePhoto();
}

Figure 1: The Passenger interface with auto-implementation

Partially implemented abstract classes

You may encounter situations where you want to provide a custom Java implementation for a specific block property or operation, but not all of them. This is referred to in Behavior Execution Engine as partial implementation, and you do this using abstract classes. When you annotate an abstract class with @AutoDelegateImplementation, you can provide custom implementations for the properties and operations you choose and just specify the signatures for the rest. This is because Behavior Execution Engine generates in-memory default implementations at runtime for every property and operation you did not implement yourself.

When you annotate an abstract class with @AutoDelegateImplementation, Behavior Execution Engine auto-generates concrete implementations in memory at runtime for abstract methods, including abstract Property methods. Behavior Execution Engine generates backing fields for properties, which support change tracking, using BasicProperty by default. Support for call events is also included with the auto-generated implementations for block operations.

Concrete implementations provided by you remain as is, with one exception. Behavior Execution Engine overrides your custom implementation for operations with logic to support call events, followed by a callback into your custom implementation.

Since Behavior Execution Engine overrides your custom implementations with callbacks into them, you cannot use final methods. Otherwise, an exception will be thrown at runtime.

All auto-implemented properties are guaranteed to be configured and available prior to the evaluation of any other properties or operations in the simulation. However, when implementing an injection constructor in your abstract class, it will get called before all of the auto-implemented properties are initialized. So, while you can accept @InjectBySlot values to obtain property input values from the SysML instance specifications, the auto-implemented properties will not be fully configured until all delegate instances are initialized. If you need to access an instance of a com.agi.moxie.api.models.Property from inside a constructor, you must create and initialize your own manually implemented backing field.

Figure 2 shows the partially implemented BalloonDelegate abstract class from the hot air balloon ride example.

package com.agi.moxie.samples.balloonride.structure.impl;

import com.agi.moxie.api.MoxieTime;
import com.agi.moxie.api.TimeProvider;
import com.agi.moxie.api.annotations.AutoDelegateImplementation;
import com.agi.moxie.api.annotations.DefaultDelegate;
import com.agi.moxie.api.annotations.InjectByName;
import com.agi.moxie.api.models.BasicProperty;
import com.agi.moxie.api.models.Property;
import com.agi.moxie.delegates.core.DynamicScalarValue;
import com.agi.moxie.delegates.core.ScalarValue;
import com.agi.moxie.samples.balloonride.structure.Balloon;
import com.agi.moxie.samples.balloonride.structure.Passenger;

import java.util.Collection;
import java.util.function.Function;

@AutoDelegateImplementation
@DefaultDelegate
public abstract class BalloonDelegate implements Balloon {

    private final TimeProvider mTimeProvider;
    private final BasicProperty<ScalarValue> mAltitude;
    private final Function<MoxieTime, Double> mStationaryFunction = (MoxieTime time) -> 0D;

    // Injection constructor
    public BalloonDelegate(@InjectByName("timeProvider") TimeProvider timeProvider) {
        mTimeProvider = timeProvider;
        mAltitude = new BasicProperty<>(timeProvider,
        new DynamicScalarValue(mTimeProvider, mStationaryFunction, mStationaryFunction));
    }

    @Override
    public Property<ScalarValue> altitudeProperty() {
        return mAltitude;
    }

    public void turnBurnerOn() {
        ScalarValue newAltitudeModel = adjustAltitudeModel(climbAccelerationProperty().getValue());
        mAltitude.setValue(newAltitudeModel);
    }

    public void turnBurnerOff() {
        ScalarValue newAltitudeModel = adjustAltitudeModel(-climbAccelerationProperty().getValue());
        mAltitude.setValue(newAltitudeModel);
    }

    @Override
    public void tieDown() {
        mAltitude.setValue(new DynamicScalarValue(mTimeProvider, mStationaryFunction, mStationaryFunction));
    }

    // This will have an implementation provided automatically if not overridden in a derived class.
    public abstract Collection<Passenger> passengersProperty();

    /**
     * Provides a new {@link DynamicScalarValue} with the given acceleration.
     *
     * @param climbAcceleration the acceleration to update the physics to use.
     * @return the new {@link DynamicScalarValue} that reflects the given acceleration
     */
    private DynamicScalarValue adjustAltitudeModel(Double climbAcceleration) {
        DynamicScalarValue dist = (DynamicScalarValue) mAltitude.getValue();
        MoxieTime currentTime = mTimeProvider.getCurrentTime();
        double mInitialAltitude = dist.getValueAt(currentTime);
        double mInitialAltitudeRate = dist.getRateAt(currentTime);
        DynamicScalarValue newDistance = new DynamicScalarValue(mTimeProvider, (MoxieTime time) -> {
            // Note that we are closing over 'currentTime' here, capturing the change in dynamics starting at the current event time.
            double t = currentTime.getDurationTo(time).getTotalSeconds();
            // The following is an extremely simplified model of the physics for a simple demonstration.
            // Ideally, this code would call into a more complex model over time as the system engineering lifecycle progresses.
            // Eventually, this could call into an external tool that would be executing the digital twin to analyze the full
            // heating and buoyancy profile for the balloon sub-system in accordance to the characteristics of the burner sub-system.
            // For now, we simply model a fixed acceleration modeling the buoyancy, air resistance, and other forces.
            return mInitialAltitude + mInitialAltitudeRate * t + 0.5 * climbAcceleration * t * t;
        }, (MoxieTime time) -> {
            double t = currentTime.getDurationTo(time).getTotalSeconds();
            return mInitialAltitudeRate + climbAcceleration * t;
        });
        return newDistance;
    }

}

Figure 2: The BalloonDelegate partially implemented abstract class

Partially implemented concrete classes

As with interfaces and abstract classes, when you add the @AutoDelegateImplementation annotation to a concrete class, Behavior Execution Engine generates the missing block properties and operations that do not have corresponding Java properties or methods at runtime. Behavior Execution Engine supports call events by default in the same way as abstract classes, so you cannot use final methods corresponding to block operations.

Differing signatures

Since Behavior Execution Engine implements missing properties and methods for you, any mistakes you make in the signatures of the properties and methods you do write will cause them to be ignored by Behavior Execution Engine at runtime. If you encounter cases where your code is not being called, be sure to check the spelling, casing, and type information to ensure it matches the model definition.

When using partial implementation, @InjectBySlot and @InjectByName are both allowed. However, the wire-up process will operate on concretely, partially, and automatically implemented properties. Furthermore, the order in which Behavior Execution Engine does the wire-up is not guaranteed. However, all properties with slot values are guaranteed to be set prior to executing any state machines.