gemlib 0.9.2__py3-none-any.whl

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 (75) hide show
  1. gemlib/__init__.py +9 -0
  2. gemlib/distributions/__init__.py +26 -0
  3. gemlib/distributions/brownian.py +141 -0
  4. gemlib/distributions/categorical2.py +33 -0
  5. gemlib/distributions/continuous_markov.py +371 -0
  6. gemlib/distributions/continuous_time_state_transition_model.py +185 -0
  7. gemlib/distributions/continuous_time_state_transition_model_test.py +289 -0
  8. gemlib/distributions/discrete_markov.py +279 -0
  9. gemlib/distributions/discrete_rejection_sampling.py +149 -0
  10. gemlib/distributions/discrete_time_state_transition_model.py +324 -0
  11. gemlib/distributions/discrete_time_state_transition_model_examples.py +453 -0
  12. gemlib/distributions/discrete_time_state_transition_model_test.py +336 -0
  13. gemlib/distributions/experimental/__init__.py +7 -0
  14. gemlib/distributions/experimental/discrete_approx_cont_state_transition_model.py +194 -0
  15. gemlib/distributions/experimental/state_transition_marginal_model.py +352 -0
  16. gemlib/distributions/hypergeometric.py +144 -0
  17. gemlib/distributions/hypergeometric_sampler.py +103 -0
  18. gemlib/distributions/hypergeometric_test.py +46 -0
  19. gemlib/distributions/kcategorical.py +113 -0
  20. gemlib/distributions/kcategorical_test.py +52 -0
  21. gemlib/distributions/uniform_integer.py +169 -0
  22. gemlib/distributions/uniform_integer_test.py +55 -0
  23. gemlib/mcmc/__init__.py +23 -0
  24. gemlib/mcmc/adaptive_random_walk_metropolis.py +859 -0
  25. gemlib/mcmc/adaptive_random_walk_metropolis_test.py +144 -0
  26. gemlib/mcmc/bb_fixture.pkl +0 -0
  27. gemlib/mcmc/brownian_bridge_kernel.py +291 -0
  28. gemlib/mcmc/brownian_bridge_kernel_test.py +164 -0
  29. gemlib/mcmc/chain_binomial_rippler.py +524 -0
  30. gemlib/mcmc/chain_binomial_rippler_test.py +120 -0
  31. gemlib/mcmc/compound_kernel.py +156 -0
  32. gemlib/mcmc/conftest.py +4 -0
  33. gemlib/mcmc/damped_chain_binomial_rippler.py +840 -0
  34. gemlib/mcmc/discrete_time_state_transition_model/__init__.py +21 -0
  35. gemlib/mcmc/discrete_time_state_transition_model/event_time_proposal.py +275 -0
  36. gemlib/mcmc/discrete_time_state_transition_model/fixtures.py +56 -0
  37. gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_mh.py +258 -0
  38. gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_mh_test.py +108 -0
  39. gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_proposal.py +188 -0
  40. gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_proposal_test.py +33 -0
  41. gemlib/mcmc/discrete_time_state_transition_model/move_events.py +239 -0
  42. gemlib/mcmc/discrete_time_state_transition_model/move_events_test.py +63 -0
  43. gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_mh.py +254 -0
  44. gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_mh_test.py +28 -0
  45. gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_proposal.py +150 -0
  46. gemlib/mcmc/discrete_time_state_transition_model/util.py +9 -0
  47. gemlib/mcmc/experimental/__init__.py +0 -0
  48. gemlib/mcmc/experimental/composable_kernel.py +270 -0
  49. gemlib/mcmc/experimental/discrete_time_state_transition_model/__init__.py +1 -0
  50. gemlib/mcmc/experimental/discrete_time_state_transition_model/left_censored_events_mh.py +149 -0
  51. gemlib/mcmc/experimental/discrete_time_state_transition_model/move_events.py +71 -0
  52. gemlib/mcmc/experimental/discrete_time_state_transition_model/move_events_test.py +48 -0
  53. gemlib/mcmc/experimental/discrete_time_state_transition_model/right_censored_events_mh.py +89 -0
  54. gemlib/mcmc/experimental/discrete_time_state_transition_model/right_censored_events_mh_test.py +28 -0
  55. gemlib/mcmc/experimental/hmc.py +111 -0
  56. gemlib/mcmc/experimental/hmc_test.py +61 -0
  57. gemlib/mcmc/experimental/mcmc_base.py +33 -0
  58. gemlib/mcmc/experimental/mcmc_sampler.py +94 -0
  59. gemlib/mcmc/experimental/mcmc_sampler_test.py +44 -0
  60. gemlib/mcmc/experimental/multi_scan.py +53 -0
  61. gemlib/mcmc/experimental/multi_scan_test.py +71 -0
  62. gemlib/mcmc/experimental/random_walk_metropolis.py +107 -0
  63. gemlib/mcmc/experimental/random_walk_metropolis_test.py +214 -0
  64. gemlib/mcmc/experimental/test_util.py +49 -0
  65. gemlib/mcmc/gibbs_kernel.py +505 -0
  66. gemlib/mcmc/gibbs_kernel_test.py +212 -0
  67. gemlib/mcmc/h5_posterior.py +77 -0
  68. gemlib/mcmc/multi_scan_kernel.py +59 -0
  69. gemlib/mcmc/zarr_posterior.py +132 -0
  70. gemlib/util.py +117 -0
  71. gemlib/util_test.py +75 -0
  72. gemlib-0.9.2.dist-info/LICENSE +21 -0
  73. gemlib-0.9.2.dist-info/METADATA +19 -0
  74. gemlib-0.9.2.dist-info/RECORD +75 -0
  75. gemlib-0.9.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,185 @@
1
+ """Continuous time state transition model"""
2
+
3
+ from typing import Callable, Optional
4
+
5
+ import tensorflow as tf
6
+ import tensorflow_probability as tfp
7
+ from tensorflow_probability.python.internal import reparameterization
8
+
9
+ from gemlib.distributions.continuous_markov import (
10
+ EpidemicEvent,
11
+ compute_state,
12
+ continuous_markov_simulation,
13
+ continuous_time_log_likelihood,
14
+ )
15
+
16
+ # aliasing for convenience
17
+ tfd = tfp.distributions
18
+ Tensor = tf.Tensor
19
+ DTYPE = tf.float32
20
+
21
+
22
+ class ContinuousTimeStateTransitionModel(tfd.Distribution):
23
+ """Continuous time state transition model."""
24
+
25
+ def __init__(
26
+ self,
27
+ transition_rate_fn: Callable[[Tensor], Tensor],
28
+ incidence_matrix: Tensor,
29
+ initial_state: Tensor,
30
+ num_events: int,
31
+ initial_time: Optional[float] = 0.0,
32
+ validate_args: Optional[bool] = False,
33
+ allow_nan_stats: Optional[bool] = True,
34
+ name: Optional[str] = "ContinuousTimeStateTransitionModel",
35
+ ) -> None:
36
+ """
37
+ Initializes a ContinuousTimeStateTransitionModel object.
38
+
39
+ Args:
40
+ transition_rate_fn: Python callable of the form `fn(t, state)`
41
+ taking the current time `t: float` and state
42
+ tensor `state`, and returning a tuple of tensors
43
+ containing transition rates between states.
44
+ incidence_matrix: Matrix representing the incidence of transitions
45
+ between states.
46
+ initial_state: A `[N, S]` tensor containing the initial state of the
47
+ population of `N` individuals in `S` epidemiological
48
+ classes.
49
+ num_events: the number of events to simulate
50
+ initial_time: Initial time of the model. Defaults to 0.0.
51
+ name: Name of the model. Defaults to
52
+ "ContinuousTimeStateTransitionModel".
53
+ """
54
+ parameters = dict(locals())
55
+
56
+ self._incidence_matrix = tf.convert_to_tensor(incidence_matrix)
57
+ self._initial_state = tf.convert_to_tensor(initial_state)
58
+ self._initial_time = tf.convert_to_tensor(initial_time)
59
+
60
+ super().__init__(
61
+ dtype=self._incidence_matrix.dtype,
62
+ reparameterization_type=reparameterization.FULLY_REPARAMETERIZED,
63
+ validate_args=validate_args,
64
+ allow_nan_stats=allow_nan_stats,
65
+ parameters=parameters,
66
+ name=name,
67
+ )
68
+
69
+ @property
70
+ def transition_rate_fn(self):
71
+ """Transition rate function for the model."""
72
+ return self._parameters["transition_rate_fn"]
73
+
74
+ @property
75
+ def incidence_matrix(self):
76
+ """Incidence matrix for the model."""
77
+ return self._parameters["incidence_matrix"]
78
+
79
+ @property
80
+ def initial_state(self):
81
+ """Initial state of the model."""
82
+ return self._parameters["initial_state"]
83
+
84
+ @property
85
+ def num_events(self):
86
+ """Number of events to simulate."""
87
+ return self._parameters["num_events"]
88
+
89
+ @property
90
+ def initial_time(self):
91
+ """Initial wall clock for the model. Sets the time scale."""
92
+ return self._parameters["initial_time"]
93
+
94
+ def compute_state(
95
+ self, event_list: EpidemicEvent, include_final_state: bool = False
96
+ ) -> Tensor:
97
+ """Given an event list `event_list`, compute a timeseries
98
+ of state given the model.
99
+
100
+ Args
101
+ ----
102
+ event_list: the event list, assumed to be sorted by time.
103
+ include_final_state: should the final state be included in the
104
+ returned timeseries? If `True`, then the time dimension of
105
+ the returned tensor will be 1 greater than the length of the
106
+ event list. If `False` (default) these will be equal.
107
+
108
+ Return
109
+ ------
110
+ A `[T, N, S]` tensor where `T` is the number of events, `N` is the
111
+ number of individuals, and `S` is the number of states.
112
+ """
113
+ return compute_state(
114
+ self.incidence_matrix,
115
+ self.initial_state,
116
+ event_list,
117
+ include_final_state,
118
+ )
119
+
120
+ # Bypass the reshaping that tfd.Distribution._call_sample_n does
121
+ def _call_sample_n(self, sample_shape, seed) -> EpidemicEvent:
122
+ return self._sample_n(sample_shape, seed)
123
+
124
+ def _sample_n(self, sample_shape: int, seed=None) -> EpidemicEvent:
125
+ """
126
+ Samples n outcomes from the continuous time state transition model.
127
+
128
+ Args:
129
+ n (int): The number of realisations of the Markov process to sample
130
+ (currently ignored).
131
+ seed (int, optional): The seed value for random number generation.
132
+ Defaults to None.
133
+
134
+ Returns:
135
+ EpidemicEvent: A list of n outcomes sampled from the continuous time
136
+ state transition model.
137
+ """
138
+
139
+ outcome = continuous_markov_simulation(
140
+ transition_rate_fn=self.transition_rate_fn,
141
+ incidence_matrix=self._incidence_matrix,
142
+ initial_state=self._initial_state,
143
+ initial_time=self._initial_time,
144
+ num_markov_jumps=self.num_events,
145
+ seed=seed,
146
+ )
147
+
148
+ return outcome
149
+
150
+ def log_prob(self, value: EpidemicEvent) -> float:
151
+ return self._log_prob(value)
152
+
153
+ def _log_prob(self, value: EpidemicEvent) -> float:
154
+ """
155
+ Computes the log probability of the given outcomes.
156
+
157
+ Args:
158
+ value (EpidemicEvent): an EpidemicEvent object representing the
159
+ outcomes.
160
+
161
+ Returns:
162
+ float: The log probability of the given outcomes.
163
+ """
164
+ log_lik = continuous_time_log_likelihood(
165
+ transition_rate_fn=self.transition_rate_fn,
166
+ incidence_matrix=self.incidence_matrix,
167
+ initial_state=self.initial_state,
168
+ initial_time=self.initial_time,
169
+ num_jumps=self.num_events,
170
+ event_list=value,
171
+ )
172
+
173
+ return log_lik
174
+
175
+ def _event_shape_tensor(self) -> Tensor:
176
+ return tf.constant([self.num_events], dtype=tf.int32)
177
+
178
+ def _event_shape(self) -> tf.TensorShape:
179
+ return tf.TensorShape([self.num_events])
180
+
181
+ def _batch_shape_tensor(self) -> Tensor:
182
+ return tf.constant([])
183
+
184
+ def _batch_shape(self) -> tf.TensorShape:
185
+ return tf.TensorShape([])
@@ -0,0 +1,289 @@
1
+ """Test ContinuousTimeStateTransitionModel"""
2
+
3
+ import numpy as np
4
+ import pytest
5
+ import tensorflow as tf
6
+ from scipy.optimize import minimize
7
+
8
+ from gemlib.distributions.continuous_time_state_transition_model import (
9
+ ContinuousTimeStateTransitionModel,
10
+ EpidemicEvent,
11
+ compute_state,
12
+ )
13
+
14
+ NUM_EVENTS = 1999
15
+
16
+
17
+ @pytest.fixture
18
+ def example_ilm():
19
+ """A simple event list with 4 individuals, SIR model"""
20
+ return {
21
+ "incidence_matrix": np.array(
22
+ [[-1, 0], [1, -1], [0, 1]], dtype=np.float32
23
+ ),
24
+ "event_list": EpidemicEvent(
25
+ time=np.array(
26
+ [0.4, 1.3, 1.5, 1.9, 2.3, np.inf, np.inf], dtype=np.float32
27
+ ),
28
+ transition=np.array([0, 0, 1, 1, 1, 2, 2], dtype=np.int32),
29
+ individual=np.array([1, 2, 0, 2, 1, 0, 0], dtype=np.int32),
30
+ ),
31
+ "initial_conditions": np.array(
32
+ [[0, 1, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]], dtype=np.float32
33
+ ),
34
+ }
35
+
36
+
37
+ @pytest.fixture
38
+ def simple_sir_model():
39
+ def rate_fn(t, state):
40
+ si_rate = 0.25 * state[:, 1] / tf.reduce_sum(state, axis=-1)
41
+ ir_rate = tf.broadcast_to([0.14], si_rate.shape)
42
+
43
+ return si_rate, ir_rate
44
+
45
+ # [3 species, 2 reactions]
46
+ incidence_matrix = np.array([[-1, 0], [1, -1], [0, 1]], dtype=np.float32)
47
+
48
+ initial_state = np.array(
49
+ [[999, 1, 0]], dtype=np.float32
50
+ ) # [1 unit, 3 classes]
51
+
52
+ return ContinuousTimeStateTransitionModel(
53
+ transition_rate_fn=rate_fn,
54
+ incidence_matrix=incidence_matrix,
55
+ initial_state=initial_state,
56
+ num_events=NUM_EVENTS,
57
+ initial_time=0.0,
58
+ )
59
+
60
+
61
+ def test_simple_sir_shapes(simple_sir_model):
62
+ """Test expected output shape"""
63
+ tf.debugging.assert_equal(
64
+ simple_sir_model.event_shape_tensor(), tf.constant(NUM_EVENTS)
65
+ )
66
+ tf.debugging.assert_equal(
67
+ simple_sir_model.event_shape, tf.TensorShape([NUM_EVENTS])
68
+ )
69
+ tf.debugging.assert_equal(
70
+ simple_sir_model.batch_shape_tensor(), tf.constant([], tf.int32)
71
+ )
72
+ tf.debugging.assert_equal(simple_sir_model.batch_shape, tf.TensorShape([]))
73
+
74
+
75
+ def test_simple_sir_eager(simple_sir_model):
76
+ """Test a simple SIR model"""
77
+
78
+ sample = simple_sir_model.sample(seed=[0, 0])
79
+
80
+ assert isinstance(sample, EpidemicEvent)
81
+
82
+ state = simple_sir_model.compute_state(sample)
83
+ tf.debugging.assert_non_negative(state)
84
+
85
+
86
+ def test_simple_sir_graph(simple_sir_model):
87
+ """Test a simple SIR model"""
88
+
89
+ @tf.function
90
+ def fn():
91
+ return simple_sir_model.sample(seed=[0, 0])
92
+
93
+ sample = fn()
94
+
95
+ assert isinstance(sample, EpidemicEvent)
96
+
97
+ state = simple_sir_model.compute_state(sample)
98
+ tf.debugging.assert_non_negative(state)
99
+
100
+
101
+ def test_compute_state(example_ilm):
102
+ expected_state_eager = compute_state(
103
+ example_ilm["incidence_matrix"],
104
+ example_ilm["initial_conditions"],
105
+ example_ilm["event_list"],
106
+ include_final_state=True,
107
+ )
108
+
109
+ @tf.function
110
+ def compute_state_graph(*args):
111
+ return compute_state(*args)
112
+
113
+ expected_state_graph = compute_state_graph(
114
+ example_ilm["incidence_matrix"],
115
+ example_ilm["initial_conditions"],
116
+ example_ilm["event_list"],
117
+ True,
118
+ )
119
+
120
+ actual_state = np.array(
121
+ [
122
+ [
123
+ [0, 1, 0], # T=0
124
+ [1, 0, 0],
125
+ [1, 0, 0],
126
+ [1, 0, 0],
127
+ ],
128
+ [
129
+ [0, 1, 0], # T=1
130
+ [0, 1, 0],
131
+ [1, 0, 0],
132
+ [1, 0, 0],
133
+ ],
134
+ [
135
+ [0, 1, 0], # T=2
136
+ [0, 1, 0],
137
+ [0, 1, 0],
138
+ [1, 0, 0],
139
+ ],
140
+ [
141
+ [0, 0, 1], # T=3
142
+ [0, 1, 0],
143
+ [0, 1, 0],
144
+ [1, 0, 0],
145
+ ],
146
+ [
147
+ [0, 0, 1], # T=4
148
+ [0, 1, 0],
149
+ [0, 0, 1],
150
+ [1, 0, 0],
151
+ ],
152
+ [
153
+ [0, 0, 1], # T=5
154
+ [0, 0, 1],
155
+ [0, 0, 1],
156
+ [1, 0, 0],
157
+ ],
158
+ [
159
+ [0, 0, 1], # T=6
160
+ [0, 0, 1],
161
+ [0, 0, 1],
162
+ [1, 0, 0],
163
+ ],
164
+ [
165
+ [0, 0, 1], # T=7
166
+ [0, 0, 1],
167
+ [0, 0, 1],
168
+ [1, 0, 0],
169
+ ],
170
+ ],
171
+ dtype=np.float32,
172
+ )
173
+
174
+ np.testing.assert_array_equal(expected_state_eager, actual_state)
175
+ np.testing.assert_array_equal(expected_state_graph, actual_state)
176
+
177
+
178
+ def test_simple_sir_loglik(example_ilm):
179
+ """Test loglikelihood function"""
180
+ # epi constants
181
+ incidence_matrix = example_ilm["incidence_matrix"]
182
+ initial_population = example_ilm["initial_conditions"]
183
+
184
+ def rate_fn(t, state):
185
+ si_rate = tf.broadcast_to([0.5], [state.shape[0]])
186
+ ir_rate = tf.broadcast_to([0.7], si_rate.shape)
187
+
188
+ return si_rate, ir_rate
189
+
190
+ # create an instance of the model
191
+ epi_model = ContinuousTimeStateTransitionModel(
192
+ transition_rate_fn=rate_fn,
193
+ incidence_matrix=incidence_matrix,
194
+ initial_state=initial_population,
195
+ num_events=NUM_EVENTS,
196
+ initial_time=0.0,
197
+ )
198
+
199
+ log_lik = epi_model.log_prob(example_ilm["event_list"])
200
+ # hand calculated log likelihood
201
+ actual_loglik = -7.256319192936088
202
+
203
+ np.testing.assert_almost_equal(log_lik, desired=actual_loglik, decimal=5)
204
+
205
+
206
+ def test_simple_sir_loglik_graph_mode(example_ilm):
207
+ """Test loglikelihood function"""
208
+ # epi constants
209
+ incidence_matrix = example_ilm["incidence_matrix"]
210
+ initial_population = example_ilm["initial_conditions"]
211
+
212
+ def rate_fn(t, state):
213
+ si_rate = tf.broadcast_to([0.5], [state.shape[0]])
214
+ ir_rate = tf.broadcast_to([0.7], si_rate.shape)
215
+
216
+ return si_rate, ir_rate
217
+
218
+ # create an instance of the model
219
+ epi_model = ContinuousTimeStateTransitionModel(
220
+ transition_rate_fn=rate_fn,
221
+ incidence_matrix=incidence_matrix,
222
+ initial_state=initial_population,
223
+ num_events=NUM_EVENTS,
224
+ initial_time=0.0,
225
+ )
226
+
227
+ @tf.function
228
+ def fn():
229
+ log_lik = epi_model.log_prob(example_ilm["event_list"])
230
+ return log_lik
231
+
232
+ log_lik = fn()
233
+ # hand calculated log likelihood
234
+ actual_loglik = -7.256319192936088
235
+
236
+ np.testing.assert_almost_equal(log_lik, desired=actual_loglik, decimal=5)
237
+
238
+
239
+ def test_simple_sir_workflow(simple_sir_model):
240
+ """Using an instance of the ContinuousTimeStateTransitionModel"""
241
+
242
+ # sample from the model
243
+ sample_epi_path = simple_sir_model.sample(seed=[20240820, 1347])
244
+
245
+ # maximize the likelihood to estimate the parameters of the model
246
+ def make_rate_fn(rate_parameters):
247
+ SI_rate = rate_parameters[0]
248
+ IR_rate = rate_parameters[1]
249
+
250
+ def rate_fn(t, state):
251
+ si_rate = SI_rate * state[:, 1] / tf.reduce_sum(state, axis=-1)
252
+ ir_rate = tf.broadcast_to([IR_rate], si_rate.shape)
253
+
254
+ return si_rate, ir_rate
255
+
256
+ return rate_fn
257
+
258
+ def mle_fn(log_rate_parameters):
259
+ rate_parameters = tf.math.exp(log_rate_parameters)
260
+ rate_fn = make_rate_fn(rate_parameters)
261
+
262
+ model = ContinuousTimeStateTransitionModel(
263
+ transition_rate_fn=rate_fn,
264
+ incidence_matrix=simple_sir_model.incidence_matrix,
265
+ initial_state=simple_sir_model.initial_state,
266
+ num_events=NUM_EVENTS,
267
+ initial_time=0.0,
268
+ )
269
+
270
+ log_lik = model.log_prob(sample_epi_path)
271
+ return -log_lik
272
+
273
+ initial_parameters = np.array([-0.1, -0.1], dtype=np.float32)
274
+ opt = minimize(
275
+ mle_fn,
276
+ initial_parameters,
277
+ method="L-BFGS-B",
278
+ jac="3-point",
279
+ options={"finite_diff_rel_step": None},
280
+ )
281
+
282
+ std_errors = np.sqrt(np.diagonal(opt.hess_inv.todense()))
283
+ lower_ci = np.exp(opt.x - 1.96 * std_errors) # 95% CI
284
+ upper_ci = np.exp(opt.x + 1.96 * std_errors)
285
+
286
+ actuals = np.array([0.25, 0.14])
287
+
288
+ assert opt.success
289
+ assert np.all((lower_ci < actuals) & (actuals < upper_ci))