From 884674a936ee7c3046c4b448ba3db2bc32ea15bc Mon Sep 17 00:00:00 2001
From: Bas Nijholt <basnijholt@gmail.com>
Date: Wed, 4 Sep 2019 17:43:33 +0200
Subject: [PATCH 1/5] fix problem when learner has no more points for
 BalancingLearner, closes #213

---
 adaptive/learner/balancing_learner.py | 28 +++++++++++++++++----------
 1 file changed, 18 insertions(+), 10 deletions(-)

diff --git a/adaptive/learner/balancing_learner.py b/adaptive/learner/balancing_learner.py
index d836bdbec..41672e7ad 100644
--- a/adaptive/learner/balancing_learner.py
+++ b/adaptive/learner/balancing_learner.py
@@ -119,20 +119,28 @@ def strategy(self, strategy):
                 ' strategy="npoints", or strategy="cycle" is implemented.'
             )
 
+    def _to_select(self, total_points):
+        to_select = []
+        for index, learner in enumerate(self.learners):
+            # Take the points from the cache
+            if index not in self._ask_cache:
+                self._ask_cache[index] = learner.ask(n=1, tell_pending=False)
+            points, loss_improvements = self._ask_cache[index]
+            if not points:
+                # cannot ask for more points
+                return to_select
+            to_select.append(
+                ((index, points[0]), (loss_improvements[0], -total_points[index]))
+            )
+        return to_select
+
     def _ask_and_tell_based_on_loss_improvements(self, n):
         selected = []  # tuples ((learner_index, point), loss_improvement)
         total_points = [l.npoints + len(l.pending_points) for l in self.learners]
         for _ in range(n):
-            to_select = []
-            for index, learner in enumerate(self.learners):
-                # Take the points from the cache
-                if index not in self._ask_cache:
-                    self._ask_cache[index] = learner.ask(n=1, tell_pending=False)
-                points, loss_improvements = self._ask_cache[index]
-                to_select.append(
-                    ((index, points[0]), (loss_improvements[0], -total_points[index]))
-                )
-
+            to_select = self._to_select(total_points)
+            if not to_select:
+                break
             # Choose the optimal improvement.
             (index, point), (loss_improvement, _) = max(to_select, key=itemgetter(1))
             total_points[index] += 1

From 2fedb39e4650365afb952dc8448c51bbef60c506 Mon Sep 17 00:00:00 2001
From: Bas Nijholt <basnijholt@gmail.com>
Date: Wed, 4 Sep 2019 17:49:45 +0200
Subject: [PATCH 2/5] fix for strategy="loss" and "npoints"

---
 adaptive/learner/balancing_learner.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/adaptive/learner/balancing_learner.py b/adaptive/learner/balancing_learner.py
index 41672e7ad..9898e8810 100644
--- a/adaptive/learner/balancing_learner.py
+++ b/adaptive/learner/balancing_learner.py
@@ -126,8 +126,7 @@ def _to_select(self, total_points):
             if index not in self._ask_cache:
                 self._ask_cache[index] = learner.ask(n=1, tell_pending=False)
             points, loss_improvements = self._ask_cache[index]
-            if not points:
-                # cannot ask for more points
+            if not points:  # cannot ask for more points
                 return to_select
             to_select.append(
                 ((index, points[0]), (loss_improvements[0], -total_points[index]))
@@ -139,7 +138,7 @@ def _ask_and_tell_based_on_loss_improvements(self, n):
         total_points = [l.npoints + len(l.pending_points) for l in self.learners]
         for _ in range(n):
             to_select = self._to_select(total_points)
-            if not to_select:
+            if not to_select:  # cannot ask for more points
                 break
             # Choose the optimal improvement.
             (index, point), (loss_improvement, _) = max(to_select, key=itemgetter(1))
@@ -164,7 +163,8 @@ def _ask_and_tell_based_on_loss(self, n):
             if index not in self._ask_cache:
                 self._ask_cache[index] = self.learners[index].ask(n=1)
             points, loss_improvements = self._ask_cache[index]
-
+            if not points:  # cannot ask for more points
+                break
             selected.append(((index, points[0]), loss_improvements[0]))
             self.tell_pending((index, points[0]))
 
@@ -180,6 +180,8 @@ def _ask_and_tell_based_on_npoints(self, n):
             if index not in self._ask_cache:
                 self._ask_cache[index] = self.learners[index].ask(n=1)
             points, loss_improvements = self._ask_cache[index]
+            if not points:  # cannot ask for more points
+                break
             total_points[index] += 1
             selected.append(((index, points[0]), loss_improvements[0]))
             self.tell_pending((index, points[0]))

From 091d8176a815d84052e6a238ea63bac278102b74 Mon Sep 17 00:00:00 2001
From: Bas Nijholt <basnijholt@gmail.com>
Date: Wed, 4 Sep 2019 17:58:20 +0200
Subject: [PATCH 3/5] add a failing test

---
 adaptive/tests/test_balancing_learner.py | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/adaptive/tests/test_balancing_learner.py b/adaptive/tests/test_balancing_learner.py
index fff0ff186..485cc0229 100644
--- a/adaptive/tests/test_balancing_learner.py
+++ b/adaptive/tests/test_balancing_learner.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from adaptive.learner import BalancingLearner, Learner1D
+from adaptive.learner import BalancingLearner, Learner1D, SequenceLearner
 from adaptive.runner import simple
 
 
@@ -44,6 +44,16 @@ def test_distribute_first_points_over_learners(strategy):
         assert len(set(i_learner)) == len(learners)
 
 
+@pytest.mark.parametrize("strategy", strategies)
+def test_asking_more_points_than_available(strategy):
+    def dummy(x):
+        return x
+
+    bl = BalancingLearner([SequenceLearner(dummy, range(5))], strategy=strategy)
+    bl.ask(100)
+    bl.ask(100)
+
+
 @pytest.mark.parametrize("strategy", strategies)
 def test_ask_0(strategy):
     learners = [Learner1D(lambda x: x, bounds=(-1, 1)) for i in range(10)]

From 8a798f7f8a6604cd715e5c4b05fd52a8daf26511 Mon Sep 17 00:00:00 2001
From: Bas Nijholt <basnijholt@gmail.com>
Date: Wed, 4 Sep 2019 17:59:13 +0200
Subject: [PATCH 4/5] make test_asking_more_points_than_available pass

---
 adaptive/learner/balancing_learner.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/adaptive/learner/balancing_learner.py b/adaptive/learner/balancing_learner.py
index 9898e8810..ad5cd124c 100644
--- a/adaptive/learner/balancing_learner.py
+++ b/adaptive/learner/balancing_learner.py
@@ -146,7 +146,7 @@ def _ask_and_tell_based_on_loss_improvements(self, n):
             selected.append(((index, point), loss_improvement))
             self.tell_pending((index, point))
 
-        points, loss_improvements = map(list, zip(*selected))
+        points, loss_improvements = map(list, zip(*selected)) if selected else [], []
         return points, loss_improvements
 
     def _ask_and_tell_based_on_loss(self, n):
@@ -168,7 +168,7 @@ def _ask_and_tell_based_on_loss(self, n):
             selected.append(((index, points[0]), loss_improvements[0]))
             self.tell_pending((index, points[0]))
 
-        points, loss_improvements = map(list, zip(*selected))
+        points, loss_improvements = map(list, zip(*selected)) if selected else [], []
         return points, loss_improvements
 
     def _ask_and_tell_based_on_npoints(self, n):
@@ -186,7 +186,7 @@ def _ask_and_tell_based_on_npoints(self, n):
             selected.append(((index, points[0]), loss_improvements[0]))
             self.tell_pending((index, points[0]))
 
-        points, loss_improvements = map(list, zip(*selected))
+        points, loss_improvements = map(list, zip(*selected)) if selected else [], []
         return points, loss_improvements
 
     def _ask_and_tell_based_on_cycle(self, n):

From 94645b3bb8cb356292d1fd27b68f549f6d4e31d3 Mon Sep 17 00:00:00 2001
From: Bas Nijholt <basnijholt@gmail.com>
Date: Wed, 4 Sep 2019 18:02:16 +0200
Subject: [PATCH 5/5] rename _to_select -> _ask_all_learners

---
 adaptive/learner/balancing_learner.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/adaptive/learner/balancing_learner.py b/adaptive/learner/balancing_learner.py
index ad5cd124c..a86932f04 100644
--- a/adaptive/learner/balancing_learner.py
+++ b/adaptive/learner/balancing_learner.py
@@ -119,7 +119,7 @@ def strategy(self, strategy):
                 ' strategy="npoints", or strategy="cycle" is implemented.'
             )
 
-    def _to_select(self, total_points):
+    def _ask_all_learners(self, total_points):
         to_select = []
         for index, learner in enumerate(self.learners):
             # Take the points from the cache
@@ -137,7 +137,7 @@ def _ask_and_tell_based_on_loss_improvements(self, n):
         selected = []  # tuples ((learner_index, point), loss_improvement)
         total_points = [l.npoints + len(l.pending_points) for l in self.learners]
         for _ in range(n):
-            to_select = self._to_select(total_points)
+            to_select = self._ask_all_learners(total_points)
             if not to_select:  # cannot ask for more points
                 break
             # Choose the optimal improvement.