1#![no_std]
2#![doc = include_str!("../README.md")]
3
4pub type Float = f32;
9
10const MIN_ABS_B0: Float = 1.0e-9;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ConfigError {
19 NonFinite,
21 NonPositiveSamplePeriod,
23 NonPositiveParameter,
25 ZeroPlantGain,
27 InvalidOutputLimit,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct OutputLimit {
39 pub min: Float,
41 pub max: Float,
43}
44
45impl OutputLimit {
46 #[inline]
48 pub const fn new(min: Float, max: Float) -> Self {
49 Self { min, max }
50 }
51
52 #[inline]
54 pub fn apply(self, value: Float) -> Float {
55 if value < self.min {
56 self.min
57 } else if value > self.max {
58 self.max
59 } else {
60 value
61 }
62 }
63
64 #[inline]
65 fn validate(self) -> Result<(), ConfigError> {
66 if !finite(self.min) || !finite(self.max) {
67 return Err(ConfigError::NonFinite);
68 }
69
70 if self.min > self.max {
71 return Err(ConfigError::InvalidOutputLimit);
72 }
73
74 Ok(())
75 }
76}
77
78#[inline]
79fn apply_limit(value: Float, limit: Option<OutputLimit>) -> Float {
80 match limit {
81 Some(limit) => limit.apply(value),
82 None => value,
83 }
84}
85
86#[inline]
87fn validate_limit(limit: Option<OutputLimit>) -> Result<(), ConfigError> {
88 match limit {
89 Some(limit) => limit.validate(),
90 None => Ok(()),
91 }
92}
93
94#[inline]
95fn finite(value: Float) -> bool {
96 value.is_finite()
97}
98
99#[inline]
100fn validate_sample_period(sample_period: Float) -> Result<(), ConfigError> {
101 if !finite(sample_period) {
102 return Err(ConfigError::NonFinite);
103 }
104
105 if sample_period <= 0.0 {
106 return Err(ConfigError::NonPositiveSamplePeriod);
107 }
108
109 Ok(())
110}
111
112#[inline]
113fn validate_time(time_seconds: Float) -> Result<(), ConfigError> {
114 if !finite(time_seconds) {
115 return Err(ConfigError::NonFinite);
116 }
117
118 Ok(())
119}
120
121#[inline]
122fn elapsed_sample_period(
123 now_seconds: Float,
124 last_update_at: &mut Option<Float>,
125 fallback_sample_period: Float,
126) -> Result<Float, ConfigError> {
127 validate_time(now_seconds)?;
128
129 let sample_period = match *last_update_at {
130 Some(previous) => {
131 let elapsed = now_seconds - previous;
132 validate_sample_period(elapsed)?;
133 elapsed
134 }
135 None => fallback_sample_period,
136 };
137
138 *last_update_at = Some(now_seconds);
139 Ok(sample_period)
140}
141
142#[inline]
143fn elapsed_sample_period_millis(
144 now_millis: u64,
145 last_update_at_millis: &mut Option<u64>,
146 fallback_sample_period: Float,
147) -> Result<Float, ConfigError> {
148 let sample_period = match *last_update_at_millis {
149 Some(previous) => {
150 let elapsed_millis = now_millis
151 .checked_sub(previous)
152 .ok_or(ConfigError::NonPositiveSamplePeriod)?;
153
154 if elapsed_millis == 0 {
155 return Err(ConfigError::NonPositiveSamplePeriod);
156 }
157
158 elapsed_millis as Float * 0.001
159 }
160 None => fallback_sample_period,
161 };
162
163 *last_update_at_millis = Some(now_millis);
164 Ok(sample_period)
165}
166
167#[inline]
168fn validate_positive(value: Float) -> Result<(), ConfigError> {
169 if !finite(value) {
170 return Err(ConfigError::NonFinite);
171 }
172
173 if value <= 0.0 {
174 return Err(ConfigError::NonPositiveParameter);
175 }
176
177 Ok(())
178}
179
180#[inline]
181fn validate_b0(b0: Float) -> Result<(), ConfigError> {
182 if !finite(b0) {
183 return Err(ConfigError::NonFinite);
184 }
185
186 if b0.abs() <= MIN_ABS_B0 {
187 return Err(ConfigError::ZeroPlantGain);
188 }
189
190 Ok(())
191}
192
193pub mod ladrc {
194 use super::{
223 apply_limit, elapsed_sample_period, elapsed_sample_period_millis, validate_b0,
224 validate_limit, validate_positive, validate_sample_period, validate_time, ConfigError,
225 Float, OutputLimit,
226 };
227
228 #[derive(Debug, Clone, Copy, Default, PartialEq)]
230 pub struct FirstOrderEstimate {
231 pub output: Float,
233 pub disturbance: Float,
235 }
236
237 #[derive(Debug, Clone, Copy, PartialEq)]
239 pub struct FirstOrderOutput {
240 pub control: Float,
242 pub unsaturated_control: Float,
244 pub feedback: Float,
246 pub estimate: FirstOrderEstimate,
248 }
249
250 #[derive(Debug, Clone, Copy, PartialEq)]
261 pub struct LadrcFirstOrderConfig {
262 pub sample_period: Float,
267 pub b0: Float,
272 pub kp: Float,
274 pub observer_beta1: Float,
276 pub observer_beta2: Float,
278 pub output_limit: Option<OutputLimit>,
280 }
281
282 impl LadrcFirstOrderConfig {
283 #[inline]
293 pub fn from_bandwidth(
294 sample_period: Float,
295 b0: Float,
296 controller_bandwidth: Float,
297 observer_bandwidth: Float,
298 ) -> Self {
299 Self {
300 sample_period,
301 b0,
302 kp: controller_bandwidth,
303 observer_beta1: 2.0 * observer_bandwidth,
304 observer_beta2: observer_bandwidth * observer_bandwidth,
305 output_limit: None,
306 }
307 }
308
309 #[inline]
311 pub const fn with_output_limit(mut self, limit: OutputLimit) -> Self {
312 self.output_limit = Some(limit);
313 self
314 }
315
316 pub fn validate(self) -> Result<(), ConfigError> {
318 validate_sample_period(self.sample_period)?;
319 validate_b0(self.b0)?;
320 validate_positive(self.kp)?;
321 validate_positive(self.observer_beta1)?;
322 validate_positive(self.observer_beta2)?;
323 validate_limit(self.output_limit)
324 }
325 }
326
327 #[derive(Debug, Clone, Copy, PartialEq)]
333 pub struct LadrcFirstOrder {
334 config: LadrcFirstOrderConfig,
335 z1: Float,
336 z2: Float,
337 last_control: Float,
338 last_update_at: Option<Float>,
339 last_update_at_millis: Option<u64>,
340 }
341
342 impl LadrcFirstOrder {
343 #[inline]
345 pub fn new(config: LadrcFirstOrderConfig) -> Result<Self, ConfigError> {
346 config.validate()?;
347 Ok(Self {
348 config,
349 z1: 0.0,
350 z2: 0.0,
351 last_control: 0.0,
352 last_update_at: None,
353 last_update_at_millis: None,
354 })
355 }
356
357 #[inline]
359 pub const fn config(&self) -> LadrcFirstOrderConfig {
360 self.config
361 }
362
363 #[inline]
365 pub const fn estimate(&self) -> FirstOrderEstimate {
366 FirstOrderEstimate {
367 output: self.z1,
368 disturbance: self.z2,
369 }
370 }
371
372 #[inline]
374 pub const fn last_control(&self) -> Float {
375 self.last_control
376 }
377
378 #[inline]
381 pub const fn last_update_at(&self) -> Option<Float> {
382 self.last_update_at
383 }
384
385 #[inline]
388 pub const fn last_update_at_millis(&self) -> Option<u64> {
389 self.last_update_at_millis
390 }
391
392 #[inline]
395 pub fn reset(&mut self, measurement: Float) {
396 self.reset_with(measurement, 0.0, 0.0);
397 }
398
399 #[inline]
406 pub fn reset_at(
407 &mut self,
408 now_seconds: Float,
409 measurement: Float,
410 ) -> Result<(), ConfigError> {
411 validate_time(now_seconds)?;
412 self.reset(measurement);
413 self.last_update_at = Some(now_seconds);
414 self.last_update_at_millis = None;
415 Ok(())
416 }
417
418 #[inline]
425 pub fn reset_at_millis(&mut self, now_millis: u64, measurement: Float) {
426 self.reset(measurement);
427 self.last_update_at_millis = Some(now_millis);
428 self.last_update_at = None;
429 }
430
431 #[inline]
433 pub fn reset_with(
434 &mut self,
435 estimated_output: Float,
436 estimated_disturbance: Float,
437 last_control: Float,
438 ) {
439 self.z1 = estimated_output;
440 self.z2 = estimated_disturbance;
441 self.last_control = last_control;
442 self.last_update_at = None;
443 self.last_update_at_millis = None;
444 }
445
446 pub fn update(&mut self, reference: Float, measurement: Float) -> FirstOrderOutput {
455 self.update_unchecked(self.config.sample_period, reference, measurement)
456 }
457
458 pub fn update_with_period(
464 &mut self,
465 sample_period: Float,
466 reference: Float,
467 measurement: Float,
468 ) -> Result<FirstOrderOutput, ConfigError> {
469 validate_sample_period(sample_period)?;
470 Ok(self.update_unchecked(sample_period, reference, measurement))
471 }
472
473 pub fn update_at(
481 &mut self,
482 now_seconds: Float,
483 reference: Float,
484 measurement: Float,
485 ) -> Result<FirstOrderOutput, ConfigError> {
486 let sample_period = elapsed_sample_period(
487 now_seconds,
488 &mut self.last_update_at,
489 self.config.sample_period,
490 )?;
491 Ok(self.update_unchecked(sample_period, reference, measurement))
492 }
493
494 pub fn update_at_millis(
500 &mut self,
501 now_millis: u64,
502 reference: Float,
503 measurement: Float,
504 ) -> Result<FirstOrderOutput, ConfigError> {
505 let sample_period = elapsed_sample_period_millis(
506 now_millis,
507 &mut self.last_update_at_millis,
508 self.config.sample_period,
509 )?;
510 Ok(self.update_unchecked(sample_period, reference, measurement))
511 }
512
513 fn update_unchecked(
514 &mut self,
515 sample_period: Float,
516 reference: Float,
517 measurement: Float,
518 ) -> FirstOrderOutput {
519 self.update_observer(measurement, sample_period);
520
521 let feedback = self.config.kp * (reference - self.z1);
522 let unsaturated_control = (feedback - self.z2) / self.config.b0;
523 let control = apply_limit(unsaturated_control, self.config.output_limit);
524 self.last_control = control;
525
526 FirstOrderOutput {
527 control,
528 unsaturated_control,
529 feedback,
530 estimate: self.estimate(),
531 }
532 }
533
534 fn update_observer(&mut self, measurement: Float, sample_period: Float) {
535 let e = self.z1 - measurement;
536 let h = sample_period;
537
538 self.z1 +=
539 h * (self.z2 - self.config.observer_beta1 * e + self.config.b0 * self.last_control);
540 self.z2 += h * (-self.config.observer_beta2 * e);
541 }
542 }
543
544 #[derive(Debug, Clone, Copy, Default, PartialEq)]
546 pub struct SecondOrderEstimate {
547 pub position: Float,
549 pub velocity: Float,
551 pub disturbance: Float,
553 }
554
555 #[derive(Debug, Clone, Copy, PartialEq)]
557 pub struct SecondOrderOutput {
558 pub control: Float,
560 pub unsaturated_control: Float,
562 pub feedback: Float,
564 pub estimate: SecondOrderEstimate,
566 }
567
568 #[derive(Debug, Clone, Copy, PartialEq)]
580 pub struct LadrcSecondOrderConfig {
581 pub sample_period: Float,
586 pub b0: Float,
591 pub kp: Float,
593 pub kd: Float,
595 pub observer_beta1: Float,
597 pub observer_beta2: Float,
599 pub observer_beta3: Float,
601 pub output_limit: Option<OutputLimit>,
603 }
604
605 impl LadrcSecondOrderConfig {
606 #[inline]
616 pub fn from_bandwidth(
617 sample_period: Float,
618 b0: Float,
619 controller_bandwidth: Float,
620 observer_bandwidth: Float,
621 ) -> Self {
622 Self {
623 sample_period,
624 b0,
625 kp: controller_bandwidth * controller_bandwidth,
626 kd: 2.0 * controller_bandwidth,
627 observer_beta1: 3.0 * observer_bandwidth,
628 observer_beta2: 3.0 * observer_bandwidth * observer_bandwidth,
629 observer_beta3: observer_bandwidth * observer_bandwidth * observer_bandwidth,
630 output_limit: None,
631 }
632 }
633
634 #[inline]
636 pub const fn with_output_limit(mut self, limit: OutputLimit) -> Self {
637 self.output_limit = Some(limit);
638 self
639 }
640
641 pub fn validate(self) -> Result<(), ConfigError> {
643 validate_sample_period(self.sample_period)?;
644 validate_b0(self.b0)?;
645 validate_positive(self.kp)?;
646 validate_positive(self.kd)?;
647 validate_positive(self.observer_beta1)?;
648 validate_positive(self.observer_beta2)?;
649 validate_positive(self.observer_beta3)?;
650 validate_limit(self.output_limit)
651 }
652 }
653
654 #[derive(Debug, Clone, Copy, PartialEq)]
662 pub struct LadrcSecondOrder {
663 config: LadrcSecondOrderConfig,
664 z1: Float,
665 z2: Float,
666 z3: Float,
667 last_control: Float,
668 last_update_at: Option<Float>,
669 last_update_at_millis: Option<u64>,
670 }
671
672 impl LadrcSecondOrder {
673 #[inline]
675 pub fn new(config: LadrcSecondOrderConfig) -> Result<Self, ConfigError> {
676 config.validate()?;
677 Ok(Self {
678 config,
679 z1: 0.0,
680 z2: 0.0,
681 z3: 0.0,
682 last_control: 0.0,
683 last_update_at: None,
684 last_update_at_millis: None,
685 })
686 }
687
688 #[inline]
690 pub const fn config(&self) -> LadrcSecondOrderConfig {
691 self.config
692 }
693
694 #[inline]
696 pub const fn estimate(&self) -> SecondOrderEstimate {
697 SecondOrderEstimate {
698 position: self.z1,
699 velocity: self.z2,
700 disturbance: self.z3,
701 }
702 }
703
704 #[inline]
706 pub const fn last_control(&self) -> Float {
707 self.last_control
708 }
709
710 #[inline]
713 pub const fn last_update_at(&self) -> Option<Float> {
714 self.last_update_at
715 }
716
717 #[inline]
720 pub const fn last_update_at_millis(&self) -> Option<u64> {
721 self.last_update_at_millis
722 }
723
724 #[inline]
727 pub fn reset(&mut self, measurement: Float) {
728 self.reset_with(measurement, 0.0, 0.0, 0.0);
729 }
730
731 #[inline]
738 pub fn reset_at(
739 &mut self,
740 now_seconds: Float,
741 measurement: Float,
742 ) -> Result<(), ConfigError> {
743 validate_time(now_seconds)?;
744 self.reset(measurement);
745 self.last_update_at = Some(now_seconds);
746 self.last_update_at_millis = None;
747 Ok(())
748 }
749
750 #[inline]
757 pub fn reset_at_millis(&mut self, now_millis: u64, measurement: Float) {
758 self.reset(measurement);
759 self.last_update_at_millis = Some(now_millis);
760 self.last_update_at = None;
761 }
762
763 #[inline]
765 pub fn reset_with(
766 &mut self,
767 estimated_position: Float,
768 estimated_velocity: Float,
769 estimated_disturbance: Float,
770 last_control: Float,
771 ) {
772 self.z1 = estimated_position;
773 self.z2 = estimated_velocity;
774 self.z3 = estimated_disturbance;
775 self.last_control = last_control;
776 self.last_update_at = None;
777 self.last_update_at_millis = None;
778 }
779
780 #[inline]
785 pub fn update(&mut self, reference: Float, measurement: Float) -> SecondOrderOutput {
786 self.update_with_rate(reference, 0.0, measurement)
787 }
788
789 #[inline]
796 pub fn update_with_period(
797 &mut self,
798 sample_period: Float,
799 reference: Float,
800 measurement: Float,
801 ) -> Result<SecondOrderOutput, ConfigError> {
802 self.update_with_period_and_rate(sample_period, reference, 0.0, measurement)
803 }
804
805 #[inline]
814 pub fn update_at(
815 &mut self,
816 now_seconds: Float,
817 reference: Float,
818 measurement: Float,
819 ) -> Result<SecondOrderOutput, ConfigError> {
820 self.update_at_with_rate(now_seconds, reference, 0.0, measurement)
821 }
822
823 #[inline]
830 pub fn update_at_millis(
831 &mut self,
832 now_millis: u64,
833 reference: Float,
834 measurement: Float,
835 ) -> Result<SecondOrderOutput, ConfigError> {
836 self.update_at_millis_with_rate(now_millis, reference, 0.0, measurement)
837 }
838
839 pub fn update_with_rate(
848 &mut self,
849 reference: Float,
850 reference_rate: Float,
851 measurement: Float,
852 ) -> SecondOrderOutput {
853 self.update_unchecked(
854 self.config.sample_period,
855 reference,
856 reference_rate,
857 measurement,
858 )
859 }
860
861 pub fn update_with_period_and_rate(
868 &mut self,
869 sample_period: Float,
870 reference: Float,
871 reference_rate: Float,
872 measurement: Float,
873 ) -> Result<SecondOrderOutput, ConfigError> {
874 validate_sample_period(sample_period)?;
875 Ok(self.update_unchecked(sample_period, reference, reference_rate, measurement))
876 }
877
878 pub fn update_at_with_rate(
886 &mut self,
887 now_seconds: Float,
888 reference: Float,
889 reference_rate: Float,
890 measurement: Float,
891 ) -> Result<SecondOrderOutput, ConfigError> {
892 let sample_period = elapsed_sample_period(
893 now_seconds,
894 &mut self.last_update_at,
895 self.config.sample_period,
896 )?;
897 Ok(self.update_unchecked(sample_period, reference, reference_rate, measurement))
898 }
899
900 pub fn update_at_millis_with_rate(
905 &mut self,
906 now_millis: u64,
907 reference: Float,
908 reference_rate: Float,
909 measurement: Float,
910 ) -> Result<SecondOrderOutput, ConfigError> {
911 let sample_period = elapsed_sample_period_millis(
912 now_millis,
913 &mut self.last_update_at_millis,
914 self.config.sample_period,
915 )?;
916 Ok(self.update_unchecked(sample_period, reference, reference_rate, measurement))
917 }
918
919 fn update_unchecked(
920 &mut self,
921 sample_period: Float,
922 reference: Float,
923 reference_rate: Float,
924 measurement: Float,
925 ) -> SecondOrderOutput {
926 self.update_observer(measurement, sample_period);
927
928 let feedback = self.config.kp * (reference - self.z1)
929 + self.config.kd * (reference_rate - self.z2);
930 let unsaturated_control = (feedback - self.z3) / self.config.b0;
931 let control = apply_limit(unsaturated_control, self.config.output_limit);
932 self.last_control = control;
933
934 SecondOrderOutput {
935 control,
936 unsaturated_control,
937 feedback,
938 estimate: self.estimate(),
939 }
940 }
941
942 fn update_observer(&mut self, measurement: Float, sample_period: Float) {
943 let e = self.z1 - measurement;
944 let h = sample_period;
945
946 self.z1 += h * (self.z2 - self.config.observer_beta1 * e);
947 self.z2 +=
948 h * (self.z3 - self.config.observer_beta2 * e + self.config.b0 * self.last_control);
949 self.z3 += h * (-self.config.observer_beta3 * e);
950 }
951 }
952
953 pub type Ladrc = LadrcSecondOrder;
955}
956
957pub use ladrc::{
958 FirstOrderEstimate, FirstOrderOutput, Ladrc, LadrcFirstOrder, LadrcFirstOrderConfig,
959 LadrcSecondOrder, LadrcSecondOrderConfig, SecondOrderEstimate, SecondOrderOutput,
960};
961
962#[cfg(test)]
963extern crate std;
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968
969 fn assert_close(actual: Float, expected: Float, tolerance: Float) {
970 assert!(
971 (actual - expected).abs() <= tolerance,
972 "actual={actual}, expected={expected}, tolerance={tolerance}"
973 );
974 }
975
976 #[test]
977 fn output_limit_clamps_values() {
978 let limit = OutputLimit::new(-2.0, 3.0);
979
980 assert_eq!(limit.apply(-4.0), -2.0);
981 assert_eq!(limit.apply(1.0), 1.0);
982 assert_eq!(limit.apply(5.0), 3.0);
983 }
984
985 #[test]
986 fn validates_bad_configurations() {
987 let bad_b0 = ladrc::LadrcSecondOrderConfig::from_bandwidth(0.001, 0.0, 10.0, 50.0);
988 assert_eq!(
989 ladrc::LadrcSecondOrder::new(bad_b0).unwrap_err(),
990 ConfigError::ZeroPlantGain
991 );
992
993 let bad_h = ladrc::LadrcFirstOrderConfig::from_bandwidth(0.0, 1.0, 10.0, 50.0);
994 assert_eq!(
995 ladrc::LadrcFirstOrder::new(bad_h).unwrap_err(),
996 ConfigError::NonPositiveSamplePeriod
997 );
998
999 let bad_limit = ladrc::LadrcFirstOrderConfig::from_bandwidth(0.001, 1.0, 10.0, 50.0)
1000 .with_output_limit(OutputLimit::new(1.0, -1.0));
1001 assert_eq!(
1002 ladrc::LadrcFirstOrder::new(bad_limit).unwrap_err(),
1003 ConfigError::InvalidOutputLimit
1004 );
1005 }
1006
1007 #[test]
1008 fn ladrc_bandwidth_tuning_sets_expected_gains() {
1009 let first = ladrc::LadrcFirstOrderConfig::from_bandwidth(0.001, 2.0, 12.0, 40.0);
1010 assert_close(first.kp, 12.0, 1.0e-6);
1011 assert_close(first.observer_beta1, 80.0, 1.0e-6);
1012 assert_close(first.observer_beta2, 1_600.0, 1.0e-3);
1013
1014 let second = ladrc::LadrcSecondOrderConfig::from_bandwidth(0.001, 2.0, 12.0, 40.0);
1015 assert_close(second.kp, 144.0, 1.0e-6);
1016 assert_close(second.kd, 24.0, 1.0e-6);
1017 assert_close(second.observer_beta1, 120.0, 1.0e-6);
1018 assert_close(second.observer_beta2, 4_800.0, 1.0e-3);
1019 assert_close(second.observer_beta3, 64_000.0, 1.0e-2);
1020 }
1021
1022 #[test]
1023 fn first_order_ladrc_tracks_with_constant_disturbance() {
1024 let dt = 0.001;
1025 let config = ladrc::LadrcFirstOrderConfig::from_bandwidth(dt, 1.0, 18.0, 80.0)
1026 .with_output_limit(OutputLimit::new(-20.0, 20.0));
1027 let mut controller = ladrc::LadrcFirstOrder::new(config).unwrap();
1028
1029 let mut y = 0.0;
1030 for _ in 0..5_000 {
1031 let output = controller.update(1.0, y);
1032 let disturbance = 0.35;
1033 let y_dot = -0.45 * y + output.control + disturbance;
1034 y += dt * y_dot;
1035 }
1036
1037 assert_close(y, 1.0, 0.03);
1038 assert_close(controller.estimate().output, y, 0.03);
1039 }
1040
1041 #[test]
1042 fn second_order_ladrc_tracks_with_model_error_and_disturbance() {
1043 let dt = 0.001;
1044 let config = ladrc::LadrcSecondOrderConfig::from_bandwidth(dt, 1.0, 14.0, 70.0)
1045 .with_output_limit(OutputLimit::new(-30.0, 30.0));
1046 let mut controller = ladrc::LadrcSecondOrder::new(config).unwrap();
1047
1048 let mut y = 0.0;
1049 let mut v = 0.0;
1050
1051 for _ in 0..7_000 {
1052 let output = controller.update(1.0, y);
1053 let acceleration = -1.6 * v - 2.0 * y + output.control + 0.4;
1054 v += dt * acceleration;
1055 y += dt * v;
1056 }
1057
1058 assert_close(y, 1.0, 0.04);
1059 assert_close(v, 0.0, 0.08);
1060 assert_close(controller.estimate().position, y, 0.04);
1061 }
1062
1063 #[test]
1064 fn first_order_ladrc_accepts_explicit_variable_period() {
1065 let config = ladrc::LadrcFirstOrderConfig::from_bandwidth(0.001, 1.0, 18.0, 80.0)
1066 .with_output_limit(OutputLimit::new(-20.0, 20.0));
1067 let mut controller = ladrc::LadrcFirstOrder::new(config).unwrap();
1068 let periods = [0.0007, 0.0012, 0.0009, 0.0015, 0.0010];
1069
1070 let mut y = 0.0;
1071 for step in 0..5_000 {
1072 let dt = periods[step % periods.len()];
1073 let output = controller.update_with_period(dt, 1.0, y).unwrap();
1074 let disturbance = 0.35;
1075 let y_dot = -0.45 * y + output.control + disturbance;
1076 y += dt * y_dot;
1077 }
1078
1079 assert_close(y, 1.0, 0.04);
1080 }
1081
1082 #[test]
1083 fn second_order_ladrc_update_at_handles_variable_periods() {
1084 let config = ladrc::LadrcSecondOrderConfig::from_bandwidth(0.001, 1.0, 14.0, 70.0)
1085 .with_output_limit(OutputLimit::new(-30.0, 30.0));
1086 let mut controller = ladrc::LadrcSecondOrder::new(config).unwrap();
1087 let periods = [0.0007, 0.0013, 0.0009, 0.0011, 0.0015];
1088
1089 let mut now = 0.0;
1090 let mut y = 0.0;
1091 let mut v = 0.0;
1092 controller.reset_at(now, y).unwrap();
1093
1094 for step in 0..7_000 {
1095 let dt = periods[step % periods.len()];
1096 now += dt;
1097 let output = controller.update_at(now, 1.0, y).unwrap();
1098 let acceleration = -1.6 * v - 2.0 * y + output.control + 0.4;
1099 v += dt * acceleration;
1100 y += dt * v;
1101 }
1102
1103 assert_close(y, 1.0, 0.05);
1104 assert_close(v, 0.0, 0.10);
1105 }
1106
1107 #[test]
1108 fn second_order_ladrc_update_at_millis_handles_large_uptime() {
1109 let config = ladrc::LadrcSecondOrderConfig::from_bandwidth(0.001, 1.0, 14.0, 70.0)
1110 .with_output_limit(OutputLimit::new(-30.0, 30.0));
1111 let mut controller = ladrc::LadrcSecondOrder::new(config).unwrap();
1112 let periods_ms = [1_u64, 2, 1, 1, 2];
1113
1114 let mut now_ms = 50_000_000_u64;
1115 let mut y = 0.0;
1116 let mut v = 0.0;
1117 controller.reset_at_millis(now_ms, y);
1118
1119 for step in 0..6_000 {
1120 let dt_ms = periods_ms[step % periods_ms.len()];
1121 let dt = dt_ms as Float * 0.001;
1122 now_ms += dt_ms;
1123
1124 let output = controller.update_at_millis(now_ms, 1.0, y).unwrap();
1125 let acceleration = -1.6 * v - 2.0 * y + output.control + 0.4;
1126 v += dt * acceleration;
1127 y += dt * v;
1128 }
1129
1130 assert_close(y, 1.0, 0.05);
1131 assert_close(v, 0.0, 0.10);
1132 assert_eq!(controller.last_update_at_millis(), Some(now_ms));
1133 }
1134
1135 #[test]
1136 fn update_at_rejects_non_monotonic_time() {
1137 let config = ladrc::LadrcSecondOrderConfig::from_bandwidth(0.001, 1.0, 14.0, 70.0);
1138 let mut controller = ladrc::LadrcSecondOrder::new(config).unwrap();
1139 controller.reset_at(1.0, 0.0).unwrap();
1140
1141 assert_eq!(
1142 controller.update_at(1.0, 1.0, 0.0).unwrap_err(),
1143 ConfigError::NonPositiveSamplePeriod
1144 );
1145 assert_eq!(
1146 controller.update_at(0.9, 1.0, 0.0).unwrap_err(),
1147 ConfigError::NonPositiveSamplePeriod
1148 );
1149 }
1150
1151 #[test]
1152 fn update_at_millis_rejects_non_monotonic_time() {
1153 let config = ladrc::LadrcSecondOrderConfig::from_bandwidth(0.001, 1.0, 14.0, 70.0);
1154 let mut controller = ladrc::LadrcSecondOrder::new(config).unwrap();
1155 controller.reset_at_millis(1_000, 0.0);
1156
1157 assert_eq!(
1158 controller.update_at_millis(1_000, 1.0, 0.0).unwrap_err(),
1159 ConfigError::NonPositiveSamplePeriod
1160 );
1161 assert_eq!(
1162 controller.update_at_millis(999, 1.0, 0.0).unwrap_err(),
1163 ConfigError::NonPositiveSamplePeriod
1164 );
1165 }
1166}