This is unreleased documentation for Lingua Franca Nightly 🚧 version.
For up-to-date documentation, see the latest version (0.9.0).
Version: Nightly 🚧
Multiports and Banks
This article has examples in the following target languages:
C
C++
Python
Rust
TypeScript
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.
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:
c
cpp
py
rs
ts
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;
}
target Cpp;
reactor Source {
output[4] out:int;
reaction(startup) -> out {=
for(auto i = 0ul; i < out.size(); i++) {
out[i].set(i);
}
=}
}
reactor Destination {
input[4] in:int;
reaction(in) {=
int sum = 0;
for (auto i = 0ul; i < in.size(); i++) {
if (in[i].is_present()){
sum += *in[i].get();
}
}
std::cout << "Sum of received: " << sum << std::endl;
=}
}
main reactor {
a = new Source();
b = new Destination();
a.out -> b.in;
}
target Python;
reactor Source {
output[4] out;
reaction(startup) -> out {=
for i, port in enumerate(out):
port.set(i)
=}
}
reactor Destination {
input[4] inp;
reaction(inp) {=
sum = 0
for port in inp:
if port.is_present: sum += port.value
print(f"Sum of received: {sum}.")
=}
}
main reactor {
a = new Source();
b = new Destination();
a.out -> b.inp;
}
target Rust;
reactor Source {
output[4] out:usize;
reaction(startup) -> out {=
for (i, o) in out.into_iter().enumerate() {
ctx.set(o, i);
}
=}
}
reactor Destination {
input[4] inp:usize;
reaction(inp) {=
let mut sum = 0;
for i in inp {
if let Some(v) = ctx.get(&i) {
sum += v;
}
}
println!("Sum of received: {}.", sum);
=}
}
main reactor {
a = new Source();
b = new Destination();
a.out -> b.inp;
}
target TypeScript
reactor Source {
output[4] out:number
reaction(startup) -> out {=
for (let i = 0 ; i < out.length; i++) {
out[i] = i
}
=}
}
reactor Destination {
input[4] inp:number
reaction(inp) {=
let sum = 0
for (let i = 0 ; i < inp.length; i++) {
const val = inp[i]
if (val) sum += val
}
console.log(`Sum of received: ${sum}`)
=}
}
main reactor {
a = new Source()
b = new Destination()
a.out -> b.inp
}
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:
c
cpp
py
ts
rs
if(in[i]->is_present)...
if(in[i]->is_present())...
if port.is_present:...
if(val)...
if let Some(v) = ctx.get(&i) ... // Or using is_present() if ctx.is_present(&inp[0]) ...
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.
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 use a more efficient iterator to access the port. For example:
c
target C;
reactor 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);
}
=}
}
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.
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.
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.
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;
}
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:
c
cpp
py
rs
ts
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;
=}
}
target Cpp
reactor MultiportSource(bank_index: int(0)) {
timer t(0, 200 ms)
output out: int
state s: int(0)
reaction(t) -> out {=
out.set(s);
s += bank_index;
=}
}
target Python
reactor MultiportSource(bank_index=0) {
timer t(0, 200 msec)
output out
state s = 0
reaction(t) -> out {=
out.set(self.s)
self.s += self.bank_index
=}
}
target TypeScript
reactor MultiportSource {
timer t(0, 200 msec)
output out: number
state s: number = 0
reaction(t) -> out {=
out = s
s += this.getBankIndex()
=}
}
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;
}
The parameter table defined in the main reactor is used to initialize the value parameter of each bank member. The result of running this is something like:
Banks of reactors can be nested. For example, note the following program:
c
cpp
py
rs
ts
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();
}
target Cpp;
reactor Child (
bank_index:int = 0
) {
reaction(startup) {=
std::cout << "My bank index:" << bank_index << std::endl;
=}
}
reactor Parent (
bank_index:int = 0
) {
c = new[2] Child();
}
main reactor {
p = new[2] Parent();
}
target Python;
reactor Child (
bank_index = 0
) {
reaction(startup) {=
print(f"My bank index: {self.bank_index}.")
=}
}
reactor Parent (
bank_index = 0
) {
c = new[2] Child();
}
main reactor {
p = new[2] Parent();
}
target Rust;
reactor Child (
bank_index:usize = 0
) {
state bank_index = bank_index;
reaction(startup) {=
println!("My bank index: {}.", self.bank_index);
=}
}
reactor Parent (
bank_index:usize = 0
) {
c = new[2] Child();
}
main reactor {
p = new[2] Parent();
}
target TypeScript
reactor Child {
reaction(startup) {=
console.log(`My bank index ${this.getBankIndex()}`)
=}
}
reactor Parent {
c = new[2] Child()
}
main reactor {
p = new[2] Parent()
}
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:
c
cpp
py
rs
ts
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()
}
target Cpp
reactor Child(bank_index: int(0), parent_bank_index: int(0)) {
reaction(startup) {=
std::cout <<"My bank index: " << bank_index << " My parent's bank index: " << parent_bank_index << std::endl;
=}
}
reactor Parent(bank_index: int(0)) {
c = new[2] Child(parent_bank_index=bank_index)
}
main reactor {
p = new[2] Parent()
}
target Python
reactor Child(bank_index=0, parent_bank_index=0) {
reaction(startup) {=
print(
f"My bank index: {self.bank_index}. "
f"My parent's bank index: {self.parent_bank_index}."
)
=}
}
reactor Parent(bank_index=0) {
c = new[2] Child(parent_bank_index=bank_index)
}
main reactor {
p = new[2] Parent()
}
target Rust
reactor Child(bank_index: usize = 0, parent_bank_index: usize = 0) {
state bank_index = bank_index
state parent_bank_index = parent_bank_index
reaction(startup) {=
println!(
"My bank index: {}. My parent's bank index: {}.",
self.bank_index,
self.parent_bank_index,
);
=}
}
reactor Parent(bank_index: usize = 0) {
c = new[2] Child(parent_bank_index=bank_index)
}
main reactor {
p = new[2] Parent()
}
target TypeScript
reactor Child(parentBankIndex: number = 0) {
reaction(startup) {=
console.log(`My bank index: ${this.getBankIndex()} My parent's bank index: ${parentBankIndex}`)
=}
}
reactor Parent {
c = new[2] Child(parentBankIndex = {= this.getBankIndex() =})
}
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:
c
cpp
py
rs
ts
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()
}
target Cpp
reactor Child(bank_index: int(0), parent_bank_index: int(0)) {
output out: int
reaction(startup) -> out {=
out.set(parent_bank_index * 2 + bank_index);
=}
}
reactor Parent(bank_index: int(0)) {
c = new[2] Child(parent_bank_index=bank_index)
reaction(c.out) {=
for (auto i = 0ul; i < c.size(); i++) {
std::cout << "Received " << *c[i].out.get() <<" from child " << i << std::endl;
}
=}
}
main reactor {
p = new[2] Parent()
}
target Python
reactor Child(bank_index=0, parent_bank_index=0) {
output out
reaction(startup) -> out {=
out.set(self.parent_bank_index * 2 + self.bank_index)
=}
}
reactor Parent(bank_index=0) {
c = new[2] Child(parent_bank_index=bank_index)
reaction(c.out) {=
for i, child in enumerate(c):
print(f"Received {child.out.value} from child {i}.")
=}
}
main reactor {
p = new[2] Parent()
}
ChildParentBank2.lf missing for Rust
target TypeScript
reactor Child(parentBankIndex: number = 0) {
output out: number
reaction(startup) -> out {=
out = parentBankIndex * 2 + this.getBankIndex()
=}
}
reactor Parent {
c = new[2] Child(parentBankIndex = {= this.getBankIndex() =})
reaction(c.out) {=
for (let i = 0; i < c.length; i++) {
console.log(`Received ${c[i].out} from child ${i}`)
}
=}
}
main reactor {
p = new[2] Parent()
}
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.
c
py
cpp
ts
rs
Note the usage of c_width, which holds the width of the c bank of reactors.
Note that len(c) can be used to get the width of the bank, and for p in c or for (i, p) in enumerate(c) can be used to iterate over the bank members.
Note that c.size() can be used to get the width of the bank c.
Note that that bank instance c in TypeScript is an array, so c.length is the width of the bank, and the bank members are referenced by indexing the array, as in c[i].
danger
FIXME: How to get the width of the bank in target code?
Banks of reactors may be combined with multiports, as in the following example:
c
cpp
py
rs
ts
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
}
target Cpp
reactor Source {
output[3] out: int
reaction(startup) -> out {=
for(int i = 0; i < out.size(); i++) {
out[i].set(i);
}
=}
}
reactor Destination(bank_index: int(0)) {
input in: int
reaction(in) {=
std::cout << "Destination " << bank_index << " received " << *in.get() << std::endl;
=}
}
main reactor MultiportToBank {
a = new Source()
b = new[3] Destination()
a.out -> b.in
}
target Python
reactor Source {
output[3] out
reaction(startup) -> out {=
for i, port in enumerate(out):
port.set(i)
=}
}
reactor Destination(bank_index=0) {
input inp
reaction(inp) {=
print(f"Destination {self.bank_index} received {inp.value}.")
=}
}
main reactor MultiportToBank {
a = new Source()
b = new[3] Destination()
a.out -> b.inp
}
target Rust
reactor Source {
output[3] out: usize
reaction(startup) -> out {=
for (i, o) in out.into_iter().enumerate() {
ctx.set(o, i);
}
=}
}
reactor Destination(bank_index: usize = 0) {
state bank_index = bank_index
input inp: usize
reaction(inp) {=
println!(
"Destination {} received {}.",
self.bank_index,
ctx.get(inp).unwrap(),
);
=}
}
main reactor MultiportToBank {
a = new Source()
b = new[3] Destination()
a.out -> b.inp
}
target TypeScript
reactor Source {
output[3] out: number
reaction(startup) -> out {=
for (let i = 0 ; i < out.length; i++) {
out[i] = i
}
=}
}
reactor Destination {
input inp: number
reaction(inp) {=
console.log(`Destination ${this.getBankIndex()} received ${inp}`)
=}
}
main reactor MultiportToBank {
a = new Source()
b = new[3] Destination()
a.out -> b.inp
}
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.
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.
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:
c
cpp
py
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)
}
target Cpp
reactor Node(num_nodes: size_t(4), bank_index: int(0)) {
input[num_nodes] in: int
output[num_nodes] out: int
reaction(startup) -> out {=
out[1].set(42);
std::cout << "Bank index " << bank_index << " sent 42 on channel 1." << std::endl;
=}
reaction(in) {=
for (auto i = 0ul; i < in.size(); i++) {
if (in[i].is_present()) {
std::cout << "Bank index " << bank_index
<< " received " << *in[i].get() << " on channel" << std::endl;
}
}
=}
}
main reactor(num_nodes: size_t(4)) {
nodes = new[num_nodes] Node(num_nodes=num_nodes)
nodes.out -> interleaved(nodes.in)
}
target Python
reactor Node(num_nodes=4, bank_index=0) {
input[num_nodes] inp
output[num_nodes] out
reaction(startup) -> out {=
out[1].set(42)
print(f"Bank index {self.bank_index} sent 42 on channel 1.")
=}
reaction(inp) {=
for i, port in enumerate(inp):
if port.is_present:
print(
f"Bank index {self.bank_index} received {port.value} on channel {i}.",
)
=}
}
main reactor(num_nodes=4) {
nodes = new[num_nodes] Node(num_nodes=num_nodes)
nodes.out -> interleaved(nodes.inp)
}
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:
If we were to use a normal connection nodes.out -> nodes.in; instead of the interleaved connection, then the following pattern would be created:
Effectively, this connects each reactor instance to itself, which isn't very useful.
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:
c
cpp
py
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)
}
target Cpp
reactor Node(num_nodes: size_t(4), bank_index: int(0)) {
input[num_nodes] in: int
output[num_nodes] out: int
reaction(startup) -> out {=
out[1].set(42);
std::cout << "Bank index " << bank_index << " sent 42 on channel 1." << std::endl;
=}
reaction(in) {=
for (auto i = 0ul; i < in.size(); i++) {
if (in[i].is_present()) {
std::cout << "Bank index " << bank_index
<< " received " << *in[i].get() << " on channel" << std::endl;
}
}
=}
}
main reactor(num_nodes: size_t(4)) {
nodes = new[num_nodes] Node(num_nodes=num_nodes)
nodes.out -> interleaved(nodes.in)
}
target Python
reactor Node(num_nodes=4, bank_index=0) {
input[num_nodes] inp
output[num_nodes] out
reaction(startup) -> out {=
out[1].set(42)
print(f"Bank index {self.bank_index} sent 42 on channel 1.")
=}
reaction(inp) {=
for i, port in enumerate(inp):
if port.is_present:
print(
f"Bank index {self.bank_index} received {port.value} on channel {i}.",
)
=}
}
main reactor(num_nodes=4) {
nodes = new[num_nodes] Node(num_nodes=num_nodes)
nodes.out -> interleaved(nodes.inp)
}
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:
If we were to use a normal connection nodes.out -> nodes.in; instead of the interleaved connection, then the following pattern would be created:
Effectively, this connects each reactor instance to itself, which isn't very useful.
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:
c
cpp
py
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)
}
target Cpp
reactor Node(num_nodes: size_t(4), bank_index: int(0)) {
input[num_nodes] in: int
output[num_nodes] out: int
reaction(startup) -> out {=
out[1].set(42);
std::cout << "Bank index " << bank_index << " sent 42 on channel 1." << std::endl;
=}
reaction(in) {=
for (auto i = 0ul; i < in.size(); i++) {
if (in[i].is_present()) {
std::cout << "Bank index " << bank_index
<< " received " << *in[i].get() << " on channel" << std::endl;
}
}
=}
}
main reactor(num_nodes: size_t(4)) {
nodes = new[num_nodes] Node(num_nodes=num_nodes)
nodes.out -> interleaved(nodes.in)
}
target Python
reactor Node(num_nodes=4, bank_index=0) {
input[num_nodes] inp
output[num_nodes] out
reaction(startup) -> out {=
out[1].set(42)
print(f"Bank index {self.bank_index} sent 42 on channel 1.")
=}
reaction(inp) {=
for i, port in enumerate(inp):
if port.is_present:
print(
f"Bank index {self.bank_index} received {port.value} on channel {i}.",
)
=}
}
main reactor(num_nodes=4) {
nodes = new[num_nodes] Node(num_nodes=num_nodes)
nodes.out -> interleaved(nodes.inp)
}
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:
If we were to use a normal connection nodes.out -> nodes.in; instead of the interleaved connection, then the following pattern would be created:
Effectively, this connects each reactor instance to itself, which isn't very useful.
The interleaved keyword is not supported by this target language.
The interleaved keyword is not supported by this target language.