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:
Define your configurable parameters in
__init__()
.Implement
get_config()
and return aConfig
object. This declares your configurable parameters and their invariants.Implement
apply_config()
, which receives aConfigValues
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()
andapply_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 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 ofConfig.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
- 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,
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,
-
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'
- cernml.coi.ConfigValues¶
alias of
SimpleNamespace
- 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 returnstype(value)
. However, in the case ofbool
andnumpy.bool
, a special wrapperStrSafeBool
is returned. This wrapper ensures thatdeduce_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
becausebool("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])¶
-
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 valuesTrue
andFalse
:>>> 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 eitherbool
ornumpy.bool_
.