From 06d90bfd295e057e2cacb84879fe1c9c877447f5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:00:15 +0200 Subject: [PATCH 1/8] Step 1 --- examples/01_Simple/simple_example.py | 2 +- examples/02_Complex/complex_example.py | 4 +- examples/04_Scenarios/scenario_example.py | 2 +- flixopt/effects.py | 78 +++++++++++------------ tests/conftest.py | 3 +- tests/test_effect.py | 46 +++++++++---- tests/test_scenarios.py | 4 +- 7 files changed, 78 insertions(+), 61 deletions(-) diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index a201ee8c4..62fa8f6a9 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -29,6 +29,7 @@ description='Kosten', is_standard=True, # standard effect: no explicit value needed for costs is_objective=True, # Minimizing costs as the optimization objective + share_from_temporal={'CO2': 0.2}, ) # CO2 emissions effect with an associated cost impact @@ -36,7 +37,6 @@ label='CO2', unit='kg', description='CO2_e-Emissionen', - specific_share_to_other_effects_operation={costs.label: 0.2}, maximum_operation_per_hour=1000, # Max CO2 emissions per hour ) diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 175211c26..506e7ac78 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -40,8 +40,8 @@ # --- Define Effects --- # Specify effects related to costs, CO2 emissions, and primary energy consumption - Costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) - CO2 = fx.Effect('CO2', 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={Costs.label: 0.2}) + Costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2}) + CO2 = fx.Effect('CO2', 'kg', 'CO2_e-Emissionen') PE = fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3) # --- Define Components --- diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index f675211e8..73d37fb90 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -35,6 +35,7 @@ description='Kosten', is_standard=True, # standard effect: no explicit value needed for costs is_objective=True, # Minimizing costs as the optimization objective + share_from_temporal={'CO2': 0.2}, ) # CO2 emissions effect with an associated cost impact @@ -42,7 +43,6 @@ label='CO2', unit='kg', description='CO2_e-Emissionen', - specific_share_to_other_effects_operation={costs.label: 0.2}, maximum_operation_per_hour=1000, # Max CO2 emissions per hour ) diff --git a/flixopt/effects.py b/flixopt/effects.py index e1bf0b17b..d04b388fe 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -50,10 +50,10 @@ class Effect(Element): without effect dictionaries. Used for simplified effect specification (and less boilerplate code). is_objective: If True, this effect serves as the optimization objective function. Only one effect can be marked as objective per optimization. - specific_share_to_other_effects_operation: Operational cross-effect contributions. - Maps this effect's operational values to contributions to other effects - specific_share_to_other_effects_invest: Investment cross-effect contributions. - Maps this effect's investment values to contributions to other effects. + share_from_temporal: Temporal cross-effect contributions. + Maps temporal contributions from other effects to this effect + share_from_nontemporal: Nontemporal cross-effect contributions. + Maps nontemporal contributions from other effects to this effect. minimum_temporal: Minimum allowed total contribution across all timesteps. maximum_temporal: Maximum allowed total contribution across all timesteps. minimum_per_hour: Minimum allowed contribution per hour. @@ -87,7 +87,7 @@ class Effect(Element): label='co2_emissions', unit='kg_CO2', description='Carbon dioxide emissions', - specific_share_to_other_effects_operation={'costs': 50}, # €50/t_CO2 + share_from_temporal={'CO2': 0.2}, # €0.2 per kg CO2 maximum_total=1_000_000, # 1000 t CO2 annual limit ) ``` @@ -110,7 +110,7 @@ class Effect(Element): label='primary_energy', unit='kWh_primary', description='Primary energy consumption', - specific_share_to_other_effects_operation={'costs': 0.08}, # €0.08/kWh + share_from_temporal={'primary_energy': 0.08}, # €0.08/kWh ) ``` @@ -150,8 +150,8 @@ def __init__( meta_data: dict | None = None, is_standard: bool = False, is_objective: bool = False, - specific_share_to_other_effects_operation: TemporalEffectsUser | None = None, - specific_share_to_other_effects_invest: NonTemporalEffectsUser | None = None, + share_from_temporal: TemporalEffectsUser | None = None, + share_from_nontemporal: NonTemporalEffectsUser | None = None, minimum_temporal: NonTemporalEffectsUser | None = None, maximum_temporal: NonTemporalEffectsUser | None = None, minimum_nontemporal: NonTemporalEffectsUser | None = None, @@ -168,11 +168,9 @@ def __init__( self.description = description self.is_standard = is_standard self.is_objective = is_objective - self.specific_share_to_other_effects_operation: TemporalEffectsUser = ( - specific_share_to_other_effects_operation if specific_share_to_other_effects_operation is not None else {} - ) - self.specific_share_to_other_effects_invest: NonTemporalEffectsUser = ( - specific_share_to_other_effects_invest if specific_share_to_other_effects_invest is not None else {} + self.share_from_temporal: TemporalEffectsUser = share_from_temporal if share_from_temporal is not None else {} + self.share_from_nontemporal: NonTemporalEffectsUser = ( + share_from_nontemporal if share_from_nontemporal is not None else {} ) # Handle backwards compatibility for deprecated parameters @@ -394,8 +392,8 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.maximum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|maximum_per_hour', self.maximum_per_hour) - self.specific_share_to_other_effects_operation = flow_system.fit_effects_to_model_coords( - f'{prefix}|operation->', self.specific_share_to_other_effects_operation, 'temporal' + self.share_from_temporal = flow_system.fit_effects_to_model_coords( + f'{prefix}|temporal<-', self.share_from_temporal, 'temporal' ) self.minimum_temporal = flow_system.fit_to_model_coords( @@ -407,7 +405,7 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.minimum_nontemporal = flow_system.fit_to_model_coords( f'{prefix}|minimum_nontemporal', self.minimum_nontemporal, dims=['year', 'scenario'] ) - self.minimum_nontemporal = flow_system.fit_to_model_coords( + self.maximum_nontemporal = flow_system.fit_to_model_coords( f'{prefix}|maximum_nontemporal', self.maximum_nontemporal, dims=['year', 'scenario'] ) self.minimum_total = flow_system.fit_to_model_coords( @@ -418,10 +416,10 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.maximum_total = flow_system.fit_to_model_coords( f'{prefix}|maximum_total', self.maximum_total, dims=['year', 'scenario'] ) - self.specific_share_to_other_effects_invest = flow_system.fit_effects_to_model_coords( - f'{prefix}|operation->', - self.specific_share_to_other_effects_invest, - 'operation', + self.share_from_nontemporal = flow_system.fit_effects_to_model_coords( + f'{prefix}|nontemporal<-', + self.share_from_nontemporal, + 'nontemporal', dims=['year', 'scenario'], ) @@ -659,18 +657,20 @@ def calculate_effect_share_factors( ]: shares_nontemporal = {} for name, effect in self.effects.items(): - if effect.specific_share_to_other_effects_invest: - shares_nontemporal[name] = { - target: data for target, data in effect.specific_share_to_other_effects_invest.items() - } + if effect.share_from_nontemporal: + for source, data in effect.share_from_nontemporal.items(): + if source not in shares_nontemporal: + shares_nontemporal[source] = {} + shares_nontemporal[source][name] = data shares_nontemporal = calculate_all_conversion_paths(shares_nontemporal) shares_temporal = {} for name, effect in self.effects.items(): - if effect.specific_share_to_other_effects_operation: - shares_temporal[name] = { - target: data for target, data in effect.specific_share_to_other_effects_operation.items() - } + if effect.share_from_temporal: + for source, data in effect.share_from_temporal.items(): + if source not in shares_temporal: + shares_temporal[source] = {} + shares_temporal[source][name] = data shares_temporal = calculate_all_conversion_paths(shares_temporal) return shares_temporal, shares_nontemporal @@ -729,19 +729,19 @@ def _do_modeling(self): ) def _add_share_between_effects(self): - for origin_effect in self.effects: - # 1. temporal: -> hier sind es Zeitreihen (share_TS) - for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items(): - self.effects[target_effect].submodel.temporal.add_share( - origin_effect.submodel.temporal.label_full, - origin_effect.submodel.temporal.total_per_timestep * time_series, + for target_effect in self.effects: + # 1. temporal: <- receiving temporal shares from other effects + for source_effect, time_series in target_effect.share_from_temporal.items(): + target_effect.submodel.temporal.add_share( + self.effects[source_effect].submodel.temporal.label_full, + self.effects[source_effect].submodel.temporal.total_per_timestep * time_series, dims=('time', 'year', 'scenario'), ) - # 2. nontemporal: -> hier ist es Scalar (share) - for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): - self.effects[target_effect].submodel.nontemporal.add_share( - origin_effect.submodel.nontemporal.label_full, - origin_effect.submodel.nontemporal.total * factor, + # 2. nontemporal: <- receiving nontemporal shares from other effects + for source_effect, factor in target_effect.share_from_nontemporal.items(): + target_effect.submodel.nontemporal.add_share( + self.effects[source_effect].submodel.nontemporal.label_full, + self.effects[source_effect].submodel.nontemporal.total * factor, dims=('year', 'scenario'), ) diff --git a/tests/conftest.py b/tests/conftest.py index cc70ecc17..034d9f789 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -109,7 +109,6 @@ def co2_with_costs_share(): 'CO2', 'kg', 'CO2_e-Emissionen', - specific_share_to_other_effects_operation={'costs': 0.2}, ) @staticmethod @@ -471,7 +470,7 @@ def flow_system_complex() -> fx.FlowSystem: # Define the components and flow_system costs = Effects.costs() co2 = Effects.co2() - co2.specific_share_to_other_effects_operation = {'costs': 0.2} + costs.share_from_temporal = {'CO2': 0.2} pe = Effects.primary_energy() pe.maximum_total = 3.5e3 diff --git a/tests/test_effect.py b/tests/test_effect.py index e0b737771..48e057419 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -154,11 +154,21 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): 'Effect1', '€', 'Testing Effect', - specific_share_to_other_effects_operation={'Effect2': 1.1, 'Effect3': 1.2}, - specific_share_to_other_effects_invest={'Effect2': 2.1, 'Effect3': 2.2}, ) - effect2 = fx.Effect('Effect2', '€', 'Testing Effect') - effect3 = fx.Effect('Effect3', '€', 'Testing Effect') + effect2 = fx.Effect( + 'Effect2', + '€', + 'Testing Effect', + share_from_temporal={'Effect1': 1.1}, + share_from_nontemporal={'Effect1': 2.1}, + ) + effect3 = fx.Effect( + 'Effect3', + '€', + 'Testing Effect', + share_from_temporal={'Effect1': 1.2}, + share_from_nontemporal={'Effect1': 2.2}, + ) flow_system.add_elements(effect1, effect2, effect3) model = create_linopy_model(flow_system) @@ -215,17 +225,25 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): class TestEffectResults: def test_shares(self, basic_flow_system_linopy_coords, coords_config): flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - flow_system.effects['costs'].specific_share_to_other_effects_operation['Effect1'] = 0.5 + effect1 = fx.Effect('Effect1', '€', 'Testing Effect', share_from_temporal={'costs': 0.5}) + effect2 = fx.Effect( + 'Effect2', + '€', + 'Testing Effect', + share_from_temporal={'Effect1': 1.1}, + share_from_nontemporal={'Effect1': 2.1}, + ) + effect3 = fx.Effect( + 'Effect3', + '€', + 'Testing Effect', + share_from_temporal={'Effect1': 1.2, 'Effect2': 5}, + share_from_nontemporal={'Effect1': 2.2}, + ) flow_system.add_elements( - fx.Effect( - 'Effect1', - '€', - 'Testing Effect', - specific_share_to_other_effects_operation={'Effect2': 1.1, 'Effect3': 1.2}, - specific_share_to_other_effects_invest={'Effect2': 2.1, 'Effect3': 2.2}, - ), - fx.Effect('Effect2', '€', 'Testing Effect', specific_share_to_other_effects_operation={'Effect3': 5}), - fx.Effect('Effect3', '€', 'Testing Effect'), + effect1, + effect2, + effect3, fx.linear_converters.Boiler( 'Boiler', eta=0.5, diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index ebc633a04..bfc537b8e 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -124,8 +124,8 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: ) # Define the components and flow_system flow_system.add_elements( - fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Effect('CO2', 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={'costs': 0.2}), + fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2}), + fx.Effect('CO2', 'kg', 'CO2_e-Emissionen'), fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3), fx.Bus('Strom'), fx.Bus('Fernwärme'), From 0795fc8dc164acf2797b9b0db98e0f2b094b4f1c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:17:56 +0200 Subject: [PATCH 2/8] Bugfix --- flixopt/effects.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index d04b388fe..0e7457bdf 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -393,7 +393,10 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.maximum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|maximum_per_hour', self.maximum_per_hour) self.share_from_temporal = flow_system.fit_effects_to_model_coords( - f'{prefix}|temporal<-', self.share_from_temporal, 'temporal' + f'{prefix}(temporal)->', self.share_from_temporal, '(temporal)' + ) + self.share_from_nontemporal = flow_system.fit_effects_to_model_coords( + f'{prefix}(nontemporal)->', self.share_from_nontemporal, '(nontemporal)' ) self.minimum_temporal = flow_system.fit_to_model_coords( @@ -416,12 +419,6 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.maximum_total = flow_system.fit_to_model_coords( f'{prefix}|maximum_total', self.maximum_total, dims=['year', 'scenario'] ) - self.share_from_nontemporal = flow_system.fit_effects_to_model_coords( - f'{prefix}|nontemporal<-', - self.share_from_nontemporal, - 'nontemporal', - dims=['year', 'scenario'], - ) def create_model(self, model: FlowSystemModel) -> EffectModel: self._plausibility_checks() From 9e23bfb133df26d0a91a301b652fe81ffbd290a6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:21:07 +0200 Subject: [PATCH 3/8] Make fit_effects_to_model_coords() more flexible --- flixopt/effects.py | 6 ++++-- flixopt/flow_system.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 0e7457bdf..9620da838 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -393,10 +393,12 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.maximum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|maximum_per_hour', self.maximum_per_hour) self.share_from_temporal = flow_system.fit_effects_to_model_coords( - f'{prefix}(temporal)->', self.share_from_temporal, '(temporal)' + label_prefix=None, effect_values=self.share_from_temporal, label_suffix=f'(temporal)->{prefix}(temporal)' ) self.share_from_nontemporal = flow_system.fit_effects_to_model_coords( - f'{prefix}(nontemporal)->', self.share_from_nontemporal, '(nontemporal)' + label_prefix=None, + effect_values=self.share_from_nontemporal, + label_suffix=f'(nontemporal)->{prefix}(nontemporal)', ) self.minimum_temporal = flow_system.fit_to_model_coords( diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 09781ae3f..ffd9761d7 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -407,6 +407,7 @@ def fit_effects_to_model_coords( effect_values: TemporalEffectsUser | NonTemporalEffectsUser | None, label_suffix: str | None = None, dims: Collection[FlowSystemDimensions] | None = None, + delimiter: str = '|', ) -> TemporalEffects | NonTemporalEffects | None: """ Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. @@ -418,7 +419,7 @@ def fit_effects_to_model_coords( return { effect: self.fit_to_model_coords( - '|'.join(filter(None, [label_prefix, effect, label_suffix])), + str(delimiter).join(filter(None, [label_prefix, effect, label_suffix])), value, dims=dims, ) From dfb60b5f11595f127926b13987581061ed1a4243 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:23:12 +0200 Subject: [PATCH 4/8] Fix dims --- flixopt/effects.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 9620da838..579fa095a 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -393,12 +393,16 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.maximum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|maximum_per_hour', self.maximum_per_hour) self.share_from_temporal = flow_system.fit_effects_to_model_coords( - label_prefix=None, effect_values=self.share_from_temporal, label_suffix=f'(temporal)->{prefix}(temporal)' + label_prefix=None, + effect_values=self.share_from_temporal, + label_suffix=f'(temporal)->{prefix}(temporal)', + dims=['time', 'year', 'scenario'], ) self.share_from_nontemporal = flow_system.fit_effects_to_model_coords( label_prefix=None, effect_values=self.share_from_nontemporal, label_suffix=f'(nontemporal)->{prefix}(nontemporal)', + dims=['year', 'scenario'], ) self.minimum_temporal = flow_system.fit_to_model_coords( From 1d779893d126154d1e56f21c9b8ae32016b4b382 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:31:52 +0200 Subject: [PATCH 5/8] Update conftest.py --- CHANGELOG.md | 16 ++++++++++++++++ tests/conftest.py | 20 ++++++++------------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ecf091eb..2c92c268f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,18 @@ Several internal improvements were made to the codebase. ### ✨ Added +**Intuitive effect share syntax:** +Effects now support an intuitive `share_from_*` syntax for cross-effect relationships: +```python +costs = fx.Effect( + 'costs', '€', 'Total costs', + is_standard=True, is_objective=True, + share_from_temporal={'CO2': 0.2, 'energy': 0.05}, # Costs receive contributions from other effects + share_from_nontemporal={'land': 100} # €100 per m² land use +) +``` +This replaces the less intuitive `specific_share_to_other_effects_*` parameters and makes it clearer where effect contributions are coming from, rather then where they are going to. + **Multi-year investments:** A flixopt model might be modeled with a "year" dimension. This enables modeling transformation pathways over multiple years with several investment decisions @@ -116,6 +128,10 @@ The weighted sum of the total objective effect of each scenario is used as the o - `minimum_operation_per_hour` → `minimum_per_hour` - `maximum_operation_per_hour` → `maximum_per_hour` +### 🔥 Removed +* **Effect share parameters**: The old `specific_share_to_other_effects_*` parameters were replaced WITHOUT DEPRECTATION + - `specific_share_to_other_effects_operation` → `share_from_temporal` (with inverted direction) + - `specific_share_to_other_effects_invest` → `share_from_nontemporal` (with inverted direction) ### 🐛 Fixed * Enhanced NetCDF I/O with proper attribute preservation for DataArrays diff --git a/tests/conftest.py b/tests/conftest.py index 034d9f789..1b0f23a29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,16 +100,12 @@ def costs(): return fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) @staticmethod - def co2(): - return fx.Effect('CO2', 'kg', 'CO2_e-Emissionen') + def costs_with_co2_share(): + return fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2}) @staticmethod - def co2_with_costs_share(): - return fx.Effect( - 'CO2', - 'kg', - 'CO2_e-Emissionen', - ) + def co2(): + return fx.Effect('CO2', 'kg', 'CO2_e-Emissionen') @staticmethod def primary_energy(): @@ -387,8 +383,8 @@ def simple_flow_system() -> fx.FlowSystem: base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time') # Define effects - costs = Effects.costs() - co2 = Effects.co2_with_costs_share() + costs = Effects.costs_with_co2_share() + co2 = Effects.co2() co2.maximum_operation_per_hour = 1000 # Create components @@ -417,8 +413,8 @@ def simple_flow_system_scenarios() -> fx.FlowSystem: base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time') # Define effects - costs = Effects.costs() - co2 = Effects.co2_with_costs_share() + costs = Effects.costs_with_co2_share() + co2 = Effects.co2() co2.maximum_operation_per_hour = 1000 # Create components From 9bac31b0a4cff4a6f18f6eb4dd16356cd8318dca Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 27 Sep 2025 18:12:33 +0200 Subject: [PATCH 6/8] Typos --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c92c268f..e3e1c4414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,7 +57,7 @@ costs = fx.Effect( share_from_nontemporal={'land': 100} # €100 per m² land use ) ``` -This replaces the less intuitive `specific_share_to_other_effects_*` parameters and makes it clearer where effect contributions are coming from, rather then where they are going to. +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. **Multi-year investments:** A flixopt model might be modeled with a "year" dimension. @@ -129,7 +129,7 @@ The weighted sum of the total objective effect of each scenario is used as the o - `maximum_operation_per_hour` → `maximum_per_hour` ### 🔥 Removed -* **Effect share parameters**: The old `specific_share_to_other_effects_*` parameters were replaced WITHOUT DEPRECTATION +* **Effect share parameters**: The old `specific_share_to_other_effects_*` parameters were replaced WITHOUT DEPRECATION - `specific_share_to_other_effects_operation` → `share_from_temporal` (with inverted direction) - `specific_share_to_other_effects_invest` → `share_from_nontemporal` (with inverted direction) From 39cda0322aaffd2d34db98ca3081330e7ef73ed4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 27 Sep 2025 18:19:19 +0200 Subject: [PATCH 7/8] Improve Effect examples --- flixopt/effects.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 579fa095a..7ec275a9b 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -77,17 +77,21 @@ class Effect(Element): Basic cost objective: ```python - cost_effect = Effect(label='system_costs', unit='€', description='Total system costs', is_objective=True) + cost_effect = Effect( + label='system_costs', + unit='€', + description='Total system costs', + is_objective=True, + ) ``` - CO2 emissions with carbon pricing: + CO2 emissions: ```python co2_effect = Effect( - label='co2_emissions', + label='CO2', unit='kg_CO2', description='Carbon dioxide emissions', - share_from_temporal={'CO2': 0.2}, # €0.2 per kg CO2 maximum_total=1_000_000, # 1000 t CO2 annual limit ) ``` @@ -110,7 +114,21 @@ class Effect(Element): label='primary_energy', unit='kWh_primary', description='Primary energy consumption', - share_from_temporal={'primary_energy': 0.08}, # €0.08/kWh + ) + ``` + + Cost objective with carbon and primary energy pricing: + + ```python + cost_effect = Effect( + label='system_costs', + unit='€', + description='Total system costs', + is_objective=True, + share_from_temporal={ + 'primary_energy': 0.08, # 0.08 €/kWh_primary + 'CO2': 0.2, # Carbon pricing: 0.2 €/kg_CO2 into costs if used on a cost effect + }, ) ``` @@ -137,8 +155,7 @@ class Effect(Element): across all contributions to each effect manually. Effects are accumulated as: - - Total = Σ(operational contributions) + Σ(investment contributions) - - Cross-effects add to target effects based on specific_share ratios + - Total = Σ(temporal contributions) + Σ(nontemporal contributions) """ From 025441629d1b5d56cabcaffa6954c8340d2e6618 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 27 Sep 2025 18:20:57 +0200 Subject: [PATCH 8/8] Add extra validation for Effect Shares --- flixopt/effects.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flixopt/effects.py b/flixopt/effects.py index 7ec275a9b..356bf7e93 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -593,6 +593,11 @@ def _plausibility_checks(self) -> None: # Check circular loops in effects: temporal, nontemporal = self.calculate_effect_share_factors() + # Validate all referenced sources exist + unknown = {src for src, _ in list(temporal.keys()) + list(nontemporal.keys()) if src not in self.effects} + if unknown: + raise KeyError(f'Unknown effects used in in effect share mappings: {sorted(unknown)}') + temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal])) nontemporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in nontemporal]))