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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions examples/04_Scenarios/scenario_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@
import flixopt as fx

if __name__ == '__main__':
# --- Create Time Series Data ---
# Heat demand profile (e.g., kW) over time and corresponding power prices
heat_demand_per_h = np.array([[30, 0, 90, 110, 110, 20, 20, 20, 20],
[30, 0, 100, 118, 125, 20, 20, 20, 20]]).T
power_prices = np.ones(9) * 0.08

# Create datetime array starting from '2020-01-01' for the given time period
timesteps = pd.date_range('2020-01-01', periods=9, freq='h')
scenarios = pd.Index(['Base Case', 'High Demand'])

# --- Create Time Series Data ---
# Heat demand profile (e.g., kW) over time and corresponding power prices
heat_demand_per_h = pd.DataFrame({'Base Case':[30, 0, 90, 110, 110, 20, 20, 20, 20],
'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)

# --- Define Energy Buses ---
Expand Down
139 changes: 130 additions & 9 deletions flixopt/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ def as_dataarray(
elif isinstance(data, np.ndarray):
return DataConverter._convert_ndarray(data, coords, dims)

elif isinstance(data, pd.Series):
return DataConverter._convert_series(data, coords, dims)

elif isinstance(data, pd.DataFrame):
return DataConverter._convert_dataframe(data, coords, dims)

else:
raise ConversionError(f'Unsupported data type: {type(data).__name__}')

Expand Down Expand Up @@ -171,6 +177,8 @@ def _convert_scalar(
Returns:
DataArray with the scalar value
"""
if isinstance(data, (np.integer, np.floating)):
data = data.item()
return xr.DataArray(data, coords=coords, dims=dims)

@staticmethod
Expand All @@ -192,7 +200,7 @@ def _convert_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tu
raise ConversionError('When converting to dimensionless DataArray, source must be scalar')
return xr.DataArray(data.values.item())

# Check if data already has matching dimensions
# Check if data already has matching dimensions and coordinates
if set(data.dims) == set(dims):
# Check if coordinates match
is_compatible = True
Expand All @@ -202,8 +210,13 @@ def _convert_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tu
break

if is_compatible:
# Return existing DataArray if compatible
return data.copy(deep=True)
# Ensure dimensions are in the correct order
if data.dims != dims:
# Transpose to get dimensions in the right order
return data.transpose(*dims).copy(deep=True)
else:
# Return existing DataArray if compatible and order is correct
return data.copy(deep=True)

# Handle dimension broadcasting
if len(data.dims) == 1 and len(dims) == 2:
Expand All @@ -216,8 +229,9 @@ def _convert_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tu
# Broadcast scenario dimension to include time
return DataConverter._broadcast_scenario_to_time(data, coords, dims)

raise ConversionError(f'Cannot convert {data.dims} to {dims}')

raise ConversionError(
f'Cannot convert {data.dims} to {dims}. Source coordinates: {data.coords}, Target coordinates: {coords}'
)
@staticmethod
def _broadcast_time_to_scenarios(
data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]
Expand All @@ -239,7 +253,7 @@ def _broadcast_time_to_scenarios(

# Broadcast values
values = np.tile(data.values, (len(coords['scenario']), 1))
return xr.DataArray(values, coords=coords, dims=dims)
return xr.DataArray(values.copy(), coords=coords, dims=dims)

@staticmethod
def _broadcast_scenario_to_time(
Expand All @@ -262,7 +276,7 @@ def _broadcast_scenario_to_time(

# Broadcast values
values = np.repeat(data.values[:, np.newaxis], len(coords['time']), axis=1)
return xr.DataArray(values, coords=coords, dims=dims)
return xr.DataArray(values.copy(), coords=coords, dims=dims)

@staticmethod
def _convert_ndarray(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray:
Expand Down Expand Up @@ -359,6 +373,113 @@ def _convert_ndarray_two_dims(data: np.ndarray, coords: Dict[str, pd.Index], dim
else:
raise ConversionError(f'Expected 1D or 2D array for two dimensions, got {data.ndim}D')

@staticmethod
def _convert_series(data: pd.Series, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray:
"""
Convert pandas Series to xarray DataArray.

Args:
data: pandas Series to convert
coords: Target coordinates
dims: Target dimensions

Returns:
DataArray from the pandas Series
"""
# Handle single dimension case
if len(dims) == 1:
dim_name = dims[0]

# Check if series index matches the dimension
if data.index.equals(coords[dim_name]):
return xr.DataArray(data.values.copy(), coords=coords, dims=dims)
else:
raise ConversionError(
f"Series index doesn't match {dim_name} coordinates.\n"
f'Series index: {data.index}\n'
f'Target {dim_name} coordinates: {coords[dim_name]}'
)

# Handle two dimensions case
elif len(dims) == 2:
# Check if dimensions are time and scenario
if dims != ('time', 'scenario'):
raise ConversionError(
f'Two-dimensional conversion only supports time and scenario dimensions, got {dims}'
)

# Case 1: Series is indexed by time
if data.index.equals(coords['time']):
# Broadcast across scenarios
values = np.tile(data.values[:, np.newaxis], (1, len(coords['scenario'])))
return xr.DataArray(values.copy(), coords=coords, dims=dims)

# Case 2: Series is indexed by scenario
elif data.index.equals(coords['scenario']):
# Broadcast across time
values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0)
return xr.DataArray(values.copy(), coords=coords, dims=dims)

else:
raise ConversionError(
"Series index must match either 'time' or 'scenario' coordinates.\n"
f'Series index: {data.index}\n'
f'Target time coordinates: {coords["time"]}\n'
f'Target scenario coordinates: {coords["scenario"]}'
)

else:
raise ConversionError(f'Maximum 2 dimensions supported, got {len(dims)}')

@staticmethod
def _convert_dataframe(data: pd.DataFrame, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray:
"""
Convert pandas DataFrame to xarray DataArray.
Only allows time as index and scenarios as columns.

Args:
data: pandas DataFrame to convert
coords: Target coordinates
dims: Target dimensions

Returns:
DataArray from the pandas DataFrame
"""
# Single dimension case
if len(dims) == 1:
# If DataFrame has one column, treat it like a Series
if len(data.columns) == 1:
series = data.iloc[:, 0]
return DataConverter._convert_series(series, coords, dims)

raise ConversionError(
f'When converting DataFrame to single-dimension DataArray, DataFrame must have exactly one column, got {len(data.columns)}'
)

# Two dimensions case
elif len(dims) == 2:
# Check if dimensions are time and scenario
if dims != ('time', 'scenario'):
raise ConversionError(
f'Two-dimensional conversion only supports time and scenario dimensions, got {dims}'
)

# DataFrame must have time as index and scenarios as columns
if data.index.equals(coords['time']) and data.columns.equals(coords['scenario']):
# Create DataArray with proper dimension order
return xr.DataArray(data.values.copy(), coords=coords, dims=dims)
else:
raise ConversionError(
'DataFrame must have time as index and scenarios as columns.\n'
f'DataFrame index: {data.index}\n'
f'DataFrame columns: {data.columns}\n'
f'Target time coordinates: {coords["time"]}\n'
f'Target scenario coordinates: {coords["scenario"]}'
)

else:
raise ConversionError(f'Maximum 2 dimensions supported, got {len(dims)}')


class TimeSeriesData:
# TODO: Move to Interface.py
Expand Down Expand Up @@ -913,8 +1034,8 @@ def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> Non
if scenarios:
self._selected_scenarios = None

# Apply the selection to all TimeSeries objects
self._propagate_selection_to_time_series()
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:
"""
Expand Down
3 changes: 3 additions & 0 deletions flixopt/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,9 @@ def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_la
return copy_and_convert_datatypes(data.selected_data, use_numpy, use_element_label)
elif isinstance(data, TimeSeriesData):
return copy_and_convert_datatypes(data.data, use_numpy, use_element_label)
elif isinstance(data, (pd.Series, pd.DataFrame)):
#TODO: This can be improved
return copy_and_convert_datatypes(data.values, use_numpy, use_element_label)

elif isinstance(data, Interface):
if use_element_label and isinstance(data, Element):
Expand Down
Loading