Simulation sequences
The Simantics Sequences framework (Simantics/Sequences, exposed in Apros as
Apros/Sequences) allows discrete-event control logic to be defined in SCL and executed
during a running simulation. Sequences share simulation time and data with the solver but
do not block the solver thread.
Design properties
Sequences have four key properties:
- Lightweight — thousands of parallel sequences can run simultaneously without
significant overhead.
- Deterministic — parallel sequences initiated from the same root sequence execute in
a deterministic order (unless explicit random number generation is used).
- Non-exclusive — sequences do not reserve the simulation thread; they interleave with
the solver.
- Restricted — sequences interact with simulator state (variable values) only; model
configuration (database operations) cannot be performed from within a sequence.
Sequence monad
data Sequence a (Simantics/Sequences)
Sequence a is a plan of some operations that may happen during the simulation and may take some
(simulation) time. A sequence is initiated at a specific time and it may either finish at a specific
time or operate forever. If it completes, it retuns a value of type a.
Sequence :: ((a -> <Action,Proc> ()) -> <Action,Proc> ()) -> Sequence a (Simantics/Sequences)
We call the sequence instantaneous, if its duration is zero, i.e, the sequence finishes
immediately after starting.
A cooking recipe is an example of a sequence in the real world. Its return value could be
for example the success indication of the cooking process.
instance Monad Sequence
In order to build complex sequences from simple primitives, the sequences implement
Monad operations and
its laws. These are:
return :: Monad a => b -> a b (Prelude)
Inject a value into the monadic type.
(>>=) :: Monad a => a b -> (b -> a c) -> a c (Prelude)
Sequentially compose two actions, passing any value produced by the first as an argument to the second.
The sequence return v has zero duration, does not modify the simulator state, and
returns v. The sequence seqA >>= f first behaves like seqA; when it completes and
returns a value resultA, it continues as f resultA. In other words, >>= concatenates
two sequences and the second sequence may depend on the return value of the first.
No execution occurs when a sequence value is constructed. Execution begins only when
runSequence or registerSequence is called.
(>>) :: Monad a => a b -> a c -> a c (Prelude)
Sequentially compose two actions, discarding any value produced by the first, like sequencing operators
(such as the semicolon) in imperative languages."
fmap :: Functor a => (b -> c) -> a b -> a c (Prelude)
Lifts a pure function to the given functor.
join :: Monad a => a (a b) -> a b (Prelude)
The join function is the conventional monad join operator. It removes one level of monadic
structure.
For lists, join concatenates a list of lists:
join [[1,2], [3,4]] = [1, 2, 3, 4]
sequence :: FunctorM a => Monad b => a (b c) -> b (a c) (Prelude)
Evaluate each action in the sequence from left to right, and collect the results.
repeatForever :: Monad a => a b -> a c (Prelude)
Sequences the given monadic value infinitely:
repeatForever m = m >> m >> m >> ...
These operations are derived from the primitive monad operations.
The sequence seqA >> seqB behaves first like seqA and when it has finished it
continues like seqB. The sequence fmap f seq maps the result of the sequence seq by the function
f. The sequence join seq first behaves like the sequence seq and then like the sequence seq returned.
The sequence sequence seqs executes every sequence in the container seqs sequentially. The container can be for example list or Maybe. The sequence repeatForever seq repeats the sequence seq forever, never returning.
replicateM :: Monad a => Integer -> a b -> a [b] (Prelude)
replicateM n seq runs a sequence n times and collects all results into a list.
Actions
effect Action
<Action> a is an instantaneous operation happening in the simulator and returning a
value of type a. It can be a pure reading operation, but may also modify the simulator
state. The duration of an action is always zero.
Variable references use Apros syntax: "MODULE_NAME#ATTRIBUTE_NAME".
time :: <Action> Double (Simantics/Sequences)
Gives the current simulation time.
getVar :: Serializable a => String -> <Action> a (Simantics/Sequences)
Returns the current value of a variable
setVar :: Serializable a => String -> a -> <Action> () (Simantics/Sequences)
Sets the value of a variable
execute :: <Action,Proc> a -> Sequence a (Simantics/Sequences)
The sequence execute action is an instantious sequence that executes the operation action in the simulator.
Multiple actions happening at the same time may be written either as separate sequences:
mdo execute (setVar "SP1#SP_VALUE" 13)
execute (setVar "SP2#SP_VALUE" 14)
or as one sequence with a more complex action:
execute do
setVar "SP1#SP_VALUE" 13
setVar "SP2#SP_VALUE" 14
Controlling time
waitStep :: Sequence () (Simantics/Sequences)
The sequence waitStep waits that the simulator takes one simulation step.
It is a primitive mechanism that can be used to implement other events by
inspecting the simulator state after each time step.
waitUntil :: Double -> Sequence () (Simantics/Sequences)
The sequence waitUntil time waits until the simulation time is at least the given time.
wait :: Double -> Sequence () (Simantics/Sequences)
The sequence wait duration waits that duration seconds elapses from the current simulation time.
waitCondition :: <Action,Proc> Boolean -> Sequence () (Simantics/Sequences)
The sequence waitCondition condition waits until the condition is satisfied.
Parallel execution
fork :: Sequence a -> Sequence () (Simantics/Sequences)
The sequence fork seq is an instantious sequence that creates a new sequence thread behaving like the sequence seq.
halt :: Sequence a (Simantics/Sequences)
The sequence halt ends the current sequence thread and the sequence .
stop :: Sequence a (Simantics/Sequences)
The sequence stop stops all sequence threads, stopping the simulation completely.
fork seq starts seq in parallel with the current sequence. The forked sequence has
priority at the same time step: its actions execute before those of the forking
sequence at the same simulation instant.
halt stops the current sequence only. Other parallel sequences continue.
stop stops all sequences initiated from the same root sequence. This is the
mechanism for signalling a terminal error condition from any thread.
Running sequences
runSequence seq — registers the sequence and starts the simulation if it is not
already running. Blocks until all threads have halted or stop is called. Returns
Maybe a where a is the return value of the root sequence.
import "Apros/Sequences"
runSequence mdo
fork $ repeatForever mdo
waitCondition (getVar "TA01#TA11_LIQ_LEVEL" >= 3.0)
execute (setVar "BP01#PU11_SPEED_SET_POINT" 0.0)
wait 1
fork $ repeatForever mdo
waitCondition (getVar "TA01#TA11_LIQ_LEVEL" <= 2.0)
execute (setVar "BP01#PU11_SPEED_SET_POINT" 100.0)
wait 1
registerSequence seq — registers the sequence in the current experiment without
controlling simulation start or stop. Returns an ActionContext that can be used to
remove the sequence later. Use this when the simulation is already running and you want to
add a new control thread dynamically.
stopActionContext ctx — removes a previously registered sequence from the experiment
without stopping the simulation. Use the ActionContext returned by registerSequence.
Semantics
Although simulation sequences support threading, the semantics is deterministic. This is
ensured by the following equivalences:
halt >> seqA = halt
stop >> seqA = stop
fork (execute actionA >> seqA) >> seqB = execute actionA >> fork seqA >> seqB
fork (waitStep >> seqA) >> execute actionB >> seqB = execute actionB >> fork seqA >> seqB
fork (waitStep >> seqA) >> waitStep >> seqB = waitStep >> fork seqA >> seqB
fork halt >> seqB = seqB
fork seqA >> halt = seqA
fork stop >> seqB = stop
fork (waitStep >> seqA) >> stop = stop
Examples
Check that pressure of a point stays below a certain value:
fork mdo
waitCondition (getVar "POINT1#PO11_PRESSURE" > 120.0)
execute (print "Error! Error!")
stop
Check that the valve is closed 10 seconds after the operator presses a button:
fork $ repeatForever mdo
waitCondition (getVar "BUTTON#BINARY_VALUE")
fork mdo
wait 10
valvePos <- execute (getVar "VALVE#VA11_POSITION")
if valvePos == 0
then return () // OK
else mdo
execute (print "Error! Error!")
stop
Custom combinators
Sequences are first-class values, so reusable control patterns can be factored into
functions. For example:
forkAndJoin runs two sequences in parallel and waits for both to complete, returning
both results as a tuple:
forkAndJoin :: Sequence a -> Sequence b -> Sequence (a, b)
forkAndJoin seqA seqB = mdo
// Implementation uses fork and shared references
...
printElapsedTime wraps any sequence to print its elapsed simulation time on
completion:
printElapsedTime :: Sequence a -> Sequence a
printElapsedTime seq = mdo
t0 <- execute time
res <- seq
t1 <- execute time
execute (print "Elapsed: \(t1 - t0) s")
return res
Sequences vs SCL scripts
|
Scripts |
Sequences |
| Execution model |
Sequential, single thread |
Parallel lightweight threads |
| Simulation control |
Can start/stop/configure |
Interacts with running state only |
| DB access |
Full <ReadGraph>/<WriteGraph> |
Not available |
| Use case |
Model building, batch setup |
Runtime control logic, discrete events |
|