1use chrono::{DateTime, Duration, Utc};
11use serde::{Deserialize, Serialize};
12
13use crate::{direction::ControllerDirection, loop_config::LoopConfig};
14
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
18#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
19pub struct Tick {
20 pub time: DateTime<Utc>,
21 pub pv: f32,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
31pub enum Action {
32 WriteMv(f32),
34 Complete {
38 peaks: Vec<f32>,
39 troughs: Vec<f32>,
40 switch_times: Vec<DateTime<Utc>>,
41 mv_sign_init: i8,
42 },
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
48pub struct InitialReadings {
49 pub pv_ini: f32,
50 pub mv_ini: f32,
51 pub mv_range_low: f32,
53 pub mv_range_high: f32,
55}
56
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
62pub struct MrftCompat {
63 pub replicate_lower_clamp_bug: bool,
68 pub replicate_extrema_reset_bug: bool,
72}
73
74pub fn clamp_relay_amplitude(
78 relay_amp_percent: f32,
79 mv_ini: f32,
80 mv_range_low: f32,
81 mv_range_high: f32,
82 compat: MrftCompat,
83) -> f32 {
84 let mut relay_amp_raw = relay_amp_percent / 100.0 * (mv_range_high - mv_range_low);
85
86 if mv_ini + relay_amp_raw > mv_range_high {
87 relay_amp_raw = mv_range_high - mv_ini;
88 } else if mv_ini - relay_amp_raw < mv_range_low {
89 relay_amp_raw = if compat.replicate_lower_clamp_bug {
90 mv_range_low + mv_ini
91 } else {
92 mv_ini - mv_range_low
93 };
94 }
95
96 relay_amp_raw
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
103#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
104pub struct MrftState {
105 pub hysteresis: f32,
106 pub mv_value_current: f32,
107 pub mv_sign_next_step: i8,
108 pub counter_all_switches: u32,
109 pub cycles_completed: i32,
111 pub cycles_remaining: i32,
115}
116
117#[derive(Debug, Clone)]
125pub struct MrftEngine {
126 beta: f32,
128 action_multiplier: i8,
129 relay_amp_raw: f32,
130 pv_value_ini: f32,
131 mv_value_ini: f32,
132 num_switches_skip: u32,
133 num_cycles_count: u32,
134 noise_protection_secs: u32,
135
136 mv_value_current: f32,
138 mv_value_next_step: f32,
139 max_pv_cycle: f32,
140 min_pv_cycle: f32,
141 max_pv_result: f32,
142 min_pv_result: f32,
143 hysteresis: f32,
144 mv_sign_next_step: i8,
145 mv_sign_init: i8,
146 time_previous_switch: DateTime<Utc>,
147 counter_all_switches: u32,
148 peaks: Vec<f32>,
149 troughs: Vec<f32>,
150 switch_times: Vec<DateTime<Utc>>,
151 completed: bool,
152 compat: MrftCompat,
153}
154
155impl MrftEngine {
156 pub fn new(
169 config: LoopConfig,
170 direction: ControllerDirection,
171 beta: f32,
172 initial: InitialReadings,
173 start_time: DateTime<Utc>,
174 compat: MrftCompat,
175 ) -> MrftEngine {
176 let action_multiplier = direction.action_multiplier();
177
178 let relay_amp_raw = clamp_relay_amplitude(
179 config.relay_amp_percent,
180 initial.mv_ini,
181 initial.mv_range_low,
182 initial.mv_range_high,
183 compat,
184 );
185
186 MrftEngine {
187 beta,
188 action_multiplier,
189 relay_amp_raw,
190 pv_value_ini: initial.pv_ini,
191 mv_value_ini: initial.mv_ini,
192 num_switches_skip: config.num_cycles_skip * 2 + 1,
193 num_cycles_count: config.num_cycles_count,
194 noise_protection_secs: config.noise_protection_secs,
195
196 mv_value_current: initial.mv_ini,
197 mv_value_next_step: initial.mv_ini,
198 max_pv_cycle: initial.pv_ini,
199 min_pv_cycle: initial.pv_ini,
200 max_pv_result: initial.pv_ini,
201 min_pv_result: initial.pv_ini,
202 hysteresis: 0.0,
203 mv_sign_next_step: 1,
204 mv_sign_init: 0,
205 time_previous_switch: start_time,
206 counter_all_switches: 0,
207 peaks: Vec::new(),
208 troughs: Vec::new(),
209 switch_times: Vec::new(),
210 completed: false,
211 compat,
212 }
213 }
214
215 pub fn step(&mut self, tick: Tick) -> Vec<Action> {
223 if self.completed {
224 return Vec::new();
225 }
226
227 let mut actions = Vec::new();
228
229 if self.switch_is_needed(tick) {
230 actions.push(Action::WriteMv(self.perform_switch(tick)));
231 }
232
233 if self.is_complete() {
234 self.completed = true;
235 actions.push(Action::Complete {
236 peaks: self.peaks.clone(),
237 troughs: self.troughs.clone(),
238 switch_times: self.switch_times.clone(),
239 mv_sign_init: self.mv_sign_init,
240 });
241 }
242
243 actions
244 }
245
246 pub fn state(&self) -> MrftState {
248 MrftState {
249 hysteresis: self.hysteresis,
250 mv_value_current: self.mv_value_current,
251 mv_sign_next_step: self.mv_sign_next_step,
252 counter_all_switches: self.counter_all_switches,
253 cycles_completed: self.cycles_completed(),
254 cycles_remaining: self.cycles_remaining(),
255 }
256 }
257
258 pub fn is_complete(&self) -> bool {
260 self.counter_all_switches >= self.num_switches_skip + self.num_cycles_count * 2
261 }
262
263 fn cycles_completed(&self) -> i32 {
267 (self.counter_all_switches as i32 - 1) / 2
268 }
269
270 fn cycles_remaining(&self) -> i32 {
273 (self.num_switches_skip as i32 + self.num_cycles_count as i32 * 2
274 - self.counter_all_switches as i32
275 + 1)
276 / 2
277 }
278
279 fn switch_is_needed(&mut self, tick: Tick) -> bool {
284 let sp_pv_diff = self.pv_value_ini - tick.pv;
285
286 self.max_pv_cycle = self.max_pv_cycle.max(tick.pv);
287 self.min_pv_cycle = self.min_pv_cycle.min(tick.pv);
288 self.max_pv_result = self.max_pv_result.max(tick.pv);
289 self.min_pv_result = self.min_pv_result.min(tick.pv);
290
291 self.hysteresis = self.beta
292 * (self.max_pv_cycle - self.pv_value_ini).max(self.pv_value_ini - self.min_pv_cycle);
293
294 let mv_sign_previous: i8 = if self.mv_value_current >= self.mv_value_ini {
295 1
296 } else {
297 -1
298 };
299
300 let valve_switch: f32;
301 if self.action_multiplier == 1 {
302 valve_switch = sp_pv_diff + mv_sign_previous as f32 * self.hysteresis;
303 self.mv_sign_next_step = if valve_switch >= 0.0 { 1 } else { -1 };
304 } else {
305 valve_switch = sp_pv_diff - mv_sign_previous as f32 * self.hysteresis;
306 self.mv_sign_next_step = if valve_switch <= 0.0 { 1 } else { -1 };
307 }
308
309 if self.counter_all_switches == 0 {
314 self.mv_sign_init = self.mv_sign_next_step;
315 }
316
317 self.mv_value_next_step =
318 self.mv_value_ini + self.mv_sign_next_step as f32 * self.relay_amp_raw;
319
320 let mv_switch_required =
323 (self.mv_value_next_step as f64 - self.mv_value_current as f64).abs() >= 0.01;
324
325 let enable_mv_switch = self.time_previous_switch
326 + Duration::seconds(self.noise_protection_secs as i64)
327 <= tick.time
328 || self.counter_all_switches == 0;
329
330 mv_switch_required && enable_mv_switch
331 }
332
333 fn perform_switch(&mut self, tick: Tick) -> f32 {
342 self.time_previous_switch = tick.time;
343
344 self.counter_all_switches += 1;
345 if self.counter_all_switches >= self.num_switches_skip {
346 self.switch_times.push(tick.time);
347
348 if self.mv_sign_next_step as i32 * self.action_multiplier as i32 == 1 {
349 self.peaks.push(self.max_pv_result);
350 } else {
351 self.troughs.push(self.min_pv_result);
352 }
353 }
354
355 self.max_pv_cycle = self.pv_value_ini;
356 self.min_pv_cycle = self.pv_value_ini;
357 if self.compat.replicate_extrema_reset_bug {
358 self.max_pv_result = self.pv_value_ini;
359 self.min_pv_result = self.pv_value_ini;
360 } else {
361 self.max_pv_result = tick.pv;
362 self.min_pv_result = tick.pv;
363 }
364
365 self.mv_value_current = if self.cycles_remaining() == 0 {
366 self.mv_value_ini
367 } else {
368 self.mv_value_next_step
369 };
370
371 self.mv_value_current
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::{
379 constants::{ResponseLevel, lookup},
380 controller_type::ControllerType,
381 process_type::ProcessType,
382 };
383
384 fn t(secs: i64) -> DateTime<Utc> {
385 DateTime::UNIX_EPOCH + Duration::seconds(secs)
386 }
387
388 fn config() -> LoopConfig {
393 LoopConfig {
394 process_type: ProcessType::Flow,
395 controller_type: ControllerType::Pi,
396 relay_amp_percent: 10.0,
397 num_cycles_skip: 0,
398 num_cycles_count: 1,
399 noise_protection_secs: 0,
400 mrft_delay_secs: 0,
401 }
402 }
403
404 fn initial() -> InitialReadings {
405 InitialReadings {
406 pv_ini: 50.0,
407 mv_ini: 50.0,
408 mv_range_low: 0.0,
409 mv_range_high: 100.0,
410 }
411 }
412
413 fn t_ms(offset_ms: i64) -> DateTime<Utc> {
414 DateTime::UNIX_EPOCH + Duration::milliseconds(offset_ms)
415 }
416
417 mod clamp_relay_amplitude_tests {
418 use super::*;
419
420 #[test]
421 fn no_clamp_needed_when_within_range() {
422 let amp = clamp_relay_amplitude(10.0, 50.0, 0.0, 100.0, MrftCompat::default());
423 assert_eq!(amp, 10.0); }
425
426 #[test]
427 fn unclamped_amplitude_uses_the_mv_span() {
428 let amp = clamp_relay_amplitude(10.0, 50.0, 20.0, 100.0, MrftCompat::default());
429 assert_eq!(amp, 8.0); }
431
432 #[test]
433 fn upper_clamp_engages_near_ceiling() {
434 let amp = clamp_relay_amplitude(10.0, 95.0, 0.0, 100.0, MrftCompat::default());
435 assert_eq!(amp, 5.0); }
437
438 #[test]
439 fn lower_clamp_uses_dimensionally_correct_formula_by_default() {
440 let amp = clamp_relay_amplitude(10.0, 10.0, 5.0, 100.0, MrftCompat::default());
443 assert_eq!(amp, 5.0);
444 }
445
446 #[test]
447 fn lower_clamp_replicates_legacy_bug_when_compat_flag_set() {
448 let compat = MrftCompat {
449 replicate_lower_clamp_bug: true,
450 ..MrftCompat::default()
451 };
452 let amp = clamp_relay_amplitude(10.0, 10.0, 5.0, 100.0, compat);
453 assert_eq!(amp, 15.0); }
455
456 #[test]
457 fn fixed_and_buggy_formulas_agree_when_mv_range_low_is_zero() {
458 let fixed = clamp_relay_amplitude(50.0, 10.0, 0.0, 100.0, MrftCompat::default());
461 let buggy = clamp_relay_amplitude(
462 50.0,
463 10.0,
464 0.0,
465 100.0,
466 MrftCompat {
467 replicate_lower_clamp_bug: true,
468 ..MrftCompat::default()
469 },
470 );
471 assert_eq!(fixed, buggy);
472 }
473
474 #[test]
475 fn cascade_case_with_nonzero_floor() {
476 let amp = clamp_relay_amplitude(10.0, 10.0, 5.0, 100.0, MrftCompat::default());
479 assert_eq!(amp, 5.0);
480 }
481
482 #[test]
483 fn lower_clamp_boundary_uses_subtraction() {
484 let relay_amp_percent = 100.0 * 1.1 / 91.0;
485 let amp =
486 clamp_relay_amplitude(relay_amp_percent, 10.0, 9.0, 100.0, MrftCompat::default());
487 assert_eq!(amp, 1.0);
488 }
489 }
490
491 #[test]
492 fn action_multiplier_is_negative_one_for_direct() {
493 let engine = MrftEngine::new(
494 config(),
495 ControllerDirection::Direct,
496 0.0,
497 initial(),
498 t(0),
499 MrftCompat::default(),
500 );
501 assert_eq!(engine.action_multiplier, -1);
502 }
503
504 #[test]
505 fn action_multiplier_is_positive_one_for_reverse() {
506 let engine = MrftEngine::new(
507 config(),
508 ControllerDirection::Reverse,
509 0.0,
510 initial(),
511 t(0),
512 MrftCompat::default(),
513 );
514 assert_eq!(engine.action_multiplier, 1);
515 }
516
517 #[test]
523 fn full_run_reverse_action_completes_with_expected_peaks_troughs_and_snap_back() {
524 let mut engine = MrftEngine::new(
525 config(),
526 ControllerDirection::Reverse,
527 0.3,
528 initial(),
529 t(0),
530 MrftCompat::default(),
531 );
532
533 let actions = engine.step(Tick {
534 time: t(1),
535 pv: 55.0,
536 });
537 assert_eq!(actions, vec![Action::WriteMv(40.0)]);
538 assert_eq!(
539 engine.state(),
540 MrftState {
541 hysteresis: 1.5,
542 mv_value_current: 40.0,
543 mv_sign_next_step: -1,
544 counter_all_switches: 1,
545 cycles_completed: 0,
546 cycles_remaining: 1,
547 }
548 );
549
550 let actions = engine.step(Tick {
551 time: t(2),
552 pv: 45.0,
553 });
554 assert_eq!(actions, vec![Action::WriteMv(60.0)]);
555 assert_eq!(
556 engine.state(),
557 MrftState {
558 hysteresis: 1.5,
559 mv_value_current: 60.0,
560 mv_sign_next_step: 1,
561 counter_all_switches: 2,
562 cycles_completed: 0,
563 cycles_remaining: 1,
564 }
565 );
566
567 let actions = engine.step(Tick {
570 time: t(3),
571 pv: 55.0,
572 });
573 assert_eq!(
574 actions,
575 vec![
576 Action::WriteMv(50.0),
577 Action::Complete {
578 peaks: vec![55.0],
579 troughs: vec![50.0, 45.0],
580 switch_times: vec![t(1), t(2), t(3)],
581 mv_sign_init: -1,
582 },
583 ]
584 );
585 assert_eq!(
586 engine.state(),
587 MrftState {
588 hysteresis: 1.5,
589 mv_value_current: 50.0,
590 mv_sign_next_step: -1,
591 counter_all_switches: 3,
592 cycles_completed: 1,
593 cycles_remaining: 0,
594 }
595 );
596 assert!(engine.is_complete());
597 }
598
599 #[test]
600 fn extrema_compatibility_flag_reproduces_the_legacy_reset() {
601 let mut engine = MrftEngine::new(
602 config(),
603 ControllerDirection::Reverse,
604 0.3,
605 initial(),
606 t(0),
607 MrftCompat {
608 replicate_extrema_reset_bug: true,
609 ..MrftCompat::default()
610 },
611 );
612
613 let mut last_actions = Vec::new();
614 for (i, pv) in [55.0, 45.0, 55.0].into_iter().enumerate() {
615 last_actions = engine.step(Tick {
616 time: t(i as i64 + 1),
617 pv,
618 });
619 }
620
621 assert_eq!(
622 last_actions,
623 vec![
624 Action::WriteMv(50.0),
625 Action::Complete {
626 peaks: vec![50.0],
627 troughs: vec![50.0, 50.0],
628 switch_times: vec![t(1), t(2), t(3)],
629 mv_sign_init: -1,
630 },
631 ]
632 );
633 }
634
635 #[test]
636 fn direct_action_mirrors_reverse_with_peaks_and_troughs_swapped() {
637 let mut engine = MrftEngine::new(
638 config(),
639 ControllerDirection::Direct,
640 0.0,
641 initial(),
642 t(0),
643 MrftCompat::default(),
644 );
645
646 let mut last_actions = Vec::new();
647 for (i, pv) in [40.0, 60.0, 40.0].into_iter().enumerate() {
648 last_actions = engine.step(Tick {
649 time: t(i as i64 + 1),
650 pv,
651 });
652 }
653
654 assert_eq!(
655 last_actions,
656 vec![
657 Action::WriteMv(50.0),
658 Action::Complete {
659 peaks: vec![50.0, 60.0],
662 troughs: vec![40.0],
663 switch_times: vec![t(1), t(2), t(3)],
664 mv_sign_init: -1,
665 },
666 ]
667 );
668 }
669
670 #[test]
671 fn run_7_like_trace_includes_switch_endpoints_in_recorded_extrema() {
672 let config = LoopConfig {
673 process_type: ProcessType::Flow,
674 controller_type: ControllerType::Pi,
675 relay_amp_percent: 10.0,
676 num_cycles_skip: 1,
677 num_cycles_count: 2,
678 noise_protection_secs: 3,
679 mrft_delay_secs: 0,
680 };
681 let initial = InitialReadings {
682 pv_ini: 2.25,
683 mv_ini: 50.0,
684 mv_range_low: 12.0,
685 mv_range_high: 100.0,
686 };
687 let beta = lookup(
688 ProcessType::Flow,
689 ControllerType::Pi,
690 ResponseLevel::Aggressive,
691 )
692 .beta;
693 let samples = [
694 (0, 2.25),
695 (2001, 2.49),
696 (2999, 2.67),
697 (3999, 2.72),
698 (5999, 2.25),
699 (6999, 1.89),
700 (8999, 2.26),
701 (10001, 2.61),
702 (12002, 2.24),
703 (12999, 1.89),
704 (13999, 1.80),
705 (15999, 2.25),
706 (17999, 2.61),
707 (19000, 2.24),
708 (20000, 1.89),
709 (21000, 1.80),
710 ];
711 let mut engine = MrftEngine::new(
712 config,
713 ControllerDirection::Reverse,
714 beta,
715 initial,
716 t_ms(0),
717 MrftCompat::default(),
718 );
719
720 let completion = samples.iter().find_map(|(offset_ms, pv)| {
721 engine
722 .step(Tick {
723 time: t_ms(*offset_ms),
724 pv: *pv,
725 })
726 .into_iter()
727 .find_map(|action| match action {
728 Action::Complete {
729 peaks,
730 troughs,
731 switch_times,
732 mv_sign_init,
733 } => Some((peaks, troughs, switch_times, mv_sign_init)),
734 Action::WriteMv(_) => None,
735 })
736 });
737
738 let (peaks, troughs, switch_times, mv_sign_init) =
739 completion.expect("run 7-like trace must complete");
740 assert_eq!(mv_sign_init, 1);
741 assert_eq!(switch_times.len(), 5);
742 assert_eq!(peaks, vec![2.72, 2.61, 2.61]);
743 assert_eq!(troughs, vec![1.89, 1.80]);
744 }
745
746 #[test]
747 fn run_10_like_trace_keeps_the_final_switch_pv_as_a_trough() {
748 let config = LoopConfig {
749 process_type: ProcessType::PressureLine,
750 controller_type: ControllerType::Pi,
751 relay_amp_percent: 10.0,
752 num_cycles_skip: 1,
753 num_cycles_count: 2,
754 noise_protection_secs: 3,
755 mrft_delay_secs: 0,
756 };
757 let initial = InitialReadings {
758 pv_ini: 189.94,
759 mv_ini: 50.0,
760 mv_range_low: 0.0,
761 mv_range_high: 100.0,
762 };
763 let beta = lookup(
764 ProcessType::PressureLine,
765 ControllerType::Pi,
766 ResponseLevel::Aggressive,
767 )
768 .beta;
769 let samples = [
770 (0, 189.96),
771 (2001, 189.97),
772 (3003, 181.07),
773 (5989, 174.54),
774 (6989, 187.07),
775 (8989, 204.0),
776 (10990, 204.43),
777 (11988, 192.14),
778 (12990, 182.73),
779 (15990, 175.28),
780 (17989, 197.09),
781 (20003, 200.38),
782 (20988, 189.04),
783 (22991, 173.70),
784 (24991, 174.22),
785 (25990, 186.82),
786 (27989, 203.86),
787 ];
788 let mut engine = MrftEngine::new(
789 config,
790 ControllerDirection::Reverse,
791 beta,
792 initial,
793 t_ms(0),
794 MrftCompat::default(),
795 );
796
797 let completion = samples.iter().find_map(|(offset_ms, pv)| {
798 engine
799 .step(Tick {
800 time: t_ms(*offset_ms),
801 pv: *pv,
802 })
803 .into_iter()
804 .find_map(|action| match action {
805 Action::Complete {
806 peaks,
807 troughs,
808 switch_times,
809 mv_sign_init,
810 } => Some((peaks, troughs, switch_times, mv_sign_init)),
811 Action::WriteMv(_) => None,
812 })
813 });
814
815 let (peaks, troughs, switch_times, mv_sign_init) =
816 completion.expect("run 10-like trace must complete");
817 assert_eq!(mv_sign_init, -1);
818 assert_eq!(switch_times.len(), 5);
819 assert_eq!(peaks, vec![204.43, 200.38]);
820 assert_eq!(troughs, vec![174.54, 175.28, 173.70]);
821 }
822
823 #[test]
824 fn direct_action_hysteresis_uses_previous_sign_and_subtracts() {
825 let mut engine = MrftEngine::new(
826 config(),
827 ControllerDirection::Direct,
828 0.3,
829 initial(),
830 t(0),
831 MrftCompat::default(),
832 );
833
834 assert_eq!(
835 engine.step(Tick {
836 time: t(1),
837 pv: 40.0,
838 }),
839 vec![Action::WriteMv(40.0)]
840 );
841 assert_eq!(engine.mv_sign_init, -1);
842
843 assert!(
846 engine
847 .step(Tick {
848 time: t(2),
849 pv: 40.0,
850 })
851 .is_empty()
852 );
853
854 assert!(
858 engine
859 .step(Tick {
860 time: t(3),
861 pv: 49.0,
862 })
863 .is_empty()
864 );
865 }
866
867 #[test]
868 fn direct_hysteresis_switch_sign_uses_multiplication() {
869 let mut engine = MrftEngine::new(
870 LoopConfig {
871 noise_protection_secs: 100,
872 ..config()
873 },
874 ControllerDirection::Direct,
875 0.3,
876 initial(),
877 t(0),
878 MrftCompat::default(),
879 );
880
881 assert_eq!(
882 engine.step(Tick {
883 time: t(1),
884 pv: 40.0,
885 }),
886 vec![Action::WriteMv(40.0)]
887 );
888 assert!(
889 engine
890 .step(Tick {
891 time: t(2),
892 pv: 60.0,
893 })
894 .is_empty()
895 );
896 assert!(
897 engine
898 .step(Tick {
899 time: t(3),
900 pv: 53.0,
901 })
902 .is_empty()
903 );
904
905 assert_eq!(engine.hysteresis, 3.0);
906 assert_eq!(engine.mv_sign_next_step, 1);
907 assert_eq!(engine.mv_value_next_step, 60.0);
908 }
909
910 #[test]
915 fn skip_cycles_are_excluded_from_recorded_peaks_and_troughs() {
916 let config = LoopConfig {
917 num_cycles_skip: 1,
918 num_cycles_count: 1,
919 ..config()
920 };
921 let mut engine = MrftEngine::new(
922 config,
923 ControllerDirection::Reverse,
924 0.0,
925 initial(),
926 t(0),
927 MrftCompat::default(),
928 );
929
930 let mut last_actions = Vec::new();
931 for (i, pv) in [60.0, 40.0, 60.0, 40.0, 60.0].into_iter().enumerate() {
932 last_actions = engine.step(Tick {
933 time: t(i as i64 + 1),
934 pv,
935 });
936 }
937
938 assert_eq!(
939 last_actions,
940 vec![
941 Action::WriteMv(50.0),
942 Action::Complete {
943 peaks: vec![60.0],
944 troughs: vec![40.0, 40.0],
945 switch_times: vec![t(3), t(4), t(5)],
946 mv_sign_init: -1,
947 },
948 ]
949 );
950 assert_eq!(engine.state().counter_all_switches, 5);
951 }
952
953 #[test]
957 fn noise_protection_suppresses_and_then_allows_a_switch() {
958 let config = LoopConfig {
959 noise_protection_secs: 5,
960 ..config()
961 };
962 let mut engine = MrftEngine::new(
963 config,
964 ControllerDirection::Reverse,
965 0.0,
966 initial(),
967 t(0),
968 MrftCompat::default(),
969 );
970
971 let actions = engine.step(Tick {
972 time: t(1),
973 pv: 60.0,
974 });
975 assert_eq!(actions, vec![Action::WriteMv(40.0)]);
976
977 let actions = engine.step(Tick {
980 time: t(2),
981 pv: 40.0,
982 });
983 assert!(actions.is_empty());
984 assert_eq!(engine.state().mv_sign_next_step, 1);
985 assert_eq!(engine.state().counter_all_switches, 1);
986
987 let actions = engine.step(Tick {
989 time: t(6),
990 pv: 40.0,
991 });
992 assert_eq!(actions, vec![Action::WriteMv(60.0)]);
993 assert_eq!(engine.state().counter_all_switches, 2);
994 }
995
996 #[test]
997 fn step_after_completion_is_a_no_op() {
998 let mut engine = MrftEngine::new(
999 config(),
1000 ControllerDirection::Reverse,
1001 0.3,
1002 initial(),
1003 t(0),
1004 MrftCompat::default(),
1005 );
1006
1007 for (i, pv) in [55.0, 45.0, 55.0].into_iter().enumerate() {
1008 engine.step(Tick {
1009 time: t(i as i64 + 1),
1010 pv,
1011 });
1012 }
1013 assert!(engine.is_complete());
1014
1015 let actions = engine.step(Tick {
1016 time: t(100),
1017 pv: 0.0,
1018 });
1019 assert!(actions.is_empty());
1020 }
1021
1022 #[test]
1023 fn tick_serde_round_trip() {
1024 let tick = Tick {
1025 time: t(42),
1026 pv: 12.5,
1027 };
1028 let json = serde_json::to_string(&tick).unwrap();
1029 let back: Tick = serde_json::from_str(&json).unwrap();
1030 assert_eq!(tick, back);
1031 }
1032
1033 #[test]
1034 fn action_serde_round_trip() {
1035 for action in [
1036 Action::WriteMv(12.5),
1037 Action::Complete {
1038 peaks: vec![1.0, 2.0],
1039 troughs: vec![3.0],
1040 switch_times: vec![t(1), t(2)],
1041 mv_sign_init: 1,
1042 },
1043 ] {
1044 let json = serde_json::to_string(&action).unwrap();
1045 let back: Action = serde_json::from_str(&json).unwrap();
1046 assert_eq!(action, back);
1047 }
1048 }
1049}