From 8d64a8db68c7788dbe31bd2bff8cff6b9ab29724 Mon Sep 17 00:00:00 2001 From: Eddie Bergman Date: Mon, 15 Aug 2022 08:42:45 +0200 Subject: [PATCH] Easy api (#255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add `py.typed` * Trim whitespace * Simple HP types interface * Add dictionary parsing * Add note about compiling from source * Specified space types * Added examples to readme and docs * Add `py.typed` * Trim whitespace * Simple HP types interface * Add dictionary parsing * Add note about compiling from source * Pre-commit fixes * Specified space types * Added examples to readme and docs * Add requirement for `typing_extensions` * Remove unknown `bibtex` lexer * Fix doctests * Add code doc for Int * Docs for Float's * Fix import bug * Add doc for categoricals * Add doc for distributions * Integrated AutoML Sphinx Theme * Add in some removed plugins * Add a Makefile * Increased veresion * Big ol doc update * Deflate API * Code fix * Comments * Remove references to `examples` folder for docs * Emphasise Ordinal and add more doc on diff for HP * Change from Int to Integer * Fix unused import * Fix doctests, make reproducible * Rename "master" to "main" around the place * Update changelog * Add tolerance to failing test * Update docs workflow properly * Revert back to v3 * Compile extra requirements into just "dev" * Fix Makefile * Fix install in pytest workflow * Export types * Make authors it's own file like version * Add some more indicators of Mac/Windows support * Fix doctest Co-authored-by: René Sass --- .github/workflows/docs.yml | 47 +-- .github/workflows/pre-commit.yaml | 6 +- .github/workflows/pytest.yml | 8 +- .github/workflows/release.yml | 7 +- .gitignore | 5 +- .pre-commit-config.yaml | 6 +- ConfigSpace/__authors__.py | 12 + ConfigSpace/__init__.py | 90 +++-- ConfigSpace/__version__.py | 2 +- ConfigSpace/api/__init__.py | 16 + ConfigSpace/api/distributions.py | 49 +++ ConfigSpace/api/types/__init__.py | 5 + ConfigSpace/api/types/categorical.py | 140 +++++++ ConfigSpace/api/types/float.py | 181 +++++++++ ConfigSpace/api/types/integer.py | 193 +++++++++ ConfigSpace/conditions.pyx | 207 +++++----- ConfigSpace/configuration_space.pyx | 144 +++++-- ConfigSpace/forbidden.pyx | 142 +++---- ConfigSpace/hyperparameters.pyx | 365 ++++++++---------- ConfigSpace/nx/algorithms/dag.py | 7 +- .../py.typed | 0 ConfigSpace/read_and_write/json.py | 37 +- ConfigSpace/read_and_write/pcs.py | 39 +- MANIFEST.in | 1 + Makefile | 83 ++++ README.md | 23 +- changelog.md | 27 +- docs/Makefile | 44 +-- docs/api/conditions.rst | 53 +++ docs/api/configuration.rst | 5 + docs/api/configurationspace.rst | 5 + docs/api/forbidden_clauses.rst | 26 ++ docs/api/hyperparameters.rst | 115 ++++++ docs/api/index.rst | 14 + docs/api/serialization.rst | 32 ++ docs/api/utils.rst | 10 + docs/conf.py | 39 ++ docs/{source/User-Guide.rst => guide.rst} | 239 ++++++------ docs/images/logo.png | Bin 0 -> 89809 bytes docs/index.html | 1 - docs/index.rst | 137 +++++++ docs/make.bat | 36 -- docs/quickstart.rst | 228 +++++++++++ docs/source/API-Doc.rst | 193 --------- docs/source/_templates/layout.html | 23 -- docs/source/_templates/navbar.html | 51 --- docs/source/conf.py | 242 ------------ docs/source/index.rst | 101 ----- docs/source/quickstart.rst | 73 ---- setup.py | 120 +++--- .../test_api/__init__.py | 0 test/test_api/test_hp_construction.py | 252 ++++++++++++ test/test_configspace_from_dict.py | 115 ++++++ test/test_hyperparameters.py | 6 +- 54 files changed, 2508 insertions(+), 1494 deletions(-) create mode 100644 ConfigSpace/__authors__.py create mode 100644 ConfigSpace/api/__init__.py create mode 100644 ConfigSpace/api/distributions.py create mode 100644 ConfigSpace/api/types/__init__.py create mode 100644 ConfigSpace/api/types/categorical.py create mode 100644 ConfigSpace/api/types/float.py create mode 100644 ConfigSpace/api/types/integer.py rename docs/source/_templates/navbarsearchbox.html => ConfigSpace/py.typed (100%) create mode 100644 Makefile create mode 100644 docs/api/conditions.rst create mode 100644 docs/api/configuration.rst create mode 100644 docs/api/configurationspace.rst create mode 100644 docs/api/forbidden_clauses.rst create mode 100644 docs/api/hyperparameters.rst create mode 100644 docs/api/index.rst create mode 100644 docs/api/serialization.rst create mode 100644 docs/api/utils.rst create mode 100644 docs/conf.py rename docs/{source/User-Guide.rst => guide.rst} (60%) create mode 100644 docs/images/logo.png delete mode 100644 docs/index.html create mode 100644 docs/index.rst delete mode 100644 docs/make.bat create mode 100644 docs/quickstart.rst delete mode 100644 docs/source/API-Doc.rst delete mode 100644 docs/source/_templates/layout.html delete mode 100644 docs/source/_templates/navbar.html delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/index.rst delete mode 100644 docs/source/quickstart.rst rename docs/source/_templates/searchbox.html => test/test_api/__init__.py (100%) create mode 100644 test/test_api/test_hp_construction.py create mode 100644 test/test_configspace_from_dict.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d152420a..4ed7856d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,63 +1,64 @@ -name: Docs +name: docs on: - # Trigger manually workflow_dispatch: - # Trigger on any push to the master + # Trigger on any push to the main push: branches: - - master + - main + - development - # Trigger on any push to a PR that targets master + # Trigger on any push to a PR that targets main pull_request: branches: - - master + - main + - development -jobs: +permissions: + contents: write + +env: + name: "ConfigSpace" +jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: - python-version: 3.8 + python-version: "3.8" - name: Install dependencies run: | - pip install -e .[docs,examples,examples_unix] + pip install ".[dev]" - name: Make docs run: | - cd docs - make html - - - name: Run doctests - run: | - cd docs - make doctest + make clean + make docs - name: Pull latest gh-pages - if: (contains(github.ref, 'master')) && github.event_name == 'push' + if: (contains(github.ref, 'develop') || contains(github.ref, 'main')) && github.event_name == 'push' run: | cd .. - git clone https://github.com/automl/ConfigSpace.git --branch gh-pages --single-branch gh-pages + git clone https://github.com/${{ github.repository }}.git --branch gh-pages --single-branch gh-pages - name: Copy new docs into gh-pages - if: (contains(github.ref, 'develop') || contains(github.ref, 'master')) && github.event_name == 'push' + if: (contains(github.ref, 'develop') || contains(github.ref, 'main')) && github.event_name == 'push' run: | branch_name=${GITHUB_REF##*/} cd ../gh-pages rm -rf $branch_name - cp -r ../ConfigSpace/docs/build/html $branch_name + cp -r ../${{ env.name }}/docs/build/html $branch_name - name: Push to gh-pages - if: (contains(github.ref, 'master')) && github.event_name == 'push' + if: (contains(github.ref, 'develop') || contains(github.ref, 'main')) && github.event_name == 'push' run: | last_commit=$(git log --pretty=format:"%an: %s") cd ../gh-pages diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 34f23949..69b7edf0 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -8,12 +8,14 @@ on: # Trigger on any push to the master push: branches: - - master + - main + - development # Trigger on any push to a PR that targets master pull_request: branches: - - master + - main + - development jobs: diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index dcd61723..18e51f3f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -7,12 +7,14 @@ on: # Triggers with push to master push: branches: - - master + - main + - development # Triggers with push to a pr aimed at master pull_request: branches: - - master + - main + - development schedule: # Every day at 7AM UTC @@ -22,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: >- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57e0ed3e..e34ab6c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ on: push: branches: - - master + - main # Release branches - "[0-9]+.[0-9]+.X" @@ -54,7 +54,7 @@ env: test-dir: test test-reqs: "pytest" test-cmd: "pytest -v" - extra-requires: "[test]" + extra-requires: "[dev]" jobs: @@ -74,11 +74,10 @@ jobs: # Not supported by numpy - system: "musllinux" - # Scipy doesn't have a wheel for cp310 i686 + # Scipy doesn't have a wheel for cp310 i686 - py: cp310 arch: "i686" - steps: - name: Checkout ${{ env.package-name }} uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index d812bf16..8bf83c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ +.DS_Store + # Documentation docs/build/* +docs/examples/* *.py[cod] @@ -68,4 +71,4 @@ prof/ .vscode # Running pre-commit seems to generate these -.mypy_cache +.mypy_cache \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2dd2c63..3d1d9a5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,19 @@ repos: - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.761 + rev: v0.961 hooks: - id: mypy args: [--show-error-codes, --ignore-missing-imports, --follow-imports, skip] name: mypy ConfigSpace files: ConfigSpace + - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 4.0.1 hooks: - id: flake8 name: flake8 ConfigSpace files: ConfigSpace + - id: flake8 name: flake8 test files: test diff --git a/ConfigSpace/__authors__.py b/ConfigSpace/__authors__.py new file mode 100644 index 00000000..caa17d5b --- /dev/null +++ b/ConfigSpace/__authors__.py @@ -0,0 +1,12 @@ +__authors__ = [ + "Matthias Feurer", + "Katharina Eggensperger", + "Syed Mohsin Ali", + "Christina Hernandez Wunsch", + "Julien-Charles Levesque", + "Jost Tobias Springenberg", + "Philipp Mueller", + "Marius Lindauer", + "Jorn Tuyls", + "Eddie Bergman", +] diff --git a/ConfigSpace/__init__.py b/ConfigSpace/__init__.py index 74e68396..525b50ba 100644 --- a/ConfigSpace/__init__.py +++ b/ConfigSpace/__init__.py @@ -27,32 +27,68 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from ConfigSpace.__version__ import __version__ -__authors__ = [ - "Matthias Feurer", "Katharina Eggensperger", "Syed Mohsin Ali", - "Christina Hernandez Wunsch", "Julien-Charles Levesque", - "Jost Tobias Springenberg", "Philipp Mueller", "Marius Lindauer", - "Jorn Tuyls" -] +from ConfigSpace.__authors__ import __authors__ -from ConfigSpace.configuration_space import Configuration, \ - ConfigurationSpace -from ConfigSpace.hyperparameters import CategoricalHyperparameter, \ - UniformFloatHyperparameter, UniformIntegerHyperparameter, Constant, \ - UnParametrizedHyperparameter, OrdinalHyperparameter -from ConfigSpace.conditions import AndConjunction, OrConjunction, \ - EqualsCondition, NotEqualsCondition, InCondition, GreaterThanCondition, LessThanCondition -from ConfigSpace.forbidden import ForbiddenAndConjunction, \ - ForbiddenEqualsClause, ForbiddenInClause, ForbiddenLessThanRelation, ForbiddenEqualsRelation, \ - ForbiddenGreaterThanRelation +import ConfigSpace.api.distributions as distributions +import ConfigSpace.api.types as types +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) -__all__ = ["__version__", "Configuration", "ConfigurationSpace", - "CategoricalHyperparameter", "UniformFloatHyperparameter", - "UniformIntegerHyperparameter", "Constant", - "UnParametrizedHyperparameter", "OrdinalHyperparameter", - "AndConjunction", "OrConjunction", - "EqualsCondition", "NotEqualsCondition", - "InCondition", "GreaterThanCondition", - "LessThanCondition", "ForbiddenAndConjunction", - "ForbiddenEqualsClause", "ForbiddenInClause", - "ForbiddenLessThanRelation", "ForbiddenEqualsRelation", - "ForbiddenGreaterThanRelation"] +__all__ = [ + "__authors__", + "__version__", + "Configuration", + "ConfigurationSpace", + "CategoricalHyperparameter", + "UniformFloatHyperparameter", + "UniformIntegerHyperparameter", + "BetaFloatHyperparameter", + "BetaIntegerHyperparameter", + "NormalFloatHyperparameter", + "NormalIntegerHyperparameter", + "Constant", + "UnParametrizedHyperparameter", + "OrdinalHyperparameter", + "AndConjunction", + "OrConjunction", + "EqualsCondition", + "NotEqualsCondition", + "InCondition", + "GreaterThanCondition", + "LessThanCondition", + "ForbiddenAndConjunction", + "ForbiddenEqualsClause", + "ForbiddenInClause", + "ForbiddenLessThanRelation", + "ForbiddenEqualsRelation", + "ForbiddenGreaterThanRelation", + "Beta", + "Categorical", + "Distribution", + "Float", + "Integer", + "Normal", + "Uniform", + "distributions", + "types", +] diff --git a/ConfigSpace/__version__.py b/ConfigSpace/__version__.py index a3e938c6..ee3313ca 100644 --- a/ConfigSpace/__version__.py +++ b/ConfigSpace/__version__.py @@ -1,4 +1,4 @@ """Version information.""" # The following line *must* be the last in the module, exactly as formatted: -__version__ = "0.5.0" +__version__ = "0.6.0" diff --git a/ConfigSpace/api/__init__.py b/ConfigSpace/api/__init__.py new file mode 100644 index 00000000..fd1b6507 --- /dev/null +++ b/ConfigSpace/api/__init__.py @@ -0,0 +1,16 @@ +import ConfigSpace.api.distributions as distributions +import ConfigSpace.api.types as types +from ConfigSpace.api.distributions import Beta, Distribution, Normal, Uniform +from ConfigSpace.api.types import Categorical, Float, Integer + +__all__ = [ + "types", + "distributions", + "Beta", + "Distribution", + "Normal", + "Uniform", + "Categorical", + "Float", + "Integer", +] diff --git a/ConfigSpace/api/distributions.py b/ConfigSpace/api/distributions.py new file mode 100644 index 00000000..028c8a5a --- /dev/null +++ b/ConfigSpace/api/distributions.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass + + +@dataclass +class Distribution: + """Base distribution type""" + + pass + + +@dataclass +class Uniform(Distribution): + """A uniform distribution""" + + pass + + +@dataclass +class Normal(Distribution): + """Represents a normal distribution. + + Parameters + ---------- + mu: float + The mean of the distribution + + sigma: float + The standard deviation of the float + """ + + mu: float + sigma: float + + +@dataclass +class Beta(Distribution): + """Represents a beta distribution. + + Parameters + ---------- + alpha: float + The alpha parameter of a beta distribution + + beta: float + The beta parameter of a beta distribution + """ + + alpha: float + beta: float diff --git a/ConfigSpace/api/types/__init__.py b/ConfigSpace/api/types/__init__.py new file mode 100644 index 00000000..28259e38 --- /dev/null +++ b/ConfigSpace/api/types/__init__.py @@ -0,0 +1,5 @@ +from ConfigSpace.api.types.categorical import Categorical +from ConfigSpace.api.types.float import Float +from ConfigSpace.api.types.integer import Integer + +__all__ = ["Categorical", "Float", "Integer"] diff --git a/ConfigSpace/api/types/categorical.py b/ConfigSpace/api/types/categorical.py new file mode 100644 index 00000000..da095eef --- /dev/null +++ b/ConfigSpace/api/types/categorical.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from typing import Sequence, Union, overload + +from typing_extensions import (Literal, # Move to `typing` when 3.8 minimum + TypeAlias) + +from ConfigSpace.hyperparameters import (CategoricalHyperparameter, + OrdinalHyperparameter) + +# We only accept these types in `items` +T: TypeAlias = Union[str, int, float] + + +# ordered False -> CategoricalHyperparameter +@overload +def Categorical( + name: str, + items: Sequence[T], + *, + default: T | None = None, + weights: Sequence[float] | None = None, + ordered: Literal[False], + meta: dict | None = None, +) -> CategoricalHyperparameter: + ... + + +# ordered True -> OrdinalHyperparameter +@overload +def Categorical( + name: str, + items: Sequence[T], + *, + default: T | None = None, + weights: Sequence[float] | None = None, + ordered: Literal[True], + meta: dict | None = None, +) -> OrdinalHyperparameter: + ... + + +# ordered bool (unknown) -> Either +@overload +def Categorical( + name: str, + items: Sequence[T], + *, + default: T | None = None, + weights: Sequence[float] | None = None, + ordered: bool = ..., + meta: dict | None = None, +) -> CategoricalHyperparameter | OrdinalHyperparameter: + ... + + +def Categorical( + name: str, + items: Sequence[T], + *, + default: T | None = None, + weights: Sequence[float] | None = None, + ordered: bool = False, + meta: dict | None = None, +) -> CategoricalHyperparameter | OrdinalHyperparameter: + """Creates a Categorical Hyperparameter. + + CategoricalHyperparameter's can be used to represent a discrete + choice. Optionally, you can specify that these values are also ordered in + some manner, e.g. ``["small", "medium", "large"]``. + + .. code:: python + + # A simple categorical hyperparameter + c = Categorical("animals", ["cat", "dog", "mouse"]) + + # With a default + c = Categorical("animals", ["cat", "dog", "mouse"], default="mouse") + + # Make them weighted + c = Categorical("animals", ["cat", "dog", "mouse"], weights=[0.1, 0.8, 3.14]) + + # Specify it's an OrdinalHyperparameter (ordered categories) + # ... note that you can't apply weights to an Ordinal + o = Categorical("size", ["small", "medium", "large"], ordered=True) + + # Add some meta information for your own tracking + c = Categorical("animals", ["cat", "dog", "mouse"], meta={"use": "Favourite Animal"}) + + Note + ---- + ``Categorical`` is actually a function, please use the corresponding return types if + doing an `isinstance(param, type)` check with either + :py:class:`~ConfigSpace.hyperparameters.CategoricalHyperparameter` + and/or :py:class:`~ConfigSpace.hyperparameters.OrdinalHyperparameter`. + + Parameters + ---------- + name: str + The name of the hyperparameter + + items: Sequence[T], + A list of items to put in the category. Note that there are limitations: + + * Can't use `None`, use a string "None" instead and convert as required. + * Can't have duplicate categories, use weights if required. + + default: T | None = None + The default value of the categorical hyperparameter + + weights: Sequence[float] | None = None + The weights to apply to each categorical. Each item will be sampled according + to these weights. + + ordered: bool = False + Whether the categorical is ordered or not. If True, this will return an + :py:class:`OrdinalHyperparameter`, otherwise it remain a + :py:class:`CategoricalHyperparameter`. + + meta: dict | None = None + Any additional meta information you would like to store along with the hyperparamter. + """ + if ordered and weights is not None: + raise ValueError("Can't apply `weights` to `ordered` Categorical") + + if ordered: + return OrdinalHyperparameter( + name=name, + sequence=items, + default_value=default, + meta=meta, + ) + else: + 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 new file mode 100644 index 00000000..31b5117e --- /dev/null +++ b/ConfigSpace/api/types/float.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from typing import overload + +from ConfigSpace.api.distributions import Beta, Distribution, Normal, Uniform +from ConfigSpace.hyperparameters import (BetaFloatHyperparameter, + NormalFloatHyperparameter, + UniformFloatHyperparameter) + + +# Uniform | None -> UniformFloatHyperparameter +@overload +def Float( + name: str, + bounds: tuple[float, float] | None = ..., + *, + distribution: Uniform | None = ..., + default: float | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> UniformFloatHyperparameter: + ... + + +# Normal -> NormalFloatHyperparameter +@overload +def Float( + name: str, + bounds: tuple[float, float] | None = ..., + *, + distribution: Normal, + default: float | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> NormalFloatHyperparameter: + ... + + +# Beta -> BetaFloatHyperparameter +@overload +def Float( + name: str, + bounds: tuple[float, float] | None = ..., + *, + distribution: Beta, + default: float | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> BetaFloatHyperparameter: + ... + + +def Float( + name: str, + bounds: tuple[float, float] | None = None, + *, + distribution: Distribution | None = None, + default: float | None = None, + q: int | None = None, + log: bool = False, + meta: dict | None = None, +) -> UniformFloatHyperparameter | NormalFloatHyperparameter | BetaFloatHyperparameter: + """Create a FloatHyperparameter. + + .. code:: python + + # Uniformly distributed + Float("a", (1, 10)) + Float("a", (1, 10), distribution=Uniform()) + + # Normally distributed at 2 with std 3 + Float("b", distribution=Normal(2, 3)) + Float("b", (0, 5), distribution=Normal(2, 3)) # ... bounded + + # Beta distributed with alpha 1 and beta 2 + Float("c", distribution=Beta(1, 2)) + Float("c", (0, 3), distribution=Beta(1, 2)) # ... bounded + + # Give it a default value + Float("a", (1, 10), default=4.3) + + # Sample on a log scale + Float("a", (1, 100), log=True) + + # Quantized into three brackets + Float("a", (1, 10), q=3) + + # Add meta info to the param + Float("a", (1.0, 10), meta={"use": "For counting chickens"}) + + Note + ---- + `Float` is actually a function, please use the corresponding return types if + doing an `isinstance(param, type)` check and not `Float`. + + Parameters + ---------- + name : str + The name to give to this hyperparameter + + bounds : tuple[float, float] | None = None + The bounds to give to the float. Note that by default, this is required + for Uniform distribution, which is the default distribution + + distribution : Uniform | Normal | Beta, = Uniform + The distribution to use for the hyperparameter. See above + + default : float | None = None + The default value to give to the hyperparameter. + + q : float | None = None + The quantization factor, must evenly divide the boundaries. + + Note + ---- + Quantization points act are not equal and require experimentation + to be certain about + + * https://github.com/automl/ConfigSpace/issues/264 + + log : bool = False + Whether to this parameter lives on a log scale + + meta : dict | None = None + Any meta information you want to associate with this parameter + + Returns + ------- + UniformFloatHyperparameter | NormalFloatHyperparameter | BetaFloatHyperparameter + Returns the corresponding hyperparameter type + """ + if distribution is None: + distribution = Uniform() + + if bounds is None and isinstance(distribution, Uniform): + raise ValueError("`bounds` must be specifed for Uniform distribution") + + if bounds is None: + lower, upper = (None, None) + else: + lower, upper = bounds + + if isinstance(distribution, Uniform): + return UniformFloatHyperparameter( + name=name, + lower=lower, + upper=upper, + default_value=default, + q=q, + log=log, + meta=meta, + ) + elif isinstance(distribution, Normal): + return NormalFloatHyperparameter( + name=name, + lower=lower, + upper=upper, + default_value=default, + mu=distribution.mu, + sigma=distribution.sigma, + q=q, + log=log, + meta=meta, + ) + elif isinstance(distribution, Beta): + return BetaFloatHyperparameter( + name=name, + lower=lower, + upper=upper, + alpha=distribution.alpha, + beta=distribution.beta, + default_value=default, + q=q, + log=log, + meta=meta, + ) + else: + raise ValueError(f"Unknown distribution type {type(distribution)}") diff --git a/ConfigSpace/api/types/integer.py b/ConfigSpace/api/types/integer.py new file mode 100644 index 00000000..539c6650 --- /dev/null +++ b/ConfigSpace/api/types/integer.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +from typing import overload + +from ConfigSpace.api.distributions import Beta, Distribution, Normal, Uniform +from ConfigSpace.hyperparameters import ( + BetaIntegerHyperparameter, + NormalIntegerHyperparameter, + UniformIntegerHyperparameter, +) + + +# Uniform | None -> UniformIntegerHyperparameter +@overload +def Integer( + name: str, + bounds: tuple[int, int] | None = ..., + *, + distribution: Uniform | None = ..., + default: int | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> UniformIntegerHyperparameter: + ... + + +# Normal -> NormalIntegerHyperparameter +@overload +def Integer( + name: str, + bounds: tuple[int, int] | None = ..., + *, + distribution: Normal, + default: int | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> NormalIntegerHyperparameter: + ... + + +# Beta -> BetaIntegerHyperparameter +@overload +def Integer( + name: str, + bounds: tuple[int, int] | None = ..., + *, + distribution: Beta, + default: int | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> BetaIntegerHyperparameter: + ... + + +def Integer( + name: str, + bounds: tuple[int, int] | None = None, + *, + distribution: Distribution | None = None, + default: int | None = None, + q: int | None = None, + log: bool = False, + meta: dict | None = None, +) -> UniformIntegerHyperparameter | NormalIntegerHyperparameter | BetaIntegerHyperparameter: + """Create an IntegerHyperparameter. + + .. code:: python + + # Uniformly distributed + Integer("a", (1, 10)) + Integer("a", (1, 10), distribution=Uniform()) + + # Normally distributed at 2 with std 3 + Integer("b", distribution=Normal(2, 3)) + Integer("b", (0, 5), distribution=Normal(2, 3)) # ... bounded + + # Beta distributed with alpha 1 and beta 2 + Integer("c", distribution=Beta(1, 2)) + Integer("c", (0, 3), distribution=Beta(1, 2)) # ... bounded + + # Give it a default value + Integer("a", (1, 10), default=4) + + # Sample on a log scale + Integer("a", (1, 100), log=True) + + # Quantized into three brackets + Integer("a", (1, 10), q=3) + + # Add meta info to the param + Integer("a", (1, 10), meta={"use": "For counting chickens"}) + + Note + ---- + `Integer` is actually a function, please use the corresponding return types if + doing an `isinstance(param, type)` check and not `Integer`. + + Parameters + ---------- + name : str + The name to give to this hyperparameter + + bounds : tuple[int, int] | None = None + The bounds to give to the integer. Note that by default, this is required + for Uniform distribution, which is the default distribution + + distribution : Uniform | Normal | Beta, = Uniform + The distribution to use for the hyperparameter. See above + + default : int | None = None + The default value to give to the hyperparameter. + + q : int | None = None + The quantization factor, must evenly divide the boundaries. + Sampled values will be + + .. code:: + + full range + 1 4 7 10 + |--------------| + | | | | q = 3 + + All samples here will then be in {1, 4, 7, 10} + + Note + ---- + Quantization points act are not equal and require experimentation + to be certain about + + * https://github.com/automl/ConfigSpace/issues/264 + + log : bool = False + Whether to this parameter lives on a log scale + + meta : dict | None = None + Any meta information you want to associate with this parameter + + Returns + ------- + UniformIntegerHyperparameter | NormalIntegerHyperparameter | BetaIntegerHyperparameter + Returns the corresponding hyperparameter type + """ + if distribution is None: + distribution = Uniform() + + if bounds is None and isinstance(distribution, Uniform): + raise ValueError("`bounds` must be specifed for Uniform distribution") + + if bounds is None: + lower, upper = (None, None) + else: + lower, upper = bounds + + if isinstance(distribution, Uniform): + return UniformIntegerHyperparameter( + name=name, + lower=lower, + upper=upper, + q=q, + log=log, + default_value=default, + meta=meta, + ) + elif isinstance(distribution, Normal): + return NormalIntegerHyperparameter( + name=name, + lower=lower, + upper=upper, + q=q, + log=log, + default_value=default, + meta=meta, + mu=distribution.mu, + sigma=distribution.sigma, + ) + elif isinstance(distribution, Beta): + return BetaIntegerHyperparameter( + name=name, + lower=lower, + upper=upper, + q=q, + log=log, + default_value=default, + meta=meta, + alpha=distribution.alpha, + beta=distribution.beta, + ) + else: + raise ValueError(f"Unknown distribution type {type(distribution)}") diff --git a/ConfigSpace/conditions.pyx b/ConfigSpace/conditions.pyx index 24e266fb..08f7add2 100644 --- a/ConfigSpace/conditions.pyx +++ b/ConfigSpace/conditions.pyx @@ -40,7 +40,6 @@ from libc.stdlib cimport malloc, free import numpy as np -from ConfigSpace.hyperparameters import NumericalHyperparameter, OrdinalHyperparameter from ConfigSpace.hyperparameters cimport Hyperparameter cimport numpy as np @@ -114,9 +113,9 @@ cdef class AbstractCondition(ConditionComponent): Additionally, it defines the __ne__() as stated in the documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result + By default, object implements __eq__() by using is, returning NotImplemented + in the case of a false comparison: True if x is y else NotImplemented. + For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ @@ -169,24 +168,18 @@ cdef class EqualsCondition(AbstractCondition): def __init__(self, child: Hyperparameter, parent: Hyperparameter, value: Union[str, float, int]) -> None: - """ - Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter + """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *equal* to ``value``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace() - >>> a = CSH.CategoricalHyperparameter('a', choices=[1, 2, 3]) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] + Make *b* an active hyperparameter if *a* has the value 1 - make *b* an active hyperparameter if *a* has the value 1 - - >>> cond = CS.EqualsCondition(b, a, 1) + >>> from ConfigSpace import ConfigurationSpace, EqualsCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": [1, 2, 3], + ... "b": (1.0, 8.0) + ... }) + >>> cond = EqualsCondition(cs['b'], cs['a'], 1) >>> cs.add_condition(cond) b | a == 1 @@ -245,24 +238,18 @@ cdef class EqualsCondition(AbstractCondition): cdef class NotEqualsCondition(AbstractCondition): def __init__(self, child: Hyperparameter, parent: Hyperparameter, value: Union[str, float, int]) -> None: - """ - Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter + """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *not equal* to ``value``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace() - >>> a = CSH.CategoricalHyperparameter('a', choices=[1, 2, 3]) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - make *b* an active hyperparameter if *a* has **not** the value 1 + Make *b* an active hyperparameter if *a* has **not** the value 1 - >>> cond = CS.NotEqualsCondition(b, a, 1) + >>> from ConfigSpace import ConfigurationSpace, NotEqualsCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": [1, 2, 3], + ... "b": (1.0, 8.0) + ... }) + >>> cond = NotEqualsCondition(cs['b'], cs['a'], 1) >>> cs.add_condition(cond) b | a != 1 @@ -276,7 +263,6 @@ cdef class NotEqualsCondition(AbstractCondition): *not equal condition* value : str, float, int Value, which the parent is compared to - """ super(NotEqualsCondition, self).__init__(child, parent) if not parent.is_legal(value): @@ -326,22 +312,17 @@ cdef class LessThanCondition(AbstractCondition): Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *less than* ``value``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace() - >>> a = CSH.UniformFloatHyperparameter('a', lower=0., upper=10.) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: UniformFloat, Range: [0.0, 10.0], Default: 5.0, b, Type: ...] + Make *b* an active hyperparameter if *a* is less than 5 - make *b* an active hyperparameter if *a* is less than 5 - - >>> cond = CS.LessThanCondition(b, a, 5.) + >>> from ConfigSpace import ConfigurationSpace, LessThanCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": (0, 10), + ... "b": (1.0, 8.0) + ... }) + >>> cond = LessThanCondition(cs['b'], cs['a'], 5) >>> cs.add_condition(cond) - b | a < 5.0 + b | a < 5 Parameters ---------- @@ -352,9 +333,7 @@ cdef class LessThanCondition(AbstractCondition): The hyperparameter, which has to satisfy the *LessThanCondition* value : str, float, int Value, which the parent is compared to - """ - super(LessThanCondition, self).__init__(child, parent) self.parent.allow_greater_less_comparison() if not parent.is_legal(value): @@ -404,22 +383,17 @@ cdef class GreaterThanCondition(AbstractCondition): Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *greater than* ``value``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.UniformFloatHyperparameter('a', lower=0., upper=10.) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: UniformFloat, Range: [0.0, 10.0], Default: 5.0, b, Type: ...] - - make *b* an active hyperparameter if *a* is greater than 5 + Make *b* an active hyperparameter if *a* is greater than 5 - >>> cond = CS.GreaterThanCondition(b, a, 5.) + >>> from ConfigSpace import ConfigurationSpace, GreaterThanCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": (0, 10), + ... "b": (1.0, 8.0) + ... }) + >>> cond = GreaterThanCondition(cs['b'], cs['a'], 5) >>> cs.add_condition(cond) - b | a > 5.0 + b | a > 5 Parameters ---------- @@ -430,7 +404,6 @@ cdef class GreaterThanCondition(AbstractCondition): The hyperparameter, which has to satisfy the *GreaterThanCondition* value : str, float, int Value, which the parent is compared to - """ super(GreaterThanCondition, self).__init__(child, parent) @@ -484,20 +457,15 @@ cdef class InCondition(AbstractCondition): Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *in* a set of ``values``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.UniformIntegerHyperparameter('a', lower=0, upper=10) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: UniformInteger, Range: [0, 10], Default: 5, b, Type: ...] - make *b* an active hyperparameter if *a* is in the set [1, 2, 3, 4] - >>> cond = CS.InCondition(b, a, [1, 2, 3, 4]) + >>> from ConfigSpace import ConfigurationSpace, InCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": (0, 10), + ... "b": (1.0, 8.0) + ... }) + >>> cond = InCondition(cs['b'], cs['a'], [1, 2, 3, 4]) >>> cs.add_condition(cond) b | a in {1, 2, 3, 4} @@ -567,9 +535,9 @@ cdef class AbstractConjunction(ConditionComponent): Additionally, it defines the __ne__() as stated in the documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result + By default, object implements __eq__() by using is, returning NotImplemented + in the case of a false comparison: True if x is y else NotImplemented. + For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ @@ -676,37 +644,35 @@ cdef class AndConjunction(AbstractConjunction): # TODO: test if an AndConjunction results in an illegal state or a # Tautology! -> SAT solver def __init__(self, *args: AbstractCondition) -> None: - """ - By using the *AndConjunction*, constraints can easily be connected. - - Example - ------- - The following example shows how two constraints with an - *AndConjunction* can be combined. - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.UniformIntegerHyperparameter('a', lower=5, upper=15) - >>> b = CSH.UniformIntegerHyperparameter('b', lower=0, upper=10) - >>> c = CSH.UniformFloatHyperparameter('c', lower=0., upper=1.) - >>> cs.add_hyperparameters([a, b, c]) - [a, Type: UniformInteger, Range: [5, 15], Default: 10, b, Type: ...] - - >>> less_cond = CS.LessThanCondition(c, a, 10) - >>> greater_cond = CS.GreaterThanCondition(c, b, 5) - >>> cs.add_condition(CS.AndConjunction(less_cond, greater_cond)) + """By using the *AndConjunction*, constraints can easily be connected. + + The following example shows how two constraints with an *AndConjunction* + can be combined. + + >>> from ConfigSpace import ( + ... ConfigurationSpace, + ... LessThanCondition, + ... GreaterThanCondition, + ... AndConjunction + ... ) + >>> + >>> cs = ConfigurationSpace({ + ... "a": (5, 15), + ... "b": (0, 10), + ... "c": (0.0, 1.0) + ... }) + >>> less_cond = LessThanCondition(cs['c'], cs['a'], 10) + >>> greater_cond = GreaterThanCondition(cs['c'], cs['b'], 5) + >>> cs.add_condition(AndConjunction(less_cond, greater_cond)) (c | a < 10 && c | b > 5) Parameters ---------- *args : :ref:`Conditions` conditions, which will be combined with an *AndConjunction* - """ if len(args) < 2: - raise ValueError("AndConjunction must at least have two " - "Conditions.") + raise ValueError("AndConjunction must at least have two Conditions.") super(AndConjunction, self).__init__(*args) def __repr__(self) -> str: @@ -744,21 +710,21 @@ cdef class OrConjunction(AbstractConjunction): Similar to the *AndConjunction*, constraints can be combined by using the *OrConjunction*. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.UniformIntegerHyperparameter('a', lower=5, upper=15) - >>> b = CSH.UniformIntegerHyperparameter('b', lower=0, upper=10) - >>> c = CSH.UniformFloatHyperparameter('c', lower=0., upper=1.) - >>> cs.add_hyperparameters([a, b, c]) - [a, Type: UniformInteger, Range: [5, 15], Default: 10, b, Type: ...] - - >>> less_cond = CS.LessThanCondition(c, a, 10) - >>> greater_cond = CS.GreaterThanCondition(c, b, 5) - >>> cs.add_condition(CS.OrConjunction(less_cond, greater_cond)) + >>> from ConfigSpace import ( + ... ConfigurationSpace, + ... LessThanCondition, + ... GreaterThanCondition, + ... OrConjunction + ... ) + >>> + >>> cs = ConfigurationSpace({ + ... "a": (5, 15), + ... "b": (0, 10), + ... "c": (0.0, 1.0) + ... }) + >>> less_cond = LessThanCondition(cs['c'], cs['a'], 10) + >>> greater_cond = GreaterThanCondition(cs['c'], cs['b'], 5) + >>> cs.add_condition(OrConjunction(less_cond, greater_cond)) (c | a < 10 || c | b > 5) Parameters @@ -767,8 +733,7 @@ cdef class OrConjunction(AbstractConjunction): conditions, which will be combined with an *OrConjunction* """ if len(args) < 2: - raise ValueError("OrConjunction must at least have two " - "Conditions.") + raise ValueError("OrConjunction must at least have two Conditions.") super(OrConjunction, self).__init__(*args) def __repr__(self) -> str: diff --git a/ConfigSpace/configuration_space.pyx b/ConfigSpace/configuration_space.pyx index bd1e806d..fcfc62d0 100644 --- a/ConfigSpace/configuration_space.pyx +++ b/ConfigSpace/configuration_space.pyx @@ -43,7 +43,8 @@ from ConfigSpace.hyperparameters import ( FloatHyperparameter, UniformFloatHyperparameter, UniformIntegerHyperparameter, - OrdinalHyperparameter + CategoricalHyperparameter, + OrdinalHyperparameter, ) from ConfigSpace.conditions import ( ConditionComponent, @@ -69,13 +70,14 @@ class ConfigurationSpace(collections.abc.Mapping): # TODO add a method to add whole configuration spaces as a child "tree" def __init__( - self, - name: Union[str, None] = None, - seed: Union[int, None] = None, - meta: Optional[Dict] = None, + 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 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 @@ -85,14 +87,36 @@ class ConfigurationSpace(collections.abc.Mapping): Parameters ---------- - name : str, optional - Name of the configuration space + 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 @@ -125,6 +149,56 @@ class ConfigurationSpace(collections.abc.Mapping): 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` @@ -626,7 +700,7 @@ class ConfigurationSpace(collections.abc.Mapping): prefix: str, configuration_space: 'ConfigurationSpace', delimiter: str = ":", - parent_hyperparameter: Hyperparameter = None + parent_hyperparameter: dict = None ) -> 'ConfigurationSpace': """ Combine two configuration space by adding one the other configuration @@ -642,9 +716,11 @@ class ConfigurationSpace(collections.abc.Mapping): The configuration space which should be added delimiter : str, optional Defaults to ':' - parent_hyperparameter : :ref:`Hyperparameters`, optional + parent_hyperparameter : dict | None = None Adds for each new hyperparameter the condition, that - ``parent_hyperparameter`` is active + ``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 ------- @@ -1370,9 +1446,9 @@ class ConfigurationSpace(collections.abc.Mapping): 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. - + Non-uniform hyperpararmeters are replaced with uniform ones, and + CategoricalHyperparameters with weights have their weights removed. + Returns ------- :class:`~ConfigSpace.configuration_space.ConfigurationSpace` @@ -1384,12 +1460,12 @@ class ConfigurationSpace(collections.abc.Mapping): 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]: @@ -1421,21 +1497,21 @@ class ConfigurationSpace(collections.abc.Mapping): @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 + 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 + new_configspace: ConfigurationSpace A ConfigurationSpace containing hyperparameters with the same names as those in the conditions. Returns ------- - List[ConditionComponent]: + List[ConditionComponent]: The list of conditions, adjusted to fit the new ConfigurationSpace - """ + """ new_conditions = [] for condition in conditions: if isinstance(condition, AbstractConjunction): @@ -1444,14 +1520,14 @@ class ConfigurationSpace(collections.abc.Mapping): 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, 'value'): condition_arg = getattr(condition, 'value') substituted_condition = condition_type(child=new_child, parent=new_parent, value=condition_arg) @@ -1460,31 +1536,31 @@ class ConfigurationSpace(collections.abc.Mapping): substituted_condition = condition_type(child=new_child, parent=new_parent, values=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 + 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 + new_configspace: ConfigurationSpace A ConfigurationSpace containing hyperparameters with the same names as those in the forbidden clauses. Returns ------- - List[AbstractForbiddenComponent]: + List[AbstractForbiddenComponent]: The list of forbidden clauses, adjusted to fit the new ConfigurationSpace - """ + """ new_forbiddens = [] for forbidden in forbiddens: if isinstance(forbidden, AbstractForbiddenConjunction): @@ -1493,12 +1569,12 @@ class ConfigurationSpace(collections.abc.Mapping): 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, 'value'): forbidden_arg = getattr(forbidden, 'value') substituted_forbidden = forbidden_type(hyperparameter=new_hyperparameter, value=forbidden_arg) @@ -1507,11 +1583,11 @@ class ConfigurationSpace(collections.abc.Mapping): substituted_forbidden = forbidden_type(hyperparameter=new_hyperparameter, values=forbidden_arg) else: raise AttributeError(f'Did not find the expected attribute in forbidden {type(forbidden)}.') - + new_forbiddens.append(substituted_forbidden) else: raise TypeError(f'Did not expect the supplied forbidden type {type(forbidden)}.') - + return new_forbiddens diff --git a/ConfigSpace/forbidden.pyx b/ConfigSpace/forbidden.pyx index 559877fa..ec1e75f5 100644 --- a/ConfigSpace/forbidden.pyx +++ b/ConfigSpace/forbidden.pyx @@ -56,9 +56,9 @@ cdef class AbstractForbiddenComponent(object): Additionally, it defines the __ne__() as stated in the documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result + By default, object implements __eq__() by using is, returning NotImplemented + in the case of a false comparison: True if x is y else NotImplemented. + For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ @@ -218,23 +218,17 @@ cdef class MultipleValueForbiddenClause(AbstractForbiddenClause): cdef class ForbiddenEqualsClause(SingleValueForbiddenClause): - """ - A ForbiddenEqualsClause + """A ForbiddenEqualsClause It forbids a value from the value range of a hyperparameter to be *equal to* ``value``. - Example - ------- - - >>> cs = CS.ConfigurationSpace() - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> cs.add_hyperparameters([a]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1] + Forbids the value 2 for the hyperparameter *a* - It forbids the value 2 for the hyperparameter *a* - - >>> forbidden_clause_a = CS.ForbiddenEqualsClause(a, 2) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenEqualsClause + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3]}) + >>> forbidden_clause_a = ForbiddenEqualsClause(cs["a"], 2) >>> cs.add_forbidden_clause(forbidden_clause_a) Forbidden: a == 2 @@ -260,35 +254,29 @@ cdef class ForbiddenEqualsClause(SingleValueForbiddenClause): cdef class ForbiddenInClause(MultipleValueForbiddenClause): def __init__(self, hyperparameter: Dict[str, Union[None, str, float, int]], values: Any) -> None: - """ - A ForbiddenInClause. + """A ForbiddenInClause. It forbids a value from the value range of a hyperparameter to be *in* a collection of ``values``. - Note - ---- - - The forbidden values have to be a subset of the hyperparameter's values. + Forbids the values 2, 3 for the hyperparameter *a* - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> cs.add_hyperparameters([a]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1] - - It forbids the values 2, 3, 4 for the hyperparameter *a* - - >>> forbidden_clause_a = CS.ForbiddenInClause(a, [2, 3]) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenInClause + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3]}) + >>> forbidden_clause_a = ForbiddenInClause(cs['a'], [2, 3]) >>> cs.add_forbidden_clause(forbidden_clause_a) Forbidden: a in {2, 3} + Note + ---- + The forbidden values have to be a subset of the hyperparameter's values. + Parameters ---------- hyperparameter : (:ref:`Hyperparameters`, dict) Hyperparameter on which a restriction will be made + values : Any Collection of forbidden values """ @@ -342,9 +330,9 @@ cdef class AbstractForbiddenConjunction(AbstractForbiddenComponent): Additionally, it defines the __ne__() as stated in the documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result + By default, object implements __eq__() by using is, returning NotImplemented + in the case of a false comparison: True if x is y else NotImplemented. + For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ @@ -428,24 +416,25 @@ cdef class AbstractForbiddenConjunction(AbstractForbiddenComponent): cdef class ForbiddenAndConjunction(AbstractForbiddenConjunction): - """ - A ForbiddenAndConjunction. + """A ForbiddenAndConjunction. The ForbiddenAndConjunction combines forbidden-clauses, which allows to build powerful constraints. - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> b = CSH.CategoricalHyperparameter('b', [2,5,6]) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - >>> forbidden_clause_a = CS.ForbiddenEqualsClause(a, 2) - >>> forbidden_clause_b = CS.ForbiddenInClause(b, [2]) - >>> forbidden_clause = CS.ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_b) + >>> from ConfigSpace import ( + ... ConfigurationSpace, + ... ForbiddenEqualsClause, + ... ForbiddenInClause, + ... ForbiddenAndConjunction + ... ) + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]}) + >>> + >>> forbidden_clause_a = ForbiddenEqualsClause(cs["a"], 2) + >>> forbidden_clause_b = ForbiddenInClause(cs["b"], [2]) + >>> + >>> forbidden_clause = ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_b) + >>> >>> cs.add_forbidden_clause(forbidden_clause) (Forbidden: a == 2 && Forbidden: b in {2}) @@ -583,21 +572,15 @@ cdef class ForbiddenRelation(AbstractForbiddenComponent): cdef class ForbiddenLessThanRelation(ForbiddenRelation): - """ - A ForbiddenLessThan relation between two hyperparameters. + """A ForbiddenLessThan relation between two hyperparameters. The ForbiddenLessThan compares the values of two hyperparameters. - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> b = CSH.CategoricalHyperparameter('b', [2,5,6]) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - >>> forbidden_clause = CS.ForbiddenLessThanRelation(a, b) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenLessThanRelation + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]}) + >>> + >>> forbidden_clause = ForbiddenLessThanRelation(cs['a'], cs['b']) >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a < b @@ -611,6 +594,7 @@ cdef class ForbiddenLessThanRelation(ForbiddenRelation): ---------- left : :ref:`Hyperparameters` left side of the comparison + right : :ref:`Hyperparameters` right side of the comparison """ @@ -626,21 +610,15 @@ cdef class ForbiddenLessThanRelation(ForbiddenRelation): cdef class ForbiddenEqualsRelation(ForbiddenRelation): - """ - A ForbiddenEquals relation between two hyperparameters. + """A ForbiddenEquals relation between two hyperparameters. The ForbiddenEquals compares the values of two hyperparameters. - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> b = CSH.CategoricalHyperparameter('b', [2,5,6]) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - >>> forbidden_clause = CS.ForbiddenEqualsRelation(a, b) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenEqualsRelation + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]}) + >>> + >>> forbidden_clause = ForbiddenEqualsRelation(cs['a'], cs['b']) >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a == b @@ -669,21 +647,15 @@ cdef class ForbiddenEqualsRelation(ForbiddenRelation): cdef class ForbiddenGreaterThanRelation(ForbiddenRelation): - """ - A ForbiddenGreaterThan relation between two hyperparameters. + """A ForbiddenGreaterThan relation between two hyperparameters. The ForbiddenGreaterThan compares the values of two hyperparameters. - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> b = CSH.CategoricalHyperparameter('b', [2,5,6]) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - >>> forbidden_clause = CS.ForbiddenGreaterThanRelation(a, b) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenGreaterThanRelation + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]}) + >>> forbidden_clause = ForbiddenGreaterThanRelation(cs['a'], cs['b']) + >>> >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a > b diff --git a/ConfigSpace/hyperparameters.pyx b/ConfigSpace/hyperparameters.pyx index 347e397c..f6347ecf 100644 --- a/ConfigSpace/hyperparameters.pyx +++ b/ConfigSpace/hyperparameters.pyx @@ -25,14 +25,13 @@ # 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 copy import io # cython: language_level=3 import math import warnings from collections import OrderedDict, Counter -from typing import List, Any, Dict, Union, Set, Tuple, Optional +from typing import List, Any, Dict, Union, Set, Tuple, Optional, Sequence import numpy as np from scipy.stats import truncnorm, beta as spbeta, norm @@ -153,40 +152,40 @@ cdef class Hyperparameter(object): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in + Computes the probability density function of the hyperparameter in the hyperparameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which + For each hyperparameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) hyperparameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) Probability density values of the input vector """ raise NotImplementedError() - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the hyperparameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -307,19 +306,19 @@ cdef class Constant(Hyperparameter): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in + Computes the probability density function of the parameter in the original parameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which + For each hyperparameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) parameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -331,18 +330,18 @@ cdef class Constant(Hyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -471,19 +470,19 @@ cdef class FloatHyperparameter(NumericalHyperparameter): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in + Computes the probability density function of the parameter in the original parameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which + For each parameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) parameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -493,21 +492,21 @@ cdef class FloatHyperparameter(NumericalHyperparameter): raise ValueError("Method pdf expects a one-dimensional numpy array") vector = self._inverse_transform(vector) return self._pdf(vector) - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -517,7 +516,7 @@ cdef class FloatHyperparameter(NumericalHyperparameter): def get_max_density(self) -> float: """ - Returns the maximal density on the pdf for the parameter (so not + Returns the maximal density on the pdf for the parameter (so not the mode, but the value of the pdf on the mode). """ raise NotImplementedError() @@ -536,7 +535,7 @@ cdef class IntegerHyperparameter(NumericalHyperparameter): def check_default(self, default_value) -> int: raise NotImplemented - + def check_int(self, parameter: int, name: str) -> int: if abs(int(parameter) - parameter) > 0.00000001 and \ type(parameter) is not int: @@ -559,22 +558,22 @@ cdef class IntegerHyperparameter(NumericalHyperparameter): cpdef np.ndarray _transform_vector(self, np.ndarray vector): raise NotImplementedError() - + def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in + Computes the probability density function of the hyperparameter in the hyperparameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which + For each hyperparameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) hyperparameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -585,33 +584,33 @@ cdef class IntegerHyperparameter(NumericalHyperparameter): is_integer = (np.round(vector) == vector).astype(int) vector = self._inverse_transform(vector) return self._pdf(vector) * is_integer - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls to the probability density function (see e.g. NormalIntegerHyperparameter) - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) Probability density values of the input vector """ raise NotImplementedError() - + def get_max_density(self) -> float: """ - Returns the maximal density on the pdf for the parameter (so not + Returns the maximal density on the pdf for the parameter (so not the mode, but the value of the pdf on the mode). """ raise NotImplementedError() @@ -628,16 +627,10 @@ cdef class UniformFloatHyperparameter(FloatHyperparameter): Its values are sampled from a uniform distribution with values from ``lower`` to ``upper``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> uniform_float_hp = CSH.UniformFloatHyperparameter('uni_float', lower=10, - ... upper=100, log = False) - >>> cs.add_hyperparameter(uniform_float_hp) - uni_float, Type: UniformFloat, Range: [10.0, 100.0], Default: 55.0 + >>> from ConfigSpace import UniformFloatHyperparameter + >>> + >>> UniformFloatHyperparameter('u', lower=10, upper=100, log = False) + u, Type: UniformFloat, Range: [10.0, 100.0], Default: 55.0 Parameters ---------- @@ -815,18 +808,18 @@ cdef class UniformFloatHyperparameter(FloatHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -842,7 +835,7 @@ cdef class UniformFloatHyperparameter(FloatHyperparameter): def get_max_density(self) -> float: return 1 / (self.upper - self.lower) - + def get_size(self) -> float: if self.q is None: return np.inf @@ -865,16 +858,10 @@ cdef class NormalFloatHyperparameter(FloatHyperparameter): Its values are sampled from a normal distribution :math:`\mathcal{N}(\mu, \sigma^2)`. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> normal_float_hp = CSH.NormalFloatHyperparameter('normal_float', mu=0, - ... sigma=1, log=False) - >>> cs.add_hyperparameter(normal_float_hp) - normal_float, Type: NormalFloat, Mu: 0.0 Sigma: 1.0, Default: 0.0 + >>> from ConfigSpace import NormalFloatHyperparameter + >>> + >>> NormalFloatHyperparameter('n', mu=0, sigma=1, log=False) + n, Type: NormalFloat, Mu: 0.0 Sigma: 1.0, Default: 0.0 Parameters ---------- @@ -1046,7 +1033,7 @@ cdef class NormalFloatHyperparameter(FloatHyperparameter): else: lower=np.ceil(self.lower) upper=np.floor(self.upper) - + return NormalIntegerHyperparameter(self.name, int(np.rint(self.mu)), self.sigma, lower=lower, upper=upper, default_value=int(np.rint(self.default_value)), @@ -1123,23 +1110,23 @@ cdef class NormalFloatHyperparameter(FloatHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) Probability density values of the input vector - """ + """ mu = self.mu sigma = self.sigma if self.lower == None: @@ -1151,7 +1138,7 @@ cdef class NormalFloatHyperparameter(FloatHyperparameter): upper = self._upper a = (lower - mu) / sigma b = (upper - mu) / sigma - + return truncnorm(a, b, loc=mu, scale=sigma).pdf(vector) def get_max_density(self) -> float: @@ -1184,16 +1171,10 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): Its values are sampled from a beta distribution :math:`Beta(\alpha, \beta)`. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> beta_float_hp = CSH.BetaFloatHyperparameter('beta_float', alpha=3, - ... beta=2, lower=1, upper=4, log=False) - >>> cs.add_hyperparameter(beta_float_hp) - beta_float, Type: BetaFloat, Alpha: 3.0 Beta: 2.0, Range: [1.0, 4.0], Default: 3.0 + >>> from ConfigSpace import BetaFloatHyperparameter + >>> + >>> BetaFloatHyperparameter('b', alpha=3, beta=2, lower=1, upper=4, log=False) + b, Type: BetaFloat, Alpha: 3.0 Beta: 2.0, Range: [1.0, 4.0], Default: 3.0 Parameters ---------- @@ -1223,7 +1204,7 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): # TODO - we cannot use the check_default of UniformFloat (but everything else), # but we still need to overwrite it. Thus, we first just need it not to raise an # error, which we do by setting default_value = upper - lower / 2 to not raise an error, - # then actually call check_default once we have alpha and beta, and are not inside + # then actually call check_default once we have alpha and beta, and are not inside # UniformFloatHP. super(BetaFloatHyperparameter, self).__init__( name, lower, upper, (upper + lower) / 2, q, log, meta) @@ -1232,14 +1213,14 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): if (alpha < 1) or (beta < 1): raise ValueError("Please provide values of alpha and beta larger than or equal to\ 1 so that the probability density is finite.") - - if (self.q is not None) and (self.log is not None) and (default_value is None): + + if (self.q is not None) and (self.log is not None) and (default_value is None): warnings.warn('Logscale and quantization together results in incorrect default values. ' 'We recommend specifying a default value manually for this specific case.') - + self.default_value = self.check_default(default_value) self.normalized_default_value = self._inverse_transform(self.default_value) - + def __repr__(self) -> str: repr_str = io.StringIO() repr_str.write("%s, Type: BetaFloat, Alpha: %s Beta: %s, Range: [%s, %s], Default: %s" % (self.name, repr(self.alpha), repr(self.beta), repr(self.lower), repr(self.upper), repr(self.default_value))) @@ -1304,20 +1285,20 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): def check_default(self, default_value: Union[int, float, None]) -> Union[int, float]: # return mode as default # TODO - for log AND quantization together specifially, this does not give the exact right - # value, due to the bounds _lower and _upper being adjusted when quantizing in + # value, due to the bounds _lower and _upper being adjusted when quantizing in # UniformFloat. if default_value is None: if (self.alpha > 1) or (self.beta > 1): normalized_mode = (self.alpha - 1) / (self.alpha + self.beta - 2) - else: + else: # If both alpha and beta are 1, we have a uniform distribution. normalized_mode = 0.5 - + ub = self._inverse_transform(self.upper) lb = self._inverse_transform(self.lower) scaled_mode = normalized_mode * (ub - lb) + lb return self._transform_scalar(scaled_mode) - + elif self.is_legal(default_value): return default_value else: @@ -1328,7 +1309,7 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): q_int = None else: q_int = int(np.rint(self.q)) - + lower = int(np.ceil(self.lower)) upper = int(np.floor(self.upper)) default_value = int(np.rint(self.default_value)) @@ -1353,18 +1334,18 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -1384,17 +1365,17 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): normalized_mode = 0 elif self.alpha > self.beta: normalized_mode = 1 - else: + else: normalized_mode = 0.5 ub = self._inverse_transform(self.upper) lb = self._inverse_transform(self.lower) scaled_mode = normalized_mode * (ub - lb) + lb - # Since _pdf takes only a numpy array, we have to create the array, + # Since _pdf takes only a numpy array, we have to create the array, # and retrieve the element in the first (and only) spot in the array return self._pdf(np.array([scaled_mode]))[0] - + cdef class UniformIntegerHyperparameter(IntegerHyperparameter): def __init__(self, name: str, lower: int, upper: int, default_value: Union[int, None] = None, q: Union[int, None] = None, log: bool = False, @@ -1405,16 +1386,10 @@ cdef class UniformIntegerHyperparameter(IntegerHyperparameter): Its values are sampled from a uniform distribution with bounds ``lower`` and ``upper``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> uniform_integer_hp = CSH.UniformIntegerHyperparameter(name='uni_int', lower=10, - ... upper=100, log=False) - >>> cs.add_hyperparameter(uniform_integer_hp) - uni_int, Type: UniformInteger, Range: [10, 100], Default: 55 + >>> from ConfigSpace import UniformIntegerHyperparameter + >>> + >>> UniformIntegerHyperparameter(name='u', lower=10, upper=100, log=False) + u, Type: UniformInteger, Range: [10, 100], Default: 55 Parameters ---------- @@ -1648,20 +1623,20 @@ cdef class UniformIntegerHyperparameter(IntegerHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls to the probability density function (see e.g. NormalIntegerHyperparameter) - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -1701,16 +1676,10 @@ cdef class NormalIntegerHyperparameter(IntegerHyperparameter): Its values are sampled from a normal distribution :math:`\mathcal{N}(\mu, \sigma^2)`. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> normal_int_hp = CSH.NormalIntegerHyperparameter(name='normal_int', mu=0, - ... sigma=1, log=False) - >>> cs.add_hyperparameter(normal_int_hp) - normal_int, Type: NormalInteger, Mu: 0 Sigma: 1, Default: 0 + >>> from ConfigSpace import NormalIntegerHyperparameter + >>> + >>> NormalIntegerHyperparameter(name='n', mu=0, sigma=1, log=False) + n, Type: NormalInteger, Mu: 0 Sigma: 1, Default: 0 Parameters ---------- @@ -1785,7 +1754,7 @@ cdef class NormalIntegerHyperparameter(IntegerHyperparameter): self.default_value = self.check_default(default_value) self.normalized_default_value = self._inverse_transform(self.default_value) - + if (self.lower is None) or (self.upper is None): # Since a bound is missing, the pdf cannot be normalized. Working with the unnormalized variant) self.normalization_constant = 1 @@ -1938,7 +1907,7 @@ cdef class NormalIntegerHyperparameter(IntegerHyperparameter): else: neighbors.append(new_value) return neighbors - + def _compute_normalization(self): if self.lower is None: warnings.warn('Cannot normalize the pdf exactly for a NormalIntegerHyperparameter' @@ -1952,20 +1921,20 @@ cdef class NormalIntegerHyperparameter(IntegerHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls to the probability density function (see e.g. NormalIntegerHyperparameter) - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2010,16 +1979,10 @@ cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): Its values are sampled from a beta distribution :math:`Beta(\alpha, \beta)`. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> beta_int_hp = CSH.BetaIntegerHyperparameter('beta_int', alpha=3, - ... beta=2, lower=1, upper=4, log=False) - >>> cs.add_hyperparameter(beta_int_hp) - beta_int, Type: BetaInteger, Alpha: 3.0 Beta: 2.0, Range: [1, 4], Default: 3 + >>> from ConfigSpace import BetaIntegerHyperparameter + >>> + >>> BetaIntegerHyperparameter('b', alpha=3, beta=2, lower=1, upper=4, log=False) + b, Type: BetaInteger, Alpha: 3.0 Beta: 2.0, Range: [1, 4], Default: 3 Parameters @@ -2074,7 +2037,7 @@ cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): def __repr__(self) -> str: repr_str = io.StringIO() repr_str.write("%s, Type: BetaInteger, Alpha: %s Beta: %s, Range: [%s, %s], Default: %s" % (self.name, repr(self.alpha), repr(self.beta), repr(self.lower), repr(self.upper), repr(self.default_value))) - + if self.log: repr_str.write(", on log-scale") if self.q is not None: @@ -2136,16 +2099,16 @@ cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): if default_value is None: # Here, we just let the BetaFloat take care of the default value # computation, and just tansform it accordingly - value = self.bfhp.check_default(None) + value = self.bfhp.check_default(None) value = self._inverse_transform(value) value = self._transform(value) return value - + if self.is_legal(default_value): return default_value else: raise ValueError('Illegal default value {}'.format(default_value)) - + def _sample(self, rs: np.random.RandomState, size: Optional[int] = None ) -> Union[np.ndarray, float]: value = self.bfhp._sample(rs, size=size) @@ -2163,20 +2126,20 @@ cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls to the probability density function (see e.g. NormalIntegerHyperparameter) - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2197,7 +2160,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): cdef public tuple probabilities cdef list choices_vector cdef set _choices_set - + # TODO add more magic for automated type recognition # TODO move from list to tuple for choices argument def __init__( @@ -2206,7 +2169,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): choices: Union[List[Union[str, float, int]], Tuple[Union[float, int, str]]], default_value: Union[int, float, str, None] = None, meta: Optional[Dict] = None, - weights: Union[List[float], Tuple[float]] = None + weights: Optional[Sequence[Union[int, float]]] = None ) -> None: """ A categorical hyperparameter. @@ -2217,15 +2180,10 @@ cdef class CategoricalHyperparameter(Hyperparameter): it in your own code, see `here _` for further details. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> cat_hp = CSH.CategoricalHyperparameter('cat_hp', choices=['red', 'green', 'blue']) - >>> cs.add_hyperparameter(cat_hp) - cat_hp, Type: Categorical, Choices: {red, green, blue}, Default: red + >>> from ConfigSpace import CategoricalHyperparameter + >>> + >>> CategoricalHyperparameter('c', choices=['red', 'green', 'blue']) + c, Type: Categorical, Choices: {red, green, blue}, Default: red Parameters ---------- @@ -2238,7 +2196,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): meta : Dict, optional Field for holding meta data provided by the user. Not used by the configuration space. - weights: (list[float], optional) + weights: Sequence[int | float] | None = None List of weights for the choices to be used (after normalization) as probabilities during sampling, no negative values allowed """ @@ -2356,7 +2314,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): Creates a categorical parameter with equal weights for all choices This is used for the uniform configspace when sampling configurations in the local search in PiBO: https://openreview.net/forum?id=MMAeCXIa89 - + Returns ---------- CategoricalHyperparameter @@ -2452,7 +2410,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): return self._transform_scalar(vector) except ValueError: return None - + def _inverse_transform(self, vector: Union[None, str, float, int]) -> Union[int, float]: if vector is None: return np.NaN @@ -2509,19 +2467,19 @@ cdef class CategoricalHyperparameter(Hyperparameter): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in + Computes the probability density function of the parameter in the original parameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which + For each parameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) parameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2532,24 +2490,24 @@ cdef class CategoricalHyperparameter(Hyperparameter): raise ValueError("Method pdf expects a one-dimensional numpy array") vector = np.array(self._inverse_transform(vector)) return self._pdf(vector) - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). For categoricals, each vector gets - transformed to its corresponding index (but in float form). To be - able to retrieve the element corresponding to the index, the float - must be cast to int. - + in the pdf method handles these). For categoricals, each vector gets + transformed to its corresponding index (but in float form). To be + able to retrieve the element corresponding to the index, the float + must be cast to int. + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2591,15 +2549,10 @@ cdef class OrdinalHyperparameter(Hyperparameter): it in your own code, see `here _` for further details. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> ord_hp = CSH.OrdinalHyperparameter('ordinal_hp', sequence=['10', '20', '30']) - >>> cs.add_hyperparameter(ord_hp) - ordinal_hp, Type: Ordinal, Sequence: {10, 20, 30}, Default: 10 + >>> from ConfigSpace import OrdinalHyperparameter + >>> + >>> OrdinalHyperparameter('o', sequence=['10', '20', '30']) + o, Type: Ordinal, Sequence: {10, 20, 30}, Default: 10 Parameters ---------- @@ -2856,20 +2809,20 @@ cdef class OrdinalHyperparameter(Hyperparameter): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in + Computes the probability density function of the hyperparameter in the original hyperparameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which + For each parameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) hyperparameter space. Only legal values return a positive probability density, otherwise zero. The OrdinalHyperparameter is treated as a UniformHyperparameter with regard to its probability density. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2878,22 +2831,22 @@ cdef class OrdinalHyperparameter(Hyperparameter): if vector.ndim != 1: raise ValueError("Method pdf expects a one-dimensional numpy array") return self._pdf(vector) - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in - the transformed (and possibly normalized, depends on the hyperparameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the hyperparameter in + the transformed (and possibly normalized, depends on the hyperparameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). The OrdinalHyperparameter is treated as a UniformHyperparameter with regard to its probability density. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) diff --git a/ConfigSpace/nx/algorithms/dag.py b/ConfigSpace/nx/algorithms/dag.py index 691121c3..0871158d 100644 --- a/ConfigSpace/nx/algorithms/dag.py +++ b/ConfigSpace/nx/algorithms/dag.py @@ -1,8 +1,9 @@ -# -*- coding: utf-8 -*- try: - from math import gcd # >= Python 3.9 + # >= Python 3.9 + from math import gcd # type: ignore except ImportError: - from fractions import gcd # < Python 3.9 + # < Python 3.9 + from fractions import gcd # type: ignore import ConfigSpace.nx diff --git a/docs/source/_templates/navbarsearchbox.html b/ConfigSpace/py.typed similarity index 100% rename from docs/source/_templates/navbarsearchbox.html rename to ConfigSpace/py.typed diff --git a/ConfigSpace/read_and_write/json.py b/ConfigSpace/read_and_write/json.py index fcbfed34..b79f26a7 100644 --- a/ConfigSpace/read_and_write/json.py +++ b/ConfigSpace/read_and_write/json.py @@ -319,18 +319,15 @@ def write(configuration_space, indent=2): :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in json format. This string can be written to file. - Example: - - ..code:: python + .. code:: python from ConfigSpace import ConfigurationSpace - import ConfigSpace.hyperparameters as CSH - from ConfigSpace.read_and_write import json - cs = ConfigurationSpace() - cs.add_hyperparameter(CSH.CategoricalHyperparameter('a', choices=[1, 2, 3])) + from ConfigSpace.read_and_write import json as cs_json + + cs = ConfigurationSpace({"a": [1, 2, 3]}) with open('configspace.json', 'w') as f: - f.write(json.write(cs)) + f.write(cs_json.write(cs)) Parameters ---------- @@ -408,25 +405,21 @@ def read(jason_string): """ Create a configuration space definition from a json string. - Example - ------- - - .. testsetup:: json_test + .. code:: python from ConfigSpace import ConfigurationSpace - import ConfigSpace.hyperparameters as CSH - from ConfigSpace.read_and_write import json - cs = ConfigurationSpace() - cs.add_hyperparameter(CSH.CategoricalHyperparameter('a', choices=[1, 2, 3])) + from ConfigSpace.read_and_write import json as cs_json + + cs = ConfigurationSpace({"a": [1, 2, 3]}) + + cs_string = cs_json.write(cs) with open('configspace.json', 'w') as f: - f.write(json.write(cs)) + f.write(cs_string) - .. doctest:: json_test + with open('configspace.json', 'r') as f: + json_string = f.read() + config = cs_json.read(json_string) - >>> from ConfigSpace.read_and_write import json - >>> with open('configspace.json', 'r') as f: - ... jason_string = f.read() - ... config = json.read(jason_string) Parameters ---------- diff --git a/ConfigSpace/read_and_write/pcs.py b/ConfigSpace/read_and_write/pcs.py index 0016650d..3abfa2f9 100644 --- a/ConfigSpace/read_and_write/pcs.py +++ b/ConfigSpace/read_and_write/pcs.py @@ -173,37 +173,31 @@ def read(pcs_string, debug=False): Read in a :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` definition from a pcs file. - Example - ------- - .. testsetup:: pcs_test + .. code:: python from ConfigSpace import ConfigurationSpace - import ConfigSpace.hyperparameters as CSH from ConfigSpace.read_and_write import pcs - cs = ConfigurationSpace() - cs.add_hyperparameter(CSH.CategoricalHyperparameter('a', choices=[1, 2, 3])) + + cs = ConfigurationSpace({"a": [1, 2, 3]}) with open('configspace.pcs', 'w') as f: f.write(pcs.write(cs)) - .. doctest:: pcs_test - - >>> from ConfigSpace.read_and_write import pcs - >>> with open('configspace.pcs', 'r') as fh: - ... deserialized_conf = pcs.read(fh) + with open('configspace.pcs', 'r') as f: + deserialized_conf = pcs.read(f) Parameters ---------- pcs_string : str ConfigSpace definition in pcs format - debug : bool + + debug : bool = False Provides debug information. Defaults to False. Returns ------- :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` The deserialized ConfigurationSpace object - """ configuration_space = ConfigurationSpace() conditions = [] @@ -351,22 +345,15 @@ def write(configuration_space): :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in pcs format. This string can be written to file. - Example - ------- + .. code:: python - .. doctest:: + from ConfigSpace import ConfigurationSpace + from ConfigSpace.read_and_write import pcs - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> from ConfigSpace.read_and_write import pcs - >>> cs = CS.ConfigurationSpace() - >>> cs.add_hyperparameter(CSH.CategoricalHyperparameter('a', choices=[1, 2, 3])) - a, Type: Categorical, Choices: {1, 2, 3}, Default: 1 + cs = ConfigurationSpace({"a": [1, 2, 3]}) - - >>> with open('configspace.pcs', 'w') as fh: - ... fh.write(pcs.write(cs)) - 15 + with open('configspace.pcs', 'w') as fh: + fh.write(pcs.write(cs)) Parameters ---------- diff --git a/MANIFEST.in b/MANIFEST.in index 839b3733..83859389 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include *.md recursive-include ConfigSpace *.pyx *.pxd include LICENSE include pyproject.toml +include ConfigSpace/py.typed diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..bde61c2b --- /dev/null +++ b/Makefile @@ -0,0 +1,83 @@ +# NOTE: Used on linux, limited support outside of Linux +# +# A simple makefile to help with small tasks related to development of ConfigSpace +# These have been configured to only really run short tasks. Longer form tasks +# are usually completed in github actions. + +.PHONY: help install-dev pre-commit clean clean-doc clean-build build docs links publish test + +help: + @echo "Makefile ConfigSpace" + @echo "* install-dev to install all dev requirements and install pre-commit" + @echo "* pre-commit to run the pre-commit check" + @echo "* docs to generate and view the html files" + @echo "* linkcheck to check the documentation links" + @echo "* publish to help publish the current branch to pypi" + @echo "* test to run the tests" + +PYTHON ?= python +CYTHON ?= cython +PYTEST ?= python -m pytest +CTAGS ?= ctags +PRECOMMIT ?= pre-commit +PIP ?= python -m pip +MAKE ?= make + +DIR := "${CURDIR}" +DIST := "${DIR}/dist"" +DOCDIR := "${DIR}/docs" +BUILD := "${DIR}/build" +INDEX_HTML := "file://${DOCDIR}/build/html/index.html" + +install-dev: + $(PIP) install -e ".[dev]" + pre-commit install + +pre-commit: + $(PRECOMMIT) run --all-files + +clean-build: + rm -rf ${BUILD} + +clean-docs: + $(MAKE) -C ${DOCDIR} clean + +clean: clean-build clean-docs + +build: + python setup.py develop + +# 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 +docs: clean build + $(MAKE) -C ${DOCDIR} html + @echo + @echo "View docs at:" + @echo ${INDEX_HTML} + +links: + $(MAKE) -C ${DOCDIR} linkcheck + +# Publish to testpypi +# Will echo the commands to actually publish to be run to publish to actual PyPi +# This is done to prevent accidental publishing but provide the same conveniences +publish: + $(PIP) install twine + $(PYTHON) -m twine upload --repository testpypi ${DIST}/* + @echo + @echo "Test with the following:" + @echo "* Create a new virtual environment to install the uplaoded distribution into" + @echo "* Run the following:" + @echo + @echo " pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ ConfigSpace" + @echo + @echo "* Run this to make sure it can import correctly, plus whatever else you'd like to test:" + @echo + @echo " python -c 'import ConfigSpace'" + @echo + @echo "Once you have decided it works, publish to actual pypi with" + @echo + @echo " python -m twine upload dist/*" + +test: + $(PYTEST) test diff --git a/README.md b/README.md index 894abfaa..09ed85f0 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,27 @@ Distributed under BSD 3-clause, see LICENSE except all files in the directory ConfigSpace.nx, which are copied from the networkx package and licensed under a BSD license. -The documentation can be found at [https://automl.github.io/ConfigSpace/master/](https://automl.github.io/ConfigSpace/master/). -Further examples can be found in the [SMAC documentation](https://automl.github.io/SMAC3/master/pages/examples/index.html). +The documentation can be found at [https://automl.github.io/ConfigSpace/main/](https://automl.github.io/ConfigSpace/main/). +Further examples can be found in the [SMAC documentation](https://automl.github.io/SMAC3/main/pages/examples/index.html). + + +## Minimum Example + +```python +from ConfigSpace import ConfigurationSpace + +cs = ConfigurationSpace( + name="myspace", + space={ + "a": (0.1, 1.5), # UniformFloat + "b": (2, 10), # UniformInt + "c": ["mouse", "cat", "dog"], # Categorical + }, +) + +configs = cs.sample_configuration(2) +``` + ## Citing the ConfigSpace diff --git a/changelog.md b/changelog.md index df969ebd..e328fc1f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,14 +1,23 @@ +# Version 0.6.0 + +* ADD #255: An easy interface of `Float`, `Integer`, `Categorical` for creating search spaces. +* ADD #243: Add forbidden relations between two hyperparamters +* MAINT #243: Change branch `master` to `main` +* FIX #259: Numpy runtime error when rounding +* FIX #247: No longer errors when serliazing spaces with an `InCondition` +* FIX #219: Hyperparamters correctly active with diamond-or conditions + # Version 0.5.0 -* Fix #231: Links to the pcs formats. -* Fix #230: Allow Forbidden Clauses with non-numeric values. -* Fix #232: Equality `==` between hyperparameters now considers default values. -* Fix #221: Normal Hyperparameters should now properly sample from correct distribution in log space -* Fix #221: Fixed boundary problems with integer hyperparameters due to numerical rounding after sampling. -* Maint #221: Categorical Hyperparameters now always have associated probabilities, remaining uniform if non are provided. (Same behaviour) -* Add #222: BetaFloat and BetaInteger hyperparamters, hyperparameters distributed according to a beta distribution. -* Add #241: Implements support for [PiBo](https://openreview.net/forum?id=MMAeCXIa89), you can now embed some prior distribution knowledge into ConfigSpace hyperparameters. - * See the example [here](https://automl.github.io/ConfigSpace/master/User-Guide.html#th-example-placing-priors-on-the-hyperparameters). +* FIX #231: Links to the pcs formats. +* FIX #230: Allow Forbidden Clauses with non-numeric values. +* FIX #232: Equality `==` between hyperparameters now considers default values. +* FIX #221: Normal Hyperparameters should now properly sample from correct distribution in log space +* FIX #221: Fixed boundary problems with integer hyperparameters due to numerical rounding after sampling. +* MAINT #221: Categorical Hyperparameters now always have associated probabilities, remaining uniform if non are provided. (Same behaviour) +* ADD #222: BetaFloat and BetaInteger hyperparamters, hyperparameters distributed according to a beta distribution. +* ADD #241: Implements support for [PiBo](https://openreview.net/forum?id=MMAeCXIa89), you can now embed some prior distribution knowledge into ConfigSpace hyperparameters. + * See the example [here](https://automl.github.io/ConfigSpace/main/User-Guide.html#th-example-placing-priors-on-the-hyperparameters). * Hyperparameters now have a `pdf(vector: np.ndarray) -> np.ndarray` to get the probability density values for the input * Hyperparameters now have a `get_max_density() -> float` to get the greatest value in it's probability distribution function, the probability of the mode of the distriubtion. * `ConfigurationSpace` objects now have a `remove_parameter_priors() -> ConfigurationSpace` to remove any priors diff --git a/docs/Makefile b/docs/Makefile index fd7cff1c..bbeebd67 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,32 +1,24 @@ -# Minimal makefile for Sphinx documentation -# +SPHINXBUILD = sphinx-build +BUILDDIR = build +SPHINXOPTS = +ALLSPHINXOPTS = $(SPHINXOPTS) . -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = ConfigSpace -SOURCEDIR = source -BUILDDIR = build +.PHONY: clean buildapi linkcheck html docs html-noexamples -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +clean: + rm -rf $(BUILDDIR)/* + rm -rf ../build/ -.PHONY: help Makefile +linkcheck: + SPHINX_GALLERY_PLOT=False $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." -buildapi: - sphinx-apidoc -fePTMo apidoc ../ConfigSpace/ - for f in apidoc/*; \ - do \ - sed -i '/Submodules/d' $$f; \ - sed -i '/----------/d' $$f; \ - sed -i 's/ :show-inheritance:/ :show-inheritance:\n :inherited-members:/g' $$f; \ - done; +html: clean linkcheck + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + $(SPHINXBUILD) -M doctest $(ALLSPHINXOPTS) $(BUILDDIR)/html || true @echo - @echo "Auto-generation of API documentation finished. " \ - "The generated files are in 'apidoc/'" + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +docs: html linkcheck diff --git a/docs/api/conditions.rst b/docs/api/conditions.rst new file mode 100644 index 00000000..0d068ccf --- /dev/null +++ b/docs/api/conditions.rst @@ -0,0 +1,53 @@ +.. _Conditions: + +Conditions +========== + +ConfigSpace can realize *equal*, *not equal*, *less than*, *greater than* and +*in conditions*. Conditions can be combined by using the conjunctions *and* and +*or*. To see how to use conditions, please take a look at the +:doc:`user guide <../guide>`. + +EqualsCondition +------------------- + +.. autoclass:: ConfigSpace.conditions.EqualsCondition + +.. _NotEqualsCondition: + +NotEqualsCondition +------------------ + +.. autoclass:: ConfigSpace.conditions.NotEqualsCondition + +.. _LessThanCondition: + +LessThanCondition +----------------- + +.. autoclass:: ConfigSpace.conditions.LessThanCondition + + + +GreaterThanCondition +-------------------- + +.. autoclass:: ConfigSpace.conditions.GreaterThanCondition + + +InCondition +----------- + +.. autoclass:: ConfigSpace.conditions.InCondition + + +AndConjunction +-------------- + +.. autoclass:: ConfigSpace.conditions.AndConjunction + + +OrConjunction +------------- + +.. autoclass:: ConfigSpace.conditions.OrConjunction \ No newline at end of file diff --git a/docs/api/configuration.rst b/docs/api/configuration.rst new file mode 100644 index 00000000..d291d0e7 --- /dev/null +++ b/docs/api/configuration.rst @@ -0,0 +1,5 @@ +Configuration +============= + +.. autoclass:: ConfigSpace.configuration_space.Configuration + :members: \ No newline at end of file diff --git a/docs/api/configurationspace.rst b/docs/api/configurationspace.rst new file mode 100644 index 00000000..6b16b0cf --- /dev/null +++ b/docs/api/configurationspace.rst @@ -0,0 +1,5 @@ +ConfigurationSpace +================== + +.. autoclass:: ConfigSpace.configuration_space.ConfigurationSpace + :members: \ No newline at end of file diff --git a/docs/api/forbidden_clauses.rst b/docs/api/forbidden_clauses.rst new file mode 100644 index 00000000..f4be9c80 --- /dev/null +++ b/docs/api/forbidden_clauses.rst @@ -0,0 +1,26 @@ +.. _Forbidden clauses: + +Forbidden Clauses +================= + +ConfigSpace contains *forbidden equal* and *forbidden in clauses*. +The *ForbiddenEqualsClause* and the *ForbiddenInClause* can forbid values to be +sampled from a configuration space if a certain condition is met. The +*ForbiddenAndConjunction* can be used to combine *ForbiddenEqualsClauses* and +the *ForbiddenInClauses*. + +For a further example, please take a look in the :doc:`user guide <../guide>`. + +ForbiddenEqualsClause +--------------------- +.. autoclass:: ConfigSpace.ForbiddenEqualsClause(hyperparameter, value) + + +ForbiddenInClause +----------------- +.. autoclass:: ConfigSpace.ForbiddenInClause(hyperparameter, values) + + +ForbiddenAndConjunction +----------------------- +.. autoclass:: ConfigSpace.ForbiddenAndConjunction(*args) \ No newline at end of file diff --git a/docs/api/hyperparameters.rst b/docs/api/hyperparameters.rst new file mode 100644 index 00000000..e44a485e --- /dev/null +++ b/docs/api/hyperparameters.rst @@ -0,0 +1,115 @@ +.. _Hyperparameters: + +Hyperparameters +=============== +ConfigSpace contains +:func:`~ConfigSpace.api.types.float.Float`, +:func:`~ConfigSpace.api.types.integer.Integer` +and :func:`~ConfigSpace.api.types.categorical.Categorical` hyperparamters, each with their own customizability. + +For :func:`~ConfigSpace.api.types.float.Float` and :func:`~ConfigSpace.api.types.integer.Integer`, you will find their +interface much the same, being able to take the same :ref:`distributions ` and parameters. + +A :func:`~ConfigSpace.api.types.categorical.Categorical` can optionally take weights to define your own custom distribution over the discrete **un-ordered** choices +or you can pass ``ordered=True`` to make it an :class:`~ConfigSpace.hyperparameters.OrdinalHyperparameter`. + +These are all **convenience** functions that construct the more complex :ref:`hyperparameter classes `, *e.g.* :class:`~ConfigSpace.hyperparameters.UniformIntegerHyperparameter`, +which are the underlying complex types which make up the backbone of what's possible. +You may still use these complex classes without any functional difference. + +.. note:: + + The Simple types, `Integer`, `Float` and `Categorical` are just simple functions that construct the more complex underlying types. + +Example usages are shown below each. + +Simple Types +------------ + +Float +^^^^^ + +.. automodule:: ConfigSpace.api.types.float + +Integer +^^^^^^^ + +.. automodule:: ConfigSpace.api.types.integer + +Categorical +^^^^^^^^^^^ + +.. automodule:: ConfigSpace.api.types.categorical + + +.. _Distributions: + +Distributions +------------- +These can be used as part of the ``distribution`` parameter for the basic +:func:`~ConfigSpace.api.types.integer.Integer` and :func:`~ConfigSpace.api.types.float.Float` functions. + +.. automodule:: ConfigSpace.api.distributions + :exclude-members: Distribution + +.. _Advanced_Hyperparameters: + +Advanced Types +-------------- +The full hyperparameters are exposed through the following API points. + + +Integer hyperparameters +^^^^^^^^^^^^^^^^^^^^^^^ + +These can all be constructed with the simple :func:`~ConfigSpace.api.types.integer.Integer` function and +passing the corresponding :ref:`distribution `. + +.. autoclass:: ConfigSpace.hyperparameters.UniformIntegerHyperparameter + +.. autoclass:: ConfigSpace.hyperparameters.NormalIntegerHyperparameter + +.. autoclass:: ConfigSpace.hyperparameters.BetaIntegerHyperparameter + + + +.. _advanced_float: + +Float hyperparameters +^^^^^^^^^^^^^^^^^^^^^ + +These can all be constructed with the simple :func:`~ConfigSpace.api.types.float` function and +passing the corresponding :ref:`distribution `. + +.. autoclass:: ConfigSpace.hyperparameters.UniformFloatHyperparameter + +.. autoclass:: ConfigSpace.hyperparameters.NormalFloatHyperparameter + +.. autoclass:: ConfigSpace.hyperparameters.BetaFloatHyperparameter + + + +.. _advanced_categorical: + +Categorical Hyperparameter +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This can be constructed with the simple form :func:`~ConfigSpace.api.types.categorical` and setting +``ordered=False`` which is the default. + +.. autoclass:: ConfigSpace.hyperparameters.CategoricalHyperparameter + + +Ordinal Hyperparameter +^^^^^^^^^^^^^^^^^^^^^^ +This can be constructed with the simple form :func:`~ConfigSpace.api.types.categorical` and setting +``ordered=True``. + +.. autoclass:: ConfigSpace.hyperparameters.OrdinalHyperparameter + +.. _Other hyperparameters: + +Constant +^^^^^^^^ + +.. autoclass:: ConfigSpace.hyperparameters.Constant diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 00000000..adf4c7f3 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,14 @@ +API ++++ + + +.. toctree:: + :maxdepth: 2 + + configurationspace + configuration + hyperparameters + conditions + forbidden_clauses + serialization + utils \ No newline at end of file diff --git a/docs/api/serialization.rst b/docs/api/serialization.rst new file mode 100644 index 00000000..f57d08e8 --- /dev/null +++ b/docs/api/serialization.rst @@ -0,0 +1,32 @@ +.. _Serialization: + +Serialization +============= + +ConfigSpace offers *json*, *pcs* and *pcs_new* writers/readers. +These classes can serialize and deserialize configuration spaces. +Serializing configuration spaces is useful to share configuration spaces across +experiments, or use them in other tools, for example, to analyze hyperparameter +importance with `CAVE `_. + +.. _json: + +Serialization to JSON +--------------------- + +.. automodule:: ConfigSpace.read_and_write.json + :members: read, write + +.. _pcs_new: + +Serialization with pcs-new (new format) +--------------------------------------- + +.. automodule:: ConfigSpace.read_and_write.pcs_new + :members: read, write + +Serialization with pcs (old format) +----------------------------------- + +.. automodule:: ConfigSpace.read_and_write.pcs + :members: read, write diff --git a/docs/api/utils.rst b/docs/api/utils.rst new file mode 100644 index 00000000..361565ed --- /dev/null +++ b/docs/api/utils.rst @@ -0,0 +1,10 @@ +Utils +===== + +Functions defined in the utils module can be helpful to +develop custom tools that create configurations from a given configuration +space or modify a given configuration space. + +.. automodule:: ConfigSpace.util + :members: + :undoc-members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..2dbe1b6c --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,39 @@ +import datetime + +import automl_sphinx_theme +from ConfigSpace import __authors__, __version__ + +authors = ", ".join(__authors__) + + +options = { + "copyright": f"""Copyright {datetime.date.today().strftime('%Y')}, {authors}""", + "author": authors, + "version": __version__, + "name": "ConfigSpace", + "html_theme_options": { + "github_url": "https://github.com/automl/automl_sphinx_theme", + "twitter_url": "https://twitter.com/automl_org?lang=de", + }, +} + +# Import conf.py from the automl theme +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', +] + +autodoc_typehints = "description" +autoclass_content = "both" +autodoc_default_options = { + "inherited-members": True, +} diff --git a/docs/source/User-Guide.rst b/docs/guide.rst similarity index 60% rename from docs/source/User-Guide.rst rename to docs/guide.rst index edf50c6d..f5f37a10 100644 --- a/docs/source/User-Guide.rst +++ b/docs/guide.rst @@ -15,7 +15,7 @@ Assume that we want to use a support vector machine (=SVM) for classification tasks and therefore, we want to optimize its hyperparameters: - :math:`\mathcal{C}`: regularization constant with :math:`\mathcal{C} \in \mathbb{R}` -- ``max_iter``: the maximum number of iterations within the solver with :math:`max_iter \in \mathbb{N}` +- ``max_iter``: the maximum number of iterations within the solver with :math:`max\_iter \in \mathbb{N}` The implementation of the classifier is out of scope and thus not shown. But for further reading about @@ -24,37 +24,31 @@ reading `here `_ or in the `scikit-learn documentation `_. The first step is always to create a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. All the -hyperparameters and constraints will be added to this object. +:class:`~ConfigSpace.configuration_space.ConfigurationSpace` with the +hyperparameters :math:`\mathcal{C}` and ``max_iter``. ->>> import ConfigSpace as CS ->>> cs = CS.ConfigurationSpace(seed=1234) - -Now, we have to define the hyperparameters :math:`\mathcal{C}` and ``max_iter``. To restrict the search space, we choose :math:`\mathcal{C}` to be a -:class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter` between -1 and 1. -Furthermore, we choose ``max_iter`` to be an -:class:`~ConfigSpace.hyperparameters.UniformIntegerHyperparameter` . - ->>> import ConfigSpace.hyperparameters as CSH ->>> c = CSH.UniformFloatHyperparameter(name='C', lower=-1, upper=1) ->>> max_iter = CSH.UniformIntegerHyperparameter(name='max_iter', lower=10, upper=100) +:class:`~ConfigSpace.api.types.float` between -1 and 1. +Furthermore, we choose ``max_iter`` to be an :class:`~ConfigSpace.api.types.integer.Integer` . + +>>> from ConfigSpace import ConfigurationSpace +>>> +>>> cs = ConfigurationSpace( +... seed=1234, +... space={ +... "C": (-1.0, 1.0), # Note the decimal to make it a float +... "max_iter": (10, 100), +... } +... ) -As last step, we need to add them to the -:class:`~ConfigSpace.configuration_space.ConfigurationSpace`. For demonstration purpose, we sample a configuration from it. -.. doctest:: - - >>> cs.add_hyperparameters([c, max_iter]) - [C, Type: UniformFloat, Range: [-1.0, 1.0], Default: 0.0, max_iter, Type: ...] - >>> cs.sample_configuration() - Configuration(values={ - 'C': -0.6169610992422154, - 'max_iter': 66, - }) - - +>>> cs.sample_configuration() +Configuration(values={ + 'C': -0.6169610992422154, + 'max_iter': 66, +}) + Now, the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object *cs* contains definitions of the hyperparameters :math:`\mathcal{C}` and ``max_iter`` with their @@ -69,16 +63,17 @@ of a parameter can be accessed or modified similar to a python dictionary. >>> conf = cs.sample_configuration() >>> conf['max_iter'] = 42 ->>> conf['max_iter'] +>>> print(conf['max_iter']) 42 + 2nd Example: Categorical hyperparameters and conditions ------------------------------------------------------- The scikit-learn SVM supports different kernels, such as an RBF, a sigmoid, a linear or a polynomial kernel. We want to include them in the configuration space. Since this new hyperparameter has a finite number of values, we use a -:class:`~ConfigSpace.hyperparameters.CategoricalHyperparameter`. +:class:`~ConfigSpace.api.types.categorical`. - ``kernel_type``: with values 'linear', 'poly', 'rbf', 'sigmoid'. @@ -106,43 +101,56 @@ To add conditions on hyperparameters to the configuration space, we first have to insert the new hyperparameters in the ``ConfigSpace`` and in a second step, the conditions on them. ->>> kernel_type = CSH.CategoricalHyperparameter( -... name='kernel_type', choices=['linear', 'poly', 'rbf', 'sigmoid']) ->>> degree = CSH.UniformIntegerHyperparameter( -... 'degree', lower=2, upper=4, default_value=2) ->>> coef0 = CSH.UniformFloatHyperparameter( -... name='coef0', lower=0, upper=1, default_value=0.0) ->>> gamma = CSH.UniformFloatHyperparameter( -... name='gamma', lower=1e-5, upper=1e2, default_value=1, log=True) - +>>> from ConfigSpace import ConfigurationSpace, Categorical, Float, Integer +>>> +>>> kernel_type = Categorical('kernel_type', ['linear', 'poly', 'rbf', 'sigmoid']) +>>> degree = Integer('degree', bounds=(2, 4), default=2) +>>> coef0 = Float('coef0', bounds=(0, 1), default=0.0) +>>> gamma = Float('gamma', bounds=(1e-5, 1e2), default=1, log=True) +>>> +>>> cs = ConfigurationSpace() >>> cs.add_hyperparameters([kernel_type, degree, coef0, gamma]) [kernel_type, Type: Categorical, Choices: {linear, poly, rbf, sigmoid}, ...] First, we define the conditions. Conditions work by constraining a child hyperparameter (the first argument) on its parent hyperparameter (the second argument) being in a certain relation to a value (the third argument). -``CS.EqualsCondition(degree, kernel_type, 'poly')`` expresses that ``degree`` is +``EqualsCondition(degree, kernel_type, 'poly')`` expresses that ``degree`` is constrained on ``kernel_type`` being equal to the value 'poly'. To express constraints involving multiple parameters or values, we can use conjunctions. In the following example, ``cond_2`` describes that ``coef0`` is a valid hyperparameter, if the ``kernel_type`` has either the value 'poly' or 'sigmoid'. ->>> cond_1 = CS.EqualsCondition(degree, kernel_type, 'poly') - ->>> cond_2 = CS.OrConjunction(CS.EqualsCondition(coef0, kernel_type, 'poly'), -... CS.EqualsCondition(coef0, kernel_type, 'sigmoid')) - ->>> cond_3 = CS.OrConjunction(CS.EqualsCondition(gamma, kernel_type, 'rbf'), -... CS.EqualsCondition(gamma, kernel_type, 'poly'), -... CS.EqualsCondition(gamma, kernel_type, 'sigmoid')) - -Again, we add the conditions to the configuration space +>>> from ConfigSpace import EqualsCondition, OrConjunction +>>> +>>> cond_1 = EqualsCondition(degree, kernel_type, 'poly') +>>> +>>> cond_2 = OrConjunction( +... EqualsCondition(coef0, kernel_type, 'poly'), +... EqualsCondition(coef0, kernel_type, 'sigmoid') +... ) +>>> +>>> cond_3 = OrConjunction( +... EqualsCondition(gamma, kernel_type, 'rbf'), +... EqualsCondition(gamma, kernel_type, 'poly'), +... EqualsCondition(gamma, kernel_type, 'sigmoid') +... ) + +In this specific example, you may wish to use the :class:`~ConfigSpace.conditions.InCondition` to express +that ``gamma`` is valid if ``kernel_type in ["rbf", "poly", "sigmoid"]`` which we show for completness + +>>> from ConfigSpace import InCondition +>>> +>>> cond_3 = InCondition(gamma, kernel_type, ["rbf", "poly", "sigmoid"]) + +Finally, we add the conditions to the configuration space >>> cs.add_conditions([cond_1, cond_2, cond_3]) [degree | kernel_type == 'poly', (coef0 | kernel_type == 'poly' || coef0 | ...), ...] .. note:: + ConfigSpace offers a lot of different condition types. For example the :class:`~ConfigSpace.conditions.NotEqualsCondition`, :class:`~ConfigSpace.conditions.LessThanCondition`, @@ -178,11 +186,11 @@ configuration space. First, we add these three new hyperparameters to the configuration space. ->>> penalty = CSH.CategoricalHyperparameter( -... name="penalty", choices=["l1", "l2"], default_value="l2") ->>> loss = CSH.CategoricalHyperparameter( -... name="loss", choices=["hinge", "squared_hinge"], default_value="squared_hinge") ->>> dual = CSH.Constant("dual", "False") +>>> from ConfigSpace import ConfigurationSpace, Categorical, Constant +>>> +>>> penalty = Categorical("penalty", ["l1", "l2"], default="l2") +>>> loss = Categorical("loss", ["hinge", "squared_hinge"], default="squared_hinge") +>>> dual = Constant("dual", "False") >>> cs.add_hyperparameters([penalty, loss, dual]) [penalty, Type: Categorical, Choices: {l1, l2}, Default: l2, ...] @@ -192,27 +200,28 @@ Now, we want to forbid the following hyperparameter combinations: - ``dual`` is False and ``penalty`` is 'l2' and ``loss`` is 'hinge' - ``dual`` is False and ``penalty`` is 'l1' ->>> penalty_and_loss = CS.ForbiddenAndConjunction( -... CS.ForbiddenEqualsClause(penalty, "l1"), -... CS.ForbiddenEqualsClause(loss, "hinge") -... ) ->>> constant_penalty_and_loss = CS.ForbiddenAndConjunction( -... CS.ForbiddenEqualsClause(dual, "False"), -... CS.ForbiddenEqualsClause(penalty, "l2"), -... CS.ForbiddenEqualsClause(loss, "hinge") -... ) ->>> penalty_and_dual = CS.ForbiddenAndConjunction( -... CS.ForbiddenEqualsClause(dual, "False"), -... CS.ForbiddenEqualsClause(penalty, "l1") -... ) +>>> from ConfigSpace import ForbiddenEqualsClause, ForbiddenAndConjunction +>>> +>>> penalty_and_loss = ForbiddenAndConjunction( +... ForbiddenEqualsClause(penalty, "l1"), +... ForbiddenEqualsClause(loss, "hinge") +... ) +>>> constant_penalty_and_loss = ForbiddenAndConjunction( +... ForbiddenEqualsClause(dual, "False"), +... ForbiddenEqualsClause(penalty, "l2"), +... ForbiddenEqualsClause(loss, "hinge") +... ) +>>> penalty_and_dual = ForbiddenAndConjunction( +... ForbiddenEqualsClause(dual, "False"), +... ForbiddenEqualsClause(penalty, "l1") +... ) In the last step, we add them to the configuration space object: ->>> cs.add_forbidden_clauses([penalty_and_loss, -... constant_penalty_and_loss, -... penalty_and_dual]) +>>> cs.add_forbidden_clauses([penalty_and_loss, constant_penalty_and_loss, penalty_and_dual]) [(Forbidden: penalty == 'l1' && Forbidden: loss == 'hinge'), ...] + 4th Example Serialization ------------------------- @@ -225,24 +234,16 @@ we can choose between different output formats, such as In this example, we want to store the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object as json file -.. testcode:: - - from ConfigSpace.read_and_write import json - with open('configspace.json', 'w') as fh: - fh.write(json.write(cs)) +>>> from ConfigSpace.read_and_write import json +>>> with open('configspace.json', 'w') as fh: +... fh.write(json.write(cs)) +2828 To read it from file -.. testsetup:: json_block - - from ConfigSpace.read_and_write import json - -.. doctest:: json_block - - >>> with open('configspace.json', 'r') as fh: - ... json_string = fh.read() - ... restored_conf = json.read(json_string) - +>>> with open('configspace.json', 'r') as fh: +... json_string = fh.read() +>>> restored_conf = json.read(json_string) 5th Example: Placing priors on the hyperparameters @@ -252,43 +253,37 @@ If you want to conduct black-box optimization in SMAC (https://arxiv.org/abs/210 Consider the case of optimizing the accuracy of an MLP with three hyperparameters: learning rate [1e-5, 1e-1], dropout [0, 0.99] and activation {Tanh, ReLU}. From prior experience, you believe the optimal learning rate to be around 1e-3, a good dropout to be around 0.25, and the optimal activation function to be ReLU about 80% of the time. This can be represented accordingly: -.. code-block:: python - - import numpy as np - import ConfigSpace.hyperparameters as CSH - from ConfigSpace.configuration_space import ConfigurationSpace - - # convert 10 log to natural log for learning rate, mean 1e-3 - logmean = np.log(1e-3) - # two standard deviations on either side of the mean to cover the search space - logstd = np.log(10.0) - - learning_rate = CSH.NormalFloatHyperparameter(name='learning_rate', lower=1e-5, upper=1e-1, default_value=1e-3, mu=logmean, sigma=logstd, log=True) - dropout = CSH.BetaFloatHyperparameter(name='dropout', lower=0, upper=0.99, default_value=0.25, alpha=2, beta=4, log=False) - activation = CSH.CategoricalHyperparameter(name='activation', choices=['tanh', 'relu'], weights=[0.2, 0.8]) - - cs = ConfigurationSpace() - - cs.add_hyperparameters([learning_rate, dropout, activation]) - # [learning_rate, Type: NormalFloat, Mu: -6.907755278982137 Sigma: 2.302585092994046, Range: [1e-05, 0.1], Default: 0.001, on log-scale, dropout, Type: BetaFloat, Alpha: 2.0 Beta: 4.0, Range: [0.0, 0.99], Default: 0.25, activation, Type: Categorical, Choices: {tanh, relu}, Default: tanh, Probabilities: (0.2, 0.8)] - -To check that your prior makes sense for each hyperparameter, you can easily do so with the __pdf__ method. There, you will see that the probability of the optimal learning rate peaks at 10^-3, and decays as we go further away from it: - -.. code-block:: python - - test_points = np.logspace(-5, -1, 5) - - print(test_points) - # array([1.e-05, 1.e-04, 1.e-03, 1.e-02, 1.e-01]) +>>> import numpy as np +>>> from ConfigSpace import ConfigurationSpace, Float, Categorical, Beta, Normal +>>> +>>> # convert 10 log to natural log for learning rate, mean 1e-3 +>>> # with two standard deviations on either side of the mean to cover the search space +>>> logmean = np.log(1e-3) +>>> logstd = np.log(10.0) +>>> +>>> cs = ConfigurationSpace( +... seed=1234, +... space={ +... "lr": Float('lr', bounds=(1e-5, 1e-1), default=1e-3, log=True, distribution=Normal(logmean, logstd)), +... "dropout": Float('dropout', bounds=(0, 0.99), default=0.25, distribution=Beta(alpha=2, beta=4)), +... "activation": Categorical('activation', ['tanh', 'relu'], weights=[0.2, 0.8]), +... } +... ) +>>> print(cs) +Configuration space object: + Hyperparameters: + activation, Type: Categorical, Choices: {tanh, relu}, Default: tanh, Probabilities: (0.2, 0.8) + dropout, Type: BetaFloat, Alpha: 2.0 Beta: 4.0, Range: [0.0, 0.99], Default: 0.25 + lr, Type: NormalFloat, Mu: -6.907755278982137 Sigma: 2.302585092994046, Range: [1e-05, 0.1], Default: 0.001, on log-scale + + +To check that your prior makes sense for each hyperparameter, you can easily do so with the ``__pdf__`` method. There, you will see that the probability of the optimal learning rate peaks at 10^-3, and decays as we go further away from it: + +>>> test_points = np.logspace(-5, -1, 5) +>>> print(test_points) +[1.e-05 1.e-04 1.e-03 1.e-02 1.e-01] The pdf function accepts an (N, ) numpy array as input. -.. code-block:: python - - test_points_pdf = learning_rate.pdf(test_points) - print(test_points_pdf) - # array([0.02456573, 0.11009594, 0.18151753, 0.11009594, 0.02456573]) - - - - +>>> cs['lr'].pdf(test_points) +array([0.02456573, 0.11009594, 0.18151753, 0.11009594, 0.02456573]) diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e8c0f3ac1e679f4c2e644e9335d64129e947ec73 GIT binary patch literal 89809 zcma&NcU)6nvo}ibMIiJZP(z2%yVL;E0tgBMV(7j1-b*L}6i}pt6lqcgfhba?7wHg@ zCLQVh4gQ|;6AS|b7h-4d)bpv9rWDM@iQmG~<)szBuahg9j)5T~@9Sy-bFlJcdue53 z=PV1{YwZBC*;&d0jYPEswOo~~Z0*$i-L3TfwGCkY4lqeepu8NBjIR`$fs>V|1)Hyv zqqB#UuPpE{Un%tecee$AY=4J%I>-W_YU#2mxwu=giSmo`3j*bc*ks%-t)=voRsS^^ zeI*OD_4IU=5)kn5@!|Io=67+o5fGA;loSvI3xL6V=n;G#e$Jj2zI@Ie9Cs}K;h}8h z0du!=^|W(wX1n8Q@zTZ1Qx*tB)7k#T+{)MPKe;=5{7ZebAq4Iy0z&+P0{?ZmtF4Qt zi-)a?>%S%XZ*%@l{vQ)P?X3SV>)cWP$8fay{`VDpEnNS%!CG4X@57y({+k*eo+{pG zL;TCA|4oJeSir-;&(%sm&&tEa%N=H=;%(*Z$??|`cXp9da&dHV*GIdCl`K%`KX2>W z`C2(XRkm}oa`r$Q9ZeGy6#RdXpZyoIh~z($|82K_7;Cv$qWKE{o%o+5btNTTcNc3r zM>Net52DDXuA(F+Bq=7w2j>5W&7CQw)SWDBtTe5hZ9Hvdf&a0Ebj>a7VIzO zPEALzJ3o2iY=<@s`0vtxNl*WiCJU4l6ZkJHJ^Not=&h5|Li@O-g{OtGg(q5Auprt; z1;zOUMGS-lrGzD=gv59R!BYRS%-``YmUh;D|6il;{F+S$Jzh&oO3lv0)5YEI@2!8F zUEj*>pR0c^9qs;l`JJENjkkc^X(tPGb$7A!f>~MqRq$^V+)+JTtUY}!+^rOB(Az8v zRIs+TL)*cR4IMysHqKV=Y(o5!{QvC7zvuYaTA}%&bra$fL~j9FHz6re2`NFzf9WRh zze@Ah^Zu{W{6qc!A&rnAzsP^6@Xw@wi}Qam^}ocC5x4__|FD&e!2hu3KLh>?a-h}t z=N=l7(9kCEZ)`(f{2LvuoY9czj>f)M_iw1s_!?%1cGmylq!<_=gcoA?v7*Ag9{H^N zbf|6&88hcVnhL#|X?S|+Bj7-uIc<=fsBXY#n@CU679w0|MlkcHK&iusBtbkdHfi)z z>EkYk93qVs-nzq?yC2YXEeajU*4Q?jv*HXHX1aSjr>VIn($=M1H3T2`Vb>@ zReR=vfkF1|?ho@74;do{1{;RDvVwtc_VzoU%;#3;H}b+JsgC~aDY{Ae?4dys@OMvEkFYHN>%J)KH4r(5Jcp97Gjcn^I^>j^n2F_M9?-z84U~r*s00(Z-VRy9r`Yk_hJtkGCI&$T+5GGwNy{X?`oqRH` zlyEgO?xN4yppE9@)okd=z#e|s&w)LNuza#*Fbfu5X#2q4rLa7?<>D#kR#?u!(~d90 zTcMLkh8IuPyqwlNAi^-q2#O==ziY8Q*(0Pw|L0YNP5%Xk7=0@m*|XB7jZ_42n&ajoG9X5_YXXMb$;rVH6^-I za9V*txM1Hg8UKd~N1AH%Q8!Zah3euoLFUlo!a1rxs_T#C$2dbR(#<-OvJp9=6LnlC z-=WIEgf$|Gb8qMiPa(Ipe$mq)YVELVI3}?hOmi zEE=~1xK9A?l0m_ zCuXzDm1%p3RI1DGbAo@cDn(x^fue#_vd6A5Bvk^@wtqn5n8yf%$GriR?+tNh>nK1E zK4sNXgTINdcI|wHs}mI>!6+^ip(Rp2(SH8$)$ggi-p^=3sR;iH8fgU-WSoq3N%iZX zFQ-^ynv8Q%-l}I29_#~w2i6PUoSbLXVm!_EGfh8dgv_EfGl(MfL@)blBniIn6t)Ad zHc8Rf&CT&^ka%l{8x7?Ql9arE9)%QM0qzYK9QmS$mT>-E2~bEy|ML>7m1DH=+2`gb zS>qFleL|5F%Tv~7tq8$|+t7ee{F*mt(K0dqYSj0hc^Y4WK;oXSq~I%Bo|4NMs5xG5 zSyD1ELQuX~YRO_O8s|K4VDr1$;s*XMt}~`{|7Ua4jxu@}T%9ub1tUyzYgD_+<;=1u zCm5qf9z4`D>0%$p*`YP%8Yc7+?bQh1>1Yl{sA;bFNBL80GkgT5c*<$8;NH4meLg%^ zXoHFFvGx=BIr(6jPacIl!XG={#sW^nnW%atU1v4mCj(1aOtNZ{H`*v~%bMz+UF(r( z(P_TmwFq^K$nTVj$(THpqb;tm(f{!ty|CB&+-&xVx96V(T#kS+yZahXhPZvI`vN+o z_JYVO-)}BN!_VS+e+qLQz6r{`_T!eCdcYooZ2mm8z0rReR8a-}Q7Gneig{Y-e{#RH zJxEdtg5C=euF8vj;qFZ92T>#ipGt!R`Dkh!SfTYC_l{;Ga~L&VaTvvtbSCsy7kAb&Tdt9 z#r~7lM_VPtk)!{$uNkkV1g=%YfNB=|TkFN)p{sy8a3qsf0{E95z^Vh^mnmVG9;Vp| zyNi8>j51^}k{$U4*}YPbI`|qWgT5&O)xx^$RrH)Q}f=&aSS`zdFqbkoj6zh0+FmF(juwQ&nPjr`oO zO;pbhhwvxhc)g!{Si6t3Yu85`K2b7mp&`uYF8(xwvfyJ*b$r9#imFrHB15wNI4Zx^ zI67VA_OZ%I%~MWWXc#dj!7S5McBI=;Ls^xGFJ^>=8xk@}8u+_7&Pto8A!E>xgu9~W z+cb|7r)c>3AR?j&k!`oI@x0f%Mn?IkJ0%(%L<^uaA!x+a)Wkhe5M2qtHYrfKv1Itw zKA#NVhd?zhe)Y2LsVX`zOw%RD1Gt?e=%=yiS4}B`gT| zY0AFl#tw~&rS3f!>CTMq6gNaeeYAIafgI^HnJ=F!pj=C9xy_s4SlcY7z)}ZM0cj_H zYTi-S!0V(Ifg0~6V9>uBhFLMY^1IUazjpnkC<9mez5h(1R~8x>MA={Sn=|V)*%{@J za#5oWR$ucODnf%;3g(rR-AW37E7TlwNwt5kN-H_HcEjwW z$(k2GWx?C1iOXNy9;L4-Jas-%H{&im&Qb8%68nMP9HP|5P_(M5FMu^Xl>T$X#n>*7 z>z7TTHDr9RSrxb!bga6^n_r8l7wLYxdaU;J0~1Z2*t63@*aenUNd@$z6Qt+MnygG{ zlkb@HE=BOCSt(8-U@L43>=8gG?Pjk|#N1jLD!IW@$Lx;bocz%%wLXbab$vMkM7%DK zlx=>Mi$39$Q=PZF>C!Bvbh$Sxpy@Jmw0`cg4;wH!)Hb&GQO-rZZJfe}Z0v$*yn4tv zsMPuE`#0VIt~K*0oYou_RlgA2xi_IhbML~Yuw9BgD;k4Qxk&txbE{1yMsUAPf$arZ zuX;19UQ%`as#*32$PV>7W)pS@{tYq7gooZg-gA%@PEy)|k)UhfpLsznX4Qk?D&Wdu z-_u}cJp`+vYNU|BRAGKh80m#g%T~x6-)QQzAtQOOIUl}5UhwAE5J96;K|Ylvfepv2 zGo1q1gwpli;~AlF`0;(UrR86}6srMg=nMb`=SuKS{~AoNPsN#MxToUmoi80b`-V@U z;mgCq2`d3_#$-Tz@S@7WFFCjdAPMfqPZc(t$8u6ka?G0cS<3eCWjA(D-sO~V?;{pA zN$vw#HPRc~^wH6*N7`RRh^;-R)zvKF$D| zvzKS+wn80rBSoC0LdEmm{LBY>Y~so(e;g~*5;R4A4;wBNIdq(P%zE6>kyrpY+hfC@ zFjP)seu+Mr&<92epoU^!14RNDRadP36I^fGhIvNe%AjH109}-UW6FhJXyCuYUqCkL zdbS!CjU4Y+0}mNgUb^A`a5a0tFq?9MoI`+b%@HLE0>>SDl?vLu!yK?64>smuXEX|1 z{z?bJG%H5&wy~Jtz{df=FHQbGeppW?@2PC|Bx5AAQwTP3+sk=lR;iZHM?EEd9&SIB zF=?;ow@rG5(Ll=k4THN@g6cOov%3)6%G&bDPajDC&0Hbi++QeuH{g^b|9eQG+?K3Y z_CLE37gnZLdqGrxwpG$DniWw>eon5NP}w#TJ_CF{IGjuRhBFLRQ2)kgR2|)!+3@vbe`rqbE+&Tte(K29j3Ovuw?t9ezEqJ`4bO3_3t3^n ziakmkY1RAb+3;599U^w8tK*l?$7;Ov>j?D`E!fxAVhgG(7;Z=%XT z{YOLqzNLf<-ua8rVr1Zi;+8*(%yJ;Im^d?weh#ZOzkS-I@MyTK*LGM6zXj^)A!qz_ zDS1;m!*Xro;$-$Ck1J0U#~548n6&y2w;9^zdkv91-;#9!ZB5p~_Huf?^k7;^nIxsJ zDHgB(WN@%IJ3yIF0|8#-O8EU)l6VUaO^=>wm`((1NyhgJR^i-)?K*u||8a~q6ZKst zG~QcZSbr=F{dVPQ+5AIFPbQY8BN@nXMuw8_y+`(#L8a^TQTN!5+DO@(oApjm*NyXv z%$dMGMtp46**U|$IuWB8Pngo^c^YSTKFjaiJlBU|Q5)HT?XT19fe-SpPZyp zAC!uzLX;8fJscPFalLLHY%TIT@NAM}+b>OY^uKQvPa7ZvdvL+O!E6O4lfXCw#_I~a zmn(byIECTVCe#SqgtF4(1=tlFe@|AWzHKHZ#VwdHetPobJ0l)2DVH98F|N6y`@6Nv zJQ8@G$M2O3t;+lv(gx{(@a4AkLWV6wSTK^yn7JDBO;(DT;Wv^16%_Kj}J#G4`1# z&1U(Q_bl9*uw=CMB^w+TtPS62qu%34$5xdsO)#W-qYPHqc$(T}%7wz25;8EQyN?#E z+20S@A#+QU$<*ZcN%n14QZnf~eMIlHwtMyfdryIQ)@$M+xQ^hA4BY>a!kl6KLFt+g zb%U22Nr7gh4&}R)cYOzLo|?ovmv5nlYN>w2HPK%W%^zO_+8GnMN(arz8#-c~eA|{1 zG$NL{3GE>i+k9cdP#V=r=JL*7!aq-~$)MI`e?z36gsZ-04v()qr=*d%)uKMsEVAIZ zhvT3@!VY40c?(>O^(zw!S1xY+qH1JNgoqCrag$A0kyf$J-(uhRJX08~??5nCDtGCV zzGN=r@Z7u5_*Od9oDK5RNVnq;aaac%wkMZ+fq@=T9oA&~RDp9KuRFmNj@8bG;*-;s zC#4@=&>H;s6_9;rb4`j_-Mxj*&^>Gc-MKCk*7DwoJ#Pba zLv~DbiEfzHKAjW3pkHOgEd(k3G~{fd^|}b=0aKmHMx7qBx#ILy$`7y8gb!z({%|u> zf0#(}bxGgsWEewZ_=d-Qkx9`atUXW)ntRlQEw6MMu?ub#jsKXcUEFv)Tc>L{F}cy+ z^k!Dd!l=ObTs)7TPTeO3{fsBnJ&{f}!M`nwtmX=_pzoqnt?aGB9#Z+LsQ>G_MWBFc z(*E@poDMTbIpMmxpz(Q3Uf22d!2Ej-NDn`!ly&=#QJdP0F?fA33wKGZmm3LtJK|c?kC0CfZrL z&C^T-N#IP@`4n!)`yq>!AL5+^hYzRmaLlUK2@y&%KQ4af!*TysDRxNBS!wF(H|T9i zdIU;xWxoRQwqC@Yn}vbW1}AsPu_$y8+Va_wU@~nu-i`$Z%?Pc~7=Y0=lS>kA_C0+; z)(<=*EU&DdJj|z6IjB~iF{px~3otNE{qg96xDH5JR0DwEO>)7iM+(Y>&;EPDffV2; zZ^Mgr!s!HC2hO=LMh(Y+PNC#8l343?*CHxi+m0Jd6|zou0*wrEso*WBx-wnEnNob< zO(nZr(=$Kf)h4LPqpRoxy7XFo-c-5p-Ql-AG8yh_`2)GnkUgbN7ng$W&I{4tSOx`- zuXjON!aJ+Ww;ER3T{BNG|UrD?}TM68~7r z7lW37$&|8yQ72msrO(-UV|0ysUSPzz@2Gm&4dBzBZ_g$^VK9M%6P5wI z4iK)ruFbKSP;&iTgVUEqVTCE!7MT98Cki<22?#lJLUCBr8qNRaJpDkE!$3+f5jT{A zoa*=~=6vF;F|+EQ3BUIRQavv(VM2N?Nm6YeseBnH-^0BNgBRVJvK_?8*d@BI{R`CH z&<}pYkC{6u3{G#X4OmSHwcP}g3DtC~=QsRro??A|?OR+T7uB2dxWa$znOhd}o6H_!a_NT9rt%#hyPC$o`is%IbhVs~$ z_2$vIlZW&5PN+aZ*LRI&a}X&zw6Jcpz*m3M+{6rifXpShk*i242x78ZnZuaUm2nS| zu6amlO$7dPp^Nv3&tz0oN;;3gBmbJ->TLr+Dp(%NAFHwHXYFSeU(!`m@pans4=#`+ zW`=iVw&plxjn!B;ss~d07AUAeCyNbd5F?5e$06*4(i`?fqzH;yP4m#uKCN~#hRJa~ zKekFy9qqia-@{l;1#7QcO6x^}$K%QXVxbJJE%XkoMItkxAcEX|6%Frclt4I$%m z&G%i}g`&77TBp-{x=RN*Hkw)0>k<8lx}*nJp-yYb6rM8jNqV^nP|c_NZ4^Tth?%!9 z6p+vf4JYZMSu@^o3p(bEDXQF`8Hz($PgK_2sQZ!&)`iJbCX*pUBgV=T0VA~+?4wry z%Yx!;(XCbwIp)1}O zM1KfHZXL`cJqubz^X*;fudmTw4G(F~;SIPq?bKPSu0GKUp6|4JFE7MoJAnP$X0GS=&9BF6-|YkifW=`r2L{S*G^HAA!=UQ*xo9HpKKh)aU^oFEpD-_4ys#`riBDusL3jvkj-t&gDLDnQoIYCQ*1Cx#_UN&@?BWk1h zGE)F2m^eH7TSI}fs3Cb{k#F**&bOMuXCQZDUcDm_m?XLmm3Cj~=Z5^M5!X{}&&r%3XL zSk)^1=2B59+KD{c+(W-w$nN zphFs&p&H?GQQ2&6a)%cqLRVs=(l&RVHf{-5={6OL;EfjwCpmN(xuTfn4-a^ej8&cF z8xX+0y8H6-_LMq!O(#C^$0;z&Yipl2{M1U^zTydwb)HAv?9t+}+WWjL(kAyTmHl9U zGD+6&s}<>T4;IvX!Q6UCA(h4e`r^5LQQ~m~!!7YQfd^CAfi#jffy{Ttz|&1U?M6aN zPrb5;3NTX0b+eF+xe&j9av%Ddd6ONVG{#K}y6*Gqq_ME}lCl{_iSxw492QPj& z|58bP8@oGG`OR}8=x2zndC#PVC!H+qQp8E^U-ZnRxt~}d=b}vXOMz<|3V+gjcFSAC zS80)HBMC>P_sv09IEkd7@%FkV_0S?vAnz*y3?I;+tjDoKk8G)fRrhbURpNiY{oyJH zgO+_AOc%iVbWb8~jaR)ax{2y@b+B32x)U4>4?kl42&3E^2K7Yd_^!2A0)C{N4GCZG%O>6hyawGlp| z#m5I1YsG4O?QcXTg>WE2Q@}wieYpTO8}*UeO;x+02HsoS?*m(k8mFr)k5tz`FboO& z4va-zdj-GBm1LX@&d~GeV9ca?x%;e<{V8ad%s_6uya?)f$&7rp!I1bXOENy`R5Z)! zlPbDy4!6TJO9J{1>&nbBSbu;-pU;I>+9esV0B&dc-MfcZc0Q0vk@EpH)5c+IsG2x;r0pzm#h6HhaTOTU& z)Vfggoc{{OKb8@wPYf8XE1Xm>{FpSAw{BZ@b8ON5GJV0DiRPsUI?msuN|v^LS6mhz zfTV0p2!Z%U8a($E!p9AU^JVTI>jh>lA9wmtwFir+`BufOZ6coYh$#haCYb8TB3(ku zL^E|5Uddl2FLQlvQ+vdtfqP(`ml+o9+&i3uk*!sOXvd&;v&f3j`#|jUtK&>+_+eDl zcWlHi_3{I?AsS!Z=vO02sSO0JKz& z29?;}=v4I-Coq_tG6Vdoj&)Pmto+*w=8KT!K4o=w=tuJWSkp3x>#645&-Yl_d^>`~ z3!Ji*EY>$=mD7mYNcXF;a^>Uo=MxZkW?`O&7je~=A84)gHM^$r?&8k|!ElqZru zPZR_T^`}q2vK=!W2Yp{N>G_OwoKP;;Fdw(9EQf5u_&0rja)E3d zu+FAX<>I@pL*rEy#!jz=IIRRgWucvTK9q@nb)Lf?kzXrJ6UEaF2J+Y zCjC6R?jbjO)NbEHOe0<6xQ^Lc4Cc2}DDRBBr;&nbIxbv!weui24?Q;03CZ>JRh!-k zE=$3Fa8R}_E^=1ap{h?g08Yk!Ht$Qs=nNgfV~YCL%>79$0D*bN)}rcybMuDrGk^Yn z^a9XN6;7Su6C%Ga8c1fK#2`78 zq(HG3To58hUU<3RRf^Mn3cK6Qq@t9V8)*2YK@2l70%J_`!~Ap7Is{TLKS){)Ru8)5 z{j%P(wJX~OZxw!jW35=4 z=m#cF#6=9fDjM#f$O^A&EB;oOBGAwqa!-Qo#APP%a|G)8!X>qGs+Q-=a4N(#p#hW? z`%P&*e~WL^L1J##)1`od{jTJ4S9WOmZ4H6@V$s?*E->lYjdI{&%ti= zM-b6q5M7g5d;)-*ubHN}riU52rx1Pq>8WSA@mj$UP1MOA7RrfJn2{a#D)t3zuUZ1g zh+hYNrtqBJRTks{qb;D|xLaue&oO+A#D$Zk`ZBO+Gmtas4P=mq(5#s}U+S1M6|en~ zt9a-`*BPaG&w~2t+(I?$d~bh?ZYjXRHA8&!xv$}{C8`NWx-K~BNHMjWOl`Y%YCII(nD(Gs@gYO_r!Mn3L(NX z)-OFMjZ}J-+=PpH{NnYHI&>L_OHb`;PZ#B(=E$vyrzNoIb4@jYun^BCV#M7+wi41{ z1z`GO_qF_x9=(>|c>W9yxYGKp`Do{`J?6x3V)*#7fk4AUbts@_oPVbR2p(}{cZg9D((c8AGfw&1$7;vwhoIbbX48z?ZV@kSIeV^ z4p4)siDWSV>D$<EN8m%Uy3Z zY>o{E)&ijxO%X;e092l+K@EO!E+6>=!v@1t^g{d2pE}3F3Sb#jLEi!iitBDM_*S%b z&RM?n*htzZr$yRMDs!3vJjqV-4f;6g=({-+uXTAU**cqOCEyoQ8YS!&&3SLuDGabI zHouFkjQB;rc(2m{!)RuiX3c26iwc2!(rG4%cN>%F(kIbr^=QO2SLHE`|4aNvA&B%` zaMPjaeOMZSb^tTxO?Z2P&{}alM`h?Fj``MO-9b0ms$;IwI%OfPwD-`sSL%4J*cm`2 zS^M>CV$c>$I@=V@zZ*kCT9&5lw(9;Y{vHF0ytHk2!Z?VXO6{Sm*cU6}&e9xTpUrhO zNv26*{CD~ZQ1LKM<%>jhZj*uyOI<(^pkd0Nxq*7;O)UJdSY@q&;f$cgJZohn2?rLU z;0AGNNV;N#TBpzP-o0zKYkUNB)w$PZbj6RtM*fhn&*AjOa0Z?=lVXKnHI{YGCtmSZPd*eWmi66++@G>Euenajs_{A?uk6!Ko z^Tg@{!6K-56n5BxE(BgVQro+@7l0V*884mqiMf&<6xM$dByFE|;k9C!*k7AEnAt*T zwJb|Jy3PRWN1}U58eo*=d$Rb){fV0wjE}y$uNThNFA~TJw~px8sK!69x1r;E5QBZ& z#1-?bcREP4YjVVhLv%GE>|A->mp*1}qa$|nMS_(HiS?SRl-Ru9hHpc@Lt!Pd_a>M~ z&mu>)YI272h5Mp|x2IM2*#Mo=bwOs~3d!+t)~N%cq>I5*yJ{$)gJ?t56xbr%!3y;0 zAYhoe*tWKzbY*cRS}n6$iUn@^U~cuQw0+MfHJwY-tr#N5mVR|`+cMgQDj;gKQhr#z zvqU37A%WE(_2KA(S$WktM4_N+xNxf~J0Mu9Zoxx$ZSrY>3G9PHK}<*g=cdE%%*@&_ zXU(hn!kA+&(1P1bj_=h-Pt+IvHt3%HKy^y4W9&GcZV2L*d6iB)_`W;eyQ3hgq& z7mAJ@Pi83S7xDG4f@Q*iycq?c@4~h}4Y}+rLC%SrgG*7HdI_~GXUy@)hOa`}9N6!9 z4yD9g;7auP!Xv@iuTS4F>-W(wRL({_-+Px^0d4d6;W{k7Tvsi-vdh=D!i2RY+~Gn4 zdm#)^4X@a5RZLvC&yuiXM?IWPX=HHTtQEx+Rw>QAE4{ryq*3bjP@E zd(T)9kA4*&+vSm|Ubo88j1sbAwC$7zU?kwaNg1n&|7tj=*M`u5&=7E;a^D?ey`eR|WXKM1EJSstJ9! zKMk5SW~k4|>^~W7=E%VNX>a0Aj$yVT)j(g1cdBKT9tfou@+e))qKucMuJ<*1%niDi z#EG1G)%wLt-=`$_>I}n4Z}~1gDi{`_!r>(6PqJ!LGZ_9n;|TFuTi;#+NUx{$(;)4T zWxaj+{6W2szDuc2v2n;IGC}ka`d&PoyM|TGQ=k@?T>L9M$PkZ(H0pTcyj}c&>)^C7 z;D$BSA++cFnLM)^1hoT_wzpRHpjQ~q&MjR3dPoseC1j0_?m(d;-|4;QFCTBIJlSZ8 zLvNi?(0h(mZc?8EOdJCbIh);4c!uFb}NrX{zS6L(9#G+j3i8NYGzS*MtnFD zY##d!(ej2?Xt(A?W!dW(PIBNF zt#(6~jQcV5E5WCu3rA%+`L19|cP73^>l;{}e4NihV0ap`@>G(8n=115*r%qR!&TjV zbMcyrqkq!EuuDx7KC`5&yWL;S_^72cG_4PXgq148j0#I{$iqTLGKcZdkj~fb%US+M z@XK{o#S=qWR=?fvWiZ}vLr5K{$b-!j-kDml!-b9{xT5|WK)h`T`A&z%={Z~Zlbr`f z?lnYwo8@*(O-yH;T`Ip)m8CbOgXs3nP!1rV??Zc-C^^0R!p7(1mg0QKP7guL!zxAY zjYDwHg*<5Xj4%C@C#Z$|KG#-0l@;H0*hO~2!UjW@(#Mm>V>4LI_ujd5UCi}JzUJup zoef^k9=h6j7;3|j-6I&E7s7n1g|5k`xjw))VvpeJkNL9zqHde*n(!@JX-%2sBZx_n z+SzhzZE~lNiJX3|m)o@jl7f&u{6u_8W?*t|xKeW=4Q{{GUGYrGe)O#EBp(t4G0f(# zFkFfOYdR}v*ZAaL+8G>wog9lY=9kmjIT^gwt@PBZ-Yfb@#m&R>16*wgqq9R8=!s>h zF1yX7HkYn}G3H6OpL}_aJ|Q!w)e-swK}j1dfl9qfLHw^KdDh0GS^7Iv$jAJ7AD~Ma z#}6dDnxCI2EbA^zbmxO(NivDKbdcQqvnivg)ZFt75;zUP7n0=}JHaN&->Pv)3uq`^R+d#ZOMXCBkfw~G}V zsy9Hcc%SrbUTR^ba7O^GzuaP2zX-SF%_(pv)m;?-(ToqPio+|=6wYt~{FVI_cXy@? z1H0E~Cg(V&W3qw1&V8w<6HzsSZQ#l~mJiFq-55Q2C!kA%gWJ0Pe(YW~M@U)LuQGmM zu(Ye;T0=+t?UKDGhKpiDQft17=|sZT2dWKiON=GuolPHoP9=yF|%GyD?){U;t_n5pJmZp7wW^RS`Y!4ctNy@y`}HKKws?ZTcX1K;y- zytQ*kQ6$>Q$P3UDQiZOKD{W+58yRdqab8c*1{BLvzOvd5uNT&+9B0Lsi%vSQ&6>k{ z)fz=@6N6pL`x zSCgwCR<2V@&8Jhtyg&>=4~;b!(eg7T%PS#H%LknSJpvlRJMV2p|BMZ)=W@5ARlHdET`&F4}nJ* z0om(KHLe!8SLUxcC|yh$+*^f5uWx)UYck3Yf=SK@C><*z%Rq;F%*s5zr)>5!rvd3% zR`9ZWj2qnsJ(?5aC%&AI09_3eq*s*m>!*297NVNUIS0vkUC+5*V~Xb)FFs7sf5G+1 zV3|}sA9tl2^tFy>-yUF>sr%A|!}j2eI^EkmiL!E3E;p&behwHjvI{B~emAzhMBv^m z16Qu!RYGLEPq8&-DoRnP6^NfZFQyd{Q>sPSz-beugBgwija{Wbh=EHJKTd2WtQ&vi*N2Mr`Fdv0_7&HVr#Ki*y_;!&GSgZ6kdh(r{ug?Z!VS3#CDd@QA60Ro zwU~cnPW0Gzf)%=TAspAos5y}=z3k{gQ3&NLl!>_e(<=;Fm-*4IOd{LkD1^|YD}lf3 zp0WtbIu!8xi~*c#q<_84Z(@GJr7;y=sSxxeB%FqR#(!Pow=((h0AiQDh+%{Jng!P5p9|5? zw?5iG=3t1!G@I`XSj8)3+_`P=t8QC??;t{jG=8^C@G`PYP2lZCs7mcMjM#pMAFLRU zY4yX)vV{2dt_?iN-)LKjeoXAeXesd@aSI}{dOsgVQqr9L;sJ{_(Cbgy=pkRM2}0@u z`&&lwNZiMgAywI_h~0b+Q1}gt!L1n=6+-yaq%J={j7%WUwkGcNhc8cvXLV<h{Ur&*HPWyC zN81ScacaObZ)0B|R$HNkoKJH}TSV2JNQG}tpmyQO20N?G(iq^+nRx=uV%OTC3&2je z&zdAYdI*yYM=mL8k7&~c=ZRFbZjr}*cTU+DCPQ+zhn>Tr0)=W56d-+TZnNjz>I@5M z-U^~kdkT0v#l7r**rZg43t^3vD#d9df67zQJsYJ{dA#qj5iFgao{8s76#`u88o}gU ziGw`457aB|APoy!2?KkCU!|9%Q-pWcDCf#O=ZUr-U(Yczr1@S63YJJvh5gcf`@W4Z zWDs7^dyos2U+hr|HhZ0gJPk{WzZyI&HQWuDcrfYcVcFY;UEGd$po9{ZAVs$ujtb3* z2zx(Tzs)HnYkxXxqerI<&717aGf5mv&fws%!yGetnsV+Zsr^Nhn|XspocLXG-J#k0 zw8ss(+2RcO*)Q)&vh1p;{`|Ev^xgS+lZiujeq%{)-hN#$#w)9LVd*zpUnEei8=s6z zzc~Y%DJ4V8sj~*b_-7dVs+^#jk*6yjs{ND2(OtzD#E% z(aT?@Lp;5%{xoNJ{aB^N{ud8Qe_R#fK$w0MTn9d>d|*X17FXaW4U7o99N6;ieX6s0 z_!5VHDFF-5X7|dL1);)!W=xR%|hT68)%NLbsL?^`bHXv z=;EohO!)2+O;AE^n{GgT7VeD`K(=G}Q47|wXU>2^+jw%JYf<2R?v78<`wRlGUaJNs z6_FO{1zTs@{15V5Xg`8;sY`26TuOt>3)OZul@MDCv2qv9eDN(fP61qcUe!kOzv*p- zC`39e(v6v3#Vn=|(;3KLvrph(i@jhOEfH0fj2`_IOzbMTe;@VE_GdGzJGW!_9Hkb2P5TRbDJOh2Q)7hJ{*8=xG=Vq`|{3xm=b^nMrc==w) z$%nf|te{_zN#U92+^@qaG&)4rf^a`hM^vQ>#m))^#rlr-OOGgld-F#kfW_+hq!O=C zN55ZzQdeVbC8QAgwnjy{`;Yq_+Kh-{fwExk97O~&7n@aS6!}syyn6y&;2W0&*aclt z#M4z03my42fD{tSsTcG?ZskY%zENOlA0(B6{vi82iX&$3$Hob6U{p|pC-W@&4# zHHLr2fOE5oG;z^8==FleRZ34f@nwEJOEUv~_m0qTkWst}_o(txX!urWzuTX31 z1>^QGN8ukB@@FY8Ct<_5`YM~{0L^TMQdWo#_Z*PoOPn&&4UHS3o(W|{bh zDy$j^&GVaRdL2m0U2(>|$-OUEgz=e=rU^Xwn6_n!CFx`2kdjjoOHq4xLc~T6`i+zW zl5yma%Pwio|4`lFrM@leBIKjVNpVW-ixJ8F3Wfm-4wOiJb%F2A5gYzErf6m>I+>0f zNdhK{Qw%QK5j3;V0~^4K53n;RnAqE5S5!68s|L>d1rAkSix-U_k}$ z2TwQ~#kZxr_SPP@;@^X{8_9NBdK>dgSf<2~{PtO+0vh|{7Q;)Z<+ULSQrhH|@BOk; zmB@+-SN8F66}ou5<{O@T=`pcpCPOy)+gwY8p0l%e=8`3~^)pz9D zqX*JKTt-l;-1QI-wr}i5==CFOX)c8)GL8{)e8zCI!2JrhPa^{+g*_B zBjLPr=Kdsovg`1&hp-V*Frx~td}50+4wq|>EZ&IMl|(;KI`&7K_d^bd%}Bey-*t(h zSQBd zt$9o9`DlS3-kaAs#;nhDvYOQu>!v}jhe1k%Ot%Exu|7bw!xCbfO!I^)yPu{H9?PP? z7$7tCI37ttmy9T^Bod3gzDYGf%4VWY`ZNmKmb}>34gECRd`Fo7`(YAF1&z<3-Pl?A7VGlu{1$OUC34NbY%lM_vsn$d@*>-BB zYOrG}&eW#HTl1r6#N);WWWo{eai%??^@VE>{Z0&}smNV&dT&vJRRJ1(SnAR;DN)4q z+%`!G853tXVJ%}^r1H#siSQ~46?5{1!kOo{CXQJ&%{Te?jfyTDYBhT-oZBb&QC=YT zJ+&yIWA;QTGbZvtdKL0Lg(-W!#N+j_@1t#Y=F3iu#sWzkPjRnxl=Yn_qq6xMq@2$S zS>s}VBxI>Yu!Fvu4@&wmP?iXn*tr>e6A>0pslCwt%2PqkzPF=-WQ7*hnt4+2?$*(8 zHIu8>yc4oLa0h^ax!6^hHwh;YI=qnx@7S+HkvpL#iq0H;_4yuusF7hHy#|g32iZm} z!u|P#v3t3Xg5K33$Y7e+y{~oKLTB}~mC6R@^8J_FMr2`l2U25V_={mUt1Jqq3DPMy zed9vTO|RkX5=t-lz(k!N8TCf0@wqnVi~_mFwEYOIVwkif+HwZNTri{y9lt<2wMQ~J zUt%wa@R(q)7Y46bIQ05MaelShBEa|l9DW7AMgdNoIf8Q-OW4A>J-~6z19m+R>Jl?z_&W_7fo?n zkax~3kX3zF_W2e^pevCeZ3FpdW@a8V6cesdZz$YTd9%;9?{@S^O{0I02FVJNS;8MZ zC0CNR_d3B6(0b3L$d&O+$r;;ViK*zwKNu%Bm$QUDrY8ikuUl!%cbo@LZp!QJh>#yNaaClHxo*VX!z3XYeUrU zN5A1+CqMU~_Et}|*=gGrT%QBJtWTwhtxU{dpOM8I^t9b$RK+`|G+4UjXYd(ygqJ1R zZAki}^O@@RV%@EMygz+Vm9_5(1s97R|##QWFU1Va5s0C|aZd z8i>_Uu7Iw0DuG^G!n%nEWynON3$%OWu$SVkK?{_~n|Bnwj{VxfAY3^e2t6q>Su}^)SL<1!A4Ei&SnrK(E z=?@y0Gn}o1Q`Ty3ts_3ZBn|9k=_2KPfe%m~b-!>?$I8FvF9GqpfZ3r>XyGf>3jIf5 zh_AOHu^q_=8U{W-Tp9ukQ5TaE46sx!e6X1|CclC)z>VkYX(7DoFcdZ-EXrl|@_Ybq z_raO8BgM}~yg7X1aHsUP*G3(NnZ={x+{e!sWH-n)ohRPDX@O6(E!sg~N*9<}$Z9jhos zZ53Kn?VZ|Hq*l!mRlA5+-Ef@}Pio;An40IX>`sp+mO;?HZ`m8YKwv^vWF+Ii zG|)OP8^xoGdZRQiW<%GOS9N@}vs!pmKORcNUYxZA6q#35W=gIXZs;;FBdXh%8edM>&ECJ)bydVe%2cAAW&V@0WB$l1? z#kb05{DEa;06tVmjYQGecwSQLfx_vXwA(+dFg+19h1bK#UD-Qjhc+`=wa#W9mlgaC z)lzbP(mgZm8j$~_WBfz#eD2J?Y>NqNs@7RKA4?ardsgb6 z49+#qHZN!q{ZfVhLR3-il~?SocroI`THkmam2A>qcmLjNQc1#>QB86C<8C!}a?Rz} zMI`T*4Q6(tJ2welbH?nK+aMk&0ynFqYDOYG3O29L)mtA{5LJMa{&21L1Y}4XL*# zeKnwA25e$Wvz~UU2w^Pn)x*J(9`t2&H4}uOR6*ePXhfM=HMklNt?<=z?6A_u>P8SE zRk{#q5r_XnhWjcWLRwYtbnWjg_qKHQ+M;b^k@&r{Il!1EgE>ql_r#-_sVI%Y&%wYU zO9SzvB3iUG^sJQF>sj2ME0WY~`azIZ1nbvDQ|_JB9?Nk**y~sD=lIT2{QDyUY{7~FDzYmkSOU2>6pWl3g;RzZ3FGyu ze_6W0qLZc=04+2%+jYYTte#+~D@uC(J+}A4?+=4y7)0rTh~lkUI9Vor3bKKG;hYnt z>$b7zrHLCu_0Sp2ns8~Kv-vq1*SCP+%D2P8{h)f$Lpy`BTX?B`CVak(}!w5A34u3>8ndCG=5Q|Igt% z6FzFx@*Z2nMA1S^vP8tlxq-Rjijqu$&i2uIcXiaj13+6oRH%PUZK{OAoNI}uo8{JO zlsMlZZ~Mbu_4zs&xJ$78a=uF|&xHuWsVXRWs>@A3!HPOP;f2HuVr-zr;6&5D(9>ic zE7ehK$XGYmT04Vr`-+b=?N1${D353np4cA8dhSsjfMU5>$7h;}TsR2ksV^zX28xM0 zA5hS%Zn@z4`bEyNFb&i)uYR%v(hz%98I_^l633ywNDmQZ2+J&8ae_(0^O<_%uq+%i zoeA6N<_NcFX`A3wQj{kyhGU38gG0-LzeQ$R7I(6&9Xu93JFn~-8wimk za3xhXmTT-+r!SJjsqSWR7}s~vl>Sea`nK$b+_bwE|SXB0L%l#inwaM0z8kP@myTF_bY6;d~0= z%psgCr>`%;L>YZP=v)+3x^tUNpP{t^mSVr7AH$d9Pst1M`Cw+3KK*)2WFYF562d6+v3VY-ODviLb6m>hW^PU3{8@^n~rLtjSh5Sc* z@{G)bZR)%?iM<{o_@zOEtmXLv9~K|@bSs>v%prlMITwdCw)`o+h)?!jo^Zt2%A^HS zTgOR>)H-eY?|0o_hmWjd=D=lSE&XKKhbk$oN;U(b64)LQj<~4E{tH8^#KDd?KMI>y z$F=%{H<@;AzF{I}t$!Oi`q&t_W?(NV|4=V8<9PtW$)7#5x6BpU`-yYGO;hl4?~yxH zT-QkwX+SS-ZP+~db0nIo+!_2R>yVP?#qyJ_z$=rh+kF%8Jh`JBL+H;BKP3HVX{(oW zi}GDN5Ve=@Jb~gQ%58bnhnVN7Uyfxy1mLM+2jw8+BeFi%Y@=3u{Iph9bVvl72w4Ty zfCgLOB%>P~G7?G*Elr9P!mst%x*oVZe3}L~V)@L50`QQV)u~yRHIk!=^aNLERr9gN z^3wx9oStM%m^WNYYOS<~E&1)!at;Y#i;$JY*H%K4koo^~LT>0Cb@~WFBN>2IoNy*k zgtr#Zd6PLe4mP)P{2)BzsU5BbM~*;)pmeCo5AGBpRcu!esbw5S(_UGHorl-ddog?T zm3dz{LKpHZW}&5{C%G!;jLA6<_yU51>A=AU#Ly2QJrKJ z-0+WzVJ|bEs7~{h-x|?>P5)xl!vVaju5i|qr`+3x14jW?bpxeT{0j|g&n@N%`L9f# zuVn>O&<`?Idi!&%Pv)vL%Pf3ooxvKyQX?D6uOIky0RBtoEaHV{^_o~^W}}tv_s> zRA%FC2>tUlGSGZA=hvd$(d=+))0cfXXfj$y!TI$sq-et2kE__6?2<*Ygl0~|lON?k zKav06dJ#708`Ni3PE02hscNp*?Qq~Zc^;)bdF#9M#Rw-|Beod=Ium5uhYUt&3pqYb zkufWb;$A<$G})3ohaH@ziStIbYVpt6<+Nx=E1Hx%srG;HI39|$F?pUcFIKy66KheS zOrQi%Dh`PttQiui{&()QA7fnZ|J4^5XNud@<5y9u%Zd}3!oK>X5nYI_t9eJ?X?{e8 zd5!x04hi8H- zO-fGIs67=Y)RAts&SNtii{#0(^^dL&A6_ErwjrSr=8qJ=btk1|275j`s7Qo)2xF7V z$dB82qvL5??dhGrGD4Olu9BHX#jX&3?IvHqQ~?2YqT$VDVHl^KVh$NS1wE0fOU<1{Gb7+Ew!R5chgq9y z@(`~oX>*6P-4cb&)cwG4iJ~67A6>FlE zSOzt$Os*i0e+TF*B;g~QhLj{SNqn>Ra3cg;`&dnpoIW`*ZdWm$pY&7PlW%M_bqw*A zEAAByN~$B!otM2cx~=R`((*OBD1>k3H{(t)vx9;Cu3C=@2j`1|c`{f=75Db>50bw) zMa1D8{Eg<8nr)=O4d;ggQDkF&w*jS~*^=b?_(@VEEz&3a5Suy*O5n-S%cCo(vsu)^ zr*md&>L2KwvJEwjc56m~2X8)J{h(weQqpPna}XcH_;q5lhbdM(CGB?lO)oP^<|dsXesl#v&q$JP)v4KnJS&I#n{ByA`SYH#CQeuJP>f3 zv`6!JqgfIwPz&HKbMBwhdh$~mUr1BnpV#+AbTiiJ)`bleG3v({Ke1@IkD-j1LBaz` znNB^kq92b}3}1Dc?Abm`)FlNXIMjf&5T;E$9i5TdTI|A-PQ}C$-974#wha7)Yov|P z&P$b%xh@q^w~HkZgs?n!$DR{fO-9ty+3n7y!evfc$Hlf`KiL*L0X_NdVic|qx6b;F z46=t05Bpsx|FD6XENgsBu~pj&PB)?Qh6hYmBn(fMb~2_37md$wAvw$w{SAH%QSfh( zZBaSR43jr9^pY6{5S1xh@P#%~aT;H|ga4Gquw#eDT-Ziqbv=n`Bv#&_?3dn58#jVW zzuw3FK@m>LWV6wMjbrYdCEV2ivCdT!o|P@F=aq$W&S0;a0TI0G1bEtz*&CHvb|>TT z`umZU0A1>6Kx7CF2jUi8MY0x4$+T|_Ixejldx-csxH*5r&?jvEGA3sh_29$XU?#LO zEzs{zcZg2KTUXg~g`Ur3CGqf((PW2pHSBrL_2m*P35K3v8Yz{pjRwy~)5bhzQ+pHl z5OM5CMY&(Un4UPSY6oxZyUltH23SrEcpypryy$;-A3ZE$P!BxC zGX^w_fIF~+_?|968({o|Wg~pxSjvYs{lf@|SsvAfXQbwK<5%%|m9$-yi^$C8w1?|Nc=c5N$Q#s`|PNu~4? zEMsfnE)Hyu_7QIGPk)Fs*WvFLTy0o3^#^Fs{d2db)90-@F?bEGOqZ)fogAguE_h&UlU}R`_f)(q%GqUC3)iK({ZD=5%m>@EG z>F;xb`%9Wfj5;N_exxNNzd8w%K^i|GPa#covSif9TNl}WHpYo0_V?UKDp(_Bg@Pt1 z2E?Ff7s5z`OE<&-vK^I>D_mKnbuChZ+R9WYy#z^FY9`ZsuxyYVI8Xp3&_b!DwLMS00^3Nhq(`}g& z>;}Ip>tuIp+1zlrrhcNU^0jnv5>pd-^5a_k%&vwLc6%1Cdbtt)Dm^ zXz9KaJkvMvl$|=;@6tHj8|!&KsNIoQX#W5mKNfOJ8=Syjfd~Dkn%QvT-#{_PV~b~Bg&U)0LDnpA7qrL*tR zm{aGT?tQN1+czHu%L_HLfOuZ#5c|C&3dft-7ANP<@SL*Wyf)oer`hG1_UGk1L-AO* zr%vHo9i{%(j+uwBy~x?WB>v3*G9SCAOq<(M(=>_u8tUmUmi#3@v!M8y8-N<+SeuCoY{(zomK^W%7qFy8_m!Oo@cP={e^Q;!M`-#hf1X_) zi+&6pzn9qo=}NM~H#B=SQ9JNp*-f}K6EKX+m}Nvb_#NJ>g)nu|dd}*{J?W(CVeG^d zpi8(w(sG7MGeQtdYC;y;w`EA$<9AYEA`TBxJ@}K=(@ZH!$Mq3^;;gxx%UA2LYnqV7 z;1FZU-)A>ZrW7qfquSDj33rRx~Lfy3f>lLrHdvlVCxbPOd@ERuBm;Z!xwceL?m& zy~+-V{o?K$^@RU&Ooi7P@zElttW*PPNC!9v3Ss@dTx!of)1h?f$!cP0;3s{)yQ4f+ zYa+^&^`e64>?B@#H2G&kazE^e(96eJ(UQVLZ}VhR_VT(xOw*gsH5>qA3v8v@O15X2*O-jM!bH8ltFPu*lD~zw$1*83HdR3MN zq7`9KWt&?x$LApOZ8_>)smqr{jxi%J#M7_@h%7l_WbB>&0^o%)Ml^=J)}c4e-}=iC z!!U>Fp7!uU?ri_mF7k_J4??S4MNdHod@YFrXaDK4_A|tI8Htb>U^k+%BJM z6Z&@fQs?P4Wc0KjQQu&my?D4`6{I9^)rt zIQth7LGsIPui2g>?f-1n^CsXX;vhDn4)vp>xZ>}zWdJ4>$g9kKG%sGPO=e2dM|8Jz z>V@os8P5M;+Gco60SEK>m?VewVvglL6HQTUti#&6j^stPFDzRQKokRf{kBN*Qi{HK zJ0MEC88$p2hm;)!WS(ohaA~fH55U@~DhY3SQdP@| zUV-YH&b_2Dw?2NK z_v76gb7jL>+{otKL&E*Gb%%dfoBg(vDizsjqg4Hja(vCRwkvfiaG@ zorVwgRD;jNr~|5WH3`BMrq6Vnin|Ep2<5jU_K()o!jjfWCjIbzUz+)robYjzVN!!T zum2`T$?xeQy|MiqoS47{XrLgA8 zqvWonNMpxntcK09B*ee8_n#*BBsSE*cLCKgGW@=XZUd~TukN;AKAKy6I^gt5iH(2e z@wY3jkLra)MWmmyP)h#;NG}QaaV1^u4USy;>D&)LsZwm5Wa8zw6x%?-7cyZ;0xrn*zJaVFjJie&>hZ_*I90@p@3B9w4m{A^+_M6STCY zrW(emZal`x26-x`4|c=;XM-mp)2XA8E!Y_`x|h031N%fozh@yATD6v0DDgzfTh9_> zhb54Dscd>vMf$63*5e!J7Z)ZkIfEF&@uq>hNQCvp4lGv@0 z=*P$VY)d37gnv`!9^U)uIRrD>nH0?CZ4%L6TL=4qu|dE0BSPUUrJ@gMB>6s~Ub?l- zS(Ih@`D%GO$O6!9+pG#PeYD0hukDx~x)MevC{k5r2_C(~iY zV+L!|;Z%J(>MW+?9Erf~|IKP9n)R-i&8<8=`;F?5)+lH0g}#Z_+*x|iy^riYjPj3ebxz?E_{ir7dH-^=%b!VpSoe;fwiCZ@nb%F-LYf?Aq`l5K(Xf?8 zuu4eD)%i+=xW%n-TRty7qNJ^W`+m2UTmdrN77a(RjezoMfoem#CCSN=K9L)pauO2n zMCeWPg~uk=U4Kk|81f9BcafaD{pyy=;k^5leKz{U==vWt@vR*uhAkq7q6ZB4ue4S0DBtQw)q&&;S<%eWLi9 zbiTdf{w@8760rIeYhNTaP?hcDOR14?e zRVwDDiuHLW%^gpH$$f7jk5TlUDk|3*yvU7+wXE}_7asl!0ue_7 z7tipEre%X{m?6)j8~ir#Dm86ui|#F5`U|^++18T~;r6Gvec&sF76Y=+s*#UUl=)0P zw{17FWR>lItj;;z0kGrCIGy$Qaep>`-O+#(32NrV9!NzJ1(U$hOyUt)XlFGbIZ7CQ zq+UcC&O3bhhv7pY;!+5A(*BHbO~j?K#_<6WC!xXm4#W-ydfTtu3q&1!!0~TRT`McN`HJ94mnp zYjrbBH5%D{>(Q-$l#9@o!Yp8`AJKc^#RIBon8HEEak@y~|E~p*_XZsdeh3|}Vw49? zR{MN^^TO&4LZ5555eE&JQc54l;`)@)R7KIw#>8>IoLINuMaXH_IFrmSZuOdVMOXf$ zE2c1cE2#n7tnT+K6{nokm?NZUbSp=$LuC-)f-bTlfN$!2aQk-y#T-sJbJXLn{plHOf~^ z{?CyXW)-wV6EGuJiAd{xCl`Q#Q_=PUWjkU+P8TYJ?9!5)l*RRe8*AD!OmjRFyf=8P z;l7ZNBcS+F5S-3 zUb=Rwh6H7742!?Tll?9FA)K1j_L4P>GNtnRLt#U55x2fl#+`ZjSsT`j$^dedcbz9) z$+AmV;EV}n+SC*B_iPf=rairc zj5S=8b~#EZRm$?`DETyPs?tqifm(;a_Q7nBCGixzZ>?5A)MtQzu zBLO&ln!@f8`Wl7TTek(Hk{HJcXW8B0b81BbJVLs>$wS!LHaSLd^3QM>JTKGQ z@X6>#8yDWbJ?6r=2d0D)$T(?`E-;B`%4l3kW0g|bJxqf;A2;j=vI)jmnt;Kx5q{jX zPUm&iHtA#h5oxE#@WSGe;__B&Am$=89*w({aXs$(a7^K> z=dN|QkKmtUCEf#P{g(qU5-|5VT?vspP3=X>oAmb;z%%)a2Y;WDzKAvb_1l&#hu4~t zyN810%Vlkr3{Ka>b&3*)wUp`WiB#8n--I310Y;}Xm~Hb{H!hG+_2F&M8V@~RFy%p3 z|KzTd;|`O(AJV4X8GMRkTz2F5apYB>nlCt15f%1E@S&1Q{|dG}HT_x#TXGb`w}YKU z8S;vZGQO6f>OQm{GW0i1hzsF4IJMT>cw9fpX4d10V>j>OV`VHVj%Y}_b*yIt8u6>< zp8r|759hU*P5iI4kXWh0)^+azelcMrbgbh|%94`_@9H}pg2-1%9TRG z+S`fFo~cszS?`X}Mv=7+!5fl{b>x!_)>f)RCtoqsf=^$0kgL7pvpYP~xVA0XU$2>;KPDZ7a$muZga!H@^Ku>c=XH=&Byy|oo5Y@NUc%G)= zImZ8sjdgu%cWnp`zuLfMF6tv_T)Y43pqWo#Qrwz1!HzJ-LWi(543tWeupK8qW7)eH z^qi#RSnegBKw0p}aen+T7m{nN!lJma%e&;?MW#*@m3xJkk;)@>{U1$UOaX}VQND9= zrRuWN3b_UPs@O}WZNV6hhmbs;=R}}_jlU5E*dMZ)3eq;ei@o+R~dBpl_&}_%d9=9vIrtIl^(^lXb}>K_r{gj-;&S18_s>2b9h@k zB7Q=FD5)Sv_~z13CdpkWDrFR^Bd`L{WF-XN`KeN?-7J0ugbnH)b!U1?YhTrT`625&awt0%sFAlRKjXg1;5Q; zH+|KT-3^PNydakFmBvW$D`XCgZ#LCB^J<=QRCz$w9#{-!`$84TKKfv%EP19W z!0UAgG~*S!9Wc7PhC^<=?QTac_VIo&dla#cgx)(}c`sShPs;SFU+J+!8AvJ==UX}_ zTA@drXj4twUsAX&&c2w;_CY>NDH-8Hx_qk8Ht03X7}gFxhdp6XR)g39Ttj8;d@dZy$Re?9QJ)2|1n@8f#4Zqf8qrL8acXYQ=j z8%q5@Qmjd{kM+C0b2aof#5quy#ckHMmw@)r*Y;WWQ4n_o7%#DGr`Y?-XHqN*4=qiq zVs@1f$(`a%>=@(6cX#`Nc@g`V#f9m_F1*dB;S7OO{d;ZLL&k?;^U{+D&t^n*0+W@$ z)yP}xeDQAJ<7kNUWCGs)I47CqR;~$@xcB-r3F)GOy0>%-(oHNgeRSpHy1g5 z;SJ2L8f?*rBR!5Mc=S_&6mrrPLSP&22ocjgPhT;2|}Hzf048^BdWHBZo%i!lkXV*lj*jGe2m3YqIN3cJRr&p` z3&)h9)PLvfIRx$UbA>cNq2+}`lYZi#pIFd+GX05HJa|()*3uQwQ^IWW`2B@)`eJIV z)7pugm$(G{60Pd+{Ys_eb^b54=J_L3zB!eT53ZjC)a=_CPdAn7#=np^=BBI(OL@X4 z`3|C=>E$_pr=w)cW>Gc8Cl-VlhA8nafDX?;+3D~{)@;9E z0I2R>7gsV7t)$qTk8}Omk%Si5;rjq1g&9@>DyAPo>NT4gLljNTGrph9lZUegQ>uD9 zhOJ$GItano!9Ky~urbQ?w#=DQ{k?BROVA&%M5grlDayVq8@=Av`RT(aYl-I#!K{cr zdiSN@!dZ&cr2Qq^7XHeM-0zZb67+l+LapBqB1I1dsB&7cw3G26BwJZXF_(Wo9)ZBU zSOk2G%qv!=np?;@ArNOL>1}VN-tU7K8=d~7HiTP`((TU(_LB9yn8${AK9#@s3Lro5 zUKl%_M@nOH*)30fr{|IkqcHTR@B1A&Yy4eU^P6|k61XW=aoWjkl?SX&LA0Iithg5sS zuff9;VR*&&>~-y5~VS zCg39Q2RiSSQqarx$d^bHOcSCu#%?qV^4CJBKZ*l#mVPg3O-Y;WWTD(l^_r~-Q%)TqnFB2gxO_G?=(&=w3K7F!`6+QWacqWFO-ksN6{Vcn6Ivb`Vs!Mpj>snVJa#Buks2L0BEJc)f8D|=oRTUI2&U`vYo}knVEzxx zZ7Nlgc1_WTi+-3p)9WavcX4jGwhKNm&{G~v(`nu$Kx_zU)|l-F7dzAGExuAl$e5*w z{XA*^KTLc&TX=22bGzlvdRnTpPN&1|tR`<<9=pI`6=y%c7c=|P+>%_qimtxAt7%Gk zuOMwP>AbiXeC3a9kKTA`vO6+|ZdpA5Ih9st_r9WBx@M;w|LVPV)A7%EQ4F(48;swB zZ{>|vgehgr>Nr+LMX%X6(^h?Fq%CmOI`hjwGS=C|Ohb8ti)|!*cn)zn4&V)q1^@FM z@&z%T(cZX--z9TDQ_JHn@Tvj(Z%=pxGN)jhW{vwTNvEcn?rC57GJh9Tp=I1cwyzZb zK>ay3niy*^QN{m(LkSNg8&2gCSAsd5-ad_eRqVhs{r;bPb=&835I9$iv`X=Mw$k8( z?_A$Jes5f#-ln}7OR=cJ49?%z7P9E(vhTFN&{9*!kJse(YdAA3aB-K0 zP7xor+u_SFIOFk?6ypNJ=r9yrem4v7G6B*kRYA1q{4hUm@VUUdb@==%W5`%q{Em~{ zhqWND^w+N8dpn^f1zFw}Vcb_etHZ+oiIHIhj+FivmQnhWE^GtthO7v>?sl{mU2QaQ zKjoULnDd9fn~q8Ju9E3m;%1ERCA?7kxer%iA7M>meWK!2-J|p1RxYRfsPN}4rLUM- zE!rOMvuq?OTCnER^oxOKX=gEcsIHDa?SM)Mg<1``*RAmY_Gri?$9ij7PdHWSU9abk zgbwFcRC_MhALvP0@yI)1%S$2Hy03)rrTcTx%+mQ0wMk4Er5>9ne|h4sDH$HSU$N8fNd9)B$a#9O_qrhdi>TFiDT~bFX|9ot%XA?z z0yOy?os}2PZSuAE8&Fm4#&=bx0X_plIozOzCzAs`HkZ|`@1D-O`zqYBN0wk)wL)c5 z#4dzQ#!9}w%EKnVHv3q?fgRJ0tHx1|!!d&Qds^VE$U)*Wa~gB6ieTPfN|^Fiuq&7= z?uYc7Tb@14h8vm^ClczP^=7r9HyJ9FOxU?29-$a*n~42ZnMgLWHtDL8jb z3n^ptrM3Q(WxcNwRTvMAo-^K(ys|6x;%?6$4W%D51ybyLjx!S_>2g--4#Q+tJv$-P1a=@a%5i(MEDdyK0*0xOCt|`#K zZS!ieHQz?YW0H{oyr41*WgEf@g{VB0FUoMg(J+^LlM<0CZLKN6>W!Add>)NWcQHTF zF?yrVcdKZina;o+5{R=peDhqMOqC+xA$+L)fbKd&2&IIAWH^{B7{U>#{kvOU-)JWA zq6_%IB;0hl>xBGbN~|$&jX71cav(s1RY2U8J{qbO2X+j+F~rtxBKLin!t~y7-FZB*A6g3a zn?z1^eRoMZ11h(n6K7lXF~8)vuW$f-oo`55kLq7gOkYLs4+NLfs|uxYFpw->q17=H z_wL!LBJkUZw=L6Bo+r~kNxDphYYn9O%DJz8^vP7@jep9;$>Blanmjy}as>929ejOR zvT^QZ$$}>JRp@;W?{;I5{PF7&`>aoAa9k*4@P^RML6bH?1gT<1hzgtiEksU*S51AO zz6uOcw{n|TgxpNCMcggJyuqak?U4NKHfsHt$3G;!U@GXu(Pu2NzhvWgKrHJAkgmoD zyGD;`9bzO+2ED8>Q77`FF;QeF=1vuG$S!FAdE?vKjV#+b;Lhg5R6S4v=Rn9W>XL7z zLh~mkKQpY+RWPhQCsyy56Mjp39K4xyK!ieeLGuWwpnwL>ipYB<;;U#ih(oW`aMiE36^xCE6?nL77vL z{8N?%b;)iHF9wyDGTpXeNh30DsZnowk4+67Xx>Ilzn+boxfQN;6#^g4xm6RO*{UeI zgqAc{Vq`F9hs_OIU{i(VBSKq$Y}V!nE?0Z!o>*F9f6)8eQ5pGkMKZC|q_vhNHgwG_ z-s#EgQ%~U$gfQ5;4cLf)UF{IuCc1^nsJS-wz}j}YwCJ4N)meJzNE$vb#kdtlCesH| zaBJ%?(ZM8q_f~j6wJSU~BYKXow*m5;HXAL`=_ZLFnJbk|#up1gT#e3vqdYt71Vt;I zqQDuhs3u@%$#$+27#ziby9Nf5C(^EdI}11svr3&)8~N&=1MH^woIJ2ykR=0uqVs-@ zFBZ5{1xy&E2<*3FIwYh|lYBZAKg+t7bgW1PN)23?eaDUzuhfy39uC2RHsvP^12S^Th=-c*cf{{B1gZ0O6x=EtU>)6#>eovEUc>g5u20}l6XpXN zdfC-Le3h7`@&B4aBkI1;lGp@70~Zc5i~%Ge)%vW4hgLLg_U~`#j@V~W}TLO0qyYODMoQJC>y_N z)bW{gs%T=8qfMTDcQNbp$Q|pLxn9KWNv1X`O9*+~{r65t18gH62*(w3%?7=vj*J;( z9LQ7$3f(12JcRBj#gBok4&EC^KOt#g`7daJtXym#Pqn}25c2?O43FjEb;7A#zZUuY zc>QSo65DX-`b|O3Jq0*Fu3dmPh$9Vm? zn)|cp&(%QbaW$Lw$?$n%j`tIdxMvrFEijhbPl;|;{a$Wg_zt$J;p3IOPWO>Rgb?1X z;V$15)9iz(6=UEd^2{nQEP71X6)H zGCzVwrYi)~c1Mws%74~maTLqW-RmFK4JYmrSZg`;)-To;mcPNP;CNZuz!evI$p-C0 zk34~c@@ewvtD-GGFCA6F=qd7o9*y3K~t4M`j}rXv&D#MMZwg|(FMV`^@>ac ze$cTp#$ceU&IHd&aW~-Lx(}D!*vlo zDI~*Ce^fM1i5dN&va~LlYEnC+oIE~yAH=4daQVm%#p_x^maE@?m*D0W>ft-_a>edP z4|YFRb_fq~>p8=D+6d$bT#etA2TN_?nQvG>U3zV?RrlZ;T1zka1H_$D%n`dh&_P?} zr<4XlWS5elrD$hJK9YQ9S{*m&2`*p|MzKn zGSj4xW*FR(ghPoX`Kn^soB6$Vmgk?bR@nGL5l?%FtYz0JWtwE+%16n~ju<)>p zi~m`>z=^`EFZVzA-{lX_H~LxiN+!JMo2=dxh=g?sf ztorWzx+Kt+4%=ol6T2IR`7x9gnYdw11J0q?{Vwt8x`g^j0Y*v_rT!jCP>`G%en6AW zVOQxbkRY6rq#l6GrO#wc5 z^@hN@v^@ciLl2S_$P0o{i+Fr+C(M0XoVut32_e7monb)Yti*kg*6wLbasid&*N>%P zb2KNjdJrdttFT;MtPypfxNYs1E0bcr8Ft0fj$m7cK(VC?QFoYUB+8zV}1zn_jEM_ zy)fl8(1!w_MUkB)0mqCoI)MKG-$ar5I4}cUYyvz;>EGZ6N*e^bAOWUQ-!&gSv!?@F zVI>W&WPs->evO~Sy*;my4x6Ao<4VG^{s=l`h1xL{1f;6&6B{| z5sqtAAq~JcPhGBu2b=}m0XziUiv%zCz&%4Rr?D{$_r1*75zoV@GPXpm7hJtL_hBL|Nzed%3Duf} z{Vir8A&GJ?ni=3oWB|o!nCFi!)W+{<+yo4b=p=2RuYvll&O{EGr0$_kR6=oNO!GDa zy)fl4(6>MtM#}?^Q)sL}S}9YIkY=eA>;9t$c(rf-QPx>A&{weu@Cc=^{a+~@?cS8& zH7e}=FcY{9)rDHAUTHo{@oTyirSKZnK@$>(eL=^~FCgEg%ERJkPTlyfkP zUSwTpQ@sEgSToSZqLc9VP%jc)jJg>&1HCXw8|WLOjG-IWn_@@ z=?l2s94nC$VU!w-Mzmr28>>49vS{=5c_jQ(PZ86xz;6k^P<8@;Ks7PtBGY%gODQhi z)q@tF$-uS1@yPiw24!9h>ieFZQO3aasCIo0#W%^zkz*jPnoy%SNf_uEq_%v2z<1~W z)>D!2(3vR3coZMrJU1W>js1b2pqk-Xl)i_qK=-2Wi_Q%6!X#;+Z-TOE3jMYn!0Sk% z*tINX6e{=nFU5_!8Te=On1TLb)}yquWhL-OR?pa53q`;4|GpjgAxhu7hjZ@*2d_DF1Cs8M#jO3=tWuULSc<4H7sn(W)^sg;;m|u zU@f$$6iSKeK}OY0D0Rxk%Q^}(B-W5d%|3h&V^8EMM%fOF-Paub|8S3JC zieI~z(7J6@7n6{4GOR7I6xBozN@KDyvo7!%;8y&)SuMgZvuYjS4aZ~kpjM#uD%MvQ z?20xtE;mLS@=Ql*qcITmU<_OHvJ~BnUqwOp>~j`8DeqYx zT{NLqgi{E=4}A+c#H+OT(>5MkBgcE~K!L zz7Qz^TfJa%#A%?9qKlO;Wy?eqV<#$r-M(~Cxw@81LXqgp-7=$*oL8<>))FNcB1deNnxD5106oD$VA$|${j!8Vz zgHbQ*Lg}sVq0$W(%s?+p+y?q5QQC6QZ}$|%4YD{GhDs(1ZOYDb_%1WhgK0r2=%wEO z*CGeruM7Omt%Td7WKibJhRA_8qr|cM&!HVf)qjed>-A=+uSMy!+}~h2G1bagBt#b0 zXzfDnE{CNtSr2wV0m=o$-#O+D+yiGBU{Bmz%(eqxqrP;yYMw#3MQd|oGSStuM-sjU ztkmd49;Drov09BU$y^WmLXn}M@gn7~JsC>jn&B15@Jf;<XKp#!vn%6hb#-Qi_>vGiUw-U*9jM@zNJ*C%}sN)QeIgS7>$6XWb3Dj!Rr~w*~Ad5@y*j^MRI5^_5 zn}Dkwj~iwzK^wT;sV>p52GnN3#aS{FZDPt>dJoPm@Zax@VGbkFz56g)o5E%qE<@s`2T}XoXIfDj zbg}C(A9sBQqp~=T@Tu;-$YDAmm6;Z15b9QSAxdSB+x`d|^H3R$D<4D7UYBF_pv>l4 zUT^A#Nhl@I)k||T(T&1gaCZ#2j5)xsP&#Ba80zEE3q7niZ!t1ZeZ|*&nFKt<>i0|+ z63!V_#<{GAu33Eiiu=@>i$^=F1 zgT@k+jdLn83~F&LXrOQwL;rC&)tT97h$hso(j^W2HsTZNjKTm^mU$jBdR=XvC?ks; zwypuAA*fYsA&s6w<4vU3WK=P&D2i1`xf`lxbAzw>Ja5 zIOB2GWa&la2T3Tb4@Z%$@1y?cVU3mLXnnf~Wy;lBgli|%U%uEi_&KIBUG1z%xU81k z=mx$=dcS)!YCZfD;065kuA+{&&_2VJZGq=;U;i%iIF?aC?1*|Cy8iIz5j}PIV6>?! z*Tb;_8S2-usvl}4u?OlE9`<}*M=AW}_C}9Gj+Oj-CfENm4K@816m<=XqTfp8I%TSr zs0R9Qf{m;m{Q5}8!64LLEq}A!jk1NpL;%b{FHBqp`dI|OuWqFL9V~J*0wqHyw&rw^^Kc?*jXkd5#$rz`76#&L)|y8+J|lHSYV-CbYK>+lIRT`(p>kV;L2~K;RtQ zmD^uLEj6OB2Jjc+S4#{tx=>W?Dk>-axf!(u3hO9Y0h~xW9?3lkuG@K<=vfs}m_TqH zVmqZb$c7tH4fNv!KHrNrxoJjMc19ld{CT~M9B8F51HCYD8R*Xhd`B<(?b2i%ijL;b zYbJ7p7sCwn(u_eROJU!UPm;Wne~v~}Lbo@1{=*moS+oc8sY|U?iYe*m#uCh7UJEp# zwW*D8&tngA-jr+4JrFr_rxCss#bxvWe*!MWEh7IK@ClFeS(DOT5`xBE)Rq&lZrB=k z<@QdbMi^zYnL#yWVOyBw9)={+Qf4D3#dy*a6Ke#1SY(btFZNqV=b>vwDVzCoT0v!` z(_BIDHTzncld3_Y80Z<`9|525A>MjXABLj)IDf5K3VfZ?&zu?Pg^9yJA4~9iU~=4( z_2d$Qvv|HpamSMx=;1gXi7l0SN3KL|($dK0%Ah*)KcF(OLgJIXNZ%q&r*_;lp#4Fq zpwF&pP0)yyHBsqk*g)GdR2GudbD49L9giaRaoY>s=v{s%tDf7f33kjD8a-%_G1TMy zzKFXWv>1s<8&yOLdQe<`=q!4nL`@H08+o3>dcw0PYH}xf=;Aiidr(%uDZnO_H~lID z?1P(!??u%9B@ITRl=1vIeSo3|X|o-{0(%dpIjI^Xih(``m>%%?*C?;JU?Gksxb)^K zN~gJ-fnJz64D>Gqe7+a>PTZ6AWM6_6j(v;6ettJkmbnxYSsm;!an;n723BNCY6RneRX3AwyWd0b`X`z@? z%-2SEAHCZT;=>MxR^YpY&m`zYwdfzmT^p$hW!k#Ty&L!&l_An+gk%$f6_ksOg(!L+ z#ei)>o3Wt!(?VkoZ~{^@+#mQYFopOP7+0d3{~IWUzZ?W}p`)4g>x2fY0|( zIZ?>bXpPRF<0BMbAI(4y&u3W`p$*ytbRj*0N_ZcE43SBwmF*qq{rMi+G=D7M@0*a2 z)Fpj?e%kf*IK2CTc>Hc=ZQCO;b)o>+lKX4E7Ohr`;AB|p2F2}u-57%i8 z1r7kNA(*a#Ug{nNzfpjy&1P#1$rSSJgy7Qt&^|FHZD?Hy^BqC!<&&hR-X6qttUkw1RN7StGtk`_kDSFp*=G;q zSKnDFe2U}|szWa&oM==RV^L>0Bhs0&flV?O6_%rIjG zYR~u?WSEDII*&t{q%Oz0)#IEqNU-)j$79Vwd*hmFIh>YoSW;wwAK?B1dmZ>QdPzj# z9NeNEuOP!T|F~K7fIonY=&Mm*<&Q?3UyfW2*hkPt;L9k@B`zkRwEeIit+$ZZ?7bN0 z;6tJL{;#aFD9#%N{`&@HorO_|wOD;!osA5&N?`^vs`}TO_3u1UjG#3AA*hC}-U%|K z2cwka>q2}!C?zo0+N@HT9`N5Sn4dyr zqH*yS5^TO3&B^7NnuXqBR}tQ*{|qv=A9pxTFRSdLGaNoQ8fB_IP7UovYRy332lq%- zH}E6mS&G6Jar2sXqB^YQMgAs>B3+N6+KkVj)buTpBOq$cMFu%Dev9g=-i!Espm`mR zL~O#$ZyEBm-B0=((L&;MbILl4;>l`s-qU3K@qxq5G;G-RqTNP*p3-QJGsa%E0JA znJ;&wIjIhIeyY(ccrv0TLbeRMBgga1CH7de_wLk_esGy%Urt&3fby9BrgmHKWH z@-vyfzyCd}^a6|uVkoLZaP2+*Pok$qj6p9Rm*d{$dEC*ct=1z1E9&}=pLxtBh`k7| zoeLUSB&f1(OvjE&p&qAera7rRMxk`%IMbeNBs=aywU?E`p|}ZI+DLEwhRSlJf1TiY zW|4s9Pf@AFBvxgUS&Dl!8mS!tqzvjtay`mS3#&-og$(sCAhaXz3Z+I?t&DuqG@ zdL^}j8O5ronvLoh4^UYK`XQKsUM=G;h3YZ5Z7^8d~Z^!jB0 zN`L(^3O0n*4eCLu;}@We$uu%|8jvASOx1c8ux$!+Gm@nd_$11r2y?%7qm=RuJ&tuU z?s{6?C=LI7)WbY(=WZjq_ruO73p|Qn5N=OY(&=*k9avxVienUtG?WtV$RF1c-OITZ zN_}xT?&ZYe(aN9{kWyOV|`QHABJ}#&Sg*Fbiymb{~d{FR8pz))&r@u?$*Q(0}u zM&h>i_n?>FVpMB9Ht*lNQK?ELJ49p<(+dKV?BdGi9&!Z{-t zsh4QIRfiJXAGgq$g`5bL#&HDC_n0&%mB)U#|F$;L9Scx8XQl8n+*>zJMUK#a1pM1l zv<9@)@fd{KG3H;JHe_(LpxXbbj@P}1(tT>nMJw*@Z;FlOC?a1e=a)9Ez^9QI<$5X4 zB`q>2(6TPtqq*p|IP{&fbrS6@WGrd;Hpn%vc!rhx$Bw#ke@i>DVj*%Px zBzet>eF(2L)r*`O-@v`4a|Zai!*O!sC6cqV%0{(E&~qu?pT+HloPuuRu)U3g7z7-M zQryGx3Q{L5rraKd9+Y;qx#Q=aMiFfvnt)SSJ#tit705uC>Uq2*c@sU{`S;S+RE{^! zN4uZ{MJN8sstu13Oa=*E(vp;ST@ewGB*odCdG-*X88?F)ki6~8ZUV(q_ z>KpY+mcr`NgHg2MT-l8|RcKeSuR8!)6yxmdCOPFJ=M2piz&*LS{dca&(pZB8# z_c$&$8B~+%6Rm0ou13zAT2TZ}iU(Nz`Yl82hHE`kZd^uXcyu=dp1|)M3XP>yRtkR_ zG@;E({qbjeDq!XZCDX*T2Z3vZ@v&iggZ}@0$UCTWVyH(Q_p-$hJo5k(F@n zNzw{@m(|5yAH`2xsumfCItbhgOsBNg&3z;7N5QJ*|-UDXOV#R+Foa7#KM*Y8&t){RHCc=4Mue#E-l@j zL$wh}vH^OKUDEykjQ{3(EoAhCHCo=pzvVzan`SYGm_zZz08IK$)t{eQ^Scz(d z_oZ@wgGwOCK<^>kbCt_t$^Y*!qWddtj=(K7EzB?jy|96v+bo2=mO-Tl%Yco`cPfFb ziPpyab6EgOgh#0la9>5&JuWdKrR{GG0%A}S!9OHcyK~B=S zISlup#q;0cWK+uN>c^@MhNKIe4q@yOPZ2 zsAhsT%VEcpz%v2=y$gAq(_|>>t(xyZ=)}Dyz5g%+y|{rMcFY29EbwnXE8nREau~ty zy!()2IEY5HvDydscXe)L(dPF_;98U>8fRbMv}r;c>HKy|S+owhM%(Iv3@RHsfZz@~ zS(K5Gbp7|*6c5t$AP1AZXKR}bN<$ADuv>-Q7><1{Zc&jpNWKs97AXDMWnNd}?`2*a^)C&3 zyk|*%v4~0rz4Y^)99a~3_UYvq7sHU+ZT@v#iNrUGwR{O03xKOIfu%H! zlx|hpgd!Qf*QWnkHy3yry}S0N`lBOj61?$lLz{kAQKhg9M&&M_L;|r#BYlmJqx&ps z``L1^FTp9cZ=e#yq@6e!O83Kl2zN~&qxzVL+rHtyxCucvBC%qZNd5f2&m_4kx^GIhF-M6{x_AWsGlaj2KsMN8v77}>uDdSK&a!@LAspq^0t*!N3?-kWRzY9hDM)v*h{D1F6-Ad9hNDn1A zQ|e6=9WO-_YBLmcPA}lL>Fj?`_MoW#d8A(g;7cKg%v9oa>Ng+<-H*^aqXWH{djdu` zayCAS1h6K?IFGn!MdDFG5w*)HUtjba0*>E*ypNRmjoPLGt&?H94lGAyvM%fSSQHg^ zStoPv_9PjPyEe~1JdTso>TtEcev6yHoDq@jk!Qr^M_P_VY@#p#DHpmNcN)n@mo@_) zcbL;k4^4hfIP1GhSEwToYRCN;l)G?iy2uj zB4MyokhVpG5`?Dp%(WV6`;txt!dpF>3dXd5KK80(E zdr(HrN2v@!#d6Yw-YvNvw}qa+LeJnVdGfYkCgMUIY2?^2UNt(St%=ZYAhBr$Sg#?^Up4U&WVGg=OFQ!5l~av-IP$dS z*C_U)Ru`M~{avn>abCdRW&1u~OjUUfGWN%m;hZXwu}ERmrSEQTw4oPN|C+}Q3646h zAY58-oD89`A@Bqr{>{%OI4aeN>Y%3-_@3N&6@FBn761Ss07*naRHZWqwd1Nq)<9{5 zQz?8+mZMEoP?RCp-J}2Y+5oMw`9^9MiC=9+c_QgU&~F}eKfFtLh3ZlUdX5Y*2|b6m zB8TG=+%|r_$l>t^R+S8lqB2wAClM_A*^U(KeHn-h*dGG#1bn~m=tAqoNu=ASy%ibA z`Fp@zCQsC|tqi*NH$=wxXHmJqZ76HRHClKXRsvKn?2ZJX!>n1|NMvm-hwmSW?s1n| z>_3pc#V} zq97;I2PpfanD@1_@AKPIKite;;pc<1mE{rg4-cA0~ZkdB7F}j5trkI(Sk~B zg6@fKK5QU?#Fy?1_}gA&oIh9K-};Z)$goROv!NVpfShOZDSR!ukWu{oZJ{vd^w``vSaSJ4@L|Ghh(+q5f0uImNp7k^rIZ<8N zjndi=L2E<)d)A1VR+N71Dh_u&(W0IWXpj7>> z?eK=X$FUWqWZyt_CH$b~HT>VZFpH2BP+IwYhR^gP(Zr)kH}$a<5gc1*hd*Oh7~T`R zBga+ky$hvAAC$^BYb=6#-m9s8CUhZjK$AhW6xFSp-m@vlk!owZb`|OEtz&>MSmr#4 z_C=qX?>GcKKoMoP9E6_LZ_sn|IrB>8@A5GE%|3vl*Nu#-&&BN6=_FoD&ldEIeurNt zms-X%$Vpt!lAQvIXqzHv47D9e!{TuVf74^-pryJ^3K(wnxc;oec0MRe59G%g?>DQGW$*403N5-H|qF}2VI zMPyXkM6;el=}c*1ZsSVcph4FS+56n zMG>7R|HLiTwS?KB+>eZ`sMOav*dw96$oZ4B;XfUT(b>+GF621$S+lhe^ze12BFE&p z_;nGfT$8KNd;e=xuiOa5@~>mg2>3nz?@9r!$;WUj!Rtn3^!unh?!Bl^^;8u7`#cx-25{ehT{D528$_<%nP;Nytsl>9{1me2%8Up}1%K z^rEa6AMA#IO8NiP@`cJ61$-K1G_9xh_hu!sk$i2Sz47Nh8)#f7lwpq+Bmm=^ZuARk ztE+8AseW+ur0k>j*FZxl5lP{7Y=qBud_NYbWdB-srUqg=cz0ux~c6rY>$iOx2w=Syq zl_(&gscLJoA32pih50$jkqt1Fa%Usr=E)oT_mRnGU$q6Xh_5}VV z5=o5`iqemivauY(cX3}MYw_nPKlHf%Aad5*obpj9ps4kGG3)BoIN`%RwvBO5%xS8=BZwxQjZ!*lO>I3485XYh??>M8IGBpO>Mn$40Hy5g z7V}9W;@)Mp&FbWsVz#_&PIdCW^9!Z@pjqBkHRb%H;5t#?TB2ShicrTe%rsG zbiig$d2dBoC%-1zkat<<8*tZGn1cjSY-^|+MbvhnHmBBDM6#~L?s!>;jKyjqZbek< z_JZX)m7}XX|5va-*Dl~9ve~k!8j7ff^g^Q7hN}~|AMF{gIFV?z==CTX|9X@v>ADZK zAgA#zh~!4yV=XGDiW6;4lb{C}2fPWjy?Pw&eZGXO2c7lj1N)+9nY3bgkey;mL%$I} zfwMN`AdVvEYtZ?1BJmw$6H$p@R6TY#N_S7&DSwXT{=Y*GmVZaY#WlkvjS;k|dl8kx z{gK-3&>YQ&ap?DVT{llTs2 z<_MIvvMAyn>wq8QUZ*(;yI5l7Sc{0>X=t599ppCxN0N>lX1o>IQwWJEf5LfQ+bBFF2CRpz@KDd+mA4OgNb z1icd50jDFfejm!<@XeN4j(+oxB4_6ek}1>J8byr4zvBiJ-S#n_k3(%-ZQo%Y%G`_l zyM^z!KN9S-W#+6!PRlgU{$nt|hYN8R=1O|>qxCWm)gE7lGKP*ud-HJ1K+EBdQ*hUW z=%#vRTO~S>R)`M|n2*Zps$m8avb`36?X5WDPUH;m#buuN3A&~Xmy)|opdB@~_xT}K z03W8d&2bLVf?BnVE@Z5J1bM=I4z;&YokWBe3XkgiJwNFRRjV&Hxg~6 zuS0c_-$cEP_eBSE<#TUC&vaj8%zOhmY@&>dek8cHm*>1EBSLDr<^yPbgVx?YXtT3v z3?>y{=dKZeU?}z=F1!Dp6^c4<3(X6OzuSdcwKPb=n^@vbSL(Y+j zX#MPg+FYHFoORX6+Xf^$7FL_D1{eK}wa8d#N6+QYM879bl)qcn_lYs+J-!e7_vuH3 z>y0S=yW0Nv3Ua3Sq5~OkCN+4|yZQoh1YCo}I)98xW3EDL@JZaneU_lsd$qL6tT#aq zFbZYBoq?jN58)pL8bI~xS0l1}B=IBB1!SNPaH0A_?h;zKIB|Dg5=5J3{=Z% zJFi|q`&?Q$fyPC~E8njbXrI0kIl@mvPOojq33-lmx*A7M} zilW;BEz^))dNuY8%rr*a@~Qps%Oox#{f%6-F+m(c^(apGN4SsLThV-z^u=wJ}c1rn;By+jle|IGAF9N+;Zeq4fcyD zg1!zJsIC$2Fm^$N$wh$aHAJNDN9*WsXs@^(rEt0`4)>J5Uz}DyEhrip6$J{dDb^fHDhgYw)k6SC4**<(_Ge4alav1a$#82L1k9lC9Q#IkJT!%4Tv6^sPpY zK-bLLF{u2+RlIo+l>mjE#AA_Q=CwrIN@aBlin${#AzXvs573Rg z{V?@Tbt;O?JdYx5^Di5kfpluY2yxQ(jNzem~L(%6bFMKD8wx>xZ;$R9& z6^|;ZdIhDi2Ev~%1pQ8G_uLoD^ZN+tr5^U6i1RiilM2{qf?kOtB9A+uy2roZ@9YSa z`Clx5{&d`0-W`fL^X&;F^yY$~GlVJ4ydR~YZ%6tiW*w;f&vd4(L5}@2Fpb)}Zn%yv zbjDqb>c@9P!I23xzZOf5`eH096?5(SaSzI9bj3d9&$zDb{_=bRC)$B1LwN;_6H*Mm z`~P};Z_qyXZ}=&?m(ck>)AL?A4A>ceV)F{MrQsHdHb~Q6q<5fH(RMaHXs4qC*ymb* zAzt*nfW*XH@AWjzQ&*YnBl6ybR@$rxI5kvg;KJ+f_7d!tBg)c5N|=}=j# z=V?J+>t~35Hx?kV#Yz+q2{(P_RYWwBKoLb@9wB-0l@c;Ae@k*L9=F1tO12hrMnM6Q zl7A6h1NT}Zo-y`Y(9d>dYosu8bxxBL{83DHeDa1QT9ambN#5a#`Wx+i85+T>+&}0o4Ppx z^`$qRD|ZmDrnDMbiGDwQ_sM=loH|it@%zYWokZP!jvN8@M*21`>#`3y@Le-%isire z$ufQ!1p|Lg?cHpW9INqHbl(PLmHFIfImvfm8gXbrDMh|Nvac5=j3e+{>}iZOsJ8Sd z6v-|im0BN9(Q=fI6$b}-JcIQp^=zbJ8d76^7#ZNTltT=l-}ce+HIuB*Eg~Xq%DmWs zdf}Rc?Y2hg*irTIYmh4V3>u#@R6r5UbBR_TUx(5#O$WgYM8w0NDWStG>7L1(q38c_ z`M$Lftv(bnzmeKhn{9Y6=2W^Kq)csc)D%#8#&3zvNLq{P(x)aoXID%>5!I)$zsKE( zcv#wZd8PDlJ8VQEx@*pst@AK(*&_>P{yk zkCdyh#Z|;ph#wAUK_XrsN2M6^@Y^WX0gs_I_X$+Ln}(q<1qnr&2-X5*Xqhk;)muJ^ zeLbv4oQ z4$>1}8-+azFrR4siEWT0CraO-6YX`oQr~F$4CEAhDPnHRP_)`cvPYrkZ~E+$C|e?J zY=P4KK8FM!|Aa`$JxCz!K9pwvSL87GDsrsOCf!Ivvk>%3Oh-IFo`^V;JoxTtE-(Wp@X1Ad= z|3Bz=-I@7xpXN#wkkJ&?6Ss0KLaEq|PEXAOf_7JDporEL zNZ9LMl;-q%mnj zhao~@JD~azNjVMsq4yr_bz*B%oO>lEqTlHh=YrhcFwUkJ8xA z8bKLLcwX~R6tEU-k5tnCfSkamqMBLT``U>d&VN9^*Ey)e!d#NiD9Vw;7jMNqE9X^Y z8@ggEk}LmcWu$|m~$n$*cdqp2g`#ubNkNR0e*NCl7?Hkz9f)1$}$WWby*3L|{ ze#TMX!zGHK4 zVtFGO%}me(6wrCP6UtJ!0PQ)oWZFHB`x)Ja(z3(XC?TW2kr<1dbK&=>liG9_CZd2# zWm@nOR7w);)qN;J{u`uzeH?N`ZAbi08#!{Mg$~q}$90dm5x@86RF7+CdBn*&+k6~s2P!9t%9I*F?X+f7-AQ2Uk}!zglamsjf3smY5cEn^ zI$)1O2DgtkOfb&!xaX0W*Vw~kL!|R`%vpEeA%4rXT^VYRl@J*}1NbmrObol1!A(Y?0U0-toa)|)Wbc2KaG22b24&TqgBweP0q zS4&A>l4wWSGvPTd0OrKF-egoR6~10KDly2BqnWWIDz9>}wR?~w+jYMfk1`k=%|((J zKf&+JFCeF&?|nvk8JaW3p$LpmZP#j4mlQYEYcr&5+vvqu>li>$?KrKVX5!wbv4q+x zj(;n*QNRhnKO*Mai6ZsKB4=eav>_tDG~$}U zddvy>$RX&JI2m(m%V+R&qHl*eU~wyr6^9Gs7Yw!ARueB^@JYstqUb(261ym;ZQMvMBI$%3F)M;bjk{2aRa~6H@Mur) zBx;uxC(Zw}xYuqxNG=T5>?i`S$1JMRiE4jcTjWdw&O?T0R1fLj5>FceNwz3=)Pd4)8@&i?9VJ9g;vCppVfOT!Mj!b?9xfP*T2q+nahISJaaY1J7OjgY!W*P2 zpNw0S5;mqL|K2jsV5~qSz(<>a)Rzh&<7Ejev{`tCZLjtC{_M$ls;UoSL$q} zQxFwN*npySTj1{448#dt+HK_oePj~!gOPw+_&YB~!g4-N^wTg4=AKKodU>CZ*>*b` zzwi4&!!-s`W|xaTN{$@OfL2s$SWVVvo8mxbK}T!On@ zIwH}6xsEAnJO>ynlY)l=LFqO(EBlGJ(Z9~flsPC=y%)~ zk#O7ITtZQCQ!BPY`SZ79uQwk=>+neYNKOHfuWG67!N>{v$Rp_OsO6U_&GIVz)6zf2 z&{WtGT31<8E&n)Xf_@C{^Be~}V7SI0DsO6bi~}a+BXUe68=VR2foYiSFVpBn0TtI0 z)~$)Q#hPi%!|$A)j(f>&FdjlJ8gp21Abw^=0$Y)e#2W1Hb1!fadIwF{AB}`NqUyuDP-J~O&-qP6>G-B= zK7xo{_`l}_-A>TgA)z-@J>6PlRK#)WZH63zQIYCZD4M-FT1V|DYQHezGd;+05J$Xf zJnG?YThpdzlN0ojMbPIU=Vti(E=QUdzW8roedt>2Q6xS~_%!Bs;B~m4A@9Z<^_zzX zM>C>`p22(?Z!0#-SOxqqBO_ztJ-F%M3?fIkt3D9Of$bWh&+_O**#@rrNCy(u^NFIb zMO`uS&Qt~z@mC(~M22A;w4$0RUon9!jzz?ao{vT;pe|{?{V39&*PahQw4wAK6PbPu zzXKt_7!>iCM|8=`-%#3C;2kX>;kj>MkA4PG_^+e7Wgm0^U&Z`gR{#fKu3yRtx}Bh} zMVS^CW8P061&$)#!PkPEL<_MWwq3x_kn`}9L<`NWKsD?$W6T9)@R;VY4%HKy{(DZ) zM-D-60p7(>r&0+y{CO%p+1hkyA<+W0m@_v~Jj&EkTID8nFZkMyCoRAUmJZ@sV+ z!=sF{5#ziV&%4<$8h4TE650=3Ytg450W=??n#EB<0+BvWy8Wr%J~uqB0k-wH$B4mP z+-mwN$0NiSN!gp>;nD0z>Zx%KuVXQbv6#kFNUSDHDRnuL!}i$Uhdc03Qw9q z!+qb5T20y3Q3(kiMmZZ7qB_|)YZ=ti>IKWSe+_)Z@ZWQSKC%e1WG!Nc_c_S|&1yLVG?f?aJm|RFSxq2C8&^(Ae;$A|Y=A($0 zGCc#emNQ-d4%9&ca)NF{HGa4h_`P)031*JDBM`Ic1+iP z7Ns?xZTPpGppPtqzCY#+j)lOinAiL$dRCR!8$jvcSxLv7fJEHF-}_-U?yFH44}6QE zXGA{|^K9nKy$_=OuhF(*Gmgh&Jnv>iEAUI)pUZbWu04j~QHvgE+U(-^y^E+rm#G7W zZ44l8n5FI*4aanp3hVP@y+FJO8mO<{BcAum>i7ZijNUikp8oL!ik{~f-e^Na+O!6q zL(%ZKn2ZQol;%T;XyxvUfE{AqzZDrurdqG%s17W6k8*-;Mm2&CYHRif%#rK`)RsP0 zM5**&z`ZnS07bWFQyZB%6SI-s5Bxh?J2^o&VwL1)6Z`0;@;>e0p;=kc90CG7p41F;U)o$$(_*sYNH5|tM0nz07gQ4; zZo@W+Qd0kh|9g~UC|00ssKXFhuf$GBJn(%nuN`Dsd<_}%*APumf}YdgL~AH6W+U=# z${_kZD&feH1LX7`VAE`)KIB|1#k}Sclgt3GsgEXpv4!Za$Gr{$G!Jrj^;-ziUNgGDhE*%;G@*uW^WfnYNG1Q2a!R$2t|-; zaVCyM1mDz3Za#8IR3eX{o8b~c5AZq6QS=h3CyyHuYegxurr*dko<#zhaU+eRkwd_w zkFpr;r3LZ`dK5-d1brNemcNGHi!{CULsahvV?*NeIB zOSt4@=VWj%G_UfvYi|TcC*85tEG92^vj6R80*7=1_xM6w36+Y@8BvqCo7gwL0HhwH z5%5>92&)a#188le`^~-@gc@R8^OqgY{pVSX~i)8^LmTY$`B$9PN>RQF!Z9z&d^S=pS;3+-dp7 zB53%Aarbv7I-rHuz0@pLap8S?PwTd&a`L7>_EYA+stDBzUM?AP z#6m`y=GqYN0^h+{%Q6IBreyu`KUEG2*i7qmW2^kN*AmXK2&*nlK#WPS#glOsh$d}J zoD|^Cm|>Qc`c_h%6yOad&hhm1cfr!#S9-S+R*OT&qvqjd zo~GqdqpS$a8}K+9QTK1})sWf5gz)}dmCj3=BMB@hoGI$;D52TPS3)rL?=_vA#&1>k z1F7IIeJas;Kf*LW8P8FPxM(97o@gg=OS3*Oxv1%qr#hg0`GmOUVp(z$XHLlbpv)-0 z7O6;5>IRz2>Ct4e4G71<(T_3tkS-?Vmhe;5f7kiZEQ;am162#Y=c{&p8Hm&tuici% z115mWIjYaEjpBmw|GzKe_0Wcw$IJzin;W|F9=O`6<|feksZ5`%Jxm?SCV*y|=pg3m z{di3@ZJ4bL5K{HvFMI`)SZQjp4&!Zg!%o?zEuoQryGR43Vn!1h;Fk`nMHnX#)NSm+w2y z_~%oj0VmG6nlLn2C_Q1H|NOwHCWFixdR9yetiDb(kf13W%4Lqv+iS``P zs(auvv}b?pfO-t|6_Prk>2nsuld2_R8eLi0wM`F>GnMT-7l$?{ZF^?hn5_~n!ELXj zJ2ai_;zR=nv5hd&Ak&9S>53}Jt zjMu1U49iz)*K-ZydU~^uno+?U^sO^ZfKJ=5!$3e;A{r=-Fi4*YeKUtln0Kr3qf^bS z1!1cKcOD7Tby5sqPz%7_A*oq(^yoI`N~}0019HhY6jb(wlrClb_46r3Av)8Hq?h!; zli5WX*H&n;*Rk}8ZN@_Pnw_{a*iG4+D z&gqpcF;Vrxk3K7_8Q!7%X$7hS;8g5O|N2jt&#oWO+kG3;4|vsO#CKqlZZNl4NoN_C zZ%J(z6u@cx;@l_a8g)krh!@WaT|hL%&2-#~tX^m*+<$gRPAo~OAQR}Xd_tR9(kBG4 z-d%u|sa<2aW0jh4iZ5OGTKyD;IH!!=JRX0>wegBo&Vov>p9y-F_?$ z>!f|U_h)+`eY)rW-^Vo!CW>}hi&$WOW{6II1#>xI2U|(C59jnG)5Hl$9awLICV0(@7 zN5~hBIR$pPUsW5LV_8D3EtNFJf1#4t@_|~+pFc3GPiA73Z?hM#T2R$G;P&N=gE$=# z$6~SY@vk*AOr;6+aN}vOVDntylR z$-J+?9ZP|p1=HpRwk}kvva+d&F9cIT75XQ1u{$bk?ACPRrBX~f-WTR7*=_po`m*ve zq4@HA%rM!hjrkDU)_BO_-_+T;h}jv*^NXC*SONyReLj}DTRwQL=hWg?QV1bx+}Xs? z1Q* zIRikxd2^P_8Q>(L+E-2b^o3%@mqFFX#Qr8zq$6M}mF63{n~*IFf|+x)f*_X(Txx+| z`P`eWl6CxIGaDlljS1ps>0sw7VmUb;GwZI~EyIcMAW#mLc?og(yF)JBajnzhh*s40 zT`&|ICh#ouP17?s7T~67#!EvZw-4ua&zEHN;dm&o02jJ9+N}Mb^!}oRhQ(|s{`bWf zqH5JFNWG*1VP5MdH~8CD?OZvy1I@3xA#I{%PnX*lt(~bfZF{n}7rl=ahDL=J&K*k@ z{m|2CJWqUa=7o+ovE5Iqga$h>plSMY2YuX82TQIt1&-z6MkOdX;;5=OB_W!SRQyRT z+F$*bBKYj(HO5CuQu0Dn-F$1(B^Ib@ey2W?gh7Fj8~KOWZw62{R@;%?|K+dBctgh# zu3?xk!6cZDYk+?xfCR+N)V-=SN#zQq8RQy1RmZ!LR(zMO2F$DGNcuQ6RKm06bjXET+vt-Sp^2L(y+fk`w~&(oI#5MqqHV1 z7TB;if4Uv7zj;C4FSb#bAB|zPe1S8&pL$+8|DI~XzcSg{{bca`%>%u!9t)!LE+dfW zSLu!b4T>x#F~<2iScWEr4g6>+twPMo(^*og5?ijnl+qmJS8~!gj4sa4Y4GBEstLd-0KYS6%3hhd`0mL_F}PV8rPy0S703 zqA@(6pzy1*ua&G2uJ5N zz}Jw}x>qE9RjXlF2iL4Vf;{KzS$WD^FP2}e>}^pUuMptg!W#6>Fk=Ag-M0@6f5AaVwW5P%#S> zv1uu+Zd(OuvTX53mw2#DG(Lr%{pM>^;{tqmZ2P-|quQ}b${K8(}qN9Sm! z3gD5>>Y?spgoX*K`8g^!jBm6kka+8v=}VVES@2vOUT|rko`EtJxZFej{?V*|s-0&- zqa%=cOu20LM7j6uq8|k}#Ov_?aFr!`_!EDDP$Wvf>;R>Lg0A8{)B1+`bG-0CKkmlc z1Zj>%T*nvw8!@XP@|)ws(3BkYl;Zl&?bTPVy!xgefF*mMz0J>)Qyq$B%0J~JonMJE; z6VMX6@G<{e^phef^(bPfbP<2cc)VkWlrqcTOQxGBJT}lBw2;4?zmpU^_v63#L+T*E zCo;J=fL~MqC}VCN!|Lz%7|)pg;D>MFwG-jfi;kS$=7%Wj8vT7Iw3j=#x{|f;3*0Q{ z^oPC4kOGkwMlmX>F~*?|Zmc1@RQ*etwKbf)@_Yd0t;jWLah4RvyKzA4SnQb1)Bwr9tj0EpASN8?|UE%0La7D>>ah)!N_x0%0E zO_7QA8m+0&E0-jeZWk|TwS&cZU<`7Iql>ZMdKVaD`q5D^du1S_W?j~~mcu^$V&O#0 zRI$5sH$T+#ZKRbMazD4mU@%A3xuQkSi)niuX9MrY-B;Nu#HmxMV+xGQ-Qk_R*>24w zOtA`#Nflbpv-TG-QpyI=Guys?p@R37Ju*yeX9Rj)Nzj#Asm z(C$T1{g2~U3~^Z>dBG#FN5F`ik-%y$LGyoG$dJ!)NlCv5W8T zQpDAYFhfMKsNRecr>aO6UDNwk+&j#5*AipC0TFUWJ1wyEgY=eZBMDNVKf}TMWN*rg z(-RFg)fKq*jsESSqTPl#MSfh8kpOA));eMTs1K`4VQ*Zuqa612Bf@@_r&nr9P8#@y#Fd zxe`D~Nq>bEHe;*yzjft~`!Gw~9qEfZ`%;%GBlSiX%}dC#s<`A_N)QvCW3!0qC#&h{+)%tTF*HU`iXq8fAP4V0D zn38N`k;&PFoE^T2TdEX>so>d*G$^f0o$K@0+T2-}Srn*FXt2I2dh7fUmb<1T_!*dT z|CW>Mk9g!!6l(yTYy>!JyIM&w1YmnzM|+J!iX?Z?HBq9wQl99v73r;u{SnDD!wYaM z8S;~WN!ZRBfB*dnpz7@WHfCQLT2jxP!h9 z+izZuBf1k_-Jns)5ceHJ+ot~*vQlDz<4f-ouJ9ex4}blzF(ucnpst>z$a$16fpbDa zKi)vqm-r{m?>dL5H>kvfRWq5IY+2UNbM&gbsC_$!A zYe{ApG}-)u8Ic9?04{0kb}y1fTs%95K>wJLiN&Q?pn92v1B%>YZ^9>6l}^zwO2baf z2=l;GA>?pH9R-gACLp*EUOU;&GI_@aFd0jIzQ&_aZ$gW*Z40wlqL=O(Uu#c8!NflOR2IzW(G{ zo0{15kAD+~95tYd*JN4BTl{{i+RV>V7L1?S3YF zSQug-FZC=#yy2+(A?gc-#5mg3yJTC$q~@GEl9i>XvG6PJ$!k?HjHRBIF_kXiLN22b zomR51;sN1O*PQtwcDet=>5Cq6610a-3cW|x?K+;n8Jnn(_p-owe=2#tXe2B(&Fp4s z)}mCK#Qa4F*;B{vB176g)3s*ShZKii6}D2_>J~!AJ;8?n+jaz`_b7Kl{_G0{T5j0n zOoPJd%|=H?B>l4P(<(ILKmutnTw?abi`j|+X;&MobV=|b@gk;+ix4yST=x&#Cc5#M z7VO`)Fo?%BPy8u`0sCHJ)r|e9QS&w-Z%ipe18Qf9#%1Frbh1cXX8jnz*xCmrLRyh* zBqa4p)8pRBtFW>?Fi^s+R@KrYXQvre6hC{`iWOye*#2~A#ks!Jl{=U|h3KJqGF$1= zSGT=v)y#!V>@mDelPi=TU(#ZGFza#;xj43=U{M2XieY7!;`5(|J(fI}TWx~2T~Xh} zK|r!aZA*sjZa*aC?AHiUMKek@EzTgjC&xM-oC6#4^i4%xrW{0Ll$PJoi;B0YT?H(U z1U0ZK%9hK0^lAxhPXCV@3%P7qU`wZ^%X9d1L$3o9ocO^sAScLr5M>s8aYQi9c z2b=DTjL$L&p(jYOEBKg`xqr(oAMQpr&p>m%agWTeBlj28A;~u(InTaJRNT?-YM)UZ zZ&2=yKNd3zX_BK?d~m5mcL@&&N35ODOovZXji3fi7`c|#)3z^;Z*AOvQfu7siLy_< zfraoAW!NbfZn15*G)EmsFR7!tfb)k}pp3Ps#U%FZ^AurJBiy_pI(+|`PohhMS+7q~ z@}=5J_=3P>7^dr%MG^|)AU5fAi1UQINSNFoUc}E4=eh@QuQ}R%=M=i;ez=Y^rxTo_ z7!L1uHj+VD_{J{2enO0P{%Md*_jCI3T^w>%g|3*?V?w(AL($HtEXEYgL;D&^Go1-1Xg zEqJ5z9OJaF;RPdyFbrqOp-Zbzx4tpf(zqyFvr?Xxau;8uAIcpCV3~!uo3(5MV z(DrVZ_KuRWo+$D#256IJvd}T!wiN!{=_^=BXYIr#%g#RA*=*!(cErL*qCs-|cJpz~ z2TmYa78|OcT6~^AX{Bv#AXn+7<_1(hltucK19C8?mBxLbgVwr6%kLr!As}phEv0V* zXCGq#Ep@=pOu=6R3=;$`3d)VZ*!N>+zs6$TjpO&@SqdKEz_2_0|J$Xbq zb?h4l;)j`W0!IH;*4rixIA!hiygrF+@bd*~TFdlOg}lpMnu&IcvEqmK?M|0afWM%JsRc%`@5)5LS(Ks*RMg%WHTSjpQe> zL?5W1t$v-|o*7kz7nwuj+oH`n7GyJ(!WRnhAYE?v@|TB#y0jR?wPK zW2l$=7rkz9A?2fH!N8aV_-b>Oib^Z`afIUGn&_I{LhXvv9x2C`SA96jHk( zpKzSw{7{*(UEhugAINp1SD@oNo6nt|K{U5a)*Lvn(oNMw7e?<{Je>Twv2Kft*(|&H zTx@!i#LeK*2@gc8!LqVCa9naV_1@z`@&S;Kni`PbU_6yg63K6dZg^IN-l4>3EykoY z1I36oPrapksd)G;^SB%8gF2C5}=ar$$D(F&XrQNJ(ajU;On zi1R6}(07;SC!Y~mi;w-*p>?EqFt^5Rj34`AOUvO0{ys|ix4T^l4-{n~2c3ZTyDnAs zGD7`7=nS4#|D)gfLc&v`d7>HV!6+SpsmS4Fc#kHw5IV}Mem`$xL>3uK0fVOqnRKwj0JtVrHGcgF*QIgs}q%ELTTdoIN<&TF$8nMgeJi<3^Y_6 z@Dt_fIXn=l)SkY~n1wt22RI!<%-Z`1Opxi}zcwXJL zdqK8z1%TC`a)z+bIr1T_-BZ^4st(o3ei9aZQfJ_1{L5oI_Z!pCWEeX8U-y!$5?OIU z%@zibM=D0ao2Ng>OZdRrmE9$Fp^18436J#52gWg?tmW}G?>q@p-Vq`S-*0(h9f+>H z+vFY|JF;BPJ_pGsL>$9Pehf+YrAi(4h0ED>b6Q`JJuNtA{KYw6LfkoPX4<`{NZgPJ zvY4bWsEi3u@BKm6xaXvVCaswy-iy9$XOomQMw)&fTcNC!IOeM#M6A;qwm8#o&g2<) zLbUAI=)pIs>J<089`4s|vEC93pmMIk?aiY-zb`cosGYr7%g%`Fg!*nUcfSHB_2 z@aU9`JV{t2UVcIUR`!fs@hPK6#YG0%N2ti%Fzkafh-PBXP=8gL`4dlo8e>%SUobKjr8*kYxq;}=1P zo7BFqU~Xa!bHKEC6wp<@A=Ul<`K9hH3Gz?6B(f!Zv{LD%+X&cwaO3n^>SRa#f1&cI z#c8$({H@I102-dh04*4&q?+5Y}kB z691HR4E^VaugWO`GAr+t)&`;7uz0wib|9V@y@~3?0s1gt=Ick>R{#y(?SK!%pY$Gq z(tlqO&O6Qf)m0?JNs-0@6u zY;>X&YRNGGs*LW>w{9}w`{$)>CRQkBDp<|Oxu8}+Wbpt#ZhtIM+ae^L1N8~69oiVS z8lbff4G~=2Va(K!e23+ohA+^}L1-!c%lT$!W%xyJgcOP z6~*}pFl}EzrzK+nf=|4h-U<5T4yze+{6qQ0&&OnRfiMvx}F;2^*00Bxd? z?l>iVT?Ls6FuPfPL3U}!#$oy%$0SAqTbDNaII{aG>_BzL?yXqOwp{WT+BQcqm-Ns% zEmBCS`U>VO+&-}IZ7Q)m@$AIdL=OFL_}lbX*SV~%9`Q1ju-Tf(QEnU2U>Tg5xy;#H zg8@rv_xDcXyRH5-F2SOb`dV7sTR>N58C9X{G6N>?XxSUB=Hk}GtvL(xqrifjjWJo( zfAPJI;xB4;2i+T^^mj2^MYAIH~N47X7)Pvn+)3G7L;sQU32|xcM$gIQ%gOmeR#ywb^J1ji`&a(!FH57nYBck zQ6I&_;CQ>No1A0+{o;6f^%L*Q`bY*NC`0h35frfKI zkx8X988*Lp0yubLm5w*n76@asa?e%w{t6f7lRPb)w$e<-wEB;q=z_URKMI=@W7%6mx)f~$_fMIXO zlB~u-zKkx(A95TcKD}3cW?ek24%7p#h)MNHI!zf4;CVZwV=qT9ef1L?$sYN}(aRFY zmS!Jnx~yUjp>3@a`*(M5ug&Dpua-&2)OEbz0bmuqeo!`lmmE&-@2{B{M^%aCDt+Z0 zuEgmfXS)I2m*B%NkrOGjDIRc$A|jrmfBNs~8l@+Zw>p#$(2aJ*f{CYHAN2f_dUNl> z6#b`Yj<3YT=Qr>u6!hQi?7wz(Na#3t(}C3?n24XQ25mkea_T6$-A{H`1?i%z^SxKb znib)vVmLV)?eGFmH9KapD&uK)oMM*_Cy5__e&th@U~A*RzrC z&Xp?I!a#?G{BjhdFXvB{;si-~2U)Pi*8(l3X>laJKCY)r;kN84&W!)Vd9B!pC%cE) z>fuW(R@ErdLBqzXEluyt=pSGIAJA)7_y*Tfv)Q3VDao&sz?8tU+LG3DYWwl-Aao&} z@P2LlMxx?H{r4l*H&44-$(>~XiM8ND0uNI>yhwJf4A6N$>nLuyrym~UK#iOiabn)c zGjRv}SgVGLAJ`8o9pKpbq86H}JcsAM&KP6)}WYVI)WUB^d4a4T|s1+T>u0(iWjaCL&QAj~Jei z16U*=8jz{o%J*qI@|kixLxM`2sEAxP%mmC!oo&r$&^^A_M7FS?*9jg*H@FDwqL;}f zM=lDGU#xzX#Qz~?k-E^w*x0Z}sKj;pi8lNT&QBVo0M`iw|4oEsJQzA@OAWS3<|~%W zHVa}9m@CHqtFPB#6d4;@B_s$)P!2Y#sq&fIgX`}7nCLVimI_oE{GjmM8c2^70Er$a zhq*NDpWRSJ(;|O}sXN9(c8G;wI6g#*aQhWtSXD*2kPsZ_@npsR=Tp0gcRjp{ZfYn| zsE(9tR-Wk$p4g~eRAbKl&wDL)@UOgt6VXF7}YxJMdk^wSy2f_l3Q6_#ou!Wg;d0kt65#*XrVv>*liq*8#Je7E8zSSr* znYdK?QC9w?0_m}iv(OJE^VK0a*kqcyu^1tNsmz*wKWp9P-;0v&ZMBGYY0A&MyDfIW zT~;55;y$Z*Wul*8SZ|CF4k^Rjbf&y+z{!%87aJVQOY)0?GCtHn_rp&iTE@yce0{As zJuqvDBat%FQyIF*$3NXcFdns7?U#N>oMRqnnxqW6| zV=7u>+d@1&e`1qz+Unt@MPcamOp%3MfI+Z|GP{j!Urw83WVQP!SR<7m1(HPAJ62B# zZ>Fhu@h>l0VB3N$1}SL4-CiR5pJQuHE4mm%a*k(!+E1+EsqXV6*0B~?F@(=$FYI4! z=|d>KCPcScq46i(|Hev#%-7xdofjxQ#Q*Mo&Z(FA%sjbF zH*QH?`8yy1+Rm(AlNUO;8{aAIdWGv2U#8{)K*3BaXBSc}(bK>hGIne1VR_q-8EwmV zCD_)+2mcERq$+jRopR!180h%&7}zrlu2uzxY(A5KO2j}Y4X|x}z-TdMQQ%daBMU2^ zedTq2?U7Gp{A5v-%yid74RFTWHT=t}D9#vdbzOUFw25##6=bwM5()WB9Yzr1{U}=R zcK0Xs8D+xaGqV^p6O6v=>PX>|^0$7^rutjHkVDhlIiyU+9b#lf8kbitqBagEcw*EYs-qNV zDf;^76PXKT=$<6e{c@8Ql1uB4207O7ST%ze>*B8*c|T_(yTLgakNHo0S;3W{wr0XI z3;0kFVOf9BOo;oTeU((KnUTn~?Q6Fzj%VIUgVvZdanw;^#eA|C9tKdP#(%7UDHeySG zw89G;jwWC}lT?Bh?ymZL5iVQ15Jy#QI>7>>w`RO;K+|j0#Vfdgir@W8>993WE&e-- zvA|T@ep|AqRP-!@PEHLfP#=lMv#>j6TalGH^dj-08=|kijqe#@9Dvd5ztubwB;xqQ z>#7&iWVKzw^hLR#Vn+3yKGfrb#(2ahlFtrzBuEUcb>wX!2TWK#H^?NQ^;HQrKr_g> zZ?wb#A`(2F&^?l)=BLz!Q!l*DUEB9ktD`;F@67@?*Z~N7Yt8VBaynhdc%t6lDZ!I-(Q2U-I<%D zO7YligzNK17L~bozk!q*D647+qzjthzpZUX&kg1N!ry3u#>dC!=m*46*y1~oEPk(runsc9N zOn0*P6@NruPB6%n8wXuu3#yLy$d{ez*pe zwCYh-vMwNgWOp$bOx_R#s@lgicjbjnkIkh#KabFGVZVwbnWfu`djH~7Sx;a3GJ|M~ z`e67C%ced}n1ei56YuG{@HWyoxf|0C{D#bxz?|jIzkY8yy49mqcGLB;`nCNLheVQKP!IiO53|0IC%haqPcZq zuQ)KpoLTQh7pGDlqv#?9=z)TZjIA;1l|?Rj5fC0_|IoPG&hpz6!$36d)OS#NDQ(EFI$ZBod6q->SC{$yRp?bbSF88_`6h8eAFzte@D9Q1{ z253zw2FQ}bkP&v79wYONr8g1hTx5%b&;V}8B2C4QCLw*L?5blfOuzbu`szwTjX-E0?FdlCi>zf9+mi}gVw^5x6f^i^hm1d!t@Q;?jeZFzn?;c8L*I6#Sf73j$8Fq~ep#_|2@Jjf zo*990YXWUl*5_))=vO+v&R}H1>wKjL2ld`vu1$){gy$5!Gqnq#kmAoyD84)OM)@mv zsIMI-++e*};ty!=BhNZjQ6#+)t~UBv)|19Bsjq~h0qczQyeI61H>LsRu$tG~L4?{E{Z6pJcEii}f07kAmXM@HrXTfsB? zEsx<%U7Jqu_Bg_X9(oOww~Nmb9QmiHRtQ=R_(0v{cCi1PB@`j{7I*w9B-lNzoa&Hzi2X&yNJ>87}ce@X+5PY zMF4EY7@zP21OGnH;ymzO>L`ejB7t4EkoB5Z7nyB%jpTpJ$DroDoPB^F)m;MxmO_Czfy+8vevCw92np8VX$?5?`R(fxY#jaIz;e(?|pZ#!4V5EXHf!v2+V};V8AN<1EW2rxV#iFa{=p^(1gzwd0-xO|~ zqDlk8R+MXhsaGPrp6VSfgSB5ez4(w;lyY9nv-B{mmkl=%$3@92Xb)Fen#SRt9a z?o|#vSE&*a1yO7=fd>bfG+WTL3$tvbbkX=d!vQal63%;wtj%N><*F5FsZ%PG3n-051I1jpMMMV!bmf%>r>l5B)+6 zEwAx4y#nb>U6a(D!9ah3wF`Z0@>tiHCy(l(i^HEp%-#a)sQthIbQqAFBVV^WEPM=W zOT?mNv`D7glXutcm(2Q)608{wAmP&Xc`HmE9%s`TmgmV5T%Ef^Tcc(4;l_2Pr02af zYkrx%t|{@@9_jXYl^M>qeWc435=F1!CVJVAXkKzT&8;pqov|O**pi`G5RTfOdHErD zm3SFkTXLWVZu^UUCU;xYti#0mlYxeVBVvIJ%x}1hGRt9{rU-AJ(tc*$3Mns2h#XCjci zCb5B;iO@#fgNPv6?FjX1JSNicI*e*Sh2hS}iHeU7ynlR)?$k#=TngS0c;TC|E5%k5 zF3{1y&omtaWoux>ovu+YtdBUbS#v;iv`wAen*LUPe1EOELuU`h2h?&@+<($#A{4yQ z?53ALV)qj{sVZp(PdC@2N0mZITSeLWE$`Z50+UKjBoNA#lF%`@#K0nf7t=(9FS7!| zzy}drRnoQ$_TLo}j-E&PW9!uVv7h06^Gu9I;}ux0aIu$TV_s%qj%AL1D%hYk0%DXg zX7HUVXNYcqkgNrw6Xz0o0K{?nyL3@zkuN3`BsPD-tQCwDB+zB$*KVC^i7fMuk_Lb^ z90=-v65yk9mPL?gBz+JQ$0Nm_*eA-DzP}&*zT?3s7H*`sZ+x)Yt-s=d`rBhvZZ+nk zY`$T(1_BC-TPs6VP6i5f23-XYxJ>o;g5bg@ZOP$ClkijWc4HTj9NGH8#$?Uua(P!G8Q;ivj_ShTL#v6#cYUJe3*!rtaODqF3MF zw;Cj#O^eJ!M(^fzf!u^GI6iLs1fuM}80?Sk=~4WQ~x@v95$4)2_wKdW9`LDgzrzO2DVBXCl*z>SXn39`f2}LQqMJ1 z1#R+@{&Lt%x8Jxe^+~4|uFc0(zI5j2jIRCqa$$Yc5ru-1mqmX3!yTOowY7@hb`rfR z>DRV!DyoAyME$W8=eHlbZn@r#tV};>4}mH;fWF0d+edldeLwjM-x1mpP$t14-B;IM zFAt1ew=DzY{?*(rdj(>$chF!|DDb!WosWJ-G+jL4cB=6JUpWR|HZ2+>1*5q*&Re5V z(nQpLPw+1)rr-qxKGB$dN&>$1Se^R^++O^C>T7mQNq|-Gag!&jb35DI7%;iq{@n!+ zOG*HfMOYqHU_14$K%}6suK*6H(7s)hY0yLEC-+{QHP1ri9s1;9d=1WS-Hp_uRdqDL zMWGEZ%1kLcp#E|-fxaeJQ6afa<$GR;AkR~@A5U_kdLzC!cAF0hXvE?yAZP>bjk^NV zweVenb9i4idXC-_#gQtGN{s$!V$|G8?;|_fZ8U;70L!pt0i&2p^md%cQ~HBOVm=McM+haSreTUjU-5CVo_v^Quh{CWfg{S z8FSp32GH%6Y-3w+&wTIwAO{;1iYlLB0z-$y?}&6JE@~mRyKu&-w;+1-umj>xvWau> zX%CY|GNdVkU-Dx2mVV&a$MW`|gCY({VY#D(t|YV|7njvm9GuNDRk1kT#qDEo-y7TDBc%)aqG`e9J=f&}dE*l87*%aPs-VvgrNPE1Y3VHP+6+ORNeLBO^-sBZ0u27u@mn=ZK=coqishTqcUeU1BbB9ABF^7vq6_$bl zmbo3=yp*w$rDd;_NFg1J9VcE0rN5yOl~5YVlh)N}koRhoHX(e{Eu_g<=MI5x`+hVHkSBk1yuJsKqe>tD+Ni_L1MuCjwSUT^AJhoI?Us$6QDek=W7Gc08P zjX)i2Z;b7rsd|)Ja@+ra6#GqRKFyFN1lZ^AfBh`b!wg1d9#o@mg(TFS3p$WrJ9eq{ zSc_=F$i{4Gmg;lNdz2o0!tKSbt>BnReZz^^M+qG;N#1qj6Mi>O6(DU0wAr~q`N&2S z91c0@&=U404CFM3{7MguHAvg@VcLHU+|wew=9zzcCjst|32evs8L0AC4|qqKqHv-4 zjoel8#cSLp(IbD0M4U<8zLk*JxW+?&rs<{0|Ed}&QCjvt z=7KIE6EO!jGC{bl3C^-FA9GB=pGoVbSxby^AT4XtX?Zx;5BOxQ{|3C$qYu8QpDruj zkWMI8NpkacRYcs!TbtAyjn#@xqqKY_eYJ1%eodC>hpskg7UT>_GIM^|aa;+eI3)%v z-Sr?IqXp0AlG`slx(&qim=X7;lmAS1~O@18`^l$f(u^&4$E|XGgYwn8)=dCex~-@%G!fZKZ2iYl#GWEMD^^ ze0q5U0qd)u&S6h0u3#x^OCJkKePc%CU9W>{zx*myuTTW6ET%{F}6!&sg^1^qa+2b(o|r-dR1J{-4MM8NhxmNpkpC z(uYZSD_vAr+j%}ri=K=<>w6|A&h-Dd z0L&0u+_%gu*~n$@(Wi6CE$NK=(c$|X(BVrr_uCcF#$Vm2nl|jMkaIQYX5LL`_ zWul2UT9{wiT@LKK_=a{3fLOv~1zZDiYwy3BFmZq|3cJ(4yU&88&_RWUxf65apM;ZN zBUatBm*NUNKO+?4^IqZYQMAg=`5N(IFw{m$qmAIOWZ1z|j+M1X*F@^`D0W*^Ys*B( z6FMAp9TiIvEEfj`&T|38d1N7$5`!`Q`#X0h9;C&!x!p94?_y_lo>g2av664k*^||) z!z%CdVv+Q19~Z_}`>kziKe-DLxQ#zJiF*gI6#bAfXW(M^sA3*$Z)8aZn1Ux<2z31u|csH9#vp=jvI#ifyc1bS@GpjQp zU!6;uD0pLoCL4XS0q-{sy~>-`oAW=(s~3LcL?0r%x}}+fBd{#C19IFV`d^&MFK5VK z3XaQ9L};>Z@pPNMWMCIoZvX!PA3@;0o%$?7(+u>!%6J1>1gE{P@1jyC*Evl`J?32x z!8^#;n(k4!qw)jj+K696kpt^}q%U}fP;~muj=$B@K#!OXd-=pMWZU2DyDDnRoL^oRlyU;QRB#)b%=Cq~e`csmm6Y;?OQGtisE!_eIB za}9i>Md#S=Bz0Fx8npUUc3u=hFug<=0j3FbF_%N z1J&Du^akSF)YCu*eSR-O>7!}=vuhcnC|dn%WYE?tLX>5o!&nKy%_M)`BWU4&0(%-Y zMdT=Qsn2gmMnRt5-_udKhRaE_3^yWR$R#G_DV z%L>BVN4eh124u*bO?{odtu)XNz+Ghge;yJvK^)Vwla&Y3-^69NIoO*mAFMTKSTYq1$xn3_$-t?cs=ga`n0hT zIhekH=KXdtE?Q}zM-=aFGgjnhtir%5Hrx5#|!Z5T(cfab9Zqf zVVRFRP|&q$JR1L|_hsa@ds;J3u!BkK{pQ;;+w#ENFDxU8XGBVm4QANWkAH|uw5vM6!PqR0&W4WYf%Kdl@yS%7k4I&l248K zG{#5J@29?5IkSMLqOz9XAZJQQI(8&L`9xVvovL%D5MeLyqrHzHOH(HBSWsPjQ@ONfhioE1T>li>$@0*eFvIrSS z$DxS85>#&+MG1GJhxJNioZNu!jR(9D z+cK*0y{-=D7Ny^PnDGye=c3Y(n}K(t=-KRq{p>`M>x+>$xz*f`Qp$dT=GMjNb3YjM zYoCp-p;5-Jq5IIab0>OWJV4_e2_f8%dh0Gnb?~mXJO@_(b_{a9xQwQsqPebIhjSXK z?CWvJIIXTbpN=v^dP%-+22gp|*U;x(s>3v&tAozk(wg9ucn~Ib|`;A z4$QZqYiJXyYmW@6TIoV_+cVHJBU(>}7}lFm#>l0}p}7*JYa51<&N8%aIEi#eejl1Y z=F=W)q;_Et&5h6T+&=08_1%V@Q5DmW_u1EbFp5&cS{)Va6!ycfjZcYuf=Nc>=t2wQ zStzY+K1w&Xt$+6jS{%L-rB=J5AAh3ue&sF};1)WGdO`n{ zbP>OM(S(^KEZ2$%dYmG9Ku$;3%3O5a%tNW0(e=|y^E}+45%lWUoq?PXNf{YQY1!+j zf2uyn;7Sr5Od4B|)5n$0-0BE=JJX5?dJ#E?;@4ZW9tjCf%t09m@$+9z`$R$aMNWtK zIo?6O(vYcWk^WqiDUc_Xe`;kvmm()gExHMJpbW!9QPxK%wC%MBdNvk8Z!WLLucYoy zlJ$KiD+SzwuxaCJnw0H47p2Jgp4fI2%`*IW9e$tdUE~vhF$`aNP((e=aa6)>d~HE> zh4oCiE+T_3tu>r$3?Xr{gERKu4hDKVQmcU;aR^EY_N^@)W=y!R5uA-*y>^sF`^c1i znSq{6D-876xJxT`BYlAo4#Mpa3~BM$Xl9|zqWC$kq;(7IZsbHf3I%mS#okKj`&xs< z;yN`TVFr3OW}r8h<53hn>mJ;JGH5K#eNRqA>q+0a>`rRXm8xaYJe@Fcm7Pe0#>l3C zA~Zhyp4!sZ3?r0bG)cdgu^Rh01;$U}!zfzy=Y-ePN(n{DzlEFzS(H&qcMuD z=J^0xG#yL!{Bb*~yIJ3W^LYr>k3W{|zUnJbE#03Y`Sj47 zR#`JpHo-B#DM(M^K>UM1BdBEHR#eyj`^vwz#xacgGl;tCJf7@z+~G(E$H%EYS=f%| zs-rx&j|}tz*-nvvQj38enr5_&#w^e$B2UDx#_K-hy$M}t7)Bm;0zX0vsdu1$=t+zq z$J}iVI+s3Fo1AsOT!(t${tnAHX%}`Ok+s?AI!a29e_myqkE0r{deDv1)82xj`Dwyb zqbOtK!8#q=Xb#Uq3(7cuMTTDp1#}I@uaglJfoc~@$Z;R1K$y-+5gGI&jI~%x+jf5> zxcF$`;b_4+HQ;!IsO;q`l-+O($^@{rjbSvF%TQKimK}H~${@-s4mpw7f#xcgD6)%w z6cm_=I%9az3RE!&Zuf51;PB$2>~nZUDAq~UKl zj%zfJxv1VfP8qf03hk0n#GT$*qP-$Y1>DiA<^COSwVdqRNOH{=Fzl{%mBJ zFG8QY?|QO~o*kE>t`#d#efmz%*BgeBpD4{eOGjfi%8a=TIoAzk_S}QA{CqN$3P=Ea zF%2}(`^ioxjdN|__LG&lD6*492?-WiR)Wzg%4kuwh2-?)0-B6Ij9UMki_(Jf(pA~# z>rnJOO}wNFrBna54#zd>M>i6#dKTHyy#k8LeHuOB&PU$sT6iVu{k<1{qBjNfyn7MK z1T&$)_UGwH)Gf}9z#nVer)!;5roGayyKyfRNAX`&zZ11Q>_zYI1XSAbyUO1eAz`zB zL+R`Ngmvc`Lf^sHQN(-;W8F3k!)Pvp$VrxEpchc-?JUedkBBr#R^l)Iwg?3qyt7I} zMdD5*tj)$C_33>)88{g~Pt0nh2sqr>(=m)xjwZW1Ljk2bz7Yv+Eu!|ea$P8$@)u|k zITb%6>!~P;Ur!ZzBOm*sD0V!xJu-UY3-$ui=lB3JutQq*6;K59eBgg7KgX||GSViv zi}4GPZr~-rW3X$e-I;-+x^YposI^+H5$~jQ>vn39*@s&cZwSBlJs-6Win2-yNFeHL z;B@r){R275jzyn!mT^9UB20G!|BW1NrvpDg5p&nQZWu=M_$hw(;0a7jp|~QN$9f5W zzCVb{jeXe{tzo?t@k1l%rv`3+9ra0DDj?&v2N|Zjn0N;2$>@QY#eL|3XBe$wFI3m< zN)ayOo`B9kDN=7k(UTBQ>{MhRorfM2E=tIGwV&ho??&~FjjD$?{OLu;xvMl`6^gDs z8!hJJ*H=IACX|u!L6oAGR=UuK>S8|xJUL;Ta%DfCMc2q@kQPX_@wGRqS-%Ani)&{d zhf*=)3$44*oD-Vf4!S;Eb5)V%s-tHy6?f*q0NL4g1yrgu-SM}x2yX|r3Ej�l!3T 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�e8u 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 diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..fff21dac --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,137 @@ +Welcome to ConfigSpace's documentation! +======================================= + +.. toctree:: + :hidden: + :maxdepth: 2 + + quickstart + guide + api/index + +ConfigSpace is a simple python package to manage configuration spaces for +`algorithm configuration `_ and +`hyperparameter optimization `_ tasks. +It includes various modules to translate between different text formats for +configuration space descriptions. + +ConfigSpace is often used in AutoML tools such as `SMAC3`_, `BOHB`_ or +`auto-sklearn`_. To read more about our group and projects, visit our homepage +`AutoML.org `_. + +This documentation explains how to use ConfigSpace and demonstrates its features. +In the :doc:`quickstart`, you will see how to set up a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +and add hyperparameters of different types to it. +Besides containing hyperparameters, a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` can contain constraints such as conditions and forbidden clauses. +Those are introduced in the :doc:`user guide `. + +Furthermore, in the :ref:`serialization section `, it will be +explained how to serialize a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` for later usage. + +.. _SMAC3: https://github.com/automl/SMAC3 +.. _BOHB: https://github.com/automl/HpBandSter +.. _auto-sklearn: https://github.com/automl/auto-sklearn + + + +Get Started +----------- + +Create a simple :class:`~ConfigSpace.configuration_space.ConfigurationSpace` and then sample a :class:`~ConfigSpace.configuration_space.Configuration` from it! + +>>> from ConfigSpace import ConfigurationSpace +>>> +>>> cs = ConfigurationSpace({ +... "myfloat": (0.1, 1.5), # Uniform Float +... "myint": (2, 10), # Uniform Integer +... "species": ["mouse", "cat", "dog"], # Categorical +... }) +>>> configs = cs.sample_configuration(2) + + +Use :mod:`~ConfigSpace.api.types.float`, :mod:`~ConfigSpace.api.types.integer` +or :mod:`~ConfigSpace.api.types.categorical` to customize how sampling is done! + +>>> from ConfigSpace import ConfigurationSpace, Integer, Float, Categorical, Normal +>>> cs = ConfigurationSpace( +... name="myspace", +... seed=1234, +... space={ +... "a": Float("a", bounds=(0.1, 1.5), distribution=Normal(1, 10), log=True), +... "b": Integer("b", bounds=(2, 10)), +... "c": Categorical("c", ["mouse", "cat", "dog"], weights=[2, 1, 1]), +... }, +... ) +>>> cs.sample_configuration(2) +[Configuration(values={ + 'a': 0.17013149799713567, + 'b': 5, + 'c': 'dog', +}) +, Configuration(values={ + 'a': 0.5476203000512754, + 'b': 9, + 'c': 'mouse', +}) +] + +Maximum flexibility with conditionals, see :ref:`forbidden clauses ` and :ref:`conditionals ` for more info. + +>>> from ConfigSpace import Categorical, ConfigurationSpace, EqualsCondition, Float +... +>>> cs = ConfigurationSpace(seed=1234) +... +>>> c = Categorical("c1", items=["a", "b"]) +>>> f = Float("f1", bounds=(1.0, 10.0)) +... +>>> # A condition where `f` is only active if `c` is equal to `a` when sampled +>>> cond = EqualsCondition(f, c, "a") +... +>>> # Add them explicitly to the configuration space +>>> cs.add_hyperparameters([c, f]) +[c1, Type: Categorical, Choices: {a, b}, Default: a, f1, Type: UniformFloat, Range: [1.0, 10.0], Default: 5.5] + +>>> cs.add_condition(cond) +f1 | c1 == 'a' + + + +Installation +============ + +*ConfigSpace* requires Python 3.7 or higher. + +*ConfigSpace* can be installed with *pip*: + +.. code:: bash + + pip install ConfigSpace + +If installing from source, the *ConfigSpace* package requires *numpy*, *cython* +and *pyparsing*. Additionally, a functioning C compiler is required. + +On Ubuntu, the required compiler tools and Python headers can be installed with: + +.. code:: bash + + sudo apt-get install build-essential python3 python3-dev + +When using Anaconda/Miniconda, the compiler has to be installed with: + +.. code:: bash + + conda install gxx_linux-64 gcc_linux-64 + + +Citing the ConfigSpace +====================== + +.. code:: + + @article{ + title = {BOAH: A Tool Suite for Multi-Fidelity Bayesian Optimization & Analysis of Hyperparameters}, + author = {M. Lindauer and K. Eggensperger and M. Feurer and A. Biedenkapp and J. Marben and P. Müller and F. Hutter}, + journal = {arXiv:1908.06756 {[cs.LG]}}, + date = {2019}, + } + diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 05d6e0ab..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=ConfigSpace - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 00000000..bad7efa8 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,228 @@ +Quickstart +========== + +A :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +is a data structure to describe the configuration space of an algorithm to tune. +Possible hyperparameter types are numerical, categorical, conditional and ordinal hyperparameters. + +AutoML tools, such as `SMAC3`_ and `BOHB`_ are using the configuration space +module to sample hyperparameter configurations. +Also, `auto-sklearn`_, an automated machine learning toolkit, which frees the +machine learning user from algorithm selection and hyperparameter tuning, +makes heavy use of the ConfigSpace package. + +This simple quickstart tutorial will show you, how to set up your own +:class:`~ConfigSpace.configuration_space.ConfigurationSpace`, and will demonstrate +what you can realize with it. This :ref:`Basic Usage` will include the following: + +- Create a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +- Define a simple :ref:`hyperparameter ` and its range +- Change its :ref:`distributions `. + +The :ref:`Advanced Usage` will cover: + +- Creating two sets of possible model configs, using :ref:`Conditions` +- Create two subspaces from these and add them to a parent :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +- Turn these configs into actual models! + +These will not show the following and you should refer to the :doc:`user guide ` for more: + +- Add :ref:`Forbidden clauses` +- Add :ref:`Conditions` to the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +- :ref:`Serialize ` the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` + + +.. _Basic Usage: + +Basic Usage +----------- + +We take a look at a simple +`ridge regression `_, +which has only one floating hyperparameter :math:`\alpha`. + +The first step is always to create a +:class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. All the +hyperparameters and constraints will be added to this object. + +>>> from ConfigSpace import ConfigurationSpace, Float +>>> +>>> cs = ConfigurationSpace( +... seed=1234, +... space={ "alpha": (0.0, 1.0) } +... ) + +The hyperparameter :math:`\alpha` is chosen to have floating point values from 0 to 1. +For demonstration purpose, we sample a configuration from the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. + +>>> config = cs.sample_configuration() +>>> print(config) +Configuration(values={ + 'alpha': 0.1915194503788923, +}) + + +You can use this configuration just like you would a regular old python dictionary! + +>>> for key, value in config.items(): +... print(key, value) +alpha 0.1915194503788923 + +And that's it! + + +.. _Advanced Usage: + +Advanced Usage +-------------- +Lets create a more complex example where we have two models, model ``A`` and model ``B``. +Model ``B`` is some kernel based algorithm and ``A`` just needs a simple float hyperparamter. + + +We're going to create a config space that will let us correctly build a randomly selected model. + +.. code:: python + + class ModelA: + + def __init__(self, alpha: float): + """ + Parameters + ---------- + alpha: float + Some value between 0 and 1 + """ + ... + + class ModelB: + + def __init__(self, kernel: str, kernel_floops: int | None = None): + """ + Parameters + ---------- + kernel: "rbf" or "flooper" + If the kernel is set to "flooper", kernel_floops must be set. + + kernel_floops: int | None = None + Floop factor of the kernel + """ + ... + + +First, lets start with building the two individual subspaces where for ``A``, we want to sample alpha from a normal distribution and for ``B`` we have the conditioned parameter and we slightly weight one kernel over another. + +.. code:: python + + from ConfigSpace import ConfigSpace, Categorical, Integer, Float, Normal + + class ModelA: + + def __init__(self, alpha: float): + ... + + @staticmethod + def space(self) -> ConfigSpace: + return ConfigurationSpace({ + "alpha": Float("alpha", bounds=(0, 1), distribution=Normal(mu=0.5, sigma=0.2) + }) + + class ModelB: + + def __init__(self, kernel: str, kernel_floops: int | None = None): + ... + + @staticmethod + def space(self) -> ConfigSpace: + cs = ConfigurationSpace( + { + "kernel": Categorical("kernel", ["rbf", "flooper"], default="rbf", weights=[.75, .25]), + "kernel_floops": Integer("kernel_floops", bounds=(1, 10)), + } + ) + + # We have to make sure "kernel_floops" is only active when the kernel is "floops" + cs.add_condition(EqualsCondition(cs_B["kernel_floops"], cs_B["kernel"], "flooper")) + + return cs + + +Finally, we need add these two a parent space where we condition each subspace to only be active depending on a **parent**. +We'll have the default configuration be ``A`` but we put more emphasis when sampling on ``B`` + +.. code:: python + + cs = ConfigurationSpace( + seed=1234, + space={ + "model": Categorical("model", ["A", "B"], default="A", weights=[1, 2]), + } + ) + + # We set the prefix and delimiter to be empty string "" so that we don't have to do + # any extra parsing once sampling + cs.add_configuration_space( + prefix="", + delimiter="", + configuration_space=ModelA.space(), + parent_hyperparameter={"parent": cs["model"], "value": "A"}, + ) + + cs.add_configuration_space( + prefix="", + delimiter="", + configuration_space=modelB.space(), + parent_hyperparameter={"parent": cs["model"], "value": "B"} + ) + +And that's it! + +However for completness, lets examine how this works by first sampling from our config space. + +.. code:: python + + configs = cs.sample_configuration(4) + print(configs) + + # [Configuration(values={ + # 'model': 'A', + # 'alpha': 0.7799758081188035, + # }) + # , Configuration(values={ + # 'model': 'B', + # 'kernel': 'flooper', + # 'kernel_floops': 8, + # }) + # , Configuration(values={ + # 'model': 'B', + # 'kernel': 'rbf', + # }) + # , Configuration(values={ + # 'model': 'B', + # 'kernel': 'rbf', + # }) + # ] + +We can see the three different kinds of models we have, our basic ``A`` model as well as our ``B`` model +with the two kernels. + +Next, we do some processing of these configs to generate valid params to pass to these models + +.. code:: python + + models = [] + + for config in configs: + model_type = config.pop("model") + + model = ModelA(**config) if model_type == "A" else ModelB(**config) + + models.append(model) + + +To continue reading, visit the :doc:`user guide ` section. There are +more information about hyperparameters, as well as an introduction to the +powerful concepts of :ref:`Conditions` and :ref:`Forbidden clauses`. + +.. _SMAC3: https://github.com/automl/SMAC3 +.. _BOHB: https://github.com/automl/HpBandSter +.. _auto-sklearn: https://github.com/automl/auto-sklearn diff --git a/docs/source/API-Doc.rst b/docs/source/API-Doc.rst deleted file mode 100644 index 130e3080..00000000 --- a/docs/source/API-Doc.rst +++ /dev/null @@ -1,193 +0,0 @@ -API-Documentation -+++++++++++++++++ - -ConfigurationSpace -================== - -.. autoclass:: ConfigSpace.configuration_space.ConfigurationSpace - :members: - -Configuration -============= - -.. autoclass:: ConfigSpace.configuration_space.Configuration - :members: - -.. _Hyperparameters: - -Hyperparameters -=============== - -ConfigSpace contains integer, float, categorical, as well as ordinal -hyperparameters. Integer and float hyperparameter can be sampled from a uniform -or normal distribution. Example usages are shown in the -:doc:`quickstart `. - -3.1 Integer hyperparameters ---------------------------- - -.. autoclass:: ConfigSpace.hyperparameters.UniformIntegerHyperparameter - -.. autoclass:: ConfigSpace.hyperparameters.NormalIntegerHyperparameter - - - -3.2 Float hyperparameters -------------------------- - -.. autoclass:: ConfigSpace.hyperparameters.UniformFloatHyperparameter - -.. autoclass:: ConfigSpace.hyperparameters.NormalFloatHyperparameter - - - -.. _Categorical hyperparameters: - -3.3 Categorical hyperparameters -------------------------------- - -.. autoclass:: ConfigSpace.hyperparameters.CategoricalHyperparameter - - -3.4 OrdinalHyperparameters --------------------------- - -.. autoclass:: ConfigSpace.hyperparameters.OrdinalHyperparameter - -.. _Other hyperparameters: - -3.5 Constant ------------- - -.. autoclass:: ConfigSpace.hyperparameters.Constant - - -.. _Conditions: - -Conditions -========== - -ConfigSpace can realize *equal*, *not equal*, *less than*, *greater than* and -*in conditions*. Conditions can be combined by using the conjunctions *and* and -*or*. To see how to use conditions, please take a look at the -:doc:`user guide `. - -4.1 EqualsCondition -------------------- - -.. autoclass:: ConfigSpace.conditions.EqualsCondition - -.. _NotEqualsCondition: - -4.2 NotEqualsCondition ----------------------- - -.. autoclass:: ConfigSpace.conditions.NotEqualsCondition - -.. _LessThanCondition: - -4.3 LessThanCondition ---------------------- - -.. autoclass:: ConfigSpace.conditions.LessThanCondition - - - -4.4 GreaterThanCondition ------------------------- - -.. autoclass:: ConfigSpace.conditions.GreaterThanCondition - - -4.5 InCondition ---------------- - -.. autoclass:: ConfigSpace.conditions.InCondition - - -4.6 AndConjunction ------------------- - -.. autoclass:: ConfigSpace.conditions.AndConjunction - - -4.7 OrConjunction ------------------ - -.. autoclass:: ConfigSpace.conditions.OrConjunction - - -.. _Forbidden clauses: - -Forbidden Clauses -================= - -ConfigSpace contains *forbidden equal* and *forbidden in clauses*. -The *ForbiddenEqualsClause* and the *ForbiddenInClause* can forbid values to be -sampled from a configuration space if a certain condition is met. The -*ForbiddenAndConjunction* can be used to combine *ForbiddenEqualsClauses* and -the *ForbiddenInClauses*. - -For a further example, please take a look in the :doc:`user guide `. - -5.1 ForbiddenEqualsClause -------------------------- -.. autoclass:: ConfigSpace.ForbiddenEqualsClause(hyperparameter, value) - - -5.2 ForbiddenInClause ---------------------- -.. autoclass:: ConfigSpace.ForbiddenInClause(hyperparameter, values) - - -5.3 ForbiddenAndConjunction ---------------------------- -.. autoclass:: ConfigSpace.ForbiddenAndConjunction(*args) - - -.. _Serialization: - -Serialization -============= - -ConfigSpace offers *json*, *pcs* and *pcs_new* writers/readers. -These classes can serialize and deserialize configuration spaces. -Serializing configuration spaces is useful to share configuration spaces across -experiments, or use them in other tools, for example, to analyze hyperparameter -importance with `CAVE `_. - -.. _json: - -6.1 Serialization to JSON -------------------------- - -.. automodule:: ConfigSpace.read_and_write.json - :members: read, write - :undoc-members: - -.. _pcs_new: - -6.2 Serialization with pcs-new (new format) -------------------------------------------- - -.. automodule:: ConfigSpace.read_and_write.pcs_new - :members: read, write - :undoc-members: - -6.3 Serialization with pcs (old format) ---------------------------------------- - -.. automodule:: ConfigSpace.read_and_write.pcs - :members: read, write - :undoc-members: - -Utils -===== - -Functions defined in the utils module can be helpful to -develop custom tools that create configurations from a given configuration -space or modify a given configuration space. - -.. automodule:: ConfigSpace.util - :members: - :undoc-members: diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html deleted file mode 100644 index 244d7a5d..00000000 --- a/docs/source/_templates/layout.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "!layout.html" %} - -{# Custom CSS overrides #} -{# set bootswatch_css_custom = ['_static/my-styles.css'] #} - -{# Add github banner (from: https://github.com/blog/273-github-ribbons). #} -{% block header %} - {{ super() }} - - - {% endblock %} - diff --git a/docs/source/_templates/navbar.html b/docs/source/_templates/navbar.html deleted file mode 100644 index 48f75c56..00000000 --- a/docs/source/_templates/navbar.html +++ /dev/null @@ -1,51 +0,0 @@ - diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 53e47365..00000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config - -import sphinx_bootstrap_theme -import ConfigSpace -from datetime import datetime - - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -sys.path.insert(0, os.path.abspath('../..')) - -# -- Project information ----------------------------------------------------- - - -project = 'ConfigSpace' -copyright = "2014-{}, ".format(datetime.now().year) + ", ".join(ConfigSpace.__authors__) -author = ', '.join(ConfigSpace.__authors__) - -# The short X.Y version -version = ConfigSpace.__version__ -# The full version, including alpha/beta/rc tags -release = '' - - -# -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -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', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path . -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'bootstrap' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = { - # Insert options - # Navigation bar title. (Default: ``project`` value) - # 'navbar_title': "Title", - - # Tab name for entire site. (Default: "Site") - 'navbar_site_name': "Site", - - # A list of tuples containting pages to link to. The value should - # be in the form [(name, page), ..] - 'navbar_links': [ - ('Start', 'index'), - ('Quickstart', 'quickstart'), - ('User Guide', 'User-Guide'), - ('API', 'API-Doc'), - ], - # Render the next and previous page links in navbar. (Default: true) - 'navbar_sidebarrel': False, - - # Render the current pages TOC in the navbar. (Default: true) - 'navbar_pagenav': False, - - # Tab name for the current pages TOC. (Default: "Page") - 'navbar_pagenav_name': "On this page", - - # Global TOC depth for "site" navbar tab. (Default: 1) - # Switching to -1 shows all levels. - 'globaltoc_depth': 2, - - # Include hidden TOCs in Site navbar? - # - # Note: If this is "false", you cannot have mixed ``:hidden:`` and - # non-hidden ``toctree`` directives in the same page, or else the build - # will break. - # - # Values: "true" (default) or "false" - 'globaltoc_includehidden': "True", - - # HTML navbar class (Default: "navbar") to attach to
element. - # For black navbar, do "navbar navbar-inverse" - 'navbar_class': "navbar", - - # Fix navigation bar to top of page? - # Values: "true" (default) or "false" - 'navbar_fixed_top': "true", - - # Location of link to source. - # Options are "nav" (default), "footer" or anything else to exclude. - 'source_link_position': "footer", - - # Bootswatch (http://bootswatch.com/) theme. - # - # Options are nothing with "" (default) or the name of a valid theme - # such as "amelia" or "cosmo". - 'bootswatch_theme': "cosmo", - - # Choose Bootstrap version. - # Values: "3" (default) or "2" (in quotes) - 'bootstrap_version': "3", -} - -html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. - - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -html_sidebars = {'**': ['localtoc.html', 'searchbox.html']} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = 'ConfigSpacedoc' - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'ConfigSpace.tex', 'ConfigSpace Documentation', - [author], 'manual'), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'configspace', 'ConfigSpace Documentation', - ', '.join(author), 1) -] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'ConfigSpace', 'ConfigSpace Documentation', - ', '.join(author), 'ConfigSpace', 'One line description of project.', - 'Miscellaneous'), -] - - -# -- Extension configuration ------------------------------------------------- -# Show init as well as moduledoc -autoclass_content = 'both' diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 389fe60a..00000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,101 +0,0 @@ -.. ConfigSpace documentation master file, created by - sphinx-quickstart on Mon Jul 23 18:06:55 2018. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to ConfigSpace's documentation! -======================================= - -ConfigSpace is a simple python package to manage configuration spaces for -`algorithm configuration `_ and -`hyperparameter optimization `_ tasks. -It includes various modules to translate between different text formats for -configuration space descriptions. - -ConfigSpace is often used in AutoML tools such as `SMAC3`_, `BOHB`_ or -`auto-sklearn`_. To read more about our group and projects, visit our homepage -`AutoML.org `_. - -This documentation explains how to use ConfigSpace and demonstrates its features. -In the :doc:`quickstart`, you will see how to set up a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` -and add hyperparameters of different types to it. -Besides containing hyperparameters, a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -can contain constraints such as conditions and forbidden clauses. -Those are introduced in the :doc:`user guide `. - -Furthermore, in the :ref:`serialization section `, it will be -explained how to serialize a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` for later usage. - -.. _SMAC3: https://github.com/automl/SMAC3 -.. _BOHB: https://github.com/automl/HpBandSter -.. _auto-sklearn: https://github.com/automl/auto-sklearn - -Basic usage - -.. doctest:: - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1234) - >>> a = CSH.UniformIntegerHyperparameter('a', lower=10, upper=100, log=False) - >>> b = CSH.CategoricalHyperparameter('b', choices=['red', 'green', 'blue']) - >>> cs.add_hyperparameters([a, b]) - [a, Type: UniformInteger, Range: [10, 100], Default: 55,...] - >>> cs.sample_configuration() - Configuration(values={ - 'a': 27, - 'b': 'green', - }) - - -Installation -============ - -*ConfigSpace* requires Python 3.7 or higher. - -*ConfigSpace* can be installed with *pip*: - -.. code:: bash - - pip install ConfigSpace - -The *ConfigSpace* package requires *numpy*, *cython* and *pyparsing*. -Additionally, a functioning C compiler is required. - -On Ubuntu, the required compiler tools and Python headers can be installed with: - -.. code:: bash - - sudo apt-get install build-essential python3 python3-dev - -When using Anaconda/Miniconda, the compiler has to be installed with: - -.. code:: bash - - conda install gxx_linux-64 gcc_linux-64 - - -Citing the ConfigSpace -====================== - -.. code:: bibtex - - @article{ - title = {BOAH: A Tool Suite for Multi-Fidelity Bayesian Optimization & Analysis of Hyperparameters}, - author = {M. Lindauer and K. Eggensperger and M. Feurer and A. Biedenkapp and J. Marben and P. Müller and F. Hutter}, - journal = {arXiv:1908.06756 {[cs.LG]}}, - date = {2019}, - } - - -Contents -======== - -.. toctree:: - :maxdepth: 2 - - quickstart.rst - User-Guide.rst - API-Doc.rst diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst deleted file mode 100644 index b704e93d..00000000 --- a/docs/source/quickstart.rst +++ /dev/null @@ -1,73 +0,0 @@ -Quickstart -========== - -A :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -is a data structure to describe the configuration space of an algorithm to tune. -Possible hyperparameter types are numerical, categorical, conditional and ordinal hyperparameters. - -AutoML tools, such as `SMAC3`_ and `BOHB`_ are using the configuration space -module to sample hyperparameter configurations. -Also, `auto-sklearn`_, an automated machine learning toolkit, which frees the -machine learning user from algorithm selection and hyperparameter tuning, -makes heavy use of the ConfigSpace package. - -This simple quickstart tutorial will show you, how to set up your own -:class:`~ConfigSpace.configuration_space.ConfigurationSpace`, and will demonstrate -what you can realize with it. This tutorial will include the following steps: - -- Create a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -- Define :ref:`hyperparameters ` and their value ranges -- Add the :ref:`hyperparameters ` to the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -- (Optional) Add :ref:`Conditions` to the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -- (Optional) Add :ref:`Forbidden clauses` -- (Optional) :ref:`Serialize ` the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - -We will show those steps in an exemplary way by creating a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` for ridge regression. -Note that the topics adding constraints, adding forbidden clauses and -serialization are explained in the :doc:`user guide `. - - -Basic Usage ------------ - -We take a look at a simple -`ridge regression `_, -which has only one floating hyperparameter :math:`\alpha`. - -The first step is always to create a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. All the -hyperparameters and constraints will be added to this object. - ->>> import ConfigSpace as CS ->>> cs = CS.ConfigurationSpace(seed=1234) - -The hyperparameter :math:`\alpha` is chosen to have floating point values from 0 to 1. - ->>> import ConfigSpace.hyperparameters as CSH ->>> alpha = CSH.UniformFloatHyperparameter(name='alpha', lower=0, upper=1) - -We add it to the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. - ->>> cs.add_hyperparameter(alpha) -alpha, Type: UniformFloat, Range: [0.0, 1.0], Default: 0.5 - -For demonstration purpose, we sample a configuration from the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. - -.. doctest:: - - >>> cs.sample_configuration() - Configuration(values={ - 'alpha': 0.1915194503788923, - }) - - -And that's it. - -To continue reading, visit the :doc:`user guide ` section. There are -more information about hyperparameters, as well as an introduction to the -powerful concepts of :ref:`Conditions` and :ref:`Forbidden clauses`. - -.. _SMAC3: https://github.com/automl/SMAC3 -.. _BOHB: https://github.com/automl/HpBandSter -.. _auto-sklearn: https://github.com/automl/auto-sklearn diff --git a/setup.py b/setup.py index d477cabf..15773e0b 100644 --- a/setup.py +++ b/setup.py @@ -9,17 +9,29 @@ # 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() + return open(os.path.join(os.path.dirname(__file__), fname), encoding="utf-8").read() def get_version(fname): """Get the module version""" - with open(fname, encoding='utf-8') as file_handle: + with open(fname, encoding="utf-8") as file_handle: return file_handle.readlines()[-1].split()[-1].strip("\"'") +def get_authors(fname): + """Get the authors""" + with open(fname, "r") as f: + content = f.read() + + return [ + line.replace(",", "").replace('"', "").replace(" ", "") # Remove noise + for line in content.split("\n") + if line.startswith(" ") # Lines with space + ] + + class BuildExt(build_ext): - """ build_ext command for use when numpy headers are needed. + """build_ext command for use when numpy headers are needed. SEE tutorial: https://stackoverflow.com/questions/2379898 SEE fix: https://stackoverflow.com/questions/19919905 """ @@ -27,90 +39,94 @@ class BuildExt(build_ext): def finalize_options(self): build_ext.finalize_options(self) import numpy + self.include_dirs.append(numpy.get_include()) # Configure setup parameters -MODULE_NAME = 'ConfigSpace' -MODULE_URL = 'https://github.com/automl/ConfigSpace' +MODULE_NAME = "ConfigSpace" +MODULE_URL = "https://github.com/automl/ConfigSpace" SHORT_DESCRIPTION = ( - 'Creation and manipulation of parameter configuration spaces for ' - 'automated algorithm configuration and hyperparameter tuning.' + "Creation and manipulation of parameter configuration spaces for " + "automated algorithm configuration and hyperparameter tuning." ) KEYWORDS = ( - 'algorithm configuration hyperparameter optimization empirical ' - 'evaluation black box' + "algorithm configuration hyperparameter optimization empirical " + "evaluation black box" ) -LICENSE = 'BSD 3-clause' -PLATS = ['Linux'] -AUTHORS = ', '.join(["Matthias Feurer", "Katharina Eggensperger", - "Syed Mohsin Ali", "Christina Hernandez Wunsch", - "Julien-Charles Levesque", "Jost Tobias Springenberg", "Philipp Mueller" - "Marius Lindauer", "Jorn Tuyls"]), -AUTHOR_EMAIL = 'feurerm@informatik.uni-freiburg.de' +LICENSE = "BSD 3-clause" +PLATS = ["Linux", "Windows", "Mac"] + +AUTHOR_EMAIL = "feurerm@informatik.uni-freiburg.de" TEST_SUITE = "pytest" -SETUP_REQS = ['numpy', 'cython'] -INSTALL_REQS = ['numpy', 'cython', 'pyparsing', 'scipy'] -MIN_PYTHON_VERSION = '>=3.7' -CLASSIFIERS = ['Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Development Status :: 4 - Beta', - 'Natural Language :: English', - 'Intended Audience :: Developers', - 'Intended Audience :: Education', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: POSIX :: Linux', - 'Topic :: Scientific/Engineering :: Artificial Intelligence', - 'Topic :: Scientific/Engineering', - 'Topic :: Software Development'] +SETUP_REQS = ["numpy", "cython"] +INSTALL_REQS = ["numpy", "cython", "pyparsing", "scipy", "typing_extensions"] +MIN_PYTHON_VERSION = ">=3.7" +CLASSIFIERS = [ + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Development Status :: 4 - Beta", + "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", +] # These do not really change the speed of the benchmarks COMPILER_DIRECTIVES = { - 'boundscheck': False, - 'wraparound': False, + "boundscheck": False, + "wraparound": False, } -EXTENSIONS = [Extension('ConfigSpace.hyperparameters', - sources=['ConfigSpace/hyperparameters.pyx']), - Extension('ConfigSpace.forbidden', - sources=['ConfigSpace/forbidden.pyx']), - Extension('ConfigSpace.conditions', - sources=['ConfigSpace/conditions.pyx']), - Extension('ConfigSpace.c_util', - sources=['ConfigSpace/c_util.pyx']), - Extension('ConfigSpace.util', - sources=['ConfigSpace/util.pyx']), - Extension('ConfigSpace.configuration_space', - sources=['ConfigSpace/configuration_space.pyx'])] +EXTENSIONS = [ + Extension( + "ConfigSpace.hyperparameters", sources=["ConfigSpace/hyperparameters.pyx"] + ), + Extension("ConfigSpace.forbidden", sources=["ConfigSpace/forbidden.pyx"]), + Extension("ConfigSpace.conditions", sources=["ConfigSpace/conditions.pyx"]), + Extension("ConfigSpace.c_util", sources=["ConfigSpace/c_util.pyx"]), + Extension("ConfigSpace.util", sources=["ConfigSpace/util.pyx"]), + Extension( + "ConfigSpace.configuration_space", + sources=["ConfigSpace/configuration_space.pyx"], + ), +] for e in EXTENSIONS: e.cython_directives = COMPILER_DIRECTIVES extras_reqs = { - "test": [ + "dev": [ "pytest>=4.6", "mypy", "pre-commit", "pytest-cov", + "automl_sphinx_theme>=0.1.11", ], - "docs": ["sphinx", "sphinx-gallery", "sphinx_bootstrap_theme", "numpydoc"], } setup( name=MODULE_NAME, - version=get_version('ConfigSpace/__version__.py'), - cmdclass={'build_ext': BuildExt}, + version=get_version("ConfigSpace/__version__.py"), + cmdclass={"build_ext": BuildExt}, url=MODULE_URL, description=SHORT_DESCRIPTION, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", ext_modules=EXTENSIONS, long_description=read_file("README.md"), license=LICENSE, platforms=PLATS, - author=AUTHORS, + author=get_authors("ConfigSpace/__authors__.py"), author_email=AUTHOR_EMAIL, test_suite=TEST_SUITE, setup_requires=SETUP_REQS, diff --git a/docs/source/_templates/searchbox.html b/test/test_api/__init__.py similarity index 100% rename from docs/source/_templates/searchbox.html rename to test/test_api/__init__.py diff --git a/test/test_api/test_hp_construction.py b/test/test_api/test_hp_construction.py new file mode 100644 index 00000000..862033eb --- /dev/null +++ b/test/test_api/test_hp_construction.py @@ -0,0 +1,252 @@ +"""Testing the API for creating the different hyperparameters avialable. + +These are intentionally verbose and using all parameters to ensure they maintain equality. +""" +from __future__ import annotations + +from ConfigSpace import Beta, Categorical, Float, Integer, Normal, Uniform +from ConfigSpace.hyperparameters import (BetaFloatHyperparameter, + BetaIntegerHyperparameter, + CategoricalHyperparameter, + NormalFloatHyperparameter, + NormalIntegerHyperparameter, + OrdinalHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter) + + +def test_uniform_int() -> None: + """ + Expects + ------- + * Should create an identical UniformIntegerHyperparameter + """ + expected = UniformIntegerHyperparameter( + "hp", + lower=2, + upper=10, + default_value=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + a = Integer( + "hp", + bounds=(2, 10), + default=5, + distribution=Uniform(), + q=2, + log=True, + meta={"a": "b"}, + ) + assert a == expected + assert a.meta == expected.meta + + +def test_normal_int() -> None: + """ + Expects + ------- + * Should create an identical NormalIntegerHyperparameter with Normal distribution + """ + expected = NormalIntegerHyperparameter( + "hp", + lower=2, + upper=10, + default_value=5, + q=2, + mu=5, + sigma=1, + log=True, + meta={"a": "b"}, + ) + + a = Integer( + "hp", + bounds=(2, 10), + distribution=Normal(mu=5, sigma=1), + default=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_beta_int() -> None: + """ + Expects + ------- + * Should create an identical BetaIntegerHyperparameter with a BetaDistribution + """ + expected = BetaIntegerHyperparameter( + "hp", + lower=2, + upper=10, + alpha=1, + beta=2, + default_value=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + a = Integer( + "hp", + bounds=(2, 10), + distribution=Beta(alpha=1, beta=2), + default=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_uniform_float() -> None: + """ + Expects + ------- + * Should create an identical UniformFloatHyperparameter with a UniformDistribution + """ + expected = UniformFloatHyperparameter( + "hp", + lower=2, + upper=10, + default_value=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + a = Float( + "hp", + bounds=(2, 10), + default=5, + distribution=Uniform(), + q=2, + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_normal_float() -> None: + """ + Expects + ------- + * Should create an identical NormalFloatHyperparameter with a Normal distribution + """ + expected = NormalFloatHyperparameter( + "hp", + lower=2, + upper=10, + mu=5, + sigma=2, + default_value=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + a = Float( + "hp", + bounds=(2, 10), + default=5, + distribution=Normal(mu=5, sigma=2), + q=2, + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_beta_float() -> None: + """ + Expects + ------- + * Should create an identical BetaFloatHyperparameter with a BetaDistribution + """ + expected = BetaFloatHyperparameter( + "hp", + lower=2, + upper=10, + default_value=5, + alpha=1, + beta=2, + log=True, + meta={"a": "b"}, + ) + + a = Float( + "hp", + bounds=(2, 10), + default=5, + distribution=Beta(alpha=1, beta=2), + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_categorical() -> None: + """ + Expects + ------- + * Should create an identical CategoricalHyperparameter + """ + expected = CategoricalHyperparameter( + "hp", + choices=["a", "b", "c"], + default_value="a", + weights=[1, 2, 3], + meta={"hello": "world"}, + ) + + a = Categorical( + "hp", + items=["a", "b", "c"], + default="a", + weights=[1, 2, 3], + ordered=False, + meta={"hello": "world"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_ordinal() -> None: + """ + Expects + ------- + * Should create an identical CategoricalHyperparameter + """ + expected = OrdinalHyperparameter( + "hp", + sequence=["a", "b", "c"], + default_value="a", + meta={"hello": "world"}, + ) + + a = Categorical( + "hp", + items=["a", "b", "c"], + default="a", + ordered=True, + meta={"hello": "world"}, + ) + + assert a == expected + assert a.meta == expected.meta diff --git a/test/test_configspace_from_dict.py b/test/test_configspace_from_dict.py new file mode 100644 index 00000000..6784117f --- /dev/null +++ b/test/test_configspace_from_dict.py @@ -0,0 +1,115 @@ +"""This file tests the easy api to create configspaces +Configspace({ + "constant": "hello", + "depth": (1, 10), + "lr": (0.1, 1.0), + "categorical": ["a", "b"], +}) +""" +from __future__ import annotations + +from typing import Any + +import pytest + +from ConfigSpace import ConfigurationSpace +from ConfigSpace.hyperparameters import ( + CategoricalHyperparameter, + Constant, + Hyperparameter, + NormalFloatHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, +) + + +@pytest.mark.parametrize( + "value, expected", + [ + # Constant is just a value + ( + "a", + Constant("hp", "a"), + ), + ( + 1337, + Constant("hp", 1337), + ), + ( + 1, + Constant("hp", 1), + ), + ( + 1.0, + Constant("hp", 1.0), + ), + # Boundaries are tuples of length 2, int for Integer + ( + (1, 10), + UniformIntegerHyperparameter("hp", 1, 10), + ), + ( + (-5, 5), + UniformIntegerHyperparameter("hp", -5, 5), + ), + # Boundaries are tuples of length 2, float for Float + ( + (1.0, 10.0), + UniformFloatHyperparameter("hp", 1.0, 10.0), + ), + ( + (-5.5, 5.5), + UniformFloatHyperparameter("hp", -5.5, 5.5), + ), + # Lists are categorical + ( + ["a"], + CategoricalHyperparameter("hp", ["a"]), + ), + ( + ["a", "b"], + CategoricalHyperparameter("hp", ["a", "b"]), + ), + # Something that is already a hyperparameter will stay a hyperparameter + ( + NormalFloatHyperparameter("hp", mu=1, sigma=10), + NormalFloatHyperparameter("hp", mu=1, sigma=10), + ) + # We can't use {} for categoricals as it becomes undeterministic + # Hence we give Categorical the tuple() syntax and not support + # Ordinal + ], +) +def test_individual_hyperparameters(value: Any, expected: Hyperparameter) -> None: + """ + Expects + ------- + * Creating a constant with the dictionary easy api will insert a Constant + into it's hyperparameters + """ + cs = ConfigurationSpace({"hp": value}) + assert cs["hp"] == expected + + +@pytest.mark.parametrize( + "value", [(1, 10, 999), (10,), (1.0, 10.0, 999.0), (1.0,), tuple()] +) +def test_bad_tuple_in_dict(value: tuple[int, ...]) -> None: + """ + Expects + ------- + * Using a tuple that doesn't have 2 values will raise an error + """ + with pytest.raises(ValueError): + ConfigurationSpace({"hp": value}) + + +@pytest.mark.parametrize("value", [[]]) +def test_bad_categorical(value: list) -> None: + """ + Expects + ------- + * Using an empty list will raise an error + """ + with pytest.raises(ValueError): + ConfigurationSpace({"hp": value}) diff --git a/test/test_hyperparameters.py b/test/test_hyperparameters.py index b53cd6e1..ca8186f9 100644 --- a/test/test_hyperparameters.py +++ b/test/test_hyperparameters.py @@ -667,9 +667,9 @@ def test_normalfloat_get_max_density(self): c2 = NormalFloatHyperparameter("logparam", lower=np.exp( 0), upper=np.exp(10), mu=3, sigma=2, log=True) c3 = NormalFloatHyperparameter("param", lower=0, upper=0.5, mu=-1, sigma=0.2) - self.assertEqual(c1.get_max_density(), 0.2138045617479014) - self.assertAlmostEqual(c2.get_max_density(), 0.2138045617479014) - self.assertAlmostEqual(c3.get_max_density(), 25.932522722334905) + self.assertAlmostEqual(c1.get_max_density(), 0.2138045617479014, places=9) + self.assertAlmostEqual(c2.get_max_density(), 0.2138045617479014, places=9) + self.assertAlmostEqual(c3.get_max_density(), 25.932522722334905, places=9) def test_betafloat(self): # TODO test non-equality