Skip to content

Commit 12075a1

Browse files
committed
evaluate coeffs lazily
1 parent 1e3d26a commit 12075a1

3 files changed

Lines changed: 126 additions & 70 deletions

File tree

adaptive/learner/integrator_coeffs.py

Lines changed: 102 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -141,39 +141,105 @@ def calc_V(x, n):
141141
return np.array(V).T
142142

143143

144-
eps = np.spacing(1)
145-
146-
# the nodes and Newton polynomials
147-
ns = (5, 9, 17, 33)
148-
xi = [-np.cos(np.linspace(0, np.pi, n)) for n in ns]
149-
150-
# Make `xi` perfectly anti-symmetric, important for splitting the intervals
151-
xi = [(row - row[::-1]) / 2 for row in xi]
152-
153-
# Compute the Vandermonde-like matrix and its inverse.
154-
V = [calc_V(x, n) for x, n in zip(xi, ns)]
155-
V_inv = list(map(scipy.linalg.inv, V))
156-
Vcond = [scipy.linalg.norm(a, 2) * scipy.linalg.norm(b, 2) for a, b in zip(V, V_inv)]
157-
158-
# Compute the shift matrices.
159-
T_left, T_right = [V_inv[3] @ calc_V((xi[3] + a) / 2, ns[3]) for a in [-1, 1]]
160-
161-
# If the relative difference between two consecutive approximations is
162-
# lower than this value, the error estimate is considered reliable.
163-
# See section 6.2 of Pedro Gonnet's thesis.
164-
hint = 0.1
165-
166-
# Smallest acceptable relative difference of points in a rule. This was chosen
167-
# such that no artifacts are apparent in plots of (i, log(a_i)), where a_i is
168-
# the sequence of estimates of the integral value of an interval and all its
169-
# ancestors..
170-
min_sep = 16 * eps
171-
172-
ndiv_max = 20
173-
174-
# set-up the downdate matrix
175-
k = np.arange(ns[3])
176-
alpha = np.sqrt((k + 1) ** 2 / (2 * k + 1) / (2 * k + 3))
177-
gamma = np.concatenate([[0, 0], np.sqrt(k[2:] ** 2 / (4 * k[2:] ** 2 - 1))])
178-
179-
b_def = calc_bdef(ns)
144+
class Coefficients:
145+
def __init__(self) -> None:
146+
self.is_set = False
147+
# The nodes
148+
self.ns = (5, 9, 17, 33)
149+
self.eps = np.spacing(1)
150+
151+
# If the relative difference between two consecutive approximations is
152+
# lower than this value, the error estimate is considered reliable.
153+
# See section 6.2 of Pedro Gonnet's thesis.
154+
self.hint = 0.1
155+
156+
# Smallest acceptable relative difference of points in a rule. This was chosen
157+
# such that no artifacts are apparent in plots of (i, log(a_i)), where a_i is
158+
# the sequence of estimates of the integral value of an interval and all its
159+
# ancestors..
160+
self.min_sep = 16 * self.eps
161+
162+
# Maximum amount of subdivisions
163+
self.ndiv_max = 20
164+
165+
def set(self):
166+
self.is_set = True
167+
# The Newton polynomials
168+
xi = [-np.cos(np.linspace(0, np.pi, n)) for n in self.ns]
169+
# Make `xi` perfectly anti-symmetric, important for splitting the intervals
170+
self._xi = [(row - row[::-1]) / 2 for row in xi]
171+
172+
# Compute the Vandermonde-like matrix and its inverse.
173+
self._V = [calc_V(x, n) for x, n in zip(xi, self.ns)]
174+
self._V_inv = list(map(scipy.linalg.inv, self._V))
175+
self._Vcond = [
176+
scipy.linalg.norm(a, 2) * scipy.linalg.norm(b, 2)
177+
for a, b in zip(self._V, self._V_inv)
178+
]
179+
180+
# Compute the shift matrices.
181+
self._T_left, self._T_right = [
182+
self._V_inv[3] @ calc_V((xi[3] + a) / 2, self.ns[3]) for a in [-1, 1]
183+
]
184+
185+
# set-up the downdate matrix
186+
k = np.arange(self.ns[3])
187+
self._alpha = np.sqrt((k + 1) ** 2 / (2 * k + 1) / (2 * k + 3))
188+
self._gamma = np.concatenate(
189+
[[0, 0], np.sqrt(k[2:] ** 2 / (4 * k[2:] ** 2 - 1))]
190+
)
191+
self._b_def = calc_bdef(self.ns)
192+
193+
@property
194+
def xi(self):
195+
if not self.is_set:
196+
self.set()
197+
return self._xi
198+
199+
@property
200+
def V(self):
201+
if not self.is_set:
202+
self.set()
203+
return self._V
204+
205+
@property
206+
def V_inv(self):
207+
if not self.is_set:
208+
self.set()
209+
return self._V_inv
210+
211+
@property
212+
def Vcond(self):
213+
if not self.is_set:
214+
self.set()
215+
return self._Vcond
216+
217+
@property
218+
def T_right(self):
219+
if not self.is_set:
220+
self.set()
221+
return self._T_right
222+
223+
@property
224+
def T_left(self):
225+
if not self.is_set:
226+
self.set()
227+
return self._T_left
228+
229+
@property
230+
def alpha(self):
231+
if not self.is_set:
232+
self.set()
233+
return self._alpha
234+
235+
@property
236+
def gamma(self):
237+
if not self.is_set:
238+
self.set()
239+
return self._gamma
240+
241+
@property
242+
def b_def(self):
243+
if not self.is_set:
244+
self.set()
245+
return self._b_def

adaptive/learner/integrator_learner.py

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,21 @@
1414
from adaptive.notebook_integration import ensure_holoviews
1515
from adaptive.utils import cache_latest, restore
1616

17-
from .integrator_coeffs import (
18-
T_left,
19-
T_right,
20-
V_inv,
21-
Vcond,
22-
alpha,
23-
b_def,
24-
eps,
25-
gamma,
26-
hint,
27-
min_sep,
28-
ndiv_max,
29-
ns,
30-
xi,
31-
)
17+
from .integrator_coeffs import Coefficients
18+
19+
c = Coefficients()
3220

3321

3422
def _downdate(c, nans, depth):
3523
# This is algorithm 5 from the thesis of Pedro Gonnet.
36-
b = b_def[depth].copy()
37-
m = ns[depth] - 1
24+
b = c.b_def[depth].copy()
25+
m = c.ns[depth] - 1
3826
for i in nans:
39-
b[m + 1] /= alpha[m]
40-
xii = xi[depth][i]
41-
b[m] = (b[m] + xii * b[m + 1]) / alpha[m - 1]
27+
b[m + 1] /= c.alpha[m]
28+
xii = c.xi[depth][i]
29+
b[m] = (b[m] + xii * b[m + 1]) / c.alpha[m - 1]
4230
for j in range(m - 1, 0, -1):
43-
b[j] = (b[j] + xii * b[j + 1] - gamma[j + 1] * b[j + 2]) / alpha[j - 1]
31+
b[j] = (b[j] + xii * b[j + 1] - c.gamma[j + 1] * b[j + 2]) / c.alpha[j - 1]
4432
b = b[1:]
4533

4634
c[:m] -= c[m] / b[m] * b[:m]
@@ -62,7 +50,7 @@ def _zero_nans(fx):
6250
def _calc_coeffs(fx, depth):
6351
"""Caution: this function modifies fx."""
6452
nans = _zero_nans(fx)
65-
c_new = V_inv[depth] @ fx
53+
c_new = c.V_inv[depth] @ fx
6654
if nans:
6755
fx[nans] = np.nan
6856
c_new = _downdate(c_new, nans, depth)
@@ -168,11 +156,11 @@ def T(self):
168156
left = self.a == self.parent.a
169157
right = self.b == self.parent.b
170158
assert left != right
171-
return T_left if left else T_right
159+
return c.T_left if left else c.T_right
172160

173161
def refinement_complete(self, depth):
174162
"""The interval has all the y-values to calculate the intergral."""
175-
if len(self.data) < ns[depth]:
163+
if len(self.data) < c.ns[depth]:
176164
return False
177165
return all(p in self.data for p in self.points(depth))
178166

@@ -181,7 +169,7 @@ def points(self, depth=None):
181169
depth = self.depth
182170
a = self.a
183171
b = self.b
184-
return (a + b) / 2 + (b - a) * xi[depth] / 2
172+
return (a + b) / 2 + (b - a) * c.xi[depth] / 2
185173

186174
def refine(self):
187175
self.depth += 1
@@ -234,7 +222,7 @@ def calc_ndiv(self):
234222
div = self.parent.c00 and self.c00 / self.parent.c00 > 2
235223
self.ndiv += div
236224

237-
if self.ndiv > ndiv_max and 2 * self.ndiv > self.rdepth:
225+
if self.ndiv > c.ndiv_max and 2 * self.ndiv > self.rdepth:
238226
raise DivergentIntegralError
239227

240228
if div:
@@ -243,7 +231,7 @@ def calc_ndiv(self):
243231

244232
def update_ndiv_recursively(self):
245233
self.ndiv += 1
246-
if self.ndiv > ndiv_max and 2 * self.ndiv > self.rdepth:
234+
if self.ndiv > c.ndiv_max and 2 * self.ndiv > self.rdepth:
247235
raise DivergentIntegralError
248236

249237
for child in self.children:
@@ -276,21 +264,21 @@ def complete_process(self, depth):
276264
if depth:
277265
# Refine
278266
c_diff = self.calc_err(c_old)
279-
force_split = c_diff > hint * norm(self.c)
267+
force_split = c_diff > c.hint * norm(self.c)
280268
else:
281269
# Split
282270
self.c00 = self.c[0]
283271

284272
if self.parent.depth_complete is not None:
285-
c_old = self.T[:, : ns[self.parent.depth_complete]] @ self.parent.c
273+
c_old = self.T[:, : c.ns[self.parent.depth_complete]] @ self.parent.c
286274
self.calc_err(c_old)
287275
self.calc_ndiv()
288276

289277
for child in self.children:
290278
if child.depth_complete is not None:
291279
child.calc_ndiv()
292280
if child.depth_complete == 0:
293-
c_old = child.T[:, : ns[self.depth_complete]] @ self.c
281+
c_old = child.T[:, : c.ns[self.depth_complete]] @ self.c
294282
child.calc_err(c_old)
295283

296284
if self.done_leaves is not None and not len(self.done_leaves):
@@ -320,7 +308,7 @@ def complete_process(self, depth):
320308
ival.done_leaves -= old_leaves
321309
ival = ival.parent
322310

323-
remove = self.err < (abs(self.igral) * eps * Vcond[depth])
311+
remove = self.err < (abs(self.igral) * c.eps * c.Vcond[depth])
324312

325313
return force_split, remove
326314

@@ -496,8 +484,8 @@ def _fill_stack(self):
496484
points = ival.points()
497485

498486
if (
499-
points[1] - points[0] < points[0] * min_sep
500-
or points[-1] - points[-2] < points[-2] * min_sep
487+
points[1] - points[0] < points[0] * c.min_sep
488+
or points[-1] - points[-2] < points[-2] * c.min_sep
501489
):
502490
self.ivals.remove(ival)
503491
elif ival.depth == 3 or force_split:

adaptive/tests/test_cquad.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
import pytest
66

77
from adaptive.learner import IntegratorLearner
8-
from adaptive.learner.integrator_coeffs import ns
8+
from adaptive.learner.integrator_coeffs import Coefficients
99
from adaptive.learner.integrator_learner import DivergentIntegralError
1010

1111
from .algorithm_4 import DivergentIntegralError as A4DivergentIntegralError
1212
from .algorithm_4 import algorithm_4, f0, f7, f21, f24, f63, fdiv
1313

1414
eps = np.spacing(1)
1515

16+
ns = Coefficients().ns
17+
1618

1719
def run_integrator_learner(f, a, b, tol, n):
1820
learner = IntegratorLearner(f, bounds=(a, b), tol=tol)

0 commit comments

Comments
 (0)