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.
- LoopStructural/datatypes/_bounding_box.py +19 -4
- LoopStructural/datatypes/_point.py +36 -2
- LoopStructural/datatypes/_structured_grid.py +17 -0
- LoopStructural/datatypes/_surface.py +17 -0
- LoopStructural/export/omf_wrapper.py +49 -21
- LoopStructural/interpolators/__init__.py +13 -0
- LoopStructural/interpolators/_api.py +81 -13
- LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
- LoopStructural/interpolators/_discrete_interpolator.py +100 -53
- LoopStructural/interpolators/_finite_difference_interpolator.py +68 -78
- LoopStructural/interpolators/_geological_interpolator.py +27 -10
- LoopStructural/interpolators/_p1interpolator.py +3 -3
- LoopStructural/interpolators/_surfe_wrapper.py +42 -12
- LoopStructural/interpolators/supports/_2d_base_unstructured.py +16 -0
- LoopStructural/interpolators/supports/_2d_structured_grid.py +44 -9
- LoopStructural/interpolators/supports/_3d_base_structured.py +24 -7
- LoopStructural/interpolators/supports/_3d_structured_grid.py +38 -12
- LoopStructural/interpolators/supports/_3d_structured_tetra.py +7 -3
- LoopStructural/interpolators/supports/_3d_unstructured_tetra.py +8 -2
- LoopStructural/interpolators/supports/__init__.py +7 -0
- LoopStructural/interpolators/supports/_base_support.py +7 -0
- LoopStructural/modelling/__init__.py +1 -3
- LoopStructural/modelling/core/geological_model.py +2 -4
- LoopStructural/modelling/features/_analytical_feature.py +25 -16
- LoopStructural/modelling/features/_base_geological_feature.py +21 -8
- LoopStructural/modelling/features/_geological_feature.py +47 -11
- LoopStructural/modelling/features/_structural_frame.py +10 -18
- LoopStructural/modelling/features/_unconformity_feature.py +3 -3
- LoopStructural/modelling/features/builders/_base_builder.py +8 -0
- LoopStructural/modelling/features/builders/_folded_feature_builder.py +45 -14
- LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
- LoopStructural/modelling/features/builders/_structural_frame_builder.py +5 -0
- LoopStructural/modelling/features/fault/__init__.py +1 -1
- LoopStructural/modelling/features/fault/_fault_function.py +19 -1
- LoopStructural/modelling/features/fault/_fault_segment.py +40 -51
- LoopStructural/modelling/features/fold/__init__.py +1 -2
- LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py +0 -23
- LoopStructural/modelling/features/fold/_foldframe.py +4 -4
- LoopStructural/modelling/features/fold/_svariogram.py +81 -46
- LoopStructural/modelling/features/fold/fold_function/__init__.py +27 -0
- LoopStructural/modelling/features/fold/fold_function/_base_fold_rotation_angle.py +253 -0
- LoopStructural/modelling/features/fold/fold_function/_fourier_series_fold_rotation_angle.py +153 -0
- LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py +46 -0
- LoopStructural/modelling/features/fold/fold_function/_trigo_fold_rotation_angle.py +151 -0
- LoopStructural/modelling/input/process_data.py +47 -26
- LoopStructural/modelling/input/project_file.py +49 -23
- LoopStructural/utils/__init__.py +1 -0
- LoopStructural/utils/_surface.py +11 -4
- LoopStructural/utils/colours.py +26 -0
- LoopStructural/utils/features.py +5 -0
- LoopStructural/utils/maths.py +51 -0
- LoopStructural/version.py +1 -1
- LoopStructural-1.6.5.dist-info/METADATA +146 -0
- {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/RECORD +57 -52
- {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/WHEEL +1 -1
- LoopStructural/interpolators/_non_linear_discrete_interpolator.py +0 -0
- LoopStructural/modelling/features/fold/_fold_rotation_angle.py +0 -149
- LoopStructural-1.6.2.dist-info/METADATA +0 -81
- {LoopStructural-1.6.2.dist-info → LoopStructural-1.6.5.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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=
|
|
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
|
-
|
|
109
|
+
fault_edge_properties=fault_edge_properties,
|
|
84
110
|
)
|
LoopStructural/utils/__init__.py
CHANGED
LoopStructural/utils/_surface.py
CHANGED
|
@@ -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,
|
|
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) +
|
|
101
|
-
np.nanmax(all_values) -
|
|
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
|
LoopStructural/utils/maths.py
CHANGED
|
@@ -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.
|
|
1
|
+
__version__ = "1.6.5"
|