Skip to content

refac: rename cross-effect API and add once domain #105

@FBumann

Description

@FBumann

Summary

Rename the cross-effect fields on Effect to domain-explicit names and add the missing cross_once for one-time investment costs.

Current API (to be removed)

Effect(
    'cost',
    contribution_from={'co2': 50},           # → cf_periodic
    contribution_from_per_hour={'co2': 50},   # → cf_temporal
)

Problems:

  • contribution_from sounds like it applies to everything, but only covers the periodic domain (sizing, recurring investment)
  • contribution_from_per_hour hints at temporal but the mapping is implicit
  • No way to price one-time investment costs (embodied emissions, etc.) through cross-effects

New API

Effect(
    'cost',
    cross_temporal={'co2': 50},    # per-timestep domain
    cross_periodic={'co2': 50},    # sizing + recurring investment domain
    cross_once={'co2': 50},        # one-time investment domain
)

Hard rename, no deprecation (pre-v1 alpha).

Mapping

Effect field EffectsData matrix Applied to Domain
cross_temporal cf_temporal (existing) effect_temporal operational per-timestep
cross_periodic cf_periodic (existing) effect_periodic sizing, recurring investment
cross_once cf_once (new) effect_once one-time investment

Implementation

1. elements.py — rename fields on Effect

@dataclass
class Effect:
    # ...
    cross_temporal: dict[str, TimeSeries] = field(default_factory=dict)
    cross_periodic: dict[str, TimeSeries] = field(default_factory=dict)
    cross_once: dict[str, TimeSeries] = field(default_factory=dict)

Remove contribution_from and contribution_from_per_hour.

2. model_data.py — build cf_once matrix

EffectsData.build() already constructs cf_temporal and cf_periodic from the old fields. Add parallel construction for cf_once from cross_once. Add cf_once: xr.DataArray | None to EffectsData.

3. model.py — apply Leontief to effect_once

In _create_effects(), after computing once_direct, apply cf_once Leontief:

once_rhs: Any = once_direct
if ds.cf_once is not None:
    source_o = self.effect_once.rename({'effect': 'source_effect'})
    cross = (ds.cf_once * source_o).sum('source_effect')
    once_rhs = cross + once_direct

self.m.add_constraints(self.effect_once == once_rhs, name='effect_once_eq')

4. contributions.py — apply Leontief to once domain

if data.effects.cf_once is not None:
    once = _apply_leontief(_leontief(data.effects.cf_once), once)

5. io.py — serialize cf_once

Add cf_once to the effects dataset serialization, parallel to cf_temporal / cf_periodic.

6. Tests — update all cross-effect tests

  • Rename contribution_from=cross_periodic= in all test files
  • Rename contribution_from_per_hour=cross_temporal=
  • Add tests for cross_once with Investment one-time costs
  • Verify the existing test_contribution_from_with_investment_once_costs works with cross_once

Files to change

  • src/fluxopt/elements.py — field rename
  • src/fluxopt/model_data.py — build cf_once, rename field references
  • src/fluxopt/model.py — apply cf_once Leontief to effect_once
  • src/fluxopt/contributions.py — apply cf_once Leontief to once contributions
  • src/fluxopt/io.py — serialize/deserialize cf_once
  • tests/ — rename all contribution_fromcross_periodic etc.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions