Skip to content

Commit 60e84c1

Browse files
committed
Address review: reset bound cursor in remove_unfinished, simplify test
- Reset _next_bound_idx in remove_unfinished() so discarded pending bound points can be asked again (previously they were silently retired and the domain corners never sampled). - Replace the unreachable 'raise StopIteration' with a RuntimeError; StopIteration would surface as a confusing PEP 479 RuntimeError from the generator in _ask_and_tell_pending anyway. - Drop the brittle message-matching try/except in _try_adding_pending_point_to_simplex; _is_known_point makes that path unreachable and its cleanup was incomplete. - Scale _is_known_point tolerance with coordinate magnitude instead of flooring at an absolute 1e-10, which was too coarse for tiny domains. - Simplify the regression test: derive the drifted hull vertex instead of magic constants, use pytest.importorskip, and remove the half that re-installed the old implementation to assert it fails. - Add a regression test for the remove_unfinished bound-point reset.
1 parent 202f0fb commit 60e84c1

2 files changed

Lines changed: 32 additions & 57 deletions

File tree

adaptive/learner/learnerND.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -613,8 +613,11 @@ def _is_known_point(self, point):
613613
if point in self.data or point in self.pending_points:
614614
return True
615615

616+
# Scale the tolerance with the coordinate magnitude so that float
617+
# round-trip drift (e.g. through a dataframe) is matched in domains
618+
# of any size.
616619
tolerances = [
617-
max(self._bound_match_tol, self._bound_match_tol * (hi - lo))
620+
self._bound_match_tol * max(abs(lo), abs(hi), hi - lo)
618621
for lo, hi in self._bbox
619622
]
620623

@@ -678,12 +681,7 @@ def _try_adding_pending_point_to_simplex(self, point, simplex):
678681
self._subtriangulations[simplex] = Triangulation(vertices)
679682

680683
self._pending_to_simplex[point] = simplex
681-
try:
682-
return self._subtriangulations[simplex].add_point(point)
683-
except ValueError as exc:
684-
if str(exc) == "Point already in triangulation.":
685-
self._pending_to_simplex.pop(point, None)
686-
raise
684+
return self._subtriangulations[simplex].add_point(point)
687685

688686
def _update_subsimplex_losses(self, simplex, new_subsimplices):
689687
loss = self._losses[simplex]
@@ -718,7 +716,9 @@ def _ask_bound_point(self):
718716
self.tell_pending(new_point)
719717
return new_point, np.inf
720718

721-
raise StopIteration
719+
# Unreachable: _ask only calls this method when _bounds_available,
720+
# which guarantees an unknown bound point at index >= _next_bound_idx.
721+
raise RuntimeError("No bound points available to ask.")
722722

723723
def _ask_point_without_known_simplices(self):
724724
assert not self._bounds_available
@@ -797,7 +797,7 @@ def _bounds_available(self):
797797

798798
def _ask(self):
799799
if self._bounds_available:
800-
return self._ask_bound_point() # O(1)
800+
return self._ask_bound_point() # O(N) worst case, amortized O(1)
801801

802802
if self.tri is None:
803803
# All bound points are pending or have been evaluated, but we do not
@@ -967,6 +967,9 @@ def remove_unfinished(self):
967967
self.pending_points = set()
968968
self._subtriangulations = {}
969969
self._pending_to_simplex = {}
970+
# Discarded pending points may include bound points that were already
971+
# consumed by _ask_bound_point; rescan them so they can be asked again.
972+
self._next_bound_idx = 0
970973

971974
##########################
972975
# Plotting related stuff #

adaptive/tests/unit/test_learnernd_integration.py

Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import math
22

3-
import numpy as np
43
import pytest
54
from scipy.spatial import ConvexHull
65

76
from adaptive.learner import LearnerND
8-
from adaptive.learner.learner1D import with_pandas
97
from adaptive.learner.learnerND import curvature_loss_function
108
from adaptive.runner import BlockingRunner
119
from adaptive.runner import simple as SimpleRunner
@@ -58,11 +56,9 @@ def test_learnerND_log_works():
5856
# furthermore the last two points that were asked should be in this simplex
5957

6058

61-
@pytest.mark.skipif(not with_pandas, reason="pandas is not installed")
62-
def test_learnerND_resume_after_loading_dataframe_convex_hull(monkeypatch):
63-
from types import MethodType
64-
65-
import pandas
59+
def test_learnerND_resume_after_loading_dataframe_convex_hull():
60+
# Regression test for https://github.com/python-adaptive/adaptive/issues/470
61+
pandas = pytest.importorskip("pandas")
6662

6763
hull_points = [
6864
(4.375872112626925, 8.917730007820797),
@@ -71,54 +67,19 @@ def test_learnerND_resume_after_loading_dataframe_convex_hull(monkeypatch):
7167
(9.636627605010293, 3.8344151882577773),
7268
]
7369

74-
data_points = [
75-
(4.375872112626925, 8.917730007820797),
76-
(4.236547993389047, 6.458941130666561),
77-
(6.027633760716439, 5.448831829968968),
78-
(9.636627605086398, 3.834415188269945),
79-
(0.7103605819788694, 0.8712929970154071),
80-
(0.2021839744032572, 8.32619845547938),
81-
(7.781567509498505, 8.700121482468191),
82-
]
70+
# Simulate float drift from a dataframe round-trip: one hull vertex is
71+
# off by 1e-10, so exact membership checks miss it and the learner used
72+
# to re-ask it, crashing with "Point already in triangulation.".
73+
drifted = tuple(c + 1e-10 for c in hull_points[-1])
74+
data_points = [*hull_points[:-1], drifted, (7.0, 6.0)]
8375

8476
df = pandas.DataFrame(data_points, columns=["x", "y"])
8577
df["value"] = df["x"] + df["y"]
8678

87-
hull = ConvexHull(hull_points)
88-
8979
def some_f(xy):
9080
return xy[0] + xy[1]
9181

92-
learner_old = LearnerND(some_f, hull)
93-
learner_old.load_dataframe(
94-
df,
95-
with_default_function_args=False,
96-
point_names=("x", "y"),
97-
value_name="value",
98-
)
99-
100-
def old_ask_bound_point(self):
101-
new_point = next(
102-
p
103-
for p in self._bounds_points
104-
if p not in self.data and p not in self.pending_points
105-
)
106-
self.tell_pending(new_point)
107-
return new_point, np.inf
108-
109-
learner_old._ask_bound_point = MethodType(old_ask_bound_point, learner_old)
110-
111-
def naive_is_known_point(self, point):
112-
point = tuple(map(float, point))
113-
return point in self.data or point in self.pending_points
114-
115-
learner_old._is_known_point = MethodType(naive_is_known_point, learner_old)
116-
learner_old._bound_match_tol = 0.0
117-
118-
with pytest.raises(ValueError):
119-
BlockingRunner(learner_old, npoints_goal=len(df) + 1)
120-
121-
learner = LearnerND(some_f, hull)
82+
learner = LearnerND(some_f, ConvexHull(hull_points))
12283
learner.load_dataframe(
12384
df,
12485
with_default_function_args=False,
@@ -129,3 +90,14 @@ def naive_is_known_point(self, point):
12990
target = len(df) + 1
13091
BlockingRunner(learner, npoints_goal=target)
13192
assert learner.npoints >= target
93+
94+
95+
def test_learnerND_remove_unfinished_reasks_bound_points():
96+
learner = LearnerND(ring_of_fire, bounds=[(-1, 1), (-1, 1)])
97+
points, _ = learner.ask(4)
98+
assert set(points) == set(learner._bounds_points)
99+
100+
# Discarding the pending bound points must make them available again.
101+
learner.remove_unfinished()
102+
points, _ = learner.ask(4)
103+
assert set(points) == set(learner._bounds_points)

0 commit comments

Comments
 (0)