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, 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