LoopStructural 1.6.2__py3-none-any.whl → 1.6.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of LoopStructural might be problematic. Click here for more details.

Files changed (60) hide show
  1. LoopStructural/datatypes/_bounding_box.py +19 -4
  2. LoopStructural/datatypes/_point.py +36 -2
  3. LoopStructural/datatypes/_structured_grid.py +17 -0
  4. LoopStructural/datatypes/_surface.py +17 -0
  5. LoopStructural/export/omf_wrapper.py +49 -21
  6. LoopStructural/interpolators/__init__.py +13 -0
  7. LoopStructural/interpolators/_api.py +81 -13
  8. LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
  9. LoopStructural/interpolators/_discrete_interpolator.py +100 -53
  10. LoopStructural/interpolators/_finite_difference_interpolator.py +68 -78
  11. LoopStructural/interpolators/_geological_interpolator.py +27 -10
  12. LoopStructural/interpolators/_p1interpolator.py +3 -3
  13. LoopStructural/interpolators/_surfe_wrapper.py +42 -12
  14. LoopStructural/interpolators/supports/_2d_base_unstructured.py +16 -0
  15. LoopStructural/interpolators/supports/_2d_structured_grid.py +44 -9
  16. LoopStructural/interpolators/supports/_3d_base_structured.py +24 -7
  17. LoopStructural/interpolators/supports/_3d_structured_grid.py +38 -12
  18. LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
  19. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +8 -2
  20. LoopStructural/interpolators/supports/__init__.py +7 -0
  21. LoopStructural/interpolators/supports/_base_support.py +7 -0
  22. LoopStructural/modelling/__init__.py +1 -3
  23. LoopStructural/modelling/core/geological_model.py +2 -4
  24. LoopStructural/modelling/features/_analytical_feature.py +25 -16
  25. LoopStructural/modelling/features/_base_geological_feature.py +21 -8
  26. LoopStructural/modelling/features/_geological_feature.py +47 -11
  27. LoopStructural/modelling/features/_structural_frame.py +10 -18
  28. LoopStructural/modelling/features/_unconformity_feature.py +3 -3
  29. LoopStructural/modelling/features/builders/_base_builder.py +8 -0
  30. LoopStructural/modelling/features/builders/_folded_feature_builder.py +45 -14
  31. LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
  32. LoopStructural/modelling/features/builders/_structural_frame_builder.py +5 -0
  33. LoopStructural/modelling/features/fault/__init__.py +1 -1
  34. LoopStructural/modelling/features/fault/_fault_function.py +19 -1
  35. LoopStructural/modelling/features/fault/_fault_segment.py +40 -51
  36. LoopStructural/modelling/features/fold/__init__.py +1 -2
  37. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
  38. LoopStructural/modelling/features/fold/_foldframe.py +4 -4
  39. LoopStructural/modelling/features/fold/_svariogram.py +81 -46
  40. LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
  41. LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
  42. LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
  43. LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
  44. LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
  45. LoopStructural/modelling/input/process_data.py +47 -26
  46. LoopStructural/modelling/input/project_file.py +49 -23
  47. LoopStructural/utils/__init__.py +1 -0
  48. LoopStructural/utils/_surface.py +11 -4
  49. LoopStructural/utils/colours.py +26 -0
  50. LoopStructural/utils/features.py +5 -0
  51. LoopStructural/utils/maths.py +51 -0
  52. LoopStructural/version.py +1 -1
  53. LoopStructural-1.6.5.dist-info/METADATA +146 -0
  54. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/RECORD +57 -52
  55. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/WHEEL +1 -1
  56. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  57. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
  58. LoopStructural-1.6.2.dist-info/METADATA +0 -81
  59. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/LICENSE +0 -0
  60. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/top_level.txt +0 -0
@@ -2,10 +2,11 @@
2
2
  Geological features
3
3
  """
4
4
 
5
+ from LoopStructural.utils.maths import regular_tetraherdron_for_points, gradient_from_tetrahedron
5
6
  from ...modelling.features import BaseFeature
6
7
  from ...utils import getLogger
7
8
  from ...modelling.features import FeatureType
8
- from ...interpolators import GeologicalInterpolator, DiscreteInterpolator
9
+ from ...interpolators import GeologicalInterpolator
9
10
  import numpy as np
10
11
  from typing import Optional, List, Union
11
12
  from ...datatypes import ValuePoints, VectorPoints
@@ -131,7 +132,9 @@ class GeologicalFeature(BaseFeature):
131
132
  v[nanmask] = v[~nanmask][i]
132
133
  return v
133
134
 
134
- def evaluate_gradient(self, pos: np.ndarray, ignore_regions=False) -> np.ndarray:
135
+ def evaluate_gradient(
136
+ self, pos: np.ndarray, ignore_regions=False, element_scale_parameter=None
137
+ ) -> np.ndarray:
135
138
  """
136
139
 
137
140
  Parameters
@@ -147,6 +150,17 @@ class GeologicalFeature(BaseFeature):
147
150
  if pos.shape[1] != 3:
148
151
  raise LoopValueError("Need Nx3 array of xyz points to evaluate gradient")
149
152
  logger.info(f'Calculating gradient for {self.name}')
153
+ if element_scale_parameter is None:
154
+ if self.model is not None:
155
+ element_scale_parameter = np.min(self.model.bounding_box.step_vector) / 10
156
+ else:
157
+ element_scale_parameter = 1
158
+ else:
159
+ try:
160
+ element_scale_parameter = float(element_scale_parameter)
161
+ except ValueError:
162
+ logger.error("element_scale_parameter must be a float")
163
+ element_scale_parameter = 1
150
164
 
151
165
  self.builder.up_to_date()
152
166
 
@@ -156,16 +170,38 @@ class GeologicalFeature(BaseFeature):
156
170
  # evaluate the faults on the nodes of the faulted feature support
157
171
  # then evaluate the gradient at these points
158
172
  if len(self.faults) > 0:
173
+ # generate a regular tetrahedron for each point
174
+ # we will then move these points by the fault and then recalculate the gradient.
175
+ # this should work...
176
+ resolved = False
177
+ tetrahedron = regular_tetraherdron_for_points(pos, element_scale_parameter)
178
+
179
+ while resolved:
180
+ for f in self.faults:
181
+ v = (
182
+ f[0]
183
+ .evaluate_value(tetrahedron.reshape(-1, 3), fillnan='nearest')
184
+ .reshape(tetrahedron.shape[0], 4)
185
+ )
186
+ flag = np.logical_or(np.all(v > 0, axis=1), np.all(v < 0, axis=1))
187
+ if np.any(~flag):
188
+ logger.warning(
189
+ f"Points are too close to fault {f[0].name}. Refining the tetrahedron"
190
+ )
191
+ element_scale_parameter *= 0.5
192
+ tetrahedron = regular_tetraherdron_for_points(pos, element_scale_parameter)
193
+
194
+ resolved = True
195
+
196
+ tetrahedron_faulted = self._apply_faults(np.array(tetrahedron.reshape(-1, 3))).reshape(
197
+ tetrahedron.shape
198
+ )
199
+
200
+ values = self.interpolator.evaluate_value(tetrahedron_faulted.reshape(-1, 3)).reshape(
201
+ (-1, 4)
202
+ )
203
+ v[mask, :] = gradient_from_tetrahedron(tetrahedron[mask, :, :], values[mask])
159
204
 
160
- if issubclass(type(self.interpolator), DiscreteInterpolator):
161
- points = self.interpolator.support.nodes
162
- else:
163
- raise NotImplementedError(
164
- "Faulted feature gradients are only supported by DiscreteInterpolator at the moment."
165
- )
166
- points_faulted = self._apply_faults(points)
167
- values = self.interpolator.evaluate_value(points_faulted)
168
- v[mask, :] = self.interpolator.support.evaluate_gradient(pos[mask, :], values)
169
205
  return v
170
206
  pos = self._apply_faults(pos)
171
207
  if mask.dtype not in [int, bool]:
@@ -115,7 +115,7 @@ class StructuralFrame(BaseFeature):
115
115
  """
116
116
  return self.features[i]
117
117
 
118
- def evaluate_value(self, evaluation_points, ignore_regions=False):
118
+ def evaluate_value(self, pos, ignore_regions=False):
119
119
  """
120
120
  Evaluate the value of the structural frame for the points.
121
121
  Can optionally only evaluate one coordinate
@@ -129,14 +129,14 @@ class StructuralFrame(BaseFeature):
129
129
  -------
130
130
 
131
131
  """
132
- v = np.zeros(evaluation_points.shape) # create new 3d array of correct length
132
+ v = np.zeros(pos.shape) # create new 3d array of correct length
133
133
  v[:] = np.nan
134
- v[:, 0] = self.features[0].evaluate_value(evaluation_points, ignore_regions=ignore_regions)
135
- v[:, 1] = self.features[1].evaluate_value(evaluation_points, ignore_regions=ignore_regions)
136
- v[:, 2] = self.features[2].evaluate_value(evaluation_points, ignore_regions=ignore_regions)
134
+ v[:, 0] = self.features[0].evaluate_value(pos, ignore_regions=ignore_regions)
135
+ v[:, 1] = self.features[1].evaluate_value(pos, ignore_regions=ignore_regions)
136
+ v[:, 2] = self.features[2].evaluate_value(pos, ignore_regions=ignore_regions)
137
137
  return v
138
138
 
139
- def evaluate_gradient(self, evaluation_points, i=None, ignore_regions=False):
139
+ def evaluate_gradient(self, pos, i=None, ignore_regions=False):
140
140
  """
141
141
  Evaluate the gradient of the structural frame.
142
142
  Can optionally only evaluate the ith coordinate
@@ -151,19 +151,11 @@ class StructuralFrame(BaseFeature):
151
151
 
152
152
  """
153
153
  if i is not None:
154
- return self.features[i].support.evaluate_gradient(
155
- evaluation_points, ignore_regions=ignore_regions
156
- )
154
+ return self.features[i].support.evaluate_gradient(pos, ignore_regions=ignore_regions)
157
155
  return (
158
- self.features[0].support.evaluate_gradient(
159
- evaluation_points, ignore_regions=ignore_regions
160
- ),
161
- self.features[1].support.evaluate_gradient(
162
- evaluation_points, ignore_regions=ignore_regions
163
- ),
164
- self.features[2].support.evaluate_gradient(
165
- evaluation_points, ignore_regions=ignore_regions
166
- ),
156
+ self.features[0].support.evaluate_gradient(pos, ignore_regions=ignore_regions),
157
+ self.features[1].support.evaluate_gradient(pos, ignore_regions=ignore_regions),
158
+ self.features[2].support.evaluate_gradient(pos, ignore_regions=ignore_regions),
167
159
  )
168
160
 
169
161
  def get_data(self, value_map: Optional[dict] = None) -> List[Union[ValuePoints, VectorPoints]]:
@@ -19,7 +19,7 @@ class UnconformityFeature(GeologicalFeature):
19
19
  # just don't link the regions
20
20
  GeologicalFeature.__init__(
21
21
  self,
22
- name=f"{feature.name}_unconformity",
22
+ name=f"__{feature.name}_unconformity",
23
23
  faults=feature.faults,
24
24
  regions=[], # feature.regions.copy(), # don't want to share regionsbetween unconformity and # feature.regions,
25
25
  builder=feature.builder,
@@ -75,9 +75,9 @@ class UnconformityFeature(GeologicalFeature):
75
75
  true if above the unconformity, false if below
76
76
  """
77
77
  if self.sign:
78
- return self.evaluate_value(pos) < self.value
78
+ return self.evaluate_value(pos) <= self.value
79
79
  if not self.sign:
80
- return self.evaluate_value(pos) > self.value
80
+ return self.evaluate_value(pos) >= self.value
81
81
 
82
82
  def __call__(self, pos) -> np.ndarray:
83
83
  return self.evaluate(pos)
@@ -29,6 +29,12 @@ class BaseBuilder:
29
29
  self._build_arguments = {}
30
30
  self.faults = []
31
31
 
32
+ def set_not_up_to_date(self, caller):
33
+ logger.info(
34
+ f"Setting {self.name} to not up to date from an instance of {caller.__class__.__name__}"
35
+ )
36
+ self._up_to_date = False
37
+
32
38
  @property
33
39
  def model(self):
34
40
  return self._model
@@ -46,6 +52,7 @@ class BaseBuilder:
46
52
  # self._build_arguments = {}
47
53
  for k, i in build_arguments.items():
48
54
  if i != self._build_arguments.get(k, None):
55
+ logger.info(f"Setting {k} to {i} for {self.name}")
49
56
  self._build_arguments[k] = i
50
57
  ## if build_arguments change then flag to reinterpolate
51
58
  self._up_to_date = False
@@ -107,5 +114,6 @@ class BaseBuilder:
107
114
  -------
108
115
 
109
116
  """
117
+ logger.info(f'Adding fault {fault.name} to {self.name}')
110
118
  self._up_to_date = False
111
119
  self.faults.append(fault)
@@ -1,5 +1,5 @@
1
1
  from ....modelling.features.builders import GeologicalFeatureBuilder
2
- from ....modelling.features.fold import FoldRotationAngle
2
+ from ....modelling.features.fold.fold_function import FoldRotationType, get_fold_rotation_profile
3
3
  import numpy as np
4
4
 
5
5
  from ....utils import getLogger, InterpolatorError
@@ -19,6 +19,8 @@ class FoldedFeatureBuilder(GeologicalFeatureBuilder):
19
19
  name="Feature",
20
20
  region=None,
21
21
  svario=True,
22
+ axis_profile_type=FoldRotationType.FOURIER_SERIES,
23
+ limb_profile_type=FoldRotationType.FOURIER_SERIES,
22
24
  **kwargs,
23
25
  ):
24
26
  """Builder for creating a geological feature using fold constraints
@@ -36,6 +38,7 @@ class FoldedFeatureBuilder(GeologicalFeatureBuilder):
36
38
  region : _type_, optional
37
39
  _description_, by default None
38
40
  """
41
+ # create the feature builder, this intialises the interpolator
39
42
  GeologicalFeatureBuilder.__init__(
40
43
  self,
41
44
  interpolatortype=interpolatortype,
@@ -45,11 +48,27 @@ class FoldedFeatureBuilder(GeologicalFeatureBuilder):
45
48
  region=region,
46
49
  **kwargs,
47
50
  )
51
+ # link the interpolator to the fold object
48
52
  self.interpolator.fold = fold
49
53
  self.fold = fold
50
54
  self.fold_weights = fold_weights
51
55
  self.kwargs = kwargs
52
56
  self.svario = svario
57
+ self.axis_profile_type = axis_profile_type
58
+ self.limb_profile_type = limb_profile_type
59
+
60
+ @property
61
+ def fold_axis_rotation(self):
62
+ if self.fold.fold_axis_rotation is None:
63
+ self.set_fold_axis()
64
+ return self.fold.fold_axis_rotation
65
+
66
+ @property
67
+ def fold_limb_rotation(self):
68
+ _axis = self.fold.fold_axis # get axis to make sure its initialised
69
+ if self.fold.fold_limb_rotation is None:
70
+ self.set_fold_limb_rotation()
71
+ return self.fold.fold_limb_rotation
53
72
 
54
73
  def set_fold_axis(self):
55
74
  """calculates the fold axis/ fold axis rotation and adds this to the fold"""
@@ -67,31 +86,41 @@ class FoldedFeatureBuilder(GeologicalFeatureBuilder):
67
86
  if not self.fold.foldframe[1].is_valid():
68
87
  raise InterpolatorError("Fold frame direction coordinate is not valid")
69
88
  far, fad = self.fold.foldframe.calculate_fold_axis_rotation(self)
70
- fold_axis_rotation = FoldRotationAngle(far, fad, svario=self.svario)
71
- a_wl = kwargs.get("axis_wl", None)
89
+ fold_axis_rotation = get_fold_rotation_profile(self.axis_profile_type, far, fad)
72
90
  if "axis_function" in kwargs:
73
91
  # allow predefined function to be used
74
92
  fold_axis_rotation.set_function(kwargs["axis_function"])
75
93
  else:
76
- fold_axis_rotation.fit_fourier_series(wl=a_wl)
94
+ fold_axis_rotation.fit(params={'wavelength': kwargs.get("axis_wl", None)})
77
95
  self.fold.fold_axis_rotation = fold_axis_rotation
96
+ fold_axis_rotation.add_observer(self)
78
97
 
79
98
  def set_fold_limb_rotation(self):
80
99
  """Calculates the limb rotation of the fold and adds it to the fold object"""
81
100
  kwargs = self.kwargs
101
+ # need to calculate the fold axis before the fold limb rotation angle
102
+ if self.fold.fold_axis is None:
103
+ self.set_fold_axis()
82
104
  # give option of passing own fold limb rotation function
83
- flr, fld = self.fold.foldframe.calculate_fold_limb_rotation(
84
- self, self.fold.get_fold_axis_orientation
85
- )
86
- fold_limb_rotation = FoldRotationAngle(flr, fld, svario=self.svario)
87
- l_wl = kwargs.get("limb_wl", None)
105
+ flr, fld = self.calculate_fold_limb_rotation_angle()
106
+
107
+ fold_limb_rotation = get_fold_rotation_profile(self.limb_profile_type, flr, fld)
88
108
  if "limb_function" in kwargs:
89
109
  # allow for predefined functions to be used
90
110
  fold_limb_rotation.set_function(kwargs["limb_function"])
91
111
  else:
92
- fold_limb_rotation.fit_fourier_series(wl=l_wl, **kwargs)
112
+ fold_limb_rotation.fit(params={'wavelength': kwargs.get("limb_wl", None)})
113
+
93
114
  self.fold.fold_limb_rotation = fold_limb_rotation
115
+ fold_limb_rotation.add_observer(self)
116
+
117
+ def calculate_fold_limb_rotation_angle(self):
118
+ flr, fld = self.fold.foldframe.calculate_fold_limb_rotation(
119
+ self, self.fold.get_fold_axis_orientation
120
+ )
121
+ return flr, fld
94
122
 
123
+ # def
95
124
  def build(self, data_region=None, constrained=None, **kwargs):
96
125
  """the main function to run the interpolation and set up the parameters
97
126
 
@@ -115,15 +144,17 @@ class FoldedFeatureBuilder(GeologicalFeatureBuilder):
115
144
  self.add_data_to_interpolator(constrained=constrained)
116
145
  if not self.fold.foldframe[0].is_valid():
117
146
  raise InterpolatorError("Fold frame main coordinate is not valid")
118
- self.set_fold_axis()
119
- self.set_fold_limb_rotation()
147
+ if self.fold.fold_axis is None:
148
+ self.set_fold_axis()
149
+ if self.fold.fold_limb_rotation is None:
150
+ self.set_fold_limb_rotation()
120
151
  logger.info("Adding fold to {}".format(self.name))
121
152
  self.interpolator.fold = self.fold
122
153
  # if we have fold weights use those, otherwise just use default
123
154
  # self.interpolator.add_fold_constraints(**self.fold_weights)
124
- kwargs["fold_weights"] = self.fold_weights
155
+ # kwargs["fold_weights"] = self.fold_weights
125
156
  if "cgw" not in kwargs:
126
157
  # try adding very small cg
127
158
  kwargs["cgw"] = 0.0
128
159
  # now the fold is set up run the standard interpolation
129
- super().build(self, data_region=data_region, **kwargs)
160
+ return super().build(data_region=data_region, **kwargs)
@@ -67,7 +67,7 @@ class GeologicalFeatureBuilder(BaseBuilder):
67
67
  "interpolator is {} and must be a GeologicalInterpolator".format(type(interpolator))
68
68
  )
69
69
  self._interpolator = interpolator
70
-
70
+ self._up_to_date = self._interpolator.up_to_date
71
71
  header = (
72
72
  xyz_names()
73
73
  + val_name()
@@ -93,6 +93,13 @@ class GeologicalFeatureBuilder(BaseBuilder):
93
93
  self._orthogonal_features = {}
94
94
  self._equality_constraints = {}
95
95
 
96
+ def set_not_up_to_date(self, caller):
97
+ logger.info(
98
+ f"Setting {self.name} to not up to date from an instance of {caller.__class__.__name__}"
99
+ )
100
+ self._up_to_date = False
101
+ self._interpolator.up_to_date = False
102
+
96
103
  @property
97
104
  def interpolator(self):
98
105
  return self._interpolator
@@ -109,6 +116,7 @@ class GeologicalFeatureBuilder(BaseBuilder):
109
116
  else:
110
117
  self._interpolation_region = RegionEverywhere()
111
118
  self._interpolator.set_region(region=self._interpolation_region)
119
+ logger.info(f'Setting interpolation region {self.name}')
112
120
  self._up_to_date = False
113
121
 
114
122
  def add_data_from_data_frame(self, data_frame, overwrite=False):
@@ -156,6 +164,8 @@ class GeologicalFeatureBuilder(BaseBuilder):
156
164
  logger.error("Cannot cast {} as integer, setting step to 1".format(step))
157
165
  step = 1
158
166
  self._orthogonal_features[feature.name] = [feature, w, region, step, B]
167
+
168
+ logger.info(f"Adding orthogonal constraint {feature.name} to {self.name}")
159
169
  self._up_to_date = False
160
170
 
161
171
  def add_data_to_interpolator(self, constrained=False, force_constrained=False, **kwargs):
@@ -315,12 +325,13 @@ class GeologicalFeatureBuilder(BaseBuilder):
315
325
  self.interpolator.support.barycentre[element_idx[::step], :],
316
326
  vector[element_idx[::step], :],
317
327
  w=w,
318
- B=B,
328
+ b=B,
319
329
  name=f'{feature.name}_orthogonal',
320
330
  )
321
331
 
322
332
  def add_equality_constraints(self, feature, region, scalefactor=1.0):
323
333
  self._equality_constraints[feature.name] = [feature, region, scalefactor]
334
+ logger.info(f'Adding equality constraints to {self.name}')
324
335
  self._up_to_date = False
325
336
 
326
337
  def install_equality_constraints(self):
@@ -463,19 +474,22 @@ class GeologicalFeatureBuilder(BaseBuilder):
463
474
  Apply the fault to the model grid to ensure that the support
464
475
  is big enough to capture the faulted feature.
465
476
  """
477
+ if self.interpolator.support is not None:
466
478
 
467
- origin = self.interpolator.support.origin
468
- maximum = self.interpolator.support.maximum
469
- pts = self.model.bounding_box.with_buffer(buffer).regular_grid(local=True)
470
- for f in self.faults:
471
- pts = f.apply_to_points(pts)
479
+ origin = self.interpolator.support.origin
480
+ maximum = self.interpolator.support.maximum
481
+ pts = self.model.bounding_box.with_buffer(buffer).regular_grid(local=True)
482
+ for f in self.faults:
483
+ pts = f.apply_to_points(pts)
472
484
 
473
- origin[origin > np.min(pts, axis=0)] = np.min(pts, axis=0)[origin > np.min(pts, axis=0)]
474
- maximum[maximum < np.max(pts, axis=0)] = np.max(pts, axis=0)[maximum < np.max(pts, axis=0)]
475
- self.interpolator.support.origin = origin
476
- self.interpolator.support.maximum = maximum
485
+ origin[origin > np.min(pts, axis=0)] = np.min(pts, axis=0)[origin > np.min(pts, axis=0)]
486
+ maximum[maximum < np.max(pts, axis=0)] = np.max(pts, axis=0)[
487
+ maximum < np.max(pts, axis=0)
488
+ ]
489
+ self.interpolator.support.origin = origin
490
+ self.interpolator.support.maximum = maximum
477
491
 
478
- def build(self, fold=None, fold_weights={}, data_region=None, **kwargs):
492
+ def build(self, data_region=None, **kwargs):
479
493
  """
480
494
  Runs the interpolation and builds the geological feature
481
495
 
@@ -536,7 +550,9 @@ class GeologicalFeatureBuilder(BaseBuilder):
536
550
  logger.info(f'running interpolation for {self.name}')
537
551
 
538
552
  self.interpolator.solve_system(
539
- solver=kwargs.get('solver', None), solver_kwargs=kwargs.get('solver_kwargs', {})
553
+ solver=kwargs.get('solver', 'cg'),
554
+ tol=kwargs.get('tol', None),
555
+ solver_kwargs=kwargs.get('solver_kwargs', {}),
540
556
  )
541
557
  logger.info(f'Finished building {self.name}')
542
558
  self._up_to_date = True
@@ -235,3 +235,8 @@ class StructuralFrameBuilder:
235
235
  def up_to_date(self, callback=None):
236
236
  for i in range(3):
237
237
  self.builders[i].up_to_date(callback=callback)
238
+
239
+ def set_not_up_to_date(self, caller):
240
+
241
+ for i in range(3):
242
+ self.builders[i].set_not_up_to_date(caller)
@@ -1,3 +1,3 @@
1
- from ._fault_function import Composite, CubicFunction, Ones, Zeros
1
+ from ._fault_function import Composite, CubicFunction, Ones, Zeros, FaultDisplacement
2
2
  from ._fault_function_feature import FaultDisplacementFeature
3
3
  from ._fault_segment import FaultSegment
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import abstractmethod, ABCMeta
4
- from typing import Optional
4
+ from typing import Optional, List
5
5
  import numpy as np
6
6
 
7
7
  from ....utils import getLogger
@@ -367,6 +367,24 @@ class FaultDisplacement:
367
367
  gz = CubicFunction.from_dict(data["gz"])
368
368
  return cls(gx=gx, gy=gy, gz=gz)
369
369
 
370
+ def plot(self, range=(-1, 1), axs: Optional[List] = None):
371
+ try:
372
+ import matplotlib.pyplot as plt
373
+
374
+ if axs is None:
375
+ fig, ax = plt.subplots(1, 3, figsize=(15, 5))
376
+ for i, (name, f) in enumerate(zip(["gx", "gy", "gz"], [self.gx, self.gy, self.gz])):
377
+ x = np.linspace(range[0], range[1], 100)
378
+ ax[i].plot(x, f(x), label=name)
379
+ ax[i].set_title(name)
380
+ ax[i].set_xlabel("Fault frame coordinate")
381
+ ax[i].set_ylabel("Displacement")
382
+ ax[i].legend()
383
+
384
+ except ImportError:
385
+ logger.warning("matplotlib not installed, not plotting")
386
+ return
387
+
370
388
 
371
389
  class BaseFault(object):
372
390
  """ """
@@ -1,8 +1,9 @@
1
+ from LoopStructural.utils.maths import regular_tetraherdron_for_points, gradient_from_tetrahedron
1
2
  from ....modelling.features.fault._fault_function_feature import (
2
3
  FaultDisplacementFeature,
3
4
  )
4
5
  from ....modelling.features import FeatureType
5
- from ....modelling.features.fault._fault_function import BaseFault, BaseFault3D
6
+ from ....modelling.features.fault._fault_function import BaseFault, BaseFault3D, FaultDisplacement
6
7
  from ....utils import getLogger, NegativeRegion, PositiveRegion
7
8
  from ....modelling.features import StructuralFrame
8
9
 
@@ -39,7 +40,7 @@ class FaultSegment(StructuralFrame):
39
40
  StructuralFrame.__init__(self, features, name, fold, model)
40
41
  self.type = FeatureType.FAULT
41
42
  self.displacement = displacement
42
- self._faultfunction = BaseFault.fault_displacement
43
+ self._faultfunction = BaseFault().fault_displacement
43
44
  self.steps = steps
44
45
  self.regions = []
45
46
  self.faults_enabled = True
@@ -57,10 +58,13 @@ class FaultSegment(StructuralFrame):
57
58
  def faultfunction(self, value):
58
59
  if callable(value):
59
60
  self._faultfunction = value
61
+ if issubclass(FaultDisplacement, type(value)):
62
+ self._faultfunction = value
63
+
60
64
  elif isinstance(value, str) and value == "BaseFault":
61
- self._faultfunction = BaseFault.fault_displacement
65
+ self._faultfunction = BaseFault().fault_displacement
62
66
  elif isinstance(value, str) and value == "BaseFault3D":
63
- self._faultfunction = BaseFault3D.fault_displacement
67
+ self._faultfunction = BaseFault3D().fault_displacement
64
68
  else:
65
69
  raise ValueError("Fault function must be a function or BaseFault")
66
70
 
@@ -110,6 +114,26 @@ class FaultSegment(StructuralFrame):
110
114
  def displacementfeature(self):
111
115
  return FaultDisplacementFeature(self, self.faultfunction, name=self.name, model=self.model)
112
116
 
117
+ def fault_ellipsoid(self, **kwargs):
118
+ try:
119
+ import pyvista as pv
120
+
121
+ fault_ellipsoid = pv.PolyData(
122
+ self.model.rescale(self.fault_centre[None, :], inplace=False)
123
+ )
124
+ fault_ellipsoid["norm"] = self.builder.fault_normal_vector[None, :]
125
+
126
+ geom = pv.ParametricEllipsoid(
127
+ self.fault_minor_axis,
128
+ self.fault_major_axis,
129
+ self.fault_intermediate_axis,
130
+ )
131
+ ellipsoid = fault_ellipsoid.glyph(geom=geom, **kwargs)
132
+ return ellipsoid
133
+ except ImportError:
134
+ logger.error("pyvista not installed")
135
+ return None
136
+
113
137
  def set_fault_offset(self, offset: float):
114
138
  self.fault_offset = offset
115
139
 
@@ -173,7 +197,7 @@ class FaultSegment(StructuralFrame):
173
197
  return np.abs(v) > threshold
174
198
  # return np.all(np.logical_and(v > -1,v<1),axis=1)
175
199
 
176
- def evaluate_value(self, locations):
200
+ def evaluate_value(self, locations, ignore_regions=False):
177
201
  """
178
202
  Return the value of the fault surface scalar field
179
203
 
@@ -198,7 +222,10 @@ class FaultSegment(StructuralFrame):
198
222
  # except:
199
223
  # logger.error("nan slicing")
200
224
  # v[mask] = self.__getitem__(0).evaluate_value(locations[mask, :])
201
- return self.__getitem__(0).evaluate_value(locations)
225
+ return self.__getitem__(0).evaluate_value(locations, ignore_regions=ignore_regions)
226
+
227
+ def ellipsoid(self):
228
+ pass
202
229
 
203
230
  def mean(self):
204
231
  return self.__getitem__(0).mean()
@@ -282,6 +309,7 @@ class FaultSegment(StructuralFrame):
282
309
  -------
283
310
 
284
311
  """
312
+ logger.info(f'Applying fault {self.name} to points {points.shape}')
285
313
  steps = self.steps
286
314
  newp = np.copy(points).astype(float)
287
315
  # evaluate fault function for all points
@@ -389,51 +417,12 @@ class FaultSegment(StructuralFrame):
389
417
  # the nodes of the tetrahedron are then restored by the fault and then gradient is
390
418
  # recalculated using the updated node positions but the original corner values
391
419
 
392
- regular_tetrahedron = np.array(
393
- [
394
- [np.sqrt(8 / 9), 0, -1 / 3],
395
- [-np.sqrt(2 / 9), np.sqrt(2 / 3), -1 / 3],
396
- [-np.sqrt(2 / 9), -np.sqrt(2 / 3), -1 / 3],
397
- [0, 0, 1],
398
- ]
399
- )
400
- regular_tetrahedron *= scale_parameter
401
- xyz = vector[:, :3]
402
- tetrahedron = np.zeros((xyz.shape[0], 4, 3))
403
- tetrahedron[:] = xyz[:, None, :]
404
- tetrahedron[:, :, :] += regular_tetrahedron[None, :, :]
405
-
406
- vectors = vector[:, 3:]
407
- corners = np.einsum('ikj,ij->ik', tetrahedron - xyz[:, None, :], vectors)
408
- tetrahedron = tetrahedron.reshape(-1, 3)
409
- tetrahedron = self.apply_to_points(tetrahedron)
410
- tetrahedron = tetrahedron.reshape(-1, 4, 3)
411
- m = np.array(
412
- [
413
- [
414
- (tetrahedron[:, 1, 0] - tetrahedron[:, 0, 0]),
415
- (tetrahedron[:, 1, 1] - tetrahedron[:, 0, 1]),
416
- (tetrahedron[:, 1, 2] - tetrahedron[:, 0, 2]),
417
- ],
418
- [
419
- (tetrahedron[:, 2, 0] - tetrahedron[:, 0, 0]),
420
- (tetrahedron[:, 2, 1] - tetrahedron[:, 0, 1]),
421
- (tetrahedron[:, 2, 2] - tetrahedron[:, 0, 2]),
422
- ],
423
- [
424
- (tetrahedron[:, 3, 0] - tetrahedron[:, 0, 0]),
425
- (tetrahedron[:, 3, 1] - tetrahedron[:, 0, 1]),
426
- (tetrahedron[:, 3, 2] - tetrahedron[:, 0, 2]),
427
- ],
428
- ]
429
- )
430
- I = np.array([[-1.0, 1.0, 0.0, 0.0], [-1.0, 0.0, 1.0, 0.0], [-1.0, 0.0, 0.0, 1.0]])
431
- m = np.swapaxes(m, 0, 2)
432
- element_gradients = np.linalg.inv(m)
433
-
434
- element_gradients = element_gradients.swapaxes(1, 2)
435
- element_gradients = element_gradients @ I
436
- v = np.sum(element_gradients * corners[:, None, :], axis=2)
420
+ tetrahedron = regular_tetraherdron_for_points(vector[:, :3], scale_parameter)
421
+ corners = np.einsum('ikj,ij->ik', tetrahedron - vector[:, None, :3], vector[:, 3:])
422
+
423
+ tetrahedron = self.apply_to_points(tetrahedron.reshape(-1, 3)).reshape(-1, 4, 3)
424
+ v = gradient_from_tetrahedron(tetrahedron, corners)
425
+
437
426
  return v
438
427
 
439
428
  def add_abutting_fault(self, abutting_fault_feature, positive=None):
@@ -4,6 +4,5 @@
4
4
 
5
5
  from ._fold import FoldEvent
6
6
  from ._svariogram import SVariogram
7
- from ._fold_rotation_angle_feature import FoldRotationAngleFeature, fourier_series
7
+ from ._fold_rotation_angle_feature import FoldRotationAngleFeature
8
8
  from ._foldframe import FoldFrame
9
- from ._fold_rotation_angle import FoldRotationAngle
@@ -1,6 +1,4 @@
1
- import numpy as np
2
1
  from ....modelling.features import BaseFeature
3
-
4
2
  from ....utils import getLogger
5
3
 
6
4
  logger = getLogger(__name__)
@@ -44,24 +42,3 @@ class FoldRotationAngleFeature(BaseFeature):
44
42
  s1 = self.fold_frame.features[0].evaluate_value(location)
45
43
  r = self.rotation(s1)
46
44
  return r
47
-
48
-
49
- def fourier_series(x, c0, c1, c2, w):
50
- """
51
-
52
- Parameters
53
- ----------
54
- x
55
- c0
56
- c1
57
- c2
58
- w
59
-
60
- Returns
61
- -------
62
-
63
- """
64
- v = np.array(x.astype(float))
65
- # v.fill(c0)
66
- v = c0 + c1 * np.cos(2 * np.pi / w * x) + c2 * np.sin(2 * np.pi / w * x)
67
- return np.rad2deg(np.arctan(v))