Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions benchmarks/patterns/nodal_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
result's ``_term`` axis blows up — most of it fill. ``severity`` dials that
skew; the build's peak memory is expected to climb steeply with it on the
current (dense) kernel.

``nodal_balance_sparse`` builds the identical constraint through the
CSR-backed path (``sum(sparse=True)`` + ``freeze=True`` under v1): the
grouped sum never materializes the padded rectangle and is realized directly
as a CSRConstraint, so its peak memory should stay flat across the severity
sweep — the pair makes the padding cost visible.
"""

from __future__ import annotations
Expand All @@ -16,7 +22,14 @@
import xarray as xr

import linopy
from benchmarks.registry import SEVERITIES, BenchSpec, register_pattern
from benchmarks.registry import (
BUILD,
MATRICES,
SEVERITIES,
TO_LP,
BenchSpec,
register_pattern,
)

N_GEN = 2000
N_BUS = 50
Expand All @@ -43,7 +56,7 @@ def _bus_of_gen(severity: int) -> np.ndarray:
return bus


def build_nodal_balance(severity: int) -> linopy.Model:
def _build(severity: int, sparse: bool) -> linopy.Model:
gens = pd.RangeIndex(N_GEN, name="gen")
time = pd.RangeIndex(N_TIME, name="time")
buses = pd.RangeIndex(N_BUS, name="bus")
Expand All @@ -53,15 +66,29 @@ def build_nodal_balance(severity: int) -> linopy.Model:
gen = m.add_variables(lower=0, coords=[gens, time], name="gen")

bus_of_gen = pd.Series(_bus_of_gen(severity), index=gens, name="bus")
supply = (1 * gen).groupby(bus_of_gen).sum()
supply = (1 * gen).groupby(bus_of_gen).sum(sparse=sparse)
demand = xr.DataArray(
rng.uniform(10.0, 100.0, size=(N_BUS, N_TIME)), coords=[buses, time]
)
m.add_constraints(supply == demand, name="balance")
m.add_constraints(supply == demand, name="balance", freeze=sparse)
m.add_objective(gen.sum())
return m


def build_nodal_balance(severity: int) -> linopy.Model:
return _build(severity, sparse=False)


def build_nodal_balance_sparse(severity: int) -> linopy.Model:
"""The same balance via the sparse groupby + frozen CSR path (v1-only)."""
previous = linopy.options["semantics"]
linopy.options["semantics"] = "v1"
try:
return _build(severity, sparse=True)
finally:
linopy.options["semantics"] = previous


SPEC = register_pattern(
BenchSpec(
name="nodal_balance",
Expand All @@ -70,3 +97,13 @@ def build_nodal_balance(severity: int) -> linopy.Model:
axis="severity",
)
)

SPARSE_SPEC = register_pattern(
BenchSpec(
name="nodal_balance_sparse",
build=build_nodal_balance_sparse,
sweep=SEVERITIES,
axis="severity",
phases=frozenset({BUILD, MATRICES, TO_LP}),
)
)
1 change: 1 addition & 0 deletions linopy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,5 @@ def __repr__(self) -> str:
display_max_rows=14,
display_max_terms=6,
semantics=LEGACY_SEMANTICS,
sparse_groupby=False,
)
22 changes: 21 additions & 1 deletion linopy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,7 @@ class Constraint(ConstraintBase):
Supports setters, xarray operations via conwrap, and from_rule construction.
"""

__slots__ = ("_data", "_model", "_assigned", "_coef_dirty")
__slots__ = ("_data", "_model", "_assigned", "_coef_dirty", "_pending")

def __init__(
self,
Expand Down Expand Up @@ -1174,9 +1174,29 @@ def __init__(
self._data = data
self._model = model
self._coef_dirty = False
self._pending = None

@classmethod
def _from_pending(
cls, lhs: expressions.LinearExpression, sign: str, rhs: Any, model: Model
) -> Constraint:
"""Anonymous constraint over a still-sparse lhs (see linopy.csr)."""
obj = cls.__new__(cls)
obj._model = model
obj._data = None
obj._assigned = False
obj._coef_dirty = False
obj._pending = (lhs, sign, rhs)
return obj

@property
def data(self) -> Dataset:
if self._data is None and self._pending is not None:
lhs, sign, rhs = self._pending
lhs.data # noqa: B018
self._data = lhs.to_constraint(sign, rhs).data
self._assigned = "labels" in self._data
self._pending = None
return self._data

@property
Expand Down
Loading