Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ForbiddenCallableRelation #280

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0e41484
Add support for sigma=0 in normal distributions
nchristensen Jan 5, 2023
e07f87b
Update docs to clarify float now allowed
nchristensen Jan 9, 2023
d6a172a
Base normalized_default_value on the original unrounded default value…
nchristensen Jan 9, 2023
c89bc7a
Update NormalIntegerHyperparameter tests
nchristensen Jan 9, 2023
2ab524f
Don't convert normalized_default_value to int'
nchristensen Jan 9, 2023
a8b1d6d
Merge branch 'main' into enable-sigma-0
nchristensen Jan 13, 2023
df5640e
Add ForbiddenCallableRelation
nchristensen Nov 8, 2022
132c9c2
Flake8 fixes
nchristensen Nov 8, 2022
8d90a9b
Remove unnecessary spaces added by autopep8
nchristensen Nov 8, 2022
71650c4
Remove more autopep8 spaces
nchristensen Nov 8, 2022
1178b1e
Add ForbiddenCallableRelation to __init__.py
nchristensen Nov 14, 2022
dfd6d31
Remove newline characters from ForbiddenCallableRelation repr
nchristensen Nov 15, 2022
78360ab
Keep spaces after some asterisks
nchristensen Nov 15, 2022
2fb245f
Use inspect instead of dill
nchristensen Nov 15, 2022
305f52d
Fix example comment
nchristensen Nov 30, 2022
350f79a
Update documentation. Works with any callable.
nchristensen Dec 5, 2022
1ffed8c
Address comments
nchristensen Dec 16, 2022
0a6d1d3
Add support for pickling callables in json serialization, json serial…
nchristensen Dec 16, 2022
ed781af
Fix argument order in example
nchristensen Dec 16, 2022
9078b81
Add serialization tests for ForbiddenCallableRelation
nchristensen Dec 16, 2022
001729b
Document pickle_callables flag
nchristensen Dec 16, 2022
8a409c6
fix: Memory leak (#282)
eddiebergman Jan 11, 2023
51f9eaf
ci(wheels): python 3.11 (#286)
eddiebergman Jan 11, 2023
720b218
doc(setup): Add qualifier for Python 3.11
eddiebergman Jan 11, 2023
8290fe6
Bump version
mfeurer Jan 11, 2023
aaa6149
ci(release): Fix invocation of `build` for sdist
eddiebergman Jan 11, 2023
33ac083
ci: Fix exclude list
eddiebergman Jan 11, 2023
f4e8758
Fix small bugs related to nan values in vector passed to pdf (#256)
Jan 30, 2023
8f5e7e9
pin cython version
nchristensen Sep 12, 2023
6373e8f
Fix toml version syntax
nchristensen Sep 12, 2023
3a3355d
Hack to avoid type error
nchristensen Sep 12, 2023
53070d3
Merge branch 'enable-sigma-0' into ForbiddenCallableRelation
nchristensen Oct 19, 2023
815b18b
Merge sigma=0 branch
nchristensen Oct 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ConfigSpace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
ForbiddenEqualsRelation,
ForbiddenGreaterThanRelation,
ForbiddenInClause,
ForbiddenLessThanRelation)
ForbiddenLessThanRelation,
ForbiddenCallableRelation)
from ConfigSpace.hyperparameters import (BetaFloatHyperparameter,
BetaIntegerHyperparameter,
CategoricalHyperparameter, Constant,
Expand Down
27 changes: 18 additions & 9 deletions ConfigSpace/configuration_space.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class ConfigurationSpace(collections.abc.Mapping):

# changing this to a normal dict will break sampling because there is
# no guarantee that the parent of a condition was evaluated before
self._conditionals = set() # type: Set[str]
self._conditionals = []#set() # type: Set[str]
self.forbidden_clauses = [] # type: List['AbstractForbiddenComponent']
self.random = np.random.RandomState(seed)

Expand Down Expand Up @@ -1526,12 +1526,12 @@ class ConfigurationSpace(collections.abc.Mapping):
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)
elif hasattr(condition, 'values'):
if hasattr(condition, 'values'):
condition_arg = getattr(condition, 'values')
substituted_condition = condition_type(child=new_child, parent=new_parent, values=condition_arg)
elif hasattr(condition, 'value'):
condition_arg = getattr(condition, 'value')
substituted_condition = condition_type(child=new_child, parent=new_parent, value=condition_arg)
else:
raise AttributeError(f'Did not find the expected attribute in condition {type(condition)}.')

Expand Down Expand Up @@ -1573,15 +1573,24 @@ class ConfigurationSpace(collections.abc.Mapping):
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)
elif hasattr(forbidden, 'values'):
if hasattr(forbidden, 'values'):
forbidden_arg = getattr(forbidden, 'values')
substituted_forbidden = forbidden_type(hyperparameter=new_hyperparameter, values=forbidden_arg)
elif hasattr(forbidden, 'value'):
forbidden_arg = getattr(forbidden, 'value')
substituted_forbidden = forbidden_type(hyperparameter=new_hyperparameter, value=forbidden_arg)
else:
raise AttributeError(f'Did not find the expected attribute in forbidden {type(forbidden)}.')

new_forbiddens.append(substituted_forbidden)
elif isinstance(forbidden, ForbiddenRelation):
forbidden_type = type(forbidden)
left_name = getattr(forbidden.left, 'name')
left_hyperparameter = new_configspace[left_name]
right_name = getattr(forbidden.right, 'name')
right_hyperparameter = new_configspace[right_name]

substituted_forbidden = forbidden_type(left=left_hyperparameter, right=right_hyperparameter)
new_forbiddens.append(substituted_forbidden)
else:
raise TypeError(f'Did not expect the supplied forbidden type {type(forbidden)}.')
Expand Down
95 changes: 85 additions & 10 deletions ConfigSpace/forbidden.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the <organization> nor the
# names of itConfigurationSpaces contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
# names of ConfigurationSpace contributors may be used to endorse or
# promote products derived from this software without specific prior
# written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
Expand All @@ -31,13 +32,14 @@ import numpy as np
import io
from ConfigSpace.hyperparameters import Hyperparameter
from ConfigSpace.hyperparameters cimport Hyperparameter
from typing import List, Dict, Any, Union
from typing import Dict, Any, Union, Callable

from ConfigSpace.forbidden cimport AbstractForbiddenComponent

from libc.stdlib cimport malloc, free
cimport numpy as np

ForbiddenCallable = Callable[[Hyperparameter, Hyperparameter], bool]

cdef class AbstractForbiddenComponent(object):

Expand Down Expand Up @@ -90,7 +92,8 @@ cdef class AbstractForbiddenComponent(object):
def is_forbidden_vector(self, instantiated_hyperparameters, strict):
return bool(self.c_is_forbidden_vector(instantiated_hyperparameters, strict))

cdef int c_is_forbidden_vector(self, np.ndarray instantiated_hyperparameters, int strict):
cdef int c_is_forbidden_vector(self, np.ndarray instantiated_hyperparameters,
int strict):
pass


Expand Down Expand Up @@ -343,7 +346,6 @@ cdef class AbstractForbiddenConjunction(AbstractForbiddenComponent):
return all([self.components[i] == other.components[i]
for i in range(self.n_components)])


cpdef set_vector_idx(self, hyperparameter_to_idx):
for component in self.components:
component.set_vector_idx(hyperparameter_to_idx)
Expand Down Expand Up @@ -431,7 +433,8 @@ cdef class ForbiddenAndConjunction(AbstractForbiddenConjunction):
>>> forbidden_clause_a = ForbiddenEqualsClause(cs["a"], 2)
>>> forbidden_clause_b = ForbiddenInClause(cs["b"], [2])
>>>
>>> forbidden_clause = ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_b)
>>> forbidden_clause = ForbiddenAndConjunction(
... forbidden_clause_a, forbidden_clause_b)
>>>
>>> cs.add_forbidden_clause(forbidden_clause)
(Forbidden: a == 2 && Forbidden: b in {2})
Expand Down Expand Up @@ -488,7 +491,7 @@ cdef class ForbiddenRelation(AbstractForbiddenComponent):
cdef public right
cdef public int[2] vector_ids

def __init__(self, left: Hyperparameter, right : Hyperparameter):
def __init__(self, left: Hyperparameter, right: Hyperparameter):
if not isinstance(left, Hyperparameter):
raise TypeError("Argument 'left' is not of type %s." % Hyperparameter)
if not isinstance(right, Hyperparameter):
Expand All @@ -513,7 +516,8 @@ cdef class ForbiddenRelation(AbstractForbiddenComponent):
return (self,)

cpdef set_vector_idx(self, hyperparameter_to_idx):
self.vector_ids = (hyperparameter_to_idx[self.left.name], hyperparameter_to_idx[self.right.name])
self.vector_ids = (hyperparameter_to_idx[self.left.name],
hyperparameter_to_idx[self.right.name])

cpdef is_forbidden(self, instantiated_hyperparameters, strict):
left = instantiated_hyperparameters.get(self.left.name)
Expand Down Expand Up @@ -562,13 +566,84 @@ cdef class ForbiddenRelation(AbstractForbiddenComponent):
else:
return False

# Relation is always evaluated against actual value and not vector representation
return self._is_forbidden(self.left._transform(left), self.right._transform(right))
# Relation is always evaluated against actual value and
# not vector representation
return self._is_forbidden(self.left._transform(left),
self.right._transform(right))

cdef int _is_forbidden_vector(self, DTYPE_t left, DTYPE_t right) except -1:
pass


cdef class ForbiddenCallableRelation(ForbiddenRelation):
"""A ForbiddenCallable relation between two hyperparameters.

The ForbiddenCallable uses two hyperparameters as input to a
specified callable, which returns True if the relationship
between the two hyperparameters is forbidden.

A ForbiddenCallableRelation may not be serializable.
:func:`ConfigSpace.read_and_write.write` will attempt to pickle and base64 encode
the callable with pickle_callables=True. However, the unpicklability
of the callable cannot be assured.

>>> from ConfigSpace import ConfigurationSpace, ForbiddenCallableRelation
>>>
>>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]})
>>>
>>> forbidden_clause = ForbiddenCallableRelation(cs['a'], cs['b'], lambda a, b: a + b > 10)
>>> cs.add_forbidden_clause(forbidden_clause)
Forbidden: f(a,b) == True

Parameters
----------
left : :ref:`Hyperparameters`
first argument of callable

right : :ref:`Hyperparameters`
second argument of callable

f : Callable
callable that relates the two hyperparameters
"""
cdef public f

def __init__(self, left: Hyperparameter, right: Hyperparameter,
f: ForbiddenCallable):
if not isinstance(f, Callable): # Can't use ForbiddenCallable here apparently
raise TypeError("Argument 'f' is not of type %s." % Callable)

super().__init__(left, right)
self.f = f

def __eq__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return False
return super().__eq__(other) and self.f == other.f
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of the super class to keep consistency in equality checking.


def __copy__(self):
return self.__class__(
a=copy.copy(self.left),
b=copy.copy(self.right),
f=copy.copy(self.f)
)

def __repr__(self):
if hasattr(self.f, "__qualname__"):
f_repr = self.f.__qualname__
elif hasattr(self.f, "__class__"):
f_repr = self.__class__.__qualname__
else:
raise ValueError("Could not find a qualname for the callable")
return f"Forbidden: {f_repr} | Arguments: {self.left.name}, {self.right.name}"

cdef int _is_forbidden(self, left, right) except -1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this except -1 do? I wish I knew more Cython but unfortunatly not. I looked at the class above and it didn't seem to have this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modeled this off of the ForbiddenLessThanRelation directly below which uses this. I'm not very familiar with Cython either, but it seems any cdef function that might return a Python exception needs to be declared with an except value. https://docs.cython.org/en/latest/src/userguide/language_basics.html#error-return-values

return self.f(left, right)

cdef int _is_forbidden_vector(self, DTYPE_t left, DTYPE_t right) except -1:
return self.f(left, right)


cdef class ForbiddenLessThanRelation(ForbiddenRelation):
"""A ForbiddenLessThan relation between two hyperparameters.

Expand Down
Loading