1use core::time::Duration;
2
3use crate::{
4 traits::{Float, MatrixOps},
5 Scalar,
6};
7use pictorus_block_data::{BlockData as OldBlockData, FromPass};
8use pictorus_traits::{HasIc, Matrix, Pass, PassBy, ProcessBlock};
9
10pub struct IntegralBlock<T: Apply>
15where
16 OldBlockData: FromPass<T::Output>,
17{
18 previous_sample: Option<T::Output>,
19 output: Option<T::Output>,
20 pub data: OldBlockData,
21}
22
23impl<T: Apply> Default for IntegralBlock<T>
24where
25 OldBlockData: FromPass<T::Output>,
26{
27 fn default() -> Self {
28 Self {
29 previous_sample: None,
30 output: None,
31 data: <OldBlockData as FromPass<T::Output>>::from_pass(T::Output::default().as_by()),
32 }
33 }
34}
35
36impl<F: Float, R: Scalar> ProcessBlock for IntegralBlock<(F, R)>
37where
38 OldBlockData: FromPass<F>,
39{
40 type Inputs = (F, R);
41 type Output = F;
42 type Parameters = Parameters<(F, R)>;
43
44 fn process<'b>(
45 &'b mut self,
46 parameters: &Self::Parameters,
47 context: &dyn pictorus_traits::Context,
48 inputs: PassBy<'_, Self::Inputs>,
49 ) -> PassBy<'b, Self::Output> {
50 let (sample, reset) = inputs;
51 if reset.is_truthy() {
52 self.output = None;
54 self.previous_sample = None;
55 } else {
56 let delta = match parameters.method {
57 IntgeralMethod::Rectangle => {
58 F::from_duration(context.timestep().unwrap_or(Duration::ZERO)) * sample
59 }
60 IntgeralMethod::Trapezoidal => {
61 F::from_duration(context.timestep().unwrap_or(Duration::ZERO))
62 * (sample + self.previous_sample.unwrap_or(parameters.ic))
63 / (F::one() + F::one())
64 }
65 };
66 let output = self
68 .output
69 .insert(self.output.unwrap_or(parameters.ic) + delta);
70
71 if *output > parameters.clamp_limit {
73 *output = parameters.clamp_limit;
74 } else if *output < -parameters.clamp_limit {
75 *output = -parameters.clamp_limit;
76 }
77 self.previous_sample = Some(sample);
79 }
80 let output = self.output.get_or_insert(parameters.ic);
82 self.data = OldBlockData::from_pass(output.as_by());
83 output.as_by()
84 }
85}
86
87impl<F: Float, R: Scalar> HasIc for IntegralBlock<(F, R)>
88where
89 OldBlockData: FromPass<F>,
90{
91 fn new(parameters: &Self::Parameters) -> Self {
92 IntegralBlock::<(F, R)> {
93 previous_sample: None,
94 output: Some(parameters.ic),
95 data: <OldBlockData as FromPass<F>>::from_pass(parameters.ic.as_by()),
96 }
97 }
98}
99
100impl<F: Float, const NROWS: usize, const NCOLS: usize, R: Scalar> ProcessBlock
101 for IntegralBlock<(Matrix<NROWS, NCOLS, F>, R)>
102where
103 OldBlockData: FromPass<Matrix<NROWS, NCOLS, F>>,
104{
105 type Inputs = (Matrix<NROWS, NCOLS, F>, R);
106 type Output = Matrix<NROWS, NCOLS, F>;
107 type Parameters = Parameters<(Matrix<NROWS, NCOLS, F>, R)>;
108
109 fn process<'b>(
110 &'b mut self,
111 parameters: &Self::Parameters,
112 context: &dyn pictorus_traits::Context,
113 inputs: PassBy<'_, Self::Inputs>,
114 ) -> PassBy<'b, Self::Output> {
115 let (sample, reset) = inputs;
116 if reset.is_truthy() {
117 self.output = None;
118 self.previous_sample = None;
119 } else {
120 let output = self.output.get_or_insert(parameters.ic);
121 sample.for_each(|sample, c, r| {
122 let delta = match parameters.method {
123 IntgeralMethod::Rectangle => {
124 F::from_duration(context.timestep().unwrap_or(Duration::ZERO)) * sample
125 }
126 IntgeralMethod::Trapezoidal => {
127 F::from_duration(context.timestep().unwrap_or(Duration::ZERO))
128 * (sample + self.previous_sample.unwrap_or(parameters.ic).data[c][r])
129 / (F::one() + F::one())
130 }
131 };
132 output.data[c][r] += delta;
133 if output.data[c][r] > parameters.clamp_limit {
134 output.data[c][r] = parameters.clamp_limit;
135 } else if output.data[c][r] < -parameters.clamp_limit {
136 output.data[c][r] = -parameters.clamp_limit;
137 }
138 });
139 self.previous_sample = Some(*sample);
140 }
141 let output = self.output.get_or_insert(parameters.ic);
142 self.data = OldBlockData::from_pass(output);
143 output
144 }
145}
146
147impl<F: Float, const NROWS: usize, const NCOLS: usize, R: Scalar> HasIc
148 for IntegralBlock<(Matrix<NROWS, NCOLS, F>, R)>
149where
150 OldBlockData: FromPass<Matrix<NROWS, NCOLS, F>>,
151{
152 fn new(parameters: &Self::Parameters) -> Self {
153 IntegralBlock::<(Matrix<NROWS, NCOLS, F>, R)> {
154 previous_sample: None,
155 output: Some(parameters.ic),
156 data: <OldBlockData as FromPass<Matrix<NROWS, NCOLS, F>>>::from_pass(¶meters.ic),
157 }
158 }
159}
160
161pub trait Apply: Pass + Default {
162 type Float: Float;
163 type Output: Pass + Default;
164}
165
166impl<F: Float, R: Scalar> Apply for (F, R) {
167 type Float = F;
168 type Output = F;
169}
170
171impl<F: Float, const NROWS: usize, const NCOLS: usize, R: Scalar> Apply
172 for (Matrix<NROWS, NCOLS, F>, R)
173{
174 type Float = F;
175 type Output = Matrix<NROWS, NCOLS, F>;
176}
177
178#[derive(strum::EnumString, Debug, Clone, Copy)]
182pub enum IntgeralMethod {
183 Rectangle,
184 Trapezoidal,
185}
186
187pub struct Parameters<T: Apply> {
188 pub clamp_limit: T::Float,
189 pub ic: T::Output,
190 pub method: IntgeralMethod,
191}
192
193impl<T: Apply> Parameters<T> {
198 pub fn new(ic: T::Output, clamp_limit: T::Float, method: &str) -> Self {
199 Parameters {
200 clamp_limit,
201 ic,
202 method: method.parse().unwrap(),
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::testing::{StubContext, StubRuntime};
211 use crate::SinewaveBlock;
212 use approx::assert_relative_eq;
213 use pictorus_traits::GeneratorBlock;
214
215 #[test]
216 fn test_integral_scalar() {
217 let mut runtime = StubRuntime::default();
218
219 let mut block = IntegralBlock::<(f64, bool)>::default();
220 let parameters = Parameters::new(0.0, 20.0, "Trapezoidal");
221
222 let mut sine_wave_block = SinewaveBlock::<f64>::default();
223 let sine_wave_parameters =
224 <SinewaveBlock<f64> as GeneratorBlock>::Parameters::new(1.0, 1.0, 0.0, 0.0);
225
226 let mut cosine_wave_block = SinewaveBlock::<f64>::default();
227 let cosine_wave_parameters =
229 <SinewaveBlock<f64> as GeneratorBlock>::Parameters::new(1.0, 1.0, f64::PI / 2., 0.0);
230
231 for _ in 0..100 {
232 let sine_output = sine_wave_block.generate(&sine_wave_parameters, &runtime.context());
233 let cosine_output =
234 cosine_wave_block.generate(&cosine_wave_parameters, &runtime.context());
235 runtime.tick();
236
237 let integral_output = block.process(
238 ¶meters,
239 &runtime.context(),
240 (cosine_output, false).as_by(),
241 );
242 assert_relative_eq!(integral_output, sine_output + 0.05, epsilon = 0.01);
244 }
245
246 let reset_output =
248 block.process(¶meters, &runtime.context(), (1000000.0, true).as_by());
249 assert_relative_eq!(reset_output, 0.0, epsilon = 0.01);
250 }
251
252 #[test]
253 fn test_integral_matrix() {
254 let mut runtime = StubRuntime::new(StubContext::new(
255 Duration::ZERO,
256 None,
257 Duration::from_secs(1),
258 ));
259
260 let mut block: IntegralBlock<(Matrix<1, 3, f64>, bool)> =
261 IntegralBlock::<(Matrix<1, 3, f64>, bool)>::default();
262 let parameters: Parameters<(Matrix<1, 3, f64>, bool)> = Parameters::new(
263 Matrix {
264 data: [[0.0], [0.0], [0.0]],
265 },
266 20.0,
267 "Rectangle",
268 );
269
270 let input = Matrix {
271 data: [[1.0], [1.0], [15.0]],
272 };
273
274 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
275 assert_eq!(output.data, [[0.0], [0.0], [0.0]]);
276 runtime.tick();
277 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
278 assert_eq!(output.data, [[1.0], [1.0], [15.0]]);
279
280 runtime.tick();
281 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
282 assert_eq!(output.data, [[2.0], [2.0], [20.0]]);
284 }
285
286 #[test]
287 fn test_integral_ic_scalar() {
288 let mut runtime = StubRuntime::new(StubContext::new(
289 Duration::ZERO,
290 None,
291 Duration::from_secs(1),
292 ));
293
294 let parameters = Parameters::new(10.0, 50.0, "Rectangle");
295 let mut block = IntegralBlock::<(f64, bool)>::new(¶meters);
296 assert_eq!(block.data.scalar(), 10.0);
298
299 let input = 25.0;
300 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
301 assert_eq!(output, 10.0);
303 runtime.tick();
304 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
305 assert_eq!(output, 35.0);
306
307 runtime.tick();
308 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
309 assert_eq!(output, 50.0);
310 }
311
312 #[test]
313 fn test_integral_ic_matrix() {
314 let mut runtime = StubRuntime::new(StubContext::new(
315 Duration::ZERO,
316 None,
317 Duration::from_secs(1),
318 ));
319
320 let parameters = Parameters::new(
321 Matrix {
322 data: [[10.0], [10.0], [10.0]],
323 },
324 50.0,
325 "Rectangle",
326 );
327 let mut block = IntegralBlock::<(Matrix<1, 3, f64>, bool)>::new(¶meters);
328 assert_eq!(
330 block.data.get_data().as_slice(),
331 [[10.0], [10.0], [10.0]].as_flattened()
332 );
333
334 let input = Matrix {
335 data: [[1.0], [1.0], [25.0]],
336 };
337 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
338 assert_eq!(output.data, [[10.0], [10.0], [10.0]]);
339 runtime.tick();
340 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
341 assert_eq!(output.data, [[11.0], [11.0], [35.0]]);
342
343 runtime.tick();
344 let output = block.process(¶meters, &runtime.context(), (input, false).as_by());
345 assert_eq!(output.data, [[12.0], [12.0], [50.0]]);
347 }
348}