apsg 1.3.0__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.
- AUTHORS.md +9 -0
- CHANGELOG.md +304 -0
- CONTRIBUTING.md +91 -0
- apsg/__init__.py +104 -0
- apsg/config.py +214 -0
- apsg/database/__init__.py +23 -0
- apsg/database/_alchemy.py +609 -0
- apsg/database/_sdbread.py +284 -0
- apsg/decorator/__init__.py +5 -0
- apsg/decorator/_decorator.py +43 -0
- apsg/feature/__init__.py +79 -0
- apsg/feature/_container.py +1808 -0
- apsg/feature/_geodata.py +702 -0
- apsg/feature/_paleomag.py +425 -0
- apsg/feature/_statistics.py +430 -0
- apsg/feature/_tensor2.py +550 -0
- apsg/feature/_tensor3.py +1108 -0
- apsg/helpers/__init__.py +28 -0
- apsg/helpers/_helper.py +7 -0
- apsg/helpers/_math.py +46 -0
- apsg/helpers/_notation.py +119 -0
- apsg/math/__init__.py +6 -0
- apsg/math/_matrix.py +406 -0
- apsg/math/_vector.py +590 -0
- apsg/pandas/__init__.py +27 -0
- apsg/pandas/_pandas_api.py +507 -0
- apsg/plotting/__init__.py +25 -0
- apsg/plotting/_fabricplot.py +563 -0
- apsg/plotting/_paleomagplots.py +71 -0
- apsg/plotting/_plot_artists.py +551 -0
- apsg/plotting/_projection.py +326 -0
- apsg/plotting/_roseplot.py +360 -0
- apsg/plotting/_stereogrid.py +332 -0
- apsg/plotting/_stereonet.py +992 -0
- apsg/shell.py +35 -0
- apsg-1.3.0.dist-info/AUTHORS.md +9 -0
- apsg-1.3.0.dist-info/METADATA +141 -0
- apsg-1.3.0.dist-info/RECORD +40 -0
- apsg-1.3.0.dist-info/WHEEL +4 -0
- apsg-1.3.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,1808 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from scipy.stats import vonmises
|
|
6
|
+
from scipy.cluster.hierarchy import linkage, fcluster, dendrogram
|
|
7
|
+
|
|
8
|
+
from apsg.config import apsg_conf
|
|
9
|
+
from apsg.math._vector import Vector2, Vector3
|
|
10
|
+
from apsg.helpers._math import acosd
|
|
11
|
+
from apsg.feature._geodata import Lineation, Foliation, Pair, Fault, Cone
|
|
12
|
+
from apsg.feature._tensor3 import OrientationTensor3, Ellipsoid, DeformationGradient3
|
|
13
|
+
from apsg.feature._tensor2 import OrientationTensor2, Ellipse
|
|
14
|
+
from apsg.feature._statistics import KentDistribution, vonMisesFisher
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FeatureSet:
|
|
18
|
+
"""
|
|
19
|
+
Base class for containers
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__slots__ = ("data", "name")
|
|
23
|
+
|
|
24
|
+
def __init__(self, data, name="Default"):
|
|
25
|
+
dtype_cls = getattr(sys.modules[__name__], type(self).__feature_type__)
|
|
26
|
+
assert all(
|
|
27
|
+
[isinstance(obj, dtype_cls) for obj in data]
|
|
28
|
+
), f"Data must be instances of {type(self).__feature_type__}"
|
|
29
|
+
# cast to correct instances
|
|
30
|
+
self.data = tuple([dtype_cls(d) for d in data])
|
|
31
|
+
self.name = name
|
|
32
|
+
self._cache = {}
|
|
33
|
+
|
|
34
|
+
def __copy__(self):
|
|
35
|
+
return type(self)(list(self.data), name=self.name)
|
|
36
|
+
|
|
37
|
+
copy = __copy__
|
|
38
|
+
|
|
39
|
+
def to_json(self):
|
|
40
|
+
"""Return as JSON dict"""
|
|
41
|
+
return {
|
|
42
|
+
"datatype": type(self).__name__,
|
|
43
|
+
"args": ({"collection": tuple(item.to_json() for item in self)},),
|
|
44
|
+
"kwargs": {"name": self.name},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def label(self):
|
|
48
|
+
"""Return label"""
|
|
49
|
+
return self.name
|
|
50
|
+
|
|
51
|
+
def __array__(self, dtype=None, copy=None):
|
|
52
|
+
return np.array([np.array(p) for p in self.data], dtype=dtype)
|
|
53
|
+
|
|
54
|
+
def __eq__(self, other):
|
|
55
|
+
return NotImplemented
|
|
56
|
+
|
|
57
|
+
def __ne__(self, other):
|
|
58
|
+
return NotImplemented
|
|
59
|
+
|
|
60
|
+
def __bool__(self):
|
|
61
|
+
return len(self) != 0
|
|
62
|
+
|
|
63
|
+
def __len__(self):
|
|
64
|
+
return len(self.data)
|
|
65
|
+
|
|
66
|
+
def __getitem__(self, key):
|
|
67
|
+
if isinstance(key, slice):
|
|
68
|
+
return type(self)(self.data[key])
|
|
69
|
+
# elif isinstance(key, int):
|
|
70
|
+
elif np.issubdtype(type(key), np.integer):
|
|
71
|
+
return self.data[key]
|
|
72
|
+
elif isinstance(key, np.ndarray): # fancy indexing
|
|
73
|
+
idxs = np.arange(len(self.data), dtype=int)[key]
|
|
74
|
+
return type(self)([self.data[ix] for ix in idxs])
|
|
75
|
+
else:
|
|
76
|
+
raise TypeError(
|
|
77
|
+
"Wrong index. Only slice, int and np.array are allowed for indexing."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def __iter__(self):
|
|
81
|
+
return iter(self.data)
|
|
82
|
+
|
|
83
|
+
def __add__(self, other):
|
|
84
|
+
if isinstance(other, type(self)):
|
|
85
|
+
return type(self)(self.data + other.data, name=self.name)
|
|
86
|
+
else:
|
|
87
|
+
raise TypeError(f"Only {self.__name__} is allowed")
|
|
88
|
+
|
|
89
|
+
def rotate(self, axis, phi):
|
|
90
|
+
"""Rotate ``FeatureSet`` object `phi` degress about `axis`."""
|
|
91
|
+
return type(self)([e.rotate(axis, phi) for e in self], name=self.name)
|
|
92
|
+
|
|
93
|
+
def bootstrap(self, n=100, size=None):
|
|
94
|
+
"""Return generator of bootstraped samples from ``FeatureSet``.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
n: number of samples to be generated. Default 100.
|
|
98
|
+
size: number of data in sample. Default is same as ``FeatureSet``.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> np.random.seed(6034782)
|
|
102
|
+
>>> l = Vector3Set.random_fisher(n=100, position=lin(120,40))
|
|
103
|
+
>>> sm = [lb.R() for lb in l.bootstrap()]
|
|
104
|
+
>>> l.fisher_statistics()
|
|
105
|
+
{'k': 19.91236110604979, 'a95': 3.249027370399397, 'csd': 18.15196473425630}
|
|
106
|
+
>>> Vector3Set(sm).fisher_statistics()
|
|
107
|
+
{'k': 1735.360206701859, 'a95': 0.3393224356447341, 'csd': 1.944420546779801}
|
|
108
|
+
"""
|
|
109
|
+
if size is None:
|
|
110
|
+
size = len(self)
|
|
111
|
+
for i in range(n):
|
|
112
|
+
yield self[np.random.choice(range(len(self)), size)]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Vector2Set(FeatureSet):
|
|
116
|
+
"""
|
|
117
|
+
Class to store set of ``Vector2`` features
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
__feature_type__ = "Vector2"
|
|
121
|
+
|
|
122
|
+
def __repr__(self):
|
|
123
|
+
return f"V2({len(self)}) {self.name}"
|
|
124
|
+
|
|
125
|
+
def __abs__(self):
|
|
126
|
+
"""Returns array of euclidean norms"""
|
|
127
|
+
return np.asarray([abs(e) for e in self])
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def x(self):
|
|
131
|
+
"""Return numpy array of x-components"""
|
|
132
|
+
return np.array([e.x for e in self])
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def y(self):
|
|
136
|
+
"""Return numpy array of y-components"""
|
|
137
|
+
return np.array([e.y for e in self])
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def direction(self):
|
|
141
|
+
"""Return array of direction angles"""
|
|
142
|
+
return np.asarray([e.direction for e in self]).T
|
|
143
|
+
|
|
144
|
+
def proj(self, vec):
|
|
145
|
+
"""Return projections of all features in ``Vector2Set`` onto vector."""
|
|
146
|
+
return type(self)([e.project() for e in self], name=self.name)
|
|
147
|
+
|
|
148
|
+
def dot(self, vec):
|
|
149
|
+
"""Return array of dot products of all features in ``Vector2Set`` with vector."""
|
|
150
|
+
return np.array([e.dot(vec) for e in self])
|
|
151
|
+
|
|
152
|
+
def cross(self, other=None):
|
|
153
|
+
"""Return cross products of all features in ``Vector2Set``
|
|
154
|
+
|
|
155
|
+
Without arguments it returns cross product of all pairs in dataset.
|
|
156
|
+
If argument is ``Vector2Set`` of same length or single data object
|
|
157
|
+
element-wise cross-products are calculated.
|
|
158
|
+
"""
|
|
159
|
+
res = []
|
|
160
|
+
if other is None:
|
|
161
|
+
res = [e.cross(f) for e, f in combinations(self.data, 2)]
|
|
162
|
+
elif issubclass(type(other), FeatureSet):
|
|
163
|
+
res = [e.cross(f) for e, f in zip(self, other)]
|
|
164
|
+
elif issubclass(type(other), Vector3):
|
|
165
|
+
res = [e.cross(other) for e in self]
|
|
166
|
+
else:
|
|
167
|
+
raise TypeError("Wrong argument type!")
|
|
168
|
+
return np.asarray(res)
|
|
169
|
+
|
|
170
|
+
__pow__ = cross
|
|
171
|
+
|
|
172
|
+
def angle(self, other=None):
|
|
173
|
+
"""Return angles of all data in ``Vector2Set`` object
|
|
174
|
+
|
|
175
|
+
Without arguments it returns angles of all pairs in dataset.
|
|
176
|
+
If argument is ``Vector2Set`` of same length or single data object
|
|
177
|
+
element-wise angles are calculated.
|
|
178
|
+
"""
|
|
179
|
+
res = []
|
|
180
|
+
if other is None:
|
|
181
|
+
res = [e.angle(f) for e, f in combinations(self.data, 2)]
|
|
182
|
+
elif issubclass(type(other), FeatureSet):
|
|
183
|
+
res = [e.angle(f) for e, f in zip(self, other)]
|
|
184
|
+
elif issubclass(type(other), Vector3):
|
|
185
|
+
res = [e.angle(other) for e in self]
|
|
186
|
+
else:
|
|
187
|
+
raise TypeError("Wrong argument type!")
|
|
188
|
+
return np.asarray(res)
|
|
189
|
+
|
|
190
|
+
def normalized(self):
|
|
191
|
+
"""Return ``Vector2Set`` object with normalized (unit length) elements."""
|
|
192
|
+
return type(self)([e.normalized() for e in self], name=self.name)
|
|
193
|
+
|
|
194
|
+
uv = normalized
|
|
195
|
+
|
|
196
|
+
def transform(self, F, **kwargs):
|
|
197
|
+
"""Return affine transformation of all features ``Vector2Set`` by matrix 'F'.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
F: Transformation matrix. Array-like value e.g. ``DeformationGradient3``
|
|
201
|
+
|
|
202
|
+
Keyword Args:
|
|
203
|
+
norm: normalize transformed features. True or False. Default False
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
return type(self)([e.transform(F, **kwargs) for e in self], name=self.name)
|
|
207
|
+
|
|
208
|
+
def R(self, mean=False):
|
|
209
|
+
"""Return resultant of data in ``Vector2Set`` object.
|
|
210
|
+
|
|
211
|
+
Resultant is of same type as features in ``Vector2Set``. Note
|
|
212
|
+
that ``Axial2`` is axial in nature so resultant can give
|
|
213
|
+
other result than expected. Anyway for axial data orientation
|
|
214
|
+
tensor analysis will give you right answer.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
mean: if True returns mean resultant. Default False
|
|
218
|
+
"""
|
|
219
|
+
R = sum(self)
|
|
220
|
+
if mean:
|
|
221
|
+
R = R / len(self)
|
|
222
|
+
return R
|
|
223
|
+
|
|
224
|
+
def fisher_statistics(self):
|
|
225
|
+
"""Fisher's statistics
|
|
226
|
+
|
|
227
|
+
fisher_statistics returns dictionary with keys:
|
|
228
|
+
`k` estimated precision parameter,
|
|
229
|
+
`csd` estimated angular standard deviation
|
|
230
|
+
`a95` confidence limit
|
|
231
|
+
"""
|
|
232
|
+
stats = {"k": np.inf, "a95": 0, "csd": 0}
|
|
233
|
+
N = len(self)
|
|
234
|
+
R = abs(self.normalized().R())
|
|
235
|
+
if N != R:
|
|
236
|
+
stats["k"] = (N - 1) / (N - R)
|
|
237
|
+
stats["csd"] = 81 / np.sqrt(stats["k"])
|
|
238
|
+
stats["a95"] = acosd(1 - ((N - R) / R) * (20 ** (1 / (N - 1)) - 1))
|
|
239
|
+
return stats
|
|
240
|
+
|
|
241
|
+
def var(self):
|
|
242
|
+
"""Spherical variance based on resultant length (Mardia 1972).
|
|
243
|
+
|
|
244
|
+
var = 1 - abs(R) / n
|
|
245
|
+
"""
|
|
246
|
+
return 1 - abs(self.normalized().R(mean=True))
|
|
247
|
+
|
|
248
|
+
def delta(self):
|
|
249
|
+
"""Cone angle containing ~63% of the data in degrees.
|
|
250
|
+
|
|
251
|
+
For enough large sample it approach angular standard deviation (csd)
|
|
252
|
+
of Fisher statistics
|
|
253
|
+
"""
|
|
254
|
+
return acosd(abs(self.R(mean=True)))
|
|
255
|
+
|
|
256
|
+
def rdegree(self):
|
|
257
|
+
"""Degree of preffered orientation of vectors in ``Vector2Set``.
|
|
258
|
+
|
|
259
|
+
D = 100 * (2 * abs(R) - n) / n
|
|
260
|
+
"""
|
|
261
|
+
N = len(self)
|
|
262
|
+
return 100 * (2 * abs(self.normalized().R()) - N) / N
|
|
263
|
+
|
|
264
|
+
def ortensor(self):
|
|
265
|
+
"""Return orientation tensor ``Ortensor`` of ``Group``."""
|
|
266
|
+
|
|
267
|
+
return self._ortensor
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def _ortensor(self):
|
|
271
|
+
if "ortensor" not in self._cache:
|
|
272
|
+
self._cache["ortensor"] = OrientationTensor2.from_features(self)
|
|
273
|
+
return self._cache["ortensor"]
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def _svd(self):
|
|
277
|
+
if "svd" not in self._cache:
|
|
278
|
+
self._cache["svd"] = np.linalg.svd(self._ortensor)
|
|
279
|
+
return self._cache["svd"]
|
|
280
|
+
|
|
281
|
+
def halfspace(self):
|
|
282
|
+
"""Change orientation of vectors in ``Vector2Set``, so all have angle<=90 with
|
|
283
|
+
resultant.
|
|
284
|
+
|
|
285
|
+
"""
|
|
286
|
+
dtype_cls = getattr(sys.modules[__name__], type(self).__feature_type__)
|
|
287
|
+
v = Vector3Set(self)
|
|
288
|
+
v_data = list(v)
|
|
289
|
+
alldone = np.all(v.angle(v.R()) <= 90)
|
|
290
|
+
while not alldone:
|
|
291
|
+
ang = v.angle(v.R())
|
|
292
|
+
for ix, do in enumerate(ang > 90):
|
|
293
|
+
if do:
|
|
294
|
+
v_data[ix] = -v_data[ix]
|
|
295
|
+
v = Vector3Set(v_data)
|
|
296
|
+
alldone = np.all(v.angle(v.R()) <= 90)
|
|
297
|
+
return type(self)([dtype_cls(vec) for vec in v], name=self.name)
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def from_direction(cls, angles, name="Default"):
|
|
301
|
+
"""Create ``Vector2Set`` object from arrays of direction angles
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
angles: list or angles
|
|
305
|
+
|
|
306
|
+
Keyword Args:
|
|
307
|
+
name: name of ``Vector2Set`` object. Default is 'Default'
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
>>> f = vec2set.from_angles([120,130,140,125, 132. 131])
|
|
311
|
+
"""
|
|
312
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
313
|
+
return cls([dtype_cls(a) for a in angles], name=name)
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def from_xy(cls, x, y, name="Default"):
|
|
317
|
+
"""Create ``Vector2Set`` object from arrays of x and y components
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
x: list or array of x components
|
|
321
|
+
y: list or array of y components
|
|
322
|
+
|
|
323
|
+
Keyword Args:
|
|
324
|
+
name: name of ``Vector2Set`` object. Default is 'Default'
|
|
325
|
+
|
|
326
|
+
Example:
|
|
327
|
+
>>> v = vec2set.from_xy([-0.4330127, -0.4330127, -0.66793414],
|
|
328
|
+
[0.75, 0.25, 0.60141061])
|
|
329
|
+
"""
|
|
330
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
331
|
+
return cls([dtype_cls(xx, yy) for xx, yy in zip(x, y)], name=name)
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def random(cls, n=100, name="Default"):
|
|
335
|
+
"""Method to create ``Vector2Set`` of features with uniformly distributed
|
|
336
|
+
random orientation.
|
|
337
|
+
|
|
338
|
+
Keyword Args:
|
|
339
|
+
n: number of objects to be generated
|
|
340
|
+
name: name of dataset. Default is 'Default'
|
|
341
|
+
|
|
342
|
+
Example:
|
|
343
|
+
>>> np.random.seed(58463123)
|
|
344
|
+
>>> l = vec2set.random(100)
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
348
|
+
return cls([dtype_cls.random() for i in range(n)], name=name)
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def random_vonmises(cls, n=100, position=0, kappa=5, name="Default"):
|
|
352
|
+
"""Return ``Vector2Set`` of random vectors sampled from von Mises distribution
|
|
353
|
+
around center position with concentration kappa.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
n: number of objects to be generated
|
|
357
|
+
position: mean orientation given as angle. Default 0
|
|
358
|
+
kappa: precision parameter of the distribution. Default 20
|
|
359
|
+
name: name of dataset. Default is 'Default'
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
>>> l = linset.random_fisher(position=lin(120,50))
|
|
363
|
+
"""
|
|
364
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
365
|
+
angles = np.degrees(vonmises.rvs(kappa, loc=np.radians(position), size=n))
|
|
366
|
+
return cls([dtype_cls(a) for a in angles], name=name)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class Vector3Set(FeatureSet):
|
|
370
|
+
"""
|
|
371
|
+
Class to store set of ``Vector3`` features
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
__feature_type__ = "Vector3"
|
|
375
|
+
|
|
376
|
+
def __repr__(self):
|
|
377
|
+
return f"V3({len(self)}) {self.name}"
|
|
378
|
+
|
|
379
|
+
def __abs__(self):
|
|
380
|
+
"""Returns array of euclidean norms"""
|
|
381
|
+
return np.asarray([abs(e) for e in self])
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def x(self):
|
|
385
|
+
"""Return numpy array of x-components"""
|
|
386
|
+
return np.array([e.x for e in self])
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def y(self):
|
|
390
|
+
"""Return numpy array of y-components"""
|
|
391
|
+
return np.array([e.y for e in self])
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def z(self):
|
|
395
|
+
"""Return numpy array of z-components"""
|
|
396
|
+
return np.array([e.z for e in self])
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def geo(self):
|
|
400
|
+
"""Return arrays of azi and inc according to apsg_conf['notation']"""
|
|
401
|
+
azi, inc = np.asarray([e.geo for e in self]).T
|
|
402
|
+
return azi, inc
|
|
403
|
+
|
|
404
|
+
def to_lin(self):
|
|
405
|
+
"""Return ``LineationSet`` object with all data converted to ``Lineation``."""
|
|
406
|
+
return LineationSet([Lineation(e) for e in self], name=self.name)
|
|
407
|
+
|
|
408
|
+
def to_fol(self):
|
|
409
|
+
"""Return ``FoliationSet`` object with all data converted to ``Foliation``."""
|
|
410
|
+
return FoliationSet([Foliation(e) for e in self], name=self.name)
|
|
411
|
+
|
|
412
|
+
def to_vec(self):
|
|
413
|
+
"""Return ``Vector3Set`` object with all data converted to ``Vector3``."""
|
|
414
|
+
return Vector3Set([Vector3(e) for e in self], name=self.name)
|
|
415
|
+
|
|
416
|
+
def project(self, vec):
|
|
417
|
+
"""Return projections of all features in ``FeatureSet`` onto vector."""
|
|
418
|
+
return type(self)([e.project(vec) for e in self], name=self.name)
|
|
419
|
+
|
|
420
|
+
proj = project
|
|
421
|
+
|
|
422
|
+
def reject(self, vec):
|
|
423
|
+
"""Return rejections of all features in ``FeatureSet`` onto vector."""
|
|
424
|
+
return type(self)([e.reject(vec) for e in self], name=self.name)
|
|
425
|
+
|
|
426
|
+
def dot(self, vec):
|
|
427
|
+
"""Return array of dot products of all features in ``FeatureSet`` with vector."""
|
|
428
|
+
return np.array([e.dot(vec) for e in self])
|
|
429
|
+
|
|
430
|
+
def cross(self, other=None):
|
|
431
|
+
"""Return cross products of all features in ``FeatureSet``
|
|
432
|
+
|
|
433
|
+
Without arguments it returns cross product of all pairs in dataset.
|
|
434
|
+
If argument is ``FeatureSet`` of same length or single data object
|
|
435
|
+
element-wise cross-products are calculated.
|
|
436
|
+
"""
|
|
437
|
+
res = []
|
|
438
|
+
if other is None:
|
|
439
|
+
res = [e.cross(f) for e, f in combinations(self.data, 2)]
|
|
440
|
+
elif issubclass(type(other), FeatureSet):
|
|
441
|
+
res = [e.cross(f) for e, f in zip(self, other)]
|
|
442
|
+
elif issubclass(type(other), Vector3):
|
|
443
|
+
res = [e.cross(other) for e in self]
|
|
444
|
+
else:
|
|
445
|
+
raise TypeError("Wrong argument type!")
|
|
446
|
+
return G(res, name=self.name)
|
|
447
|
+
|
|
448
|
+
__pow__ = cross
|
|
449
|
+
|
|
450
|
+
def angle(self, other=None):
|
|
451
|
+
"""Return angles of all data in ``FeatureSet`` object
|
|
452
|
+
|
|
453
|
+
Without arguments it returns angles of all pairs in dataset.
|
|
454
|
+
If argument is ``FeatureSet`` of same length or single data object
|
|
455
|
+
element-wise angles are calculated.
|
|
456
|
+
"""
|
|
457
|
+
res = []
|
|
458
|
+
if other is None:
|
|
459
|
+
res = [e.angle(f) for e, f in combinations(self.data, 2)]
|
|
460
|
+
elif issubclass(type(other), FeatureSet):
|
|
461
|
+
res = [e.angle(f) for e, f in zip(self, other)]
|
|
462
|
+
elif issubclass(type(other), Vector3):
|
|
463
|
+
res = [e.angle(other) for e in self]
|
|
464
|
+
else:
|
|
465
|
+
raise TypeError("Wrong argument type!")
|
|
466
|
+
return np.asarray(res)
|
|
467
|
+
|
|
468
|
+
def normalized(self):
|
|
469
|
+
"""Return ``FeatureSet`` object with normalized (unit length) elements."""
|
|
470
|
+
return type(self)([e.normalized() for e in self], name=self.name)
|
|
471
|
+
|
|
472
|
+
uv = normalized
|
|
473
|
+
|
|
474
|
+
def transform(self, F, **kwargs):
|
|
475
|
+
"""Return affine transformation of all features ``FeatureSet`` by matrix 'F'.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
F: Transformation matrix. Array-like value e.g. ``DeformationGradient3``
|
|
479
|
+
|
|
480
|
+
Keyword Args:
|
|
481
|
+
norm: normalize transformed features. True or False. Default False
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
return type(self)([e.transform(F, **kwargs) for e in self], name=self.name)
|
|
485
|
+
|
|
486
|
+
def is_upper(self):
|
|
487
|
+
"""
|
|
488
|
+
Return boolean array of z-coordinate negative test
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
return np.asarray([e.is_upper() for e in self])
|
|
492
|
+
|
|
493
|
+
def R(self, mean=False):
|
|
494
|
+
"""Return resultant of data in ``FeatureSet`` object.
|
|
495
|
+
|
|
496
|
+
Resultant is of same type as features in ``FeatureSet``. Note
|
|
497
|
+
that ``Foliation`` and ``Lineation`` are axial in nature so
|
|
498
|
+
resultant can give other result than expected. Anyway for axial
|
|
499
|
+
data orientation tensor analysis will give you right answer.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
mean: if True returns mean resultant. Default False
|
|
503
|
+
"""
|
|
504
|
+
R = sum(self)
|
|
505
|
+
if mean:
|
|
506
|
+
R = R / len(self)
|
|
507
|
+
return R
|
|
508
|
+
|
|
509
|
+
def fisher_statistics(self):
|
|
510
|
+
"""Fisher's statistics
|
|
511
|
+
|
|
512
|
+
fisher_statistics returns dictionary with keys:
|
|
513
|
+
`k` estimated precision parameter,
|
|
514
|
+
`csd` estimated angular standard deviation
|
|
515
|
+
`a95` confidence limit
|
|
516
|
+
"""
|
|
517
|
+
stats = {"k": np.inf, "a95": 0, "csd": 0}
|
|
518
|
+
N = len(self)
|
|
519
|
+
R = abs(self.normalized().R())
|
|
520
|
+
if N != R:
|
|
521
|
+
stats["k"] = (N - 1) / (N - R)
|
|
522
|
+
stats["csd"] = 81 / np.sqrt(stats["k"])
|
|
523
|
+
stats["a95"] = acosd(1 - ((N - R) / R) * (20 ** (1 / (N - 1)) - 1))
|
|
524
|
+
return stats
|
|
525
|
+
|
|
526
|
+
def fisher_cone_a95(self):
|
|
527
|
+
"""Confidence limit cone based on Fisher's statistics
|
|
528
|
+
|
|
529
|
+
Cone axis is resultant and apical angle is a95 confidence limit
|
|
530
|
+
"""
|
|
531
|
+
stats = self.fisher_statistics()
|
|
532
|
+
return Cone(self.normalized().R(), stats["a95"])
|
|
533
|
+
|
|
534
|
+
def fisher_cone_csd(self):
|
|
535
|
+
"""Angular standard deviation cone based on Fisher's statistics
|
|
536
|
+
|
|
537
|
+
Cone axis is resultant and apical angle is angular standard deviation
|
|
538
|
+
"""
|
|
539
|
+
stats = self.fisher_statistics()
|
|
540
|
+
return Cone(self.normalized().R(), stats["csd"])
|
|
541
|
+
|
|
542
|
+
def var(self):
|
|
543
|
+
"""Spherical variance based on resultant length (Mardia 1972).
|
|
544
|
+
|
|
545
|
+
var = 1 - abs(R) / n
|
|
546
|
+
"""
|
|
547
|
+
return 1 - abs(self.normalized().R(mean=True))
|
|
548
|
+
|
|
549
|
+
def delta(self):
|
|
550
|
+
"""Cone angle containing ~63% of the data in degrees.
|
|
551
|
+
|
|
552
|
+
For enough large sample it approach angular standard deviation (csd)
|
|
553
|
+
of Fisher statistics
|
|
554
|
+
"""
|
|
555
|
+
return acosd(abs(self.R(mean=True)))
|
|
556
|
+
|
|
557
|
+
def rdegree(self):
|
|
558
|
+
"""Degree of preffered orientation of vectors in ``FeatureSet``.
|
|
559
|
+
|
|
560
|
+
D = 100 * (2 * abs(R) - n) / n
|
|
561
|
+
"""
|
|
562
|
+
N = len(self)
|
|
563
|
+
return 100 * (2 * abs(self.normalized().R()) - N) / N
|
|
564
|
+
|
|
565
|
+
def ortensor(self):
|
|
566
|
+
"""Return orientation tensor ``Ortensor`` of ``Group``."""
|
|
567
|
+
return self._ortensor
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def _ortensor(self):
|
|
571
|
+
if "ortensor" not in self._cache:
|
|
572
|
+
self._cache["ortensor"] = OrientationTensor3.from_features(self)
|
|
573
|
+
return self._cache["ortensor"]
|
|
574
|
+
|
|
575
|
+
@property
|
|
576
|
+
def _svd(self):
|
|
577
|
+
if "svd" not in self._cache:
|
|
578
|
+
self._cache["svd"] = np.linalg.svd(self._ortensor)
|
|
579
|
+
return self._cache["svd"]
|
|
580
|
+
|
|
581
|
+
def centered(self, max_vertical=False):
|
|
582
|
+
"""Rotate ``FeatureSet`` object to position that eigenvectors are parallel
|
|
583
|
+
to axes of coordinate system: E1||X (north-south), E2||X(east-west),
|
|
584
|
+
E3||X(vertical)
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
max_vertical: If True E1 is rotated to vertical. Default False
|
|
588
|
+
|
|
589
|
+
"""
|
|
590
|
+
if max_vertical:
|
|
591
|
+
return self.transform(self._svd[2]).rotate(Vector3(0, -1, 0), 90)
|
|
592
|
+
else:
|
|
593
|
+
return self.transform(self._svd[2])
|
|
594
|
+
|
|
595
|
+
def halfspace(self):
|
|
596
|
+
"""Change orientation of vectors in ``FeatureSet``, so all have angle<=90 with
|
|
597
|
+
resultant.
|
|
598
|
+
|
|
599
|
+
"""
|
|
600
|
+
dtype_cls = getattr(sys.modules[__name__], type(self).__feature_type__)
|
|
601
|
+
v = Vector3Set(self)
|
|
602
|
+
v_data = list(v)
|
|
603
|
+
alldone = np.all(v.angle(v.R()) <= 90)
|
|
604
|
+
while not alldone:
|
|
605
|
+
ang = v.angle(v.R())
|
|
606
|
+
for ix, do in enumerate(ang > 90):
|
|
607
|
+
if do:
|
|
608
|
+
v_data[ix] = -v_data[ix]
|
|
609
|
+
v = Vector3Set(v_data)
|
|
610
|
+
alldone = np.all(v.angle(v.R()) <= 90)
|
|
611
|
+
return type(self)([dtype_cls(vec) for vec in v], name=self.name)
|
|
612
|
+
|
|
613
|
+
@classmethod
|
|
614
|
+
def from_csv(cls, filename, acol=0, icol=1):
|
|
615
|
+
"""Create ``FeatureSet`` object from csv file of azimuths and inclinations
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
filename (str): name of CSV file to load
|
|
619
|
+
|
|
620
|
+
Keyword Args:
|
|
621
|
+
acol (int or str): azimuth column (starts from 0). Default 0
|
|
622
|
+
icol (int or str): inclination column (starts from 0). Default 1
|
|
623
|
+
When acol and icol are strings they are used as column headers.
|
|
624
|
+
|
|
625
|
+
Example:
|
|
626
|
+
>>> gf = folset.from_csv('file1.csv') #doctest: +SKIP
|
|
627
|
+
>>> gl = linset.from_csv('file2.csv', acol=1, icol=2) #doctest: +SKIP
|
|
628
|
+
|
|
629
|
+
"""
|
|
630
|
+
from os.path import basename
|
|
631
|
+
import csv
|
|
632
|
+
|
|
633
|
+
with open(filename) as csvfile:
|
|
634
|
+
has_header = csv.Sniffer().has_header(csvfile.read(1024))
|
|
635
|
+
csvfile.seek(0)
|
|
636
|
+
dialect = csv.Sniffer().sniff(csvfile.read(1024))
|
|
637
|
+
csvfile.seek(0)
|
|
638
|
+
if isinstance(acol, int) and isinstance(icol, int):
|
|
639
|
+
if has_header:
|
|
640
|
+
reader = csv.DictReader(csvfile, dialect=dialect)
|
|
641
|
+
aname, iname = reader.fieldnames[acol], reader.fieldnames[icol]
|
|
642
|
+
r = [(float(row[aname]), float(row[iname])) for row in reader]
|
|
643
|
+
else:
|
|
644
|
+
reader = csv.reader(csvfile, dialect=dialect)
|
|
645
|
+
r = [(float(row[acol]), float(row[icol])) for row in reader]
|
|
646
|
+
else:
|
|
647
|
+
if has_header:
|
|
648
|
+
reader = csv.DictReader(csvfile, dialect=dialect)
|
|
649
|
+
r = [(float(row[acol]), float(row[icol])) for row in reader]
|
|
650
|
+
else:
|
|
651
|
+
raise ValueError("No header line in CSV file...")
|
|
652
|
+
|
|
653
|
+
azi, inc = zip(*r)
|
|
654
|
+
return cls.from_array(azi, inc, name=basename(filename))
|
|
655
|
+
|
|
656
|
+
def to_csv(self, filename, delimiter=","):
|
|
657
|
+
"""Save ``FeatureSet`` object to csv file of azimuths and inclinations
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
filename (str): name of CSV file to save.
|
|
661
|
+
|
|
662
|
+
Keyword Args:
|
|
663
|
+
delimiter (str): values delimiter. Default ','
|
|
664
|
+
|
|
665
|
+
Note: Written values are rounded according to `ndigits` settings in apsg_conf
|
|
666
|
+
|
|
667
|
+
"""
|
|
668
|
+
import csv
|
|
669
|
+
|
|
670
|
+
n = apsg_conf["ndigits"]
|
|
671
|
+
|
|
672
|
+
with open(filename, "w", newline="") as csvfile:
|
|
673
|
+
fieldnames = ["azi", "inc"]
|
|
674
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
675
|
+
writer.writeheader()
|
|
676
|
+
for dt in self:
|
|
677
|
+
azi, inc = dt.geo
|
|
678
|
+
writer.writerow({"azi": round(azi, n), "inc": round(inc, n)})
|
|
679
|
+
|
|
680
|
+
@classmethod
|
|
681
|
+
def from_array(cls, azis, incs, name="Default"):
|
|
682
|
+
"""Create ``FeatureSet`` object from arrays of azimuths and inclinations
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
azis: list or array of azimuths
|
|
686
|
+
incs: list or array of inclinations
|
|
687
|
+
|
|
688
|
+
Keyword Args:
|
|
689
|
+
name: name of ``FeatureSet`` object. Default is 'Default'
|
|
690
|
+
|
|
691
|
+
Example:
|
|
692
|
+
>>> f = folset.from_array([120,130,140], [10,20,30])
|
|
693
|
+
>>> l = linset.from_array([120,130,140], [10,20,30])
|
|
694
|
+
"""
|
|
695
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
696
|
+
return cls([dtype_cls(azi, inc) for azi, inc in zip(azis, incs)], name=name)
|
|
697
|
+
|
|
698
|
+
@classmethod
|
|
699
|
+
def from_xyz(cls, x, y, z, name="Default"):
|
|
700
|
+
"""Create ``FeatureSet`` object from arrays of x, y and z components
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
x: list or array of x components
|
|
704
|
+
y: list or array of y components
|
|
705
|
+
z: list or array of z components
|
|
706
|
+
|
|
707
|
+
Keyword Args:
|
|
708
|
+
name: name of ``FeatureSet`` object. Default is 'Default'
|
|
709
|
+
|
|
710
|
+
Example:
|
|
711
|
+
>>> v = vecset.from_xyz([-0.4330127, -0.4330127, -0.66793414],
|
|
712
|
+
[0.75, 0.25, 0.60141061],
|
|
713
|
+
[0.5, 0.8660254, 0.43837115])
|
|
714
|
+
"""
|
|
715
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
716
|
+
return cls([dtype_cls(xx, yy, zz) for xx, yy, zz in zip(x, y, z)], name=name)
|
|
717
|
+
|
|
718
|
+
@classmethod
|
|
719
|
+
def random_normal(cls, n=100, position=Vector3(0, 0, 1), sigma=20, name="Default"):
|
|
720
|
+
"""Method to create ``FeatureSet`` of normaly distributed features.
|
|
721
|
+
|
|
722
|
+
Keyword Args:
|
|
723
|
+
n: number of objects to be generated
|
|
724
|
+
position: mean orientation given as ``Vector3``. Default Vector3(0, 0, 1)
|
|
725
|
+
sigma: sigma of normal distribution. Default 20
|
|
726
|
+
name: name of dataset. Default is 'Default'
|
|
727
|
+
|
|
728
|
+
Example:
|
|
729
|
+
>>> np.random.seed(58463123)
|
|
730
|
+
>>> l = linset.random_normal(100, lin(120, 40))
|
|
731
|
+
>>> l.R
|
|
732
|
+
L:120/39
|
|
733
|
+
|
|
734
|
+
"""
|
|
735
|
+
data = []
|
|
736
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
737
|
+
orig = Vector3(0, 0, 1)
|
|
738
|
+
ax = orig.cross(position)
|
|
739
|
+
ang = orig.angle(position)
|
|
740
|
+
for s, r in zip(
|
|
741
|
+
180 * np.random.uniform(low=0, high=180, size=n),
|
|
742
|
+
np.random.normal(loc=0, scale=sigma, size=n),
|
|
743
|
+
):
|
|
744
|
+
v = orig.rotate(Vector3(s, 0), r).rotate(ax, ang)
|
|
745
|
+
data.append(dtype_cls(v))
|
|
746
|
+
return cls(data, name=name)
|
|
747
|
+
|
|
748
|
+
@classmethod
|
|
749
|
+
def random_fisher(cls, n=100, position=Vector3(0, 0, 1), kappa=20, name="Default"):
|
|
750
|
+
"""Return ``FeatureSet`` of random vectors sampled from von Mises Fisher
|
|
751
|
+
distribution around center position with concentration kappa.
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
n: number of objects to be generated
|
|
755
|
+
position: mean orientation given as ``Vector3``. Default Vector3(0, 0, 1)
|
|
756
|
+
kappa: precision parameter of the distribution. Default 20
|
|
757
|
+
name: name of dataset. Default is 'Default'
|
|
758
|
+
|
|
759
|
+
Example:
|
|
760
|
+
>>> l = linset.random_fisher(position=lin(120,50))
|
|
761
|
+
"""
|
|
762
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
763
|
+
dc = vonMisesFisher(position, kappa, n)
|
|
764
|
+
return cls([dtype_cls(d) for d in dc], name=name)
|
|
765
|
+
|
|
766
|
+
@classmethod
|
|
767
|
+
def random_fisher2(cls, n=100, position=Vector3(0, 0, 1), kappa=20, name="Default"):
|
|
768
|
+
"""Method to create ``FeatureSet`` of vectors distributed according to
|
|
769
|
+
Fisher distribution.
|
|
770
|
+
|
|
771
|
+
Note: For proper von Mises Fisher distrinbution implementation use
|
|
772
|
+
``random.fisher`` method.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
n: number of objects to be generated
|
|
776
|
+
position: mean orientation given as ``Vector3``. Default Vector3(0, 0, 1)
|
|
777
|
+
kappa: precision parameter of the distribution. Default 20
|
|
778
|
+
name: name of dataset. Default is 'Default'
|
|
779
|
+
|
|
780
|
+
Example:
|
|
781
|
+
>>> l = linset.random_fisher2(position=lin(120,50))
|
|
782
|
+
"""
|
|
783
|
+
orig = Vector3(0, 0, 1)
|
|
784
|
+
ax = orig.cross(position)
|
|
785
|
+
ang = orig.angle(position)
|
|
786
|
+
L = np.exp(-2 * kappa)
|
|
787
|
+
a = np.random.random(n) * (1 - L) + L
|
|
788
|
+
fac = np.sqrt(-np.log(a) / (2 * kappa))
|
|
789
|
+
inc = 90 - 2 * np.degrees(np.arcsin(fac))
|
|
790
|
+
azi = 360 * np.random.random(n)
|
|
791
|
+
return cls.from_array(azi, inc, name=name).rotate(ax, ang)
|
|
792
|
+
|
|
793
|
+
@classmethod
|
|
794
|
+
def random_kent(cls, p, n=100, kappa=20, beta=None, name="Default"):
|
|
795
|
+
"""Return ``FeatureSet`` of random vectors sampled from Kent distribution
|
|
796
|
+
(Kent, 1982) - The 5-parameter Fisher–Bingham distribution.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
p: Pair object defining orientation of data
|
|
800
|
+
N: number of objects to be generated
|
|
801
|
+
kappa: concentration parameter. Default 20
|
|
802
|
+
beta: ellipticity 0 <= beta < kappa
|
|
803
|
+
name: name of dataset. Default is 'Default'
|
|
804
|
+
|
|
805
|
+
Example:
|
|
806
|
+
>>> p = pair(150, 40, 150, 40)
|
|
807
|
+
>>> l = linset.random_kent(p, n=300, kappa=30)
|
|
808
|
+
"""
|
|
809
|
+
assert issubclass(type(p), Pair), "Argument must be Pair object."
|
|
810
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
811
|
+
if beta is None:
|
|
812
|
+
beta = kappa / 2
|
|
813
|
+
kd = KentDistribution(p.lvec, p.fvec.cross(p.lvec), p.fvec, kappa, beta)
|
|
814
|
+
return cls([dtype_cls(d) for d in kd.rvs(n)], name=name)
|
|
815
|
+
|
|
816
|
+
@classmethod
|
|
817
|
+
def uniform_sfs(cls, n=100, name="Default"):
|
|
818
|
+
"""Method to create ``FeatureSet`` of uniformly distributed vectors.
|
|
819
|
+
Spherical Fibonacci Spiral points on a sphere algorithm adopted from
|
|
820
|
+
John Burkardt.
|
|
821
|
+
|
|
822
|
+
http://people.sc.fsu.edu/~jburkardt/
|
|
823
|
+
|
|
824
|
+
Keyword Args:
|
|
825
|
+
n: number of objects to be generated. Default 1000
|
|
826
|
+
name: name of dataset. Default is 'Default'
|
|
827
|
+
|
|
828
|
+
Example:
|
|
829
|
+
>>> v = vecset.uniform_sfs(300)
|
|
830
|
+
>>> v.ortensor().eigenvalues()
|
|
831
|
+
(0.3334645347163635, 0.33333474915201167, 0.33320071613162483)
|
|
832
|
+
"""
|
|
833
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
834
|
+
phi = (1 + np.sqrt(5)) / 2
|
|
835
|
+
i2 = 2 * np.arange(n) - n + 1
|
|
836
|
+
theta = 2 * np.pi * i2 / phi
|
|
837
|
+
sp = i2 / n
|
|
838
|
+
cp = np.sqrt((n + i2) * (n - i2)) / n
|
|
839
|
+
dc = np.array([cp * np.sin(theta), cp * np.cos(theta), sp]).T
|
|
840
|
+
return cls([dtype_cls(d) for d in dc], name=name)
|
|
841
|
+
|
|
842
|
+
@classmethod
|
|
843
|
+
def uniform_gss(cls, n=100, name="Default"):
|
|
844
|
+
"""Method to create ``FeatureSet`` of uniformly distributed vectors.
|
|
845
|
+
Golden Section Spiral points on a sphere algorithm.
|
|
846
|
+
|
|
847
|
+
http://www.softimageblog.com/archives/115
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
n: number of objects to be generated. Default 1000
|
|
851
|
+
name: name of dataset. Default is 'Default'
|
|
852
|
+
|
|
853
|
+
Example:
|
|
854
|
+
>>> v = vecset.uniform_gss(300)
|
|
855
|
+
>>> v.ortensor().eigenvalues()
|
|
856
|
+
(0.33335688569571587, 0.33332315115436933, 0.33331996314991513)
|
|
857
|
+
"""
|
|
858
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
859
|
+
inc = np.pi * (3 - np.sqrt(5))
|
|
860
|
+
off = 2 / n
|
|
861
|
+
k = np.arange(n)
|
|
862
|
+
y = k * off - 1 + (off / 2)
|
|
863
|
+
r = np.sqrt(1 - y * y)
|
|
864
|
+
phi = k * inc
|
|
865
|
+
dc = np.array([np.cos(phi) * r, y, np.sin(phi) * r]).T
|
|
866
|
+
return cls([dtype_cls(d) for d in dc], name=name)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
class LineationSet(Vector3Set):
|
|
870
|
+
"""
|
|
871
|
+
Class to store set of ``Lineation`` features
|
|
872
|
+
"""
|
|
873
|
+
|
|
874
|
+
__feature_type__ = "Lineation"
|
|
875
|
+
|
|
876
|
+
def __repr__(self):
|
|
877
|
+
return f"L({len(self)}) {self.name}"
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
class FoliationSet(Vector3Set):
|
|
881
|
+
"""
|
|
882
|
+
Class to store set of ``Foliation`` features
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
__feature_type__ = "Foliation"
|
|
886
|
+
|
|
887
|
+
def __repr__(self):
|
|
888
|
+
return f"S({len(self)}) {self.name}"
|
|
889
|
+
|
|
890
|
+
def dipvec(self):
|
|
891
|
+
"""Return ``FeatureSet`` object with plane dip vector."""
|
|
892
|
+
return Vector3Set([e.dipvec() for e in self], name=self.name)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class PairSet(FeatureSet):
|
|
896
|
+
"""
|
|
897
|
+
Class to store set of ``Pair`` features
|
|
898
|
+
"""
|
|
899
|
+
|
|
900
|
+
__feature_type__ = "Pair"
|
|
901
|
+
|
|
902
|
+
def __repr__(self):
|
|
903
|
+
return f"P({len(self)}) {self.name}"
|
|
904
|
+
|
|
905
|
+
def __array__(self, dtype=None, copy=None):
|
|
906
|
+
return np.array([np.array(p) for p in self.data], dtype=dtype)
|
|
907
|
+
|
|
908
|
+
@property
|
|
909
|
+
def fol(self):
|
|
910
|
+
"""Return Foliations of pairs as FoliationSet"""
|
|
911
|
+
return FoliationSet([e.fol for e in self], name=self.name)
|
|
912
|
+
|
|
913
|
+
@property
|
|
914
|
+
def fvec(self):
|
|
915
|
+
"""Return planar normal vectors of pairs as Vector3Set"""
|
|
916
|
+
return Vector3Set([e.fvec for e in self], name=self.name)
|
|
917
|
+
|
|
918
|
+
@property
|
|
919
|
+
def lin(self):
|
|
920
|
+
"""Return Lineation of pairs as LineationSet"""
|
|
921
|
+
return LineationSet([e.lin for e in self], name=self.name)
|
|
922
|
+
|
|
923
|
+
@property
|
|
924
|
+
def lvec(self):
|
|
925
|
+
"""Return lineation vectors of pairs as Vector3Set"""
|
|
926
|
+
return Vector3Set([e.lvec for e in self], name=self.name)
|
|
927
|
+
|
|
928
|
+
@property
|
|
929
|
+
def misfit(self):
|
|
930
|
+
"""Return array of misfits"""
|
|
931
|
+
return np.array([f.misfit for f in self])
|
|
932
|
+
|
|
933
|
+
@property
|
|
934
|
+
def rax(self):
|
|
935
|
+
"""
|
|
936
|
+
Return vectors perpendicular to both planar and linear parts of
|
|
937
|
+
pairs as Vector3Set
|
|
938
|
+
"""
|
|
939
|
+
return Vector3Set([e.rax for e in self], name=self.name)
|
|
940
|
+
|
|
941
|
+
def angle(self, other=None):
|
|
942
|
+
"""Return angles of all data in ``PairSet`` object
|
|
943
|
+
|
|
944
|
+
Without arguments it returns angles of all pairs in dataset.
|
|
945
|
+
If argument is ``PairSet`` of same length or single data object
|
|
946
|
+
element-wise angles are calculated.
|
|
947
|
+
"""
|
|
948
|
+
res = []
|
|
949
|
+
if other is None:
|
|
950
|
+
res = [
|
|
951
|
+
abs(
|
|
952
|
+
DeformationGradient3.from_two_pairs(
|
|
953
|
+
e, f, symmetry=True
|
|
954
|
+
).axisangle()[1]
|
|
955
|
+
)
|
|
956
|
+
for e, f in combinations(self.data, 2)
|
|
957
|
+
]
|
|
958
|
+
elif issubclass(type(other), PairSet):
|
|
959
|
+
res = [
|
|
960
|
+
abs(
|
|
961
|
+
DeformationGradient3.from_two_pairs(
|
|
962
|
+
e, f, symmetry=True
|
|
963
|
+
).axisangle()[1]
|
|
964
|
+
)
|
|
965
|
+
for e, f in zip(self, other)
|
|
966
|
+
]
|
|
967
|
+
elif issubclass(type(other), Pair):
|
|
968
|
+
res = [
|
|
969
|
+
abs(
|
|
970
|
+
DeformationGradient3.from_two_pairs(
|
|
971
|
+
e, other, symmetry=True
|
|
972
|
+
).axisangle()[1]
|
|
973
|
+
)
|
|
974
|
+
for e in self
|
|
975
|
+
]
|
|
976
|
+
else:
|
|
977
|
+
raise TypeError("Wrong argument type!")
|
|
978
|
+
return np.asarray(res)
|
|
979
|
+
|
|
980
|
+
@property
|
|
981
|
+
def ortensor(self):
|
|
982
|
+
"""Return Lisle (1989) orientation tensor ``OrientationTensor3`` of orientations
|
|
983
|
+
defined by pairs"""
|
|
984
|
+
return OrientationTensor3.from_pairs(self)
|
|
985
|
+
|
|
986
|
+
def label(self):
|
|
987
|
+
return str(self)
|
|
988
|
+
|
|
989
|
+
@classmethod
|
|
990
|
+
def random(cls, n=25):
|
|
991
|
+
"""Create PairSet of random pairs"""
|
|
992
|
+
return PairSet([Pair.random() for i in range(n)])
|
|
993
|
+
|
|
994
|
+
@classmethod
|
|
995
|
+
def from_csv(cls, filename, delimiter=",", facol=0, ficol=1, lacol=2, licol=3):
|
|
996
|
+
"""Read ``PairSet`` from csv file"""
|
|
997
|
+
|
|
998
|
+
from os.path import basename
|
|
999
|
+
import csv
|
|
1000
|
+
|
|
1001
|
+
with open(filename) as csvfile:
|
|
1002
|
+
has_header = csv.Sniffer().has_header(csvfile.read(1024))
|
|
1003
|
+
csvfile.seek(0)
|
|
1004
|
+
dialect = csv.Sniffer().sniff(csvfile.read(1024))
|
|
1005
|
+
csvfile.seek(0)
|
|
1006
|
+
if (
|
|
1007
|
+
isinstance(facol, int)
|
|
1008
|
+
and isinstance(ficol, int)
|
|
1009
|
+
and isinstance(lacol, int)
|
|
1010
|
+
and isinstance(licol, int)
|
|
1011
|
+
):
|
|
1012
|
+
if has_header:
|
|
1013
|
+
reader = csv.DictReader(csvfile, dialect=dialect)
|
|
1014
|
+
faname, finame = reader.fieldnames[facol], reader.fieldnames[ficol]
|
|
1015
|
+
laname, liname = reader.fieldnames[lacol], reader.fieldnames[licol]
|
|
1016
|
+
r = [
|
|
1017
|
+
(
|
|
1018
|
+
float(row[faname]),
|
|
1019
|
+
float(row[finame]),
|
|
1020
|
+
float(row[laname]),
|
|
1021
|
+
float(row[liname]),
|
|
1022
|
+
)
|
|
1023
|
+
for row in reader
|
|
1024
|
+
]
|
|
1025
|
+
else:
|
|
1026
|
+
reader = csv.reader(csvfile, dialect=dialect)
|
|
1027
|
+
r = [
|
|
1028
|
+
(
|
|
1029
|
+
float(row[facol]),
|
|
1030
|
+
float(row[ficol]),
|
|
1031
|
+
float(row[lacol]),
|
|
1032
|
+
float(row[licol]),
|
|
1033
|
+
)
|
|
1034
|
+
for row in reader
|
|
1035
|
+
]
|
|
1036
|
+
else:
|
|
1037
|
+
if has_header:
|
|
1038
|
+
reader = csv.DictReader(csvfile, dialect=dialect)
|
|
1039
|
+
r = [
|
|
1040
|
+
(
|
|
1041
|
+
float(row[facol]),
|
|
1042
|
+
float(row[ficol]),
|
|
1043
|
+
float(row[lacol]),
|
|
1044
|
+
float(row[licol]),
|
|
1045
|
+
)
|
|
1046
|
+
for row in reader
|
|
1047
|
+
]
|
|
1048
|
+
else:
|
|
1049
|
+
raise ValueError("No header line in CSV file...")
|
|
1050
|
+
|
|
1051
|
+
fazi, finc, lazi, linc = zip(*r)
|
|
1052
|
+
return cls.from_array(fazi, finc, lazi, linc, name=basename(filename))
|
|
1053
|
+
|
|
1054
|
+
def to_csv(self, filename, delimiter=","):
|
|
1055
|
+
"""Save ``PairSet`` object to csv file
|
|
1056
|
+
|
|
1057
|
+
Args:
|
|
1058
|
+
filename (str): name of CSV file to save.
|
|
1059
|
+
|
|
1060
|
+
Keyword Args:
|
|
1061
|
+
delimiter (str): values delimiter. Default ','
|
|
1062
|
+
|
|
1063
|
+
Note: Written values are rounded according to `ndigits` settings in apsg_conf
|
|
1064
|
+
|
|
1065
|
+
"""
|
|
1066
|
+
import csv
|
|
1067
|
+
|
|
1068
|
+
n = apsg_conf["ndigits"]
|
|
1069
|
+
|
|
1070
|
+
with open(filename, "w", newline="") as csvfile:
|
|
1071
|
+
fieldnames = ["azi", "inc", "lazi", "linc"]
|
|
1072
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
1073
|
+
writer.writeheader()
|
|
1074
|
+
for dt in self:
|
|
1075
|
+
fazi, finc = dt.fol.geo
|
|
1076
|
+
lazi, linc = dt.lin.geo
|
|
1077
|
+
writer.writerow(
|
|
1078
|
+
{
|
|
1079
|
+
"fazi": round(fazi, n),
|
|
1080
|
+
"finc": round(finc, n),
|
|
1081
|
+
"lazi": round(lazi, n),
|
|
1082
|
+
"linc": round(linc, n),
|
|
1083
|
+
}
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
@classmethod
|
|
1087
|
+
def from_array(cls, fazis, fincs, lazis, lincs, name="Default"):
|
|
1088
|
+
"""Create ``PairSet`` from arrays of azimuths and inclinations
|
|
1089
|
+
|
|
1090
|
+
Args:
|
|
1091
|
+
azis: list or array of azimuths
|
|
1092
|
+
incs: list or array of inclinations
|
|
1093
|
+
|
|
1094
|
+
Keyword Args:
|
|
1095
|
+
name: name of ``PairSet`` object. Default is 'Default'
|
|
1096
|
+
"""
|
|
1097
|
+
|
|
1098
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
1099
|
+
return cls(
|
|
1100
|
+
[
|
|
1101
|
+
dtype_cls(fazi, finc, lazi, linc)
|
|
1102
|
+
for fazi, finc, lazi, linc in zip(fazis, fincs, lazis, lincs)
|
|
1103
|
+
],
|
|
1104
|
+
name=name,
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
class FaultSet(PairSet):
|
|
1109
|
+
"""
|
|
1110
|
+
Class to store set of ``Fault`` features
|
|
1111
|
+
"""
|
|
1112
|
+
|
|
1113
|
+
__feature_type__ = "Fault"
|
|
1114
|
+
|
|
1115
|
+
def __repr__(self):
|
|
1116
|
+
return f"F({len(self)}) {self.name}"
|
|
1117
|
+
|
|
1118
|
+
@property
|
|
1119
|
+
def sense(self):
|
|
1120
|
+
"""Return array of sense values"""
|
|
1121
|
+
return np.array([f.sense for f in self])
|
|
1122
|
+
|
|
1123
|
+
@property
|
|
1124
|
+
def p_vector(self, ptangle=90):
|
|
1125
|
+
"""Return p-axes of FaultSet as Vector3Set"""
|
|
1126
|
+
return Vector3Set([e.p_vector(ptangle) for e in self], name=self.name)
|
|
1127
|
+
|
|
1128
|
+
@property
|
|
1129
|
+
def t_vector(self, ptangle=90):
|
|
1130
|
+
"""Return t-axes of FaultSet as Vector3Set"""
|
|
1131
|
+
return Vector3Set([e.t_vector(ptangle) for e in self], name=self.name)
|
|
1132
|
+
|
|
1133
|
+
@property
|
|
1134
|
+
def p(self):
|
|
1135
|
+
"""Return p-axes of FaultSet as LineationSet"""
|
|
1136
|
+
return LineationSet([e.p for e in self], name=self.name + "-P")
|
|
1137
|
+
|
|
1138
|
+
@property
|
|
1139
|
+
def t(self):
|
|
1140
|
+
"""Return t-axes of FaultSet as LineationSet"""
|
|
1141
|
+
return LineationSet([e.t for e in self], name=self.name + "-T")
|
|
1142
|
+
|
|
1143
|
+
@property
|
|
1144
|
+
def m(self):
|
|
1145
|
+
"""Return m-planes of FaultSet as FoliationSet"""
|
|
1146
|
+
return FoliationSet([e.m for e in self], name=self.name + "-M")
|
|
1147
|
+
|
|
1148
|
+
@property
|
|
1149
|
+
def d(self):
|
|
1150
|
+
"""Return dihedra planes of FaultSet as FoliationSet"""
|
|
1151
|
+
return FoliationSet([e.d for e in self], name=self.name + "-D")
|
|
1152
|
+
|
|
1153
|
+
def angle(self, other=None):
|
|
1154
|
+
"""Return angles of all data in ``FaultSet`` object
|
|
1155
|
+
|
|
1156
|
+
Without arguments it returns angles of all pairs in dataset.
|
|
1157
|
+
If argument is ``FaultSet`` of same length or single data object
|
|
1158
|
+
element-wise angles are calculated.
|
|
1159
|
+
"""
|
|
1160
|
+
res = []
|
|
1161
|
+
if other is None:
|
|
1162
|
+
res = [
|
|
1163
|
+
abs(
|
|
1164
|
+
DeformationGradient3.from_two_pairs(
|
|
1165
|
+
e, f, symmetry=False
|
|
1166
|
+
).axisangle()[1]
|
|
1167
|
+
)
|
|
1168
|
+
for e, f in combinations(self.data, 2)
|
|
1169
|
+
]
|
|
1170
|
+
elif issubclass(type(other), FaultSet):
|
|
1171
|
+
res = [
|
|
1172
|
+
abs(
|
|
1173
|
+
DeformationGradient3.from_two_pairs(
|
|
1174
|
+
e, f, symmetry=False
|
|
1175
|
+
).axisangle()[1]
|
|
1176
|
+
)
|
|
1177
|
+
for e, f in zip(self, other)
|
|
1178
|
+
]
|
|
1179
|
+
elif issubclass(type(other), Fault):
|
|
1180
|
+
res = [
|
|
1181
|
+
abs(
|
|
1182
|
+
DeformationGradient3.from_two_pairs(
|
|
1183
|
+
e, other, symmetry=False
|
|
1184
|
+
).axisangle()[1]
|
|
1185
|
+
)
|
|
1186
|
+
for e in self
|
|
1187
|
+
]
|
|
1188
|
+
else:
|
|
1189
|
+
raise TypeError("Wrong argument type!")
|
|
1190
|
+
return np.asarray(res)
|
|
1191
|
+
|
|
1192
|
+
@classmethod
|
|
1193
|
+
def random(cls, n=25):
|
|
1194
|
+
"""Create PairSet of random pairs"""
|
|
1195
|
+
return FaultSet([Fault.random() for i in range(n)])
|
|
1196
|
+
|
|
1197
|
+
@classmethod
|
|
1198
|
+
def from_csv(
|
|
1199
|
+
cls, filename, delimiter=",", facol=0, ficol=1, lacol=2, licol=3, scol=4
|
|
1200
|
+
):
|
|
1201
|
+
"""Read ``FaultSet`` from csv file"""
|
|
1202
|
+
|
|
1203
|
+
from os.path import basename
|
|
1204
|
+
import csv
|
|
1205
|
+
|
|
1206
|
+
with open(filename) as csvfile:
|
|
1207
|
+
has_header = csv.Sniffer().has_header(csvfile.read(1024))
|
|
1208
|
+
csvfile.seek(0)
|
|
1209
|
+
dialect = csv.Sniffer().sniff(csvfile.read(1024))
|
|
1210
|
+
csvfile.seek(0)
|
|
1211
|
+
if (
|
|
1212
|
+
isinstance(facol, int)
|
|
1213
|
+
and isinstance(ficol, int)
|
|
1214
|
+
and isinstance(lacol, int)
|
|
1215
|
+
and isinstance(licol, int)
|
|
1216
|
+
and isinstance(scol, int)
|
|
1217
|
+
):
|
|
1218
|
+
if has_header:
|
|
1219
|
+
reader = csv.DictReader(csvfile, dialect=dialect)
|
|
1220
|
+
faname, finame = reader.fieldnames[facol], reader.fieldnames[ficol]
|
|
1221
|
+
laname, liname = reader.fieldnames[lacol], reader.fieldnames[licol]
|
|
1222
|
+
sname = reader.fieldnames[scol]
|
|
1223
|
+
r = [
|
|
1224
|
+
(
|
|
1225
|
+
float(row[faname]),
|
|
1226
|
+
float(row[finame]),
|
|
1227
|
+
float(row[laname]),
|
|
1228
|
+
float(row[liname]),
|
|
1229
|
+
int(row[sname]),
|
|
1230
|
+
)
|
|
1231
|
+
for row in reader
|
|
1232
|
+
]
|
|
1233
|
+
else:
|
|
1234
|
+
reader = csv.reader(csvfile, dialect=dialect)
|
|
1235
|
+
r = [
|
|
1236
|
+
(
|
|
1237
|
+
float(row[facol]),
|
|
1238
|
+
float(row[ficol]),
|
|
1239
|
+
float(row[lacol]),
|
|
1240
|
+
float(row[licol]),
|
|
1241
|
+
int(row[scol]),
|
|
1242
|
+
)
|
|
1243
|
+
for row in reader
|
|
1244
|
+
]
|
|
1245
|
+
else:
|
|
1246
|
+
if has_header:
|
|
1247
|
+
reader = csv.DictReader(csvfile, dialect=dialect)
|
|
1248
|
+
r = [
|
|
1249
|
+
(
|
|
1250
|
+
float(row[facol]),
|
|
1251
|
+
float(row[ficol]),
|
|
1252
|
+
float(row[lacol]),
|
|
1253
|
+
float(row[licol]),
|
|
1254
|
+
int(row[scol]),
|
|
1255
|
+
)
|
|
1256
|
+
for row in reader
|
|
1257
|
+
]
|
|
1258
|
+
else:
|
|
1259
|
+
raise ValueError("No header line in CSV file...")
|
|
1260
|
+
|
|
1261
|
+
fazi, finc, lazi, linc, sense = zip(*r)
|
|
1262
|
+
return cls.from_array(fazi, finc, lazi, linc, sense, name=basename(filename))
|
|
1263
|
+
|
|
1264
|
+
def to_csv(self, filename, delimiter=","):
|
|
1265
|
+
"""Save ``FaultSet`` object to csv file
|
|
1266
|
+
|
|
1267
|
+
Args:
|
|
1268
|
+
filename (str): name of CSV file to save.
|
|
1269
|
+
|
|
1270
|
+
Keyword Args:
|
|
1271
|
+
delimiter (str): values delimiter. Default ','
|
|
1272
|
+
|
|
1273
|
+
Note: Written values are rounded according to `ndigits` settings in apsg_conf
|
|
1274
|
+
|
|
1275
|
+
"""
|
|
1276
|
+
import csv
|
|
1277
|
+
|
|
1278
|
+
n = apsg_conf["ndigits"]
|
|
1279
|
+
|
|
1280
|
+
with open(filename, "w", newline="") as csvfile:
|
|
1281
|
+
fieldnames = ["fazi", "finc", "lazi", "linc", "sense"]
|
|
1282
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
1283
|
+
writer.writeheader()
|
|
1284
|
+
for dt in self:
|
|
1285
|
+
fazi, finc = dt.fol.geo
|
|
1286
|
+
lazi, linc = dt.lin.geo
|
|
1287
|
+
writer.writerow(
|
|
1288
|
+
{
|
|
1289
|
+
"fazi": round(fazi, n),
|
|
1290
|
+
"finc": round(finc, n),
|
|
1291
|
+
"lazi": round(lazi, n),
|
|
1292
|
+
"linc": round(linc, n),
|
|
1293
|
+
"sense": dt.sense,
|
|
1294
|
+
}
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
@classmethod
|
|
1298
|
+
def from_array(cls, fazis, fincs, lazis, lincs, senses, name="Default"):
|
|
1299
|
+
"""Create ``PairSet`` from arrays of azimuths and inclinations
|
|
1300
|
+
|
|
1301
|
+
Args:
|
|
1302
|
+
azis: list or array of azimuths
|
|
1303
|
+
incs: list or array of inclinations
|
|
1304
|
+
|
|
1305
|
+
Keyword Args:
|
|
1306
|
+
name: name of ``PairSet`` object. Default is 'Default'
|
|
1307
|
+
"""
|
|
1308
|
+
|
|
1309
|
+
dtype_cls = getattr(sys.modules[__name__], cls.__feature_type__)
|
|
1310
|
+
return cls(
|
|
1311
|
+
[
|
|
1312
|
+
dtype_cls(fazi, finc, lazi, linc, sense)
|
|
1313
|
+
for fazi, finc, lazi, linc, sense in zip(
|
|
1314
|
+
fazis, fincs, lazis, lincs, senses
|
|
1315
|
+
)
|
|
1316
|
+
],
|
|
1317
|
+
name=name,
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
class ConeSet(FeatureSet):
|
|
1322
|
+
"""
|
|
1323
|
+
Class to store set of ``Cone`` features
|
|
1324
|
+
"""
|
|
1325
|
+
|
|
1326
|
+
__feature_type__ = "Cone"
|
|
1327
|
+
|
|
1328
|
+
def __repr__(self):
|
|
1329
|
+
return f"C({len(self)}) {self.name}"
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
class EllipseSet(FeatureSet):
|
|
1333
|
+
"""
|
|
1334
|
+
Class to store set of ``Ellipse`` features
|
|
1335
|
+
"""
|
|
1336
|
+
|
|
1337
|
+
__feature_type__ = "Ellipse"
|
|
1338
|
+
|
|
1339
|
+
@property
|
|
1340
|
+
def S1(self) -> np.ndarray:
|
|
1341
|
+
"""
|
|
1342
|
+
Return the array of maximum principal stretches.
|
|
1343
|
+
"""
|
|
1344
|
+
return np.array([e.S1 for e in self])
|
|
1345
|
+
|
|
1346
|
+
@property
|
|
1347
|
+
def S2(self) -> np.ndarray:
|
|
1348
|
+
"""
|
|
1349
|
+
Return the array of minimum principal stretches.
|
|
1350
|
+
"""
|
|
1351
|
+
return np.array([e.S2 for e in self])
|
|
1352
|
+
|
|
1353
|
+
@property
|
|
1354
|
+
def e1(self) -> np.ndarray:
|
|
1355
|
+
"""
|
|
1356
|
+
Return the maximum natural principal strains.
|
|
1357
|
+
"""
|
|
1358
|
+
return np.array([e.e1 for e in self])
|
|
1359
|
+
|
|
1360
|
+
@property
|
|
1361
|
+
def e2(self) -> np.ndarray:
|
|
1362
|
+
"""
|
|
1363
|
+
Return the array of minimum natural principal strains.
|
|
1364
|
+
"""
|
|
1365
|
+
return np.array([e.e2 for e in self])
|
|
1366
|
+
|
|
1367
|
+
@property
|
|
1368
|
+
def ar(self) -> np.ndarray:
|
|
1369
|
+
"""
|
|
1370
|
+
Return the array of axial ratios.
|
|
1371
|
+
"""
|
|
1372
|
+
return np.array([e.ar for e in self])
|
|
1373
|
+
|
|
1374
|
+
@property
|
|
1375
|
+
def orientation(self) -> np.ndarray:
|
|
1376
|
+
"""
|
|
1377
|
+
Return the array of orientations of the maximum eigenvector.
|
|
1378
|
+
"""
|
|
1379
|
+
return np.array([e.orientation for e in self])
|
|
1380
|
+
|
|
1381
|
+
@property
|
|
1382
|
+
def e12(self) -> np.ndarray:
|
|
1383
|
+
"""
|
|
1384
|
+
Return the array of differences between natural principal strains.
|
|
1385
|
+
"""
|
|
1386
|
+
return np.array([e.e12 for e in self])
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
class OrientationTensor2Set(EllipseSet):
|
|
1390
|
+
"""
|
|
1391
|
+
Class to store set of ``OrientationTensor2`` features
|
|
1392
|
+
"""
|
|
1393
|
+
|
|
1394
|
+
__feature_type__ = "OrientationTensor2"
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
class EllipsoidSet(FeatureSet):
|
|
1398
|
+
"""
|
|
1399
|
+
Class to store set of ``Ellipsoid`` features
|
|
1400
|
+
"""
|
|
1401
|
+
|
|
1402
|
+
__feature_type__ = "Ellipsoid"
|
|
1403
|
+
|
|
1404
|
+
@property
|
|
1405
|
+
def strength(self) -> np.ndarray:
|
|
1406
|
+
"""
|
|
1407
|
+
Return the array of the Woodcock strength.
|
|
1408
|
+
"""
|
|
1409
|
+
return np.array([e.strength for e in self])
|
|
1410
|
+
|
|
1411
|
+
@property
|
|
1412
|
+
def shape(self) -> np.ndarray:
|
|
1413
|
+
"""
|
|
1414
|
+
Return the array of the Woodcock shape.
|
|
1415
|
+
"""
|
|
1416
|
+
return np.array([e.shape for e in self])
|
|
1417
|
+
|
|
1418
|
+
@property
|
|
1419
|
+
def S1(self) -> np.ndarray:
|
|
1420
|
+
"""
|
|
1421
|
+
Return the array of maximum principal stretches.
|
|
1422
|
+
"""
|
|
1423
|
+
return np.array([e.S1 for e in self])
|
|
1424
|
+
|
|
1425
|
+
@property
|
|
1426
|
+
def S2(self) -> np.ndarray:
|
|
1427
|
+
"""
|
|
1428
|
+
Return the array of middle principal stretches.
|
|
1429
|
+
"""
|
|
1430
|
+
return np.array([e.S2 for e in self])
|
|
1431
|
+
|
|
1432
|
+
@property
|
|
1433
|
+
def S3(self) -> np.ndarray:
|
|
1434
|
+
"""
|
|
1435
|
+
Return the array of minimum principal stretches.
|
|
1436
|
+
"""
|
|
1437
|
+
return np.array([e.S3 for e in self])
|
|
1438
|
+
|
|
1439
|
+
@property
|
|
1440
|
+
def e1(self) -> np.ndarray:
|
|
1441
|
+
"""
|
|
1442
|
+
Return the array of the maximum natural principal strain.
|
|
1443
|
+
"""
|
|
1444
|
+
return np.array([e.e1 for e in self])
|
|
1445
|
+
|
|
1446
|
+
@property
|
|
1447
|
+
def e2(self) -> np.ndarray:
|
|
1448
|
+
"""
|
|
1449
|
+
Return the array of the middle natural principal strain.
|
|
1450
|
+
"""
|
|
1451
|
+
return np.array([e.e2 for e in self])
|
|
1452
|
+
|
|
1453
|
+
@property
|
|
1454
|
+
def e3(self) -> np.ndarray:
|
|
1455
|
+
"""
|
|
1456
|
+
Return the array of the minimum natural principal strain.
|
|
1457
|
+
"""
|
|
1458
|
+
return np.array([e.e3 for e in self])
|
|
1459
|
+
|
|
1460
|
+
@property
|
|
1461
|
+
def Rxy(self) -> np.ndarray:
|
|
1462
|
+
"""
|
|
1463
|
+
Return the array of the Rxy ratios.
|
|
1464
|
+
"""
|
|
1465
|
+
return np.array([e.Rxy for e in self])
|
|
1466
|
+
|
|
1467
|
+
@property
|
|
1468
|
+
def Ryz(self) -> np.ndarray:
|
|
1469
|
+
"""
|
|
1470
|
+
Return the array of the Ryz ratios.
|
|
1471
|
+
"""
|
|
1472
|
+
return np.array([e.Ryz for e in self])
|
|
1473
|
+
|
|
1474
|
+
@property
|
|
1475
|
+
def e12(self) -> np.ndarray:
|
|
1476
|
+
"""
|
|
1477
|
+
Return the array of the e1 - e2 values.
|
|
1478
|
+
"""
|
|
1479
|
+
return np.array([e.e12 for e in self])
|
|
1480
|
+
|
|
1481
|
+
@property
|
|
1482
|
+
def e13(self) -> np.ndarray:
|
|
1483
|
+
"""
|
|
1484
|
+
Return the array of the e1 - e3 values.
|
|
1485
|
+
"""
|
|
1486
|
+
return np.array([e.e13 for e in self])
|
|
1487
|
+
|
|
1488
|
+
@property
|
|
1489
|
+
def e23(self) -> np.ndarray:
|
|
1490
|
+
"""
|
|
1491
|
+
Return the array of the e2 - e3 values.
|
|
1492
|
+
"""
|
|
1493
|
+
return np.array([e.e23 for e in self])
|
|
1494
|
+
|
|
1495
|
+
@property
|
|
1496
|
+
def k(self) -> np.ndarray:
|
|
1497
|
+
"""
|
|
1498
|
+
Return the array of the strain symmetries.
|
|
1499
|
+
"""
|
|
1500
|
+
return np.array([e.k for e in self])
|
|
1501
|
+
|
|
1502
|
+
@property
|
|
1503
|
+
def d(self) -> np.ndarray:
|
|
1504
|
+
"""
|
|
1505
|
+
Return the array of the strain intensities.
|
|
1506
|
+
"""
|
|
1507
|
+
return np.array([e.d for e in self])
|
|
1508
|
+
|
|
1509
|
+
@property
|
|
1510
|
+
def K(self) -> np.ndarray:
|
|
1511
|
+
"""
|
|
1512
|
+
Return the array of the strain symmetries K (Ramsay, 1983).
|
|
1513
|
+
"""
|
|
1514
|
+
return np.array([e.K for e in self])
|
|
1515
|
+
|
|
1516
|
+
@property
|
|
1517
|
+
def D(self) -> np.ndarray:
|
|
1518
|
+
"""
|
|
1519
|
+
Return the array of the strain intensities D (Ramsay, 1983)..
|
|
1520
|
+
"""
|
|
1521
|
+
return np.array([e.D for e in self])
|
|
1522
|
+
|
|
1523
|
+
@property
|
|
1524
|
+
def r(self) -> np.ndarray:
|
|
1525
|
+
"""
|
|
1526
|
+
Return the array of the strain intensities (Watterson, 1968).
|
|
1527
|
+
"""
|
|
1528
|
+
return np.array([e.r for e in self])
|
|
1529
|
+
|
|
1530
|
+
@property
|
|
1531
|
+
def goct(self) -> np.ndarray:
|
|
1532
|
+
"""
|
|
1533
|
+
Return the array of the natural octahedral unit shears (Nadai, 1963).
|
|
1534
|
+
"""
|
|
1535
|
+
return np.array([e.goct for e in self])
|
|
1536
|
+
|
|
1537
|
+
@property
|
|
1538
|
+
def eoct(self) -> np.ndarray:
|
|
1539
|
+
"""
|
|
1540
|
+
Return the array of the natural octahedral unit strains (Nadai, 1963).
|
|
1541
|
+
"""
|
|
1542
|
+
return np.array([e.eoct for e in self])
|
|
1543
|
+
|
|
1544
|
+
@property
|
|
1545
|
+
def lode(self) -> np.ndarray:
|
|
1546
|
+
"""
|
|
1547
|
+
Return the array of Lode parameters (Lode, 1926).
|
|
1548
|
+
"""
|
|
1549
|
+
return np.array([e.lode for e in self])
|
|
1550
|
+
|
|
1551
|
+
@property
|
|
1552
|
+
def P(self) -> np.ndarray:
|
|
1553
|
+
"""
|
|
1554
|
+
Return the array of Point indexes (Vollmer, 1990).
|
|
1555
|
+
"""
|
|
1556
|
+
return np.array([e.P for e in self])
|
|
1557
|
+
|
|
1558
|
+
@property
|
|
1559
|
+
def G(self) -> np.ndarray:
|
|
1560
|
+
"""
|
|
1561
|
+
Return the array of Girdle indexes (Vollmer, 1990).
|
|
1562
|
+
"""
|
|
1563
|
+
return np.array([e.G for e in self])
|
|
1564
|
+
|
|
1565
|
+
@property
|
|
1566
|
+
def R(self) -> np.ndarray:
|
|
1567
|
+
"""
|
|
1568
|
+
Return the array of Random indexes (Vollmer, 1990).
|
|
1569
|
+
"""
|
|
1570
|
+
return np.array([e.R for e in self])
|
|
1571
|
+
|
|
1572
|
+
@property
|
|
1573
|
+
def B(self) -> np.ndarray:
|
|
1574
|
+
"""
|
|
1575
|
+
Return the array of Cylindricity indexes (Vollmer, 1990).
|
|
1576
|
+
"""
|
|
1577
|
+
return np.array([e.B for e in self])
|
|
1578
|
+
|
|
1579
|
+
@property
|
|
1580
|
+
def Intensity(self) -> np.ndarray:
|
|
1581
|
+
"""
|
|
1582
|
+
Return the array of Intensity indexes (Lisle, 1985).
|
|
1583
|
+
"""
|
|
1584
|
+
return np.array([e.Intensity for e in self])
|
|
1585
|
+
|
|
1586
|
+
@property
|
|
1587
|
+
def aMAD_l(self) -> np.ndarray:
|
|
1588
|
+
"""
|
|
1589
|
+
Return approximate angular deviation from the major axis along E1.
|
|
1590
|
+
"""
|
|
1591
|
+
return np.array([e.aMAD_l for e in self])
|
|
1592
|
+
|
|
1593
|
+
@property
|
|
1594
|
+
def aMAD_p(self) -> np.ndarray:
|
|
1595
|
+
"""
|
|
1596
|
+
Return approximate deviation from the plane normal to E3.
|
|
1597
|
+
"""
|
|
1598
|
+
return np.array([e.aMAD_p for e in self])
|
|
1599
|
+
|
|
1600
|
+
@property
|
|
1601
|
+
def aMAD(self) -> np.ndarray:
|
|
1602
|
+
"""
|
|
1603
|
+
Return approximate deviation according to the shape
|
|
1604
|
+
"""
|
|
1605
|
+
return np.array([e.aMAD for e in self])
|
|
1606
|
+
|
|
1607
|
+
@property
|
|
1608
|
+
def MAD_l(self) -> np.ndarray:
|
|
1609
|
+
"""
|
|
1610
|
+
Return maximum angular deviation (MAD) of linearly distributed vectors.
|
|
1611
|
+
Kirschvink 1980
|
|
1612
|
+
"""
|
|
1613
|
+
return np.array([e.MAD_l for e in self])
|
|
1614
|
+
|
|
1615
|
+
@property
|
|
1616
|
+
def MAD_p(self) -> np.ndarray:
|
|
1617
|
+
"""
|
|
1618
|
+
Return maximum angular deviation (MAD) of planarly distributed vectors.
|
|
1619
|
+
Kirschvink 1980
|
|
1620
|
+
"""
|
|
1621
|
+
return np.array([e.MAD_p for e in self])
|
|
1622
|
+
|
|
1623
|
+
@property
|
|
1624
|
+
def MAD(self) -> np.ndarray:
|
|
1625
|
+
"""
|
|
1626
|
+
Return approximate deviation according to shape
|
|
1627
|
+
"""
|
|
1628
|
+
return np.array([e.MAD for e in self])
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
class OrientationTensor3Set(EllipsoidSet):
|
|
1632
|
+
"""
|
|
1633
|
+
Class to store set of ``OrientationTensor3`` features
|
|
1634
|
+
"""
|
|
1635
|
+
|
|
1636
|
+
__feature_type__ = "OrientationTensor3"
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
class ClusterSet(object):
|
|
1640
|
+
"""
|
|
1641
|
+
Provides a hierarchical clustering using `scipy.cluster` routines.
|
|
1642
|
+
The distance matrix is calculated as an angle between features, where ``Foliation``
|
|
1643
|
+
and ``Lineation`` use axial angles while ``Vector3`` uses direction angles.
|
|
1644
|
+
|
|
1645
|
+
Args:
|
|
1646
|
+
azi (float): plunge direction of linear feature in degrees
|
|
1647
|
+
inc (float): plunge of linear feature in degrees
|
|
1648
|
+
|
|
1649
|
+
Keyword Args:
|
|
1650
|
+
maxclust (int): Desired number of clusters. Default 2
|
|
1651
|
+
angle (float): Forms flat clusters so that the original observations in each
|
|
1652
|
+
cluster have no greater angle. Default is None to use maxclust criterion.
|
|
1653
|
+
method (str): Method for calculating the distance between the newly formed
|
|
1654
|
+
cluster and observations. Default is 'average' for UPGMA algorithm
|
|
1655
|
+
|
|
1656
|
+
"""
|
|
1657
|
+
|
|
1658
|
+
def __init__(self, d, **kwargs):
|
|
1659
|
+
assert (
|
|
1660
|
+
isinstance(d, Vector2Set)
|
|
1661
|
+
or isinstance(d, Vector3Set)
|
|
1662
|
+
or isinstance(d, PairSet)
|
|
1663
|
+
), "Only vec2set or vecset could be clustered"
|
|
1664
|
+
self.data = d.copy()
|
|
1665
|
+
self.maxclust = kwargs.get("maxclust", 2)
|
|
1666
|
+
self.angle = kwargs.get("angle", None)
|
|
1667
|
+
self.method = kwargs.get("method", "average")
|
|
1668
|
+
self.pdist = self.data.angle()
|
|
1669
|
+
self.linkage()
|
|
1670
|
+
self.cluster()
|
|
1671
|
+
|
|
1672
|
+
def __repr__(self):
|
|
1673
|
+
info = f"Already {len(self.groups)} clusters created."
|
|
1674
|
+
if self.angle is not None:
|
|
1675
|
+
crit = f"Criterion: Angle\nSettings: distance={self.angle:.4g}\n"
|
|
1676
|
+
else:
|
|
1677
|
+
crit = f"Criterion: Maxclust\nSettings: maxclust={self.maxclust:.4g}\n"
|
|
1678
|
+
return (
|
|
1679
|
+
"ClusterSet\n"
|
|
1680
|
+
+ f"Number of data: {len(self.data)}\n"
|
|
1681
|
+
+ f"Linkage method: {self.method}\n"
|
|
1682
|
+
+ crit
|
|
1683
|
+
+ info
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
def cluster(self, **kwargs):
|
|
1687
|
+
"""Do clustering on data
|
|
1688
|
+
|
|
1689
|
+
Result is stored as tuple of Groups in ``groups`` property.
|
|
1690
|
+
|
|
1691
|
+
Keyword Args:
|
|
1692
|
+
maxclust: number of clusters
|
|
1693
|
+
distance: maximum cophenetic distance in clusters
|
|
1694
|
+
"""
|
|
1695
|
+
|
|
1696
|
+
self.maxclust = kwargs.get("maxclust", self.maxclust)
|
|
1697
|
+
self.angle = kwargs.get("angle", self.angle)
|
|
1698
|
+
|
|
1699
|
+
if self.angle is not None:
|
|
1700
|
+
self.idx = fcluster(self.Z, self.angle, criterion="distance")
|
|
1701
|
+
else:
|
|
1702
|
+
self.idx = fcluster(self.Z, self.maxclust, criterion="maxclust")
|
|
1703
|
+
self.groups = tuple(
|
|
1704
|
+
self.data[np.flatnonzero(self.idx == c)] for c in np.unique(self.idx)
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
def linkage(self, **kwargs):
|
|
1708
|
+
"""Do linkage of distance matrix
|
|
1709
|
+
|
|
1710
|
+
Keyword Args:
|
|
1711
|
+
method: The linkage algorithm to use
|
|
1712
|
+
"""
|
|
1713
|
+
|
|
1714
|
+
self.method = kwargs.get("method", self.method)
|
|
1715
|
+
self.Z = linkage(self.pdist, method=self.method, metric=angle_metric)
|
|
1716
|
+
|
|
1717
|
+
def dendrogram(self, **kwargs):
|
|
1718
|
+
"""Show dendrogram
|
|
1719
|
+
|
|
1720
|
+
See ``scipy.cluster.hierarchy.dendrogram`` for possible kwargs.
|
|
1721
|
+
"""
|
|
1722
|
+
|
|
1723
|
+
fig, ax = plt.subplots(figsize=apsg_conf["figsize"])
|
|
1724
|
+
dendrogram(self.Z, ax=ax, **kwargs)
|
|
1725
|
+
plt.show()
|
|
1726
|
+
|
|
1727
|
+
def elbow(self, no_plot=False, n=None):
|
|
1728
|
+
"""Plot within groups variance vs. number of clusters.
|
|
1729
|
+
Elbow criterion could be used to determine number of clusters.
|
|
1730
|
+
"""
|
|
1731
|
+
|
|
1732
|
+
if n is None:
|
|
1733
|
+
idx = fcluster(self.Z, len(self.data), criterion="maxclust")
|
|
1734
|
+
nclust = list(np.arange(1, np.sqrt(idx.max() / 2) + 1, dtype=int))
|
|
1735
|
+
else:
|
|
1736
|
+
nclust = list(np.arange(1, n + 1, dtype=int))
|
|
1737
|
+
within_grp_var = []
|
|
1738
|
+
mean_var = []
|
|
1739
|
+
for n in nclust:
|
|
1740
|
+
idx = fcluster(self.Z, n, criterion="maxclust")
|
|
1741
|
+
grp = [np.flatnonzero(idx == c) for c in np.unique(idx)]
|
|
1742
|
+
var = [100 * self.data[ix].var() for ix in grp]
|
|
1743
|
+
within_grp_var.append(var)
|
|
1744
|
+
mean_var.append(np.mean(var))
|
|
1745
|
+
if not no_plot:
|
|
1746
|
+
fig, ax = plt.subplots(figsize=apsg_conf["figsize"])
|
|
1747
|
+
ax.boxplot(within_grp_var, positions=nclust)
|
|
1748
|
+
ax.plot(nclust, mean_var, "k")
|
|
1749
|
+
ax.set_xlabel("Number of clusters")
|
|
1750
|
+
ax.set_ylabel("Variance")
|
|
1751
|
+
ax.set_title("Within-groups variance vs. number of clusters")
|
|
1752
|
+
plt.show()
|
|
1753
|
+
else:
|
|
1754
|
+
return nclust, within_grp_var
|
|
1755
|
+
|
|
1756
|
+
@property
|
|
1757
|
+
def R(self):
|
|
1758
|
+
"""Return group of clusters resultants."""
|
|
1759
|
+
return type(self.data)([group.R() for group in self.groups])
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
def G(lst, name="Default"):
|
|
1763
|
+
"""
|
|
1764
|
+
Function to create appropriate container (FeatueSet) from list of features.
|
|
1765
|
+
|
|
1766
|
+
Args:
|
|
1767
|
+
lst (list): Homogeneous list of objects of ``Vector2``, ``Vector3``,
|
|
1768
|
+
``Lineation``, ``Foliation``, ``Pair``, ``Cone``, ``Ellipse``
|
|
1769
|
+
or ``OrientationTensor3``.
|
|
1770
|
+
|
|
1771
|
+
Keyword Args:
|
|
1772
|
+
name (str): name of feature set. Default `Default`
|
|
1773
|
+
|
|
1774
|
+
Example:
|
|
1775
|
+
>>> fols = [fol(120,30), fol(130, 40), fol(126, 37)]
|
|
1776
|
+
>>> f = G(fols)
|
|
1777
|
+
"""
|
|
1778
|
+
if hasattr(lst, "__len__"):
|
|
1779
|
+
dtype_cls = type(lst[0])
|
|
1780
|
+
assert all([isinstance(obj, dtype_cls) for obj in lst])
|
|
1781
|
+
if dtype_cls is Vector3:
|
|
1782
|
+
return Vector3Set(lst, name=name)
|
|
1783
|
+
elif dtype_cls is Vector2:
|
|
1784
|
+
return Vector2Set(lst, name=name)
|
|
1785
|
+
elif dtype_cls is Lineation:
|
|
1786
|
+
return LineationSet(lst, name=name)
|
|
1787
|
+
elif dtype_cls is Foliation:
|
|
1788
|
+
return FoliationSet(lst, name=name)
|
|
1789
|
+
elif dtype_cls is Pair:
|
|
1790
|
+
return PairSet(lst, name=name)
|
|
1791
|
+
elif dtype_cls is Fault:
|
|
1792
|
+
return FaultSet(lst, name=name)
|
|
1793
|
+
elif dtype_cls is Cone:
|
|
1794
|
+
return ConeSet(lst, name=name)
|
|
1795
|
+
elif dtype_cls is Ellipsoid:
|
|
1796
|
+
return EllipsoidSet(lst, name=name)
|
|
1797
|
+
elif dtype_cls is OrientationTensor3:
|
|
1798
|
+
return OrientationTensor3Set(lst, name=name)
|
|
1799
|
+
elif dtype_cls is Ellipse:
|
|
1800
|
+
return EllipseSet(lst, name=name)
|
|
1801
|
+
elif dtype_cls is OrientationTensor2:
|
|
1802
|
+
return OrientationTensor2Set(lst, name=name)
|
|
1803
|
+
else:
|
|
1804
|
+
raise TypeError("Wrong datatype to create FeatureSet")
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
def angle_metric(u, v):
|
|
1808
|
+
return np.degrees(np.arccos(np.abs(np.dot(u, v))))
|