LoopStructural 1.6.1__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 (129) hide show
  1. LoopStructural/__init__.py +52 -0
  2. LoopStructural/datasets/__init__.py +23 -0
  3. LoopStructural/datasets/_base.py +301 -0
  4. LoopStructural/datasets/_example_models.py +10 -0
  5. LoopStructural/datasets/data/claudius.csv +21049 -0
  6. LoopStructural/datasets/data/claudiusbb.txt +2 -0
  7. LoopStructural/datasets/data/duplex.csv +126 -0
  8. LoopStructural/datasets/data/duplexbb.txt +2 -0
  9. LoopStructural/datasets/data/fault_trace/fault_trace.cpg +1 -0
  10. LoopStructural/datasets/data/fault_trace/fault_trace.dbf +0 -0
  11. LoopStructural/datasets/data/fault_trace/fault_trace.prj +1 -0
  12. LoopStructural/datasets/data/fault_trace/fault_trace.shp +0 -0
  13. LoopStructural/datasets/data/fault_trace/fault_trace.shx +0 -0
  14. LoopStructural/datasets/data/geological_map_data/bbox.csv +2 -0
  15. LoopStructural/datasets/data/geological_map_data/contacts.csv +657 -0
  16. LoopStructural/datasets/data/geological_map_data/fault_displacement.csv +7 -0
  17. LoopStructural/datasets/data/geological_map_data/fault_edges.txt +2 -0
  18. LoopStructural/datasets/data/geological_map_data/fault_locations.csv +79 -0
  19. LoopStructural/datasets/data/geological_map_data/fault_orientations.csv +19 -0
  20. LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv +13 -0
  21. LoopStructural/datasets/data/geological_map_data/stratigraphic_orientations.csv +207 -0
  22. LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv +13 -0
  23. LoopStructural/datasets/data/intrusion.csv +1017 -0
  24. LoopStructural/datasets/data/intrusionbb.txt +2 -0
  25. LoopStructural/datasets/data/onefoldbb.txt +2 -0
  26. LoopStructural/datasets/data/onefolddata.csv +2226 -0
  27. LoopStructural/datasets/data/refolded_bb.txt +2 -0
  28. LoopStructural/datasets/data/refolded_fold.csv +205 -0
  29. LoopStructural/datasets/data/tabular_intrusion.csv +23 -0
  30. LoopStructural/datatypes/__init__.py +4 -0
  31. LoopStructural/datatypes/_bounding_box.py +422 -0
  32. LoopStructural/datatypes/_point.py +166 -0
  33. LoopStructural/datatypes/_structured_grid.py +94 -0
  34. LoopStructural/datatypes/_surface.py +184 -0
  35. LoopStructural/export/exporters.py +554 -0
  36. LoopStructural/export/file_formats.py +15 -0
  37. LoopStructural/export/geoh5.py +100 -0
  38. LoopStructural/export/gocad.py +126 -0
  39. LoopStructural/export/omf_wrapper.py +88 -0
  40. LoopStructural/interpolators/__init__.py +105 -0
  41. LoopStructural/interpolators/_api.py +143 -0
  42. LoopStructural/interpolators/_builders.py +149 -0
  43. LoopStructural/interpolators/_cython/__init__.py +0 -0
  44. LoopStructural/interpolators/_discrete_fold_interpolator.py +183 -0
  45. LoopStructural/interpolators/_discrete_interpolator.py +692 -0
  46. LoopStructural/interpolators/_finite_difference_interpolator.py +470 -0
  47. LoopStructural/interpolators/_geological_interpolator.py +380 -0
  48. LoopStructural/interpolators/_interpolator_factory.py +89 -0
  49. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  50. LoopStructural/interpolators/_operator.py +38 -0
  51. LoopStructural/interpolators/_p1interpolator.py +228 -0
  52. LoopStructural/interpolators/_p2interpolator.py +277 -0
  53. LoopStructural/interpolators/_surfe_wrapper.py +174 -0
  54. LoopStructural/interpolators/supports/_2d_base_unstructured.py +340 -0
  55. LoopStructural/interpolators/supports/_2d_p1_unstructured.py +68 -0
  56. LoopStructural/interpolators/supports/_2d_p2_unstructured.py +288 -0
  57. LoopStructural/interpolators/supports/_2d_structured_grid.py +462 -0
  58. LoopStructural/interpolators/supports/_2d_structured_tetra.py +0 -0
  59. LoopStructural/interpolators/supports/_3d_base_structured.py +467 -0
  60. LoopStructural/interpolators/supports/_3d_p2_tetra.py +331 -0
  61. LoopStructural/interpolators/supports/_3d_structured_grid.py +470 -0
  62. LoopStructural/interpolators/supports/_3d_structured_tetra.py +746 -0
  63. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +637 -0
  64. LoopStructural/interpolators/supports/__init__.py +55 -0
  65. LoopStructural/interpolators/supports/_aabb.py +77 -0
  66. LoopStructural/interpolators/supports/_base_support.py +114 -0
  67. LoopStructural/interpolators/supports/_face_table.py +70 -0
  68. LoopStructural/interpolators/supports/_support_factory.py +32 -0
  69. LoopStructural/modelling/__init__.py +29 -0
  70. LoopStructural/modelling/core/__init__.py +0 -0
  71. LoopStructural/modelling/core/geological_model.py +1867 -0
  72. LoopStructural/modelling/features/__init__.py +32 -0
  73. LoopStructural/modelling/features/_analytical_feature.py +79 -0
  74. LoopStructural/modelling/features/_base_geological_feature.py +364 -0
  75. LoopStructural/modelling/features/_cross_product_geological_feature.py +100 -0
  76. LoopStructural/modelling/features/_geological_feature.py +288 -0
  77. LoopStructural/modelling/features/_lambda_geological_feature.py +93 -0
  78. LoopStructural/modelling/features/_region.py +18 -0
  79. LoopStructural/modelling/features/_structural_frame.py +186 -0
  80. LoopStructural/modelling/features/_unconformity_feature.py +83 -0
  81. LoopStructural/modelling/features/builders/__init__.py +5 -0
  82. LoopStructural/modelling/features/builders/_base_builder.py +111 -0
  83. LoopStructural/modelling/features/builders/_fault_builder.py +590 -0
  84. LoopStructural/modelling/features/builders/_folded_feature_builder.py +129 -0
  85. LoopStructural/modelling/features/builders/_geological_feature_builder.py +543 -0
  86. LoopStructural/modelling/features/builders/_structural_frame_builder.py +237 -0
  87. LoopStructural/modelling/features/fault/__init__.py +3 -0
  88. LoopStructural/modelling/features/fault/_fault_function.py +444 -0
  89. LoopStructural/modelling/features/fault/_fault_function_feature.py +82 -0
  90. LoopStructural/modelling/features/fault/_fault_segment.py +505 -0
  91. LoopStructural/modelling/features/fold/__init__.py +9 -0
  92. LoopStructural/modelling/features/fold/_fold.py +167 -0
  93. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +149 -0
  94. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +67 -0
  95. LoopStructural/modelling/features/fold/_foldframe.py +194 -0
  96. LoopStructural/modelling/features/fold/_svariogram.py +188 -0
  97. LoopStructural/modelling/input/__init__.py +2 -0
  98. LoopStructural/modelling/input/fault_network.py +80 -0
  99. LoopStructural/modelling/input/map2loop_processor.py +165 -0
  100. LoopStructural/modelling/input/process_data.py +650 -0
  101. LoopStructural/modelling/input/project_file.py +84 -0
  102. LoopStructural/modelling/intrusions/__init__.py +25 -0
  103. LoopStructural/modelling/intrusions/geom_conceptual_models.py +142 -0
  104. LoopStructural/modelling/intrusions/geometric_scaling_functions.py +123 -0
  105. LoopStructural/modelling/intrusions/intrusion_builder.py +672 -0
  106. LoopStructural/modelling/intrusions/intrusion_feature.py +410 -0
  107. LoopStructural/modelling/intrusions/intrusion_frame_builder.py +971 -0
  108. LoopStructural/modelling/intrusions/intrusion_support_functions.py +460 -0
  109. LoopStructural/utils/__init__.py +38 -0
  110. LoopStructural/utils/_surface.py +143 -0
  111. LoopStructural/utils/_transformation.py +76 -0
  112. LoopStructural/utils/config.py +18 -0
  113. LoopStructural/utils/dtm_creator.py +17 -0
  114. LoopStructural/utils/exceptions.py +31 -0
  115. LoopStructural/utils/helper.py +292 -0
  116. LoopStructural/utils/json_encoder.py +18 -0
  117. LoopStructural/utils/linalg.py +8 -0
  118. LoopStructural/utils/logging.py +79 -0
  119. LoopStructural/utils/maths.py +245 -0
  120. LoopStructural/utils/regions.py +103 -0
  121. LoopStructural/utils/typing.py +7 -0
  122. LoopStructural/utils/utils.py +68 -0
  123. LoopStructural/version.py +1 -0
  124. LoopStructural/visualisation/__init__.py +11 -0
  125. LoopStructural-1.6.1.dist-info/LICENSE +21 -0
  126. LoopStructural-1.6.1.dist-info/METADATA +81 -0
  127. LoopStructural-1.6.1.dist-info/RECORD +129 -0
  128. LoopStructural-1.6.1.dist-info/WHEEL +5 -0
  129. LoopStructural-1.6.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,129 @@
1
+ from ....modelling.features.builders import GeologicalFeatureBuilder
2
+ from ....modelling.features.fold import FoldRotationAngle
3
+ import numpy as np
4
+
5
+ from ....utils import getLogger, InterpolatorError
6
+ from ....datatypes import BoundingBox
7
+
8
+ logger = getLogger(__name__)
9
+
10
+
11
+ class FoldedFeatureBuilder(GeologicalFeatureBuilder):
12
+ def __init__(
13
+ self,
14
+ interpolatortype: str,
15
+ bounding_box: BoundingBox,
16
+ fold,
17
+ nelements: int = 1000,
18
+ fold_weights={},
19
+ name="Feature",
20
+ region=None,
21
+ svario=True,
22
+ **kwargs,
23
+ ):
24
+ """Builder for creating a geological feature using fold constraints
25
+
26
+ Parameters
27
+ ----------
28
+ interpolator : GeologicalInterpolator
29
+ the interpolator to add the fold constraints to
30
+ fold : FoldEvent
31
+ a fold event object that contains the geometry of the fold
32
+ fold_weights : dict, optional
33
+ interpolation weights for the fold, by default {}
34
+ name : str, optional
35
+ name of the geological feature, by default "Feature"
36
+ region : _type_, optional
37
+ _description_, by default None
38
+ """
39
+ GeologicalFeatureBuilder.__init__(
40
+ self,
41
+ interpolatortype=interpolatortype,
42
+ bounding_box=bounding_box,
43
+ nelements=nelements,
44
+ name=name,
45
+ region=region,
46
+ **kwargs,
47
+ )
48
+ self.interpolator.fold = fold
49
+ self.fold = fold
50
+ self.fold_weights = fold_weights
51
+ self.kwargs = kwargs
52
+ self.svario = svario
53
+
54
+ def set_fold_axis(self):
55
+ """calculates the fold axis/ fold axis rotation and adds this to the fold"""
56
+ kwargs = self.kwargs
57
+ fold_axis = kwargs.get("fold_axis", None)
58
+ if fold_axis is not None:
59
+ fold_axis = np.array(fold_axis)
60
+ if len(fold_axis.shape) == 1:
61
+ self.fold.fold_axis = fold_axis
62
+
63
+ if "av_fold_axis" in kwargs:
64
+ l2 = self.fold.foldframe.calculate_intersection_lineation(self)
65
+ self.fold.fold_axis = np.mean(l2, axis=0)
66
+ if self.fold.fold_axis is None:
67
+ if not self.fold.foldframe[1].is_valid():
68
+ raise InterpolatorError("Fold frame direction coordinate is not valid")
69
+ 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)
72
+ if "axis_function" in kwargs:
73
+ # allow predefined function to be used
74
+ fold_axis_rotation.set_function(kwargs["axis_function"])
75
+ else:
76
+ fold_axis_rotation.fit_fourier_series(wl=a_wl)
77
+ self.fold.fold_axis_rotation = fold_axis_rotation
78
+
79
+ def set_fold_limb_rotation(self):
80
+ """Calculates the limb rotation of the fold and adds it to the fold object"""
81
+ kwargs = self.kwargs
82
+ # 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)
88
+ if "limb_function" in kwargs:
89
+ # allow for predefined functions to be used
90
+ fold_limb_rotation.set_function(kwargs["limb_function"])
91
+ else:
92
+ fold_limb_rotation.fit_fourier_series(wl=l_wl, **kwargs)
93
+ self.fold.fold_limb_rotation = fold_limb_rotation
94
+
95
+ def build(self, data_region=None, constrained=None, **kwargs):
96
+ """the main function to run the interpolation and set up the parameters
97
+
98
+ Parameters
99
+ ----------
100
+ data_region : [type], optional
101
+ [description], by default None
102
+ """
103
+ # add the data to the interpolator and force constraints to be
104
+ # gradient not norm, to prevent issues with fold norm constraint
105
+ # TODO folding norm constraint should be minimising the difference in norm
106
+ # not setting the norm
107
+
108
+ # Use norm constraints if the fold normalisation weight is 0.
109
+ if constrained is None:
110
+ if "fold_normalisation" in kwargs:
111
+ if kwargs["fold_normalisation"] == 0.0:
112
+ constrained = False
113
+ else:
114
+ constrained = True
115
+ self.add_data_to_interpolator(constrained=constrained)
116
+ if not self.fold.foldframe[0].is_valid():
117
+ raise InterpolatorError("Fold frame main coordinate is not valid")
118
+ self.set_fold_axis()
119
+ self.set_fold_limb_rotation()
120
+ logger.info("Adding fold to {}".format(self.name))
121
+ self.interpolator.fold = self.fold
122
+ # if we have fold weights use those, otherwise just use default
123
+ # self.interpolator.add_fold_constraints(**self.fold_weights)
124
+ kwargs["fold_weights"] = self.fold_weights
125
+ if "cgw" not in kwargs:
126
+ # try adding very small cg
127
+ kwargs["cgw"] = 0.0
128
+ # now the fold is set up run the standard interpolation
129
+ super().build(self, data_region=data_region, **kwargs)
@@ -0,0 +1,543 @@
1
+ """
2
+ Feature builder
3
+ """
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from ....utils import getLogger
9
+
10
+
11
+ from ....interpolators import GeologicalInterpolator
12
+ from ....utils.helper import (
13
+ xyz_names,
14
+ val_name,
15
+ normal_vec_names,
16
+ weight_name,
17
+ gradient_vec_names,
18
+ tangent_vec_names,
19
+ interface_name,
20
+ inequality_name,
21
+ pairs_name,
22
+ )
23
+ from ....modelling.features import GeologicalFeature
24
+ from ....modelling.features.builders import BaseBuilder
25
+ from ....utils.helper import (
26
+ get_data_bounding_box_map as get_data_bounding_box,
27
+ )
28
+ from ....utils import RegionEverywhere
29
+ from ....interpolators import DiscreteInterpolator
30
+ from ....interpolators import InterpolatorFactory
31
+
32
+ logger = getLogger(__name__)
33
+
34
+
35
+ class GeologicalFeatureBuilder(BaseBuilder):
36
+ def __init__(
37
+ self,
38
+ interpolatortype: str,
39
+ bounding_box,
40
+ nelements: int = 1000,
41
+ name="Feature",
42
+ interpolation_region=None,
43
+ model=None,
44
+ **kwargs,
45
+ ):
46
+ """
47
+ Constructor for a GeologicalFeatureBuilder
48
+
49
+ Parameters
50
+ ----------
51
+ interpolator : GeologicalInterpolator
52
+ An empty GeologicalInterpolator
53
+ region : lambda function
54
+ defining whether the location (xyz) should be included in the
55
+ kwargs - name of the feature, region to interpolate the feature
56
+ """
57
+ BaseBuilder.__init__(self, model, name)
58
+ interpolator = InterpolatorFactory.create_interpolator(
59
+ interpolatortype=interpolatortype,
60
+ boundingbox=bounding_box,
61
+ nelements=nelements,
62
+ buffer=kwargs.get("buffer", 0.2),
63
+ )
64
+
65
+ if not issubclass(type(interpolator), GeologicalInterpolator):
66
+ raise TypeError(
67
+ "interpolator is {} and must be a GeologicalInterpolator".format(type(interpolator))
68
+ )
69
+ self._interpolator = interpolator
70
+
71
+ header = (
72
+ xyz_names()
73
+ + val_name()
74
+ + gradient_vec_names()
75
+ + normal_vec_names()
76
+ + tangent_vec_names()
77
+ + weight_name()
78
+ )
79
+ self.data = pd.DataFrame(columns=header)
80
+ self.data_added = False
81
+ self._interpolation_region = None
82
+ self.interpolation_region = interpolation_region
83
+ if self.interpolation_region is not None:
84
+ self._interpolator.set_region(region=self.interpolation_region)
85
+
86
+ self._feature = GeologicalFeature(
87
+ self._name,
88
+ self._interpolator,
89
+ builder=self,
90
+ regions=[],
91
+ faults=self.faults,
92
+ )
93
+ self._orthogonal_features = {}
94
+ self._equality_constraints = {}
95
+
96
+ @property
97
+ def interpolator(self):
98
+ return self._interpolator
99
+
100
+ @property
101
+ def interpolation_region(self):
102
+ return self._interpolation_region
103
+
104
+ @interpolation_region.setter
105
+ def interpolation_region(self, interpolation_region):
106
+ if interpolation_region is not None:
107
+ self._interpolation_region = interpolation_region
108
+ self._interpolator.set_region(region=self._interpolation_region)
109
+ else:
110
+ self._interpolation_region = RegionEverywhere()
111
+ self._interpolator.set_region(region=self._interpolation_region)
112
+ self._up_to_date = False
113
+
114
+ def add_data_from_data_frame(self, data_frame, overwrite=False):
115
+ """
116
+ Extract data from a pandas dataframe with columns for
117
+
118
+ Parameters
119
+ ----------
120
+ data_frame : pd.DataFrame
121
+ a dataframe containing the data to be added
122
+
123
+ Returns
124
+ -------
125
+
126
+ """
127
+ self.data = data_frame.copy()
128
+
129
+ def add_orthogonal_feature(self, feature, w=1.0, region=None, step=1, B=0):
130
+ """
131
+ Add a constraint to the interpolator so that the gradient of an exisitng
132
+ feature is orthogonal to the feature being built. E.g. dot product
133
+ between gradients should be = 0
134
+
135
+ Parameters
136
+ ----------
137
+ feature : GeologicalFeature
138
+ feature which we want to be orthogonal to
139
+ w : double
140
+ how much to weight in least squares sense
141
+ region : unused
142
+ step : int
143
+ numpy slicing step size to see how many tetras to add
144
+
145
+ Returns
146
+ -------
147
+
148
+ Notes
149
+ -----
150
+ The constraint can be applied to a random subset of the tetrahedral
151
+ elements in the mesh
152
+ """
153
+ try:
154
+ step = int(step) # cast as int in case it was a float
155
+ except ValueError:
156
+ logger.error("Cannot cast {} as integer, setting step to 1".format(step))
157
+ step = 1
158
+ self._orthogonal_features[feature.name] = [feature, w, region, step, B]
159
+ self._up_to_date = False
160
+
161
+ def add_data_to_interpolator(self, constrained=False, force_constrained=False, **kwargs):
162
+ """
163
+ Iterates through the list of data and applies any faults active on the
164
+ data in the order they are added
165
+
166
+ Parameters
167
+ -----------
168
+ constrained : boolean
169
+ force_constrained : boolean
170
+
171
+ Returns
172
+ -------
173
+
174
+ """
175
+ if self.data_added:
176
+ return
177
+ # first move the data for the fault
178
+ logger.info(f"Adding {len(self.faults)} faults to {self.name}")
179
+ data = self.data.copy()
180
+
181
+ # convert data locations to numpy array and then update
182
+ # for gradient data we need to calculate the rotation of the vector field
183
+ # create a tetrahedron at each point, and assign the corner values as the values that would
184
+ # fit the vector for the gradient. Then apply the fault to these points and then recalculate
185
+ # the gradient using the tetrahedron.
186
+
187
+ mask_gradient = np.all(~np.isnan(data.loc[:, gradient_vec_names()].to_numpy(float)), axis=1)
188
+ mask_normal = np.all(~np.isnan(data.loc[:, normal_vec_names()].to_numpy(float)), axis=1)
189
+ mask_tangent = np.all(~np.isnan(data.loc[:, tangent_vec_names()].to_numpy(float)), axis=1)
190
+ mask_vector = mask_gradient | mask_normal | mask_tangent
191
+ if np.sum(mask_vector) > 0:
192
+ for f in self.faults:
193
+ if np.sum(mask_normal) > 0:
194
+ data.loc[mask_normal, normal_vec_names()] = f.apply_to_vectors(
195
+ data.loc[mask_normal, xyz_names() + normal_vec_names()].to_numpy(float)
196
+ )
197
+ if np.sum(mask_gradient) > 0:
198
+ data.loc[mask_gradient, gradient_vec_names()] = f.apply_to_vectors(
199
+ data.loc[mask_gradient, xyz_names() + gradient_vec_names()].to_numpy(float)
200
+ )
201
+ if np.sum(mask_tangent) > 0:
202
+ data.loc[mask_tangent, tangent_vec_names()] = f.apply_to_vectors(
203
+ data.loc[mask_tangent, xyz_names() + tangent_vec_names()].to_numpy(float)
204
+ )
205
+
206
+ for f in self.faults:
207
+ data.loc[:, xyz_names()] = f.apply_to_points(data.loc[:, xyz_names()])
208
+
209
+ # self.check_interpolation_geometry(data.loc[:,xyz_names()].to_numpy())
210
+ # Now check whether there are enough constraints for the
211
+ # interpolator to be able to solve
212
+ # we need at least 2 different value points or a single norm
213
+ # constraint. If there are not enough
214
+ # try converting grad to norms, if still not enough send user an error
215
+ if constrained:
216
+ # Change normals to gradients
217
+ mask = np.all(~np.isnan(data.loc[:, normal_vec_names()]), axis=1)
218
+ if mask.sum() > 0:
219
+ data.loc[mask, gradient_vec_names()] = data.loc[mask, normal_vec_names()].to_numpy(
220
+ float
221
+ )
222
+ data.loc[mask, normal_vec_names()] = np.nan
223
+ if self.get_norm_constraints().shape[0] > 0:
224
+ constrained = True
225
+
226
+ if np.unique(self.get_value_constraints()[:, 3]).shape[0] > 1:
227
+ constrained = True
228
+
229
+ if not constrained or force_constrained:
230
+ # change gradient constraints to normal vector constraints
231
+ mask = np.all(~np.isnan(data.loc[:, gradient_vec_names()]), axis=1)
232
+ if mask.sum() > 0:
233
+ data.loc[mask, normal_vec_names()] = data.loc[mask, gradient_vec_names()].to_numpy(
234
+ float
235
+ )
236
+ data.loc[mask, gradient_vec_names()] = np.nan
237
+ logger.info("Setting gradient points to norm constraints")
238
+ constrained = True
239
+ mask = np.all(~np.isnan(data.loc[:, normal_vec_names()].to_numpy(float)), axis=1)
240
+
241
+ if not constrained:
242
+ logger.error("Not enough constraints for scalar field add more")
243
+ # self.interpolator.reset()
244
+ mask = ~np.isnan(data.loc[:, val_name()].to_numpy(float))
245
+ # add value constraints
246
+ if mask.sum() > 0:
247
+ value_data = data.loc[mask[:, 0], xyz_names() + val_name() + weight_name()].to_numpy(
248
+ float
249
+ )
250
+ self.interpolator.set_value_constraints(value_data)
251
+
252
+ # add gradient constraints
253
+ mask = np.all(~np.isnan(data.loc[:, gradient_vec_names()].to_numpy(float)), axis=1)
254
+ if mask.sum() > 0:
255
+ gradient_data = data.loc[
256
+ mask, xyz_names() + gradient_vec_names() + weight_name()
257
+ ].to_numpy(float)
258
+ self.interpolator.set_gradient_constraints(gradient_data)
259
+
260
+ # add normal vector data
261
+ mask = np.all(~np.isnan(data.loc[:, normal_vec_names()].to_numpy(float)), axis=1)
262
+ if mask.sum() > 0:
263
+ normal_data = data.loc[mask, xyz_names() + normal_vec_names() + weight_name()].to_numpy(
264
+ float
265
+ )
266
+ self.interpolator.set_normal_constraints(normal_data)
267
+
268
+ # add tangent data
269
+ mask = np.all(~np.isnan(data.loc[:, tangent_vec_names()].to_numpy(float)), axis=1)
270
+ if mask.sum() > 0:
271
+ tangent_data = data.loc[
272
+ mask, xyz_names() + tangent_vec_names() + weight_name()
273
+ ].to_numpy(float)
274
+ self.interpolator.set_tangent_constraints(tangent_data)
275
+
276
+ # add interface constraints
277
+ mask = np.all(~np.isnan(data.loc[:, interface_name()].to_numpy(float)), axis=1)
278
+ if mask.sum() > 0:
279
+ interface_data = data.loc[
280
+ mask, xyz_names() + interface_name() + weight_name()
281
+ ].to_numpy(float)
282
+ self.interpolator.set_interface_constraints(interface_data)
283
+ # add inequality constraints
284
+ mask = np.all(~np.isnan(data.loc[:, inequality_name()].to_numpy(float)), axis=1)
285
+ if mask.sum() > 0:
286
+ inequality_data = data.loc[mask, xyz_names() + inequality_name()].to_numpy(float)
287
+ self.interpolator.set_value_inequality_constraints(inequality_data)
288
+ mask = np.all(~np.isnan(data.loc[:, pairs_name()].to_numpy(float)), axis=1)
289
+ if mask.sum() > 0:
290
+ pairs_data = data.loc[mask, xyz_names() + pairs_name()].to_numpy(float)
291
+ self.interpolator.set_inequality_pairs_constraints(pairs_data)
292
+ self.data_added = True
293
+ self._up_to_date = False
294
+
295
+ def install_gradient_constraint(self):
296
+ if issubclass(type(self.interpolator), DiscreteInterpolator):
297
+ for g in self._orthogonal_features.values():
298
+ feature, w, region, step, B = g
299
+ if w == 0:
300
+ continue
301
+ logger.info(f"Adding gradient orthogonal constraint {feature.name} to {self.name}")
302
+ logger.info(f'Evaluating gradient {feature.name} for {self.name}')
303
+ # don't worry about masking for unconformities here
304
+ vector = feature.evaluate_gradient(
305
+ self.interpolator.support.barycentre, ignore_regions=True
306
+ )
307
+
308
+ norm = np.linalg.norm(vector, axis=1)
309
+
310
+ vector[norm > 0] /= norm[norm > 0, None]
311
+ element_idx = np.arange(self.interpolator.support.n_elements)
312
+ logger.info(f"Adding to least squares matrix: {self.name}")
313
+
314
+ self.interpolator.add_gradient_orthogonal_constraints(
315
+ self.interpolator.support.barycentre[element_idx[::step], :],
316
+ vector[element_idx[::step], :],
317
+ w=w,
318
+ B=B,
319
+ name=f'{feature.name}_orthogonal',
320
+ )
321
+
322
+ def add_equality_constraints(self, feature, region, scalefactor=1.0):
323
+ self._equality_constraints[feature.name] = [feature, region, scalefactor]
324
+ self._up_to_date = False
325
+
326
+ def install_equality_constraints(self):
327
+ for e in self._equality_constraints.values():
328
+ try:
329
+ # assume all parts of structural frame have the same support
330
+ support = self.interpolator.support
331
+
332
+ # work out the values of the nodes where we want hard
333
+ # constraints
334
+ idc = np.arange(0, support.n_nodes)[e[1](support.nodes)]
335
+ val = e[0].evaluate_value(support.nodes[e[1](support.nodes), :])
336
+ mask = ~np.isnan(val)
337
+ self.interpolator.add_equality_constraints(idc[mask], val[mask] * e[2])
338
+ except BaseException as e:
339
+ logger.error(f"Could not add equality for {self.name}")
340
+ logger.error(f"Exception: {e}")
341
+
342
+ def get_value_constraints(self):
343
+ """
344
+ Get the value constraints for this geological feature
345
+
346
+ Returns
347
+ -------
348
+ np.array((N,4),dtype=double)
349
+ """
350
+ header = xyz_names() + val_name() + weight_name()
351
+ mask = ~np.isnan(self.data.loc[:, val_name()].to_numpy(float))
352
+ return self.data.loc[mask[:, 0], header].to_numpy(float)
353
+
354
+ def get_gradient_constraints(self):
355
+ """
356
+ Get the gradient direction constraints
357
+
358
+ Returns
359
+ -------
360
+ numpy array
361
+ """
362
+ mask = np.all(~np.isnan(self.data.loc[:, gradient_vec_names()].to_numpy(float)), axis=1)
363
+ if mask.shape[0] > 0:
364
+ return self.data.loc[mask, xyz_names() + gradient_vec_names() + weight_name()].to_numpy(
365
+ float
366
+ )
367
+ else:
368
+ return np.zeros((0, 7))
369
+
370
+ def get_tangent_constraints(self):
371
+ """
372
+
373
+ Returns
374
+ -------
375
+ numpy array
376
+ """
377
+ mask = np.all(~np.isnan(self.data.loc[:, tangent_vec_names()].to_numpy(float)), axis=1)
378
+ if mask.shape[0] > 0:
379
+ return self.data.loc[mask, xyz_names() + tangent_vec_names() + weight_name()].to_numpy(
380
+ float
381
+ )
382
+ else:
383
+ return np.zeros((0, 7))
384
+
385
+ def get_norm_constraints(self):
386
+ """
387
+ Get the gradient norm constraints
388
+
389
+ Returns
390
+ -------
391
+ numpy array
392
+ """
393
+ mask = np.all(~np.isnan(self.data.loc[:, normal_vec_names()].to_numpy(float)), axis=1)
394
+ if mask.shape[0] > 0:
395
+ return self.data.loc[mask, xyz_names() + normal_vec_names() + weight_name()].to_numpy(
396
+ float
397
+ )
398
+ else:
399
+ return np.zeros((0, 7))
400
+
401
+ def get_orientation_constraints(self):
402
+ """
403
+ Get the orientation constraints
404
+
405
+ Returns
406
+ -------
407
+ numpy array
408
+ """
409
+ gradient_constraints = self.get_gradient_constraints()
410
+ normal_constraints = self.get_norm_constraints()
411
+ return np.vstack([gradient_constraints, normal_constraints])
412
+
413
+ def get_interface_constraints(self):
414
+ mask = np.all(~np.isnan(self.data.loc[:, interface_name()].to_numpy(float)), axis=1)
415
+ if mask.shape[0] > 0:
416
+ return self.data.loc[mask, xyz_names() + interface_name() + weight_name()].to_numpy(
417
+ float
418
+ )
419
+ else:
420
+ return np.zeros((0, 5))
421
+
422
+ def get_data_locations(self):
423
+ """
424
+ Get only the location for all data points
425
+
426
+ Returns
427
+ -------
428
+
429
+ """
430
+ return self.data.loc[:, xyz_names()].to_numpy(float)
431
+
432
+ def set_interpolation_geometry(self, origin, maximum, rotation=None):
433
+ """Update the interpolation support geometry to new bounding box
434
+
435
+ Parameters
436
+ ----------
437
+ origin : np.array(3)
438
+ origin vector
439
+ maximum : np.array(3)
440
+ maximum vector
441
+ """
442
+ logger.info(f"Setting mesh origin: {origin[0]} {origin[1]} {origin[2]} ")
443
+ logger.info(f"Setting mesh maximum: {maximum[0]} {maximum[1]} {maximum[2]}")
444
+ if np.any(np.isnan(origin)):
445
+ logger.warning("Origin is NaN, not updating")
446
+ return
447
+
448
+ if np.any(np.isnan(maximum)):
449
+ logger.warning("Maximum is NaN, not updating")
450
+ return
451
+
452
+ self.interpolator.support.origin = origin
453
+ self.interpolator.support.maximum = maximum
454
+ self.interpolator.support.rotation_xy = rotation
455
+ self._up_to_date = False
456
+
457
+ while self.interpolator.nx < 100:
458
+ self.interpolator.support.step_vector = self.interpolator.support.step_vector * 0.9
459
+
460
+ def check_interpolation_geometry(self, data, buffer=0.3):
461
+ """Check the interpolation support geometry
462
+ to data to make sure everything fits
463
+ Apply the fault to the model grid to ensure that the support
464
+ is big enough to capture the faulted feature.
465
+ """
466
+
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)
472
+
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
477
+
478
+ def build(self, fold=None, fold_weights={}, data_region=None, **kwargs):
479
+ """
480
+ Runs the interpolation and builds the geological feature
481
+
482
+ Parameters
483
+ ----------
484
+ fold : FoldEvent
485
+ fold_weights : dict
486
+ data_region : double <1
487
+ If not none adds a region around the data points to the interpolation
488
+ with data_region as a buffer
489
+ kwargs
490
+
491
+ Returns
492
+ -------
493
+
494
+ """
495
+ logger.info(f'Building {self.name}')
496
+ # self.get_interpolator(**kwargs)
497
+ for f in self.faults:
498
+ f.builder.update()
499
+ domain = kwargs.get("domain", None)
500
+ if domain:
501
+ self.check_interpolation_geometry(None)
502
+ self.add_data_to_interpolator(**kwargs)
503
+ if data_region is not None:
504
+ xyz = self.interpolator.get_data_locations()
505
+ bb, region = get_data_bounding_box(xyz, data_region)
506
+ # if self.model.reuse_supports == False:
507
+ if np.any(np.min(bb[0, :]) < self.interpolator.support.origin):
508
+ neworigin = np.min([self.interpolator.support.origin, bb[0, :]], axis=0)
509
+ logger.info(
510
+ f"Changing origin of support for {self.name} from \
511
+ {self.interpolator.support.origin[0]} \
512
+ {self.interpolator.support.origin[1]} \
513
+ {self.interpolator.support.origin[2]} \
514
+ to {neworigin[0]} {neworigin[1]} {neworigin[2]}"
515
+ )
516
+
517
+ self.interpolator.support.origin = neworigin
518
+ if np.any(np.max(bb[0, :]) < self.interpolator.support.maximum):
519
+ newmax = np.max([self.interpolator.support.maximum, bb[0, :]], axis=0)
520
+ logger.info(
521
+ f"Changing origin of support for {self.name} from \
522
+ from {self.interpolator.support.maximum[0]} \
523
+ {self.interpolator.support.maximum[1]} \
524
+ {self.interpolator.support.maximum[2]} \
525
+ to {newmax[0]} {newmax[1]} {newmax[2]}"
526
+ )
527
+
528
+ self.interpolator.support.maximum = newmax
529
+ self.interpolator.set_region(region=region)
530
+ logger.info(f'Setting up interpolator for {self.name}')
531
+ self.interpolator.setup_interpolator(**kwargs)
532
+ logger.info(f'installing gradient constraints {self.name}')
533
+ self.install_gradient_constraint()
534
+ logger.info(f'installing equality constraints {self.name}')
535
+ self.install_equality_constraints()
536
+ logger.info(f'running interpolation for {self.name}')
537
+
538
+ self.interpolator.solve_system(
539
+ solver=kwargs.get('solver', None), solver_kwargs=kwargs.get('solver_kwargs', {})
540
+ )
541
+ logger.info(f'Finished building {self.name}')
542
+ self._up_to_date = True
543
+ return self._feature