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.
- LoopStructural/__init__.py +36 -0
- LoopStructural/analysis/__init__.py +2 -0
- LoopStructural/analysis/_fault_displacement.py +67 -0
- LoopStructural/analysis/_fault_intersection.py +36 -0
- LoopStructural/analysis/_plane_fit.py +43 -0
- LoopStructural/datasets/__init__.py +12 -0
- LoopStructural/datasets/_base.py +65 -0
- LoopStructural/datasets/data/claudius.csv +21049 -0
- LoopStructural/datasets/data/claudiusbb.txt +2 -0
- LoopStructural/datasets/data/duplex.csv +126 -0
- LoopStructural/datasets/data/duplexbb.txt +2 -0
- LoopStructural/datasets/data/intrusion.csv +1017 -0
- LoopStructural/datasets/data/intrusionbb.txt +2 -0
- LoopStructural/datasets/data/onefoldbb.txt +2 -0
- LoopStructural/datasets/data/onefolddata.csv +2226 -0
- LoopStructural/datasets/data/refolded_bb.txt +2 -0
- LoopStructural/datasets/data/refolded_fold.csv +2126 -0
- LoopStructural/interpolators/__init__.py +34 -0
- LoopStructural/interpolators/base_structured_3d_support.py +101 -0
- LoopStructural/interpolators/cython/__init__.py +0 -0
- LoopStructural/interpolators/cython/dsi_helper.c +31046 -0
- LoopStructural/interpolators/cython/dsi_helper.cp38-win32.pyd +0 -0
- LoopStructural/interpolators/discrete_fold_interpolator.py +202 -0
- LoopStructural/interpolators/discrete_interpolator.py +584 -0
- LoopStructural/interpolators/finite_difference_interpolator.py +399 -0
- LoopStructural/interpolators/geological_interpolator.py +197 -0
- LoopStructural/interpolators/operator.py +61 -0
- LoopStructural/interpolators/piecewiselinear_interpolator.py +437 -0
- LoopStructural/interpolators/structured_grid.py +421 -0
- LoopStructural/interpolators/structured_tetra.py +681 -0
- LoopStructural/interpolators/surfe_wrapper.py +118 -0
- LoopStructural/modelling/__init__.py +46 -0
- LoopStructural/modelling/core/__init__.py +0 -0
- LoopStructural/modelling/core/geological_model.py +1513 -0
- LoopStructural/modelling/core/geological_model_graph.py +880 -0
- LoopStructural/modelling/core/stratigraphic_column.py +5 -0
- LoopStructural/modelling/fault/__init__.py +4 -0
- LoopStructural/modelling/fault/fault_builder.py +143 -0
- LoopStructural/modelling/fault/fault_function.py +188 -0
- LoopStructural/modelling/fault/fault_function_feature.py +76 -0
- LoopStructural/modelling/fault/fault_segment.py +298 -0
- LoopStructural/modelling/features/__init__.py +8 -0
- LoopStructural/modelling/features/cross_product_geological_feature.py +90 -0
- LoopStructural/modelling/features/geological_feature.py +252 -0
- LoopStructural/modelling/features/geological_feature_builder.py +414 -0
- LoopStructural/modelling/features/lambda_geological_feature.py +36 -0
- LoopStructural/modelling/features/region_feature.py +34 -0
- LoopStructural/modelling/features/structural_frame.py +133 -0
- LoopStructural/modelling/features/structural_frame_builder.py +206 -0
- LoopStructural/modelling/features/unconformity_feature.py +74 -0
- LoopStructural/modelling/fold/__init__.py +8 -0
- LoopStructural/modelling/fold/fold.py +143 -0
- LoopStructural/modelling/fold/fold_rotation_angle.py +133 -0
- LoopStructural/modelling/fold/fold_rotation_angle_feature.py +58 -0
- LoopStructural/modelling/fold/foldframe.py +193 -0
- LoopStructural/modelling/fold/svariogram.py +180 -0
- LoopStructural/utils/__init__.py +18 -0
- LoopStructural/utils/bounding_box.py +21 -0
- LoopStructural/utils/exceptions.py +10 -0
- LoopStructural/utils/helper.py +382 -0
- LoopStructural/utils/logging.py +60 -0
- LoopStructural/utils/map2loop.py +353 -0
- LoopStructural/utils/regions.py +11 -0
- LoopStructural/utils/utils.py +69 -0
- LoopStructural/visualisation/__init__.py +31 -0
- LoopStructural/visualisation/map_viewer.py +322 -0
- LoopStructural/visualisation/model_plotter.py +17 -0
- LoopStructural/visualisation/model_visualisation.py +1113 -0
- LoopStructural/visualisation/rotation_angle_plotter.py +83 -0
- LoopStructural/visualisation/sphinx_scraper.py +34 -0
- LoopStructural/visualisation/stratigraphic_column.py +60 -0
- LoopStructural-1.0.9.dist-info/LICENSE +21 -0
- LoopStructural-1.0.9.dist-info/METADATA +19 -0
- LoopStructural-1.0.9.dist-info/RECORD +77 -0
- LoopStructural-1.0.9.dist-info/WHEEL +5 -0
- LoopStructural-1.0.9.dist-info/top_level.txt +2 -0
- 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
|
+
|