Skip to content

Variable.fix: move from equality-constraint to bound-collapse semantics #769

@FabianHofmann

Description

@FabianHofmann

Following strategies about persistent solving and decomposition, using equality constraints via Variable.fix() is sub-optimal and should be exchanged to use fix lower and upper bounds instead (like JuMP and Pyomo do).

Note

AI assisted writing

Describe the feature you'd like to see

Variable.fix() (added in #625, released in v0.7.0) fixes a variable by adding a __fix__{name} equality constraint. It should instead collapse the bounds:

import linopy

m = linopy.Model()
x = m.add_variables(lower=0, upper=10, name="x")

x.fix(5.0)
# today:    adds equality constraint "__fix__x" (new row, new labels)
# proposed: sets x.lower = x.upper = 5.0 (pure value change, no new row)

Why

  1. The equality-constraint mechanism breaks the persistent in-place path (feat(persistent): in-place solver updates (Solver framework + HiGHS/Gurobi/Xpress/Mosek) #718). Adding/removing the fix constraint changes the active label set → STRUCTURAL_LABELS → full solver rebuild. Bound collapse is a pure value change and stays on the in-place update path — so fix/re-solve loops (including feat: Add fix(), unfix(), and fixed to Variable and Variables #625's stated MILP dual-extraction use case: fix binaries, relax, re-solve as LP) get warm starts instead of rebuilds.
  2. Ecosystem convention. JuMP fix(x, v), Pyomo var.fix(v), gurobipy lb=ub — "fix" means bound/domain collapse everywhere else. linopy's equality-constraint version is the outlier.
  3. Cheaper and cleaner model. No extra rows, no __fix__ constraint-namespace pollution, no cleanup hooks in Model.remove_variables; presolve removes fixed columns.
  4. Unblocks the fix= kwarg in Model.restrict/advance (Design: Model.restrict / advance — slice a model along a coordinate and condition on boundary values (rolling horizon) #768): with consistent semantics, "fix" means hold a variable at a value everywhere — bound collapse in a live model, fold-out (elimination) in restrict.

Implementation sketch

  • fix(value) stashes original lower/upper in a registry and sets both to value — unify with the existing _relaxed_registry pattern into one fix-registry holding bounds + integrality.
  • unfix() restores stashed bounds (and integrality, as today).
  • fixed checks the registry instead of the constraint namespace.
  • FIX_CONSTRAINT_PREFIX, the __fix__ handling in model.py, and the remove_variables cleanup go away.
  • The value=None (use solution), decimals rounding, bounds-validation, and relax=True behavior carry over unchanged.

Behavioral differences

  • The fix's marginal value appears as the variable's reduced cost instead of a constraint dual — equivalent information for the dual-extraction workflow, but a documented change.
  • m.constraints no longer lists __fix__* entries.

No deprecation shim: the API is ~10 weeks old with no downstream usage (PyPSA does not use it), and an in-place semantics swap behind a warning helps nobody. Clean break with a release note.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions