LoopStructural 1.0.9__cp36-cp36m-win32.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.
Files changed (77) hide show
  1. LoopStructural/__init__.py +36 -0
  2. LoopStructural/analysis/__init__.py +2 -0
  3. LoopStructural/analysis/_fault_displacement.py +67 -0
  4. LoopStructural/analysis/_fault_intersection.py +36 -0
  5. LoopStructural/analysis/_plane_fit.py +43 -0
  6. LoopStructural/datasets/__init__.py +12 -0
  7. LoopStructural/datasets/_base.py +65 -0
  8. LoopStructural/datasets/data/claudius.csv +21049 -0
  9. LoopStructural/datasets/data/claudiusbb.txt +2 -0
  10. LoopStructural/datasets/data/duplex.csv +126 -0
  11. LoopStructural/datasets/data/duplexbb.txt +2 -0
  12. LoopStructural/datasets/data/intrusion.csv +1017 -0
  13. LoopStructural/datasets/data/intrusionbb.txt +2 -0
  14. LoopStructural/datasets/data/onefoldbb.txt +2 -0
  15. LoopStructural/datasets/data/onefolddata.csv +2226 -0
  16. LoopStructural/datasets/data/refolded_bb.txt +2 -0
  17. LoopStructural/datasets/data/refolded_fold.csv +2126 -0
  18. LoopStructural/interpolators/__init__.py +34 -0
  19. LoopStructural/interpolators/base_structured_3d_support.py +101 -0
  20. LoopStructural/interpolators/cython/__init__.py +0 -0
  21. LoopStructural/interpolators/cython/dsi_helper.c +31046 -0
  22. LoopStructural/interpolators/cython/dsi_helper.cp36-win32.pyd +0 -0
  23. LoopStructural/interpolators/discrete_fold_interpolator.py +202 -0
  24. LoopStructural/interpolators/discrete_interpolator.py +584 -0
  25. LoopStructural/interpolators/finite_difference_interpolator.py +399 -0
  26. LoopStructural/interpolators/geological_interpolator.py +197 -0
  27. LoopStructural/interpolators/operator.py +61 -0
  28. LoopStructural/interpolators/piecewiselinear_interpolator.py +437 -0
  29. LoopStructural/interpolators/structured_grid.py +421 -0
  30. LoopStructural/interpolators/structured_tetra.py +681 -0
  31. LoopStructural/interpolators/surfe_wrapper.py +118 -0
  32. LoopStructural/modelling/__init__.py +46 -0
  33. LoopStructural/modelling/core/__init__.py +0 -0
  34. LoopStructural/modelling/core/geological_model.py +1513 -0
  35. LoopStructural/modelling/core/geological_model_graph.py +880 -0
  36. LoopStructural/modelling/core/stratigraphic_column.py +5 -0
  37. LoopStructural/modelling/fault/__init__.py +4 -0
  38. LoopStructural/modelling/fault/fault_builder.py +143 -0
  39. LoopStructural/modelling/fault/fault_function.py +188 -0
  40. LoopStructural/modelling/fault/fault_function_feature.py +76 -0
  41. LoopStructural/modelling/fault/fault_segment.py +298 -0
  42. LoopStructural/modelling/features/__init__.py +8 -0
  43. LoopStructural/modelling/features/cross_product_geological_feature.py +90 -0
  44. LoopStructural/modelling/features/geological_feature.py +252 -0
  45. LoopStructural/modelling/features/geological_feature_builder.py +414 -0
  46. LoopStructural/modelling/features/lambda_geological_feature.py +36 -0
  47. LoopStructural/modelling/features/region_feature.py +34 -0
  48. LoopStructural/modelling/features/structural_frame.py +133 -0
  49. LoopStructural/modelling/features/structural_frame_builder.py +206 -0
  50. LoopStructural/modelling/features/unconformity_feature.py +74 -0
  51. LoopStructural/modelling/fold/__init__.py +8 -0
  52. LoopStructural/modelling/fold/fold.py +143 -0
  53. LoopStructural/modelling/fold/fold_rotation_angle.py +133 -0
  54. LoopStructural/modelling/fold/fold_rotation_angle_feature.py +58 -0
  55. LoopStructural/modelling/fold/foldframe.py +193 -0
  56. LoopStructural/modelling/fold/svariogram.py +180 -0
  57. LoopStructural/utils/__init__.py +18 -0
  58. LoopStructural/utils/bounding_box.py +21 -0
  59. LoopStructural/utils/exceptions.py +10 -0
  60. LoopStructural/utils/helper.py +382 -0
  61. LoopStructural/utils/logging.py +60 -0
  62. LoopStructural/utils/map2loop.py +353 -0
  63. LoopStructural/utils/regions.py +11 -0
  64. LoopStructural/utils/utils.py +69 -0
  65. LoopStructural/visualisation/__init__.py +31 -0
  66. LoopStructural/visualisation/map_viewer.py +322 -0
  67. LoopStructural/visualisation/model_plotter.py +17 -0
  68. LoopStructural/visualisation/model_visualisation.py +1113 -0
  69. LoopStructural/visualisation/rotation_angle_plotter.py +83 -0
  70. LoopStructural/visualisation/sphinx_scraper.py +34 -0
  71. LoopStructural/visualisation/stratigraphic_column.py +60 -0
  72. LoopStructural-1.0.9.dist-info/LICENSE +21 -0
  73. LoopStructural-1.0.9.dist-info/METADATA +19 -0
  74. LoopStructural-1.0.9.dist-info/RECORD +77 -0
  75. LoopStructural-1.0.9.dist-info/WHEEL +5 -0
  76. LoopStructural-1.0.9.dist-info/top_level.txt +2 -0
  77. tests/__init__.py +0 -0
@@ -0,0 +1,1513 @@
1
+ """
2
+ Main entry point for creating a geological model
3
+ """
4
+ import logging
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from LoopStructural.datasets import normal_vector_headers
9
+ from LoopStructural.interpolators.discrete_fold_interpolator import \
10
+ DiscreteFoldInterpolator as DFI
11
+ from LoopStructural.interpolators.finite_difference_interpolator import \
12
+ FiniteDifferenceInterpolator as FDI
13
+ from LoopStructural.interpolators.piecewiselinear_interpolator import \
14
+ PiecewiseLinearInterpolator as PLI
15
+
16
+
17
+ from LoopStructural.interpolators.structured_grid import StructuredGrid
18
+ from LoopStructural.interpolators.structured_tetra import TetMesh
19
+ from LoopStructural.modelling.fault.fault_segment import FaultSegment
20
+ from LoopStructural.modelling.fault import FaultBuilder
21
+ from LoopStructural.modelling.features import (GeologicalFeatureInterpolator,
22
+ RegionFeature,
23
+ StructuralFrameBuilder,
24
+ UnconformityFeature)
25
+ from LoopStructural.modelling.fold import FoldRotationAngle
26
+ from LoopStructural.modelling.fold.fold import FoldEvent
27
+ from LoopStructural.modelling.fold.foldframe import FoldFrame
28
+ from LoopStructural.utils.exceptions import LoopBaseException
29
+ from LoopStructural.utils.helper import (all_heading, gradient_vec_names,
30
+ strike_dip_vector)
31
+
32
+ from LoopStructural.utils import getLogger, log_to_file
33
+ logger = getLogger(__name__)
34
+
35
+
36
+
37
+ def _calculate_average_intersection(series_builder, fold_frame, fold,
38
+ **kwargs):
39
+ """
40
+
41
+ Parameters
42
+ ----------
43
+ series_builder
44
+ fold_frame
45
+ fold
46
+
47
+ Returns
48
+ -------
49
+
50
+ """
51
+ l2 = fold_frame.calculate_intersection_lineation(
52
+ series_builder)
53
+ fold.fold_axis = np.mean(l2, axis=0)
54
+
55
+
56
+ class GeologicalModel:
57
+ """
58
+ A geological model is the recipe for building a 3D model and can include
59
+ the rescaling of the model between 0 and 1.
60
+
61
+ Attributes
62
+ ----------
63
+ features : list
64
+ Contains all features youngest to oldest
65
+ feature_name_index : dict
66
+ maps feature name to the list index of the features
67
+ data : pandas dataframe
68
+ the dataframe used for building the geological model
69
+ nsteps : tuple/np.array(3,dtype=int)
70
+ the number of steps x,y,z to evaluate the model
71
+ origin : tuple/np.array(3,dtype=doubles)
72
+ the origin of the model box
73
+ parameters : dict
74
+ a dictionary tracking the parameters used to build the model
75
+ scale_factor : double
76
+ the scale factor used to rescale the model
77
+
78
+
79
+ """
80
+ def __init__(self, origin, maximum, rescale=True, nsteps=(50, 50, 25),
81
+ reuse_supports=False, logfile=None, loglevel='info'):
82
+ """
83
+ Parameters
84
+ ----------
85
+ origin : numpy array
86
+ specifying the origin of the model
87
+ maximum : numpy array
88
+ specifying the maximum extent of the model
89
+ rescale : bool
90
+ whether to rescale the model to between 0/1
91
+
92
+ Examples
93
+ --------
94
+ Demo data
95
+
96
+ >>> from LoopStructural.datasets import load_claudius
97
+ >>> from LoopStructural import GeologicalModel
98
+
99
+ >>> data, bb = load_claudius()
100
+
101
+ >>> model = GeologicalModel(bb[:,0],bb[:,1]
102
+ >>> model.set_model_data(data)
103
+ >>> model.create_and_add_foliation('strati')
104
+
105
+ >>> y = np.linspace(model.bounding_box[0, 1], model.bounding_box[1, 1],
106
+ nsteps[1])
107
+ >>> z = np.linspace(model.bounding_box[1, 2], model.bounding_box[0, 2],
108
+ nsteps[2])
109
+ >>> xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
110
+ >>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
111
+ >>> model.evaluate_feature_value('strati',xyz,scale=False)
112
+
113
+
114
+ """
115
+ # print('tet')
116
+ if logfile:
117
+ self.logfile = logfile
118
+ log_to_file(logfile,loglevel)
119
+
120
+ logger.info('Initialising geological model')
121
+ self.features = []
122
+ self.feature_name_index = {}
123
+ self.data = None
124
+ self.nsteps = nsteps
125
+ self._str = 'Instance of LoopStructural.GeologicalModel \n'
126
+ self._str += '------------------------------------------ \n'
127
+ # we want to rescale the model area so that the maximum length is
128
+ # 1
129
+ self.origin = np.array(origin).astype(float)
130
+ originstr = 'Model origin: {} {} {}'.format(self.origin[0],self.origin[1],self.origin[2])
131
+ logger.info(originstr)
132
+ self._str+=originstr+'\n'
133
+ self.maximum = np.array(maximum).astype(float)
134
+ maximumstr = 'Model maximum: {} {} {}'.format(self.maximum[0],self.maximum[1],self.maximum[2])
135
+ logger.info(maximumstr)
136
+ self._str+=maximumstr+'\n'
137
+
138
+ lengths = self.maximum - self.origin
139
+ self.scale_factor = 1.
140
+ self.bounding_box = np.zeros((2, 3))
141
+ self.bounding_box[1, :] = self.maximum - self.origin
142
+ self.bounding_box[1, :] = self.maximum - self.origin
143
+ if rescale:
144
+ self.scale_factor = float(np.max(lengths))
145
+ logger.info('Rescaling model using scale factor {}'.format(self.scale_factor))
146
+ self._str+='Model rescale factor: {} \n'.format(self.scale_factor)
147
+ self._str+='The model contains {} GeologicalFeatures \n'.format(len(self.features))
148
+ self._str+=''
149
+ self._str += '------------------------------------------ \n'
150
+ self._str += ''
151
+ self.bounding_box /= self.scale_factor
152
+ self.support = {}
153
+ self.reuse_supports = reuse_supports
154
+ if self.reuse_supports:
155
+ logger.warning("Supports are shared between geological features \n"
156
+ "this may cause unexpected behaviour and should only\n"
157
+ "be use by advanced users")
158
+ logger.info('Reusing interpolation supports: {}'.format(self.reuse_supports))
159
+ self.stratigraphic_column = None
160
+ self.parameters = {'features': [], 'model': {'bounding_box': self.origin.tolist() + self.maximum.tolist(),
161
+ 'rescale': rescale,
162
+ 'nsteps': nsteps,
163
+ 'reuse_supports': reuse_supports}}
164
+
165
+ def __str__(self):
166
+ return self._str
167
+
168
+ def _ipython_key_completions_(self):
169
+ return self.feature_name_index.keys()
170
+
171
+ @classmethod
172
+ def from_map2loop_directory(cls, m2l_directory,**kwargs):
173
+ """Alternate constructor for a geological model using m2l output
174
+
175
+ Uses the information saved in the map2loop files to build a geological model.
176
+ You can specify kwargs for building foliation using foliation_params and for
177
+ faults using fault_params. faults is a flag that allows for the faults to be skipped.
178
+
179
+ Parameters
180
+ ----------
181
+ m2l_directory : string
182
+ path to map2loop directory
183
+
184
+ Returns
185
+ -------
186
+ (GeologicalModel, dict)
187
+ the created geological model and a dictionary of the map2loop data
188
+ """
189
+ from LoopStructural.utils import build_model, process_map2loop
190
+ logger.info('LoopStructural model initialised from m2l directory: {}'.format(m2l_directory))
191
+ m2lflags = kwargs.pop('m2lflags',{})
192
+ m2l_data = process_map2loop(m2l_directory,m2lflags)
193
+ return build_model(m2l_data,**kwargs), m2l_data
194
+
195
+ @classmethod
196
+ def from_file(cls, file):
197
+ """Load a geological model from file
198
+
199
+ Parameters
200
+ ----------
201
+ file : string
202
+ path to the file
203
+
204
+ Returns
205
+ -------
206
+ GeologicalModel
207
+ the geological model object
208
+ """
209
+ try:
210
+ import dill as pickle
211
+ except ImportError:
212
+ logger.error("Cannot import from file, dill not installed")
213
+ return None
214
+ model = pickle.load(open(file,'rb'))
215
+ if type(model) == GeologicalModel:
216
+ logger.info('GeologicalModel initialised from file')
217
+ return model
218
+ else:
219
+ logger.error('{} does not contain a geological model'.format(file))
220
+ return None
221
+
222
+ def __getitem__(self, feature_name):
223
+ """Accessor for feature in features using feature_name_index
224
+
225
+ Parameters
226
+ ----------
227
+ feature_name : string
228
+ name of the feature to return
229
+ """
230
+ return self.get_feature_by_name(feature_name)
231
+
232
+ def feature_names(self):
233
+ return self.feature_name_index.keys()
234
+
235
+ def fault_names(self):
236
+ pass
237
+
238
+ def check_inialisation(self):
239
+ if self.data is None:
240
+ logger.error("Data not associated with GeologicalModel. Run set_data")
241
+ return False
242
+
243
+ def to_file(self, file):
244
+ """Save a model to a pickle file requires dill
245
+
246
+ Parameters
247
+ ----------
248
+ file : string
249
+ path to file location
250
+ """
251
+ try:
252
+ import dill as pickle
253
+ except ImportError:
254
+ logger.error("Cannot write to file, dill not installed \n"
255
+ "pip install dill")
256
+ return
257
+ try:
258
+ logger.info('Writing GeologicalModel to: {}'.format(file))
259
+ pickle.dump(self,open(file,'wb'))
260
+ except pickle.PicklingError:
261
+ logger.error('Error saving file')
262
+
263
+ def _add_feature(self, feature):
264
+ """
265
+ Add a feature to the model stack
266
+
267
+ Parameters
268
+ ----------
269
+ feature : GeologicalFeature
270
+ the geological feature to add
271
+
272
+ """
273
+
274
+ if feature.name in self.feature_name_index:
275
+ logger.info("Feature %s already exists at %i, overwriting" %
276
+ (feature.name, self.feature_name_index[feature.name]))
277
+ self.features[self.feature_name_index[feature.name]] = feature
278
+ else:
279
+ self._str += 'GeologicalFeature: {} of type - {} \n'.format(feature.name,feature.type)
280
+ self.features.append(feature)
281
+ self.feature_name_index[feature.name] = len(self.features) - 1
282
+ logger.info("Adding %s to model at location %i" % (
283
+ feature.name, len(self.features)))
284
+ self._add_domain_fault_above(feature)
285
+ self._add_unconformity_above(feature)
286
+ feature.set_model(self)
287
+
288
+ def data_for_feature(self,feature_name):
289
+ return self.data.loc[self.data['feature_name'] == feature_name,:]
290
+
291
+ def set_model_data(self, data):
292
+ """
293
+ Set the data array for the model
294
+
295
+ Parameters
296
+ ----------
297
+ data : pandas data frame
298
+ with column headers corresponding to the
299
+ type, X, Y, Z, nx, ny, nz, val, strike, dip, dip_dir, plunge,
300
+ plunge_dir, azimuth
301
+
302
+ Returns
303
+ -------
304
+ Note
305
+ ----
306
+ Type can be any unique identifier for the feature the data point
307
+ 'eg' 'S0', 'S2', 'F1_axis'
308
+ it is then used by the create functions to get the correct data
309
+ """
310
+ if type(data) != pd.DataFrame:
311
+ logger.warning(
312
+ "Data is not a pandas data frame, trying to read data frame "
313
+ "from csv")
314
+ try:
315
+ data = pd.read_csv(data)
316
+ except:
317
+ logger.error("Could not load pandas data frame from data")
318
+ logger.info('Adding data to GeologicalModel with {} data points'.format(len(data)))
319
+ self.data = data.copy()
320
+ self.data['X'] -= self.origin[0]
321
+ self.data['Y'] -= self.origin[1]
322
+ self.data['Z'] -= self.origin[2]
323
+ self.data['X'] /= self.scale_factor
324
+ self.data['Y'] /= self.scale_factor
325
+ self.data['Z'] /= self.scale_factor
326
+ if 'type' in self.data:
327
+ logger.warning("'type' is depreciated replace with 'feature_name' \n")
328
+ self.data.rename(columns={'type':'feature_name'},inplace=True)
329
+ for h in all_heading():
330
+ if h not in self.data:
331
+ self.data[h] = np.nan
332
+ if h == 'w':
333
+ self.data[h] = 1.
334
+ if h == 'coord':
335
+ self.data[h] = 0
336
+
337
+ if 'strike' in self.data and 'dip' in self.data:
338
+ logger.info('Converting strike and dip to vectors')
339
+ mask = np.all(~np.isnan(self.data.loc[:, ['strike', 'dip']]),
340
+ axis=1)
341
+ self.data.loc[mask, gradient_vec_names()] = strike_dip_vector(
342
+ self.data.loc[mask, 'strike'], self.data.loc[mask, 'dip'])
343
+ self.data.drop(['strike', 'dip'], axis=1, inplace=True)
344
+ # self.data.loc
345
+ # if 'nx' in self.data and 'ny' in self.data and 'nz' in self.data:
346
+ # mask = np.all(~np.isnan(self.data.loc[:, ['nx', 'ny','nz']]),
347
+ # axis=1)
348
+ # self.data.loc[mask,['nx', 'ny','nz']] /= self.scale_factor
349
+ def extend_model_data(self, newdata):
350
+ """
351
+ Extends the data frame
352
+
353
+ Parameters
354
+ ----------
355
+ newdata : pandas data frame
356
+ data to add to the existing dataframe
357
+ Returns
358
+ -------
359
+ """
360
+ logger.warning("Extend data is untested and may have unexpected consequences")
361
+ data_temp = newdata.copy()
362
+ data_temp['X'] -= self.origin[0]
363
+ data_temp['Y'] -= self.origin[1]
364
+ data_temp['Z'] -= self.origin[2]
365
+ data_temp['X'] /= self.scale_factor
366
+ data_temp['Y'] /= self.scale_factor
367
+ data_temp['Z'] /= self.scale_factor
368
+ self.data.concat([self.data, data_temp], sort=True)
369
+
370
+ def set_stratigraphic_column(self, stratigraphic_column,cmap='tab20'):
371
+ """
372
+ Adds a stratigraphic column to the model
373
+
374
+ Parameters
375
+ ----------
376
+ stratigraphic_column : dictionary
377
+ cmap : matplotlib.cmap
378
+ Returns
379
+ -------
380
+
381
+ Notes
382
+ -----
383
+ stratigraphic_column is a nested dictionary with the format
384
+ {'group':
385
+ {'series1':
386
+ {'min':0., 'max':10.,'id':0,'colour':}
387
+ }
388
+ }
389
+
390
+ """
391
+ # if the colour for a unit hasn't been specified we can just sample from
392
+ # a colour map e.g. tab20
393
+ logger.info('Adding stratigraphic column to model')
394
+ random_colour = True
395
+ n_units=0
396
+ for g in stratigraphic_column.keys():
397
+ for u in stratigraphic_column[g].keys():
398
+ if 'colour' in stratigraphic_column[g][u]:
399
+ random_colour = False
400
+ break
401
+ n_units+=1
402
+ if random_colour:
403
+ import matplotlib.cm as cm
404
+ cmap = cm.get_cmap(cmap,n_units)
405
+ cmap_colours = cmap.colors
406
+ ci = 0
407
+ for g in stratigraphic_column.keys():
408
+ for u in stratigraphic_column[g].keys():
409
+ stratigraphic_column[g][u]['colour'] = cmap_colours[ci,:]
410
+
411
+ self.stratigraphic_column = stratigraphic_column
412
+
413
+ def create_from_feature_list(self, features):
414
+ """Initialises a model from a dictionary containing the features
415
+
416
+ Parameters
417
+ ----------
418
+ features : [type]
419
+ [description]
420
+
421
+ Raises
422
+ ------
423
+ LoopBaseException
424
+ [description]
425
+ """
426
+ for f in features:
427
+ featuretype = f.pop('featuretype', None)
428
+ if featuretype is None:
429
+ raise LoopBaseException
430
+ if featuretype == 'strati':
431
+ self.create_and_add_foliation(f)
432
+ # if featuretype == 'fault':
433
+ # self.create_and_add_fault(f)
434
+ if featuretype == 'folded_strati':
435
+ self.create_and_add_folded_foliation(f)
436
+
437
+ def get_interpolator(self, interpolatortype='PLI', nelements=1e5,
438
+ buffer=0.2, element_volume = None, **kwargs):
439
+ """
440
+ Returns an interpolator given the arguments, also constructs a
441
+ support for a discrete interpolator
442
+
443
+ Parameters
444
+ ----------
445
+ interpolatortype : string
446
+ define the interpolator type
447
+ nelements : int
448
+ number of elements in the interpolator
449
+ buffer : double or numpy array 3x1
450
+ value(s) between 0,1 specifying the buffer around the bounding box
451
+ data_bb : bool
452
+ whether to use the model boundary or the boundary around
453
+ kwargs : no kwargs used, this just catches any additional arguments
454
+
455
+ Returns
456
+ -------
457
+ """
458
+ # get an interpolator for
459
+ interpolator = None
460
+ bb = np.copy(self.bounding_box)
461
+ # add a buffer to the interpolation domain, this is necessary for
462
+ # faults but also generally a good
463
+ # idea to avoid boundary problems
464
+ # buffer = bb[1, :]
465
+ buffer = (bb[1,:]-bb[0,:])*buffer
466
+ bb[0, :] -= buffer # *(bb[1,:]-bb[0,:])
467
+ bb[1, :] += buffer # *(bb[1,:]-bb[0,:])
468
+ box_vol = (bb[1, 0]-bb[0, 0]) * (bb[1, 1]-bb[0, 1]) * (bb[1, 2]-bb[0, 2])
469
+ if interpolatortype == "PLI":
470
+ if element_volume is None:
471
+ nelements /= 5
472
+ element_volume = box_vol / nelements
473
+ # calculate the step vector of a regular cube
474
+ step_vector = np.zeros(3)
475
+ step_vector[:] = element_volume ** (1. / 3.)
476
+ # step_vector /= np.array([1,1,2])
477
+ # number of steps is the length of the box / step vector
478
+ nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
479
+ # create a structured grid using the origin and number of steps
480
+ if self.reuse_supports:
481
+ mesh_id = 'mesh_{}'.format(nelements)
482
+ mesh = self.support.get(mesh_id,
483
+ TetMesh(origin=bb[0, :], nsteps=nsteps,
484
+ step_vector=step_vector))
485
+ if mesh_id not in self.support:
486
+ self.support[mesh_id] = mesh
487
+ else:
488
+ mesh = TetMesh(origin=bb[0, :], nsteps=nsteps, step_vector=step_vector)
489
+ logger.info("Creating regular tetrahedron mesh with %i elements \n"
490
+ "for modelling using PLI" % (mesh.ntetra))
491
+
492
+ return PLI(mesh)
493
+
494
+ if interpolatortype == 'FDI':
495
+
496
+ # find the volume of one element
497
+ if element_volume is None:
498
+ element_volume = box_vol / nelements
499
+ # calculate the step vector of a regular cube
500
+ step_vector = np.zeros(3)
501
+ step_vector[:] = element_volume ** (1. / 3.)
502
+ # number of steps is the length of the box / step vector
503
+ nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
504
+ if np.any(np.less(nsteps, 3)):
505
+ logger.error("Cannot create interpolator: number of steps is too small")
506
+ return None
507
+ # create a structured grid using the origin and number of steps
508
+ if self.reuse_supports:
509
+ grid_id = 'grid_{}'.format(nelements)
510
+ grid = self.support.get(grid_id, StructuredGrid(origin=bb[0, :],
511
+ nsteps=nsteps,
512
+ step_vector=step_vector))
513
+ if grid_id not in self.support:
514
+ self.support[grid_id] = grid
515
+ else:
516
+ grid = StructuredGrid(origin=bb[0, :], nsteps=nsteps,step_vector=step_vector)
517
+ logger.info("Creating regular grid with %i elements \n"
518
+ "for modelling using FDI" % grid.n_elements)
519
+ return FDI(grid)
520
+
521
+ if interpolatortype == "DFI": # "fold" in kwargs:
522
+ if element_volume is None:
523
+ nelements /= 5
524
+ element_volume = box_vol / nelements
525
+ # calculate the step vector of a regular cube
526
+ step_vector = np.zeros(3)
527
+ step_vector[:] = element_volume ** (1. / 3.)
528
+ # number of steps is the length of the box / step vector
529
+ nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
530
+ # create a structured grid using the origin and number of steps
531
+ mesh = kwargs.get('mesh', TetMesh(origin=bb[0, :], nsteps=nsteps,
532
+ step_vector=step_vector))
533
+ logger.info("Creating regular tetrahedron mesh with %i elements \n"
534
+ "for modelling using DFI" % mesh.ntetra)
535
+ return DFI(mesh, kwargs['fold'])
536
+ if interpolatortype == 'Surfe' or interpolatortype == 'surfe':
537
+ # move import of surfe to where we actually try and use it
538
+ try:
539
+ from LoopStructural.interpolators.surfe_wrapper import \
540
+ SurfeRBFInterpolator as Surfe
541
+
542
+ surfe = True
543
+
544
+ except ImportError:
545
+ surfe = False
546
+ if not surfe:
547
+ logger.warning("Cannot import Surfe, try another interpolator")
548
+ raise ImportError
549
+ method = kwargs.get('method', 'single_surface')
550
+ logger.info("Using surfe interpolator")
551
+ return Surfe(method)
552
+ logger.warning("No interpolator")
553
+ return interpolator
554
+
555
+ def create_and_add_foliation(self, series_surface_data, **kwargs):
556
+ """
557
+ Parameters
558
+ ----------
559
+ series_surface_data : string
560
+ corresponding to the feature_name in the data
561
+ kwargs
562
+
563
+ Returns
564
+ -------
565
+ feature : GeologicalFeature
566
+ the created geological feature
567
+ """
568
+ if self.check_inialisation() == False:
569
+ return False
570
+ self.parameters['features'].append({'feature_type': 'foliation', 'feature_name': series_surface_data, **kwargs})
571
+ interpolator = self.get_interpolator(**kwargs)
572
+ series_builder = GeologicalFeatureInterpolator(interpolator,
573
+ name=series_surface_data,
574
+ **kwargs)
575
+ # add data
576
+ series_data = self.data[self.data['feature_name'] == series_surface_data]
577
+ if series_data.shape[0] == 0:
578
+ logger.warning("No data for %s, skipping" % series_surface_data)
579
+ return
580
+ series_builder.add_data_from_data_frame(series_data)
581
+ self._add_faults(series_builder)
582
+
583
+ # build feature
584
+ # series_feature = series_builder.build(**kwargs)
585
+ series_feature = series_builder.feature
586
+ series_builder.build_arguments = kwargs
587
+ series_feature.type = 'series'
588
+ # see if any unconformities are above this feature if so add region
589
+ # self._add_unconformity_above(series_feature)self._add_feature(series_feature)
590
+ self._add_feature(series_feature)
591
+ return series_feature
592
+
593
+ def create_and_add_dtm(self, series_surface_data, **kwargs):
594
+ """
595
+ Parameters
596
+ ----------
597
+ series_surface_data : string
598
+ corresponding to the feature_name in the data
599
+ kwargs
600
+
601
+ Returns
602
+ -------
603
+ feature : GeologicalFeature
604
+ the created geological feature
605
+ """
606
+ if self.check_inialisation() == False:
607
+ return False
608
+ self.parameters['features'].append({'feature_type': 'foliation', 'feature_name': series_surface_data, **kwargs})
609
+ interpolator = self.get_interpolator(**kwargs)
610
+ series_builder = GeologicalFeatureInterpolator(interpolator,
611
+ name=series_surface_data,
612
+ **kwargs)
613
+ # add data
614
+ series_data = self.data[self.data['feature_name'] == series_surface_data]
615
+ if series_data.shape[0] == 0:
616
+ logger.warning("No data for %s, skipping" % series_surface_data)
617
+ return
618
+ series_builder.add_data_from_data_frame(series_data)
619
+ # self._add_faults(series_builder)
620
+
621
+ # build feature
622
+ # series_feature = series_builder.build(**kwargs)
623
+ series_feature = series_builder.feature
624
+ series_builder.build_arguments = kwargs
625
+ series_feature.type = 'dtm'
626
+ # see if any unconformities are above this feature if so add region
627
+ # self._add_unconformity_above(series_feature)self._add_feature(series_feature)
628
+ self._add_feature(series_feature)
629
+ return series_feature
630
+
631
+ def create_and_add_fold_frame(self, foldframe_data, **kwargs):
632
+ """
633
+ Parameters
634
+ ----------
635
+ foldframe_data : string
636
+ unique string in feature_name column
637
+
638
+ kwargs
639
+
640
+ Returns
641
+ -------
642
+ fold_frame : FoldFrame
643
+ the created fold frame
644
+ """
645
+ if self.check_inialisation() == False:
646
+ return False
647
+ self.parameters['features'].append({'feature_type': 'fold_frame', 'feature_name': foldframe_data, **kwargs})
648
+ # create fault frame
649
+ interpolator = self.get_interpolator(**kwargs)
650
+ #
651
+ fold_frame_builder = StructuralFrameBuilder(interpolator,
652
+ name=foldframe_data,
653
+ **kwargs)
654
+ # add data
655
+ fold_frame_data = self.data[self.data['feature_name'] == foldframe_data]
656
+ fold_frame_builder.add_data_from_data_frame(fold_frame_data)
657
+ self._add_faults(fold_frame_builder[0])
658
+ self._add_faults(fold_frame_builder[1])
659
+ self._add_faults(fold_frame_builder[2])
660
+
661
+ fold_frame = fold_frame_builder.build(frame=FoldFrame, **kwargs)
662
+ # for i in range(3):
663
+ # self._add_unconformity_above(fold_frame[i])
664
+ fold_frame.type = 'structuralframe'
665
+ self._add_feature(fold_frame)
666
+
667
+ return fold_frame
668
+
669
+ def create_and_add_folded_foliation(self, foliation_data, fold_frame=None, svario=True,
670
+ **kwargs):
671
+ """
672
+ Create a folded foliation field from data and a fold frame
673
+
674
+ Parameters
675
+ ----------
676
+ foliation_data : str
677
+ unique string in type column of data frame
678
+ fold_frame : FoldFrame
679
+ svario : Boolean
680
+ whether to calculate svariograms, saves time if avoided
681
+ kwargs
682
+ additional kwargs to be passed through to other functions
683
+
684
+ Returns
685
+ -------
686
+ feature : GeologicalFeature
687
+ created geological feature
688
+ """
689
+ if self.check_inialisation() == False:
690
+ return False
691
+ self.parameters['features'].append(
692
+ {'feature_type': 'fold_foliation', 'feature_name': foliation_data, 'fold_frame': fold_frame, **kwargs})
693
+ if fold_frame is None:
694
+ logger.info("Using last feature as fold frame")
695
+ fold_frame = self.features[-1]
696
+ assert type(fold_frame) == FoldFrame, "Please specify a FoldFrame"
697
+ fold = FoldEvent(fold_frame,name='Fold_{}'.format(foliation_data))
698
+ fold_interpolator = self.get_interpolator("DFI", fold=fold, **kwargs)
699
+ series_builder = GeologicalFeatureInterpolator(
700
+ interpolator=fold_interpolator,
701
+ name=foliation_data)
702
+
703
+ series_builder.add_data_from_data_frame(
704
+ self.data[self.data['feature_name'] == foliation_data])
705
+ self._add_faults(series_builder)
706
+ series_builder.add_data_to_interpolator(True)
707
+ fold_axis = kwargs.get('fold_axis',None)
708
+ if fold_axis is not None:
709
+ fold_axis = np.array(fold_axis)
710
+ if len(fold_axis.shape) == 1:
711
+ fold.fold_axis = fold_axis
712
+
713
+ if "av_fold_axis" in kwargs:
714
+ _calculate_average_intersection(series_builder, fold_frame, fold)
715
+ if fold.fold_axis is None:
716
+ far, fad = fold_frame.calculate_fold_axis_rotation(
717
+ series_builder)
718
+ fold_axis_rotation = FoldRotationAngle(far, fad,svario=svario)
719
+ a_wl = kwargs.get("axis_wl", None)
720
+ if 'axis_function' in kwargs:
721
+ # allow predefined function to be used
722
+ fold_axis_rotation.set_function(kwargs['axis_function'])
723
+ else:
724
+ fold_axis_rotation.fit_fourier_series(wl=a_wl)
725
+ fold.fold_axis_rotation = fold_axis_rotation
726
+ # give option of passing own fold limb rotation function
727
+ flr, fld = fold_frame.calculate_fold_limb_rotation(
728
+ series_builder)
729
+ fold_limb_rotation = FoldRotationAngle(flr, fld,svario=svario)
730
+ l_wl = kwargs.get("limb_wl", None)
731
+ if 'limb_function' in kwargs:
732
+ # allow for predefined functions to be used
733
+ fold_limb_rotation.set_function(kwargs['limb_function'])
734
+ else:
735
+ fold_limb_rotation.fit_fourier_series(wl=l_wl,**kwargs)
736
+ fold.fold_limb_rotation = fold_limb_rotation
737
+ # fold_limb_fitter = kwargs.get("fold_limb_function",
738
+ # _interpolate_fold_limb_rotation_angle)
739
+ # fold_limb_fitter(series_builder, fold_frame, fold, result, **kwargs)
740
+ kwargs['fold_weights'] = kwargs.get('fold_weights', {})
741
+
742
+ self._add_faults(series_builder)
743
+ # build feature
744
+ kwargs['cgw'] = 0.
745
+ kwargs['fold'] = fold
746
+ # series_feature = series_builder.build(**kwargs)
747
+ series_feature = series_builder.feature
748
+ series_builder.build_arguments = kwargs
749
+ series_feature.type = 'series'
750
+ series_feature.fold = fold
751
+ # see if any unconformities are above this feature if so add region
752
+ # self._add_unconformity_above(series_feature)self._add_feature(series_feature)
753
+ # result['support'] = series_feature.get_interpolator().support
754
+ self._add_feature(series_feature)
755
+ return series_feature
756
+
757
+ def create_and_add_folded_fold_frame(self, fold_frame_data,
758
+ fold_frame=None,
759
+ **kwargs):
760
+ """
761
+
762
+ Parameters
763
+ ----------
764
+ fold_frame_data : string
765
+ name of the feature to be added
766
+
767
+ fold_frame : StructuralFrame, optional
768
+ the fold frame for the fold if not specified uses last feature added
769
+
770
+ kwargs
771
+
772
+ Returns
773
+ -------
774
+ fold_frame : FoldFrame
775
+ created fold frame
776
+ """
777
+ if self.check_inialisation() == False:
778
+ return False
779
+ self.parameters['features'].append(
780
+ {'feature_type': 'folded_fold_frame', 'feature_name': fold_frame_data, 'fold_frame': fold_frame, **kwargs})
781
+ if fold_frame is None:
782
+ logger.info("Using last feature as fold frame")
783
+ fold_frame = self.features[-1]
784
+ assert type(fold_frame) == FoldFrame, "Please specify a FoldFrame"
785
+ fold = FoldEvent(fold_frame,name='Fold_{}'.format(fold_frame_data))
786
+ fold_interpolator = self.get_interpolator("DFI", fold=fold, **kwargs)
787
+ gy_fold_interpolator = self.get_interpolator("DFI", fold=fold, **kwargs)
788
+
789
+ frame_interpolator = self.get_interpolator(**kwargs)
790
+ interpolators = [fold_interpolator, gy_fold_interpolator,
791
+ frame_interpolator.copy()]
792
+ fold_frame_builder = StructuralFrameBuilder(
793
+ interpolators=interpolators, name=fold_frame_data, **kwargs)
794
+ fold_frame_builder.add_data_from_data_frame(
795
+ self.data[self.data['feature_name'] == fold_frame_data])
796
+
797
+ ## add the data to the interpolator for the main foliation
798
+ fold_frame_builder[0].add_data_to_interpolator(True)
799
+
800
+ if "fold_axis" in kwargs:
801
+ logger.info("Using cylindrical fold axis")
802
+ fold.fold_axis = kwargs['fold_axis']
803
+ if "av_fold_axis" in kwargs:
804
+ logger.info("Using average intersection lineation for \n"
805
+ "fold axis")
806
+ _calculate_average_intersection(fold_frame_builder[0], fold_frame,
807
+ fold)
808
+
809
+ if fold.fold_axis is None:
810
+ logger.info("Fitting fold axis rotation angle")
811
+ far, fad = fold_frame.calculate_fold_axis_rotation(
812
+ fold_frame_builder[0])
813
+ fold_axis_rotation = FoldRotationAngle(far, fad)
814
+ a_wl = kwargs.get("axis_wl", None)
815
+ if 'axis_function' in kwargs:
816
+ # allow predefined function to be used
817
+ fold_axis_rotation.set_function(kwargs['axis_function'])
818
+ else:
819
+ fold_axis_rotation.fit_fourier_series(wl=a_wl)
820
+ fold.fold_axis_rotation = fold_axis_rotation
821
+ # give option of passing own fold limb rotation function
822
+ flr, fld = fold_frame.calculate_fold_limb_rotation(
823
+ fold_frame_builder[0])
824
+ fold_limb_rotation = FoldRotationAngle(flr, fld)
825
+ l_wl = kwargs.get("limb_wl", None)
826
+ if 'limb_function' in kwargs:
827
+ # allow for predefined functions to be used
828
+ fold_limb_rotation.set_function(kwargs['limb_function'])
829
+ else:
830
+ fold_limb_rotation.fit_fourier_series(wl=l_wl)
831
+ fold.fold_limb_rotation = fold_limb_rotation
832
+ # fold_limb_fitter = kwargs.get("fold_limb_function",
833
+ # _interpolate_fold_limb_rotation_angle)
834
+ # fold_limb_fitter(series_builder, fold_frame, fold, result, **kwargs)
835
+ kwargs['fold_weights'] = kwargs.get('fold_weights', {})
836
+
837
+ for i in range(3):
838
+ self._add_faults(fold_frame_builder[i])
839
+ # build feature
840
+ kwargs['fold'] = fold
841
+ self._add_faults(fold_frame_builder[0])
842
+ self._add_faults(fold_frame_builder[1])
843
+ self._add_faults(fold_frame_builder[2])
844
+ fold_frame = fold_frame_builder.build(**kwargs, frame=FoldFrame)
845
+ fold_frame.type = 'structuralframe'
846
+ # see if any unconformities are above this feature if so add region
847
+ # for i in range(3):
848
+ # self._add_unconformity_above(fold_frame[i])
849
+
850
+ self._add_feature(fold_frame)
851
+
852
+
853
+ return fold_frame
854
+
855
+ def _add_faults(self, feature_builder, features=None):
856
+ """Adds all existing faults to a geological feature builder
857
+
858
+ Parameters
859
+ ----------
860
+ feature_builder : GeologicalFeatureInterpolator/StructuralFrameBuilder
861
+ The feature buider to add the faults to
862
+ features : list, optional
863
+ A specific list of features rather than all features in the model
864
+ Returns
865
+ -------
866
+
867
+ """
868
+ if features is None:
869
+ features = self.features
870
+ for f in reversed(features):
871
+ if f.type == 'fault':
872
+ feature_builder.add_fault(f)
873
+ # if f.type == 'unconformity':
874
+ # break
875
+ def _add_domain_fault_above(self, feature):
876
+ """
877
+ Looks through the feature list and adds any domain faults to the feature. The domain fault masks everything
878
+ where the fault scalar field is < 0 as being active when added to feature.
879
+
880
+ Parameters
881
+ ----------
882
+ feature : GeologicalFeatureInterpolator
883
+ the feature being added to the model where domain faults should be added
884
+
885
+ Returns
886
+ -------
887
+
888
+ """
889
+ for f in reversed(self.features):
890
+ if f.name == feature.name:
891
+ continue
892
+ if f.type == 'domain_fault':
893
+ feature.add_region(lambda pos: f.evaluate_value(pos) < 0)
894
+ break
895
+
896
+ def _add_domain_fault_below(self, domain_fault):
897
+ """
898
+ Looks through the feature list and adds any the domain_fault to the features that already exist in the stack
899
+ until an unconformity is reached. domain faults to the feature. The domain fault masks everything
900
+ where the fault scalar field is < 0 as being active when added to feature.
901
+
902
+ Parameters
903
+ ----------
904
+ feature : GeologicalFeatureInterpolator
905
+ the feature being added to the model where domain faults should be added
906
+
907
+ Returns
908
+ -------
909
+
910
+ """
911
+ for f in reversed(self.features):
912
+ if f.name == domain_fault.name:
913
+ continue
914
+ f.add_region(lambda pos: domain_fault.evaluate_value(pos) > 0)
915
+ if f.type == 'unconformity':
916
+ break
917
+
918
+ def _add_unconformity_above(self, feature):
919
+ """
920
+
921
+ Adds a region to the feature to prevent the value from being
922
+ interpolated where the unconformities exists above e.g.
923
+ if there is another feature above and the unconformity is at 0
924
+ then the features added below (after) will only be visible where the
925
+ uncomformity is <0
926
+
927
+ Parameters
928
+ ----------
929
+ feature - GeologicalFeature
930
+
931
+ Returns
932
+ -------
933
+
934
+ """
935
+ for f in reversed(self.features):
936
+ if f.type == 'unconformity':
937
+ feature.add_region(lambda pos: f.evaluate(pos))
938
+ break
939
+
940
+ def _add_unconformity_below(self, feature):
941
+ """
942
+ Adds a region to the features that represents the
943
+ unconformity so it is not evaluated below the unconformity
944
+
945
+ Parameters
946
+ ----------
947
+ feature
948
+
949
+ Returns
950
+ -------
951
+
952
+ """
953
+ for f in self.features:
954
+ if f.type == 'series' and feature.feature.name != f.name:
955
+ f.add_region(lambda pos: ~feature.evaluate(pos))
956
+ # for f in reversed(self.features):
957
+ # if f.type == 'unconformity':
958
+ # feature.add_region(lambda pos: f.evaluate(pos))
959
+ # break
960
+ # feature.add_region(lambda pos: ~uc.evaluate(pos))
961
+
962
+ def create_and_add_unconformity(self, unconformity_surface_data, **kwargs):
963
+ """
964
+ Parameters
965
+ ----------
966
+ unconformity_surface_data : string
967
+ name of the unconformity data in the data frame
968
+
969
+ Returns
970
+ -------
971
+ """
972
+ if not self.check_initialisation():
973
+ return False
974
+ # self.parameters['features'].append({'feature_type':'unconformity','feature_name':unconformity_surface_data,**kwargs})
975
+ interpolator = self.get_interpolator(**kwargs)
976
+ unconformity_feature_builder = GeologicalFeatureInterpolator(
977
+ interpolator, name=unconformity_surface_data)
978
+ # add data
979
+ unconformity_data = self.data[
980
+ self.data['feature_name'] == unconformity_surface_data]
981
+
982
+ unconformity_feature_builder.add_data_from_data_frame(
983
+ unconformity_data)
984
+ # look through existing features if there is a fault before an
985
+ # unconformity
986
+ # then add to the feature, once we get to an unconformity stop
987
+ self._add_faults(unconformity_feature_builder)
988
+
989
+ # build feature
990
+ # uc_feature_base = unconformity_feature_builder.build(**kwargs)
991
+ uc_feature_base = unconformity_feature_builder.feature
992
+ unconformity_feature_builder.build_arguments = kwargs
993
+ uc_feature_base.type = 'unconformity_base'
994
+ # uc_feature = UnconformityFeature(uc_feature_base,0)
995
+ # iterate over existing features and add the unconformity as a
996
+ # region so the feature is only
997
+ # evaluated where the unconformity is positive
998
+ return self.add_unconformity(uc_feature_base, 0)
999
+
1000
+ def add_unconformity(self, feature, value):
1001
+ """
1002
+ Use an existing feature to add an unconformity to the model.
1003
+
1004
+ Parameters
1005
+ ----------
1006
+ feature : GeologicalFeature
1007
+ existing geological feature
1008
+ value : float
1009
+ scalar value of isosurface that represents
1010
+
1011
+ Returns
1012
+ -------
1013
+ unconformity : GeologicalFeature
1014
+ unconformity feature
1015
+
1016
+ """
1017
+ self.parameters['features'].append({'feature_type': 'unconformity', 'feature': feature, 'value': value})
1018
+ uc_feature = UnconformityFeature(feature, value)
1019
+
1020
+ # for f in self.features:
1021
+ # f.add_region(lambda pos: uc_feature.evaluate(pos))
1022
+
1023
+ # see if any unconformities are above this feature if so add region
1024
+ # self._add_unconformity_above(uc_feature)
1025
+ # self._add_unconformity_below(feature)#, uc_feature)
1026
+ self._add_feature(uc_feature)
1027
+
1028
+
1029
+ return uc_feature
1030
+
1031
+ def add_onlap_unconformity(self, feature, value):
1032
+ """
1033
+ Use an existing feature to add an unconformity to the model.
1034
+
1035
+ Parameters
1036
+ ----------
1037
+ feature : GeologicalFeature
1038
+ existing geological feature
1039
+ value : float
1040
+ scalar value of isosurface that represents
1041
+
1042
+ Returns
1043
+ -------
1044
+ unconformity_feature : GeologicalFeature
1045
+ the created unconformity
1046
+
1047
+ """
1048
+ self.parameters['features'].append({'feature_type': 'onlap', 'feature': feature, 'value': value})
1049
+
1050
+ uc_feature = UnconformityFeature(feature, value)
1051
+
1052
+ # for f in self.features:
1053
+ # f.add_region(lambda pos: uc_feature.evaluate(pos))
1054
+
1055
+ # see if any unconformities are above this feature if so add region
1056
+ # self._add_unconformity_above(uc_feature)
1057
+ self._add_unconformity_below(uc_feature) # , uc_feature)
1058
+ self._add_feature(uc_feature)
1059
+
1060
+
1061
+ return uc_feature
1062
+
1063
+ def create_and_add_domain_fault(self, fault_surface_data, **kwargs):
1064
+ """
1065
+ Parameters
1066
+ ----------
1067
+ fault_surface_data : string
1068
+ name of the domain fault data in the data frame
1069
+
1070
+ Returns
1071
+ -------
1072
+ domain_Fault : GeologicalFeature
1073
+ the created domain fault
1074
+
1075
+ """
1076
+ # self.parameters['features'].append({'feature_type':'unconformity','feature_name':unconformity_surface_data,**kwargs})
1077
+ interpolator = self.get_interpolator(**kwargs)
1078
+ domain_fault_feature_builder = GeologicalFeatureInterpolator(
1079
+ interpolator, name=fault_surface_data)
1080
+ # add data
1081
+ unconformity_data = self.data[
1082
+ self.data['feature_name'] == fault_surface_data]
1083
+
1084
+ domain_fault_feature_builder.add_data_from_data_frame(
1085
+ unconformity_data)
1086
+ # look through existing features if there is a fault before an
1087
+ # unconformity
1088
+ # then add to the feature, once we get to an unconformity stop
1089
+ self._add_faults(domain_fault_feature_builder)
1090
+
1091
+ # build feature
1092
+ # domain_fault = domain_fault_feature_builder.build(**kwargs)
1093
+ domain_fault = domain_fault_feature_builder.feature
1094
+ domain_fault_feature_builder.build_arguments = kwargs
1095
+ domain_fault.type = 'domain_fault'
1096
+ self._add_feature(domain_fault)
1097
+ self._add_domain_fault_below(domain_fault)
1098
+
1099
+ domain_fault_uc = UnconformityFeature(domain_fault,0)
1100
+ # iterate over existing features and add the unconformity as a
1101
+ # region so the feature is only
1102
+ # evaluated where the unconformity is positive
1103
+ return domain_fault_uc
1104
+
1105
+ def create_and_add_fault(self,
1106
+ fault_surface_data,
1107
+ displacement,
1108
+ fault_slip_vector=None,
1109
+ fault_center = None,
1110
+ fault_extent = None,
1111
+ fault_influence = None,
1112
+ fault_vectical_radius = None,
1113
+ faultfunction=None,
1114
+ **kwargs):
1115
+ """
1116
+ Parameters
1117
+ ----------
1118
+ fault_surface_data : string
1119
+ name of the fault surface data in the dataframe
1120
+ displacement : displacement magnitude
1121
+ fault_extent : [type], optional
1122
+ [description], by default None
1123
+ fault_influence : [type], optional
1124
+ [description], by default None
1125
+ fault_vectical_radius : [type], optional
1126
+ [description], by default None
1127
+ kwargs : additional kwargs for Fault and interpolators
1128
+
1129
+ Returns
1130
+ -------
1131
+ fault : FaultSegment
1132
+ created fault
1133
+ """
1134
+ self.parameters['features'].append(
1135
+ {'feature_type': 'fault', 'feature_name': fault_surface_data, 'displacement': displacement, **kwargs})
1136
+ if 'data_region' in kwargs:
1137
+ kwargs.pop('data_region')
1138
+ logger.error("kwarg data_region currently not supported, disabling")
1139
+ displacement_scaled = displacement / self.scale_factor
1140
+ # create fault frame
1141
+ interpolator = self.get_interpolator(**kwargs)
1142
+ fault_frame_builder = FaultBuilder(interpolator,
1143
+ name=fault_surface_data,
1144
+ model=self,
1145
+ **kwargs)
1146
+ # add data
1147
+ fault_frame_data = self.data[ self.data['feature_name'] == fault_surface_data].copy()
1148
+ mask = np.logical_and(fault_frame_data['coord']==0,~np.isnan(fault_frame_data['gz']))
1149
+ fault_normal_vector = fault_frame_data.loc[mask,['gx','gy','gz']].mean(axis=0).to_numpy()
1150
+ mask = np.logical_and(fault_frame_data['coord']==1,~np.isnan(fault_frame_data['gz']))
1151
+ if fault_slip_vector is None:
1152
+ fault_slip_vector = fault_frame_data.loc[mask,['gx','gy','gz']].mean(axis=0).to_numpy()
1153
+ if fault_center is not None:
1154
+ fault_center = self.scale(fault_center,inplace=False)
1155
+ if fault_center is None:
1156
+ # if we haven't defined a fault centre take the center of mass for lines assocaited with
1157
+ # the fault trace
1158
+ mask = np.logical_and(fault_frame_data['coord']==0,fault_frame_data['val']==0)
1159
+ fault_center = fault_frame_data.loc[mask,['X','Y','Z']].mean(axis=0).to_numpy()
1160
+ if fault_influence:
1161
+ fault_influence=fault_influence/self.scale_factor
1162
+ if fault_extent:
1163
+ fault_extent=fault_extent/self.scale_factor
1164
+ if fault_vectical_radius:
1165
+ fault_vectical_radius=fault_vectical_radius/self.scale_factor
1166
+ fault_frame_builder.create_data_from_geometry(fault_frame_data,
1167
+ fault_center,
1168
+ fault_normal_vector,
1169
+ fault_slip_vector,
1170
+ influence_distance=fault_influence,
1171
+ horizontal_radius=fault_extent,
1172
+ vertical_radius=fault_vectical_radius
1173
+ )
1174
+ if fault_influence == None or fault_extent == None or fault_vectical_radius == None:
1175
+ fault_frame_builder.origin = self.bounding_box[0,:]
1176
+ fault_frame_builder.maximum = self.bounding_box[1,:]
1177
+ fault_frame_builder.set_mesh_geometry(kwargs.get('fault_buffer',0.1))
1178
+ # fault_frame_builder.add_data_from_data_frame(fault_frame_data)
1179
+ # check if this fault overprint any existing faults exist in the stack
1180
+ overprinted = kwargs.get('overprints', [])
1181
+ overprinted_faults = []
1182
+ for o in overprinted:
1183
+ try:
1184
+ overprinted_faults.append(self.features[self.feature_name_index[o]])
1185
+ except KeyError:
1186
+ logger.warning("{} could not be added to overprint {} \
1187
+ because it does not exist".format(o,fault_surface_data))
1188
+ self._add_faults(fault_frame_builder[0],overprinted_faults)
1189
+ self._add_faults(fault_frame_builder[1],overprinted_faults)
1190
+ self._add_faults(fault_frame_builder[2],overprinted_faults)
1191
+
1192
+ fault_frame = fault_frame_builder.build(**kwargs)
1193
+ if 'abut' in kwargs:
1194
+ fault_frame[0].add_region(lambda pos: kwargs['abut'].evaluate(pos))
1195
+
1196
+ fault = FaultSegment(fault_frame, displacement=displacement_scaled,
1197
+ faultfunction=faultfunction,
1198
+ **kwargs)
1199
+ fault.builder=fault_frame_builder
1200
+ for f in reversed(self.features):
1201
+ if f.type == 'unconformity':
1202
+ fault.add_region(lambda pos: f.evaluate_value(pos) <= 0)
1203
+ break
1204
+ if displacement == 0:
1205
+ fault.type = 'fault_inactive'
1206
+ self._add_feature(fault)
1207
+
1208
+
1209
+ return fault
1210
+
1211
+ def rescale(self, points, inplace=True):
1212
+ """
1213
+ Convert from model scale to real world scale - in the future this
1214
+ should also do transformations?
1215
+
1216
+ Parameters
1217
+ ----------
1218
+ points : np.array((N,3),dtype=double)
1219
+ inplace : boolean
1220
+ whether to return a modified copy or modify the original array
1221
+
1222
+ Returns
1223
+ -------
1224
+ points : np.array((N,3),dtype=double)
1225
+
1226
+ """
1227
+ if inplace == False:
1228
+ points = points.copy()
1229
+ points *= self.scale_factor
1230
+ points += self.origin
1231
+ return points
1232
+
1233
+ def scale(self, points, inplace=True):
1234
+ """ Take points in UTM coordinates and reproject
1235
+ into scaled model space
1236
+
1237
+ Parameters
1238
+ ----------
1239
+ points : np.array((N,3),dtype=float)
1240
+ points to
1241
+ inplace : bool, optional default = True
1242
+ whether to copy the points array or update the passed array
1243
+ Returns
1244
+ -------
1245
+ points : np.array((N,3),dtype=double)
1246
+
1247
+ """
1248
+ points = np.array(points).astype(float)
1249
+ if inplace==False:
1250
+ points = points.copy()
1251
+ # if len(points.shape) == 1:
1252
+ # points = points[None,:]
1253
+ # if len(points.shape) != 2:
1254
+ # logger.error("cannot scale array of dimensions".format(len(points.shape)))
1255
+ points -= self.origin
1256
+ points /= self.scale_factor
1257
+ return points
1258
+
1259
+
1260
+ def regular_grid(self, nsteps=None, shuffle = True, rescale=False):
1261
+ """
1262
+ Return a regular grid within the model bounding box
1263
+
1264
+ Parameters
1265
+ ----------
1266
+ nsteps : tuple
1267
+ number of cells in x,y,z
1268
+
1269
+ Returns
1270
+ -------
1271
+ xyz : np.array((N,3),dtype=float)
1272
+ locations of points in regular grid
1273
+ """
1274
+ if nsteps is None:
1275
+ nsteps = self.nsteps
1276
+ x = np.linspace(self.bounding_box[0, 0], self.bounding_box[1, 0],
1277
+ nsteps[0])
1278
+ y = np.linspace(self.bounding_box[0, 1], self.bounding_box[1, 1],
1279
+ nsteps[1])
1280
+ z = np.linspace(self.bounding_box[1, 2], self.bounding_box[0, 2],
1281
+ nsteps[2])
1282
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
1283
+ locs = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
1284
+ if shuffle:
1285
+ logger.info("Shuffling points")
1286
+ np.random.shuffle(locs)
1287
+ if rescale:
1288
+ locs = self.rescale(locs)
1289
+ return locs
1290
+
1291
+ def evaluate_model(self, xyz, scale=True):
1292
+ """Evaluate the stratigraphic id at each location
1293
+
1294
+
1295
+ Parameters
1296
+ ----------
1297
+ xyz : np.array((N,3),dtype=float)
1298
+ locations
1299
+ scale : bool
1300
+ whether to rescale the xyz before evaluating model
1301
+
1302
+ Returns
1303
+ -------
1304
+ stratigraphic_id : np.array(N,dtype=int)
1305
+ the stratigraphic index for locations
1306
+
1307
+ Examples
1308
+ --------
1309
+ Evaluate on a voxet
1310
+
1311
+ >>> x = np.linspace(model.bounding_box[0, 0], model.bounding_box[1, 0],
1312
+ nsteps[0])
1313
+ >>> y = np.linspace(model.bounding_box[0, 1], model.bounding_box[1, 1],
1314
+ nsteps[1])
1315
+ >>> z = np.linspace(model.bounding_box[1, 2], model.bounding_box[0, 2],
1316
+ nsteps[2])
1317
+ >>> xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
1318
+ >>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
1319
+ >>> model.evaluate_model(xyz)
1320
+
1321
+ Evaluate on points defined by regular grid function
1322
+
1323
+ >>> model.evaluate_model(model.regular_grid())
1324
+
1325
+
1326
+ Evaluate on a map
1327
+
1328
+ >>> x = np.linspace(self.bounding_box[0, 0], self.bounding_box[1, 0],
1329
+ nsteps[0])
1330
+ >>> y = np.linspace(self.bounding_box[0, 1], self.bounding_box[1, 1],
1331
+ nsteps[1])
1332
+ >>> xx, yy = np.meshgrid(x, y, indexing='ij')
1333
+ >>> zz = np.zeros_like(yy)
1334
+ >>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
1335
+ >>> model.evaluate_model(xyz)
1336
+
1337
+ """
1338
+ xyz = np.array(xyz)
1339
+ if scale:
1340
+ xyz = self.scale(xyz,inplace=False)
1341
+ strat_id = np.zeros(xyz.shape[0],dtype=int)
1342
+ for group in self.stratigraphic_column.keys():
1343
+ if group == 'faults':
1344
+ continue
1345
+ feature_id = self.feature_name_index.get(group, -1)
1346
+ if feature_id >= 0:
1347
+ feature = self.features[feature_id]
1348
+ vals = feature.evaluate_value(xyz)
1349
+ for series in self.stratigraphic_column[group].values():
1350
+ strat_id[np.logical_and(vals < series.get('max',feature.max()), vals > series.get('min',feature.min()))] = series['id']
1351
+ if feature_id == -1:
1352
+ logger.error('Model does not contain {}'.format(group))
1353
+ return strat_id
1354
+
1355
+ def evaluate_fault_displacements(self,points,scale=True):
1356
+ """Evaluate the fault displacement magnitude at each location
1357
+
1358
+
1359
+ Parameters
1360
+ ----------
1361
+ xyz : np.array((N,3),dtype=float)
1362
+ locations
1363
+ scale : bool
1364
+ whether to rescale the xyz before evaluating model
1365
+
1366
+ Returns
1367
+ -------
1368
+ fault_displacement : np.array(N,dtype=float)
1369
+ the fault displacement magnitude
1370
+ """
1371
+ if scale:
1372
+ points = self.scale(points,inplace=False)
1373
+ vals = np.zeros(points.shape[0])
1374
+ for f in self.features:
1375
+ if f.type == 'fault':
1376
+ disp = f.displacementfeature.evaluate_value(points)
1377
+ vals[~np.isnan(disp)] += disp[~np.isnan(disp)]
1378
+ return vals*-self.scale_factor # convert from restoration magnutude to displacement
1379
+
1380
+ def get_feature_by_name(self, feature_name):
1381
+ """Returns a feature from the mode given a name
1382
+
1383
+
1384
+ Parameters
1385
+ ----------
1386
+ feature_name : string
1387
+ the name of the feature
1388
+
1389
+ Returns
1390
+ -------
1391
+ feature : GeologicalFeature
1392
+ the geological feature with the specified name, or none if no feature
1393
+
1394
+
1395
+
1396
+ """
1397
+ feature_index = self.feature_name_index.get(feature_name,-1)
1398
+ if feature_index > -1:
1399
+ return self.features[feature_index]
1400
+ else:
1401
+ logger.error("{} does not exist!".format(feature_name))
1402
+ return None
1403
+
1404
+ def evaluate_feature_value(self, feature_name, xyz, scale=True):
1405
+ """Evaluate the scalar value of the geological feature given the name at locations
1406
+ xyz
1407
+
1408
+ Parameters
1409
+ ----------
1410
+ feature_name : string
1411
+ name of the feature
1412
+ xyz : np.array((N,3))
1413
+ locations to evaluate
1414
+ scale : bool, optional
1415
+ whether to scale real world points into model scale, by default True
1416
+
1417
+ Returns
1418
+ -------
1419
+ np.array((N))
1420
+ vector of scalar values
1421
+
1422
+ Examples
1423
+ --------
1424
+ Evaluate on a voxet using model boundaries
1425
+
1426
+ >>> x = np.linspace(model.bounding_box[0, 0], model.bounding_box[1, 0],
1427
+ nsteps[0])
1428
+ >>> y = np.linspace(model.bounding_box[0, 1], model.bounding_box[1, 1],
1429
+ nsteps[1])
1430
+ >>> z = np.linspace(model.bounding_box[1, 2], model.bounding_box[0, 2],
1431
+ nsteps[2])
1432
+ >>> xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
1433
+ >>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
1434
+ >>> model.evaluate_feature_vaue('feature',xyz,scale=False)
1435
+
1436
+ Evaluate on points in UTM coordinates
1437
+
1438
+ >>> model.evaluate_feature_vaue('feature',utm_xyz)
1439
+
1440
+ """
1441
+ feature = self.get_feature_by_name(feature_name)
1442
+ if feature:
1443
+ scaled_xyz = xyz
1444
+ if scale:
1445
+ scaled_xyz = self.scale(xyz,inplace=False)
1446
+ return feature.evaluate_value(scaled_xyz)
1447
+ else:
1448
+ return np.zeros(xyz.shape[0])
1449
+
1450
+ def evaluate_feature_gradient(self, feature_name, xyz, scale=True):
1451
+ """Evaluate the gradient of the geological feature at a location
1452
+
1453
+ Parameters
1454
+ ----------
1455
+ feature_name : string
1456
+ name of the geological feature
1457
+ xyz : np.array((N,3))
1458
+ locations to evaluate
1459
+ scale : bool, optional
1460
+ whether to scale real world points into model scale, by default True
1461
+
1462
+ Returns
1463
+ -------
1464
+ results : np.array((N,3))
1465
+ gradient of the scalar field at the locations specified
1466
+ """
1467
+ feature = self.get_feature_by_name(feature_name)
1468
+ if feature:
1469
+ scaled_xyz = xyz
1470
+ if scale:
1471
+ scaled_xyz = self.scale(xyz, inplace = False)
1472
+ return feature.evaluate_gradient(scaled_xyz)
1473
+ else:
1474
+ return np.zeros(xyz.shape[0])
1475
+
1476
+ def update(self,verbose=False,progressbar=True):
1477
+ total_dof = 0
1478
+ nfeatures = 0
1479
+ for f in self.features:
1480
+ if f.type=='fault':
1481
+ nfeatures+=3
1482
+ total_dof+=f[0].interpolator.nx*3
1483
+ if f.type == 'series':
1484
+ nfeatures+=1
1485
+ total_dof+=f.interpolator.nx
1486
+ if verbose==True:
1487
+ print('Updating geological model. There are: \n'
1488
+ '{} geological features that need to be interpolated\n'.format(nfeatures)
1489
+ )
1490
+
1491
+ from tqdm.auto import tqdm
1492
+ import time
1493
+ start = time.time()
1494
+ sizecounter = 0
1495
+
1496
+ # Load tqdm with size counter instead of file counter
1497
+ with tqdm(total=nfeatures) as pbar:
1498
+ buf=0
1499
+ for f in self.features:
1500
+ pbar.set_description('Interpolating {}'.format(f.name))
1501
+ if f.type == 'fault':
1502
+ for i in range(3):
1503
+ buf+=1
1504
+ f[i].builder.update()
1505
+ pbar.update()
1506
+ if f.type == 'series':
1507
+ f.builder.update()
1508
+ pbar.update()
1509
+
1510
+
1511
+ if verbose:
1512
+ print("Model update took: {} seconds".format(time.time()-start))
1513
+