Skip to content

Commit fb7f1e7

Browse files
authored
Merge pull request #282 from Loop3D/lachlan/fix-fault-builder-gx-only
update fault builder for building faults from surface vertices/ 3D points
2 parents df209b2 + 5d9e04b commit fb7f1e7

2 files changed

Lines changed: 41 additions & 17 deletions

File tree

LoopStructural/interpolators/_geological_interpolator.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def set_value_constraints(self, points: np.ndarray):
252252
points = np.hstack([points, np.ones((points.shape[0], 1))])
253253
if points.shape[1] < self.dimensions + 2:
254254
raise ValueError("Value points must at least have X,Y,Z,val,w")
255-
self.data["value"] = points
255+
self.data["value"] = points.copy()
256256
self.n_i = points.shape[0]
257257
self.up_to_date = False
258258

@@ -282,7 +282,7 @@ def set_gradient_constraints(self, points: np.ndarray):
282282
if points.shape[1] < self.dimensions * 2 + 1:
283283
raise ValueError("Gradient constraints must at least have X,Y,Z,gx,gy,gz")
284284
self.n_g = points.shape[0]
285-
self.data["gradient"] = points
285+
self.data["gradient"] = points.copy()
286286
self.up_to_date = False
287287

288288
def set_normal_constraints(self, points: np.ndarray):
@@ -308,7 +308,7 @@ def set_normal_constraints(self, points: np.ndarray):
308308
if points.shape[1] < self.dimensions * 2 + 1:
309309
raise ValueError("Normal constraints must at least have X,Y,Z,nx,ny,nz")
310310
self.n_n = points.shape[0]
311-
self.data["normal"] = points
311+
self.data["normal"] = points.copy()
312312
self.up_to_date = False
313313

314314
def set_tangent_constraints(self, points: np.ndarray):
@@ -328,24 +328,24 @@ def set_tangent_constraints(self, points: np.ndarray):
328328
points = np.hstack([points, np.ones((points.shape[0], 1))])
329329
if points.shape[1] < self.dimensions * 2 + 1:
330330
raise ValueError("Tangent constraints must at least have X,Y,Z,tx,ty,tz")
331-
self.data["tangent"] = points
331+
self.data["tangent"] = points.copy()
332332
self.up_to_date = False
333333

334334
def set_interface_constraints(self, points: np.ndarray):
335-
self.data["interface"] = points
335+
self.data["interface"] = points.copy()
336336
self.up_to_date = False
337337

338338
def set_value_inequality_constraints(self, points: np.ndarray):
339339
if points.shape[1] < self.dimensions + 2:
340340
raise ValueError("Inequality constraints must at least have X,Y,Z,lower,upper")
341-
self.data["inequality"] = points
341+
self.data["inequality"] = points.copy()
342342
self.up_to_date = False
343343

344344
def set_inequality_pairs_constraints(self, points: np.ndarray):
345345
if points.shape[1] < self.dimensions + 1:
346346
raise ValueError("Inequality pairs constraints must at least have X,Y,Z,rock_id")
347347

348-
self.data["inequality_pairs"] = points
348+
self.data["inequality_pairs"] = points.copy()
349349
self.up_to_date = False
350350

351351
def get_value_constraints(self):

LoopStructural/modelling/features/builders/_fault_builder.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def create_data_from_geometry(
246246
fault_dip=90,
247247
fault_dip_anisotropy=1.0,
248248
fault_pitch=None,
249+
plane_line_threshold=0.05,
249250
):
250251
"""
251252
Generate the required data for building a fault frame with the specified parameters.
@@ -294,30 +295,53 @@ def create_data_from_geometry(
294295
].to_numpy()
295296
self.fault_dip = fault_dip
296297
if fault_normal_vector is None:
297-
if fault_frame_data.loc[
298-
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["nx"].notna())].shape[0]>0:
298+
gx_mask = np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["gx"].notna())
299+
nx_mask = np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["nx"].notna())
300+
value_mask = np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["val"] == 0)
301+
if not fault_frame_data.loc[gx_mask].empty:
302+
fault_normal_vector = fault_frame_data.loc[
303+
gx_mask,
304+
["gx", "gy", "gz"],
305+
].to_numpy().mean(axis=0)
306+
elif not fault_frame_data.loc[nx_mask].empty:
299307
fault_normal_vector = fault_frame_data.loc[
300-
np.logical_and(fault_frame_data["coord"] == 0, fault_frame_data["nx"].notna()),
308+
nx_mask,
301309
["nx", "ny", "nz"],
302310
].to_numpy().mean(axis=0)
303311

304312
else:
313+
fault_surface_pts = fault_frame_data.loc[
314+
value_mask,
315+
["X", "Y", "Z"],
316+
].to_numpy()
317+
fault_surface_pts = fault_surface_pts[
318+
~np.isnan(fault_surface_pts).any(axis=1)
319+
]
305320

306-
# Calculate fault strike using eigenvectors
321+
if fault_surface_pts.shape[0] >= 3:
322+
pts_3d = fault_surface_pts - fault_surface_pts.mean(axis=0)
323+
cov_matrix = pts_3d.T @ pts_3d
324+
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
325+
# If points span a surface, use the smallest eigenvector as plane normal
326+
if eigenvalues[-1] > 0 and (eigenvalues[1] / eigenvalues[-1]) > plane_line_threshold:
327+
fault_normal_vector = eigenvectors[:, 0]
328+
329+
if fault_normal_vector is None:
330+
# Fall back to line-on-map strike logic
307331
pts = fault_trace - fault_trace.mean(axis=0)
308-
# Calculate covariance matrix
309332
cov_matrix = pts.T @ pts
310-
# Get eigenvectors and eigenvalues
311333
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
312-
# Use eigenvector with largest eigenvalue as strike direction
313334
strike_vector = eigenvectors[:, np.argmax(eigenvalues)]
314335
strike_vector = np.append(strike_vector, 0) # Add z component
315336
strike_vector /= np.linalg.norm(strike_vector)
316337

317338
fault_normal_vector = np.cross(strike_vector, [0, 0, 1])
318-
# Rotate the fault normal vector according to the fault dip
319-
rotation_matrix = rotation(strike_vector[None, :], np.array([90 - fault_dip]))
320-
fault_normal_vector = np.einsum("ijk,ik->ij", rotation_matrix, fault_normal_vector[None, :])[0]
339+
rotation_matrix = rotation(
340+
strike_vector[None, :], np.array([90 - fault_dip])
341+
)
342+
fault_normal_vector = np.einsum(
343+
"ijk,ik->ij", rotation_matrix, fault_normal_vector[None, :]
344+
)[0]
321345

322346
if not isinstance(fault_normal_vector, np.ndarray):
323347
fault_normal_vector = np.array(fault_normal_vector)

0 commit comments

Comments
 (0)