Actors
An actor is composed on 3 elements:
- a set of inputs,
- a set of outputs,
- a client.
Both outputs and inputs are optional but an actor must have at least either one input or one output. Inputs and outputs may be sampled at a different rate, but the rate must be the same for all inputs and for all outputs.
An actor runs within its own thread independently of other actors and perform 3 functions:
- collect and read inputs into the client,
- update the state of the client,
- write and distribute the outputs from the client to other actors.
These 3 functions are excuted sequentially within a loop.
A client must comply with the definition of the actor interface.
The interface consists in 3 traits: Update
, Read
and Write
.
A client must:
- implement the
Update
trait, - have an implementation of the
Read
trait for each input, - have an implementation of the
Write
trait for each output.
Actor inputs and outputs are given a unique type, usually an empty Enum.
Each input and output must implement the UniqueIdentifier
trait which associated type DataType
is set to the primitive type of the client data.
As an example, lets write an interface for a client which task is to multiply an integer by e. Lets define
- the client:
#[derive(Default)]
struct Client {
data: i32,
}
- the input
In
:
#[derive(UID)]
#[uid(data = "i32")]
enum In {}
- the output
Out
:
#[derive(UID)]
#[uid(data = "f32")]
enum Out {}
Each input and output is given a unique type (here an empty Enum
) that implements the UniqueIdentifier
trait with the derive macro UID
.
The input/ouput primitive types (i32
for the input and f32
for the ouput) are affected to the associated type DataType
of the UniqueIdentifier
traits.
And now lets build the interface:
- update is empty, this simple task can be done at the output
impl Update for Client {}
- read input
impl Read<In> for Client {
fn read(&mut self, data: Data<In>) {
self.data = *data;
}
}
- write output
impl Write<Out> for Client {
fn write(&mut self) -> Option<Data<Out>> {
Some(Data::new(self.data as f32 * std::f32::consts::E))
}
}
Actors exchange their clients data that is contained inside the structure Data
.
The type of the client data can be anything as long as the input that receives it or the output that sends it, implements the UniqueIdentifier
trait.
Once the actor to client interface has been written, the client can then be used to build an actor.
Here is the signature of the Actor
type:
struct Actor<C, const NI: usize = 1, const NO: usize = 1> where C: Update
An actor takes 3 generic type parameters:
C
: the type of the client,NI
: the sampling rate of the inputs,NO
: the sampling rate of the outputs.
Sampling rates are given as ratio between the simulation sampling frequency and the actor inputs or outputs sampling frequency.
The where clause required that the client implements the Update
trait, meaning that anything can be an actor's client as long as it implements the Update
trait.
Actors implements the From trait for any type that implements the Update
trait.
As a consequence, a client can be converted into an actor with:
let actor = Actor::<Client,1,1>::from(client);
When using the default value (1) for the inputs and outputs rate, they can be omitted:
let actor = Actor::<Client>::from(client);
In that case, the compiler is also able to infer the client type:
let actor = Actor::<_>::from(client);
or we can use the Into syntax:
let actor: Actor::<_> = client.into();
An actor with no inputs must set NI
to 0 or use the type alias Initiator
defined as Initiator<C, const NO: usize = 1> = Actor<C, 0, NO>
:
let no_input_actor = Initiator::<_>::from(client);
An actor with no outputs must set NO
to 0 or use the type alias Terminator
defined as Terminator<C, const NI: usize = 1> = Actor<C, NI, 0>
:
let no_output_actor = Terminator::<_>::from(client);
The conversion methods from
and into
consume their arguments meaning that the client is no longer available once the actor has been created.
This is not always desirable, instead the new
method of Actor
can be used to pass a reference to the client into an actor.
It is worth noting that all the inputs and outputs of an actor will also be given a copy of the reference to the client in order to pass data to it and to get data from it. And because an actor performs many of its own tasks asynchronously, a client must first be wrapped into the thread-safe smart pointers Arc and Mutex like so
let thread_safe_client = Arc::new(Mutex::new(client));
followed by the actor declaration
let actor = Actor::new(thread_safe_client.clone());
Note that all types that implements the Update
trait can be converted into a thread safe type with
let thread_safe_client = client.into_arcx();
A unique name can be given to an actor. The name will be use to identify the actor's client in the model flowchart. The actor's name (here "aleph") can be set either like this:
let actor = Actor::<_>::from((client, "aleph"));
or like this
let actor: Actor::<_> = (client, "aleph").into();
or even like this
let actor = Actor::new(thread_safe_client.clone()).name("aleph");