From e56083360e1ab90f68de237229ab52272a785e0f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Johannes=20P=2E=20D=C3=BCrholt?=
 <johannespeter.duerholt@evonik.com>
Date: Tue, 14 Jan 2025 08:29:39 +0100
Subject: [PATCH] fix strict candidate enforcement (#492)

---
 bofire/data_models/strategies/factorial.py           | 2 +-
 bofire/strategies/fractional_factorial.py            | 4 +++-
 bofire/strategies/shortest_path.py                   | 6 ++++--
 bofire/strategies/strategy.py                        | 6 ++++--
 tests/bofire/strategies/test_fractional_factorial.py | 7 ++++---
 tests/bofire/strategies/test_shortest_path.py        | 4 +++-
 tests/bofire/strategies/test_strategy.py             | 2 +-
 7 files changed, 20 insertions(+), 11 deletions(-)

diff --git a/bofire/data_models/strategies/factorial.py b/bofire/data_models/strategies/factorial.py
index e2934c087..537981e1c 100644
--- a/bofire/data_models/strategies/factorial.py
+++ b/bofire/data_models/strategies/factorial.py
@@ -20,7 +20,7 @@ class FactorialStrategy(Strategy):
     This strategy is deprecated, please use FractionalFactorialStrategy instead.
     """
 
-    type: Literal["FactorialStrategy"] = "FactorialStrategy"
+    type: Literal["FactorialStrategy"] = "FactorialStrategy"  # type: ignore
 
     @classmethod
     def is_constraint_implemented(cls, my_type: Type[Constraint]) -> bool:
diff --git a/bofire/strategies/fractional_factorial.py b/bofire/strategies/fractional_factorial.py
index 1b4097d5a..f3fc86f87 100644
--- a/bofire/strategies/fractional_factorial.py
+++ b/bofire/strategies/fractional_factorial.py
@@ -1,3 +1,4 @@
+import warnings
 from typing import Optional
 
 import numpy as np
@@ -58,10 +59,11 @@ def _get_categorical_design(self) -> pd.DataFrame:
 
     def _ask(self, candidate_count: Optional[int] = None) -> pd.DataFrame:
         if candidate_count is not None:
-            raise ValueError(
+            warnings.warn(
                 "FractionalFactorialStrategy will ignore the specified value of candidate_count. "
                 "The strategy automatically determines how many candidates to "
                 "propose.",
+                UserWarning,
             )
         design = None
         if len(self.domain.inputs.get(ContinuousInput)) > 0:
diff --git a/bofire/strategies/shortest_path.py b/bofire/strategies/shortest_path.py
index 68abdbe10..3b98ec15a 100644
--- a/bofire/strategies/shortest_path.py
+++ b/bofire/strategies/shortest_path.py
@@ -1,3 +1,4 @@
+import warnings
 from typing import Optional, Tuple
 
 import cvxpy as cp
@@ -127,10 +128,11 @@ def _ask(self, candidate_count: Optional[int] = None) -> pd.DataFrame:
 
         """
         if candidate_count is not None:
-            raise ValueError(
-                "ShortestPath will ignore the specified value of candidate_count. "
+            warnings.warn(
+                "ShortestPathStrategy will ignore the specified value of candidate_count. "
                 "The strategy automatically determines how many candidates to "
                 "propose.",
+                UserWarning,
             )
         start = self.start
         steps = []
diff --git a/bofire/strategies/strategy.py b/bofire/strategies/strategy.py
index 81c118697..951cf6379 100644
--- a/bofire/strategies/strategy.py
+++ b/bofire/strategies/strategy.py
@@ -1,3 +1,4 @@
+import warnings
 from abc import ABC, abstractmethod
 from typing import List, Optional
 
@@ -129,8 +130,9 @@ def ask(
 
         if candidate_count is not None:
             if len(candidates) != candidate_count:
-                raise ValueError(
-                    f"expected {candidate_count} candidates, got {len(candidates)}",
+                warnings.warn(
+                    f"Expected {candidate_count} candidates, got {len(candidates)}",
+                    UserWarning,
                 )
 
         if add_pending:
diff --git a/tests/bofire/strategies/test_fractional_factorial.py b/tests/bofire/strategies/test_fractional_factorial.py
index 6e18098ab..e5ce0bd0f 100644
--- a/tests/bofire/strategies/test_fractional_factorial.py
+++ b/tests/bofire/strategies/test_fractional_factorial.py
@@ -169,10 +169,11 @@ def test_FractionalFactorialStrategy_ask_invalid():
         ),
     )
     strategy = strategies.map(strategy_data)
-    with pytest.raises(
-        ValueError,
+    with pytest.warns(
+        UserWarning,
         match="FractionalFactorialStrategy will ignore the specified value of candidate_count. "
         "The strategy automatically determines how many candidates to "
         "propose.",
     ):
-        strategy.ask(5)
+        candidates = strategy.ask(7)
+    assert len(candidates) == 5
diff --git a/tests/bofire/strategies/test_shortest_path.py b/tests/bofire/strategies/test_shortest_path.py
index e65427e3b..7c7ebe83a 100644
--- a/tests/bofire/strategies/test_shortest_path.py
+++ b/tests/bofire/strategies/test_shortest_path.py
@@ -42,7 +42,9 @@ def test_step():
 def test_ask():
     data_model = specs.valid(data_models.ShortestPathStrategy).obj()
     strategy = strategies.map(data_model=data_model)
-    with pytest.raises(ValueError, match="ShortestPath will ignore the specified "):
+    with pytest.warns(
+        UserWarning, match="ShortestPathStrategy will ignore the specified "
+    ):
         strategy.ask(candidate_count=4)
     steps = strategy.ask()
     assert np.allclose(
diff --git a/tests/bofire/strategies/test_strategy.py b/tests/bofire/strategies/test_strategy.py
index 5314354ca..027320743 100644
--- a/tests/bofire/strategies/test_strategy.py
+++ b/tests/bofire/strategies/test_strategy.py
@@ -449,7 +449,7 @@ def test_ask(self: Strategy, candidate_count: int):
         return candidates
 
     with mock.patch.object(dummy.DummyStrategy, "_ask", new=test_ask):
-        with pytest.raises(ValueError):
+        with pytest.warns(UserWarning, match="Expected"):
             strategy.ask(candidate_count=4)