cfs-python 0.1.0__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.
cfs_lib/__init__.py ADDED
File without changes
@@ -0,0 +1,142 @@
1
+ import numpy as np
2
+
3
+ def calc_coulomb(strike_m, dip_m, rake_m, friction_m, ss):
4
+ """
5
+ Calculates shear, normal, and Coulomb stresses on a given fault.
6
+ strike_m, dip_m, rake_m, friction_m: 1D numpy arrays of shape (n,)
7
+ ss: 2D numpy array of shape (6, n) [SXX, SYY, SZZ, SYZ, SXZ, SXY]
8
+ Returns shear, normal, coulomb (each shape (n,))
9
+ """
10
+ n = len(strike_m)
11
+ friction = float(friction_m[0]) if isinstance(friction_m, np.ndarray) else friction_m
12
+
13
+ # adjustment for coordinate system from Aki & Richards
14
+ c1 = strike_m >= 180.0
15
+ c2 = strike_m < 180.0
16
+
17
+ strike = (strike_m - 180.0) * c1 + strike_m * c2
18
+ dip = -1.0 * dip_m * c1 + dip_m * c2
19
+ rake_m = rake_m - 90.0
20
+
21
+ c1 = rake_m <= -180.0
22
+ c2 = rake_m > -180.0
23
+ rake = (360.0 + rake_m) * c1 + rake_m * c2
24
+
25
+ strike = np.deg2rad(strike)
26
+ dip = np.deg2rad(dip)
27
+ rake = np.deg2rad(rake)
28
+
29
+ # Rake rotation
30
+ rsc = -rake
31
+ mtran = np.zeros((3, 3, n), dtype=np.float64)
32
+ # create xrotate matrix for each n
33
+ # rr = makehgtform('xrotate', rsc); in python:
34
+ # [1, 0, 0; 0, cos(a), -sin(a); 0, sin(a), cos(a)]
35
+ for i in range(n):
36
+ c_a = np.cos(rsc[i])
37
+ s_a = np.sin(rsc[i])
38
+ mtran[:, :, i] = np.array([
39
+ [1.0, 0.0, 0.0],
40
+ [0.0, c_a, -s_a],
41
+ [0.0, s_a, c_a]
42
+ ])
43
+
44
+ ver = np.pi / 2.0
45
+
46
+ c1 = strike >= 0.0
47
+ c2 = strike < 0.0
48
+ c3 = strike <= ver
49
+ c4 = strike > ver
50
+
51
+ c24 = c2 | c4
52
+
53
+ d1 = dip >= 0.0
54
+ d2 = dip < 0.0
55
+
56
+ xbeta = -1.0 * strike * d1 + (np.pi - strike) * d2
57
+ ybeta = (np.pi - strike) * d1 + -1.0 * strike * d2
58
+ zbeta = (ver - strike) * d1 + (-1.0 * ver - strike) * d2 * c1 * c3 + (np.pi + ver - strike) * d2 * c24
59
+
60
+ xdel = ver - np.abs(dip)
61
+ ydel = np.abs(dip)
62
+ zdel = np.zeros(n)
63
+
64
+ xl = np.cos(xdel) * np.cos(xbeta)
65
+ xm = np.cos(xdel) * np.sin(xbeta)
66
+ xn = np.sin(xdel)
67
+ yl = np.cos(ydel) * np.cos(ybeta)
68
+ ym = np.cos(ydel) * np.sin(ybeta)
69
+ yn = np.sin(ydel)
70
+ zl = np.cos(zdel) * np.cos(zbeta)
71
+ zm = np.cos(zdel) * np.sin(zbeta)
72
+ zn = np.sin(zdel)
73
+
74
+ t = np.zeros((6, 6, n), dtype=np.float64)
75
+
76
+ t[0, 0, :] = xl * xl
77
+ t[0, 1, :] = xm * xm
78
+ t[0, 2, :] = xn * xn
79
+ t[0, 3, :] = 2.0 * xm * xn
80
+ t[0, 4, :] = 2.0 * xn * xl
81
+ t[0, 5, :] = 2.0 * xl * xm
82
+
83
+ t[1, 0, :] = yl * yl
84
+ t[1, 1, :] = ym * ym
85
+ t[1, 2, :] = yn * yn
86
+ t[1, 3, :] = 2.0 * ym * yn
87
+ t[1, 4, :] = 2.0 * yn * yl
88
+ t[1, 5, :] = 2.0 * yl * ym
89
+
90
+ t[2, 0, :] = zl * zl
91
+ t[2, 1, :] = zm * zm
92
+ t[2, 2, :] = zn * zn
93
+ t[2, 3, :] = 2.0 * zm * zn
94
+ t[2, 4, :] = 2.0 * zn * zl
95
+ t[2, 5, :] = 2.0 * zl * zm
96
+
97
+ t[3, 0, :] = yl * zl
98
+ t[3, 1, :] = ym * zm
99
+ t[3, 2, :] = yn * zn
100
+ t[3, 3, :] = ym * zn + zm * yn
101
+ t[3, 4, :] = yn * zl + zn * yl
102
+ t[3, 5, :] = yl * zm + zl * ym
103
+
104
+ t[4, 0, :] = zl * xl
105
+ t[4, 1, :] = zm * xm
106
+ t[4, 2, :] = zn * xn
107
+ t[4, 3, :] = xm * zn + zm * xn
108
+ t[4, 4, :] = xn * zl + zn * xl
109
+ t[4, 5, :] = xl * zm + zl * xm
110
+
111
+ t[5, 0, :] = xl * yl
112
+ t[5, 1, :] = xm * ym
113
+ t[5, 2, :] = xn * yn
114
+ t[5, 3, :] = xm * yn + ym * xn
115
+ t[5, 4, :] = xn * yl + yn * xl
116
+ t[5, 5, :] = xl * ym + yl * xm
117
+
118
+ sn = np.zeros((6, n), dtype=np.float64)
119
+ sn9 = np.zeros((3, 3, n), dtype=np.float64)
120
+
121
+ for k in range(n):
122
+ sn[:, k] = np.dot(t[:, :, k], ss[:, k])
123
+
124
+ sn9[0, 0, k] = sn[0, k]
125
+ sn9[0, 1, k] = sn[5, k]
126
+ sn9[0, 2, k] = sn[4, k]
127
+
128
+ sn9[1, 0, k] = sn[5, k]
129
+ sn9[1, 1, k] = sn[1, k]
130
+ sn9[1, 2, k] = sn[3, k]
131
+
132
+ sn9[2, 0, k] = sn[4, k]
133
+ sn9[2, 1, k] = sn[3, k]
134
+ sn9[2, 2, k] = sn[2, k]
135
+
136
+ sn9[:, :, k] = np.dot(sn9[:, :, k], mtran[:, :, k])
137
+
138
+ shear = sn9[0, 1, :]
139
+ normal = sn9[0, 0, :]
140
+ coulomb = shear + friction * normal
141
+
142
+ return shear, normal, coulomb
cfs_lib/io_parser.py ADDED
@@ -0,0 +1,165 @@
1
+ import re
2
+ import numpy as np
3
+
4
+ def open_input_file_cui(filename):
5
+ """
6
+ Parses a Coulomb 3.3 format text file.
7
+ Returns: xvec, yvec, z, el, kode, pois, young, cdepth, fric, rstress
8
+ """
9
+ with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
10
+ lines = f.readlines()
11
+
12
+ num = 0
13
+ pois = 0.25
14
+ cdepth = 7.5
15
+ young = 8e5
16
+ fric = 0.4
17
+ rstress = [0.0, 0.0, 0.0]
18
+
19
+ grid = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # xstart, ystart, xend, yend, xinc, yinc
20
+ map_info = {}
21
+ cross_section = {}
22
+
23
+ in_fault_elements = False
24
+ in_grid_params = False
25
+ faults = []
26
+
27
+ for i, line in enumerate(lines):
28
+ # We need to read PR1, YOUNG'S, FRIC. COEFFICIENT, SIGMA1-SIGMA3, Grid parameters
29
+ up_line = line.upper()
30
+
31
+ if "PR1=" in up_line:
32
+ parts = line.split()
33
+ try:
34
+ pois = float(parts[1])
35
+ cdepth = float(parts[5])
36
+ except: pass
37
+
38
+ if "YOUNG'S MODULUS" in up_line:
39
+ try: young = float(line.split()[0])
40
+ except: pass
41
+
42
+ if "FRIC. COEFFICIENT" in up_line:
43
+ try: fric = float(line.split()[0])
44
+ except: pass
45
+
46
+ if "SIGMA1-SIGMA3" in up_line:
47
+ floats = [float(s) for s in re.findall(r'-?\d+\.?\d*', line)]
48
+ if len(floats) >= 3:
49
+ rstress = floats[:3]
50
+
51
+ if "X-START" in up_line:
52
+ in_grid_params = True
53
+
54
+ if "XXX" in up_line:
55
+ in_fault_elements = not in_fault_elements
56
+ continue
57
+
58
+ if in_fault_elements:
59
+ parts = line.split()
60
+ if len(parts) >= 11:
61
+ try:
62
+ xs, ys, xf, yf = map(float, parts[1:5])
63
+ kode = int(parts[5])
64
+
65
+ if "RAKE" in " ".join(lines).upper():
66
+ rake, netslip, dip, top, bottom = map(float, parts[6:11])
67
+ latslip = np.cos(np.deg2rad(rake)) * netslip * -1.0
68
+ dipslip = np.sin(np.deg2rad(rake)) * netslip
69
+ else:
70
+ latslip, dipslip, dip, top, bottom = map(float, parts[6:11])
71
+
72
+ faults.append([xs, ys, xf, yf, latslip, dipslip, dip, top, bottom, kode])
73
+ except ValueError:
74
+ pass
75
+
76
+ # reading grid info
77
+ if "CROSS SECTION DEFAULT" in up_line:
78
+ in_grid_params = False
79
+ in_fault_elements = False
80
+ continue
81
+ elif "MAP INFO" in up_line:
82
+ in_grid_params = False
83
+ in_fault_elements = False
84
+ continue
85
+
86
+ if in_grid_params and "=" in line:
87
+ parts = line.split("=")
88
+ if len(parts) == 2:
89
+ try:
90
+ val = float(parts[1].split()[0])
91
+ if "X-start" in up_line: grid[0] = val
92
+ elif "Y-start" in up_line: grid[1] = val
93
+ elif "X-finish" in up_line: grid[2] = val
94
+ elif "Y-finish" in up_line: grid[3] = val
95
+ elif "X-inc" in up_line: grid[4] = val
96
+ elif "Y-inc" in up_line:
97
+ grid[5] = val
98
+ in_grid_params = False
99
+ except:
100
+ pass
101
+
102
+ # We can just look for specific keywords for map info and cross section
103
+ if "=" in line:
104
+ parts = line.split("=")
105
+ if len(parts) == 2:
106
+ try:
107
+ val = float(parts[1].split()[0])
108
+ # Cross section
109
+ if "START-X" in up_line and "DEFAULT" not in up_line and not in_grid_params: cross_section["start_x"] = val
110
+ elif "START-Y" in up_line and not in_grid_params: cross_section["start_y"] = val
111
+ elif "FINISH-X" in up_line and not in_grid_params: cross_section["finish_x"] = val
112
+ elif "FINISH-Y" in up_line and not in_grid_params: cross_section["finish_y"] = val
113
+
114
+ # Map Info
115
+ elif "MIN. LON" in up_line: map_info["min_lon"] = val
116
+ elif "MAX. LON" in up_line: map_info["max_lon"] = val
117
+ elif "ZERO LON" in up_line: map_info["zero_lon"] = val
118
+ elif "MIN. LAT" in up_line: map_info["min_lat"] = val
119
+ elif "MAX. LAT" in up_line: map_info["max_lat"] = val
120
+ elif "ZERO LAT" in up_line: map_info["zero_lat"] = val
121
+ except:
122
+ pass
123
+
124
+ if len(faults) == 0:
125
+ raise ValueError("No valid faults found in the input file.")
126
+
127
+ faults = np.array(faults)
128
+ el = faults[:, :9]
129
+ kode = faults[:, 9].astype(int)
130
+
131
+ # safeguard grid increments to prevent memory error
132
+ xin = max(grid[4], 0.001)
133
+ yin = max(grid[5], 0.001)
134
+
135
+ xvec = np.arange(grid[0], grid[2] + xin*0.5, xin)
136
+ yvec = np.arange(grid[1], grid[3] + yin*0.5, yin)
137
+ z = cdepth
138
+
139
+ # Default missing map info to 0.0
140
+ for k in ["min_lon", "max_lon", "zero_lon", "min_lat", "max_lat", "zero_lat"]:
141
+ if k not in map_info:
142
+ map_info[k] = 0.0
143
+
144
+ return xvec, yvec, z, el, kode, pois, young, cdepth, fric, rstress, map_info, cross_section
145
+
146
+
147
+ def open_batch_file(filename):
148
+ with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
149
+ lines = f.readlines()
150
+
151
+ data = []
152
+ for line in lines[2:]:
153
+ parts = line.split()
154
+ if len(parts) >= 6:
155
+ try:
156
+ data.append([float(x) for x in parts[:6]])
157
+ except: pass
158
+
159
+ data = np.array(data)
160
+ pos = data[:, :3]
161
+ strike = data[:, 3]
162
+ dip = data[:, 4]
163
+ rake = data[:, 5]
164
+
165
+ return pos, strike, dip, rake
cfs_lib/main.py ADDED
@@ -0,0 +1,173 @@
1
+ import numpy as np
2
+ from .io_parser import open_input_file_cui, open_batch_file
3
+ from .okada_wrapper import okada_elastic_halfspace
4
+ from .coulomb_math import calc_coulomb
5
+
6
+ def coulomb_cui(sourceFileName='test.dat', calcFunction='deformation', receiver='30/90/180', batchFileName='test.dat', parse_only=False, grid_params=None):
7
+ """
8
+ Python entry point for Coulomb calculations.
9
+ """
10
+ # Parse input file
11
+ try:
12
+ xvec, yvec, z, el, kode, pois, young, cdepth, fric, rstress = open_input_file_cui(sourceFileName)
13
+ except FileNotFoundError:
14
+ print(f"File {sourceFileName} not found.")
15
+ return None, None
16
+
17
+ # If explicit grid params are provided (e.g. from UI), override the file ones
18
+ if grid_params:
19
+ x_start, x_end, x_inc = grid_params['min_x'], grid_params['max_x'], grid_params['inc']
20
+ y_start, y_end, y_inc = grid_params['min_y'], grid_params['max_y'], grid_params['inc']
21
+ cdepth = grid_params['depth']
22
+
23
+ # recreate xvec, yvec
24
+ xvec = np.arange(x_start, x_end + x_inc, x_inc)
25
+ yvec = np.arange(y_start, y_end + y_inc, y_inc)
26
+
27
+ if parse_only:
28
+ # Just return the faults and grid params for UI preview
29
+ results_dict = {
30
+ "faults": el.tolist(),
31
+ "x": xvec.tolist(),
32
+ "y": yvec.tolist(),
33
+ "cdepth": cdepth
34
+ }
35
+ return "parsed", results_dict
36
+ return None
37
+
38
+ if calcFunction == 'deformation':
39
+ print(f"Calculating deformation for {sourceFileName}...")
40
+ dc3d = okada_elastic_halfspace(xvec, yvec, el, young, pois, cdepth, kode)
41
+ fout = 'halfspace_def_out.dat'
42
+ with open(fout, 'w') as f:
43
+ f.write("x y z ux uy uz sxx syy szz syz sxz sxy\n")
44
+ f.write("(km) (km) (km) (m) (m) (m) (bar) (bar) (bar) (bar) (bar) (bar)\n")
45
+ for i in range(dc3d.shape[0]):
46
+ # print 1:2, 5:14 (0-based: 0, 1 and 4-13)
47
+ row = dc3d[i]
48
+ arr = [row[0], row[1]] + list(row[4:14])
49
+ # format string
50
+ fmt = "".join([f"{x:10.4f}" for x in arr])
51
+ f.write(fmt + " \n")
52
+ print(f"Done. Wrote to {fout}")
53
+ results_dict = {
54
+ "x": dc3d[:, 0].tolist(),
55
+ "y": dc3d[:, 1].tolist(),
56
+ "z": dc3d[:, 4].tolist(),
57
+ "ux": dc3d[:, 5].tolist(),
58
+ "uy": dc3d[:, 6].tolist(),
59
+ "uz": dc3d[:, 7].tolist(),
60
+ "sxx": dc3d[:, 8].tolist(),
61
+ "syy": dc3d[:, 9].tolist(),
62
+ "szz": dc3d[:, 10].tolist(),
63
+ "syz": dc3d[:, 11].tolist(),
64
+ "sxz": dc3d[:, 12].tolist(),
65
+ "sxy": dc3d[:, 13].tolist(),
66
+ "faults": el.tolist()
67
+ }
68
+ return fout, results_dict
69
+
70
+ elif calcFunction == 'coulomb':
71
+ print(f"Calculating coulomb for {sourceFileName} at {receiver}...")
72
+ dc3d = okada_elastic_halfspace(xvec, yvec, el, young, pois, cdepth, kode)
73
+
74
+ parts = receiver.split('/')
75
+ strike_m = float(parts[0]) * np.ones(dc3d.shape[0])
76
+ dip_m = float(parts[1]) * np.ones(dc3d.shape[0])
77
+ rake_m = float(parts[2]) * np.ones(dc3d.shape[0])
78
+ friction_m = fric * np.ones(dc3d.shape[0])
79
+
80
+ # passed array: ss is dc3d(:, 9:14)' -> shape (6, ncell) in MATLAB
81
+ # python dc3d is shape (n, 14), 8-13 is stress (sxx_n to sxy_n)
82
+ ss = dc3d[:, 8:14].T
83
+
84
+ shear, normal, coulomb = calc_coulomb(strike_m, dip_m, rake_m, friction_m, ss)
85
+
86
+ fout = 'coulomb_out.dat'
87
+ with open(fout, 'w') as f:
88
+ f.write("x y z strike dip rake shear normal coulomb\n")
89
+ f.write("(km) (km) (km) (deg) (deg) (deg) (bar) (bar) (bar)\n")
90
+ for i in range(dc3d.shape[0]):
91
+ f.write(f"{dc3d[i,0]:10.4f}{dc3d[i,1]:10.4f}{dc3d[i,4]:10.4f}")
92
+ f.write(f"{strike_m[i]:7.1f}{dip_m[i]:6.1f}{rake_m[i]:8.1f}")
93
+ f.write(f"{shear[i]:10.4f}{normal[i]:10.4f}{coulomb[i]:10.4f} \n")
94
+ print(f"Done. Wrote to {fout}")
95
+ results_dict = {
96
+ "x": dc3d[:, 0].tolist(),
97
+ "y": dc3d[:, 1].tolist(),
98
+ "z": dc3d[:, 4].tolist(),
99
+ "ux": dc3d[:, 5].tolist(),
100
+ "uy": dc3d[:, 6].tolist(),
101
+ "uz": dc3d[:, 7].tolist(),
102
+ "sxx": dc3d[:, 8].tolist(),
103
+ "syy": dc3d[:, 9].tolist(),
104
+ "szz": dc3d[:, 10].tolist(),
105
+ "syz": dc3d[:, 11].tolist(),
106
+ "sxz": dc3d[:, 12].tolist(),
107
+ "sxy": dc3d[:, 13].tolist(),
108
+ "shear": shear.tolist(),
109
+ "normal": normal.tolist(),
110
+ "coulomb": coulomb.tolist(),
111
+ "faults": el.tolist()
112
+ }
113
+ return fout, results_dict
114
+
115
+ elif calcFunction == 'batch':
116
+ print(f"Calculating batch from {batchFileName} using {sourceFileName}...")
117
+ try:
118
+ pos, strike_m, dip_m, rake_m = open_batch_file(batchFileName)
119
+ except FileNotFoundError:
120
+ print(f"Batch file {batchFileName} not found.")
121
+ return None
122
+
123
+ x_g, y_g, z_g = pos[:, 0], pos[:, 1], pos[:, 2] # actually -z in okada
124
+ cdepth_i = -z_g
125
+
126
+ # call okada_elastic_halfspace with all points
127
+ res = okada_elastic_halfspace(x_g, y_g, el, young, pois, cdepth_i, kode)
128
+ dc3d = res
129
+
130
+ # res shape (n_batch, 14), ss needed shape (6, n_batch) -> 8:14 transposed
131
+ ss = res[:, 8:14].T
132
+
133
+ s, n_s, c = calc_coulomb(strike_m, dip_m, rake_m, np.full(pos.shape[0], fric), ss)
134
+ shear = s
135
+ normal = n_s
136
+ coulomb = c
137
+
138
+ fout = 'coulomb_out.dat'
139
+ with open(fout, 'w') as f:
140
+ f.write("x y z strike dip rake shear normal coulomb\n")
141
+ f.write("(km) (km) (km) (deg) (deg) (deg) (bar) (bar) (bar)\n")
142
+ for i in range(dc3d.shape[0]):
143
+ f.write(f"{dc3d[i,0]:10.4f}{dc3d[i,1]:10.4f}{dc3d[i,4]:10.4f}")
144
+ f.write(f"{strike_m[i]:7.1f}{dip_m[i]:6.1f}{rake_m[i]:8.1f}")
145
+ f.write(f"{shear[i]:10.4f}{normal[i]:10.4f}{coulomb[i]:10.4f} \n")
146
+ print(f"Done. Wrote to {fout}")
147
+ results_dict = {
148
+ "x": dc3d[:, 0].tolist(),
149
+ "y": dc3d[:, 1].tolist(),
150
+ "z": dc3d[:, 4].tolist(),
151
+ "ux": dc3d[:, 5].tolist(),
152
+ "uy": dc3d[:, 6].tolist(),
153
+ "uz": dc3d[:, 7].tolist(),
154
+ "sxx": dc3d[:, 8].tolist(),
155
+ "syy": dc3d[:, 9].tolist(),
156
+ "szz": dc3d[:, 10].tolist(),
157
+ "syz": dc3d[:, 11].tolist(),
158
+ "sxz": dc3d[:, 12].tolist(),
159
+ "sxy": dc3d[:, 13].tolist(),
160
+ "shear": shear.tolist(),
161
+ "normal": normal.tolist(),
162
+ "coulomb": coulomb.tolist(),
163
+ "faults": el.tolist()
164
+ }
165
+ return fout, results_dict
166
+ else:
167
+ print("Invalid calcFunction")
168
+ return None, None
169
+
170
+ if __name__ == "__main__":
171
+ import sys
172
+ # test defaults
173
+ coulomb_cui()