Source code for baybe.constraints.validation

"""Validation functionality for constraints."""

from collections.abc import Collection
from itertools import combinations

from baybe.constraints.base import Constraint
from baybe.constraints.continuous import ContinuousCardinalityConstraint
from baybe.constraints.discrete import (
    DiscreteDependenciesConstraint,
)
from baybe.parameters import NumericalContinuousParameter
from baybe.parameters.base import Parameter

try:  # For python < 3.11, use the exceptiongroup backport
    ExceptionGroup
except NameError:
    from exceptiongroup import ExceptionGroup


[docs] def validate_constraints( # noqa: DOC101, DOC103 constraints: Collection[Constraint], parameters: Collection[Parameter] ) -> None: """Assert that a given collection of constraints is valid. Raises: ValueError: If there is more than one :class:`baybe.constraints.discrete.DiscreteDependenciesConstraint` declared. ValueError: If any two continuous cardinality constraints have an overlapping parameter set. ValueError: If any constraint contains an invalid parameter name. ValueError: If any continuous constraint includes a discrete parameter. ValueError: If any discrete constraint includes a continuous parameter. ValueError: If any discrete constraint that is valid only for numerical discrete parameters includes non-numerical discrete parameters. ValueError: If any parameter affected by a cardinality constraint does not include zero. """ if sum(isinstance(itm, DiscreteDependenciesConstraint) for itm in constraints) > 1: raise ValueError( f"There is only one {DiscreteDependenciesConstraint.__name__} allowed. " f"Please specify all dependencies in one single constraint." ) validate_cardinality_constraints_are_nonoverlapping( [con for con in constraints if isinstance(con, ContinuousCardinalityConstraint)] ) param_names_all = [p.name for p in parameters] param_names_discrete = [p.name for p in parameters if p.is_discrete] param_names_continuous = [p.name for p in parameters if p.is_continuous] param_names_non_numerical = [p.name for p in parameters if not p.is_numerical] params_continuous: list[NumericalContinuousParameter] = [ p for p in parameters if isinstance(p, NumericalContinuousParameter) ] for constraint in constraints: if not all(p in param_names_all for p in constraint.parameters): raise ValueError( f"You are trying to create a constraint with at least one parameter " f"name that does not exist in the list of defined parameters. " f"Parameter list of the affected constraint: {constraint.parameters}" ) if constraint.is_continuous and any( p in param_names_discrete for p in constraint.parameters ): raise ValueError( f"You are trying to initialize a continuous constraint over a " f"parameter that is discrete. Parameter list of the affected " f"constraint: {constraint.parameters}" ) if constraint.is_discrete and any( p in param_names_continuous for p in constraint.parameters ): raise ValueError( f"You are trying to initialize a discrete constraint over a parameter " f"that is continuous. Parameter list of the affected constraint: " f"{constraint.parameters}" ) if constraint.numerical_only and any( p in param_names_non_numerical for p in constraint.parameters ): raise ValueError( f"You are trying to initialize a constraint of type " f"'{constraint.__class__.__name__}', which is valid only for numerical " f"discrete parameters, over a non-numerical parameter. " f"Parameter list of the affected constraint: {constraint.parameters}." ) if isinstance(constraint, ContinuousCardinalityConstraint): validate_cardinality_constraint_parameter_bounds( constraint, params_continuous )
[docs] def validate_cardinality_constraints_are_nonoverlapping( constraints: Collection[ContinuousCardinalityConstraint], ) -> None: """Validate that cardinality constraints are non-overlapping. Args: constraints: A collection of continuous cardinality constraints. Raises: ValueError: If any two continuous cardinality constraints have an overlapping parameter set. """ for c1, c2 in combinations(constraints, 2): if (s1 := set(c1.parameters)).intersection(s2 := set(c2.parameters)): raise ValueError( f"Constraints of type `{ContinuousCardinalityConstraint.__name__}` " f"cannot share the same parameters. Found the following overlapping " f"parameter sets: {s1}, {s2}." )
[docs] def validate_cardinality_constraint_parameter_bounds( constraint: ContinuousCardinalityConstraint, parameters: Collection[NumericalContinuousParameter], ) -> None: """Validate that all parameters of a continuous cardinality constraint include zero. Args: constraint: A continuous cardinality constraint. parameters: A collection of parameters, including those affected by the constraint. Raises: ValueError: If one of the affected parameters does not include zero. ExceptionGroup: If several of the affected parameters do not include zero. """ exceptions = [] for name in constraint.parameters: try: parameter = next(p for p in parameters if p.name == name) except StopIteration as ex: raise ValueError( f"The parameter '{name}' referenced by the constraint is not contained " f"in the given collection of parameters." ) from ex if not parameter.is_in_range(0.0): exceptions.append( ValueError( f"The bounds of all parameters affected by a constraint of type " f"'{ContinuousCardinalityConstraint.__name__}' must include zero, " f"but the bounds of parameter '{name}' are " f"{parameter.bounds.to_tuple()}, which may indicate unintended " f"settings in your parameter definition. " f"A parameter whose value range excludes zero trivially " f"increases the cardinality of the resulting configuration by one. " f"Therefore, if your parameter definitions are all correct, " f"consider excluding the parameter from the constraint and " f"reducing the cardinality limits by one accordingly." ) ) if exceptions: if len(exceptions) == 1: raise exceptions[0] raise ExceptionGroup("Invalid parameter bounds", exceptions)