ground-motion-tools 0.1.0__tar.gz

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.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.3
2
+ Name: ground-motion-tools
3
+ Version: 0.1.0
4
+ Summary:
5
+ License: MIT
6
+ Author: RichardoGu
7
+ Author-email: xiaopenggu@qq.com
8
+ Requires-Python: >=3.10
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: geopy (>=2.4.1,<3.0.0)
16
+ Requires-Dist: numpy (>=2.2.4,<3.0.0)
17
+ Requires-Dist: scipy (>=1.15.2,<2.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Ground-Motion-Tools
21
+
22
+ ## Installation
23
+
24
+ ##
@@ -0,0 +1,5 @@
1
+ # Ground-Motion-Tools
2
+
3
+ ## Installation
4
+
5
+ ##
@@ -0,0 +1,3 @@
1
+ from .im import GMIntensityMeasures
2
+ from .io import read_from_peer, read_from_kik, read_from_single, save_to_single
3
+ from .process import length_normalize, down_sample, gm_data_fill, butter_worth_filter, fourier
@@ -0,0 +1,3 @@
1
+ from .gm_data_enum import GMDataEnum
2
+ from .gm_spectrum_enum import GMSpectrumEnum
3
+ from .gm_im_enum import GMIMEnum
@@ -0,0 +1,15 @@
1
+ from enum import Enum, unique
2
+
3
+
4
+ @unique
5
+ class GMDataEnum(Enum):
6
+ """
7
+ Type of Ground Motion Data
8
+ Attributes:
9
+ ACC: Acceleration
10
+ VEL: Velocity
11
+ DISP: Displacement
12
+ """
13
+ ACC = 0
14
+ VEL = 1
15
+ DISP = 2
@@ -0,0 +1,42 @@
1
+ from enum import Enum, unique
2
+
3
+
4
+ @unique
5
+ class GMIMEnum(Enum):
6
+ """
7
+ Type of Ground Motions Intensity Measures.
8
+
9
+ ['PGA', 'PGV', 'PGD', 'RMSA', 'RMSV', 'RMSD', 'I_A', 'I_C', 'SED', 'CAV',
10
+ 'ASI', 'VSI', 'HI', 'SMA', 'SMV', 'Ia', 'Id', 'Iv',
11
+ 'If', 'Sa(T1)', 'Sv(T1)', 'Sd(T1)', 'T70', 'T90', 'FFT']
12
+
13
+ Attributes:
14
+ PGA: Peek Ground Acceleration.
15
+ TODO Add More
16
+ """
17
+ PGA = 0
18
+ PGV = 1
19
+ PGD = 2
20
+ RMSA = 3
21
+ RMSV = 4
22
+ RMSD = 5
23
+ I_SUFFIX_A = 6
24
+ I_SUFFIX_C = 7
25
+ SED = 8
26
+ CAV = 9
27
+ SA_T1 = 10
28
+ SV_T1 = 11
29
+ SD_T1 = 12
30
+ ASI = 13
31
+ VSI = 14
32
+ HI = 15
33
+ SMA = 16
34
+ SMV = 17
35
+ I_A = 18
36
+ I_D = 19
37
+ I_V = 20
38
+ I_F = 21
39
+ MIV = 22
40
+ DI = 23
41
+ T70 = 24
42
+ T90 = 25
@@ -0,0 +1,20 @@
1
+ from enum import Enum, unique
2
+
3
+
4
+ @unique
5
+ class GMSpectrumEnum(Enum):
6
+ """
7
+ Type of Ground Motion Spectrum.
8
+ Each Ground Motion have five different Spectrum types, see Details: http://www.jdcui.com/?p=713.
9
+ Attributes:
10
+ ACC: Acceleration response spectrum
11
+ VEL: Velocity response spectrum
12
+ DISP: Displacement response spectrum
13
+ PSE_ACC: Pseudo acceleration response spectrum
14
+ PSE_VEL: Pseudo velocity response spectrum
15
+ """
16
+ ACC = 0
17
+ VEL = 1
18
+ DISP = 2
19
+ PSE_ACC = 3
20
+ PSE_VEL = 4
@@ -0,0 +1,368 @@
1
+ # -*- coding:utf-8 -*-
2
+ # @Time: 2025/3/17 17:15
3
+ # @Author: RichardoGu
4
+ """
5
+ Intensity measures
6
+ """
7
+ import numpy as np
8
+ from process import gm_data_fill
9
+ from sbs_integration_linear import segmented_parsing
10
+ from spectrum import SPECTRUM_PERIOD, get_spectrum
11
+ from enums import GMIMEnum, GMDataEnum
12
+
13
+ IM_ADJUST_DICT = {
14
+ GMIMEnum.PGA.name: { # 按照PGA进行调幅的其余IM变化率
15
+ GMIMEnum.PGA.name: lambda x: x,
16
+ GMIMEnum.PGV.name: lambda x: x,
17
+ GMIMEnum.PGD.name: lambda x: x,
18
+
19
+ GMIMEnum.RMSA.name: lambda x: x,
20
+ GMIMEnum.RMSV.name: lambda x: x,
21
+ GMIMEnum.RMSD.name: lambda x: x,
22
+
23
+ GMIMEnum.I_SUFFIX_A.name: lambda x: x,
24
+ GMIMEnum.I_SUFFIX_C.name: lambda x: x ** (3 / 2),
25
+
26
+ GMIMEnum.SED.name: lambda x: x,
27
+ GMIMEnum.CAV.name: lambda x: x,
28
+
29
+ GMIMEnum.ASI.name: lambda x: x,
30
+ GMIMEnum.VSI.name: lambda x: x,
31
+ GMIMEnum.HI.name: lambda x: x,
32
+
33
+ GMIMEnum.SMA.name: lambda x: x,
34
+ GMIMEnum.SMV.name: lambda x: x,
35
+
36
+ GMIMEnum.I_A.name: lambda x: x,
37
+ GMIMEnum.I_D.name: lambda x: x,
38
+ GMIMEnum.I_V.name: lambda x: x ** (2 / 3),
39
+ GMIMEnum.I_F.name: lambda x: x,
40
+
41
+ GMIMEnum.SA_T1.name: lambda x: x,
42
+ GMIMEnum.SV_T1.name: lambda x: x,
43
+ GMIMEnum.SD_T1.name: lambda x: x
44
+ }
45
+ }
46
+
47
+
48
+ class GMIntensityMeasures:
49
+ def __init__(self, gm_acc_data: np.ndarray, time_step: float):
50
+ self.acc, self.vel, self.disp = gm_data_fill(gm_acc_data, time_step, GMDataEnum.ACC)
51
+ if self.acc.ndim == 1:
52
+ self.acc = np.expand_dims(self.acc, axis=0)
53
+ self.vel = np.expand_dims(self.vel, axis=0)
54
+ self.disp = np.expand_dims(self.disp, axis=0)
55
+
56
+ self.time_step = time_step
57
+ self.batch_size = self.acc.shape[0]
58
+ self.seq_len = self.acc.shape[1]
59
+ self.duration = self.seq_len * self.time_step
60
+
61
+ self.spectrum_acc = None
62
+ self.spectrum_vel = None
63
+ self.spectrum_disp = None
64
+
65
+ self.intensity_measures = {}
66
+
67
+ def _get_spectrum(self):
68
+ self.spectrum_acc, self.spectrum_vel, self.spectrum_disp, _, _ = get_spectrum(
69
+ self.acc, self.time_step, 0.05
70
+ )
71
+ if self.spectrum_acc.ndim == 1:
72
+ self.spectrum_acc = np.expand_dims(self.spectrum_acc, axis=0)
73
+ self.spectrum_vel = np.expand_dims(self.spectrum_vel, axis=0)
74
+ self.spectrum_disp = np.expand_dims(self.spectrum_disp, axis=0)
75
+
76
+ def get_im(
77
+ self,
78
+ im_list: [list, GMIMEnum],
79
+ period: float = 1
80
+ ) -> dict[str, np.ndarray[float]]:
81
+ """
82
+ An external query must call this interface to get intensity measures.
83
+
84
+ The parameter ``im`` is the intensity measures' name, and this func will output the corresponding result.
85
+
86
+ All the input is saved by a dict named ``self.intensity_measures``, and each im will just be calculated once
87
+ when it is first queried.
88
+
89
+ The input im can be both upper and lower, but must be included in :class:`GroundMotionDataIntensityMeasures`.
90
+ Args:
91
+ im_list: Intensity measures' name.
92
+ period: Some intensity measures need param ``period`` to calculate. Default 0.9s
93
+
94
+ Returns:
95
+ A dict ``{str,np.ndarray[float]}`` of ``{im_name, im_value}``
96
+
97
+ """
98
+ if im_list is None or len(im_list) == 0:
99
+ raise ValueError("Parameter 'im_list' can not be None or empty.")
100
+
101
+ result = {}
102
+ if type(im_list) is GMIMEnum:
103
+ im_list = [im_list]
104
+ for im in im_list:
105
+ im_upper = im.name.upper()
106
+ im_lower = im.name.lower()
107
+ try:
108
+ result[im_upper] = self.intensity_measures[im_upper]
109
+ except KeyError:
110
+ self.intensity_measures[im_upper] = eval("self.im_" + im_lower)(period=period)
111
+ result[im_upper] = self.intensity_measures[im_upper]
112
+ return result
113
+
114
+ def im_pga(self, **kwargs):
115
+ """
116
+ PGA
117
+ .. math:: |max(a(t))|
118
+ """
119
+ return np.abs(self.acc).max(1)
120
+
121
+ def im_pgv(self, **kwargs):
122
+ """
123
+ PGV
124
+ .. math:: |max(v(t))|
125
+ """
126
+ return np.abs(self.vel).max(1)
127
+
128
+ def im_pgd(self, **kwargs):
129
+ """
130
+ PGD
131
+ .. math:: |max(d(t))|
132
+ """
133
+ return np.abs(self.disp).max(1)
134
+
135
+ def im_rmsa(self, **kwargs):
136
+ """
137
+ Arms
138
+ .. math:: \\sqrt{\\frac{1}{t_{tot}} \\int_{0}^{tot}a(t)^2dt}
139
+ """
140
+ return ((self.acc ** 2).sum(1) * self.time_step / self.duration) ** 0.5
141
+
142
+ def im_rmsv(self, **kwargs):
143
+ """
144
+ Vrms
145
+ .. math:: \\sqrt{\\frac{1}{t_{tot}} \\int_{0}^{tot}v(t)^2dt}
146
+ """
147
+ return ((self.vel ** 2).sum(1) * self.time_step / self.duration) ** 0.5
148
+
149
+ def im_rmsd(self, **kwargs):
150
+ """
151
+ Drms
152
+ .. math:: \\sqrt{\\frac{1}{t_{tot}} \\int_{0}^{tot}d(t)^2dt}
153
+ """
154
+ return ((self.disp ** 2).sum(1) * self.time_step / self.duration) ** 0.5
155
+
156
+ def im_i_suffix_a(self, **kwargs):
157
+ """
158
+ IA
159
+ .. math:: \\frac{\\pi}{2g}\\int^{t_{tot}}_{0}{a(t)^2dt}
160
+ """
161
+ return (self.acc ** 2).sum(1) * self.time_step * np.pi / (2 * 9.8)
162
+
163
+ def im_i_suffix_c(self, **kwargs):
164
+ """
165
+ IC
166
+ .. math:: (Arms)^{3/2}\\sqrt{t_{tot}}
167
+ """
168
+ return self.get_im(GMIMEnum.RMSA)[GMIMEnum.RMSA.name.upper()] ** 1.5 * (self.duration ** 0.5)
169
+
170
+ def im_sed(self, **kwargs):
171
+ """
172
+ SED
173
+ .. math:: \\int_{0}^{tot}{v(t)^2}dt
174
+ """
175
+ return (self.vel ** 2).sum(1) * self.time_step
176
+
177
+ def im_cav(self, **kwargs):
178
+ """
179
+ CAV
180
+ .. math:: \\int_{0}^{tot}{|a(t)|}dt
181
+ """
182
+ return np.abs(self.acc).sum(1) * self.time_step
183
+
184
+ def im_sa_t1(self, **kwargs):
185
+ """
186
+ Sa(T1)
187
+ Spectrum acceleration at the first natural period of vibration.
188
+ """
189
+ acc, vel, disp = segmented_parsing(mass=1,
190
+ stiffness=((2 * np.pi) / kwargs["period"]) ** 2,
191
+ damping_ratio=0.05,
192
+ load=self.acc, time_step=self.time_step)
193
+ self.intensity_measures[GMIMEnum.SV_T1.name.upper()] = np.abs(vel).max(1)
194
+ self.intensity_measures[GMIMEnum.SD_T1.name.upper()] = np.abs(disp).max(1)
195
+ return np.abs(acc).max(1)
196
+
197
+ def im_sv_t1(self, **kwargs):
198
+ """
199
+ Sv(T1)
200
+ Spectrum velocity at the first natural period of vibration.
201
+ """
202
+ acc, vel, disp = segmented_parsing(mass=1,
203
+ stiffness=((2 * np.pi) / kwargs["period"]) ** 2,
204
+ damping_ratio=0.05,
205
+ load=self.acc, time_step=self.time_step)
206
+ self.intensity_measures[GMIMEnum.SA_T1.name.upper()] = np.abs(acc).max(1)
207
+ self.intensity_measures[GMIMEnum.SD_T1.name.upper()] = np.abs(disp).max(1)
208
+ return np.abs(vel).max(1)
209
+
210
+ def im_sd_t1(self, **kwargs):
211
+ """
212
+ Sd(T1)
213
+ Spectrum displacement at the first natural period of vibration.
214
+ """
215
+ acc, vel, disp = segmented_parsing(mass=1,
216
+ stiffness=((2 * np.pi) / kwargs["period"]) ** 2,
217
+ damping_ratio=0.05,
218
+ load=self.acc, time_step=self.time_step)
219
+ self.intensity_measures[GMIMEnum.SA_T1.name.upper()] = np.abs(acc).max(1)
220
+ self.intensity_measures[GMIMEnum.SV_T1.name.upper()] = np.abs(vel).max(1)
221
+ return np.abs(disp).max(1)
222
+
223
+ def im_asi(self, **kwargs):
224
+ """
225
+ ASI
226
+ .. math:: \\int_{0.1}^{0.5}{Sa(\\xi = 0.05, t) dt}
227
+ """
228
+ if self.spectrum_acc is None:
229
+ self._get_spectrum()
230
+
231
+ result = np.zeros(self.batch_size)
232
+ for i in range(len(SPECTRUM_PERIOD)):
233
+ if SPECTRUM_PERIOD[i] < 0.1:
234
+ continue
235
+ if SPECTRUM_PERIOD[i] > 0.5:
236
+ break
237
+ result += self.spectrum_acc[:, i] * (SPECTRUM_PERIOD[i] - SPECTRUM_PERIOD[i - 1])
238
+ return result
239
+
240
+ def im_vsi(self, **kwargs):
241
+ """
242
+ VSI
243
+ .. math:: \\int_{0.1}^{2.5}{Sv(\\xi = 0.05, t) dt}
244
+ """
245
+ if self.spectrum_vel is None:
246
+ self._get_spectrum()
247
+ result = np.zeros(self.batch_size)
248
+ for i in range(len(SPECTRUM_PERIOD)):
249
+ if SPECTRUM_PERIOD[i] < 0.1:
250
+ continue
251
+ if SPECTRUM_PERIOD[i] > 2.5:
252
+ break
253
+ result += self.spectrum_vel[:, i] * (SPECTRUM_PERIOD[i] - SPECTRUM_PERIOD[i - 1])
254
+ return result
255
+
256
+ def im_hi(self, **kwargs):
257
+ """
258
+ HI
259
+ .. math:: \\int_{0.1}^{2.5}{PSv(\\xi = 0.05, t) dt}
260
+ """
261
+ if self.spectrum_disp is None:
262
+ self._get_spectrum()
263
+ result = np.zeros(self.batch_size)
264
+ for i in range(len(SPECTRUM_PERIOD)):
265
+ if SPECTRUM_PERIOD[i] < 0.1:
266
+ continue
267
+ if SPECTRUM_PERIOD[i] > 2.5:
268
+ break
269
+ result += self.spectrum_disp[:, i] * (SPECTRUM_PERIOD[i] - SPECTRUM_PERIOD[i - 1])
270
+ return result
271
+
272
+ def im_sma(self, **kwargs):
273
+ """
274
+ The third peek in acceleration time history.
275
+ """
276
+ return np.sort(np.abs(self.acc), axis=1)[:, -3]
277
+
278
+ def im_smv(self, **kwargs):
279
+ """
280
+ The third peek in velocity time history.
281
+ """
282
+ return np.sort(np.abs(self.vel), axis=1)[:, -3]
283
+
284
+ def im_i_a(self, **kwargs):
285
+ """
286
+ Ia
287
+ .. math:: PGA{\\dot}t^{1/3}_{tot}
288
+ """
289
+ return self.get_im(GMIMEnum.PGA)[GMIMEnum.PGA.name.upper()] * self.duration ** (1 / 3)
290
+
291
+ def im_i_d(self, **kwargs):
292
+ """
293
+ Id
294
+ .. math:: PGD{\\dot}t^{1/3}_{tot}
295
+ """
296
+ return self.get_im(GMIMEnum.PGD)[GMIMEnum.PGD.name.upper()] * self.duration ** (1 / 3)
297
+
298
+ def im_i_v(self, **kwargs):
299
+ """
300
+ Iv
301
+ .. math:: PGV^{2/3}{\\dot}t^{1/3}_{tot}
302
+ """
303
+ return self.get_im(GMIMEnum.PGV)[GMIMEnum.PGV.name.upper()] ** (2 / 3) * self.duration ** (1 / 3)
304
+
305
+ def im_i_f(self, **kwargs):
306
+ """
307
+ IF
308
+ .. math:: PGV{\\dot}t^{1/4}_{tot}
309
+ """
310
+ return self.get_im(GMIMEnum.PGV)[GMIMEnum.PGV.name.upper()] * self.duration ** (1 / 4)
311
+
312
+ def im_miv(self, **kwargs):
313
+ """
314
+ MIV
315
+ """
316
+ # TODO ADD
317
+ return np.zeros(self.batch_size)
318
+
319
+ def im_di(self, **kwargs):
320
+ """
321
+ DI
322
+ """
323
+ # TODO ADD
324
+ return np.zeros(self.batch_size)
325
+
326
+ def im_t70(self, **kwargs):
327
+ """
328
+ T0.75-T0.05
329
+ """
330
+ # TODO ADD
331
+ return np.zeros(self.batch_size)
332
+
333
+ def im_t90(self, **kwargs):
334
+ """
335
+ T0.95-T0.05
336
+ """
337
+ # TODO ADD
338
+ return np.zeros(self.batch_size)
339
+
340
+ @staticmethod
341
+ def im_adjust(im_data: dict, base_im: GMIMEnum, target_value: float):
342
+ """
343
+ 对IM指标进行调幅
344
+ Args:
345
+ im_data: 需要进行调幅的IM指标
346
+ base_im: 以哪个IM为基准调幅
347
+ target_value: 要调幅的值
348
+
349
+ Returns:
350
+
351
+ """
352
+ # 初始化一个新数组,不改变原有的数据
353
+ adjusted_im_data = {}
354
+
355
+ # 首先将base_im调幅到target_value,并记录调幅参数
356
+ base_im_data = im_data[base_im.name.upper()]
357
+ base_adjust_value = target_value / base_im_data
358
+ adjusted_im_data[base_im.name.upper()] = im_data[base_im.name.upper()] * base_adjust_value
359
+
360
+ # 逐项计算
361
+ for im_key in im_data.keys():
362
+ if im_key not in IM_ADJUST_DICT[base_im.name].keys():
363
+ raise KeyError(f"调幅系数中并未收录IM指标:{im_key}")
364
+ adjust_func = IM_ADJUST_DICT[base_im.name][GMIMEnum[im_key].name] # 获取该IM的调幅系数
365
+ adjust_value = np.array([adjust_func(bav_i) for bav_i in base_adjust_value]) # 计算调幅矩阵
366
+ adjusted_im_data[im_key] = im_data[im_key] * adjust_value # 原始值与调幅矩阵逐项相乘
367
+
368
+ return adjusted_im_data
@@ -0,0 +1,123 @@
1
+ # -*- coding:utf-8 -*-
2
+ # @Time: 2025/3/17 16:10
3
+ # @Author: RichardoGu
4
+ """
5
+ This file is mainly used to read or write ground motion.
6
+ """
7
+ import re
8
+ import numpy as np
9
+
10
+ TIME_RE = r"\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}"
11
+ NUMBER_RE = r"[+-]?(\d+([.]\d*)?([eE][+-]?\d+)?|[.]\d+([eE][+-]?\d+)?)"
12
+ TIME_FORMAT = "%Y/%m/%d %H:%M:%S"
13
+
14
+
15
+ def read_from_kik(file_path: str) -> (
16
+ np.ndarray[np.float64], float
17
+ ):
18
+ """
19
+ Read ground motion data from KIK format.
20
+ Args:
21
+ file_path: File path
22
+
23
+ Returns:
24
+ gm_data, time_step
25
+ """
26
+ gm_data = None
27
+ with open(file_path, 'r') as f:
28
+ lines = f.readlines()
29
+ for line in lines:
30
+ if re.match('^Memo.', line):
31
+ # if line is begin of "Memo.", it means all the params are read.
32
+ # The following data is time-history data.
33
+ idx = lines.index(line) + 1
34
+ gm_data = []
35
+ for i in range(idx, len(lines)):
36
+ temp = lines[i].split()
37
+ for j in temp:
38
+ gm_data.append(eval(j) * 0.01) # Times 0.01 to convert gal(cm/s^2) to SI(m/s^2)
39
+ break
40
+ elif re.match("^Scale Factor.", line):
41
+ match_result = re.search(r'(\d+)\D*(\d+)/?$', line)
42
+ scale_factor = eval(match_result.group(1)) / eval(match_result.group(2))
43
+ elif re.match("^Sampling Freq.", line):
44
+ time_step = 1 / eval(re.search(NUMBER_RE, line).group())
45
+
46
+ if gm_data is not None and scale_factor:
47
+ gm_data = np.array(gm_data, dtype=np.float64)
48
+ gm_data = (gm_data - gm_data.mean()) * scale_factor
49
+ else:
50
+ raise ValueError("Read ground motion data error. Parameter 'scale_factor' or gm_data may be None.")
51
+ return gm_data, time_step
52
+
53
+
54
+ def read_from_peer(file_path: str) -> (
55
+ np.ndarray[np.float64], float
56
+ ):
57
+ """
58
+ Read ground motion data from PEER format.
59
+ Args:
60
+ file_path:
61
+
62
+ Returns:
63
+ gm_data, time_step
64
+ """
65
+ with open(file_path, 'r') as fp:
66
+ lines = fp.readlines()
67
+ time_step = eval(re.findall(r"\.[0-9]*", lines[3])[0])
68
+ gm_data = []
69
+ for i in range(4, len(lines)):
70
+ temp = lines[i].split()
71
+ for j in temp:
72
+ gm_data.append(eval(j) * 9.8) # Times 9.8 to convert G(9.8m/s^2) to m/s^2
73
+ return np.array(gm_data, dtype=np.float64), time_step
74
+
75
+
76
+ def read_from_single(file_path: str, start_line: int = 1,
77
+ end_line: int = None, time_step: [int, float] = 0) -> (
78
+ np.ndarray[np.float64], float
79
+ ):
80
+ """
81
+ Reading seismic wave data from a single column file
82
+ The default single column file format is: the first row is the sampling interval,
83
+ the second row to the end of the file is the seismic wave data.
84
+ Args:
85
+ file_path: File path
86
+ start_line: Number of rows of ground motion. Default is the second row.
87
+ end_line: Number of end rows of ground motion. Default is None
88
+ time_step:
89
+ Ground motion Sampling Interval Directly indicates the sampling interval.
90
+ if it is a floating point number,
91
+ otherwise it indicates the number of rows where the sampling interval is located.
92
+
93
+ Returns:
94
+ gm_data, time_step
95
+ """
96
+ with open(file_path, 'r') as fp:
97
+ lines = fp.readlines()
98
+ wave_data = [float(line) for line in lines[start_line:end_line]]
99
+ if type(time_step) is float:
100
+ pass
101
+ elif type(time_step) is int:
102
+ time_step = float(lines[time_step].split(" ")[-1])
103
+ else:
104
+ raise ValueError(f"Type of parameter 'time_step' need to be float or int. But got {type(time_step)}.")
105
+ return np.array(wave_data, dtype=np.float64), time_step
106
+
107
+
108
+ def save_to_single(file_path: str, gm_data: np.ndarray[np.float64], time_step: float = None) -> None:
109
+ """
110
+ Save ground motion to single file.
111
+ Args:
112
+ gm_data: Data of ground motion.
113
+ time_step: Time step of ground motion.
114
+ file_path: Path to save.
115
+
116
+ Returns:
117
+ None
118
+ """
119
+ with open(file_path, 'w') as fp:
120
+ if time_step is not None:
121
+ fp.write(f"Time Step: {time_step}\n")
122
+ for data in gm_data:
123
+ fp.write(f"{data}\n")
@@ -0,0 +1,155 @@
1
+ # -*- coding:utf-8 -*-
2
+ # @Time: 2025/3/17 16:49
3
+ # @Author: RichardoGu
4
+ """
5
+ Some utils for processing ground motion.
6
+ """
7
+ import numpy as np
8
+ from scipy import signal
9
+ from enums import GMDataEnum
10
+
11
+
12
+ def gm_data_fill(gm_data: np.ndarray,
13
+ time_step: float = 0.02,
14
+ wave_type: GMDataEnum = GMDataEnum.ACC) -> (
15
+ np.ndarray[np.float64], np.ndarray[np.float64], np.ndarray[np.float64]
16
+ ):
17
+ """
18
+ Fit wave by input.
19
+
20
+ Args:
21
+ gm_data:
22
+ wave_type:
23
+ time_step:
24
+
25
+ Returns:
26
+ None
27
+ """
28
+ if gm_data.ndim == 1:
29
+ # All the wave_array hereafter should have 2 dims as [batch_size, seq_len]
30
+ gm_data = np.expand_dims(gm_data, axis=0)
31
+ if wave_type.value == GMDataEnum.ACC.value:
32
+ acc = gm_data
33
+ vel = np.cumsum(acc, axis=1) * time_step
34
+ disp = np.cumsum(vel, axis=1) * time_step
35
+ elif wave_type.value == GMDataEnum.VEL.value:
36
+ vel = gm_data
37
+ disp = np.cumsum(vel, axis=1) * time_step
38
+ acc = np.gradient(vel, time_step, axis=1)
39
+ elif wave_type.value == GMDataEnum.DISP.value:
40
+ disp = gm_data
41
+ vel = np.gradient(disp, time_step, axis=1)
42
+ acc = np.gradient(vel, time_step, axis=1)
43
+ else:
44
+ raise ValueError("Parameter wave_type must be included in [ACC, VEL, DISP].")
45
+ return np.squeeze(acc), np.squeeze(vel), np.squeeze(disp)
46
+
47
+
48
+ def fourier(gm_data: np.ndarray, time_step: float) -> (
49
+ np.ndarray, np.ndarray, np.ndarray
50
+ ):
51
+ """
52
+ Calculate the fourier spectrum of wave.
53
+ Args:
54
+ gm_data: Ground motion data.
55
+ time_step: Time step.
56
+
57
+ Returns:
58
+ x-axis of fourier spectrum(HZ)
59
+ amp: A
60
+
61
+ """
62
+ x_fourier = np.abs(np.fft.fftfreq(d=time_step, n=len(gm_data))[len(gm_data) // 2:])[::-1]
63
+ # Take the absolute value of the complex number, i.e., the mode of the complex number (bilateral spectrum)
64
+ amp = np.abs(np.fft.fft(gm_data)) / len(gm_data)
65
+ # extract a unilateral spectrum
66
+ amp = (amp[len(gm_data) // 2:])[::-1]
67
+ return x_fourier, amp, amp ** 2
68
+
69
+
70
+ def butter_worth_filter(
71
+ gm_data: np.ndarray[np.float64],
72
+ time_step: float,
73
+ order: int = 4,
74
+ start_freq: float = 0.1,
75
+ end_freq: float = 15,
76
+ pass_way: str = 'band'):
77
+ """
78
+
79
+ The start frequency and stop frequency are suggested as 0.1HZ and 25HZ.
80
+ Because commonly the effective frequency in ground motions is range from 0.1hz to 25hz.
81
+
82
+ Args:
83
+ gm_data: Ground motion data.
84
+ time_step: Time step.
85
+ order: The order of butterworth filter. Default 4.
86
+ start_freq: The start freq of filter. Default 0.1.
87
+ end_freq: The end freq of filter. Default 0.1.
88
+ pass_way: The pass way of filter. Default bandpass.
89
+
90
+ Returns: Filtered waves.
91
+
92
+ """
93
+ b, a = signal.butter(order, [2 * start_freq * time_step, 2 * end_freq * time_step], pass_way)
94
+ # ! Result by using ``lfilter`` is sample to Seismic Signal, not filtfilt
95
+ return signal.lfilter(b, a, gm_data)
96
+
97
+
98
+ def down_sample(gm_data: np.ndarray, ori_time_step: float, tar_time_step: float) -> np.ndarray:
99
+ """
100
+ Down-sample the wave data.
101
+
102
+ The method used for down-sampling is mean-down-sampling.
103
+ Use mean down-samping method can let the calculated displacement and velocity be the same as before.
104
+
105
+ Args:
106
+ gm_data: The input wave data.
107
+ ori_time_step: Origin time step.
108
+ tar_time_step: Target time step.
109
+ Returns:
110
+ Downsized wave_data. Using scipy.signal.resample
111
+ """
112
+
113
+ # The two lines that are commented out are the previous methods.
114
+ # tar_data_size = int(ori_time_step / tar_time_step * wave_data.shape[wave_data.ndim - 1])
115
+ # return signal.resample(wave_data, tar_data_size, axis=axis).mean()
116
+
117
+ num_samples = int(gm_data.shape[-1] * ori_time_step / tar_time_step)
118
+ return signal.resample(gm_data, num_samples, axis=gm_data.ndim - 1)
119
+
120
+
121
+ def length_normalize(gm_data: np.ndarray[np.float64], normal_length: int):
122
+ """
123
+ Seismic wave length normalisation method.
124
+
125
+ Normalisation algorithm:
126
+ 1. If the original seismic wave length l1 is less than the normalised seismic wave length ln,
127
+ then zero is added directly at the end.
128
+ 2. If the original seismic wave length l1 is greater than the seismic wave length ln to be normalised,
129
+ the following operation is performed.
130
+ 2.1 Extraction of ground shaking PGA occurrences i1
131
+ 2.2 Calculate the ratio a(0<a<1) of the original length to the normalised length,
132
+ then the original ground shaking should be extracted int(i1*a) units, before the peak appears.
133
+ When the peak appears, it is dealt with directly by the truncation and zero filling method.
134
+ Args:
135
+ gm_data: Ground motion data.
136
+ normal_length: 要归一化的长度
137
+
138
+ Returns:
139
+
140
+ """
141
+ # 1 The normalised length is greater than the original length
142
+ if gm_data.shape[0] <= normal_length:
143
+ return np.pad(
144
+ gm_data,
145
+ (0, normal_length - gm_data.shape[0]),
146
+ 'constant',
147
+ constant_values=(0, 0)
148
+ )
149
+ # 2 The normalised length is less than the original length
150
+ else:
151
+ cut_rate = normal_length / gm_data.shape[0]
152
+ pga_loca = np.argmax(np.abs(gm_data))
153
+ forward_length = int(pga_loca * cut_rate)
154
+ res = gm_data[pga_loca - forward_length:normal_length - forward_length + pga_loca]
155
+ return res
@@ -0,0 +1,131 @@
1
+ # -*- coding:utf-8 -*-
2
+ # @FileName :sbs_integration_linear.py
3
+ # @Time :2024/8/25 下午7:57
4
+ # @Author :RichardoGu
5
+ import numpy as np
6
+
7
+
8
+ def segmented_parsing(mass: float,
9
+ stiffness: float,
10
+ load: np.ndarray,
11
+ time_step: float,
12
+ damping_ratio: float = 0.05,
13
+ disp_0: float = 0,
14
+ vel_0: float = 0) -> (np.ndarray, np.ndarray, np.ndarray):
15
+ """
16
+ This function is Segmented Parsing method, which is generally applicable
17
+ for solving the dynamic response of single degree of freedom system.
18
+
19
+ Args:
20
+ mass:
21
+ stiffness:
22
+ load: The dynamic load array, changeable over time, is often the ground motion.
23
+ This parameter should have 2 dims as (batch size, sequence length)
24
+ time_step: The time step of load
25
+ damping_ratio:
26
+ disp_0: The init displacement of system, is often 0
27
+ vel_0: The init velocity of system, is often 0
28
+
29
+ Returns:
30
+ The result is tuple, which consist of the ``(acceleration, velocity, displacement)`` response in order.
31
+ -------
32
+
33
+ """
34
+ # Array fit
35
+ if load.ndim == 1:
36
+ load = np.expand_dims(load, 0)
37
+ # Data preparation
38
+ batch_size = load.shape[0]
39
+ seq_length = load.shape[1]
40
+
41
+ omega_n = np.sqrt(stiffness / mass)
42
+ omega_d = omega_n * np.sqrt(1 - damping_ratio ** 2)
43
+ temp_1 = np.e ** (-damping_ratio * omega_n * time_step)
44
+ temp_2 = damping_ratio / np.sqrt(1 - damping_ratio ** 2)
45
+ temp_3 = 2 * damping_ratio / (omega_n * time_step)
46
+ temp_4 = (1 - 2 * damping_ratio ** 2) / (omega_d * time_step)
47
+ temp_5 = omega_n / np.sqrt(1 - damping_ratio ** 2)
48
+ sin = np.sin(omega_d * time_step)
49
+ cos = np.cos(omega_d * time_step)
50
+
51
+ p_a = temp_1 * (temp_2 * sin + cos)
52
+ p_b = temp_1 * (sin / omega_d)
53
+ p_c = 1 / stiffness * (temp_3 + temp_1 * (
54
+ (temp_4 - temp_2) * sin - (1 + temp_3) * cos
55
+ ))
56
+ p_d = 1 / stiffness * (1 - temp_3 + temp_1 * (
57
+ -temp_4 * sin + temp_3 * cos
58
+ ))
59
+ p_a_prime = -temp_1 * (temp_5 * sin)
60
+ p_b_prime = temp_1 * (cos - temp_2 * sin)
61
+ p_c_prime = 1 / stiffness * (-1 / time_step + temp_1 * (
62
+ (temp_5 + temp_2 / time_step) * sin + 1 / time_step * cos
63
+ ))
64
+ p_d_prime = 1 / (stiffness * time_step) * (
65
+ 1 - temp_1 * (temp_2 * sin + cos)
66
+ )
67
+
68
+ # Init the start displacement and velocity.
69
+ disp = np.zeros((batch_size, seq_length))
70
+ vel = np.zeros((batch_size, seq_length))
71
+ acc = np.zeros((batch_size, seq_length))
72
+
73
+ if type(disp_0) is not np.ndarray:
74
+ disp_0 = np.zeros(batch_size)
75
+ if type(vel_0) is not np.ndarray:
76
+ vel_0 = np.zeros(batch_size)
77
+ disp[:, 0] = disp_0
78
+ vel[:, 0] = vel_0
79
+
80
+ # Start Iteration
81
+ for i in range(seq_length - 1):
82
+ disp[:, i + 1] = p_a * disp[:, i] + p_b * vel[:, i] + p_c * load[:, i] + p_d * load[:, i + 1]
83
+ vel[:, i + 1] = (p_a_prime * disp[:, i] +
84
+ p_b_prime * vel[:, i] + p_c_prime * load[:, i] + p_d_prime * load[:, i + 1])
85
+ acc[:, i + 1] = -2 * damping_ratio * omega_n * vel[:, i + 1] - stiffness / mass * disp[:, i + 1]
86
+
87
+ return acc, vel, disp
88
+
89
+
90
+ def newmark_beta_single(mass, stiffness, load, time_step,
91
+ damping_ratio=0.05, disp_0=0, vel_0=0,
92
+ acc_0=0, beta=0.25, gamma=0.5,
93
+ result_length=0):
94
+ batch_size = load.shape[0]
95
+ seq_length = load.shape[1]
96
+ if result_length == 0:
97
+ result_length = int(1.2 * load.shape[1]) # 计算持时
98
+ load = np.append(load, np.zeros((batch_size, result_length - seq_length)), axis=1)
99
+
100
+ disp = np.zeros((batch_size, result_length))
101
+ vel = np.zeros((batch_size, result_length))
102
+ acc = np.zeros((batch_size, result_length))
103
+ if type(disp_0) is not np.ndarray:
104
+ disp_0 = np.zeros(batch_size)
105
+ if type(vel_0) is not np.ndarray:
106
+ vel_0 = np.zeros(batch_size)
107
+ if type(acc_0) is not np.ndarray:
108
+ acc_0 = np.zeros(batch_size)
109
+ disp[:, 0] = disp_0
110
+ vel[:, 0] = vel_0
111
+ acc[:, 0] = acc_0
112
+ a_0 = 1 / (beta * time_step ** 2)
113
+ a_1 = gamma / (beta * time_step)
114
+ a_2 = 1 / (beta * time_step)
115
+ a_3 = 1 / (2 * beta) - 1
116
+ a_4 = gamma / beta - 1
117
+ a_5 = time_step / 2 * (a_4 - 1)
118
+ a_6 = time_step * (1 - gamma)
119
+ a_7 = gamma * time_step
120
+ omega_n = np.sqrt(stiffness / mass)
121
+ damping = 2 * mass * omega_n * damping_ratio
122
+ equ_k = stiffness + a_0 * mass + a_1 * damping # 计算等效刚度
123
+ # 迭代正式开始
124
+ for i in range(result_length - 1):
125
+ equ_p = load[:, i + 1] + mass * (
126
+ a_0 * disp[:, i] + a_2 * vel[:, i] + a_3 * acc[:, i]) + damping * (
127
+ a_1 * disp[:, i] + a_4 * vel[:, i] + a_5 * acc[:, i]) # 计算等效荷载
128
+ disp[:, i + 1] = equ_p / equ_k # 计算位移
129
+ acc[:, i + 1] = a_0 * (disp[:, i + 1] - disp[:, i]) - a_2 * vel[:, i] - a_3 * acc[:, i] # 计算加速度
130
+ vel[:, i + 1] = vel[:, i] + a_6 * acc[:, i] + a_7 * acc[:, i + 1] # 计算速度
131
+ return acc, vel, disp
@@ -0,0 +1,113 @@
1
+ # -*- coding:utf-8 -*-
2
+ # @Time: 2025/3/17 17:32
3
+ # @Author: RichardoGu
4
+ """
5
+ Ground motion spectrum Calc
6
+ """
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ from numpy import ndarray, float64
11
+ from sbs_integration_linear import newmark_beta_single
12
+
13
+ SPECTRUM_PERIOD = [
14
+ 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09,
15
+ 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.6, 0.8, 1.0,
16
+ 1.2, 1.4, 1.6, 1.8, 2.0, 2.5, 3.0,
17
+ 3.5, 4.0, 5.0, 6.0
18
+ ] # 反应谱取点,单位秒
19
+
20
+
21
+ def get_spectrum(
22
+ gm_acc_data: np.ndarray,
23
+ time_step: float,
24
+ damping_ratio: float = 0.05,
25
+ # Calculation way options
26
+ calc_func: any = newmark_beta_single,
27
+ calc_opt: int = 0,
28
+ max_process: int = 4) -> (
29
+ ndarray[float64],
30
+ ndarray[float64],
31
+ ndarray[float64],
32
+ ndarray[float64],
33
+ ndarray[float64]):
34
+ """
35
+ There are three types of response spectrum of ground motion: acceleration, velocity and displacement.
36
+ Type must in tuple ("ACC", "VEL", "DISP"), and can be both upper and lower.
37
+
38
+ Class :class:`GroundMotionData` use ``self.spectrum_acc`` , ``self.spectrum_acc`` , ``self.spectrum_acc``
39
+ to save. And this three variants will be calculated when they are first used.
40
+
41
+ TODO we use default damping ratio 0.05, try to use changeable damping ratio as input.
42
+
43
+ Warnings:
44
+ ------
45
+ The programme starts multi-threaded calculations by default
46
+
47
+ Args:
48
+ gm_acc_data: Ground motion acc data.
49
+ time_step: Time step.
50
+ damping_ratio: Damping ratio.
51
+ calc_opt: The type of calculation to use.
52
+
53
+ - 0 Use single_threaded. Slow
54
+
55
+ - 1 Use multi_threaded. Faster TODO This func not completed.
56
+
57
+ calc_func: The type of calculation to use.
58
+ The return of calc_func should be a tuple ``(acc, vel, disp)``.
59
+
60
+ max_process: if calc_opt in [1,2], the multi thread will be used.
61
+ This is the maximum number of threads.
62
+
63
+ Returns:
64
+ Calculated spectrum. np.ndarray[float] (batch size)
65
+
66
+ """
67
+ if gm_acc_data.ndim == 1:
68
+ gm_acc_data = np.expand_dims(gm_acc_data, axis=0)
69
+ elif gm_acc_data.ndim == 2:
70
+ pass
71
+ else:
72
+ raise ValueError("ndim of gm_acc_data must be 1 or 2.")
73
+
74
+ batch_size = gm_acc_data.shape[0]
75
+ seq_len = gm_acc_data.shape[1]
76
+ spectrum_acc = np.zeros((batch_size, len(SPECTRUM_PERIOD)))
77
+ spectrum_vel = np.zeros((batch_size, len(SPECTRUM_PERIOD)))
78
+ spectrum_disp = np.zeros((batch_size, len(SPECTRUM_PERIOD)))
79
+ spectrum_pse_acc = np.zeros((batch_size, len(SPECTRUM_PERIOD)))
80
+ spectrum_pse_vel = np.zeros((batch_size, len(SPECTRUM_PERIOD)))
81
+
82
+ if calc_opt == 0:
83
+ for i in range(len(SPECTRUM_PERIOD)):
84
+ acc, vel, disp = calc_func(
85
+ mass=1,
86
+ stiffness=(2 * np.pi / SPECTRUM_PERIOD[i]) ** 2,
87
+ load=gm_acc_data,
88
+ damping_ratio=damping_ratio,
89
+ time_step=time_step,
90
+ result_length=seq_len
91
+ )
92
+ spectrum_acc[:, i] = np.abs(acc).max(1)
93
+ spectrum_vel[:, i] = np.abs(vel).max(1)
94
+ spectrum_disp[:, i] = np.abs(disp).max(1)
95
+ spectrum_pse_acc[:, i] = np.abs(disp).max(1) * (2 * np.pi / SPECTRUM_PERIOD[i]) ** 2
96
+ spectrum_pse_vel[:, i] = np.abs(disp).max(1) * (2 * np.pi / SPECTRUM_PERIOD[i])
97
+
98
+ elif calc_opt == 1:
99
+ # Create Processes
100
+ # TODO IF really need mutil-process, use cpp dll. Don't use python's mutil-process.
101
+ pass
102
+
103
+ else:
104
+ raise KeyError("Parameter 'calc_opt' should be 0 or 1.")
105
+
106
+ if gm_acc_data.shape[0] == 1:
107
+ spectrum_acc = spectrum_acc.squeeze()
108
+ spectrum_vel = spectrum_vel.squeeze()
109
+ spectrum_disp = spectrum_disp.squeeze()
110
+ spectrum_pse_acc = spectrum_pse_acc.squeeze()
111
+ spectrum_pse_acc = spectrum_pse_acc.squeeze()
112
+
113
+ return spectrum_acc, spectrum_vel, spectrum_disp, spectrum_pse_acc, spectrum_pse_acc
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "ground-motion-tools"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = [
6
+ {name = "RichardoGu",email = "xiaopenggu@qq.com"}
7
+ ]
8
+ license = {text = "MIT"}
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "numpy (>=2.2.4,<3.0.0)",
13
+ "scipy (>=1.15.2,<2.0.0)",
14
+ "geopy (>=2.4.1,<3.0.0)"
15
+ ]
16
+
17
+
18
+ [build-system]
19
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
20
+ build-backend = "poetry.core.masonry.api"