Add lower_bound()/upper_bound() expression operators - #264
Add lower_bound()/upper_bound() expression operators#264Juliette-Gerbaux wants to merge 131 commits into
Conversation
Cross-check each heuristic input/output declared in optim-config.yml against the model: the referenced id must exist and have the time-dependence the fast/accurate thermal heuristics expect (e.g. min_up_duration constant, generation_power per-timestep), catching mismatches at load time instead of a runtime crash mid-solve. nb_units_max and cluster_max_generation now accept either form, with the heuristics broadcasting a scalar internally. Also warn (instead of silently truncating) when min_up_duration/ min_down_duration resolve to a non-integer number of timesteps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The merged Variable rebuilt for split integer/binary variables was a detached xr.concat copy: setting .lower/.upper on it silently wrote to an orphaned copy instead of the solver, and its name/label_range were inherited from only one of the two groups. Now fails loudly and carries correct metadata. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents the new per-component integer relaxation strategy (exact/relaxed/heuristic) and the built-in fast/accurate thermal heuristics in optim-config.md, building.md, AGENTS.md, and the changelog. Also fixes a mypy error-code annotation in optimization.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Surfaces a variable's current bound in extra-outputs and port-field definitions, mirroring dual()/reduced_cost(). This lets heuristic-mutated bounds (e.g. the fast thermal heuristic tightening generation_power's lower bound) be read back post-solve instead of only the solved value. Reading bypasses the detached merged relaxed/exact variable copy the same way get_variable_solution() already does, so mutations reach the output. Use the new operator to fix num_units_on/non_prop_cost in the thermal heuristic model libraries: they previously approximated unit commitment from generation_power / max_power_per_unit, which diverges from what the fast heuristic actually enforces; deriving it from lower_bound(generation_power) / min_power_per_unit matches it exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| return self.context.get_variable_structure(node.variable_id) | ||
|
|
||
| def lower_bound(self, node: LowerBoundNode) -> IndexingStructure: | ||
| return self.context.get_variable_structure(node.variable_id) |
There was a problem hiding this comment.
That's a bit a detail, but do you think the lower/upper bounds always have the same indexing structure as their related variable? I would say that this is not necessarily the case (ex: variable time-dep but bounds time-indep).
There was a problem hiding this comment.
I agree, and more precisely, I think a lower/upper bound should always be "less granularly" indexed than the variable (the remark is valid for both time and scenario). Ie. the var can depend on time/scenario but not the bound. The reverse should not be possible (at least you think about a use case where we need that ?)
And if you agree on that, no checks are currently made within GemsPy, which may lead to some failures, see #267
There was a problem hiding this comment.
Then here, I think we should enrich the IdexingStructureProvider to also bear information about variable bounds indexing (clean way)
Otherwise simply using the variable indexing should be fine at the cost of possibly having some expressions with a constant timeseries (should work but not optimal)
There was a problem hiding this comment.
I thought it was better to have the same indexing structure as the variable: since the heuristic can change the value of the bound and make it time-dependent, it seemed more intuitive for the heuristic to change only the value and not the indexing structure — but I'm open to changing that. The issue #267 would still be valid.
| return self.context.get_variable_structure(node.variable_id) | ||
|
|
||
| def lower_bound(self, node: LowerBoundNode) -> IndexingStructure: | ||
| return self.context.get_variable_structure(node.variable_id) |
There was a problem hiding this comment.
I agree, and more precisely, I think a lower/upper bound should always be "less granularly" indexed than the variable (the remark is valid for both time and scenario). Ie. the var can depend on time/scenario but not the bound. The reverse should not be possible (at least you think about a use case where we need that ?)
And if you agree on that, no checks are currently made within GemsPy, which may lead to some failures, see #267
| return self.context.get_variable_structure(node.variable_id) | ||
|
|
||
| def lower_bound(self, node: LowerBoundNode) -> IndexingStructure: | ||
| return self.context.get_variable_structure(node.variable_id) |
There was a problem hiding this comment.
Then here, I think we should enrich the IdexingStructureProvider to also bear information about variable bounds indexing (clean way)
Otherwise simply using the variable indexing should be fine at the cost of possibly having some expressions with a constant timeseries (should work but not optimal)
| assert sum(_SPIL_FAST) == pytest.approx(255873) | ||
| assert total_output_sum(st, THERMAL_COMPONENTS, "non_prop_cost") == pytest.approx( | ||
| 4640150 | ||
| 2273100 |
There was a problem hiding this comment.
Do you get the same value in Antares legacy ?
There was a problem hiding this comment.
I had 2123660 in Antares legacy so I don't have the same value but this one is closer than the old one.
Process ID
Process: GP-01
Description
Adds two new expression-language operators,
lower_bound(variable_name)andupper_bound(variable_name), that return a variable's current (post-solve) lower/upper bound. This lets results surface bound mutations applied by thermal heuristics (see the integer-strategy/thermal-heuristic work already on this branch), which previously had no way to be inspected in outputs.The new
LowerBoundNode/UpperBoundNodeare wired through everyExpressionVisitorimplementation (copy,degree,equality,indexing,print,evaluate,uses_sum_connections_on) and through the port-field/model validators. They resolve at parse time to a bare model-variable identifier, and are restricted toextra-outputsand port-field-definitions — using them in constraints, binding-constraints, objective contributions, or variable-bound expressions raisesValueError.On the runner side,
OptimizationProblem.get_variable_solutionis refactored into a shared_reassemble_variable_attr(model_id, var_name, attr)helper, parameterized overlinopy.Variable'ssolution/lower/upperattributes, so the existing relaxed/exact merged-variable-group correctness fix now also applies to bound retrieval. Two new accessors,get_variable_lower_bound/get_variable_upper_bound, are added and plumbed throughSimulationTableBuilderandVectorizedExtraOutputBuildersoextra-outputscan evaluate the new operators.Impact Analysis
expression/(2 new node types + all visitor implementations),simulation/(optimization.py,extra_output.py,simulation_table.py,vectorized_builder.py).study/,optim_config/.get_variable_solutionrefactor preserves the exact same reassembly logic for thesolutionattribute (same per-component lookup +xr.concat), just generalized to also readlower/upper; existing solved outputs are unaffected.Checklist
pytest)mypy)black,isort)pyproject.tomlversion bumped if applicableAGENTS.mdreviewed for impact and updated if needed