The Configurable Interface

See also

Making an Optimization Configurable via GUI

User guide page on the topic.

These classes provide an interface for GUI configurability.

Some Problem classes have several parameters that determine certain details of how they are solved, e.g. the bounds within which to search, or a subset of parameters which are to be optimized.

These parameters are often set through the initializer method __init__(). However, this can usually not well be annotated with limits, invariants, or other metadata.

The Configurable interface provides a uniform way for problem authors to declare which parameters of their class are configurable and what each parameter’s invariants are. It’s implemented as follows:

  1. Define your configurable parameters in __init__().

  2. Implement get_config() and return a Config object. This declares your configurable parameters and their invariants.

  3. Implement apply_config(), which receives a ConfigValues object. Transfer each value into your object. Apply any further checks and raise an exception if any fail.

class cernml.coi.Configurable(*args, **kwargs)

Bases: Protocol

Interface for problems that are configurable.

This protocol is defined by two methods: get_config() and apply_config(). Because it is a protocol, you need not inherit from it to implement its interface:

>>> class VirtualSubclass:
...     def get_config(self):
...         return Config()
...     def apply_config(self, values):
...         pass
>>> issubclass(VirtualSubclass, Configurable)
True
>>> issubclass(int, Configurable)
False
abstractmethod get_config() Config

Return a declaration of configurable parameters.

abstractmethod apply_config(values: ConfigValues) None

Configure this object using the given values.

The values have already been validated using the information given in get_config(), but this method may apply further checks.

This method should be transactional, i.e. in the case of failure, an effort should be made that none of the values are applied.

Parameters:

values – A namespace with one attribute for each field declared in get_config(). The attribute name is exactly the dest parameter of Config.add().

Raises:

Exception – If any additional validation checks fail.

class cernml.coi.Config

Bases: object

Declaration of configurable parameters.

This is the return type of get_config(). It is used by environments to declare their configurable parameters, including each parameter’s validity invariants. This makes it possible for users of the environment to automatically generate an interface that prevents invalid values as early as possible.

Usage example:

>>> config = Config()
>>> config
<Config: []>
>>> config.add("foo", 3).add("bar", 4)
<Config: ['foo', 'bar']>
>>> values = config.validate_all({"foo": "3", "bar": "4"})
>>> values.foo
3
>>> config.validate_all({"foo": "a"})
Traceback (most recent call last):
...
BadConfig: invalid value for foo: 'a'
>>> config.add("foo", 0)
Traceback (most recent call last):
...
DuplicateConfig: foo
fields() Iterable[Field]

Return a read-only view of all declared fields.

get_field_values() dict[str, Any]

Return a dict of the pre-configured field values.

Note that this is not quite the expected input to validate_all(); the latter expects the dict values to be strings:

>>> c = Config().add("flag", False).add("count", 10)
>>> vars(c.validate_all({"flag": "False", "count": "10"}))
{'flag': False, 'count': 10}

Nonetheless, passing this dict to validate_all() may accidentally work, even though the type signatures don’t match:

>>> vars(c.validate_all(c.get_field_values()))
{'flag': False, 'count': 10}
add(
dest: str,
value: T,
*,
label: str | None = None,
help: str | None = None,
type: Callable[[str], T] | None = None,
range: tuple[T, T] | None = None,
choices: Sequence[T] | None = None,
default: T | None = None,
) Config

Add a new config field.

Parameters:
  • dest – The name of the configurable parameter being declared. This is the name under which the value will be available in apply_config().

  • value – The value to initialize this parameter with. Typically, this is the current setting for this field.

  • label – If passed, the name under which the parameter will be shown to the user. If not passed, dest is used.

  • help – If passed, a string that further explains this configurable parameter. In contrast to label, this may be one or more sentences.

  • type – If passed, a function that type-checks the user input and converts it to the same type as value. This should raise an exception if the given input is invalid. If not passed, the result of calling deduce_type() on the given value is used.

  • range – If passed, a tuple (low, high) of limits between which the user-chosen value must lie. The interval is closed, i.e. exact values of low or high are accepted.

  • choices – If passed, a list of values of the same type as value. A user-chosen value for this field must be in this list.

  • default – If passed, a default value that the user should be able to easily reset this field to. This is preferrable if there is a single obvious choice for this field.

Returns:

This object itself, to allow method-chaining.

Raises:
  • DuplicateConfig – if a config parameter with this dest value has already been declared.

  • TypeError – if both range and choices are passed.

extend(other: Config) Config

Add all fields of another config to this one.

Parameters:

other – Another Config object from which to copy each field in sequence.

Returns:

This config object itself to allow method-chaining.

Raises:

DuplicateConfig – if other contains a config parameter with the same dest value as a parameter in this config.

Example

>>> first = Config().add("first", 0.0)
>>> second = Config().add("second", 0.0)
>>> first.extend(second)
<Config: ['first', 'second']>
>>> first.extend(second)
Traceback (most recent call last):
...
DuplicateConfig: {'second'}
validate(name: str, text_repr: str) Any

Validate a user-chosen value.

Parameters:
  • name – The name of the configurable field. This is the same as the dest parameter of add().

  • text_repr – The string input to validate.

Returns:

The validated and converted value.

Raises:

BadConfig – if the value is invalid. If the field’s type raised an exception, it is attached as the __cause__ of this exception.

validate_all(values: Mapping[str, str]) ConfigValues

Validate user-chosen set of configurations.

Parameters:

values – A mapping from dest names to unparsed string values. This must have exactly one item for every configurable field. Neither missing nor excess items are allowed.

Returns:

A namespace with the validated and converted values as attributes.

Raises:

BadConfig – if one of the values is invalid, if there are too many items or if an item is missing. If a field’s type raised an exception, it is attached as the __cause__ of this exception.

class cernml.coi.Config.Field(
dest: str,
value: T,
label: str,
help: str | None,
type: Callable[[str], T],
range: tuple[T, T] | None,
choices: tuple[T, ...] | None,
default: T | None,
)

Bases: Generic[T]

A single configurable field.

This class is created exclusively via Config.add(). It is a dataclass, so constructor parameters are also available as public fields.

>>> config = Config().add("foo", 1.0)
>>> fields = {field.dest: field for field in config.fields()}
>>> fields["foo"].value
1.0
>>> fields["foo"].value = 2.0
Traceback (most recent call last):
...
dataclasses.FrozenInstanceError: cannot assign to field 'value'
validate(text_repr: str) T

Validate a user-chosen value.

Parameters:

text_repr – The string input to validate.

Returns:

The validated and converted value.

Raises:

BadConfig – if the value is invalid. If the field’s type raised an exception, it is attached as the __cause__ of this exception.

cernml.coi.ConfigValues

alias of SimpleNamespace

exception cernml.coi.BadConfig

Bases: Exception

A configuration value failed the validation.

exception cernml.coi.DuplicateConfig

Bases: Exception

Attempted to add two configs with the same name.

Advanced Configurable Features

cernml.coi.configurable.deduce_type(value: Any, /) Callable[[str], Any]
cernml.coi.configurable.deduce_type(value: bool, /) StrSafeBool[bool]
cernml.coi.configurable.deduce_type(value: bool, /) StrSafeBool[bool]

For a given Field value, deduce the correct type.

If you don’t pass an explicit type to Config.add(), this function determines it based on the value. In almost all cases, this simply returns type(value). However, in the case of bool and numpy.bool, a special wrapper StrSafeBool is returned. This wrapper ensures that deduce_type(bool)(str(bool)) round-trips correctly:

>>> sbool = deduce_type(np.bool_(True))
>>> sbool
<coi.configurable.StrSafeBool(<class 'numpy.bool...'>)>
>>> sbool(str(True)) is np.True_
True
>>> sbool(str(False)) is np.False_
True

The naive choice would produce the wrong result for False because bool("False") actually tests whether the string "False" is empty:

>>> bool(str(True))
True
>>> bool(str(False))
True

This function uses singledispatch, so you can add your own special-cases:

>>> class Point:
...     def __init__(self, x, y):
...         self.x, self.y = x, y
...     def __repr__(self):
...         return f"({self.x}; {self.y})"
...     @classmethod
...     def fromstr(cls, s):
...         if (s[0], s[-1]) != ("(", ")"):
...             raise ValueError(f"not a point: {s!r}")
...         s = s[1:-1]
...         x, y = (float(c.strip()) for c in s.split(";"))
...         return cls(x, y)
...
>>> @deduce_type.register(Point)
... def _(p):
...     return type(p).fromstr
...
>>> p = Point(1.0, 2.0)
>>> deduce_type(p)(str(p))
(1.0; 2.0)
class cernml.coi.configurable.StrSafeBool(base_type: type[AnyBool])

Bases: Generic[AnyBool]

String-safe wrapper around Boolean types.

Integers round-trip through string conversion:

>>> int(str(123))
123

Booleans, however, do not:

>>> bool(str(True))
True
>>> bool(str(False))
True

This wrapper special-cases the strings "True" and "False" and replaces them with the built-in Boolean values True and False:

>>> sbool = StrSafeBool(bool)
>>> sbool
<...StrSafeBool(<class 'bool'>)>
>>> sbool(str(True))
True
>>> sbool(str(False))
False

This special case is case-sensitive:

>>> sbool("False")
False
>>> sbool("false")
True

You usually don’t interact with this wrapper directly. It is used internally by Config.Field as a default value for the type argument in case the value is a Boolean.

__call__(text_repr: str, /) AnyBool

Coerce the given string to the chosen boolean type.

The exact strings "True" and "False" are converted to the True and False value of the chosen boolean type. This is case-sensitive. Any other string is passed directly to the boolean constructor.

>>> b = StrSafeBool(repr)
>>> b('1')
"'1'"
>>> b('yes')
"'yes'"
>>> b('true')
"'true'"
>>> b('True')
'True'
cernml.coi.configurable.AnyBool: TypeVar

The generic type variable of StrSafeBool. Must be either bool or numpy.bool_.