Skip to content

Commit 1a64676

Browse files
authored
Merge pull request #216 from python-adaptive/data_on_grid
2D: add interpolated_on_grid method
2 parents 6914a78 + 6794a06 commit 1a64676

File tree

3 files changed

+127
-53
lines changed

3 files changed

+127
-53
lines changed

adaptive/learner/learner2D.py

Lines changed: 123 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
import itertools
4+
import warnings
45
from collections import OrderedDict
56
from copy import copy
67
from math import sqrt
@@ -26,7 +27,7 @@ def deviations(ip):
2627
2728
Returns
2829
-------
29-
numpy array
30+
deviations : list
3031
The deviation per triangle.
3132
"""
3233
values = ip.values / (ip.values.ptp(axis=0).max() or 1)
@@ -64,7 +65,7 @@ def areas(ip):
6465
6566
Returns
6667
-------
67-
numpy array
68+
areas : numpy.ndarray
6869
The area per triangle in ``ip.tri``.
6970
"""
7071
p = ip.tri.points[ip.tri.vertices]
@@ -78,16 +79,27 @@ def uniform_loss(ip):
7879
7980
Works with `~adaptive.Learner2D` only.
8081
82+
Parameters
83+
----------
84+
ip : `scipy.interpolate.LinearNDInterpolator` instance
85+
86+
Returns
87+
-------
88+
losses : numpy.ndarray
89+
Loss per triangle in ``ip.tri``.
90+
8191
Examples
8292
--------
8393
>>> from adaptive.learner.learner2D import uniform_loss
8494
>>> def f(xy):
8595
... x, y = xy
8696
... return x**2 + y**2
8797
>>>
88-
>>> learner = adaptive.Learner2D(f,
89-
... bounds=[(-1, -1), (1, 1)],
90-
... loss_per_triangle=uniform_loss)
98+
>>> learner = adaptive.Learner2D(
99+
... f,
100+
... bounds=[(-1, -1), (1, 1)],
101+
... loss_per_triangle=uniform_loss,
102+
... )
91103
>>>
92104
"""
93105
return np.sqrt(areas(ip))
@@ -102,17 +114,18 @@ def resolution_loss_function(min_distance=0, max_distance=1):
102114
The arguments `min_distance` and `max_distance` should be in between 0 and 1
103115
because the total area is normalized to 1.
104116
117+
Returns
118+
-------
119+
loss_function : callable
120+
105121
Examples
106122
--------
107123
>>> def f(xy):
108124
... x, y = xy
109125
... return x**2 + y**2
110126
>>>
111127
>>> loss = resolution_loss_function(min_distance=0.01, max_distance=1)
112-
>>> learner = adaptive.Learner2D(f,
113-
... bounds=[(-1, -1), (1, 1)],
114-
... loss_per_triangle=loss)
115-
>>>
128+
>>> learner = adaptive.Learner2D(f, bounds=[(-1, -1), (1, 1)], loss_per_triangle=loss)
116129
"""
117130

118131
def resolution_loss(ip):
@@ -132,12 +145,21 @@ def resolution_loss(ip):
132145

133146

134147
def minimize_triangle_surface_loss(ip):
135-
"""Loss function that is similar to the default loss function in the
148+
"""Loss function that is similar to the distance loss function in the
136149
`~adaptive.Learner1D`. The loss is the area spanned by the 3D
137150
vectors of the vertices.
138151
139152
Works with `~adaptive.Learner2D` only.
140153
154+
Parameters
155+
----------
156+
ip : `scipy.interpolate.LinearNDInterpolator` instance
157+
158+
Returns
159+
-------
160+
losses : numpy.ndarray
161+
Loss per triangle in ``ip.tri``.
162+
141163
Examples
142164
--------
143165
>>> from adaptive.learner.learner2D import minimize_triangle_surface_loss
@@ -169,6 +191,19 @@ def _get_vectors(points):
169191

170192

171193
def default_loss(ip):
194+
"""Loss function that combines `deviations` and `areas` of the triangles.
195+
196+
Works with `~adaptive.Learner2D` only.
197+
198+
Parameters
199+
----------
200+
ip : `scipy.interpolate.LinearNDInterpolator` instance
201+
202+
Returns
203+
-------
204+
losses : numpy.ndarray
205+
Loss per triangle in ``ip.tri``.
206+
"""
172207
dev = np.sum(deviations(ip), axis=0)
173208
A = areas(ip)
174209
losses = dev * np.sqrt(A) + 0.3 * A
@@ -186,15 +221,15 @@ def choose_point_in_triangle(triangle, max_badness):
186221
187222
Parameters
188223
----------
189-
triangle : numpy array
190-
The coordinates of a triangle with shape (3, 2)
224+
triangle : numpy.ndarray
225+
The coordinates of a triangle with shape (3, 2).
191226
max_badness : int
192227
The badness at which the point is either chosen on a edge or
193228
in the middle.
194229
195230
Returns
196231
-------
197-
point : numpy array
232+
point : numpy.ndarray
198233
The x and y coordinate of the suggested new point.
199234
"""
200235
a, b, c = triangle
@@ -230,7 +265,6 @@ class Learner2D(BaseLearner):
230265
triangle area, to determine the loss. See the notes
231266
for more details.
232267
233-
234268
Attributes
235269
----------
236270
data : dict
@@ -248,12 +282,6 @@ class Learner2D(BaseLearner):
248282
triangles will be stretched along ``x``, otherwise
249283
along ``y``.
250284
251-
Methods
252-
-------
253-
data_combined : dict
254-
Sampled points and values so far including
255-
the unknown interpolated points in `pending_points`.
256-
257285
Notes
258286
-----
259287
Adapted from an initial implementation by Pauli Virtanen.
@@ -345,6 +373,38 @@ def bounds_are_done(self):
345373
(p in self.pending_points or p in self._stack) for p in self._bounds_points
346374
)
347375

376+
def interpolated_on_grid(self, n=None):
377+
"""Get the interpolated data on a grid.
378+
379+
Parameters
380+
----------
381+
n : int, optional
382+
Number of points in x and y. If None (default) this number is
383+
evaluated by looking at the size of the smallest triangle.
384+
385+
Returns
386+
-------
387+
xs : 1D numpy.ndarray
388+
ys : 1D numpy.ndarray
389+
interpolated_on_grid : 2D numpy.ndarray
390+
"""
391+
ip = self.interpolator(scaled=True)
392+
if n is None:
393+
# Calculate how many grid points are needed.
394+
# factor from A=√3/4 * a² (equilateral triangle)
395+
n = int(0.658 / sqrt(areas(ip).min()))
396+
n = max(n, 10)
397+
398+
# The bounds of the linspace should be (-0.5, 0.5) but because of
399+
# numerical precision problems it could (for example) be
400+
# (-0.5000000000000001, 0.49999999999999983), then any point at exact
401+
# boundary would be outside of the domain. See #181.
402+
eps = 1e-13
403+
xs = ys = np.linspace(-0.5 + eps, 0.5 - eps, n)
404+
zs = ip(xs[:, None], ys[None, :] * self.aspect_ratio).squeeze()
405+
xs, ys = self._unscale(np.vstack([xs, ys]).T).T
406+
return xs, ys, zs
407+
348408
def _data_in_bounds(self):
349409
if self.data:
350410
points = np.array(list(self.data.keys()))
@@ -358,7 +418,8 @@ def _data_interp(self):
358418
if self.pending_points:
359419
points = list(self.pending_points)
360420
if self.bounds_are_done:
361-
values = self.ip()(self._scale(points))
421+
ip = self.interpolator(scaled=True)
422+
values = ip(self._scale(points))
362423
else:
363424
# Without the bounds the interpolation cannot be done properly,
364425
# so we just set everything to zero.
@@ -375,23 +436,47 @@ def _data_combined(self):
375436
values_combined = np.vstack([values, values_interp])
376437
return points_combined, values_combined
377438

378-
def data_combined(self):
379-
"""Like `data`, however this includes the points in
380-
`pending_points` for which the values are interpolated."""
381-
# Interpolate the unfinished points
382-
points, values = self._data_combined()
383-
return {tuple(k): v for k, v in zip(points, values)}
384-
385439
def ip(self):
440+
"""Deprecated, use `self.interpolator(scaled=True)`"""
441+
warnings.warn(
442+
"`learner.ip()` is deprecated, use `learner.interpolator(scaled=True)`."
443+
" This will be removed in v1.0.",
444+
DeprecationWarning,
445+
)
446+
return self.interpolator(scaled=True)
447+
448+
def interpolator(self, *, scaled=False):
386449
"""A `scipy.interpolate.LinearNDInterpolator` instance
387-
containing the learner's data."""
388-
if self._ip is None:
450+
containing the learner's data.
451+
452+
Parameters
453+
----------
454+
scaled : bool
455+
Use True if all points are inside the
456+
unit-square [(-0.5, 0.5), (-0.5, 0.5)] or False if
457+
the data points are inside the ``learner.bounds``.
458+
459+
Returns
460+
-------
461+
interpolator : `scipy.interpolate.LinearNDInterpolator`
462+
463+
Examples
464+
--------
465+
>>> xs, ys = [np.linspace(*b, num=100) for b in learner.bounds]
466+
>>> ip = learner.interpolator()
467+
>>> zs = ip(xs[:, None], ys[None, :])
468+
"""
469+
if scaled:
470+
if self._ip is None:
471+
points, values = self._data_in_bounds()
472+
points = self._scale(points)
473+
self._ip = interpolate.LinearNDInterpolator(points, values)
474+
return self._ip
475+
else:
389476
points, values = self._data_in_bounds()
390-
points = self._scale(points)
391-
self._ip = interpolate.LinearNDInterpolator(points, values)
392-
return self._ip
477+
return interpolate.LinearNDInterpolator(points, values)
393478

394-
def ip_combined(self):
479+
def _interpolator_combined(self):
395480
"""A `scipy.interpolate.LinearNDInterpolator` instance
396481
containing the learner's data *and* interpolated data of
397482
the `pending_points`."""
@@ -428,7 +513,7 @@ def _fill_stack(self, stack_till=1):
428513
raise ValueError("too few points...")
429514

430515
# Interpolate
431-
ip = self.ip_combined()
516+
ip = self._interpolator_combined()
432517

433518
losses = self.loss_per_triangle(ip)
434519

@@ -496,7 +581,7 @@ def ask(self, n, tell_pending=True):
496581
def loss(self, real=True):
497582
if not self.bounds_are_done:
498583
return np.inf
499-
ip = self.ip() if real else self.ip_combined()
584+
ip = self.interpolator(scaled=True) if real else self._interpolator_combined()
500585
losses = self.loss_per_triangle(ip)
501586
return losses.max()
502587

@@ -541,21 +626,8 @@ def plot(self, n=None, tri_alpha=0):
541626
lbrt = x[0], y[0], x[1], y[1]
542627

543628
if len(self.data) >= 4:
544-
ip = self.ip()
545-
546-
if n is None:
547-
# Calculate how many grid points are needed.
548-
# factor from A=√3/4 * a² (equilateral triangle)
549-
n = int(0.658 / sqrt(areas(ip).min()))
550-
n = max(n, 10)
551-
552-
# The bounds of the linspace should be (-0.5, 0.5) but because of
553-
# numerical precision problems it could (for example) be
554-
# (-0.5000000000000001, 0.49999999999999983), then any point at exact
555-
# boundary would be outside of the domain. See #181.
556-
eps = 1e-13
557-
x = y = np.linspace(-0.5 + eps, 0.5 - eps, n)
558-
z = ip(x[:, None], y[None, :] * self.aspect_ratio).squeeze()
629+
ip = self.interpolator(scaled=True)
630+
x, y, z = self.interpolated_on_grid(n)
559631

560632
if self.vdim > 1:
561633
ims = {

adaptive/tests/test_learners.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,9 @@ def test_expected_loss_improvement_is_less_than_total_loss(
400400
_, loss_improvements = learner.ask(M)
401401

402402
if learner_type is Learner2D:
403-
assert sum(loss_improvements) < sum(learner.loss_per_triangle(learner.ip()))
403+
assert sum(loss_improvements) < sum(
404+
learner.loss_per_triangle(learner.interpolator(scaled=True))
405+
)
404406
elif learner_type is Learner1D:
405407
assert sum(loss_improvements) < sum(learner.losses.values())
406408
elif learner_type is AverageLearner:

docs/logo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def ring(xy):
2828

2929
def plot_learner_and_save(learner, fname):
3030
fig, ax = plt.subplots()
31-
tri = learner.ip().tri
31+
tri = learner.interpolator(scaled=True).tri
3232
triang = mtri.Triangulation(*tri.points.T, triangles=tri.vertices)
3333
ax.triplot(triang, c="k", lw=0.8)
3434
ax.imshow(learner.plot().Image.I.data, extent=(-0.5, 0.5, -0.5, 0.5))

0 commit comments

Comments
 (0)