LoopStructural 1.0.9__cp38-cp38-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.cp38-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,880 @@
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
+ import networkx as nx
9
+
10
+ from LoopStructural.datasets import normal_vector_headers
11
+ from LoopStructural.interpolators.discrete_fold_interpolator import \
12
+ DiscreteFoldInterpolator as DFI
13
+ from LoopStructural.interpolators.finite_difference_interpolator import \
14
+ FiniteDifferenceInterpolator as FDI
15
+ from LoopStructural.interpolators.piecewiselinear_interpolator import \
16
+ PiecewiseLinearInterpolator as PLI
17
+
18
+ try:
19
+ from LoopStructural.interpolators.surfe_wrapper import \
20
+ SurfeRBFInterpolator as Surfe
21
+
22
+ surfe = True
23
+
24
+ except ImportError:
25
+ surfe = False
26
+
27
+ from LoopStructural.interpolators.structured_grid import StructuredGrid
28
+ from LoopStructural.interpolators.structured_tetra import TetMesh
29
+ from LoopStructural.modelling.fault.fault_segment import FaultSegment
30
+ from LoopStructural.modelling.features import (GeologicalFeatureInterpolator,
31
+ RegionFeature,
32
+ StructuralFrameBuilder,
33
+ UnconformityFeature)
34
+ from LoopStructural.modelling.fold import FoldRotationAngle
35
+ from LoopStructural.modelling.fold.fold import FoldEvent
36
+ from LoopStructural.modelling.fold.foldframe import FoldFrame
37
+ from LoopStructural.utils.exceptions import LoopBaseException
38
+ from LoopStructural.utils.helper import (all_heading, gradient_vec_names,
39
+ strike_dip_vector)
40
+
41
+ from LoopStructural.utils import getLogger, log_to_file
42
+ logger = getLogger(__name__)
43
+ if not surfe:
44
+ logger.warning("Cannot import Surfe")
45
+
46
+
47
+ def _calculate_average_intersection(series_builder, fold_frame, fold,
48
+ **kwargs):
49
+ """
50
+
51
+ Parameters
52
+ ----------
53
+ series_builder
54
+ fold_frame
55
+ fold
56
+
57
+ Returns
58
+ -------
59
+
60
+ """
61
+ l2 = fold_frame.calculate_intersection_lineation(
62
+ series_builder)
63
+ fold.fold_axis = np.mean(l2, axis=0)
64
+
65
+
66
+ class GeologicalModel:
67
+ """
68
+ A geological model is the recipe for building a 3D model and can include
69
+ the rescaling of the model between 0 and 1.
70
+
71
+ Attributes
72
+ ----------
73
+ features : list
74
+ Contains all features youngest to oldest
75
+ feature_name_index : dict
76
+ maps feature name to the list index of the features
77
+ data : pandas dataframe
78
+ the dataframe used for building the geological model
79
+ nsteps : tuple/np.array(3,dtype=int)
80
+ the number of steps x,y,z to evaluate the model
81
+ origin : tuple/np.array(3,dtype=doubles)
82
+ the origin of the model box
83
+ parameters : dict
84
+ a dictionary tracking the parameters used to build the model
85
+ scale_factor : double
86
+ the scale factor used to rescale the model
87
+
88
+
89
+ """
90
+ def __init__(self, origin, maximum, rescale=True, nsteps=(40, 40, 40),
91
+ reuse_supports=False, logfile=None, loglevel='info'):
92
+ """
93
+ Parameters
94
+ ----------
95
+ origin : numpy array
96
+ specifying the origin of the model
97
+ maximum : numpy array
98
+ specifying the maximum extent of the model
99
+ rescale : bool
100
+ whether to rescale the model to between 0/1
101
+
102
+ Examples
103
+ --------
104
+ Demo data
105
+
106
+ >>> from LoopStructural.datasets import load_claudius
107
+ >>> from LoopStructural import GeologicalModel
108
+
109
+ >>> data, bb = load_claudius()
110
+
111
+ >>> model = GeologicalModel(bb[:,0],bb[:,1]
112
+ >>> model.set_model_data(data)
113
+ >>> model.create_and_add_foliation('strati')
114
+
115
+ >>> y = np.linspace(model.bounding_box[0, 1], model.bounding_box[1, 1],
116
+ nsteps[1])
117
+ >>> z = np.linspace(model.bounding_box[1, 2], model.bounding_box[0, 2],
118
+ nsteps[2])
119
+ >>> xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
120
+ >>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
121
+ >>> model.evaluate_feature_value('strati',xyz,scale=False)
122
+
123
+
124
+ """
125
+ if logfile:
126
+ self.logfile = logfile
127
+ log_to_file(logfile,loglevel)
128
+
129
+ logger.info('Initialising geological model')
130
+ self.features = []
131
+ self.feature_name_index = {}
132
+ self.data = None
133
+ self.nsteps = nsteps
134
+ self._str = 'Instance of LoopStructural.GeologicalModel \n'
135
+ self._str += '------------------------------------------ \n'
136
+ # we want to rescale the model area so that the maximum length is
137
+ # 1
138
+ self.origin = np.array(origin).astype(float)
139
+ originstr = 'Model origin: {} {} {}'.format(self.origin[0],self.origin[1],self.origin[2])
140
+ logger.info(originstr)
141
+ self._str+=originstr+'\n'
142
+ self.maximum = np.array(maximum).astype(float)
143
+ maximumstr = 'Model maximum: {} {} {}'.format(self.maximum[0],self.maximum[1],self.maximum[2])
144
+ logger.info(maximumstr)
145
+ self._str+=maximumstr+'\n'
146
+
147
+ lengths = self.maximum - self.origin
148
+ self.scale_factor = 1.
149
+ self.bounding_box = np.zeros((2, 3))
150
+ self.bounding_box[1, :] = self.maximum - self.origin
151
+ if rescale:
152
+ self.scale_factor = np.max(lengths)
153
+ logger.info('Rescaling model using scale factor {}'.format(self.scale_factor))
154
+ self._str+='Model rescale factor: {} \n'.format(self.scale_factor)
155
+ self._str+='The model contains {} GeologicalFeatures \n'.format(len(self.features))
156
+ self._str+=''
157
+ self._str += '------------------------------------------ \n'
158
+ self._str += ''
159
+ self.bounding_box /= self.scale_factor
160
+ self.support = {}
161
+ self.reuse_supports = reuse_supports
162
+ if self.reuse_supports:
163
+ logger.warning("Supports are shared between geological features \n"
164
+ "this may cause unexpected behaviour and should only\n"
165
+ "be use by advanced users")
166
+ logger.info('Reusing interpolation supports: {}'.format(self.reuse_supports))
167
+ self.stratigraphic_column = None
168
+ self.feature_graph = nx.DiGraph()
169
+ self.parameters = {'features': [], 'model': {'bounding_box': self.origin.tolist() + self.maximum.tolist(),
170
+ 'rescale': rescale,
171
+ 'nsteps': nsteps,
172
+ 'reuse_supports': reuse_supports}}
173
+
174
+ def __str__(self):
175
+ return self._str
176
+
177
+ def _ipython_key_completions_(self):
178
+ return self.feature_name_index.keys()
179
+
180
+ @classmethod
181
+ def from_map2loop_directory(cls, m2l_directory,**kwargs):
182
+ """Alternate constructor for a geological model using m2l output
183
+
184
+ Uses the information saved in the map2loop files to build a geological model.
185
+ You can specify kwargs for building foliation using foliation_params and for
186
+ faults using fault_params. faults is a flag that allows for the faults to be skipped.
187
+
188
+ Parameters
189
+ ----------
190
+ m2l_directory : string
191
+ path to map2loop directory
192
+
193
+ Returns
194
+ -------
195
+ (GeologicalModel, dict)
196
+ the created geological model and a dictionary of the map2loop data
197
+ """
198
+ from LoopStructural.utils import build_model, process_map2loop
199
+ logger.info('LoopStructural model initialised from m2l directory: {}'.format(m2l_directory))
200
+ m2lflags = kwargs.pop('m2lflags',{})
201
+ m2l_data = process_map2loop(m2l_directory,m2lflags)
202
+ return build_model(m2l_data,**kwargs), m2l_data
203
+
204
+ @classmethod
205
+ def from_file(cls, file):
206
+ """Load a geological model from file
207
+
208
+ Parameters
209
+ ----------
210
+ file : string
211
+ path to the file
212
+
213
+ Returns
214
+ -------
215
+ GeologicalModel
216
+ the geological model object
217
+ """
218
+ try:
219
+ import dill as pickle
220
+ except ImportError:
221
+ logger.error("Cannot import from file, dill not installed")
222
+ return None
223
+ model = pickle.load(open(file,'rb'))
224
+ if type(model) == GeologicalModel:
225
+ logger.info('GeologicalModel initialised from file')
226
+ return model
227
+ else:
228
+ logger.error('{} does not contain a geological model'.format(file))
229
+ return None
230
+
231
+ def __getitem__(self, feature_name):
232
+ """Accessor for feature in features using feature_name_index
233
+
234
+ Parameters
235
+ ----------
236
+ feature_name : string
237
+ name of the feature to return
238
+ """
239
+ return self.get_feature_by_name(feature_name)
240
+
241
+ def feature_names(self):
242
+ return self.feature_name_index.keys()
243
+
244
+ def fault_names(self):
245
+ pass
246
+
247
+ def check_inialisation(self):
248
+ if self.data is None:
249
+ logger.error("Data not associated with GeologicalModel. Run set_data")
250
+ return False
251
+
252
+ def to_file(self, file):
253
+ """Save a model to a pickle file requires dill
254
+
255
+ Parameters
256
+ ----------
257
+ file : string
258
+ path to file location
259
+ """
260
+ try:
261
+ import dill as pickle
262
+ except ImportError:
263
+ logger.error("Cannot write to file, dill not installed \n"
264
+ "pip install dill")
265
+ return
266
+ try:
267
+ logger.info('Writing GeologicalModel to: {}'.format(file))
268
+ pickle.dump(self,open(file,'wb'))
269
+ except pickle.PicklingError:
270
+ logger.error('Error saving file')
271
+
272
+ def _add_feature(self, feature):
273
+ """
274
+ Add a feature to the model stack
275
+
276
+ Parameters
277
+ ----------
278
+ feature : GeologicalFeature
279
+ the geological feature to add
280
+
281
+ """
282
+
283
+ if feature.name in self.feature_name_index:
284
+ logger.info("Feature %s already exists at %i, overwriting" %
285
+ (feature.name, self.feature_name_index[feature.name]))
286
+ self.features[self.feature_name_index[feature.name]] = feature
287
+ else:
288
+ self._str += 'GeologicalFeature: {} of type - {} \n'.format(feature.name,feature.type)
289
+ self.features.append(feature)
290
+ self.feature_name_index[feature.name] = len(self.features) - 1
291
+ logger.info("Adding %s to model at location %i" % (
292
+ feature.name, len(self.features)))
293
+ # self._add_domain_fault_above(feature)
294
+ # self._add_unconformity_above(feature)
295
+ feature.set_model(self)
296
+
297
+ def _add_faults(self, feature_builder, features=None):
298
+ """Adds all existing faults to a geological feature builder
299
+
300
+ Parameters
301
+ ----------
302
+ feature_builder : GeologicalFeatureInterpolator/StructuralFrameBuilder
303
+ The feature buider to add the faults to
304
+ features : list, optional
305
+ A specific list of features rather than all features in the model
306
+ Returns
307
+ -------
308
+
309
+ """
310
+ if features is None:
311
+ features = self.features
312
+ for f in reversed(features):
313
+ if f.type == 'fault':
314
+ feature_builder.add_fault(f)
315
+ # if f.type == 'unconformity':
316
+ # break
317
+
318
+ def data_for_feature(self,feature_name):
319
+ return self.data.loc[self.data['feature_name'] == feature_name,:]
320
+
321
+ def set_model_data(self, data):
322
+ """
323
+ Set the data array for the model
324
+
325
+ Parameters
326
+ ----------
327
+ data : pandas data frame
328
+ with column headers corresponding to the
329
+ type, X, Y, Z, nx, ny, nz, val, strike, dip, dip_dir, plunge,
330
+ plunge_dir, azimuth
331
+
332
+ Returns
333
+ -------
334
+ Note
335
+ ----
336
+ Type can be any unique identifier for the feature the data point
337
+ 'eg' 'S0', 'S2', 'F1_axis'
338
+ it is then used by the create functions to get the correct data
339
+ """
340
+ if type(data) != pd.DataFrame:
341
+ logger.warning(
342
+ "Data is not a pandas data frame, trying to read data frame "
343
+ "from csv")
344
+ try:
345
+ data = pd.read_csv(data)
346
+ except:
347
+ logger.error("Could not load pandas data frame from data")
348
+ logger.info('Adding data to GeologicalModel with {} data points'.format(len(data)))
349
+ self.data = data.copy()
350
+ self.data['X'] -= self.origin[0]
351
+ self.data['Y'] -= self.origin[1]
352
+ self.data['Z'] -= self.origin[2]
353
+ self.data['X'] /= self.scale_factor
354
+ self.data['Y'] /= self.scale_factor
355
+ self.data['Z'] /= self.scale_factor
356
+ if 'type' in self.data:
357
+ logger.warning("'type' is depreciated replace with 'feature_name' \n")
358
+ self.data.rename(columns={'type':'feature_name'},inplace=True)
359
+ for h in all_heading():
360
+ if h not in self.data:
361
+ self.data[h] = np.nan
362
+ if h == 'w':
363
+ self.data[h] = 1.
364
+ if h == 'coord':
365
+ self.data[h] = 0
366
+
367
+ if 'strike' in self.data and 'dip' in self.data:
368
+ logger.info('Converting strike and dip to vectors')
369
+ mask = np.all(~np.isnan(self.data.loc[:, ['strike', 'dip']]),
370
+ axis=1)
371
+ self.data.loc[mask, gradient_vec_names()] = strike_dip_vector(
372
+ self.data.loc[mask, 'strike'], self.data.loc[mask, 'dip'])
373
+ self.data.drop(['strike', 'dip'], axis=1, inplace=True)
374
+ # self.data.loc
375
+ # if 'nx' in self.data and 'ny' in self.data and 'nz' in self.data:
376
+ # mask = np.all(~np.isnan(self.data.loc[:, ['nx', 'ny','nz']]),
377
+ # axis=1)
378
+ # self.data.loc[mask,['nx', 'ny','nz']] /= self.scale_factor
379
+
380
+ def set_stratigraphic_column(self, stratigraphic_column,cmap='tab20'):
381
+ """
382
+ Adds a stratigraphic column to the model
383
+
384
+ Parameters
385
+ ----------
386
+ stratigraphic_column : dictionary
387
+ cmap : matplotlib.cmap
388
+ Returns
389
+ -------
390
+
391
+ Notes
392
+ -----
393
+ stratigraphic_column is a nested dictionary with the format
394
+ {'group':
395
+ {'series1':
396
+ {'min':0., 'max':10.,'id':0,'colour':}
397
+ }
398
+ }
399
+
400
+ """
401
+ # if the colour for a unit hasn't been specified we can just sample from
402
+ # a colour map e.g. tab20
403
+ logger.info('Adding stratigraphic column to model')
404
+ random_colour = True
405
+ n_units=0
406
+ for g in stratigraphic_column.keys():
407
+ for u in stratigraphic_column[g].keys():
408
+ if 'colour' in stratigraphic_column[g][u]:
409
+ random_colour = False
410
+ break
411
+ n_units+=1
412
+ if random_colour:
413
+ import matplotlib.cm as cm
414
+ cmap = cm.get_cmap(cmap,n_units)
415
+ cmap_colours = cmap.colors
416
+ ci = 0
417
+ for g in stratigraphic_column.keys():
418
+ for u in stratigraphic_column[g].keys():
419
+ stratigraphic_column[g][u]['colour'] = cmap_colours[ci,:]
420
+
421
+ self.stratigraphic_column = stratigraphic_column
422
+
423
+ def get_interpolator(self, interpolatortype='PLI', nelements=1e5,
424
+ buffer=0.2, element_volume = None, **kwargs):
425
+ """
426
+ Returns an interpolator given the arguments, also constructs a
427
+ support for a discrete interpolator
428
+
429
+ Parameters
430
+ ----------
431
+ interpolatortype : string
432
+ define the interpolator type
433
+ nelements : int
434
+ number of elements in the interpolator
435
+ buffer : double or numpy array 3x1
436
+ value(s) between 0,1 specifying the buffer around the bounding box
437
+ data_bb : bool
438
+ whether to use the model boundary or the boundary around
439
+ kwargs : no kwargs used, this just catches any additional arguments
440
+
441
+ Returns
442
+ -------
443
+ """
444
+ # get an interpolator for
445
+ interpolator = None
446
+ bb = np.copy(self.bounding_box)
447
+ # add a buffer to the interpolation domain, this is necessary for
448
+ # faults but also generally a good
449
+ # idea to avoid boundary problems
450
+ # buffer = bb[1, :]
451
+ buffer = (bb[1,:]-bb[0,:])*buffer
452
+ bb[0, :] -= buffer # *(bb[1,:]-bb[0,:])
453
+ bb[1, :] += buffer # *(bb[1,:]-bb[0,:])
454
+ box_vol = (bb[1, 0]-bb[0, 0]) * (bb[1, 1]-bb[0, 1]) * (bb[1, 2]-bb[0, 2])
455
+ if interpolatortype == "PLI":
456
+ if element_volume is None:
457
+ nelements /= 5
458
+ element_volume = box_vol / nelements
459
+ # calculate the step vector of a regular cube
460
+ step_vector = np.zeros(3)
461
+ step_vector[:] = element_volume ** (1. / 3.)
462
+ # step_vector /= np.array([1,1,2])
463
+ # number of steps is the length of the box / step vector
464
+ nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
465
+ # create a structured grid using the origin and number of steps
466
+ if self.reuse_supports:
467
+ mesh_id = 'mesh_{}'.format(nelements)
468
+ mesh = self.support.get(mesh_id,
469
+ TetMesh(origin=bb[0, :], nsteps=nsteps,
470
+ step_vector=step_vector))
471
+ if mesh_id not in self.support:
472
+ self.support[mesh_id] = mesh
473
+ else:
474
+ mesh = TetMesh(origin=bb[0, :], nsteps=nsteps, step_vector=step_vector)
475
+ logger.info("Creating regular tetrahedron mesh with %i elements \n"
476
+ "for modelling using PLI" % (mesh.ntetra))
477
+
478
+ return PLI(mesh)
479
+
480
+ if interpolatortype == 'FDI':
481
+
482
+ # find the volume of one element
483
+ if element_volume is None:
484
+ element_volume = box_vol / nelements
485
+ # calculate the step vector of a regular cube
486
+ step_vector = np.zeros(3)
487
+ step_vector[:] = element_volume ** (1. / 3.)
488
+ # number of steps is the length of the box / step vector
489
+ nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
490
+ if np.any(np.less(nsteps, 3)):
491
+ logger.error("Cannot create interpolator: number of steps is too small")
492
+ return None
493
+ # create a structured grid using the origin and number of steps
494
+ if self.reuse_supports:
495
+ grid_id = 'grid_{}'.format(nelements)
496
+ grid = self.support.get(grid_id, StructuredGrid(origin=bb[0, :],
497
+ nsteps=nsteps,
498
+ step_vector=step_vector))
499
+ if grid_id not in self.support:
500
+ self.support[grid_id] = grid
501
+ else:
502
+ grid = StructuredGrid(origin=bb[0, :], nsteps=nsteps,step_vector=step_vector)
503
+ logger.info("Creating regular grid with %i elements \n"
504
+ "for modelling using FDI" % grid.n_elements)
505
+ return FDI(grid)
506
+
507
+ if interpolatortype == "DFI": # "fold" in kwargs:
508
+ if element_volume is None:
509
+ nelements /= 5
510
+ element_volume = box_vol / nelements
511
+ # calculate the step vector of a regular cube
512
+ step_vector = np.zeros(3)
513
+ step_vector[:] = element_volume ** (1. / 3.)
514
+ # number of steps is the length of the box / step vector
515
+ nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
516
+ # create a structured grid using the origin and number of steps
517
+ mesh = kwargs.get('mesh', TetMesh(origin=bb[0, :], nsteps=nsteps,
518
+ step_vector=step_vector))
519
+ logger.info("Creating regular tetrahedron mesh with %i elements \n"
520
+ "for modelling using DFI" % mesh.ntetra)
521
+ return DFI(mesh, kwargs['fold'])
522
+ if interpolatortype == 'Surfe' or interpolatortype == 'surfe' and \
523
+ surfe:
524
+ method = kwargs.get('method', 'single_surface')
525
+ logger.info("Using surfe interpolator")
526
+ return Surfe(method)
527
+ logger.warning("No interpolator")
528
+ return interpolator
529
+
530
+ def create_and_add_foliation(self, series_surface_data, **kwargs):
531
+ """
532
+ Parameters
533
+ ----------
534
+ series_surface_data : string
535
+ corresponding to the feature_name in the data
536
+ kwargs
537
+
538
+ Returns
539
+ -------
540
+ feature : GeologicalFeature
541
+ the created geological feature
542
+ """
543
+ if self.check_inialisation() == False:
544
+ return False
545
+ interpolator = self.get_interpolator(**kwargs)
546
+ series_builder = GeologicalFeatureInterpolator(interpolator,
547
+ name=series_surface_data,
548
+ **kwargs)
549
+ # add data
550
+ series_data = self.data[self.data['feature_name'] == series_surface_data]
551
+ if series_data.shape[0] == 0:
552
+ logger.warning("No data for %s, skipping" % series_surface_data)
553
+ return
554
+ series_builder.add_data_from_data_frame(series_data)
555
+ self._add_faults(series_builder)
556
+
557
+ series_builder.build_arguments = kwargs
558
+ self._add_feature(series_builder.feature)
559
+ return series_builder.feature
560
+
561
+
562
+ def _add_unconformity_above(self, feature):
563
+ """
564
+
565
+ Adds a region to the feature to prevent the value from being
566
+ interpolated where the unconformities exists above e.g.
567
+ if there is another feature above and the unconformity is at 0
568
+ then the features added below (after) will only be visible where the
569
+ uncomformity is <0
570
+
571
+ Parameters
572
+ ----------
573
+ feature - GeologicalFeature
574
+
575
+ Returns
576
+ -------
577
+
578
+ """
579
+ for f in reversed(self.features):
580
+ if f.type == 'unconformity':
581
+ feature.add_region(lambda pos: f.evaluate(pos))
582
+ break
583
+
584
+ def _add_unconformity_below(self, feature):
585
+ """
586
+ Adds a region to the features that represents the
587
+ unconformity so it is not evaluated below the unconformity
588
+
589
+ Parameters
590
+ ----------
591
+ feature
592
+
593
+ Returns
594
+ -------
595
+
596
+ """
597
+ for f in self.features:
598
+ if f.type == 'series' and feature.feature.name != f.name:
599
+ f.add_region(lambda pos: ~feature.evaluate(pos))
600
+
601
+ def add_unconformity(self, feature, value):
602
+ """
603
+ Use an existing feature to add an unconformity to the model.
604
+
605
+ Parameters
606
+ ----------
607
+ feature : GeologicalFeature
608
+ existing geological feature
609
+ value : float
610
+ scalar value of isosurface that represents
611
+
612
+ Returns
613
+ -------
614
+ unconformity : GeologicalFeature
615
+ unconformity feature
616
+
617
+ """
618
+ self.parameters['features'].append({'feature_type': 'unconformity', 'feature': feature, 'value': value})
619
+ uc_feature = UnconformityFeature(feature, value)
620
+
621
+ # for f in self.features:
622
+ # f.add_region(lambda pos: uc_feature.evaluate(pos))
623
+
624
+ # see if any unconformities are above this feature if so add region
625
+ # self._add_unconformity_above(uc_feature)
626
+ # self._add_unconformity_below(feature)#, uc_feature)
627
+ self._add_feature(uc_feature)
628
+
629
+
630
+ return uc_feature
631
+
632
+ def add_onlap_unconformity(self, feature, value):
633
+ """
634
+ Use an existing feature to add an unconformity to the model.
635
+
636
+ Parameters
637
+ ----------
638
+ feature : GeologicalFeature
639
+ existing geological feature
640
+ value : float
641
+ scalar value of isosurface that represents
642
+
643
+ Returns
644
+ -------
645
+ unconformity_feature : GeologicalFeature
646
+ the created unconformity
647
+
648
+ """
649
+ self.parameters['features'].append({'feature_type': 'onlap', 'feature': feature, 'value': value})
650
+
651
+ uc_feature = UnconformityFeature(feature, value)
652
+
653
+ # for f in self.features:
654
+ # f.add_region(lambda pos: uc_feature.evaluate(pos))
655
+
656
+ # see if any unconformities are above this feature if so add region
657
+ # self._add_unconformity_above(uc_feature)
658
+ self._add_unconformity_below(uc_feature) # , uc_feature)
659
+ self._add_feature(uc_feature)
660
+
661
+
662
+ return uc_feature
663
+
664
+ def create_and_add_fault(self, fault_surface_data, displacement, renormalise=True, **kwargs):
665
+ """
666
+ Parameters
667
+ ----------
668
+ fault_surface_data : string
669
+ name of the fault surface data in the dataframe
670
+ displacement : displacement magnitude
671
+ kwargs : additional kwargs for Fault and interpolators
672
+
673
+ Returns
674
+ -------
675
+ fault : FaultSegment
676
+ created fault
677
+ """
678
+ self.parameters['features'].append(
679
+ {'feature_type': 'fault', 'feature_name': fault_surface_data, 'displacement': displacement, **kwargs})
680
+
681
+ displacement_scaled = displacement / self.scale_factor
682
+ # create fault frame
683
+ interpolator = self.get_interpolator(**kwargs)
684
+ fault_frame_builder = StructuralFrameBuilder(interpolator,
685
+ name=fault_surface_data,
686
+ **kwargs)
687
+ # add data
688
+ fault_frame_data = self.data[
689
+ self.data['feature_name'] == fault_surface_data].copy()
690
+ if 'coord' not in fault_frame_data:
691
+ fault_frame_data['coord'] = 0
692
+ vals = fault_frame_data['val']
693
+ fault_frame_builder.add_data_from_data_frame(fault_frame_data)
694
+ # if there is no fault slip data then we could find the strike of
695
+ # the fault and build
696
+ # the second coordinate
697
+ # if we add a region to the fault then the fault operator doesn't
698
+ # work but for visualisation
699
+ # we want to add a region!
700
+ # check if this fault overprint any existing faults exist in the stack
701
+
702
+ fault_frame = fault_frame_builder.build(**kwargs)
703
+
704
+
705
+ fault = FaultSegment(fault_frame, displacement=displacement_scaled,
706
+ **kwargs)
707
+ # for f in reversed(self.features):
708
+ # if f.type == 'unconformity':
709
+ # fault.add_region(lambda pos: f.evaluate_value(pos) <= 0)
710
+ # break
711
+ # if displacement == 0:
712
+ # fault.type = 'fault_inactive'
713
+ # self._add_feature(fault)
714
+ self._add_feature(fault)
715
+
716
+ return fault
717
+
718
+ def rescale(self, points, inplace=True):
719
+ """
720
+ Convert from model scale to real world scale - in the future this
721
+ should also do transformations?
722
+
723
+ Parameters
724
+ ----------
725
+ points : np.array((N,3),dtype=double)
726
+ inplace : boolean
727
+ whether to return a modified copy or modify the original array
728
+
729
+ Returns
730
+ -------
731
+ points : np.array((N,3),dtype=double)
732
+
733
+ """
734
+ if inplace == False:
735
+ points = points.copy()
736
+ points *= self.scale_factor
737
+ points += self.origin
738
+ return points
739
+
740
+ def scale(self, points, inplace=True):
741
+ """ Take points in UTM coordinates and reproject
742
+ into scaled model space
743
+
744
+ Parameters
745
+ ----------
746
+ points : np.array((N,3),dtype=float)
747
+ points to
748
+ inplace : bool, optional default = True
749
+ whether to copy the points array or update the passed array
750
+ Returns
751
+ -------
752
+ points : np.array((N,3),dtype=double)
753
+
754
+ """
755
+ if inplace==False:
756
+ points = points.copy()
757
+
758
+ points[:, :] -= self.origin
759
+ points /= self.scale_factor
760
+ return points
761
+
762
+
763
+ def regular_grid(self, nsteps=(50, 50, 25), shuffle = True, rescale=False):
764
+ """
765
+ Return a regular grid within the model bounding box
766
+
767
+ Parameters
768
+ ----------
769
+ nsteps : tuple
770
+ number of cells in x,y,z
771
+
772
+ Returns
773
+ -------
774
+ xyz : np.array((N,3),dtype=float)
775
+ locations of points in regular grid
776
+ """
777
+ x = np.linspace(self.bounding_box[0, 0], self.bounding_box[1, 0],
778
+ nsteps[0])
779
+ y = np.linspace(self.bounding_box[0, 1], self.bounding_box[1, 1],
780
+ nsteps[1])
781
+ z = np.linspace(self.bounding_box[1, 2], self.bounding_box[0, 2],
782
+ nsteps[2])
783
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
784
+ locs = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
785
+ if shuffle:
786
+ logger.info("Shuffling points")
787
+ np.random.shuffle(locs)
788
+ if rescale:
789
+ locs = self.rescale(locs)
790
+ return locs
791
+
792
+ def evaluate_model(self, xyz, scale=True):
793
+ """Evaluate the stratigraphic id at each location
794
+
795
+
796
+ Parameters
797
+ ----------
798
+ xyz : np.array((N,3),dtype=float)
799
+ locations
800
+ scale : bool
801
+ whether to rescale the xyz before evaluating model
802
+
803
+ Returns
804
+ -------
805
+ stratigraphic_id : np.array(N,dtype=int)
806
+ the stratigraphic index for locations
807
+
808
+ Examples
809
+ --------
810
+ Evaluate on a voxet
811
+
812
+ >>> x = np.linspace(model.bounding_box[0, 0], model.bounding_box[1, 0],
813
+ nsteps[0])
814
+ >>> y = np.linspace(model.bounding_box[0, 1], model.bounding_box[1, 1],
815
+ nsteps[1])
816
+ >>> z = np.linspace(model.bounding_box[1, 2], model.bounding_box[0, 2],
817
+ nsteps[2])
818
+ >>> xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
819
+ >>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
820
+ >>> model.evaluate_model(xyz)
821
+
822
+ Evaluate on points defined by regular grid function
823
+
824
+ >>> model.evaluate_model(model.regular_grid())
825
+
826
+
827
+ Evaluate on a map
828
+
829
+ >>> x = np.linspace(self.bounding_box[0, 0], self.bounding_box[1, 0],
830
+ nsteps[0])
831
+ >>> y = np.linspace(self.bounding_box[0, 1], self.bounding_box[1, 1],
832
+ nsteps[1])
833
+ >>> xx, yy = np.meshgrid(x, y, indexing='ij')
834
+ >>> zz = np.zeros_like(yy)
835
+ >>> xyz = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
836
+ >>> model.evaluate_model(xyz)
837
+
838
+ """
839
+ if scale:
840
+ xyz = self.scale(xyz,inplace=False)
841
+ strat_id = np.zeros(xyz.shape[0],dtype=int)
842
+ for group in self.stratigraphic_column.keys():
843
+ if group == 'faults':
844
+ continue
845
+ feature_id = self.feature_name_index.get(group, -1)
846
+ if feature_id >= 0:
847
+ feature = self.features[feature_id]
848
+ vals = feature.evaluate_value(xyz)
849
+ for series in self.stratigraphic_column[group].values():
850
+ strat_id[np.logical_and(vals < series.get('max',feature.max()), vals > series.get('min',feature.min()))] = series['id']
851
+ if feature_id == -1:
852
+ logger.error('Model does not contain {}'.format(group))
853
+ return strat_id
854
+
855
+ def evaluate_fault_displacements(self,points,scale=True):
856
+ """Evaluate the fault displacement magnitude at each location
857
+
858
+
859
+ Parameters
860
+ ----------
861
+ xyz : np.array((N,3),dtype=float)
862
+ locations
863
+ scale : bool
864
+ whether to rescale the xyz before evaluating model
865
+
866
+ Returns
867
+ -------
868
+ fault_displacement : np.array(N,dtype=float)
869
+ the fault displacement magnitude
870
+ """
871
+ if scale:
872
+ points = self.scale(points,inplace=False)
873
+ vals = np.zeros(points.shape[0])
874
+ for f in self.features:
875
+ if f.type == 'fault':
876
+ disp = f.displacementfeature.evaluate_value(points)
877
+ vals[~np.isnan(disp)] += disp[~np.isnan(disp)]
878
+ return vals*-self.scale_factor # convert from restoration magnutude to displacement
879
+
880
+