-
Notifications
You must be signed in to change notification settings - Fork 320
Expand file tree
/
Copy pathtom.py
More file actions
382 lines (322 loc) · 13.3 KB
/
Copy pathtom.py
File metadata and controls
382 lines (322 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2012-2025 GEM Foundation
#
# OpenQuake is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OpenQuake is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with OpenQuake. If not, see <http://www.gnu.org/licenses/>.
"""
Module :mod:`openquake.hazardlib.tom` contains implementations of probability
density functions for earthquake temporal occurrence modeling.
"""
import toml
import numpy
import scipy.stats
from openquake.baselib.performance import compile
registry = {}
F64 = numpy.float64
class BaseTOM(object):
"""
Base class for temporal occurrence model.
:param time_span:
The time interval of interest, in years.
:raises ValueError:
If ``time_span`` is not positive.
"""
@classmethod
def __init_subclass__(cls):
registry[cls.__name__] = cls
def __init__(self, time_span):
if time_span <= 0:
raise ValueError('time_span must be positive')
self.time_span = time_span
def get_probability_one_or_more_occurrences(self):
"""
Calculate and return the probability of event to happen one or more
times within the time range defined by constructor's ``time_span``
parameter value.
"""
raise NotImplementedError
def get_probability_n_occurrences(self):
"""
Calculate the probability of occurrence of a number of events in the
constructor's ``time_span``.
"""
raise NotImplementedError
def sample_number_of_occurrences(self, seeds=None):
"""
Draw a random sample from the distribution and return a number
of events to occur.
The method uses the numpy random generator, which needs a seed
in order to get reproducible results. If the seed is None, it
should be set outside of this method.
"""
def get_probability_no_exceedance(self):
"""
Compute and return, for a number of ground motion levels and sites,
the probability that a rupture with annual occurrence rate given by
``occurrence_rate`` and able to cause ground motion values higher than
a given level at a site with probability ``poes``, does not cause any
exceedance in the time window specified by the ``time_span`` parameter
given in the constructor.
"""
raise NotImplementedError
def __str__(self):
return toml.dumps({self.__class__.__name__: self.__dict__})
class FatedTOM(BaseTOM):
def __init__(self, time_span):
self.time_span = time_span
def get_probability_one_or_more_occurrences(self, occurrence_rate):
return 1
def get_probability_n_occurrences(self, occurrence_rate, num):
if num != 1:
return 0
else:
return 1
def sample_number_of_occurrences(self, seeds=None):
return 1
def get_probability_no_exceedance(self, occurrence_rate, poes):
return 1-poes
class PoissonTOM(BaseTOM):
"""
Poissonian temporal occurrence model.
"""
def get_probability_one_or_more_occurrences(self, occurrence_rate):
"""
Calculates probability as ``1 - e ** (-occurrence_rate*time_span)``.
:param occurrence_rate:
The average number of events per year.
:return:
Float value between 0 and 1 inclusive.
"""
return 1 - numpy.exp(- occurrence_rate * self.time_span)
def get_probability_n_occurrences(self, occurrence_rate, num):
"""
Calculate the probability of occurrence of ``num`` events in the
constructor's ``time_span``.
:param occurrence_rate:
Annual rate of occurrence
:param num:
Number of events
:return:
Probability of occurrence
"""
return scipy.stats.poisson(occurrence_rate * self.time_span).pmf(num)
def sample_number_of_occurrences(self, occurrence_rate, seeds=None):
"""
Draw a random sample from the distribution and return a number
of events to occur.
The method uses the numpy random generator, which needs a seed
in order to get reproducible results. If the seed is None, it
should be set outside of this method.
:param occurrence_rate:
The average number of events per year.
:param seeds:
Random number generator seeds, one per each occurrence_rate
:return:
Sampled integer number of events to occur within model's
time span.
"""
if isinstance(seeds, numpy.ndarray): # array of seeds
assert len(seeds) == len(occurrence_rate), (
len(seeds), len(occurrence_rate))
rates = occurrence_rate * self.time_span
occ = []
for rate, seed in zip(rates, seeds):
numpy.random.seed(seed)
occ.append(numpy.random.poisson(rate))
return numpy.array(occ)
elif isinstance(seeds, int):
numpy.random.seed(seeds)
return numpy.random.poisson(occurrence_rate * self.time_span)
def get_probability_no_exceedance(self, occurrence_rate, poes):
"""
:param occurrence_rate:
The average number of events per year.
:param poes:
2D numpy array containing conditional probabilities that the
rupture occurrence causes a ground shaking value exceeding a
ground motion level at a site. First dimension represent sites,
second dimension intensity measure levels. ``poes`` can be obtained
calling the :func:`func <openquake.hazardlib.gsim.base.get_poes>`.
:returns:
2D numpy array containing probabilities of no exceedance. First
dimension represents sites, second dimension intensity measure
levels.
The probability is computed as exp(-occurrence_rate * time_span * poes)
"""
return numpy.exp(- occurrence_rate * self.time_span * poes)
class ClusterPoissonTOM(PoissonTOM):
"""
Poissonian temporal occurrence model with an occurrence rate
"""
def __init__(self, time_span, occurrence_rate):
self.time_span = time_span
self.occurrence_rate = occurrence_rate
@compile(["(float64, float64[:], float64[:], float64)",
"(float64, float64[:], float64[:,:,:], float64)"])
def get_pnes(rate, probs, poes, time_span):
"""
:param rate: occurrence rate in case of a poissonian rupture
:param probs: probabilities of occurrence in the nonpoissonian case
:param poes: array of PoEs of shape 1D or 3D
:param time_span: time span in the poissonian case (0. for FatedTOM)
Fast way to return probabilities of no exceedence given an array
of PoEs and some parameter.
"""
# NB: the NegativeBinomialTOM creates probs_occur with a rate not NaN
if time_span == 0.: # FatedTOM
return 1. - poes
elif len(probs) == 0: # poissonian
return numpy.exp(- rate * time_span * poes)
else:
# Uses the formula
#
# ∑ p(k|T) * p(X<x|rup)^k
#
# where `p(k|T)` is the probability that the rupture occurs k times
# in the time span `T`, `p(X<x|rup)` is the probability that a
# rupture occurrence does not cause a ground motion exceedance, and
# the summation `∑` is done over the number of occurrences `k`.
#
# `p(k|T)` is given by the attribute probs_occur and
# `p(X<x|rup)` is computed as ``1 - poes``.
pnes = numpy.full(poes.shape, probs[0])
for p, prob in enumerate(probs[1:], 1):
pnes[:] += prob * (1 - poes) ** p
return pnes.clip(0., 1.) # avoid numeric issues
class NegativeBinomialTOM(BaseTOM):
"""
Negative Binomial temporal occurrence model.
"""
def __init__(self, time_span, mu, alpha):
"""
:param time_span:
The time interval of interest, in years.
:param mu, alpha:
(list/np.ndarray) Parameters of the negbinom temporal model, in
form of mean rate/dispersion (μ / α) Kagan and Jackson, (2010)
k
Γ(τ + k) μ 1
f(k) = -------- . -------- . --------
Γ(τ) k 1/α
(μ + τ) (1 + μ/τ)
where τ=1/α
"""
super().__init__(time_span)
self.mu = mu
self.alpha = alpha
if numpy.any(self.mu <= 0) or numpy.any(self.alpha <= 0):
raise ValueError('Mean rate and rate dispersion must be greater '
'than 0')
self.time_span = time_span
def get_probability_one_or_more_occurrences(self, mean_rate=None):
"""
:param mean_rate:
The mean rate, or mean number of events per year
:return:
Float value between 0 and 1 inclusive.
"""
if mean_rate is None:
mean_rate = self.mu
tau = 1 / self.alpha
theta = tau / (tau + (mean_rate * self.time_span))
return 1 - scipy.stats.nbinom.cdf(1, tau, theta)
def get_probability_n_occurrences(self, num):
"""
Calculate the probability of occurrence of ``num`` events in the
constructor's ``time_span``.
:param num:
Number of events
:return:
Probability of occurrence
"""
tau = 1 / self.alpha
theta = tau / (tau + (self.mu * self.time_span))
return scipy.stats.nbinom.pmf(num, tau, theta)
def sample_number_of_occurrences(self, mean_rate=None, seed=None):
"""
Draw a random sample from the distribution and return a number
of events to occur.
The method uses the numpy random generator, which needs a seed
in order to get reproducible results. If the seed is None, it
should be set outside of this method.
:param mean_rate:
The mean rate, or mean number of events per year
:param seed:
Random number generator seed
:return:
Sampled integer number of events to occur within model's
time span.
"""
if mean_rate is None:
mean_rate = self.mu
tau = 1 / self.alpha
theta = tau / (tau + (mean_rate * self.time_span))
if isinstance(seed, int):
numpy.random.seed(seed)
return scipy.stats.nbinom.rvs(tau, theta)
def get_pmf(self, mean_rate, tol=1-1e-14, n_max=None):
"""
:param mean_rate:
The average number of events per year.
:param tol:
Quantile value up to which calculate the pmf
:returns:
1D numpy array containing the probability mass distribution,
up to tolerance level.
"""
# Gets dispersion from source object
alpha = self.alpha
# Recovers NB2 parametrization (tau/theta or n,p in literature)
tau = 1 / alpha
theta = tau / (tau + numpy.array(mean_rate).flatten()*self.time_span)
if not n_max:
n_max = numpy.max(
scipy.stats.nbinom.ppf(tol, tau, theta).astype(int))
if n_max < 4:
# minimum n_max for which the hazard equation is integrated,
# to avoid precision issues for probabilities of occur (<1e-6)
n_max = 4
pmf = scipy.stats.nbinom.pmf(
numpy.arange(0, n_max), tau, theta[:, None])
return pmf
def get_probability_no_exceedance(self, mean_rate, poes):
"""
:param mean_rate:
The average number of events per year.
:param poes:
2D numpy array containing conditional probabilities that the
rupture occurrence causes a ground shaking value exceeding a
ground motion level at a site. First dimension represent sites,
second dimension intensity measure levels. ``poes`` can be obtained
calling the :func:`func <openquake.hazardlib.gsim.base.get_poes>`.
:returns:
2D numpy array containing probabilities of no exceedance. First
dimension represents sites, second dimension intensity measure
levels.
"""
# Gets dispersion from source object
alpha = self.alpha
# Recovers NB2 parametrization (tau/theta or n,p in literature)
tau = 1 / alpha
theta = tau / (tau + mean_rate*self.time_span)
# Defines tol for the max quantile value, up to which the infinite series is calculated.
tol = 1 - 1e-14
n_max = scipy.stats.nbinom.ppf(tol, tau, theta)
pdf = scipy.stats.nbinom.pmf(numpy.arange(0, n_max), tau, theta)
poes_1 = 1 - poes
prob_no_exceed = numpy.zeros(poes.shape)
for k, prob in enumerate(pdf):
prob_no_exceed += prob * numpy.power(poes_1, k)
return prob_no_exceed