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
@@ -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
@@ -41,6 +41,10 @@ class BaseStructuredSupport(BaseSupport):
41
41
  raise LoopException("nsteps cannot be zero")
42
42
  if np.any(nsteps < 0):
43
43
  raise LoopException("nsteps cannot be negative")
44
+ if np.any(nsteps < 3):
45
+ raise LoopException(
46
+ "step vector cannot be less than 3. Try increasing the resolution of the interpolator"
47
+ )
44
48
  self._nsteps = np.array(nsteps, dtype=int) + 1
45
49
  self._step_vector = np.array(step_vector)
46
50
  self._origin = np.array(origin)
@@ -134,7 +138,15 @@ class BaseStructuredSupport(BaseSupport):
134
138
  origin = np.array(origin)
135
139
  length = self.maximum - origin
136
140
  length /= self.step_vector
137
- self._nsteps = np.ceil(length).astype(int)
141
+ self._nsteps = np.ceil(length).astype(np.int64)
142
+ self._nsteps[self._nsteps == 0] = (
143
+ 3 # need to have a minimum of 3 elements to apply the finite difference mask
144
+ )
145
+ if np.any(~(self._nsteps > 0)):
146
+ logger.error(
147
+ f"Cannot resize the interpolation support. The proposed number of steps is {self._nsteps}, these must be all > 0"
148
+ )
149
+ raise ValueError("Cannot resize the interpolation support.")
138
150
  self._origin = origin
139
151
  self.onGeometryChange()
140
152
 
@@ -150,7 +162,13 @@ class BaseStructuredSupport(BaseSupport):
150
162
  maximum = np.array(maximum, dtype=float)
151
163
  length = maximum - self.origin
152
164
  length /= self.step_vector
153
- self._nsteps = np.ceil(length).astype(int) + 1
165
+ self._nsteps = np.ceil(length).astype(np.int64)
166
+ self._nsteps[self._nsteps == 0] = 3
167
+ if np.any(~(self._nsteps > 0)):
168
+ logger.error(
169
+ f"Cannot resize the interpolation support. The proposed number of steps is {self._nsteps}, these must be all > 0"
170
+ )
171
+ raise ValueError("Cannot resize the interpolation support.")
154
172
  self.onGeometryChange()
155
173
 
156
174
  @property
@@ -236,6 +254,7 @@ class BaseStructuredSupport(BaseSupport):
236
254
  cell_indexes[inside, 0] = x[inside] // self.step_vector[None, 0]
237
255
  cell_indexes[inside, 1] = y[inside] // self.step_vector[None, 1]
238
256
  cell_indexes[inside, 2] = z[inside] // self.step_vector[None, 2]
257
+
239
258
  return cell_indexes, inside
240
259
 
241
260
  def position_to_cell_global_index(self, pos):
@@ -331,12 +350,9 @@ class BaseStructuredSupport(BaseSupport):
331
350
  return corner_indexes
332
351
 
333
352
  def position_to_cell_corners(self, pos):
334
-
335
353
  cell_indexes, inside = self.position_to_cell_index(pos)
336
354
  corner_indexes = self.cell_corner_indexes(cell_indexes)
337
-
338
355
  globalidx = self.global_node_indices(corner_indexes)
339
-
340
356
  # if global index is not inside the support set to -1
341
357
  globalidx[~inside] = -1
342
358
  return globalidx, inside
@@ -451,7 +467,7 @@ class BaseStructuredSupport(BaseSupport):
451
467
  # all elements are the same size
452
468
  return 1.0
453
469
 
454
- def vtk(self):
470
+ def vtk(self, node_properties={}, cell_properties={}):
455
471
  try:
456
472
  import pyvista as pv
457
473
  except ImportError:
@@ -464,4 +480,9 @@ class BaseStructuredSupport(BaseSupport):
464
480
  [np.zeros(self.elements.shape[0], dtype=int)[:, None] + 8, self.elements]
465
481
  )
466
482
  elements = elements.flatten()
467
- return pv.UnstructuredGrid(elements, celltype, self.nodes)
483
+ grid = pv.UnstructuredGrid(elements, celltype, self.nodes)
484
+ for key, value in node_properties.items():
485
+ grid[key] = value
486
+ for key, value in cell_properties.items():
487
+ grid.cell_arrays[key] = value
488
+ 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)
@@ -336,9 +342,6 @@ class StructuredGrid(BaseStructuredSupport):
336
342
  idc, inside = self.position_to_cell_corners(evaluation_points)
337
343
  T = np.zeros((idc.shape[0], 3, 8))
338
344
  T[inside, :, :] = self.get_element_gradient_for_location(evaluation_points[inside, :])[1]
339
- # indices = np.array([self.position_to_cell_index(evaluation_points)])
340
- # idc = self.global_indicies(indices.swapaxes(0,1))
341
- # print(idc)
342
345
  if np.max(idc[inside, :]) > property_array.shape[0]:
343
346
  cix, ciy, ciz = self.position_to_cell_index(evaluation_points)
344
347
  if not np.all(cix[inside] < self.nsteps_cells[0]):
@@ -468,3 +471,26 @@ class StructuredGrid(BaseStructuredSupport):
468
471
  "type": self.type.numerator,
469
472
  **super().to_dict(),
470
473
  }
474
+
475
+ def get_operators(self, weights: Dict[str, float]) -> Dict[str, Tuple[np.ndarray, float]]:
476
+ """Gets the operators specific to this support
477
+
478
+ Parameters
479
+ ----------
480
+ weights : Dict[str, float]
481
+ weight value per operator
482
+
483
+ Returns
484
+ -------
485
+ operators
486
+ A dictionary with a numpy array and float weight
487
+ """
488
+ operators = {
489
+ 'dxy': (Operator.Dxy_mask, weights['dxy'] / 4),
490
+ 'dyz': (Operator.Dyz_mask, weights['dyz'] / 4),
491
+ 'dxz': (Operator.Dxz_mask, weights['dxz'] / 4),
492
+ 'dxx': (Operator.Dxx_mask, weights['dxx'] / 1),
493
+ 'dyy': (Operator.Dyy_mask, weights['dyy'] / 1),
494
+ 'dzz': (Operator.Dzz_mask, weights['dzz'] / 1),
495
+ }
496
+ 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 (
@@ -25,5 +25,3 @@ except (LoopImportError, ImportError):
25
25
  logger.warning(
26
26
  "Cannot use LoopProjectfileProcessor: Loop project file cannot be imported, try installing LoopProjectFile"
27
27
  )
28
- # from LoopStructural.modelling.features import StructuralFrame
29
- # from LoopStructural.modelling.features.fault import FaultSegment
@@ -649,7 +649,7 @@ class GeologicalModel:
649
649
  for g in stratigraphic_column.keys():
650
650
  for u in stratigraphic_column[g].keys():
651
651
  stratigraphic_column[g][u]["colour"] = cmap_colours[ci, :]
652
-
652
+ ci += 1
653
653
  self.stratigraphic_column = stratigraphic_column
654
654
 
655
655
  def create_and_add_foliation(
@@ -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"
@@ -1184,7 +1182,7 @@ class GeologicalModel:
1184
1182
  logger.debug(f"Reached unconformity {f.name}")
1185
1183
  break
1186
1184
  logger.debug(f"Adding {uc_feature.name} as unconformity to {f.name}")
1187
- if f.type == FeatureType.FAULT:
1185
+ if f.type == FeatureType.FAULT or f.type == FeatureType.INACTIVEFAULT:
1188
1186
  continue
1189
1187
  if f == feature:
1190
1188
  continue
@@ -1419,7 +1417,7 @@ class GeologicalModel:
1419
1417
  return fault
1420
1418
 
1421
1419
  # TODO move rescale to bounding box/transformer
1422
- def rescale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
1420
+ def rescale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
1423
1421
  """
1424
1422
  Convert from model scale to real world scale - in the future this
1425
1423
  should also do transformations?
@@ -1442,7 +1440,7 @@ class GeologicalModel:
1442
1440
  return points
1443
1441
 
1444
1442
  # TODO move scale to bounding box/transformer
1445
- def scale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
1443
+ def scale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
1446
1444
  """Take points in UTM coordinates and reproject
1447
1445
  into scaled model space
1448
1446
 
@@ -1811,7 +1809,7 @@ class GeologicalModel:
1811
1809
  grid = self.bounding_box.structured_grid(name=name)
1812
1810
 
1813
1811
  grid.cell_properties['stratigraphy'] = self.evaluate_model(
1814
- self.bounding_box.cell_centers(), scale=False
1812
+ self.rescale(self.bounding_box.cell_centers())
1815
1813
  )
1816
1814
  return grid, self.stratigraphic_ids()
1817
1815
 
@@ -1826,6 +1824,7 @@ class GeologicalModel:
1826
1824
  ):
1827
1825
  path = pathlib.Path(filename)
1828
1826
  extension = path.suffix
1827
+ parent = path.parent
1829
1828
  name = path.stem
1830
1829
  stratigraphic_surfaces = self.get_stratigraphic_surfaces()
1831
1830
  if fault_surfaces:
@@ -1834,19 +1833,19 @@ class GeologicalModel:
1834
1833
  if extension == ".geoh5" or extension == '.omf':
1835
1834
  s.save(filename)
1836
1835
  else:
1837
- s.save(f'{name}_{s.name}.{extension}')
1836
+ s.save(f'{parent}/{name}_{s.name}{extension}')
1838
1837
  if stratigraphic_surfaces:
1839
1838
  for s in self.get_stratigraphic_surfaces():
1840
1839
  if extension == ".geoh5" or extension == '.omf':
1841
1840
  s.save(filename)
1842
1841
  else:
1843
- s.save(f'{name}_{s.name}.{extension}')
1842
+ s.save(f'{parent}/{name}_{s.name}{extension}')
1844
1843
  if block_model:
1845
1844
  grid, ids = self.get_block_model()
1846
1845
  if extension == ".geoh5" or extension == '.omf':
1847
1846
  grid.save(filename)
1848
1847
  else:
1849
- grid.save(f'{name}_block_model.{extension}')
1848
+ grid.save(f'{parent}/{name}_block_model{extension}')
1850
1849
  if stratigraphic_data:
1851
1850
  if self.stratigraphic_column is not None:
1852
1851
  for group in self.stratigraphic_column.keys():
@@ -1856,7 +1855,7 @@ class GeologicalModel:
1856
1855
  if extension == ".geoh5" or extension == '.omf':
1857
1856
  data.save(filename)
1858
1857
  else:
1859
- data.save(f'{name}_{group}_data.{extension}')
1858
+ data.save(f'{parent}/{name}_{group}_data{extension}')
1860
1859
  if fault_data:
1861
1860
  for f in self.fault_names():
1862
1861
  for d in self.__getitem__(f).get_data():
@@ -1864,4 +1863,4 @@ class GeologicalModel:
1864
1863
 
1865
1864
  d.save(filename)
1866
1865
  else:
1867
- d.save(f'{name}_{group}.{extension}')
1866
+ d.save(f'{parent}/{name}_{group}{extension}')
@@ -30,3 +30,4 @@ from ._cross_product_geological_feature import CrossProductGeologicalFeature
30
30
 
31
31
  from ._unconformity_feature import UnconformityFeature
32
32
  from ._analytical_feature import AnalyticalGeologicalFeature
33
+ from ._projected_vector_feature import ProjectedVectorFeature
@@ -28,10 +28,27 @@ 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
- self.vector = np.array(vector, dtype=float)
34
- self.origin = np.array(origin, dtype=float)
42
+ try:
43
+ self.vector = np.array(vector, dtype=float).reshape(3)
44
+ except ValueError:
45
+ logger.error("AnalyticalGeologicalFeature: vector must be a 3 element array")
46
+ raise ValueError("vector must be a 3 element array")
47
+ try:
48
+ self.origin = np.array(origin, dtype=float).reshape(3)
49
+ except ValueError:
50
+ logger.error("AnalyticalGeologicalFeature: origin must be a 3 element array")
51
+ raise ValueError("origin must be a 3 element array")
35
52
  self.type = FeatureType.ANALYTICAL
36
53
 
37
54
  def to_json(self):
@@ -48,16 +65,16 @@ class AnalyticalGeologicalFeature(BaseFeature):
48
65
  json["origin"] = self.origin.tolist()
49
66
  return json
50
67
 
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:
68
+ def evaluate_value(self, pos: np.ndarray, ignore_regions=False):
69
+ pos = np.array(pos)
70
+ if len(pos.shape) == 1:
71
+ pos = pos[None, :]
72
+ if len(pos.shape) != 2:
56
73
  raise ValueError("xyz must be a 1D or 2D array")
57
- xyz2 = np.zeros(xyz.shape)
58
- xyz2[:] = xyz[:]
74
+ xyz2 = np.zeros(pos.shape)
75
+ xyz2[:] = pos[:]
59
76
  for f in self.faults:
60
- xyz2[:] = f.apply_to_points(xyz)
77
+ xyz2[:] = f.apply_to_points(pos)
61
78
  if self.model is not None:
62
79
  xyz2[:] = self.model.rescale(xyz2, inplace=False)
63
80
  xyz2[:] = xyz2 - self.origin
@@ -65,15 +82,28 @@ class AnalyticalGeologicalFeature(BaseFeature):
65
82
  distance = normal[0] * xyz2[:, 0] + normal[1] * xyz2[:, 1] + normal[2] * xyz2[:, 2]
66
83
  return distance / np.linalg.norm(self.vector)
67
84
 
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)
85
+ def evaluate_gradient(self, pos: np.ndarray, ignore_regions=False):
86
+ pos = np.array(pos)
87
+ if len(pos.shape) == 1:
88
+ pos = pos[None, :]
89
+ if len(pos.shape) != 2:
90
+ raise ValueError("pos must be a 1D or 2D array")
91
+ v = np.zeros(pos.shape)
75
92
  v[:, :] = self.vector[None, :]
76
93
  return v
77
94
 
78
95
  def get_data(self, value_map: Optional[dict] = None):
79
96
  return
97
+
98
+ def copy(self, name: Optional[str] = None):
99
+ if name is None:
100
+ name = self.name
101
+ return AnalyticalGeologicalFeature(
102
+ name,
103
+ self.vector.copy(),
104
+ self.origin.copy(),
105
+ list(self.regions),
106
+ list(self.faults),
107
+ self.model,
108
+ self.builder,
109
+ )
@@ -291,11 +291,29 @@ class BaseFeature(metaclass=ABCMeta):
291
291
  if self.model is None:
292
292
  raise ValueError("Must specify bounding box")
293
293
  bounding_box = self.model.bounding_box
294
- callable = lambda xyz: self.evaluate_value(self.model.scale(xyz))
295
- isosurfacer = LoopIsosurfacer(bounding_box, callable=callable)
296
- if name is None and self.name is not None:
297
- name = self.name
298
- return isosurfacer.fit(value, name)
294
+ regions = self.regions
295
+ try:
296
+ self.regions = [
297
+ r for r in self.regions if r.name != self.name and r.parent.name != self.name
298
+ ]
299
+
300
+ callable = lambda xyz: (
301
+ self.evaluate_value(self.model.scale(xyz))
302
+ if self.model is not None
303
+ else self.evaluate_value(xyz)
304
+ )
305
+ isosurfacer = LoopIsosurfacer(bounding_box, callable=callable)
306
+ if name is None and self.name is not None:
307
+ name = self.name
308
+ surfaces = isosurfacer.fit(value, name)
309
+ except Exception as e:
310
+ logger.error(f"Failed to create surface for {self.name} at value {value}")
311
+ logger.error(e)
312
+ surfaces = []
313
+ finally:
314
+ self.regions = regions
315
+
316
+ return surfaces
299
317
 
300
318
  def scalar_field(self, bounding_box=None):
301
319
  """Create a scalar field for the feature
@@ -341,10 +359,10 @@ class BaseFeature(metaclass=ABCMeta):
341
359
  if self.model is None:
342
360
  raise ValueError("Must specify bounding box")
343
361
  bounding_box = self.model.bounding_box
344
- grid = bounding_box.vtk()
345
- points = grid.points
362
+ points = bounding_box.cell_centers()
346
363
  value = self.evaluate_gradient(points)
347
-
364
+ if self.model is not None:
365
+ points = self.model.rescale(points)
348
366
  return VectorPoints(points, value, self.name)
349
367
 
350
368
  @abstractmethod
@@ -362,3 +380,14 @@ class BaseFeature(metaclass=ABCMeta):
362
380
  dictionary of data
363
381
  """
364
382
  raise NotImplementedError
383
+
384
+ @abstractmethod
385
+ def copy(self, name: Optional[str] = None):
386
+ """Copy the feature
387
+
388
+ Returns
389
+ -------
390
+ BaseFeature
391
+ copied feature
392
+ """
393
+ raise NotImplementedError
@@ -98,3 +98,10 @@ class CrossProductGeologicalFeature(BaseFeature):
98
98
 
99
99
  def get_data(self, value_map: Optional[dict] = None):
100
100
  return
101
+
102
+ def copy(self, name: Optional[str] = None):
103
+ if name is None:
104
+ name = f'{self.name}_copy'
105
+ return CrossProductGeologicalFeature(
106
+ name, self.geological_feature_a, self.geological_feature_b
107
+ )