LoopStructural 1.6.1__py3-none-any.whl → 1.6.6__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 (69) hide show
  1. LoopStructural/datatypes/_bounding_box.py +77 -7
  2. LoopStructural/datatypes/_point.py +67 -7
  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/_builders.py +141 -141
  9. LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
  10. LoopStructural/interpolators/_discrete_interpolator.py +100 -53
  11. LoopStructural/interpolators/_finite_difference_interpolator.py +78 -88
  12. LoopStructural/interpolators/_geological_interpolator.py +27 -10
  13. LoopStructural/interpolators/_interpolator_builder.py +55 -0
  14. LoopStructural/interpolators/_interpolator_factory.py +7 -18
  15. LoopStructural/interpolators/_p1interpolator.py +3 -3
  16. LoopStructural/interpolators/_surfe_wrapper.py +42 -12
  17. LoopStructural/interpolators/supports/_2d_base_unstructured.py +16 -0
  18. LoopStructural/interpolators/supports/_2d_structured_grid.py +44 -9
  19. LoopStructural/interpolators/supports/_3d_base_structured.py +28 -7
  20. LoopStructural/interpolators/supports/_3d_structured_grid.py +38 -12
  21. LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
  22. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +8 -2
  23. LoopStructural/interpolators/supports/__init__.py +7 -0
  24. LoopStructural/interpolators/supports/_base_support.py +7 -0
  25. LoopStructural/modelling/__init__.py +1 -3
  26. LoopStructural/modelling/core/geological_model.py +11 -12
  27. LoopStructural/modelling/features/__init__.py +1 -0
  28. LoopStructural/modelling/features/_analytical_feature.py +48 -18
  29. LoopStructural/modelling/features/_base_geological_feature.py +37 -8
  30. LoopStructural/modelling/features/_cross_product_geological_feature.py +7 -0
  31. LoopStructural/modelling/features/_geological_feature.py +50 -12
  32. LoopStructural/modelling/features/_projected_vector_feature.py +112 -0
  33. LoopStructural/modelling/features/_structural_frame.py +16 -18
  34. LoopStructural/modelling/features/_unconformity_feature.py +3 -3
  35. LoopStructural/modelling/features/builders/_base_builder.py +8 -0
  36. LoopStructural/modelling/features/builders/_folded_feature_builder.py +47 -16
  37. LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
  38. LoopStructural/modelling/features/builders/_structural_frame_builder.py +7 -2
  39. LoopStructural/modelling/features/fault/__init__.py +1 -1
  40. LoopStructural/modelling/features/fault/_fault_function.py +19 -1
  41. LoopStructural/modelling/features/fault/_fault_function_feature.py +3 -0
  42. LoopStructural/modelling/features/fault/_fault_segment.py +50 -53
  43. LoopStructural/modelling/features/fold/__init__.py +1 -2
  44. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
  45. LoopStructural/modelling/features/fold/_foldframe.py +4 -4
  46. LoopStructural/modelling/features/fold/_svariogram.py +81 -46
  47. LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
  48. LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
  49. LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
  50. LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
  51. LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
  52. LoopStructural/modelling/input/process_data.py +47 -26
  53. LoopStructural/modelling/input/project_file.py +49 -23
  54. LoopStructural/modelling/intrusions/intrusion_feature.py +3 -0
  55. LoopStructural/utils/__init__.py +1 -0
  56. LoopStructural/utils/_surface.py +18 -6
  57. LoopStructural/utils/_transformation.py +98 -14
  58. LoopStructural/utils/colours.py +50 -0
  59. LoopStructural/utils/features.py +5 -0
  60. LoopStructural/utils/maths.py +53 -1
  61. LoopStructural/version.py +1 -1
  62. LoopStructural-1.6.6.dist-info/METADATA +160 -0
  63. {LoopStructural-1.6.1.dist-info → LoopStructural-1.6.6.dist-info}/RECORD +66 -59
  64. {LoopStructural-1.6.1.dist-info → LoopStructural-1.6.6.dist-info}/WHEEL +1 -1
  65. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  66. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
  67. LoopStructural-1.6.1.dist-info/METADATA +0 -81
  68. {LoopStructural-1.6.1.dist-info → LoopStructural-1.6.6.dist-info}/LICENSE +0 -0
  69. {LoopStructural-1.6.1.dist-info → LoopStructural-1.6.6.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
@@ -112,7 +113,9 @@ class GeologicalFeature(BaseFeature):
112
113
  # if evaluation_points is not a numpy array try and convert
113
114
  # otherwise error
114
115
  evaluation_points = np.asarray(pos)
115
- self.builder.up_to_date()
116
+ # if there is a builder lets make sure that the feature is up to date
117
+ if self.builder is not None:
118
+ self.builder.up_to_date()
116
119
  # check if the points are within the display region
117
120
  v = np.zeros(evaluation_points.shape[0])
118
121
  v[:] = np.nan
@@ -131,7 +134,9 @@ class GeologicalFeature(BaseFeature):
131
134
  v[nanmask] = v[~nanmask][i]
132
135
  return v
133
136
 
134
- def evaluate_gradient(self, pos: np.ndarray, ignore_regions=False) -> np.ndarray:
137
+ def evaluate_gradient(
138
+ self, pos: np.ndarray, ignore_regions=False, element_scale_parameter=None
139
+ ) -> np.ndarray:
135
140
  """
136
141
 
137
142
  Parameters
@@ -147,6 +152,17 @@ class GeologicalFeature(BaseFeature):
147
152
  if pos.shape[1] != 3:
148
153
  raise LoopValueError("Need Nx3 array of xyz points to evaluate gradient")
149
154
  logger.info(f'Calculating gradient for {self.name}')
155
+ if element_scale_parameter is None:
156
+ if self.model is not None:
157
+ element_scale_parameter = np.min(self.model.bounding_box.step_vector) / 10
158
+ else:
159
+ element_scale_parameter = 1
160
+ else:
161
+ try:
162
+ element_scale_parameter = float(element_scale_parameter)
163
+ except ValueError:
164
+ logger.error("element_scale_parameter must be a float")
165
+ element_scale_parameter = 1
150
166
 
151
167
  self.builder.up_to_date()
152
168
 
@@ -156,16 +172,38 @@ class GeologicalFeature(BaseFeature):
156
172
  # evaluate the faults on the nodes of the faulted feature support
157
173
  # then evaluate the gradient at these points
158
174
  if len(self.faults) > 0:
175
+ # generate a regular tetrahedron for each point
176
+ # we will then move these points by the fault and then recalculate the gradient.
177
+ # this should work...
178
+ resolved = False
179
+ tetrahedron = regular_tetraherdron_for_points(pos, element_scale_parameter)
180
+
181
+ while resolved:
182
+ for f in self.faults:
183
+ v = (
184
+ f[0]
185
+ .evaluate_value(tetrahedron.reshape(-1, 3), fillnan='nearest')
186
+ .reshape(tetrahedron.shape[0], 4)
187
+ )
188
+ flag = np.logical_or(np.all(v > 0, axis=1), np.all(v < 0, axis=1))
189
+ if np.any(~flag):
190
+ logger.warning(
191
+ f"Points are too close to fault {f[0].name}. Refining the tetrahedron"
192
+ )
193
+ element_scale_parameter *= 0.5
194
+ tetrahedron = regular_tetraherdron_for_points(pos, element_scale_parameter)
195
+
196
+ resolved = True
197
+
198
+ tetrahedron_faulted = self._apply_faults(np.array(tetrahedron.reshape(-1, 3))).reshape(
199
+ tetrahedron.shape
200
+ )
201
+
202
+ values = self.interpolator.evaluate_value(tetrahedron_faulted.reshape(-1, 3)).reshape(
203
+ (-1, 4)
204
+ )
205
+ v[mask, :] = gradient_from_tetrahedron(tetrahedron[mask, :, :], values[mask])
159
206
 
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
207
  return v
170
208
  pos = self._apply_faults(pos)
171
209
  if mask.dtype not in [int, bool]:
@@ -0,0 +1,112 @@
1
+ """
2
+ """
3
+
4
+ import numpy as np
5
+ from typing import Optional
6
+
7
+ from ...modelling.features import BaseFeature
8
+
9
+ from ...utils import getLogger
10
+
11
+ logger = getLogger(__name__)
12
+
13
+
14
+ class ProjectedVectorFeature(BaseFeature):
15
+ def __init__(
16
+ self,
17
+ name: str,
18
+ vector: np.ndarray,
19
+ plane_feature: BaseFeature,
20
+ ):
21
+ """
22
+
23
+ Create a geological feature by projecting a vector onto a feature representing a plane
24
+ E.g. project a thickness vector onto an axial surface
25
+
26
+ Parameters
27
+ ----------
28
+ name: feature name
29
+ vector: the vector to project
30
+ plane_feature: the plane
31
+
32
+
33
+ Parameters
34
+ ----------
35
+ name : str
36
+ name of the feature
37
+ geological_feature_a : BaseFeature
38
+ Left hand side of cross product
39
+ geological_feature_b : BaseFeature
40
+ Right hand side of cross product
41
+ """
42
+ super().__init__(name)
43
+ self.plane_feature = plane_feature
44
+ self.vector = vector
45
+
46
+ self.value_feature = None
47
+
48
+ def evaluate_gradient(self, locations: np.ndarray, ignore_regions=False) -> np.ndarray:
49
+ """
50
+ Calculate the gradient of the geological feature by using numpy to
51
+ calculate the cross
52
+ product between the two existing feature gradients.
53
+ This means both features have to be evaluated for the locations
54
+
55
+ Parameters
56
+ ----------
57
+ locations
58
+
59
+ Returns
60
+ -------
61
+
62
+ """
63
+
64
+ # project s0 onto axis plane B X A X B
65
+ plane_normal = self.plane_feature.evaluate_gradient(locations, ignore_regions)
66
+ vector = np.tile(self.vector, (locations.shape[0], 1))
67
+
68
+ projected_vector = np.cross(
69
+ plane_normal, np.cross(vector, plane_normal, axisa=1, axisb=1), axisa=1, axisb=1
70
+ )
71
+ return projected_vector
72
+
73
+ def evaluate_value(self, evaluation_points: np.ndarray, ignore_regions=False) -> np.ndarray:
74
+ """
75
+ Return 0 because there is no value for this feature
76
+ Parameters
77
+ ----------
78
+ evaluation_points
79
+
80
+ Returns
81
+ -------
82
+
83
+ """
84
+ values = np.zeros(evaluation_points.shape[0])
85
+ if self.value_feature is not None:
86
+ values[:] = self.value_feature.evaluate_value(evaluation_points, ignore_regions)
87
+ return values
88
+
89
+ def mean(self):
90
+ if self.value_feature:
91
+ return self.value_feature.mean()
92
+ return 0.0
93
+
94
+ def min(self):
95
+ if self.value_feature:
96
+ return self.value_feature.min()
97
+ return 0.0
98
+
99
+ def max(self):
100
+ if self.value_feature:
101
+ return self.value_feature.max()
102
+ return 0.0
103
+
104
+ def get_data(self, value_map: Optional[dict] = None):
105
+ return
106
+
107
+ def copy(self, name: Optional[str] = None):
108
+ if name is None:
109
+ name = f'{self.name}_copy'
110
+ return ProjectedVectorFeature(
111
+ name=name, vector=self.vector, plane_feature=self.plane_feature
112
+ )
@@ -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]]:
@@ -184,3 +176,9 @@ class StructuralFrame(BaseFeature):
184
176
  for f in self.features:
185
177
  data.extend(f.get_data(value_map))
186
178
  return data
179
+
180
+ def copy(self, name: Optional[str] = None):
181
+ if name is None:
182
+ name = f'{self.name}_copy'
183
+ # !TODO check if this needs to be a deep copy
184
+ return StructuralFrame(name, self.features, self.fold, self.model)
@@ -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
- fold_axis_rotation.set_function(kwargs["axis_function"])
92
+ logger.error("axis_function is deprecated, use a specific fold rotation angle profile type")
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
- fold_limb_rotation.set_function(kwargs["limb_function"])
110
+ logger.error("limb_function is deprecated, use a specific fold rotation angle profile type")
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
@@ -110,8 +110,8 @@ class StructuralFrameBuilder:
110
110
  ) # ,region=self.region))
111
111
 
112
112
  self._frame = frame(
113
- self.name,
114
- [
113
+ name=self.name,
114
+ features=[
115
115
  self.builders[0].feature,
116
116
  self.builders[1].feature,
117
117
  self.builders[2].feature,
@@ -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
  """ """
@@ -80,3 +80,6 @@ class FaultDisplacementFeature(BaseFeature):
80
80
 
81
81
  def get_data(self, value_map: Optional[dict] = None):
82
82
  pass
83
+
84
+ def copy(self, name: Optional[str] = None):
85
+ raise NotImplementedError("Not implemented yet")