dpp-calc
Pure, stateless EU-methodology compliance calculators for Odal Node.
Every function in this crate is deterministic and side-effect free — no I/O, no infrastructure, no async.
The math is open-source (Apache-2.0). Licensed lifecycle-inventory (LCI) data is injected at runtime
through the FactorProvider trait and never bundled here — open methodology, licensed data supplied
by the operator. That split is the licensing rationale for this crate.
When to use this crate
- You are a sector plugin (
sector-battery,sector-electronics, …) and need to call an EU calculator to fillPluginResult.repairability_indexorco2e_score. - You are writing an integration test and need golden-vector inputs and expected outputs.
- You are adding a new EU methodology or product category (see Adding a sector calculator below).
When NOT to use this crate
- You need the DPP data model or port traits →
dpp-domain. - You need the Wasm plugin ABI →
dpp-plugin-traits/dpp-plugin-sdk. - You need field-level validation rules (REACH, fibre percentages, voltage ranges) →
dpp-rules.
Module structure
src/
├── lib.rs public API + sector-calculator scaling guide (read this first)
│
├── kernel/ machinery shared by every methodology
│ ├── error.rs CalcError (InvalidInput | RulesetExpired | FactorNotFound | …)
│ ├── receipt.rs CalculationReceipt — proof-of-calculation envelope
│ ├── ruleset.rs RulesetId, RulesetVersion, Effectivity, RegulatoryBasis
│ │ Ruleset trait — every methodology trait extends this
│ ├── clock.rs AssessmentClock — the governing-law date; there is no now()
│ ├── assessability.rs Assessability<T>: Assessed | NotYetInForce | Undetermined
│ │ | Expired | OutOfScope — none of which is non-compliance
│ ├── factor.rs FactorProvider trait
│ ├── synthetic_factor.rs SyntheticFactorProvider (test/CI only)
│ └── hashing.rs canonical input hashing behind input_hash
│
├── ruleset_registry/ date-based ruleset resolution; all_rulesets() CI iterator
│
├── repairability_index/ The enacted EU 2023/1669 Annex IV index
│ ├── calculator.rs calculate(inputs, ruleset) → RepairabilityIndexResult
│ ├── parameters.rs RepairabilityIndexInputs — 3 part-level parameters over the
│ │ 10 Annex IV priority parts + 3 product-level, each scored 1–5
│ ├── thresholds.rs RepairabilityIndexRuleset + Eu2023_1669Ruleset
│ └── golden_vectors.rs #[cfg(test)]
│
├── repairability/ Simplified, non-regulatory six-parameter A–E heuristic
│ ├── calculator.rs calculate(inputs, ruleset, clock) → RepairabilityResult
│ ├── parameters.rs RepairabilityInputs (6 × u8, ordinal 0–2)
│ ├── thresholds/ RepairabilityRuleset trait + SimplifiedRepairabilityHeuristic,
│ │ LaptopRuleset, DisplaysRuleset, WashingMachineRuleset
│ └── golden_vectors.rs #[cfg(test)] — A–E coverage, regulatory-basis non-empty CI check
│
└── co2e/
├── calculator.rs calculate(inputs, ruleset, clock) → Co2eResult (cradle-to-gate,
│ operator-supplied emission factors)
├── parameters.rs Co2eInputs, MaterialFootprint
├── thresholds.rs Co2eRuleset trait + CradleToGateRuleset
├── cfb.rs CfbRuleset: Ruleset stub + calculate_cfb() STUB (Phase 2)
├── gwp_factors.rs Embedded GWP100 characterisation factors (EF 3.1 / AR6 — free)
└── golden_vectors.rs #[cfg(test)]
Phase status:
| Module | Status |
|---|---|
repairability_index |
✅ Enacted — Reg. (EU) 2023/1669 Annex IV point 5, smartphones and slate tablets |
repairability |
✅ Available — non-regulatory six-parameter heuristic; not the enacted index |
co2e::calculate |
✅ Baseline — operator-supplied emission factors |
co2e::cfb |
🔒 Stub — gated on signed ecoinvent/EF sublicense (Phase 1 gate) |
pef/ (future) |
📋 Not yet — awaits per-sector PEFCR finalisation (2026–2030) |
Which repairability module do I want?
repairability_index implements the scoring a delegated act actually prescribes,
and is what belongs in a passport field that claims to be the repairability
index. repairability is an internal six-factor heuristic that predates it and
covers product categories the regulation does not reach (laptops, displays,
washing machines). They are separate modules, not versions of each other: the
inputs, the scale and the legal standing all differ. Note that only the
heuristic takes an AssessmentClock — the index has a single ruleset, so there
is no date-dependent choice to make.
Usage
Repairability index (enacted — EU 2023/1669 Annex IV)
use NaiveDate;
use ;
use ruleset_registry;
// Each of the three part-level parameters is scored 1–5 for all ten Annex IV
// priority parts. `folding_mechanism` is None for a non-foldable product.
let parts = ;
let inputs = RepairabilityIndexInputs ;
let result = calculate?;
println!;
// Or resolve the governing ruleset by product category and law date:
let law_date = from_ymd_opt.unwrap;
if let Some =
resolve_repairability_index.assessed
resolve_repairability_index returns an Assessability, not an Option:
NotYetInForce, Undetermined, Expired and OutOfScope are distinct answers,
and none of them means non-compliant.
Repairability (simplified non-regulatory heuristic, A–E)
use NaiveDate;
use AssessmentClock;
use ;
use ruleset_registry;
// The date the governing law attached to this product — read from the product's
// own record (`placedOnMarketDate`), never from the wall clock. There is
// deliberately no `AssessmentClock::now()`.
let placed_on_market = from_ymd_opt.unwrap;
let clock = placed_on;
let inputs = RepairabilityInputs ;
// Option A: use the ruleset directly (when you know the product category at compile time)
let result = calculate?;
println!; // "B (7.75/10)"
println!;
// Option B: resolution by governing-law date (when the category comes from a passport field)
let ruleset = resolve_repairability
.assessed
.ok_or?;
let result = calculate?;
Cradle-to-gate CO₂e
use NaiveDate;
use AssessmentClock;
use ;
let clock = placed_on;
let result = calculate?;
println!; // 5.20 kg CO₂e
CalculationReceipt
Every calculator returns a CalculationReceipt alongside the computed value:
The platform stores this alongside the passport record. A notified body can re-run
any calculation from the receipt: same inputs (verify via input_hash) + same ruleset
version + same factor dataset version → must produce the same output.
RegulatoryBasis
Every concrete ruleset carries a machine-readable legal citation:
Eu2023_1669Ruleset.regulatory_basis
// RegulatoryBasis {
// regulation: "EU 2023/1669",
// article: "Annex IV point 5 (calculation method); \
// Annex II Table 4 (class boundaries)",
// standard: Some("EN 45554:2020"),
// …
// }
A non-regulatory ruleset says so in the same field rather than leaving it blank —
SimplifiedRepairabilityHeuristic reports its regulation as
"Non-regulatory: simplified repairability heuristic (NOT EU 2023/1669 Annex IV)",
so a receipt can never imply legal standing the calculation does not have.
A CI test (expired_rulesets_have_superseded_by) asserts that any ruleset with
Effectivity::InForce.until < today has a non-empty superseded_by. This keeps the audit chain intact as regulations evolve.
Feature flags
| Flag | Default | Purpose |
|---|---|---|
synthetic-factors |
off | Exposes SyntheticFactorProvider outside #[cfg(test)]. Enable in integration test harnesses only. Values are not real LCI data. |
real-factors |
off | Gates licensed LCI factor-data implementations. Enable only in managed-service builds where a valid ecoinvent/EF reseller sublicense is in place. Never enable in open-source or self-hosted builds. |
Adding a sector calculator
See the # Adding a new sector calculator section in src/lib.rs for the full
step-by-step guide. Short version:
- New methodology → add
src/{methodology}/withmod.rs,calculator.rs,parameters.rs,thresholds.rs(trait extendsRuleset),golden_vectors.rs. Register insrc/ruleset_registry/resolve.rs. - New product category on an existing methodology → add
impl Ruleset + impl {Methodology}Rulesetin that methodology'sthresholdsmodule (a file, or a directory with one file per category as inrepairability/thresholds/), add a row to the registry, add golden vectors. - Pending delegated act → use
Effectivity::pending(empowerment, adoption_deadline). There is no date sentinel: a pending ruleset has no application date and resolves for no date at all. - Superseded ruleset → set
until, setsuperseded_by. Never delete rows.
Invariants
| Rule | Rationale |
|---|---|
#![forbid(unsafe_code)] |
Calculators run in the hot path; no unsafe allowed |
Every {Methodology}Ruleset extends Ruleset |
Forces a legal citation on every ruleset; compiler-enforced |
FactorProvider::table_hash() is pre-computed |
Called once per receipt, not per gwp100() lookup |
| Never bundle LCI secondary data | ecoinvent / EF inventory datasets are licensed; FactorProvider injects them at runtime |
real-factors feature off by default |
Open-source builds must never include licensed data |
all_rulesets() must list every concrete ruleset |
Required for the CI expiry check to cover new additions |
Relationship to other crates
| Crate | Role |
|---|---|
dpp-domain |
Domain types and port traits; dpp-calc does not depend on it |
dpp-rules |
Field-level validation rules (no_std); repairability scoring belongs here, not there |
dpp-plugin-sdk |
Sector plugins import dpp-calc for calculator functions (Phase 2) |
dpp-engine (BSL-1.1) |
Stores CalculationReceipt, serves the verification endpoint, manages FactorProvider lifecycle |
License
Apache-2.0 — see LICENSE