Skip to main content

qualia_core_db/modalities/control_feedback/
mod.rs

1// Control Theory & Feedback Modality
2// Provides self-stabilizing agents for power systems and sanctuary management
3
4use crate::NQuin;
5
6// Canonical bit positions live in the FrameLayout ABI (single source of truth).
7pub use crate::frame_layout::{CONTROL_BIT, FEEDBACK_BIT, STABILIZATION_BIT};
8
9// Advanced control algorithms (split per CLAUDE.md §10): adaptive tuning, MPC, MIMO state-space.
10pub mod advanced;
11pub use advanced::{
12    adaptive_gains, mimo_output, mimo_step, mit_rule_adapt, mpc_control, scheduled_gain,
13};
14
15use super::epistemic_boundaries::{
16    degrade_claim_to_socratic, detect_referral_by_severity, detect_referral_trigger,
17    identify_degradation_vector, DegradationVector, ReferralTrigger, SocraticDegradation,
18};
19
20/// Filters definitive claims through the Linguistic Degradation Matrix
21pub fn enforce_linguistic_degradation(claim_quin: &NQuin) -> Option<SocraticDegradation> {
22    let vector = identify_degradation_vector(claim_quin);
23    if vector != DegradationVector::Unknown {
24        return degrade_claim_to_socratic(vector);
25    }
26    None
27}
28
29/// Guided Referral Trigger: the **overriding** gate that runs *before* the Linguistic
30/// Degradation Matrix. If a claim crosses a high-liability threshold — either an explicit
31/// acute-harm / imminent-jeopardy predicate, or an analytical vector whose `metadata`
32/// severity meets the referral floor — it returns an overriding referral that must be
33/// surfaced ahead of any analysis. `None` means "no override; degrade as normal".
34pub fn enforce_guided_referral(claim_quin: &NQuin) -> Option<ReferralTrigger> {
35    // 1. Explicit high-liability predicate.
36    if let Some(t) = detect_referral_trigger(claim_quin) {
37        return Some(t);
38    }
39    // 2. Severity escalation of an otherwise-analytical claim.
40    detect_referral_by_severity(identify_degradation_vector(claim_quin), claim_quin.metadata)
41}
42
43/// Control system state for feedback loops
44#[derive(Debug, Clone)]
45pub struct ControlState {
46    pub setpoint: f64,         // Target value
47    pub process_variable: f64, // Current measured value
48    pub error: f64,            // Difference between setpoint and PV
49    pub integral: f64,         // Accumulated error for integral control
50    pub derivative: f64,       // Rate of change for derivative control
51    pub last_error: f64,       // Previous error for derivative calculation
52    pub last_time: u64,        // Timestamp for derivative calculation
53}
54
55impl ControlState {
56    /// Create a new control state.
57    ///
58    /// `last_time` is initialised to 0 so that the first `update()` call with any
59    /// non-zero `current_time` produces a well-defined `dt`.  Callers that need
60    /// wall-clock alignment should call `state.last_time = current_unix_secs` after
61    /// construction.
62    pub fn new(setpoint: f64, initial_value: f64) -> Self {
63        let error = setpoint - initial_value;
64        Self {
65            setpoint,
66            process_variable: initial_value,
67            error,
68            integral: 0.0,
69            derivative: 0.0,
70            last_error: error,
71            last_time: 0,
72        }
73    }
74
75    /// Update control state with new measurement
76    pub fn update(&mut self, new_value: f64, current_time: u64) {
77        self.process_variable = new_value;
78        self.last_error = self.error;
79        self.error = self.setpoint - new_value;
80
81        // Calculate derivative (rate of change).
82        // Use saturating_sub to avoid u64 underflow panic when current_time is behind
83        // last_time (e.g. in unit tests that supply a synthetic timestamp of 1 while
84        // last_time was initialised from the real wall clock).
85        let dt = current_time.saturating_sub(self.last_time) as f64;
86        if dt > 0.0 {
87            self.derivative = (self.error - self.last_error) / dt;
88            self.integral += self.error * dt;
89        }
90
91        self.last_time = current_time;
92    }
93
94    /// Reset integral term to prevent windup
95    pub fn reset_integral(&mut self) {
96        self.integral = 0.0;
97    }
98}
99
100/// PID controller parameters
101#[derive(Debug, Clone)]
102pub struct PidParameters {
103    pub kp: f64, // Proportional gain
104    pub ki: f64, // Integral gain
105    pub kd: f64, // Derivative gain
106    pub output_min: f64,
107    pub output_max: f64,
108}
109
110impl PidParameters {
111    /// Create conservative PID parameters for power systems
112    pub fn conservative_power_system() -> Self {
113        Self {
114            kp: 0.5,
115            ki: 0.1,
116            kd: 0.05,
117            output_min: 0.0,
118            output_max: 100.0,
119        }
120    }
121
122    /// Create aggressive PID parameters for fast response
123    pub fn aggressive_response() -> Self {
124        Self {
125            kp: 1.0,
126            ki: 0.5,
127            kd: 0.2,
128            output_min: 0.0,
129            output_max: 100.0,
130        }
131    }
132}
133
134/// Feedback controller using PID algorithm
135#[derive(Debug, Clone)]
136pub struct FeedbackController {
137    pub name: String,
138    pub parameters: PidParameters,
139    pub state: ControlState,
140    pub enabled: bool,
141}
142
143impl FeedbackController {
144    /// Create a new feedback controller
145    pub fn new(name: String, setpoint: f64, initial_value: f64, params: PidParameters) -> Self {
146        Self {
147            name,
148            parameters: params,
149            state: ControlState::new(setpoint, initial_value),
150            enabled: true,
151        }
152    }
153
154    /// Compute control output using PID algorithm
155    pub fn compute_output(&mut self) -> f64 {
156        if !self.enabled {
157            return 0.0;
158        }
159
160        // PID calculation: output = Kp*error + Ki*integral + Kd*derivative
161        let proportional = self.parameters.kp * self.state.error;
162        let integral = self.parameters.ki * self.state.integral;
163        let derivative = self.parameters.kd * self.state.derivative;
164
165        let mut output = proportional + integral + derivative;
166
167        // Clamp output to limits
168        output = output.clamp(self.parameters.output_min, self.parameters.output_max);
169
170        // Anti-windup: reset integral if output is saturated
171        if output >= self.parameters.output_max && self.state.error > 0.0 {
172            self.state.reset_integral();
173        } else if output <= self.parameters.output_min && self.state.error < 0.0 {
174            self.state.reset_integral();
175        }
176
177        output
178    }
179
180    /// Update controller with new measurement
181    pub fn update(&mut self, new_value: f64) -> f64 {
182        let current_time = std::time::SystemTime::now()
183            .duration_since(std::time::UNIX_EPOCH)
184            .unwrap_or_default()
185            .as_secs();
186
187        self.state.update(new_value, current_time);
188        self.compute_output()
189    }
190
191    /// Change setpoint
192    pub fn set_setpoint(&mut self, new_setpoint: f64) {
193        self.state.setpoint = new_setpoint;
194        self.state.error = new_setpoint - self.state.process_variable;
195    }
196
197    /// Enable/disable controller
198    pub fn set_enabled(&mut self, enabled: bool) {
199        self.enabled = enabled;
200        if !enabled {
201            self.state.reset_integral();
202        }
203    }
204}
205
206/// Power system controller for 12V battery management
207#[derive(Debug, Clone)]
208pub struct PowerSystemController {
209    pub battery_voltage_controller: FeedbackController,
210    pub solar_current_controller: FeedbackController,
211    pub load_balance_controller: FeedbackController,
212    pub system_state: PowerSystemState,
213}
214
215#[derive(Debug, Clone)]
216pub struct PowerSystemState {
217    pub battery_voltage: f64,     // Volts
218    pub solar_current: f64,       // Amps
219    pub load_current: f64,        // Amps
220    pub battery_soc: f64,         // State of charge (0-100%)
221    pub solar_irradiance: f64,    // W/m²
222    pub ambient_temperature: f64, // Celsius
223}
224
225impl PowerSystemController {
226    /// Create a new power system controller for Jayco Songbird setup
227    pub fn new() -> Self {
228        // Battery voltage controller (target 12.6V for LiFePO4)
229        let battery_voltage_controller = FeedbackController::new(
230            "Battery Voltage".to_string(),
231            12.6, // Target voltage for LiFePO4
232            12.5, // Initial reading
233            PidParameters::conservative_power_system(),
234        );
235
236        // Solar current controller (target based on irradiance)
237        let solar_current_controller = FeedbackController::new(
238            "Solar Current".to_string(),
239            10.0, // Target current (will be updated based on conditions)
240            5.0,  // Initial reading
241            PidParameters::aggressive_response(),
242        );
243
244        // Load balance controller (target 0 net current)
245        let load_balance_controller = FeedbackController::new(
246            "Load Balance".to_string(),
247            0.0, // Target: balanced current
248            2.0, // Initial load current
249            PidParameters::conservative_power_system(),
250        );
251
252        Self {
253            battery_voltage_controller,
254            solar_current_controller,
255            load_balance_controller,
256            system_state: PowerSystemState {
257                battery_voltage: 12.5,
258                solar_current: 5.0,
259                load_current: 2.0,
260                battery_soc: 80.0,
261                solar_irradiance: 800.0,
262                ambient_temperature: 25.0,
263            },
264        }
265    }
266
267    /// Update all controllers with current measurements
268    pub fn update(&mut self, measurements: PowerSystemState) -> Vec<ControlAction> {
269        self.system_state = measurements.clone();
270
271        let mut actions = Vec::new();
272
273        // Update battery voltage controller
274        let voltage_output = self
275            .battery_voltage_controller
276            .update(measurements.battery_voltage);
277        if voltage_output > 50.0 {
278            actions.push(ControlAction::IncreaseCharging);
279        } else if voltage_output < -50.0 {
280            actions.push(ControlAction::DecreaseCharging);
281        }
282
283        // Update solar current controller based on irradiance
284        let solar_target = self.calculate_solar_target(measurements.solar_irradiance);
285        self.solar_current_controller.set_setpoint(solar_target);
286        let solar_output = self
287            .solar_current_controller
288            .update(measurements.solar_current);
289        if solar_output > 50.0 {
290            actions.push(ControlAction::IncreaseSolarOutput);
291        } else if solar_output < -50.0 {
292            actions.push(ControlAction::DecreaseSolarOutput);
293        }
294
295        // Update load balance controller
296        let net_current = measurements.solar_current - measurements.load_current;
297        let balance_output = self.load_balance_controller.update(net_current);
298        if balance_output > 50.0 {
299            actions.push(ControlAction::ReduceLoad);
300        } else if balance_output < -50.0 {
301            actions.push(ControlAction::IncreaseLoad);
302        }
303
304        actions
305    }
306
307    /// Calculate optimal solar current target based on irradiance
308    fn calculate_solar_target(&self, irradiance: f64) -> f64 {
309        // Simple linear model: max 20A at 1000 W/m²
310        let max_current = 20.0;
311        let efficiency_factor = 0.85; // Account for system losses
312        (irradiance / 1000.0) * max_current * efficiency_factor
313    }
314
315    /// Get system health status
316    pub fn get_health_status(&self) -> SystemHealth {
317        let voltage_error = (self.system_state.battery_voltage - 12.6).abs();
318        let soc_low = self.system_state.battery_soc < 20.0;
319        let high_temp = self.system_state.ambient_temperature > 35.0;
320
321        // Warning: any deviation > 0.5 V (≈ 4% on a 12.6 V LiFePO4 cell) is significant,
322        // as is low SoC or high ambient temperature.
323        // Caution: smaller deviations between 0.2 V and 0.5 V.
324        if voltage_error > 0.5 || soc_low || high_temp {
325            SystemHealth::Warning
326        } else if voltage_error > 0.2 {
327            SystemHealth::Caution
328        } else {
329            SystemHealth::Good
330        }
331    }
332}
333
334/// Control actions for power system
335#[derive(Debug, Clone, PartialEq)]
336pub enum ControlAction {
337    IncreaseCharging,
338    DecreaseCharging,
339    IncreaseSolarOutput,
340    DecreaseSolarOutput,
341    IncreaseLoad,
342    ReduceLoad,
343    EmergencyShutdown,
344}
345
346/// System health status
347#[derive(Debug, Clone, PartialEq)]
348pub enum SystemHealth {
349    Good,
350    Caution,
351    Warning,
352    Critical,
353}
354
355/// Sanctuary perimeter controller for geofencing
356#[derive(Debug, Clone)]
357pub struct SanctuaryController {
358    pub perimeter_controller: FeedbackController,
359    pub intrusion_detection: bool,
360    pub sanctuary_radius: f64, // meters
361    pub current_position: (f64, f64),
362}
363
364impl SanctuaryController {
365    /// Create a new sanctuary controller
366    pub fn new(radius: f64) -> Self {
367        Self {
368            perimeter_controller: FeedbackController::new(
369                "Sanctuary Perimeter".to_string(),
370                radius,        // Target: stay within radius
371                radius - 10.0, // Current position (slightly inside)
372                PidParameters::conservative_power_system(),
373            ),
374            intrusion_detection: true,
375            sanctuary_radius: radius,
376            current_position: (0.0, 0.0),
377        }
378    }
379
380    /// Update sanctuary controller with current position
381    pub fn update(&mut self, position: (f64, f64)) -> Vec<SanctuaryAction> {
382        self.current_position = position;
383
384        // Calculate distance from center
385        let distance = (position.0.powi(2) + position.1.powi(2)).sqrt();
386
387        // Update controller (negative error means outside perimeter)
388        let error = self.sanctuary_radius - distance;
389        let output = self.perimeter_controller.update(error);
390
391        let mut actions = Vec::new();
392
393        if distance > self.sanctuary_radius {
394            actions.push(SanctuaryAction::IntrusionAlert);
395            if output > 70.0 {
396                actions.push(SanctuaryAction::ReturnToPerimeter);
397            }
398        } else if distance < self.sanctuary_radius * 0.5 {
399            actions.push(SanctuaryAction::NearingCenter);
400        }
401
402        actions
403    }
404}
405
406/// Sanctuary control actions
407#[derive(Debug, Clone, PartialEq)]
408pub enum SanctuaryAction {
409    IntrusionAlert,
410    ReturnToPerimeter,
411    NearingCenter,
412    PerimeterBreach,
413}
414
415/// Convert controller state to NQuin for storage
416pub fn controller_to_quin(controller: &FeedbackController, context: u64) -> NQuin {
417    let mut quin = NQuin {
418        subject: crate::q_hash(&controller.name),
419        predicate: crate::q_hash("has_control_state"),
420        object: ((controller.state.setpoint * 1000.0) as u64) << 32
421            | ((controller.state.process_variable * 1000.0) as u64 & 0xFFFFFFFF),
422        context,
423        metadata: CONTROL_BIT | if controller.enabled { 1 } else { 0 },
424        parity: 0,
425    };
426    quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
427    quin
428}
429
430/// Create a power management scenario for testing
431pub fn create_power_scenario() -> PowerSystemController {
432    let mut controller = PowerSystemController::new();
433
434    // Simulate high solar irradiance scenario
435    let measurements = PowerSystemState {
436        battery_voltage: 13.2,     // Slightly high (overcharging)
437        solar_current: 15.0,       // Good solar output
438        load_current: 8.0,         // Moderate load
439        battery_soc: 85.0,         // Good charge level
440        solar_irradiance: 900.0,   // Bright sun
441        ambient_temperature: 30.0, // Warm day
442    };
443
444    let actions = controller.update(measurements);
445    println!("Power actions: {:?}", actions);
446
447    controller
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_pid_controller() {
456        let mut controller = FeedbackController::new(
457            "Test".to_string(),
458            10.0,
459            8.0,
460            PidParameters::conservative_power_system(),
461        );
462
463        // First update
464        let output1 = controller.update(8.0);
465        assert!(output1 > 0.0); // Should increase output to reach setpoint
466
467        // Second update (closer to setpoint)
468        let output2 = controller.update(9.0);
469        assert!(output2 < output1); // Output should decrease as error reduces
470    }
471
472    #[test]
473    fn test_power_system_controller() {
474        let mut controller = PowerSystemController::new();
475
476        let measurements = PowerSystemState {
477            battery_voltage: 12.0, // Low voltage
478            solar_current: 5.0,
479            load_current: 10.0,
480            battery_soc: 60.0,
481            solar_irradiance: 600.0,
482            ambient_temperature: 20.0,
483        };
484
485        let actions = controller.update(measurements);
486        assert!(!actions.is_empty());
487
488        let health = controller.get_health_status();
489        assert_eq!(health, SystemHealth::Warning); // Low voltage should trigger warning
490    }
491
492    #[test]
493    fn test_sanctuary_controller() {
494        let mut controller = SanctuaryController::new(100.0);
495
496        // Test position inside sanctuary
497        let actions1 = controller.update((50.0, 50.0));
498        assert!(actions1.is_empty());
499
500        // Test position outside sanctuary
501        let actions2 = controller.update((110.0, 110.0));
502        assert!(actions2.contains(&SanctuaryAction::IntrusionAlert));
503    }
504
505    #[test]
506    fn test_control_state_update() {
507        let mut state = ControlState::new(10.0, 8.0);
508        assert_eq!(state.error, 2.0);
509
510        state.update(9.0, 1);
511        assert_eq!(state.error, 1.0);
512        assert_eq!(state.process_variable, 9.0);
513        assert!(state.integral > 0.0); // Should accumulate error
514    }
515}