LoopStructural 1.6.3__tar.gz → 1.6.6__tar.gz

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 (146) hide show
  1. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datatypes/_bounding_box.py +58 -3
  2. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datatypes/_point.py +36 -7
  3. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/__init__.py +1 -1
  4. loopstructural-1.6.6/LoopStructural/interpolators/_builders.py +149 -0
  5. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_finite_difference_interpolator.py +11 -11
  6. loopstructural-1.6.6/LoopStructural/interpolators/_interpolator_builder.py +55 -0
  7. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_interpolator_factory.py +7 -18
  8. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_3d_base_structured.py +4 -0
  9. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_3d_structured_grid.py +0 -3
  10. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/__init__.py +0 -2
  11. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/core/geological_model.py +11 -10
  12. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/__init__.py +1 -0
  13. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/_analytical_feature.py +23 -2
  14. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/_base_geological_feature.py +37 -8
  15. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/_cross_product_geological_feature.py +7 -0
  16. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/_geological_feature.py +3 -1
  17. loopstructural-1.6.6/LoopStructural/modelling/features/_projected_vector_feature.py +112 -0
  18. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/_structural_frame.py +16 -18
  19. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/_unconformity_feature.py +3 -3
  20. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/builders/_folded_feature_builder.py +2 -2
  21. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/builders/_structural_frame_builder.py +2 -2
  22. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fault/_fault_function_feature.py +3 -0
  23. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fault/_fault_segment.py +35 -4
  24. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/input/process_data.py +41 -26
  25. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/input/project_file.py +44 -39
  26. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/intrusions/intrusion_feature.py +3 -0
  27. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/__init__.py +1 -0
  28. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/_surface.py +12 -3
  29. loopstructural-1.6.6/LoopStructural/utils/_transformation.py +160 -0
  30. loopstructural-1.6.6/LoopStructural/utils/colours.py +50 -0
  31. loopstructural-1.6.6/LoopStructural/version.py +1 -0
  32. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural.egg-info/PKG-INFO +16 -2
  33. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural.egg-info/SOURCES.txt +2 -0
  34. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural.egg-info/requires.txt +15 -0
  35. {loopstructural-1.6.3 → loopstructural-1.6.6}/PKG-INFO +16 -2
  36. {loopstructural-1.6.3 → loopstructural-1.6.6}/pyproject.toml +1 -0
  37. loopstructural-1.6.3/LoopStructural/interpolators/_builders.py +0 -149
  38. loopstructural-1.6.3/LoopStructural/utils/_transformation.py +0 -76
  39. loopstructural-1.6.3/LoopStructural/utils/colours.py +0 -26
  40. loopstructural-1.6.3/LoopStructural/version.py +0 -1
  41. {loopstructural-1.6.3 → loopstructural-1.6.6}/LICENSE +0 -0
  42. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/__init__.py +0 -0
  43. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/__init__.py +0 -0
  44. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/_base.py +0 -0
  45. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/_example_models.py +0 -0
  46. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/claudius.csv +0 -0
  47. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/claudiusbb.txt +0 -0
  48. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/duplex.csv +0 -0
  49. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/duplexbb.txt +0 -0
  50. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/fault_trace/fault_trace.cpg +0 -0
  51. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/fault_trace/fault_trace.dbf +0 -0
  52. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/fault_trace/fault_trace.prj +0 -0
  53. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/fault_trace/fault_trace.shp +0 -0
  54. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/fault_trace/fault_trace.shx +0 -0
  55. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/bbox.csv +0 -0
  56. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/contacts.csv +0 -0
  57. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/fault_displacement.csv +0 -0
  58. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/fault_edges.txt +0 -0
  59. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/fault_locations.csv +0 -0
  60. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/fault_orientations.csv +0 -0
  61. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv +0 -0
  62. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/stratigraphic_orientations.csv +0 -0
  63. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv +0 -0
  64. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/intrusion.csv +0 -0
  65. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/intrusionbb.txt +0 -0
  66. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/onefoldbb.txt +0 -0
  67. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/onefolddata.csv +0 -0
  68. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/refolded_bb.txt +0 -0
  69. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/refolded_fold.csv +0 -0
  70. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datasets/data/tabular_intrusion.csv +0 -0
  71. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datatypes/__init__.py +0 -0
  72. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datatypes/_structured_grid.py +0 -0
  73. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/datatypes/_surface.py +0 -0
  74. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/export/exporters.py +0 -0
  75. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/export/file_formats.py +0 -0
  76. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/export/geoh5.py +0 -0
  77. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/export/gocad.py +0 -0
  78. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/export/omf_wrapper.py +0 -0
  79. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_api.py +0 -0
  80. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_cython/__init__.py +0 -0
  81. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_discrete_fold_interpolator.py +0 -0
  82. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_discrete_interpolator.py +0 -0
  83. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_geological_interpolator.py +0 -0
  84. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_operator.py +0 -0
  85. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_p1interpolator.py +0 -0
  86. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_p2interpolator.py +0 -0
  87. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/_surfe_wrapper.py +0 -0
  88. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_2d_base_unstructured.py +0 -0
  89. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_2d_p1_unstructured.py +0 -0
  90. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_2d_p2_unstructured.py +0 -0
  91. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_2d_structured_grid.py +0 -0
  92. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_2d_structured_tetra.py +0 -0
  93. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_3d_p2_tetra.py +0 -0
  94. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_3d_structured_tetra.py +0 -0
  95. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +0 -0
  96. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/__init__.py +0 -0
  97. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_aabb.py +0 -0
  98. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_base_support.py +0 -0
  99. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_face_table.py +0 -0
  100. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/interpolators/supports/_support_factory.py +0 -0
  101. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/core/__init__.py +0 -0
  102. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/_lambda_geological_feature.py +0 -0
  103. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/_region.py +0 -0
  104. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/builders/__init__.py +0 -0
  105. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/builders/_base_builder.py +0 -0
  106. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/builders/_fault_builder.py +0 -0
  107. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/builders/_geological_feature_builder.py +0 -0
  108. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fault/__init__.py +0 -0
  109. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fault/_fault_function.py +0 -0
  110. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/__init__.py +0 -0
  111. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/_fold.py +0 -0
  112. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -0
  113. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/_foldframe.py +0 -0
  114. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/_svariogram.py +0 -0
  115. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/fold_function/__init__.py +0 -0
  116. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +0 -0
  117. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +0 -0
  118. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +0 -0
  119. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +0 -0
  120. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/input/__init__.py +0 -0
  121. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/input/fault_network.py +0 -0
  122. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/input/map2loop_processor.py +0 -0
  123. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/intrusions/__init__.py +0 -0
  124. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/intrusions/geom_conceptual_models.py +0 -0
  125. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/intrusions/geometric_scaling_functions.py +0 -0
  126. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/intrusions/intrusion_builder.py +0 -0
  127. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/intrusions/intrusion_frame_builder.py +0 -0
  128. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/modelling/intrusions/intrusion_support_functions.py +0 -0
  129. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/config.py +0 -0
  130. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/dtm_creator.py +0 -0
  131. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/exceptions.py +0 -0
  132. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/features.py +0 -0
  133. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/helper.py +0 -0
  134. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/json_encoder.py +0 -0
  135. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/linalg.py +0 -0
  136. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/logging.py +0 -0
  137. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/maths.py +0 -0
  138. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/regions.py +0 -0
  139. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/typing.py +0 -0
  140. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/utils/utils.py +0 -0
  141. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural/visualisation/__init__.py +0 -0
  142. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural.egg-info/dependency_links.txt +0 -0
  143. {loopstructural-1.6.3 → loopstructural-1.6.6}/LoopStructural.egg-info/top_level.txt +0 -0
  144. {loopstructural-1.6.3 → loopstructural-1.6.6}/README.md +0 -0
  145. {loopstructural-1.6.3 → loopstructural-1.6.6}/setup.cfg +0 -0
  146. {loopstructural-1.6.3 → loopstructural-1.6.6}/setup.py +0 -0
@@ -410,9 +410,15 @@ class BoundingBox:
410
410
  import pyvista as pv
411
411
  except ImportError:
412
412
  raise ImportError("pyvista is required for vtk support")
413
- x = np.linspace(self.global_origin[0], self.global_maximum[0], self.nsteps[0])
414
- y = np.linspace(self.global_origin[1], self.global_maximum[1], self.nsteps[1])
415
- z = np.linspace(self.global_origin[2], self.global_maximum[2], self.nsteps[2])
413
+ x = np.linspace(
414
+ self.global_origin[0] + self.origin[0], self.global_maximum[0], self.nsteps[0]
415
+ )
416
+ y = np.linspace(
417
+ self.global_origin[1] + self.origin[1], self.global_maximum[1], self.nsteps[1]
418
+ )
419
+ z = np.linspace(
420
+ self.global_origin[2] + self.origin[2], self.global_maximum[2], self.nsteps[2]
421
+ )
416
422
  return pv.RectilinearGrid(
417
423
  x,
418
424
  y,
@@ -435,3 +441,52 @@ class BoundingBox:
435
441
  properties=_vertex_data,
436
442
  name=name,
437
443
  )
444
+
445
+ def project(self, xyz):
446
+ """Project a point into the bounding box
447
+
448
+ Parameters
449
+ ----------
450
+ xyz : np.ndarray
451
+ point to project
452
+
453
+ Returns
454
+ -------
455
+ np.ndarray
456
+ projected point
457
+ """
458
+
459
+ return (xyz - self.global_origin) / np.max(
460
+ (self.global_maximum - self.global_origin)
461
+ ) # np.clip(xyz, self.origin, self.maximum)
462
+
463
+ def reproject(self, xyz):
464
+ """Reproject a point from the bounding box to the global space
465
+
466
+ Parameters
467
+ ----------
468
+ xyz : np.ndarray
469
+ point to reproject
470
+
471
+ Returns
472
+ -------
473
+ np.ndarray
474
+ reprojected point
475
+ """
476
+
477
+ return xyz * np.max((self.global_maximum - self.global_origin)) + self.global_origin
478
+
479
+ def __repr__(self):
480
+ return f"BoundingBox({self.origin}, {self.maximum}, {self.nsteps})"
481
+
482
+ def __str__(self):
483
+ return f"BoundingBox({self.origin}, {self.maximum}, {self.nsteps})"
484
+
485
+ def __eq__(self, other):
486
+ if not isinstance(other, BoundingBox):
487
+ return False
488
+ return (
489
+ np.allclose(self.origin, other.origin)
490
+ and np.allclose(self.maximum, other.maximum)
491
+ and np.allclose(self.nsteps, other.nsteps)
492
+ )
@@ -25,11 +25,14 @@ class ValuePoints:
25
25
  ),
26
26
  }
27
27
 
28
- def vtk(self):
28
+ def vtk(self, scalars=None):
29
29
  import pyvista as pv
30
30
 
31
31
  points = pv.PolyData(self.locations)
32
- points["values"] = self.values
32
+ if scalars is not None and len(scalars) == len(self.locations):
33
+ points.point_data['scalars'] = scalars
34
+ else:
35
+ points["values"] = self.values
33
36
  return points
34
37
 
35
38
  def plot(self, pyvista_kwargs={}):
@@ -123,22 +126,48 @@ class VectorPoints:
123
126
  def from_dict(self, d):
124
127
  return VectorPoints(d['locations'], d['vectors'], d['name'], d.get('properties', None))
125
128
 
126
- def vtk(self, geom='arrow', scale=1.0, scale_function=None, tolerance=0.05):
129
+ def vtk(
130
+ self,
131
+ geom='arrow',
132
+ scale=0.10,
133
+ scale_function=None,
134
+ normalise=True,
135
+ tolerance=0.05,
136
+ bb=None,
137
+ scalars=None,
138
+ ):
127
139
  import pyvista as pv
128
140
 
141
+ _projected = False
129
142
  vectors = np.copy(self.vectors)
143
+ if normalise:
144
+ norm = np.linalg.norm(vectors, axis=1)
145
+ vectors[norm > 0, :] /= norm[norm > 0][:, None]
130
146
  if scale_function is not None:
131
- vectors /= np.linalg.norm(vectors, axis=1)[:, None]
147
+ # vectors /= np.linalg.norm(vectors, axis=1)[:, None]
132
148
  vectors *= scale_function(self.locations)[:, None]
133
- points = pv.PolyData(self.locations)
149
+ locations = self.locations
150
+ if bb is not None:
151
+ try:
152
+ locations = bb.project(locations)
153
+ _projected = True
154
+ except Exception as e:
155
+ logger.error(f'Failed to project points to bounding box: {e}')
156
+ logger.error('Using unprojected points, this may cause issues with the glyphing')
157
+ points = pv.PolyData(locations)
158
+ if scalars is not None and len(scalars) == len(self.locations):
159
+ points['scalars'] = scalars
134
160
  points.point_data.set_vectors(vectors, 'vectors')
135
161
  if geom == 'arrow':
136
162
  geom = pv.Arrow(scale=scale)
137
163
  elif geom == 'disc':
138
- geom = pv.Disc(inner=0, outer=scale)
164
+ geom = pv.Disc(inner=0, outer=scale).rotate_y(90)
139
165
 
140
166
  # Perform the glyph
141
- return points.glyph(orient="vectors", geom=geom, tolerance=tolerance)
167
+ glyphed = points.glyph(orient="vectors", geom=geom, tolerance=tolerance, scale=False)
168
+ if _projected:
169
+ glyphed.points = bb.reproject(glyphed.points)
170
+ return glyphed
142
171
 
143
172
  def plot(self, pyvista_kwargs={}):
144
173
  """Calls pyvista plot on the vtk object
@@ -114,5 +114,5 @@ support_interpolator_map = {
114
114
  }
115
115
 
116
116
  from ._interpolator_factory import InterpolatorFactory
117
-
117
+ from ._interpolator_builder import InterpolatorBuilder
118
118
  # from ._api import LoopInterpolator
@@ -0,0 +1,149 @@
1
+ # from LoopStructural.utils.exceptions import LoopException
2
+ # import numpy as np
3
+ # from typing import Optional
4
+ # from LoopStructural.interpolators import (
5
+ # P1Interpolator,
6
+ # P2Interpolator,
7
+ # FiniteDifferenceInterpolator,
8
+ # GeologicalInterpolator,
9
+ # DiscreteFoldInterpolator,
10
+ # StructuredGrid,
11
+ # TetMesh,
12
+ # )
13
+ # from LoopStructural.datatypes import BoundingBox
14
+ # from LoopStructural.utils.logging import getLogger
15
+
16
+ # logger = getLogger(__name__)
17
+
18
+
19
+ # def get_interpolator(
20
+ # bounding_box: BoundingBox,
21
+ # interpolatortype: str,
22
+ # nelements: int,
23
+ # element_volume: Optional[float] = None,
24
+ # buffer: float = 0.2,
25
+ # dimensions: int = 3,
26
+ # support=None,
27
+ # ) -> GeologicalInterpolator:
28
+ # # add a buffer to the interpolation domain, this is necessary for
29
+ # # faults but also generally a good
30
+ # # idea to avoid boundary problems
31
+ # # buffer = bb[1, :]
32
+ # origin = bounding_box.with_buffer(buffer).origin
33
+ # maximum = bounding_box.with_buffer(buffer).maximum
34
+ # box_vol = np.prod(maximum - origin)
35
+ # if interpolatortype == "PLI":
36
+ # if support is None:
37
+ # if element_volume is None:
38
+ # # nelements /= 5
39
+ # element_volume = box_vol / nelements
40
+ # # calculate the step vector of a regular cube
41
+ # step_vector = np.zeros(3)
42
+ # step_vector[:] = element_volume ** (1.0 / 3.0)
43
+ # # step_vector /= np.array([1,1,2])
44
+ # # number of steps is the length of the box / step vector
45
+ # nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
46
+ # if np.any(np.less(nsteps, 3)):
47
+ # axis_labels = ["x", "y", "z"]
48
+ # for i in range(3):
49
+ # if nsteps[i] < 3:
50
+ # nsteps[i] = 3
51
+ # logger.error(
52
+ # f"Number of steps in direction {axis_labels[i]} is too small, try increasing nelements"
53
+ # )
54
+ # logger.error("Cannot create interpolator: number of steps is too small")
55
+ # raise ValueError("Number of steps too small cannot create interpolator")
56
+
57
+ # support = TetMesh(origin=origin, nsteps=nsteps, step_vector=step_vector)
58
+ # logger.info(
59
+ # "Creating regular tetrahedron mesh with %i elements \n"
60
+ # "for modelling using PLI" % (support.ntetra)
61
+ # )
62
+
63
+ # return P1Interpolator(support)
64
+ # if interpolatortype == "P2":
65
+ # if support is not None:
66
+ # logger.info(
67
+ # "Creating regular tetrahedron mesh with %i elements \n"
68
+ # "for modelling using P2" % (support.ntetra)
69
+ # )
70
+ # return P2Interpolator(support)
71
+ # else:
72
+ # raise ValueError("Cannot create P2 interpolator without support, try using PLI")
73
+
74
+ # if interpolatortype == "FDI":
75
+ # # find the volume of one element
76
+ # if element_volume is None:
77
+ # element_volume = box_vol / nelements
78
+ # # calculate the step vector of a regular cube
79
+ # step_vector = np.zeros(3)
80
+ # step_vector[:] = element_volume ** (1.0 / 3.0)
81
+ # # number of steps is the length of the box / step vector
82
+ # nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
83
+ # if np.any(np.less(nsteps, 3)):
84
+ # logger.error("Cannot create interpolator: number of steps is too small")
85
+ # axis_labels = ["x", "y", "z"]
86
+ # for i in range(3):
87
+ # if nsteps[i] < 3:
88
+ # nsteps[i] = 3
89
+ # # logger.error(
90
+ # # f"Number of steps in direction {axis_labels[i]} is too small, try increasing nelements"
91
+ # # )
92
+ # # raise ValueError("Number of steps too small cannot create interpolator")
93
+ # # create a structured grid using the origin and number of steps
94
+
95
+ # grid = StructuredGrid(origin=origin, nsteps=nsteps, step_vector=step_vector)
96
+ # logger.info(
97
+ # f"Creating regular grid with {grid.n_elements} elements \n" "for modelling using FDI"
98
+ # )
99
+ # return FiniteDifferenceInterpolator(grid)
100
+ # if interpolatortype == "DFI":
101
+ # if element_volume is None:
102
+ # nelements /= 5
103
+ # element_volume = box_vol / nelements
104
+ # # calculate the step vector of a regular cube
105
+ # step_vector = np.zeros(3)
106
+ # step_vector[:] = element_volume ** (1.0 / 3.0)
107
+ # # number of steps is the length of the box / step vector
108
+ # nsteps = np.ceil((maximum - origin) / step_vector).astype(int)
109
+ # # create a structured grid using the origin and number of steps
110
+
111
+ # mesh = TetMesh(origin=origin, nsteps=nsteps, step_vector=step_vector)
112
+ # logger.info(
113
+ # f"Creating regular tetrahedron mesh with {mesh.ntetra} elements \n"
114
+ # "for modelling using DFI"
115
+ # )
116
+ # return DiscreteFoldInterpolator(mesh, None)
117
+ # raise LoopException("No interpolator")
118
+ # # fi interpolatortype == "DFI" and dfi is True:
119
+ # # if element_volume is None:
120
+ # # nelements /= 5
121
+ # # element_volume = box_vol / nelements
122
+ # # # calculate the step vector of a regular cube
123
+ # # step_vector = np.zeros(3)
124
+ # # step_vector[:] = element_volume ** (1.0 / 3.0)
125
+ # # # number of steps is the length of the box / step vector
126
+ # # nsteps = np.ceil((bb[1, :] - bb[0, :]) / step_vector).astype(int)
127
+ # # # create a structured grid using the origin and number of steps
128
+ # # if "meshbuilder" in kwargs:
129
+ # # mesh = kwargs["meshbuilder"].build(bb, nelements)
130
+ # # else:
131
+ # # mesh = kwargs.get(
132
+ # # "mesh",
133
+ # # TetMesh(origin=bb[0, :], nsteps=nsteps, step_vector=step_vector),
134
+ # # )
135
+ # # logger.info(
136
+ # # f"Creating regular tetrahedron mesh with {mesh.ntetra} elements \n"
137
+ # # "for modelling using DFI"
138
+ # # )
139
+ # # return DFI(mesh, kwargs["fold"])
140
+ # # if interpolatortype == "Surfe" or interpolatortype == "surfe":
141
+ # # # move import of surfe to where we actually try and use it
142
+ # # if not surfe:
143
+ # # logger.warning("Cannot import Surfe, try another interpolator")
144
+ # # raise ImportError("Cannot import surfepy, try pip install surfe")
145
+ # # method = kwargs.get("method", "single_surface")
146
+ # # logger.info("Using surfe interpolator")
147
+ # # return SurfeRBFInterpolator(method)
148
+ # # logger.warning("No interpolator")
149
+ # # raise InterpolatorError("Could not create interpolator")
@@ -97,7 +97,7 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
97
97
  self.add_interface_constraints(self.interpolation_weights["ipw"])
98
98
  self.add_value_inequality_constraints()
99
99
  self.add_inequality_pairs_constraints(
100
- kwargs.get('inequality_pairs', None),
100
+ pairs=kwargs.get('inequality_pairs', None),
101
101
  upper_bound=kwargs.get('inequality_pair_upper_bound', np.finfo(float).eps),
102
102
  lower_bound=kwargs.get('inequality_pair_lower_bound', -np.inf),
103
103
  )
@@ -172,16 +172,18 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
172
172
  # get elements for points
173
173
  points = self.get_interface_constraints()
174
174
  if points.shape[0] > 1:
175
- vertices, c, tetras, inside = self.support.get_element_for_location(
175
+ node_idx, inside = self.support.position_to_cell_corners(
176
176
  points[:, : self.support.dimension]
177
177
  )
178
- # calculate volume of tetras
179
- # vecs = vertices[inside, 1:, :] - vertices[inside, 0, None, :]
180
- # vol = np.abs(np.linalg.det(vecs)) / 6
181
- A = c[inside, :]
182
- # A *= vol[:,None]
183
- idc = tetras[inside, :]
184
-
178
+ gi = np.zeros(self.support.n_nodes, dtype=int)
179
+ gi[:] = -1
180
+ gi[self.region] = np.arange(0, self.nx, dtype=int)
181
+ idc = np.zeros(node_idx.shape).astype(int)
182
+ idc[:] = -1
183
+ idc[inside, :] = gi[node_idx[inside, :]]
184
+ inside = np.logical_and(~np.any(idc == -1, axis=1), inside)
185
+ idc = idc[inside, :]
186
+ A = self.support.position_to_dof_coefs(points[inside, : self.support.dimension])
185
187
  for unique_id in np.unique(
186
188
  points[
187
189
  np.logical_and(~np.isnan(points[:, self.support.dimension]), inside),
@@ -197,7 +199,6 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
197
199
  ).T.reshape(-1, 2)
198
200
  interface_A = np.hstack([A[mask, :][ij[:, 0], :], -A[mask, :][ij[:, 1], :]])
199
201
  interface_idc = np.hstack([idc[mask, :][ij[:, 0], :], idc[mask, :][ij[:, 1], :]])
200
-
201
202
  # now map the index from global to region create array size of mesh
202
203
  # initialise as np.nan, then map points inside region to 0->nx
203
204
  gi = np.zeros(self.support.n_nodes).astype(int)
@@ -229,7 +230,6 @@ class FiniteDifferenceInterpolator(DiscreteInterpolator):
229
230
  points = self.get_gradient_constraints()
230
231
  if points.shape[0] > 0:
231
232
  # calculate unit vector for orientation data
232
- # points[:,3:]/=np.linalg.norm(points[:,3:],axis=1)[:,None]
233
233
 
234
234
  node_idx, inside = self.support.position_to_cell_corners(
235
235
  points[:, : self.support.dimension]
@@ -0,0 +1,55 @@
1
+ from LoopStructural.interpolators import (
2
+ GeologicalInterpolator,
3
+ InterpolatorFactory,
4
+ InterpolatorType,
5
+ )
6
+ from LoopStructural.datatypes import BoundingBox
7
+ from typing import Optional, Union
8
+ import numpy as np
9
+
10
+
11
+ class InterpolatorBuilder:
12
+ def __init__(
13
+ self,
14
+ interpolatortype: Union[str, InterpolatorType],
15
+ bounding_box: BoundingBox,
16
+ nelements: int = 1000,
17
+ buffer: float = 0.2,
18
+ **kwargs,
19
+ ):
20
+ self.interpolatortype = interpolatortype
21
+ self.bounding_box = bounding_box
22
+ self.nelements = nelements
23
+ self.buffer = buffer
24
+ self.kwargs = kwargs
25
+ self.interpolator : Optional[GeologicalInterpolator]= None
26
+
27
+ def create_interpolator(self) -> 'InterpolatorBuilder':
28
+ self.interpolator = InterpolatorFactory.create_interpolator(
29
+ interpolatortype=self.interpolatortype,
30
+ boundingbox=self.bounding_box,
31
+ nelements=self.nelements,
32
+ buffer=self.buffer,
33
+ **self.kwargs,
34
+ )
35
+ return self
36
+
37
+ def set_value_constraints(self, value_constraints: np.ndarray) -> 'InterpolatorBuilder':
38
+ if self.interpolator:
39
+ self.interpolator.set_value_constraints(value_constraints)
40
+ return self
41
+
42
+ def set_gradient_constraints(self, gradient_constraints: np.ndarray) -> 'InterpolatorBuilder':
43
+ if self.interpolator:
44
+ self.interpolator.set_gradient_constraints(gradient_constraints)
45
+ return self
46
+
47
+ def set_normal_constraints(self, normal_constraints: np.ndarray) -> 'InterpolatorBuilder':
48
+ if self.interpolator:
49
+ self.interpolator.set_normal_constraints(normal_constraints)
50
+ return self
51
+
52
+ def setup_interpolator(self, **kwargs) -> Optional[GeologicalInterpolator]:
53
+ if self.interpolator:
54
+ self.interpolator.setup(**kwargs)
55
+ return self.interpolator
@@ -65,25 +65,14 @@ class InterpolatorFactory:
65
65
  gradient_norm_constraints: Optional[np.ndarray] = None,
66
66
  gradient_constraints: Optional[np.ndarray] = None,
67
67
  ):
68
- if interpolatortype is None:
69
- raise ValueError("No interpolator type specified")
70
- if boundingbox is None:
71
- raise ValueError("No bounding box specified")
72
- if nelements is None:
73
- raise ValueError("No number of elements specified")
74
- if isinstance(interpolatortype, str):
75
- interpolatortype = InterpolatorType._member_map_[interpolatortype].numerator
76
- if support is None:
77
- raise Exception("Support must be specified")
78
- # supporttype = support_interpolator_map[interpolatortype]
79
- # support = SupportFactory.create_support(
80
- # supporttype, boundingbox, nelements, element_volume
81
- # )
82
- interpolator = interpolator_map[interpolatortype](support)
68
+ interpolator = InterpolatorFactory.create_interpolator(
69
+ interpolatortype, boundingbox, nelements, element_volume, support
70
+ )
83
71
  if value_constraints is not None:
84
- interpolator.add_value_constraints(value_constraints)
72
+ interpolator.set_value_constraints(value_constraints)
85
73
  if gradient_norm_constraints is not None:
86
- interpolator.add_gradient_constraints(gradient_norm_constraints)
74
+ interpolator.set_normal_constraints(gradient_norm_constraints)
87
75
  if gradient_constraints is not None:
88
- interpolator.add_gradient_constraints(gradient_constraints)
76
+ interpolator.set_gradient_constraints(gradient_constraints)
77
+ interpolator.setup()
89
78
  return interpolator
@@ -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)
@@ -342,9 +342,6 @@ class StructuredGrid(BaseStructuredSupport):
342
342
  idc, inside = self.position_to_cell_corners(evaluation_points)
343
343
  T = np.zeros((idc.shape[0], 3, 8))
344
344
  T[inside, :, :] = self.get_element_gradient_for_location(evaluation_points[inside, :])[1]
345
- # indices = np.array([self.position_to_cell_index(evaluation_points)])
346
- # idc = self.global_indicies(indices.swapaxes(0,1))
347
- # print(idc)
348
345
  if np.max(idc[inside, :]) > property_array.shape[0]:
349
346
  cix, ciy, ciz = self.position_to_cell_index(evaluation_points)
350
347
  if not np.all(cix[inside] < self.nsteps_cells[0]):
@@ -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(
@@ -1182,7 +1182,7 @@ class GeologicalModel:
1182
1182
  logger.debug(f"Reached unconformity {f.name}")
1183
1183
  break
1184
1184
  logger.debug(f"Adding {uc_feature.name} as unconformity to {f.name}")
1185
- if f.type == FeatureType.FAULT:
1185
+ if f.type == FeatureType.FAULT or f.type == FeatureType.INACTIVEFAULT:
1186
1186
  continue
1187
1187
  if f == feature:
1188
1188
  continue
@@ -1417,7 +1417,7 @@ class GeologicalModel:
1417
1417
  return fault
1418
1418
 
1419
1419
  # TODO move rescale to bounding box/transformer
1420
- def rescale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
1420
+ def rescale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
1421
1421
  """
1422
1422
  Convert from model scale to real world scale - in the future this
1423
1423
  should also do transformations?
@@ -1440,7 +1440,7 @@ class GeologicalModel:
1440
1440
  return points
1441
1441
 
1442
1442
  # TODO move scale to bounding box/transformer
1443
- def scale(self, points: np.ndarray, inplace: bool = True) -> np.ndarray:
1443
+ def scale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
1444
1444
  """Take points in UTM coordinates and reproject
1445
1445
  into scaled model space
1446
1446
 
@@ -1809,7 +1809,7 @@ class GeologicalModel:
1809
1809
  grid = self.bounding_box.structured_grid(name=name)
1810
1810
 
1811
1811
  grid.cell_properties['stratigraphy'] = self.evaluate_model(
1812
- self.bounding_box.cell_centers(), scale=False
1812
+ self.rescale(self.bounding_box.cell_centers())
1813
1813
  )
1814
1814
  return grid, self.stratigraphic_ids()
1815
1815
 
@@ -1824,6 +1824,7 @@ class GeologicalModel:
1824
1824
  ):
1825
1825
  path = pathlib.Path(filename)
1826
1826
  extension = path.suffix
1827
+ parent = path.parent
1827
1828
  name = path.stem
1828
1829
  stratigraphic_surfaces = self.get_stratigraphic_surfaces()
1829
1830
  if fault_surfaces:
@@ -1832,19 +1833,19 @@ class GeologicalModel:
1832
1833
  if extension == ".geoh5" or extension == '.omf':
1833
1834
  s.save(filename)
1834
1835
  else:
1835
- s.save(f'{name}_{s.name}.{extension}')
1836
+ s.save(f'{parent}/{name}_{s.name}{extension}')
1836
1837
  if stratigraphic_surfaces:
1837
1838
  for s in self.get_stratigraphic_surfaces():
1838
1839
  if extension == ".geoh5" or extension == '.omf':
1839
1840
  s.save(filename)
1840
1841
  else:
1841
- s.save(f'{name}_{s.name}.{extension}')
1842
+ s.save(f'{parent}/{name}_{s.name}{extension}')
1842
1843
  if block_model:
1843
1844
  grid, ids = self.get_block_model()
1844
1845
  if extension == ".geoh5" or extension == '.omf':
1845
1846
  grid.save(filename)
1846
1847
  else:
1847
- grid.save(f'{name}_block_model.{extension}')
1848
+ grid.save(f'{parent}/{name}_block_model{extension}')
1848
1849
  if stratigraphic_data:
1849
1850
  if self.stratigraphic_column is not None:
1850
1851
  for group in self.stratigraphic_column.keys():
@@ -1854,7 +1855,7 @@ class GeologicalModel:
1854
1855
  if extension == ".geoh5" or extension == '.omf':
1855
1856
  data.save(filename)
1856
1857
  else:
1857
- data.save(f'{name}_{group}_data.{extension}')
1858
+ data.save(f'{parent}/{name}_{group}_data{extension}')
1858
1859
  if fault_data:
1859
1860
  for f in self.fault_names():
1860
1861
  for d in self.__getitem__(f).get_data():
@@ -1862,4 +1863,4 @@ class GeologicalModel:
1862
1863
 
1863
1864
  d.save(filename)
1864
1865
  else:
1865
- 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
@@ -39,8 +39,16 @@ class AnalyticalGeologicalFeature(BaseFeature):
39
39
  builder=None,
40
40
  ):
41
41
  BaseFeature.__init__(self, name, model, faults, regions, builder)
42
- self.vector = np.array(vector, dtype=float)
43
- 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")
44
52
  self.type = FeatureType.ANALYTICAL
45
53
 
46
54
  def to_json(self):
@@ -86,3 +94,16 @@ class AnalyticalGeologicalFeature(BaseFeature):
86
94
 
87
95
  def get_data(self, value_map: Optional[dict] = None):
88
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