Version 4 (modified by jorisborgdorff, 13 years ago) (diff)

--

About the API

There are two ways to exploit the MUSCLE API: either by using these defined by the Multiscale Modeling Language, or by writing free-form code, where all orchestration and deadlock detection is your own responsibility. The former gives code that is easy to read and to debug, while the latter makes it possible to optimally exploit any parallelism in the model. This page section is split in two parts, one for each approach. With care, it is also possible to combine these approaches in a model, choosing the more appropriate form per submodel.

The computational elements that the Multiscale Modeling Language (MML) defines are:

  • the conduit, for sending messages;
  • submodel, for modeling phenomena at certain scales;
  • mapper, for dimensionless mappings of data;
  • filter, for time and datatype filtering; and
  • terminal, for terminating empty conduits.

These elements are all supported in the MUSCLE API. Of these elements, only submodels have the explicit concept of scale. In MUSCLE this is mapped to one temporal scale and multiple spatial scales. The temporal scale is used to determine the simulation time at the current iteration, see the configuration documentation? on how to set the scales. Conduits and parameters are used the same in MML-based or free-form code.

Conduits

The conduit is the mechanism in the MUSCLE runtime environment to send data. In the API, the ConduitEntrance? and ConduitExit? are accessible. The ConduitEntrance? is used to send data, while the ConduitExit? receives data. These entrances and exits, as we call them, can be accessed in two ways. Either they are created in the addPortals() method, or they are called on the fly in the code using the out() and in() methods.

The addPortals method is called before the submodel starts, so the exits and entrances are stored as fields of your class. The conduit exit and entrance uses Java Generics to define what kind of data will be received or sent, both in the field declaration and in the addExit or addEntrance method. This allows for compile-time and run-time checking of data that is sent over the conduit.

ConduitExit<double[]> exitA;
ConduitEntrance<double[]> entranceA;

@Override
protected void addPortals() {
    exitA = addExit("exitName", double[].class);
    entranceA = addEntrance("entranceName", double[].class);
}

To send or receive, use the fields that were initialized in addPortals().

double[] dataA = exitA.receive();
entranceA.send(dataA);

If addPortals() is not overridden it is possible to use the out() and in() methods to send or receive data. It is then not necessary to store the ConduitExit? and ConduitEntrance? as a field.

double[] dataA = (double[]) in("exitName").receive();
out("entranceName").send(dataA);

In this case received data needs to be cast to double[] to use it, and in the send statement, the Java compiler will give a warning that an unchecked conversion is being done. If the cast is not correct, a ClassCastException? will be thrown by Java. If the exit or entrance name is not configured, an IllegalArgumentException? will be thrown by MUSCLE.

The advantage of the first method is that it is type-safe, so there is no need to cast the data. Also if the code and the configuration file do not match this is detected immediately. The advantage of the second method is that it is less verbose and leads to smaller classes.

A ConduitExit? is used for receiving in a blocking mode. There are two receive methods: receive() and receiveObservation(). In the first case only the data is received. In the second, an observation is received, which contains the data but also the timestamp at which the data was sent, and the timestamp at which the next message will arrive. So,

double[] dataA = exitA.receive();

or

Observation<double[]> obsA = exitA.receiveObservation();
double[] dataA = obsA.getData();
Timestamp time = obsA.getTimestamp();

If receive is called and the submodel on the other end has stopped, a MUSCLEConduitExhaustedException is thrown. To prevent this, it is possible to first call exitA.hasNext(), which is also blocking but returns a boolean.

If a lot of conduit exits are used and the order in which they are received is not important, it is possible to loop over multiple conduits and call the ready() method. This is non-blocking, and it will return true when hasNext() will return an answer without blocking.

A ConduitEntrance? has several different send functions. The most basic send(data) sends the data and deduces the timestamp at which the data is sent, by adding delta T of the timescale to the previous timestamp. If the instance is dimensionless, or if required, you can explicitly set the timestamp of the data, and the timestamp at which you send the next message with send(data, time, nextTime):

Timestamp time = Timestamp.ZERO;
Timestamp nextTime = new Timestamp(2.0);
entranceA.send(data, time, nextTime);

Finally, a muscle.core.model.Observation object can also be sent and it contains the same information: data, a time and the next time.

Observation<double[]> obs = new Observation<double[]>(data, time, nextTime);
entranceA.send(obs);

Although the entrance is automatically closed once the instance is finished, it is also possible to close the conduit earlier with the close() method, if it is not going to be used again.

Parameters

Parameters that are defined in the configuration file are accessed through a range of get*Property() methods. The most basic form is

String parameter = getProperty("propName");

In this form the property is read as a string. It first tries to find the instance property by searching for "instanceName:propName" and then for a global property named "propName". All get*Property() methods will throw an IllegalArgumentException? if the property does not exist, this can be prevented by first calling hasProperty(). If a instance property is specifically required, this can be checked by first calling

String parameter = null;
if (hasInstanceProperty("propName")) {
    parameter = getProperty("propName");
}

If on the other hand only a global property is wanted, write

String parameter = getGlobalProperty("propName");

To get other types of parameters than strings, use getPathProperty for a File, getIntProperty for an int, getDoubleProperty for a double and getBoolProperty for a boolean.

MUSCLE MML API

Submodels

In MML, submodels are governed by a Submodel Execution Loop. Interpreting this for MUSCLE, this looks like:

while (true) {
    state, timeOrigin = init(t0) // we are allowed to receive messages
    while not endCondition() {
        intermediateObservation() // we are allowed to send messages
        state = solvingStep() // we are allowed to receive messages
    }
    finalObservation() // we are allowed to send messages
    if  not restartSubmodel() {
        break
    }
}

Reading this code step by step, a submodel can be restarted any number of times depending on the couplings, this is what the outer loop does. Next, each time a submodel is started, it has an initialization phase where it determines the initial state and what the simulation time of this initial state should be. Then it enters a while loop while some end condition is not met. Each iteration it sends some observation of the state, and it computes the next state. When the end condition is met, it is possible to do some cleaning and to send a final observation. At the end of the submodel it decides whether it should restart.

In MUSCLE, a submodel is created by extending muscle.core.kernel.Submodel. endCondition is implemented as the willStop() method, which looks at all the messages sent and received and the message with the highest simulation time is compared with the total time in the timescale of the submodel. Restart submodel returns false by default, but could depend on whether one of the conduits will receive more messages.

The other methods are empty by default so to have a meaningful submodel they should be overridden. For example, if the model depends on some initial geometry which will be calculated by another submodel, the init() method could be implemented as such:

int paramA;
int[] geometry;

@Override
protected Timestamp init(Timestamp prevOrigin) {
    paramA = getIntProperty("paramName");
    Observation<int[]> initialGeometry = in("geometry").receiveObservation();
    geometry = initialGeometry.getData();
    return initialGeometry.getTimestamp();
}

If no message is received in the init() function, the best way to return is return super.init(prevOrigin);. This will take the previous origin and return 0 if it was null, and prevOrigin plus the total time of the timescale if not null. It is not allowed to send messages during the initialization.

After initialization the submodel continues by first calling intermediateObservation() and then solvingStep(). In intermediateObservation the model may send messages, in solvingStep it may only receive messages. Other than that they are regular functions. Although intermediateObservation is not necessarily overridden, solvingStep should be overridden: it should contain the core of the code.

@Override
protected void intermediateObservation() {
    double[] dens = calculateDensity(geometry);
    out("density").send(dens);
}

@Override
protected void solvingStep() {
    int[] changedLocations = (int[]) in("update").receive();
    geometry = updateGeometry(changedLocations);
}

Finally, when this loop has iterated as often as the timescale says it should, the method finalObservation is called. Here any clean-up can be performed, and final messages may be sent.

@Override
protected void finalObservation() {
    out("finalGeometry").send(geometry);
}

If restartSubmodel is overridden and returns true, then again init will be called, etc. If the state needs to be stored after restarting, this can be done by setting a field of the class.

Mappers

In MML, a mapper is a computational element that may have multiple in- and outbound ports and may perform any mapping on the data received. In principle it should be stateless, but in MUSCLE this is not enforced. Also, a mapper should first receive on all its ports and then send on all its ports. This is only partially enforced in MUSCLE. There are two specializations of the mapper: the fan-in mapper, which receives on multiple ports but sends on one; and the fan-out mapper which receives on a single port and

A mapper is created in MUSCLE by extending muscle.core.kernel.Mapper or its subclasses muscle.core.kernel.FanInMapper and muscle.core.kernel.FanOutMapper. The mapper has the following loop

init()
while (continueComputation()) {
    receiveAll()
    sendAll()
}

The default implementation of continueComputation() is to check whether all incoming ports will receive a next message, and returns false only if this is the case. If some of the ports are not mandatory, it is possible to override continueComputation(). In init any parameters may be read or initialization may be performed. In receiveAll() messages are received from the ports and in sendAll() messages are sent. The mapping may be performed in either of the two, whichever is convenient. Since the mapper is scaleless, the implementation should explicitly set the timestamps of the messages.

Observation<int[]> input;

@Override
protected void receiveAll() {
    input = in("geometry").receiveObservation();
}

@Override
protected void sendAll() {
    double[] geomDouble = convertToDouble(input.getData());
    Observation<double[]> geomDoubleObs = input.copyWithData(geomDouble);
    out("geometryDouble").send(geomDoubleObs);

    out("geometryInt").send(input);
}

In the code above a geometry is received, and one is passed on un-altered and the other converted to int[] with some function. The convenience method copyWithData is used to have the same timestamps as the original observation, but different data.

In the fan-out mapper, the receiveAll method is already defined, and the result of the single port is saved in the Observation value field. Conversely, in the fan-in mapper the implementation of receiveAll should store a single observation in the field Observation value, which the mapper will then send.

Filters

A conduit filter is like a mapper, but is only applied to a single conduit. The implementation is also more light-weight, and theoretically it is allowed to modify the timestamps of messages, or drop them all-together. To implement a filter, extend muscle.core.conduit.filter.AbstractObservationFilter. This uses generics to indicate what values it should convert between. It is allowed to define a constructor, this should then either be empty or take a single double argument. The only function that should be overridden is apply() and this should call put() as many times as it wants to send a message. A simple multiplication-filter would look as follows

public class MultiplicationFilter extends muscle.core.conduit.filter.AbstractObservationFilter<double[], double[]> {
    double factor;

    public MultiplicationFilter(double factor) {
        this.factor = factor;
    }

    @Override
    public void apply(Observation<double[]> obs) {
        double[] data = obs.getData();

        // perform multiplication
        for (int i = 0; i < data.length; i++) {
            data[i] *= factor;
        }

        // Create a new observable to send
        Observation<double[]> multObs = obs.copyWithData(data);
        put(multObs);
    }
}

In the muscle.core.conduit.filter are some examples of filters which are ready to use.

Terminals

MUSCLE free-form API

In the free-form API all kernels extend muscle.core.kernel.Instance. At least the execute() method of Instance should be overridden. In this method any sequence of send and receive is possible, and the conceptual coupling between different submodels can be as small or large as wanted.

One notable difference with the MUSCLE MML API is that it can quit a submodel more directly. If there should still be some synchronization with other submodels, the willStop() method checks when the conduits have sent messages that are larger than the total timescale of the current submodel, or larger than the "max_timesteps" property. An example submodel Sender with name "w" (see src/java/examples/simplejava for the full code), which sends data each iteration would look as follows when using willStop():

import muscle.core.ConduitEntrance;
import muscle.core.kernel.Instance;

/**
a simple java example kernel which sends data
*/
public class Sender extends Instance {
	private ConduitEntrance<double[]> entrance;

	@Override
	protected void addPortals() {
		entrance = addEntrance("data", double[].class);
	}

	@Override
	protected void execute() {
		double[] dataA = new double[5];
		
		while (!this.willStop()) {
			// process data
			for(int i = 0; i < dataA.length; i++) {
				dataA[i] = i;
			}
						
			// send the data
			entrance.send(dataA);
		}
	}

}

By changing the "max_timesteps" and the "w:dt" property, this submodel will send different number of messages. For example, if max_timesteps is 4, and w:dt is 1 it will send 4 messages.