Skip to content

fix(semantics): close gaps and false positives in legacy v1 warnings - #1026

Merged
FabianHofmann merged 1 commit into
masterfrom
fix/legacy-warning-gaps
Oct 9, 2026
Merged

FabianHofmann merged 1 commit into
masterfrom
fix/legacy-warning-gaps

Conversation

@FabianHofmann

Copy link
Copy Markdown
Collaborator

Note

The following content was generated by AI.

Changes proposed in this Pull Request

An audit of the legacy → v1 semantics warnings found operations whose result changes under v1 without any LinopySemanticsWarning, plus some warnings that fire although results agree. This PR closes those gaps so that, under legacy, each diverging operation warns once and points at user code.

Missing warnings (now warn)

  • Arithmetic that fills absent slots of an expression with 0: x.diff("t") >= 0, expr.shift(t=1) + y, expr.where(mask) + 5, expr.reindex(...) + 1, quadratic (x*y).where(mask) + 1. New helper warn_legacy_absence in semantics.py, used in merge and the legacy constant paths.
  • .div with a reindexing join= that creates divisor positions (legacy fills 1, v1 inf).
  • Quadratic products with mismatched or reordered coordinates (x(t=0,1,2) * y(t=0,1)): the coordinate check now runs before broadcast_like. Under v1 this raises, like other exact-join mismatches. Legacy also no longer writes NaN floats into the factor labels; labels stay integer with -1.

False positives / noise removed

  • x.where(mask).sum() and .groupby(...).sum() no longer warn (identical results).
  • (1*x).le(rhs_superset, join="left") no longer warns.
  • (1*x) <= rhs_subset warns once instead of twice (spurious "NaN in constant" from linopy's own reindex).
  • Quadratic × constant with mismatched coords warns once instead of 2–4 times.
  • Hardcoded stacklevels removed from the legacy warning paths, so warnings point at user code.

Corrected messages

  • Merge mismatches: "positional when sizes match, otherwise outer join by label" (was "left-join").
  • Quadratic products: label-aligned, no join= hint (mul rejects join= for expr × expr).
  • to_constraint with explicit join=: legacy ignores the join, v1 applies it (was "v1 raises").
  • Variable * constant: aligns by label (was "positional").

x.where(mask) <= 3 keeps warning: legacy keeps a labelled empty row, v1 drops it, so constraint labels and ncons differ.

Legacy results are unchanged apart from the integer-label fix for quadratic products. The only v1 behaviour change is the new raise for mismatched quadratic factors.

Test runs
  • test/test_legacy_violations.py: 412 passed, 234 skipped (new parametrized class TestLegacyWarnsAtDivergence)
  • Broader suite excluding test/remote, Xpress and non-HiGHS optimization: 5304 passed, 1096 skipped
  • test_optimization with HiGHS: 539 passed
  • ruff clean; mypy shows no new errors
  • A 48-case script compared results and warnings against master under both semantics.

Checklist

  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Build cost — v1 vs legacy

v1 build peak & time relative to legacy, on this commit — not a comparison against master (that is CodSpeed).

peak — v1 / legacy time — v1 / legacy
peak v1/legacy time v1/legacy
Full table (time + peak, mean)
benchmarks/drivers/test_build.py::test_build[basic-n=10]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.06366 (1.13)   │   15.03 (1.00) 
 (v1)        0.05621 (1.0)   │    15.00 (1.0) 

benchmarks/drivers/test_build.py::test_build[basic-n=250]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.06886 (1.08)   │   12.04 (1.00) 
 (v1)        0.06353 (1.0)   │    12.04 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=0]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.02708 (1.12)   │   15.20 (1.0) 
 (v1)        0.02429 (1.0)   │   15.20 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=100]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.04078 (1.11)   │   44.93 (1.0) 
 (v1)        0.03689 (1.0)   │   44.93 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=50]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.02998 (1.12)   │   11.51 (1.0) 
 (v1)        0.02685 (1.0)   │   11.51 (1.0) 

benchmarks/drivers/test_build.py::test_build[expression_arithmetic-n=10]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.07086 (1.07)   │   24.45 (1.06) 
 (v1)        0.06596 (1.0)   │    23.04 (1.0) 

benchmarks/drivers/test_build.py::test_build[expression_arithmetic-n=250]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.07973 (1.10)   │   16.12 (1.00) 
 (v1)        0.07276 (1.0)   │    16.12 (1.0) 

benchmarks/drivers/test_build.py::test_build[knapsack-n=10000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.01651 (1.07)   │   752.18 (1.10) 
 (v1)        0.01538 (1.0)   │    685.15 (1.0) 

benchmarks/drivers/test_build.py::test_build[knapsack-n=100]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.01636 (1.08)   │   3.12 (1.33) 
 (v1)        0.01508 (1.0)   │    2.34 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=0]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04763 (1.18)   │   126.16 (1.44) 
 (v1)        0.04027 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=100]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04667 (1.23)   │   126.16 (1.44) 
 (v1)        0.03786 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=50]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04747 (1.27)   │   126.16 (1.44) 
 (v1)        0.03738 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[masked-n=100]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.03928 (1.05)   │    712.45 (1.0) 
 (v1)        0.03754 (1.0)   │   787.73 (1.11) 

benchmarks/drivers/test_build.py::test_build[masked-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.03753 (1.08)   │   4.62 (1.29) 
 (v1)        0.03468 (1.0)   │    3.57 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=0]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.2607 (1.08)   │   707.91 (1.10) 
 (v1)        0.2418 (1.0)   │    643.85 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=100]
                 time (s)        peak (MiB) 
 name                mean   │          mean 
────────────────────────────────────────────
 (legacy)   0.2718 (1.06)   │   18.34 (1.0) 
 (v1)        0.2566 (1.0)   │   18.34 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=50]
                 time (s)       peak (MiB) 
 name                mean   │         mean 
───────────────────────────────────────────
 (legacy)   0.2729 (1.09)   │   9.54 (1.0) 
 (v1)        0.2503 (1.0)   │   9.54 (1.0) 

benchmarks/drivers/test_build.py::test_build[milp-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.05302 (1.09)   │   3.85 (1.14) 
 (v1)        0.04854 (1.0)   │    3.37 (1.0) 

benchmarks/drivers/test_build.py::test_build[milp-n=50]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.0538 (1.12)   │   216.98 (1.11) 
 (v1)       0.04784 (1.0)   │    196.23 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=0]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.02759 (1.12)   │   938.49 (1.0) 
 (v1)        0.02467 (1.0)   │   938.49 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=100]
                 time (s)       peak (MiB) 
 name                mean   │         mean 
───────────────────────────────────────────
 (legacy)   0.0284 (1.12)   │   9.66 (1.0) 
 (v1)       0.02533 (1.0)   │   9.66 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=50]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.02795 (1.15)   │   5.32 (1.0) 
 (v1)        0.02439 (1.0)   │   5.32 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=0]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01348 (1.02)   │   1.28 (1.0) 
 (v1)        0.01319 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=100]
                 time (s)       peak (MiB) 
 name                mean   │         mean 
───────────────────────────────────────────
 (legacy)   0.0134 (1.02)   │   1.28 (1.0) 
 (v1)       0.01317 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=50]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01346 (1.01)   │   1.28 (1.0) 
 (v1)        0.01329 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[piecewise-n=1000]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.1357 (1.10)   │   946.85 (1.06) 
 (v1)        0.1238 (1.0)   │    891.54 (1.0) 

benchmarks/drivers/test_build.py::test_build[piecewise-n=10]
                 time (s)         peak (KiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.1317 (1.09)   │   12.01 (1.00) 
 (v1)         0.121 (1.0)   │    11.99 (1.0) 

benchmarks/drivers/test_build.py::test_build[qp-n=1000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.03397 (1.12)   │   147.70 (1.06) 
 (v1)         0.0304 (1.0)   │    139.87 (1.0) 

benchmarks/drivers/test_build.py::test_build[qp-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.03372 (1.11)   │   2.60 (1.09) 
 (v1)        0.03038 (1.0)   │    2.38 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=0]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.02782 (1.09)   │   704.59 (1.05) 
 (v1)        0.02549 (1.0)   │    673.70 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=100]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.06684 (1.06)   │   137.97 (1.0) 
 (v1)         0.0628 (1.0)   │   137.97 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=50]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.04391 (1.09)   │   69.22 (1.0) 
 (v1)        0.04028 (1.0)   │   69.22 (1.0) 

benchmarks/drivers/test_build.py::test_build[sos-n=1000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.03252 (1.13)   │   402.37 (1.00) 
 (v1)        0.02879 (1.0)   │    402.30 (1.0) 

benchmarks/drivers/test_build.py::test_build[sos-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.03211 (1.13)   │   3.23 (1.20) 
 (v1)        0.02829 (1.0)   │    2.69 (1.0) 

benchmarks/drivers/test_build.py::test_build[sparse_network-n=10]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.03544 (1.10)   │   29.00 (1.54) 
 (v1)        0.03222 (1.0)   │    18.84 (1.0) 

benchmarks/drivers/test_build.py::test_build[sparse_network-n=250]
                 time (s)         peak (MiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.0448 (1.15)   │   37.95 (1.43) 
 (v1)       0.03881 (1.0)   │    26.51 (1.0) 

benchmarks/drivers/test_build.py::test_build[storage-n=10]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.06681 (1.01)   │    410.14 (1.0) 
 (v1)         0.0659 (1.0)   │   427.84 (1.04) 

benchmarks/drivers/test_build.py::test_build[storage-n=250]
                 time (s)         peak (MiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.0711 (1.00)   │     9.94 (1.0) 
 (v1)       0.07105 (1.0)   │   10.22 (1.03) 

📊 Interactive plots + CSV: download the semantics-report-v1-vs-legacy artifact from this run.

Report-only · not a gate · refreshed on every push · obsolete once legacy is dropped.

@codspeed

codspeed Bot commented Oct 9, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 181 untouched benchmarks
⏩ 181 skipped benchmarks1


Comparing fix/legacy-warning-gaps (12cf59c) with master (1d3c516)

Open in CodSpeed

Footnotes

  1. 181 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@FabianHofmann
FabianHofmann merged commit e0177b2 into master Oct 9, 2026
24 checks passed
@FabianHofmann
FabianHofmann deleted the fix/legacy-warning-gaps branch October 9, 2026 08:27
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.

1 participant