nomass 1.0.0__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.
- nomass/__init__.py +29 -0
- nomass/distributions.py +437 -0
- nomass/simulation.py +968 -0
- nomass-1.0.0.dist-info/METADATA +233 -0
- nomass-1.0.0.dist-info/RECORD +8 -0
- nomass-1.0.0.dist-info/WHEEL +5 -0
- nomass-1.0.0.dist-info/licenses/LICENSE +21 -0
- nomass-1.0.0.dist-info/top_level.txt +1 -0
nomass/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NoMaSS: Non-Markovian Stochastic Simulation.
|
|
3
|
+
|
|
4
|
+
A Gillespie-based framework to simulate reaction networks whose inter-event
|
|
5
|
+
times follow arbitrary distributions, mixing three exact methods per reaction
|
|
6
|
+
channel: MOSAIC (rejection), Laplace Gillespie and DelaySSA.
|
|
7
|
+
|
|
8
|
+
Quick start::
|
|
9
|
+
|
|
10
|
+
import nomass
|
|
11
|
+
|
|
12
|
+
r1 = nomass.Reaction_channel('A -> B', rate=1, shape_param=20, distribution='Gamma')
|
|
13
|
+
sim = nomass.Gillespie_simulation({'A': 300, 'B': 0}, [r1], Tend=10, N_simulations=20, unit='h')
|
|
14
|
+
populations = sim.run_simulations()
|
|
15
|
+
sim.plot_populations()
|
|
16
|
+
|
|
17
|
+
Reference: Pelissier A., Phan M., Beerenwinkel N., Rodriguez Martinez M.
|
|
18
|
+
"Gillespie-based simulation and inference for non-Markovian stochastic reaction
|
|
19
|
+
networks", Briefings in Bioinformatics 27(5), bbag486 (2026). doi:10.1093/bib/bbag486
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .simulation import Reaction_channel, Reactant, Gillespie_simulation, parse_reaction, PALETTE
|
|
23
|
+
from .distributions import DISTRIBUTIONS, METHODS
|
|
24
|
+
from . import distributions
|
|
25
|
+
|
|
26
|
+
__version__ = '1.0.0'
|
|
27
|
+
|
|
28
|
+
__all__ = ['Reaction_channel', 'Reactant', 'Gillespie_simulation', 'parse_reaction',
|
|
29
|
+
'distributions', 'DISTRIBUTIONS', 'METHODS', 'PALETTE', '__version__']
|
nomass/distributions.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inter-event time distributions supported by NoMaSS.
|
|
3
|
+
|
|
4
|
+
Every distribution is parametrised by a nominal ``rate`` (the inverse of the
|
|
5
|
+
mean waiting time, or of the median when the mean is undefined) and a
|
|
6
|
+
dimensionless ``shape`` parameter. This module provides, for each distribution:
|
|
7
|
+
|
|
8
|
+
* ``hazard(t, rate, shape)`` instantaneous rate at time ``t`` since the
|
|
9
|
+
reactant entered the channel (used by MOSAIC)
|
|
10
|
+
* ``pdf(t, rate, shape)`` theoretical density (used for plotting)
|
|
11
|
+
* ``sample(rate, shape, size)`` random waiting times (used by DelaySSA)
|
|
12
|
+
* ``laplace_rate(rate, shape)`` a random Poisson rate drawn from the mixing
|
|
13
|
+
density of the Laplace Gillespie algorithm
|
|
14
|
+
(only for completely monotone survival
|
|
15
|
+
functions)
|
|
16
|
+
|
|
17
|
+
See Table S1 of Pelissier et al., Brief. Bioinform. 27(5), 2026 for the
|
|
18
|
+
parametrisations.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from math import log, gamma as gamma_fn, exp, pi, sqrt, erf, atan, sin, inf, isfinite
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import scipy.stats as stat
|
|
25
|
+
from scipy.special import gammainc
|
|
26
|
+
|
|
27
|
+
# --------------------------------------------------------------------------
|
|
28
|
+
# Names
|
|
29
|
+
# --------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
_ALIASES = {
|
|
32
|
+
'exponential': 'Exponential', 'exp': 'Exponential', 'poisson': 'Exponential',
|
|
33
|
+
'gamma': 'Gamma', 'gam': 'Gamma',
|
|
34
|
+
'weibull': 'Weibull', 'weib': 'Weibull',
|
|
35
|
+
'gaussian': 'Gaussian', 'normal': 'Gaussian', 'norm': 'Gaussian',
|
|
36
|
+
'lognormal': 'Lognormal', 'lognorm': 'Lognormal', 'log-normal': 'Lognormal',
|
|
37
|
+
'cauchy': 'Cauchy', 'cau': 'Cauchy',
|
|
38
|
+
'pareto': 'Pareto', 'pareto1': 'Pareto', 'power-law': 'Pareto', 'powerlaw': 'Pareto',
|
|
39
|
+
'lomax': 'Lomax', 'pareto2': 'Lomax',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
DISTRIBUTIONS = ('Exponential', 'Gamma', 'Weibull', 'Gaussian', 'Lognormal',
|
|
43
|
+
'Cauchy', 'Pareto', 'Lomax')
|
|
44
|
+
|
|
45
|
+
METHODS = ('MOSAIC', 'Laplace', 'Delay')
|
|
46
|
+
|
|
47
|
+
_METHOD_ALIASES = {
|
|
48
|
+
'mosaic': 'MOSAIC', 'rejection': 'MOSAIC', 'regir': 'MOSAIC',
|
|
49
|
+
'laplace': 'Laplace', 'laplace gillespie': 'Laplace', 'lga': 'Laplace',
|
|
50
|
+
'delay': 'Delay', 'delayssa': 'Delay', 'delay-ssa': 'Delay', 'nrm': 'Delay',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def canonical_distribution(name):
|
|
55
|
+
"""Return the canonical distribution name for any accepted alias."""
|
|
56
|
+
key = str(name).strip().lower()
|
|
57
|
+
if key not in _ALIASES:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"Unsupported distribution '%s'. Supported: %s" % (name, ', '.join(DISTRIBUTIONS)))
|
|
60
|
+
return _ALIASES[key]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def canonical_method(name):
|
|
64
|
+
"""Return the canonical method name for any accepted alias."""
|
|
65
|
+
key = str(name).strip().lower()
|
|
66
|
+
if key not in _METHOD_ALIASES:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"Unsupported method '%s'. Supported: %s" % (name, ', '.join(METHODS)))
|
|
69
|
+
return _METHOD_ALIASES[key]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# --------------------------------------------------------------------------
|
|
73
|
+
# Parameter conversions (rate, shape) -> native scipy parameters
|
|
74
|
+
# --------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _gamma_params(rate, shape):
|
|
77
|
+
"""Gamma with mean 1/rate and shape alpha: returns (alpha, scale)."""
|
|
78
|
+
return shape, 1.0 / (shape * rate)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _weibull_params(rate, shape):
|
|
82
|
+
"""Weibull with mean 1/rate and shape k: returns (k, scale)."""
|
|
83
|
+
return shape, 1.0 / (rate * gamma_fn(1.0 + 1.0 / shape))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _gaussian_params(rate, shape):
|
|
87
|
+
"""Normal with mean 1/rate and std = shape * mean: returns (mu, sigma)."""
|
|
88
|
+
mu = 1.0 / rate
|
|
89
|
+
return mu, shape * mu
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _lognormal_params(rate, shape):
|
|
93
|
+
"""Lognormal with mean 1/rate and std = shape * mean: returns (mu, sigma) of log."""
|
|
94
|
+
sigma2 = log(1.0 + shape ** 2)
|
|
95
|
+
mu = log(1.0 / rate) - sigma2 / 2.0
|
|
96
|
+
return mu, sqrt(sigma2)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _cauchy_params(rate, shape):
|
|
100
|
+
"""Cauchy with median 1/rate and scale = shape * median: returns (x0, gamma)."""
|
|
101
|
+
x0 = 1.0 / rate
|
|
102
|
+
return x0, shape * x0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _pareto_params(rate, shape):
|
|
106
|
+
"""Pareto type I with median 1/rate and tail index alpha: returns (alpha, x_min)."""
|
|
107
|
+
return shape, 1.0 / (rate * 2.0 ** (1.0 / shape))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _lomax_params(rate, shape):
|
|
111
|
+
"""Lomax (Pareto II) with median 1/rate and tail index alpha: returns (alpha, kappa)."""
|
|
112
|
+
return shape, 1.0 / (rate * (2.0 ** (1.0 / shape) - 1.0))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def scipy_distribution(distribution, rate, shape):
|
|
116
|
+
"""Return a frozen ``scipy.stats`` distribution for (distribution, rate, shape).
|
|
117
|
+
|
|
118
|
+
Gaussian and Cauchy are truncated to non-negative waiting times, which is
|
|
119
|
+
the distribution actually produced by a hazard-based simulation.
|
|
120
|
+
"""
|
|
121
|
+
d = canonical_distribution(distribution)
|
|
122
|
+
if d == 'Exponential':
|
|
123
|
+
return stat.expon(scale=1.0 / rate)
|
|
124
|
+
if d == 'Gamma':
|
|
125
|
+
a, s = _gamma_params(rate, shape)
|
|
126
|
+
return stat.gamma(a, scale=s)
|
|
127
|
+
if d == 'Weibull':
|
|
128
|
+
k, s = _weibull_params(rate, shape)
|
|
129
|
+
return stat.weibull_min(k, scale=s)
|
|
130
|
+
if d == 'Gaussian':
|
|
131
|
+
mu, sigma = _gaussian_params(rate, shape)
|
|
132
|
+
return stat.truncnorm(-mu / sigma, inf, loc=mu, scale=sigma)
|
|
133
|
+
if d == 'Lognormal':
|
|
134
|
+
mu, sigma = _lognormal_params(rate, shape)
|
|
135
|
+
return stat.lognorm(sigma, scale=exp(mu))
|
|
136
|
+
if d == 'Cauchy':
|
|
137
|
+
x0, g = _cauchy_params(rate, shape)
|
|
138
|
+
return _TruncatedCauchy(x0, g)
|
|
139
|
+
if d == 'Pareto':
|
|
140
|
+
a, xm = _pareto_params(rate, shape)
|
|
141
|
+
return stat.pareto(a, scale=xm)
|
|
142
|
+
if d == 'Lomax':
|
|
143
|
+
a, kappa = _lomax_params(rate, shape)
|
|
144
|
+
return stat.lomax(a, scale=kappa)
|
|
145
|
+
raise ValueError(d)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class _TruncatedCauchy:
|
|
149
|
+
"""Cauchy distribution conditioned on t >= 0 (minimal frozen-distribution API)."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, x0, g):
|
|
152
|
+
self.x0, self.g = x0, g
|
|
153
|
+
self._F0 = stat.cauchy.cdf(0.0, loc=x0, scale=g)
|
|
154
|
+
self._norm = 1.0 - self._F0
|
|
155
|
+
|
|
156
|
+
def pdf(self, t):
|
|
157
|
+
t = np.asarray(t, dtype=float)
|
|
158
|
+
return np.where(t >= 0, stat.cauchy.pdf(t, self.x0, self.g) / self._norm, 0.0)
|
|
159
|
+
|
|
160
|
+
def sf(self, t):
|
|
161
|
+
t = np.asarray(t, dtype=float)
|
|
162
|
+
return np.where(t >= 0, stat.cauchy.sf(t, self.x0, self.g) / self._norm, 1.0)
|
|
163
|
+
|
|
164
|
+
def rvs(self, size=None, random_state=None):
|
|
165
|
+
rng = np.random if random_state is None else random_state
|
|
166
|
+
u = rng.uniform(self._F0, 1.0, size=size)
|
|
167
|
+
return stat.cauchy.ppf(u, self.x0, self.g)
|
|
168
|
+
|
|
169
|
+
def mean(self):
|
|
170
|
+
return inf
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# --------------------------------------------------------------------------
|
|
174
|
+
# Hazard functions h(t) = pdf(t) / survival(t)
|
|
175
|
+
# (fast closed forms; used at every MOSAIC step)
|
|
176
|
+
# --------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def exponential_hazard(t, rate, shape=None):
|
|
179
|
+
return rate
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def gamma_hazard(t, rate, shape):
|
|
183
|
+
alpha = shape
|
|
184
|
+
if t <= 0:
|
|
185
|
+
return 0.0 if alpha > 1 else (rate if alpha == 1 else inf)
|
|
186
|
+
beta = alpha * rate
|
|
187
|
+
pdf = (beta ** alpha) * (t ** (alpha - 1)) * exp(-beta * t) / gamma_fn(alpha)
|
|
188
|
+
sf = 1.0 - gammainc(alpha, beta * t)
|
|
189
|
+
return pdf / sf if sf > 0 else inf
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def weibull_hazard(t, rate, shape):
|
|
193
|
+
k, s = _weibull_params(rate, shape)
|
|
194
|
+
if t <= 0:
|
|
195
|
+
return 0.0 if k > 1 else (rate if k == 1 else inf)
|
|
196
|
+
return (k / s) * (t / s) ** (k - 1)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def gaussian_hazard(t, rate, shape):
|
|
200
|
+
mu, sigma = _gaussian_params(rate, shape)
|
|
201
|
+
if t > mu + 7 * sigma:
|
|
202
|
+
# asymptotic form: the cdf is numerically 1 in this region
|
|
203
|
+
return (t - mu) / sigma ** 2
|
|
204
|
+
pdf = exp(-0.5 * ((t - mu) / sigma) ** 2) / (sigma * sqrt(2 * pi))
|
|
205
|
+
sf = 0.5 * (1.0 - erf((t - mu) / (sigma * sqrt(2))))
|
|
206
|
+
return pdf / sf if sf > 0 else inf
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def lognormal_hazard(t, rate, shape):
|
|
210
|
+
if t <= 0:
|
|
211
|
+
return 0.0
|
|
212
|
+
mu, sigma = _lognormal_params(rate, shape)
|
|
213
|
+
z = (log(t) - mu) / sigma
|
|
214
|
+
pdf = exp(-0.5 * z * z) / (t * sigma * sqrt(2 * pi))
|
|
215
|
+
sf = 0.5 * (1.0 - erf(z / sqrt(2)))
|
|
216
|
+
return pdf / sf if sf > 0 else inf
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def cauchy_hazard(t, rate, shape):
|
|
220
|
+
x0, g = _cauchy_params(rate, shape)
|
|
221
|
+
pdf = 1.0 / (pi * g * (1.0 + ((t - x0) / g) ** 2))
|
|
222
|
+
sf = 0.5 - atan((t - x0) / g) / pi
|
|
223
|
+
return pdf / sf if sf > 0 else inf
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def pareto_hazard(t, rate, shape):
|
|
227
|
+
a, xm = _pareto_params(rate, shape)
|
|
228
|
+
return a / t if t >= xm else 0.0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def lomax_hazard(t, rate, shape):
|
|
232
|
+
a, kappa = _lomax_params(rate, shape)
|
|
233
|
+
return a / (kappa + max(t, 0.0))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
_HAZARDS = {
|
|
237
|
+
'Exponential': exponential_hazard,
|
|
238
|
+
'Gamma': gamma_hazard,
|
|
239
|
+
'Weibull': weibull_hazard,
|
|
240
|
+
'Gaussian': gaussian_hazard,
|
|
241
|
+
'Lognormal': lognormal_hazard,
|
|
242
|
+
'Cauchy': cauchy_hazard,
|
|
243
|
+
'Pareto': pareto_hazard,
|
|
244
|
+
'Lomax': lomax_hazard,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def hazard_function(distribution):
|
|
249
|
+
"""Return the hazard function ``h(t, rate, shape)`` of a distribution."""
|
|
250
|
+
return _HAZARDS[canonical_distribution(distribution)]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def hazard(t, distribution, rate, shape):
|
|
254
|
+
"""Evaluate the hazard with a slow but robust fallback for numerical overflow."""
|
|
255
|
+
h = _HAZARDS[canonical_distribution(distribution)](t, rate, shape)
|
|
256
|
+
if isfinite(h):
|
|
257
|
+
return h
|
|
258
|
+
frozen = scipy_distribution(distribution, rate, shape)
|
|
259
|
+
try:
|
|
260
|
+
h = float(np.exp(frozen.logpdf(t) - frozen.logsf(t)))
|
|
261
|
+
except AttributeError:
|
|
262
|
+
h = float(frozen.pdf(t) / frozen.sf(t))
|
|
263
|
+
return h
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# --------------------------------------------------------------------------
|
|
267
|
+
# Hazard shape information used by MOSAIC to bound the rate
|
|
268
|
+
# --------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def hazard_is_bounded(distribution, shape):
|
|
271
|
+
"""True if the hazard is finite for all t >= 0 (requirement of MOSAIC)."""
|
|
272
|
+
d = canonical_distribution(distribution)
|
|
273
|
+
if d in ('Gamma', 'Weibull'):
|
|
274
|
+
return shape >= 1
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def hazard_is_increasing(distribution, shape):
|
|
279
|
+
"""True if the hazard is monotonically non-decreasing in t.
|
|
280
|
+
|
|
281
|
+
For such distributions the maximum rate over a population is the hazard of
|
|
282
|
+
the reactant that has been waiting the longest. Lognormal hazards are not
|
|
283
|
+
monotone, but for a small relative standard deviation the maximum lies far
|
|
284
|
+
in the tail, so they are treated as increasing (see Pelissier et al.).
|
|
285
|
+
"""
|
|
286
|
+
d = canonical_distribution(distribution)
|
|
287
|
+
if d in ('Gamma', 'Weibull'):
|
|
288
|
+
return shape >= 1
|
|
289
|
+
if d == 'Gaussian':
|
|
290
|
+
return True
|
|
291
|
+
if d == 'Lognormal':
|
|
292
|
+
return shape < 0.25
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def hazard_upper_bound(distribution, rate, shape, Tend):
|
|
297
|
+
"""A fixed upper bound of the hazard on [0, Tend] for non-increasing hazards."""
|
|
298
|
+
d = canonical_distribution(distribution)
|
|
299
|
+
if d == 'Exponential':
|
|
300
|
+
return rate
|
|
301
|
+
if d == 'Pareto':
|
|
302
|
+
a, xm = _pareto_params(rate, shape)
|
|
303
|
+
return a / xm
|
|
304
|
+
if d == 'Lomax':
|
|
305
|
+
a, kappa = _lomax_params(rate, shape)
|
|
306
|
+
return a / kappa
|
|
307
|
+
# Cauchy, Lognormal with large shape: scan numerically
|
|
308
|
+
f = _HAZARDS[d]
|
|
309
|
+
t = np.linspace(0.0, Tend, num=1000)
|
|
310
|
+
values = np.array([f(ti, rate, shape) for ti in t])
|
|
311
|
+
values = values[np.isfinite(values)]
|
|
312
|
+
return float(np.max(values)) if len(values) else rate
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# --------------------------------------------------------------------------
|
|
316
|
+
# Laplace Gillespie: completely monotone survival functions as mixtures of
|
|
317
|
+
# exponentials. psi(tau) = int p(lambda) exp(-lambda tau) dlambda
|
|
318
|
+
# --------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
def laplace_supported(distribution, shape):
|
|
321
|
+
"""True if the distribution can be simulated with the Laplace Gillespie algorithm."""
|
|
322
|
+
d = canonical_distribution(distribution)
|
|
323
|
+
if d == 'Exponential' or d == 'Lomax':
|
|
324
|
+
return True
|
|
325
|
+
if d in ('Gamma', 'Weibull'):
|
|
326
|
+
return shape <= 1
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _positive_stable(alpha, rng):
|
|
331
|
+
"""Kanter's sampler of a positive stable variable S with E[exp(-u S)] = exp(-u^alpha)."""
|
|
332
|
+
u = rng.uniform(0.0, pi)
|
|
333
|
+
w = rng.exponential(1.0)
|
|
334
|
+
a = (sin((1.0 - alpha) * u) * sin(alpha * u) ** (alpha / (1.0 - alpha))
|
|
335
|
+
/ sin(u) ** (1.0 / (1.0 - alpha)))
|
|
336
|
+
return (a / w) ** ((1.0 - alpha) / alpha)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def laplace_rate(distribution, rate, shape, rng=None):
|
|
340
|
+
"""Draw a Poisson rate from the mixing density of the survival function.
|
|
341
|
+
|
|
342
|
+
* Exponential: deterministic ``rate``
|
|
343
|
+
* Gamma (alpha <= 1): lambda = beta / B, B ~ Beta(alpha, 1 - alpha), beta = alpha * rate
|
|
344
|
+
* Weibull (alpha <= 1): lambda = S / scale, S positive alpha-stable
|
|
345
|
+
* Lomax: lambda ~ Gamma(alpha, scale = 1 / kappa)
|
|
346
|
+
"""
|
|
347
|
+
rng = np.random if rng is None else rng
|
|
348
|
+
d = canonical_distribution(distribution)
|
|
349
|
+
if d == 'Exponential' or ((d in ('Gamma', 'Weibull')) and shape == 1):
|
|
350
|
+
return float(rate)
|
|
351
|
+
if d == 'Gamma':
|
|
352
|
+
if shape > 1:
|
|
353
|
+
raise ValueError('Laplace Gillespie requires a Gamma shape parameter <= 1')
|
|
354
|
+
beta = shape * rate
|
|
355
|
+
return beta / rng.beta(shape, 1.0 - shape)
|
|
356
|
+
if d == 'Weibull':
|
|
357
|
+
if shape > 1:
|
|
358
|
+
raise ValueError('Laplace Gillespie requires a Weibull shape parameter <= 1')
|
|
359
|
+
_, s = _weibull_params(rate, shape)
|
|
360
|
+
return _positive_stable(shape, rng) / s
|
|
361
|
+
if d == 'Lomax':
|
|
362
|
+
a, kappa = _lomax_params(rate, shape)
|
|
363
|
+
return rng.gamma(a, 1.0 / kappa)
|
|
364
|
+
raise ValueError("The Laplace Gillespie method does not support the %s distribution" % d)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# --------------------------------------------------------------------------
|
|
368
|
+
# Sampling and theoretical density
|
|
369
|
+
# --------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
def sample(distribution, rate, shape, size=None, rng=None):
|
|
372
|
+
"""Draw ``size`` waiting times (a float if ``size`` is None)."""
|
|
373
|
+
frozen = scipy_distribution(distribution, rate, shape)
|
|
374
|
+
out = frozen.rvs(size=1 if size is None else size, random_state=rng)
|
|
375
|
+
return float(out[0]) if size is None else np.asarray(out, dtype=float)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def pdf(t, distribution, rate, shape):
|
|
379
|
+
"""Theoretical probability density of the waiting time at ``t``."""
|
|
380
|
+
return scipy_distribution(distribution, rate, shape).pdf(np.asarray(t, dtype=float))
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def mean(distribution, rate, shape):
|
|
384
|
+
"""Theoretical mean waiting time (inf when undefined)."""
|
|
385
|
+
return float(scipy_distribution(distribution, rate, shape).mean())
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def fit(distribution, samples):
|
|
389
|
+
"""Fit the native scipy parameters to observed waiting times (for plotting)."""
|
|
390
|
+
d = canonical_distribution(distribution)
|
|
391
|
+
samples = np.asarray(samples, dtype=float)
|
|
392
|
+
if d == 'Exponential':
|
|
393
|
+
P = stat.expon.fit(samples, floc=0)
|
|
394
|
+
return P, lambda t: stat.expon.pdf(t, *P)
|
|
395
|
+
if d == 'Gamma':
|
|
396
|
+
P = stat.gamma.fit(samples, floc=0)
|
|
397
|
+
return P, lambda t: stat.gamma.pdf(t, *P)
|
|
398
|
+
if d == 'Weibull':
|
|
399
|
+
P = stat.weibull_min.fit(samples, floc=0)
|
|
400
|
+
return P, lambda t: stat.weibull_min.pdf(t, *P)
|
|
401
|
+
if d == 'Gaussian':
|
|
402
|
+
P = stat.norm.fit(samples)
|
|
403
|
+
return P, lambda t: stat.norm.pdf(t, *P)
|
|
404
|
+
if d == 'Lognormal':
|
|
405
|
+
P = stat.lognorm.fit(samples, floc=0)
|
|
406
|
+
return P, lambda t: stat.lognorm.pdf(t, *P)
|
|
407
|
+
if d == 'Cauchy':
|
|
408
|
+
P = stat.cauchy.fit(samples)
|
|
409
|
+
return P, lambda t: stat.cauchy.pdf(t, *P)
|
|
410
|
+
if d == 'Pareto':
|
|
411
|
+
P = stat.pareto.fit(samples, floc=0)
|
|
412
|
+
return P, lambda t: stat.pareto.pdf(t, *P)
|
|
413
|
+
if d == 'Lomax':
|
|
414
|
+
P = stat.lomax.fit(samples, floc=0)
|
|
415
|
+
return P, lambda t: stat.lomax.pdf(t, *P)
|
|
416
|
+
raise ValueError(d)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def default_method(distribution, shape):
|
|
420
|
+
"""Pick the recommended simulation method for a distribution.
|
|
421
|
+
|
|
422
|
+
MOSAIC whenever the hazard is bounded, Laplace Gillespie for the heavy
|
|
423
|
+
early-time hazards of Gamma/Weibull with shape < 1.
|
|
424
|
+
"""
|
|
425
|
+
if hazard_is_bounded(distribution, shape):
|
|
426
|
+
return 'MOSAIC'
|
|
427
|
+
return 'Laplace'
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def method_supported(method, distribution, shape):
|
|
431
|
+
"""Check whether ``method`` can simulate ``distribution`` with this shape."""
|
|
432
|
+
m = canonical_method(method)
|
|
433
|
+
if m == 'MOSAIC':
|
|
434
|
+
return hazard_is_bounded(distribution, shape)
|
|
435
|
+
if m == 'Laplace':
|
|
436
|
+
return laplace_supported(distribution, shape)
|
|
437
|
+
return True # Delay works for every distribution
|