diff --git a/flaml/tune/searcher/flow2.py b/flaml/tune/searcher/flow2.py index 7ccf6fa6bb..371b503273 100644 --- a/flaml/tune/searcher/flow2.py +++ b/flaml/tune/searcher/flow2.py @@ -674,5 +674,8 @@ def reach(self, other: Searcher) -> bool: # unordered cat choice is hard to reach by chance if config1[key] != config2.get(key): return False - delta = np.array([incumbent1[key] - incumbent2.get(key, np.inf) for key in self._tunable_keys]) + try: + delta = np.array([incumbent1[key] - incumbent2.get(key, np.inf) for key in self._tunable_keys]) + except TypeError: + return False return np.linalg.norm(delta) <= self.step diff --git a/test/tune/test_searcher.py b/test/tune/test_searcher.py index e49c1aa5c1..de33e545cf 100644 --- a/test/tune/test_searcher.py +++ b/test/tune/test_searcher.py @@ -345,5 +345,27 @@ def test_unresolved_search_space(caplog): ), "BlendSearch should not produce warning about unresolved search space" +def test_flow2_reach_mixed_type_incumbents(): + """Regression test for #903. + + FLOW2.reach() must return False instead of raising TypeError when conditional choice parameters lead to mixed-type incumbents. + """ + from flaml.tune.searcher.flow2 import FLOW2 + + f1 = FLOW2.__new__(FLOW2) + f1.best_config = {"x": 1.0} + f1.incumbent = {"x": 1.0} + f1._resource = None + f1._unordered_cat_hp = {} + f1._tunable_keys = ["x"] + f1.step = 0.5 + + f2 = FLOW2.__new__(FLOW2) + f2.best_config = {"x": "None"} + f2.incumbent = {"x": "None"} + + assert f1.reach(f2) is False + + if __name__ == "__main__": test_unresolved_search_space(None)