CSRConstraint drops auxiliary coordinates when a sparse expression becomes a constraint
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 257
- Forks
- 87
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 29
Description
[!NOTE]
This issue was written by AI (Claude), based on a limitation found while working on #940.
Version Checks (indicate both or one)
- I have confirmed this bug exists on the lastest release of Linopy.
- I have confirmed this bug exists on the current
masterbranch of Linopy (with #940 applied, which is the first change that puts auxiliary coordinates onto a CSR payload).
Issue Description
Under v1 semantics, a CSR-backed LinearExpression (the result of groupby(...).sum(sparse=True)) carries one-dimensional auxiliary coordinates, e.g. the period/region labels of a multi-key observed=True grouping on the flat group dim, or aux coords on surviving dims. When such an expression is turned into a constraint via Model.add_constraints, the resulting CSRConstraint drops every auxiliary coordinate and keeps only the per-dimension index.
The dense path keeps them, so the sparse and dense results differ in their coordinates. This matters for later sel/where on the constraint by aux label and for the v1 aux-conflict rule, which cannot fire on the constraint side anymore.
Cause: CSRConstraint.from_payload builds the constraint from payload.indexes[d] for d in payload.grid_dims only and never looks at payload.coords (linopy/constraints.py, from_payload). CSRConstraint itself stores only a list of per-dim pd.Index, so there is currently no slot for aux coords to live in.
Reproducible Example
import linopy
import pandas as pd
from linopy import Model
linopy.options["semantics"] = "v1"
m = Model()
x = m.add_variables(coords=[pd.RangeIndex(4, name="s")], name="x")
expr = x.to_linexpr().assign_coords(
period=("s", [1, 1, 2, 2]), region=("s", ["n", "s", "n", "s"])
)
dense = expr.groupby(["period", "region"]).sum(observed=True)
sparse = expr.groupby(["period", "region"]).sum(observed=True, sparse=True)
print(list(dense.coords)) # ['period', 'region', 'group']
print(list(sparse.coords)) # ['group', 'period', 'region']
c_dense = m.add_constraints(dense <= 1, name="dense")
c_sparse = m.add_constraints(sparse <= 1, name="sparse")
print(list(c_dense.coords)) # ['period', 'region', 'group']
print(list(c_sparse.coords)) # ['group'] <- period and region are gone
Expected Behavior
c_sparse.coords contains period and region on the group dim, matching the dense constraint and the sparse expression it was built from.
Installed Versions
linopy 0.9.1.post1.dev20+g73e65c22d plus PR #940, Python 3.13, xarray/pandas from uv sync --extra dev.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in linopy/constraints.py at CSRConstraint.from_payload and reproduce the sparse groupby example from the issue. Trace how payload.coords are handled compared with payload.indexes, then ensure the resulting sparse constraint preserves auxiliary coordinates such as period and region, matching the dense constraint and enabling later sel/where operations.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 62/100