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/output.py ADDED
@@ -0,0 +1,832 @@
1
+ # -*- coding: utf-8 -*-
2
+ # The MIT License (MIT)
3
+ #
4
+ # Copyright (c) 2016-2021 Albert Kottke
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ import collections
24
+
25
+ import matplotlib.pyplot as plt
26
+ import numpy as np
27
+ import scipy.integrate
28
+ from matplotlib.colors import LogNorm
29
+ from matplotlib.colors import TwoSlopeNorm
30
+ from scipy.interpolate import interp1d
31
+
32
+ try:
33
+ import pandas as pd
34
+ except ImportError:
35
+ pd = None
36
+
37
+ import pykooh
38
+
39
+ from .motion import TimeSeriesMotion, WaveField, GRAVITY
40
+
41
+
42
+ def plot_amplification_evolv(
43
+ calc,
44
+ metric="accel_tf",
45
+ depths=None,
46
+ freqs=None,
47
+ normalized=False,
48
+ wave_field_out="within",
49
+ diverging_cmap=True,
50
+ include_vs_profile=False,
51
+ ax=None,
52
+ **kwds,
53
+ ):
54
+ # Default plotting kwds. Combine both set of plot keywords and prefer the provided
55
+ kwds = {
56
+ "cmap": "RdBu" if diverging_cmap else "magma_r",
57
+ "shading": "gouraud",
58
+ "norm": TwoSlopeNorm(vmin=0, vcenter=1) if diverging_cmap else LogNorm(),
59
+ } | kwds
60
+
61
+ if freqs is None:
62
+ freqs = np.logspace(-1, 2, num=301)
63
+
64
+ osc_damping = 0.05 if "osc_damping" not in kwds else kwds["osc_damping"]
65
+
66
+ ln_freqs = np.log(freqs)
67
+ ln_freqs_mot = np.log(calc.motion.freqs)
68
+
69
+ def get_amp(metric, depth):
70
+ loc_output = calc.profile.location(wave_field_out, depth=depth)
71
+ if metric == "accel_tf":
72
+ y = np.abs(calc.calc_accel_tf(calc.loc_input, loc_output))
73
+ # Interpolate the specific frequencies
74
+ y = np.interp(ln_freqs, ln_freqs_mot, y)
75
+ elif metric == "site_amp":
76
+ if get_amp.in_ars is None:
77
+ get_amp.in_ars = calc.motion.calc_osc_accels(freqs, osc_damping)
78
+
79
+ out_ars = calc.motion.calc_osc_accels(
80
+ freqs, osc_damping, calc.calc_accel_tf(calc.loc_input, loc_output)
81
+ )
82
+ y = out_ars / get_amp.in_ars
83
+ else:
84
+ raise NotImplementedError
85
+
86
+ return y
87
+
88
+ # Initialize static variable
89
+ get_amp.in_ars = None
90
+
91
+ if depths is None:
92
+ depths = np.linspace(0, calc.profile[-1].depth)
93
+
94
+ if ax is None:
95
+ fig, ax = plt.subplots()
96
+
97
+ amps = np.array([get_amp(metric, d) for d in depths])
98
+ if normalized:
99
+ amps /= amps[-1, :]
100
+
101
+ cf = ax.pcolormesh(freqs, depths, amps, **kwds)
102
+
103
+ cb = plt.colorbar(cf, ax=ax)
104
+ cb.set_label("|TF|" if metric == "accel_tf" else "Site Ampl.")
105
+
106
+ ax.set(
107
+ xlabel="Frequency (Hz)",
108
+ xscale="log",
109
+ ylabel="Depth (m)",
110
+ yscale="linear",
111
+ ylim=(depths[0], 0),
112
+ )
113
+
114
+ return ax
115
+
116
+
117
+ class OutputCollection(collections.abc.Collection):
118
+ def __init__(self, outputs):
119
+ super().__init__()
120
+ self.outputs = outputs
121
+
122
+ def __iter__(self):
123
+ return iter(self.outputs)
124
+
125
+ def __contains__(self, value):
126
+ return value in self.outputs
127
+
128
+ def __len__(self):
129
+ return len(self.outputs)
130
+
131
+ def __getitem__(self, key):
132
+ return self.outputs[key]
133
+
134
+ def __call__(self, calc, name=None):
135
+ # Save results
136
+ for o in self:
137
+ o(calc, name=name)
138
+
139
+ def reset(self):
140
+ for o in self:
141
+ o.reset()
142
+
143
+
144
+ def append_arrays(many, single):
145
+ """Append an array to another padding with NaNs for constant length.
146
+
147
+ Parameters
148
+ ----------
149
+ many : array_like of rank (j, k)
150
+ values appended to a copy of this array. This may be a 1-D or 2-D
151
+ array.
152
+ single : array_like of rank l
153
+ values to append. This should be a 1-D array.
154
+
155
+ Returns
156
+ -------
157
+ append : :class:`numpy.ndarray`
158
+ 2-D array with rank (j + 1, max(k, l)) with missing values padded
159
+ with :class:`numpy.nan`
160
+ """
161
+ assert np.ndim(single) == 1
162
+
163
+ # Check if the values need to be padded to for equal length
164
+ diff = single.shape[0] - many.shape[0]
165
+ if diff < 0:
166
+ single = np.pad(single, (0, -diff), "constant", constant_values=np.nan)
167
+ elif diff > 0:
168
+ # Need different padding based on if many is 1d or 2d.
169
+ padding = ((0, diff), (0, 0)) if len(many.shape) > 1 else (0, diff)
170
+ many = np.pad(many, padding, "constant", constant_values=np.nan)
171
+ else:
172
+ # No padding needed
173
+ pass
174
+ return np.c_[many, single]
175
+
176
+
177
+ class Output(object):
178
+ _const_ref = False
179
+
180
+ xscale = "log"
181
+ yscale = "log"
182
+ drawstyle = "default"
183
+
184
+ def __init__(self, refs=None):
185
+ self._refs = np.asarray([] if refs is None else refs)
186
+ self._values = None
187
+ self._names = []
188
+
189
+ def __call__(self, calc, name=None):
190
+ if name is None:
191
+ if self.values is None:
192
+ i = 1
193
+ elif len(self.values.shape) == 1:
194
+ i = 2
195
+ else:
196
+ i = self.values.shape[1] + 1
197
+ name = "r%d" % i
198
+ self._names.append(name)
199
+
200
+ @property
201
+ def refs(self):
202
+ return self._refs
203
+
204
+ @property
205
+ def values(self):
206
+ return self._values
207
+
208
+ @property
209
+ def names(self):
210
+ return self._names
211
+
212
+ def reset(self):
213
+ self._values = None
214
+ self._names = []
215
+ if not self._const_ref:
216
+ self._refs = np.array([])
217
+
218
+ def iter_results(self):
219
+ shared_ref = len(self.refs.shape) == 1
220
+ for i, name in enumerate(self.names):
221
+ refs = self.refs if shared_ref else self.refs[:, i]
222
+ values = self.values if len(self.values.shape) == 1 else self.values[:, i]
223
+ yield name, refs, values
224
+
225
+ def _add_refs(self, refs):
226
+ refs = np.asarray(refs)
227
+ if len(self._refs) == 0:
228
+ self._refs = np.array(refs)
229
+ else:
230
+ self._refs = append_arrays(self._refs, refs)
231
+
232
+ def _add_values(self, values):
233
+ values = np.asarray(values)
234
+ if self._values is None:
235
+ self._values = values
236
+ else:
237
+ self._values = append_arrays(self._values, values)
238
+
239
+ def calc_stats(self, as_dataframe=False):
240
+ ln_values = np.log(self.values)
241
+ median = np.exp(np.nanmean(ln_values, axis=1))
242
+ ln_std = np.nanstd(ln_values, axis=1)
243
+
244
+ stats = {"ref": self.refs, "median": median, "ln_std": ln_std}
245
+ if as_dataframe and pd:
246
+ stats = pd.DataFrame(stats).set_index("ref")
247
+ stats.index.name = self.ref_name
248
+
249
+ return stats
250
+
251
+ def to_dataframe(self):
252
+ if not pd:
253
+ raise RuntimeError("Install `pandas` library.")
254
+
255
+ if isinstance(self.names[0], tuple):
256
+ columns = pd.MultiIndex.from_tuples(self.names)
257
+ else:
258
+ columns = self.names
259
+
260
+ df = pd.DataFrame(self.values, index=self.refs, columns=columns)
261
+
262
+ return df
263
+
264
+ @staticmethod
265
+ def _get_xy(refs, values):
266
+ return refs, values
267
+
268
+ def plot(self, ax=None, style="indiv"):
269
+ assert style in ["stats", "indiv"]
270
+
271
+ if ax is None:
272
+ fig, ax = plt.subplots()
273
+
274
+ if style == "stats" and len(self.values.shape) > 1 and self.values.shape[1] < 3:
275
+ raise RuntimeError("Unable to plot stats for less than 3 values.")
276
+
277
+ if style == "stats":
278
+ kwds = {"color": "C0", "alpha": 0.6, "lw": 0.8, "drawstyle": self.drawstyle}
279
+ elif style == "indiv":
280
+ kwds = {"lw": 1.0, "drawstyle": self.drawstyle}
281
+ else:
282
+ raise NotImplementedError("Valid options are: stats, indiv.")
283
+
284
+ # Add the data
285
+ x, y = self._get_xy(self.refs, self.values)
286
+ lines = ax.plot(x, y, **kwds)
287
+
288
+ if style == "stats":
289
+ lines[0].set_label("Realization")
290
+ else:
291
+ for layer, name in zip(lines, self.names):
292
+ layer.set_label(name)
293
+
294
+ if style == "stats":
295
+ stats = self.calc_stats()
296
+
297
+ ax.plot(
298
+ *self._get_xy(stats["ref"], stats["median"]),
299
+ color="C1",
300
+ lw=2,
301
+ label="Median",
302
+ )
303
+
304
+ ax.set(
305
+ xlabel=self.xlabel,
306
+ xscale=self.xscale,
307
+ ylabel=self.ylabel,
308
+ yscale=self.yscale,
309
+ )
310
+
311
+ if len(lines) > 1:
312
+ ax.legend()
313
+
314
+ return ax
315
+
316
+
317
+ class OutputLocation(object):
318
+ def __init__(self, wave_field, depth=None, index=None):
319
+ self._depth = depth
320
+ self._index = index
321
+ if not isinstance(wave_field, WaveField):
322
+ wave_field = WaveField[wave_field]
323
+ self._wave_field = wave_field
324
+
325
+ @property
326
+ def depth(self):
327
+ return self._depth
328
+
329
+ @property
330
+ def index(self):
331
+ return self._index
332
+
333
+ @property
334
+ def wave_field(self):
335
+ return self._wave_field
336
+
337
+ def __call__(self, profile):
338
+ """Lookup the location with the profile."""
339
+ return profile.location(self.wave_field, depth=self.depth, index=self.index)
340
+
341
+
342
+ class LocationBasedOutput(Output):
343
+ def __init__(self, ref, location):
344
+ super().__init__(ref)
345
+ self._location = location
346
+
347
+ @property
348
+ def location(self):
349
+ return self._location
350
+
351
+ def __call__(self, calc, name=None):
352
+ raise NotImplementedError
353
+
354
+ def _get_location(self, calc):
355
+ """Locate location within the profile."""
356
+ return self._location(calc.profile)
357
+
358
+
359
+ class TimeSeriesOutput(LocationBasedOutput):
360
+ xlabel = "Time (sec)"
361
+ xscale = "linear"
362
+ ylabel = NotImplemented
363
+ yscale = "linear"
364
+
365
+ ref_name = "time"
366
+
367
+ def __init__(self, location):
368
+ super().__init__(None, location)
369
+
370
+ @property
371
+ def times(self):
372
+ return self.refs
373
+
374
+ def __call__(self, calc, name=None):
375
+ if not isinstance(calc.motion, TimeSeriesMotion):
376
+ raise NotImplementedError
377
+ Output.__call__(self, calc, name)
378
+ # Compute the response
379
+ loc = self._get_location(calc)
380
+ tf = self._get_trans_func(calc, loc)
381
+ values = calc.motion.calc_time_series(tf)
382
+ values = self._modify_values(calc, loc, values)
383
+ self._add_values(values)
384
+ # Add the reference
385
+ refs = calc.motion.time_step * np.arange(len(values))
386
+ self._add_refs(refs)
387
+
388
+ def _get_trans_func(self, calc, location):
389
+ raise NotImplementedError
390
+
391
+ def _modify_values(self, calc, location, values):
392
+ return values
393
+
394
+ def to_dataframe(self):
395
+ raise NotImplementedError
396
+
397
+
398
+ class AccelerationTSOutput(TimeSeriesOutput):
399
+ ylabel = "Acceleration (g)"
400
+
401
+ def _get_trans_func(self, calc, location):
402
+ return calc.calc_accel_tf(calc.loc_input, location)
403
+
404
+
405
+ class AriasIntensityTSOutput(AccelerationTSOutput):
406
+ ylabel = "Arias Intensity (m/s)"
407
+
408
+ def _modify_values(self, calc, location, values):
409
+ time_step = calc.motion.time_step
410
+ values = scipy.integrate.cumtrapz(values**2, dx=time_step)
411
+ values *= GRAVITY * np.pi / 2
412
+ return values
413
+
414
+
415
+ class StrainTSOutput(TimeSeriesOutput):
416
+ def __init__(self, location, in_percent=False):
417
+ super().__init__(location)
418
+ self._in_percent = in_percent
419
+ assert self.location.wave_field == WaveField.within
420
+
421
+ def _get_trans_func(self, calc, location):
422
+ return calc.calc_strain_tf(calc.loc_input, location)
423
+
424
+ def _modify_values(self, calc, location, values):
425
+ if self._in_percent:
426
+ # Convert to percent
427
+ values *= 100.0
428
+ return values
429
+
430
+ @property
431
+ def ylabel(self):
432
+ suffix = "(%)" if self._in_percent else "(dec)"
433
+ return "Shear Strain " + suffix
434
+
435
+
436
+ class StressTSOutput(TimeSeriesOutput):
437
+ def __init__(self, location, damped=False, normalized=False):
438
+ super().__init__(location)
439
+ self._damped = damped
440
+ self._normalized = normalized
441
+ assert self.location.wave_field == WaveField.within
442
+
443
+ @property
444
+ def damped(self):
445
+ return self._damped
446
+
447
+ @property
448
+ def ylabel(self):
449
+ if self._normalized:
450
+ ylabel = "Stress Ratio (τ/σ`ᵥ)"
451
+ else:
452
+ ylabel = "Stress (τ)"
453
+
454
+ return ylabel
455
+
456
+ def _get_trans_func(self, calc, location):
457
+ tf = calc.calc_stress_tf(calc.loc_input, location, self.damped)
458
+
459
+ if self._normalized:
460
+ # Correct by effective stress at depth
461
+ tf /= location.stress_vert(effective=True)
462
+
463
+ return tf
464
+
465
+
466
+ class FourierAmplitudeSpectrumOutput(LocationBasedOutput):
467
+ _const_ref = True
468
+ xlabel = "Frequency (Hz)"
469
+ ylabel = "Fourier Ampl. (cm/s)"
470
+
471
+ ref_name = "freq"
472
+
473
+ def __init__(self, freqs, location, ko_bandwidth=30):
474
+ super().__init__(freqs, location)
475
+ self._ko_bandwidth = ko_bandwidth
476
+
477
+ @property
478
+ def freqs(self):
479
+ return self._refs
480
+
481
+ @property
482
+ def ko_bandwidth(self):
483
+ return self._ko_bandwidth
484
+
485
+ def __call__(self, calc, name=None):
486
+ Output.__call__(self, calc, name)
487
+ loc = self._get_location(calc)
488
+ tf = calc.calc_accel_tf(calc.loc_input, loc)
489
+
490
+ # Only return the absolute value
491
+ fas = np.abs(tf * calc.motion.fourier_amps)
492
+
493
+ # Interpolate to the specified frequencies
494
+ if self._ko_bandwidth is None:
495
+ fas = np.interp(self.freqs, calc.motion.freqs, fas)
496
+ else:
497
+ fas = pykooh.smooth(
498
+ self.freqs,
499
+ calc.motion.freqs,
500
+ fas,
501
+ self.ko_bandwidth,
502
+ )
503
+
504
+ self._add_values(fas)
505
+
506
+
507
+ class ResponseSpectrumOutput(LocationBasedOutput):
508
+ _const_ref = True
509
+ xlabel = "Frequency (Hz)"
510
+
511
+ ref_name = "freq"
512
+
513
+ def __init__(self, freqs, location, osc_damping):
514
+ super().__init__(freqs, location)
515
+ self._osc_damping = osc_damping
516
+
517
+ @property
518
+ def freqs(self):
519
+ return self._refs
520
+
521
+ @property
522
+ def periods(self):
523
+ return 1.0 / np.asarray(self._refs)
524
+
525
+ @property
526
+ def osc_damping(self):
527
+ return self._osc_damping
528
+
529
+ @property
530
+ def ylabel(self):
531
+ return f"{100 * self.osc_damping:g}%-Damped, Spec. Accel. (g)"
532
+
533
+ def __call__(self, calc, name=None):
534
+ Output.__call__(self, calc, name)
535
+ loc = self._get_location(calc)
536
+ tf = calc.calc_accel_tf(calc.loc_input, loc)
537
+ ars = calc.motion.calc_osc_accels(self.freqs, self.osc_damping, tf)
538
+ self._add_values(ars)
539
+
540
+
541
+ class RatioBasedOutput(Output):
542
+ _const_ref = True
543
+
544
+ def __init__(self, refs, location_in, location_out):
545
+ super().__init__(refs)
546
+ self._location_in = location_in
547
+ self._location_out = location_out
548
+
549
+ @property
550
+ def location_in(self):
551
+ return self._location_in
552
+
553
+ @property
554
+ def location_out(self):
555
+ return self._location_out
556
+
557
+ def __call__(self, calc, name=None):
558
+ raise NotImplementedError
559
+
560
+ def _get_locations(self, calc):
561
+ """Locate locations within the profile."""
562
+ return (self._location_in(calc.profile), self._location_out(calc.profile))
563
+
564
+
565
+ class AccelTransferFunctionOutput(RatioBasedOutput):
566
+ xlabel = "Frequency (Hz)"
567
+ ylabel = "Accel. Transfer Func."
568
+
569
+ ref_name = "freq"
570
+
571
+ def __init__(self, refs, location_in, location_out, ko_bandwidth=None, absolute=True):
572
+ super().__init__(refs, location_in, location_out)
573
+ self._ko_bandwidth = ko_bandwidth
574
+ self._absolute = absolute
575
+
576
+ def __call__(self, calc, name=None):
577
+ Output.__call__(self, calc, name)
578
+ # Locate position within the profile
579
+ loc_in, loc_out = self._get_locations(calc)
580
+ # Compute the response
581
+ if self._absolute:
582
+ tf = np.abs(calc.calc_accel_tf(loc_in, loc_out))
583
+ else:
584
+ tf = calc.calc_accel_tf(loc_in, loc_out)
585
+
586
+ if self._ko_bandwidth is None:
587
+ tf = np.interp(self.freqs, calc.motion.freqs, tf)
588
+ else:
589
+ tf = pykooh.smooth(self.freqs, calc.motion.freqs, tf, self._ko_bandwidth)
590
+
591
+ self._add_values(tf)
592
+
593
+ @property
594
+ def freqs(self):
595
+ return self._refs
596
+
597
+
598
+ class ResponseSpectrumRatioOutput(RatioBasedOutput):
599
+ xlabel = "Frequency (Hz)"
600
+
601
+ ref_name = "freq"
602
+
603
+ def __init__(self, freqs, location_in, location_out, osc_damping):
604
+ super().__init__(freqs, location_in, location_out)
605
+ self._osc_damping = osc_damping
606
+
607
+ @property
608
+ def freqs(self):
609
+ return self._refs
610
+
611
+ @property
612
+ def periods(self):
613
+ return 1.0 / np.asarray(self._refs)
614
+
615
+ @property
616
+ def osc_damping(self):
617
+ return self._osc_damping
618
+
619
+ @property
620
+ def ylabel(self):
621
+ return f"{100 * self.osc_damping:g}%-Damped, Resp. Spectral Ratio"
622
+
623
+ def __call__(self, calc, name=None):
624
+ Output.__call__(self, calc, name)
625
+ loc_in, loc_out = self._get_locations(calc)
626
+ in_ars = calc.motion.calc_osc_accels(
627
+ self.freqs, self.osc_damping, calc.calc_accel_tf(calc.loc_input, loc_in)
628
+ )
629
+ out_ars = calc.motion.calc_osc_accels(
630
+ self.freqs, self.osc_damping, calc.calc_accel_tf(calc.loc_input, loc_out)
631
+ )
632
+ ratio = out_ars / in_ars
633
+ self._add_values(ratio)
634
+
635
+
636
+ class ProfileBasedOutput(Output):
637
+ ylabel = "Depth (m)"
638
+ yscale = "linear"
639
+ drawstyle = "steps-post"
640
+
641
+ ref_name = "depth"
642
+
643
+ def __init__(self):
644
+ super().__init__()
645
+
646
+ def __call__(self, calc, name=None):
647
+ Output.__call__(self, calc, name)
648
+ depths = np.r_[0, calc.profile.depth_mid[:-1]]
649
+ self._add_refs(depths)
650
+
651
+ def calc_stats(self, as_dataframe=False, ref=None):
652
+ if ref is None:
653
+ ref = np.linspace(0, np.nanmax(self.refs) * 1.05, num=512)
654
+
655
+ n = self.values.shape[1]
656
+ with np.errstate(divide="ignore", invalid="ignore"):
657
+ # Ignore zeros in the data
658
+ ln_values = np.array([self._ln_interp(i, ref) for i in range(n)]).T
659
+ median = np.exp(np.nanmean(ln_values, axis=1))
660
+ ln_std = np.nanstd(ln_values, axis=1)
661
+
662
+ stats = {"ref": ref, "median": median, "ln_std": ln_std}
663
+ if as_dataframe and pd:
664
+ stats = pd.DataFrame(stats).set_index("ref")
665
+ stats.index.name = self.ref_name
666
+
667
+ return stats
668
+
669
+ @staticmethod
670
+ def _get_xy(refs, values):
671
+ return values, refs
672
+
673
+ def plot(self, ax=None, style="stats"):
674
+ ax = Output.plot(self, ax, style)
675
+ ax.invert_yaxis()
676
+ return ax
677
+
678
+ def _ln_interp(self, i, ref):
679
+ """Interpolate the values in log-y space."""
680
+
681
+ _ref = self.refs[:, i]
682
+ # Only select points with valid entries
683
+ mask = np.isfinite(_ref)
684
+ _ref = _ref[mask]
685
+ _ln_values = np.log(self.values[mask, i])
686
+
687
+ if np.any(mask):
688
+ f = interp1d(
689
+ _ref,
690
+ _ln_values,
691
+ kind="next",
692
+ fill_value=(_ln_values[0], _ln_values[-1]),
693
+ bounds_error=False,
694
+ )
695
+ _ln_interped = f(ref)
696
+ else:
697
+ nans = np.empty_like(ref)
698
+ nans[:] = np.nan
699
+ _ln_interped = np.array(nans)
700
+
701
+ return _ln_interped
702
+
703
+ def to_dataframe(self, ref=None):
704
+ if not pd:
705
+ raise RuntimeError("Install `pandas` library.")
706
+
707
+ if ref is None:
708
+ ref = np.linspace(0, np.nanmax(self.refs))
709
+
710
+ if isinstance(self.names[0], tuple):
711
+ columns = pd.MultiIndex.from_tuples(self.names)
712
+ else:
713
+ columns = self.names
714
+
715
+ # Ignore zeros in the data
716
+ n = self.values.shape[1]
717
+ values = np.exp(np.array([self._ln_interp(i, ref) for i in range(n)])).T
718
+
719
+ df = pd.DataFrame(values, index=ref, columns=columns)
720
+
721
+ return df
722
+
723
+
724
+ class MaxStrainProfile(ProfileBasedOutput):
725
+ xlabel = "Max. Strain (dec)"
726
+
727
+ def __init__(self):
728
+ super().__init__()
729
+
730
+ def __call__(self, calc, name=None):
731
+ ProfileBasedOutput.__call__(self, calc, name)
732
+ values = [0] + [layer.strain_max for layer in calc.profile[:-1]]
733
+ self._add_values(values)
734
+
735
+
736
+ class DampingProfile(ProfileBasedOutput):
737
+ xlabel = "Damping (dec)"
738
+
739
+ def __call__(self, calc, name=None):
740
+ Output.__call__(self, calc, name)
741
+ # Add depth at top of layer
742
+ self._add_refs(calc.profile.depth)
743
+
744
+ values = [layer.damping for layer in calc.profile[:-1]]
745
+ # Bring the first mid-layer value to the surface
746
+ values.insert(0, values[0])
747
+ self._add_values(values)
748
+
749
+
750
+ class ShearModReducProfile(ProfileBasedOutput):
751
+ xlabel = "G/Gmax"
752
+
753
+ def __call__(self, calc, name=None):
754
+ Output.__call__(self, calc, name)
755
+ # Add depth at top of layer
756
+ self._add_refs(calc.profile.depth)
757
+
758
+ values = [layer.shear_mod_reduc for layer in calc.profile[:-1]]
759
+ # Bring the first mid-layer value to the surface
760
+ values.insert(0, values[0])
761
+ self._add_values(values)
762
+
763
+
764
+ class InitialVelProfile(ProfileBasedOutput):
765
+ xlabel = "Initial Velocity (m/s)"
766
+
767
+ def __init__(self):
768
+ super().__init__()
769
+
770
+ def __call__(self, calc, name=None):
771
+ Output.__call__(self, calc, name)
772
+ # Add depth at top of layer
773
+ self._add_refs(calc.profile.depth)
774
+
775
+ values = [layer.initial_shear_vel for layer in calc.profile[:-1]]
776
+ values.insert(0, values[0])
777
+ self._add_values(values)
778
+
779
+
780
+ class CompatVelProfile(ProfileBasedOutput):
781
+ xlabel = "Strain-Compatible Velocity (m/s)"
782
+
783
+ def __init__(self):
784
+ super().__init__()
785
+
786
+ def __call__(self, calc, name=None):
787
+ Output.__call__(self, calc, name)
788
+ # Add depth at top of layer
789
+ self._add_refs(calc.profile.depth)
790
+
791
+ values = [np.min(layer.shear_vel) for layer in calc.profile[:-1]]
792
+ values.insert(0, values[0])
793
+ self._add_values(values)
794
+
795
+
796
+ class CyclicStressRatioProfile(ProfileBasedOutput):
797
+ # From Idriss and Boulanger (2008, pg. 70):
798
+ # The 0.65 is a constant used to represent the reference stress
799
+ # level. While being somewhat arbitrary it was selected in the
800
+ # beginning of the development of liquefaction procedures in 1966
801
+ # and has been in use ever since.
802
+ _stress_level = 0.65
803
+
804
+ def __init__(self):
805
+ super().__init__()
806
+
807
+ def __call__(self, calc, name=None):
808
+ ProfileBasedOutput.__call__(self, calc, name)
809
+ values = [
810
+ layer.stress_shear_max / layer.stress_vert(layer.thickness / 2, True)
811
+ for layer in calc.profile[:-1]
812
+ ]
813
+ # Repeat the first value for the surface
814
+ values = self._stress_level * np.array([values[0]] + values)
815
+ self._add_values(values)
816
+
817
+
818
+ class MaxAccelProfile(ProfileBasedOutput):
819
+ xlabel = "Max. Accel. (g)"
820
+
821
+ def __call__(self, calc, name=None):
822
+ Output.__call__(self, calc, name)
823
+ # Add depth at top of layer
824
+ depths = calc.profile.depth
825
+ values = [self._calc_accel(calc, depth) for depth in depths]
826
+ self._add_refs(depths)
827
+ self._add_values(values)
828
+
829
+ def _calc_accel(self, calc, depth):
830
+ return calc.motion.calc_peak(
831
+ calc.calc_accel_tf(calc.loc_input, calc.profile.location("within", depth=depth))
832
+ )