import weakref
from copy import deepcopy
import logging
import warnings
from .exceptions import ModelParameterDefaultValueError, ContextualValueError
from .utilities.tables import Table
from .freeze import canFreeze
from collections import defaultdict, ChainMap
LOGGER = logging.getLogger(__name__)
[docs]@canFreeze
class ModelElement:
"""Base for any object which can be an element of a :class:`.Model`.
When added to a model it will attempt to call the method `_on_add` so that the element can do
some initialisation if required.
Parameters
----------
name : str
Name of newly created model element.
"""
# A global dictionary to keep a record of all the declared
# model parameters, validators, etc.
_param_dict = defaultdict(list)
_validators = defaultdict(dict)
_setters = defaultdict(dict)
_default_parameter_name = dict()
_unique_element = False
# Info parameters.
_info_param_dict = defaultdict(dict)
def __new__(cls, name, *args, **kwargs):
from finesse.parameter import (
Parameter,
GeometricParameter,
)
instance = super(ModelElement, cls).__new__(cls)
instance._unfreeze()
instance._params = []
instance._info_params = instance._info_param_dict[cls]
instance._ModelElement__name = name
instance._unique_element = bool(cls._unique_element)
instance._ModelElement__model = None
# Loop through each of the parameters that have been defined
# in the class and instantiate an object to represent them
# for this instance of the object
for pinfo in instance._param_dict[cls]:
if pinfo.is_geometric:
p = GeometricParameter(pinfo, instance)
else:
p = Parameter(pinfo, instance)
setattr(instance, f"__param_{pinfo.name}", p)
instance._params.append(p)
return instance
[docs] def __init__(self, name):
from finesse.utilities import check_name
self._params_changing = None
self._params_evald = None
self._legacy_script_line_number = 0
try:
self.__name = check_name(name)
except ValueError:
raise ContextualValueError(
{"name": name},
"can only contain alphanumeric and underscore characters",
)
self._add_to_model_namespace = True
self._namespace = (".",)
def __str__(self):
params = {param.name: str(param.value) for param in self.parameters}
info_params = {param: str(getattr(self, param)) for param in self._info_params}
values = [repr(self.name)] + [
f"{k}={v}" for k, v in ChainMap(info_params, params).items()
]
return f"{self.__class__.__name__}({', '.join(values)})"
def __repr__(self):
return "❮'{}' @ {} ({})❯".format(
self.name, hex(id(self)), self.__class__.__name__
)
def __deepcopy__(self, memo):
new = object.__new__(type(self))
memo[id(self)] = new
# For debugging what causes deepcopy errors
# try:
# for key in self.__dict__:
# new.__dict__[key] = deepcopy(self.__dict__[key], memo)
# except Exception:
# print("ERROR on deepcopy", key)
# raise
new.__dict__.update(deepcopy(self.__dict__, memo))
# Manually update the weakrefs to be correct
new.__model = weakref.ref(memo[id(self.__model())])
return new
[docs] def info(self, eval_refs=False):
"""Element information.
Parameters
----------
eval_refs : bool
Whether to evaluate symbolic references to their numerical values.
Defaults to False.
Returns
-------
str
The formatted info.
"""
params = self.parameter_table(eval_refs=eval_refs, return_str=True)
info_params = self.info_parameter_table()
msg = f"{self.__class__.__name__} {self.name}\n"
msg += "\nParameters:\n"
if params is not None:
msg += params
else:
msg += "n/a\n"
if info_params is not None:
msg += "\nInformation:\n"
msg += str(self.info_parameter_table())
return msg
[docs] def parameter_table(self, eval_refs=False, return_str=False):
"""Model parameter table.
Parameters
----------
eval_refs : bool
Whether to evaluate symbolic references to their numerical values. Does not
have effect when `return_str` is False. Defaults to False.
return_str : bool
Return str representation instead of :class:`finesse.utilities.Table`.
Necessary when setting `eval_refs` to True. Defaults to False.
Returns
-------
:class:`finesse.utilities.Table`
The formatted parameter info table.
str
String representation of the table, if 'return_str' is True
None
If there are no parameters.
"""
if not self.parameters:
return
if eval_refs and not return_str:
warnings.warn(
"'eval_refs' will not have any effect when 'return_str' False",
stacklevel=1,
)
table = [["Description", "Value"]]
try:
field_rows = []
old_eval_strings = []
# Loop in reverse so we can keep the natural order of each element's
# parameters in its corresponding model parameter class decorators.
for field in reversed(self.parameters):
old_eval_strings.append(field.eval_string)
field.eval_string = eval_refs
field_rows.append([field.description, field])
tab = Table(table + field_rows, headerrow=True, headercolumn=False)
# If we want `eval_refs` to have any effect, we need to convert the table
# before returning, since we are resetting the `eval_string` attribute in
# this function
if return_str:
return str(tab)
else:
return tab
# we don't want to permanently change the Parameter string representation
finally:
for eval_string, field in zip(old_eval_strings, reversed(self.parameters)):
field.eval_string = eval_string
[docs] def info_parameter_table(self):
"""Info parameter table.
This provides a table with useful fields in addition to those contained in
:meth:`.parameter_table`.
Parameters
----------
Returns
-------
str
The formatted extra info table.
None
If there are no info parameters.
"""
if not self._info_params:
return
table = [["Description", "Value"]]
# Loop in reverse so we can keep the natural order of each element's parameters in its
# corresponding info parameter class decorators.
table += [
[description, getattr(self, name)]
for name, (description, _) in reversed(self._info_params.items())
]
return Table(table, headerrow=True, headercolumn=False)
@property
def parameters(self):
"""Returns a list of the parameters available for this element."""
return self._params.copy()
@property
def default_parameter_name(self):
"""The default parameter to assume when the component is directly referenced.
This is used for example in kat script when the component is directly referenced in an
expression, instead of the model parameter, e.g. &l1 instead of &l1.P.
Returns
-------
str
The name of the default model parameter.
None
If there is no default.
"""
return self._default_parameter_name.get(self.__class__)
@property
def name(self):
"""Name of the element.
Returns
-------
str
The name of the element.
"""
return self.__name
@property
def ref(self):
"""Reference to the default model parameter, if set.
Returns
-------
:class:`.ParameterRef`
Reference to the default model parameter, if set.
Raises
------
ValueError
If there is no default model parameter set for this element.
"""
if self.default_parameter_name is None:
raise ModelParameterDefaultValueError(self)
return getattr(self, self.default_parameter_name).ref
@property
def _model(self):
"""Internal reference to the model this element has been added to.
Raises
------
ComponentNotConnected when not connected
"""
from finesse.exceptions import ComponentNotConnected
if self.__model is None:
raise ComponentNotConnected(f"{self.name} is not connected to a model")
else:
return self.__model()
@property
def has_model(self):
"""Returns true if this element has been associated with a Model."""
return self.__model is not None
def _set_model(self, model):
"""A :class:`.Model` instance calls this to associate itself with the element.
.. note::
This method should never be called by the user, it should
only be called internally by the :class:`.Model` class.
Parameters
----------
model : :class:`.Model`
The model to associate with this element.
Raises
------
Exception
If the model is already set for this element.
"""
if model is not None and self.__model is not None:
raise Exception("Model is already set for this element")
if model is None:
# The element has been removed from a model
self.__model = None
else:
self.__model = weakref.ref(model)
def _reset_model(self, new_model):
"""Resets the model that this element is associated with.
Note, this should not be used in normal coding situations. It should only be
used when writing new elements that override the `__deepcopy__` method.
"""
self.__model = weakref.ref(new_model)
def _setup_changing_params(self):
"""For any parameter that has been set to be changing during a simulation this
method will store them and their evaluated values in the set
`self._params_changing` and the dict `self._params_evald`."""
# N.B. Decorators are evaluated from inside - out, so to make the order returned here match
# the order defined, we must reverse self.parameters
self._params_changing = set(
p for p in reversed(self.parameters) if p.is_changing
)
try:
self._params_evald = {}
for p in reversed(self.parameters):
self._params_evald[p.name] = (
p.value.eval() if hasattr(p.value, "eval") else p.value
)
except ArithmeticError as ex:
ex.args = (f"Error evaluating {p}: {str(ex)}",)
raise ex
def _clear_changing_params(self):
"""Sets the set `self._params_changing` and the dict `self._params_evald` to
None after a simulation has completed."""
self._params_changing = None
self._params_evald = None
def _eval_parameters(self):
"""Only call this methods when the model is built. It is optimised for returned
changed parameter values in this state.
To get a dictionary of parameter values in other cases use:
>>>> values = {p.name:p.eval() for p in element.parameters}
Returns
-------
params : dict(str:float)
Dictionary of parameter values
params_changing : set(str)
A set of parameter names which are changing.
"""
# Now we know which are evaluable so no need for repeated checks
# Also reduce memory bashing creating a ton of dictionaries and
# reuse just one.
for p in self._params_changing:
self._params_evald[p.name] = p.eval()
return self._params_evald, self._params_changing