Skip to content
2 changes: 1 addition & 1 deletion examples/04_Scenarios/scenario_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
'High Demand':[30, 0, 100, 118, 125, 20, 20, 20, 20]}, index=timesteps)
power_prices = np.array([0.08, 0.09])

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

# --- Define Energy Buses ---
# These represent nodes, where the used medias are balanced (electricity, heat, and gas)
Expand Down
2 changes: 1 addition & 1 deletion flixopt/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def transform_data(self, flow_system: 'FlowSystem') -> None:
self.relative_loss_per_hour = flow_system.create_time_series(
f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour
)
if self.initial_charge_state != 'lastValueOfSim':
if not isinstance(self.initial_charge_state, str):
self.initial_charge_state = flow_system.create_time_series(
f'{self.label_full}|initial_charge_state', self.initial_charge_state, has_time_dim=False
)
Expand Down
100 changes: 49 additions & 51 deletions flixopt/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ def reset(self) -> None:
Reset selections to include all timesteps and scenarios.
This is equivalent to clearing all selections.
"""
self.clear_selection()
self.set_selection(None, None)

def restore_data(self) -> None:
"""
Expand Down Expand Up @@ -755,13 +755,7 @@ def update_stored_data(self, value: xr.DataArray) -> None:
return

self._stored_data = new_data
self.clear_selection() # Reset selections to full dataset

def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> None:
if timesteps:
self._selected_timesteps = None
if scenarios:
self._selected_scenarios = None
self.set_selection(None, None) # Reset selections to full dataset

def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None) -> None:
"""
Expand All @@ -773,15 +767,15 @@ def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios:
"""
# Only update timesteps if the series has time dimension
if self.has_time_dim:
if timesteps is None:
self.clear_selection(timesteps=True, scenarios=False)
if timesteps is None or timesteps.equals(self._stored_data.indexes['time']):
self._selected_timesteps = None
else:
self._selected_timesteps = timesteps

# Only update scenarios if the series has scenario dimension
if self.has_scenario_dim:
if scenarios is None:
self.clear_selection(timesteps=False, scenarios=True)
if scenarios is None or scenarios.equals(self._stored_data.indexes['scenario']):
self._selected_scenarios = None
else:
self._selected_scenarios = scenarios

Expand Down Expand Up @@ -1077,22 +1071,6 @@ def add_time_series(
# Return the TimeSeries object
return time_series

def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> None:
"""
Clear selection for timesteps and/or scenarios.

Args:
timesteps: Whether to clear timesteps selection
scenarios: Whether to clear scenarios selection
"""
if timesteps:
self._update_selected_timesteps(timesteps=None)
if scenarios:
self._selected_scenarios = None

for ts in self._time_series.values():
ts.clear_selection(timesteps=timesteps, scenarios=scenarios)

def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None) -> None:
"""
Set active subset for timesteps and scenarios.
Expand All @@ -1102,35 +1080,30 @@ def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios:
scenarios: Scenarios to activate, or None to clear
"""
if timesteps is None:
self.clear_selection(timesteps=True, scenarios=False)
self._selected_timesteps = None
self._selected_timesteps_extra = None
else:
self._update_selected_timesteps(timesteps)
self._selected_timesteps = self._validate_timesteps(timesteps, self._full_timesteps)
self._selected_timesteps_extra = self._create_timesteps_with_extra(
timesteps, self._calculate_hours_of_final_timestep(timesteps, self._full_timesteps)
)

if scenarios is None:
self.clear_selection(timesteps=False, scenarios=True)
self._selected_scenarios = None
else:
self._selected_scenarios = self._validate_scenarios(scenarios)
self._selected_scenarios = self._validate_scenarios(scenarios, self._full_scenarios)

# Apply the selection to all TimeSeries objects
self._propagate_selection_to_time_series()
self._selected_hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra, self.scenarios)

def _update_selected_timesteps(self, timesteps: Optional[pd.DatetimeIndex]) -> None:
"""
Updates the timestep and related metrics (timesteps_extra, hours_per_timestep) based on the current selection.
"""
if timesteps is None:
self._selected_timesteps = None
self._selected_timesteps_extra = None
self._selected_hours_per_timestep = None
return
# Apply the selection to all TimeSeries objects
for ts_name, ts in self._time_series.items():
if ts.has_time_dim:
timesteps = self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps
else:
timesteps = None

self._selected_timesteps = self._validate_timesteps(timesteps, self._full_timesteps)
self._selected_timesteps_extra = self._create_timesteps_with_extra(
timesteps, self._calculate_hours_of_final_timestep(timesteps, self._full_timesteps)
)
self._selected_hours_per_timestep = self.calculate_hours_per_timestep(
self._selected_timesteps_extra, self._selected_scenarios
)
ts.set_selection(timesteps=timesteps, scenarios=self.scenarios if ts.has_scenario_dim else None)
self._propagate_selection_to_time_series()

def as_dataset(self, with_extra_timestep: bool = True, with_constants: bool = True) -> xr.Dataset:
"""
Expand Down Expand Up @@ -1188,7 +1161,7 @@ def _propagate_selection_to_time_series(self) -> None:
"""Apply the current selection to all TimeSeries objects."""
for ts_name, ts in self._time_series.items():
if ts.has_time_dim:
timesteps = self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps
timesteps = self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps
else:
timesteps = None

Expand Down Expand Up @@ -1482,3 +1455,28 @@ def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10, by_
std = data.std().item()

return f'{mean:{format_spec}} (mean), {median:{format_spec}} (median), {min_val:{format_spec}} (min), {max_val:{format_spec}} (max), {std:{format_spec}} (std)'


def extract_data(
data: Optional[Union[int, float, xr.DataArray, TimeSeries]],
if_none: Any = None
) -> Any:
"""
Convert data to xr.DataArray.

Args:
data: The data to convert (scalar, array, or DataArray)
if_none: The value to return if data is None

Returns:
DataArray with the converted data, or the value specified by if_none
"""
if data is None:
return if_none
if isinstance(data, TimeSeries):
return data.selected_data
if isinstance(data, xr.DataArray):
return data
if isinstance(data, (int, float, np.integer, np.floating)):
return xr.DataArray(data)
raise TypeError(f'Unsupported data type: {type(data).__name__}')
22 changes: 9 additions & 13 deletions flixopt/effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import linopy
import numpy as np

from .core import NumericDataTS, ScenarioData, TimeSeries, TimeSeriesCollection, TimestepData
from .core import NumericDataTS, ScenarioData, TimeSeries, TimeSeriesCollection, TimestepData, extract_data
from .features import ShareAllocationModel
from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io

Expand Down Expand Up @@ -125,8 +125,8 @@ def __init__(self, model: SystemModel, element: Effect):
label_of_element=self.label_of_element,
label='invest',
label_full=f'{self.label_full}(invest)',
total_max=self.element.maximum_invest,
total_min=self.element.minimum_invest,
total_max=extract_data(self.element.maximum_invest),
total_min=extract_data(self.element.minimum_invest),
)
)

Expand All @@ -138,14 +138,10 @@ def __init__(self, model: SystemModel, element: Effect):
label_of_element=self.label_of_element,
label='operation',
label_full=f'{self.label_full}(operation)',
total_max=self.element.maximum_operation,
total_min=self.element.minimum_operation,
min_per_hour=self.element.minimum_operation_per_hour.selected_data
if self.element.minimum_operation_per_hour is not None
else None,
max_per_hour=self.element.maximum_operation_per_hour.selected_data
if self.element.maximum_operation_per_hour is not None
else None,
total_max=extract_data(self.element.maximum_operation),
total_min=extract_data(self.element.minimum_operation),
min_per_hour=extract_data(self.element.minimum_operation_per_hour),
max_per_hour=extract_data(self.element.maximum_operation_per_hour),
)
)

Expand All @@ -155,8 +151,8 @@ def do_modeling(self):

self.total = self.add(
self._model.add_variables(
lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf,
upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf,
lower=extract_data(self.element.minimum_total, if_none=-np.inf),
upper=extract_data(self.element.maximum_total, if_none=np.inf),
coords=self._model.get_coords(time_dim=False),
name=f'{self.label_full}|total',
),
Expand Down
22 changes: 11 additions & 11 deletions flixopt/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import numpy as np

from .config import CONFIG
from .core import PlausibilityError, Scalar, ScenarioData, TimestepData
from .core import PlausibilityError, Scalar, ScenarioData, TimestepData, extract_data
from .effects import EffectValuesUserTimestep
from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel
from .interface import InvestParameters, OnOffParameters
Expand Down Expand Up @@ -375,8 +375,8 @@ def do_modeling(self):

self.total_flow_hours = self.add(
self._model.add_variables(
lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0,
upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf,
lower=extract_data(self.element.flow_hours_total_min, 0),
upper=extract_data(self.element.flow_hours_total_max, np.inf),
coords=self._model.get_coords(time_dim=False),
name=f'{self.label_full}|total_flow_hours',
),
Expand Down Expand Up @@ -456,16 +456,16 @@ def flow_rate_lower_bound_relative(self) -> TimestepData:
"""Returns the lower bound of the flow_rate relative to its size"""
fixed_profile = self.element.fixed_relative_profile
if fixed_profile is None:
return self.element.relative_minimum.selected_data
return fixed_profile.selected_data
return extract_data(self.element.relative_minimum)
return extract_data(fixed_profile)

@property
def flow_rate_upper_bound_relative(self) -> TimestepData:
""" Returns the upper bound of the flow_rate relative to its size"""
fixed_profile = self.element.fixed_relative_profile
if fixed_profile is None:
return self.element.relative_maximum.selected_data
return fixed_profile.selected_data
return extract_data(self.element.relative_maximum)
return extract_data(fixed_profile)

@property
def flow_rate_lower_bound(self) -> TimestepData:
Expand All @@ -478,8 +478,8 @@ def flow_rate_lower_bound(self) -> TimestepData:
if isinstance(self.element.size, InvestParameters):
if self.element.size.optional:
return 0
return self.flow_rate_lower_bound_relative * self.element.size.minimum_size
return self.flow_rate_lower_bound_relative * self.element.size
return self.flow_rate_lower_bound_relative * extract_data(self.element.size.minimum_size)
return self.flow_rate_lower_bound_relative * extract_data(self.element.size)

@property
def flow_rate_upper_bound(self) -> TimestepData:
Expand All @@ -488,8 +488,8 @@ def flow_rate_upper_bound(self) -> TimestepData:
Further constraining might be done in OnOffModel and InvestmentModel
"""
if isinstance(self.element.size, InvestParameters):
return self.flow_rate_upper_bound_relative * self.element.size.maximum_size
return self.flow_rate_upper_bound_relative * self.element.size
return self.flow_rate_upper_bound_relative * extract_data(self.element.size.maximum_size)
return self.flow_rate_upper_bound_relative * extract_data(self.element.size)


class BusModel(ElementModel):
Expand Down
30 changes: 15 additions & 15 deletions flixopt/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import numpy as np

from .config import CONFIG
from .core import Scalar, ScenarioData, TimeSeries, TimestepData
from .core import Scalar, ScenarioData, TimeSeries, TimestepData, extract_data
from .interface import InvestParameters, OnOffParameters, Piecewise
from .structure import Model, SystemModel

Expand Down Expand Up @@ -45,8 +45,8 @@ def __init__(
def do_modeling(self):
self.size = self.add(
self._model.add_variables(
lower=0 if self.parameters.optional else self.parameters.minimum_size*1,
upper=self.parameters.maximum_size*1,
lower=0 if self.parameters.optional else extract_data(self.parameters.minimum_size),
upper=extract_data(self.parameters.maximum_size),
name=f'{self.label_full}|size',
coords=self._model.get_coords(time_dim=False),
),
Expand Down Expand Up @@ -295,8 +295,8 @@ def do_modeling(self):

self.total_on_hours = self.add(
self._model.add_variables(
lower=self._on_hours_total_min,
upper=self._on_hours_total_max,
lower=extract_data(self._on_hours_total_min),
upper=extract_data(self._on_hours_total_max),
coords=self._model.get_coords(time_dim=False),
name=f'{self.label_full}|on_hours_total',
),
Expand Down Expand Up @@ -440,7 +440,7 @@ def do_modeling(self):
# Create count variable for number of switches
self.switch_on_nr = self.add(
self._model.add_variables(
upper=self._switch_on_max,
upper=extract_data(self._switch_on_max),
lower=0,
name=f'{self.label_full}|switch_on_nr',
),
Expand Down Expand Up @@ -534,7 +534,7 @@ def do_modeling(self):
self.duration = self.add(
self._model.add_variables(
lower=0,
upper=self._maximum_duration if self._maximum_duration is not None else mega,
upper=extract_data(self._maximum_duration, mega),
coords=self._model.get_coords(),
name=f'{self.label_full}|hours',
),
Expand Down Expand Up @@ -588,7 +588,7 @@ def do_modeling(self):
)

# Handle initial condition
if 0 < self.previous_duration < self._minimum_duration.isel(time=0):
if 0 < self.previous_duration < self._minimum_duration.isel(time=0).max():
self.add(
self._model.add_constraints(
self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum'
Expand All @@ -613,7 +613,7 @@ def previous_duration(self) -> Scalar:
"""Computes the previous duration of the state variable"""
#TODO: Allow for other/dynamic timestep resolutions
return ConsecutiveStateModel.compute_consecutive_hours_in_state(
self._previous_states, self._model.hours_per_step.isel(time=0).item()
self._previous_states, self._model.hours_per_step.isel(time=0).values.flatten()[0]
)

@staticmethod
Expand Down Expand Up @@ -715,8 +715,8 @@ def do_modeling(self):
defining_bounds=self._defining_bounds,
previous_values=self._previous_values,
use_off=self.parameters.use_off,
on_hours_total_min=self.parameters.on_hours_total_min,
on_hours_total_max=self.parameters.on_hours_total_max,
on_hours_total_min=extract_data(self.parameters.on_hours_total_min),
on_hours_total_max=extract_data(self.parameters.on_hours_total_max),
effects_per_running_hour=self.parameters.effects_per_running_hour,
)
self.add(self.state_model)
Expand Down Expand Up @@ -965,8 +965,8 @@ def __init__(
label_of_element: Optional[str] = None,
label: Optional[str] = None,
label_full: Optional[str] = None,
total_max: Optional[Scalar] = None,
total_min: Optional[Scalar] = None,
total_max: Optional[ScenarioData] = None,
total_min: Optional[ScenarioData] = None,
max_per_hour: Optional[TimestepData] = None,
min_per_hour: Optional[TimestepData] = None,
):
Expand Down Expand Up @@ -1009,8 +1009,8 @@ def do_modeling(self):
if self._has_time_dim:
self.total_per_timestep = self.add(
self._model.add_variables(
lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step,
upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step,
lower=-np.inf if (self._min_per_hour is None) else extract_data(self._min_per_hour) * self._model.hours_per_step,
upper=np.inf if (self._max_per_hour is None) else extract_data(self._max_per_hour) * self._model.hours_per_step,
coords=self._model.get_coords(time_dim=True, scenario_dim=self._has_scenario_dim),
name=f'{self.label_full}|total_per_timestep',
),
Expand Down
Loading