From df6dd5e151c9e69863b1a64d6fb7b25c6dac0d7a Mon Sep 17 00:00:00 2001 From: Eddie Bergman Date: Tue, 13 Jun 2023 11:37:46 +0200 Subject: [PATCH] Refactor: decythonize configuration space (#321) * refactor: Initial Cleanup * refactor: ... * chore: Update and Fix tooling * fix: Make tooling work for 3.7 --- .flake8 | 11 - .github/.dependabot.yml | 14 + .github/workflows/docs.yml | 3 +- .github/workflows/pytest.yml | 9 +- .landscape.yaml | 1 - .pre-commit-config.yaml | 77 +- ConfigSpace/__authors__.py | 3 + ConfigSpace/__init__.py | 93 +- ConfigSpace/__version__.py | 2 + ConfigSpace/api/__init__.py | 3 +- ConfigSpace/api/distributions.py | 10 +- ConfigSpace/api/types/categorical.py | 29 +- ConfigSpace/api/types/float.py | 18 +- ConfigSpace/api/types/integer.py | 10 +- ConfigSpace/c_util.pyx | 32 +- ConfigSpace/configuration.py | 264 ++ ConfigSpace/configuration_space.py | 1581 ++++++++++++ ConfigSpace/configuration_space.pyx | 1895 --------------- ConfigSpace/exceptions.py | 131 +- ConfigSpace/functional.py | 4 +- ConfigSpace/hyperparameters/__init__.py | 35 +- ConfigSpace/nx/__init__.py | 44 +- ConfigSpace/nx/algorithms/__init__.py | 16 +- .../nx/algorithms/components/__init__.py | 6 +- .../components/strongly_connected.py | 29 +- ConfigSpace/nx/algorithms/cycles.py | 26 +- ConfigSpace/nx/algorithms/dag.py | 63 +- ConfigSpace/nx/classes/__init__.py | 8 +- ConfigSpace/nx/classes/digraph.py | 65 +- ConfigSpace/nx/classes/graph.py | 87 +- ConfigSpace/nx/exception.py | 22 +- ConfigSpace/nx/release.py | 83 +- ConfigSpace/read_and_write/json.py | 730 +++--- ConfigSpace/read_and_write/pcs.py | 300 ++- ConfigSpace/read_and_write/pcs_new.py | 558 +++-- ConfigSpace/{util.pyx => util.py} | 343 +-- Makefile | 35 +- changelog.md | 4 +- docs/conf.py | 18 +- pyproject.toml | 305 +++ scripts/benchmark_sampling.py | 27 +- setup.cfg | 9 - setup.py | 177 +- test/read_and_write/test_json.py | 62 +- test/read_and_write/test_pcs_converter.py | 370 +-- test/test_api/test_hp_construction.py | 34 +- test/test_conditions.py | 288 +-- test/test_configspace_from_dict.py | 16 +- test/test_configuration_space.py | 808 +++---- .../test_sample_configuration_spaces.py | 29 +- test/test_forbidden.py | 311 +-- test/test_functional.py | 4 +- test/test_hyperparameters.py | 2116 ++++++++++------- test/test_util.py | 490 ++-- 54 files changed, 6410 insertions(+), 5298 deletions(-) delete mode 100644 .flake8 create mode 100644 .github/.dependabot.yml delete mode 100644 .landscape.yaml create mode 100644 ConfigSpace/configuration.py create mode 100644 ConfigSpace/configuration_space.py delete mode 100644 ConfigSpace/configuration_space.pyx rename ConfigSpace/{util.pyx => util.py} (71%) delete mode 100644 setup.cfg diff --git a/.flake8 b/.flake8 deleted file mode 100644 index a6996a48..00000000 --- a/.flake8 +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -filename = *.py, *.pyx, *.pxd -max-line-length = 100 -show-source = True -application-import-names = ConfigSpace -exclude = - venv - build - .eggs - *.egg - diff --git a/.github/.dependabot.yml b/.github/.dependabot.yml new file mode 100644 index 00000000..1269fa91 --- /dev/null +++ b/.github/.dependabot.yml @@ -0,0 +1,14 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "main" + schedule: + interval: "weekly" + assignees: + - "Neonkraft" + reviewers: + - "Neonkraft" + commit-message: + prefix: "chore: " diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c0985c4c..4ed7856d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,8 +36,7 @@ jobs: - name: Install dependencies run: | - pip install build - pip install ".[docs]" + pip install ".[dev]" - name: Make docs run: | diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index fa5da6cd..e1374300 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -24,7 +24,7 @@ env: package-name: ConfigSpace test-dir: test - extra-requires: "[test]" # "" for no extra_requires + extra-requires: "[dev]" # "" for no extra_requires # Arguments used for pytest pytest-args: >- @@ -37,7 +37,7 @@ env: # code-cov-active: true # Copied in job setup code-cov-os: ubuntu-latest # Copied in job setup - code-cov-python-version: "3.7" + code-cov-python-version: "3.8" code-cov-args: >- --cov=ConfigSpace --cov-report=xml @@ -71,7 +71,6 @@ jobs: - name: Install ${{ env.package-name }} run: | python -m pip install --upgrade pip - python -m pip install wheel python -m pip install -e ".${{ env.extra-requires }}" - name: Store git status @@ -107,7 +106,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: ["ubuntu-latest", "macos-latest", "windows-latest"] steps: @@ -144,7 +143,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: ["ubuntu-latest", "macos-latest", "windows-latest"] steps: diff --git a/.landscape.yaml b/.landscape.yaml deleted file mode 100644 index db93009e..00000000 --- a/.landscape.yaml +++ /dev/null @@ -1 +0,0 @@ -ignore-paths: HPOlibConfigSpace/nx \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ab46bb5..645a947f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,69 @@ +default_language_version: + python: python3 +files: | + (?x)^( + ConfigSpace| + test + )/.*\.py$ repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + files: ".*" + - id: check-case-conflict + files: ".*" + - id: check-merge-conflict + files: ".*" + - id: check-yaml + files: ".*" + - id: end-of-file-fixer + files: ".*" + types: ["yaml"] + - id: check-toml + files: ".*" + types: ["toml"] + - id: debug-statements + files: '^src/.*\.py$' + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.22.0 + hooks: + - id: check-github-workflows + files: '^github/workflows/.*\.ya?ml$' + types: ["yaml"] + - id: check-dependabot + files: '^\.github/dependabot\.ya?ml$' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.961 + rev: v1.2.0 hooks: - id: mypy - args: [--show-error-codes, --ignore-missing-imports, --follow-imports, skip] - name: mypy ConfigSpace - files: ConfigSpace + files: '^ConfigSpace/.*\.py$' + args: + - "--no-warn-return-any" # Disable this because it doesn't know about 3rd party imports + - "--ignore-missing-imports" + - "--show-traceback" + - id: mypy + files: '^test/.*\.py$' + args: + - "--no-warn-return-any" # Disable this because it doesn't know about 3rd party imports + - "--ignore-missing-imports" + - "--show-traceback" + - "--disable-error-code" + - "no-untyped-def" - - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + - repo: https://github.com/psf/black + rev: 23.3.0 hooks: - - id: flake8 - name: flake8 ConfigSpace - files: ConfigSpace - - - id: flake8 - name: flake8 test - files: test - + - id: black + args: ["--config=pyproject.toml"] + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.263 + hooks: + - id: ruff + args: ["--fix", "ConfigSpace", "test"] - repo: https://github.com/MarcoGorelli/cython-lint - rev: v0.14.0 + rev: v0.15.0 hooks: - - id: cython-lint + - id: cython-lint args: [--ignore=E501] - - id: double-quote-cython-strings + - id: double-quote-cython-strings diff --git a/ConfigSpace/__authors__.py b/ConfigSpace/__authors__.py index caa17d5b..f5c8c903 100644 --- a/ConfigSpace/__authors__.py +++ b/ConfigSpace/__authors__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + __authors__ = [ "Matthias Feurer", "Katharina Eggensperger", @@ -9,4 +11,5 @@ "Marius Lindauer", "Jorn Tuyls", "Eddie Bergman", + "Arjun Krishnakumar", ] diff --git a/ConfigSpace/__init__.py b/ConfigSpace/__init__.py index 06a1ee91..bce613bd 100644 --- a/ConfigSpace/__init__.py +++ b/ConfigSpace/__init__.py @@ -26,33 +26,63 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from ConfigSpace.__version__ import __version__ from ConfigSpace.__authors__ import __authors__ - -from ConfigSpace.api import (Beta, Categorical, Distribution, Float, Integer, - Normal, Uniform) -from ConfigSpace.conditions import (AndConjunction, EqualsCondition, - GreaterThanCondition, InCondition, - LessThanCondition, NotEqualsCondition, - OrConjunction) -from ConfigSpace.configuration_space import Configuration, ConfigurationSpace -from ConfigSpace.forbidden import (ForbiddenAndConjunction, - ForbiddenEqualsClause, - ForbiddenEqualsRelation, - ForbiddenGreaterThanRelation, - ForbiddenInClause, - ForbiddenLessThanRelation) -from ConfigSpace.hyperparameters import (BetaFloatHyperparameter, - BetaIntegerHyperparameter, - CategoricalHyperparameter, Constant, - NormalFloatHyperparameter, - NormalIntegerHyperparameter, - OrdinalHyperparameter, - UniformFloatHyperparameter, - UniformIntegerHyperparameter, - UnParametrizedHyperparameter) -import ConfigSpace.api.distributions as distributions -import ConfigSpace.api.types as types +from ConfigSpace.__version__ import __version__ +from ConfigSpace.api import ( + Beta, + Categorical, + Distribution, + Float, + Integer, + Normal, + Uniform, + distributions, + types, +) +from ConfigSpace.conditions import ( + AndConjunction, + EqualsCondition, + GreaterThanCondition, + InCondition, + LessThanCondition, + NotEqualsCondition, + OrConjunction, +) +from ConfigSpace.configuration import Configuration +from ConfigSpace.configuration_space import ConfigurationSpace +from ConfigSpace.exceptions import ( + ActiveHyperparameterNotSetError, + AmbiguousConditionError, + ChildNotFoundError, + CyclicDependancyError, + ForbiddenValueError, + HyperparameterAlreadyExistsError, + HyperparameterIndexError, + HyperparameterNotFoundError, + IllegalValueError, + InactiveHyperparameterSetError, + ParentNotFoundError, +) +from ConfigSpace.forbidden import ( + ForbiddenAndConjunction, + ForbiddenEqualsClause, + ForbiddenEqualsRelation, + ForbiddenGreaterThanRelation, + ForbiddenInClause, + ForbiddenLessThanRelation, +) +from ConfigSpace.hyperparameters import ( + BetaFloatHyperparameter, + BetaIntegerHyperparameter, + CategoricalHyperparameter, + Constant, + NormalFloatHyperparameter, + NormalIntegerHyperparameter, + OrdinalHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, + UnParametrizedHyperparameter, +) __all__ = [ "__authors__", @@ -91,4 +121,15 @@ "Uniform", "distributions", "types", + "ForbiddenValueError", + "IllegalValueError", + "ActiveHyperparameterNotSetError", + "InactiveHyperparameterSetError", + "HyperparameterNotFoundError", + "ChildNotFoundError", + "ParentNotFoundError", + "HyperparameterIndexError", + "AmbiguousConditionError", + "HyperparameterAlreadyExistsError", + "CyclicDependancyError", ] diff --git a/ConfigSpace/__version__.py b/ConfigSpace/__version__.py index 28e42f3f..cb01db0d 100644 --- a/ConfigSpace/__version__.py +++ b/ConfigSpace/__version__.py @@ -1,4 +1,6 @@ """Version information.""" # The following line *must* be the last in the module, exactly as formatted: +from __future__ import annotations + __version__ = "0.6.1" diff --git a/ConfigSpace/api/__init__.py b/ConfigSpace/api/__init__.py index fd1b6507..e1ea41b9 100644 --- a/ConfigSpace/api/__init__.py +++ b/ConfigSpace/api/__init__.py @@ -1,5 +1,4 @@ -import ConfigSpace.api.distributions as distributions -import ConfigSpace.api.types as types +from ConfigSpace.api import distributions, types from ConfigSpace.api.distributions import Beta, Distribution, Normal, Uniform from ConfigSpace.api.types import Categorical, Float, Integer diff --git a/ConfigSpace/api/distributions.py b/ConfigSpace/api/distributions.py index 028c8a5a..8494ca3d 100644 --- a/ConfigSpace/api/distributions.py +++ b/ConfigSpace/api/distributions.py @@ -1,18 +1,16 @@ +from __future__ import annotations + from dataclasses import dataclass @dataclass class Distribution: - """Base distribution type""" - - pass + """Base distribution type.""" @dataclass class Uniform(Distribution): - """A uniform distribution""" - - pass + """A uniform distribution.""" @dataclass diff --git a/ConfigSpace/api/types/categorical.py b/ConfigSpace/api/types/categorical.py index da095eef..c0c84a27 100644 --- a/ConfigSpace/api/types/categorical.py +++ b/ConfigSpace/api/types/categorical.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import Sequence, Union, overload +from typing import ( + Sequence, + Union, + overload, +) +from typing_extensions import Literal, TypeAlias -from typing_extensions import (Literal, # Move to `typing` when 3.8 minimum - TypeAlias) - -from ConfigSpace.hyperparameters import (CategoricalHyperparameter, - OrdinalHyperparameter) +from ConfigSpace.hyperparameters import CategoricalHyperparameter, OrdinalHyperparameter # We only accept these types in `items` T: TypeAlias = Union[str, int, float] @@ -130,11 +131,11 @@ def Categorical( default_value=default, meta=meta, ) - else: - return CategoricalHyperparameter( - name=name, - choices=items, - default_value=default, - weights=weights, - meta=meta, - ) + + return CategoricalHyperparameter( + name=name, + choices=items, + default_value=default, + weights=weights, + meta=meta, + ) diff --git a/ConfigSpace/api/types/float.py b/ConfigSpace/api/types/float.py index 31b5117e..bf3bc559 100644 --- a/ConfigSpace/api/types/float.py +++ b/ConfigSpace/api/types/float.py @@ -3,9 +3,11 @@ from typing import overload from ConfigSpace.api.distributions import Beta, Distribution, Normal, Uniform -from ConfigSpace.hyperparameters import (BetaFloatHyperparameter, - NormalFloatHyperparameter, - UniformFloatHyperparameter) +from ConfigSpace.hyperparameters import ( + BetaFloatHyperparameter, + NormalFloatHyperparameter, + UniformFloatHyperparameter, +) # Uniform | None -> UniformFloatHyperparameter @@ -153,7 +155,8 @@ def Float( log=log, meta=meta, ) - elif isinstance(distribution, Normal): + + if isinstance(distribution, Normal): return NormalFloatHyperparameter( name=name, lower=lower, @@ -165,7 +168,8 @@ def Float( log=log, meta=meta, ) - elif isinstance(distribution, Beta): + + if isinstance(distribution, Beta): return BetaFloatHyperparameter( name=name, lower=lower, @@ -177,5 +181,5 @@ def Float( log=log, meta=meta, ) - else: - raise ValueError(f"Unknown distribution type {type(distribution)}") + + raise ValueError(f"Unknown distribution type {type(distribution)}") diff --git a/ConfigSpace/api/types/integer.py b/ConfigSpace/api/types/integer.py index 539c6650..099cbdbc 100644 --- a/ConfigSpace/api/types/integer.py +++ b/ConfigSpace/api/types/integer.py @@ -165,7 +165,8 @@ def Integer( default_value=default, meta=meta, ) - elif isinstance(distribution, Normal): + + if isinstance(distribution, Normal): return NormalIntegerHyperparameter( name=name, lower=lower, @@ -177,7 +178,8 @@ def Integer( mu=distribution.mu, sigma=distribution.sigma, ) - elif isinstance(distribution, Beta): + + if isinstance(distribution, Beta): return BetaIntegerHyperparameter( name=name, lower=lower, @@ -189,5 +191,5 @@ def Integer( alpha=distribution.alpha, beta=distribution.beta, ) - else: - raise ValueError(f"Unknown distribution type {type(distribution)}") + + raise ValueError(f"Unknown distribution type {type(distribution)}") diff --git a/ConfigSpace/c_util.pyx b/ConfigSpace/c_util.pyx index 29394bbe..1f028305 100644 --- a/ConfigSpace/c_util.pyx +++ b/ConfigSpace/c_util.pyx @@ -8,7 +8,12 @@ from ConfigSpace.hyperparameters.hyperparameter cimport Hyperparameter from ConfigSpace.conditions import ConditionComponent from ConfigSpace.conditions cimport ConditionComponent from ConfigSpace.conditions import OrConjunction -from ConfigSpace.exceptions import ForbiddenValueError +from ConfigSpace.exceptions import ( + ForbiddenValueError, + IllegalValueError, + ActiveHyperparameterNotSetError, + InactiveHyperparameterSetError, +) from libc.stdlib cimport malloc, free cimport numpy as np @@ -73,10 +78,7 @@ cpdef int check_configuration( if not np.isnan(hp_value) and not hyperparameter.is_legal_vector(hp_value): free(active) - raise ValueError("Hyperparameter instantiation '%s' " - "(type: %s) is illegal for hyperparameter %s" % - (hp_value, str(type(hp_value)), - hyperparameter)) + raise IllegalValueError(hyperparameter, hp_value) children = self._children_of[hp_name] for child in children: @@ -95,8 +97,7 @@ cpdef int check_configuration( if active[hp_idx] and np.isnan(hp_value): free(active) - raise ValueError("Active hyperparameter '%s' not specified!" % - hyperparameter.name) + raise ActiveHyperparameterNotSetError(hyperparameter) for hp_idx in self._idx_to_hyperparameter: @@ -105,9 +106,8 @@ cpdef int check_configuration( hp_name = self._idx_to_hyperparameter[hp_idx] hp_value = vector[hp_idx] free(active) - raise ValueError("Inactive hyperparameter '%s' must not be " - "specified, but has the vector value: '%s'." % - (hp_name, hp_value)) + raise InactiveHyperparameterSetError(hyperparameter, hp_value) + free(active) self._check_forbidden(vector) @@ -148,11 +148,8 @@ cpdef np.ndarray correct_sampled_array( clause = forbidden_clauses_unconditionals[j] if clause.c_is_forbidden_vector(vector, strict=False): free(active) - raise ForbiddenValueError( - "Given vector violates forbidden clause %s" % ( - str(clause) - ) - ) + msg = "Given vector violates forbidden clause %s" % str(clause) + raise ForbiddenValueError(msg) hps = deque() visited = set() @@ -223,9 +220,8 @@ cpdef np.ndarray correct_sampled_array( for j in range(len(forbidden_clauses_conditionals)): clause = forbidden_clauses_conditionals[j] if clause.c_is_forbidden_vector(vector, strict=False): - raise ForbiddenValueError( - "Given vector violates forbidden clause %s" % ( - str(clause))) + msg = "Given vector violates forbidden clause %s" % str(clause) + raise ForbiddenValueError(msg) return vector diff --git a/ConfigSpace/configuration.py b/ConfigSpace/configuration.py new file mode 100644 index 00000000..2d82c38e --- /dev/null +++ b/ConfigSpace/configuration.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING, Any, Iterator, KeysView, Mapping, Sequence + +import numpy as np + +from ConfigSpace import c_util +from ConfigSpace.exceptions import HyperparameterNotFoundError, IllegalValueError +from ConfigSpace.hyperparameters import FloatHyperparameter + +if TYPE_CHECKING: + from ConfigSpace.configuration_space import ConfigurationSpace + + +class Configuration(Mapping[str, Any]): + def __init__( + self, + configuration_space: ConfigurationSpace, + values: Mapping[str, str | float | int | None] | None = None, + vector: Sequence[float] | np.ndarray | None = None, + allow_inactive_with_values: bool = False, + origin: Any | None = None, + config_id: int | None = None, + ) -> None: + """Class for a single configuration. + + The :class:`~ConfigSpace.configuration_space.Configuration` object holds + for all active hyperparameters a value. While the + :class:`~ConfigSpace.configuration_space.ConfigurationSpace` stores the + definitions for the hyperparameters (value ranges, constraints,...), a + :class:`~ConfigSpace.configuration_space.Configuration` object is + more an instance of it. Parameters of a + :class:`~ConfigSpace.configuration_space.Configuration` object can be + accessed and modified similar to python dictionaries + (c.f. :ref:`Guide<1st_Example>`). + + Parameters + ---------- + configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace` + values : dict, optional + A dictionary with pairs (hyperparameter_name, value), where value is + a legal value of the hyperparameter in the above configuration_space + vector : np.ndarray, optional + A numpy array for efficient representation. Either values or vector + has to be given + allow_inactive_with_values : bool, optional + Whether an Exception will be raised if a value for an inactive + hyperparameter is given. Default is to raise an Exception. + Default to False + origin : Any, optional + Store information about the origin of this configuration. Defaults to None + config_id : int, optional + Integer configuration ID which can be used by a program using the ConfigSpace + package. + """ + if values is not None and vector is not None or values is None and vector is None: + raise ValueError("Specify Configuration as either a dictionary or a vector.") + + self.config_space = configuration_space + self.allow_inactive_with_values = allow_inactive_with_values + self.origin = origin + self.config_id = config_id + + # This is cached. When it's None, it means it needs to be relaoaded + # which is primarly handled in __getitem__. + self._values: dict[str, Any] | None = None + + # Will be set below + self._vector: np.ndarray + + if values is not None: + unknown_keys = values.keys() - self.config_space._hyperparameters.keys() + if any(unknown_keys): + raise ValueError(f"Unknown hyperparameter(s) {unknown_keys}") + + # Using cs._hyperparameters to iterate makes sure that the hyperparameters in + # the configuration are sorted in the same way as they are sorted in + # the configuration space + self._values = {} + self._vector = np.ndarray(shape=len(configuration_space), dtype=float) + + for i, (key, hp) in enumerate(configuration_space.items()): + value = values.get(key) + if value is None: + self._vector[i] = np.nan # By default, represent None values as NaN + continue + + if not hp.is_legal(value): + raise IllegalValueError(hp, value) + + # Truncate the float to be of constant length for a python version + if isinstance(hp, FloatHyperparameter): + value = float(repr(value)) + + self._values[key] = value + self._vector[i] = hp._inverse_transform(value) + + self.is_valid_configuration() + + elif vector is not None: + _vector = np.asarray(vector, dtype=float) + + # If we have a 2d array with shape (n, 1), flatten it + if len(_vector.shape) == 2 and _vector.shape[1] == 1: + _vector = _vector.flatten() + + if len(_vector.shape) > 1: + raise ValueError( + "Only 1d arrays can be converted to a Configuration, " + f"you passed an array of shape {_vector.shape}", + ) + + n_hyperparameters = len(self.config_space) + if len(_vector) != len(self.config_space): + raise ValueError( + f"Expected array of length {n_hyperparameters}, got {len(_vector)}", + ) + + self._vector = _vector + + def is_valid_configuration(self) -> None: + """Check if the object is a valid. + + Raises + ------ + ValueError: If configuration is not valid. + """ + c_util.check_configuration( + self.config_space, + self._vector, + allow_inactive_with_values=self.allow_inactive_with_values, + ) + + def get_array(self) -> np.ndarray: + """The internal vector representation of this config. + + All continuous values are scaled between zero and one. + + Returns + ------- + numpy.ndarray + The vector representation of the configuration + """ + return self._vector + + def __contains__(self, item: object) -> bool: + if not isinstance(item, str): + return False + + return item in self + + def __setitem__(self, key: str, value: Any) -> None: + param = self.config_space[key] + if not param.is_legal(value): + raise IllegalValueError(param, value) + + idx = self.config_space._hyperparameter_idx[key] + + # Recalculate the vector with respect to this new value + vector_value = param._inverse_transform(value) + new_array = c_util.change_hp_value( + self.config_space, + self.get_array().copy(), + param.name, + vector_value, + idx, + ) + c_util.check_configuration(self.config_space, new_array, False) + + # Reset cached items + self._vector = new_array + self._values = None + + def __getitem__(self, key: str) -> Any: + if self._values is not None and key in self._values: + return self._values[key] + + if key not in self.config_space: + raise HyperparameterNotFoundError(key, space=self.config_space) + + item_idx = self.config_space._hyperparameter_idx[key] + + raw_value = self._vector[item_idx] + if not np.isfinite(raw_value): + # NOTE: Techinically we could raise an `InactiveHyperparameterError` here + # but that causes the `.get()` method from being a mapping to fail. + # Normally `config.get(key)`, if it fails, will return None. Apparently, + # this only works if `__getitem__[]` raises a KeyError or something derived + # from it. + raise KeyError(key) + + hyperparameter = self.config_space._hyperparameters[key] + value = hyperparameter._transform(raw_value) + + # Truncate float to be of constant length for a python version + if isinstance(hyperparameter, FloatHyperparameter): + value = float(repr(value)) + + if self._values is None: + self._values = {} + + self._values[key] = value + return value + + def keys(self) -> KeysView[str]: + """Return the keys of the configuration. + + Returns + ------- + KeysView[str] + The keys of the configuration + """ + d = { + key: self._vector[idx] + for idx, key in enumerate(self.config_space.keys()) + if np.isfinite(self._vector[idx]) + } + return d.keys() + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return dict(self) == dict(other) and self.config_space == other.config_space + return NotImplemented + + def __hash__(self) -> int: + return hash(self.__repr__()) + + def __repr__(self) -> str: + values = dict(self) + header = "Configuration(values={" + lines = [f" '{key}': {repr(values[key])}," for key in sorted(values.keys())] + end = "})" + return "\n".join([header, *lines, end]) + + def __iter__(self) -> Iterator[str]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self.config_space) + + # ------------ Marked Deprecated -------------------- + # Probably best to only remove these once we actually + # make some other breaking changes + # * Search `Marked Deprecated` to find others + def get_dictionary(self) -> dict[str, Any]: + """A representation of the :class:`~ConfigSpace.configuration_space.Configuration` + in dictionary form. + + Returns + ------- + dict + Configuration as dictionary + """ + warnings.warn( + "`Configuration` act's like a dictionary." + " Please use `dict(config)` instead of `get_dictionary`" + " if you explicitly need a `dict`", + DeprecationWarning, + stacklevel=2, + ) + return dict(self) + + # --------------------------------------------------- diff --git a/ConfigSpace/configuration_space.py b/ConfigSpace/configuration_space.py new file mode 100644 index 00000000..7f7a7953 --- /dev/null +++ b/ConfigSpace/configuration_space.py @@ -0,0 +1,1581 @@ +# Copyright (c) 2014-2016, ConfigSpace developers +# Matthias Feurer +# Katharina Eggensperger +# and others (see commit history). +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from __future__ import annotations + +import contextlib +import copy +import io +import warnings +from collections import OrderedDict, defaultdict, deque +from itertools import chain +from typing import Any, Iterable, Iterator, KeysView, Mapping, cast, overload +from typing_extensions import Final + +import numpy as np + +import ConfigSpace.c_util +from ConfigSpace import nx +from ConfigSpace.conditions import ( + AbstractCondition, + AbstractConjunction, + ConditionComponent, + EqualsCondition, +) +from ConfigSpace.configuration import Configuration +from ConfigSpace.exceptions import ( + ActiveHyperparameterNotSetError, + AmbiguousConditionError, + ChildNotFoundError, + CyclicDependancyError, + ForbiddenValueError, + HyperparameterAlreadyExistsError, + HyperparameterIndexError, + HyperparameterNotFoundError, + IllegalValueError, + InactiveHyperparameterSetError, + ParentNotFoundError, +) +from ConfigSpace.forbidden import ( + AbstractForbiddenClause, + AbstractForbiddenComponent, + AbstractForbiddenConjunction, + ForbiddenRelation, +) +from ConfigSpace.hyperparameters import ( + CategoricalHyperparameter, + Constant, + Hyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, +) + +_ROOT: Final = "__HPOlib_configuration_space_root__" + + +def _parse_hyperparameters_from_dict(items: dict[str, Any]) -> Iterator[Hyperparameter]: + for name, hp in items.items(): + # Anything that is a Hyperparameter already is good + # Note that we discard the key name in this case in favour + # of the name given in the dictionary + if isinstance(hp, Hyperparameter): + yield hp + + # Tuples are bounds, check if float or int + elif isinstance(hp, tuple): + if len(hp) != 2: + raise ValueError(f"'{name}' must be (lower, upper) bound, got {hp}") + + lower, upper = hp + if isinstance(lower, float): + yield UniformFloatHyperparameter(name, lower, upper) + else: + yield UniformIntegerHyperparameter(name, lower, upper) + + # Lists are categoricals + elif isinstance(hp, list): + if len(hp) == 0: + raise ValueError(f"Can't have empty list for categorical {name}") + + yield CategoricalHyperparameter(name, hp) + + # If it's an allowed type, it's a constant + elif isinstance(hp, (int, str, float)): + yield Constant(name, hp) + + else: + raise ValueError(f"Unknown value '{hp}' for '{name}'") + + +def _assert_type(item: Any, expected: type, method: str | None = None) -> None: + if not isinstance(item, expected): + msg = f"Expected {expected}, got {type(item)}" + if method: + msg += " in method " + method + raise TypeError(msg) + + +def _assert_legal(hyperparameter: Hyperparameter, value: tuple | list | Any) -> None: + if isinstance(value, (tuple, list)): + for v in value: + if not hyperparameter.is_legal(v): + raise IllegalValueError(hyperparameter, v) + elif not hyperparameter.is_legal(value): + raise IllegalValueError(hyperparameter, value) + + +class ConfigurationSpace(Mapping[str, Hyperparameter]): + """A collection-like object containing a set of hyperparameter definitions and conditions. + + A configuration space organizes all hyperparameters and its conditions + as well as its forbidden clauses. Configurations can be sampled from + this configuration space. As underlying data structure, the + configuration space uses a tree-based approach to represent the + conditions and restrictions between hyperparameters. + """ + + def __init__( + self, + name: str | dict | None = None, + seed: int | None = None, + meta: dict | None = None, + *, + space: None + | ( + dict[ + str, + tuple[int, int] | tuple[float, float] | list[Any] | int | float | str, + ] + ) = None, + ) -> None: + """ + + Parameters + ---------- + name : str | dict, optional + Name of the configuration space. If a dict is passed, this is considered the same + as the `space` arg. + seed : int, optional + Random seed + meta : dict, optional + Field for holding meta data provided by the user. + Not used by the configuration space. + space: + A simple configuration space to use: + + .. code:: python + + ConfigurationSpace( + name="myspace", + space={ + "uniform_integer": (1, 10), + "uniform_float": (1.0, 10.0), + "categorical": ["a", "b", "c"], + "constant": 1337, + } + ) + + """ + # If first arg is a dict, we assume this to be `space` + if isinstance(name, dict): + space = name + name = None + + self.name = name + self.meta = meta + + # NOTE: The idx of a hyperparamter is tied to its order in _hyperparamters + # Having three variables to keep track of this seems excessive + self._hyperparameters: OrderedDict[str, Hyperparameter] = OrderedDict() + self._hyperparameter_idx: dict[str, int] = {} + self._idx_to_hyperparameter: dict[int, str] = {} + + # Use dictionaries to make sure that we don't accidently add + # additional keys to these mappings (which happened with defaultdict()). + # This once broke auto-sklearn's equal comparison of configuration + # spaces when _children of one instance contained all possible + # hyperparameters as keys and empty dictionaries as values while the + # other instance not containing these. + self._children: OrderedDict[str, OrderedDict[str, None | AbstractCondition]] + self._children = OrderedDict() + + self._parents: OrderedDict[str, OrderedDict[str, None | AbstractCondition]] + self._parents = OrderedDict() + + # Changing this to a normal dict will break sampling because there is + # no guarantee that the parent of a condition was evaluated before + self._conditionals: set[str] = set() + self.forbidden_clauses: list[AbstractForbiddenComponent] = [] + self.random = np.random.RandomState(seed) + + self._children[_ROOT] = OrderedDict() + + self._parent_conditions_of: dict[str, list[AbstractCondition]] = {} + self._child_conditions_of: dict[str, list[AbstractCondition]] = {} + self._parents_of: dict[str, list[Hyperparameter]] = {} + self._children_of: dict[str, list[Hyperparameter]] = {} + + if space is not None: + hyperparameters = list(_parse_hyperparameters_from_dict(space)) + self.add_hyperparameters(hyperparameters) + + def add_hyperparameter(self, hyperparameter: Hyperparameter) -> Hyperparameter: + """Add a hyperparameter to the configuration space. + + Parameters + ---------- + hyperparameter : :ref:`Hyperparameters` + The hyperparameter to add + + Returns + ------- + :ref:`Hyperparameters` + The added hyperparameter + """ + _assert_type(hyperparameter, Hyperparameter, method="add_hyperparameter") + + self._add_hyperparameter(hyperparameter) + self._update_cache() + self._check_default_configuration() + self._sort_hyperparameters() + + return hyperparameter + + def add_hyperparameters( + self, + hyperparameters: Iterable[Hyperparameter], + ) -> list[Hyperparameter]: + """Add hyperparameters to the configuration space. + + Parameters + ---------- + hyperparameters : Iterable(:ref:`Hyperparameters`) + Collection of hyperparameters to add + + Returns + ------- + list(:ref:`Hyperparameters`) + List of added hyperparameters (same as input) + """ + hyperparameters = list(hyperparameters) + for hp in hyperparameters: + _assert_type(hp, Hyperparameter, method="add_hyperparameters") + + for hyperparameter in hyperparameters: + self._add_hyperparameter(hyperparameter) + + self._update_cache() + self._check_default_configuration() + self._sort_hyperparameters() + return hyperparameters + + def add_condition(self, condition: ConditionComponent) -> ConditionComponent: + """Add a condition to the configuration space. + + Check if adding the condition is legal: + + - The parent in a condition statement must exist + - The condition must add no cycles + + The internal array keeps track of all edges which must be + added to the DiGraph; if the checks don't raise any Exception, + these edges are finally added at the end of the function. + + Parameters + ---------- + condition : :ref:`Conditions` + Condition to add + + Returns + ------- + :ref:`Conditions` + Same condition as input + """ + _assert_type(condition, ConditionComponent, method="add_condition") + + if isinstance(condition, AbstractCondition): + self._check_edges([(condition.parent, condition.child)], [condition.value]) + self._check_condition(condition.child, condition) + self._add_edge(condition.parent, condition.child, condition=condition) + + # Loop over the Conjunctions to find out the conditions we must add! + elif isinstance(condition, AbstractConjunction): + dlcs = condition.get_descendant_literal_conditions() + edges = [(dlc.parent, dlc.child) for dlc in dlcs] + values = [dlc.value for dlc in dlcs] + self._check_edges(edges, values) + + for dlc in dlcs: + self._check_condition(dlc.child, condition) + self._add_edge(dlc.parent, dlc.child, condition=condition) + + else: + raise Exception("This should never happen!") + + self._sort_hyperparameters() + self._update_cache() + return condition + + def add_conditions( + self, + conditions: list[ConditionComponent], + ) -> list[ConditionComponent]: + """Add a list of conditions to the configuration space. + + They must be legal. Take a look at + :meth:`~ConfigSpace.configuration_space.ConfigurationSpace.add_condition`. + + Parameters + ---------- + conditions : list(:ref:`Conditions`) + collection of conditions to add + + Returns + ------- + list(:ref:`Conditions`) + Same as input conditions + """ + for condition in conditions: + _assert_type(condition, ConditionComponent, method="add_conditions") + + edges = [] + values = [] + conditions_to_add = [] + for condition in conditions: + if isinstance(condition, AbstractCondition): + edges.append((condition.parent, condition.child)) + values.append(condition.value) + conditions_to_add.append(condition) + elif isinstance(condition, AbstractConjunction): + dlcs = condition.get_descendant_literal_conditions() + edges.extend([(dlc.parent, dlc.child) for dlc in dlcs]) + values.extend([dlc.value for dlc in dlcs]) + conditions_to_add.extend([condition] * len(dlcs)) + + for edge, condition in zip(edges, conditions_to_add): + self._check_condition(edge[1], condition) + + self._check_edges(edges, values) + for edge, condition in zip(edges, conditions_to_add): + self._add_edge(edge[0], edge[1], condition) + + self._sort_hyperparameters() + self._update_cache() + return conditions + + def add_forbidden_clause( + self, + clause: AbstractForbiddenComponent, + ) -> AbstractForbiddenComponent: + """ + Add a forbidden clause to the configuration space. + + Parameters + ---------- + clause : :ref:`Forbidden clauses` + Forbidden clause to add + + Returns + ------- + :ref:`Forbidden clauses` + Same as input forbidden clause + """ + self._check_forbidden_component(clause=clause) + clause.set_vector_idx(self._hyperparameter_idx) + self.forbidden_clauses.append(clause) + self._check_default_configuration() + return clause + + def add_forbidden_clauses( + self, + clauses: list[AbstractForbiddenComponent], + ) -> list[AbstractForbiddenComponent]: + """ + Add a list of forbidden clauses to the configuration space. + + Parameters + ---------- + clauses : list(:ref:`Forbidden clauses`) + Collection of forbidden clauses to add + + Returns + ------- + list(:ref:`Forbidden clauses`) + Same as input clauses + """ + for clause in clauses: + self._check_forbidden_component(clause=clause) + clause.set_vector_idx(self._hyperparameter_idx) + self.forbidden_clauses.append(clause) + + self._check_default_configuration() + return clauses + + def add_configuration_space( + self, + prefix: str, + configuration_space: ConfigurationSpace, + delimiter: str = ":", + parent_hyperparameter: dict | None = None, + ) -> ConfigurationSpace: + """ + Combine two configuration space by adding one the other configuration + space. The contents of the configuration space, which should be added, + are renamed to ``prefix`` + ``delimiter`` + old_name. + + Parameters + ---------- + prefix : str + The prefix for the renamed hyperparameter | conditions | + forbidden clauses + configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace` + The configuration space which should be added + delimiter : str, optional + Defaults to ':' + parent_hyperparameter : dict | None = None + Adds for each new hyperparameter the condition, that + ``parent_hyperparameter`` is active. Must be a dictionary with two keys + "parent" and "value", meaning that the added configuration space is active + when `parent` is equal to `value` + + Returns + ------- + :class:`~ConfigSpace.configuration_space.ConfigurationSpace` + The configuration space, which was added + """ + _assert_type(configuration_space, ConfigurationSpace, method="add_configuration_space") + + prefix_delim = f"{prefix}{delimiter}" + + def _new_name(_item: Hyperparameter) -> str: + if _item.name in ("", prefix): + return prefix + + if not _item.name.startswith(prefix_delim): + return f"{prefix_delim}{_item.name}" + + return cast(str, _item.name) + + new_parameters = [] + for hp in configuration_space.values(): + new_hp = copy.copy(hp) + new_hp.name = _new_name(hp) + new_parameters.append(new_hp) + + self.add_hyperparameters(new_parameters) + + conditions_to_add = [] + for condition in configuration_space.get_conditions(): + new_condition = copy.copy(condition) + for dlc in new_condition.get_descendant_literal_conditions(): + # Rename children + dlc.child.name = _new_name(dlc.child) + dlc.parent.name = _new_name(dlc.parent) + + conditions_to_add.append(new_condition) + + self.add_conditions(conditions_to_add) + + forbiddens_to_add = [] + for forbidden_clause in configuration_space.forbidden_clauses: + new_forbidden = forbidden_clause + for dlc in new_forbidden.get_descendant_literal_clauses(): + if isinstance(dlc, ForbiddenRelation): + dlc.left.name = _new_name(dlc.left) + dlc.right.name = _new_name(dlc.right) + else: + dlc.hyperparameter.name = _new_name(dlc.hyperparameter) + forbiddens_to_add.append(new_forbidden) + + self.add_forbidden_clauses(forbiddens_to_add) + + conditions_to_add = [] + if parent_hyperparameter is not None: + parent = parent_hyperparameter["parent"] + value = parent_hyperparameter["value"] + + # Only add a condition if the parameter is a top-level parameter of the new + # configuration space (this will be some kind of tree structure). + for new_hp in new_parameters: + parents = self.get_parents_of(new_hp) + if not any(parents): + condition = EqualsCondition(new_hp, parent, value) + conditions_to_add.append(condition) + + self.add_conditions(conditions_to_add) + + return configuration_space + + def get_hyperparameter_by_idx(self, idx: int) -> str: + """Name of a hyperparameter from the space given its id. + + Parameters + ---------- + idx : int + Id of a hyperparameter + + Returns + ------- + str + Name of the hyperparameter + """ + hp = self._idx_to_hyperparameter.get(idx) + if hp is None: + raise HyperparameterIndexError(idx, self) + + return hp + + def get_idx_by_hyperparameter_name(self, name: str) -> int: + """The id of a hyperparameter by its ``name``. + + Parameters + ---------- + name : str + Name of a hyperparameter + + Returns + ------- + int + Id of the hyperparameter with name ``name`` + """ + idx = self._hyperparameter_idx.get(name) + + if idx is None: + raise HyperparameterNotFoundError(name, space=self) + + return idx + + def get_conditions(self) -> list[AbstractCondition]: + """All conditions from the configuration space. + + Returns + ------- + list(:ref:`Conditions`) + Conditions of the configuration space + """ + conditions = [] + added_conditions: set[str] = set() + + # Nodes is a list of nodes + for source_node in self._hyperparameters.values(): + # This is a list of keys in a dictionary + # TODO sort the edges by the order of their source_node in the + # hyperparameter list! + for target_node in self._children[source_node.name]: + if target_node not in added_conditions: + condition = self._children[source_node.name][target_node] + conditions.append(condition) + added_conditions.add(target_node) + + return conditions + + def get_forbiddens(self) -> list[AbstractForbiddenComponent]: + """All forbidden clauses from the configuration space. + + Returns + ------- + list(:ref:`Forbidden clauses`) + List with the forbidden clauses + """ + return self.forbidden_clauses + + def get_children_of(self, name: str | Hyperparameter) -> list[Hyperparameter]: + """ + Return a list with all children of a given hyperparameter. + + Parameters + ---------- + name : str, :ref:`Hyperparameters` + Hyperparameter or its name, for which all children are requested + + Returns + ------- + list(:ref:`Hyperparameters`) + Children of the hyperparameter + """ + conditions = self.get_child_conditions_of(name) + parents: list[Hyperparameter] = [] + for condition in conditions: + parents.extend(condition.get_children()) + return parents + + def generate_all_continuous_from_bounds( + self, + bounds: list[tuple[float, float]], + ) -> None: + """Generate :class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter` + from a list containing lists with lower and upper bounds. + + The generated hyperparameters are added to the configuration space. + + Parameters + ---------- + bounds : list[tuple([float, float])] + List containing lists with two elements: lower and upper bound + """ + self.add_hyperparameters( + [ + UniformFloatHyperparameter(name=f"x{i}", lower=lower, upper=upper) + for i, (lower, upper) in enumerate(bounds) + ], + ) + + def get_child_conditions_of( + self, + name: str | Hyperparameter, + ) -> list[AbstractCondition]: + """ + Return a list with conditions of all children of a given + hyperparameter referenced by its ``name``. + + Parameters + ---------- + name : str, :ref:`Hyperparameters` + Hyperparameter or its name, for which conditions are requested + + Returns + ------- + list(:ref:`Conditions`) + List with the conditions on the children of the given hyperparameter + """ + name = name if isinstance(name, str) else name.name + + # This raises an exception if the hyperparameter does not exist + self[name] + return self._get_child_conditions_of(name) + + def get_parents_of(self, name: str | Hyperparameter) -> list[Hyperparameter]: + """The parents hyperparameters of a given hyperparameter. + + Parameters + ---------- + name : str, :ref:`Hyperparameters` + Can either be the name of a hyperparameter or the hyperparameter + object. + + Returns + ------- + list[:ref:`Conditions`] + List with all parent hyperparameters + """ + conditions = self.get_parent_conditions_of(name) + parents: list[Hyperparameter] = [] + for condition in conditions: + parents.extend(condition.get_parents()) + return parents + + def get_parent_conditions_of( + self, + name: str | Hyperparameter, + ) -> list[AbstractCondition]: + """The conditions of all parents of a given hyperparameter. + + Parameters + ---------- + name : str, :ref:`Hyperparameters` + Can either be the name of a hyperparameter or the hyperparameter + object + + Returns + ------- + list[:ref:`Conditions`] + List with all conditions on parent hyperparameters + """ + if isinstance(name, Hyperparameter): + name = name.name # type: ignore + + # This raises an exception if the hyperparameter does not exist + self[name] + return self._get_parent_conditions_of(name) + + def get_all_unconditional_hyperparameters(self) -> list[str]: + """Names of unconditional hyperparameters. + + Returns + ------- + list[:ref:`Hyperparameters`] + List with all parent hyperparameters, which are not part of a condition + """ + return list(self._children[_ROOT]) + + def get_all_conditional_hyperparameters(self) -> set[str]: + """Names of all conditional hyperparameters. + + Returns + ------- + set[:ref:`Hyperparameters`] + Set with all conditional hyperparameter + """ + return self._conditionals + + def get_default_configuration(self) -> Configuration: + """Configuration containing hyperparameters with default values. + + Returns + ------- + :class:`~ConfigSpace.configuration_space.Configuration` + Configuration with the set default values + + """ + return self._check_default_configuration() + + # For backward compatibility + def check_configuration(self, configuration: Configuration) -> None: + """ + Check if a configuration is legal. Raises an error if not. + + Parameters + ---------- + configuration : :class:`~ConfigSpace.configuration_space.Configuration` + Configuration to check + """ + _assert_type(configuration, Configuration, method="check_configuration") + ConfigSpace.c_util.check_configuration(self, configuration.get_array(), False) + + def check_configuration_vector_representation(self, vector: np.ndarray) -> None: + """ + Raise error if configuration in vector representation is not legal. + + Parameters + ---------- + vector : np.ndarray + Configuration in vector representation + """ + _assert_type(vector, np.ndarray, method="check_configuration_vector_representation") + ConfigSpace.c_util.check_configuration(self, vector, False) + + def get_active_hyperparameters( + self, + configuration: Configuration, + ) -> set[Hyperparameter]: + """Set of active hyperparameter for a given configuration. + + Parameters + ---------- + configuration : :class:`~ConfigSpace.configuration_space.Configuration` + Configuration for which the active hyperparameter are returned + + Returns + ------- + set(:class:`~ConfigSpace.configuration_space.Configuration`) + The set of all active hyperparameter + + """ + vector = configuration.get_array() + active_hyperparameters = set() + for hp_name, hyperparameter in self._hyperparameters.items(): + conditions = self._parent_conditions_of[hyperparameter.name] + + active = True + for condition in conditions: + parent_vector_idx = condition.get_parents_vector() + + # if one of the parents is None, the hyperparameter cannot be + # active! Else we have to check this + # Note from trying to optimize this - this is faster than using + # dedicated numpy functions and indexing + if any(vector[i] != vector[i] for i in parent_vector_idx): + active = False + break + + if not condition.evaluate_vector(vector): + active = False + break + + if active: + active_hyperparameters.add(hp_name) + + return active_hyperparameters + + @overload + def sample_configuration(self, size: None = None) -> Configuration: + ... + + # Technically this is wrong given the current behaviour but it's + # sufficient for most cases. Once deprecation warning is up, + # we can just have `1` always return a list of configurations + # because an `int` was specified, `None` for single config. + @overload + def sample_configuration(self, size: int) -> list[Configuration]: + ... + + def sample_configuration( + self, + size: int | None = None, + ) -> Configuration | list[Configuration]: + """ + Sample ``size`` configurations from the configuration space object. + + Parameters + ---------- + size : int, optional + Number of configurations to sample. Default to 1 + + Returns + ------- + :class:`~ConfigSpace.configuration_space.Configuration`, + list[:class:`~ConfigSpace.configuration_space.Configuration`]: + A single configuration if ``size`` 1 else a list of Configurations + """ + if size == 1: + warnings.warn( + "Please leave at default or explicitly set `size=None`." + " In the future, specifying a size will always retunr a list, even if 1", + DeprecationWarning, + stacklevel=2, + ) + + # Maintain old behaviour by setting this + if size is None: + size = 1 + + _assert_type(size, int, method="sample_configuration") + if size < 1: + return [] + + iteration = 0 + missing = size + accepted_configurations: list[Configuration] = [] + num_hyperparameters = len(self._hyperparameters) + + unconditional_hyperparameters = self.get_all_unconditional_hyperparameters() + hyperparameters_with_children = [] + + _forbidden_clauses_unconditionals = [] + _forbidden_clauses_conditionals = [] + for clause in self.get_forbiddens(): + based_on_conditionals = False + for subclause in clause.get_descendant_literal_clauses(): + if isinstance(subclause, ForbiddenRelation): + if ( + subclause.left.name not in unconditional_hyperparameters + or subclause.right.name not in unconditional_hyperparameters + ): + based_on_conditionals = True + break + elif subclause.hyperparameter.name not in unconditional_hyperparameters: + based_on_conditionals = True + break + if based_on_conditionals: + _forbidden_clauses_conditionals.append(clause) + else: + _forbidden_clauses_unconditionals.append(clause) + + for uhp in unconditional_hyperparameters: + children = self._children_of[uhp] + if len(children) > 0: + hyperparameters_with_children.append(uhp) + + while len(accepted_configurations) < size: + if missing != size: + missing = int(1.1 * missing) + vector: np.ndarray = np.ndarray((missing, num_hyperparameters), dtype=float) + + for i, hp_name in enumerate(self._hyperparameters): + hyperparameter = self._hyperparameters[hp_name] + vector[:, i] = hyperparameter._sample(self.random, missing) + + for i in range(missing): + try: + configuration = Configuration( + self, + vector=ConfigSpace.c_util.correct_sampled_array( + vector[i].copy(), + _forbidden_clauses_unconditionals, + _forbidden_clauses_conditionals, + hyperparameters_with_children, + num_hyperparameters, + unconditional_hyperparameters, + self._hyperparameter_idx, + self._parent_conditions_of, + self._parents_of, + self._children_of, + ), + ) + accepted_configurations.append(configuration) + except ForbiddenValueError: + iteration += 1 + + if iteration == size * 100: + msg = (f"Cannot sample valid configuration for {self}",) + raise ForbiddenValueError(msg) from None + + missing = size - len(accepted_configurations) + + if size <= 1: + return accepted_configurations[0] + + return accepted_configurations + + def seed(self, seed: int) -> None: + """Set the random seed to a number. + + Parameters + ---------- + seed : int + The random seed + """ + self.random = np.random.RandomState(seed) + + def remove_hyperparameter_priors(self) -> ConfigurationSpace: + """Produces a new ConfigurationSpace where all priors on parameters are removed. + + Non-uniform hyperpararmeters are replaced with uniform ones, and + CategoricalHyperparameters with weights have their weights removed. + + Returns + ------- + :class:`~ConfigSpace.configuration_space.ConfigurationSpace` + The resulting configuration space, without priors on the hyperparameters + """ + uniform_config_space = ConfigurationSpace() + for parameter in self.values(): + if hasattr(parameter, "to_uniform"): + uniform_config_space.add_hyperparameter(parameter.to_uniform()) + else: + uniform_config_space.add_hyperparameter(copy.copy(parameter)) + + new_conditions = self.substitute_hyperparameters_in_conditions( + self.get_conditions(), + uniform_config_space, + ) + new_forbiddens = self.substitute_hyperparameters_in_forbiddens( + self.get_forbiddens(), + uniform_config_space, + ) + uniform_config_space.add_conditions(new_conditions) + uniform_config_space.add_forbidden_clauses(new_forbiddens) + + return uniform_config_space + + def estimate_size(self) -> float | int: + """Estimate the size of the current configuration space (i.e. unique configurations). + + This is ``np.inf`` in case if there is a single hyperparameter of size ``np.inf`` (i.e. a + :class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter`), otherwise + it is the product of the size of all hyperparameters. The function correctly guesses the + number of unique configurations if there are no condition and forbidden statements in the + configuration spaces. Otherwise, this is an upper bound. Use + :func:`~ConfigSpace.util.generate_grid` to generate all valid configurations if required. + + Returns + ------- + Union[float, int] + """ + sizes = [hp.get_size() for hp in self._hyperparameters.values()] + + if len(sizes) == 0: + return 0.0 + + acc = 1 + for size in sizes: + acc *= size + + return acc + + @staticmethod + def substitute_hyperparameters_in_conditions( + conditions: Iterable[ConditionComponent], + new_configspace: ConfigurationSpace, + ) -> list[ConditionComponent]: + """ + Takes a set of conditions and generates a new set of conditions with the same structure, + where each hyperparameter is replaced with its namesake in new_configspace. As such, the + set of conditions remain unchanged, but the included hyperparameters are changed to match + those types that exist in new_configspace. + + Parameters + ---------- + new_configspace: ConfigurationSpace + A ConfigurationSpace containing hyperparameters with the same names as those in the + conditions. + + Returns + ------- + list[ConditionComponent]: + The list of conditions, adjusted to fit the new ConfigurationSpace + """ + new_conditions = [] + for condition in conditions: + if isinstance(condition, AbstractConjunction): + conjunction_type = type(condition) + children = condition.get_descendant_literal_conditions() + substituted_children = ConfigurationSpace.substitute_hyperparameters_in_conditions( + children, + new_configspace, + ) + substituted_conjunction = conjunction_type(*substituted_children) + new_conditions.append(substituted_conjunction) + + elif isinstance(condition, AbstractCondition): + condition_type = type(condition) + child_name = condition.get_children()[0].name + parent_name = condition.get_parents()[0].name + new_child = new_configspace[child_name] + new_parent = new_configspace[parent_name] + + if hasattr(condition, "values"): + condition_arg = condition.values + substituted_condition = condition_type( + child=new_child, + parent=new_parent, + values=condition_arg, + ) + elif hasattr(condition, "value"): + condition_arg = condition.value + substituted_condition = condition_type( + child=new_child, + parent=new_parent, + value=condition_arg, + ) + else: + raise AttributeError( + f"Did not find the expected attribute in condition {type(condition)}.", + ) + + new_conditions.append(substituted_condition) + else: + raise TypeError(f"Did not expect the supplied condition type {type(condition)}.") + + return new_conditions + + @staticmethod + def substitute_hyperparameters_in_forbiddens( + forbiddens: Iterable[AbstractForbiddenComponent], + new_configspace: ConfigurationSpace, + ) -> list[AbstractForbiddenComponent]: + """ + Takes a set of forbidden clauses and generates a new set of forbidden clauses with the + same structure, where each hyperparameter is replaced with its namesake in new_configspace. + As such, the set of forbidden clauses remain unchanged, but the included hyperparameters are + changed to match those types that exist in new_configspace. + + Parameters + ---------- + forbiddens: Iterable[AbstractForbiddenComponent] + An iterable of forbiddens + new_configspace: ConfigurationSpace + A ConfigurationSpace containing hyperparameters with the same names as those in the + forbidden clauses. + + Returns + ------- + list[AbstractForbiddenComponent]: + The list of forbidden clauses, adjusted to fit the new ConfigurationSpace + """ + new_forbiddens = [] + for forbidden in forbiddens: + if isinstance(forbidden, AbstractForbiddenConjunction): + conjunction_type = type(forbidden) + children = forbidden.get_descendant_literal_clauses() + substituted_children = ConfigurationSpace.substitute_hyperparameters_in_forbiddens( + children, + new_configspace, + ) + substituted_conjunction = conjunction_type(*substituted_children) + new_forbiddens.append(substituted_conjunction) + + elif isinstance(forbidden, AbstractForbiddenClause): + forbidden_type = type(forbidden) + hyperparameter_name = forbidden.hyperparameter.name + new_hyperparameter = new_configspace[hyperparameter_name] + + if hasattr(forbidden, "values"): + forbidden_arg = forbidden.values + substituted_forbidden = forbidden_type( + hyperparameter=new_hyperparameter, + values=forbidden_arg, + ) + elif hasattr(forbidden, "value"): + forbidden_arg = forbidden.value + substituted_forbidden = forbidden_type( + hyperparameter=new_hyperparameter, + value=forbidden_arg, + ) + else: + raise AttributeError( + f"Did not find the expected attribute in forbidden {type(forbidden)}.", + ) + + new_forbiddens.append(substituted_forbidden) + elif isinstance(forbidden, ForbiddenRelation): + forbidden_type = type(forbidden) + left_name = forbidden.left.name + left_hyperparameter = new_configspace[left_name] + right_name = forbidden.right.name + right_hyperparameter = new_configspace[right_name] + + substituted_forbidden = forbidden_type( + left=left_hyperparameter, + right=right_hyperparameter, + ) + new_forbiddens.append(substituted_forbidden) + else: + raise TypeError(f"Did not expect type {type(forbidden)}.") + + return new_forbiddens + + def __eq__(self, other: Any) -> bool: + """Override the default Equals behavior.""" + if isinstance(other, self.__class__): + this_dict = self.__dict__.copy() + del this_dict["random"] + other_dict = other.__dict__.copy() + del other_dict["random"] + return this_dict == other_dict + return NotImplemented + + def __hash__(self) -> int: + """Override the default hash behavior (that returns the id or the object).""" + return hash(self.__repr__()) + + def __getitem__(self, key: str) -> Hyperparameter: + hp = self._hyperparameters.get(key) + if hp is None: + raise HyperparameterNotFoundError(key, space=self) + + return hp + + def __repr__(self) -> str: + retval = io.StringIO() + retval.write("Configuration space object:\n Hyperparameters:\n") + + if self.name is not None: + retval.write(self.name) + retval.write("\n") + + hyperparameters = sorted(self.values(), key=lambda t: t.name) # type: ignore + if hyperparameters: + retval.write(" ") + retval.write("\n ".join([str(hyperparameter) for hyperparameter in hyperparameters])) + retval.write("\n") + + conditions = sorted(self.get_conditions(), key=lambda t: str(t)) + if conditions: + retval.write(" Conditions:\n") + retval.write(" ") + retval.write("\n ".join([str(condition) for condition in conditions])) + retval.write("\n") + + if self.get_forbiddens(): + retval.write(" Forbidden Clauses:\n") + retval.write(" ") + retval.write("\n ".join([str(clause) for clause in self.get_forbiddens()])) + retval.write("\n") + + retval.seek(0) + return retval.getvalue() + + def __iter__(self) -> Iterator[str]: + """Iterate over the hyperparameter names in the right order.""" + return iter(self._hyperparameters.keys()) + + def keys(self) -> KeysView[str]: + """Return the hyperparameter names in the right order.""" + return self._hyperparameters.keys() + + def __len__(self) -> int: + return len(self._hyperparameters) + + def _add_hyperparameter(self, hyperparameter: Hyperparameter) -> None: + hp_name = hyperparameter.name + + existing = self._hyperparameters.get(hp_name) + if existing is not None: + raise HyperparameterAlreadyExistsError(existing, hyperparameter, space=self) + + self._hyperparameters[hp_name] = hyperparameter + self._children[hp_name] = OrderedDict() + + # TODO remove (_ROOT) __HPOlib_configuration_space_root__, it is only used in + # to check for cyclic configuration spaces. If it is only added when + # cycles are checked, the code can become much easier (e.g. the parent + # caching can be more or less removed). + self._children[_ROOT][hp_name] = None + self._parents[hp_name] = OrderedDict() + self._parents[hp_name][_ROOT] = None + + # Save the index of each hyperparameter name to later on access a + # vector of hyperparameter values by indices, must be done twice + # because check_default_configuration depends on it + self._hyperparameter_idx.update({hp: i for i, hp in enumerate(self._hyperparameters)}) + + def _sort_hyperparameters(self) -> None: + levels: OrderedDict[str, int] = OrderedDict() + to_visit: deque[str] = deque() + for hp_name in self._hyperparameters: + to_visit.appendleft(hp_name) + + while len(to_visit) > 0: + current = to_visit.pop() + if _ROOT in self._parents[current]: + assert len(self._parents[current]) == 1 + levels[current] = 1 + + else: + all_parents_visited = True + depth = -1 + for parent in self._parents[current]: + if parent not in levels: + all_parents_visited = False + break + + depth = max(depth, levels[parent] + 1) + + if all_parents_visited: + levels[current] = depth + else: + to_visit.appendleft(current) + + by_level: defaultdict[int, list[str]] = defaultdict(list) + for hp in levels: + level = levels[hp] + by_level[level].append(hp) + + nodes = [] + # Sort and add to list + for level in sorted(by_level): + sorted_by_level = by_level[level] + sorted_by_level.sort() + nodes.extend(sorted_by_level) + + # Resort the OrderedDict + new_order = OrderedDict() + for node in nodes: + new_order[node] = self._hyperparameters[node] + self._hyperparameters = new_order + + # Update to reflect sorting + for i, hp in enumerate(self._hyperparameters): + self._hyperparameter_idx[hp] = i + self._idx_to_hyperparameter[i] = hp + + # Update order of _children + new_order = OrderedDict() + new_order[_ROOT] = self._children[_ROOT] + for hp in chain([_ROOT], self._hyperparameters): + # Also resort the children dict + children_sorting = [ + (self._hyperparameter_idx[child_name], child_name) + for child_name in self._children[hp] + ] + children_sorting.sort() + children_order = OrderedDict() + for _, child_name in children_sorting: + children_order[child_name] = self._children[hp][child_name] + new_order[hp] = children_order + self._children = new_order + + # Update order of _parents + new_order = OrderedDict() + for hp in self._hyperparameters: + # Also resort the parent's dict + if _ROOT in self._parents[hp]: + parent_sorting = [(-1, _ROOT)] + else: + parent_sorting = [ + (self._hyperparameter_idx[parent_name], parent_name) + for parent_name in self._parents[hp] + ] + parent_sorting.sort() + parent_order = OrderedDict() + for _, parent_name in parent_sorting: + parent_order[parent_name] = self._parents[hp][parent_name] + new_order[hp] = parent_order + self._parents = new_order + + # update conditions + for condition in self.get_conditions(): + condition.set_vector_idx(self._hyperparameter_idx) + + # forbidden clauses + for clause in self.get_forbiddens(): + clause.set_vector_idx(self._hyperparameter_idx) + + def _check_condition( + self, + child_node: Hyperparameter, + condition: ConditionComponent, + ) -> None: + for present_condition in self._get_parent_conditions_of(child_node.name): + if present_condition != condition: + raise AmbiguousConditionError(present_condition, condition) + + def _add_edge( + self, + parent_node: Hyperparameter, + child_node: Hyperparameter, + condition: ConditionComponent, + ) -> None: + with contextlib.suppress(Exception): + # TODO maybe this has to be done more carefully + del self._children[_ROOT][child_node.name] + + with contextlib.suppress(Exception): + del self._parents[child_node.name][_ROOT] + + self._children[parent_node.name][child_node.name] = condition + self._parents[child_node.name][parent_node.name] = condition + + self._conditionals.add(child_node.name) + + def _create_tmp_dag(self) -> nx.DiGraph: + tmp_dag = nx.DiGraph() + for hp_name in self._hyperparameters: + tmp_dag.add_node(hp_name) + tmp_dag.add_edge(_ROOT, hp_name) + + for parent_node_ in self._children: + if parent_node_ == _ROOT: + continue + for child_node_ in self._children[parent_node_]: + with contextlib.suppress(Exception): + tmp_dag.remove_edge(_ROOT, child_node_) + + condition = self._children[parent_node_][child_node_] + tmp_dag.add_edge(parent_node_, child_node_, condition=condition) + + return tmp_dag + + def _check_edges( + self, + edges: list[tuple[Hyperparameter, Hyperparameter]], + values: list[Any], + ) -> None: + for (parent, child), value in zip(edges, values): + # check if both nodes are already inserted into the graph + if child.name not in self._hyperparameters: + raise ChildNotFoundError(child, space=self) + + if parent.name not in self._hyperparameters: + raise ParentNotFoundError(parent, space=self) + + if child != self._hyperparameters[child.name]: + existing = self._hyperparameters[child.name] + raise HyperparameterAlreadyExistsError(existing, child, space=self) + + if parent != self._hyperparameters[parent.name]: + existing = self._hyperparameters[child.name] + raise HyperparameterAlreadyExistsError(existing, child, space=self) + + _assert_legal(parent, value) + + # TODO: recursively check everything which is inside the conditions, + # this means we have to recursively traverse the condition + tmp_dag = self._create_tmp_dag() + for parent, child in edges: + tmp_dag.add_edge(parent.name, child.name) + + if not nx.is_directed_acyclic_graph(tmp_dag): + cycles: list[list[str]] = list(nx.simple_cycles(tmp_dag)) + for cycle in cycles: + cycle.sort() + cycles.sort() + raise CyclicDependancyError(cycles) + + def _update_cache(self) -> None: + self._parent_conditions_of = { + name: self._get_parent_conditions_of(name) for name in self._hyperparameters + } + self._child_conditions_of = { + name: self._get_child_conditions_of(name) for name in self._hyperparameters + } + self._parents_of = {name: self.get_parents_of(name) for name in self._hyperparameters} + self._children_of = {name: self.get_children_of(name) for name in self._hyperparameters} + + def _check_forbidden_component(self, clause: AbstractForbiddenComponent) -> None: + _assert_type(clause, AbstractForbiddenComponent, "_check_forbidden_component") + + to_check = [] + relation_to_check = [] + if isinstance(clause, AbstractForbiddenClause): + to_check.append(clause) + elif isinstance(clause, AbstractForbiddenConjunction): + to_check.extend(clause.get_descendant_literal_clauses()) + elif isinstance(clause, ForbiddenRelation): + relation_to_check.extend(clause.get_descendant_literal_clauses()) + else: + raise NotImplementedError(type(clause)) + + def _check_hp(tmp_clause: AbstractForbiddenComponent, hp: Hyperparameter) -> None: + if hp.name not in self._hyperparameters: + raise HyperparameterNotFoundError( + hp, + space=self, + preamble=f"Cannot add '{tmp_clause}' because it references '{hp.name}'", + ) + + for tmp_clause in to_check: + _check_hp(tmp_clause, tmp_clause.hyperparameter) + + for tmp_clause in relation_to_check: + _check_hp(tmp_clause, tmp_clause.left) + _check_hp(tmp_clause, tmp_clause.right) + + def _get_children_of(self, name: str) -> list[Hyperparameter]: + conditions = self._get_child_conditions_of(name) + parents: list[Hyperparameter] = [] + for condition in conditions: + parents.extend(condition.get_children()) + return parents + + def _get_child_conditions_of(self, name: str) -> list[AbstractCondition]: + children = self._children[name] + return [children[child_name] for child_name in children if child_name != _ROOT] + + def _get_parents_of(self, name: str) -> list[Hyperparameter]: + """The parents hyperparameters of a given hyperparameter. + + Parameters + ---------- + name : str + + Returns + ------- + list + List with all parent hyperparameters + """ + conditions = self._get_parent_conditions_of(name) + parents: list[Hyperparameter] = [] + for condition in conditions: + parents.extend(condition.get_parents()) + return parents + + def _check_default_configuration(self) -> Configuration: + # Check if adding that hyperparameter leads to an illegal default configuration + instantiated_hyperparameters: dict[str, int | float | str | None] = {} + for hp in self.values(): + conditions = self._get_parent_conditions_of(hp.name) + active = True + for condition in conditions: + parent_names = [ + c.parent.name for c in condition.get_descendant_literal_conditions() + ] + + parents = { + parent_name: instantiated_hyperparameters[parent_name] + for parent_name in parent_names + } + + if not condition.evaluate(parents): + # TODO find out why a configuration is illegal! + active = False + + if not active: + instantiated_hyperparameters[hp.name] = None + elif isinstance(hp, Constant): + instantiated_hyperparameters[hp.name] = hp.value + else: + instantiated_hyperparameters[hp.name] = hp.default_value + + # TODO copy paste from check configuration + + # TODO add an extra Exception type for the case that the default + # configuration is forbidden! + return Configuration(self, values=instantiated_hyperparameters) + + def _get_parent_conditions_of(self, name: str) -> list[AbstractCondition]: + parents = self._parents[name] + return [parents[parent_name] for parent_name in parents if parent_name != _ROOT] + + def _check_configuration_rigorous( + self, + configuration: Configuration, + allow_inactive_with_values: bool = False, + ) -> None: + vector = configuration.get_array() + active_hyperparameters = self.get_active_hyperparameters(configuration) + + for hp_name, hyperparameter in self._hyperparameters.items(): + hp_value = vector[self._hyperparameter_idx[hp_name]] + active = hp_name in active_hyperparameters + + if not np.isnan(hp_value) and not hyperparameter.is_legal_vector(hp_value): + raise IllegalValueError(hyperparameter, hp_value) + + if active and np.isnan(hp_value): + raise ActiveHyperparameterNotSetError(hyperparameter) + + if not allow_inactive_with_values and not active and not np.isnan(hp_value): + raise InactiveHyperparameterSetError(hyperparameter, hp_value) + + self._check_forbidden(vector) + + def _check_forbidden(self, vector: np.ndarray) -> None: + ConfigSpace.c_util.check_forbidden(self.forbidden_clauses, vector) + + # ------------ Marked Deprecated -------------------- + # Probably best to only remove these once we actually + # make some other breaking changes + # * Search `Marked Deprecated` to find others + + def get_hyperparameter(self, name: str) -> Hyperparameter: + """Hyperparameter from the space with a given name. + + Parameters + ---------- + name : str + Name of the searched hyperparameter + + Returns + ------- + :ref:`Hyperparameters` + Hyperparameter with the name ``name`` + """ + warnings.warn( + "Prefer `space[name]` over `get_hyperparameter`", + DeprecationWarning, + stacklevel=2, + ) + return self[name] + + def get_hyperparameters(self) -> list[Hyperparameter]: + """All hyperparameters in the space. + + Returns + ------- + list(:ref:`Hyperparameters`) + A list with all hyperparameters stored in the configuration space object + """ + warnings.warn( + "Prefer using `list(space.values())` over `get_hyperparameters`", + DeprecationWarning, + stacklevel=2, + ) + return list(self._hyperparameters.values()) + + def get_hyperparameters_dict(self) -> dict[str, Hyperparameter]: + """All the ``(name, Hyperparameter)`` contained in the space. + + Returns + ------- + dict(str, :ref:`Hyperparameters`) + An OrderedDict of names and hyperparameters + """ + warnings.warn( + "Prefer using `dict(space)` over `get_hyperparameters_dict`", + DeprecationWarning, + stacklevel=2, + ) + return self._hyperparameters.copy() + + def get_hyperparameter_names(self) -> list[str]: + """Names of all the hyperparameter in the space. + + Returns + ------- + list(str) + List of hyperparameter names + """ + warnings.warn( + "Prefer using `list(space.keys())` over `get_hyperparameter_names`", + DeprecationWarning, + stacklevel=2, + ) + return list(self._hyperparameters.keys()) + + # --------------------------------------------------- diff --git a/ConfigSpace/configuration_space.pyx b/ConfigSpace/configuration_space.pyx deleted file mode 100644 index 7a5e9ae4..00000000 --- a/ConfigSpace/configuration_space.pyx +++ /dev/null @@ -1,1895 +0,0 @@ -# Copyright (c) 2014-2016, ConfigSpace developers -# Matthias Feurer -# Katharina Eggensperger -# and others (see commit history). -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import collections.abc -from collections import defaultdict, deque, OrderedDict -import copy -from itertools import chain - -import numpy as np -import io - -import ConfigSpace.nx -from ConfigSpace.hyperparameters import ( - Hyperparameter, - Constant, - FloatHyperparameter, - UniformIntegerHyperparameter, - UniformFloatHyperparameter, - CategoricalHyperparameter, -) -from ConfigSpace.conditions import ( - ConditionComponent, - AbstractCondition, - AbstractConjunction, - EqualsCondition, -) -from ConfigSpace.forbidden import ( - AbstractForbiddenComponent, - AbstractForbiddenClause, - AbstractForbiddenConjunction, - ForbiddenRelation, -) -from typing import ( - Union, List, Any, Dict, Iterable, Set, Tuple, Optional, KeysView -) -from ConfigSpace.exceptions import ForbiddenValueError -import ConfigSpace.c_util - - -class ConfigurationSpace(collections.abc.Mapping): - - # TODO add a method to add whole configuration spaces as a child "tree" - - def __init__( - self, - name: Union[str, Dict, None] = None, - seed: Union[int, None] = None, - meta: Optional[Dict] = None, - *, - space: Optional[Dict[str, Union[Tuple[int, int], Tuple[float, float], List[Union[int, float, str]], int, float, str]]] = None - ) -> None: - """A collection-like object containing a set of hyperparameter definitions and conditions. - - A configuration space organizes all hyperparameters and its conditions - as well as its forbidden clauses. Configurations can be sampled from - this configuration space. As underlying data structure, the - configuration space uses a tree-based approach to represent the - conditions and restrictions between hyperparameters. - - Parameters - ---------- - name : str | Dict, optional - Name of the configuration space. If a dict is passed, this is considered the same - as the `space` arg. - - seed : int, optional - random seed - meta : dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. - - space: Dict[str, Tuple[int, int] | Tuple[float, float] | List[str] | int | float | str] | None = None - A simple configuration space to use: - - .. code:: python - - ConfigurationSpace( - name="myspace", - space={ - "uniform_integer": (1, 10), - "uniform_float": (1.0, 10.0), - "categorical": ["a", "b", "c"], - "constant": 1337, - } - ) - - """ - # If first arg is a dict, we assume this to be `space` - if isinstance(name, Dict): - space = name - name = None - - self.name = name - self.meta = meta - - self._hyperparameters = OrderedDict() # type: OrderedDict[str, Hyperparameter] - self._hyperparameter_idx = dict() # type: Dict[str, int] - self._idx_to_hyperparameter = dict() # type: Dict[int, str] - - # Use dictionaries to make sure that we don't accidently add - # additional keys to these mappings (which happened with defaultdict()). - # This once broke auto-sklearn's equal comparison of configuration - # spaces when _children of one instance contained all possible - # hyperparameters as keys and empty dictionaries as values while the - # other instance not containing these. - self._children: OrderedDict[ - str, OrderedDict[str, Union[None, AbstractCondition]]] = OrderedDict() - self._parents: OrderedDict[ - str, OrderedDict[str, Union[None, AbstractCondition]]] = OrderedDict() - - # changing this to a normal dict will break sampling because there is - # no guarantee that the parent of a condition was evaluated before - self._conditionals = set() # type: Set[str] - self.forbidden_clauses = [] # type: List['AbstractForbiddenComponent'] - self.random = np.random.RandomState(seed) - - self._children["__HPOlib_configuration_space_root__"] = OrderedDict() - - # caching - self._parent_conditions_of = dict() - self._child_conditions_of = dict() - self._parents_of = dict() - self._children_of = dict() - - # User provided a basic configspace - if space is not None: - - # We store and do in one go due to caching mechanisms - hps = [] - for name, hp in space.items(): - - # Anything that is a Hyperparameter already is good - # Note that we discard the key name in this case in favour - # of the name given in the dictionary - if isinstance(hp, Hyperparameter): - hps.append(hp) - - # Tuples are bounds, check if float or int - elif isinstance(hp, Tuple): - if len(hp) != 2: - raise ValueError( - "'%s' must be (lower, upper) bound, got %s" - % (name, hp) - ) - lower, upper = hp - if isinstance(lower, float): - real_hp = UniformFloatHyperparameter(name, lower, upper) - else: - real_hp = UniformIntegerHyperparameter(name, lower, upper) - - hps.append(real_hp) - - # Lists are categoricals - elif isinstance(hp, List): - if len(hp) == 0: - raise ValueError( - "Can't have empty list for categorical '%s'" % name - ) - - real_hp = CategoricalHyperparameter(name, hp) - hps.append(real_hp) - - # If it's an allowed type, it's a constant - elif isinstance(hp, (int, str, float)): - real_hp = Constant(name, hp) - hps.append(real_hp) - - else: - raise ValueError("Unknown value '%s' for '%s'" % (hp, name)) - - # Finally, add them in - self.add_hyperparameters(hps) - - def generate_all_continuous_from_bounds(self, bounds: List[List[Any]]) -> None: - """ - Generate :class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter` - from a list containing lists with lower and upper bounds. The generated - hyperparameters are added to the configuration space. - - Parameters - ---------- - bounds : list[tuple([Any, Any])] - List containing lists with two elements: lower and upper bound - """ - for i, (l, u) in enumerate(bounds): - hp = ConfigSpace.UniformFloatHyperparameter("x%d" % i, l, u) - self.add_hyperparameter(hp) - - def add_hyperparameters(self, hyperparameters: List[Hyperparameter]) -> List[Hyperparameter]: - """ - Add hyperparameters to the configuration space. - - Parameters - ---------- - hyperparameters : list(:ref:`Hyperparameters`) - Collection of hyperparameters to add - - Returns - ------- - list(:ref:`Hyperparameters`) - List of added hyperparameters (same as input) - """ - - for hyperparameter in hyperparameters: - if not isinstance(hyperparameter, Hyperparameter): - raise TypeError("Hyperparameter '%s' is not an instance of " - "ConfigSpace.hyperparameters.Hyperparameter." % - str(hyperparameter)) - - for hyperparameter in hyperparameters: - self._add_hyperparameter(hyperparameter) - - self._update_cache() - self._check_default_configuration() - self._sort_hyperparameters() - return hyperparameters - - def add_hyperparameter(self, hyperparameter: Hyperparameter) -> Hyperparameter: - """ - Add a hyperparameter to the configuration space. - - Parameters - ---------- - hyperparameter : :ref:`Hyperparameters` - The hyperparameter to add - - Returns - ------- - :ref:`Hyperparameters` - The added hyperparameter - """ - if not isinstance(hyperparameter, Hyperparameter): - raise TypeError("The method add_hyperparameter must be called " - "with an instance of " - "ConfigSpace.hyperparameters.Hyperparameter.") - - self._add_hyperparameter(hyperparameter) - self._update_cache() - self._check_default_configuration() - self._sort_hyperparameters() - - return hyperparameter - - def _add_hyperparameter(self, hyperparameter: Hyperparameter) -> None: - # Check if adding the hyperparameter is legal: - # * Its name must not already exist - if hyperparameter.name in self._hyperparameters: - raise ValueError("Hyperparameter '%s' is already in the " - "configuration space." % hyperparameter.name) - self._hyperparameters[hyperparameter.name] = hyperparameter - self._children[hyperparameter.name] = OrderedDict() - - # TODO remove __HPOlib_configuration_space_root__, it is only used in - # to check for cyclic configuration spaces. If it is only added when - # cycles are checked, the code can become much easier (e.g. the parent - # caching can be more or less removed). - self._children["__HPOlib_configuration_space_root__"][ - hyperparameter.name] = None - self._parents[hyperparameter.name] = OrderedDict() - self._parents[hyperparameter.name][ - "__HPOlib_configuration_space_root__"] = None - # Save the index of each hyperparameter name to later on access a - # vector of hyperparameter values by indices, must be done twice - # because check_default_configuration depends on it - for i, hp in enumerate(self._hyperparameters): - self._hyperparameter_idx[hp] = i - - def add_condition(self, condition: ConditionComponent) -> ConditionComponent: - """ - Add a condition to the configuration space. - Check if adding the condition is legal: - - - The parent in a condition statement must exist - - The condition must add no cycles - - The internal array keeps track of all edges which must be - added to the DiGraph; if the checks don't raise any Exception, - these edges are finally added at the end of the function. - - Parameters - ---------- - condition : :ref:`Conditions` - Condition to add - - Returns - ------- - :ref:`Conditions` - Same condition as input - """ - if not isinstance(condition, ConditionComponent): - raise TypeError("The method add_condition must be called " - "with an instance of " - "ConfigSpace.condition.ConditionComponent.") - - if isinstance(condition, AbstractCondition): - self._check_edges( - [(condition.parent, condition.child)], - [condition.value], - ) - self._check_condition(condition.child, condition) - self._add_edge( - condition.parent, - condition.child, - condition, - ) - - # Loop over the Conjunctions to find out the conditions we must add! - elif isinstance(condition, AbstractConjunction): - dlcs = condition.get_descendant_literal_conditions() - edges = [(dlc.parent, dlc.child) for dlc in dlcs] - values = [dlc.value for dlc in dlcs] - self._check_edges(edges, values) - - for dlc in dlcs: - self._check_condition(dlc.child, condition) - self._add_edge( - dlc.parent, - dlc.child, - condition=condition, - ) - - else: - raise Exception("This should never happen!") - - self._sort_hyperparameters() - self._update_cache() - return condition - - def add_conditions(self, conditions: List[ConditionComponent]) -> List[ConditionComponent]: - """ - Add a list of conditions to the configuration space. - They must be legal. Take a look at - :meth:`~ConfigSpace.configuration_space.ConfigurationSpace.add_condition`. - - Parameters - ---------- - conditions : list(:ref:`Conditions`) - collection of conditions to add - - Returns - ------- - list(:ref:`Conditions`) - Same as input conditions - """ - for condition in conditions: - if not isinstance(condition, ConditionComponent): - raise TypeError("Condition '%s' is not an instance of " - "ConfigSpace.condition.ConditionComponent." % - str(condition)) - - edges = [] - values = [] - conditions_to_add = [] - for condition in conditions: - if isinstance(condition, AbstractCondition): - edges.append((condition.parent, condition.child)) - values.append(condition.value) - conditions_to_add.append(condition) - elif isinstance(condition, AbstractConjunction): - dlcs = condition.get_descendant_literal_conditions() - edges.extend( - [(dlc.parent, dlc.child) for dlc in dlcs]) - values.extend([dlc.value for dlc in dlcs]) - conditions_to_add.extend([condition] * len(dlcs)) - - for edge, condition in zip(edges, conditions_to_add): - self._check_condition(edge[1], condition) - self._check_edges(edges, values) - for edge, condition in zip(edges, conditions_to_add): - self._add_edge(edge[0], edge[1], condition) - - self._sort_hyperparameters() - self._update_cache() - return conditions - - def _add_edge( - self, - parent_node: Hyperparameter, - child_node: Hyperparameter, - condition: ConditionComponent, - ) -> None: - try: - # TODO maybe this has to be done more carefully - del self._children["__HPOlib_configuration_space_root__"][child_node.name] - except Exception: - pass - - try: - del self._parents[child_node.name]["__HPOlib_configuration_space_root__"] - except Exception: - pass - - self._children[parent_node.name][child_node.name] = condition - self._parents[child_node.name][parent_node.name] = condition - self._conditionals.add(child_node.name) - - def _check_condition(self, child_node: Hyperparameter, condition: ConditionComponent) \ - -> None: - for other_condition in self._get_parent_conditions_of(child_node.name): - if other_condition != condition: - raise ValueError("Adding a second condition (different) for a " - "hyperparameter is ambigouos and " - "therefore forbidden. Add a conjunction " - "instead!\nAlready inserted: %s\nNew one: " - "%s" % (str(other_condition), str(condition))) - - def _check_edges( - self, - edges: List[Tuple[Hyperparameter, Hyperparameter]], - values: List[Union[float, str, int]] - ) -> None: - for (parent_node, child_node), value in zip(edges, values): - # check if both nodes are already inserted into the graph - if child_node.name not in self._hyperparameters: - raise ValueError( - "Child hyperparameter '%s' not in configuration " - "space." % child_node.name) - if child_node != self._hyperparameters[child_node.name]: - # TODO test this - raise ValueError( - "Child hyperparameter '%s' different to hyperparameter " - "with the same name in configuration space: '%s'." % - (child_node, self._hyperparameters[child_node.name]) - ) - if parent_node.name not in self._hyperparameters: - raise ValueError( - "Parent hyperparameter '%s' not in configuration " - "space." % parent_node.name) - if parent_node != self._hyperparameters[parent_node.name]: - # TODO test this - raise ValueError( - "Parent hyperparameter '%s' different to hyperparameter " - "with the same name in configuration space: '%s'." % - (parent_node, self._hyperparameters[parent_node.name]) - ) - if isinstance(value, (tuple, list)): - # TODO test this - for v in value: - if not self._hyperparameters[parent_node.name].is_legal(v): - raise ValueError( - "Value '%s' is not legal for hyperparameter %s." % - (v, self._hyperparameters[parent_node.name]) - ) - else: - if not self._hyperparameters[parent_node.name].is_legal(value): - raise ValueError( - "Value '%s' is not legal for hyperparameter %s." % - (value, self._hyperparameters[parent_node.name]) - ) - - # TODO: recursively check everything which is inside the conditions, - # this means we have to recursively traverse the condition - - tmp_dag = self._create_tmp_dag() - for parent_node, child_node in edges: - tmp_dag.add_edge(parent_node.name, child_node.name) - - if not ConfigSpace.nx.is_directed_acyclic_graph(tmp_dag): - cycles = list( - ConfigSpace.nx.simple_cycles(tmp_dag) - ) # type: List[List[str]] - for cycle in cycles: - cycle.sort() - cycles.sort() - raise ValueError("Hyperparameter configuration contains a " - "cycle %s" % str(cycles)) - - def _sort_hyperparameters(self) -> None: - levels = OrderedDict() # type: OrderedDict[str, int] - to_visit = deque() # type: ignore - for hp_name in self._hyperparameters: - to_visit.appendleft(hp_name) - - while len(to_visit) > 0: - current = to_visit.pop() - if "__HPOlib_configuration_space_root__" in self._parents[current]: - assert len(self._parents[current]) == 1 - levels[current] = 1 - - else: - all_parents_visited = True - depth = -1 - for parent in self._parents[current]: - if parent not in levels: - all_parents_visited = False - break - else: - depth = max(depth, levels[parent] + 1) - - if all_parents_visited: - levels[current] = depth - else: - to_visit.appendleft(current) - - by_level = defaultdict(list) # type: defaultdict[int, List[str]] - for hp in levels: - level = levels[hp] - by_level[level].append(hp) - - nodes = [] - # Sort and add to list - for level in sorted(by_level): - sorted_by_level = by_level[level] - sorted_by_level.sort() - nodes.extend(sorted_by_level) - - # Resort the OrderedDict - new_order = OrderedDict() - for node in nodes: - new_order[node] = self._hyperparameters[node] - self._hyperparameters = new_order - - # Update to reflect sorting - for i, hp in enumerate(self._hyperparameters): - self._hyperparameter_idx[hp] = i - self._idx_to_hyperparameter[i] = hp - - # Update order of _children - new_order = OrderedDict() - new_order["__HPOlib_configuration_space_root__"] = self._children[ - "__HPOlib_configuration_space_root__" - ] - for hp in chain(["__HPOlib_configuration_space_root__"], self._hyperparameters): - # Also resort the children dict - children_sorting = [(self._hyperparameter_idx[child_name], child_name) - for child_name in self._children[hp]] - children_sorting.sort() - children_order = OrderedDict() - for _, child_name in children_sorting: - children_order[child_name] = self._children[hp][child_name] - new_order[hp] = children_order - self._children = new_order - - # Update order of _parents - new_order = OrderedDict() - for hp in self._hyperparameters: - # Also resort the parent's dict - if "__HPOlib_configuration_space_root__" in self._parents[hp]: - parent_sorting = [(-1, "__HPOlib_configuration_space_root__")] - else: - parent_sorting = [(self._hyperparameter_idx[parent_name], parent_name) - for parent_name in self._parents[hp]] - parent_sorting.sort() - parent_order = OrderedDict() - for _, parent_name in parent_sorting: - parent_order[parent_name] = self._parents[hp][parent_name] - new_order[hp] = parent_order - self._parents = new_order - - # update conditions - for condition in self.get_conditions(): - condition.set_vector_idx(self._hyperparameter_idx) - - # forbidden clauses - for clause in self.get_forbiddens(): - clause.set_vector_idx(self._hyperparameter_idx) - - def _update_cache(self): - self._parent_conditions_of = dict() - self._child_conditions_of = dict() - self._parents_of = dict() - self._children_of = dict() - - for hp_name in self._hyperparameters: - self._parent_conditions_of[hp_name] = self._get_parent_conditions_of(hp_name) - self._child_conditions_of[hp_name] = self._get_child_conditions_of(hp_name) - self._parents_of[hp_name] = self.get_parents_of(hp_name) - self._children_of[hp_name] = self.get_children_of(hp_name) - - def _create_tmp_dag(self) -> ConfigSpace.nx.DiGraph: - tmp_dag = ConfigSpace.nx.DiGraph() - for hp_name in self._hyperparameters: - tmp_dag.add_node(hp_name) - tmp_dag.add_edge("__HPOlib_configuration_space_root__", hp_name) - - for parent_node_ in self._children: - if parent_node_ == "__HPOlib_configuration_space_root__": - continue - for child_node_ in self._children[parent_node_]: - try: - tmp_dag.remove_edge("__HPOlib_configuration_space_root__", - child_node_) - except Exception: - pass - condition = self._children[parent_node_][child_node_] - tmp_dag.add_edge(parent_node_, child_node_, condition=condition) - - return tmp_dag - - def add_forbidden_clause(self, - clause: AbstractForbiddenComponent) -> AbstractForbiddenComponent: - """ - Add a forbidden clause to the configuration space. - - Parameters - ---------- - clause : :ref:`Forbidden clauses` - Forbidden clause to add - - Returns - ------- - :ref:`Forbidden clauses` - Same as input forbidden clause - """ - self._check_forbidden_component(clause=clause) - clause.set_vector_idx(self._hyperparameter_idx) - self.forbidden_clauses.append(clause) - self._check_default_configuration() - return clause - - def add_forbidden_clauses( - self, - clauses: List[AbstractForbiddenComponent] - ) -> List[AbstractForbiddenComponent]: - """ - Add a list of forbidden clauses to the configuration space. - - Parameters - ---------- - clauses : list(:ref:`Forbidden clauses`) - Collection of forbidden clauses to add - - Returns - ------- - list(:ref:`Forbidden clauses`) - Same as input clauses - """ - for clause in clauses: - self._check_forbidden_component(clause=clause) - clause.set_vector_idx(self._hyperparameter_idx) - self.forbidden_clauses.append(clause) - self._check_default_configuration() - return clauses - - def _check_forbidden_component(self, clause: AbstractForbiddenComponent): - if not isinstance(clause, AbstractForbiddenComponent): - raise TypeError("The method add_forbidden_clause must be called " - "with an instance of " - "ConfigSpace.forbidden.AbstractForbiddenComponent.") - to_check = list() - relation_to_check = list() - if isinstance(clause, AbstractForbiddenClause): - to_check.append(clause) - elif isinstance(clause, AbstractForbiddenConjunction): - to_check.extend(clause.get_descendant_literal_clauses()) - elif isinstance(clause, ForbiddenRelation): - relation_to_check.extend(clause.get_descendant_literal_clauses()) - else: - raise NotImplementedError(type(clause)) - - def _check_hp(tmp_clause, hp): - if hp.name not in self._hyperparameters: - raise ValueError( - "Cannot add clause '%s' because it references hyperparameter" - " %s which is not in the configuration space (allowed " - "hyperparameters are: %s)" - % ( - tmp_clause, - hp.name, - list(self._hyperparameters), - ) - ) - - for tmp_clause in to_check: - _check_hp(tmp_clause, tmp_clause.hyperparameter) - - for tmp_clause in relation_to_check: - _check_hp(tmp_clause, tmp_clause.left) - _check_hp(tmp_clause, tmp_clause.right) - - def add_configuration_space(self, - prefix: str, - configuration_space: "ConfigurationSpace", - delimiter: str = ":", - parent_hyperparameter: dict = None - ) -> "ConfigurationSpace": - """ - Combine two configuration space by adding one the other configuration - space. The contents of the configuration space, which should be added, - are renamed to ``prefix`` + ``delimiter`` + old_name. - - Parameters - ---------- - prefix : str - The prefix for the renamed hyperparameter | conditions | - forbidden clauses - configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - The configuration space which should be added - delimiter : str, optional - Defaults to ':' - parent_hyperparameter : dict | None = None - Adds for each new hyperparameter the condition, that - ``parent_hyperparameter`` is active. Must be a dictionary with two keys - "parent" and "value", meaning that the added configuration space is active - when `parent` is equal to `value` - - Returns - ------- - :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - The configuration space, which was added - """ - if not isinstance(configuration_space, ConfigurationSpace): - raise TypeError("The method add_configuration_space must be " - "called with an instance of " - "ConfigSpace.configuration_space." - "ConfigurationSpace.") - - new_parameters = [] - for hp in configuration_space.get_hyperparameters(): - new_parameter = copy.copy(hp) - # Allow for an empty top-level parameter - if new_parameter.name == "": - new_parameter.name = prefix - else: - new_parameter.name = "%s%s%s" % (prefix, delimiter, - new_parameter.name) - new_parameters.append(new_parameter) - self.add_hyperparameters(new_parameters) - - conditions_to_add = [] - for condition in configuration_space.get_conditions(): - new_condition = copy.copy(condition) - dlcs = new_condition.get_descendant_literal_conditions() - for dlc in dlcs: - if dlc.child.name == prefix or dlc.child.name == "": - dlc.child.name = prefix - elif not dlc.child.name.startswith( - "%s%s" % (prefix, delimiter)): - dlc.child.name = "%s%s%s" % ( - prefix, delimiter, dlc.child.name) - if dlc.parent.name == prefix or dlc.parent.name == "": - dlc.parent.name = prefix - elif not dlc.parent.name.startswith( - "%s%s" % (prefix, delimiter)): - dlc.parent.name = "%s%s%s" % ( - prefix, delimiter, dlc.parent.name) - conditions_to_add.append(new_condition) - self.add_conditions(conditions_to_add) - - def prefix_hp_name(hyperparameter: Hyperparameter): - if hyperparameter.name == prefix or \ - hyperparameter.name == "": - hyperparameter.name = prefix - elif not hyperparameter.name.startswith( - "%s%s" % (prefix, delimiter)): - hyperparameter.name = "%s%s%s" % \ - (prefix, delimiter, - hyperparameter.name) - - forbiddens_to_add = [] - for forbidden_clause in configuration_space.forbidden_clauses: - # new_forbidden = copy.deepcopy(forbidden_clause) - new_forbidden = forbidden_clause - dlcs = new_forbidden.get_descendant_literal_clauses() - for dlc in dlcs: - if isinstance(dlc, ForbiddenRelation): - prefix_hp_name(dlc.left) - prefix_hp_name(dlc.right) - else: - prefix_hp_name(dlc.hyperparameter) - forbiddens_to_add.append(new_forbidden) - self.add_forbidden_clauses(forbiddens_to_add) - - conditions_to_add = [] - if parent_hyperparameter is not None: - for new_parameter in new_parameters: - # Only add a condition if the parameter is a top-level - # parameter of the new configuration space (this will be some - # kind of tree structure). - if self.get_parents_of(new_parameter): - continue - condition = EqualsCondition(new_parameter, - parent_hyperparameter["parent"], - parent_hyperparameter["value"]) - conditions_to_add.append(condition) - self.add_conditions(conditions_to_add) - - return configuration_space - - def get_hyperparameters(self) -> List[Hyperparameter]: - """ - Return a list with all the hyperparameter, which are contained in the - configuration space object. - - Returns - ------- - list(:ref:`Hyperparameters`) - A list with all hyperparameters stored in the configuration - space object - """ - return list(self._hyperparameters.values()) - - def get_hyperparameters_dict(self) -> Dict[str, Hyperparameter]: - """ - Return an OrderedDict with all the ``(name, Hyperparameter)`` contained in - the configuration space object. - - Returns - ------- - OrderedDict(str, :ref:`Hyperparameters`) - An OrderedDict of names and hyperparameters - - """ - return self._hyperparameters.copy() - - def get_hyperparameter_names(self) -> List[str]: - """ - Return a list with all names of hyperparameter, which are contained in - the configuration space object. - - Returns - ------- - list(str) - List of hyperparameter names - - """ - return list(self._hyperparameters.keys()) - - def __getitem__(self, key: str) -> Hyperparameter: - return self.get_hyperparameter(key) - - def get_hyperparameter(self, name: str) -> Hyperparameter: - """ - Gives the hyperparameter from the configuration space given its name. - - Parameters - ---------- - name : str - Name of the searched hyperparameter - - Returns - ------- - :ref:`Hyperparameters` - Hyperparameter with the name ``name`` - - """ - hp = self._hyperparameters.get(name) - - if hp is None: - if self.name is None: - raise KeyError("Hyperparameter '%s' does not exist in this " - "configuration space." % name) - else: - raise KeyError("Hyperparameter '%s' does not exist in " - "configuration space %s." % (name, self.name)) - else: - return hp - - def get_hyperparameter_by_idx(self, idx: int) -> str: - """ - Return the name of a hyperparameter from the configuration space given - its id. - - Parameters - ---------- - idx : int - Id of a hyperparameter - - Returns - ------- - str - Name of the hyperparameter - - """ - hp = self._idx_to_hyperparameter.get(idx) - - if hp is None: - if self.name is None: - raise KeyError("Hyperparameter #'%d' does not exist in this " - "configuration space." % idx) - else: - raise KeyError("Hyperparameter #'%d' does not exist in " - "configuration space %s." % (idx, self.name)) - else: - return hp - - def get_idx_by_hyperparameter_name(self, name: str) -> int: - """ - Return the id of a hyperparameter by its ``name``. - - Parameters - ---------- - name : str - Name of a hyperparameter - - Returns - ------- - int - Id of the hyperparameter with name ``name`` - - """ - idx = self._hyperparameter_idx.get(name) - - if idx is None: - if self.name is None: - raise KeyError("Hyperparameter '%s' does not exist in this " - "configuration space." % name) - else: - raise KeyError("Hyperparameter '%s' does not exist in " - "configuration space %s." % (name, self.name)) - else: - return idx - - def get_conditions(self) -> List[AbstractCondition]: - """ - Return a list with all conditions from the configuration space. - - Returns - ------- - list(:ref:`Conditions`) - Conditions of the configuration space - - """ - conditions = [] - added_conditions = set() # type: Set[str] - - # Nodes is a list of nodes - for source_node in self.get_hyperparameters(): - # This is a list of keys in a dictionary - # TODO sort the edges by the order of their source_node in the - # hyperparameter list! - for target_node in self._children[source_node.name]: - if target_node not in added_conditions: - condition = self._children[source_node.name][target_node] - conditions.append(condition) - added_conditions.add(target_node) - - return conditions - - def get_forbiddens(self) -> List[AbstractForbiddenComponent]: - """ - Return a list with all forbidden clauses from the configuration space. - - Returns - ------- - list(:ref:`Forbidden clauses`) - List with the forbidden clauses - """ - return self.forbidden_clauses - - def get_children_of(self, name: Union[str, Hyperparameter]) -> List[Hyperparameter]: - """ - Return a list with all children of a given hyperparameter. - - Parameters - ---------- - name : str, :ref:`Hyperparameters` - Hyperparameter or its name, for which all children are requested - - Returns - ------- - list(:ref:`Hyperparameters`) - Children of the hyperparameter - - """ - conditions = self.get_child_conditions_of(name) - parents = [] # type: List[Hyperparameter] - for condition in conditions: - parents.extend(condition.get_children()) - return parents - - def _get_children_of(self, name: str) -> List[Hyperparameter]: - conditions = self._get_child_conditions_of(name) - parents = [] # type: List[Hyperparameter] - for condition in conditions: - parents.extend(condition.get_children()) - return parents - - def get_child_conditions_of(self, name: Union[str, Hyperparameter]) -> List[AbstractCondition]: - """ - Return a list with conditions of all children of a given - hyperparameter referenced by its ``name``. - - Parameters - ---------- - name : str, :ref:`Hyperparameters` - Hyperparameter or its name, for which conditions are requested - - Returns - ------- - list(:ref:`Conditions`) - List with the conditions on the children of the given hyperparameter - - """ - if isinstance(name, Hyperparameter): - name = name.name # type: ignore - - # This raises an exception if the hyperparameter does not exist - self.get_hyperparameter(name) - return self._get_child_conditions_of(name) - - def _get_child_conditions_of(self, name: str) -> List[AbstractCondition]: - children = self._children[name] - conditions = [children[child_name] for child_name in children - if child_name != "__HPOlib_configuration_space_root__"] - return conditions - - def get_parents_of(self, name: Union[str, Hyperparameter]) -> List[Hyperparameter]: - """ - Return the parent hyperparameters of a given hyperparameter. - - Parameters - ---------- - name : str, :ref:`Hyperparameters` - Can either be the name of a hyperparameter or the hyperparameter - object - - Returns - ------- - list[:ref:`Conditions`] - List with all parent hyperparameters - """ - - conditions = self.get_parent_conditions_of(name) - parents = [] # type: List[Hyperparameter] - for condition in conditions: - parents.extend(condition.get_parents()) - return parents - - def _get_parents_of(self, name: str) -> List[Hyperparameter]: - """ - Return the parent hyperparameters of a given hyperparameter. - - Parameters - ---------- - name : str - - Returns - ------- - list - List with all parent hyperparameters - """ - conditions = self._get_parent_conditions_of(name) - parents = [] # type: List[Hyperparameter] - for condition in conditions: - parents.extend(condition.get_parents()) - return parents - - def get_parent_conditions_of(self, name: Union[str, Hyperparameter]) -> List[AbstractCondition]: - """ - Return a list with conditions of all parents of a given hyperparameter. - - Parameters - ---------- - name : str, :ref:`Hyperparameters` - Can either be the name of a hyperparameter or the hyperparameter - object - - Returns - ------- - List[:ref:`Conditions`] - List with all conditions on parent hyperparameters - - """ - if isinstance(name, Hyperparameter): - name = name.name # type: ignore - - # This raises an exception if the hyperparameter does not exist - self.get_hyperparameter(name) - return self._get_parent_conditions_of(name) - - def _get_parent_conditions_of(self, name: str) -> List[AbstractCondition]: - parents = self._parents[name] - conditions = [parents[parent_name] for parent_name in parents if parent_name != "__HPOlib_configuration_space_root__"] - return conditions - - def get_all_unconditional_hyperparameters(self) -> List[str]: - """ - Return a list with names of unconditional hyperparameters. - - Returns - ------- - list[:ref:`Hyperparameters`] - List with all parent hyperparameters, which are not part of a condition - - """ - hyperparameters = [hp_name for hp_name in - self._children[ - "__HPOlib_configuration_space_root__"]] - return hyperparameters - - def get_all_conditional_hyperparameters(self) -> List[str]: - """ - Return a list with names of all conditional hyperparameters. - - Returns - ------- - list[:ref:`Hyperparameters`] - List with all conditional hyperparameter - - """ - return self._conditionals - - def get_default_configuration(self) -> "Configuration": - """ - Return a configuration containing hyperparameters with default values. - - Returns - ------- - :class:`~ConfigSpace.configuration_space.Configuration` - Configuration with the set default values - - """ - return self._check_default_configuration() - - def _check_default_configuration(self) -> "Configuration": - # Check if adding that hyperparameter leads to an illegal default configuration - instantiated_hyperparameters = {} # type: Dict[str, Optional[Union[int, float, str]]] - for hp in self.get_hyperparameters(): - conditions = self._get_parent_conditions_of(hp.name) - active = True - for condition in conditions: - parent_names = [c.parent.name for c in - condition.get_descendant_literal_conditions()] - - parents = { - parent_name: instantiated_hyperparameters[parent_name] - for parent_name in parent_names - } - - if not condition.evaluate(parents): - # TODO find out why a configuration is illegal! - active = False - - if not active: - instantiated_hyperparameters[hp.name] = None - elif isinstance(hp, Constant): - instantiated_hyperparameters[hp.name] = hp.value - else: - instantiated_hyperparameters[hp.name] = hp.default_value - - # TODO copy paste from check configuration - - # TODO add an extra Exception type for the case that the default - # configuration is forbidden! - return Configuration(self, instantiated_hyperparameters) - - # For backward compatibility - def check_configuration(self, configuration: "Configuration") -> None: - """ - Check if a configuration is legal. Raises an error if not. - - Parameters - ---------- - configuration : :class:`~ConfigSpace.configuration_space.Configuration` - Configuration to check - """ - if not isinstance(configuration, Configuration): - raise TypeError("The method check_configuration must be called " - "with an instance of %s. " - "Your input was of type %s" % (Configuration, type(configuration))) - ConfigSpace.c_util.check_configuration( - self, configuration.get_array(), False - ) - - def check_configuration_vector_representation(self, vector: np.ndarray) -> None: - """ - Raise error if configuration in vector representation is not legal. - - Parameters - ---------- - vector : np.ndarray - Configuration in vector representation - """ - if not isinstance(vector, np.ndarray): - raise TypeError("The method check_configuration must be called " - "with an instance of np.ndarray " - "Your input was of type %s" % (type(vector))) - ConfigSpace.c_util.check_configuration(self, vector, False) - - def get_active_hyperparameters(self, configuration: "Configuration") -> Set: - """ - Return a set of active hyperparameter for a given configuration. - - Parameters - ---------- - configuration : :class:`~ConfigSpace.configuration_space.Configuration` - Configuration for which the active hyperparameter are returned - - Returns - ------- - set(:class:`~ConfigSpace.configuration_space.Configuration`) - The set of all active hyperparameter - - """ - vector = configuration.get_array() - active_hyperparameters = set() - for hp_name, hyperparameter in self._hyperparameters.items(): - conditions = self._parent_conditions_of[hyperparameter.name] - - active = True - for condition in conditions: - - parent_vector_idx = condition.get_parents_vector() - - # if one of the parents is None, the hyperparameter cannot be - # active! Else we have to check this - # Note from trying to optimize this - this is faster than using - # dedicated numpy functions and indexing - if any([vector[i] != vector[i] for i in parent_vector_idx]): - active = False - break - - else: - if not condition.evaluate_vector(vector): - active = False - break - - if active: - active_hyperparameters.add(hp_name) - return active_hyperparameters - - def _check_configuration_rigorous(self, configuration: "Configuration", - allow_inactive_with_values: bool = False) -> None: - vector = configuration.get_array() - active_hyperparameters = self.get_active_hyperparameters(configuration) - - for hp_name, hyperparameter in self._hyperparameters.items(): - hp_value = vector[self._hyperparameter_idx[hp_name]] - active = hp_name in active_hyperparameters - - if not np.isnan(hp_value) and not hyperparameter.is_legal_vector(hp_value): - raise ValueError("Hyperparameter instantiation '%s' " - "(type: %s) is illegal for hyperparameter %s" % - (hp_value, str(type(hp_value)), - hyperparameter)) - - if active and np.isnan(hp_value): - raise ValueError("Active hyperparameter '%s' not specified!" % - hyperparameter.name) - - if not allow_inactive_with_values and not active and \ - not np.isnan(hp_value): - raise ValueError("Inactive hyperparameter '%s' must not be " - "specified, but has the vector value: '%s'." % - (hp_name, hp_value)) - self._check_forbidden(vector) - - def _check_forbidden(self, vector: np.ndarray) -> None: - ConfigSpace.c_util.check_forbidden(self.forbidden_clauses, vector) - # for clause in self.forbidden_clauses: - # if clause.is_forbidden_vector(vector, strict=False): - # raise ForbiddenValueError("Given vector violates forbidden - # clause %s" % (str(clause))) - - # http://stackoverflow.com/a/25176504/4636294 - def __eq__(self, other: Any) -> bool: - """Override the default Equals behavior""" - if isinstance(other, self.__class__): - this_dict = self.__dict__.copy() - del this_dict["random"] - other_dict = other.__dict__.copy() - del other_dict["random"] - return this_dict == other_dict - return NotImplemented - - def __ne__(self, other: Any) -> bool: - """Define a non-equality test""" - if isinstance(other, self.__class__): - return not self.__eq__(other) - return NotImplemented - - def __hash__(self) -> int: - """Override the default hash behavior (that returns the id or the object)""" - return hash(self.__repr__()) - - def __repr__(self) -> str: - retval = io.StringIO() - retval.write("Configuration space object:\n Hyperparameters:\n") - - if self.name is not None: - retval.write(self.name) - retval.write("\n") - - hyperparameters = sorted(self.get_hyperparameters(), - key=lambda t: t.name) - if hyperparameters: - retval.write(" ") - retval.write("\n ".join( - [str(hyperparameter) for hyperparameter in hyperparameters])) - retval.write("\n") - - conditions = sorted(self.get_conditions(), - key=lambda t: str(t)) - if conditions: - retval.write(" Conditions:\n") - retval.write(" ") - retval.write("\n ".join( - [str(condition) for condition in conditions])) - retval.write("\n") - - if self.get_forbiddens(): - retval.write(" Forbidden Clauses:\n") - retval.write(" ") - retval.write("\n ".join( - [str(clause) for clause in self.get_forbiddens()])) - retval.write("\n") - - retval.seek(0) - return retval.getvalue() - - def __iter__(self) -> Iterable: - """ Allows to iterate over the hyperparameter names in (hopefully?) the right order.""" - return iter(self._hyperparameters.keys()) - - def keys(self) -> KeysView[str]: - return self._hyperparameters.keys() - - def __len__(self) -> int: - return len(self._hyperparameters) - - def sample_configuration(self, size: int = 1) -> Union["Configuration", List["Configuration"]]: - """ - Sample ``size`` configurations from the configuration space object. - - Parameters - ---------- - size : int, optional - Number of configurations to sample. Default to 1 - - Returns - ------- - :class:`~ConfigSpace.configuration_space.Configuration`, - List[:class:`~ConfigSpace.configuration_space.Configuration`]: - A single configuration if ``size`` 1 else a list of Configurations - """ - if not isinstance(size, int): - raise TypeError("Argument size must be of type int, but is %s" - % type(size)) - elif size < 1: - return [] - - iteration = 0 - missing = size - accepted_configurations = [] # type: List['Configuration'] - num_hyperparameters = len(self._hyperparameters) - - unconditional_hyperparameters = self.get_all_unconditional_hyperparameters() - hyperparameters_with_children = list() - - _forbidden_clauses_unconditionals = [] - _forbidden_clauses_conditionals = [] - for clause in self.get_forbiddens(): - based_on_conditionals = False - for subclause in clause.get_descendant_literal_clauses(): - if isinstance(subclause, ForbiddenRelation): - if subclause.left.name not in unconditional_hyperparameters or \ - subclause.right.name not in unconditional_hyperparameters: - based_on_conditionals = True - break - elif subclause.hyperparameter.name not in unconditional_hyperparameters: - based_on_conditionals = True - break - if based_on_conditionals: - _forbidden_clauses_conditionals.append(clause) - else: - _forbidden_clauses_unconditionals.append(clause) - - for uhp in unconditional_hyperparameters: - children = self._children_of[uhp] - if len(children) > 0: - hyperparameters_with_children.append(uhp) - - while len(accepted_configurations) < size: - if missing != size: - missing = int(1.1 * missing) - vector = np.ndarray((missing, num_hyperparameters), - dtype=np.float64) - - for i, hp_name in enumerate(self._hyperparameters): - hyperparameter = self._hyperparameters[hp_name] - vector[:, i] = hyperparameter._sample(self.random, missing) - - for i in range(missing): - try: - configuration = Configuration( - self, - vector=ConfigSpace.c_util.correct_sampled_array( - vector[i].copy(), - _forbidden_clauses_unconditionals, - _forbidden_clauses_conditionals, - hyperparameters_with_children, - num_hyperparameters, - unconditional_hyperparameters, - self._hyperparameter_idx, - self._parent_conditions_of, - self._parents_of, - self._children_of, - )) - accepted_configurations.append(configuration) - except ForbiddenValueError: - iteration += 1 - - if iteration == size * 100: - raise ForbiddenValueError( - "Cannot sample valid configuration for " - "%s" % self) - - missing = size - len(accepted_configurations) - - if size <= 1: - return accepted_configurations[0] - else: - return accepted_configurations - - def seed(self, seed: int) -> None: - """ - Set the random seed to a number. - - Parameters - ---------- - seed : int - The random seed - """ - self.random = np.random.RandomState(seed) - - def remove_hyperparameter_priors(self) -> "ConfigurationSpace": - """ - Produces a new ConfigurationSpace where all priors on parameters are removed. - Non-uniform hyperpararmeters are replaced with uniform ones, and - CategoricalHyperparameters with weights have their weights removed. - - Returns - ------- - :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - The resulting configuration space, without priors on the hyperparameters - """ - uniform_config_space = ConfigurationSpace() - for parameter in self.get_hyperparameters(): - if hasattr(parameter, "to_uniform"): - uniform_config_space.add_hyperparameter(parameter.to_uniform()) - else: - uniform_config_space.add_hyperparameter(copy.copy(parameter)) - - new_conditions = self.substitute_hyperparameters_in_conditions(self.get_conditions(), uniform_config_space) - new_forbiddens = self.substitute_hyperparameters_in_forbiddens(self.get_forbiddens(), uniform_config_space) - uniform_config_space.add_conditions(new_conditions) - uniform_config_space.add_forbidden_clauses(new_forbiddens) - - return uniform_config_space - - def estimate_size(self) -> Union[float, int]: - """ - Estimate the size of the current configuration space (i.e. unique configurations). - - This is ``np.inf`` in case if there is a single hyperparameter of size ``np.inf`` (i.e. a - :class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter`), otherwise - it is the product of the size of all hyperparameters. The function correctly guesses the - number of unique configurations if there are no condition and forbidden statements in the - configuration spaces. Otherwise, this is an upper bound. Use - :func:`~ConfigSpace.util.generate_grid` to generate all valid configurations if required. - - Returns - ------- - Union[float, int] - """ - sizes = [] - for hp in self._hyperparameters.values(): - sizes.append(hp.get_size()) - if len(sizes) == 0: - return 0.0 - else: - size = sizes[0] - for i in range(1, len(sizes)): - size = size * sizes[i] - return size - - @staticmethod - def substitute_hyperparameters_in_conditions(conditions, new_configspace) -> List["ConditionComponent"]: - """ - Takes a set of conditions and generates a new set of conditions with the same structure, where - each hyperparameter is replaced with its namesake in new_configspace. As such, the set of conditions - remain unchanged, but the included hyperparameters are changed to match those types that exist in - new_configspace. - - Parameters - ---------- - new_configspace: ConfigurationSpace - A ConfigurationSpace containing hyperparameters with the same names as those in the conditions. - - Returns - ------- - List[ConditionComponent]: - The list of conditions, adjusted to fit the new ConfigurationSpace - """ - new_conditions = [] - for condition in conditions: - if isinstance(condition, AbstractConjunction): - conjunction_type = type(condition) - children = condition.get_descendant_literal_conditions() - substituted_children = ConfigurationSpace.substitute_hyperparameters_in_conditions(children, new_configspace) - substituted_conjunction = conjunction_type(*substituted_children) - new_conditions.append(substituted_conjunction) - - elif isinstance(condition, AbstractCondition): - condition_type = type(condition) - child_name = getattr(condition.get_children()[0], "name") - parent_name = getattr(condition.get_parents()[0], "name") - new_child = new_configspace[child_name] - new_parent = new_configspace[parent_name] - - if hasattr(condition, "values"): - condition_arg = getattr(condition, "values") - substituted_condition = condition_type(child=new_child, parent=new_parent, values=condition_arg) - elif hasattr(condition, "value"): - condition_arg = getattr(condition, "value") - substituted_condition = condition_type(child=new_child, parent=new_parent, value=condition_arg) - else: - raise AttributeError(f"Did not find the expected attribute in condition {type(condition)}.") - - new_conditions.append(substituted_condition) - else: - raise TypeError(f"Did not expect the supplied condition type {type(condition)}.") - - return new_conditions - - @staticmethod - def substitute_hyperparameters_in_forbiddens(forbiddens, new_configspace) -> List["ConditionComponent"]: - """ - Takes a set of forbidden clauses and generates a new set of forbidden clauses with the same structure, - where each hyperparameter is replaced with its namesake in new_configspace. As such, the set of forbidden - clauses remain unchanged, but the included hyperparameters are changed to match those types that exist in - new_configspace. - - Parameters - ---------- - new_configspace: ConfigurationSpace - A ConfigurationSpace containing hyperparameters with the same names as those in the forbidden clauses. - - Returns - ------- - List[AbstractForbiddenComponent]: - The list of forbidden clauses, adjusted to fit the new ConfigurationSpace - """ - new_forbiddens = [] - for forbidden in forbiddens: - if isinstance(forbidden, AbstractForbiddenConjunction): - conjunction_type = type(forbidden) - children = forbidden.get_descendant_literal_clauses() - substituted_children = ConfigurationSpace.substitute_hyperparameters_in_forbiddens(children, new_configspace) - substituted_conjunction = conjunction_type(*substituted_children) - new_forbiddens.append(substituted_conjunction) - - elif isinstance(forbidden, AbstractForbiddenClause): - forbidden_type = type(forbidden) - hyperparameter_name = getattr(forbidden.hyperparameter, "name") - new_hyperparameter = new_configspace[hyperparameter_name] - - if hasattr(forbidden, "values"): - forbidden_arg = getattr(forbidden, "values") - substituted_forbidden = forbidden_type(hyperparameter=new_hyperparameter, values=forbidden_arg) - elif hasattr(forbidden, "value"): - forbidden_arg = getattr(forbidden, "value") - substituted_forbidden = forbidden_type(hyperparameter=new_hyperparameter, value=forbidden_arg) - else: - raise AttributeError(f"Did not find the expected attribute in forbidden {type(forbidden)}.") - - new_forbiddens.append(substituted_forbidden) - elif isinstance(forbidden, ForbiddenRelation): - forbidden_type = type(forbidden) - left_name = getattr(forbidden.left, "name") - left_hyperparameter = new_configspace[left_name] - right_name = getattr(forbidden.right, "name") - right_hyperparameter = new_configspace[right_name] - - substituted_forbidden = forbidden_type(left=left_hyperparameter, right=right_hyperparameter) - new_forbiddens.append(substituted_forbidden) - else: - raise TypeError(f"Did not expect the supplied forbidden type {type(forbidden)}.") - - return new_forbiddens - - -class Configuration(collections.abc.Mapping): - def __init__(self, configuration_space: ConfigurationSpace, - values: Union[None, Dict[str, Union[str, float, int]]] = None, - vector: Union[None, np.ndarray] = None, - allow_inactive_with_values: bool = False, origin: Any = None, - config_id: Optional[int] = None) -> None: - """ - Class for a single configuration. - - The :class:`~ConfigSpace.configuration_space.Configuration` object holds - for all active hyperparameters a value. While the - :class:`~ConfigSpace.configuration_space.ConfigurationSpace` stores the - definitions for the hyperparameters (value ranges, constraints,...), a - :class:`~ConfigSpace.configuration_space.Configuration` object is - more an instance of it. Parameters of a - :class:`~ConfigSpace.configuration_space.Configuration` object can be - accessed and modified similar to python dictionaries - (c.f. :ref:`Guide<1st_Example>`). - - Parameters - ---------- - configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - values : dict, optional - A dictionary with pairs (hyperparameter_name, value), where value is - a legal value of the hyperparameter in the above - configuration_space - vector : np.ndarray, optional - A numpy array for efficient representation. Either values or vector - has to be given - allow_inactive_with_values : bool, optional - Whether an Exception will be raised if a value for an inactive - hyperparameter is given. Default is to raise an Exception. - Default to False - origin : Any, optional - Store information about the origin of this configuration. - Default to None - config_id : int, optional - Integer configuration ID which can be used by a program using the ConfigSpace - package. - """ - if not isinstance(configuration_space, ConfigurationSpace): - raise TypeError("Configuration expects an instance of %s, " - "you provided '%s'" % - (ConfigurationSpace, type(configuration_space))) - - self.configuration_space = configuration_space - self.allow_inactive_with_values = allow_inactive_with_values - self._query_values = False - self._num_hyperparameters = len(self.configuration_space._hyperparameters) - self.origin = origin - self.config_id = config_id - self._keys = None # type: Union[None, List[str]] - - if values is not None and vector is not None: - raise ValueError("Configuration specified both as dictionary and " - "vector, can only do one.") - if values is not None: - # Using cs._hyperparameters to iterate makes sure that the - # hyperparameters in the configuration are sorted in the same way as - # they are sorted in the configuration space - self._values = dict() # type: Dict[str, Union[str, float, int]] - for key in configuration_space._hyperparameters: - value = values.get(key) - if value is None: - continue - hyperparameter = configuration_space.get_hyperparameter(key) - if not hyperparameter.is_legal(value): - raise ValueError("Trying to set illegal value '%s' (type '%s') for " - "hyperparameter '%s' (default-value has type '%s')." % ( - str(value), - type(value), hyperparameter, - type(hyperparameter.default_value))) - # Truncate the representation of the float to be of constant - # length for a python version - if isinstance(hyperparameter, FloatHyperparameter): - value = float(repr(value)) - - self._values[key] = value - - for key in values: - if key not in configuration_space._hyperparameters: - raise ValueError("Tried to specify unknown hyperparameter " - "%s" % key) - - self._query_values = True - self._vector = np.ndarray((self._num_hyperparameters,), - dtype=float) - - # Populate the vector - # TODO very unintuitive calls... - for key in configuration_space._hyperparameters: - self._vector[self.configuration_space._hyperparameter_idx[ - key]] = self.configuration_space.get_hyperparameter(key). \ - _inverse_transform(self[key]) - self.is_valid_configuration() - - elif vector is not None: - self._values = dict() - if not isinstance(vector, np.ndarray): - vector = np.array(vector, dtype=float) - if len(vector.shape) > 1: - if len(vector.shape) == 2 and vector.shape[1] == 1: - vector = vector.flatten() - else: - raise ValueError( - "Only 1d arrays can be converted to a Configuration, " - "you passed an array of shape %s." % str(vector.shape) - ) - if len(vector) != len(self.configuration_space.get_hyperparameters()): - raise ValueError( - "Expected array of length %d, got %d" % - (len(self.configuration_space.get_hyperparameters()), len(vector)) - ) - self._vector = vector - else: - raise ValueError("Configuration neither specified as dictionary " - "or vector.") - - def is_valid_configuration(self) -> None: - """ - Check if the object is a valid - :class:`~ConfigSpace.configuration_space.Configuration`. - Raise an error if configuration is not valid. - """ - ConfigSpace.c_util.check_configuration( - self.configuration_space, - self._vector, - allow_inactive_with_values=self.allow_inactive_with_values - ) - - def __getitem__(self, item: str) -> Any: - if self._query_values or item in self._values: - return self._values.get(item) - - hyperparameter = self.configuration_space._hyperparameters[item] - item_idx = self.configuration_space._hyperparameter_idx[item] - - if not np.isfinite(self._vector[item_idx]): - raise KeyError() - - value = hyperparameter._transform(self._vector[item_idx]) - # Truncate the representation of the float to be of constant - # length for a python version - if isinstance(hyperparameter, FloatHyperparameter): - value = float(repr(value)) - # TODO make everything faster, then it'll be possible to init all values - # at the same time and use an OrderedDict instead of only a dict here to - # support iterating that dict in the same order as the actual order of - # hyperparameters - self._values[item] = value - return self._values[item] - - def get(self, item: str, default: Union[None, Any] = None) -> Union[None, Any]: - """ - Return for a given hyperparameter name ``item`` the value of this - hyperparameter. ``default`` if the hyperparameter ``name`` doesn't exist. - - Parameters - ---------- - item : str - Name of the desired hyperparameter - default : None, Any - - Returns - ------- - Any, None - Value of the hyperparameter - """ - try: - return self[item] - except Exception: - return default - - def __setitem__(self, key, value): - param = self.configuration_space.get_hyperparameter(key) - if not param.is_legal(value): - raise ValueError( - "Illegal value '%s' for hyperparameter %s" % (str(value), key)) - idx = self.configuration_space.get_idx_by_hyperparameter_name(key) - vector_value = param._inverse_transform(value) - new_array = ConfigSpace.c_util.change_hp_value( - self.configuration_space, - self.get_array().copy(), - param.name, - vector_value, - idx - ) - ConfigSpace.c_util.check_configuration( - self.configuration_space, - new_array, - False - ) - self._vector = new_array - self._values = dict() - self._query_values = False - - def __contains__(self, item: str) -> bool: - self._populate_values() - return item in self._values - - # http://stackoverflow.com/a/25176504/4636294 - def __eq__(self, other: Any) -> bool: - """Override the default Equals behavior""" - if isinstance(other, self.__class__): - self._populate_values() - other._populate_values() - return self._values == other._values and \ - self.configuration_space == other.configuration_space - return NotImplemented - - def __ne__(self, other: Any) -> bool: - """Define a non-equality test""" - if isinstance(other, self.__class__): - return not self.__eq__(other) - return NotImplemented - - def __hash__(self) -> int: - """Override the default hash behavior (that returns the id or the object)""" - self._populate_values() - return hash(self.__repr__()) - - def _populate_values(self) -> None: - if self._query_values is False: - for hyperparameter in self.configuration_space.get_hyperparameters(): - self.get(hyperparameter.name) - self._query_values = True - - def __repr__(self) -> str: - self._populate_values() - - representation = io.StringIO() - representation.write("Configuration(values={\n") - - hyperparameters = self.configuration_space.get_hyperparameters() - hyperparameters.sort(key=lambda t: t.name) - for hyperparameter in hyperparameters: - hp_name = hyperparameter.name - if hp_name in self._values and self._values[hp_name] is not None: - representation.write(" ") - value = repr(self._values[hp_name]) - representation.write("'%s': %s,\n" % (hp_name, value)) - - representation.write("})\n") - return representation.getvalue() - - def __iter__(self) -> Iterable: - return iter(self.keys()) - - def __len__(self) -> int: - return len(self.configuration_space) - - def keys(self) -> List[str]: - """ - Cache the keys to speed up the process of retrieving the keys. - - Returns - ------- - list(str) - list of keys - - """ - if self._keys is None: - keys = list(self.configuration_space._hyperparameters.keys()) - keys = [ - key for i, key in enumerate(keys) if - np.isfinite(self._vector[i]) - ] - self._keys = keys - return self._keys - - def get_dictionary(self) -> Dict[str, Union[str, float, int]]: - """ - Return a representation of the - :class:`~ConfigSpace.configuration_space.Configuration` in dictionary - form. - - Returns - ------- - dict - Configuration as dictionary - - """ - self._populate_values() - return self._values - - def get_array(self) -> np.ndarray: - """ - Return the internal vector representation of the - :class:`~ConfigSpace.configuration_space.Configuration`. All continuous - values are scaled between zero and one. - - Returns - ------- - numpy.ndarray - The vector representation of the configuration - """ - return self._vector diff --git a/ConfigSpace/exceptions.py b/ConfigSpace/exceptions.py index ea6641a1..2003953c 100644 --- a/ConfigSpace/exceptions.py +++ b/ConfigSpace/exceptions.py @@ -1,2 +1,131 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ConfigSpace.conditions import ConditionComponent + from ConfigSpace.configuration_space import ConfigurationSpace + from ConfigSpace.hyperparameters import Hyperparameter + + class ForbiddenValueError(ValueError): - pass + """Raised when a combination of values is forbidden for a Configuration.""" + + +class IllegalValueError(ValueError): + def __init__(self, hyperparameter: Hyperparameter, value: Any): + super().__init__() + self.hyperparameter = hyperparameter + self.value = value + + def __str__(self) -> str: + return ( + f"Value {self.value}: ({type(self.value)}) is not allowed for" + f" hyperparameter {self.hyperparameter}" + ) + + +class ActiveHyperparameterNotSetError(ValueError): + def __init__(self, hyperparameter: Hyperparameter) -> None: + super().__init__(hyperparameter) + self.hyperparameter = hyperparameter + + def __str__(self) -> str: + return f"Hyperparameter is active but has no value set.\n{self.hyperparameter}" + + +class InactiveHyperparameterSetError(ValueError): + def __init__(self, hyperparameter: Hyperparameter, value: Any) -> None: + super().__init__(hyperparameter) + self.hyperparameter = hyperparameter + self.value = value + + def __str__(self) -> str: + return ( + f"Hyperparameter is inactive but has a value set as {self.value}.\n" + f"{self.hyperparameter}" + ) + + +class HyperparameterNotFoundError(ValueError): + def __init__( + self, + hyperparameter: Hyperparameter | str, + space: ConfigurationSpace, + preamble: str | None = None, + ): + super().__init__(hyperparameter, space, preamble) + self.preamble = preamble + self.hp_name = hyperparameter if isinstance(hyperparameter, str) else hyperparameter.name + self.space = space + + def __str__(self) -> str: + pre = f"{self.preamble}\n" if self.preamble is not None else "" + return f"{pre}" f"Hyperparameter {self.hp_name} not found in space." f"\n{self.space}" + + +class ChildNotFoundError(HyperparameterNotFoundError): + def __str__(self) -> str: + return "Child " + super().__str__() + + +class ParentNotFoundError(HyperparameterNotFoundError): + def __str__(self) -> str: + return "Parent " + super().__str__() + + +class HyperparameterIndexError(KeyError): + def __init__(self, idx: int, space: ConfigurationSpace): + super().__init__(idx, space) + self.idx = idx + self.space = space + + def __str__(self) -> str: + raise KeyError( + f"Hyperparameter #'{self.idx}' does not exist in this space." f"\n{self.space}", + ) + + +class AmbiguousConditionError(ValueError): + def __init__(self, present: ConditionComponent, new_condition: ConditionComponent): + super().__init__(present, new_condition) + self.present = present + self.new_condition = new_condition + + def __str__(self) -> str: + return ( + "Adding a second condition (different) for a hyperparameter is ambiguous" + " and therefore forbidden. Add a conjunction instead!" + f"\nAlready inserted: {self.present}" + f"\nNew one: {self.new_condition}" + ) + + +class HyperparameterAlreadyExistsError(ValueError): + def __init__( + self, + existing: Hyperparameter, + other: Hyperparameter, + space: ConfigurationSpace, + ): + super().__init__(existing, other, space) + self.existing = existing + self.other = other + self.space = space + + def __str__(self) -> str: + return ( + f"Hyperparameter {self.existing.name} already exists in space." + f"\nExisting: {self.existing}" + f"\nNew one: {self.other}" + f"{self.space}" + ) + + +class CyclicDependancyError(ValueError): + def __init__(self, cycles: list[list[str]]) -> None: + super().__init__(cycles) + self.cycles = cycles + + def __str__(self) -> str: + return f"Hyperparameter configuration contains a cycle {self.cycles}" diff --git a/ConfigSpace/functional.py b/ConfigSpace/functional.py index 395f0c33..4abeed3b 100644 --- a/ConfigSpace/functional.py +++ b/ConfigSpace/functional.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from typing import Iterator -from more_itertools import roundrobin import numpy as np +from more_itertools import roundrobin def center_range( diff --git a/ConfigSpace/hyperparameters/__init__.py b/ConfigSpace/hyperparameters/__init__.py index a409ff0e..e8410058 100644 --- a/ConfigSpace/hyperparameters/__init__.py +++ b/ConfigSpace/hyperparameters/__init__.py @@ -1,19 +1,30 @@ -from .hyperparameter import Hyperparameter +from .beta_float import BetaFloatHyperparameter +from .beta_integer import BetaIntegerHyperparameter +from .categorical import CategoricalHyperparameter from .constant import Constant, UnParametrizedHyperparameter -from .numerical import NumericalHyperparameter from .float_hyperparameter import FloatHyperparameter +from .hyperparameter import Hyperparameter from .integer_hyperparameter import IntegerHyperparameter +from .normal_float import NormalFloatHyperparameter +from .normal_integer import NormalIntegerHyperparameter +from .numerical import NumericalHyperparameter from .ordinal import OrdinalHyperparameter -from .categorical import CategoricalHyperparameter from .uniform_float import UniformFloatHyperparameter from .uniform_integer import UniformIntegerHyperparameter -from .normal_float import NormalFloatHyperparameter -from .normal_integer import NormalIntegerHyperparameter -from .beta_float import BetaFloatHyperparameter -from .beta_integer import BetaIntegerHyperparameter -__all__ = ["Hyperparameter", "Constant", "UnParametrizedHyperparameter", "OrdinalHyperparameter", - "CategoricalHyperparameter", "NumericalHyperparameter", "FloatHyperparameter", - "IntegerHyperparameter", "UniformFloatHyperparameter", "UniformIntegerHyperparameter", - "NormalFloatHyperparameter", "NormalIntegerHyperparameter", "BetaFloatHyperparameter", - "BetaIntegerHyperparameter"] +__all__ = [ + "Hyperparameter", + "Constant", + "UnParametrizedHyperparameter", + "OrdinalHyperparameter", + "CategoricalHyperparameter", + "NumericalHyperparameter", + "FloatHyperparameter", + "IntegerHyperparameter", + "UniformFloatHyperparameter", + "UniformIntegerHyperparameter", + "NormalFloatHyperparameter", + "NormalIntegerHyperparameter", + "BetaFloatHyperparameter", + "BetaIntegerHyperparameter", +] diff --git a/ConfigSpace/nx/__init__.py b/ConfigSpace/nx/__init__.py index 761f2939..380aec8e 100644 --- a/ConfigSpace/nx/__init__.py +++ b/ConfigSpace/nx/__init__.py @@ -9,34 +9,38 @@ # # Modified by Matthias Feurer for the package HPOlibConfigSpace -from __future__ import absolute_import # Release data -from ConfigSpace.nx.release import authors, license, date, version +from ConfigSpace.nx.release import authors, date, license, version -__author__ = '%s <%s>\n%s <%s>\n%s <%s>' % \ - (authors['Hagberg'] + authors['Schult'] + authors['Swart']) +__author__ = "%s <%s>\n%s <%s>\n%s <%s>" % ( + authors["Hagberg"] + authors["Schult"] + authors["Swart"] +) __license__ = license __date__ = date __version__ = version -from ConfigSpace.nx.exception import ( - NetworkXException, NetworkXError, - NetworkXPointlessConcept, NetworkXAlgorithmError, - NetworkXUnfeasible, NetworkXNoPath, - NetworkXUnbounded, NetworkXNotImplemented -) - -# import ConfigSpace.nx.classes -from ConfigSpace.nx.classes import ( - Graph, DiGraph -) - from ConfigSpace.nx.algorithms import ( - descendants, ancestors, topological_sort, topological_sort_recursive, - is_directed_acyclic_graph, is_aperiodic, simple_cycles, - strongly_connected_components + ancestors, + descendants, + is_aperiodic, + is_directed_acyclic_graph, + simple_cycles, + strongly_connected_components, + topological_sort, + topological_sort_recursive, +) +from ConfigSpace.nx.classes import DiGraph, Graph +from ConfigSpace.nx.exception import ( + NetworkXAlgorithmError, + NetworkXError, + NetworkXException, + NetworkXNoPath, + NetworkXNotImplemented, + NetworkXPointlessConcept, + NetworkXUnbounded, + NetworkXUnfeasible, ) __all__ = [ @@ -57,5 +61,5 @@ "is_directed_acyclic_graph", "is_aperiodic", "simple_cycles", - "strongly_connected_components" + "strongly_connected_components", ] diff --git a/ConfigSpace/nx/algorithms/__init__.py b/ConfigSpace/nx/algorithms/__init__.py index 4a7e641a..35b0c488 100644 --- a/ConfigSpace/nx/algorithms/__init__.py +++ b/ConfigSpace/nx/algorithms/__init__.py @@ -1,11 +1,13 @@ +from ConfigSpace.nx.algorithms.components import strongly_connected_components +from ConfigSpace.nx.algorithms.cycles import simple_cycles from ConfigSpace.nx.algorithms.dag import ( - descendants, ancestors, topological_sort, topological_sort_recursive, - is_directed_acyclic_graph, is_aperiodic + ancestors, + descendants, + is_aperiodic, + is_directed_acyclic_graph, + topological_sort, + topological_sort_recursive, ) -from ConfigSpace.nx.algorithms.cycles import simple_cycles - -from ConfigSpace.nx.algorithms.components import strongly_connected_components - __all__ = [ "descendants", @@ -15,5 +17,5 @@ "is_directed_acyclic_graph", "is_aperiodic", "simple_cycles", - "strongly_connected_components" + "strongly_connected_components", ] diff --git a/ConfigSpace/nx/algorithms/components/__init__.py b/ConfigSpace/nx/algorithms/components/__init__.py index 92337e9d..48c8934e 100644 --- a/ConfigSpace/nx/algorithms/components/__init__.py +++ b/ConfigSpace/nx/algorithms/components/__init__.py @@ -1,5 +1,3 @@ -from ConfigSpace.nx.algorithms.components.strongly_connected import ( - strongly_connected_components -) +from ConfigSpace.nx.algorithms.components.strongly_connected import strongly_connected_components -__all__ = ['strongly_connected_components'] +__all__ = ["strongly_connected_components"] diff --git a/ConfigSpace/nx/algorithms/components/strongly_connected.py b/ConfigSpace/nx/algorithms/components/strongly_connected.py index 1b8a7d73..60dcc984 100644 --- a/ConfigSpace/nx/algorithms/components/strongly_connected.py +++ b/ConfigSpace/nx/algorithms/components/strongly_connected.py @@ -1,21 +1,23 @@ -# -*- coding: utf-8 -*- -""" -Strongly connected components. -""" +"""Strongly connected components.""" # Copyright (C) 2004-2011 by # Aric Hagberg # Dan Schult # Pieter Swart # All rights reserved. # BSD license. +from __future__ import annotations + import ConfigSpace.nx -__authors__ = "\n".join(['Eben Kenah', - 'Aric Hagberg (hagberg@lanl.gov)' - 'Christopher Ellison', - 'Ben Edwards (bedwards@cs.unm.edu)']) +__authors__ = "\n".join( + [ + "Eben Kenah", + "Aric Hagberg (hagberg@lanl.gov)" "Christopher Ellison", + "Ben Edwards (bedwards@cs.unm.edu)", + ], +) -__all__ = ['strongly_connected_components'] +__all__ = ["strongly_connected_components"] def strongly_connected_components(G): @@ -55,8 +57,10 @@ def strongly_connected_components(G): Information Processing Letters 49(1): 9-14, (1994).. """ if not G.is_directed(): - raise ConfigSpace.nx.NetworkXError("""Not allowed for undirected graph G. - Use connected_components() """) + raise ConfigSpace.nx.NetworkXError( + """Not allowed for undirected graph G. + Use connected_components() """, + ) preorder = {} lowlink = {} scc_found = {} @@ -90,8 +94,7 @@ def strongly_connected_components(G): if lowlink[v] == preorder[v]: scc_found[v] = True scc = [v] - while (scc_queue - and preorder[scc_queue[-1]] > preorder[v]): + while scc_queue and preorder[scc_queue[-1]] > preorder[v]: k = scc_queue.pop() scc_found[k] = True scc.append(k) diff --git a/ConfigSpace/nx/algorithms/cycles.py b/ConfigSpace/nx/algorithms/cycles.py index 4c3a79d6..945316c2 100644 --- a/ConfigSpace/nx/algorithms/cycles.py +++ b/ConfigSpace/nx/algorithms/cycles.py @@ -1,7 +1,7 @@ """ ======================== Cycle finding algorithms -======================== +========================. """ # Copyright (C) 2010-2012 by # Aric Hagberg @@ -9,13 +9,20 @@ # Pieter Swart # All rights reserved. # BSD license. +from __future__ import annotations + from collections import defaultdict + import ConfigSpace.nx -__all__ = ['simple_cycles'] -__author__ = "\n".join(['Jon Olav Vik ', - 'Dan Schult ', - 'Aric Hagberg ']) +__all__ = ["simple_cycles"] +__author__ = "\n".join( + [ + "Jon Olav Vik ", + "Dan Schult ", + "Aric Hagberg ", + ], +) def simple_cycles(G): @@ -78,8 +85,9 @@ def simple_cycles(G): -------- cycle_basis """ + def _unblock(thisnode, blocked, B): - stack = set([thisnode]) + stack = {thisnode} while stack: node = stack.pop() if node in blocked: @@ -90,7 +98,7 @@ def _unblock(thisnode, blocked, B): # Johnson's algorithm requires some ordering of the nodes. # We assign the arbitrary ordering given by the strongly connected comps # There is no need to track the ordering as each node removed as processed. - subG = G.copy() # save the actual graph so we can mutate it here + subG = G.copy() # save the actual graph so we can mutate it here sccs = ConfigSpace.nx.strongly_connected_components(subG) while sccs: scc = sccs.pop() @@ -107,12 +115,9 @@ def _unblock(thisnode, blocked, B): thisnode, nbrs = stack[-1] if nbrs: nextnode = nbrs.pop() -# print thisnode,nbrs,":",nextnode,blocked,B,path,stack,startnode -# f=raw_input("pause") if nextnode == startnode: yield path[:] closed.update(path) -# print "Found a cycle",path,closed elif nextnode not in blocked: path.append(nextnode) stack.append((nextnode, list(subG[nextnode]))) @@ -127,7 +132,6 @@ def _unblock(thisnode, blocked, B): if thisnode not in B[nbr]: B[nbr].add(thisnode) stack.pop() -# assert path[-1]==thisnode path.pop() # done processing this node subG.remove_node(startnode) diff --git a/ConfigSpace/nx/algorithms/dag.py b/ConfigSpace/nx/algorithms/dag.py index 0871158d..2fed3405 100644 --- a/ConfigSpace/nx/algorithms/dag.py +++ b/ConfigSpace/nx/algorithms/dag.py @@ -1,3 +1,5 @@ +from __future__ import annotations + try: # >= Python 3.9 from math import gcd # type: ignore @@ -7,7 +9,6 @@ import ConfigSpace.nx - """Algorithms for directed acyclic graphs (DAGs).""" # Copyright (C) 2006-2011 by # Aric Hagberg @@ -15,17 +16,21 @@ # Pieter Swart # All rights reserved. # BSD license. -__author__ = """\n""".join(['Aric Hagberg ', - 'Dan Schult (dschult@colgate.edu)', - 'Ben Edwards (bedwards@cs.unm.edu)']) +__author__ = """\n""".join( + [ + "Aric Hagberg ", + "Dan Schult (dschult@colgate.edu)", + "Ben Edwards (bedwards@cs.unm.edu)", + ], +) __all__ = [ - 'descendants', - 'ancestors', - 'topological_sort', - 'topological_sort_recursive', - 'is_directed_acyclic_graph', - 'is_aperiodic' - ] + "descendants", + "ancestors", + "topological_sort", + "topological_sort_recursive", + "is_directed_acyclic_graph", + "is_aperiodic", +] def descendants(G, source): @@ -42,11 +47,8 @@ def descendants(G, source): The descendants of source in G """ if not G.has_node(source): - raise ConfigSpace.nx.NetworkXError( - "The node %s is not in the graph." % source) - des = set(ConfigSpace.nx.shortest_path_length(G, - source=source).keys()) - set( - [source]) + raise ConfigSpace.nx.NetworkXError("The node %s is not in the graph." % source) + des = set(ConfigSpace.nx.shortest_path_length(G, source=source).keys()) - {source} return des @@ -64,11 +66,8 @@ def ancestors(G, source): The ancestors of source in G """ if not G.has_node(source): - raise ConfigSpace.nx.NetworkXError( - "The node %s is not in the graph." % source) - anc = set(ConfigSpace.nx.shortest_path_length(G, - target=source).keys()) - set( - [source]) + raise ConfigSpace.nx.NetworkXError("The node %s is not in the graph." % source) + anc = set(ConfigSpace.nx.shortest_path_length(G, target=source).keys()) - {source} return anc @@ -125,7 +124,7 @@ def topological_sort(G, nbunch=None): This algorithm is based on a description and proof in The Algorithm Design Manual [1]_ . - See also + See Also -------- is_directed_acyclic_graph @@ -135,8 +134,7 @@ def topological_sort(G, nbunch=None): http://www.amazon.com/exec/obidos/ASIN/0387948600/ref=ase_thealgorithmrepo/ """ if not G.is_directed(): - raise ConfigSpace.nx.NetworkXError( - "Topological sort not defined on undirected graphs.") + raise ConfigSpace.nx.NetworkXError("Topological sort not defined on undirected graphs.") # nonrecursive version seen = set() @@ -160,8 +158,7 @@ def topological_sort(G, nbunch=None): for n in G[w]: if n not in explored: if n in seen: # CYCLE !! - raise ConfigSpace.nx.NetworkXUnfeasible( - "Graph contains a cycle.") + raise ConfigSpace.nx.NetworkXUnfeasible("Graph contains a cycle.") new_nodes.append(n) if new_nodes: # Add new_nodes to fringe fringe.extend(new_nodes) @@ -200,23 +197,21 @@ def topological_sort_recursive(G, nbunch=None): ----- This is a recursive version of topological sort. - See also + See Also -------- topological_sort is_directed_acyclic_graph """ if not G.is_directed(): - raise ConfigSpace.nx.NetworkXError( - "Topological sort not defined on undirected graphs.") + raise ConfigSpace.nx.NetworkXError("Topological sort not defined on undirected graphs.") def _dfs(v): ancestors.add(v) for w in G[v]: if w in ancestors: - raise ConfigSpace.nx.NetworkXUnfeasible( - "Graph contains a cycle.") + raise ConfigSpace.nx.NetworkXUnfeasible("Graph contains a cycle.") if w not in explored: _dfs(w) @@ -274,8 +269,7 @@ def is_aperiodic(G): A Multidisciplinary Approach, CRC Press. """ if not G.is_directed(): - raise ConfigSpace.nx.NetworkXError( - "is_aperiodic not defined for undirected graphs") + raise ConfigSpace.nx.NetworkXError("is_aperiodic not defined for undirected graphs") s = next(G.nodes_iter()) levels = {s: 0} @@ -296,5 +290,4 @@ def is_aperiodic(G): if len(levels) == len(G): # All nodes in tree return g == 1 else: - return g == 1 and ConfigSpace.nx.is_aperiodic( - G.subgraph(set(G) - set(levels))) + return g == 1 and ConfigSpace.nx.is_aperiodic(G.subgraph(set(G) - set(levels))) diff --git a/ConfigSpace/nx/classes/__init__.py b/ConfigSpace/nx/classes/__init__.py index 560340f0..bdd0332a 100644 --- a/ConfigSpace/nx/classes/__init__.py +++ b/ConfigSpace/nx/classes/__init__.py @@ -1,8 +1,4 @@ -from ConfigSpace.nx.classes.graph import Graph from ConfigSpace.nx.classes.digraph import DiGraph +from ConfigSpace.nx.classes.graph import Graph - -__all__ = [ - "Graph", - "DiGraph" -] +__all__ = ["Graph", "DiGraph"] diff --git a/ConfigSpace/nx/classes/digraph.py b/ConfigSpace/nx/classes/digraph.py index 6f2e952c..277fffcf 100644 --- a/ConfigSpace/nx/classes/digraph.py +++ b/ConfigSpace/nx/classes/digraph.py @@ -5,16 +5,21 @@ # Pieter Swart # All rights reserved. # BSD license. +from __future__ import annotations + import collections from copy import deepcopy from ConfigSpace.nx.classes.graph import Graph from ConfigSpace.nx.exception import NetworkXError - -__author__ = """\n""".join(['Aric Hagberg (hagberg@lanl.gov)', - 'Pieter Swart (swart@lanl.gov)', - 'Dan Schult(dschult@colgate.edu)']) +__author__ = """\n""".join( + [ + "Aric Hagberg (hagberg@lanl.gov)", + "Pieter Swart (swart@lanl.gov)", + "Dan Schult(dschult@colgate.edu)", + ], +) class DiGraph(Graph): @@ -211,7 +216,6 @@ def __init__(self, data=None, **attr): # attempt to load graph with data # if data is not None: - # convert.to_networkx_graph(data, create_using=self) # load graph attributes (must be after convert) self.graph.update(attr) self.edge = self.adj @@ -353,7 +357,7 @@ def remove_node(self, n): A node in the graph Raises - ------- + ------ NetworkXError If n is not in the graph. @@ -376,7 +380,7 @@ def remove_node(self, n): nbrs = self.succ[n] del self.node[n] except KeyError: # NetworkXError if n not in self - raise NetworkXError("The node %s is not in the digraph." % (n,)) + raise NetworkXError(f"The node {n} is not in the digraph.") for u in nbrs: del self.pred[u][n] # remove all edges n-u in digraph del self.succ[n] # remove node from succ @@ -478,8 +482,7 @@ def add_edge(self, u, v, attr_dict=None, **attr): try: attr_dict.update(attr) except AttributeError: - raise NetworkXError( - "The attr_dict argument must be a dictionary.") + raise NetworkXError("The attr_dict argument must be a dictionary.") # add nodes if u not in self.succ: self.succ[u] = collections.OrderedDict() @@ -526,7 +529,7 @@ def remove_edge(self, u, v): del self.succ[u][v] del self.pred[v][u] except KeyError: - raise NetworkXError("The edge %s-%s not in graph." % (u, v)) + raise NetworkXError(f"The edge {u}-{v} not in graph.") def remove_edges_from(self, ebunch): """Remove all edges specified in ebunch. @@ -566,14 +569,14 @@ def has_successor(self, u, v): This is true if graph has the edge u->v. """ - return (u in self.succ and v in self.succ[u]) + return u in self.succ and v in self.succ[u] def has_predecessor(self, u, v): """Return True if node u has predecessor v. This is true if graph has the edge u<-v. """ - return (u in self.pred and v in self.pred[u]) + return u in self.pred and v in self.pred[u] def successors_iter(self, n): """Return an iterator over successor nodes of n. @@ -583,14 +586,14 @@ def successors_iter(self, n): try: return iter(self.succ[n]) except KeyError: - raise NetworkXError("The node %s is not in the digraph." % (n,)) + raise NetworkXError(f"The node {n} is not in the digraph.") def predecessors_iter(self, n): """Return an iterator over predecessor nodes of n.""" try: return iter(self.pred[n]) except KeyError: - raise NetworkXError("The node %s is not in the digraph." % (n,)) + raise NetworkXError(f"The node {n} is not in the digraph.") def successors(self, n): """Return a list of successor nodes of n. @@ -748,17 +751,20 @@ def degree_iter(self, nbunch=None, weight=None): else: nodes_nbrs = zip( ((n, self.succ[n]) for n in self.nbunch_iter(nbunch)), - ((n, self.pred[n]) for n in self.nbunch_iter(nbunch))) + ((n, self.pred[n]) for n in self.nbunch_iter(nbunch)), + ) if weight is None: - for (n, succ), (n2, pred) in nodes_nbrs: + for (n, succ), (_n2, pred) in nodes_nbrs: yield (n, len(succ) + len(pred)) else: # edge weighted graph - degree is sum of edge weights - for (n, succ), (n2, pred) in nodes_nbrs: - yield (n, - sum((succ[nbr].get(weight, 1) for nbr in succ)) + - sum((pred[nbr].get(weight, 1) for nbr in pred))) + for (n, succ), (_n2, pred) in nodes_nbrs: + yield ( + n, + sum(succ[nbr].get(weight, 1) for nbr in succ) + + sum(pred[nbr].get(weight, 1) for nbr in pred), + ) def in_degree_iter(self, nbunch=None, weight=None): """Return an iterator for (node, in-degree). @@ -1046,14 +1052,16 @@ def to_undirected(self, reciprocal=False): H.name = self.name H.add_nodes_from(self) if reciprocal is True: - H.add_edges_from((u, v, deepcopy(d)) - for u, nbrs in self.adjacency_iter() - for v, d in nbrs.items() - if v in self.pred[u]) + H.add_edges_from( + (u, v, deepcopy(d)) + for u, nbrs in self.adjacency_iter() + for v, d in nbrs.items() + if v in self.pred[u] + ) else: - H.add_edges_from((u, v, deepcopy(d)) - for u, nbrs in self.adjacency_iter() - for v, d in nbrs.items()) + H.add_edges_from( + (u, v, deepcopy(d)) for u, nbrs in self.adjacency_iter() for v, d in nbrs.items() + ) H.graph = deepcopy(self.graph) H.node = deepcopy(self.node) return H @@ -1074,8 +1082,7 @@ def reverse(self, copy=True): if copy: H = self.__class__(name="Reverse of (%s)" % self.name) H.add_nodes_from(self) - H.add_edges_from((v, u, deepcopy(d)) for u, v, d - in self.edges(data=True)) + H.add_edges_from((v, u, deepcopy(d)) for u, v, d in self.edges(data=True)) H.graph = deepcopy(self.graph) H.node = deepcopy(self.node) else: diff --git a/ConfigSpace/nx/classes/graph.py b/ConfigSpace/nx/classes/graph.py index 9eea0848..701aa02c 100644 --- a/ConfigSpace/nx/classes/graph.py +++ b/ConfigSpace/nx/classes/graph.py @@ -13,18 +13,23 @@ # Pieter Swart # All rights reserved. # BSD license. +from __future__ import annotations + import collections from copy import deepcopy from ConfigSpace.nx.exception import NetworkXError - -__author__ = """\n""".join(['Aric Hagberg (hagberg@lanl.gov)', - 'Pieter Swart (swart@lanl.gov)', - 'Dan Schult(dschult@colgate.edu)']) +__author__ = """\n""".join( + [ + "Aric Hagberg (hagberg@lanl.gov)", + "Pieter Swart (swart@lanl.gov)", + "Dan Schult(dschult@colgate.edu)", + ], +) -class Graph(object): +class Graph: """ Base class for undirected graphs. @@ -214,18 +219,17 @@ def __init__(self, data=None, **attr): self.adj = collections.OrderedDict() # empty adjacency dict # attempt to load graph with data # if data is not None: - # convert.to_networkx_graph(data, create_using=self) # load graph attributes (must be after convert) self.graph.update(attr) self.edge = self.adj @property def name(self): - return self.graph.get('name', '') + return self.graph.get("name", "") @name.setter def name(self, s): - self.graph['name'] = s + self.graph["name"] = s def __str__(self): """Return the graph name. @@ -371,8 +375,7 @@ def add_node(self, n, attr_dict=None, **attr): try: attr_dict.update(attr) except AttributeError: - raise NetworkXError( - "The attr_dict argument must be a dictionary.") + raise NetworkXError("The attr_dict argument must be a dictionary.") if n not in self.node: self.adj[n] = collections.OrderedDict() self.node[n] = attr_dict @@ -457,7 +460,7 @@ def remove_node(self, n): A node in the graph Raises - ------- + ------ NetworkXError If n is not in the graph. @@ -478,11 +481,10 @@ def remove_node(self, n): """ adj = self.adj try: - nbrs = list( - adj[n].keys()) # keys handles self-loops (allow mutation later) + nbrs = list(adj[n].keys()) # keys handles self-loops (allow mutation later) del self.node[n] except KeyError: # NetworkXError if n not in self - raise NetworkXError("The node %s is not in the graph." % (n,)) + raise NetworkXError(f"The node {n} is not in the graph.") for u in nbrs: del adj[u][n] # remove all edges n-u in graph del adj[n] # now remove node @@ -701,8 +703,7 @@ def add_edge(self, u, v, attr_dict=None, **attr): try: attr_dict.update(attr) except AttributeError: - raise NetworkXError( - "The attr_dict argument must be a dictionary.") + raise NetworkXError("The attr_dict argument must be a dictionary.") # add nodes if u not in self.node: self.adj[u] = collections.OrderedDict() @@ -763,8 +764,7 @@ def add_edges_from(self, ebunch, attr_dict=None, **attr): try: attr_dict.update(attr) except AttributeError: - raise NetworkXError( - "The attr_dict argument must be a dictionary.") + raise NetworkXError("The attr_dict argument must be a dictionary.") # process ebunch for e in ebunch: ne = len(e) @@ -774,8 +774,7 @@ def add_edges_from(self, ebunch, attr_dict=None, **attr): u, v = e dd = collections.OrderedDict() else: - raise NetworkXError( - "Edge tuple %s must be a 2-tuple or 3-tuple." % (e,)) + raise NetworkXError(f"Edge tuple {e} must be a 2-tuple or 3-tuple.") if u not in self.node: self.adj[u] = collections.OrderedDict() self.node[u] = collections.OrderedDict() @@ -788,7 +787,7 @@ def add_edges_from(self, ebunch, attr_dict=None, **attr): self.adj[u][v] = datadict self.adj[v][u] = datadict - def add_weighted_edges_from(self, ebunch, weight='weight', **attr): + def add_weighted_edges_from(self, ebunch, weight="weight", **attr): """Add all the edges in ebunch as weighted edges with specified weights. @@ -853,7 +852,7 @@ def remove_edge(self, u, v): if u != v: # self-loop needs only one entry removed del self.adj[v][u] except KeyError: - raise NetworkXError("The edge %s-%s is not in the graph" % (u, v)) + raise NetworkXError(f"The edge {u}-{v} is not in the graph") def remove_edges_from(self, ebunch): """Remove all edges specified in ebunch. @@ -971,7 +970,7 @@ def neighbors(self, n): try: return list(self.adj[n]) except KeyError: - raise NetworkXError("The node %s is not in the graph." % (n,)) + raise NetworkXError(f"The node {n} is not in the graph.") def neighbors_iter(self, n): """Return an iterator over all neighbors of node n. @@ -994,7 +993,7 @@ def neighbors_iter(self, n): try: return iter(self.adj[n]) except KeyError: - raise NetworkXError("The node %s is not in the graph." % (n,)) + raise NetworkXError(f"The node {n} is not in the graph.") def edges(self, nbunch=None, data=False): """Return a list of edges. @@ -1011,7 +1010,7 @@ def edges(self, nbunch=None, data=False): Return two tuples (u,v) (False) or three-tuples (u,v,data) (True). Returns - -------- + ------- edge_list: list of edge tuples Edges that are adjacent to any node in nbunch, or a list of all edges if nbunch is not specified. @@ -1287,8 +1286,11 @@ def degree_iter(self, nbunch=None, weight=None): else: # edge weighted graph - degree is sum of nbr edge weights for n, nbrs in nodes_nbrs: - yield (n, sum((nbrs[nbr].get(weight, 1) for nbr in nbrs)) + - (n in nbrs and nbrs[n].get(weight, 1))) + yield ( + n, + sum(nbrs[nbr].get(weight, 1) for nbr in nbrs) + + (n in nbrs and nbrs[n].get(weight, 1)), + ) def clear(self): """Remove all nodes and edges from the graph. @@ -1306,7 +1308,7 @@ def clear(self): [] """ - self.name = '' + self.name = "" self.adj.clear() self.node.clear() self.graph.clear() @@ -1388,9 +1390,13 @@ def to_directed(self): G = DiGraph() G.name = self.name G.add_nodes_from(self) - G.add_edges_from(((u, v, deepcopy(data)) - for u, nbrs in self.adjacency_iter() - for v, data in nbrs.items())) + G.add_edges_from( + ( + (u, v, deepcopy(data)) + for u, nbrs in self.adjacency_iter() + for v, data in nbrs.items() + ), + ) G.graph = deepcopy(self.graph) G.node = deepcopy(self.node) return G @@ -1523,7 +1529,7 @@ def selfloop_edges(self, data=False): A selfloop edge has the same node at both ends. Parameters - ----------- + ---------- data : bool, optional (default=False) Return selfloop edges as two tuples (u,v) (data=False) or three-tuples (u,v,data) (data=True) @@ -1548,11 +1554,9 @@ def selfloop_edges(self, data=False): [(1, 1, {})] """ if data: - return [(n, n, nbrs[n]) - for n, nbrs in self.adj.items() if n in nbrs] + return [(n, n, nbrs[n]) for n, nbrs in self.adj.items() if n in nbrs] else: - return [(n, n) - for n, nbrs in self.adj.items() if n in nbrs] + return [(n, n) for n, nbrs in self.adj.items() if n in nbrs] def number_of_selfloops(self): """Return the number of selfloop edges. @@ -1782,6 +1786,7 @@ def nbunch_iter(self, nbunch=None): elif nbunch in self: # if nbunch is a single node bunch = iter([nbunch]) else: # if nbunch is a sequence of nodes + def bunch_iter(nlist, adj): try: for n in nlist: @@ -1793,13 +1798,13 @@ def bunch_iter(nlist, adj): sys.stdout.write(message) # capture error for non-sequence/iterator nbunch. - if 'iter' in message: - raise NetworkXError( - "nbunch is not a node or a sequence of nodes.") + if "iter" in message: + raise NetworkXError("nbunch is not a node or a sequence of nodes.") # capture error for unhashable node. - elif 'hashable' in message: + elif "hashable" in message: raise NetworkXError( - "Node %s in the sequence nbunch is not a valid node." % n) + "Node %s in the sequence nbunch is not a valid node." % n, + ) else: raise diff --git a/ConfigSpace/nx/exception.py b/ConfigSpace/nx/exception.py index 63e189f9..50e322c8 100644 --- a/ConfigSpace/nx/exception.py +++ b/ConfigSpace/nx/exception.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- """ ********** Exceptions -********** +**********. Base exceptions and errors for NetworkX. """ +from __future__ import annotations + __author__ = """Aric Hagberg (hagberg@lanl.gov) Pieter Swart (swart@lanl.gov) Dan Schult(dschult@colgate.edu) @@ -28,14 +29,14 @@ class NetworkXException(Exception): class NetworkXError(NetworkXException): - """Exception for a serious error in NetworkX""" + """Exception for a serious error in NetworkX.""" class NetworkXPointlessConcept(NetworkXException): """Harary, F. and Read, R. "Is the Null Graph a Pointless Concept?" -In Graphs and Combinatorics Conference, George Washington University. -New York: Springer-Verlag, 1973. -""" + In Graphs and Combinatorics Conference, George Washington University. + New York: Springer-Verlag, 1973. + """ class NetworkXAlgorithmError(NetworkXException): @@ -44,17 +45,20 @@ class NetworkXAlgorithmError(NetworkXException): class NetworkXUnfeasible(NetworkXAlgorithmError): """Exception raised by algorithms trying to solve a problem - instance that has no feasible solution.""" + instance that has no feasible solution. + """ class NetworkXNoPath(NetworkXUnfeasible): """Exception for algorithms that should return a path when running - on graphs where such a path does not exist.""" + on graphs where such a path does not exist. + """ class NetworkXUnbounded(NetworkXAlgorithmError): """Exception raised by algorithms trying to solve a maximization - or a minimization problem instance that is unbounded.""" + or a minimization problem instance that is unbounded. + """ class NetworkXNotImplemented(NetworkXException): diff --git a/ConfigSpace/nx/release.py b/ConfigSpace/nx/release.py index 4f6ef361..4b1a330d 100644 --- a/ConfigSpace/nx/release.py +++ b/ConfigSpace/nx/release.py @@ -4,14 +4,13 @@ # Pieter Swart # All rights reserved. # BSD license. +from __future__ import annotations -from __future__ import absolute_import - +import datetime import os +import subprocess import sys import time -import datetime -import subprocess basedir = os.path.abspath(os.path.split(__file__)[0]) @@ -20,15 +19,13 @@ def get_revision(): """Returns revision and vcs information, dynamically obtained.""" vcs, revision, tag = None, None, None - hgdir = os.path.join(basedir, '..', '.hg') - gitdir = os.path.join(basedir, '..', '.git') + hgdir = os.path.join(basedir, "..", ".hg") + gitdir = os.path.join(basedir, "..", ".git") if os.path.isdir(hgdir): - vcs = 'mercurial' + vcs = "mercurial" try: - p = subprocess.Popen(['hg', 'id'], - cwd=basedir, - stdout=subprocess.PIPE) + p = subprocess.Popen(["hg", "id"], cwd=basedir, stdout=subprocess.PIPE) except OSError: # Could not run hg, even though this is a mercurial repository. pass @@ -50,7 +47,7 @@ def get_revision(): tag = str(x[1]) elif os.path.isdir(gitdir): - vcs = 'git' + vcs = "git" # For now, we are not bothering with revision and tag. vcs_info = (vcs, (revision, tag)) @@ -79,7 +76,7 @@ def get_info(dynamic=True): # no vcs information will be provided. sys.path.insert(0, basedir) try: - from version import date, date_info, version, version_info, vcs_info + from version import date, date_info, vcs_info, version, version_info # type: ignore except ImportError: import_failed = True vcs_info = (None, (None, None)) @@ -91,16 +88,16 @@ def get_info(dynamic=True): # We are here if: # we failed to determine static versioning info, or # we successfully obtained dynamic revision info - version = ''.join([str(major), '.', str(minor)]) # noqa + version = "".join([str(major), ".", str(minor)]) # noqa if dev: - version += '.dev_' + date_info.strftime("%Y%m%d%H%M%S") + version += ".dev_" + date_info.strftime("%Y%m%d%H%M%S") version_info = (name, major, minor, revision) # noqa return date, date_info, version, version_info, vcs_info # Version information -name = 'networkx' +name = "networkx" major = "1" minor = "8.1" @@ -117,35 +114,43 @@ def get_info(dynamic=True): study of the structure, dynamics, and functions of complex networks. """ -license = 'BSD' +license = "BSD" authors = { - 'Hagberg': ('Aric Hagberg', 'hagberg@lanl.gov'), - 'Schult': ('Dan Schult', 'dschult@colgate.edu'), - 'Swart': ('Pieter Swart', 'swart@lanl.gov') + "Hagberg": ("Aric Hagberg", "hagberg@lanl.gov"), + "Schult": ("Dan Schult", "dschult@colgate.edu"), + "Swart": ("Pieter Swart", "swart@lanl.gov"), } maintainer = "NetworkX Developers" maintainer_email = "networkx-discuss@googlegroups.com" -url = 'http://networkx.lanl.gov/' +url = "http://networkx.lanl.gov/" download_url = "http://networkx.lanl.gov/download/networkx" -platforms = ['Linux', 'Mac OSX', 'Windows', 'Unix'] -keywords = ['Networks', 'Graph Theory', 'Mathematics', 'network', 'graph', - 'discrete mathematics', 'math'] +platforms = ["Linux", "Mac OSX", "Windows", "Unix"] +keywords = [ + "Networks", + "Graph Theory", + "Mathematics", + "network", + "graph", + "discrete mathematics", + "math", +] classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Scientific/Engineering :: Bio-Informatics', - 'Topic :: Scientific/Engineering :: Information Analysis', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Scientific/Engineering :: Physics'] + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.1", + "Programming Language :: Python :: 3.2", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Physics", +] date, date_info, version, version_info, vcs_info = get_info() diff --git a/ConfigSpace/read_and_write/json.py b/ConfigSpace/read_and_write/json.py index 8142e8ea..cf2ff111 100644 --- a/ConfigSpace/read_and_write/json.py +++ b/ConfigSpace/read_and_write/json.py @@ -1,333 +1,322 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- +from __future__ import annotations import json -from typing import Dict from ConfigSpace import __version__ -from ConfigSpace.configuration_space import ConfigurationSpace -from ConfigSpace.hyperparameters import ( - Hyperparameter, - CategoricalHyperparameter, - UniformIntegerHyperparameter, - UniformFloatHyperparameter, - NormalIntegerHyperparameter, - NormalFloatHyperparameter, - OrdinalHyperparameter, - Constant, - UnParametrizedHyperparameter, - BetaFloatHyperparameter, - BetaIntegerHyperparameter -) from ConfigSpace.conditions import ( AbstractCondition, - EqualsCondition, - NotEqualsCondition, - InCondition, AndConjunction, - OrConjunction, + EqualsCondition, GreaterThanCondition, + InCondition, LessThanCondition, + NotEqualsCondition, + OrConjunction, ) +from ConfigSpace.configuration_space import ConfigurationSpace from ConfigSpace.forbidden import ( - ForbiddenEqualsClause, - ForbiddenAndConjunction, - ForbiddenInClause, AbstractForbiddenComponent, - ForbiddenRelation, - ForbiddenLessThanRelation, + ForbiddenAndConjunction, + ForbiddenEqualsClause, ForbiddenEqualsRelation, ForbiddenGreaterThanRelation, + ForbiddenInClause, + ForbiddenLessThanRelation, + ForbiddenRelation, +) +from ConfigSpace.hyperparameters import ( + BetaFloatHyperparameter, + BetaIntegerHyperparameter, + CategoricalHyperparameter, + Constant, + Hyperparameter, + NormalFloatHyperparameter, + NormalIntegerHyperparameter, + OrdinalHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, + UnParametrizedHyperparameter, ) - JSON_FORMAT_VERSION = 0.4 ################################################################################ # Builder for hyperparameters -def _build_constant(param: Constant) -> Dict: +def _build_constant(param: Constant) -> dict: return { - 'name': param.name, - 'type': 'constant', - 'value': param.value, + "name": param.name, + "type": "constant", + "value": param.value, } -def _build_unparametrized_hyperparameter(param: UnParametrizedHyperparameter) -> Dict: +def _build_unparametrized_hyperparameter(param: UnParametrizedHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'unparametrized', - 'value': param.value, + "name": param.name, + "type": "unparametrized", + "value": param.value, } -def _build_uniform_float(param: UniformFloatHyperparameter) -> Dict: +def _build_uniform_float(param: UniformFloatHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'uniform_float', - 'log': param.log, - 'lower': param.lower, - 'upper': param.upper, - 'default': param.default_value, - 'q': param.q, + "name": param.name, + "type": "uniform_float", + "log": param.log, + "lower": param.lower, + "upper": param.upper, + "default": param.default_value, + "q": param.q, } -def _build_normal_float(param: NormalFloatHyperparameter) -> Dict: +def _build_normal_float(param: NormalFloatHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'normal_float', - 'log': param.log, - 'mu': param.mu, - 'sigma': param.sigma, - 'default': param.default_value, - 'lower': param.lower, - 'upper': param.upper, - 'q': param.q, + "name": param.name, + "type": "normal_float", + "log": param.log, + "mu": param.mu, + "sigma": param.sigma, + "default": param.default_value, + "lower": param.lower, + "upper": param.upper, + "q": param.q, } -def _build_beta_float(param: BetaFloatHyperparameter) -> Dict: +def _build_beta_float(param: BetaFloatHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'beta_float', - 'log': param.log, - 'alpha': param.alpha, - 'beta': param.beta, - 'lower': param.lower, - 'upper': param.upper, - 'default': param.default_value, - 'q': param.q, + "name": param.name, + "type": "beta_float", + "log": param.log, + "alpha": param.alpha, + "beta": param.beta, + "lower": param.lower, + "upper": param.upper, + "default": param.default_value, + "q": param.q, } -def _build_uniform_int(param: UniformIntegerHyperparameter) -> Dict: +def _build_uniform_int(param: UniformIntegerHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'uniform_int', - 'log': param.log, - 'lower': param.lower, - 'upper': param.upper, - 'default': param.default_value, - 'q': param.q, + "name": param.name, + "type": "uniform_int", + "log": param.log, + "lower": param.lower, + "upper": param.upper, + "default": param.default_value, + "q": param.q, } -def _build_normal_int(param: NormalIntegerHyperparameter) -> Dict: +def _build_normal_int(param: NormalIntegerHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'normal_int', - 'log': param.log, - 'mu': param.mu, - 'sigma': param.sigma, - 'lower': param.lower, - 'upper': param.upper, - 'default': param.default_value, - 'q': param.q, + "name": param.name, + "type": "normal_int", + "log": param.log, + "mu": param.mu, + "sigma": param.sigma, + "lower": param.lower, + "upper": param.upper, + "default": param.default_value, + "q": param.q, } -def _build_beta_int(param: BetaIntegerHyperparameter) -> Dict: +def _build_beta_int(param: BetaIntegerHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'beta_int', - 'log': param.log, - 'alpha': param.alpha, - 'beta': param.beta, - 'lower': param.lower, - 'upper': param.upper, - 'default': param.default_value, - 'q': param.q, + "name": param.name, + "type": "beta_int", + "log": param.log, + "alpha": param.alpha, + "beta": param.beta, + "lower": param.lower, + "upper": param.upper, + "default": param.default_value, + "q": param.q, } -def _build_categorical(param: CategoricalHyperparameter) -> Dict: +def _build_categorical(param: CategoricalHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'categorical', - 'choices': param.choices, - 'default': param.default_value, - 'weights': param.weights, + "name": param.name, + "type": "categorical", + "choices": param.choices, + "default": param.default_value, + "weights": param.weights, } -def _build_ordinal(param: OrdinalHyperparameter) -> Dict: +def _build_ordinal(param: OrdinalHyperparameter) -> dict: return { - 'name': param.name, - 'type': 'ordinal', - 'sequence': param.sequence, - 'default': param.default_value + "name": param.name, + "type": "ordinal", + "sequence": param.sequence, + "default": param.default_value, } ################################################################################ # Builder for Conditions -def _build_condition(condition: AbstractCondition) -> Dict: - if isinstance(condition, AndConjunction): - return _build_and_conjunction(condition) - elif isinstance(condition, OrConjunction): - return _build_or_conjunction(condition) - elif isinstance(condition, InCondition): - return _build_in_condition(condition) - elif isinstance(condition, EqualsCondition): - return _build_equals_condition(condition) - elif isinstance(condition, NotEqualsCondition): - return _build_not_equals_condition(condition) - elif isinstance(condition, GreaterThanCondition): - return _build_greater_than_condition(condition) - elif isinstance(condition, LessThanCondition): - return _build_less_than_condition(condition) - else: - raise TypeError(condition) +def _build_condition(condition: AbstractCondition) -> dict: + methods = { + AndConjunction: _build_and_conjunction, + OrConjunction: _build_or_conjunction, + InCondition: _build_in_condition, + EqualsCondition: _build_equals_condition, + NotEqualsCondition: _build_not_equals_condition, + GreaterThanCondition: _build_greater_than_condition, + LessThanCondition: _build_less_than_condition, + } + return methods[type(condition)](condition) -def _build_and_conjunction(conjunction: AndConjunction) -> Dict: +def _build_and_conjunction(conjunction: AndConjunction) -> dict: child = conjunction.get_descendant_literal_conditions()[0].child.name - cond_list = list() + cond_list = [] for component in conjunction.components: cond_list.append(_build_condition(component)) return { - 'child': child, - 'type': 'AND', - 'conditions': cond_list, + "child": child, + "type": "AND", + "conditions": cond_list, } -def _build_or_conjunction(conjunction: OrConjunction) -> Dict: +def _build_or_conjunction(conjunction: OrConjunction) -> dict: child = conjunction.get_descendant_literal_conditions()[0].child.name - cond_list = list() + cond_list = [] for component in conjunction.components: cond_list.append(_build_condition(component)) return { - 'child': child, - 'type': 'OR', - 'conditions': cond_list, + "child": child, + "type": "OR", + "conditions": cond_list, } -def _build_in_condition(condition: InCondition) -> Dict: +def _build_in_condition(condition: InCondition) -> dict: child = condition.child.name parent = condition.parent.name values = list(condition.values) return { - 'child': child, - 'parent': parent, - 'type': 'IN', - 'values': values, + "child": child, + "parent": parent, + "type": "IN", + "values": values, } -def _build_equals_condition(condition: EqualsCondition) -> Dict: +def _build_equals_condition(condition: EqualsCondition) -> dict: child = condition.child.name parent = condition.parent.name value = condition.value return { - 'child': child, - 'parent': parent, - 'type': 'EQ', - 'value': value, + "child": child, + "parent": parent, + "type": "EQ", + "value": value, } -def _build_not_equals_condition(condition: NotEqualsCondition) -> Dict: +def _build_not_equals_condition(condition: NotEqualsCondition) -> dict: child = condition.child.name parent = condition.parent.name value = condition.value return { - 'child': child, - 'parent': parent, - 'type': 'NEQ', - 'value': value, + "child": child, + "parent": parent, + "type": "NEQ", + "value": value, } -def _build_greater_than_condition(condition: GreaterThanCondition) -> Dict: +def _build_greater_than_condition(condition: GreaterThanCondition) -> dict: child = condition.child.name parent = condition.parent.name value = condition.value return { - 'child': child, - 'parent': parent, - 'type': 'GT', - 'value': value, + "child": child, + "parent": parent, + "type": "GT", + "value": value, } -def _build_less_than_condition(condition: LessThanCondition) -> Dict: +def _build_less_than_condition(condition: LessThanCondition) -> dict: child = condition.child.name parent = condition.parent.name value = condition.value return { - 'child': child, - 'parent': parent, - 'type': 'LT', - 'value': value, + "child": child, + "parent": parent, + "type": "LT", + "value": value, } ################################################################################ # Builder for forbidden -def _build_forbidden(clause) -> Dict: - if isinstance(clause, ForbiddenEqualsClause): - return _build_forbidden_equals_clause(clause) - elif isinstance(clause, ForbiddenInClause): - return _build_forbidden_in_clause(clause) - elif isinstance(clause, ForbiddenAndConjunction): - return _build_forbidden_and_conjunction(clause) - elif isinstance(clause, ForbiddenRelation): - return _build_forbidden_relation(clause) - else: - raise TypeError(clause) +def _build_forbidden(clause: AbstractForbiddenComponent) -> dict: + methods = { + ForbiddenEqualsClause: _build_forbidden_equals_clause, + ForbiddenInClause: _build_forbidden_in_clause, + ForbiddenAndConjunction: _build_forbidden_and_conjunction, + ForbiddenEqualsRelation: _build_forbidden_relation, + ForbiddenLessThanRelation: _build_forbidden_relation, + ForbiddenGreaterThanRelation: _build_forbidden_relation, + } + return methods[type(clause)](clause) -def _build_forbidden_equals_clause(clause: ForbiddenEqualsClause) -> Dict: +def _build_forbidden_equals_clause(clause: ForbiddenEqualsClause) -> dict: return { - 'name': clause.hyperparameter.name, - 'type': 'EQUALS', - 'value': clause.value, + "name": clause.hyperparameter.name, + "type": "EQUALS", + "value": clause.value, } -def _build_forbidden_in_clause(clause: ForbiddenInClause) -> Dict: +def _build_forbidden_in_clause(clause: ForbiddenInClause) -> dict: return { - 'name': clause.hyperparameter.name, - 'type': 'IN', + "name": clause.hyperparameter.name, + "type": "IN", # The values are a set, but a set cannot be serialized to json - 'values': list(clause.values), + "values": list(clause.values), } -def _build_forbidden_and_conjunction(clause: ForbiddenAndConjunction) -> Dict: +def _build_forbidden_and_conjunction(clause: ForbiddenAndConjunction) -> dict: return { - 'name': clause.get_descendant_literal_clauses()[0].hyperparameter.name, - 'type': 'AND', - 'clauses': [ - _build_forbidden(component) for component in clause.components - ], + "name": clause.get_descendant_literal_clauses()[0].hyperparameter.name, + "type": "AND", + "clauses": [_build_forbidden(component) for component in clause.components], } -def _build_forbidden_relation(clause: ForbiddenRelation) -> Dict: +def _build_forbidden_relation(clause: ForbiddenRelation) -> dict: if isinstance(clause, ForbiddenLessThanRelation): - lambda_ = 'LESS' + lambda_ = "LESS" elif isinstance(clause, ForbiddenEqualsRelation): - lambda_ = 'EQUALS' + lambda_ = "EQUALS" elif isinstance(clause, ForbiddenGreaterThanRelation): - lambda_ = 'GREATER' + lambda_ = "GREATER" else: raise ValueError("Unknown relation '%s'" % type(clause)) return { - 'left': clause.left.name, - 'right': clause.right.name, - 'type': 'RELATION', - 'lambda': lambda_ + "left": clause.left.name, + "right": clause.right.name, + "type": "RELATION", + "lambda": lambda_, } ################################################################################ -def write(configuration_space, indent=2): +def write(configuration_space: ConfigurationSpace, indent: int = 2) -> str: """ Create a string representation of a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in json format. @@ -357,22 +346,20 @@ def write(configuration_space, indent=2): which will be written to file """ if not isinstance(configuration_space, ConfigurationSpace): - raise TypeError("pcs_parser.write expects an instance of %s, " - "you provided '%s'" % (ConfigurationSpace, - type(configuration_space))) + raise TypeError( + "pcs_parser.write expects an instance of {}, " + "you provided '{}'".format(ConfigurationSpace, type(configuration_space)), + ) hyperparameters = [] conditions = [] forbiddens = [] - for hyperparameter in configuration_space.get_hyperparameters(): - + for hyperparameter in configuration_space.values(): if isinstance(hyperparameter, Constant): hyperparameters.append(_build_constant(hyperparameter)) elif isinstance(hyperparameter, UnParametrizedHyperparameter): - hyperparameters.append( - _build_unparametrized_hyperparameter(hyperparameter) - ) + hyperparameters.append(_build_unparametrized_hyperparameter(hyperparameter)) elif isinstance(hyperparameter, BetaFloatHyperparameter): hyperparameters.append(_build_beta_float(hyperparameter)) elif isinstance(hyperparameter, UniformFloatHyperparameter): @@ -391,9 +378,10 @@ def write(configuration_space, indent=2): hyperparameters.append(_build_ordinal(hyperparameter)) else: raise TypeError( - "Unknown type: %s (%s)" % ( - type(hyperparameter), hyperparameter, - ) + "Unknown type: {} ({})".format( + type(hyperparameter), + hyperparameter, + ), ) for condition in configuration_space.get_conditions(): @@ -402,20 +390,20 @@ def write(configuration_space, indent=2): for forbidden_clause in configuration_space.get_forbiddens(): forbiddens.append(_build_forbidden(forbidden_clause)) - rval = {} + rval: dict = {} if configuration_space.name is not None: - rval['name'] = configuration_space.name - rval['hyperparameters'] = hyperparameters - rval['conditions'] = conditions - rval['forbiddens'] = forbiddens - rval['python_module_version'] = __version__ - rval['json_format_version'] = JSON_FORMAT_VERSION + rval["name"] = configuration_space.name + rval["hyperparameters"] = hyperparameters + rval["conditions"] = conditions + rval["forbiddens"] = forbiddens + rval["python_module_version"] = __version__ + rval["json_format_version"] = JSON_FORMAT_VERSION return json.dumps(rval, indent=indent) ################################################################################ -def read(jason_string): +def read(jason_string: str) -> ConfigurationSpace: """ Create a configuration space definition from a json string. @@ -446,274 +434,276 @@ def read(jason_string): The deserialized ConfigurationSpace object """ jason = json.loads(jason_string) - if 'name' in jason: - configuration_space = ConfigurationSpace(name=jason['name']) + if "name" in jason: + configuration_space = ConfigurationSpace(name=jason["name"]) else: configuration_space = ConfigurationSpace() - for hyperparameter in jason['hyperparameters']: - configuration_space.add_hyperparameter(_construct_hyperparameter( - hyperparameter, - )) + for hyperparameter in jason["hyperparameters"]: + configuration_space.add_hyperparameter( + _construct_hyperparameter( + hyperparameter, + ), + ) - for condition in jason['conditions']: - configuration_space.add_condition(_construct_condition( - condition, configuration_space, - )) + for condition in jason["conditions"]: + configuration_space.add_condition( + _construct_condition( + condition, + configuration_space, + ), + ) - for forbidden in jason['forbiddens']: - configuration_space.add_forbidden_clause(_construct_forbidden( - forbidden, configuration_space, - )) + for forbidden in jason["forbiddens"]: + configuration_space.add_forbidden_clause( + _construct_forbidden( + forbidden, + configuration_space, + ), + ) return configuration_space -def _construct_hyperparameter(hyperparameter: Dict) -> Hyperparameter: - hp_type = hyperparameter['type'] - name = hyperparameter['name'] - if hp_type == 'constant': +def _construct_hyperparameter(hyperparameter: dict) -> Hyperparameter: # noqa: PLR0911 + hp_type = hyperparameter["type"] + name = hyperparameter["name"] + if hp_type == "constant": return Constant( name=name, - value=hyperparameter['value'], + value=hyperparameter["value"], ) - elif hp_type == 'unparametrized': + + if hp_type == "unparametrized": return UnParametrizedHyperparameter( name=name, - value=hyperparameter['value'], + value=hyperparameter["value"], ) - elif hp_type == 'uniform_float': + + if hp_type == "uniform_float": return UniformFloatHyperparameter( name=name, - log=hyperparameter['log'], - lower=hyperparameter['lower'], - upper=hyperparameter['upper'], - default_value=hyperparameter['default'], - q=hyperparameter['q'], + log=hyperparameter["log"], + lower=hyperparameter["lower"], + upper=hyperparameter["upper"], + default_value=hyperparameter["default"], + q=hyperparameter["q"], ) - elif hp_type == 'normal_float': + + if hp_type == "normal_float": return NormalFloatHyperparameter( name=name, - log=hyperparameter['log'], - mu=hyperparameter['mu'], - sigma=hyperparameter['sigma'], - lower=hyperparameter['lower'], - upper=hyperparameter['upper'], - default_value=hyperparameter['default'], - q=hyperparameter['q'], + log=hyperparameter["log"], + mu=hyperparameter["mu"], + sigma=hyperparameter["sigma"], + lower=hyperparameter["lower"], + upper=hyperparameter["upper"], + default_value=hyperparameter["default"], + q=hyperparameter["q"], ) - elif hp_type == 'beta_float': + + if hp_type == "beta_float": return BetaFloatHyperparameter( name=name, - alpha=hyperparameter['alpha'], - beta=hyperparameter['beta'], - lower=hyperparameter['lower'], - upper=hyperparameter['upper'], - log=hyperparameter['log'], - q=hyperparameter['q'], - default_value=hyperparameter['default'], + alpha=hyperparameter["alpha"], + beta=hyperparameter["beta"], + lower=hyperparameter["lower"], + upper=hyperparameter["upper"], + log=hyperparameter["log"], + q=hyperparameter["q"], + default_value=hyperparameter["default"], ) - elif hp_type == 'uniform_int': + + if hp_type == "uniform_int": return UniformIntegerHyperparameter( name=name, - log=hyperparameter['log'], - lower=hyperparameter['lower'], - upper=hyperparameter['upper'], - default_value=hyperparameter['default'], - q=hyperparameter['q'], + log=hyperparameter["log"], + lower=hyperparameter["lower"], + upper=hyperparameter["upper"], + default_value=hyperparameter["default"], + q=hyperparameter["q"], ) - elif hp_type == 'normal_int': + + if hp_type == "normal_int": return NormalIntegerHyperparameter( name=name, - mu=hyperparameter['mu'], - sigma=hyperparameter['sigma'], - log=hyperparameter['log'], - lower=hyperparameter['lower'], - upper=hyperparameter['upper'], - default_value=hyperparameter['default'], - q=hyperparameter['q'], + mu=hyperparameter["mu"], + sigma=hyperparameter["sigma"], + log=hyperparameter["log"], + lower=hyperparameter["lower"], + upper=hyperparameter["upper"], + default_value=hyperparameter["default"], + q=hyperparameter["q"], ) - elif hp_type == 'beta_int': + + if hp_type == "beta_int": return BetaIntegerHyperparameter( name=name, - alpha=hyperparameter['alpha'], - beta=hyperparameter['beta'], - lower=hyperparameter['lower'], - upper=hyperparameter['upper'], - log=hyperparameter['log'], - q=hyperparameter['q'], - default_value=hyperparameter['default'], + alpha=hyperparameter["alpha"], + beta=hyperparameter["beta"], + lower=hyperparameter["lower"], + upper=hyperparameter["upper"], + log=hyperparameter["log"], + q=hyperparameter["q"], + default_value=hyperparameter["default"], ) - elif hp_type == 'categorical': + + if hp_type == "categorical": return CategoricalHyperparameter( name=name, - choices=hyperparameter['choices'], - default_value=hyperparameter['default'], - weights=hyperparameter.get('weights'), + choices=hyperparameter["choices"], + default_value=hyperparameter["default"], + weights=hyperparameter.get("weights"), ) - elif hp_type == 'ordinal': + + if hp_type == "ordinal": return OrdinalHyperparameter( name=name, - sequence=hyperparameter['sequence'], - default_value=hyperparameter['default'], + sequence=hyperparameter["sequence"], + default_value=hyperparameter["default"], ) - else: - raise ValueError(hp_type) + + raise ValueError(hp_type) def _construct_condition( - condition: Dict, - cs: ConfigurationSpace, + condition: dict, + cs: ConfigurationSpace, ) -> AbstractCondition: - condition_type = condition['type'] - if condition_type == 'AND': - return _construct_and_condition(condition, cs) - elif condition_type == 'OR': - return _construct_or_condition(condition, cs) - elif condition_type == 'IN': - return _construct_in_condition(condition, cs) - elif condition_type == 'EQ': - return _construct_eq_condition(condition, cs) - elif condition_type == 'NEQ': - return _construct_neq_condition(condition, cs) - elif condition_type == 'GT': - return _construct_gt_condition(condition, cs) - elif condition_type == 'LT': - return _construct_lt_condition(condition, cs) - else: - raise ValueError(condition_type) + condition_type = condition["type"] + methods = { + "AND": _construct_and_condition, + "OR": _construct_or_condition, + "IN": _construct_in_condition, + "EQ": _construct_eq_condition, + "NEQ": _construct_neq_condition, + "GT": _construct_gt_condition, + "LT": _construct_lt_condition, + } + return methods[condition_type](condition, cs) def _construct_and_condition( - condition: Dict, - cs: ConfigurationSpace, + condition: dict, + cs: ConfigurationSpace, ) -> AndConjunction: - conditions = [ - _construct_condition(cond, cs) for cond in condition['conditions'] - ] + conditions = [_construct_condition(cond, cs) for cond in condition["conditions"]] return AndConjunction(*conditions) def _construct_or_condition( - condition: Dict, - cs: ConfigurationSpace, + condition: dict, + cs: ConfigurationSpace, ) -> OrConjunction: - conditions = [_construct_condition(cond, cs) for cond in condition['conditions']] + conditions = [_construct_condition(cond, cs) for cond in condition["conditions"]] return OrConjunction(*conditions) def _construct_in_condition( - condition: Dict, - cs: ConfigurationSpace, + condition: dict, + cs: ConfigurationSpace, ) -> InCondition: return InCondition( - child=cs.get_hyperparameter(condition['child']), - parent=cs.get_hyperparameter(condition['parent']), - values=condition['values'], + child=cs[condition["child"]], + parent=cs[condition["parent"]], + values=condition["values"], ) def _construct_eq_condition( - condition: Dict, - cs: ConfigurationSpace, + condition: dict, + cs: ConfigurationSpace, ) -> EqualsCondition: return EqualsCondition( - child=cs.get_hyperparameter(condition['child']), - parent=cs.get_hyperparameter(condition['parent']), - value=condition['value'], + child=cs[condition["child"]], + parent=cs[condition["parent"]], + value=condition["value"], ) def _construct_neq_condition( - condition: Dict, - cs: ConfigurationSpace, + condition: dict, + cs: ConfigurationSpace, ) -> NotEqualsCondition: return NotEqualsCondition( - child=cs.get_hyperparameter(condition['child']), - parent=cs.get_hyperparameter(condition['parent']), - value=condition['value'], + child=cs[condition["child"]], + parent=cs[condition["parent"]], + value=condition["value"], ) def _construct_gt_condition( - condition: Dict, - cs: ConfigurationSpace, + condition: dict, + cs: ConfigurationSpace, ) -> GreaterThanCondition: return GreaterThanCondition( - child=cs.get_hyperparameter(condition['child']), - parent=cs.get_hyperparameter(condition['parent']), - value=condition['value'], + child=cs[condition["child"]], + parent=cs[condition["parent"]], + value=condition["value"], ) def _construct_lt_condition( - condition: Dict, - cs: ConfigurationSpace, + condition: dict, + cs: ConfigurationSpace, ) -> LessThanCondition: return LessThanCondition( - child=cs.get_hyperparameter(condition['child']), - parent=cs.get_hyperparameter(condition['parent']), - value=condition['value'], + child=cs[condition["child"]], + parent=cs[condition["parent"]], + value=condition["value"], ) def _construct_forbidden( - clause: Dict, - cs: ConfigurationSpace, + clause: dict, + cs: ConfigurationSpace, ) -> AbstractForbiddenComponent: - forbidden_type = clause['type'] - if forbidden_type == 'EQUALS': - return _construct_forbidden_equals(clause, cs) - elif forbidden_type == 'IN': - return _construct_forbidden_in(clause, cs) - elif forbidden_type == 'AND': - return _construct_forbidden_and(clause, cs) - elif forbidden_type == 'RELATION': - return _construct_forbidden_equals(clause, cs) - else: - return ValueError(forbidden_type) + forbidden_type = clause["type"] + methods = { + "EQUALS": _construct_forbidden_equals, + "IN": _construct_forbidden_in, + "AND": _construct_forbidden_and, + "RELATION": _construct_forbidden_equals, + } + return methods[forbidden_type](clause, cs) def _construct_forbidden_equals( - clause: Dict, - cs: ConfigurationSpace, + clause: dict, + cs: ConfigurationSpace, ) -> ForbiddenEqualsClause: - return ForbiddenEqualsClause( - hyperparameter=cs.get_hyperparameter(clause['name']), - value=clause['value'] - ) + return ForbiddenEqualsClause(hyperparameter=cs[clause["name"]], value=clause["value"]) def _construct_forbidden_in( - clause: Dict, - cs: ConfigurationSpace, + clause: dict, + cs: ConfigurationSpace, ) -> ForbiddenEqualsClause: - return ForbiddenInClause( - hyperparameter=cs.get_hyperparameter(clause['name']), - values=clause['values'] - ) + return ForbiddenInClause(hyperparameter=cs[clause["name"]], values=clause["values"]) def _construct_forbidden_and( - clause: Dict, - cs: ConfigurationSpace, + clause: dict, + cs: ConfigurationSpace, ) -> ForbiddenAndConjunction: - clauses = [_construct_forbidden(cl, cs) for cl in clause['clauses']] + clauses = [_construct_forbidden(cl, cs) for cl in clause["clauses"]] return ForbiddenAndConjunction(*clauses) -def _construct_forbidden_relation( - clause: Dict, - cs: ConfigurationSpace, +def _construct_forbidden_relation( # pyright: ignore + clause: dict, + cs: ConfigurationSpace, ) -> ForbiddenRelation: - left = cs.get_hyperparameter(clause['left']) - right = cs.get_hyperparameter(clause['right']) + left = cs[clause["left"]] + right = cs[clause["right"]] - if clause['lambda'] == "LESS": + if clause["lambda"] == "LESS": return ForbiddenLessThanRelation(left, right) - elif clause['lambda'] == "EQUALS": + + if clause["lambda"] == "EQUALS": return ForbiddenEqualsRelation(left, right) - elif clause['lambda'] == "GREATER": + + if clause["lambda"] == "GREATER": return ForbiddenGreaterThanRelation(left, right) - else: - raise ValueError("Unknown relation '%s'" % clause['lambda']) + + raise ValueError("Unknown relation '%s'" % clause["lambda"]) diff --git a/ConfigSpace/read_and_write/pcs.py b/ConfigSpace/read_and_write/pcs.py index 3abfa2f9..45c38e49 100644 --- a/ConfigSpace/read_and_write/pcs.py +++ b/ConfigSpace/read_and_write/pcs.py @@ -1,61 +1,78 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ The old PCS format is part of the `Algorithm Configuration Library `_. A detailed explanation of the **old** PCS format can be found `here. `_ """ +from __future__ import annotations __authors__ = ["Katharina Eggensperger", "Matthias Feurer"] __contact__ = "automl.org" +import sys from collections import OrderedDict -from itertools import product from io import StringIO -import sys +from itertools import product +from typing import Iterable import pyparsing -from ConfigSpace.configuration_space import ConfigurationSpace -from ConfigSpace.hyperparameters import ( - CategoricalHyperparameter, - UniformIntegerHyperparameter, - UniformFloatHyperparameter, - NumericalHyperparameter, - Constant, - IntegerHyperparameter, - NormalIntegerHyperparameter, - NormalFloatHyperparameter, -) from ConfigSpace.conditions import ( + AndConjunction, + ConditionComponent, EqualsCondition, - NotEqualsCondition, InCondition, - AndConjunction, + NotEqualsCondition, OrConjunction, - ConditionComponent, ) +from ConfigSpace.configuration_space import ConfigurationSpace from ConfigSpace.forbidden import ( - ForbiddenEqualsClause, + AbstractForbiddenComponent, ForbiddenAndConjunction, + ForbiddenEqualsClause, ForbiddenInClause, - AbstractForbiddenComponent, MultipleValueForbiddenClause, ) - +from ConfigSpace.hyperparameters import ( + CategoricalHyperparameter, + Constant, + IntegerHyperparameter, + NormalFloatHyperparameter, + NormalIntegerHyperparameter, + NumericalHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, +) # Build pyparsing expressions for params pp_param_name = pyparsing.Word( - pyparsing.alphanums + "_" + "-" + "@" + "." + ":" + ";" + "\\" + "/" + "?" + "!" - + "$" + "%" + "&" + "*" + "+" + "<" + ">") + pyparsing.alphanums + + "_" + + "-" + + "@" + + "." + + ":" + + ";" + + "\\" + + "/" + + "?" + + "!" + + "$" + + "%" + + "&" + + "*" + + "+" + + "<" + + ">", +) pp_digits = "0123456789" -pp_plusorminus = pyparsing.Literal('+') | pyparsing.Literal('-') +pp_plusorminus = pyparsing.Literal("+") | pyparsing.Literal("-") pp_int = pyparsing.Combine(pyparsing.Optional(pp_plusorminus) + pyparsing.Word(pp_digits)) pp_float = pyparsing.Combine( - pyparsing.Optional(pp_plusorminus) + pyparsing.Optional(pp_int) + "." + pp_int + pyparsing.Optional(pp_plusorminus) + pyparsing.Optional(pp_int) + "." + pp_int, ) -pp_eorE = pyparsing.Literal('e') | pyparsing.Literal('E') +pp_eorE = pyparsing.Literal("e") | pyparsing.Literal("E") pp_floatorint = pp_float | pp_int pp_e_notation = pyparsing.Combine(pp_floatorint + pp_eorE + pp_int) pp_number = pp_e_notation | pp_float | pp_int @@ -63,32 +80,51 @@ pp_il = pyparsing.Word("il") pp_choices = pp_param_name + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name)) -pp_cont_param = pp_param_name + "[" + pp_number + "," + pp_number + "]" + \ - "[" + pp_number + "]" + pyparsing.Optional(pp_il) +pp_cont_param = ( + pp_param_name + + "[" + + pp_number + + "," + + pp_number + + "]" + + "[" + + pp_number + + "]" + + pyparsing.Optional(pp_il) +) pp_cat_param = pp_param_name + "{" + pp_choices + "}" + "[" + pp_param_name + "]" pp_condition = pp_param_name + "|" + pp_param_name + "in" + "{" + pp_choices + "}" -pp_forbidden_clause = "{" + pp_param_name + "=" + pp_numberorname + \ - pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name + "=" + pp_numberorname)) + "}" +pp_forbidden_clause = ( + "{" + + pp_param_name + + "=" + + pp_numberorname + + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name + "=" + pp_numberorname)) + + "}" +) -def build_categorical(param): +def build_categorical(param: CategoricalHyperparameter) -> str: if param.weights is not None: - raise ValueError('The pcs format does not support categorical hyperparameters with ' - 'assigned weights (for hyperparameter %s)' % param.name) + raise ValueError( + "The pcs format does not support categorical hyperparameters with " + "assigned weights (for hyperparameter %s)" % param.name, + ) cat_template = "%s {%s} [%s]" - return cat_template % (param.name, - ", ".join([str(value) for value in param.choices]), - str(param.default_value)) + return cat_template % ( + param.name, + ", ".join([str(value) for value in param.choices]), + str(param.default_value), + ) -def build_constant(param): +def build_constant(param: Constant) -> str: constant_template = "%s {%s} [%s]" return constant_template % (param.name, param.value, param.value) -def build_continuous(param): - if type(param) in (NormalIntegerHyperparameter, - NormalFloatHyperparameter): +def build_continuous(param: NormalIntegerHyperparameter | NormalFloatHyperparameter) -> str: + if type(param) in (NormalIntegerHyperparameter, NormalFloatHyperparameter): param = param.to_uniform() float_template = "%s%s [%s, %s] [%s]" @@ -97,62 +133,65 @@ def build_continuous(param): float_template += "l" int_template += "l" - if param.q is not None: - q_prefix = "Q%d_" % (int(param.q),) - else: - q_prefix = "" + q_prefix = "Q%d_" % (int(param.q),) if param.q is not None else "" default_value = param.default_value if isinstance(param, IntegerHyperparameter): default_value = int(default_value) - return int_template % (q_prefix, param.name, param.lower, - param.upper, default_value) - else: - return float_template % (q_prefix, param.name, str(param.lower), - str(param.upper), str(default_value)) + return int_template % (q_prefix, param.name, param.lower, param.upper, default_value) + + return float_template % ( + q_prefix, + param.name, + str(param.lower), + str(param.upper), + str(default_value), + ) -def build_condition(condition): +def build_condition(condition: ConditionComponent) -> str: if not isinstance(condition, ConditionComponent): - raise TypeError("build_condition must be called with an instance of " - "'%s', got '%s'" % - (ConditionComponent, type(condition))) + raise TypeError( + "build_condition must be called with an instance of " + "'{}', got '{}'".format(ConditionComponent, type(condition)), + ) # Check if SMAC can handle the condition if isinstance(condition, OrConjunction): - raise NotImplementedError("SMAC cannot handle OR conditions: %s" % - (condition)) + raise NotImplementedError("SMAC cannot handle OR conditions: %s" % (condition)) if isinstance(condition, NotEqualsCondition): - raise NotImplementedError("SMAC cannot handle != conditions: %s" % - (condition)) + raise NotImplementedError("SMAC cannot handle != conditions: %s" % (condition)) # Now handle the conditions SMAC can handle condition_template = "%s | %s in {%s}" if isinstance(condition, AndConjunction): - return '\n'.join([ - build_condition(cond) for cond in condition.components - ]) - elif isinstance(condition, InCondition): - return condition_template % (condition.child.name, - condition.parent.name, - ", ".join(condition.values)) - elif isinstance(condition, EqualsCondition): - return condition_template % (condition.child.name, - condition.parent.name, - condition.value) - else: - raise NotImplementedError(condition) - - -def build_forbidden(clause): + return "\n".join([build_condition(cond) for cond in condition.components]) + + if isinstance(condition, InCondition): + return condition_template % ( + condition.child.name, + condition.parent.name, + ", ".join(condition.values), + ) + + if isinstance(condition, EqualsCondition): + return condition_template % (condition.child.name, condition.parent.name, condition.value) + + raise NotImplementedError(condition) + + +def build_forbidden(clause: AbstractForbiddenComponent) -> str: if not isinstance(clause, AbstractForbiddenComponent): - raise TypeError("build_forbidden must be called with an instance of " - "'%s', got '%s'" % - (AbstractForbiddenComponent, type(clause))) + raise TypeError( + "build_forbidden must be called with an instance of " + "'{}', got '{}'".format(AbstractForbiddenComponent, type(clause)), + ) if not isinstance(clause, (ForbiddenEqualsClause, ForbiddenAndConjunction)): - raise NotImplementedError("SMAC cannot handle '%s' of type %s" % - str(clause), (type(clause))) + raise NotImplementedError( + "SMAC cannot handle '{}' of type {}".format(*str(clause)), + (type(clause)), + ) retval = StringIO() retval.write("{") @@ -162,13 +201,13 @@ def build_forbidden(clause): for dlc in dlcs: if retval.tell() > 1: retval.write(", ") - retval.write("%s=%s" % (dlc.hyperparameter.name, dlc.value)) + retval.write(f"{dlc.hyperparameter.name}={dlc.value}") retval.write("}") retval.seek(0) return retval.getvalue() -def read(pcs_string, debug=False): +def read(pcs_string: Iterable[str]) -> ConfigurationSpace: """ Read in a :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` definition from a pcs file. @@ -188,17 +227,17 @@ def read(pcs_string, debug=False): Parameters ---------- - pcs_string : str - ConfigSpace definition in pcs format - - debug : bool = False - Provides debug information. Defaults to False. + pcs_string : Iterable[str] + ConfigSpace definition in pcs format as an iterable of strings Returns ------- :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` The deserialized ConfigurationSpace object """ + if isinstance(pcs_string, str): + pcs_string = pcs_string.split("\n") + configuration_space = ConfigurationSpace() conditions = [] forbidden = [] @@ -226,8 +265,9 @@ def read(pcs_string, debug=False): try: c = pp_condition.parseString(line) conditions.append(c) - except pyparsing.ParseException: - raise NotImplementedError("Could not parse condition: %s" % line) + except pyparsing.ParseException as e: + raise NotImplementedError(f"Could not parse condition: {line}") from e + continue if "}" not in line and "]" not in line: continue @@ -240,9 +280,11 @@ def read(pcs_string, debug=False): ct += 1 param = None - create = {"int": UniformIntegerHyperparameter, - "float": UniformFloatHyperparameter, - "categorical": CategoricalHyperparameter} + create = { + "int": UniformIntegerHyperparameter, + "float": UniformFloatHyperparameter, + "categorical": CategoricalHyperparameter, + } try: param_list = pp_cont_param.parseString(line) @@ -251,13 +293,19 @@ def read(pcs_string, debug=False): il = il[0] param_list = param_list[:9] name = param_list[0] - lower = float(param_list[2]) - upper = float(param_list[4]) + lower = float(param_list[2]) # type: ignore + upper = float(param_list[4]) # type: ignore paramtype = "int" if "i" in il else "float" - log = True if "l" in il else False - default_value = float(param_list[7]) - param = create[paramtype](name=name, lower=lower, upper=upper, - q=None, log=log, default_value=default_value) + log = "l" in il + default_value = float(param_list[7]) # type: ignore + param = create[paramtype]( + name=name, + lower=lower, + upper=upper, + q=None, + log=log, + default_value=default_value, + ) cont_ct += 1 except pyparsing.ParseException: pass @@ -265,10 +313,9 @@ def read(pcs_string, debug=False): try: param_list = pp_cat_param.parseString(line) name = param_list[0] - choices = [c for c in param_list[2:-4:2]] + choices = list(param_list[2:-4:2]) default_value = param_list[-2] - param = create["categorical"](name=name, choices=choices, - default_value=default_value) + param = create["categorical"](name=name, choices=choices, default_value=default_value) cat_ct += 1 except pyparsing.ParseException: pass @@ -283,41 +330,40 @@ def read(pcs_string, debug=False): # TODO Add a try/catch here! # noinspection PyUnusedLocal param_list = pp_forbidden_clause.parseString(clause) - tmp_list = [] + tmp_list: list = [] clause_list = [] for value in param_list[1:]: if len(tmp_list) < 3: tmp_list.append(value) else: # So far, only equals is supported by SMAC - if tmp_list[1] == '=': + if tmp_list[1] == "=": # TODO maybe add a check if the hyperparameter is # actually in the configuration space - clause_list.append(ForbiddenEqualsClause( - configuration_space.get_hyperparameter(tmp_list[0]), - tmp_list[2])) + clause_list.append( + ForbiddenEqualsClause(configuration_space[tmp_list[0]], tmp_list[2]), + ) else: raise NotImplementedError() tmp_list = [] - configuration_space.add_forbidden_clause(ForbiddenAndConjunction( - *clause_list)) + configuration_space.add_forbidden_clause(ForbiddenAndConjunction(*clause_list)) # Now handle conditions # If there are two conditions for one child, these two conditions are an # AND-conjunction of conditions, thus we have to connect them - conditions_per_child = OrderedDict() + conditions_per_child: dict = OrderedDict() for condition in conditions: child_name = condition[0] if child_name not in conditions_per_child: - conditions_per_child[child_name] = list() + conditions_per_child[child_name] = [] conditions_per_child[child_name].append(condition) for child_name in conditions_per_child: condition_objects = [] for condition in conditions_per_child[child_name]: - child = configuration_space.get_hyperparameter(child_name) + child = configuration_space[child_name] parent_name = condition[2] - parent = configuration_space.get_hyperparameter(parent_name) + parent = configuration_space[parent_name] restrictions = condition[5:-1:2] # TODO: cast the type of the restriction! @@ -339,7 +385,7 @@ def read(pcs_string, debug=False): return configuration_space -def write(configuration_space): +def write(configuration_space: ConfigurationSpace) -> str: """ Create a string representation of a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in pcs format. @@ -367,19 +413,22 @@ def write(configuration_space): """ if not isinstance(configuration_space, ConfigurationSpace): - raise TypeError("pcs_parser.write expects an instance of %s, " - "you provided '%s'" % (ConfigurationSpace, type(configuration_space))) + raise TypeError( + f"pcs_parser.write expects an instance of {ConfigurationSpace}, " + f"you provided '{type(configuration_space)}'", + ) param_lines = StringIO() condition_lines = StringIO() forbidden_lines = [] - for hyperparameter in configuration_space.get_hyperparameters(): - # Check if the hyperparameter names are valid SMAC names! + for hyperparameter in configuration_space.values(): + # Check if the hyperparameter names are valid ConfigSpace names! try: pp_param_name.parseString(hyperparameter.name) - except pyparsing.ParseException: + except pyparsing.ParseException as e: raise ValueError( - "Illegal hyperparameter name for SMAC: %s" % hyperparameter.name) + f"Illegal hyperparameter name for ConfigSpace: {hyperparameter.name}", + ) from e # First build params if param_lines.tell() > 0: @@ -391,8 +440,7 @@ def write(configuration_space): elif isinstance(hyperparameter, Constant): param_lines.write(build_constant(hyperparameter)) else: - raise TypeError("Unknown type: %s (%s)" % ( - type(hyperparameter), hyperparameter)) + raise TypeError(f"Unknown type: {type(hyperparameter)} ({hyperparameter})") for condition in configuration_space.get_conditions(): if condition_lines.tell() > 0: @@ -408,18 +456,17 @@ def write(configuration_space): for dlc in dlcs: if isinstance(dlc, MultipleValueForbiddenClause): if not isinstance(dlc, ForbiddenInClause): - raise ValueError("SMAC cannot handle this forbidden " - "clause: %s" % dlc) + raise ValueError("SMAC cannot handle this forbidden " "clause: %s" % dlc) in_statements.append( - [ForbiddenEqualsClause(dlc.hyperparameter, value) - for value in dlc.values]) + [ForbiddenEqualsClause(dlc.hyperparameter, value) for value in dlc.values], + ) else: other_statements.append(dlc) # Second, create the product of all elements in the IN statements, # create a ForbiddenAnd and add all ForbiddenEquals if len(in_statements) > 0: - for i, p in enumerate(product(*in_statements)): + for p in product(*in_statements): all_forbidden_clauses = list(p) + other_statements f = ForbiddenAndConjunction(*all_forbidden_clauses) forbidden_lines.append(build_forbidden(f)) @@ -446,9 +493,10 @@ def write(configuration_space): if __name__ == "__main__": - fh = open(sys.argv[1]) - orig_pcs = fh.readlines() - sp = read(orig_pcs, debug=True) + with open(sys.argv[1]) as fh: + orig_pcs = fh.readlines() + + sp = read(orig_pcs) created_pcs = write(sp).split("\n") print("============== Writing Results") print("#Lines: ", len(created_pcs)) diff --git a/ConfigSpace/read_and_write/pcs_new.py b/ConfigSpace/read_and_write/pcs_new.py index 80d671ab..ee54eb3a 100644 --- a/ConfigSpace/read_and_write/pcs_new.py +++ b/ConfigSpace/read_and_write/pcs_new.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ PCS (parameter configuration space) is a simple, human-readable file format for the description of an algorithm's configurable parameters, their possible values, as well @@ -19,65 +18,90 @@ AClib 2.0, as well as SMAC (v2 and v3). To write or to read the **old** version of pcs, please use the :class:`~ConfigSpace.read_and_write.pcs` module. """ +from __future__ import annotations -__authors__ = ["Katharina Eggensperger", "Matthias Feurer", "Christina Hernández Wunsch"] +__authors__ = [ + "Katharina Eggensperger", + "Matthias Feurer", + "Christina Hernández Wunsch", +] __contact__ = "automl.org" from collections import OrderedDict -from itertools import product from io import StringIO +from itertools import product +from typing import Iterable import pyparsing -from ConfigSpace.configuration_space import ConfigurationSpace -from ConfigSpace.hyperparameters import ( - CategoricalHyperparameter, - UniformIntegerHyperparameter, - UniformFloatHyperparameter, - NumericalHyperparameter, - IntegerHyperparameter, - FloatHyperparameter, - NormalIntegerHyperparameter, - NormalFloatHyperparameter, - OrdinalHyperparameter, - Constant, -) from ConfigSpace.conditions import ( - EqualsCondition, - NotEqualsCondition, - InCondition, + AbstractConjunction, AndConjunction, - OrConjunction, ConditionComponent, + EqualsCondition, GreaterThanCondition, + InCondition, LessThanCondition, + NotEqualsCondition, + OrConjunction, ) +from ConfigSpace.configuration_space import ConfigurationSpace from ConfigSpace.forbidden import ( - ForbiddenEqualsClause, + AbstractForbiddenComponent, ForbiddenAndConjunction, + ForbiddenEqualsClause, ForbiddenInClause, - AbstractForbiddenComponent, - MultipleValueForbiddenClause, ForbiddenRelation, + MultipleValueForbiddenClause, +) +from ConfigSpace.hyperparameters import ( + CategoricalHyperparameter, + Constant, + FloatHyperparameter, + IntegerHyperparameter, + NormalFloatHyperparameter, + NormalIntegerHyperparameter, + NumericalHyperparameter, + OrdinalHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, ) # Build pyparsing expressions for params pp_param_name = pyparsing.Word( - pyparsing.alphanums + "_" + "-" + "@" + "." + ":" + ";" + "\\" + "/" + "?" + "!" - + "$" + "%" + "&" + "*" + "+" + "<" + ">" + pyparsing.alphanums + + "_" + + "-" + + "@" + + "." + + ":" + + ";" + + "\\" + + "/" + + "?" + + "!" + + "$" + + "%" + + "&" + + "*" + + "+" + + "<" + + ">", ) pp_param_operation = pyparsing.Word("in" + "!=" + "==" + ">" + "<") pp_digits = "0123456789" pp_param_val = pp_param_name + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name)) -pp_plusorminus = pyparsing.Literal('+') | pyparsing.Literal('-') +pp_plusorminus = pyparsing.Literal("+") | pyparsing.Literal("-") pp_int = pyparsing.Combine(pyparsing.Optional(pp_plusorminus) + pyparsing.Word(pp_digits)) pp_float = pyparsing.Combine( - pyparsing.Optional(pp_plusorminus) + pyparsing.Optional(pp_int) + "." + pp_int + pyparsing.Optional(pp_plusorminus) + pyparsing.Optional(pp_int) + "." + pp_int, ) -pp_eorE = pyparsing.Literal('e') | pyparsing.Literal('E') +pp_eorE = pyparsing.Literal("e") | pyparsing.Literal("E") pp_param_type = ( - pyparsing.Literal("integer") | pyparsing.Literal("real") - | pyparsing.Literal("categorical") | pyparsing.Literal("ordinal") + pyparsing.Literal("integer") + | pyparsing.Literal("real") + | pyparsing.Literal("categorical") + | pyparsing.Literal("ordinal") ) pp_floatorint = pp_float | pp_int pp_e_notation = pyparsing.Combine(pp_floatorint + pp_eorE + pp_int) @@ -92,48 +116,78 @@ pp_sequence = pp_param_name + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name)) pp_ord_param = pp_param_name + pp_param_type + "{" + pp_sequence + "}" + "[" + pp_param_name + "]" pp_cont_param = ( - pp_param_name + pp_param_type + "[" + pp_number + "," - + pp_number + "]" + "[" + pp_number + "]" + pyparsing.Optional(pp_log) + pp_param_name + + pp_param_type + + "[" + + pp_number + + "," + + pp_number + + "]" + + "[" + + pp_number + + "]" + + pyparsing.Optional(pp_log) ) pp_cat_param = pp_param_name + pp_param_type + "{" + pp_choices + "}" + "[" + pp_param_name + "]" -pp_condition = pp_param_name + "|" + pp_param_name + pp_param_operation + \ - pyparsing.Optional('{') + pp_param_val + pyparsing.Optional('}') + \ - pyparsing.Optional( +pp_condition = ( + pp_param_name + + "|" + + pp_param_name + + pp_param_operation + + pyparsing.Optional("{") + + pp_param_val + + pyparsing.Optional("}") + + pyparsing.Optional( pyparsing.OneOrMore( - (pp_connectiveAND | pp_connectiveOR) + pp_param_name + pp_param_operation - + pyparsing.Optional('{') + pp_param_val + pyparsing.Optional('}') - ) + (pp_connectiveAND | pp_connectiveOR) + + pp_param_name + + pp_param_operation + + pyparsing.Optional("{") + + pp_param_val + + pyparsing.Optional("}"), + ), ) -pp_forbidden_clause = "{" + pp_param_name + "=" + pp_numberorname + \ - pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name + "=" + pp_numberorname)) + "}" +) +pp_forbidden_clause = ( + "{" + + pp_param_name + + "=" + + pp_numberorname + + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name + "=" + pp_numberorname)) + + "}" +) -def build_categorical(param): +def build_categorical(param: CategoricalHyperparameter) -> str: if param.weights is not None: - raise ValueError('The pcs format does not support categorical hyperparameters with ' - 'assigned weights (for hyperparameter %s)' % param.name) + raise ValueError( + "The pcs format does not support categorical hyperparameters with " + "assigned weights (for hyperparameter %s)" % param.name, + ) cat_template = "%s categorical {%s} [%s]" - return cat_template % (param.name, - ", ".join([str(value) for value in param.choices]), - str(param.default_value)) + return cat_template % ( + param.name, + ", ".join([str(value) for value in param.choices]), + str(param.default_value), + ) -def build_ordinal(param): - ordinal_template = '%s ordinal {%s} [%s]' - return ordinal_template % (param.name, - ", ".join([str(value) for value in param.sequence]), - str(param.default_value)) +def build_ordinal(param: OrdinalHyperparameter) -> str: + ordinal_template = "%s ordinal {%s} [%s]" + return ordinal_template % ( + param.name, + ", ".join([str(value) for value in param.sequence]), + str(param.default_value), + ) -def build_constant(param): - const_template = '%s categorical {%s} [%s]' - return const_template % (param.name, - param.value, param.value) +def build_constant(param: Constant) -> str: + const_template = "%s categorical {%s} [%s]" + return const_template % (param.name, param.value, param.value) -def build_continuous(param): - if type(param) in (NormalIntegerHyperparameter, - NormalFloatHyperparameter): +def build_continuous(param: NormalFloatHyperparameter | NormalIntegerHyperparameter) -> str: + if type(param) in (NormalIntegerHyperparameter, NormalFloatHyperparameter): param = param.to_uniform() float_template = "%s%s real [%s, %s] [%s]" @@ -142,27 +196,35 @@ def build_continuous(param): float_template += "log" int_template += "log" - if param.q is not None: - q_prefix = "Q%d_" % (int(param.q),) - else: - q_prefix = "" + q_prefix = "Q%d_" % (int(param.q),) if param.q is not None else "" default_value = param.default_value if isinstance(param, IntegerHyperparameter): default_value = int(default_value) - return int_template % (q_prefix, param.name, param.lower, - param.upper, default_value) - else: - return float_template % (q_prefix, param.name, str(param.lower), - str(param.upper), str(default_value)) + return int_template % ( + q_prefix, + param.name, + param.lower, + param.upper, + default_value, + ) + + return float_template % ( + q_prefix, + param.name, + str(param.lower), + str(param.upper), + str(default_value), + ) -def build_condition(condition): +def build_condition(condition: ConditionComponent) -> str: if not isinstance(condition, ConditionComponent): raise TypeError( - "build_condition must be called with an instance of '%s', got '%s'" % - (ConditionComponent, type(condition)) + "build_condition must be called with an instance" + f" of '{ConditionComponent}', got '{type(condition)}'", ) + # Now handle the conditions SMAC can handle in_template = "%s | %s in {%s}" less_template = "%s | %s < %s" @@ -171,38 +233,51 @@ def build_condition(condition): equal_template = "%s | %s == %s" if isinstance(condition, InCondition): - cond_values = [str(value) for value in condition.value] + cond_values = ", ".join([str(value) for value in condition.value]) else: cond_values = str(condition.value) if isinstance(condition, NotEqualsCondition): - return notequal_template % (condition.child.name, - condition.parent.name, - cond_values) - - elif isinstance(condition, InCondition): - return in_template % (condition.child.name, - condition.parent.name, - ", ".join(cond_values)) - - elif isinstance(condition, EqualsCondition): - return equal_template % (condition.child.name, - condition.parent.name, - cond_values) - elif isinstance(condition, LessThanCondition): - return less_template % (condition.child.name, - condition.parent.name, - cond_values) - elif isinstance(condition, GreaterThanCondition): - return greater_template % (condition.child.name, - condition.parent.name, - cond_values) - - -def build_conjunction(conjunction): + return notequal_template % ( + condition.child.name, + condition.parent.name, + cond_values, + ) + + if isinstance(condition, InCondition): + return in_template % ( + condition.child.name, + condition.parent.name, + cond_values, + ) + + if isinstance(condition, EqualsCondition): + return equal_template % ( + condition.child.name, + condition.parent.name, + cond_values, + ) + if isinstance(condition, LessThanCondition): + return less_template % ( + condition.child.name, + condition.parent.name, + cond_values, + ) + if isinstance(condition, GreaterThanCondition): + return greater_template % ( + condition.child.name, + condition.parent.name, + cond_values, + ) + + raise TypeError(f"Didn't find a matching template for type {condition}") + + +def build_conjunction(conjunction: AbstractConjunction) -> str: + line: str line = conjunction.get_children()[0].name + " | " - cond_list = list() + cond_list = [] for component in conjunction.components: tmp = build_condition(component.get_descendant_literal_conditions()[0]) @@ -214,18 +289,22 @@ def build_conjunction(conjunction): line += " && ".join(cond_list) elif isinstance(conjunction, OrConjunction): line += " || ".join(cond_list) + return line -def build_forbidden(clause): +def build_forbidden(clause: AbstractForbiddenComponent) -> str: if not isinstance(clause, AbstractForbiddenComponent): - raise TypeError("build_forbidden must be called with an instance of " - "'%s', got '%s'" % - (AbstractForbiddenComponent, type(clause))) + raise TypeError( + "build_forbidden must be called with an instance of " + f"'{AbstractForbiddenComponent}', got '{type(clause)}'", + ) + if isinstance(clause, ForbiddenRelation): - raise TypeError("build_forbidden must not be called with an instance of " - "'%s', got '%s'" % - (ForbiddenRelation, type(clause))) + raise TypeError( + "build_forbidden must not be called with an instance of " + f"'{AbstractForbiddenComponent}', got '{type(clause)}'", + ) retval = StringIO() retval.write("{") @@ -235,62 +314,68 @@ def build_forbidden(clause): for dlc in dlcs: if retval.tell() > 1: retval.write(", ") - retval.write("%s=%s" % (dlc.hyperparameter.name, dlc.value)) + retval.write(f"{dlc.hyperparameter.name}={dlc.value}") retval.write("}") retval.seek(0) return retval.getvalue() -def condition_specification(child_name, condition, configuration_space): +def condition_specification( + child_name: str, + condition: list[str], + configuration_space: ConfigurationSpace, +) -> ConditionComponent: # specifies the condition type - child = configuration_space.get_hyperparameter(child_name) + child = configuration_space[child_name] parent_name = condition[0] - parent = configuration_space.get_hyperparameter(parent_name) + parent = configuration_space[parent_name] operation = condition[1] - if operation == 'in': - restrictions = condition[3:-1:2] - for i in range(len(restrictions)): + if operation == "in": + restrictions = list(condition[3:-1:2]) + for i, val in enumerate(restrictions): if isinstance(parent, FloatHyperparameter): - restrictions[i] = float(restrictions[i]) + restrictions[i] = float(val) # type: ignore elif isinstance(parent, IntegerHyperparameter): - restrictions[i] = int(restrictions[i]) + restrictions[i] = int(val) # type: ignore if len(restrictions) == 1: condition = EqualsCondition(child, parent, restrictions[0]) else: condition = InCondition(child, parent, values=restrictions) return condition + + restriction: float | int | str = condition[2] + if isinstance(parent, FloatHyperparameter): + restriction = float(restriction) + elif isinstance(parent, IntegerHyperparameter): + restriction = int(restriction) + + if operation == "==": + condition = EqualsCondition(child, parent, restriction) + elif operation == "!=": + condition = NotEqualsCondition(child, parent, restriction) else: - restrictions = condition[2] if isinstance(parent, FloatHyperparameter): - restrictions = float(restrictions) + restriction = float(restriction) elif isinstance(parent, IntegerHyperparameter): - restrictions = int(restrictions) - - if operation == '==': - condition = EqualsCondition(child, parent, restrictions) - elif operation == '!=': - condition = NotEqualsCondition(child, parent, restrictions) + restriction = int(restriction) + elif isinstance(parent, OrdinalHyperparameter): + pass else: - if isinstance(parent, FloatHyperparameter): - restrictions = float(restrictions) - elif isinstance(parent, IntegerHyperparameter): - restrictions = int(restrictions) - elif isinstance(parent, OrdinalHyperparameter): - pass - else: - raise ValueError('The parent of a conditional hyperparameter ' - 'must be either a float, int or ordinal ' - 'hyperparameter, but is %s.' % type(parent)) - - if operation == '<': - condition = LessThanCondition(child, parent, restrictions) - elif operation == '>': - condition = GreaterThanCondition(child, parent, restrictions) - return condition + raise ValueError( + "The parent of a conditional hyperparameter " + "must be either a float, int or ordinal " + "hyperparameter, but is %s." % type(parent), + ) + + if operation == "<": + condition = LessThanCondition(child, parent, restriction) + elif operation == ">": + condition = GreaterThanCondition(child, parent, restriction) + return condition -def read(pcs_string, debug=False): +def read(pcs_string: Iterable[str]) -> ConfigurationSpace: """ Read in a :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` definition from a pcs file. @@ -316,10 +401,8 @@ def read(pcs_string, debug=False): Parameters ---------- - pcs_string : str + pcs_string : Iterable[str] ConfigSpace definition in pcs format - debug : bool - Provides debug information. Defaults to False. Returns ------- @@ -354,8 +437,9 @@ def read(pcs_string, debug=False): try: c = pp_condition.parseString(line) conditions.append(c) - except pyparsing.ParseException: - raise NotImplementedError("Could not parse condition: %s" % line) + except pyparsing.ParseException as e: + raise NotImplementedError(f"Could not parse condition: {line}") from e + continue if "}" not in line and "]" not in line: continue @@ -368,33 +452,40 @@ def read(pcs_string, debug=False): ct += 1 param = None - create = {"int": UniformIntegerHyperparameter, - "float": UniformFloatHyperparameter, - "categorical": CategoricalHyperparameter, - "ordinal": OrdinalHyperparameter - } + create = { + "int": UniformIntegerHyperparameter, + "float": UniformFloatHyperparameter, + "categorical": CategoricalHyperparameter, + "ordinal": OrdinalHyperparameter, + } try: param_list = pp_cont_param.parseString(line) name = param_list[0] - if param_list[1] == 'integer': - paramtype = 'int' - elif param_list[1] == 'real': - paramtype = 'float' + if param_list[1] == "integer": + paramtype = "int" + elif param_list[1] == "real": + paramtype = "float" else: paramtype = None - if paramtype in ['int', 'float']: + if paramtype in ["int", "float"]: log = param_list[10:] param_list = param_list[:10] if len(log) > 0: log = log[0] - lower = float(param_list[3]) - upper = float(param_list[5]) - log_on = True if "log" in log else False - default_value = float(param_list[8]) - param = create[paramtype](name=name, lower=lower, upper=upper, - q=None, log=log_on, default_value=default_value) + lower = float(param_list[3]) # type: ignore + upper = float(param_list[5]) # type: ignore + log_on = "log" in log + default_value = float(param_list[8]) # type: ignore + param = create[paramtype]( + name=name, + lower=lower, + upper=upper, + q=None, + log=log_on, + default_value=default_value, + ) cont_ct += 1 except pyparsing.ParseException: @@ -404,7 +495,7 @@ def read(pcs_string, debug=False): if "categorical" in line: param_list = pp_cat_param.parseString(line) name = param_list[0] - choices = [choice for choice in param_list[3:-4:2]] + choices = list(param_list[3:-4:2]) default_value = param_list[-2] param = create["categorical"]( name=name, @@ -416,7 +507,7 @@ def read(pcs_string, debug=False): elif "ordinal" in line: param_list = pp_ord_param.parseString(line) name = param_list[0] - sequence = [seq for seq in param_list[3:-4:2]] + sequence = list(param_list[3:-4:2]) default_value = param_list[-2] param = create["ordinal"]( name=name, @@ -435,66 +526,75 @@ def read(pcs_string, debug=False): for clause in forbidden: param_list = pp_forbidden_clause.parseString(clause) - tmp_list = [] + tmp_list: list = [] clause_list = [] for value in param_list[1:]: if len(tmp_list) < 3: tmp_list.append(value) else: # So far, only equals is supported by SMAC - if tmp_list[1] == '=': - hp = configuration_space.get_hyperparameter(tmp_list[0]) + if tmp_list[1] == "=": + hp = configuration_space[tmp_list[0]] if isinstance(hp, NumericalHyperparameter): + forbidden_value: float | int if isinstance(hp, IntegerHyperparameter): forbidden_value = int(tmp_list[2]) elif isinstance(hp, FloatHyperparameter): forbidden_value = float(tmp_list[2]) else: raise NotImplementedError + if forbidden_value < hp.lower or forbidden_value > hp.upper: - raise ValueError(f'forbidden_value is set out of the bound, it needs to' - f' be set between [{hp.lower}, {hp.upper}]' - f' but its value is {forbidden_value}') + raise ValueError( + f"forbidden_value is set out of the bound, it needs to" + f" be set between [{hp.lower}, {hp.upper}]" + f" but its value is {forbidden_value}", + ) + elif isinstance(hp, (CategoricalHyperparameter, OrdinalHyperparameter)): - hp_values = hp.choices if isinstance(hp, CategoricalHyperparameter)\ - else hp.sequence + hp_values = ( + hp.choices if isinstance(hp, CategoricalHyperparameter) else hp.sequence + ) forbidden_value_in_hp_values = tmp_list[2] in hp_values + if forbidden_value_in_hp_values: forbidden_value = tmp_list[2] else: - raise ValueError(f'forbidden_value is set out of the allowed value ' - f'sets, it needs to be one member from {hp_values} ' - f'but its value is {forbidden_value}') + raise ValueError( + f"forbidden_value is set out of the allowed value " + f"sets, it needs to be one member from {hp_values} " + f"but its value is {tmp_list[2]}", + ) else: - raise ValueError('Unsupported Hyperparamter sorts') + raise ValueError("Unsupported Hyperparamter sorts") - clause_list.append(ForbiddenEqualsClause( - configuration_space.get_hyperparameter(tmp_list[0]), - forbidden_value)) + clause_list.append( + ForbiddenEqualsClause(configuration_space[tmp_list[0]], forbidden_value), + ) else: raise NotImplementedError() tmp_list = [] - configuration_space.add_forbidden_clause(ForbiddenAndConjunction( - *clause_list)) + configuration_space.add_forbidden_clause(ForbiddenAndConjunction(*clause_list)) + + conditions_per_child: dict = OrderedDict() - conditions_per_child = OrderedDict() for condition in conditions: child_name = condition[0] if child_name not in conditions_per_child: - conditions_per_child[child_name] = list() + conditions_per_child[child_name] = [] conditions_per_child[child_name].append(condition) for child_name in conditions_per_child: for condition in conditions_per_child[child_name]: condition = condition[2:] - condition = ' '.join(condition) - if '||' in str(condition): + condition = " ".join(condition) # type: ignore + if "||" in str(condition): ors = [] # 1st case we have a mixture of || and && - if '&&' in str(condition): + if "&&" in str(condition): ors_combis = [] - for cond_parts in str(condition).split('||'): - condition = str(cond_parts).split('&&') + for cond_parts in str(condition).split("||"): + condition = str(cond_parts).split("&&") # type: ignore # if length is 1 it must be or if len(condition) == 1: element_list = condition[0].split() @@ -503,67 +603,68 @@ def read(pcs_string, debug=False): child_name, element_list, configuration_space, - ) + ), ) else: # now taking care of ands ands = [] for and_part in condition: element_list = [ - element for part in condition for element in and_part.split() + element for _ in condition for element in and_part.split() ] ands.append( condition_specification( child_name, element_list, configuration_space, - ) + ), ) ors_combis.append(AndConjunction(*ands)) mixed_conjunction = OrConjunction(*ors_combis) configuration_space.add_condition(mixed_conjunction) else: # 2nd case: we only have ors - for cond_parts in str(condition).split('||'): - element_list = [element for element in cond_parts.split()] + for cond_parts in str(condition).split("||"): + element_list = list(cond_parts.split()) ors.append( condition_specification( child_name, element_list, configuration_space, - ) + ), ) or_conjunction = OrConjunction(*ors) configuration_space.add_condition(or_conjunction) - else: - # 3rd case: we only have ands - if '&&' in str(condition): - ands = [] - for cond_parts in str(condition).split('&&'): - element_list = [element for element in cond_parts.split()] - ands.append( - condition_specification( - child_name, - element_list, - configuration_space, - ) - ) - and_conjunction = AndConjunction(*ands) - configuration_space.add_condition(and_conjunction) - else: - # 4th case: we have a normal condition - element_list = [element for element in condition.split()] - normal_condition = condition_specification( - child_name, - element_list, - configuration_space, + + # 3rd case: we only have ands + elif "&&" in str(condition): + ands = [] + for cond_parts in str(condition).split("&&"): + element_list = list(cond_parts.split()) + ands.append( + condition_specification( + child_name, + element_list, + configuration_space, + ), ) - configuration_space.add_condition(normal_condition) + and_conjunction = AndConjunction(*ands) + configuration_space.add_condition(and_conjunction) + + # 4th case: we have a normal condition + else: + element_list = list(condition.split()) + normal_condition = condition_specification( + child_name, + element_list, + configuration_space, + ) + configuration_space.add_condition(normal_condition) return configuration_space -def write(configuration_space): +def write(configuration_space: ConfigurationSpace) -> str: """ Create a string representation of a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` @@ -597,19 +698,22 @@ def write(configuration_space): """ if not isinstance(configuration_space, ConfigurationSpace): - raise TypeError("pcs_parser.write expects an instance of %s, " - "you provided '%s'" % (ConfigurationSpace, type(configuration_space))) + raise TypeError( + "pcs_parser.write expects an instance of {}, " + "you provided '{}'".format(ConfigurationSpace, type(configuration_space)), + ) param_lines = StringIO() condition_lines = StringIO() forbidden_lines = [] - for hyperparameter in configuration_space.get_hyperparameters(): - # Check if the hyperparameter names are valid SMAC names! + for hyperparameter in configuration_space.values(): + # Check if the hyperparameter names are valid ConfigSpace names! try: pp_param_name.parseString(hyperparameter.name) - except pyparsing.ParseException: + except pyparsing.ParseException as e: raise ValueError( - "Illegal hyperparameter name for SMAC: %s" % hyperparameter.name) + f"Illegal hyperparameter name for ConfigSpace: {hyperparameter.name}", + ) from e # First build params if param_lines.tell() > 0: @@ -623,13 +727,12 @@ def write(configuration_space): elif isinstance(hyperparameter, Constant): param_lines.write(build_constant(hyperparameter)) else: - raise TypeError("Unknown type: %s (%s)" % ( - type(hyperparameter), hyperparameter)) + raise TypeError(f"Unknown type: {type(hyperparameter)} ({hyperparameter})") for condition in configuration_space.get_conditions(): if condition_lines.tell() > 0: condition_lines.write("\n") - if isinstance(condition, AndConjunction) or isinstance(condition, OrConjunction): + if isinstance(condition, (AndConjunction, OrConjunction)): condition_lines.write(build_conjunction(condition)) else: condition_lines.write(build_condition(condition)) @@ -643,18 +746,17 @@ def write(configuration_space): for dlc in dlcs: if isinstance(dlc, MultipleValueForbiddenClause): if not isinstance(dlc, ForbiddenInClause): - raise ValueError("SMAC cannot handle this forbidden " - "clause: %s" % dlc) + raise ValueError("SMAC cannot handle this forbidden " "clause: %s" % dlc) in_statements.append( - [ForbiddenEqualsClause(dlc.hyperparameter, value) - for value in dlc.values]) + [ForbiddenEqualsClause(dlc.hyperparameter, value) for value in dlc.values], + ) else: other_statements.append(dlc) # Second, create the product of all elements in the IN statements, # create a ForbiddenAnd and add all ForbiddenEquals if len(in_statements) > 0: - for i, p in enumerate(product(*in_statements)): + for p in product(*in_statements): all_forbidden_clauses = list(p) + other_statements f = ForbiddenAndConjunction(*all_forbidden_clauses) forbidden_lines.append(build_forbidden(f)) diff --git a/ConfigSpace/util.pyx b/ConfigSpace/util.py similarity index 71% rename from ConfigSpace/util.pyx rename to ConfigSpace/util.py index 80329900..120bfce3 100644 --- a/ConfigSpace/util.pyx +++ b/ConfigSpace/util.py @@ -25,25 +25,32 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from __future__ import annotations -# cython: language_level=3 - -from collections import deque import copy -from typing import Union, Dict, Generator, List, Tuple, Optional - -import numpy as np # type: ignore -from ConfigSpace import Configuration, ConfigurationSpace -from ConfigSpace.exceptions import ForbiddenValueError -from ConfigSpace.hyperparameters import CategoricalHyperparameter, \ - UniformFloatHyperparameter, UniformIntegerHyperparameter, Constant, \ - OrdinalHyperparameter, NumericalHyperparameter -import ConfigSpace.c_util -cimport cython +from collections import deque +from typing import Any, Iterator, cast +import numpy as np -def impute_inactive_values(configuration: Configuration, - strategy: Union[str, float] = "default") -> Configuration: +import ConfigSpace.c_util +from ConfigSpace import Configuration, ConfigurationSpace +from ConfigSpace.exceptions import ActiveHyperparameterNotSetError, ForbiddenValueError +from ConfigSpace.hyperparameters import ( + CategoricalHyperparameter, + Constant, + Hyperparameter, + NumericalHyperparameter, + OrdinalHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, +) + + +def impute_inactive_values( + configuration: Configuration, + strategy: str | float = "default", +) -> Configuration: """Impute inactive parameters. Iterate through the hyperparameters of a ``Configuration`` and set the @@ -68,11 +75,10 @@ def impute_inactive_values(configuration: Configuration, A new configuration with the imputed values. In this new configuration inactive values are included. """ - values = dict() - for hp in configuration.configuration_space.get_hyperparameters(): + values = {} + for hp in configuration.config_space.values(): value = configuration.get(hp.name) if value is None: - if strategy == "default": new_value = hp.default_value @@ -80,23 +86,25 @@ def impute_inactive_values(configuration: Configuration, new_value = strategy else: - raise ValueError("Unknown imputation strategy %s" % str(strategy)) + raise ValueError(f"Unknown imputation strategy {strategy}") value = new_value values[hp.name] = value - new_configuration = Configuration(configuration.configuration_space, - values=values, - allow_inactive_with_values=True) - return new_configuration + return Configuration( + configuration.config_space, + values=values, + allow_inactive_with_values=True, + ) def get_one_exchange_neighbourhood( - configuration: Configuration, - seed: int, - num_neighbors: int = 4, - stdev: float = 0.2) -> Generator[Configuration]: + configuration: Configuration, + seed: int, + num_neighbors: int = 4, + stdev: float = 0.2, +) -> Iterator[Configuration]: """ Return all configurations in a one-exchange neighborhood. @@ -122,22 +130,20 @@ def get_one_exchange_neighbourhood( Returns ------- - Generator + Iterator It contains configurations, with values being situated around the given configuration. """ random = np.random.RandomState(seed) - hyperparameters_list = list( - list(configuration.configuration_space._hyperparameters.keys()) - ) + hyperparameters_list = list(configuration.config_space._hyperparameters.keys()) hyperparameters_list_length = len(hyperparameters_list) hyperparameters_used = [ hp.name - for hp in configuration.configuration_space.get_hyperparameters() + for hp in configuration.config_space.values() if ( hp.get_num_neighbors(configuration.get(hp.name)) == 0 - and configuration.get(hp.name)is not None + and configuration.get(hp.name) is not None ) ] number_of_usable_hyperparameters = sum(np.isfinite(configuration.get_array())) @@ -145,14 +151,14 @@ def get_one_exchange_neighbourhood( hp.name: num_neighbors if ( isinstance(hp, NumericalHyperparameter) - and hp.get_num_neighbors(configuration.get(hp.name))> num_neighbors - ) else - hp.get_num_neighbors(configuration.get(hp.name)) - for hp in configuration.configuration_space.get_hyperparameters() + and hp.get_num_neighbors(configuration.get(hp.name)) > num_neighbors + ) + else hp.get_num_neighbors(configuration.get(hp.name)) + for hp in configuration.config_space.values() } - finite_neighbors_stack = {} # type: Dict - configuration_space = configuration.configuration_space # type: ConfigSpace + finite_neighbors_stack: dict[str, list[Hyperparameter]] = {} + configuration_space = configuration.config_space while len(hyperparameters_used) < number_of_usable_hyperparameters: index = int(random.randint(hyperparameters_list_length)) @@ -171,15 +177,12 @@ def get_one_exchange_neighbourhood( continue iteration = 0 - hp = configuration_space.get_hyperparameter(hp_name) # type: Hyperparameter + hp = configuration_space[hp_name] num_neighbors_for_hp = hp.get_num_neighbors(configuration.get(hp_name)) while True: # Obtain neigbors differently for different possible numbers of # neighbors - if num_neighbors_for_hp == 0: - break - # No infinite loops - elif iteration > 100: + if num_neighbors_for_hp == 0 or iteration > 100: break elif np.isinf(num_neighbors_for_hp): if number_of_sampled_neighbors >= 1: @@ -194,8 +197,10 @@ def get_one_exchange_neighbourhood( if hp_name not in finite_neighbors_stack: if isinstance(hp, UniformIntegerHyperparameter): neighbors = hp.get_neighbors( - value, random, - number=n_neighbors_per_hp[hp_name], std=stdev, + value, + random, + number=n_neighbors_per_hp[hp_name], + std=stdev, ) else: neighbors = hp.get_neighbors(value, random) @@ -212,12 +217,15 @@ def get_one_exchange_neighbourhood( configuration_array=new_array, hp_name=hp_name, hp_value=neighbor, - index=index) + index=index, + ) try: # Populating a configuration from an array does not check # if it is a legal configuration - check this (slow) - new_configuration = Configuration(configuration_space, - vector=new_array) # type: Configuration + new_configuration = Configuration( + configuration_space, + vector=new_array, + ) # type: Configuration # Only rigorously check every tenth configuration ( # because moving around in the neighborhood should # just work!) @@ -239,13 +247,12 @@ def get_one_exchange_neighbourhood( hyperparameters_used.append(hp_name) n_neighbors_per_hp[hp_name] = 0 hyperparameters_used.append(hp_name) - else: - if hp_name not in hyperparameters_used: - n_ = neighbourhood.pop() - n_neighbors_per_hp[hp_name] -= 1 - if n_neighbors_per_hp[hp_name] == 0: - hyperparameters_used.append(hp_name) - yield n_ + elif hp_name not in hyperparameters_used: + n_ = neighbourhood.pop() + n_neighbors_per_hp[hp_name] -= 1 + if n_neighbors_per_hp[hp_name] == 0: + hyperparameters_used.append(hp_name) + yield n_ def get_random_neighbor(configuration: Configuration, seed: int) -> Configuration: @@ -277,26 +284,25 @@ def get_random_neighbor(configuration: Configuration, seed: int) -> Configuratio """ random = np.random.RandomState(seed) rejected = True - values = copy.deepcopy(configuration.get_dictionary()) + values = copy.deepcopy(dict(configuration)) + new_configuration = None while rejected: # First, choose an active hyperparameter active = False iteration = 0 + hp = None + value = None while not active: iteration += 1 - if configuration._num_hyperparameters > 1: - rand_idx = random.randint(0, configuration._num_hyperparameters) - else: - rand_idx = 0 + rand_idx = random.randint(0, len(configuration)) if len(configuration) > 1 else 0 value = configuration.get_array()[rand_idx] if np.isfinite(value): active = True - hp_name = configuration.configuration_space \ - .get_hyperparameter_by_idx(rand_idx) - hp = configuration.configuration_space.get_hyperparameter(hp_name) + hp_name = configuration.config_space.get_hyperparameter_by_idx(rand_idx) + hp = configuration.config_space[hp_name] # Only choose if there is a possibility of finding a neigboor if not hp.has_neighbors(): @@ -304,28 +310,32 @@ def get_random_neighbor(configuration: Configuration, seed: int) -> Configuratio if iteration > 10000: raise ValueError("Probably caught in an infinite loop.") + + assert hp is not None + assert value is not None + # Get a neighboor and adapt the rest of the configuration if necessary neighbor = hp.get_neighbors(value, random, number=1, transform=True)[0] previous_value = values[hp.name] values[hp.name] = neighbor try: - new_configuration = Configuration( - configuration.configuration_space, values=values) + new_configuration = Configuration(configuration.config_space, values=values) rejected = False except ValueError: values[hp.name] = previous_value + assert new_configuration is not None return new_configuration def deactivate_inactive_hyperparameters( - configuration: Dict, - configuration_space: ConfigurationSpace, - vector: Union[None, np.ndarray] = None, -): + configuration: dict, + configuration_space: ConfigurationSpace, + vector: None | np.ndarray = None, +) -> Configuration: """ - Remove inactive hyperparameters from a given configuration + Remove inactive hyperparameters from a given configuration. Parameters ---------- @@ -349,16 +359,18 @@ def deactivate_inactive_hyperparameters( that inactivate hyperparameters have been removed. """ - hyperparameters = configuration_space.get_hyperparameters() - configuration = Configuration(configuration_space=configuration_space, - values=configuration, - vector=vector, - allow_inactive_with_values=True) + hyperparameters = configuration_space.values() + config = Configuration( + configuration_space=configuration_space, + values=configuration, + vector=vector, + allow_inactive_with_values=True, + ) - hps = deque() + hps: deque[Hyperparameter] = deque() unconditional_hyperparameters = configuration_space.get_all_unconditional_hyperparameters() - hyperparameters_with_children = list() + hyperparameters_with_children = [] for uhp in unconditional_hyperparameters: children = configuration_space._children_of[uhp] if len(children) > 0: @@ -373,36 +385,40 @@ def deactivate_inactive_hyperparameters( for child in children: conditions = configuration_space._parent_conditions_of[child.name] for condition in conditions: - if not condition.evaluate_vector(configuration.get_array()): - dic = configuration.get_dictionary() + if not condition.evaluate_vector(config.get_array()): + dic = dict(config) try: del dic[child.name] except KeyError: continue - configuration = Configuration( + config = Configuration( configuration_space=configuration_space, values=dic, - allow_inactive_with_values=True) + allow_inactive_with_values=True, + ) inactive.add(child.name) hps.appendleft(child.name) for hp in hyperparameters: if hp.name in inactive: - dic = configuration.get_dictionary() + dic = dict(config) try: del dic[hp.name] except KeyError: continue - configuration = Configuration( + config = Configuration( configuration_space=configuration_space, values=dic, - allow_inactive_with_values=True) + allow_inactive_with_values=True, + ) - return Configuration(configuration_space, values=configuration.get_dictionary()) + return Configuration(configuration_space, values=dict(config)) -def fix_types(configuration: dict, - configuration_space: ConfigurationSpace): +def fix_types( + configuration: dict[str, Any], + configuration_space: ConfigurationSpace, +) -> dict[str, Any]: """ Iterate over all hyperparameters in the ConfigSpace and fix the types of the parameter values in configuration. @@ -420,42 +436,47 @@ def fix_types(configuration: dict, dict configuration with fixed types of parameter values """ - def fix_type_from_candidates(value, candidates): + + def fix_type_from_candidates(value: Any, candidates: list[Any]) -> Any: result = [c for c in candidates if str(value) == str(c)] if len(result) != 1: - raise ValueError("Parameter value %s cannot be matched to candidates %s. " - "Either none or too many matching candidates." % ( - str(value), candidates - ) - ) + raise ValueError( + "Parameter value {} cannot be matched to candidates {}. " + "Either none or too many matching candidates.".format(str(value), candidates), + ) return result[0] - for param in configuration_space.get_hyperparameters(): + for param in configuration_space.values(): param_name = param.name if configuration.get(param_name) is not None: if isinstance(param, (CategoricalHyperparameter)): - configuration[param_name] = fix_type_from_candidates(configuration[param_name], - param.choices) + configuration[param_name] = fix_type_from_candidates( + configuration[param_name], + param.choices, + ) elif isinstance(param, (OrdinalHyperparameter)): - configuration[param_name] = fix_type_from_candidates(configuration[param_name], - param.sequence) + configuration[param_name] = fix_type_from_candidates( + configuration[param_name], + param.sequence, + ) elif isinstance(param, Constant): - configuration[param_name] = fix_type_from_candidates(configuration[param_name], - [param.value]) + configuration[param_name] = fix_type_from_candidates( + configuration[param_name], + [param.value], + ) elif isinstance(param, UniformFloatHyperparameter): configuration[param_name] = float(configuration[param_name]) elif isinstance(param, UniformIntegerHyperparameter): configuration[param_name] = int(configuration[param_name]) else: - raise TypeError("Unknown hyperparameter type %s" % type(param)) + raise TypeError(f"Unknown hyperparameter type {type(param)}") return configuration -@cython.boundscheck(True) # Activate bounds checking -@cython.wraparound(True) # Activate negative indexing -def generate_grid(configuration_space: ConfigurationSpace, - num_steps_dict: Optional[Dict[str, int]] = None, - ) -> List[Configuration]: +def generate_grid( + configuration_space: ConfigurationSpace, + num_steps_dict: dict[str, int] | None = None, +) -> list[Configuration]: """ Generates a grid of Configurations for a given ConfigurationSpace. Can be used, for example, for grid search. @@ -483,8 +504,8 @@ def generate_grid(configuration_space: ConfigurationSpace, for the OrderedDict within the ConfigurationSpace. """ - def get_value_set(num_steps_dict: Optional[Dict[str, int]], hp_name: str): - ''' + def get_value_set(num_steps_dict: dict[str, int] | None, hp_name: str) -> tuple: + """ Gets values along the grid for a particular hyperparameter. Uses the num_steps_dict to determine number of grid values for UniformFloatHyperparameter @@ -505,18 +526,18 @@ def get_value_set(num_steps_dict: Optional[Dict[str, int]], hp_name: str): tuple Holds grid values for the given hyperparameter - ''' - param = configuration_space.get_hyperparameter(hp_name) + """ + param = configuration_space[hp_name] if isinstance(param, (CategoricalHyperparameter)): - return param.choices + return cast(tuple, param.choices) - elif isinstance(param, (OrdinalHyperparameter)): - return param.sequence + if isinstance(param, (OrdinalHyperparameter)): + return cast(tuple, param.sequence) - elif isinstance(param, Constant): - return tuple([param.value, ]) + if isinstance(param, Constant): + return (param.value,) - elif isinstance(param, UniformFloatHyperparameter): + if isinstance(param, UniformFloatHyperparameter): if param.log: lower, upper = np.log([param.lower, param.upper]) else: @@ -525,16 +546,16 @@ def get_value_set(num_steps_dict: Optional[Dict[str, int]], hp_name: str): if num_steps_dict is not None and param.name in num_steps_dict: num_steps = num_steps_dict[param.name] grid_points = np.linspace(lower, upper, num_steps) + + # check for log and for rounding issues + elif param.q is not None: + grid_points = np.arange(lower, upper + param.q, param.q) else: - # check for log and for rounding issues - if param.q is not None: - grid_points = np.arange(lower, upper + param.q, param.q) - else: - raise ValueError( - "num_steps_dict is None or doesn't contain the number of points" - f" to divide {param.name} into. And its quantization factor " - "is None. Please provide/set one of these values." - ) + raise ValueError( + "num_steps_dict is None or doesn't contain the number of points" + f" to divide {param.name} into. And its quantization factor " + "is None. Please provide/set one of these values.", + ) if param.log: grid_points = np.exp(grid_points) @@ -547,7 +568,7 @@ def get_value_set(num_steps_dict: Optional[Dict[str, int]], hp_name: str): return tuple(grid_points) - elif isinstance(param, UniformIntegerHyperparameter): + if isinstance(param, UniformIntegerHyperparameter): if param.log: lower, upper = np.log([param.lower, param.upper]) else: @@ -556,16 +577,16 @@ def get_value_set(num_steps_dict: Optional[Dict[str, int]], hp_name: str): if num_steps_dict is not None and param.name in num_steps_dict: num_steps = num_steps_dict[param.name] grid_points = np.linspace(lower, upper, num_steps) + + # check for log and for rounding issues + elif param.q is not None: + grid_points = np.arange(lower, upper + param.q, param.q) else: - # check for log and for rounding issues - if param.q is not None: - grid_points = np.arange(lower, upper + param.q, param.q) - else: - raise ValueError( - "num_steps_dict is None or doesn't contain the number of points " - f"to divide {param.name} into. And its quantization factor " - "is None. Please provide/set one of these values." - ) + raise ValueError( + "num_steps_dict is None or doesn't contain the number of points " + f"to divide {param.name} into. And its quantization factor " + "is None. Please provide/set one of these values.", + ) if param.log: grid_points = np.exp(grid_points) @@ -579,11 +600,10 @@ def get_value_set(num_steps_dict: Optional[Dict[str, int]], hp_name: str): return tuple(grid_points) - else: - raise TypeError("Unknown hyperparameter type %s" % type(param)) + raise TypeError(f"Unknown hyperparameter type {type(param)}") - def get_cartesian_product(value_sets: List[Tuple], hp_names: List[str]): - ''' + def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[dict[str, Any]]: + """ Returns a grid for a subspace of the configuration with given hyperparameters and their grid values. @@ -604,19 +624,17 @@ def get_cartesian_product(value_sets: List[Tuple], hp_names: List[str]): ------- list of dicts List of configuration dicts - - ''' - grid = [] + """ import itertools + if len(value_sets) == 0: # Edge case - pass - else: - for element in itertools.product(*value_sets): - config_dict = {} - for j, hp_name in enumerate(hp_names): - config_dict[hp_name] = element[j] - grid.append(config_dict) + return [] + + grid = [] + for element in itertools.product(*value_sets): + config_dict = dict(zip(hp_names, element)) + grid.append(config_dict) return grid @@ -648,24 +666,23 @@ def get_cartesian_product(value_sets: List[Tuple], hp_names: List[str]): unchecked_grid_pts.popleft() continue - except ValueError as e: - assert (str(e)[:23] == "Active hyperparameter '" and - str(e)[-16:] == "' not specified!"), \ - "Caught exception contains unexpected message." + except ActiveHyperparameterNotSetError: value_sets = [] hp_names = [] new_active_hp_names = [] # "for" loop over currently active HP names for hp_name in unchecked_grid_pts[0]: - value_sets.append(tuple([unchecked_grid_pts[0][hp_name], ])) + value_sets.append( + (unchecked_grid_pts[0][hp_name],), + ) hp_names.append(hp_name) # Checks if the conditionally dependent children of already active # HPs are now active for new_hp_name in configuration_space._children[hp_name]: if ( - new_hp_name not in new_active_hp_names and - new_hp_name not in unchecked_grid_pts[0] + new_hp_name not in new_active_hp_names + and new_hp_name not in unchecked_grid_pts[0] ): all_cond_ = True for cond in configuration_space._parent_conditions_of[new_hp_name]: @@ -679,15 +696,15 @@ def get_cartesian_product(value_sets: List[Tuple], hp_names: List[str]): hp_names.append(hp_name) # this check might not be needed, as there is always going to be a new # active HP when in this except block? - if len(new_active_hp_names) > 0: - new_conditonal_grid = get_cartesian_product(value_sets, hp_names) - unchecked_grid_pts += new_conditonal_grid - else: + if len(new_active_hp_names) <= 0: raise RuntimeError( "Unexpected error: There should have been a newly activated hyperparameter" f" for the current configuration values: {str(unchecked_grid_pts[0])}. " - "Please contact the developers with the code you ran and the stack trace." - ) + "Please contact the developers with the code you ran and the stack trace.", + ) from None + + new_conditonal_grid = get_cartesian_product(value_sets, hp_names) + unchecked_grid_pts += new_conditonal_grid unchecked_grid_pts.popleft() return checked_grid_pts diff --git a/Makefile b/Makefile index 8919248c..bd80b016 100644 --- a/Makefile +++ b/Makefile @@ -36,24 +36,25 @@ CP := ) benchmark: python scripts/benchmark_sampling.py -cython-annotate: - C_INCLUDE_PATH=$(NUMPY_INCLUDE) cython -3 --directive boundscheck=False,wraparound=False --annotate ConfigSpace/*.pyx - -cython-html: cython-annotate - python -c "import webbrowser; from pathlib import Path; [webbrowser.open(f'file://{path}') for path in Path('ConfigSpace').absolute().glob('*.html')]" - install-dev: $(PIP) install -e ".[dev]" pre-commit install -install-test: - $(PIP) install -e ".[test]" +check: + $(PRECOMMIT) run --all-files -install-docs: - $(PIP) install -e ".[docs]" +check-types: + mypy ConfigSpace -pre-commit: - $(PRECOMMIT) run --all-files +fix: + black --quiet ConfigSpace test + ruff --silent --exit-zero --no-cache --fix ConfigSpace test + +build: + python -m build + +test: + $(PYTEST) test clean-build: rm -rf ${BUILD} @@ -63,8 +64,7 @@ clean-docs: clean: clean-build clean-docs -build: - python -m build +clean-test: clean-build build test # Running build before making docs is needed all be it very slow. # Without doing a full build, the doctests seem to use docstrings from the last compiled build @@ -98,7 +98,8 @@ publish: @echo @echo " python -m twine upload dist/*" -test: - $(PYTEST) test +cython-annotate: + C_INCLUDE_PATH=$(NUMPY_INCLUDE) cython -3 --directive boundscheck=False,wraparound=False --annotate ConfigSpace/*.pyx -clean-test: clean-build build test +cython-html: cython-annotate + python -c "import webbrowser; from pathlib import Path; [webbrowser.open(f'file://{path}') for path in Path('ConfigSpace').absolute().glob('*.html')]" diff --git a/changelog.md b/changelog.md index 9766a5af..dc62c41f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,7 @@ # Version 0.6.1 -# MAINT #286: Add support for Python 3.11. -# FIX #282: Fixes a memory leak in the neighborhood generation of integer hyperparameters. +* MAINT #286: Add support for Python 3.11. +* FIX #282: Fixes a memory leak in the neighborhood generation of integer hyperparameters. # Version 0.6.0 diff --git a/docs/conf.py b/docs/conf.py index ccfb6316..af78f86c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,15 +21,15 @@ automl_sphinx_theme.set_options(globals(), options) extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', - 'sphinx.ext.githubpages', - 'sphinx.ext.doctest', + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", + "sphinx.ext.githubpages", + "sphinx.ext.doctest", ] autodoc_typehints = "description" diff --git a/pyproject.toml b/pyproject.toml index efbe4d93..fd8008ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,307 @@ +[project] +name = "ConfigSpace" +version = "0.7.0" +description = """\ + Creation and manipulation of parameter configuration spaces for \ + automated algorithm configuration and hyperparameter tuning. \ + """ +license.file = "LICENSE" +requires-python = ">=3.7" +readme = "README.md" +authors = [ + { name = "Matthias Feurer" }, + { name = "Katharina Eggensperger" }, + { name = "Syed Mohsin Ali" }, + { name = "Christina Hernandez Wunsch" }, + { name = "Julien-Charles Levesque" }, + { name = "Jost Tobias Springenberg" }, + { name = "Philipp Mueller" }, + { name = "Marius Lindauer" }, + { name = "Jorn Tuyls" }, + { name = "Eddie Bergman" }, + { name = "Arjun Krishnakumar" }, +] +maintainers = [ + { name = "Matthias Feurer", email = "feurerm@informatik.uni-freiburg.de" }, + { name = "Eddie Bergman", email = "eddiebergmanhs@gmail.com" }, + { name = "Arjun Krishnakumar", email = "arjunkrishnakumarmec@gmail.com" }, +] +keywords = [ + "algorithm", + "configuration", + "hyperparameter", + "optimization", + "empirical", + "evaluation", + "black", + "box", +] +classifiers = [ + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Natural Language :: English", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering", + "Topic :: Software Development", +] +urls.homepage = "https://github.com/automl/ConfigSpace" +urls.documentation = "https://automl.github.io/ConfigSpace/main/" + +dependencies = [ + "numpy", + "pyparsing", + "scipy", + "typing_extensions", + "more_itertools", +] + +[project.optional-dependencies] +dev = [ + "mypy", + "pre-commit", + "build", + "ruff", + "black", + "pytest>=4.6", + "pytest-cov", + "automl_sphinx_theme>=0.1.11", +] + [build-system] requires = ["setuptools", "wheel", "oldest-supported-numpy", "Cython"] +build-backend = "setuptools.build_meta" + + +[tool.pytest.ini_options] +testpaths = ["test"] +minversion = "7.0" +log_cli = false +log_level = "DEBUG" +xfail_strict = true +addopts = "--durations=10 -vv" +markers = ["example: An example"] + + +[tool.coverage.run] +branch = true +context = "ConfigSpace" + +[tool.coverage.report] +show_missing = true +skip_covered = true +exclude_lines = [ + "pragma: no cover", + '\.\.\.', + "raise NotImplementedError", + "if TYPE_CHECKING", +] + +[tool.black] +target-version = ['py37'] +line-length = 100 + +# https://github.com/charliermarsh/ruff +[tool.ruff] +target-version = "py37" +line-length = 100 +show-source = true +src = ["ConfigSpace", "test"] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +select = [ + "A", + # "ANN", # Handled by mypy + "ARG", + "B", + "BLE", + "COM", + "C4", + "D", + "DTZ", + "E", + # "EXE", Meh + "ERA", + "F", + # "FBT", # Too many boolean positional params around + "I", + # "ISC", # Favours implicit string concatenation + "INP", + # "INT", # I don't understand this one + "N", + "NPY", + # "PD", # Pandas specific + "PLC", + "PLE", + "PLR", + "PLW", + "PIE", + # "PT", # Too ingrained into unittest settings to go `pytest` style + # "PTH", # Too ingrained into `os` module over `Path` + # "PYI", # Specific to .pyi files for type stubs + "Q", + "PGH004", + "RET", + "RUF", + # "C90", # Too many methods marked as too complex + "S", + # "SLF", # Private member accessed (sure, it's python) + "SIM", + # "TRY", # Good in principle, would take a lot of work to statisfy + "T10", + "T20", + "TID", + "TCH", + "UP", + "N", + "W", + "YTT", +] + +ignore = [ + "D100", + "D101", # Missing docstring in public class + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic mthod + "D203", # 1 blank line required before class docstring + "D205", # 1 blank line between summary and description + "D401", # First line of docstring should be in imperative mood + "N806", # Variable X in function should be lowercase + "E731", # Do not assign a lambda expression, use a def + "A003", # Shadowing a builtin + "S101", # Use of assert detected. + "W292", # No newline at end of file + "PLC1901", # "" can be simplified to be falsey + "TCH003", # Move stdlib import into TYPE_CHECKING + "PLR0915", # Too many statements + "PLR0912", # Too many branches + "PLR0913", # Too many arguments to function call + "PLR2004", # Magic constants + "N999", # Invalid Module name + "N802", # Function name should be lowercase + # These tend to be lighweight and confuse pyright +] + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "docs", + # This is vendored, ignore it + "ConfigSpace/nx/**" +] + +# Exclude a variety of commonly ignored directories. +[tool.ruff.per-file-ignores] +"test/*.py" = [ + "S101", + "D102", + "D103", + "D404", + "ARG001", + "A002", + "INP001", + "A001", + "E501", + "ANN001", + "ANN201", + "FBT001", + "D100", + "PLR2004", + "TCH", + "N802", + "PLR0915", + "BLE001", +] +"setup.py" = ["D102"] +"__init__.py" = ["I002"] +"ConfigSpace/read_and_write/pcs_new.py" = [ + "N816", + "D103", + "PLW2901", +] +"ConfigSpace/read_and_write/pcs.py" = [ + "N816", + "D103", + "PLW2901", + "T201", +] + + +[tool.ruff.isort] +known-first-party = ["ConfigSpace"] +no-lines-before = ["future"] +required-imports = ["from __future__ import annotations"] +combine-as-imports = true +extra-standard-library = ["typing_extensions"] +force-wrap-aliases = true + +[tool.ruff.pydocstyle] +convention = "numpy" + + +[tool.mypy] +python_version = "3.7" +packages = ["ConfigSpace", "test"] + +show_error_codes = true + +warn_unused_configs = true # warn about unused [tool.mypy] lines + +follow_imports = "normal" # Type check top level api code we use from imports +ignore_missing_imports = false # prefer explicit ignores + +disallow_untyped_defs = true # All functions must have types +disallow_incomplete_defs = true # ...all types +disallow_untyped_decorators = false # ... but not decorators + +no_implicit_optional = true +check_untyped_defs = true + +warn_return_any = true + +[[tool.mypy.overrides]] +module = [ + "ConfigSpace.hyperparameters.*", + "ConfigSpace.conditions.*", + "ConfigSpace.forbidden.*", + "ConfigSpace.c_util.*", +] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["test.*"] +disallow_untyped_defs = false # Sometimes we just want to ignore verbose types +disallow_untyped_decorators = false # Test decorators are not properly typed +disallow_incomplete_defs = false # Sometimes we just want to ignore verbose types + +[[tool.mypy.overrides]] +module = ["ConfigSpace.nx.*"] # This is vendored, we ignore it +ignore_errors = true diff --git a/scripts/benchmark_sampling.py b/scripts/benchmark_sampling.py index cbf97d3c..71ed3bbc 100644 --- a/scripts/benchmark_sampling.py +++ b/scripts/benchmark_sampling.py @@ -2,6 +2,7 @@ # Average time sampling 100 configurations 0.0115247011185 # Average time retrieving a nearest neighbor 0.00251974105835 # Average time checking one configuration 0.000194481347553 +from __future__ import annotations import os import time @@ -9,21 +10,20 @@ import numpy as np import ConfigSpace -import ConfigSpace.util import ConfigSpace.read_and_write.pcs as pcs_parser - +import ConfigSpace.util n_configs = 100 def run_test(configuration_space_path): - if '2017_11' not in configuration_space_path: + if "2017_11" not in configuration_space_path: return with open(configuration_space_path) as fh: cs = pcs_parser.read(fh) - print('###') + print("###") print(configuration_space_path, flush=True) sampling_time = [] @@ -39,14 +39,16 @@ def run_test(configuration_space_path): sampling_time.append(end_time - start_time) for j, c in enumerate(configurations): - if i > 10: neighborhood = ConfigSpace.util.get_one_exchange_neighbourhood( - c, seed=i * j, num_neighbors=4) + c, + seed=i * j, + num_neighbors=4, + ) start_time = time.time() validation_time = [] - for shuffle, n in enumerate(neighborhood): + for _shuffle, n in enumerate(neighborhood): v_start_time = time.time() n.is_valid_configuration() v_end_time = time.time() @@ -55,19 +57,18 @@ def run_test(configuration_space_path): neighborhood_time.append(end_time - start_time - np.sum(validation_time)) validation_times.extend(validation_time) - print('Average time sampling %d configurations' % n_configs, np.mean(sampling_time)) - print('Average time retrieving a nearest neighbor', np.mean(neighborhood_time)) - print('Average time checking one configuration', np.mean(validation_times)) + print("Average time sampling %d configurations" % n_configs, np.mean(sampling_time)) + print("Average time retrieving a nearest neighbor", np.mean(neighborhood_time)) + print("Average time checking one configuration", np.mean(validation_times)) this_file = os.path.abspath(__file__) this_directory = os.path.dirname(this_file) -configuration_space_path = os.path.join(this_directory, '..', - "test", "test_searchspaces") +configuration_space_path = os.path.join(this_directory, "..", "test", "test_searchspaces") configuration_space_path = os.path.abspath(configuration_space_path) pcs_files = os.listdir(configuration_space_path) for pcs_file in pcs_files: - if '.pcs' in pcs_file: + if ".pcs" in pcs_file: full_path = os.path.join(configuration_space_path, pcs_file) run_test(full_path) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 68fb8e4a..00000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[metadata] -description-file = README.rst - -[build_ext] -inplace=1 - -[flake8] -max-line-length = 120 -exclude = build/*,scripts/*,docs/* diff --git a/setup.py b/setup.py index 3b82feb6..5eb9bee3 100644 --- a/setup.py +++ b/setup.py @@ -1,40 +1,38 @@ -"""Setup.py for ConfigSpace""" +"""Setup.py for ConfigSpace. -import os - -from setuptools import Extension, find_packages, setup -from setuptools.command.build_ext import build_ext -from Cython.Build import cythonize # must go after setuptools - - -# Helper functions -def read_file(fname): - """Get contents of file from the modules directory""" - return open(os.path.join(os.path.dirname(__file__), fname), encoding="utf-8").read() +# Profiling +Set the below flag to True to enable profiling of the code. This will cause some minor performance +overhead so it should only be used for debugging purposes. +Use [`py-spy`](https://github.com/benfred/py-spy) with [speedscope.app](https://www.speedscope.app/) +```bash +pip install py-spy +py-spy record --rate 800 --format speedscope --subprocesses --native -o profile.svg -- python