Feedback System

A feedback system is a system with a feedback loop:

Feedback system

Such a system with a direct feedthrough from C to B is also known as an algebraic loop. It is a singular system as shown with the sequence of events:

ABC
1Update--
2Write::<U>--
3-Read::<U>-

After step 3, the system cannot progress: B is waiting for Y from C before sendind E to C, while at the same time, C is waiting for E from B before sending Y to B.

In order to resolve the conflict, we can bootstrap the system but having C sending a default value for Y at the start of the simulation:

ABC
1Update-Write::<Y>
2Write::<U>--
3-Read::<U,Y>-
4UpdateUpdate-
5Write::<U>Write::<E>-
6--Read::<E>
7-Read::<U>Update
8Update-Write::<Y>
9Write::<U>Read::<Y>
10-Update-
11...

gmt_dos-actors implements such bootstrapping method for feedback system like the kind of system with an integral controller.

Integrator is the client that performs the functions of an integral controller. It continuously integrates the negative of the input (weighted by the gain of the controller) and returns the integral.

An actor for a scalar integrator with a gain of 0.5 is declared with

    let mut integrator: Actor<_> = Integrator::new(1).gain(0.5).into();

Lets add a constant signal and a logger to the model:

    let mut signal: Initiator<_> = Signals::new(1, n_step)
        .channel(0, Signal::Constant(1f64))
        .into();
    let logging = Logging::<f64>::new(3).into_arcx();
    let mut logger = Terminator::<_>::new(logging.clone());

The client of the last actor to be added to the model, sums the signal and the feedback from the integral controller:

    let mut sum: Actor<_> = (Sum::default(), "+").into();

Lets define the types for inputs and outputs:

#[derive(UID)]
enum U {}
#[derive(UID)]
enum Y {}
#[derive(UID)]
enum E {}

The connections are defined with, for the feedthrough:

    signal
        .add_output()
        .multiplex(2)
        .build::<U>()
        .into_input(&mut sum)
        .into_input(&mut logger)?;
    sum.add_output()
        .multiplex(2)
        .build::<E>()
        .into_input(&mut integrator)
        .into_input(&mut logger)?;

and for the feedback with the bootstrapping of Y:

    integrator
        .add_output()
        .multiplex(2)
        .bootstrap()
        .build::<Y>()
        .into_input(&mut sum)
        .into_input(&mut logger)?;

The model is:

    model!(signal, sum, integrator, logger)
        .name("feedback-model")
        .flowchart()
        .check()?
        .run()
        .await?;

Feedback model

Note the bolder line for the Y output (this is how the bootstrapped outputs are always drawn).

The logged data is

    println!("Logs:");
    println!("    :     U       E       Y");
    (*logging.lock().await)
        .chunks()
        .enumerate()
        .take(20)
        .for_each(|(i, x)| println!("{:4}: {:+.3?}", i, x));

Feedback logs

Implementation of the Sum client:

pub struct Sum {
    left: Data<U>,
    right: Data<Y>,
}
impl Default for Sum {
    fn default() -> Self {
        Self {
            left: Data::new(vec![]),
            right: Data::new(vec![]),
        }
    }
}
impl Update for Sum {}
impl Read<U> for Sum {
    fn read(&mut self, data: Data<U>) {
        self.left = data.clone();
    }
}
impl Read<Y> for Sum {
    fn read(&mut self, data: Data<Y>) {
        self.right = data.clone();
    }
}
impl Write<E> for Sum {
    fn write(&mut self) -> Option<Data<E>> {
        Some(Data::new(
            self.left
                .iter()
                .zip(self.right.iter())
                .map(|(l, r)| l + r)
                .collect(),
        ))
    }
}