You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Direct-API SOS construction is broken for both solvers that implement it (Gurobi and Xpress, after #684). Both pass linopy variable labels as if they were solver column positions; the two are only equal when no variable in the model is masked. With a single masked entry anywhere, the SOS set is built against the wrong columns — and the solvers happen to raise rather than silently mis-build, so the bug has been invisible until you try a masked SOS variable.
Output (against the xpress-direct branch from #684, on master for Gurobi):
=== gurobi ===
RAISED IndexError: index 3 is out of bounds for axis 0 with size 3
=== xpress ===
?404 Error: Invalid column number passed to XPRSaddsets.
Element 2 of your array has invalid column number 3
RAISED SolverError
Root cause
In both _build_solver_model implementations, the SOS code passes var.labels values straight into the solver's addSOS as if they were column indices:
Gurobi (linopy/solvers.py:1530-1532): gm.addSOS(sos_type, x[indices].tolist(), weights) — x is gm.addMVar(M.vlabels.shape, ...), indexed by position. The code passes linopy labels as positions.
Xpress (feat(xpress): add direct API support via loadproblem #684, linopy/solvers.py:2168-2170): problem.addSOS(indices, weights, type=sos_type) — problem was built via loadproblem, where column position in the solver matches position in M.vlabels. Same mistake.
Whenever any variable in the model is masked, M.vlabels becomes non-contiguous (e.g. [0, 2, 3] instead of [0, 1, 2, 3]), and linopy label ≠ solver column position. The labels that worked correctly under the no-mask happy path now over-index the solver model.
The Xpress branch additionally filters out -1 labels (Xpress lines 2161-2167), which the Gurobi branch doesn't — that catches one shape of the bug (the masked entry inside the SOS variable itself) but not the broader label-vs-position mismatch caused by masks anywhere in the model.
Fix direction
The SOS preparation is the same logic in both solvers up to the vendor call. Worth pulling it into a single shared step that:
drops -1 (missing) entries from var.labels and the matching weights
resolves the remaining linopy labels to solver column positions (e.g. via model.variables.label_index)
returns (indices, weights) ready for the vendor addSOS
Each solver then keeps just its native call. Where the helper lives is open.
Needs regression coverage with a masked SOS variable in both test_optimization.py (end-to-end solve) and test_sos_constraints.py (model construction).
Summary
Direct-API SOS construction is broken for both solvers that implement it (Gurobi and Xpress, after #684). Both pass linopy variable labels as if they were solver column positions; the two are only equal when no variable in the model is masked. With a single masked entry anywhere, the SOS set is built against the wrong columns — and the solvers happen to raise rather than silently mis-build, so the bug has been invisible until you try a masked SOS variable.
Repro
Output (against the
xpress-directbranch from #684, on master for Gurobi):Root cause
In both
_build_solver_modelimplementations, the SOS code passesvar.labelsvalues straight into the solver'saddSOSas if they were column indices:linopy/solvers.py:1530-1532):gm.addSOS(sos_type, x[indices].tolist(), weights)—xisgm.addMVar(M.vlabels.shape, ...), indexed by position. The code passes linopy labels as positions.linopy/solvers.py:2168-2170):problem.addSOS(indices, weights, type=sos_type)—problemwas built vialoadproblem, where column position in the solver matches position inM.vlabels. Same mistake.Whenever any variable in the model is masked,
M.vlabelsbecomes non-contiguous (e.g.[0, 2, 3]instead of[0, 1, 2, 3]), and linopy label ≠ solver column position. The labels that worked correctly under the no-mask happy path now over-index the solver model.The Xpress branch additionally filters out
-1labels (Xpress lines 2161-2167), which the Gurobi branch doesn't — that catches one shape of the bug (the masked entry inside the SOS variable itself) but not the broader label-vs-position mismatch caused by masks anywhere in the model.Fix direction
The SOS preparation is the same logic in both solvers up to the vendor call. Worth pulling it into a single shared step that:
-1(missing) entries fromvar.labelsand the matching weightsmodel.variables.label_index)(indices, weights)ready for the vendoraddSOSEach solver then keeps just its native call. Where the helper lives is open.
Needs regression coverage with a masked SOS variable in both
test_optimization.py(end-to-end solve) andtest_sos_constraints.py(model construction).