Skip to content

feat: add SCI report output method - #1361

Open
davidberenstein1957 wants to merge 2 commits into
masterfrom
feat/sci-report-output
Open

davidberenstein1957 wants to merge 2 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 — 14 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). Plus 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 91.85%. Comparing base (3ec31a0) to head (89d2897).
⚠️ Report is 41 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1361      +/-   ##
==========================================
+ Coverage   91.43%   91.85%   +0.41%     
==========================================
  Files          49       50       +1     
  Lines        5057     5253     +196     
==========================================
+ Hits         4624     4825     +201     
+ Misses        433      428       -5     

☔ 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
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>
@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.

… 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>
@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.

This branch has not been deployed

No deployments
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

2 participants