Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
87bd422
Feature/speed up resample (#455)
FBumann Nov 4, 2025
efb8532
Update CHANGELOG.md
FBumann Nov 4, 2025
fe3fe23
Feature/compact repr (#457)
FBumann Nov 5, 2025
275cd6a
Feature/speed up resample again (#458)
FBumann Nov 6, 2025
968ff89
Update CHANGELOG.md
FBumann Nov 6, 2025
5f96f6f
Fix bug regarding attrs
FBumann Nov 6, 2025
3a4d773
Add test for resampling
FBumann Nov 6, 2025
3f2542f
chore(deps): update dependency mkdocs-material to v9.6.23 (#462)
renovate[bot] Nov 14, 2025
097f73f
Overhaul types
FBumann Nov 14, 2025
01d4c2d
Introduce Bool data
FBumann Nov 14, 2025
a5b37a7
Introduce Bool data
FBumann Nov 14, 2025
cd5b72b
Fix typehints
FBumann Nov 14, 2025
f188f04
Add EffectData type
FBumann Nov 14, 2025
d8bf7f2
EffectData Type - Complete Redesign
FBumann Nov 14, 2025
bfc645e
Use NumericData instead of Data
FBumann Nov 14, 2025
2a06dfb
Update type hints
FBumann Nov 14, 2025
9e11da8
Update type hints
FBumann Nov 14, 2025
d772169
Use | instead of Union
FBumann Nov 14, 2025
18d5162
Use | instead of Union
FBumann Nov 14, 2025
242eecd
Direct type hints
FBumann Nov 15, 2025
d82c5d2
Use direct type hints instead of subscripts
FBumann Nov 15, 2025
d4f4df0
Update type hints
FBumann Nov 15, 2025
540f1f8
Update type hints
FBumann Nov 15, 2025
cfceef6
Update type documentation
FBumann Nov 15, 2025
d0fac14
Update type documentation
FBumann Nov 15, 2025
1187a2c
Update type documentation
FBumann Nov 15, 2025
8abf6fb
Add another datatype
FBumann Nov 15, 2025
73dc336
Fix typehints
FBumann Nov 15, 2025
c5afcbd
Fix typehints
FBumann Nov 15, 2025
559a9fe
Fix validation in linear_converter classes
FBumann Nov 15, 2025
94a0ca4
Fix validation in linear_converter classes
FBumann Nov 15, 2025
ef1a2ee
pre commit run
FBumann Nov 15, 2025
08ce29d
Step 1
FBumann Nov 15, 2025
0b3394c
Step 2
FBumann Nov 15, 2025
bddce4f
Step 3
FBumann Nov 15, 2025
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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp

Until here -->

## [3.5.0] - 2025-11-06

**Summary**: Improve representations and improve resampling

If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).

### ✨ Added
- Added options to resample and select subsets of flowsystems without converting to and from Dataset each time. Use the new methods `FlowSystem.__dataset_resample()`, `FlowSystem.__dataset_sel()` and `FlowSystem.__dataset_isel()`. All of them expect and return a dataset.

### 💥 Breaking Changes

### ♻️ Changed
- Truncate repr of FlowSystem and CalculationResults to only show the first 10 items of each category
- Greatly sped up the resampling of a FlowSystem again

---

## [3.4.1] - 2025-11-04

**Summary**: Speed up resampling by 20-40 times.

If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).

### ♻️ Changed
- Greatly sped up the resampling of a FlowSystem (x20 - x40) by converting to dataarray internally

---

## [3.4.0] - 2025-11-01

**Summary**: Enhanced solver configuration with new CONFIG.Solving section for centralized solver parameter management.
Expand Down
14 changes: 14 additions & 0 deletions flixopt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@
solvers,
)

# Type system for dimension-aware type hints
from .types import (
Bool_PS,
Bool_S,
Bool_TPS,
Effect_PS,
Effect_S,
Effect_TPS,
Numeric_PS,
Numeric_S,
Numeric_TPS,
Scalar,
)

# === Runtime warning suppression for third-party libraries ===
# These warnings are from dependencies and cannot be fixed by end users.
# They are suppressed at runtime to provide a cleaner user experience.
Expand Down
4 changes: 2 additions & 2 deletions flixopt/calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from .aggregation import Aggregation, AggregationModel, AggregationParameters
from .components import Storage
from .config import CONFIG
from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays
from .core import DataConverter, TimeSeriesData, drop_constant_arrays
from .features import InvestmentModel
from .flow_system import FlowSystem
from .results import CalculationResults, SegmentedCalculationResults
Expand Down Expand Up @@ -103,7 +103,7 @@ def __init__(
self._modeled = False

@property
def main_results(self) -> dict[str, Scalar | dict]:
def main_results(self) -> dict[str, int | float | dict]:
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 | 🟠 Major

Incomplete return type annotation: missing list type.

The return type annotation dict[str, int | float | dict] doesn't account for the 'Buses with excess' key at lines 134-146, which returns a list of dicts. Type checkers will flag this inconsistency.

Apply this diff to fix the type annotation:

-    def main_results(self) -> dict[str, int | float | dict]:
+    def main_results(self) -> dict[str, int | float | dict | list]:
🤖 Prompt for AI Agents
In flixopt/calculation.py around line 106, the return type annotation def
main_results(self) -> dict[str, int | float | dict] is missing the case where a
value is a list of dicts (the 'Buses with excess' key); update the annotation to
include list[dict[str, int | float | dict]] (e.g. def main_results(self) ->
dict[str, int | float | dict | list[dict[str, int | float | dict]]]) so type
checkers accept the list-of-dicts return value.

from flixopt.features import InvestmentModel

main_results = {
Expand Down
43 changes: 22 additions & 21 deletions flixopt/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import xarray as xr

from . import io as fx_io
from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser
from .core import PlausibilityError
from .elements import Component, ComponentModel, Flow
from .features import InvestmentModel, PiecewiseModel
from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
Expand All @@ -23,6 +23,7 @@
import linopy

from .flow_system import FlowSystem
from .types import Numeric_PS, Numeric_TPS

logger = logging.getLogger('flixopt')

Expand Down Expand Up @@ -169,7 +170,7 @@ def __init__(
inputs: list[Flow],
outputs: list[Flow],
on_off_parameters: OnOffParameters | None = None,
conversion_factors: list[dict[str, TemporalDataUser]] | None = None,
conversion_factors: list[dict[str, Numeric_TPS]] | None = None,
piecewise_conversion: PiecewiseConversion | None = None,
meta_data: dict | None = None,
):
Expand Down Expand Up @@ -386,17 +387,17 @@ def __init__(
label: str,
charging: Flow,
discharging: Flow,
capacity_in_flow_hours: PeriodicDataUser | InvestParameters,
relative_minimum_charge_state: TemporalDataUser = 0,
relative_maximum_charge_state: TemporalDataUser = 1,
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,
capacity_in_flow_hours: Numeric_PS | InvestParameters,
relative_minimum_charge_state: Numeric_TPS = 0,
relative_maximum_charge_state: Numeric_TPS = 1,
initial_charge_state: Numeric_PS | Literal['lastValueOfSim'] = 0,
minimal_final_charge_state: Numeric_PS | None = None,
maximal_final_charge_state: Numeric_PS | None = None,
relative_minimum_final_charge_state: Numeric_PS | None = None,
relative_maximum_final_charge_state: Numeric_PS | None = None,
eta_charge: Numeric_TPS = 1,
eta_discharge: Numeric_TPS = 1,
relative_loss_per_hour: Numeric_TPS = 0,
prevent_simultaneous_charge_and_discharge: bool = True,
balanced: bool = False,
meta_data: dict | None = None,
Expand All @@ -413,8 +414,8 @@ def __init__(
self.charging = charging
self.discharging = discharging
self.capacity_in_flow_hours = capacity_in_flow_hours
self.relative_minimum_charge_state: TemporalDataUser = relative_minimum_charge_state
self.relative_maximum_charge_state: TemporalDataUser = relative_maximum_charge_state
self.relative_minimum_charge_state: Numeric_TPS = relative_minimum_charge_state
self.relative_maximum_charge_state: Numeric_TPS = relative_maximum_charge_state

self.relative_minimum_final_charge_state = relative_minimum_final_charge_state
self.relative_maximum_final_charge_state = relative_maximum_final_charge_state
Expand All @@ -423,9 +424,9 @@ def __init__(
self.minimal_final_charge_state = minimal_final_charge_state
self.maximal_final_charge_state = maximal_final_charge_state

self.eta_charge: TemporalDataUser = eta_charge
self.eta_discharge: TemporalDataUser = eta_discharge
self.relative_loss_per_hour: TemporalDataUser = relative_loss_per_hour
self.eta_charge: Numeric_TPS = eta_charge
self.eta_discharge: Numeric_TPS = eta_discharge
self.relative_loss_per_hour: Numeric_TPS = relative_loss_per_hour
self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
self.balanced = balanced

Expand Down Expand Up @@ -663,8 +664,8 @@ def __init__(
out1: Flow,
in2: Flow | None = None,
out2: Flow | None = None,
relative_losses: TemporalDataUser | None = None,
absolute_losses: TemporalDataUser | None = None,
relative_losses: Numeric_TPS | None = None,
absolute_losses: Numeric_TPS | None = None,
on_off_parameters: OnOffParameters = None,
prevent_simultaneous_flows_in_both_directions: bool = True,
balanced: bool = False,
Expand Down Expand Up @@ -916,7 +917,7 @@ def _initial_and_final_charge_state(self):
)

@property
def _absolute_charge_state_bounds(self) -> tuple[TemporalData, TemporalData]:
def _absolute_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]:
relative_lower_bound, relative_upper_bound = self._relative_charge_state_bounds
if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
return (
Expand Down
37 changes: 3 additions & 34 deletions flixopt/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,9 @@
import pandas as pd
import xarray as xr

logger = logging.getLogger('flixopt')

Scalar = int | float
"""A single number, either integer or float."""

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."""
from .types import NumericOrBool

PeriodicData = xr.DataArray
"""Internally used datatypes for periodic data."""
logger = logging.getLogger('flixopt')

FlowSystemDimensions = Literal['time', 'period', 'scenario']
"""Possible dimensions of a FlowSystem."""
Expand Down Expand Up @@ -150,15 +143,6 @@ def agg_weight(self):
return self.aggregation_weight


TemporalDataUser = (
int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray | TimeSeriesData
)
"""User data which might have a time dimension. Internally converted to an xr.DataArray with time dimension."""

TemporalData = xr.DataArray | TimeSeriesData
"""Internally used datatypes for temporal data (data with a time dimension)."""


class DataConverter:
"""
Converts various data types into xarray.DataArray with specified target coordinates.
Expand Down Expand Up @@ -405,16 +389,7 @@ def _broadcast_dataarray_to_target_specification(
@classmethod
def to_dataarray(
cls,
data: int
| float
| bool
| np.integer
| np.floating
| np.bool_
| np.ndarray
| pd.Series
| pd.DataFrame
| xr.DataArray,
data: NumericOrBool,
coords: dict[str, pd.Index] | None = None,
) -> xr.DataArray:
"""
Expand Down Expand Up @@ -637,9 +612,3 @@ def drop_constant_arrays(ds: xr.Dataset, dim: str = 'time', drop_arrays_without_
)

return ds.drop_vars(drop_vars)


# Backward compatibility aliases
# TODO: Needed?
NonTemporalDataUser = PeriodicDataUser
NonTemporalData = PeriodicData
71 changes: 34 additions & 37 deletions flixopt/effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@
import numpy as np
import xarray as xr

from . import io as fx_io
from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser
from .features import ShareAllocationModel
from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io

if TYPE_CHECKING:
from collections.abc import Iterator

from .flow_system import FlowSystem
from .types import Effect_PS, Effect_TPS, Numeric_PS, Numeric_TPS, Scalar

logger = logging.getLogger('flixopt')

Expand Down Expand Up @@ -52,17 +51,27 @@ class Effect(Element):
is_objective: If True, this effect serves as the optimization objective function.
Only one effect can be marked as objective per optimization.
share_from_temporal: Temporal cross-effect contributions.
Maps temporal contributions from other effects to this effect
Maps temporal contributions from other effects to this effect.
Type: `Effect_TPS` (single value or dict with dimensions [Time, Period, Scenario])
share_from_periodic: Periodic cross-effect contributions.
Maps periodic contributions from other effects to this effect.
Type: `Effect_PS` (single value or dict with dimensions [Period, Scenario])
minimum_temporal: Minimum allowed total contribution across all timesteps.
Type: `Numeric_PS` (sum over time, can vary by period/scenario)
maximum_temporal: Maximum allowed total contribution across all timesteps.
Type: `Numeric_PS` (sum over time, can vary by period/scenario)
minimum_per_hour: Minimum allowed contribution per hour.
Type: `Numeric_TPS` (per-timestep constraint, can vary by period)
maximum_per_hour: Maximum allowed contribution per hour.
Type: `Numeric_TPS` (per-timestep constraint, can vary by period)
minimum_periodic: Minimum allowed total periodic contribution.
Type: `Numeric_PS` (periodic constraint)
maximum_periodic: Maximum allowed total periodic contribution.
Type: `Numeric_PS` (periodic constraint)
minimum_total: Minimum allowed total effect (temporal + periodic combined).
Type: `Numeric_PS` (total constraint per period)
maximum_total: Maximum allowed total effect (temporal + periodic combined).
Type: `Numeric_PS` (total constraint per period)
meta_data: Used to store additional information. Not used internally but saved
in results. Only use Python native types.

Expand Down Expand Up @@ -170,25 +179,25 @@ def __init__(
meta_data: dict | None = None,
is_standard: bool = False,
is_objective: bool = False,
share_from_temporal: TemporalEffectsUser | None = None,
share_from_periodic: PeriodicEffectsUser | None = None,
minimum_temporal: PeriodicEffectsUser | None = None,
maximum_temporal: PeriodicEffectsUser | None = None,
minimum_periodic: PeriodicEffectsUser | None = None,
maximum_periodic: PeriodicEffectsUser | None = None,
minimum_per_hour: TemporalDataUser | None = None,
maximum_per_hour: TemporalDataUser | None = None,
minimum_total: Scalar | None = None,
maximum_total: Scalar | None = None,
share_from_temporal: Effect_TPS | None = None,
share_from_periodic: Effect_PS | None = None,
minimum_temporal: Numeric_PS | None = None,
maximum_temporal: Numeric_PS | None = None,
minimum_periodic: Numeric_PS | None = None,
maximum_periodic: Numeric_PS | None = None,
minimum_per_hour: Numeric_TPS | None = None,
maximum_per_hour: Numeric_TPS | None = None,
minimum_total: Numeric_PS | None = None,
maximum_total: Numeric_PS | None = None,
**kwargs,
):
super().__init__(label, meta_data=meta_data)
self.unit = unit
self.description = description
self.is_standard = is_standard
self.is_objective = is_objective
self.share_from_temporal: TemporalEffectsUser = share_from_temporal if share_from_temporal is not None else {}
self.share_from_periodic: PeriodicEffectsUser = share_from_periodic if share_from_periodic is not None else {}
self.share_from_temporal: Effect_TPS = share_from_temporal if share_from_temporal is not None else {}
self.share_from_periodic: Effect_PS = share_from_periodic if share_from_periodic is not None else {}

# Handle backwards compatibility for deprecated parameters using centralized helper
minimum_temporal = self._handle_deprecated_kwarg(
Expand Down Expand Up @@ -436,18 +445,6 @@ def _do_modeling(self):
)


TemporalEffectsUser = TemporalDataUser | dict[str, TemporalDataUser] # User-specified Shares to Effects
""" This datatype is used to define a temporal share to an effect by a certain attribute. """

PeriodicEffectsUser = PeriodicDataUser | dict[str, PeriodicDataUser] # User-specified Shares to Effects
""" This datatype is used to define a scalar share to an effect by a certain attribute. """

TemporalEffects = dict[str, TemporalData] # User-specified Shares to Effects
""" This datatype is used internally to handle temporal shares to an effect. """

PeriodicEffects = dict[str, Scalar]
""" This datatype is used internally to handle scalar shares to an effect. """

EffectExpr = dict[str, linopy.LinearExpression] # Used to create Shares


Expand All @@ -458,8 +455,15 @@ class EffectCollection(ElementContainer[Effect]):

submodel: EffectCollectionModel | None

def __init__(self, *effects: Effect):
super().__init__(element_type_name='effects')
def __init__(self, *effects: Effect, truncate_repr: int | None = None):
"""
Initialize the EffectCollection.

Args:
*effects: Effects to register in the collection.
truncate_repr: Maximum number of items to show in repr. If None, show all items. Default: None
"""
super().__init__(element_type_name='effects', truncate_repr=truncate_repr)
self._standard_effect: Effect | None = None
self._objective_effect: Effect | None = None

Expand All @@ -482,9 +486,7 @@ def add_effects(self, *effects: Effect) -> None:
self.add(effect) # Use the inherited add() method from ElementContainer
logger.info(f'Registered new Effect: {effect.label}')

def create_effect_values_dict(
self, effect_values_user: PeriodicEffectsUser | TemporalEffectsUser
) -> dict[str, Scalar | TemporalDataUser] | None:
def create_effect_values_dict(self, effect_values_user: Numeric_TPS | Effect_TPS | None) -> Effect_TPS | None:
"""Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type.

Examples:
Expand Down Expand Up @@ -844,8 +846,3 @@ def tuples_to_adjacency_list(edges: list[tuple[str, str]]) -> dict[str, list[str
graph[target] = []

return graph


# Backward compatibility aliases
NonTemporalEffectsUser = PeriodicEffectsUser
NonTemporalEffects = PeriodicEffects
Loading
Loading