Skip to main content

qualia_core_db/inference/ambient_orchestration/
power.rs

1//! Power / thermal / battery governance: PowerManager and its monitors.
2
3use super::*;
4use std::time::{Duration, Instant};
5
6/// Power manager
7pub struct PowerManager {
8    power_policy: PowerPolicy,
9    pub(super) battery_monitor: BatteryMonitor,
10    thermal_monitor: ThermalMonitor,
11    power_optimizer: PowerOptimizer,
12    /// Current orchestration state, used to estimate power when no platform
13    /// power API is available.
14    orchestration_state: AmbientOrchestrationState,
15    /// Number of models currently loaded/active on the managed device.
16    active_model_count: usize,
17}
18
19/// Battery monitor
20pub struct BatteryMonitor {
21    pub(super) current_level: f64,
22    voltage: f64,
23    temperature: f64,
24    health: f64,
25    charging: bool,
26    estimated_time_remaining: Duration,
27}
28
29/// Thermal monitor
30pub struct ThermalMonitor {
31    cpu_temperature: f64,
32    gpu_temperature: f64,
33    battery_temperature: f64,
34    ambient_temperature: f64,
35    thermal_state: ThermalState,
36}
37
38/// Power optimizer
39pub struct PowerOptimizer {
40    optimization_algorithm: OptimizationAlgorithm,
41    optimization_history: Vec<OptimizationRecord>,
42    target_efficiency: f64,
43}
44
45impl PowerManager {
46    /// Create new power manager
47    pub fn new() -> Self {
48        Self {
49            power_policy: PowerPolicy::Balanced,
50            battery_monitor: BatteryMonitor::new(),
51            thermal_monitor: ThermalMonitor::new(),
52            power_optimizer: PowerOptimizer::new(),
53            orchestration_state: AmbientOrchestrationState::Idle,
54            active_model_count: 0,
55        }
56    }
57
58    /// Set the current orchestration state so power estimates track the
59    /// real state machine in `orchestrator.rs` (`ModelLifecycle`).
60    pub fn set_orchestration_state(&mut self, state: AmbientOrchestrationState) {
61        self.orchestration_state = state;
62    }
63
64    /// Set the number of models currently active on the managed device.
65    pub fn set_active_model_count(&mut self, count: usize) {
66        self.active_model_count = count;
67    }
68
69    /// Check if device can execute task
70    pub fn can_execute(&self, _device: &AmbientDevice) -> bool {
71        let battery_level = self.battery_monitor.current_level;
72        let thermal_state = &self.thermal_monitor.thermal_state;
73
74        battery_level > 20.0 && *thermal_state != ThermalState::Critical
75    }
76
77    /// Update power consumption after executing a task on a device.
78    ///
79    /// Uses the power optimizer to record the power state transition and
80    /// the power policy to determine the power budget.
81    pub fn update_power_consumption(
82        &mut self,
83        device: &mut AmbientDevice,
84        execution_time: Duration,
85    ) {
86        let power_consumed = device.power_profile.active_power * execution_time.as_secs_f64();
87
88        // Drain battery based on power consumed.
89        self.battery_monitor.drain(power_consumed, execution_time);
90
91        // Apply thermal impact from the power draw.
92        self.thermal_monitor
93            .apply_heat(power_consumed, execution_time);
94
95        // Record the optimization state transition.
96        let input_state = PowerState {
97            power_consumption: power_consumed,
98            performance: device.performance_profile.sustainable_performance,
99            efficiency: if power_consumed > 0.0 {
100                device.performance_profile.sustainable_performance / power_consumed
101            } else {
102                0.0
103            },
104            thermal_state: self.thermal_monitor.state(),
105            battery_level: self.battery_monitor.level(),
106        };
107        self.power_optimizer.optimize(&input_state);
108    }
109
110    /// Get the current power policy.
111    pub fn power_policy(&self) -> &PowerPolicy {
112        &self.power_policy
113    }
114
115    /// Set the power policy.
116    pub fn set_power_policy(&mut self, policy: PowerPolicy) {
117        self.power_policy = policy;
118    }
119
120    /// Get a mutable reference to the power optimizer for direct optimization.
121    pub fn power_optimizer_mut(&mut self) -> &mut PowerOptimizer {
122        &mut self.power_optimizer
123    }
124
125    /// Get a mutable reference to the battery monitor for updates.
126    pub fn battery_monitor_mut(&mut self) -> &mut BatteryMonitor {
127        &mut self.battery_monitor
128    }
129
130    /// Get a mutable reference to the thermal monitor for updates.
131    pub fn thermal_monitor_mut(&mut self) -> &mut ThermalMonitor {
132        &mut self.thermal_monitor
133    }
134
135    /// Get battery level
136    pub fn get_battery_level(&self, _device_id: &str) -> f64 {
137        self.battery_monitor.current_level
138    }
139
140    /// Get thermal state derived from the current estimated power draw.
141    ///
142    /// Power-to-thermal mapping (mobile SoC heuristic):
143    /// - `< 3W`  → `Normal` (cool)
144    /// - `3–7W`  → `Warm`
145    /// - `> 7W`  → `Critical`
146    pub fn get_thermal_state(&self, device_id: &str) -> ThermalState {
147        let power = self.get_power_consumption(device_id);
148        if power > 7.0 {
149            ThermalState::Critical
150        } else if power >= 3.0 {
151            ThermalState::Warm
152        } else {
153            ThermalState::Normal
154        }
155    }
156
157    /// Get power consumption in watts.
158    ///
159    /// On platforms exposing a power API (e.g. RAPL on Intel, the Energy
160    /// Meter on Android) this would query the hardware. On every other target
161    /// we estimate consumption from the current orchestration state and the
162    /// number of active models, which is what battery-aware ML scheduling and
163    /// thermal management rely on:
164    ///
165    /// | State            | Base power |
166    /// |------------------|------------|
167    /// | Idle             | ~0.5 W     |
168    /// | Active inference | ~5.0 W + (active_models × 2.0 W) |
169    /// | Scrubbing        | ~3.0 W     |
170    /// | Streaming        | ~4.0 W     |
171    pub fn get_power_consumption(&self, device_id: &str) -> f64 {
172        // NOTE: a real implementation would probe `/sys/class/powercap/` (RAPL),
173        // `android.os.PowerManager` via JNI, or the CoreML energy log. Until a
174        // platform power API is wired in, estimate from the orchestration state.
175        let _ = device_id; // hardware query would be keyed on this id
176        match self.orchestration_state {
177            AmbientOrchestrationState::Idle => 0.5,
178            AmbientOrchestrationState::ActiveInference => {
179                5.0 + (self.active_model_count as f64) * 2.0
180            }
181            AmbientOrchestrationState::Scrubbing => 3.0,
182            AmbientOrchestrationState::Streaming => 4.0,
183        }
184    }
185
186    /// Estimate battery life remaining in hours.
187    ///
188    /// `hours = (battery_capacity_wh * current_battery_pct / 100.0) / power_consumption`
189    ///
190    /// Returns `0.0` if the estimated power consumption is zero (avoids
191    /// division by zero) or if the battery percentage is non-positive.
192    pub fn estimate_battery_life_remaining(
193        &self,
194        current_battery_pct: f64,
195        battery_capacity_wh: f64,
196    ) -> f64 {
197        if current_battery_pct <= 0.0 || battery_capacity_wh <= 0.0 {
198            return 0.0;
199        }
200        let power = self.get_power_consumption("");
201        if power <= 0.0 {
202            return 0.0;
203        }
204        (battery_capacity_wh * current_battery_pct / 100.0) / power
205    }
206
207    /// Decide whether inference should be throttled.
208    ///
209    /// Returns `true` when the thermal state is `Critical` or when the
210    /// estimated battery life (using the battery monitor's current charge
211    /// against a 15 Wh mobile battery as a reasonable default) drops below
212    /// 1 hour.
213    pub fn should_throttle_inference(&self) -> bool {
214        let thermal = self.get_thermal_state("");
215        if thermal == ThermalState::Critical {
216            return true;
217        }
218        // Reasonable mobile default: 15 Wh battery. Use the battery monitor's
219        // current charge level so real battery drain drives the decision.
220        let battery_pct = self.battery_monitor.current_level;
221        if battery_pct <= 0.0 {
222            return true; // No battery left — must throttle.
223        }
224        let estimated_hours = self.estimate_battery_life_remaining(battery_pct, 15.0);
225        estimated_hours < 1.0
226    }
227
228    /// Aggregate the current power/thermal/battery snapshot.
229    ///
230    /// `estimated_battery_hours` is `Some` when a non-zero battery capacity is
231    /// known; here we use the battery monitor's current level against a 15 Wh
232    /// mobile battery default. Returns `None` when the device has no battery
233    /// (e.g. mains-powered embedded host).
234    pub fn get_power_metrics(&self) -> PowerMetrics {
235        let current_power_w = self.get_power_consumption("");
236        let thermal_state = self.get_thermal_state("");
237
238        // The battery monitor tracks a 0–100 percentage. Use a 15 Wh mobile
239        // battery as the default capacity when one is present.
240        let battery_pct = self.battery_monitor.current_level;
241        let estimated_battery_hours = if battery_pct > 0.0 {
242            let hours = self.estimate_battery_life_remaining(battery_pct, 15.0);
243            if hours > 0.0 {
244                Some(hours)
245            } else {
246                None
247            }
248        } else {
249            None
250        };
251
252        PowerMetrics {
253            current_power_w,
254            thermal_state,
255            estimated_battery_hours,
256            active_model_count: self.active_model_count,
257        }
258    }
259}
260
261impl BatteryMonitor {
262    pub fn new() -> Self {
263        Self {
264            current_level: 100.0,
265            voltage: 3.7,
266            temperature: 25.0,
267            health: 100.0,
268            charging: false,
269            estimated_time_remaining: Duration::from_secs(3600 * 10), // 10 hours
270        }
271    }
272
273    /// Current battery level as a percentage (0–100).
274    pub fn level(&self) -> f64 {
275        self.current_level
276    }
277
278    /// Battery voltage in volts.
279    pub fn voltage(&self) -> f64 {
280        self.voltage
281    }
282
283    /// Battery temperature in degrees Celsius.
284    pub fn temperature(&self) -> f64 {
285        self.temperature
286    }
287
288    /// Battery health as a percentage (0–100, where 100 = new).
289    pub fn health(&self) -> f64 {
290        self.health
291    }
292
293    /// Whether the battery is currently charging.
294    pub fn is_charging(&self) -> bool {
295        self.charging
296    }
297
298    /// Estimated time remaining until the battery is depleted.
299    pub fn time_remaining(&self) -> Duration {
300        self.estimated_time_remaining
301    }
302
303    /// Update the battery state from platform telemetry.
304    pub fn update(&mut self, level: f64, voltage: f64, temperature: f64, charging: bool) {
305        self.current_level = level.clamp(0.0, 100.0);
306        self.voltage = voltage;
307        self.temperature = temperature;
308        self.charging = charging;
309        // Estimate time remaining based on current drain rate.
310        // A simple linear model: if not charging, estimate from level and
311        // a nominal drain of 10%/hour for active use.
312        if !charging && level > 0.0 {
313            let hours = level / 10.0;
314            self.estimated_time_remaining = Duration::from_secs((hours * 3600.0) as u64);
315        } else if charging {
316            // While charging, estimate time to full at ~20%/hour charge rate.
317            let hours_to_full = (100.0 - level) / 20.0;
318            self.estimated_time_remaining = Duration::from_secs((hours_to_full * 3600.0) as u64);
319        }
320    }
321
322    /// Apply battery drain from a computation that consumed `power_w` watts
323    /// for `duration`. Uses a nominal 15 Wh battery capacity.
324    pub fn drain(&mut self, power_w: f64, duration: Duration) {
325        if self.charging {
326            return; // No drain while charging.
327        }
328        let wh_consumed = power_w * duration.as_secs_f64() / 3600.0;
329        let battery_capacity_wh = 15.0;
330        let pct_drained = (wh_consumed / battery_capacity_wh) * 100.0;
331        self.current_level = (self.current_level - pct_drained).max(0.0);
332        // Update temperature estimate from power draw.
333        self.temperature += power_w * 0.5 * duration.as_secs_f64();
334    }
335}
336
337impl ThermalMonitor {
338    pub fn new() -> Self {
339        Self {
340            cpu_temperature: 45.0,
341            gpu_temperature: 40.0,
342            battery_temperature: 30.0,
343            ambient_temperature: 25.0,
344            thermal_state: ThermalState::Normal,
345        }
346    }
347
348    /// CPU temperature in degrees Celsius.
349    pub fn cpu_temp(&self) -> f64 {
350        self.cpu_temperature
351    }
352
353    /// GPU temperature in degrees Celsius.
354    pub fn gpu_temp(&self) -> f64 {
355        self.gpu_temperature
356    }
357
358    /// Battery temperature in degrees Celsius.
359    pub fn battery_temp(&self) -> f64 {
360        self.battery_temperature
361    }
362
363    /// Ambient (environmental) temperature in degrees Celsius.
364    pub fn ambient_temp(&self) -> f64 {
365        self.ambient_temperature
366    }
367
368    /// Current thermal state classification.
369    pub fn state(&self) -> ThermalState {
370        self.thermal_state
371    }
372
373    /// Update thermal readings from platform sensors and reclassify the
374    /// thermal state.
375    ///
376    /// State thresholds (mobile SoC heuristic):
377    /// - CPU < 50°C → `Normal`
378    /// - CPU 50–70°C → `Warm`
379    /// - CPU 70–85°C → `Hot`
380    /// - CPU > 85°C → `Critical`
381    pub fn update(&mut self, cpu: f64, gpu: f64, battery: f64, ambient: f64) {
382        self.cpu_temperature = cpu;
383        self.gpu_temperature = gpu;
384        self.battery_temperature = battery;
385        self.ambient_temperature = ambient;
386        self.thermal_state = if cpu > 85.0 {
387            ThermalState::Critical
388        } else if cpu > 70.0 {
389            ThermalState::Hot
390        } else if cpu > 50.0 {
391            ThermalState::Warm
392        } else {
393            ThermalState::Normal
394        };
395    }
396
397    /// Apply thermal impact from a computation that drew `power_w` watts
398    /// for `duration`. Increases CPU/GPU temperatures proportionally.
399    pub fn apply_heat(&mut self, power_w: f64, duration: Duration) {
400        let secs = duration.as_secs_f64();
401        // Each watt for 1 second raises CPU temp by ~0.1°C (simplified model).
402        self.cpu_temperature += power_w * 0.1 * secs;
403        self.gpu_temperature += power_w * 0.08 * secs;
404        // Reclassify state after heating.
405        self.thermal_state = if self.cpu_temperature > 85.0 {
406            ThermalState::Critical
407        } else if self.cpu_temperature > 70.0 {
408            ThermalState::Hot
409        } else if self.cpu_temperature > 50.0 {
410            ThermalState::Warm
411        } else {
412            ThermalState::Normal
413        };
414    }
415
416    /// Cool down toward ambient temperature. Called during idle periods.
417    pub fn cool(&mut self, duration: Duration) {
418        let secs = duration.as_secs_f64();
419        // Cool at ~0.5°C/s toward ambient.
420        let cooling = 0.5 * secs;
421        self.cpu_temperature =
422            self.cpu_temperature.max(self.ambient_temperature + cooling) - cooling;
423        self.gpu_temperature =
424            self.gpu_temperature.max(self.ambient_temperature + cooling) - cooling;
425        // Reclassify state after cooling.
426        self.thermal_state = if self.cpu_temperature > 85.0 {
427            ThermalState::Critical
428        } else if self.cpu_temperature > 70.0 {
429            ThermalState::Hot
430        } else if self.cpu_temperature > 50.0 {
431            ThermalState::Warm
432        } else {
433            ThermalState::Normal
434        };
435    }
436}
437
438impl PowerOptimizer {
439    pub fn new() -> Self {
440        Self {
441            optimization_algorithm: OptimizationAlgorithm::Greedy,
442            optimization_history: Vec::new(),
443            target_efficiency: 0.85,
444        }
445    }
446
447    /// Optimize the power state to approach the target efficiency.
448    ///
449    /// Returns the optimized power state. The optimization algorithm
450    /// determines the strategy:
451    /// - `Greedy`: picks the lowest-power state that meets the target efficiency.
452    /// - `Genetic`/`SimulatedAnnealing`/`ReinforcementLearning`: uses the same
453    ///   greedy heuristic but records the decision for future learning.
454    pub fn optimize(&mut self, input: &PowerState) -> PowerState {
455        let mut output = input.clone();
456
457        // Greedy: if efficiency is below target, reduce power consumption.
458        if output.efficiency < self.target_efficiency {
459            // Reduce power by 20% and see if efficiency improves.
460            output.power_consumption *= 0.8;
461            // Recalculate efficiency as performance per watt.
462            if output.power_consumption > 0.0 {
463                output.efficiency = output.performance / output.power_consumption;
464            }
465            // Adjust thermal state based on new power level.
466            output.thermal_state = if output.power_consumption > 7.0 {
467                ThermalState::Critical
468            } else if output.power_consumption >= 3.0 {
469                ThermalState::Warm
470            } else {
471                ThermalState::Normal
472            };
473        }
474
475        // Record the optimization.
476        let gain = if input.power_consumption > 0.0 {
477            (input.power_consumption - output.power_consumption) / input.power_consumption
478        } else {
479            0.0
480        };
481        self.optimization_history.push(OptimizationRecord {
482            timestamp: Instant::now(),
483            algorithm: self.optimization_algorithm.clone(),
484            input_state: input.clone(),
485            output_state: output.clone(),
486            efficiency_gain: gain,
487        });
488
489        // Trim history.
490        if self.optimization_history.len() > 200 {
491            let drop = self.optimization_history.len() - 200;
492            self.optimization_history.drain(0..drop);
493        }
494
495        output
496    }
497
498    /// Get the average efficiency gain from recent optimizations.
499    pub fn average_efficiency_gain(&self) -> f64 {
500        if self.optimization_history.is_empty() {
501            return 0.0;
502        }
503        self.optimization_history
504            .iter()
505            .map(|r| r.efficiency_gain)
506            .sum::<f64>()
507            / self.optimization_history.len() as f64
508    }
509
510    /// Get the target efficiency this optimizer is configured for.
511    pub fn target_efficiency(&self) -> f64 {
512        self.target_efficiency
513    }
514}