refac: Harmonize linopy operations with breaking convention#591
refac: Harmonize linopy operations with breaking convention#591FBumann wants to merge 19 commits intoharmonize-linopy-operationsfrom
Conversation
Use "exact" join for +/- (raises ValueError on mismatch), "inner" join for *// (intersection), and "exact" for constraint DataArray RHS. Named methods (.add(), .sub(), .mul(), .div(), .le(), .ge(), .eq()) accept explicit join= parameter as escape hatch. - Remove shape-dependent "override" heuristic from merge() and _align_constant() - Add join parameter support to to_constraint() for DataArray RHS - Forbid extra dimensions on constraint RHS - Update tests with structured raise-then-recover pattern - Update coordinate-alignment notebook with examples and migration guide Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@FabianHofmann Im quite happy with the notebook now. It showcases the convention and its consequences. |
…ords. Here's what changed: - test_linear_expression_sum / test_linear_expression_sum_with_const: v.loc[:9].add(v.loc[10:], join="override") → v.loc[:9] + v.loc[10:].assign_coords(dim_2=v.loc[:9].coords["dim_2"]) - test_add_join_override → test_add_positional_assign_coords: uses v + disjoint.assign_coords(...) - test_add_constant_join_override → test_add_constant_positional: now uses different coords [5,6,7] + assign_coords to make the test meaningful - test_same_shape_add_join_override → test_same_shape_add_assign_coords: uses + c.to_linexpr().assign_coords(...) - test_add_constant_override_positional → test_add_constant_positional_different_coords: expr + other.assign_coords(...) - test_sub_constant_override → test_sub_constant_positional: expr - other.assign_coords(...) - test_mul_constant_override_positional → test_mul_constant_positional: expr * other.assign_coords(...) - test_div_constant_override_positional → test_div_constant_positional: expr / other.assign_coords(...) - test_variable_mul_override → test_variable_mul_positional: a * other.assign_coords(...) - test_variable_div_override → test_variable_div_positional: a / other.assign_coords(...) - test_add_same_coords_all_joins: removed "override" from loop, added assign_coords variant - test_add_scalar_with_explicit_join → test_add_scalar: simplified to expr + 10
|
The convention should be Why
cost = xr.DataArray([10, 20], coords=[("tech", ["wind", "solar"])])
capacity # dims: (tech=["wind", "solar"], region=["A", "B"])
cost * capacity # ✓ tech matches exactly, region broadcasts freely
capacity.sel(tech=["wind", "solar"]) * renewable_costNo operation should introduce new dimensions Neither side of any arithmetic operation should be allowed to introduce dimensions the other doesn't have. The same problem applies to cost_expr # dims: (tech, time)
regional_expr # dims: (tech, time, region)
cost_expr + regional_expr # ✗ silently expands to (tech, time, region)
capacity # dims: (tech, region, time)
risk # dims: (tech, scenario)
risk * capacity # ✗ silently expands to (tech, region, time, scenario)An explicit pre-check on all operations: asymmetric_dims = set(other.dims).symmetric_difference(set(self.dims))
if asymmetric_dims:
raise ValueError(f"Operation introduces new dimensions: {asymmetric_dims}")Summary
|
Let's clearly differentiate between dimensions and labels. labelsI agree with "exact" for labels by default, but we need an easy way to have inner or outer joining characteristics. I found the pyoframe conventions x + y.keep_extras() to say that an outer join is in order and mismatches should fill with 0. x + y.drop_extras() to say that you want an I have in a different project used | 0 to indicate keep_extras ie (x + y | 0). dimensionsi am actually fond of the ability to auto broadcast over different dimensions. and would want to keep that (actually my main problem with pyoframe). your first example actually implicitly assumes broadcasting. |
Dimensions and broadcastingI agree that auto broadcasting is helpful in some cases. So the full convention requires two separate things: labelsI'm not sure if I like this approach, as it's needs careful state management of the flags on expressions. The flag (keep or drop extras) needs to be handled. import linopy
# outer join — fill gaps with 0 before adding
x_aligned, y_aligned = linopy.align(x, y, join="outer", fill_value=0)
x_aligned + y_aligned
# inner join — drop non-matching coords before adding
x_aligned, y_aligned = linopy.align(x, y, join="inner")
x_aligned + y_alignedCombining disjoint expressions would then still need the explicit methods though. |
|
The proposed convention for all arithmetic operations in linopy: I'm not sure how to implement the | operator yet. Might need some sort of flag/state for defered indexing |
|
I thought about the pipe operator: Would this be an issue for you? |
… constants Implements the new arithmetic convention for all operations (+, -, *, /): - Rule 1: Exact label matching on shared dimensions (join="exact") - Rule 2: Constants cannot introduce new dimensions not in the expression Adds escape hatches: FillWrapper via `expr | 0`, named methods with explicit join= parameter, and linopy.align() with configurable join. Changes FILL_VALUE["const"] from NaN to 0 for cleaner semantics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of unwrapping to Datasets, aligning, and manually reconstructing linopy types, align now calls each object's own .reindex() which handles type-specific fill values (vars=-1, coeffs=NaN, const=0) automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove FillWrapper class and | operator (deferred for later design) - Expression.reindex: scalar fill_value applies to const only, vars/coeffs always use sentinels (-1/NaN) - Variable.reindex: no fill_value param, always uses sentinels - Update notebook: remove | 0 section, fix align example - Clean up __init__.py exports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Default fill_value=0, always applies to const only. No dict pass-through. vars/coeffs always use sentinels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests verify commutativity, associativity, distributivity, identity, and negation laws. Two known breakages (associativity and distributivity with constants that introduce new dims) are marked xfail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Spec and tests for commutativity, associativity, distributivity, identity, negation, and zero. Two known violations marked xfail: associativity and distributivity with constants that introduce new dims. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Constants can now introduce new dimensions in arithmetic (+, -, *, /), preserving all standard algebraic laws (associativity, distributivity). The dim-subset check remains for constraint RHS to catch accidental broadcasting. Default fill value for const changed from 0 to NaN. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Constraint RHS can now introduce new dimensions, just like arithmetic. For ==, broadcasting to incompatible values results in solver infeasibility. For <=/>= it creates redundant but harmless constraints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pure xarray/pandas/numpy operations before entering linopy use their own alignment rules. Document the risks and the xarray exact join workaround. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document assign_coords (recommended) and join="override" for handling operands with mismatched coordinate labels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@coroa @FabianHofmann Im quite happy with this. Looking forward to your thoughts. |
Arithmetic Convention for linopy
Strict coordinate alignment for linopy: mismatches are loud, broadcasting preserves algebra.
Convention
Exact match on shared dimensions — When two operands share a dimension, their labels must match exactly (
join="exact"). RaisesValueErroron mismatch.Free broadcasting on non-shared dimensions — Constants and expressions can introduce new dimensions. All standard algebraic laws hold (commutativity, associativity, distributivity).
Escape hatches: `.sel()` to subset, `.add(join="outer")` for explicit join, `linopy.align()` for pre-alignment.
Changes vs. master
Open Questions / TODOs