import operator
import sympy as sp
from means.approximation.approximation_baseclass import ApproximationBaseClass
from means.core import Moment, VarianceTerm, ODEProblem
[docs]def lna_approximation(model):
r"""
A wrapper around :class:`~means.approximation.lna.lna.LinearNoiseApproximation`.
It performs linear noise approximation (MEA).
:return: an ODE problem which can be further used in inference and simulation.
:rtype: :class:`~means.core.problems.ODEProblem`
"""
lna = LinearNoiseApproximation(model)
return lna.run()
[docs]class LinearNoiseApproximation(ApproximationBaseClass):
"""
A class to performs Linear Noise Approximation of a model.
"""
[docs] def run(self):
"""
Overrides the default _run() private method.
Performs the complete analysis
:return: A fully computed set of Ordinary Differential Equations that can be used for further simulation
:rtype: :class:`~means.core.problems.ODEProblem`
"""
S = self.model.stoichiometry_matrix
amat = self.model.propensities
ymat = self.model.species
n_species = len(ymat)
# dPdt is matrix of each species differentiated w.r.t. time
# The code below literally multiplies the stoichiometry matrix to a column vector of propensities
# from the right (::math::`\frac{dP}{dt} = \mathbf{Sa}`)
dPdt = S * amat
# A Is a matrix of each species (rows) and the derivatives of their stoichiometry matrix rows
# against each other species
# Code below computes the matrix A, that is of size `len(ymat) x len(ymat)`, for which each entry
# ::math::`A_{ik} = \sum_j S_{ij} \frac{\partial a_j}{\partial y_k} = \mathfb{S_i} \frac{\partial \mathbf{a}}{\partial y_k}`
A = sp.Matrix(len(ymat), len(ymat), lambda i, j: 0)
for i in range(A.rows):
for k in range(A.cols):
A[i, k] = reduce(operator.add, [S[i, j] * sp.diff(amat[j], ymat[k]) for j in range(len(amat))])
# `diagA` is a matrix that has values sqrt(a[i]) on the diagonal (0 elsewhere)
diagA = sp.Matrix(len(amat), len(amat), lambda i, j: amat[i] ** sp.Rational(1,2) if i==j else 0)
# E is stoichiometry matrix times diagA
E = S * diagA
variance_terms = []
cov_matrix = []
for i in range(len(ymat)):
row = []
for j in range(len(ymat)):
if i <= j:
symbol = 'V_{0}_{1}'.format(i, j)
variance_terms.append(VarianceTerm(position=(i,j), symbol=symbol))
else:
# Since Vi,j = Vj,i, i.e. covariance are equal, we only record Vi,j but not Vj,i
symbol = 'V_{0}_{1}'.format(j, i)
variance_terms.append(VarianceTerm(position=(j,i), symbol=symbol))
row.append(symbol)
cov_matrix.append(row)
V = sp.Matrix(cov_matrix)
# Matrix of variances (diagonal) and covariances of species i and j differentiated wrt time.
# I.e. if i=j, V_ij is the variance, and if i!=j, V_ij is the covariance between species i and species j
dVdt = A * V + V * (A.T) + E * (E.T)
# build ODEProblem object
rhs_redundant = sp.Matrix([i for i in dPdt] + [i for i in dVdt])
#generate ODE terms
n_vectors = [tuple([1 if i==j else 0 for i in range(n_species)]) for j in range(n_species)]
moment_terms = [Moment(nvec,lhs) for (lhs, nvec) in zip(ymat, n_vectors)]
ode_description = moment_terms + variance_terms
non_redundant_idx = []
ode_terms = []
# remove repetitive covariances, as Vij = Vji
for i, cov in enumerate(ode_description):
if cov in ode_terms:
continue
else:
ode_terms.append(cov)
non_redundant_idx.append(i)
rhs = []
for i in non_redundant_idx:
rhs.append(rhs_redundant[i])
out_problem = ODEProblem("LNA", ode_terms, rhs, sp.Matrix(self.model.parameters))
return out_problem