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.
@@ -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))))