Repository navigation
feat: long-form inputs via from_long and relation group-by via Relation (#1019 step 5) - #1059
Merged
Merged
Conversation
Table pivots pandas/polars long tables onto target coords for bounds, coefficients, constants, rhs and penalty. expr.groupby(relation_frame).sum() scatters repeated member rows through from_grouper on both stores; quadratic relation group-by raises.
Merging this PR will not alter performance
Comparing Footnotes
|
…n replace lazy Table Long tables convert to a DataArray at the call site with strict key checks; nothing leaks into ConstantLike or alignment. groupby takes linopy.Relation instead of detecting relation frames by shape.
from_long(frame, coords, default) takes keys from columns naming a dim of coords and exactly one value column; relation group-by scatters members directly; from_long/Relation emit EvolvingAPIWarning via a shared helper in constants; stricter key dtypes (date, categorical); long-form notebook and docs.
…es gaps, vectorised selection keeps CSR Relation sums compose a vectorised isel with the row groupby, so the positions/take plumbing in csr.py and store.py returns to master.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1049. Part of #1019 (step 5, the last step).
Note
The following content was generated by AI.
Changes proposed in this Pull Request
Both entry points are marked as evolving: they emit
linopy.EvolvingAPIWarningonce per session (message prefixlong-form:), as their signature and input format may still change in minor releases. The once-per-key helper moved frompiecewise.pytoconstants.pyand is shared.linopy.from_long(frame, coords, default=None) -> DataArray. Converts a long table (polars or pandas frame) to a DataArray, eagerly at the call site. Columns named after a dimension ofcoordsare the key columns; exactly one other column must remain and is the value column, the result is named after it.coordsis required and defines the cube, e.g.coords=var.coords: cells not listed raise unlessdefault=fills them, dims ofcoordswithout a key column are left to linopy's usual broadcasting. The result is an ordinary DataArray, so it works everywhere a constant does (bounds, coefficients, arithmetic, rhs, penalty) and passes the strict 1.0 alignment unchanged; nothing is added toConstantLikeoras_dataarray.Strict schema, fail fast: no key column, zero or several value columns (so a misspelled key column raises), null keys, non-numeric or null values, duplicate keys, keys outside the coords, coords with duplicate labels, a key dtype family (numeric / datetime / date / duration / string, pandas categoricals by their categories) or time zone differing from the coord, lossy key casts, and pandas frames with a non-default index raise.
pl.LazyFrame, Series and MultiIndex forms are not accepted.Pivot: polars keys are mapped to positions by a left join against each coord (arithmetic for a
RangeIndex), combined into a flat index in polars, checked for duplicates and gaps withn_unique, then scattered into onenp.fullbuffer; a full, sorted table is reshaped without the buffer. pandas input usesIndex.get_indexer(no pyarrow dependency). The key-to-position step is one private helper shared withRelation.linopy.Relation(frame, member, group, groups=None)for relation group-by.expr.groupby(Relation(...)).sum()sums over a mapping with repeated member keys, e.g.(link, port) → bus.membernames one or several key columns, each a dimension of the expression; the frame has exactly the member and group columns. The sum is a composition of two existing operations: a vectorisediselwith the member positions of the relation rows, sorted by group, then the row-dimgroupby(pd.Series).sum(). No store changes are needed for it,csr.pyandstore.pyare back to master apart from a sorted fast path incoo_to_csr.groups=lists the labels the result spans: groups with no member get a zero cell, rows with a group outsidegroupsraise.Relation.group_of(coords)is the adjoint, the group of each member cell as a DataArray, soprice.sel(bus=bus_of.group_of(gens))spreads nodal prices onto generators; gaps and duplicate members raise. A bare frame passed togroupbyraises and points toRelation. Quadratic expressions raiseNotImplementedError.Vectorised selection keeps the sparse store:
isel,sel,__getitem__with DataArray indexers over a new indexed dimension, andwhere, no longer fall back to dense onModel(sparse=True). The composition costs more than a dedicated scatter on CSR (20k links × 500 timesteps, 2 rows per link: 0.89 s / 1142 MB against 0.48 s / 819 MB), accepted for the ~60 deleted store lines and the general sparse selection it buys.A tutorial notebook,
examples/long-form-inputs.ipynb, builds a small dispatch model from long-form data with both tools.Design notes
A first draft used a lazy
linopy.Tableconstant pivoted once target coords were known; it leaked into ~40ConstantLikecall sites and converted polars through Python lists and a cube-sized MultiIndex. It was replaced by the eager function.An explicit
valueargument was dropped in favour of inferring the single non-key column. Inferring the coords from the data was rejected: absent rows would shrink the cube, label order would depend on the data,defaultwould lose its meaning and dtype checks their reference (xr.DataArray.from_seriesalready covers that). Wide frames returning a Dataset were rejected: a misspelled numeric key column would silently become a value column, andConstantLikehas no Dataset.Benchmark: long table → 10k × 2920 cube
Table(first draft)from_long, polarsfrom_long, pandasReview and tests
Branch review (four reviewers plus a coordinator) on the first draft found one must-fix (quadratic relation group-by crashed), fixed. Not addressed: sparse quadratic group-by with a plain Series already fails on master; a cube too large for
np.prodgives an unclearIndexError.Full suite: 5048 passed, 84 skipped.
mypy .,ruff check .,ruff format --check .and the doctests pass.Checklist
AGENTS.md).doc.doc/release_notes.rstof the upcoming release is included.