LoopStructural 1.6.2__py3-none-any.whl → 1.6.5__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 (60) hide show
  1. LoopStructural/datatypes/_bounding_box.py +19 -4
  2. LoopStructural/datatypes/_point.py +36 -2
  3. LoopStructural/datatypes/_structured_grid.py +17 -0
  4. LoopStructural/datatypes/_surface.py +17 -0
  5. LoopStructural/export/omf_wrapper.py +49 -21
  6. LoopStructural/interpolators/__init__.py +13 -0
  7. LoopStructural/interpolators/_api.py +81 -13
  8. LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
  9. LoopStructural/interpolators/_discrete_interpolator.py +100 -53
  10. LoopStructural/interpolators/_finite_difference_interpolator.py +68 -78
  11. LoopStructural/interpolators/_geological_interpolator.py +27 -10
  12. LoopStructural/interpolators/_p1interpolator.py +3 -3
  13. LoopStructural/interpolators/_surfe_wrapper.py +42 -12
  14. LoopStructural/interpolators/supports/_2d_base_unstructured.py +16 -0
  15. LoopStructural/interpolators/supports/_2d_structured_grid.py +44 -9
  16. LoopStructural/interpolators/supports/_3d_base_structured.py +24 -7
  17. LoopStructural/interpolators/supports/_3d_structured_grid.py +38 -12
  18. LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
  19. LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +8 -2
  20. LoopStructural/interpolators/supports/__init__.py +7 -0
  21. LoopStructural/interpolators/supports/_base_support.py +7 -0
  22. LoopStructural/modelling/__init__.py +1 -3
  23. LoopStructural/modelling/core/geological_model.py +2 -4
  24. LoopStructural/modelling/features/_analytical_feature.py +25 -16
  25. LoopStructural/modelling/features/_base_geological_feature.py +21 -8
  26. LoopStructural/modelling/features/_geological_feature.py +47 -11
  27. LoopStructural/modelling/features/_structural_frame.py +10 -18
  28. LoopStructural/modelling/features/_unconformity_feature.py +3 -3
  29. LoopStructural/modelling/features/builders/_base_builder.py +8 -0
  30. LoopStructural/modelling/features/builders/_folded_feature_builder.py +45 -14
  31. LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
  32. LoopStructural/modelling/features/builders/_structural_frame_builder.py +5 -0
  33. LoopStructural/modelling/features/fault/__init__.py +1 -1
  34. LoopStructural/modelling/features/fault/_fault_function.py +19 -1
  35. LoopStructural/modelling/features/fault/_fault_segment.py +40 -51
  36. LoopStructural/modelling/features/fold/__init__.py +1 -2
  37. LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
  38. LoopStructural/modelling/features/fold/_foldframe.py +4 -4
  39. LoopStructural/modelling/features/fold/_svariogram.py +81 -46
  40. LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
  41. LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
  42. LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
  43. LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
  44. LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
  45. LoopStructural/modelling/input/process_data.py +47 -26
  46. LoopStructural/modelling/input/project_file.py +49 -23
  47. LoopStructural/utils/__init__.py +1 -0
  48. LoopStructural/utils/_surface.py +11 -4
  49. LoopStructural/utils/colours.py +26 -0
  50. LoopStructural/utils/features.py +5 -0
  51. LoopStructural/utils/maths.py +51 -0
  52. LoopStructural/version.py +1 -1
  53. LoopStructural-1.6.5.dist-info/METADATA +146 -0
  54. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/RECORD +57 -52
  55. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/WHEEL +1 -1
  56. LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
  57. LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
  58. LoopStructural-1.6.2.dist-info/METADATA +0 -81
  59. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/LICENSE +0 -0
  60. {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,46 @@
1
+ from ._base_fold_rotation_angle import BaseFoldRotationAngleProfile
2
+ import numpy as np
3
+ import numpy.typing as npt
4
+ from typing import Optional, Callable
5
+ from .....utils import getLogger
6
+
7
+ logger = getLogger(__name__)
8
+
9
+
10
+ class LambdaFoldRotationAngleProfile(BaseFoldRotationAngleProfile):
11
+
12
+ def __init__(
13
+ self,
14
+ fn: Callable[[np.ndarray], np.ndarray],
15
+ rotation_angle: Optional[npt.NDArray[np.float64]] = None,
16
+ fold_frame_coordinate: Optional[npt.NDArray[np.float64]] = None,
17
+ ):
18
+ """The fold frame function using the lambda profile from Laurent 2016
19
+
20
+ Parameters
21
+ ----------
22
+ rotation_angle : npt.NDArray[np.float64], optional
23
+ the calculated fold rotation angle from observations in degrees, by default None
24
+ fold_frame_coordinate : npt.NDArray[np.float64], optional
25
+ fold frame coordinate scalar field value, by default None
26
+ lambda_ : float, optional
27
+ lambda parameter, by default 0
28
+ """
29
+ super().__init__(rotation_angle, fold_frame_coordinate)
30
+ self._function = fn
31
+
32
+ @property
33
+ def params(self):
34
+ return {}
35
+
36
+ def update_params(self, params):
37
+ pass
38
+
39
+ def initial_guess(
40
+ self,
41
+ wavelength: float | None = None,
42
+ calculate_wavelength: bool = True,
43
+ svariogram_parameters: dict = {},
44
+ reset: bool = False,
45
+ ) -> np.ndarray:
46
+ return np.array([])
@@ -0,0 +1,151 @@
1
+ from ._base_fold_rotation_angle import BaseFoldRotationAngleProfile
2
+ import numpy as np
3
+ import numpy.typing as npt
4
+ from typing import Optional, Union, List
5
+ from .....utils import getLogger
6
+
7
+ logger = getLogger(__name__)
8
+
9
+
10
+ class TrigoFoldRotationAngleProfile(BaseFoldRotationAngleProfile):
11
+
12
+ def __init__(
13
+ self,
14
+ rotation_angle: Optional[npt.NDArray[np.float64]] = None,
15
+ fold_frame_coordinate: Optional[npt.NDArray[np.float64]] = None,
16
+ origin: float = 0,
17
+ wavelength: float = 0,
18
+ inflectionpointangle: float = 0,
19
+ ):
20
+ """The fold frame function using the trigo profile from Laurent 2016
21
+
22
+ Parameters
23
+ ----------
24
+ rotation_angle : npt.NDArray[np.float64], optional
25
+ the calculated fold rotation angle from observations in degrees, by default None
26
+ fold_frame_coordinate : npt.NDArray[np.float64], optional
27
+ fold frame coordinate scalar field value, by default None
28
+ origin : float, optional
29
+ phase shift parameter, by default 0
30
+ wavelength : float, optional
31
+ wavelength of the profile, by default 0
32
+ inflectionpointangle : float, optional
33
+ height of the profile, tightness of fold, by default 0
34
+ """
35
+ super().__init__(rotation_angle, fold_frame_coordinate)
36
+ self._origin = origin
37
+ self._wavelength = wavelength
38
+ self._inflectionpointangle = inflectionpointangle
39
+
40
+ @property
41
+ def origin(self):
42
+ return self._origin
43
+
44
+ @property
45
+ def wavelength(self):
46
+ return self._wavelength
47
+
48
+ @property
49
+ def inflectionpointangle(self):
50
+ return self._inflectionpointangle
51
+
52
+ @origin.setter
53
+ def origin(self, value):
54
+ if np.isfinite(value):
55
+ self.notify_observers()
56
+
57
+ self._origin = value
58
+ else:
59
+ raise ValueError("origin must be a finite number")
60
+
61
+ @wavelength.setter
62
+ def wavelength(self, value):
63
+ if np.isfinite(value):
64
+ self.notify_observers()
65
+
66
+ self._wavelength = value
67
+ else:
68
+ raise ValueError("wavelength must be a finite number")
69
+
70
+ @inflectionpointangle.setter
71
+ def inflectionpointangle(self, value):
72
+ if np.isfinite(value):
73
+ if value < np.deg2rad(-90) or value > np.deg2rad(90):
74
+ logger.error(f"Inflection point angle is {np.rad2deg(value)} degrees")
75
+ raise ValueError("inflectionpointangle must be between 0 and 90")
76
+ self.notify_observers()
77
+ self._inflectionpointangle = value
78
+
79
+ else:
80
+ raise ValueError("inflectionpointangle must be a finite number")
81
+
82
+ @property
83
+ def params(self):
84
+ return {
85
+ "origin": self.origin,
86
+ "wavelength": self.wavelength,
87
+ "inflectionpointangle": self.inflectionpointangle,
88
+ }
89
+
90
+ @staticmethod
91
+ def _function(s, origin, wavelength, inflectionpointangle):
92
+ """
93
+
94
+ Parameters
95
+ ----------
96
+ s
97
+ origin
98
+ wavelength
99
+ inflectionpointangle
100
+
101
+ Returns
102
+ -------
103
+
104
+ """
105
+ tan_alpha_delta_half = np.tan(inflectionpointangle)
106
+ tan_alpha_shift = 0
107
+ x = (s - origin) / wavelength
108
+ return tan_alpha_delta_half * np.sin(2 * np.pi * x) + tan_alpha_shift
109
+
110
+ # def __call__(self, fold_frame_coordinate):
111
+ # return np.rad2deg(
112
+ # np.tan(
113
+ # self._function(
114
+ # fold_frame_coordinate, self.origin, self.wavelength, self.inflectionpointangle
115
+ # )
116
+ # )
117
+ # )
118
+
119
+ def calculate_misfit(
120
+ self, rotation_angle: np.ndarray, fold_frame_coordinate: np.ndarray
121
+ ) -> np.ndarray:
122
+ return super().calculate_misfit(rotation_angle, fold_frame_coordinate)
123
+
124
+ def update_params(self, params: Union[List, npt.NDArray[np.float64]]) -> None:
125
+ self.origin = params[0]
126
+ self.wavelength = params[1]
127
+ self.inflectionpointangle = params[2]
128
+
129
+ def initial_guess(
130
+ self,
131
+ wavelength: Optional[float] = None,
132
+ calculate_wavelength: bool = True,
133
+ svariogram_parameters: dict = {},
134
+ reset: bool = True,
135
+ ):
136
+ # reset the fold paramters before fitting
137
+ # otherwise use the current values to fit
138
+ if reset:
139
+ self.origin = 0
140
+ self.wavelength = 0
141
+ self.inflectionpointangle = np.deg2rad(45) # otherwise there is a numerical error
142
+ if calculate_wavelength:
143
+ self.wavelength = self.estimate_wavelength(svariogram_parameters=svariogram_parameters)
144
+ if wavelength is not None:
145
+ self.wavelength = wavelength
146
+ guess = [
147
+ self.fold_frame_coordinate.mean(),
148
+ self.wavelength,
149
+ np.max(np.arctan(np.deg2rad(self.rotation_angle))),
150
+ ]
151
+ return np.array(guess)
@@ -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
  )
@@ -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
@@ -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
@@ -86,19 +87,25 @@ class LoopIsosurfacer:
86
87
  all_values = self.callable(self.bounding_box.regular_grid(local=False))
87
88
  ## set value to mean value if its not specified
88
89
  if values is None:
89
- values = [(np.nanmax(all_values) - np.nanmin(all_values)) / 2]
90
- if isinstance(values, list):
90
+ values = [((np.nanmax(all_values) - np.nanmin(all_values)) / 2) + np.nanmin(all_values)]
91
+ if isinstance(values, Iterable):
91
92
  isovalues = values
92
93
  elif isinstance(values, float):
93
94
  isovalues = [values]
95
+ if isinstance(values, int) and values == 0:
96
+ values = 0.0 # assume 0 isosurface is meant to be a float
97
+
94
98
  elif isinstance(values, int) and values < 1:
95
99
  raise ValueError(
96
100
  "Number of isosurfaces must be greater than 1. Either use a positive integer or provide a list or float for a specific isovalue."
97
101
  )
98
102
  elif isinstance(values, int):
103
+ var = np.nanmax(all_values) - np.nanmin(all_values)
104
+ # buffer slices by 5% to make sure that we don't get isosurface does't exist issues
105
+ buffer = var * 0.05
99
106
  isovalues = np.linspace(
100
- np.nanmin(all_values) + np.finfo(float).eps,
101
- np.nanmax(all_values) - np.finfo(float).eps,
107
+ np.nanmin(all_values) + buffer,
108
+ np.nanmax(all_values) - buffer,
102
109
  values,
103
110
  )
104
111
  logger.info(f'Isosurfacing at values: {isovalues}')
@@ -0,0 +1,26 @@
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
@@ -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")
@@ -244,3 +244,54 @@ def get_dip_vector(strike, dip):
244
244
  ]
245
245
  )
246
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.2"
1
+ __version__ = "1.6.5"