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,702 @@
1
+ import warnings
2
+ import numpy as np
3
+
4
+ from apsg.helpers._notation import (
5
+ geo2vec_planar,
6
+ vec2geo_planar,
7
+ vec2geo_planar_signed,
8
+ vec2geo_linear,
9
+ vec2geo_linear_signed,
10
+ )
11
+ from apsg.decorator._decorator import ensure_first_arg_same, ensure_arguments
12
+ from apsg.math._vector import Vector3, Axial3
13
+
14
+
15
+ class Lineation(Axial3):
16
+ """
17
+ A class to represent axial (non-oriented) linear feature (lineation).
18
+
19
+ There are different way to create ``Lineation`` object:
20
+
21
+ - without arguments create default ``Lineation`` L:0/0
22
+ - with single argument `l`, where:
23
+
24
+ - `l` could be Vector3-like object
25
+ - `l` could be string 'x', 'y' or 'z' - principal axes of coordinate system
26
+ - `l` could be tuple of (x, y, z) - vector components
27
+ - with 2 arguments plunge direction and plunge
28
+ - with 3 numerical arguments defining vector components
29
+
30
+ Args:
31
+ azi (float): plunge direction of linear feature in degrees
32
+ inc (float): plunge of linear feature in degrees
33
+
34
+ Example:
35
+ >>> lin()
36
+ >>> lin('y')
37
+ >>> lin(1,2,-1)
38
+ >>> l = lin(110, 26)
39
+ """
40
+
41
+ def __repr__(self):
42
+ azi, inc = self.geo
43
+ return f"L:{azi:.0f}/{inc:.0f}"
44
+
45
+ def cross(self, other):
46
+ """Return Foliation defined by two linear features"""
47
+ return Foliation(super().cross(other))
48
+
49
+ __pow__ = cross
50
+
51
+ @property
52
+ def geo(self):
53
+ """Return tuple of plunge direction and plunge"""
54
+ return vec2geo_linear(self)
55
+
56
+ def to_json(self):
57
+ """Return as JSON dict"""
58
+ azi, inc = vec2geo_linear_signed(self)
59
+ return {"datatype": type(self).__name__, "args": (azi, inc)}
60
+
61
+
62
+ class Foliation(Axial3):
63
+ """
64
+ A class to represent non-oriented planar feature (foliation).
65
+
66
+ There are different way to create ``Foliation`` object:
67
+
68
+ - without arguments create default ``Foliation`` S:180/0
69
+ - with single argument `f`, where:
70
+
71
+ - `f` could be Vector3-like object
72
+ - `f` could be string 'x', 'y' or 'z' - principal planes of coordinate system
73
+ - `f` could be tuple of (x, y, z) - vector components
74
+ - with 2 arguments follows active notation. See apsg_conf["notation"]
75
+ - with 3 numerical arguments defining vector components of plane normal
76
+
77
+ Args:
78
+ azi (float): dip direction (or strike) of planar feature in degrees.
79
+ inc (float): dip of planar feature in degrees
80
+
81
+ Example:
82
+ >>> fol()
83
+ >>> fol('y')
84
+ >>> fol(1,2,-1)
85
+ >>> f = fol(250, 30)
86
+
87
+ """
88
+
89
+ def __init__(self, *args):
90
+ if len(args) == 0:
91
+ coords = (0, 0, 1)
92
+ elif len(args) == 1:
93
+ if np.asarray(args[0]).shape == Foliation.__shape__:
94
+ coords = np.asarray(args[0])
95
+ elif isinstance(args[0], str):
96
+ if args[0].lower() == "x":
97
+ coords = (1, 0, 0)
98
+ elif args[0].lower() == "y":
99
+ coords = (0, 1, 0)
100
+ elif args[0].lower() == "z":
101
+ coords = (0, 0, 1)
102
+ else:
103
+ raise TypeError(f"Not valid arguments for {type(self).__name__}")
104
+ else:
105
+ raise TypeError(f"Not valid arguments for {type(self).__name__}")
106
+ elif len(args) == 2:
107
+ coords = geo2vec_planar(*args)
108
+ elif len(args) == 3:
109
+ coords = [float(v) for v in args]
110
+ else:
111
+ raise TypeError("Not valid arguments for Foliation")
112
+ self._coords = tuple(coords)
113
+
114
+ def __repr__(self):
115
+ azi, inc = self.geo
116
+ return f"S:{azi:.0f}/{inc:.0f}"
117
+
118
+ def cross(self, other):
119
+ """Return Lineation defined by intersection of planar features"""
120
+ return Lineation(super().cross(other))
121
+
122
+ __pow__ = cross
123
+
124
+ @property
125
+ def geo(self):
126
+ """Return tuple of dip direction and dip"""
127
+ return vec2geo_planar(self)
128
+
129
+ def to_json(self):
130
+ """Return as JSON dict"""
131
+ azi, inc = vec2geo_planar_signed(self)
132
+ return {"datatype": type(self).__name__, "args": (azi, inc)}
133
+
134
+ def dipvec(self):
135
+ """Return dip vector"""
136
+ return Vector3(*vec2geo_planar(self))
137
+
138
+ def pole(self):
139
+ """Return plane normal as vector"""
140
+ return Vector3(self)
141
+
142
+ def rake(self, rake):
143
+ """Return rake vector"""
144
+ return Vector3(self.dipvec().rotate(self, rake - 90))
145
+
146
+ def transform(self, F, **kwargs):
147
+ """
148
+ Return affine transformation by matrix `F`.
149
+
150
+ Args:
151
+ F: transformation matrix
152
+
153
+ Keyword Args:
154
+ norm: normalize transformed ``Foliation``. [True or False] Default False
155
+
156
+ Returns:
157
+ vector representation of affine transformation (dot product)
158
+ of `self` by `F`
159
+
160
+ Example:
161
+ >>> # Reflexion of `y` axis.
162
+ >>> F = [[1, 0, 0], [0, -1, 0], [0, 0, 1]]
163
+ >>> f = fol(45, 20)
164
+ >>> f.transform(F)
165
+ S:315/20
166
+ """
167
+ r = np.dot(self, np.linalg.inv(F))
168
+ if kwargs.get("norm", False):
169
+ r = r.normalized()
170
+ return type(self)(r)
171
+
172
+
173
+ class Pair:
174
+ """
175
+ The class to store pair of planar and linear feature.
176
+
177
+ When ``Pair`` object is created, both planar and linear feature are
178
+ adjusted, so linear feature perfectly fit onto planar one. Warning
179
+ is issued, when misfit angle is bigger than 20 degrees.
180
+
181
+ There are different way to create ``Pair`` object:
182
+
183
+ - without arguments create default Pair with fol(0,0) and lin(0,0)
184
+ - with single argument `p`, where:
185
+
186
+ - `p` could be Pair
187
+ - `p` could be tuple of (fazi, finc, lazi, linc)
188
+ - `p` could be tuple of (fx, fy ,fz, lx, ly, lz)
189
+ - with 2 arguments f and l could be Vector3 like objects,
190
+ e.g. Foliation and Lineation
191
+ - with four numerical arguments defining `fol(fazi, finc)` and `lin(lazi, linc)`
192
+
193
+ Args:
194
+ fazi (float): dip azimuth of planar feature in degrees
195
+ finc (float): dip of planar feature in degrees
196
+ lazi (float): plunge direction of linear feature in degrees
197
+ linc (float): plunge of linear feature in degrees
198
+
199
+ Attributes:
200
+ fvec (Vector3): corrected vector normal to plane
201
+ lvec (Vector3): corrected vector of linear feature
202
+
203
+ Example:
204
+ >>> pair()
205
+ >>> pair(p)
206
+ >>> pair(f, l)
207
+ >>> pair(fazi, finc, lazi, linc)
208
+ >>> p = pair(140, 30, 110, 26)
209
+
210
+ """
211
+
212
+ __slots__ = ("fvec", "lvec", "misfit")
213
+ __shape__ = (6,)
214
+
215
+ def __init__(self, *args):
216
+ if len(args) == 0:
217
+ fvec, lvec = Vector3(0, 0, 1), Vector3(1, 0, 0)
218
+ elif len(args) == 1 and issubclass(type(args[0]), Pair):
219
+ fvec, lvec = args[0].fvec, args[0].lvec
220
+ elif len(args) == 1 and np.asarray(args[0]).shape == (4,):
221
+ fazi, finc, lazi, linc = (float(v) for v in args[0])
222
+ fvec, lvec = Foliation(fazi, finc), Lineation(lazi, linc)
223
+ elif len(args) == 1 and np.asarray(args[0]).shape == Pair.__shape__:
224
+ fvec, lvec = Vector3(args[0][:3]), Vector3(args[0][-3:])
225
+ elif len(args) == 2:
226
+ if issubclass(type(args[0]), Vector3) and issubclass(
227
+ type(args[1]), Vector3
228
+ ):
229
+ fvec, lvec = args
230
+ else:
231
+ raise TypeError("Not valid arguments for Pair")
232
+ elif len(args) == 4:
233
+ fvec = Foliation(args[0], args[1])
234
+ lvec = Lineation(args[2], args[3])
235
+ else:
236
+ raise TypeError("Not valid arguments for Pair")
237
+
238
+ fvec = Vector3(fvec)
239
+ lvec = Vector3(lvec)
240
+ misfit = abs(90 - fvec.angle(lvec))
241
+ if misfit > 20:
242
+ warnings.warn(f"Warning: Misfit angle is {misfit:.1f} degrees.")
243
+ ax = fvec.cross(lvec)
244
+ ang = (lvec.angle(fvec) - 90) / 2
245
+ self.fvec = Vector3(fvec.rotate(ax, ang))
246
+ self.lvec = Vector3(lvec.rotate(ax, -ang))
247
+ self.misfit = misfit
248
+
249
+ def __repr__(self):
250
+ fazi, finc = self.fol.geo
251
+ lazi, linc = self.lin.geo
252
+ return f"P:{fazi:.0f}/{finc:.0f}-{lazi:.0f}/{linc:.0f}"
253
+
254
+ @ensure_first_arg_same
255
+ def __eq__(self, other):
256
+ """
257
+ Return `True` if pairs are equal, otherwise `False`.
258
+ """
259
+ return (self.fvec == other.fvec) and (self.lvec == other.lvec)
260
+
261
+ def __ne__(self, other):
262
+ """
263
+ Return `True` if pairs are not equal, otherwise `False`.
264
+
265
+ """
266
+ return not self == other
267
+
268
+ def __array__(self, dtype=None, copy=None):
269
+ return np.hstack((self.fvec, self.lvec)).astype(dtype)
270
+
271
+ def label(self):
272
+ """Return label"""
273
+ return str(self)
274
+
275
+ def to_json(self):
276
+ """Return as JSON dict"""
277
+ fazi, finc = vec2geo_planar_signed(self.fvec)
278
+ lazi, linc = vec2geo_linear_signed(self.lvec)
279
+ return {"datatype": type(self).__name__, "args": (fazi, finc, lazi, linc)}
280
+
281
+ @classmethod
282
+ def random(cls):
283
+ """
284
+ Random Pair
285
+ """
286
+
287
+ lin, p = Vector3.random(), Vector3.random()
288
+ fol = lin.cross(p)
289
+ return cls(fol, lin)
290
+
291
+ @ensure_arguments(Vector3)
292
+ def rotate(self, axis, phi):
293
+ """Rotates ``Pair`` by angle `phi` about `axis`.
294
+
295
+ Args:
296
+ axis (``Vector3``): axis of rotation
297
+ phi (float): angle of rotation in degrees
298
+
299
+ Example:
300
+ >>> p = pair(fol(140, 30), lin(110, 26))
301
+ >>> p.rotate(lin(40, 50), 120)
302
+ P:210/83-287/60
303
+
304
+ """
305
+ return type(self)(self.fvec.rotate(axis, phi), self.lvec.rotate(axis, phi))
306
+
307
+ @property
308
+ def rax(self):
309
+ return self.lvec.cross(self.fvec)
310
+
311
+ @property
312
+ def fol(self):
313
+ """
314
+ Return a planar feature of ``Pair`` as ``Foliation``.
315
+ """
316
+ return Foliation(self.fvec)
317
+
318
+ @property
319
+ def lin(self):
320
+ """
321
+ Return a linear feature of ``Pair`` as ``Lineation``.
322
+ """
323
+ return Lineation(self.lvec)
324
+
325
+ def transform(self, F, **kwargs):
326
+ """Return an affine transformation of ``Pair`` by matrix `F`.
327
+
328
+ Args:
329
+ F: transformation matrix
330
+
331
+ Keyword Args:
332
+ norm: normalize transformed vectors. True or False. Default False
333
+
334
+ Returns:
335
+ representation of affine transformation (dot product) of `self`
336
+ by `F`
337
+
338
+ Example:
339
+ >>> F = defgrad.from_axisangle(lin(0,0), 60)
340
+ >>> p = pair(90, 90, 0, 50)
341
+ >>> p.transform(F)
342
+ P:270/30-314/23
343
+
344
+ """
345
+
346
+ fvec = self.fol.transform(F)
347
+ lvec = self.lin.transform(F)
348
+ if kwargs.get("norm", False):
349
+ fvec = fvec.normalized()
350
+ lvec = lvec.normalized()
351
+ return type(self)(fvec, lvec)
352
+
353
+
354
+ class Fault(Pair):
355
+ """
356
+ The class to store ``Pair`` with associated sense of movement.
357
+
358
+ When ``Fault`` object is created, both planar and linear feature are
359
+ adjusted, so linear feature perfectly fit onto planar one. Warning
360
+ is issued, when misfit angle is bigger than 20 degrees.
361
+
362
+ There are different way to create ``Fault`` object:
363
+
364
+ - without arguments create default ``Fault`` with `fol(0,0)` and `lin(0,0)`
365
+ - with single argument `p`:
366
+
367
+ - `p` could be Fault
368
+ - `p` could be tuple of (fazi, finc, lazi, linc, sense)
369
+ - `p` could be tuple of (fx, fy ,fz, lx, ly, lz)
370
+ - with 2 arguments p (Pair object) and sense
371
+ - with 3 arguments f, l (Vector3 like objects), e.g. ``Foliation``
372
+ and ``Lineation`` and sense
373
+ - with 5 numerical arguments defining `fol(fazi, finc)`, `lin(lazi, linc)` and sense
374
+
375
+ Args:
376
+ fazi (float): dip azimuth of planar feature in degrees
377
+ finc (float): dip of planar feature in degrees
378
+ lazi (float): plunge direction of linear feature in degrees
379
+ linc (float): plunge of linear feature in degrees
380
+ sense (float or str): sense of movement +/11 hanging-wall down/up. When str,
381
+ ,ust be one of 's', 'd', 'n', 'r'.
382
+
383
+
384
+ Attributes:
385
+ fvec (Vector3): corrected vector normal to plane
386
+ lvec (Vector3): corrected vector of linear feature
387
+ sense (int): sense of movement (+/-1)
388
+
389
+ Example:
390
+ >>> f = fault(140, 30, 110, 26, -1)
391
+ >>> f = fault(140, 30, 110, 26, 'r')
392
+ >>> p = pair(140, 30, 110, 26)
393
+ >>> f = fault(p, 'n')
394
+ >>> f = fault(fol(120, 80), lin(32, 10), 's')
395
+
396
+ """
397
+
398
+ __shape__ = (7,)
399
+
400
+ def __init__(self, *args):
401
+ if len(args) == 0:
402
+ fvec, lvec = Vector3(0, 0, 1), Vector3(1, 0, 0)
403
+ elif len(args) == 1 and np.asarray(args[0]).shape == (5,):
404
+ fazi, finc, lazi, linc, sense = (float(v) for v in args[0])
405
+ fvec, lvec = Foliation(fazi, finc), Lineation(lazi, linc)
406
+ sense = self.calc_sense(fvec, lvec, sense)
407
+ if sense < 0:
408
+ lvec = -lvec
409
+ elif len(args) == 1 and issubclass(type(args[0]), Pair):
410
+ fvec, lvec = args[0].fvec, args[0].lvec
411
+ elif len(args) == 2 and issubclass(type(args[0]), Pair):
412
+ fvec, lvec = args[0].fvec, args[0].lvec
413
+ sense = self.calc_sense(fvec, lvec, args[1])
414
+ georax = lvec.lower().cross(fvec.lower())
415
+ if args[0].rax == georax and sense < 0:
416
+ lvec = -lvec
417
+ elif len(args) == 2:
418
+ if issubclass(type(args[0]), Vector3) and issubclass(
419
+ type(args[1]), Vector3
420
+ ):
421
+ fvec, lvec = args[0], args[1]
422
+ elif len(args) == 3:
423
+ if issubclass(type(args[0]), Vector3) and issubclass(
424
+ type(args[1]), Vector3
425
+ ):
426
+ fvec, lvec = args[0], args[1]
427
+ sense = self.calc_sense(fvec, lvec, args[2])
428
+ rax = lvec.cross(fvec)
429
+ georax = lvec.lower().cross(fvec.lower())
430
+ if rax == georax and sense < 0:
431
+ lvec = -lvec
432
+ elif len(args) == 5:
433
+ fvec = Foliation(args[0], args[1])
434
+ lvec = Lineation(args[2], args[3])
435
+ sense = self.calc_sense(fvec, lvec, args[4])
436
+ if sense < 0:
437
+ lvec = -lvec
438
+ else:
439
+ raise TypeError("Not valid arguments for Fault")
440
+ super().__init__(fvec, lvec)
441
+
442
+ @classmethod
443
+ def calc_sense(cls, fvec, lvec, sense):
444
+ if isinstance(sense, int) or isinstance(sense, float):
445
+ return sense
446
+ elif isinstance(sense, str):
447
+ p = Pair(fvec, lvec)
448
+ if sense.lower() == "s":
449
+ if p.rax == p.rax.lower():
450
+ res = -1
451
+ else:
452
+ res = 1
453
+ elif sense.lower() == "d":
454
+ if p.rax == p.rax.lower():
455
+ res = 1
456
+ else:
457
+ res = -1
458
+ elif sense.lower() == "n":
459
+ res = 1
460
+ elif sense.lower() == "r":
461
+ res = -1
462
+ return res
463
+
464
+ def __repr__(self):
465
+ fazi, finc = self.fol.geo
466
+ lazi, linc = self.lin.geo
467
+ schar = [" ", "+", "-"][self.sense]
468
+ return f"F:{fazi:.0f}/{finc:.0f}-{lazi:.0f}/{linc:.0f} {schar}"
469
+
470
+ @ensure_first_arg_same
471
+ def __eq__(self, other):
472
+ """
473
+ Return `True` if pairs are equal, otherwise `False`.
474
+ """
475
+ return (
476
+ (self.fvec == other.fvec)
477
+ and (self.lvec == other.lvec)
478
+ and (self.sense == other.sense)
479
+ )
480
+
481
+ def __ne__(self, other):
482
+ """
483
+ Return `True` if pairs are not equal, otherwise `False`.
484
+
485
+ """
486
+ return not self == other
487
+
488
+ def __array__(self, dtype=None, copy=None):
489
+ return np.hstack((self.fvec, self.lvec, self.sense)).astype(dtype)
490
+
491
+ def to_json(self):
492
+ """Return as JSON dict"""
493
+ fazi, finc = vec2geo_planar_signed(self.fvec)
494
+ lazi, linc = vec2geo_linear_signed(self.lvec)
495
+ return {
496
+ "datatype": type(self).__name__,
497
+ "args": (fazi, finc, lazi, linc, self.sense),
498
+ }
499
+
500
+ @classmethod
501
+ def random(cls):
502
+ """
503
+ Random Fault
504
+ """
505
+ import random
506
+
507
+ lvec, p = Vector3.random(), Vector3.random()
508
+ fvec = lvec.cross(p)
509
+ return cls(fvec, lvec, random.choice([-1, 1]))
510
+
511
+ @property
512
+ def georax(self):
513
+ return self.lvec.lower().cross(self.fvec.lower())
514
+
515
+ @property
516
+ def sense(self):
517
+ if self.rax == self.georax:
518
+ return 1
519
+ else:
520
+ return -1
521
+
522
+ def p_vector(self, ptangle=90):
523
+ """Return P axis as ``Vector3``"""
524
+ return self.fvec.rotate(self.lvec.cross(self.fvec), -ptangle / 2)
525
+
526
+ def t_vector(self, ptangle=90):
527
+ """Return T-axis as ``Vector3``."""
528
+ return self.fvec.rotate(self.lvec.cross(self.fvec), +ptangle / 2)
529
+
530
+ @property
531
+ def p(self):
532
+ """Return P-axis as ``Lineation``"""
533
+ return Lineation(self.p_vector())
534
+
535
+ @property
536
+ def t(self):
537
+ """Return T-axis as ``Lineation``"""
538
+ return Lineation(self.t_vector())
539
+
540
+ @property
541
+ def m(self):
542
+ """Return kinematic M-plane as ``Foliation``"""
543
+ return Foliation(self.lvec.cross(self.fvec))
544
+
545
+ @property
546
+ def d(self):
547
+ """Return dihedra plane as ``Fol``"""
548
+ return Foliation(self.lvec.cross(self.fvec).cross(self.fvec))
549
+
550
+
551
+ class Cone:
552
+ """
553
+ The class to store cone with given axis, secant line and revolution angle
554
+ in degrees.
555
+
556
+ There are different way to create ``Cone`` object according to number
557
+ of arguments:
558
+
559
+ - without args, you can create default``Cone`` with axis ``lin(0, 90)``,
560
+ secant ``lin(0, 0)`` angle 360°
561
+ - with single argument `c`, where `c` could be ``Cone``, 5-tuple of
562
+ `(aazi, ainc, sazi, sinc, revangle)` or 7-tuple of
563
+ `(ax, ay ,az, sx, sy, sz, revangle)`
564
+ - with 3 arguments, where axis and secant line could be Vector3 like objects,
565
+ e.g. Lineation and third argument is revolution angle
566
+ - with 5 arguments defining axis `lin(aazi, ainc)`, secant line
567
+ `lin(sazi, sinc)` and angle of revolution
568
+
569
+ Attributes:
570
+ axis (Vector3): axis of the cone
571
+ secant (Vector3): secant line
572
+ revangle (float): revolution angle
573
+
574
+ Example:
575
+ >>> cone()
576
+ >>> cone(c)
577
+ >>> cone(a, s, revangle)
578
+ >>> cone(aazi, ainc, sazi, sinc, revangle)
579
+ >>> c = cone(140, 30, 110, 26, 360)
580
+
581
+ """
582
+
583
+ __slots__ = ("axis", "secant", "revangle")
584
+ __shape__ = (7,)
585
+
586
+ def __init__(self, *args):
587
+ if len(args) == 0:
588
+ axis, secant, revangle = Vector3(0, 0, 1), Vector3(1, 0, 0), 360
589
+ elif len(args) == 1 and issubclass(type(args[0]), Cone):
590
+ axis, secant, revangle = args[0].axis, args[0].secant, args[0].revangle
591
+ elif len(args) == 1 and np.asarray(args[0]).shape == (5,):
592
+ aazi, ainc, sazi, sinc, revangle = (float(v) for v in args[0])
593
+ axis, secant = Lineation(aazi, ainc), Lineation(sazi, sinc)
594
+ elif len(args) == 1 and np.asarray(args[0]).shape == Cone.__shape__:
595
+ axis, secant, revangle = (
596
+ Vector3(args[0][:3]),
597
+ Vector3(args[0][3:6]),
598
+ args[0][-1],
599
+ )
600
+ elif len(args) == 2:
601
+ if issubclass(type(args[0]), Vector3) and issubclass(
602
+ type(args[1]), Vector3
603
+ ):
604
+ axis, secant = args
605
+ revangle = 360
606
+ elif issubclass(type(args[0]), Vector3) and np.isscalar(args[1]):
607
+ axis = args[0]
608
+ azi, inc = axis.geo
609
+ secant = Vector3(azi, inc + args[1])
610
+ revangle = 360
611
+ else:
612
+ raise TypeError("Not valid arguments for Cone")
613
+ elif len(args) == 3:
614
+ if issubclass(type(args[0]), Vector3) and issubclass(
615
+ type(args[1]), Vector3
616
+ ):
617
+ axis, secant, revangle = args
618
+ else:
619
+ raise TypeError("Not valid arguments for Cone")
620
+ elif len(args) == 4:
621
+ axis = Lineation(args[0], args[1])
622
+ secant = Lineation(args[2], args[3])
623
+ revangle = 360
624
+ elif len(args) == 5:
625
+ axis = Lineation(args[0], args[1])
626
+ secant = Lineation(args[2], args[3])
627
+ revangle = args[4]
628
+ else:
629
+ raise TypeError("Not valid arguments for Cone")
630
+
631
+ self.axis = Vector3(axis)
632
+ self.secant = Vector3(secant)
633
+ self.revangle = float(revangle)
634
+ if self.axis.angle(self.secant) > 90:
635
+ self.secant = -self.secant
636
+
637
+ def __repr__(self):
638
+ azi, inc = vec2geo_linear(self.axis)
639
+ return f"C:{azi:.0f}/{inc:.0f} [{self.apical_angle():g}]"
640
+
641
+ @ensure_first_arg_same
642
+ def __eq__(self, other):
643
+ return (
644
+ (self.axis == other.axis)
645
+ and (self.secant == other.secant)
646
+ and (self.revangle == other.revangle)
647
+ )
648
+
649
+ def __ne__(self, other):
650
+ return not self == other
651
+
652
+ def __array__(self, dtype=None, copy=None):
653
+ return np.hstack((self.axis, self.secant, self.revangle)).astype(dtype)
654
+
655
+ def label(self):
656
+ """Return label"""
657
+ return str(self)
658
+
659
+ def to_json(self):
660
+ """Return as JSON dict"""
661
+ aazi, ainc = vec2geo_linear_signed(self.axis)
662
+ sazi, sinc = vec2geo_linear_signed(self.secant)
663
+ return {
664
+ "datatype": type(self).__name__,
665
+ "args": (aazi, ainc, sazi, sinc, self.revangle),
666
+ }
667
+
668
+ @classmethod
669
+ def random(cls):
670
+ """
671
+ Random Cone
672
+ """
673
+
674
+ axis, secant = Vector3.random(), Vector3.random()
675
+ return cls(axis, secant, 360)
676
+
677
+ @ensure_arguments(Vector3)
678
+ def rotate(self, axis, phi):
679
+ """Rotates ``Cone`` by angle `phi` about `axis`.
680
+
681
+ Args:
682
+ axis (``Vector3``): axis of rotation
683
+ phi (float): angle of rotation in degrees
684
+
685
+ Example:
686
+ >>> c = cone(lin(140, 30), lin(110, 26), 360)
687
+ >>> c.rotate(lin(40, 50), 120)
688
+ C:210/83-287/60
689
+
690
+ """
691
+ return type(self)(
692
+ self.axis.rotate(axis, phi), self.secant.rotate(axis, phi), self.revangle
693
+ )
694
+
695
+ def apical_angle(self):
696
+ """Return apical angle"""
697
+ return self.axis.angle(self.secant)
698
+
699
+ @property
700
+ def rotated_secant(self):
701
+ """Return revangle rotated secant vector"""
702
+ return self.secant.rotate(self.axis, self.revangle)