Source code for pyvsim.Primitives

"""
.. module :: Primitives
    :platform: Unix, Windows
    :synopsis: Classes representing geometric entities or basic building blocks
    
The classes contained in this module represent the basics of geometric modelling
in pyvsim. It also contains the ray tracing engine and its models (reflection,
refraction, etc)
    
.. moduleauthor :: Ricardo Entz <maiko at thebigheads.net>

.. license::
    PyVSim v.1
    Copyright 2013 Ricardo Entz
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
"""
from __future__ import division
import numpy as np
import copy
import Utils
import Library
import Core
import weakref

# Global constants
GLOBAL_NDIM  = 3
GLOBAL_TOL   = 1e-8

[docs]class Component(Core.PyvsimObject): """ The class component is a representation of elements used in the simulation. Most of its methods are abstract (throw a NotImplementedError) because the really useful classes are its derivatives:: * :class:`~.Core.Assembly` * :class:`~.Core.Part` * :class:`~.Core.Line` However, this class exists to stipulate a common interface for all elements in simulation, allowing a tree-like nesting of them. Some attributes (x, y, z, origin, id) are not directly changeable (for obvious reasons), only with geometrical transforms, etc, so no setter is implemented, and if you try to change them, you will get an error. There is also a implementation of the visitor pattern using the :meth:`~Core.Component.acceptVisitor` method """ MIRROR = 0 TRANSPARENT = 1 OPAQUE = 2 DUMP = 3 PLOTDIMS = -1 def __init__(self): Core.PyvsimObject.__init__(self) self._origin = np.array([0,0,0]) self._x = np.array([1,0,0]) self._y = np.array([0,1,0]) self._z = np.array([0,0,1]) self.parent = None self._depth = None @property
[docs] def x(self): return self._x
@property
[docs] def y(self): return self._y
@property
[docs] def z(self): return self._z
@property
[docs] def origin(self): return self._origin
@property
[docs] def bounds(self): return None
@property
[docs] def depth(self): """ Return the depth of the component within a tree """ if self.parent is None: self._depth = 0 else: self._depth = 1 + self.parent.depth return self._depth
[docs] def intersections(self, p0, p1, tol = GLOBAL_TOL): """ This is a method used specifically for ray tracing. The method returns data about the first intersection between line segments and the polygons defined in the Component. The implementation of the intersection is given by the inheriting classes. Parameters ---------- p0, p1 - numpy.array (N x 3) Coordinates defining N segments by 2 points (each p0, p1 pair), which will be tested for intersection with the polygons defined in the structure. tol - double Tolerance used in the criteria for intersection (see documentation of each implementation) Returns ------- None If no intersections are found. Otherwise returns a list with:: lineParameter This is used to indicate how far the intersection point is from the segment starting point, if 0, the intersection is at p0 and if 1, the intersection is at p1 *Iff* the parameter is > 1 (999), no intersection was found intersectionCoordinates This is where the intersections are found triangleIndexes This is the index of the triangle where the intersection was found. If no intersection found, will return 0, *but attention*, the only way to guarantee that no intersection was found is when the lineParameter is zero. normals The normal vector at the intersection point (if the surface is defined with normals at vertices, interpolation is performed). part A list with references to this object. This is, in this case, redundant, but that makes the function signature uniform with the `:class:~Core.Assembly` """ return None
[docs] def translate(self, vector): """ This method should be used when there is a change in the component position. This method operates only with the origin position, and delegates the responsibility to the inheriting class by means of the :meth:`~Core.Component.translateImplementation()` method. Parameters ---------- vector : numpy.array (1 x 3) Vector to translate the component. An array with x, y and z coordinates """ self._origin = self._origin + vector self.translateImplementation(vector) self.clearData()
[docs] def translateImplementation(self, vector): """ This method must be implemented by the interested inheriting class in case a translation affects its internals. For example: a class with a vector of points P will probably need to update that to P+vector This is a way of implementing the `Chain of Responsibility <http://http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern>` pattern, so that these geometrical operations are executed recursively. *This is a protected method, do not use it unless you are inheriting from this class!* Parameters ---------- vector : numpy.array (1 x 3) Vector to translate the component. An array with x, y and z coordinates Raises ------ NotImplementedError """ raise NotImplementedError
[docs] def rotate(self, angle, axis, pivotPoint=None): """ This method should be used when there is a change in the component position. This method operates only with the origin and the x, y and z vectors. It delegates the responsibility to the inheriting class by means of the :meth:`~Core.Component.rotateImplementation()` method. Parameters ---------- angle Angle : scalar (in radians) axis : numpy.array (1 x 3) Vector around which the rotation occurs. pivotPoint : numpy.array (1 x 3) Point in space around which the rotation occurs. If not given, rotates around origin. """ if (np.abs(angle) < GLOBAL_TOL): return if pivotPoint is None: pivotPoint = self._origin self._origin = Utils.rotatePoints(self.origin,angle,axis,pivotPoint) self._x = Utils.rotateVector(self.x,angle,axis) self._y = Utils.rotateVector(self.y,angle,axis) self._z = Utils.rotateVector(self.z,angle,axis) self.rotateImplementation(angle,axis,pivotPoint) self.clearData()
[docs] def rotateImplementation(self, angle, axis, pivotPoint): """ This method must be implemented by the interested inheriting class in case a rotation affects its internals. For example: a class with a vector of points P will probably need to update them accordingly using the following code:: P = Utils.rotatePoints(P,angle,axis,pivotPoint) This is a way of implementing the `Chain of Responsibility <http://http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern>` pattern, so that these geometrical operations are executed recursively. *This is a protected method, do not use it unless you are inheriting from this class!* Parameters ---------- angle Angle : scalar (in radians) axis : numpy.array (1 x 3) Vector around which the rotation occurs. pivotPoint : numpy.array (1 x 3) Point in space around which the rotation occurs. If not given, rotates around origin. Raises ------ NotImplementedError """ raise NotImplementedError
[docs] def alignTo(self,x_new,y_new,z_new=None, pivotPoint = None, tol=1e-8): """ This method allows the alignment of the part to a specific direction (this is very useful in optical systems definition). The implementation assumes that at least two *orthogonal* vectors are given. There is an assertion to guarantee that. With the new vector base, a rotation matrix M is calculated, and the `formulation to convert rotation matrix to axis-angle <http://http://en.wikipedia.org/wiki/Rotation_matrix#Conversion_from_and_to_axis-angle>`_ is used for convenience (as then the method :meth:`~Core.Component.rotate` can be directly called). Obs: No concerns about code efficiency are made, as this method will probably not be used all the time. Parameters ---------- x_new, [y_new, z_new] : numpy.array(1,3) New vectors defining the orientation of the part. The vectors need NOT to be normalized, but MUST be orthogonal. Vectors y_new or z_new can be omitted (one at a time) and will be implicitly calculated pivotPoint : numpy.array(1,3) Point in space around which the rotation occurs. If not given, rotates around local origin. tol : 1e-8 Many checks are executed in order to guarantee that the new x, y and z vectors are orthogonal and normalized. This is the tolerance of these checks. Raises ------ AssertionError If the vectors are not perpendicular to the given tolerance (check is done with a dot product), or if the rotation is performed from a right-handed coordinate system to a left-handed and vice-versa. LinAlgError If the calculation has other mathematical problems. """ if pivotPoint is None: pivotPoint = self.origin if y_new is not None and z_new is None: z_new = np.cross(x_new,y_new) if y_new is None and z_new is not None: y_new = np.cross(z_new,x_new) Xnew = Utils.normalize(np.vstack([x_new, y_new, z_new])) # Verification that the base is orthonormal assert (Utils.aeq(np.dot(Xnew[0],Xnew[1]),0, tol) and Utils.aeq(np.dot(Xnew[0],Xnew[2]),0, tol) and Utils.aeq(np.dot(Xnew[2],Xnew[1]),0, tol)) # Verify it is right-handed assert (Utils.aeq(np.cross(Xnew[0],Xnew[1]), Xnew[2], tol) and Utils.aeq(np.cross(Xnew[0],Xnew[2]),-Xnew[1], tol) and Utils.aeq(np.cross(Xnew[2],Xnew[1]),-Xnew[0], tol)) Xold = np.array([self.x, self.y, self.z]) M = np.linalg.solve(Xold,Xnew) if Utils.aeq(M, np.eye(3)): return assert (np.linalg.det(M) - 1)**2 < GLOBAL_TOL # prop of rotation Matrix # Formulation from Wikipedia (See documentation above) D,V = np.linalg.eig(M) D = np.real(D) V = np.real(V) # Verifies that the matrix M is a rotation Matrix assert ((D-1)**2 < GLOBAL_TOL).any() axis = np.squeeze(V[:,(D-1)**2 < GLOBAL_TOL].T) cosAngle = (np.trace(M)-1)/2 # Sometimes small numeric errors are found, so must correct them, # otherwise np.arrccos returns nan if (abs(cosAngle) > 1): cosAngle = np.sign(cosAngle) angle = np.arccos(cosAngle) # We can't know the right rotation, so we must check if Utils.aeq(Utils.rotateVector(Xold, angle, axis), Xnew): self.rotate(angle,axis,pivotPoint) else: self.rotate(-angle,axis,pivotPoint)
[docs] def clearData(self): """ This method must be implemented by each inheriting class. Its function is to avoid classes having inconsistent data after a geometric transform. For example: a camera has a mapnp.ping funcion calculated from raytracing, then the user moves this camera, making the mapnp.ping invalid. When a rotation or translation is called the clearData method is also called, and the class is in charge of cleaning all data that is now not valid anymore. """ raise NotImplementedError
[docs]class Assembly(Component): """ The assembly class is a non-terminal node in the Components tree. It carries almost no properties of its own, but makes sure that all geometrical transformations are propagated to its subcomponents. The subcomponents are stored in a numpy array. This is definetely not always needed (except when fancy slicing is desired), but this keeps the consistency all along the code, making all lists instances of numpy.ndarray. """ PLOTDIMS = -1 def __init__(self): self._items = np.array([], dtype = object) self._bounds = None self.surfaceProperty = Component.TRANSPARENT Component.__init__(self) self.name = 'Assembly '+str(self._id) # Ray tracing properties self.material = Library.IdealMaterial(1) self.surfaceProperty = Component.TRANSPARENT def __repr__(self): """ Returns a pretty-printable tree representation of the assembly generated recursively """ string = Component.__repr__(self) + "\n|" if self._items is not None: for item in self._items: string = string + (item.depth*3) * " " + "+->" string = string + item.__repr__() + "\n|" return string def __iadd__(self,other): """ Overloads the "+=" operator to act as an append element, or a list extension """ if not issubclass(type(other), Component): raise TypeError("Operations are only allowed between \ pyvsim components") self.append(other) return self def __isub__(self,other): """ Overloads the "-=" operator to act as an remove element, or a list extension """ if not issubclass(type(other), Component): raise TypeError("Operations are only allowed between \ pyvsim components") self.remove(other) return self def __eq__(self, other): """ This overloading is used in ray tracing, because it might be that the tracing target is given as an assembly, but the intersection always happens with a subcomponent of the assembly """ answer = np.zeros_like(other) answer += (other is self) for item in self._items: answer += (item == other) return answer def __neq__(self, other): """ This overloading is given to maintain coherence with __eq__ """ return 1 - (self == other) def __getitem__(self, k): """ This overloading is provided so that the assembly can be referenced by index, as in an array """ if type(k) is str: for item in self._items: if item.name == k: return item raise KeyError("Element ", k, "is not available") else: return self._items[k] def __setitem__(self, k, value): """ This overloading is provided so that the assembly can be referenced by index, as in an array """ self.append(value, k) def __delitem__(self,k): """ Removes the element of a list Method provided to align assembly behavior to that of a list. """ self.remove(k) def __len__(self): """ Returns the length of the items list. Method provided to align assembly behavior to that of a list. """ return len(self._items) def __contains__(self, other): """ Check if items list contains element Method provided to align assembly behavior to that of a list. """ return other in self._items
[docs] def refractiveIndex(self, wavelength = 532e-9): """ Returns the index of refraction of the material given the wavelength (or a list of them) Parameters ---------- wavelength : scalar or numpy.array The wavelength of the incoming light given in *meters* Returns ------- refractiveIndex : same dimension as wavelength The index of refraction """ return self.material.refractiveIndex(wavelength)
@property
[docs] def bounds(self): """ Returns the boundaries of the assembly by finding the minimum bounding box aligned to the axis that contains it. The algorithm works by finding the maximum and minimum values of x, y and z by traversing the subcomponents. Returns ------- bounds : numpy.array(2,3) An array containing the following elements: [[xmin, ymin, zmin], [xmax, ymax, zmax]] """ if self._bounds is None: mini = np.ones((len(self.items),3))*1000 maxi = -np.ones((len(self.items),3))*1000 if len(self._items) > 0: for n in range(len(self._items)): b = self._items[n].bounds if b is not None: [mini[n],maxi[n]] = b self._bounds = np.array([np.min(mini,0),np.max(maxi,0)]) return self._bounds
@property def items(self): return self._items @items.setter def items(self,value): self._items = value for part in self._items: part.parent = self @items.deleter def items(self): del self._items
[docs] def append(self, component, n = None): """ Adds element at the component list. Parameters ---------- component : Component The Component to be added n : int = None [Optional] The position of the component to be added. If no parameter is given, the element is added at the end of the list, otherwise it is added at the n-th position. Returns ------- n The list length. """ if n is None: n = len(self._items) self._items = np.append(self._items, n, None) self._items[n] = component component.parent = weakref.proxy(self) self._bounds = None return len(self._items)
[docs] def remove(self, element): """ Remove the element at the n-th position of the component list. This also de-registers this assembly as its parent. Parameters ---------- element : string, int or object The name, the position in the list or the object to be removed Returns ------- element A reference to the element, if one is to re-use that. """ index = None if type(element) is str: for i, elem in enumerate(self._items): if elem.name == element: index = i elif issubclass(type(element), Core.PyvsimObject): for i, elem in enumerate(self._items): if elem is element: index = i elif type(element) is int: index = element else: raise TypeError("Input must be either string, int or pyvsimobject") if index is None: raise IndexError("index out of bounds") element = self._items[index] element.parent = None self._items = np.delete(self._items, index) return element
[docs] def acceptVisitor(self, visitor): """ This method is a provision for the `Visitor Pattern <http://http://en.wikipedia.org/wiki/Visitor_pattern>` and is be used for traversing the tree. As an assembly has sub-elements, we must iterate through them """ visitor.visit(self) for part in self._items: part.acceptVisitor(visitor)
def _boundingBoxTest(self, bounds, p0, p1): """ Determines if lines defined by the segments p0-p1 intersects the box bounding the polygon The algorithm implemented was taken from: author = {Amy Williams and Steve Barrus and R. Keith and Morley Peter Shirley}, title = {An efficient and robust ray-box intersection algorithm}, journal = {Journal of Graphics Tools}, year = {2003}, volume = {10}, pages = {54} It was written in a way to accept a list of lines. In the code, there is the following trick:: V[V == 0] = GLOBAL_TOL Which seems to work to avoid the creation of NaNs in the calculation. Parameters ---------- bounds : numpy.array([[xmin,ymin,zmin],[xmax,ymax,zmax]]) the dimensions of the bounding box p0 : numpy.array (N x 3) segment initial point - accepts simultaneous calculation of N points p1 : numpy.array (N x 3) segment final point - accepts simultaneous calculation of N points Returns ------- +----------------------+----+ | if intersection | 1 | +----------------------+----+ |if not intersection | 0 | +----------------------+----+ if N lives were given, will return a N-long numpy.array """ [xmin,xmax] = bounds V = p1 - p0 V[V == 0] = GLOBAL_TOL T1 = (xmin - p0) / V T2 = (xmax - p0) / V Tmin = T1 * (V >= 0) + T2 * (V < 0) Tmax = T2 * (V >= 0) + T1 * (V < 0) eliminated1 = (Tmin[:,0] > Tmax[:,1]) + (Tmin[:,1] > Tmax[:,0]) Tmin[:,0] = (Tmin[:,0] * (Tmin[:,1] <= Tmin[:,0]) + Tmin[:,1] * (Tmin[:,1] > Tmin[:,0])) Tmax[:,0] = (Tmax[:,0] * (Tmax[:,1] >= Tmax[:,0]) + Tmax[:,1] * (Tmax[:,1] < Tmax[:,0])) eliminated2 = (Tmin[:,0] > Tmax[:,2]) + (Tmin[:,2] > Tmax[:,0]) Tmin[:,0] = (Tmin[:,0] * (Tmin[:,2] <= Tmin[:,0]) + Tmin[:,2] * (Tmin[:,2] > Tmin[:,0])) Tmax[:,0] = (Tmax[:,0] * (Tmax[:,2] >= Tmax[:,0]) + Tmax[:,2] * (Tmax[:,2] < Tmax[:,0])) eliminated3 = (Tmin[:,0] > 1) + (Tmax[:,0] < 0) return 1 - (eliminated1 + eliminated2 + eliminated3)
[docs] def intersections(self, p0, p1, tol = GLOBAL_TOL): """ This method searches for intersections between a given set of line segments and the Parts included in this Assembly. Please check the documentation at `:class:~Core.Part` for a better description of its internals. Parameters ---------- p0, p1 - numpy.array (N x 3) Coordinates defining N segments by 2 points (each p0, p1 pair), which will be tested for intersection with the polygons defined in the structure. tol - double Tolerance used in the criteria for intersection (see documentation of each implementation) Returns ------- None If no intersections are found. Otherwise returns a list with:: lineParameter This is used to indicate how far the intersection point is from the segment starting point, if 0, the intersection is at p0 and if 1, the intersection is at p1 *Iff* the parameter is > 1 (999), no intersection was found intersectionCoordinates This is where the intersections are found triangleIndexes This is the index of the triangle where the intersection was found. If no intersection found, will return 0, *but attention*, the only way to guarantee that no intersection was found is when the lineParameter is zero. normals The normal vector at the intersection point (if the surface is defined with normals at vertices, interpolation is performed). part A list with references to this object. This is, in this case, redundant, but that makes the function signature uniform with the `:class:~Core.Assembly` """ nlins = np.size(p0,0) ndim = np.size(p0,1) lineParameter = np.zeros(nlins) + 999 coordinates = copy.deepcopy(p1) triangleNumber = np.zeros(nlins) normalVector = np.zeros((nlins,ndim)) intersectedSurface = np.array([None] * nlins) for part in self._items: if part.bounds is not None: boxTest = self._boundingBoxTest(part.bounds, p0, p1) if np.sum(boxTest) > 0: p0_refined = p0[boxTest == 1] p1_refined = p1[boxTest == 1] [lineT, coords, triInd, N, partList] = part.intersections(p0_refined, p1_refined, tol) # Create a mask of elements which must be substituted lineParameter_temp = np.zeros(nlins) + 999 lineParameter_temp[boxTest == 1] = lineT # This mask is used for the arrays that contain the answer maskLong = lineParameter > lineParameter_temp # This mask is for the result of part.intersections maskShort = lineParameter[boxTest == 1] > lineT lineParameter[maskLong] = lineT[maskShort] coordinates[maskLong] = coords[maskShort] triangleNumber[maskLong] = triInd[maskShort] normalVector[maskLong] = N[maskShort] intersectedSurface[maskLong] = partList[maskShort] else: pass # if nothing was found, nothing was found return [lineParameter, coordinates, triangleNumber, normalVector, intersectedSurface]
[docs] def translateImplementation(self, vector): """ This method iterates the translation to all the items found in the list, making the `:meth:~Core.Component.translate` method be executed through the whole components tree. """ for part in self._items: part.translate(vector)
[docs] def rotateImplementation(self, angle, axis, pivotPoint): """ This method iterates the rotation to all the items found in the list, making the `:meth:~Core.Component.rotate` method be executed through the whole components tree. """ for part in self._items: part.rotate(angle, axis, pivotPoint)
[docs] def clearData(self): """ This method iterates the clearData to all the items found in the list, making the `:meth:~Core.Component.clearData` method be executed through the whole components tree. """ self._bounds = None for part in self._items: part.clearData()
[docs]class Points(Component): """ This class is used for representation of 0D elements, i.e. points in the 3D space. *Warning* - do not change the self.bounds This is an indication that this class does not take part in ray tracing activities """ PLOTDIMS = 0 def __init__(self): Component.__init__(self) self.name = 'Line '+str(self._id) self.points = np.array([]) self.connectivity = None self.color = None self.opacity = 0.5 self.visible = True
[docs] def translateImplementation(self, vector): """ This method is in charge of updating the position of the point cloud (provided it exists) when the Line is translated. There is an exception handling because there is the possibility that the line is translated before the points are defined. This is extremely unlikely, but should not stop the program execution. """ try: self.points = self.points + vector except TypeError: # There is no problem if a translation is executed before the points # are defined pass
[docs] def rotateImplementation(self, angle, axis, pivotPoint): """ This method is in charge of updating the position of the point cloud (provided it exists) when the Line is rotated. There is an exception handling because there is the possibility that the line is translated before the points are defined. This is extremely unlikely, but should not stop the program execution. """ try: self.points = Utils.rotatePoints(self.points,angle,axis,pivotPoint) except TypeError: # There is no problem if a rotation is executed before the points # are defined pass
[docs] def clearData(self): """ In the pure implementation of the line, there are no features to be deleted when a geometrical operation is executed """ pass
[docs]class Line(Component): """ This class is used for representation of 1D elements, i.e. lines and curves in the 3D space. *Warning* - do not change the self.bounds property value. This is an indication that this class does not take part in ray tracing activities """ PLOTDIMS = 1 def __init__(self): Component.__init__(self) self.name = 'Line '+str(self._id) self.points = np.array([]) self.color = None self.width = None self.opacity = 0.5 self.visible = True
[docs] def translateImplementation(self, vector): """ This method is in charge of updating the position of the point cloud (provided it exists) when the Line is translated. There is an exception handling because there is the possibility that the line is translated before the points are defined. This is extremely unlikely, but should not stop the program execution. """ try: self.points = self.points + vector except TypeError: # There is no problem if a translation is executed before the points # are defined pass
[docs] def rotateImplementation(self, angle, axis, pivotPoint): """ This method is in charge of updating the position of the point cloud (provided it exists) when the Line is rotated. There is an exception handling because there is the possibility that the line is translated before the points are defined. This is extremely unlikely, but should not stop the program execution. """ try: self.points = Utils.rotatePoints(self.points,angle,axis,pivotPoint) except TypeError: # There is no problem if a rotation is executed before the points # are defined pass
[docs] def clearData(self): """ In the pure implementation of the line, there are no features to be deleted when a geometrical operation is executed """ pass
[docs]class Part(Component): """ This implementation of the Component class is the representation of a surface using triangle elements. This is supposed to be the standard element in np.piVSim, as raytracing with such surfaces is relatively easy and plotting is also made easy by using libraries such as VTK, Matplotlib and OpenGL. Another benefit is the possibility of directly reading this topology from a STL file, that can be exported from a CAD program. """ PLOTDIMS = 3 def __init__(self): Component.__init__(self) self.name = 'Part ' + str(self.id) self.points = np.array([]) self.connectivity = np.array([]) self.data = None self.normals = None self.color = None self.opacity = 0.5 self.visible = True # Ray tracing properties self.material = Library.IdealMaterial(1) self.surfaceProperty = Component.OPAQUE # Variables for raytracing self.surfaceProperty = Component.OPAQUE self._bounds = None self._triangleVectors = None self._trianglePoints = None self._triangleNormals = None self._triVectorsDots = None """ This avoids the saving of the specific ray tracing variables, which can consume a lot of storage while being more or less easy to be calculated when ray tracing is performed """ self.transientFields.extend(["_bounds", "_trianglePoints" "_triangleVectors", "_triangleNormals", "_triVectorsDots"])
[docs] def refractiveIndex(self, wavelength = 532e-9): """ Returns the index of refraction of the material given the wavelength (or a list of them) Parameters ---------- wavelength : scalar or numpy.array The wavelength of the incoming light given in *meters* Returns ------- refractiveIndex : same dimension as wavelength The index of refraction """ return self.material.refractiveIndex(wavelength)
@property
[docs] def bounds(self): """ Returns the coordinates of the aligned-to-axis-bounding box Returns ------- bounds : numpy.array An array with the following data [xmin, xmax, ymin, ymax, zmin, zmax]. Defining a box aligned to axis bounding the Part """ if self._bounds is None: self._computeRaytracingData() return self._bounds
@property def triangleVectors(self): if self._triangleVectors is None: self._computeRaytracingData() return self._triangleVectors @property def trianglePoints(self): if self._trianglePoints is None: self._computeRaytracingData() return self._trianglePoints @property def triangleNormals(self): if self._triangleNormals is None: self._computeRaytracingData() return self._triangleNormals @property def triVectorsDots(self): if self._triVectorsDots is None: self._computeRaytracingData() return self._triVectorsDots def _computeRaytracingData(self): """ Computes data that will be used for raytracing, such as:: _bounds Points that define a bounding box around the polygon. Format: [[xmin,ymin,zmin],[xmax,ymax,zmax]] _triangleVectors Vectors defining the triangle sides (V1 and V2), in addition, there are some products defined to determine if a point in the triangle plane is inside the triangle or not. Format: [V1,V2, dot(V1,V1), dot(V1,V2), dot(V2,V2), (UV**2 - UU*VV)] _trianglePoints A shortcut, with the coordinates of each triangle, instead of the point-connectivity lists _triangleNormals Vectors that are normal to the triangle surfaces (this is needed even if normals were already determined) Format: [Normals(raw), Normals(normalized) """ if len(self.points) > 0: xmin = np.min(self.points,0) xmax = np.max(self.points,0) self._bounds = np.array([xmin,xmax]) Ptriangles = self.points[self.connectivity] V1 = Ptriangles[:,1] - Ptriangles[:,0] V2 = Ptriangles[:,2] - Ptriangles[:,0] N = np.cross(V1,V2) Nnorm = Utils.normalize(N) UU = np.sum(V1*V1,1) UV = np.sum(V1*V2,1) VV = np.sum(V2*V2,1) UVden = (UV**2 - UU*VV) self._triangleVectors = np.array([V1,V2]) self._triVectorsDots = np.array([UU,UV,VV,UVden]) self._trianglePoints = Ptriangles self._triangleNormals = np.array([N,Nnorm]) else: self._bounds = np.zeros((2,3))
[docs] def intersections(self, p0, p1, tol = GLOBAL_TOL): """ This is a method used specifically for ray tracing. The method returns data about the first intersection between line segments and the polygons defined in the Component. The implementation of the intersection is given by the inheriting classes. This method is intended for use in raytracing algorithms, as there is an initial, fast verification to see if there is a chance of any triangle in the polygon to be intersected, then, if it is the case, it executes expensive search. Special cases when intersecting with individual triangles: - if line is contained on triangle plane, will ignore - if intersection is at p0, will not return p0 Algorithm adapted from http://geomalgorithms.com/a06-_intersect-2.html Parameters ---------- p0, p1 - numpy.array (N x 3) Coordinates defining N segments by 2 points (each p0, p1 pair), which will be tested for intersection with the polygons defined in the structure. tol - double Tolerance used in the criteria for intersection (see documentation of each implementation) Returns ------- None If no intersections are found. Otherwise returns a list with:: lineParameter This is used to indicate how far the intersection point is from the segment starting point, if 0, the intersection is at p0 and if 1, the intersection is at p1 *Iff* the parameter is > 1 (999), no intersection was found intersectionCoordinates This is where the intersections are found triangleIndexes This is the index of the triangle where the intersection was found. If no intersection found, will return 0, *but attention*, the only way to guarantee that no intersection was found is when the lineParameter is zero. normals The normal vector at the intersection point (if the surface is defined with normals at vertices, interpolation is performed). part A list with references to this object. This is, in this case, redundant, but that makes the function signature uniform with the `:class:~Core.Assembly` """ # # Some assertions to guarantee that the input data is correct: # assert p0.ndim == 2 # assert this is a numpy list of coordinates assert p1.ndim == 2 # assert this is also a numpy list assert np.size(p0,1) == np.size(p1,1) # assert list lengths are equal assert np.size(p0,0) == np.size(p1,0) # assert coordinate system is equal # # Start dumb search, step 1 - determine if line intercept triangle plane # # Retrieve data about the triangles (either pre-existing or will be # calculated once) Ptriangles = self.trianglePoints [V1,V2] = self.triangleVectors [UU,UV,VV,UVden] = self.triVectorsDots [N,Nnorm] = self.triangleNormals # Some variable definitions to make latter code more readable: ntris = np.size(N,0) # Number of triangles in surface nlins = np.size(p0,0) # Number of lines in surface V = p1 - p0 # First we need to do a dot product between each vector and all triangle # normals V = V.reshape(nlins,1,GLOBAL_NDIM) den = np.sum(N*V,2) # # When we find a case of den == 0, it means that the line is parallel to # the triangle, a case which we'll ignore. # # Numpy seems to deal better with infinity than NaN, then: den[(den == 0)] = np.inf # # We now must find the vector that goes from a triangle point to the # initial point of the line # P0 = p0.reshape(nlins,1,GLOBAL_NDIM) V0 = Ptriangles[:,0] - P0 num = np.sum(N*V0,2) # # Now the parameter T can be calculated. The formula is: # # T = N dot V0 # ---------- # N dot V # T = num / den T_0 = copy.deepcopy(T) # Now, in order to find the intersection point between the intersection # plane and the line, we have:: # P = P0 + T*V T = T.reshape(nlins,ntris,1) np.tile(T.T,(1,1,GLOBAL_NDIM)) P = P0 + T * V # Finally we have to check if the points are inside the triangles. This # is theoretically an easy task, as it's just projecting the vector # at the triangle sides: # # P0 V2 # o-----------> # \ in / # \ / beyond U = P - P0 # V1 \ / # \ / (P) # \ / # v U = P - Ptriangles[:,0] UW = np.sum(V1 * U,2) VW = np.sum(V2 * U,2) S1 = (UV*VW - VV*UW) / UVden T1 = (UV*UW - UU*VW) / UVden # Now we have to mark all points that do not lie in the triangles:: # # the first line will check if the point is really in the given segment, # there is an important check to eliminate if the intersection is at # the beginning of the segment # # The second and third lines check if the point is really at the triangle # [Ii,Ij] = np.nonzero((T_0 <= tol)+(T_0 > 1+tol)+ (S1 < -tol)+(T1 < -tol)+ (S1 + T1 > 1 + tol)) T_0[Ii,Ij] = 999 # # Finally, we take only the first intersection of the line with the # polygon, and arrange data for returning # triangleIndexes = np.argmin(T_0,1) lineParameters = T_0[range(nlins),triangleIndexes] intersectionCoords = P[range(nlins),triangleIndexes,:] # Assures outputs p1 as coordinates, when intersection is not found intersectionCoords[lineParameters > 1] = p1[lineParameters > 1] if self.normals is None: normals = Nnorm[triangleIndexes] else: normals = self._calculateNormals(triangleIndexes, intersectionCoords) # This, when uncommented, assures that no normals are given if no # intersection is found #normals[lineParameters > 1] = 0*normals[lineParameters > 1] return [lineParameters, intersectionCoords, triangleIndexes, normals, np.array([self]*nlins)]
def _calculateNormals(self,triangleIndexes,intersectionCoords): """ This method returns a 3-element list corresponding to the normalized normal vector. It returns the interpolation (using barycentric coordinates) of the normals on the triangle vertices - use this if representing lenses, etc *WARNING* - will return a result even if point is not on the polygon Parameters ---------- triangleIndexes : numpy.array (N x 3) indexes of the triangles vertices (the order is important, otherwise the normals can be inverted. As this method is vectorized, it is possible to execute N calculations at the same time. intersectionCoords : numpy.array (N x 3) coordinates of the intersection points Returns ------- result : numpy.array (N x 3) normal vectors """ triangleCoords = self.points[self.connectivity[triangleIndexes]] normals = self.normals[self.connectivity[triangleIndexes]] # Calculation of the barycentric coordinates for each point lambdas = Utils.barycentricCoordinates(intersectionCoords, triangleCoords[:,0], triangleCoords[:,1], triangleCoords[:,2]) # Barycentric interpolation result = (np.tile(lambdas[:,0],(3,1)).T * np.array(normals[:,0,:]) + np.tile(lambdas[:,1],(3,1)).T * np.array(normals[:,1,:]) + np.tile(lambdas[:,2],(3,1)).T * np.array(normals[:,2,:])) # As a side-effect, normals must be normalized after barycentric interp result = Utils.normalize(result) return result
[docs] def translateImplementation(self, vector): """ This method is in charge of updating the position of the point cloud (provided it exists) when the Part is translated. There is an exception handling because there is the possibility that the part is translated before the points are defined. This is extremely unlikely, but should not stop the program execution. Parameters ---------- vector : numpy.array (1 x 3) Vector to translate the component. An array with x, y and z coordinates """ try: self.points = self.points + vector except TypeError: # There is no problem if a translation is executed before the points # are defined pass
[docs] def rotateImplementation(self, angle, axis, pivotPoint): """ This method is in charge of updating the position of the point cloud (provided it exists) when the Part is rotated. There is an exception handling because there is the possibility that the part is translated before the points are defined. This is extremely unlikely, but should not stop the program execution. In case the surface is defined with normals on vertices (thus making self.normals not None), these vectors are rotated. Parameters ---------- angle Angle : scalar (in radians) axis : numpy.array (1 x 3) Vector around which the rotation occurs. pivotPoint : numpy.array (1 x 3) Point in space around which the rotation occurs. If not given, rotates around origin. """ try: self.points = Utils.rotatePoints(self.points,angle,axis,pivotPoint) except TypeError: # There is no problem if a rotation is executed before the points # are defined pass if self.normals is not None: self.normals = Utils.rotateVector(self.normals,angle,axis)
[docs] def clearData(self): """ Implement this method whenever your object possesses geometrical features that are calculated from their interaction with the ambient (e.g. - any raytraced features). This method is called after all spatial transformations """ self._bounds = None self._triangleVectors = None self._trianglePoints = None self._triangleNormals = None
[docs]class Plane(Part): """ This is a convenience class that inherits from Part and represents a rectangle. There are also convenience methods to make coordinate transformation. To navigate in the plane, one can use the following coordinate system: :math:`\\left[u,v\\right]` , where :math:`-1 <= u,v <=1` As a default, the :math:`\\vec{x}` vector is the normal for the triangles. This class can represent **only** rectangles, so that most of its methods are greatly simplified. If you need to represent a parallelogram, you would have to implement your own class. """ PARAMETRIC_COORDS = np.array([[+0,-0.5,-0.5], [+0,+0.5,-0.5], [+0,+0.5,+0.5], [+0,-0.5,+0.5]]) PLOTDIMS = 3 def __init__(self, dimension = np.array([0,1,1]), fastInit=False): Part.__init__(self) self.name = 'Plane '+str(self._id) self.connectivity = np.array([[0,1,2], [0,2,3]]) self.normals = None self._dimension = dimension if not fastInit: self._resize() @property def dimension(self): return self._dimension @dimension.setter
[docs] def dimension(self, d): self._dimension = d self._resize()
def _resize(self): """ Convenience function to position the points of the plane and set up internal variables """ self.points = np.einsum('ij,j->ij', Plane.PARAMETRIC_COORDS, self.dimension) self.points = (np.tile(self.points[:,1],(GLOBAL_NDIM,1)).T * self.y + np.tile(self.points[:,2],(GLOBAL_NDIM,1)).T * self.z)
[docs] def parametricToPhysical(self,coordinates): """ Transforms a 2-component vector in the range -1..1 in sensor coordinates :math:`[u,v] \\rarrow [x,y,z]` (global reference frame) Vectorization for this method is implemented. """ # This is unfortunate, because: # u = Zcamera # v = Ycamera # but the dimension vector is [x,y,z] coordinates = self.dimension[::-1][:2]*coordinates/2 # print "Coordinates" # print coordinates if coordinates.ndim == 1: # print "ndim == 1" return self.origin + (coordinates[0] * self.z + coordinates[1] * self.y) else: # print "ndim > 1" # print np.tile(coordinates[:,0],(GLOBAL_NDIM,1)).T # print np.tile(coordinates[:,1],(GLOBAL_NDIM,1)).T return (self.origin + np.tile(coordinates[:,0],(GLOBAL_NDIM,1)).T * self.z + np.tile(coordinates[:,1],(GLOBAL_NDIM,1)).T * self.y)
[docs] def physicalToParametric(self,coords): """ Transforms a 3-component coordinates vector to a 2-component vector which value falls in the range -1..1 in sensor coordinates :math:`[x,y,z] \\rarrow [u,v]` Vectorization for this method is implemented. """ vector = coords - self.origin if coords.ndim == 1: pv = 2*(np.dot(self.y,vector) / self.dimension[1]) pu = 2*(np.dot(self.z,vector) / self.dimension[2]) return np.array([pu,pv]) else: nvecs = np.size(vector,0) pu = (np.sum(np.tile(self.z,(nvecs,1).T*vector,1) / self.dimension[1])) pv = (np.sum(np.tile(self.y,(nvecs,1).T*vector,1) / self.dimension[2])) return 2*np.array([pu,pv]).T
[docs]class Volume(Part): """ This class is used to represent a general hexahedron. Even though some methods will force it to become a cuboid (where all angles are right), such as:: * :meth:`~Core.Volume.width` * :meth:`~Core.Volume.depth` * :meth:`~Core.Volume.heigth` * :meth:`~Core.Volume.dimension` These methods can be safely ignored a set of points can be directly given, allowing quadrilaterally-faced hexas to be represented (check `Wikipedia's article <http://en.wikipedia.org/wiki/Hexahedron>`_ for more information) """ PLOTDIMS = 3 PARAMETRIC_COORDS = np.array([[+0,+0.5,+0.5], [+0,-0.5,+0.5], [+0,-0.5,-0.5], [+0,+0.5,-0.5], [+1,+0.5,+0.5], [+1,-0.5,+0.5], [+1,-0.5,-0.5], [+1,+0.5,-0.5],]) def __init__(self, dimension = np.array([1,1,1]), fastInit=False): """ This is another convenience class to represent volumes (a hexahedron). The point numbering convention is:: 6--------5 / /| (X) / / | ^ 7--------4 | | | | | +-> (Z) | 2 | 1 / | | / v | |/ (Y) 3--------0 """ Part.__init__(self) self.name = 'Volume '+str(self._id) self._dimension = dimension # normals pointing outside self.connectivity = np.array([[1,4,0],[1,5,4], # normal +z [7,2,3],[7,6,2], # normal -z [0,7,3],[0,4,7], # normal +y [6,5,1],[6,1,2], # normal -y [4,6,7],[4,5,6], # normal +x [0,3,2],[2,1,0]]) # normal -x self.normals = None if not fastInit: self._resize() @property def dimension(self): return self._dimension @dimension.setter def dimension(self, d): self._dimension = d self._resize() def _resize(self): self.points = Volume.PARAMETRIC_COORDS * np.tile(self.dimension,(8,1)) self.points =((np.reshape(self.points[:,0],(8,1,1)) * self.x).squeeze()+ (np.reshape(self.points[:,1],(8,1,1)) * self.y).squeeze()+ (np.reshape(self.points[:,2],(8,1,1)) * self.z).squeeze())
[docs] def expand(self, factor): """ Inflates the volume by "factor" """ center = np.mean(self.points,0) for n in range(np.size(self.points,0)): self.points[n] = self.points[n] + factor*(self.points[n] - center)
[docs] def physicalToParametric(self, c): """ Transforms a vector or a list of vectors in parametric coordinates with the following properties: [x,y,z] (global) -> [x',y',z'] (parametrical) 0 <= x',y',z' <= 1 if [x,y,z] lies inside the volume """ return Utils.hexaInterpolation(c, self.points, Volume.PARAMETRIC_COORDS[:,1:3])
[docs] def parametricToPhysical(self, p): """ Transforms a vector or a list of vectors in parametric coordinates with the following properties: [x,y,z] (global) -> [x',y',z'] (parametrical) 0 <= x',y',z' <= 1 if [x,y,z] lies inside the volume """ return Utils.hexaInterpolation(p, Volume.PARAMETRIC_COORDS[:,1:3], self.points)
[docs] def pointInHexa(self,p): """ This is intended as a lightweigth test for checking if a point (or a set of them) lies inside an hexahedron. This uses the algorithm implemented in :mod:`Utils`. Parameters ---------- p : numpy.array (N,3) A collection of points Returns ------- result : numpy.array (N) An array with "1" corresponding to points in the hexa or "0" otherwise """ return Utils.pointInHexa(p,self.points)
[docs] def interpolate(self, p, verify = True): """ This is a convienience function to interpolate the data in the ".data" field of this object. As the field is not under surveillance, it is the responsibility of the user to ensure that the field contains a numpy.array with the shape (M,8), i.e. one data point for each vertex Parameters ---------- p : numpy.array (N,3) A collection of points verify : boolean (True) Verifies if the point is contained in the hexa defined by the edges. If not, the corresponding row of result will be zeroed out Returns ------- result : numpy.array (N,M) An array with the data from the field ".data" interpolated """ if not verify: return Utils.hexaInterpolation(p, self.points, self.data) else: return np.einsum("ij,i->ij", Utils.hexaInterpolation(p, self.points, self.data), Utils.pointInHexa(p,self.points))
[docs]class RayBundle(Assembly): """ This class represents an arbitrary number of light rays. The reason for having them wrapped in a single class is that it allows full vectorization of the raytracing procedure. This can be called a disposable class """ TRACING_FOV = 1 TRACING_LASER_REFLECTION = 0 PLOTDIMS = -1 def __init__(self): Assembly.__init__(self) self.name = 'Bundle ' + str(self._id) self.material = None # Ray tracing configuration self.maximumRayTrace = 10 self.stepRayTrace = 10 self.preAllocatedSteps = 10 self.wavelength = None self.startingPoints = None self.initialVectors = None # Ray tracing statistics self.rayPaths = None self.rayLastVectors = None self.rayIntersections = None self.rayLength = None self.steps = None self.finalIntersections = None @property def bounds(self): return None
[docs] def append(self, initialVector, initialPosition = None, wavelength = 532e-9): """ This method is used to append new rays in the bundle. Parameters ---------- initialVector : numpy.array (N x 3) if N rays are given, each element is the initial vector for ray tracing of each ray initialPosition : numpy.array (N x 3) if no parameter is passed, rays will depart from the origin of the bundle. Otherwise they will depart from the given points. If N points were given, a single common starting point can be given wavelength : numpy.array (N) the wavelength of the rays in meters (this changes the color and the behavior of the rays, if any dispersing element is present in the simulation Notes ----- * If the starting point is omitted, it will assume that the rays departs from the origin of the bundle * This method was not made to be efficient in a loop (there are many checks), so it should be used ideally only once to append all rays * This method destroys data that was already ray-traced, if any Examples -------- 1) A single ray is inserted:: >>> bundle = RayBundle() >>> bundle.append(np.array([1,0,0]), np.array([0,0,0])) 2) Several rays are inserted, each with a starting point:: >>> bundle.append(np.array([[1,0,0],[1,0,0]]), ... np.array([[0,0,0],[0,0,1]])) 3) Several rays are inserted, with a common starting point:: >>> bundle.append(np.array([[1,0,0],[0,1,0]], ... np.array([0,0,0])) """ # clear data, as it would be really difficult to manage rays with # different number of points self.clearData() # Determine how many vectors were received if initialVector.ndim == 1: nrays = 1 assert len(initialVector) == GLOBAL_NDIM else: nrays = np.size(initialVector,0) # Adjust initialPosition according to the given conditions if initialPosition is None: initPos = np.tile(self.origin, (nrays,1)) else: if initialPosition.ndim == 1 and nrays > 1: initPos = np.tile(initialPosition, (nrays,1)) else: initPos = initialPosition # properly add the newly received vectors to the stack if self.startingPoints is not None: self.startingPoints = np.vstack(self.startingPoints, initPos) self.initialVectors = np.vstack(self.initialVectors, initialVector) else: self.startingPoints = initPos self.initialVectors = initialVector # create the new storage for ray paths self.rayPaths = copy.deepcopy(self.startingPoints) # This looks not careful, but in reality enforces that wavelength # is either scalar or the same length as the startingPoints self.wavelength = np.ones(np.size(self.startingPoints,0)) * wavelength
[docs] def delete(self, n): """ This method is not implemented, and is present only to respect the Assembly interface. As deleting a single ray requires many matrix reshapings, it is better to clear all the data and redo the ray tracing. Raises ------ NotImplementedError """ return NotImplementedError
[docs] def clear(self): """ Removes all rays and ray tracing data from the bundle. """ self.wavelength = None self.startingPoints = None self.initialVectors = None self.clearData()
[docs] def translateImplementation(self, translation): """ This method changes the rays starting points, and waits for clear data to delete all ray tracing related information. Parameters ---------- translation : numpy.array (3) A [dx, dy, dz] vector """ try: self.startingPoints = self.startingPoints + translation except TypeError: pass # there might be no starting points registered
[docs] def rotateImplementation(self, angle, axis, pivotPoint): """ This method rotates the starting points and deletes all ray tracing data, because a rotation or a translation of light rays may completely change the light paths. Parameters ---------- angle Angle : scalar (in radians) axis : numpy.array (1 x 3) Vector around which the rotation occurs. pivotPoint : numpy.array (1 x 3) Point in space around which the rotation occurs. If not given, rotates around origin. """ try: self.startingPoints = Utils.rotatePoints(self.startingPoints, angle, axis, pivotPoint) self.initialVectors = Utils.rotateVector(self.initialVectors, angle, axis) except TypeError: pass
[docs] def trace(self, tracingRule = TRACING_FOV, restart = False): """ A method for starting ray tracing. The bundle must be included in the environment assembly where ray tracing will be done (the position, however, is not important) Parameters ---------- tracingRule Either RayBundle.TRACING_FOV or RayBundle.TRACING_LASER_REFLECTION, this parameter tells if ray tracing should stop at opaque surfaces (this is the case when tracing to determine the field of view) or not (to trace for laser safety calculations) Raises ------ RuntimeError If the bundle is not inserted in an assembly, this error will be raised. """ # Make sure everything is clear nrays = np.size(self.startingPoints, 0) if restart: # Memorize scope variables currVector = self.rayLastVectors step = self.steps rayPoints = self.rayPaths rayIntersc = self.rayIntersections # Reallocate vectors rayPoints = Utils.reallocateArray(rayPoints, self.preAllocatedSteps) rayIntersc = Utils.reallocateArray(rayIntersc, self.preAllocatedSteps) distance = self.rayLength surfaceRef = self.finalIntersections else: self.clearData() # Shortcut to the number of rays being traced: # Initialize variables distance = np.zeros(nrays) currVector = copy.deepcopy(self.initialVectors) self.finalIntersections = np.empty(nrays,dtype='object') self.rayLastVectors = copy.deepcopy(self.initialVectors) # Do matrix pre-allocation to store ray paths rayPoints = np.empty((self.preAllocatedSteps, nrays, GLOBAL_NDIM), dtype='double') rayIntersc = np.empty((self.preAllocatedSteps, nrays, 1), dtype='object') step = 0 rayPoints[0,:,:] = copy.deepcopy(self.startingPoints) stepsize = np.ones(nrays) * self.stepRayTrace stepsize[distance + stepsize > self.maximumRayTrace] = (self.maximumRayTrace - distance[distance + stepsize > self.maximumRayTrace]) rayPoints[step+1,:,:] = (rayPoints[step,:,:] + np.tile(stepsize,(GLOBAL_NDIM,1)).T* currVector) # Routine to find the top element in the hierarchy if self.parent is None: raise RuntimeError("Could not find parent element. " + "Is this bundle really inside an assembly?") topComponent = self while topComponent.parent is not None: topComponent = topComponent.parent # Ray tracing loop while np.sum(stepsize) > GLOBAL_TOL: # Increase matrix size (if this is done too often, performance is # really, really bad. So adjust self.preAllocatedSteps wisely if step + 2 >= np.size(rayPoints,0): rayPoints = Utils.reallocateArray(rayPoints, self.preAllocatedSteps) rayIntersc = Utils.reallocateArray(rayIntersc, self.preAllocatedSteps) # Ask for the top assembly to intersect the rays with the whole # Component tree, will receive results only for the first # intersection of each ray [t, coords, _, N, surfaceRef] = topComponent.intersections(rayPoints[step], rayPoints[step+1], GLOBAL_TOL) self.finalIntersections[t <= 1] = surfaceRef[t <= 1] rayIntersc[step+1,:,:] = np.reshape(surfaceRef,(nrays,1)) # Calculate the distance ran by the rays distance = (distance + t * (t <= 1) * stepsize + (t > 1) * stepsize) # Calculate the next vectors currVector = self._calculateNextVectors(currVector, t, N, surfaceRef, tracingRule) self.rayLastVectors[Utils.norm(currVector) > 0] = \ currVector[Utils.norm(currVector) > 0] # Calculate next step size # stepsize = ((self.stepRayTrace * # (distance + self.stepRayTrace <= self.maximumRayTrace)+ # (self.maximumRayTrace - distance) * # (distance + self.stepRayTrace > self.maximumRayTrace)))#* # #Utils.norm(currVector)) stepsize[distance + stepsize > self.maximumRayTrace] = \ (self.maximumRayTrace - distance)[distance + stepsize > self.maximumRayTrace] # Calculate next inputs: step = step + 1 rayPoints[step] = coords rayPoints[step+1] = (rayPoints[step] + currVector * np.tile(stepsize,(GLOBAL_NDIM,1)).T) # Now, clean up the mess with the preallocated matrix: self.rayPaths = rayPoints[range(step+1)] self.rayIntersections = rayIntersc[range(step+1)] # Save ray tracing statistics self.rayLength = distance self.steps = step self.finalIntersections = surfaceRef # And create lines to represent the rays if restart: for n in range(nrays): self._items[n].points = self.rayPaths[:,n,:] else: self._items = np.empty(nrays,"object") for n in range(nrays): self._items[n] = Line() self._items[n].parent = self self._items[n].points = self.rayPaths[:,n,:] self._items[n].color = Utils.metersToRGB(self.wavelength[n])
def _calculateNextVectors(self, currVector, t, N, surface, tracingRule): """ A method to calculate the vectors to continue ray tracing. This includes the logic of determining if the tracing stops at opaque interfaces or not. Parameters ---------- currVector : numpy.array (N x 3) The current ray path t : numpy.array (N) The position of the intersection given by the equation p = p0 + t*(p1 - p0). The value of t must be between zero (exclusive) and 1 (inclusive) to be considered valid. N : numpy.array (N x 3) The normal vector of the intersected surface surface : numpy.array(N) of Components The references to the intersected surfaces tracing rule : RayBundle.TRACING_FOV or TRACING_LASER_REFLECTION This parameter tells if ray tracing should stop at opaque surfaces (this is the case when tracing to determine the field of view) or not (to trace for laser safety calculations) Returns ------- vectors : numpy.array (N x 3) the vectors indicating the direction that ray paths must continue in ray tracing Raises ------ AssertionError If the norm(N) or norm(currVector) is not 1. """ # Returns same vector if no intersection was found if (t > 1 + GLOBAL_TOL).all(): return currVector # Keep these assertions here if you are unsure that you are getting # the correct input data assert(Utils.aeq(N, Utils.normalize(N))) assert(Utils.aeq(currVector, Utils.normalize(currVector))) # Calculate as if all rays were reflected reflected = currVector - (2 * N * np.tile(np.sum(N * currVector,1), (GLOBAL_NDIM,1)).T) # Calculate refractions # Important calculation: NdotV = np.sum(currVector * N, 1) # Properties: # NdotV < 0 => Ray entering the surface (normals point outwards) # NdotV > 0 => Ray exiting the surface # NdotV = 0 => Should not happen, as the intersection algorithm # rejects that. Spurious cases will be filtered later Nsurf = np.zeros_like(surface) Nparent = np.zeros_like(surface) N1 = np.ones_like(surface) N2 = np.ones_like(surface) surfaceProperty = np.zeros(len(surface)) #cosTheta1 = -NdotV for n, surf in enumerate(surface): if surf is not None: Nsurf[n] = surf.refractiveIndex(self.wavelength[n]) Nparent[n] = surf.parent.refractiveIndex(self.wavelength[n]) surfaceProperty[n] = surf.surfaceProperty # If entering surface, N1 is the external index of refraction, N2 is # the internal N1[(NdotV < 0) * (t <= 1)] = Nparent[(NdotV < 0) * (t <= 1)] N2[(NdotV < 0) * (t <= 1)] = Nsurf[ (NdotV < 0) * (t <= 1)] # and vice versa N1[(NdotV > 0) * (t <= 1)] = Nsurf[ (NdotV > 0) * (t <= 1)] N2[(NdotV > 0) * (t <= 1)] = Nparent[(NdotV > 0) * (t <= 1)] # formulation taken from: http://en.wikipedia.org/wiki/Snell's_law cosTheta2 = 1 - (1 - NdotV**2) * ((N1 / N2) ** 2) cosTheta2[cosTheta2 >= 0] = cosTheta2[cosTheta2 >= 0]**0.5 refracted = (np.tile(N1/N2,(GLOBAL_NDIM,1)).T * currVector + np.tile(np.sign(-NdotV)* ((N1/N2)*np.abs(NdotV) - cosTheta2), (GLOBAL_NDIM,1)).T * N) refracted = Utils.normalize(refracted) # Big if block to sort the cases out #========================================= # First, assume rays were undisturbed: result = currVector # Then substitute those who were successfully refracted / pass through result[(cosTheta2 <= 1 + GLOBAL_TOL) * (surfaceProperty == Part.TRANSPARENT)] = Utils.normalize( refracted[(cosTheta2 <= 1 + GLOBAL_TOL)* (surfaceProperty == Part.TRANSPARENT)]) # Then zero those rays who found a dump if tracingRule == RayBundle.TRACING_FOV: result[(surfaceProperty == Part.OPAQUE) + (surfaceProperty == Part.DUMP)] = 0 * result[(surfaceProperty == Part.OPAQUE) + (surfaceProperty == Part.DUMP)] else: result[(surfaceProperty == Part.DUMP)] = 0 * result[(surfaceProperty == Part.DUMP)] result[(surfaceProperty == Part.OPAQUE)] = reflected[(surfaceProperty == Part.OPAQUE)] # Then put reflected rays result[(surfaceProperty == Part.MIRROR)] = reflected[(surfaceProperty == Part.MIRROR)] # Total internal reflection result[(cosTheta2 < 0)* (surfaceProperty == Part.TRANSPARENT)] = reflected[(cosTheta2 < 0)* (surfaceProperty == Part.TRANSPARENT)] return result
[docs] def clearData(self): """ This method removes all elements from the self.items list and the ray pathes """ self.items = np.array([]) self.rayPaths = copy.deepcopy(self.startingPoints) self.rayLength = None self.steps = None
if __name__=="__main__": """ Code for unit testing basic functionality of classes in the Core module """ print "" print "*********************************************************************" print "********** pyvsim primitives module unit test ************" print "*********************************************************************" #===================================== # Simplified geometry creation - cube #===================================== points = [[0,0,0], [1,0,0], [1,1,0], [0,1,0], [0,0,1], [1,0,1], [1,1,1], [0,1,1]] # normals pointing outside conn = [[5,7,4],[5,6,7], # normal +z [3,2,1],[0,3,1], # normal -z [3,6,2],[6,3,7], # normal +y [1,5,4],[4,0,1], # normal -y [5,1,6],[1,2,6], # normal +x [7,0,4],[7,3,0]] # normal -x # # Create values for putting normals at edges # edgenormals = [] for n in range(8): norm = points[n] - np.array([0.5,0.5,0.5]) norm = norm/np.sqrt(np.dot(norm,norm)) edgenormals.append(norm) edgenormals = np.array(edgenormals) print "******* Creation of the part and an assembly containing it ********" part = Part() part.points = np.array(points) part.connectivity = np.array(conn) assembly = Assembly() assembly += part print "Successfully created a project tree" print part.name print assembly.name # # Test refraction coefficient # print "************ Index of refraction calculation ******************" part.refractiveIndexConstant = 1 assert Utils.aeq(part.refractiveIndex(532e-9), 1) sellmeierCoeffs = np.array([[1.03961212, 0.00600069867], [0.23179234, 0.02001791440], [1.01046945, 103.560653000]]) part.material = Library.Glass(sellmeierCoeffs) assert Utils.aeq(part.refractiveIndex(532e-9), 1.51947, 1e-3) assert Utils.aeq(part.refractiveIndex(486e-9), 1.52238, 1e-3) assert Utils.aeq(part.refractiveIndex(np.ones(10)*532e-9), np.ones(10)*1.51947, 1e-3) print "************ Intersection test ******************" # hit miss hit miss p0 = [[0.5,0.5,-1], [1.5,1.5,-1], [0.999999,2 ,0.999999],[-1,-1e-6,-1e-6]] p1 = [[0.5,0.5, 2], [1.5,1.5, 2], [1 ,-1,1 ],[ 2, 0, 0]] t0 = [ 0, 10, 0, 10] p0 = np.array(p0) p1 = np.array(p1) t0 = np.array(t0) # Parameter for the speed test repetitions = 1000 cases = np.size(p0,0) triangles = np.size(part.points,0) tic = Utils.Tictoc() # Verify that the intersections were correctly found tic.tic() [t, coords, inds, norms, refs] = part.intersections(p0, p1, GLOBAL_TOL) tic.toc(cases*triangles) assert sum((t > t0)) # Assert that the assembly will give exactly the same answer as the part tic.tic() [t2, coords2, inds2, norms2, refs2] = assembly.intersections(p0, p1, GLOBAL_TOL) tic.toc(cases*triangles) assert Utils.aeq(t,t2) assert Utils.aeq(coords,coords2) assert Utils.aeq(inds,inds2) print "************* Intersection performance test ******************" p0 = np.tile(p0, (repetitions,1)) p1 = np.tile(p1, (repetitions,1)) t0 = np.tile(t0, repetitions) print "Intersections with line using the method from Core.Part" _ = part.bounds # This forces the pre-calcs for raytracing (optional) tic.tic() [t, coords, inds, norms, refs] = part.intersections(p0, p1, GLOBAL_TOL) tic.toc(cases*triangles*repetitions) assert sum((t > t0)) print "Intersections with line using the method from Core.Assembly" tic.tic() [t2, coords2, inds2, norms2, refs2] = assembly.intersections(p0, p1, GLOBAL_TOL) tic.toc(cases*triangles*repetitions) assert Utils.aeq(t,t2) assert Utils.aeq(coords,coords2) assert Utils.aeq(inds,inds2) print "Intersections with line using the method after random rotation" angle = np.random.rand() axis = np.array([1,1,1]) / np.linalg.norm(np.array([1,1,1])) pivot = assembly.origin assembly.rotate(angle,axis,pivot) p0 = Utils.rotatePoints(p0,angle,axis,pivot) p1 = Utils.rotatePoints(p1,angle,axis,pivot) _ = part.bounds tic.tic() [t, coords, inds, norms, refs] = assembly.intersections(p0, p1, GLOBAL_TOL) tic.toc(cases*triangles*repetitions) assert Utils.aeq(t, t2) print "Reference result from pyVSim v.0 - 71500 polygon intersection/s" #============================= # Testing the provided normals #============================= print "************ Normal vector calculation test ******************" assembly.rotate(-angle, axis,pivot) p0 = [[0.5,0.5,-1.], [0.5,0.5,0.5], [-1,0.5,0.5], [.5,0.5,0.5]] p1 = [[0.5,0.5,0.5], [0.5,0.5,1.5], [.5,0.5,0.5], [2.,0.5,0.5]] t0 = [[0,0,-1], [0,0,1], [-1,0,0], [1,0,0]] p0 = np.array(p0) p1 = np.array(p1) t0 = np.array(t0) [t, coords, inds, norms, refs] = assembly.intersections(p0, p1, GLOBAL_TOL) assert Utils.aeq(norms, t0) # # Test of interpolated normals (result must be the same) # part.normals = edgenormals [t, coords, inds, norms, refs] = assembly.intersections(p0, p1, GLOBAL_TOL) assert Utils.aeq(norms,t0) #================================ # Testing of the ray bundle class #================================ print "************ Basic ray tracing test ******************" part.terminalOnFOVCalculation = False part.terminalAlways = False part.reflectAlways = False part.lightSource = False print "Items:" print assembly bundle = RayBundle() bundle.translate(np.array([0.5,0.5,0.5])) bundle.append(np.array([[1,0,0],[0,1,0],[0,0,1]])) assembly += bundle print "Tracing ray bundle" print "Pre allocated steps : ", bundle.preAllocatedSteps print "Step ray trace : ", bundle.stepRayTrace tic.tic() bundle.trace(RayBundle.TRACING_LASER_REFLECTION) tic.toc() print "Ray lengths : ", bundle.rayLength print "Number of steps : ", bundle.steps # print bundle.rayPaths[-1] bundle.preAllocatedSteps = 10 bundle.stepRayTrace = 5 print "Tracing ray bundle" print "Pre allocated steps : ", bundle.preAllocatedSteps print "Step ray trace : ", bundle.stepRayTrace # print bundle.startingPoints # print bundle.initialVectors bundle.rotate(np.pi/4, np.array([0,0,1])) # print bundle.initialVectors tic.tic() bundle.trace() tic.toc() print "Ray lengths : ", bundle.rayLength print "Number of steps : ", bundle.steps # print bundle.rayPaths[-1] print "************ Testing geometrical operations ******************" theorypoints = np.array([[0,-1,-1], [0,+1,-1], [0,+1,+1], [0,-1,+1]])*0.5 m = Plane() assert Utils.aeq(m.origin, np.zeros(3)) assert Utils.aeq(m.x, np.eye(3)[0]) assert Utils.aeq(m.y, np.eye(3)[1]) assert Utils.aeq(m.z, np.eye(3)[2]) coords = m.parametricToPhysical(np.array([[0,0],[1,1],[-1,-1]])) assert Utils.aeq(coords[0], np.zeros(3)) assert Utils.aeq(coords[1], m.parametricToPhysical(np.array([1,1]))) assert Utils.aeq(coords[2], m.parametricToPhysical(np.array([-1,-1]))) print m.points print theorypoints assert Utils.aeq(m.points, theorypoints) print "testing translation" m.translate(np.array([0,1,1])*0.5) theorypoints = np.array([[0,-0,-0], [0,+2,-0], [0,+2,+2], [0,-0,+2]])*0.5 assert Utils.aeq(m.points, theorypoints) print "testing rotation aroung axis passing through origin" m.translate(np.array([0,-1,-1])*0.5) m.rotate(np.pi/2,np.array([1,0,0])) theorypoints = np.array([[0,+1,-1], [0,+1,+1], [0,-1,+1], [0,-1,-1]])*0.5 assert Utils.aeq(m.points, theorypoints) m.rotate(2*np.pi,np.array([1,0,0])) assert Utils.aeq(m.points, theorypoints) m.rotate(-np.pi/2,np.array([1,0,0])) theorypoints = np.array([[0,-1,-1], [0,+1,-1], [0,+1,+1], [0,-1,+1]])*0.5 assert Utils.aeq(m.points, theorypoints) m.rotate(np.pi/2,np.array([0,1,0])) theorypoints = np.array([[-1,-1,-0], [-1,+1,-0], [+1,+1,+0], [+1,-1,+0]])*0.5 assert Utils.aeq(m.points, theorypoints) assert Utils.aeq(m.x, np.array([0,0,-1])) m.rotate(-np.pi/2,np.array([0,1,0])) print "testing the align to axis function" m.alignTo(np.array([1,0,0]),np.array([0,1,0])) theorypoints = np.array([[0,-1,-1], [0,+1,-1], [0,+1,+1], [0,-1,+1]])*0.5 assert Utils.aeq(m.points, theorypoints) m.alignTo(np.array([-1,0,0]),np.array([0,1,0])) theorypoints = np.array([[0,-1,+1], [0,+1,+1], [0,+1,-1], [0,-1,-1]])*0.5 assert Utils.aeq(m.points, theorypoints) m.alignTo(np.array([1,0,0]),None,np.array([0,0,1])) theorypoints = np.array([[0,-1,-1], [0,+1,-1], [0,+1,+1], [0,-1,+1]])*0.5 assert Utils.aeq(m.points, theorypoints) print "testing rotation by axis off-origin" m.rotate(np.pi,np.array([1,0,0]),np.array([0,1,1])*0.5) theorypoints = np.array([[0,+1.5,+1.5], [0,+0.5,+1.5], [0,+0.5,+0.5], [0,+1.5,+0.5]]) assert Utils.aeq(m.points, theorypoints) print "************ END OF TESTS ******************"