Skip to content

Commit 62cfe34

Browse files
perf(matrices): cache MatrixAccessor properties with cached_property (#716)
Convert the remaining `@property` methods on `MatrixAccessor` (`c`, `Q`, `sol`, `dual`) to `@cached_property`. After #630, vlabels/clabels/A/b/sense/lb/ub are materialised once in `__init__`, but these four properties still recomputed on every access. Each `MatrixAccessor` is short-lived (rebuilt on every `model.matrices` call), so cache invalidation is moot: the instance never sees a mutated model. Also fix a latent double-build in `expressions.BaseExpression._map_solution`: `m.matrices.sol` followed by `m.matrices.vlabels` constructed two accessors (each running `_build_vars`+`_build_cons`). Bind once to `M`. Benchmark on a 360k-var / 1.2k-constraint model (Python 3.12, scipy 1.17, numpy 2.4): cold (first) warm (repeat) master: 1.0 ms 1.1 ms (recomputed) patch: 1.2 ms 0.4 us (~2500x on warm) Repeat access happens in `solvers.Highs._build_solver_model` (M.c / M.Q both read at lines 1278/1299 and again in Gurobi/Mosek paths at 1586-1587, 2860-2861) and anywhere a caller assigns `M = model.matrices` then reads multiple times.
1 parent d8d52b4 commit 62cfe34

2 files changed

Lines changed: 7 additions & 5 deletions

File tree

linopy/expressions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,8 @@ def _map_solution(self) -> DataArray:
969969
Replace variable labels by solution values.
970970
"""
971971
m = self.model
972-
sol = pd.Series(m.matrices.sol, m.matrices.vlabels)
972+
M = m.matrices
973+
sol = pd.Series(M.sol, M.vlabels)
973974
sol[-1] = np.nan
974975
idx = np.ravel(self.vars)
975976
values = np.asarray(sol[idx]).reshape(self.vars.shape)

linopy/matrices.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import annotations
99

10+
from functools import cached_property
1011
from typing import TYPE_CHECKING, cast
1112

1213
import numpy as np
@@ -96,7 +97,7 @@ def _build_cons(self) -> None:
9697
np.concatenate(sense_list) if sense_list else np.array([], dtype=object)
9798
)
9899

99-
@property
100+
@cached_property
100101
def c(self) -> ndarray:
101102
"""Objective coefficients aligned with vlabels."""
102103
m = self._parent
@@ -121,7 +122,7 @@ def c(self) -> ndarray:
121122
np.add.at(result, label_to_pos[var_labels[mask]], coeffs[mask])
122123
return result
123124

124-
@property
125+
@cached_property
125126
def Q(self) -> scipy.sparse.csc_matrix | None:
126127
"""Quadratic objective matrix, shape (n_active_vars, n_active_vars)."""
127128
m = self._parent
@@ -130,7 +131,7 @@ def Q(self) -> scipy.sparse.csc_matrix | None:
130131
return None
131132
return expr.to_matrix()[self.vlabels][:, self.vlabels]
132133

133-
@property
134+
@cached_property
134135
def sol(self) -> ndarray:
135136
"""Solution values aligned with vlabels."""
136137
if not self._parent.status == "ok":
@@ -146,7 +147,7 @@ def sol(self) -> ndarray:
146147
result[positions] = var.solution.values.ravel()[mask]
147148
return result
148149

149-
@property
150+
@cached_property
150151
def dual(self) -> ndarray:
151152
"""Dual values aligned with clabels."""
152153
if not self._parent.status == "ok":

0 commit comments

Comments
 (0)