Skip to content

Commit f1bd392

Browse files
authored
Feature/v3/low-impact-improvements (#355)
* Fix typo * Prefer robust scalar extraction for timestep sizes in aggregation * Improve docs and error messages * Update examples * Use validated timesteps * Remove unnessesary import * Use FlowSystem.model instead of FlowSystem.submodel * Fix Error message * Improve CHANGELOG.md * Use self.standard_effect instead of provate self._standard_effect and update docstring * in calculate_all_conversion_paths, use `collections.deque` for efficiency on large graphs * Make aggregation_parameters.hours_per_period more robust by using rounding * Improve import and typos * Improve docstring * Use validated timesteps * Improve error * Improve warning * Improve type hint * Improve CHANGELOG.md: typos, wording and duplicate entries
1 parent acae94e commit f1bd392

11 files changed

Lines changed: 67 additions & 63 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@ Please remove all irrelevant sections before releasing.
3030
Until here -->
3131

3232
## [Unreleased] - ????-??-??
33-
This Release brings Multi-year-investments and stochastic modeling to flixopt.
34-
Further, IO methods were improved and resampling and selection of parts of the FlowSystem is now possible.
33+
This release brings multi-year investments and stochastic modeling to flixopt.
34+
Furthermore, I/O methods were improved, and resampling and selection of parts of the FlowSystem are now possible.
3535
Several internal improvements were made to the codebase.
3636

3737

38-
#### Multi-year-investments
38+
### Multi-year investments
3939
A flixopt model might be modeled with a "year" dimension.
40-
This enables to model transformation pathways over multiple years with several investment decisions
40+
This enables modeling transformation pathways over multiple years with several investment decisions
4141

42-
#### Stochastic modeling
42+
### Stochastic modeling
4343
A flixopt model can be modeled with a scenario dimension.
44-
Scenarios can be weighted and variables can be equated across scenarios. This enables to model uncertainties in the flow system, such as:
44+
Scenarios can be weighted and variables can be equated across scenarios. This enables modeling uncertainties in the flow system, such as:
4545
* Different demand profiles
4646
* Different price forecasts
4747
* Different weather conditions
@@ -52,7 +52,7 @@ Common use cases are:
5252

5353
The weighted sum of the total objective effect of each scenario is used as the objective of the optimization.
5454

55-
#### Improved Data handling: IO, resampling and more through xarray
55+
#### Improved Data handling: I/O, resampling and more through xarray
5656
* IO for all Interfaces and the FlowSystem with round-trip serialization support
5757
* NetCDF export/import capabilities for all Interface objects and FlowSystem
5858
* 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
6969

7070

7171
### Added
72-
* 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.
72+
* 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.
7373
* FlowResults added as a new class to store the results of Flows. They can now be accessed directly.
7474
* Added precomputed DataArrays for `size`s, `flow_rate`s and `flow_hour`s.
7575
* 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
8383
* **BREAKING**: Renamed class `SystemModel` to `FlowSystemModel`
8484
* **BREAKING**: Renamed class `Model` to `Submodel`
8585
* **BREAKING**: Renamed `mode` parameter in plotting methods to `style`
86-
* FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent
86+
* FlowSystems cannot be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent
8787
* Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object
8888
* Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity
8989
* Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods
@@ -109,14 +109,12 @@ The weighted sum of the total objective effect of each scenario is used as the o
109109

110110
### *Development*
111111
* **BREAKING**: Calculation.do_modeling() now returns the Calculation object instead of its linopy.Model
112-
* **BREAKING**: Renamed class `SystemModel` to `FlowSystemModel`
113-
* **BREAKING**: Renamed class `Model` to `Submodel`
114112
* FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties
115113
* 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.
116114
* Added new module `.modeling`that contains Modelling primitives and utilities
117115
* Clearer separation between the main Model and "Submodels"
118116
* Improved access to the Submodels and their variables, constraints and submodels
119-
* Added __repr__() for Submodels to easily inspect its content
117+
* Added `__repr__()` for Submodels to easily inspect its content
120118
* Enhanced data handling methods
121119
* `fit_to_model_coords()` method for data alignment
122120
* `fit_effects_to_model_coords()` method for effect data processing
@@ -179,7 +177,7 @@ There are no changes or new features.
179177
## [2.1.6] - 2025-09-02
180178

181179
### Changed
182-
- `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)]
180+
- `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)]
183181
- 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)]
184182

185183
### Added

examples/03_Calculation_types/example_calculation_types.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@
136136
a_strom_tarif = fx.Source(
137137
'Stromtarif',
138138
source=fx.Flow(
139-
'P_el', bus='Strom', size=1000, effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2: 0.3}
139+
'P_el',
140+
bus='Strom',
141+
size=1000,
142+
effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2.label: 0.3},
140143
),
141144
)
142145

examples/05_Two-stage-optimization/two_stage_optimization.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""
22
This script demonstrates how to use downsampling of a FlowSystem to effectively reduce the size of a model.
3-
This can be very useful when working with large models or during developement state,
3+
This can be very useful when working with large models or during development,
44
as it can drastically reduce the computational time.
55
This leads to faster results and easier debugging.
6-
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.
6+
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.
77
While the final optimum might differ from the global optimum, the solving will be much faster.
88
"""
99

@@ -124,10 +124,10 @@
124124
calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 600))
125125
timer_dispatch = timeit.default_timer() - start
126126

127-
if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all():
128-
logger.info('Sizes where correctly equalized')
127+
if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all().item():
128+
logger.info('Sizes were correctly equalized')
129129
else:
130-
raise RuntimeError('Sizes where not correctly equalized')
130+
raise RuntimeError('Sizes were not correctly equalized')
131131

132132
# Optimization of both flow sizes and dispatch together
133133
start = timeit.default_timer()

flixopt/calculation.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def summary(self):
160160
@property
161161
def active_timesteps(self) -> pd.DatetimeIndex:
162162
warnings.warn(
163-
'active_timesteps is deprecated. Use active_timesteps instead.',
163+
'active_timesteps is deprecated. Use flow_system.sel(time=...) or flow_system.isel(time=...) instead.',
164164
DeprecationWarning,
165165
stacklevel=2,
166166
)
@@ -322,23 +322,18 @@ def _perform_aggregation(self):
322322
t_start_agg = timeit.default_timer()
323323

324324
# Validation
325-
dt_min, dt_max = (
326-
np.min(self.flow_system.hours_per_timestep),
327-
np.max(self.flow_system.hours_per_timestep),
328-
)
325+
dt_min = float(self.flow_system.hours_per_timestep.min().item())
326+
dt_max = float(self.flow_system.hours_per_timestep.max().item())
329327
if not dt_min == dt_max:
330328
raise ValueError(
331329
f'Aggregation failed due to inconsistent time step sizes:'
332330
f'delta_t varies from {dt_min} to {dt_max} hours.'
333331
)
334-
steps_per_period = self.aggregation_parameters.hours_per_period / self.flow_system.hours_per_timestep.max()
335-
is_integer = (
336-
self.aggregation_parameters.hours_per_period % self.flow_system.hours_per_timestep.max()
337-
).item() == 0
338-
if not (steps_per_period.size == 1 and is_integer):
332+
ratio = self.aggregation_parameters.hours_per_period / dt_max
333+
if not np.isclose(ratio, round(ratio), atol=1e-9):
339334
raise ValueError(
340335
f'The selected {self.aggregation_parameters.hours_per_period=} does not match the time '
341-
f'step size of {dt_min} hours). It must be a multiple of {dt_min} hours.'
336+
f'step size of {dt_max} hours. It must be an integer multiple of {dt_max} hours.'
342337
)
343338

344339
logger.info(f'{"":#^80}')

flixopt/components.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ class LinearConverter(Component):
142142
Note:
143143
Conversion factors define linear relationships where the sum of (coefficient × flow_rate)
144144
equals zero for each equation: factor1×flow1 + factor2×flow2 + ... = 0
145-
Conversion factors define linear relationships.
146-
`{flow1: a1, flow2: a2, ...}` leads to `a1×flow_rate1 + a2×flow_rate2 + ... = 0`
147-
Unfortunately the current input format doest read intuitively:
148-
{"electricity": 1, "H2": 50} means that the electricity_in flow rate is multiplied by 1
149-
and the hydrogen_out flow rate is multiplied by 50. THis leads to 50 electricity --> 1 H2.
145+
Conversion factors define linear relationships:
146+
`{flow1: a1, flow2: a2, ...}` yields `a1×flow_rate1 + a2×flow_rate2 + ... = 0`.
147+
Note: The input format may be unintuitive. For example,
148+
`{"electricity": 1, "H2": 50}` implies `1×electricity = 50×H2`,
149+
i.e., 50 units of electricity produce 1 unit of H2.
150150
151151
The system must have fewer conversion factors than total flows (degrees of freedom > 0)
152152
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:
200200
for flow in self.flows.values():
201201
if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
202202
logger.warning(
203-
f'Using a FLow with a fixed size ({flow.label_full}) AND a piecewise_conversion '
204-
f'(in {self.label_full}) and variable size is uncommon. Please check if this is intended!'
203+
f'Using a Flow with variable size (InvestParameters without fixed_size) '
204+
f'and a piecewise_conversion in {self.label_full} is uncommon. Please verify intent '
205+
f'({flow.label_full}).'
205206
)
206207

207208
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:

flixopt/effects.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import logging
1111
import warnings
12-
from collections.abc import Iterator
12+
from collections import deque
1313
from typing import TYPE_CHECKING, Literal
1414

1515
import linopy
@@ -325,14 +325,16 @@ def create_effect_values_dict(
325325
326326
Examples
327327
--------
328-
effect_values_user = 20 -> {None: 20}
329-
effect_values_user = None -> None
330-
effect_values_user = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3}
328+
effect_values_user = 20 -> {'<standard_effect_label>': 20}
329+
effect_values_user = {None: 20} -> {'<standard_effect_label>': 20}
330+
effect_values_user = None -> None
331+
effect_values_user = {'effect1': 20, 'effect2': 0.3} -> {'effect1': 20, 'effect2': 0.3}
331332
332333
Returns
333334
-------
334335
dict or None
335-
A dictionary with None or Effect as the key, or None if input is None.
336+
A dictionary keyed by effect label, or None if input is None.
337+
Note: a standard effect must be defined when passing scalars or None labels.
336338
"""
337339

338340
def get_effect_label(eff: Effect | str) -> str:
@@ -354,6 +356,11 @@ def get_effect_label(eff: Effect | str) -> str:
354356
return None
355357
if isinstance(effect_values_user, dict):
356358
return {get_effect_label(effect): value for effect, value in effect_values_user.items()}
359+
if self.standard_effect is None:
360+
raise KeyError(
361+
'Scalar effect value provided but no standard effect is configured. '
362+
'Either set an effect as is_standard=True or provide a mapping {effect_label: value}.'
363+
)
357364
return {self.standard_effect.label: effect_values_user}
358365

359366
def _plausibility_checks(self) -> None:
@@ -532,7 +539,7 @@ def _add_share_between_effects(self):
532539

533540

534541
def calculate_all_conversion_paths(
535-
conversion_dict: dict[str, dict[str, xr.DataArray]],
542+
conversion_dict: dict[str, dict[str, Scalar | xr.DataArray]],
536543
) -> dict[tuple[str, str], xr.DataArray]:
537544
"""
538545
Calculates all possible direct and indirect conversion factors between units/domains.
@@ -564,10 +571,10 @@ def calculate_all_conversion_paths(
564571
# Keep track of visited paths to avoid repeating calculations
565572
processed_paths = set()
566573
# Use a queue with (current_domain, factor, path_history)
567-
queue = [(origin, 1, [origin])]
574+
queue = deque([(origin, 1, [origin])])
568575

569576
while queue:
570-
current_domain, factor, path = queue.pop(0)
577+
current_domain, factor, path = queue.popleft()
571578

572579
# Skip if we've processed this exact path before
573580
path_key = tuple(path)

flixopt/elements.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,8 @@ def _plausibility_checks(self) -> None:
484484
]
485485
):
486486
raise TypeError(
487-
f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}.'
488-
f'Different values in different years or scenarios are not yetsupported.'
487+
f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}. '
488+
f'Different values in different years or scenarios are not yet supported.'
489489
)
490490

491491
@property

flixopt/flow_system.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ def __init__(
7777
weights: NonTemporalDataUser | None = None,
7878
):
7979
self.timesteps = self._validate_timesteps(timesteps)
80-
self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep)
80+
self.timesteps_extra = self._create_timesteps_with_extra(self.timesteps, hours_of_last_timestep)
8181
self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps(
82-
timesteps, hours_of_previous_timesteps
82+
self.timesteps, hours_of_previous_timesteps
8383
)
8484

8585
self.years_of_last_year = years_of_last_year
@@ -432,11 +432,13 @@ def connect_and_transform(self):
432432
return
433433

434434
self.weights = self.fit_to_model_coords('weights', self.weights, dims=['year', 'scenario'])
435-
if self.weights is not None and self.weights.sum() != 1:
436-
logger.warning(
437-
f'Scenario weights are not normalized to 1. This is recomended for a better scaled model. '
438-
f'Sum of weights={self.weights.sum().item()}'
439-
)
435+
if self.weights is not None:
436+
total = float(self.weights.sum().item())
437+
if not np.isclose(total, 1.0, atol=1e-12):
438+
logger.warning(
439+
'Scenario weights are not normalized to 1. Normalizing to 1 is recommended for a better scaled model. '
440+
f'Sum of weights={total}'
441+
)
440442

441443
self._connect_network()
442444
for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()):
@@ -474,8 +476,8 @@ def create_model(self) -> FlowSystemModel:
474476
raise RuntimeError(
475477
'FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.'
476478
)
477-
self.submodel = FlowSystemModel(self)
478-
return self.submodel
479+
self.model = FlowSystemModel(self)
480+
return self.model
479481

480482
def plot_network(
481483
self,
@@ -558,7 +560,7 @@ def stop_network_app(self):
558560
)
559561

560562
if self._network_app is None:
561-
logger.warning('No network app is currently running. Cant stop it')
563+
logger.warning("No network app is currently running. Can't stop it")
562564
return
563565

564566
try:

flixopt/interface.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ class PiecewiseConversion(Interface):
420420
def __init__(self, piecewises: dict[str, Piecewise]):
421421
self.piecewises = piecewises
422422
self._has_time_dim = True
423-
self.has_time_dim = True # Inital propagation
423+
self.has_time_dim = True # Initial propagation
424424

425425
@property
426426
def has_time_dim(self):
@@ -640,7 +640,7 @@ def __init__(self, piecewise_origin: Piecewise, piecewise_shares: dict[str, Piec
640640
self.piecewise_origin = piecewise_origin
641641
self.piecewise_shares = piecewise_shares
642642
self._has_time_dim = False
643-
self.has_time_dim = False # Inital propagation
643+
self.has_time_dim = False # Initial propagation
644644

645645
@property
646646
def has_time_dim(self):
@@ -1166,7 +1166,7 @@ def use_consecutive_off_hours(self) -> bool:
11661166

11671167
@property
11681168
def use_switch_on(self) -> bool:
1169-
"""Determines wether a Variable for SWITCH-ON is needed or not"""
1169+
"""Determines whether a variable for switch_on is needed or not"""
11701170
if self.force_switch_on:
11711171
return True
11721172

flixopt/modeling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ def scaled_bounds_with_state(
525525
List[linopy.Constraint]: List of constraint objects
526526
"""
527527
if not isinstance(model, Submodel):
528-
raise ValueError('BoundingPatterns.active_bounds_with_state() can only be used with a Submodel')
528+
raise ValueError('BoundingPatterns.scaled_bounds_with_state() can only be used with a Submodel')
529529

530530
rel_lower, rel_upper = relative_bounds
531531
scaling_min, scaling_max = scaling_bounds

0 commit comments

Comments
 (0)