115 | | In MUSCLE, 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, with parameter "submodelName:T". Restart submodel returns false by default, but could depend on whether one of the conduits will receive more messages. |
| 115 | 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. |
| 116 | |
| 117 | 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: |
| 118 | {{{ |
| 119 | int paramA; |
| 120 | int[] geometry; |
| 121 | |
| 122 | @Override |
| 123 | protected Timestamp init(Timestamp prevOrigin) { |
| 124 | paramA = getIntProperty("paramName"); |
| 125 | Observation<int[]> initialGeometry = in("geometry").receiveObservation(); |
| 126 | geometry = initialGeometry.getData(); |
| 127 | return initialGeometry.getTimestamp(); |
| 128 | } |
| 129 | }}} |
| 130 | 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. |
| 131 | |
| 132 | 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. |
| 133 | |
| 134 | {{{ |
| 135 | @Override |
| 136 | protected void intermediateObservation() { |
| 137 | double[] dens = calculateDensity(geometry); |
| 138 | out("density").send(dens); |
| 139 | } |
| 140 | |
| 141 | @Override |
| 142 | protected void solvingStep() { |
| 143 | int[] changedLocations = (int[]) in("update").receive(); |
| 144 | geometry = updateGeometry(changedLocations); |
| 145 | } |
| 146 | }}} |
| 147 | |
| 148 | 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. |
| 149 | |
| 150 | {{{ |
| 151 | @Override |
| 152 | protected void finalObservation() { |
| 153 | out("finalGeometry").send(geometry); |
| 154 | } |
| 155 | }}} |
| 156 | |
| 157 | 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. |
| 158 | |
| 159 | === Mappers === |
| 160 | |
| 161 | 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 |
| 162 | |
| 163 | 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 |
| 164 | {{{ |
| 165 | init() |
| 166 | while (continueComputation()) { |
| 167 | receiveAll() |
| 168 | sendAll() |
| 169 | } |
| 170 | }}} |
| 171 | |
| 172 | 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. |
| 173 | |
| 174 | {{{ |
| 175 | Observation<int[]> input; |
| 176 | |
| 177 | @Override |
| 178 | protected void receiveAll() { |
| 179 | input = in("geometry").receiveObservation(); |
| 180 | } |
| 181 | |
| 182 | @Override |
| 183 | protected void sendAll() { |
| 184 | double[] geomDouble = convertToDouble(input.getData()); |
| 185 | Observation<double[]> geomDoubleObs = input.copyWithData(geomDouble); |
| 186 | out("geometryDouble").send(geomDoubleObs); |
| 187 | |
| 188 | out("geometryInt").send(input); |
| 189 | } |
| 190 | }}} |
| 191 | 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. |
| 192 | |
| 193 | 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. |
| 194 | |
| 195 | === Filters === |
| 196 | |
| 197 | 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 |
| 198 | {{{ |
| 199 | public class MultiplicationFilter extends muscle.core.conduit.filter.AbstractObservationFilter<double[], double[]> { |
| 200 | double factor; |
| 201 | |
| 202 | public MultiplicationFilter(double factor) { |
| 203 | this.factor = factor; |
| 204 | } |
| 205 | |
| 206 | @Override |
| 207 | public void apply(Observation<double[]> obs) { |
| 208 | double[] data = obs.getData(); |
| 209 | |
| 210 | // perform multiplication |
| 211 | for (int i = 0; i < data.length; i++) { |
| 212 | data[i] *= factor; |
| 213 | } |
| 214 | |
| 215 | // Create a new observable to send |
| 216 | Observation<double[]> multObs = obs.copyWithData(data); |
| 217 | put(multObs); |
| 218 | } |
| 219 | } |
| 220 | }}} |
| 221 | |
| 222 | In the {{{muscle.core.conduit.filter}}} are some examples of filters which are ready to use. |
| 223 | |
| 224 | === Terminals === |
| 225 | |
| 226 | |