From 2bf26afb78547a9707088593a21010a39162000f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:55:57 +0200 Subject: [PATCH 01/19] Fix typo --- flixopt/calculation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index dc525e147..b083abfc4 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -160,7 +160,7 @@ def summary(self): @property def active_timesteps(self) -> pd.DatetimeIndex: warnings.warn( - 'active_timesteps is deprecated. Use active_timesteps instead.', + 'active_timesteps is deprecated. Use flow_system.sel(time=...) or flow_system.isel(time=...) instead.', DeprecationWarning, stacklevel=2, ) From 037a2908def618c426beac7721aa7a83b24fdfe7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:57:54 +0200 Subject: [PATCH 02/19] Prefer robust scalar extraction for timestep sizes in aggregation --- flixopt/calculation.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index b083abfc4..33825ecf6 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -322,20 +322,15 @@ def _perform_aggregation(self): t_start_agg = timeit.default_timer() # Validation - dt_min, dt_max = ( - np.min(self.flow_system.hours_per_timestep), - np.max(self.flow_system.hours_per_timestep), - ) + dt_min = float(self.flow_system.hours_per_timestep.min().item()) + dt_max = float(self.flow_system.hours_per_timestep.max().item()) if not dt_min == dt_max: raise ValueError( f'Aggregation failed due to inconsistent time step sizes:' f'delta_t varies from {dt_min} to {dt_max} hours.' ) - steps_per_period = self.aggregation_parameters.hours_per_period / self.flow_system.hours_per_timestep.max() - is_integer = ( - self.aggregation_parameters.hours_per_period % self.flow_system.hours_per_timestep.max() - ).item() == 0 - if not (steps_per_period.size == 1 and is_integer): + is_integer = (self.aggregation_parameters.hours_per_period % dt_max) == 0 + if not is_integer: raise ValueError( f'The selected {self.aggregation_parameters.hours_per_period=} does not match the time ' f'step size of {dt_min} hours). It must be a multiple of {dt_min} hours.' From 1c43ad99368903e20ce0275c748e2274ae3ad25a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:01:41 +0200 Subject: [PATCH 03/19] Improve docs and error messages --- .../two_stage_optimization.py | 6 +++--- flixopt/components.py | 15 ++++++++------- flixopt/effects.py | 8 +++++++- flixopt/elements.py | 2 +- flixopt/flow_system.py | 2 +- flixopt/interface.py | 2 +- flixopt/modeling.py | 2 +- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index eee8dd92d..75535990d 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -1,9 +1,9 @@ """ This script demonstrates how to use downsampling of a FlowSystem to effectively reduce the size of a model. -This can be very useful when working with large models or during developement state, +This can be very useful when working with large models or during development, as it can drastically reduce the computational time. This leads to faster results and easier debugging. -A common use case is to do optimize the investments of a model with a downsampled version of the original model, and than fix the computed sizes when calculating th actual dispatch. +A common use case is to optimize the investments of a model with a downsampled version of the original model, and then fix the computed sizes when calculating the actual dispatch. While the final optimum might differ from the global optimum, the solving will be much faster. """ @@ -127,7 +127,7 @@ if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all(): logger.info('Sizes where correctly equalized') else: - raise RuntimeError('Sizes where not correctly equalized') + raise RuntimeError('Sizes were not correctly equalized') # Optimization of both flow sizes and dispatch together start = timeit.default_timer() diff --git a/flixopt/components.py b/flixopt/components.py index 4b6f3699e..dcd3fd205 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -142,11 +142,11 @@ class LinearConverter(Component): Note: Conversion factors define linear relationships where the sum of (coefficient × flow_rate) equals zero for each equation: factor1×flow1 + factor2×flow2 + ... = 0 - Conversion factors define linear relationships. - `{flow1: a1, flow2: a2, ...}` leads to `a1×flow_rate1 + a2×flow_rate2 + ... = 0` - Unfortunately the current input format doest read intuitively: - {"electricity": 1, "H2": 50} means that the electricity_in flow rate is multiplied by 1 - and the hydrogen_out flow rate is multiplied by 50. THis leads to 50 electricity --> 1 H2. + Conversion factors define linear relationships: + `{flow1: a1, flow2: a2, ...}` yields `a1×flow_rate1 + a2×flow_rate2 + ... = 0`. + Note: The input format may be unintuitive. For example, + `{"electricity": 1, "H2": 50}` implies `1×electricity = 50×H2`, + i.e., 50 units of electricity produce 1 unit of H2. The system must have fewer conversion factors than total flows (degrees of freedom > 0) to avoid over-constraining the problem. For n total flows, use at most n-1 conversion factors. @@ -200,8 +200,9 @@ def _plausibility_checks(self) -> None: for flow in self.flows.values(): if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None: logger.warning( - f'Using a FLow with a fixed size ({flow.label_full}) AND a piecewise_conversion ' - f'(in {self.label_full}) and variable size is uncommon. Please check if this is intended!' + f'Using a Flow with variable size (InvestParameters without fixed_size) ' + f'and a piecewise_conversion in {self.label_full} is uncommon. Please verify intent ' + f'({flow.label_full}).' ) def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: diff --git a/flixopt/effects.py b/flixopt/effects.py index f465dcc3b..a914d8219 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -332,7 +332,8 @@ def create_effect_values_dict( Returns ------- dict or None - A dictionary with None or Effect as the key, or None if input is None. + A dictionary keyed by effect label, or None if input is None. + Note: a standard effect must be defined when passing scalars or None labels. """ def get_effect_label(eff: Effect | str) -> str: @@ -354,6 +355,11 @@ def get_effect_label(eff: Effect | str) -> str: return None if isinstance(effect_values_user, dict): return {get_effect_label(effect): value for effect, value in effect_values_user.items()} + if self._standard_effect is None: + raise KeyError( + 'Scalar effect value provided but no standard effect is configured. ' + 'Either set an effect as is_standard=True or provide a mapping {effect_label: value}.' + ) return {self.standard_effect.label: effect_values_user} def _plausibility_checks(self) -> None: diff --git a/flixopt/elements.py b/flixopt/elements.py index e67f6bd8f..c32c795f0 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -485,7 +485,7 @@ def _plausibility_checks(self) -> None: ): raise TypeError( f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}.' - f'Different values in different years or scenarios are not yetsupported.' + f'Different values in different years or scenarios are not yet supported.' ) @property diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 5a91dc36c..78f1de41f 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -434,7 +434,7 @@ def connect_and_transform(self): self.weights = self.fit_to_model_coords('weights', self.weights, dims=['year', 'scenario']) if self.weights is not None and self.weights.sum() != 1: logger.warning( - f'Scenario weights are not normalized to 1. This is recomended for a better scaled model. ' + f'Scenario weights are not normalized to 1. This is recommended for a better scaled model. ' f'Sum of weights={self.weights.sum().item()}' ) diff --git a/flixopt/interface.py b/flixopt/interface.py index fea55b17e..d8e0dbe8f 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1166,7 +1166,7 @@ def use_consecutive_off_hours(self) -> bool: @property def use_switch_on(self) -> bool: - """Determines wether a Variable for SWITCH-ON is needed or not""" + """Determines whether a variable for SWITCH-ON is needed or not""" if self.force_switch_on: return True diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 4e368d7ae..ac3231d60 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -525,7 +525,7 @@ def scaled_bounds_with_state( List[linopy.Constraint]: List of constraint objects """ if not isinstance(model, Submodel): - raise ValueError('BoundingPatterns.active_bounds_with_state() can only be used with a Submodel') + raise ValueError('BoundingPatterns.scaled_bounds_with_state() can only be used with a Submodel') rel_lower, rel_upper = relative_bounds scaling_min, scaling_max = scaling_bounds From 3cb25dcf616bdaac6b651544f54686177ff4acd1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:02:35 +0200 Subject: [PATCH 04/19] Update examples --- examples/03_Calculation_types/example_calculation_types.py | 5 ++++- examples/05_Two-stage-optimization/two_stage_optimization.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 7248c87ad..f449485de 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -136,7 +136,10 @@ a_strom_tarif = fx.Source( 'Stromtarif', source=fx.Flow( - 'P_el', bus='Strom', size=1000, effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2: 0.3} + 'P_el', + bus='Strom', + size=1000, + effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2.label: 0.3}, ), ) diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 75535990d..7701f6056 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -124,8 +124,8 @@ calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) timer_dispatch = timeit.default_timer() - start - if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all(): - logger.info('Sizes where correctly equalized') + if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all().item(): + logger.info('Sizes were correctly equalized') else: raise RuntimeError('Sizes were not correctly equalized') From 5303f693ba7fcae7d9ca6865ebb01dac3a92c98d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:02:50 +0200 Subject: [PATCH 05/19] Use validated timesteps --- flixopt/flow_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 78f1de41f..ccded16db 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -77,7 +77,7 @@ def __init__( weights: NonTemporalDataUser | None = None, ): self.timesteps = self._validate_timesteps(timesteps) - self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) + self.timesteps_extra = self._create_timesteps_with_extra(self.timesteps, hours_of_last_timestep) self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( timesteps, hours_of_previous_timesteps ) From d66701c9ba11d72aa636cc4e77e9a42fd14a800d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:03:29 +0200 Subject: [PATCH 06/19] Remove unnessesary import --- flixopt/results.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index d8834213a..e153178be 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -292,8 +292,6 @@ def flow_system(self) -> FlowSystem: Contains all input parameters.""" if self._flow_system is None: try: - from . import FlowSystem - current_logger_level = logger.getEffectiveLevel() logger.setLevel(logging.CRITICAL) self._flow_system = FlowSystem.from_dataset(self.flow_system_data) From 7e67e43ccaacbe916c7a52531b663be9dc24e8e4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:05:10 +0200 Subject: [PATCH 07/19] Use FlowSystem.model instead of FlowSystem.submodel --- flixopt/flow_system.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index ccded16db..02b3bee3e 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -474,8 +474,8 @@ def create_model(self) -> FlowSystemModel: raise RuntimeError( 'FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.' ) - self.submodel = FlowSystemModel(self) - return self.submodel + self.model = FlowSystemModel(self) + return self.model def plot_network( self, From 22f99fe3b033bf4a6bd3a8efbc2015ae55cec951 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:05:29 +0200 Subject: [PATCH 08/19] Fix Error message --- flixopt/flow_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 02b3bee3e..bf89b3aca 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -558,7 +558,7 @@ def stop_network_app(self): ) if self._network_app is None: - logger.warning('No network app is currently running. Cant stop it') + logger.warning("No network app is currently running. Can't stop it") return try: From f05fddb83a1bb2647578d705d898749ff0eb247f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:06:56 +0200 Subject: [PATCH 09/19] Improve CHANGELOG.md --- CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d16443eeb..b73620b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,16 +30,16 @@ Please remove all irrelevant sections before releasing. Until here --> ## [Unreleased] - ????-??-?? -This Release brings Multi-year-investments and stochastic modeling to flixopt. -Further, IO methods were improved and resampling and selection of parts of the FlowSystem is now possible. +This release brings multi-year investments and stochastic modeling to flixopt. +Furthermore, I/O methods were improved, and resampling and selection of parts of the FlowSystem are now possible. Several internal improvements were made to the codebase. -#### Multi-year-investments +### Multi-year investments A flixopt model might be modeled with a "year" dimension. This enables to model transformation pathways over multiple years with several investment decisions -#### Stochastic modeling +### Stochastic modeling A flixopt model can be modeled with a scenario dimension. Scenarios can be weighted and variables can be equated across scenarios. This enables to model uncertainties in the flow system, such as: * Different demand profiles @@ -52,7 +52,7 @@ Common use cases are: The weighted sum of the total objective effect of each scenario is used as the objective of the optimization. -#### Improved Data handling: IO, resampling and more through xarray +#### Improved Data handling: I/O, resampling and more through xarray * IO for all Interfaces and the FlowSystem with round-trip serialization support * NetCDF export/import capabilities for all Interface objects and FlowSystem * JSON export for documentation purposes @@ -69,7 +69,7 @@ The weighted sum of the total objective effect of each scenario is used as the o ### Added -* FlowSystem Restoring: The used FlowSystem is now accessible directly form the results without manual restoring (lazily). All Parameters can be safely accessed anytime after the solve. +* FlowSystem restoring: The used FlowSystem is now accessible directly from the results without manual restoring (lazily). All parameters can be safely accessed anytime after the solve. * FlowResults added as a new class to store the results of Flows. They can now be accessed directly. * Added precomputed DataArrays for `size`s, `flow_rate`s and `flow_hour`s. * Added `effects_per_component()`-Dataset to Results that stores the direct (and indirect) effects of each component. This greatly improves the evaluation of the impact of individual Components, even with many and complex effects. @@ -83,7 +83,7 @@ The weighted sum of the total objective effect of each scenario is used as the o * **BREAKING**: Renamed class `SystemModel` to `FlowSystemModel` * **BREAKING**: Renamed class `Model` to `Submodel` * **BREAKING**: Renamed `mode` parameter in plotting methods to `style` -* FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent +* FlowSystems cannot be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent * Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object * Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity * Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods @@ -116,7 +116,7 @@ The weighted sum of the total objective effect of each scenario is used as the o * Added new module `.modeling`that contains Modelling primitives and utilities * Clearer separation between the main Model and "Submodels" * Improved access to the Submodels and their variables, constraints and submodels -* Added __repr__() for Submodels to easily inspect its content +* Added `__repr__()` for Submodels to easily inspect its content * Enhanced data handling methods * `fit_to_model_coords()` method for data alignment * `fit_effects_to_model_coords()` method for effect data processing From 76beada51d8b28a0b28c668ba39d20948f492a12 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:08:47 +0200 Subject: [PATCH 10/19] Use self.standard_effect instead of provate self._standard_effect and update docstring --- flixopt/effects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index a914d8219..9866f24ee 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -325,7 +325,7 @@ def create_effect_values_dict( Examples -------- - effect_values_user = 20 -> {None: 20} + effect_values_user = 20 -> {'': 20} effect_values_user = None -> None effect_values_user = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3} @@ -355,7 +355,7 @@ def get_effect_label(eff: Effect | str) -> str: return None if isinstance(effect_values_user, dict): return {get_effect_label(effect): value for effect, value in effect_values_user.items()} - if self._standard_effect is None: + if self.standard_effect is None: raise KeyError( 'Scalar effect value provided but no standard effect is configured. ' 'Either set an effect as is_standard=True or provide a mapping {effect_label: value}.' From da2efccc1ed4747b7b91367ca558b18dd1858b92 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:09:56 +0200 Subject: [PATCH 11/19] in calculate_all_conversion_paths, use `collections.deque` for efficiency on large graphs --- flixopt/effects.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 9866f24ee..e460e5d23 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -9,6 +9,7 @@ import logging import warnings +from collections import deque from collections.abc import Iterator from typing import TYPE_CHECKING, Literal @@ -570,10 +571,10 @@ def calculate_all_conversion_paths( # Keep track of visited paths to avoid repeating calculations processed_paths = set() # Use a queue with (current_domain, factor, path_history) - queue = [(origin, 1, [origin])] + queue = deque([(origin, 1, [origin])]) while queue: - current_domain, factor, path = queue.pop(0) + current_domain, factor, path = queue.popleft() # Skip if we've processed this exact path before path_key = tuple(path) From 783f8339d11d8a2d73030a4404e5008ab02e3eff Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:30:52 +0200 Subject: [PATCH 12/19] Make aggregation_parameters.hours_per_period more robust by using rounding --- flixopt/calculation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 33825ecf6..966d31e1e 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -329,11 +329,11 @@ def _perform_aggregation(self): f'Aggregation failed due to inconsistent time step sizes:' f'delta_t varies from {dt_min} to {dt_max} hours.' ) - is_integer = (self.aggregation_parameters.hours_per_period % dt_max) == 0 - if not is_integer: + ratio = self.aggregation_parameters.hours_per_period / dt_max + if not np.isclose(ratio, round(ratio), atol=1e-9): raise ValueError( f'The selected {self.aggregation_parameters.hours_per_period=} does not match the time ' - f'step size of {dt_min} hours). It must be a multiple of {dt_min} hours.' + f'step size of {dt_max} hours. It must be an integer multiple of {dt_max} hours.' ) logger.info(f'{"":#^80}') From 60735501ab8709462565cced1f19bb670865e634 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:31:30 +0200 Subject: [PATCH 13/19] Improve import and typos --- flixopt/effects.py | 1 - flixopt/interface.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index e460e5d23..1b5a292a1 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -10,7 +10,6 @@ import logging import warnings from collections import deque -from collections.abc import Iterator from typing import TYPE_CHECKING, Literal import linopy diff --git a/flixopt/interface.py b/flixopt/interface.py index d8e0dbe8f..da456efee 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -420,7 +420,7 @@ class PiecewiseConversion(Interface): def __init__(self, piecewises: dict[str, Piecewise]): self.piecewises = piecewises self._has_time_dim = True - self.has_time_dim = True # Inital propagation + self.has_time_dim = True # Initial propagation @property def has_time_dim(self): @@ -640,7 +640,7 @@ def __init__(self, piecewise_origin: Piecewise, piecewise_shares: dict[str, Piec self.piecewise_origin = piecewise_origin self.piecewise_shares = piecewise_shares self._has_time_dim = False - self.has_time_dim = False # Inital propagation + self.has_time_dim = False # Initial propagation @property def has_time_dim(self): @@ -1166,7 +1166,7 @@ def use_consecutive_off_hours(self) -> bool: @property def use_switch_on(self) -> bool: - """Determines whether a variable for SWITCH-ON is needed or not""" + """Determines whether a variable for switch_on is needed or not""" if self.force_switch_on: return True From 0f7f0989a0513da3a283c336c6a426993f35b658 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:31:42 +0200 Subject: [PATCH 14/19] Improve docstring --- flixopt/effects.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 1b5a292a1..9a43aca3f 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -325,9 +325,10 @@ def create_effect_values_dict( Examples -------- - effect_values_user = 20 -> {'': 20} - effect_values_user = None -> None - effect_values_user = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3} + effect_values_user = 20 -> {'': 20} + effect_values_user = {None: 20} -> {'': 20} + effect_values_user = None -> None + effect_values_user = {'effect1': 20, 'effect2': 0.3} -> {'effect1': 20, 'effect2': 0.3} Returns ------- From adee836ac1ce79712baa624296d41658e48137c0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:31:53 +0200 Subject: [PATCH 15/19] Use validated timesteps --- flixopt/flow_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index bf89b3aca..dc06d106f 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -79,7 +79,7 @@ def __init__( self.timesteps = self._validate_timesteps(timesteps) self.timesteps_extra = self._create_timesteps_with_extra(self.timesteps, hours_of_last_timestep) self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( - timesteps, hours_of_previous_timesteps + self.timesteps, hours_of_previous_timesteps ) self.years_of_last_year = years_of_last_year From c9af91dd8ae12182a4cd3824c042bbf4ac15d3e3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:34:40 +0200 Subject: [PATCH 16/19] Improve error --- flixopt/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index c32c795f0..dde1c3379 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -484,7 +484,7 @@ def _plausibility_checks(self) -> None: ] ): raise TypeError( - f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}.' + f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}. ' f'Different values in different years or scenarios are not yet supported.' ) From db2f78e7c0d8f17aaa2173395041b28d55038a56 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:34:51 +0200 Subject: [PATCH 17/19] Improve warning --- flixopt/flow_system.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index dc06d106f..09781ae3f 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -432,11 +432,13 @@ def connect_and_transform(self): return self.weights = self.fit_to_model_coords('weights', self.weights, dims=['year', 'scenario']) - if self.weights is not None and self.weights.sum() != 1: - logger.warning( - f'Scenario weights are not normalized to 1. This is recommended for a better scaled model. ' - f'Sum of weights={self.weights.sum().item()}' - ) + if self.weights is not None: + total = float(self.weights.sum().item()) + if not np.isclose(total, 1.0, atol=1e-12): + logger.warning( + 'Scenario weights are not normalized to 1. Normalizing to 1 is recommended for a better scaled model. ' + f'Sum of weights={total}' + ) self._connect_network() for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): From 68aeccbde93bb87266bb2ec05a1e6ef5032fa8b7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:50:32 +0200 Subject: [PATCH 18/19] Improve type hint --- flixopt/effects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 9a43aca3f..c6c6cc374 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -539,7 +539,7 @@ def _add_share_between_effects(self): def calculate_all_conversion_paths( - conversion_dict: dict[str, dict[str, xr.DataArray]], + conversion_dict: dict[str, dict[str, Scalar | xr.DataArray]], ) -> dict[tuple[str, str], xr.DataArray]: """ Calculates all possible direct and indirect conversion factors between units/domains. From 814e475a10f254737ccb0e21ce4ae10983ad9f73 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:51:48 +0200 Subject: [PATCH 19/19] Improve CHANGELOG.md: typos, wording and duplicate entries --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b73620b15..f6097e7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,11 +37,11 @@ Several internal improvements were made to the codebase. ### Multi-year investments A flixopt model might be modeled with a "year" dimension. -This enables to model transformation pathways over multiple years with several investment decisions +This enables modeling transformation pathways over multiple years with several investment decisions ### Stochastic modeling A flixopt model can be modeled with a scenario dimension. -Scenarios can be weighted and variables can be equated across scenarios. This enables to model uncertainties in the flow system, such as: +Scenarios can be weighted and variables can be equated across scenarios. This enables modeling uncertainties in the flow system, such as: * Different demand profiles * Different price forecasts * Different weather conditions @@ -109,8 +109,6 @@ The weighted sum of the total objective effect of each scenario is used as the o ### *Development* * **BREAKING**: Calculation.do_modeling() now returns the Calculation object instead of its linopy.Model -* **BREAKING**: Renamed class `SystemModel` to `FlowSystemModel` -* **BREAKING**: Renamed class `Model` to `Submodel` * FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties * Change modeling hierarchy to allow for more flexibility in future development. This leads to minimal changes in the access and creation of Submodels and their variables. * Added new module `.modeling`that contains Modelling primitives and utilities @@ -179,7 +177,7 @@ There are no changes or new features. ## [2.1.6] - 2025-09-02 ### Changed -- `Sink`, `Source` and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables to model more use cases using these classes. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] +- `Sink`, `Source` and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables modeling more use cases using these classes. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] - Further, both `Sink` and `Source` now have a `prevent_simultaneous_flow_rates` argument to prevent simultaneous flow rates of more than one of their Flows. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] ### Added