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/motion.py ADDED
@@ -0,0 +1,367 @@
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
+ """Classes used to define input motions."""
24
+ import enum
25
+ import re
26
+
27
+ import numpy as np
28
+ import pyrvt
29
+ from scipy.constants import g as GRAVITY
30
+
31
+ # Gravity in m/sec²
32
+
33
+
34
+ class WaveField(enum.Enum):
35
+ outcrop = 0
36
+ within = 1
37
+ incoming_only = 2
38
+
39
+
40
+ class Motion(object):
41
+ def __init__(self, freqs=None):
42
+ object.__init__(self)
43
+
44
+ self._freqs = np.array([] if freqs is None else freqs)
45
+ self._pga = None
46
+ self._pgv = None
47
+
48
+ @property
49
+ def freqs(self):
50
+ return self._freqs
51
+
52
+ @property
53
+ def angular_freqs(self):
54
+ return 2 * np.pi * self.freqs
55
+
56
+ @property
57
+ def pgv(self, centimeters=False):
58
+ """Peak-ground velocity [m/sec]."""
59
+ if self._pgv is None:
60
+ # Compute transfer function -- only over non-zero frequencies
61
+ mask = ~np.isclose(self.angular_freqs, 0)
62
+ tf = np.zeros_like(mask, dtype=complex)
63
+ tf[mask] = 1 / (self.angular_freqs[mask] * 1j)
64
+ if centimeters:
65
+ self._pgv = GRAVITY * 100 * self.calc_peak(tf)
66
+ else:
67
+ self._pgv = GRAVITY * self.calc_peak(tf)
68
+ return self._pgv
69
+
70
+ @property
71
+ def pga(self):
72
+ if self._pga is None:
73
+ self._pga = self.calc_peak()
74
+ return self._pga
75
+
76
+
77
+ class TimeSeriesMotion(Motion):
78
+ """Time-series motion for time series based site response analysis."""
79
+
80
+ def __init__(self, filename: str, description: str, time_step: float, accels, fa_length=None):
81
+ """Initialize the class from specified acceleration values.
82
+
83
+ The *filename* and *description* parameters are only used to help track the motion.
84
+
85
+ Parameters
86
+ ----------
87
+ filename: str
88
+ Source of data
89
+ description: str
90
+ Description to store helpful information
91
+ time_step: float
92
+ Time step of the accleration values
93
+ accels: array_like
94
+ Accelerations in units of *g*
95
+ fa_length: optional int
96
+ Length to use for the Fourier amplitude spectrum. If not provided, will be automatically computed to the next power of 2.
97
+ """
98
+ Motion.__init__(self)
99
+
100
+ self._filename = filename
101
+ self._description = description
102
+ self._time_step = time_step
103
+ self._accels = np.asarray(accels)
104
+
105
+ self._calc_fourier_spectrum(fa_length)
106
+
107
+ @property
108
+ def accels(self):
109
+ return self._accels
110
+
111
+ @property
112
+ def filename(self):
113
+ return self._filename
114
+
115
+ @property
116
+ def description(self):
117
+ return self._description
118
+
119
+ @property
120
+ def time_step(self):
121
+ return self._time_step
122
+
123
+ @property
124
+ def times(self):
125
+ return self._time_step * np.arange(self._accels.size)
126
+
127
+ @property
128
+ def freqs(self):
129
+ """Return the frequencies."""
130
+ if self._freqs is None:
131
+ self._calc_fourier_spectrum()
132
+
133
+ return self._freqs
134
+
135
+ @property
136
+ def fourier_amps(self):
137
+ """Return the frequencies."""
138
+ if self._fourier_amps is None:
139
+ self._calc_fourier_spectrum()
140
+
141
+ # Normalize the Fourier amplitude by the time step
142
+ return self.time_step * self._fourier_amps
143
+
144
+ def calc_time_series(self, tf=None):
145
+ if tf is None:
146
+ ts = np.fft.irfft(self.fourier_amps / self.time_step)
147
+ else:
148
+ ts = np.fft.irfft(tf * self.fourier_amps / self.time_step)
149
+ return ts
150
+
151
+ def calc_peak(self, tf=None, **kwargs):
152
+ ts = self.calc_time_series(tf)
153
+ return np.abs(ts).max()
154
+
155
+ def calc_osc_accels(self, osc_freqs, osc_damping=0.05, tf=None):
156
+ """Compute the pseudo-acceleration spectral response of an oscillator
157
+ with a specific frequency and damping.
158
+
159
+ Parameters
160
+ ----------
161
+ osc_freq : float
162
+ Frequency of the oscillator (Hz).
163
+ osc_damping : float
164
+ Fractional damping of the oscillator (dec). For example, 0.05 for a
165
+ damping ratio of 5%.
166
+ tf : array_like, optional
167
+ Transfer function to be applied to motion prior calculation of the
168
+ oscillator response.
169
+
170
+ Returns
171
+ -------
172
+ spec_accels : :class:`numpy.ndarray`
173
+ Peak pseudo-spectral acceleration of the oscillator
174
+ """
175
+ if tf is None:
176
+ tf = np.ones_like(self.freqs)
177
+ else:
178
+ tf = np.asarray(tf).astype(complex)
179
+
180
+ resp = np.array(
181
+ [self.calc_peak(tf * self._calc_sdof_tf(of, osc_damping)) for of in osc_freqs]
182
+ )
183
+ return resp
184
+
185
+ def _calc_fourier_spectrum(self, fa_length=None):
186
+ """Compute the Fourier Amplitude Spectrum of the time series."""
187
+
188
+ if fa_length is None:
189
+ # Use the next power of 2 for the length
190
+ n = 1
191
+ while n < self.accels.size:
192
+ n <<= 1
193
+ else:
194
+ n = fa_length
195
+
196
+ self._fourier_amps = np.fft.rfft(self._accels, n)
197
+
198
+ freq_step = 1.0 / (2 * self._time_step * (n / 2))
199
+ self._freqs = freq_step * np.arange(1 + n / 2)
200
+
201
+ def _calc_sdof_tf(self, osc_freq, damping=0.05):
202
+ """Compute the transfer function for a single-degree-of-freedom
203
+ oscillator.
204
+
205
+ The transfer function computes the pseudo-spectral acceleration.
206
+
207
+ Parameters
208
+ ----------
209
+ osc_freq : float
210
+ natural frequency of the oscillator [Hz]
211
+ damping : float, optional
212
+ damping ratio of the oscillator in decimal. Default value is
213
+ 0.05, or 5%.
214
+
215
+ Returns
216
+ -------
217
+ tf : :class:`numpy.ndarray`
218
+ Complex-valued transfer function with length equal to `self.freq`.
219
+ """
220
+ return -(osc_freq**2.0) / (
221
+ np.square(self.freqs) - np.square(osc_freq) - 2.0j * damping * osc_freq * self.freqs
222
+ )
223
+
224
+ @classmethod
225
+ def load_at2_file(cls, filename, scale=1.0):
226
+ """Read an AT2 formatted time series.
227
+
228
+ Parameters
229
+ ----------
230
+ filename: str
231
+ Filename to open.
232
+ scale: float, default: 1.
233
+ Scale factor to apply to the motion.
234
+ """
235
+ with open(filename) as fp:
236
+ next(fp)
237
+ description = next(fp).strip()
238
+ next(fp)
239
+ parts = next(fp).split()
240
+ time_step = float(parts[1])
241
+
242
+ accels = np.array([float(p) for l in fp for p in l.split()])
243
+
244
+ accels *= scale
245
+ return cls(filename, description, time_step, accels)
246
+
247
+ @classmethod
248
+ def load_smc_file(cls, filename, scale=1.0):
249
+ """Read an SMC formatted time series.
250
+
251
+ Format of the time series is provided by:
252
+ https://escweb.wr.usgs.gov/nsmp-data/smcfmt.html
253
+
254
+ Parameters
255
+ ----------
256
+ filename: str
257
+ Filename to open.
258
+ scale: float, default: 1.
259
+ Scale factor to apply to the motion.
260
+ """
261
+ from .tools import parse_fixed_width
262
+
263
+ with open(filename) as fp:
264
+ lines = list(fp)
265
+
266
+ # 11 lines of strings
267
+ lines_str = [lines.pop(0) for _ in range(11)]
268
+
269
+ if lines_str[0].strip() != "2 CORRECTED ACCELEROGRAM":
270
+ raise RuntimeWarning("Loading uncorrected SMC file.")
271
+
272
+ m = re.search("station =(.+)component=(.+)", lines_str[5])
273
+ description = "; ".join([g.strip() for g in m.groups()])
274
+
275
+ # 6 lines of (8i10) formatted integers
276
+ values_int = parse_fixed_width(48 * [(10, int)], [lines.pop(0) for _ in range(6)])
277
+ count_comment = values_int[15]
278
+ count = values_int[16]
279
+
280
+ # 10 lines of (5e15.7) formatted floats
281
+ values_float = parse_fixed_width(50 * [(15, float)], [lines.pop(0) for _ in range(10)])
282
+ time_step = 1 / values_float[1]
283
+
284
+ # Skip comments
285
+ lines = lines[count_comment:]
286
+
287
+ accels = np.array(
288
+ parse_fixed_width(
289
+ count
290
+ * [
291
+ (10, float),
292
+ ],
293
+ lines,
294
+ )
295
+ )
296
+ accels *= scale
297
+
298
+ return TimeSeriesMotion(filename, description, time_step, accels)
299
+
300
+
301
+ class RvtMotion(pyrvt.motions.RvtMotion, Motion):
302
+ """RVT motion based on user specified Fourier amplitude spectrum and duration."""
303
+
304
+ def __init__(self, freqs, fourier_amps, duration=None, peak_calculator=None, calc_kwds=None):
305
+ Motion.__init__(self)
306
+ pyrvt.motions.RvtMotion.__init__(
307
+ self,
308
+ freqs,
309
+ fourier_amps,
310
+ duration=duration,
311
+ peak_calculator=peak_calculator,
312
+ calc_kwds=calc_kwds,
313
+ )
314
+
315
+
316
+ class CompatibleRvtMotion(pyrvt.motions.CompatibleRvtMotion, Motion):
317
+ """RVT motion based on user specified acceleration response spectrum and duration."""
318
+
319
+ def __init__(
320
+ self,
321
+ osc_freqs,
322
+ osc_accels_target,
323
+ duration=None,
324
+ osc_damping=0.05,
325
+ event_kwds=None,
326
+ window_len=None,
327
+ peak_calculator=None,
328
+ calc_kwds=None,
329
+ ):
330
+ Motion.__init__(self)
331
+ pyrvt.motions.CompatibleRvtMotion.__init__(
332
+ self,
333
+ osc_freqs,
334
+ osc_accels_target,
335
+ duration=duration,
336
+ osc_damping=osc_damping,
337
+ event_kwds=event_kwds,
338
+ window_len=window_len,
339
+ peak_calculator=peak_calculator,
340
+ calc_kwds=calc_kwds,
341
+ )
342
+
343
+
344
+ class SourceTheoryRvtMotion(pyrvt.motions.SourceTheoryMotion, Motion):
345
+ """RVT motion based on seismological point source model and earthquake scenario parameters."""
346
+
347
+ def __init__(
348
+ self,
349
+ magnitude,
350
+ distance,
351
+ region,
352
+ stress_drop=None,
353
+ depth=8,
354
+ peak_calculator=None,
355
+ calc_kwds=None,
356
+ ):
357
+ Motion.__init__(self)
358
+ pyrvt.motions.SourceTheoryMotion.__init__(
359
+ self,
360
+ magnitude,
361
+ distance,
362
+ region,
363
+ stress_drop,
364
+ depth,
365
+ peak_calculator=peak_calculator,
366
+ calc_kwds=calc_kwds,
367
+ )