Skip to content

Commit d598714

Browse files
committed
add support for continuous dimensions
1 parent 0a3318d commit d598714

7 files changed

Lines changed: 728 additions & 44 deletions

File tree

src/hyperactive/opt/_adapters/_adapter_utils.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ def adapt_search_space(experiment, search_config, capabilities):
2929
"""Adapt search space and experiment for backend capabilities.
3030
3131
If the backend doesn't support certain search space features
32-
(e.g., categorical values), this function encodes the search space
33-
and wraps the experiment to handle encoding/decoding transparently.
32+
(e.g., categorical values, continuous ranges), this function:
33+
- Validates the search space format
34+
- Encodes categorical dimensions (strings to integers)
35+
- Discretizes continuous dimensions (tuples to lists)
36+
- Wraps the experiment to decode parameters during scoring
3437
3538
Parameters
3639
----------
@@ -46,9 +49,14 @@ def adapt_search_space(experiment, search_config, capabilities):
4649
experiment : BaseExperiment
4750
The experiment, possibly wrapped for decoding.
4851
search_config : dict
49-
The search config, possibly with encoded search space.
52+
The search config, possibly with encoded/discretized search space.
5053
adapter : SearchSpaceAdapter or None
51-
The adapter if encoding was applied, None otherwise.
54+
The adapter if adaptation was applied, None otherwise.
55+
56+
Raises
57+
------
58+
ValueError, TypeError
59+
If the search space format is invalid.
5260
"""
5361
search_space_key = detect_search_space_key(search_config)
5462

@@ -59,11 +67,14 @@ def adapt_search_space(experiment, search_config, capabilities):
5967
# Create adapter with backend capabilities
6068
adapter = SearchSpaceAdapter(search_config[search_space_key], capabilities)
6169

70+
# Validate search space format
71+
adapter.validate()
72+
6273
# Backend supports all features - pass through unchanged
63-
if not adapter.needs_encoding:
74+
if not adapter.needs_adaptation:
6475
return experiment, search_config, None
6576

66-
# Encoding needed - transform search space and wrap experiment
77+
# Adaptation needed - transform search space and wrap experiment
6778
encoded_config = search_config.copy()
6879
encoded_config[search_space_key] = adapter.encode()
6980
wrapped_experiment = adapter.wrap_experiment(experiment)

src/hyperactive/opt/_adapters/_base_optuna_adapter.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,66 @@ def _suggest_params(self, trial, param_space):
117117
for key, space in param_space.items():
118118
if hasattr(space, "suggest"): # optuna distribution object
119119
params[key] = trial._suggest(space, key)
120-
elif isinstance(space, tuple) and len(space) == 2:
121-
# Tuples are treated as ranges (low, high)
122-
low, high = space
123-
if isinstance(low, int) and isinstance(high, int):
124-
params[key] = trial.suggest_int(key, low, high)
125-
else:
126-
params[key] = trial.suggest_float(key, low, high, log=False)
120+
elif isinstance(space, tuple):
121+
# Tuples are continuous ranges in unified format
122+
params[key] = self._suggest_continuous(trial, key, space)
127123
elif isinstance(space, list):
128124
# Lists are treated as categorical choices
129125
params[key] = trial.suggest_categorical(key, space)
130126
else:
131127
raise ValueError(f"Invalid parameter space for key '{key}': {space}")
132128
return params
133129

130+
def _suggest_continuous(self, trial, key, space):
131+
"""Suggest a continuous parameter from a tuple specification.
132+
133+
Handles unified tuple formats:
134+
- (low, high) - linear scale
135+
- (low, high, "log") - log scale
136+
- (low, high, n_points) - linear scale (n_points ignored for Optuna)
137+
- (low, high, n_points, "log") - log scale (n_points ignored for Optuna)
138+
139+
Parameters
140+
----------
141+
trial : optuna.Trial
142+
The Optuna trial object
143+
key : str
144+
The parameter name
145+
space : tuple
146+
The continuous range specification
147+
148+
Returns
149+
-------
150+
float or int
151+
The suggested value
152+
"""
153+
if len(space) < 2:
154+
raise ValueError(
155+
f"Parameter '{key}': continuous range needs at least 2 values "
156+
f"(low, high), got {len(space)}."
157+
)
158+
159+
low, high = space[0], space[1]
160+
log_scale = False
161+
162+
# Parse optional arguments
163+
if len(space) == 3:
164+
third = space[2]
165+
if isinstance(third, str) and third.lower() == "log":
166+
log_scale = True
167+
# If third is int/float, it's n_points - ignore for Optuna
168+
elif len(space) == 4:
169+
# (low, high, n_points, "log")
170+
fourth = space[3]
171+
if isinstance(fourth, str) and fourth.lower() == "log":
172+
log_scale = True
173+
174+
# Suggest based on type
175+
if isinstance(low, int) and isinstance(high, int):
176+
return trial.suggest_int(key, low, high, log=log_scale)
177+
else:
178+
return trial.suggest_float(key, low, high, log=log_scale)
179+
134180
def _objective(self, trial):
135181
"""Objective function for Optuna optimization.
136182

src/hyperactive/opt/_adapters/_gfo.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class _BaseGFOadapter(BaseOptimizer):
2525
"python_dependencies": ["gradient-free-optimizers>=1.5.0"],
2626
# Search space capabilities
2727
"capability:discrete": True,
28-
"capability:continuous": True,
28+
"capability:continuous": False, # GFO needs lists, not (low, high) tuples
2929
"capability:categorical": False, # GFO only supports numeric values
3030
"capability:constraints": True,
3131
}
@@ -78,7 +78,9 @@ def get_search_config(self):
7878

7979
search_config = self._handle_gfo_defaults(search_config)
8080

81-
search_config["search_space"] = self._to_dict_np(search_config["search_space"])
81+
# Note: _to_dict_np is called in _solve(), after SearchSpaceAdapter processes
82+
# continuous tuples. If we convert here, tuples like (1e-4, 1e-1, "log")
83+
# would become numpy arrays with strings before the adapter can discretize them.
8284

8385
return search_config
8486

@@ -151,6 +153,10 @@ def _solve(self, experiment, **search_config):
151153
n_iter = search_config.pop("n_iter", 100)
152154
max_time = search_config.pop("max_time", None)
153155

156+
# Convert search_space lists to numpy arrays (GFO requirement)
157+
# This must happen after SearchSpaceAdapter has processed continuous tuples
158+
search_config["search_space"] = self._to_dict_np(search_config["search_space"])
159+
154160
gfo_cls = self._get_gfo_class()
155161
gfopt = gfo_cls(**search_config)
156162

0 commit comments

Comments
 (0)