Skip to content

feat: Add fix(), unfix(), and fixed to Variable and Variables#625

Merged
FBumann merged 10 commits intomasterfrom
feat/variable-fix-unfix
Mar 24, 2026
Merged

feat: Add fix(), unfix(), and fixed to Variable and Variables#625
FBumann merged 10 commits intomasterfrom
feat/variable-fix-unfix

Conversation

@FBumann
Copy link
Collaborator

@FBumann FBumann commented Mar 18, 2026

Changes proposed in this Pull Request

Adds fix(), unfix(), and fixed to both Variable and the Variables container for fixing variables to values via equality constraints.

Features

  • fix(value=None, decimals=8, relax=False) — Fixes a variable by adding a __fix__{name} equality constraint. Uses the current solution if no value is given.
  • unfix() — Removes the fix constraint and restores integrality if it was relaxed.
  • fixed — Property indicating whether the variable is currently fixed.
  • Precision handling — Rounds integers/binaries to 0 decimal places, continuous variables to decimals. Clips to variable bounds to prevent infeasibility from floating-point noise.
  • Integrality relaxationfix(relax=True) temporarily converts integer/binary variables to continuous (tracked in Model._relaxed_registry), enabling dual extraction after re-solve. unfix() restores the original type.
  • CleanupModel.remove_variables() automatically cleans up fix constraints and relaxed registry entries.

Use cases

  • MILP dual extraction: Solve MILP, fix binaries with relax=True, re-solve as LP, read duals.
  • Fixing decision variables after an initial (aggregated) solve.

Includes

  • Tests covering rounding, clipping, relaxation, restore, container delegation, and cleanup.
  • Example added to the manipulating-models notebook.

Checklist

  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

FBumann and others added 4 commits March 18, 2026 08:46
Add methods to fix variables to values via equality constraints, with:
- Automatic rounding (0 decimals for int/binary, configurable for continuous)
- Clipping to variable bounds to prevent infeasibility
- Optional integrality relaxation (relax=True) for MILP dual extraction
- Relaxed state tracked in Model._relaxed_registry and restored by unfix()
- Cleanup on variable removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@FBumann FBumann changed the title feat: variable fix unfix relax feat: Add fix(), unfix(), and fixed to Variable and Variables Mar 18, 2026
FBumann and others added 2 commits March 18, 2026 09:53
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Serialize the relaxed registry as a JSON string in netCDF attrs so that
unfix() can restore integrality after a save/load roundtrip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@FBumann FBumann requested a review from FabianHofmann March 18, 2026 09:17
@FBumann FBumann marked this pull request as ready for review March 18, 2026 09:17
@FBumann FBumann added the enhancement New feature or request label Mar 18, 2026
Copy link
Collaborator

@FabianHofmann FabianHofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convenient feature which does not hurt. this could be extended to fix the variables on the solver side as well. I remember the LP files allow to define fixed variables (without an explicit constraint). but this should wait after #630

if value is None:
value = self.solution

value = DataArray(value).broadcast_like(self.labels)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably use as_dataarray here


value = DataArray(value).broadcast_like(self.labels)

# Round: integers/binaries to 0 decimals, continuous to `decimals`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comments like these which explain the code


FILL_VALUE = {"labels": -1, "lower": np.nan, "upper": np.nan}

FIX_CONSTRAINT_PREFIX = "__fix__"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to constants.py

value = value.round(decimals)

# Clip to bounds
value = value.clip(min=self.lower, max=self.upper)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would fail-fast raise here in case values are incompatible with bounds

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clipping is meant to prevent infeasibilities due to floating point precision. Ill check if it can be done differntly while not hiding real infeasible values.

constraint_name = f"{FIX_CONSTRAINT_PREFIX}{self.name}"

# Remove existing fix constraint if present
if constraint_name in self.model.constraints:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add an additional argument overwrite that allows to drop existing fix constraints

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did so. I argue that this defaults to True, as the semantics of "fixing" are pretty straight forward: fix this variable to this value. But if you disagree we can default it to false, which might catch unexpected refixing.

# Handle integrality relaxation
if relax and (self.attrs.get("integer") or self.attrs.get("binary")):
original_type = "binary" if self.attrs.get("binary") else "integer"
self.model._relaxed_registry[self.name] = original_type
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is interesting, are we relaxing fixed binary and integer variable to save computation in the solving?

Copy link
Collaborator Author

@FBumann FBumann Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the problem pure LP if all binaries are fixed. My intention was to allow computing duals etc, but it might also speed up the solve, although i imagine the MILP solver to be as fast with fixed binaries.
This is the main reason for this PR, as this cannot be implemented outside of linopy easily. The equality constraints themselves could be done by a user with reasonable effort.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes perfect sense!

FBumann and others added 2 commits March 24, 2026 10:14
- Use as_dataarray() instead of DataArray() for value conversion
- Remove explanatory inline comments
- Move FIX_CONSTRAINT_PREFIX to constants.py
- Raise ValueError for out-of-bounds fix values instead of clipping
- Add overwrite parameter to fix() (default True)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Collaborator

@FabianHofmann FabianHofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel free to merge when you want

@FBumann FBumann merged commit f8dcfea into master Mar 24, 2026
21 checks passed
@FBumann FBumann deleted the feat/variable-fix-unfix branch March 24, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants