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
10 changes: 9 additions & 1 deletion flixopt/transform_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,8 @@ def fix_sizes(
- None: Uses sizes from this FlowSystem's solution (must be solved)
- xr.Dataset: Dataset with size variables (e.g., from statistics.sizes)
- dict: Mapping of component names to sizes (e.g., {'Boiler(Q_fu)': 100})
Sizes with period/scenario dimensions are preserved, fixing each
period/scenario to its own value.
decimal_rounding: Number of decimal places to round sizes to.
Rounding helps avoid numerical infeasibility. Set to None to disable.

Expand Down Expand Up @@ -1436,7 +1438,13 @@ def fix_sizes(
for size_var in sizes.data_vars:
# Normalize: strip '|size' suffix if present
base_name = size_var.replace('|size', '') if size_var.endswith('|size') else size_var
fixed_value = float(sizes[size_var].item())
size_data = sizes[size_var]
if size_data.ndim == 0:
fixed_value = float(size_data.item())
else:
# Per-period/per-scenario sizes: keep the DataArray so each
# coordinate retains its own fixed size
fixed_value = size_data

# Find matching element with InvestParameters
found = False
Expand Down
67 changes: 67 additions & 0 deletions tests/test_math/test_multi_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,70 @@ def test_storage_relative_maximum_final_charge_state_scalar(self, optimize):
)
fs = optimize(fs)
assert_allclose(fs.solution['objective'].item(), 500.0, rtol=1e-5)

def test_fix_sizes_preserves_per_period_sizes(self, optimize):
"""Proves: transform.fix_sizes() preserves per-period investment sizes
in multi-period models (two-stage sizing -> dispatch workflow).

3 ts, periods=[2020, 2025], weight_of_last_period=5. Weights=[5, 5].
Demand peaks at 50 (2020) and 80 (2025), so optimal sizes differ per period.
Boiler invest: 10 fixed + 1 per size. Fuel @1.
Per-period costs: 2020: (10+50) + 80 = 140; 2025: (10+80) + 110 = 200.
Objective = 5*140 + 5*200 = 1700.

Stage 2 (fixed sizes) must reproduce the same sizes and objective.

Sensitivity: Before the fix, fix_sizes() collapsed sizes via .item(),
raising 'ValueError: can only convert an array of size 1 to a Python
scalar' on any multi-period model. If per-period sizes were collapsed
to a single value instead, stage-2 sizes or objective would differ.
"""
from .conftest import _SOLVER

fs = make_multi_period_flow_system(n_timesteps=3, periods=[2020, 2025], weight_of_last_period=5)
demand = xr.DataArray(
np.array([[10, 50, 20], [10, 80, 20]], dtype=float),
coords={'period': [2020, 2025], 'time': fs.timesteps},
dims=['period', 'time'],
)
fs.add_elements(
fx.Bus('Heat'),
fx.Bus('Gas'),
fx.Effect('costs', '€', is_standard=True, is_objective=True),
fx.Sink(
'Demand',
inputs=[
fx.Flow('heat', bus='Heat', size=1, fixed_relative_profile=demand),
],
),
fx.Source(
'GasSrc',
outputs=[
fx.Flow('gas', bus='Gas', effects_per_flow_hour=1),
],
),
fx.linear_converters.Boiler(
'Boiler',
thermal_efficiency=1.0,
fuel_flow=fx.Flow('fuel', bus='Gas'),
thermal_flow=fx.Flow(
'heat',
bus='Heat',
size=fx.InvestParameters(
maximum_size=200,
effects_of_investment=10,
effects_of_investment_per_size=1,
),
),
),
)
# Stage 1: sizing
fs = optimize(fs)
assert_allclose(fs.solution['Boiler(heat)|size'].values, [50.0, 80.0], rtol=1e-5)
assert_allclose(fs.solution['objective'].item(), 1700.0, rtol=1e-5)

# Stage 2: fix sizes and dispatch
fs_dispatch = fs.transform.fix_sizes()
fs_dispatch.optimize(_SOLVER)
assert_allclose(fs_dispatch.solution['Boiler(heat)|size'].values, [50.0, 80.0], rtol=1e-5)
assert_allclose(fs_dispatch.solution['objective'].item(), 1700.0, rtol=1e-5)
Loading