"""The ParamDim classes define parameter dimensions along which discrete values
can be assumed. While they provide iteration abilities on their own, they make
sense mostly to use as objects in a dict that is converted to a ParamSpace.
"""
import abc
import copy
import logging
from typing import Any, Iterable, List, Optional, Sequence, Tuple, Union
import numpy as np
log = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Small helper classes
[docs]class Masked:
"""To indicate a masked value in a ParamDim"""
[docs] def __init__(self, value):
"""Initialize a Masked object that is a placeholder for the given value
Args:
value: The value to mask
"""
self._val = value
@property
def value(self):
return self._val
def __str__(self) -> str:
return f"<{self.value}>"
def __repr__(self) -> str:
return f"<Masked object, value: {repr(self.value)}>"
def __eq__(self, other) -> bool:
return self.value == other
# YAML representation .....................................................
[docs] @classmethod
def to_yaml(cls, representer, node: "Masked"):
"""
Args:
representer (ruamel.yaml.representer): The representer module
node (Masked): The node, i.e. an instance of this class
Returns:
the scalar value that this object masks, without tag
"""
return representer.represent_data(node._val)
[docs]class MaskedValueError(ValueError):
"""Raised when trying to set the state of a ParamDim to a masked value"""
pass
# -----------------------------------------------------------------------------
[docs]class ParamDimBase(metaclass=abc.ABCMeta):
"""The ParamDim base class."""
# Which __dict__ content to omit when checking for equivalence
_OMIT_ATTR_IN_EQ = tuple()
# Which additional attributes to use in __repr__
_REPR_ATTRS = tuple()
# Keyword parameters used to set the values
_VKWARGS = (
"values",
"range",
"linspace",
"logspace",
)
# .........................................................................
[docs] def __init__(
self,
*,
default,
values: Iterable = None,
order: Optional[Union[int, float]] = 0,
name: str = None,
as_type: str = None,
assert_unique: bool = True,
**kwargs,
) -> None:
"""Initialise a parameter dimension object.
Args:
default: default value of this parameter dimension
values (Iterable, optional): Which discrete values this parameter
dimension can take. This argument takes precedence over any
constructors given in the kwargs (like range, linspace, …).
order (float, optional): If given, this allows to specify an order
within a ParamSpace that includes this ParamDim object.
Dimensions with lowest ``order`` will then be iterated over
more frequently. Default is 0.
name (str, optional): If given, this is an *additional* name of
this ParamDim object, and can be used by the ParamSpace to
access this object.
as_type (str, optional): If given, casts the individual created
values to a certain python type. The following string values
are possible: str, int, bool, float
assert_unique (bool, optional): Whether to assert uniqueness of
the values among them.
**kwargs: Constructors for the ``values`` argument, valid keys are
``range``, ``linspace``, and ``logspace``; corresponding
values are expected to be iterables and are passed to
``range(*args)``, ``np.linspace(*args)``, or
``np.logspace(*args)``, respectively.
See also: :py:func:`numpy.linspace`, :py:func:`numpy.logspace`.
Raises:
TypeError: For invalid arguments
"""
# Initialize attributes that are managed by properties or methods
self._state = 0 # corresponding to the default state
# Set attributes that need no further checks
self._name = name
# To allow for <2.6 `order` values, check against None.
if order is None:
order = 0
self._order = order
# Package values into kwargs, for easier handling
if values is not None:
kwargs["values"] = values
# Gather the initialization kwargs for use with yaml representer
init_kwargs = dict(
default=default,
order=order,
name=name,
as_type=as_type,
assert_unique=assert_unique,
**kwargs,
)
# TODO Make this more elegant!
# As the base class __init__ is used by derived classes that might
# already have set private members, check if the `default` argument
# should even be used...
if not hasattr(self, "_default"):
self._default = self._parse_value(default, as_type=as_type)
else:
# Was already set; don't store the passed value
del init_kwargs["default"]
# Same for value-setting arguments
if not hasattr(self, "_vals"):
# Now let the helper function take care of the rest
self._init_vals(
as_type=as_type, assert_unique=assert_unique, **kwargs
)
# Store the initialization kwargs
self._init_kwargs = init_kwargs
# Derived classes should add the necessary kwargs in their __init__s
# Done.
[docs] def _init_vals(self, *, as_type: str, assert_unique: bool, **kwargs):
"""Parses the arguments and invokes ``_set_vals``"""
# Now check for unexpected ones and set the valid ones
if any([k not in self._VKWARGS for k in kwargs.keys()]):
raise TypeError(
"Received invalid keyword argument(s) for {}: {}. "
"Allowed arguments: {}"
"".format(
self.__class__.__name__,
", ".join([k for k in kwargs if k not in self._VKWARGS]),
", ".join(self._VKWARGS),
)
)
elif len(kwargs) > 1:
_vkws = ", ".join(self._VKWARGS)
raise TypeError(
f"Received too many keyword arguments. Need _one_ of: {_vkws}"
)
elif "values" in kwargs:
self._set_values(
kwargs["values"], assert_unique=assert_unique, as_type=as_type
)
elif "range" in kwargs:
self._set_values(
range(*kwargs["range"]),
assert_unique=assert_unique,
as_type=as_type,
)
elif "linspace" in kwargs:
self._set_values(
np.linspace(*kwargs["linspace"]),
assert_unique=assert_unique,
as_type="float" if as_type is None else as_type,
)
elif "logspace" in kwargs:
self._set_values(
np.logspace(*kwargs["logspace"]),
assert_unique=assert_unique,
as_type="float" if as_type is None else as_type,
)
else:
_vkws = ", ".join(self._VKWARGS)
raise TypeError(
f"Missing one of the following required keyword arguments to "
f"set the values of {self.__class__.__name__}: {_vkws}."
)
# Properties ..............................................................
@property
def name(self):
"""The name value."""
return self._name
@property
def order(self):
"""The order value."""
return self._order
@property
def default(self) -> Union[Any, Masked]:
"""The default value, which may be masked."""
return self._default
@property
def values(self) -> tuple:
"""The values that are iterated over.
Returns:
tuple: the values this parameter dimension can take. If None, the
values are not yet set.
"""
return self._vals
@property
def coords(self) -> tuple:
"""Returns the coordinates of this parameter dimension, i.e., the
combined default value and the sequence of iteration values.
Returns:
tuple: coordinates associated with the indices of this dimension
"""
return (self.default,) + self._vals
@property
def pure_coords(self) -> tuple:
"""Returns the pure coordinates of this parameter dimension, i.e., the
combined default value and the sequence of iteration values, but with
masked values resolved.
Returns:
tuple: coordinates associated with the indices of this dimension
"""
return tuple(
c if not isinstance(c, Masked) else c.value for c in self.coords
)
@property
def num_values(self) -> int:
"""The number of values available.
Returns:
int: The number of available values
"""
return len(self.values)
@property
def num_states(self) -> int:
"""The number of possible states, i.e., including the default state
Returns:
int: The number of possible states
"""
return self.num_values + 1
@property
def state(self) -> int:
"""The current iterator state
Returns:
Union[int, None]: The state of the iterator; if it is None, the
ParamDim is not inside an iteration.
"""
return self._state
@property
def current_value(self):
"""If in an iteration, returns the value according to the current
state. Otherwise, returns the default value.
"""
if self.state == 0:
return self.default
return self.values[self.state - 1]
# Magic methods ...........................................................
[docs] def __eq__(self, other) -> bool:
"""Check for equality between self and other
Args:
other: the object to compare to
Returns:
bool: Whether the two objects are equivalent
"""
if not isinstance(other, type(self)):
return False
# Check equality of the objects' __dict__s, leaving out _mask_cache
return all(
[
self.__dict__[k] == other.__dict__[k]
for k in self.__dict__.keys()
if k not in ("_init_kwargs",) + self._OMIT_ATTR_IN_EQ
]
)
[docs] @abc.abstractmethod
def __len__(self) -> int:
"""Returns the effective length of the parameter dimension, i.e. the
number of values that will be iterated over
Returns:
int: The number of values to be iterated over
"""
[docs] def __str__(self) -> str:
"""
Returns:
str: Returns the string representation of the ParamDimBase-derived
object
"""
return repr(self)
[docs] def __repr__(self) -> str:
"""
Returns:
str: Returns the string representation of the ParamDimBase-derived
object
"""
return (
f"<paramspace.paramdim.{self.__class__.__name__} object at "
f"{id(self)} with {repr(self._parse_repr_attrs())}>"
)
[docs] def _parse_repr_attrs(self) -> dict:
"""For the __repr__ method, collects some attributes into a dict"""
d = dict(
default=self.default,
order=self.order,
values=self.values,
name=self.name,
)
for attr_name in self._REPR_ATTRS:
d[attr_name] = getattr(self, attr_name)
return d
# Iterator functionality ..................................................
[docs] def __iter__(self):
"""Iterate over available values"""
return self
[docs] def __next__(self):
"""Move to the next valid state and return the corresponding parameter
value.
Returns:
The current value (inside an iteration)
"""
self.iterate_state()
return self.current_value
# Public API ..............................................................
# These are needed by the ParamSpace class to have more control over the
# iteration.
[docs] @abc.abstractmethod
def enter_iteration(self) -> None:
"""Sets the state to the first possible one, symbolising that an
iteration has started.
Returns:
None
Raises:
StopIteration: If no iteration is possible
"""
[docs] @abc.abstractmethod
def iterate_state(self) -> None:
"""Iterates the state of the parameter dimension.
Returns:
None
Raises:
StopIteration: Upon end of iteration
"""
[docs] @abc.abstractmethod
def reset(self) -> None:
"""Called after the end of an iteration and should reset the object to
a state where it is possible to start another iteration over it.
Returns:
None
"""
# Non-public API ..........................................................
[docs] def _parse_value(self, val, *, as_type: str = None):
"""Parses a single value and ensures it is of correct type."""
# Map of available type conversions
type_convs = dict(
str=str,
int=int,
float=float,
bool=bool,
tuple=self._rec_tuple_conv,
)
# Apply type conversion
if as_type is not None:
val = type_convs[as_type](val)
return val
[docs] def _set_values(
self, values: Iterable, *, assert_unique: bool, as_type: str = None
):
"""This function sets the values attribute; it is needed for the
values setter function that is overwritten when changing the property
in a derived class.
Args:
values (Iterable): The iterable to set the values with
assert_unique (bool): Whether to assert uniqueness of the values
as_type (str, optional): The following values are possible:
str, int, bool, float. If not given, will leave the values
as they are.
Raises:
AttributeError: If the attribute is already set
ValueError: If the iterator is invalid
Deleted Parameters:
as_float (bool, optional): If given, makes sure that values are
of type float; this is needed for the numpy initializers
"""
# Check the values
if hasattr(self, "_vals"):
# Was already set
raise AttributeError("Values already set; cannot be set again!")
elif len(values) < 1:
raise ValueError(
f"{self.__class__.__name__} values need be a container of "
f"length >= 1, was {values}"
)
# Parse each individual value, changing type if configured to do so
values = [self._parse_value(v, as_type=as_type) for v in values]
# Convert it to a tuple
values = tuple(values)
# Assert that values are unique
if assert_unique and any([values.count(v) > 1 for v in values]):
raise ValueError(
f"Values need to be unique, but there were "
f"duplicates: {values}"
)
# Now store it as attribute
self._vals = values
[docs] def _rec_tuple_conv(self, obj: list):
"""Recursively converts a list-like object into a tuple, replacing
all occurences of lists with tuples.
"""
# Recursive branch
if isinstance(obj, list):
return tuple(
o if not isinstance(o, list) else self._rec_tuple_conv(o)
for o in obj
)
# End of recursion
return obj
# YAML representation .....................................................
# NOTE The `yaml_tag` class variable needs be set in the derived classes
# Define some settings needed for saving to yaml
# Which entries to update and with which attribute
_YAML_UPDATE = dict()
# Which entries to remove if they have a certain value
_YAML_REMOVE_IF = dict(name=(None,))
[docs] @classmethod
def to_yaml(cls, representer, node):
"""
Args:
representer (ruamel.yaml.representer): The representer module
node (type(self)): The node, i.e. an instance of this class
Returns:
a yaml mapping that is able to recreate this object
"""
# Get the init_kwargs and use them as basis for the mapping
d = copy.deepcopy(node._init_kwargs)
# Depending on the class variables, update some entries
for k, attr_name in cls._YAML_UPDATE.items():
# Resolve the attribute and, if possible, call it
attr = getattr(node, attr_name)
new_val = attr if not callable(attr) else attr()
d[k] = new_val
# ... and remove some if they match the given value
d = {
k: v
for k, v in d.items()
if k not in cls._YAML_REMOVE_IF or v not in cls._YAML_REMOVE_IF[k]
}
# Can now call the representer
return representer.represent_mapping(cls.yaml_tag, d)
[docs] @classmethod
def from_yaml(cls, loader, node):
"""The default loader for ParamDim-derived objects"""
from .yaml_constructors import _pdim_constructor
return _pdim_constructor(loader, node, Cls=cls)
# -----------------------------------------------------------------------------
[docs]class ParamDim(ParamDimBase):
"""The ParamDim class."""
# Which __dict__ content to omit when checking for equivalence
_OMIT_ATTR_IN_EQ = (
"_mask_cache",
"_inside_iter",
"_target_of",
)
# Define the additional attribute names that are to be added to __repr__
_REPR_ATTRS = ("mask",)
# The YAML tag to use for representation
yaml_tag = "!sweep"
# And the other yaml representer settings
_YAML_UPDATE = dict(
mask="mask",
)
_YAML_REMOVE_IF = dict(
name=(None,),
mask=(None, False),
)
# .........................................................................
[docs] def __init__(self, *, mask: Union[bool, Tuple[bool]] = False, **kwargs):
"""Initialize a regular parameter dimension.
Args:
mask (Union[bool, Tuple[bool]], optional): Which values of the
dimension to mask, i.e., skip in iteration. Note that masked
values still count to the length of the parameter dimension!
**kwargs: Passed to :py:meth:`ParamDimBase.__init__`.
Possible arguments:
- ``default``: default value of this parameter dimension
- ``values`` (Iterable, optional): Which discrete values this
parameter dimension can take. This argument takes
precedence over any constructors given in the kwargs
(like range, linspace, …).
- ``order`` (float, optional): If given, this allows to
specify an order within a ParamSpace that includes this
ParamDim. If not given, 0 will be used.
See :py:meth:`~paramspace.paramspace.ParamSpace.iterator`
for more information on iteration order.
- ``name`` (str, optional): If given, this is an *additional*
name of this ParamDim object, and can be used by the
:py:class:`~paramspace.paramspace.ParamSpace` to access
this object.
- ``**kwargs``: Constructors for the ``values`` argument, valid
keys are ``range``, ``linspace``, and ``logspace``;
corresponding values are expected to be iterables and are
passed to ``range(*args)``, ``np.linspace(*args)``, or
``np.logspace(*args)``, respectively.
"""
super().__init__(**kwargs)
# Additional attributes, needed for coupling, masking, iteration
self._target_of = []
self._inside_iter = False
self._mask_cache = None
self.mask = mask
# Add further initialization kwargs, needed for yaml
self._init_kwargs["mask"] = mask
log.debug("ParamDim initialised.")
# Additional properties ...................................................
@property
def target_of(self):
"""Returns the list that holds all the CoupledParamDim objects that
point to this instance of ParamDim.
"""
return self._target_of
@property
def state(self) -> int:
"""The current iterator state
Returns:
Union[int, None]: The state of the iterator; if it is None, the
ParamDim is not inside an iteration.
"""
return super().state
@state.setter
def state(self, new_state: int):
"""Sets the current iterator state."""
# Perform type and value checks
if not isinstance(new_state, int):
raise TypeError(
f"New state can only be of type int, was {type(new_state)}!"
)
elif new_state < 0:
raise ValueError(f"New state needs to be >= 0, was {new_state}.")
elif new_state > self.num_values:
raise ValueError(
f"New state needs to be <= {self.num_values}, was {new_state}."
)
elif new_state > 0 and self.mask_tuple[new_state - 1] is True:
raise MaskedValueError(
f"Value at index {new_state} is masked: "
f"{self.values[new_state - 1]}. Cannot set the state to this "
"index!"
)
# Everything ok. Can set the state
self._state = new_state
@property
def mask_tuple(self) -> Tuple[bool]:
"""Returns a tuple representation of the current mask"""
if self._mask_cache is None:
self._mask_cache = tuple(
isinstance(v, Masked) for v in self.values
)
return self._mask_cache
@property
def mask(self) -> Union[bool, Tuple[bool]]:
"""Returns False if no value is masked or a tuple of booleans that
represents the mask
"""
m = self.mask_tuple # uses a cached value, if available
if not any(m): # no entry masked
return False
elif all(m): # all entries masked
return True
# leave it as a tuple
return m
@mask.setter
def mask(self, mask: Union[bool, Tuple[bool]]):
"""Sets the mask
Args:
mask (Union[bool, Tuple[bool]]): A bool or an iterable of booleans
Raises:
ValueError: If the length of the iterable does not match that of
this parameter dimension
"""
# Helper function for setting a mask value
def set_val(mask: bool, val):
if mask and not isinstance(val, Masked):
# Should be masked but is not
return Masked(val)
elif isinstance(val, Masked) and not mask:
# Is masked but shouldn't be
return val.value
# Already the desired status
return val
# Resolve boolean values
if isinstance(mask, bool):
mask = [mask] * self.num_values
elif isinstance(mask, slice):
# Apply the slice to a list of indices in order to know which ones
# to set to True in the mask
idcs = list(range(self.num_values))[mask]
mask = [(i in idcs) for i in range(self.num_values)]
# Should be a container now. Assert correct length.
if len(mask) != self.num_values:
raise ValueError(
f"Given mask needs to be a boolean, a slice, or a container "
f"of same length as the values container ({self.num_values}), "
f"was: {mask}"
)
# Mark the mask cache as invalid, such that it is re-calculated when
# the mask getter is accessed the next time
self._mask_cache = None
# Now build a new values container and store as attribute
self._vals = tuple(set_val(m, v) for m, v in zip(mask, self.values))
# Mask the default value, if all other values are masked
self._default = set_val(not all(mask), self.default)
@property
def num_masked(self) -> int:
"""Returns the number of unmasked values"""
return sum(self.mask_tuple)
# Magic Methods ...........................................................
[docs] def __len__(self) -> int:
"""Returns the effective length of the parameter dimension, i.e. the
number of values that will be iterated over.
Returns:
int: The number of values to be iterated over
"""
if self.mask is True:
# Will only return the default value, thus effective length is 1
return 1
return len(self._vals) - self.num_masked
# Public API ..............................................................
[docs] def enter_iteration(self) -> None:
"""Sets the state to the first possible one, symbolising that an
iteration has started.
Raises:
StopIteration: If no iteration is possible because all values are
masked.
"""
# Need to distinguish mask states
if self.mask is False:
# Trivial case, start with 1, the first iteration value state
self.state = 1
elif self.mask is True:
# There is only the default state to go to; go there, but then
# communicate that the iteration is over
self.state = 0
else:
# Find the first unmasked state
self.state = self.mask.index(False) + 1 # +1 accounts for default
# Set the flag to signify that inside iteration
self._inside_iter = True
[docs] def iterate_state(self) -> None:
"""Iterates the state of the parameter dimension.
Raises:
StopIteration: Upon end of iteration
"""
# Set to zero or increment, depending on whether inside or outside of
# an iteration
if not self._inside_iter:
self.enter_iteration()
return
# Else: within iteration
# Look for further possible states in the remainder of the mask tuple
sub_mask = self.mask_tuple[self.state :]
if False in sub_mask:
# There is another possible state, find it via index
self.state += sub_mask.index(False) + 1
else:
# No more possible state values
# Reset the state, allowing to reuse the object (unlike with
# other Python iterators). Then communicate: iteration should stop.
self.reset()
raise StopIteration
[docs] def reset(self) -> None:
"""Called after the end of an iteration and should reset the object to
a state where it is possible to start another iteration over it.
Returns:
None
"""
self.state = 0 # the state corresponding to the default value
self._inside_iter = False
# -----------------------------------------------------------------------------
[docs]class CoupledParamDim(ParamDimBase):
"""A CoupledParamDim object is recognized by the ParamSpace and its state
moves alongside with another ParamDim's state.
"""
# Which __dict__ content to omit when checking for equivalence
_OMIT_ATTR_IN_EQ = ()
# Define the additional attribute names that are to be added to __repr__
_REPR_ATTRS = (
"target_pdim",
"target_name",
"_use_coupled_default",
"_use_coupled_values",
)
# The YAML tag to use for representation
yaml_tag = "!coupled-sweep"
# And the other yaml representer settings
_YAML_UPDATE = dict(
target_name="_target_name_as_list",
)
_YAML_REMOVE_IF = dict(
name=(None,),
order=(None,),
assert_unique=(True, False),
default=(None,),
values=(None, [None]),
target_name=(None,),
target_pdim=(None,),
)
# .........................................................................
[docs] def __init__(
self,
*,
default=None,
target_pdim: ParamDim = None,
target_name: Union[str, Sequence[str]] = None,
**kwargs,
):
"""Initialize a coupled parameter dimension.
If the `default` or any values-setting argument is set, those will be
used. If that is not the case, the respective parts from the coupled
dimension will be used.
Args:
default (None, optional): The default value. If not given, will
use the one from the coupled object.
target_pdim (ParamDim, optional): The ParamDim object to couple to
target_name (Union[str, Sequence[str]], optional): The *name* of
the ParamDim object to couple to; needs to be within the same
ParamSpace and the ParamSpace needs to be able to resolve it
using this name.
**kwargs: Passed to ParamDimBase.__init__
Raises:
TypeError: If neither target_pdim nor target_name were given or
or both were given
"""
# TODO Make this __init__ more elegant!
# Disallow mask argument
if "mask" in kwargs:
raise TypeError(
"Received invalid keyword-argument `mask` for "
"CoupledParamDim!"
)
# Set attributes
self._target_pdim = None # the object that is coupled to
self._target_name = None # the name of it in a ParamSpace
# Determine whether coupled values or given values are to be used
self._use_coupled_default = default is None
self._use_coupled_values = not any(
[k in self._VKWARGS for k in kwargs]
)
# In order to not invoke the default- and value-setters in the parent
# classes' initializer, set them here already. Setting the attributes
# (regardless of value) already makes the base class __init__ jump that
# part of the initialization
if self._use_coupled_default:
self._default = None # Value does not matter, is never used
if self._use_coupled_values:
self._vals = [None] # Value does not matter, is never used
# Initialise via parent
super().__init__(default=default, assert_unique=False, **kwargs)
# Check and set the target-related attributes
if target_pdim is not None and target_name is not None:
raise TypeError(
"Got both `target_pdim` and `target_name` "
"arguments, but only accepting one of them at the "
"same time!"
)
elif target_name:
# Save only the name of object to couple to. Resolved by ParamSpace
self.target_name = target_name
elif target_pdim:
# Directly save the object to couple to
self.target_pdim = target_pdim
else:
raise TypeError(
"Expected either argument `target_pdim` or "
"`target_name`, got neither."
)
# Add further initialization kwargs, needed for yaml representation
self._init_kwargs["target_name"] = target_name
self._init_kwargs["target_pdim"] = target_pdim
# NOTE The other kwargs are already stored in the base class __init__
# Done now.
log.debug("CoupledParamDim initialised.")
# Magic methods ...........................................................
[docs] def __len__(self) -> int:
"""Returns the effective length of the parameter dimension, i.e. the
number of values that will be iterated over; corresponds to that of
the target ParamDim
Returns:
int: The number of values to be iterated over
"""
return len(self.target_pdim)
# Public API ..............................................................
# These are needed by the ParamSpace class to have more control over the
# iteration. Here, the parent class' behaviour is overwritten as the
# CoupledParamDim's state and iteration should depend completely on that of
# the target ParamDim...
# TODO these should allow standalone iteration as well!
[docs] def enter_iteration(self) -> None:
"""Does nothing, as state has no effect for CoupledParamDim"""
[docs] def iterate_state(self) -> None:
"""Does nothing, as state has no effect for CoupledParamDim"""
[docs] def reset(self) -> None:
"""Does nothing, as state has no effect for CoupledParamDim"""
# Properties that only the CoupledParamDim has ............................
@property
def target_name(self) -> Union[str, Sequence[str]]:
"""The ParamDim object this CoupledParamDim couples to."""
return self._target_name
@target_name.setter
def target_name(self, target_name: Union[str, Sequence[str]]):
"""Sets the target name, ensuring it to be a valid string or key
sequence.
"""
# Check if it is even reasonable to set it
if self._target_name is not None:
raise ValueError("Target name cannot be changed!")
elif self._target_pdim is not None:
raise ValueError(
"Target name cannot be changed after the target "
"object is already set!"
)
# Make sure it is of valid type
elif not isinstance(target_name, (tuple, list, str)):
raise TypeError(
f"Argument `target_name` should be a tuple or list (i.e., a "
f"key sequence) or a string! "
f"Was {type(target_name)}: {target_name}"
)
elif isinstance(target_name, list):
target_name = tuple(target_name)
self._target_name = target_name
@property
def _target_name_as_list(self) -> Union[str, List[str]]:
"""For the safe yaml representer, the target_name cannot be a tuple.
This property returns it as str or list of strings.
"""
if self.target_name is None or isinstance(self.target_name, str):
return self.target_name
return list(self.target_name)
@property
def target_pdim(self) -> ParamDim:
"""The ParamDim object this CoupledParamDim couples to."""
if self._target_pdim is None:
raise ValueError(
"The coupling target has not been set! Either "
"set the `target_pdim` to a ParamDim object or "
"incorporate this CoupledParamDim into a "
"ParamSpace to resolve its coupling target using "
"the given `target_name` attribute."
)
return self._target_pdim
@target_pdim.setter
def target_pdim(self, pdim: ParamDim):
"""Set the target ParamDim"""
if not isinstance(pdim, ParamDim):
raise TypeError(
f"Target of CoupledParamDim needs to be of type "
f"ParamDim, was {type(pdim)}!"
)
elif (
not self._use_coupled_values and self.num_values != pdim.num_values
):
raise ValueError(
f"The lengths of the value sequences of target ParamDim and "
f"this CoupledParamDim need to match, were: "
f"{pdim.num_values} and {self.num_values}, respectively."
)
self._target_pdim = pdim
log.debug("Set CoupledParamDim target.")
# Properties that need to relay to the coupled ParamDim ...................
@property
def default(self) -> Union[Any, Masked]:
"""The default value.
Returns:
the default value this parameter dimension can take.
Raises:
RuntimeError: If no ParamDim was associated yet
"""
if self._use_coupled_default:
return self.target_pdim.default
return self._default
@property
def values(self) -> tuple:
"""The values that are iterated over.
If self._use_coupled_values is set, will be those of the coupled pdim.
Returns:
tuple: The values of this CoupledParamDim or the target ParamDim
"""
if self._use_coupled_values:
return self.target_pdim.values
return self._vals
@property
def state(self) -> int:
"""The current iterator state of the target ParamDim
Returns:
Union[int, None]: The state of the iterator; if it is None, the
ParamDim is not inside an iteration.
"""
return self.target_pdim.state
@property
def current_value(self):
"""If in an iteration, returns the value according to the current
state. Otherwise, returns the default value.
"""
if self.state == 0:
return self.default
return self.values[self.state - 1]
@property
def mask(self) -> Union[bool, Tuple[bool]]:
"""Return the coupled object's mask value"""
return self.target_pdim.mask