@@ -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