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.
- gemlib/__init__.py +9 -0
- gemlib/distributions/__init__.py +26 -0
- gemlib/distributions/brownian.py +141 -0
- gemlib/distributions/categorical2.py +33 -0
- gemlib/distributions/continuous_markov.py +371 -0
- gemlib/distributions/continuous_time_state_transition_model.py +185 -0
- gemlib/distributions/continuous_time_state_transition_model_test.py +289 -0
- gemlib/distributions/discrete_markov.py +279 -0
- gemlib/distributions/discrete_rejection_sampling.py +149 -0
- gemlib/distributions/discrete_time_state_transition_model.py +324 -0
- gemlib/distributions/discrete_time_state_transition_model_examples.py +453 -0
- gemlib/distributions/discrete_time_state_transition_model_test.py +336 -0
- gemlib/distributions/experimental/__init__.py +7 -0
- gemlib/distributions/experimental/discrete_approx_cont_state_transition_model.py +194 -0
- gemlib/distributions/experimental/state_transition_marginal_model.py +352 -0
- gemlib/distributions/hypergeometric.py +144 -0
- gemlib/distributions/hypergeometric_sampler.py +103 -0
- gemlib/distributions/hypergeometric_test.py +46 -0
- gemlib/distributions/kcategorical.py +113 -0
- gemlib/distributions/kcategorical_test.py +52 -0
- gemlib/distributions/uniform_integer.py +169 -0
- gemlib/distributions/uniform_integer_test.py +55 -0
- gemlib/mcmc/__init__.py +23 -0
- gemlib/mcmc/adaptive_random_walk_metropolis.py +859 -0
- gemlib/mcmc/adaptive_random_walk_metropolis_test.py +144 -0
- gemlib/mcmc/bb_fixture.pkl +0 -0
- gemlib/mcmc/brownian_bridge_kernel.py +291 -0
- gemlib/mcmc/brownian_bridge_kernel_test.py +164 -0
- gemlib/mcmc/chain_binomial_rippler.py +524 -0
- gemlib/mcmc/chain_binomial_rippler_test.py +120 -0
- gemlib/mcmc/compound_kernel.py +156 -0
- gemlib/mcmc/conftest.py +4 -0
- gemlib/mcmc/damped_chain_binomial_rippler.py +840 -0
- gemlib/mcmc/discrete_time_state_transition_model/__init__.py +21 -0
- gemlib/mcmc/discrete_time_state_transition_model/event_time_proposal.py +275 -0
- gemlib/mcmc/discrete_time_state_transition_model/fixtures.py +56 -0
- gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_mh.py +258 -0
- gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_mh_test.py +108 -0
- gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_proposal.py +188 -0
- gemlib/mcmc/discrete_time_state_transition_model/left_censored_events_proposal_test.py +33 -0
- gemlib/mcmc/discrete_time_state_transition_model/move_events.py +239 -0
- gemlib/mcmc/discrete_time_state_transition_model/move_events_test.py +63 -0
- gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_mh.py +254 -0
- gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_mh_test.py +28 -0
- gemlib/mcmc/discrete_time_state_transition_model/right_censored_events_proposal.py +150 -0
- gemlib/mcmc/discrete_time_state_transition_model/util.py +9 -0
- gemlib/mcmc/experimental/__init__.py +0 -0
- gemlib/mcmc/experimental/composable_kernel.py +270 -0
- gemlib/mcmc/experimental/discrete_time_state_transition_model/__init__.py +1 -0
- gemlib/mcmc/experimental/discrete_time_state_transition_model/left_censored_events_mh.py +149 -0
- gemlib/mcmc/experimental/discrete_time_state_transition_model/move_events.py +71 -0
- gemlib/mcmc/experimental/discrete_time_state_transition_model/move_events_test.py +48 -0
- gemlib/mcmc/experimental/discrete_time_state_transition_model/right_censored_events_mh.py +89 -0
- gemlib/mcmc/experimental/discrete_time_state_transition_model/right_censored_events_mh_test.py +28 -0
- gemlib/mcmc/experimental/hmc.py +111 -0
- gemlib/mcmc/experimental/hmc_test.py +61 -0
- gemlib/mcmc/experimental/mcmc_base.py +33 -0
- gemlib/mcmc/experimental/mcmc_sampler.py +94 -0
- gemlib/mcmc/experimental/mcmc_sampler_test.py +44 -0
- gemlib/mcmc/experimental/multi_scan.py +53 -0
- gemlib/mcmc/experimental/multi_scan_test.py +71 -0
- gemlib/mcmc/experimental/random_walk_metropolis.py +107 -0
- gemlib/mcmc/experimental/random_walk_metropolis_test.py +214 -0
- gemlib/mcmc/experimental/test_util.py +49 -0
- gemlib/mcmc/gibbs_kernel.py +505 -0
- gemlib/mcmc/gibbs_kernel_test.py +212 -0
- gemlib/mcmc/h5_posterior.py +77 -0
- gemlib/mcmc/multi_scan_kernel.py +59 -0
- gemlib/mcmc/zarr_posterior.py +132 -0
- gemlib/util.py +117 -0
- gemlib/util_test.py +75 -0
- gemlib-0.9.2.dist-info/LICENSE +21 -0
- gemlib-0.9.2.dist-info/METADATA +19 -0
- gemlib-0.9.2.dist-info/RECORD +75 -0
- 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))
|