use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EnergyBudget {
pub max_millijoules: f64,
}
impl EnergyBudget {
pub fn new(max_millijoules: f64) -> Self {
assert!(
max_millijoules >= 0.0,
"Energy budget cannot be negative: {max_millijoules}"
);
Self { max_millijoules }
}
pub fn is_exceeded_by(&self, measured_millijoules: f64) -> bool {
measured_millijoules > self.max_millijoules
}
pub fn usage_percent(&self, measured_millijoules: f64) -> f64 {
if self.max_millijoules == 0.0 {
if measured_millijoules > 0.0 {
return f64::INFINITY;
}
return 0.0;
}
(measured_millijoules / self.max_millijoules) * 100.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CarbonBudget {
pub max_mg_co2: f64,
}
impl CarbonBudget {
pub fn new(max_mg_co2: f64) -> Self {
assert!(
max_mg_co2 >= 0.0,
"Carbon budget cannot be negative: {max_mg_co2}"
);
Self { max_mg_co2 }
}
pub fn is_exceeded_by(&self, measured_mg_co2: f64) -> bool {
measured_mg_co2 > self.max_mg_co2
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CarbonProvider {
Watttime,
ElectricityMaps,
Static,
}
impl CarbonProvider {
pub fn display_name(&self) -> &'static str {
match self {
CarbonProvider::Watttime => "WattTime",
CarbonProvider::ElectricityMaps => "Electricity Maps",
CarbonProvider::Static => "Static",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CarbonConfig {
pub provider: CarbonProvider,
pub region: String,
#[serde(rename = "static-intensity", default)]
pub static_intensity: f64,
}
impl CarbonConfig {
pub fn estimate_carbon_mg(&self, millijoules: f64) -> f64 {
millijoules * self.static_intensity / 3_600_000_000.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReportFormat {
Text,
Json,
Csrd,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReportConfig {
pub format: ReportFormat,
#[serde(rename = "include-recommendations", default)]
pub include_recommendations: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionBudget {
pub name: String,
pub source: String,
#[serde(rename = "energy-budget-mj")]
pub energy_budget_mj: Option<f64>,
#[serde(rename = "carbon-budget-mg")]
pub carbon_budget_mg: Option<f64>,
}
impl FunctionBudget {
pub fn energy_budget(&self) -> Option<EnergyBudget> {
self.energy_budget_mj.map(EnergyBudget::new)
}
pub fn carbon_budget(&self) -> Option<CarbonBudget> {
self.carbon_budget_mg.map(CarbonBudget::new)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ComplianceStatus {
Compliant,
Warning,
Exceeded,
Unbounded,
}
impl ComplianceStatus {
pub fn from_usage_percent(percent: f64) -> Self {
if percent > 100.0 {
ComplianceStatus::Exceeded
} else if percent > 80.0 {
ComplianceStatus::Warning
} else {
ComplianceStatus::Compliant
}
}
pub fn label(&self) -> &'static str {
match self {
ComplianceStatus::Compliant => "COMPLIANT",
ComplianceStatus::Warning => "WARNING",
ComplianceStatus::Exceeded => "EXCEEDED",
ComplianceStatus::Unbounded => "UNBOUNDED",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionMeasurement {
pub function_name: String,
pub measured_energy_mj: f64,
pub estimated_carbon_mg: f64,
pub energy_compliance: ComplianceStatus,
pub carbon_compliance: ComplianceStatus,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SustainabilityReport {
pub project_name: String,
pub region: String,
pub provider: String,
pub measurements: Vec<FunctionMeasurement>,
pub total_energy_mj: f64,
pub total_carbon_mg: f64,
pub all_compliant: bool,
pub recommendations: Vec<String>,
}
impl SustainabilityReport {
pub fn energy_violations(&self) -> usize {
self.measurements
.iter()
.filter(|m| m.energy_compliance == ComplianceStatus::Exceeded)
.count()
}
pub fn carbon_violations(&self) -> usize {
self.measurements
.iter()
.filter(|m| m.carbon_compliance == ComplianceStatus::Exceeded)
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_energy_budget_compliance() {
let budget = EnergyBudget::new(50.0);
assert!(!budget.is_exceeded_by(49.9));
assert!(!budget.is_exceeded_by(50.0));
assert!(budget.is_exceeded_by(50.1));
assert!((budget.usage_percent(25.0) - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_carbon_budget_compliance() {
let budget = CarbonBudget::new(10.0);
assert!(!budget.is_exceeded_by(10.0));
assert!(budget.is_exceeded_by(10.001));
}
#[test]
fn test_carbon_estimation() {
let config = CarbonConfig {
provider: CarbonProvider::Static,
region: "GB".to_string(),
static_intensity: 200.0,
};
let carbon = config.estimate_carbon_mg(3_600_000.0);
assert!((carbon - 0.2).abs() < 1e-6);
}
#[test]
fn test_compliance_status_from_percent() {
assert_eq!(
ComplianceStatus::from_usage_percent(50.0),
ComplianceStatus::Compliant
);
assert_eq!(
ComplianceStatus::from_usage_percent(85.0),
ComplianceStatus::Warning
);
assert_eq!(
ComplianceStatus::from_usage_percent(101.0),
ComplianceStatus::Exceeded
);
}
#[test]
fn test_function_budget_builders() {
let fb = FunctionBudget {
name: "process".to_string(),
source: "src/lib.rs".to_string(),
energy_budget_mj: Some(50.0),
carbon_budget_mg: None,
};
assert!(fb.energy_budget().is_some());
assert!(fb.carbon_budget().is_none());
}
}