Skip to main content

ladrc_no_std/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4/// Floating-point type used by the crate.
5///
6/// `f32` keeps the implementation compact for microcontrollers and avoids
7/// pulling in generic numeric traits.
8pub type Float = f32;
9
10const MIN_ABS_B0: Float = 1.0e-9;
11
12/// Configuration validation error.
13///
14/// Constructors validate parameters before a controller is created. This avoids
15/// silently running a loop with a zero sample time, zero plant gain, inverted
16/// output limit, or non-finite coefficient.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ConfigError {
19    /// A value must be finite.
20    NonFinite,
21    /// The sample period must be positive.
22    NonPositiveSamplePeriod,
23    /// A bandwidth, gain, tracking speed, or delta must be positive.
24    NonPositiveParameter,
25    /// The plant input gain estimate `b0` is zero or too close to zero.
26    ZeroPlantGain,
27    /// The lower output limit is greater than the upper output limit.
28    InvalidOutputLimit,
29}
30
31/// Optional control output clamp.
32///
33/// Use this to match the command range of the real actuator, for example
34/// `0.0..1.0` for normalized heater power or `-1.0..1.0` for a bidirectional
35/// motor command. The controller still reports the unsaturated value in its
36/// update output, which is useful during tuning.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct OutputLimit {
39    /// Minimum allowed controller output.
40    pub min: Float,
41    /// Maximum allowed controller output.
42    pub max: Float,
43}
44
45impl OutputLimit {
46    /// Creates a new output clamp.
47    #[inline]
48    pub const fn new(min: Float, max: Float) -> Self {
49        Self { min, max }
50    }
51
52    /// Applies the clamp to `value`.
53    #[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    //! Linear active disturbance rejection controllers.
195    //!
196    //! LADRC keeps the model small and estimates the unknown part of the plant
197    //! as one total disturbance. The crate provides first-order and
198    //! second-order controllers with a bandwidth-based configuration helper.
199    //!
200    //! Use [`LadrcFirstOrder`] for plants that are well represented by:
201    //!
202    //! ```text
203    //! y' = f + b0 * u
204    //! ```
205    //!
206    //! Use [`LadrcSecondOrder`] for plants that are well represented by:
207    //!
208    //! ```text
209    //! y'' = f + b0 * u
210    //! ```
211    //!
212    //! In both cases `f` is not modeled explicitly. The extended state observer
213    //! estimates it as `disturbance`, and the control law subtracts that
214    //! estimate before dividing by `b0`.
215    //!
216    //! Typical update order in an application:
217    //!
218    //! ```text
219    //! read sensor -> controller.update(reference, measurement) -> write actuator
220    //! ```
221
222    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    /// Estimated first-order LADRC state.
229    #[derive(Debug, Clone, Copy, Default, PartialEq)]
230    pub struct FirstOrderEstimate {
231        /// Estimated process output.
232        pub output: Float,
233        /// Estimated total disturbance.
234        pub disturbance: Float,
235    }
236
237    /// One first-order LADRC update result.
238    #[derive(Debug, Clone, Copy, PartialEq)]
239    pub struct FirstOrderOutput {
240        /// Saturated control signal.
241        pub control: Float,
242        /// Control before output saturation.
243        pub unsaturated_control: Float,
244        /// Pure feedback term before disturbance compensation.
245        pub feedback: Float,
246        /// Estimated state after the observer update.
247        pub estimate: FirstOrderEstimate,
248    }
249
250    /// First-order LADRC configuration.
251    ///
252    /// This configuration is for processes where the control command mostly
253    /// changes the first derivative of the measured output:
254    ///
255    /// ```text
256    /// y' = f + b0 * u
257    /// ```
258    ///
259    /// Examples include temperature, pressure, flow, and motor speed loops.
260    #[derive(Debug, Clone, Copy, PartialEq)]
261    pub struct LadrcFirstOrderConfig {
262        /// Nominal sampling period in seconds.
263        ///
264        /// Plain `update` uses this value directly. `update_at` uses it only
265        /// for the first call when no previous timestamp is known yet.
266        pub sample_period: Float,
267        /// Estimated plant input gain from command to output rate.
268        ///
269        /// The value may be approximate, but the sign must match the real
270        /// plant. A wrong sign usually makes the closed loop diverge.
271        pub b0: Float,
272        /// Controller feedback gain.
273        pub kp: Float,
274        /// First observer gain for output estimation.
275        pub observer_beta1: Float,
276        /// Second observer gain for total-disturbance estimation.
277        pub observer_beta2: Float,
278        /// Optional output clamp.
279        pub output_limit: Option<OutputLimit>,
280    }
281
282    impl LadrcFirstOrderConfig {
283        /// Creates a configuration from controller and observer bandwidths.
284        ///
285        /// The gain placement is `kp = wc`, `beta1 = 2 * wo`,
286        /// `beta2 = wo^2`.
287        ///
288        /// `controller_bandwidth` controls the closed-loop response speed.
289        /// `observer_bandwidth` controls how fast the observer estimates the
290        /// total disturbance. A common first value is `observer_bandwidth`
291        /// around three to five times `controller_bandwidth`.
292        #[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        /// Returns the same configuration with an output clamp.
310        #[inline]
311        pub const fn with_output_limit(mut self, limit: OutputLimit) -> Self {
312            self.output_limit = Some(limit);
313            self
314        }
315
316        /// Validates all parameters.
317        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    /// First-order linear active disturbance rejection controller.
328    ///
329    /// Call [`LadrcFirstOrder::update`] once per fixed sample period. The
330    /// method reads no hardware; it only consumes the reference and measured
331    /// output and returns the next actuator command.
332    #[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        /// Creates a controller and validates the configuration.
344        #[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        /// Returns the current configuration.
358        #[inline]
359        pub const fn config(&self) -> LadrcFirstOrderConfig {
360            self.config
361        }
362
363        /// Returns the current observer estimate.
364        #[inline]
365        pub const fn estimate(&self) -> FirstOrderEstimate {
366            FirstOrderEstimate {
367                output: self.z1,
368                disturbance: self.z2,
369            }
370        }
371
372        /// Returns the control signal used by the previous observer update.
373        #[inline]
374        pub const fn last_control(&self) -> Float {
375            self.last_control
376        }
377
378        /// Returns the timestamp stored by the last [`LadrcFirstOrder::update_at`]
379        /// call.
380        #[inline]
381        pub const fn last_update_at(&self) -> Option<Float> {
382            self.last_update_at
383        }
384
385        /// Returns the millisecond timestamp stored by the last
386        /// [`LadrcFirstOrder::update_at_millis`] call.
387        #[inline]
388        pub const fn last_update_at_millis(&self) -> Option<u64> {
389            self.last_update_at_millis
390        }
391
392        /// Resets the observer to a measured output and clears disturbance and
393        /// control memory.
394        #[inline]
395        pub fn reset(&mut self, measurement: Float) {
396            self.reset_with(measurement, 0.0, 0.0);
397        }
398
399        /// Resets the observer and initializes the timestamp used by
400        /// [`LadrcFirstOrder::update_at`].
401        ///
402        /// Use this before enabling a variable-period loop. It prevents the
403        /// first `update_at` call from falling back to the nominal
404        /// `config.sample_period`.
405        #[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        /// Resets the observer and initializes the millisecond timestamp used
419        /// by [`LadrcFirstOrder::update_at_millis`].
420        ///
421        /// This is the preferred timestamp API for HAL clocks that return
422        /// integer milliseconds, such as `esp-hal`'s
423        /// `Instant::now().duration_since_epoch().as_millis()`.
424        #[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        /// Resets all controller state.
432        #[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        /// Runs one LADRC sample.
447        ///
448        /// The extended state observer uses the previous saturated control
449        /// signal, then the new control signal is computed and stored for the
450        /// next call.
451        ///
452        /// `reference` and `measurement` must use the same units. The returned
453        /// `control` uses the actuator units implied by `b0`.
454        pub fn update(&mut self, reference: Float, measurement: Float) -> FirstOrderOutput {
455            self.update_unchecked(self.config.sample_period, reference, measurement)
456        }
457
458        /// Runs one LADRC sample with an explicit period in seconds.
459        ///
460        /// Use this when the control loop period is not perfectly constant and
461        /// the application already computed the elapsed time since the previous
462        /// sample.
463        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        /// Runs one LADRC sample at a monotonic timestamp in seconds.
474        ///
475        /// The controller stores the previous timestamp and computes
476        /// `sample_period = now_seconds - previous_now_seconds` internally.
477        /// The first call uses the nominal `config.sample_period` because no
478        /// previous timestamp exists yet. Call [`LadrcFirstOrder::reset_at`] to
479        /// initialize the timestamp before the first variable-period update.
480        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        /// Runs one LADRC sample at a monotonic timestamp in milliseconds.
495        ///
496        /// The controller computes `dt` with integer subtraction first, then
497        /// converts only that short elapsed interval to seconds. This avoids
498        /// losing millisecond precision after long uptime.
499        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    /// Estimated second-order LADRC state.
545    #[derive(Debug, Clone, Copy, Default, PartialEq)]
546    pub struct SecondOrderEstimate {
547        /// Estimated process output.
548        pub position: Float,
549        /// Estimated output derivative.
550        pub velocity: Float,
551        /// Estimated total disturbance.
552        pub disturbance: Float,
553    }
554
555    /// One second-order LADRC update result.
556    #[derive(Debug, Clone, Copy, PartialEq)]
557    pub struct SecondOrderOutput {
558        /// Saturated control signal.
559        pub control: Float,
560        /// Control before output saturation.
561        pub unsaturated_control: Float,
562        /// Pure feedback term before disturbance compensation.
563        pub feedback: Float,
564        /// Estimated state after the observer update.
565        pub estimate: SecondOrderEstimate,
566    }
567
568    /// Second-order LADRC configuration.
569    ///
570    /// This configuration is for processes where the control command mostly
571    /// changes the second derivative of the measured output:
572    ///
573    /// ```text
574    /// y'' = f + b0 * u
575    /// ```
576    ///
577    /// Examples include motor position, robot joint angle, gimbal angle, and
578    /// linear actuator position loops.
579    #[derive(Debug, Clone, Copy, PartialEq)]
580    pub struct LadrcSecondOrderConfig {
581        /// Nominal sampling period in seconds.
582        ///
583        /// Plain `update` uses this value directly. `update_at` uses it only
584        /// for the first call when no previous timestamp is known yet.
585        pub sample_period: Float,
586        /// Estimated plant input gain from command to output acceleration.
587        ///
588        /// The value may be approximate, but the sign must match the real
589        /// plant. A wrong sign usually makes the closed loop diverge.
590        pub b0: Float,
591        /// Proportional-like state feedback gain.
592        pub kp: Float,
593        /// Derivative-like state feedback gain.
594        pub kd: Float,
595        /// First observer gain for output estimation.
596        pub observer_beta1: Float,
597        /// Second observer gain for derivative estimation.
598        pub observer_beta2: Float,
599        /// Third observer gain for total-disturbance estimation.
600        pub observer_beta3: Float,
601        /// Optional output clamp.
602        pub output_limit: Option<OutputLimit>,
603    }
604
605    impl LadrcSecondOrderConfig {
606        /// Creates a configuration from controller and observer bandwidths.
607        ///
608        /// The gain placement is `kp = wc^2`, `kd = 2 * wc`,
609        /// `beta1 = 3 * wo`, `beta2 = 3 * wo^2`, `beta3 = wo^3`.
610        ///
611        /// `controller_bandwidth` controls the closed-loop response speed.
612        /// `observer_bandwidth` controls how fast the observer estimates the
613        /// total disturbance. A common first value is `observer_bandwidth`
614        /// around three to five times `controller_bandwidth`.
615        #[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        /// Returns the same configuration with an output clamp.
635        #[inline]
636        pub const fn with_output_limit(mut self, limit: OutputLimit) -> Self {
637            self.output_limit = Some(limit);
638            self
639        }
640
641        /// Validates all parameters.
642        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    /// Second-order linear active disturbance rejection controller.
655    ///
656    /// This is the recommended default controller for position-like embedded
657    /// plants. Call [`LadrcSecondOrder::update`] once per fixed sample period
658    /// when the reference derivative is zero, or
659    /// [`LadrcSecondOrder::update_with_rate`] when a trajectory generator
660    /// provides both position and velocity references.
661    #[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        /// Creates a controller and validates the configuration.
674        #[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        /// Returns the current configuration.
689        #[inline]
690        pub const fn config(&self) -> LadrcSecondOrderConfig {
691            self.config
692        }
693
694        /// Returns the current observer estimate.
695        #[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        /// Returns the control signal used by the previous observer update.
705        #[inline]
706        pub const fn last_control(&self) -> Float {
707            self.last_control
708        }
709
710        /// Returns the timestamp stored by the last [`LadrcSecondOrder::update_at`]
711        /// call.
712        #[inline]
713        pub const fn last_update_at(&self) -> Option<Float> {
714            self.last_update_at
715        }
716
717        /// Returns the millisecond timestamp stored by the last
718        /// [`LadrcSecondOrder::update_at_millis`] call.
719        #[inline]
720        pub const fn last_update_at_millis(&self) -> Option<u64> {
721            self.last_update_at_millis
722        }
723
724        /// Resets the observer to a measured output and clears derivative,
725        /// disturbance, and control memory.
726        #[inline]
727        pub fn reset(&mut self, measurement: Float) {
728            self.reset_with(measurement, 0.0, 0.0, 0.0);
729        }
730
731        /// Resets the observer and initializes the timestamp used by
732        /// [`LadrcSecondOrder::update_at`].
733        ///
734        /// Use this before enabling a variable-period loop. It prevents the
735        /// first `update_at` call from falling back to the nominal
736        /// `config.sample_period`.
737        #[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        /// Resets the observer and initializes the millisecond timestamp used
751        /// by [`LadrcSecondOrder::update_at_millis`].
752        ///
753        /// This is the preferred timestamp API for HAL clocks that return
754        /// integer milliseconds, such as `esp-hal`'s
755        /// `Instant::now().duration_since_epoch().as_millis()`.
756        #[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        /// Resets all controller state.
764        #[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        /// Runs one LADRC sample with zero reference velocity.
781        ///
782        /// Use this for setpoint control where the desired target is a position
783        /// or angle and the desired final velocity is zero.
784        #[inline]
785        pub fn update(&mut self, reference: Float, measurement: Float) -> SecondOrderOutput {
786            self.update_with_rate(reference, 0.0, measurement)
787        }
788
789        /// Runs one LADRC sample with zero reference velocity and an explicit
790        /// period in seconds.
791        ///
792        /// Use this when the control loop period is not perfectly constant and
793        /// the application already computed the elapsed time since the previous
794        /// sample.
795        #[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        /// Runs one LADRC sample with zero reference velocity at a monotonic
806        /// timestamp in seconds.
807        ///
808        /// The controller stores the previous timestamp and computes
809        /// `sample_period = now_seconds - previous_now_seconds` internally.
810        /// The first call uses the nominal `config.sample_period` because no
811        /// previous timestamp exists yet. Call [`LadrcSecondOrder::reset_at`] to
812        /// initialize the timestamp before the first variable-period update.
813        #[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        /// Runs one LADRC sample with zero reference velocity at a monotonic
824        /// timestamp in milliseconds.
825        ///
826        /// The controller computes `dt` with integer subtraction first, then
827        /// converts only that short elapsed interval to seconds. This avoids
828        /// losing millisecond precision after long uptime.
829        #[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        /// Runs one LADRC sample with an explicit reference velocity.
840        ///
841        /// The extended state observer uses the previous saturated control
842        /// signal, then the new control signal is computed and stored for the
843        /// next call.
844        ///
845        /// Use this when an external trajectory generator provides both
846        /// reference position and reference velocity.
847        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        /// Runs one LADRC sample with explicit period and explicit reference
862        /// velocity.
863        ///
864        /// Use this when an external trajectory generator provides both
865        /// reference position and reference velocity, and the application
866        /// already computed the elapsed time since the previous sample.
867        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        /// Runs one LADRC sample at a monotonic timestamp with explicit
879        /// reference velocity.
880        ///
881        /// The controller stores the previous timestamp and computes the sample
882        /// period internally. Call [`LadrcSecondOrder::reset_at`] before the
883        /// first call if you do not want to use the nominal sample period for
884        /// the first update.
885        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        /// Runs one LADRC sample at a monotonic millisecond timestamp with
901        /// explicit reference velocity.
902        ///
903        /// This is useful with HAL clocks that expose integer milliseconds.
904        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    /// Conventional alias for the second-order LADRC controller.
954    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}