LoopStructural 1.6.16__tar.gz → 1.6.17__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 (148) hide show
  1. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/__init__.py +1 -0
  2. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_geological_interpolator.py +1 -2
  3. loopstructural-1.6.17/LoopStructural/modelling/core/fault_topology.py +234 -0
  4. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/core/geological_model.py +37 -22
  5. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/core/stratigraphic_column.py +42 -15
  6. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/__init__.py +1 -0
  7. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/_surface.py +6 -1
  8. loopstructural-1.6.17/LoopStructural/utils/observer.py +150 -0
  9. loopstructural-1.6.17/LoopStructural/version.py +1 -0
  10. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural.egg-info/PKG-INFO +1 -1
  11. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural.egg-info/SOURCES.txt +2 -1
  12. {loopstructural-1.6.16 → loopstructural-1.6.17}/PKG-INFO +1 -1
  13. {loopstructural-1.6.16 → loopstructural-1.6.17}/setup.cfg +0 -2
  14. loopstructural-1.6.16/LoopStructural/version.py +0 -1
  15. {loopstructural-1.6.16 → loopstructural-1.6.17}/LICENSE +0 -0
  16. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/__init__.py +0 -0
  17. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/_base.py +0 -0
  18. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/_example_models.py +0 -0
  19. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/claudius.csv +0 -0
  20. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/claudiusbb.txt +0 -0
  21. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/duplex.csv +0 -0
  22. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/duplexbb.txt +0 -0
  23. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/fault_trace/fault_trace.cpg +0 -0
  24. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/fault_trace/fault_trace.dbf +0 -0
  25. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/fault_trace/fault_trace.prj +0 -0
  26. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/fault_trace/fault_trace.shp +0 -0
  27. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/fault_trace/fault_trace.shx +0 -0
  28. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/bbox.csv +0 -0
  29. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/contacts.csv +0 -0
  30. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/fault_displacement.csv +0 -0
  31. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/fault_edges.txt +0 -0
  32. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/fault_locations.csv +0 -0
  33. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/fault_orientations.csv +0 -0
  34. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv +0 -0
  35. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/stratigraphic_orientations.csv +0 -0
  36. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv +0 -0
  37. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/intrusion.csv +0 -0
  38. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/intrusionbb.txt +0 -0
  39. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/onefoldbb.txt +0 -0
  40. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/onefolddata.csv +0 -0
  41. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/refolded_bb.txt +0 -0
  42. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/refolded_fold.csv +0 -0
  43. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datasets/data/tabular_intrusion.csv +0 -0
  44. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datatypes/__init__.py +0 -0
  45. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datatypes/_bounding_box.py +0 -0
  46. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datatypes/_point.py +0 -0
  47. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datatypes/_structured_grid.py +0 -0
  48. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/datatypes/_surface.py +0 -0
  49. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/export/exporters.py +0 -0
  50. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/export/file_formats.py +0 -0
  51. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/export/geoh5.py +0 -0
  52. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/export/gocad.py +0 -0
  53. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/export/omf_wrapper.py +0 -0
  54. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/__init__.py +0 -0
  55. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_api.py +0 -0
  56. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_builders.py +0 -0
  57. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_constant_norm.py +0 -0
  58. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_cython/__init__.py +0 -0
  59. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_discrete_fold_interpolator.py +0 -0
  60. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_discrete_interpolator.py +0 -0
  61. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_finite_difference_interpolator.py +0 -0
  62. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_interpolator_builder.py +0 -0
  63. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_interpolator_factory.py +0 -0
  64. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_interpolatortype.py +0 -0
  65. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_operator.py +0 -0
  66. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_p1interpolator.py +0 -0
  67. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_p2interpolator.py +0 -0
  68. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/_surfe_wrapper.py +0 -0
  69. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_2d_base_unstructured.py +0 -0
  70. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_2d_p1_unstructured.py +0 -0
  71. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_2d_p2_unstructured.py +0 -0
  72. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_2d_structured_grid.py +0 -0
  73. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_2d_structured_tetra.py +0 -0
  74. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_3d_base_structured.py +0 -0
  75. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_3d_p2_tetra.py +0 -0
  76. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_3d_structured_grid.py +0 -0
  77. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_3d_structured_tetra.py +0 -0
  78. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +0 -0
  79. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/__init__.py +0 -0
  80. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_aabb.py +0 -0
  81. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_base_support.py +0 -0
  82. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_face_table.py +0 -0
  83. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/interpolators/supports/_support_factory.py +0 -0
  84. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/__init__.py +0 -0
  85. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/core/__init__.py +0 -0
  86. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/__init__.py +0 -0
  87. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_analytical_feature.py +0 -0
  88. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_base_geological_feature.py +0 -0
  89. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_cross_product_geological_feature.py +0 -0
  90. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_geological_feature.py +0 -0
  91. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_lambda_geological_feature.py +0 -0
  92. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_projected_vector_feature.py +0 -0
  93. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_region.py +0 -0
  94. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_structural_frame.py +0 -0
  95. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/_unconformity_feature.py +0 -0
  96. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/builders/__init__.py +0 -0
  97. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/builders/_base_builder.py +0 -0
  98. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/builders/_fault_builder.py +0 -0
  99. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/builders/_folded_feature_builder.py +0 -0
  100. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/builders/_geological_feature_builder.py +0 -0
  101. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/builders/_structural_frame_builder.py +0 -0
  102. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fault/__init__.py +0 -0
  103. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fault/_fault_function.py +0 -0
  104. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fault/_fault_function_feature.py +0 -0
  105. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fault/_fault_segment.py +0 -0
  106. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/__init__.py +0 -0
  107. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/_fold.py +0 -0
  108. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -0
  109. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/_foldframe.py +0 -0
  110. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/_svariogram.py +0 -0
  111. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/fold_function/__init__.py +0 -0
  112. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +0 -0
  113. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +0 -0
  114. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +0 -0
  115. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +0 -0
  116. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/input/__init__.py +0 -0
  117. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/input/fault_network.py +0 -0
  118. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/input/map2loop_processor.py +0 -0
  119. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/input/process_data.py +0 -0
  120. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/input/project_file.py +0 -0
  121. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/intrusions/__init__.py +0 -0
  122. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/intrusions/geom_conceptual_models.py +0 -0
  123. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/intrusions/geometric_scaling_functions.py +0 -0
  124. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/intrusions/intrusion_builder.py +0 -0
  125. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/intrusions/intrusion_feature.py +0 -0
  126. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/intrusions/intrusion_frame_builder.py +0 -0
  127. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/modelling/intrusions/intrusion_support_functions.py +0 -0
  128. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/_transformation.py +0 -0
  129. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/colours.py +0 -0
  130. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/config.py +0 -0
  131. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/dtm_creator.py +0 -0
  132. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/exceptions.py +0 -0
  133. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/features.py +0 -0
  134. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/helper.py +0 -0
  135. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/json_encoder.py +0 -0
  136. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/linalg.py +0 -0
  137. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/logging.py +0 -0
  138. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/maths.py +0 -0
  139. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/regions.py +0 -0
  140. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/typing.py +0 -0
  141. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/utils/utils.py +0 -0
  142. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural/visualisation/__init__.py +0 -0
  143. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural.egg-info/dependency_links.txt +0 -0
  144. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural.egg-info/requires.txt +0 -0
  145. {loopstructural-1.6.16 → loopstructural-1.6.17}/LoopStructural.egg-info/top_level.txt +0 -0
  146. {loopstructural-1.6.16 → loopstructural-1.6.17}/README.md +0 -0
  147. {loopstructural-1.6.16 → loopstructural-1.6.17}/pyproject.toml +0 -0
  148. {loopstructural-1.6.16 → loopstructural-1.6.17}/setup.py +0 -0
@@ -20,6 +20,7 @@ ch.setLevel(logging.WARNING)
20
20
  loggers = {}
21
21
  from .modelling.core.geological_model import GeologicalModel
22
22
  from .modelling.core.stratigraphic_column import StratigraphicColumn
23
+ from .modelling.core.fault_topology import FaultTopology
23
24
  from .interpolators._api import LoopInterpolator
24
25
  from .interpolators import InterpolatorBuilder
25
26
  from .datatypes import BoundingBox
@@ -166,8 +166,7 @@ class GeologicalInterpolator(metaclass=ABCMeta):
166
166
  """
167
167
  if points.shape[1] == self.dimensions * 2:
168
168
  points = np.hstack([points, np.ones((points.shape[0], 1))])
169
- logger.warning(f"No weight provided for normal constraints, all weights are set to 1")
170
- raise Warning
169
+ logger.info("No weight provided for normal constraints, all weights are set to 1")
171
170
  if points.shape[1] < self.dimensions * 2 + 1:
172
171
  raise ValueError("Normal constraints must at least have X,Y,Z,nx,ny,nz")
173
172
  self.n_n = points.shape[0]
@@ -0,0 +1,234 @@
1
+ from ..features.fault import FaultSegment
2
+ from ...utils import Observable
3
+ from .stratigraphic_column import StratigraphicColumn
4
+ import enum
5
+ import numpy as np
6
+ class FaultRelationshipType(enum.Enum):
7
+ ABUTTING = "abutting"
8
+ FAULTED = "faulted"
9
+ NONE = "none"
10
+
11
+ class FaultTopology(Observable['FaultTopology']):
12
+ """A graph representation of the relationships between faults and the
13
+ relationship with stratigraphic units.
14
+ """
15
+ def __init__(self, stratigraphic_column: 'StratigraphicColumn'):
16
+ super().__init__()
17
+ self.faults = []
18
+ self.stratigraphic_column = stratigraphic_column
19
+ self.adjacency = {}
20
+ self.stratigraphy_fault_relationships = {}
21
+ def add_fault(self, fault: FaultSegment):
22
+ """
23
+ Adds a fault to the fault topology.
24
+ """
25
+ if not isinstance(fault, str):
26
+ raise TypeError("Expected a fault name.")
27
+
28
+ self.faults.append(fault)
29
+ self.notify('fault_added', fault=fault)
30
+
31
+ def remove_fault(self, fault: str):
32
+ """
33
+ Removes a fault from the fault topology.
34
+ """
35
+ if fault not in self.faults:
36
+ raise ValueError(f"Fault {fault} not found in the topology.")
37
+
38
+ self.faults.remove(fault)
39
+ # Remove any relationships involving this fault
40
+ self.adjacency = {k: v for k, v in self.adjacency.items() if fault not in k}
41
+ self.stratigraphy_fault_relationships = {
42
+ k: v for k, v in self.stratigraphy_fault_relationships.items() if k[1] != fault
43
+ }
44
+ self.notify('fault_removed', fault=fault)
45
+
46
+ def add_abutting_relationship(self, fault_name: str, abutting_fault: str):
47
+ """
48
+ Adds an abutting relationship between two faults.
49
+ """
50
+ if fault_name not in self.faults or abutting_fault not in self.faults:
51
+ raise ValueError("Both faults must be part of the fault topology.")
52
+
53
+ if fault_name not in self.adjacency:
54
+ self.adjacency[fault_name] = []
55
+
56
+ self.adjacency[(fault_name, abutting_fault)] = FaultRelationshipType.ABUTTING
57
+ self.notify('abutting_relationship_added', {'fault': fault_name, 'abutting_fault': abutting_fault})
58
+ def add_stratigraphy_fault_relationship(self, unit_name:str, fault_name: str):
59
+ """
60
+ Adds a relationship between a stratigraphic unit and a fault.
61
+ """
62
+ if fault_name not in self.faults:
63
+ raise ValueError("Fault must be part of the fault topology.")
64
+
65
+ if unit_name is None:
66
+ raise ValueError(f"No stratigraphic group found for unit name: {unit_name}")
67
+ self.stratigraphy_fault_relationships[(unit_name,fault_name)] = True
68
+
69
+ self.notify('stratigraphy_fault_relationship_added', {'unit': unit_name, 'fault': fault_name})
70
+ def add_faulted_relationship(self, fault_name: str, faulted_fault_name: str):
71
+ """
72
+ Adds a faulted relationship between two faults.
73
+ """
74
+ if fault_name not in self.faults or faulted_fault_name not in self.faults:
75
+ raise ValueError("Both faults must be part of the fault topology.")
76
+
77
+ if fault_name not in self.adjacency:
78
+ self.adjacency[fault_name] = []
79
+
80
+ self.adjacency[(fault_name, faulted_fault_name)] = FaultRelationshipType.FAULTED
81
+ self.notify('faulted_relationship_added', {'fault': fault_name, 'faulted_fault': faulted_fault_name})
82
+ def remove_fault_relationship(self, fault_name: str, related_fault_name: str):
83
+ """
84
+ Removes a relationship between two faults.
85
+ """
86
+ if (fault_name, related_fault_name) in self.adjacency:
87
+ del self.adjacency[(fault_name, related_fault_name)]
88
+ elif (related_fault_name, fault_name) in self.adjacency:
89
+ del self.adjacency[(related_fault_name, fault_name)]
90
+ else:
91
+ raise ValueError(f"No relationship found between {fault_name} and {related_fault_name}.")
92
+ self.notify('fault_relationship_removed', {'fault': fault_name, 'related_fault': related_fault_name})
93
+ def update_fault_relationship(self, fault_name: str, related_fault_name: str, new_relationship_type: FaultRelationshipType):
94
+ if new_relationship_type == FaultRelationshipType.NONE:
95
+ self.adjacency.pop((fault_name, related_fault_name), None)
96
+ else:
97
+ self.adjacency[(fault_name, related_fault_name)] = new_relationship_type
98
+ self.notify('fault_relationship_updated', {'fault': fault_name, 'related_fault': related_fault_name, 'new_relationship_type': new_relationship_type})
99
+ def change_relationship_type(self, fault_name: str, related_fault_name: str, new_relationship_type: FaultRelationshipType):
100
+ """
101
+ Changes the relationship type between two faults.
102
+ """
103
+ if (fault_name, related_fault_name) in self.adjacency:
104
+ self.adjacency[(fault_name, related_fault_name)] = new_relationship_type
105
+
106
+ else:
107
+ raise ValueError(f"No relationship found between {fault_name} and {related_fault_name}.")
108
+ self.notify('relationship_type_changed', {'fault': fault_name, 'related_fault': related_fault_name, 'new_relationship_type': new_relationship_type})
109
+ def get_fault_relationships(self, fault_name: str):
110
+ """
111
+ Returns a list of relationships for a given fault.
112
+ """
113
+ relationships = []
114
+ for (f1, f2), relationship_type in self.adjacency.items():
115
+ if f1 == fault_name or f2 == fault_name:
116
+ relationships.append((f1, f2, relationship_type))
117
+ return relationships
118
+ def get_fault_relationship(self, fault_name: str, related_fault_name: str):
119
+ """
120
+ Returns the relationship type between two faults.
121
+ """
122
+ return self.adjacency.get((fault_name, related_fault_name), FaultRelationshipType.NONE)
123
+ def get_faults(self):
124
+ """
125
+ Returns a list of all faults in the topology.
126
+ """
127
+ return self.faults
128
+
129
+ def get_stratigraphy_fault_relationships(self):
130
+ """
131
+ Returns a dictionary of stratigraphic unit to fault relationships.
132
+ """
133
+ return self.stratigraphy_fault_relationships
134
+ def get_fault_stratigraphic_unit_relationships(self):
135
+ units_group_pairs = self.stratigraphic_column.get_group_unit_pairs()
136
+ matrix = np.zeros((len(self.faults), len(units_group_pairs)), dtype=int)
137
+ for i, fault in enumerate(self.faults):
138
+ for j, (unit_name, _group) in enumerate(units_group_pairs):
139
+ if (unit_name, fault) in self.stratigraphy_fault_relationships:
140
+ matrix[i, j] = 1
141
+
142
+ return matrix
143
+ def get_fault_stratigraphic_relationship(self, unit_name: str, fault:str) -> bool:
144
+ """
145
+ Returns a dictionary of fault to stratigraphic unit relationships.
146
+ """
147
+ if unit_name is None:
148
+ raise ValueError(f"No stratigraphic group found for unit name: {unit_name}")
149
+ if (unit_name, fault) not in self.stratigraphy_fault_relationships:
150
+ return False
151
+ return self.stratigraphy_fault_relationships[(unit_name, fault)]
152
+
153
+ def update_fault_stratigraphy_relationship(self, unit_name: str, fault_name: str, flag: bool = True):
154
+ """
155
+ Updates the relationship between a stratigraphic unit and a fault.
156
+ """
157
+ if not flag:
158
+ if (unit_name, fault_name) in self.stratigraphy_fault_relationships:
159
+ del self.stratigraphy_fault_relationships[(unit_name, fault_name)]
160
+ else:
161
+ self.stratigraphy_fault_relationships[(unit_name, fault_name)] = flag
162
+
163
+ self.notify('stratigraphy_fault_relationship_updated', {'unit': unit_name, 'fault': fault_name})
164
+
165
+ def remove_fault_stratigraphy_relationship(self, unit_name: str, fault_name: str):
166
+ """
167
+ Removes a relationship between a stratigraphic unit and a fault.
168
+ """
169
+ if (unit_name, fault_name) not in self.stratigraphy_fault_relationships:
170
+ raise ValueError(f"No relationship found between unit {unit_name} and fault {fault_name}.")
171
+ else:
172
+ self.stratigraphy_fault_relationships.pop((unit_name, fault_name), None)
173
+
174
+ self.notify('stratigraphy_fault_relationship_removed', {'unit': unit_name, 'fault': fault_name})
175
+ def get_matrix(self):
176
+ """
177
+ Returns a matrix representation of the fault relationships.
178
+ """
179
+ matrix = np.zeros((len(self.faults), len(self.faults)), dtype=int)
180
+ for (fault_name, related_fault_name), relationship_type in self.adjacency.items():
181
+ fault_index = self.faults.index(next(f for f in self.faults if f == fault_name))
182
+ related_fault_index = self.faults.index(next(f for f in self.faults if f == related_fault_name))
183
+ if relationship_type == FaultRelationshipType.ABUTTING:
184
+ matrix[fault_index, related_fault_index] = 1
185
+ elif relationship_type == FaultRelationshipType.FAULTED:
186
+ matrix[fault_index, related_fault_index] = 2
187
+ return matrix
188
+
189
+ def to_dict(self):
190
+ """
191
+ Returns a dictionary representation of the fault topology.
192
+ """
193
+ return {
194
+ "faults": self.faults,
195
+ "adjacency": self.adjacency,
196
+ "stratigraphy_fault_relationships": self.stratigraphy_fault_relationships,
197
+ }
198
+
199
+ def update_from_dict(self, data):
200
+ """
201
+ Updates the fault topology from a dictionary representation.
202
+ """
203
+ with self.freeze_notifications():
204
+ self.faults.extend(data.get("faults", []))
205
+ adjacency = data.get("adjacency", {})
206
+ stratigraphy_fault_relationships = data.get("stratigraphy_fault_relationships", {})
207
+ for (fault,abutting_fault) in adjacency.values():
208
+ if fault not in self.faults:
209
+ self.add_fault(fault)
210
+ if abutting_fault not in self.faults:
211
+ self.add_fault(abutting_fault)
212
+ self.add_abutting_relationship(fault, abutting_fault)
213
+ for unit_name, fault_names in stratigraphy_fault_relationships.items():
214
+ for fault_name in fault_names:
215
+ if fault_name not in self.faults:
216
+ self.add_fault(fault_name)
217
+ self.add_stratigraphy_fault_relationship(unit_name, fault_name)
218
+
219
+ @classmethod
220
+ def from_dict(cls, data):
221
+ """
222
+ Creates a FaultTopology instance from a dictionary representation.
223
+ """
224
+ from .stratigraphic_column import StratigraphicColumn
225
+ stratigraphic_column = data.get("stratigraphic_column",None)
226
+ if not isinstance(stratigraphic_column, StratigraphicColumn):
227
+ if isinstance(stratigraphic_column, dict):
228
+ stratigraphic_column = StratigraphicColumn.from_dict(stratigraphic_column)
229
+ elif not isinstance(stratigraphic_column, StratigraphicColumn):
230
+ raise TypeError("Expected 'stratigraphic_column' to be a StratigraphicColumn instance or dict.")
231
+
232
+ topology = cls(stratigraphic_column)
233
+ topology.update_from_dict(data)
234
+ return topology
@@ -6,7 +6,7 @@ from ...utils import getLogger
6
6
 
7
7
  import numpy as np
8
8
  import pandas as pd
9
- from typing import List, Optional
9
+ from typing import List, Optional, Union, Dict
10
10
  import pathlib
11
11
  from ...modelling.features.fault import FaultSegment
12
12
 
@@ -123,8 +123,7 @@ class GeologicalModel:
123
123
  self.feature_name_index = {}
124
124
  self._data = pd.DataFrame() # None
125
125
 
126
- self.stratigraphic_column = StratigraphicColumn()
127
-
126
+ self._stratigraphic_column = StratigraphicColumn()
128
127
 
129
128
  self.tol = 1e-10 * np.max(self.bounding_box.maximum - self.bounding_box.origin)
130
129
  self._dtm = None
@@ -187,7 +186,6 @@ class GeologicalModel:
187
186
  ].astype(float)
188
187
  return data
189
188
 
190
-
191
189
  if "type" in data:
192
190
  logger.warning("'type' is deprecated replace with 'feature_name' \n")
193
191
  data.rename(columns={"type": "feature_name"}, inplace=True)
@@ -409,7 +407,6 @@ class GeologicalModel:
409
407
  """
410
408
  return [f.name for f in self.faults]
411
409
 
412
-
413
410
  def to_file(self, file):
414
411
  """Save a model to a pickle file requires dill
415
412
 
@@ -506,10 +503,34 @@ class GeologicalModel:
506
503
  self._data = data.copy()
507
504
  # self._data[['X','Y','Z']] = self.bounding_box.project(self._data[['X','Y','Z']].to_numpy())
508
505
 
509
-
510
506
  def set_model_data(self, data):
511
507
  logger.warning("deprecated method. Model data can now be set using the data attribute")
512
508
  self.data = data.copy()
509
+ @property
510
+ def stratigraphic_column(self):
511
+ """Get the stratigraphic column of the model
512
+
513
+ Returns
514
+ -------
515
+ StratigraphicColumn
516
+ the stratigraphic column of the model
517
+ """
518
+ return self._stratigraphic_column
519
+ @stratigraphic_column.setter
520
+ def stratigraphic_column(self, stratigraphic_column: Union[StratigraphicColumn,Dict]):
521
+ """Set the stratigraphic column of the model
522
+
523
+ Parameters
524
+ ----------
525
+ stratigraphic_column : StratigraphicColumn
526
+ the stratigraphic column to set
527
+ """
528
+ if isinstance(stratigraphic_column, dict):
529
+ self.set_stratigraphic_column(stratigraphic_column)
530
+ return
531
+ elif not isinstance(stratigraphic_column, StratigraphicColumn):
532
+ raise ValueError("stratigraphic_column must be a StratigraphicColumn object")
533
+ self._stratigraphic_column = stratigraphic_column
513
534
 
514
535
  def set_stratigraphic_column(self, stratigraphic_column, cmap="tab20"):
515
536
  """
@@ -1400,7 +1421,6 @@ class GeologicalModel:
1400
1421
 
1401
1422
  return self.bounding_box.reproject(points, inplace=inplace)
1402
1423
 
1403
-
1404
1424
  # TODO move scale to bounding box/transformer
1405
1425
  def scale(self, points: np.ndarray, *, inplace: bool = False) -> np.ndarray:
1406
1426
  """Take points in UTM coordinates and reproject
@@ -1419,7 +1439,6 @@ class GeologicalModel:
1419
1439
  """
1420
1440
  return self.bounding_box.project(np.array(points).astype(float), inplace=inplace)
1421
1441
 
1422
-
1423
1442
  def regular_grid(self, *, nsteps=None, shuffle=True, rescale=False, order="C"):
1424
1443
  """
1425
1444
  Return a regular grid within the model bounding box
@@ -1494,22 +1513,18 @@ class GeologicalModel:
1494
1513
  if self.stratigraphic_column is None:
1495
1514
  logger.warning("No stratigraphic column defined")
1496
1515
  return strat_id
1497
- for group in reversed(self.stratigraphic_column.keys()):
1498
- if group == "faults":
1499
- continue
1500
- feature_id = self.feature_name_index.get(group, -1)
1516
+
1517
+ s_id = 0
1518
+ for g in reversed(self.stratigraphic_column.get_groups()):
1519
+ feature_id = self.feature_name_index.get(g.name, -1)
1501
1520
  if feature_id >= 0:
1502
- feature = self.features[feature_id]
1503
- vals = feature.evaluate_value(xyz)
1504
- for series in self.stratigraphic_column[group].values():
1505
- strat_id[
1506
- np.logical_and(
1507
- vals < series.get("max", feature.max()),
1508
- vals > series.get("min", feature.min()),
1509
- )
1510
- ] = series["id"]
1521
+ vals = self.features[feature_id].evaluate_value(xyz)
1522
+ for u in g.units:
1523
+ strat_id[np.logical_and(vals < u.max, vals > u.min)] = s_id
1524
+ s_id += 1
1511
1525
  if feature_id == -1:
1512
- logger.error(f"Model does not contain {group}")
1526
+ logger.error(f"Model does not contain {g.name}")
1527
+
1513
1528
  return strat_id
1514
1529
 
1515
1530
  def evaluate_model_gradient(self, points: np.ndarray, *, scale: bool = True) -> np.ndarray:
@@ -1,8 +1,7 @@
1
1
  import enum
2
- from typing import Dict
2
+ from typing import Dict, Optional, List, Tuple
3
3
  import numpy as np
4
- from LoopStructural.utils import rng, getLogger
5
-
4
+ from LoopStructural.utils import rng, getLogger, Observable
6
5
  logger = getLogger(__name__)
7
6
  logger.info("Imported LoopStructural Stratigraphic Column module")
8
7
  class UnconformityType(enum.Enum):
@@ -154,7 +153,7 @@ class StratigraphicGroup:
154
153
  self.units = units if units is not None else []
155
154
 
156
155
 
157
- class StratigraphicColumn:
156
+ class StratigraphicColumn(Observable['StratigraphicColumn']):
158
157
  """
159
158
  A class to represent a stratigraphic column, which is a vertical section of the Earth's crust
160
159
  showing the sequence of rock layers and their relationships.
@@ -164,6 +163,7 @@ class StratigraphicColumn:
164
163
  """
165
164
  Initializes the StratigraphicColumn with a name and a list of layers.
166
165
  """
166
+ super().__init__()
167
167
  self.order = [StratigraphicUnit(name='Basement', colour='grey', thickness=np.inf),StratigraphicUnconformity(name='Base Unconformity', unconformity_type=UnconformityType.ERODE)]
168
168
  self.group_mapping = {}
169
169
  def clear(self,basement=True):
@@ -175,7 +175,7 @@ class StratigraphicColumn:
175
175
  else:
176
176
  self.order = []
177
177
  self.group_mapping = {}
178
-
178
+ self.notify('column_cleared')
179
179
  def add_unit(self, name,*, colour=None, thickness=None, where='top'):
180
180
  unit = StratigraphicUnit(name=name, colour=colour, thickness=thickness)
181
181
 
@@ -185,7 +185,7 @@ class StratigraphicColumn:
185
185
  self.order.insert(0, unit)
186
186
  else:
187
187
  raise ValueError("Invalid 'where' argument. Use 'top' or 'bottom'.")
188
-
188
+ self.notify('unit_added', unit=unit)
189
189
  return unit
190
190
 
191
191
  def remove_unit(self, uuid):
@@ -195,7 +195,9 @@ class StratigraphicColumn:
195
195
  for i, element in enumerate(self.order):
196
196
  if element.uuid == uuid:
197
197
  del self.order[i]
198
+ self.notify('unit_removed', uuid=uuid)
198
199
  return True
200
+
199
201
  return False
200
202
 
201
203
  def add_unconformity(self, name, *, unconformity_type=UnconformityType.ERODE, where='top' ):
@@ -209,6 +211,7 @@ class StratigraphicColumn:
209
211
  self.order.insert(0, unconformity)
210
212
  else:
211
213
  raise ValueError("Invalid 'where' argument. Use 'top' or 'bottom'.")
214
+ self.notify('unconformity_added', unconformity=unconformity)
212
215
  return unconformity
213
216
 
214
217
  def get_element_by_index(self, index):
@@ -228,6 +231,7 @@ class StratigraphicColumn:
228
231
  return unit
229
232
 
230
233
  return None
234
+
231
235
  def get_unconformity_by_name(self, name):
232
236
  """
233
237
  Retrieves an unconformity by its name from the stratigraphic column.
@@ -245,6 +249,15 @@ class StratigraphicColumn:
245
249
  if element.uuid == uuid:
246
250
  return element
247
251
  raise KeyError(f"No element found with uuid: {uuid}")
252
+
253
+ def get_group_for_unit_name(self, unit_name:str) -> Optional[StratigraphicGroup]:
254
+ """
255
+ Retrieves the group for a given unit name.
256
+ """
257
+ for group in self.get_groups():
258
+ if any(unit.name == unit_name for unit in group.units):
259
+ return group
260
+ return None
248
261
  def add_element(self, element):
249
262
  """
250
263
  Adds a StratigraphicColumnElement to the stratigraphic column.
@@ -296,7 +309,18 @@ class StratigraphicColumn:
296
309
  group = [u.name for u in g.units if isinstance(u, StratigraphicUnit)]
297
310
  groups_list.append(group)
298
311
  return groups_list
299
-
312
+
313
+ def get_group_unit_pairs(self) -> List[Tuple[str,str]]:
314
+ """
315
+ Returns a list of tuples containing group names and unit names.
316
+ """
317
+ groups = self.get_groups()
318
+ group_unit_pairs = []
319
+ for g in groups:
320
+ for u in g.units:
321
+ if isinstance(u, StratigraphicUnit):
322
+ group_unit_pairs.append((g.name, u.name))
323
+ return group_unit_pairs
300
324
 
301
325
  def __getitem__(self, uuid):
302
326
  """
@@ -316,6 +340,7 @@ class StratigraphicColumn:
316
340
  self.order = [
317
341
  self.__getitem__(uuid) for uuid in new_order if self.__getitem__(uuid) is not None
318
342
  ]
343
+ self.notify('order_updated', new_order=self.order)
319
344
 
320
345
  def update_element(self, unit_data: Dict):
321
346
  """
@@ -334,6 +359,7 @@ class StratigraphicColumn:
334
359
  element.unconformity_type = UnconformityType(
335
360
  unit_data.get('unconformity_type', element.unconformity_type.value)
336
361
  )
362
+ self.notify('element_updated', element=element)
337
363
 
338
364
  def __str__(self):
339
365
  """
@@ -354,14 +380,15 @@ class StratigraphicColumn:
354
380
  """
355
381
  if not isinstance(data, dict):
356
382
  raise TypeError("Data must be a dictionary")
357
- self.clear(basement=False)
358
- elements_data = data.get("elements", [])
359
- for element_data in elements_data:
360
- if "unconformity_type" in element_data:
361
- element = StratigraphicUnconformity.from_dict(element_data)
362
- else:
363
- element = StratigraphicUnit.from_dict(element_data)
364
- self.add_element(element)
383
+ with self.freeze_notifications():
384
+ self.clear(basement=False)
385
+ elements_data = data.get("elements", [])
386
+ for element_data in elements_data:
387
+ if "unconformity_type" in element_data:
388
+ element = StratigraphicUnconformity.from_dict(element_data)
389
+ else:
390
+ element = StratigraphicUnit.from_dict(element_data)
391
+ self.add_element(element)
365
392
  @classmethod
366
393
  def from_dict(cls, data):
367
394
  """
@@ -38,3 +38,4 @@ rng = np.random.default_rng()
38
38
 
39
39
  from ._surface import LoopIsosurfacer, surface_list
40
40
  from .colours import random_colour, random_hex_colour
41
+ from .observer import Callback, Disposable, Observable
@@ -115,12 +115,17 @@ class LoopIsosurfacer:
115
115
  values,
116
116
  )
117
117
  logger.info(f'Isosurfacing at values: {isovalues}')
118
+ individual_names = False
118
119
  if name is None:
119
120
  names = ["surface"] * len(isovalues)
120
121
  if isinstance(name, str):
121
122
  names = [name] * len(isovalues)
123
+ if len(isovalues) == 1:
124
+ individual_names = True
122
125
  if isinstance(name, list):
123
126
  names = name
127
+ if len(names) == len(isovalues):
128
+ individual_names = True
124
129
  if colours is None:
125
130
  colours = [None] * len(isovalues)
126
131
  for name, isovalue, colour in zip(names, isovalues, colours):
@@ -151,7 +156,7 @@ class LoopIsosurfacer:
151
156
  vertices=verts,
152
157
  triangles=faces,
153
158
  normals=normals,
154
- name=f"{name}_{isovalue}",
159
+ name=name if individual_names else f"{name}_{isovalue}",
155
160
  values=values,
156
161
  colour=colour,
157
162
  )