pid/
pid.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright 2024 UxuginPython
3#[cfg(feature = "alloc")]
4use rrtk::streams::converters::*;
5#[cfg(feature = "alloc")]
6use rrtk::streams::math::*;
7#[cfg(feature = "alloc")]
8use rrtk::*;
9//Note that RRTK includes the rrtk::streams::control::PIDControllerStream type which should be a
10//bit faster than this. This example is to show how streams can be chained for more complex data
11//processing and control theory. Using the PID controller shown here in production is discouraged.
12#[cfg(feature = "alloc")]
13struct StreamPID {
14    //It is possible to avoid dynamic dispatch by using the actual stream types instead of
15    //`dyn Getter`, but doing that fully makes the types look like this:
16    //Reference<IntegralStream<DifferenceStream<Quantity, ConstantGetter<Quantity, TimeGetterFromGetter<Quantity, dyn Getter<Quantity, ()>, ()>, ()>, dyn Getter<Quantity, ()>, ()>, ()>>
17    //Here, to make this example legible, we don't do that. However, if you're actually making
18    //something for production, expanding the type fully to avoid dynamic dispatch may be a good
19    //idea. It really depends on how much readability you're willing to give up for a small
20    //performance boost.
21    int: Reference<dyn Getter<Quantity, ()>>,
22    drv: Reference<dyn Getter<Quantity, ()>>,
23    pro_float_maker: Reference<dyn Getter<f32, ()>>,
24    int_float_maker: Reference<dyn Getter<f32, ()>>,
25    drv_float_maker: Reference<dyn Getter<f32, ()>>,
26    output: SumStream<f32, 3, ()>,
27}
28#[cfg(feature = "alloc")]
29impl StreamPID {
30    pub fn new(
31        input: Reference<dyn Getter<Quantity, ()>>,
32        setpoint: Quantity,
33        kp: Quantity,
34        ki: Quantity,
35        kd: Quantity,
36    ) -> Self {
37        let time_getter = rc_ref_cell_reference(TimeGetterFromGetter::new(input.clone()));
38        let setpoint = rc_ref_cell_reference(ConstantGetter::new(time_getter.clone(), setpoint));
39        let kp = rc_ref_cell_reference(ConstantGetter::new(time_getter.clone(), kp));
40        let ki = rc_ref_cell_reference(ConstantGetter::new(time_getter.clone(), ki));
41        let kd = rc_ref_cell_reference(ConstantGetter::new(time_getter.clone(), kd));
42        let error = rc_ref_cell_reference(DifferenceStream::new(setpoint.clone(), input.clone()));
43        let int = rc_ref_cell_reference(IntegralStream::new(error.clone()));
44        let drv = rc_ref_cell_reference(DerivativeStream::new(error.clone()));
45        //`ProductStream`'s behavior is to treat all `None` values as 1.0 so that it's as if they
46        //were not included. However, this is not what we want with the coefficient. `NoneToValue`
47        //is used to convert all `None` values to `Some(0.0)` to effectively exlude them from the
48        //final sum.
49        let int_zeroer = rc_ref_cell_reference(NoneToValue::new(
50            int.clone(),
51            time_getter.clone(),
52            Quantity::new(0.0, MILLIMETER),
53        ));
54        let drv_zeroer = rc_ref_cell_reference(NoneToValue::new(
55            drv.clone(),
56            time_getter.clone(),
57            Quantity::new(0.0, MILLIMETER),
58        ));
59        let kp_mul = rc_ref_cell_reference(ProductStream::new([
60            to_dyn!(Getter<Quantity, ()>, kp.clone()),
61            to_dyn!(Getter<Quantity, ()>, error.clone()),
62        ]));
63        //The way a PID controller works necessitates that it adds quantities of different units.
64        //Thus, QuantityToFloat streams are required to keep the dimensional analysis system from
65        //stopping this.
66        let pro_float_maker = rc_ref_cell_reference(QuantityToFloat::new(kp_mul));
67        let ki_mul = rc_ref_cell_reference(ProductStream::new([
68            to_dyn!(Getter<Quantity, ()>, ki.clone()),
69            to_dyn!(Getter<Quantity, ()>, int_zeroer.clone()),
70        ]));
71        let int_float_maker = rc_ref_cell_reference(QuantityToFloat::new(ki_mul));
72        let kd_mul = rc_ref_cell_reference(ProductStream::new([
73            to_dyn!(Getter<Quantity, ()>, kd.clone()),
74            to_dyn!(Getter<Quantity, ()>, drv_zeroer.clone()),
75        ]));
76        let drv_float_maker = rc_ref_cell_reference(QuantityToFloat::new(kd_mul));
77        let output = SumStream::new([
78            to_dyn!(Getter<f32, ()>, pro_float_maker.clone()),
79            to_dyn!(Getter<f32, ()>, int_float_maker.clone()),
80            to_dyn!(Getter<f32, ()>, drv_float_maker.clone()),
81        ]);
82        Self {
83            int: to_dyn!(Getter<Quantity, ()>, int),
84            drv: to_dyn!(Getter<Quantity, ()>, drv),
85            pro_float_maker: to_dyn!(Getter<f32, ()>, pro_float_maker),
86            int_float_maker: to_dyn!(Getter<f32, ()>, int_float_maker),
87            drv_float_maker: to_dyn!(Getter<f32, ()>, drv_float_maker),
88            output: output,
89        }
90    }
91}
92#[cfg(feature = "alloc")]
93impl Getter<f32, ()> for StreamPID {
94    fn get(&self) -> Output<f32, ()> {
95        self.output.get()
96    }
97}
98#[cfg(feature = "alloc")]
99impl Updatable<()> for StreamPID {
100    fn update(&mut self) -> NothingOrError<()> {
101        //The other streams used that are not updated here do not need to be updated. Streams like
102        //SumStream just calculate their output in the get method since they do not need to store
103        //any data beyond the `Reference`s to their inputs. The non-math streams used here work in
104        //a similar way.
105        self.int.borrow_mut().update()?;
106        self.drv.borrow_mut().update()?;
107        self.pro_float_maker.borrow_mut().update()?;
108        self.int_float_maker.borrow_mut().update()?;
109        self.drv_float_maker.borrow_mut().update()?;
110        Ok(())
111    }
112}
113#[cfg(feature = "alloc")]
114struct MyStream {
115    time: Time,
116}
117#[cfg(feature = "alloc")]
118impl MyStream {
119    pub fn new() -> Self {
120        Self { time: Time(0) }
121    }
122}
123//In a real system, obviously, the process variable must be dependent on the command. This is a
124//very rudimentary placeholder and a poor model of an actual system. All this example is
125//intended to do is to show the PID controller's command values and not model a real system by
126//assuming a constant velocity.
127#[cfg(feature = "alloc")]
128impl Getter<Quantity, ()> for MyStream {
129    fn get(&self) -> Output<Quantity, ()> {
130        Ok(Some(Datum::new(
131            self.time,
132            Quantity::from(self.time) * Quantity::new(0.5, MILLIMETER_PER_SECOND),
133        )))
134    }
135}
136#[cfg(feature = "alloc")]
137impl Updatable<()> for MyStream {
138    fn update(&mut self) -> NothingOrError<()> {
139        self.time += Time(2_000_000_000);
140        Ok(())
141    }
142}
143#[cfg(feature = "alloc")]
144fn main() {
145    const SETPOINT: Quantity = Quantity::new(5.0, MILLIMETER);
146    const KP: Quantity = Quantity::dimensionless(1.0);
147    const KI: Quantity = Quantity::dimensionless(0.01);
148    const KD: Quantity = Quantity::dimensionless(0.1);
149    println!("PID Controller using RRTK Streams");
150    println!(
151        "kp = {:?}; ki = {:?}; kd = {:?}",
152        KP.value, KI.value, KD.value
153    );
154    let input = to_dyn!(Getter<Quantity, ()>, rc_ref_cell_reference(MyStream::new()));
155    let mut stream = StreamPID::new(input.clone(), SETPOINT, KP, KI, KD);
156    stream.update().unwrap();
157    println!(
158        "time: {:?}; setpoint: {:?}; process: {:?}; command: {:?}",
159        stream.get().unwrap().unwrap().time.0,
160        SETPOINT.value,
161        input.borrow().get().unwrap().unwrap().value.value,
162        stream.get().unwrap().unwrap().value
163    );
164    for _ in 0..6 {
165        input.borrow_mut().update().unwrap();
166        stream.update().unwrap();
167        println!(
168            "time: {:?}; setpoint: {:?}; process: {:?}; command: {:?}",
169            stream.get().unwrap().unwrap().time,
170            SETPOINT,
171            input.borrow().get().unwrap().unwrap().value,
172            stream.get().unwrap().unwrap().value
173        );
174    }
175}
176#[cfg(not(feature = "alloc"))]
177fn main() {
178    println!("Enable the `alloc` feature to run this example.\nAssuming you're using Cargo, add the `--features alloc` flag to your command.");
179}