This page is showing examples in the target language CC++PythonTypeScriptRust. You can change the target language in the left sidebar.
Logical Time
A key property of Lingua Franca is logical time. All events occur at an instant in logical time. By default, the runtime system does its best to align logical time with physical time, which is some measurement of time on the execution platform. The lag is defined to be physical time minus logical time, and the goal of the runtime system is maintain a small non-negative lag.
The lag is allowed to go negative only if the fast
target property or the --fast
command-line argument is set to true
. In that case, the program will execute as fast as possible with no regard to physical time.
In Lingua Franca, time is a data type.
A parameter, state variable, port, or action may have type time.
In the C target, time values internally have type instant_t
or interval_t
,
both of which are (usually) equivalent to the C type long long
.
In the C++ target, time values internally have the type std::chrono::nanoseconds
. For details, see the Target Language Details.
In the Rust target, time values internally have type FIXME.
Time Values
A time value is given with units (unless the value is 0, in which case the units can be omitted). The allowable units are:
- For nanoseconds:
ns
,nsec
, ornsecs
- For microseconds:
us
,usec
, orusecs
- For milliseconds:
ms
,msec
, ormsecs
- For seconds:
s
,sec
,secs
,second
, orseconds
- For minutes:
min
,minute
,mins
, orminutes
- For hours:
h
,hour
, orhours
- For days:
d
,day
, ordays
- For weeks:
week
orweeks
The following example illustrates using time values for parameters and state variables:
main reactor SlowingClock(start: time = 100 ms, incr: time = 100 ms) { state interval:= start logical action a reaction(startup) -> a reaction(a) -> a }
main reactor SlowingClock(start: time(100 ms), incr: time(100 ms)) { state interval: logical action a reaction(startup) -> a reaction(a) -> a }
main reactor SlowingClock(start = 100 ms, incr = 100 ms) { state interval = start logical action a reaction(startup) -> a reaction(a) -> a }
main reactor SlowingClock(start: time = 100 ms, incr: time = 100 ms) { state interval: time = start logical action a reaction(startup) -> a reaction(a) -> a }
main reactor SlowingClock(start: time = 100 ms, incr: time = 100 ms) { state start = start state incr = incr state interval: time = start state expected_time: time() logical action a reaction(startup) -> a reaction(a) -> a }
This has two time parameters, start
and incr
, each with default value 100 ms
and type time. This parameter is used to initialize the interval
state variable, which also stores a time. The logical action a
, explained in Actions, is used to schedule events to occur at time start
after program startup and then at intervals that are increased each time by incr
. The result of executing this program will look like this:
Logical time since start: 100000000 nsec. Logical time since start: 300000000 nsec. Logical time since start: 600000000 nsec. Logical time since start: 1000000000 nsec. ...
Timers
The simplest use of logical time in Lingua Franca is to invoke a reaction periodically. This is done by first declaring a timer using this syntax:
timer <name>(<offset>, <period>)
The <period>
, which is optional, specifies the time interval between timer events. The <offset>
, which is also optional, specifies the (logical) time interval between when the program starts executing and the first timer event. If no period is given, then the timer event occurs only once. If neither an offset nor a period is specified, then one timer event occurs at program start, simultaneous with the startup event.
The period and offset are given by a number and a units, for example, 10 ms
. See the expressions documentation for allowable units. Consider the following example:
main reactor Timer { timer t(0, 1 sec) reaction(t) }
main reactor Timer { timer t(0, 1 s) reaction(t) }
main reactor Timer { timer t(0, 1 sec) reaction(t) }
main reactor Timer { timer t(0, 1 sec) reaction(t) }
main reactor Timer { timer t(0, 1 sec) reaction(t) }
This specifies a timer named t
that will first trigger at the start of execution and then repeatedly trigger at intervals of one second. Notice that the time units can be left off if the value is zero.
This target provides a built-in function for retrieving the logical time at which the reaction is invoked,
get_logical_time()
FIXME
lf.time.logical()
util.getCurrentLogicalTime()
FIXME.
On most platforms (with the exception of some embedded platforms), the returned value is a 64-bit number representing the number of nanoseconds that have elapsed since January 1, 1970. Executing the above displays something like the following:
Logical time is 1648402121312985000. Logical time is 1648402122312985000. Logical time is 1648402123312985000. ...
The output lines appear at one second intervals unless the fast
option has been specified.
Elapsed Time
The times above are a bit hard to read, so, for convenience, each target provides a built-in function to retrieve the elapsed time. For example:
main reactor TimeElapsed { timer t(0, 1 s) reaction(t) }
main reactor TimeElapsed { timer t(0, 1 s) reaction(t) }
main reactor TimeElapsed { timer t(0, 1 s) reaction(t) }
main reactor TimeElapsed { timer t(0, 1 s) reaction(t) }
main reactor TimeElapsed { timer t(0, 1 s) reaction(t) }
See the Target Language Details for the full set of functions provided for accessing time values.
Executing this program will produce something like this:
Elapsed logical time is 0. Elapsed logical time is 1000000000. Elapsed logical time is 2000000000. ...
Comparing Logical and Physical Times
The following program compares logical and physical times:
main reactor TimeLag { timer t(0, 1 s) reaction(t) }
main reactor TimeLag { timer t(0, 1 s) reaction(t) }
main reactor TimeLag { timer t(0, 1 s) reaction(t) }
main reactor TimeLag { timer t(0, 1 s) reaction(t) }
main reactor TimeLag { timer t(0, 1 s) reaction(t) }
Execution will show something like this:
Elapsed logical time: 0, physical time: 855000, lag: 855000 Elapsed logical time: 1000000000, physical time: 1004714000, lag: 4714000 Elapsed logical time: 2000000000, physical time: 2004663000, lag: 4663000 Elapsed logical time: 3000000000, physical time: 3000210000, lag: 210000 ...
In this case, the lag varies from a few hundred microseconds to a small number of milliseconds. The amount of lag will depend on the execution platform.
Simultaneity and Instantaneity
If two timers have the same offset and period, then their events are logically simultaneous. No observer will be able to see that one timer has triggered and the other has not.
A reaction is always invoked at a well-defined logical time, and logical time does not advance during its execution. Any output produced by the reaction will be logically simultaneous with the input. In other words, reactions are logically instantaneous (for an exception, see Logical Execution Time). Physical time, however, does elapse during execution of a reaction.
Timeout
By default, a Lingua Franca program will terminate when there are no more events to process. If there is a timer with a non-zero period, then there will always be more events to process, so the default execution will be unbounded. To specify a finite execution horizon, you can either specify a timeout
target property or a --timeout
command-line option. For example, the following timeout
property will cause the above timer with a period of one second to terminate after 11 events:
Startup and Shutdown
To cause a reaction to be invoked at the start of execution, a special startup trigger is provided:
reactor Foo { reaction(startup) {= ... perform initialization ... =} }
The startup trigger is equivalent to a timer with no offset or period.
To cause a reaction to be invoked at the end of execution, a special shutdown trigger is provided. Consider the following reactor, commonly used to build regression tests:
reactor TestCount(start: int = 0, stride: int = 1, num_inputs:= 1) { state count:= start state inputs_received:= 0 input x: reaction(x) reaction(shutdown) }
reactor TestCount(start: int = 0, stride: int = 1, num_inputs:= 1) { state count:= start state inputs_received:= 0 input x: reaction(x) reaction(shutdown) }
reactor TestCount(start=0, stride=1, num_inputs=1) { state count = start state inputs_received = 0 input x reaction(x) reaction(shutdown) }
reactor TestCount(start: number = 0, stride: number = 1, numInputs: number = 1) { state count: number = start state inputsReceived: number = 0 input x: number reaction(x) reaction(shutdown) }
reactor TestCount(start: u32 = 0, stride: u32 = 1, num_inputs: u32 = 1) { state stride = stride state num_inputs = num_inputs state count: u32 = start state inputs_received: u32 = 0 input x: u32 reaction(x) reaction(shutdown) }
This reactor tests its inputs against expected values, which are expected to start with the value given by the start
parameter and increase by stride
with each successive input. It expects to receive a total of num_inputs
input events. It checks the total number of inputs received in its shutdown reaction.
The shutdown trigger typically occurs at microstep 0, but may occur at a larger microstep. See Superdense Time and Termination.