Skip to content

fix: reject MultiIndex constant operands under v1 - #1040

Merged
FabianHofmann merged 1 commit into
masterfrom
fix/v1-multiindex-constant
Oct 22, 2026
Merged

FabianHofmann merged 1 commit into
masterfrom
fix/v1-multiindex-constant

Conversation

@FabianHofmann

Copy link
Copy Markdown
Collaborator

Related to #846.

Note

The following was generated by AI.

Changes proposed in this Pull Request

Under v1, arithmetic or comparison between a variable/expression and a constant DataArray on a MultiIndex dimension failed with an opaque xarray error, or silently produced an expression over the MultiIndex:

import pandas as pd, xarray as xr, linopy
linopy.options["semantics"] = "v1"
m = linopy.Model()
x = m.add_variables(lower=0, coords=[pd.RangeIndex(2, name="a")], name="x")
mi = pd.MultiIndex.from_product([[0, 1], ["u", "v"]], names=["a", "b"])
c = xr.DataArray(range(4), coords={"s": mi}, dims="s")
x + c  # ValueError: a already exists as coordinate or variable name.
  • broadcast_to_coords (the choke point for constant operands) now rejects, under v1, a MultiIndex dimension the target coords don't already carry. It raises the same message as add_variables, with the reset_index hint. This covers + - * /, reflected ops, <= >= == and add_constraints.
  • Legacy is unchanged.
  • A parametrized test covers operations × Variable/LinearExpression operand × colliding/fresh level names.

This is a step toward removing the MultiIndex machinery at 1.0 (#846).

Checklist

  • AI-generated content is marked (see AGENTS.md).
  • 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

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.06865 (1.12)   │   15.03 (1.00) 
 (v1)        0.06111 (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.07702 (1.14)   │   12.04 (1.00) 
 (v1)        0.06736 (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.02885 (1.11)   │   15.20 (1.0) 
 (v1)        0.02601 (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.04451 (1.07)   │   44.93 (1.0) 
 (v1)        0.04148 (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.03262 (1.10)   │   11.51 (1.0) 
 (v1)        0.02952 (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.07549 (1.09)   │   24.45 (1.06) 
 (v1)        0.06924 (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.08641 (1.10)   │   16.12 (1.00) 
 (v1)        0.07863 (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.018 (1.08)   │   752.18 (1.10) 
 (v1)       0.0166 (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.0175 (1.09)   │   3.12 (1.33) 
 (v1)       0.01612 (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.06035 (1.38)   │   126.16 (1.44) 
 (v1)        0.04359 (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.05943 (1.36)   │   126.16 (1.44) 
 (v1)        0.04356 (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.05953 (1.43)   │   126.16 (1.44) 
 (v1)         0.0415 (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.04105 (1.04)   │    712.45 (1.0) 
 (v1)        0.03943 (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.04107 (1.16)   │   4.62 (1.29) 
 (v1)        0.03551 (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.2757 (1.03)   │   707.91 (1.10) 
 (v1)        0.2673 (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.293 (1.06)   │   18.34 (1.0) 
 (v1)       0.2772 (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.2876 (1.05)   │   9.54 (1.0) 
 (v1)        0.2738 (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.05754 (1.13)   │   3.85 (1.14) 
 (v1)          0.051 (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.05698 (1.12)   │   216.98 (1.11) 
 (v1)        0.05097 (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.02899 (1.10)   │   938.49 (1.0) 
 (v1)        0.02644 (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.03071 (1.10)   │   9.66 (1.0) 
 (v1)        0.02793 (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.03011 (1.11)   │   5.32 (1.0) 
 (v1)        0.02722 (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.01254 (1.01)   │   1.29 (1.0) 
 (v1)        0.01245 (1.0)   │   1.29 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=100]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01244 (1.01)   │   1.29 (1.0) 
 (v1)        0.01233 (1.0)   │   1.29 (1.0) 

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

benchmarks/drivers/test_build.py::test_build[piecewise-n=1000]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.1391 (1.06)   │   946.85 (1.06) 
 (v1)        0.1306 (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.137 (1.04)   │   12.01 (1.00) 
 (v1)       0.1321 (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.03621 (1.10)   │   147.70 (1.06) 
 (v1)        0.03305 (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.03583 (1.09)   │   2.60 (1.09) 
 (v1)        0.03281 (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.03012 (1.11)   │   704.59 (1.05) 
 (v1)        0.02721 (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.07677 (1.07)   │   137.97 (1.0) 
 (v1)         0.0717 (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.04751 (1.04)   │   69.22 (1.0) 
 (v1)        0.04565 (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.03461 (1.13)   │   402.37 (1.00) 
 (v1)        0.03063 (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.03384 (1.13)   │   3.23 (1.20) 
 (v1)        0.03003 (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.03718 (1.09)   │   29.00 (1.54) 
 (v1)        0.03405 (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.04919 (1.15)   │   37.95 (1.43) 
 (v1)        0.04279 (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.0728 (1.03)   │    410.14 (1.0) 
 (v1)       0.07092 (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.0789 (1.00)   │     9.94 (1.0) 
 (v1)       0.07864 (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 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 181 untouched benchmarks
⏩ 181 skipped benchmarks1


Comparing fix/v1-multiindex-constant (c8c172e) with master (0af6929)

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 54291d4 into master Oct 22, 2026
24 checks passed
@FabianHofmann
FabianHofmann deleted the fix/v1-multiindex-constant branch October 22, 2026 09:13
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