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.
- LoopStructural/datatypes/_bounding_box.py +77 -7
- LoopStructural/datatypes/_point.py +67 -7
- 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/_builders.py +141 -141
- LoopStructural/interpolators/_discrete_fold_interpolator.py +11 -4
- LoopStructural/interpolators/_discrete_interpolator.py +100 -53
- LoopStructural/interpolators/_finite_difference_interpolator.py +78 -88
- LoopStructural/interpolators/_geological_interpolator.py +27 -10
- LoopStructural/interpolators/_interpolator_builder.py +55 -0
- LoopStructural/interpolators/_interpolator_factory.py +7 -18
- 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 +28 -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 +11 -12
- LoopStructural/modelling/features/__init__.py +1 -0
- LoopStructural/modelling/features/_analytical_feature.py +48 -18
- LoopStructural/modelling/features/_base_geological_feature.py +37 -8
- LoopStructural/modelling/features/_cross_product_geological_feature.py +7 -0
- LoopStructural/modelling/features/_geological_feature.py +50 -12
- LoopStructural/modelling/features/_projected_vector_feature.py +112 -0
- LoopStructural/modelling/features/_structural_frame.py +16 -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 +47 -16
- LoopStructural/modelling/features/builders/_geological_feature_builder.py +29 -13
- LoopStructural/modelling/features/builders/_structural_frame_builder.py +7 -2
- LoopStructural/modelling/features/fault/__init__.py +1 -1
- LoopStructural/modelling/features/fault/_fault_function.py +19 -1
- LoopStructural/modelling/features/fault/_fault_function_feature.py +3 -0
- LoopStructural/modelling/features/fault/_fault_segment.py +50 -53
- 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/modelling/intrusions/intrusion_feature.py +3 -0
- LoopStructural/utils/__init__.py +1 -0
- LoopStructural/utils/_surface.py +18 -6
- LoopStructural/utils/_transformation.py +98 -14
- LoopStructural/utils/colours.py +50 -0
- LoopStructural/utils/features.py +5 -0
- LoopStructural/utils/maths.py +53 -1
- LoopStructural/version.py +1 -1
- LoopStructural-1.6.6.dist-info/METADATA +160 -0
- {LoopStructural-1.6.1.dist-info → LoopStructural-1.6.6.dist-info}/RECORD +66 -59
- {LoopStructural-1.6.1.dist-info → LoopStructural-1.6.6.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.1.dist-info/METADATA +0 -81
- {LoopStructural-1.6.1.dist-info → LoopStructural-1.6.6.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
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
|
|
@@ -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 =
|
|
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=
|
|
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,
|
|
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) +
|
|
101
|
-
np.nanmax(all_values) -
|
|
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
|
|
2
|
+
from . import getLogger
|
|
3
|
+
|
|
4
|
+
logger = getLogger(__name__)
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class EuclideanTransformation:
|
|
6
|
-
def __init__(
|
|
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.
|
|
16
|
-
self.translation = None
|
|
23
|
+
self.translation = translation[:dimensions]
|
|
17
24
|
self.dimensions = dimensions
|
|
18
|
-
self.angle =
|
|
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
|
-
|
|
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
|
-
"""
|
|
81
|
+
"""Transform points using the transformation and rotation
|
|
59
82
|
|
|
60
83
|
Parameters
|
|
61
84
|
----------
|
|
62
|
-
points :
|
|
63
|
-
|
|
85
|
+
points : np.ndarray
|
|
86
|
+
xyz points as as numpy array
|
|
64
87
|
|
|
65
88
|
Returns
|
|
66
89
|
-------
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
np.ndarray
|
|
91
|
+
xyz points in the transformed coordinate system
|
|
69
92
|
"""
|
|
70
|
-
|
|
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
|
-
|
|
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]
|
LoopStructural/utils/maths.py
CHANGED
|
@@ -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) ->
|
|
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
|
+
__version__ = "1.6.6"
|