LoopStructural 1.6.1__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.
Potentially problematic release.
This version of LoopStructural might be problematic. Click here for more details.
- LoopStructural/__init__.py +52 -0
- LoopStructural/datasets/__init__.py +23 -0
- LoopStructural/datasets/_base.py +301 -0
- LoopStructural/datasets/_example_models.py +10 -0
- LoopStructural/datasets/data/claudius.csv +21049 -0
- LoopStructural/datasets/data/claudiusbb.txt +2 -0
- LoopStructural/datasets/data/duplex.csv +126 -0
- LoopStructural/datasets/data/duplexbb.txt +2 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.cpg +1 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.dbf +0 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.prj +1 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.shp +0 -0
- LoopStructural/datasets/data/fault_trace/fault_trace.shx +0 -0
- LoopStructural/datasets/data/geological_map_data/bbox.csv +2 -0
- LoopStructural/datasets/data/geological_map_data/contacts.csv +657 -0
- LoopStructural/datasets/data/geological_map_data/fault_displacement.csv +7 -0
- LoopStructural/datasets/data/geological_map_data/fault_edges.txt +2 -0
- LoopStructural/datasets/data/geological_map_data/fault_locations.csv +79 -0
- LoopStructural/datasets/data/geological_map_data/fault_orientations.csv +19 -0
- LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv +13 -0
- LoopStructural/datasets/data/geological_map_data/stratigraphic_orientations.csv +207 -0
- LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv +13 -0
- LoopStructural/datasets/data/intrusion.csv +1017 -0
- LoopStructural/datasets/data/intrusionbb.txt +2 -0
- LoopStructural/datasets/data/onefoldbb.txt +2 -0
- LoopStructural/datasets/data/onefolddata.csv +2226 -0
- LoopStructural/datasets/data/refolded_bb.txt +2 -0
- LoopStructural/datasets/data/refolded_fold.csv +205 -0
- LoopStructural/datasets/data/tabular_intrusion.csv +23 -0
- LoopStructural/datatypes/__init__.py +4 -0
- LoopStructural/datatypes/_bounding_box.py +422 -0
- LoopStructural/datatypes/_point.py +166 -0
- LoopStructural/datatypes/_structured_grid.py +94 -0
- LoopStructural/datatypes/_surface.py +184 -0
- LoopStructural/export/exporters.py +554 -0
- LoopStructural/export/file_formats.py +15 -0
- LoopStructural/export/geoh5.py +100 -0
- LoopStructural/export/gocad.py +126 -0
- LoopStructural/export/omf_wrapper.py +88 -0
- LoopStructural/interpolators/__init__.py +105 -0
- LoopStructural/interpolators/_api.py +143 -0
- LoopStructural/interpolators/_builders.py +149 -0
- LoopStructural/interpolators/_cython/__init__.py +0 -0
- LoopStructural/interpolators/_discrete_fold_interpolator.py +183 -0
- LoopStructural/interpolators/_discrete_interpolator.py +692 -0
- LoopStructural/interpolators/_finite_difference_interpolator.py +470 -0
- LoopStructural/interpolators/_geological_interpolator.py +380 -0
- LoopStructural/interpolators/_interpolator_factory.py +89 -0
- LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
- LoopStructural/interpolators/_operator.py +38 -0
- LoopStructural/interpolators/_p1interpolator.py +228 -0
- LoopStructural/interpolators/_p2interpolator.py +277 -0
- LoopStructural/interpolators/_surfe_wrapper.py +174 -0
- LoopStructural/interpolators/supports/_2d_base_unstructured.py +340 -0
- LoopStructural/interpolators/supports/_2d_p1_unstructured.py +68 -0
- LoopStructural/interpolators/supports/_2d_p2_unstructured.py +288 -0
- LoopStructural/interpolators/supports/_2d_structured_grid.py +462 -0
- LoopStructural/interpolators/supports/_2d_structured_tetra.py +0 -0
- LoopStructural/interpolators/supports/_3d_base_structured.py +467 -0
- LoopStructural/interpolators/supports/_3d_p2_tetra.py +331 -0
- LoopStructural/interpolators/supports/_3d_structured_grid.py +470 -0
- LoopStructural/interpolators/supports/_3d_structured_tetra.py +746 -0
- LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +637 -0
- LoopStructural/interpolators/supports/__init__.py +55 -0
- LoopStructural/interpolators/supports/_aabb.py +77 -0
- LoopStructural/interpolators/supports/_base_support.py +114 -0
- LoopStructural/interpolators/supports/_face_table.py +70 -0
- LoopStructural/interpolators/supports/_support_factory.py +32 -0
- LoopStructural/modelling/__init__.py +29 -0
- LoopStructural/modelling/core/__init__.py +0 -0
- LoopStructural/modelling/core/geological_model.py +1867 -0
- LoopStructural/modelling/features/__init__.py +32 -0
- LoopStructural/modelling/features/_analytical_feature.py +79 -0
- LoopStructural/modelling/features/_base_geological_feature.py +364 -0
- LoopStructural/modelling/features/_cross_product_geological_feature.py +100 -0
- LoopStructural/modelling/features/_geological_feature.py +288 -0
- LoopStructural/modelling/features/_lambda_geological_feature.py +93 -0
- LoopStructural/modelling/features/_region.py +18 -0
- LoopStructural/modelling/features/_structural_frame.py +186 -0
- LoopStructural/modelling/features/_unconformity_feature.py +83 -0
- LoopStructural/modelling/features/builders/__init__.py +5 -0
- LoopStructural/modelling/features/builders/_base_builder.py +111 -0
- LoopStructural/modelling/features/builders/_fault_builder.py +590 -0
- LoopStructural/modelling/features/builders/_folded_feature_builder.py +129 -0
- LoopStructural/modelling/features/builders/_geological_feature_builder.py +543 -0
- LoopStructural/modelling/features/builders/_structural_frame_builder.py +237 -0
- LoopStructural/modelling/features/fault/__init__.py +3 -0
- LoopStructural/modelling/features/fault/_fault_function.py +444 -0
- LoopStructural/modelling/features/fault/_fault_function_feature.py +82 -0
- LoopStructural/modelling/features/fault/_fault_segment.py +505 -0
- LoopStructural/modelling/features/fold/__init__.py +9 -0
- LoopStructural/modelling/features/fold/_fold.py +167 -0
- LoopStructural/modelling/features/fold/_fold_rotation_angle.py +149 -0
- LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +67 -0
- LoopStructural/modelling/features/fold/_foldframe.py +194 -0
- LoopStructural/modelling/features/fold/_svariogram.py +188 -0
- LoopStructural/modelling/input/__init__.py +2 -0
- LoopStructural/modelling/input/fault_network.py +80 -0
- LoopStructural/modelling/input/map2loop_processor.py +165 -0
- LoopStructural/modelling/input/process_data.py +650 -0
- LoopStructural/modelling/input/project_file.py +84 -0
- LoopStructural/modelling/intrusions/__init__.py +25 -0
- LoopStructural/modelling/intrusions/geom_conceptual_models.py +142 -0
- LoopStructural/modelling/intrusions/geometric_scaling_functions.py +123 -0
- LoopStructural/modelling/intrusions/intrusion_builder.py +672 -0
- LoopStructural/modelling/intrusions/intrusion_feature.py +410 -0
- LoopStructural/modelling/intrusions/intrusion_frame_builder.py +971 -0
- LoopStructural/modelling/intrusions/intrusion_support_functions.py +460 -0
- LoopStructural/utils/__init__.py +38 -0
- LoopStructural/utils/_surface.py +143 -0
- LoopStructural/utils/_transformation.py +76 -0
- LoopStructural/utils/config.py +18 -0
- LoopStructural/utils/dtm_creator.py +17 -0
- LoopStructural/utils/exceptions.py +31 -0
- LoopStructural/utils/helper.py +292 -0
- LoopStructural/utils/json_encoder.py +18 -0
- LoopStructural/utils/linalg.py +8 -0
- LoopStructural/utils/logging.py +79 -0
- LoopStructural/utils/maths.py +245 -0
- LoopStructural/utils/regions.py +103 -0
- LoopStructural/utils/typing.py +7 -0
- LoopStructural/utils/utils.py +68 -0
- LoopStructural/version.py +1 -0
- LoopStructural/visualisation/__init__.py +11 -0
- LoopStructural-1.6.1.dist-info/LICENSE +21 -0
- LoopStructural-1.6.1.dist-info/METADATA +81 -0
- LoopStructural-1.6.1.dist-info/RECORD +129 -0
- LoopStructural-1.6.1.dist-info/WHEEL +5 -0
- LoopStructural-1.6.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1867 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entry point for creating a geological model
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from ...utils import getLogger, log_to_file
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from typing import List
|
|
10
|
+
import pathlib
|
|
11
|
+
from ...modelling.features.fault import FaultSegment
|
|
12
|
+
|
|
13
|
+
from ...modelling.features.builders import (
|
|
14
|
+
FaultBuilder,
|
|
15
|
+
GeologicalFeatureBuilder,
|
|
16
|
+
StructuralFrameBuilder,
|
|
17
|
+
FoldedFeatureBuilder,
|
|
18
|
+
)
|
|
19
|
+
from ...modelling.features import (
|
|
20
|
+
UnconformityFeature,
|
|
21
|
+
StructuralFrame,
|
|
22
|
+
GeologicalFeature,
|
|
23
|
+
FeatureType,
|
|
24
|
+
)
|
|
25
|
+
from ...modelling.features.fold import (
|
|
26
|
+
FoldEvent,
|
|
27
|
+
FoldFrame,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from ...utils.helper import (
|
|
31
|
+
all_heading,
|
|
32
|
+
gradient_vec_names,
|
|
33
|
+
)
|
|
34
|
+
from ...utils import strikedip2vector
|
|
35
|
+
from ...datatypes import BoundingBox
|
|
36
|
+
|
|
37
|
+
from ...modelling.intrusions import IntrusionBuilder
|
|
38
|
+
|
|
39
|
+
from ...modelling.intrusions import IntrusionFrameBuilder
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
logger = getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GeologicalModel:
|
|
46
|
+
"""
|
|
47
|
+
A geological model is the recipe for building a 3D model and can include
|
|
48
|
+
the rescaling of the model between 0 and 1.
|
|
49
|
+
|
|
50
|
+
Attributes
|
|
51
|
+
----------
|
|
52
|
+
features : list
|
|
53
|
+
Contains all features youngest to oldest
|
|
54
|
+
feature_name_index : dict
|
|
55
|
+
maps feature name to the list index of the features
|
|
56
|
+
data : pandas dataframe
|
|
57
|
+
the dataframe used for building the geological model
|
|
58
|
+
nsteps : tuple/np.array(3,dtype=int)
|
|
59
|
+
the number of steps x,y,z to evaluate the model
|
|
60
|
+
origin : tuple/np.array(3,dtype=doubles)
|
|
61
|
+
the origin of the model box
|
|
62
|
+
parameters : dict
|
|
63
|
+
a dictionary tracking the parameters used to build the model
|
|
64
|
+
scale_factor : double
|
|
65
|
+
the scale factor used to rescale the model
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
origin: np.ndarray,
|
|
73
|
+
maximum: np.ndarray,
|
|
74
|
+
data=None,
|
|
75
|
+
nsteps=(50, 50, 25),
|
|
76
|
+
reuse_supports=False,
|
|
77
|
+
logfile=None,
|
|
78
|
+
loglevel="info",
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
origin : numpy array
|
|
84
|
+
specifying the origin of the model
|
|
85
|
+
maximum : numpy array
|
|
86
|
+
specifying the maximum extent of the model
|
|
87
|
+
rescale : bool
|
|
88
|
+
whether to rescale the model to between 0/1
|
|
89
|
+
epsion : float
|
|
90
|
+
a fudge factor for isosurfacing, used to make sure surfaces appear
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
Demo data
|
|
94
|
+
|
|
95
|
+
>>> from LoopStructural.datasets import load_claudius
|
|
96
|
+
>>> from LoopStructural import GeologicalModel
|
|
97
|
+
|
|
98
|
+
>>> data, bb = load_claudius()
|
|
99
|
+
|
|
100
|
+
>>> model = GeologicalModel(bb[:,0],bb[:,1]
|
|
101
|
+
>>> model.set_model_data(data)
|
|
102
|
+
>>> model.create_and_add_foliation('strati')
|
|
103
|
+
|
|
104
|
+
>>> y = np.linspace(model.bounding_box[0, 1], model.bounding_box[1, 1],
|
|
105
|
+
nsteps[1])
|
|
106
|
+
>>> z = np.linspace(model.bounding_box[1, 2], model.bounding_box[0, 2],
|
|
107
|
+
nsteps[2])
|
|
108
|
+
>>> xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
|
|
109
|
+
>>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
|
|
110
|
+
>>> model.evaluate_feature_value('strati',xyz,scale=False)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
if logfile:
|
|
115
|
+
self.logfile = logfile
|
|
116
|
+
log_to_file(logfile, level=loglevel)
|
|
117
|
+
|
|
118
|
+
logger.info("Initialising geological model")
|
|
119
|
+
self.features = []
|
|
120
|
+
self.feature_name_index = {}
|
|
121
|
+
self._data = pd.DataFrame() # None
|
|
122
|
+
if data is not None:
|
|
123
|
+
self.data = data
|
|
124
|
+
self.nsteps = nsteps
|
|
125
|
+
|
|
126
|
+
# we want to rescale the model area so that the maximum length is
|
|
127
|
+
# 1
|
|
128
|
+
self.origin = np.array(origin).astype(float)
|
|
129
|
+
originstr = f"Model origin: {self.origin[0]} {self.origin[1]} {self.origin[2]}"
|
|
130
|
+
logger.info(originstr)
|
|
131
|
+
self.maximum = np.array(maximum).astype(float)
|
|
132
|
+
maximumstr = "Model maximum: {} {} {}".format(
|
|
133
|
+
self.maximum[0], self.maximum[1], self.maximum[2]
|
|
134
|
+
)
|
|
135
|
+
logger.info(maximumstr)
|
|
136
|
+
|
|
137
|
+
self.scale_factor = 1.0
|
|
138
|
+
|
|
139
|
+
self.bounding_box = BoundingBox(
|
|
140
|
+
dimensions=3,
|
|
141
|
+
origin=np.zeros(3),
|
|
142
|
+
maximum=self.maximum - self.origin,
|
|
143
|
+
global_origin=self.origin,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.stratigraphic_column = None
|
|
147
|
+
|
|
148
|
+
self.tol = 1e-10 * np.max(self.bounding_box.maximum - self.bounding_box.origin)
|
|
149
|
+
self._dtm = None
|
|
150
|
+
|
|
151
|
+
def to_dict(self):
|
|
152
|
+
"""
|
|
153
|
+
Convert the geological model to a json string
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
json : str
|
|
158
|
+
json string of the geological model
|
|
159
|
+
"""
|
|
160
|
+
json = {}
|
|
161
|
+
json["model"] = {}
|
|
162
|
+
json["model"]["features"] = [f.name for f in self.features]
|
|
163
|
+
# json["model"]["data"] = self.data.to_json()
|
|
164
|
+
# json["model"]["origin"] = self.origin.tolist()
|
|
165
|
+
# json["model"]["maximum"] = self.maximum.tolist()
|
|
166
|
+
# json["model"]["nsteps"] = self.nsteps
|
|
167
|
+
json["model"]["stratigraphic_column"] = self.stratigraphic_column
|
|
168
|
+
# json["features"] = [f.to_json() for f in self.features]
|
|
169
|
+
return json
|
|
170
|
+
|
|
171
|
+
# @classmethod
|
|
172
|
+
# def from_json(cls,json):
|
|
173
|
+
# """
|
|
174
|
+
# Create a geological model from a json string
|
|
175
|
+
|
|
176
|
+
# Parameters
|
|
177
|
+
# ----------
|
|
178
|
+
# json : str
|
|
179
|
+
# json string of the geological model
|
|
180
|
+
|
|
181
|
+
# Returns
|
|
182
|
+
# -------
|
|
183
|
+
# model : GeologicalModel
|
|
184
|
+
# a geological model
|
|
185
|
+
# """
|
|
186
|
+
# model = cls(json["model"]["origin"],json["model"]["maximum"],data=None)
|
|
187
|
+
# model.stratigraphic_column = json["model"]["stratigraphic_column"]
|
|
188
|
+
# model.nsteps = json["model"]["nsteps"]
|
|
189
|
+
# model.data = pd.read_json(json["model"]["data"])
|
|
190
|
+
# model.features = []
|
|
191
|
+
# for feature in json["features"]:
|
|
192
|
+
# model.features.append(GeologicalFeature.from_json(feature,model))
|
|
193
|
+
# return model
|
|
194
|
+
def __str__(self):
|
|
195
|
+
lengths = self.maximum - self.origin
|
|
196
|
+
_str = "GeologicalModel - {} x {} x {}\n".format(*lengths)
|
|
197
|
+
_str += "------------------------------------------ \n"
|
|
198
|
+
_str += "The model contains {} GeologicalFeatures \n".format(len(self.features))
|
|
199
|
+
_str += ""
|
|
200
|
+
_str += "------------------------------------------ \n"
|
|
201
|
+
_str += ""
|
|
202
|
+
_str += "Model origin: {} {} {}\n".format(self.origin[0], self.origin[1], self.origin[2])
|
|
203
|
+
_str += "Model maximum: {} {} {}\n".format(
|
|
204
|
+
self.maximum[0], self.maximum[1], self.maximum[2]
|
|
205
|
+
)
|
|
206
|
+
_str += "Model rescale factor: {} \n".format(self.scale_factor)
|
|
207
|
+
_str += "------------------------------------------ \n"
|
|
208
|
+
_str += "Feature list: \n"
|
|
209
|
+
for feature in self.features:
|
|
210
|
+
_str += " {} \n".format(feature.name)
|
|
211
|
+
return _str
|
|
212
|
+
|
|
213
|
+
def _ipython_key_completions_(self):
|
|
214
|
+
return self.feature_name_index.keys()
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def from_map2loop_directory(
|
|
218
|
+
cls,
|
|
219
|
+
m2l_directory,
|
|
220
|
+
foliation_params={},
|
|
221
|
+
fault_params={},
|
|
222
|
+
use_thickness=True,
|
|
223
|
+
vector_scale=1,
|
|
224
|
+
gradient=False,
|
|
225
|
+
**kwargs,
|
|
226
|
+
):
|
|
227
|
+
"""Alternate constructor for a geological model using m2l output
|
|
228
|
+
|
|
229
|
+
Uses the information saved in the map2loop files to build a geological model.
|
|
230
|
+
You can specify kwargs for building foliation using foliation_params and for
|
|
231
|
+
faults using fault_params. faults is a flag that allows for the faults to be
|
|
232
|
+
skipped.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
m2l_directory : string
|
|
237
|
+
path to map2loop directory
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
(GeologicalModel, dict)
|
|
242
|
+
the created geological model and a dictionary of the map2loop data
|
|
243
|
+
|
|
244
|
+
Notes
|
|
245
|
+
------
|
|
246
|
+
For additional information see :class:`LoopStructural.modelling.input.Map2LoopProcessor`
|
|
247
|
+
and :meth:`LoopStructural.GeologicalModel.from_processor`
|
|
248
|
+
"""
|
|
249
|
+
from LoopStructural.modelling.input.map2loop_processor import Map2LoopProcessor
|
|
250
|
+
|
|
251
|
+
log_to_file(f"{m2l_directory}/loopstructural_log.txt")
|
|
252
|
+
logger.info("Creating model from m2l directory")
|
|
253
|
+
processor = Map2LoopProcessor(m2l_directory, use_thickness)
|
|
254
|
+
processor._gradient = gradient
|
|
255
|
+
processor.vector_scale = vector_scale
|
|
256
|
+
for foliation_name in processor.stratigraphic_column.keys():
|
|
257
|
+
if foliation_name != "faults":
|
|
258
|
+
if foliation_name in foliation_params.keys():
|
|
259
|
+
processor.foliation_properties[foliation_name] = foliation_params[
|
|
260
|
+
foliation_name
|
|
261
|
+
]
|
|
262
|
+
else:
|
|
263
|
+
processor.foliation_properties[foliation_name] = foliation_params
|
|
264
|
+
|
|
265
|
+
for fault_name in processor.fault_names:
|
|
266
|
+
if fault_name in fault_params.keys():
|
|
267
|
+
for param_name, value in fault_params[fault_name].items():
|
|
268
|
+
processor.fault_properties.loc[fault_name, param_name] = value
|
|
269
|
+
else:
|
|
270
|
+
for param_name, value in fault_params.items():
|
|
271
|
+
processor.fault_properties.loc[fault_name, param_name] = value
|
|
272
|
+
|
|
273
|
+
model = GeologicalModel.from_processor(processor)
|
|
274
|
+
return model, processor
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def from_processor(cls, processor):
|
|
278
|
+
"""Builds a model from a :class:`LoopStructural.modelling.input.ProcessInputData` object
|
|
279
|
+
This object stores the observations and order of the geological features
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
processor : ProcessInputData
|
|
284
|
+
any type of ProcessInputData
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
GeologicalModel
|
|
289
|
+
a model with all of the features, need to call model.update() to run interpolation
|
|
290
|
+
"""
|
|
291
|
+
logger.info("Creating model from processor")
|
|
292
|
+
model = GeologicalModel(processor.origin, processor.maximum)
|
|
293
|
+
model.data = processor.data
|
|
294
|
+
if processor.fault_properties is not None:
|
|
295
|
+
for i in processor.fault_network.faults:
|
|
296
|
+
model.create_and_add_fault(
|
|
297
|
+
i,
|
|
298
|
+
**processor.fault_properties.to_dict("index")[i],
|
|
299
|
+
faultfunction="BaseFault",
|
|
300
|
+
)
|
|
301
|
+
for (
|
|
302
|
+
edge,
|
|
303
|
+
properties,
|
|
304
|
+
) in processor.fault_network.fault_edge_properties.items():
|
|
305
|
+
if model[edge[1]] is None or model[edge[0]] is None:
|
|
306
|
+
logger.warning(f"Cannot add splay {edge[1]} or {edge[0]} are not in the model")
|
|
307
|
+
continue
|
|
308
|
+
splay = False
|
|
309
|
+
if "angle" in properties:
|
|
310
|
+
if float(properties["angle"]) < 30 and (
|
|
311
|
+
"dip_dir" not in processor.stratigraphic_column["faults"][edge[0]]
|
|
312
|
+
or np.abs(
|
|
313
|
+
processor.stratigraphic_column["faults"][edge[0]]["dip_dir"]
|
|
314
|
+
- processor.stratigraphic_column["faults"][edge[1]]["dip_dir"]
|
|
315
|
+
)
|
|
316
|
+
< 90
|
|
317
|
+
):
|
|
318
|
+
# splay
|
|
319
|
+
region = model[edge[1]].builder.add_splay(model[edge[0]])
|
|
320
|
+
|
|
321
|
+
model[edge[1]].splay[model[edge[0]].name] = region
|
|
322
|
+
splay = True
|
|
323
|
+
if splay is False:
|
|
324
|
+
positive = None
|
|
325
|
+
if "downthrow_dir" in processor.stratigraphic_column["faults"][edge[0]]:
|
|
326
|
+
positive = (
|
|
327
|
+
np.abs(
|
|
328
|
+
processor.stratigraphic_column["faults"][edge[0]]["downthrow_dir"]
|
|
329
|
+
- processor.stratigraphic_column["faults"][edge[1]]["downthrow_dir"]
|
|
330
|
+
)
|
|
331
|
+
< 90
|
|
332
|
+
)
|
|
333
|
+
model[edge[1]].add_abutting_fault(
|
|
334
|
+
model[edge[0]],
|
|
335
|
+
positive=positive,
|
|
336
|
+
)
|
|
337
|
+
for s in processor.stratigraphic_column.keys():
|
|
338
|
+
if s != "faults":
|
|
339
|
+
faults = None
|
|
340
|
+
if processor.fault_stratigraphy is not None:
|
|
341
|
+
faults = processor.fault_stratigraphy[s]
|
|
342
|
+
logger.info(f"Adding foliation {s}")
|
|
343
|
+
f = model.create_and_add_foliation(
|
|
344
|
+
s, **processor.foliation_properties[s], faults=faults
|
|
345
|
+
)
|
|
346
|
+
if not f:
|
|
347
|
+
logger.warning(f"Foliation {s} not added")
|
|
348
|
+
# check feature was built, and is an interpolated feature.
|
|
349
|
+
if f is not None and f.type == FeatureType.INTERPOLATED:
|
|
350
|
+
model.add_unconformity(f, 0)
|
|
351
|
+
model.stratigraphic_column = processor.stratigraphic_column
|
|
352
|
+
return model
|
|
353
|
+
|
|
354
|
+
@classmethod
|
|
355
|
+
def from_file(cls, file):
|
|
356
|
+
"""Load a geological model from file
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
file : string
|
|
361
|
+
path to the file
|
|
362
|
+
|
|
363
|
+
Returns
|
|
364
|
+
-------
|
|
365
|
+
GeologicalModel
|
|
366
|
+
the geological model object
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
import dill as pickle
|
|
370
|
+
except ImportError:
|
|
371
|
+
logger.error("Cannot import from file, dill not installed")
|
|
372
|
+
return None
|
|
373
|
+
model = pickle.load(open(file, "rb"))
|
|
374
|
+
if isinstance(model, GeologicalModel):
|
|
375
|
+
logger.info("GeologicalModel initialised from file")
|
|
376
|
+
return model
|
|
377
|
+
else:
|
|
378
|
+
logger.error(f"{file} does not contain a geological model")
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
def __getitem__(self, feature_name):
|
|
382
|
+
"""Accessor for feature in features using feature_name_index
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
feature_name : string
|
|
387
|
+
name of the feature to return
|
|
388
|
+
"""
|
|
389
|
+
return self.get_feature_by_name(feature_name)
|
|
390
|
+
|
|
391
|
+
def __contains__(self, feature_name):
|
|
392
|
+
return feature_name in self.feature_name_index
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def dtm(self):
|
|
396
|
+
return self._dtm
|
|
397
|
+
|
|
398
|
+
@dtm.setter
|
|
399
|
+
def dtm(self, dtm):
|
|
400
|
+
"""Set a dtm to the model.
|
|
401
|
+
The dtm is a function that can be called for dtm(xy) where xy is
|
|
402
|
+
a numpy array of xy locations. The function will return an array of
|
|
403
|
+
z values corresponding to the elevation at xy.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
dtm : callable
|
|
408
|
+
|
|
409
|
+
"""
|
|
410
|
+
if not callable(dtm):
|
|
411
|
+
raise BaseException("DTM must be a callable function \n")
|
|
412
|
+
else:
|
|
413
|
+
self._dtm = dtm
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def faults(self):
|
|
417
|
+
"""Get all of the fault features in the model
|
|
418
|
+
|
|
419
|
+
Returns
|
|
420
|
+
-------
|
|
421
|
+
list
|
|
422
|
+
a list of :class:`LoopStructural.modelling.features.FaultSegment`
|
|
423
|
+
"""
|
|
424
|
+
faults = []
|
|
425
|
+
for f in self.features:
|
|
426
|
+
if isinstance(f, FaultSegment):
|
|
427
|
+
faults.append(f)
|
|
428
|
+
|
|
429
|
+
return faults
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def series(self):
|
|
433
|
+
series = []
|
|
434
|
+
for f in self.features:
|
|
435
|
+
if f.type == FeatureType.INTERPOLATED:
|
|
436
|
+
series.append(f)
|
|
437
|
+
return series
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def intrusions(self):
|
|
441
|
+
intrusions = []
|
|
442
|
+
for f in self.features:
|
|
443
|
+
if f.type == "intrusion":
|
|
444
|
+
intrusions.append(f)
|
|
445
|
+
return intrusions
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def faults_displacement_magnitude(self):
|
|
449
|
+
displacements = []
|
|
450
|
+
for f in self.faults:
|
|
451
|
+
displacements.append(f.displacement)
|
|
452
|
+
return np.array(displacements)
|
|
453
|
+
|
|
454
|
+
def feature_names(self):
|
|
455
|
+
return self.feature_name_index.keys()
|
|
456
|
+
|
|
457
|
+
def fault_names(self):
|
|
458
|
+
"""Get name of all faults in the model
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
list
|
|
463
|
+
list of the names of the faults in the model
|
|
464
|
+
"""
|
|
465
|
+
return [f.name for f in self.faults]
|
|
466
|
+
|
|
467
|
+
def check_inialisation(self):
|
|
468
|
+
if self.data is None:
|
|
469
|
+
logger.error("Data not associated with GeologicalModel. Run set_data")
|
|
470
|
+
return False
|
|
471
|
+
if self.data.shape[0] > 0:
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
def to_file(self, file):
|
|
475
|
+
"""Save a model to a pickle file requires dill
|
|
476
|
+
|
|
477
|
+
Parameters
|
|
478
|
+
----------
|
|
479
|
+
file : string
|
|
480
|
+
path to file location
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
import dill as pickle
|
|
484
|
+
except ImportError:
|
|
485
|
+
logger.error("Cannot write to file, dill not installed \n" "pip install dill")
|
|
486
|
+
return
|
|
487
|
+
try:
|
|
488
|
+
logger.info(f"Writing GeologicalModel to: {file}")
|
|
489
|
+
pickle.dump(self, open(file, "wb"))
|
|
490
|
+
except pickle.PicklingError:
|
|
491
|
+
logger.error("Error saving file")
|
|
492
|
+
|
|
493
|
+
def _add_feature(self, feature):
|
|
494
|
+
"""
|
|
495
|
+
Add a feature to the model stack
|
|
496
|
+
|
|
497
|
+
Parameters
|
|
498
|
+
----------
|
|
499
|
+
feature : GeologicalFeature
|
|
500
|
+
the geological feature to add
|
|
501
|
+
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
if feature.name in self.feature_name_index:
|
|
505
|
+
logger.info(
|
|
506
|
+
f"Feature {feature.name} already exists at {self.feature_name_index[feature.name]}, overwriting"
|
|
507
|
+
)
|
|
508
|
+
self.features[self.feature_name_index[feature.name]] = feature
|
|
509
|
+
else:
|
|
510
|
+
self.features.append(feature)
|
|
511
|
+
self.feature_name_index[feature.name] = len(self.features) - 1
|
|
512
|
+
logger.info(f"Adding {feature.name} to model at location {len(self.features)}")
|
|
513
|
+
self._add_domain_fault_above(feature)
|
|
514
|
+
if feature.type == FeatureType.INTERPOLATED:
|
|
515
|
+
self._add_unconformity_above(feature)
|
|
516
|
+
feature.model = self
|
|
517
|
+
|
|
518
|
+
def data_for_feature(self, feature_name: str) -> pd.DataFrame:
|
|
519
|
+
"""Get all of the data associated with a geological feature
|
|
520
|
+
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
feature_name : str
|
|
524
|
+
the unique identifying name of the feature
|
|
525
|
+
|
|
526
|
+
Returns
|
|
527
|
+
-------
|
|
528
|
+
pd.DataFrame
|
|
529
|
+
data frame containing all of the data in the model associated with this feature
|
|
530
|
+
"""
|
|
531
|
+
return self.data.loc[self.data["feature_name"] == feature_name, :]
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
def data(self) -> pd.DataFrame:
|
|
535
|
+
return self._data
|
|
536
|
+
|
|
537
|
+
@data.setter
|
|
538
|
+
def data(self, data: pd.DataFrame):
|
|
539
|
+
"""
|
|
540
|
+
Set the data array for the model
|
|
541
|
+
|
|
542
|
+
Parameters
|
|
543
|
+
----------
|
|
544
|
+
data : pandas data frame
|
|
545
|
+
with column headers corresponding to the
|
|
546
|
+
type, X, Y, Z, nx, ny, nz, val, strike, dip, dip_dir, plunge,
|
|
547
|
+
plunge_dir, azimuth
|
|
548
|
+
|
|
549
|
+
Returns
|
|
550
|
+
-------
|
|
551
|
+
Note
|
|
552
|
+
----
|
|
553
|
+
Type can be any unique identifier for the feature the data point
|
|
554
|
+
'eg' 'S0', 'S2', 'F1_axis'
|
|
555
|
+
it is then used by the create functions to get the correct data
|
|
556
|
+
"""
|
|
557
|
+
if data is None:
|
|
558
|
+
return
|
|
559
|
+
if not issubclass(type(data), pd.DataFrame):
|
|
560
|
+
logger.warning("Data is not a pandas data frame, trying to read data frame " "from csv")
|
|
561
|
+
try:
|
|
562
|
+
data = pd.read_csv(data)
|
|
563
|
+
except:
|
|
564
|
+
logger.error("Could not load pandas data frame from data")
|
|
565
|
+
raise BaseException("Cannot load data")
|
|
566
|
+
logger.info(f"Adding data to GeologicalModel with {len(data)} data points")
|
|
567
|
+
self._data = data.copy()
|
|
568
|
+
|
|
569
|
+
self._data["X"] -= self.origin[0]
|
|
570
|
+
self._data["Y"] -= self.origin[1]
|
|
571
|
+
self._data["Z"] -= self.origin[2]
|
|
572
|
+
self._data["X"] /= self.scale_factor
|
|
573
|
+
self._data["Y"] /= self.scale_factor
|
|
574
|
+
self._data["Z"] /= self.scale_factor
|
|
575
|
+
if "type" in self._data:
|
|
576
|
+
logger.warning("'type' is deprecated replace with 'feature_name' \n")
|
|
577
|
+
self._data.rename(columns={"type": "feature_name"}, inplace=True)
|
|
578
|
+
if "feature_name" not in self._data:
|
|
579
|
+
logger.error("Data does not contain 'feature_name' column")
|
|
580
|
+
raise BaseException("Cannot load data")
|
|
581
|
+
for h in all_heading():
|
|
582
|
+
if h not in self._data:
|
|
583
|
+
self._data[h] = np.nan
|
|
584
|
+
if h == "w":
|
|
585
|
+
self._data[h] = 1.0
|
|
586
|
+
if h == "coord":
|
|
587
|
+
self._data[h] = 0
|
|
588
|
+
if h == "polarity":
|
|
589
|
+
self._data[h] = 1.0
|
|
590
|
+
# LS wants polarity as -1 or 1, change 0 to -1
|
|
591
|
+
self._data.loc[self._data["polarity"] == 0, "polarity"] = -1.0
|
|
592
|
+
self._data.loc[np.isnan(self._data["w"]), "w"] = 1.0
|
|
593
|
+
if "strike" in self._data and "dip" in self._data:
|
|
594
|
+
logger.info("Converting strike and dip to vectors")
|
|
595
|
+
mask = np.all(~np.isnan(self._data.loc[:, ["strike", "dip"]]), axis=1)
|
|
596
|
+
self._data.loc[mask, gradient_vec_names()] = (
|
|
597
|
+
strikedip2vector(self._data.loc[mask, "strike"], self._data.loc[mask, "dip"])
|
|
598
|
+
* self._data.loc[mask, "polarity"].to_numpy()[:, None]
|
|
599
|
+
)
|
|
600
|
+
self._data.drop(["strike", "dip"], axis=1, inplace=True)
|
|
601
|
+
self._data[['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']] = (
|
|
602
|
+
self._data[
|
|
603
|
+
['X', 'Y', 'Z', 'val', 'nx', 'ny', 'nz', 'gx', 'gy', 'gz', 'tx', 'ty', 'tz']
|
|
604
|
+
].astype(float)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
def set_model_data(self, data):
|
|
608
|
+
logger.warning("deprecated method. Model data can now be set using the data attribute")
|
|
609
|
+
self.data = data
|
|
610
|
+
|
|
611
|
+
def set_stratigraphic_column(self, stratigraphic_column, cmap="tab20"):
|
|
612
|
+
"""
|
|
613
|
+
Adds a stratigraphic column to the model
|
|
614
|
+
|
|
615
|
+
Parameters
|
|
616
|
+
----------
|
|
617
|
+
stratigraphic_column : dictionary
|
|
618
|
+
cmap : matplotlib.cmap
|
|
619
|
+
Returns
|
|
620
|
+
-------
|
|
621
|
+
|
|
622
|
+
Notes
|
|
623
|
+
-----
|
|
624
|
+
stratigraphic_column is a nested dictionary with the format
|
|
625
|
+
{'group':
|
|
626
|
+
{'series1':
|
|
627
|
+
{'min':0., 'max':10.,'id':0,'colour':}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
"""
|
|
632
|
+
# if the colour for a unit hasn't been specified we can just sample from
|
|
633
|
+
# a colour map e.g. tab20
|
|
634
|
+
logger.info("Adding stratigraphic column to model")
|
|
635
|
+
random_colour = True
|
|
636
|
+
n_units = 0
|
|
637
|
+
for g in stratigraphic_column.keys():
|
|
638
|
+
for u in stratigraphic_column[g].keys():
|
|
639
|
+
if "colour" in stratigraphic_column[g][u]:
|
|
640
|
+
random_colour = False
|
|
641
|
+
break
|
|
642
|
+
n_units += 1
|
|
643
|
+
if random_colour:
|
|
644
|
+
import matplotlib.cm as cm
|
|
645
|
+
|
|
646
|
+
cmap = cm.get_cmap(cmap, n_units)
|
|
647
|
+
cmap_colours = cmap.colors
|
|
648
|
+
ci = 0
|
|
649
|
+
for g in stratigraphic_column.keys():
|
|
650
|
+
for u in stratigraphic_column[g].keys():
|
|
651
|
+
stratigraphic_column[g][u]["colour"] = cmap_colours[ci, :]
|
|
652
|
+
|
|
653
|
+
self.stratigraphic_column = stratigraphic_column
|
|
654
|
+
|
|
655
|
+
def create_and_add_foliation(
|
|
656
|
+
self,
|
|
657
|
+
series_surface_data: str,
|
|
658
|
+
interpolatortype: str = "FDI",
|
|
659
|
+
nelements: int = 1000,
|
|
660
|
+
tol=None,
|
|
661
|
+
faults=None,
|
|
662
|
+
**kwargs,
|
|
663
|
+
):
|
|
664
|
+
"""
|
|
665
|
+
Parameters
|
|
666
|
+
----------
|
|
667
|
+
series_surface_data : string
|
|
668
|
+
corresponding to the feature_name in the data
|
|
669
|
+
kwargs
|
|
670
|
+
|
|
671
|
+
Returns
|
|
672
|
+
-------
|
|
673
|
+
feature : GeologicalFeature
|
|
674
|
+
the created geological feature
|
|
675
|
+
|
|
676
|
+
Notes
|
|
677
|
+
------
|
|
678
|
+
This function creates an instance of a
|
|
679
|
+
:class:`LoopStructural.modelling.features.builders.GeologicalFeatureBuilder` and will return
|
|
680
|
+
a :class:`LoopStructural.modelling.features.builders.GeologicalFeature`
|
|
681
|
+
The feature is not interpolated until either
|
|
682
|
+
:meth:`LoopStructural.modelling.features.builders.GeologicalFeature.evaluate_value` is called or
|
|
683
|
+
:meth:`LoopStructural.modelling.core.GeologicalModel.update`
|
|
684
|
+
|
|
685
|
+
An interpolator will be chosen by calling :meth:`LoopStructural.GeologicalModel.get_interpolator`
|
|
686
|
+
|
|
687
|
+
"""
|
|
688
|
+
if not self.check_inialisation():
|
|
689
|
+
logger.warning(f"{series_surface_data} not added, model not initialised")
|
|
690
|
+
return
|
|
691
|
+
# if tol is not specified use the model default
|
|
692
|
+
if tol is None:
|
|
693
|
+
tol = self.tol
|
|
694
|
+
|
|
695
|
+
series_builder = GeologicalFeatureBuilder(
|
|
696
|
+
bounding_box=self.bounding_box,
|
|
697
|
+
interpolatortype=interpolatortype,
|
|
698
|
+
nelements=nelements,
|
|
699
|
+
name=series_surface_data,
|
|
700
|
+
model=self,
|
|
701
|
+
**kwargs,
|
|
702
|
+
)
|
|
703
|
+
# add data
|
|
704
|
+
series_data = self.data[self.data["feature_name"] == series_surface_data]
|
|
705
|
+
if series_data.shape[0] == 0:
|
|
706
|
+
logger.warning("No data for {series_surface_data}, skipping")
|
|
707
|
+
return
|
|
708
|
+
series_builder.add_data_from_data_frame(series_data)
|
|
709
|
+
self._add_faults(series_builder, features=faults)
|
|
710
|
+
|
|
711
|
+
# build feature
|
|
712
|
+
# series_feature = series_builder.build(**kwargs)
|
|
713
|
+
series_feature = series_builder.feature
|
|
714
|
+
series_builder.build_arguments = kwargs
|
|
715
|
+
# this support is built for the entire model domain? Possibly would
|
|
716
|
+
# could just pass a regular grid of points - mask by any above unconformities??
|
|
717
|
+
series_builder.build_arguments['domain'] = True
|
|
718
|
+
series_builder.build_arguments["tol"] = tol
|
|
719
|
+
series_feature.type = FeatureType.INTERPOLATED
|
|
720
|
+
self._add_feature(series_feature)
|
|
721
|
+
return series_feature
|
|
722
|
+
|
|
723
|
+
def create_and_add_fold_frame(
|
|
724
|
+
self,
|
|
725
|
+
foldframe_data,
|
|
726
|
+
interpolatortype="FDI",
|
|
727
|
+
nelements=1000,
|
|
728
|
+
tol=None,
|
|
729
|
+
buffer=0.1,
|
|
730
|
+
**kwargs,
|
|
731
|
+
):
|
|
732
|
+
"""
|
|
733
|
+
Parameters
|
|
734
|
+
----------
|
|
735
|
+
foldframe_data : string
|
|
736
|
+
unique string in feature_name column
|
|
737
|
+
|
|
738
|
+
kwargs
|
|
739
|
+
|
|
740
|
+
Returns
|
|
741
|
+
-------
|
|
742
|
+
fold_frame : FoldFrame
|
|
743
|
+
the created fold frame
|
|
744
|
+
"""
|
|
745
|
+
if not self.check_inialisation():
|
|
746
|
+
return False
|
|
747
|
+
if tol is None:
|
|
748
|
+
tol = self.tol
|
|
749
|
+
|
|
750
|
+
# create fault frame
|
|
751
|
+
#
|
|
752
|
+
fold_frame_builder = StructuralFrameBuilder(
|
|
753
|
+
interpolatortype=interpolatortype,
|
|
754
|
+
bounding_box=self.bounding_box.with_buffer(buffer),
|
|
755
|
+
name=foldframe_data,
|
|
756
|
+
frame=FoldFrame,
|
|
757
|
+
nelements=nelements,
|
|
758
|
+
model=self,
|
|
759
|
+
**kwargs,
|
|
760
|
+
)
|
|
761
|
+
# add data
|
|
762
|
+
fold_frame_data = self.data[self.data["feature_name"] == foldframe_data]
|
|
763
|
+
fold_frame_builder.add_data_from_data_frame(fold_frame_data)
|
|
764
|
+
self._add_faults(fold_frame_builder[0])
|
|
765
|
+
self._add_faults(fold_frame_builder[1])
|
|
766
|
+
self._add_faults(fold_frame_builder[2])
|
|
767
|
+
kwargs["tol"] = tol
|
|
768
|
+
fold_frame_builder.setup(**kwargs)
|
|
769
|
+
fold_frame = fold_frame_builder.frame
|
|
770
|
+
|
|
771
|
+
fold_frame.type = FeatureType.STRUCTURALFRAME
|
|
772
|
+
fold_frame.builder = fold_frame_builder
|
|
773
|
+
self._add_feature(fold_frame)
|
|
774
|
+
|
|
775
|
+
return fold_frame
|
|
776
|
+
|
|
777
|
+
def create_and_add_folded_foliation(
|
|
778
|
+
self,
|
|
779
|
+
foliation_data,
|
|
780
|
+
interpolatortype="DFI",
|
|
781
|
+
nelements=10000,
|
|
782
|
+
buffer=0.1,
|
|
783
|
+
fold_frame=None,
|
|
784
|
+
svario=True,
|
|
785
|
+
tol=None,
|
|
786
|
+
invert_fold_norm=False,
|
|
787
|
+
**kwargs,
|
|
788
|
+
):
|
|
789
|
+
"""
|
|
790
|
+
Create a folded foliation field from data and a fold frame
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
foliation_data : str
|
|
795
|
+
unique string in type column of data frame
|
|
796
|
+
fold_frame : FoldFrame
|
|
797
|
+
svario : Boolean
|
|
798
|
+
whether to calculate svariograms, saves time if avoided
|
|
799
|
+
kwargs
|
|
800
|
+
additional kwargs to be passed through to other functions
|
|
801
|
+
|
|
802
|
+
Returns
|
|
803
|
+
-------
|
|
804
|
+
feature : GeologicalFeature
|
|
805
|
+
created geological feature
|
|
806
|
+
|
|
807
|
+
Notes
|
|
808
|
+
-----
|
|
809
|
+
|
|
810
|
+
- Building a folded foliation uses the fold interpolation code from Laurent et al., 2016
|
|
811
|
+
and fold profile fitting from Grose et al., 2017. For more information about the fold modelling
|
|
812
|
+
see :class:`LoopStructural.modelling.features.fold.FoldEvent`,
|
|
813
|
+
:class:`LoopStructural.modelling.features.builders.FoldedFeatureBuilder`
|
|
814
|
+
|
|
815
|
+
"""
|
|
816
|
+
if not self.check_inialisation():
|
|
817
|
+
return False
|
|
818
|
+
if tol is None:
|
|
819
|
+
tol = self.tol
|
|
820
|
+
|
|
821
|
+
if fold_frame is None:
|
|
822
|
+
logger.info("Using last feature as fold frame")
|
|
823
|
+
fold_frame = self.features[-1]
|
|
824
|
+
assert isinstance(fold_frame, FoldFrame), "Please specify a FoldFrame"
|
|
825
|
+
|
|
826
|
+
fold = FoldEvent(fold_frame, name=f"Fold_{foliation_data}", invert_norm=invert_fold_norm)
|
|
827
|
+
|
|
828
|
+
if "fold_weights" not in kwargs:
|
|
829
|
+
kwargs["fold_weights"] = {}
|
|
830
|
+
if interpolatortype != "DFI":
|
|
831
|
+
logger.warning("Folded foliation only supports DFI interpolator, changing to DFI")
|
|
832
|
+
interpolatortype = "DFI"
|
|
833
|
+
series_builder = FoldedFeatureBuilder(
|
|
834
|
+
interpolatortype=interpolatortype,
|
|
835
|
+
bounding_box=self.bounding_box.with_buffer(buffer),
|
|
836
|
+
nelements=nelements,
|
|
837
|
+
fold=fold,
|
|
838
|
+
name=foliation_data,
|
|
839
|
+
svario=svario,
|
|
840
|
+
model=self,
|
|
841
|
+
**kwargs,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
series_builder.add_data_from_data_frame(
|
|
845
|
+
self.data[self.data["feature_name"] == foliation_data]
|
|
846
|
+
)
|
|
847
|
+
self._add_faults(series_builder)
|
|
848
|
+
# series_builder.add_data_to_interpolator(True)
|
|
849
|
+
# build feature
|
|
850
|
+
|
|
851
|
+
kwargs["tol"] = tol
|
|
852
|
+
|
|
853
|
+
# series_feature = series_builder.build(**kwargs)
|
|
854
|
+
series_feature = series_builder.feature
|
|
855
|
+
series_builder.build_arguments = kwargs
|
|
856
|
+
series_feature.type = FeatureType.INTERPOLATED
|
|
857
|
+
series_feature.fold = fold
|
|
858
|
+
|
|
859
|
+
self._add_feature(series_feature)
|
|
860
|
+
return series_feature
|
|
861
|
+
|
|
862
|
+
def create_and_add_folded_fold_frame(
|
|
863
|
+
self,
|
|
864
|
+
fold_frame_data,
|
|
865
|
+
interpolatortype="FDI",
|
|
866
|
+
nelements=10000,
|
|
867
|
+
fold_frame=None,
|
|
868
|
+
tol=None,
|
|
869
|
+
**kwargs,
|
|
870
|
+
):
|
|
871
|
+
"""
|
|
872
|
+
|
|
873
|
+
Parameters
|
|
874
|
+
----------
|
|
875
|
+
fold_frame_data : string
|
|
876
|
+
name of the feature to be added
|
|
877
|
+
|
|
878
|
+
fold_frame : StructuralFrame, optional
|
|
879
|
+
the fold frame for the fold if not specified uses last feature added
|
|
880
|
+
|
|
881
|
+
kwargs : dict
|
|
882
|
+
parameters passed to child functions
|
|
883
|
+
|
|
884
|
+
Returns
|
|
885
|
+
-------
|
|
886
|
+
fold_frame : FoldFrame
|
|
887
|
+
created fold frame
|
|
888
|
+
|
|
889
|
+
Notes
|
|
890
|
+
-----
|
|
891
|
+
This function build a structural frame where the first coordinate is constrained
|
|
892
|
+
with a fold interpolator.
|
|
893
|
+
Keyword arguments can be included to constrain
|
|
894
|
+
|
|
895
|
+
- :meth:`LoopStructural.GeologicalModel.get_interpolator`
|
|
896
|
+
- :class:`LoopStructural.StructuralFrameBuilder`
|
|
897
|
+
- :meth:`LoopStructural.StructuralFrameBuilder.setup`
|
|
898
|
+
- Building a folded foliation uses the fold interpolation code from Laurent et al., 2016
|
|
899
|
+
and fold profile fitting from Grose et al., 2017. For more information about the fold modelling
|
|
900
|
+
see :class:`LoopStructural.modelling.features.fold.FoldEvent`,
|
|
901
|
+
:class:`LoopStructural.modelling.features.builders.FoldedFeatureBuilder`
|
|
902
|
+
"""
|
|
903
|
+
if not self.check_inialisation():
|
|
904
|
+
return False
|
|
905
|
+
if tol is None:
|
|
906
|
+
tol = self.tol
|
|
907
|
+
|
|
908
|
+
if fold_frame is None:
|
|
909
|
+
logger.info("Using last feature as fold frame")
|
|
910
|
+
fold_frame = self.features[-1]
|
|
911
|
+
assert isinstance(fold_frame, FoldFrame), "Please specify a FoldFrame"
|
|
912
|
+
fold = FoldEvent(fold_frame, name=f"Fold_{fold_frame_data}")
|
|
913
|
+
|
|
914
|
+
interpolatortypes = [
|
|
915
|
+
"DFI",
|
|
916
|
+
"FDI",
|
|
917
|
+
"FDI",
|
|
918
|
+
]
|
|
919
|
+
fold_frame_builder = StructuralFrameBuilder(
|
|
920
|
+
interpolatortype=interpolatortypes,
|
|
921
|
+
bounding_box=self.bounding_box.with_buffer(kwargs.get("buffer", 0.1)),
|
|
922
|
+
nelements=[nelements, nelements, nelements],
|
|
923
|
+
name=fold_frame_data,
|
|
924
|
+
fold=fold,
|
|
925
|
+
frame=FoldFrame,
|
|
926
|
+
model=self,
|
|
927
|
+
**kwargs,
|
|
928
|
+
)
|
|
929
|
+
fold_frame_builder.add_data_from_data_frame(
|
|
930
|
+
self.data[self.data["feature_name"] == fold_frame_data]
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
for i in range(3):
|
|
934
|
+
self._add_faults(fold_frame_builder[i])
|
|
935
|
+
# build feature
|
|
936
|
+
kwargs["frame"] = FoldFrame
|
|
937
|
+
kwargs["tol"] = tol
|
|
938
|
+
fold_frame_builder.setup(**kwargs)
|
|
939
|
+
# fold_frame_builder.build_arguments = kwargs
|
|
940
|
+
folded_fold_frame = fold_frame_builder.frame
|
|
941
|
+
folded_fold_frame.builder = fold_frame_builder
|
|
942
|
+
|
|
943
|
+
folded_fold_frame.type = FeatureType.STRUCTURALFRAME
|
|
944
|
+
|
|
945
|
+
self._add_feature(folded_fold_frame)
|
|
946
|
+
|
|
947
|
+
return folded_fold_frame
|
|
948
|
+
|
|
949
|
+
def create_and_add_intrusion(
|
|
950
|
+
self,
|
|
951
|
+
intrusion_name,
|
|
952
|
+
intrusion_frame_name,
|
|
953
|
+
intrusion_frame_parameters={},
|
|
954
|
+
intrusion_lateral_extent_model=None,
|
|
955
|
+
intrusion_vertical_extent_model=None,
|
|
956
|
+
geometric_scaling_parameters={},
|
|
957
|
+
**kwargs,
|
|
958
|
+
):
|
|
959
|
+
"""
|
|
960
|
+
|
|
961
|
+
Note
|
|
962
|
+
-----
|
|
963
|
+
An intrusion in built in two main steps:
|
|
964
|
+
(1) Intrusion builder: intrusion builder creates the intrusion structural frame.
|
|
965
|
+
This object is curvilinear coordinate system of the intrusion constrained with intrusion network points,
|
|
966
|
+
and flow and inflation measurements (provided by the user).
|
|
967
|
+
The intrusion network is a representation of the approximated location of roof or floor contact of the intrusion.
|
|
968
|
+
This object might be constrained using the anisotropies of the host rock if the roof (or floor) contact is not well constrained.
|
|
969
|
+
|
|
970
|
+
(2) Intrusion feature: simulation of lateral and vertical extent of intrusion within the model volume.
|
|
971
|
+
The simulations outcome consist in thresholds distances along the structural frame coordinates
|
|
972
|
+
that are used to constrained the extent of the intrusion.
|
|
973
|
+
|
|
974
|
+
Parameters
|
|
975
|
+
----------
|
|
976
|
+
intrusion_name : string,
|
|
977
|
+
name of intrusion feature in model data
|
|
978
|
+
intrusion_frame_name : string,
|
|
979
|
+
name of intrusion frame in model data
|
|
980
|
+
intrusion_lateral_extent_model = function,
|
|
981
|
+
geometrical conceptual model for simulation of lateral extent
|
|
982
|
+
intrusion_vertical_extent_model = function,
|
|
983
|
+
geometrical conceptual model for simulation of vertical extent
|
|
984
|
+
intrusion_frame_parameters = dictionary
|
|
985
|
+
|
|
986
|
+
kwargs
|
|
987
|
+
|
|
988
|
+
Returns
|
|
989
|
+
-------
|
|
990
|
+
intrusion feature
|
|
991
|
+
|
|
992
|
+
"""
|
|
993
|
+
# if intrusions is False:
|
|
994
|
+
# logger.error("Libraries not installed")
|
|
995
|
+
# raise Exception("Libraries not installed")
|
|
996
|
+
|
|
997
|
+
intrusion_data = self.data[self.data["feature_name"] == intrusion_name].copy()
|
|
998
|
+
intrusion_frame_data = self.data[self.data["feature_name"] == intrusion_frame_name].copy()
|
|
999
|
+
|
|
1000
|
+
# -- get variables for intrusion frame interpolation
|
|
1001
|
+
gxxgz = kwargs.get("gxxgz", 0)
|
|
1002
|
+
gxxgy = kwargs.get("gxxgy", 0)
|
|
1003
|
+
gyxgz = kwargs.get("gyxgz", 0)
|
|
1004
|
+
|
|
1005
|
+
interpolatortype = kwargs.get("interpolatortype", "PLI")
|
|
1006
|
+
# buffer = kwargs.get("buffer", 0.1)
|
|
1007
|
+
nelements = kwargs.get("nelements", 1e2)
|
|
1008
|
+
|
|
1009
|
+
weights = [gxxgz, gxxgy, gyxgz]
|
|
1010
|
+
|
|
1011
|
+
intrusion_frame_builder = IntrusionFrameBuilder(
|
|
1012
|
+
interpolatortype=interpolatortype,
|
|
1013
|
+
bounding_box=self.bounding_box.with_buffer(kwargs.get("buffer", 0.1)),
|
|
1014
|
+
nelements=kwargs.get("nelements", 1e2),
|
|
1015
|
+
name=intrusion_frame_name,
|
|
1016
|
+
model=self,
|
|
1017
|
+
**kwargs,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
self._add_faults(intrusion_frame_builder)
|
|
1021
|
+
# intrusion_frame_builder.post_intrusion_faults = faults # LG unused?
|
|
1022
|
+
|
|
1023
|
+
# -- create intrusion frame using intrusion structures (steps and marginal faults) and flow/inflation measurements
|
|
1024
|
+
if len(intrusion_frame_parameters) == 0:
|
|
1025
|
+
logger.error("Please specify parameters to build intrusion frame")
|
|
1026
|
+
intrusion_frame_builder.set_intrusion_frame_parameters(
|
|
1027
|
+
intrusion_data, intrusion_frame_parameters
|
|
1028
|
+
)
|
|
1029
|
+
intrusion_frame_builder.create_constraints_for_c0()
|
|
1030
|
+
|
|
1031
|
+
intrusion_frame_builder.set_intrusion_frame_data(intrusion_frame_data)
|
|
1032
|
+
|
|
1033
|
+
## -- create intrusion frame
|
|
1034
|
+
intrusion_frame_builder.setup(
|
|
1035
|
+
nelements=nelements,
|
|
1036
|
+
w2=weights[0],
|
|
1037
|
+
w1=weights[1],
|
|
1038
|
+
gxygz=weights[2],
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
intrusion_frame = intrusion_frame_builder.frame
|
|
1042
|
+
|
|
1043
|
+
# -- create intrusion builder to compute distance thresholds along the frame coordinates
|
|
1044
|
+
intrusion_builder = IntrusionBuilder(
|
|
1045
|
+
intrusion_frame,
|
|
1046
|
+
model=self,
|
|
1047
|
+
# interpolator=interpolator,
|
|
1048
|
+
name=f"{intrusion_name}_feature",
|
|
1049
|
+
lateral_extent_model=intrusion_lateral_extent_model,
|
|
1050
|
+
vertical_extent_model=intrusion_vertical_extent_model,
|
|
1051
|
+
**kwargs,
|
|
1052
|
+
)
|
|
1053
|
+
intrusion_builder.set_data_for_extent_calculation(intrusion_data)
|
|
1054
|
+
|
|
1055
|
+
intrusion_builder.build_arguments = {
|
|
1056
|
+
"geometric_scaling_parameters": geometric_scaling_parameters,
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
intrusion_feature = intrusion_builder.feature
|
|
1060
|
+
self._add_feature(intrusion_feature)
|
|
1061
|
+
|
|
1062
|
+
return intrusion_feature
|
|
1063
|
+
|
|
1064
|
+
def _add_faults(self, feature_builder, features=None):
|
|
1065
|
+
"""Adds all existing faults to a geological feature builder
|
|
1066
|
+
|
|
1067
|
+
Parameters
|
|
1068
|
+
----------
|
|
1069
|
+
feature_builder : GeologicalFeatureBuilder/StructuralFrameBuilder
|
|
1070
|
+
The feature buider to add the faults to
|
|
1071
|
+
features : list, optional
|
|
1072
|
+
A specific list of features rather than all features in the model
|
|
1073
|
+
Returns
|
|
1074
|
+
-------
|
|
1075
|
+
|
|
1076
|
+
"""
|
|
1077
|
+
if features is None:
|
|
1078
|
+
features = self.features
|
|
1079
|
+
for f in reversed(features):
|
|
1080
|
+
if isinstance(f, str):
|
|
1081
|
+
f = self.__getitem__(f)
|
|
1082
|
+
if f.type == FeatureType.FAULT:
|
|
1083
|
+
feature_builder.add_fault(f)
|
|
1084
|
+
|
|
1085
|
+
def _add_domain_fault_above(self, feature):
|
|
1086
|
+
"""
|
|
1087
|
+
Looks through the feature list and adds any domain faults to the feature. The domain fault masks everything
|
|
1088
|
+
where the fault scalar field is < 0 as being active when added to feature.
|
|
1089
|
+
|
|
1090
|
+
Parameters
|
|
1091
|
+
----------
|
|
1092
|
+
feature : GeologicalFeatureBuilder
|
|
1093
|
+
the feature being added to the model where domain faults should be added
|
|
1094
|
+
|
|
1095
|
+
Returns
|
|
1096
|
+
-------
|
|
1097
|
+
|
|
1098
|
+
"""
|
|
1099
|
+
for f in reversed(self.features):
|
|
1100
|
+
if f.name == feature.name:
|
|
1101
|
+
continue
|
|
1102
|
+
if f.type == "domain_fault":
|
|
1103
|
+
feature.add_region(lambda pos: f.evaluate_value(pos) < 0)
|
|
1104
|
+
break
|
|
1105
|
+
|
|
1106
|
+
def _add_domain_fault_below(self, domain_fault):
|
|
1107
|
+
"""
|
|
1108
|
+
Looks through the feature list and adds any the domain_fault to the features
|
|
1109
|
+
that already exist in the stack until an unconformity is reached. domain faults
|
|
1110
|
+
to the feature. The domain fault masks everything where the fault scalar field
|
|
1111
|
+
is < 0 as being active when added to feature.
|
|
1112
|
+
|
|
1113
|
+
Parameters
|
|
1114
|
+
----------
|
|
1115
|
+
feature : GeologicalFeatureBuilder
|
|
1116
|
+
the feature being added to the model where domain faults should be added
|
|
1117
|
+
|
|
1118
|
+
Returns
|
|
1119
|
+
-------
|
|
1120
|
+
|
|
1121
|
+
"""
|
|
1122
|
+
for f in reversed(self.features):
|
|
1123
|
+
if f.name == domain_fault.name:
|
|
1124
|
+
continue
|
|
1125
|
+
f.add_region(lambda pos: domain_fault.evaluate_value(pos) > 0)
|
|
1126
|
+
if f.type == FeatureType.UNCONFORMITY:
|
|
1127
|
+
break
|
|
1128
|
+
|
|
1129
|
+
def _add_unconformity_above(self, feature):
|
|
1130
|
+
"""
|
|
1131
|
+
|
|
1132
|
+
Adds a region to the feature to prevent the value from being
|
|
1133
|
+
interpolated where the unconformities exists above e.g.
|
|
1134
|
+
if there is another feature above and the unconformity is at 0
|
|
1135
|
+
then the features added below (after) will only be visible where the
|
|
1136
|
+
uncomformity is <0
|
|
1137
|
+
|
|
1138
|
+
Parameters
|
|
1139
|
+
----------
|
|
1140
|
+
feature - GeologicalFeature
|
|
1141
|
+
|
|
1142
|
+
Returns
|
|
1143
|
+
-------
|
|
1144
|
+
|
|
1145
|
+
"""
|
|
1146
|
+
|
|
1147
|
+
if feature.type == FeatureType.FAULT:
|
|
1148
|
+
return
|
|
1149
|
+
for f in reversed(self.features):
|
|
1150
|
+
if f.type == FeatureType.UNCONFORMITY and f.name != feature.name:
|
|
1151
|
+
logger.info(f"Adding {f.name} as unconformity to {feature.name}")
|
|
1152
|
+
feature.add_region(f)
|
|
1153
|
+
if f.type == FeatureType.ONLAPUNCONFORMITY and f.name != feature.name:
|
|
1154
|
+
feature.add_region(f)
|
|
1155
|
+
break
|
|
1156
|
+
|
|
1157
|
+
def add_unconformity(self, feature: GeologicalFeature, value: float) -> UnconformityFeature:
|
|
1158
|
+
"""
|
|
1159
|
+
Use an existing feature to add an unconformity to the model.
|
|
1160
|
+
|
|
1161
|
+
Parameters
|
|
1162
|
+
----------
|
|
1163
|
+
feature : GeologicalFeature
|
|
1164
|
+
existing geological feature
|
|
1165
|
+
value : float
|
|
1166
|
+
scalar value of isosurface that represents
|
|
1167
|
+
|
|
1168
|
+
Returns
|
|
1169
|
+
-------
|
|
1170
|
+
unconformity : GeologicalFeature
|
|
1171
|
+
unconformity feature
|
|
1172
|
+
|
|
1173
|
+
"""
|
|
1174
|
+
logger.debug(f"Adding {feature.name} as unconformity at {value}")
|
|
1175
|
+
if feature is None:
|
|
1176
|
+
logger.warning("Cannot add unconformtiy, base feature is None")
|
|
1177
|
+
return
|
|
1178
|
+
# look backwards through features and add the unconformity as a region until
|
|
1179
|
+
# we get to an unconformity
|
|
1180
|
+
uc_feature = UnconformityFeature(feature, value)
|
|
1181
|
+
feature.add_region(uc_feature.inverse())
|
|
1182
|
+
for f in reversed(self.features):
|
|
1183
|
+
if f.type == FeatureType.UNCONFORMITY:
|
|
1184
|
+
logger.debug(f"Reached unconformity {f.name}")
|
|
1185
|
+
break
|
|
1186
|
+
logger.debug(f"Adding {uc_feature.name} as unconformity to {f.name}")
|
|
1187
|
+
if f.type == FeatureType.FAULT:
|
|
1188
|
+
continue
|
|
1189
|
+
if f == feature:
|
|
1190
|
+
continue
|
|
1191
|
+
else:
|
|
1192
|
+
f.add_region(uc_feature)
|
|
1193
|
+
# now add the unconformity to the feature list
|
|
1194
|
+
self._add_feature(uc_feature)
|
|
1195
|
+
return uc_feature
|
|
1196
|
+
|
|
1197
|
+
def add_onlap_unconformity(self, feature: GeologicalFeature, value: float) -> GeologicalFeature:
|
|
1198
|
+
"""
|
|
1199
|
+
Use an existing feature to add an unconformity to the model.
|
|
1200
|
+
|
|
1201
|
+
Parameters
|
|
1202
|
+
----------
|
|
1203
|
+
feature : GeologicalFeature
|
|
1204
|
+
existing geological feature
|
|
1205
|
+
value : float
|
|
1206
|
+
scalar value of isosurface that represents
|
|
1207
|
+
|
|
1208
|
+
Returns
|
|
1209
|
+
-------
|
|
1210
|
+
unconformity_feature : GeologicalFeature
|
|
1211
|
+
the created unconformity
|
|
1212
|
+
|
|
1213
|
+
"""
|
|
1214
|
+
feature.regions = []
|
|
1215
|
+
uc_feature = UnconformityFeature(feature, value, False, onlap=True)
|
|
1216
|
+
feature.add_region(uc_feature.inverse())
|
|
1217
|
+
for f in reversed(self.features):
|
|
1218
|
+
if f.type == FeatureType.UNCONFORMITY:
|
|
1219
|
+
# f.add_region(uc_feature)
|
|
1220
|
+
continue
|
|
1221
|
+
if f.type == FeatureType.FAULT:
|
|
1222
|
+
continue
|
|
1223
|
+
if f != feature:
|
|
1224
|
+
f.add_region(uc_feature)
|
|
1225
|
+
self._add_feature(uc_feature.inverse())
|
|
1226
|
+
|
|
1227
|
+
return uc_feature
|
|
1228
|
+
|
|
1229
|
+
def create_and_add_domain_fault(
|
|
1230
|
+
self, fault_surface_data, nelements=10000, interpolatortype="FDI", **kwargs
|
|
1231
|
+
):
|
|
1232
|
+
"""
|
|
1233
|
+
Parameters
|
|
1234
|
+
----------
|
|
1235
|
+
fault_surface_data : string
|
|
1236
|
+
name of the domain fault data in the data frame
|
|
1237
|
+
|
|
1238
|
+
Returns
|
|
1239
|
+
-------
|
|
1240
|
+
domain_Fault : GeologicalFeature
|
|
1241
|
+
the created domain fault
|
|
1242
|
+
|
|
1243
|
+
Notes
|
|
1244
|
+
-----
|
|
1245
|
+
* :meth:`LoopStructural.GeologicalModel.get_interpolator`
|
|
1246
|
+
|
|
1247
|
+
"""
|
|
1248
|
+
domain_fault_feature_builder = GeologicalFeatureBuilder(
|
|
1249
|
+
bounding_box=self.bounding_box,
|
|
1250
|
+
interpolatortype=interpolatortype,
|
|
1251
|
+
nelements=nelements,
|
|
1252
|
+
name=fault_surface_data,
|
|
1253
|
+
model=self,
|
|
1254
|
+
**kwargs,
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
# add data
|
|
1258
|
+
unconformity_data = self.data[self.data["feature_name"] == fault_surface_data]
|
|
1259
|
+
|
|
1260
|
+
domain_fault_feature_builder.add_data_from_data_frame(unconformity_data)
|
|
1261
|
+
# look through existing features if there is a fault before an
|
|
1262
|
+
# unconformity
|
|
1263
|
+
# then add to the feature, once we get to an unconformity stop
|
|
1264
|
+
self._add_faults(domain_fault_feature_builder)
|
|
1265
|
+
|
|
1266
|
+
# build feature
|
|
1267
|
+
# domain_fault = domain_fault_feature_builder.build(**kwargs)
|
|
1268
|
+
domain_fault = domain_fault_feature_builder.feature
|
|
1269
|
+
domain_fault_feature_builder.build_arguments = kwargs
|
|
1270
|
+
domain_fault.type = FeatureType.DOMAINFAULT
|
|
1271
|
+
self._add_feature(domain_fault)
|
|
1272
|
+
self._add_domain_fault_below(domain_fault)
|
|
1273
|
+
|
|
1274
|
+
domain_fault_uc = UnconformityFeature(domain_fault, 0)
|
|
1275
|
+
# iterate over existing features and add the unconformity as a region
|
|
1276
|
+
# so the feature is only evaluated where the unconformity is positive
|
|
1277
|
+
return domain_fault_uc
|
|
1278
|
+
|
|
1279
|
+
def create_and_add_fault(
|
|
1280
|
+
self,
|
|
1281
|
+
fault_surface_data,
|
|
1282
|
+
displacement,
|
|
1283
|
+
interpolatortype="FDI",
|
|
1284
|
+
tol=None,
|
|
1285
|
+
fault_slip_vector=None,
|
|
1286
|
+
fault_normal_vector=None,
|
|
1287
|
+
fault_center=None,
|
|
1288
|
+
major_axis=None,
|
|
1289
|
+
minor_axis=None,
|
|
1290
|
+
intermediate_axis=None,
|
|
1291
|
+
faultfunction="BaseFault",
|
|
1292
|
+
faults=[],
|
|
1293
|
+
force_mesh_geometry: bool = False,
|
|
1294
|
+
points: bool = False,
|
|
1295
|
+
fault_buffer=0.2,
|
|
1296
|
+
fault_trace_anisotropy=0.0,
|
|
1297
|
+
fault_dip=90,
|
|
1298
|
+
fault_dip_anisotropy=0.0,
|
|
1299
|
+
**kwargs,
|
|
1300
|
+
):
|
|
1301
|
+
"""
|
|
1302
|
+
Parameters
|
|
1303
|
+
----------
|
|
1304
|
+
fault_surface_data : string
|
|
1305
|
+
name of the fault surface data in the dataframe
|
|
1306
|
+
displacement : displacement magnitude
|
|
1307
|
+
major_axis : [type], optional
|
|
1308
|
+
[description], by default None
|
|
1309
|
+
minor_axis : [type], optional
|
|
1310
|
+
[description], by default None
|
|
1311
|
+
intermediate_axis : [type], optional
|
|
1312
|
+
[description], by default None
|
|
1313
|
+
kwargs : additional kwargs for Fault and interpolators
|
|
1314
|
+
|
|
1315
|
+
Returns
|
|
1316
|
+
-------
|
|
1317
|
+
fault : FaultSegment
|
|
1318
|
+
created fault
|
|
1319
|
+
|
|
1320
|
+
Notes
|
|
1321
|
+
-----
|
|
1322
|
+
* :meth:`LoopStructural.GeologicalModel.get_interpolator`
|
|
1323
|
+
* :class:`LoopStructural.modelling.features.builders.FaultBuilder`
|
|
1324
|
+
* :meth:`LoopStructural.modelling.features.builders.FaultBuilder.setup`
|
|
1325
|
+
"""
|
|
1326
|
+
if "fault_extent" in kwargs and major_axis is None:
|
|
1327
|
+
major_axis = kwargs["fault_extent"]
|
|
1328
|
+
if "fault_influence" in kwargs and minor_axis is None:
|
|
1329
|
+
minor_axis = kwargs["fault_influence"]
|
|
1330
|
+
if "fault_vectical_radius" in kwargs and intermediate_axis is None:
|
|
1331
|
+
intermediate_axis = kwargs["fault_vectical_radius"]
|
|
1332
|
+
|
|
1333
|
+
logger.info(f'Creating fault "{fault_surface_data}"')
|
|
1334
|
+
logger.info(f"Displacement: {displacement}")
|
|
1335
|
+
logger.info(f"Tolerance: {tol}")
|
|
1336
|
+
logger.info(f"Fault function: {faultfunction}")
|
|
1337
|
+
logger.info(f"Fault slip vector: {fault_slip_vector}")
|
|
1338
|
+
logger.info(f"Fault center: {fault_center}")
|
|
1339
|
+
logger.info(f"Major axis: {major_axis}")
|
|
1340
|
+
logger.info(f"Minor axis: {minor_axis}")
|
|
1341
|
+
logger.info(f"Intermediate axis: {intermediate_axis}")
|
|
1342
|
+
if fault_slip_vector is not None:
|
|
1343
|
+
fault_slip_vector = np.array(fault_slip_vector, dtype="float")
|
|
1344
|
+
if fault_center is not None:
|
|
1345
|
+
fault_center = np.array(fault_center, dtype="float")
|
|
1346
|
+
|
|
1347
|
+
for k, v in kwargs.items():
|
|
1348
|
+
logger.info(f"{k}: {v}")
|
|
1349
|
+
|
|
1350
|
+
if tol is None:
|
|
1351
|
+
tol = self.tol
|
|
1352
|
+
# divide the tolerance by half of the minor axis, as this is the equivalent of the distance
|
|
1353
|
+
# of the unit vector
|
|
1354
|
+
# if minor_axis:
|
|
1355
|
+
# tol *= 0.1*minor_axis
|
|
1356
|
+
|
|
1357
|
+
if displacement == 0:
|
|
1358
|
+
logger.warning(f"{fault_surface_data} displacement is 0")
|
|
1359
|
+
|
|
1360
|
+
if "data_region" in kwargs:
|
|
1361
|
+
kwargs.pop("data_region")
|
|
1362
|
+
logger.error("kwarg data_region currently not supported, disabling")
|
|
1363
|
+
displacement_scaled = displacement / self.scale_factor
|
|
1364
|
+
fault_frame_builder = FaultBuilder(
|
|
1365
|
+
interpolatortype,
|
|
1366
|
+
bounding_box=self.bounding_box,
|
|
1367
|
+
nelements=kwargs.pop("nelements", 1e4),
|
|
1368
|
+
name=fault_surface_data,
|
|
1369
|
+
model=self,
|
|
1370
|
+
**kwargs,
|
|
1371
|
+
)
|
|
1372
|
+
fault_frame_data = self.data.loc[self.data["feature_name"] == fault_surface_data].copy()
|
|
1373
|
+
self._add_faults(fault_frame_builder, features=faults)
|
|
1374
|
+
# add data
|
|
1375
|
+
fault_frame_data = self.data.loc[self.data["feature_name"] == fault_surface_data].copy()
|
|
1376
|
+
|
|
1377
|
+
if fault_center is not None and ~np.isnan(fault_center).any():
|
|
1378
|
+
fault_center = self.scale(fault_center, inplace=False)
|
|
1379
|
+
if minor_axis:
|
|
1380
|
+
minor_axis = minor_axis / self.scale_factor
|
|
1381
|
+
if major_axis:
|
|
1382
|
+
major_axis = major_axis / self.scale_factor
|
|
1383
|
+
if intermediate_axis:
|
|
1384
|
+
intermediate_axis = intermediate_axis / self.scale_factor
|
|
1385
|
+
fault_frame_builder.create_data_from_geometry(
|
|
1386
|
+
fault_frame_data=fault_frame_data,
|
|
1387
|
+
fault_center=fault_center,
|
|
1388
|
+
fault_normal_vector=fault_normal_vector,
|
|
1389
|
+
fault_slip_vector=fault_slip_vector,
|
|
1390
|
+
minor_axis=minor_axis,
|
|
1391
|
+
major_axis=major_axis,
|
|
1392
|
+
intermediate_axis=intermediate_axis,
|
|
1393
|
+
points=points,
|
|
1394
|
+
force_mesh_geometry=force_mesh_geometry,
|
|
1395
|
+
fault_buffer=fault_buffer,
|
|
1396
|
+
fault_trace_anisotropy=fault_trace_anisotropy,
|
|
1397
|
+
fault_dip=fault_dip,
|
|
1398
|
+
fault_dip_anisotropy=fault_dip_anisotropy,
|
|
1399
|
+
)
|
|
1400
|
+
if "force_mesh_geometry" not in kwargs:
|
|
1401
|
+
fault_frame_builder.set_mesh_geometry(kwargs.get("fault_buffer", 0.2), 0)
|
|
1402
|
+
if "splay" in kwargs and "splayregion" in kwargs:
|
|
1403
|
+
fault_frame_builder.add_splay(kwargs["splay"], kwargs["splayregion"])
|
|
1404
|
+
|
|
1405
|
+
kwargs["tol"] = tol
|
|
1406
|
+
fault_frame_builder.setup(**kwargs)
|
|
1407
|
+
fault = fault_frame_builder.frame
|
|
1408
|
+
fault.displacement = displacement_scaled
|
|
1409
|
+
fault.faultfunction = faultfunction
|
|
1410
|
+
|
|
1411
|
+
for f in reversed(self.features):
|
|
1412
|
+
if f.type == FeatureType.UNCONFORMITY:
|
|
1413
|
+
fault.add_region(f)
|
|
1414
|
+
break
|
|
1415
|
+
if displacement == 0:
|
|
1416
|
+
fault.type = FeatureType.INACTIVEFAULT
|
|
1417
|
+
self._add_feature(fault)
|
|
1418
|
+
|
|
1419
|
+
return fault
|
|
1420
|
+
|
|
1421
|
+
# TODO move rescale to bounding box/transformer
|
|
1422
|
+
def rescale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
|
|
1423
|
+
"""
|
|
1424
|
+
Convert from model scale to real world scale - in the future this
|
|
1425
|
+
should also do transformations?
|
|
1426
|
+
|
|
1427
|
+
Parameters
|
|
1428
|
+
----------
|
|
1429
|
+
points : np.array((N,3),dtype=double)
|
|
1430
|
+
inplace : boolean
|
|
1431
|
+
whether to return a modified copy or modify the original array
|
|
1432
|
+
|
|
1433
|
+
Returns
|
|
1434
|
+
-------
|
|
1435
|
+
points : np.array((N,3),dtype=double)
|
|
1436
|
+
|
|
1437
|
+
"""
|
|
1438
|
+
if not inplace:
|
|
1439
|
+
points = points.copy()
|
|
1440
|
+
points *= self.scale_factor
|
|
1441
|
+
points += self.origin
|
|
1442
|
+
return points
|
|
1443
|
+
|
|
1444
|
+
# TODO move scale to bounding box/transformer
|
|
1445
|
+
def scale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
|
|
1446
|
+
"""Take points in UTM coordinates and reproject
|
|
1447
|
+
into scaled model space
|
|
1448
|
+
|
|
1449
|
+
Parameters
|
|
1450
|
+
----------
|
|
1451
|
+
points : np.array((N,3),dtype=float)
|
|
1452
|
+
points to
|
|
1453
|
+
inplace : bool, optional default = True
|
|
1454
|
+
whether to copy the points array or update the passed array
|
|
1455
|
+
Returns
|
|
1456
|
+
-------
|
|
1457
|
+
points : np.a::rray((N,3),dtype=double)
|
|
1458
|
+
|
|
1459
|
+
"""
|
|
1460
|
+
points = np.array(points).astype(float)
|
|
1461
|
+
if not inplace:
|
|
1462
|
+
points = points.copy()
|
|
1463
|
+
# if len(points.shape) == 1:
|
|
1464
|
+
# points = points[None,:]
|
|
1465
|
+
# if len(points.shape) != 2:
|
|
1466
|
+
# logger.error("cannot scale array of dimensions".format(len(points.shape)))
|
|
1467
|
+
points -= self.origin
|
|
1468
|
+
points /= self.scale_factor
|
|
1469
|
+
return points
|
|
1470
|
+
|
|
1471
|
+
def regular_grid(self, nsteps=None, shuffle=True, rescale=False, order="C"):
|
|
1472
|
+
"""
|
|
1473
|
+
Return a regular grid within the model bounding box
|
|
1474
|
+
|
|
1475
|
+
Parameters
|
|
1476
|
+
----------
|
|
1477
|
+
nsteps : tuple
|
|
1478
|
+
number of cells in x,y,z
|
|
1479
|
+
|
|
1480
|
+
Returns
|
|
1481
|
+
-------
|
|
1482
|
+
xyz : np.array((N,3),dtype=float)
|
|
1483
|
+
locations of points in regular grid
|
|
1484
|
+
"""
|
|
1485
|
+
return self.bounding_box.regular_grid(nsteps=nsteps, shuffle=shuffle, order=order)
|
|
1486
|
+
|
|
1487
|
+
def evaluate_model(self, xyz: np.ndarray, scale: bool = True) -> np.ndarray:
|
|
1488
|
+
"""Evaluate the stratigraphic id at each location
|
|
1489
|
+
|
|
1490
|
+
Parameters
|
|
1491
|
+
----------
|
|
1492
|
+
xyz : np.array((N,3),dtype=float)
|
|
1493
|
+
locations
|
|
1494
|
+
scale : bool
|
|
1495
|
+
whether to rescale the xyz before evaluating model
|
|
1496
|
+
|
|
1497
|
+
Returns
|
|
1498
|
+
-------
|
|
1499
|
+
stratigraphic_id : np.array(N,dtype=int)
|
|
1500
|
+
the stratigraphic index for locations
|
|
1501
|
+
|
|
1502
|
+
Examples
|
|
1503
|
+
--------
|
|
1504
|
+
Evaluate on a voxet
|
|
1505
|
+
|
|
1506
|
+
>>> x = np.linspace(model.bounding_box[0, 0], model.bounding_box[1, 0],
|
|
1507
|
+
nsteps[0])
|
|
1508
|
+
>>> y = np.linspace(model.bounding_box[0, 1], model.bounding_box[1, 1],
|
|
1509
|
+
nsteps[1])
|
|
1510
|
+
>>> z = np.linspace(model.bounding_box[1, 2], model.bounding_box[0, 2],
|
|
1511
|
+
nsteps[2])
|
|
1512
|
+
>>> xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
|
|
1513
|
+
>>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
|
|
1514
|
+
>>> model.evaluate_model(xyz,scale=False)
|
|
1515
|
+
|
|
1516
|
+
Evaluate on points defined by regular grid function
|
|
1517
|
+
|
|
1518
|
+
>>> model.evaluate_model(model.regular_grid(shuffle=False),scale=False)
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
Evaluate on a map
|
|
1522
|
+
|
|
1523
|
+
>>> x = np.linspace(self.bounding_box[0, 0], self.bounding_box[1, 0],
|
|
1524
|
+
nsteps[0])
|
|
1525
|
+
>>> y = np.linspace(self.bounding_box[0, 1], self.bounding_box[1, 1],
|
|
1526
|
+
nsteps[1])
|
|
1527
|
+
>>> xx, yy = np.meshgrid(x, y, indexing='ij')
|
|
1528
|
+
>>> zz = np.zeros_like(yy)
|
|
1529
|
+
>>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
|
|
1530
|
+
>>> model.evaluate_model(model.regular_grid(shuffle=False),scale=False)
|
|
1531
|
+
|
|
1532
|
+
Evaluate on points in reference coordinate system
|
|
1533
|
+
>>> model.evaluate_model(xyz,scale=True)
|
|
1534
|
+
|
|
1535
|
+
"""
|
|
1536
|
+
xyz = np.array(xyz)
|
|
1537
|
+
if scale:
|
|
1538
|
+
xyz = self.scale(xyz, inplace=False)
|
|
1539
|
+
strat_id = np.zeros(xyz.shape[0], dtype=int)
|
|
1540
|
+
# set strat id to -1 to identify which areas of the model aren't covered
|
|
1541
|
+
strat_id[:] = -1
|
|
1542
|
+
if self.stratigraphic_column is None:
|
|
1543
|
+
logger.warning("No stratigraphic column defined")
|
|
1544
|
+
return strat_id
|
|
1545
|
+
for group in reversed(self.stratigraphic_column.keys()):
|
|
1546
|
+
if group == "faults":
|
|
1547
|
+
continue
|
|
1548
|
+
feature_id = self.feature_name_index.get(group, -1)
|
|
1549
|
+
if feature_id >= 0:
|
|
1550
|
+
feature = self.features[feature_id]
|
|
1551
|
+
vals = feature.evaluate_value(xyz)
|
|
1552
|
+
for series in self.stratigraphic_column[group].values():
|
|
1553
|
+
strat_id[
|
|
1554
|
+
np.logical_and(
|
|
1555
|
+
vals < series.get("max", feature.max()),
|
|
1556
|
+
vals > series.get("min", feature.min()),
|
|
1557
|
+
)
|
|
1558
|
+
] = series["id"]
|
|
1559
|
+
if feature_id == -1:
|
|
1560
|
+
logger.error(f"Model does not contain {group}")
|
|
1561
|
+
return strat_id
|
|
1562
|
+
|
|
1563
|
+
def evaluate_model_gradient(self, points: np.ndarray, scale: bool = True) -> np.ndarray:
|
|
1564
|
+
"""Evaluate the gradient of the stratigraphic column at each location
|
|
1565
|
+
|
|
1566
|
+
Parameters
|
|
1567
|
+
----------
|
|
1568
|
+
points : np.ndarray
|
|
1569
|
+
location to evaluate
|
|
1570
|
+
scale : bool, optional
|
|
1571
|
+
whether to scale the points into model domain, by default True
|
|
1572
|
+
|
|
1573
|
+
Returns
|
|
1574
|
+
-------
|
|
1575
|
+
np.ndarray
|
|
1576
|
+
N,3 array of gradient vectors
|
|
1577
|
+
"""
|
|
1578
|
+
xyz = np.array(points)
|
|
1579
|
+
if scale:
|
|
1580
|
+
xyz = self.scale(xyz, inplace=False)
|
|
1581
|
+
grad = np.zeros(xyz.shape)
|
|
1582
|
+
for group in reversed(self.stratigraphic_column.keys()):
|
|
1583
|
+
if group == "faults":
|
|
1584
|
+
continue
|
|
1585
|
+
feature_id = self.feature_name_index.get(group, -1)
|
|
1586
|
+
if feature_id >= 0:
|
|
1587
|
+
feature = self.features[feature_id]
|
|
1588
|
+
gradt = feature.evaluate_gradient(xyz)
|
|
1589
|
+
grad[~np.isnan(gradt).any(axis=1)] = gradt[~np.isnan(gradt).any(axis=1)]
|
|
1590
|
+
if feature_id == -1:
|
|
1591
|
+
logger.error(f"Model does not contain {group}")
|
|
1592
|
+
|
|
1593
|
+
return grad
|
|
1594
|
+
|
|
1595
|
+
def evaluate_fault_displacements(self, points, scale=True):
|
|
1596
|
+
"""Evaluate the fault displacement magnitude at each location
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
Parameters
|
|
1600
|
+
----------
|
|
1601
|
+
xyz : np.array((N,3),dtype=float)
|
|
1602
|
+
locations
|
|
1603
|
+
scale : bool
|
|
1604
|
+
whether to rescale the xyz before evaluating model
|
|
1605
|
+
|
|
1606
|
+
Returns
|
|
1607
|
+
-------
|
|
1608
|
+
fault_displacement : np.array(N,dtype=float)
|
|
1609
|
+
the fault displacement magnitude
|
|
1610
|
+
"""
|
|
1611
|
+
if scale:
|
|
1612
|
+
points = self.scale(points, inplace=False)
|
|
1613
|
+
vals = np.zeros(points.shape[0])
|
|
1614
|
+
for f in self.features:
|
|
1615
|
+
if f.type == FeatureType.FAULT:
|
|
1616
|
+
disp = f.displacementfeature.evaluate_value(points)
|
|
1617
|
+
vals[~np.isnan(disp)] += disp[~np.isnan(disp)]
|
|
1618
|
+
return vals * -self.scale_factor # convert from restoration magnutude to displacement
|
|
1619
|
+
|
|
1620
|
+
def get_feature_by_name(self, feature_name) -> GeologicalFeature:
|
|
1621
|
+
"""Returns a feature from the mode given a name
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
Parameters
|
|
1625
|
+
----------
|
|
1626
|
+
feature_name : string
|
|
1627
|
+
the name of the feature
|
|
1628
|
+
|
|
1629
|
+
Returns
|
|
1630
|
+
-------
|
|
1631
|
+
feature : GeologicalFeature
|
|
1632
|
+
the geological feature with the specified name, or none if no feature
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
"""
|
|
1637
|
+
feature_index = self.feature_name_index.get(feature_name, -1)
|
|
1638
|
+
if feature_index > -1:
|
|
1639
|
+
return self.features[feature_index]
|
|
1640
|
+
else:
|
|
1641
|
+
raise ValueError(f"{feature_name} does not exist!")
|
|
1642
|
+
|
|
1643
|
+
def evaluate_feature_value(self, feature_name, xyz, scale=True):
|
|
1644
|
+
"""Evaluate the scalar value of the geological feature given the name at locations
|
|
1645
|
+
xyz
|
|
1646
|
+
|
|
1647
|
+
Parameters
|
|
1648
|
+
----------
|
|
1649
|
+
feature_name : string
|
|
1650
|
+
name of the feature
|
|
1651
|
+
xyz : np.array((N,3))
|
|
1652
|
+
locations to evaluate
|
|
1653
|
+
scale : bool, optional
|
|
1654
|
+
whether to scale real world points into model scale, by default True
|
|
1655
|
+
|
|
1656
|
+
Returns
|
|
1657
|
+
-------
|
|
1658
|
+
np.array((N))
|
|
1659
|
+
vector of scalar values
|
|
1660
|
+
|
|
1661
|
+
Examples
|
|
1662
|
+
--------
|
|
1663
|
+
Evaluate on a voxet using model boundaries
|
|
1664
|
+
|
|
1665
|
+
>>> x = np.linspace(model.bounding_box[0, 0], model.bounding_box[1, 0],
|
|
1666
|
+
nsteps[0])
|
|
1667
|
+
>>> y = np.linspace(model.bounding_box[0, 1], model.bounding_box[1, 1],
|
|
1668
|
+
nsteps[1])
|
|
1669
|
+
>>> z = np.linspace(model.bounding_box[1, 2], model.bounding_box[0, 2],
|
|
1670
|
+
nsteps[2])
|
|
1671
|
+
>>> xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
|
|
1672
|
+
>>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
|
|
1673
|
+
>>> model.evaluate_feature_vaue('feature',xyz,scale=False)
|
|
1674
|
+
|
|
1675
|
+
Evaluate on points in UTM coordinates
|
|
1676
|
+
|
|
1677
|
+
>>> model.evaluate_feature_vaue('feature',utm_xyz)
|
|
1678
|
+
|
|
1679
|
+
"""
|
|
1680
|
+
feature = self.get_feature_by_name(feature_name)
|
|
1681
|
+
if feature:
|
|
1682
|
+
scaled_xyz = xyz
|
|
1683
|
+
if scale:
|
|
1684
|
+
scaled_xyz = self.scale(xyz, inplace=False)
|
|
1685
|
+
return feature.evaluate_value(scaled_xyz)
|
|
1686
|
+
else:
|
|
1687
|
+
return np.zeros(xyz.shape[0])
|
|
1688
|
+
|
|
1689
|
+
def evaluate_feature_gradient(self, feature_name, xyz, scale=True):
|
|
1690
|
+
"""Evaluate the gradient of the geological feature at a location
|
|
1691
|
+
|
|
1692
|
+
Parameters
|
|
1693
|
+
----------
|
|
1694
|
+
feature_name : string
|
|
1695
|
+
name of the geological feature
|
|
1696
|
+
xyz : np.array((N,3))
|
|
1697
|
+
locations to evaluate
|
|
1698
|
+
scale : bool, optional
|
|
1699
|
+
whether to scale real world points into model scale, by default True
|
|
1700
|
+
|
|
1701
|
+
Returns
|
|
1702
|
+
-------
|
|
1703
|
+
results : np.array((N,3))
|
|
1704
|
+
gradient of the scalar field at the locations specified
|
|
1705
|
+
"""
|
|
1706
|
+
feature = self.get_feature_by_name(feature_name)
|
|
1707
|
+
if feature:
|
|
1708
|
+
scaled_xyz = xyz
|
|
1709
|
+
if scale:
|
|
1710
|
+
scaled_xyz = self.scale(xyz, inplace=False)
|
|
1711
|
+
return feature.evaluate_gradient(scaled_xyz)
|
|
1712
|
+
else:
|
|
1713
|
+
return np.zeros(xyz.shape[0])
|
|
1714
|
+
|
|
1715
|
+
def update(self, verbose=False, progressbar=True):
|
|
1716
|
+
total_dof = 0
|
|
1717
|
+
nfeatures = 0
|
|
1718
|
+
for f in self.features:
|
|
1719
|
+
if f.type == FeatureType.FAULT:
|
|
1720
|
+
nfeatures += 3
|
|
1721
|
+
total_dof += f[0].interpolator.nx * 3
|
|
1722
|
+
continue
|
|
1723
|
+
if isinstance(f, StructuralFrame):
|
|
1724
|
+
nfeatures += 3
|
|
1725
|
+
total_dof += f[0].interpolator.nx * 3
|
|
1726
|
+
continue
|
|
1727
|
+
if f.type == FeatureType.INTERPOLATED:
|
|
1728
|
+
nfeatures += 1
|
|
1729
|
+
total_dof += f.interpolator.nx
|
|
1730
|
+
continue
|
|
1731
|
+
if verbose:
|
|
1732
|
+
print(
|
|
1733
|
+
f"Updating geological model. There are: \n {nfeatures} \
|
|
1734
|
+
geological features that need to be interpolated\n"
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
if progressbar:
|
|
1738
|
+
try:
|
|
1739
|
+
from tqdm.auto import tqdm
|
|
1740
|
+
|
|
1741
|
+
# Load tqdm with size counter instead of file counter
|
|
1742
|
+
with tqdm(total=nfeatures) as pbar:
|
|
1743
|
+
for f in self.features:
|
|
1744
|
+
pbar.set_description(f"Interpolating {f.name}")
|
|
1745
|
+
f.builder.up_to_date(callback=pbar.update)
|
|
1746
|
+
return
|
|
1747
|
+
except ImportError:
|
|
1748
|
+
logger.warning("Failed to import tqdm, disabling progress bar")
|
|
1749
|
+
|
|
1750
|
+
for f in self.features:
|
|
1751
|
+
f.builder.up_to_date()
|
|
1752
|
+
|
|
1753
|
+
def stratigraphic_ids(self):
|
|
1754
|
+
"""Return a list of all stratigraphic ids in the model
|
|
1755
|
+
|
|
1756
|
+
Returns
|
|
1757
|
+
-------
|
|
1758
|
+
ids : list
|
|
1759
|
+
list of unique stratigraphic ids, featurename, unit name and min and max scalar values
|
|
1760
|
+
"""
|
|
1761
|
+
ids = []
|
|
1762
|
+
if self.stratigraphic_column is None:
|
|
1763
|
+
logger.warning('No stratigraphic column defined')
|
|
1764
|
+
return ids
|
|
1765
|
+
for group in self.stratigraphic_column.keys():
|
|
1766
|
+
if group == "faults":
|
|
1767
|
+
continue
|
|
1768
|
+
for name, series in self.stratigraphic_column[group].items():
|
|
1769
|
+
ids.append([series["id"], group, name, series['min'], series['max']])
|
|
1770
|
+
return ids
|
|
1771
|
+
|
|
1772
|
+
def get_fault_surfaces(self, faults: List[str] = []):
|
|
1773
|
+
surfaces = []
|
|
1774
|
+
if len(faults) == 0:
|
|
1775
|
+
faults = self.fault_names()
|
|
1776
|
+
|
|
1777
|
+
for f in faults:
|
|
1778
|
+
surfaces.extend(self.get_feature_by_name(f).surfaces([0], self.bounding_box))
|
|
1779
|
+
return surfaces
|
|
1780
|
+
|
|
1781
|
+
def get_stratigraphic_surfaces(self, units: List[str] = [], bottoms: bool = True):
|
|
1782
|
+
## TODO change the stratigraphic column to its own class and have methods to get the relevant surfaces
|
|
1783
|
+
surfaces = []
|
|
1784
|
+
units = []
|
|
1785
|
+
if self.stratigraphic_column is None:
|
|
1786
|
+
return []
|
|
1787
|
+
for group in self.stratigraphic_column.keys():
|
|
1788
|
+
if group == "faults":
|
|
1789
|
+
continue
|
|
1790
|
+
for series in self.stratigraphic_column[group].values():
|
|
1791
|
+
series['feature_name'] = group
|
|
1792
|
+
units.append(series)
|
|
1793
|
+
unit_table = pd.DataFrame(units)
|
|
1794
|
+
for u in unit_table['feature_name'].unique():
|
|
1795
|
+
|
|
1796
|
+
values = unit_table.loc[unit_table['feature_name'] == u, 'min' if bottoms else 'max']
|
|
1797
|
+
if 'name' not in unit_table.columns:
|
|
1798
|
+
unit_table['name'] = unit_table['feature_name']
|
|
1799
|
+
|
|
1800
|
+
names = unit_table[unit_table['feature_name'] == u]['name']
|
|
1801
|
+
values = values.loc[~np.logical_or(values == np.inf, values == -np.inf)]
|
|
1802
|
+
surfaces.extend(
|
|
1803
|
+
self.get_feature_by_name(u).surfaces(
|
|
1804
|
+
values.to_list(), self.bounding_box, name=names.loc[values.index].to_list()
|
|
1805
|
+
)
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
return surfaces
|
|
1809
|
+
|
|
1810
|
+
def get_block_model(self, name='block model'):
|
|
1811
|
+
grid = self.bounding_box.structured_grid(name=name)
|
|
1812
|
+
|
|
1813
|
+
grid.cell_properties['stratigraphy'] = self.evaluate_model(
|
|
1814
|
+
self.bounding_box.cell_centers(), scale=False
|
|
1815
|
+
)
|
|
1816
|
+
return grid, self.stratigraphic_ids()
|
|
1817
|
+
|
|
1818
|
+
def save(
|
|
1819
|
+
self,
|
|
1820
|
+
filename: str,
|
|
1821
|
+
block_model: bool = True,
|
|
1822
|
+
stratigraphic_surfaces=True,
|
|
1823
|
+
fault_surfaces=True,
|
|
1824
|
+
stratigraphic_data=True,
|
|
1825
|
+
fault_data=True,
|
|
1826
|
+
):
|
|
1827
|
+
path = pathlib.Path(filename)
|
|
1828
|
+
extension = path.suffix
|
|
1829
|
+
name = path.stem
|
|
1830
|
+
stratigraphic_surfaces = self.get_stratigraphic_surfaces()
|
|
1831
|
+
if fault_surfaces:
|
|
1832
|
+
for s in self.get_fault_surfaces():
|
|
1833
|
+
## geoh5 can save everything into the same file
|
|
1834
|
+
if extension == ".geoh5" or extension == '.omf':
|
|
1835
|
+
s.save(filename)
|
|
1836
|
+
else:
|
|
1837
|
+
s.save(f'{name}_{s.name}.{extension}')
|
|
1838
|
+
if stratigraphic_surfaces:
|
|
1839
|
+
for s in self.get_stratigraphic_surfaces():
|
|
1840
|
+
if extension == ".geoh5" or extension == '.omf':
|
|
1841
|
+
s.save(filename)
|
|
1842
|
+
else:
|
|
1843
|
+
s.save(f'{name}_{s.name}.{extension}')
|
|
1844
|
+
if block_model:
|
|
1845
|
+
grid, ids = self.get_block_model()
|
|
1846
|
+
if extension == ".geoh5" or extension == '.omf':
|
|
1847
|
+
grid.save(filename)
|
|
1848
|
+
else:
|
|
1849
|
+
grid.save(f'{name}_block_model.{extension}')
|
|
1850
|
+
if stratigraphic_data:
|
|
1851
|
+
if self.stratigraphic_column is not None:
|
|
1852
|
+
for group in self.stratigraphic_column.keys():
|
|
1853
|
+
if group == "faults":
|
|
1854
|
+
continue
|
|
1855
|
+
for data in self.__getitem__(group).get_data():
|
|
1856
|
+
if extension == ".geoh5" or extension == '.omf':
|
|
1857
|
+
data.save(filename)
|
|
1858
|
+
else:
|
|
1859
|
+
data.save(f'{name}_{group}_data.{extension}')
|
|
1860
|
+
if fault_data:
|
|
1861
|
+
for f in self.fault_names():
|
|
1862
|
+
for d in self.__getitem__(f).get_data():
|
|
1863
|
+
if extension == ".geoh5" or extension == '.omf':
|
|
1864
|
+
|
|
1865
|
+
d.save(filename)
|
|
1866
|
+
else:
|
|
1867
|
+
d.save(f'{name}_{group}.{extension}')
|