import numpy as np
from simframe.frame.abstractgroup import AbstractGroup
from simframe.frame.heartbeat import Heartbeat
from simframe.utils.color import colorize
[docs]
class Field(np.ndarray, AbstractGroup):
"""Class for storing simulation quantities.
In addition to ``Group``, ``Field`` can have an ``differentiator`` for calculating its derivative and/or an
``jacobinator`` for calculating its Jacobian. The function that is calculating the derivative needs the parent
``Frame`` object as first, the integration variable of type ``IntVar`` as second, and the ``Field`` itself as
third positional argument
``Field`` behaves like ``numpy.ndarray`` and can perform the same numerical operations.
Notes
-----
When ``Field.update()`` is called ``Field`` will be updated according return value of the ``updater`` of the
``Heartbeat`` object assigned to the ``Field``. The function that is updating ``Field`` needs the parent ``Frame``
object as first positional argument."""
__name__ = "Field"
def __new__(cls, owner, value, updater=None, differentiator=None, jacobinator=None, description="", constant=False, save=True, copy=False):
"""Parameters
----------
owner : Frame
Parent frame object to which the field belongs
value : number, array, string
Initial value of the field. Needs to have correct type and shape
updater : Heartbeat, Updater, callable or None, optional, default : None
Instruction for field update
differentiator : Heartbeat, Updater, callable or None, optional, default : None
Instruction for calculating derivative
jacobinator : Heartbeat, Updater, callable or None, optional, default : None
Instruction for calculating the Jacobi matrix
description : string, optional, default : ""
Descriptive string for the field
constant : boolean, optional, default : False
True if the field is immutable.
save : boolean, optional, default : True
If False the field is not written into output files
copy : boolean, optional, default : False
If True <value> will be copied, not referenced"""
obj = np.array(value, copy=copy).view(cls)
if obj.size == 1:
obj = obj.squeeze()
obj._owner = owner
obj.updater = Heartbeat(updater)
obj.differentiator = Heartbeat(differentiator)
obj.jacobinator = Heartbeat(jacobinator)
obj.description = description
obj.constant = constant
obj.save = save
obj._buffer = None
return obj
def __array_finalize__(self, obj):
if obj is None:
return
self._owner = getattr(obj, "_owner", None)
self.updater = getattr(obj, "updater", Heartbeat(None))
self.differentiator = getattr(obj, "differentiator", Heartbeat(None))
self.jacobinator = getattr(obj, "jacobinator", Heartbeat(None))
self.description = getattr(obj, "description", "")
self.constant = getattr(obj, "constant", False)
self._save = getattr(obj, "_save", True)
self._buffer = getattr(obj, "_buffer", None)
def __str__(self):
ret = AbstractGroup.__str__(self)
if self.constant:
ret += ", {}".format(colorize("constant", "purple"))
return ret
def __repr__(self):
val = self.getfield(self.dtype)
ret = f"{np.ndarray.__str__(val)}"
return ret
def __reduce__(self):
"""
Custom ``__reduce__`` function that carries extra information about
custom attributes of ``Field`` class.
"""
pickled_state = super(Field, self).__reduce__()
new_state = pickled_state[2] + (self.__dict__,)
return (pickled_state[0], pickled_state[1], new_state)
def __setstate__(self, state):
"""
Custom ``__setstate__`` function that adds extra
custom attributes of ``Field`` class.
"""
self.__dict__.update(state[-1])
super(Field, self).__setstate__(state[0:-1])
@property
def constant(self):
'''If True, ``Field`` is immutable.'''
return self._constant
@constant.setter
def constant(self, value):
if isinstance(value, int):
if value:
self._constant = True
else:
self._constant = False
else:
raise TypeError("<value> hat to be of type bool.")
@property
def save(self):
'''If False, ``Field`` will not be stored in output files.'''
return self._save
@save.setter
def save(self, value):
if isinstance(value, int):
if value:
self._save = True
else:
self._save = False
else:
raise TypeError("<value> hat to be of type bool.")
@property
def buffer(self):
'''Temporary buffer that stores the new value of ``Field`` after successful integration.'''
return self._buffer
@buffer.setter
def buffer(self, value):
raise RuntimeError("Do not set buffer directly.")
@property
def differentiator(self):
'''``Heartbeat`` object with instructions for calculating the derivative of ``Field``'''
return self._differentiator
@differentiator.setter
def differentiator(self, value):
if isinstance(value, Heartbeat):
self._differentiator = value
else:
self._differentiator = Heartbeat(value)
@property
def jacobinator(self):
'''``Heartbeat`` object with instructions for calculating the Jacobian of ``Field``'''
return self._jacobinator
@jacobinator.setter
def jacobinator(self, value):
if isinstance(value, Heartbeat):
self._jacobinator = value
else:
self._jacobinator = Heartbeat(value)
[docs]
def update(self, *args, **kwargs):
"""Function to update the ``Field``.
Parameter
---------
args : additional positional arguments
kwargs : additional keyword arguments
Notes
-----
Function calls the Heartbeat object of the ``Field``. Additional positional and keyword arguments are only
passed to the ``updater``, NOT to ``systole`` and ``diastole``."""
self.updater.beat(self._owner, *args, Y=self, **kwargs)
[docs]
def derivative(self, x=None, Y=None, *args, **kwargs):
"""If ``differentiator`` or ``jacobinator`` is set, this returns the derivative of the ``Field``.
Parameters
----------
x : IntVar, optional, default : None
Integration variable
If None it uses the integration variable of the integrator of the parent Frame
Y : Field, optional, default : None
Derivative of Y with respect to the integration variable.
If None it uses the field itself
Returns
-------
deriv : derivative of the field according the differetiator or jacobinator
Notes
-----
The function that calculates the derivative needs the parent ``Frame`` as first positional, the
integration variable ``IntVar`` as second positional, and the ``Field`` itself as third positional argument.
The ``differentiator`` is not set, it will try to calculate the derivative from the Jacobian.
If ``jacobinator`` is also not set, it will return ``False``"""
if x is None:
if self._owner.integrator is None:
raise RuntimeError("x not given and no integrator set.")
if self._owner.integrator.var is None:
raise RuntimeError(
"x not given and no integration variable set in integrator.")
x = self._owner.integrator.var
Y = Y if Y is not None else self
deriv = self.differentiator.beat(self._owner, x, Y, *args, **kwargs)
if deriv is not None:
return deriv
jac = self.jacobinator.beat(self._owner, x)
if jac is not None:
return np.dot(jac, Y)
else:
# If no differentiator or jacobian is set we return zeros.
return np.zeros_like(self)
[docs]
def jacobian(self, x=None, *args, **kwargs):
"""If ``jacobinator`` is set, this returns the Jacobi matrix of the ``Field``.
Parameters
----------
x : IntVar, optional, default : None
Integration variable
If None it uses the integration variable of the integrator of the parent Frame
Returns
-------
jac : Jacobi matrix of the field according the differetiator
The function that calculates the Jacobian needs the parent frame as first positional and the
integration variable as second positional."""
if x is None:
if self._owner.integrator is None:
raise RuntimeError("x not given and no integrator set.")
if self._owner.integrator.var is None:
raise RuntimeError(
"x not given and no integration variable set in integrator.")
x = self._owner.integrator.var
return self.jacobinator.beat(self._owner, x, *args, **kwargs)
def _setvalue(self, value):
"""Function to set a value to the field. Direct assignement of values does overwrite the Field object.
Parameters
----------
value : number, array, string
Value to be written into the field. Needs to have correct type and shape"""
if self._constant:
raise RuntimeError("Field is constant.")
value = np.asarray(value)
if value.shape == ():
value = np.array([value])
self.setfield(value, self.dtype)