Summary
Variable.lower_bound / Variable.upper_bound expressions are only validated for being constant with respect to decision variables (is_constant, i.e. compute_degree(expr) == 0 in variable.py). There is no check that the bound expression's own time/scenario indexing is compatible with (a subset of) the variable's structure: IndexingStructure.
Details
- A bound expression can reference parameters that are themselves
time_dependent/scenario_dependent (resolve_library.py:408-413), regardless of whether the variable itself declares time_dependent/scenario_dependent.
Model.__post_init__ and _make_structure_provider in model.py do cross-check indexing for objective contributions — they require compute_indexation(contribution, provider) == IndexingStructure(False, False) (model.py:139) — but no equivalent check exists for Variable.lower_bound / Variable.upper_bound against Variable.structure.
- The mismatch only surfaces downstream, at simulation-build time, in
_add_variable_for_group / _to_bound_array (optimization.py:794-985). The bound expression is vectorized against the variable's var_shape/dims (derived from var.structure), and _to_bound_array only knows how to add missing axes for dims the variable has but the bound value lacks. It has no handling for the reverse case: a bound whose underlying parameter carries a time or scenario axis that the variable's own dims don't include.
Impact
When a bound's effective indexing exceeds the variable's own indexing:
- Usually: a numpy broadcast shape-mismatch error at solve-build time, with no context pointing back to the offending variable/bound definition.
- Worst case: if the extra axis happens to match another axis's length (e.g. number of time steps equals number of components),
np.broadcast_to silently succeeds and produces silently wrong bound values — no error at all.
Suggested fix
Add an explicit check (either in Variable.__post_init__/Model.__post_init__, or wherever the structure provider is available) that compute_indexation(bound_expr, provider) is a "subset" of var.structure for both lower_bound and upper_bound, and raise a clear ValueError at model-definition/resolution time instead of letting it fail (or silently misbehave) deep in optimization.py.
Where this came from
Surfaced while reviewing Variable (variable.py:23-33) on feat/lb_ub_operator.
Summary
Variable.lower_bound/Variable.upper_boundexpressions are only validated for being constant with respect to decision variables (is_constant, i.e.compute_degree(expr) == 0invariable.py). There is no check that the bound expression's own time/scenario indexing is compatible with (a subset of) the variable'sstructure: IndexingStructure.Details
time_dependent/scenario_dependent(resolve_library.py:408-413), regardless of whether the variable itself declarestime_dependent/scenario_dependent.Model.__post_init__and_make_structure_providerinmodel.pydo cross-check indexing for objective contributions — they requirecompute_indexation(contribution, provider) == IndexingStructure(False, False)(model.py:139) — but no equivalent check exists forVariable.lower_bound/Variable.upper_boundagainstVariable.structure._add_variable_for_group/_to_bound_array(optimization.py:794-985). The bound expression is vectorized against the variable'svar_shape/dims(derived fromvar.structure), and_to_bound_arrayonly knows how to add missing axes for dims the variable has but the bound value lacks. It has no handling for the reverse case: a bound whose underlying parameter carries atimeorscenarioaxis that the variable's owndimsdon't include.Impact
When a bound's effective indexing exceeds the variable's own indexing:
np.broadcast_tosilently succeeds and produces silently wrong bound values — no error at all.Suggested fix
Add an explicit check (either in
Variable.__post_init__/Model.__post_init__, or wherever the structure provider is available) thatcompute_indexation(bound_expr, provider)is a "subset" ofvar.structurefor bothlower_boundandupper_bound, and raise a clearValueErrorat model-definition/resolution time instead of letting it fail (or silently misbehave) deep inoptimization.py.Where this came from
Surfaced while reviewing
Variable(variable.py:23-33) onfeat/lb_ub_operator.