"""
Descriptors
-----------
Descriptors are small classes that describe the terms in ODE equations or the meaning of the trajectory generations.
They are the key objects in parameter inference as they provide ways for the system to know which trajectory
generated to compare to which of the trajectories observed.
Descriptor objects can be initialised directly, and the appropriate class documentations
should be viewed for instructions on how to do so.
If one would like to create a new descriptor object, such object must inherit from the :class:`Descriptor` object
defined below.
"""
import numpy as np
import sympy
from means.io.serialise import SerialisableObject
[docs]class Descriptor(SerialisableObject):
yaml_tag = u"!descriptor"
[docs] def mathtext(self):
"""
Return the mathtext_ representation of this object.
Used in the legend labels while plotting the trajectories.
Should include the dollar signs (``$``) separating the mathtext from plaintext, e.g. ::
def mathtext(self):
return "Some plain text string before the math representation of lambda $\lambda$"
Defaults to the same representation as provided by ``__str__``.
.. _mathtext: http://matplotlib.org/1.3.1/users/mathtext.html
"""
return str(self)
[docs]class ODETermBase(Descriptor):
"""
Base class for explaining terms in the ODE expressions.
Instances of this class allow providing a description for each of the equations in the generated ODE system.
"""
_symbol = None
def __init__(self, symbol):
super(ODETermBase, self).__init__()
# Sometimes we want to code the moment as sympy.Integer(1) for instance to reduce number of calculations
if isinstance(symbol, int):
symbol = sympy.Integer(symbol)
if symbol is not None and not isinstance(symbol, sympy.Symbol) and not isinstance(symbol, sympy.Integer):
symbol = sympy.Symbol(symbol)
self._symbol = symbol
@property
def symbol(self):
return self._symbol
@property
def descriptor(self):
"""
Returns an uniquely identifying descriptor for this particular ODE term.
"""
return None
def __repr__(self):
return str(self)
def __str__(self):
return unicode(self).encode('utf8')
def __unicode__(self):
return u'{0}({1})'.format(self.__class__.__name__, self.symbol)
[docs] def mathtext(self):
# Double {{ and }} in multiple places as to escape the curly braces in \frac{} from .format
return r'${0}$'.format(sympy.printing.latex(self.symbol))
def _repr_latex(self):
return '${0}$'.format(self.symbol)
[docs]class VarianceTerm(ODETermBase):
"""
Signifies that a particular equation generated from the model is part of a Variance Term
"""
_position = None
yaml_tag = '!variance-term'
def __init__(self, position, symbol):
"""
Creates a Descriptor for a particular ODE in the system that signifies that that particular equation
computes the position-th term of a covariance matrix, where position is some tuple (row,column).
It is used in LNA approximation as there we need to deal with moment and variance terms differently
:param position: position in the covariance matrix
:param symbol: symbol assigned to the term
"""
super(VarianceTerm, self).__init__(symbol=symbol)
self._position = position
@property
def position(self):
return self._position
def __unicode__(self):
return u'{0}(position{1}, symbol={2})'.format(self.__class__.__name__, self.position, self.symbol)
def _repr_latex_(self):
return '(Variance term $V_{{{0}, {1}}} ${2}$)'.format(self.position[0], self.position[1], self.symbol)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self.symbol == other.symbol and self.position == other.position
@classmethod
[docs] def to_yaml(cls, dumper, data):
mapping = [('symbol', str(data.symbol)), ('position', data.position)]
return dumper.represent_mapping(cls.yaml_tag, mapping)
[docs]class Moment(ODETermBase):
"""
An annotator for ODE expressions that describes that a particular expression in a set of ODEs corresponds to a Moment
of the probability distribution. The particular moment is described by :attr:`Moment.n_vector`.
"""
__n_vector = None
yaml_tag = u'!moment'
def __init__(self, n_vector, symbol):
"""
Creates an ODETerm that describes that a particular ODE term is a moment defined by the ``n_vector`` parameter.
The said parameter should be a vector of ints describing the order of the particular species moment.
For instance, for a two species system, the variance for the first species would have ``n_vector=[2, 0]``
as it is the expectation of the squared central moment for the first species.
Similarly, the covariance of the first and second species would have a ``n_vector=[1, 1]`` as it is the
expectation of the product of the central moments for each of the species.
The sum of the terms of ``n_vector`` is referred as the `order` of the moment, and is accessible
via the :attr:`Moment.order` attribute. The concept of ``n_vector`` is genearalisable to higher order moments
as well.
It is worth noting that the concentrations of particular species are the first-order moments themselves,
equivalent to vectors ``[1, 0]`` and ``[0, 1]`` for a two-species system.
These moments are commonly referred to as the means of the systems.
When performing data inference, the :class:`~means.simulation.Trajectory` objects with appropriate descriptors
have to be defined. In most cases, these descriptors will be the aforementioned first-order moments as most
measurable data would be the mean concentrations of species, for instance::
>>> x_concentration_descriptor = Moment([1,0], symbol='x')
Note that both the moment's ``n_vector`` and the ``symbol`` parameters must match the parameters generated by
the trajectory. In the example above the first species in the model need to be named ``'x'``.
:param n_vector: a vector specifying the multidimensional moment e.g. ``[1,0]``
:param symbol: the symbol for the particular descriptor, e.g. ``'x'``
"""
super(Moment, self).__init__(symbol=symbol)
self.__n_vector = np.array(n_vector, dtype=int)
self.__order = sum(self.n_vector)
self.__descriptor = self.n_vector
@property
def descriptor(self):
return self.__n_vector
@property
def n_vector(self):
"""
The n_vector this moment represents
"""
return self.__n_vector
@property
def order(self):
"""
The order of the moment
"""
return self.__order
@property
def is_mixed(self):
"""
Returns whether the moment is a mixed moment, i.e. has a non-zero power to more than one species,
or a raw moment (non-zero power to only one species).
"""
# If moment is not mixed, it will be of form [0, ... , k, ..., 0] where k is the max order
return self.order not in self.n_vector
def __hash__(self):
# Allows moment objects to be stored as keys to dictionaries
return hash(repr(self.n_vector))
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return np.equal(self.n_vector, other.n_vector).all() and self.symbol == other.symbol
def __ne__(self, other):
return not other == self
def __ge__(self, other):
"""
A moment is said greater or equal than another iff all the values of n_vec are greater or equal.
Mathematically: ::math::`n_i^a \ge n_i^b ~ \textrm{for all i}`
"""
return (self.n_vector >= other.n_vector).all()
def __unicode__(self):
return u'{self.__class__.__name__}({self.n_vector!r}, symbol={self.symbol!r})'.format(self=self)
def __str__(self):
return unicode(self).encode("utf8")
def __repr__(self):
return str(self)
def _repr_latex_(self):
return '{0}($[{1}]$, symbol=${2}$)'.format(self.__class__.__name__, ', '.join(map(str, self.n_vector)), self.symbol)
@classmethod
[docs] def to_yaml(cls, dumper, data):
mapping = [('symbol', str(data.symbol)), ('n_vector', data.n_vector.tolist())]
return dumper.represent_mapping(cls.yaml_tag, mapping)