Skip to main content

dsfb_robotics/
platform.rs

1//! Robot operating context for envelope scaling and violation suppression.
2//!
3//! Residuals observed during commissioning, calibration, or planned
4//! maintenance are **not** violations — they are expected. The
5//! [`RobotContext`] encodes which operating regime the residual stream
6//! is in, and the [`RobotContext::admissibility_multiplier`] method
7//! returns the scaling factor DSFB applies to the envelope radius ρ.
8//!
9//! During commissioning and maintenance the multiplier is `+∞`, which
10//! makes envelope violations structurally impossible — the grammar FSM
11//! is forced to `Admissible` regardless of residual magnitude.
12
13/// The robot's current operating regime.
14///
15/// Set by the caller from whatever state source is authoritative for
16/// the deployment (ROS 2 `mode` topic, OPC UA `OperationalStatus`
17/// node, programmable-logic flag, manual operator switch). DSFB never
18/// transitions between contexts on its own — it is an observer.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum RobotContext {
22    /// Arm is being commissioned: dynamic parameter identification in
23    /// progress, friction tables being populated, excitation
24    /// trajectories being executed. Residuals are expected to be large
25    /// and non-stationary. Violations are suppressed.
26    ArmCommissioning,
27
28    /// Arm is in normal operation: trajectory tracking, force control,
29    /// teleoperation. Residuals are expected to be small and stationary
30    /// within the calibrated envelope. Full violation enforcement.
31    ArmOperating,
32
33    /// Legged platform is in a stance phase: at least one end effector
34    /// in contact with the ground, contact forces actively controlled
35    /// by the whole-body controller. Residuals include MPC force
36    /// tracking error and centroidal-momentum estimator discrepancy.
37    /// Full violation enforcement.
38    LeggedStance,
39
40    /// Legged platform is in a swing phase: at least one end effector
41    /// off the ground, swing-foot trajectory tracking active. Contact
42    /// residuals are not applicable; only joint-level kinematic
43    /// residuals are enforced. Full violation enforcement with a
44    /// relaxed envelope (swing-phase residuals are typically looser
45    /// than stance-phase).
46    LeggedSwing,
47
48    /// Planned maintenance: operator-initiated diagnostics or
49    /// mechanical service. Residuals may be very large as the robot is
50    /// deliberately moved through unusual configurations. Violations
51    /// are suppressed.
52    Maintenance,
53}
54
55impl RobotContext {
56    /// Returns `true` if violations are suppressed in this context.
57    ///
58    /// Commissioning and maintenance periods both produce residual
59    /// patterns that look like faults to a naive observer but are
60    /// expected by design. DSFB recognises these contexts and holds
61    /// the grammar FSM in `Admissible`.
62    #[inline]
63    #[must_use]
64    pub const fn is_suppressed(self) -> bool {
65        matches!(self, Self::ArmCommissioning | Self::Maintenance)
66    }
67
68    /// Multiplier applied to the envelope radius ρ in this context.
69    ///
70    /// - `ArmOperating`, `LeggedStance`: `1.0` (baseline envelope).
71    /// - `LeggedSwing`: `1.5` (swing residuals run wider than stance).
72    /// - `ArmCommissioning`, `Maintenance`: `f64::INFINITY` (no
73    ///   violation possible — residuals are expected to be arbitrary).
74    ///
75    /// The choice of `1.5` for swing is intentionally modest; the
76    /// caller may override with a custom envelope per-phase if a
77    /// tighter bound is known from the controller.
78    #[inline]
79    #[must_use]
80    pub fn admissibility_multiplier(self) -> f64 {
81        match self {
82            Self::ArmOperating | Self::LeggedStance => 1.0,
83            Self::LeggedSwing => 1.5,
84            Self::ArmCommissioning | Self::Maintenance => f64::INFINITY,
85        }
86    }
87
88    /// Short stable string label, for logging and JSON emission.
89    #[inline]
90    #[must_use]
91    pub const fn label(self) -> &'static str {
92        match self {
93            Self::ArmCommissioning => "ArmCommissioning",
94            Self::ArmOperating => "ArmOperating",
95            Self::LeggedStance => "LeggedStance",
96            Self::LeggedSwing => "LeggedSwing",
97            Self::Maintenance => "Maintenance",
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn suppressed_contexts_are_commissioning_and_maintenance() {
108        assert!(RobotContext::ArmCommissioning.is_suppressed());
109        assert!(RobotContext::Maintenance.is_suppressed());
110        assert!(!RobotContext::ArmOperating.is_suppressed());
111        assert!(!RobotContext::LeggedStance.is_suppressed());
112        assert!(!RobotContext::LeggedSwing.is_suppressed());
113    }
114
115    #[test]
116    fn multiplier_matches_contract() {
117        assert_eq!(RobotContext::ArmOperating.admissibility_multiplier(), 1.0);
118        assert_eq!(RobotContext::LeggedStance.admissibility_multiplier(), 1.0);
119        assert_eq!(RobotContext::LeggedSwing.admissibility_multiplier(), 1.5);
120        assert!(RobotContext::ArmCommissioning.admissibility_multiplier().is_infinite());
121        assert!(RobotContext::Maintenance.admissibility_multiplier().is_infinite());
122    }
123
124    #[test]
125    fn labels_are_stable_and_unique() {
126        let labels = [
127            RobotContext::ArmCommissioning.label(),
128            RobotContext::ArmOperating.label(),
129            RobotContext::LeggedStance.label(),
130            RobotContext::LeggedSwing.label(),
131            RobotContext::Maintenance.label(),
132        ];
133        for (i, a) in labels.iter().enumerate() {
134            for (j, b) in labels.iter().enumerate() {
135                if i != j {
136                    assert_ne!(a, b, "labels must be unique");
137                }
138            }
139        }
140    }
141}