From 5990409ef8131f4c898885fa69ad6d547c56a317 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:32:02 +0200 Subject: [PATCH 1/2] perf(v1): skip no-op exact-join reindex in constant ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _apply_constant_op_v1 / _add_constant_v1 resolve join=None to "exact", then _align_constant returned needs_data_reindex=True unconditionally — so a plain `coeffs * expr` ran xr.align + self.data.reindex_like (two full-dataset deepcopies) even though the factor was already broadcast to self.coords, making the exact-join alignment a no-op. first_mismatched_dim is the §8 exact check (order/label-strict via indexes.equals), so when it finds no mismatch the align changes nothing: return needs_data_reindex=False and take the cheap assign() path, skipping both copies. v1 build peak: isolated multiply -56%; full-build v1/legacy median 1.20x -> 1.00x (legacy unchanged — its default path already returned False). Full suite: 7629 passed under both semantics. Co-Authored-By: Claude Opus 4.8 (1M context) --- linopy/expressions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/linopy/expressions.py b/linopy/expressions.py index dd0a72d1..9205ef08 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -668,7 +668,12 @@ def _align_constant( aligned : DataArray The aligned constant. needs_data_reindex : bool - Whether the expression's data needs reindexing. + Whether the expression's data needs reindexing. ``False`` whenever the + shared-dim indexes already agree — the ``override`` / ``left`` paths + and ``exact`` join once ``first_mismatched_dim`` confirms the match — + so the caller skips a full-dataset ``reindex_like`` deepcopy on the + hot multiply/add path. Only the label-changing joins (inner / outer / + right) return ``True``. """ # §11: aux-coord conflict is independent of dim alignment — fires # on every join path. Gating it behind ``join is None`` (alongside @@ -748,6 +753,7 @@ def _align_constant( mismatch = first_mismatched_dim(self.const, other) if mismatch is not None: raise ValueError(_shared_dim_mismatch_message(*mismatch)) + return self.const, other, False self_const, aligned = xr.align( self.const, other, From c1be6328cc1e8f8bd095d3daddbdca9a2bd67afc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:33:41 +0200 Subject: [PATCH 2/2] perf: fold Variable*constant into one construction step (both modes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1a3f2be rerouted Variable.__mul__(DataArray) through `to_linexpr() * other` so the §5/§8 checks fire — but that materialises a unit `1*var` expression and then multiplies it, doubling the peak on multiply-heavy builds in BOTH semantics (kvl_cycles 129→168 MB, sparse_network, masked, sos, knapsack — the CodSpeed regressions on #717). Move the checks into the one-step path instead: to_linexpr(other) now enforces §8 (mismatched coefficient coords raise v1 / warn legacy), §11 (aux-coord conflict), alongside the existing §5 NaN check, so Variable.__mul__ can fold the coefficient in at construction with no intermediate expression. Build peak vs merge-base master (1dbde37): kvl_cycles 168→129 MB (parity) in both modes; all varying-data specs 1.00×; legacy/master and v1/master medians 1.00×. Full suite 7629 passed; §5/§8/§11 alignment tests green under both semantics. Co-Authored-By: Claude Opus 4.8 (1M context) --- linopy/variables.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/linopy/variables.py b/linopy/variables.py index cffc2b4e..ff01fd74 100644 --- a/linopy/variables.py +++ b/linopy/variables.py @@ -31,7 +31,7 @@ from xarray.core.utils import Frozen import linopy.expressions as expressions -from linopy.alignment import broadcast_to_coords +from linopy.alignment import as_dataarray, broadcast_to_coords from linopy.common import ( LabelPositionIndex, LocIndexer, @@ -64,8 +64,12 @@ TERM_DIM, ) from linopy.semantics import ( + _legacy_coord_mismatch_message, _legacy_masked_variable_message, + _shared_dim_mismatch_message, check_user_nan, + enforce_aux_conflict, + first_mismatched_dim, is_v1, warn_legacy, ) @@ -334,6 +338,20 @@ def to_linexpr( linopy.LinearExpression Linear expression with the variables and coefficients. """ + # §8: check on the raw coefficient, before broadcast aligns it away. + if not np.isscalar(coefficient): + coeff_da = as_dataarray(coefficient) + enforce_aux_conflict([self.labels, coeff_da], stacklevel=4) + mismatch = first_mismatched_dim(self.labels, coeff_da) + if mismatch is not None: + if is_v1(): + raise ValueError(_shared_dim_mismatch_message(*mismatch)) + warn_legacy( + _legacy_coord_mismatch_message( + "this operator's constant operand", *mismatch + ), + stacklevel=4, + ) coefficient = broadcast_to_coords(coefficient, coords=self.coords, strict=False) # §5: user-supplied NaN in the coefficient must raise (v1) / warn # (legacy) — it's the multiplicative analogue of ``x + nan_data`` @@ -446,14 +464,7 @@ def __mul__(self, other: SideLike) -> ExpressionLike: try: if isinstance(other, Variable | ScalarVariable): return self.to_linexpr() * other - # Scalars can take the fast path; for arrays / expressions go - # through the LinearExpression operator so semantics-aware - # alignment and NaN checks apply. - if np.isscalar(other): - if isinstance(other, float) and np.isnan(other): - return self.to_linexpr() * other - return self.to_linexpr(other) - return self.to_linexpr() * other + return self.to_linexpr(other) except TypeError: return NotImplemented