Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ 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
This enables modeling 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:
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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -109,14 +109,12 @@ 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
* 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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion examples/03_Calculation_types/example_calculation_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
),
Comment on lines +139 to 143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

LGTM: switch to label-based effect keys aligns with deprecation.

Using costs.label/CO2.label prevents deprecated Effect-object keys.

Run to spot any remaining dicts using Effect objects as keys (heuristic):


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inline dicts without .label as keys:"
rg -n --type=py -P 'effects_per_flow_hour\s*=\s*{[^}]*}' | rg -n -v '\.label' || true

echo
echo "Approx. multi-line dicts without .label in following lines:"
rg -n --type=py -P 'effects_per_flow_hour\s*=' -A8 | awk '
/effects_per_flow_hour *=/ {inblk=1; buf=""; }
/^--$/ {inblk=0; if (buf != "" && buf !~ /\.label/) print buf; buf=""; next}
inblk { buf = buf $0 ORS }'

Length of output: 2680


Convert remaining literal effect keys to use .label
Occurrences in tests (tests/*.py), examples (examples/05_Two-stage-optimization/*.py) and core files (flixopt/elements.py, flixopt/components.py) still use string keys like 'costs', 'CO2', 'fuel_cost', etc.—replace them with the corresponding Effect.label constants.

)

Expand Down
10 changes: 5 additions & 5 deletions examples/05_Two-stage-optimization/two_stage_optimization.py
Original file line number Diff line number Diff line change
@@ -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.
"""

Expand Down Expand Up @@ -124,10 +124,10 @@
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 where not correctly equalized')
raise RuntimeError('Sizes were not correctly equalized')

# Optimization of both flow sizes and dispatch together
start = timeit.default_timer()
Expand Down
17 changes: 6 additions & 11 deletions flixopt/calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -322,23 +322,18 @@ 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):
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}')
Expand Down
15 changes: 8 additions & 7 deletions flixopt/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
23 changes: 15 additions & 8 deletions flixopt/effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import logging
import warnings
from collections.abc import Iterator
from collections import deque
from typing import TYPE_CHECKING, Literal

import linopy
Expand Down Expand Up @@ -325,14 +325,16 @@ def create_effect_values_dict(

Examples
--------
effect_values_user = 20 -> {None: 20}
effect_values_user = None -> None
effect_values_user = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3}
effect_values_user = 20 -> {'<standard_effect_label>': 20}
effect_values_user = {None: 20} -> {'<standard_effect_label>': 20}
effect_values_user = None -> None
effect_values_user = {'effect1': 20, 'effect2': 0.3} -> {'effect1': 20, 'effect2': 0.3}

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:
Expand All @@ -354,6 +356,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:
Expand Down Expand Up @@ -532,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.
Expand Down Expand Up @@ -564,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)
Expand Down
4 changes: 2 additions & 2 deletions flixopt/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,8 @@ 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'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.'
)

@property
Expand Down
22 changes: 12 additions & 10 deletions flixopt/flow_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ 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
self.timesteps, hours_of_previous_timesteps
)

self.years_of_last_year = years_of_last_year
Expand Down Expand Up @@ -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 recomended 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()):
Expand Down Expand Up @@ -474,8 +476,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,
Expand Down Expand Up @@ -558,7 +560,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:
Expand Down
6 changes: 3 additions & 3 deletions flixopt/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion flixopt/modeling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions flixopt/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down