Skip to main content

optirs_core/neuromorphic/
energy_efficient.rs

1// Energy-Efficient Optimization for Neuromorphic Computing
2//
3// This module implements energy-efficient optimization algorithms specifically designed
4// for neuromorphic computing platforms, focusing on minimizing power consumption while
5// maintaining performance and accuracy.
6
7use super::{NeuromorphicMetrics, ThermalManagementConfig};
8use crate::error::Result;
9use scirs2_core::ndarray::{Array2, ArrayBase, Data, Dimension};
10use scirs2_core::numeric::Float;
11use std::collections::{HashMap, VecDeque};
12use std::fmt::Debug;
13use std::time::{Duration, Instant};
14
15/// Convert an `f64` literal/derived value to the generic float type `T`,
16/// falling back to `fallback` when the numeric type cannot represent it.
17/// Defensive only: always succeeds for the `f32`/`f64` types this crate
18/// targets.
19#[inline]
20fn to_t_or<T: Float>(value: f64, fallback: T) -> T {
21    T::from(value).unwrap_or(fallback)
22}
23
24// --- Documented device power model -----------------------------------
25//
26// These constants define a small but real CMOS-style power model used to
27// derive `current_power` and every optimization strategy's savings from
28// the actual `WorkloadSample`/system state, instead of fixed percentages.
29// The model has two components:
30//
31//   P_total = P_static(active_neurons) + P_dynamic * (V/V_nom)^2 * (f/f_nom)
32//
33// where `P_dynamic` is itself an activity-weighted sum of spike, synaptic
34// and communication contributions. `(V/V_nom)^2 * (f/f_nom)` is the
35// standard CMOS dynamic-power scaling law (`P_dyn ∝ C·V²·f`).
36
37/// Nominal (reference) operating point: matches `DVFSController`'s
38/// mid-range voltage/frequency level, used as the DVFS scaling baseline.
39const NOMINAL_VOLTAGE: f64 = 1.0;
40const NOMINAL_FREQUENCY_MHZ: f64 = 1000.0;
41
42/// Static leakage power per provisioned neuron (nW), independent of
43/// activity — this is what power-gating and clock-gating recover.
44const STATIC_POWER_PER_NEURON_NW: f64 = 0.05;
45/// Dynamic energy per spike (nJ) at the nominal operating point.
46const DYNAMIC_ENERGY_PER_SPIKE_NJ: f64 = 0.02;
47/// Dynamic power per unit of synaptic activity (nW) at nominal V/f.
48const DYNAMIC_POWER_PER_SYNAPTIC_ACTIVITY_NW: f64 = 5.0;
49/// Dynamic power per unit of communication overhead (nW) at nominal V/f.
50const DYNAMIC_POWER_PER_COMM_OVERHEAD_NW: f64 = 2.0;
51
52/// Number of independently power/clock-gateable neuron domains (a coarse,
53/// fixed partition of the provisioned neuron population).
54const GATING_DOMAINS: usize = 8;
55/// Minimum idle fraction before any domain is considered worth gating (the
56/// wake-up overhead makes gating unprofitable below this).
57const GATING_IDLE_THRESHOLD: f64 = 0.15;
58/// Fraction of a fully idle domain's dynamic power that clock gating can
59/// actually recover (gating logic itself has overhead).
60const CLOCK_GATING_EFFICIENCY: f64 = 0.9;
61/// Fraction of dynamic power recovered in light vs. deep sleep, before
62/// scaling by idle fraction.
63const LIGHT_SLEEP_SAVINGS: f64 = 0.4;
64const DEEP_SLEEP_SAVINGS: f64 = 0.85;
65/// Idle fraction above which sleep mode escalates from light to deep sleep.
66const DEEP_SLEEP_IDLE_THRESHOLD: f64 = 0.7;
67/// Thermal throttling proportional-controller range (°C): no throttling
68/// below the safe temperature, `THERMAL_MAX_REDUCTION` reached at or above
69/// the critical temperature, linear in between.
70const THERMAL_SAFE_TEMP_C: f64 = 60.0;
71const THERMAL_CRITICAL_TEMP_C: f64 = 90.0;
72const THERMAL_MIN_REDUCTION: f64 = 0.05;
73const THERMAL_MAX_REDUCTION: f64 = 0.6;
74
75/// Energy optimization strategies for neuromorphic computing
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum EnergyOptimizationStrategy {
78    /// Dynamic voltage and frequency scaling
79    DynamicVoltageScaling,
80
81    /// Power gating for unused neurons
82    PowerGating,
83
84    /// Clock gating for inactive regions
85    ClockGating,
86
87    /// Adaptive precision reduction
88    AdaptivePrecision,
89
90    /// Sparse computation optimization
91    SparseComputation,
92
93    /// Event-driven processing
94    EventDrivenProcessing,
95
96    /// Sleep mode management
97    SleepModeOptimization,
98
99    /// Thermal-aware optimization
100    ThermalAwareOptimization,
101
102    /// Multi-level optimization
103    MultiLevel,
104}
105
106/// Energy budget configuration
107#[derive(Debug, Clone)]
108pub struct EnergyBudget<T: Float + Debug + Send + Sync + 'static> {
109    /// Total energy budget (nJ)
110    pub total_budget: T,
111
112    /// Current energy consumption (nJ)
113    pub current_consumption: T,
114
115    /// Energy budget per operation (nJ/op)
116    pub per_operation_budget: T,
117
118    /// Energy allocation per component
119    pub component_allocation: HashMap<EnergyComponent, T>,
120
121    /// Energy efficiency targets
122    pub efficiency_targets: EnergyEfficiencyTargets<T>,
123
124    /// Emergency energy reserves
125    pub emergency_reserves: T,
126
127    /// Energy monitoring frequency
128    pub monitoring_frequency: Duration,
129}
130
131/// Energy components for budget allocation
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133pub enum EnergyComponent {
134    /// Synaptic operations
135    SynapticOps,
136
137    /// Membrane dynamics
138    MembraneDynamics,
139
140    /// Spike generation
141    SpikeGeneration,
142
143    /// Plasticity updates
144    PlasticityUpdates,
145
146    /// Memory access
147    MemoryAccess,
148
149    /// Communication
150    Communication,
151
152    /// Control logic
153    ControlLogic,
154
155    /// Thermal management
156    ThermalManagement,
157}
158
159/// Energy efficiency targets
160#[derive(Debug, Clone)]
161pub struct EnergyEfficiencyTargets<T: Float + Debug + Send + Sync + 'static> {
162    /// Operations per joule target
163    pub ops_per_joule: T,
164
165    /// Spikes per joule target
166    pub spikes_per_joule: T,
167
168    /// Synaptic updates per joule target
169    pub synaptic_updates_per_joule: T,
170
171    /// Memory bandwidth efficiency (ops/J/bandwidth)
172    pub memory_bandwidth_efficiency: T,
173
174    /// Thermal efficiency (performance/Watt/°C)
175    pub thermal_efficiency: T,
176}
177
178/// Energy-efficient optimizer configuration
179#[derive(Debug, Clone)]
180pub struct EnergyEfficientConfig<T: Float + Debug + Send + Sync + 'static> {
181    /// Primary optimization strategy
182    pub primary_strategy: EnergyOptimizationStrategy,
183
184    /// Fallback strategies
185    pub fallback_strategies: Vec<EnergyOptimizationStrategy>,
186
187    /// Energy budget configuration
188    pub energy_budget: EnergyBudget<T>,
189
190    /// Enable adaptive strategy switching
191    pub adaptive_strategy_switching: bool,
192
193    /// Strategy switching threshold (efficiency drop %)
194    pub strategy_switching_threshold: T,
195
196    /// Enable predictive energy management
197    pub predictive_energy_management: bool,
198
199    /// Prediction horizon (ms)
200    pub prediction_horizon: T,
201
202    /// Enable energy harvesting support
203    pub energy_harvesting: bool,
204
205    /// Harvesting efficiency
206    pub harvesting_efficiency: T,
207
208    /// Enable distributed energy management
209    pub distributed_energy_management: bool,
210
211    /// Enable real-time energy monitoring
212    pub real_time_monitoring: bool,
213
214    /// Monitoring resolution (μs)
215    pub monitoring_resolution: T,
216
217    /// Enable energy-aware workload balancing
218    pub energy_aware_load_balancing: bool,
219
220    /// Energy optimization aggressiveness (0.0 to 1.0)
221    pub optimization_aggressiveness: T,
222}
223
224impl<T: Float + Debug + Send + Sync + 'static> Default for EnergyEfficientConfig<T> {
225    fn default() -> Self {
226        let mut component_allocation = HashMap::new();
227        component_allocation.insert(
228            EnergyComponent::SynapticOps,
229            T::from(0.4).unwrap_or_else(|| T::zero()),
230        );
231        component_allocation.insert(
232            EnergyComponent::MembraneDynamics,
233            T::from(0.2).unwrap_or_else(|| T::zero()),
234        );
235        component_allocation.insert(
236            EnergyComponent::SpikeGeneration,
237            T::from(0.15).unwrap_or_else(|| T::zero()),
238        );
239        component_allocation.insert(
240            EnergyComponent::PlasticityUpdates,
241            T::from(0.1).unwrap_or_else(|| T::zero()),
242        );
243        component_allocation.insert(
244            EnergyComponent::MemoryAccess,
245            T::from(0.1).unwrap_or_else(|| T::zero()),
246        );
247        component_allocation.insert(
248            EnergyComponent::Communication,
249            T::from(0.05).unwrap_or_else(|| T::zero()),
250        );
251
252        Self {
253            primary_strategy: EnergyOptimizationStrategy::DynamicVoltageScaling,
254            fallback_strategies: vec![
255                EnergyOptimizationStrategy::PowerGating,
256                EnergyOptimizationStrategy::ClockGating,
257                EnergyOptimizationStrategy::SparseComputation,
258            ],
259            energy_budget: EnergyBudget {
260                total_budget: T::from(1000.0).unwrap_or_else(|| T::zero()), // 1 μJ
261                current_consumption: T::zero(),
262                per_operation_budget: T::from(10.0).unwrap_or_else(|| T::zero()), // 10 nJ per op
263                component_allocation,
264                efficiency_targets: EnergyEfficiencyTargets {
265                    ops_per_joule: T::from(1e12).unwrap_or_else(|| T::zero()), // 1 TOP/J
266                    spikes_per_joule: T::from(1e9).unwrap_or_else(|| T::zero()), // 1 GSp/J
267                    synaptic_updates_per_joule: T::from(1e10).unwrap_or_else(|| T::zero()), // 10 GSyOp/J
268                    memory_bandwidth_efficiency: T::from(1e6).unwrap_or_else(|| T::zero()),
269                    thermal_efficiency: T::from(1e9).unwrap_or_else(|| T::zero()),
270                },
271                emergency_reserves: T::from(100.0).unwrap_or_else(|| T::zero()), // 100 nJ reserve
272                monitoring_frequency: Duration::from_micros(100),
273            },
274            adaptive_strategy_switching: true,
275            strategy_switching_threshold: T::from(0.1).unwrap_or_else(|| T::zero()), // 10% efficiency drop
276            predictive_energy_management: true,
277            prediction_horizon: T::from(10.0).unwrap_or_else(|| T::zero()), // 10 ms
278            energy_harvesting: false,
279            harvesting_efficiency: T::from(0.1).unwrap_or_else(|| T::zero()),
280            distributed_energy_management: false,
281            real_time_monitoring: true,
282            monitoring_resolution: T::from(1.0).unwrap_or_else(|| T::zero()), // 1 μs
283            energy_aware_load_balancing: true,
284            optimization_aggressiveness: T::from(0.7).unwrap_or_else(|| T::zero()),
285        }
286    }
287}
288
289/// Energy monitoring and tracking
290#[derive(Debug, Clone)]
291struct EnergyMonitor<
292    T: Float
293        + Debug
294        + scirs2_core::ndarray::ScalarOperand
295        + std::fmt::Debug
296        + std::iter::Sum
297        + Send
298        + Sync,
299> {
300    /// Energy consumption history
301    consumption_history: VecDeque<(Instant, T)>,
302
303    /// Power consumption history
304    power_history: VecDeque<(Instant, T)>,
305
306    /// Current power draw (nW)
307    current_power: T,
308
309    /// Peak power observed (nW)
310    peak_power: T,
311
312    /// Average power over window (nW)
313    average_power: T,
314
315    /// Last monitoring update
316    last_update: Instant,
317
318    /// Monitoring window size
319    window_size: Duration,
320}
321
322/// Dynamic voltage and frequency scaling controller
323#[derive(Debug, Clone)]
324struct DVFSController<T: Float + Debug + Send + Sync + 'static> {
325    /// Available voltage levels (V)
326    voltage_levels: Vec<T>,
327
328    /// Available frequency levels (MHz)
329    frequency_levels: Vec<T>,
330
331    /// Current voltage index
332    current_voltage_idx: usize,
333
334    /// Current frequency index
335    current_frequency_idx: usize,
336}
337
338impl<T: Float + Debug + Send + Sync + 'static> DVFSController<T> {
339    fn new() -> Self {
340        Self {
341            voltage_levels: vec![
342                to_t_or(0.7, T::one()),
343                to_t_or(0.9, T::one()),
344                to_t_or(1.0, T::one()),
345                to_t_or(1.2, T::one()),
346            ],
347            frequency_levels: vec![
348                to_t_or(500.0, T::one()),
349                to_t_or(1000.0, T::one()),
350                to_t_or(1500.0, T::one()),
351                to_t_or(2000.0, T::one()),
352            ],
353            current_voltage_idx: 2,
354            current_frequency_idx: 2,
355        }
356    }
357
358    /// Choose a voltage/frequency level proportional to how utilized the
359    /// device is for this workload sample: a workload using few of the
360    /// provisioned neurons is scaled down towards the lowest V/f level, a
361    /// fully-active workload runs at the highest.
362    fn compute_optimal_levels(
363        &mut self,
364        workload: &WorkloadSample<T>,
365        total_neurons: usize,
366    ) -> Result<(T, T)> {
367        let total = to_t_or(total_neurons.max(1) as f64, T::one());
368        let utilization = (to_t_or(workload.active_neurons as f64, T::zero()) / total)
369            .max(T::zero())
370            .min(T::one());
371        let idx = (utilization * to_t_or((self.voltage_levels.len() - 1) as f64, T::zero()))
372            .to_usize()
373            .unwrap_or(2);
374
375        self.current_voltage_idx = idx.min(self.voltage_levels.len() - 1);
376        self.current_frequency_idx = idx.min(self.frequency_levels.len() - 1);
377
378        Ok((
379            self.voltage_levels[self.current_voltage_idx],
380            self.frequency_levels[self.current_frequency_idx],
381        ))
382    }
383}
384
385/// Power gating controller
386#[derive(Debug, Clone)]
387struct PowerGatingController<T: Float + Debug + Send + Sync + 'static> {
388    /// Gated neuron groups
389    gated_groups: HashMap<usize, GatedGroup>,
390
391    /// Power gate overhead energy
392    gate_overhead_energy: f64,
393
394    /// Total provisioned neurons, used to size each gateable domain.
395    total_neurons: usize,
396
397    /// Phantom data for type parameter
398    _phantom: std::marker::PhantomData<T>,
399}
400
401impl<T: Float + Debug + Send + Sync + 'static> PowerGatingController<T> {
402    fn new(total_neurons: usize) -> Self {
403        Self {
404            gated_groups: HashMap::new(),
405            gate_overhead_energy: 0.001,
406            total_neurons: total_neurons.max(1),
407            _phantom: std::marker::PhantomData,
408        }
409    }
410
411    /// Power saved (nW) by gating `region_id`, proportional to the static
412    /// leakage power of the neurons in that domain scaled by how idle the
413    /// current workload is (a domain fully idle recovers its whole static
414    /// power budget; the recovery is proportional otherwise).
415    fn gate_region(&mut self, region_id: usize, idle_fraction: T) -> Result<T> {
416        let neurons_per_domain = (self.total_neurons / GATING_DOMAINS.max(1)).max(1);
417        let domain_static_power = to_t_or(
418            neurons_per_domain as f64 * STATIC_POWER_PER_NEURON_NW,
419            T::zero(),
420        );
421        let saved = domain_static_power * idle_fraction;
422
423        // Only the gated flag is ever read back; the surrounding bookkeeping
424        // fields (`neuron_ids`, `last_activity`, thresholds) were written once
425        // at insert and never updated or consulted, so they are not modelled.
426        self.gated_groups
427            .insert(region_id, GatedGroup { is_gated: true });
428
429        Ok(saved)
430    }
431
432    /// Identify which fixed neuron domains are idle enough (given this
433    /// workload's idle fraction) to be worth power-gating. The number of
434    /// domains gated scales with how idle the device is, rather than a
435    /// single fixed region.
436    fn identify_gatable_regions(&self, idle_fraction: T) -> Vec<usize> {
437        if idle_fraction < to_t_or(GATING_IDLE_THRESHOLD, T::zero()) {
438            return Vec::new();
439        }
440        let fraction = idle_fraction.to_f64().unwrap_or(0.0).clamp(0.0, 1.0);
441        let gatable_domains =
442            ((fraction * GATING_DOMAINS as f64).round() as usize).clamp(1, GATING_DOMAINS);
443        (0..gatable_domains).collect()
444    }
445}
446
447/// Gated neuron group
448#[derive(Debug, Clone)]
449struct GatedGroup {
450    /// Whether this domain is currently power-gated.
451    is_gated: bool,
452}
453
454/// Sparse computation optimizer
455#[derive(Debug, Clone)]
456struct SparseComputationOptimizer<T: Float + Debug + Send + Sync + 'static> {
457    /// Sparsity threshold
458    sparsity_threshold: T,
459
460    /// Total provisioned neurons, used as the activity-estimate denominator
461    /// when no real weight/activation matrix is available.
462    total_neurons: usize,
463}
464
465impl<T: Float + Debug + Send + Sync + 'static> SparseComputationOptimizer<T> {
466    fn new(total_neurons: usize) -> Self {
467        Self {
468            sparsity_threshold: to_t_or(0.01, T::zero()),
469            total_neurons: total_neurons.max(1),
470        }
471    }
472
473    /// Estimate the sparsity relevant to this optimization step.
474    ///
475    /// When `matrix` is provided, sparsity is the *real* fraction of
476    /// near-zero entries in it (elements with `|v| <= sparsity_threshold`).
477    /// Otherwise it falls back to an activity-derived estimate using the
478    /// device's actual provisioned neuron count: `1 - active/total`.
479    fn analyze_sparsity<S, Dm>(
480        &mut self,
481        workload: &WorkloadSample<T>,
482        matrix: Option<&ArrayBase<S, Dm>>,
483    ) -> Result<SparsityAnalysis<T>>
484    where
485        S: Data<Elem = T>,
486        Dm: Dimension,
487    {
488        let sparsity_ratio = if let Some(m) = matrix {
489            let total = m.len().max(1);
490            let zero_count = m
491                .iter()
492                .filter(|&&v| v.abs() <= self.sparsity_threshold)
493                .count();
494            to_t_or(zero_count as f64 / total as f64, T::zero())
495        } else {
496            let total = to_t_or(self.total_neurons as f64, T::one());
497            let active = to_t_or(workload.active_neurons as f64, T::zero());
498            (T::one() - (active / total)).max(T::zero()).min(T::one())
499        };
500
501        Ok(SparsityAnalysis { sparsity_ratio })
502    }
503
504    /// Energy saved by skipping the near-zero entries.
505    ///
506    /// Derived from the *measured* `sparsity_ratio` rather than a value
507    /// pre-baked at analysis time: `SPARSE_SAVING_EFFICIENCY` is the fraction
508    /// of the skipped work that translates into energy, the rest being indexing
509    /// and gather overhead a sparse kernel still pays.
510    fn apply_compression(&mut self, analysis: &SparsityAnalysis<T>) -> Result<T> {
511        Ok(analysis.sparsity_ratio * to_t_or(SPARSE_SAVING_EFFICIENCY, T::zero()))
512    }
513
514    fn apply_sparse_optimizations(&mut self, analysis: &SparsityAnalysis<T>) -> Result<T> {
515        // Apply sparse optimizations based on analysis
516        let compression_savings = self.apply_compression(analysis)?;
517        Ok(compression_savings)
518    }
519}
520
521/// Fraction of the work skipped by sparsity that becomes real energy saving;
522/// the remainder is indexing/gather overhead a sparse kernel still pays.
523const SPARSE_SAVING_EFFICIENCY: f64 = 0.8;
524
525#[derive(Debug, Clone)]
526struct SparsityAnalysis<T: Float + Debug + Send + Sync + 'static> {
527    /// Measured fraction of near-zero entries.
528    sparsity_ratio: T,
529}
530
531/// Energy-efficient optimizer
532pub struct EnergyEfficientOptimizer<
533    T: Float
534        + Debug
535        + scirs2_core::ndarray::ScalarOperand
536        + std::fmt::Debug
537        + std::iter::Sum
538        + Send
539        + Sync,
540> {
541    /// Configuration
542    config: EnergyEfficientConfig<T>,
543
544    /// Energy monitor
545    energy_monitor: EnergyMonitor<T>,
546
547    /// DVFS controller
548    dvfs_controller: DVFSController<T>,
549
550    /// Power gating controller
551    power_gating_controller: PowerGatingController<T>,
552
553    /// Sparse computation optimizer
554    sparse_optimizer: SparseComputationOptimizer<T>,
555
556    /// Thermal management
557    thermal_manager: ThermalManager<T>,
558
559    /// Predictive energy manager
560    predictive_manager: PredictiveEnergyManager<T>,
561
562    /// Current optimization strategy
563    current_strategy: EnergyOptimizationStrategy,
564
565    /// Strategy effectiveness history
566    strategy_effectiveness: HashMap<EnergyOptimizationStrategy, T>,
567
568    /// System state
569    system_state: EnergySystemState<T>,
570
571    /// Performance metrics
572    metrics: NeuromorphicMetrics<T>,
573}
574
575/// Energy system state
576#[derive(Debug, Clone)]
577pub struct EnergySystemState<T: Float + Debug + Send + Sync + 'static> {
578    /// Current energy consumption (nJ)
579    pub current_energy: T,
580
581    /// Current power consumption (nW)
582    pub current_power: T,
583
584    /// Temperature (°C)
585    pub temperature: T,
586
587    /// Active neuron count
588    pub active_neurons: usize,
589
590    /// Active synapses count
591    pub active_synapses: usize,
592
593    /// Current voltage (V)
594    pub current_voltage: T,
595
596    /// Current frequency (MHz)
597    pub current_frequency: T,
598
599    /// Gated regions
600    pub gated_regions: Vec<usize>,
601
602    /// Sleep mode status
603    pub sleep_status: SleepStatus,
604}
605
606#[derive(Debug, Clone, Copy)]
607pub enum SleepStatus {
608    Active,
609    LightSleep,
610    DeepSleep,
611    Hibernation,
612}
613
614/// Thermal manager for energy efficiency
615#[derive(Debug, Clone)]
616struct ThermalManager<T: Float + Debug + Send + Sync + 'static> {
617    /// Current temperature reading (°C)
618    current_temperature: T,
619
620    /// Temperature history
621    temperature_history: VecDeque<(Instant, T)>,
622
623    /// Thermal model parameters
624    thermal_model: ThermalModel<T>,
625
626    /// Timestamp of the previous `update()` call, used to integrate the
627    /// thermal RC model over the actual elapsed time (F59).
628    last_update: Instant,
629}
630
631impl<T: Float + Debug + Send + Sync + 'static> ThermalManager<T> {
632    /// Builds a thermal manager. The configuration is consumed to seed the
633    /// thermal model rather than stored: nothing read it back.
634    fn new(_config: ThermalManagementConfig<T>) -> Self {
635        Self {
636            current_temperature: to_t_or(25.0, T::zero()),
637            temperature_history: VecDeque::new(),
638            thermal_model: ThermalModel {
639                time_constant: to_t_or(10.0, T::one()),
640                thermal_resistance: to_t_or(0.5, T::zero()),
641                ambient_temperature: to_t_or(25.0, T::zero()),
642            },
643            last_update: Instant::now(),
644        }
645    }
646
647    /// Integrate the thermal RC model one step forward (F59):
648    /// `dT/dt = (P·R + T_amb - T) / τ`, discretized over the actual
649    /// elapsed time since the previous call as
650    /// `T += dt/τ · (P·R + T_amb - T)`. Unlike the previous instantaneous
651    /// `T = P·R + T_amb` formula, this respects thermal inertia (a sudden
652    /// power spike heats the die gradually, not instantly).
653    fn update(&mut self, system_state: &EnergySystemState<T>) -> Result<()> {
654        let now = Instant::now();
655        let dt_seconds = to_t_or(
656            now.duration_since(self.last_update).as_secs_f64().max(1e-6),
657            to_t_or(1e-3, T::one()),
658        );
659        self.last_update = now;
660
661        let steady_state = system_state.current_power * self.thermal_model.thermal_resistance
662            + self.thermal_model.ambient_temperature;
663        let tau = if self.thermal_model.time_constant > T::zero() {
664            self.thermal_model.time_constant
665        } else {
666            T::one()
667        };
668        self.current_temperature = self.current_temperature
669            + (dt_seconds / tau) * (steady_state - self.current_temperature);
670
671        self.temperature_history
672            .push_back((now, self.current_temperature));
673        // Bound the history so it cannot grow without limit (F59).
674        while self.temperature_history.len() > 100 {
675            self.temperature_history.pop_front();
676        }
677        Ok(())
678    }
679}
680
681/// Thermal model for prediction
682#[derive(Debug, Clone)]
683struct ThermalModel<T: Float + Debug + Send + Sync + 'static> {
684    /// Thermal time constant (s)
685    time_constant: T,
686
687    /// Thermal resistance (°C/W)
688    thermal_resistance: T,
689
690    /// Ambient temperature (°C)
691    ambient_temperature: T,
692}
693
694/// Predictive energy manager
695#[derive(Debug, Clone)]
696struct PredictiveEnergyManager<T: Float + Debug + Send + Sync + 'static> {
697    /// Workload history
698    workload_history: VecDeque<WorkloadSample<T>>,
699
700    /// Observed `(timestamp, power_nw)` samples used to fit the linear
701    /// trend that [`Self::predict_energy`] extrapolates from (F18: this
702    /// history used to never be populated, so predictions always fell back
703    /// to a fabricated constant regardless of `horizon`).
704    power_history: VecDeque<(Instant, T)>,
705}
706
707impl<T: Float + Debug + Send + Sync + 'static> PredictiveEnergyManager<T> {
708    fn new() -> Self {
709        Self {
710            workload_history: VecDeque::new(),
711            power_history: VecDeque::new(),
712        }
713    }
714
715    /// Record an observed workload/power sample so future
716    /// [`Self::predict_energy`] calls have real data to extrapolate from.
717    fn record_sample(&mut self, workload: WorkloadSample<T>, power: T) {
718        self.power_history.push_back((Instant::now(), power));
719        while self.power_history.len() > 100 {
720            self.power_history.pop_front();
721        }
722        self.workload_history.push_back(workload);
723        while self.workload_history.len() > 100 {
724            self.workload_history.pop_front();
725        }
726    }
727
728    /// Predict total energy (nJ) expected over the next `horizon`, by
729    /// extrapolating the recent average power draw (nW) across that
730    /// window: `E = P_avg * horizon` (F18: `horizon` was previously
731    /// accepted but never used, and with no history ever recorded this
732    /// always returned a hardcoded `1.0`).
733    fn predict_energy(&self, horizon: Duration) -> Result<T> {
734        if self.power_history.is_empty() {
735            return Ok(T::zero());
736        }
737        let sum: T = self
738            .power_history
739            .iter()
740            .map(|(_, power)| *power)
741            .fold(T::zero(), |acc, x| acc + x);
742        let avg_power = sum / to_t_or(self.power_history.len() as f64, T::one());
743
744        // Mirrors the file's `energy(nJ) = power(nW) * time(ms) / 1000`
745        // convention used throughout the strategy implementations above.
746        let horizon_ms = to_t_or(horizon.as_secs_f64() * 1000.0, T::zero());
747        let predicted = avg_power * horizon_ms / to_t_or(1000.0, T::one());
748
749        Ok(predicted)
750    }
751}
752
753/// Workload sample for prediction
754#[derive(Debug, Clone)]
755pub struct WorkloadSample<T: Float + Debug + Send + Sync + 'static> {
756    /// Timestamp
757    pub timestamp: Instant,
758
759    /// Number of active neurons
760    pub active_neurons: usize,
761
762    /// Spike rate (Hz)
763    pub spike_rate: T,
764
765    /// Synaptic activity
766    pub synaptic_activity: T,
767
768    /// Memory access pattern
769    pub memory_access_pattern: MemoryAccessPattern,
770
771    /// Communication overhead
772    pub communication_overhead: T,
773}
774
775#[derive(Debug, Clone, Copy)]
776pub enum MemoryAccessPattern {
777    Sequential,
778    Random,
779    Sparse,
780    Burst,
781    Mixed,
782}
783
784impl<
785        T: Float
786            + Debug
787            + Send
788            + Sync
789            + scirs2_core::ndarray::ScalarOperand
790            + std::fmt::Debug
791            + std::iter::Sum,
792    > EnergyEfficientOptimizer<T>
793{
794    /// Create a new energy-efficient optimizer
795    pub fn new(_config: EnergyEfficientConfig<T>, numneurons: usize) -> Self {
796        Self {
797            config: _config.clone(),
798            energy_monitor: EnergyMonitor::new(_config.energy_budget.monitoring_frequency),
799            dvfs_controller: DVFSController::new(),
800            power_gating_controller: PowerGatingController::new(numneurons),
801            sparse_optimizer: SparseComputationOptimizer::new(numneurons),
802            thermal_manager: ThermalManager::new(ThermalManagementConfig::default()),
803            predictive_manager: PredictiveEnergyManager::new(),
804            current_strategy: _config.primary_strategy,
805            strategy_effectiveness: HashMap::new(),
806            system_state: EnergySystemState {
807                current_energy: T::zero(),
808                current_power: T::zero(),
809                temperature: T::from(25.0).unwrap_or_else(|| T::zero()), // 25°C ambient
810                active_neurons: numneurons,
811                active_synapses: numneurons * numneurons,
812                current_voltage: T::from(1.0).unwrap_or_else(|| T::zero()), // 1V
813                current_frequency: T::from(100.0).unwrap_or_else(|| T::zero()), // 100 MHz
814                gated_regions: Vec::new(),
815                sleep_status: SleepStatus::Active,
816            },
817            metrics: NeuromorphicMetrics::default(),
818        }
819    }
820
821    /// Optimize energy consumption
822    pub fn optimize_energy(
823        &mut self,
824        workload: &WorkloadSample<T>,
825    ) -> Result<EnergyOptimizationResult<T>> {
826        self.optimize_energy_impl(workload, None::<&Array2<T>>)
827    }
828
829    /// Like [`Self::optimize_energy`], but lets the caller supply a real
830    /// weight/activation matrix so that, when the active strategy is
831    /// [`EnergyOptimizationStrategy::SparseComputation`], sparsity is
832    /// measured from the matrix's actual zero fraction instead of an
833    /// activity-derived estimate (F18).
834    pub fn optimize_energy_with_matrix<S, Dm>(
835        &mut self,
836        workload: &WorkloadSample<T>,
837        matrix: Option<&ArrayBase<S, Dm>>,
838    ) -> Result<EnergyOptimizationResult<T>>
839    where
840        S: Data<Elem = T>,
841        Dm: Dimension,
842    {
843        self.optimize_energy_impl(workload, matrix)
844    }
845
846    fn optimize_energy_impl<S, Dm>(
847        &mut self,
848        workload: &WorkloadSample<T>,
849        matrix: Option<&ArrayBase<S, Dm>>,
850    ) -> Result<EnergyOptimizationResult<T>>
851    where
852        S: Data<Elem = T>,
853        Dm: Dimension,
854    {
855        // Update energy monitoring
856        self.energy_monitor.update(&self.system_state)?;
857
858        // Get energy predictions (horizon-aware forecast for the next
859        // minute; exercised here so predictive strategy switching has a
860        // real signal to react to in future extensions).
861        let _prediction = if self.config.predictive_energy_management {
862            self.predictive_manager
863                .predict_energy(Duration::from_secs(60))?
864        } else {
865            T::zero()
866        };
867
868        // Apply current optimization strategy
869        let optimization_result = match self.current_strategy {
870            EnergyOptimizationStrategy::DynamicVoltageScaling => {
871                self.apply_dvfs_optimization(workload)?
872            }
873            EnergyOptimizationStrategy::PowerGating => {
874                self.apply_power_gating_optimization(workload)?
875            }
876            EnergyOptimizationStrategy::ClockGating => {
877                self.apply_clock_gating_optimization(workload)?
878            }
879            EnergyOptimizationStrategy::SparseComputation => {
880                self.apply_sparse_computation_optimization(workload, matrix)?
881            }
882            EnergyOptimizationStrategy::SleepModeOptimization => {
883                self.apply_sleep_mode_optimization(workload)?
884            }
885            EnergyOptimizationStrategy::ThermalAwareOptimization => {
886                self.apply_thermal_aware_optimization(workload)?
887            }
888            EnergyOptimizationStrategy::MultiLevel => {
889                self.apply_multi_level_optimization(workload)?
890            }
891            _ => {
892                // Default optimization
893                self.apply_default_optimization(workload)?
894            }
895        };
896
897        // Record this (workload, power) sample for future predictions
898        // (F18: `predict_energy` previously always saw an empty history and
899        // returned a fabricated constant).
900        self.predictive_manager
901            .record_sample(workload.clone(), self.system_state.current_power);
902
903        // Evaluate strategy effectiveness
904        self.evaluate_strategy_effectiveness(&optimization_result);
905
906        // Adaptive strategy switching
907        if self.config.adaptive_strategy_switching {
908            self.consider_strategy_switch()?;
909        }
910
911        // Update thermal management (RC-integrated, F59) and sync the
912        // authoritative temperature back into `system_state` so the
913        // thermal-aware strategy above reacts to the real modeled
914        // temperature rather than a disconnected, manually decayed value.
915        self.thermal_manager.update(&self.system_state)?;
916        self.system_state.temperature = self.thermal_manager.current_temperature;
917
918        // Update metrics
919        self.update_metrics(&optimization_result);
920
921        Ok(optimization_result)
922    }
923
924    /// Fraction of the device's provisioned neurons that are idle for this
925    /// workload sample, clamped to `[0, 1]`. This is the common activity
926    /// signal driving clock-gating, power-gating and sleep-mode savings
927    /// below (F18): a workload using few of the provisioned neurons frees
928    /// up a correspondingly large fraction of gateable/sleepable capacity.
929    fn idle_fraction(&self, workload: &WorkloadSample<T>) -> T {
930        let total = to_t_or(self.system_state.active_neurons.max(1) as f64, T::one());
931        let active = to_t_or(workload.active_neurons as f64, T::zero());
932        (T::one() - (active / total)).max(T::zero()).min(T::one())
933    }
934
935    /// Estimate the instantaneous power draw (nW) implied by a workload
936    /// sample at the device's current voltage/frequency operating point:
937    /// `P = P_static(active_neurons) + P_dynamic(spikes, synapses, comm) *
938    /// (V/V_nom)² * (f/f_nom)`. This replaces reading back a stored
939    /// `current_power` field that starts at (and can get stuck at) zero;
940    /// every strategy below derives its baseline from the actual workload.
941    fn estimate_workload_power(&self, workload: &WorkloadSample<T>) -> T {
942        // Static leakage is incurred by the whole provisioned chip
943        // (`system_state.active_neurons`, fixed at construction), not just
944        // by however many neurons this particular sample activates — an
945        // idle neuron still leaks, which is exactly what clock/power
946        // gating and sleep mode recover below via `idle_fraction`.
947        let static_power = to_t_or(
948            self.system_state.active_neurons as f64 * STATIC_POWER_PER_NEURON_NW,
949            T::zero(),
950        );
951        let spike_power = workload.spike_rate * to_t_or(DYNAMIC_ENERGY_PER_SPIKE_NJ, T::zero());
952        let synaptic_power =
953            workload.synaptic_activity * to_t_or(DYNAMIC_POWER_PER_SYNAPTIC_ACTIVITY_NW, T::zero());
954        let comm_power = workload.communication_overhead
955            * to_t_or(DYNAMIC_POWER_PER_COMM_OVERHEAD_NW, T::zero());
956        let dynamic_baseline = spike_power + synaptic_power + comm_power;
957
958        let v_nom = to_t_or(NOMINAL_VOLTAGE, T::one());
959        let f_nom = to_t_or(NOMINAL_FREQUENCY_MHZ, T::one());
960        let voltage_ratio = if v_nom > T::zero() {
961            self.system_state.current_voltage / v_nom
962        } else {
963            T::one()
964        };
965        let freq_ratio = if f_nom > T::zero() {
966            self.system_state.current_frequency / f_nom
967        } else {
968            T::one()
969        };
970
971        static_power + dynamic_baseline * voltage_ratio * voltage_ratio * freq_ratio
972    }
973
974    /// Apply DVFS optimization
975    fn apply_dvfs_optimization(
976        &mut self,
977        workload: &WorkloadSample<T>,
978    ) -> Result<EnergyOptimizationResult<T>> {
979        // Baseline power under the workload at the CURRENT operating point.
980        let initial_power = self.estimate_workload_power(workload);
981
982        // Capture the pre-transition operating point BEFORE mutating state
983        // (F17: previously voltage/frequency were overwritten first and
984        // the reduction ratio was computed against those *new* values,
985        // forcing numerator == denominator == 1.0 on every call).
986        let v_old = self.system_state.current_voltage;
987        let f_old = self.system_state.current_frequency;
988
989        // Determine optimal voltage and frequency for this workload.
990        let (optimal_voltage, optimal_frequency) = self
991            .dvfs_controller
992            .compute_optimal_levels(workload, self.system_state.active_neurons)?;
993
994        let power_reduction =
995            self.calculate_power_reduction(v_old, f_old, optimal_voltage, optimal_frequency);
996        let performance_impact = self.calculate_performance_impact(f_old, optimal_frequency);
997        let new_power = initial_power * power_reduction;
998        let thermal_impact = self.calculate_thermal_impact(initial_power, new_power);
999
1000        // Commit the new operating point and derived power.
1001        self.system_state.current_voltage = optimal_voltage;
1002        self.system_state.current_frequency = optimal_frequency;
1003        self.system_state.current_power = new_power;
1004
1005        // Update accumulated energy consumption (nJ) over a 1 ms step.
1006        let time_delta = to_t_or(1.0, T::one());
1007        let energy_delta = new_power * time_delta / to_t_or(1000.0, T::one());
1008        self.system_state.current_energy = self.system_state.current_energy + energy_delta;
1009
1010        Ok(EnergyOptimizationResult {
1011            strategy_used: EnergyOptimizationStrategy::DynamicVoltageScaling,
1012            energy_saved: (initial_power - new_power).max(T::zero()) * time_delta
1013                / to_t_or(1000.0, T::one()),
1014            power_reduction: initial_power - new_power,
1015            performance_impact,
1016            thermal_impact,
1017            optimization_overhead: to_t_or(0.1, T::zero()), // 0.1 nJ overhead
1018        })
1019    }
1020
1021    /// Apply power gating optimization
1022    fn apply_power_gating_optimization(
1023        &mut self,
1024        workload: &WorkloadSample<T>,
1025    ) -> Result<EnergyOptimizationResult<T>> {
1026        let initial_power = self.estimate_workload_power(workload);
1027        let idle_fraction = self.idle_fraction(workload);
1028
1029        // Identify idle domains worth gating, sized to how idle we are.
1030        let gatable_regions = self
1031            .power_gating_controller
1032            .identify_gatable_regions(idle_fraction);
1033
1034        let mut total_power_saved = T::zero();
1035        for region_id in gatable_regions {
1036            let power_saved = self
1037                .power_gating_controller
1038                .gate_region(region_id, idle_fraction)?;
1039            total_power_saved = total_power_saved + power_saved;
1040            if !self.system_state.gated_regions.contains(&region_id) {
1041                self.system_state.gated_regions.push(region_id);
1042            }
1043        }
1044
1045        let new_power = (initial_power - total_power_saved).max(T::zero());
1046        self.system_state.current_power = new_power;
1047
1048        let time_delta = to_t_or(1.0, T::one());
1049        let energy_saved = total_power_saved * time_delta / to_t_or(1000.0, T::one());
1050        let overhead = to_t_or(self.power_gating_controller.gate_overhead_energy, T::zero())
1051            * to_t_or(self.system_state.gated_regions.len() as f64, T::zero());
1052
1053        Ok(EnergyOptimizationResult {
1054            strategy_used: EnergyOptimizationStrategy::PowerGating,
1055            energy_saved,
1056            power_reduction: total_power_saved,
1057            performance_impact: T::zero(), // Gated domains resume on demand
1058            thermal_impact: total_power_saved * to_t_or(0.8, T::zero()),
1059            optimization_overhead: overhead,
1060        })
1061    }
1062
1063    /// Apply sparse computation optimization. `matrix`, when present, gives
1064    /// the *real* sparsity of the current weight/activation tensor (F18);
1065    /// otherwise sparsity is estimated from workload activity.
1066    fn apply_sparse_computation_optimization<S, Dm>(
1067        &mut self,
1068        workload: &WorkloadSample<T>,
1069        matrix: Option<&ArrayBase<S, Dm>>,
1070    ) -> Result<EnergyOptimizationResult<T>>
1071    where
1072        S: Data<Elem = T>,
1073        Dm: Dimension,
1074    {
1075        let initial_power = self.estimate_workload_power(workload);
1076
1077        // Analyze sparsity patterns
1078        let sparsity_analysis = self.sparse_optimizer.analyze_sparsity(workload, matrix)?;
1079
1080        // Apply sparse optimizations
1081        let energy_savings = self
1082            .sparse_optimizer
1083            .apply_sparse_optimizations(&sparsity_analysis)?;
1084
1085        // Update system state
1086        let new_power = initial_power * (T::one() - energy_savings);
1087        self.system_state.current_power = new_power;
1088
1089        Ok(EnergyOptimizationResult {
1090            strategy_used: EnergyOptimizationStrategy::SparseComputation,
1091            energy_saved: initial_power * energy_savings,
1092            power_reduction: initial_power - new_power,
1093            performance_impact: energy_savings * to_t_or(0.1, T::zero()), // Small performance impact
1094            thermal_impact: (initial_power - new_power) * to_t_or(0.9, T::zero()),
1095            optimization_overhead: to_t_or(0.2, T::zero()), // Moderate overhead
1096        })
1097    }
1098
1099    /// Apply multi-level optimization
1100    fn apply_multi_level_optimization(
1101        &mut self,
1102        workload: &WorkloadSample<T>,
1103    ) -> Result<EnergyOptimizationResult<T>> {
1104        let mut total_result = EnergyOptimizationResult {
1105            strategy_used: EnergyOptimizationStrategy::MultiLevel,
1106            energy_saved: T::zero(),
1107            power_reduction: T::zero(),
1108            performance_impact: T::zero(),
1109            thermal_impact: T::zero(),
1110            optimization_overhead: T::zero(),
1111        };
1112
1113        // Apply multiple strategies in sequence
1114        let strategies = [
1115            EnergyOptimizationStrategy::SparseComputation,
1116            EnergyOptimizationStrategy::DynamicVoltageScaling,
1117            EnergyOptimizationStrategy::PowerGating,
1118        ];
1119
1120        for strategy in &strategies {
1121            let prev_strategy = self.current_strategy;
1122            self.current_strategy = *strategy;
1123
1124            let result = match strategy {
1125                EnergyOptimizationStrategy::SparseComputation => {
1126                    self.apply_sparse_computation_optimization(workload, None::<&Array2<T>>)?
1127                }
1128                EnergyOptimizationStrategy::DynamicVoltageScaling => {
1129                    self.apply_dvfs_optimization(workload)?
1130                }
1131                EnergyOptimizationStrategy::PowerGating => {
1132                    self.apply_power_gating_optimization(workload)?
1133                }
1134                _ => continue,
1135            };
1136
1137            // Accumulate results
1138            total_result.energy_saved = total_result.energy_saved + result.energy_saved;
1139            total_result.power_reduction = total_result.power_reduction + result.power_reduction;
1140            total_result.performance_impact =
1141                total_result.performance_impact + result.performance_impact;
1142            total_result.thermal_impact = total_result.thermal_impact + result.thermal_impact;
1143            total_result.optimization_overhead =
1144                total_result.optimization_overhead + result.optimization_overhead;
1145
1146            self.current_strategy = prev_strategy;
1147        }
1148
1149        Ok(total_result)
1150    }
1151
1152    /// Apply default optimization
1153    fn apply_default_optimization(
1154        &mut self,
1155        _workload: &WorkloadSample<T>,
1156    ) -> Result<EnergyOptimizationResult<T>> {
1157        // Minimal optimization - just monitoring
1158        Ok(EnergyOptimizationResult {
1159            strategy_used: self.current_strategy,
1160            energy_saved: T::zero(),
1161            power_reduction: T::zero(),
1162            performance_impact: T::zero(),
1163            thermal_impact: T::zero(),
1164            optimization_overhead: to_t_or(0.01, T::zero()),
1165        })
1166    }
1167
1168    /// Apply clock gating optimization. The fraction of dynamic power
1169    /// recoverable is the workload's idle fraction times the gating
1170    /// circuit's own efficiency (it cannot recover 100% due to gating
1171    /// overhead) — derived from the workload, not a fixed percentage.
1172    fn apply_clock_gating_optimization(
1173        &mut self,
1174        workload: &WorkloadSample<T>,
1175    ) -> Result<EnergyOptimizationResult<T>> {
1176        let initial_power = self.estimate_workload_power(workload);
1177        let idle_fraction = self.idle_fraction(workload);
1178        let reduction_factor = idle_fraction * to_t_or(CLOCK_GATING_EFFICIENCY, T::zero());
1179        let new_power = initial_power * (T::one() - reduction_factor);
1180
1181        self.system_state.current_power = new_power;
1182
1183        Ok(EnergyOptimizationResult {
1184            strategy_used: EnergyOptimizationStrategy::ClockGating,
1185            energy_saved: initial_power * reduction_factor,
1186            power_reduction: initial_power - new_power,
1187            performance_impact: T::zero(), // Gated clocks resume with no latency
1188            thermal_impact: (initial_power - new_power) * to_t_or(0.8, T::zero()),
1189            optimization_overhead: to_t_or(0.05, T::zero()),
1190        })
1191    }
1192
1193    /// Apply sleep mode optimization. Sleep depth (light vs. deep) and the
1194    /// resulting savings scale with the workload's idle fraction, rather
1195    /// than a fixed 50% constant.
1196    fn apply_sleep_mode_optimization(
1197        &mut self,
1198        workload: &WorkloadSample<T>,
1199    ) -> Result<EnergyOptimizationResult<T>> {
1200        let initial_power = self.estimate_workload_power(workload);
1201        let idle_fraction = self.idle_fraction(workload);
1202
1203        let deep_sleep_threshold = to_t_or(DEEP_SLEEP_IDLE_THRESHOLD, T::one());
1204        let (status, base_savings, wakeup_latency) = if idle_fraction >= deep_sleep_threshold {
1205            (SleepStatus::DeepSleep, DEEP_SLEEP_SAVINGS, 0.2)
1206        } else {
1207            (SleepStatus::LightSleep, LIGHT_SLEEP_SAVINGS, 0.1)
1208        };
1209        self.system_state.sleep_status = status;
1210
1211        let reduction_factor = idle_fraction * to_t_or(base_savings, T::zero());
1212        let new_power = initial_power * (T::one() - reduction_factor);
1213        self.system_state.current_power = new_power;
1214
1215        Ok(EnergyOptimizationResult {
1216            strategy_used: EnergyOptimizationStrategy::SleepModeOptimization,
1217            energy_saved: initial_power * reduction_factor,
1218            power_reduction: initial_power - new_power,
1219            performance_impact: to_t_or(wakeup_latency, T::zero()),
1220            thermal_impact: (initial_power - new_power) * to_t_or(0.95, T::zero()),
1221            optimization_overhead: to_t_or(0.1, T::zero()),
1222        })
1223    }
1224
1225    /// Apply thermal-aware optimization: a proportional controller that
1226    /// scales power reduction linearly with how far the (RC-modeled)
1227    /// temperature has overshot a safe operating range, rather than a
1228    /// hardcoded two-step reduction factor.
1229    fn apply_thermal_aware_optimization(
1230        &mut self,
1231        workload: &WorkloadSample<T>,
1232    ) -> Result<EnergyOptimizationResult<T>> {
1233        let initial_power = self.estimate_workload_power(workload);
1234
1235        let safe_temp = to_t_or(THERMAL_SAFE_TEMP_C, T::zero());
1236        let critical_temp = to_t_or(THERMAL_CRITICAL_TEMP_C, T::one());
1237        let min_reduction = to_t_or(THERMAL_MIN_REDUCTION, T::zero());
1238        let max_reduction = to_t_or(THERMAL_MAX_REDUCTION, T::zero());
1239
1240        let span = (critical_temp - safe_temp).max(to_t_or(1e-6, T::one()));
1241        let overshoot = ((self.system_state.temperature - safe_temp) / span)
1242            .max(T::zero())
1243            .min(T::one());
1244        let reduction_factor = min_reduction + (max_reduction - min_reduction) * overshoot;
1245
1246        let new_power = initial_power * (T::one() - reduction_factor);
1247        self.system_state.current_power = new_power;
1248
1249        Ok(EnergyOptimizationResult {
1250            strategy_used: EnergyOptimizationStrategy::ThermalAwareOptimization,
1251            energy_saved: initial_power * reduction_factor,
1252            power_reduction: initial_power - new_power,
1253            performance_impact: reduction_factor * to_t_or(0.5, T::zero()),
1254            thermal_impact: initial_power - new_power,
1255            optimization_overhead: to_t_or(0.15, T::zero()),
1256        })
1257    }
1258
1259    /// Ratio of new to old power (`P_new / P_old`) under a simplified CMOS
1260    /// dynamic power model `P ∝ V²·f`. Equals 1.0 only when voltage and
1261    /// frequency are genuinely unchanged; any real DVFS transition yields a
1262    /// ratio strictly different from 1.0 (F17).
1263    fn calculate_power_reduction(&self, v_old: T, f_old: T, v_new: T, f_new: T) -> T {
1264        let denom = v_old * v_old * f_old;
1265        if denom <= T::zero() {
1266            return T::one();
1267        }
1268        (v_new * v_new * f_new) / denom
1269    }
1270
1271    /// Calculate performance impact of a frequency change:
1272    /// `(old_freq - new_freq) / old_freq`.
1273    fn calculate_performance_impact(&self, old_frequency: T, new_frequency: T) -> T {
1274        if old_frequency <= T::zero() {
1275            return T::zero();
1276        }
1277        (old_frequency - new_frequency) / old_frequency
1278    }
1279
1280    /// Calculate thermal impact (°C-equivalent reduction) of a power
1281    /// change, via the thermal model's resistance: `ΔP · R_thermal`.
1282    fn calculate_thermal_impact(&self, old_power: T, newpower: T) -> T {
1283        let power_reduction = old_power - newpower;
1284        power_reduction * self.thermal_manager.thermal_model.thermal_resistance
1285    }
1286
1287    /// Evaluate strategy effectiveness
1288    fn evaluate_strategy_effectiveness(&mut self, result: &EnergyOptimizationResult<T>) {
1289        // Calculate effectiveness score
1290        let effectiveness =
1291            result.energy_saved / (result.optimization_overhead + to_t_or(1e-6, T::zero()));
1292
1293        // Update strategy effectiveness history
1294        *self
1295            .strategy_effectiveness
1296            .entry(result.strategy_used)
1297            .or_insert(T::zero()) = effectiveness;
1298    }
1299
1300    /// Consider switching optimization strategy
1301    fn consider_strategy_switch(&mut self) -> Result<()> {
1302        if let Some(&current_effectiveness) =
1303            self.strategy_effectiveness.get(&self.current_strategy)
1304        {
1305            // Find best alternative strategy
1306            if let Some((&best_strategy, &best_effectiveness)) = self
1307                .strategy_effectiveness
1308                .iter()
1309                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
1310            {
1311                // Switch if improvement exceeds threshold (guarded against
1312                // division by a zero/negative baseline effectiveness).
1313                if current_effectiveness > T::zero() {
1314                    let improvement =
1315                        (best_effectiveness - current_effectiveness) / current_effectiveness;
1316                    if improvement > self.config.strategy_switching_threshold {
1317                        self.current_strategy = best_strategy;
1318                    }
1319                } else if best_effectiveness > T::zero() {
1320                    self.current_strategy = best_strategy;
1321                }
1322            }
1323        }
1324
1325        Ok(())
1326    }
1327
1328    /// Update optimization metrics
1329    fn update_metrics(&mut self, _result: &EnergyOptimizationResult<T>) {
1330        self.metrics.energy_consumption = self.system_state.current_energy;
1331        self.metrics.power_consumption = self.system_state.current_power;
1332        let ambient = to_t_or(25.0, T::one());
1333        self.metrics.thermal_efficiency = if self.system_state.temperature > T::zero() {
1334            ambient / self.system_state.temperature
1335        } else {
1336            T::one()
1337        };
1338    }
1339
1340    /// Get current energy budget status
1341    pub fn get_energy_budget_status(&self) -> EnergyBudgetStatus<T> {
1342        let remaining_budget =
1343            self.config.energy_budget.total_budget - self.system_state.current_energy;
1344        let budget_utilization =
1345            self.system_state.current_energy / self.config.energy_budget.total_budget;
1346
1347        EnergyBudgetStatus {
1348            total_budget: self.config.energy_budget.total_budget,
1349            current_consumption: self.system_state.current_energy,
1350            remaining_budget,
1351            budget_utilization,
1352            emergency_reserve_available: remaining_budget
1353                > self.config.energy_budget.emergency_reserves,
1354        }
1355    }
1356
1357    /// Get current metrics
1358    pub fn get_metrics(&self) -> &NeuromorphicMetrics<T> {
1359        &self.metrics
1360    }
1361
1362    /// Get current system state
1363    /// Number of power domains currently power-gated.
1364    ///
1365    /// The gating decision is computed per optimization step from the
1366    /// workload's idle fraction; before this accessor existed the result was
1367    /// recorded and never read, so callers had no way to see whether power
1368    /// gating had actually engaged.
1369    pub fn gated_domain_count(&self) -> usize {
1370        self.power_gating_controller
1371            .gated_groups
1372            .values()
1373            .filter(|group| group.is_gated)
1374            .count()
1375    }
1376
1377    pub fn get_system_state(&self) -> &EnergySystemState<T> {
1378        &self.system_state
1379    }
1380}
1381
1382/// Energy optimization result
1383#[derive(Debug, Clone)]
1384pub struct EnergyOptimizationResult<T: Float + Debug + Send + Sync + 'static> {
1385    /// Strategy that was used
1386    pub strategy_used: EnergyOptimizationStrategy,
1387
1388    /// Energy saved (nJ)
1389    pub energy_saved: T,
1390
1391    /// Power reduction (nW)
1392    pub power_reduction: T,
1393
1394    /// Performance impact (ratio)
1395    pub performance_impact: T,
1396
1397    /// Thermal impact (°C reduction)
1398    pub thermal_impact: T,
1399
1400    /// Optimization overhead (nJ)
1401    pub optimization_overhead: T,
1402}
1403
1404/// Energy budget status
1405#[derive(Debug, Clone)]
1406pub struct EnergyBudgetStatus<T: Float + Debug + Send + Sync + 'static> {
1407    /// Total energy budget (nJ)
1408    pub total_budget: T,
1409
1410    /// Current energy consumption (nJ)
1411    pub current_consumption: T,
1412
1413    /// Remaining budget (nJ)
1414    pub remaining_budget: T,
1415
1416    /// Budget utilization (0.0 to 1.0)
1417    pub budget_utilization: T,
1418
1419    /// Emergency reserve available
1420    pub emergency_reserve_available: bool,
1421}
1422
1423impl<
1424        T: Float
1425            + Debug
1426            + Send
1427            + Sync
1428            + scirs2_core::ndarray::ScalarOperand
1429            + std::fmt::Debug
1430            + std::iter::Sum,
1431    > EnergyMonitor<T>
1432{
1433    fn new(_monitoringfrequency: Duration) -> Self {
1434        Self {
1435            consumption_history: VecDeque::new(),
1436            power_history: VecDeque::new(),
1437            current_power: T::zero(),
1438            peak_power: T::zero(),
1439            average_power: T::zero(),
1440            last_update: Instant::now(),
1441            window_size: Duration::from_secs(1),
1442        }
1443    }
1444
1445    fn update(&mut self, systemstate: &EnergySystemState<T>) -> Result<()> {
1446        let now = Instant::now();
1447        self.consumption_history
1448            .push_back((now, systemstate.current_energy));
1449        self.power_history
1450            .push_back((now, systemstate.current_power));
1451
1452        // Clean old entries out of both histories (F59: `power_history` was
1453        // previously never trimmed and grew without bound).
1454        while let Some(&(time_, _)) = self.consumption_history.front() {
1455            if now.duration_since(time_) > self.window_size {
1456                self.consumption_history.pop_front();
1457            } else {
1458                break;
1459            }
1460        }
1461        while let Some(&(time_, _)) = self.power_history.front() {
1462            if now.duration_since(time_) > self.window_size {
1463                self.power_history.pop_front();
1464            } else {
1465                break;
1466            }
1467        }
1468
1469        // Update current metrics
1470        self.current_power = systemstate.current_power;
1471        self.peak_power = self.peak_power.max(systemstate.current_power);
1472
1473        // Update average power
1474        if !self.power_history.is_empty() {
1475            let sum: T = self.power_history.iter().map(|(_, power)| *power).sum();
1476            self.average_power = sum / to_t_or(self.power_history.len() as f64, T::one());
1477        }
1478
1479        self.last_update = now;
1480        Ok(())
1481    }
1482}
1483
1484#[cfg(test)]
1485#[path = "energy_efficient_tests.rs"]
1486mod tests;