Skip to main content
Version: 0.6.0

Multiports and Banks

This article has examples in the following target languages:

Lingua Franca provides a compact syntax for ports that can send or receive over multiple channels and another syntax for multiple instances of a reactor class. These are respectively called multiports and banks of reactors.

Multiports​

To declare an input or output port to be a multiport, use the following syntax:

input[<width>] <name>:<type>; output[<width>] <name>:<type>;

where <width> is a positive integer. This can be given either as an integer literal or a parameter name. The width can also be given by target code enclosed in {=...=}. Consider the following example:

target C; reactor Source { output[4] out:int; reaction(startup) -> out {= for(int i = 0; i < out_width; i++) { lf_set(out[i], i); } =} } reactor Destination { input[4] in:int; reaction(in) {= int sum = 0; for (int i = 0; i < in_width; i++) { if (in[i]->is_present) sum += in[i]->value; } printf("Sum of received: %d.\n", sum); =} } main reactor { a = new Source(); b = new Destination(); a.out -> b.in; }
Lingua Franca diagram: MultiportMultiportSourceout[4]Destinationin[4]

Executing this program will yield:

Sum of received: 6.

The Source reactor has a four-way multiport output and the Destination reactor has a four-way multiport input. These channels are connected all at once on one line, the second line from the last. Notice that the generated diagram shows multiports with hollow triangles. Whether it shows the widths is controlled by an option in the diagram generator.

The Source reactor specifies out as an effect of its reaction using the syntax -> out. This brings into scope of the reaction body a way to access the width of the port and a way to write to each channel of the port.

note

In Destination, the reaction is triggered by in, not by some individual channel of the multiport input. Hence, it is important when using multiport inputs to test for presence of the input on each channel, as done above with the syntax:

if (in[i]->is_present) ...

An event on any one of the channels is sufficient to trigger the reaction.

In the Python target, multiports can be iterated on in a for loop (e.g., for p in out) or enumerated (e.g., for i, p in enumerate(out)) and the length of the multiport can be obtained by using the len() (e.g., len(out)) expression.

Sparse Inputs​

Sometimes, a program needs a wide multiport input, but when reactions are triggered by this input, few of the channels are present. In this case, it can be inefficient to iterate over all the channels to determine which are present. If you know that a multiport input will be sparse in this way, then you can provide a hint to the compiler and use a more efficient iterator to access the port. For example:

target C; reactor Sparse { @sparse input[100] in:int; reaction(in) {= // Create an iterator over the input channels. struct lf_multiport_iterator_t i = lf_multiport_iterator(in); // Get the least index of a channel with present inputs. int channel = lf_multiport_next(&i); // Iterate until no more channels have present inputs. while(channel >= 0) { printf("Received %d on channel %d\n", in[channel]->value, channel); // Get the next channel with a present input. channel = lf_multiport_next(&i); } =} }

Notice the @sparse annotation on the input declaration. This provides a hint to the compiler to optimize for sparse inputs. Then, instead of iterating over all input channels, this code uses the built-in function lf_multiport_iterator() to construct an iterator. The function lf_multiport_next() returns the first (and later, the next) channel index that is present. It returns -1 when no more channels have present inputs.

The multiport iterator can be used for any input multiport, even if it is not marked sparse. But if it is not marked sparse, then the lf_multiport_next() function will not optimize for sparse inputs and will simply iterate over the channels until it finds one that is present.

Parameterized Widths​

The width of a port may be given by a parameter. For example, the above Source reactor can be rewritten

reactor Source(width:int = 4) { output[width] out:int; reaction(startup) -> out {= ... =} }

Parameters to the main reactor can be overwritten on the command line interface when running the generated program. As a consequence, the scale of the application can be determined at run time rather than at compile time.

Connecting Reactors with Different Widths​

Assume that the Source and Destination reactors above both use a parameter width to specify the width of their ports. Then the following connection is valid:

main reactor { a1 = new Source(width = 3); a2 = new Source(width = 2); b = new Destination(width = 5); a1.out, a2.out -> b.in; }

The first three ports of b will received input from a1, and the last two ports will receive input from a2. Parallel composition can appear on either side of a connection. For example:

a1.out, a2.out -> b1.out, b2.out, b3.out;

If the total width on the left does not match the total width on the right, then a warning is issued. If the left side is wider than the right, then output data will be discarded. If the right side is wider than the left, then input channels will be absent.

Any given port can appear only once on the right side of the -> connection operator, so all connections to a multiport destination must be made in one single connection statement.

Banks of Reactors​

Using a similar notation, it is possible to create a bank of reactors. For example, we can create a bank of four instances of Source and four instances of Destination and connect them as follows:

main reactor { a = new[4] Source(); b = new[4] Destination(); a.out -> b.in; }
Lingua Franca diagram: BankToBankMultiportMultiportSource4out[4]Destination4in[4]

If the Source and Destination reactors have multiport inputs and outputs, as in the examples above, then a warning will be issued if the total width on the left does not match the total width on the right. For example, the following is balanced:

main reactor { a = new[3] Source(width = 4); b = new[4] Destination(width = 3); a.out -> b.in; }

There will be three instances of Source, each with an output of width four, and four instances of Destination, each with an input of width 3, for a total of 12 connections.

To distinguish the instances in a bank of reactors, the reactor can define a parameter called bank_index with any type that can be assigned a non-negative integer value (for example, int, size_t, or uint32_t). If such a parameter is defined for the reactor, then when the reactor is instantiated in a bank, each instance will be assigned a number between 0 and n-1, where n is the number of reactor instances in the bank. For example, the following source reactor increments the output it produces by the value of bank_index on each reaction to the timer:

target C reactor MultiportSource(bank_index: int = 0) { timer t(0, 200 msec) output out: int state s: int = 0 reaction(t) -> out {= lf_set(out, self->s); self->s += self->bank_index; =} }

The width of a bank may also be given by a parameter, as in

main reactor( source_bank_width:int = 3, destination_bank_width:int = 4 ) { a = new[source_bank_width] Source(width = 4); b = new[destination_bank_width] Destination(width = 3); a.out -> b.in; }

Initializing Bank Members from a Table​

It is often convenient to initialize parameters of bank members from a table. Here is an example:

target C; preamble {= int table[] = {4, 3, 2, 1}; =} reactor A(bank_index:int = 0, value:int = 0) { reaction (startup) {= printf("bank_index: %d, value: %d\n", self->bank_index, self->value); =} } main reactor { a = new[4] A(value = {= table[bank_index] =}); }

The global table defined in the preamble is used to initialize the value parameter of each bank member. The result of running this is something like:

bank_index: 0, value: 4
bank_index: 1, value: 3
bank_index: 2, value: 2
bank_index: 3, value: 1

Contained Banks​

Banks of reactors can be nested. For example, note the following program:

target C; reactor Child ( bank_index:int = 0 ) { reaction(startup) {= printf("My bank index: %d.\n", self->bank_index); =} } reactor Parent ( bank_index:int = 0 ) { c = new[2] Child(); } main reactor { p = new[2] Parent(); }
Lingua Franca diagram: ChildBankChildBankParentChild22

In this program, the Parent reactor contains a bank of Child reactor instances with a width of 2. In the main reactor, a bank of Parent reactors is instantiated with a width of 2, therefore, creating 4 Child instances in the program in total. The output of this program will be:

My bank index: 0.
My bank index: 1.
My bank index: 0.
My bank index: 1.

The order of these outputs will be nondeterministic if the execution is multithreaded (which it will be by default) because there is no dependence between the reactions, and, hence, they can execute in parallel.

The bank index of a container (parent) reactor can be passed down to contained (child) reactors. For example, note the following program:

target C reactor Child(bank_index: int = 0, parent_bank_index: int = 0) { reaction(startup) {= printf( "My bank index: %d. My parent's bank index: %d.\n", self->bank_index, self->parent_bank_index ); =} } reactor Parent(bank_index: int = 0) { c = new[2] Child(parent_bank_index=bank_index) } main reactor { p = new[2] Parent() }

In this example, the bank index of the Parent reactor is passed to the parent_bank_index parameter of the Child reactor instances. The output from this program will be:

My bank index: 1. My parent's bank index: 1.
My bank index: 0. My parent's bank index: 0.
My bank index: 0. My parent's bank index: 1.
My bank index: 1. My parent's bank index: 0.

Again, note that the order of these outputs is nondeterministic.

Finally, members of contained banks of reactors can be individually addressed in the body of reactions of the parent reactor if their input/output port appears in the reaction signature. For example, note the following program:

target C reactor Child(bank_index: int = 0, parent_bank_index: int = 0) { output out: int reaction(startup) -> out {= lf_set(out, self->parent_bank_index * 2 + self->bank_index); =} } reactor Parent(bank_index: int = 0) { c = new[2] Child(parent_bank_index=bank_index) reaction(c.out) {= for (int i=0; i < c_width; i++) { printf("Received %d from child %d.\n", c[i].out->value, i); } =} } main reactor { p = new[2] Parent() }
Lingua Franca diagram: ChildParentBank2ChildParentBank2ParentChild2out2

Running this program will give something like the following:

Received 0 from child 0.
Received 1 from child 1.
Received 2 from child 0.
Received 3 from child 1.

Note the usage of c_width, which holds the width of the c bank of reactors.

Combining Banks and Multiports​

Banks of reactors may be combined with multiports, as in the following example:

target C reactor Source { output[3] out: int reaction(startup) -> out {= for(int i = 0; i < out_width; i++) { lf_set(out[i], i); } =} } reactor Destination(bank_index: int = 0) { input in: int reaction(in) {= printf("Destination %d received %d.\n", self->bank_index, in->value); =} } main reactor MultiportToBank { a = new Source() b = new[3] Destination() a.out -> b.in }
Lingua Franca diagram: MultiportToBankMultiportToBankSourceout[3]Destination3in

The three outputs from the Source instance a will be sent, respectively, to each of three instances of Destination, b[0], b[1], and b[2]. The result of the program will be something like the following:

Destination 0 received 0.
Destination 1 received 1.
Destination 2 received 2.

Again, the order is nondeterministic in a multithreaded context.

The reactors in a bank may themselves have multiports. In all cases, the number of ports on the left of a connection must match the number on the right, unless the ones on the left are iterated, as explained next.

Broadcast Connections​

Occasionally, you will want to have fewer ports on the left of a connection and have their outputs used repeatedly to broadcast to the ports on the right. In the following example, the outputs from an ordinary port are broadcast to the inputs of all instances of a bank of reactors:

reactor Source { output out:int; reaction(startup) -> out {= ... write to out ... =} } reactor Destination { input in:int; reaction(in) {= ... read from in ... =} } main reactor ThreadedThreaded(width:int(4)) { a = new Source(); d = new[width] Destination(); (a.out)+ -> d.in; }

The syntax (a.out)+ means "repeat the output port a.out one or more times as needed to supply all the input ports of d.in." The content inside the parentheses can be a comma-separated list of ports, the ports inside can be ordinary ports or multiports, and the reactors inside can be ordinary reactors or banks of reactors. In all cases, the number of ports inside the parentheses on the left must divide the number of ports on the right.

Interleaved Connections​

Sometimes, we don't want to broadcast messages to all reactors, but need more fine-grained control as to which reactor within a bank receives a message. If we have separate source and destination reactors, this can be done by combining multiports and banks as was shown in Combining Banks and Multiports. Setting a value on the index n of the output multiport, will result in a message to the n-th reactor instance within the destination bank. However, this pattern gets slightly more complicated, if we want to exchange addressable messages between instances of the same bank. This pattern is shown in the following example:

target C reactor Node(num_nodes: size_t = 4, bank_index: int = 0) { input[num_nodes] in: int output[num_nodes] out: int reaction(startup) -> out {= lf_set(out[1], 42); printf("Bank index %d sent 42 on channel 1.\n", self->bank_index); =} reaction(in) {= for (int i = 0; i < in_width; i++) { if (in[i]->is_present) { printf("Bank index %d received %d on channel %d.\n", self->bank_index, in[i]->value, i ); } } =} } main reactor(num_nodes: size_t = 4) { nodes = new[num_nodes] Node(num_nodes=num_nodes) nodes.out -> interleaved(nodes.in) }
Lingua Franca diagram: InterleavedInterleavedNode124in[4]out[4]

In the above program, four instance of Node are created, and, at startup, each instance sends 42 to its second (index 1) output channel. The result is that the second bank member (bank_index 1) will receive the number 42 on each input channel of its multiport input. Running this program gives something like the following:

Bank index 0 sent 42 on channel 1.
Bank index 1 sent 42 on channel 1.
Bank index 2 sent 42 on channel 1.
Bank index 3 sent 42 on channel 1.
Bank index 1 received 42 on channel 0.
Bank index 1 received 42 on channel 1.
Bank index 1 received 42 on channel 2.
Bank index 1 received 42 on channel 3.

In bank index 1, the 0-th channel receives from bank_index 0, the 1-th channel from bank_index 1, etc. In effect, the choice of output channel specifies the destination reactor in the bank, and the input channel specifies the source reactor from which the input comes.

This style of connection is accomplished using the new keyword interleaved in the connection. Normally, a port reference such as nodes.out where nodes is a bank and out is a multiport, would list all the individual ports by first iterating over the banks and then, for each bank index, iterating over the ports. If we consider the tuple (b,p) to denote the index b within the bank and the index p within the multiport, then the following list is created: (0,0), (0,1), (0,2), (0,3), (1,0), (1,1), (1,2), (1,3), (2,0), (2,1), (2,2), (2,3), (3,0), (3,1), (3,2), (3,3). However, if we use interleaved``(nodes.out) instead, the connection logic will iterate over the ports first and then the banks, creating the following list: (0,0), (1,0), (2,0), (3,0), (0,1), (1,1), (2,1), (3,1), (0,2), (1,2), (2,2), (3,2), (0,3), (1,3), (2,3), (3,3). By combining a normal port reference with a interleaved reference, we can construct a fully connected network. The figure below visualizes this how this pattern would look without banks or multiports:

Lingua Franca diagram: AddressableDesugared

If we were to use a normal connection nodes.out -> nodes.in; instead of the interleaved connection, then the following pattern would be created:

Lingua Franca diagram: AddressableNaiveDesugared

Effectively, this connects each reactor instance to itself, which isn't very useful.