Source code for finesse.parameter

cimport cython
import math
import numpy as np
cimport numpy as np
import textwrap
from enum import Enum
from copy import deepcopy
import logging
import operator
import weakref
import finesse

from collections import namedtuple
from finesse_numpydoc import ClassDoc
from .element import ModelElement
from .exceptions import (
    ComponentNotConnected, ParameterLocked, ModelParameterSelfReferenceError
)
from .env import warn
from .symbols import PYFUNCTION_MAP, Symbol, Resolving, Function
from .utilities import is_iterable
from .exceptions import FinesseException, ExternallyControlledException, NotChangeableDuringSimulation

LOGGER = logging.getLogger(__name__)


# Allowed datatypes for parameters
allowed_datatypes = (int, float, bool, Enum, np.int64, np.float64, np.bool_)


# struct for storing float_parameter information on each element
ParameterInfo = namedtuple(
    "ParameterInfo", (
        "name",
        "description",
        "dtype",
        "dtype_cast",
        "units",
        "is_geometric",
        "changeable_during_simulation"
    )
)

class ParameterRef(Symbol):
    """A symbolic instance of a parameter in the model.

    A parameter is owned by some model element, which can be used to connect its value
    to other aspects of the simulation.
    """

    def __init__(self, param):
        if param is None:
            raise ValueError("ParameterRef cannot reference None")
        self.__param = param
        self.__name = self.parameter.full_name
        self.__cyexpr_name = self.__name.replace(".", "_").lower().encode("UTF-8")
        self.__full_name = self.parameter.full_name

    @property
    def parameter(self):
        return self.__param

    @property
    def name(self):
        return self.__name

    @property
    def full_name(self):
        return self.__full_name

    @property
    def cyexpr_name(self):
        """Name of the parameter reference in ``cyexpr`` compatibility format.

        This is equivalent to :attr:`.ParameterRef.name` but with ``"."``
        replaced with ``"_"``, converted to lower case and encoded
        in UTF-8 format as a ``bytes`` object.

        The above format makes this compatible with passing to the underlying
        math evaluator engine (``tinyexpr``) used via the :mod:`.cyexpr` sub-module.

        .. note::

            This should, typically, never need to be used outside of internal usage. It
            exists primarily to act as the owner for the parameter name strings (avoiding
            dangling pointers in the expression code).

        :`getter`: Returns the ``cyexpr`` compatible name format of the pref name (read-only).
        """
        return self.__cyexpr_name

    @property
    def owner(self):
        return self.parameter.owner

    def __symeq__(self, obj):
        if isinstance(obj, Function):
            return obj == self
        elif isinstance(obj, ParameterRef):
            if obj.parameter is self.parameter:
                return True
            else:
                return False
        elif isinstance(obj, Parameter):
            if obj is self.parameter:
                return True
            else:
                return False
        else:
            # Otherwise it's not another parameter ref, I guess we could
            # also check for numeric equality?
            return False

    def __hash__(self):
        return hash(self.parameter)

    def __deepcopy__(self, memo):
        from finesse.model import Model

        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        result.__dict__.update(deepcopy(self.__dict__, memo))
        return result

    def eval(self, keep_changing_symbols=False, subs=None, keep=None, **kwargs):
        p = self.parameter

        # variables might always need to be kept as it's value is changing
        if keep_changing_symbols and p.is_changing:
            return self

        # Always just keep if requested to
        if keep is not None:
            if self.name == keep:
                return self
            elif is_iterable(keep):
                if self.name in keep or self in keep:
                    return self

        result = None
        if subs is not None and (self.name in subs or self in subs):
            # If we are subbing the value of this symbol
            result = self.substitute(subs)
        else:
            # else lets use it's current value
            result = p.value

        if hasattr(result, "eval"):
            if subs is not None:
                result = result.substitute(subs).eval()
            else:
                result = result.eval()

        return result


[docs]cdef class Parameter: # __doc__ can't be changed in a cdef, but we can override the property # so that when the `help` is used @property def __doc__(self): doc = ClassDoc(type(self.__owner())) bkp = "\n" for p in doc["Parameters"]: if p.name == self.name: return textwrap.dedent(f"""{p.name} : {p.type}{bkp}{bkp.join(p.desc)}""")
def __init__(self, parameter_info, owner): from finesse.element import ModelElement from finesse.model import Model self.__value = None self.__cvalue = 0.0 self.__units = parameter_info.units self.__name = parameter_info.name self.__datatype = parameter_info.dtype self.__datatype_cast = parameter_info.dtype_cast self.__description = parameter_info.description self.__owner = weakref.ref(owner) self.__owner_type = type(owner) self.__changeable_during_simulation = parameter_info.changeable_during_simulation self._locked = False self.__external_setters = {} self.__is_tunable = False self.__update_state() self.__eval_string = False self.is_geometric = False self.__resolving = False self.__disable_state_type_change = False if self.__owner_type != Model: if owner.name is None: raise ValueError("Owner name should not be None") if parameter_info.name is None: raise ValueError("Parameter name should not be None") self.__full_name = f"{owner.name}.{parameter_info.name}" self.__validator = ModelElement._validators[self.__owner_type][self.name] else: # Model parameters don't have a full name as they are global self.__full_name = parameter_info.name # TODO: (jwp) temporary solution to work around deepcopy complications with Cython # inheritance. This would ideally be defined in GeometricParameter which inherits # from Parameter. We should determine if a separate class for GeometricParameter # is necessary. self.is_nr = False cdef __cdeepcopy__(self, Parameter new, dict memo) : new.__units = self.__units new.__name = self.__name new.__eval_string = self.__eval_string new.__datatype = self.__datatype new.__datatype_cast = self.__datatype_cast new.__full_name = self.__full_name new.__description = self.__description new._locked = self._locked new.__external_setters = deepcopy(self.__external_setters, memo) new.__is_tunable = self.__is_tunable new.state = self.state new.__value = deepcopy(self.__value, memo) new.__cvalue = self.__cvalue new.__owner_type = self.__owner_type new.__validator = self.__validator new.__changeable_during_simulation = self.__changeable_during_simulation new.__disable_state_type_change = self.__disable_state_type_change # TODO: (jwp) temporary solution, see above. new.is_geometric = self.is_geometric new.is_nr = self.is_nr @property def eval_string(self): "Whether to cal 'eval' on the parameter value in the string representation" return self.__eval_string @eval_string.setter def eval_string(self, val): self.__eval_string = bool(val) def __deepcopy__(self, memo): new = Parameter.__new__(Parameter) memo[id(self)] = new self.__cdeepcopy__(new, memo) # Manually update the weakrefs to be correct id_component = id(self.owner) if id_component not in memo: # We need to update this reference later on # This will be called when the port property # is accessed. When this happens we'll peak back # at the memo once it has been filled and get # the new port reference. After this the refcount # for this function should goto zero and be garbage # collected def update_later(): new.__owner = weakref.ref(memo[id(self.owner)]) new.__owner = update_later # just in case something calls # this weakref in the meantime memo[id(self.__owner()._model)].after_deepcopy.append(update_later) else: new.__owner = weakref.ref(memo[id(self.owner)]) return new cdef __update_state(self) : prev_state = self.state if issubclass(type(self.__value), allowed_datatypes): self.state = ParameterState.Numeric elif self.__value is None: self.state = ParameterState.NONE elif isinstance(self.__value, Symbol): self.state = ParameterState.Symbolic elif callable(self.__value): self.state = ParameterState.Unresolved else: raise Exception(f"Unexpected parameter value '{self.__value}' ({type(self.__value)})") if self.__disable_state_type_change and prev_state != self.state: raise RuntimeError(f"Trying to chanage {repr(self)} from state {prev_state} to {self.state} which should not be happening.") @property def units(self): if self.__units is None: return "" return self.__units @property def name(self): return self.__name @property def datatype(self): """The underlying C datatype of this parameter.""" return self.__datatype def datatype_cast(self, value, ignore_failure=False): """Casts a value into the datatype of this parameter. If ignore_failure is True then if this value cannot be cast it is just returned. """ try: return self.__datatype_cast(value) except (ValueError, TypeError) as ex: if ignore_failure: return value else: raise ex @property def owner(self): """The component/element this parameter is associated with, this could be a :class:`finesse.element.ModelElement` or a :class:`finesse.model.Model`.""" return self.__owner() def _set_owner(self, owner): self.__owner = owner @property def locked(self): """If locked, this parameters value cannot be changed.""" return self._locked @property def is_default_for_owner(self): """Whether this parameter is the default for the owning model element.""" try: return self.owner.default_parameter_name == self.name except AttributeError: return False @property def changeable_during_simulation(self): """True if this parameter cannot be changed during a simulation.""" return self.__changeable_during_simulation @property def _model(self): """Returns the model that this owner is connected to. Raises an exception if it is not connected to anything to stop code accidently using unconnected owner by accident. Returns ------- :class:`.model.Model` Raises ------ :class:`.exceptions.ComponentNotConnected` """ if self.__owner() is None: raise ComponentNotConnected("Lost weak reference") elif isinstance(self.__owner(), finesse.model.Model): return self.__owner() else: return self.__owner()._model @property def full_name(self): return self.__full_name @property def description(self): return self.__description @property def ref(self): """Returns a reference to this parameter's value to be used in symbolic expressions.""" return ParameterRef(self) cdef bint _is_changing(self) noexcept: if self.state == ParameterState.Symbolic: return self.value.is_changing return self.__is_tunable @property def is_changing(self): """True if this parameter will be changing during a simulation.""" return self._is_changing() @property def is_tunable(self): """True if this parameter will be directly changed during a simulation.""" return self.__is_tunable @property def is_symbolic(self): """True if this parameter's value is symbolic.""" return self.state == ParameterState.Symbolic @is_tunable.setter def is_tunable(self, bint value): if not self.changeable_during_simulation: raise NotChangeableDuringSimulation(f"The parameter {self.full_name} cannot be changed during a simulation") if self._locked: raise ParameterLocked() if self.is_externally_controlled: raise ExternallyControlledException(f"{repr(self)} is being controlled by {tuple(self.__external_setters.keys())}, so it cannot be tuned directly.") if self.state == ParameterState.Symbolic: raise FinesseException( f"{repr(self)} depends on a symbolic value so it cannot be directly changed. Instead try changing one of its dependencies: {self.value.parameters()}" ) self.__is_tunable = value def resolve(self): """When this parameters value has some dependency whose value has not yet been set, like during parsing, its value will be a callable object, this method will call this function to return the value.""" self.__resolving = True if callable(self.value): self.value = self.value(self.owner._model) self.__resolving = False self.__update_state() def lambdify(self, *args): """Returns a lambda function that returns the value of this parameter. Parameters in a symbolic function can be kept as variables by passing the Parameter object as optional arguments. The returned lambda function will then have len(args) arguments - effectively subsituting values at call time. """ if self.state == ParameterState.Symbolic: refs = {**self.owner._model.elements, **PYFUNCTION_MAP} sym_str = str(self.value) ARGS = [] for i, arg in enumerate(args): ARGS.append(arg.full_name.replace(".", "__")) sym_str = sym_str.replace(arg.full_name, ARGS[-1]) return eval(f'lambda {",".join(ARGS)}: {sym_str}', refs) else: # if len(args) > 0: # raise Exception("Can't specify symbolic arguments to lambdify if this parameter isn't symbolic.") return lambda *x: self.value def eval(self, bint keep_changing_symbols=False): """Evaluates the value of this parameter. If the parameter is dependant on some symbolic statement this will evaluate that. If it is not the value itself is returned. This method should be used when filling in matrices for computing solutions of a model. """ if self.state == ParameterState.Unresolved: self.resolve() if self.state == ParameterState.Symbolic: y = self.value.eval(keep_changing_symbols=keep_changing_symbols) if self.__validator is None: return y else: return self.__validator(self.owner, y) else: return self.value cdef _get_value(self) : return self.__value @property def value(self): return self._get_value() def set_external_setter(self, element, symbol): """Sets an element as an external controller of the value of this parameter. It is used in cases where an element is physically imposing its value onto this one, such as degrees of freedom. Parameters ---------- element : ModelElement Element that will be controlling this parameter symbol : symbolic expression The expression that this element is imposing upon the parameter. """ if element in self.__external_setters: raise Exception(f"{repr(element)} is already an external setter of {repr(self)}") self.__external_setters[element] = symbol self._set_value((self.value + symbol).sympy_simplify()) def remove_external_setter(self, element): """Stops an element from being an external setter. Parameters ---------- element : ModelElement Element that is controlling this parameter """ if element not in self.__external_setters: raise Exception(f"{repr(element)} is not an external setter of {repr(self)}") # Use sympy to simplify the expression to remove the symbol self._set_value((self.value - self.__external_setters[element]).sympy_simplify()) del self.__external_setters[element] def _get_unset_value(self): """Returns what the value of this parameter without any external setters.""" if not self.is_externally_controlled: return self.__value else: v = self.__value for x in self.__external_setters.values(): v -= x return v.sympy_simplify() @property def is_externally_controlled(self): "Whether this parameter is being externally controlled by another element." return len(self.__external_setters) > 0 def _reset_cvalue(self): """Resets the C-level value, to be used in clean-up stage of an action.""" self.__cvalue = self.__datatype_cast(self.eval()) # cast to correct datatype cdef _set_value(self, value) : from finesse.element import ModelElement setting_fn = ModelElement._setters[type(self.owner)].get(self.name) cdef bint is_symbol = isinstance(value, Symbol) cdef bint is_callable = ( isinstance(value, Resolving) or getattr(value, "contains_unresolved_symbol", False) ) if setting_fn is not None and not self.__resolving and not is_symbol: setting_fn(self.owner) if self.locked: if self.state == ParameterState.Symbolic: raise ParameterLocked(f"Parameter {repr(self)}'s value is symbolic so it cannot be directly set") else: raise ParameterLocked(f"Parameter {repr(self)} is locked") if is_symbol: for p in value.parameters(): if p == self.ref: raise ModelParameterSelfReferenceError(value, self) if self.__is_tunable and self.state == ParameterState.Symbolic: warn( f"{repr(self)}: setting `is_tunable` to False as new value is " f"symbolic; is_tunable was previously True" ) self.__is_tunable = False if is_symbol or value is None or is_callable: # Don't cast here as need to keep original object self.__value = value elif self.__validator is None: self.__value = self.__datatype_cast(value) else: self.__value = self.__validator(self.owner, self.__datatype_cast(value)) # Correspondingly set the low-level __cvalue as long as value # is valid and not in an unresolved state if value is not None and not is_callable: try: self.__cvalue = self.__datatype_cast(self.__value) # Return of self.eval is allowed to be None, unfortunately, so # take care of that here (any other type error would invariably # show up earlier than this anyway) except TypeError: pass self.__update_state() @value.setter def value(self, value): #if self.is_externally_controlled: # raise ExternallyControlledException(f"Model parameter {repr(self)} is being externally controlled by {tuple(self.__external_setters.keys())}") self._set_value(value) cdef int set_double_value(self, double value) except -1: """UNSAFE, Only use when you know the parameter should be changing!""" # TODO (sjr) Not sure setting of self.__value here is reqd # anymore as just setting __cvalue should suffice # here now (as this method should only be called # during simulations anyway) if self.__validator is None: self.__value = value else: self.__value = self.__validator(self.owner, value) self.__cvalue = value return 0 def _set(self, value): self.value = value def __str__(self): units = self.units if units: units = " " + units if isinstance(self.value, (Parameter, ParameterRef)) and self.eval_string: value = self.value.eval() else: value = self.value return f"{value}{units}" def __repr__(self): if self.__owner() is None: return f"<LOST WEAKREF.{self.name}={str(self.value)}>" else: return f"<{self.full_name}={str(self.value)} @ {hex(id(self))}>" def __add__(A, B): return _operator(A, B, operator.add) def __radd__(self, other): return other + self.value def __sub__(A, B): return _operator(A, B, operator.sub) def __rsub__(self, other): return other - self.value def __mul__(A, B): return _operator(A, B, operator.mul) def __rmul__(self, other): return other * self.value def __truediv__(A, B): return _operator(A, B, operator.truediv) def __rtruediv__(self, other): return other / self.value def __floordiv__(A, B): return _operator(A, B, operator.floordiv) def __rfloordiv__(self, other): return other // self.value def __mod__(A, B): return _operator(A, B, operator.mod) def __rmod__(self, other): return other % self.value def __divmod__(A, B): return _operator(A, B, operator.divmod) def __rdivmod__(self, other): return divmod(other, self.value) def __pow__(self, other, order): return pow(self.value, other, order) def __rpow__(self, other, order): return pow(other, self.value, order) def __neg__(self): return -1 * self.value def __pos__(self): return self.value def __abs__(self): return abs(self.value) def __complex__(self): return complex(self.value) def __float__(self): return float(self.value) def __int__(self): return int(self.value) def __round__(self, ndigits=None): return round(self.value, ndigits) def __trunc__(self): return math.trunc(self.value) def __floor__(self): return math.floor(self.value) def __ceil__(self): return math.ceil(self.value) def __hash__(self): return id(self) def __eq__(A, B): return A.value == B or (A.value == B.value if hasattr(B, "value") else False) def __ge__(A, B): return A.value >= B or (A.value >= B.value if hasattr(B, "value") else False) def __gt__(A, B): return A.value > B or (A.value > B.value if hasattr(B, "value") else False) def __le__(A, B): return A.value <= B or (A.value <= B.value if hasattr(B, "value") else False) def __lt__(A, B): return A.value < B or (A.value < B.value if hasattr(B, "value") else False) def __bool__(self): return bool(self.value) def __array_ufunc__(self, ufunc, method, *args, **kwargs): if method == "__call__": ARGS = ( (_.value if isinstance(_, Parameter) else _) for _ in args ) return ufunc(*ARGS, **kwargs) return NotImplemented cdef class GeometricParameter(Parameter): """Specialised parameter class for variables which are dependencies of ABCD matrices. These include surface radii of curvature, lens focal lengths, beamsplitter angles of incidence, space lengths and space refractive indices. When setting the value of a GeometricParameter *outside of a simulation*, the dependent ABCD matrices get updated via the `update_abcd_matrices` C method. Inside a simulation, the ABCD matrix elements are updated in a much more efficient way via the connector workspaces. """ def __init__(self, parameter_info, owner): if not isinstance(owner, finesse.element.ModelElement): raise ValueError(f"owner of a geometric parmameter must be a ModelElement, not `{repr(owner)}`") super().__init__(parameter_info, owner) self.is_geometric = True self.is_nr = self.name == "nr" def __deepcopy__(self, memo): new = GeometricParameter.__new__(GeometricParameter) memo[id(self)] = new self.__cdeepcopy__(new, memo) # Manually update the weakrefs to be correct id_component = id(self.owner) if id_component not in memo: # We need to update this reference later on # This will be called when the port property # is accessed. When this happens we'll peak back # at the memo once it has been filled and get # the new port reference. After this the refcount # for this function should goto zero and be garbage # collected def update_later(): new._set_owner(weakref.ref(memo[id(self.owner)])) new._set_owner(update_later) # just in case something calls # this weakref in the meantime memo[id(self.owner._model)].after_deepcopy.append(update_later) else: new._set_owner(weakref.ref(memo[id(self.owner)])) return new @cython.boundscheck(False) @cython.wraparound(False) @cython.initializedcheck(False) cdef void update_abcd_matrices(self) noexcept: """Method for updating dependent ABCD matrices of the parameter *outside* of a simulation.""" cdef: dict abcd_matrices = self.owner._abcd_matrices list matrix_handles = list(abcd_matrices.values()) object full_symbolic # Full symbolic ABCD matrix double[:, ::1] M_num # Numeric ABCD matrix object[:, ::1] M_sym # Actual symbolic matrix to use if self.is_nr: # Parameter is a refractive index from finesse.components import Surface if self.owner.portA is not None: p1_comp = self.owner.portA.component else: p1_comp = None if self.owner.portB is not None: p2_comp = self.owner.portB.component else: p2_comp = None # and if either / both ports are associated with Surfaces # then append their ABCD matrices so that they get updated # too (as some of them depend on nr of attached space) if isinstance(p1_comp, Surface): matrix_handles.extend(list(p1_comp._abcd_matrices.values())) if isinstance(p2_comp, Surface): matrix_handles.extend(list(p2_comp._abcd_matrices.values())) cdef Py_ssize_t i, j, k cdef Py_ssize_t N = len(matrix_handles) for i in range(N): full_symbolic = matrix_handles[i][0] # Total reflection means no symbolic matrix is formed # so ignore it and move on if full_symbolic is None: continue M_sym = full_symbolic M_num = matrix_handles[i][1] for j in range(2): for k in range(2): M_num[j][k] = float(M_sym[j][k]) # NOTE (sjr) ABCD matrices are updated using cyexpr's in the relevant # workspace during a simulation, so we don't override # set_double_value here. The update_abcd_matrices method # is only called when using the value setter, which will be # used outside of a simulation context. @property def value(self): return Parameter._get_value(self) @value.setter def value(self, value): Parameter._set_value(self, value) self.update_abcd_matrices() class parameterproperty(property): """Descriptor class for declaring a simulation parameter. A simulation parameter is one that can be changed during a simulation and affect the resulting outputs. The idea is that output dependant variables should be marked as having been changed or will be changed during a simulation run. This allows us to then optimise parts of the model, as we can determine what will or will not be changing. This descriptor is paired with the :class:Parameter. Parameters can then be superficially locked once a model has been built so accidentally changing some parameter that isn't expected to change can flag a warning. """ def __init__(self, fget, fset, flocked, doc=None): super().__init__(fget, fset, doc=doc) self.__doc__ = doc self.fget.__doc__ = ":`getter`: " + doc self.fset.__doc__ = ":`setter`: " + doc self.flocked = flocked self.flocked.__doc__ = "Parameter locking function for " + doc def __get__(self, obj, objtype=None): if obj is None: return self else: return self.fget(obj) def __set__(self, obj, value): if self.flocked(obj): raise ParameterLocked(f"{obj} is locked during this simulation") else: self.fset(obj, value) def __delete__(self, obj): raise AttributeError("can't delete parameter") def getter(self, fget): return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): return type(self)(self.fget, fset, self.fdel, self.__doc__) def float_parameter( name, description, units=None, validate=None, setter=None, is_default=False, is_geometric=False, changeable_during_simulation=True ): """A parameter of an element whose value is decribed by a 64-bit floating point number.""" return model_parameter( name, description, float, float, units=units, validate=validate, setter=setter, is_default=is_default, is_geometric=is_geometric, changeable_during_simulation=changeable_during_simulation ) def int_parameter( name, description, units=None, validate=None, setter=None, is_default=False, is_geometric=False, changeable_during_simulation=True ): """A parameter of an element whose value is decribed by a 64-bit integer number.""" return model_parameter( name, description, int, int, units=units, validate=validate, setter=setter, is_default=is_default, is_geometric=is_geometric, changeable_during_simulation=changeable_during_simulation ) def enum_parameter( name, description, enum, units=None, validate=None, setter=None, is_default=False, is_geometric=False, changeable_during_simulation=True ): """A parameter of an element whose value is decribed by a Enum definition. Enum must only use integer values to describe its members. Unlike a general python Enum which can use strings. """ if not issubclass(enum, Enum): raise Exception("enum argument should be an enum.Enum type object") for _ in enum.__members__.values(): if type(_.value) is not int: raise Exception(f"Value for item {repr(_)} in {enum} must be an integer") # enums are irritating as they don't act like normal types. You need # to getitem to go from value to key. They also don't cast themselves # back to themselves as float(1.0) would. def enum_cast(value): if type(value) is enum: return value else: try: return enum[value] except KeyError as e: if validate: return value else: raise ValueError( f"'{value}' is not a valid {enum} enum, valid options: " f"{enum.__members__.keys()}") from e return model_parameter( name, description, int, enum_cast, units=units, validate=validate, setter=setter, is_default=is_default, is_geometric=is_geometric, changeable_during_simulation=changeable_during_simulation ) def bool_parameter( name, description, units=None, validate=None, setter=None, is_default=False, is_geometric=False, changeable_during_simulation=True ): """A parameter of an element whose value is decribed by a True or False value.""" return model_parameter( name, description, bool, bool, units=units, validate=validate, setter=setter, is_default=is_default, is_geometric=is_geometric, changeable_during_simulation=changeable_during_simulation ) cdef object model_parameter( str name, str description, type _type, object _type_cast, str units, str validate, str setter, bint is_default, bint is_geometric, bint changeable_during_simulation ) : """Decorator to register a model parameter with a double datatype field in the class. This shouldn't be called by users. Use the [type]_parameter functions above instead. """ if not issubclass(_type, allowed_datatypes): raise Exception(f"Data type {_type} not allowed ({allowed_datatypes})") if validate is None: vld = None else: vld = lambda x, v: getattr(x, validate)(v) # setter is the name of a method which will be the only method that can # be used to set the parameter (direct setting will be forbidden) if setter is None: st = None else: st = lambda x: getattr(x, setter) def func(cls): doc = description # use short description if no doc can be found try: # TODO : numpydoc needs to be changed to use something less heavy on requirements from numpydoc.docscrape import ClassDoc cdoc = ClassDoc(cls) bkp = "\n" for p in cdoc["Parameters"]: if p.name == name: doc = textwrap.dedent(f"""{p.name} : {p.type}{bkp}{bkp.join(p.desc)}""") except ImportError as ex: pass p = parameterproperty( lambda x: getattr(x, f"__param_{name}"), lambda x, v: getattr(x, f"__param_{name}")._set(v), lambda x: getattr(x, f"__param_{name}").locked, doc=doc ) cls._param_dict[cls].append( ParameterInfo( name, description, _type, _type_cast, units, is_geometric, changeable_during_simulation ) ) cls._validators[cls][name] = vld cls._setters[cls][name] = st if is_default: if cls in cls._default_parameter_name: raise ValueError( f"is_default cannot be set for more than one model parameter on {cls!r}" ) cls._default_parameter_name[cls] = name setattr(cls, name, p) return cls return func def info_parameter(name, description, units=None): """Decorator to register an info parameter field in the class. Info parameters are purely informative properties, and cannot be directly scanned using an axis. """ def func(cls): cls._info_param_dict[cls][name] = (description, units) return cls return func def deref(parameter): """Get the :class:`.Parameter` from a :class:`.ParameterRef` or :class:`.Parameter` (no-op). This is useful in actions which require a parameter but may be passed either a parameter or parameter reference. """ if isinstance(parameter, ParameterRef): parameter = parameter.parameter return parameter def _operator(A, B, op): """Used in operater dunder methods in Parameter class, for compatibility with cython 0.x, since it handles operator methods and the order of their arguments differently from cython3. See https://cython.readthedocs.io/en/latest/src/userguide/special_methods.html#arithmetic-methods for details """ if isinstance(A, Parameter): return op(A.value, B) else: return op(A, B.value)