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/site.py ADDED
@@ -0,0 +1,1422 @@
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
+ from __future__ import annotations
24
+
25
+ import collections
26
+ import warnings
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+ from typing import List
30
+ from typing import Optional
31
+ from typing import Tuple
32
+
33
+ import matplotlib.pyplot as plt
34
+ import numpy as np
35
+ import numpy.typing as npt
36
+ import scipy.constants
37
+ import tomli
38
+ from scipy.interpolate import interp1d
39
+
40
+ from .motion import GRAVITY
41
+ from .motion import WaveField
42
+
43
+ COMP_MODULUS_MODEL = "dormieux"
44
+
45
+ KPA_TO_ATM = scipy.constants.kilo / scipy.constants.atm
46
+
47
+ PUBLISHED_CURVES = None
48
+
49
+
50
+ def _load_published_curves():
51
+ """Load published nonlinear curves."""
52
+ global PUBLISHED_CURVES
53
+
54
+ fpath = Path(__file__).parent / "data" / "published_curves.toml"
55
+ with fpath.open("rb") as fp:
56
+ models = tomli.load(fp)["models"]
57
+
58
+ # Count to make sure there aren't repeated names
59
+ counts = collections.Counter([m["name"] for m in models])
60
+ if max(counts.values()) > 1:
61
+ names = ", ".join([k for k, v in counts.items() if v > 1])
62
+ warnings.warn(f"Repeated names in {fpath}: " + names)
63
+
64
+ PUBLISHED_CURVES = {m["name"]: m for m in models}
65
+
66
+
67
+ def known_published_curves() -> List[dict]:
68
+ """List of published curves in the database."""
69
+ if not PUBLISHED_CURVES:
70
+ _load_published_curves()
71
+ return list(PUBLISHED_CURVES.keys())
72
+
73
+
74
+ class NonlinearProperty(object):
75
+ """Class for nonlinear property with a method for log-linear interpolation.
76
+
77
+ Parameters
78
+ ----------
79
+ name: str, optional
80
+ used for identification
81
+ strains: :class:`numpy.ndarray`, optional
82
+ strains for each of the values [decimal].
83
+ values: :class:`numpy.ndarray`, optional
84
+ value of the property corresponding to each strain. Damping should be
85
+ specified in decimal, e.g., 0.05 for 5%.
86
+ param: str, optional
87
+ type of parameter. Possible values are:
88
+
89
+ mod_reduc
90
+ Shear-modulus reduction curve
91
+
92
+ damping
93
+ Damping ratio curve [decimal]
94
+ """
95
+
96
+ PARAMS = ["mod_reduc", "damping"]
97
+
98
+ def __init__(self, name="", strains=None, values=None, param=None):
99
+ self.name = name
100
+ self._strains = np.asarray(strains).astype(float)
101
+ self._values = np.asarray(values).astype(float)
102
+
103
+ self._interpolater = None
104
+
105
+ self._param = None
106
+ self.param = param
107
+
108
+ self._update()
109
+
110
+ @classmethod
111
+ def from_published(cls, name, param):
112
+ assert param in cls.PARAMS
113
+ if not PUBLISHED_CURVES:
114
+ _load_published_curves()
115
+
116
+ selected = PUBLISHED_CURVES[name][param]
117
+
118
+ return cls(name, strains=selected["strains"], values=selected["values"], param=param)
119
+
120
+ def __call__(self, strains):
121
+ """Return the nonlinear property at a specific strain.
122
+
123
+ If the strain is within the range of the provided strains, then the
124
+ value is interpolated in log-space is calculate the value at the
125
+ requested strain. If the strain falls outside the provided range
126
+ then the value corresponding to the smallest or largest value is
127
+ returned.
128
+
129
+ The interpolation is performed using either a cubic-spline, if enough
130
+ points are provided, or using linear interpolation.
131
+
132
+ Parameters
133
+ ----------
134
+ strains: float or array_like
135
+ Shear strain of interest [decimal].
136
+
137
+ Returns
138
+ -------
139
+ float or array_like
140
+ The nonlinear property at the requested strain(s).
141
+ """
142
+ ln_strains = np.log(np.maximum(1e-9, strains))
143
+
144
+ if self.strains.shape == self.values.shape:
145
+ # 1D interpolate
146
+ values = self._interpolater(ln_strains)
147
+ else:
148
+ ln_strains = np.atleast_1d(ln_strains)
149
+ values = np.array([i(ln_strains[0]) for i in self._interpolater])
150
+ return values
151
+
152
+ @property
153
+ def strains(self):
154
+ """Strains [decimal]."""
155
+ return self._strains
156
+
157
+ @strains.setter
158
+ def strains(self, strains):
159
+ self._strains = np.asarray(strains).astype(float)
160
+ self._update()
161
+
162
+ @property
163
+ def values(self):
164
+ """Values of either shear-modulus reduction or damping ratio."""
165
+ return self._values
166
+
167
+ @values.setter
168
+ def values(self, values):
169
+ self._values = np.asarray(values).astype(float)
170
+ self._update()
171
+
172
+ @property
173
+ def param(self):
174
+ """Nonlinear parameter name."""
175
+ return self._param
176
+
177
+ @param.setter
178
+ def param(self, value):
179
+ if value:
180
+ assert value in self.PARAMS
181
+ self._param = value
182
+
183
+ def _update(self):
184
+ """Initialize the interpolation."""
185
+
186
+ if not self.strains.size:
187
+ self._interpolater = None
188
+ return
189
+
190
+ x = np.log(self.strains)
191
+ y = self.values
192
+
193
+ if self.strains.shape == self.values.shape:
194
+ # 1D interpolate
195
+ self._interpolater = interp1d(
196
+ x, y, "linear", bounds_error=False, fill_value=(y[0], y[-1])
197
+ )
198
+ elif self.values.ndim == 2 and self.strains.shape[0] == self.values.shape[0]:
199
+ self._interpolater = [
200
+ interp1d(
201
+ x,
202
+ y[:, i],
203
+ "linear",
204
+ bounds_error=False,
205
+ fill_value=(y[0, i], y[-1, i]),
206
+ )
207
+ for i in range(y.shape[1])
208
+ ]
209
+ else:
210
+ self._interpolater = None
211
+
212
+
213
+ class SoilType(object):
214
+ """Soiltype that combines nonlinear behavior and material properties.
215
+
216
+ Parameters
217
+ ----------
218
+ name: str, optional
219
+ used for identification
220
+ unit_wt: float
221
+ unit weight of the material in [kN/m³]
222
+ mod_reduc: :class:`NonlinearProperty` or None
223
+ shear-modulus reduction curves. If None, linear behavior with no
224
+ reduction is used
225
+ damping: :class:`NonlinearProperty` or float
226
+ damping ratio. [decimal] If float, then linear behavior with constant
227
+ damping is used.
228
+ """
229
+
230
+ def __init__(self, name="", unit_wt=0.0, mod_reduc=None, damping=None):
231
+ self.name = name
232
+ self._unit_wt = unit_wt
233
+ self.mod_reduc = mod_reduc
234
+ self.damping = damping
235
+
236
+ @classmethod
237
+ def from_published(cls, name, unit_wt, *args):
238
+ if not PUBLISHED_CURVES:
239
+ _load_published_curves()
240
+
241
+ model_mr = args[0]
242
+ model_d = args[1] if len(args) > 1 else args[0]
243
+
244
+ return cls(
245
+ name,
246
+ unit_wt=unit_wt,
247
+ mod_reduc=NonlinearProperty.from_published(model_mr, "mod_reduc"),
248
+ damping=NonlinearProperty.from_published(model_d, "damping"),
249
+ )
250
+
251
+ def copy(self):
252
+ return SoilType(self.name, self.unit_wt, self.mod_reduc, self.damping)
253
+
254
+ @property
255
+ def density(self):
256
+ """Density of the soil in kg/m³."""
257
+ return self.unit_wt / GRAVITY
258
+
259
+ @property
260
+ def damping_min(self):
261
+ """Return the small-strain damping."""
262
+ try:
263
+ return self.damping.values[0]
264
+ except AttributeError:
265
+ return self.damping
266
+
267
+ @property
268
+ def quality(self):
269
+ return 1 / (2 * self.damping_min)
270
+
271
+ @property
272
+ def unit_wt(self):
273
+ return self._unit_wt
274
+
275
+ @property
276
+ def is_nonlinear(self):
277
+ """If nonlinear properties are specified."""
278
+ return any(isinstance(p, NonlinearProperty) for p in [self.mod_reduc, self.damping])
279
+
280
+ def __eq__(self, other):
281
+ # return all(
282
+ # getattr(self, attr) == getattr(other, attr)
283
+ # for attr in ['name', 'unit_wt', 'mod_reduc', 'damping'])
284
+ return (type(self) == type(other)) and (self.__dict__ == other.__dict__)
285
+
286
+ def __hash__(self):
287
+ return hash(self.__dict__.values())
288
+
289
+
290
+ class ModifiedHyperbolicSoilType(SoilType):
291
+ def __init__(self, name, unit_wt, damping_min, strains=None):
292
+ """
293
+
294
+ Parameters
295
+ ----------
296
+ name: str, optional
297
+ used for identification
298
+ unit_wt: float
299
+ unit weight of the material in [kN/m³]
300
+ damping_min: float
301
+ Minimum damping at low strains [decimal]
302
+ strains: `array_like`, default: np.logspace(-6, -1.5, num=20)
303
+ shear strains levels [decimal]
304
+ """
305
+ super().__init__(name, unit_wt)
306
+
307
+ if strains is None:
308
+ strains = np.logspace(-6, -1.5, num=20) # in decimal
309
+ else:
310
+ strains = np.asarray(strains)
311
+
312
+ # Modified hyperbolic shear modulus reduction
313
+ mod_reduc = 1 / (1 + (strains / self.strain_ref) ** self.curvature)
314
+ self.mod_reduc = NonlinearProperty(name, strains, mod_reduc, "mod_reduc")
315
+
316
+ # Masing damping based on shear -modulus reduction [%]
317
+ strains_percent = strains * 100
318
+ strain_ref_percent = self.strain_ref * 100
319
+ damping_masing_a1 = (100.0 / np.pi) * (
320
+ 4
321
+ * (
322
+ strains_percent
323
+ - strain_ref_percent
324
+ * np.log((strains_percent + strain_ref_percent) / strain_ref_percent)
325
+ )
326
+ / (strains_percent**2 / (strains_percent + strain_ref_percent))
327
+ - 2.0
328
+ )
329
+ # Correction between perfect hyperbolic strain model and modified
330
+ # model [%].
331
+ c1 = -1.1143 * self.curvature**2 + 1.8618 * self.curvature + 0.2523
332
+ c2 = 0.0805 * self.curvature**2 - 0.0710 * self.curvature - 0.0095
333
+ c3 = -0.0005 * self.curvature**2 + 0.0002 * self.curvature + 0.0003
334
+ damping_masing = (
335
+ c1 * damping_masing_a1 + c2 * damping_masing_a1**2 + c3 * damping_masing_a1**3
336
+ )
337
+
338
+ # Compute the damping correction in percent
339
+ d_correction = self.masing_scaling * damping_masing * mod_reduc**0.1
340
+
341
+ # Prevent the damping from reducing as it can at large strains
342
+ damping = np.maximum.accumulate(d_correction / 100.0)
343
+
344
+ # Add the minimum damping component
345
+ if isinstance(damping_min, np.ndarray):
346
+ # Broadcast
347
+ damping = damping_min + damping[:, np.newaxis]
348
+ else:
349
+ damping += damping_min
350
+
351
+ # Convert to decimal values
352
+ self.damping = NonlinearProperty(name, strains, damping, "damping")
353
+
354
+ @property
355
+ def masing_scaling(self):
356
+ """Scaling of the Masing damping component."""
357
+ return NotImplementedError
358
+
359
+ @property
360
+ def curvature(self):
361
+ """Curvature of the model."""
362
+ return NotImplementedError
363
+
364
+ @property
365
+ def strain_ref(self):
366
+ """Reference strain."""
367
+ return NotImplementedError
368
+
369
+
370
+ @dataclass
371
+ class TwoParamModifiedHyperbolicCoeffs:
372
+ a: float
373
+ b1: float
374
+ b2: float
375
+ g1: float
376
+ g2: float
377
+ d0: float
378
+ d1: float
379
+ d: float
380
+ c: float
381
+ gd1: float
382
+ gd2: float
383
+
384
+
385
+ class TwoParamModifiedHyperbolicSoilType(SoilType):
386
+ def __init__(
387
+ self,
388
+ name: str = "",
389
+ unit_wt: float = 0,
390
+ stress_mean: float = 101.3,
391
+ strains: Optional[npt.ArrayLike] = None,
392
+ coeffs: Optional[TwoParamModifiedHyperbolicCoeffs] = None,
393
+ ):
394
+ """
395
+
396
+ Parameters
397
+ ----------
398
+ name: str, optional
399
+ used for identification
400
+ unit_wt: float
401
+ unit weight of the material in [kN/m³]
402
+ stress_mean: float, default=101.3
403
+ mean effective stress [kN/m²]
404
+ damping_min: float
405
+ Minimum damping at low strains [decimal]
406
+ strains: `array_like`, default: np.logspace(-6, -1.5, num=20)
407
+ shear strains levels [decimal]
408
+ """
409
+
410
+ if coeffs is None:
411
+ C = TwoParamModifiedHyperbolicCoeffs(
412
+ 1.04, 0.438, -0.007, 0.011, 0.318, 1.47, -0.2, 13.125, 1.187, 0.11, 0.23
413
+ )
414
+ else:
415
+ C = coeffs
416
+
417
+ if strains is None:
418
+ strains = np.logspace(-6, -1.5, num=20) # in decimal
419
+ else:
420
+ strains = np.asarray(strains)
421
+
422
+ self._stress_mean = stress_mean
423
+ stress_mean_atm = stress_mean * KPA_TO_ATM
424
+
425
+ # Convert from percent to decimal strain
426
+ strain_mr = C.g1 * stress_mean_atm**C.g2 / 100
427
+ b = C.b1 + C.b2 * stress_mean_atm
428
+ values_mr = 1 / ((1 + (strains / strain_mr) ** C.a)) ** b
429
+ mod_reduc = NonlinearProperty(name, strains, values_mr)
430
+
431
+ d_min = C.d0 * stress_mean_atm**C.d1
432
+ # Convert from percent to decimal strain
433
+ strain_dr = (C.gd1 * stress_mean_atm**C.gd2) / 100
434
+ term = (strains / strain_dr) ** C.c
435
+ # Convert to damping in decimal
436
+ values_d = (C.d * term + d_min) / (term + 1) / 100
437
+ damping = NonlinearProperty(name, strains, values_d)
438
+
439
+ super().__init__(name, unit_wt, mod_reduc, damping)
440
+
441
+
442
+ class DarendeliSoilType(ModifiedHyperbolicSoilType):
443
+ """
444
+ Darendeli (2001) model for fine grained soils.
445
+
446
+ Parameters
447
+ ----------
448
+ unit_wt: float
449
+ unit weight of the material [kN/m³]
450
+ plas_index: float, default=0
451
+ plasticity index [percent]
452
+ ocr: float, default=1
453
+ over-consolidation ratio
454
+ stress_mean: float, default=101.3
455
+ mean effective stress [kN/m²]
456
+ freq: float, default=1
457
+ excitation frequency [Hz]
458
+ num_cycles: float, default=10
459
+ number of cycles of loading
460
+ strains: `array_like`, default: np.logspace(-6, -1.5, num=20)
461
+ shear strains levels [decimal]
462
+ """
463
+
464
+ def __init__(
465
+ self,
466
+ unit_wt=0.0,
467
+ plas_index=0,
468
+ ocr=1,
469
+ stress_mean=101.3,
470
+ freq=1,
471
+ num_cycles=10,
472
+ damping_min=None,
473
+ strains=None,
474
+ ):
475
+ self._plas_index = plas_index
476
+ self._ocr = ocr
477
+ self._stress_mean = stress_mean
478
+ self._freq = freq
479
+ self._num_cycles = num_cycles
480
+
481
+ if damping_min is None:
482
+ damping_min = self._calc_damping_min()
483
+
484
+ name = self._create_name()
485
+
486
+ super().__init__(name, unit_wt, damping_min, strains)
487
+
488
+ def _calc_damping_min(self):
489
+ """minimum damping [decimal]"""
490
+ return (
491
+ (0.8005 + 0.0129 * self._plas_index * self._ocr**-0.1069)
492
+ * (self._stress_mean * KPA_TO_ATM) ** -0.2889
493
+ * (1 + 0.2919 * np.log(self._freq))
494
+ ) / 100
495
+
496
+ @property
497
+ def masing_scaling(self):
498
+ # Masing correction factor
499
+ return 0.6329 - 0.00566 * np.log(self._num_cycles)
500
+
501
+ @property
502
+ def strain_ref(self):
503
+ """reference strain [decimal]"""
504
+ return (
505
+ (0.0352 + 0.0010 * self._plas_index * self._ocr**0.3246)
506
+ * (self._stress_mean * KPA_TO_ATM) ** 0.3483
507
+ ) / 100
508
+
509
+ @property
510
+ def curvature(self):
511
+ return 0.9190
512
+
513
+ def _create_name(self):
514
+ fmt = "Darendeli (PI={:.0f}, OCR={:.1f}, σₘ'={:.1f} kN/m²)"
515
+ return fmt.format(self._plas_index, self._ocr, self._stress_mean)
516
+
517
+
518
+ class MenqSoilType(ModifiedHyperbolicSoilType):
519
+ """
520
+ Menq SoilType for gravelly soils.
521
+
522
+ Parameters
523
+ ----------
524
+ unit_wt: float
525
+ unit weight of the material [kN/m³]
526
+ uniformity_coeff: float, default=10
527
+ uniformity coeffecient (Cᵤ)
528
+ diam_mean: float, default=5
529
+ mean diameter (D₅₀) [mm]
530
+ stress_mean: float, default=101.3
531
+ mean effective stress [kN/m²]
532
+ num_cycles: float, default=10
533
+ number of cycles of loading
534
+ strains: `array_like`, default: np.logspace(-4, 0.5, num=20)
535
+ shear strains levels [decimal]
536
+ """
537
+
538
+ def __init__(
539
+ self,
540
+ unit_wt=0.0,
541
+ uniformity_coeff=10,
542
+ diam_mean=5,
543
+ stress_mean=101.3,
544
+ num_cycles=10,
545
+ damping_min=None,
546
+ strains=None,
547
+ ):
548
+ self._uniformity_coeff = uniformity_coeff
549
+ self._diam_mean = diam_mean
550
+ self._stress_mean = stress_mean
551
+ self._num_cycles = num_cycles
552
+
553
+ if damping_min is None:
554
+ damping_min = self._calc_damping_min()
555
+
556
+ name = self._create_name()
557
+
558
+ super().__init__(name, unit_wt, damping_min, strains)
559
+
560
+ def _calc_damping_min(self):
561
+ return (
562
+ 0.55
563
+ * self._uniformity_coeff**0.1
564
+ * self._diam_mean**-0.3
565
+ * (self._stress_mean * KPA_TO_ATM) ** -0.08
566
+ ) / 100
567
+
568
+ @property
569
+ def masing_scaling(self):
570
+ # Masing correction factor
571
+ return 0.6329 - 0.00566 * np.log(self._num_cycles)
572
+
573
+ @property
574
+ def strain_ref(self):
575
+ return (
576
+ 0.12
577
+ * self._uniformity_coeff**-0.6
578
+ * (self._stress_mean * KPA_TO_ATM) ** (0.5 * self._uniformity_coeff**-0.15)
579
+ ) / 100
580
+
581
+ @property
582
+ def curvature(self):
583
+ return 0.86 + 0.1 * np.log10(self._stress_mean * KPA_TO_ATM)
584
+
585
+ def _create_name(self):
586
+ fmt = "Menq (Cᵤ={:.1f}, D₅₀={:.1f} mm, σₘ'={:.1f} kN/m²)"
587
+ return fmt.format(self._uniformity_coeff, self._diam_mean, self._stress_mean)
588
+
589
+
590
+ class FixedValues:
591
+ """Utility class to store fixed values"""
592
+
593
+ def __init__(self, **kwds):
594
+ self._params = kwds
595
+
596
+ def __getattr__(self, name):
597
+ return self._params[name]
598
+
599
+
600
+ class KishidaSoilType(SoilType):
601
+ """Empirical nonlinear model for highly organic soils.
602
+
603
+ Parameters
604
+ ----------
605
+ name: str, optional
606
+ used for identification
607
+ unit_wt: float or None, default=None
608
+ unit weight of the material [kN/m³]. If *None*, then unit weight is
609
+ computed by the empirical model.
610
+ stress_vert: float
611
+ vertical effective stress [kN/m²]
612
+ organic_content: float
613
+ organic_content [percent]
614
+ lab_consol_ratio: float, default=1
615
+ laboratory consolidation ratio. This parameter is included for
616
+ completeness, but the default value of 1 should be used for field
617
+ applications.
618
+ strains: `array_like` or None
619
+ shear strains levels. If *None*, a default of `np.logspace(-6, 0.5,
620
+ num=20)` will be used. The first strain should be small such that the
621
+ shear modulus reduction is equal to 1. [decimal]
622
+ """
623
+
624
+ def __init__(
625
+ self,
626
+ name="",
627
+ unit_wt=None,
628
+ stress_vert=101.3,
629
+ organic_content=10,
630
+ lab_consol_ratio=1,
631
+ strains=None,
632
+ ):
633
+ super().__init__(name, unit_wt)
634
+
635
+ self._stress_vert = float(stress_vert)
636
+ self._organic_content = float(organic_content)
637
+ self._lab_consol_ratio = float(lab_consol_ratio)
638
+
639
+ if strains is None:
640
+ strains = np.logspace(-6, -1.5, num=20)
641
+ else:
642
+ strains = np.asarray(strains)
643
+
644
+ strains_percent = strains * 100
645
+
646
+ # Mean values of the predictors defined in the paper
647
+ x_1_mean = -2.5
648
+ x_2_mean = 4.0
649
+ x_3_mean = 0.5
650
+ # Predictor variables
651
+ x_3 = 2.0 / (1 + np.exp(self._organic_content / 23))
652
+ strain_ref = self._calc_strain_ref(x_3, x_3_mean)
653
+ strain_ref_percent = strain_ref * 100
654
+ x_1 = np.log(strains_percent + strain_ref_percent)
655
+ x_2 = np.log(self._stress_vert)
656
+
657
+ if unit_wt is None:
658
+ self._unit_wt = self._calc_unit_wt(x_2, x_3)
659
+ else:
660
+ self._unit_wt = float(unit_wt)
661
+
662
+ # Convert to 1D arrays for matrix math support
663
+ ones = np.ones_like(strains)
664
+ x_2 = x_2 * ones
665
+ x_3 = x_3 * ones
666
+
667
+ mod_reducs = self._calc_mod_reduc(
668
+ strains_percent,
669
+ strain_ref_percent,
670
+ x_1,
671
+ x_1_mean,
672
+ x_2,
673
+ x_2_mean,
674
+ x_3,
675
+ x_3_mean,
676
+ )
677
+ dampings = self._calc_damping(mod_reducs, x_2, x_2_mean, x_3, x_3_mean)
678
+
679
+ name = self._create_name()
680
+ self.mod_reduc = NonlinearProperty(name, strains, mod_reducs, "mod_reduc")
681
+ self.damping = NonlinearProperty(name, strains, dampings, "damping")
682
+
683
+ @staticmethod
684
+ def _calc_strain_ref(x_3, x_3_mean):
685
+ """Compute the reference strain using Equation (6)."""
686
+ b_9 = -1.41
687
+ b_10 = -0.950
688
+ return np.exp(b_9 + b_10 * (x_3 - x_3_mean)) / 100
689
+
690
+ def _calc_mod_reduc(self, strains, strain_ref, x_1, x_1_mean, x_2, x_2_mean, x_3, x_3_mean):
691
+ """Compute the shear modulus reduction using Equation (1)."""
692
+
693
+ ones = np.ones_like(strains)
694
+ # Predictor
695
+ x_4 = np.log(self._lab_consol_ratio) * ones
696
+ x = np.c_[
697
+ ones,
698
+ x_1,
699
+ x_2,
700
+ x_3,
701
+ x_4,
702
+ (x_1 - x_1_mean) * (x_2 - x_2_mean),
703
+ (x_1 - x_1_mean) * (x_3 - x_3_mean),
704
+ (x_2 - x_2_mean) * (x_3 - x_3_mean),
705
+ (x_1 - x_1_mean) * (x_2 - x_2_mean) * (x_3 - x_3_mean),
706
+ ]
707
+ # Coefficients
708
+ denom = np.log(1 / strain_ref + strains / strain_ref) # TODO: is this percent or decimal?
709
+ b = np.c_[
710
+ 5.11 * ones,
711
+ -0.729 * ones,
712
+ (1 - 0.37 * x_3_mean * (1 + ((np.log(strain_ref) - x_1_mean) / denom))),
713
+ -0.693 * ones,
714
+ 0.8 - 0.4 * x_3,
715
+ 0.37 * x_3_mean / denom,
716
+ 0.0 * ones,
717
+ -0.37 * (1 + (np.log(strain_ref) - x_1_mean) / denom),
718
+ 0.37 / denom,
719
+ ]
720
+ ln_shear_mod = (b * x).sum(axis=1)
721
+ shear_mod = np.exp(ln_shear_mod)
722
+ mod_reduc = shear_mod / shear_mod[0]
723
+ return mod_reduc
724
+
725
+ @staticmethod
726
+ def _calc_damping(mod_reducs, x_2, x_2_mean, x_3, x_3_mean):
727
+ """Compute the damping ratio using Equation (16)."""
728
+ # Mean values of the predictors
729
+ x_1_mean = -1.0
730
+ x_1 = np.log(np.log(1 / mod_reducs) + 0.103)
731
+
732
+ ones = np.ones_like(mod_reducs)
733
+ x = np.c_[
734
+ ones,
735
+ x_1,
736
+ x_2,
737
+ x_3,
738
+ (x_1 - x_1_mean) * (x_2 - x_2_mean),
739
+ (x_2 - x_2_mean) * (x_3 - x_3_mean),
740
+ ]
741
+ c = np.c_[2.86, 0.571, -0.103, -0.141, 0.0419, -0.240]
742
+
743
+ ln_damping = (c * x).sum(axis=1)
744
+ return np.exp(ln_damping) / 100.0
745
+
746
+ @staticmethod
747
+ def _calc_unit_wt(x_1, x_2):
748
+ x = np.r_[1, x_1, x_2]
749
+ d = np.r_[-0.112, 0.038, 0.360]
750
+
751
+ ln_density = d.T @ x
752
+ unit_wt = np.exp(ln_density) * scipy.constants.g
753
+ return unit_wt
754
+
755
+ def _create_name(self):
756
+ return "Kishida (σᵥ'={:.1f} kN/m², OC={:.0f} %)".format(
757
+ self._stress_vert, self._organic_content
758
+ )
759
+
760
+
761
+ # TODO: for nonlinear site response this class wouldn't be used. Better way
762
+ # to do this? Maybe have the calculator create it?
763
+ class IterativeValue(object):
764
+ def __init__(self, value):
765
+ self._value = value
766
+ self._previous = 1e-9
767
+
768
+ @property
769
+ def value(self):
770
+ return self._value
771
+
772
+ @value.setter
773
+ def value(self, value):
774
+ self._previous = self._value
775
+ self._value = value
776
+
777
+ @property
778
+ def previous(self):
779
+ return self._previous
780
+
781
+ @property
782
+ def relative_error(self):
783
+ """The relative error, in percent, between the two iterations."""
784
+ if self.previous is not None:
785
+ # FIXME
786
+ # Use the maximum strain value -- this is important for error
787
+ # calculation with frequency dependent properties
788
+ # prev = np.max(self.previous)
789
+ # value = np.max(self.value)
790
+ try:
791
+ err = 100.0 * np.max((self.previous - self.value) / self.value)
792
+ except ZeroDivisionError:
793
+ err = np.inf
794
+ else:
795
+ err = 0
796
+ return err
797
+
798
+ def reset(self):
799
+ self._previous = None
800
+
801
+
802
+ class Layer(object):
803
+ """Docstring for Layer"""
804
+
805
+ def __init__(self, soil_type, thickness, shear_vel):
806
+ """@todo: to be defined!"""
807
+ self._profile = None
808
+
809
+ self._soil_type = soil_type
810
+ self._thickness = thickness
811
+ self.initial_shear_vel = shear_vel
812
+
813
+ self._depth = 0
814
+ self._stress_vert = 0
815
+
816
+ def __repr__(self):
817
+ index = self._profile.index(self)
818
+ shear_vel = self._initial_shear_vel
819
+ thickness = self._thickness
820
+ st_name = self.soil_type.name
821
+ return (
822
+ f"<Layer(index={index}, "
823
+ f"shear_vel={shear_vel:0.1f} m/s, "
824
+ f"thickness={thickness:0.1f} m, "
825
+ f"soil_type={st_name})>"
826
+ )
827
+
828
+ def __eq__(self, other):
829
+ attrs = ["_soil_type", "_thickness", "initial_shear_vel"]
830
+ return (type(self) == type(other)) and all(
831
+ [getattr(self, a) == getattr(other, a) for a in attrs]
832
+ )
833
+
834
+ def __hash__(self):
835
+ return hash(self.__dict__.values())
836
+
837
+ @classmethod
838
+ def copy_of(cls, other):
839
+ """Return a copy of the Layer instance with previously defined SoilType."""
840
+ return cls(other.soil_type, other.thickness, other.shear_vel)
841
+
842
+ @property
843
+ def depth(self):
844
+ """Depth to the top of the layer [m]."""
845
+ return self._depth
846
+
847
+ @property
848
+ def depth_mid(self):
849
+ """Depth to the middle of the layer [m]."""
850
+ return self._depth + self._thickness / 2
851
+
852
+ @property
853
+ def depth_base(self):
854
+ """Depth to the base of the layer [m]."""
855
+ return self._depth + self._thickness
856
+
857
+ @property
858
+ def density(self):
859
+ """Density of soil in [kg/m³]."""
860
+ return self.soil_type.density
861
+
862
+ @property
863
+ def damping(self):
864
+ """Strain-compatible damping."""
865
+ try:
866
+ value = self._damping.value
867
+ except AttributeError:
868
+ value = self._damping
869
+ return value
870
+
871
+ @property
872
+ def initial_shear_mod(self):
873
+ """Initial (small-strain) shear modulus [kN/m²]."""
874
+ return self.density * self.initial_shear_vel**2
875
+
876
+ @property
877
+ def initial_shear_vel(self):
878
+ """Initial (small-strain) shear-wave velocity [m/s]."""
879
+ return self._initial_shear_vel
880
+
881
+ @initial_shear_vel.setter
882
+ def initial_shear_vel(self, value: float):
883
+ """Set initial (small-strain) shear-wave velocity [m/s]."""
884
+
885
+ self._initial_shear_vel = value
886
+ # Reset the iterated values
887
+ self.reset()
888
+
889
+ @property
890
+ def comp_shear_mod(self):
891
+ """Strain-compatible complex shear modulus [kN/m²]."""
892
+ damping = self.damping
893
+ if COMP_MODULUS_MODEL == "seed":
894
+ # Frequency independent model (Seed et al., 1970)
895
+ # Correct dissipated energy
896
+ # Incorrect shear modulus: G * \sqrt{1 + 4 \beta^2 }
897
+ comp_factor = 1 + 2j * damping
898
+ elif COMP_MODULUS_MODEL == "kramer":
899
+ # Simplifed shear modulus (Kramer, 1996)
900
+ # Correct dissipated energy
901
+ # Incorrect shear modulus: G * \sqrt{1 + 2 \beta^2 + \beta^4 }
902
+ comp_factor = 1 - damping**2 + 2j * damping
903
+ elif COMP_MODULUS_MODEL == "dormieux":
904
+ # Dormieux and Canou (1990)
905
+ # Correct dissipated energy
906
+ # Correct shear modulus:
907
+ comp_factor = np.sqrt(1 - 4 * damping**2) + 2j * damping
908
+ else:
909
+ raise NotImplementedError
910
+ comp_shear_mod = self.shear_mod * comp_factor
911
+ return comp_shear_mod
912
+
913
+ @property
914
+ def comp_shear_vel(self):
915
+ """Strain-compatible complex shear-wave velocity [m/s]."""
916
+ return np.sqrt(self.comp_shear_mod / self.density)
917
+
918
+ @property
919
+ def max_error(self):
920
+ return max(self._shear_mod.relative_error, self._damping.relative_error)
921
+
922
+ def reset(self):
923
+ self._shear_mod = IterativeValue(self.initial_shear_mod)
924
+ self._damping = IterativeValue(self.soil_type.damping_min)
925
+ # Use a small initial value
926
+ self._strain = IterativeValue(1e-6)
927
+
928
+ self.strain_max = None
929
+
930
+ @property
931
+ def shear_mod(self):
932
+ """Strain-compatible shear modulus [kN//m²]."""
933
+ try:
934
+ value = self._shear_mod.value
935
+ except AttributeError:
936
+ value = self._shear_mod
937
+ return value
938
+
939
+ @property
940
+ def shear_mod_reduc(self):
941
+ return self.shear_mod / self.initial_shear_mod
942
+
943
+ @property
944
+ def shear_vel(self):
945
+ """Strain-compatible shear-wave velocity [m/s]."""
946
+ return np.sqrt(self.shear_mod / self.density)
947
+
948
+ @property
949
+ def stress_shear_eff(self):
950
+ """Effective shear stress at layer midpoint"""
951
+ return self.shear_mod * self.strain
952
+
953
+ @property
954
+ def stress_shear_max(self):
955
+ """Maximum shear stress at layer midpoint"""
956
+ return self.shear_mod * self.strain_max
957
+
958
+ @property
959
+ def strain(self):
960
+ try:
961
+ value = self._strain.value
962
+ except AttributeError:
963
+ value = self._strain
964
+
965
+ return value
966
+
967
+ @strain.setter
968
+ def strain(self, strain):
969
+ if self.soil_type.is_nonlinear:
970
+ self._strain.value = strain
971
+ else:
972
+ self._strain = strain
973
+
974
+ # Update the shear modulus and damping
975
+ try:
976
+ mod_reduc = self.soil_type.mod_reduc(strain)
977
+ except TypeError:
978
+ mod_reduc = 1.0
979
+
980
+ self._shear_mod.value = self.initial_shear_mod * mod_reduc
981
+
982
+ try:
983
+ self._damping.value = self.soil_type.damping(strain)
984
+ except TypeError:
985
+ # No iteration provided by damping
986
+ self._damping.value = self.soil_type.damping
987
+
988
+ @property
989
+ def soil_type(self):
990
+ return self._soil_type
991
+
992
+ @property
993
+ def thickness(self):
994
+ return self._thickness
995
+
996
+ @thickness.setter
997
+ def thickness(self, thickness):
998
+ self._thickness = thickness
999
+ self._profile.update_layers(self._profile.index(self) + 1)
1000
+
1001
+ @property
1002
+ def travel_time(self):
1003
+ """Travel time through the layer."""
1004
+ return self.thickness / self.shear_vel
1005
+
1006
+ @property
1007
+ def unit_wt(self):
1008
+ return self.soil_type.unit_wt
1009
+
1010
+ def stress_vert(self, depth_within=0, effective=False):
1011
+ """Vertical stress from the top of the layer [kN//m²]."""
1012
+ assert depth_within <= self.thickness
1013
+ stress_vert = self._stress_vert + depth_within * self.unit_wt
1014
+ if effective:
1015
+ pore_pressure = self._profile.pore_pressure(self.depth + depth_within)
1016
+ stress_vert -= pore_pressure
1017
+ return stress_vert
1018
+
1019
+ def stress_mean(self, depth_within=0, effective=False, k0=0.5):
1020
+ """Mean effective stress from the top of the layer [kN//m²]."""
1021
+ stress_vert = self.stress_vert(depth_within, effective)
1022
+ return (2 * k0 * stress_vert + stress_vert) / 3.0
1023
+
1024
+ @property
1025
+ def incr_site_atten(self):
1026
+ return (2 * self.soil_type.damping_min * self._thickness) / self.initial_shear_vel
1027
+
1028
+
1029
+ class Location(object):
1030
+ """Location within a profile"""
1031
+
1032
+ def __init__(self, index, layer, wave_field, depth_within=0):
1033
+ self._index = index
1034
+ self._layer = layer
1035
+ self._depth_within = depth_within
1036
+
1037
+ if not isinstance(wave_field, WaveField):
1038
+ wave_field = WaveField[wave_field]
1039
+ self._wave_field = wave_field
1040
+
1041
+ @property
1042
+ def depth_within(self):
1043
+ return self._depth_within
1044
+
1045
+ @property
1046
+ def layer(self):
1047
+ return self._layer
1048
+
1049
+ @property
1050
+ def index(self):
1051
+ return self._index
1052
+
1053
+ @property
1054
+ def wave_field(self):
1055
+ return self._wave_field
1056
+
1057
+ def stress_vert(self, effective=False):
1058
+ return self._layer.stress_vert(self.depth_within, effective=effective)
1059
+
1060
+ def __repr__(self):
1061
+ return (
1062
+ "<Location(layer_index={_index}, depth_within={_depth_within} "
1063
+ "wave_field={_wave_field})>".format(**self.__dict__)
1064
+ )
1065
+
1066
+
1067
+ class Profile(collections.abc.Container):
1068
+ """Soil profile with an infinite halfspace at the base."""
1069
+
1070
+ def __init__(self, layers=None, wt_depth=0):
1071
+ super().__init__()
1072
+ self.layers = layers or []
1073
+ self.wt_depth = wt_depth
1074
+ if layers:
1075
+ self.update_layers()
1076
+
1077
+ @classmethod
1078
+ def from_dataframe(cls, df, wt_depth=0):
1079
+ """Create a profile based on a table with columns:
1080
+ - thickness (m)
1081
+ - vel_shear (m)
1082
+ - unit_wt (kN/m³)
1083
+ - damping (dec)
1084
+ """
1085
+
1086
+ layers = []
1087
+ for _, row in df.iterrows():
1088
+ layers.append(
1089
+ Layer(
1090
+ SoilType(
1091
+ name=row.get("name", ""),
1092
+ unit_wt=row["unit_wt"],
1093
+ mod_reduc=None,
1094
+ damping=row["damping"],
1095
+ ),
1096
+ row["thickness"],
1097
+ row["vel_shear"],
1098
+ )
1099
+ )
1100
+ return cls(layers, wt_depth)
1101
+
1102
+ def __iter__(self):
1103
+ return iter(self.layers)
1104
+
1105
+ def __contains__(self, value):
1106
+ return value in self.layers
1107
+
1108
+ def __len__(self):
1109
+ return len(self.layers)
1110
+
1111
+ def __getitem__(self, key):
1112
+ return self.layers[key]
1113
+
1114
+ @classmethod
1115
+ def copy_of(cls, other):
1116
+ """Return a copy of the profile with new Layer instances."""
1117
+ return cls([Layer.copy_of(layer) for layer in other], other.wt_depth)
1118
+
1119
+ def index(self, layer):
1120
+ return self.layers.index(layer)
1121
+
1122
+ def append(self, layer):
1123
+ last = len(self.layers)
1124
+ self.layers.append(layer)
1125
+ self.update_layers(last)
1126
+
1127
+ def insert(self, index, layer):
1128
+ self.layers.insert(index, layer)
1129
+ self.update_layers(index)
1130
+
1131
+ def reset_layers(self):
1132
+ """Set initial properties from the soil types."""
1133
+ for layer in self:
1134
+ layer.reset()
1135
+
1136
+ def update_layers(self, start_layer=0):
1137
+ if start_layer < 1:
1138
+ depth = 0
1139
+ stress_vert = 0
1140
+ else:
1141
+ ref_layer = self[start_layer - 1]
1142
+ depth = ref_layer.depth_base
1143
+ stress_vert = ref_layer.stress_vert(ref_layer.thickness, effective=False)
1144
+
1145
+ for layer in self[start_layer:]:
1146
+ layer._profile = self
1147
+ layer._depth = depth
1148
+ layer._stress_vert = stress_vert
1149
+ if layer != self[-1]:
1150
+ # Use the layer to compute the values at the base of the
1151
+ # layer, and apply them at the top of the next layer
1152
+ depth = layer.depth_base
1153
+ stress_vert = layer.stress_vert(layer.thickness, effective=False)
1154
+
1155
+ def iter_soil_types(self):
1156
+ yielded = set()
1157
+ for layer in self:
1158
+ if layer.soil_type in yielded:
1159
+ continue
1160
+ else:
1161
+ yielded.add(layer)
1162
+ yield layer.soil_type
1163
+
1164
+ def auto_discretize(
1165
+ self,
1166
+ max_freq: float = 50.0,
1167
+ wave_frac: float = 0.2,
1168
+ nonlinear_only: bool = True,
1169
+ ) -> Profile:
1170
+ """Subdivide the layers to capture strain variation.
1171
+
1172
+ Parameters
1173
+ ----------
1174
+ max_freq: float
1175
+ Maximum frequency of interest [Hz].
1176
+ wave_frac: float
1177
+ Fraction of wavelength required. Typically 1/3 to 1/5.
1178
+
1179
+ max_thick: float *optional*
1180
+ If provided, layers are limited to be at most that thick. This is applied to
1181
+ all layers regardless of nonlinearity.
1182
+
1183
+ Returns
1184
+ -------
1185
+ profile: Profile
1186
+ A new profile with modified layer thicknesses
1187
+ """
1188
+ layers = []
1189
+ for l in self[:-1]:
1190
+ if not nonlinear_only or l.soil_type.is_nonlinear:
1191
+ opt_thickness = l.shear_vel / max_freq * wave_frac
1192
+ count = max(np.ceil(l.thickness / opt_thickness).astype(int), 1)
1193
+ thickness = l.thickness / count
1194
+ for _ in range(count):
1195
+ layers.append(Layer(l.soil_type, thickness, l.shear_vel))
1196
+ else:
1197
+ layers.append(l)
1198
+ # Add the halfspace
1199
+ layers.append(self[-1])
1200
+
1201
+ return Profile(layers, wt_depth=self.wt_depth)
1202
+
1203
+ def pore_pressure(self, depth):
1204
+ """Pore pressure at a given depth in [kN//m²].
1205
+
1206
+ Parameters
1207
+ ----------
1208
+ depth
1209
+
1210
+ Returns
1211
+ -------
1212
+ pore_pressure
1213
+ """
1214
+ return GRAVITY * max(depth - self.wt_depth, 0)
1215
+
1216
+ def site_attenuation(self):
1217
+ return sum(layer.incr_site_atten for layer in self)
1218
+
1219
+ def lookup_depth(self, depth: float) -> Tuple[int, float]:
1220
+ """Look up the layer and the depth within the layer for a specific depth.
1221
+
1222
+ Parameters
1223
+ ----------
1224
+ depth: float
1225
+ Depth corresponding to the location of interest.
1226
+
1227
+ Returns
1228
+ -------
1229
+ index: int
1230
+ Layer index
1231
+
1232
+ depth_within: float
1233
+ Depth from the top of the layer to achieve the specific depth.
1234
+ """
1235
+
1236
+ # Make sure all of the depths to updated
1237
+ self.update_layers()
1238
+
1239
+ for i, layer in enumerate(self[:-1]):
1240
+ if layer.depth <= depth < layer.depth_base:
1241
+ depth_within = depth - layer.depth
1242
+ break
1243
+ else:
1244
+ # Bedrock
1245
+ i = len(self) - 1
1246
+ depth_within = depth - self[-1].depth
1247
+
1248
+ return i, depth_within
1249
+
1250
+ def location(self, wave_field, depth=None, index=None):
1251
+ """Create a Location for a specific depth.
1252
+
1253
+ Parameters
1254
+ ----------
1255
+ wave_field: str
1256
+ Wave field. See :class:`Location` for possible values.
1257
+ depth: float, optional
1258
+ Depth corresponding to the :class`Location` of interest. If
1259
+ provided, then index is ignored.
1260
+ index: int, optional
1261
+ Index corresponding to layer of interest in :class:`Profile`. If
1262
+ provided, then depth is ignored and location is provided a top
1263
+ of layer.
1264
+
1265
+ Returns
1266
+ -------
1267
+ Location
1268
+ Corresponding :class:`Location` object.
1269
+ """
1270
+ if not isinstance(wave_field, WaveField):
1271
+ wave_field = WaveField[wave_field]
1272
+
1273
+ if index is None and depth is not None:
1274
+ i, depth_within = self.lookup_depth(depth)
1275
+ layer = self[i]
1276
+ elif index is not None and depth is None:
1277
+ layer = self[index]
1278
+ i = self.index(layer)
1279
+ depth_within = 0
1280
+ else:
1281
+ raise NotImplementedError
1282
+
1283
+ return Location(i, layer, wave_field, depth_within)
1284
+
1285
+ def time_average_vel(self, depth):
1286
+ """Calculate the time-average velocity.
1287
+
1288
+ Parameters
1289
+ ----------
1290
+ depth: float
1291
+ Depth over which the average velocity is computed.
1292
+
1293
+ Returns
1294
+ -------
1295
+ avg_vel: float
1296
+ Time averaged velocity.
1297
+ """
1298
+ depths = self.depth
1299
+ # Final layer is infinite and is treated separately
1300
+ travel_times = np.r_[0, self.travel_time[:-1]]
1301
+ # If needed, add the final layer to the required depth
1302
+ if depths[-1] < depth:
1303
+ depths = np.r_[depths, depth]
1304
+ travel_times = np.r_[travel_times, (depth - self[-1].depth) / self[-1].shear_vel]
1305
+
1306
+ total_travel_times = np.cumsum(travel_times)
1307
+ # Interpolate the travel time to the depth of interest
1308
+ avg_shear_vel = depth / np.interp(depth, depths, total_travel_times)
1309
+ return avg_shear_vel
1310
+
1311
+ def simplified_rayliegh_vel(self):
1312
+ """Simplified Rayliegh velocity of the site.
1313
+
1314
+ This follows the simplifications proposed by Urzua et al. (2017)
1315
+
1316
+ Returns
1317
+ -------
1318
+ rayleigh_vel : float
1319
+ Equivalent shear-wave velocity.
1320
+ """
1321
+ # FIXME: What if last layer has no thickness?
1322
+ thicks = self.thickness
1323
+ depths_mid = self.depth_mid
1324
+ shear_vels = self.initial_shear_vel
1325
+
1326
+ mode_incr = depths_mid * thicks / shear_vels**2
1327
+ # Mode shape is computed as the sumation from the base of
1328
+ # the profile. Need to append a 0 for the roll performed in the next
1329
+ # step
1330
+ shape = np.r_[np.cumsum(mode_incr[::-1])[::-1], 0]
1331
+
1332
+ # Roll is used to offset the mode_shape so that the sum
1333
+ # can be calculated for two adjacent layers
1334
+ freq_fund = np.sqrt(
1335
+ 4
1336
+ * np.sum(thicks * depths_mid**2 / shear_vels**2)
1337
+ / np.sum(thicks * np.sum(np.c_[shape, np.roll(shape, -1)], axis=1)[:-1] ** 2)
1338
+ )
1339
+ period_fun = 2 * np.pi / freq_fund
1340
+ rayleigh_vel = 4 * thicks.sum() / period_fun
1341
+ return rayleigh_vel
1342
+
1343
+ def plot(self, prop, ax=None, plot_kwds=None, axis_kwds=None):
1344
+ # Defaults
1345
+ xlabels = {
1346
+ "density": "Density (kN/m³)",
1347
+ "max_error": "Max. Error (%)",
1348
+ "travel_time": "Travel time (sec)",
1349
+ "slowness": "Slowness (1/s)",
1350
+ "initial_shear_vel": "Initial $V_s$ (m/s)",
1351
+ "shear_vel": "$V_s$ (m/s)",
1352
+ "strain": "Strain (dec)",
1353
+ "damping": "Damping (dec)",
1354
+ }
1355
+ _axis_kwds = {
1356
+ "ylabel": "Depth (m)",
1357
+ "ylim": (1.1 * self.depth[-1], 0),
1358
+ "xlabel": xlabels[prop],
1359
+ "xlim": (0, None),
1360
+ }
1361
+
1362
+ plot_kwds = plot_kwds or dict()
1363
+ axis_kwds = {**_axis_kwds, **(axis_kwds or dict())}
1364
+
1365
+ if ax is None:
1366
+ fig, ax = plt.subplots()
1367
+
1368
+ ax.step(getattr(self, prop), self.depth, where="pre", **plot_kwds)
1369
+ ax.set(**axis_kwds)
1370
+
1371
+ return ax
1372
+
1373
+ @property
1374
+ def damping(self):
1375
+ return self._get_values("damping")
1376
+
1377
+ @property
1378
+ def density(self):
1379
+ return self._get_values("density")
1380
+
1381
+ @property
1382
+ def depth(self):
1383
+ return self._get_values("depth")
1384
+
1385
+ @property
1386
+ def depth_mid(self):
1387
+ return self._get_values("depth_mid")
1388
+
1389
+ @property
1390
+ def thickness(self):
1391
+ return self._get_values("thickness")
1392
+
1393
+ @property
1394
+ def max_error(self):
1395
+ return self._get_values("max_error")
1396
+
1397
+ @property
1398
+ def travel_time(self):
1399
+ return self._get_values("travel_time")
1400
+
1401
+ @property
1402
+ def slowness(self):
1403
+ return 1 / self.initial_shear_vel
1404
+
1405
+ @property
1406
+ def initial_shear_vel(self):
1407
+ return self._get_values("initial_shear_vel")
1408
+
1409
+ @property
1410
+ def shear_vel(self):
1411
+ return self._get_values("shear_vel")
1412
+
1413
+ @property
1414
+ def strain(self):
1415
+ return self._get_values("strain")
1416
+
1417
+ @property
1418
+ def unit_wt(self):
1419
+ return self._get_values("unit_wt")
1420
+
1421
+ def _get_values(self, attr):
1422
+ return np.array([getattr(layer, attr) for layer in self])