use std::collections::HashMap;
use std::sync::LazyLock;
pub(crate) const SPECPOWER_VINTAGE: &str = "2026-04-24 (CCF aligned)";
#[allow(dead_code)]
pub(crate) const CCF_LEGACY_VINTAGE: &str = "2026-04-24";
#[allow(dead_code)]
pub(crate) const DRAM_PREMIUM_W_PER_GB_IDLE: f64 = 0.02;
#[allow(dead_code)]
pub(crate) const DRAM_PREMIUM_W_PER_GB_MAX: f64 = 0.05;
static INSTANCE_POWER: LazyLock<HashMap<&'static str, (f64, f64)>> = LazyLock::new(|| {
let entries: &[(&str, f64, f64)] = &[
("t3.nano", 1.4, 8.1),
("t3.micro", 1.4, 8.1),
("t3.small", 1.4, 8.1),
("t3.medium", 1.4, 8.1),
("t3.large", 1.4, 8.1),
("t3.xlarge", 2.8, 16.3),
("t3.2xlarge", 5.5, 32.5),
("t3a.nano", 1.7, 5.2),
("t3a.micro", 1.7, 5.2),
("t3a.small", 1.7, 5.2),
("t3a.medium", 1.7, 5.2),
("t3a.large", 1.7, 5.2),
("t3a.xlarge", 3.4, 10.4),
("t3a.2xlarge", 6.8, 20.8),
("m5.large", 1.4, 8.1),
("m5.xlarge", 2.8, 16.3),
("m5.2xlarge", 5.5, 32.5),
("m5.4xlarge", 11.0, 65.0),
("m5.8xlarge", 22.1, 130.0),
("m5.12xlarge", 33.1, 195.0),
("m5.16xlarge", 44.2, 260.0),
("m5.24xlarge", 66.3, 390.1),
("m5a.large", 1.7, 5.2),
("m5a.xlarge", 3.4, 10.4),
("m5a.2xlarge", 6.8, 20.8),
("m5a.4xlarge", 13.5, 41.7),
("m5a.8xlarge", 27.1, 83.3),
("m5a.12xlarge", 40.6, 125.0),
("m5a.16xlarge", 54.2, 166.7),
("m5a.24xlarge", 81.3, 250.0),
("c5.large", 1.4, 8.1),
("c5.xlarge", 2.8, 16.3),
("c5.2xlarge", 5.5, 32.5),
("c5.4xlarge", 11.0, 65.0),
("c5.9xlarge", 24.9, 146.3),
("c5.12xlarge", 33.1, 195.0),
("c5.18xlarge", 49.7, 292.5),
("c5.24xlarge", 66.3, 390.1),
("c5a.large", 0.9, 3.4),
("c5a.xlarge", 1.9, 6.8),
("c5a.2xlarge", 3.8, 13.5),
("c5a.4xlarge", 7.6, 27.1),
("c5a.8xlarge", 15.2, 54.2),
("c5a.12xlarge", 22.8, 81.3),
("c5a.16xlarge", 30.4, 108.3),
("c5a.24xlarge", 45.5, 162.5),
("r5.large", 1.7, 8.9),
("r5.xlarge", 3.4, 17.9),
("r5.2xlarge", 6.8, 35.7),
("r5.4xlarge", 13.6, 71.4),
("r5.8xlarge", 27.2, 142.8),
("r5.12xlarge", 40.8, 214.2),
("r5.16xlarge", 54.4, 285.6),
("r5.24xlarge", 81.6, 428.4),
("r5a.large", 2.0, 6.0),
("r5a.xlarge", 4.0, 12.0),
("r5a.2xlarge", 8.1, 24.0),
("r5a.4xlarge", 16.1, 48.1),
("r5a.8xlarge", 32.2, 96.1),
("r5a.12xlarge", 48.3, 144.2),
("r5a.16xlarge", 64.4, 192.3),
("r5a.24xlarge", 96.7, 288.4),
("m6i.large", 1.5, 7.5),
("m6i.xlarge", 3.1, 15.0),
("m6i.2xlarge", 6.1, 30.1),
("m6i.4xlarge", 12.3, 60.1),
("m6i.8xlarge", 24.5, 120.3),
("m6i.12xlarge", 36.8, 180.4),
("m6i.16xlarge", 49.1, 240.5),
("m6i.24xlarge", 73.6, 360.8),
("m6i.32xlarge", 98.2, 481.0),
("c6i.large", 1.5, 7.5),
("c6i.xlarge", 3.1, 15.0),
("c6i.2xlarge", 6.1, 30.1),
("c6i.4xlarge", 12.3, 60.1),
("c6i.8xlarge", 24.5, 120.3),
("c6i.12xlarge", 36.8, 180.4),
("c6i.16xlarge", 49.1, 240.5),
("c6i.24xlarge", 73.6, 360.8),
("c6i.32xlarge", 98.2, 481.0),
("r6i.large", 1.9, 8.3),
("r6i.xlarge", 3.7, 16.6),
("r6i.2xlarge", 7.4, 33.3),
("r6i.4xlarge", 14.8, 66.5),
("r6i.8xlarge", 29.7, 133.1),
("r6i.12xlarge", 44.5, 199.6),
("r6i.16xlarge", 59.3, 266.1),
("r6i.24xlarge", 89.0, 399.2),
("m7i.large", 2.1, 8.3),
("m7i.xlarge", 4.1, 16.6),
("m7i.2xlarge", 8.3, 33.3),
("m7i.4xlarge", 16.6, 66.6),
("m7i.8xlarge", 33.2, 133.1),
("m7i.16xlarge", 66.3, 266.3),
("c7i.large", 2.1, 8.3),
("c7i.xlarge", 4.1, 16.6),
("c7i.2xlarge", 8.3, 33.3),
("c7i.4xlarge", 16.6, 66.6),
("c7i.8xlarge", 33.2, 133.1),
("c7i.16xlarge", 66.3, 266.3),
("r7i.large", 2.4, 9.1),
("r7i.xlarge", 4.8, 18.2),
("r7i.2xlarge", 9.6, 36.5),
("r7i.4xlarge", 19.1, 73.0),
("r7i.8xlarge", 38.3, 145.9),
("r7i.16xlarge", 76.5, 291.8),
("m7a.large", 1.5, 4.6),
("m7a.xlarge", 3.0, 9.1),
("m7a.2xlarge", 5.9, 18.3),
("m7a.4xlarge", 11.8, 36.5),
("m7a.8xlarge", 23.7, 73.0),
("m7a.16xlarge", 47.3, 146.1),
("c7a.large", 1.5, 4.6),
("c7a.xlarge", 3.0, 9.1),
("c7a.2xlarge", 5.9, 18.3),
("c7a.4xlarge", 11.8, 36.5),
("c7a.8xlarge", 23.7, 73.0),
("c7a.16xlarge", 47.3, 146.1),
("r7a.large", 1.8, 5.4),
("r7a.xlarge", 3.6, 10.7),
("r7a.2xlarge", 7.2, 21.5),
("r7a.4xlarge", 14.4, 42.9),
("r7a.8xlarge", 28.8, 85.8),
("r7a.16xlarge", 57.5, 171.6),
("m6a.large", 0.9, 4.0),
("m6a.xlarge", 1.8, 8.1),
("m6a.2xlarge", 3.6, 16.2),
("m6a.4xlarge", 7.1, 32.3),
("m6a.8xlarge", 14.2, 64.6),
("m6a.16xlarge", 28.5, 129.2),
("c6a.large", 0.9, 4.0),
("c6a.xlarge", 1.8, 8.1),
("c6a.2xlarge", 3.6, 16.2),
("c6a.4xlarge", 7.1, 32.3),
("c6a.8xlarge", 14.2, 64.6),
("c6a.16xlarge", 28.5, 129.2),
("m7g.large", 0.9, 3.4),
("m7g.xlarge", 1.9, 6.8),
("m7g.2xlarge", 3.8, 13.5),
("m7g.4xlarge", 7.6, 27.1),
("m7g.8xlarge", 15.2, 54.2),
("m7g.16xlarge", 30.4, 108.3),
("c7g.large", 0.9, 3.4),
("c7g.xlarge", 1.9, 6.8),
("c7g.2xlarge", 3.8, 13.5),
("c7g.4xlarge", 7.6, 27.1),
("c7g.8xlarge", 15.2, 54.2),
("c7g.16xlarge", 30.4, 108.3),
("m8g.large", 0.9, 3.4),
("m8g.xlarge", 1.9, 6.8),
("m8g.2xlarge", 3.8, 13.5),
("m8g.4xlarge", 7.6, 27.1),
("m8g.8xlarge", 15.2, 54.2),
("m8g.16xlarge", 30.4, 108.3),
("c8g.large", 0.9, 3.4),
("c8g.xlarge", 1.9, 6.8),
("c8g.2xlarge", 3.8, 13.5),
("c8g.4xlarge", 7.6, 27.1),
("c8g.8xlarge", 15.2, 54.2),
("c8g.16xlarge", 30.4, 108.3),
("m8a.large", 1.5, 4.6),
("m8a.xlarge", 3.0, 9.1),
("m8a.2xlarge", 5.9, 18.3),
("m8a.4xlarge", 11.8, 36.5),
("m8a.8xlarge", 23.7, 73.0),
("m8a.16xlarge", 47.3, 146.1),
("c8a.large", 1.5, 4.6),
("c8a.xlarge", 3.0, 9.1),
("c8a.2xlarge", 5.9, 18.3),
("c8a.4xlarge", 11.8, 36.5),
("c8a.8xlarge", 23.7, 73.0),
("c8a.16xlarge", 47.3, 146.1),
("m8i.large", 1.6, 9.0),
("m8i.xlarge", 3.3, 17.9),
("m8i.2xlarge", 6.5, 35.9),
("m8i.4xlarge", 13.0, 71.7),
("m8i.8xlarge", 26.0, 143.4),
("m8i.16xlarge", 52.1, 286.9),
("c8i.large", 1.6, 9.0),
("c8i.xlarge", 3.3, 17.9),
("c8i.2xlarge", 6.5, 35.9),
("c8i.4xlarge", 13.0, 71.7),
("c8i.8xlarge", 26.0, 143.4),
("c8i.16xlarge", 52.1, 286.9),
("n2-standard-2", 1.4, 7.5),
("n2-standard-4", 2.8, 15.0),
("n2-standard-8", 5.5, 30.0),
("n2-standard-16", 11.0, 60.1),
("n2-standard-32", 22.1, 120.2),
("n2-standard-48", 33.1, 180.2),
("n2-standard-64", 44.2, 240.3),
("n2-standard-80", 55.2, 300.4),
("n2-standard-96", 66.3, 360.5),
("n2-standard-128", 88.4, 480.6),
("n2-highcpu-2", 1.4, 7.5),
("n2-highcpu-4", 2.8, 15.0),
("n2-highcpu-8", 5.5, 30.0),
("n2-highcpu-16", 11.0, 60.1),
("n2-highcpu-32", 22.1, 120.2),
("n2-highcpu-48", 33.1, 180.2),
("n2-highcpu-64", 44.2, 240.3),
("n2-highcpu-80", 55.2, 300.4),
("n2-highcpu-96", 66.3, 360.5),
("n2-highmem-2", 1.7, 8.3),
("n2-highmem-4", 3.4, 16.6),
("n2-highmem-8", 6.8, 33.2),
("n2-highmem-16", 13.6, 66.5),
("n2-highmem-32", 27.2, 133.0),
("n2-highmem-48", 40.8, 199.4),
("n2-highmem-64", 54.4, 265.9),
("n2-highmem-80", 68.0, 332.4),
("n2-highmem-96", 81.6, 398.9),
("n2-highmem-128", 108.8, 531.8),
("e2-standard-2", 0.9, 3.2),
("e2-standard-4", 1.9, 6.3),
("e2-standard-8", 3.8, 12.6),
("e2-standard-16", 7.6, 25.2),
("e2-standard-32", 15.2, 50.4),
("c2-standard-4", 2.8, 15.0),
("c2-standard-8", 5.5, 30.0),
("c2-standard-16", 11.0, 60.1),
("c2-standard-30", 20.7, 112.6),
("c2-standard-60", 41.4, 225.3),
("c3-standard-4", 4.1, 16.2),
("c3-standard-8", 8.3, 32.5),
("c3-standard-22", 22.8, 89.4),
("c3-standard-44", 45.6, 178.7),
("c3-standard-88", 91.2, 357.5),
("c3-standard-176", 182.4, 714.9),
("c3d-standard-4", 3.0, 8.8),
("c3d-standard-8", 5.9, 17.6),
("c3d-standard-16", 11.8, 35.1),
("c3d-standard-30", 22.2, 65.9),
("c3d-standard-60", 44.3, 131.8),
("c3d-standard-180", 133.0, 395.3),
("c4-standard-2", 1.6, 8.8),
("c4-standard-4", 3.3, 17.5),
("c4-standard-8", 6.5, 35.1),
("c4-standard-16", 13.0, 70.1),
("c4-standard-32", 26.0, 140.2),
("c4-standard-96", 78.1, 420.6),
("c4d-standard-2", 0.6, 3.8),
("c4d-standard-4", 1.3, 7.6),
("c4d-standard-8", 2.6, 15.3),
("c4d-standard-16", 5.1, 30.6),
("c4d-standard-32", 10.2, 61.1),
("c4d-standard-96", 30.7, 183.4),
("n2d-standard-2", 1.5, 4.4),
("n2d-standard-4", 3.0, 8.8),
("n2d-standard-8", 5.9, 17.6),
("n2d-standard-16", 11.8, 35.1),
("n2d-standard-32", 23.7, 70.3),
("n2d-standard-64", 47.3, 140.5),
("t2a-standard-1", 0.7, 1.8),
("t2a-standard-2", 1.3, 3.5),
("t2a-standard-4", 2.7, 7.0),
("t2a-standard-8", 5.4, 14.0),
("t2a-standard-16", 10.7, 28.0),
("t2a-standard-32", 21.4, 56.0),
("c4a-standard-1", 0.5, 1.7),
("c4a-standard-2", 0.9, 3.4),
("c4a-standard-4", 1.9, 6.8),
("c4a-standard-8", 3.8, 13.5),
("c4a-standard-16", 7.6, 27.1),
("c4a-standard-32", 15.2, 54.2),
("c4a-standard-48", 22.8, 81.3),
("c4a-standard-72", 34.1, 121.9),
("Standard_D2s_v3", 1.3, 8.4),
("Standard_D4s_v3", 2.6, 16.8),
("Standard_D8s_v3", 5.2, 33.5),
("Standard_D16s_v3", 10.3, 67.1),
("Standard_D32s_v3", 20.6, 134.2),
("Standard_D48s_v3", 31.0, 201.3),
("Standard_D64s_v3", 41.3, 268.4),
("Standard_D2s_v4", 1.3, 7.9),
("Standard_D4s_v4", 2.6, 15.9),
("Standard_D8s_v4", 5.1, 31.7),
("Standard_D16s_v4", 10.2, 63.5),
("Standard_D32s_v4", 20.4, 126.9),
("Standard_D48s_v4", 30.7, 190.4),
("Standard_D64s_v4", 40.9, 253.9),
("Standard_D2s_v5", 1.3, 7.9),
("Standard_D4s_v5", 2.6, 15.9),
("Standard_D8s_v5", 5.1, 31.7),
("Standard_D16s_v5", 10.2, 63.5),
("Standard_D32s_v5", 20.4, 126.9),
("Standard_D48s_v5", 30.7, 190.4),
("Standard_D64s_v5", 40.9, 253.9),
("Standard_D96s_v5", 61.3, 380.8),
("Standard_D2as_v5", 0.9, 4.0),
("Standard_D4as_v5", 1.8, 8.1),
("Standard_D8as_v5", 3.6, 16.2),
("Standard_D16as_v5", 7.1, 32.3),
("Standard_D32as_v5", 14.2, 64.6),
("Standard_D48as_v5", 21.4, 96.9),
("Standard_D64as_v5", 28.5, 129.2),
("Standard_D96as_v5", 42.7, 193.8),
("Standard_E2s_v3", 1.6, 9.2),
("Standard_E4s_v3", 3.2, 18.4),
("Standard_E8s_v3", 6.4, 36.7),
("Standard_E16s_v3", 12.9, 73.5),
("Standard_E32s_v3", 25.8, 147.0),
("Standard_E48s_v3", 38.6, 220.5),
("Standard_E64s_v3", 51.5, 294.0),
("Standard_E2s_v4", 1.6, 8.7),
("Standard_E4s_v4", 3.2, 17.5),
("Standard_E8s_v4", 6.4, 34.9),
("Standard_E16s_v4", 12.8, 69.9),
("Standard_E32s_v4", 25.6, 139.7),
("Standard_E48s_v4", 38.4, 209.6),
("Standard_E64s_v4", 51.1, 279.5),
("Standard_E2s_v5", 1.6, 8.7),
("Standard_E4s_v5", 3.2, 17.5),
("Standard_E8s_v5", 6.4, 34.9),
("Standard_E16s_v5", 12.8, 69.9),
("Standard_E32s_v5", 25.6, 139.7),
("Standard_E48s_v5", 38.4, 209.6),
("Standard_E64s_v5", 51.1, 279.5),
("Standard_E96s_v5", 76.7, 419.2),
("Standard_F2s_v2", 1.3, 7.9),
("Standard_F4s_v2", 2.6, 15.9),
("Standard_F8s_v2", 5.1, 31.7),
("Standard_F16s_v2", 10.2, 63.5),
("Standard_F32s_v2", 20.4, 126.9),
("Standard_F48s_v2", 30.7, 190.4),
("Standard_F64s_v2", 40.9, 253.9),
("Standard_F72s_v2", 46.0, 285.6),
("Standard_D2s_v6", 1.1, 6.4),
("Standard_D4s_v6", 2.2, 12.8),
("Standard_D8s_v6", 4.4, 25.6),
("Standard_D16s_v6", 8.8, 51.2),
("Standard_D32s_v6", 17.6, 102.4),
("Standard_D64s_v6", 35.2, 204.8),
("Standard_D96s_v6", 52.8, 307.2),
("Standard_D2ads_v6", 0.8, 4.1),
("Standard_D4ads_v6", 1.6, 8.2),
("Standard_D8ads_v6", 3.2, 16.4),
("Standard_D16ads_v6", 6.4, 32.8),
("Standard_D32ads_v6", 12.8, 65.6),
("Standard_D64ads_v6", 25.6, 131.2),
("Standard_D96ads_v6", 38.4, 196.8),
("Standard_D2ps_v6", 1.2, 4.4),
("Standard_D4ps_v6", 2.4, 8.8),
("Standard_D8ps_v6", 4.8, 17.6),
("Standard_D16ps_v6", 9.6, 35.2),
("Standard_D32ps_v6", 19.2, 70.4),
("Standard_D64ps_v6", 38.4, 140.8),
("Standard_D96ps_v6", 57.6, 211.2),
("Standard_E2s_v6", 1.4, 7.2),
("Standard_E4s_v6", 2.8, 14.4),
("Standard_E8s_v6", 5.7, 28.8),
("Standard_E16s_v6", 11.4, 57.6),
("Standard_E32s_v6", 22.7, 115.2),
("Standard_E64s_v6", 45.4, 230.4),
("Standard_E96s_v6", 68.2, 345.6),
("xeon-6780e", 100.0, 420.0),
];
let mut m = HashMap::with_capacity(entries.len());
for &(name, idle, max) in entries {
m.insert(name, (idle, max));
}
m
});
static PROVIDER_DEFAULTS: LazyLock<HashMap<&'static str, (f64, f64)>> = LazyLock::new(|| {
let mut m = HashMap::with_capacity(4);
m.insert("aws", (1.4, 8.1)); m.insert("gcp", (1.4, 7.5)); m.insert("azure", (1.1, 6.4)); m.insert("generic", (3.0, 20.0)); m
});
#[must_use]
pub fn lookup_instance_power(instance_type: &str, provider: &str) -> (f64, f64) {
if let Some(&power) = INSTANCE_POWER.get(instance_type) {
return power;
}
if let Some(&power) = PROVIDER_DEFAULTS.get(provider) {
return power;
}
*PROVIDER_DEFAULTS
.get("generic")
.expect("generic default must exist")
}
#[must_use]
pub fn is_known_instance_type(instance_type: &str) -> bool {
INSTANCE_POWER.contains_key(instance_type)
}
#[must_use]
pub fn interpolate_watts(idle_watts: f64, max_watts: f64, cpu_percent: f64) -> f64 {
if !cpu_percent.is_finite() {
return idle_watts;
}
let clamped = cpu_percent.clamp(0.0, 100.0);
idle_watts + (max_watts - idle_watts) * (clamped / 100.0)
}
#[must_use]
pub fn compute_cloud_energy_per_op_kwh(
watts: f64,
scrape_interval_secs: f64,
ops: u64,
) -> Option<f64> {
if ops == 0 || !watts.is_finite() || watts < 0.0 {
return None;
}
let kwh = (watts / 1000.0) * (scrape_interval_secs / 3600.0);
let per_op = kwh / ops as f64;
if per_op.is_finite() {
Some(per_op)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_aws_instance() {
let (idle, max) = lookup_instance_power("m5.large", "aws");
assert!((idle - 1.4).abs() < 0.01);
assert!((max - 8.1).abs() < 0.01);
}
#[test]
fn known_gcp_instance() {
let (idle, max) = lookup_instance_power("n2-standard-8", "gcp");
assert!((idle - 5.5).abs() < 0.01);
assert!((max - 30.0).abs() < 0.01);
}
#[test]
fn known_azure_instance() {
let (idle, max) = lookup_instance_power("Standard_D8s_v3", "azure");
assert!((idle - 5.2).abs() < 0.01);
assert!((max - 33.5).abs() < 0.1);
}
#[test]
fn unknown_instance_falls_back_to_provider_default() {
let (idle, max) = lookup_instance_power("m999.future", "aws");
assert!((idle - 1.4).abs() < 0.01);
assert!((max - 8.1).abs() < 0.01);
}
#[test]
fn modern_architecture_keys_present() {
for key in [
"m7i.large",
"c7a.large",
"r7a.large",
"m6a.xlarge",
"c7g.large",
"m8g.large",
"m8a.large",
"c8a.large",
"m8i.large",
"c8i.large",
"c4-standard-4",
"c4d-standard-8",
"c4a-standard-2",
"t2a-standard-2",
"Standard_D2s_v6",
"Standard_D2ps_v6",
"xeon-6780e",
] {
assert!(is_known_instance_type(key), "missing modern entry: {key}");
}
}
#[test]
fn turin_overrides_to_genoa_proxy() {
let sizes = [
"large", "xlarge", "2xlarge", "4xlarge", "8xlarge", "16xlarge",
];
for size in sizes {
for (turin, genoa) in [("m8a", "m7a"), ("c8a", "c7a")] {
let turin_key = format!("{turin}.{size}");
let genoa_key = format!("{genoa}.{size}");
assert_eq!(
lookup_instance_power(&turin_key, "aws"),
lookup_instance_power(&genoa_key, "aws"),
"{turin_key} (Turin) must alias to {genoa_key} (Genoa) until CCF correction"
);
}
}
}
#[test]
fn m_series_does_not_carry_dram_premium() {
let (idle, max) = lookup_instance_power("m5.large", "aws");
assert!(
(idle - 1.4).abs() < 0.05,
"m5.large idle drifted: {idle} expected ~1.4"
);
assert!(
(max - 8.1).abs() < 0.1,
"m5.large max drifted: {max} expected ~8.1"
);
}
#[test]
fn r_series_includes_dram_premium_over_general_purpose() {
let (m5_idle, m5_max) = lookup_instance_power("m5.large", "aws");
let (r5_idle, r5_max) = lookup_instance_power("r5.large", "aws");
assert!(
(r5_idle - m5_idle - 0.32).abs() < 0.05,
"DRAM idle uplift drift: r5 {r5_idle} - m5 {m5_idle} expected ~0.32"
);
assert!(
(r5_max - m5_max - 0.80).abs() < 0.05,
"DRAM max uplift drift: r5 {r5_max} - m5 {m5_max} expected ~0.80"
);
}
#[test]
fn sierra_forest_entries_are_chip_level_not_vcpu_level() {
let (idle, _) = lookup_instance_power("xeon-6780e", "generic");
assert!(
idle >= 50.0,
"xeon-6780e must be system-level (>=50W idle), got {idle}"
);
}
#[test]
fn unknown_provider_falls_back_to_generic() {
let (idle, max) = lookup_instance_power("custom.instance", "onprem");
assert!((idle - 3.0).abs() < 0.01);
assert!((max - 20.0).abs() < 0.01);
}
#[test]
fn is_known_true_for_table_entry() {
assert!(is_known_instance_type("c5.4xlarge"));
}
#[test]
fn is_known_false_for_missing_entry() {
assert!(!is_known_instance_type("m99.jumbo"));
}
#[test]
fn interpolate_at_zero_percent() {
let w = interpolate_watts(2.0, 20.0, 0.0);
assert!((w - 2.0).abs() < 1e-10);
}
#[test]
fn interpolate_at_fifty_percent() {
let w = interpolate_watts(2.0, 20.0, 50.0);
assert!((w - 11.0).abs() < 1e-10);
}
#[test]
fn interpolate_at_hundred_percent() {
let w = interpolate_watts(2.0, 20.0, 100.0);
assert!((w - 20.0).abs() < 1e-10);
}
#[test]
fn interpolate_clamps_below_zero() {
let w = interpolate_watts(2.0, 20.0, -10.0);
assert!((w - 2.0).abs() < 1e-10, "should clamp to idle");
}
#[test]
fn interpolate_clamps_above_hundred() {
let w = interpolate_watts(2.0, 20.0, 150.0);
assert!((w - 20.0).abs() < 1e-10, "should clamp to max");
}
#[test]
fn interpolate_nan_returns_idle() {
let w = interpolate_watts(2.0, 20.0, f64::NAN);
assert!((w - 2.0).abs() < 1e-10, "NaN input should return idle");
}
#[test]
fn interpolate_infinity_returns_idle() {
let w = interpolate_watts(2.0, 20.0, f64::INFINITY);
assert!((w - 2.0).abs() < 1e-10, "Inf input should return idle");
}
#[test]
fn basic_energy_computation() {
let result = compute_cloud_energy_per_op_kwh(10.0, 15.0, 100);
assert!(result.is_some());
let per_op = result.unwrap();
let expected = (10.0 / 1000.0) * (15.0 / 3600.0) / 100.0;
assert!((per_op - expected).abs() < 1e-15);
}
#[test]
fn zero_ops_returns_none() {
assert!(compute_cloud_energy_per_op_kwh(10.0, 15.0, 0).is_none());
}
#[test]
fn negative_watts_returns_none() {
assert!(compute_cloud_energy_per_op_kwh(-1.0, 15.0, 100).is_none());
}
#[test]
fn nan_watts_returns_none() {
assert!(compute_cloud_energy_per_op_kwh(f64::NAN, 15.0, 100).is_none());
}
#[test]
fn infinite_watts_returns_none() {
assert!(compute_cloud_energy_per_op_kwh(f64::INFINITY, 15.0, 100).is_none());
}
#[test]
fn all_entries_have_positive_values() {
for (name, &(idle, max)) in INSTANCE_POWER.iter() {
assert!(idle > 0.0, "{name}: idle must be positive, got {idle}");
assert!(max > 0.0, "{name}: max must be positive, got {max}");
assert!(max >= idle, "{name}: max ({max}) must be >= idle ({idle})");
}
}
#[test]
fn table_has_expected_entry_count() {
assert!(
INSTANCE_POWER.len() >= 300,
"expected >= 300 entries, got {}",
INSTANCE_POWER.len()
);
}
}