From 097f73fdee165b6ebc887910fea74d015477f930 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:54:08 +0100 Subject: [PATCH 01/34] Overhaul types --- flixopt/__init__.py | 3 + flixopt/components.py | 29 ++++---- flixopt/core.py | 22 +++++-- flixopt/effects.py | 21 ++++-- flixopt/elements.py | 7 +- flixopt/interface.py | 19 +++--- flixopt/types.py | 149 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 216 insertions(+), 34 deletions(-) create mode 100644 flixopt/types.py diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 3633d86a1..31a242fe0 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -41,6 +41,9 @@ solvers, ) +# Type system for dimension-aware type hints +from .types import Data, Period, Scalar, Scenario, Time + # === 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. diff --git a/flixopt/components.py b/flixopt/components.py index e4209c8ac..818f349f5 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -23,6 +23,7 @@ import linopy from .flow_system import FlowSystem + from .types import Data, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -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, Data[Time, Scenario]]] | None = None, piecewise_conversion: PiecewiseConversion | None = None, meta_data: dict | None = None, ): @@ -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: Data[Period, Scenario] | InvestParameters, + relative_minimum_charge_state: Data[Time, Scenario] = 0, + relative_maximum_charge_state: Data[Time, Scenario] = 1, + initial_charge_state: Data[Period, Scenario] | Literal['lastValueOfSim'] = 0, + minimal_final_charge_state: Data[Period, Scenario] | None = None, + maximal_final_charge_state: Data[Period, Scenario] | None = None, + relative_minimum_final_charge_state: Data[Period, Scenario] | None = None, + relative_maximum_final_charge_state: Data[Period, Scenario] | None = None, + eta_charge: Data[Time, Scenario] = 1, + eta_discharge: Data[Time, Scenario] = 1, + relative_loss_per_hour: Data[Time, Scenario] = 0, prevent_simultaneous_charge_and_discharge: bool = True, balanced: bool = False, meta_data: dict | None = None, @@ -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: Data[Time, Scenario] | None = None, + absolute_losses: Data[Time, Scenario] | None = None, on_off_parameters: OnOffParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, balanced: bool = False, diff --git a/flixopt/core.py b/flixopt/core.py index 917ee2984..1b8e1a660 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -12,13 +12,22 @@ import pandas as pd import xarray as xr +from flixopt.types import Data, Period, Scalar, Scenario, Time + logger = logging.getLogger('flixopt') -Scalar = int | float +# Legacy type aliases (kept for backward compatibility) +# These are being replaced by dimension-aware Data[...] types +Scalar = Scalar """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.""" +PeriodicDataUser = Data[Period, Scenario] +""" +User data which has no time dimension. Internally converted to a Scalar or an xr.DataArray without a time dimension. + +.. deprecated:: + Use dimension-aware types instead: `Data[Period, Scenario]` or `Data[Scenario]` +""" PeriodicData = xr.DataArray """Internally used datatypes for periodic data.""" @@ -153,7 +162,12 @@ def agg_weight(self): 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.""" +""" +User data which might have a time dimension. Internally converted to an xr.DataArray with time dimension. + +.. deprecated:: + Use dimension-aware types instead: `Data[Time]`, `Data[Time, Scenario]`, or `Data[Time, Period, Scenario]` +""" TemporalData = xr.DataArray | TimeSeriesData """Internally used datatypes for temporal data (data with a time dimension).""" diff --git a/flixopt/effects.py b/flixopt/effects.py index ddf8eadeb..44bc6d25c 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -20,6 +20,7 @@ from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io +from .types import Data, Period, Scenario, Time if TYPE_CHECKING: from collections.abc import Iterator @@ -436,11 +437,23 @@ 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. """ +TemporalEffectsUser = Data[Time, Scenario] | dict[str, Data[Time, Scenario]] # User-specified Shares to Effects +""" +This datatype is used to define a temporal share to an effect by a certain attribute. + +Can be: +- A single value (scalar, array, Series, DataFrame, DataArray) with at most [Time, Scenario] dimensions +- A dictionary mapping effect names to values with at most [Time, Scenario] dimensions +""" -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. """ +PeriodicEffectsUser = Data[Period, Scenario] | dict[str, Data[Period, Scenario]] # User-specified Shares to Effects +""" +This datatype is used to define a periodic share to an effect by a certain attribute. + +Can be: +- A single value (scalar, array, Series, DataFrame, DataArray) with at most [Period, Scenario] dimensions +- A dictionary mapping effect names to values with at most [Period, Scenario] dimensions +""" TemporalEffects = dict[str, TemporalData] # User-specified Shares to Effects """ This datatype is used internally to handle temporal shares to an effect. """ diff --git a/flixopt/elements.py b/flixopt/elements.py index 337f34fce..46ac8f6e8 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -24,6 +24,7 @@ from .effects import TemporalEffectsUser from .flow_system import FlowSystem + from .types import Data, Scenario, Time logger = logging.getLogger('flixopt') @@ -420,9 +421,9 @@ def __init__( label: str, bus: str, size: Scalar | InvestParameters = None, - fixed_relative_profile: TemporalDataUser | None = None, - relative_minimum: TemporalDataUser = 0, - relative_maximum: TemporalDataUser = 1, + fixed_relative_profile: Data[Time, Scenario] | None = None, + relative_minimum: Data[Time, Scenario] = 0, + relative_maximum: Data[Time, Scenario] = 1, effects_per_flow_hour: TemporalEffectsUser | None = None, on_off_parameters: OnOffParameters | None = None, flow_hours_total_max: Scalar | None = None, diff --git a/flixopt/interface.py b/flixopt/interface.py index 21cbc82b9..72d7342a3 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -22,6 +22,7 @@ from .core import PeriodicData, PeriodicDataUser, Scalar, TemporalDataUser from .effects import PeriodicEffectsUser, TemporalEffectsUser from .flow_system import FlowSystem + from .types import Data, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -73,7 +74,7 @@ class Piece(Interface): """ - def __init__(self, start: TemporalDataUser, end: TemporalDataUser): + def __init__(self, start: Data[Time, Period, Scenario], end: Data[Time, Period, Scenario]): self.start = start self.end = end self.has_time_dim = False @@ -874,15 +875,15 @@ class InvestParameters(Interface): def __init__( self, - fixed_size: PeriodicDataUser | None = None, - minimum_size: PeriodicDataUser | None = None, - maximum_size: PeriodicDataUser | None = None, + fixed_size: Data[Period, Scenario] | None = None, + minimum_size: Data[Period, Scenario] | None = None, + maximum_size: Data[Period, Scenario] | None = None, mandatory: bool = False, effects_of_investment: PeriodicEffectsUser | None = None, effects_of_investment_per_size: PeriodicEffectsUser | None = None, effects_of_retirement: PeriodicEffectsUser | None = None, piecewise_effects_of_investment: PiecewiseEffects | None = None, - linked_periods: PeriodicDataUser | tuple[int, int] | None = None, + linked_periods: Data[Period, Scenario] | tuple[int, int] | None = None, **kwargs, ): # Handle deprecated parameters using centralized helper @@ -1272,10 +1273,10 @@ def __init__( effects_per_running_hour: TemporalEffectsUser | None = None, on_hours_total_min: int | None = None, on_hours_total_max: int | None = None, - consecutive_on_hours_min: TemporalDataUser | None = None, - consecutive_on_hours_max: TemporalDataUser | None = None, - consecutive_off_hours_min: TemporalDataUser | None = None, - consecutive_off_hours_max: TemporalDataUser | None = None, + consecutive_on_hours_min: Data[Time, Scenario] | None = None, + consecutive_on_hours_max: Data[Time, Scenario] | None = None, + consecutive_off_hours_min: Data[Time, Scenario] | None = None, + consecutive_off_hours_max: Data[Time, Scenario] | None = None, switch_on_total_max: int | None = None, force_switch_on: bool = False, ): diff --git a/flixopt/types.py b/flixopt/types.py new file mode 100644 index 000000000..345d4a48c --- /dev/null +++ b/flixopt/types.py @@ -0,0 +1,149 @@ +""" +Type system for dimension-aware data in flixopt. + +This module provides generic types that clearly communicate which dimensions +data can have. The type system is designed to be self-documenting while +maintaining maximum flexibility for input formats. + +Key Concepts +------------ +- Dimension markers (`Time`, `Period`, `Scenario`) represent the possible dimensions +- `Data[...]` generic type indicates the **maximum** dimensions data can have +- Data can have any subset of the specified dimensions (including being scalar) +- All standard input formats are supported (scalar, array, Series, DataFrame, DataArray) + +Examples +-------- +Type hint `Data[Time]` accepts: + - Scalar: `0.5` (broadcast to all timesteps) + - 1D array: `np.array([1, 2, 3])` (matched to time dimension) + - pandas Series: with DatetimeIndex matching flow system + - xarray DataArray: with 'time' dimension + +Type hint `Data[Time, Scenario]` accepts: + - Scalar: `100` (broadcast to all time and scenario combinations) + - 1D array: matched to time OR scenario dimension + - 2D array: matched to both dimensions + - pandas DataFrame: columns as scenarios, index as time + - xarray DataArray: with any subset of 'time', 'scenario' dimensions + +Type hint `Data[Period, Scenario]` (periodic data, no time): + - Used for investment parameters that vary by planning period + - Accepts scalars, arrays matching periods/scenarios, or DataArrays + +Type hint `Scalar`: + - Only numeric scalars (int, float) + - Not converted to DataArray, stays as scalar +""" + +from typing import Any, TypeAlias + +import numpy as np +import pandas as pd +import xarray as xr + + +# Dimension marker classes for generic type subscripting +class Time: + """Marker for the time dimension in Data generic types.""" + + pass + + +class Period: + """Marker for the period dimension in Data generic types (for multi-period optimization).""" + + pass + + +class Scenario: + """Marker for the scenario dimension in Data generic types (for scenario analysis).""" + + pass + + +class _DataMeta(type): + """Metaclass for Data to enable subscript notation Data[Time, Scenario].""" + + def __getitem__(cls, dimensions): + """ + Create a type hint showing maximum dimensions. + + The dimensions parameter can be: + - A single dimension: Data[Time] + - Multiple dimensions: Data[Time, Scenario] + + The type hint communicates that data can have **at most** these dimensions. + Actual data can be: + - Scalar (broadcast to all dimensions) + - Have any subset of the specified dimensions + - Have all specified dimensions + + This is consistent with xarray's broadcasting semantics and the + framework's data conversion behavior. + """ + # For type checking purposes, we return the same union type regardless + # of which dimensions are specified. The dimension parameters serve + # as documentation rather than runtime validation. + + # Return type that includes all possible input formats + return int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray + + +class Data(metaclass=_DataMeta): + """ + Generic type for data that can have various dimensions. + + Use subscript notation to specify the maximum dimensions: + - `Data[Time]`: Time-varying data (at most 'time' dimension) + - `Data[Time, Scenario]`: Time-varying with scenarios (at most 'time', 'scenario') + - `Data[Period, Scenario]`: Periodic data without time (at most 'period', 'scenario') + - `Data[Time, Period, Scenario]`: Full dimensionality (rarely used) + + Semantics: "At Most" Dimensions + -------------------------------- + When you see `Data[Time, Scenario]`, it means the data can have: + - No dimensions (scalar): broadcast to all time and scenario values + - Just 'time': broadcast across scenarios + - Just 'scenario': broadcast across time + - Both 'time' and 'scenario': full dimensionality + + Accepted Input Formats + ---------------------- + All dimension combinations accept these formats: + - Scalars: int, float (including numpy types) + - Arrays: numpy ndarray (matched by length/shape to dimensions) + - pandas Series: matched by index to dimension coordinates + - pandas DataFrame: typically columns=scenarios, index=time + - xarray DataArray: used directly with dimension validation + + Conversion Behavior + ------------------- + Input data is converted to xarray.DataArray internally: + - Scalars are broadcast to all specified dimensions + - Arrays are matched by length (unambiguous) or shape (multi-dimensional) + - Series are matched by index equality with coordinate values + - DataArrays are validated and broadcast as needed + + See Also + -------- + DataConverter.to_dataarray : The conversion implementation + FlowSystem.fit_to_model_coords : Fits data to the model's coordinate system + """ + + # This class is not meant to be instantiated, only used for type hints + def __init__(self): + raise TypeError('Data is a type hint only and cannot be instantiated') + + +# Simple scalar type for dimension-less numeric values +Scalar: TypeAlias = int | float | np.integer | np.floating + +# Export public API +__all__ = [ + 'Data', + 'Time', + 'Period', + 'Scenario', + 'Scalar', +] From 01d4c2dec432afdf40c6d281a1a6ed4f2e513eb9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:07:46 +0100 Subject: [PATCH 02/34] Introduce Bool data --- flixopt/__init__.py | 2 +- flixopt/types.py | 92 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 31a242fe0..5583e9aaf 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -42,7 +42,7 @@ ) # Type system for dimension-aware type hints -from .types import Data, Period, Scalar, Scenario, Time +from .types import BoolData, Data, Period, Scalar, Scenario, Time # === Runtime warning suppression for third-party libraries === # These warnings are from dependencies and cannot be fixed by end users. diff --git a/flixopt/types.py b/flixopt/types.py index 345d4a48c..77a1e596e 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -62,12 +62,12 @@ class Scenario: pass -class _DataMeta(type): - """Metaclass for Data to enable subscript notation Data[Time, Scenario].""" +class _NumericDataMeta(type): + """Metaclass for Data to enable subscript notation Data[Time, Scenario] for numeric data.""" def __getitem__(cls, dimensions): """ - Create a type hint showing maximum dimensions. + Create a type hint showing maximum dimensions for numeric data. The dimensions parameter can be: - A single dimension: Data[Time] @@ -86,11 +86,24 @@ def __getitem__(cls, dimensions): # of which dimensions are specified. The dimension parameters serve # as documentation rather than runtime validation. - # Return type that includes all possible input formats + # Return type that includes all possible numeric input formats return int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray -class Data(metaclass=_DataMeta): +class _BoolDataMeta(type): + """Metaclass for BoolData to enable subscript notation BoolData[Time, Scenario] for boolean data.""" + + def __getitem__(cls, dimensions): + """ + Create a type hint showing maximum dimensions for boolean data. + + Same semantics as numeric Data, but for boolean values. + """ + # Return type that includes all possible boolean input formats + return bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray + + +class Data(metaclass=_NumericDataMeta): """ Generic type for data that can have various dimensions. @@ -125,8 +138,13 @@ class Data(metaclass=_DataMeta): - Series are matched by index equality with coordinate values - DataArrays are validated and broadcast as needed + Note + ---- + This type is for **numeric** data only. For boolean data, use `BoolData`. + See Also -------- + BoolData : For boolean data with dimensions DataConverter.to_dataarray : The conversion implementation FlowSystem.fit_to_model_coords : Fits data to the model's coordinate system """ @@ -136,12 +154,76 @@ def __init__(self): raise TypeError('Data is a type hint only and cannot be instantiated') +class BoolData(metaclass=_BoolDataMeta): + """ + Generic type for boolean data that can have various dimensions. + + Use subscript notation to specify the maximum dimensions: + - `BoolData[Time]`: Time-varying boolean data + - `BoolData[Time, Scenario]`: Boolean data with time and scenario dimensions + - `BoolData[Period, Scenario]`: Periodic boolean data + + Semantics: "At Most" Dimensions + -------------------------------- + Same semantics as Data, but for boolean values. + When you see `BoolData[Time, Scenario]`, the data can have: + - No dimensions (scalar bool): broadcast to all time and scenario values + - Just 'time': broadcast across scenarios + - Just 'scenario': broadcast across time + - Both 'time' and 'scenario': full dimensionality + + Accepted Input Formats (Boolean) + --------------------------------- + All dimension combinations accept these formats: + - Scalars: bool, np.bool_ + - Arrays: numpy ndarray with boolean dtype (matched by length/shape to dimensions) + - pandas Series: with boolean values, matched by index to dimension coordinates + - pandas DataFrame: with boolean values + - xarray DataArray: with boolean values, used directly with dimension validation + + Use Cases + --------- + Boolean data is typically used for: + - Binary decision variables (on/off states) + - Constraint activation flags + - Feasibility indicators + - Conditional parameters + + Examples + -------- + >>> # Scalar boolean (broadcast to all dimensions) + >>> active: BoolData[Time] = True + >>> + >>> # Time-varying on/off pattern + >>> import numpy as np + >>> pattern: BoolData[Time] = np.array([True, False, True, False]) + >>> + >>> # Scenario-specific activation + >>> import pandas as pd + >>> scenario_active: BoolData[Scenario] = pd.Series([True, False, True], index=['low', 'mid', 'high']) + + Note + ---- + This type is for **boolean** data only. For numeric data, use `Data`. + + See Also + -------- + Data : For numeric data with dimensions + DataConverter.to_dataarray : The conversion implementation + """ + + # This class is not meant to be instantiated, only used for type hints + def __init__(self): + raise TypeError('BoolData is a type hint only and cannot be instantiated') + + # Simple scalar type for dimension-less numeric values Scalar: TypeAlias = int | float | np.integer | np.floating # Export public API __all__ = [ 'Data', + 'BoolData', 'Time', 'Period', 'Scenario', From a5b37a7e56d6f71ea086c38188958163c318da91 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:14:03 +0100 Subject: [PATCH 03/34] Introduce Bool data --- flixopt/__init__.py | 2 +- flixopt/types.py | 30 ++++++++++++++++++++---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 5583e9aaf..f744d05a3 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -42,7 +42,7 @@ ) # Type system for dimension-aware type hints -from .types import BoolData, Data, Period, Scalar, Scenario, Time +from .types import BoolData, Data, NumericData, Period, Scalar, Scenario, Time # === Runtime warning suppression for third-party libraries === # These warnings are from dependencies and cannot be fixed by end users. diff --git a/flixopt/types.py b/flixopt/types.py index 77a1e596e..17aadddce 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -105,17 +105,19 @@ def __getitem__(cls, dimensions): class Data(metaclass=_NumericDataMeta): """ - Generic type for data that can have various dimensions. + Base type for numeric data that can have various dimensions. + + This is the internal base class. Use `NumericData` publicly for clarity. Use subscript notation to specify the maximum dimensions: - - `Data[Time]`: Time-varying data (at most 'time' dimension) - - `Data[Time, Scenario]`: Time-varying with scenarios (at most 'time', 'scenario') - - `Data[Period, Scenario]`: Periodic data without time (at most 'period', 'scenario') - - `Data[Time, Period, Scenario]`: Full dimensionality (rarely used) + - `NumericData[Time]`: Time-varying numeric data (at most 'time' dimension) + - `NumericData[Time, Scenario]`: Time-varying with scenarios (at most 'time', 'scenario') + - `NumericData[Period, Scenario]`: Periodic data without time (at most 'period', 'scenario') + - `NumericData[Time, Period, Scenario]`: Full dimensionality (rarely used) Semantics: "At Most" Dimensions -------------------------------- - When you see `Data[Time, Scenario]`, it means the data can have: + When you see `NumericData[Time, Scenario]`, it means the data can have: - No dimensions (scalar): broadcast to all time and scenario values - Just 'time': broadcast across scenarios - Just 'scenario': broadcast across time @@ -142,8 +144,11 @@ class Data(metaclass=_NumericDataMeta): ---- This type is for **numeric** data only. For boolean data, use `BoolData`. + This is the base class - use `NumericData` alias publicly for clarity and symmetry with `BoolData`. + See Also -------- + NumericData : Public alias for this class BoolData : For boolean data with dimensions DataConverter.to_dataarray : The conversion implementation FlowSystem.fit_to_model_coords : Fits data to the model's coordinate system @@ -204,11 +209,11 @@ class BoolData(metaclass=_BoolDataMeta): Note ---- - This type is for **boolean** data only. For numeric data, use `Data`. + This type is for **boolean** data only. For numeric data, use `NumericData`. See Also -------- - Data : For numeric data with dimensions + NumericData : For numeric data with dimensions DataConverter.to_dataarray : The conversion implementation """ @@ -217,13 +222,18 @@ def __init__(self): raise TypeError('BoolData is a type hint only and cannot be instantiated') +# Public alias for Data (for clarity and symmetry with BoolData) +NumericData = Data +"""Public type for numeric data with dimensions. Alias for the internal `Data` class.""" + # Simple scalar type for dimension-less numeric values Scalar: TypeAlias = int | float | np.integer | np.floating # Export public API __all__ = [ - 'Data', - 'BoolData', + 'NumericData', # Primary public type for numeric data + 'BoolData', # Primary public type for boolean data + 'Data', # Also exported (internal base class, can be used as shorthand) 'Time', 'Period', 'Scenario', From cd5b72b5048b67e86029cebced54d54f9259e987 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:23:03 +0100 Subject: [PATCH 04/34] Fix typehints --- flixopt/components.py | 14 +++++++------- flixopt/effects.py | 2 +- flixopt/elements.py | 8 ++++---- flixopt/interface.py | 8 ++++---- flixopt/types.py | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 818f349f5..b91f219d9 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -388,16 +388,16 @@ def __init__( charging: Flow, discharging: Flow, capacity_in_flow_hours: Data[Period, Scenario] | InvestParameters, - relative_minimum_charge_state: Data[Time, Scenario] = 0, - relative_maximum_charge_state: Data[Time, Scenario] = 1, + relative_minimum_charge_state: Data[Time, Period, Scenario] = 0, + relative_maximum_charge_state: Data[Time, Period, Scenario] = 1, initial_charge_state: Data[Period, Scenario] | Literal['lastValueOfSim'] = 0, minimal_final_charge_state: Data[Period, Scenario] | None = None, maximal_final_charge_state: Data[Period, Scenario] | None = None, relative_minimum_final_charge_state: Data[Period, Scenario] | None = None, relative_maximum_final_charge_state: Data[Period, Scenario] | None = None, - eta_charge: Data[Time, Scenario] = 1, - eta_discharge: Data[Time, Scenario] = 1, - relative_loss_per_hour: Data[Time, Scenario] = 0, + eta_charge: Data[Time, Period, Scenario] = 1, + eta_discharge: Data[Time, Period, Scenario] = 1, + relative_loss_per_hour: Data[Time, Period, Scenario] = 0, prevent_simultaneous_charge_and_discharge: bool = True, balanced: bool = False, meta_data: dict | None = None, @@ -664,8 +664,8 @@ def __init__( out1: Flow, in2: Flow | None = None, out2: Flow | None = None, - relative_losses: Data[Time, Scenario] | None = None, - absolute_losses: Data[Time, Scenario] | None = None, + relative_losses: Data[Time, Period, Scenario] | None = None, + absolute_losses: Data[Time, Period, Scenario] | None = None, on_off_parameters: OnOffParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, balanced: bool = False, diff --git a/flixopt/effects.py b/flixopt/effects.py index 44bc6d25c..9c388fe2c 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -437,7 +437,7 @@ def _do_modeling(self): ) -TemporalEffectsUser = Data[Time, Scenario] | dict[str, Data[Time, Scenario]] # User-specified Shares to Effects +TemporalEffectsUser = Data[Time, Period, Scenario] | dict[str, Data[Time, Scenario]] # User-specified Shares to Effects """ This datatype is used to define a temporal share to an effect by a certain attribute. diff --git a/flixopt/elements.py b/flixopt/elements.py index 46ac8f6e8..aa18a67ab 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -24,7 +24,7 @@ from .effects import TemporalEffectsUser from .flow_system import FlowSystem - from .types import Data, Scenario, Time + from .types import Data, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -421,9 +421,9 @@ def __init__( label: str, bus: str, size: Scalar | InvestParameters = None, - fixed_relative_profile: Data[Time, Scenario] | None = None, - relative_minimum: Data[Time, Scenario] = 0, - relative_maximum: Data[Time, Scenario] = 1, + fixed_relative_profile: Data[Time, Period, Scenario] | None = None, + relative_minimum: Data[Time, Period, Scenario] = 0, + relative_maximum: Data[Time, Period, Scenario] = 1, effects_per_flow_hour: TemporalEffectsUser | None = None, on_off_parameters: OnOffParameters | None = None, flow_hours_total_max: Scalar | None = None, diff --git a/flixopt/interface.py b/flixopt/interface.py index 72d7342a3..1ff30b933 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1273,10 +1273,10 @@ def __init__( effects_per_running_hour: TemporalEffectsUser | None = None, on_hours_total_min: int | None = None, on_hours_total_max: int | None = None, - consecutive_on_hours_min: Data[Time, Scenario] | None = None, - consecutive_on_hours_max: Data[Time, Scenario] | None = None, - consecutive_off_hours_min: Data[Time, Scenario] | None = None, - consecutive_off_hours_max: Data[Time, Scenario] | None = None, + consecutive_on_hours_min: Data[Time, Period, Scenario] | None = None, + consecutive_on_hours_max: Data[Time, Period, Scenario] | None = None, + consecutive_off_hours_min: Data[Time, Period, Scenario] | None = None, + consecutive_off_hours_max: Data[Time, Period, Scenario] | None = None, switch_on_total_max: int | None = None, force_switch_on: bool = False, ): diff --git a/flixopt/types.py b/flixopt/types.py index 17aadddce..58bd61cd9 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -71,7 +71,7 @@ def __getitem__(cls, dimensions): The dimensions parameter can be: - A single dimension: Data[Time] - - Multiple dimensions: Data[Time, Scenario] + - Multiple dimensions: Data[Time, Period, Scenario] The type hint communicates that data can have **at most** these dimensions. Actual data can be: From f188f042755f47fbb82359515f2945f840783525 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:59:09 +0100 Subject: [PATCH 05/34] Add EffectData type --- flixopt/__init__.py | 5 +- flixopt/components.py | 30 +++++------ flixopt/core.py | 10 ++-- flixopt/effects.py | 88 ++++++++++++++++++++++++------- flixopt/elements.py | 20 ++++---- flixopt/features.py | 5 +- flixopt/interface.py | 26 +++++----- flixopt/types.py | 117 +++++++++++++++++++++++++++++++++++++----- 8 files changed, 224 insertions(+), 77 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index f744d05a3..47838745a 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -41,8 +41,11 @@ solvers, ) +# Effect-specific types +from .effects import PeriodicEffectsUser, TemporalEffectsUser + # Type system for dimension-aware type hints -from .types import BoolData, Data, NumericData, Period, Scalar, Scenario, Time +from .types import BoolData, Data, EffectData, NumericData, Period, Scalar, Scenario, Time # === Runtime warning suppression for third-party libraries === # These warnings are from dependencies and cannot be fixed by end users. diff --git a/flixopt/components.py b/flixopt/components.py index b91f219d9..2c0239559 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -23,7 +23,7 @@ import linopy from .flow_system import FlowSystem - from .types import Data, Period, Scenario, Time + from .types import Data, NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -170,7 +170,7 @@ def __init__( inputs: list[Flow], outputs: list[Flow], on_off_parameters: OnOffParameters | None = None, - conversion_factors: list[dict[str, Data[Time, Scenario]]] | None = None, + conversion_factors: list[dict[str, NumericData[Time, Scenario]]] | None = None, piecewise_conversion: PiecewiseConversion | None = None, meta_data: dict | None = None, ): @@ -387,17 +387,17 @@ def __init__( label: str, charging: Flow, discharging: Flow, - capacity_in_flow_hours: Data[Period, Scenario] | InvestParameters, - relative_minimum_charge_state: Data[Time, Period, Scenario] = 0, - relative_maximum_charge_state: Data[Time, Period, Scenario] = 1, - initial_charge_state: Data[Period, Scenario] | Literal['lastValueOfSim'] = 0, - minimal_final_charge_state: Data[Period, Scenario] | None = None, - maximal_final_charge_state: Data[Period, Scenario] | None = None, - relative_minimum_final_charge_state: Data[Period, Scenario] | None = None, - relative_maximum_final_charge_state: Data[Period, Scenario] | None = None, - eta_charge: Data[Time, Period, Scenario] = 1, - eta_discharge: Data[Time, Period, Scenario] = 1, - relative_loss_per_hour: Data[Time, Period, Scenario] = 0, + capacity_in_flow_hours: NumericData[Period, Scenario] | InvestParameters, + relative_minimum_charge_state: NumericData[Time, Period, Scenario] = 0, + relative_maximum_charge_state: NumericData[Time, Period, Scenario] = 1, + initial_charge_state: NumericData[Period, Scenario] | Literal['lastValueOfSim'] = 0, + minimal_final_charge_state: NumericData[Period, Scenario] | None = None, + maximal_final_charge_state: NumericData[Period, Scenario] | None = None, + relative_minimum_final_charge_state: NumericData[Period, Scenario] | None = None, + relative_maximum_final_charge_state: NumericData[Period, Scenario] | None = None, + eta_charge: NumericData[Time, Period, Scenario] = 1, + eta_discharge: NumericData[Time, Period, Scenario] = 1, + relative_loss_per_hour: NumericData[Time, Period, Scenario] = 0, prevent_simultaneous_charge_and_discharge: bool = True, balanced: bool = False, meta_data: dict | None = None, @@ -664,8 +664,8 @@ def __init__( out1: Flow, in2: Flow | None = None, out2: Flow | None = None, - relative_losses: Data[Time, Period, Scenario] | None = None, - absolute_losses: Data[Time, Period, Scenario] | None = None, + relative_losses: NumericData[Time, Period, Scenario] | None = None, + absolute_losses: NumericData[Time, Period, Scenario] | None = None, on_off_parameters: OnOffParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, balanced: bool = False, diff --git a/flixopt/core.py b/flixopt/core.py index 1b8e1a660..cba519223 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -12,21 +12,21 @@ import pandas as pd import xarray as xr -from flixopt.types import Data, Period, Scalar, Scenario, Time +from flixopt.types import Data, NumericData, Period, Scalar, Scenario, Time logger = logging.getLogger('flixopt') # Legacy type aliases (kept for backward compatibility) -# These are being replaced by dimension-aware Data[...] types +# These are being replaced by dimension-aware NumericData[...] types Scalar = Scalar """A single number, either integer or float.""" -PeriodicDataUser = Data[Period, Scenario] +PeriodicDataUser = NumericData[Period, Scenario] """ User data which has no time dimension. Internally converted to a Scalar or an xr.DataArray without a time dimension. .. deprecated:: - Use dimension-aware types instead: `Data[Period, Scenario]` or `Data[Scenario]` + Use dimension-aware types instead: `NumericData[Period, Scenario]` or `NumericData[Scenario]` """ PeriodicData = xr.DataArray @@ -166,7 +166,7 @@ def agg_weight(self): User data which might have a time dimension. Internally converted to an xr.DataArray with time dimension. .. deprecated:: - Use dimension-aware types instead: `Data[Time]`, `Data[Time, Scenario]`, or `Data[Time, Period, Scenario]` + Use dimension-aware types instead: `NumericData[Time]`, `NumericData[Time, Scenario]`, or `NumericData[Time, Period, Scenario]` """ TemporalData = xr.DataArray | TimeSeriesData diff --git a/flixopt/effects.py b/flixopt/effects.py index 9c388fe2c..20e970dda 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -20,7 +20,7 @@ from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io -from .types import Data, Period, Scenario, Time +from .types import Data, EffectData, NumericData, Period, Scenario, Time if TYPE_CHECKING: from collections.abc import Iterator @@ -53,17 +53,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: `TemporalEffectsUser` (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: `PeriodicEffectsUser` (single value or dict with dimensions [Period, Scenario]) minimum_temporal: Minimum allowed total contribution across all timesteps. + Type: `NumericData[Period, Scenario]` (sum over time, can vary by period/scenario) maximum_temporal: Maximum allowed total contribution across all timesteps. + Type: `NumericData[Period, Scenario]` (sum over time, can vary by period/scenario) minimum_per_hour: Minimum allowed contribution per hour. + Type: `NumericData[Time, Period, Scenario]` (per-timestep constraint, can vary by period) maximum_per_hour: Maximum allowed contribution per hour. + Type: `NumericData[Time, Period, Scenario]` (per-timestep constraint, can vary by period) minimum_periodic: Minimum allowed total periodic contribution. + Type: `NumericData[Period, Scenario]` (periodic constraint) maximum_periodic: Maximum allowed total periodic contribution. + Type: `NumericData[Period, Scenario]` (periodic constraint) minimum_total: Minimum allowed total effect (temporal + periodic combined). + Type: `NumericData[Period, Scenario]` (total constraint per period) maximum_total: Maximum allowed total effect (temporal + periodic combined). + Type: `NumericData[Period, Scenario]` (total constraint per period) meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -173,14 +183,14 @@ def __init__( 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, + minimum_temporal: NumericData[Period, Scenario] | None = None, + maximum_temporal: NumericData[Period, Scenario] | None = None, + minimum_periodic: NumericData[Period, Scenario] | None = None, + maximum_periodic: NumericData[Period, Scenario] | None = None, + minimum_per_hour: NumericData[Time, Period, Scenario] | None = None, + maximum_per_hour: NumericData[Time, Period, Scenario] | None = None, + minimum_total: NumericData[Period, Scenario] | None = None, + maximum_total: NumericData[Period, Scenario] | None = None, **kwargs, ): super().__init__(label, meta_data=meta_data) @@ -437,22 +447,64 @@ def _do_modeling(self): ) -TemporalEffectsUser = Data[Time, Period, Scenario] | dict[str, Data[Time, Scenario]] # User-specified Shares to Effects +TemporalEffectsUser = NumericData[Time, Period, Scenario] | dict[str, NumericData[Time, Period, Scenario]] """ -This datatype is used to define a temporal share to an effect by a certain attribute. +Temporal effects data: numeric values that can vary with time, periods, and scenarios. Can be: -- A single value (scalar, array, Series, DataFrame, DataArray) with at most [Time, Scenario] dimensions -- A dictionary mapping effect names to values with at most [Time, Scenario] dimensions +- A single numeric value (scalar, array, Series, DataFrame, DataArray) with at most [Time, Period, Scenario] dimensions + → Applied to the standard effect +- A dictionary mapping effect names to numeric values with at most [Time, Period, Scenario] dimensions + → Applied to named effects (e.g., {'costs': 10, 'CO2': 0.5}) + +Dimensions: +- Time: Hourly/timestep variation (e.g., varying electricity prices) +- Period: Multi-period planning horizon (e.g., costs in different years) +- Scenario: Scenario-based variation (e.g., high/low price scenarios) + +Note: Data can have any subset of these dimensions - scalars, 1D, 2D, or 3D arrays. + +Examples: + >>> # Single value for standard effect (broadcast to all dimensions) + >>> effects_per_flow_hour = 10.5 + >>> + >>> # Time-varying costs (same across periods and scenarios) + >>> effects_per_flow_hour = np.array([10, 12, 11, 10]) + >>> + >>> # Multiple effects with different dimensions + >>> effects_per_flow_hour = { + ... 'costs': 10.5, # Scalar + ... 'CO2': np.array([0.3, 0.4, 0.3]), # Time-varying + ... } """ -PeriodicEffectsUser = Data[Period, Scenario] | dict[str, Data[Period, Scenario]] # User-specified Shares to Effects +PeriodicEffectsUser = NumericData[Period, Scenario] | dict[str, NumericData[Period, Scenario]] """ -This datatype is used to define a periodic share to an effect by a certain attribute. +Periodic effects data: numeric values that can vary with planning periods and scenarios (no time dimension). Can be: -- A single value (scalar, array, Series, DataFrame, DataArray) with at most [Period, Scenario] dimensions -- A dictionary mapping effect names to values with at most [Period, Scenario] dimensions +- A single numeric value (scalar, array, Series, DataFrame, DataArray) with at most [Period, Scenario] dimensions + → Applied to the standard effect +- A dictionary mapping effect names to numeric values with at most [Period, Scenario] dimensions + → Applied to named effects (e.g., {'costs': 1000, 'CO2': 50}) + +Typical uses: +- Investment costs (vary by period but not time) +- Fixed operating costs (per period) +- Retirement effects + +Examples: + >>> # Fixed cost for investment + >>> effects_of_investment = 1000 + >>> + >>> # Period-varying costs (e.g., different years) + >>> effects_of_investment = np.array([1000, 1200, 1100]) # Years 2020, 2025, 2030 + >>> + >>> # Multiple periodic effects + >>> effects_of_investment = { + ... 'costs': 1000, + ... 'CO2': 50, + ... } """ TemporalEffects = dict[str, TemporalData] # User-specified Shares to Effects diff --git a/flixopt/elements.py b/flixopt/elements.py index aa18a67ab..d9282ddc0 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -24,7 +24,7 @@ from .effects import TemporalEffectsUser from .flow_system import FlowSystem - from .types import Data, Period, Scenario, Time + from .types import Data, NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -420,17 +420,17 @@ def __init__( self, label: str, bus: str, - size: Scalar | InvestParameters = None, - fixed_relative_profile: Data[Time, Period, Scenario] | None = None, - relative_minimum: Data[Time, Period, Scenario] = 0, - relative_maximum: Data[Time, Period, Scenario] = 1, + size: NumericData[Period, Scenario] | InvestParameters = None, + fixed_relative_profile: NumericData[Time, Period, Scenario] | None = None, + relative_minimum: NumericData[Time, Period, Scenario] = 0, + relative_maximum: NumericData[Time, Period, Scenario] = 1, effects_per_flow_hour: TemporalEffectsUser | None = None, on_off_parameters: OnOffParameters | None = None, - flow_hours_total_max: Scalar | None = None, - flow_hours_total_min: Scalar | None = None, - load_factor_min: Scalar | None = None, - load_factor_max: Scalar | None = None, - previous_flow_rate: Scalar | list[Scalar] | None = None, + flow_hours_total_max: NumericData[Period, Scenario] | None = None, + flow_hours_total_min: NumericData[Period, Scenario] | None = None, + load_factor_min: NumericData[Period, Scenario] | None = None, + load_factor_max: NumericData[Period, Scenario] | None = None, + previous_flow_rate: NumericData[Period, Scenario] | list[Scalar] | None = None, meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) diff --git a/flixopt/features.py b/flixopt/features.py index 0d1fc7784..e6d400556 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from .core import FlowSystemDimensions, Scalar, TemporalData from .interface import InvestParameters, OnOffParameters, Piecewise + from .types import NumericData, Period, Scenario logger = logging.getLogger('flixopt') @@ -517,8 +518,8 @@ def __init__( dims: list[FlowSystemDimensions], label_of_element: str | None = None, label_of_model: str | None = None, - total_max: Scalar | None = None, - total_min: Scalar | None = None, + total_max: NumericData[Period, Scenario] | None = None, + total_min: NumericData[Period, Scenario] | None = None, max_per_hour: TemporalData | None = None, min_per_hour: TemporalData | None = None, ): diff --git a/flixopt/interface.py b/flixopt/interface.py index 1ff30b933..3f72f8122 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -22,7 +22,7 @@ from .core import PeriodicData, PeriodicDataUser, Scalar, TemporalDataUser from .effects import PeriodicEffectsUser, TemporalEffectsUser from .flow_system import FlowSystem - from .types import Data, Period, Scenario, Time + from .types import Data, NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -74,7 +74,7 @@ class Piece(Interface): """ - def __init__(self, start: Data[Time, Period, Scenario], end: Data[Time, Period, Scenario]): + def __init__(self, start: NumericData[Time, Period, Scenario], end: NumericData[Time, Period, Scenario]): self.start = start self.end = end self.has_time_dim = False @@ -875,15 +875,15 @@ class InvestParameters(Interface): def __init__( self, - fixed_size: Data[Period, Scenario] | None = None, - minimum_size: Data[Period, Scenario] | None = None, - maximum_size: Data[Period, Scenario] | None = None, + fixed_size: NumericData[Period, Scenario] | None = None, + minimum_size: NumericData[Period, Scenario] | None = None, + maximum_size: NumericData[Period, Scenario] | None = None, mandatory: bool = False, effects_of_investment: PeriodicEffectsUser | None = None, effects_of_investment_per_size: PeriodicEffectsUser | None = None, effects_of_retirement: PeriodicEffectsUser | None = None, piecewise_effects_of_investment: PiecewiseEffects | None = None, - linked_periods: Data[Period, Scenario] | tuple[int, int] | None = None, + linked_periods: NumericData[Period, Scenario] | tuple[int, int] | None = None, **kwargs, ): # Handle deprecated parameters using centralized helper @@ -1273,10 +1273,10 @@ def __init__( effects_per_running_hour: TemporalEffectsUser | None = None, on_hours_total_min: int | None = None, on_hours_total_max: int | None = None, - consecutive_on_hours_min: Data[Time, Period, Scenario] | None = None, - consecutive_on_hours_max: Data[Time, Period, Scenario] | None = None, - consecutive_off_hours_min: Data[Time, Period, Scenario] | None = None, - consecutive_off_hours_max: Data[Time, Period, Scenario] | None = None, + consecutive_on_hours_min: NumericData[Time, Period, Scenario] | None = None, + consecutive_on_hours_max: NumericData[Time, Period, Scenario] | None = None, + consecutive_off_hours_min: NumericData[Time, Period, Scenario] | None = None, + consecutive_off_hours_max: NumericData[Time, Period, Scenario] | None = None, switch_on_total_max: int | None = None, force_switch_on: bool = False, ): @@ -1286,13 +1286,13 @@ def __init__( self.effects_per_running_hour: TemporalEffectsUser = ( effects_per_running_hour if effects_per_running_hour is not None else {} ) - self.on_hours_total_min: Scalar = on_hours_total_min - self.on_hours_total_max: Scalar = on_hours_total_max + self.on_hours_total_min: NumericData[Period, Scenario] = on_hours_total_min + self.on_hours_total_max: NumericData[Period, Scenario] = on_hours_total_max self.consecutive_on_hours_min: TemporalDataUser = consecutive_on_hours_min self.consecutive_on_hours_max: TemporalDataUser = consecutive_on_hours_max self.consecutive_off_hours_min: TemporalDataUser = consecutive_off_hours_min self.consecutive_off_hours_max: TemporalDataUser = consecutive_off_hours_max - self.switch_on_total_max: Scalar = switch_on_total_max + self.switch_on_total_max: NumericData[Period, Scenario] = switch_on_total_max self.force_switch_on: bool = force_switch_on def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: diff --git a/flixopt/types.py b/flixopt/types.py index 58bd61cd9..2f3a2754d 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -8,26 +8,26 @@ Key Concepts ------------ - Dimension markers (`Time`, `Period`, `Scenario`) represent the possible dimensions -- `Data[...]` generic type indicates the **maximum** dimensions data can have +- `NumericData[...]` generic type indicates the **maximum** dimensions data can have - Data can have any subset of the specified dimensions (including being scalar) - All standard input formats are supported (scalar, array, Series, DataFrame, DataArray) Examples -------- -Type hint `Data[Time]` accepts: +Type hint `NumericData[Time]` accepts: - Scalar: `0.5` (broadcast to all timesteps) - 1D array: `np.array([1, 2, 3])` (matched to time dimension) - pandas Series: with DatetimeIndex matching flow system - xarray DataArray: with 'time' dimension -Type hint `Data[Time, Scenario]` accepts: +Type hint `NumericData[Time, Scenario]` accepts: - Scalar: `100` (broadcast to all time and scenario combinations) - 1D array: matched to time OR scenario dimension - 2D array: matched to both dimensions - pandas DataFrame: columns as scenarios, index as time - xarray DataArray: with any subset of 'time', 'scenario' dimensions -Type hint `Data[Period, Scenario]` (periodic data, no time): +Type hint `NumericData[Period, Scenario]` (periodic data, no time): - Used for investment parameters that vary by planning period - Accepts scalars, arrays matching periods/scenarios, or DataArrays @@ -36,7 +36,7 @@ - Not converted to DataArray, stays as scalar """ -from typing import Any, TypeAlias +from typing import Any, TypeAlias, Union import numpy as np import pandas as pd @@ -63,15 +63,15 @@ class Scenario: class _NumericDataMeta(type): - """Metaclass for Data to enable subscript notation Data[Time, Scenario] for numeric data.""" + """Metaclass for Data to enable subscript notation NumericData[Time, Scenario] for numeric data.""" def __getitem__(cls, dimensions): """ Create a type hint showing maximum dimensions for numeric data. The dimensions parameter can be: - - A single dimension: Data[Time] - - Multiple dimensions: Data[Time, Period, Scenario] + - A single dimension: NumericData[Time] + - Multiple dimensions: NumericData[Time, Period, Scenario] The type hint communicates that data can have **at most** these dimensions. Actual data can be: @@ -86,8 +86,9 @@ def __getitem__(cls, dimensions): # of which dimensions are specified. The dimension parameters serve # as documentation rather than runtime validation. - # Return type that includes all possible numeric input formats - return int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray + # Return Union[] for better type checker compatibility (especially with | None) + # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax + return Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] # noqa: UP007 class _BoolDataMeta(type): @@ -99,8 +100,24 @@ def __getitem__(cls, dimensions): Same semantics as numeric Data, but for boolean values. """ - # Return type that includes all possible boolean input formats - return bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray + # Return Union[] for better type checker compatibility (especially with | None) + # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax + return Union[bool, np.bool_, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] # noqa: UP007 + + +class _EffectDataMeta(type): + """Metaclass for EffectData to enable subscript notation EffectData[Time, Period, Scenario] for effect data.""" + + def __getitem__(cls, dimensions): + """ + Create a type hint showing maximum dimensions for effect data. + + Effect data is numeric data specifically for effects, with full dimensional support. + Same as NumericData but semantically distinct for effect-related parameters. + """ + # Return Union[] for better type checker compatibility (especially with | None) + # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax + return Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] # noqa: UP007 class Data(metaclass=_NumericDataMeta): @@ -222,7 +239,80 @@ def __init__(self): raise TypeError('BoolData is a type hint only and cannot be instantiated') -# Public alias for Data (for clarity and symmetry with BoolData) +class EffectData(metaclass=_EffectDataMeta): + """ + Generic type for effect data that can have various dimensions. + + EffectData is semantically identical to NumericData but specifically intended for + effect-related parameters. It supports the full dimensional space including Time, + Period, and Scenario dimensions, making it ideal for effect contributions, constraints, + and cross-effect relationships. + + Use subscript notation to specify the maximum dimensions: + - `EffectData[Time]`: Time-varying effect data + - `EffectData[Period, Scenario]`: Periodic effect data + - `EffectData[Time, Period, Scenario]`: Full dimensional effect data + + Semantics: "At Most" Dimensions + -------------------------------- + When you see `EffectData[Time, Period, Scenario]`, it means the data can have: + - No dimensions (scalar): broadcast to all time, period, and scenario values + - Any subset: just time, just period, just scenario, time+period, etc. + - All dimensions: full 3D data + + Accepted Input Formats (Numeric) + --------------------------------- + All dimension combinations accept these formats: + - Scalars: int, float (including numpy types) + - Arrays: numpy ndarray with numeric dtype (matched by length/shape to dimensions) + - pandas Series: matched by index to dimension coordinates + - pandas DataFrame: typically columns=scenarios/periods, index=time + - xarray DataArray: used directly with dimension validation + + Typical Use Cases + ----------------- + - Effect contributions varying by time, period, and scenario + - Per-hour constraints that tighten over planning periods + - Cross-effect pricing (e.g., escalating carbon prices) + - Multi-period optimization with temporal detail + + Examples + -------- + >>> # Scalar effect cost (broadcast to all dimensions) + >>> cost: EffectData[Time, Period, Scenario] = 10.5 + >>> + >>> # Time-varying emissions + >>> emissions: EffectData[Time, Period, Scenario] = np.array([100, 120, 110]) + >>> + >>> # Period-varying carbon price (escalating over years) + >>> carbon_price: EffectData[Period] = np.array([0.1, 0.2, 0.3]) # €/kg in 2020, 2025, 2030 + >>> + >>> # Full 3D effect data + >>> import xarray as xr + >>> full_data: EffectData[Time, Period, Scenario] = xr.DataArray( + ... data=np.random.rand(24, 3, 2), # 24 hours × 3 periods × 2 scenarios + ... dims=['time', 'period', 'scenario'], + ... ) + + Note + ---- + EffectData is functionally identical to NumericData. The distinction is semantic: + use EffectData for effect-related parameters to make code intent clearer. + + See Also + -------- + NumericData : General numeric data with dimensions + BoolData : For boolean data with dimensions + TemporalEffectsUser : Effect type for temporal contributions (dict or single value) + PeriodicEffectsUser : Effect type for periodic contributions (dict or single value) + """ + + # This class is not meant to be instantiated, only used for type hints + def __init__(self): + raise TypeError('EffectData is a type hint only and cannot be instantiated') + + +# Public alias for Data (for clarity and symmetry with BoolData and EffectData) NumericData = Data """Public type for numeric data with dimensions. Alias for the internal `Data` class.""" @@ -233,6 +323,7 @@ def __init__(self): __all__ = [ 'NumericData', # Primary public type for numeric data 'BoolData', # Primary public type for boolean data + 'EffectData', # Primary public type for effect data (semantic variant of NumericData) 'Data', # Also exported (internal base class, can be used as shorthand) 'Time', 'Period', From d8bf7f22126ea8f63db29bd2a874c1c94c13e53c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:56:15 +0100 Subject: [PATCH 06/34] EffectData Type - Complete Redesign --- flixopt/components.py | 10 ++--- flixopt/core.py | 37 +++++----------- flixopt/effects.py | 12 +++-- flixopt/elements.py | 2 +- flixopt/flow_system.py | 12 +++-- flixopt/interface.py | 13 +++--- flixopt/linear_converters.py | 23 +++++----- flixopt/types.py | 85 ++++++++++++++++++++++++------------ 8 files changed, 104 insertions(+), 90 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 2c0239559..fd060283c 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -414,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: NumericData[Time, Period, Scenario] = relative_minimum_charge_state + self.relative_maximum_charge_state: NumericData[Time, Period, Scenario] = 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 @@ -424,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: NumericData[Time, Period, Scenario] = eta_charge + self.eta_discharge: NumericData[Time, Period, Scenario] = eta_discharge + self.relative_loss_per_hour: NumericData[Time, Period, Scenario] = relative_loss_per_hour self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge self.balanced = balanced diff --git a/flixopt/core.py b/flixopt/core.py index cba519223..8f9cc8827 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -16,22 +16,6 @@ logger = logging.getLogger('flixopt') -# Legacy type aliases (kept for backward compatibility) -# These are being replaced by dimension-aware NumericData[...] types -Scalar = Scalar -"""A single number, either integer or float.""" - -PeriodicDataUser = NumericData[Period, Scenario] -""" -User data which has no time dimension. Internally converted to a Scalar or an xr.DataArray without a time dimension. - -.. deprecated:: - Use dimension-aware types instead: `NumericData[Period, Scenario]` or `NumericData[Scenario]` -""" - -PeriodicData = xr.DataArray -"""Internally used datatypes for periodic data.""" - FlowSystemDimensions = Literal['time', 'period', 'scenario'] """Possible dimensions of a FlowSystem.""" @@ -159,14 +143,19 @@ def agg_weight(self): return self.aggregation_weight -TemporalDataUser = ( - int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray | TimeSeriesData -) +TemporalDataUser = NumericData[Time, Scenario] """ User data which might have a time dimension. Internally converted to an xr.DataArray with time dimension. -.. deprecated:: - Use dimension-aware types instead: `NumericData[Time]`, `NumericData[Time, Scenario]`, or `NumericData[Time, Period, Scenario]` +Supports data with at most [Time, Scenario] dimensions. For periodic data (no time dimension), use PeriodicDataUser. +For data with all three dimensions [Time, Period, Scenario], use NumericData[Time, Period, Scenario] directly. +""" + +PeriodicDataUser = NumericData[Period, Scenario] +""" +User data for periodic parameters (no time dimension). Internally converted to an xr.DataArray. + +Supports data with at most [Period, Scenario] dimensions. For temporal data (with time), use TemporalDataUser. """ TemporalData = xr.DataArray | TimeSeriesData @@ -651,9 +640,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 diff --git a/flixopt/effects.py b/flixopt/effects.py index 20e970dda..a58ce94e3 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -17,7 +17,7 @@ import xarray as xr from . import io as fx_io -from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser +from .core import Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io from .types import Data, EffectData, NumericData, Period, Scenario, Time @@ -447,10 +447,12 @@ def _do_modeling(self): ) -TemporalEffectsUser = NumericData[Time, Period, Scenario] | dict[str, NumericData[Time, Period, Scenario]] +TemporalEffectsUser = EffectData[Time, Period, Scenario] """ Temporal effects data: numeric values that can vary with time, periods, and scenarios. +Type: `EffectData[Time, Period, Scenario]` = `NumericData[Time, Period, Scenario] | dict[str, NumericData[Time, Period, Scenario]]` + Can be: - A single numeric value (scalar, array, Series, DataFrame, DataArray) with at most [Time, Period, Scenario] dimensions → Applied to the standard effect @@ -478,10 +480,12 @@ def _do_modeling(self): ... } """ -PeriodicEffectsUser = NumericData[Period, Scenario] | dict[str, NumericData[Period, Scenario]] +PeriodicEffectsUser = EffectData[Period, Scenario] """ Periodic effects data: numeric values that can vary with planning periods and scenarios (no time dimension). +Type: `EffectData[Period, Scenario]` = `NumericData[Period, Scenario] | dict[str, NumericData[Period, Scenario]]` + Can be: - A single numeric value (scalar, array, Series, DataFrame, DataArray) with at most [Period, Scenario] dimensions → Applied to the standard effect @@ -556,7 +560,7 @@ def add_effects(self, *effects: Effect) -> None: def create_effect_values_dict( self, effect_values_user: PeriodicEffectsUser | TemporalEffectsUser - ) -> dict[str, Scalar | TemporalDataUser] | None: + ) -> dict[str, Scalar | NumericData[Time, Period, Scenario]] | None: """Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. Examples: diff --git a/flixopt/elements.py b/flixopt/elements.py index d9282ddc0..16cc2513e 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -229,7 +229,7 @@ class Bus(Element): def __init__( self, label: str, - excess_penalty_per_flow_hour: TemporalDataUser | None = 1e5, + excess_penalty_per_flow_hour: NumericData[Time, Period, Scenario] | None = 1e5, meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 1fc280226..250f3fc20 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -20,10 +20,6 @@ ConversionError, DataConverter, FlowSystemDimensions, - PeriodicData, - PeriodicDataUser, - TemporalData, - TemporalDataUser, TimeSeriesData, ) from .effects import ( @@ -43,6 +39,8 @@ import pyvis + from .types import Data, Period, Scenario, Time + logger = logging.getLogger('flixopt') @@ -168,7 +166,7 @@ def __init__( scenarios: pd.Index | None = None, hours_of_last_timestep: int | float | None = None, hours_of_previous_timesteps: int | float | np.ndarray | None = None, - weights: PeriodicDataUser | None = None, + weights: Data[Period, Scenario] | None = None, scenario_independent_sizes: bool | list[str] = True, scenario_independent_flow_rates: bool | list[str] = False, ): @@ -532,9 +530,9 @@ def to_json(self, path: str | pathlib.Path): def fit_to_model_coords( self, name: str, - data: TemporalDataUser | PeriodicDataUser | None, + data: Data[Time, Period, Scenario] | None, dims: Collection[FlowSystemDimensions] | None = None, - ) -> TemporalData | PeriodicData | None: + ) -> xr.DataArray | None: """ Fit data to model coordinate system (currently time, but extensible). diff --git a/flixopt/interface.py b/flixopt/interface.py index 3f72f8122..5cc2d2683 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -19,7 +19,6 @@ if TYPE_CHECKING: # for type checking and preventing circular imports from collections.abc import Iterator - from .core import PeriodicData, PeriodicDataUser, Scalar, TemporalDataUser from .effects import PeriodicEffectsUser, TemporalEffectsUser from .flow_system import FlowSystem from .types import Data, NumericData, Period, Scenario, Time @@ -1045,11 +1044,11 @@ def piecewise_effects(self) -> PiecewiseEffects | None: return self.piecewise_effects_of_investment @property - def minimum_or_fixed_size(self) -> PeriodicData: + def minimum_or_fixed_size(self) -> Data[Period, Scenario]: return self.fixed_size if self.fixed_size is not None else self.minimum_size @property - def maximum_or_fixed_size(self) -> PeriodicData: + def maximum_or_fixed_size(self) -> Data[Period, Scenario]: return self.fixed_size if self.fixed_size is not None else self.maximum_size def format_for_repr(self) -> str: @@ -1288,10 +1287,10 @@ def __init__( ) self.on_hours_total_min: NumericData[Period, Scenario] = on_hours_total_min self.on_hours_total_max: NumericData[Period, Scenario] = on_hours_total_max - self.consecutive_on_hours_min: TemporalDataUser = consecutive_on_hours_min - self.consecutive_on_hours_max: TemporalDataUser = consecutive_on_hours_max - self.consecutive_off_hours_min: TemporalDataUser = consecutive_off_hours_min - self.consecutive_off_hours_max: TemporalDataUser = consecutive_off_hours_max + self.consecutive_on_hours_min: NumericData[Time, Period, Scenario] = consecutive_on_hours_min + self.consecutive_on_hours_max: NumericData[Time, Period, Scenario] = consecutive_on_hours_max + self.consecutive_off_hours_min: NumericData[Time, Period, Scenario] = consecutive_off_hours_min + self.consecutive_off_hours_max: NumericData[Time, Period, Scenario] = consecutive_off_hours_max self.switch_on_total_max: NumericData[Period, Scenario] = switch_on_total_max self.force_switch_on: bool = force_switch_on diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 47c545506..661cdc030 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -10,12 +10,13 @@ import numpy as np from .components import LinearConverter -from .core import TemporalDataUser, TimeSeriesData +from .core import TimeSeriesData from .structure import register_class_for_io if TYPE_CHECKING: from .elements import Flow from .interface import OnOffParameters + from .types import NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -76,7 +77,7 @@ class Boiler(LinearConverter): def __init__( self, label: str, - eta: TemporalDataUser, + eta: NumericData[Time, Period, Scenario], Q_fu: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -163,7 +164,7 @@ class Power2Heat(LinearConverter): def __init__( self, label: str, - eta: TemporalDataUser, + eta: NumericData[Time, Period, Scenario], P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -250,7 +251,7 @@ class HeatPump(LinearConverter): def __init__( self, label: str, - COP: TemporalDataUser, + COP: NumericData[Time, Period, Scenario], P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -339,7 +340,7 @@ class CoolingTower(LinearConverter): def __init__( self, label: str, - specific_electricity_demand: TemporalDataUser, + specific_electricity_demand: NumericData[Time, Period, Scenario], P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -437,8 +438,8 @@ class CHP(LinearConverter): def __init__( self, label: str, - eta_th: TemporalDataUser, - eta_el: TemporalDataUser, + eta_th: NumericData[Time, Period, Scenario], + eta_el: NumericData[Time, Period, Scenario], Q_fu: Flow, P_el: Flow, Q_th: Flow, @@ -551,7 +552,7 @@ class HeatPumpWithSource(LinearConverter): def __init__( self, label: str, - COP: TemporalDataUser, + COP: NumericData[Time, Period, Scenario], P_el: Flow, Q_ab: Flow, Q_th: Flow, @@ -589,11 +590,11 @@ def COP(self, value): # noqa: N802 def check_bounds( - value: TemporalDataUser, + value: NumericData[Time, Period, Scenario], parameter_label: str, element_label: str, - lower_bound: TemporalDataUser, - upper_bound: TemporalDataUser, + lower_bound: NumericData[Time, Period, Scenario], + upper_bound: NumericData[Time, Period, Scenario], ) -> None: """ Check if the value is within the bounds. The bounds are exclusive. diff --git a/flixopt/types.py b/flixopt/types.py index 2f3a2754d..5a98cd442 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -112,12 +112,27 @@ def __getitem__(cls, dimensions): """ Create a type hint showing maximum dimensions for effect data. - Effect data is numeric data specifically for effects, with full dimensional support. - Same as NumericData but semantically distinct for effect-related parameters. + Effect data can be either: + - A single numeric value (scalar, array, Series, DataFrame, DataArray) + - A dict with string keys mapping to numeric values + + This matches the pattern used for effects: either a single contribution or + a dictionary of named contributions. """ # Return Union[] for better type checker compatibility (especially with | None) # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax - return Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] # noqa: UP007 + # EffectData = NumericData | dict[str, NumericData] + return Union[ # noqa: UP007 + int, + float, + np.integer, + np.floating, + np.ndarray, + pd.Series, + pd.DataFrame, + xr.DataArray, + dict[str, Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray]], # noqa: UP007 + ] class Data(metaclass=_NumericDataMeta): @@ -241,18 +256,25 @@ def __init__(self): class EffectData(metaclass=_EffectDataMeta): """ - Generic type for effect data that can have various dimensions. + Generic type for effect data that can be a single value or a dictionary of values. - EffectData is semantically identical to NumericData but specifically intended for - effect-related parameters. It supports the full dimensional space including Time, - Period, and Scenario dimensions, making it ideal for effect contributions, constraints, - and cross-effect relationships. + EffectData represents the common pattern for effects: either a single numeric contribution + or a dictionary with string keys mapping to numeric contributions. This is useful for + specifying effects where you either have a single effect or multiple named effects. Use subscript notation to specify the maximum dimensions: - `EffectData[Time]`: Time-varying effect data - `EffectData[Period, Scenario]`: Periodic effect data - `EffectData[Time, Period, Scenario]`: Full dimensional effect data + Type Structure + -------------- + `EffectData[dims]` = `NumericData[dims] | dict[str, NumericData[dims]]` + + This means you can provide: + - A single numeric value (scalar, array, Series, DataFrame, DataArray) + - A dict mapping effect names to numeric values + Semantics: "At Most" Dimensions -------------------------------- When you see `EffectData[Time, Period, Scenario]`, it means the data can have: @@ -260,44 +282,51 @@ class EffectData(metaclass=_EffectDataMeta): - Any subset: just time, just period, just scenario, time+period, etc. - All dimensions: full 3D data - Accepted Input Formats (Numeric) - --------------------------------- - All dimension combinations accept these formats: + Accepted Input Formats + ---------------------- + Single value: - Scalars: int, float (including numpy types) - Arrays: numpy ndarray with numeric dtype (matched by length/shape to dimensions) - pandas Series: matched by index to dimension coordinates - pandas DataFrame: typically columns=scenarios/periods, index=time - xarray DataArray: used directly with dimension validation + Dictionary of values: + - dict[str, ] + Typical Use Cases ----------------- - - Effect contributions varying by time, period, and scenario - - Per-hour constraints that tighten over planning periods - - Cross-effect pricing (e.g., escalating carbon prices) - - Multi-period optimization with temporal detail + - Single effect: `EffectData[Time] = 10.5` or `np.array([10, 12, 11])` + - Multiple effects: `EffectData[Time] = {'CO2': 0.5, 'costs': 100}` + - Cross-effect relationships in Effect class + - Component effect contributions (effects_per_flow_hour, etc.) Examples -------- - >>> # Scalar effect cost (broadcast to all dimensions) - >>> cost: EffectData[Time, Period, Scenario] = 10.5 + >>> # Single scalar effect (broadcast to all dimensions) + >>> single_cost: EffectData[Time, Period, Scenario] = 10.5 >>> - >>> # Time-varying emissions - >>> emissions: EffectData[Time, Period, Scenario] = np.array([100, 120, 110]) + >>> # Single time-varying effect + >>> single_emissions: EffectData[Time] = np.array([100, 120, 110]) >>> - >>> # Period-varying carbon price (escalating over years) - >>> carbon_price: EffectData[Period] = np.array([0.1, 0.2, 0.3]) # €/kg in 2020, 2025, 2030 + >>> # Multiple named effects (dict) + >>> multiple_effects: EffectData[Time] = { + ... 'CO2': np.array([0.5, 0.6, 0.5]), + ... 'costs': 100, # scalar broadcast to all time + ... } >>> - >>> # Full 3D effect data - >>> import xarray as xr - >>> full_data: EffectData[Time, Period, Scenario] = xr.DataArray( - ... data=np.random.rand(24, 3, 2), # 24 hours × 3 periods × 2 scenarios - ... dims=['time', 'period', 'scenario'], + >>> # Cross-effect in Effect class + >>> cost_effect = Effect( + ... label='total_costs', + ... unit='€', + ... is_objective=True, + ... share_from_temporal={'CO2': 0.1}, # EffectData[Time, Period, Scenario] ... ) Note ---- - EffectData is functionally identical to NumericData. The distinction is semantic: - use EffectData for effect-related parameters to make code intent clearer. + EffectData = NumericData | dict[str, NumericData]. This pattern is specific to effects + and different from NumericData which only represents single numeric values. See Also -------- From bfc645efc2d10037d91f07586a5ba9c8455a42fc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:00:48 +0100 Subject: [PATCH 07/34] Use NumericData instead of Data --- flixopt/__init__.py | 2 +- flixopt/components.py | 2 +- flixopt/core.py | 2 +- flixopt/effects.py | 2 +- flixopt/elements.py | 2 +- flixopt/flow_system.py | 6 +++--- flixopt/interface.py | 6 +++--- flixopt/types.py | 16 +++++----------- 8 files changed, 16 insertions(+), 22 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 47838745a..3984b5394 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -45,7 +45,7 @@ from .effects import PeriodicEffectsUser, TemporalEffectsUser # Type system for dimension-aware type hints -from .types import BoolData, Data, EffectData, NumericData, Period, Scalar, Scenario, Time +from .types import BoolData, EffectData, NumericData, Period, Scalar, Scenario, Time # === Runtime warning suppression for third-party libraries === # These warnings are from dependencies and cannot be fixed by end users. diff --git a/flixopt/components.py b/flixopt/components.py index fd060283c..632bb6ee9 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -23,7 +23,7 @@ import linopy from .flow_system import FlowSystem - from .types import Data, NumericData, Period, Scenario, Time + from .types import NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') diff --git a/flixopt/core.py b/flixopt/core.py index 8f9cc8827..8ec91e2e2 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -12,7 +12,7 @@ import pandas as pd import xarray as xr -from flixopt.types import Data, NumericData, Period, Scalar, Scenario, Time +from flixopt.types import NumericData, Period, Scalar, Scenario, Time logger = logging.getLogger('flixopt') diff --git a/flixopt/effects.py b/flixopt/effects.py index a58ce94e3..6aeb98451 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -20,7 +20,7 @@ from .core import Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io -from .types import Data, EffectData, NumericData, Period, Scenario, Time +from .types import EffectData, NumericData, Period, Scenario, Time if TYPE_CHECKING: from collections.abc import Iterator diff --git a/flixopt/elements.py b/flixopt/elements.py index 16cc2513e..e97169f0a 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -24,7 +24,7 @@ from .effects import TemporalEffectsUser from .flow_system import FlowSystem - from .types import Data, NumericData, Period, Scenario, Time + from .types import NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 250f3fc20..9c195d926 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -39,7 +39,7 @@ import pyvis - from .types import Data, Period, Scenario, Time + from .types import BoolData, NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -166,7 +166,7 @@ def __init__( scenarios: pd.Index | None = None, hours_of_last_timestep: int | float | None = None, hours_of_previous_timesteps: int | float | np.ndarray | None = None, - weights: Data[Period, Scenario] | None = None, + weights: NumericData[Period, Scenario] | None = None, scenario_independent_sizes: bool | list[str] = True, scenario_independent_flow_rates: bool | list[str] = False, ): @@ -530,7 +530,7 @@ def to_json(self, path: str | pathlib.Path): def fit_to_model_coords( self, name: str, - data: Data[Time, Period, Scenario] | None, + data: NumericData[Time, Period, Scenario] | BoolData[Time, Period, Scenario] | None, dims: Collection[FlowSystemDimensions] | None = None, ) -> xr.DataArray | None: """ diff --git a/flixopt/interface.py b/flixopt/interface.py index 5cc2d2683..541357e48 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -21,7 +21,7 @@ from .effects import PeriodicEffectsUser, TemporalEffectsUser from .flow_system import FlowSystem - from .types import Data, NumericData, Period, Scenario, Time + from .types import NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -1044,11 +1044,11 @@ def piecewise_effects(self) -> PiecewiseEffects | None: return self.piecewise_effects_of_investment @property - def minimum_or_fixed_size(self) -> Data[Period, Scenario]: + def minimum_or_fixed_size(self) -> NumericData[Period, Scenario]: return self.fixed_size if self.fixed_size is not None else self.minimum_size @property - def maximum_or_fixed_size(self) -> Data[Period, Scenario]: + def maximum_or_fixed_size(self) -> NumericData[Period, Scenario]: return self.fixed_size if self.fixed_size is not None else self.maximum_size def format_for_repr(self) -> str: diff --git a/flixopt/types.py b/flixopt/types.py index 5a98cd442..a7527c835 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -63,7 +63,7 @@ class Scenario: class _NumericDataMeta(type): - """Metaclass for Data to enable subscript notation NumericData[Time, Scenario] for numeric data.""" + """Metaclass for NumericData to enable subscript notation NumericData[Time, Scenario] for numeric data.""" def __getitem__(cls, dimensions): """ @@ -98,7 +98,7 @@ def __getitem__(cls, dimensions): """ Create a type hint showing maximum dimensions for boolean data. - Same semantics as numeric Data, but for boolean values. + Same semantics as NumericData, but for boolean values. """ # Return Union[] for better type checker compatibility (especially with | None) # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax @@ -135,7 +135,7 @@ def __getitem__(cls, dimensions): ] -class Data(metaclass=_NumericDataMeta): +class NumericData(metaclass=_NumericDataMeta): """ Base type for numeric data that can have various dimensions. @@ -180,7 +180,6 @@ class Data(metaclass=_NumericDataMeta): See Also -------- - NumericData : Public alias for this class BoolData : For boolean data with dimensions DataConverter.to_dataarray : The conversion implementation FlowSystem.fit_to_model_coords : Fits data to the model's coordinate system @@ -188,7 +187,7 @@ class Data(metaclass=_NumericDataMeta): # This class is not meant to be instantiated, only used for type hints def __init__(self): - raise TypeError('Data is a type hint only and cannot be instantiated') + raise TypeError('NumericData is a type hint only and cannot be instantiated') class BoolData(metaclass=_BoolDataMeta): @@ -202,7 +201,7 @@ class BoolData(metaclass=_BoolDataMeta): Semantics: "At Most" Dimensions -------------------------------- - Same semantics as Data, but for boolean values. + Same semantics as NumericData, but for boolean values. When you see `BoolData[Time, Scenario]`, the data can have: - No dimensions (scalar bool): broadcast to all time and scenario values - Just 'time': broadcast across scenarios @@ -341,10 +340,6 @@ def __init__(self): raise TypeError('EffectData is a type hint only and cannot be instantiated') -# Public alias for Data (for clarity and symmetry with BoolData and EffectData) -NumericData = Data -"""Public type for numeric data with dimensions. Alias for the internal `Data` class.""" - # Simple scalar type for dimension-less numeric values Scalar: TypeAlias = int | float | np.integer | np.floating @@ -353,7 +348,6 @@ def __init__(self): 'NumericData', # Primary public type for numeric data 'BoolData', # Primary public type for boolean data 'EffectData', # Primary public type for effect data (semantic variant of NumericData) - 'Data', # Also exported (internal base class, can be used as shorthand) 'Time', 'Period', 'Scenario', From 2a06dfb2346a90de992d02d2a628affb80ada918 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:12:56 +0100 Subject: [PATCH 08/34] Update type hints --- flixopt/__init__.py | 3 -- flixopt/core.py | 2 +- flixopt/effects.py | 95 +++++------------------------------------- flixopt/elements.py | 5 +-- flixopt/flow_system.py | 15 ++----- flixopt/interface.py | 39 +++++++---------- flixopt/types.py | 23 ++-------- 7 files changed, 37 insertions(+), 145 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 3984b5394..52560bc85 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -41,9 +41,6 @@ solvers, ) -# Effect-specific types -from .effects import PeriodicEffectsUser, TemporalEffectsUser - # Type system for dimension-aware type hints from .types import BoolData, EffectData, NumericData, Period, Scalar, Scenario, Time diff --git a/flixopt/core.py b/flixopt/core.py index 8ec91e2e2..406a6ea7c 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -12,7 +12,7 @@ import pandas as pd import xarray as xr -from flixopt.types import NumericData, Period, Scalar, Scenario, Time +from flixopt.types import NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') diff --git a/flixopt/effects.py b/flixopt/effects.py index 6aeb98451..120158b09 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -16,16 +16,14 @@ import numpy as np import xarray as xr -from . import io as fx_io -from .core import Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io -from .types import EffectData, NumericData, Period, Scenario, Time if TYPE_CHECKING: from collections.abc import Iterator from .flow_system import FlowSystem + from .types import EffectData, NumericData, Period, Scalar, Scenario, Time logger = logging.getLogger('flixopt') @@ -181,8 +179,8 @@ 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, + share_from_temporal: EffectData[Time, Period, Scenario] | None = None, + share_from_periodic: EffectData[Period, Scenario] | None = None, minimum_temporal: NumericData[Period, Scenario] | None = None, maximum_temporal: NumericData[Period, Scenario] | None = None, minimum_periodic: NumericData[Period, Scenario] | None = None, @@ -198,8 +196,12 @@ def __init__( 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: EffectData[Time, Period, Scenario] = ( + share_from_temporal if share_from_temporal is not None else {} + ) + self.share_from_periodic: EffectData[Period, Scenario] = ( + 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( @@ -447,76 +449,6 @@ def _do_modeling(self): ) -TemporalEffectsUser = EffectData[Time, Period, Scenario] -""" -Temporal effects data: numeric values that can vary with time, periods, and scenarios. - -Type: `EffectData[Time, Period, Scenario]` = `NumericData[Time, Period, Scenario] | dict[str, NumericData[Time, Period, Scenario]]` - -Can be: -- A single numeric value (scalar, array, Series, DataFrame, DataArray) with at most [Time, Period, Scenario] dimensions - → Applied to the standard effect -- A dictionary mapping effect names to numeric values with at most [Time, Period, Scenario] dimensions - → Applied to named effects (e.g., {'costs': 10, 'CO2': 0.5}) - -Dimensions: -- Time: Hourly/timestep variation (e.g., varying electricity prices) -- Period: Multi-period planning horizon (e.g., costs in different years) -- Scenario: Scenario-based variation (e.g., high/low price scenarios) - -Note: Data can have any subset of these dimensions - scalars, 1D, 2D, or 3D arrays. - -Examples: - >>> # Single value for standard effect (broadcast to all dimensions) - >>> effects_per_flow_hour = 10.5 - >>> - >>> # Time-varying costs (same across periods and scenarios) - >>> effects_per_flow_hour = np.array([10, 12, 11, 10]) - >>> - >>> # Multiple effects with different dimensions - >>> effects_per_flow_hour = { - ... 'costs': 10.5, # Scalar - ... 'CO2': np.array([0.3, 0.4, 0.3]), # Time-varying - ... } -""" - -PeriodicEffectsUser = EffectData[Period, Scenario] -""" -Periodic effects data: numeric values that can vary with planning periods and scenarios (no time dimension). - -Type: `EffectData[Period, Scenario]` = `NumericData[Period, Scenario] | dict[str, NumericData[Period, Scenario]]` - -Can be: -- A single numeric value (scalar, array, Series, DataFrame, DataArray) with at most [Period, Scenario] dimensions - → Applied to the standard effect -- A dictionary mapping effect names to numeric values with at most [Period, Scenario] dimensions - → Applied to named effects (e.g., {'costs': 1000, 'CO2': 50}) - -Typical uses: -- Investment costs (vary by period but not time) -- Fixed operating costs (per period) -- Retirement effects - -Examples: - >>> # Fixed cost for investment - >>> effects_of_investment = 1000 - >>> - >>> # Period-varying costs (e.g., different years) - >>> effects_of_investment = np.array([1000, 1200, 1100]) # Years 2020, 2025, 2030 - >>> - >>> # Multiple periodic effects - >>> effects_of_investment = { - ... 'costs': 1000, - ... 'CO2': 50, - ... } -""" - -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 @@ -559,8 +491,8 @@ def add_effects(self, *effects: Effect) -> None: logger.info(f'Registered new Effect: {effect.label}') def create_effect_values_dict( - self, effect_values_user: PeriodicEffectsUser | TemporalEffectsUser - ) -> dict[str, Scalar | NumericData[Time, Period, Scenario]] | None: + self, effect_values_user: EffectData[Time, Period, Scenario] + ) -> dict[str, NumericData[Time, Period, Scenario]] | None: """Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. Examples: @@ -920,8 +852,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 diff --git a/flixopt/elements.py b/flixopt/elements.py index e97169f0a..bc7b78b7f 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -22,9 +22,8 @@ if TYPE_CHECKING: import linopy - from .effects import TemporalEffectsUser from .flow_system import FlowSystem - from .types import NumericData, Period, Scenario, Time + from .types import EffectData, NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -424,7 +423,7 @@ def __init__( fixed_relative_profile: NumericData[Time, Period, Scenario] | None = None, relative_minimum: NumericData[Time, Period, Scenario] = 0, relative_maximum: NumericData[Time, Period, Scenario] = 1, - effects_per_flow_hour: TemporalEffectsUser | None = None, + effects_per_flow_hour: EffectData[Time, Period, Scenario] | NumericData[Time, Period, Scenario] | None = None, on_off_parameters: OnOffParameters | None = None, flow_hours_total_max: NumericData[Period, Scenario] | None = None, flow_hours_total_min: NumericData[Period, Scenario] | None = None, diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 9c195d926..aa1cec586 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -22,14 +22,7 @@ FlowSystemDimensions, TimeSeriesData, ) -from .effects import ( - Effect, - EffectCollection, - PeriodicEffects, - PeriodicEffectsUser, - TemporalEffects, - TemporalEffectsUser, -) +from .effects import Effect, EffectCollection from .elements import Bus, Component, Flow from .structure import CompositeContainerMixin, Element, ElementContainer, FlowSystemModel, Interface @@ -39,7 +32,7 @@ import pyvis - from .types import BoolData, NumericData, Period, Scenario, Time + from .types import BoolData, EffectData, NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -570,11 +563,11 @@ def fit_to_model_coords( def fit_effects_to_model_coords( self, label_prefix: str | None, - effect_values: TemporalEffectsUser | PeriodicEffectsUser | None, + effect_values: EffectData[Time, Period, Scenario] | NumericData[Time, Period, Scenario] | None, label_suffix: str | None = None, dims: Collection[FlowSystemDimensions] | None = None, delimiter: str = '|', - ) -> TemporalEffects | PeriodicEffects | None: + ) -> EffectData[Time, Period, Scenario] | None: """ Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. """ diff --git a/flixopt/interface.py b/flixopt/interface.py index 541357e48..e986bb078 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -19,9 +19,8 @@ if TYPE_CHECKING: # for type checking and preventing circular imports from collections.abc import Iterator - from .effects import PeriodicEffectsUser, TemporalEffectsUser from .flow_system import FlowSystem - from .types import NumericData, Period, Scenario, Time + from .types import EffectData, NumericData, Period, Scenario, Time logger = logging.getLogger('flixopt') @@ -878,9 +877,9 @@ def __init__( minimum_size: NumericData[Period, Scenario] | None = None, maximum_size: NumericData[Period, Scenario] | None = None, mandatory: bool = False, - effects_of_investment: PeriodicEffectsUser | None = None, - effects_of_investment_per_size: PeriodicEffectsUser | None = None, - effects_of_retirement: PeriodicEffectsUser | None = None, + effects_of_investment: EffectData[Period, Scenario] | NumericData[Period, Scenario] | None = None, + effects_of_investment_per_size: EffectData[Period, Scenario] | NumericData[Period, Scenario] | None = None, + effects_of_retirement: EffectData[Period, Scenario] | NumericData[Period, Scenario] | None = None, piecewise_effects_of_investment: PiecewiseEffects | None = None, linked_periods: NumericData[Period, Scenario] | tuple[int, int] | None = None, **kwargs, @@ -912,15 +911,11 @@ def __init__( # Validate any remaining unexpected kwargs self._validate_kwargs(kwargs) - self.effects_of_investment: PeriodicEffectsUser = ( - effects_of_investment if effects_of_investment is not None else {} - ) - self.effects_of_retirement: PeriodicEffectsUser = ( - effects_of_retirement if effects_of_retirement is not None else {} - ) + self.effects_of_investment = effects_of_investment if effects_of_investment is not None else {} + self.effects_of_retirement = effects_of_retirement if effects_of_retirement is not None else {} self.fixed_size = fixed_size self.mandatory = mandatory - self.effects_of_investment_per_size: PeriodicEffectsUser = ( + self.effects_of_investment_per_size = ( effects_of_investment_per_size if effects_of_investment_per_size is not None else {} ) self.piecewise_effects_of_investment = piecewise_effects_of_investment @@ -1004,7 +999,7 @@ def optional(self, value: bool): self.mandatory = not value @property - def fix_effects(self) -> PeriodicEffectsUser: + def fix_effects(self) -> EffectData[Period, Scenario] | NumericData[Period, Scenario]: """Deprecated property. Use effects_of_investment instead.""" warnings.warn( 'The fix_effects property is deprecated. Use effects_of_investment instead.', @@ -1014,7 +1009,7 @@ def fix_effects(self) -> PeriodicEffectsUser: return self.effects_of_investment @property - def specific_effects(self) -> PeriodicEffectsUser: + def specific_effects(self) -> EffectData[Period, Scenario] | NumericData[Period, Scenario]: """Deprecated property. Use effects_of_investment_per_size instead.""" warnings.warn( 'The specific_effects property is deprecated. Use effects_of_investment_per_size instead.', @@ -1024,7 +1019,7 @@ def specific_effects(self) -> PeriodicEffectsUser: return self.effects_of_investment_per_size @property - def divest_effects(self) -> PeriodicEffectsUser: + def divest_effects(self) -> EffectData[Period, Scenario] | NumericData[Period, Scenario]: """Deprecated property. Use effects_of_retirement instead.""" warnings.warn( 'The divest_effects property is deprecated. Use effects_of_retirement instead.', @@ -1268,8 +1263,10 @@ class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: TemporalEffectsUser | None = None, - effects_per_running_hour: TemporalEffectsUser | None = None, + effects_per_switch_on: EffectData[Time, Period, Scenario] | NumericData[Time, Period, Scenario] | None = None, + effects_per_running_hour: EffectData[Time, Period, Scenario] + | NumericData[Time, Period, Scenario] + | None = None, on_hours_total_min: int | None = None, on_hours_total_max: int | None = None, consecutive_on_hours_min: NumericData[Time, Period, Scenario] | None = None, @@ -1279,12 +1276,8 @@ def __init__( switch_on_total_max: int | None = None, force_switch_on: bool = False, ): - self.effects_per_switch_on: TemporalEffectsUser = ( - effects_per_switch_on if effects_per_switch_on is not None else {} - ) - self.effects_per_running_hour: TemporalEffectsUser = ( - effects_per_running_hour if effects_per_running_hour is not None else {} - ) + self.effects_per_switch_on = effects_per_switch_on if effects_per_switch_on is not None else {} + self.effects_per_running_hour = effects_per_running_hour if effects_per_running_hour is not None else {} self.on_hours_total_min: NumericData[Period, Scenario] = on_hours_total_min self.on_hours_total_max: NumericData[Period, Scenario] = on_hours_total_max self.consecutive_on_hours_min: NumericData[Time, Period, Scenario] = consecutive_on_hours_min diff --git a/flixopt/types.py b/flixopt/types.py index a7527c835..5566da117 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -112,27 +112,12 @@ def __getitem__(cls, dimensions): """ Create a type hint showing maximum dimensions for effect data. - Effect data can be either: - - A single numeric value (scalar, array, Series, DataFrame, DataArray) - - A dict with string keys mapping to numeric values - - This matches the pattern used for effects: either a single contribution or - a dictionary of named contributions. + Effect data is a dict with string keys mapping to numeric values """ # Return Union[] for better type checker compatibility (especially with | None) # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax - # EffectData = NumericData | dict[str, NumericData] - return Union[ # noqa: UP007 - int, - float, - np.integer, - np.floating, - np.ndarray, - pd.Series, - pd.DataFrame, - xr.DataArray, - dict[str, Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray]], # noqa: UP007 - ] + # EffectData = dict[str, NumericData] + return dict[str, Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray]] # noqa: UP007 class NumericData(metaclass=_NumericDataMeta): @@ -331,8 +316,6 @@ class EffectData(metaclass=_EffectDataMeta): -------- NumericData : General numeric data with dimensions BoolData : For boolean data with dimensions - TemporalEffectsUser : Effect type for temporal contributions (dict or single value) - PeriodicEffectsUser : Effect type for periodic contributions (dict or single value) """ # This class is not meant to be instantiated, only used for type hints From 9e11da8ab186e114cc3d0520c8b4e86335dc61b7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:17:42 +0100 Subject: [PATCH 09/34] Update type hints --- flixopt/calculation.py | 4 ++-- flixopt/elements.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 5de2c8870..1125da401 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -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 @@ -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]: from flixopt.features import InvestmentModel main_results = { diff --git a/flixopt/elements.py b/flixopt/elements.py index bc7b78b7f..c7ff91066 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -13,7 +13,7 @@ from . import io as fx_io from .config import CONFIG -from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser +from .core import PlausibilityError from .features import InvestmentModel, OnOffModel from .interface import InvestParameters, OnOffParameters from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilitiesAbstract @@ -23,7 +23,7 @@ import linopy from .flow_system import FlowSystem - from .types import EffectData, NumericData, Period, Scenario, Time + from .types import EffectData, NumericData, Period, Scalar, Scenario, Time logger = logging.getLogger('flixopt') @@ -429,7 +429,7 @@ def __init__( flow_hours_total_min: NumericData[Period, Scenario] | None = None, load_factor_min: NumericData[Period, Scenario] | None = None, load_factor_max: NumericData[Period, Scenario] | None = None, - previous_flow_rate: NumericData[Period, Scenario] | list[Scalar] | None = None, + previous_flow_rate: Scalar | list[Scalar] | None = None, meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) @@ -716,13 +716,13 @@ def _create_bounds_for_load_factor(self): ) @property - def relative_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]: + def relative_flow_rate_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: if self.element.fixed_relative_profile is not None: return self.element.fixed_relative_profile, self.element.fixed_relative_profile return self.element.relative_minimum, self.element.relative_maximum @property - def absolute_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]: + def absolute_flow_rate_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: """ Returns the absolute bounds the flow_rate can reach. Further constraining might be needed @@ -765,7 +765,7 @@ def investment(self) -> InvestmentModel | None: return self.submodels['investment'] @property - def previous_states(self) -> TemporalData | None: + def previous_states(self) -> xr.DataArray | None: """Previous states of the flow rate""" # TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well. previous_flow_rate = self.element.previous_flow_rate From d772169dfb0fe4f3cb154e73089d02fa40b1484b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:43:58 +0100 Subject: [PATCH 10/34] Use | instead of Union --- flixopt/types.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/flixopt/types.py b/flixopt/types.py index 5566da117..6776ad415 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -100,9 +100,8 @@ def __getitem__(cls, dimensions): Same semantics as NumericData, but for boolean values. """ - # Return Union[] for better type checker compatibility (especially with | None) - # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax - return Union[bool, np.bool_, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] # noqa: UP007 + # Return using | operator for better IDE compatibility + return bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray class _EffectDataMeta(type): @@ -114,10 +113,9 @@ def __getitem__(cls, dimensions): Effect data is a dict with string keys mapping to numeric values """ - # Return Union[] for better type checker compatibility (especially with | None) - # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax + # Return using | operator for better IDE compatibility # EffectData = dict[str, NumericData] - return dict[str, Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray]] # noqa: UP007 + return dict[str, int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray] class NumericData(metaclass=_NumericDataMeta): From 18d51623d333154e29f75c75080e2b38c0bda411 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:44:10 +0100 Subject: [PATCH 11/34] Use | instead of Union --- flixopt/types.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/flixopt/types.py b/flixopt/types.py index 6776ad415..43dd642e0 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -86,9 +86,8 @@ def __getitem__(cls, dimensions): # of which dimensions are specified. The dimension parameters serve # as documentation rather than runtime validation. - # Return Union[] for better type checker compatibility (especially with | None) - # Using Union[] instead of | to avoid IDE warnings with "Type[...] | None" syntax - return Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] # noqa: UP007 + # Return using | operator for better IDE compatibility + return int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray class _BoolDataMeta(type): @@ -328,7 +327,7 @@ def __init__(self): __all__ = [ 'NumericData', # Primary public type for numeric data 'BoolData', # Primary public type for boolean data - 'EffectData', # Primary public type for effect data (semantic variant of NumericData) + 'EffectData', # Primary public type for effect data 'Time', 'Period', 'Scenario', From 242eecd2fae5d97d23dfb664b302291bffcc94a2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 09:00:27 +0100 Subject: [PATCH 12/34] Direct type hints --- flixopt/types.py | 336 +++++++---------------------------------------- 1 file changed, 44 insertions(+), 292 deletions(-) diff --git a/flixopt/types.py b/flixopt/types.py index 43dd642e0..0a66436e3 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -1,33 +1,36 @@ """ Type system for dimension-aware data in flixopt. -This module provides generic types that clearly communicate which dimensions +This module provides type aliases that clearly communicate which dimensions data can have. The type system is designed to be self-documenting while maintaining maximum flexibility for input formats. Key Concepts ------------ -- Dimension markers (`Time`, `Period`, `Scenario`) represent the possible dimensions -- `NumericData[...]` generic type indicates the **maximum** dimensions data can have +- Type aliases use suffix notation to indicate dimensions: + - `_T`: Time dimension only + - `_TS`: Time and Scenario dimensions + - `_PS`: Period and Scenario dimensions (no time) + - `_TPS`: Time, Period, and Scenario dimensions - Data can have any subset of the specified dimensions (including being scalar) - All standard input formats are supported (scalar, array, Series, DataFrame, DataArray) Examples -------- -Type hint `NumericData[Time]` accepts: +Type hint `Numeric_T` accepts: - Scalar: `0.5` (broadcast to all timesteps) - 1D array: `np.array([1, 2, 3])` (matched to time dimension) - pandas Series: with DatetimeIndex matching flow system - xarray DataArray: with 'time' dimension -Type hint `NumericData[Time, Scenario]` accepts: +Type hint `Numeric_TS` accepts: - Scalar: `100` (broadcast to all time and scenario combinations) - 1D array: matched to time OR scenario dimension - 2D array: matched to both dimensions - pandas DataFrame: columns as scenarios, index as time - xarray DataArray: with any subset of 'time', 'scenario' dimensions -Type hint `NumericData[Period, Scenario]` (periodic data, no time): +Type hint `Numeric_PS` (periodic data, no time): - Used for investment parameters that vary by planning period - Accepts scalars, arrays matching periods/scenarios, or DataArrays @@ -36,300 +39,49 @@ - Not converted to DataArray, stays as scalar """ -from typing import Any, TypeAlias, Union +from typing import TypeAlias import numpy as np import pandas as pd import xarray as xr - -# Dimension marker classes for generic type subscripting -class Time: - """Marker for the time dimension in Data generic types.""" - - pass - - -class Period: - """Marker for the period dimension in Data generic types (for multi-period optimization).""" - - pass - - -class Scenario: - """Marker for the scenario dimension in Data generic types (for scenario analysis).""" - - pass - - -class _NumericDataMeta(type): - """Metaclass for NumericData to enable subscript notation NumericData[Time, Scenario] for numeric data.""" - - def __getitem__(cls, dimensions): - """ - Create a type hint showing maximum dimensions for numeric data. - - The dimensions parameter can be: - - A single dimension: NumericData[Time] - - Multiple dimensions: NumericData[Time, Period, Scenario] - - The type hint communicates that data can have **at most** these dimensions. - Actual data can be: - - Scalar (broadcast to all dimensions) - - Have any subset of the specified dimensions - - Have all specified dimensions - - This is consistent with xarray's broadcasting semantics and the - framework's data conversion behavior. - """ - # For type checking purposes, we return the same union type regardless - # of which dimensions are specified. The dimension parameters serve - # as documentation rather than runtime validation. - - # Return using | operator for better IDE compatibility - return int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray - - -class _BoolDataMeta(type): - """Metaclass for BoolData to enable subscript notation BoolData[Time, Scenario] for boolean data.""" - - def __getitem__(cls, dimensions): - """ - Create a type hint showing maximum dimensions for boolean data. - - Same semantics as NumericData, but for boolean values. - """ - # Return using | operator for better IDE compatibility - return bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray - - -class _EffectDataMeta(type): - """Metaclass for EffectData to enable subscript notation EffectData[Time, Period, Scenario] for effect data.""" - - def __getitem__(cls, dimensions): - """ - Create a type hint showing maximum dimensions for effect data. - - Effect data is a dict with string keys mapping to numeric values - """ - # Return using | operator for better IDE compatibility - # EffectData = dict[str, NumericData] - return dict[str, int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray] - - -class NumericData(metaclass=_NumericDataMeta): - """ - Base type for numeric data that can have various dimensions. - - This is the internal base class. Use `NumericData` publicly for clarity. - - Use subscript notation to specify the maximum dimensions: - - `NumericData[Time]`: Time-varying numeric data (at most 'time' dimension) - - `NumericData[Time, Scenario]`: Time-varying with scenarios (at most 'time', 'scenario') - - `NumericData[Period, Scenario]`: Periodic data without time (at most 'period', 'scenario') - - `NumericData[Time, Period, Scenario]`: Full dimensionality (rarely used) - - Semantics: "At Most" Dimensions - -------------------------------- - When you see `NumericData[Time, Scenario]`, it means the data can have: - - No dimensions (scalar): broadcast to all time and scenario values - - Just 'time': broadcast across scenarios - - Just 'scenario': broadcast across time - - Both 'time' and 'scenario': full dimensionality - - Accepted Input Formats - ---------------------- - All dimension combinations accept these formats: - - Scalars: int, float (including numpy types) - - Arrays: numpy ndarray (matched by length/shape to dimensions) - - pandas Series: matched by index to dimension coordinates - - pandas DataFrame: typically columns=scenarios, index=time - - xarray DataArray: used directly with dimension validation - - Conversion Behavior - ------------------- - Input data is converted to xarray.DataArray internally: - - Scalars are broadcast to all specified dimensions - - Arrays are matched by length (unambiguous) or shape (multi-dimensional) - - Series are matched by index equality with coordinate values - - DataArrays are validated and broadcast as needed - - Note - ---- - This type is for **numeric** data only. For boolean data, use `BoolData`. - - This is the base class - use `NumericData` alias publicly for clarity and symmetry with `BoolData`. - - See Also - -------- - BoolData : For boolean data with dimensions - DataConverter.to_dataarray : The conversion implementation - FlowSystem.fit_to_model_coords : Fits data to the model's coordinate system - """ - - # This class is not meant to be instantiated, only used for type hints - def __init__(self): - raise TypeError('NumericData is a type hint only and cannot be instantiated') - - -class BoolData(metaclass=_BoolDataMeta): - """ - Generic type for boolean data that can have various dimensions. - - Use subscript notation to specify the maximum dimensions: - - `BoolData[Time]`: Time-varying boolean data - - `BoolData[Time, Scenario]`: Boolean data with time and scenario dimensions - - `BoolData[Period, Scenario]`: Periodic boolean data - - Semantics: "At Most" Dimensions - -------------------------------- - Same semantics as NumericData, but for boolean values. - When you see `BoolData[Time, Scenario]`, the data can have: - - No dimensions (scalar bool): broadcast to all time and scenario values - - Just 'time': broadcast across scenarios - - Just 'scenario': broadcast across time - - Both 'time' and 'scenario': full dimensionality - - Accepted Input Formats (Boolean) - --------------------------------- - All dimension combinations accept these formats: - - Scalars: bool, np.bool_ - - Arrays: numpy ndarray with boolean dtype (matched by length/shape to dimensions) - - pandas Series: with boolean values, matched by index to dimension coordinates - - pandas DataFrame: with boolean values - - xarray DataArray: with boolean values, used directly with dimension validation - - Use Cases - --------- - Boolean data is typically used for: - - Binary decision variables (on/off states) - - Constraint activation flags - - Feasibility indicators - - Conditional parameters - - Examples - -------- - >>> # Scalar boolean (broadcast to all dimensions) - >>> active: BoolData[Time] = True - >>> - >>> # Time-varying on/off pattern - >>> import numpy as np - >>> pattern: BoolData[Time] = np.array([True, False, True, False]) - >>> - >>> # Scenario-specific activation - >>> import pandas as pd - >>> scenario_active: BoolData[Scenario] = pd.Series([True, False, True], index=['low', 'mid', 'high']) - - Note - ---- - This type is for **boolean** data only. For numeric data, use `NumericData`. - - See Also - -------- - NumericData : For numeric data with dimensions - DataConverter.to_dataarray : The conversion implementation - """ - - # This class is not meant to be instantiated, only used for type hints - def __init__(self): - raise TypeError('BoolData is a type hint only and cannot be instantiated') - - -class EffectData(metaclass=_EffectDataMeta): - """ - Generic type for effect data that can be a single value or a dictionary of values. - - EffectData represents the common pattern for effects: either a single numeric contribution - or a dictionary with string keys mapping to numeric contributions. This is useful for - specifying effects where you either have a single effect or multiple named effects. - - Use subscript notation to specify the maximum dimensions: - - `EffectData[Time]`: Time-varying effect data - - `EffectData[Period, Scenario]`: Periodic effect data - - `EffectData[Time, Period, Scenario]`: Full dimensional effect data - - Type Structure - -------------- - `EffectData[dims]` = `NumericData[dims] | dict[str, NumericData[dims]]` - - This means you can provide: - - A single numeric value (scalar, array, Series, DataFrame, DataArray) - - A dict mapping effect names to numeric values - - Semantics: "At Most" Dimensions - -------------------------------- - When you see `EffectData[Time, Period, Scenario]`, it means the data can have: - - No dimensions (scalar): broadcast to all time, period, and scenario values - - Any subset: just time, just period, just scenario, time+period, etc. - - All dimensions: full 3D data - - Accepted Input Formats - ---------------------- - Single value: - - Scalars: int, float (including numpy types) - - Arrays: numpy ndarray with numeric dtype (matched by length/shape to dimensions) - - pandas Series: matched by index to dimension coordinates - - pandas DataFrame: typically columns=scenarios/periods, index=time - - xarray DataArray: used directly with dimension validation - - Dictionary of values: - - dict[str, ] - - Typical Use Cases - ----------------- - - Single effect: `EffectData[Time] = 10.5` or `np.array([10, 12, 11])` - - Multiple effects: `EffectData[Time] = {'CO2': 0.5, 'costs': 100}` - - Cross-effect relationships in Effect class - - Component effect contributions (effects_per_flow_hour, etc.) - - Examples - -------- - >>> # Single scalar effect (broadcast to all dimensions) - >>> single_cost: EffectData[Time, Period, Scenario] = 10.5 - >>> - >>> # Single time-varying effect - >>> single_emissions: EffectData[Time] = np.array([100, 120, 110]) - >>> - >>> # Multiple named effects (dict) - >>> multiple_effects: EffectData[Time] = { - ... 'CO2': np.array([0.5, 0.6, 0.5]), - ... 'costs': 100, # scalar broadcast to all time - ... } - >>> - >>> # Cross-effect in Effect class - >>> cost_effect = Effect( - ... label='total_costs', - ... unit='€', - ... is_objective=True, - ... share_from_temporal={'CO2': 0.1}, # EffectData[Time, Period, Scenario] - ... ) - - Note - ---- - EffectData = NumericData | dict[str, NumericData]. This pattern is specific to effects - and different from NumericData which only represents single numeric values. - - See Also - -------- - NumericData : General numeric data with dimensions - BoolData : For boolean data with dimensions - """ - - # This class is not meant to be instantiated, only used for type hints - def __init__(self): - raise TypeError('EffectData is a type hint only and cannot be instantiated') - - -# Simple scalar type for dimension-less numeric values +# Internal base types +_Numeric: TypeAlias = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray +_Bool: TypeAlias = bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray +_Effect: TypeAlias = _Numeric | dict[str, _Numeric] + +# Numeric data with dimension combinations +Numeric_T: TypeAlias = _Numeric # Time +Numeric_TS: TypeAlias = _Numeric # Time, Scenario +Numeric_PS: TypeAlias = _Numeric # Period, Scenario +Numeric_TPS: TypeAlias = _Numeric # Time, Period, Scenario + +# Boolean data with dimension combinations +Bool_T: TypeAlias = _Bool +Bool_TS: TypeAlias = _Bool +Bool_PS: TypeAlias = _Bool + +# Effect data with dimension combinations +Effect_T: TypeAlias = _Effect +Effect_TS: TypeAlias = _Effect +Effect_PS: TypeAlias = _Effect +Effect_TPS: TypeAlias = _Effect + +# Scalar (no dimensions) Scalar: TypeAlias = int | float | np.integer | np.floating # Export public API __all__ = [ - 'NumericData', # Primary public type for numeric data - 'BoolData', # Primary public type for boolean data - 'EffectData', # Primary public type for effect data - 'Time', - 'Period', - 'Scenario', + 'Numeric_T', + 'Numeric_TS', + 'Numeric_PS', + 'Numeric_TPS', + 'Bool_T', + 'Bool_TS', + 'Bool_PS', + 'Effect_T', + 'Effect_TS', + 'Effect_PS', + 'Effect_TPS', 'Scalar', ] From d82c5d2665092b2727a31e05e27a611b040bc066 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:35:08 +0100 Subject: [PATCH 13/34] Use direct type hints instead of subscripts --- flixopt/__init__.py | 2 +- flixopt/components.py | 38 ++++++++++++------------- flixopt/core.py | 21 -------------- flixopt/effects.py | 44 ++++++++++++++--------------- flixopt/elements.py | 22 +++++++-------- flixopt/features.py | 4 +-- flixopt/flow_system.py | 8 +++--- flixopt/interface.py | 54 ++++++++++++++++++------------------ flixopt/linear_converters.py | 20 ++++++------- flixopt/modeling.py | 1 - flixopt/types.py | 32 ++++++++++----------- 11 files changed, 110 insertions(+), 136 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 52560bc85..29d6813ab 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -42,7 +42,7 @@ ) # Type system for dimension-aware type hints -from .types import BoolData, EffectData, NumericData, Period, Scalar, Scenario, Time +from .types import Numeric_TPS, Numeric_PS, Numeric_S, Bool_TPS, Bool_PS, Bool_S, Effect_TPS, Effect_PS, Effect_S, Scalar # === Runtime warning suppression for third-party libraries === # These warnings are from dependencies and cannot be fixed by end users. diff --git a/flixopt/components.py b/flixopt/components.py index 632bb6ee9..b22b0caf5 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -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 @@ -387,17 +387,17 @@ def __init__( label: str, charging: Flow, discharging: Flow, - capacity_in_flow_hours: NumericData[Period, Scenario] | InvestParameters, - relative_minimum_charge_state: NumericData[Time, Period, Scenario] = 0, - relative_maximum_charge_state: NumericData[Time, Period, Scenario] = 1, - initial_charge_state: NumericData[Period, Scenario] | Literal['lastValueOfSim'] = 0, - minimal_final_charge_state: NumericData[Period, Scenario] | None = None, - maximal_final_charge_state: NumericData[Period, Scenario] | None = None, - relative_minimum_final_charge_state: NumericData[Period, Scenario] | None = None, - relative_maximum_final_charge_state: NumericData[Period, Scenario] | None = None, - eta_charge: NumericData[Time, Period, Scenario] = 1, - eta_discharge: NumericData[Time, Period, Scenario] = 1, - relative_loss_per_hour: NumericData[Time, Period, Scenario] = 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, @@ -414,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: NumericData[Time, Period, Scenario] = relative_minimum_charge_state - self.relative_maximum_charge_state: NumericData[Time, Period, Scenario] = 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 @@ -424,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: NumericData[Time, Period, Scenario] = eta_charge - self.eta_discharge: NumericData[Time, Period, Scenario] = eta_discharge - self.relative_loss_per_hour: NumericData[Time, Period, Scenario] = 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 @@ -664,8 +664,8 @@ def __init__( out1: Flow, in2: Flow | None = None, out2: Flow | None = None, - relative_losses: NumericData[Time, Period, Scenario] | None = None, - absolute_losses: NumericData[Time, Period, Scenario] | 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, diff --git a/flixopt/core.py b/flixopt/core.py index 406a6ea7c..c10248c6c 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -12,8 +12,6 @@ import pandas as pd import xarray as xr -from flixopt.types import NumericData, Period, Scenario, Time - logger = logging.getLogger('flixopt') FlowSystemDimensions = Literal['time', 'period', 'scenario'] @@ -143,25 +141,6 @@ def agg_weight(self): return self.aggregation_weight -TemporalDataUser = NumericData[Time, Scenario] -""" -User data which might have a time dimension. Internally converted to an xr.DataArray with time dimension. - -Supports data with at most [Time, Scenario] dimensions. For periodic data (no time dimension), use PeriodicDataUser. -For data with all three dimensions [Time, Period, Scenario], use NumericData[Time, Period, Scenario] directly. -""" - -PeriodicDataUser = NumericData[Period, Scenario] -""" -User data for periodic parameters (no time dimension). Internally converted to an xr.DataArray. - -Supports data with at most [Period, Scenario] dimensions. For temporal data (with time), use TemporalDataUser. -""" - -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. diff --git a/flixopt/effects.py b/flixopt/effects.py index 120158b09..3aa0117e1 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -57,21 +57,21 @@ class Effect(Element): Maps periodic contributions from other effects to this effect. Type: `PeriodicEffectsUser` (single value or dict with dimensions [Period, Scenario]) minimum_temporal: Minimum allowed total contribution across all timesteps. - Type: `NumericData[Period, Scenario]` (sum over time, can vary by period/scenario) + Type: `Numeric_PS` (sum over time, can vary by period/scenario) maximum_temporal: Maximum allowed total contribution across all timesteps. - Type: `NumericData[Period, Scenario]` (sum over time, can vary by period/scenario) + Type: `Numeric_PS` (sum over time, can vary by period/scenario) minimum_per_hour: Minimum allowed contribution per hour. - Type: `NumericData[Time, Period, Scenario]` (per-timestep constraint, can vary by period) + Type: `Numeric_TPS` (per-timestep constraint, can vary by period) maximum_per_hour: Maximum allowed contribution per hour. - Type: `NumericData[Time, Period, Scenario]` (per-timestep constraint, can vary by period) + Type: `Numeric_TPS` (per-timestep constraint, can vary by period) minimum_periodic: Minimum allowed total periodic contribution. - Type: `NumericData[Period, Scenario]` (periodic constraint) + Type: `Numeric_PS` (periodic constraint) maximum_periodic: Maximum allowed total periodic contribution. - Type: `NumericData[Period, Scenario]` (periodic constraint) + Type: `Numeric_PS` (periodic constraint) minimum_total: Minimum allowed total effect (temporal + periodic combined). - Type: `NumericData[Period, Scenario]` (total constraint per period) + Type: `Numeric_PS` (total constraint per period) maximum_total: Maximum allowed total effect (temporal + periodic combined). - Type: `NumericData[Period, Scenario]` (total constraint per period) + 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. @@ -179,16 +179,16 @@ def __init__( meta_data: dict | None = None, is_standard: bool = False, is_objective: bool = False, - share_from_temporal: EffectData[Time, Period, Scenario] | None = None, - share_from_periodic: EffectData[Period, Scenario] | None = None, - minimum_temporal: NumericData[Period, Scenario] | None = None, - maximum_temporal: NumericData[Period, Scenario] | None = None, - minimum_periodic: NumericData[Period, Scenario] | None = None, - maximum_periodic: NumericData[Period, Scenario] | None = None, - minimum_per_hour: NumericData[Time, Period, Scenario] | None = None, - maximum_per_hour: NumericData[Time, Period, Scenario] | None = None, - minimum_total: NumericData[Period, Scenario] | None = None, - maximum_total: NumericData[Period, Scenario] | 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) @@ -196,10 +196,10 @@ def __init__( self.description = description self.is_standard = is_standard self.is_objective = is_objective - self.share_from_temporal: EffectData[Time, Period, Scenario] = ( + self.share_from_temporal: Effect_TPS = ( share_from_temporal if share_from_temporal is not None else {} ) - self.share_from_periodic: EffectData[Period, Scenario] = ( + self.share_from_periodic: Effect_PS = ( share_from_periodic if share_from_periodic is not None else {} ) @@ -491,8 +491,8 @@ def add_effects(self, *effects: Effect) -> None: logger.info(f'Registered new Effect: {effect.label}') def create_effect_values_dict( - self, effect_values_user: EffectData[Time, Period, Scenario] - ) -> dict[str, NumericData[Time, Period, Scenario]] | None: + self, effect_values_user: Effect_TPS + ) -> dict[str, Numeric_TPS] | None: """Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. Examples: diff --git a/flixopt/elements.py b/flixopt/elements.py index c7ff91066..37e208435 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -23,7 +23,7 @@ import linopy from .flow_system import FlowSystem - from .types import EffectData, NumericData, Period, Scalar, Scenario, Time + from .types import Numeric_TPS, Numeric_PS, Numeric_S, Bool_TPS, Bool_PS, Bool_S, Effect_TPS, Effect_PS, Effect_S, Scalar logger = logging.getLogger('flixopt') @@ -228,7 +228,7 @@ class Bus(Element): def __init__( self, label: str, - excess_penalty_per_flow_hour: NumericData[Time, Period, Scenario] | None = 1e5, + excess_penalty_per_flow_hour: Numeric_TPS | None = 1e5, meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) @@ -419,16 +419,16 @@ def __init__( self, label: str, bus: str, - size: NumericData[Period, Scenario] | InvestParameters = None, - fixed_relative_profile: NumericData[Time, Period, Scenario] | None = None, - relative_minimum: NumericData[Time, Period, Scenario] = 0, - relative_maximum: NumericData[Time, Period, Scenario] = 1, - effects_per_flow_hour: EffectData[Time, Period, Scenario] | NumericData[Time, Period, Scenario] | None = None, + size: Numeric_PS | InvestParameters = None, + fixed_relative_profile: Numeric_TPS | None = None, + relative_minimum: Numeric_TPS = 0, + relative_maximum: Numeric_TPS = 1, + effects_per_flow_hour: Effect_TPS | Numeric_TPS | None = None, on_off_parameters: OnOffParameters | None = None, - flow_hours_total_max: NumericData[Period, Scenario] | None = None, - flow_hours_total_min: NumericData[Period, Scenario] | None = None, - load_factor_min: NumericData[Period, Scenario] | None = None, - load_factor_max: NumericData[Period, Scenario] | None = None, + flow_hours_total_max: Numeric_PS | None = None, + flow_hours_total_min: Numeric_PS | None = None, + load_factor_min: Numeric_PS | None = None, + load_factor_max: Numeric_PS | None = None, previous_flow_rate: Scalar | list[Scalar] | None = None, meta_data: dict | None = None, ): diff --git a/flixopt/features.py b/flixopt/features.py index e6d400556..172b3dc24 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -518,8 +518,8 @@ def __init__( dims: list[FlowSystemDimensions], label_of_element: str | None = None, label_of_model: str | None = None, - total_max: NumericData[Period, Scenario] | None = None, - total_min: NumericData[Period, Scenario] | None = None, + total_max: Numeric_PS | None = None, + total_min: Numeric_PS | None = None, max_per_hour: TemporalData | None = None, min_per_hour: TemporalData | None = None, ): diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index aa1cec586..0af1007f1 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -159,7 +159,7 @@ def __init__( scenarios: pd.Index | None = None, hours_of_last_timestep: int | float | None = None, hours_of_previous_timesteps: int | float | np.ndarray | None = None, - weights: NumericData[Period, Scenario] | None = None, + weights: Numeric_PS | None = None, scenario_independent_sizes: bool | list[str] = True, scenario_independent_flow_rates: bool | list[str] = False, ): @@ -523,7 +523,7 @@ def to_json(self, path: str | pathlib.Path): def fit_to_model_coords( self, name: str, - data: NumericData[Time, Period, Scenario] | BoolData[Time, Period, Scenario] | None, + data: Numeric_TPS | Bool_TPS | None, dims: Collection[FlowSystemDimensions] | None = None, ) -> xr.DataArray | None: """ @@ -563,11 +563,11 @@ def fit_to_model_coords( def fit_effects_to_model_coords( self, label_prefix: str | None, - effect_values: EffectData[Time, Period, Scenario] | NumericData[Time, Period, Scenario] | None, + effect_values: Effect_TPS | Numeric_TPS | None, label_suffix: str | None = None, dims: Collection[FlowSystemDimensions] | None = None, delimiter: str = '|', - ) -> EffectData[Time, Period, Scenario] | None: + ) -> Effect_TPS | None: """ Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. """ diff --git a/flixopt/interface.py b/flixopt/interface.py index e986bb078..715f894f2 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -72,7 +72,7 @@ class Piece(Interface): """ - def __init__(self, start: NumericData[Time, Period, Scenario], end: NumericData[Time, Period, Scenario]): + def __init__(self, start: Numeric_TPS, end: Numeric_TPS): self.start = start self.end = end self.has_time_dim = False @@ -873,15 +873,15 @@ class InvestParameters(Interface): def __init__( self, - fixed_size: NumericData[Period, Scenario] | None = None, - minimum_size: NumericData[Period, Scenario] | None = None, - maximum_size: NumericData[Period, Scenario] | None = None, + fixed_size: Numeric_PS | None = None, + minimum_size: Numeric_PS | None = None, + maximum_size: Numeric_PS | None = None, mandatory: bool = False, - effects_of_investment: EffectData[Period, Scenario] | NumericData[Period, Scenario] | None = None, - effects_of_investment_per_size: EffectData[Period, Scenario] | NumericData[Period, Scenario] | None = None, - effects_of_retirement: EffectData[Period, Scenario] | NumericData[Period, Scenario] | None = None, + effects_of_investment: Effect_PS | Numeric_PS | None = None, + effects_of_investment_per_size: Effect_PS | Numeric_PS | None = None, + effects_of_retirement: Effect_PS | Numeric_PS | None = None, piecewise_effects_of_investment: PiecewiseEffects | None = None, - linked_periods: NumericData[Period, Scenario] | tuple[int, int] | None = None, + linked_periods: Numeric_PS | tuple[int, int] | None = None, **kwargs, ): # Handle deprecated parameters using centralized helper @@ -999,7 +999,7 @@ def optional(self, value: bool): self.mandatory = not value @property - def fix_effects(self) -> EffectData[Period, Scenario] | NumericData[Period, Scenario]: + def fix_effects(self) -> Effect_PS | Numeric_PS: """Deprecated property. Use effects_of_investment instead.""" warnings.warn( 'The fix_effects property is deprecated. Use effects_of_investment instead.', @@ -1009,7 +1009,7 @@ def fix_effects(self) -> EffectData[Period, Scenario] | NumericData[Period, Scen return self.effects_of_investment @property - def specific_effects(self) -> EffectData[Period, Scenario] | NumericData[Period, Scenario]: + def specific_effects(self) -> Effect_PS | Numeric_PS: """Deprecated property. Use effects_of_investment_per_size instead.""" warnings.warn( 'The specific_effects property is deprecated. Use effects_of_investment_per_size instead.', @@ -1019,7 +1019,7 @@ def specific_effects(self) -> EffectData[Period, Scenario] | NumericData[Period, return self.effects_of_investment_per_size @property - def divest_effects(self) -> EffectData[Period, Scenario] | NumericData[Period, Scenario]: + def divest_effects(self) -> Effect_PS | Numeric_PS: """Deprecated property. Use effects_of_retirement instead.""" warnings.warn( 'The divest_effects property is deprecated. Use effects_of_retirement instead.', @@ -1039,11 +1039,11 @@ def piecewise_effects(self) -> PiecewiseEffects | None: return self.piecewise_effects_of_investment @property - def minimum_or_fixed_size(self) -> NumericData[Period, Scenario]: + def minimum_or_fixed_size(self) -> Numeric_PS: return self.fixed_size if self.fixed_size is not None else self.minimum_size @property - def maximum_or_fixed_size(self) -> NumericData[Period, Scenario]: + def maximum_or_fixed_size(self) -> Numeric_PS: return self.fixed_size if self.fixed_size is not None else self.maximum_size def format_for_repr(self) -> str: @@ -1263,28 +1263,28 @@ class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: EffectData[Time, Period, Scenario] | NumericData[Time, Period, Scenario] | None = None, - effects_per_running_hour: EffectData[Time, Period, Scenario] - | NumericData[Time, Period, Scenario] + effects_per_switch_on: Effect_TPS | Numeric_TPS | None = None, + effects_per_running_hour: Effect_TPS + | Numeric_TPS | None = None, on_hours_total_min: int | None = None, on_hours_total_max: int | None = None, - consecutive_on_hours_min: NumericData[Time, Period, Scenario] | None = None, - consecutive_on_hours_max: NumericData[Time, Period, Scenario] | None = None, - consecutive_off_hours_min: NumericData[Time, Period, Scenario] | None = None, - consecutive_off_hours_max: NumericData[Time, Period, Scenario] | None = None, + consecutive_on_hours_min: Numeric_TPS | None = None, + consecutive_on_hours_max: Numeric_TPS | None = None, + consecutive_off_hours_min: Numeric_TPS | None = None, + consecutive_off_hours_max: Numeric_TPS | None = None, switch_on_total_max: int | None = None, force_switch_on: bool = False, ): self.effects_per_switch_on = effects_per_switch_on if effects_per_switch_on is not None else {} self.effects_per_running_hour = effects_per_running_hour if effects_per_running_hour is not None else {} - self.on_hours_total_min: NumericData[Period, Scenario] = on_hours_total_min - self.on_hours_total_max: NumericData[Period, Scenario] = on_hours_total_max - self.consecutive_on_hours_min: NumericData[Time, Period, Scenario] = consecutive_on_hours_min - self.consecutive_on_hours_max: NumericData[Time, Period, Scenario] = consecutive_on_hours_max - self.consecutive_off_hours_min: NumericData[Time, Period, Scenario] = consecutive_off_hours_min - self.consecutive_off_hours_max: NumericData[Time, Period, Scenario] = consecutive_off_hours_max - self.switch_on_total_max: NumericData[Period, Scenario] = switch_on_total_max + self.on_hours_total_min: Numeric_PS = on_hours_total_min + self.on_hours_total_max: Numeric_PS = on_hours_total_max + self.consecutive_on_hours_min: Numeric_TPS = consecutive_on_hours_min + self.consecutive_on_hours_max: Numeric_TPS = consecutive_on_hours_max + self.consecutive_off_hours_min: Numeric_TPS = consecutive_off_hours_min + self.consecutive_off_hours_max: Numeric_TPS = consecutive_off_hours_max + self.switch_on_total_max: Numeric_PS = switch_on_total_max self.force_switch_on: bool = force_switch_on def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 661cdc030..5a9e46f94 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -77,7 +77,7 @@ class Boiler(LinearConverter): def __init__( self, label: str, - eta: NumericData[Time, Period, Scenario], + eta: Numeric_TPS, Q_fu: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -164,7 +164,7 @@ class Power2Heat(LinearConverter): def __init__( self, label: str, - eta: NumericData[Time, Period, Scenario], + eta: Numeric_TPS, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -251,7 +251,7 @@ class HeatPump(LinearConverter): def __init__( self, label: str, - COP: NumericData[Time, Period, Scenario], + COP: Numeric_TPS, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -340,7 +340,7 @@ class CoolingTower(LinearConverter): def __init__( self, label: str, - specific_electricity_demand: NumericData[Time, Period, Scenario], + specific_electricity_demand: Numeric_TPS, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -438,8 +438,8 @@ class CHP(LinearConverter): def __init__( self, label: str, - eta_th: NumericData[Time, Period, Scenario], - eta_el: NumericData[Time, Period, Scenario], + eta_th: Numeric_TPS, + eta_el: Numeric_TPS, Q_fu: Flow, P_el: Flow, Q_th: Flow, @@ -552,7 +552,7 @@ class HeatPumpWithSource(LinearConverter): def __init__( self, label: str, - COP: NumericData[Time, Period, Scenario], + COP: Numeric_TPS, P_el: Flow, Q_ab: Flow, Q_th: Flow, @@ -590,11 +590,11 @@ def COP(self, value): # noqa: N802 def check_bounds( - value: NumericData[Time, Period, Scenario], + value: Numeric_TPS, parameter_label: str, element_label: str, - lower_bound: NumericData[Time, Period, Scenario], - upper_bound: NumericData[Time, Period, Scenario], + lower_bound: Numeric_TPS, + upper_bound: Numeric_TPS, ) -> None: """ Check if the value is within the bounds. The bounds are exclusive. diff --git a/flixopt/modeling.py b/flixopt/modeling.py index c7f0bf314..b2676db48 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -5,7 +5,6 @@ import xarray as xr from .config import CONFIG -from .core import TemporalData from .structure import Submodel logger = logging.getLogger('flixopt') diff --git a/flixopt/types.py b/flixopt/types.py index 0a66436e3..1411f3e30 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -51,37 +51,33 @@ _Effect: TypeAlias = _Numeric | dict[str, _Numeric] # Numeric data with dimension combinations -Numeric_T: TypeAlias = _Numeric # Time -Numeric_TS: TypeAlias = _Numeric # Time, Scenario -Numeric_PS: TypeAlias = _Numeric # Period, Scenario Numeric_TPS: TypeAlias = _Numeric # Time, Period, Scenario +Numeric_PS: TypeAlias = _Numeric # Period, Scenario +Numeric_S: TypeAlias = _Numeric # Scenario # Boolean data with dimension combinations -Bool_T: TypeAlias = _Bool -Bool_TS: TypeAlias = _Bool -Bool_PS: TypeAlias = _Bool +Bool_TPS: TypeAlias = _Bool # Time, Period, Scenario +Bool_PS: TypeAlias = _Bool # Period, Scenario +Bool_S: TypeAlias = _Bool # Scenario # Effect data with dimension combinations -Effect_T: TypeAlias = _Effect -Effect_TS: TypeAlias = _Effect -Effect_PS: TypeAlias = _Effect -Effect_TPS: TypeAlias = _Effect +Effect_TPS: TypeAlias = _Effect # Time, Period, Scenario +Effect_PS: TypeAlias = _Effect # Period, Scenario +Effect_S: TypeAlias = _Effect # Scenario # Scalar (no dimensions) Scalar: TypeAlias = int | float | np.integer | np.floating # Export public API __all__ = [ - 'Numeric_T', - 'Numeric_TS', - 'Numeric_PS', 'Numeric_TPS', - 'Bool_T', - 'Bool_TS', + 'Numeric_PS', + 'Numeric_S', + 'Bool_TPS', 'Bool_PS', - 'Effect_T', - 'Effect_TS', - 'Effect_PS', + 'Bool_S', 'Effect_TPS', + 'Effect_PS', + 'Effect_S', 'Scalar', ] From d4f4df03b07a9e4c5c57180e5c9d0056f28b13ff Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:37:53 +0100 Subject: [PATCH 14/34] Update type hints --- flixopt/modeling.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index b2676db48..13b4c0e3e 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -118,7 +118,7 @@ def count_consecutive_states( class ModelingUtilities: @staticmethod def compute_consecutive_hours_in_state( - binary_values: TemporalData, + binary_values: xr.DataArray, hours_per_timestep: int | float, epsilon: float = None, ) -> float: @@ -202,7 +202,7 @@ def expression_tracking_variable( tracked_expression, name: str = None, short_name: str = None, - bounds: tuple[TemporalData, TemporalData] = None, + bounds: tuple[xr.DataArray, xr.DataArray] = None, coords: str | list[str] | None = None, ) -> tuple[linopy.Variable, linopy.Constraint]: """ @@ -241,11 +241,11 @@ def consecutive_duration_tracking( state_variable: linopy.Variable, name: str = None, short_name: str = None, - minimum_duration: TemporalData | None = None, - maximum_duration: TemporalData | None = None, + minimum_duration: xr.DataArray | None = None, + maximum_duration: xr.DataArray | None = None, duration_dim: str = 'time', - duration_per_step: int | float | TemporalData = None, - previous_duration: TemporalData = 0, + duration_per_step: int | float | xr.DataArray = None, + previous_duration: xr.DataArray = 0, ) -> tuple[linopy.Variable, tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]]: """ Creates consecutive duration tracking for a binary state variable. @@ -393,7 +393,7 @@ class BoundingPatterns: def basic_bounds( model: Submodel, variable: linopy.Variable, - bounds: tuple[TemporalData, TemporalData], + bounds: tuple[xr.DataArray, xr.DataArray], name: str = None, ) -> list[linopy.constraints.Constraint]: """Create simple bounds. @@ -425,7 +425,7 @@ def basic_bounds( def bounds_with_state( model: Submodel, variable: linopy.Variable, - bounds: tuple[TemporalData, TemporalData], + bounds: tuple[xr.DataArray, xr.DataArray], variable_state: linopy.Variable, name: str = None, ) -> list[linopy.Constraint]: @@ -472,7 +472,7 @@ def scaled_bounds( model: Submodel, variable: linopy.Variable, scaling_variable: linopy.Variable, - relative_bounds: tuple[TemporalData, TemporalData], + relative_bounds: tuple[xr.DataArray, xr.DataArray], name: str = None, ) -> list[linopy.Constraint]: """Constraint a variable by scaling bounds, dependent on another variable. @@ -515,8 +515,8 @@ def scaled_bounds_with_state( model: Submodel, variable: linopy.Variable, scaling_variable: linopy.Variable, - relative_bounds: tuple[TemporalData, TemporalData], - scaling_bounds: tuple[TemporalData, TemporalData], + relative_bounds: tuple[xr.DataArray, xr.DataArray], + scaling_bounds: tuple[xr.DataArray, xr.DataArray], variable_state: linopy.Variable, name: str = None, ) -> list[linopy.Constraint]: From 540f1f8c7fdd344435c943dc7419dcf01b2faa24 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:47:51 +0100 Subject: [PATCH 15/34] Update type hints --- flixopt/__init__.py | 13 ++++++++++++- flixopt/components.py | 6 +++--- flixopt/effects.py | 14 ++++---------- flixopt/elements.py | 13 ++++++++++++- flixopt/features.py | 8 ++++---- flixopt/flow_system.py | 2 +- flixopt/interface.py | 6 ++---- flixopt/linear_converters.py | 2 +- 8 files changed, 39 insertions(+), 25 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 29d6813ab..b40855905 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -42,7 +42,18 @@ ) # Type system for dimension-aware type hints -from .types import Numeric_TPS, Numeric_PS, Numeric_S, Bool_TPS, Bool_PS, Bool_S, Effect_TPS, Effect_PS, Effect_S, Scalar +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. diff --git a/flixopt/components.py b/flixopt/components.py index b22b0caf5..6a5abfc4e 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -23,7 +23,7 @@ import linopy from .flow_system import FlowSystem - from .types import NumericData, Period, Scenario, Time + from .types import Numeric_PS, Numeric_TPS logger = logging.getLogger('flixopt') @@ -170,7 +170,7 @@ def __init__( inputs: list[Flow], outputs: list[Flow], on_off_parameters: OnOffParameters | None = None, - conversion_factors: list[dict[str, NumericData[Time, Scenario]]] | None = None, + conversion_factors: list[dict[str, Numeric_TPS]] | None = None, piecewise_conversion: PiecewiseConversion | None = None, meta_data: dict | None = None, ): @@ -917,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 ( diff --git a/flixopt/effects.py b/flixopt/effects.py index 3aa0117e1..fc77a3169 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -23,7 +23,7 @@ from collections.abc import Iterator from .flow_system import FlowSystem - from .types import EffectData, NumericData, Period, Scalar, Scenario, Time + from .types import Effect_PS, Effect_TPS, Numeric_PS, Numeric_TPS, Scalar logger = logging.getLogger('flixopt') @@ -196,12 +196,8 @@ def __init__( self.description = description self.is_standard = is_standard self.is_objective = is_objective - 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 {} - ) + 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( @@ -490,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: Effect_TPS - ) -> dict[str, Numeric_TPS] | 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: diff --git a/flixopt/elements.py b/flixopt/elements.py index 37e208435..224cc0f9c 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -23,7 +23,18 @@ import linopy from .flow_system import FlowSystem - from .types import Numeric_TPS, Numeric_PS, Numeric_S, Bool_TPS, Bool_PS, Bool_S, Effect_TPS, Effect_PS, Effect_S, Scalar + from .types import ( + Bool_PS, + Bool_S, + Bool_TPS, + Effect_PS, + Effect_S, + Effect_TPS, + Numeric_PS, + Numeric_S, + Numeric_TPS, + Scalar, + ) logger = logging.getLogger('flixopt') diff --git a/flixopt/features.py b/flixopt/features.py index 172b3dc24..e42b148e1 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from .core import FlowSystemDimensions, Scalar, TemporalData from .interface import InvestParameters, OnOffParameters, Piecewise - from .types import NumericData, Period, Scenario + from .types import Numeric_PS, Numeric_TPS logger = logging.getLogger('flixopt') @@ -154,7 +154,7 @@ def __init__( label_of_element: str, parameters: OnOffParameters, on_variable: linopy.Variable, - previous_states: TemporalData | None, + previous_states: Numeric_TPS | None, label_of_model: str | None = None, ): """ @@ -520,8 +520,8 @@ def __init__( label_of_model: str | None = None, total_max: Numeric_PS | None = None, total_min: Numeric_PS | None = None, - max_per_hour: TemporalData | None = None, - min_per_hour: TemporalData | None = None, + max_per_hour: Numeric_TPS | None = None, + min_per_hour: Numeric_TPS | None = None, ): if 'time' not in dims and (max_per_hour is not None or min_per_hour is not None): raise ValueError('Both max_per_hour and min_per_hour cannot be used when has_time_dim is False') diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 0af1007f1..72da046fd 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -32,7 +32,7 @@ import pyvis - from .types import BoolData, EffectData, NumericData, Period, Scenario, Time + from .types import Bool_TPS, Effect_TPS, Numeric_PS, Numeric_TPS logger = logging.getLogger('flixopt') diff --git a/flixopt/interface.py b/flixopt/interface.py index 715f894f2..23f9b72ab 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -20,7 +20,7 @@ from collections.abc import Iterator from .flow_system import FlowSystem - from .types import EffectData, NumericData, Period, Scenario, Time + from .types import Effect_PS, Effect_TPS, Numeric_PS, Numeric_TPS logger = logging.getLogger('flixopt') @@ -1264,9 +1264,7 @@ class OnOffParameters(Interface): def __init__( self, effects_per_switch_on: Effect_TPS | Numeric_TPS | None = None, - effects_per_running_hour: Effect_TPS - | Numeric_TPS - | None = None, + effects_per_running_hour: Effect_TPS | Numeric_TPS | None = None, on_hours_total_min: int | None = None, on_hours_total_max: int | None = None, consecutive_on_hours_min: Numeric_TPS | None = None, diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 5a9e46f94..d59c68f09 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from .elements import Flow from .interface import OnOffParameters - from .types import NumericData, Period, Scenario, Time + from .types import Numeric_TPS logger = logging.getLogger('flixopt') From cfceef61a4bbff83a6e83fc481a27a3ec1d04f11 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:54:39 +0100 Subject: [PATCH 16/34] Update type documentation --- flixopt/types.py | 250 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 197 insertions(+), 53 deletions(-) diff --git a/flixopt/types.py b/flixopt/types.py index 1411f3e30..a9ba8a63b 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -1,42 +1,56 @@ -""" -Type system for dimension-aware data in flixopt. - -This module provides type aliases that clearly communicate which dimensions -data can have. The type system is designed to be self-documenting while -maintaining maximum flexibility for input formats. - -Key Concepts ------------- -- Type aliases use suffix notation to indicate dimensions: - - `_T`: Time dimension only - - `_TS`: Time and Scenario dimensions - - `_PS`: Period and Scenario dimensions (no time) - - `_TPS`: Time, Period, and Scenario dimensions -- Data can have any subset of the specified dimensions (including being scalar) -- All standard input formats are supported (scalar, array, Series, DataFrame, DataArray) - -Examples --------- -Type hint `Numeric_T` accepts: - - Scalar: `0.5` (broadcast to all timesteps) - - 1D array: `np.array([1, 2, 3])` (matched to time dimension) - - pandas Series: with DatetimeIndex matching flow system - - xarray DataArray: with 'time' dimension - -Type hint `Numeric_TS` accepts: - - Scalar: `100` (broadcast to all time and scenario combinations) - - 1D array: matched to time OR scenario dimension - - 2D array: matched to both dimensions - - pandas DataFrame: columns as scenarios, index as time - - xarray DataArray: with any subset of 'time', 'scenario' dimensions - -Type hint `Numeric_PS` (periodic data, no time): - - Used for investment parameters that vary by planning period - - Accepts scalars, arrays matching periods/scenarios, or DataArrays - -Type hint `Scalar`: - - Only numeric scalars (int, float) - - Not converted to DataArray, stays as scalar +"""Type system for dimension-aware data in flixopt. + +This module provides type aliases that clearly communicate which dimensions data can +have, making function signatures self-documenting while maintaining maximum flexibility +for input formats. + +The type system uses suffix notation to indicate maximum dimensions: + - ``_TPS``: Time, Period, and Scenario dimensions + - ``_PS``: Period and Scenario dimensions (no time) + - ``_S``: Scenario dimension only + - No suffix: Scalar values only + +All dimensioned types accept any subset of their specified dimensions, including scalars +which are automatically broadcast to all dimensions. + +Supported Input Formats: + - Scalars: ``int``, ``float`` (including numpy types) + - Arrays: ``numpy.ndarray`` (matched by length/shape to dimensions) + - Series: ``pandas.Series`` (matched by index to dimension coordinates) + - DataFrames: ``pandas.DataFrame`` (typically columns=scenarios, index=time) + - DataArrays: ``xarray.DataArray`` (used directly with dimension validation) + +Example: + Basic usage with different dimension combinations:: + ```python + from flixopt.types import Numeric_TPS, Numeric_PS, Scalar + + def create_flow( + label: str, + size: Numeric_PS = None, # Can be scalar, array, Series, etc. + profile: Numeric_TPS = 1.0, # Accepts time-varying data + efficiency: Scalar = 0.95, # Only scalars + ): + ... + + # All of these are valid: + create_flow("heat", size=100) # Scalar broadcast + create_flow("heat", size=np.array([100, 150])) # Period-varying + create_flow("heat", profile=pd.DataFrame(...)) # Time + scenario + ``` + +Note: + Data can have **any subset** of the specified dimensions. For example, + ``Numeric_TPS`` can accept: + - Scalar: ``0.5`` (broadcast to all time, period, scenario combinations) + - 1D array: matched to one dimension + - 2D array: matched to two dimensions + - 3D array: matched to all three dimensions + - ``xarray.DataArray``: with any subset of 'time', 'period', 'scenario' dims + +See Also: + DataConverter.to_dataarray: Implementation of data conversion logic + FlowSystem.fit_to_model_coords: Fits data to model coordinate system """ from typing import TypeAlias @@ -45,28 +59,158 @@ import pandas as pd import xarray as xr -# Internal base types +# Internal base types - not exported _Numeric: TypeAlias = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray +"""Base numeric type union accepting scalars, arrays, Series, DataFrames, and DataArrays.""" + _Bool: TypeAlias = bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray +"""Base boolean type union accepting bool scalars, arrays, Series, DataFrames, and DataArrays.""" + _Effect: TypeAlias = _Numeric | dict[str, _Numeric] +"""Base effect type union accepting numeric data or dict of numeric data for named effects.""" + + +# Numeric data type aliases with dimension combinations +Numeric_TPS: TypeAlias = _Numeric +"""Numeric data with at most Time, Period, and Scenario dimensions. + +Use this for data that can vary across time steps, planning periods, and scenarios. +Accepts any subset of these dimensions including scalars (broadcast to all dimensions). + +Example: + :: -# Numeric data with dimension combinations -Numeric_TPS: TypeAlias = _Numeric # Time, Period, Scenario -Numeric_PS: TypeAlias = _Numeric # Period, Scenario -Numeric_S: TypeAlias = _Numeric # Scenario + efficiency: Numeric_TPS = 0.95 # Scalar broadcast + efficiency: Numeric_TPS = np.array([...]) # Time-varying + efficiency: Numeric_TPS = pd.DataFrame(...) # Time + scenarios +""" + +Numeric_PS: TypeAlias = _Numeric +"""Numeric data with at most Period and Scenario dimensions (no time variation). + +Use this for investment parameters that vary by planning period and scenario but not +within each period (e.g., investment costs, capacity sizes). + +Example: + :: + + size: Numeric_PS = 100 # Scalar + size: Numeric_PS = np.array([100, 150, 200]) # Period-varying + size: Numeric_PS = pd.DataFrame(...) # Period + scenario combinations +""" + +Numeric_S: TypeAlias = _Numeric +"""Numeric data with at most Scenario dimension. + +Use this for scenario-specific parameters that don't vary over time or periods. -# Boolean data with dimension combinations -Bool_TPS: TypeAlias = _Bool # Time, Period, Scenario -Bool_PS: TypeAlias = _Bool # Period, Scenario -Bool_S: TypeAlias = _Bool # Scenario +Example: + :: + + discount_rate: Numeric_S = 0.05 # Same for all scenarios + discount_rate: Numeric_S = pd.Series([0.03, 0.05, 0.07]) # Scenario-varying +""" + + +# Boolean data type aliases with dimension combinations +Bool_TPS: TypeAlias = _Bool +"""Boolean data with at most Time, Period, and Scenario dimensions. + +Use this for binary flags or activation states that can vary across time, periods, +and scenarios (e.g., on/off constraints, feasibility indicators). + +Example: + :: + + is_active: Bool_TPS = True # Always active + is_active: Bool_TPS = np.array([True, False, True, ...]) # Time-varying +""" -# Effect data with dimension combinations -Effect_TPS: TypeAlias = _Effect # Time, Period, Scenario -Effect_PS: TypeAlias = _Effect # Period, Scenario -Effect_S: TypeAlias = _Effect # Scenario +Bool_PS: TypeAlias = _Bool +"""Boolean data with at most Period and Scenario dimensions. -# Scalar (no dimensions) +Use this for binary investment decisions or constraints that vary by period and +scenario but not within each period. + +Example: + :: + + can_invest: Bool_PS = True # Can invest in all periods + can_invest: Bool_PS = np.array([False, True, True]) # Period-specific +""" + +Bool_S: TypeAlias = _Bool +"""Boolean data with at most Scenario dimension. + +Use this for scenario-specific binary flags. + +Example: + :: + + high_demand: Bool_S = False # Same for all scenarios + high_demand: Bool_S = pd.Series([False, True, True]) # Scenario-varying +""" + + +# Effect data type aliases with dimension combinations +Effect_TPS: TypeAlias = _Effect +"""Effect data with at most Time, Period, and Scenario dimensions. + +Effects represent costs, emissions, or other impacts. Can be a single numeric value +or a dict mapping effect names to numeric values for multiple named effects. + +Example: + :: + + # Single effect + cost: Effect_TPS = 10.5 + cost: Effect_TPS = np.array([10, 12, 11, ...]) + + # Multiple named effects + effects: Effect_TPS = { + 'CO2': 0.5, + 'costs': np.array([100, 120, 110, ...]), + } +""" + +Effect_PS: TypeAlias = _Effect +"""Effect data with at most Period and Scenario dimensions. + +Use this for period-specific effects like investment costs or periodic emissions. + +Example: + :: + + investment_cost: Effect_PS = 1000 # Fixed cost + investment_cost: Effect_PS = {'capex': 1000, 'opex': 50} # Multiple effects +""" + +Effect_S: TypeAlias = _Effect +"""Effect data with at most Scenario dimension. + +Use this for scenario-specific effects. + +Example: + :: + + carbon_price: Effect_S = 50 # Same for all scenarios + carbon_price: Effect_S = pd.Series([30, 50, 70]) # Scenario-varying +""" + + +# Scalar type (no dimensions) Scalar: TypeAlias = int | float | np.integer | np.floating +"""Scalar numeric values only (no arrays or DataArrays). + +Use this when you specifically want to accept only scalar values, not arrays. +Unlike dimensioned types, scalars are not converted to DataArrays internally. + +Example: + :: + + efficiency: Scalar = 0.95 # OK + efficiency: Scalar = np.array([0.95]) # Type error - array not allowed +""" # Export public API __all__ = [ From d0fac14daf36a1133285d21069924f63ce679bc4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:57:40 +0100 Subject: [PATCH 17/34] Update type documentation --- flixopt/types.py | 215 ++++++++++------------------------------------- 1 file changed, 45 insertions(+), 170 deletions(-) diff --git a/flixopt/types.py b/flixopt/types.py index a9ba8a63b..cc4069a2e 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -1,56 +1,38 @@ """Type system for dimension-aware data in flixopt. -This module provides type aliases that clearly communicate which dimensions data can -have, making function signatures self-documenting while maintaining maximum flexibility -for input formats. - -The type system uses suffix notation to indicate maximum dimensions: - - ``_TPS``: Time, Period, and Scenario dimensions - - ``_PS``: Period and Scenario dimensions (no time) - - ``_S``: Scenario dimension only - - No suffix: Scalar values only - -All dimensioned types accept any subset of their specified dimensions, including scalars -which are automatically broadcast to all dimensions. - -Supported Input Formats: - - Scalars: ``int``, ``float`` (including numpy types) - - Arrays: ``numpy.ndarray`` (matched by length/shape to dimensions) - - Series: ``pandas.Series`` (matched by index to dimension coordinates) - - DataFrames: ``pandas.DataFrame`` (typically columns=scenarios, index=time) - - DataArrays: ``xarray.DataArray`` (used directly with dimension validation) +Type aliases use suffix notation to indicate maximum dimensions. Data can have any +subset of these dimensions (including scalars, which are broadcast to all dimensions). + +| Suffix | Dimensions | Use Case | +|--------|------------|----------| +| `_TPS` | Time, Period, Scenario | Time-varying data across all dimensions | +| `_PS` | Period, Scenario | Investment parameters (no time variation) | +| `_S` | Scenario | Scenario-specific parameters | +| (none) | Scalar only | Single numeric values | + +All dimensioned types accept: scalars (`int`, `float`), arrays (`ndarray`), +Series (`pd.Series`), DataFrames (`pd.DataFrame`), or DataArrays (`xr.DataArray`). Example: - Basic usage with different dimension combinations:: - ```python - from flixopt.types import Numeric_TPS, Numeric_PS, Scalar - - def create_flow( - label: str, - size: Numeric_PS = None, # Can be scalar, array, Series, etc. - profile: Numeric_TPS = 1.0, # Accepts time-varying data - efficiency: Scalar = 0.95, # Only scalars - ): - ... - - # All of these are valid: - create_flow("heat", size=100) # Scalar broadcast - create_flow("heat", size=np.array([100, 150])) # Period-varying - create_flow("heat", profile=pd.DataFrame(...)) # Time + scenario - ``` + ```python + from flixopt.types import Numeric_TPS, Numeric_PS, Scalar + + def create_flow( + size: Numeric_PS = None, # Scalar, array, Series, DataFrame, or DataArray + profile: Numeric_TPS = 1.0, # Time-varying data + efficiency: Scalar = 0.95, # Scalars only + ): + ... + + # All valid: + create_flow(size=100) # Scalar broadcast + create_flow(size=np.array([100, 150])) # Period-varying + create_flow(profile=pd.DataFrame(...)) # Time + scenario + ``` Note: - Data can have **any subset** of the specified dimensions. For example, - ``Numeric_TPS`` can accept: - - Scalar: ``0.5`` (broadcast to all time, period, scenario combinations) - - 1D array: matched to one dimension - - 2D array: matched to two dimensions - - 3D array: matched to all three dimensions - - ``xarray.DataArray``: with any subset of 'time', 'period', 'scenario' dims - -See Also: - DataConverter.to_dataarray: Implementation of data conversion logic - FlowSystem.fit_to_model_coords: Fits data to model coordinate system + Data can have **any subset** of specified dimensions. `Numeric_TPS` accepts scalars, + 1D/2D/3D arrays, or DataArrays with any subset of 'time', 'period', 'scenario' dims. """ from typing import TypeAlias @@ -61,156 +43,49 @@ def create_flow( # Internal base types - not exported _Numeric: TypeAlias = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray -"""Base numeric type union accepting scalars, arrays, Series, DataFrames, and DataArrays.""" - _Bool: TypeAlias = bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray -"""Base boolean type union accepting bool scalars, arrays, Series, DataFrames, and DataArrays.""" - _Effect: TypeAlias = _Numeric | dict[str, _Numeric] -"""Base effect type union accepting numeric data or dict of numeric data for named effects.""" -# Numeric data type aliases with dimension combinations +# Numeric data types Numeric_TPS: TypeAlias = _Numeric -"""Numeric data with at most Time, Period, and Scenario dimensions. - -Use this for data that can vary across time steps, planning periods, and scenarios. -Accepts any subset of these dimensions including scalars (broadcast to all dimensions). - -Example: - :: - - efficiency: Numeric_TPS = 0.95 # Scalar broadcast - efficiency: Numeric_TPS = np.array([...]) # Time-varying - efficiency: Numeric_TPS = pd.DataFrame(...) # Time + scenarios -""" +"""Time, Period, Scenario dimensions. For time-varying data across all dimensions.""" Numeric_PS: TypeAlias = _Numeric -"""Numeric data with at most Period and Scenario dimensions (no time variation). - -Use this for investment parameters that vary by planning period and scenario but not -within each period (e.g., investment costs, capacity sizes). - -Example: - :: - - size: Numeric_PS = 100 # Scalar - size: Numeric_PS = np.array([100, 150, 200]) # Period-varying - size: Numeric_PS = pd.DataFrame(...) # Period + scenario combinations -""" +"""Period, Scenario dimensions. For investment parameters (e.g., size, costs).""" Numeric_S: TypeAlias = _Numeric -"""Numeric data with at most Scenario dimension. +"""Scenario dimension. For scenario-specific parameters (e.g., discount rates).""" -Use this for scenario-specific parameters that don't vary over time or periods. -Example: - :: - - discount_rate: Numeric_S = 0.05 # Same for all scenarios - discount_rate: Numeric_S = pd.Series([0.03, 0.05, 0.07]) # Scenario-varying -""" - - -# Boolean data type aliases with dimension combinations +# Boolean data types Bool_TPS: TypeAlias = _Bool -"""Boolean data with at most Time, Period, and Scenario dimensions. - -Use this for binary flags or activation states that can vary across time, periods, -and scenarios (e.g., on/off constraints, feasibility indicators). - -Example: - :: - - is_active: Bool_TPS = True # Always active - is_active: Bool_TPS = np.array([True, False, True, ...]) # Time-varying -""" +"""Time, Period, Scenario dimensions. For time-varying binary flags/constraints.""" Bool_PS: TypeAlias = _Bool -"""Boolean data with at most Period and Scenario dimensions. - -Use this for binary investment decisions or constraints that vary by period and -scenario but not within each period. - -Example: - :: - - can_invest: Bool_PS = True # Can invest in all periods - can_invest: Bool_PS = np.array([False, True, True]) # Period-specific -""" +"""Period, Scenario dimensions. For period-specific binary decisions.""" Bool_S: TypeAlias = _Bool -"""Boolean data with at most Scenario dimension. - -Use this for scenario-specific binary flags. +"""Scenario dimension. For scenario-specific binary flags.""" -Example: - :: - high_demand: Bool_S = False # Same for all scenarios - high_demand: Bool_S = pd.Series([False, True, True]) # Scenario-varying -""" - - -# Effect data type aliases with dimension combinations +# Effect data types Effect_TPS: TypeAlias = _Effect -"""Effect data with at most Time, Period, and Scenario dimensions. - -Effects represent costs, emissions, or other impacts. Can be a single numeric value -or a dict mapping effect names to numeric values for multiple named effects. - -Example: - :: - - # Single effect - cost: Effect_TPS = 10.5 - cost: Effect_TPS = np.array([10, 12, 11, ...]) - - # Multiple named effects - effects: Effect_TPS = { - 'CO2': 0.5, - 'costs': np.array([100, 120, 110, ...]), - } -""" +"""Time, Period, Scenario dimensions. For time-varying effects (costs, emissions). +Can be single numeric value or dict mapping effect names to values.""" Effect_PS: TypeAlias = _Effect -"""Effect data with at most Period and Scenario dimensions. - -Use this for period-specific effects like investment costs or periodic emissions. - -Example: - :: - - investment_cost: Effect_PS = 1000 # Fixed cost - investment_cost: Effect_PS = {'capex': 1000, 'opex': 50} # Multiple effects -""" +"""Period, Scenario dimensions. For period-specific effects (investment costs). +Can be single numeric value or dict mapping effect names to values.""" Effect_S: TypeAlias = _Effect -"""Effect data with at most Scenario dimension. - -Use this for scenario-specific effects. - -Example: - :: - - carbon_price: Effect_S = 50 # Same for all scenarios - carbon_price: Effect_S = pd.Series([30, 50, 70]) # Scenario-varying -""" +"""Scenario dimension. For scenario-specific effects (carbon prices). +Can be single numeric value or dict mapping effect names to values.""" # Scalar type (no dimensions) Scalar: TypeAlias = int | float | np.integer | np.floating -"""Scalar numeric values only (no arrays or DataArrays). - -Use this when you specifically want to accept only scalar values, not arrays. -Unlike dimensioned types, scalars are not converted to DataArrays internally. - -Example: - :: - - efficiency: Scalar = 0.95 # OK - efficiency: Scalar = np.array([0.95]) # Type error - array not allowed -""" +"""Scalar numeric values only. Not converted to DataArray (unlike dimensioned types).""" # Export public API __all__ = [ From 1187a2ca827bd176f9d895e4f8f1be01a0f8a085 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:59:32 +0100 Subject: [PATCH 18/34] Update type documentation --- flixopt/types.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flixopt/types.py b/flixopt/types.py index cc4069a2e..0a8ef66d9 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -30,9 +30,11 @@ def create_flow( create_flow(profile=pd.DataFrame(...)) # Time + scenario ``` -Note: - Data can have **any subset** of specified dimensions. `Numeric_TPS` accepts scalars, - 1D/2D/3D arrays, or DataArrays with any subset of 'time', 'period', 'scenario' dims. +Important: + Data can have **any subset** of specified dimensions, but **cannot have more + dimensions than the FlowSystem**. If the FlowSystem has only time dimension, + you cannot pass period or scenario data. The type hints indicate the maximum + dimensions that could be used if they exist in the FlowSystem. """ from typing import TypeAlias From 8abf6fb175c1c41eef14cc402980bd349617b784 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:12:11 +0100 Subject: [PATCH 19/34] Add another datatype --- flixopt/core.py | 13 +++---------- flixopt/features.py | 2 +- flixopt/io.py | 4 +++- flixopt/types.py | 5 +++++ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index c10248c6c..0d70e255b 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -12,6 +12,8 @@ import pandas as pd import xarray as xr +from .types import NumericOrBool + logger = logging.getLogger('flixopt') FlowSystemDimensions = Literal['time', 'period', 'scenario'] @@ -387,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: """ diff --git a/flixopt/features.py b/flixopt/features.py index e42b148e1..b00ccc547 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -15,7 +15,7 @@ from .structure import FlowSystemModel, Submodel if TYPE_CHECKING: - from .core import FlowSystemDimensions, Scalar, TemporalData + from .core import FlowSystemDimensions from .interface import InvestParameters, OnOffParameters, Piecewise from .types import Numeric_PS, Numeric_TPS diff --git a/flixopt/io.py b/flixopt/io.py index 3c53c4170..e83738d89 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -19,6 +19,8 @@ if TYPE_CHECKING: import linopy + from .types import Numeric_TPS + logger = logging.getLogger('flixopt') @@ -651,7 +653,7 @@ def update(self, new_name: str | None = None, new_folder: pathlib.Path | None = def numeric_to_str_for_repr( - value: int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray, + value: Numeric_TPS, precision: int = 1, atol: float = 1e-10, ) -> str: diff --git a/flixopt/types.py b/flixopt/types.py index 0a8ef66d9..b5c92a5fe 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -48,6 +48,10 @@ def create_flow( _Bool: TypeAlias = bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray _Effect: TypeAlias = _Numeric | dict[str, _Numeric] +# Combined type for numeric or boolean data (no dimension information) +NumericOrBool: TypeAlias = int | float | bool | np.integer | np.floating | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray +"""Numeric or boolean data without dimension metadata. For internal utilities.""" + # Numeric data types Numeric_TPS: TypeAlias = _Numeric @@ -101,4 +105,5 @@ def create_flow( 'Effect_PS', 'Effect_S', 'Scalar', + 'NumericOrBool', ] From 73dc336747721d7d4d86ebe207494073340ef83b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:12:20 +0100 Subject: [PATCH 20/34] Fix typehints --- flixopt/interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 23f9b72ab..bc7adabbc 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1265,13 +1265,13 @@ def __init__( self, effects_per_switch_on: Effect_TPS | Numeric_TPS | None = None, effects_per_running_hour: Effect_TPS | Numeric_TPS | None = None, - on_hours_total_min: int | None = None, - on_hours_total_max: int | None = None, + on_hours_total_min: Numeric_PS | None = None, + on_hours_total_max: Numeric_PS | None = None, consecutive_on_hours_min: Numeric_TPS | None = None, consecutive_on_hours_max: Numeric_TPS | None = None, consecutive_off_hours_min: Numeric_TPS | None = None, consecutive_off_hours_max: Numeric_TPS | None = None, - switch_on_total_max: int | None = None, + switch_on_total_max: Numeric_PS | None = None, force_switch_on: bool = False, ): self.effects_per_switch_on = effects_per_switch_on if effects_per_switch_on is not None else {} From c5afcbd824b8fa9b4f28977759f52f87f6a445ca Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:14:22 +0100 Subject: [PATCH 21/34] Fix typehints --- flixopt/effects.py | 4 ++-- flixopt/flow_system.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index fc77a3169..d428cccd2 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -52,10 +52,10 @@ class Effect(Element): 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. - Type: `TemporalEffectsUser` (single value or dict with dimensions [Time, Period, Scenario]) + 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: `PeriodicEffectsUser` (single value or dict with dimensions [Period, Scenario]) + 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. diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 72da046fd..081359076 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -32,7 +32,7 @@ import pyvis - from .types import Bool_TPS, Effect_TPS, Numeric_PS, Numeric_TPS + from .types import Bool_TPS, Effect_TPS, Numeric_PS, Numeric_TPS, NumericOrBool logger = logging.getLogger('flixopt') @@ -523,7 +523,7 @@ def to_json(self, path: str | pathlib.Path): def fit_to_model_coords( self, name: str, - data: Numeric_TPS | Bool_TPS | None, + data: NumericOrBool | None, dims: Collection[FlowSystemDimensions] | None = None, ) -> xr.DataArray | None: """ @@ -531,7 +531,7 @@ def fit_to_model_coords( Args: name: Name of the data - data: Data to fit to model coordinates + data: Data to fit to model coordinates (accepts any dimensionality including scalars) dims: Collection of dimension names to use for fitting. If None, all dimensions are used. Returns: From 559a9fe486d6e64d8850629ffc76e83a04f47705 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:15:01 +0100 Subject: [PATCH 22/34] Fix validation in linear_converter classes --- flixopt/linear_converters.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index d59c68f09..364af1b1d 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -87,12 +87,12 @@ def __init__( label, inputs=[Q_fu], outputs=[Q_th], - conversion_factors=[{Q_fu.label: eta, Q_th.label: 1}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.Q_fu = Q_fu self.Q_th = Q_th + self.eta = eta #Uses setter @property def eta(self): @@ -101,7 +101,7 @@ def eta(self): @eta.setter def eta(self, value): check_bounds(value, 'eta', self.label_full, 0, 1) - self.conversion_factors[0][self.Q_fu.label] = value + self.conversion_factors = [{self.Q_fu.label: value, self.Q_th.label: 1}] @register_class_for_io @@ -563,16 +563,14 @@ def __init__( label, inputs=[P_el, Q_ab], outputs=[Q_th], - conversion_factors=[{P_el.label: COP, Q_th.label: 1}, {Q_ab.label: COP / (COP - 1), Q_th.label: 1}], + conversion_factors=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.P_el = P_el self.Q_ab = Q_ab self.Q_th = Q_th - - if np.any(np.asarray(self.COP) <= 1): - raise ValueError(f'{self.label_full}.COP must be strictly > 1 for HeatPumpWithSource.') + self.COP = COP # Uses setter @property def COP(self): # noqa: N802 From 94a0ca4ff283ebf73897eaa33e261e0b255fbbe7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:23:36 +0100 Subject: [PATCH 23/34] Fix validation in linear_converter classes --- flixopt/linear_converters.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 364af1b1d..040675b76 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -174,13 +174,13 @@ def __init__( label, inputs=[P_el], outputs=[Q_th], - conversion_factors=[{P_el.label: eta, Q_th.label: 1}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.P_el = P_el self.Q_th = Q_th + self.eta = eta # Uses setter @property def eta(self): @@ -189,7 +189,7 @@ def eta(self): @eta.setter def eta(self, value): check_bounds(value, 'eta', self.label_full, 0, 1) - self.conversion_factors[0][self.P_el.label] = value + self.conversion_factors = [{self.P_el.label: value, self.Q_th.label: 1}] @register_class_for_io @@ -261,13 +261,13 @@ def __init__( label, inputs=[P_el], outputs=[Q_th], - conversion_factors=[{P_el.label: COP, Q_th.label: 1}], + conversion_factors=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.P_el = P_el self.Q_th = Q_th - self.COP = COP + self.COP = COP # Uses setter @property def COP(self): # noqa: N802 @@ -276,7 +276,7 @@ def COP(self): # noqa: N802 @COP.setter def COP(self, value): # noqa: N802 check_bounds(value, 'COP', self.label_full, 1, 20) - self.conversion_factors[0][self.P_el.label] = value + self.conversion_factors = [{self.P_el.label: value, self.Q_th.label: 1}] @register_class_for_io @@ -350,15 +350,13 @@ def __init__( label, inputs=[P_el, Q_th], outputs=[], - conversion_factors=[{P_el.label: -1, Q_th.label: specific_electricity_demand}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.P_el = P_el self.Q_th = Q_th - - check_bounds(specific_electricity_demand, 'specific_electricity_demand', self.label_full, 0, 1) + self.specific_electricity_demand = specific_electricity_demand # Uses setter @property def specific_electricity_demand(self): @@ -367,7 +365,7 @@ def specific_electricity_demand(self): @specific_electricity_demand.setter def specific_electricity_demand(self, value): check_bounds(value, 'specific_electricity_demand', self.label_full, 0, 1) - self.conversion_factors[0][self.Q_th.label] = value + self.conversion_factors = [{self.P_el.label: -1, self.Q_th.label: value}] @register_class_for_io @@ -446,14 +444,11 @@ def __init__( on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, ): - heat = {Q_fu.label: eta_th, Q_th.label: 1} - electricity = {Q_fu.label: eta_el, P_el.label: 1} - super().__init__( label, inputs=[Q_fu], outputs=[Q_th, P_el], - conversion_factors=[heat, electricity], + conversion_factors=[], on_off_parameters=on_off_parameters, meta_data=meta_data, ) @@ -461,6 +456,8 @@ def __init__( self.Q_fu = Q_fu self.P_el = P_el self.Q_th = Q_th + self.eta_th = eta_th # Uses setter + self.eta_el = eta_el # Uses setter check_bounds(eta_el + eta_th, 'eta_th+eta_el', self.label_full, 0, 1) @@ -471,7 +468,11 @@ def eta_th(self): @eta_th.setter def eta_th(self, value): check_bounds(value, 'eta_th', self.label_full, 0, 1) - self.conversion_factors[0][self.Q_fu.label] = value + if len(self.conversion_factors) < 2: + # Initialize structure if not yet set + self.conversion_factors = [{self.Q_fu.label: value, self.Q_th.label: 1}, {}] + else: + self.conversion_factors[0] = {self.Q_fu.label: value, self.Q_th.label: 1} @property def eta_el(self): @@ -480,7 +481,11 @@ def eta_el(self): @eta_el.setter def eta_el(self, value): check_bounds(value, 'eta_el', self.label_full, 0, 1) - self.conversion_factors[1][self.Q_fu.label] = value + if len(self.conversion_factors) < 2: + # Initialize structure if not yet set + self.conversion_factors = [{}, {self.Q_fu.label: value, self.P_el.label: 1}] + else: + self.conversion_factors[1] = {self.Q_fu.label: value, self.P_el.label: 1} @register_class_for_io From ef1a2eea2cc9554d7a8c84709e830d76def37725 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:26:54 +0100 Subject: [PATCH 24/34] pre commit run --- flixopt/linear_converters.py | 2 +- flixopt/types.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 040675b76..44fe11795 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -92,7 +92,7 @@ def __init__( ) self.Q_fu = Q_fu self.Q_th = Q_th - self.eta = eta #Uses setter + self.eta = eta # Uses setter @property def eta(self): diff --git a/flixopt/types.py b/flixopt/types.py index b5c92a5fe..f53d308c4 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -17,17 +17,18 @@ ```python from flixopt.types import Numeric_TPS, Numeric_PS, Scalar + def create_flow( - size: Numeric_PS = None, # Scalar, array, Series, DataFrame, or DataArray - profile: Numeric_TPS = 1.0, # Time-varying data - efficiency: Scalar = 0.95, # Scalars only - ): - ... + size: Numeric_PS = None, # Scalar, array, Series, DataFrame, or DataArray + profile: Numeric_TPS = 1.0, # Time-varying data + efficiency: Scalar = 0.95, # Scalars only + ): ... + # All valid: - create_flow(size=100) # Scalar broadcast - create_flow(size=np.array([100, 150])) # Period-varying - create_flow(profile=pd.DataFrame(...)) # Time + scenario + create_flow(size=100) # Scalar broadcast + create_flow(size=np.array([100, 150])) # Period-varying + create_flow(profile=pd.DataFrame(...)) # Time + scenario ``` Important: @@ -49,7 +50,9 @@ def create_flow( _Effect: TypeAlias = _Numeric | dict[str, _Numeric] # Combined type for numeric or boolean data (no dimension information) -NumericOrBool: TypeAlias = int | float | bool | np.integer | np.floating | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray +NumericOrBool: TypeAlias = ( + int | float | bool | np.integer | np.floating | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray +) """Numeric or boolean data without dimension metadata. For internal utilities.""" From 4fa6c6b047176f5f91e32d499b9fb60c1aa1f06d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:46:19 +0100 Subject: [PATCH 25/34] Fix Effect type --- flixopt/effects.py | 8 ++++---- flixopt/types.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index d428cccd2..79bdf7f2f 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -179,8 +179,8 @@ def __init__( meta_data: dict | None = None, is_standard: bool = False, is_objective: bool = False, - share_from_temporal: Effect_TPS | None = None, - share_from_periodic: Effect_PS | None = None, + share_from_temporal: Effect_TPS | Numeric_TPS | None = None, + share_from_periodic: Effect_PS | Numeric_PS | None = None, minimum_temporal: Numeric_PS | None = None, maximum_temporal: Numeric_PS | None = None, minimum_periodic: Numeric_PS | None = None, @@ -196,8 +196,8 @@ def __init__( self.description = description self.is_standard = is_standard self.is_objective = is_objective - 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 {} + self.share_from_temporal = share_from_temporal if share_from_temporal is not None else {} + self.share_from_periodic = 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( diff --git a/flixopt/types.py b/flixopt/types.py index f53d308c4..80d1c2062 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -47,7 +47,7 @@ def create_flow( # Internal base types - not exported _Numeric: TypeAlias = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray _Bool: TypeAlias = bool | np.bool_ | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray -_Effect: TypeAlias = _Numeric | dict[str, _Numeric] +_Effect: TypeAlias = dict[str, _Numeric] # Combined type for numeric or boolean data (no dimension information) NumericOrBool: TypeAlias = ( From 12bc0a696c62e98205ce15aaee1434d38c4dde15 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:58:28 +0100 Subject: [PATCH 26/34] Improve Fix: check_bounds function to allow for more types --- flixopt/linear_converters.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 44fe11795..041678342 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -615,13 +615,19 @@ def check_bounds( lower_bound = lower_bound.data if isinstance(upper_bound, TimeSeriesData): upper_bound = upper_bound.data - if not np.all(value > lower_bound): + + # Convert to NumPy arrays to handle xr.DataArray, pd.Series, pd.DataFrame + value_arr = np.asarray(value) + lower_arr = np.asarray(lower_bound) + upper_arr = np.asarray(upper_bound) + + if not np.all(value_arr > lower_arr): logger.warning( f"'{element_label}.{parameter_label}' is equal or below the common lower bound {lower_bound}." - f' {parameter_label}.min={np.min(value)}; {parameter_label}={value}' + f' {parameter_label}.min={np.min(value_arr)}; {parameter_label}={value}' ) - if not np.all(value < upper_bound): + if not np.all(value_arr < upper_arr): logger.warning( f"'{element_label}.{parameter_label}' exceeds or matches the common upper bound {upper_bound}." - f' {parameter_label}.max={np.max(value)}; {parameter_label}={value}' + f' {parameter_label}.max={np.max(value_arr)}; {parameter_label}={value}' ) From 9b4781d6a73dae9a663d07da765706e151e2205d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:59:02 +0100 Subject: [PATCH 27/34] Type Hints: OnOffParameters attribute --- flixopt/interface.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index bc7adabbc..e22ceebd5 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1276,13 +1276,13 @@ def __init__( ): self.effects_per_switch_on = effects_per_switch_on if effects_per_switch_on is not None else {} self.effects_per_running_hour = effects_per_running_hour if effects_per_running_hour is not None else {} - self.on_hours_total_min: Numeric_PS = on_hours_total_min - self.on_hours_total_max: Numeric_PS = on_hours_total_max - self.consecutive_on_hours_min: Numeric_TPS = consecutive_on_hours_min - self.consecutive_on_hours_max: Numeric_TPS = consecutive_on_hours_max - self.consecutive_off_hours_min: Numeric_TPS = consecutive_off_hours_min - self.consecutive_off_hours_max: Numeric_TPS = consecutive_off_hours_max - self.switch_on_total_max: Numeric_PS = switch_on_total_max + self.on_hours_total_min = on_hours_total_min + self.on_hours_total_max = on_hours_total_max + self.consecutive_on_hours_min = consecutive_on_hours_min + self.consecutive_on_hours_max = consecutive_on_hours_max + self.consecutive_off_hours_min = consecutive_off_hours_min + self.consecutive_off_hours_max = consecutive_off_hours_max + self.switch_on_total_max = switch_on_total_max self.force_switch_on: bool = force_switch_on def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: From ae59f91c80c8223eb89751473c2f4512203a3cd6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:01:43 +0100 Subject: [PATCH 28/34] ShareAllocationModel None/inf inconsistency --- flixopt/features.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index b00ccc547..519693885 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -536,18 +536,18 @@ def __init__( self._eq_total: linopy.Constraint | None = None # Parameters - self._total_max = total_max if total_max is not None else np.inf - self._total_min = total_min if total_min is not None else -np.inf - self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf - self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf + self._total_max = total_max + self._total_min = total_min + self._max_per_hour = max_per_hour + self._min_per_hour = min_per_hour super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) def _do_modeling(self): super()._do_modeling() self.total = self.add_variables( - lower=self._total_min, - upper=self._total_max, + lower=self._total_min if self._total_min is not None else -np.inf, + upper=self._total_max if self._total_max is not None else np.inf, coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']), name=self.label_full, short_name='total', From 2dd882a3b5ad5554b6b2024d35b116149a06d0de Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:23:36 +0100 Subject: [PATCH 29/34] 4. Documentation: Effect type docstrings (flixopt/types.py:82-92) - Problem: Docstrings incorrectly stated Effect types "can be single numeric value" - Solution: Clarified that Effect types are dicts, and single values work through union types like Effect_TPS | Numeric_TPS 5. Documentation: Share parameter semantics (flixopt/effects.py:199-203) - Problem: Mixed handling of dict/numeric values between initialization and transformation wasn't documented - Solution: Added clarifying comments explaining that parameters accept unions, are stored as-is, and normalized later via transform_data() All changes maintain backward compatibility while fixing runtime errors and improving code clarity --- flixopt/effects.py | 3 +++ flixopt/types.py | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 79bdf7f2f..d1f133678 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -196,6 +196,9 @@ def __init__( self.description = description self.is_standard = is_standard self.is_objective = is_objective + # Share parameters accept Effect_* | Numeric_* unions (dict or single value). + # Store as-is here; transform_data() will normalize via fit_effects_to_model_coords(). + # Default to {} when None (no shares defined). self.share_from_temporal = share_from_temporal if share_from_temporal is not None else {} self.share_from_periodic = share_from_periodic if share_from_periodic is not None else {} diff --git a/flixopt/types.py b/flixopt/types.py index 80d1c2062..924fae380 100644 --- a/flixopt/types.py +++ b/flixopt/types.py @@ -80,16 +80,16 @@ def create_flow( # Effect data types Effect_TPS: TypeAlias = _Effect -"""Time, Period, Scenario dimensions. For time-varying effects (costs, emissions). -Can be single numeric value or dict mapping effect names to values.""" +"""Time, Period, Scenario dimensions. Dict mapping effect names to values. +For time-varying effects (costs, emissions). Use `Effect_TPS | Numeric_TPS` to accept single values.""" Effect_PS: TypeAlias = _Effect -"""Period, Scenario dimensions. For period-specific effects (investment costs). -Can be single numeric value or dict mapping effect names to values.""" +"""Period, Scenario dimensions. Dict mapping effect names to values. +For period-specific effects (investment costs). Use `Effect_PS | Numeric_PS` to accept single values.""" Effect_S: TypeAlias = _Effect -"""Scenario dimension. For scenario-specific effects (carbon prices). -Can be single numeric value or dict mapping effect names to values.""" +"""Scenario dimension. Dict mapping effect names to values. +For scenario-specific effects (carbon prices). Use `Effect_S | Numeric_S` to accept single values.""" # Scalar type (no dimensions) From c80804eb7ee45d5c26356ddc89b03930cb10c494 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:25:43 +0100 Subject: [PATCH 30/34] Remove types from init --- flixopt/__init__.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index b40855905..3633d86a1 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -41,20 +41,6 @@ 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. From d70958bbdaf2d9a3ab63860a17b05e3de3927c8f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:27:45 +0100 Subject: [PATCH 31/34] Lets type hints define types instead of docstring --- flixopt/effects.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index d1f133678..02c850050 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -52,26 +52,15 @@ class Effect(Element): 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. - 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. From 3dda0033dd9752b8406e88f3ec8670606342cec9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:36:37 +0100 Subject: [PATCH 32/34] Revert changes adressed in another PR --- flixopt/linear_converters.py | 45 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 041678342..046fcbd51 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -87,12 +87,12 @@ def __init__( label, inputs=[Q_fu], outputs=[Q_th], + conversion_factors=[{Q_fu.label: eta, Q_th.label: 1}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.Q_fu = Q_fu self.Q_th = Q_th - self.eta = eta # Uses setter @property def eta(self): @@ -101,7 +101,7 @@ def eta(self): @eta.setter def eta(self, value): check_bounds(value, 'eta', self.label_full, 0, 1) - self.conversion_factors = [{self.Q_fu.label: value, self.Q_th.label: 1}] + self.conversion_factors[0][self.Q_fu.label] = value @register_class_for_io @@ -174,13 +174,13 @@ def __init__( label, inputs=[P_el], outputs=[Q_th], + conversion_factors=[{P_el.label: eta, Q_th.label: 1}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.P_el = P_el self.Q_th = Q_th - self.eta = eta # Uses setter @property def eta(self): @@ -189,7 +189,7 @@ def eta(self): @eta.setter def eta(self, value): check_bounds(value, 'eta', self.label_full, 0, 1) - self.conversion_factors = [{self.P_el.label: value, self.Q_th.label: 1}] + self.conversion_factors[0][self.P_el.label] = value @register_class_for_io @@ -261,13 +261,13 @@ def __init__( label, inputs=[P_el], outputs=[Q_th], - conversion_factors=[], + conversion_factors=[{P_el.label: COP, Q_th.label: 1}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.P_el = P_el self.Q_th = Q_th - self.COP = COP # Uses setter + self.COP = COP @property def COP(self): # noqa: N802 @@ -276,7 +276,7 @@ def COP(self): # noqa: N802 @COP.setter def COP(self, value): # noqa: N802 check_bounds(value, 'COP', self.label_full, 1, 20) - self.conversion_factors = [{self.P_el.label: value, self.Q_th.label: 1}] + self.conversion_factors[0][self.P_el.label] = value @register_class_for_io @@ -350,13 +350,15 @@ def __init__( label, inputs=[P_el, Q_th], outputs=[], + conversion_factors=[{P_el.label: -1, Q_th.label: specific_electricity_demand}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.P_el = P_el self.Q_th = Q_th - self.specific_electricity_demand = specific_electricity_demand # Uses setter + + check_bounds(specific_electricity_demand, 'specific_electricity_demand', self.label_full, 0, 1) @property def specific_electricity_demand(self): @@ -365,7 +367,7 @@ def specific_electricity_demand(self): @specific_electricity_demand.setter def specific_electricity_demand(self, value): check_bounds(value, 'specific_electricity_demand', self.label_full, 0, 1) - self.conversion_factors = [{self.P_el.label: -1, self.Q_th.label: value}] + self.conversion_factors[0][self.Q_th.label] = value @register_class_for_io @@ -444,11 +446,14 @@ def __init__( on_off_parameters: OnOffParameters | None = None, meta_data: dict | None = None, ): + heat = {Q_fu.label: eta_th, Q_th.label: 1} + electricity = {Q_fu.label: eta_el, P_el.label: 1} + super().__init__( label, inputs=[Q_fu], outputs=[Q_th, P_el], - conversion_factors=[], + conversion_factors=[heat, electricity], on_off_parameters=on_off_parameters, meta_data=meta_data, ) @@ -456,8 +461,6 @@ def __init__( self.Q_fu = Q_fu self.P_el = P_el self.Q_th = Q_th - self.eta_th = eta_th # Uses setter - self.eta_el = eta_el # Uses setter check_bounds(eta_el + eta_th, 'eta_th+eta_el', self.label_full, 0, 1) @@ -468,11 +471,7 @@ def eta_th(self): @eta_th.setter def eta_th(self, value): check_bounds(value, 'eta_th', self.label_full, 0, 1) - if len(self.conversion_factors) < 2: - # Initialize structure if not yet set - self.conversion_factors = [{self.Q_fu.label: value, self.Q_th.label: 1}, {}] - else: - self.conversion_factors[0] = {self.Q_fu.label: value, self.Q_th.label: 1} + self.conversion_factors[0][self.Q_fu.label] = value @property def eta_el(self): @@ -481,11 +480,7 @@ def eta_el(self): @eta_el.setter def eta_el(self, value): check_bounds(value, 'eta_el', self.label_full, 0, 1) - if len(self.conversion_factors) < 2: - # Initialize structure if not yet set - self.conversion_factors = [{}, {self.Q_fu.label: value, self.P_el.label: 1}] - else: - self.conversion_factors[1] = {self.Q_fu.label: value, self.P_el.label: 1} + self.conversion_factors[1][self.Q_fu.label] = value @register_class_for_io @@ -568,14 +563,16 @@ def __init__( label, inputs=[P_el, Q_ab], outputs=[Q_th], - conversion_factors=[], + conversion_factors=[{P_el.label: COP, Q_th.label: 1}, {Q_ab.label: COP / (COP - 1), Q_th.label: 1}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) self.P_el = P_el self.Q_ab = Q_ab self.Q_th = Q_th - self.COP = COP # Uses setter + + if np.any(np.asarray(self.COP) <= 1): + raise ValueError(f'{self.label_full}.COP must be strictly > 1 for HeatPumpWithSource.') @property def COP(self): # noqa: N802 From 420085e3b72768e01fe15cd116f429dae43062f4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:38:23 +0100 Subject: [PATCH 33/34] Update CHANGELOG.md --- CHANGELOG.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f004372b0..0c11e987e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,31 +51,40 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? -**Summary**: +**Summary**: Type system overhaul with comprehensive type hints for better IDE support and code clarity. 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 +- **New type system** (`flixopt/types.py`): + - Introduced dimension-aware type aliases using suffix notation (`_TPS`, `_PS`, `_S`) to clearly indicate which dimensions data can have + - Added `Numeric_TPS`, `Numeric_PS`, `Numeric_S` for numeric data with Time/Period/Scenario dimensions + - Added `Bool_TPS`, `Bool_PS`, `Bool_S` for boolean data with dimension support + - Added `Effect_TPS`, `Effect_PS`, `Effect_S` for effect dictionaries with dimension support + - Added `Scalar` type for scalar-only numeric values + - Added `NumericOrBool` utility type for internal use + - Type system supports scalars, numpy arrays, pandas Series/DataFrames, and xarray DataArrays ### 💥 Breaking Changes ### ♻️ Changed - -### 🗑️ Deprecated - -### 🔥 Removed +- **Type handling improvements**: Updated internal data handling to work seamlessly with the new type system ### 🐛 Fixed +- Fixed `ShareAllocationModel` inconsistency where None/inf conversion happened in `__init__` instead of during modeling, which could cause issues with parameter validation +- Fixed numerous type hint inconsistencies across the codebase -### 🔒 Security +### 📝 Docs +- Enhanced documentation in `flixopt/types.py` with comprehensive examples and dimension explanation table +- Clarified Effect type docstrings - Effect types are dicts, but single numeric values work through union types +- Added clarifying comments in `effects.py` explaining parameter handling and transformation +- Improved OnOffParameters attribute documentation ### 📦 Dependencies - -### 📝 Docs +- Updated `mkdocs-material` to v9.6.23 ### 👷 Development - -### 🚧 Known Issues +- Added test for FlowSystem resampling --- From a76090da3046c28ed4547449e82acc669c269451 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:39:48 +0100 Subject: [PATCH 34/34] Update CHANGELOG.md --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c11e987e..dc7d64e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,22 +70,31 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### ♻️ Changed - **Type handling improvements**: Updated internal data handling to work seamlessly with the new type system +### 🗑️ Deprecated + +### 🔥 Removed + ### 🐛 Fixed - Fixed `ShareAllocationModel` inconsistency where None/inf conversion happened in `__init__` instead of during modeling, which could cause issues with parameter validation - Fixed numerous type hint inconsistencies across the codebase +### 🔒 Security + +### 📦 Dependencies +- Updated `mkdocs-material` to v9.6.23 + ### 📝 Docs - Enhanced documentation in `flixopt/types.py` with comprehensive examples and dimension explanation table - Clarified Effect type docstrings - Effect types are dicts, but single numeric values work through union types - Added clarifying comments in `effects.py` explaining parameter handling and transformation - Improved OnOffParameters attribute documentation -### 📦 Dependencies -- Updated `mkdocs-material` to v9.6.23 ### 👷 Development - Added test for FlowSystem resampling +### 🚧 Known Issues + --- Until here -->