gemlib 0.9.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gemlib-0.9.2/LICENSE +21 -0
- gemlib-0.9.2/PKG-INFO +19 -0
- gemlib-0.9.2/gemlib/__init__.py +9 -0
- gemlib-0.9.2/gemlib/distributions/__init__.py +26 -0
- gemlib-0.9.2/gemlib/distributions/brownian.py +141 -0
- gemlib-0.9.2/gemlib/distributions/categorical2.py +33 -0
- gemlib-0.9.2/gemlib/distributions/continuous_markov.py +371 -0
- gemlib-0.9.2/gemlib/distributions/continuous_time_state_transition_model.py +185 -0
- gemlib-0.9.2/gemlib/distributions/continuous_time_state_transition_model_test.py +289 -0
- gemlib-0.9.2/gemlib/distributions/discrete_markov.py +279 -0
- gemlib-0.9.2/gemlib/distributions/discrete_rejection_sampling.py +149 -0
- gemlib-0.9.2/gemlib/distributions/discrete_time_state_transition_model.py +324 -0
- gemlib-0.9.2/gemlib/distributions/discrete_time_state_transition_model_examples.py +453 -0
- gemlib-0.9.2/gemlib/distributions/discrete_time_state_transition_model_test.py +336 -0
- gemlib-0.9.2/gemlib/distributions/experimental/__init__.py +7 -0
- gemlib-0.9.2/gemlib/distributions/experimental/discrete_approx_cont_state_transition_model.py +194 -0
- gemlib-0.9.2/gemlib/distributions/experimental/state_transition_marginal_model.py +352 -0
- gemlib-0.9.2/gemlib/distributions/hypergeometric.py +144 -0
- gemlib-0.9.2/gemlib/distributions/hypergeometric_sampler.py +103 -0
- gemlib-0.9.2/gemlib/distributions/hypergeometric_test.py +46 -0
- gemlib-0.9.2/gemlib/distributions/kcategorical.py +113 -0
- gemlib-0.9.2/gemlib/distributions/kcategorical_test.py +52 -0
- gemlib-0.9.2/gemlib/distributions/uniform_integer.py +169 -0
- gemlib-0.9.2/gemlib/distributions/uniform_integer_test.py +55 -0
- gemlib-0.9.2/gemlib/mcmc/__init__.py +23 -0
- gemlib-0.9.2/gemlib/mcmc/adaptive_random_walk_metropolis.py +859 -0
- gemlib-0.9.2/gemlib/mcmc/adaptive_random_walk_metropolis_test.py +144 -0
- gemlib-0.9.2/gemlib/mcmc/bb_fixture.pkl +0 -0
- gemlib-0.9.2/gemlib/mcmc/brownian_bridge_kernel.py +291 -0
- gemlib-0.9.2/gemlib/mcmc/brownian_bridge_kernel_test.py +164 -0
- gemlib-0.9.2/gemlib/mcmc/chain_binomial_rippler.py +524 -0
- gemlib-0.9.2/gemlib/mcmc/chain_binomial_rippler_test.py +120 -0
- gemlib-0.9.2/gemlib/mcmc/compound_kernel.py +156 -0
- gemlib-0.9.2/gemlib/mcmc/conftest.py +4 -0
- gemlib-0.9.2/gemlib/mcmc/damped_chain_binomial_rippler.py +840 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/__init__.py +21 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/event_time_proposal.py +275 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/fixtures.py +56 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_mh.py +258 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_mh_test.py +108 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_proposal.py +188 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_proposal_test.py +33 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/move_events.py +239 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/move_events_test.py +63 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_mh.py +254 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_mh_test.py +28 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_proposal.py +150 -0
- gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/util.py +9 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/__init__.py +0 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/composable_kernel.py +270 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/__init__.py +1 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/left_censored_events_mh.py +149 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/move_events.py +71 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/move_events_test.py +48 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/right_censored_events_mh.py +89 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/right_censored_events_mh_test.py +28 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/hmc.py +111 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/hmc_test.py +61 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/mcmc_base.py +33 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/mcmc_sampler.py +94 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/mcmc_sampler_test.py +44 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/multi_scan.py +53 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/multi_scan_test.py +71 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/random_walk_metropolis.py +107 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/random_walk_metropolis_test.py +214 -0
- gemlib-0.9.2/gemlib/mcmc/experimental/test_util.py +49 -0
- gemlib-0.9.2/gemlib/mcmc/gibbs_kernel.py +505 -0
- gemlib-0.9.2/gemlib/mcmc/gibbs_kernel_test.py +212 -0
- gemlib-0.9.2/gemlib/mcmc/h5_posterior.py +77 -0
- gemlib-0.9.2/gemlib/mcmc/multi_scan_kernel.py +59 -0
- gemlib-0.9.2/gemlib/mcmc/zarr_posterior.py +132 -0
- gemlib-0.9.2/gemlib/util.py +117 -0
- gemlib-0.9.2/gemlib/util_test.py +75 -0
- gemlib-0.9.2/pyproject.toml +60 -0
gemlib-0.9.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 The GEM Authors. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gemlib-0.9.2/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: gemlib
|
|
3
|
+
Version: 0.9.2
|
|
4
|
+
Summary: GEMlib scientific compute library for epidemic modelling
|
|
5
|
+
Home-page: http://fhm-chicas-code.lancs.ac.uk/GEM/gemlib
|
|
6
|
+
Author: Chris Jewell
|
|
7
|
+
Author-email: c.jewell@lancaster.ac.uk
|
|
8
|
+
Requires-Python: >=3.9.0,<3.12.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Requires-Dist: scipy (>=1.12.0,<2.0.0)
|
|
14
|
+
Requires-Dist: sphinx-book-theme (>=1.1.3,<2.0.0)
|
|
15
|
+
Requires-Dist: tensorflow (>=2.15.0,<2.16.0) ; sys_platform == "linux"
|
|
16
|
+
Requires-Dist: tensorflow-cpu (>=2.15.0,<2.16.0) ; sys_platform == "darwin"
|
|
17
|
+
Requires-Dist: tensorflow-intel (>=2.15.0,<2.16.0) ; sys_platform == "win32"
|
|
18
|
+
Requires-Dist: tensorflow-probability (>=0.23.0,<0.24.0)
|
|
19
|
+
Project-URL: Repository, http://fhm-chicas-code.lancs.ac.uk/GEM/gemlib
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Distribution addons for Tensorflow Probability"""
|
|
2
|
+
|
|
3
|
+
from gemlib.distributions.brownian import BrownianBridge, BrownianMotion
|
|
4
|
+
from gemlib.distributions.categorical2 import Categorical2
|
|
5
|
+
from gemlib.distributions.continuous_time_state_transition_model import (
|
|
6
|
+
ContinuousTimeStateTransitionModel,
|
|
7
|
+
)
|
|
8
|
+
from gemlib.distributions.discrete_time_state_transition_model import (
|
|
9
|
+
DiscreteTimeStateTransitionModel,
|
|
10
|
+
)
|
|
11
|
+
from gemlib.distributions.hypergeometric import Hypergeometric
|
|
12
|
+
from gemlib.distributions.kcategorical import UniformKCategorical
|
|
13
|
+
from gemlib.distributions.uniform_integer import UniformInteger
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BrownianBridge",
|
|
17
|
+
"BrownianMotion",
|
|
18
|
+
"Categorical2",
|
|
19
|
+
"DiscreteApproxContStateTransitionModel",
|
|
20
|
+
"DiscreteTimeStateTransitionModel",
|
|
21
|
+
"ContinuousTimeStateTransitionModel",
|
|
22
|
+
"StateTransitionMarginalModel",
|
|
23
|
+
"UniformKCategorical",
|
|
24
|
+
"UniformInteger",
|
|
25
|
+
"Hypergeometric",
|
|
26
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Brownian motion as a distribution"""
|
|
2
|
+
|
|
3
|
+
import tensorflow as tf
|
|
4
|
+
import tensorflow_probability as tfp
|
|
5
|
+
from tensorflow_probability.python.internal import (
|
|
6
|
+
distribution_util as dist_util,
|
|
7
|
+
)
|
|
8
|
+
from tensorflow_probability.python.internal import (
|
|
9
|
+
dtype_util,
|
|
10
|
+
reparameterization,
|
|
11
|
+
)
|
|
12
|
+
from tensorflow_probability.python.internal.tensor_util import (
|
|
13
|
+
convert_nonref_to_tensor,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
tfd = tfp.distributions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BrownianMotion(tfd.Distribution):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
index_points,
|
|
23
|
+
x0=0.0,
|
|
24
|
+
scale=1.0,
|
|
25
|
+
validate_args=False,
|
|
26
|
+
allow_nan_stats=True,
|
|
27
|
+
name="BrownianMotion",
|
|
28
|
+
):
|
|
29
|
+
parameters = dict(locals())
|
|
30
|
+
dtype = dtype_util.common_dtype([x0, index_points, scale])
|
|
31
|
+
self._x0 = convert_nonref_to_tensor(x0, dtype_hint=dtype)
|
|
32
|
+
|
|
33
|
+
self._index_points = convert_nonref_to_tensor(
|
|
34
|
+
index_points, dtype_hint=dtype
|
|
35
|
+
)
|
|
36
|
+
self._scale = tf.convert_to_tensor(scale, dtype_hint=dtype)
|
|
37
|
+
|
|
38
|
+
self._increments = tfd.MultivariateNormalDiag(
|
|
39
|
+
loc=tf.zeros_like(self._index_points[..., 1:]),
|
|
40
|
+
scale_diag=tf.math.sqrt(
|
|
41
|
+
self._index_points[..., 1:] - self._index_points[..., :-1]
|
|
42
|
+
)
|
|
43
|
+
* self._scale,
|
|
44
|
+
validate_args=validate_args,
|
|
45
|
+
allow_nan_stats=allow_nan_stats,
|
|
46
|
+
name="bm_increments",
|
|
47
|
+
) # iid increments
|
|
48
|
+
|
|
49
|
+
with tf.name_scope(name) as name:
|
|
50
|
+
super().__init__(
|
|
51
|
+
dtype=dtype,
|
|
52
|
+
reparameterization_type=reparameterization.FULLY_REPARAMETERIZED,
|
|
53
|
+
validate_args=validate_args,
|
|
54
|
+
allow_nan_stats=allow_nan_stats,
|
|
55
|
+
parameters=parameters,
|
|
56
|
+
name=name,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _batch_shape(self):
|
|
60
|
+
return tf.TensorShape(self._x0.shape)
|
|
61
|
+
|
|
62
|
+
def _event_shape(self):
|
|
63
|
+
return tf.TensorShape(self._index_points.shape[-1] - 1)
|
|
64
|
+
|
|
65
|
+
def _sample_n(self, n, seed=None):
|
|
66
|
+
return self._x0 + tf.math.cumsum(
|
|
67
|
+
self._increments.sample(n, seed=seed), axis=-1
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _log_prob(self, x):
|
|
71
|
+
path = dist_util.pad(x, axis=-1, front=True, value=self._x0)
|
|
72
|
+
diff = path[..., 1:] - path[..., :-1]
|
|
73
|
+
return self._increments.log_prob(diff)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BrownianBridge(tfd.Distribution):
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
index_points,
|
|
80
|
+
x0=0.0,
|
|
81
|
+
x1=0.0,
|
|
82
|
+
scale=1.0,
|
|
83
|
+
validate_args=False,
|
|
84
|
+
allow_nan_stats=True,
|
|
85
|
+
name="BrownianBridge",
|
|
86
|
+
):
|
|
87
|
+
parameters = dict(locals())
|
|
88
|
+
dtype = dtype_util.common_dtype([index_points, x0, x1, scale])
|
|
89
|
+
self._index_points = convert_nonref_to_tensor(
|
|
90
|
+
index_points, dtype_hint=dtype
|
|
91
|
+
)
|
|
92
|
+
self._x0 = convert_nonref_to_tensor(x0, dtype_hint=dtype)
|
|
93
|
+
self._x1 = convert_nonref_to_tensor(x1, dtype_hint=dtype)
|
|
94
|
+
self._scale = convert_nonref_to_tensor(scale, dtype_hint=dtype)
|
|
95
|
+
|
|
96
|
+
self._increments = tfd.MultivariateNormalDiag(
|
|
97
|
+
loc=0.0,
|
|
98
|
+
scale_diag=tf.math.sqrt(
|
|
99
|
+
self._index_points[..., 1:] - self._index_points[..., :-1]
|
|
100
|
+
)
|
|
101
|
+
* self._scale,
|
|
102
|
+
name="bb_increments",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
with tf.name_scope(name) as name:
|
|
106
|
+
super().__init__(
|
|
107
|
+
dtype=dtype,
|
|
108
|
+
reparameterization_type=reparameterization.FULLY_REPARAMETERIZED,
|
|
109
|
+
validate_args=validate_args,
|
|
110
|
+
allow_nan_stats=allow_nan_stats,
|
|
111
|
+
name=name,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _batch_shape(self):
|
|
115
|
+
return tf.TensorShape(self._x0.shape)
|
|
116
|
+
|
|
117
|
+
def _event_shape(self):
|
|
118
|
+
return tf.TensorShape(self._index_points.shape[-1] - 2)
|
|
119
|
+
|
|
120
|
+
def _sample_n(self, n, seed=None):
|
|
121
|
+
"""Sampling based on re-leveling pure
|
|
122
|
+
Brownian motion
|
|
123
|
+
"""
|
|
124
|
+
z = self._increments.sample(n)
|
|
125
|
+
z = tf.cumsum(z, axis=-1)
|
|
126
|
+
|
|
127
|
+
y_ref_0 = tf.stack([tf.zeros_like(z[..., 0]), z[..., -1]], axis=-1)
|
|
128
|
+
y_ref_1 = tf.stack([self._x0, self._x1], axis=-1)
|
|
129
|
+
line = tfp.math.interp_regular_1d_grid(
|
|
130
|
+
x=self._index_points[..., 1:-1],
|
|
131
|
+
x_ref_min=self._index_points[..., 0],
|
|
132
|
+
x_ref_max=self._index_points[..., -1],
|
|
133
|
+
y_ref=y_ref_1 - y_ref_0,
|
|
134
|
+
)
|
|
135
|
+
return z[..., :-1] + line
|
|
136
|
+
|
|
137
|
+
def _log_prob(self, x):
|
|
138
|
+
path = dist_util.pad(x, -1, front=True, value=self._x0)
|
|
139
|
+
path = dist_util.pad(path, -1, back=True, value=self._x1)
|
|
140
|
+
diff = path[..., 1:] - path[..., :-1]
|
|
141
|
+
return self._increments.log_prob(diff)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Categorical2 corrects a bug in the tfd.Categorical.log_prob"""
|
|
2
|
+
|
|
3
|
+
import tensorflow as tf
|
|
4
|
+
import tensorflow_probability as tfp
|
|
5
|
+
from tensorflow_probability.python.distributions.categorical import (
|
|
6
|
+
_broadcast_cat_event_and_params,
|
|
7
|
+
)
|
|
8
|
+
from tensorflow_probability.python.internal import distribution_util, dtype_util
|
|
9
|
+
|
|
10
|
+
tfd = tfp.distributions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Todo remove this class when https://github.com/tensorflow/tensorflow/issues/40606
|
|
14
|
+
# is fixed
|
|
15
|
+
class Categorical2(tfd.Categorical):
|
|
16
|
+
"""Done to override the faulty log_prob in tfd.Categorical due to
|
|
17
|
+
https://github.com/tensorflow/tensorflow/issues/40606
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def _log_prob(self, k):
|
|
21
|
+
with tf.name_scope("Cat2log_prob"):
|
|
22
|
+
logits = self.logits_parameter()
|
|
23
|
+
if self.validate_args:
|
|
24
|
+
k = distribution_util.embed_check_integer_casting_closed(
|
|
25
|
+
k, target_dtype=self.dtype
|
|
26
|
+
)
|
|
27
|
+
k, logits = _broadcast_cat_event_and_params(
|
|
28
|
+
k, logits, base_dtype=dtype_util.base_dtype(self.dtype)
|
|
29
|
+
)
|
|
30
|
+
logits_normalised = tf.math.log(tf.math.softmax(logits))
|
|
31
|
+
return tf.cast(
|
|
32
|
+
tf.gather(logits_normalised, k, batch_dims=1), logits.dtype
|
|
33
|
+
)
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Function for continuous time simulation"""
|
|
2
|
+
|
|
3
|
+
from typing import Callable, List, NamedTuple
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import tensorflow as tf
|
|
7
|
+
import tensorflow_probability as tfp
|
|
8
|
+
|
|
9
|
+
from gemlib.util import batch_gather, transition_coords
|
|
10
|
+
|
|
11
|
+
# aliasing for convenience
|
|
12
|
+
tfd = tfp.distributions
|
|
13
|
+
Tensor = tf.Tensor
|
|
14
|
+
DTYPE = tf.float32
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EpidemicEvent(NamedTuple):
|
|
18
|
+
"""Tracker of an event in an epidemic simulation
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
time (float): The time at which the event occurred.
|
|
22
|
+
transition (int): The type of transition that occurred.
|
|
23
|
+
individual (int): The individual involved in the event.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
time: float
|
|
27
|
+
transition: int
|
|
28
|
+
individual: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _one_hot_expand_state(condensed_state: tf.Tensor) -> tf.Tensor:
|
|
32
|
+
"""Expand the state of the epidemic to a one-hot representation
|
|
33
|
+
Args:
|
|
34
|
+
epidemic_state: The state of the epidemic
|
|
35
|
+
Returns:
|
|
36
|
+
The one-hot representation of the epidemic state
|
|
37
|
+
"""
|
|
38
|
+
# Create one-hot encoded vectors for each state
|
|
39
|
+
one_hot_states = tf.one_hot(
|
|
40
|
+
tf.range(len(condensed_state)),
|
|
41
|
+
depth=len(condensed_state),
|
|
42
|
+
dtype=tf.float32,
|
|
43
|
+
)
|
|
44
|
+
# Repeat each one-hot state based on its corresponding count
|
|
45
|
+
repeated_states = tf.repeat(one_hot_states, condensed_state, axis=0)
|
|
46
|
+
|
|
47
|
+
# Reshape and transpose to get state per row representation
|
|
48
|
+
return repeated_states
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _total_flux(transition_rates, state, incidence_matrix):
|
|
52
|
+
"""Multiplies `transition_rates` by source `state`s to return
|
|
53
|
+
the total flux along transitions given `state`.
|
|
54
|
+
|
|
55
|
+
Args
|
|
56
|
+
----
|
|
57
|
+
transition_rates: a `[R,N]` tensor of per-individual transition rates
|
|
58
|
+
for `R` transitions and `N` aggregation units.
|
|
59
|
+
state: a `[N, S]` tensor of `N` aggregation units and `S` states.
|
|
60
|
+
incidence_matrix: a `[S, R]` matrix describing the change in `S` for
|
|
61
|
+
each transition `R`.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
A [R,N] tensor of total flux along each transition, taking into account the
|
|
66
|
+
availability of individuals in the source state.
|
|
67
|
+
"""
|
|
68
|
+
source_state_idx = transition_coords(incidence_matrix)[:, 0]
|
|
69
|
+
source_states = batch_gather(state, indices=source_state_idx[:, tf.newaxis])
|
|
70
|
+
transition_rates = tf.stack(transition_rates, axis=-1)
|
|
71
|
+
|
|
72
|
+
return tf.einsum("...nr,...nr->...rn", transition_rates, source_states)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def compute_state(
|
|
76
|
+
incidence_matrix: Tensor,
|
|
77
|
+
initial_state: Tensor,
|
|
78
|
+
event_list: EpidemicEvent,
|
|
79
|
+
include_final_state: bool = False,
|
|
80
|
+
):
|
|
81
|
+
"""Given an event list `event_list`, compute a timeseries
|
|
82
|
+
of state given the model.
|
|
83
|
+
|
|
84
|
+
Args
|
|
85
|
+
----
|
|
86
|
+
incidence_matrix: a `[S,R]` graph incidence matrix for `S`
|
|
87
|
+
compartments and `R` transitions.
|
|
88
|
+
initial_state: a `[N,S]` representing the initial state of `N`
|
|
89
|
+
units by `S` compartments.
|
|
90
|
+
event_list: the event list, assumed to be sorted by time.
|
|
91
|
+
include_final_state: should the final state be included in the
|
|
92
|
+
returned timeseries? If `True`, then the time dimension of
|
|
93
|
+
the returned tensor will be 1 greater than the length of the
|
|
94
|
+
event list. If `False` (default) these will be equal.
|
|
95
|
+
|
|
96
|
+
Return
|
|
97
|
+
------
|
|
98
|
+
A `[T, N, S]` tensor where `T` is the number of events, `N` is the
|
|
99
|
+
number of individuals, and `S` is the number of states.
|
|
100
|
+
"""
|
|
101
|
+
event_list = event_list.__class__(
|
|
102
|
+
*[tf.convert_to_tensor(x) for x in event_list]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
initial_state = tf.convert_to_tensor(initial_state)
|
|
106
|
+
incidence_matrix = tf.convert_to_tensor(incidence_matrix)
|
|
107
|
+
|
|
108
|
+
# Compute one-hot encoding of event timeseries
|
|
109
|
+
event_tensor_shape = (
|
|
110
|
+
event_list.time.shape[0], # T
|
|
111
|
+
initial_state.shape[-2], # N
|
|
112
|
+
incidence_matrix.shape[-1] + 1, # R + pad for ghost events
|
|
113
|
+
)
|
|
114
|
+
hot_indices = tf.stack(
|
|
115
|
+
[
|
|
116
|
+
tf.range(event_list.time.shape[0]), # T
|
|
117
|
+
event_list.individual, # N
|
|
118
|
+
event_list.transition, # R
|
|
119
|
+
],
|
|
120
|
+
axis=-1,
|
|
121
|
+
)
|
|
122
|
+
event_tensor = tf.scatter_nd(
|
|
123
|
+
indices=hot_indices,
|
|
124
|
+
updates=tf.ones([event_list.individual.shape[-1]], initial_state.dtype),
|
|
125
|
+
shape=event_tensor_shape,
|
|
126
|
+
)[..., :-1] # Clip last dimension to remove ghost events
|
|
127
|
+
|
|
128
|
+
# Compute deltas and cumsum over the state
|
|
129
|
+
delta = tf.matmul(event_tensor, incidence_matrix, transpose_b=True)
|
|
130
|
+
|
|
131
|
+
if include_final_state is False:
|
|
132
|
+
delta = delta[:-1]
|
|
133
|
+
|
|
134
|
+
state = tf.cumsum(
|
|
135
|
+
tf.concat([initial_state[tf.newaxis, ...], delta], axis=-3)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return state
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def exponential_propogate(
|
|
142
|
+
transition_rate_fn: Callable, incidence_matrix: Tensor
|
|
143
|
+
) -> EpidemicEvent:
|
|
144
|
+
"""Generates a function for propogating an epidemic forward in time
|
|
145
|
+
|
|
146
|
+
Closure over the transition rate function and the incidence matrix
|
|
147
|
+
which outline the epidemic dynamics and model structure. The returned
|
|
148
|
+
function can be used to simulate the epidemic forward in time one step.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
transition_rate_fn (Callable): a function that takes the current
|
|
152
|
+
state of the epidemic and returns the transition rates for each
|
|
153
|
+
individual/meta-population.
|
|
154
|
+
incidence_matrix (tensor): A `[R, S]` matrix that describes the graph
|
|
155
|
+
structure of the state transition mode. The rows correspond to the
|
|
156
|
+
`R` transitions and the columns correspond to the `S` states.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
EpidemicEvent: A NamedTuple that describes the next event in the
|
|
160
|
+
epidemic.
|
|
161
|
+
"""
|
|
162
|
+
tr_incidence_matrix = tf.transpose(incidence_matrix)
|
|
163
|
+
|
|
164
|
+
def propogate_fn(time: float, state: Tensor, seed: int) -> List:
|
|
165
|
+
"""Propogates the state of the epidemic forward in time
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
time (float): Wall clock of the epidemic - can easily recover
|
|
169
|
+
the time delta
|
|
170
|
+
state (tensor): `[N,S]` representing the current state.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
EpidemicEvent: The next event in the epidemic.
|
|
174
|
+
"""
|
|
175
|
+
seed_exp, seed_cat = tfp.random.split_seed(seed, n=2)
|
|
176
|
+
num_units = state.shape[-2]
|
|
177
|
+
|
|
178
|
+
# compute event rates for all possible events
|
|
179
|
+
transition_rates = _total_flux(
|
|
180
|
+
transition_rate_fn(time, state), state, incidence_matrix
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# simulate next time
|
|
184
|
+
t_next = tfd.Exponential(rate=tf.reduce_sum(transition_rates)).sample(
|
|
185
|
+
seed=seed_exp
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# use categorical distribution to get event type and indiviudal id
|
|
189
|
+
event_id = tfd.Categorical(
|
|
190
|
+
probs=tf.reshape(transition_rates, shape=(-1,)),
|
|
191
|
+
dtype=tf.int32,
|
|
192
|
+
).sample(seed=seed_cat)
|
|
193
|
+
|
|
194
|
+
unit_idx = tf.math.floormod(event_id, num_units)
|
|
195
|
+
transition_idx = tf.math.floordiv(event_id, num_units)
|
|
196
|
+
|
|
197
|
+
# update the state
|
|
198
|
+
new_state = tf.tensor_scatter_nd_add(
|
|
199
|
+
state,
|
|
200
|
+
[[unit_idx]],
|
|
201
|
+
[tr_incidence_matrix[transition_idx]],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
time + t_next,
|
|
206
|
+
new_state,
|
|
207
|
+
EpidemicEvent(time + t_next, transition_idx, unit_idx),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return propogate_fn
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def continuous_markov_simulation(
|
|
214
|
+
transition_rate_fn: Callable,
|
|
215
|
+
initial_state: Tensor,
|
|
216
|
+
incidence_matrix: Tensor,
|
|
217
|
+
num_markov_jumps: int,
|
|
218
|
+
initial_time: float = 0.0,
|
|
219
|
+
seed=None,
|
|
220
|
+
) -> EpidemicEvent:
|
|
221
|
+
"""
|
|
222
|
+
Simulates a continuous-time Markov process
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
transition_rate_fn (Callable): A function that computes the transition
|
|
226
|
+
rates given the current state and incidence matrix.
|
|
227
|
+
initial_state (Tensor): A [N, S] tensor, respresenting a population of N
|
|
228
|
+
units and S states.
|
|
229
|
+
num_markov_jumps (int): The number of iterations to simulate.
|
|
230
|
+
incidence_matrix (Tensor): The `[S,R]` incidence matrix representing the
|
|
231
|
+
state transition model with S states and R transitions.
|
|
232
|
+
seed (Optional[List(int,int)): The random seed.
|
|
233
|
+
Returns:
|
|
234
|
+
EpidemicEvent: An object containing the simulated epidemic events.
|
|
235
|
+
|
|
236
|
+
"""
|
|
237
|
+
initial_state = tf.convert_to_tensor(initial_state)
|
|
238
|
+
incidence_matrix = tf.convert_to_tensor(incidence_matrix)
|
|
239
|
+
dtype = initial_state.dtype
|
|
240
|
+
seed = tfp.random.sanitize_seed(seed, salt="continuous_markov_simulation")
|
|
241
|
+
|
|
242
|
+
propagate_fn = exponential_propogate(transition_rate_fn, incidence_matrix)
|
|
243
|
+
|
|
244
|
+
accum = EpidemicEvent(
|
|
245
|
+
time=tf.TensorArray(dtype, size=num_markov_jumps, dynamic_size=False),
|
|
246
|
+
transition=tf.TensorArray(
|
|
247
|
+
tf.int32, size=num_markov_jumps, dynamic_size=False
|
|
248
|
+
),
|
|
249
|
+
individual=tf.TensorArray(
|
|
250
|
+
tf.int32, size=num_markov_jumps, dynamic_size=False
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def cond(i, time, state, *_):
|
|
255
|
+
transition_rates = _total_flux(
|
|
256
|
+
transition_rate_fn(time, state), state, incidence_matrix
|
|
257
|
+
)
|
|
258
|
+
cont = (i < num_markov_jumps) & (tf.reduce_sum(transition_rates) > 0.0)
|
|
259
|
+
return cont
|
|
260
|
+
|
|
261
|
+
def body(i, time, state, seed, accum):
|
|
262
|
+
next_seed, this_seed = tfp.random.split_seed(seed, salt="body")
|
|
263
|
+
next_time, next_state, event = propagate_fn(time, state, this_seed)
|
|
264
|
+
accum = EpidemicEvent(*[x.write(i, y) for x, y in zip(accum, event)])
|
|
265
|
+
return i + 1, next_time, next_state, next_seed, accum
|
|
266
|
+
|
|
267
|
+
actual_markov_jumps, _, _, _, accum = tf.while_loop(
|
|
268
|
+
cond, body, loop_vars=(0, initial_time, initial_state, seed, accum)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Pad unused parts of the output TensorArrays if the
|
|
272
|
+
# loop terminates before num_markov_jumps
|
|
273
|
+
indices = tf.range(actual_markov_jumps, num_markov_jumps)
|
|
274
|
+
fills = tf.fill([num_markov_jumps - actual_markov_jumps], np.inf)
|
|
275
|
+
|
|
276
|
+
output = EpidemicEvent(
|
|
277
|
+
time=accum.time.scatter(
|
|
278
|
+
indices,
|
|
279
|
+
fills,
|
|
280
|
+
),
|
|
281
|
+
individual=accum.individual.scatter(indices, tf.zeros_like(indices)),
|
|
282
|
+
transition=accum.transition.scatter(
|
|
283
|
+
indices,
|
|
284
|
+
tf.fill(
|
|
285
|
+
[num_markov_jumps - actual_markov_jumps],
|
|
286
|
+
incidence_matrix.shape[1],
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
output = tf.nest.map_structure(lambda x: x.stack(), output)
|
|
292
|
+
|
|
293
|
+
return output
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def continuous_time_log_likelihood(
|
|
297
|
+
transition_rate_fn: Callable,
|
|
298
|
+
incidence_matrix: Tensor,
|
|
299
|
+
initial_state: Tensor,
|
|
300
|
+
initial_time: float,
|
|
301
|
+
num_jumps: int,
|
|
302
|
+
event_list: EpidemicEvent,
|
|
303
|
+
) -> float:
|
|
304
|
+
"""
|
|
305
|
+
Computes the log-likelihood of a continuous-time Markov process
|
|
306
|
+
given the transition rate function,
|
|
307
|
+
incidence matrix, initial state, number of jumps, and event data.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
transition_rate_fn (Callable): A function that computes the
|
|
311
|
+
transition rate given the current state and time.
|
|
312
|
+
incidence_matrix: The incidence matrix representing
|
|
313
|
+
the connections between states in `[S,R]` format.
|
|
314
|
+
initial_state: The initial state of the process as a `[N,R]`.
|
|
315
|
+
num_jumps (int): The number of jumps to simulate.
|
|
316
|
+
event (EpidemicEvent): The event data containing the times
|
|
317
|
+
and states.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Tensor: The log-likelihood of the continuous-time Markov process.
|
|
321
|
+
"""
|
|
322
|
+
# construct the epidemic states [T, N, S]
|
|
323
|
+
states = compute_state(
|
|
324
|
+
incidence_matrix=incidence_matrix,
|
|
325
|
+
initial_state=initial_state,
|
|
326
|
+
event_list=event_list,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# compute the transition rates for each of the states in the event
|
|
330
|
+
time = tf.concat([[initial_time], event_list.time], axis=-1)
|
|
331
|
+
rates = tf.vectorized_map(
|
|
332
|
+
fn=lambda x: transition_rate_fn(*x),
|
|
333
|
+
elems=(time[:-1], states),
|
|
334
|
+
) # R-tuple of [T,N] tensors
|
|
335
|
+
|
|
336
|
+
# zero out impossible events - this is a hacky way to do it (element wise
|
|
337
|
+
# matrix multiplication)
|
|
338
|
+
total_flux = _total_flux(rates, states, incidence_matrix)
|
|
339
|
+
|
|
340
|
+
indices = tf.stack(
|
|
341
|
+
[
|
|
342
|
+
tf.range(event_list.time.shape[0]),
|
|
343
|
+
tf.clip_by_value( # Clip due to ghost events (values zeroed later)
|
|
344
|
+
event_list.transition,
|
|
345
|
+
clip_value_min=0,
|
|
346
|
+
clip_value_max=incidence_matrix.shape[-1] - 1,
|
|
347
|
+
),
|
|
348
|
+
event_list.individual,
|
|
349
|
+
],
|
|
350
|
+
axis=-1,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# compute event specific rate - get indices of the event that happened
|
|
354
|
+
event_rate = tf.gather_nd(total_flux, indices)
|
|
355
|
+
|
|
356
|
+
# compute total rate per timestep
|
|
357
|
+
total_rate = tf.reduce_sum(total_flux, axis=(-2, -1))
|
|
358
|
+
# total_rate = tf.einsum("tns -> t", total_flux)
|
|
359
|
+
|
|
360
|
+
# compute time deltas
|
|
361
|
+
time_delta = time[1:] - time[:-1]
|
|
362
|
+
|
|
363
|
+
# compute the log-likelihood
|
|
364
|
+
loglik_t = -total_rate * time_delta + tf.math.log(event_rate)
|
|
365
|
+
|
|
366
|
+
# Zero out for any inf times (i.e. possible padding of event_list chunk)
|
|
367
|
+
loglik_t = tf.where(
|
|
368
|
+
tf.math.is_finite(time_delta), loglik_t, tf.zeros_like(loglik_t)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return tf.reduce_sum(loglik_t)
|