Skip to content

fluxopt.contributions

Per-contributor effect breakdown, read off the program.

Every contribution the ledger sums is a named expression in math/program/, declared with the entity it came from and reduced only where effect_accounting adds it up. So the breakdown is not a second implementation of the effect math that has to be checked against the first — it is the same declaration, read one step before the sum.

Two views, as before:

  • with cross-effects (default): each contributor is charged the full priced-in cost, CO2 through to cost. This is what the expressions give directly, because the coefficients bound to the program already carry the Leontief inverse (:mod:fluxopt.leontief) — the model never multiplies it at build time.
  • direct: each contributor shows only what it directly emits, recovered as (I - C) . charged. A forward multiply, and the exact inverse of the fold the binder applied.

The contributor axis is a presentation choice rather than model math: flows and storages share one dimension, and a component-level cost is attributed to the first flow its status governs, having no single natural flow of its own.

Functions:

Name Description
contributions_from

The breakdown, assembled from the program's own contribution expressions.

validate_against_solver

The breakdown must add up to the totals the solver reported.

contributions_from

contributions_from(
    read: Any, data: ModelData, *, cross_effects: bool = True
) -> Dataset

The breakdown, assembled from the program's own contribution expressions.

Parameters:

Name Type Description Default

read

Any

Reads one named expression as a labelled array, or None where the program declares it but this system has no rows for it.

required

data

ModelData

The model data that was bound.

required

cross_effects

bool

Charge each contributor the full priced-in cost (default), or show only what it directly emits.

True

Returns:

Type Description
Dataset

Dataset with temporal (contributor, effect, time), lump

Dataset

(contributor, effect) and total (contributor, effect).

Source code in src/fluxopt/contributions.py
def contributions_from(read: Any, data: ModelData, *, cross_effects: bool = True) -> xr.Dataset:
    """The breakdown, assembled from the program's own contribution expressions.

    Args:
        read: Reads one named expression as a labelled array, or None where
            the program declares it but this system has no rows for it.
        data: The model data that was bound.
        cross_effects: Charge each contributor the full priced-in cost
            (default), or show only what it directly emits.

    Returns:
        Dataset with ``temporal`` (contributor, effect, time), ``lump``
        (contributor, effect) and ``total`` (contributor, effect).
    """
    flow_ids = data.flows.ids
    stor_ids = data.storages.ids if data.storages is not None else []
    all_ids = flow_ids + stor_ids

    temporal = _gather(read, TEMPORAL, data, all_ids, None)
    lump = _gather(read, LUMP, data, all_ids, 'build_period')
    if not cross_effects:
        temporal, lump = _undo_cross_effects(temporal, lump, data)
    return _finalize(temporal, lump, all_ids, data)

validate_against_solver

validate_against_solver(total: DataArray, solution: Dataset) -> None

The breakdown must add up to the totals the solver reported.

Kept although the breakdown and the ledger are now one declaration: what it guards is no longer a second implementation of the math, but the assembly above it — the contributor mapping, the reindexing, and the coordinate order the comparison is positional in.

Source code in src/fluxopt/contributions.py
def validate_against_solver(total: xr.DataArray, solution: xr.Dataset) -> None:
    """The breakdown must add up to the totals the solver reported.

    Kept although the breakdown and the ledger are now one declaration: what
    it guards is no longer a second implementation of the math, but the
    assembly above it — the contributor mapping, the reindexing, and the
    coordinate order the comparison is positional in.
    """
    solver = solution[Var.EFFECT_TOTAL]
    # Aligned by dim *name* — which axis comes first is arbitrary and differs
    # between the solution and an expression's own declaration order. What
    # stays positional is the coordinate order *within* each dim, because a
    # misordering there is a real pipeline bug rather than a presentation one.
    computed = total.sum('contributor').transpose(*solver.dims)
    if not np.allclose(computed.values, solver.values, atol=1e-6):
        diff = abs(computed - solver)
        raise ValueError(
            f'Effect contributions do not sum to solver totals. Max deviation: {float(diff.max().values):.6g}'
        )