Skip to content

Commit f71d44a

Browse files
committed
Add tests for legacy solution access
1 parent 92f5000 commit f71d44a

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""Tests for legacy solution access patterns.
2+
3+
These tests verify that CONFIG.Legacy.solution_access enables backward-compatible
4+
access to solution variables using the old naming convention.
5+
"""
6+
7+
import numpy as np
8+
import pytest
9+
from numpy.testing import assert_allclose
10+
11+
import flixopt as fx
12+
13+
from .conftest import make_flow_system
14+
15+
16+
class TestLegacySolutionAccess:
17+
"""Tests for legacy solution access patterns."""
18+
19+
def test_effect_access(self, optimize):
20+
"""Test legacy effect access: solution['costs'] -> solution['effect|total'].sel(effect='costs')."""
21+
fs = make_flow_system(2)
22+
fs.add_elements(
23+
fx.Bus('Heat'),
24+
fx.Effect('costs', '€', is_standard=True, is_objective=True),
25+
fx.Source('Src', outputs=[fx.Flow('heat', bus='Heat', size=10, effects_per_flow_hour=1)]),
26+
fx.Sink('Snk', inputs=[fx.Flow('heat', bus='Heat', size=10, fixed_relative_profile=np.array([1, 1]))]),
27+
)
28+
fs = optimize(fs)
29+
30+
# Legacy access should work
31+
legacy_result = fs.solution['costs'].item()
32+
# New access
33+
new_result = fs.solution['effect|total'].sel(effect='costs').item()
34+
35+
assert_allclose(legacy_result, new_result, rtol=1e-10)
36+
assert_allclose(legacy_result, 20.0, rtol=1e-5) # 2 timesteps * 10 flow * 1 cost
37+
38+
def test_flow_rate_access(self, optimize):
39+
"""Test legacy flow rate access: solution['Src(heat)|flow_rate'] -> solution['flow|rate'].sel(flow='Src(heat)')."""
40+
fs = make_flow_system(2)
41+
fs.add_elements(
42+
fx.Bus('Heat'),
43+
fx.Effect('costs', '€', is_standard=True, is_objective=True),
44+
fx.Source('Src', outputs=[fx.Flow('heat', bus='Heat', size=10)]),
45+
fx.Sink('Snk', inputs=[fx.Flow('heat', bus='Heat', size=10, fixed_relative_profile=np.array([1, 1]))]),
46+
)
47+
fs = optimize(fs)
48+
49+
# Legacy access should work
50+
legacy_result = fs.solution['Src(heat)|flow_rate'].values[:-1] # Exclude trailing NaN
51+
# New access
52+
new_result = fs.solution['flow|rate'].sel(flow='Src(heat)').values[:-1]
53+
54+
assert_allclose(legacy_result, new_result, rtol=1e-10)
55+
assert_allclose(legacy_result, [10, 10], rtol=1e-5)
56+
57+
def test_flow_size_access(self, optimize):
58+
"""Test legacy flow size access: solution['Src(heat)|size'] -> solution['flow|size'].sel(flow='Src(heat)')."""
59+
fs = make_flow_system(2)
60+
fs.add_elements(
61+
fx.Bus('Heat'),
62+
fx.Effect('costs', '€', is_standard=True, is_objective=True),
63+
fx.Source(
64+
'Src',
65+
outputs=[fx.Flow('heat', bus='Heat', size=fx.InvestParameters(fixed_size=50), effects_per_flow_hour=1)],
66+
),
67+
fx.Sink('Snk', inputs=[fx.Flow('heat', bus='Heat', size=10, fixed_relative_profile=np.array([5, 5]))]),
68+
)
69+
fs = optimize(fs)
70+
71+
# Legacy access should work
72+
legacy_result = fs.solution['Src(heat)|size'].item()
73+
# New access
74+
new_result = fs.solution['flow|size'].sel(flow='Src(heat)').item()
75+
76+
assert_allclose(legacy_result, new_result, rtol=1e-10)
77+
assert_allclose(legacy_result, 50.0, rtol=1e-5)
78+
79+
def test_storage_charge_state_access(self, optimize):
80+
"""Test legacy storage charge state access: solution['Battery|charge_state'] -> solution['storage|charge'].sel(storage='Battery')."""
81+
fs = make_flow_system(3)
82+
fs.add_elements(
83+
fx.Bus('Elec'),
84+
fx.Effect('costs', '€', is_standard=True, is_objective=True),
85+
fx.Source('Grid', outputs=[fx.Flow('elec', bus='Elec', size=100, effects_per_flow_hour=1)]),
86+
fx.Storage(
87+
'Battery',
88+
charging=fx.Flow('charge', bus='Elec', size=10),
89+
discharging=fx.Flow('discharge', bus='Elec', size=10),
90+
capacity_in_flow_hours=50,
91+
initial_charge_state=25,
92+
),
93+
fx.Sink('Load', inputs=[fx.Flow('elec', bus='Elec', size=10, fixed_relative_profile=np.array([1, 1, 1]))]),
94+
)
95+
fs = optimize(fs)
96+
97+
# Legacy access should work
98+
legacy_result = fs.solution['Battery|charge_state'].values
99+
# New access
100+
new_result = fs.solution['storage|charge'].sel(storage='Battery').values
101+
102+
assert_allclose(legacy_result, new_result, rtol=1e-10)
103+
# Initial charge state is 25
104+
assert legacy_result[0] == 25.0
105+
106+
def test_legacy_access_disabled_by_default(self):
107+
"""Test that legacy access is disabled when CONFIG.Legacy.solution_access is False."""
108+
# Save current setting
109+
original_setting = fx.CONFIG.Legacy.solution_access
110+
111+
try:
112+
# Disable legacy access
113+
fx.CONFIG.Legacy.solution_access = False
114+
115+
fs = make_flow_system(2)
116+
fs.add_elements(
117+
fx.Bus('Heat'),
118+
fx.Effect('costs', '€', is_standard=True, is_objective=True),
119+
fx.Source('Src', outputs=[fx.Flow('heat', bus='Heat', size=10, effects_per_flow_hour=1)]),
120+
fx.Sink('Snk', inputs=[fx.Flow('heat', bus='Heat', size=10, fixed_relative_profile=np.array([1, 1]))]),
121+
)
122+
solver = fx.solvers.HighsSolver(log_to_console=False)
123+
fs.optimize(solver)
124+
125+
# Legacy access should raise KeyError
126+
with pytest.raises(KeyError):
127+
_ = fs.solution['costs']
128+
129+
# New access should work
130+
result = fs.solution['effect|total'].sel(effect='costs').item()
131+
assert_allclose(result, 20.0, rtol=1e-5)
132+
133+
finally:
134+
# Restore original setting
135+
fx.CONFIG.Legacy.solution_access = original_setting
136+
137+
def test_legacy_access_emits_deprecation_warning(self, optimize):
138+
"""Test that legacy access emits DeprecationWarning."""
139+
fs = make_flow_system(2)
140+
fs.add_elements(
141+
fx.Bus('Heat'),
142+
fx.Effect('costs', '€', is_standard=True, is_objective=True),
143+
fx.Source('Src', outputs=[fx.Flow('heat', bus='Heat', size=10, effects_per_flow_hour=1)]),
144+
fx.Sink('Snk', inputs=[fx.Flow('heat', bus='Heat', size=10, fixed_relative_profile=np.array([1, 1]))]),
145+
)
146+
fs = optimize(fs)
147+
148+
import warnings
149+
150+
with warnings.catch_warnings(record=True) as w:
151+
warnings.simplefilter('always')
152+
_ = fs.solution['costs']
153+
154+
# Should have exactly one DeprecationWarning
155+
deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
156+
assert len(deprecation_warnings) == 1
157+
assert 'Legacy solution access' in str(deprecation_warnings[0].message)
158+
assert "solution['effect|total'].sel(effect='costs')" in str(deprecation_warnings[0].message)

0 commit comments

Comments
 (0)