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.

Files changed (32) hide show
  1. LoopStructural/datatypes/_bounding_box.py +58 -3
  2. LoopStructural/datatypes/_point.py +32 -6
  3. LoopStructural/interpolators/__init__.py +1 -1
  4. LoopStructural/interpolators/_builders.py +141 -141
  5. LoopStructural/interpolators/_finite_difference_interpolator.py +11 -11
  6. LoopStructural/interpolators/_interpolator_builder.py +55 -0
  7. LoopStructural/interpolators/_interpolator_factory.py +7 -18
  8. LoopStructural/interpolators/supports/_3d_base_structured.py +4 -0
  9. LoopStructural/modelling/core/geological_model.py +9 -8
  10. LoopStructural/modelling/features/__init__.py +1 -0
  11. LoopStructural/modelling/features/_analytical_feature.py +23 -2
  12. LoopStructural/modelling/features/_base_geological_feature.py +17 -1
  13. LoopStructural/modelling/features/_cross_product_geological_feature.py +7 -0
  14. LoopStructural/modelling/features/_geological_feature.py +3 -1
  15. LoopStructural/modelling/features/_projected_vector_feature.py +112 -0
  16. LoopStructural/modelling/features/_structural_frame.py +6 -0
  17. LoopStructural/modelling/features/builders/_folded_feature_builder.py +2 -2
  18. LoopStructural/modelling/features/builders/_structural_frame_builder.py +2 -2
  19. LoopStructural/modelling/features/fault/_fault_function_feature.py +3 -0
  20. LoopStructural/modelling/features/fault/_fault_segment.py +10 -2
  21. LoopStructural/modelling/input/process_data.py +7 -6
  22. LoopStructural/modelling/intrusions/intrusion_feature.py +3 -0
  23. LoopStructural/utils/__init__.py +1 -1
  24. LoopStructural/utils/_surface.py +6 -1
  25. LoopStructural/utils/_transformation.py +98 -14
  26. LoopStructural/utils/colours.py +25 -2
  27. LoopStructural/version.py +1 -1
  28. {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/METADATA +16 -2
  29. {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/RECORD +32 -30
  30. {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/WHEEL +1 -1
  31. {LoopStructural-1.6.5.dist-info → LoopStructural-1.6.7.dist-info}/LICENSE +0 -0
  32. {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 = True) -> np.ndarray:
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 = True) -> np.ndarray:
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}.{extension}')
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}.{extension}')
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.{extension}')
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.{extension}')
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}.{extension}')
1866
+ d.save(f'{parent}/{name}_{group}{extension}')
@@ -30,3 +30,4 @@ from ._cross_product_geological_feature import CrossProductGeologicalFeature
30
30
 
31
31
  from ._unconformity_feature import UnconformityFeature
32
32
  from ._analytical_feature import AnalyticalGeologicalFeature
33
+ from ._projected_vector_feature import ProjectedVectorFeature
@@ -39,8 +39,16 @@ class AnalyticalGeologicalFeature(BaseFeature):
39
39
  builder=None,
40
40
  ):
41
41
  BaseFeature.__init__(self, name, model, faults, regions, builder)
42
- self.vector = np.array(vector, dtype=float)
43
- self.origin = np.array(origin, dtype=float)
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
- callable = lambda xyz: self.evaluate_value(self.model.scale(xyz))
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
- self.builder.up_to_date()
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
- fold_axis_rotation.set_function(kwargs["axis_function"])
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
- fold_limb_rotation.set_function(kwargs["limb_function"])
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
 
@@ -110,8 +110,8 @@ class StructuralFrameBuilder:
110
110
  ) # ,region=self.region))
111
111
 
112
112
  self._frame = frame(
113
- self.name,
114
- [
113
+ name=self.name,
114
+ features=[
115
115
  self.builders[0].feature,
116
116
  self.builders[1].feature,
117
117
  self.builders[2].feature,
@@ -80,3 +80,6 @@ class FaultDisplacementFeature(BaseFeature):
80
80
 
81
81
  def get_data(self, value_map: Optional[dict] = None):
82
82
  pass
83
+
84
+ def copy(self, name: Optional[str] = None):
85
+ raise NotImplementedError("Not implemented yet")
@@ -22,7 +22,14 @@ class FaultSegment(StructuralFrame):
22
22
  """
23
23
 
24
24
  def __init__(
25
- self, features, name, faultfunction=None, steps=10, displacement=1.0, fold=None, model=None
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, features, name, fold, model)
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 = None
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
- fault_properties.loc[
317
- fname,
318
- ["avgNormalEasting", "avgNormalNorthing", "avgNormalAltitude"],
319
- ] = np.nanmean(pts, axis=0)
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 is not None:
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)
@@ -408,3 +408,6 @@ class IntrusionFeature(BaseFeature):
408
408
 
409
409
  def get_data(self, value_map: Optional[dict] = None):
410
410
  pass
411
+
412
+ def copy(self):
413
+ pass
@@ -36,4 +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
39
+ from .colours import random_colour, random_hex_colour
@@ -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=False))
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 sklearn import decomposition
2
+ from . import getLogger
3
+
4
+ logger = getLogger(__name__)
3
5
 
4
6
 
5
7
  class EuclideanTransformation:
6
- def __init__(self, dimensions=2):
8
+ def __init__(
9
+ self, dimensions: int = 2, angle: float = 0, translation: np.ndarray = np.zeros(3)
10
+ ):
7
11
  """Transforms points into a new coordinate
8
12
  system where the main eigenvector is aligned with x
9
13
 
@@ -11,11 +15,14 @@ class EuclideanTransformation:
11
15
  ----------
12
16
  dimensions : int, optional
13
17
  Do transformation in map view or on 3d volume, by default 2
18
+ angle : float, optional
19
+ Angle to rotate the points by, by default 0
20
+ translation : np.ndarray, default zeros
21
+ Translation to apply to the points, by default
14
22
  """
15
- self.rotation = None
16
- self.translation = None
23
+ self.translation = translation[:dimensions]
17
24
  self.dimensions = dimensions
18
- self.angle = 0
25
+ self.angle = angle
19
26
 
20
27
  def fit(self, points: np.ndarray):
21
28
  """Fit the transformation to a point cloud
@@ -28,10 +35,16 @@ class EuclideanTransformation:
28
35
  points : np.ndarray
29
36
  xyz points as as numpy array
30
37
  """
38
+ try:
39
+ from sklearn import decomposition
40
+ except ImportError:
41
+ logger.error('scikit-learn is required for this function')
42
+ return
31
43
  points = np.array(points)
32
44
  if points.shape[1] < self.dimensions:
33
45
  raise ValueError("Points must have at least {} dimensions".format(self.dimensions))
34
46
  # standardise the points so that centre is 0
47
+ # self.translation = np.zeros(3)
35
48
  self.translation = np.mean(points, axis=0)
36
49
  # find main eigenvector and and calculate the angle of this with x
37
50
  pca = decomposition.PCA(n_components=self.dimensions).fit(
@@ -39,38 +52,109 @@ class EuclideanTransformation:
39
52
  )
40
53
  coeffs = pca.components_
41
54
  self.angle = -np.arccos(np.dot(coeffs[0, :], [1, 0]))
42
- self.rotation = self._rotation(self.angle)
55
+ return self
56
+
57
+ @property
58
+ def rotation(self):
59
+ return self._rotation(self.angle)
60
+
61
+ @property
62
+ def inverse_rotation(self):
63
+ return self._rotation(-self.angle)
43
64
 
44
65
  def _rotation(self, angle):
45
66
  return np.array(
46
67
  [
47
68
  [np.cos(angle), -np.sin(angle), 0],
48
69
  [np.sin(angle), np.cos(angle), 0],
49
- [0, 0, 1],
70
+ [0, 0, -1],
50
71
  ]
51
72
  )
52
73
 
53
74
  def fit_transform(self, points: np.ndarray) -> np.ndarray:
75
+ """Fit the transformation and transform the points"""
76
+
54
77
  self.fit(points)
55
78
  return self.transform(points)
56
79
 
57
80
  def transform(self, points: np.ndarray) -> np.ndarray:
58
- """_summary_
81
+ """Transform points using the transformation and rotation
59
82
 
60
83
  Parameters
61
84
  ----------
62
- points : _type_
63
- _description_
85
+ points : np.ndarray
86
+ xyz points as as numpy array
64
87
 
65
88
  Returns
66
89
  -------
67
- _type_
68
- _description_
90
+ np.ndarray
91
+ xyz points in the transformed coordinate system
69
92
  """
70
- return np.dot(points - self.translation, self.rotation)
93
+ points = np.array(points)
94
+ if points.shape[1] < self.dimensions:
95
+ raise ValueError("Points must have at least {} dimensions".format(self.dimensions))
96
+ centred = points - self.translation
97
+
98
+ return np.einsum(
99
+ 'ik,jk->ij',
100
+ centred,
101
+ self.rotation[: self.dimensions, : self.dimensions],
102
+ )
71
103
 
72
104
  def inverse_transform(self, points: np.ndarray) -> np.ndarray:
73
- return np.dot(points, self._rotation(-self.angle)) + self.translation
105
+ """
106
+ Transform points back to the original coordinate system
107
+
108
+ Parameters
109
+ ----------
110
+ points : np.ndarray
111
+ xyz points as as numpy array
112
+
113
+ Returns
114
+ -------
115
+ np.ndarray
116
+ xyz points in the original coordinate system
117
+ """
118
+
119
+ return (
120
+ np.einsum(
121
+ 'ik,jk->ij',
122
+ points,
123
+ self.inverse_rotation[: self.dimensions, : self.dimensions],
124
+ )
125
+ + self.translation
126
+ )
74
127
 
75
128
  def __call__(self, points: np.ndarray) -> np.ndarray:
129
+ """
130
+ Transform points into the transformed space
131
+
132
+ Parameters
133
+ ----------
134
+ points : np.ndarray
135
+ xyz points as as numpy array
136
+
137
+ Returns
138
+ -------
139
+ np.ndarray
140
+ xyz points in the transformed coordinate system
141
+ """
142
+
76
143
  return self.transform(points)
144
+
145
+ def _repr_html_(self):
146
+ """
147
+ Provides an HTML representation of the TransRotator.
148
+ """
149
+ html_str = """
150
+ <div class="collapsible">
151
+ <button class="collapsible-button">{self.__class__.__name__}</button>
152
+ <div class="content">
153
+ <p>Translation: {self.translation}</p>
154
+ <p>Rotation Angle: {self.angle} degrees</p>
155
+ </div>
156
+ </div>
157
+ """.format(
158
+ self=self
159
+ )
160
+ return html_str
@@ -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
- import matplotlib.cm as cm
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.5"
1
+ __version__ = "1.6.7"