Skip to content

Commit c65098f

Browse files
committed
formatting
1 parent ff2f83f commit c65098f

File tree

4 files changed

+47
-29
lines changed

4 files changed

+47
-29
lines changed

doc/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Release Notes
33

44
.. Upcoming Version
55
6+
* Add simplify method to LinearExpression to combine duplicate terms
67
* Fix compatibility for xpress versions below 9.6 (regression)
78
* Performance: Up to 50x faster ``repr()`` for variables/constraints via O(log n) label lookup and direct numpy indexing
89
* Performance: Up to 46x faster ``ncons`` property by replacing ``.flat.labels.unique()`` with direct counting

linopy/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@
3939
GROUP_DIM = "_group"
4040
FACTOR_DIM = "_factor"
4141
CONCAT_DIM = "_concat"
42+
CV_DIM = "_cv"
4243
HELPER_DIMS: list[str] = [
4344
TERM_DIM,
4445
STACKED_TERM_DIM,
4546
GROUPED_TERM_DIM,
4647
FACTOR_DIM,
4748
CONCAT_DIM,
49+
CV_DIM,
4850
]
4951

5052

linopy/expressions.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
)
6767
from linopy.config import options
6868
from linopy.constants import (
69+
CV_DIM,
6970
EQUAL,
7071
FACTOR_DIM,
7172
GREATER_EQUAL,
@@ -1473,63 +1474,73 @@ def simplify(self) -> LinearExpression:
14731474
"""
14741475

14751476
def _simplify_row(vars_row: np.ndarray, coeffs_row: np.ndarray) -> np.ndarray:
1476-
"""Simplify a single row by grouping vars and summing coefficients.
1477-
1477+
"""
1478+
Simplify a single row by grouping vars and summing coefficients.
1479+
14781480
Returns a 2D array of shape (2, input_len) where first row is vars, second is coeffs.
14791481
"""
14801482
input_len = len(vars_row)
1481-
1483+
14821484
# Filter out invalid entries
14831485
mask = (vars_row != -1) & (coeffs_row != 0) & ~np.isnan(coeffs_row)
14841486
valid_vars = vars_row[mask]
14851487
valid_coeffs = coeffs_row[mask]
14861488

14871489
if len(valid_vars) == 0:
14881490
# Return arrays filled with -1 and 0.0, same length as input
1489-
return np.vstack([
1490-
np.full(input_len, -1, dtype=float),
1491-
np.zeros(input_len, dtype=float)
1492-
])
1491+
return np.vstack(
1492+
[
1493+
np.full(input_len, -1, dtype=float),
1494+
np.zeros(input_len, dtype=float),
1495+
]
1496+
)
14931497

14941498
# Use bincount to sum coefficients for each variable ID efficiently
14951499
max_var = int(valid_vars.max())
1496-
summed = np.bincount(valid_vars, weights=valid_coeffs, minlength=max_var + 1)
1500+
summed = np.bincount(
1501+
valid_vars, weights=valid_coeffs, minlength=max_var + 1
1502+
)
14971503

14981504
# Get non-zero entries
14991505
unique_vars = np.where(summed != 0)[0]
15001506
unique_coeffs = summed[unique_vars]
1501-
1507+
15021508
# Pad to match input length
15031509
result_vars = np.full(input_len, -1, dtype=float)
15041510
result_coeffs = np.zeros(input_len, dtype=float)
1505-
1511+
15061512
n_unique = len(unique_vars)
15071513
result_vars[:n_unique] = unique_vars
15081514
result_coeffs[:n_unique] = unique_coeffs
15091515

15101516
return np.vstack([result_vars, result_coeffs])
15111517

1512-
# Stack vars and coeffs, apply simplification once, then unstack
1513-
combined = xr.apply_ufunc(
1518+
# Coeffs and vars have dimensions (.., TERM_DIM)
1519+
# A row-wise operation is applied over the .. dimensions on both coeffs and vars, which are stacked together over a new "CV_DIM" dimension
1520+
combined: xr.DataArray = xr.apply_ufunc(
15141521
_simplify_row,
15151522
self.vars,
15161523
self.coeffs,
15171524
input_core_dims=[[TERM_DIM], [TERM_DIM]],
1518-
output_core_dims=[["_field", TERM_DIM]],
1525+
output_core_dims=[[CV_DIM, TERM_DIM]],
15191526
vectorize=True,
15201527
)
1521-
1528+
# Combined has dimensions (.., CV_DIM, TERM_DIM)
1529+
1530+
# Drop terms where all vars are -1 (i.e., empty terms across all positions)
1531+
vars = combined.isel({CV_DIM: 0}).astype(int)
1532+
non_empty_terms = (vars != -1).any(dim=[d for d in vars.dims if d != TERM_DIM])
1533+
combined = combined.isel({TERM_DIM: non_empty_terms})
1534+
15221535
# Extract vars and coeffs from the combined result
1523-
vars_simplified = combined.isel(_field=0).astype(int)
1524-
coeffs_simplified = combined.isel(_field=1)
1536+
vars = combined.isel({CV_DIM: 0}).astype(int)
1537+
coeffs = combined.isel({CV_DIM: 1})
15251538

15261539
# Create new dataset with simplified data
15271540
new_data = self.data.copy()
1528-
new_data = assign_multiindex_safe(
1529-
new_data, vars=vars_simplified, coeffs=coeffs_simplified
1530-
)
1541+
new_data = assign_multiindex_safe(new_data, vars=vars, coeffs=coeffs)
15311542

1532-
return LinearExpression(new_data, self.model).densify_terms()
1543+
return LinearExpression(new_data, self.model)
15331544

15341545
@classmethod
15351546
def _from_scalarexpression_list(

test/test_linear_expression.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,23 +1195,29 @@ def test_cumsum(m: Model, multiple: float) -> None:
11951195

11961196
def test_simplify_basic(x: Variable) -> None:
11971197
"""Test basic simplification with duplicate terms."""
1198-
expr = 2 * x + 3 * x
1198+
expr = 2 * x + 3 * x + 1 * x
11991199
simplified = expr.simplify()
12001200
assert simplified.nterm == 1, f"Expected 1 term, got {simplified.nterm}"
12011201

1202+
x_len = len(x.coords["dim_0"])
12021203
# Check that the coefficient is 5
12031204
coeffs: np.ndarray = simplified.coeffs.values
1204-
assert len(coeffs) == 1, f"Expected 1 valid coefficient, got {len(coeffs)}"
1205-
assert all(coeffs == 5.0), f"Expected coefficient 5.0, got {coeffs[0]}"
1205+
assert len(coeffs) == x_len, f"Expected {x_len} coefficients, got {len(coeffs)}"
1206+
assert all(coeffs == 6.0), f"Expected coefficient 5.0, got {coeffs[0]}"
12061207

12071208

1208-
def test_simplify_array(x: Variable) -> None:
1209-
"""Test simplification with array variables."""
1210-
# Create expression with duplicate terms
1209+
def test_simplify_multiple_dimensions() -> None:
1210+
model = Model()
1211+
a_index = pd.Index([0, 1, 2, 3], name="a")
1212+
b_index = pd.Index([0, 1, 2], name="b")
1213+
coords = [a_index, b_index]
1214+
x = model.add_variables(name="x", coords=coords)
1215+
12111216
expr = 2 * x + 3 * x + x
12121217
# Simplify
12131218
simplified = expr.simplify()
12141219
assert simplified.nterm == 1, f"Expected 1 term, got {simplified.nterm}"
1220+
assert simplified.ndim == 2, f"Expected 2 dimensions, got {simplified.ndim}"
12151221
assert all(simplified.coeffs.values == 6), (
12161222
f"Expected coefficients of 6, got {simplified.coeffs.values}"
12171223
)
@@ -1227,9 +1233,7 @@ def test_simplify_with_different_variables(x: Variable, y: Variable) -> None:
12271233
# Should have 2 terms (one for x with coeff 5, one for y with coeff 4)
12281234
assert simplified.nterm == 2, f"Expected 2 terms, got {simplified.nterm}"
12291235

1230-
coeffs: np.ndarray = simplified.coeffs.values
1231-
assert len(coeffs) == 2, f"Expected 2 valid coefficients, got {len(coeffs)}"
1232-
# Check that coefficients are 5 and 4 (in some order)
1236+
coeffs: list[float] = simplified.coeffs.values.flatten().tolist()
12331237
assert set(coeffs) == {5.0, 4.0}, (
12341238
f"Expected coefficients {{5.0, 4.0}}, got {set(coeffs)}"
12351239
)

0 commit comments

Comments
 (0)