From a98d1bf89c61c8bdf9de463b0f52daf274f4a142 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Thu, 19 Feb 2026 14:28:14 +0100 Subject: [PATCH 01/13] refactor method for readability --- src/ConfigSpace/util.py | 92 ++++++++++++----------------------------- 1 file changed, 27 insertions(+), 65 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 42008059..17ffa25a 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -28,6 +28,7 @@ from __future__ import annotations import copy +import itertools from collections import deque from collections.abc import Iterator, Sequence from typing import TYPE_CHECKING, Any, cast @@ -671,85 +672,46 @@ def generate_grid( is the same for the OrderedDict within the ConfigurationSpace. """ - def _get_value_set(num_steps_dict: dict[str, int] | None, hp_name: str) -> tuple: - param = configuration_space[hp_name] - if isinstance(param, (CategoricalHyperparameter)): - return cast(tuple, param.choices) - - if isinstance(param, (OrdinalHyperparameter)): - return cast(tuple, param.sequence) - - if isinstance(param, Constant): - return (param.value,) - - if isinstance(param, UniformFloatHyperparameter): - if param.log: - lower, upper = np.log([param.lower, param.upper]) - else: - lower, upper = param.lower, param.upper - - if num_steps_dict is not None and param.name in num_steps_dict: - num_steps = num_steps_dict[param.name] - grid_points = np.linspace(lower, upper, num_steps) - else: + def _get_value_set(num_steps_dict: dict[str, int] | None, hp: Hyperparameter) -> tuple: + if isinstance(hp, (CategoricalHyperparameter)): + return cast(tuple, hp.choices) + elif isinstance(hp, (OrdinalHyperparameter)): + return cast(tuple, hp.sequence) + elif isinstance(hp, Constant): + return (hp.value,) + elif isinstance(hp, (UniformFloatHyperparameter, UniformIntegerHyperparameter)): + if not num_steps_dict or hp.name not in num_steps_dict: raise ValueError( "num_steps_dict is None or doesn't contain the number of points" - f" to divide {param.name} into. And its quantization factor " + f" to divide {hp.name} into. And its quantization factor " "is None. Please provide/set one of these values.", ) - - if param.log: - grid_points = np.exp(grid_points) - - # Avoiding rounding off issues - grid_points[0] = max(grid_points[0], param.lower) - grid_points[-1] = min(grid_points[-1], param.upper) - - return tuple(grid_points) - - if isinstance(param, UniformIntegerHyperparameter): - if param.log: - lower, upper = np.log([param.lower, param.upper]) + num_steps = num_steps_dict[hp.name] + if hp.log: + lower, upper = np.log([hp.lower, hp.upper]) + grid_points = np.exp(np.linspace(lower, upper, num_steps)) else: - lower, upper = param.lower, param.upper - - if num_steps_dict is not None and param.name in num_steps_dict: - num_steps = num_steps_dict[param.name] + lower, upper = hp.lower, hp.upper grid_points = np.linspace(lower, upper, num_steps) - else: - raise ValueError( - "num_steps_dict is None or doesn't contain the number of points " - f"to divide {param.name} into. And its quantization factor " - "is None. Please provide/set one of these values.", - ) - - if param.log: - grid_points = np.exp(grid_points) - grid_points = np.round(grid_points).astype(int) + if isinstance(hp, UniformIntegerHyperparameter): + grid_points = np.round(grid_points).astype(int) # Avoiding rounding off issues - grid_points[0] = max(grid_points[0], param.lower) - grid_points[-1] = min(grid_points[-1], param.upper) - + grid_points[0] = max(grid_points[0], hp.lower) + grid_points[-1] = min(grid_points[-1], hp.upper) return tuple(grid_points) - raise TypeError(f"Unknown hyperparameter type {type(param)}") + raise TypeError(f"Unknown hyperparameter type {type(hp)}") def _get_cartesian_product( value_sets: list[tuple], hp_names: list[str], ) -> list[dict[str, Any]]: - import itertools - - if len(value_sets) == 0: - # Edge case - return [] - grid = [] - for element in itertools.product(*value_sets): - config_dict = dict(zip(hp_names, element)) - grid.append(config_dict) - + if len(value_sets) > 0: # Edge case for empty value set + for element in itertools.product(*value_sets): + config_dict = dict(zip(hp_names, element)) + grid.append(config_dict) return grid # Each tuple within is the grid values to be taken on by a Hyperparameter @@ -759,7 +721,7 @@ def _get_cartesian_product( # Get HP names and allowed grid values they can take for the HPs at the top # level of ConfigSpace tree for hp_name in configuration_space.unconditional_hyperparameters: - value_sets.append(_get_value_set(num_steps_dict, hp_name)) + value_sets.append(_get_value_set(num_steps_dict, configuration_space[hp_name])) hp_names.append(hp_name) # Create a Cartesian product of above allowed values for the HPs. Hold them in an @@ -810,7 +772,7 @@ def _get_cartesian_product( new_active_hp_names.append(new_hp_name) for hp_name in new_active_hp_names: - value_sets.append(_get_value_set(num_steps_dict, hp_name)) + value_sets.append(_get_value_set(num_steps_dict, configuration_space[hp_name])) hp_names.append(hp_name) # this check might not be needed, as there is always going to be a new From 3f295463e1a6f9d0342eeea100d253d3cc648a55 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 20 Feb 2026 14:18:15 +0100 Subject: [PATCH 02/13] First version reworking grid generator as actual generator --- src/ConfigSpace/util.py | 242 +++++++++++++++++++++++++++------------- test/test_util.py | 71 +++++++----- 2 files changed, 212 insertions(+), 101 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 17ffa25a..c774096c 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -29,9 +29,10 @@ import copy import itertools +import math from collections import deque from collections.abc import Iterator, Sequence -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, cast, Generator import numpy as np @@ -39,6 +40,7 @@ from ConfigSpace.exceptions import ( ActiveHyperparameterNotSetError, ForbiddenValueError, + IllegalValueError, IllegalVectorizedValueError, InactiveHyperparameterSetError, NoPossibleNeighborsError, @@ -645,10 +647,10 @@ def change_hp_value( # noqa: D103 return arr -def generate_grid( +def grid_generator( configuration_space: ConfigurationSpace, num_steps_dict: dict[str, int] | None = None, -) -> list[Configuration]: +) -> Generator[Configuration, None, None]: """Generates a grid of Configurations for a given ConfigurationSpace. Can be used, for example, for grid search. @@ -665,13 +667,24 @@ def generate_grid( points to divide the grid side formed by the corresponding Hyperparameter in to. Returns: - List containing Configurations. It is a cartesian product of tuples - of HyperParameter values. - Each tuple lists the possible values taken by the corresponding HyperParameter. - Within the cartesian product, in each element, the ordering of HyperParameters - is the same for the OrderedDict within the ConfigurationSpace. + A generator producing Configurations for a given ConfigurationSpace as a cartesian product of tuples of HyperParameter values. + It is a cartesian product of tuples, where each tuple lists the possible values taken by the corresponding HyperParameter. + Within the cartesian product, in each element, the ordering of HyperParameters is the same for the OrderedDict within the ConfigurationSpace. """ + # TODO: + # Idea; we can perhaps create a generator for each HP, to avoid taking the entire grid into memory + # Then we can draw for each HP a value from each generator and test the yielded configuration (masking out the HP values that actually should be inactive) + # For each combination that **could** result in a duplicate (due to active vs inactive HPs), we need to store a light weight hash of the configuration + # That we can check each time s.t. we can quickly skip over combinations that are known to be duplicates + # 1. Build a generator(?) for each HP based on their min/max and step size + # 2. Make sure this generator allows us to build a 'cartesian product' generator s.t. all combinations are made (including inactive HPs?) + # 3. It would be best if we could make the HPs generate values for active HPs only when applicable but this is complicated due to not knowing the dependency order + # 4. ?? + # 5. Profit + + + def _get_value_set(num_steps_dict: dict[str, int] | None, hp: Hyperparameter) -> tuple: if isinstance(hp, (CategoricalHyperparameter)): return cast(tuple, hp.choices) @@ -714,79 +727,156 @@ def _get_cartesian_product( grid.append(config_dict) return grid - # Each tuple within is the grid values to be taken on by a Hyperparameter - value_sets = [] - hp_names = [] - - # Get HP names and allowed grid values they can take for the HPs at the top - # level of ConfigSpace tree - for hp_name in configuration_space.unconditional_hyperparameters: - value_sets.append(_get_value_set(num_steps_dict, configuration_space[hp_name])) - hp_names.append(hp_name) - # Create a Cartesian product of above allowed values for the HPs. Hold them in an - # "unchecked" deque because some of the conditionally dependent HPs may become - # active for some of the elements of the Cartesian product and in these cases - # creating a Configuration would throw an Error (see below). - # Creates a deque of Configuration dicts - unchecked_grid_pts = deque(_get_cartesian_product(value_sets, hp_names)) - checked_grid_pts = [] + def _hyperparameter_range(hp: Hyperparameter, num_steps: int) -> range | tuple | Generator: + """Constructs the range of the hyperparameter or tuple for categorical / ordinal hyperparameters and constants.""" + + def frange(lower: float, upper: float, numsteps: int, log: bool=False, as_int: bool=False, conditional: bool=False) -> Generator[float, None, None]: + """For some reason this does not exist by default in Python, and Numpy returns arrays instead of generators.""" + if log: + lower_source, upper_source = lower, upper + lower, upper = math.log(lower), math.log(upper) + x = lower # Starting point + step_size = float((upper - lower) / (numsteps-1)) + if not log: # Determine precision + precision = len(str(step_size).split(".", maxsplit=1)[1]) # This is so ugly... + while x <= upper: + if log: # Capping for float rounding errors + # NOTE: What if the capping is now letting through a final value that was originally waaaaaay out of bounds? Should it not be rejected? + value = min(max(math.exp(x), lower_source), upper_source) + if as_int: + value = round(value) + else: + value = round(x) if as_int else x + yield value + x += step_size + if not log: # Linear, thus we can make the precision to be the same as the step_size for accuracy purposes + x = round(x, precision) + if conditional: + yield None # Include the 'inactive' option + + conditional_hp = hp.name in configuration_space.conditional_hyperparameters + if isinstance(hp, (CategoricalHyperparameter)): + return cast(tuple, list(hp.choices) + [None] if conditional_hp else hp.choices) + elif isinstance(hp, (OrdinalHyperparameter)): + return cast(tuple, list(hp.sequence) + [None] if conditional_hp else hp.sequence) + elif isinstance(hp, Constant): + return (hp.value, None) if conditional_hp else (hp.value,) + elif num_steps is None: # The latter two hyperparameter require a number of steps, do a quick check if to see if we can proceed + raise ValueError(f"No number of steps provided for {hp.name} i.e. the number of points to divide {hp.name} into.") + elif isinstance(hp, UniformIntegerHyperparameter): + return frange(hp.lower, hp.upper, num_steps, log=hp.log, as_int=True, conditional=conditional_hp) + elif isinstance(hp, UniformFloatHyperparameter): + return frange(hp.lower, hp.upper, num_steps, log=hp.log, conditional=conditional_hp) + raise TypeError(f"Unknown hyperparameter type {type(hp)}") - while len(unchecked_grid_pts) > 0: + def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, None, None]: + """Constructs a generator that produces a cartesian product of the Hyperparameter values.""" + hp_ranges = [_hyperparameter_range(hp, num_steps_dict.get(hp.name, None) if num_steps_dict else None) for hp in hps] + if not hp_ranges: + # Itertools.product returns an empty tuple if hp_ranges is empty, to prevent this we check if the list contains anything before unpacking + return itertools.product([]) + return itertools.product(*hp_ranges) + + # We record the hash of the configurations that we have seen so far? + duplicates_memory: set[int] = set() + hyperparameter_names = list(configuration_space.keys()) + hyperparameters = configuration_space.values() + for configuration in _cartesian_product_generator(hyperparameters): try: + # Zip the configuration in to a dictionary, filtering out the None values (inactive hyperparameters) + # TODO: Mask inactive hyperparameters? + configuration = {key: value for key, value in zip(hyperparameter_names, configuration) if value is not None} + grid_point = Configuration( configuration_space, - values=unchecked_grid_pts[0], + values=configuration, ) - checked_grid_pts.append(grid_point) - - # When creating a configuration that violates a forbidden clause, simply skip it - except ForbiddenValueError: - unchecked_grid_pts.popleft() + yield grid_point + except InactiveHyperparameterSetError as ex: + # The grid generator generates all possible combinations, thus also providing values for inactive hyperparameters continue + except ActiveHyperparameterNotSetError as ex: + # The grid includes the 'None', e.g. empty, value for conditional parameters thus including combinations where active hyperparameters are NOT set + continue + except ForbiddenValueError as ex: + # The grid generator generates all possible combinations, including illegal ones + continue + except IllegalValueError as ex: + # Should not occur + raise ex + + # # Each tuple within is the grid values to be taken on by a Hyperparameter + # hp_names = list(configuration_space.unconditional_hyperparameters) # Create a copy + # # Get HP names and allowed grid values they can take for the HPs at the top + # # level of ConfigSpace tree + # value_sets = [_get_value_set(num_steps_dict, configuration_space[hp_name]) + # for hp_name in hp_names] - except ActiveHyperparameterNotSetError: - value_sets = [] - hp_names = [] - new_active_hp_names = [] - - # "for" loop over currently active HP names - for hp_name in unchecked_grid_pts[0]: - value_sets.append((unchecked_grid_pts[0][hp_name],)) - hp_names.append(hp_name) - # Checks if the conditionally dependent children of already active - # HPs are now active - # TODO: Shorten this - for new_hp_name in configuration_space._dag.nodes[hp_name].children: - if ( - new_hp_name not in new_active_hp_names - and new_hp_name not in unchecked_grid_pts[0] - ): - all_cond_ = True - for cond in configuration_space.parent_conditions_of[ - new_hp_name - ]: - if not cond.satisfied_by_value(unchecked_grid_pts[0]): - all_cond_ = False - if all_cond_: - new_active_hp_names.append(new_hp_name) - - for hp_name in new_active_hp_names: - value_sets.append(_get_value_set(num_steps_dict, configuration_space[hp_name])) - hp_names.append(hp_name) - - # this check might not be needed, as there is always going to be a new - # active HP when in this except block? - if len(new_active_hp_names) <= 0: - raise RuntimeError( - "Unexpected error: There should have been a newly activated" - " hyperparameter for the current configuration values:" - f" {unchecked_grid_pts[0]!s}. Please contact the developers with" - " the code you ran and the stack trace.", - ) from None - - new_conditonal_grid = _get_cartesian_product(value_sets, hp_names) - unchecked_grid_pts += new_conditonal_grid - unchecked_grid_pts.popleft() - - return checked_grid_pts + # Create a Cartesian product of above allowed values for the HPs. Hold them in an + # "unchecked" deque because some of the conditionally dependent HPs may become + # active for some of the elements of the Cartesian product and in these cases + # creating a Configuration would throw an Error (see below). + # Creates a deque of Configuration dicts + # unchecked_grid_pts = deque(_get_cartesian_product(value_sets, hp_names)) + # checked_grid_pts = [] + + # while len(unchecked_grid_pts) > 0: + # try: + # grid_point = Configuration( + # configuration_space, + # values=unchecked_grid_pts[0], + # ) + # yield grid_point + # #checked_grid_pts.append(grid_point) + + # # When creating a configuration that violates a forbidden clause, simply skip it + # except ForbiddenValueError: + # unchecked_grid_pts.popleft() + # continue + # # As we initially only created a grid for the unconditional HPs, we now need to create the grid for the now activated HPs + # except ActiveHyperparameterNotSetError: + # value_sets = [] + # hp_names = [] + # new_active_hp_names = [] + + # # "for" loop over currently active HP names + # for hp_name in unchecked_grid_pts[0]: + # value_sets.append((unchecked_grid_pts[0][hp_name],)) + # hp_names.append(hp_name) + # # Checks if the conditionally dependent children of already active + # # HPs are now active + # # TODO: Shorten this + # for new_hp_name in configuration_space._dag.nodes[hp_name].children: + # if ( + # new_hp_name not in new_active_hp_names + # and new_hp_name not in unchecked_grid_pts[0] + # ): + # all_cond_ = True + # for cond in configuration_space.parent_conditions_of[ + # new_hp_name + # ]: + # if not cond.satisfied_by_value(unchecked_grid_pts[0]): + # all_cond_ = False + # if all_cond_: + # new_active_hp_names.append(new_hp_name) + + # for hp_name in new_active_hp_names: + # value_sets.append(_get_value_set(num_steps_dict, configuration_space[hp_name])) + # hp_names.append(hp_name) + + # # this check might not be needed, as there is always going to be a new + # # active HP when in this except block? + # if len(new_active_hp_names) <= 0: + # raise RuntimeError( + # "Unexpected error: There should have been a newly activated" + # " hyperparameter for the current configuration values:" + # f" {unchecked_grid_pts[0]!s}. Please contact the developers with" + # " the code you ran and the stack trace.", + # ) from None + + # new_conditonal_grid = _get_cartesian_product(value_sets, hp_names) + # unchecked_grid_pts += new_conditonal_grid + # unchecked_grid_pts.popleft() + + # return checked_grid_pts diff --git a/test/test_util.py b/test/test_util.py index f6e7f8bb..ca6e6d24 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -60,9 +60,9 @@ change_hp_value, deactivate_inactive_hyperparameters, fix_types, - generate_grid, get_one_exchange_neighbourhood, get_random_neighbor, + grid_generator, impute_inactive_values, ) @@ -466,11 +466,24 @@ def test_generate_grid(): cs.add([float1, int1, cat1, ord1, const1]) num_steps_dict = {"float1": 11, "int1": 6} - generated_grid = generate_grid(cs, num_steps_dict) + generated_grid = list(grid_generator(cs, num_steps_dict)) + # cat1 2 + # const1 1 + # float1 11 + # int1 7 + # ord1 3 # Check randomly pre-selected values in the generated_grid # 2 * 1 * 11 * 6 * 3 total diff. possible configurations - assert len(generated_grid) == 396 + # My output for int1: + # 10 10.000000000000002 + # 14 14.677992676220699 + # 21 21.544346900318843 + # 31 31.622776601683803 + # 46 46.41588833612781 + # 68 68.12920690579618 + # 100 100.00000000000004 # <- This one is new. Should this not be rounded to 100 and thus be accepted? + assert len(generated_grid) == 396, "Wrong number of generated configurations" # Check 1st and last generated configurations completely: first_expected_dict = { "cat1": "T", @@ -507,7 +520,7 @@ def test_generate_grid(): cs.add([float1, int1]) num_steps_dict = {"float1": 11, "int1": 6} - generated_grid = generate_grid(cs, num_steps_dict) + generated_grid = list(grid_generator(cs, num_steps_dict)) assert len(generated_grid) == 66 # Check 1st and last generated configurations completely: @@ -520,7 +533,7 @@ def test_generate_grid(): cs = ConfigurationSpace(seed=1234) cs.add([cat1]) - generated_grid = generate_grid(cs) + generated_grid = list(grid_generator(cs)) assert len(generated_grid) == 2 # Check 1st and last generated configurations completely: @@ -531,7 +544,7 @@ def test_generate_grid(): cs = ConfigurationSpace(seed=1234) cs.add([const1]) - generated_grid = generate_grid(cs) + generated_grid = list(grid_generator(cs)) assert len(generated_grid) == 1 # Check 1st and only generated configuration completely: @@ -540,8 +553,7 @@ def test_generate_grid(): # Test: no hyperparameters yet cs = ConfigurationSpace(seed=1234) - generated_grid = generate_grid(cs, num_steps_dict) - + generated_grid = list(grid_generator(cs, num_steps_dict)) # For the case of no hyperparameters, in get_cartesian_product, itertools.product() returns # a single empty tuple element which leads to a single empty Configuration. assert len(generated_grid) == 0 @@ -585,7 +597,7 @@ def test_generate_grid(): cond_3 = GreaterThanCondition(float2_cond, int2_cond, 50) cs2.add([cond_3]) num_steps_dict1 = {"float1": 4, "int2_cond": 3, "float2_cond": 3, "int1": 3} - generated_grid = generate_grid(cs2, num_steps_dict1) + generated_grid = list(grid_generator(cs2, num_steps_dict1)) assert len(generated_grid) == 18 # RR: I manually generated the grid and verified the values were correct. @@ -611,9 +623,14 @@ def test_generate_grid(): assert generated_value == expected_value # Here, we test that a few randomly chosen values in the generated grid # correspond to the ones I checked. + # NOTE: Should we not check the full configuration instead? assert generated_grid[3]["int1"] == 1000 - assert generated_grid[12]["cat1_cond"] == "orange" - assert generated_grid[-2]["float2_cond"] == pytest.approx( + assert ( + generated_grid[8]["cat1_cond"] == "apple" + ) # NOTE: Was index 12, code changed order + assert generated_grid[5][ + "float2_cond" + ] == pytest.approx( # NOTE: Was index 5, code changed order 31.622776601683803, abs=1e-3, ) @@ -624,16 +641,14 @@ def test_generate_grid(): cs.add([float1]) num_steps_dict = {"float1": 11} - try: - generated_grid = generate_grid(cs) - except ValueError as e: - assert ( - str(e) == "num_steps_dict is None or doesn't contain " - "the number of points to divide float1 into. And its quantization " - "factor is None. Please provide/set one of these values." - ) + with pytest.raises(ValueError) as e: + generated_grid = list(grid_generator(cs)) + assert ( + str(e.value) + == "No number of steps provided for float1 i.e. the number of points to divide float1 into." + ) - generated_grid = generate_grid(cs, num_steps_dict) + generated_grid = list(grid_generator(cs, num_steps_dict)) assert len(generated_grid) == 11 # Check 1st and last generated configurations completely: @@ -651,10 +666,16 @@ def test_generate_grid(): ), ) - generated_grid = generate_grid(cs, {"int1": 2}) + generated_grid = list(grid_generator(cs, {"int1": 2})) + for i, c in enumerate(generated_grid): + print(i, c) assert len(generated_grid) == 8 - assert dict(generated_grid[0]) == {"cat1": "F", "ord1": "1"} - assert dict(generated_grid[1]) == {"cat1": "F", "ord1": "2"} - assert dict(generated_grid[2]) == {"cat1": "T", "ord1": "1", "int1": 0} - assert dict(generated_grid[-1]) == {"cat1": "T", "ord1": "3", "int1": 1000} + assert dict(generated_grid[0]) == {"cat1": "T", "ord1": "1", "int1": 0} + assert dict(generated_grid[1]) == {"cat1": "T", "ord1": "1", "int1": 1000} + assert dict(generated_grid[2]) == {"cat1": "T", "ord1": "2", "int1": 0} + assert dict(generated_grid[3]) == {"cat1": "T", "ord1": "2", "int1": 1000} + assert dict(generated_grid[4]) == {"cat1": "T", "ord1": "3", "int1": 0} + assert dict(generated_grid[5]) == {"cat1": "T", "ord1": "3", "int1": 1000} + assert dict(generated_grid[6]) == {"cat1": "F", "ord1": "1"} + assert dict(generated_grid[7]) == {"cat1": "F", "ord1": "2"} From a3f7c048109ac25df12078ca99b85a2180f6b682 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 20 Feb 2026 14:31:21 +0100 Subject: [PATCH 03/13] Minor fixes --- src/ConfigSpace/util.py | 75 ----------------------------------------- test/test_util.py | 8 ----- 2 files changed, 83 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index c774096c..400b6c8a 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -805,78 +805,3 @@ def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, except IllegalValueError as ex: # Should not occur raise ex - - # # Each tuple within is the grid values to be taken on by a Hyperparameter - # hp_names = list(configuration_space.unconditional_hyperparameters) # Create a copy - # # Get HP names and allowed grid values they can take for the HPs at the top - # # level of ConfigSpace tree - # value_sets = [_get_value_set(num_steps_dict, configuration_space[hp_name]) - # for hp_name in hp_names] - - # Create a Cartesian product of above allowed values for the HPs. Hold them in an - # "unchecked" deque because some of the conditionally dependent HPs may become - # active for some of the elements of the Cartesian product and in these cases - # creating a Configuration would throw an Error (see below). - # Creates a deque of Configuration dicts - # unchecked_grid_pts = deque(_get_cartesian_product(value_sets, hp_names)) - # checked_grid_pts = [] - - # while len(unchecked_grid_pts) > 0: - # try: - # grid_point = Configuration( - # configuration_space, - # values=unchecked_grid_pts[0], - # ) - # yield grid_point - # #checked_grid_pts.append(grid_point) - - # # When creating a configuration that violates a forbidden clause, simply skip it - # except ForbiddenValueError: - # unchecked_grid_pts.popleft() - # continue - # # As we initially only created a grid for the unconditional HPs, we now need to create the grid for the now activated HPs - # except ActiveHyperparameterNotSetError: - # value_sets = [] - # hp_names = [] - # new_active_hp_names = [] - - # # "for" loop over currently active HP names - # for hp_name in unchecked_grid_pts[0]: - # value_sets.append((unchecked_grid_pts[0][hp_name],)) - # hp_names.append(hp_name) - # # Checks if the conditionally dependent children of already active - # # HPs are now active - # # TODO: Shorten this - # for new_hp_name in configuration_space._dag.nodes[hp_name].children: - # if ( - # new_hp_name not in new_active_hp_names - # and new_hp_name not in unchecked_grid_pts[0] - # ): - # all_cond_ = True - # for cond in configuration_space.parent_conditions_of[ - # new_hp_name - # ]: - # if not cond.satisfied_by_value(unchecked_grid_pts[0]): - # all_cond_ = False - # if all_cond_: - # new_active_hp_names.append(new_hp_name) - - # for hp_name in new_active_hp_names: - # value_sets.append(_get_value_set(num_steps_dict, configuration_space[hp_name])) - # hp_names.append(hp_name) - - # # this check might not be needed, as there is always going to be a new - # # active HP when in this except block? - # if len(new_active_hp_names) <= 0: - # raise RuntimeError( - # "Unexpected error: There should have been a newly activated" - # " hyperparameter for the current configuration values:" - # f" {unchecked_grid_pts[0]!s}. Please contact the developers with" - # " the code you ran and the stack trace.", - # ) from None - - # new_conditonal_grid = _get_cartesian_product(value_sets, hp_names) - # unchecked_grid_pts += new_conditonal_grid - # unchecked_grid_pts.popleft() - - # return checked_grid_pts diff --git a/test/test_util.py b/test/test_util.py index ca6e6d24..ffffc781 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -475,14 +475,6 @@ def test_generate_grid(): # Check randomly pre-selected values in the generated_grid # 2 * 1 * 11 * 6 * 3 total diff. possible configurations - # My output for int1: - # 10 10.000000000000002 - # 14 14.677992676220699 - # 21 21.544346900318843 - # 31 31.622776601683803 - # 46 46.41588833612781 - # 68 68.12920690579618 - # 100 100.00000000000004 # <- This one is new. Should this not be rounded to 100 and thus be accepted? assert len(generated_grid) == 396, "Wrong number of generated configurations" # Check 1st and last generated configurations completely: first_expected_dict = { From e7153537764a8b58fa37362f14db042d403fef10 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 20 Feb 2026 14:48:07 +0100 Subject: [PATCH 04/13] Update comments --- src/ConfigSpace/util.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 400b6c8a..2cf79acf 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -785,7 +785,6 @@ def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, for configuration in _cartesian_product_generator(hyperparameters): try: # Zip the configuration in to a dictionary, filtering out the None values (inactive hyperparameters) - # TODO: Mask inactive hyperparameters? configuration = {key: value for key, value in zip(hyperparameter_names, configuration) if value is not None} grid_point = Configuration( @@ -793,15 +792,11 @@ def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, values=configuration, ) yield grid_point - except InactiveHyperparameterSetError as ex: - # The grid generator generates all possible combinations, thus also providing values for inactive hyperparameters + except InactiveHyperparameterSetError as ex: # The grid generator generates all possible combinations, thus also providing values for inactive hyperparameters continue - except ActiveHyperparameterNotSetError as ex: - # The grid includes the 'None', e.g. empty, value for conditional parameters thus including combinations where active hyperparameters are NOT set + except ActiveHyperparameterNotSetError as ex: # The grid includes the 'None', e.g. empty, value for conditional parameters thus including combinations where active hyperparameters are NOT set continue - except ForbiddenValueError as ex: - # The grid generator generates all possible combinations, including illegal ones + except ForbiddenValueError as ex: # The grid generator generates all possible combinations, including those violating the Forbidden rules continue - except IllegalValueError as ex: - # Should not occur + except IllegalValueError as ex: # Should not occur: The grid should only generate legal values for each HP. raise ex From 29e43ca66a6bd1542cb10d2486ad335562813f4f Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 20 Feb 2026 14:48:16 +0100 Subject: [PATCH 05/13] Remove empty line --- src/ConfigSpace/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 2cf79acf..770d02bf 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -786,7 +786,6 @@ def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, try: # Zip the configuration in to a dictionary, filtering out the None values (inactive hyperparameters) configuration = {key: value for key, value in zip(hyperparameter_names, configuration) if value is not None} - grid_point = Configuration( configuration_space, values=configuration, From afda91a05e1651b886d6e4fbd1bdfb1883e787e8 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 20 Feb 2026 14:49:27 +0100 Subject: [PATCH 06/13] remove old code --- src/ConfigSpace/util.py | 50 ++--------------------------------------- 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 770d02bf..59cdd0b1 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -672,62 +672,16 @@ def grid_generator( Within the cartesian product, in each element, the ordering of HyperParameters is the same for the OrderedDict within the ConfigurationSpace. """ - # TODO: # Idea; we can perhaps create a generator for each HP, to avoid taking the entire grid into memory # Then we can draw for each HP a value from each generator and test the yielded configuration (masking out the HP values that actually should be inactive) # For each combination that **could** result in a duplicate (due to active vs inactive HPs), we need to store a light weight hash of the configuration # That we can check each time s.t. we can quickly skip over combinations that are known to be duplicates - # 1. Build a generator(?) for each HP based on their min/max and step size - # 2. Make sure this generator allows us to build a 'cartesian product' generator s.t. all combinations are made (including inactive HPs?) + # 1. Build a generator for each HP based on their min/max and step size + # 2. This generator allows us to build a 'cartesian product' generator s.t. all combinations are made (including inactive HPs....) # 3. It would be best if we could make the HPs generate values for active HPs only when applicable but this is complicated due to not knowing the dependency order # 4. ?? # 5. Profit - - - def _get_value_set(num_steps_dict: dict[str, int] | None, hp: Hyperparameter) -> tuple: - if isinstance(hp, (CategoricalHyperparameter)): - return cast(tuple, hp.choices) - elif isinstance(hp, (OrdinalHyperparameter)): - return cast(tuple, hp.sequence) - elif isinstance(hp, Constant): - return (hp.value,) - elif isinstance(hp, (UniformFloatHyperparameter, UniformIntegerHyperparameter)): - if not num_steps_dict or hp.name not in num_steps_dict: - raise ValueError( - "num_steps_dict is None or doesn't contain the number of points" - f" to divide {hp.name} into. And its quantization factor " - "is None. Please provide/set one of these values.", - ) - num_steps = num_steps_dict[hp.name] - if hp.log: - lower, upper = np.log([hp.lower, hp.upper]) - grid_points = np.exp(np.linspace(lower, upper, num_steps)) - else: - lower, upper = hp.lower, hp.upper - grid_points = np.linspace(lower, upper, num_steps) - - if isinstance(hp, UniformIntegerHyperparameter): - grid_points = np.round(grid_points).astype(int) - # Avoiding rounding off issues - grid_points[0] = max(grid_points[0], hp.lower) - grid_points[-1] = min(grid_points[-1], hp.upper) - return tuple(grid_points) - - raise TypeError(f"Unknown hyperparameter type {type(hp)}") - - def _get_cartesian_product( - value_sets: list[tuple], - hp_names: list[str], - ) -> list[dict[str, Any]]: - grid = [] - if len(value_sets) > 0: # Edge case for empty value set - for element in itertools.product(*value_sets): - config_dict = dict(zip(hp_names, element)) - grid.append(config_dict) - return grid - - def _hyperparameter_range(hp: Hyperparameter, num_steps: int) -> range | tuple | Generator: """Constructs the range of the hyperparameter or tuple for categorical / ordinal hyperparameters and constants.""" From f6ad9d31a987ff5bbe6a4cdf1787acdc3caa6aaa Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 20 Feb 2026 14:49:46 +0100 Subject: [PATCH 07/13] typos --- src/ConfigSpace/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 59cdd0b1..9895b175 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -655,11 +655,11 @@ def grid_generator( Can be used, for example, for grid search. Args: - configuration_spac: + configuration_space: The Configuration space over which to create a grid of HyperParameter Configuration values. It knows the types for all parameter values. - num_steps_dic: + num_steps_dict: A dict containing the number of points to divide the grid side formed by Hyperparameters which are either of type UniformFloatHyperparameter or type UniformIntegerHyperparameter. The keys in the dict should be the names From 04caf20c51a3aabee919b04bffed8292e57d1ef6 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 6 Mar 2026 10:13:48 +0100 Subject: [PATCH 08/13] typo fix --- src/ConfigSpace/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConfigSpace/configuration.py b/src/ConfigSpace/configuration.py index 65149baa..c2365539 100644 --- a/src/ConfigSpace/configuration.py +++ b/src/ConfigSpace/configuration.py @@ -116,7 +116,7 @@ def __init__( if not hp.legal_value(value): raise IllegalValueError(hp, value) - # Truncate the float to be of constant lengt + # Truncate the float to be of constant length if isinstance(hp, FloatHyperparameter): value = float(np.round(value, ROUND_PLACES)) # type: ignore From a3fc1cc3419088cef3d4668abf1fff9a33edc53e Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 6 Mar 2026 10:15:14 +0100 Subject: [PATCH 09/13] docs fix --- src/ConfigSpace/configuration_space.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConfigSpace/configuration_space.py b/src/ConfigSpace/configuration_space.py index 9652e385..1da97530 100644 --- a/src/ConfigSpace/configuration_space.py +++ b/src/ConfigSpace/configuration_space.py @@ -759,7 +759,7 @@ def estimate_size(self) -> float | int: otherwise it is the product of the size of all hyperparameters. The function correctly guesses the number of unique configurations if there are no condition and forbidden statements in the configuration spaces. Otherwise, this is an - upper bound. Use [`generate_grid()`][ConfigSpace.util.generate_grid] to generate + upper bound. Use [`grid_generator()`][ConfigSpace.util.grid_generator] to generate all valid configurations if required. Returns: From 8da58a0abba0b49c8a8903ad93a8996b69ac838d Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 6 Mar 2026 11:03:21 +0100 Subject: [PATCH 10/13] Speedup fix --- src/ConfigSpace/util.py | 75 ++++++++++++++++++++++++++++++++++------- test/test_util.py | 2 +- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 9895b175..6e7fefa9 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -574,14 +574,19 @@ def check_configuration( # noqa: D103 space: ConfigurationSpace, vector: np.ndarray, allow_inactive_with_values: bool = False, + #yield_all_unset_active_hyperparameters: bool = False, ) -> None: activated = np.isfinite(vector) + #unset_active_hps: list[Hyperparameter] = [] # Make sure the roots are all good for root in space._dag.roots.values(): hp_idx = root.idx if not activated[hp_idx]: + #if not yield_all_unset_active_hyperparameters: raise ActiveHyperparameterNotSetError(root.hp) + #else: + # unset_active_hps.append(hp) for cnode in space._dag.minimum_conditions: # Everything for the condition is satisfied, make sure active @@ -593,7 +598,10 @@ def check_configuration( # noqa: D103 idx: int = children_idxs[~active_mask][0] hp_name = space.at[idx] hp = space[hp_name] + #if not yield_all_unset_active_hyperparameters: raise ActiveHyperparameterNotSetError(hp) + #else: + # unset_active_hps.append(hp) for hp_idx, hp_node in cnode.unique_children.items(): # OPTIM: We bypass the larger safety checking of the hp and access @@ -616,6 +624,10 @@ def check_configuration( # noqa: D103 f"Given vector violates forbidden clause: {forbidden}", ) + # All checks passed, except for possible plural ActiveHyperparameterNotSetError + #if unset_active_hps: + # raise ActiveHyperparametersNotSetError(unset_active_hps) + def change_hp_value( # noqa: D103 configuration_space: ConfigurationSpace, @@ -706,16 +718,19 @@ def frange(lower: float, upper: float, numsteps: int, log: bool=False, as_int: b x += step_size if not log: # Linear, thus we can make the precision to be the same as the step_size for accuracy purposes x = round(x, precision) - if conditional: - yield None # Include the 'inactive' option + #if conditional: + # yield NotSet # Include the 'inactive' option conditional_hp = hp.name in configuration_space.conditional_hyperparameters if isinstance(hp, (CategoricalHyperparameter)): - return cast(tuple, list(hp.choices) + [None] if conditional_hp else hp.choices) + #return cast(tuple, list(hp.choices) + [NotSet] if conditional_hp else hp.choices) + return cast(tuple, hp.choices) elif isinstance(hp, (OrdinalHyperparameter)): - return cast(tuple, list(hp.sequence) + [None] if conditional_hp else hp.sequence) + #return cast(tuple, list(hp.sequence) + [NotSet] if conditional_hp else hp.sequence) + return cast(tuple, hp.sequence) elif isinstance(hp, Constant): - return (hp.value, None) if conditional_hp else (hp.value,) + #return (hp.value, NotSet) if conditional_hp else (hp.value,) + return (hp.value,) elif num_steps is None: # The latter two hyperparameter require a number of steps, do a quick check if to see if we can proceed raise ValueError(f"No number of steps provided for {hp.name} i.e. the number of points to divide {hp.name} into.") elif isinstance(hp, UniformIntegerHyperparameter): @@ -736,20 +751,54 @@ def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, duplicates_memory: set[int] = set() hyperparameter_names = list(configuration_space.keys()) hyperparameters = configuration_space.values() - for configuration in _cartesian_product_generator(hyperparameters): + + regular_hyperparameters = [hp for hp in configuration_space.values() if hp.name not in configuration_space.conditional_hyperparameters] + conditional_hyperparameters = [hp for hp in configuration_space.values() if hp.name in configuration_space.conditional_hyperparameters] + + # hyperparameters = [hp for hp in configuration_space.values() if hp.name not in configuration_space.conditional_hyperparameters] + # hyperparameter_names = [hp.name for hp in hyperparameters] + from ConfigSpace.hyperparameters import FloatHyperparameter + from ConfigSpace.types import Array, Mask, f64 + from ConfigSpace.hyperparameters.hp_components import ROUND_PLACES + + def generate_with_conditionals(regular_configuration: dict[str, Any], active_conditionals: list[Hyperparameter]) -> Generator[Configuration, None, None]: + """Recursively adds all conditional hyperparameters to some configuration of regular HPs.""" + for conditional_configuration in _cartesian_product_generator(active_conditionals): + new_configuration = regular_configuration.copy()# + conditional_configuration + for hp, value in zip(active_conditionals, conditional_configuration): # Combine the existing configuration with new conditional values + new_configuration[hp.name] = value + try: + grid_point = Configuration( + configuration_space, + values=new_configuration, + ) + yield grid_point + except ActiveHyperparameterNotSetError as ex: + for configuration_with_conditionals in generate_with_conditionals(new_configuration, [ex.hyperparameter]): + yield configuration_with_conditionals + except ForbiddenValueError as ex: # The grid generator generates all possible combinations, including those violating the Forbidden rules + continue + except InactiveHyperparameterSetError as ex: # This should not happen? + raise ex + except IllegalValueError as ex: # Should not occur: The grid should only generate legal values for each HP. + raise ex + + for configuration in _cartesian_product_generator(regular_hyperparameters): + configuration_dict = {key: value for key, value in zip(hyperparameter_names, configuration)} try: - # Zip the configuration in to a dictionary, filtering out the None values (inactive hyperparameters) - configuration = {key: value for key, value in zip(hyperparameter_names, configuration) if value is not None} + # NOTE: Build vector instead and call check_configuration here directly? grid_point = Configuration( configuration_space, - values=configuration, + values=configuration_dict, ) yield grid_point - except InactiveHyperparameterSetError as ex: # The grid generator generates all possible combinations, thus also providing values for inactive hyperparameters - continue - except ActiveHyperparameterNotSetError as ex: # The grid includes the 'None', e.g. empty, value for conditional parameters thus including combinations where active hyperparameters are NOT set - continue + except ActiveHyperparameterNotSetError as ex: + # NOTE: We are not getting all possible known ActiveHyperparameterNotSetErrors at once here; its thrown for the first 'mistake' only. + for configuration_with_conditionals in generate_with_conditionals(configuration_dict, [ex.hyperparameter]): + yield configuration_with_conditionals except ForbiddenValueError as ex: # The grid generator generates all possible combinations, including those violating the Forbidden rules continue + except InactiveHyperparameterSetError as ex: # This should not occur due to how conditionals are handled + raise ex except IllegalValueError as ex: # Should not occur: The grid should only generate legal values for each HP. raise ex diff --git a/test/test_util.py b/test/test_util.py index ffffc781..41a82a68 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -452,7 +452,7 @@ def test_fix_types(): assert fix_types(c_str, cs) == c -def test_generate_grid(): +def test_grid_generator(): """Test grid generation.""" # Sub-test 1 cs = ConfigurationSpace(seed=1234) From a230fbce069635e606cb50381dfe6dd33bbd3ccb Mon Sep 17 00:00:00 2001 From: Thijs Snelleman <32924404+thijssnelleman@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:15:45 +0100 Subject: [PATCH 11/13] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ConfigSpace/util.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 6e7fefa9..bc80b4b9 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -747,19 +747,15 @@ def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, return itertools.product([]) return itertools.product(*hp_ranges) - # We record the hash of the configurations that we have seen so far? - duplicates_memory: set[int] = set() hyperparameter_names = list(configuration_space.keys()) - hyperparameters = configuration_space.values() + regular_hyperparameters = [ + hp + for hp in configuration_space.values() + if hp.name not in configuration_space.conditional_hyperparameters + ] - regular_hyperparameters = [hp for hp in configuration_space.values() if hp.name not in configuration_space.conditional_hyperparameters] - conditional_hyperparameters = [hp for hp in configuration_space.values() if hp.name in configuration_space.conditional_hyperparameters] - # hyperparameters = [hp for hp in configuration_space.values() if hp.name not in configuration_space.conditional_hyperparameters] # hyperparameter_names = [hp.name for hp in hyperparameters] - from ConfigSpace.hyperparameters import FloatHyperparameter - from ConfigSpace.types import Array, Mask, f64 - from ConfigSpace.hyperparameters.hp_components import ROUND_PLACES def generate_with_conditionals(regular_configuration: dict[str, Any], active_conditionals: list[Hyperparameter]) -> Generator[Configuration, None, None]: """Recursively adds all conditional hyperparameters to some configuration of regular HPs.""" From f78a7957eac437879ab9c4543936841066a1631d Mon Sep 17 00:00:00 2001 From: Thijs Snelleman <32924404+thijssnelleman@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:18:15 +0100 Subject: [PATCH 12/13] Update test/test_util.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/test_util.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index 41a82a68..574e8591 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -659,8 +659,6 @@ def test_grid_generator(): ) generated_grid = list(grid_generator(cs, {"int1": 2})) - for i, c in enumerate(generated_grid): - print(i, c) assert len(generated_grid) == 8 assert dict(generated_grid[0]) == {"cat1": "T", "ord1": "1", "int1": 0} From 63373e7e52102630bbf06a732d693c059d2bb672 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 16 Mar 2026 13:27:21 +0100 Subject: [PATCH 13/13] cleanup --- src/ConfigSpace/util.py | 53 ++++++++++------------------------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index bc80b4b9..8c2cb4c8 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -32,7 +32,7 @@ import math from collections import deque from collections.abc import Iterator, Sequence -from typing import TYPE_CHECKING, Any, cast, Generator +from typing import TYPE_CHECKING, Any, cast, Generator, Iterable import numpy as np @@ -574,19 +574,14 @@ def check_configuration( # noqa: D103 space: ConfigurationSpace, vector: np.ndarray, allow_inactive_with_values: bool = False, - #yield_all_unset_active_hyperparameters: bool = False, ) -> None: activated = np.isfinite(vector) - #unset_active_hps: list[Hyperparameter] = [] # Make sure the roots are all good for root in space._dag.roots.values(): hp_idx = root.idx if not activated[hp_idx]: - #if not yield_all_unset_active_hyperparameters: raise ActiveHyperparameterNotSetError(root.hp) - #else: - # unset_active_hps.append(hp) for cnode in space._dag.minimum_conditions: # Everything for the condition is satisfied, make sure active @@ -598,10 +593,7 @@ def check_configuration( # noqa: D103 idx: int = children_idxs[~active_mask][0] hp_name = space.at[idx] hp = space[hp_name] - #if not yield_all_unset_active_hyperparameters: raise ActiveHyperparameterNotSetError(hp) - #else: - # unset_active_hps.append(hp) for hp_idx, hp_node in cnode.unique_children.items(): # OPTIM: We bypass the larger safety checking of the hp and access @@ -624,10 +616,6 @@ def check_configuration( # noqa: D103 f"Given vector violates forbidden clause: {forbidden}", ) - # All checks passed, except for possible plural ActiveHyperparameterNotSetError - #if unset_active_hps: - # raise ActiveHyperparametersNotSetError(unset_active_hps) - def change_hp_value( # noqa: D103 configuration_space: ConfigurationSpace, @@ -663,8 +651,8 @@ def grid_generator( configuration_space: ConfigurationSpace, num_steps_dict: dict[str, int] | None = None, ) -> Generator[Configuration, None, None]: - """Generates a grid of Configurations for a given ConfigurationSpace. - Can be used, for example, for grid search. + """Creates a Generator for a grid of Configurations for a given ConfigurationSpace. + Can be used, for example, for grid search. Args: configuration_space: @@ -684,21 +672,13 @@ def grid_generator( Within the cartesian product, in each element, the ordering of HyperParameters is the same for the OrderedDict within the ConfigurationSpace. """ - # Idea; we can perhaps create a generator for each HP, to avoid taking the entire grid into memory - # Then we can draw for each HP a value from each generator and test the yielded configuration (masking out the HP values that actually should be inactive) - # For each combination that **could** result in a duplicate (due to active vs inactive HPs), we need to store a light weight hash of the configuration - # That we can check each time s.t. we can quickly skip over combinations that are known to be duplicates - # 1. Build a generator for each HP based on their min/max and step size - # 2. This generator allows us to build a 'cartesian product' generator s.t. all combinations are made (including inactive HPs....) - # 3. It would be best if we could make the HPs generate values for active HPs only when applicable but this is complicated due to not knowing the dependency order - # 4. ?? - # 5. Profit - - def _hyperparameter_range(hp: Hyperparameter, num_steps: int) -> range | tuple | Generator: + def _hyperparameter_range(hp: Hyperparameter, num_steps: int | None) -> Iterable[Any]: """Constructs the range of the hyperparameter or tuple for categorical / ordinal hyperparameters and constants.""" - def frange(lower: float, upper: float, numsteps: int, log: bool=False, as_int: bool=False, conditional: bool=False) -> Generator[float, None, None]: - """For some reason this does not exist by default in Python, and Numpy returns arrays instead of generators.""" + def frange(lower: float, upper: float, numsteps: int, log: bool=False, as_int: bool=False) -> Generator[float, None, None]: + """Range function for floats. For some reason this does not exist by default in Python, and Numpy returns arrays instead of generators.""" + if numsteps <= 1: + raise ValueError(f"Parameter numsteps must be a positive integer > 1, got {numsteps}") if log: lower_source, upper_source = lower, upper lower, upper = math.log(lower), math.log(upper) @@ -718,25 +698,19 @@ def frange(lower: float, upper: float, numsteps: int, log: bool=False, as_int: b x += step_size if not log: # Linear, thus we can make the precision to be the same as the step_size for accuracy purposes x = round(x, precision) - #if conditional: - # yield NotSet # Include the 'inactive' option - conditional_hp = hp.name in configuration_space.conditional_hyperparameters if isinstance(hp, (CategoricalHyperparameter)): - #return cast(tuple, list(hp.choices) + [NotSet] if conditional_hp else hp.choices) return cast(tuple, hp.choices) elif isinstance(hp, (OrdinalHyperparameter)): - #return cast(tuple, list(hp.sequence) + [NotSet] if conditional_hp else hp.sequence) return cast(tuple, hp.sequence) elif isinstance(hp, Constant): - #return (hp.value, NotSet) if conditional_hp else (hp.value,) return (hp.value,) - elif num_steps is None: # The latter two hyperparameter require a number of steps, do a quick check if to see if we can proceed - raise ValueError(f"No number of steps provided for {hp.name} i.e. the number of points to divide {hp.name} into.") + elif num_steps is None or num_steps <= 1: # The latter two hyperparameter require a number of steps, do a quick check if to see if we can proceed + raise ValueError(f"No valid number of steps provided for {hp.name} i.e. the number of points to divide {hp.name} into (num_steps == {num_steps}).") elif isinstance(hp, UniformIntegerHyperparameter): - return frange(hp.lower, hp.upper, num_steps, log=hp.log, as_int=True, conditional=conditional_hp) + return frange(hp.lower, hp.upper, num_steps, log=hp.log, as_int=True) elif isinstance(hp, UniformFloatHyperparameter): - return frange(hp.lower, hp.upper, num_steps, log=hp.log, conditional=conditional_hp) + return frange(hp.lower, hp.upper, num_steps, log=hp.log) raise TypeError(f"Unknown hyperparameter type {type(hp)}") def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, None, None]: @@ -754,9 +728,6 @@ def _cartesian_product_generator(hps: list[Hyperparameter]) -> Generator[tuple, if hp.name not in configuration_space.conditional_hyperparameters ] - # hyperparameters = [hp for hp in configuration_space.values() if hp.name not in configuration_space.conditional_hyperparameters] - # hyperparameter_names = [hp.name for hp in hyperparameters] - def generate_with_conditionals(regular_configuration: dict[str, Any], active_conditionals: list[Hyperparameter]) -> Generator[Configuration, None, None]: """Recursively adds all conditional hyperparameters to some configuration of regular HPs.""" for conditional_configuration in _cartesian_product_generator(active_conditionals):