Summary
Several constraint-rule functions call value(model.SomeParam[...]) while building the returned Pyomo expression. This eagerly extracts a plain Python float from a mutable Param at model-construction time, which "bakes" that value into the constraint's coefficients. If the Param value is later updated on the same model instance (e.g., between iterative solves), the constraint will not reflect the new value — the old, stale coefficient remains embedded in the expression.
This was originally flagged in PR #345 review (see comment link below) for temoa/extensions/integer_capacity/components/integer_capacity.py:
unit_cap = value(model.limit_integer_new_capacity[r, t])
...
capacity = value(model.limit_integer_net_capacity[r, t])
Passing the Param object directly (e.g. model.limit_integer_new_capacity[r, t]) instead of calling value(...) keeps the expression symbolic, so Pyomo re-evaluates it if the underlying parameter is updated.
Why this matters
@idelder noted this is particularly important for a planned Monte Carlo extension, where the same model instance's parameters would be updated and re-solved across runs without rebuilding the model from scratch. Any constraint that has "baked in" a value()-extracted parameter at construction time would silently continue to use the old value after a param update, producing incorrect results with no error or warning.
Affected areas (non-exhaustive, needs audit)
Based on a repo-wide scan for value(model.<Param>[...]) used inside expressions returned by constraint-rule functions:
temoa/extensions/integer_capacity/components/integer_capacity.py (Lines 51, 97) — limit_integer_new_capacity / limit_integer_net_capacity
temoa/extensions/template/components/example_limit.py (Line 49) — example_new_capacity_limit
temoa/components/reserves.py (Lines 104-156) — reserve_capacity_derate, capacity_to_activity, segment_fraction
temoa/extensions/growth_rates/components/growth_capacity.py, growth_new_capacity.py, growth_new_capacity_delta.py — several value(model.existing_capacity[...]) / value(model.lifetime_process[...]) / value(model.process_life_frac[...]) calls used while assembling constraint terms (some may be legitimate control-flow/index decisions rather than expression coefficients; needs case-by-case review)
Note: temoa/_internal/table_data_puller.py and similar post-solve reporting/output-extraction code are out of scope — value() is appropriate there since it reads final numeric results after solving, not while building constraints.
Proposed work
- Audit all constraint-rule functions (core
temoa/components/* and all temoa/extensions/*/components/*) for value(model.<Param>[...]) calls that feed into the returned constraint expression.
- For each genuine case, replace the eager
value() extraction with a direct reference to the Param object (or Param indexed access) so the expression stays symbolic.
- Distinguish and leave alone legitimate uses where
value() is required for pure Python control flow (e.g., deciding which index/branch to take, or set membership checks) versus building the returned algebraic expression.
- Consider adding a coding-guideline / lint check (e.g., a custom
ast-grep or ruff rule, or a note in docs/source/extensions.rst) to catch new occurrences of this pattern in constraint-rule functions going forward.
- Add/extend tests (e.g., in
tests/test_extensions.py or a new test) that update a mutable Param value on an already-built model instance and re-verify the constraint expression/bounds reflect the new value, to guard against regressions.
Acceptance criteria
References
Summary
Several constraint-rule functions call
value(model.SomeParam[...])while building the returned Pyomo expression. This eagerly extracts a plain Python float from a mutableParamat model-construction time, which "bakes" that value into the constraint's coefficients. If theParamvalue is later updated on the same model instance (e.g., between iterative solves), the constraint will not reflect the new value — the old, stale coefficient remains embedded in the expression.This was originally flagged in PR #345 review (see comment link below) for
temoa/extensions/integer_capacity/components/integer_capacity.py:Passing the
Paramobject directly (e.g.model.limit_integer_new_capacity[r, t]) instead of callingvalue(...)keeps the expression symbolic, so Pyomo re-evaluates it if the underlying parameter is updated.Why this matters
@idelder noted this is particularly important for a planned Monte Carlo extension, where the same model instance's parameters would be updated and re-solved across runs without rebuilding the model from scratch. Any constraint that has "baked in" a
value()-extracted parameter at construction time would silently continue to use the old value after a param update, producing incorrect results with no error or warning.Affected areas (non-exhaustive, needs audit)
Based on a repo-wide scan for
value(model.<Param>[...])used inside expressions returned by constraint-rule functions:temoa/extensions/integer_capacity/components/integer_capacity.py(Lines 51, 97) —limit_integer_new_capacity/limit_integer_net_capacitytemoa/extensions/template/components/example_limit.py(Line 49) —example_new_capacity_limittemoa/components/reserves.py(Lines 104-156) —reserve_capacity_derate,capacity_to_activity,segment_fractiontemoa/extensions/growth_rates/components/growth_capacity.py,growth_new_capacity.py,growth_new_capacity_delta.py— severalvalue(model.existing_capacity[...])/value(model.lifetime_process[...])/value(model.process_life_frac[...])calls used while assembling constraint terms (some may be legitimate control-flow/index decisions rather than expression coefficients; needs case-by-case review)Note:
temoa/_internal/table_data_puller.pyand similar post-solve reporting/output-extraction code are out of scope —value()is appropriate there since it reads final numeric results after solving, not while building constraints.Proposed work
temoa/components/*and alltemoa/extensions/*/components/*) forvalue(model.<Param>[...])calls that feed into the returned constraint expression.value()extraction with a direct reference to theParamobject (orParamindexed access) so the expression stays symbolic.value()is required for pure Python control flow (e.g., deciding which index/branch to take, or set membership checks) versus building the returned algebraic expression.ast-greporruffrule, or a note indocs/source/extensions.rst) to catch new occurrences of this pattern in constraint-rule functions going forward.tests/test_extensions.pyor a new test) that update a mutableParamvalue on an already-built model instance and re-verify the constraint expression/bounds reflect the new value, to guard against regressions.Acceptance criteria
Paramvalues viavalue()when building the returned expression.Paramafter model construction correctly changes the corresponding constraint's behavior without rebuilding the model.value()calls (control-flow only) are left in place with a comment noting why.References