ModuleGeometry

Contains a bunch of useful function for geometric transformations and measurements. Usually these don't need to be called by users of ART, but they may be useful.

Some general conventions:

  • As much as possible, avoid having lists of points or vectors transiting between functions. Instead, use the Vector and Point classes defined at the end of this file.
  • Functions operating on Rays should preferentially operate on lists of Rays, not individual Rays. The reason for that is that it's fairly rare to manipluate a single Ray, and it's easier to just put it in a single-element list and call the function that way.

Created in 2019

@author: Anthony Guillaume + Stefan Haessler + André Kalouguine

  1"""
  2Contains a bunch of useful function for geometric transformations and measurements.
  3Usually these don't need to be called by users of ART, but they may be useful.
  4
  5Some general conventions:
  6- As much as possible, avoid having lists of points or vectors transiting between functions.
  7    Instead, use the Vector and Point classes defined at the end of this file.
  8- Functions operating on Rays should preferentially operate on lists of Rays, not individual Rays.
  9    The reason for that is that it's fairly rare to manipluate a single Ray, and it's easier to
 10    just put it in a single-element list and call the function that way.
 11
 12Created in 2019
 13
 14@author: Anthony Guillaume + Stefan Haessler + André Kalouguine
 15"""
 16# %% Modules
 17import numpy as np
 18import quaternion
 19from functools import singledispatch
 20import logging
 21import math
 22
 23logger = logging.getLogger(__name__)
 24
 25# %% Points and Vectors classes
 26class Vector(np.ndarray):
 27    def __new__(cls, input_array):
 28        input_array = np.asarray(input_array)
 29        if input_array.ndim > 1:
 30            return VectorArray(input_array)
 31        obj = input_array.view(cls)  # Ensure we are viewing as `Vector`
 32        return obj
 33    @property
 34    def norm(self):
 35        return np.linalg.norm(self)
 36    def normalized(self):
 37        return self / self.norm
 38    def __add__(self, other):
 39        if isinstance(other, Point):
 40            return Point(super().__add__(other))
 41        return Vector(super().__add__(other))
 42    def __sub__(self, other):
 43        if isinstance(other, Point):
 44            return Point(super().__sub__(other))
 45        return Vector(super().__sub__(other))
 46    def translate(self, vector):
 47        return self
 48    def rotate(self,q):
 49        return Vector((q*np.quaternion(0,*self)*q.conj()).imag)
 50    def from_basis(self, r0, r, q):
 51        return self.rotate(q)
 52    def to_basis(self, r0, r, q):
 53        return self.rotate(q.conj())
 54    def _add_dimension(self, value=0):
 55        return Vector(np.concatenate((self, [value])))
 56    def __hash__(self) -> int:
 57        vector_tuple = tuple(self.reshape(1, -1)[0])
 58        return hash(vector_tuple)
 59class VectorArray(np.ndarray):
 60    def __new__(cls, input_array):
 61        input_array = np.asarray(input_array)
 62        if input_array.ndim == 1:
 63            return Vector(input_array)
 64        obj = input_array.view(cls)  # Ensure we are viewing as `Vector`
 65        return obj
 66    @property
 67    def norm(self):
 68        return np.linalg.norm(self, axis=1)
 69    def __getitem__(self, index):
 70        return Vector(super().__getitem__(index))
 71    def normalized(self):
 72        return self / self.norm[:, np.newaxis]
 73    def __add__(self, other):
 74        if isinstance(other, Point):
 75            return PointArray(super().__add__(other))
 76        return VectorArray(super().__add__(other))
 77    def __sub__(self, other):
 78        if isinstance(other, Point):
 79            return PointArray(super().__sub__(other))
 80        return VectorArray(super().__sub__(other))
 81    def translate(self, vector):
 82        return self
 83    def rotate(self,q):
 84        return VectorArray(quaternion.rotate_vectors(q, self))
 85    def from_basis(self, r0, r, q):
 86        return self.rotate(q)
 87    def to_basis(self, r0, r, q):
 88        return self.rotate(q.conj())
 89    def _add_dimension(self, values=0):
 90        if values == 0:
 91            values = np.zeros((self.shape[0], 1))
 92        if not isinstance(values, np.ndarray):
 93            values = np.array(values)
 94        return VectorArray(np.concatenate((self, values), axis=1))
 95    
 96class Point(np.ndarray):
 97    def __new__(cls, input_array):
 98        input_array = np.asarray(input_array)
 99        if input_array.ndim > 1:
100            return PointArray(input_array)
101        obj = input_array.view(cls)  # Ensure we are viewing as `Point`
102        return obj
103    def __add__(self, other):
104        return Point(super().__add__(other))
105    def __sub__(self, other):
106        return Vector(super().__sub__(other))
107    def translate(self, vector):
108        return Point(super()._add__(vector))
109    def rotate(self,q):
110        return (self-Origin).rotate(q)
111    def from_basis(self, r0, r, q):
112        return Point([0,0,0]) + r0 + Vector(self-r0).rotate(q) + r
113    def to_basis(self, r0, r, q):
114        return Point([0,0,0]) + r0 + Vector(self-r0-r).rotate(q.conj())
115    def _add_dimension(self, value=0):
116        return Point(np.concatenate((self, [value])))
117    def __hash__(self) -> int:
118        point_tuple = tuple(self.reshape(1, -1)[0])
119        return hash(point_tuple)
120
121class PointArray(np.ndarray):
122    def __new__(cls, input_array):
123        input_array = np.asarray(input_array)
124        if input_array.ndim == 1:
125            return Point(input_array)
126        obj = input_array.view(cls)  # Ensure we are viewing as `Point`
127        return obj
128    def __getitem__(self, index):
129        return Point(super().__getitem__(index))
130    def __add__(self, other):
131        return PointArray(super().__add__(other))
132    def __sub__(self, other):
133        return VectorArray(super().__sub__(other))
134    def translate(self, vector):
135        return PointArray(super().__add__(vector))
136    def rotate(self,q):
137        return PointArray(quaternion.rotate_vectors(q, self))
138    def from_basis(self, r0, r, q):
139        return Point([0,0,0]) + r0 + VectorArray(self-r0).rotate(q) + r
140        #return PointArray([Point([0,0,0]) + r0 + (p-r0).rotate(q) + r for p in self])
141    def to_basis(self, r0, r, q):
142        return Point([0,0,0]) + r0 + VectorArray(self-r0-r).rotate(q.conj())
143        #return PointArray([Point([0,0,0]) + r0 + (p-r0-r).rotate(q.conj()) for p in self])
144    def _add_dimension(self, values=0):
145        if values == 0:
146            values = np.zeros((self.shape[0], 1))
147        if not isinstance(values, np.ndarray):
148            values = np.array(values)
149        return PointArray(np.concatenate((self, values), axis=1))
150    
151Origin = Point([0,0,0])
152
153# %% More traditional vector operations that don't belong in the classes
154def Normalize(vector):
155    """
156    Normalize Vector.
157    Obsolete, use use the mgeo.Vector class instead as it has a `normalize` method.
158    """
159    return vector / np.linalg.norm(vector)
160
161def VectorPerpendicular(vector):
162    """
163    Find a perpendicular 3D vector in some arbitrary direction
164    Undefined behavior, use with caution. There is no unique perpendicular vector to a 3D vector.
165    """
166    logger.warning("VectorPerpendicular is undefined behavior. There is no unique perpendicular vector to a 3D vector.")
167    if abs(vector[0]) < 1e-15:
168        return Vector([1, 0, 0])
169    if abs(vector[1]) < 1e-15:
170        return Vector([0, 1, 0])
171    if abs(vector[2]) < 1e-15:
172        return Vector([0, 0, 1])
173
174    # set arbitrarily a = b =1
175    return Vector([1, 1, -1.0 * (vector[0] + vector[1]) / vector[2]]).normalized()
176
177def AngleBetweenTwoVectors(U, V):
178    """
179    Return the angle in radians between the vectors U and V ; formula from W.Kahan
180    Value in radians between 0 and pi.
181    """
182    u = np.linalg.norm(U)
183    v = np.linalg.norm(V)
184    return 2 * np.arctan2(np.linalg.norm(U * v - V * u), np.linalg.norm(U * v + V * u))
185
186def SymmetricalVector(V, SymmetryAxis):
187    """
188    Return the symmetrical vector to V
189    """
190    q = QRotationAroundAxis(SymmetryAxis, np.pi)
191    return V.rotate(q)
192
193def normal_add(N1, N2):
194    """
195    Simple function that takes in two normal vectors of a deformation and calculates
196    the total normal vector if the two deformations were individually applied.
197    Be very careful, this *only* works when the surface is z = f(x,y) and when 
198    the deformation is small.
199    Might be made obsolete when shifting to a modernised deformation system.
200    """
201    normal1 = N1.normalized()
202    normal2 = N2.normalized()
203    grad1 = -normal1[:2] / normal1[2]
204    grad2 = -normal2[:2] / normal2[2]
205    grad = grad1 + grad2
206    total_normal = np.append(-grad, 1)
207    return Vector(total_normal).normalized()
208
209# %% Intersection finding
210def IntersectionLinePlane(A, u, P, n):
211    """
212    Return the intersection point between a line and a plane.
213    A is a point of the line, u a vector of the line ; P is a point of the plane, n a normal vector
214    Line's equation : OM = u*t + OA , t a real
215    Plane's equation : n.OP - n.OM = 0
216    """
217    t = np.dot(n, -A + P) / np.dot(u, n)
218    I = u * t + A
219    return I
220
221def IntersectionRayListZPlane(RayList, Z=np.array([0])):
222    """
223    Return the intersection of a list of rays with a different planes with equiations z = Z[i]
224    Basically, by default it returns the intersection of the rays with the Z=0 plane but you can 
225    give it a few values of Z and it should be faster than calling it multiple times.
226    This should let us quickly find the optimal position of the detector as well as trace the caustics.
227    If a ray does not intersect the plane... it should replace that point with a NaN. 
228    """
229    Positions = np.vstack([i.point for i in RayList])
230    Vectors = np.vstack([i.vector for i in RayList])
231    non_zero = Vectors[:,2] != 0
232    Positions = Positions[non_zero]
233    Vectors = Vectors[non_zero]
234    Z = Z[:, np.newaxis]
235    A = Positions[:,2]-Z
236    B = -Vectors[:,2]
237    #times = (Positions[:,2]-Z)/Vectors[:,2]
238    #return A,B
239    with np.errstate(divide='ignore', invalid='ignore'):
240        times = np.divide(A, B, where=(B != 0), out=np.full_like(A, np.nan))
241        #times[times < 0] = np.nan  # Set negative results to NaN
242    #return times
243    #positive_times = times >= 0
244    intersect_positions = Positions[:, :2] + times[:, :, np.newaxis] * Vectors[:, :2]
245    result = []
246    for i in range(Z.shape[0]):
247        # For each plane, we find the intersection points
248        #valid_intersections = intersect_positions[i][positive_times[i]]
249        valid_intersections = intersect_positions[i]
250        result.append(PointArray(valid_intersections))
251    return result
252
253
254# %% Geometrical utilities for plotting
255def SpiralVogel(NbPoint, Radius):
256    """
257    Return a NbPoint x 2 matrix of 2D points representative of Vogel's spiral with radius Radius
258    Careful, contrary to most of the code, this is *not* in the 
259    ARTcore.Vector or ARTcore.Point format. It is a simple numpy array. 
260    The reason is that this is a utility function that can be used both to define directions
261    and to generate grids of points.
262    """
263    GoldenAngle = np.pi * (3 - np.sqrt(5))
264    r = np.sqrt(np.arange(NbPoint) / NbPoint) * Radius
265
266    theta = GoldenAngle * np.arange(NbPoint)
267
268    Matrix = np.zeros((NbPoint, 2))
269    Matrix[:, 0] = np.cos(theta)
270    Matrix[:, 1] = np.sin(theta)
271    Matrix = Matrix * r.reshape((NbPoint, 1))
272
273    return Matrix
274
275def find_hull(points):
276    """
277    Find the convex hull of a set of points using a greedy algorithm.
278    This is used to create a polygon that encloses the points.
279    """
280    # start from leftmost point
281    current_point = min(range(len(points)), key=lambda i: points[i][0])
282    # initialize hull with current point
283    hull = [current_point]
284    # initialize list of linked points
285    linked = []
286    # continue until all points have been linked
287    while len(linked) < len(points) - 1:
288        # initialize minimum distance and closest point
289        min_distance = math.inf
290        closest_point = None
291        # find closest unlinked point to current point
292        for i, point in enumerate(points):
293            if i not in linked:
294                distance = math.dist(points[current_point], point)
295                if distance < min_distance:
296                    min_distance = distance
297                    closest_point = i
298        # add closest point to hull and linked list
299        hull.append(closest_point)
300        linked.append(closest_point)
301        # update current point
302        current_point = closest_point
303    # add link between last point and first point
304    hull.append(hull[0])
305    # convert hull to a list of pairs of indices
306    indices = [[hull[i], hull[i + 1]] for i in range(len(hull) - 1)]
307    return indices
308
309
310# %% Solvers and utilities for solving equations
311def SolverQuadratic(a, b, c):
312    """
313    Solve the quadratic equation a*x^2 + b*x +c = 0 ; keep only real solutions
314    """
315    Solution = np.roots([a, b, c])
316    RealSolution = []
317
318    for k in range(len(Solution)):
319        if abs(Solution[k].imag) < 1e-15:
320            RealSolution.append(Solution[k].real)
321
322    return RealSolution
323
324
325def SolverQuartic(a, b, c, d, e):
326    """
327    Solve the quartic equation a*x^4 + b*x^3 +c*x^2 + d*x + e = 0 ; keep only real solutions
328    """
329    Solution = np.roots([a, b, c, d, e])
330    RealSolution = []
331
332    for k in range(len(Solution)):
333        if abs(Solution[k].imag) < 1e-15:
334            RealSolution.append(Solution[k].real)
335
336    return RealSolution
337
338
339def KeepPositiveSolution(SolutionList):
340    """
341    Keep only positive solution (numbers) in the list
342    """
343    PositiveSolutionList = []
344    epsilon = 1e-12
345    for k in SolutionList:
346        if k > epsilon:
347            PositiveSolutionList.append(k)
348
349    return PositiveSolutionList
350
351
352def KeepNegativeSolution(SolutionList):
353    """
354    Keep only positive solution (numbers) in the list
355    """
356    NegativeSolutionList = []
357    epsilon = -1e-12
358    for k in SolutionList:
359        if k < epsilon:
360            NegativeSolutionList.append(k)
361
362    return NegativeSolutionList
363
364
365# %% Point geometry tools
366def ClosestPoint(A: Point, Points: PointArray):
367    """
368    Given a reference point A and an array of points, return the index of the point closest to A
369    """
370    distances = (Points-A).norm
371    return np.argmin(distances)
372
373def DiameterPointArray(Points: PointArray):
374    """
375    Return the diameter of the smallest circle (for 2D points) 
376    or sphere (3D points) including all the points.
377    """
378    if len(Points) == 0:
379        return None
380    return float(np.ptp(Points, axis=0).max())
381
382def CentrePointList(Points):
383    """
384    Shift all 2D-points in PointList so as to center the point-cloud on the origin [0,0].
385    """
386    return Points - np.mean(Points, axis=0)
387
388# %% Solid object orientation
389
390def RotateSolid(Object, q):
391    """
392    Rotate object around basepoint by quaternion q
393    """
394    Object.q = q*Object.q
395
396def TranslateSolid(Object, T):
397    """
398    Translate object by vector T
399    """
400    Object.r = Object.r + T
401
402def RotateSolidAroundInternalPointByQ(Object, q, P):
403    """
404    Rotate object around P by quaternion q where P is in the object's frame
405    """
406    pass #TODO
407
408def RotateSolidAroundExternalPointByQ(Object, q, P):
409    """Rotate object around P by quaternion q, where P is in the global frame"""
410    pass #TODO
411
412
413# %% Signed distance functions 
414
415def SDF_Rectangle(Point, SizeX, SizeY):
416    """Signed distance function for a rectangle centered at the origin"""
417    d = np.abs(Point[:2]) - np.array([SizeX, SizeY]) / 2
418    return (np.linalg.norm(np.maximum(d, 0)) + np.min(np.max(d, 0)))/2
419
420def SDF_Circle(Point, Radius):
421    """Signed distance function for a circle centered at the origin"""
422    return np.linalg.norm(Point[:2]) - Radius
423
424def Union_SDF(SDF1, SDF2):
425    """Union of two signed distance functions"""
426    return np.minimum(SDF1, SDF2)
427
428def Difference_SDF(SDF1, SDF2):
429    """Difference of two signed distance functions"""
430    return np.maximum(SDF1, -SDF2)
431
432def Intersection_SDF(SDF1, SDF2):
433    """Intersection of two signed distance functions"""
434    return np.maximum(SDF1, SDF2)
435
436
437
438# %% Quaternion calculations
439def QRotationAroundAxis(Axis, Angle):
440    """
441    Return quaternion for rotation by Angle (in rad) around Axis
442    """
443    rot_axis = Normalize(np.array([0.0] + Axis))
444    axis_angle = (Angle * 0.5) * rot_axis
445    qlog = np.quaternion(*axis_angle)
446    q = np.exp(qlog)
447    return q
448
449def QRotationVector2Vector(Vector1, Vector2):
450    """
451    Return a possible quaternion (among many) that would rotate Vector1 into Vector2.
452    Undefined behavior, use with caution. There is no unique quaternion that rotates one vector into another.
453    """
454    Vector1 = Normalize(Vector1)
455    Vector2 = Normalize(Vector2)
456    a = np.cross(Vector1, Vector2)
457    return np.quaternion(1 + np.dot(Vector1, Vector2), *a).normalized()
458
459def QRotationVectorPair2VectorPair(InitialVector1, Vector1, InitialVector2, Vector2):
460    """
461    Return the only quaternion that rotates InitialVector1 to Vector1 and InitialVector2 to Vector2.
462    Please ensure orthogonality two input and two output vectors.
463    """
464    Vector1 = Normalize(Vector1)
465    Vector2 = Normalize(Vector2)
466    Vector3 = Normalize(np.cross(Vector1,Vector2))
467    InitialVector1 = Normalize(InitialVector1)
468    InitialVector2 = Normalize(InitialVector2)
469    InitialVector3 = Normalize(np.cross(InitialVector1,InitialVector2))
470    rot_2Initial = np.zeros((3,3))
471    rot_2Initial[:,0] = InitialVector1
472    rot_2Initial[:,1] = InitialVector2
473    rot_2Initial[:,2] = InitialVector3
474    rot_2Final = np.zeros((3,3))
475    rot_2Final[:,0] = Vector1
476    rot_2Final[:,1] = Vector2
477    rot_2Final[:,2] = Vector3
478    q2Init = quaternion.from_rotation_matrix(rot_2Initial)
479    q2Fin = quaternion.from_rotation_matrix(rot_2Final)
480    return (q2Fin/q2Init).normalized()
481
482
483# %% RayList stuff
484def RotationRayList(RayList, q):
485    """Like RotationPointList but with a list of Ray objects"""
486    return [i.rotate(q) for i in RayList]
487
488def TranslationRayList(RayList, T):
489    """Translate a RayList by vector T"""
490    return [i.translate(T) for i in RayList]
logger = <Logger ARTcore.ModuleGeometry (WARNING)>
class Vector(numpy.ndarray):
27class Vector(np.ndarray):
28    def __new__(cls, input_array):
29        input_array = np.asarray(input_array)
30        if input_array.ndim > 1:
31            return VectorArray(input_array)
32        obj = input_array.view(cls)  # Ensure we are viewing as `Vector`
33        return obj
34    @property
35    def norm(self):
36        return np.linalg.norm(self)
37    def normalized(self):
38        return self / self.norm
39    def __add__(self, other):
40        if isinstance(other, Point):
41            return Point(super().__add__(other))
42        return Vector(super().__add__(other))
43    def __sub__(self, other):
44        if isinstance(other, Point):
45            return Point(super().__sub__(other))
46        return Vector(super().__sub__(other))
47    def translate(self, vector):
48        return self
49    def rotate(self,q):
50        return Vector((q*np.quaternion(0,*self)*q.conj()).imag)
51    def from_basis(self, r0, r, q):
52        return self.rotate(q)
53    def to_basis(self, r0, r, q):
54        return self.rotate(q.conj())
55    def _add_dimension(self, value=0):
56        return Vector(np.concatenate((self, [value])))
57    def __hash__(self) -> int:
58        vector_tuple = tuple(self.reshape(1, -1)[0])
59        return hash(vector_tuple)

ndarray(shape, dtype=float, buffer=None, offset=0, strides=None, order=None)

An array object represents a multidimensional, homogeneous array of fixed-size items. An associated data-type object describes the format of each element in the array (its byte-order, how many bytes it occupies in memory, whether it is an integer, a floating point number, or something else, etc.)

Arrays should be constructed using array, zeros or empty (refer to the See Also section below). The parameters given here refer to a low-level method (ndarray(...)) for instantiating an array.

For more information, refer to the numpy module and examine the methods and attributes of an array.

Parameters

(for the __new__ method; see Notes below)

shape : tuple of ints Shape of created array. dtype : data-type, optional Any object that can be interpreted as a numpy data type. Default is numpy.float64. buffer : object exposing buffer interface, optional Used to fill the array with data. offset : int, optional Offset of array data in buffer. strides : tuple of ints, optional Strides of data in memory. order : {'C', 'F'}, optional Row-major (C-style) or column-major (Fortran-style) order.

Attributes

T : ndarray Transpose of the array. data : buffer The array's elements, in memory. dtype : dtype object Describes the format of the elements in the array. flags : dict Dictionary containing information related to memory use, e.g., 'C_CONTIGUOUS', 'OWNDATA', 'WRITEABLE', etc. flat : numpy.flatiter object Flattened version of the array as an iterator. The iterator allows assignments, e.g., x.flat = 3 (See ndarray.flat for assignment examples; TODO). imag : ndarray Imaginary part of the array. real : ndarray Real part of the array. size : int Number of elements in the array. itemsize : int The memory use of each array element in bytes. nbytes : int The total number of bytes required to store the array data, i.e., itemsize * size. ndim : int The array's number of dimensions. shape : tuple of ints Shape of the array. strides : tuple of ints The step-size required to move from one element to the next in memory. For example, a contiguous (3, 4) array of type int16 in C-order has strides (8, 2). This implies that to move from element to element in memory requires jumps of 2 bytes. To move from row-to-row, one needs to jump 8 bytes at a time (2 * 4). ctypes : ctypes object Class containing properties of the array needed for interaction with ctypes. base : ndarray If the array is a view into another array, that array is its base (unless that array is also a view). The base array is where the array data is actually stored.

See Also

array : Construct an array. zeros : Create an array, each element of which is zero. empty : Create an array, but leave its allocated memory unchanged (i.e., it contains "garbage"). dtype : Create a data-type. numpy.typing.NDArray : An ndarray alias :term:generic <generic type> w.r.t. its dtype.type <numpy.dtype.type>.

Notes

There are two modes of creating an array using __new__:

  1. If buffer is None, then only shape, dtype, and order are used.
  2. If buffer is an object exposing the buffer interface, then all keywords are interpreted.

No __init__ method is needed because the array is fully initialized after the __new__ method.

Examples

These examples illustrate the low-level ndarray constructor. Refer to the See Also section above for easier ways of constructing an ndarray.

First mode, buffer is None:

>>> import numpy as np
>>> np.ndarray(shape=(2,2), dtype=float, order='F')
array([[0.0e+000, 0.0e+000], # random
       [     nan, 2.5e-323]])

Second mode:

>>> np.ndarray((2,), buffer=np.array([1,2,3]),
...            offset=np.int_().itemsize,
...            dtype=int) # offset = 1*itemsize, i.e. skip first element
array([2, 3])
norm
34    @property
35    def norm(self):
36        return np.linalg.norm(self)
def normalized(self):
37    def normalized(self):
38        return self / self.norm
def translate(self, vector):
47    def translate(self, vector):
48        return self
def rotate(self, q):
49    def rotate(self,q):
50        return Vector((q*np.quaternion(0,*self)*q.conj()).imag)
def from_basis(self, r0, r, q):
51    def from_basis(self, r0, r, q):
52        return self.rotate(q)
def to_basis(self, r0, r, q):
53    def to_basis(self, r0, r, q):
54        return self.rotate(q.conj())
class VectorArray(numpy.ndarray):
60class VectorArray(np.ndarray):
61    def __new__(cls, input_array):
62        input_array = np.asarray(input_array)
63        if input_array.ndim == 1:
64            return Vector(input_array)
65        obj = input_array.view(cls)  # Ensure we are viewing as `Vector`
66        return obj
67    @property
68    def norm(self):
69        return np.linalg.norm(self, axis=1)
70    def __getitem__(self, index):
71        return Vector(super().__getitem__(index))
72    def normalized(self):
73        return self / self.norm[:, np.newaxis]
74    def __add__(self, other):
75        if isinstance(other, Point):
76            return PointArray(super().__add__(other))
77        return VectorArray(super().__add__(other))
78    def __sub__(self, other):
79        if isinstance(other, Point):
80            return PointArray(super().__sub__(other))
81        return VectorArray(super().__sub__(other))
82    def translate(self, vector):
83        return self
84    def rotate(self,q):
85        return VectorArray(quaternion.rotate_vectors(q, self))
86    def from_basis(self, r0, r, q):
87        return self.rotate(q)
88    def to_basis(self, r0, r, q):
89        return self.rotate(q.conj())
90    def _add_dimension(self, values=0):
91        if values == 0:
92            values = np.zeros((self.shape[0], 1))
93        if not isinstance(values, np.ndarray):
94            values = np.array(values)
95        return VectorArray(np.concatenate((self, values), axis=1))

ndarray(shape, dtype=float, buffer=None, offset=0, strides=None, order=None)

An array object represents a multidimensional, homogeneous array of fixed-size items. An associated data-type object describes the format of each element in the array (its byte-order, how many bytes it occupies in memory, whether it is an integer, a floating point number, or something else, etc.)

Arrays should be constructed using array, zeros or empty (refer to the See Also section below). The parameters given here refer to a low-level method (ndarray(...)) for instantiating an array.

For more information, refer to the numpy module and examine the methods and attributes of an array.

Parameters

(for the __new__ method; see Notes below)

shape : tuple of ints Shape of created array. dtype : data-type, optional Any object that can be interpreted as a numpy data type. Default is numpy.float64. buffer : object exposing buffer interface, optional Used to fill the array with data. offset : int, optional Offset of array data in buffer. strides : tuple of ints, optional Strides of data in memory. order : {'C', 'F'}, optional Row-major (C-style) or column-major (Fortran-style) order.

Attributes

T : ndarray Transpose of the array. data : buffer The array's elements, in memory. dtype : dtype object Describes the format of the elements in the array. flags : dict Dictionary containing information related to memory use, e.g., 'C_CONTIGUOUS', 'OWNDATA', 'WRITEABLE', etc. flat : numpy.flatiter object Flattened version of the array as an iterator. The iterator allows assignments, e.g., x.flat = 3 (See ndarray.flat for assignment examples; TODO). imag : ndarray Imaginary part of the array. real : ndarray Real part of the array. size : int Number of elements in the array. itemsize : int The memory use of each array element in bytes. nbytes : int The total number of bytes required to store the array data, i.e., itemsize * size. ndim : int The array's number of dimensions. shape : tuple of ints Shape of the array. strides : tuple of ints The step-size required to move from one element to the next in memory. For example, a contiguous (3, 4) array of type int16 in C-order has strides (8, 2). This implies that to move from element to element in memory requires jumps of 2 bytes. To move from row-to-row, one needs to jump 8 bytes at a time (2 * 4). ctypes : ctypes object Class containing properties of the array needed for interaction with ctypes. base : ndarray If the array is a view into another array, that array is its base (unless that array is also a view). The base array is where the array data is actually stored.

See Also

array : Construct an array. zeros : Create an array, each element of which is zero. empty : Create an array, but leave its allocated memory unchanged (i.e., it contains "garbage"). dtype : Create a data-type. numpy.typing.NDArray : An ndarray alias :term:generic <generic type> w.r.t. its dtype.type <numpy.dtype.type>.

Notes

There are two modes of creating an array using __new__:

  1. If buffer is None, then only shape, dtype, and order are used.
  2. If buffer is an object exposing the buffer interface, then all keywords are interpreted.

No __init__ method is needed because the array is fully initialized after the __new__ method.

Examples

These examples illustrate the low-level ndarray constructor. Refer to the See Also section above for easier ways of constructing an ndarray.

First mode, buffer is None:

>>> import numpy as np
>>> np.ndarray(shape=(2,2), dtype=float, order='F')
array([[0.0e+000, 0.0e+000], # random
       [     nan, 2.5e-323]])

Second mode:

>>> np.ndarray((2,), buffer=np.array([1,2,3]),
...            offset=np.int_().itemsize,
...            dtype=int) # offset = 1*itemsize, i.e. skip first element
array([2, 3])
norm
67    @property
68    def norm(self):
69        return np.linalg.norm(self, axis=1)
def normalized(self):
72    def normalized(self):
73        return self / self.norm[:, np.newaxis]
def translate(self, vector):
82    def translate(self, vector):
83        return self
def rotate(self, q):
84    def rotate(self,q):
85        return VectorArray(quaternion.rotate_vectors(q, self))
def from_basis(self, r0, r, q):
86    def from_basis(self, r0, r, q):
87        return self.rotate(q)
def to_basis(self, r0, r, q):
88    def to_basis(self, r0, r, q):
89        return self.rotate(q.conj())
class Point(numpy.ndarray):
 97class Point(np.ndarray):
 98    def __new__(cls, input_array):
 99        input_array = np.asarray(input_array)
100        if input_array.ndim > 1:
101            return PointArray(input_array)
102        obj = input_array.view(cls)  # Ensure we are viewing as `Point`
103        return obj
104    def __add__(self, other):
105        return Point(super().__add__(other))
106    def __sub__(self, other):
107        return Vector(super().__sub__(other))
108    def translate(self, vector):
109        return Point(super()._add__(vector))
110    def rotate(self,q):
111        return (self-Origin).rotate(q)
112    def from_basis(self, r0, r, q):
113        return Point([0,0,0]) + r0 + Vector(self-r0).rotate(q) + r
114    def to_basis(self, r0, r, q):
115        return Point([0,0,0]) + r0 + Vector(self-r0-r).rotate(q.conj())
116    def _add_dimension(self, value=0):
117        return Point(np.concatenate((self, [value])))
118    def __hash__(self) -> int:
119        point_tuple = tuple(self.reshape(1, -1)[0])
120        return hash(point_tuple)

ndarray(shape, dtype=float, buffer=None, offset=0, strides=None, order=None)

An array object represents a multidimensional, homogeneous array of fixed-size items. An associated data-type object describes the format of each element in the array (its byte-order, how many bytes it occupies in memory, whether it is an integer, a floating point number, or something else, etc.)

Arrays should be constructed using array, zeros or empty (refer to the See Also section below). The parameters given here refer to a low-level method (ndarray(...)) for instantiating an array.

For more information, refer to the numpy module and examine the methods and attributes of an array.

Parameters

(for the __new__ method; see Notes below)

shape : tuple of ints Shape of created array. dtype : data-type, optional Any object that can be interpreted as a numpy data type. Default is numpy.float64. buffer : object exposing buffer interface, optional Used to fill the array with data. offset : int, optional Offset of array data in buffer. strides : tuple of ints, optional Strides of data in memory. order : {'C', 'F'}, optional Row-major (C-style) or column-major (Fortran-style) order.

Attributes

T : ndarray Transpose of the array. data : buffer The array's elements, in memory. dtype : dtype object Describes the format of the elements in the array. flags : dict Dictionary containing information related to memory use, e.g., 'C_CONTIGUOUS', 'OWNDATA', 'WRITEABLE', etc. flat : numpy.flatiter object Flattened version of the array as an iterator. The iterator allows assignments, e.g., x.flat = 3 (See ndarray.flat for assignment examples; TODO). imag : ndarray Imaginary part of the array. real : ndarray Real part of the array. size : int Number of elements in the array. itemsize : int The memory use of each array element in bytes. nbytes : int The total number of bytes required to store the array data, i.e., itemsize * size. ndim : int The array's number of dimensions. shape : tuple of ints Shape of the array. strides : tuple of ints The step-size required to move from one element to the next in memory. For example, a contiguous (3, 4) array of type int16 in C-order has strides (8, 2). This implies that to move from element to element in memory requires jumps of 2 bytes. To move from row-to-row, one needs to jump 8 bytes at a time (2 * 4). ctypes : ctypes object Class containing properties of the array needed for interaction with ctypes. base : ndarray If the array is a view into another array, that array is its base (unless that array is also a view). The base array is where the array data is actually stored.

See Also

array : Construct an array. zeros : Create an array, each element of which is zero. empty : Create an array, but leave its allocated memory unchanged (i.e., it contains "garbage"). dtype : Create a data-type. numpy.typing.NDArray : An ndarray alias :term:generic <generic type> w.r.t. its dtype.type <numpy.dtype.type>.

Notes

There are two modes of creating an array using __new__:

  1. If buffer is None, then only shape, dtype, and order are used.
  2. If buffer is an object exposing the buffer interface, then all keywords are interpreted.

No __init__ method is needed because the array is fully initialized after the __new__ method.

Examples

These examples illustrate the low-level ndarray constructor. Refer to the See Also section above for easier ways of constructing an ndarray.

First mode, buffer is None:

>>> import numpy as np
>>> np.ndarray(shape=(2,2), dtype=float, order='F')
array([[0.0e+000, 0.0e+000], # random
       [     nan, 2.5e-323]])

Second mode:

>>> np.ndarray((2,), buffer=np.array([1,2,3]),
...            offset=np.int_().itemsize,
...            dtype=int) # offset = 1*itemsize, i.e. skip first element
array([2, 3])
def translate(self, vector):
108    def translate(self, vector):
109        return Point(super()._add__(vector))
def rotate(self, q):
110    def rotate(self,q):
111        return (self-Origin).rotate(q)
def from_basis(self, r0, r, q):
112    def from_basis(self, r0, r, q):
113        return Point([0,0,0]) + r0 + Vector(self-r0).rotate(q) + r
def to_basis(self, r0, r, q):
114    def to_basis(self, r0, r, q):
115        return Point([0,0,0]) + r0 + Vector(self-r0-r).rotate(q.conj())
class PointArray(numpy.ndarray):
122class PointArray(np.ndarray):
123    def __new__(cls, input_array):
124        input_array = np.asarray(input_array)
125        if input_array.ndim == 1:
126            return Point(input_array)
127        obj = input_array.view(cls)  # Ensure we are viewing as `Point`
128        return obj
129    def __getitem__(self, index):
130        return Point(super().__getitem__(index))
131    def __add__(self, other):
132        return PointArray(super().__add__(other))
133    def __sub__(self, other):
134        return VectorArray(super().__sub__(other))
135    def translate(self, vector):
136        return PointArray(super().__add__(vector))
137    def rotate(self,q):
138        return PointArray(quaternion.rotate_vectors(q, self))
139    def from_basis(self, r0, r, q):
140        return Point([0,0,0]) + r0 + VectorArray(self-r0).rotate(q) + r
141        #return PointArray([Point([0,0,0]) + r0 + (p-r0).rotate(q) + r for p in self])
142    def to_basis(self, r0, r, q):
143        return Point([0,0,0]) + r0 + VectorArray(self-r0-r).rotate(q.conj())
144        #return PointArray([Point([0,0,0]) + r0 + (p-r0-r).rotate(q.conj()) for p in self])
145    def _add_dimension(self, values=0):
146        if values == 0:
147            values = np.zeros((self.shape[0], 1))
148        if not isinstance(values, np.ndarray):
149            values = np.array(values)
150        return PointArray(np.concatenate((self, values), axis=1))

ndarray(shape, dtype=float, buffer=None, offset=0, strides=None, order=None)

An array object represents a multidimensional, homogeneous array of fixed-size items. An associated data-type object describes the format of each element in the array (its byte-order, how many bytes it occupies in memory, whether it is an integer, a floating point number, or something else, etc.)

Arrays should be constructed using array, zeros or empty (refer to the See Also section below). The parameters given here refer to a low-level method (ndarray(...)) for instantiating an array.

For more information, refer to the numpy module and examine the methods and attributes of an array.

Parameters

(for the __new__ method; see Notes below)

shape : tuple of ints Shape of created array. dtype : data-type, optional Any object that can be interpreted as a numpy data type. Default is numpy.float64. buffer : object exposing buffer interface, optional Used to fill the array with data. offset : int, optional Offset of array data in buffer. strides : tuple of ints, optional Strides of data in memory. order : {'C', 'F'}, optional Row-major (C-style) or column-major (Fortran-style) order.

Attributes

T : ndarray Transpose of the array. data : buffer The array's elements, in memory. dtype : dtype object Describes the format of the elements in the array. flags : dict Dictionary containing information related to memory use, e.g., 'C_CONTIGUOUS', 'OWNDATA', 'WRITEABLE', etc. flat : numpy.flatiter object Flattened version of the array as an iterator. The iterator allows assignments, e.g., x.flat = 3 (See ndarray.flat for assignment examples; TODO). imag : ndarray Imaginary part of the array. real : ndarray Real part of the array. size : int Number of elements in the array. itemsize : int The memory use of each array element in bytes. nbytes : int The total number of bytes required to store the array data, i.e., itemsize * size. ndim : int The array's number of dimensions. shape : tuple of ints Shape of the array. strides : tuple of ints The step-size required to move from one element to the next in memory. For example, a contiguous (3, 4) array of type int16 in C-order has strides (8, 2). This implies that to move from element to element in memory requires jumps of 2 bytes. To move from row-to-row, one needs to jump 8 bytes at a time (2 * 4). ctypes : ctypes object Class containing properties of the array needed for interaction with ctypes. base : ndarray If the array is a view into another array, that array is its base (unless that array is also a view). The base array is where the array data is actually stored.

See Also

array : Construct an array. zeros : Create an array, each element of which is zero. empty : Create an array, but leave its allocated memory unchanged (i.e., it contains "garbage"). dtype : Create a data-type. numpy.typing.NDArray : An ndarray alias :term:generic <generic type> w.r.t. its dtype.type <numpy.dtype.type>.

Notes

There are two modes of creating an array using __new__:

  1. If buffer is None, then only shape, dtype, and order are used.
  2. If buffer is an object exposing the buffer interface, then all keywords are interpreted.

No __init__ method is needed because the array is fully initialized after the __new__ method.

Examples

These examples illustrate the low-level ndarray constructor. Refer to the See Also section above for easier ways of constructing an ndarray.

First mode, buffer is None:

>>> import numpy as np
>>> np.ndarray(shape=(2,2), dtype=float, order='F')
array([[0.0e+000, 0.0e+000], # random
       [     nan, 2.5e-323]])

Second mode:

>>> np.ndarray((2,), buffer=np.array([1,2,3]),
...            offset=np.int_().itemsize,
...            dtype=int) # offset = 1*itemsize, i.e. skip first element
array([2, 3])
def translate(self, vector):
135    def translate(self, vector):
136        return PointArray(super().__add__(vector))
def rotate(self, q):
137    def rotate(self,q):
138        return PointArray(quaternion.rotate_vectors(q, self))
def from_basis(self, r0, r, q):
139    def from_basis(self, r0, r, q):
140        return Point([0,0,0]) + r0 + VectorArray(self-r0).rotate(q) + r
141        #return PointArray([Point([0,0,0]) + r0 + (p-r0).rotate(q) + r for p in self])
def to_basis(self, r0, r, q):
142    def to_basis(self, r0, r, q):
143        return Point([0,0,0]) + r0 + VectorArray(self-r0-r).rotate(q.conj())
144        #return PointArray([Point([0,0,0]) + r0 + (p-r0-r).rotate(q.conj()) for p in self])
Origin = Point([0, 0, 0])
def Normalize(vector):
155def Normalize(vector):
156    """
157    Normalize Vector.
158    Obsolete, use use the mgeo.Vector class instead as it has a `normalize` method.
159    """
160    return vector / np.linalg.norm(vector)

Normalize Vector. Obsolete, use use the mgeo.Vector class instead as it has a normalize method.

def VectorPerpendicular(vector):
162def VectorPerpendicular(vector):
163    """
164    Find a perpendicular 3D vector in some arbitrary direction
165    Undefined behavior, use with caution. There is no unique perpendicular vector to a 3D vector.
166    """
167    logger.warning("VectorPerpendicular is undefined behavior. There is no unique perpendicular vector to a 3D vector.")
168    if abs(vector[0]) < 1e-15:
169        return Vector([1, 0, 0])
170    if abs(vector[1]) < 1e-15:
171        return Vector([0, 1, 0])
172    if abs(vector[2]) < 1e-15:
173        return Vector([0, 0, 1])
174
175    # set arbitrarily a = b =1
176    return Vector([1, 1, -1.0 * (vector[0] + vector[1]) / vector[2]]).normalized()

Find a perpendicular 3D vector in some arbitrary direction Undefined behavior, use with caution. There is no unique perpendicular vector to a 3D vector.

def AngleBetweenTwoVectors(U, V):
178def AngleBetweenTwoVectors(U, V):
179    """
180    Return the angle in radians between the vectors U and V ; formula from W.Kahan
181    Value in radians between 0 and pi.
182    """
183    u = np.linalg.norm(U)
184    v = np.linalg.norm(V)
185    return 2 * np.arctan2(np.linalg.norm(U * v - V * u), np.linalg.norm(U * v + V * u))

Return the angle in radians between the vectors U and V ; formula from W.Kahan Value in radians between 0 and pi.

def SymmetricalVector(V, SymmetryAxis):
187def SymmetricalVector(V, SymmetryAxis):
188    """
189    Return the symmetrical vector to V
190    """
191    q = QRotationAroundAxis(SymmetryAxis, np.pi)
192    return V.rotate(q)

Return the symmetrical vector to V

def normal_add(N1, N2):
194def normal_add(N1, N2):
195    """
196    Simple function that takes in two normal vectors of a deformation and calculates
197    the total normal vector if the two deformations were individually applied.
198    Be very careful, this *only* works when the surface is z = f(x,y) and when 
199    the deformation is small.
200    Might be made obsolete when shifting to a modernised deformation system.
201    """
202    normal1 = N1.normalized()
203    normal2 = N2.normalized()
204    grad1 = -normal1[:2] / normal1[2]
205    grad2 = -normal2[:2] / normal2[2]
206    grad = grad1 + grad2
207    total_normal = np.append(-grad, 1)
208    return Vector(total_normal).normalized()

Simple function that takes in two normal vectors of a deformation and calculates the total normal vector if the two deformations were individually applied. Be very careful, this only works when the surface is z = f(x,y) and when the deformation is small. Might be made obsolete when shifting to a modernised deformation system.

def IntersectionLinePlane(A, u, P, n):
211def IntersectionLinePlane(A, u, P, n):
212    """
213    Return the intersection point between a line and a plane.
214    A is a point of the line, u a vector of the line ; P is a point of the plane, n a normal vector
215    Line's equation : OM = u*t + OA , t a real
216    Plane's equation : n.OP - n.OM = 0
217    """
218    t = np.dot(n, -A + P) / np.dot(u, n)
219    I = u * t + A
220    return I

Return the intersection point between a line and a plane. A is a point of the line, u a vector of the line ; P is a point of the plane, n a normal vector Line's equation : OM = u*t + OA , t a real Plane's equation : n.OP - n.OM = 0

def IntersectionRayListZPlane(RayList, Z=array([0])):
222def IntersectionRayListZPlane(RayList, Z=np.array([0])):
223    """
224    Return the intersection of a list of rays with a different planes with equiations z = Z[i]
225    Basically, by default it returns the intersection of the rays with the Z=0 plane but you can 
226    give it a few values of Z and it should be faster than calling it multiple times.
227    This should let us quickly find the optimal position of the detector as well as trace the caustics.
228    If a ray does not intersect the plane... it should replace that point with a NaN. 
229    """
230    Positions = np.vstack([i.point for i in RayList])
231    Vectors = np.vstack([i.vector for i in RayList])
232    non_zero = Vectors[:,2] != 0
233    Positions = Positions[non_zero]
234    Vectors = Vectors[non_zero]
235    Z = Z[:, np.newaxis]
236    A = Positions[:,2]-Z
237    B = -Vectors[:,2]
238    #times = (Positions[:,2]-Z)/Vectors[:,2]
239    #return A,B
240    with np.errstate(divide='ignore', invalid='ignore'):
241        times = np.divide(A, B, where=(B != 0), out=np.full_like(A, np.nan))
242        #times[times < 0] = np.nan  # Set negative results to NaN
243    #return times
244    #positive_times = times >= 0
245    intersect_positions = Positions[:, :2] + times[:, :, np.newaxis] * Vectors[:, :2]
246    result = []
247    for i in range(Z.shape[0]):
248        # For each plane, we find the intersection points
249        #valid_intersections = intersect_positions[i][positive_times[i]]
250        valid_intersections = intersect_positions[i]
251        result.append(PointArray(valid_intersections))
252    return result

Return the intersection of a list of rays with a different planes with equiations z = Z[i] Basically, by default it returns the intersection of the rays with the Z=0 plane but you can give it a few values of Z and it should be faster than calling it multiple times. This should let us quickly find the optimal position of the detector as well as trace the caustics. If a ray does not intersect the plane... it should replace that point with a NaN.

def SpiralVogel(NbPoint, Radius):
256def SpiralVogel(NbPoint, Radius):
257    """
258    Return a NbPoint x 2 matrix of 2D points representative of Vogel's spiral with radius Radius
259    Careful, contrary to most of the code, this is *not* in the 
260    ARTcore.Vector or ARTcore.Point format. It is a simple numpy array. 
261    The reason is that this is a utility function that can be used both to define directions
262    and to generate grids of points.
263    """
264    GoldenAngle = np.pi * (3 - np.sqrt(5))
265    r = np.sqrt(np.arange(NbPoint) / NbPoint) * Radius
266
267    theta = GoldenAngle * np.arange(NbPoint)
268
269    Matrix = np.zeros((NbPoint, 2))
270    Matrix[:, 0] = np.cos(theta)
271    Matrix[:, 1] = np.sin(theta)
272    Matrix = Matrix * r.reshape((NbPoint, 1))
273
274    return Matrix

Return a NbPoint x 2 matrix of 2D points representative of Vogel's spiral with radius Radius Careful, contrary to most of the code, this is not in the ARTcore.Vector or ARTcore.Point format. It is a simple numpy array. The reason is that this is a utility function that can be used both to define directions and to generate grids of points.

def find_hull(points):
276def find_hull(points):
277    """
278    Find the convex hull of a set of points using a greedy algorithm.
279    This is used to create a polygon that encloses the points.
280    """
281    # start from leftmost point
282    current_point = min(range(len(points)), key=lambda i: points[i][0])
283    # initialize hull with current point
284    hull = [current_point]
285    # initialize list of linked points
286    linked = []
287    # continue until all points have been linked
288    while len(linked) < len(points) - 1:
289        # initialize minimum distance and closest point
290        min_distance = math.inf
291        closest_point = None
292        # find closest unlinked point to current point
293        for i, point in enumerate(points):
294            if i not in linked:
295                distance = math.dist(points[current_point], point)
296                if distance < min_distance:
297                    min_distance = distance
298                    closest_point = i
299        # add closest point to hull and linked list
300        hull.append(closest_point)
301        linked.append(closest_point)
302        # update current point
303        current_point = closest_point
304    # add link between last point and first point
305    hull.append(hull[0])
306    # convert hull to a list of pairs of indices
307    indices = [[hull[i], hull[i + 1]] for i in range(len(hull) - 1)]
308    return indices

Find the convex hull of a set of points using a greedy algorithm. This is used to create a polygon that encloses the points.

def SolverQuadratic(a, b, c):
312def SolverQuadratic(a, b, c):
313    """
314    Solve the quadratic equation a*x^2 + b*x +c = 0 ; keep only real solutions
315    """
316    Solution = np.roots([a, b, c])
317    RealSolution = []
318
319    for k in range(len(Solution)):
320        if abs(Solution[k].imag) < 1e-15:
321            RealSolution.append(Solution[k].real)
322
323    return RealSolution

Solve the quadratic equation ax^2 + bx +c = 0 ; keep only real solutions

def SolverQuartic(a, b, c, d, e):
326def SolverQuartic(a, b, c, d, e):
327    """
328    Solve the quartic equation a*x^4 + b*x^3 +c*x^2 + d*x + e = 0 ; keep only real solutions
329    """
330    Solution = np.roots([a, b, c, d, e])
331    RealSolution = []
332
333    for k in range(len(Solution)):
334        if abs(Solution[k].imag) < 1e-15:
335            RealSolution.append(Solution[k].real)
336
337    return RealSolution

Solve the quartic equation ax^4 + bx^3 +cx^2 + dx + e = 0 ; keep only real solutions

def KeepPositiveSolution(SolutionList):
340def KeepPositiveSolution(SolutionList):
341    """
342    Keep only positive solution (numbers) in the list
343    """
344    PositiveSolutionList = []
345    epsilon = 1e-12
346    for k in SolutionList:
347        if k > epsilon:
348            PositiveSolutionList.append(k)
349
350    return PositiveSolutionList

Keep only positive solution (numbers) in the list

def KeepNegativeSolution(SolutionList):
353def KeepNegativeSolution(SolutionList):
354    """
355    Keep only positive solution (numbers) in the list
356    """
357    NegativeSolutionList = []
358    epsilon = -1e-12
359    for k in SolutionList:
360        if k < epsilon:
361            NegativeSolutionList.append(k)
362
363    return NegativeSolutionList

Keep only positive solution (numbers) in the list

def ClosestPoint( A: Point, Points: PointArray):
367def ClosestPoint(A: Point, Points: PointArray):
368    """
369    Given a reference point A and an array of points, return the index of the point closest to A
370    """
371    distances = (Points-A).norm
372    return np.argmin(distances)

Given a reference point A and an array of points, return the index of the point closest to A

def DiameterPointArray(Points: PointArray):
374def DiameterPointArray(Points: PointArray):
375    """
376    Return the diameter of the smallest circle (for 2D points) 
377    or sphere (3D points) including all the points.
378    """
379    if len(Points) == 0:
380        return None
381    return float(np.ptp(Points, axis=0).max())

Return the diameter of the smallest circle (for 2D points) or sphere (3D points) including all the points.

def CentrePointList(Points):
383def CentrePointList(Points):
384    """
385    Shift all 2D-points in PointList so as to center the point-cloud on the origin [0,0].
386    """
387    return Points - np.mean(Points, axis=0)

Shift all 2D-points in PointList so as to center the point-cloud on the origin [0,0].

def RotateSolid(Object, q):
391def RotateSolid(Object, q):
392    """
393    Rotate object around basepoint by quaternion q
394    """
395    Object.q = q*Object.q

Rotate object around basepoint by quaternion q

def TranslateSolid(Object, T):
397def TranslateSolid(Object, T):
398    """
399    Translate object by vector T
400    """
401    Object.r = Object.r + T

Translate object by vector T

def RotateSolidAroundInternalPointByQ(Object, q, P):
403def RotateSolidAroundInternalPointByQ(Object, q, P):
404    """
405    Rotate object around P by quaternion q where P is in the object's frame
406    """
407    pass #TODO

Rotate object around P by quaternion q where P is in the object's frame

def RotateSolidAroundExternalPointByQ(Object, q, P):
409def RotateSolidAroundExternalPointByQ(Object, q, P):
410    """Rotate object around P by quaternion q, where P is in the global frame"""
411    pass #TODO

Rotate object around P by quaternion q, where P is in the global frame

def SDF_Rectangle(Point, SizeX, SizeY):
416def SDF_Rectangle(Point, SizeX, SizeY):
417    """Signed distance function for a rectangle centered at the origin"""
418    d = np.abs(Point[:2]) - np.array([SizeX, SizeY]) / 2
419    return (np.linalg.norm(np.maximum(d, 0)) + np.min(np.max(d, 0)))/2

Signed distance function for a rectangle centered at the origin

def SDF_Circle(Point, Radius):
421def SDF_Circle(Point, Radius):
422    """Signed distance function for a circle centered at the origin"""
423    return np.linalg.norm(Point[:2]) - Radius

Signed distance function for a circle centered at the origin

def Union_SDF(SDF1, SDF2):
425def Union_SDF(SDF1, SDF2):
426    """Union of two signed distance functions"""
427    return np.minimum(SDF1, SDF2)

Union of two signed distance functions

def Difference_SDF(SDF1, SDF2):
429def Difference_SDF(SDF1, SDF2):
430    """Difference of two signed distance functions"""
431    return np.maximum(SDF1, -SDF2)

Difference of two signed distance functions

def Intersection_SDF(SDF1, SDF2):
433def Intersection_SDF(SDF1, SDF2):
434    """Intersection of two signed distance functions"""
435    return np.maximum(SDF1, SDF2)

Intersection of two signed distance functions

def QRotationAroundAxis(Axis, Angle):
440def QRotationAroundAxis(Axis, Angle):
441    """
442    Return quaternion for rotation by Angle (in rad) around Axis
443    """
444    rot_axis = Normalize(np.array([0.0] + Axis))
445    axis_angle = (Angle * 0.5) * rot_axis
446    qlog = np.quaternion(*axis_angle)
447    q = np.exp(qlog)
448    return q

Return quaternion for rotation by Angle (in rad) around Axis

def QRotationVector2Vector(Vector1, Vector2):
450def QRotationVector2Vector(Vector1, Vector2):
451    """
452    Return a possible quaternion (among many) that would rotate Vector1 into Vector2.
453    Undefined behavior, use with caution. There is no unique quaternion that rotates one vector into another.
454    """
455    Vector1 = Normalize(Vector1)
456    Vector2 = Normalize(Vector2)
457    a = np.cross(Vector1, Vector2)
458    return np.quaternion(1 + np.dot(Vector1, Vector2), *a).normalized()

Return a possible quaternion (among many) that would rotate Vector1 into Vector2. Undefined behavior, use with caution. There is no unique quaternion that rotates one vector into another.

def QRotationVectorPair2VectorPair(InitialVector1, Vector1, InitialVector2, Vector2):
460def QRotationVectorPair2VectorPair(InitialVector1, Vector1, InitialVector2, Vector2):
461    """
462    Return the only quaternion that rotates InitialVector1 to Vector1 and InitialVector2 to Vector2.
463    Please ensure orthogonality two input and two output vectors.
464    """
465    Vector1 = Normalize(Vector1)
466    Vector2 = Normalize(Vector2)
467    Vector3 = Normalize(np.cross(Vector1,Vector2))
468    InitialVector1 = Normalize(InitialVector1)
469    InitialVector2 = Normalize(InitialVector2)
470    InitialVector3 = Normalize(np.cross(InitialVector1,InitialVector2))
471    rot_2Initial = np.zeros((3,3))
472    rot_2Initial[:,0] = InitialVector1
473    rot_2Initial[:,1] = InitialVector2
474    rot_2Initial[:,2] = InitialVector3
475    rot_2Final = np.zeros((3,3))
476    rot_2Final[:,0] = Vector1
477    rot_2Final[:,1] = Vector2
478    rot_2Final[:,2] = Vector3
479    q2Init = quaternion.from_rotation_matrix(rot_2Initial)
480    q2Fin = quaternion.from_rotation_matrix(rot_2Final)
481    return (q2Fin/q2Init).normalized()

Return the only quaternion that rotates InitialVector1 to Vector1 and InitialVector2 to Vector2. Please ensure orthogonality two input and two output vectors.

def RotationRayList(RayList, q):
485def RotationRayList(RayList, q):
486    """Like RotationPointList but with a list of Ray objects"""
487    return [i.rotate(q) for i in RayList]

Like RotationPointList but with a list of Ray objects

def TranslationRayList(RayList, T):
489def TranslationRayList(RayList, T):
490    """Translate a RayList by vector T"""
491    return [i.translate(T) for i in RayList]

Translate a RayList by vector T