cgse-coordinates 2023.1.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,707 @@
1
+ """
2
+ The point module provides two classes, a `Point` class which simply represents a point
3
+ in the 3D space, and a `Points` class which is a collection of Point objects.
4
+
5
+ A Point is defined with respect to a given reference frame and is given a name.
6
+
7
+ Point objects defined in the same reference frame can be combined with the
8
+ natural `+`, `-` , `+=` and `-=` operations.
9
+
10
+ In order to work with 4x4 transformation matrices, the 3D [x,y,z] coordinates are
11
+ automatically converted to a [x,y,z,1] coordinates array attribute.
12
+
13
+ @author: Pierre Royer
14
+ """
15
+ import logging
16
+ import random
17
+ import string
18
+
19
+ import numpy as np
20
+
21
+ import egse.coordinates.transform3d_addon as t3add
22
+
23
+ LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ class Point:
27
+ """
28
+ A Point object represents a point in 3D space and is defined with respect to
29
+ a given reference frame.
30
+
31
+ .. note::
32
+ There is no check that the randomly generated name is unique, so two Point
33
+ objects can be different but have the same name.
34
+
35
+ """
36
+ debug = 0
37
+
38
+ def __init__(self, coordinates, ref, name=None):
39
+ """
40
+ This initializes a Point object in a given reference frame.
41
+
42
+ Args:
43
+ coordinates (numpy.ndarray, list): 1x3 or 1x4 matrix defining this system in "ref" system
44
+ (1x3 being x,y,z + an additional 1 for the affine operations)
45
+
46
+ ref (ReferenceFrame): the reference system in which this Point object will be defined,
47
+ if not given or None the master reference frame will be used
48
+
49
+ """
50
+
51
+ # Makes sure of format [x,y,z,1] and sets coordinates
52
+ self.setCoordinates(coordinates)
53
+
54
+ # set the reference frame of reference
55
+ if ref is None:
56
+ raise ValueError("A Point shall be defined with a reference frame, ref can not be None.")
57
+ else:
58
+ self.ref = ref
59
+
60
+ self.setName(name)
61
+
62
+ self.definition = [self.coordinates[:-1], self.ref, self.name]
63
+
64
+ def __repr__(self):
65
+ return f"{self.coordinates[:-1]} (ref {self.ref.name})"
66
+
67
+ def __str__(self):
68
+ return f"{self.coordinates[:-1]} (ref {self.ref.name}), name {self.name}"
69
+
70
+ def __eq__(self, other):
71
+ """
72
+ Re-implements the == operator which by default checks if id(self) == id(other).
73
+
74
+ Two Point objects are equal when:
75
+
76
+ * their coordinates are equal
77
+ * the reference system in which they are defined is equal
78
+ * the name must not be equal
79
+ """
80
+ if self is other:
81
+ return True
82
+
83
+ if isinstance(other, Point):
84
+ if not np.array_equal(self.coordinates, other.coordinates):
85
+ return False
86
+ if self.ref != other.ref:
87
+ return False
88
+ return True
89
+ return NotImplemented
90
+
91
+ def __hash__(self):
92
+ return id(self.definition) // 16
93
+
94
+ def isSame(self, other):
95
+ """
96
+ This checks if a Point is the same as another Point in a different reference frame.
97
+
98
+ Two Point objects are the same when their position, i.e. coordinates, are equal
99
+ after they have been expressed in the same reference frame.
100
+
101
+ :param other: a Point object that you want to check
102
+ :type other: Point
103
+
104
+ :returns: True when the two Point objects are the same, False otherwise
105
+
106
+ :raises TypeError: when other is not a compatible type, NotImplemented will returned
107
+ which will result in a ```TypeError: unsupported operand type(s) for +:```.
108
+ """
109
+
110
+ if isinstance(other, Point):
111
+ if self == other:
112
+ return True
113
+ else:
114
+ if np.array_equal(self.coordinates, other.expressIn(self.ref)):
115
+ return True
116
+ return False
117
+ return NotImplemented
118
+
119
+ @staticmethod
120
+ def __coords__(coordinates):
121
+ """
122
+ Formats 1x3 or 1x4 input lists or np.arrays into 1x4 np.array coordinates
123
+ Static --> can be called 'from outside', without passing a Point object
124
+ """
125
+ if isinstance(coordinates, Point):
126
+ return coordinates.coordinates
127
+ elif isinstance(coordinates, (np.ndarray, list)):
128
+ coordinates = list(coordinates)
129
+ if len(coordinates) == 3:
130
+ coordinates.append(1)
131
+ return coordinates
132
+ else:
133
+ raise ValueError("input must be a list, numpy.ndarray or Point")
134
+
135
+ def setName(self, name=None):
136
+ """
137
+ Set or change the name of a Point object.
138
+
139
+ :param str name: the new name for the Point, if None, a random name will be generated.
140
+
141
+ .. todo:: Should we care about the possibility the the generation of random names does not
142
+ necessarily create a unique name for the Point?
143
+ """
144
+ if name is None:
145
+ self.name = 'p'+''.join(random.choices(string.ascii_lowercase, k=3))
146
+ else:
147
+ self.name = name
148
+
149
+ def setCoordinates(self, coordinates):
150
+ """
151
+ Set the coordinates of this Point object.
152
+ """
153
+ coordinates = Point.__coords__(coordinates)
154
+ self.coordinates = np.array(coordinates)
155
+
156
+ self.x = self.coordinates[0]
157
+ self.y = self.coordinates[1]
158
+ self.z = self.coordinates[2]
159
+
160
+ def getCoordinates(self, ref=None):
161
+ """
162
+ Returns the coordinates of this Points object.
163
+
164
+ If no reference frame is given, the coordinates of the Point will just be returned,
165
+ other wise this method behaves the same as the ``expressIn(ref)`` method.
166
+
167
+ :param ref: the Reference Frame in which the Point shall be defined
168
+ :type ref: ReferenceFrame
169
+ """
170
+ if ref is None:
171
+ return self.coordinates
172
+ else:
173
+ return self.expressIn(ref)
174
+
175
+ def distanceTo(self, target):
176
+ """
177
+ Returns the distance of this Point object to the target. Target can be another Point,
178
+ a ReferenceFrame object or a Numpy dnarray or list with coordinates.
179
+ """
180
+ from egse.coordinates.referenceFrame import ReferenceFrame
181
+
182
+ if isinstance(target, Point):
183
+ targetCoords = target.expressIn(self.ref)[:3]
184
+ elif isinstance(target, ReferenceFrame):
185
+ return np.linalg.norm(self.expressIn(target)[:3])
186
+ elif isinstance(target, (np.ndarray, list)):
187
+ if len(target) > 3:
188
+ target = target[:3]
189
+ targetCoords = target
190
+ else:
191
+ raise ValueError("input must be a list, numpy.ndarray, Point or ReferenceFrame")
192
+
193
+ LOGGER.info(f"self={self.coordinates[:-1]}, target={targetCoords}")
194
+
195
+ return np.linalg.norm(self.coordinates[:3]-targetCoords)
196
+
197
+ def inPlaneDistanceTo(self,target,plane='xy'):
198
+ """
199
+ Returns the distance of this Point object to the target, considering 2 coordinates only!
200
+
201
+ target: can be another Point, a ReferenceFrame object or a Numpy dnarray or list with coordinates.
202
+
203
+ plane : must be in ['xy', 'xz', 'yz']
204
+
205
+ NB: The xy, yz or xz plane used to project the points coordinates before
206
+ computing the distances is taken from the coordinate system of "self"
207
+ ==> pointA.inPlaneDistanceTo(pointB) != pointB.inPlaneDistanceTo(pointA)
208
+ The first projects on the xy plane of pointA.ref, the second on the xy plane of pointB.ref
209
+ """
210
+ from egse.coordinates.referenceFrame import ReferenceFrame
211
+
212
+ if isinstance(target, Point):
213
+ targetCoords = target.expressIn(self.ref)
214
+ elif isinstance(target, ReferenceFrame):
215
+ targetCoords = target.getOrigin().expressIn(self)
216
+ elif isinstance(target, (np.ndarray, list)):
217
+ targetCoords = target
218
+ else:
219
+ raise ValueError("input must be a list, numpy.ndarray, Point or ReferenceFrame")
220
+
221
+ LOGGER.info(f"self={self.coordinates[:-1]}, target={targetCoords}")
222
+
223
+ planeSelect = {'xy':[0,1], 'xz':[0,2], 'yz':[1,2]}
224
+
225
+ LOGGER.info(f"self.coordinates[planeSelect[plane]] {self.coordinates[planeSelect[plane]]}")
226
+ LOGGER.info(f"targetCoords[planeSelect[plane]] {targetCoords[planeSelect[plane]]}")
227
+ LOGGER.info(f"Difference {self.coordinates[planeSelect[plane]]-targetCoords[planeSelect[plane]]}")
228
+
229
+ return np.linalg.norm(self.coordinates[planeSelect[plane]]-targetCoords[planeSelect[plane]])
230
+
231
+ def distanceToPlane(self,plane="xy",ref=None):
232
+ """
233
+ distanceToPlane(self,plane="xy",ref=None)
234
+
235
+ The target plane is defined by one of it coordinate planes: ["xy", "yz", "xz"]
236
+
237
+ :param ref: reference frame
238
+ :type ref: ReferenceFrame
239
+
240
+ :param plane: in ["xy", "xz", "yz"]
241
+ :type plane: str
242
+
243
+ :returns: the distance from self to plane
244
+ """
245
+ if (ref is None) or (self.ref == ref):
246
+ coordinates = self.coordinates[:-1]
247
+ elif self.ref != ref:
248
+ coordinates = self.expressIn(ref)
249
+
250
+ outOfPlaneIndex = {'xy':2,'xz':1,'yz':0}
251
+
252
+ return coordinates[outOfPlaneIndex[plane]]
253
+
254
+ def __sub__(self,apoint):
255
+ """
256
+ Takes care for
257
+ newPoint = self + point
258
+ """
259
+ if isinstance(apoint, Point):
260
+
261
+ try:
262
+ if apoint.ref != self.ref:
263
+ raise ValueError
264
+ except ValueError:
265
+ print("WARNING: The points have different reference frames, returning NotImplemented")
266
+ return NotImplemented
267
+ newCoordinates = self.coordinates - apoint.coordinates
268
+
269
+ elif isinstance(apoint, (np.ndarray, list)):
270
+
271
+ newCoordinates = self.coordinates - Point.__coords__(apoint)
272
+
273
+ # For the affine transforms, the 4th digit must be set to 1 (it has been modified above)
274
+ newCoordinates[-1] = 1
275
+
276
+ return Point(coordinates=newCoordinates,ref=self.ref)
277
+
278
+ def __isub__(self,apoint):
279
+ """
280
+ Takes care for
281
+ self += coordinates (modifies self in place)
282
+ """
283
+ if isinstance(apoint, Point):
284
+
285
+ try:
286
+ if apoint.ref != self.ref:
287
+ raise ValueError
288
+ except ValueError:
289
+ print("WARNING: The points have different reference frames, returning NotImplemented")
290
+ return NotImplemented
291
+ newCoordinates = self.coordinates - apoint.coordinates
292
+
293
+ elif isinstance(apoint, (np.ndarray, list)):
294
+
295
+ newCoordinates = self.coordinates - Point.__coords__(apoint)
296
+
297
+ # For the affine transforms, the 4th digit must be set to 1 (it has been modified above)
298
+ newCoordinates[-1] = 1
299
+
300
+ self.coordinates = newCoordinates
301
+
302
+ return self
303
+
304
+ def __add__(self,apoint):
305
+ """
306
+ Takes care for
307
+ newPoint = self + point
308
+ """
309
+ if isinstance(apoint, Point):
310
+
311
+ try:
312
+ if apoint.ref != self.ref:
313
+ print(f"DEBUG: {apoint} = {apoint.expressIn(self.ref)}")
314
+ raise ValueError
315
+ except ValueError:
316
+ print("WARNING: The points have different reference frames, returning NotImplemented")
317
+ return NotImplemented
318
+ newCoordinates = self.coordinates + apoint.coordinates
319
+
320
+ elif isinstance(apoint, (np.ndarray, list)):
321
+
322
+ newCoordinates = self.coordinates + Point.__coords__(apoint)
323
+
324
+ else:
325
+ return NotImplemented
326
+
327
+ # For the affine transforms, the 4th digit must be set to 1 (it has been modified above)
328
+ newCoordinates[-1] = 1
329
+
330
+ return Point(coordinates=newCoordinates,ref=self.ref)
331
+
332
+ def __iadd__(self,apoint):
333
+ """
334
+ Takes care for
335
+ self += coordinates (modifies self in place)
336
+ """
337
+ if isinstance(apoint, Point):
338
+
339
+ try:
340
+ if apoint.ref != self.ref:
341
+ raise ValueError
342
+ except ValueError:
343
+ print("WARNING: The points have different reference frames, returning NotImplemented")
344
+ return NotImplemented
345
+ newCoordinates = self.coordinates + apoint.coordinates
346
+
347
+ elif isinstance(apoint, (np.ndarray, list)):
348
+
349
+ newCoordinates = self.coordinates + Point.__coords__(apoint)
350
+
351
+ # For the affine transforms, the 4th digit must be set to 1 (it has been modified above)
352
+ newCoordinates[-1] = 1
353
+
354
+ self.coordinates = newCoordinates
355
+
356
+ return self
357
+
358
+ def expressIn(self,targetFrame):
359
+ """
360
+ expressIn(self,targetFrame)
361
+ """
362
+ if targetFrame == self.ref:
363
+ """
364
+ targetFrame == self.ref
365
+ We're after the definition of self
366
+ """
367
+ result = self.coordinates
368
+ else:
369
+ """
370
+ We're after the coordinates of self, i.e. the definition of self in targetFrame
371
+
372
+ We know the coordinates in self.ref
373
+ #We need to apply the transformation from targetFrame to self.ref
374
+ self.ref --> self (self.transformation)
375
+ """
376
+ #transform = targetFrame.getTransformationFrom(self.ref)
377
+ transform = self.ref.getPassiveTransformationTo(targetFrame)
378
+ if self.debug: print("transform \n{0}".format(transform))
379
+ result = np.dot(transform,self.coordinates)
380
+ return result
381
+
382
+ def changeRef(self,targetFrame):
383
+ """
384
+ We redefine self as attached to another reference frame
385
+ . calculate self's coordinates in the new reference frame
386
+ . update the definition
387
+ """
388
+ newCoordinates = self.expressIn(targetFrame)
389
+ self.setCoordinates(newCoordinates)
390
+ self.ref = targetFrame
391
+ return
392
+
393
+
394
+
395
+
396
+ class Points():
397
+ """
398
+ A Points object is a collection of Point objects.
399
+
400
+ Points can be constructed from either a numpy.ndarray of shape 3 x n or 4 x n, or
401
+ a list of Point objects. The coordinates of the Point objects are transferred to
402
+ the desired ReferenceFrame and concatenated in the list order.
403
+
404
+ When automatically generated a Points object name consists in a capital 'P' followed by
405
+ three lower case letters. A Point can be extracted from a Points object by its
406
+ position in the coordinates array (see below).
407
+
408
+ """
409
+ debug=0
410
+ def __init__(self, coordinates, ref,name=None):
411
+ """
412
+ Points.__init__(self, coordinates, ref, name=None)
413
+
414
+ Constructor
415
+
416
+ coordinates : must be of one of the following type:
417
+ * numpy.ndarray:
418
+ 4xn matrix defining this system in "ref" system
419
+ (3 being x,y,z + an additional 1 for the affine operations)
420
+ * list of Point objects:
421
+ the coordinates of the Point objects are extracted in the order of the list
422
+ and concatenated in a numpy.ndarray
423
+
424
+ ref : reference system in which self is defined
425
+
426
+ Both parameters are mandatory.
427
+ """
428
+
429
+ # TODO: accept a list made of Point and Points rather than strictly Point
430
+
431
+ if isinstance(coordinates, list):
432
+ coordinateList = []
433
+ for apoint in coordinates:
434
+ if not isinstance(apoint, Point):
435
+ raise ValueError("If the input is a list, all items in it must be Point(s) objects")
436
+ coordinateList.append(apoint.expressIn(ref))
437
+ self.setCoordinates(np.array(coordinateList).T)
438
+ elif isinstance(coordinates, np.ndarray):
439
+ self.setCoordinates(coordinates)
440
+ else:
441
+ raise ValueError("The input must be either a numpy.ndarray or a list of Point objects")
442
+
443
+ self.ref = ref
444
+
445
+ self.setName(name)
446
+
447
+ return
448
+
449
+ def __repr__(self):
450
+ return "{0} (ref {1})".format(self.coordinates[:-1], self.ref.name)
451
+
452
+ def __str__(self):
453
+ return "{1} (ref {2}), name {0}".format(self.name, self.coordinates[:-1], self.ref.name)
454
+
455
+ @staticmethod
456
+ def __coords__(coordinates):
457
+ """
458
+ Formats 3xn or 4xn input lists or np.arrays into 4xn np.array coordinates
459
+ Static --> can be called 'from outside', without passing a Points object
460
+ """
461
+ if isinstance(coordinates, Point):
462
+ return coordinates.coordinates
463
+ elif isinstance(coordinates, np.ndarray):
464
+
465
+ if coordinates.shape[0] not in [3,4]:
466
+ raise ValueError("Input coordinates array must be 3 x n or 4 x n")
467
+
468
+ if coordinates.shape[0]==3:
469
+ newCoords = np.ones([4,coordinates.shape[1]])
470
+ newCoords[:3,:] = coordinates
471
+ coordinates = newCoords
472
+ return coordinates
473
+ else:
474
+ raise ValueError("input must be a list, numpy.ndarray or Point")
475
+
476
+ def setName(self,name=None):
477
+ if name is None:
478
+ self.name = 'P'+ ''.join(random.choices(string.ascii_lowercase, k=2))+\
479
+ ''.join(random.choices(string.ascii_uppercase, k=1))
480
+ else:
481
+ self.name = name
482
+
483
+ def setCoordinates(self,coordinates):
484
+ #coordinates = list(coordinates)
485
+ #if len(coordinates)==3: coordinates.append(1)
486
+ coordinates = Points.__coords__(coordinates)
487
+ self.coordinates = coordinates
488
+
489
+ self.x = self.coordinates[0,:]
490
+ self.y = self.coordinates[1,:]
491
+ self.z = self.coordinates[2,:]
492
+
493
+ return
494
+
495
+ def getCoordinates(self,ref=None):
496
+ if ref is None:
497
+ return self.coordinates
498
+ else:
499
+ return self.expressIn(ref)
500
+
501
+ def expressIn(self,targetFrame):
502
+ """
503
+ expressIn(self,targetFrame)
504
+ """
505
+ if targetFrame == self.ref:
506
+ """
507
+ targetFrame == self.ref
508
+ We're after the definition of self
509
+ """
510
+ result = self.coordinates
511
+ else:
512
+ """
513
+ We're after the coordinates of self, i.e. the definition of self in targetFrame
514
+
515
+ We know the coordinates in self.ref
516
+ #We need to apply the transformation from targetFrame to self.ref
517
+ self.ref --> self (self.transformation)
518
+ """
519
+ #transform = targetFrame.getTransformationFrom(self.ref)
520
+ transform = self.ref.getPassiveTransformationTo(targetFrame)
521
+ if self.debug: print("transform \n{0}".format(transform))
522
+ result = np.dot(transform,self.coordinates)
523
+ return result
524
+
525
+ def changeRef(self,targetFrame):
526
+ """
527
+ We redefine self as attached to another reference frame
528
+ . calculate self's coordinates in the new reference frame
529
+ . update the definition
530
+ """
531
+ newCoordinates = self.expressIn(targetFrame)
532
+ self.setCoordinates(newCoordinates)
533
+ self.ref = targetFrame
534
+ return
535
+
536
+ def getPoint(self,index,name=None):
537
+ """
538
+ Returns the point with coordinates self.coordinates[:,index], in reference frame pts.ref
539
+ """
540
+ return Point(self.coordinates[:,index],ref=self.ref,name=name)
541
+
542
+ get = getPoint
543
+
544
+ def bestFittingPlane(self,fitPlane="xy", usesvd=False,verbose=True):
545
+ """
546
+ bestFittingPlane(self,fitPlane="xy", usesvd=False,verbose=True)
547
+
548
+ fitPlane in ['xy,'yz','zx']
549
+ usesvd see transform3d_addon.affine_matrix_from_points.__doc__
550
+
551
+ OUTPUT
552
+ returns the reference Frame having as X-Y plane the plane best fitting all points in self
553
+ """
554
+ # Import necessary due to a circular dependency between Point and ReferenceFrame
555
+ from egse.coordinates.referenceFrame import ReferenceFrame
556
+
557
+ debug=True
558
+
559
+ a,b,c = self.fitPlane(fitPlane=fitPlane,verbose=verbose)
560
+ #print (f"a {a}, b {b}, c {c}")
561
+ #print()
562
+
563
+ unitaxes = Points.fromPlaneParameters(a,b,c,ref=self.ref,plane=fitPlane)
564
+ #print(f"Unitaxes coordinates \n{np.round(unitaxes.coordinates,3)}")
565
+ #print()
566
+
567
+
568
+ # unitaxes contain 3 unit axes and an origin
569
+ # => the unit vectors do NOT belong to the target plane
570
+ # => they must be translated before
571
+ unitcoords = unitaxes.coordinates
572
+ for ax in range(3):
573
+ unitcoords[:3,ax] += unitcoords[:3,3]
574
+
575
+ newaxes = Points(unitcoords, ref=self.ref)
576
+
577
+ #print(f"newaxes {np.round(newaxes.coordinates,3)}")
578
+
579
+ selfaxes = Points(np.identity(4),ref=self.ref)
580
+
581
+ transform = t3add.affine_matrix_from_points(selfaxes.coordinates[:3,:], newaxes.coordinates[:3,:], shear=False, scale=False, usesvd=usesvd)
582
+
583
+
584
+ if debug:
585
+ transform2 = t3add.rigid_transform_3D(selfaxes.coordinates[:3,:], newaxes.coordinates[:3,:], verbose=verbose)
586
+
587
+ if verbose:
588
+ print()
589
+ print(f"Transform \n{np.round(transform,3)}")
590
+ if debug:
591
+ print()
592
+ print(f"Transform2 \n{np.round(transform2,3)}")
593
+ print()
594
+ print(f"Both methods consistent ? {np.allclose(transform, transform2)}")
595
+
596
+ return ReferenceFrame(transformation=transform, ref=self.ref)
597
+
598
+
599
+
600
+ def fitPlane(self,fitPlane="xy",verbose=True):
601
+ """
602
+ fitPlane(self,fitPlane="xy",verbose=True)
603
+
604
+ :returns: the best fitting parameters a, b, c corresponding to the fitted plane
605
+
606
+ :param fitPlane: defines the expression of the fitted plane in ['xy','yz','zx']:
607
+ 'xy' : z = ax + by + c
608
+ 'yz' : x = ay + bz + c
609
+ 'zx' : y = az + bx + c
610
+
611
+ :param verbose: default = True
612
+ """
613
+ xyz = [self.x,self.y,self.z]
614
+
615
+ ndata = len(xyz[0])
616
+
617
+ # Account for cases
618
+ startingIndex={'xy':0,'yz':1,'zx':2}[fitPlane]
619
+
620
+ # System matrix
621
+ A = np.vstack([xyz[startingIndex],xyz[(startingIndex+1)%3], np.ones(ndata)]).T
622
+
623
+ # Solve linear equations
624
+ a, b, c = np.linalg.lstsq(A, xyz[(startingIndex+2)%3],rcond=None)[0]
625
+
626
+ # Print results on screen
627
+ if verbose:
628
+ hprint = {'xy':'z = ax + by + c','yz':'x = ay + bz + c','zx':'y = az + bx + c'}
629
+ print(f'{hprint[fitPlane]} : \n a = {a:7.3e} \n b = {b:7.3e} \n c = {c:7.3e}')
630
+
631
+ return a,b,c
632
+
633
+ @classmethod
634
+ def fromPlaneParameters(cls,a,b,c,ref,plane='xy',verbose=False):
635
+ """fromPlaneParameters(cls,a,b,c,ref,plane='xy',verbose=False)
636
+
637
+ Returns a Points object describing the unit axes and the origin of the reference frame defined by
638
+ the input parameters.
639
+
640
+ :param plane: The plane definition depends on the "plane" parameter plane must be in ["xy","yz","zx"]
641
+ 'xy' : z = ax + by + c (origin defined at x = y = 0)
642
+ 'yz' : x = ay + bz + c --> NOT IMPLEMENTED
643
+ 'zx' : y = az + bx + c --> NOT IMPLEMENTED
644
+
645
+ :returns: A Points with 4 Point objects corresponding to
646
+ - the 3 unit axes defining a coordinate system with this plane as "plane" plane
647
+ - the origin
648
+ in this order (origin last)
649
+ """
650
+ if plane != 'xy':
651
+ print(f"WARNING: plane = {plane} NOT IMPLEMENTED")
652
+ # p0 : on the Z-axis
653
+ # x = y = 0
654
+ p0 = np.array([0,0,c])
655
+
656
+ # pxy : on the intersection between target plane and plane // to xy passing through z=c
657
+ if np.abs(b)>1.e-5:
658
+ # z = c, x = 1
659
+ pxy = np.array([1,-a/float(b),c])
660
+ else:
661
+ # z = c, y = 1
662
+ pxy = np.array([-b/float(a),1,c])
663
+
664
+ # pyz : intersection of target plane and Y-Z plane
665
+ # x = 0, y = 1
666
+ pyz = np.array([0,1,b+c])
667
+
668
+ """
669
+ # pzx : intersection of target plane and Z-X plane
670
+ if np.abs(a)>1.e-3:
671
+ # y = 0, z = 1
672
+ pzx = np.array([(1-c)/float(a),0,1])
673
+ else:
674
+ # y = 0, x = 1
675
+ pzx = np.array([1,0,a+c])
676
+ """
677
+ #
678
+ # xunit = unit vector from [0,0,0] along the intersection between target plane and plane // to xy passing through z=c
679
+ xunit = (pxy - p0)/np.linalg.norm(pxy-p0)
680
+ # ytemp = unit vector from [0,0,0] along the intersection between target plane and Y-Z plane
681
+ # ytemp is in the target plane, but not perpendicular to xunit
682
+ # its norm doesn't matter
683
+ ytemp = (pyz - p0)#/np.linalg.norm(pyz-p0)
684
+
685
+ # xunit and ytemp are both in the plane
686
+ # => zunit is perpendicular to both
687
+ zunit = np.cross(xunit,ytemp)
688
+ zunit /= np.linalg.norm(zunit)
689
+
690
+ # yunit completes the right handed reference frame
691
+ # The renormalisation shouldn't be necessary...
692
+ yunit = np.cross(zunit,xunit)
693
+ yunit /= np.linalg.norm(yunit)
694
+
695
+ xu = Point(xunit,ref=ref)
696
+ yu = Point(yunit,ref=ref)
697
+ zu = Point(zunit,ref=ref)
698
+ origin = Point(p0,ref=ref)
699
+
700
+ if verbose:
701
+ print(f'xunit {xunit}')
702
+ print(f'yunit {yunit}')
703
+ print(f'zunit {zunit}')
704
+ print(f'origin {p0}')
705
+ print()
706
+
707
+ return cls([xu,yu,zu,origin],ref=ref)