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