Skip to content

Commit bc0a262

Browse files
authored
Feature/v3/feature/305 rename specific share to other effects to specific share from effect (#366)
* Step 1 * Bugfix * Make fit_effects_to_model_coords() more flexible * Fix dims * Update conftest.py * Typos * Improve Effect examples * Add extra validation for Effect Shares
1 parent 10f455b commit bc0a262

9 files changed

Lines changed: 135 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ Several internal improvements were made to the codebase.
4848
4949
### ✨ Added
5050
51+
**Intuitive effect share syntax:**
52+
Effects now support an intuitive `share_from_*` syntax for cross-effect relationships:
53+
```python
54+
costs = fx.Effect(
55+
'costs', '€', 'Total costs',
56+
is_standard=True, is_objective=True,
57+
share_from_temporal={'CO2': 0.2, 'energy': 0.05}, # Costs receive contributions from other effects
58+
share_from_nontemporal={'land': 100} # €100 per m² land use
59+
)
60+
```
61+
This replaces the less intuitive `specific_share_to_other_effects_*` parameters and makes it clearer where effect contributions are coming from, rather than where they are going to.
62+
5163
**Multi-year investments:**
5264
A flixopt model might be modeled with a "year" dimension.
5365
This enables modeling transformation pathways over multiple years with several investment decisions
@@ -117,6 +129,10 @@ The weighted sum of the total objective effect of each scenario is used as the o
117129
- `minimum_operation_per_hour` → `minimum_per_hour`
118130
- `maximum_operation_per_hour` → `maximum_per_hour`
119131
132+
### 🔥 Removed
133+
* **Effect share parameters**: The old `specific_share_to_other_effects_*` parameters were replaced WITHOUT DEPRECATION
134+
- `specific_share_to_other_effects_operation` → `share_from_temporal` (with inverted direction)
135+
- `specific_share_to_other_effects_invest` → `share_from_nontemporal` (with inverted direction)
120136
121137
### 🐛 Fixed
122138
* Enhanced NetCDF I/O with proper attribute preservation for DataArrays

examples/01_Simple/simple_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@
2929
description='Kosten',
3030
is_standard=True, # standard effect: no explicit value needed for costs
3131
is_objective=True, # Minimizing costs as the optimization objective
32+
share_from_temporal={'CO2': 0.2},
3233
)
3334

3435
# CO2 emissions effect with an associated cost impact
3536
CO2 = fx.Effect(
3637
label='CO2',
3738
unit='kg',
3839
description='CO2_e-Emissionen',
39-
specific_share_to_other_effects_operation={costs.label: 0.2},
4040
maximum_operation_per_hour=1000, # Max CO2 emissions per hour
4141
)
4242

examples/02_Complex/complex_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040

4141
# --- Define Effects ---
4242
# Specify effects related to costs, CO2 emissions, and primary energy consumption
43-
Costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True)
44-
CO2 = fx.Effect('CO2', 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={Costs.label: 0.2})
43+
Costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2})
44+
CO2 = fx.Effect('CO2', 'kg', 'CO2_e-Emissionen')
4545
PE = fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3)
4646

4747
# --- Define Components ---

examples/04_Scenarios/scenario_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@
3535
description='Kosten',
3636
is_standard=True, # standard effect: no explicit value needed for costs
3737
is_objective=True, # Minimizing costs as the optimization objective
38+
share_from_temporal={'CO2': 0.2},
3839
)
3940

4041
# CO2 emissions effect with an associated cost impact
4142
CO2 = fx.Effect(
4243
label='CO2',
4344
unit='kg',
4445
description='CO2_e-Emissionen',
45-
specific_share_to_other_effects_operation={costs.label: 0.2},
4646
maximum_operation_per_hour=1000, # Max CO2 emissions per hour
4747
)
4848

flixopt/effects.py

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ class Effect(Element):
5050
without effect dictionaries. Used for simplified effect specification (and less boilerplate code).
5151
is_objective: If True, this effect serves as the optimization objective function.
5252
Only one effect can be marked as objective per optimization.
53-
specific_share_to_other_effects_operation: Operational cross-effect contributions.
54-
Maps this effect's operational values to contributions to other effects
55-
specific_share_to_other_effects_invest: Investment cross-effect contributions.
56-
Maps this effect's investment values to contributions to other effects.
53+
share_from_temporal: Temporal cross-effect contributions.
54+
Maps temporal contributions from other effects to this effect
55+
share_from_nontemporal: Nontemporal cross-effect contributions.
56+
Maps nontemporal contributions from other effects to this effect.
5757
minimum_temporal: Minimum allowed total contribution across all timesteps.
5858
maximum_temporal: Maximum allowed total contribution across all timesteps.
5959
minimum_per_hour: Minimum allowed contribution per hour.
@@ -77,17 +77,21 @@ class Effect(Element):
7777
Basic cost objective:
7878
7979
```python
80-
cost_effect = Effect(label='system_costs', unit='€', description='Total system costs', is_objective=True)
80+
cost_effect = Effect(
81+
label='system_costs',
82+
unit='€',
83+
description='Total system costs',
84+
is_objective=True,
85+
)
8186
```
8287
83-
CO2 emissions with carbon pricing:
88+
CO2 emissions:
8489
8590
```python
8691
co2_effect = Effect(
87-
label='co2_emissions',
92+
label='CO2',
8893
unit='kg_CO2',
8994
description='Carbon dioxide emissions',
90-
specific_share_to_other_effects_operation={'costs': 50}, # €50/t_CO2
9195
maximum_total=1_000_000, # 1000 t CO2 annual limit
9296
)
9397
```
@@ -110,7 +114,21 @@ class Effect(Element):
110114
label='primary_energy',
111115
unit='kWh_primary',
112116
description='Primary energy consumption',
113-
specific_share_to_other_effects_operation={'costs': 0.08}, # €0.08/kWh
117+
)
118+
```
119+
120+
Cost objective with carbon and primary energy pricing:
121+
122+
```python
123+
cost_effect = Effect(
124+
label='system_costs',
125+
unit='€',
126+
description='Total system costs',
127+
is_objective=True,
128+
share_from_temporal={
129+
'primary_energy': 0.08, # 0.08 €/kWh_primary
130+
'CO2': 0.2, # Carbon pricing: 0.2 €/kg_CO2 into costs if used on a cost effect
131+
},
114132
)
115133
```
116134
@@ -137,8 +155,7 @@ class Effect(Element):
137155
across all contributions to each effect manually.
138156
139157
Effects are accumulated as:
140-
- Total = Σ(operational contributions) + Σ(investment contributions)
141-
- Cross-effects add to target effects based on specific_share ratios
158+
- Total = Σ(temporal contributions) + Σ(nontemporal contributions)
142159
143160
"""
144161

@@ -150,8 +167,8 @@ def __init__(
150167
meta_data: dict | None = None,
151168
is_standard: bool = False,
152169
is_objective: bool = False,
153-
specific_share_to_other_effects_operation: TemporalEffectsUser | None = None,
154-
specific_share_to_other_effects_invest: NonTemporalEffectsUser | None = None,
170+
share_from_temporal: TemporalEffectsUser | None = None,
171+
share_from_nontemporal: NonTemporalEffectsUser | None = None,
155172
minimum_temporal: NonTemporalEffectsUser | None = None,
156173
maximum_temporal: NonTemporalEffectsUser | None = None,
157174
minimum_nontemporal: NonTemporalEffectsUser | None = None,
@@ -167,11 +184,9 @@ def __init__(
167184
self.description = description
168185
self.is_standard = is_standard
169186
self.is_objective = is_objective
170-
self.specific_share_to_other_effects_operation: TemporalEffectsUser = (
171-
specific_share_to_other_effects_operation if specific_share_to_other_effects_operation is not None else {}
172-
)
173-
self.specific_share_to_other_effects_invest: NonTemporalEffectsUser = (
174-
specific_share_to_other_effects_invest if specific_share_to_other_effects_invest is not None else {}
187+
self.share_from_temporal: TemporalEffectsUser = share_from_temporal if share_from_temporal is not None else {}
188+
self.share_from_nontemporal: NonTemporalEffectsUser = (
189+
share_from_nontemporal if share_from_nontemporal is not None else {}
175190
)
176191

177192
# Handle backwards compatibility for deprecated parameters
@@ -393,8 +408,17 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None
393408

394409
self.maximum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|maximum_per_hour', self.maximum_per_hour)
395410

396-
self.specific_share_to_other_effects_operation = flow_system.fit_effects_to_model_coords(
397-
f'{prefix}|operation->', self.specific_share_to_other_effects_operation, 'temporal'
411+
self.share_from_temporal = flow_system.fit_effects_to_model_coords(
412+
label_prefix=None,
413+
effect_values=self.share_from_temporal,
414+
label_suffix=f'(temporal)->{prefix}(temporal)',
415+
dims=['time', 'year', 'scenario'],
416+
)
417+
self.share_from_nontemporal = flow_system.fit_effects_to_model_coords(
418+
label_prefix=None,
419+
effect_values=self.share_from_nontemporal,
420+
label_suffix=f'(nontemporal)->{prefix}(nontemporal)',
421+
dims=['year', 'scenario'],
398422
)
399423

400424
self.minimum_temporal = flow_system.fit_to_model_coords(
@@ -417,12 +441,6 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None
417441
self.maximum_total = flow_system.fit_to_model_coords(
418442
f'{prefix}|maximum_total', self.maximum_total, dims=['year', 'scenario']
419443
)
420-
self.specific_share_to_other_effects_invest = flow_system.fit_effects_to_model_coords(
421-
f'{prefix}|operation->',
422-
self.specific_share_to_other_effects_invest,
423-
'operation',
424-
dims=['year', 'scenario'],
425-
)
426444

427445
def create_model(self, model: FlowSystemModel) -> EffectModel:
428446
self._plausibility_checks()
@@ -569,6 +587,11 @@ def _plausibility_checks(self) -> None:
569587
# Check circular loops in effects:
570588
temporal, nontemporal = self.calculate_effect_share_factors()
571589

590+
# Validate all referenced sources exist
591+
unknown = {src for src, _ in list(temporal.keys()) + list(nontemporal.keys()) if src not in self.effects}
592+
if unknown:
593+
raise KeyError(f'Unknown effects used in in effect share mappings: {sorted(unknown)}')
594+
572595
temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal]))
573596
nontemporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in nontemporal]))
574597

@@ -656,18 +679,20 @@ def calculate_effect_share_factors(
656679
]:
657680
shares_nontemporal = {}
658681
for name, effect in self.effects.items():
659-
if effect.specific_share_to_other_effects_invest:
660-
shares_nontemporal[name] = {
661-
target: data for target, data in effect.specific_share_to_other_effects_invest.items()
662-
}
682+
if effect.share_from_nontemporal:
683+
for source, data in effect.share_from_nontemporal.items():
684+
if source not in shares_nontemporal:
685+
shares_nontemporal[source] = {}
686+
shares_nontemporal[source][name] = data
663687
shares_nontemporal = calculate_all_conversion_paths(shares_nontemporal)
664688

665689
shares_temporal = {}
666690
for name, effect in self.effects.items():
667-
if effect.specific_share_to_other_effects_operation:
668-
shares_temporal[name] = {
669-
target: data for target, data in effect.specific_share_to_other_effects_operation.items()
670-
}
691+
if effect.share_from_temporal:
692+
for source, data in effect.share_from_temporal.items():
693+
if source not in shares_temporal:
694+
shares_temporal[source] = {}
695+
shares_temporal[source][name] = data
671696
shares_temporal = calculate_all_conversion_paths(shares_temporal)
672697

673698
return shares_temporal, shares_nontemporal
@@ -726,19 +751,19 @@ def _do_modeling(self):
726751
)
727752

728753
def _add_share_between_effects(self):
729-
for origin_effect in self.effects:
730-
# 1. temporal: -> hier sind es Zeitreihen (share_TS)
731-
for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items():
732-
self.effects[target_effect].submodel.temporal.add_share(
733-
origin_effect.submodel.temporal.label_full,
734-
origin_effect.submodel.temporal.total_per_timestep * time_series,
754+
for target_effect in self.effects:
755+
# 1. temporal: <- receiving temporal shares from other effects
756+
for source_effect, time_series in target_effect.share_from_temporal.items():
757+
target_effect.submodel.temporal.add_share(
758+
self.effects[source_effect].submodel.temporal.label_full,
759+
self.effects[source_effect].submodel.temporal.total_per_timestep * time_series,
735760
dims=('time', 'year', 'scenario'),
736761
)
737-
# 2. nontemporal: -> hier ist es Scalar (share)
738-
for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items():
739-
self.effects[target_effect].submodel.nontemporal.add_share(
740-
origin_effect.submodel.nontemporal.label_full,
741-
origin_effect.submodel.nontemporal.total * factor,
762+
# 2. nontemporal: <- receiving nontemporal shares from other effects
763+
for source_effect, factor in target_effect.share_from_nontemporal.items():
764+
target_effect.submodel.nontemporal.add_share(
765+
self.effects[source_effect].submodel.nontemporal.label_full,
766+
self.effects[source_effect].submodel.nontemporal.total * factor,
742767
dims=('year', 'scenario'),
743768
)
744769

flixopt/flow_system.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ def fit_effects_to_model_coords(
407407
effect_values: TemporalEffectsUser | NonTemporalEffectsUser | None,
408408
label_suffix: str | None = None,
409409
dims: Collection[FlowSystemDimensions] | None = None,
410+
delimiter: str = '|',
410411
) -> TemporalEffects | NonTemporalEffects | None:
411412
"""
412413
Transform EffectValues from the user to Internal Datatypes aligned with model coordinates.
@@ -418,7 +419,7 @@ def fit_effects_to_model_coords(
418419

419420
return {
420421
effect: self.fit_to_model_coords(
421-
'|'.join(filter(None, [label_prefix, effect, label_suffix])),
422+
str(delimiter).join(filter(None, [label_prefix, effect, label_suffix])),
422423
value,
423424
dims=dims,
424425
)

tests/conftest.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,12 @@ def costs():
100100
return fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True)
101101

102102
@staticmethod
103-
def co2():
104-
return fx.Effect('CO2', 'kg', 'CO2_e-Emissionen')
103+
def costs_with_co2_share():
104+
return fx.Effect('costs', '', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2})
105105

106106
@staticmethod
107-
def co2_with_costs_share():
108-
return fx.Effect(
109-
'CO2',
110-
'kg',
111-
'CO2_e-Emissionen',
112-
specific_share_to_other_effects_operation={'costs': 0.2},
113-
)
107+
def co2():
108+
return fx.Effect('CO2', 'kg', 'CO2_e-Emissionen')
114109

115110
@staticmethod
116111
def primary_energy():
@@ -388,8 +383,8 @@ def simple_flow_system() -> fx.FlowSystem:
388383
base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time')
389384

390385
# Define effects
391-
costs = Effects.costs()
392-
co2 = Effects.co2_with_costs_share()
386+
costs = Effects.costs_with_co2_share()
387+
co2 = Effects.co2()
393388
co2.maximum_operation_per_hour = 1000
394389

395390
# Create components
@@ -418,8 +413,8 @@ def simple_flow_system_scenarios() -> fx.FlowSystem:
418413
base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time')
419414

420415
# Define effects
421-
costs = Effects.costs()
422-
co2 = Effects.co2_with_costs_share()
416+
costs = Effects.costs_with_co2_share()
417+
co2 = Effects.co2()
423418
co2.maximum_operation_per_hour = 1000
424419

425420
# Create components
@@ -471,7 +466,7 @@ def flow_system_complex() -> fx.FlowSystem:
471466
# Define the components and flow_system
472467
costs = Effects.costs()
473468
co2 = Effects.co2()
474-
co2.specific_share_to_other_effects_operation = {'costs': 0.2}
469+
costs.share_from_temporal = {'CO2': 0.2}
475470
pe = Effects.primary_energy()
476471
pe.maximum_total = 3.5e3
477472

0 commit comments

Comments
 (0)