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
{{ message }}
Repository navigation
Tracking: sparse/CSR path follow-ups after #961 #972
Tracking issue for the CSR follow-ups after #961 (umbrella #756 is closed). Each child names a place where a CSR-backed expression silently densifies, or a gap in the frozen-constraint API. This issue fixes the order in which to address them.
Key observations
Line references in this section point to the code before PR 1.
LinearExpression.data (linopy/expressions.py:2358) is where a CSR-backed expression densifies on read. Other paths drop the backing without passing through it: @ returns dense when the input was dense and sparse_groupby is off (expressions.py:2609), CSRConstraint.to_dense()/mutable(), the rebuild at model.py:1345, and the return None fallbacks in _sparse_matmul, _try_csr_merge, _aligned and csr_rhs, which lose the reason.
Zero policy on the sparse path: every operation keeps explicit zeros (merge, scaling, sum, groupby, selection), so sparse and dense give the same terms up to order; only @/dot prunes them. Cell activeness is carried by const alone. Export culls zeros regardless; Zero culling forces model rebuilding #925 is an export/persistent-diff question and does not block this series.
Plan
Four pull requests, grouped by shared design rather than one per issue:
Serve shape/sizes/coords/dims/isnull from the CSR store. .is_sparse, repr marker. Opt-in warn_on_densify through one _densify_notice(reason) helper, called at data, at each return None fallback and in CSRConstraint.to_dense. data stays a one-way conversion (no cached dense copy next to _csr, the setters would make it stale). Move aux coords into Grid.
sum(dim) by merging rows directly (CSRLinearExpression.aggregated), keeping explicit zeros. Chained groupby from a CSR source; fix the gate at expressions.py:613 to use coord_dims instead of self.data.dims.
Aux coords on CSRConstraint (trivial once in Grid), incl. netcdf round trip (io.py:1260). Explanatory errors for loc/update/from_rule naming .mutable(). Expression rhs moved to lhs.
flat/to_polars from the CSR store, modelled on CSRConstraint.to_polars. Sparse objective across objective.py, matrices.py, io.py, persistent/diff.py; decide where the objective name lives (CSRLinearExpression has no attrs).
One read-only model key instead of the three opt-ins (semantics="v1", sparse=/sparse_groupby, freeze_constraints), as proposed in #976 (under discussion). Replaces the earlier plan of per-call sparse= on @/dot/merge; removes the coupling of @ to options["sparse_groupby"].
Tests extend the existing helpers in test/test_csr.py (assert_frozen_equal, assert_terms_equal, assert_cells_equal, assert_contracted_equal) with a "backing kept" assertion instead of adding a new fixture.
Phase 1 made the sparse path complete and observable. A PyPSA-like benchmark on f665a260 shows where the time and memory now go. It has 200 buses x 720 snapshots, 2.09M variables, 1.73M constraints and 4.2M nonzeros.
phase
sparse
dense
dense, frozen
supply + storage + flow @ incidence
69 ms / 137 MB
2225 ms / 2965 MB
743 ms / 2965 MB
add_constraints(balance)
4 ms
33 ms
602 ms / 1739 MB
m.matrices
106 ms / 254 MB
1337 ms / 1832 MB
110 ms / 254 MB
to_highspy + to_file (LP)
1179 ms
3040 ms
1094 ms
total build + export
1.9-2.2 s
7.4 s
3.3 s
Findings:
The sparse gain comes from three places: groupby with uneven group sizes, @, and merges of their results. From add_constraints on, a sparse model and a frozen dense model run the same code.
Export now takes about 65% of the sparse build time. A large share of it is spent inside HiGHS.
These operations still densify a CSR-backed expression, silently by default: shift, roll, diff, merge(dim=...), multiplication by a DataArray with a new dimension, rolling, cumsum and quadratic products. For an uneven groupby result, this costs about 250 ms and 1.4 GB.
A CSR backing for expressions built straight from variables gives no gain. They have one term per cell and no padding, and a chain of CSR operations was not faster than dense.
Cache Model.matrices when all constraints are frozen, skip scaling lookups without scaling, skip eliminate_zeros for frozen blocks. Add the benchmark under benchmark/.
Part H, after PR 8, when the list of operations that keep the backing is final
8
Ordering notes:
PRs 5 and 7 are independent. PRs 5 and 6 help dense models too.
Rejected: CSR backing for variables and one-term expressions, G @ csr for sum (same speed as COO), and a lazy expression graph (it does not reduce the export costs, and it adds a second set of semantics).
Out of scope: multiplication by a DataArray with a new dimension, rolling, cumsum and quadratic products. Revisit them if users report them.
Full benchmark output (sparse)
=== sparse N_BUS=200 N_T=720 N_GEN=2000 N_LINE=300 nvars=2088000 ncons=1728000 nnz=4175200
vars 58 ms peak 44.2 MB
expr: groupby nodal supply 48 ms peak 88.7 MB
expr: sto groupby 66 ms peak 19.3 MB
expr: flow @ incidence 43 ms peak 35.3 MB
expr: balance sum 69 ms peak 137.0 MB
cons: balance 4 ms peak 4.6 MB
expr: soc shift 128 ms peak 24.1 MB
cons: soc 48 ms peak 23.6 MB
expr+cons: p <= pmax 120 ms peak 152.7 MB
objective 71 ms peak 65.2 MB
matrices 106 ms peak 254.0 MB
to_highspy 436 ms peak 215.9 MB
to_file(lp) 743 ms peak 187.1 MB
TOTAL 1940 ms
Timings are from one machine with tracemalloc on, so they are higher than without it. The memory peaks are stable across runs.
Note
The following content was generated by AI.
Describe the feature you'd like to see
Tracking issue for the CSR follow-ups after #961 (umbrella #756 is closed). Each child names a place where a CSR-backed expression silently densifies, or a gap in the frozen-constraint API. This issue fixes the order in which to address them.
Key observations
Line references in this section point to the code before PR 1.
LinearExpression.data(linopy/expressions.py:2358) is where a CSR-backed expression densifies on read. Other paths drop the backing without passing through it:@returns dense when the input was dense andsparse_groupbyis off (expressions.py:2609),CSRConstraint.to_dense()/mutable(), the rebuild atmodel.py:1345, and thereturn Nonefallbacks in_sparse_matmul,_try_csr_merge,_alignedandcsr_rhs, which lose the reason.Gridholds only per-dim indexes. Aux coords live next to it inCSRLinearExpression.coordsand are dropped byCSRConstraint.from_csr(constraints.py:1401), which is CSRConstraint drops auxiliary coordinates when a sparse expression becomes a constraint #941.sum,groupby, selection), so sparse and dense give the same terms up to order; only@/dotprunes them. Cell activeness is carried byconstalone. Export culls zeros regardless; Zero culling forces model rebuilding #925 is an export/persistent-diff question and does not block this series.Plan
Four pull requests, grouped by shared design rather than one per issue:
Grid, and all operations that keep the backing. Merged.soften/penalty=on frozen constraints, so the model key in PR 4 needs no special case for softening. Merged.shape/sizes/coords/dims/isnullfrom the CSR store..is_sparse, repr marker. Opt-inwarn_on_densifythrough one_densify_notice(reason)helper, called atdata, at eachreturn Nonefallback and inCSRConstraint.to_dense.datastays a one-way conversion (no cached dense copy next to_csr, the setters would make it stale). Move aux coords intoGrid.scaled_by/shiftedfor non-scalar constants, aligned through_matmul_operand_to_matrixsum(dim)by merging rows directly (CSRLinearExpression.aggregated), keeping explicit zeros. Chainedgroupbyfrom a CSR source; fix the gate atexpressions.py:613to usecoord_dimsinstead ofself.data.dims.mask=part of #970selthroughreindexed,iselas row gather. Keep_try_csr_mergesparse when aux coords differ across grids.CSRConstraint(trivial once inGrid), incl. netcdf round trip (io.py:1260). Explanatory errors forloc/update/from_rulenaming.mutable(). Expression rhs moved to lhs.flat/to_polarsfrom the CSR store, modelled onCSRConstraint.to_polars. Sparse objective acrossobjective.py,matrices.py,io.py,persistent/diff.py; decide where the objective name lives (CSRLinearExpressionhas noattrs).CSRConstraintnatively (one slack term per active row), drop thepenalty+freezeerrorsemantics="v1",sparse=/sparse_groupby,freeze_constraints), as proposed in #976 (under discussion). Replaces the earlier plan of per-callsparse=on@/dot/merge; removes the coupling of@tooptions["sparse_groupby"].Ordering notes:
constraints.pyso fix: support adding/removing variables after adding frozen constraint #806 can land before PR 2.test/test_csr.py(assert_frozen_equal,assert_terms_equal,assert_cells_equal,assert_contracted_equal) with a "backing kept" assertion instead of adding a new fixture.signinto_constraintand non-scalarfillnastill densify. groupby-sum pads every group to the largest group size #745 is likely covered by sparsegroupby; re-check and close or narrow.Phase 2: performance and remaining densification
Note
The following section was generated by AI.
Phase 1 made the sparse path complete and observable. A PyPSA-like benchmark on
f665a260shows where the time and memory now go. It has 200 buses x 720 snapshots, 2.09M variables, 1.73M constraints and 4.2M nonzeros.supply + storage + flow @ incidenceadd_constraints(balance)m.matricesto_highspy+to_file(LP)Findings:
groupbywith uneven group sizes,@, and merges of their results. Fromadd_constraintson, a sparse model and a frozen dense model run the same code.shift,roll,diff,merge(dim=...), multiplication by a DataArray with a new dimension,rolling,cumsumand quadratic products. For an unevengroupbyresult, this costs about 250 ms and 1.4 GB.Plan: four pull requests, then the docs.
Model.matriceswhen all constraints are frozen, skip scaling lookups without scaling, skipeliminate_zerosfor frozen blocks. Add the benchmark underbenchmark/.CSRConstraint.from_dense, sort only whennterm > 1aggregatedshift/roll/diffandmerge(dim=...)(see theSnapshotWindow.mergecomment below),warn_on_densifyon by default in a sparse modelOrdering notes:
G @ csrforsum(same speed as COO), and a lazy expression graph (it does not reduce the export costs, and it adds a second set of semantics).rolling,cumsumand quadratic products. Revisit them if users report them.Full benchmark output (sparse)
Timings are from one machine with
tracemallocon, so they are higher than without it. The memory peaks are stable across runs.Children
Model.matricesand trim the matrix export path #1008CSRConstraint#1009shift,roll,diffandmerge(dim=...)#1011