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.

Files changed (129) hide show
  1. LoopStructural/__init__.py +52 -0
  2. LoopStructural/datasets/__init__.py +23 -0
  3. LoopStructural/datasets/_base.py +301 -0
  4. LoopStructural/datasets/_example_models.py +10 -0
  5. LoopStructural/datasets/data/claudius.csv +21049 -0
  6. LoopStructural/datasets/data/claudiusbb.txt +2 -0
  7. LoopStructural/datasets/data/duplex.csv +126 -0
  8. LoopStructural/datasets/data/duplexbb.txt +2 -0
  9. LoopStructural/datasets/data/fault_trace/fault_trace.cpg +1 -0
  10. LoopStructural/datasets/data/fault_trace/fault_trace.dbf +0 -0
  11. LoopStructural/datasets/data/fault_trace/fault_trace.prj +1 -0
  12. LoopStructural/datasets/data/fault_trace/fault_trace.shp +0 -0
  13. LoopStructural/datasets/data/fault_trace/fault_trace.shx +0 -0
  14. LoopStructural/datasets/data/geological_map_data/bbox.csv +2 -0
  15. LoopStructural/datasets/data/geological_map_data/contacts.csv +657 -0
  16. LoopStructural/datasets/data/geological_map_data/fault_displacement.csv +7 -0
  17. LoopStructural/datasets/data/geological_map_data/fault_edges.txt +2 -0
  18. LoopStructural/datasets/data/geological_map_data/fault_locations.csv +79 -0
  19. LoopStructural/datasets/data/geological_map_data/fault_orientations.csv +19 -0
  20. LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv +13 -0
  21. LoopStructural/datasets/data/geological_map_data/stratigraphic_orientations.csv +207 -0
  22. LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv +13 -0
  23. LoopStructural/datasets/data/intrusion.csv +1017 -0
  24. LoopStructural/datasets/data/intrusionbb.txt +2 -0
  25. LoopStructural/datasets/data/onefoldbb.txt +2 -0
  26. LoopStructural/datasets/data/onefolddata.csv +2226 -0
  27. LoopStructural/datasets/data/refolded_bb.txt +2 -0
  28. LoopStructural/datasets/data/refolded_fold.csv +205 -0
  29. LoopStructural/datasets/data/tabular_intrusion.csv +23 -0
  30. LoopStructural/datatypes/__init__.py +4 -0
  31. LoopStructural/datatypes/_bounding_box.py +422 -0
  32. LoopStructural/datatypes/_point.py +166 -0
  33. LoopStructural/datatypes/_structured_grid.py +94 -0
  34. LoopStructural/datatypes/_surface.py +184 -0
  35. LoopStructural/export/exporters.py +554 -0
  36. LoopStructural/export/file_formats.py +15 -0
  37. LoopStructural/export/geoh5.py +100 -0
  38. LoopStructural/export/gocad.py +126 -0
  39. LoopStructural/export/omf_wrapper.py +88 -0
  40. LoopStructural/interpolators/__init__.py +105 -0
  41. LoopStructural/interpolators/_api.py +143 -0
  42. LoopStructural/interpolators/_builders.py +149 -0
  43. LoopStructural/interpolators/_cython/__init__.py +0 -0
  44. LoopStructural/interpolators/_discrete_fold_interpolator.py +183 -0
  45. LoopStructural/interpolators/_discrete_interpolator.py +692 -0
  46. LoopStructural/interpolators/_finite_difference_interpolator.py +470 -0
  47. LoopStructural/interpolators/_geological_interpolator.py +380 -0
  48. LoopStructural/interpolators/_interpolator_factory.py +89 -0
  49. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  50. LoopStructural/interpolators/_operator.py +38 -0
  51. LoopStructural/interpolators/_p1interpolator.py +228 -0
  52. LoopStructural/interpolators/_p2interpolator.py +277 -0
  53. LoopStructural/interpolators/_surfe_wrapper.py +174 -0
  54. LoopStructural/interpolators/supports/_2d_base_unstructured.py +340 -0
  55. LoopStructural/interpolators/supports/_2d_p1_unstructured.py +68 -0
  56. LoopStructural/interpolators/supports/_2d_p2_unstructured.py +288 -0
  57. LoopStructural/interpolators/supports/_2d_structured_grid.py +462 -0
  58. LoopStructural/interpolators/supports/_2d_structured_tetra.py +0 -0
  59. LoopStructural/interpolators/supports/_3d_base_structured.py +467 -0
  60. LoopStructural/interpolators/supports/_3d_p2_tetra.py +331 -0
  61. LoopStructural/interpolators/supports/_3d_structured_grid.py +470 -0
  62. LoopStructural/interpolators/supports/_3d_structured_tetra.py +746 -0
  63. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +637 -0
  64. LoopStructural/interpolators/supports/__init__.py +55 -0
  65. LoopStructural/interpolators/supports/_aabb.py +77 -0
  66. LoopStructural/interpolators/supports/_base_support.py +114 -0
  67. LoopStructural/interpolators/supports/_face_table.py +70 -0
  68. LoopStructural/interpolators/supports/_support_factory.py +32 -0
  69. LoopStructural/modelling/__init__.py +29 -0
  70. LoopStructural/modelling/core/__init__.py +0 -0
  71. LoopStructural/modelling/core/geological_model.py +1867 -0
  72. LoopStructural/modelling/features/__init__.py +32 -0
  73. LoopStructural/modelling/features/_analytical_feature.py +79 -0
  74. LoopStructural/modelling/features/_base_geological_feature.py +364 -0
  75. LoopStructural/modelling/features/_cross_product_geological_feature.py +100 -0
  76. LoopStructural/modelling/features/_geological_feature.py +288 -0
  77. LoopStructural/modelling/features/_lambda_geological_feature.py +93 -0
  78. LoopStructural/modelling/features/_region.py +18 -0
  79. LoopStructural/modelling/features/_structural_frame.py +186 -0
  80. LoopStructural/modelling/features/_unconformity_feature.py +83 -0
  81. LoopStructural/modelling/features/builders/__init__.py +5 -0
  82. LoopStructural/modelling/features/builders/_base_builder.py +111 -0
  83. LoopStructural/modelling/features/builders/_fault_builder.py +590 -0
  84. LoopStructural/modelling/features/builders/_folded_feature_builder.py +129 -0
  85. LoopStructural/modelling/features/builders/_geological_feature_builder.py +543 -0
  86. LoopStructural/modelling/features/builders/_structural_frame_builder.py +237 -0
  87. LoopStructural/modelling/features/fault/__init__.py +3 -0
  88. LoopStructural/modelling/features/fault/_fault_function.py +444 -0
  89. LoopStructural/modelling/features/fault/_fault_function_feature.py +82 -0
  90. LoopStructural/modelling/features/fault/_fault_segment.py +505 -0
  91. LoopStructural/modelling/features/fold/__init__.py +9 -0
  92. LoopStructural/modelling/features/fold/_fold.py +167 -0
  93. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +149 -0
  94. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +67 -0
  95. LoopStructural/modelling/features/fold/_foldframe.py +194 -0
  96. LoopStructural/modelling/features/fold/_svariogram.py +188 -0
  97. LoopStructural/modelling/input/__init__.py +2 -0
  98. LoopStructural/modelling/input/fault_network.py +80 -0
  99. LoopStructural/modelling/input/map2loop_processor.py +165 -0
  100. LoopStructural/modelling/input/process_data.py +650 -0
  101. LoopStructural/modelling/input/project_file.py +84 -0
  102. LoopStructural/modelling/intrusions/__init__.py +25 -0
  103. LoopStructural/modelling/intrusions/geom_conceptual_models.py +142 -0
  104. LoopStructural/modelling/intrusions/geometric_scaling_functions.py +123 -0
  105. LoopStructural/modelling/intrusions/intrusion_builder.py +672 -0
  106. LoopStructural/modelling/intrusions/intrusion_feature.py +410 -0
  107. LoopStructural/modelling/intrusions/intrusion_frame_builder.py +971 -0
  108. LoopStructural/modelling/intrusions/intrusion_support_functions.py +460 -0
  109. LoopStructural/utils/__init__.py +38 -0
  110. LoopStructural/utils/_surface.py +143 -0
  111. LoopStructural/utils/_transformation.py +76 -0
  112. LoopStructural/utils/config.py +18 -0
  113. LoopStructural/utils/dtm_creator.py +17 -0
  114. LoopStructural/utils/exceptions.py +31 -0
  115. LoopStructural/utils/helper.py +292 -0
  116. LoopStructural/utils/json_encoder.py +18 -0
  117. LoopStructural/utils/linalg.py +8 -0
  118. LoopStructural/utils/logging.py +79 -0
  119. LoopStructural/utils/maths.py +245 -0
  120. LoopStructural/utils/regions.py +103 -0
  121. LoopStructural/utils/typing.py +7 -0
  122. LoopStructural/utils/utils.py +68 -0
  123. LoopStructural/version.py +1 -0
  124. LoopStructural/visualisation/__init__.py +11 -0
  125. LoopStructural-1.6.1.dist-info/LICENSE +21 -0
  126. LoopStructural-1.6.1.dist-info/METADATA +81 -0
  127. LoopStructural-1.6.1.dist-info/RECORD +129 -0
  128. LoopStructural-1.6.1.dist-info/WHEEL +5 -0
  129. 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}')