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.
Files changed (74) hide show
  1. gemlib-0.9.2/LICENSE +21 -0
  2. gemlib-0.9.2/PKG-INFO +19 -0
  3. gemlib-0.9.2/gemlib/__init__.py +9 -0
  4. gemlib-0.9.2/gemlib/distributions/__init__.py +26 -0
  5. gemlib-0.9.2/gemlib/distributions/brownian.py +141 -0
  6. gemlib-0.9.2/gemlib/distributions/categorical2.py +33 -0
  7. gemlib-0.9.2/gemlib/distributions/continuous_markov.py +371 -0
  8. gemlib-0.9.2/gemlib/distributions/continuous_time_state_transition_model.py +185 -0
  9. gemlib-0.9.2/gemlib/distributions/continuous_time_state_transition_model_test.py +289 -0
  10. gemlib-0.9.2/gemlib/distributions/discrete_markov.py +279 -0
  11. gemlib-0.9.2/gemlib/distributions/discrete_rejection_sampling.py +149 -0
  12. gemlib-0.9.2/gemlib/distributions/discrete_time_state_transition_model.py +324 -0
  13. gemlib-0.9.2/gemlib/distributions/discrete_time_state_transition_model_examples.py +453 -0
  14. gemlib-0.9.2/gemlib/distributions/discrete_time_state_transition_model_test.py +336 -0
  15. gemlib-0.9.2/gemlib/distributions/experimental/__init__.py +7 -0
  16. gemlib-0.9.2/gemlib/distributions/experimental/discrete_approx_cont_state_transition_model.py +194 -0
  17. gemlib-0.9.2/gemlib/distributions/experimental/state_transition_marginal_model.py +352 -0
  18. gemlib-0.9.2/gemlib/distributions/hypergeometric.py +144 -0
  19. gemlib-0.9.2/gemlib/distributions/hypergeometric_sampler.py +103 -0
  20. gemlib-0.9.2/gemlib/distributions/hypergeometric_test.py +46 -0
  21. gemlib-0.9.2/gemlib/distributions/kcategorical.py +113 -0
  22. gemlib-0.9.2/gemlib/distributions/kcategorical_test.py +52 -0
  23. gemlib-0.9.2/gemlib/distributions/uniform_integer.py +169 -0
  24. gemlib-0.9.2/gemlib/distributions/uniform_integer_test.py +55 -0
  25. gemlib-0.9.2/gemlib/mcmc/__init__.py +23 -0
  26. gemlib-0.9.2/gemlib/mcmc/adaptive_random_walk_metropolis.py +859 -0
  27. gemlib-0.9.2/gemlib/mcmc/adaptive_random_walk_metropolis_test.py +144 -0
  28. gemlib-0.9.2/gemlib/mcmc/bb_fixture.pkl +0 -0
  29. gemlib-0.9.2/gemlib/mcmc/brownian_bridge_kernel.py +291 -0
  30. gemlib-0.9.2/gemlib/mcmc/brownian_bridge_kernel_test.py +164 -0
  31. gemlib-0.9.2/gemlib/mcmc/chain_binomial_rippler.py +524 -0
  32. gemlib-0.9.2/gemlib/mcmc/chain_binomial_rippler_test.py +120 -0
  33. gemlib-0.9.2/gemlib/mcmc/compound_kernel.py +156 -0
  34. gemlib-0.9.2/gemlib/mcmc/conftest.py +4 -0
  35. gemlib-0.9.2/gemlib/mcmc/damped_chain_binomial_rippler.py +840 -0
  36. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/__init__.py +21 -0
  37. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/event_time_proposal.py +275 -0
  38. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/fixtures.py +56 -0
  39. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_mh.py +258 -0
  40. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_mh_test.py +108 -0
  41. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_proposal.py +188 -0
  42. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_proposal_test.py +33 -0
  43. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/move_events.py +239 -0
  44. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/move_events_test.py +63 -0
  45. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_mh.py +254 -0
  46. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_mh_test.py +28 -0
  47. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_proposal.py +150 -0
  48. gemlib-0.9.2/gemlib/mcmc/discrete_time_state_transition_model/util.py +9 -0
  49. gemlib-0.9.2/gemlib/mcmc/experimental/__init__.py +0 -0
  50. gemlib-0.9.2/gemlib/mcmc/experimental/composable_kernel.py +270 -0
  51. gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/__init__.py +1 -0
  52. gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/left_censored_events_mh.py +149 -0
  53. gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/move_events.py +71 -0
  54. gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/move_events_test.py +48 -0
  55. gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/right_censored_events_mh.py +89 -0
  56. gemlib-0.9.2/gemlib/mcmc/experimental/discrete_time_state_transition_model/right_censored_events_mh_test.py +28 -0
  57. gemlib-0.9.2/gemlib/mcmc/experimental/hmc.py +111 -0
  58. gemlib-0.9.2/gemlib/mcmc/experimental/hmc_test.py +61 -0
  59. gemlib-0.9.2/gemlib/mcmc/experimental/mcmc_base.py +33 -0
  60. gemlib-0.9.2/gemlib/mcmc/experimental/mcmc_sampler.py +94 -0
  61. gemlib-0.9.2/gemlib/mcmc/experimental/mcmc_sampler_test.py +44 -0
  62. gemlib-0.9.2/gemlib/mcmc/experimental/multi_scan.py +53 -0
  63. gemlib-0.9.2/gemlib/mcmc/experimental/multi_scan_test.py +71 -0
  64. gemlib-0.9.2/gemlib/mcmc/experimental/random_walk_metropolis.py +107 -0
  65. gemlib-0.9.2/gemlib/mcmc/experimental/random_walk_metropolis_test.py +214 -0
  66. gemlib-0.9.2/gemlib/mcmc/experimental/test_util.py +49 -0
  67. gemlib-0.9.2/gemlib/mcmc/gibbs_kernel.py +505 -0
  68. gemlib-0.9.2/gemlib/mcmc/gibbs_kernel_test.py +212 -0
  69. gemlib-0.9.2/gemlib/mcmc/h5_posterior.py +77 -0
  70. gemlib-0.9.2/gemlib/mcmc/multi_scan_kernel.py +59 -0
  71. gemlib-0.9.2/gemlib/mcmc/zarr_posterior.py +132 -0
  72. gemlib-0.9.2/gemlib/util.py +117 -0
  73. gemlib-0.9.2/gemlib/util_test.py +75 -0
  74. 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,9 @@
1
+ """gemlib scientific compute library for epidemics."""
2
+
3
+ import importlib.metadata
4
+
5
+ __version__ = importlib.metadata.version(__package__)
6
+
7
+ from gemlib import distributions, mcmc, util
8
+
9
+ __all__ = ["mcmc", "distributions", "util"]
@@ -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)