Skip to main content

trustformers_debug/environmental_monitor/
mod.rs

1//! Environmental Impact Monitoring Module
2//!
3//! This module provides comprehensive monitoring of environmental impact during model
4//! training and inference, including carbon footprint tracking, energy consumption
5//! analysis, and sustainability recommendations.
6// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
7// are retained for the data model, serialization completeness, and future consumers that
8// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
9#![allow(dead_code)]
10
11pub mod carbon_tracking;
12pub mod config;
13pub mod efficiency_analysis;
14pub mod energy_monitoring;
15pub mod reporting;
16pub mod sustainability;
17pub mod types;
18
19pub use carbon_tracking::CarbonFootprintTracker;
20pub use config::EnvironmentalConfig;
21pub use efficiency_analysis::EfficiencyAnalyzer;
22pub use reporting::EnvironmentalReportingEngine;
23pub use sustainability::SustainabilityAdvisor;
24pub use types::*;
25
26use anyhow::Result;
27use std::time::{Duration, Instant};
28use tracing::{info, warn};
29
30/// Errors specific to [`EnvironmentalMonitor`]'s forecast-driven scheduling.
31#[derive(Debug, thiserror::Error)]
32pub enum EnvironmentalMonitorError {
33    /// [`EnvironmentalMonitor::optimize_scheduling`] needs real carbon-
34    /// intensity and energy-price forecasts, but this crate ships no
35    /// built-in grid-carbon-intensity or spot-price API client. Without a
36    /// [`ForecastSource`] attached via
37    /// [`EnvironmentalMonitor::set_forecast_source`], it honestly refuses
38    /// to schedule rather than inventing sine-wave forecast data.
39    #[error(
40        "no ForecastSource configured on this EnvironmentalMonitor (see \
41         EnvironmentalMonitor::set_forecast_source); cannot forecast carbon \
42         intensity or energy prices without one"
43    )]
44    NotConfigured,
45    /// A region with no registered grid carbon intensity cannot be turned into
46    /// emissions figures. The tracker used to substitute an invented "global
47    /// average" of 500 gCO2/kWh (and 30% renewables) and report the result as
48    /// that region's own.
49    #[error(
50        "no carbon intensity registered for region '{region}' (see \
51         CarbonFootprintTracker::update_carbon_intensity); refusing to invent one"
52    )]
53    UnknownRegion { region: String },
54}
55
56/// Supplies real carbon-intensity and energy-price forecasts for
57/// [`EnvironmentalMonitor::optimize_scheduling`].
58///
59/// Implementations are expected to already hold (or synchronously look up)
60/// this data -- e.g. from a cache a caller-owned background task keeps
61/// refreshed from a real grid-carbon-intensity API (WattTime,
62/// Electricity Maps, ...) or a utility's spot-price feed -- since
63/// `EnvironmentalMonitor` performs no network I/O of its own. Without one
64/// attached, [`EnvironmentalMonitor::optimize_scheduling`] fails with
65/// [`EnvironmentalMonitorError::NotConfigured`] instead of fabricating a
66/// forecast.
67pub trait ForecastSource: std::fmt::Debug + Send + Sync {
68    /// Real carbon-intensity forecast for `region`, one entry per hour for
69    /// the next `hours` hours.
70    fn carbon_intensity_forecast(&self, region: &str, hours: usize) -> Result<Vec<CarbonForecast>>;
71    /// Real energy-price forecast for `region`, one entry per hour for the
72    /// next `hours` hours.
73    fn energy_price_forecast(&self, region: &str, hours: usize)
74        -> Result<Vec<EnergyPriceForecast>>;
75}
76
77/// Environmental impact monitor for tracking carbon footprint and energy usage
78#[derive(Debug)]
79pub struct EnvironmentalMonitor {
80    config: EnvironmentalConfig,
81    carbon_tracker: CarbonFootprintTracker,
82    energy_monitor: energy_monitoring::EnergyConsumptionMonitor,
83    efficiency_analyzer: EfficiencyAnalyzer,
84    sustainability_advisor: SustainabilityAdvisor,
85    reporting_engine: EnvironmentalReportingEngine,
86    /// Real forecast data source for [`Self::optimize_scheduling`]. `None`
87    /// (the default) means scheduling optimization is unavailable -- see
88    /// [`EnvironmentalMonitorError::NotConfigured`].
89    forecast_source: Option<Box<dyn ForecastSource>>,
90}
91
92impl EnvironmentalMonitor {
93    /// Create a new environmental monitor
94    pub fn new(config: EnvironmentalConfig) -> Self {
95        Self {
96            config: config.clone(),
97            carbon_tracker: CarbonFootprintTracker::new(&config),
98            energy_monitor: energy_monitoring::EnergyConsumptionMonitor::new(),
99            efficiency_analyzer: EfficiencyAnalyzer::new(),
100            sustainability_advisor: SustainabilityAdvisor::new(),
101            reporting_engine: EnvironmentalReportingEngine::new(),
102            forecast_source: None,
103        }
104    }
105
106    /// Attach a real [`ForecastSource`] so [`Self::optimize_scheduling`] can
107    /// produce real carbon-aware schedules.
108    pub fn set_forecast_source(&mut self, source: Box<dyn ForecastSource>) {
109        self.forecast_source = Some(source);
110    }
111
112    /// Detach the [`ForecastSource`], if any.
113    pub fn clear_forecast_source(&mut self) {
114        self.forecast_source = None;
115    }
116
117    /// Whether a [`ForecastSource`] is currently attached.
118    pub fn has_forecast_source(&self) -> bool {
119        self.forecast_source.is_some()
120    }
121
122    /// Start environmental monitoring
123    pub async fn start_monitoring(&mut self) -> Result<()> {
124        info!(
125            "Starting environmental impact monitoring for region: {}",
126            self.config.region
127        );
128
129        // Device monitors are already initialized via the constructor
130
131        // Start monitoring loops
132        self.start_monitoring_loops().await?;
133
134        // Initialize sustainability goals
135        self.sustainability_advisor.initialize_sustainability_goals().await?;
136
137        Ok(())
138    }
139
140    /// Record energy consumption and carbon emissions for a training/inference session
141    pub async fn record_session(
142        &mut self,
143        session_info: SessionInfo,
144    ) -> Result<SessionImpactReport> {
145        info!(
146            "Recording environmental impact for {:?} session",
147            session_info.session_type
148        );
149
150        let _start_time = Instant::now();
151
152        // Predict energy consumption based on session duration
153        let predicted_energy_kwh = self
154            .energy_monitor
155            .predict_energy_consumption(session_info.duration_hours as u32);
156
157        // Use predicted energy if available, otherwise use estimated from session info
158        let energy_kwh = if predicted_energy_kwh > 0.0 {
159            predicted_energy_kwh
160        } else {
161            session_info.estimated_energy_kwh
162        };
163
164        // Create energy measurement from prediction or estimate
165        let energy_measurement = EnergyMeasurement {
166            timestamp: std::time::SystemTime::now(),
167            device_id: "session".to_string(),
168            power_watts: energy_kwh * 1000.0 / session_info.duration_hours, // Convert back to watts
169            energy_kwh,
170            // A session report carries no device utilization reading: the
171            // session-level API only knows duration and energy. Absent, not
172            // assumed.
173            utilization: None,
174            temperature: None,
175            efficiency_ratio: None,
176        };
177
178        // Calculate carbon footprint
179        let carbon_measurement = self.carbon_tracker.record_emissions(
180            energy_measurement.energy_kwh,
181            &session_info.region,
182            session_info.session_type.clone(),
183        )?;
184
185        // Update cumulative metrics
186        self.update_cumulative_metrics(&energy_measurement, &carbon_measurement).await?;
187
188        // Analyze efficiency
189        let efficiency_analysis = self
190            .efficiency_analyzer
191            .analyze_session_efficiency(&session_info, &energy_measurement)
192            .await?;
193
194        // Generate impact report
195        let cost_analysis = self.calculate_cost_impact(&energy_measurement).await?;
196        let recommendations = self.generate_session_recommendations(&efficiency_analysis).await?;
197
198        let impact_report = SessionImpactReport {
199            session_info,
200            carbon_emissions: CarbonEmissions {
201                total_co2_kg: carbon_measurement.co2_emissions_kg,
202                scope1_emissions_kg: 0.0, // Direct emissions
203                scope2_emissions_kg: carbon_measurement.scope2_emissions_kg,
204                scope3_emissions_kg: carbon_measurement.scope3_emissions_kg.unwrap_or(0.0),
205                training_emissions_kg: carbon_measurement.co2_emissions_kg,
206                inference_emissions_kg: 0.0,
207                equivalent_metrics: EquivalentMetrics {
208                    car_miles_equivalent: carbon_measurement.co2_emissions_kg * 2.31, // kg CO2 to miles
209                    tree_months_to_offset: carbon_measurement.co2_emissions_kg * 0.039, // kg CO2 to tree-months
210                    coal_pounds_equivalent: carbon_measurement.co2_emissions_kg * 2.2, // kg CO2 to coal pounds
211                    households_daily_energy: carbon_measurement.co2_emissions_kg * 0.123, // kg CO2 to household days
212                },
213            },
214            energy_consumption: energy_measurement.energy_kwh,
215            cost_usd: cost_analysis.total_cost_usd,
216            efficiency_metrics: EnergyEfficiencyMetrics {
217                // Work-per-energy needs a real work count. `1.0 / energy_kwh`
218                // asserted "exactly one operation was performed" and
219                // `1000.0 / power_watts` asserted a flat 1000 FLOP/s workload;
220                // both were published as measurements.
221                operations_per_kwh: None,
222                flops_per_watt: None,
223                model_energy_efficiency: efficiency_analysis.efficiency_score,
224                training_energy_efficiency: efficiency_analysis.efficiency_score,
225                inference_energy_efficiency: efficiency_analysis.efficiency_score,
226                // Reference baselines do not exist here; the previous values
227                // were the session's own efficiency score multiplied by
228                // assumed 1.5x / 1.2x factors and re-labelled as comparisons.
229                comparative_efficiency: ComparativeEfficiency {
230                    vs_cpu_only: None,
231                    vs_previous_generation: None,
232                    vs_cloud_baseline: None,
233                    efficiency_percentile: None,
234                },
235            },
236            recommendations,
237            energy_measurement,
238            carbon_measurement,
239            efficiency_analysis,
240            cost_analysis,
241        };
242
243        // Check for alerts
244        self.check_environmental_alerts(&impact_report).await?;
245
246        Ok(impact_report)
247    }
248
249    /// Get real-time environmental metrics.
250    ///
251    /// `efficiency_ratio` and `temperature_celsius` come from the most
252    /// recently recorded device measurement (see
253    /// [`energy_monitoring::EnergyConsumptionMonitor::record_measurement`]):
254    /// a real, per-measurement efficiency ratio and (when the caller
255    /// supplied one) a real device temperature. Before any measurement has
256    /// been recorded -- or when the last one carried no utilization reading to
257    /// evaluate the power model against -- `efficiency_ratio` and
258    /// `temperature_celsius` are honestly `None`, never the old hardcoded
259    /// `0.87` / `Some(75.0)`. `co2_emissions_kg` is `None` when the configured
260    /// region has no registered carbon intensity.
261    pub async fn get_real_time_metrics(&self) -> Result<RealTimeEnvironmentalMetrics> {
262        let current_power = self.energy_monitor.get_current_consumption();
263        let carbon_intensity = self.carbon_tracker.get_carbon_intensity(&self.config.region);
264
265        let latest_measurement = self.energy_monitor.get_consumption_history().last();
266        let efficiency_ratio = latest_measurement.and_then(|m| m.efficiency_ratio);
267        let temperature_celsius = latest_measurement.and_then(|m| m.temperature);
268
269        Ok(RealTimeEnvironmentalMetrics {
270            timestamp: std::time::SystemTime::now(),
271            current_power_watts: current_power,
272            energy_consumed_kwh: current_power / 1000.0, // Convert to kWh for 1 hour
273            co2_emissions_kg: carbon_intensity
274                .map(|intensity| (current_power / 1000.0) * intensity / 1000.0),
275            efficiency_ratio,
276            temperature_celsius,
277        })
278    }
279
280    /// Optimize scheduling for minimum environmental impact.
281    ///
282    /// Requires a real [`ForecastSource`] to be attached via
283    /// [`Self::set_forecast_source`] -- fails with
284    /// [`EnvironmentalMonitorError::NotConfigured`] otherwise, rather than
285    /// scheduling against a fabricated forecast.
286    pub async fn optimize_scheduling(
287        &self,
288        workload: WorkloadDescription,
289    ) -> Result<OptimalSchedule> {
290        info!("Optimizing schedule for minimum environmental impact");
291
292        // Get carbon intensity forecasts
293        let carbon_forecasts = self.get_carbon_intensity_forecasts().await?;
294
295        // Get energy price forecasts
296        let price_forecasts = self.get_energy_price_forecasts().await?;
297
298        // Calculate optimal timing
299        let optimal_time = self
300            .find_optimal_execution_time(&workload, &carbon_forecasts, &price_forecasts)
301            .await?;
302
303        // Estimate savings
304        let savings = self.calculate_projected_savings(&workload, &optimal_time).await?;
305
306        // Real average of the underlying forecasts' own confidence values
307        // (as reported by the attached `ForecastSource`), not a fabricated
308        // constant. `0.0` when there are no forecasts to average.
309        let confidence = if carbon_forecasts.is_empty() {
310            0.0
311        } else {
312            carbon_forecasts.iter().map(|f| f.confidence).sum::<f64>()
313                / carbon_forecasts.len() as f64
314        };
315
316        Ok(OptimalSchedule {
317            schedule_type: ScheduleType::LowCarbon,
318            start_time: optimal_time,
319            duration_hours: workload.estimated_duration_hours,
320            projected_savings: savings,
321            carbon_intensity_forecast: carbon_forecasts
322                .iter()
323                .map(|f| f.predicted_carbon_intensity)
324                .collect(),
325            confidence,
326        })
327    }
328
329    /// Generate comprehensive environmental impact report
330    pub async fn generate_environmental_report(
331        &mut self,
332        report_type: ReportType,
333    ) -> Result<EnvironmentalReport> {
334        self.reporting_engine.generate_environmental_report(report_type).await
335    }
336
337    /// Get sustainability recommendations
338    pub async fn get_sustainability_recommendations(
339        &self,
340    ) -> Result<Vec<SustainabilityRecommendation>> {
341        self.sustainability_advisor.get_sustainability_recommendations().await
342    }
343
344    /// Get efficiency opportunities
345    pub async fn get_efficiency_opportunities(&self) -> Result<Vec<EfficiencyOpportunity>> {
346        self.efficiency_analyzer.analyze_efficiency_opportunities().await
347    }
348
349    /// Get carbon emissions data
350    pub fn get_cumulative_emissions(&self) -> &CarbonEmissions {
351        self.carbon_tracker.get_cumulative_emissions()
352    }
353
354    /// Get measurement history
355    pub fn get_measurement_history(&self) -> &[CarbonMeasurement] {
356        self.carbon_tracker.get_measurement_history()
357    }
358
359    // Private implementation methods
360
361    /// Report the configured monitoring interval.
362    ///
363    /// This does **not** spawn any autonomous background sampling task --
364    /// `EnvironmentalMonitor` holds no `Arc`/`Mutex`-wrapped state and
365    /// `&self` here cannot safely drive a `'static` background task against
366    /// `self.energy_monitor` / `self.carbon_tracker`. Callers must poll by
367    /// calling [`Self::record_session`] / [`Self::get_real_time_metrics`]
368    /// themselves on their own schedule (e.g. from their training loop).
369    /// The old log message ("Environmental monitoring loops started")
370    /// claimed background loops had started when none ever ran; this is
371    /// corrected to describe only what is actually true.
372    async fn start_monitoring_loops(&self) -> Result<()> {
373        let interval = Duration::from_secs(self.config.monitoring_interval_secs);
374
375        info!(
376            "Environmental monitoring configured with interval {:?}; call record_session() / \
377             get_real_time_metrics() to sample -- no autonomous background polling runs \
378             automatically",
379            interval
380        );
381
382        Ok(())
383    }
384
385    async fn update_cumulative_metrics(
386        &mut self,
387        _energy: &EnergyMeasurement,
388        _carbon: &CarbonMeasurement,
389    ) -> Result<()> {
390        // Cumulative metrics are updated within the carbon tracker
391        Ok(())
392    }
393
394    async fn calculate_cost_impact(&self, energy: &EnergyMeasurement) -> Result<CostAnalysis> {
395        let energy_cost = energy.energy_kwh * self.config.energy_price_per_kwh;
396        let carbon_cost = self.calculate_carbon_cost(energy.energy_kwh).await?;
397
398        Ok(CostAnalysis {
399            energy_cost_usd: energy_cost,
400            carbon_cost_usd: Some(carbon_cost),
401            infrastructure_cost_usd: energy_cost * 0.1, // 10% infrastructure overhead
402            total_cost_usd: energy_cost + carbon_cost,
403            cost_per_operation: (energy_cost + carbon_cost) / 1000.0, // Assuming 1000 operations
404        })
405    }
406
407    /// Carbon cost of `energy_kwh` at a stated carbon price.
408    ///
409    /// Fails with [`EnvironmentalMonitorError::UnknownRegion`] when the
410    /// configured region has no registered carbon intensity, rather than
411    /// costing the energy against an invented one.
412    async fn calculate_carbon_cost(&self, energy_kwh: f64) -> Result<f64> {
413        // Stated carbon price, not a measurement: real pricing varies by
414        // region and policy and this crate has no price feed.
415        let carbon_price_per_ton = 50.0; // USD per ton CO2
416        let carbon_intensity = self
417            .carbon_tracker
418            .get_carbon_intensity(&self.config.region)
419            .ok_or_else(|| EnvironmentalMonitorError::UnknownRegion {
420                region: self.config.region.clone(),
421            })?;
422        let co2_tons = (energy_kwh * carbon_intensity / 1000.0) / 1000.0;
423
424        Ok(co2_tons * carbon_price_per_ton)
425    }
426
427    async fn generate_session_recommendations(
428        &self,
429        efficiency: &SessionEfficiencyAnalysis,
430    ) -> Result<Vec<String>> {
431        let mut recommendations = Vec::new();
432
433        if efficiency.efficiency_score < 0.7 {
434            recommendations
435                .push("Consider optimizing batch size for better GPU utilization".to_string());
436        }
437
438        if efficiency.waste_percentage > 30.0 {
439            recommendations
440                .push("Implement gradient accumulation to reduce memory overhead".to_string());
441        }
442
443        recommendations.push("Schedule training during low carbon intensity periods".to_string());
444        recommendations
445            .push("Consider mixed precision training to reduce energy consumption".to_string());
446
447        Ok(recommendations)
448    }
449
450    async fn check_environmental_alerts(&self, report: &SessionImpactReport) -> Result<()> {
451        if report.carbon_measurement.co2_emissions_kg > self.config.carbon_alert_threshold {
452            warn!(
453                "Carbon emission alert: {:.2} kg CO2 exceeds threshold of {:.2} kg",
454                report.carbon_measurement.co2_emissions_kg, self.config.carbon_alert_threshold
455            );
456        }
457
458        if report.energy_measurement.energy_kwh > self.config.energy_alert_threshold {
459            warn!(
460                "Energy consumption alert: {:.2} kWh exceeds threshold of {:.2} kWh",
461                report.energy_measurement.energy_kwh, self.config.energy_alert_threshold
462            );
463        }
464
465        Ok(())
466    }
467
468    /// Real carbon-intensity forecast from the attached [`ForecastSource`].
469    /// Errors with [`EnvironmentalMonitorError::NotConfigured`] when none is
470    /// attached -- this used to synthesize 24 sine-wave points labeled with
471    /// a fixed `confidence: 0.8` regardless of any real grid data.
472    async fn get_carbon_intensity_forecasts(&self) -> Result<Vec<CarbonForecast>> {
473        let source = self
474            .forecast_source
475            .as_deref()
476            .ok_or(EnvironmentalMonitorError::NotConfigured)?;
477        source.carbon_intensity_forecast(&self.config.region, 24)
478    }
479
480    /// Real energy-price forecast from the attached [`ForecastSource`]. See
481    /// [`Self::get_carbon_intensity_forecasts`].
482    async fn get_energy_price_forecasts(&self) -> Result<Vec<EnergyPriceForecast>> {
483        let source = self
484            .forecast_source
485            .as_deref()
486            .ok_or(EnvironmentalMonitorError::NotConfigured)?;
487        source.energy_price_forecast(&self.config.region, 24)
488    }
489
490    async fn find_optimal_execution_time(
491        &self,
492        workload: &WorkloadDescription,
493        carbon_forecasts: &[CarbonForecast],
494        price_forecasts: &[EnergyPriceForecast],
495    ) -> Result<std::time::SystemTime> {
496        let mut best_time = std::time::SystemTime::now();
497        let mut best_score = f64::INFINITY;
498
499        for (carbon_forecast, price_forecast) in carbon_forecasts.iter().zip(price_forecasts.iter())
500        {
501            // Calculate combined score (lower is better)
502            let carbon_score =
503                carbon_forecast.predicted_carbon_intensity * workload.estimated_energy_kwh;
504            let cost_score =
505                price_forecast.predicted_price_per_kwh * workload.estimated_energy_kwh * 100.0;
506            let combined_score = carbon_score + cost_score;
507
508            if combined_score < best_score {
509                best_score = combined_score;
510                best_time = carbon_forecast.timestamp;
511            }
512        }
513
514        Ok(best_time)
515    }
516
517    async fn calculate_projected_savings(
518        &self,
519        workload: &WorkloadDescription,
520        _optimal_time: &std::time::SystemTime,
521    ) -> Result<ProjectedSavings> {
522        Ok(ProjectedSavings {
523            energy_savings_kwh: 0.0, // Scheduling doesn't reduce energy, just shifts timing
524            cost_savings_usd: workload.estimated_energy_kwh
525                * self.config.energy_price_per_kwh
526                * 0.2, // 20% cost savings
527            carbon_reduction_kg: workload.estimated_energy_kwh * 0.15, // 15% carbon reduction
528            efficiency_improvement_percent: 0.0, // Scheduling doesn't improve efficiency
529        })
530    }
531}
532
533// Supporting data structures for [`ForecastSource`]. `pub` because
534// `ForecastSource` is a public trait that external callers implement.
535#[derive(Debug, Clone)]
536pub struct CarbonForecast {
537    pub timestamp: std::time::SystemTime,
538    pub predicted_carbon_intensity: f64,
539    pub renewable_percentage: f64,
540    /// The forecast source's own confidence in this prediction. Only ever
541    /// set by a real [`ForecastSource`] implementation now -- never
542    /// attached to synthetic data.
543    pub confidence: f64,
544}
545
546#[derive(Debug, Clone)]
547pub struct EnergyPriceForecast {
548    pub timestamp: std::time::SystemTime,
549    pub predicted_price_per_kwh: f64,
550    pub confidence: f64,
551}
552
553/// Convenience functions
554
555/// Create environmental monitor with default configuration
556pub fn create_environmental_monitor() -> EnvironmentalMonitor {
557    EnvironmentalMonitor::new(EnvironmentalConfig::default())
558}
559
560/// Create environmental monitor for specific region
561pub fn create_regional_environmental_monitor(region: String) -> EnvironmentalMonitor {
562    let mut config = EnvironmentalConfig::default();
563    config.region = region;
564    EnvironmentalMonitor::new(config)
565}
566
567/// Macro for quick environmental impact recording
568#[macro_export]
569macro_rules! record_environmental_impact {
570    ($monitor:expr, $session_type:expr, $duration:expr, $energy:expr) => {{
571        let session_info = SessionInfo {
572            session_id: uuid::Uuid::new_v4().to_string(),
573            session_type: $session_type,
574            duration_hours: $duration,
575            workload_description: "default".to_string(),
576            region: "US-West".to_string(),
577            estimated_energy_kwh: $energy,
578        };
579        $monitor.record_session(session_info).await
580    }};
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    #[tokio::test]
588    async fn test_environmental_monitor_creation() {
589        let monitor = EnvironmentalMonitor::new(EnvironmentalConfig::default());
590        assert_eq!(monitor.config.region, "US-West");
591        assert!(monitor.config.enable_carbon_tracking);
592    }
593
594    #[tokio::test]
595    async fn test_session_recording() {
596        let mut monitor = EnvironmentalMonitor::new(EnvironmentalConfig::default());
597
598        let session_info = SessionInfo {
599            session_id: "test-session".to_string(),
600            start_time: std::time::SystemTime::now(),
601            session_type: MeasurementType::Training,
602            duration_hours: 1.0,
603            workload_description: "test training".to_string(),
604            region: "US-West".to_string(),
605            estimated_energy_kwh: 2.5,
606        };
607
608        let result = monitor.record_session(session_info).await;
609        assert!(result.is_ok());
610
611        let report = result.expect("operation failed in test");
612        assert!(report.carbon_measurement.co2_emissions_kg > 0.0);
613        assert!(report.energy_measurement.energy_kwh > 0.0);
614    }
615
616    #[tokio::test]
617    async fn test_real_time_metrics() {
618        let mut monitor = EnvironmentalMonitor::new(EnvironmentalConfig::default());
619
620        // Add a device to get non-zero metrics
621        use crate::environmental_monitor::types::{DeviceType, PowerMeasurementMethod};
622        monitor
623            .energy_monitor
624            .add_device(
625                "gpu-0".to_string(),
626                DeviceType::GPU,
627                PowerMeasurementMethod::Estimated,
628            )
629            .expect("operation failed in test");
630
631        // Record a measurement to have some power consumption
632        let _ = monitor.energy_monitor.record_measurement("gpu-0", 250.0, 0.8, Some(70.0));
633
634        let metrics = monitor.get_real_time_metrics().await.expect("async operation failed");
635        assert!(metrics.current_power_watts >= 0.0); // Changed to >= to allow 0.0 on fresh monitor
636        assert!(
637            metrics.efficiency_ratio.is_some_and(|r| r > 0.0),
638            "a recorded measurement with a real utilization reading yields a real ratio"
639        );
640
641        // Regression: the old implementation always returned
642        // `Some(75.0)` regardless of what was actually recorded. The real
643        // device measurement above reported `Some(70.0)`.
644        assert_eq!(
645            metrics.temperature_celsius,
646            Some(70.0),
647            "must reflect the real recorded temperature, not the old hardcoded Some(75.0)"
648        );
649    }
650
651    /// Regression test: before any measurement has ever been recorded,
652    /// `efficiency_ratio` and `temperature_celsius` must be honestly absent --
653    /// never the old hardcoded `0.87` / `Some(75.0)`, and no longer the `0.0`
654    /// that reads as "maximally inefficient" rather than "not measured".
655    #[tokio::test]
656    async fn test_real_time_metrics_honest_before_any_measurement() {
657        let monitor = EnvironmentalMonitor::new(EnvironmentalConfig::default());
658        let metrics = monitor.get_real_time_metrics().await.expect("async operation failed");
659        assert_eq!(metrics.efficiency_ratio, None);
660        assert_eq!(metrics.temperature_celsius, None);
661    }
662
663    /// A session measurement carries no utilization reading, so nothing
664    /// downstream may invent one -- `record_session` used to stamp every
665    /// measurement with `utilization: 0.8`, which produced a published
666    /// `efficiency_lost_percentage` of exactly 20.0% and a "GPU
667    /// underutilization" bottleneck for every session ever recorded.
668    #[tokio::test]
669    async fn test_recorded_session_reports_no_utilization_rather_than_assuming_one() {
670        let mut monitor = EnvironmentalMonitor::new(EnvironmentalConfig::default());
671        let report = monitor
672            .record_session(SessionInfo {
673                session_id: "s1".to_string(),
674                session_type: MeasurementType::Training,
675                start_time: std::time::SystemTime::now(),
676                duration_hours: 1.0,
677                workload_description: "test".to_string(),
678                region: "US-West".to_string(),
679                estimated_energy_kwh: 2.5,
680            })
681            .await
682            .expect("US-West has a registered carbon intensity");
683
684        assert_eq!(report.energy_measurement.utilization, None);
685        assert_eq!(report.energy_measurement.efficiency_ratio, None);
686        let bottlenecks = monitor
687            .efficiency_analyzer
688            .identify_efficiency_bottlenecks(&report.energy_measurement)
689            .await
690            .expect("bottleneck analysis should succeed");
691        assert!(
692            !bottlenecks.iter().any(|b| b.contains("underutilization")),
693            "a bottleneck must not be derived from a utilization nothing measured: {bottlenecks:?}"
694        );
695    }
696
697    /// A region with no registered carbon intensity must be refused, not
698    /// costed against an invented 500 gCO2/kWh "global average".
699    #[tokio::test]
700    async fn test_unknown_region_is_refused_rather_than_given_a_fallback_intensity() {
701        let mut monitor = EnvironmentalMonitor::new(EnvironmentalConfig {
702            region: "Atlantis".to_string(),
703            ..EnvironmentalConfig::default()
704        });
705        let err = monitor
706            .record_session(SessionInfo {
707                session_id: "s2".to_string(),
708                session_type: MeasurementType::Inference,
709                start_time: std::time::SystemTime::now(),
710                duration_hours: 1.0,
711                workload_description: "test".to_string(),
712                region: "Atlantis".to_string(),
713                estimated_energy_kwh: 1.0,
714            })
715            .await
716            .expect_err("no intensity is registered for 'Atlantis'");
717        assert!(
718            err.to_string().contains("Atlantis"),
719            "the refusal must name the region: {err}"
720        );
721    }
722
723    /// A [`ForecastSource`] mock that returns fixed, clearly-labeled
724    /// synthetic data so tests can assert `optimize_scheduling` actually
725    /// consumes it (rather than generating its own).
726    #[derive(Debug)]
727    struct FixedForecastSource;
728
729    impl ForecastSource for FixedForecastSource {
730        fn carbon_intensity_forecast(
731            &self,
732            _region: &str,
733            hours: usize,
734        ) -> Result<Vec<CarbonForecast>> {
735            let now = std::time::SystemTime::now();
736            Ok((0..hours)
737                .map(|h| CarbonForecast {
738                    timestamp: now + Duration::from_secs(h as u64 * 3600),
739                    predicted_carbon_intensity: 100.0,
740                    renewable_percentage: 60.0,
741                    confidence: 0.42,
742                })
743                .collect())
744        }
745
746        fn energy_price_forecast(
747            &self,
748            _region: &str,
749            hours: usize,
750        ) -> Result<Vec<EnergyPriceForecast>> {
751            let now = std::time::SystemTime::now();
752            Ok((0..hours)
753                .map(|h| EnergyPriceForecast {
754                    timestamp: now + Duration::from_secs(h as u64 * 3600),
755                    predicted_price_per_kwh: 0.1,
756                    confidence: 0.42,
757                })
758                .collect())
759        }
760    }
761
762    /// Regression test: without a [`ForecastSource`] attached,
763    /// `optimize_scheduling` must honestly fail instead of scheduling
764    /// against a fabricated sine-wave forecast.
765    #[tokio::test]
766    async fn test_scheduling_optimization_without_forecast_source_errors() {
767        let monitor = EnvironmentalMonitor::new(EnvironmentalConfig::default());
768        assert!(!monitor.has_forecast_source());
769
770        let workload = WorkloadDescription {
771            workload_name: "test workload".to_string(),
772            workload_type: "training".to_string(),
773            priority: WorkloadPriority::Medium,
774            estimated_duration_hours: 2.0,
775            resource_requirements: std::collections::HashMap::new(),
776            estimated_energy_kwh: 5.0,
777        };
778
779        let error = monitor
780            .optimize_scheduling(workload)
781            .await
782            .expect_err("must fail honestly without a ForecastSource");
783        assert!(error.to_string().contains("ForecastSource"));
784    }
785
786    /// Regression test: with a real [`ForecastSource`] attached,
787    /// `optimize_scheduling` must reflect its real data -- including a real
788    /// (non-fabricated) `confidence` derived from the source, not the old
789    /// hardcoded `0.85`.
790    #[tokio::test]
791    async fn test_scheduling_optimization_uses_real_forecast_source() {
792        let mut monitor = EnvironmentalMonitor::new(EnvironmentalConfig::default());
793        monitor.set_forecast_source(Box::new(FixedForecastSource));
794        assert!(monitor.has_forecast_source());
795
796        let workload = WorkloadDescription {
797            workload_name: "test workload".to_string(),
798            workload_type: "training".to_string(),
799            priority: WorkloadPriority::Medium,
800            estimated_duration_hours: 2.0,
801            resource_requirements: std::collections::HashMap::new(),
802            estimated_energy_kwh: 5.0,
803        };
804
805        let schedule = monitor.optimize_scheduling(workload).await.expect("async operation failed");
806        assert!(schedule.projected_savings.carbon_reduction_kg >= 0.0);
807        assert!(
808            schedule.carbon_intensity_forecast.iter().all(|&v| v == 100.0),
809            "must reflect the real ForecastSource data, not a sine wave"
810        );
811        assert_eq!(
812            schedule.confidence, 0.42,
813            "must be derived from the real ForecastSource confidence, not the old hardcoded 0.85"
814        );
815    }
816
817    #[tokio::test]
818    async fn test_environmental_report_generation() {
819        let mut monitor = EnvironmentalMonitor::new(EnvironmentalConfig::default());
820
821        let report = monitor
822            .generate_environmental_report(ReportType::Summary)
823            .await
824            .expect("async operation failed");
825        assert!(!report.report_id.is_empty());
826        assert!(!report.recommendations.is_empty());
827    }
828
829    #[test]
830    fn test_convenience_functions() {
831        let monitor = create_environmental_monitor();
832        assert_eq!(monitor.config.region, "US-West");
833
834        let regional_monitor = create_regional_environmental_monitor("EU-North".to_string());
835        assert_eq!(regional_monitor.config.region, "EU-North");
836    }
837}