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,524 @@
|
|
|
1
|
+
"""Chain binomial process rippler algorithm"""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
|
|
6
|
+
import tensorflow as tf
|
|
7
|
+
import tensorflow_probability as tfp
|
|
8
|
+
from tensorflow_probability.python.internal import prefer_static, samplers
|
|
9
|
+
from tensorflow_probability.python.mcmc.internal import util as mcmc_util
|
|
10
|
+
|
|
11
|
+
from gemlib.distributions import Hypergeometric, UniformInteger
|
|
12
|
+
|
|
13
|
+
tfd = tfp.distributions
|
|
14
|
+
|
|
15
|
+
__all__ = ["chain_binomial_rippler", "default_initial_ripple"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _compute_state(initial_state, events, stoichiometry, closed=False):
|
|
19
|
+
"""Computes a state tensor from initial state and event tensor
|
|
20
|
+
|
|
21
|
+
:param initial_state: a tensor of shape [S, M]
|
|
22
|
+
:param events: a tensor of shape [T, R, M]
|
|
23
|
+
:param stoichiometry: a stoichiometry matrix of shape [R, S] describing
|
|
24
|
+
how transitions update the state.
|
|
25
|
+
:param closed: if `True`, return state in close interval [0, T], otherwise
|
|
26
|
+
[0, T)
|
|
27
|
+
:return: a tensor of shape [T, S, M] if `closed=False` or [T+1, S, M] if
|
|
28
|
+
`closed=True`
|
|
29
|
+
describing the state of the
|
|
30
|
+
system for each batch M at time T.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(stoichiometry, tf.Tensor):
|
|
33
|
+
stoichiometry = prefer_static.cast(stoichiometry, dtype=events.dtype)
|
|
34
|
+
else:
|
|
35
|
+
stoichiometry = tf.convert_to_tensor(stoichiometry, dtype=events.dtype)
|
|
36
|
+
|
|
37
|
+
increments = tf.einsum("...trm,rs->...tsm", events, stoichiometry)
|
|
38
|
+
|
|
39
|
+
if closed is False:
|
|
40
|
+
cum_increments = tf.cumsum(increments, axis=-3, exclusive=True)
|
|
41
|
+
else:
|
|
42
|
+
cum_increments = tf.cumsum(increments, axis=-3, exclusive=False)
|
|
43
|
+
cum_increments = tf.concat(
|
|
44
|
+
[tf.zeros_like(cum_increments[..., 0:1, :, :]), cum_increments],
|
|
45
|
+
axis=-2,
|
|
46
|
+
)
|
|
47
|
+
state = cum_increments + tf.expand_dims(initial_state, axis=-3)
|
|
48
|
+
return state
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _log_factorial(x):
|
|
52
|
+
"""Computes x!"""
|
|
53
|
+
return tf.math.lgamma(x + 1.0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _log_choose(n, k):
|
|
57
|
+
"""Computes nCk"""
|
|
58
|
+
return _log_factorial(n) - _log_factorial(k) - _log_factorial(n - k)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Basic rippler sampling functions
|
|
62
|
+
def _pstep(z, x, p, ps, seed=None, validate_args=False):
|
|
63
|
+
"""Compute the p-step of Rippler.
|
|
64
|
+
|
|
65
|
+
Since there are two possible distributions to draw from,
|
|
66
|
+
but both are Binomial, we compute `offset`, `total_count`, and
|
|
67
|
+
`prob` parameters for both branches, and select which we need
|
|
68
|
+
based on p <= ps.
|
|
69
|
+
|
|
70
|
+
:param z: current $z$
|
|
71
|
+
:param x: current $x$
|
|
72
|
+
:param p: current probability
|
|
73
|
+
:param ps: new probability
|
|
74
|
+
"""
|
|
75
|
+
with tf.name_scope("_pstep"):
|
|
76
|
+
offset = tf.where(ps <= p, tf.zeros(1, dtype=z.dtype), z)
|
|
77
|
+
prob = tf.where(ps <= p, ps / p, 1 - (1 - ps) / (1 - p))
|
|
78
|
+
total_count = tf.where(
|
|
79
|
+
ps <= p, tf.cast(z, p.dtype), tf.cast(x - z, p.dtype)
|
|
80
|
+
)
|
|
81
|
+
z_prime = offset + tfd.Binomial(
|
|
82
|
+
total_count=total_count,
|
|
83
|
+
probs=prob,
|
|
84
|
+
validate_args=validate_args,
|
|
85
|
+
name="_pstep_Binomial",
|
|
86
|
+
).sample(seed=seed)
|
|
87
|
+
return z_prime
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _xstep(z_prime, x, xs, ps, seed=None, validate_args=False):
|
|
91
|
+
"""Computes the x-step of the Rippler algorithm.
|
|
92
|
+
|
|
93
|
+
Both xs >= x and xs < x are sampled and results selected.
|
|
94
|
+
"""
|
|
95
|
+
with tf.name_scope("_xstep"):
|
|
96
|
+
seeds = samplers.split_seed(seed, salt="_xstep")
|
|
97
|
+
|
|
98
|
+
# xs >= x
|
|
99
|
+
# Switch off `validate_args` because `xs-x` may be -ve.
|
|
100
|
+
z_new_geq = z_prime + tfd.Binomial(
|
|
101
|
+
xs - x,
|
|
102
|
+
probs=ps,
|
|
103
|
+
validate_args=False,
|
|
104
|
+
name="_xstep_Binomial",
|
|
105
|
+
).sample(seed=seeds[0])
|
|
106
|
+
|
|
107
|
+
# xs < x - explicitly vectorize
|
|
108
|
+
def safe_hypergeom(N, K, n): # noqa: N803
|
|
109
|
+
# xs is clipped to min(x, xs) to avoid errors in the Hypergeometric
|
|
110
|
+
# sampler these values won't be selected anyway due to the
|
|
111
|
+
# xs >= x condition below.
|
|
112
|
+
return Hypergeometric(
|
|
113
|
+
N=N,
|
|
114
|
+
K=K,
|
|
115
|
+
n=tf.math.minimum(N, n),
|
|
116
|
+
validate_args=False,
|
|
117
|
+
name="_xstep_Hypergeom",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
z_new_lt = safe_hypergeom(x, z_prime, xs).sample(seed=seeds[1])
|
|
121
|
+
|
|
122
|
+
return tf.where(xs >= x, z_new_geq, z_new_lt)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _dispatch_update(z, x, p, xs, ps, seed=None, validate_args=False):
|
|
126
|
+
r"""Dispatches update function based on values of
|
|
127
|
+
parameters.
|
|
128
|
+
|
|
129
|
+
:param z: current $z$
|
|
130
|
+
:param x: current $x$
|
|
131
|
+
:param p: $p$ current probability
|
|
132
|
+
:param xs: $x^\star$ new state
|
|
133
|
+
:param ps: $p_star$ new probability
|
|
134
|
+
|
|
135
|
+
:returns: an updated number of events
|
|
136
|
+
"""
|
|
137
|
+
with tf.name_scope("dispatch_update"):
|
|
138
|
+
p = tf.convert_to_tensor(p)
|
|
139
|
+
ps = tf.convert_to_tensor(ps)
|
|
140
|
+
z = tf.cast(z, p.dtype)
|
|
141
|
+
x = tf.cast(x, p.dtype)
|
|
142
|
+
xs = tf.cast(xs, p.dtype)
|
|
143
|
+
|
|
144
|
+
seeds = samplers.split_seed(seed, salt="_dispatch_update")
|
|
145
|
+
|
|
146
|
+
z_prime = _pstep(z, x, p, ps, seed=seeds[0])
|
|
147
|
+
z_new = _xstep(
|
|
148
|
+
z_prime, x, xs, ps, seed=seeds[1], validate_args=validate_args
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return z_new
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Tests
|
|
155
|
+
def test_dispatch():
|
|
156
|
+
tf.debugging.assert_scalar(
|
|
157
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=100, ps=0.1)
|
|
158
|
+
)
|
|
159
|
+
tf.debugging.assert_scalar(
|
|
160
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=100, ps=0.05)
|
|
161
|
+
)
|
|
162
|
+
tf.debugging.assert_scalar(
|
|
163
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=100, ps=0.2)
|
|
164
|
+
)
|
|
165
|
+
tf.debugging.assert_scalar(
|
|
166
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=50, ps=0.1)
|
|
167
|
+
)
|
|
168
|
+
tf.debugging.assert_scalar(
|
|
169
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=50, ps=0.05)
|
|
170
|
+
)
|
|
171
|
+
tf.debugging.assert_scalar(
|
|
172
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=50, ps=0.2)
|
|
173
|
+
)
|
|
174
|
+
tf.debugging.assert_scalar(
|
|
175
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=200, ps=0.1)
|
|
176
|
+
)
|
|
177
|
+
tf.debugging.assert_scalar(
|
|
178
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=200, ps=0.05)
|
|
179
|
+
)
|
|
180
|
+
tf.debugging.assert_scalar(
|
|
181
|
+
_dispatch_update(z=10, x=100, p=0.1, xs=200, ps=0.2)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def default_initial_ripple(model, current_events, current_state, seed):
|
|
186
|
+
"""Produces the initial ripple.
|
|
187
|
+
|
|
188
|
+
:param model: an instance of `DiscreteTimeStateTransitionModel`
|
|
189
|
+
:param current_events: a tensor of events in [T, R, M] order
|
|
190
|
+
:param current_state: a tensor of state in [T, S, M] order
|
|
191
|
+
:param seed: the seed to initialise the ripple
|
|
192
|
+
|
|
193
|
+
:returns: a tuple of `(proposed_time_idx, new_events_t, current_state_t)`
|
|
194
|
+
"""
|
|
195
|
+
init_time_seed, init_pop_seed, init_events_seed = samplers.split_seed(
|
|
196
|
+
seed, n=3, salt="_initial_ripple"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Choose timepoint, t
|
|
200
|
+
proposed_time_idx = UniformInteger(low=0, high=model.num_steps).sample(
|
|
201
|
+
seed=init_time_seed
|
|
202
|
+
)
|
|
203
|
+
current_state_t = tf.gather(current_state, proposed_time_idx, axis=-3)
|
|
204
|
+
|
|
205
|
+
# Choose subpopulation - KCategorical?
|
|
206
|
+
proposed_pop_idx = UniformInteger(
|
|
207
|
+
low=0, high=current_events.shape[-1]
|
|
208
|
+
).sample(seed=init_pop_seed)
|
|
209
|
+
|
|
210
|
+
# Choose new infection events at time t
|
|
211
|
+
proposed_transition_rates = tf.stack(
|
|
212
|
+
model.transition_rates(
|
|
213
|
+
proposed_time_idx, tf.transpose(current_state_t)
|
|
214
|
+
),
|
|
215
|
+
axis=0,
|
|
216
|
+
)
|
|
217
|
+
prob_t = 1.0 - tf.math.exp(
|
|
218
|
+
-tf.gather(proposed_transition_rates[0], proposed_pop_idx, axis=-1)
|
|
219
|
+
* model.time_delta,
|
|
220
|
+
) # First event to perturb.
|
|
221
|
+
|
|
222
|
+
required_state = tf.gather(current_state_t[0], proposed_pop_idx, axis=-1)
|
|
223
|
+
new_si_events_t = tfd.Binomial(
|
|
224
|
+
total_count=required_state,
|
|
225
|
+
probs=prob_t, # Perturb SI events here
|
|
226
|
+
).sample(seed=init_events_seed)
|
|
227
|
+
|
|
228
|
+
new_events_t = tf.tensor_scatter_nd_update(
|
|
229
|
+
current_events[proposed_time_idx],
|
|
230
|
+
[[0, proposed_pop_idx]],
|
|
231
|
+
[new_si_events_t],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return proposed_time_idx, new_events_t, current_state_t
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def chain_binomial_rippler(model, current_events, initial_ripple_fn, seed=None):
|
|
238
|
+
init_seed, ripple_seed = samplers.split_seed(
|
|
239
|
+
seed, salt="chain_binomial_rippler"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
src_states = model.source_states
|
|
243
|
+
# Transpose to [T, S/R, M]
|
|
244
|
+
current_events = tf.transpose(current_events, perm=(1, 2, 0))
|
|
245
|
+
|
|
246
|
+
# Calculate current state
|
|
247
|
+
current_state = _compute_state(
|
|
248
|
+
initial_state=tf.transpose(model.initial_state),
|
|
249
|
+
events=current_events,
|
|
250
|
+
stoichiometry=tf.transpose(model.incidence_matrix),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Begin the ripple by sampling a time point, and perturbing the
|
|
254
|
+
# events at that timepoint
|
|
255
|
+
(
|
|
256
|
+
proposed_time_idx,
|
|
257
|
+
new_events_t,
|
|
258
|
+
current_state_t,
|
|
259
|
+
) = initial_ripple_fn(model, current_events, current_state, init_seed)
|
|
260
|
+
new_events = tf.tensor_scatter_nd_update(
|
|
261
|
+
current_events, indices=[[proposed_time_idx]], updates=[new_events_t]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Propagate from t+1 up to end of the timeseries
|
|
265
|
+
def draw_events(time, new_state_t, current_events_t, current_state_t, seed):
|
|
266
|
+
with tf.name_scope("draw_events"):
|
|
267
|
+
# Calculate transition rates for current and new states
|
|
268
|
+
def transition_probs(time, state):
|
|
269
|
+
rates = tf.stack(
|
|
270
|
+
model.transition_rates(time, tf.transpose(state)), axis=-2
|
|
271
|
+
)
|
|
272
|
+
return 1.0 - tf.math.exp(-rates * model.time_delta)
|
|
273
|
+
|
|
274
|
+
current_p = transition_probs(time, current_state_t)
|
|
275
|
+
new_p = transition_probs(time, new_state_t)
|
|
276
|
+
|
|
277
|
+
new_events = _dispatch_update(
|
|
278
|
+
z=current_events_t,
|
|
279
|
+
x=prefer_static.gather(current_state_t, indices=src_states),
|
|
280
|
+
p=current_p,
|
|
281
|
+
xs=prefer_static.gather(new_state_t, indices=src_states),
|
|
282
|
+
ps=new_p,
|
|
283
|
+
seed=seed,
|
|
284
|
+
)
|
|
285
|
+
tf.debugging.assert_non_negative(new_events)
|
|
286
|
+
|
|
287
|
+
return new_events
|
|
288
|
+
|
|
289
|
+
def time_loop_body(t, new_events_t, new_state_t, new_events_buffer, seed):
|
|
290
|
+
sample_seed, next_seed = samplers.split_seed(
|
|
291
|
+
seed, salt="time_loop_body"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Propagate new_state[t] to new_state[t+1]
|
|
295
|
+
new_state_t1 = new_state_t + tf.einsum(
|
|
296
|
+
"...ik,ji->...jk", new_events_t, model.incidence_matrix
|
|
297
|
+
)
|
|
298
|
+
# tf.debugging.assert_non_negative(new_state_t1, summarize=100)
|
|
299
|
+
|
|
300
|
+
# Gather current states and events, and draw new events
|
|
301
|
+
new_events_t1 = draw_events(
|
|
302
|
+
t + 1,
|
|
303
|
+
new_state_t1,
|
|
304
|
+
current_events[t + 1],
|
|
305
|
+
current_state[t + 1],
|
|
306
|
+
sample_seed,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Update new_events_buffer
|
|
310
|
+
new_events_buffer = tf.tensor_scatter_nd_update(
|
|
311
|
+
new_events_buffer, indices=[[t + 1]], updates=[new_events_t1]
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return t + 1, new_events_t1, new_state_t1, new_events_buffer, next_seed
|
|
315
|
+
|
|
316
|
+
def time_loop_cond(t, _1, _2, new_events_buffer, _3):
|
|
317
|
+
t_stop = t < (model.num_steps - 1)
|
|
318
|
+
delta_stop = tf.reduce_any(new_events_buffer != current_events)
|
|
319
|
+
return t_stop & delta_stop
|
|
320
|
+
|
|
321
|
+
_, _, _, new_events, _ = tf.while_loop(
|
|
322
|
+
time_loop_cond,
|
|
323
|
+
time_loop_body,
|
|
324
|
+
loop_vars=(
|
|
325
|
+
proposed_time_idx,
|
|
326
|
+
new_events_t,
|
|
327
|
+
current_state_t,
|
|
328
|
+
new_events,
|
|
329
|
+
ripple_seed,
|
|
330
|
+
),
|
|
331
|
+
) # new_events.shape = [T, R, M]
|
|
332
|
+
|
|
333
|
+
new_events = tf.transpose(new_events, perm=(2, 0, 1))
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
new_events,
|
|
337
|
+
{
|
|
338
|
+
"delta": tf.transpose(
|
|
339
|
+
new_events_t - current_events[proposed_time_idx]
|
|
340
|
+
),
|
|
341
|
+
"timepoint": proposed_time_idx,
|
|
342
|
+
"initial_ripple": new_events_t,
|
|
343
|
+
"current_state_t": tf.transpose(current_state_t),
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# The Chain Binomial Rippler kernel
|
|
349
|
+
CBRResults = namedtuple(
|
|
350
|
+
"CBRResults",
|
|
351
|
+
[
|
|
352
|
+
"target_log_prob",
|
|
353
|
+
"is_accepted",
|
|
354
|
+
"delta",
|
|
355
|
+
"current_state_t",
|
|
356
|
+
"initial_ripple",
|
|
357
|
+
"timepoint",
|
|
358
|
+
"proposed_state",
|
|
359
|
+
"proposed_target_log_prob",
|
|
360
|
+
"seed",
|
|
361
|
+
],
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class CBRKernel(tfp.mcmc.TransitionKernel):
|
|
366
|
+
def __init__(
|
|
367
|
+
self,
|
|
368
|
+
target_log_prob_fn,
|
|
369
|
+
model,
|
|
370
|
+
initial_ripple_fn=default_initial_ripple,
|
|
371
|
+
ripple_decay_rate=0.1,
|
|
372
|
+
name=None,
|
|
373
|
+
):
|
|
374
|
+
self._target_log_prob_fn = target_log_prob_fn
|
|
375
|
+
self._model = model
|
|
376
|
+
|
|
377
|
+
name = mcmc_util.make_name(name, "CBRKernel", "")
|
|
378
|
+
|
|
379
|
+
self._parameters = {
|
|
380
|
+
"target_log_prob_fn": target_log_prob_fn,
|
|
381
|
+
"model": model,
|
|
382
|
+
"initial_ripple_fn": initial_ripple_fn,
|
|
383
|
+
"ripple_decay_rate": ripple_decay_rate,
|
|
384
|
+
"name": name,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def is_calibrated(self):
|
|
389
|
+
return True
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def target_log_prob(self):
|
|
393
|
+
return self._target_log_prob_fn
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def model(self):
|
|
397
|
+
return self._model
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def name(self):
|
|
401
|
+
return self._parameters["name"]
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def initial_ripple_fn(self):
|
|
405
|
+
return self._parameters["initial_ripple_fn"]
|
|
406
|
+
|
|
407
|
+
def one_step(self, current_state, previous_results, seed=None):
|
|
408
|
+
with tf.name_scope("CBRKernel/one_step"):
|
|
409
|
+
seed_rippler, seed_u, seed_results = samplers.split_seed(
|
|
410
|
+
seed, n=3, salt="cbr_kernel"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if mcmc_util.is_list_like(current_state):
|
|
414
|
+
current_state_parts = list(current_state)
|
|
415
|
+
else:
|
|
416
|
+
current_state_parts = [current_state]
|
|
417
|
+
|
|
418
|
+
if len(current_state_parts) > 1:
|
|
419
|
+
warnings.warn(
|
|
420
|
+
"CBRKernel.boostrap_results: multiple state parts detected,\
|
|
421
|
+
but only the first will be used",
|
|
422
|
+
stacklevel=2,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
current_state_part = tf.convert_to_tensor(
|
|
426
|
+
current_state_parts[0], name="current_state"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
proposed_state, proposal_trace = chain_binomial_rippler(
|
|
430
|
+
self.model,
|
|
431
|
+
current_state_part,
|
|
432
|
+
initial_ripple_fn=self.initial_ripple_fn,
|
|
433
|
+
seed=seed_rippler,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
proposed_target_log_prob = self.target_log_prob(proposed_state)
|
|
437
|
+
|
|
438
|
+
delta_logp = (
|
|
439
|
+
proposed_target_log_prob - previous_results.target_log_prob
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def accept():
|
|
443
|
+
return (
|
|
444
|
+
proposed_state,
|
|
445
|
+
CBRResults(
|
|
446
|
+
target_log_prob=proposed_target_log_prob,
|
|
447
|
+
is_accepted=tf.constant(True),
|
|
448
|
+
delta=proposal_trace["delta"],
|
|
449
|
+
current_state_t=proposal_trace["current_state_t"],
|
|
450
|
+
initial_ripple=proposal_trace["initial_ripple"],
|
|
451
|
+
timepoint=proposal_trace["timepoint"],
|
|
452
|
+
proposed_state=proposed_state,
|
|
453
|
+
proposed_target_log_prob=proposed_target_log_prob,
|
|
454
|
+
seed=seed_results,
|
|
455
|
+
),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def reject():
|
|
459
|
+
return (
|
|
460
|
+
current_state_part,
|
|
461
|
+
CBRResults(
|
|
462
|
+
target_log_prob=previous_results.target_log_prob,
|
|
463
|
+
is_accepted=tf.constant(False),
|
|
464
|
+
delta=proposal_trace["delta"],
|
|
465
|
+
current_state_t=proposal_trace["current_state_t"],
|
|
466
|
+
initial_ripple=proposal_trace["initial_ripple"],
|
|
467
|
+
timepoint=proposal_trace["timepoint"],
|
|
468
|
+
proposed_state=proposed_state,
|
|
469
|
+
proposed_target_log_prob=proposed_target_log_prob,
|
|
470
|
+
seed=seed_results,
|
|
471
|
+
),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
u = tf.math.log(
|
|
475
|
+
tfd.Uniform(low=tf.zeros(1, dtype=delta_logp.dtype)).sample(
|
|
476
|
+
seed=seed_u
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
new_state, results = tf.cond(u < delta_logp, accept, reject)
|
|
480
|
+
|
|
481
|
+
def maybe_flatten(x):
|
|
482
|
+
if mcmc_util.is_list_like(current_state):
|
|
483
|
+
return type(current_state)(new_state)
|
|
484
|
+
return x
|
|
485
|
+
|
|
486
|
+
new_state = maybe_flatten(new_state)
|
|
487
|
+
return new_state, results
|
|
488
|
+
|
|
489
|
+
def bootstrap_results(self, current_state):
|
|
490
|
+
with tf.name_scope("CBRKernel/bootstrap_results"):
|
|
491
|
+
if mcmc_util.is_list_like(current_state):
|
|
492
|
+
current_state_parts = list(current_state)
|
|
493
|
+
else:
|
|
494
|
+
current_state_parts = [current_state]
|
|
495
|
+
|
|
496
|
+
if len(current_state_parts) > 1:
|
|
497
|
+
warnings.warn(
|
|
498
|
+
"CBRKernel.boostrap_results: multiple state parts detected,\
|
|
499
|
+
but only the first will be used",
|
|
500
|
+
stacklevel=2,
|
|
501
|
+
)
|
|
502
|
+
state_part = current_state_parts[0]
|
|
503
|
+
|
|
504
|
+
num_pop = state_part.shape[-3]
|
|
505
|
+
num_transitions = state_part.shape[-1]
|
|
506
|
+
num_states = self.model.incidence_matrix.shape[-2]
|
|
507
|
+
|
|
508
|
+
target_log_prob = self.target_log_prob(state_part)
|
|
509
|
+
|
|
510
|
+
return CBRResults(
|
|
511
|
+
target_log_prob=target_log_prob,
|
|
512
|
+
is_accepted=tf.constant(False),
|
|
513
|
+
delta=tf.zeros((num_pop, num_transitions), state_part.dtype),
|
|
514
|
+
current_state_t=tf.zeros(
|
|
515
|
+
[num_pop, num_states], state_part.dtype
|
|
516
|
+
),
|
|
517
|
+
initial_ripple=tf.zeros(
|
|
518
|
+
(num_transitions, num_pop), state_part.dtype
|
|
519
|
+
),
|
|
520
|
+
timepoint=tf.constant(0, dtype=tf.int32),
|
|
521
|
+
proposed_state=state_part,
|
|
522
|
+
proposed_target_log_prob=target_log_prob,
|
|
523
|
+
seed=samplers.zeros_seed(),
|
|
524
|
+
)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Test for Chain Binomial Rippler Kernel"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import tensorflow as tf
|
|
5
|
+
import tensorflow_probability as tfp
|
|
6
|
+
from tensorflow_probability.python.internal import test_util
|
|
7
|
+
|
|
8
|
+
from gemlib.distributions import DiscreteTimeStateTransitionModel
|
|
9
|
+
from gemlib.mcmc import CBRKernel
|
|
10
|
+
|
|
11
|
+
tfd = tfp.distributions
|
|
12
|
+
|
|
13
|
+
MIN_EVENTS = 10
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _make_model():
|
|
17
|
+
incidence_matrix = np.array([[-1, 1, 0], [0, -1, 1]], dtype=np.float32)
|
|
18
|
+
init_state = np.array(
|
|
19
|
+
[[999.0, 1.0, 0.0], [1000.0, 0.0, 0.0]], dtype=np.float32
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
contact_matrix = np.array([[0, 1], [1, 0]], dtype=np.float32)
|
|
23
|
+
|
|
24
|
+
init_state = tf.convert_to_tensor(init_state)
|
|
25
|
+
contact_matrix = tf.convert_to_tensor(contact_matrix)
|
|
26
|
+
|
|
27
|
+
def hazard_fn(t, state):
|
|
28
|
+
si = (
|
|
29
|
+
0.3 * state[..., 1]
|
|
30
|
+
+ 0.1 * tf.linalg.matvec(contact_matrix, state[..., 1])
|
|
31
|
+
) / tf.reduce_sum(state, axis=-1)
|
|
32
|
+
ir = tf.broadcast_to(tf.constant([0.14]), si.shape)
|
|
33
|
+
return [si, ir]
|
|
34
|
+
|
|
35
|
+
model = DiscreteTimeStateTransitionModel(
|
|
36
|
+
transition_rates=hazard_fn,
|
|
37
|
+
initial_state=init_state,
|
|
38
|
+
initial_step=0,
|
|
39
|
+
time_delta=1.0,
|
|
40
|
+
num_steps=70,
|
|
41
|
+
stoichiometry=incidence_matrix,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return model
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@test_util.test_all_tf_execution_regimes
|
|
48
|
+
class CBRSIRTest(test_util.TestCase):
|
|
49
|
+
def setUp(self):
|
|
50
|
+
self.incidence_matrix = np.array(
|
|
51
|
+
[[-1, 0], [1, -1], [0, 1]], dtype=np.float32
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self.init_state = np.array(
|
|
55
|
+
[[999.0, 1.0, 0.0], [1000.0, 0.0, 0.0]], dtype=np.float32
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.C = np.array([[0, 1], [1, 0]], dtype=np.float32)
|
|
59
|
+
|
|
60
|
+
model = self._make_model(self.incidence_matrix, self.init_state, self.C)
|
|
61
|
+
|
|
62
|
+
while True:
|
|
63
|
+
events = self.evaluate(model.sample())
|
|
64
|
+
if tf.reduce_sum(events[..., 1]) > MIN_EVENTS:
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
observation_process = tfd.Independent(
|
|
68
|
+
distribution=tfd.Binomial(total_count=events[..., 1], probs=0.5),
|
|
69
|
+
reinterpreted_batch_ndims=1,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.sir = events
|
|
73
|
+
self.observed_cases = self.evaluate(
|
|
74
|
+
observation_process.sample(seed=[0, 0])
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _make_model(self, incidence_matrix, init_state, contact_matrix):
|
|
78
|
+
init_state = tf.convert_to_tensor(init_state)
|
|
79
|
+
contact_matrix = tf.convert_to_tensor(contact_matrix)
|
|
80
|
+
|
|
81
|
+
def hazard_fn(t, state):
|
|
82
|
+
si = (
|
|
83
|
+
0.3 * state[..., 1]
|
|
84
|
+
+ 0.1 * tf.linalg.matvec(contact_matrix, state[..., 1])
|
|
85
|
+
) / tf.reduce_sum(state, axis=-1)
|
|
86
|
+
ir = tf.broadcast_to(tf.constant([0.14]), si.shape)
|
|
87
|
+
return [si, ir]
|
|
88
|
+
|
|
89
|
+
model = DiscreteTimeStateTransitionModel(
|
|
90
|
+
transition_rates=hazard_fn,
|
|
91
|
+
initial_state=init_state,
|
|
92
|
+
initial_step=0,
|
|
93
|
+
time_delta=1.0,
|
|
94
|
+
num_steps=70,
|
|
95
|
+
incidence_matrix=incidence_matrix,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return model
|
|
99
|
+
|
|
100
|
+
def test_cbr_kernel(self):
|
|
101
|
+
model = self._make_model(
|
|
102
|
+
self.incidence_matrix,
|
|
103
|
+
self.init_state,
|
|
104
|
+
self.C,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def tlp_fn(current_events):
|
|
108
|
+
observation_process = tfd.Binomial(
|
|
109
|
+
total_count=tf.gather(current_events, indices=1, axis=-1),
|
|
110
|
+
probs=0.5,
|
|
111
|
+
)
|
|
112
|
+
return tf.reduce_sum(
|
|
113
|
+
observation_process.log_prob(self.observed_cases)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
kernel = CBRKernel(tlp_fn, model=model)
|
|
117
|
+
pkr = self.evaluate(
|
|
118
|
+
kernel.bootstrap_results(tf.convert_to_tensor(self.sir))
|
|
119
|
+
)
|
|
120
|
+
samples, results = kernel.one_step(self.sir, pkr)
|