Skip to content

refactor: move alignment code to linopy/alignment.py; restructure tests#742

Merged
FBumann merged 7 commits into
masterfrom
refactor/alignment-module
Jun 3, 2026
Merged

refactor: move alignment code to linopy/alignment.py; restructure tests#742
FBumann merged 7 commits into
masterfrom
refactor/alignment-module

Conversation

@FBumann
Copy link
Copy Markdown
Collaborator

@FBumann FBumann commented Jun 3, 2026

Follow-up requested by @FabianHofmann in #732:

move all broadcasting and alignment code from common.py to a new module alignment.py; move all related tests to test_alignment.py, make more use of test classes to structure them better

Commit 1 — the module move (pure, no behavior change)

linopy/alignment.py owns the seam between user input and linopy's labelled arrays:

Group Functions
coords parsing _coords_to_dict, _as_index, _as_multiindex
conversion get_from_iterable, pandas_to_dataarray, numpy_to_dataarray, _named_pandas_to_dataarray, fill_missing_coords, as_dataarray
MI projection _LevelProjection, _project_onto_multiindex_levels, _warn_implicit_projections (with TODO(#738))
broadcasting _broadcast_to_coords, broadcast_to_coords
validation validate_alignment
symmetric align

common.py keeps the general utilities (formatting, label indexes, polars helpers, decorators). Hard move, no re-exports (nothing outside linopy imports these from linopy.common — checked PyPSA). The split is clean: nothing remaining in common.py depends on anything moved.

Commit 2 — the test restructure

test/test_alignment.py, one class per concept:

TestAsDataarrayFrom{Pandas,Numpy,Scalar,DataArray}, TestAsDataarrayMultiIndexCoords
TestCoordsToDict            # the coords-entry naming rules
TestBroadcastToCoords       # strict=False mechanics
TestMultiIndexProjection    # projection values, deprecation warnings, gaps
TestStrictMode              # strict=True contract
TestValidateAlignment
TestAlign
  • Fixtures: mi_index / mi_coords / by_level1 replace the MultiIndex setup that was repeated inline in ~10 tests.
  • Parametrization: the pandas dims-naming rules (12 near-identical tests → 2 parametrized tables) and the numpy labeling rules (9 → 1 table + 2 edge cases).
  • test_common.py keeps the utility tests (iterate_slices, polars, formatting, …).

v1 readiness (#717)

TestMultiIndexProjection collects every test that forks under the v1 semantics (deprecation warnings → raises). When #717's autouse semantics fixture lands, these get @pytest.mark.legacy and the v1 counterparts slot into the same class — no reorganization needed. Markers themselves are deliberately not added here (they belong to #717, where they have meaning).

Verification — coverage measured, not just asserted

master (before) this PR
common.py (monolith) 787 stmts, 56 missed → 91%
alignment.py 286 stmts, 2 missed → 99%
common.py (utilities) 516 stmts, 40 missed → 90%
combined missed lines 56 42
full suite 3202 passed 3210 passed (8 new edge-case tests)

The restructure preserved coverage exactly (the same missed lines, split across files), and the third commit then closed most of the remaining gaps: bare-string dims, 0-d arrays, partially-named MI levels, gap detection with extra dims, gap-error truncation, and the previously zero-coverage MultiIndex round-trip in coords_to_dataset_vars / coords_from_dataset (CSRConstraint serialization). The 2 remaining uncovered lines in alignment.py are defensive branches for inputs outside the DimsLike type contract.

  • mypy clean, pre-commit clean.

🤖 Generated with Claude Code

FBumann and others added 2 commits June 3, 2026 09:09
…ment.py

Pure move, no behavior change. The new module owns the seam between user
input and linopy's labelled arrays:

- coords parsing: _coords_to_dict, _as_index, _as_multiindex
- conversion: get_from_iterable, pandas_to_dataarray, numpy_to_dataarray,
  _named_pandas_to_dataarray, fill_missing_coords, as_dataarray
- MultiIndex projection: _LevelProjection, _project_onto_multiindex_levels,
  _warn_implicit_projections
- broadcasting: _broadcast_to_coords, broadcast_to_coords
- validation: validate_alignment
- symmetric alignment: align

common.py keeps the general utilities (formatting, label indexes, polars
helpers, decorators). Importers (model, expressions, variables, __init__,
tests) updated; no re-exports.

Follow-up requested by @FabianHofmann in #732.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One class per concept in linopy.alignment, mirroring the module's public
surface:

- TestAsDataarrayFrom{Pandas,Numpy,Scalar,DataArray} + MultiIndexCoords
- TestCoordsToDict (the coords-entry naming rules)
- TestBroadcastToCoords (strict=False mechanics)
- TestMultiIndexProjection (projection values, deprecation warnings,
  coverage gaps — the legacy/v1 fork point for #717)
- TestStrictMode (strict=True contract)
- TestValidateAlignment
- TestAlign

Shared fixtures (mi_index / mi_coords / by_level1) replace the repeated
MultiIndex setup; the pandas dims-naming and numpy labeling tests are
consolidated into parametrized tables. test_common.py keeps the utility
tests. Full suite count unchanged (3202) — no coverage lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FBumann FBumann marked this pull request as draft June 3, 2026 07:32
Copy link
Copy Markdown
Collaborator

@FabianHofmann FabianHofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lovely!

@FBumann
Copy link
Copy Markdown
Collaborator Author

FBumann commented Jun 3, 2026

@FabianHofmann I need to convince myself that these changes are thorough and complete. Ill mark it as ready later

FBumann and others added 2 commits June 3, 2026 10:29
…tion

alignment.py: 97% -> 99%. New edge-case tests: bare-string dims, 0-d
arrays, fill_missing_coords type check, partially-named MI levels,
gap detection with extra dims, gap-error truncation (>5 missing
combinations). The two remaining uncovered lines are defensive branches
for inputs outside the DimsLike contract (non-iterable dims).

common.py: 88% -> 90%. The MultiIndex round-trip through
coords_to_dataset_vars / coords_from_dataset (used by CSRConstraint)
had zero coverage; now pinned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FBumann FBumann marked this pull request as ready for review June 3, 2026 08:55
@FBumann FBumann requested a review from FabianHofmann June 3, 2026 09:15
Copy link
Copy Markdown
Collaborator

@FabianHofmann FabianHofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only these two, feel free to merge after

Comment thread test/test_alignment.py Outdated
Comment thread test/test_alignment.py Outdated
FBumann and others added 2 commits June 3, 2026 11:36
Co-authored-by: Fabian Hofmann <fab.hof@gmx.de>
Co-authored-by: Fabian Hofmann <fab.hof@gmx.de>
@FBumann FBumann merged commit d5a82a2 into master Jun 3, 2026
18 of 19 checks passed
@FBumann FBumann deleted the refactor/alignment-module branch June 3, 2026 10:27
FBumann added a commit that referenced this pull request Jun 3, 2026
Brings in the coords-as-truth stack (#732), the alignment.py module split
(#742), has_terms (#743), and the MatrixAccessor caching (#716).

Conflict resolutions (consistent rule: keep this branch's v1/legacy
dispatch structure, use master's conversion calls inside it):

- expressions.py: _add_constant, _apply_constant_op_{v1,legacy}, and
  to_constraint use broadcast_to_coords(..., strict=False) instead of
  as_dataarray; SUPPORTED_CONSTANT_TYPES -> CONSTANT_TYPES.
- variables.py: to_linexpr converts via broadcast_to_coords(strict=False),
  then applies the v1/legacy absence handling.
- __init__.py: align from linopy.alignment + LinopySemanticsWarning export.

Test adaptations for post-#732 APIs and semantics:

- test_legacy_violations: name the MultiIndex coords entry (required
  since #732).
- test_linear_expression: the masked-addend tails of test_nterm and
  test_variable_names pin legacy absence behavior; split into
  @pytest.mark.legacy / @pytest.mark.v1 pairs (section 6 divergence).

Full suite under both semantics: 6446 passed, 514 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FBumann added a commit that referenced this pull request Jun 3, 2026
…cenario B v1)

Closes the two integration points between the alignment layer (#732/#742)
and the v1 semantics infrastructure:

- _warn_implicit_projections -> _enforce_implicit_projections: under
  legacy semantics, the implicit MultiIndex-level projection deprecation
  now goes through warn_legacy() / LinopySemanticsWarning (#738,
  replacing EvolvingAPIWarning, which stays piecewise-only); under the
  v1 convention it raises ValueError (sections 8 and 11) — the
  projection must be written explicitly (scenario B of the #732/#737
  discussion).

- as_expression no longer swallows the underlying conversion error:
  "Cannot convert to LinearExpression: <original message>" so the v1
  guidance reaches the user.

Tests: the MI-projection deprecation tests in test_alignment,
test_variable, test_constraint, and test_linear_expression are marked
@pytest.mark.legacy and assert LinopySemanticsWarning; each gains a
@pytest.mark.v1 counterpart asserting the v1 raise. Full suite under
both semantics: 6446 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants