Skip to content

Commit 8743814

Browse files
authored
Merge pull request #1025 from pints-team/890-stochastic-logistic-toy-model
WIP - Stochastic Logistic Growth (ABC Toy problem)
2 parents c1a1598 + af3262e commit 8743814

10 files changed

+475
-9
lines changed

docs/source/toy/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ Some toy classes provide extra functionality defined in the
3838
simple_harmonic_oscillator_model
3939
sir_model
4040
stochastic_degradation_model
41+
stochastic_logistic_model
4142
twisted_gaussian_logpdf
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*************************
2+
Stochastic Logistic Model
3+
*************************
4+
5+
.. currentmodule:: pints.toy
6+
7+
.. autoclass:: StochasticLogisticModel

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ relevant code.
117117
- [Simple Harmonic Oscillator model](./toy/model-simple-harmonic-oscillator.ipynb)
118118
- [SIR Epidemiology model](./toy/model-sir.ipynb)
119119
- [Stochastic Degradation model](./toy/model-stochastic-degradation.ipynb)
120+
- [Stochastic Logistic model](./toy/model-stochastic-logistic-growth.ipynb)
120121

121122
### Distributions
122123
- [Annulus](./toy/distribution-annulus.ipynb)

examples/toy/model-stochastic-logistic-growth.ipynb

Lines changed: 177 additions & 0 deletions
Large diffs are not rendered by default.

pints/tests/test_toy_logistic_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pints.toy
1313

1414

15-
class TestLogistic(unittest.TestCase):
15+
class TestLogisticModel(unittest.TestCase):
1616
"""
1717
Tests if the logistic (toy) model works.
1818
"""

pints/tests/test_toy_stochastic_degradation_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pints.toy import StochasticDegradationModel
1414

1515

16-
class TestStochasticDegradation(unittest.TestCase):
16+
class TestStochasticDegradationModel(unittest.TestCase):
1717
"""
1818
Tests if the stochastic degradation (toy) model works.
1919
"""
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Tests if the stochastic logistic growth (toy) model works.
4+
#
5+
# This file is part of PINTS (https://github.com/pints-team/pints/) which is
6+
# released under the BSD 3-clause license. See accompanying LICENSE.md for
7+
# copyright notice and full license details.
8+
#
9+
import unittest
10+
import numpy as np
11+
import pints
12+
import pints.toy
13+
14+
15+
class TestStochasticLogisticModel(unittest.TestCase):
16+
"""
17+
Tests if the stochastic logistic growth (toy) model works.
18+
"""
19+
def test_start_with_zero(self):
20+
# Test the special case where the initial population count is zero
21+
22+
# Set seed for random generator
23+
np.random.seed(1)
24+
25+
model = pints.toy.StochasticLogisticModel(0)
26+
times = [0, 1, 2, 100, 1000]
27+
parameters = [0.1, 50]
28+
values = model.simulate(parameters, times)
29+
self.assertEqual(len(values), len(times))
30+
self.assertTrue(np.all(values == np.zeros(5)))
31+
32+
def test_start_with_one(self):
33+
# Run a small simulation and check it runs properly
34+
35+
# Set seed for random generator
36+
np.random.seed(1)
37+
38+
model = pints.toy.StochasticLogisticModel(1)
39+
times = [0, 1, 2, 100, 1000]
40+
parameters = [0.1, 50]
41+
values = model.simulate(parameters, times)
42+
self.assertEqual(len(values), len(times))
43+
self.assertEqual(values[0], 1)
44+
self.assertEqual(values[-1], 50)
45+
self.assertTrue(np.all(values[1:] >= values[:-1]))
46+
47+
def test_suggested(self):
48+
# Check suggested values
49+
model = pints.toy.StochasticLogisticModel(1)
50+
times = model.suggested_times()
51+
parameters = model.suggested_parameters()
52+
self.assertTrue(len(times) == 101)
53+
self.assertTrue(np.all(parameters > 0))
54+
55+
def test_simulate(self):
56+
# Check each step in the simulation process
57+
np.random.seed(1)
58+
model = pints.toy.StochasticLogisticModel(1)
59+
times = np.linspace(0, 100, 101)
60+
params = [0.1, 50]
61+
time, raw_values = model._simulate_raw([0.1, 50])
62+
values = model._interpolate_values(time, raw_values, times, params)
63+
self.assertTrue(len(time), len(raw_values))
64+
65+
# Test output of Gillespie algorithm
66+
self.assertTrue(np.all(raw_values == np.array(range(1, 51))))
67+
68+
# Check simulate function returns expected values
69+
self.assertTrue(np.all(values[np.where(times < time[1])] == 1))
70+
71+
# Check interpolation function works as expected
72+
temp_time = np.array([np.random.uniform(time[0], time[1])])
73+
self.assertTrue(model._interpolate_values(time, raw_values, temp_time,
74+
params)[0] == 1)
75+
temp_time = np.array([np.random.uniform(time[1], time[2])])
76+
self.assertTrue(model._interpolate_values(time, raw_values, temp_time,
77+
params)[0] == 2)
78+
79+
# Check parameters, times cannot be negative
80+
parameters_0 = [-0.1, 50]
81+
self.assertRaises(ValueError, model.simulate, parameters_0, times)
82+
self.assertRaises(ValueError, model.mean, parameters_0, times)
83+
84+
parameters_1 = [0.1, -50]
85+
self.assertRaises(ValueError, model.simulate, parameters_1, times)
86+
self.assertRaises(ValueError, model.mean, parameters_1, times)
87+
88+
times_2 = np.linspace(-10, 10, 21)
89+
parameters_2 = [0.1, 50]
90+
self.assertRaises(ValueError, model.simulate, parameters_2, times_2)
91+
self.assertRaises(ValueError, model.mean, parameters_2, times_2)
92+
93+
# Check this model takes 2 parameters
94+
parameters_3 = [0.1]
95+
self.assertRaises(ValueError, model.simulate, parameters_3, times)
96+
self.assertRaises(ValueError, model.mean, parameters_3, times)
97+
98+
# Check initial value cannot be negative
99+
self.assertRaises(ValueError, pints.toy.StochasticLogisticModel, -1)
100+
101+
def test_mean_variance(self):
102+
# Check the mean is what we expected
103+
model = pints.toy.StochasticLogisticModel(1)
104+
v_mean = model.mean([1, 10], [5, 10])
105+
self.assertEqual(v_mean[0], 10 / (1 + 9 * np.exp(-5)))
106+
self.assertEqual(v_mean[1], 10 / (1 + 9 * np.exp(-10)))
107+
108+
# Check model variance is not implemented
109+
times = np.linspace(0, 100, 101)
110+
parameters = [0.1, 50]
111+
self.assertRaises(NotImplementedError, model.variance,
112+
parameters, times)
113+
114+
115+
if __name__ == '__main__':
116+
unittest.main()

pints/toy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@
3636
from ._sir_model import SIRModel
3737
from ._twisted_gaussian_banana import TwistedGaussianLogPDF
3838
from ._stochastic_degradation_model import StochasticDegradationModel
39+
from ._stochastic_logistic_model import StochasticLogisticModel

pints/toy/_hes1_michaelis_menten.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,20 @@ class Hes1Model(pints.ForwardModel, ToyModel):
3434
3535
Extends :class:`pints.ForwardModel`, :class:`pints.toy.ToyModel`.
3636
37-
References
38-
----------
39-
.. [1] Silk, D., el al. 2011. Designing attractive models via automated
40-
identification of chaotic and oscillatory dynamical regimes. Nature
41-
communications, 2, p.489.
42-
https://doi.org/10.1038/ncomms1496
43-
4437
Parameters
4538
----------
4639
y0 : float
4740
The initial condition of the observable. Requires ``y0 >= 0``.
4841
implicit_parameters
4942
The implicit parameter of the model that is not inferred, given as a
5043
vector ``[p1_0, p2_0, k_deg]`` with ``p1_0, p2_0, k_deg >= 0``.
44+
45+
References
46+
----------
47+
.. [1] Silk, D., el al. 2011. Designing attractive models via automated
48+
identification of chaotic and oscillatory dynamical regimes. Nature
49+
communications, 2, p.489.
50+
https://doi.org/10.1038/ncomms1496
5151
"""
5252
def __init__(self, y0=None, implicit_parameters=None):
5353
if y0 is None:
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#
2+
# Stochastic logistic model.
3+
#
4+
# This file is part of PINTS (https://github.com/pints-team/pints/) which is
5+
# released under the BSD 3-clause license. See accompanying LICENSE.md for
6+
# copyright notice and full license details.
7+
#
8+
from __future__ import absolute_import, division
9+
from __future__ import print_function, unicode_literals
10+
import numpy as np
11+
from scipy.interpolate import interp1d
12+
import pints
13+
14+
from . import ToyModel
15+
16+
17+
class StochasticLogisticModel(pints.ForwardModel, ToyModel):
18+
r"""
19+
This model describes the growth of a population of individuals, where the
20+
birth rate per capita, initially :math:`b_0`, decreases to :math:`0` as the
21+
population size, :math:`\mathcal{C}(t)`, starting from an initial
22+
population size, :math:`n_0`, approaches a carrying capacity, :math:`k`.
23+
This process follows a rate according to [1]_
24+
25+
.. math::
26+
A \xrightarrow{b_0(1-\frac{\mathcal{C}(t)}{k})} 2A.
27+
28+
The model is simulated using the Gillespie stochastic simulation algorithm
29+
[2]_, [3]_.
30+
31+
*Extends:* :class:`pints.ForwardModel`, :class:`pints.toy.ToyModel`.
32+
33+
Parameters
34+
----------
35+
initial_molecule_count : float
36+
Sets the initial population size :math:`n_0`.
37+
38+
References
39+
----------
40+
.. [1] Simpson, M. et al. 2019. Process noise distinguishes between
41+
indistinguishable population dynamics. bioRxiv.
42+
https://doi.org/10.1101/533182
43+
.. [2] Gillespie, D. 1976. A General Method for Numerically Simulating the
44+
Stochastic Time Evolution of Coupled Chemical Reactions.
45+
Journal of Computational Physics. 22 (4): 403-434.
46+
https://doi.org/10.1016/0021-9991(76)90041-3
47+
.. [3] Erban R. et al. 2007. A practical guide to stochastic simulations
48+
of reaction-diffusion processes. arXiv.
49+
https://arxiv.org/abs/0704.1908v2
50+
"""
51+
52+
def __init__(self, initial_molecule_count=50):
53+
super(StochasticLogisticModel, self).__init__()
54+
self._n0 = float(initial_molecule_count)
55+
if self._n0 < 0:
56+
raise ValueError('Initial molecule count cannot be negative.')
57+
58+
def n_parameters(self):
59+
""" See :meth:`pints.ForwardModel.n_parameters()`. """
60+
return 2
61+
62+
def _simulate_raw(self, parameters):
63+
"""
64+
Returns tuple (raw times, population sizes) when reactions occur.
65+
"""
66+
parameters = np.asarray(parameters)
67+
if len(parameters) != self.n_parameters():
68+
raise ValueError('This model should have only 2 parameters.')
69+
b = parameters[0]
70+
k = parameters[1]
71+
if b <= 0:
72+
raise ValueError('Rate constant must be positive.')
73+
74+
# Initial time and count
75+
t = 0
76+
a = self._n0
77+
78+
# Run stochastic logistic birth-only algorithm, calculating time until
79+
# next reaction and increasing population count by 1 at that time
80+
mol_count = [a]
81+
time = [t]
82+
while a < k:
83+
r = np.random.uniform(0, 1)
84+
t += np.log(1 / r) / (a * b * (1 - a / k))
85+
a = a + 1
86+
time.append(t)
87+
mol_count.append(a)
88+
return time, mol_count
89+
90+
def _interpolate_values(self, time, pop_size, output_times, parameters):
91+
"""
92+
Takes raw times and population size values as inputs and outputs
93+
interpolated values at output_times.
94+
"""
95+
# Interpolate as step function, increasing pop_size by 1 at each
96+
# event time point
97+
interp_func = interp1d(time, pop_size, kind='previous')
98+
99+
# Compute population size values at given time points using f1
100+
# at any time beyond the last event, pop_size = k
101+
values = interp_func(output_times[np.where(output_times <= time[-1])])
102+
zero_vector = np.full(
103+
len(output_times[np.where(output_times > time[-1])]),
104+
parameters[1])
105+
values = np.concatenate((values, zero_vector))
106+
return values
107+
108+
def simulate(self, parameters, times):
109+
""" See :meth:`pints.ForwardModel.simulate()`. """
110+
times = np.asarray(times)
111+
if np.any(times < 0):
112+
raise ValueError('Negative times are not allowed.')
113+
if self._n0 == 0:
114+
return np.zeros(times.shape)
115+
116+
# run Gillespie
117+
time, pop_size = self._simulate_raw(parameters)
118+
119+
# interpolate
120+
values = self._interpolate_values(time, pop_size, times, parameters)
121+
return values
122+
123+
def mean(self, parameters, times):
124+
r"""
125+
Computes the deterministic mean of infinitely many stochastic
126+
simulations with times :math:`t` and parameters (:math:`b`, :math:`k`),
127+
which follows:
128+
:math:`\frac{kC(0)}{C(0) + (k - C(0)) \exp(-bt)}`.
129+
130+
Returns an array with the same length as `times`.
131+
"""
132+
parameters = np.asarray(parameters)
133+
if len(parameters) != self.n_parameters():
134+
raise ValueError('This model should have only 2 parameters.')
135+
136+
b = parameters[0]
137+
if b <= 0:
138+
raise ValueError('Rate constant must be positive.')
139+
140+
k = parameters[1]
141+
if k <= 0:
142+
raise ValueError("Carrying capacity must be positive")
143+
144+
times = np.asarray(times)
145+
if np.any(times < 0):
146+
raise ValueError('Negative times are not allowed.')
147+
c0 = self._n0
148+
return (c0 * k) / (c0 + np.exp(-b * times) * (k - c0))
149+
150+
def variance(self, parameters, times):
151+
r"""
152+
Returns the deterministic variance of infinitely many stochastic
153+
simulations.
154+
"""
155+
raise NotImplementedError
156+
157+
def suggested_parameters(self):
158+
""" See :meth:`pints.toy.ToyModel.suggested_parameters()`. """
159+
return np.array([0.1, 500])
160+
161+
def suggested_times(self):
162+
""" See :meth:`pints.toy.ToyModel.suggested_times()`."""
163+
return np.linspace(0, 100, 101)

0 commit comments

Comments
 (0)