Skip to content

Commit 13fd938

Browse files
committed
perf: cache MatrixAccessor properties to avoid redundant recomputation
Converts vlabels, vtypes, lb, ub, clabels, A, sense, b, c, Q from @Property to @cached_property, and extends clean_cached_properties() to clear all of them. Avoids recomputing expensive matrix operations (e.g. flat DataFrame flattening, sparse matrix slicing) on each access.
1 parent c415b4e commit 13fd938

2 files changed

Lines changed: 59 additions & 11 deletions

File tree

linopy/matrices.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,22 @@ def __init__(self, model: Model) -> None:
5151
def clean_cached_properties(self) -> None:
5252
"""Clear the cache for all cached properties of an object"""
5353

54-
for cached_prop in ["flat_vars", "flat_cons", "sol", "dual"]:
54+
for cached_prop in [
55+
"flat_vars",
56+
"flat_cons",
57+
"sol",
58+
"dual",
59+
"vlabels",
60+
"clabels",
61+
"A",
62+
"c",
63+
"b",
64+
"sense",
65+
"lb",
66+
"ub",
67+
"vtypes",
68+
"Q",
69+
]:
5570
# check existence of cached_prop without creating it
5671
if cached_prop in self.__dict__:
5772
delattr(self, cached_prop)
@@ -66,13 +81,13 @@ def flat_cons(self) -> pd.DataFrame:
6681
m = self._parent
6782
return m.constraints.flat
6883

69-
@property
84+
@cached_property
7085
def vlabels(self) -> ndarray:
7186
"""Vector of labels of all non-missing variables."""
7287
df: pd.DataFrame = self.flat_vars
7388
return create_vector(df.key, df.labels, -1)
7489

75-
@property
90+
@cached_property
7691
def vtypes(self) -> ndarray:
7792
"""Vector of types of all non-missing variables."""
7893
m = self._parent
@@ -93,7 +108,7 @@ def vtypes(self) -> ndarray:
93108
ds = df.set_index("key").labels.map(ds)
94109
return create_vector(ds.index, ds.to_numpy(), fill_value="")
95110

96-
@property
111+
@cached_property
97112
def lb(self) -> ndarray:
98113
"""Vector of lower bounds of all non-missing variables."""
99114
df: pd.DataFrame = self.flat_vars
@@ -123,21 +138,21 @@ def dual(self) -> ndarray:
123138
)
124139
return create_vector(df.key, df.dual, fill_value=np.nan)
125140

126-
@property
141+
@cached_property
127142
def ub(self) -> ndarray:
128143
"""Vector of upper bounds of all non-missing variables."""
129144
df: pd.DataFrame = self.flat_vars
130145
return create_vector(df.key, df.upper)
131146

132-
@property
147+
@cached_property
133148
def clabels(self) -> ndarray:
134149
"""Vector of labels of all non-missing constraints."""
135150
df: pd.DataFrame = self.flat_cons
136151
if df.empty:
137152
return np.array([], dtype=int)
138153
return create_vector(df.key, df.labels, fill_value=-1)
139154

140-
@property
155+
@cached_property
141156
def A(self) -> csc_matrix | None:
142157
"""Constraint matrix of all non-missing constraints and variables."""
143158
m = self._parent
@@ -146,19 +161,19 @@ def A(self) -> csc_matrix | None:
146161
A: csc_matrix = m.constraints.to_matrix(filter_missings=False)
147162
return A[self.clabels][:, self.vlabels]
148163

149-
@property
164+
@cached_property
150165
def sense(self) -> ndarray:
151166
"""Vector of senses of all non-missing constraints."""
152167
df: pd.DataFrame = self.flat_cons
153168
return create_vector(df.key, df.sign.astype(np.dtype("<U1")), fill_value="")
154169

155-
@property
170+
@cached_property
156171
def b(self) -> ndarray:
157172
"""Vector of right-hand-sides of all non-missing constraints."""
158173
df: pd.DataFrame = self.flat_cons
159174
return create_vector(df.key, df.rhs)
160175

161-
@property
176+
@cached_property
162177
def c(self) -> ndarray:
163178
"""Vector of objective coefficients of all non-missing variables."""
164179
m = self._parent
@@ -171,7 +186,7 @@ def c(self) -> ndarray:
171186
shape: int = self.flat_vars.key.max() + 1
172187
return create_vector(vars, ds.coeffs, fill_value=0.0, shape=shape)
173188

174-
@property
189+
@cached_property
175190
def Q(self) -> csc_matrix | None:
176191
"""Matrix objective coefficients of quadratic terms of all non-missing variables."""
177192
m = self._parent

test/test_matrices.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,36 @@ def test_matrices_float_c() -> None:
7777

7878
c = m.matrices.c
7979
assert np.all(c == np.array([1.5, 1.5]))
80+
81+
82+
def test_matrices_properties_are_cached() -> None:
83+
"""Verify that MatrixAccessor properties are cached after first access."""
84+
m = Model()
85+
86+
lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
87+
upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
88+
x = m.add_variables(lower, upper, name="x")
89+
y = m.add_variables(name="y")
90+
91+
m.add_constraints(1 * x + 10 * y, EQUAL, 0)
92+
m.add_objective((10 * x + 5 * y).sum())
93+
94+
M = m.matrices
95+
96+
# Access each property twice — second access should return the same object
97+
for prop in ("vlabels", "clabels", "lb", "ub", "b", "sense", "c"):
98+
first = getattr(M, prop)
99+
second = getattr(M, prop)
100+
assert first is second, f"{prop} is not cached (returns new object each time)"
101+
102+
# A and Q return complex objects — verify they are also cached
103+
first_A = M.A
104+
second_A = M.A
105+
assert first_A is second_A, "A is not cached"
106+
107+
# Verify clean_cached_properties clears the cache
108+
M.clean_cached_properties()
109+
fresh = M.vlabels
110+
assert fresh is not first # cache was cleared — should be a new object
111+
# After cleaning, accessing again should still work
112+
assert np.array_equal(fresh, M.vlabels)

0 commit comments

Comments
 (0)