Skip to content

Adds an SCI (Software Carbon Intensity, ISO/IEC 21031:2024) output method - #1361

Merged
benoit-cty merged 5 commits into
masterfrom
feat/sci-report-output
Nov 1, 2026
Merged

benoit-cty merged 5 commits into
masterfrom
feat/sci-report-output

Conversation

@davidberenstein1957

@davidberenstein1957 davidberenstein1957 commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Adds an SCI (Software Carbon Intensity, ISO/IEC 21031:2024) output method, following the same pattern as the existing BoAmps exporter. codecarbon/output_methods/sci.py holds FunctionalUnit/EmbodiedDeclaration dataclasses for the user-supplied R and M terms, a pure map_emissions_to_sci(data, ...) mapping function (I is derived as emissions * 1000 / energy_consumed rather than recomputed, so the report agrees with the CSV by construction), and SCIOutput(BaseOutput), which writes sci_report_<run_id>.json and sci_report_tasks_<run_id>.json. OutputMethod.SCI is registered in _init_output_methods, reading an optional sci_context_file from the usual config hierarchy. When R is undeclared the report still writes with sci: null and a status field explaining why, instead of dividing by zero. When M is undeclared, M_gCO2e: 0 with M_source: "not declared". A follow-up commit apportions the declared M across per-task reports by each task's share of summed duration, instead of charging every task the device's full embodied carbon.

Related Issue

Fixes #1353

Motivation and Context

CodeCarbon had no SCI output method. Users who need an ISO/IEC 21031-aligned report had no supported path, and any naive implementation risks inventing numbers (e.g. assuming R = 1 or guessing embodied carbon) that a compliance report cannot support.

How Has This Been Tested?

uv run pytest tests/test_sci_output.py -q — 32 tests covering the formula, provenance assembly, the undeclared-R and undeclared-M branches, zero-energy, the handler and its task path, and context-file loading (valid, missing, malformed). Includes test_task_out_apportions_embodied_by_duration (unequal durations: asserts a 10/30 split of a declared 40 gCO2e and that shares sum back to 40). black --check and ruff check were run scoped to the touched files.

Screenshots (if appropriate):

N/A

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

AI Usage Disclosure

  • 🟥 AI-vibecoded: You cannot explain the logic. Car analogy : the car drive by itself, you are outside it and just tell it where to go.
  • 🟠 AI-generated: Car analogy : the car drive by itself, you are inside and give instructions.
  • ⭐ AI-assisted. Car analogy : you drive the car, AI help you find your way.
  • ♻️ No AI used. Car analogy : you drive the car.

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the docs/how-to/contributing.md document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

User-facing surface

from codecarbon import EmissionsTracker
from codecarbon.output_methods.sci import FunctionalUnit, SCIOutput

sci = SCIOutput(functional_unit=FunctionalUnit(name="inference request", count=10_000))
tracker = EmissionsTracker(output_handlers=[sci])

sci.set_functional_unit_count(n) for the common case where the count is only known at the end; SCIOutput.from_file("sci_context.json") for the declarative path; output_methods = csv,sci plus sci_context_file in .codecarbon.config. Docs: a new SCI section in docs/reference/output.md, a row in docs/how-to/examples.md, and a runnable examples/sci_output.py. No new dependencies.

Deliberately left out

Any automatic embodied-carbon estimation (no EmbodiedProfile tracker parameter, no bundled hardware table, no cloud per-instance figures) — CodeCarbon has no defensible manufacturing data, and an uncited M in a compliance report is worse than a blank one. Also out: any change to what emissions means, multiple simultaneous functional units, and schema validation on write (there is no stable machine-readable SCI schema yet).

@codecov

codecov Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.80%. Comparing base (53e67c3) to head (ee8d215).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1361      +/-   ##
==========================================
+ Coverage   93.68%   93.80%   +0.11%     
==========================================
  Files          50       51       +1     
  Lines        5604     5710     +106     
==========================================
+ Hits         5250     5356     +106     
  Misses        354      354              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@davidberenstein1957
davidberenstein1957 marked this pull request as ready for review August 12, 2026 19:14
@davidberenstein1957
davidberenstein1957 requested a review from a team as a code owner August 12, 2026 19:14
@davidberenstein1957
davidberenstein1957 force-pushed the feat/sci-report-output branch 2 times, most recently from a21d8e3 to 9474fda Compare August 20, 2026 06:14
@benoit-cty

Copy link
Copy Markdown
Contributor

🤖 This review comment was written and posted by Claude Opus 5.5 (AI assistant), at the request of @benoit-cty. Findings were checked by reading the code and running tests locally (merged with current master where relevant), but please double-check before acting on them.

Verdict: ✅ Approve after 1 doc fix and 1 robustness fix

SCIOutput(BaseOutput) is clean, self-contained and follows the BoAmps pattern. The formula (E*I + M)/R is implemented correctly. It refuses to invent R (writes sci: null with a status) and flags an undeclared M. 29 tests pass, and it merges cleanly with master.

Must fix:

  1. The sample report in docs/reference/output.md shows the wrong SCI value. It shows "sci": 0.0000127 with E=0.1007, I=417.08, M=42.5, R=10000. The formula gives (0.1007×417.08 + 42.5)/10000 = 0.00845. This is the canonical example people will copy, so it needs to be right.
  2. A context file whose top level isn't a JSON object stops all tracker output.
    • A sci_context_file holding valid JSON that isn't an object (e.g. [1]) raises AttributeError at sci.py:183.
    • The except (OSError, TypeError, ValueError) around emissions_tracker.py:~678 doesn't catch it. __init__ aborts under @suppress and leaves _output_handlers == [], so no CSV or anything else is written.
    • Fix: validate isinstance(context, dict) in from_file and raise ValueError, or add AttributeError to the except. Please add a test.

Should fix:
3. M is split by the tasks' summed duration, not the run's (sci.py:259-262).

  • share = task.duration / sum(task durations), but the docs and docstring say "share of the run's duration".
  • Example: a 100 s run with one 10 s task assigns that task 100% of M.
  • Either divide by the run duration, or change the docs and drop the "per-task figures sum back to the run-level report" claim, which only holds when tasks cover the whole run.

Nits:

  • Task reports always show "pue": 1, because TaskEmissionsData has no pue field and sci.py:103 falls back to 1. That is misleading provenance when PUE ≠ 1. I itself is still correct because it's derived.
  • set_functional_unit_count() mutates the caller's FunctionalUnit instance. Copy it, or document that.
  • A relative sci_context_file resolves against the CWD, not the config file's location. Worth a line in the docs.
  • flush() writes an interim report that applies the full declared M to only part of E. It is overwritten at stop(), but anyone reading the flushed file sees an inflated M share. Consider pro-rating M by elapsed time in interim reports.

@davidberenstein1957

Copy link
Copy Markdown
Collaborator Author

Made the changes in 89d2897: non-object context file now raises ValueError (tracker degrades, test added), sample SCI fixed to 0.00845, M-split docs now say tasks' summed duration, task pue is null, FunctionalUnit is copied, relative path documented.
Skipped pro-rating M in flush() reports: they're overwritten at stop(), and pro-rating needs a design call on elapsed vs. declared run length.

davidberenstein1957 and others added 3 commits October 31, 2026 13:55
Emit an ISO/IEC 21031 Software Carbon Intensity report alongside the existing
output methods. E and I come from the measured run; R and M are user
declarations and are reported as undeclared rather than guessed, so a report
never invents numbers the run cannot support.

Embodied carbon (M) is apportioned across tasks by each task's share of the
run duration. Handing every task the same declared M made a 5-task run report
the device's full embodied carbon 5 times. The split is exhaustive: per-task
figures sum back to the run-level report, and `M_source` records the share
applied. Per-task SCI is reported as `sciShare`, not as a per-unit rate.

Failure handling is narrow rather than broad: an unreadable
`sci_context_file` degrades instead of failing the run, while unrelated
exceptions while writing a report are no longer swallowed. Context dataclasses
are built straight from the JSON keys, with the documented camelCase `gCO2e`
key normalised so existing context files keep working.

Docs cover the formula, the term declarations, the config keys and a sample
report, and note that `output_methods = csv,sci` alone can never produce a
non-null sci unless the context file hardcodes `functionalUnit.count`. The
constructor, `from_file` and context-file walkthrough live in
`examples/sci_output.py`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… M split

- from_file raises ValueError when the context file is not a JSON object,
  so the tracker degrades instead of losing every output handler
- docs sample sci value corrected to 0.00845
- M is split by the tasks' summed duration; docs and docstring now say so
- task reports show pue null instead of a misleading 1
- SCIOutput copies the FunctionalUnit it is given
- document that a relative sci_context_file resolves against the CWD

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@benoit-cty
benoit-cty force-pushed the feat/sci-report-output branch from 89d2897 to 965eb91 Compare October 31, 2026 14:03

Copy link
Copy Markdown
Contributor

Rebased on master and reviewed. New head is 965eb91.

  • Rebase: git reported no conflict, but it placed the OutputMethod.SCI registration after return BoAmpsOutput(...) in master's new _build_boamps_output() helper. That code could never run, so output_methods=sci did nothing and 4 TestTrackerRegistration tests failed. I moved the block back into _init_output_methods, inside the feature commit 74dfe7d, so every commit in the history works.
  • Earlier review: every must-fix and should-fix item is handled in 6a229c3. That covers the non-object context file, the sample SCI value (0.00845), the docs for how M is split across tasks, pue: null in task reports, the FunctionalUnit copy, and the docs line on relative paths.
  • Docs: 965eb91 notes that flush() reports are interim. They apply the full declared M to the energy measured so far and are overwritten at stop().
  • Coverage: codecarbon/output_methods/sci.py is at 100%, and every changed line in emissions_tracker.py is covered.
  • Tests: pre-commit passes. tests/test_sci_output.py passes 32/32. The full package suite has 860 passed and 4 failed. The 4 failures are RAPL permission tests, and they fail the same way on master because the sandbox runs as root.

Maintainer decisions still open:

  1. Pro-rating M in flush() reports. The handler gets the same out() call from flush() and stop(), so it can't tell an interim report from the final one. Pro-rating would need either a new flag passed from the tracker to the handler, or an M declared over time (the spec's TE × TiR/EL form: total embodied carbon times the run's share of the hardware's lifespan). For now only the docs mention it.
  2. Where the config lives. Master now has a generic metadata bag with per-output keys, and BoAmps reads from it. SCI still uses its own sci_context_file key. Should SCI read a metadata["sci"] section instead, to match?

Not run: uv run task docs.


Generated by Claude Code

@benoit-cty benoit-cty changed the title feat: add SCI report output method Adds an SCI (Software Carbon Intensity, ISO/IEC 21031:2024) output method Nov 1, 2026

@benoit-cty benoit-cty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great !

@benoit-cty
benoit-cty merged commit 2a92628 into master Nov 1, 2026
16 checks passed
@benoit-cty
benoit-cty deleted the feat/sci-report-output branch November 1, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature proposal: SCI (ISO/IEC 21031) report output method

3 participants