Skip to content

Commit f2c5104

Browse files
author
Lachlan Grose
committed
fix: use fault normals/slip vectors from data if available.
Otherwise calculate from trace
1 parent a866916 commit f2c5104

File tree

2 files changed

+97
-20
lines changed

2 files changed

+97
-20
lines changed

LoopStructural/modelling/features/builders/_fault_builder.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,30 @@ def create_data_from_geometry(
151151
["X", "Y"],
152152
].to_numpy()
153153
if fault_normal_vector is None:
154-
# Calculate fault strike using least squares fit
155-
X = fault_trace[:, 0].reshape(-1, 1)
156-
Y = fault_trace[:, 1]
157-
# Fit line Y = mX + b
158-
A = np.hstack([X, np.ones_like(X)])
159-
m, _ = np.linalg.lstsq(A, Y, rcond=None)[0]
160-
# Convert slope to strike vector
161-
strike_vector = np.array([1, m, 0])
162-
strike_vector /= np.linalg.norm(strike_vector)
163-
fault_normal_vector = np.cross(strike_vector, [0, 0, 1])
154+
if fault_frame_data.loc[
155+
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["nx"].notna())].shape[0]>0:
156+
fault_normal_vector = fault_frame_data.loc[
157+
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["nx"].notna()),
158+
["nx", "ny", "nz"],
159+
].to_numpy().mean(axis=0)
160+
161+
else:
162+
163+
# Calculate fault strike using eigenvectors
164+
pts = fault_trace - fault_trace.mean(axis=0)
165+
# Calculate covariance matrix
166+
cov_matrix = pts.T @ pts
167+
# Get eigenvectors and eigenvalues
168+
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
169+
# Use eigenvector with largest eigenvalue as strike direction
170+
strike_vector = eigenvectors[:, np.argmax(eigenvalues)]
171+
strike_vector = np.append(strike_vector, 0) # Add z component
172+
strike_vector /= np.linalg.norm(strike_vector)
173+
174+
fault_normal_vector = np.cross(strike_vector, [0, 0, 1])
175+
# Rotate the fault normal vector according to the fault dip
176+
rotation_matrix = rotation(strike_vector[None, :], np.array([90 - fault_dip]))
177+
fault_normal_vector = np.einsum("ijk,ik->ij", rotation_matrix, fault_normal_vector[None, :])[0]
164178

165179
if not isinstance(fault_normal_vector, np.ndarray):
166180
fault_normal_vector = np.array(fault_normal_vector)
@@ -170,10 +184,18 @@ def create_data_from_geometry(
170184
fault_slip_vector = np.einsum("ijk,ik->ij", rotation_matrix, fault_normal_vector[None, :])[0]
171185

172186
if fault_slip_vector is None:
173-
fault_slip_vector = np.cross(fault_normal_vector, [1., 0., 0.])
174-
if np.linalg.norm(fault_slip_vector) == 0:
175-
fault_slip_vector = np.cross(fault_normal_vector, [0., 1., 0.])
176-
fault_slip_vector /= np.linalg.norm(fault_slip_vector)
187+
if fault_frame_data.loc[
188+
np.logical_and(fault_frame_data["coord"] == 1, fault_frame_data["nx"].notna())].shape[0]>0:
189+
fault_slip_vector = fault_frame_data.loc[
190+
np.logical_and(fault_frame_data["coord"] == 1, fault_frame_data["nx"].notna()),
191+
["nx", "ny", "nz"],
192+
].to_numpy().mean(axis=0)
193+
194+
else:
195+
fault_slip_vector = np.cross(fault_normal_vector, [1., 0., 0.])
196+
if np.linalg.norm(fault_slip_vector) == 0:
197+
fault_slip_vector = np.cross(fault_normal_vector, [0., 1., 0.])
198+
fault_slip_vector /= np.linalg.norm(fault_slip_vector)
177199
if fault_center is None:
178200
fault_trace = fault_frame_data.loc[
179201
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["val"] == 0),

tests/unit/modelling/test__fault_builder.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ def test_fault_builder_update_geometry(interpolatortype):
2121
fault_builder.update_geometry(points)
2222

2323
# Check if the origin and maximum are updated correctly
24-
expected_origin = np.array([0.5, 0.5, 0.5])
25-
expected_maximum = np.array([0.7, 0.7, 0.7])
26-
assert np.allclose(fault_builder.origin, expected_origin)
27-
assert np.allclose(fault_builder.maximum, expected_maximum)
24+
# Calculate buffer as 10% of the range
25+
buffer = 0.1 * (np.array([0.7, 0.7, 0.7]) - np.array([0.5, 0.5, 0.5]))
26+
expected_origin = np.array([0.5, 0.5, 0.5]) - buffer
27+
expected_maximum = np.array([0.7, 0.7, 0.7]) + buffer
28+
# rtol 1e-2 is 1% relative tolerance
29+
assert np.allclose(fault_builder.origin, expected_origin,rtol=1e-2)
30+
assert np.allclose(fault_builder.maximum, expected_maximum, rtol=1e-2)
2831

2932

30-
def test_fault_builder_create_data_from_geometry(interpolatortype):
33+
def test_fault_builder_create_data_from_geometry_with_normals(interpolatortype):
3134
# Create a FaultBuilder instance
3235
bounding_box = BoundingBox([0, 0, 0], [1, 1, 1])
3336
nelements = 1000
@@ -60,7 +63,59 @@ def test_fault_builder_create_data_from_geometry(interpolatortype):
6063
# Check if the fault attributes are updated correctly
6164
expected_fault_normal_vector = np.array([0, 1.0, 0])
6265
expected_fault_slip_vector = np.array([0.0, 0.0, 1.0])
63-
# expected_fault_centre = np.array([0.15, 0.6, 1.05])
66+
expected_fault_centre = np.array([0.15, 0.6, 1.05])
67+
expected_fault_normal_vector /= np.linalg.norm(expected_fault_normal_vector)
68+
expected_fault_slip_vector /= np.linalg.norm(expected_fault_slip_vector)
69+
# expected_fault_minor_axis = 0.5
70+
# expected_fault_major_axis = 1.0
71+
# expected_fault_intermediate_axis = 1.0
72+
calculated_fault_normal_vector = fault_builder.fault_normal_vector
73+
calculated_fault_slip_vector = fault_builder.fault_slip_vector
74+
calculated_fault_normal_vector /= np.linalg.norm(calculated_fault_normal_vector)
75+
calculated_fault_slip_vector /= np.linalg.norm(calculated_fault_slip_vector)
76+
assert np.allclose(calculated_fault_normal_vector, expected_fault_normal_vector)
77+
assert np.allclose(calculated_fault_slip_vector, expected_fault_slip_vector)
78+
# assert np.allclose(fault_builder.fault_centre, expected_fault_centre)
79+
# assert np.isclose(fault_builder.fault_minor_axis, expected_fault_minor_axis)
80+
# assert np.isclose(fault_builder.fault_major_axis, expected_fault_major_axis)
81+
# assert np.isclose(fault_builder.fault_intermediate_axis, expected_fault_intermediate_axis)
82+
83+
84+
def test_fault_builder_create_data_from_geometry_without_normals(interpolatortype):
85+
# Create a FaultBuilder instance
86+
bounding_box = BoundingBox([0, 0, 0], [1, 1, 1])
87+
nelements = 1000
88+
model = GeologicalModel([0, 0, 0], [1, 1, 1])
89+
fault_bounding_box_buffer = 0.2
90+
fault_builder = FaultBuilder(
91+
interpolatortype, bounding_box, nelements, model, fault_bounding_box_buffer
92+
)
93+
94+
# Create some test data
95+
fault_frame_data = pd.DataFrame(
96+
{
97+
"coord": [0.0, 0.0, 1.0, 2.0],
98+
"val": [0.0, 0.0, 1.0, 1.0],
99+
"gx": [np.nan, np.nan, np.nan, np.nan],
100+
"gy": [np.nan, np.nan, np.nan, np.nan],
101+
"gz": [np.nan, np.nan, np.nan, np.nan],
102+
"X": [0.1, 0.2, 0.3, 0.4],
103+
"Y": [0.5, 0.6, 0.7, 0.8],
104+
"Z": [0.9, 1.0, 1.1, 1.2],
105+
"nx": [np.nan, np.nan, np.nan, np.nan],
106+
"ny": [np.nan, np.nan, np.nan, np.nan],
107+
"nz": [np.nan, np.nan, np.nan, np.nan],
108+
}
109+
)
110+
111+
# Call the create_data_from_geometry method
112+
fault_builder.create_data_from_geometry(fault_frame_data)
113+
strike_vector = np.array([-.1,-.1,0])
114+
strike_vector /= np.linalg.norm(strike_vector)
115+
expected_fault_normal_vector = np.cross(strike_vector, np.array([0, 0, 1]))
116+
expected_fault_normal_vector /= np.linalg.norm(expected_fault_normal_vector)
117+
# Check if the fault attributes are updated correctly
118+
expected_fault_slip_vector = np.cross(expected_fault_normal_vector, [1.0, 0.0, 0.0])
64119
expected_fault_normal_vector /= np.linalg.norm(expected_fault_normal_vector)
65120
expected_fault_slip_vector /= np.linalg.norm(expected_fault_slip_vector)
66121
# expected_fault_minor_axis = 0.5

0 commit comments

Comments
 (0)