pyStrata 0.5.3__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.
- pystrata/__init__.py +38 -0
- pystrata/data/published_curves.toml +1565 -0
- pystrata/motion.py +367 -0
- pystrata/output.py +832 -0
- pystrata/propagation.py +690 -0
- pystrata/site.py +1422 -0
- pystrata/tools.py +299 -0
- pystrata/variation.py +929 -0
- pystrata-0.5.3.dist-info/METADATA +112 -0
- pystrata-0.5.3.dist-info/RECORD +13 -0
- pystrata-0.5.3.dist-info/WHEEL +4 -0
- pystrata-0.5.3.dist-info/licenses/AUTHORS.md +9 -0
- pystrata-0.5.3.dist-info/licenses/LICENSE +21 -0
pystrata/variation.py
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# The MIT License (MIT)
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2016-2018 Albert Kottke
|
|
5
|
+
#
|
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
# furnished to do so, subject to the following conditions:
|
|
12
|
+
#
|
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
|
14
|
+
# all copies or substantial portions of the Software.
|
|
15
|
+
#
|
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
# SOFTWARE.
|
|
23
|
+
import copy
|
|
24
|
+
from abc import abstractmethod
|
|
25
|
+
from typing import Dict
|
|
26
|
+
from typing import List
|
|
27
|
+
from typing import Optional
|
|
28
|
+
from typing import Union
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
import numpy.typing as npt
|
|
32
|
+
from scipy import stats
|
|
33
|
+
from scipy.sparse import diags
|
|
34
|
+
|
|
35
|
+
from . import site
|
|
36
|
+
|
|
37
|
+
# Used to define the random state. A specific state can be set with:
|
|
38
|
+
# random_state.set_seed(42)
|
|
39
|
+
random_state = np.random.RandomState()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TruncatedNorm:
|
|
43
|
+
"""Truncated normal random number generator.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
limit : float
|
|
48
|
+
Standard normal limits to impose
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, limit):
|
|
52
|
+
self.limit = limit
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def limit(self):
|
|
56
|
+
return self._limit
|
|
57
|
+
|
|
58
|
+
@limit.setter
|
|
59
|
+
def limit(self, value):
|
|
60
|
+
self._limit = value
|
|
61
|
+
|
|
62
|
+
# Need to scale the standard deviation to achieve sample standard
|
|
63
|
+
# deviation based on the truncation. Given truncation of 2 standard
|
|
64
|
+
# deviations, the input standard deviation must be increased to
|
|
65
|
+
# 1.136847 to maintain a unit standard deviation for the random
|
|
66
|
+
# samples.
|
|
67
|
+
self._scale = 1 / np.sqrt(stats.truncnorm.stats(-value, value, moments="v"))
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def scale(self):
|
|
71
|
+
return self._scale
|
|
72
|
+
|
|
73
|
+
def __call__(self, size=1):
|
|
74
|
+
"""Random number generator that follows a truncated normal distribution.
|
|
75
|
+
|
|
76
|
+
This is the defalut random number generator used by the program. It
|
|
77
|
+
generates normally distributed values ranging from -2 to +2 with unit
|
|
78
|
+
standard deviation.
|
|
79
|
+
|
|
80
|
+
The state of the random number generator is controlled by the
|
|
81
|
+
``np.random.RandomState`` instance.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
size : int
|
|
86
|
+
Number of random values to compute
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
rvs : ndarray or scalar
|
|
91
|
+
Random variates of given `size`.
|
|
92
|
+
"""
|
|
93
|
+
return stats.truncnorm.rvs(-self.limit, self.limit, scale=self._scale, size=size)
|
|
94
|
+
|
|
95
|
+
def correlated(self, correl):
|
|
96
|
+
# Acceptance proportion
|
|
97
|
+
accept = np.diff(stats.norm.cdf([-self.limit, self.limit]))[0]
|
|
98
|
+
# The expected number of tries required
|
|
99
|
+
expected = np.ceil(1 / accept).astype(int)
|
|
100
|
+
|
|
101
|
+
while True:
|
|
102
|
+
# Compute the multivariate normal with a unit variance and
|
|
103
|
+
# specified standard deviation. Use twice the expected since
|
|
104
|
+
# this calculation is fast and we don't want to loop.
|
|
105
|
+
randvar = np.random.multivariate_normal(
|
|
106
|
+
[0, 0], [[1, correl], [correl, 1]], size=(2 * expected)
|
|
107
|
+
)
|
|
108
|
+
valid = np.all(np.abs(randvar) < self.limit, axis=1)
|
|
109
|
+
if np.any(valid):
|
|
110
|
+
# Return the first valid value
|
|
111
|
+
return randvar[valid][0]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Random number generator used for all random number. Limited to +/- 2,
|
|
115
|
+
# and the standard deviation is scaled to maintain the standard deviation
|
|
116
|
+
# FIXME
|
|
117
|
+
randnorm = TruncatedNorm(2)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ToroThicknessVariation(object):
|
|
121
|
+
"""Toro (1995) [T95]_ thickness variation model.
|
|
122
|
+
|
|
123
|
+
The recommended values are provided as defaults to this model.
|
|
124
|
+
|
|
125
|
+
.. rubric:: References
|
|
126
|
+
|
|
127
|
+
.. [T95] Toro, G. R. (1995). Probabilistic models of site velocity
|
|
128
|
+
profiles for generic and site-specific ground-motion amplification
|
|
129
|
+
studies. Brookhaven National Laboratory Technical Report: 779574.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
c_1: float, optional
|
|
134
|
+
:math:`c_1` model parameter.
|
|
135
|
+
c_2: float, optional
|
|
136
|
+
:math:`c_2` model parameter.
|
|
137
|
+
c_3: float, optional
|
|
138
|
+
:math:`c_3` model parameter.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, c_1=10.86, c_2=-0.89, c_3=1.98):
|
|
143
|
+
self._c_1 = c_1
|
|
144
|
+
self._c_2 = c_2
|
|
145
|
+
self._c_3 = c_3
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def c_3(self):
|
|
149
|
+
return self._c_3
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def c_2(self):
|
|
153
|
+
return self._c_2
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def c_1(self):
|
|
157
|
+
return self._c_1
|
|
158
|
+
|
|
159
|
+
def iter_thickness(self, depth_total):
|
|
160
|
+
"""Iterate over the varied thicknesses.
|
|
161
|
+
|
|
162
|
+
The layering is generated using a non-homogenous Poisson process. The
|
|
163
|
+
following routine is used to generate the layering. The rate
|
|
164
|
+
function, :math:`\lambda(t)`, is integrated from 0 to t to generate
|
|
165
|
+
cumulative rate function, :math:`\Lambda(t)`. This function is then
|
|
166
|
+
inverted producing :math:`\Lambda^{-1}(t)`. Random variables
|
|
167
|
+
are produced using the a exponential random variation with
|
|
168
|
+
:math:`\mu = 1` and converted to the nonhomogenous variables using
|
|
169
|
+
the inverted function.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
depth_total: float
|
|
174
|
+
Total depth generated. Last thickness is truncated to achieve
|
|
175
|
+
this depth.
|
|
176
|
+
|
|
177
|
+
Yields
|
|
178
|
+
------
|
|
179
|
+
float
|
|
180
|
+
Varied thickness.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
total = 0
|
|
184
|
+
depth_prev = 0
|
|
185
|
+
|
|
186
|
+
while depth_prev < depth_total:
|
|
187
|
+
# Add a random exponential increment
|
|
188
|
+
total += np.random.exponential(1.0)
|
|
189
|
+
|
|
190
|
+
# Convert between x and depth using the inverse of \Lambda(t)
|
|
191
|
+
depth = (
|
|
192
|
+
np.power(
|
|
193
|
+
(self.c_2 * total) / self.c_3
|
|
194
|
+
+ total / self.c_3
|
|
195
|
+
+ np.power(self.c_1, self.c_2 + 1),
|
|
196
|
+
1 / (self.c_2 + 1),
|
|
197
|
+
)
|
|
198
|
+
- self.c_1
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
thickness = depth - depth_prev
|
|
202
|
+
|
|
203
|
+
if depth > depth_total:
|
|
204
|
+
thickness = depth_total - depth_prev
|
|
205
|
+
depth = depth_prev + thickness
|
|
206
|
+
|
|
207
|
+
depth_mid = (depth_prev + depth) / 2
|
|
208
|
+
yield thickness, depth_mid
|
|
209
|
+
|
|
210
|
+
depth_prev = depth
|
|
211
|
+
|
|
212
|
+
def __call__(self, profile):
|
|
213
|
+
"""Calculated a varied thickness profile.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
profile : site.Profile
|
|
218
|
+
Profile to be varied. Not modified in place.
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
site.Profile
|
|
223
|
+
Varied site profile.
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
layers = []
|
|
227
|
+
for thick, depth_mid in self.iter_thickness(profile[-2].depth_base):
|
|
228
|
+
# Locate the proper layer and add it to the model
|
|
229
|
+
for l in profile:
|
|
230
|
+
if l.depth < depth_mid <= l.depth_base:
|
|
231
|
+
layers.append(site.Layer(l.soil_type, thick, l.initial_shear_vel))
|
|
232
|
+
break
|
|
233
|
+
else:
|
|
234
|
+
raise LookupError
|
|
235
|
+
|
|
236
|
+
# Add the half-space
|
|
237
|
+
hsl = profile[-1]
|
|
238
|
+
layers.append(site.Layer(hsl.soil_type, 0, hsl.initial_shear_vel))
|
|
239
|
+
|
|
240
|
+
varied = site.Profile(layers, profile.wt_depth)
|
|
241
|
+
return varied
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class HalfSpaceDepthVariation(object):
|
|
245
|
+
def __init__(self, dist: stats.rv_continuous):
|
|
246
|
+
self._dist = dist
|
|
247
|
+
|
|
248
|
+
def __call__(self, profile: site.Profile) -> site.Profile:
|
|
249
|
+
# Update the distribution with the central value of the profile
|
|
250
|
+
varied_depth = self._dist.rvs()
|
|
251
|
+
|
|
252
|
+
# Find the layer
|
|
253
|
+
index, depth_within = profile.lookup_depth(varied_depth)
|
|
254
|
+
|
|
255
|
+
print(varied_depth, profile[-1].depth, index, depth_within)
|
|
256
|
+
|
|
257
|
+
half_space = profile[-1]
|
|
258
|
+
|
|
259
|
+
if index < (len(profile) - 1):
|
|
260
|
+
# Variation is within the layers
|
|
261
|
+
layers = [site.Layer.copy_of(layer) for layer in profile[: (index + 1)]]
|
|
262
|
+
# Reduce the thickness of the layer above the half-space
|
|
263
|
+
layers[-1]._thickness = depth_within
|
|
264
|
+
else:
|
|
265
|
+
# Variation extends past the depth of the model
|
|
266
|
+
orig_thick = profile[-2].thickness
|
|
267
|
+
total_thick = orig_thick + depth_within
|
|
268
|
+
count = np.ceil(total_thick // orig_thick).astype(int)
|
|
269
|
+
|
|
270
|
+
thick = total_thick / count
|
|
271
|
+
|
|
272
|
+
print(count, thick)
|
|
273
|
+
|
|
274
|
+
# Don't copy half-space
|
|
275
|
+
layers = [site.Layer.copy_of(layer) for layer in profile[:-1]]
|
|
276
|
+
# Don't call the setter function as it needs a profile defined
|
|
277
|
+
layers[-1]._thickness = thick
|
|
278
|
+
parent = layers[-1]
|
|
279
|
+
for _ in range(count - 1):
|
|
280
|
+
layers.append(site.Layer.copy_of(parent))
|
|
281
|
+
|
|
282
|
+
layers.append(half_space)
|
|
283
|
+
|
|
284
|
+
return site.Profile(layers, profile.wt_depth)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class LayerThicknessVariation(object):
|
|
288
|
+
def __init__(
|
|
289
|
+
self,
|
|
290
|
+
models: Union[List[stats.rv_continuous], Dict[int, stats.rv_continuous]],
|
|
291
|
+
discretize_kwds: Optional[Dict[str, float]] = None,
|
|
292
|
+
) -> None:
|
|
293
|
+
self._models = models
|
|
294
|
+
self._discretize_kwds = discretize_kwds
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class VelocityVariation(object):
|
|
298
|
+
"""Abstract model for varying the velocity."""
|
|
299
|
+
|
|
300
|
+
def __init__(self, vary_bedrock=False):
|
|
301
|
+
self._vary_bedrock = vary_bedrock
|
|
302
|
+
|
|
303
|
+
def __call__(self, profile: site.Profile) -> site.Profile:
|
|
304
|
+
"""Calculate a varied shear-wave velocity profile.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
profile : site.Profile
|
|
309
|
+
Profile to be varied. Not modified in place.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
site.Profile
|
|
314
|
+
Varied site profile.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
mean = np.log(profile.initial_shear_vel)
|
|
318
|
+
covar = self._calc_covar_matrix(profile)
|
|
319
|
+
|
|
320
|
+
ln_vel_rand = np.random.multivariate_normal(mean, covar, check_valid="ignore")
|
|
321
|
+
|
|
322
|
+
# Limits based on the number of standard deviations
|
|
323
|
+
offset = randnorm.limit * np.sqrt(np.diag(covar))
|
|
324
|
+
ln_vel = np.clip(ln_vel_rand, mean - offset, mean + offset)
|
|
325
|
+
vel = np.exp(ln_vel)
|
|
326
|
+
|
|
327
|
+
varied = site.Profile.copy_of(profile)
|
|
328
|
+
# Update the velocities
|
|
329
|
+
end = None if self.vary_bedrock else -1
|
|
330
|
+
for i, v in enumerate(vel[:end]):
|
|
331
|
+
varied[i].initial_shear_vel = v
|
|
332
|
+
|
|
333
|
+
return varied
|
|
334
|
+
|
|
335
|
+
def _calc_covar_matrix(self, profile):
|
|
336
|
+
"""Calculate the covariance matrix.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
profile : site.Profile
|
|
341
|
+
Input site profile
|
|
342
|
+
|
|
343
|
+
Yields
|
|
344
|
+
------
|
|
345
|
+
covar : `class`:numpy.array
|
|
346
|
+
Covariance matrix
|
|
347
|
+
"""
|
|
348
|
+
corr = self._calc_corr(profile)
|
|
349
|
+
std = self._calc_ln_std(profile)
|
|
350
|
+
# Modify the standard deviation by the truncated norm scale
|
|
351
|
+
std *= randnorm.scale
|
|
352
|
+
|
|
353
|
+
var = std**2
|
|
354
|
+
covar = corr * std[:-1] * std[1:]
|
|
355
|
+
|
|
356
|
+
# Main diagonal is the variance
|
|
357
|
+
mat = diags([covar, var, covar], [-1, 0, 1]).toarray()
|
|
358
|
+
|
|
359
|
+
return mat
|
|
360
|
+
|
|
361
|
+
@abstractmethod
|
|
362
|
+
def _calc_corr(self, profile):
|
|
363
|
+
"""Compute the adjacent-layer correlations.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
profile : site.Profile
|
|
368
|
+
Input site profile
|
|
369
|
+
|
|
370
|
+
Yields
|
|
371
|
+
------
|
|
372
|
+
np.array
|
|
373
|
+
Correlation matrix
|
|
374
|
+
"""
|
|
375
|
+
raise NotImplementedError
|
|
376
|
+
|
|
377
|
+
@abstractmethod
|
|
378
|
+
def _calc_ln_std(self, profile):
|
|
379
|
+
"""Compute the standard deviation for each layer.
|
|
380
|
+
|
|
381
|
+
Parameters
|
|
382
|
+
----------
|
|
383
|
+
profile : site.Profile
|
|
384
|
+
Input site profile
|
|
385
|
+
|
|
386
|
+
Yields
|
|
387
|
+
------
|
|
388
|
+
np.array
|
|
389
|
+
Standard deviation of the shear-wave velocity
|
|
390
|
+
"""
|
|
391
|
+
raise NotImplementedError
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def vary_bedrock(self):
|
|
395
|
+
return self._vary_bedrock
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class ToroVelocityVariation(VelocityVariation):
|
|
399
|
+
"""Toro (1995) [T95] velocity variation model.
|
|
400
|
+
|
|
401
|
+
Default values can be selected with :meth:`.generic_model`.
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
ln_std: float, optional
|
|
406
|
+
:math:`\sigma_{ln}` model parameter.
|
|
407
|
+
rho_0: float, optional
|
|
408
|
+
:math:`ρ_0` model parameter.
|
|
409
|
+
delta: float, optional
|
|
410
|
+
:math:`\Delta` model parameter.
|
|
411
|
+
rho_200: float, optional
|
|
412
|
+
:math:`ρ_200` model parameter.
|
|
413
|
+
h_0: float, optional
|
|
414
|
+
:math:`h_0` model parameter.
|
|
415
|
+
b: float, optional
|
|
416
|
+
:math:`b` model parameter.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
PARAMS = {
|
|
420
|
+
"Geomatrix AB": {
|
|
421
|
+
"ln_std": 0.46,
|
|
422
|
+
"rho_0": 0.96,
|
|
423
|
+
"delta": 13.1,
|
|
424
|
+
"rho_200": 0.96,
|
|
425
|
+
"h_0": 0.0,
|
|
426
|
+
"b": 0.095,
|
|
427
|
+
},
|
|
428
|
+
"Geomatrix CD": {
|
|
429
|
+
"ln_std": 0.38,
|
|
430
|
+
"rho_0": 0.99,
|
|
431
|
+
"delta": 8.0,
|
|
432
|
+
"rho_200": 1.00,
|
|
433
|
+
"h_0": 0.0,
|
|
434
|
+
"b": 0.160,
|
|
435
|
+
},
|
|
436
|
+
"USGS AB": {
|
|
437
|
+
"ln_std": 0.35,
|
|
438
|
+
"rho_0": 0.95,
|
|
439
|
+
"delta": 4.2,
|
|
440
|
+
"rho_200": 1.00,
|
|
441
|
+
"h_0": 0.0,
|
|
442
|
+
"b": 0.138,
|
|
443
|
+
},
|
|
444
|
+
"USGS CD": {
|
|
445
|
+
"ln_std": 0.36,
|
|
446
|
+
"rho_0": 0.99,
|
|
447
|
+
"delta": 3.9,
|
|
448
|
+
"rho_200": 1.00,
|
|
449
|
+
"h_0": 0.0,
|
|
450
|
+
"b": 0.293,
|
|
451
|
+
},
|
|
452
|
+
"USGS A": {
|
|
453
|
+
"ln_std": 0.36,
|
|
454
|
+
"rho_0": 0.95,
|
|
455
|
+
"delta": 3.4,
|
|
456
|
+
"rho_200": 0.42,
|
|
457
|
+
"h_0": 0.0,
|
|
458
|
+
"b": 0.063,
|
|
459
|
+
},
|
|
460
|
+
"USGS B": {
|
|
461
|
+
"ln_std": 0.27,
|
|
462
|
+
"rho_0": 0.97,
|
|
463
|
+
"delta": 3.8,
|
|
464
|
+
"rho_200": 1.00,
|
|
465
|
+
"h_0": 0.0,
|
|
466
|
+
"b": 0.293,
|
|
467
|
+
},
|
|
468
|
+
"USGS C": {
|
|
469
|
+
"ln_std": 0.31,
|
|
470
|
+
"rho_0": 0.99,
|
|
471
|
+
"delta": 3.9,
|
|
472
|
+
"rho_200": 0.98,
|
|
473
|
+
"h_0": 0.0,
|
|
474
|
+
"b": 0.344,
|
|
475
|
+
},
|
|
476
|
+
"USGS D": {
|
|
477
|
+
"ln_std": 0.37,
|
|
478
|
+
"rho_0": 0.00,
|
|
479
|
+
"delta": 5.0,
|
|
480
|
+
"rho_200": 0.50,
|
|
481
|
+
"h_0": 0.0,
|
|
482
|
+
"b": 0.744,
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
def __init__(
|
|
487
|
+
self,
|
|
488
|
+
ln_std: float,
|
|
489
|
+
rho_0: float,
|
|
490
|
+
delta: float,
|
|
491
|
+
rho_200: float,
|
|
492
|
+
h_0: float,
|
|
493
|
+
b: float,
|
|
494
|
+
vary_bedrock: bool = False,
|
|
495
|
+
):
|
|
496
|
+
"""Initialize the model."""
|
|
497
|
+
super().__init__(vary_bedrock=vary_bedrock)
|
|
498
|
+
|
|
499
|
+
self._ln_std = ln_std
|
|
500
|
+
self._rho_0 = rho_0
|
|
501
|
+
self._delta = delta
|
|
502
|
+
self._rho_200 = rho_200
|
|
503
|
+
self._h_0 = h_0
|
|
504
|
+
self._b = b
|
|
505
|
+
|
|
506
|
+
def _calc_corr(self, profile: site.Profile) -> np.ndarray:
|
|
507
|
+
"""Compute the adjacent-layer correlations
|
|
508
|
+
|
|
509
|
+
Parameters
|
|
510
|
+
----------
|
|
511
|
+
profile : :class:`site.Profile`
|
|
512
|
+
Input site profile
|
|
513
|
+
|
|
514
|
+
Yields
|
|
515
|
+
------
|
|
516
|
+
corr : :class:`numpy.array`
|
|
517
|
+
Adjacent-layer correlations
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
# Toro defines the depth as the average midpoint depths of layers i and i-1.
|
|
521
|
+
depths_mid = np.array(profile.depth_mid)
|
|
522
|
+
depth = np.mean(np.c_[depths_mid[:-1], depths_mid[1:]], axis=1)
|
|
523
|
+
|
|
524
|
+
# t variable from Toro; defined as the difference of the midpoint depths
|
|
525
|
+
thick = np.diff(depth)
|
|
526
|
+
# Remove the depth associated with the final layer. We will set that it is
|
|
527
|
+
# perfectly correlated later.
|
|
528
|
+
depth = depth[:-1]
|
|
529
|
+
|
|
530
|
+
# Depth dependent correlation
|
|
531
|
+
corr_depth = self.rho_200 * np.power((depth + self.h_0) / (200 + self.h_0), self.b)
|
|
532
|
+
corr_depth[depth > 200] = self.rho_200
|
|
533
|
+
|
|
534
|
+
# Thickness dependent correlation
|
|
535
|
+
corr_thick = self.rho_0 * np.exp(-thick / self.delta)
|
|
536
|
+
|
|
537
|
+
# Final correlation
|
|
538
|
+
# Correlation coefficient
|
|
539
|
+
corr = (1 - corr_depth) * corr_thick + corr_depth
|
|
540
|
+
|
|
541
|
+
# Bedrock is perfectly correlated with layer above it
|
|
542
|
+
corr = np.r_[corr, 1]
|
|
543
|
+
|
|
544
|
+
return corr
|
|
545
|
+
|
|
546
|
+
def _calc_ln_std(self, profile):
|
|
547
|
+
ln_std = self.ln_std * np.ones(len(profile))
|
|
548
|
+
return ln_std
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def ln_std(self):
|
|
552
|
+
return self._ln_std
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def rho_0(self):
|
|
556
|
+
return self._rho_0
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def delta(self):
|
|
560
|
+
return self._delta
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def rho_200(self):
|
|
564
|
+
return self._rho_200
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def h_0(self):
|
|
568
|
+
return self._h_0
|
|
569
|
+
|
|
570
|
+
@property
|
|
571
|
+
def b(self):
|
|
572
|
+
return self._b
|
|
573
|
+
|
|
574
|
+
@classmethod
|
|
575
|
+
def site_classes(cls):
|
|
576
|
+
return cls.PARAMS.keys()
|
|
577
|
+
|
|
578
|
+
@classmethod
|
|
579
|
+
def generic_model(cls, site_class, **kwds):
|
|
580
|
+
"""Use generic model parameters based on site class.
|
|
581
|
+
|
|
582
|
+
Parameters
|
|
583
|
+
----------
|
|
584
|
+
site_class: str
|
|
585
|
+
Site classification. Possible options are:
|
|
586
|
+
* Geomatrix AB
|
|
587
|
+
* Geomatrix CD
|
|
588
|
+
* USGS AB
|
|
589
|
+
* USGS CD
|
|
590
|
+
* USGS A
|
|
591
|
+
* USGS B
|
|
592
|
+
* USGS C
|
|
593
|
+
* USGS D
|
|
594
|
+
|
|
595
|
+
See the report for definitions of the Geomatrix site
|
|
596
|
+
classication. USGS site classification is based on :math:`V_{s30}`:
|
|
597
|
+
|
|
598
|
+
=========== =====================
|
|
599
|
+
Site Class :math:`V_{s30}` (m/s)
|
|
600
|
+
=========== =====================
|
|
601
|
+
A >750 m/s
|
|
602
|
+
B 360 to 750 m/s
|
|
603
|
+
C 180 to 360 m/s
|
|
604
|
+
D <180 m/s
|
|
605
|
+
=========== =====================
|
|
606
|
+
|
|
607
|
+
Returns
|
|
608
|
+
-------
|
|
609
|
+
ToroVelocityVariation
|
|
610
|
+
Initialized :class:`ToroVelocityVariation` with generic parameters.
|
|
611
|
+
"""
|
|
612
|
+
p = dict(cls.PARAMS[site_class])
|
|
613
|
+
p.update(kwds)
|
|
614
|
+
return cls(**p)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class DepthDependToroVelVariation(ToroVelocityVariation):
|
|
618
|
+
"""Toro (1995) [T95] velocity variation model modified for a depth
|
|
619
|
+
dependent standard deviation that can be overridden by the soil_type name.
|
|
620
|
+
|
|
621
|
+
Default values can be selected with :meth:`.generic_model`.
|
|
622
|
+
|
|
623
|
+
Parameters
|
|
624
|
+
----------
|
|
625
|
+
depth: array_like, optional
|
|
626
|
+
Depths defining the standard deviation model. Default is [0, 15]
|
|
627
|
+
following the SPID model.
|
|
628
|
+
ln_std: array_like, optional
|
|
629
|
+
:math:`\sigma_{ln}` model parameter. Default is [0.25, 0.15]
|
|
630
|
+
following the SPID model.
|
|
631
|
+
rho_0: float, optional
|
|
632
|
+
:math:`ρ_0` model parameter.
|
|
633
|
+
delta: float, optional
|
|
634
|
+
:math:`\Delta` model parameter.
|
|
635
|
+
rho_200: float, optional
|
|
636
|
+
:math:`ρ_200` model parameter.
|
|
637
|
+
h_0: float, optional
|
|
638
|
+
:math:`h_0` model parameter.
|
|
639
|
+
b: float, optional
|
|
640
|
+
:math:`b` model parameter.
|
|
641
|
+
ln_std_map: dict[str, float], optional
|
|
642
|
+
Mapping between the soil_type and the defined ln_std. Default is *None*.
|
|
643
|
+
"""
|
|
644
|
+
|
|
645
|
+
def __init__(
|
|
646
|
+
self,
|
|
647
|
+
depth: npt.ArrayLike,
|
|
648
|
+
ln_std: npt.ArrayLike,
|
|
649
|
+
rho_0: float,
|
|
650
|
+
delta: float,
|
|
651
|
+
rho_200: float,
|
|
652
|
+
h_0: float,
|
|
653
|
+
b: float,
|
|
654
|
+
ln_std_map=Optional[Dict[str, float]],
|
|
655
|
+
vary_bedrock=False,
|
|
656
|
+
):
|
|
657
|
+
"""Initialize the model."""
|
|
658
|
+
super().__init__(ln_std, rho_0, delta, rho_200, h_0, b, vary_bedrock=vary_bedrock)
|
|
659
|
+
self.depth = depth
|
|
660
|
+
self.ln_std_map = ln_std_map or dict()
|
|
661
|
+
|
|
662
|
+
def _calc_ln_std(self, profile):
|
|
663
|
+
# Depth based values
|
|
664
|
+
ln_std = np.interp(
|
|
665
|
+
profile.depth_mid,
|
|
666
|
+
self.depth,
|
|
667
|
+
self.ln_std,
|
|
668
|
+
left=self.ln_std[0],
|
|
669
|
+
right=self.ln_std[-1],
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# Update based on soil_type name
|
|
673
|
+
for name, value in self.ln_std_map.items():
|
|
674
|
+
for i, l in enumerate(profile):
|
|
675
|
+
if name in l.soil_type.name:
|
|
676
|
+
ln_std[i] = value
|
|
677
|
+
|
|
678
|
+
return ln_std
|
|
679
|
+
|
|
680
|
+
@classmethod
|
|
681
|
+
def generic_model(cls, site_class, /, *, ln_std_map=None, **kwds):
|
|
682
|
+
"""Use generic model parameters based on site class.
|
|
683
|
+
|
|
684
|
+
Parameters
|
|
685
|
+
----------
|
|
686
|
+
site_class: str
|
|
687
|
+
Site classification. Possible options are:
|
|
688
|
+
* Geomatrix AB
|
|
689
|
+
* Geomatrix CD
|
|
690
|
+
* USGS AB
|
|
691
|
+
* USGS CD
|
|
692
|
+
* USGS A
|
|
693
|
+
* USGS B
|
|
694
|
+
* USGS C
|
|
695
|
+
* USGS D
|
|
696
|
+
|
|
697
|
+
See the report for definitions of the Geomatrix site
|
|
698
|
+
classication. USGS site classification is based on :math:`V_{s30}`:
|
|
699
|
+
|
|
700
|
+
=========== =====================
|
|
701
|
+
Site Class :math:`V_{s30}` (m/s)
|
|
702
|
+
=========== =====================
|
|
703
|
+
A >750 m/s
|
|
704
|
+
B 360 to 750 m/s
|
|
705
|
+
C 180 to 360 m/s
|
|
706
|
+
D <180 m/s
|
|
707
|
+
=========== =====================
|
|
708
|
+
|
|
709
|
+
ln_std_map: dict[str, float], optional
|
|
710
|
+
Mapping between the soil_type and the defined ln_std. Default is *None*.
|
|
711
|
+
|
|
712
|
+
Returns
|
|
713
|
+
-------
|
|
714
|
+
DepthAndSoilTypeDependToroVelVariation
|
|
715
|
+
Initialized :class:`DepthDependToroVelVariation` with generic parameters.
|
|
716
|
+
"""
|
|
717
|
+
p = dict(cls.PARAMS[site_class])
|
|
718
|
+
p.update(kwds)
|
|
719
|
+
|
|
720
|
+
if "depth" not in kwds:
|
|
721
|
+
p["depth"] = [0, 15]
|
|
722
|
+
p["ln_std"] = [0.25, 0.15]
|
|
723
|
+
|
|
724
|
+
p["ln_std_map"] = ln_std_map
|
|
725
|
+
return cls(**p)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
class SoilTypeVariation(object):
|
|
729
|
+
def __init__(
|
|
730
|
+
self,
|
|
731
|
+
correlation,
|
|
732
|
+
limits_mod_reduc=[0.05, 1],
|
|
733
|
+
limits_damping=[0, 0.15],
|
|
734
|
+
vary_bedrock=False,
|
|
735
|
+
):
|
|
736
|
+
self._vary_bedrock = vary_bedrock
|
|
737
|
+
self._correlation = correlation
|
|
738
|
+
self._limits_mod_reduc = list(limits_mod_reduc)
|
|
739
|
+
self._limits_damping = list(limits_damping)
|
|
740
|
+
|
|
741
|
+
def __call__(self, soil_type):
|
|
742
|
+
def get_values(nlp):
|
|
743
|
+
try:
|
|
744
|
+
return nlp.values
|
|
745
|
+
except AttributeError:
|
|
746
|
+
return np.asarray(nlp).astype(float)
|
|
747
|
+
|
|
748
|
+
mod_reduc = get_values(soil_type.mod_reduc)
|
|
749
|
+
damping = get_values(soil_type.damping)
|
|
750
|
+
|
|
751
|
+
# A pair of correlated random variables
|
|
752
|
+
randvar = randnorm.correlated(self.correlation)
|
|
753
|
+
|
|
754
|
+
varied_mod_reduc, varied_damping = self._get_varied(randvar, mod_reduc, damping)
|
|
755
|
+
|
|
756
|
+
# Clip the values to the specified min/max
|
|
757
|
+
varied_mod_reduc = np.clip(
|
|
758
|
+
varied_mod_reduc, self.limits_mod_reduc[0], self.limits_mod_reduc[1]
|
|
759
|
+
)
|
|
760
|
+
varied_damping = np.clip(varied_damping, self.limits_damping[0], self.limits_damping[1])
|
|
761
|
+
|
|
762
|
+
# Set the values
|
|
763
|
+
realization = copy.deepcopy(soil_type)
|
|
764
|
+
for attr_name, values in zip(["mod_reduc", "damping"], [varied_mod_reduc, varied_damping]):
|
|
765
|
+
try:
|
|
766
|
+
getattr(realization, attr_name).values = values
|
|
767
|
+
except AttributeError:
|
|
768
|
+
setattr(realization, attr_name, values)
|
|
769
|
+
return realization
|
|
770
|
+
|
|
771
|
+
def _get_varied(self, randvar, mod_reduc, damping):
|
|
772
|
+
raise NotImplementedError
|
|
773
|
+
|
|
774
|
+
@property
|
|
775
|
+
def correlation(self):
|
|
776
|
+
return self._correlation
|
|
777
|
+
|
|
778
|
+
@property
|
|
779
|
+
def limits_damping(self):
|
|
780
|
+
return self._limits_damping
|
|
781
|
+
|
|
782
|
+
@property
|
|
783
|
+
def limits_mod_reduc(self):
|
|
784
|
+
return self._limits_mod_reduc
|
|
785
|
+
|
|
786
|
+
@property
|
|
787
|
+
def vary_bedrock(self):
|
|
788
|
+
return self._vary_bedrock
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
class DarendeliVariation(SoilTypeVariation):
|
|
792
|
+
def _get_varied(self, randvar, mod_reduc, damping):
|
|
793
|
+
mod_reduc_means = mod_reduc
|
|
794
|
+
mod_reduc_stds = self.calc_std_mod_reduc(mod_reduc_means)
|
|
795
|
+
varied_mod_reduc = mod_reduc_means + randvar[0] * mod_reduc_stds
|
|
796
|
+
|
|
797
|
+
damping_means = damping
|
|
798
|
+
damping_stds = self.calc_std_damping(damping_means)
|
|
799
|
+
varied_damping = damping_means + randvar[1] * damping_stds
|
|
800
|
+
|
|
801
|
+
return varied_mod_reduc, varied_damping
|
|
802
|
+
|
|
803
|
+
@staticmethod
|
|
804
|
+
def calc_std_mod_reduc(mod_reduc):
|
|
805
|
+
"""Calculate the standard deviation as a function of G/G_max.
|
|
806
|
+
|
|
807
|
+
Equation 7.29 from Darendeli (2001).
|
|
808
|
+
|
|
809
|
+
Parameters
|
|
810
|
+
----------
|
|
811
|
+
mod_reduc : array_like
|
|
812
|
+
Modulus reduction values.
|
|
813
|
+
|
|
814
|
+
Returns
|
|
815
|
+
-------
|
|
816
|
+
std : :class:`numpy.ndarray`
|
|
817
|
+
Standard deviation.
|
|
818
|
+
"""
|
|
819
|
+
mod_reduc = np.asarray(mod_reduc).astype(float)
|
|
820
|
+
std = np.exp(-4.23) + np.sqrt(0.25 / np.exp(3.62) - (mod_reduc - 0.5) ** 2 / np.exp(3.62))
|
|
821
|
+
return std
|
|
822
|
+
|
|
823
|
+
@staticmethod
|
|
824
|
+
def calc_std_damping(damping):
|
|
825
|
+
"""Calculate the standard deviation as a function of damping in decimal.
|
|
826
|
+
|
|
827
|
+
Equation 7.30 from Darendeli (2001).
|
|
828
|
+
|
|
829
|
+
Parameters
|
|
830
|
+
----------
|
|
831
|
+
damping : array_like
|
|
832
|
+
Material damping values in decimal.
|
|
833
|
+
|
|
834
|
+
Returns
|
|
835
|
+
-------
|
|
836
|
+
std : :class:`numpy.ndarray`
|
|
837
|
+
Standard deviation.
|
|
838
|
+
"""
|
|
839
|
+
damping = np.asarray(damping).astype(float)
|
|
840
|
+
std = (np.exp(-5) + np.exp(-0.25) * np.sqrt(100 * damping)) / 100.0
|
|
841
|
+
return std
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
class SpidVariation(SoilTypeVariation):
|
|
845
|
+
"""Variation defined by the EPRI SPID (2013) and documented in
|
|
846
|
+
PNNL (2014).
|
|
847
|
+
|
|
848
|
+
EPRI SPID (2013): https://www.nrc.gov/docs/ML1233/ML12333A170.pdf
|
|
849
|
+
|
|
850
|
+
"""
|
|
851
|
+
|
|
852
|
+
def __init__(
|
|
853
|
+
self,
|
|
854
|
+
correlation,
|
|
855
|
+
limits_mod_reduc=[0, 1],
|
|
856
|
+
limits_damping=[0, 0.15],
|
|
857
|
+
std_mod_reduc=0.15,
|
|
858
|
+
std_damping=0.30,
|
|
859
|
+
):
|
|
860
|
+
super().__init__(correlation, limits_mod_reduc, limits_damping)
|
|
861
|
+
self._std_mod_reduc = std_mod_reduc
|
|
862
|
+
self._std_damping = std_damping
|
|
863
|
+
|
|
864
|
+
def _get_varied(self, randvar, mod_reduc, damping):
|
|
865
|
+
# Vary the G/Gmax in transformed space.
|
|
866
|
+
|
|
867
|
+
# PNNL (2014) Hanford Site Wide Hazard Study is available here:
|
|
868
|
+
# https://www.hanford.gov/files.cfm/00_Front_Matter.pdf
|
|
869
|
+
# https://www.hanford.gov/files.cfm/9.0_Ground_Motion_Characterization.pdf
|
|
870
|
+
|
|
871
|
+
# Equation 9.43 of PNNL (2014)
|
|
872
|
+
# Here epsilon is added so that the denomiator doesn't go to zero.
|
|
873
|
+
f_mean = mod_reduc / (1 - mod_reduc + np.finfo(float).eps)
|
|
874
|
+
# Instead of constraining the standard deviation at a specific
|
|
875
|
+
# strain, then standard deviation is constrained at G/Gmax of 0.5.
|
|
876
|
+
# This is modified from Equation 9.44 of PNNL (2014).
|
|
877
|
+
f_std = self.std_mod_reduc * (1 / (1 - 0.5))
|
|
878
|
+
f_real = np.exp(randvar[0] * f_std) * f_mean
|
|
879
|
+
# Equation 9.45 of PNNL (2014)
|
|
880
|
+
varied_mod_reduc = f_real / (1 + f_real)
|
|
881
|
+
|
|
882
|
+
# Simple log distribution
|
|
883
|
+
varied_damping = np.exp(randvar[1] * self.std_damping) * damping
|
|
884
|
+
|
|
885
|
+
return varied_mod_reduc, varied_damping
|
|
886
|
+
|
|
887
|
+
@property
|
|
888
|
+
def std_damping(self):
|
|
889
|
+
return self._std_damping
|
|
890
|
+
|
|
891
|
+
@property
|
|
892
|
+
def std_mod_reduc(self):
|
|
893
|
+
return self._std_mod_reduc
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def iter_varied_profiles(
|
|
897
|
+
profile,
|
|
898
|
+
count,
|
|
899
|
+
var_thickness: ToroThicknessVariation = None,
|
|
900
|
+
var_velocity: VelocityVariation = None,
|
|
901
|
+
var_soiltypes: SoilTypeVariation = None,
|
|
902
|
+
):
|
|
903
|
+
for _ in range(count):
|
|
904
|
+
# Copy the profile to form the realization
|
|
905
|
+
p = site.Profile.copy_of(profile)
|
|
906
|
+
|
|
907
|
+
if var_thickness:
|
|
908
|
+
p = var_thickness(p)
|
|
909
|
+
|
|
910
|
+
if var_velocity:
|
|
911
|
+
p = var_velocity(p)
|
|
912
|
+
|
|
913
|
+
if var_soiltypes:
|
|
914
|
+
# Map of varied soil types
|
|
915
|
+
varied = {str(st): var_soiltypes(st) for st in p.iter_soil_types()}
|
|
916
|
+
# Create new layers
|
|
917
|
+
end = None if var_soiltypes.vary_bedrock else -1
|
|
918
|
+
layers = [
|
|
919
|
+
site.Layer(varied[str(l.soil_type)], l.thickness, l.initial_shear_vel)
|
|
920
|
+
for l in p[:end]
|
|
921
|
+
]
|
|
922
|
+
# Add the unrandomized bedrock
|
|
923
|
+
if not var_soiltypes.vary_bedrock:
|
|
924
|
+
layers.append(p[-1])
|
|
925
|
+
|
|
926
|
+
# Create a new profile
|
|
927
|
+
p = site.Profile(layers, p.wt_depth)
|
|
928
|
+
|
|
929
|
+
yield p
|