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 -> costis 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) |