refactor: unify coords-as-truth handling in add_variables/add_constraints#732
Open
FBumann wants to merge 21 commits into
Open
refactor: unify coords-as-truth handling in add_variables/add_constraints#732FBumann wants to merge 21 commits into
FBumann wants to merge 21 commits into
Conversation
`add_variables` had two related bugs when `lower`/`upper` were arrays: - pandas Series/DataFrame bounds missing a dimension in `coords` had the missing dimension silently dropped (#709), unlike DataArray bounds which were already broadcast. - DataArray bounds missing a dimension were expanded with `DataArray.expand_dims`, which prepends new dimensions and produces a `coords`-mismatched dimension order in the resulting variable (#706). The order depended on the type of the bounds, so scalar bounds worked but two array bounds missing the same dimension did not. Replace `_validate_dataarray_bounds` plus the downstream `as_dataarray(..., coords)` call with a single helper `_as_dataarray_in_coords`. It converts any input (pandas with named axes via `to_xarray`, otherwise via `as_dataarray`), validates the result against `coords`, expands missing dims, transposes to coords order, and reconstructs the coord variables in that order. `expand_dims` and `transpose` are no-ops when the array already matches, so scalar / full-dim DataArray bounds keep their fast path. Also fix `linopy.piecewise._broadcast_points`, which built the `expand_dims` map from a `set`, producing a hash-randomized dimension order across processes. Iterate expressions and dims in declaration order instead. Closes #706 and #709. Supersedes #710 and #719. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restate #706/#709's fix as a single principle in the docstring, release note, and `_as_dataarray_in_coords` helper docstring: when `coords` is provided to `add_variables`, it is the source of truth for dimensions, dimension order, and coordinate values, and `lower` / `upper` are broadcast and aligned to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0.7.0 already shipped "add_variables no longer ignores coords when lower / upper are DataArrays". Recast the new bullet as extending that fix to the remaining gaps (pandas bounds; dim order across bound types) so the continuity is visible from the release notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng gaps" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Parametrize test_bound_broadcast_missing_dim with three additional cases: Series with MultiIndex(time, colour), DataFrame with MultiIndex columns(space, colour), and DataFrame with MultiIndex index(time, space). Exercises the `while DataFrame: unstack()` loop and the MultiIndex branch of `_named_pandas_to_dataarray`. - Add test_dataarray_coord_reorder for the same-values-different-order reindex branch (previously only the unequal-values raise was covered). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Relocate `_as_dataarray_in_coords` and its helpers (`_coords_to_dict`, `_named_pandas_to_dataarray`) from `model.py` into `common.py`, alongside the existing `as_dataarray` they parallel. Rename to `as_dataarray_in_coords` (no leading underscore) since it is no longer file-local — other modules can import the strict-coords variant when migrating call sites. Pure relocation: no behavior change, no call-site changes beyond `add_variables`'s import. Refs #723. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…anches Replace the unstack-while-loop / split named-check structure with a single up-front "all axes named" check and a single ``DataFrame.stack(level=list(range(nlevels)), future_stack=True)`` call that collapses all column levels into the row MultiIndex in one shot. Same observable behaviour, fewer moving parts, no defensive unreachable branches. Add tests covering the unnamed-axis fall-through path, the empty-coords short-circuit in ``as_dataarray_in_coords``, and the ``MultiIndex``-on-a-dim ``continue`` in the validation loop. Together with the restructure these bring the new helper code to full patch coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pandas allows any hashable in ``pd.Index.names`` (tuples, ints, etc.), but only strings map cleanly to xarray dim names. Reject anything non-string up front so the pandas falls back to ``as_dataarray`` instead of producing a DataArray with an awkward non-string dim name that downstream validation would reject with a confusing "extra dimensions" error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inputs without their own meaningful labels — numpy arrays, polars Series, pandas with unnamed axes — fell through ``as_dataarray_in_coords`` via a short-circuit return. That meant: - The default ``dim_0`` / ``dim_1`` axis names from ``as_dataarray`` leaked into the result, so a pandas Series without an index name combined with another bound carrying a named coord produced a spurious 2-D variable. - Shape mismatches surfaced further downstream as confusing "coordinates do not match" errors against the auto-generated ``RangeIndex``. The fall-through now: (a) defaults ``dims`` to coords' keys so axes get labelled correctly; (b) runs the same validate / expand / transpose path as labelled inputs; (c) re-assigns coords from ``expected`` on the resulting DataArray so positional inputs align to coords by position. A shape mismatch surfaces as xarray's clear ``conflicting sizes`` from ``assign_coords``. MultiIndex coords are left alone (re-assigning a PandasMultiIndex emits a FutureWarning). Replaces the tautological ``test_pandas_bound_with_unnamed_axis_falls_through`` (which sneaked past by naming the coord ``"dim_0"`` to match the auto-generated dim) with ``test_positional_bound_aligns_to_coords`` that asserts actual positional alignment across numpy / Series / DataFrame, plus ``test_positional_bound_wrong_size_raises_clear_error`` for the shape-mismatch path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o_dict ``reformulate_sos1`` / ``reformulate_sos2`` built the coords for the indicator variable as ``[var.coords[d] for d in var.dims]``, which is a list of ``xarray.DataArray`` coord objects. The rest of linopy passes ``coords`` as a list of ``pd.Index``. The mix slipped through under the old short-circuit fall-through but broke once the helper started defaulting ``dims`` from ``_coords_to_dict(coords)`` — non-``pd.Index`` entries were silently dropped, so ``len(dims) < len(coords)`` and xarray raised ``different number of dimensions on data and dims: 2 vs 1``. Use ``var.indexes[d]`` instead — it returns the actual ``pd.Index`` (regular or MultiIndex) for the dim and preserves structure that ``pd.Index(coord.values, ...)`` would flatten. Also widen ``_coords_to_dict`` to accept any entry with a ``.name`` (xarray DataArrays included) so a future caller passing mixed types doesn't silently lose coords. The reformulator fix removes the only known producer of mixed-type coords; this is belt-and-suspenders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the permissive ``getattr(c, "name", None)`` check with an explicit allow-list: ``pd.Index`` (named or not — unnamed silently skip as before) and unnamed sequences (``list`` / ``tuple`` / ``range`` / ``numpy.ndarray``). Any other type (notably ``xarray.DataArray``, but also ``pd.Series`` and friends) now raises ``TypeError`` with a hint to pass ``variable.indexes[<dim>]`` instead. This would have caught the SOS-reformulator bug at the source instead of letting it surface as a confusing xarray error about mismatched dim counts ten frames down. Drop ``DataArray`` from the matching ``coords`` type hints in ``model.py`` / ``expressions.py`` so the documented and runtime type sets agree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _coords_to_dict: explicitly handle pd.MultiIndex — register under .name if set, raise TypeError with guidance if .name is missing - _named_pandas_to_dataarray: use DataArray(df) directly for single-level DataFrames; reserve stack() for MultiIndex axes - as_dataarray_in_coords: validate MultiIndex dims with .equals() instead of silently skipping them - Move MultiIndex tests into dedicated TestAddVariablesMultiIndexCoords class with shared fixture
…nts (#725) * fix(model): apply coords-as-truth rule to mask in add_variables/add_constraints Routes ``mask`` through ``as_dataarray_in_coords(mask, data.coords)`` instead of ``as_dataarray(...) + broadcast_mask(...)``, so pandas ``Series`` / ``DataFrame`` masks missing a dimension are broadcast to the variable / constraint shape (parallel to the bounds fix in the previous PR). The ``add_variables`` ``mask`` type hint widens to ``MaskLike`` to match ``add_constraints``. The deprecation announced via ``FutureWarning`` in ``broadcast_mask`` ("Missing values will be filled with False ... In a future version, this will raise an error") is now in effect: masks whose coordinates are a sparse subset of the data's coordinates raise ``ValueError`` instead of silently filling missing entries. Mask dims not in the data raise ``ValueError`` instead of ``AssertionError`` for consistency with the bounds path. ``broadcast_mask`` had no other callers and is removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update doc/release_notes.rst Co-authored-by: Fabian Hofmann <fab.hof@gmx.de> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Fabian Hofmann <fab.hof@gmx.de>
…on (#726) * fix(model): apply coords-as-truth rule to mask in add_variables/add_constraints Routes ``mask`` through ``as_dataarray_in_coords(mask, data.coords)`` instead of ``as_dataarray(...) + broadcast_mask(...)``, so pandas ``Series`` / ``DataFrame`` masks missing a dimension are broadcast to the variable / constraint shape (parallel to the bounds fix in the previous PR). The ``add_variables`` ``mask`` type hint widens to ``MaskLike`` to match ``add_constraints``. The deprecation announced via ``FutureWarning`` in ``broadcast_mask`` ("Missing values will be filled with False ... In a future version, this will raise an error") is now in effect: masks whose coordinates are a sparse subset of the data's coordinates raise ``ValueError`` instead of silently filling missing entries. Mask dims not in the data raise ``ValueError`` instead of ``AssertionError`` for consistency with the bounds path. ``broadcast_mask`` had no other callers and is removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: unify as_dataarray; split broadcasting from coords validation Closes #723. Folds the body of `as_dataarray_in_coords` into `as_dataarray` and extracts the contract checks into `assert_compatible_with_coords`, so linopy now has one broadcasting primitive and one validation companion. `as_dataarray(arr, coords)` aligns the result against `coords` for every input type: labels positional inputs (numpy / unnamed pandas / scalar) by position, reindexes same-values-different-order, expands missing dims, and transposes to coords order. Extra dims and disagreeing value sets on shared dims pass through unchanged, so xarray broadcasting in expression arithmetic keeps working. `assert_compatible_with_coords(arr, coords)` enforces the strict contract (`arr.dims ⊆ coords.dims`, plus exact coord-value equality on shared dims). `add_variables` and `add_constraints` now call it after `as_dataarray` for `lower` / `upper` / `mask`, replacing the deleted `as_dataarray_in_coords` helper. `_coords_to_dict` filters MultiIndex level coords out of `xarray.Coordinates` inputs so the new strict-by-default path treats `station` (and not its derived `letter` / `num` levels) as the dim. Test suite: 3698 passed (no regressions). Two existing tests were updated to reflect the new "coords is source of truth" semantics: `test_as_dataarray_with_ndarray_coords_dict_set_dims_not_aligned` (extra coord entries now broadcast in) and `test_dataarray_extra_dims` (now triggers the subset check rather than the value-mismatch check). Microbenchmark in dev-scripts/benchmark_as_dataarray.py shows flat timings vs the base branch on both add_variables-heavy and arithmetic- heavy workloads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: dims= names unnamed coords; doctest the add_variables contract Closes a silent-failure gap in the strict coords-as-truth path: when the caller passed ``coords=[[1, 2, 3]], dims=["x"]`` to ``add_variables``, ``_coords_to_dict`` returned an empty mapping (unnamed sequences carry no dim name), so the strict checks short-circuited and bounds with extra dims or mismatched values flowed through unchecked, producing variables with frankenstein outer-joined coord values. ``_coords_to_dict`` now accepts an optional ``dims`` argument that names unnamed sequence entries by position. ``as_dataarray`` and ``assert_compatible_with_coords`` plumb it through; ``add_variables`` forwards ``kwargs.get("dims")`` to the assertions for ``lower`` and ``upper``. ``coords=[[1, 2, 3]], dims=["x"]`` now enforces the same contract as ``coords={"x": [1, 2, 3]}`` or ``coords=[pd.Index([1, 2, 3], name="x")]``. Docstring of ``add_variables.coords`` documents the contract (subset-of-dims, dim order, value match with auto-reindex, missing-dim broadcast) and includes four doctests pinning it: the extra-dim raise, the value-mismatch raise, the same-values-different-order auto-reindex, and the unnamed-coords-plus-dims opt-in. Test suite: 3698 passed (parity with the previous commit on this branch). ``pytest --doctest-modules linopy/model.py -k add_variables`` also green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add align_to_coords with semantic validation error messages Introduce align_to_coords to wrap as_dataarray and assert_compatible_with_coords with user-facing labels (lower bound, upper bound, mask). Errors now name the argument and distinguish extra dimensions, coordinate mismatches, and conversion failures. Extend mask validation to use coords+dims= when provided. Co-authored-by: Cursor <cursoragent@cursor.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * refactor(model): simplify mask align; preserve TypeError in align_to_coords Three cleanups on top of align_to_coords: - Drop the trailing ``.broadcast_like(data.labels)`` in ``add_variables`` and ``add_constraints`` mask paths. ``as_dataarray`` already expands missing dims to ``coords`` shape, so the broadcast was a no-op. - Stop overriding the caller's ``dims=`` in the ``add_variables`` mask path when ``coords is None``. The previous code stripped ``dims`` and forced ``dims=data.dims``; with ``data.coords`` being an xarray ``Coordinates`` with already-named dims, the user's ``dims`` is harmless to forward and the override was just hiding intent. Mask now goes through one ``align_to_coords`` call regardless of whether ``coords`` is supplied. - Split the exception handler in ``align_to_coords``: ``TypeError`` from unsupported input types is re-raised as ``TypeError`` (still labeled), while ``ValueError`` / ``CoordinateValidationError`` stay ``ValueError``. Preserves the original type signature for callers that want to ``except TypeError``. New test ``test_align_to_coords_preserves_type_errors`` pins the TypeError pass-through. Suite: 3703 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: rename assert_compatible_with_coords to validate_alignment Per PR review: align on the project's `validate_*` naming convention and remove the implicit "AssertionError" connotation of `assert_*`. Pairs naturally with `align_to_coords`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
4 tasks
#729 made `.name` required on `pd.MultiIndex` sequence-form coord entries (xarray needs a single dim name for the flattened index). test_repr.py was the only remaining call site missing the assignment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`xarray.Coordinates.dims` is typed `Hashable`, so the dict-comprehension return and the `sorted()` calls in the validation message tripped mypy. The function's other branches already accept `c.name` / `dim_names[i]` (both Hashable), so widening the return type is the honest signature. Also: drop `.data` from the add_variables doctest — use the public `v.lower` property instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
@FabianHofmann Although the breaking changes are quite small, id like to test this against pypsa and flixopt, to make sure they dont use any of the now raising paths. |
This was referenced May 27, 2026
…#733) * refactor(common): clarify coords-entry rules and tighten error labels Stacks on top of #732. Three small follow-ups from PR review: - Remove dead `broadcast_mask` (claimed removed in #732, was still present). - `as_dataarray`: normalize bare-tuple coord entries to lists so `coords=[(0, 1, 2)]` behaves identically to `coords=[[0, 1, 2]]` (xarray reads `(a, b)` as `(dim_name, values)` and would otherwise raise a confusing error). - `align_to_coords`: pre-validate coords via `_coords_to_dict` so TypeErrors from a bad `coords` argument propagate with their own message instead of being relabeled "<label> could not be aligned to coords: ...", which previously misdirected users to inspect the bound/mask. Docs: replace the prose paragraph in `_coords_to_dict`'s docstring with an explicit rules table covering every container form and sequence-entry case (named/unnamed `pd.Index`, `pd.MultiIndex`, bare sequences, with/without positional `dims=`). Tests: new `TestCoordsToDictRules` class in `test_common.py` mirrors the docstring table one-test-per-rule so the executable spec stays visibly aligned with the documented contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(common): allow dims= to name an unnamed pd.MultiIndex Mirrors the existing rule for unnamed pd.Index: an unnamed MultiIndex paired with a positional dims=[i] entry now gets its flat .name set to dims[i] on a shallow copy (caller's MultiIndex is not mutated). Per-level names are preserved. Removes the asymmetry between Index and MultiIndex in _coords_to_dict: both can now be named either inline (.name) or by position (dims=[i]). An unnamed MultiIndex with no positional dims still raises TypeError since xarray requires a single flat name. Adds one rule-table row and two tests (test_unnamed_multiindex_with_dims_uses_dims, test_unnamed_multiindex_without_dims_raises). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(common): scope tuple-normalize check to lists/tuples with tuple entries The previous `not isinstance(coords, Coordinates | Mapping)` form was broad and rebuilt `coords` as a fresh list on every call (even when no tuple entries were present). Switch to a positive `isinstance(coords, list | tuple)` guard with a short-circuit `any(isinstance(c, tuple) for c in coords)` check, so the comprehension only runs when there is actually a tuple to normalize. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…anges Two pre-existing Upcoming-Version bullets from master had been dropped on this branch, most likely as merge-conflict casualties: - ``LinearExpression.where`` doc + ``BaseExpression.variable_names`` entry - Mosek basic/IPM solution-inspect fix Restore both verbatim from master. Also add an explicit Breaking Changes bullet for the coord-as-truth behaviour changes that previously lived only under Bug Fixes: the mask FutureWarning -> ValueError flip, the AssertionError -> ValueError flip on extra mask dims, and the new TypeError on an unnamed pd.MultiIndex without a positional dims=[i] entry. The Bug Fixes entries still carry the migration detail; the Breaking Changes bullet points there so readers scanning by section don't miss the rename of warnings to errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Merge the two Bug Fixes bullets (bounds + mask) into one. The separation read as "same fix, applied twice" without adding info; one bullet covers both with the same migration detail. - Shorten the Breaking Changes bullet — it duplicated the v0.6.3 ``FutureWarning`` and ``AssertionError`` parentheticals already in Bug Fixes; keep only the FutureWarning summary and the pd.MultiIndex addition. - Collapse the Internal as_dataarray bullet from 8 wrapped lines to one, and drop the "Validation errors name the argument" UX detail — accurate but not structural enough for a release note. No facts removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
@FabianHofmann This is the most relevant PR now i think, to continue working on the others |
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 #706, #709, #723.
What changes
This branch makes
coordsthe single source of truth for every shape-bearing argument ofadd_variablesandadd_constraints(lower,upper,mask), and refactors the underlying helpers along the seam between broadcasting and validation. The work landed over several stacked sub-PRs (#722, #725, #726, #729, #733 + small follow-ups); this umbrella PR consolidates them for a single review againstmaster.1. Bounds —
add_variables0.7.0 (#614) made
coordsthe source of truth forDataArraybounds. This branch closes the two remaining gaps so the rule holds for every bound type and dimension order:Series/DataFramebounds missing a dimension incoordsare now broadcast tocoordsinstead of being silently dropped (add_variables: pandas Series/DataFrame bounds with missing dimensions silently drop the dimension #709).coordsregardless of bound type. Previously, two array bounds missing the same dimension produced a prepended order while scalar bounds keptcoordsorder (add_variables: DataArray bounds with missing dimensions yield wrong dimension order #706).DataArraybounds with an extra dim or mismatched coord values now raiseValueErrorinstead of producing varied downstream errors.2. Mask —
add_variablesandadd_constraintsThe same coords-as-truth rule now applies to
mask. PandasSeries/DataFramemasks missing a dimension are broadcast to the variable/constraint shape (parallel to the bounds fix). TheFutureWarningthatbroadcast_maskhad been emitting since v0.6.3 (#580) is now in effect:False+FutureWarningValueErrormask.reindex({...}, fill_value=False)AssertionErrorValueErrorpd.Series/pd.DataFramemask missing a dim was sometimes silently droppedbroadcast_maskhad no other callers and is removed; theadd_variablesmasktype hint widens toMaskLiketo matchadd_constraints.3. Helper refactor —
linopy.commonas_dataarray_in_coordsis folded intoas_dataarrayand split along the seam between "broadcastarragainstcoords" and "enforce thecoordscontract":as_dataarray(arr, coords)— the broadcasting primitive. For every input type, the result is aligned withcoords: positional inputs (numpy / unnamed pandas / scalar) are labeled by position, shared-dim coords are reindexed when values are equal in a different order, dims present incoordsbut not inarrare expanded, and the result is transposed tocoordsorder. Extra dims and disagreeing value sets on shared dims pass through, so xarray broadcasting in expression arithmetic keeps working.validate_alignment(arr, coords)— the validation companion. RaisesValueErrorifarrintroduces dims not incoordsor if a shared dim has disagreeing coord values. Error messages name the argument ("lower bound","upper bound","mask") and explain whether dims or values disagree.align_to_coords(value, coords, *, label)— convenience wrapper used byadd_variablesandadd_constraints(callsas_dataarraythenvalidate_alignmentwith the right label). Coords-parsing errors (e.g. bad sequence entry) propagate with their original message; only failures convertingvalueget the labeled wrapper.The previous
_validate_dataarray_bounds+as_dataarray(arr, coords)pair inadd_variablescollapses into this single helper layer.expand_dims({})and identitytransposeare metadata-only no-ops, so scalar bounds and full-dimDataArraybounds already incoordsorder pay no extra cost.4. Coords-entry rules —
_coords_to_dictThe canonical contract for how each
coordsform / entry is named. Docstring of_coords_to_dictcarries the same table;TestCoordsToDictRulesintest/test_common.pymirrors it one-test-per-row.Container forms (top-level
coords):coordsis...xarray.CoordinatesMappingdictcopy.Sequence-entry rules (
iis the position;dims[i]is the matchingdims=entry when one exists). An entry is unlabeled if it's an unnamedpd.Indexor a barelist/tuple/range/ndarray.pd.Indexwith.name.namedims[i]dims[i])dim_0etc. downstreampd.MultiIndexwith.name.namepd.MultiIndexwithout.namedims[i]pd.MultiIndexwithout.namedims[i])DataArray)Bare-tuple coord entries (
coords=[(0, 1, 2)]) are normalized to lists before reaching xarray, which would otherwise read(a, b)as(dim_name, values).5. Piecewise, SOS, and small fixes
linopy.piecewise._broadcast_pointsbuilt itsexpand_dimsmap from aset, giving hash-randomized dim order across processes. Iterates expressions and dims in declaration order now.var.indexes[d]for reformulated bounds;_coords_to_dictwidened so this path doesn't choke onDataArray.coordsentries._named_pandas_to_dataarrayonly accepts string axis names (other hashables fall through toas_dataarray).Audit summary (the 12 call sites listed in #723)
model.pylower/upperinadd_variablesalign_to_coords(=as_dataarray+validate_alignment)model.pymaskinadd_variablesmodel.pymaskinadd_constraintsexpressions.pyarithmetic (5 sites)variables.py:330to_linexpr(coefficient)expressions.py/model.py/variables.pysites withoutcoords(8 sites)Breaking changes
Compared to v0.7.0:
ValueError(wasFutureWarning+ silent fill, deprecated since v0.6.3 / Reinsert broadcasted mask #580).ValueError(wasAssertionError).MultiIndexpassed as a sequence-form coord entry must have.nameset, or be paired withdims=[i](raisesTypeErrorotherwise).Series/DataFramebound or mask missing a dim is now broadcast (was sometimes silently dropped) — strictly more cases work, so not a migration concern, but observable.ValueErroronDataArraybounds with extra dims or mismatched coord values is unchanged from v0.7.0 — only the error message now carries an argument label ("lower bound: …","upper bound: …","mask: …").Sub-PRs merged into this branch
mask(stacked on fix: broadcast pandas/DataArray bounds in coords and preserve dim order #722)as_dataarray; split broadcasting from coords validation (stacked on fix: apply coords-as-truth rule to mask in add_variables/add_constraints #725)dims=parity, tuple normalization,align_to_coordserror-label tightening, remove deadbroadcast_maskassert_compatible_with_coords→validate_alignment).#724 ("part 1" of the relocation) was closed in favour of landing the relocation as part of #726.
Test plan
test/test_common.py::TestCoordsToDictRules— 22 tests, one per row of the coords-entry rules table.test/test_common.py— also covers the broadcasting/validation split (extra-dim preservation, disjoint shared-dim values,validate_alignmentextra-dim raise, value-mismatch raise, subset-dims allowed).test/test_variable.py::TestAddVariablesBoundsWithCoords— parametrized broadcast / dim-order / coord-reorder coverage.test/test_variable.py::TestAddVariablesMultiIndexCoords— MultiIndex bounds + mismatch raises.test/test_piecewise_constraints.py::test_broadcast_points_dim_order_follows_exprs.🤖 Generated with Claude Code