From 469d059d24d6e2a32881160246e758e98428978a Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 22 Sep 2025 17:56:44 +0200 Subject: [PATCH 01/29] First version --- src/ConfigSpace/util.py | 169 ++++++++++++++++++++++++++++++++++++++++ test/test_util.py | 53 +++++++++++++ 2 files changed, 222 insertions(+) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index f93baab4..b0540e9a 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -27,6 +27,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations +import ast import copy from collections import deque from collections.abc import Iterator, Sequence @@ -50,6 +51,11 @@ UniformFloatHyperparameter, UniformIntegerHyperparameter, ) + +from ConfigSpace.conditions import * +from ConfigSpace.forbidden import * + + from ConfigSpace.types import NotSet if TYPE_CHECKING: @@ -832,3 +838,166 @@ def _get_cartesian_product( unchecked_grid_pts.popleft() return checked_grid_pts + + +def expression_to_configspace( + expression: str | ast.Module, + configspace: ConfigurationSpace, + target_parameter: Hyperparameter = None, +) -> ForbiddenClause | Condition: + """Convert a logic expression to ConfigSpace expression. + + Args: + expression: The expression to convert. + configspace: The ConfigSpace to use. + target_parameter: For conditions, will parse the expression as a condition + underwhich the parameter will be active. + """ + if isinstance(expression, str): + import re + # Format expression to match the ast module + # Format logical operators: + expression = re.sub(r" & ", " and ", expression) + expression = re.sub(r" && ", " and ", expression) + expression = re.sub(r" \| ", " or ", expression) + expression = re.sub(r" \|\| ", " or ", expression) + # Format (in)equality operators: + expression = re.sub(r" !== ", " != ", expression) + expression = re.sub(r" != ", " = ", expression) + expression = re.sub(r" (?!=])=(? ForbiddenClause | Condition: + """Recursively parse the abstract syntax tree to a ConfigSpace expression. + + Args: + item: The item to parse. + configspace: The ConfigSpace to use. + target_parameter: For conditions, will parse the expression as a condition + underwhich the parameter will be active. + + Returns: + A ConfigSpace expression + """ + if isinstance(item, list): + if len(item) > 1: + raise ValueError(f"Can not parse list of elements: {item}.") + item = item[0] + if isinstance(item, ast.Expr): + return recursive_conversion(item.value, configspace, target_parameter) + if isinstance(item, ast.Name): # Convert to hyperparameter + hp = configspace.get(item.id) + return hp if hp is not None else item.id + if isinstance(item, ast.Constant): + return item.value + if ( + isinstance(item, ast.Tuple) + or isinstance(item, ast.Set) + or isinstance(item, ast.List) + ): + values = [] + for v in item.elts: + if isinstance(v, ast.Constant): + values.append(v.value) + elif isinstance(v, ast.Name): # Check if its a parameter + if v.id in list(configspace.values()): + raise ValueError( + f"Only constants allowed in tuples. Found: {item.elts}" + ) + values.append(v.id) # String value was interpreted as parameter + return values + if isinstance(item, ast.BinOp): + raise NotImplementedError("Binary operations not supported by ConfigSpace.") + if isinstance(item, ast.BoolOp): + values = [ + recursive_conversion(v, configspace, target_parameter) for v in item.values + ] + if isinstance(item.op, ast.Or): + if target_parameter: + return OrConjunction(*values) + return ForbiddenOrConjunction(*values) + elif isinstance(item.op, ast.And): + if target_parameter: + return AndConjunction(*values) + return ForbiddenAndConjunction(*values) + else: + raise ValueError(f"Unknown boolean operator: {item.op}") + if isinstance(item, ast.Compare): + if len(item.ops) > 1: + raise ValueError(f"Only single comparisons allowed. Found: {item.ops}") + left = recursive_conversion(item.left, configspace, target_parameter) + right = recursive_conversion(item.comparators, configspace, target_parameter) + operator = item.ops[0] + if isinstance(left, Hyperparameter): # Convert to HP type + if isinstance(right, Iterable) and not isinstance(right, str): + right = [type(left.default_value)(v) for v in right] + if len(right) == 1 and not isinstance(operator, ast.In): + right = right[0] + elif isinstance(right, int): + right = type(left.default_value)(right) + + is_relation = isinstance(left, Hyperparameter) and isinstance(right, Hyperparameter) + if is_relation and target_parameter: + raise ValueError("Hyperparameter relations not supported for conditions.") + + if isinstance(operator, ast.Lt): + if target_parameter: + return LessThanCondition(target_parameter, left, right) + if is_relation: + return ForbiddenLessThanRelation(left=left, right=right) + return ForbiddenLessThanClause(hyperparameter=left, value=right) + if isinstance(operator, ast.LtE): + if target_parameter: + raise ValueError("LessThanEquals not supported for conditions.") + if is_relation: + return ForbiddenLessThanEqualsRelation(left=left, right=right) + return ForbiddenLessThanEqualsClause(hyperparameter=left, value=right) + if isinstance(operator, ast.Gt): + if target_parameter: + return GreaterThanCondition(target_parameter, left, right) + if is_relation: + return ForbiddenGreaterThanRelation(left=left, right=right) + return ForbiddenGreaterThanClause(hyperparameter=left, value=right) + if isinstance(operator, ast.GtE): + if target_parameter: + raise ValueError("GreaterThanEquals not supported for conditions.") + if is_relation: + return ForbiddenGreaterThanEqualsRelation(left=left, right=right) + return ForbiddenGreaterThanEqualsClause(hyperparameter=left, value=right) + if isinstance(operator, ast.Eq): + if target_parameter: + return EqualsCondition(target_parameter, left, right) + if is_relation: + return ForbiddenEqualsRelation(left=left, right=right) + return ForbiddenEqualsClause(hyperparameter=left, value=right) + if isinstance(operator, ast.In): + if is_relation: + raise ValueError("In operator not supported for hyperparameter relations.") + if target_parameter: + return InCondition(target_parameter, left, right) + return ForbiddenInClause(hyperparameter=left, values=right) + if isinstance(operator, ast.NotEq): + if target_parameter: + return NotEqualsCondition(target_parameter, left, right) + raise ValueError("NotEq operator not supported for ForbiddenClauses.") + # The following classes do not (yet?) exist in configspace + if isinstance(operator, ast.NotIn): + raise ValueError("NotIn operator not supported for ForbiddenClauses.") + if isinstance(operator, ast.Is): + raise NotImplementedError("Is operator not supported.") + if isinstance(operator, ast.IsNot): + raise NotImplementedError("IsNot operator not supported.") + raise ValueError(f"Unsupported type: {item}") diff --git a/test/test_util.py b/test/test_util.py index 9ca17054..5d56fbb5 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -59,6 +59,7 @@ from ConfigSpace.util import ( change_hp_value, deactivate_inactive_hyperparameters, + expression_to_configspace, fix_types, generate_grid, get_one_exchange_neighbourhood, @@ -658,3 +659,55 @@ def test_generate_grid(): 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} + + +def test_expression_to_configspace(): + cs = ConfigurationSpace( + { + "a": (0, 10), + "b": (0, 10), + "c": (0, 10), + "d": (0, 10), + "e": (0, 10), + "cat1": ["cat", "dog"], + "cat2": ["sun", "rain", "snow", "fog"], + "float1": (0.0, 1.0), + "float2": (0.0, 1.0), + }, + ) + from ConfigSpace.conditions import LessThanCondition + from ConfigSpace.forbidden import ( + ForbiddenAndConjunction, + ForbiddenEqualsClause, + ForbiddenGreaterThanEqualsClause, + ForbiddenGreaterThanRelation, + ForbiddenLessThanClause, + ) + + wrong_expression = "a >!> b" + with pytest.raises(ValueError): + expression_to_configspace(wrong_expression, cs) + + simple_expression = "a > b" + cs_expression = expression_to_configspace(simple_expression, cs) + assert cs_expression == ForbiddenGreaterThanRelation(cs["a"], cs["b"]) + + simple_expression = "a < 5" + cs_expression = expression_to_configspace(simple_expression, cs) + assert cs_expression == ForbiddenLessThanClause(cs["a"], 5) + cs_expression = expression_to_configspace( + simple_expression, + cs, + target_parameter=cs["e"], + ) + assert cs_expression == LessThanCondition(cs["e"], cs["a"], 5) + + complex_expression = "a > b && c > d && e < 5 && cat1 == dog && float1 >= 0.5" + cs_expression = expression_to_configspace(complex_expression, cs) + assert cs_expression == ForbiddenAndConjunction( + ForbiddenGreaterThanRelation(cs["a"], cs["b"]), + ForbiddenGreaterThanRelation(cs["c"], cs["d"]), + ForbiddenLessThanClause(cs["e"], 5), + ForbiddenEqualsClause(cs["cat1"], "dog"), + ForbiddenGreaterThanEqualsClause(cs["float1"], 0.5), + ) From 7642320dc5811ccd2f11e605685f914ccfa7c625 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Tue, 30 Sep 2025 08:35:17 +0200 Subject: [PATCH 02/29] Expanding tests --- src/ConfigSpace/util.py | 6 +++++ test/test_util.py | 51 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index b0540e9a..b5a71f49 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -941,6 +941,7 @@ def recursive_conversion( left = recursive_conversion(item.left, configspace, target_parameter) right = recursive_conversion(item.comparators, configspace, target_parameter) operator = item.ops[0] + if isinstance(left, Hyperparameter): # Convert to HP type if isinstance(right, Iterable) and not isinstance(right, str): right = [type(left.default_value)(v) for v in right] @@ -948,6 +949,11 @@ def recursive_conversion( right = right[0] elif isinstance(right, int): right = type(left.default_value)(right) + elif not isinstance(right, Hyperparameter): + raise ValueError( + "Only hyperparameter comparisons allowed. Neither side is recognised as a hyperparameter in: " + f"{ast.unparse(item)}" + ) is_relation = isinstance(left, Hyperparameter) and isinstance(right, Hyperparameter) if is_relation and target_parameter: diff --git a/test/test_util.py b/test/test_util.py index 5d56fbb5..0e8fff96 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -679,15 +679,40 @@ def test_expression_to_configspace(): from ConfigSpace.forbidden import ( ForbiddenAndConjunction, ForbiddenEqualsClause, + ForbiddenGreaterThanClause, ForbiddenGreaterThanEqualsClause, ForbiddenGreaterThanRelation, + ForbiddenInClause, ForbiddenLessThanClause, + ForbiddenOrConjunction, ) wrong_expression = "a >!> b" with pytest.raises(ValueError): expression_to_configspace(wrong_expression, cs) + wrong_hp_name_expresion = "q <= 5" + with pytest.raises(ValueError): + cs_expression = expression_to_configspace(wrong_hp_name_expresion, cs) + + wrong_hp_value_expression = "a > 11" + with pytest.raises(ValueError): + cs_expression = expression_to_configspace(wrong_hp_value_expression, cs) + + wrong_hp_value_expression = "a == dog" + with pytest.raises(ValueError): + cs_expression = expression_to_configspace(wrong_hp_value_expression, cs) + + odd_operator_expression = ( + "a in [1, 2, 3]" # This operator is accepted by ConfigSpace for Integer HP + ) + cs_expression = expression_to_configspace(odd_operator_expression, cs) + assert cs_expression == ForbiddenInClause(cs["a"], [1, 2, 3]) + + simple_value_expression = "a > 9" + cs_expression = expression_to_configspace(simple_value_expression, cs) + assert cs_expression == ForbiddenGreaterThanClause(cs["a"], 9) + simple_expression = "a > b" cs_expression = expression_to_configspace(simple_expression, cs) assert cs_expression == ForbiddenGreaterThanRelation(cs["a"], cs["b"]) @@ -702,12 +727,26 @@ def test_expression_to_configspace(): ) assert cs_expression == LessThanCondition(cs["e"], cs["a"], 5) - complex_expression = "a > b && c > d && e < 5 && cat1 == dog && float1 >= 0.5" + complex_expression = "a > b || (c > d && e < 5 && cat1 == dog && float1 >= 0.5)" cs_expression = expression_to_configspace(complex_expression, cs) - assert cs_expression == ForbiddenAndConjunction( + assert cs_expression == ForbiddenOrConjunction( ForbiddenGreaterThanRelation(cs["a"], cs["b"]), - ForbiddenGreaterThanRelation(cs["c"], cs["d"]), - ForbiddenLessThanClause(cs["e"], 5), - ForbiddenEqualsClause(cs["cat1"], "dog"), - ForbiddenGreaterThanEqualsClause(cs["float1"], 0.5), + ForbiddenAndConjunction( + ForbiddenGreaterThanRelation(cs["c"], cs["d"]), + ForbiddenLessThanClause(cs["e"], 5), + ForbiddenEqualsClause(cs["cat1"], "dog"), + ForbiddenGreaterThanEqualsClause(cs["float1"], 0.5), + ), + ) + + complex_expression = ( + "a >= 8 and (cat1 in ['cat', 'dog'] or cat2 in ['sun', 'rain'])" + ) + cs_expression = expression_to_configspace(complex_expression, cs) + assert cs_expression == ForbiddenAndConjunction( + ForbiddenGreaterThanEqualsClause(cs["a"], 8), + ForbiddenOrConjunction( + ForbiddenInClause(cs["cat1"], ["cat", "dog"]), + ForbiddenInClause(cs["cat2"], ["sun", "rain"]), + ), ) From 753fa8da1fc88b6c3edfbc4fac4b092c1947929d Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Tue, 30 Sep 2025 09:14:01 +0200 Subject: [PATCH 03/29] Simplifying print statement as ast.unparse is only available from Python >= 3.9 --- src/ConfigSpace/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index b5a71f49..c595d712 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -952,7 +952,7 @@ def recursive_conversion( elif not isinstance(right, Hyperparameter): raise ValueError( "Only hyperparameter comparisons allowed. Neither side is recognised as a hyperparameter in: " - f"{ast.unparse(item)}" + f"{left} {operator} {right}" ) is_relation = isinstance(left, Hyperparameter) and isinstance(right, Hyperparameter) From 92d450d17bb107ffbabb3f9423a7a3cb4922f1f9 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Tue, 30 Sep 2025 09:31:47 +0200 Subject: [PATCH 04/29] Bugfixes --- src/ConfigSpace/util.py | 28 ++++++++++++++++++++++++---- test/test_util.py | 17 ++++++----------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index c595d712..eb3ac3d4 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -31,7 +31,7 @@ import copy from collections import deque from collections.abc import Iterator, Sequence -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, cast, Iterable import numpy as np @@ -52,9 +52,29 @@ UniformIntegerHyperparameter, ) -from ConfigSpace.conditions import * -from ConfigSpace.forbidden import * - +from ConfigSpace.conditions import ( + Condition, + AndConjunction, + OrConjunction, + EqualsCondition, + GreaterThanCondition, + LessThanCondition, + NotEqualsCondition, + InCondition, +) +from ConfigSpace.forbidden import ( + ForbiddenClause, + ForbiddenAndConjunction, + ForbiddenOrConjunction, + ForbiddenEqualsClause, + ForbiddenGreaterThanClause, + ForbiddenGreaterThanEqualsClause, + ForbiddenInClause, + ForbiddenLessThanClause, + ForbiddenLessThanEqualsClause, + ForbiddenGreaterThanRelation, + ForbiddenLessThanRelation, +) from ConfigSpace.types import NotSet diff --git a/test/test_util.py b/test/test_util.py index 0e8fff96..bbb83158 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -43,6 +43,12 @@ EqualsCondition, ForbiddenAndConjunction, ForbiddenEqualsClause, + ForbiddenGreaterThanClause, + ForbiddenGreaterThanEqualsClause, + ForbiddenGreaterThanRelation, + ForbiddenInClause, + ForbiddenLessThanClause, + ForbiddenOrConjunction, GreaterThanCondition, LessThanCondition, OrConjunction, @@ -675,17 +681,6 @@ def test_expression_to_configspace(): "float2": (0.0, 1.0), }, ) - from ConfigSpace.conditions import LessThanCondition - from ConfigSpace.forbidden import ( - ForbiddenAndConjunction, - ForbiddenEqualsClause, - ForbiddenGreaterThanClause, - ForbiddenGreaterThanEqualsClause, - ForbiddenGreaterThanRelation, - ForbiddenInClause, - ForbiddenLessThanClause, - ForbiddenOrConjunction, - ) wrong_expression = "a >!> b" with pytest.raises(ValueError): From de359955b78630350f1b0fbf8e9515db9d6dd223 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Fri, 19 Dec 2025 14:34:05 +0100 Subject: [PATCH 05/29] Updating documentation --- src/ConfigSpace/util.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index eb3ac3d4..529de17d 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -867,6 +867,21 @@ def expression_to_configspace( ) -> ForbiddenClause | Condition: """Convert a logic expression to ConfigSpace expression. + Given a logic expression, this function will return a ConfigSpace expression + that is equivalent to the logic expression. If a target parameter is provided, + will create a condition, otherwise a forbidden expression. + + The created expression is **NOT** automatically added to the configuration space. + + ```python exec="true", source="material-block" result="python" + from ConfigSpace.util import expression_to_configspace + + cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) }) + cond = LessThanCondition(cs['b'], cs['a'], 5) + condition = expression_to_configspace("a < 5", cs, target_parameter=cs['b']) + print(condition) + ``` + Args: expression: The expression to convert. configspace: The ConfigSpace to use. @@ -902,6 +917,8 @@ def recursive_conversion( target_parameter: Hyperparameter = None, ) -> ForbiddenClause | Condition: """Recursively parse the abstract syntax tree to a ConfigSpace expression. + + Should not be called directly, but rather through `expression_to_configspace`. Args: item: The item to parse. From be3db2db5465da4d083100111cf4477487738735 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 5 Jan 2026 10:11:16 +0100 Subject: [PATCH 06/29] Cleaning up code, expanding docs --- src/ConfigSpace/util.py | 107 ++++++++++++++++++++++------------------ test/test_util.py | 2 +- 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 529de17d..33f76d9f 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -28,6 +28,7 @@ from __future__ import annotations import ast +import re import copy from collections import deque from collections.abc import Iterator, Sequence @@ -861,10 +862,10 @@ def _get_cartesian_product( def expression_to_configspace( - expression: str | ast.Module, + expression: str, configspace: ConfigurationSpace, - target_parameter: Hyperparameter = None, -) -> ForbiddenClause | Condition: + target_hyperparameter: Hyperparameter = None, +) -> Condition | ForbiddenClause: """Convert a logic expression to ConfigSpace expression. Given a logic expression, this function will return a ConfigSpace expression @@ -873,49 +874,61 @@ def expression_to_configspace( The created expression is **NOT** automatically added to the configuration space. + Example Condition expression parsing: + ```python exec="true", source="material-block" result="python" + from ConfigSpace import ConfigurationSpace from ConfigSpace.util import expression_to_configspace cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) }) - cond = LessThanCondition(cs['b'], cs['a'], 5) condition = expression_to_configspace("a < 5", cs, target_parameter=cs['b']) print(condition) ``` + Example Forbidden Expression Parsing: + + ```python exec="true", source="material-block" result="python" + from ConfigSpace import ConfigurationSpace + from ConfigSpace.util import expression_to_configspace + + cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) }) + forbidden = expression_to_configspace("a >= 5", cs) + print(forbidden) + ``` + Args: expression: The expression to convert. configspace: The ConfigSpace to use. target_parameter: For conditions, will parse the expression as a condition - underwhich the parameter will be active. + underwhich the provided hyperparameter will be active. + + Returns: + A ConfigSpace Condition or ForbiddenClause. """ - if isinstance(expression, str): - import re - # Format expression to match the ast module - # Format logical operators: - expression = re.sub(r" & ", " and ", expression) - expression = re.sub(r" && ", " and ", expression) - expression = re.sub(r" \| ", " or ", expression) - expression = re.sub(r" \|\| ", " or ", expression) - # Format (in)equality operators: - expression = re.sub(r" !== ", " != ", expression) - expression = re.sub(r" != ", " = ", expression) - expression = re.sub(r" (?!=])=(?!=])=(? ForbiddenClause | Condition: + target_hyperparameter: Hyperparameter = None, +) -> Condition | ForbiddenClause: """Recursively parse the abstract syntax tree to a ConfigSpace expression. Should not be called directly, but rather through `expression_to_configspace`. @@ -924,17 +937,17 @@ def recursive_conversion( item: The item to parse. configspace: The ConfigSpace to use. target_parameter: For conditions, will parse the expression as a condition - underwhich the parameter will be active. + underwhich the hyperparameter will be active. Returns: - A ConfigSpace expression + A ConfigSpace Condition or ForbiddenClause """ if isinstance(item, list): if len(item) > 1: raise ValueError(f"Can not parse list of elements: {item}.") item = item[0] if isinstance(item, ast.Expr): - return recursive_conversion(item.value, configspace, target_parameter) + return recursive_conversion(item.value, configspace, target_hyperparameter) if isinstance(item, ast.Name): # Convert to hyperparameter hp = configspace.get(item.id) return hp if hp is not None else item.id @@ -960,14 +973,14 @@ def recursive_conversion( raise NotImplementedError("Binary operations not supported by ConfigSpace.") if isinstance(item, ast.BoolOp): values = [ - recursive_conversion(v, configspace, target_parameter) for v in item.values + recursive_conversion(v, configspace, target_hyperparameter) for v in item.values ] if isinstance(item.op, ast.Or): - if target_parameter: + if target_hyperparameter: return OrConjunction(*values) return ForbiddenOrConjunction(*values) elif isinstance(item.op, ast.And): - if target_parameter: + if target_hyperparameter: return AndConjunction(*values) return ForbiddenAndConjunction(*values) else: @@ -975,8 +988,8 @@ def recursive_conversion( if isinstance(item, ast.Compare): if len(item.ops) > 1: raise ValueError(f"Only single comparisons allowed. Found: {item.ops}") - left = recursive_conversion(item.left, configspace, target_parameter) - right = recursive_conversion(item.comparators, configspace, target_parameter) + left = recursive_conversion(item.left, configspace, target_hyperparameter) + right = recursive_conversion(item.comparators, configspace, target_hyperparameter) operator = item.ops[0] if isinstance(left, Hyperparameter): # Convert to HP type @@ -993,48 +1006,48 @@ def recursive_conversion( ) is_relation = isinstance(left, Hyperparameter) and isinstance(right, Hyperparameter) - if is_relation and target_parameter: + if is_relation and target_hyperparameter: raise ValueError("Hyperparameter relations not supported for conditions.") if isinstance(operator, ast.Lt): - if target_parameter: - return LessThanCondition(target_parameter, left, right) + if target_hyperparameter: + return LessThanCondition(target_hyperparameter, left, right) if is_relation: return ForbiddenLessThanRelation(left=left, right=right) return ForbiddenLessThanClause(hyperparameter=left, value=right) if isinstance(operator, ast.LtE): - if target_parameter: + if target_hyperparameter: raise ValueError("LessThanEquals not supported for conditions.") if is_relation: return ForbiddenLessThanEqualsRelation(left=left, right=right) return ForbiddenLessThanEqualsClause(hyperparameter=left, value=right) if isinstance(operator, ast.Gt): - if target_parameter: - return GreaterThanCondition(target_parameter, left, right) + if target_hyperparameter: + return GreaterThanCondition(target_hyperparameter, left, right) if is_relation: return ForbiddenGreaterThanRelation(left=left, right=right) return ForbiddenGreaterThanClause(hyperparameter=left, value=right) if isinstance(operator, ast.GtE): - if target_parameter: + if target_hyperparameter: raise ValueError("GreaterThanEquals not supported for conditions.") if is_relation: return ForbiddenGreaterThanEqualsRelation(left=left, right=right) return ForbiddenGreaterThanEqualsClause(hyperparameter=left, value=right) if isinstance(operator, ast.Eq): - if target_parameter: - return EqualsCondition(target_parameter, left, right) + if target_hyperparameter: + return EqualsCondition(target_hyperparameter, left, right) if is_relation: return ForbiddenEqualsRelation(left=left, right=right) return ForbiddenEqualsClause(hyperparameter=left, value=right) if isinstance(operator, ast.In): if is_relation: raise ValueError("In operator not supported for hyperparameter relations.") - if target_parameter: - return InCondition(target_parameter, left, right) + if target_hyperparameter: + return InCondition(target_hyperparameter, left, right) return ForbiddenInClause(hyperparameter=left, values=right) if isinstance(operator, ast.NotEq): - if target_parameter: - return NotEqualsCondition(target_parameter, left, right) + if target_hyperparameter: + return NotEqualsCondition(target_hyperparameter, left, right) raise ValueError("NotEq operator not supported for ForbiddenClauses.") # The following classes do not (yet?) exist in configspace if isinstance(operator, ast.NotIn): diff --git a/test/test_util.py b/test/test_util.py index bbb83158..135c2daa 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -718,7 +718,7 @@ def test_expression_to_configspace(): cs_expression = expression_to_configspace( simple_expression, cs, - target_parameter=cs["e"], + target_hyperparameter=cs["e"], ) assert cs_expression == LessThanCondition(cs["e"], cs["a"], 5) From 4b344dc93fa68ee2b73f5b1f33ebe48fffca0c6a Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 5 Jan 2026 10:15:57 +0100 Subject: [PATCH 07/29] bugfix --- src/ConfigSpace/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 33f76d9f..e4d42d73 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -916,7 +916,8 @@ def expression_to_configspace( expression = re.sub(r" != ", " = ", expression) expression = re.sub(r" (?!=])=(? Date: Mon, 5 Jan 2026 10:17:37 +0100 Subject: [PATCH 08/29] docs typo --- src/ConfigSpace/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index e4d42d73..07cedd60 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -881,7 +881,7 @@ def expression_to_configspace( from ConfigSpace.util import expression_to_configspace cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) }) - condition = expression_to_configspace("a < 5", cs, target_parameter=cs['b']) + condition = expression_to_configspace("a < 5", cs, target_hyperparameter=cs['b']) print(condition) ``` @@ -899,7 +899,7 @@ def expression_to_configspace( Args: expression: The expression to convert. configspace: The ConfigSpace to use. - target_parameter: For conditions, will parse the expression as a condition + target_hyperparameter: For conditions, will parse the expression as a condition underwhich the provided hyperparameter will be active. Returns: @@ -937,7 +937,7 @@ def recursive_conversion( Args: item: The item to parse. configspace: The ConfigSpace to use. - target_parameter: For conditions, will parse the expression as a condition + target_hyperparameter: For conditions, will parse the expression as a condition underwhich the hyperparameter will be active. Returns: From 76d6617356942b0311b6e55112d0508effbe5f64 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 16 Mar 2026 14:15:15 +0100 Subject: [PATCH 09/29] Adding docs, minor fixes --- docs/reference/util.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/reference/utils.md | 0 src/ConfigSpace/util.py | 1 - 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 docs/reference/util.md delete mode 100644 docs/reference/utils.md diff --git a/docs/reference/util.md b/docs/reference/util.md new file mode 100644 index 00000000..c58930fe --- /dev/null +++ b/docs/reference/util.md @@ -0,0 +1,41 @@ +## Utilities + +### Expression to Configspace + +In some cases we may have (highly) complex conditions or forbidden expressions that are already denoted as a regular expression. In that case, `ConfigSpace` can automatically convert them into a `ConfigSpace` expression using the [`expression_to_configspace`][ConfigSpace.utils.expression_to_configspace]`expression_to_configspace`. This function interprets the expression using the Python `Abstract Syntax Tree` parser and recursively converts it into the appropriate structure. + +!!! note + The converted expression is not added to ConfigSpace, only returned to the user. + +!!! note + If the expression contains illegal values, errors, or requires functionalities not available in `ConfigSpace`, appriopriate exceptions will be raised. + +```python exec="True" result="python" source="tabbed-left" +from ConfigSpace import ConfigurationSpace +from ConfigSpace.util import expression_to_configspace + +cs = ConfigurationSpace( + { + "a": (0, 10), # Integer from 0 to 10 + "b": ["cat", "dog"], # Categorical with choices "cat" and "dog" + "c": (0.0, 1.0), # Float from 0.0 to 1.0 + } +) +print(cs) + +# Now we add a condition and forbidden using regular expressions +condition = "b != cat && c >= 0.001" +condition = expression_to_configspace(condition, cs, target_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument + +print(condition) + +forbidden = "a > 5 && c >= 0.94" +forbidden = expression_to_configspace(forbidden, cs) + +print(forbidden) + +cs.add(condition) +cs.add(forbidden) + +print(cs) +``` diff --git a/docs/reference/utils.md b/docs/reference/utils.md deleted file mode 100644 index e69de29b..00000000 diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 07cedd60..15d8eb80 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -913,7 +913,6 @@ def expression_to_configspace( expression = re.sub(r" \|\| ", " or ", expression) # Format (in)equality operators: expression = re.sub(r" !== ", " != ", expression) - expression = re.sub(r" != ", " = ", expression) expression = re.sub(r" (?!=])=(? Date: Mon, 16 Mar 2026 14:16:59 +0100 Subject: [PATCH 10/29] Fixing example --- t.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 t.py diff --git a/t.py b/t.py new file mode 100644 index 00000000..7e8b6a21 --- /dev/null +++ b/t.py @@ -0,0 +1,26 @@ +from ConfigSpace import ConfigurationSpace +from ConfigSpace.util import expression_to_configspace + +cs = ConfigurationSpace( + { + "a": (0, 10), # Integer from 0 to 10 + "b": ["cat", "dog"], # Categorical with choices "cat" and "dog" + "c": (0.0, 1.0), # Float from 0.0 to 1.0 + } +) +print(cs) + +# Now we add a condition and forbidden using regular expressions +condition = "b != cat && c > 0.001" +condition = expression_to_configspace(condition, cs, target_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument + +print(condition) +forbidden = "a > 5 && c >= 0.94" +forbidden = expression_to_configspace(forbidden, cs) + +print(forbidden) + +cs.add(condition) +cs.add(forbidden) + +print(cs) \ No newline at end of file From 2f1c563b390af003533919ceee80d0f7b1d3552e Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 16 Mar 2026 14:18:44 +0100 Subject: [PATCH 11/29] Remove mistake file --- t.py | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 t.py diff --git a/t.py b/t.py deleted file mode 100644 index 7e8b6a21..00000000 --- a/t.py +++ /dev/null @@ -1,26 +0,0 @@ -from ConfigSpace import ConfigurationSpace -from ConfigSpace.util import expression_to_configspace - -cs = ConfigurationSpace( - { - "a": (0, 10), # Integer from 0 to 10 - "b": ["cat", "dog"], # Categorical with choices "cat" and "dog" - "c": (0.0, 1.0), # Float from 0.0 to 1.0 - } -) -print(cs) - -# Now we add a condition and forbidden using regular expressions -condition = "b != cat && c > 0.001" -condition = expression_to_configspace(condition, cs, target_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument - -print(condition) -forbidden = "a > 5 && c >= 0.94" -forbidden = expression_to_configspace(forbidden, cs) - -print(forbidden) - -cs.add(condition) -cs.add(forbidden) - -print(cs) \ No newline at end of file From 995f02eb44861d1bc7a43b7fb040f9e962d65503 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 16 Mar 2026 14:21:08 +0100 Subject: [PATCH 12/29] docfix --- docs/reference/util.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/util.md b/docs/reference/util.md index c58930fe..0627ca0b 100644 --- a/docs/reference/util.md +++ b/docs/reference/util.md @@ -24,7 +24,7 @@ cs = ConfigurationSpace( print(cs) # Now we add a condition and forbidden using regular expressions -condition = "b != cat && c >= 0.001" +condition = "b != cat && c > 0.001" condition = expression_to_configspace(condition, cs, target_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument print(condition) From 96f6dca34d67d17c89d79c90f7d71b8eff2672d1 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman <32924404+thijssnelleman@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:30:43 +0100 Subject: [PATCH 13/29] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/ConfigSpace/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 52350e74..d1956ea2 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -75,6 +75,9 @@ ForbiddenLessThanEqualsClause, ForbiddenGreaterThanRelation, ForbiddenLessThanRelation, + ForbiddenEqualsRelation, + ForbiddenGreaterThanEqualsRelation, + ForbiddenLessThanEqualsRelation, ) from ConfigSpace.types import NotSet From a9c9f3602aea5ac9ce9cc22a89f66d1ac2634e8c Mon Sep 17 00:00:00 2001 From: Thijs Snelleman <32924404+thijssnelleman@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:31:48 +0100 Subject: [PATCH 14/29] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/ConfigSpace/util.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index d1956ea2..b7a7ad76 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -991,6 +991,42 @@ def recursive_conversion( right = recursive_conversion(item.comparators, configspace, target_hyperparameter) operator = item.ops[0] + # Ensure that if there is exactly one Hyperparameter involved in the + # comparison, it is always on the left-hand side. This is required + # because the downstream Condition/Forbidden* constructors expect the + # hyperparameter to be passed as the "left" argument. + if isinstance(right, Hyperparameter) and not isinstance(left, Hyperparameter): + # Normalize expressions like "5 < hp" into "hp > 5" by swapping + # sides and inverting asymmetric operators. For symmetric + # operators (==, !=), we can swap without changing the operator. + if isinstance(operator, ast.Lt): + left, right = right, left + operator = ast.Gt() + elif isinstance(operator, ast.LtE): + left, right = right, left + operator = ast.GtE() + elif isinstance(operator, ast.Gt): + left, right = right, left + operator = ast.Lt() + elif isinstance(operator, ast.GtE): + left, right = right, left + operator = ast.LtE() + elif isinstance(operator, (ast.Eq, ast.NotEq)): + # Equality and inequality are symmetric; no operator change + left, right = right, left + elif isinstance(operator, ast.In): + # Having a Hyperparameter only on the right-hand side of an + # "in" comparison (e.g. "[1, 2] in hp") is not supported. + raise ValueError( + "Invalid comparison: 'in' operator requires a hyperparameter " + "on the left-hand side." + ) + else: + # For any other unsupported operator shapes, fail fast. + raise ValueError( + f"Unsupported comparison between constant and hyperparameter: {left} {operator} {right}" + ) + if isinstance(left, Hyperparameter): # Convert to HP type if isinstance(right, Iterable) and not isinstance(right, str): right = [type(left.default_value)(v) for v in right] From bb199bb805453b7be9ca8f8d955894252c65e8d5 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 16 Mar 2026 14:40:56 +0100 Subject: [PATCH 15/29] Updating copilot fix, adding tests --- src/ConfigSpace/util.py | 25 ++++++---------------- test/test_util.py | 47 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index b7a7ad76..dc94e2c4 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -991,38 +991,27 @@ def recursive_conversion( right = recursive_conversion(item.comparators, configspace, target_hyperparameter) operator = item.ops[0] - # Ensure that if there is exactly one Hyperparameter involved in the - # comparison, it is always on the left-hand side. This is required - # because the downstream Condition/Forbidden* constructors expect the - # hyperparameter to be passed as the "left" argument. + # CoPilot: Ensure that if there is exactly one Hyperparameter involved in the comparison, it is always on the left-hand side. This is required + # because the downstream Condition/Forbidden* constructors expect the hyperparameter to be passed as the "left" argument. if isinstance(right, Hyperparameter) and not isinstance(left, Hyperparameter): - # Normalize expressions like "5 < hp" into "hp > 5" by swapping - # sides and inverting asymmetric operators. For symmetric - # operators (==, !=), we can swap without changing the operator. + # Normalize expressions like "5 < hp" into "hp > 5" by swapping sides and inverting asymmetric operators. + left, right = right, left if isinstance(operator, ast.Lt): - left, right = right, left operator = ast.Gt() elif isinstance(operator, ast.LtE): - left, right = right, left operator = ast.GtE() elif isinstance(operator, ast.Gt): - left, right = right, left operator = ast.Lt() elif isinstance(operator, ast.GtE): - left, right = right, left operator = ast.LtE() - elif isinstance(operator, (ast.Eq, ast.NotEq)): - # Equality and inequality are symmetric; no operator change - left, right = right, left elif isinstance(operator, ast.In): - # Having a Hyperparameter only on the right-hand side of an - # "in" comparison (e.g. "[1, 2] in hp") is not supported. + # Having a Hyperparameter only on the right-hand side of an "in" comparison (e.g. "[1, 2] in hp") is not supported. raise ValueError( "Invalid comparison: 'in' operator requires a hyperparameter " "on the left-hand side." ) - else: - # For any other unsupported operator shapes, fail fast. + elif not isinstance(operator, (ast.Eq, ast.NotEq)): # Equality and inequality are symmetric; no operator change + # For any other unsupported operator shapes, fail. raise ValueError( f"Unsupported comparison between constant and hyperparameter: {left} {operator} {right}" ) diff --git a/test/test_util.py b/test/test_util.py index 918240df..6cebebc5 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -51,6 +51,7 @@ ForbiddenOrConjunction, GreaterThanCondition, LessThanCondition, + NotEqualsCondition, OrConjunction, OrdinalHyperparameter, UniformFloatHyperparameter, @@ -698,10 +699,44 @@ def test_expression_to_configspace(): with pytest.raises(ValueError): cs_expression = expression_to_configspace(wrong_hp_value_expression, cs) - odd_operator_expression = ( + wrong_forbidden_expression = "a != 5" + with pytest.raises(ValueError): + expression_to_configspace(wrong_forbidden_expression, cs) + + # In case the epxression is incorrecty ordered for ConfigSpace, the method fixes the ordering here where possible + wrong_order_expression = "5 < a" + assert expression_to_configspace( + wrong_order_expression, + cs, + ) == ForbiddenGreaterThanClause(cs["a"], 5) + + wrong_order_expression = "5 > a" + assert expression_to_configspace( + wrong_order_expression, + cs, + ) == ForbiddenLessThanClause(cs["a"], 5) + + wrong_order_expression = "5 == a" + assert expression_to_configspace( + wrong_order_expression, + cs, + ) == ForbiddenEqualsClause(cs["a"], 5) + + wrong_order_expression = "5 != a" + assert expression_to_configspace( + wrong_order_expression, + cs, + target_hyperparameter=cs["e"], + ) == NotEqualsCondition(cs["e"], cs["a"], 5) + + wrong_order_expression = "[1,2,5] in a" + with pytest.raises(ValueError): + expression_to_configspace(wrong_order_expression, cs) + + in_operator_expression = ( "a in [1, 2, 3]" # This operator is accepted by ConfigSpace for Integer HP ) - cs_expression = expression_to_configspace(odd_operator_expression, cs) + cs_expression = expression_to_configspace(in_operator_expression, cs) assert cs_expression == ForbiddenInClause(cs["a"], [1, 2, 3]) simple_value_expression = "a > 9" @@ -722,6 +757,14 @@ def test_expression_to_configspace(): ) assert cs_expression == LessThanCondition(cs["e"], cs["a"], 5) + simple_expression_inequality = "a != 5" + cs_expression = expression_to_configspace( + simple_expression_inequality, + cs, + target_hyperparameter=cs["e"], + ) + assert cs_expression == NotEqualsCondition(cs["e"], cs["a"], 5) + complex_expression = "a > b || (c > d && e < 5 && cat1 == dog && float1 >= 0.5)" cs_expression = expression_to_configspace(complex_expression, cs) assert cs_expression == ForbiddenOrConjunction( From 1f1a609e9d8f36be13473ae8595d1b5a13ead5d4 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 16 Mar 2026 14:44:25 +0100 Subject: [PATCH 16/29] Bugfixes --- src/ConfigSpace/util.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index dc94e2c4..9c63e71a 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -863,7 +863,7 @@ def _get_cartesian_product( def expression_to_configspace( expression: str, configspace: ConfigurationSpace, - target_hyperparameter: Hyperparameter = None, + target_hyperparameter: Hyperparameter | None = None, ) -> Condition | ForbiddenClause: """Convert a logic expression to ConfigSpace expression. @@ -918,15 +918,15 @@ def expression_to_configspace( ast_expression = ast.parse(expression).body[0] except Exception as e: raise ValueError(f"Could not parse expression: '{expression}', {e}") - return recursive_conversion( + return _recursive_conversion( ast_expression, configspace, target_hyperparameter=target_hyperparameter ) -def recursive_conversion( - item: ast.mod, +def _recursive_conversion( + item: ast.AST | list[ast.AST], configspace: ConfigurationSpace, - target_hyperparameter: Hyperparameter = None, + target_hyperparameter: Hyperparameter | None = None, ) -> Condition | ForbiddenClause: """Recursively parse the abstract syntax tree to a ConfigSpace expression. @@ -946,7 +946,7 @@ def recursive_conversion( raise ValueError(f"Can not parse list of elements: {item}.") item = item[0] if isinstance(item, ast.Expr): - return recursive_conversion(item.value, configspace, target_hyperparameter) + return _recursive_conversion(item.value, configspace, target_hyperparameter) if isinstance(item, ast.Name): # Convert to hyperparameter hp = configspace.get(item.id) return hp if hp is not None else item.id @@ -962,7 +962,7 @@ def recursive_conversion( if isinstance(v, ast.Constant): values.append(v.value) elif isinstance(v, ast.Name): # Check if its a parameter - if v.id in list(configspace.values()): + if configspace.get(v.id) is not None: raise ValueError( f"Only constants allowed in tuples. Found: {item.elts}" ) @@ -972,7 +972,7 @@ def recursive_conversion( raise NotImplementedError("Binary operations not supported by ConfigSpace.") if isinstance(item, ast.BoolOp): values = [ - recursive_conversion(v, configspace, target_hyperparameter) for v in item.values + _recursive_conversion(v, configspace, target_hyperparameter) for v in item.values ] if isinstance(item.op, ast.Or): if target_hyperparameter: @@ -987,8 +987,8 @@ def recursive_conversion( if isinstance(item, ast.Compare): if len(item.ops) > 1: raise ValueError(f"Only single comparisons allowed. Found: {item.ops}") - left = recursive_conversion(item.left, configspace, target_hyperparameter) - right = recursive_conversion(item.comparators, configspace, target_hyperparameter) + left = _recursive_conversion(item.left, configspace, target_hyperparameter) + right = _recursive_conversion(item.comparators, configspace, target_hyperparameter) operator = item.ops[0] # CoPilot: Ensure that if there is exactly one Hyperparameter involved in the comparison, it is always on the left-hand side. This is required From 3d1f4ba80d5e02d9a9c9dca3ce0c8ea045d1b218 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 16 Mar 2026 14:44:50 +0100 Subject: [PATCH 17/29] docfix --- docs/reference/util.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/util.md b/docs/reference/util.md index 0627ca0b..19916972 100644 --- a/docs/reference/util.md +++ b/docs/reference/util.md @@ -2,7 +2,7 @@ ### Expression to Configspace -In some cases we may have (highly) complex conditions or forbidden expressions that are already denoted as a regular expression. In that case, `ConfigSpace` can automatically convert them into a `ConfigSpace` expression using the [`expression_to_configspace`][ConfigSpace.utils.expression_to_configspace]`expression_to_configspace`. This function interprets the expression using the Python `Abstract Syntax Tree` parser and recursively converts it into the appropriate structure. +In some cases we may have (highly) complex conditions or forbidden expressions that are already denoted as a regular expression. In that case, `ConfigSpace` can automatically convert them into a `ConfigSpace` expression using the [`expression_to_configspace`][ConfigSpace.util.expression_to_configspace]`expression_to_configspace`. This function interprets the expression using the Python `Abstract Syntax Tree` parser and recursively converts it into the appropriate structure. !!! note The converted expression is not added to ConfigSpace, only returned to the user. From 2192f7f09d472fb9c02c30687f45fd135f7057fc Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Mon, 16 Mar 2026 14:46:40 +0100 Subject: [PATCH 18/29] reference fix --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index b6b453ed..d30c4f23 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -173,5 +173,5 @@ nav: - Conditions: "reference/conditions.md" - Forbidden Clauses: "reference/forbiddens.md" - Serialization: "reference/serialization.md" - - Util: "reference/utils.md" + - Util: "reference/util.md" - API: "api/" From fc11a3bac5ee313ab52cac1dd70de9211f6567fd Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Tue, 17 Mar 2026 14:41:42 +0100 Subject: [PATCH 19/29] Parameter rename --- docs/reference/util.md | 2 +- src/ConfigSpace/util.py | 50 ++++++++++++++++++++--------------------- test/test_util.py | 6 ++--- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/reference/util.md b/docs/reference/util.md index 19916972..e850215f 100644 --- a/docs/reference/util.md +++ b/docs/reference/util.md @@ -25,7 +25,7 @@ print(cs) # Now we add a condition and forbidden using regular expressions condition = "b != cat && c > 0.001" -condition = expression_to_configspace(condition, cs, target_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument +condition = expression_to_configspace(condition, cs, conditional_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument print(condition) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 9c63e71a..faeede4a 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -863,7 +863,7 @@ def _get_cartesian_product( def expression_to_configspace( expression: str, configspace: ConfigurationSpace, - target_hyperparameter: Hyperparameter | None = None, + conditional_hyperparameter: Hyperparameter | None = None, ) -> Condition | ForbiddenClause: """Convert a logic expression to ConfigSpace expression. @@ -880,7 +880,7 @@ def expression_to_configspace( from ConfigSpace.util import expression_to_configspace cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) }) - condition = expression_to_configspace("a < 5", cs, target_hyperparameter=cs['b']) + condition = expression_to_configspace("a < 5", cs, conditional_hyperparameter=cs['b']) print(condition) ``` @@ -898,7 +898,7 @@ def expression_to_configspace( Args: expression: The expression to convert. configspace: The ConfigSpace to use. - target_hyperparameter: For conditions, will parse the expression as a condition + conditional_hyperparameter: For conditions, will parse the expression as a condition underwhich the provided hyperparameter will be active. Returns: @@ -919,14 +919,14 @@ def expression_to_configspace( except Exception as e: raise ValueError(f"Could not parse expression: '{expression}', {e}") return _recursive_conversion( - ast_expression, configspace, target_hyperparameter=target_hyperparameter + ast_expression, configspace, conditional_hyperparameter=conditional_hyperparameter ) def _recursive_conversion( item: ast.AST | list[ast.AST], configspace: ConfigurationSpace, - target_hyperparameter: Hyperparameter | None = None, + conditional_hyperparameter: Hyperparameter | None = None, ) -> Condition | ForbiddenClause: """Recursively parse the abstract syntax tree to a ConfigSpace expression. @@ -935,7 +935,7 @@ def _recursive_conversion( Args: item: The item to parse. configspace: The ConfigSpace to use. - target_hyperparameter: For conditions, will parse the expression as a condition + conditional_hyperparameter: For conditions, will parse the expression as a condition underwhich the hyperparameter will be active. Returns: @@ -946,7 +946,7 @@ def _recursive_conversion( raise ValueError(f"Can not parse list of elements: {item}.") item = item[0] if isinstance(item, ast.Expr): - return _recursive_conversion(item.value, configspace, target_hyperparameter) + return _recursive_conversion(item.value, configspace, conditional_hyperparameter) if isinstance(item, ast.Name): # Convert to hyperparameter hp = configspace.get(item.id) return hp if hp is not None else item.id @@ -972,14 +972,14 @@ def _recursive_conversion( raise NotImplementedError("Binary operations not supported by ConfigSpace.") if isinstance(item, ast.BoolOp): values = [ - _recursive_conversion(v, configspace, target_hyperparameter) for v in item.values + _recursive_conversion(v, configspace, conditional_hyperparameter) for v in item.values ] if isinstance(item.op, ast.Or): - if target_hyperparameter: + if conditional_hyperparameter: return OrConjunction(*values) return ForbiddenOrConjunction(*values) elif isinstance(item.op, ast.And): - if target_hyperparameter: + if conditional_hyperparameter: return AndConjunction(*values) return ForbiddenAndConjunction(*values) else: @@ -987,8 +987,8 @@ def _recursive_conversion( if isinstance(item, ast.Compare): if len(item.ops) > 1: raise ValueError(f"Only single comparisons allowed. Found: {item.ops}") - left = _recursive_conversion(item.left, configspace, target_hyperparameter) - right = _recursive_conversion(item.comparators, configspace, target_hyperparameter) + left = _recursive_conversion(item.left, configspace, conditional_hyperparameter) + right = _recursive_conversion(item.comparators, configspace, conditional_hyperparameter) operator = item.ops[0] # CoPilot: Ensure that if there is exactly one Hyperparameter involved in the comparison, it is always on the left-hand side. This is required @@ -1030,48 +1030,48 @@ def _recursive_conversion( ) is_relation = isinstance(left, Hyperparameter) and isinstance(right, Hyperparameter) - if is_relation and target_hyperparameter: + if is_relation and conditional_hyperparameter: raise ValueError("Hyperparameter relations not supported for conditions.") if isinstance(operator, ast.Lt): - if target_hyperparameter: - return LessThanCondition(target_hyperparameter, left, right) + if conditional_hyperparameter: + return LessThanCondition(conditional_hyperparameter, left, right) if is_relation: return ForbiddenLessThanRelation(left=left, right=right) return ForbiddenLessThanClause(hyperparameter=left, value=right) if isinstance(operator, ast.LtE): - if target_hyperparameter: + if conditional_hyperparameter: raise ValueError("LessThanEquals not supported for conditions.") if is_relation: return ForbiddenLessThanEqualsRelation(left=left, right=right) return ForbiddenLessThanEqualsClause(hyperparameter=left, value=right) if isinstance(operator, ast.Gt): - if target_hyperparameter: - return GreaterThanCondition(target_hyperparameter, left, right) + if conditional_hyperparameter: + return GreaterThanCondition(conditional_hyperparameter, left, right) if is_relation: return ForbiddenGreaterThanRelation(left=left, right=right) return ForbiddenGreaterThanClause(hyperparameter=left, value=right) if isinstance(operator, ast.GtE): - if target_hyperparameter: + if conditional_hyperparameter: raise ValueError("GreaterThanEquals not supported for conditions.") if is_relation: return ForbiddenGreaterThanEqualsRelation(left=left, right=right) return ForbiddenGreaterThanEqualsClause(hyperparameter=left, value=right) if isinstance(operator, ast.Eq): - if target_hyperparameter: - return EqualsCondition(target_hyperparameter, left, right) + if conditional_hyperparameter: + return EqualsCondition(conditional_hyperparameter, left, right) if is_relation: return ForbiddenEqualsRelation(left=left, right=right) return ForbiddenEqualsClause(hyperparameter=left, value=right) if isinstance(operator, ast.In): if is_relation: raise ValueError("In operator not supported for hyperparameter relations.") - if target_hyperparameter: - return InCondition(target_hyperparameter, left, right) + if conditional_hyperparameter: + return InCondition(conditional_hyperparameter, left, right) return ForbiddenInClause(hyperparameter=left, values=right) if isinstance(operator, ast.NotEq): - if target_hyperparameter: - return NotEqualsCondition(target_hyperparameter, left, right) + if conditional_hyperparameter: + return NotEqualsCondition(conditional_hyperparameter, left, right) raise ValueError("NotEq operator not supported for ForbiddenClauses.") # The following classes do not (yet?) exist in configspace if isinstance(operator, ast.NotIn): diff --git a/test/test_util.py b/test/test_util.py index 6cebebc5..67319d2a 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -726,7 +726,7 @@ def test_expression_to_configspace(): assert expression_to_configspace( wrong_order_expression, cs, - target_hyperparameter=cs["e"], + conditional_hyperparameter=cs["e"], ) == NotEqualsCondition(cs["e"], cs["a"], 5) wrong_order_expression = "[1,2,5] in a" @@ -753,7 +753,7 @@ def test_expression_to_configspace(): cs_expression = expression_to_configspace( simple_expression, cs, - target_hyperparameter=cs["e"], + conditional_hyperparameter=cs["e"], ) assert cs_expression == LessThanCondition(cs["e"], cs["a"], 5) @@ -761,7 +761,7 @@ def test_expression_to_configspace(): cs_expression = expression_to_configspace( simple_expression_inequality, cs, - target_hyperparameter=cs["e"], + conditional_hyperparameter=cs["e"], ) assert cs_expression == NotEqualsCondition(cs["e"], cs["a"], 5) From d2ebe05a8a504f39d50189ea1e50b0135842841d Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Wed, 1 Apr 2026 17:35:39 +0200 Subject: [PATCH 20/29] Rename function --- docs/reference/util.md | 8 ++++---- src/ConfigSpace/util.py | 12 ++++++------ test/test_util.py | 40 ++++++++++++++++++++-------------------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/reference/util.md b/docs/reference/util.md index e850215f..e349ea32 100644 --- a/docs/reference/util.md +++ b/docs/reference/util.md @@ -2,7 +2,7 @@ ### Expression to Configspace -In some cases we may have (highly) complex conditions or forbidden expressions that are already denoted as a regular expression. In that case, `ConfigSpace` can automatically convert them into a `ConfigSpace` expression using the [`expression_to_configspace`][ConfigSpace.util.expression_to_configspace]`expression_to_configspace`. This function interprets the expression using the Python `Abstract Syntax Tree` parser and recursively converts it into the appropriate structure. +In some cases we may have (highly) complex conditions or forbidden expressions that are already denoted as a regular expression. In that case, `ConfigSpace` can automatically convert them into a `ConfigSpace` expression using the [`parse_expression_from_string`][ConfigSpace.util.parse_expression_from_string]`parse_expression_from_string`. This function interprets the expression using the Python `Abstract Syntax Tree` parser and recursively converts it into the appropriate structure. !!! note The converted expression is not added to ConfigSpace, only returned to the user. @@ -12,7 +12,7 @@ In some cases we may have (highly) complex conditions or forbidden expressions t ```python exec="True" result="python" source="tabbed-left" from ConfigSpace import ConfigurationSpace -from ConfigSpace.util import expression_to_configspace +from ConfigSpace.util import parse_expression_from_string cs = ConfigurationSpace( { @@ -25,12 +25,12 @@ print(cs) # Now we add a condition and forbidden using regular expressions condition = "b != cat && c > 0.001" -condition = expression_to_configspace(condition, cs, conditional_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument +condition = parse_expression_from_string(condition, cs, conditional_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument print(condition) forbidden = "a > 5 && c >= 0.94" -forbidden = expression_to_configspace(forbidden, cs) +forbidden = parse_expression_from_string(forbidden, cs) print(forbidden) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index faeede4a..e1f25b51 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -860,7 +860,7 @@ def _get_cartesian_product( return checked_grid_pts -def expression_to_configspace( +def parse_expression_from_string( expression: str, configspace: ConfigurationSpace, conditional_hyperparameter: Hyperparameter | None = None, @@ -877,10 +877,10 @@ def expression_to_configspace( ```python exec="true", source="material-block" result="python" from ConfigSpace import ConfigurationSpace - from ConfigSpace.util import expression_to_configspace + from ConfigSpace.util import parse_expression_from_string cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) }) - condition = expression_to_configspace("a < 5", cs, conditional_hyperparameter=cs['b']) + condition = parse_expression_from_string("a < 5", cs, conditional_hyperparameter=cs['b']) print(condition) ``` @@ -888,10 +888,10 @@ def expression_to_configspace( ```python exec="true", source="material-block" result="python" from ConfigSpace import ConfigurationSpace - from ConfigSpace.util import expression_to_configspace + from ConfigSpace.util import parse_expression_from_string cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) }) - forbidden = expression_to_configspace("a >= 5", cs) + forbidden = parse_expression_from_string("a >= 5", cs) print(forbidden) ``` @@ -930,7 +930,7 @@ def _recursive_conversion( ) -> Condition | ForbiddenClause: """Recursively parse the abstract syntax tree to a ConfigSpace expression. - Should not be called directly, but rather through `expression_to_configspace`. + Should not be called directly, but rather through `parse_expression_from_string`. Args: item: The item to parse. diff --git a/test/test_util.py b/test/test_util.py index 67319d2a..3f01ded5 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -66,12 +66,12 @@ from ConfigSpace.util import ( change_hp_value, deactivate_inactive_hyperparameters, - expression_to_configspace, fix_types, generate_grid, get_one_exchange_neighbourhood, get_random_neighbor, impute_inactive_values, + parse_expression_from_string, ) @@ -668,7 +668,7 @@ def test_generate_grid(): assert dict(generated_grid[-1]) == {"cat1": "T", "ord1": "3", "int1": 1000} -def test_expression_to_configspace(): +def test_parse_expression_from_string(): cs = ConfigurationSpace( { "a": (0, 10), @@ -685,45 +685,45 @@ def test_expression_to_configspace(): wrong_expression = "a >!> b" with pytest.raises(ValueError): - expression_to_configspace(wrong_expression, cs) + parse_expression_from_string(wrong_expression, cs) wrong_hp_name_expresion = "q <= 5" with pytest.raises(ValueError): - cs_expression = expression_to_configspace(wrong_hp_name_expresion, cs) + cs_expression = parse_expression_from_string(wrong_hp_name_expresion, cs) wrong_hp_value_expression = "a > 11" with pytest.raises(ValueError): - cs_expression = expression_to_configspace(wrong_hp_value_expression, cs) + cs_expression = parse_expression_from_string(wrong_hp_value_expression, cs) wrong_hp_value_expression = "a == dog" with pytest.raises(ValueError): - cs_expression = expression_to_configspace(wrong_hp_value_expression, cs) + cs_expression = parse_expression_from_string(wrong_hp_value_expression, cs) wrong_forbidden_expression = "a != 5" with pytest.raises(ValueError): - expression_to_configspace(wrong_forbidden_expression, cs) + parse_expression_from_string(wrong_forbidden_expression, cs) # In case the epxression is incorrecty ordered for ConfigSpace, the method fixes the ordering here where possible wrong_order_expression = "5 < a" - assert expression_to_configspace( + assert parse_expression_from_string( wrong_order_expression, cs, ) == ForbiddenGreaterThanClause(cs["a"], 5) wrong_order_expression = "5 > a" - assert expression_to_configspace( + assert parse_expression_from_string( wrong_order_expression, cs, ) == ForbiddenLessThanClause(cs["a"], 5) wrong_order_expression = "5 == a" - assert expression_to_configspace( + assert parse_expression_from_string( wrong_order_expression, cs, ) == ForbiddenEqualsClause(cs["a"], 5) wrong_order_expression = "5 != a" - assert expression_to_configspace( + assert parse_expression_from_string( wrong_order_expression, cs, conditional_hyperparameter=cs["e"], @@ -731,26 +731,26 @@ def test_expression_to_configspace(): wrong_order_expression = "[1,2,5] in a" with pytest.raises(ValueError): - expression_to_configspace(wrong_order_expression, cs) + parse_expression_from_string(wrong_order_expression, cs) in_operator_expression = ( "a in [1, 2, 3]" # This operator is accepted by ConfigSpace for Integer HP ) - cs_expression = expression_to_configspace(in_operator_expression, cs) + cs_expression = parse_expression_from_string(in_operator_expression, cs) assert cs_expression == ForbiddenInClause(cs["a"], [1, 2, 3]) simple_value_expression = "a > 9" - cs_expression = expression_to_configspace(simple_value_expression, cs) + cs_expression = parse_expression_from_string(simple_value_expression, cs) assert cs_expression == ForbiddenGreaterThanClause(cs["a"], 9) simple_expression = "a > b" - cs_expression = expression_to_configspace(simple_expression, cs) + cs_expression = parse_expression_from_string(simple_expression, cs) assert cs_expression == ForbiddenGreaterThanRelation(cs["a"], cs["b"]) simple_expression = "a < 5" - cs_expression = expression_to_configspace(simple_expression, cs) + cs_expression = parse_expression_from_string(simple_expression, cs) assert cs_expression == ForbiddenLessThanClause(cs["a"], 5) - cs_expression = expression_to_configspace( + cs_expression = parse_expression_from_string( simple_expression, cs, conditional_hyperparameter=cs["e"], @@ -758,7 +758,7 @@ def test_expression_to_configspace(): assert cs_expression == LessThanCondition(cs["e"], cs["a"], 5) simple_expression_inequality = "a != 5" - cs_expression = expression_to_configspace( + cs_expression = parse_expression_from_string( simple_expression_inequality, cs, conditional_hyperparameter=cs["e"], @@ -766,7 +766,7 @@ def test_expression_to_configspace(): assert cs_expression == NotEqualsCondition(cs["e"], cs["a"], 5) complex_expression = "a > b || (c > d && e < 5 && cat1 == dog && float1 >= 0.5)" - cs_expression = expression_to_configspace(complex_expression, cs) + cs_expression = parse_expression_from_string(complex_expression, cs) assert cs_expression == ForbiddenOrConjunction( ForbiddenGreaterThanRelation(cs["a"], cs["b"]), ForbiddenAndConjunction( @@ -780,7 +780,7 @@ def test_expression_to_configspace(): complex_expression = ( "a >= 8 and (cat1 in ['cat', 'dog'] or cat2 in ['sun', 'rain'])" ) - cs_expression = expression_to_configspace(complex_expression, cs) + cs_expression = parse_expression_from_string(complex_expression, cs) assert cs_expression == ForbiddenAndConjunction( ForbiddenGreaterThanEqualsClause(cs["a"], 8), ForbiddenOrConjunction( From b839258e0d758cddd939648e1e3e2b3b7f538fa9 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Wed, 1 Apr 2026 17:40:04 +0200 Subject: [PATCH 21/29] Separating the examples as two subsections --- docs/reference/util.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/reference/util.md b/docs/reference/util.md index e349ea32..49489ae9 100644 --- a/docs/reference/util.md +++ b/docs/reference/util.md @@ -10,6 +10,10 @@ In some cases we may have (highly) complex conditions or forbidden expressions t !!! note If the expression contains illegal values, errors, or requires functionalities not available in `ConfigSpace`, appriopriate exceptions will be raised. +#### Adding a condition + +In this code example we show how you can add a hyperparameter condition to ConfigSpace from a string. Note that the conditional hyperparameter is specified as a seperate argument and is not part of the expression string! + ```python exec="True" result="python" source="tabbed-left" from ConfigSpace import ConfigurationSpace from ConfigSpace.util import parse_expression_from_string @@ -29,13 +33,33 @@ condition = parse_expression_from_string(condition, cs, conditional_hyperparamet print(condition) +cs.add(condition) + +print(cs) +``` + +#### Adding a forbidden expression + +In this example we add a forbidden expression to ConfigSpace from string. Note that the conditional hyperparameter remains unspecified; this leads to ConfigSpace interpreting the expression as a forbidden expression. + +```python exec="True" result="python" source="tabbed-left" +from ConfigSpace import ConfigurationSpace +from ConfigSpace.util import parse_expression_from_string + +cs = ConfigurationSpace( + { + "a": (0, 10), # Integer from 0 to 10 + "b": ["cat", "dog"], # Categorical with choices "cat" and "dog" + "c": (0.0, 1.0), # Float from 0.0 to 1.0 + } +) +print(cs) forbidden = "a > 5 && c >= 0.94" forbidden = parse_expression_from_string(forbidden, cs) print(forbidden) -cs.add(condition) cs.add(forbidden) print(cs) -``` +``` \ No newline at end of file From ca5d3b752a8286c0e2c8401f7be12aa22ecae2fa Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Wed, 1 Apr 2026 17:51:48 +0200 Subject: [PATCH 22/29] Fixing pytest message check --- src/ConfigSpace/util.py | 4 ++-- test/test_util.py | 27 ++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index e1f25b51..0f6f7a6c 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -1013,7 +1013,7 @@ def _recursive_conversion( elif not isinstance(operator, (ast.Eq, ast.NotEq)): # Equality and inequality are symmetric; no operator change # For any other unsupported operator shapes, fail. raise ValueError( - f"Unsupported comparison between constant and hyperparameter: {left} {operator} {right}" + f"Unsupported comparison between constant and hyperparameter: {ast.unparse(item)}" ) if isinstance(left, Hyperparameter): # Convert to HP type @@ -1026,7 +1026,7 @@ def _recursive_conversion( elif not isinstance(right, Hyperparameter): raise ValueError( "Only hyperparameter comparisons allowed. Neither side is recognised as a hyperparameter in: " - f"{left} {operator} {right}" + f"{ast.unparse(item)}" ) is_relation = isinstance(left, Hyperparameter) and isinstance(right, Hyperparameter) diff --git a/test/test_util.py b/test/test_util.py index 3f01ded5..b1f0a4db 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -28,6 +28,7 @@ from __future__ import annotations import os +import re import numpy as np import pytest @@ -684,23 +685,39 @@ def test_parse_expression_from_string(): ) wrong_expression = "a >!> b" - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Could not parse expression: 'a >!> b'"): parse_expression_from_string(wrong_expression, cs) wrong_hp_name_expresion = "q <= 5" - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="Only hyperparameter comparisons allowed. Neither side is recognised as a hyperparameter in: q <= 5", + ): cs_expression = parse_expression_from_string(wrong_hp_name_expresion, cs) wrong_hp_value_expression = "a > 11" - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match=re.escape( + "Forbidden clause must be instantiated with a legal hyperparameter value for 'a, Type: UniformInteger, Range: [0, 10], Default: 5', but got '11'", + ), + ): cs_expression = parse_expression_from_string(wrong_hp_value_expression, cs) wrong_hp_value_expression = "a == dog" - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match=re.escape( + "Forbidden clause must be instantiated with a legal hyperparameter value for 'a, Type: UniformInteger, Range: [0, 10], Default: 5', but got 'dog'", + ), + ): cs_expression = parse_expression_from_string(wrong_hp_value_expression, cs) wrong_forbidden_expression = "a != 5" - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="NotEq operator not supported for ForbiddenClauses.", + ): parse_expression_from_string(wrong_forbidden_expression, cs) # In case the epxression is incorrecty ordered for ConfigSpace, the method fixes the ordering here where possible From 88bbcdff3745d0e036fa8a86708ba0be9d596cae Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Wed, 1 Apr 2026 17:53:44 +0200 Subject: [PATCH 23/29] docstring fix --- src/ConfigSpace/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 0f6f7a6c..28118f51 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -868,7 +868,7 @@ def parse_expression_from_string( """Convert a logic expression to ConfigSpace expression. Given a logic expression, this function will return a ConfigSpace expression - that is equivalent to the logic expression. If a target parameter is provided, + that is equivalent to the logic expression. If a conditional parameter is provided, will create a condition, otherwise a forbidden expression. The created expression is **NOT** automatically added to the configuration space. From af3d63fb94e49f3a44d4c99551cfa90db612cbf0 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Wed, 1 Apr 2026 17:57:36 +0200 Subject: [PATCH 24/29] seperating test in two parts --- test/test_util.py | 57 +++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index b1f0a4db..7929a648 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -669,7 +669,7 @@ def test_generate_grid(): assert dict(generated_grid[-1]) == {"cat1": "T", "ord1": "3", "int1": 1000} -def test_parse_expression_from_string(): +def test_parse_expression_from_string_forbidden(): cs = ConfigurationSpace( { "a": (0, 10), @@ -739,13 +739,6 @@ def test_parse_expression_from_string(): cs, ) == ForbiddenEqualsClause(cs["a"], 5) - wrong_order_expression = "5 != a" - assert parse_expression_from_string( - wrong_order_expression, - cs, - conditional_hyperparameter=cs["e"], - ) == NotEqualsCondition(cs["e"], cs["a"], 5) - wrong_order_expression = "[1,2,5] in a" with pytest.raises(ValueError): parse_expression_from_string(wrong_order_expression, cs) @@ -767,20 +760,6 @@ def test_parse_expression_from_string(): simple_expression = "a < 5" cs_expression = parse_expression_from_string(simple_expression, cs) assert cs_expression == ForbiddenLessThanClause(cs["a"], 5) - cs_expression = parse_expression_from_string( - simple_expression, - cs, - conditional_hyperparameter=cs["e"], - ) - assert cs_expression == LessThanCondition(cs["e"], cs["a"], 5) - - simple_expression_inequality = "a != 5" - cs_expression = parse_expression_from_string( - simple_expression_inequality, - cs, - conditional_hyperparameter=cs["e"], - ) - assert cs_expression == NotEqualsCondition(cs["e"], cs["a"], 5) complex_expression = "a > b || (c > d && e < 5 && cat1 == dog && float1 >= 0.5)" cs_expression = parse_expression_from_string(complex_expression, cs) @@ -805,3 +784,37 @@ def test_parse_expression_from_string(): ForbiddenInClause(cs["cat2"], ["sun", "rain"]), ), ) + + +def test_parse_expression_from_string_condition(): + cs = ConfigurationSpace( + { + "a": (0, 10), + "b": (0, 10), + "c": (0, 10), + "d": (0, 10), + "e": (0, 10), + }, + ) + simple_expression = "a < 5" + cs_expression = parse_expression_from_string( + simple_expression, + cs, + conditional_hyperparameter=cs["e"], + ) + assert cs_expression == LessThanCondition(cs["e"], cs["a"], 5) + + simple_expression_inequality = "a != 5" + cs_expression = parse_expression_from_string( + simple_expression_inequality, + cs, + conditional_hyperparameter=cs["e"], + ) + assert cs_expression == NotEqualsCondition(cs["e"], cs["a"], 5) + + wrong_order_expression = "5 != a" + assert parse_expression_from_string( + wrong_order_expression, + cs, + conditional_hyperparameter=cs["e"], + ) == NotEqualsCondition(cs["e"], cs["a"], 5) From 6f16277eb790ec35e67e391a70056bce67ab9394 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Wed, 1 Apr 2026 17:58:49 +0200 Subject: [PATCH 25/29] Adding test --- test/test_util.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/test_util.py b/test/test_util.py index 7929a648..65b5cc53 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -720,6 +720,13 @@ def test_parse_expression_from_string_forbidden(): ): parse_expression_from_string(wrong_forbidden_expression, cs) + wrong_forbidden_expression = "a != b" + with pytest.raises( + ValueError, + match="NotEq operator not supported for ForbiddenClauses.", + ): + parse_expression_from_string(wrong_forbidden_expression, cs) + # In case the epxression is incorrecty ordered for ConfigSpace, the method fixes the ordering here where possible wrong_order_expression = "5 < a" assert parse_expression_from_string( From 2f769e7cb93808497a158c2466794ab7f01cb1f6 Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Thu, 2 Apr 2026 09:50:05 +0200 Subject: [PATCH 26/29] Clarification for possible ambiguity, adding tests --- src/ConfigSpace/util.py | 2 +- test/test_util.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 28118f51..392ed98b 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -950,7 +950,7 @@ def _recursive_conversion( if isinstance(item, ast.Name): # Convert to hyperparameter hp = configspace.get(item.id) return hp if hp is not None else item.id - if isinstance(item, ast.Constant): + if isinstance(item, ast.Constant): # ast.Constant are differentiated from ast.Name by integers/floats and quoted strings return item.value if ( isinstance(item, ast.Tuple) diff --git a/test/test_util.py b/test/test_util.py index 65b5cc53..99d41d89 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -44,6 +44,7 @@ EqualsCondition, ForbiddenAndConjunction, ForbiddenEqualsClause, + ForbiddenEqualsRelation, ForbiddenGreaterThanClause, ForbiddenGreaterThanEqualsClause, ForbiddenGreaterThanRelation, @@ -679,6 +680,7 @@ def test_parse_expression_from_string_forbidden(): "e": (0, 10), "cat1": ["cat", "dog"], "cat2": ["sun", "rain", "snow", "fog"], + "dog": ["small", "medium", "large", "cat", "dog"], "float1": (0.0, 1.0), "float2": (0.0, 1.0), }, @@ -704,11 +706,11 @@ def test_parse_expression_from_string_forbidden(): ): cs_expression = parse_expression_from_string(wrong_hp_value_expression, cs) - wrong_hp_value_expression = "a == dog" + wrong_hp_value_expression = "a == cat" with pytest.raises( ValueError, match=re.escape( - "Forbidden clause must be instantiated with a legal hyperparameter value for 'a, Type: UniformInteger, Range: [0, 10], Default: 5', but got 'dog'", + "Forbidden clause must be instantiated with a legal hyperparameter value for 'a, Type: UniformInteger, Range: [0, 10], Default: 5', but got 'cat'", ), ): cs_expression = parse_expression_from_string(wrong_hp_value_expression, cs) @@ -768,7 +770,7 @@ def test_parse_expression_from_string_forbidden(): cs_expression = parse_expression_from_string(simple_expression, cs) assert cs_expression == ForbiddenLessThanClause(cs["a"], 5) - complex_expression = "a > b || (c > d && e < 5 && cat1 == dog && float1 >= 0.5)" + complex_expression = "a > b || (c > d && e < 5 && cat1 == 'dog' && float1 >= 0.5)" cs_expression = parse_expression_from_string(complex_expression, cs) assert cs_expression == ForbiddenOrConjunction( ForbiddenGreaterThanRelation(cs["a"], cs["b"]), @@ -792,6 +794,29 @@ def test_parse_expression_from_string_forbidden(): ), ) + # Check if a hyperparameter name / categorical value mixup does not occur based on the quotation marks + semi_ambigous_expression = ( + "cat1 == 'dog'" # Here we are talking about the categorical value + ) + assert parse_expression_from_string( + semi_ambigous_expression, + cs, + ) == ForbiddenEqualsClause(cs["cat1"], "dog") + semi_ambigous_expression = ( + "cat1 == dog" # Now we are referring to the Hyperparameter called dog + ) + assert parse_expression_from_string( + semi_ambigous_expression, + cs, + ) == ForbiddenEqualsRelation(cs["cat1"], cs["dog"]) + semi_ambigous_expression = ( + "dog == 'dog'" # The hyperparameter dog cannot have value 'dog' + ) + assert parse_expression_from_string( + semi_ambigous_expression, + cs, + ) == ForbiddenEqualsClause(cs["dog"], "dog") + def test_parse_expression_from_string_condition(): cs = ConfigurationSpace( From fa9ea17bd2f4f49db4c8d1e84b2f4614b75bbd1b Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Thu, 2 Apr 2026 12:07:21 +0200 Subject: [PATCH 27/29] Updating method to be more strict on HP names --- docs/reference/util.md | 5 ++++- src/ConfigSpace/util.py | 4 +++- test/test_util.py | 15 +++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/reference/util.md b/docs/reference/util.md index 49489ae9..571d9182 100644 --- a/docs/reference/util.md +++ b/docs/reference/util.md @@ -10,6 +10,9 @@ In some cases we may have (highly) complex conditions or forbidden expressions t !!! note If the expression contains illegal values, errors, or requires functionalities not available in `ConfigSpace`, appriopriate exceptions will be raised. +!!! note + Expressions differentiate variables (Hyperparameter names) from constants (Categorical values) based on quotation marks; "a != b" implies variable a does not equal b, "a != 'b'" implies variable a does not equal categorical/ordinal value b. + #### Adding a condition In this code example we show how you can add a hyperparameter condition to ConfigSpace from a string. Note that the conditional hyperparameter is specified as a seperate argument and is not part of the expression string! @@ -28,7 +31,7 @@ cs = ConfigurationSpace( print(cs) # Now we add a condition and forbidden using regular expressions -condition = "b != cat && c > 0.001" +condition = "b != 'cat' && c > 0.001" condition = parse_expression_from_string(condition, cs, conditional_hyperparameter=cs["a"]) # We have to specify the conditional HP seperately here as the final argument print(condition) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 392ed98b..417571b7 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -949,7 +949,9 @@ def _recursive_conversion( return _recursive_conversion(item.value, configspace, conditional_hyperparameter) if isinstance(item, ast.Name): # Convert to hyperparameter hp = configspace.get(item.id) - return hp if hp is not None else item.id + if hp is None: + raise ValueError(f"Unknown hyperparameter: {item.id}") + return hp #if hp is not None else item.id if isinstance(item, ast.Constant): # ast.Constant are differentiated from ast.Name by integers/floats and quoted strings return item.value if ( diff --git a/test/test_util.py b/test/test_util.py index 99d41d89..bf611239 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -693,10 +693,17 @@ def test_parse_expression_from_string_forbidden(): wrong_hp_name_expresion = "q <= 5" with pytest.raises( ValueError, - match="Only hyperparameter comparisons allowed. Neither side is recognised as a hyperparameter in: q <= 5", + match="Unknown hyperparameter: q", ): cs_expression = parse_expression_from_string(wrong_hp_name_expresion, cs) + wrong_value_expresion = "'q' <= 5" + with pytest.raises( + ValueError, + match="Only hyperparameter comparisons allowed. Neither side is recognised as a hyperparameter in: 'q' <= 5", + ): + cs_expression = parse_expression_from_string(wrong_value_expresion, cs) + wrong_hp_value_expression = "a > 11" with pytest.raises( ValueError, @@ -706,7 +713,7 @@ def test_parse_expression_from_string_forbidden(): ): cs_expression = parse_expression_from_string(wrong_hp_value_expression, cs) - wrong_hp_value_expression = "a == cat" + wrong_hp_value_expression = "a == 'cat'" with pytest.raises( ValueError, match=re.escape( @@ -817,6 +824,10 @@ def test_parse_expression_from_string_forbidden(): cs, ) == ForbiddenEqualsClause(cs["dog"], "dog") + wrong_semi_ambigous_expression = "dog == medium" # There is no variable called medium, only a constant; quotation marks are missing + with pytest.raises(ValueError, match="Unknown hyperparameter: medium"): + parse_expression_from_string(wrong_semi_ambigous_expression, cs) + def test_parse_expression_from_string_condition(): cs = ConfigurationSpace( From b5eef6f9b4487c9a86bc2a3e9c9ca0a1c20d213f Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Thu, 2 Apr 2026 12:08:11 +0200 Subject: [PATCH 28/29] Docs clarification --- docs/reference/util.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/util.md b/docs/reference/util.md index 571d9182..9f801d05 100644 --- a/docs/reference/util.md +++ b/docs/reference/util.md @@ -11,7 +11,7 @@ In some cases we may have (highly) complex conditions or forbidden expressions t If the expression contains illegal values, errors, or requires functionalities not available in `ConfigSpace`, appriopriate exceptions will be raised. !!! note - Expressions differentiate variables (Hyperparameter names) from constants (Categorical values) based on quotation marks; "a != b" implies variable a does not equal b, "a != 'b'" implies variable a does not equal categorical/ordinal value b. + Expressions differentiate variables (Hyperparameter names) from constants (Categorical values) based on quotation marks; "a != b" implies hyperparameter a does not equal hyperparameter b, "a != 'b'" implies hyperparameter a does not equal categorical/ordinal value b. #### Adding a condition From 1c9965cf37d7155b9cd167eb44223687b92555cd Mon Sep 17 00:00:00 2001 From: Thijs Snelleman Date: Thu, 2 Apr 2026 13:43:04 +0200 Subject: [PATCH 29/29] removing commented out code --- src/ConfigSpace/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index 417571b7..17576ecf 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -951,7 +951,7 @@ def _recursive_conversion( hp = configspace.get(item.id) if hp is None: raise ValueError(f"Unknown hyperparameter: {item.id}") - return hp #if hp is not None else item.id + return hp if isinstance(item, ast.Constant): # ast.Constant are differentiated from ast.Name by integers/floats and quoted strings return item.value if (