Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
469d059
First version
thijssnelleman Sep 22, 2025
7642320
Expanding tests
thijssnelleman Sep 30, 2025
753fa8d
Simplifying print statement as ast.unparse is only available from Pyt…
thijssnelleman Sep 30, 2025
92d450d
Bugfixes
thijssnelleman Sep 30, 2025
de35995
Updating documentation
thijssnelleman Dec 19, 2025
c209f0f
Merge branch 'main' of https://github.com/automl/ConfigSpace into abs…
thijssnelleman Jan 5, 2026
be3db2d
Cleaning up code, expanding docs
thijssnelleman Jan 5, 2026
4b344dc
bugfix
thijssnelleman Jan 5, 2026
83119dd
docs typo
thijssnelleman Jan 5, 2026
76d6617
Adding docs, minor fixes
thijssnelleman Mar 16, 2026
f165c9e
Merge branch 'main' of https://github.com/automl/ConfigSpace into abs…
thijssnelleman Mar 16, 2026
03a9790
Fixing example
thijssnelleman Mar 16, 2026
2f1c563
Remove mistake file
thijssnelleman Mar 16, 2026
995f02e
docfix
thijssnelleman Mar 16, 2026
96f6dca
Potential fix for pull request finding
thijssnelleman Mar 16, 2026
a9c9f36
Potential fix for pull request finding
thijssnelleman Mar 16, 2026
bb199bb
Updating copilot fix, adding tests
thijssnelleman Mar 16, 2026
1f1a609
Bugfixes
thijssnelleman Mar 16, 2026
3d1f4ba
docfix
thijssnelleman Mar 16, 2026
2192f7f
reference fix
thijssnelleman Mar 16, 2026
fc11a3b
Parameter rename
thijssnelleman Mar 17, 2026
d2ebe05
Rename function
thijssnelleman Apr 1, 2026
b839258
Separating the examples as two subsections
thijssnelleman Apr 1, 2026
ca5d3b7
Fixing pytest message check
thijssnelleman Apr 1, 2026
88bbcdf
docstring fix
thijssnelleman Apr 1, 2026
af3d63f
seperating test in two parts
thijssnelleman Apr 1, 2026
6f16277
Adding test
thijssnelleman Apr 1, 2026
2f769e7
Clarification for possible ambiguity, adding tests
thijssnelleman Apr 2, 2026
fa9ea17
Updating method to be more strict on HP names
thijssnelleman Apr 2, 2026
b5eef6f
Docs clarification
thijssnelleman Apr 2, 2026
1c9965c
removing commented out code
thijssnelleman Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions docs/reference/util.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## 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 [`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.

!!! 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 hyperparameter a does not equal hyperparameter b, "a != 'b'" implies hyperparameter 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!

```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)

# Now we add a condition and forbidden using regular expressions
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)

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(forbidden)

print(cs)
```
Empty file removed docs/reference/utils.md
Empty file.
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
257 changes: 256 additions & 1 deletion src/ConfigSpace/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import annotations

import ast
import re
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

Expand All @@ -50,6 +52,34 @@
UniformFloatHyperparameter,
UniformIntegerHyperparameter,
)

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,
ForbiddenEqualsRelation,
ForbiddenGreaterThanEqualsRelation,
ForbiddenLessThanEqualsRelation,
)

from ConfigSpace.types import NotSet

if TYPE_CHECKING:
Expand Down Expand Up @@ -828,3 +858,228 @@ def _get_cartesian_product(
unchecked_grid_pts.popleft()

return checked_grid_pts


def parse_expression_from_string(
expression: str,
configspace: ConfigurationSpace,
conditional_hyperparameter: Hyperparameter | None = None,
) -> Condition | ForbiddenClause:
"""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 conditional parameter is provided,
will create a condition, otherwise a forbidden expression.

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 parse_expression_from_string

cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) })
condition = parse_expression_from_string("a < 5", cs, conditional_hyperparameter=cs['b'])
print(condition)
```

Example Forbidden Expression Parsing:

```python exec="true", source="material-block" result="python"
from ConfigSpace import ConfigurationSpace
from ConfigSpace.util import parse_expression_from_string

cs = ConfigurationSpace({ "a": (0, 10), "b": (1.0, 8.0) })
forbidden = parse_expression_from_string("a >= 5", cs)
print(forbidden)
```

Args:
expression: The expression to convert.
configspace: The ConfigSpace to use.
conditional_hyperparameter: For conditions, will parse the expression as a condition
underwhich the provided hyperparameter will be active.

Returns:
A ConfigSpace Condition or ForbiddenClause.
"""
# 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)
try:
# Convert to abstract syntax tree, extract body of the expression
ast_expression = ast.parse(expression).body[0]
except Exception as e:
raise ValueError(f"Could not parse expression: '{expression}', {e}")
return _recursive_conversion(
ast_expression, configspace, conditional_hyperparameter=conditional_hyperparameter
)


def _recursive_conversion(
item: ast.AST | list[ast.AST],
configspace: ConfigurationSpace,
conditional_hyperparameter: Hyperparameter | None = None,
) -> Condition | ForbiddenClause:
"""Recursively parse the abstract syntax tree to a ConfigSpace expression.

Should not be called directly, but rather through `parse_expression_from_string`.

Args:
item: The item to parse.
configspace: The ConfigSpace to use.
conditional_hyperparameter: For conditions, will parse the expression as a condition
underwhich the hyperparameter will be active.

Returns:
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, conditional_hyperparameter)
if isinstance(item, ast.Name): # Convert to hyperparameter
hp = configspace.get(item.id)
if hp is None:
raise ValueError(f"Unknown hyperparameter: {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 (
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 configspace.get(v.id) is not None:
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, conditional_hyperparameter) for v in item.values
]
if isinstance(item.op, ast.Or):
if conditional_hyperparameter:
return OrConjunction(*values)
return ForbiddenOrConjunction(*values)
elif isinstance(item.op, ast.And):
if conditional_hyperparameter:
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, 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
# 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.
left, right = right, left
if isinstance(operator, ast.Lt):
operator = ast.Gt()
elif isinstance(operator, ast.LtE):
operator = ast.GtE()
elif isinstance(operator, ast.Gt):
operator = ast.Lt()
elif isinstance(operator, ast.GtE):
operator = ast.LtE()
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."
)
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: {ast.unparse(item)}"
)

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)
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 conditional_hyperparameter:
raise ValueError("Hyperparameter relations not supported for conditions.")

if isinstance(operator, ast.Lt):
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 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 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 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 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 conditional_hyperparameter:
return InCondition(conditional_hyperparameter, left, right)
return ForbiddenInClause(hyperparameter=left, values=right)
if isinstance(operator, ast.NotEq):
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):
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}")
Loading
Loading