LoopStructural 1.6.2__py3-none-any.whl → 1.6.3__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 (56) hide show
  1. LoopStructural/datatypes/_bounding_box.py +19 -4
  2. LoopStructural/datatypes/_point.py +31 -0
  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 -9
  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 -1
  23. LoopStructural/modelling/core/geological_model.py +0 -2
  24. LoopStructural/modelling/features/_analytical_feature.py +25 -16
  25. LoopStructural/modelling/features/_geological_feature.py +47 -11
  26. LoopStructural/modelling/features/builders/_base_builder.py +8 -0
  27. LoopStructural/modelling/features/builders/_folded_feature_builder.py +45 -14
  28. LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
  29. LoopStructural/modelling/features/builders/_structural_frame_builder.py +5 -0
  30. LoopStructural/modelling/features/fault/__init__.py +1 -1
  31. LoopStructural/modelling/features/fault/_fault_function.py +19 -1
  32. LoopStructural/modelling/features/fault/_fault_segment.py +15 -49
  33. LoopStructural/modelling/features/fold/__init__.py +1 -2
  34. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
  35. LoopStructural/modelling/features/fold/_foldframe.py +4 -4
  36. LoopStructural/modelling/features/fold/_svariogram.py +81 -46
  37. LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
  38. LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
  39. LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
  40. LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
  41. LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
  42. LoopStructural/modelling/input/process_data.py +6 -0
  43. LoopStructural/modelling/input/project_file.py +24 -3
  44. LoopStructural/utils/_surface.py +5 -2
  45. LoopStructural/utils/colours.py +26 -0
  46. LoopStructural/utils/features.py +5 -0
  47. LoopStructural/utils/maths.py +51 -0
  48. LoopStructural/version.py +1 -1
  49. LoopStructural-1.6.3.dist-info/METADATA +146 -0
  50. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.3.dist-info}/RECORD +53 -48
  51. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.3.dist-info}/WHEEL +1 -1
  52. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  53. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
  54. LoopStructural-1.6.2.dist-info/METADATA +0 -81
  55. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.3.dist-info}/LICENSE +0 -0
  56. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.3.dist-info}/top_level.txt +0 -0
@@ -2,15 +2,16 @@
2
2
  Wrapper for using surfepy
3
3
  """
4
4
 
5
- from ..utils.helper import get_vectors
5
+ from ..utils.maths import get_vectors
6
6
  from ..interpolators import GeologicalInterpolator
7
7
 
8
8
  import numpy as np
9
9
 
10
10
  from ..utils import getLogger
11
+ import surfepy
12
+ from typing import Optional
11
13
 
12
14
  logger = getLogger(__name__)
13
- import surfepy
14
15
 
15
16
 
16
17
  class SurfeRBFInterpolator(GeologicalInterpolator):
@@ -19,6 +20,8 @@ class SurfeRBFInterpolator(GeologicalInterpolator):
19
20
  def __init__(self, method="single_surface"):
20
21
  GeologicalInterpolator.__init__(self)
21
22
  self.surfe = None
23
+ if not method:
24
+ method = "single_surface"
22
25
  if method == "single_surface":
23
26
  logger.info("Using single surface interpolator")
24
27
  self.surfe = surfepy.Surfe_API(1)
@@ -29,7 +32,10 @@ class SurfeRBFInterpolator(GeologicalInterpolator):
29
32
  logger.info("Using surfe horizon")
30
33
  self.surfe = surfepy.Surfe_API(4)
31
34
 
32
- def add_gradient_ctr_pts(self):
35
+ def set_region(self, **kwargs):
36
+ pass
37
+
38
+ def add_gradient_constraints(self, w=1):
33
39
  points = self.get_gradient_constraints()
34
40
  if points.shape[0] > 0:
35
41
  logger.info("Adding ")
@@ -40,12 +46,12 @@ class SurfeRBFInterpolator(GeologicalInterpolator):
40
46
  self.surfe.SetTangentConstraints(strike_vector)
41
47
  self.surfe.SetTangentConstraints(dip_vector)
42
48
 
43
- def add_norm_ctr_pts(self):
49
+ def add_norm_constraints(self, w=1):
44
50
  points = self.get_norm_constraints()
45
51
  if points.shape[0] > 0:
46
52
  self.surfe.SetPlanarConstraints(points[:, :6])
47
53
 
48
- def add_ctr_pts(self):
54
+ def add_value_constraints(self, w=1):
49
55
 
50
56
  points = self.get_value_constraints()
51
57
  if points.shape[0] > 0:
@@ -59,15 +65,39 @@ class SurfeRBFInterpolator(GeologicalInterpolator):
59
65
  points[i, 3],
60
66
  )
61
67
 
62
- def add_tangent_ctr_pts(self):
68
+ def add_interface_constraints(self, w=1):
69
+ pass
70
+
71
+ def add_value_inequality_constraints(self, w=1):
72
+ ## inequalities are causing a segfault
73
+ # points = self.get_value_inequality_constraints()
74
+ # if points.shape[0] > 0:
75
+ # #
76
+ # self.surfe.AddValueInequalityConstraints(points[:,0],points[:,1],points[:,2],points[:,3])
77
+ pass
78
+
79
+ def add_inequality_pairs_constraints(
80
+ self,
81
+ w: float = 1.0,
82
+ upper_bound=np.finfo(float).eps,
83
+ lower_bound=-np.inf,
84
+ pairs: Optional[list] = None,
85
+ ):
86
+ # self.surfe.Add
87
+ pass
88
+
89
+ def reset(self):
90
+ pass
91
+
92
+ def add_tangent_constraints(self, w=1):
63
93
  points = self.get_tangent_constraints()
64
94
  if points.shape[0] > 0:
65
95
  self.surfe.SetTangentConstraints(points[:, :6])
66
96
 
67
- def _solve(self, **kwargs):
97
+ def solve_system(self, **kwargs):
68
98
  self.surfe.ComputeInterpolant()
69
99
 
70
- def _setup_interpolator(self, **kwargs):
100
+ def setup_interpolator(self, **kwargs):
71
101
  """
72
102
  Setup the interpolator
73
103
 
@@ -90,10 +120,10 @@ class SurfeRBFInterpolator(GeologicalInterpolator):
90
120
 
91
121
 
92
122
  """
93
- self.add_gradient_ctr_pts()
94
- self.add_norm_ctr_pts()
95
- self.add_ctr_pts()
96
- self.add_tangent_ctr_pts()
123
+ self.add_gradient_constraints()
124
+ self.add_norm_constraints()
125
+ self.add_value_constraints()
126
+ self.add_tangent_constraints()
97
127
 
98
128
  kernel = kwargs.get("kernel", "r3")
99
129
  logger.info("Setting surfe RBF kernel to %s" % kernel)
@@ -338,3 +338,19 @@ class BaseUnstructured2d(BaseSupport):
338
338
  """
339
339
  verts, c, tri, inside = self.get_element_for_location(pos, return_verts=False)
340
340
  return self.evaluate_shape_derivatives(pos, tri)
341
+
342
+ def vtk(self, node_properties={}, cell_properties={}):
343
+ """
344
+ Create a vtk unstructured grid from the mesh
345
+ """
346
+ import pyvista as pv
347
+
348
+ grid = pv.UnstructuredGrid()
349
+ grid.points = self.nodes
350
+ grid.cell_types = np.ones(self.elements.shape[0]) * pv.vtk.VTK_TRIANGLE
351
+ grid.cells = np.c_[np.ones(self.elements.shape[0]) * 3, self.elements]
352
+ for key, value in node_properties.items():
353
+ grid.point_data[key] = value
354
+ for key, value in cell_properties.items():
355
+ grid.cell_data[key] = value
356
+ return grid
@@ -5,10 +5,11 @@ Cartesian grid for fold interpolator
5
5
 
6
6
  import logging
7
7
 
8
- from typing import Tuple
9
8
  import numpy as np
10
9
  from . import SupportType
11
10
  from ._base_support import BaseSupport
11
+ from typing import Dict, Tuple
12
+ from .._operator import Operator
12
13
 
13
14
  logger = logging.getLogger(__name__)
14
15
 
@@ -33,7 +34,7 @@ class StructuredGrid2D(BaseSupport):
33
34
  step_vector - 2d list or numpy array of int
34
35
  """
35
36
  self.type = SupportType.StructuredGrid2D
36
- self.nsteps = np.array(nsteps)
37
+ self.nsteps = np.ceil(np.array(nsteps)).astype(int)
37
38
  self.step_vector = np.array(step_vector)
38
39
  self.origin = np.array(origin)
39
40
  self.maximum = origin + self.nsteps * self.step_vector
@@ -261,13 +262,22 @@ class StructuredGrid2D(BaseSupport):
261
262
  if "indexes" in kwargs:
262
263
  indexes = kwargs["indexes"]
263
264
  if "indexes" not in kwargs:
264
- ii = []
265
- jj = []
266
- for i in range(1, self.nsteps[0] - 1):
267
- for j in range(1, self.nsteps[1] - 1):
268
- ii.append(i)
269
- jj.append(j)
270
- indexes = np.array([ii, jj])
265
+ gi = np.arange(self.n_nodes)
266
+ indexes = self.global_index_to_node_index(gi)
267
+ edge_mask = (
268
+ (indexes[:, 0] > 0)
269
+ & (indexes[:, 0] < self.nsteps[0] - 1)
270
+ & (indexes[:, 1] > 0)
271
+ & (indexes[:, 1] < self.nsteps[1] - 1)
272
+ )
273
+ indexes = indexes[edge_mask, :].T
274
+ # ii = []
275
+ # jj = []
276
+ # for i in range(1, self.nsteps[0] - 1):
277
+ # for j in range(1, self.nsteps[1] - 1):
278
+ # ii.append(i)
279
+ # jj.append(j)
280
+ # indexes = np.array([ii, jj])
271
281
  # indexes = np.array(indexes).T
272
282
  if indexes.ndim != 2:
273
283
  print(indexes.ndim)
@@ -460,3 +470,28 @@ class StructuredGrid2D(BaseSupport):
460
470
 
461
471
  def onGeometryChange(self):
462
472
  pass
473
+
474
+ def vtk(self, node_properties={}, cell_properties={}):
475
+ raise NotImplementedError("VTK output not implemented for structured grid")
476
+ pass
477
+
478
+ def get_operators(self, weights: Dict[str, float]) -> Dict[str, Tuple[np.ndarray, float]]:
479
+ """Get
480
+
481
+ Parameters
482
+ ----------
483
+ weights : Dict[str, float]
484
+ _description_
485
+
486
+ Returns
487
+ -------
488
+ Dict[str, Tuple[np.ndarray, float]]
489
+ _description_
490
+ """
491
+ # in a map we only want the xy operators
492
+ operators = {
493
+ 'dxy': (Operator.Dxy_mask[1, :, :], weights['dxy'] * 2),
494
+ 'dxx': (Operator.Dxx_mask[1, :, :], weights['dxx']),
495
+ 'dyy': (Operator.Dyy_mask[1, :, :], weights['dyy']),
496
+ }
497
+ return operators
@@ -134,7 +134,15 @@ class BaseStructuredSupport(BaseSupport):
134
134
  origin = np.array(origin)
135
135
  length = self.maximum - origin
136
136
  length /= self.step_vector
137
- self._nsteps = np.ceil(length).astype(int)
137
+ self._nsteps = np.ceil(length).astype(np.int64)
138
+ self._nsteps[self._nsteps == 0] = (
139
+ 3 # need to have a minimum of 3 elements to apply the finite difference mask
140
+ )
141
+ if np.any(~(self._nsteps > 0)):
142
+ logger.error(
143
+ f"Cannot resize the interpolation support. The proposed number of steps is {self._nsteps}, these must be all > 0"
144
+ )
145
+ raise ValueError("Cannot resize the interpolation support.")
138
146
  self._origin = origin
139
147
  self.onGeometryChange()
140
148
 
@@ -150,7 +158,13 @@ class BaseStructuredSupport(BaseSupport):
150
158
  maximum = np.array(maximum, dtype=float)
151
159
  length = maximum - self.origin
152
160
  length /= self.step_vector
153
- self._nsteps = np.ceil(length).astype(int) + 1
161
+ self._nsteps = np.ceil(length).astype(np.int64)
162
+ self._nsteps[self._nsteps == 0] = 3
163
+ if np.any(~(self._nsteps > 0)):
164
+ logger.error(
165
+ f"Cannot resize the interpolation support. The proposed number of steps is {self._nsteps}, these must be all > 0"
166
+ )
167
+ raise ValueError("Cannot resize the interpolation support.")
154
168
  self.onGeometryChange()
155
169
 
156
170
  @property
@@ -236,6 +250,7 @@ class BaseStructuredSupport(BaseSupport):
236
250
  cell_indexes[inside, 0] = x[inside] // self.step_vector[None, 0]
237
251
  cell_indexes[inside, 1] = y[inside] // self.step_vector[None, 1]
238
252
  cell_indexes[inside, 2] = z[inside] // self.step_vector[None, 2]
253
+
239
254
  return cell_indexes, inside
240
255
 
241
256
  def position_to_cell_global_index(self, pos):
@@ -331,12 +346,9 @@ class BaseStructuredSupport(BaseSupport):
331
346
  return corner_indexes
332
347
 
333
348
  def position_to_cell_corners(self, pos):
334
-
335
349
  cell_indexes, inside = self.position_to_cell_index(pos)
336
350
  corner_indexes = self.cell_corner_indexes(cell_indexes)
337
-
338
351
  globalidx = self.global_node_indices(corner_indexes)
339
-
340
352
  # if global index is not inside the support set to -1
341
353
  globalidx[~inside] = -1
342
354
  return globalidx, inside
@@ -451,7 +463,7 @@ class BaseStructuredSupport(BaseSupport):
451
463
  # all elements are the same size
452
464
  return 1.0
453
465
 
454
- def vtk(self):
466
+ def vtk(self, node_properties={}, cell_properties={}):
455
467
  try:
456
468
  import pyvista as pv
457
469
  except ImportError:
@@ -464,4 +476,9 @@ class BaseStructuredSupport(BaseSupport):
464
476
  [np.zeros(self.elements.shape[0], dtype=int)[:, None] + 8, self.elements]
465
477
  )
466
478
  elements = elements.flatten()
467
- return pv.UnstructuredGrid(elements, celltype, self.nodes)
479
+ grid = pv.UnstructuredGrid(elements, celltype, self.nodes)
480
+ for key, value in node_properties.items():
481
+ grid[key] = value
482
+ for key, value in cell_properties.items():
483
+ grid.cell_arrays[key] = value
484
+ return grid
@@ -5,8 +5,10 @@ Cartesian grid for fold interpolator
5
5
 
6
6
  import numpy as np
7
7
 
8
- from ._3d_base_structured import BaseStructuredSupport
8
+ from LoopStructural.interpolators._operator import Operator
9
9
 
10
+ from ._3d_base_structured import BaseStructuredSupport
11
+ from typing import Dict, Tuple
10
12
  from . import SupportType
11
13
 
12
14
  from LoopStructural.utils import getLogger
@@ -158,13 +160,18 @@ class StructuredGrid(BaseStructuredSupport):
158
160
  if "indexes" in kwargs:
159
161
  indexes = kwargs["indexes"]
160
162
  if "indexes" not in kwargs:
161
- indexes = np.array(
162
- np.meshgrid(
163
- np.arange(1, self.nsteps[0] - 1),
164
- np.arange(1, self.nsteps[1] - 1),
165
- np.arange(1, self.nsteps[2] - 1),
166
- )
167
- ).reshape((3, -1))
163
+ gi = np.arange(self.n_nodes)
164
+ indexes = self.global_index_to_node_index(gi)
165
+ edge_mask = (
166
+ (indexes[:, 0] > 0)
167
+ & (indexes[:, 0] < self.nsteps[0] - 1)
168
+ & (indexes[:, 1] > 0)
169
+ & (indexes[:, 1] < self.nsteps[1] - 1)
170
+ & (indexes[:, 2] > 0)
171
+ & (indexes[:, 2] < self.nsteps[2] - 1)
172
+ )
173
+ indexes = indexes[edge_mask, :].T
174
+
168
175
  # indexes = np.array(indexes).T
169
176
  if indexes.ndim != 2:
170
177
  return
@@ -291,7 +298,6 @@ class StructuredGrid(BaseStructuredSupport):
291
298
  )
292
299
  idc, inside = self.position_to_cell_corners(evaluation_points)
293
300
  # print(idc[inside,:], self.n_nodes,inside)
294
-
295
301
  if idc.shape[0] != inside.shape[0]:
296
302
  raise ValueError("index does not match number of nodes")
297
303
  v = np.zeros(idc.shape)
@@ -468,3 +474,26 @@ class StructuredGrid(BaseStructuredSupport):
468
474
  "type": self.type.numerator,
469
475
  **super().to_dict(),
470
476
  }
477
+
478
+ def get_operators(self, weights: Dict[str, float]) -> Dict[str, Tuple[np.ndarray, float]]:
479
+ """Gets the operators specific to this support
480
+
481
+ Parameters
482
+ ----------
483
+ weights : Dict[str, float]
484
+ weight value per operator
485
+
486
+ Returns
487
+ -------
488
+ operators
489
+ A dictionary with a numpy array and float weight
490
+ """
491
+ operators = {
492
+ 'dxy': (Operator.Dxy_mask, weights['dxy'] / 4),
493
+ 'dyz': (Operator.Dyz_mask, weights['dyz'] / 4),
494
+ 'dxz': (Operator.Dxz_mask, weights['dxz'] / 4),
495
+ 'dxx': (Operator.Dxx_mask, weights['dxx'] / 1),
496
+ 'dyy': (Operator.Dyy_mask, weights['dyy'] / 1),
497
+ 'dzz': (Operator.Dzz_mask, weights['dzz'] / 1),
498
+ }
499
+ return operators
@@ -729,8 +729,7 @@ class TetMesh(BaseStructuredSupport):
729
729
 
730
730
  return neighbours
731
731
 
732
- @property
733
- def vtk(self):
732
+ def vtk(self, node_properties={}, cell_properties={}):
734
733
  try:
735
734
  import pyvista as pv
736
735
  except ImportError:
@@ -743,4 +742,9 @@ class TetMesh(BaseStructuredSupport):
743
742
  [np.zeros(self.elements.shape[0], dtype=int)[:, None] + 4, self.elements]
744
743
  )
745
744
  elements = elements.flatten()
746
- return pv.UnstructuredGrid(elements, celltype, self.nodes)
745
+ grid = pv.UnstructuredGrid(elements, celltype, self.nodes)
746
+ for prop in node_properties:
747
+ grid[prop] = node_properties[prop]
748
+ for prop in cell_properties:
749
+ grid.cell_arrays[prop] = cell_properties[prop]
750
+ return grid
@@ -621,7 +621,7 @@ class UnStructuredTetMesh(BaseSupport):
621
621
  """
622
622
  return self.neighbours
623
623
 
624
- def vtk(self):
624
+ def vtk(self, node_properties={}, cell_properties={}):
625
625
  try:
626
626
  import pyvista as pv
627
627
  except ImportError:
@@ -634,4 +634,10 @@ class UnStructuredTetMesh(BaseSupport):
634
634
  [np.zeros(self.elements.shape[0], dtype=int)[:, None] + 4, self.elements]
635
635
  )
636
636
  elements = elements.flatten()
637
- return pv.UnstructuredGrid(elements, celltype, self.nodes)
637
+ grid = pv.UnstructuredGrid(elements, celltype, self.nodes)
638
+ for key, value in node_properties.items():
639
+ grid[key] = value
640
+ for key, value in cell_properties.items():
641
+ grid.cell_arrays[key] = value
642
+
643
+ return grid
@@ -18,6 +18,7 @@ class SupportType(IntEnum):
18
18
  BaseStructured = 6
19
19
  TetMesh = 10
20
20
  P2UnstructuredTetMesh = 11
21
+ DataSupported = 12
21
22
 
22
23
 
23
24
  from ._2d_base_unstructured import BaseUnstructured2d
@@ -29,6 +30,11 @@ from ._3d_unstructured_tetra import UnStructuredTetMesh
29
30
  from ._3d_structured_tetra import TetMesh
30
31
  from ._3d_p2_tetra import P2UnstructuredTetMesh
31
32
 
33
+
34
+ def no_support(*args, **kwargs):
35
+ return None
36
+
37
+
32
38
  support_map = {
33
39
  SupportType.StructuredGrid2D: StructuredGrid2D,
34
40
  SupportType.StructuredGrid: StructuredGrid,
@@ -37,6 +43,7 @@ support_map = {
37
43
  SupportType.P2Unstructured2d: P2Unstructured2d,
38
44
  SupportType.TetMesh: TetMesh,
39
45
  SupportType.P2UnstructuredTetMesh: P2UnstructuredTetMesh,
46
+ SupportType.DataSupported: no_support,
40
47
  }
41
48
 
42
49
  from ._support_factory import SupportFactory
@@ -112,3 +112,10 @@ class BaseSupport(metaclass=ABCMeta):
112
112
  Return the element size
113
113
  """
114
114
  pass
115
+
116
+ @abstractmethod
117
+ def vtk(self, node_properties={}, cell_properties={}):
118
+ """
119
+ Return a vtk object
120
+ """
121
+ pass
@@ -6,12 +6,12 @@ Geological modelling classes and functions
6
6
  __all__ = [
7
7
  "GeologicalModel",
8
8
  "ProcessInputData",
9
- "Loop3DView",
10
9
  "Map2LoopProcessor",
11
10
  "LoopProjectfileProcessor",
12
11
  ]
13
12
  from ..utils import getLogger
14
13
  from ..utils import LoopImportError
14
+ from .core.geological_model import GeologicalModel
15
15
 
16
16
  logger = getLogger(__name__)
17
17
  from ..modelling.input import (
@@ -825,8 +825,6 @@ class GeologicalModel:
825
825
 
826
826
  fold = FoldEvent(fold_frame, name=f"Fold_{foliation_data}", invert_norm=invert_fold_norm)
827
827
 
828
- if "fold_weights" not in kwargs:
829
- kwargs["fold_weights"] = {}
830
828
  if interpolatortype != "DFI":
831
829
  logger.warning("Folded foliation only supports DFI interpolator, changing to DFI")
832
830
  interpolatortype = "DFI"
@@ -28,7 +28,16 @@ class AnalyticalGeologicalFeature(BaseFeature):
28
28
  list of FaultSegments that affect this feature
29
29
  """
30
30
 
31
- def __init__(self, name, vector, origin, regions=[], faults=[], model=None, builder=None):
31
+ def __init__(
32
+ self,
33
+ name: str,
34
+ vector: np.ndarray,
35
+ origin: np.ndarray,
36
+ regions=[],
37
+ faults=[],
38
+ model=None,
39
+ builder=None,
40
+ ):
32
41
  BaseFeature.__init__(self, name, model, faults, regions, builder)
33
42
  self.vector = np.array(vector, dtype=float)
34
43
  self.origin = np.array(origin, dtype=float)
@@ -48,16 +57,16 @@ class AnalyticalGeologicalFeature(BaseFeature):
48
57
  json["origin"] = self.origin.tolist()
49
58
  return json
50
59
 
51
- def evaluate_value(self, xyz, ignore_regions=False):
52
- xyz = np.array(xyz)
53
- if len(xyz.shape) == 1:
54
- xyz = xyz[None, :]
55
- if len(xyz.shape) != 2:
60
+ def evaluate_value(self, pos: np.ndarray, ignore_regions=False):
61
+ pos = np.array(pos)
62
+ if len(pos.shape) == 1:
63
+ pos = pos[None, :]
64
+ if len(pos.shape) != 2:
56
65
  raise ValueError("xyz must be a 1D or 2D array")
57
- xyz2 = np.zeros(xyz.shape)
58
- xyz2[:] = xyz[:]
66
+ xyz2 = np.zeros(pos.shape)
67
+ xyz2[:] = pos[:]
59
68
  for f in self.faults:
60
- xyz2[:] = f.apply_to_points(xyz)
69
+ xyz2[:] = f.apply_to_points(pos)
61
70
  if self.model is not None:
62
71
  xyz2[:] = self.model.rescale(xyz2, inplace=False)
63
72
  xyz2[:] = xyz2 - self.origin
@@ -65,13 +74,13 @@ class AnalyticalGeologicalFeature(BaseFeature):
65
74
  distance = normal[0] * xyz2[:, 0] + normal[1] * xyz2[:, 1] + normal[2] * xyz2[:, 2]
66
75
  return distance / np.linalg.norm(self.vector)
67
76
 
68
- def evaluate_gradient(self, xyz, ignore_regions=False):
69
- xyz = np.array(xyz)
70
- if len(xyz.shape) == 1:
71
- xyz = xyz[None, :]
72
- if len(xyz.shape) != 2:
73
- raise ValueError("xyz must be a 1D or 2D array")
74
- v = np.zeros(xyz.shape)
77
+ def evaluate_gradient(self, pos: np.ndarray, ignore_regions=False):
78
+ pos = np.array(pos)
79
+ if len(pos.shape) == 1:
80
+ pos = pos[None, :]
81
+ if len(pos.shape) != 2:
82
+ raise ValueError("pos must be a 1D or 2D array")
83
+ v = np.zeros(pos.shape)
75
84
  v[:, :] = self.vector[None, :]
76
85
  return v
77
86
 
@@ -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]:
@@ -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)