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/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