mecha10_behavior_patterns/
subsumption.rs

1//! Subsumption architecture for priority-based control
2//!
3//! This module implements the subsumption architecture pattern where higher priority
4//! behaviors can override lower priority ones. This is particularly useful for
5//! safety-critical systems where safety behaviors must take precedence over task behaviors.
6
7use async_trait::async_trait;
8use mecha10_behavior_runtime::{BehaviorNode, BoxedBehavior, NodeStatus};
9use mecha10_core::Context;
10use serde::{Deserialize, Serialize};
11use tracing::{debug, info};
12
13/// A layer in the subsumption architecture with a priority and behavior.
14#[derive(Debug)]
15pub struct SubsumptionLayer {
16    /// Priority level (higher = more important, executes first)
17    pub priority: u8,
18    /// The behavior to execute for this layer
19    pub behavior: BoxedBehavior,
20    /// Optional name for debugging
21    pub name: Option<String>,
22}
23
24impl SubsumptionLayer {
25    /// Create a new subsumption layer.
26    pub fn new(priority: u8, behavior: BoxedBehavior) -> Self {
27        Self {
28            priority,
29            behavior,
30            name: None,
31        }
32    }
33
34    /// Set the name of this layer.
35    pub fn with_name(mut self, name: impl Into<String>) -> Self {
36        self.name = Some(name.into());
37        self
38    }
39}
40
41/// Subsumption node for priority-based layered control.
42///
43/// This implements the subsumption architecture where behaviors are organized in layers
44/// with different priorities. Higher priority layers can override lower priority ones.
45///
46/// # Behavior
47///
48/// - Layers are executed in order of priority (highest first)
49/// - If a layer returns `Success` or `Running`, execution stops and that status is returned
50/// - If a layer returns `Failure`, the next lower priority layer is tried
51/// - If all layers fail, the node returns `Failure`
52///
53/// # Use Cases
54///
55/// - **Safety-critical systems**: Safety behaviors override task behaviors
56/// - **Reactive control**: Obstacle avoidance overrides navigation
57/// - **Hierarchical control**: High-level planning with low-level reactive layers
58///
59/// # Example
60///
61/// ```rust
62/// use mecha10_behavior_patterns::prelude::*;
63///
64/// # #[derive(Debug)]
65/// # struct EmergencyStopBehavior;
66/// # #[async_trait]
67/// # impl BehaviorNode for EmergencyStopBehavior {
68/// #     async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
69/// #         Ok(NodeStatus::Failure) // No emergency
70/// #     }
71/// # }
72/// # #[derive(Debug)]
73/// # struct AvoidObstacleBehavior;
74/// # #[async_trait]
75/// # impl BehaviorNode for AvoidObstacleBehavior {
76/// #     async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
77/// #         Ok(NodeStatus::Failure) // No obstacle
78/// #     }
79/// # }
80/// # #[derive(Debug)]
81/// # struct NavigateBehavior;
82/// # #[async_trait]
83/// # impl BehaviorNode for NavigateBehavior {
84/// #     async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
85/// #         Ok(NodeStatus::Running)
86/// #     }
87/// # }
88///
89/// let subsumption = SubsumptionNode::new()
90///     .add_layer(10, Box::new(EmergencyStopBehavior))    // Highest priority
91///     .add_layer(5, Box::new(AvoidObstacleBehavior))     // Medium priority
92///     .add_layer(1, Box::new(NavigateBehavior));         // Lowest priority
93/// ```
94#[derive(Debug)]
95pub struct SubsumptionNode {
96    pub layers: Vec<SubsumptionLayer>,
97}
98
99impl SubsumptionNode {
100    /// Create a new empty subsumption node.
101    pub fn new() -> Self {
102        Self { layers: Vec::new() }
103    }
104
105    /// Add a layer with the given priority and behavior.
106    ///
107    /// Layers are automatically sorted by priority (highest first).
108    pub fn add_layer(mut self, priority: u8, behavior: BoxedBehavior) -> Self {
109        self.layers.push(SubsumptionLayer::new(priority, behavior));
110        // Sort by priority descending (highest first)
111        self.layers.sort_by(|a, b| b.priority.cmp(&a.priority));
112        self
113    }
114
115    /// Add a named layer.
116    pub fn add_named_layer(mut self, priority: u8, name: impl Into<String>, behavior: BoxedBehavior) -> Self {
117        self.layers
118            .push(SubsumptionLayer::new(priority, behavior).with_name(name));
119        self.layers.sort_by(|a, b| b.priority.cmp(&a.priority));
120        self
121    }
122
123    /// Get the number of layers.
124    pub fn layer_count(&self) -> usize {
125        self.layers.len()
126    }
127}
128
129impl Default for SubsumptionNode {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135#[async_trait]
136impl BehaviorNode for SubsumptionNode {
137    async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
138        if self.layers.is_empty() {
139            return Ok(NodeStatus::Failure);
140        }
141
142        // Try layers in priority order (highest first)
143        for layer in &mut self.layers {
144            let layer_name = layer.name.as_deref().unwrap_or("unnamed");
145
146            debug!(
147                "Subsumption: trying layer '{}' (priority {})",
148                layer_name, layer.priority
149            );
150
151            let status = layer.behavior.tick(ctx).await?;
152
153            match status {
154                NodeStatus::Success | NodeStatus::Running => {
155                    // This layer takes control
156                    info!(
157                        "Subsumption: layer '{}' (priority {}) taking control with status {}",
158                        layer_name, layer.priority, status
159                    );
160                    return Ok(status);
161                }
162                NodeStatus::Failure => {
163                    // Try next lower priority layer
164                    debug!("Subsumption: layer '{}' failed, trying next layer", layer_name);
165                    continue;
166                }
167            }
168        }
169
170        // All layers failed
171        debug!("Subsumption: all layers failed");
172        Ok(NodeStatus::Failure)
173    }
174
175    async fn reset(&mut self) -> anyhow::Result<()> {
176        for layer in &mut self.layers {
177            layer.behavior.reset().await?;
178        }
179        Ok(())
180    }
181
182    async fn on_init(&mut self, ctx: &Context) -> anyhow::Result<()> {
183        for layer in &mut self.layers {
184            layer.behavior.on_init(ctx).await?;
185        }
186        Ok(())
187    }
188
189    async fn on_terminate(&mut self, ctx: &Context) -> anyhow::Result<()> {
190        for layer in &mut self.layers {
191            layer.behavior.on_terminate(ctx).await?;
192        }
193        Ok(())
194    }
195
196    fn name(&self) -> &str {
197        "subsumption"
198    }
199}
200
201/// Configuration for a subsumption layer (for JSON deserialization).
202#[allow(dead_code)] // Will be used when implementing JSON loading
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct SubsumptionLayerConfig {
205    /// Priority level (higher = more important)
206    pub priority: u8,
207    /// Node type to instantiate
208    pub node: String,
209    /// Optional name for this layer
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub name: Option<String>,
212    /// Configuration for the node
213    #[serde(default)]
214    pub config: serde_json::Value,
215}