open-standards/eoc/eoc-rs/crates/eoc-carbon/src/sci_bridge.rs
Transaction Science 6c37e29980 eoc-carbon: an account and an SCI score are measured only where every part was, and a run with no correct answer has no score
0db9b8be listed three things here as not done. `CarbonAccount` dropped
provenance, and `account_request` is a public way in. A report with zero
correct answers carries `joules_per_correct == u64::MAX` as a sentinel, which
`sci_from_eval_report` multiplied by `correct.max(1)` into a u64::MAX µJ
energy term (18.4 TJ). And the test that pins the bridge's provenance sat
behind the `eval-bridge` feature, which nothing in the workspace enables, so
`cargo test --workspace` never compiled it, nor any mutation it catches.

What holds now:

- `CarbonAccount` gains `it_energy_source`, the input `JouleCost`'s provenance
  unchanged, and `facility_energy_source`. The facility joules are `it_joules`
  × PUE, so they are measured only when every part of them was: the IT joules
  were measured, and the PUE added no overhead. A PUE above 1 is a published
  or assumed facility ratio applied to the reading, energy no counter on the
  host read; a figure containing any of it is an estimate. A PUE of 1 adds
  nothing, so measured IT joules stay measured, as an estimated zero adds
  nothing to a live span in eoc-cascade. Grid intensity has its own provenance
  (the reading's provider and kind) and does not enter the tag. Both fields
  read back as `Estimated` when absent. Nothing outside this crate builds a
  `CarbonAccount`.
- `SciScore::energy_source` is now the account's facility provenance, because
  the score's energy term is facility energy. Before, it was the input's
  provenance, so measured IT joules at the default PUE of 1.58 gave a
  "measured" term of which 37% no counter read. This changes what 0db9b8be
  set: an SCI score at PUE > 1 is now estimated even from measured joules.
  The SCI numbers do not change.
- The bridge's pure part is compiled in every build. `EvalEnergy`
  (micro-joules per correct answer, correct answers, provenance) and
  `sci_per_correct_answer` hold all the scoring, including the `JouleCost`
  construction 0db9b8be's mutation targeted. `sci_from_eval_report`, still
  behind `eval-bridge`, only copies the report's three fields across.
- A run with no correct answer has no score per correct answer:
  `sci_per_correct_answer` and `sci_from_eval_report` return `None` for
  `correct == 0` and for the `u64::MAX` sentinel, whatever `correct` says
  beside it. The sentinel is never read as energy. `sci_from_eval_report` now
  returns `Option<SciScore>`; its only callers are this crate's tests.

Tests in the default build: an account's (IT, facility) tags are (Measured,
Measured) at PUE 1, (Measured, Estimated) at 1.2, (Estimated, Estimated) from
an estimate at either, and (Measured, Measured) at a PUE of 0.5, which is
clamped to 1; the figures are the same whichever tag. An account round-trips
and an old one reads back estimated. A score is measured at PUE 1, estimated
at 1.2 from measured joules, with the same SCI. An eval run's score keeps the
run's provenance (2 × 1 J × 1.2 = 2.4 J over R = 2), and a run with 0
correct, or the sentinel with 3 correct, has none. `account_per_request` now
asserts its tags. Under `eval-bridge`, a report's score equals its run's score
for both provenances and both PUEs, and a 0-correct report has none.

Falsified, each mutant restored byte-for-byte (sha256 checked); every one but
the last runs in the default build:

- 0db9b8be's mutant, `JouleCost::measured(total)` in the bridge, now fails
  `an_eval_runs_score_keeps_the_runs_provenance` in a plain `cargo test`;
- an account dropping the IT provenance fails 1 test;
- the facility tag ignoring the PUE fails 3, ignoring the IT provenance 3;
- the score taking the input's provenance (0db9b8be's rule) fails 2;
- removing the no-correct-answer guard fails 1, and so does checking
  `correct == 0` without the sentinel;
- the `eval-bridge` wrapper tagging every report measured fails
  `an_eval_reports_score_is_its_runs_score` under `--features eval-bridge`.
That wrapper's three-field copy is the only part a default build cannot see.

`cargo test -p eoc-carbon` passes (42 + 8), and with `--features eval-bridge`
(43 + 8). No clippy warning in this crate, before or after.

Not done: `joules_per_correct` is an integer quotient, so the total
reconstructed from it can fall short of the run's by less than one µJ per
correct answer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:07:25 -04:00

313 lines
12 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Bridge to the Green Software Foundation **Software Carbon Intensity
//! (SCI)** specification, v1.
//!
//! SCI is defined as:
//!
//! ```text
//! SCI = ((E * I) + M) / R
//! ```
//!
//! where:
//! - `E` is energy consumed by the software (kWh),
//! - `I` is the location-based marginal grid intensity (gCO2e/kWh),
//! - `M` is embodied emissions amortized over the request (gCO2e),
//! - `R` is the functional unit (e.g. one request, one inference, one
//! correct answer for an eval).
//!
//! `eoc-eval` already references SCI; this module is the explicit
//! computation surface. [`sci_per_correct_answer`] scores an eval run's
//! energy with `R = number of correct cases`, and when the `eval-bridge`
//! feature is on, `sci_from_eval_report` feeds it an
//! [`eoc_eval::EvalReport`]. A run with no correct answer has no score per
//! correct answer: both return `None` for it.
use serde::{Deserialize, Serialize};
use crate::account::{CarbonAccount, account_request};
use crate::intensity::CarbonIntensity;
use crate::pue::Pue;
use eoc_core::{JouleCost, JouleSource};
/// Inputs to one SCI computation.
#[derive(Debug, Clone)]
pub struct SciInputs<'a> {
/// Joules at the IT layer, with their provenance: measured by a live
/// counter (see [`eoc_meter`]) or estimated. The score carries it.
pub joule_cost: &'a JouleCost,
/// Grid intensity (marginal preferred, average acceptable).
pub intensity: &'a CarbonIntensity,
/// Facility PUE.
pub pue: &'a Pue,
/// Embodied emissions amortized to this request, in gCO2e. Set to
/// 0.0 if you don't track it yet.
pub embodied_g_co2e: f64,
/// Functional units served by this request (number of inferences,
/// number of correct answers, etc.). Must be > 0; values ≤ 0 are
/// clamped to 1 to keep the score finite.
pub functional_units: f64,
}
/// A computed SCI score.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SciScore {
/// Energy term (kWh).
pub energy_kwh: f64,
/// Operational gCO2e (energy × intensity × PUE).
pub operational_g_co2e: f64,
/// Embodied gCO2e passed in.
pub embodied_g_co2e: f64,
/// `R` denominator actually used.
pub functional_units: f64,
/// The SCI value (gCO2e per functional unit).
pub sci: f64,
/// Where the energy term came from. The term is facility energy (IT
/// joules × PUE), so this is the account's
/// [`facility_energy_source`](CarbonAccount::facility_energy_source):
/// `Measured` only when the input joules were measured and the PUE
/// added no overhead, which no counter read. An SCI value is never more
/// measured than the joules it was computed from. A score serialized
/// without this field reads back as `Estimated`.
#[serde(default = "unrecorded_energy_source")]
pub energy_source: JouleSource,
}
/// The provenance of a score that recorded none: an estimate, never a
/// measurement.
fn unrecorded_energy_source() -> JouleSource {
JouleSource::Estimated
}
impl SciScore {
/// Compute an SCI score from joules + intensity + PUE + embodied.
pub fn compute(inputs: SciInputs<'_>) -> Self {
let account: CarbonAccount = account_request(inputs.joule_cost, inputs.intensity, inputs.pue);
let energy_kwh = account.facility_joules / 3_600_000.0;
let operational_g_co2e = account.g_co2e;
let r = if inputs.functional_units > 0.0 {
inputs.functional_units
} else {
1.0
};
let sci = (operational_g_co2e + inputs.embodied_g_co2e) / r;
Self {
energy_kwh,
operational_g_co2e,
embodied_g_co2e: inputs.embodied_g_co2e,
functional_units: r,
sci,
energy_source: account.facility_energy_source,
}
}
}
/// An eval run's energy, as `eoc_eval::EvalReport` carries it: micro-joules
/// per correct answer, the number of correct answers, and the provenance of
/// the joules. Kept free of `eoc-eval` so the scoring below is compiled, and
/// tested, in every build; the `eval-bridge` feature only fills it from a
/// report.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EvalEnergy {
/// Micro-joules per correct answer. `u64::MAX` is the report's sentinel
/// for "no correct answer", not a figure.
pub joules_per_correct: u64,
/// Correct answers in the run.
pub correct: usize,
/// Where the joules came from: `Measured` only when every case's were.
pub joule_source: JouleSource,
}
/// The SCI score per correct answer of an eval run: total IT energy
/// `joules_per_correct × correct`, with the run's own provenance (never
/// upgraded; see [`SciScore::energy_source`]), and `R = correct`.
///
/// `None` when the run has no correct answer — `correct == 0`, or the
/// `u64::MAX` sentinel a report carries for that — since there is no figure
/// per correct answer to score. The sentinel is never read as energy.
pub fn sci_per_correct_answer(
energy: EvalEnergy,
intensity: &CarbonIntensity,
pue: &Pue,
embodied_g_co2e: f64,
) -> Option<SciScore> {
if energy.correct == 0 || energy.joules_per_correct == u64::MAX {
return None;
}
let total_microjoules = (energy.joules_per_correct as u128)
.saturating_mul(energy.correct as u128)
.min(u64::MAX as u128) as u64;
let cost = JouleCost {
microjoules: total_microjoules,
source: energy.joule_source,
};
Some(SciScore::compute(SciInputs {
joule_cost: &cost,
intensity,
pue,
embodied_g_co2e,
functional_units: energy.correct as f64,
}))
}
/// `eoc-eval` bridge — turns an `EvalReport` into an SCI score with
/// `R = correct`. Compiled only with the `eval-bridge` Cargo feature; it
/// only hands the report's energy to [`sci_per_correct_answer`], which is
/// compiled and tested in every build.
///
/// `EvalReport` carries `joules_per_correct` in micro-joules and a
/// `correct` count, so the total IT-layer energy is reconstructed as
/// `joules_per_correct * correct`. Its provenance is the report's own
/// `joule_source`: measured only when every case of the run was. `None`
/// for a run with no correct answer.
#[cfg(feature = "eval-bridge")]
pub fn sci_from_eval_report(
report: &eoc_eval::EvalReport,
intensity: &CarbonIntensity,
pue: &Pue,
embodied_g_co2e: f64,
) -> Option<SciScore> {
sci_per_correct_answer(
EvalEnergy {
joules_per_correct: report.joules_per_correct,
correct: report.correct,
joule_source: report.joule_source,
},
intensity,
pue,
embodied_g_co2e,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::intensity::{IntensityKind, ProviderKind};
use chrono::Utc;
#[test]
fn r_clamped_when_zero() {
let cost = JouleCost::measured(1_000_000); // 1 J
let ci = CarbonIntensity::new(
"US",
400.0,
IntensityKind::Average,
ProviderKind::Mock,
Utc::now(),
);
let s = SciScore::compute(SciInputs {
joule_cost: &cost,
intensity: &ci,
pue: &Pue::new(1.2),
embodied_g_co2e: 0.0,
functional_units: 0.0,
});
assert_eq!(s.functional_units, 1.0);
assert!(s.sci > 0.0);
}
fn intensity() -> CarbonIntensity {
CarbonIntensity::new("US", 400.0, IntensityKind::Average, ProviderKind::Mock, Utc::now())
}
fn score(cost: JouleCost, pue: f64) -> SciScore {
SciScore::compute(SciInputs {
joule_cost: &cost,
intensity: &intensity(),
pue: &Pue::new(pue),
embodied_g_co2e: 0.0,
functional_units: 1.0,
})
}
/// The score says where its energy term came from, and the same figure
/// yields the same numbers either way. The term is facility energy, so
/// it is measured only when the joules were and the PUE added nothing.
#[test]
fn the_score_carries_its_energy_provenance() {
assert_eq!(score(JouleCost::measured(1_000_000), 1.0).energy_source, JouleSource::Measured);
assert_eq!(score(JouleCost::estimated(1_000_000), 1.0).energy_source, JouleSource::Estimated);
assert_eq!(
score(JouleCost::measured(1_000_000), 1.2).energy_source,
JouleSource::Estimated,
"a PUE's overhead is not a reading"
);
assert_eq!(score(JouleCost::measured(1_000_000), 1.2).sci, score(JouleCost::estimated(1_000_000), 1.2).sci);
}
/// A score written before `energy_source` existed recorded no
/// provenance, so it reads back as an estimate.
#[test]
fn a_score_without_provenance_reads_back_estimated() {
let json = r#"{"energy_kwh":1.0,"operational_g_co2e":1.0,"embodied_g_co2e":0.0,
"functional_units":1.0,"sci":1.0}"#;
let back: SciScore = serde_json::from_str(json).unwrap();
assert_eq!(back.energy_source, JouleSource::Estimated);
}
fn run(joules_per_correct: u64, correct: usize, joule_source: JouleSource) -> EvalEnergy {
EvalEnergy { joules_per_correct, correct, joule_source }
}
/// An eval run's score is measured exactly when the run's joules were
/// (and the PUE adds nothing); the run's provenance is never upgraded.
/// Compiled in every build: the `eval-bridge` feature only fills the
/// run's energy from a report.
#[test]
fn an_eval_runs_score_keeps_the_runs_provenance() {
let ci = intensity();
let per_correct = |source, pue: f64| {
sci_per_correct_answer(run(1_000_000, 2, source), &ci, &Pue::new(pue), 0.0).expect("two correct answers")
};
assert_eq!(per_correct(JouleSource::Estimated, 1.0).energy_source, JouleSource::Estimated);
assert_eq!(per_correct(JouleSource::Measured, 1.0).energy_source, JouleSource::Measured);
assert_eq!(per_correct(JouleSource::Measured, 1.2).energy_source, JouleSource::Estimated);
// 2 correct × 1 J, × 1.2 PUE = 2.4 J either way, over R = 2.
let (e, m) = (per_correct(JouleSource::Estimated, 1.2), per_correct(JouleSource::Measured, 1.2));
assert!((e.energy_kwh - 2.4 / 3_600_000.0).abs() < 1e-15, "{}", e.energy_kwh);
assert_eq!(e.functional_units, 2.0);
assert_eq!(e.sci, m.sci);
}
/// A run with no correct answer has no score per correct answer. Its
/// `u64::MAX` sentinel is never read as a u64::MAX µJ energy term,
/// whatever `correct` says beside it.
#[test]
fn a_run_with_no_correct_answer_has_no_score() {
let ci = intensity();
let pue = Pue::new(1.2);
for source in [JouleSource::Measured, JouleSource::Estimated] {
assert_eq!(sci_per_correct_answer(run(u64::MAX, 0, source), &ci, &pue, 0.0), None);
assert_eq!(sci_per_correct_answer(run(5_000_000, 0, source), &ci, &pue, 0.0), None);
assert_eq!(sci_per_correct_answer(run(u64::MAX, 3, source), &ci, &pue, 0.0), None);
}
assert!(sci_per_correct_answer(run(1, 1, JouleSource::Measured), &ci, &pue, 0.0).is_some());
}
/// The bridge scores a report exactly as the run's energy is scored, and
/// gives none for a report with no correct answer.
#[cfg(feature = "eval-bridge")]
#[test]
fn an_eval_reports_score_is_its_runs_score() {
let report = |correct, joules_per_correct, joule_source| eoc_eval::EvalReport {
harness: "fixed".into(),
model: "test".into(),
total_cases: 4,
correct,
score: correct as f64 / 4.0,
metric: eoc_eval::Metric::Accuracy,
joules_per_correct,
joule_source,
latency_p50_ms: 1,
latency_p95_ms: 1,
};
let ci = intensity();
for pue in [Pue::new(1.0), Pue::new(1.2)] {
for source in [JouleSource::Estimated, JouleSource::Measured] {
let got = sci_from_eval_report(&report(2, 1_000_000, source), &ci, &pue, 0.1);
let want = sci_per_correct_answer(run(1_000_000, 2, source), &ci, &pue, 0.1);
assert_eq!(got, want);
assert!(got.is_some());
}
}
assert_eq!(sci_from_eval_report(&report(0, u64::MAX, JouleSource::Measured), &ci, &Pue::new(1.2), 0.0), None);
}
}