Auto-Implementation
Moxie'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,
Moxie
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 the
Moxie 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.
Moxie
provides a utility to generate the Java code for these interfaces for you. The
delegate code generator
saves you development time and reduces human error by ensuring all of the block's properties and operations are correctly spelled, typed, and
annotated. It also provides a mechanism for keeping your delegate property and method signatures in sync with your model when it changes. Simply
generate code for the interface again and overwrite the previous version in your development project.
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
, the
Moxie 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
Moxie
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
Moxie
generates
in-memory default implementations at runtime for every property and operation you did not implement yourself. Similarly
to interfaces, Moxie's
delegate code generator
will generate these abstract classes for you. This way, you can just fill in the custom code where you need it, and nothing more.
When you annotate an abstract class with @AutoDelegateImplementation
,
Moxie
auto-generates concrete implementations in
memory at runtime for abstract methods, including abstract Property
methods.
Moxie
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. Moxie overrides your custom implementation for operations with logic to support call events, followed by a callback into your custom implementation.
Since
Moxie
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,
Moxie
generates the missing block properties and operations that do not have corresponding Java properties or methods at runtime.
Moxie
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 Moxie 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 Moxie 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. You can also prevent these mistakes by using the delegate code generator to generate code for the delegates and their property and operation signatures for you.
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 the
Moxie 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.