LoopStructural 1.6.5__py3-none-any.whl → 1.6.7__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 +58 -3
- LoopStructural/datatypes/_point.py +32 -6
- LoopStructural/interpolators/__init__.py +1 -1
- LoopStructural/interpolators/_builders.py +141 -141
- LoopStructural/interpolators/_finite_difference_interpolator.py +11 -11
- LoopStructural/interpolators/_interpolator_builder.py +55 -0
- LoopStructural/interpolators/_interpolator_factory.py +7 -18
- LoopStructural/interpolators/supports/_3d_base_structured.py +4 -0
- LoopStructural/modelling/core/geological_model.py +9 -8
- LoopStructural/modelling/features/__init__.py +1 -0
- LoopStructural/modelling/features/_analytical_feature.py +23 -2
- LoopStructural/modelling/features/_base_geological_feature.py +17 -1
- LoopStructural/modelling/features/_cross_product_geological_feature.py +7 -0
- LoopStructural/modelling/features/_geological_feature.py +3 -1
- LoopStructural/modelling/features/_projected_vector_feature.py +112 -0
- LoopStructural/modelling/features/_structural_frame.py +6 -0
- LoopStructural/modelling/features/builders/_folded_feature_builder.py +2 -2
- LoopStructural/modelling/features/builders/_structural_frame_builder.py +2 -2
- LoopStructural/modelling/features/fault/_fault_function_feature.py +3 -0
- LoopStructural/modelling/features/fault/_fault_segment.py +10 -2
- LoopStructural/modelling/input/process_data.py +7 -6
- LoopStructural/modelling/intrusions/intrusion_feature.py +3 -0
- LoopStructural/utils/__init__.py +1 -1
- LoopStructural/utils/_surface.py +6 -1
- LoopStructural/utils/_transformation.py +98 -14
- LoopStructural/utils/colours.py +25 -2
- LoopStructural/version.py +1 -1
- {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/METADATA +16 -2
- {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/RECORD +32 -30
- {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/WHEEL +1 -1
- {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/LICENSE +0 -0
- {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/top_level.txt +0 -0
|
@@ -1182,7 +1182,7 @@ class GeologicalModel:
|
|
|
1182
1182
|
logger.debug(f"Reached unconformity {f.name}")
|
|
1183
1183
|
break
|
|
1184
1184
|
logger.debug(f"Adding {uc_feature.name} as unconformity to {f.name}")
|
|
1185
|
-
if f.type == FeatureType.FAULT:
|
|
1185
|
+
if f.type == FeatureType.FAULT or f.type == FeatureType.INACTIVEFAULT:
|
|
1186
1186
|
continue
|
|
1187
1187
|
if f == feature:
|
|
1188
1188
|
continue
|
|
@@ -1417,7 +1417,7 @@ class GeologicalModel:
|
|
|
1417
1417
|
return fault
|
|
1418
1418
|
|
|
1419
1419
|
# TODO move rescale to bounding box/transformer
|
|
1420
|
-
def rescale(self, points: np.ndarray, inplace: bool =
|
|
1420
|
+
def rescale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
|
|
1421
1421
|
"""
|
|
1422
1422
|
Convert from model scale to real world scale - in the future this
|
|
1423
1423
|
should also do transformations?
|
|
@@ -1440,7 +1440,7 @@ class GeologicalModel:
|
|
|
1440
1440
|
return points
|
|
1441
1441
|
|
|
1442
1442
|
# TODO move scale to bounding box/transformer
|
|
1443
|
-
def scale(self, points: np.ndarray, inplace: bool =
|
|
1443
|
+
def scale(self, points: np.ndarray, inplace: bool = False) -> np.ndarray:
|
|
1444
1444
|
"""Take points in UTM coordinates and reproject
|
|
1445
1445
|
into scaled model space
|
|
1446
1446
|
|
|
@@ -1824,6 +1824,7 @@ class GeologicalModel:
|
|
|
1824
1824
|
):
|
|
1825
1825
|
path = pathlib.Path(filename)
|
|
1826
1826
|
extension = path.suffix
|
|
1827
|
+
parent = path.parent
|
|
1827
1828
|
name = path.stem
|
|
1828
1829
|
stratigraphic_surfaces = self.get_stratigraphic_surfaces()
|
|
1829
1830
|
if fault_surfaces:
|
|
@@ -1832,19 +1833,19 @@ class GeologicalModel:
|
|
|
1832
1833
|
if extension == ".geoh5" or extension == '.omf':
|
|
1833
1834
|
s.save(filename)
|
|
1834
1835
|
else:
|
|
1835
|
-
s.save(f'{name}_{s.name}
|
|
1836
|
+
s.save(f'{parent}/{name}_{s.name}{extension}')
|
|
1836
1837
|
if stratigraphic_surfaces:
|
|
1837
1838
|
for s in self.get_stratigraphic_surfaces():
|
|
1838
1839
|
if extension == ".geoh5" or extension == '.omf':
|
|
1839
1840
|
s.save(filename)
|
|
1840
1841
|
else:
|
|
1841
|
-
s.save(f'{name}_{s.name}
|
|
1842
|
+
s.save(f'{parent}/{name}_{s.name}{extension}')
|
|
1842
1843
|
if block_model:
|
|
1843
1844
|
grid, ids = self.get_block_model()
|
|
1844
1845
|
if extension == ".geoh5" or extension == '.omf':
|
|
1845
1846
|
grid.save(filename)
|
|
1846
1847
|
else:
|
|
1847
|
-
grid.save(f'{name}_block_model
|
|
1848
|
+
grid.save(f'{parent}/{name}_block_model{extension}')
|
|
1848
1849
|
if stratigraphic_data:
|
|
1849
1850
|
if self.stratigraphic_column is not None:
|
|
1850
1851
|
for group in self.stratigraphic_column.keys():
|
|
@@ -1854,7 +1855,7 @@ class GeologicalModel:
|
|
|
1854
1855
|
if extension == ".geoh5" or extension == '.omf':
|
|
1855
1856
|
data.save(filename)
|
|
1856
1857
|
else:
|
|
1857
|
-
data.save(f'{name}_{group}_data
|
|
1858
|
+
data.save(f'{parent}/{name}_{group}_data{extension}')
|
|
1858
1859
|
if fault_data:
|
|
1859
1860
|
for f in self.fault_names():
|
|
1860
1861
|
for d in self.__getitem__(f).get_data():
|
|
@@ -1862,4 +1863,4 @@ class GeologicalModel:
|
|
|
1862
1863
|
|
|
1863
1864
|
d.save(filename)
|
|
1864
1865
|
else:
|
|
1865
|
-
d.save(f'{name}_{group}
|
|
1866
|
+
d.save(f'{parent}/{name}_{group}{extension}')
|
|
@@ -39,8 +39,16 @@ class AnalyticalGeologicalFeature(BaseFeature):
|
|
|
39
39
|
builder=None,
|
|
40
40
|
):
|
|
41
41
|
BaseFeature.__init__(self, name, model, faults, regions, builder)
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
try:
|
|
43
|
+
self.vector = np.array(vector, dtype=float).reshape(3)
|
|
44
|
+
except ValueError:
|
|
45
|
+
logger.error("AnalyticalGeologicalFeature: vector must be a 3 element array")
|
|
46
|
+
raise ValueError("vector must be a 3 element array")
|
|
47
|
+
try:
|
|
48
|
+
self.origin = np.array(origin, dtype=float).reshape(3)
|
|
49
|
+
except ValueError:
|
|
50
|
+
logger.error("AnalyticalGeologicalFeature: origin must be a 3 element array")
|
|
51
|
+
raise ValueError("origin must be a 3 element array")
|
|
44
52
|
self.type = FeatureType.ANALYTICAL
|
|
45
53
|
|
|
46
54
|
def to_json(self):
|
|
@@ -86,3 +94,16 @@ class AnalyticalGeologicalFeature(BaseFeature):
|
|
|
86
94
|
|
|
87
95
|
def get_data(self, value_map: Optional[dict] = None):
|
|
88
96
|
return
|
|
97
|
+
|
|
98
|
+
def copy(self, name: Optional[str] = None):
|
|
99
|
+
if name is None:
|
|
100
|
+
name = self.name
|
|
101
|
+
return AnalyticalGeologicalFeature(
|
|
102
|
+
name,
|
|
103
|
+
self.vector.copy(),
|
|
104
|
+
self.origin.copy(),
|
|
105
|
+
list(self.regions),
|
|
106
|
+
list(self.faults),
|
|
107
|
+
self.model,
|
|
108
|
+
self.builder,
|
|
109
|
+
)
|
|
@@ -296,7 +296,12 @@ class BaseFeature(metaclass=ABCMeta):
|
|
|
296
296
|
self.regions = [
|
|
297
297
|
r for r in self.regions if r.name != self.name and r.parent.name != self.name
|
|
298
298
|
]
|
|
299
|
-
|
|
299
|
+
|
|
300
|
+
callable = lambda xyz: (
|
|
301
|
+
self.evaluate_value(self.model.scale(xyz))
|
|
302
|
+
if self.model is not None
|
|
303
|
+
else self.evaluate_value(xyz)
|
|
304
|
+
)
|
|
300
305
|
isosurfacer = LoopIsosurfacer(bounding_box, callable=callable)
|
|
301
306
|
if name is None and self.name is not None:
|
|
302
307
|
name = self.name
|
|
@@ -375,3 +380,14 @@ class BaseFeature(metaclass=ABCMeta):
|
|
|
375
380
|
dictionary of data
|
|
376
381
|
"""
|
|
377
382
|
raise NotImplementedError
|
|
383
|
+
|
|
384
|
+
@abstractmethod
|
|
385
|
+
def copy(self, name: Optional[str] = None):
|
|
386
|
+
"""Copy the feature
|
|
387
|
+
|
|
388
|
+
Returns
|
|
389
|
+
-------
|
|
390
|
+
BaseFeature
|
|
391
|
+
copied feature
|
|
392
|
+
"""
|
|
393
|
+
raise NotImplementedError
|
|
@@ -98,3 +98,10 @@ class CrossProductGeologicalFeature(BaseFeature):
|
|
|
98
98
|
|
|
99
99
|
def get_data(self, value_map: Optional[dict] = None):
|
|
100
100
|
return
|
|
101
|
+
|
|
102
|
+
def copy(self, name: Optional[str] = None):
|
|
103
|
+
if name is None:
|
|
104
|
+
name = f'{self.name}_copy'
|
|
105
|
+
return CrossProductGeologicalFeature(
|
|
106
|
+
name, self.geological_feature_a, self.geological_feature_b
|
|
107
|
+
)
|
|
@@ -113,7 +113,9 @@ class GeologicalFeature(BaseFeature):
|
|
|
113
113
|
# if evaluation_points is not a numpy array try and convert
|
|
114
114
|
# otherwise error
|
|
115
115
|
evaluation_points = np.asarray(pos)
|
|
116
|
-
|
|
116
|
+
# if there is a builder lets make sure that the feature is up to date
|
|
117
|
+
if self.builder is not None:
|
|
118
|
+
self.builder.up_to_date()
|
|
117
119
|
# check if the points are within the display region
|
|
118
120
|
v = np.zeros(evaluation_points.shape[0])
|
|
119
121
|
v[:] = np.nan
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ...modelling.features import BaseFeature
|
|
8
|
+
|
|
9
|
+
from ...utils import getLogger
|
|
10
|
+
|
|
11
|
+
logger = getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProjectedVectorFeature(BaseFeature):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
vector: np.ndarray,
|
|
19
|
+
plane_feature: BaseFeature,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
Create a geological feature by projecting a vector onto a feature representing a plane
|
|
24
|
+
E.g. project a thickness vector onto an axial surface
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
name: feature name
|
|
29
|
+
vector: the vector to project
|
|
30
|
+
plane_feature: the plane
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
name : str
|
|
36
|
+
name of the feature
|
|
37
|
+
geological_feature_a : BaseFeature
|
|
38
|
+
Left hand side of cross product
|
|
39
|
+
geological_feature_b : BaseFeature
|
|
40
|
+
Right hand side of cross product
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(name)
|
|
43
|
+
self.plane_feature = plane_feature
|
|
44
|
+
self.vector = vector
|
|
45
|
+
|
|
46
|
+
self.value_feature = None
|
|
47
|
+
|
|
48
|
+
def evaluate_gradient(self, locations: np.ndarray, ignore_regions=False) -> np.ndarray:
|
|
49
|
+
"""
|
|
50
|
+
Calculate the gradient of the geological feature by using numpy to
|
|
51
|
+
calculate the cross
|
|
52
|
+
product between the two existing feature gradients.
|
|
53
|
+
This means both features have to be evaluated for the locations
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
locations
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# project s0 onto axis plane B X A X B
|
|
65
|
+
plane_normal = self.plane_feature.evaluate_gradient(locations, ignore_regions)
|
|
66
|
+
vector = np.tile(self.vector, (locations.shape[0], 1))
|
|
67
|
+
|
|
68
|
+
projected_vector = np.cross(
|
|
69
|
+
plane_normal, np.cross(vector, plane_normal, axisa=1, axisb=1), axisa=1, axisb=1
|
|
70
|
+
)
|
|
71
|
+
return projected_vector
|
|
72
|
+
|
|
73
|
+
def evaluate_value(self, evaluation_points: np.ndarray, ignore_regions=False) -> np.ndarray:
|
|
74
|
+
"""
|
|
75
|
+
Return 0 because there is no value for this feature
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
evaluation_points
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
values = np.zeros(evaluation_points.shape[0])
|
|
85
|
+
if self.value_feature is not None:
|
|
86
|
+
values[:] = self.value_feature.evaluate_value(evaluation_points, ignore_regions)
|
|
87
|
+
return values
|
|
88
|
+
|
|
89
|
+
def mean(self):
|
|
90
|
+
if self.value_feature:
|
|
91
|
+
return self.value_feature.mean()
|
|
92
|
+
return 0.0
|
|
93
|
+
|
|
94
|
+
def min(self):
|
|
95
|
+
if self.value_feature:
|
|
96
|
+
return self.value_feature.min()
|
|
97
|
+
return 0.0
|
|
98
|
+
|
|
99
|
+
def max(self):
|
|
100
|
+
if self.value_feature:
|
|
101
|
+
return self.value_feature.max()
|
|
102
|
+
return 0.0
|
|
103
|
+
|
|
104
|
+
def get_data(self, value_map: Optional[dict] = None):
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
def copy(self, name: Optional[str] = None):
|
|
108
|
+
if name is None:
|
|
109
|
+
name = f'{self.name}_copy'
|
|
110
|
+
return ProjectedVectorFeature(
|
|
111
|
+
name=name, vector=self.vector, plane_feature=self.plane_feature
|
|
112
|
+
)
|
|
@@ -176,3 +176,9 @@ class StructuralFrame(BaseFeature):
|
|
|
176
176
|
for f in self.features:
|
|
177
177
|
data.extend(f.get_data(value_map))
|
|
178
178
|
return data
|
|
179
|
+
|
|
180
|
+
def copy(self, name: Optional[str] = None):
|
|
181
|
+
if name is None:
|
|
182
|
+
name = f'{self.name}_copy'
|
|
183
|
+
# !TODO check if this needs to be a deep copy
|
|
184
|
+
return StructuralFrame(name, self.features, self.fold, self.model)
|
|
@@ -89,7 +89,7 @@ class FoldedFeatureBuilder(GeologicalFeatureBuilder):
|
|
|
89
89
|
fold_axis_rotation = get_fold_rotation_profile(self.axis_profile_type, far, fad)
|
|
90
90
|
if "axis_function" in kwargs:
|
|
91
91
|
# allow predefined function to be used
|
|
92
|
-
|
|
92
|
+
logger.error("axis_function is deprecated, use a specific fold rotation angle profile type")
|
|
93
93
|
else:
|
|
94
94
|
fold_axis_rotation.fit(params={'wavelength': kwargs.get("axis_wl", None)})
|
|
95
95
|
self.fold.fold_axis_rotation = fold_axis_rotation
|
|
@@ -107,7 +107,7 @@ class FoldedFeatureBuilder(GeologicalFeatureBuilder):
|
|
|
107
107
|
fold_limb_rotation = get_fold_rotation_profile(self.limb_profile_type, flr, fld)
|
|
108
108
|
if "limb_function" in kwargs:
|
|
109
109
|
# allow for predefined functions to be used
|
|
110
|
-
|
|
110
|
+
logger.error("limb_function is deprecated, use a specific fold rotation angle profile type")
|
|
111
111
|
else:
|
|
112
112
|
fold_limb_rotation.fit(params={'wavelength': kwargs.get("limb_wl", None)})
|
|
113
113
|
|
|
@@ -22,7 +22,14 @@ class FaultSegment(StructuralFrame):
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
def __init__(
|
|
25
|
-
self,
|
|
25
|
+
self,
|
|
26
|
+
name: str,
|
|
27
|
+
features: list,
|
|
28
|
+
faultfunction=None,
|
|
29
|
+
steps=10,
|
|
30
|
+
displacement=1.0,
|
|
31
|
+
fold=None,
|
|
32
|
+
model=None,
|
|
26
33
|
):
|
|
27
34
|
"""
|
|
28
35
|
A slip event of a fault
|
|
@@ -37,7 +44,7 @@ class FaultSegment(StructuralFrame):
|
|
|
37
44
|
how many integration steps for faults
|
|
38
45
|
kwargs
|
|
39
46
|
"""
|
|
40
|
-
StructuralFrame.__init__(self,
|
|
47
|
+
StructuralFrame.__init__(self, name=name, features=features, fold=fold, model=model)
|
|
41
48
|
self.type = FeatureType.FAULT
|
|
42
49
|
self.displacement = displacement
|
|
43
50
|
self._faultfunction = BaseFault().fault_displacement
|
|
@@ -261,6 +268,7 @@ class FaultSegment(StructuralFrame):
|
|
|
261
268
|
logger.error("nan slicing ")
|
|
262
269
|
# need to scale with fault displacement
|
|
263
270
|
v[mask, :] = self.__getitem__(1).evaluate_gradient(locations[mask, :])
|
|
271
|
+
v[mask, :] /= np.linalg.norm(v[mask, :], axis=1)[:, None]
|
|
264
272
|
scale = self.displacementfeature.evaluate_value(locations[mask, :])
|
|
265
273
|
v[mask, :] *= scale[:, None]
|
|
266
274
|
return v
|
|
@@ -78,7 +78,7 @@ class ProcessInputData:
|
|
|
78
78
|
self.contacts = contacts
|
|
79
79
|
self._contact_orientations = None
|
|
80
80
|
self.contact_orientations = contact_orientations
|
|
81
|
-
self._fault_orientations =
|
|
81
|
+
self._fault_orientations = pd.DataFrame(columns=["X", "Y", "Z", "gx", "gy", "gz", "coord", "feature_name"])
|
|
82
82
|
self.fault_orientations = fault_orientations
|
|
83
83
|
self._fault_locations = None
|
|
84
84
|
self.fault_locations = fault_locations
|
|
@@ -313,10 +313,11 @@ class ProcessInputData:
|
|
|
313
313
|
pts = self.fault_orientations.loc[
|
|
314
314
|
self.fault_orientations["feature_name"] == fname, ["gx", "gy", "gz"]
|
|
315
315
|
]
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
if len(pts)>0:
|
|
317
|
+
fault_properties.loc[
|
|
318
|
+
fname,
|
|
319
|
+
["avgNormalEasting", "avgNormalNorthing", "avgNormalAltitude"],
|
|
320
|
+
] = np.nanmean(pts, axis=0)
|
|
320
321
|
if (
|
|
321
322
|
"avgSlipDirEasting" not in fault_properties.columns
|
|
322
323
|
or "avgSlipDirNorthing" not in fault_properties.columns
|
|
@@ -399,7 +400,7 @@ class ProcessInputData:
|
|
|
399
400
|
dataframes.append(self.contacts)
|
|
400
401
|
if self.contact_orientations is not None:
|
|
401
402
|
dataframes.append(self.contact_orientations)
|
|
402
|
-
if self.fault_orientations
|
|
403
|
+
if len(self.fault_orientations) > 0:
|
|
403
404
|
dataframes.append(self.fault_orientations)
|
|
404
405
|
if self.fault_locations is not None:
|
|
405
406
|
dataframes.append(self.fault_locations)
|
LoopStructural/utils/__init__.py
CHANGED
LoopStructural/utils/_surface.py
CHANGED
|
@@ -63,6 +63,7 @@ class LoopIsosurfacer:
|
|
|
63
63
|
self,
|
|
64
64
|
values: Optional[Union[list, int, float]],
|
|
65
65
|
name: Optional[Union[List[str], str]] = None,
|
|
66
|
+
local=False
|
|
66
67
|
) -> surface_list:
|
|
67
68
|
"""Extract isosurfaces from the interpolator
|
|
68
69
|
|
|
@@ -73,6 +74,10 @@ class LoopIsosurfacer:
|
|
|
73
74
|
to extract a single isosurface for, or an integer to extract that many
|
|
74
75
|
isosurfaces evenly spaced between the minimum and maximum values of the
|
|
75
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
|
|
76
81
|
|
|
77
82
|
Returns
|
|
78
83
|
-------
|
|
@@ -84,7 +89,7 @@ class LoopIsosurfacer:
|
|
|
84
89
|
raise ValueError("No interpolator of callable function set")
|
|
85
90
|
|
|
86
91
|
surfaces = []
|
|
87
|
-
all_values = self.callable(self.bounding_box.regular_grid(local=
|
|
92
|
+
all_values = self.callable(self.bounding_box.regular_grid(local=local))
|
|
88
93
|
## set value to mean value if its not specified
|
|
89
94
|
if values is None:
|
|
90
95
|
values = [((np.nanmax(all_values) - np.nanmin(all_values)) / 2) + np.nanmin(all_values)]
|
|
@@ -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
|
LoopStructural/utils/colours.py
CHANGED
|
@@ -17,10 +17,33 @@ def random_colour(n: int = 1, cmap='tab20'):
|
|
|
17
17
|
list
|
|
18
18
|
List of colours in the form of (r,g,b,a) tuples
|
|
19
19
|
"""
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
from matplotlib import colormaps as cm
|
|
22
21
|
colours = []
|
|
23
22
|
for _i in range(n):
|
|
24
23
|
colours.append(cm.get_cmap(cmap)(rng.random()))
|
|
25
24
|
|
|
26
25
|
return colours
|
|
26
|
+
|
|
27
|
+
def random_hex_colour(n: int = 1, cmap='tab20'):
|
|
28
|
+
"""
|
|
29
|
+
Generate a list of random colours
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
n : int
|
|
34
|
+
Number of colours to generate
|
|
35
|
+
cmap : str, optional
|
|
36
|
+
Name of the matplotlib colour map to use, by default 'tab20'
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
list
|
|
41
|
+
List of colours in the form of hex strings
|
|
42
|
+
"""
|
|
43
|
+
from matplotlib import colormaps as cm
|
|
44
|
+
|
|
45
|
+
colours = []
|
|
46
|
+
for _i in range(n):
|
|
47
|
+
colours.append(cm.get_cmap(cmap)(rng.random()))
|
|
48
|
+
|
|
49
|
+
return [f'#{int(c[0]*255):02x}{int(c[1]*255):02x}{int(c[2]*255):02x}' for c in colours]
|
LoopStructural/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.6.
|
|
1
|
+
__version__ = "1.6.7"
|