fix(transform): don't force investment when fixing a size of 0#721
Conversation
transform.fix_sizes() unconditionally set mandatory=True on every InvestParameters it fixed. When a fixed size is 0 (the sizing run chose not to invest) mandatory=True still charges the flat effects_of_investment, because a mandatory investment has no `invested` binary to gate the cost. The dispatch objective therefore no longer matched the sizing objective (e.g. 160 -> 100160 single-period; 700 -> 1200 for per-period sizes mixing 0 and non-zero values). Set mandatory based on the fixed size: only force the investment when every period/scenario value is non-zero; otherwise keep it optional so the invested binary gates the fixed cost per period. Also reset() the returned FlowSystem so it is an unsolved dispatch problem, as documented (from_dataset had been restoring the stage-1 solution). Adds regression tests asserting the two-stage objective reproduces the sizing objective for the don't-invest and mixed-period cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthrough
Changesfix_sizes Mandatory Flag and Reset Behavior
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Poem 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@flixopt/transform_accessor.py`:
- Around line 1480-1485: FlowSystem.from_dataset() is only resetting the copied
system when size keys are modified, which leaves the restored stage-1 solution
locked in the no-match case. Update the from_dataset() flow in
transform_accessor.py so the copied FlowSystem is always reset before returning,
regardless of whether modified is true, and keep the existing reset path tied to
the copied system object (new_fs).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d2a3ee52-131e-4a7a-bbac-08666378be1e
📒 Files selected for processing (2)
flixopt/transform_accessor.pytests/test_math/test_multi_period.py
| # from_dataset() restores the stage-1 solution; drop it so the returned system | ||
| # is an unsolved dispatch problem (as documented) and re-transforms cleanly | ||
| # with the sizes we just assigned on the next optimize(). | ||
| if modified: | ||
| new_fs.reset() | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm to_dataset() includes the solution by default (making from_dataset restore it)
ast-grep run --pattern 'def to_dataset($$$):
$$$' --lang python flixopt/flow_system.py
rg -nP 'def to_dataset' flixopt/flow_system.pyRepository: flixOpt/flixopt
Length of output: 264
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect relevant implementations and surrounding logic.
ast-grep outline flixopt/transform_accessor.py --match from_dataset --view expanded || true
ast-grep outline flixopt/flow_system.py --match to_dataset --view expanded || true
# Read the relevant ranges with line numbers.
sed -n '1430,1505p' flixopt/transform_accessor.py | cat -n
printf '\n--- flow_system.py ---\n'
sed -n '680,760p' flixopt/flow_system.py | cat -n
# Look for reset semantics and any solution-lock related flags.
rg -n "def reset|locked|solution|include_solution|from_dataset|to_dataset" flixopt/flow_system.py flixopt/transform_accessor.pyRepository: flixOpt/flixopt
Length of output: 25572
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether from_dataset() / reset() manipulate the stage-1 solution and lock state.
rg -n "class FlowSystem|def reset|def from_dataset|stage-1|locked|lock|solution" flixopt -g '*.py'
# Inspect the specific methods if found.
sed -n '1,260p' flixopt/flow_system.py | cat -nRepository: flixOpt/flixopt
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the dataset round-trip and reset behavior around the reported lines.
sed -n '680,760p' flixopt/flow_system.py | cat -n
printf '\n---\n'
sed -n '1430,1505p' flixopt/transform_accessor.py | cat -n
# Find the relevant method definitions and any lock/solution handling.
rg -n "def to_dataset|def from_dataset|def reset|locked|solution|include_solution|stage-1" flixopt/flow_system.py flixopt/transform_accessor.pyRepository: flixOpt/flixopt
Length of output: 23221
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect dataset restoration and reset semantics directly.
sed -n '1600,1835p' flixopt/io.py | cat -n
printf '\n--- reset ---\n'
sed -n '1600,1640p' flixopt/flow_system.py | cat -n
printf '\n--- optimize/lock ---\n'
sed -n '1410,1495p' flixopt/flow_system.py | cat -nRepository: flixOpt/flixopt
Length of output: 18126
Always reset the copied system The returned FlowSystem should be unlocked even when no size key matches; otherwise FlowSystem.from_dataset() keeps the restored solution and the copy stays locked.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@flixopt/transform_accessor.py` around lines 1480 - 1485,
FlowSystem.from_dataset() is only resetting the copied system when size keys are
modified, which leaves the restored stage-1 solution locked in the no-match
case. Update the from_dataset() flow in transform_accessor.py so the copied
FlowSystem is always reset before returning, regardless of whether modified is
true, and keep the existing reset path tied to the copied system object
(new_fs).
Problem
transform.fix_sizes()unconditionally setmandatory=Trueon everyInvestParametersit fixed. When a fixed size is 0 (the sizing run chose not to invest),mandatory=Truestill charges the flateffects_of_investment, because a mandatory investment has noinvestedbinary to gate the cost (features.py:101-109).The two-stage sizing → dispatch workflow is meant to reproduce the sizing objective at full resolution, but the phantom fixed cost silently broke that:
[0, 90]No error was raised — just a wrong objective.
Fix
mandatory = bool((fixed_value != 0).all())— only force the investment when every period/scenario value is non-zero; otherwise keep it optional so theinvestedbinary gates the fixed cost per period. This handles the don't-invest case and per-period sizes that mix 0 and non-zero values (a singleallclose(size, 0)check cannot, since it stays mandatory whenever any value is non-zero).DataArrayforfixed_value(aDataArrayalways has.dims, avoiding a scalar-floatcrash in bound construction whenmandatoryis False).reset()the returned FlowSystem so it is an unsolved dispatch problem, as the docstring promises —from_dataset()had been restoring the stage-1 solution.Tests
Adds two regression tests asserting the two-stage objective reproduces the sizing objective:
test_fix_sizes_no_invest_reproduces_objective(single period, size 0)test_fix_sizes_mixed_period_invest_reproduces_objective(per-period[0, 90])tests/test_math/test_multi_period.py: 36 passed. Fulltests/test_math/: 386 passed (the 9test_flow_investfailures pre-exist onmainand are unrelated).Known limitation
For genuinely mixed per-period sizes,
mandatory=Falsepins the non-zero periods only because demand forces theinvestedbinary on. A scalarmandatoryflag cannot express "invest in 2021 but not 2020" exactly; this is the most faithful option available without per-period invested-fixing, and is strictly better than the previous behavior.🤖 Generated with Claude Code
Summary by CodeRabbit
0are no longer treated as mandatory, preventing incorrect charges in dispatch results.