mixle.analysis.emissions module¶
Scope 1/2/3 GHG (carbon) accounting for an operation’s production activity, plus the L6 climate objective + risk terms folded into J’s objective and H’s optimizer (work-plan Sec.7-L).
Maps a production activity schedule – direct fuel combustion and blasting, purchased-grid
electricity draw, and upstream reagents / downstream haulage – onto CO2e emissions via
GHG-Protocol-style EmissionFactors.
emissions_footprint()– Scope 1 (direct combustion/blasting) + Scope 2 (purchased electricity) + Scope 3 (upstream reagents / downstream transport) totals, with an optional Monte-Carlo 90% credible interval when per-factor uncertainties (EmissionFactors.sigma) are supplied, and a content-addressedactivity_content_hashin the returnedFootprint’s provenance so every number traces back to the exact activity schedule that produced it.
transition_risk()– prices aFootprintagainst a set of carbon-price/policy scenario paths and subtracts the resulting per-scenario carbon cost from a J2npv_samplesdistribution, returning an IC-1 DerivedQuantity that carries the carbon-adjusted NPV samples (uncertainty-aware, not just a point estimate) plus a mean-value scenario ranking.
climate_terms()– folds aFootprintand an optional water budget into the two numbers J6’s objective (analysis/valuation.py) and H4’s optimizer (stochastic_opt.py) need: a priced carbon cost, and a hard water-feasibility flag (an explicitNone/unknown state, not a permissive default, when water evidence is missing or non-finite), plus a shortfall time-fraction downside term – a single-trajectory duration statistic, not a probability.
Emission factors are always supplied by the caller (or an upstream knowledge store) – this module vendors no lifecycle-inventory database; see the work-plan Non-goals for L1.
Repo-boundary note (L6): L2’s WaterBudget (a dataclass in the separate mixle-pde repo) is not a
dependency of mixle core and is never imported here; water duck-types against the local
WaterBudget structural stand-in below (any object exposing a .shortfall_m3 float and,
optionally, .storage/.provenance/.demand_m3 satisfies it), so this module has no runtime
dependency on mixle-pde. The stand-in is a real name in THIS module’s namespace – unlike the bare
forward-reference string "WaterBudget | None" this replaced, which named an import that never
happens and left climate_terms’s annotation unresolvable to typing.get_type_hints().
- class EmissionFactors(scope1, scope2, scope3, sigma=None)[source]
Bases:
objectPer-activity-key CO2e emission factors, one dict per GHG-Protocol scope.
scope1/scope2/scope3map an activity key (e.g."diesel_L","grid_kWh","explosives_kg","transport_t_km") to a CO2e factor expressed per unit of that activity (e.g. kg CO2e per litre of diesel). A key absent from a scope’s dict simply does not contribute to that scope.sigmaoptionally gives the standard deviation of each factor (not the activity quantity itself), keyed the same way across all three scopes, for Monte-Carlo uncertainty propagation inemissions_footprint(); a key with no entry insigmais treated as exactly known (std 0).
- class Footprint(scope1, scope2, scope3, total, ci=None, provenance=<factory>)[source]
Bases:
objectA Scope 1/2/3 CO2e footprint with an optional 90% credible interval and full provenance.
scope1/scope2/scope3/totalare in the same physical CO2e units as the emission factors (typically kg CO2e / tCO2e).ciis the(lo, hi)90% Monte-Carlo interval ontotalwhen factor uncertainties were propagated, elseNone(the default – a caller that only needsclimate_terms()’ point costs can construct one withoutci/provenance).provenanceidentifies both inputs to theactivity x factorsproduct, plus how the interval was produced:factor_source; the 64-hexactivity_content_hashandfactor_content_hash(sha256 of the canonical activity / factor-model encodings, the same hashing convention IC-2 uses for field artifacts);factor_keys(the union of activity keys the factor model prices, for a readable diff when the two hashes disagree); thescopesactually included intotal; anddraws/uncertainty_propagateddescribing the Monte-Carlo configuration behindci. Hashing the activity alone was not enough to distinguish two runs: the same schedule costed against a supplier-specific factor set and against a national average is a different footprint with different provenance.The record is frozen: a reported footprint is evidence, and rebinding
totalorscope2after construction would leave the numbers, the interval and the provenance describing three different computations.ciis normalized to a(lo, hi)tuple andprovenanceto a dict this record owns, so a later edit to the caller’s own dict cannot retroactively rewrite what a footprint claims it was computed from.That copy is DEEP (MXR-080-1900).
dict(provenance)detaches only the top level, so a nested container –provenance["scopes"]as a list, a nested factor-model dict – stayed aliased to the caller’s own object and could be rewritten after the footprint was reported. Deliberately NOT checked here: thattotalequalsscope1 + scope2 + scope3. The producer always makes it so, but the record is documented as constructible directly for callers that only needclimate_terms()’ point costs, and the test-suite already builds partially-specified footprints; a sum invariant would refuse states the library legitimately accepts.
- emissions_footprint(activity, factors, *, scopes=(1, 2, 3), n=0, rng=None)[source]
Scope 1/2/3 CO2e footprint of a production
activityschedule.Each
activitykey (e.g.diesel_L,grid_kWh,explosives_kg,transport_t_km) is priced by the corresponding factor in whichever offactors.scope1/scope2/scope3includes that key: Scope 1 is direct combustion/blasting, Scope 2 is purchased electricity, Scope 3 is upstream reagents plus downstream transport.scopesselects which of the three scopes are actually included in the returned footprint (a scope not requested reports0.0and does not contribute tototal– e.g.scopes=(1, 2)for a Scope-1/2-only disclosure).If
n > 0andfactors.sigmais supplied, each priced factor is treated asNormal(mean=factor, std=sigma.get(key, 0.0))and resampledntimes (factors with nosigmaentry stay fixed); the resulting distribution oftotalyields a 90% credible interval inci. Without both a positivenand a non-emptysigma,ciisNone– the point total is still returned, just without an uncertainty band.provenancealways fingerprints both factors of theactivity x factorsproduct –activity_content_hashANDfactor_content_hash(64-hex sha256 over the canonical encodings, IC-2’s hashing convention) – alongsidefactor_keys,scopes,drawsanduncertainty_propagated. A downstream carbon-cost or transition-risk term (L3/L6) can therefore be traced back to the exact activity schedule and the exact factor table it was priced with; recording only the activity meant a footprint restated against a revised or supplier-specific factor set was indistinguishable from the original by its provenance alone.scopesmust not contain duplicate entries (each GHG-Protocol scope may appear at most once) – a duplicate raises rather than being silently summed once on the point-total path and once per occurrence on the Monte-Carlo path, which previously left the point estimate and the credible interval describing two different footprints.activityvalues must be finite and nonnegative;factors(scope1/scope2/scope3) values must be finite;factors.sigmavalues, when supplied, must be finite and nonnegative – a negative sigma is rejected rather than silently treated as an exactly-known (zero-uncertainty) factor.nmust be an exact non-Boolean nonnegative integer; zero explicitly requests the point estimate without an interval. All of this is validated before either the point-total or the Monte-Carlo path runs.
- class TransitionRiskResult(samples, prior_dominated, scenario_mean, ranking, carbon_cost, provenance)[source]
Bases:
objectThe carbon-adjusted NPV distribution across carbon-price/policy scenarios (L3).
Satisfies the frozen
mixle.reason.posterior_protocol.DerivedQuantitystructural protocol –samples,prior_dominated,credible_interval– so a carbon-adjusted value can flow anywhere a DerivedQuantity is expected (J5 tail risk, J2 re-valuation).samplesis shaped(n, k): thenbaselinenpv_samplesdraws, each re-priced under every one of thekcarbon-price scenarios (one column per scenario) – the re-ranking below stays uncertainty-aware rather than collapsing straight to a point estimate.prior_dominatedis alwaysFalse: there is no prior/regulariser here, the distribution’s width is set entirely bynpv_samples.Beyond the protocol,
scenario_mean(per-scenario mean carbon-adjusted NPV),ranking(scenario indices sorted best -> worst byscenario_mean), andcarbon_cost(the priced-and-discounted carbon cost subtracted from each scenario) carry the scenario-level comparisontransition_risk()exists to produce.Construction validates
samples: non-empty and finite (no NaN/Inf) – defense-in-depth so invalid state can never flow downstream to a caller even if some upstream pushforward (here,transition_risk()) fails to validate its own inputs, the same guard applied tocarcinogenic_risk.RiskQuantityand the_SampleDerivedQuantitycarriers elsewhere inmixle.analysis.The record is frozen and owns its arrays (validated copies, write-locked), so
rankingandscenario_meancannot drift out of agreement with thesamplesthey were derived from: an unfrozen record let a caller reorder the ranking or overwrite a scenario mean while every summary method kept reporting off the original draws.Freezing only stopped the drift AFTER construction, though. Construction itself accepted any finite
scenario_meanand any permutationranking, so a record could be born already contradicting its own draws –scenario_meanreporting one ordering of the scenarios whilecredible_interval(), computed offsamples, reported another (MXR-080-1900). Two agreement invariants now close that, both holding exactly for whattransition_risk()produces:scenario_meanmust equal the per-scenario (column) mean ofsamples, to a tolerance scaled by the magnitude of the draws – summation order, not disagreement, is all that is allowed to separate them.scenario_mean[ranking]must be non-increasing, i.e. the ranking really does run best -> worst. Ties may be broken in any order, sincenp.argsortgives no stable tie order and demanding a particular one would refuse rankings the producer itself emits.
Additionally, when
provenancecarries the module’s own"carbon_cost"key (the doubled representationtransition_risk()writes), it must agree with thecarbon_costfield – the two used to be free to disagree about the very cost the summaries were priced from.Deliberately NOT checked: that
samples[:, j] + carbon_cost[j]recovers one common baseline NPV across scenarios. It is true oftransition_risk()’s output, but the record is also constructed directly with independentsamples/carbon_costin this package’s own tests, so requiring it would reject states the library legitimately produces.- Parameters:
- transition_risk(footprint, carbon_price_paths, *, npv_samples, discount=None)[source]
Carbon-adjusted NPV distribution + scenario ranking under a set of carbon-price paths (L3).
For each of the
kscenarios incarbon_price_paths(a(k,)flat price or a(k, t)per-period path – see_coerce_price_paths()), the priced carbon cost isfootprint.total * sum_t(price[t] * discount[t])(discountdefaults to all-ones, i.e. no discounting, when omitted – pass period discount factors, e.g.1 / (1 + r) ** t, to match a J2monte_carlo_npvDCF). That per-scenario carbon cost is subtracted from every draw of the baselinenpv_samples(a J2 NPVDistribution.samples-shaped(n,)array), yielding an(n, k)carbon-adjusted value distribution: one re-priced NPV distribution per scenario, still carrying the original valuation uncertainty.Scenarios are ranked by mean carbon-adjusted NPV (
scenario_mean, descending: best scenario first) so a high-carbon-price/policy scenario reliably re-ranks below a low-price one, with the gap between any two scenarios scaling linearly infootprint.total– a bigger footprint pays proportionally more carbon cost under the same price paths. The returnedTransitionRiskResultsatisfies IC-1’s DerivedQuantity protocol so the re-ranking is always inspectable with a credible interval, not just a point estimate; it feeds J5 tail risk and J2 re-valuation directly.Validated before any ranking happens:
carbon_price_pathsmust have at least one scenario and at least one period, with every price finite and nonnegative;discount, when supplied, must be finite and nonnegative;footprint.totalmust be finite; andnpv_samplesmust be a non-empty, finite, one-dimensional(n,)array (a multi-dimensional array is rejected, not silently flattened). Letting any of these through used to allow NaN scenario means and a meaningless ranking to construct successfully, failing only later when a caller requested a credible interval.
- class WaterBudget(*args, **kwargs)[source]
Bases:
ProtocolStructural stand-in for L2’s WaterBudget (mixle-pde) – see the module’s repo-boundary note.
Never constructed here; a real
WaterBudget(or anything shaped like one) is passed in and duck-typed viagetattr(water, ..., default)throughout this module, so a caller-supplied object satisfying onlyshortfall_m3(the one required field) still works – the optional fields default to absent, matching the “or None if absent” fallbacks already documented on_water_demand_m3()and_shortfall_time_fraction().
- climate_terms(footprint, water, *, carbon_price, water_limit_m3=None)[source]
Fold an emissions footprint and a water budget into J6’s carbon cost + H4’s water constraint.
Returns
{"carbon_cost": float, "water_feasible": bool | None, "water_binding": bool | None, "shortfall_time_fraction": float | None}:carbon_cost = footprint.total * carbon_price– the priced carbon term J6’s objective (analysis/valuation.py) subtracts alongside capex_opex’s cost roll-up. Always a definite float; it does not depend on water evidence.water_bindingis set when the L2 WaterBudget.shortfall_m3 > 0 (the routed water balance already ran dry at some point over the plan) OR when a caller-suppliedwater_limit_m3is given and the budget’s demand exceeds it;water_feasibleis its negation – a hard constraint H4 (stochastic_opt.py) folds into a per-option cost (derating or dropping the option) rather than into the mean-grade objective. Either can come backNone(unknown) instead of a definitebool– see below.shortfall_time_fractionis the fraction of the water budget’s own per-step storage trajectory sitting at a binding zero (_shortfall_time_fraction()) – a deterministic duration/occupancy statistic over ONE trajectory, reported independently ofwater_limit_m3even for options that are feasible on average but fragile. This is NOT a probability: it carries no uncertainty information. A genuine cross-realization probability requires an explicit ensemble of trajectories (_shortfall_probability_over_ensemble()), which is not available from a singlewaterobject.
Missing or non-finite water evidence is an explicit UNKNOWN state, never a permissive pass:
When
waterisNone(no water budget available for this option – e.g. an inland option with no catchment exposure),water_feasible,water_binding, andshortfall_time_fractionare allNone: climate risk for that option is carbon-only, but the water terms are reported as “not evaluated”, not silently “known feasible”.When
shortfall_m3, the water-limit demand, orwater_limit_m3itself is non-finite (NaN/inf) or negative, that check contributesNone(unknown) rather than silently comparing and passing as non-binding. All three are physical volumes in m3 and cannot be negative under the documented contract, so a negative one is malformed evidence rather than evidence of comfortable headroom (or, for a negative demand/limit, of a violation). The two binding checks combine via three-valued (Kleene) OR: a confirmed binding signal from either check always wins, an unknown never gets rounded down to “feasible”.When a
waterobject is supplied but carries no readable shortfall – the attribute is absent,None, a Boolean, or non-numeric – the shortfall check likewise contributesNone. This is the case the promise above used to skip (MXR-080-1900): thewater is Nonebranch abstained correctly, but a present-but-empty water object tookgetattr(..., shortfall_m3, 0.0)’s numeric default and reportedwater_feasible=True, a definite all-clear on a hard constraint from an object holding no water evidence at all.shortfall_time_fractionreportsNonein the same case rather than a confident0.0(“never short over the whole trajectory”) – unless a realstoragetrajectory is present, which is genuine evidence and is still used.