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
18 changes: 9 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Please keep the format of the changelog consistent with the other releases, so t


## [Unreleased] - ????-??-??
This release brings multi-year investments and stochastic modeling to flixopt.
This release brings multi-period 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.

Expand All @@ -55,14 +55,14 @@ 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
share_from_periodic={'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 than 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
**Multi-period investments:**
A flixopt model might be modeled with a "period" dimension.
This enables modeling transformation pathways over multiple periods with several distinct investment decisions in each period.

**Stochastic modeling:**
A flixopt model can be modeled with a scenario dimension.
Expand Down Expand Up @@ -121,9 +121,9 @@ The weighted sum of the total objective effect of each scenario is used as the o
* The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead.
* The assignment of Bus Objects to Flow.bus is deprecated and will be removed in a future version. Use the label of the Bus instead.
* The usage of Effects objects in Dicts to assign shares to Effects is deprecated and will be removed in a future version. Use the label of the Effect instead.
- Renamed `Effect` parameters:
- `minimum_investment` → `minimum_nontemporal`
- `maximum_investment` → `maximum_nontemporal`
* Effect parameters renamed:
- `minimum_investment` → `minimum_periodic`
- `maximum_investment` → `maximum_periodic`
- `minimum_operation` → `minimum_temporal`
- `maximum_operation` → `maximum_temporal`
- `minimum_operation_per_hour` → `minimum_per_hour`
Expand All @@ -132,7 +132,7 @@ The weighted sum of the total objective effect of each scenario is used as the o
### 🔥 Removed
* **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)
- `specific_share_to_other_effects_invest` → `share_from_periodic` (with inverted direction)

### 🐛 Fixed
* Enhanced NetCDF I/O with proper attribute preservation for DataArrays
Expand Down
4 changes: 2 additions & 2 deletions examples/04_Scenarios/scenario_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# Create datetime array starting from '2020-01-01' for the given time period
timesteps = pd.date_range('2020-01-01', periods=9, freq='h')
scenarios = pd.Index(['Base Case', 'High Demand'])
years = pd.Index([2020, 2021, 2022])
periods = pd.Index([2020, 2021, 2022])

# --- Create Time Series Data ---
# Heat demand profile (e.g., kW) over time and corresponding power prices
Expand All @@ -21,7 +21,7 @@
)
power_prices = np.array([0.08, 0.09, 0.10])

flow_system = fx.FlowSystem(timesteps=timesteps, years=years, scenarios=scenarios, weights=np.array([0.5, 0.6]))
flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=np.array([0.5, 0.6]))

# --- Define Energy Buses ---
# These represent nodes, where the used medias are balanced (electricity, heat, and gas)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
[
'Duration [s]',
'costs',
'costs(nontemporal)',
'costs(periodic)',
'costs(temporal)',
'BHKW2(Q_fu)|size',
'Kessel(Q_fu)|size',
Expand Down
2 changes: 1 addition & 1 deletion flixopt/aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ def use_low_peaks(self) -> bool:


class AggregationModel(Submodel):
"""The AggregationModel holds equations and variables related to the Aggregation of a FLowSystem.
"""The AggregationModel holds equations and variables related to the Aggregation of a FlowSystem.
It creates Equations that equates indices of variables, and introduces penalties related to binary variables, that
escape the equation to their related binaries in other periods"""

Expand Down
29 changes: 19 additions & 10 deletions flixopt/calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@
class Calculation:
"""
class for defined way of solving a flow_system optimization

Args:
name: name of calculation
flow_system: flow_system which should be calculated
folder: folder where results should be saved. If None, then the current working directory is used.
normalize_weights: Whether to automatically normalize the weights (periods and scenarios) to sum up to 1 when solving.
active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead.
"""

def __init__(
Expand All @@ -56,14 +63,8 @@ def __init__(
'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead',
] = None,
folder: pathlib.Path | None = None,
normalize_weights: bool = True,
):
"""
Args:
name: name of calculation
flow_system: flow_system which should be calculated
folder: folder where results should be saved. If None, then the current working directory is used.
active_timesteps: Deprecated. Use FLowSystem.sel(time=...) or FlowSystem.isel(time=...) instead.
"""
self.name = name
if flow_system.used_in_calculation:
logger.warning(
Expand All @@ -82,6 +83,7 @@ def __init__(
)
flow_system = flow_system.sel(time=active_timesteps)
self._active_timesteps = active_timesteps # deprecated
self.normalize_weights = normalize_weights

flow_system._used_in_calculation = True

Expand All @@ -108,7 +110,7 @@ def main_results(self) -> dict[str, Scalar | dict]:
'Effects': {
f'{effect.label} [{effect.unit}]': {
'temporal': effect.submodel.temporal.total.solution.values,
'nontemporal': effect.submodel.nontemporal.total.solution.values,
'periodic': effect.submodel.periodic.total.solution.values,
'total': effect.submodel.total.solution.values,
}
for effect in self.flow_system.effects
Expand Down Expand Up @@ -177,13 +179,20 @@ class FullCalculation(Calculation):

This is the most comprehensive calculation type that considers every time step
in the optimization, providing the most accurate but computationally intensive solution.

Args:
name: name of calculation
flow_system: flow_system which should be calculated
folder: folder where results should be saved. If None, then the current working directory is used.
normalize_weights: Whether to automatically normalize the weights (periods and scenarios) to sum up to 1 when solving.
active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead.
"""

def do_modeling(self) -> FullCalculation:
t_start = timeit.default_timer()
self.flow_system.connect_and_transform()

self.model = self.flow_system.create_model()
self.model = self.flow_system.create_model(self.normalize_weights)
self.model.do_modeling()

self.durations['modeling'] = round(timeit.default_timer() - t_start, 2)
Expand Down Expand Up @@ -306,7 +315,7 @@ def do_modeling(self) -> AggregatedCalculation:
self._perform_aggregation()

# Model the System
self.model = self.flow_system.create_model()
self.model = self.flow_system.create_model(self.normalize_weights)
self.model.do_modeling()
# Add Aggregation Submodel after modeling the rest
self.aggregation = AggregationModel(
Expand Down
28 changes: 14 additions & 14 deletions flixopt/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import numpy as np
import xarray as xr

from .core import NonTemporalDataUser, PlausibilityError, TemporalData, TemporalDataUser
from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser
from .elements import Component, ComponentModel, Flow
from .features import InvestmentModel, PiecewiseModel
from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
Expand Down Expand Up @@ -376,14 +376,14 @@ def __init__(
label: str,
charging: Flow,
discharging: Flow,
capacity_in_flow_hours: NonTemporalDataUser | InvestParameters,
capacity_in_flow_hours: PeriodicDataUser | InvestParameters,
relative_minimum_charge_state: TemporalDataUser = 0,
relative_maximum_charge_state: TemporalDataUser = 1,
initial_charge_state: NonTemporalDataUser | Literal['lastValueOfSim'] = 0,
minimal_final_charge_state: NonTemporalDataUser | None = None,
maximal_final_charge_state: NonTemporalDataUser | None = None,
relative_minimum_final_charge_state: NonTemporalDataUser | None = None,
relative_maximum_final_charge_state: NonTemporalDataUser | None = None,
initial_charge_state: PeriodicDataUser | Literal['lastValueOfSim'] = 0,
minimal_final_charge_state: PeriodicDataUser | None = None,
maximal_final_charge_state: PeriodicDataUser | None = None,
relative_minimum_final_charge_state: PeriodicDataUser | None = None,
relative_maximum_final_charge_state: PeriodicDataUser | None = None,
eta_charge: TemporalDataUser = 1,
eta_discharge: TemporalDataUser = 1,
relative_loss_per_hour: TemporalDataUser = 0,
Expand Down Expand Up @@ -442,29 +442,29 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None
)
if not isinstance(self.initial_charge_state, str):
self.initial_charge_state = flow_system.fit_to_model_coords(
f'{prefix}|initial_charge_state', self.initial_charge_state, dims=['year', 'scenario']
f'{prefix}|initial_charge_state', self.initial_charge_state, dims=['period', 'scenario']
)
self.minimal_final_charge_state = flow_system.fit_to_model_coords(
f'{prefix}|minimal_final_charge_state', self.minimal_final_charge_state, dims=['year', 'scenario']
f'{prefix}|minimal_final_charge_state', self.minimal_final_charge_state, dims=['period', 'scenario']
)
self.maximal_final_charge_state = flow_system.fit_to_model_coords(
f'{prefix}|maximal_final_charge_state', self.maximal_final_charge_state, dims=['year', 'scenario']
f'{prefix}|maximal_final_charge_state', self.maximal_final_charge_state, dims=['period', 'scenario']
)
self.relative_minimum_final_charge_state = flow_system.fit_to_model_coords(
f'{prefix}|relative_minimum_final_charge_state',
self.relative_minimum_final_charge_state,
dims=['year', 'scenario'],
dims=['period', 'scenario'],
)
self.relative_maximum_final_charge_state = flow_system.fit_to_model_coords(
f'{prefix}|relative_maximum_final_charge_state',
self.relative_maximum_final_charge_state,
dims=['year', 'scenario'],
dims=['period', 'scenario'],
)
if isinstance(self.capacity_in_flow_hours, InvestParameters):
self.capacity_in_flow_hours.transform_data(flow_system, f'{prefix}|InvestParameters')
else:
self.capacity_in_flow_hours = flow_system.fit_to_model_coords(
f'{prefix}|capacity_in_flow_hours', self.capacity_in_flow_hours, dims=['year', 'scenario']
f'{prefix}|capacity_in_flow_hours', self.capacity_in_flow_hours, dims=['period', 'scenario']
)

def _plausibility_checks(self) -> None:
Expand Down Expand Up @@ -787,7 +787,7 @@ def _do_modeling(self):
label_of_model=f'{self.label_of_element}',
piecewise_variables=piecewise_conversion,
zero_point=self.on_off.on if self.on_off is not None else False,
as_time_series=True,
dims=('time', 'period', 'scenario'),
),
short_name='PiecewiseConversion',
)
Expand Down
14 changes: 10 additions & 4 deletions flixopt/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
Scalar = int | float
"""A single number, either integer or float."""

NonTemporalDataUser = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray
PeriodicDataUser = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray
"""User data which has no time dimension. Internally converted to a Scalar or an xr.DataArray without a time dimension."""

NonTemporalData = xr.DataArray
"""Internally used datatypes for non-temporal data. Can be a Scalar or an xr.DataArray."""
PeriodicData = xr.DataArray
"""Internally used datatypes for periodic data."""

FlowSystemDimensions = Literal['time', 'year', 'scenario']
FlowSystemDimensions = Literal['time', 'period', 'scenario']
"""Possible dimensions of a FlowSystem."""
Comment on lines 20 to 27
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Doc/type mismatch for PeriodicData.

Type alias sets PeriodicData = xr.DataArray, but the docstring says “Can be a Scalar or an xr.DataArray.” Clarify one of:

  • If only DataArray is intended, update docstring.
  • If scalars are allowed, change the alias to Scalar | xr.DataArray.
🤖 Prompt for AI Agents
In flixopt/core.py around lines 20 to 27, the PeriodicData type alias is
declared as xr.DataArray but the docstring says it can be a Scalar or an
xr.DataArray; update one to match the other: either change the docstring to
state it is strictly an xr.DataArray, or change the type alias to include Scalar
(e.g., PeriodicData = Scalar | xr.DataArray) so the type annotation and
docstring are consistent; make the chosen fix and update the docstring
accordingly.



Expand Down Expand Up @@ -620,3 +620,9 @@ def drop_constant_arrays(ds: xr.Dataset, dim='time', drop_arrays_without_dim: bo

logger.debug(f'Dropping {len(drop_vars)} arrays with constant values')
return ds.drop_vars(drop_vars)


# Backward compatibility aliases
# TODO: Needed?
NonTemporalDataUser = PeriodicDataUser
NonTemporalData = PeriodicData
Loading