Skip to content

Effects

An Effect tracks a quantity (cost, CO2, primary energy, ...) across the optimization horizon. One effect is the objective to minimize; others can be bounded.

See Effects (Math) for the formulation.

Defining Effects

from fluxopt import Effect

# Objective effect (minimized)
cost = Effect('cost', is_objective=True)

# Tracked effect with a unit
co2 = Effect('co2', unit='kg')

Exactly one effect must have is_objective=True.

Linking Flows to Effects

Flows contribute to effects via effects_per_flow_hour. The value is in effect-units per flow-hour (e.g., €/MWh):

from fluxopt import Flow

# Single effect
gas_flow = Flow('gas', size=500, effects_per_flow_hour={'cost': 0.04})

# Multiple effects
gas_flow = Flow('gas', size=500, effects_per_flow_hour={'cost': 0.04, 'co2': 0.2})

At each timestep, the contribution is coefficient * flow_rate * dt.

Bounding Effects

Total Bounds

Limit the total effect over the entire horizon:

# CO2 budget: max 1000 kg total
co2 = Effect('co2', unit='kg', maximum_total=1000)

# Cost floor (e.g., minimum revenue)
revenue = Effect('revenue', minimum_total=500)

Per-Hour Bounds

Limit the effect value at each timestep:

# Max 50 kg CO2 per hour
co2 = Effect('co2', unit='kg', maximum_per_hour=50)

# Time-varying per-hour bound
co2 = Effect('co2', unit='kg', maximum_per_hour=[50, 40, 60, 50])

Cross-Effect Contributions

An effect can include a weighted contribution from another effect using contribution_from. This is useful for carbon pricing, primary energy factors, or any chain where one tracked quantity feeds into another.

Scalar (temporal + periodic)

A scalar factor applies to both domains — temporal (per-timestep) and periodic (sizing, yearly costs):

effects = [
    Effect('cost', is_objective=True, contribution_from={'co2': 50}),
    Effect('co2', unit='kg'),
]

Here, every kg of CO2 adds 50 to cost — both for temporal emissions (from flow operation) and periodic emissions (e.g., from Sizing.effects_per_size).

Time-Varying (temporal only)

Use contribution_from_per_hour for time-varying factors that override the scalar in the temporal domain:

effects = [
    Effect(
        'cost',
        is_objective=True,
        contribution_from={'co2': 50},  # scalar for periodic domain
        contribution_from_per_hour={'co2': [40, 50, 60]},  # time-varying for temporal domain
    ),
    Effect('co2', unit='kg'),
]

Transitive Chains

Contributions chain transitively. A PE -> CO2 -> cost chain is modeled as:

effects = [
    Effect('cost', is_objective=True, contribution_from={'co2': 50}),
    Effect('co2', unit='kg', contribution_from={'pe': 0.3}),
    Effect('pe', unit='kWh'),
]

Restrictions

  • No self-references: an effect cannot reference itself
  • No cycles: cost -> co2 -> cost is rejected at build time

See Effects (Math) for the formulation.

Accessing Results

After solving, the Result provides several views into effect values:

result = optimize(...)

# Objective value (shortcut for the objective effect's total)
print(result.objective)

# Total effect values as (effect,) DataArray
print(result.effect_totals)

# Temporal: per-timestep effect values as (effect, time) DataArray
print(result.effects_temporal)

# Periodic: sizing and fixed-cost effect values as (effect,) DataArray
print(result.effects_periodic)

Per-Contributor Breakdown

stats.effect_contributions decomposes effect totals into per-contributor parts on a unified contributor dimension (flow IDs + storage IDs), matching the model's temporal/periodic domain structure:

contrib = result.stats.effect_contributions

# Per-timestep contributions (contributor, effect, time) — flows only
contrib['temporal']

# Periodic contributions (contributor, effect) — flows + storages
contrib['periodic']

# Total per contributor: temporal summed over time + periodic (contributor, effect)
contrib['total']

The contributions are validated against the solver totals — if they don't sum to effect_totals, a ValueError is raised.

Cross-effects (e.g., CO2 -> cost) are attributed to the originating contributor. If a gas flow emits CO2 priced at 50 EUR/kg, its cost contribution includes both the direct cost and the carbon tax portion.

Full Example

Two sources with different cost/CO2 tradeoffs, subject to an emission cap:

from datetime import datetime
from fluxopt import Carrier, Effect, Flow, Port, optimize

timesteps = [datetime(2024, 1, 1, h) for h in range(3)]

demand = Flow('elec', size=100, fixed_relative_profile=[0.5, 0.8, 0.6])
cheap_dirty = Flow('elec', size=200, effects_per_flow_hour={'cost': 0.02, 'co2': 1.0})
expensive_clean = Flow('elec', size=200, effects_per_flow_hour={'cost': 0.10, 'co2': 0.0})

elec = Carrier('elec')

result = optimize(
    timesteps=timesteps,
    carriers=[elec],
    effects=[
        Effect('cost', is_objective=True),
        Effect('co2', maximum_total=100),
    ],
    ports=[
        Port('cheap', imports=[cheap_dirty]),
        Port('clean', imports=[expensive_clean]),
        Port('demand', exports=[demand]),
    ],
)

print(f"Total cost: {result.objective:.2f}")
print(result.effect_totals)

Parameters Summary

Parameter Type Default Description
id str required Effect identifier
unit str '' Unit label
is_objective bool False Whether this effect is minimized
maximum_total float \| None None Upper bound on total
minimum_total float \| None None Lower bound on total
maximum_per_hour TimeSeries \| None None Upper bound per timestep
minimum_per_hour TimeSeries \| None None Lower bound per timestep
contribution_from dict[str, float] {} Cross-effect factor (both domains)
contribution_from_per_hour dict[str, TimeSeries] {} Cross-effect factor (temporal domain only)