Skip to content

Commit 50a04af

Browse files
committed
fix: under constrained faults now work
automatically adds an anisotropy to the fault trace if no dip data assumes 90 degrees and adds an anisotropy along dip vector fault_dip parameter can be used to tune the dip value This should make faults more reliable in loopstructural without having to add lots of points fault_dip_anisotropy and fault_trace_anisotropy are weights for the least squares system default to 1.0 but can be increased/decreased to change how much these constraints are used.
1 parent 72f7744 commit 50a04af

File tree

2 files changed

+117
-32
lines changed

2 files changed

+117
-32
lines changed

LoopStructural/modelling/core/geological_model.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Main entry point for creating a geological model
33
"""
4+
45
from ...utils import getLogger, log_to_file
56

67
import numpy as np
@@ -134,7 +135,6 @@ def __init__(
134135
)
135136
logger.info(maximumstr)
136137

137-
lengths = self.maximum - self.origin
138138
self.scale_factor = 1.0
139139

140140
self.bounding_box = BoundingBox(
@@ -310,7 +310,6 @@ def from_processor(cls, processor):
310310
model.data = processor.data
311311
if processor.fault_properties is not None:
312312
for i in processor.fault_network.faults:
313-
logger.info(f"Adding fault {i}")
314313
model.create_and_add_fault(
315314
i,
316315
**processor.fault_properties.to_dict("index")[i],
@@ -642,27 +641,6 @@ def set_model_data(self, data):
642641
)
643642
self.data = data
644643

645-
def extend_model_data(self, newdata):
646-
"""
647-
Extends the data frame
648-
649-
Parameters
650-
----------
651-
newdata : pandas data frame
652-
data to add to the existing dataframe
653-
Returns
654-
-------
655-
"""
656-
logger.warning("Extend data is untested and may have unexpected consequences")
657-
data_temp = newdata.copy()
658-
data_temp["X"] -= self.origin[0]
659-
data_temp["Y"] -= self.origin[1]
660-
data_temp["Z"] -= self.origin[2]
661-
data_temp["X"] /= self.scale_factor
662-
data_temp["Y"] /= self.scale_factor
663-
data_temp["Z"] /= self.scale_factor
664-
self.data.concat([self.data, data_temp], sort=True)
665-
666644
def set_stratigraphic_column(self, stratigraphic_column, cmap="tab20"):
667645
"""
668646
Adds a stratigraphic column to the model
@@ -833,7 +811,7 @@ def create_and_add_folded_foliation(
833811
fold_frame=None,
834812
svario=True,
835813
tol=None,
836-
invert_fold_norm = False,
814+
invert_fold_norm=False,
837815
**kwargs,
838816
):
839817
"""
@@ -873,8 +851,10 @@ def create_and_add_folded_foliation(
873851
fold_frame = self.features[-1]
874852
assert type(fold_frame) == FoldFrame, "Please specify a FoldFrame"
875853

876-
fold = FoldEvent(fold_frame, name=f"Fold_{foliation_data}", invert_norm=invert_fold_norm)
877-
854+
fold = FoldEvent(
855+
fold_frame, name=f"Fold_{foliation_data}", invert_norm=invert_fold_norm
856+
)
857+
878858
if "fold_weights" not in kwargs:
879859
kwargs["fold_weights"] = {}
880860
if interpolatortype != "DFI":
@@ -1342,6 +1322,9 @@ def create_and_add_fault(
13421322
force_mesh_geometry: bool = False,
13431323
points: bool = False,
13441324
fault_buffer=0.2,
1325+
fault_trace_anisotropy=1.0,
1326+
fault_dip=90,
1327+
fault_dip_anisotropy=1.0,
13451328
**kwargs,
13461329
):
13471330
"""
@@ -1443,6 +1426,9 @@ def create_and_add_fault(
14431426
points=points,
14441427
force_mesh_geometry=force_mesh_geometry,
14451428
fault_buffer=fault_buffer,
1429+
fault_trace_anisotropy=fault_trace_anisotropy,
1430+
fault_dip=fault_dip,
1431+
fault_dip_anisotropy=fault_dip_anisotropy,
14461432
)
14471433
if "force_mesh_geometry" not in kwargs:
14481434
fault_frame_builder.set_mesh_geometry(kwargs.get("fault_buffer", 0.2), 0)

LoopStructural/modelling/features/builders/_fault_builder.py

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Union
22
from ._structural_frame_builder import StructuralFrameBuilder
3+
from .. import AnalyticalGeologicalFeature
34
from LoopStructural.utils import get_vectors
45
import numpy as np
56
import pandas as pd
@@ -90,6 +91,9 @@ def create_data_from_geometry(
9091
points=False,
9192
force_mesh_geometry=False,
9293
fault_buffer=0.2,
94+
fault_trace_anisotropy=1.0,
95+
fault_dip=90,
96+
fault_dip_anisotropy=1.0,
9397
):
9498
"""Generate the required data for building a fault frame for a fault with the
9599
specified parameters
@@ -140,12 +144,34 @@ def create_data_from_geometry(
140144
fault_frame_data.loc[normal_mask, ["nx", "ny", "nz"]].to_numpy(),
141145
]
142146
)
147+
143148
if len(vector_data) == 0:
144149
logger.error(
145150
"You cannot model a fault without defining the orientation of the fault\n\
146-
Either add orientation data or define the fault normal vector argument"
151+
Defaulting to a vertical fault"
152+
)
153+
coefficients = np.polyfit(
154+
fault_frame_data.loc[trace_mask, "X"],
155+
fault_frame_data.loc[trace_mask, "Y"],
156+
1,
147157
)
148-
raise ValueError(f"There are no points on the fault trace")
158+
slope, intercept = coefficients
159+
160+
# Create a direction vector using the slope
161+
direction_vector = np.array([1, slope])
162+
direction_vector /= np.linalg.norm(direction_vector)
163+
print(f"Fault dip: {fault_dip}")
164+
vector_data = np.array(
165+
[
166+
[
167+
direction_vector[1],
168+
-direction_vector[0],
169+
np.sin(np.deg2rad(fault_dip)),
170+
]
171+
]
172+
)
173+
vector_data /= np.linalg.norm(vector_data, axis=1)[:, None]
174+
149175
fault_normal_vector = np.mean(vector_data, axis=0)
150176

151177
logger.info(f"Fault normal vector: {fault_normal_vector}")
@@ -283,9 +309,9 @@ def create_data_from_geometry(
283309
fault_frame_data["coord"] == 0,
284310
~np.isnan(fault_frame_data["nx"]),
285311
)
286-
fault_frame_data.loc[
287-
mask, ["gx", "gy", "gz"]
288-
] = fault_frame_data.loc[mask, ["nx", "ny", "nz"]]
312+
fault_frame_data.loc[mask, ["gx", "gy", "gz"]] = (
313+
fault_frame_data.loc[mask, ["nx", "ny", "nz"]]
314+
)
289315

290316
fault_frame_data.loc[mask, ["nx", "ny", "nz"]] = np.nan
291317
mask = np.logical_and(
@@ -411,7 +437,37 @@ def create_data_from_geometry(
411437
1,
412438
w,
413439
]
440+
fault_frame_data.loc[
441+
len(fault_frame_data),
442+
[
443+
"X",
444+
"Y",
445+
"Z",
446+
"feature_name",
447+
"nx",
448+
"ny",
449+
"nz",
450+
"val",
451+
"coord",
452+
"w",
453+
],
454+
] = [
455+
fault_center[0],
456+
fault_center[1],
457+
fault_center[2],
458+
self.name,
459+
fault_normal_vector[0],
460+
fault_normal_vector[1],
461+
fault_normal_vector[2],
462+
np.nan,
463+
0,
464+
w,
465+
]
414466
self.add_data_from_data_frame(fault_frame_data)
467+
if fault_trace_anisotropy > 0:
468+
self.add_fault_trace_anisotropy(fault_trace_anisotropy)
469+
if fault_dip_anisotropy > 0:
470+
self.add_fault_dip_anisotropy(fault_dip_anisotropy)
415471
if force_mesh_geometry:
416472
self.set_mesh_geometry(fault_buffer, None)
417473
self.update_geometry(fault_frame_data[["X", "Y", "Z"]].to_numpy())
@@ -425,7 +481,6 @@ def set_mesh_geometry(self, buffer, rotation):
425481
percentage of length to add to edges
426482
"""
427483
length = np.max(self.maximum - self.origin)
428-
print(self.origin, length * buffer, self.maximum)
429484
# for builder in self.builders:
430485
# all three coordinates share the same support
431486
self.builders[0].set_interpolation_geometry(
@@ -464,6 +519,50 @@ def splayregion(xyz):
464519
self.builders[0].add_equality_constraints(splay, splayregion, scalefactor)
465520
return splayregion
466521

522+
def add_fault_trace_anisotropy(self, w=1.0):
523+
trace_data = self.builders[0].data.loc[self.builders[0].data["val"] == 0, :]
524+
coefficients = np.polyfit(
525+
trace_data["X"],
526+
trace_data["Y"],
527+
1,
528+
)
529+
slope, intercept = coefficients
530+
531+
# Create a direction vector using the slope
532+
direction_vector = np.array([1, slope])
533+
direction_vector /= np.linalg.norm(direction_vector)
534+
vector_data = np.array([[direction_vector[1], -direction_vector[0], 0]])
535+
anisotropy_feature = AnalyticalGeologicalFeature(
536+
vector=vector_data, origin=[0, 0, 0], name="fault_trace_anisotropy"
537+
)
538+
self.builders[0].add_orthogonal_feature(
539+
anisotropy_feature, w=w, region=None, step=1, B=0
540+
)
541+
542+
def add_fault_dip_anisotropy(self, dip, w=1.0):
543+
trace_data = self.builders[0].data.loc[self.builders[0].data["val"] == 0, :]
544+
coefficients = np.polyfit(
545+
trace_data["X"],
546+
trace_data["Y"],
547+
1,
548+
)
549+
slope, intercept = coefficients
550+
551+
# Create a direction vector using the slope
552+
direction_vector = np.array([1, slope])
553+
direction_vector /= np.linalg.norm(direction_vector)
554+
vector_data = np.array([[direction_vector[0], direction_vector[1], 0]])
555+
556+
vector_data = np.array(
557+
[[direction_vector[1], -direction_vector[0], np.sin(np.deg2rad(dip))]]
558+
)
559+
anisotropy_feature = AnalyticalGeologicalFeature(
560+
vector=vector_data, origin=[0, 0, 0], name="fault_dip_anisotropy"
561+
)
562+
self.builders[0].add_orthogonal_feature(
563+
anisotropy_feature, w=w, region=None, step=1, B=0
564+
)
565+
467566
def update(self):
468567
for i in range(3):
469568
self.builders[i].update()

0 commit comments

Comments
 (0)