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
@@ -165,8 +165,19 @@ class ProcessInputData:
165
165
  unit_id = 1
166
166
  val = self._stratigraphic_value()
167
167
  for name, sg in self._stratigraphic_order:
168
+ # set the oldest unit to be the basement.
169
+ # this has no observed basal contact and then
170
+ # top of the unit is the 0 isovalue.
171
+ # this is the minimum of the next unit
168
172
  stratigraphic_column[name] = {}
169
- for i, g in enumerate(reversed(sg)):
173
+ stratigraphic_column[name][sg[-1]] = {
174
+ "max": 0,
175
+ "min": -np.inf,
176
+ "id": unit_id,
177
+ "colour": self.colours[sg[-1]],
178
+ }
179
+ # iterate through the remaining units (in reverse)
180
+ for g in reversed(sg[:-1]):
170
181
  if g in self.thicknesses:
171
182
  stratigraphic_column[name][g] = {
172
183
  "max": val[g] + self.thicknesses[g],
@@ -174,10 +185,6 @@ class ProcessInputData:
174
185
  "id": unit_id,
175
186
  "colour": self.colours[g],
176
187
  }
177
- if i == 0:
178
- stratigraphic_column[name][g]["min"] = 0
179
- if i == len(sg) - 1:
180
- stratigraphic_column[name][g]["max"] = np.inf
181
188
 
182
189
  unit_id += 1
183
190
  # add faults into the column
@@ -438,9 +445,22 @@ class ProcessInputData:
438
445
  stratigraphic_value = {}
439
446
  for _name, sg in self.stratigraphic_order:
440
447
  value = 0.0 # reset for each supergroup
441
- for g in reversed(sg):
448
+ if sg[0] not in self.thicknesses or self.thicknesses[sg[0]] <= 0:
449
+ self.thicknesses[sg[0]] = (
450
+ np.inf
451
+ ) # make the top unit infinite as it should extend to the top of the model
452
+ for g in reversed(
453
+ sg[:-1]
454
+ ): # don't add the last unit as we never see the base of this unit.
455
+ # It should be "basement"
442
456
  if g not in self.thicknesses:
443
457
  logger.warning(f"No thicknesses for {g}")
458
+ stratigraphic_value[g] = np.nan
459
+ if self.thicknesses[g] <= 0:
460
+ logger.error(
461
+ f"Thickness for {g} is less than or equal to 0\n Update the thickness value for {g} before continuing"
462
+ )
463
+
444
464
  stratigraphic_value[g] = np.nan
445
465
  else:
446
466
  stratigraphic_value[g] = value
@@ -463,30 +483,13 @@ class ProcessInputData:
463
483
 
464
484
  @property
465
485
  def contacts(self):
466
- return self._contacts
467
-
468
- @contacts.setter
469
- def contacts(self, contacts):
470
- """Function to convert input contact to loopstructural input
471
-
472
- either uses the thickness values or assigns unique ids given
473
- the units named in stratigraphic order
474
-
475
- Returns
476
- -------
477
- DataFrame
478
- data frame with x,y,y,val/interface,feature_name
479
- """
480
- if contacts is None:
481
- return
482
- contacts = contacts.copy()
483
- self._update_feature_names(contacts)
486
+ contacts = self._contacts.copy()
484
487
  if self._use_thickness:
485
488
  contacts["val"] = np.nan
486
489
  for k, v in self._stratigraphic_value().items():
487
490
  contacts.loc[contacts["name"] == k, "val"] = v
488
491
 
489
- self._contacts = contacts.loc[
492
+ contacts = contacts.loc[
490
493
  ~np.isnan(contacts["val"]), ["X", "Y", "Z", "feature_name", "val"]
491
494
  ]
492
495
  if not self._use_thickness:
@@ -494,10 +497,28 @@ class ProcessInputData:
494
497
  interface_val = 0
495
498
  for k in self._stratigraphic_value().keys():
496
499
  contacts.loc[contacts["name"] == k, "interface"] = interface_val
497
- self._contacts = contacts.loc[
500
+ contacts = contacts.loc[
498
501
  ~np.isnan(contacts["interface"]),
499
502
  ["X", "Y", "Z", "feature_name", "interface"],
500
503
  ]
504
+ return contacts
505
+
506
+ @contacts.setter
507
+ def contacts(self, contacts):
508
+ """Function to convert input contact to loopstructural input
509
+
510
+ either uses the thickness values or assigns unique ids given
511
+ the units named in stratigraphic order
512
+
513
+ Returns
514
+ -------
515
+ DataFrame
516
+ data frame with x,y,y,val/interface,feature_name
517
+ """
518
+ if contacts is None:
519
+ return
520
+ self._contacts = contacts.copy()
521
+ self._update_feature_names(self._contacts)
501
522
 
502
523
  @property
503
524
  def contact_orientations(self):
@@ -22,7 +22,8 @@ class LoopProjectfileProcessor(ProcessInputData):
22
22
  orientations = self.projectfile.stratigraphyOrientations
23
23
  fault_orientations = self.projectfile.faultOrientations
24
24
  fault_locations = self.projectfile.faultLocations
25
-
25
+ fault_relationships = self.projectfile["eventRelationships"]
26
+ faultLog = self.projectfile.faultLog.set_index("eventId")
26
27
  orientations.rename(columns=column_map, inplace=True)
27
28
  contacts.rename(columns=column_map, inplace=True)
28
29
  fault_locations.rename(columns=column_map, inplace=True)
@@ -33,24 +34,48 @@ class LoopProjectfileProcessor(ProcessInputData):
33
34
  projectfile["stratigraphicLog"].ThicknessMedian,
34
35
  )
35
36
  )
36
- fault_properties = self.projectfile.faultLog
37
- fault_properties.rename(
38
- columns={
39
- "avgDisplacement": "displacement",
40
- "influenceDistance": "minor_axis",
41
- "verticalRadius": "intermediate_axis",
42
- "horizontalRadius": "major_axis",
43
- "name": "fault_name",
44
- },
45
- inplace=True,
46
- )
47
- fault_locations = fault_properties.reset_index()[["fault_name", "eventId"]].merge(
48
- fault_locations, on="eventId"
49
- )
50
- fault_orientations = fault_properties.reset_index()[["fault_name", "eventId"]].merge(
51
- fault_orientations, on="eventId"
52
- )
53
- fault_properties.set_index("fault_name", inplace=True)
37
+ fault_properties = None
38
+ fault_edges = None
39
+ fault_edge_properties = None
40
+ if self.projectfile.faultLog.shape[0] > 0:
41
+
42
+ fault_properties = self.projectfile.faultLog
43
+ fault_properties.rename(
44
+ columns={
45
+ "avgDisplacement": "displacement",
46
+ "influenceDistance": "minor_axis",
47
+ "verticalRadius": "intermediate_axis",
48
+ "horizontalRadius": "major_axis",
49
+ "name": "fault_name",
50
+ },
51
+ inplace=True,
52
+ )
53
+ fault_locations = fault_properties.reset_index()[["fault_name", "eventId"]].merge(
54
+ fault_locations, on="eventId"
55
+ )
56
+ fault_orientations = fault_properties.reset_index()[["fault_name", "eventId"]].merge(
57
+ fault_orientations, on="eventId"
58
+ )
59
+ fault_properties.set_index("fault_name", inplace=True)
60
+ for i in fault_relationships.index:
61
+ fault_relationships.loc[i, "Fault1"] = faultLog.loc[
62
+ fault_relationships.loc[i, "eventId1"], "name"
63
+ ]
64
+ fault_relationships.loc[i, "Fault2"] = faultLog.loc[
65
+ fault_relationships.loc[i, "eventId2"], "name"
66
+ ]
67
+ fault_edges = []
68
+ fault_edge_properties = []
69
+ for i in fault_relationships.index:
70
+ fault_edges.append(
71
+ (fault_relationships.loc[i, "Fault1"], fault_relationships.loc[i, "Fault2"])
72
+ )
73
+ fault_edge_properties.append(
74
+ {
75
+ "type": fault_relationships.loc[i, "type"],
76
+ "angle": fault_relationships.loc[i, "angle"],
77
+ }
78
+ )
54
79
  colours = dict(
55
80
  zip(
56
81
  self.projectfile.stratigraphicLog.name,
@@ -63,6 +88,7 @@ class LoopProjectfileProcessor(ProcessInputData):
63
88
  ],
64
89
  )
65
90
  )
91
+
66
92
  super().__init__(
67
93
  contacts=contacts,
68
94
  contact_orientations=orientations,
@@ -70,15 +96,15 @@ class LoopProjectfileProcessor(ProcessInputData):
70
96
  ("sg", list(self.projectfile.stratigraphicLog.name))
71
97
  ], # needs to be updated,
72
98
  thicknesses=thicknesses,
73
- fault_orientations=fault_orientations,
74
- fault_locations=fault_locations,
99
+ fault_orientations=fault_orientations if fault_orientations.shape[0] > 0 else None,
100
+ fault_locations=fault_locations if fault_locations.shape[0] > 0 else None,
75
101
  fault_properties=fault_properties,
76
- fault_edges=[], # list(fault_graph.edges),
102
+ fault_edges=fault_edges, # list(fault_graph.edges),
77
103
  colours=colours,
78
104
  fault_stratigraphy=None,
79
105
  intrusions=None,
80
106
  use_thickness=use_thickness,
81
107
  origin=self.projectfile.origin,
82
108
  maximum=self.projectfile.maximum,
83
- # fault_edge_properties=fault_edge_properties
109
+ fault_edge_properties=fault_edge_properties,
84
110
  )
@@ -408,3 +408,6 @@ class IntrusionFeature(BaseFeature):
408
408
 
409
409
  def get_data(self, value_map: Optional[dict] = None):
410
410
  pass
411
+
412
+ def copy(self):
413
+ pass
@@ -36,3 +36,4 @@ import numpy as np
36
36
  rng = np.random.default_rng()
37
37
 
38
38
  from ._surface import LoopIsosurfacer, surface_list
39
+ from .colours import random_colour, random_hex_colour
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import Optional, Union, Callable, List
4
+ from collections.abc import Iterable
4
5
  import numpy as np
5
6
  import numpy.typing as npt
6
7
  from LoopStructural.utils.logging import getLogger
@@ -15,7 +16,7 @@ except ImportError:
15
16
  # from LoopStructural.interpolators._geological_interpolator import GeologicalInterpolator
16
17
  from LoopStructural.datatypes import Surface, BoundingBox
17
18
 
18
- surface_list = dict[str, Surface]
19
+ surface_list = List[Surface]
19
20
 
20
21
 
21
22
  class LoopIsosurfacer:
@@ -62,6 +63,7 @@ class LoopIsosurfacer:
62
63
  self,
63
64
  values: Optional[Union[list, int, float]],
64
65
  name: Optional[Union[List[str], str]] = None,
66
+ local=False
65
67
  ) -> surface_list:
66
68
  """Extract isosurfaces from the interpolator
67
69
 
@@ -72,6 +74,10 @@ class LoopIsosurfacer:
72
74
  to extract a single isosurface for, or an integer to extract that many
73
75
  isosurfaces evenly spaced between the minimum and maximum values of the
74
76
  interpolator.
77
+ name : Optional[Union[List[str], str]], optional
78
+ name of the isosurface, by default None
79
+ local : bool, optional
80
+ whether to use the local regular grid for the bounding box or global
75
81
 
76
82
  Returns
77
83
  -------
@@ -83,22 +89,28 @@ class LoopIsosurfacer:
83
89
  raise ValueError("No interpolator of callable function set")
84
90
 
85
91
  surfaces = []
86
- all_values = self.callable(self.bounding_box.regular_grid(local=False))
92
+ all_values = self.callable(self.bounding_box.regular_grid(local=local))
87
93
  ## set value to mean value if its not specified
88
94
  if values is None:
89
- values = [(np.nanmax(all_values) - np.nanmin(all_values)) / 2]
90
- if isinstance(values, list):
95
+ values = [((np.nanmax(all_values) - np.nanmin(all_values)) / 2) + np.nanmin(all_values)]
96
+ if isinstance(values, Iterable):
91
97
  isovalues = values
92
98
  elif isinstance(values, float):
93
99
  isovalues = [values]
100
+ if isinstance(values, int) and values == 0:
101
+ values = 0.0 # assume 0 isosurface is meant to be a float
102
+
94
103
  elif isinstance(values, int) and values < 1:
95
104
  raise ValueError(
96
105
  "Number of isosurfaces must be greater than 1. Either use a positive integer or provide a list or float for a specific isovalue."
97
106
  )
98
107
  elif isinstance(values, int):
108
+ var = np.nanmax(all_values) - np.nanmin(all_values)
109
+ # buffer slices by 5% to make sure that we don't get isosurface does't exist issues
110
+ buffer = var * 0.05
99
111
  isovalues = np.linspace(
100
- np.nanmin(all_values) + np.finfo(float).eps,
101
- np.nanmax(all_values) - np.finfo(float).eps,
112
+ np.nanmin(all_values) + buffer,
113
+ np.nanmax(all_values) - buffer,
102
114
  values,
103
115
  )
104
116
  logger.info(f'Isosurfacing at values: {isovalues}')
@@ -1,9 +1,13 @@
1
1
  import numpy as np
2
- from sklearn import decomposition
2
+ from . import getLogger
3
+
4
+ logger = getLogger(__name__)
3
5
 
4
6
 
5
7
  class EuclideanTransformation:
6
- def __init__(self, dimensions=2):
8
+ def __init__(
9
+ self, dimensions: int = 2, angle: float = 0, translation: np.ndarray = np.zeros(3)
10
+ ):
7
11
  """Transforms points into a new coordinate
8
12
  system where the main eigenvector is aligned with x
9
13
 
@@ -11,11 +15,14 @@ class EuclideanTransformation:
11
15
  ----------
12
16
  dimensions : int, optional
13
17
  Do transformation in map view or on 3d volume, by default 2
18
+ angle : float, optional
19
+ Angle to rotate the points by, by default 0
20
+ translation : np.ndarray, default zeros
21
+ Translation to apply to the points, by default
14
22
  """
15
- self.rotation = None
16
- self.translation = None
23
+ self.translation = translation[:dimensions]
17
24
  self.dimensions = dimensions
18
- self.angle = 0
25
+ self.angle = angle
19
26
 
20
27
  def fit(self, points: np.ndarray):
21
28
  """Fit the transformation to a point cloud
@@ -28,10 +35,16 @@ class EuclideanTransformation:
28
35
  points : np.ndarray
29
36
  xyz points as as numpy array
30
37
  """
38
+ try:
39
+ from sklearn import decomposition
40
+ except ImportError:
41
+ logger.error('scikit-learn is required for this function')
42
+ return
31
43
  points = np.array(points)
32
44
  if points.shape[1] < self.dimensions:
33
45
  raise ValueError("Points must have at least {} dimensions".format(self.dimensions))
34
46
  # standardise the points so that centre is 0
47
+ # self.translation = np.zeros(3)
35
48
  self.translation = np.mean(points, axis=0)
36
49
  # find main eigenvector and and calculate the angle of this with x
37
50
  pca = decomposition.PCA(n_components=self.dimensions).fit(
@@ -39,38 +52,109 @@ class EuclideanTransformation:
39
52
  )
40
53
  coeffs = pca.components_
41
54
  self.angle = -np.arccos(np.dot(coeffs[0, :], [1, 0]))
42
- self.rotation = self._rotation(self.angle)
55
+ return self
56
+
57
+ @property
58
+ def rotation(self):
59
+ return self._rotation(self.angle)
60
+
61
+ @property
62
+ def inverse_rotation(self):
63
+ return self._rotation(-self.angle)
43
64
 
44
65
  def _rotation(self, angle):
45
66
  return np.array(
46
67
  [
47
68
  [np.cos(angle), -np.sin(angle), 0],
48
69
  [np.sin(angle), np.cos(angle), 0],
49
- [0, 0, 1],
70
+ [0, 0, -1],
50
71
  ]
51
72
  )
52
73
 
53
74
  def fit_transform(self, points: np.ndarray) -> np.ndarray:
75
+ """Fit the transformation and transform the points"""
76
+
54
77
  self.fit(points)
55
78
  return self.transform(points)
56
79
 
57
80
  def transform(self, points: np.ndarray) -> np.ndarray:
58
- """_summary_
81
+ """Transform points using the transformation and rotation
59
82
 
60
83
  Parameters
61
84
  ----------
62
- points : _type_
63
- _description_
85
+ points : np.ndarray
86
+ xyz points as as numpy array
64
87
 
65
88
  Returns
66
89
  -------
67
- _type_
68
- _description_
90
+ np.ndarray
91
+ xyz points in the transformed coordinate system
69
92
  """
70
- return np.dot(points - self.translation, self.rotation)
93
+ points = np.array(points)
94
+ if points.shape[1] < self.dimensions:
95
+ raise ValueError("Points must have at least {} dimensions".format(self.dimensions))
96
+ centred = points - self.translation
97
+
98
+ return np.einsum(
99
+ 'ik,jk->ij',
100
+ centred,
101
+ self.rotation[: self.dimensions, : self.dimensions],
102
+ )
71
103
 
72
104
  def inverse_transform(self, points: np.ndarray) -> np.ndarray:
73
- return np.dot(points, self._rotation(-self.angle)) + self.translation
105
+ """
106
+ Transform points back to the original coordinate system
107
+
108
+ Parameters
109
+ ----------
110
+ points : np.ndarray
111
+ xyz points as as numpy array
112
+
113
+ Returns
114
+ -------
115
+ np.ndarray
116
+ xyz points in the original coordinate system
117
+ """
118
+
119
+ return (
120
+ np.einsum(
121
+ 'ik,jk->ij',
122
+ points,
123
+ self.inverse_rotation[: self.dimensions, : self.dimensions],
124
+ )
125
+ + self.translation
126
+ )
74
127
 
75
128
  def __call__(self, points: np.ndarray) -> np.ndarray:
129
+ """
130
+ Transform points into the transformed space
131
+
132
+ Parameters
133
+ ----------
134
+ points : np.ndarray
135
+ xyz points as as numpy array
136
+
137
+ Returns
138
+ -------
139
+ np.ndarray
140
+ xyz points in the transformed coordinate system
141
+ """
142
+
76
143
  return self.transform(points)
144
+
145
+ def _repr_html_(self):
146
+ """
147
+ Provides an HTML representation of the TransRotator.
148
+ """
149
+ html_str = """
150
+ <div class="collapsible">
151
+ <button class="collapsible-button">{self.__class__.__name__}</button>
152
+ <div class="content">
153
+ <p>Translation: {self.translation}</p>
154
+ <p>Rotation Angle: {self.angle} degrees</p>
155
+ </div>
156
+ </div>
157
+ """.format(
158
+ self=self
159
+ )
160
+ return html_str
@@ -0,0 +1,50 @@
1
+ from LoopStructural.utils import rng
2
+
3
+
4
+ def random_colour(n: int = 1, cmap='tab20'):
5
+ """
6
+ Generate a list of random colours
7
+
8
+ Parameters
9
+ ----------
10
+ n : int
11
+ Number of colours to generate
12
+ cmap : str, optional
13
+ Name of the matplotlib colour map to use, by default 'tab20'
14
+
15
+ Returns
16
+ -------
17
+ list
18
+ List of colours in the form of (r,g,b,a) tuples
19
+ """
20
+ import matplotlib.cm as cm
21
+
22
+ colours = []
23
+ for _i in range(n):
24
+ colours.append(cm.get_cmap(cmap)(rng.random()))
25
+
26
+ return colours
27
+
28
+ def random_hex_colour(n: int = 1, cmap='tab20'):
29
+ """
30
+ Generate a list of random colours
31
+
32
+ Parameters
33
+ ----------
34
+ n : int
35
+ Number of colours to generate
36
+ cmap : str, optional
37
+ Name of the matplotlib colour map to use, by default 'tab20'
38
+
39
+ Returns
40
+ -------
41
+ list
42
+ List of colours in the form of hex strings
43
+ """
44
+ import matplotlib.cm as cm
45
+
46
+ colours = []
47
+ for _i in range(n):
48
+ colours.append(cm.get_cmap(cmap)(rng.random()))
49
+
50
+ return [f'#{int(c[0]*255):02x}{int(c[1]*255):02x}{int(c[2]*255):02x}' for c in colours]
@@ -0,0 +1,5 @@
1
+ from ..modelling.features import LambdaGeologicalFeature
2
+
3
+ X = LambdaGeologicalFeature(lambda pos: pos[:, 0], name="x")
4
+ Y = LambdaGeologicalFeature(lambda pos: pos[:, 1], name="y")
5
+ Z = LambdaGeologicalFeature(lambda pos: pos[:, 2], name="z")
@@ -1,6 +1,7 @@
1
1
  from LoopStructural.utils.typing import NumericInput
2
2
  import numpy as np
3
3
  import numbers
4
+ from typing import Tuple
4
5
 
5
6
 
6
7
  def strikedip2vector(strike: NumericInput, dip: NumericInput) -> np.ndarray:
@@ -177,7 +178,7 @@ def rotate(vector: NumericInput, axis: NumericInput, angle: NumericInput) -> np.
177
178
  # return vector
178
179
 
179
180
 
180
- def get_vectors(normal: NumericInput) -> tuple[np.ndarray, np.ndarray]:
181
+ def get_vectors(normal: NumericInput) -> Tuple[np.ndarray, np.ndarray]:
181
182
  """Find strike and dip vectors for a normal vector.
182
183
  Makes assumption the strike vector is horizontal component and the dip is vertical.
183
184
  Found by calculating strike and and dip angle and then finding the appropriate vectors
@@ -243,3 +244,54 @@ def get_dip_vector(strike, dip):
243
244
  ]
244
245
  )
245
246
  return v
247
+
248
+
249
+ def regular_tetraherdron_for_points(xyz, scale_parameter):
250
+ regular_tetrahedron = np.array(
251
+ [
252
+ [np.sqrt(8 / 9), 0, -1 / 3],
253
+ [-np.sqrt(2 / 9), np.sqrt(2 / 3), -1 / 3],
254
+ [-np.sqrt(2 / 9), -np.sqrt(2 / 3), -1 / 3],
255
+ [0, 0, 1],
256
+ ]
257
+ )
258
+ regular_tetrahedron *= scale_parameter
259
+ tetrahedron = np.zeros((xyz.shape[0], 4, 3))
260
+ tetrahedron[:] = xyz[:, None, :]
261
+ tetrahedron[:, :, :] += regular_tetrahedron[None, :, :]
262
+
263
+ return tetrahedron
264
+
265
+
266
+ def gradient_from_tetrahedron(tetrahedron, value):
267
+ """
268
+ Calculate the gradient from a tetrahedron
269
+ """
270
+ tetrahedron = tetrahedron.reshape(-1, 4, 3)
271
+ m = np.array(
272
+ [
273
+ [
274
+ (tetrahedron[:, 1, 0] - tetrahedron[:, 0, 0]),
275
+ (tetrahedron[:, 1, 1] - tetrahedron[:, 0, 1]),
276
+ (tetrahedron[:, 1, 2] - tetrahedron[:, 0, 2]),
277
+ ],
278
+ [
279
+ (tetrahedron[:, 2, 0] - tetrahedron[:, 0, 0]),
280
+ (tetrahedron[:, 2, 1] - tetrahedron[:, 0, 1]),
281
+ (tetrahedron[:, 2, 2] - tetrahedron[:, 0, 2]),
282
+ ],
283
+ [
284
+ (tetrahedron[:, 3, 0] - tetrahedron[:, 0, 0]),
285
+ (tetrahedron[:, 3, 1] - tetrahedron[:, 0, 1]),
286
+ (tetrahedron[:, 3, 2] - tetrahedron[:, 0, 2]),
287
+ ],
288
+ ]
289
+ )
290
+ I = np.array([[-1.0, 1.0, 0.0, 0.0], [-1.0, 0.0, 1.0, 0.0], [-1.0, 0.0, 0.0, 1.0]])
291
+ m = np.swapaxes(m, 0, 2)
292
+ element_gradients = np.linalg.inv(m)
293
+
294
+ element_gradients = element_gradients.swapaxes(1, 2)
295
+ element_gradients = element_gradients @ I
296
+ v = np.sum(element_gradients * value[:, None, :], axis=2)
297
+ return v
LoopStructural/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.6.1"
1
+ __version__ = "1.6.6"