The API may be updated without warning
The names are self-descriptive.
This is the multi-page printable view of this section. Click here to print.
The API may be updated without warning
The names are self-descriptive.
Provides classes for different deformations of optical surfaces.
Created on Fri Mar 10 2023
@author: Semptum + Stefan Haessler
1""" 2Provides classes for different deformations of optical surfaces. 3 4 5Created on Fri Mar 10 2023 6 7@author: Semptum + Stefan Haessler 8""" 9# %% Modules 10 11import numpy as np 12#import cupy as cp 13from abc import ABC, abstractmethod 14import scipy 15import math 16from ARTcore.ModuleZernike import zernike_gradient 17import logging 18 19logger = logging.getLogger(__name__) 20 21# %% Abstract class 22 23 24class Defect(ABC): 25 """ 26 Abstract class representing a defect on the optical surface. 27 """ 28 29 @abstractmethod 30 def RMS(self): 31 pass 32 33 @abstractmethod 34 def PV(self): 35 pass 36 37class MeasuredMap(Defect): 38 """ 39 Class describing a defect map measured experimentally (for instance using a Hartmann wavefront sensor). Note that the provided image should cover the entire support. 40 """ 41 def __init__(self, Support, Map): 42 self.deformation = Map 43 self. Support = Support 44 rect = Support._CircumRect() # TODO fix this so it just uses a rectangle with known size 45 X = np.linspace(-rect[0], rect[0], num=self.deformation.shape[0]) 46 Y = np.linspace(-rect[1], rect[1], num=self.deformation.shape[1]) 47 self.DerivX, self.DerivY = np.gradient(self.deformation, rect/self.deformation.shape) 48 DerivInterpX = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.DerivX), method="linear") 49 DerivInterpY = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.DerivY), method="linear") 50 SurfInterp = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.deformation), method="linear") 51 52 self.rms = np.std(self.deformation) 53 self.DerivInterp = lambda x: np.array([DerivInterpX(x[:2]), DerivInterpY(x[:2])]) 54 self.SurfInterp = lambda x: SurfInterp(x[:2]) 55 def get_normal(self, Point): 56 Grad = self.DerivInterp(Point) 57 dX, dY = Grad.flatten() 58 norm = np.linalg.norm([dX, dY, 1]) 59 dX /= norm 60 dY /= norm 61 return np.array([dX, dY, np.sqrt(1 - dX**2 - dY**2)]) 62 63 def get_offset(self, Point): 64 return self.SurfInterp(Point) 65 66 def RMS(self): 67 return self.rms 68 69 def PV(self): 70 pass 71 72class Fourrier(Defect): 73 """ 74 75 """ 76 def __init__(self, Support, RMS, slope=-2, smallest=0.1, biggest=None): 77 # The sizes are the wavelength in mm 78 rect = Support._CircumRect() # TODO fix this so it just uses a rectangle with known size 79 if biggest is None: 80 biggest = np.max(rect) 81 82 k_max = 2 / smallest 83 k_min = 2 / biggest 84 ResX = int(round(k_max * rect[0] / 2))+1 85 ResY = int(round(k_max * rect[1])) 86 87 kXX, kYY = np.meshgrid( 88 np.linspace(0, k_max, num=ResX, dtype='float32', endpoint=False), 89 np.linspace(-k_max, k_max, num=ResY, dtype='float32', endpoint=False), 90 sparse=True) 91 92 maskedFFT = np.ma.masked_outside(np.sqrt(kXX**2 + kYY**2), k_min, k_max) 93 94 FFT = maskedFFT**slope * np.exp( 95 1j *np.random.uniform(0, 2 * np.pi, size=maskedFFT.shape).astype('float32') 96 ) 97 FFT = FFT.data*(1-FFT.mask) 98 99 deformation = np.fft.irfft2(np.fft.ifftshift(FFT, axes=0)) 100 RMS_factor = RMS/np.std(deformation) 101 deformation *= RMS_factor 102 103 DerivX = np.fft.irfft2(np.fft.ifftshift(FFT * 1j * kXX * RMS_factor, axes=0))*np.pi/2 104 kY = np.concatenate((kYY[kYY.shape[0]//2:],kYY[:kYY.shape[0]//2])) 105 DerivY = np.fft.irfft2(np.fft.ifftshift(FFT * 1j * RMS_factor, axes=0)*kY)*np.pi/2 106 #del FFT 107 108 X = np.linspace(-rect[0]/2, rect[0]/2, num=(ResX-1)*2) # Because iRfft 109 Y = np.linspace(-rect[1]/2, rect[1]/2, num=ResY) 110 111 DerivInterpX = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(DerivX), method="linear") 112 DerivInterpY = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(DerivY), method="linear") 113 SurfInterp = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(deformation), method="linear") 114 115 self.DerivX = DerivX 116 self.DerivY = DerivY 117 self.rms = np.std(deformation) 118 self.deformation = deformation 119 self.DerivInterp = lambda x: np.array([DerivInterpX(x[:2]), DerivInterpY(x[:2])]) 120 self.SurfInterp = lambda x: SurfInterp(x[:2]) 121 122 def get_normal(self, Point): 123 """ 124 Calculates and returns the surface normal at the given Point. 125 It uses the derivative interpolators to compute the partial derivatives of the surface 126 deformation and returns a normalized vector representing the surface normal. 127 """ 128 dX, dY = self.DerivInterp(Point) 129 norm = np.linalg.norm([dX, dY, 1]) 130 dX /= norm 131 dY /= norm 132 return np.array([dX, dY, np.sqrt(1 - dX**2 - dY**2)]) 133 134 def get_offset(self, Point): 135 """ 136 Calculates and returns the surface offset at the given Point. 137 It uses the surface deformation interpolator to determine the displacement 138 at the specified point on the surface. 139 """ 140 return self.SurfInterp(Point) 141 142 def RMS(self): 143 """ 144 Returns the root mean square (RMS) value of the surface deformation (self.rms). 145 """ 146 return self.rms 147 148 def PV(self): 149 pass 150 151 152class Zernike(Defect): 153 def __init__(self, Support, coefficients): 154 self.coefficients = coefficients 155 self.max_order = np.max([k[0] for k in coefficients]) 156 self.support = Support 157 self.R = Support._CircumCirc() 158 159 def get_normal(self, Point): 160 x, y, z = Point / self.R 161 val, derivX, derivY = zernike_gradient([x], [y], self.max_order) 162 dX = 0 163 dY = 0 164 for k in self.coefficients: 165 dX += self.coefficients[k] * derivX[k][0][1][0] 166 dY += self.coefficients[k] * derivY[k][0][1][0] 167 dX /= self.R 168 dY /= self.R 169 return np.array([-dX, -dY, 1]) 170 171 def get_offset(self, Point): 172 x, y, z = Point / self.R 173 val, derivX, derivY = zernike_gradient([x], [y], self.max_order) 174 Z = 0 175 for k in self.coefficients: 176 Z += self.coefficients[k] * val[k][0][1][0] 177 return Z 178 179 def RMS(self): 180 return np.sqrt(np.sum([i**2 for i in self.coefficients.values()])) 181 182 def PV(self): 183 pass
25class Defect(ABC): 26 """ 27 Abstract class representing a defect on the optical surface. 28 """ 29 30 @abstractmethod 31 def RMS(self): 32 pass 33 34 @abstractmethod 35 def PV(self): 36 pass
Abstract class representing a defect on the optical surface.
38class MeasuredMap(Defect): 39 """ 40 Class describing a defect map measured experimentally (for instance using a Hartmann wavefront sensor). Note that the provided image should cover the entire support. 41 """ 42 def __init__(self, Support, Map): 43 self.deformation = Map 44 self. Support = Support 45 rect = Support._CircumRect() # TODO fix this so it just uses a rectangle with known size 46 X = np.linspace(-rect[0], rect[0], num=self.deformation.shape[0]) 47 Y = np.linspace(-rect[1], rect[1], num=self.deformation.shape[1]) 48 self.DerivX, self.DerivY = np.gradient(self.deformation, rect/self.deformation.shape) 49 DerivInterpX = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.DerivX), method="linear") 50 DerivInterpY = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.DerivY), method="linear") 51 SurfInterp = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.deformation), method="linear") 52 53 self.rms = np.std(self.deformation) 54 self.DerivInterp = lambda x: np.array([DerivInterpX(x[:2]), DerivInterpY(x[:2])]) 55 self.SurfInterp = lambda x: SurfInterp(x[:2]) 56 def get_normal(self, Point): 57 Grad = self.DerivInterp(Point) 58 dX, dY = Grad.flatten() 59 norm = np.linalg.norm([dX, dY, 1]) 60 dX /= norm 61 dY /= norm 62 return np.array([dX, dY, np.sqrt(1 - dX**2 - dY**2)]) 63 64 def get_offset(self, Point): 65 return self.SurfInterp(Point) 66 67 def RMS(self): 68 return self.rms 69 70 def PV(self): 71 pass
Class describing a defect map measured experimentally (for instance using a Hartmann wavefront sensor). Note that the provided image should cover the entire support.
42 def __init__(self, Support, Map): 43 self.deformation = Map 44 self. Support = Support 45 rect = Support._CircumRect() # TODO fix this so it just uses a rectangle with known size 46 X = np.linspace(-rect[0], rect[0], num=self.deformation.shape[0]) 47 Y = np.linspace(-rect[1], rect[1], num=self.deformation.shape[1]) 48 self.DerivX, self.DerivY = np.gradient(self.deformation, rect/self.deformation.shape) 49 DerivInterpX = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.DerivX), method="linear") 50 DerivInterpY = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.DerivY), method="linear") 51 SurfInterp = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(self.deformation), method="linear") 52 53 self.rms = np.std(self.deformation) 54 self.DerivInterp = lambda x: np.array([DerivInterpX(x[:2]), DerivInterpY(x[:2])]) 55 self.SurfInterp = lambda x: SurfInterp(x[:2])
73class Fourrier(Defect): 74 """ 75 76 """ 77 def __init__(self, Support, RMS, slope=-2, smallest=0.1, biggest=None): 78 # The sizes are the wavelength in mm 79 rect = Support._CircumRect() # TODO fix this so it just uses a rectangle with known size 80 if biggest is None: 81 biggest = np.max(rect) 82 83 k_max = 2 / smallest 84 k_min = 2 / biggest 85 ResX = int(round(k_max * rect[0] / 2))+1 86 ResY = int(round(k_max * rect[1])) 87 88 kXX, kYY = np.meshgrid( 89 np.linspace(0, k_max, num=ResX, dtype='float32', endpoint=False), 90 np.linspace(-k_max, k_max, num=ResY, dtype='float32', endpoint=False), 91 sparse=True) 92 93 maskedFFT = np.ma.masked_outside(np.sqrt(kXX**2 + kYY**2), k_min, k_max) 94 95 FFT = maskedFFT**slope * np.exp( 96 1j *np.random.uniform(0, 2 * np.pi, size=maskedFFT.shape).astype('float32') 97 ) 98 FFT = FFT.data*(1-FFT.mask) 99 100 deformation = np.fft.irfft2(np.fft.ifftshift(FFT, axes=0)) 101 RMS_factor = RMS/np.std(deformation) 102 deformation *= RMS_factor 103 104 DerivX = np.fft.irfft2(np.fft.ifftshift(FFT * 1j * kXX * RMS_factor, axes=0))*np.pi/2 105 kY = np.concatenate((kYY[kYY.shape[0]//2:],kYY[:kYY.shape[0]//2])) 106 DerivY = np.fft.irfft2(np.fft.ifftshift(FFT * 1j * RMS_factor, axes=0)*kY)*np.pi/2 107 #del FFT 108 109 X = np.linspace(-rect[0]/2, rect[0]/2, num=(ResX-1)*2) # Because iRfft 110 Y = np.linspace(-rect[1]/2, rect[1]/2, num=ResY) 111 112 DerivInterpX = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(DerivX), method="linear") 113 DerivInterpY = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(DerivY), method="linear") 114 SurfInterp = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(deformation), method="linear") 115 116 self.DerivX = DerivX 117 self.DerivY = DerivY 118 self.rms = np.std(deformation) 119 self.deformation = deformation 120 self.DerivInterp = lambda x: np.array([DerivInterpX(x[:2]), DerivInterpY(x[:2])]) 121 self.SurfInterp = lambda x: SurfInterp(x[:2]) 122 123 def get_normal(self, Point): 124 """ 125 Calculates and returns the surface normal at the given Point. 126 It uses the derivative interpolators to compute the partial derivatives of the surface 127 deformation and returns a normalized vector representing the surface normal. 128 """ 129 dX, dY = self.DerivInterp(Point) 130 norm = np.linalg.norm([dX, dY, 1]) 131 dX /= norm 132 dY /= norm 133 return np.array([dX, dY, np.sqrt(1 - dX**2 - dY**2)]) 134 135 def get_offset(self, Point): 136 """ 137 Calculates and returns the surface offset at the given Point. 138 It uses the surface deformation interpolator to determine the displacement 139 at the specified point on the surface. 140 """ 141 return self.SurfInterp(Point) 142 143 def RMS(self): 144 """ 145 Returns the root mean square (RMS) value of the surface deformation (self.rms). 146 """ 147 return self.rms 148 149 def PV(self): 150 pass
77 def __init__(self, Support, RMS, slope=-2, smallest=0.1, biggest=None): 78 # The sizes are the wavelength in mm 79 rect = Support._CircumRect() # TODO fix this so it just uses a rectangle with known size 80 if biggest is None: 81 biggest = np.max(rect) 82 83 k_max = 2 / smallest 84 k_min = 2 / biggest 85 ResX = int(round(k_max * rect[0] / 2))+1 86 ResY = int(round(k_max * rect[1])) 87 88 kXX, kYY = np.meshgrid( 89 np.linspace(0, k_max, num=ResX, dtype='float32', endpoint=False), 90 np.linspace(-k_max, k_max, num=ResY, dtype='float32', endpoint=False), 91 sparse=True) 92 93 maskedFFT = np.ma.masked_outside(np.sqrt(kXX**2 + kYY**2), k_min, k_max) 94 95 FFT = maskedFFT**slope * np.exp( 96 1j *np.random.uniform(0, 2 * np.pi, size=maskedFFT.shape).astype('float32') 97 ) 98 FFT = FFT.data*(1-FFT.mask) 99 100 deformation = np.fft.irfft2(np.fft.ifftshift(FFT, axes=0)) 101 RMS_factor = RMS/np.std(deformation) 102 deformation *= RMS_factor 103 104 DerivX = np.fft.irfft2(np.fft.ifftshift(FFT * 1j * kXX * RMS_factor, axes=0))*np.pi/2 105 kY = np.concatenate((kYY[kYY.shape[0]//2:],kYY[:kYY.shape[0]//2])) 106 DerivY = np.fft.irfft2(np.fft.ifftshift(FFT * 1j * RMS_factor, axes=0)*kY)*np.pi/2 107 #del FFT 108 109 X = np.linspace(-rect[0]/2, rect[0]/2, num=(ResX-1)*2) # Because iRfft 110 Y = np.linspace(-rect[1]/2, rect[1]/2, num=ResY) 111 112 DerivInterpX = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(DerivX), method="linear") 113 DerivInterpY = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(DerivY), method="linear") 114 SurfInterp = scipy.interpolate.RegularGridInterpolator((X, Y), np.transpose(deformation), method="linear") 115 116 self.DerivX = DerivX 117 self.DerivY = DerivY 118 self.rms = np.std(deformation) 119 self.deformation = deformation 120 self.DerivInterp = lambda x: np.array([DerivInterpX(x[:2]), DerivInterpY(x[:2])]) 121 self.SurfInterp = lambda x: SurfInterp(x[:2])
123 def get_normal(self, Point): 124 """ 125 Calculates and returns the surface normal at the given Point. 126 It uses the derivative interpolators to compute the partial derivatives of the surface 127 deformation and returns a normalized vector representing the surface normal. 128 """ 129 dX, dY = self.DerivInterp(Point) 130 norm = np.linalg.norm([dX, dY, 1]) 131 dX /= norm 132 dY /= norm 133 return np.array([dX, dY, np.sqrt(1 - dX**2 - dY**2)])
Calculates and returns the surface normal at the given Point. It uses the derivative interpolators to compute the partial derivatives of the surface deformation and returns a normalized vector representing the surface normal.
135 def get_offset(self, Point): 136 """ 137 Calculates and returns the surface offset at the given Point. 138 It uses the surface deformation interpolator to determine the displacement 139 at the specified point on the surface. 140 """ 141 return self.SurfInterp(Point)
Calculates and returns the surface offset at the given Point. It uses the surface deformation interpolator to determine the displacement at the specified point on the surface.
153class Zernike(Defect): 154 def __init__(self, Support, coefficients): 155 self.coefficients = coefficients 156 self.max_order = np.max([k[0] for k in coefficients]) 157 self.support = Support 158 self.R = Support._CircumCirc() 159 160 def get_normal(self, Point): 161 x, y, z = Point / self.R 162 val, derivX, derivY = zernike_gradient([x], [y], self.max_order) 163 dX = 0 164 dY = 0 165 for k in self.coefficients: 166 dX += self.coefficients[k] * derivX[k][0][1][0] 167 dY += self.coefficients[k] * derivY[k][0][1][0] 168 dX /= self.R 169 dY /= self.R 170 return np.array([-dX, -dY, 1]) 171 172 def get_offset(self, Point): 173 x, y, z = Point / self.R 174 val, derivX, derivY = zernike_gradient([x], [y], self.max_order) 175 Z = 0 176 for k in self.coefficients: 177 Z += self.coefficients[k] * val[k][0][1][0] 178 return Z 179 180 def RMS(self): 181 return np.sqrt(np.sum([i**2 for i in self.coefficients.values()])) 182 183 def PV(self): 184 pass
Abstract class representing a defect on the optical surface.
160 def get_normal(self, Point): 161 x, y, z = Point / self.R 162 val, derivX, derivY = zernike_gradient([x], [y], self.max_order) 163 dX = 0 164 dY = 0 165 for k in self.coefficients: 166 dX += self.coefficients[k] * derivX[k][0][1][0] 167 dY += self.coefficients[k] * derivY[k][0][1][0] 168 dX /= self.R 169 dY /= self.R 170 return np.array([-dX, -dY, 1])
Provides a class which represents a virtual detector —a plane in 3D space— with methods to analyze the impact points of a ray bundle intercepting it.
Created in Sept 2020
@author: Stefan Haessler + André Kalouguine
1""" 2Provides a class which represents a virtual detector —a plane in 3D space— with methods 3to analyze the impact points of a ray bundle intercepting it. 4 5 6 7 8 9Created in Sept 2020 10 11@author: Stefan Haessler + André Kalouguine 12""" 13# %% Modules 14 15import numpy as np 16import quaternion 17from abc import ABC, abstractmethod 18 19import ARTcore.ModuleProcessing as mp 20import ARTcore.ModuleOpticalRay as mray 21import ARTcore.ModuleGeometry as mgeo 22from ARTcore.ModuleGeometry import Point, Vector, Origin 23 24# This is to allow multiple constructors based on the type of the first argument 25from functools import singledispatchmethod 26import logging 27from copy import copy 28import time 29 30 31logger = logging.getLogger(__name__) 32 33LightSpeed = 299792458000 34 35# %% Abstract class 36class Detector(ABC): 37 """ 38 Abstract class describing a detector and the properties/methods it has to have. 39 It implements the required components for the detector to behave as a 3D object. 40 Non-abstract subclasses of `Detector` should implement the remaining 41 functiuons such as the detection of rays 42 """ 43 type = "Generic Detector (Abstract)" 44 @property 45 def r(self): 46 """ 47 Return the offset of the optical element from its reference frame to the lab reference frame. 48 Not that this is **NOT** the position of the optical element's center point. Rather it is the 49 offset of the reference frame of the optical element from the lab reference frame. 50 """ 51 return self._r 52 53 @r.setter 54 def r(self, NewPosition): 55 """ 56 Set the offset of the optical element from its reference frame to the lab reference frame. 57 """ 58 if isinstance(NewPosition, mgeo.Point) and len(NewPosition) == 3: 59 self._r = NewPosition 60 else: 61 raise TypeError("Position must be a 3D mgeo.Point.") 62 @property 63 def q(self): 64 """ 65 Return the orientation of the optical element. 66 The orientation is stored as a unit quaternion representing the rotation from the optic's coordinate frame to the lab frame. 67 """ 68 return self._q 69 70 @q.setter 71 def q(self, NewOrientation): 72 """ 73 Set the orientation of the optical element. 74 This function normalizes the input quaternion before storing it. 75 If the input is not a quaternion, raise a TypeError. 76 """ 77 if isinstance(NewOrientation, np.quaternion): 78 self._q = NewOrientation.normalized() 79 else: 80 raise TypeError("Orientation must be a quaternion.") 81 82 @property 83 def position(self): 84 """ 85 Return the position of the basepoint of the optical element. Often it is the same as the optical centre. 86 This position is the one around which all rotations are performed. 87 """ 88 return self.r0 + self.r 89 90 @property 91 def orientation(self): 92 """ 93 Return the orientation of the optical element. 94 The utility of this method is unclear. 95 """ 96 return self.q 97 98 @property 99 def basis(self): 100 return self.r0, self.r, self.q 101 102 def __init__(self): 103 self._r = mgeo.Vector([0.0, 0.0, 0.0]) 104 self._q = np.quaternion(1, 0, 0, 0) 105 self.r0 = mgeo.Point([0.0, 0.0, 0.0]) 106 107 @abstractmethod 108 def centre(self): 109 """ 110 (0,0) point of the detector. 111 """ 112 pass 113 114 @abstractmethod 115 def refpoint(self): 116 """ 117 Reference point from which to measure the distance of the detector. 118 Usually the center of the previous optical element. 119 """ 120 pass 121 122 @property 123 def distance(self): 124 """ 125 Return distance of the Detector from its reference point Detector.refpoint. 126 """ 127 return (self.refpoint - self.centre).norm 128 129 @distance.setter 130 def distance(self, NewDistance: float): 131 """ 132 Shift the Detector to the distance NewDistance from its reference point Detector.refpoint. 133 134 Parameters 135 ---------- 136 NewDistance : number 137 The distance (absolute) to which to shift the Detector. 138 """ 139 vector = (self.centre - self.refpoint).normalized 140 self.centre = self.refpoint + vector * NewDistance 141 142 @abstractmethod 143 def get_2D_points(self, RayList): 144 pass 145 146 @abstractmethod 147 def get_3D_points(self, RayList): 148 pass 149 150 @abstractmethod 151 def __copy__(self): 152 pass 153 154 155# %% Infinite plane detector class 156class InfiniteDetector(Detector): 157 """ 158 Simple infinite plane. 159 Beyond being just a solid object, the 160 161 Attributes 162 ---------- 163 centre : np.ndarray 164 3D Point in the Detector plane. 165 166 refpoint : np.ndarray 167 3D reference point from which to measure the Detector distance. 168 """ 169 type = "Infinite Detector" 170 def __init__(self, index=-1): 171 super().__init__() 172 self._centre = mgeo.Origin 173 self._refpoint = mgeo.Origin 174 self.index = index 175 176 def __copy__(self): 177 """ 178 Returns a new Detector object with the same properties. 179 """ 180 result = InfiniteDetector() 181 result._centre = copy(self._centre) 182 result._refpoint = copy(self._refpoint) 183 result._r = copy(self._r) 184 result._q = copy(self._q) 185 result.r0 = copy(self.r0) 186 result.index = self.index 187 return result 188 189 @property 190 def normal(self): 191 return mgeo.Vector([0,0,1]).from_basis(*self.basis) 192 193 @property 194 def centre(self): 195 return self._centre 196 197 # a setter function 198 @centre.setter 199 def centre(self, Centre): 200 if isinstance(Centre, np.ndarray) and Centre.shape == (3,): 201 self._centre = mgeo.Point(Centre) 202 self.r = self._centre # Simply because r0 is 0,0,0 anyways 203 else: 204 raise TypeError("Detector Centre must be a 3D-vector, given as numpy.ndarray of shape (3,).") 205 206 @property 207 def refpoint(self): 208 return self._refpoint 209 210 # a setter function 211 @refpoint.setter 212 def refpoint(self, RefPoint): 213 if isinstance(RefPoint, np.ndarray) and RefPoint.shape == (3,): 214 self._refpoint = mgeo.Point(RefPoint) 215 else: 216 raise TypeError("Detector RefPoint must a 3D-Point") 217 218 # %% Detector placement methods 219 220 @property 221 def distance(self): 222 """ 223 Return distance of the Detector from its reference point Detector.refpoint. 224 """ 225 return (self.refpoint - self.centre).norm 226 227 @distance.setter 228 def distance(self, NewDistance: float): 229 """ 230 Shift the Detector to the distance NewDistance from its reference point Detector.refpoint. 231 232 Parameters 233 ---------- 234 NewDistance : number 235 The distance (absolute) to which to shift the Detector. 236 """ 237 vector = (self.centre - self.refpoint).normalized() 238 self.centre = self.refpoint + vector * NewDistance 239 240 def set_distance(self,x): 241 self.distance = x 242 243 def autoplace(self, RayList, DistanceDetector: float): 244 """ 245 Automatically place and orient the detector such that it is normal to the central ray 246 of the supplied RayList, at the distance DistanceDetector the origin point of that central ray. 247 248 Parameters 249 ---------- 250 RayList : list[Ray] 251 A list of objects of the ModuleOpticalRay.Ray-class. 252 253 DistanceDetector : float 254 The distance at which to place the Detector. 255 """ 256 #CentralRay = mp.FindCentralRay(RayList) 257 CentralRay = RayList[0] 258 if CentralRay is None: 259 logger.warning(f"Could not find central ray! The list of rays has a length of {len(RayList)}") 260 CentralPoint = mgeo.Origin 261 for k in RayList: 262 CentralPoint = CentralPoint + (k.point - mgeo.Origin) 263 CentralPoint = CentralPoint / len(RayList) 264 else: 265 logger.debug(f"Found central ray, using it to position detector: \n{CentralRay}") 266 CentralPoint = CentralRay.point 267 268 self.centre = CentralPoint + CentralRay.vector * DistanceDetector 269 self.refpoint = CentralPoint 270 self.q = mgeo.QRotationVector2Vector(mgeo.Vector([0,0,1]), -CentralRay.vector) 271 272 # %% Detector optimisation methods 273 def test_callback_distances(self, RayList, distances, callback, 274 provide_points=False, 275 detector_reference_frame=False): 276 LocalRayList = [k.to_basis(*self.basis) for k in RayList] 277 N = len(distances) 278 # Calculate the impact points 279 Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances) 280 values = [] 281 for k in range(N): 282 Points[k] = Points[k]._add_dimension() + mgeo.Point([0,0,distances[k]]) 283 values += [callback(RayList, Points[k], self.basis)] 284 return values 285 286 def optimise_distance(self, RayList, Range, callback, 287 detector_reference_frame=False, 288 provide_points=False, 289 maxiter=5, 290 tol=1e-6, 291 splitting=50, 292 Nrays=1000, 293 callback_iteration=None 294 ): 295 """ 296 Optimise the position of the detector within the provided range, trying to 297 maximise some value calculated by the callback function. 298 299 The callback function receives a list of rays that already hit the detector. 300 If requested, the rays can be provided in the reference frame of the detector. 301 302 The callback function should return a single value that is to be minimised. 303 304 The function splits the range into `splitting` points and uses the `IntersectionRayListZPlane` 305 function to calculate the impact points for all of these. 306 If `provide_points` is set to True, the function will pass on the result to the callback function. 307 Otherwise it will generate rays for the callback function, including calculating the paths. 308 309 It will then find the best value and redo the iteration around that value. 310 Repeat until either the maximum number of iterations is reached or the tolerance is met. 311 312 Parameters 313 ---------- 314 RayList : list[Ray] 315 A list of objects of the ModuleOpticalRay.Ray-class. 316 317 Range : tuple 318 The range of distances to consider for the detector placement. 319 320 callback : function 321 The function to be minimised. It should take a list of rays and return a single value. 322 323 detector_reference_frame : bool 324 If True, the rays are provided in the reference frame of the detector. 325 326 provide_points : bool 327 If True, the callback function will receive the impact points directly. 328 329 maxiter : int 330 The maximum number of iterations to perform. 331 332 tol : float 333 The tolerance to reach before stopping the iteration. 334 335 splitting : int 336 The number of points to split the range into. 337 338 Returns 339 ---------- 340 The optimal distance of the detector. 341 """ 342 Range = [i-self.distance for i in Range] 343 if Nrays<len(RayList): 344 RayList = np.random.choice(RayList, Nrays, replace=False) 345 previous_best = None 346 values = [0]*splitting 347 for i in range(maxiter): 348 if callback_iteration is not None: 349 callback_iteration(i, Range) 350 # Split the range into `splitting` points 351 distances = np.linspace(*Range, splitting) 352 values = self.test_callback_distances(RayList, distances, callback, 353 provide_points=provide_points, 354 detector_reference_frame=detector_reference_frame) 355 best = np.argmin(values) 356 # Update the range 357 if best == 0: 358 Range = (distances[0], distances[2]) 359 elif best == splitting - 1: 360 Range = (distances[-3], distances[-1]) 361 else: 362 Range = (distances[best - 1], distances[best + 1]) 363 # Check if the tolerance is met 364 if i>0: 365 if np.abs(values[best] - previous_best) < tol: 366 break 367 previous_best = values[best] 368 self.distance -= distances[best] 369 370 def _spot_size(self, RayList, Points, basis): 371 """ 372 Returns focal spot size. 373 374 Parameters 375 ---------- 376 Points : mgeo.PointArray 377 The points where the rays hit the detector. 378 Returns 379 ---------- 380 float 381 """ 382 center = mgeo.Point(np.mean(Points[:,:2], axis=0)) 383 distances = (Points[:,:2] - center).norm 384 return np.std(distances) 385 386 def _delay_std(self, RayList, Points, basis): 387 """ 388 Returns the standard deviation of the delays of the rays 389 390 Parameters 391 ---------- 392 RayList : list[Ray] 393 A list of objects of the ModuleOpticalRay.Ray-class. 394 395 Returns 396 ---------- 397 float 398 """ 399 #paths = np.array([np.sum(k.path) for k in RayList]) 400 paths = np.sum(np.array([k.path for k in RayList], dtype=float), axis=1) 401 StartingPoints = mgeo.PointArray([k.point for k in RayList]) 402 XYZ = Points.from_basis(*basis) 403 LastDistances = (XYZ - StartingPoints).norm 404 return float(np.std(paths+LastDistances)) 405 406 def _over_intensity(self, RayList, Points, Basis): 407 """ 408 Calculates spot_size * delay_std for the given RayList. 409 """ 410 spot_size = self._spot_size(RayList, Points, Basis) 411 delay_std = self._delay_std(RayList, Points, Basis) 412 return spot_size**2 * delay_std 413 414 # %% Detector response methods 415 def get_3D_points(self, RayList) -> list[np.ndarray]: 416 """ 417 Returns the list of 3D-points in lab-space where the rays in the 418 list RayList hit the detector plane. 419 420 Parameters 421 ---------- 422 RayList : list[Ray] 423 A list of objects of the ModuleOpticalRay.Ray-class. 424 425 Returns 426 ---------- 427 ListPointDetector3D : list[np.ndarray of shape (3,)] 428 """ 429 return self.get_2D_points(RayList)[0]._add_dimension().from_basis(*self.basis) 430 431 def get_2D_points(self, RayList) -> list[np.ndarray]: 432 """ 433 Returns the list of 2D-points in the detector plane, with the origin at Detector.centre. 434 435 Parameters 436 ---------- 437 RayList : list[Ray] 438 A list of objects of the ModuleOpticalRay.Ray-class of length N 439 440 Returns 441 ---------- 442 XY : np.ndarray of shape (N,2) 443 """ 444 return mgeo.IntersectionRayListZPlane([r.to_basis(*self.basis) for r in RayList]) 445 446 def get_centre_2D_points(self, RayList) -> list[np.ndarray]: 447 """ 448 Returns the center of the 2D array of points. 449 450 Parameters 451 ---------- 452 RayList* : list[Ray] 453 A list of objects of the ModuleOpticalRay.Ray-class. 454 455 Returns 456 ---------- 457 ListPointDetector2DCentre : list[np.ndarray of shape (2,)] 458 """ 459 return np.mean(self.get_2D_points(RayList), axis=0) 460 461 def get_Delays(self, RayList) -> list[float]: 462 """ 463 Returns the list of delays of the rays of RayList as they hit the detector plane, 464 calculated as their total path length divided by the speed of light. 465 The delays are relative to the mean “travel time”, i.e. the mean path length of RayList 466 divided by the speed of light. 467 The list index corresponds to that of the RayList. 468 469 Parameters 470 ---------- 471 RayList : list[Ray] 472 A list of objects of the ModuleOpticalRay.Ray-class. 473 474 Returns 475 ---------- 476 DelayList : list[float] 477 """ 478 localRayList = RayList.to_basis(*self.basis) 479 paths = np.sum(RayList.path, axis=1) 480 StartingPoints = mgeo.PointArray([k.point for k in localRayList]) 481 XYZ = mgeo.IntersectionRayListZPlane(localRayList)[0]._add_dimension() 482 LastDistances = (XYZ - StartingPoints).norm 483 TotalPaths = paths+LastDistances 484 MeanPath = np.mean(TotalPaths) 485 return list((TotalPaths-MeanPath) / LightSpeed * 1e15) # in fs
37class Detector(ABC): 38 """ 39 Abstract class describing a detector and the properties/methods it has to have. 40 It implements the required components for the detector to behave as a 3D object. 41 Non-abstract subclasses of `Detector` should implement the remaining 42 functiuons such as the detection of rays 43 """ 44 type = "Generic Detector (Abstract)" 45 @property 46 def r(self): 47 """ 48 Return the offset of the optical element from its reference frame to the lab reference frame. 49 Not that this is **NOT** the position of the optical element's center point. Rather it is the 50 offset of the reference frame of the optical element from the lab reference frame. 51 """ 52 return self._r 53 54 @r.setter 55 def r(self, NewPosition): 56 """ 57 Set the offset of the optical element from its reference frame to the lab reference frame. 58 """ 59 if isinstance(NewPosition, mgeo.Point) and len(NewPosition) == 3: 60 self._r = NewPosition 61 else: 62 raise TypeError("Position must be a 3D mgeo.Point.") 63 @property 64 def q(self): 65 """ 66 Return the orientation of the optical element. 67 The orientation is stored as a unit quaternion representing the rotation from the optic's coordinate frame to the lab frame. 68 """ 69 return self._q 70 71 @q.setter 72 def q(self, NewOrientation): 73 """ 74 Set the orientation of the optical element. 75 This function normalizes the input quaternion before storing it. 76 If the input is not a quaternion, raise a TypeError. 77 """ 78 if isinstance(NewOrientation, np.quaternion): 79 self._q = NewOrientation.normalized() 80 else: 81 raise TypeError("Orientation must be a quaternion.") 82 83 @property 84 def position(self): 85 """ 86 Return the position of the basepoint of the optical element. Often it is the same as the optical centre. 87 This position is the one around which all rotations are performed. 88 """ 89 return self.r0 + self.r 90 91 @property 92 def orientation(self): 93 """ 94 Return the orientation of the optical element. 95 The utility of this method is unclear. 96 """ 97 return self.q 98 99 @property 100 def basis(self): 101 return self.r0, self.r, self.q 102 103 def __init__(self): 104 self._r = mgeo.Vector([0.0, 0.0, 0.0]) 105 self._q = np.quaternion(1, 0, 0, 0) 106 self.r0 = mgeo.Point([0.0, 0.0, 0.0]) 107 108 @abstractmethod 109 def centre(self): 110 """ 111 (0,0) point of the detector. 112 """ 113 pass 114 115 @abstractmethod 116 def refpoint(self): 117 """ 118 Reference point from which to measure the distance of the detector. 119 Usually the center of the previous optical element. 120 """ 121 pass 122 123 @property 124 def distance(self): 125 """ 126 Return distance of the Detector from its reference point Detector.refpoint. 127 """ 128 return (self.refpoint - self.centre).norm 129 130 @distance.setter 131 def distance(self, NewDistance: float): 132 """ 133 Shift the Detector to the distance NewDistance from its reference point Detector.refpoint. 134 135 Parameters 136 ---------- 137 NewDistance : number 138 The distance (absolute) to which to shift the Detector. 139 """ 140 vector = (self.centre - self.refpoint).normalized 141 self.centre = self.refpoint + vector * NewDistance 142 143 @abstractmethod 144 def get_2D_points(self, RayList): 145 pass 146 147 @abstractmethod 148 def get_3D_points(self, RayList): 149 pass 150 151 @abstractmethod 152 def __copy__(self): 153 pass
Abstract class describing a detector and the properties/methods it has to have.
It implements the required components for the detector to behave as a 3D object.
Non-abstract subclasses of Detector should implement the remaining
functiuons such as the detection of rays
45 @property 46 def r(self): 47 """ 48 Return the offset of the optical element from its reference frame to the lab reference frame. 49 Not that this is **NOT** the position of the optical element's center point. Rather it is the 50 offset of the reference frame of the optical element from the lab reference frame. 51 """ 52 return self._r
Return the offset of the optical element from its reference frame to the lab reference frame. Not that this is NOT the position of the optical element's center point. Rather it is the offset of the reference frame of the optical element from the lab reference frame.
63 @property 64 def q(self): 65 """ 66 Return the orientation of the optical element. 67 The orientation is stored as a unit quaternion representing the rotation from the optic's coordinate frame to the lab frame. 68 """ 69 return self._q
Return the orientation of the optical element. The orientation is stored as a unit quaternion representing the rotation from the optic's coordinate frame to the lab frame.
83 @property 84 def position(self): 85 """ 86 Return the position of the basepoint of the optical element. Often it is the same as the optical centre. 87 This position is the one around which all rotations are performed. 88 """ 89 return self.r0 + self.r
Return the position of the basepoint of the optical element. Often it is the same as the optical centre. This position is the one around which all rotations are performed.
91 @property 92 def orientation(self): 93 """ 94 Return the orientation of the optical element. 95 The utility of this method is unclear. 96 """ 97 return self.q
Return the orientation of the optical element. The utility of this method is unclear.
115 @abstractmethod 116 def refpoint(self): 117 """ 118 Reference point from which to measure the distance of the detector. 119 Usually the center of the previous optical element. 120 """ 121 pass
Reference point from which to measure the distance of the detector. Usually the center of the previous optical element.
123 @property 124 def distance(self): 125 """ 126 Return distance of the Detector from its reference point Detector.refpoint. 127 """ 128 return (self.refpoint - self.centre).norm
Return distance of the Detector from its reference point Detector.refpoint.
157class InfiniteDetector(Detector): 158 """ 159 Simple infinite plane. 160 Beyond being just a solid object, the 161 162 Attributes 163 ---------- 164 centre : np.ndarray 165 3D Point in the Detector plane. 166 167 refpoint : np.ndarray 168 3D reference point from which to measure the Detector distance. 169 """ 170 type = "Infinite Detector" 171 def __init__(self, index=-1): 172 super().__init__() 173 self._centre = mgeo.Origin 174 self._refpoint = mgeo.Origin 175 self.index = index 176 177 def __copy__(self): 178 """ 179 Returns a new Detector object with the same properties. 180 """ 181 result = InfiniteDetector() 182 result._centre = copy(self._centre) 183 result._refpoint = copy(self._refpoint) 184 result._r = copy(self._r) 185 result._q = copy(self._q) 186 result.r0 = copy(self.r0) 187 result.index = self.index 188 return result 189 190 @property 191 def normal(self): 192 return mgeo.Vector([0,0,1]).from_basis(*self.basis) 193 194 @property 195 def centre(self): 196 return self._centre 197 198 # a setter function 199 @centre.setter 200 def centre(self, Centre): 201 if isinstance(Centre, np.ndarray) and Centre.shape == (3,): 202 self._centre = mgeo.Point(Centre) 203 self.r = self._centre # Simply because r0 is 0,0,0 anyways 204 else: 205 raise TypeError("Detector Centre must be a 3D-vector, given as numpy.ndarray of shape (3,).") 206 207 @property 208 def refpoint(self): 209 return self._refpoint 210 211 # a setter function 212 @refpoint.setter 213 def refpoint(self, RefPoint): 214 if isinstance(RefPoint, np.ndarray) and RefPoint.shape == (3,): 215 self._refpoint = mgeo.Point(RefPoint) 216 else: 217 raise TypeError("Detector RefPoint must a 3D-Point") 218 219 # %% Detector placement methods 220 221 @property 222 def distance(self): 223 """ 224 Return distance of the Detector from its reference point Detector.refpoint. 225 """ 226 return (self.refpoint - self.centre).norm 227 228 @distance.setter 229 def distance(self, NewDistance: float): 230 """ 231 Shift the Detector to the distance NewDistance from its reference point Detector.refpoint. 232 233 Parameters 234 ---------- 235 NewDistance : number 236 The distance (absolute) to which to shift the Detector. 237 """ 238 vector = (self.centre - self.refpoint).normalized() 239 self.centre = self.refpoint + vector * NewDistance 240 241 def set_distance(self,x): 242 self.distance = x 243 244 def autoplace(self, RayList, DistanceDetector: float): 245 """ 246 Automatically place and orient the detector such that it is normal to the central ray 247 of the supplied RayList, at the distance DistanceDetector the origin point of that central ray. 248 249 Parameters 250 ---------- 251 RayList : list[Ray] 252 A list of objects of the ModuleOpticalRay.Ray-class. 253 254 DistanceDetector : float 255 The distance at which to place the Detector. 256 """ 257 #CentralRay = mp.FindCentralRay(RayList) 258 CentralRay = RayList[0] 259 if CentralRay is None: 260 logger.warning(f"Could not find central ray! The list of rays has a length of {len(RayList)}") 261 CentralPoint = mgeo.Origin 262 for k in RayList: 263 CentralPoint = CentralPoint + (k.point - mgeo.Origin) 264 CentralPoint = CentralPoint / len(RayList) 265 else: 266 logger.debug(f"Found central ray, using it to position detector: \n{CentralRay}") 267 CentralPoint = CentralRay.point 268 269 self.centre = CentralPoint + CentralRay.vector * DistanceDetector 270 self.refpoint = CentralPoint 271 self.q = mgeo.QRotationVector2Vector(mgeo.Vector([0,0,1]), -CentralRay.vector) 272 273 # %% Detector optimisation methods 274 def test_callback_distances(self, RayList, distances, callback, 275 provide_points=False, 276 detector_reference_frame=False): 277 LocalRayList = [k.to_basis(*self.basis) for k in RayList] 278 N = len(distances) 279 # Calculate the impact points 280 Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances) 281 values = [] 282 for k in range(N): 283 Points[k] = Points[k]._add_dimension() + mgeo.Point([0,0,distances[k]]) 284 values += [callback(RayList, Points[k], self.basis)] 285 return values 286 287 def optimise_distance(self, RayList, Range, callback, 288 detector_reference_frame=False, 289 provide_points=False, 290 maxiter=5, 291 tol=1e-6, 292 splitting=50, 293 Nrays=1000, 294 callback_iteration=None 295 ): 296 """ 297 Optimise the position of the detector within the provided range, trying to 298 maximise some value calculated by the callback function. 299 300 The callback function receives a list of rays that already hit the detector. 301 If requested, the rays can be provided in the reference frame of the detector. 302 303 The callback function should return a single value that is to be minimised. 304 305 The function splits the range into `splitting` points and uses the `IntersectionRayListZPlane` 306 function to calculate the impact points for all of these. 307 If `provide_points` is set to True, the function will pass on the result to the callback function. 308 Otherwise it will generate rays for the callback function, including calculating the paths. 309 310 It will then find the best value and redo the iteration around that value. 311 Repeat until either the maximum number of iterations is reached or the tolerance is met. 312 313 Parameters 314 ---------- 315 RayList : list[Ray] 316 A list of objects of the ModuleOpticalRay.Ray-class. 317 318 Range : tuple 319 The range of distances to consider for the detector placement. 320 321 callback : function 322 The function to be minimised. It should take a list of rays and return a single value. 323 324 detector_reference_frame : bool 325 If True, the rays are provided in the reference frame of the detector. 326 327 provide_points : bool 328 If True, the callback function will receive the impact points directly. 329 330 maxiter : int 331 The maximum number of iterations to perform. 332 333 tol : float 334 The tolerance to reach before stopping the iteration. 335 336 splitting : int 337 The number of points to split the range into. 338 339 Returns 340 ---------- 341 The optimal distance of the detector. 342 """ 343 Range = [i-self.distance for i in Range] 344 if Nrays<len(RayList): 345 RayList = np.random.choice(RayList, Nrays, replace=False) 346 previous_best = None 347 values = [0]*splitting 348 for i in range(maxiter): 349 if callback_iteration is not None: 350 callback_iteration(i, Range) 351 # Split the range into `splitting` points 352 distances = np.linspace(*Range, splitting) 353 values = self.test_callback_distances(RayList, distances, callback, 354 provide_points=provide_points, 355 detector_reference_frame=detector_reference_frame) 356 best = np.argmin(values) 357 # Update the range 358 if best == 0: 359 Range = (distances[0], distances[2]) 360 elif best == splitting - 1: 361 Range = (distances[-3], distances[-1]) 362 else: 363 Range = (distances[best - 1], distances[best + 1]) 364 # Check if the tolerance is met 365 if i>0: 366 if np.abs(values[best] - previous_best) < tol: 367 break 368 previous_best = values[best] 369 self.distance -= distances[best] 370 371 def _spot_size(self, RayList, Points, basis): 372 """ 373 Returns focal spot size. 374 375 Parameters 376 ---------- 377 Points : mgeo.PointArray 378 The points where the rays hit the detector. 379 Returns 380 ---------- 381 float 382 """ 383 center = mgeo.Point(np.mean(Points[:,:2], axis=0)) 384 distances = (Points[:,:2] - center).norm 385 return np.std(distances) 386 387 def _delay_std(self, RayList, Points, basis): 388 """ 389 Returns the standard deviation of the delays of the rays 390 391 Parameters 392 ---------- 393 RayList : list[Ray] 394 A list of objects of the ModuleOpticalRay.Ray-class. 395 396 Returns 397 ---------- 398 float 399 """ 400 #paths = np.array([np.sum(k.path) for k in RayList]) 401 paths = np.sum(np.array([k.path for k in RayList], dtype=float), axis=1) 402 StartingPoints = mgeo.PointArray([k.point for k in RayList]) 403 XYZ = Points.from_basis(*basis) 404 LastDistances = (XYZ - StartingPoints).norm 405 return float(np.std(paths+LastDistances)) 406 407 def _over_intensity(self, RayList, Points, Basis): 408 """ 409 Calculates spot_size * delay_std for the given RayList. 410 """ 411 spot_size = self._spot_size(RayList, Points, Basis) 412 delay_std = self._delay_std(RayList, Points, Basis) 413 return spot_size**2 * delay_std 414 415 # %% Detector response methods 416 def get_3D_points(self, RayList) -> list[np.ndarray]: 417 """ 418 Returns the list of 3D-points in lab-space where the rays in the 419 list RayList hit the detector plane. 420 421 Parameters 422 ---------- 423 RayList : list[Ray] 424 A list of objects of the ModuleOpticalRay.Ray-class. 425 426 Returns 427 ---------- 428 ListPointDetector3D : list[np.ndarray of shape (3,)] 429 """ 430 return self.get_2D_points(RayList)[0]._add_dimension().from_basis(*self.basis) 431 432 def get_2D_points(self, RayList) -> list[np.ndarray]: 433 """ 434 Returns the list of 2D-points in the detector plane, with the origin at Detector.centre. 435 436 Parameters 437 ---------- 438 RayList : list[Ray] 439 A list of objects of the ModuleOpticalRay.Ray-class of length N 440 441 Returns 442 ---------- 443 XY : np.ndarray of shape (N,2) 444 """ 445 return mgeo.IntersectionRayListZPlane([r.to_basis(*self.basis) for r in RayList]) 446 447 def get_centre_2D_points(self, RayList) -> list[np.ndarray]: 448 """ 449 Returns the center of the 2D array of points. 450 451 Parameters 452 ---------- 453 RayList* : list[Ray] 454 A list of objects of the ModuleOpticalRay.Ray-class. 455 456 Returns 457 ---------- 458 ListPointDetector2DCentre : list[np.ndarray of shape (2,)] 459 """ 460 return np.mean(self.get_2D_points(RayList), axis=0) 461 462 def get_Delays(self, RayList) -> list[float]: 463 """ 464 Returns the list of delays of the rays of RayList as they hit the detector plane, 465 calculated as their total path length divided by the speed of light. 466 The delays are relative to the mean “travel time”, i.e. the mean path length of RayList 467 divided by the speed of light. 468 The list index corresponds to that of the RayList. 469 470 Parameters 471 ---------- 472 RayList : list[Ray] 473 A list of objects of the ModuleOpticalRay.Ray-class. 474 475 Returns 476 ---------- 477 DelayList : list[float] 478 """ 479 localRayList = RayList.to_basis(*self.basis) 480 paths = np.sum(RayList.path, axis=1) 481 StartingPoints = mgeo.PointArray([k.point for k in localRayList]) 482 XYZ = mgeo.IntersectionRayListZPlane(localRayList)[0]._add_dimension() 483 LastDistances = (XYZ - StartingPoints).norm 484 TotalPaths = paths+LastDistances 485 MeanPath = np.mean(TotalPaths) 486 return list((TotalPaths-MeanPath) / LightSpeed * 1e15) # in fs
Simple infinite plane. Beyond being just a solid object, the
centre : np.ndarray
3D Point in the Detector plane.
refpoint : np.ndarray
3D reference point from which to measure the Detector distance.
Reference point from which to measure the distance of the detector. Usually the center of the previous optical element.
221 @property 222 def distance(self): 223 """ 224 Return distance of the Detector from its reference point Detector.refpoint. 225 """ 226 return (self.refpoint - self.centre).norm
Return distance of the Detector from its reference point Detector.refpoint.
244 def autoplace(self, RayList, DistanceDetector: float): 245 """ 246 Automatically place and orient the detector such that it is normal to the central ray 247 of the supplied RayList, at the distance DistanceDetector the origin point of that central ray. 248 249 Parameters 250 ---------- 251 RayList : list[Ray] 252 A list of objects of the ModuleOpticalRay.Ray-class. 253 254 DistanceDetector : float 255 The distance at which to place the Detector. 256 """ 257 #CentralRay = mp.FindCentralRay(RayList) 258 CentralRay = RayList[0] 259 if CentralRay is None: 260 logger.warning(f"Could not find central ray! The list of rays has a length of {len(RayList)}") 261 CentralPoint = mgeo.Origin 262 for k in RayList: 263 CentralPoint = CentralPoint + (k.point - mgeo.Origin) 264 CentralPoint = CentralPoint / len(RayList) 265 else: 266 logger.debug(f"Found central ray, using it to position detector: \n{CentralRay}") 267 CentralPoint = CentralRay.point 268 269 self.centre = CentralPoint + CentralRay.vector * DistanceDetector 270 self.refpoint = CentralPoint 271 self.q = mgeo.QRotationVector2Vector(mgeo.Vector([0,0,1]), -CentralRay.vector)
Automatically place and orient the detector such that it is normal to the central ray of the supplied RayList, at the distance DistanceDetector the origin point of that central ray.
RayList : list[Ray]
A list of objects of the ModuleOpticalRay.Ray-class.
DistanceDetector : float
The distance at which to place the Detector.
274 def test_callback_distances(self, RayList, distances, callback, 275 provide_points=False, 276 detector_reference_frame=False): 277 LocalRayList = [k.to_basis(*self.basis) for k in RayList] 278 N = len(distances) 279 # Calculate the impact points 280 Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances) 281 values = [] 282 for k in range(N): 283 Points[k] = Points[k]._add_dimension() + mgeo.Point([0,0,distances[k]]) 284 values += [callback(RayList, Points[k], self.basis)] 285 return values
287 def optimise_distance(self, RayList, Range, callback, 288 detector_reference_frame=False, 289 provide_points=False, 290 maxiter=5, 291 tol=1e-6, 292 splitting=50, 293 Nrays=1000, 294 callback_iteration=None 295 ): 296 """ 297 Optimise the position of the detector within the provided range, trying to 298 maximise some value calculated by the callback function. 299 300 The callback function receives a list of rays that already hit the detector. 301 If requested, the rays can be provided in the reference frame of the detector. 302 303 The callback function should return a single value that is to be minimised. 304 305 The function splits the range into `splitting` points and uses the `IntersectionRayListZPlane` 306 function to calculate the impact points for all of these. 307 If `provide_points` is set to True, the function will pass on the result to the callback function. 308 Otherwise it will generate rays for the callback function, including calculating the paths. 309 310 It will then find the best value and redo the iteration around that value. 311 Repeat until either the maximum number of iterations is reached or the tolerance is met. 312 313 Parameters 314 ---------- 315 RayList : list[Ray] 316 A list of objects of the ModuleOpticalRay.Ray-class. 317 318 Range : tuple 319 The range of distances to consider for the detector placement. 320 321 callback : function 322 The function to be minimised. It should take a list of rays and return a single value. 323 324 detector_reference_frame : bool 325 If True, the rays are provided in the reference frame of the detector. 326 327 provide_points : bool 328 If True, the callback function will receive the impact points directly. 329 330 maxiter : int 331 The maximum number of iterations to perform. 332 333 tol : float 334 The tolerance to reach before stopping the iteration. 335 336 splitting : int 337 The number of points to split the range into. 338 339 Returns 340 ---------- 341 The optimal distance of the detector. 342 """ 343 Range = [i-self.distance for i in Range] 344 if Nrays<len(RayList): 345 RayList = np.random.choice(RayList, Nrays, replace=False) 346 previous_best = None 347 values = [0]*splitting 348 for i in range(maxiter): 349 if callback_iteration is not None: 350 callback_iteration(i, Range) 351 # Split the range into `splitting` points 352 distances = np.linspace(*Range, splitting) 353 values = self.test_callback_distances(RayList, distances, callback, 354 provide_points=provide_points, 355 detector_reference_frame=detector_reference_frame) 356 best = np.argmin(values) 357 # Update the range 358 if best == 0: 359 Range = (distances[0], distances[2]) 360 elif best == splitting - 1: 361 Range = (distances[-3], distances[-1]) 362 else: 363 Range = (distances[best - 1], distances[best + 1]) 364 # Check if the tolerance is met 365 if i>0: 366 if np.abs(values[best] - previous_best) < tol: 367 break 368 previous_best = values[best] 369 self.distance -= distances[best]
Optimise the position of the detector within the provided range, trying to maximise some value calculated by the callback function.
The callback function receives a list of rays that already hit the detector. If requested, the rays can be provided in the reference frame of the detector.
The callback function should return a single value that is to be minimised.
The function splits the range into splitting points and uses the IntersectionRayListZPlane
function to calculate the impact points for all of these.
If provide_points is set to True, the function will pass on the result to the callback function.
Otherwise it will generate rays for the callback function, including calculating the paths.
It will then find the best value and redo the iteration around that value. Repeat until either the maximum number of iterations is reached or the tolerance is met.
RayList : list[Ray]
A list of objects of the ModuleOpticalRay.Ray-class.
Range : tuple
The range of distances to consider for the detector placement.
callback : function
The function to be minimised. It should take a list of rays and return a single value.
detector_reference_frame : bool
If True, the rays are provided in the reference frame of the detector.
provide_points : bool
If True, the callback function will receive the impact points directly.
maxiter : int
The maximum number of iterations to perform.
tol : float
The tolerance to reach before stopping the iteration.
splitting : int
The number of points to split the range into.
The optimal distance of the detector.
416 def get_3D_points(self, RayList) -> list[np.ndarray]: 417 """ 418 Returns the list of 3D-points in lab-space where the rays in the 419 list RayList hit the detector plane. 420 421 Parameters 422 ---------- 423 RayList : list[Ray] 424 A list of objects of the ModuleOpticalRay.Ray-class. 425 426 Returns 427 ---------- 428 ListPointDetector3D : list[np.ndarray of shape (3,)] 429 """ 430 return self.get_2D_points(RayList)[0]._add_dimension().from_basis(*self.basis)
Returns the list of 3D-points in lab-space where the rays in the list RayList hit the detector plane.
RayList : list[Ray]
A list of objects of the ModuleOpticalRay.Ray-class.
ListPointDetector3D : list[np.ndarray of shape (3,)]
432 def get_2D_points(self, RayList) -> list[np.ndarray]: 433 """ 434 Returns the list of 2D-points in the detector plane, with the origin at Detector.centre. 435 436 Parameters 437 ---------- 438 RayList : list[Ray] 439 A list of objects of the ModuleOpticalRay.Ray-class of length N 440 441 Returns 442 ---------- 443 XY : np.ndarray of shape (N,2) 444 """ 445 return mgeo.IntersectionRayListZPlane([r.to_basis(*self.basis) for r in RayList])
Returns the list of 2D-points in the detector plane, with the origin at Detector.centre.
RayList : list[Ray]
A list of objects of the ModuleOpticalRay.Ray-class of length N
XY : np.ndarray of shape (N,2)
447 def get_centre_2D_points(self, RayList) -> list[np.ndarray]: 448 """ 449 Returns the center of the 2D array of points. 450 451 Parameters 452 ---------- 453 RayList* : list[Ray] 454 A list of objects of the ModuleOpticalRay.Ray-class. 455 456 Returns 457 ---------- 458 ListPointDetector2DCentre : list[np.ndarray of shape (2,)] 459 """ 460 return np.mean(self.get_2D_points(RayList), axis=0)
Returns the center of the 2D array of points.
RayList* : list[Ray]
A list of objects of the ModuleOpticalRay.Ray-class.
ListPointDetector2DCentre : list[np.ndarray of shape (2,)]
462 def get_Delays(self, RayList) -> list[float]: 463 """ 464 Returns the list of delays of the rays of RayList as they hit the detector plane, 465 calculated as their total path length divided by the speed of light. 466 The delays are relative to the mean “travel time”, i.e. the mean path length of RayList 467 divided by the speed of light. 468 The list index corresponds to that of the RayList. 469 470 Parameters 471 ---------- 472 RayList : list[Ray] 473 A list of objects of the ModuleOpticalRay.Ray-class. 474 475 Returns 476 ---------- 477 DelayList : list[float] 478 """ 479 localRayList = RayList.to_basis(*self.basis) 480 paths = np.sum(RayList.path, axis=1) 481 StartingPoints = mgeo.PointArray([k.point for k in localRayList]) 482 XYZ = mgeo.IntersectionRayListZPlane(localRayList)[0]._add_dimension() 483 LastDistances = (XYZ - StartingPoints).norm 484 TotalPaths = paths+LastDistances 485 MeanPath = np.mean(TotalPaths) 486 return list((TotalPaths-MeanPath) / LightSpeed * 1e15) # in fs
Returns the list of delays of the rays of RayList as they hit the detector plane, calculated as their total path length divided by the speed of light. The delays are relative to the mean “travel time”, i.e. the mean path length of RayList divided by the speed of light. The list index corresponds to that of the RayList.
RayList : list[Ray]
A list of objects of the ModuleOpticalRay.Ray-class.
DelayList : list[float]
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:
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]
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.
(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.
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.
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>.
There are two modes of creating an array using __new__:
buffer is None, then only shape, dtype, and order
are used.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.
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])
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.
(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.
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.
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>.
There are two modes of creating an array using __new__:
buffer is None, then only shape, dtype, and order
are used.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.
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])
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.
(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.
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.
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>.
There are two modes of creating an array using __new__:
buffer is None, then only shape, dtype, and order
are used.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.
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])
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.
(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.
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.
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>.
There are two modes of creating an array using __new__:
buffer is None, then only shape, dtype, and order
are used.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.
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])
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.
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.
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.
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
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.
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
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.
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.
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.
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
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
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
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
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
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.
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].
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
397def TranslateSolid(Object, T): 398 """ 399 Translate object by vector T 400 """ 401 Object.r = Object.r + T
Translate object by vector T
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
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
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
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
425def Union_SDF(SDF1, SDF2): 426 """Union of two signed distance functions""" 427 return np.minimum(SDF1, SDF2)
Union of two signed distance functions
429def Difference_SDF(SDF1, SDF2): 430 """Difference of two signed distance functions""" 431 return np.maximum(SDF1, -SDF2)
Difference of two signed distance functions
433def Intersection_SDF(SDF1, SDF2): 434 """Intersection of two signed distance functions""" 435 return np.maximum(SDF1, SDF2)
Intersection of two signed distance functions
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
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.
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.
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
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
Provides a class for masks, which are a type of optical element that simply stops rays that hit it.
Also provides the function TransmitMaskRayList that returns the rays transmitted by the mask.
Created in 2021
@author: Stefan Haessler + André Kalouguine
1""" 2Provides a class for masks, which are a type of optical element that simply stops rays that hit it. 3 4Also provides the function *TransmitMaskRayList* that returns the rays transmitted by the mask. 5 6 7 8 9 10Created in 2021 11 12@author: Stefan Haessler + André Kalouguine 13""" 14# %% Modules 15import ARTcore.ModuleGeometry as mgeo 16from ARTcore.ModuleGeometry import Point, Vector, Origin 17import ARTcore.ModuleSupport as msup 18import ARTcore.ModuleOpticalRay as mray 19import ARTcore.ModuleOpticalElement as moe 20import ARTcore.ModuleMirror as mmirror 21 22import numpy as np 23from copy import copy 24import logging 25 26logger = logging.getLogger(__name__) 27 28# %%############################################################################ 29class Mask(moe.OpticalElement): 30 """ 31 A mask: a plane surface of the shape of its [Support](ModuleSupport.html), which stops all rays that hit it, 32 while all other rays continue their path unchanged. No diffraction effects. 33 34 Attributes 35 ---------- 36 support : [Support](ModuleSupport.html)-object 37 38 type : str 'Mask'. 39 40 Methods 41 ---------- 42 get_local_normal(Point) 43 44 get_centre() 45 46 get_grid3D(NbPoints) 47 48 """ 49 50 def __init__(self, Support, **kwargs): 51 """ 52 Parameters 53 ---------- 54 Support : [Support](ModuleSupport.html)-object 55 """ 56 self.type = "Mask" 57 self.support = Support 58 self.curvature = mmirror.Curvature.FLAT 59 60 self.r0 = mgeo.Point([0.0, 0.0, 0.0]) 61 self._r = mgeo.Vector([0.0, 0.0, 0.0]) 62 self._q = np.quaternion(1) 63 64 self.centre_ref = mgeo.Point([0, 0, 0]) 65 66 self.support_normal_ref = mgeo.Vector([0, 0, 1]) 67 self.normal_ref = mgeo.Vector([0, 0, 1]) 68 self.majoraxis_ref = mgeo.Vector([1, 0, 0]) 69 70 self.add_global_vectors("support_normal", "normal", "majoraxis") 71 self.add_global_points("centre") 72 73 super().__init__() 74 75 def _get_intersection(self, Ray): 76 """ 77 Return the intersection point between Ray and the xy-plane. 78 If not in alignment mode, any intersection point that is on the support is blocked. 79 """ 80 t = -Ray.point[2] / Ray.vector[2] 81 I = mgeo.Point(Ray.point +Ray.vector * t) 82 return I, I in self.support 83 84 def _get_intersections(self, RayList): 85 """ 86 Return the intersection point between Ray and the xy-plane. 87 If not in alignment mode, any intersection point that is on the support is blocked. 88 """ 89 vector = mgeo.VectorArray([ray.vector for ray in RayList]) 90 point = mgeo.PointArray([ray.point for ray in RayList]) 91 t = -point[:, 2] / vector[:, 2] 92 I = mgeo.PointArray(point + vector * t[:, np.newaxis]) 93 return I, [not(i in self.support) for i in I] 94 95 def get_local_normal(self, Point): 96 """Return normal unit vector in point 'Point' on the mask.""" 97 return np.array([0, 0, 1]) 98 99 def _zfunc(self, PointArray): 100 """Return the z-values of the plane surface at the points in PointArray.""" 101 return np.zeros(len(PointArray)) 102 103 def propagate_raylist(self, RayList, alignment=False): 104 """ 105 Propagate a list of rays through the mask, returning the transmitted rays. 106 107 Parameters 108 ---------- 109 RayList : list 110 List of Ray objects to be propagated through the mask. 111 alignment : bool, optional 112 If True, the alignment of the mask is considered. Default is False. 113 114 Returns 115 ------- 116 list 117 List of transmitted Ray objects. 118 """ 119 transmitted_rays = [] 120 local_rays = [ray.to_basis(*self.basis) for ray in RayList] 121 intersection_points, OK = self._get_intersections(local_rays) 122 N = len(RayList) 123 for i in range(N): 124 # Then we find the intersection point in the mirror reference frame 125 intersection_point = intersection_points[i] 126 local_ray = local_rays[i] 127 if OK[i]: 128 local_transmittedray = copy(local_ray) 129 local_transmittedray.point = intersection_point 130 local_transmittedray.vector = local_ray.vector 131 local_transmittedray.incidence = mgeo.AngleBetweenTwoVectors(-local_ray.vector, self.normal_ref) 132 local_transmittedray.path = local_ray.path + (np.linalg.norm(intersection_point - local_ray.point),) 133 transmitted_rays.append(local_transmittedray.from_basis(*self.basis)) 134 if len(transmitted_rays) == 0: 135 logger.warning("No rays were transmitted by the mask.") 136 logger.debug(f"Mask: {self}") 137 logger.debug(f"First ray: {RayList[0]}") 138 logger.debug(f"First ray in mask reference frame: {RayList[0].to_basis(self.r0, self.r, self.q)}") 139 logger.debug(f"First ray intersection point: {self._get_intersection(RayList[0].to_basis(self.r0, self.r, self.q))}") 140 return mray.RayList.from_list(transmitted_rays) 141 142 143# %%############################################################################ 144def _TransmitMaskRay(Mask, PointMask, Ray): 145 """Returns the transmitted ray""" 146 PointRay = Ray.point 147 VectorRay = Ray.vector 148 NormalMask = Mask.get_local_normal(PointMask) 149 150 AngleIncidence = mgeo.AngleBetweenTwoVectors(VectorRay, NormalMask) 151 Path = Ray.path + (np.linalg.norm(PointMask - PointRay),) 152 153 RayTransmitted = Ray.copy_ray() 154 RayTransmitted.point = PointMask 155 RayTransmitted.vector = VectorRay 156 RayTransmitted.incidence = AngleIncidence 157 RayTransmitted.path = Path 158 159 return RayTransmitted 160 161 162# %% 163def TransmitMaskRayList(Mask, RayList): 164 """ 165 Returns the the transmitted rays that pass the mask for the list of 166 incident rays ListRay. 167 168 Rays that hit the support are not further propagated. 169 170 Updates the reflected rays' incidence angle and path. 171 172 Parameters 173 ---------- 174 Mask : Mask-object 175 176 ListRay : list[Ray-object] 177 178 """ 179 ListRayTransmitted = [] 180 for Ray in RayList: 181 PointMask = Mask._get_intersection(Ray) 182 183 if PointMask is not None: 184 RayTransmitted = _TransmitMaskRay(Mask, PointMask, Ray) 185 ListRayTransmitted.append(RayTransmitted) 186 187 return ListRayTransmitted
30class Mask(moe.OpticalElement): 31 """ 32 A mask: a plane surface of the shape of its [Support](ModuleSupport.html), which stops all rays that hit it, 33 while all other rays continue their path unchanged. No diffraction effects. 34 35 Attributes 36 ---------- 37 support : [Support](ModuleSupport.html)-object 38 39 type : str 'Mask'. 40 41 Methods 42 ---------- 43 get_local_normal(Point) 44 45 get_centre() 46 47 get_grid3D(NbPoints) 48 49 """ 50 51 def __init__(self, Support, **kwargs): 52 """ 53 Parameters 54 ---------- 55 Support : [Support](ModuleSupport.html)-object 56 """ 57 self.type = "Mask" 58 self.support = Support 59 self.curvature = mmirror.Curvature.FLAT 60 61 self.r0 = mgeo.Point([0.0, 0.0, 0.0]) 62 self._r = mgeo.Vector([0.0, 0.0, 0.0]) 63 self._q = np.quaternion(1) 64 65 self.centre_ref = mgeo.Point([0, 0, 0]) 66 67 self.support_normal_ref = mgeo.Vector([0, 0, 1]) 68 self.normal_ref = mgeo.Vector([0, 0, 1]) 69 self.majoraxis_ref = mgeo.Vector([1, 0, 0]) 70 71 self.add_global_vectors("support_normal", "normal", "majoraxis") 72 self.add_global_points("centre") 73 74 super().__init__() 75 76 def _get_intersection(self, Ray): 77 """ 78 Return the intersection point between Ray and the xy-plane. 79 If not in alignment mode, any intersection point that is on the support is blocked. 80 """ 81 t = -Ray.point[2] / Ray.vector[2] 82 I = mgeo.Point(Ray.point +Ray.vector * t) 83 return I, I in self.support 84 85 def _get_intersections(self, RayList): 86 """ 87 Return the intersection point between Ray and the xy-plane. 88 If not in alignment mode, any intersection point that is on the support is blocked. 89 """ 90 vector = mgeo.VectorArray([ray.vector for ray in RayList]) 91 point = mgeo.PointArray([ray.point for ray in RayList]) 92 t = -point[:, 2] / vector[:, 2] 93 I = mgeo.PointArray(point + vector * t[:, np.newaxis]) 94 return I, [not(i in self.support) for i in I] 95 96 def get_local_normal(self, Point): 97 """Return normal unit vector in point 'Point' on the mask.""" 98 return np.array([0, 0, 1]) 99 100 def _zfunc(self, PointArray): 101 """Return the z-values of the plane surface at the points in PointArray.""" 102 return np.zeros(len(PointArray)) 103 104 def propagate_raylist(self, RayList, alignment=False): 105 """ 106 Propagate a list of rays through the mask, returning the transmitted rays. 107 108 Parameters 109 ---------- 110 RayList : list 111 List of Ray objects to be propagated through the mask. 112 alignment : bool, optional 113 If True, the alignment of the mask is considered. Default is False. 114 115 Returns 116 ------- 117 list 118 List of transmitted Ray objects. 119 """ 120 transmitted_rays = [] 121 local_rays = [ray.to_basis(*self.basis) for ray in RayList] 122 intersection_points, OK = self._get_intersections(local_rays) 123 N = len(RayList) 124 for i in range(N): 125 # Then we find the intersection point in the mirror reference frame 126 intersection_point = intersection_points[i] 127 local_ray = local_rays[i] 128 if OK[i]: 129 local_transmittedray = copy(local_ray) 130 local_transmittedray.point = intersection_point 131 local_transmittedray.vector = local_ray.vector 132 local_transmittedray.incidence = mgeo.AngleBetweenTwoVectors(-local_ray.vector, self.normal_ref) 133 local_transmittedray.path = local_ray.path + (np.linalg.norm(intersection_point - local_ray.point),) 134 transmitted_rays.append(local_transmittedray.from_basis(*self.basis)) 135 if len(transmitted_rays) == 0: 136 logger.warning("No rays were transmitted by the mask.") 137 logger.debug(f"Mask: {self}") 138 logger.debug(f"First ray: {RayList[0]}") 139 logger.debug(f"First ray in mask reference frame: {RayList[0].to_basis(self.r0, self.r, self.q)}") 140 logger.debug(f"First ray intersection point: {self._get_intersection(RayList[0].to_basis(self.r0, self.r, self.q))}") 141 return mray.RayList.from_list(transmitted_rays)
A mask: a plane surface of the shape of its Support, which stops all rays that hit it, while all other rays continue their path unchanged. No diffraction effects.
support : [Support](ModuleSupport.html)-object
type : str 'Mask'.
get_local_normal(Point)
get_centre()
get_grid3D(NbPoints)
51 def __init__(self, Support, **kwargs): 52 """ 53 Parameters 54 ---------- 55 Support : [Support](ModuleSupport.html)-object 56 """ 57 self.type = "Mask" 58 self.support = Support 59 self.curvature = mmirror.Curvature.FLAT 60 61 self.r0 = mgeo.Point([0.0, 0.0, 0.0]) 62 self._r = mgeo.Vector([0.0, 0.0, 0.0]) 63 self._q = np.quaternion(1) 64 65 self.centre_ref = mgeo.Point([0, 0, 0]) 66 67 self.support_normal_ref = mgeo.Vector([0, 0, 1]) 68 self.normal_ref = mgeo.Vector([0, 0, 1]) 69 self.majoraxis_ref = mgeo.Vector([1, 0, 0]) 70 71 self.add_global_vectors("support_normal", "normal", "majoraxis") 72 self.add_global_points("centre") 73 74 super().__init__()
Support : [Support](ModuleSupport.html)-object
96 def get_local_normal(self, Point): 97 """Return normal unit vector in point 'Point' on the mask.""" 98 return np.array([0, 0, 1])
Return normal unit vector in point 'Point' on the mask.
104 def propagate_raylist(self, RayList, alignment=False): 105 """ 106 Propagate a list of rays through the mask, returning the transmitted rays. 107 108 Parameters 109 ---------- 110 RayList : list 111 List of Ray objects to be propagated through the mask. 112 alignment : bool, optional 113 If True, the alignment of the mask is considered. Default is False. 114 115 Returns 116 ------- 117 list 118 List of transmitted Ray objects. 119 """ 120 transmitted_rays = [] 121 local_rays = [ray.to_basis(*self.basis) for ray in RayList] 122 intersection_points, OK = self._get_intersections(local_rays) 123 N = len(RayList) 124 for i in range(N): 125 # Then we find the intersection point in the mirror reference frame 126 intersection_point = intersection_points[i] 127 local_ray = local_rays[i] 128 if OK[i]: 129 local_transmittedray = copy(local_ray) 130 local_transmittedray.point = intersection_point 131 local_transmittedray.vector = local_ray.vector 132 local_transmittedray.incidence = mgeo.AngleBetweenTwoVectors(-local_ray.vector, self.normal_ref) 133 local_transmittedray.path = local_ray.path + (np.linalg.norm(intersection_point - local_ray.point),) 134 transmitted_rays.append(local_transmittedray.from_basis(*self.basis)) 135 if len(transmitted_rays) == 0: 136 logger.warning("No rays were transmitted by the mask.") 137 logger.debug(f"Mask: {self}") 138 logger.debug(f"First ray: {RayList[0]}") 139 logger.debug(f"First ray in mask reference frame: {RayList[0].to_basis(self.r0, self.r, self.q)}") 140 logger.debug(f"First ray intersection point: {self._get_intersection(RayList[0].to_basis(self.r0, self.r, self.q))}") 141 return mray.RayList.from_list(transmitted_rays)
Propagate a list of rays through the mask, returning the transmitted rays.
RayList : list List of Ray objects to be propagated through the mask. alignment : bool, optional If True, the alignment of the mask is considered. Default is False.
list List of transmitted Ray objects.
234def _RenderMirror(Mirror, Npoints=1000, draw_support = False, draw_points = False, draw_vectors = True , recenter_support = True): 235 """ 236 This function renders a mirror in 3D. 237 """ 238 mesh = Mirror._Render(Npoints) 239 p = pv.Plotter() 240 p.add_mesh(mesh) 241 if draw_support: 242 support = Mirror.support._Render() 243 if recenter_support: 244 support.translate(Mirror.r0,inplace=True) 245 p.add_mesh(support, color="gray", opacity=0.5) 246 247 if draw_vectors: 248 # We draw the important vectors of the optical element 249 # For that, if we have a "vectors" attribute, we use that 250 # (a dictionary with the vector names as keys and the colors as values) 251 # Otherwise we use the default vectors: "support_normal", "majoraxis" 252 if hasattr(Mirror, "vectors"): 253 vectors = Mirror.vectors 254 else: 255 vectors = {"support_normal_ref": "red", "majoraxis_ref": "blue"} 256 for vector, color in vectors.items(): 257 if hasattr(Mirror, vector): 258 p.add_arrows(Mirror.r0, 10*getattr(Mirror, vector), color=color) 259 260 if draw_points: 261 # We draw the important points of the optical element 262 # For that, if we have a "points" attribute, we use that 263 # (a dictionary with the point names as keys and the colors as values) 264 # Otherwise we use the default points: "centre_ref" 265 if hasattr(Mirror, "points"): 266 points = Mirror.points 267 else: 268 points = {"centre_ref": "red"} 269 for point, color in points.items(): 270 if hasattr(Mirror, point): 271 p.add_mesh(pv.Sphere(radius=1, center=getattr(Mirror, point)), color=color) 272 else: 273 p.add_mesh(pv.Sphere(radius=1, center=Mirror.r0), color="red") 274 p.show() 275 return p
This function renders a mirror in 3D.
164def TransmitMaskRayList(Mask, RayList): 165 """ 166 Returns the the transmitted rays that pass the mask for the list of 167 incident rays ListRay. 168 169 Rays that hit the support are not further propagated. 170 171 Updates the reflected rays' incidence angle and path. 172 173 Parameters 174 ---------- 175 Mask : Mask-object 176 177 ListRay : list[Ray-object] 178 179 """ 180 ListRayTransmitted = [] 181 for Ray in RayList: 182 PointMask = Mask._get_intersection(Ray) 183 184 if PointMask is not None: 185 RayTransmitted = _TransmitMaskRay(Mask, PointMask, Ray) 186 ListRayTransmitted.append(RayTransmitted) 187 188 return ListRayTransmitted
Returns the the transmitted rays that pass the mask for the list of incident rays ListRay.
Rays that hit the support are not further propagated.
Updates the reflected rays' incidence angle and path.
Mask : Mask-object
ListRay : list[Ray-object]
Provides classes for different mirror surfaces, which are types of optics.
Think of these as the z-coordinates on top of the x-y-grid provided by the ART.ModuleSupport.Support.
Also provides the function ReflectionMirrorRayList that returns the rays reflected on the mirror. Rays that do not hit the support are not propagated any further.
Created in 2019
@author: Anthony Guillaume and Stefan Haessler and Andre Kalouguine and Charles Bourassin-Bouchet
1""" 2Provides classes for different mirror surfaces, which are types of optics. 3 4Think of these as the z-coordinates on top of the x-y-grid provided by the ART.ModuleSupport.Support. 5 6Also provides the function *ReflectionMirrorRayList* that returns the rays reflected on 7the mirror. Rays that do not hit the support are not propagated any further. 8 9 10 11 12 13Created in 2019 14 15@author: Anthony Guillaume and Stefan Haessler and Andre Kalouguine and Charles Bourassin-Bouchet 16""" 17# %% Modules 18import ARTcore.ModuleGeometry as mgeo 19from ARTcore.ModuleGeometry import Point, Vector, Origin 20import ARTcore.ModuleSupport as msup 21import ARTcore.ModuleOpticalRay as mray 22import ARTcore.ModuleOpticalElement as moe 23from ARTcore.DepGraphDefinitions import OAP_calculator, EllipsoidalMirrorCalculator 24import ARTcore.ModuleSurface as msurf 25 26import numpy as np 27from enum import Enum 28import math 29from abc import ABC, abstractmethod 30from copy import copy 31import logging 32import time 33 34logger = logging.getLogger(__name__) 35 36# %% Generic mirror class definition 37 38class Curvature(Enum): 39 CONVEX = -1 40 FLAT = 0 41 CONCAVE = 1 42 43class Mirror(moe.OpticalElement): 44 """Abstract base class for mirrors.""" 45 vectorised = False 46 def __init__(self): 47 self.Surface = msurf.IdealSurface() 48 super().__init__() 49 50 @abstractmethod 51 def _get_intersection(self, Ray): 52 """ 53 This method should return the intersection point between the ray and the mirror surface. 54 The calculation should be done in the reference frame of the mirror. 55 It essentially defines the mirror surface (but not its normal). 56 """ 57 pass 58 59 @abstractmethod 60 def get_local_normal(self, Point: np.ndarray): 61 """ 62 This method should return the normal unit vector in point 'Point' on the mirror surface. 63 The normal is in the reference frame of the mirror. 64 """ 65 pass 66 67 def propagate_raylist(self, RayList, alignment=False): 68 """ 69 Propagate a list of rays to the mirror and return the reflected rays. 70 71 Parameters 72 ---------- 73 RayList : a RayList (a class defined in ARTcore.ModuleOpticalRay) 74 Rays to propagate through the mirror. 75 76 alignment : bool 77 If True, the rays are not blocked by the support. Not implemented yet 78 79 Returns 80 ------- 81 ReflectedRayList : a RayList 82 The rays that were reflected by the mirror. 83 """ 84 #local_rays = [ray.to_basis(*self.basis) for ray in RayList] 85 local_rays = RayList.to_basis(*self.basis) 86 # So local_rays is a list of rays in the mirror reference frame 87 if self.vectorised and len(local_rays) > 10: 88 # If the class implements the vectorised version of the intersection and normal calculation 89 intersection_points, OK = self._get_intersections(local_rays) 90 # NB: the _get_intersections method should return two things: 91 # - the intersection points in the mirror reference frame with nan's for rays that do not intersect 92 # - a boolean array indicating which rays intersections make sense 93 local_normals = mgeo.VectorArray(np.zeros_like(intersection_points)) 94 local_normals[OK] = self.get_local_normals(intersection_points[OK]) 95 local_reflected_rays = [ 96 self.Surface.reflect_ray(local_rays[i], intersection_points[i], local_normals[i]) 97 for i in range(len(local_rays)) if OK[i]] 98 else: 99 intersection_points, OK = zip(*[self._get_intersection(r) for r in local_rays]) 100 # OK is a boolean list indicating if the ray intersects the mirror (for instance is in the support) 101 local_reflected_rays = [ 102 self.Surface.reflect_ray(local_rays[i], intersection_points[i], self.get_local_normal(intersection_points[i])) 103 for i in range(len(local_rays)) if OK[i]] 104 reflected_rays = mray.RayList.from_list(local_reflected_rays).from_basis(*self.basis) 105 if alignment and len(reflected_rays) == 0: 106 logger.error("Unexpected error occurred: no rays were reflected during alignment procedure.") 107 logger.error("This should not happen.") 108 logger.error(f"Mirror on which error ocurred: {self}") 109 logger.error(f"Alignment ray: {RayList[0]}") 110 logger.error(f"Alignment ray in mirror reference frame: {local_rays[0]}") 111 logger.error(f"Intersection point: {self._get_intersection(local_rays[0])[0]}") 112 logger.error(f"Support: {self.support}") 113 elif len(reflected_rays) == 0: 114 logger.warning("No rays were reflected by the mirror.") 115 logger.debug(f"Mirror: {self}") 116 logger.debug(f"First ray: {RayList[0]}") 117 logger.debug(f"First ray in mirror reference frame: {RayList[0].to_basis(self.r0, self.r, self.q)}") 118 logger.debug(f"First ray intersection point: {self._get_intersection(RayList[0].to_basis(self.r0, self.r, self.q))}") 119 return reflected_rays 120 121 122def _IntersectionRayMirror(PointMirror, ListPointIntersectionMirror): 123 """When the ray has pierced the mirror twice, select the first point, otherwise just keep that one.""" 124 if len(ListPointIntersectionMirror) == 2: 125 return mgeo.ClosestPoint( 126 PointMirror, 127 ListPointIntersectionMirror[0], 128 ListPointIntersectionMirror[1], 129 ) 130 elif len(ListPointIntersectionMirror) == 1: 131 return ListPointIntersectionMirror[0] 132 else: 133 return None 134 135 136# %% Plane mirror definitions 137class MirrorPlane(Mirror): 138 """ 139 A plane mirror. 140 141 Attributes 142 ---------- 143 support : ART.ModuleSupport.Support 144 Mirror support 145 type : str = 'Plane Mirror' 146 Human readable mirror type 147 Methods 148 ------- 149 MirrorPlane.get_normal(Point) 150 151 MirrorPlane.get_centre() 152 153 MirrorPlane.get_grid3D(NbPoints) 154 """ 155 vectorised = True 156 def __init__(self, Support, **kwargs): 157 """ 158 Create a plane mirror on a given Support. 159 160 Parameters 161 ---------- 162 Support : ART.ModuleSupport.Support 163 164 """ 165 super().__init__() 166 self.support = Support 167 self.type = "Plane Mirror" 168 self.curvature = Curvature.FLAT 169 170 if "Surface" in kwargs: 171 self.Surface = kwargs["Surface"] 172 173 self.r0 = Point([0.0, 0.0, 0.0]) 174 175 self.centre_ref = Point([0.0, 0.0, 0.0]) 176 self.support_normal_ref = Vector([0, 0, 1.0]) 177 self.majoraxis_ref = Vector([1.0, 0, 0]) 178 179 self.add_global_points("centre") 180 self.add_global_vectors("support_normal", "majoraxis") 181 182 def _get_intersection(self, Ray): 183 """Return the intersection point between Ray and the xy-plane.""" 184 t = -Ray.point[2] / Ray.vector[2] 185 PointIntersection = Ray.point + Ray.vector * t 186 return PointIntersection, t > 0 and PointIntersection in self.support 187 188 def get_local_normal(self, Point: np.ndarray): 189 """Return normal unit vector in point 'Point' on the plane mirror.""" 190 return Vector([0, 0, 1]) 191 192 def _get_intersections(self, RayList): 193 """ 194 Vectorised version of the intersection calculation. 195 """ 196 vectors = np.array([ray.vector for ray in RayList]) 197 points = np.array([ray.point for ray in RayList]) 198 199 t = -points[:,2] / vectors[:,2] 200 Points = points + vectors * t[:,np.newaxis] 201 OK = (t > 0) & np.array([p in self.support for p in Points]) 202 return mgeo.PointArray(Points), OK 203 204 def get_local_normals(self, Points): 205 return mgeo.VectorArray(np.zeros_like(Points) + [0, 0, 1]) 206 207 def _zfunc(self, PointArray): 208 x = PointArray[:,0] 209 y = PointArray[:,1] 210 return np.zeros_like(x) 211 212# %% Spherical mirror definitions 213class MirrorSpherical(Mirror): 214 """ 215 Spherical mirror surface with eqn. $x^2 + y^2 + z^2 = R^2$, where $R$ is the radius. 216 217  218 219 Attributes 220 ---------- 221 radius : float 222 Radius of curvature. A postive value for concave mirror, a negative value for a convex mirror. 223 224 support : ART.ModuleSupport.Support 225 226 type : str SphericalCC Mirror' or SphericalCX Mirror' 227 228 Methods 229 ------- 230 MirrorSpherical.get_normal(Point) 231 232 MirrorSpherical.get_centre() 233 234 MirrorSpherical.get_grid3D(NbPoints) 235 236 """ 237 def __init__(self, Support, Radius, **kwargs): 238 """ 239 Construct a spherical mirror. 240 241 Parameters 242 ---------- 243 Radius : float 244 The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror. 245 246 Support : ART.ModuleSupport.Support 247 248 """ 249 super().__init__() 250 if Radius < 0: 251 self.type = "SphericalCX Mirror" 252 self.curvature = Curvature.CONVEX 253 self.radius = -Radius 254 elif Radius > 0: 255 self.type = "SphericalCC Mirror" 256 self.curvature = Curvature.CONCAVE 257 self.radius = Radius 258 else: 259 raise ValueError("Radius of curvature must be non-zero") 260 self.support = Support 261 262 if "Surface" in kwargs: 263 self.Surface = kwargs["Surface"] 264 265 self.r0 = Point([0.0, 0, -self.radius]) 266 267 self.centre_ref = Point([0.0, 0, -self.radius]) 268 self.sphere_center_ref = Point([0.0, 0, 0]) 269 self.focus_ref = Point([0.0, 0, -self.radius/2]) 270 271 self.support_normal_ref = Vector([0, 0, 1.0]) 272 self.majoraxis_ref = Vector([1.0, 0, 0]) 273 self.towards_focus_ref = (self.focus_ref - self.centre_ref).normalized() 274 275 self.add_global_points("sphere_center", "focus", "centre") 276 self.add_global_vectors("towards_focus", "majoraxis", "support_normal") 277 278 def _get_intersection(self, Ray): 279 """Return the intersection point between the ray and the sphere.""" 280 a = np.dot(Ray.vector, Ray.vector) 281 b = 2 * np.dot(Ray.vector, Ray.point) 282 c = np.dot(Ray.point, Ray.point) - self.radius**2 283 284 Solution = mgeo.SolverQuadratic(a, b, c) 285 Solution = mgeo.KeepPositiveSolution(Solution) 286 287 for t in Solution: 288 Intersect = Ray.vector * t + Ray.point 289 if Intersect - self.r0 in self.support: 290 return Intersect, True 291 292 return Ray.point, False 293 294 def get_local_normal(self, Point): 295 """Return the normal unit vector on the spherical surface at point Point.""" 296 return -Vector(Point).normalized() 297 298 def _zfunc(self, PointArray): 299 x = PointArray[:,0] 300 y = PointArray[:,1] 301 return -np.sqrt(self.radius**2 - x**2 - y**2) 302 303# %% Parabolic mirror definitions 304class MirrorParabolic(Mirror): 305 r""" 306 A paraboloid with vertex at the origin $O=[0,0,0]$ and symmetry axis z: 307 $z = \frac{1}{4f}[x^2 + y^2]$ where $f$ is the focal lenght of the *mother* 308 parabola (i.e. measured from its center at $O$ to the focal point $F$). 309 310 The center of the support is shifted along the x-direction by the off-axis distance $x_c$. 311 This leads to an *effective focal length* $f_\mathrm{eff}$, measured from the shifted center 312 of the support $P$ to the focal point $F$. 313 It is related to the mother focal length by $f = f_\\mathrm{eff} \cos^2(\alpha/2) $, 314 or equivalently $ p = 2f = f_\mathrm{eff} (1+\\cos\\alpha)$, where $\\alpha$ 315 is the off-axis angle, and $p = 2f$ is called the semi latus rectum. 316 317 Another useful relationship is that between the off-axis distance and the resulting 318 off-axis angle: $x_c = 2 f \tan(\alpha/2)$. 319 320 321  322 323 Attributes 324 ---------- 325 offaxisangle : float 326 Off-axis angle of the parabola. Modifying this also updates p, keeping feff constant. 327 Attention: The off-axis angle must be *given in degrees*, but is stored and will be *returned in radian* ! 328 329 feff : float 330 Effective focal length of the parabola in mm. Modifying this also updates p, keeping offaxisangle constant. 331 332 p : float 333 Semi latus rectum of the parabola in mm. Modifying this also updates feff, keeping offaxisangle constant. 334 335 support : ART.ModuleSupport.Support 336 337 type : str 'Parabolic Mirror'. 338 339 Methods 340 ------- 341 MirrorParabolic.get_normal(Point) 342 343 MirrorParabolic.get_centre() 344 345 MirrorParabolic.get_grid3D(NbPoints) 346 347 """ 348 #vectorised = True # Unitl I fix the closest solution thing 349 def __init__(self, Support, 350 FocalEffective: float=None, 351 OffAxisAngle: float = None, 352 FocalParent: float = None, 353 RadiusParent: float = None, 354 OffAxisDistance: float = None, 355 MoreThan90: bool = None, 356 **kwargs): 357 """ 358 Initialise a Parabolic mirror. 359 360 Parameters 361 ---------- 362 FocalEffective : float 363 Effective focal length of the parabola in mm. 364 365 OffAxisAngle : float 366 Off-axis angle *in degrees* of the parabola. 367 368 Support : ART.ModuleSupport.Support 369 370 """ 371 super().__init__() 372 self.curvature = Curvature.CONCAVE 373 self.support = Support 374 self.type = "Parabolic Mirror" 375 376 if "Surface" in kwargs: 377 self.Surface = kwargs["Surface"] 378 379 parameter_calculator = OAP_calculator() 380 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 381 fs=FocalEffective, 382 theta=OffAxisAngle, 383 fp=FocalParent, 384 Rc=RadiusParent, 385 OAD=OffAxisDistance, 386 more_than_90=MoreThan90) 387 self._offaxisangle = values["theta"] 388 self._feff = values["fs"] 389 self._p = values["p"] 390 self._offaxisdistance = values["OAD"] 391 self._fparent = values["fp"] 392 self._rparent = values["Rc"] 393 394 self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 395 396 self.centre_ref = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 397 self.support_normal_ref = Vector([0, 0, 1.0]) 398 self.majoraxis_ref = Vector([1.0, 0, 0]) 399 400 self.focus_ref = Point([0.0, 0, self._fparent]) 401 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 402 self.towards_collimated_ref = Vector([0, 0, 1.0]) 403 404 self.add_global_points("focus", "centre") 405 self.add_global_vectors("towards_focusing", "towards_collimated", "support_normal", "majoraxis") 406 407 def _get_intersection(self, Ray): 408 """Return the intersection point between the ray and the parabola.""" 409 ux, uy, uz = Ray.vector 410 xA, yA, zA = Ray.point 411 412 da = ux**2 + uy**2 413 db = 2 * (ux * xA + uy * yA) - 2 * self._p * uz 414 dc = xA**2 + yA**2 - 2 * self._p * zA 415 416 Solution = mgeo.SolverQuadratic(da, db, dc) 417 Solution = mgeo.KeepPositiveSolution(Solution) 418 IntersectionPoint = [Ray.point + Ray.vector * t for t in Solution if t > 0] 419 distances = [mgeo.Vector(i - self.r0).norm for i in IntersectionPoint] 420 IntersectionPoint = IntersectionPoint[distances.index(min(distances))] 421 return IntersectionPoint, IntersectionPoint-self.r0 in self.support 422 423 def _get_intersections(self, RayList): 424 """ 425 Vectorised version of the intersection calculation. 426 """ 427 vectors = np.array([ray.vector for ray in RayList]) 428 points = np.array([ray.point for ray in RayList]) 429 Solutions = np.zeros(len(RayList), dtype=float) 430 431 da = np.sum(vectors[:,0:2]**2, axis=1) 432 db = 2 * (np.sum(vectors[:,0:2] * points[:,0:2], axis=1) - self._p * vectors[:,2]) 433 dc = np.sum(points[:,0:2]**2, axis=1) - 2 * self._p * points[:,2] 434 435 linear = da == 0 436 Solutions[linear] = -dc[linear] / db[linear] 437 438 centered = (dc==0) & (da!=0) # If equation is of form ax**2 + bx = 0 439 Solutions[centered] = np.maximum(-db[centered] / da[centered], 0) 440 441 unstable = (4*da*dc) < db**2 * 1e-10 # Cases leading to catastrophic cancellation 442 443 Deltas = db**2 - 4 * da * dc 444 OK = Deltas >= 0 445 SolutionsPlus = (-db + np.sqrt(Deltas)) / 2 / da 446 SolutionsMinus = (-db - np.sqrt(Deltas)) / 2 / da 447 SolutionsPlus[unstable] = dc[unstable] / (da[unstable] * SolutionsMinus[unstable]) 448 SolutionsPlus = np.where(SolutionsPlus >= 0, SolutionsPlus, np.inf) 449 SolutionsMinus = np.where(SolutionsMinus >= 0, SolutionsMinus, np.inf) 450 Solutions = np.minimum(SolutionsPlus, SolutionsMinus) 451 Solutions = np.maximum(Solutions, 0) 452 Points = points + vectors * Solutions[:,np.newaxis] 453 OK = OK & (Solutions > 0) & np.array([p-self.r0 in self.support for p in Points]) 454 return mgeo.PointArray(Points), OK 455 456 def get_local_normal(self, Point): 457 """Return the normal unit vector on the paraboloid surface at point Point.""" 458 Gradient = Vector(np.zeros(3)) 459 Gradient[0] = -Point[0] 460 Gradient[1] = -Point[1] 461 Gradient[2] = self._p 462 return Gradient.normalized() 463 464 def get_local_normals(self, Points): 465 """ 466 Vectorised version of the normal calculation. 467 """ 468 Gradients = mgeo.VectorArray(np.zeros_like(Points)) 469 Gradients[:,0] = -Points[:,0] 470 Gradients[:,1] = -Points[:,1] 471 Gradients[:,2] = self._p 472 return Gradients.normalized() 473 474 def _zfunc(self, PointArray): 475 x = PointArray[:,0] 476 y = PointArray[:,1] 477 return (x**2 + y**2) / 2 / self._p 478 479 480# %% Toroidal mirror definitions 481class MirrorToroidal(Mirror): 482 r""" 483 Toroidal mirror surface with eqn.$(\sqrt{x^2+z^2}-R)^2 + y^2 = r^2$ where $R$ and $r$ the major and minor radii. 484 485  486 487 Attributes 488 ---------- 489 majorradius : float 490 Major radius of the toroid in mm. 491 492 minorradius : float 493 Minor radius of the toroid in mm. 494 495 support : ART.ModuleSupport.Support 496 497 type : str 'Toroidal Mirror'. 498 499 Methods 500 ------- 501 MirrorToroidal.get_normal(Point) 502 503 MirrorToroidal.get_centre() 504 505 MirrorToroidal.get_grid3D(NbPoints) 506 507 """ 508 509 def __init__(self, Support, MajorRadius, MinorRadius, **kwargs): 510 """ 511 Construct a toroidal mirror. 512 513 Parameters 514 ---------- 515 MajorRadius : float 516 Major radius of the toroid in mm. 517 518 MinorRadius : float 519 Minor radius of the toroid in mm. 520 521 Support : ART.ModuleSupport.Support 522 523 """ 524 super().__init__() 525 self.support = Support 526 self.type = "Toroidal Mirror" 527 528 if "Surface" in kwargs: 529 self.Surface = kwargs["Surface"] 530 531 self.curvature = Curvature.CONCAVE 532 533 self.majorradius = MajorRadius 534 self.minorradius = MinorRadius 535 536 self.r0 = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 537 538 self.centre_ref = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 539 self.support_normal_ref = Vector([0, 0, 1.0]) 540 self.majoraxis_ref = Vector([1.0, 0, 0]) 541 542 self.add_global_vectors("support_normal", "majoraxis") 543 self.add_global_points("centre") 544 545 def _get_intersection(self, Ray): 546 """Return the intersection point between Ray and the toroidal mirror surface. The intersection points are given in the reference frame of the mirror""" 547 ux = Ray.vector[0] 548 uz = Ray.vector[2] 549 xA = Ray.point[0] 550 zA = Ray.point[2] 551 552 G = 4.0 * self.majorradius**2 * (ux**2 + uz**2) 553 H = 8.0 * self.majorradius**2 * (ux * xA + uz * zA) 554 I = 4.0 * self.majorradius**2 * (xA**2 + zA**2) 555 J = np.dot(Ray.vector, Ray.vector) 556 K = 2.0 * np.dot(Ray.vector, Ray.point) 557 L = ( 558 np.dot(Ray.point, Ray.point) 559 + self.majorradius**2 560 - self.minorradius**2 561 ) 562 563 a = J**2 564 b = 2 * J * K 565 c = 2 * J * L + K**2 - G 566 d = 2 * K * L - H 567 e = L**2 - I 568 569 Solution = mgeo.SolverQuartic(a, b, c, d, e) 570 if len(Solution) == 0: 571 return None, False 572 Solution = [t for t in Solution if t > 0] 573 IntersectionPoint = [Ray.point + Ray.vector *i for i in Solution] 574 distances = [mgeo.Vector(i - self.r0).norm for i in IntersectionPoint] 575 IntersectionPoint = IntersectionPoint[distances.index(min(distances))] 576 OK = IntersectionPoint - self.r0 in self.support and np.abs(IntersectionPoint[2]-self.r0[2]) < self.majorradius 577 return IntersectionPoint, OK 578 579 def get_local_normal(self, Point): 580 """Return the normal unit vector on the toroidal surface at point Point in the reference frame of the mirror""" 581 x = Point[0] 582 y = Point[1] 583 z = Point[2] 584 A = self.majorradius**2 - self.minorradius**2 585 586 Gradient = Vector(np.zeros(3)) 587 Gradient[0] = ( 588 4 * (x**3 + x * y**2 + x * z**2 + x * A) 589 - 8 * x * self.majorradius**2 590 ) 591 Gradient[1] = 4 * (y**3 + y * x**2 + y * z**2 + y * A) 592 Gradient[2] = ( 593 4 * (z**3 + z * x**2 + z * y**2 + z * A) 594 - 8 * z * self.majorradius**2 595 ) 596 597 return -Gradient.normalized() 598 599 def _zfunc(self, PointArray): 600 x = PointArray[:,0] 601 y = PointArray[:,1] 602 return -np.sqrt( 603 (np.sqrt(self.minorradius**2 - y**2) + self.majorradius)**2 - x**2 604 ) 605 606def ReturnOptimalToroidalRadii( 607 Focal: float, AngleIncidence: float 608) -> (float, float): 609 """ 610 Get optimal parameters for a toroidal mirror. 611 612 Useful helper function to get the optimal major and minor radii for a toroidal mirror to achieve a 613 focal length 'Focal' with an angle of incidence 'AngleIncidence' and with vanishing astigmatism. 614 615 Parameters 616 ---------- 617 Focal : float 618 Focal length in mm. 619 620 AngleIncidence : int 621 Angle of incidence in degrees. 622 623 Returns 624 ------- 625 OptimalMajorRadius, OptimalMinorRadius : float, float. 626 """ 627 AngleIncidenceRadian = AngleIncidence * np.pi / 180 628 OptimalMajorRadius = ( 629 2 630 * Focal 631 * (1 / np.cos(AngleIncidenceRadian) - np.cos(AngleIncidenceRadian)) 632 ) 633 OptimalMinorRadius = 2 * Focal * np.cos(AngleIncidenceRadian) 634 return OptimalMajorRadius, OptimalMinorRadius 635 636 637# %% Ellipsoidal mirror definitions 638class MirrorEllipsoidal(Mirror): 639 """ 640 Ellipdoidal mirror surface with eqn. $(x/a)^2 + (y/b)^2 + (z/b)^2 = 1$, where $a$ and $b$ are semi major and semi minor axes. 641 642  643 644 Attributes 645 ---------- 646 a : float 647 Semi major axis of the ellipsoid in mm. 648 649 b : float 650 Semi minor axis of the ellipsoid in mm. 651 652 support : ART.ModuleSupport.Support 653 654 type : str 'Ellipsoidal Mirror'. 655 656 Methods 657 ------- 658 MirrorEllipsoidal.get_normal(Point) 659 660 MirrorEllipsoidal.get_centre() 661 662 MirrorEllipsoidal.get_grid3D(NbPoints) 663 664 """ 665 666 def __init__( 667 self, 668 Support, 669 SemiMajorAxis=None, 670 SemiMinorAxis=None, 671 OffAxisAngle=None, 672 f_object=None, 673 f_image=None, 674 IncidenceAngle=None, 675 Magnification=None, 676 DistanceObjectImage=None, 677 Eccentricity=None, 678 **kwargs, 679 ): 680 """ 681 Generate an ellipsoidal mirror with given parameters. 682 683 The angles are given in degrees but converted to radians for internal calculations. 684 You can specify the semi-major and semi-minor axes, the off-axis angle, the object and image focal distances or some other parameters. 685 The constructor uses a magic calculator to determine the missing parameters. 686 687 Parameters 688 ---------- 689 Support : TYPE 690 ART.ModuleSupport.Support. 691 SemiMajorAxis : float (optional) 692 Semi major axis of the ellipsoid in mm.. 693 SemiMinorAxis : float (optional) 694 Semi minor axis of the ellipsoid in mm.. 695 OffAxisAngle : float (optional) 696 Off-axis angle of the mirror in mm. Defined as the angle at the centre of the mirror between the two foci.. 697 f_object : float (optional) 698 Object focal distance in mm. 699 f_image : float (optional) 700 Image focal distance in mm. 701 IncidenceAngle : float (optional) 702 Angle of incidence in degrees. 703 Magnification : float (optional) 704 Magnification. 705 DistanceObjectImage : float (optional) 706 Distance between object and image in mm. 707 Eccentricity : float (optional) 708 Eccentricity of the ellipsoid. 709 """ 710 super().__init__() 711 self.type = "Ellipsoidal Mirror" 712 self.support = Support 713 714 if "Surface" in kwargs: 715 self.Surface = kwargs["Surface"] 716 717 parameter_calculator = EllipsoidalMirrorCalculator() 718 values, steps = parameter_calculator.calculate_values(verify_consistency=True, 719 f1=f_object, 720 f2=f_image, 721 a=SemiMajorAxis, 722 b=SemiMinorAxis, 723 offset_angle=OffAxisAngle, 724 incidence_angle=IncidenceAngle, 725 m=Magnification, 726 l=DistanceObjectImage, 727 e=Eccentricity) 728 self.a = values["a"] 729 self.b = values["b"] 730 self._offaxisangle = np.deg2rad(values["offset_angle"]) 731 self._f_object = values["f1"] 732 self._f_image = values["f2"] 733 734 self.centre_ref = self._get_centre_ref() 735 736 self.r0 = self.centre_ref 737 738 self.support_normal_ref = Vector([0, 0, 1.0]) 739 self.majoraxis_ref = Vector([1.0, 0, 0]) 740 741 self.f1_ref = Point([-self.a, 0,0]) 742 self.f2_ref = Point([ self.a, 0,0]) 743 self.towards_image_ref = (self.f2_ref - self.centre_ref).normalized() 744 self.towards_object_ref = (self.f1_ref - self.centre_ref).normalized() 745 self.centre_normal_ref = self.get_local_normal(self.centre_ref) 746 747 self.add_global_points("f1", "f2", "centre") 748 self.add_global_vectors("towards_image", "towards_object", "centre_normal", "support_normal", "majoraxis") 749 750 def _get_intersection(self, Ray): 751 """Return the intersection point between Ray and the ellipsoidal mirror surface.""" 752 ux, uy, uz = Ray.vector 753 xA, yA, zA = Ray.point 754 755 da = (uy**2 + uz**2) / self.b**2 + (ux / self.a) ** 2 756 db = 2 * ((uy * yA + uz * zA) / self.b**2 + (ux * xA) / self.a**2) 757 dc = (yA**2 + zA**2) / self.b**2 + (xA / self.a) ** 2 - 1 758 759 Solution = mgeo.SolverQuadratic(da, db, dc) 760 Solution = mgeo.KeepPositiveSolution(Solution) 761 762 ListPointIntersection = [] 763 C = self.get_centre_ref() 764 for t in Solution: 765 Intersect = Ray.vector * t + Ray.point 766 if Intersect[2] < 0 and Intersect - C in self.support: 767 ListPointIntersection.append(Intersect) 768 769 return _IntersectionRayMirror(Ray.point, ListPointIntersection) 770 771 def get_local_normal(self, Point): 772 """Return the normal unit vector on the ellipsoidal surface at point Point.""" 773 Gradient = Vector(np.zeros(3)) 774 775 Gradient[0] = -Point[0] / self.a**2 776 Gradient[1] = -Point[1] / self.b**2 777 Gradient[2] = -Point[2] / self.b**2 778 779 return Gradient.normalized() 780 781 def _get_centre_ref(self): 782 """Return 3D coordinates of the point on the mirror surface at the center of its support.""" 783 foci = 2 * np.sqrt(self.a**2 - self.b**2) # distance between the foci 784 h = -foci / 2 / np.tan(self._offaxisangle) 785 R = np.sqrt(foci**2 / 4 + h**2) 786 sign = 1 787 if math.isclose(self._offaxisangle, np.pi / 2): 788 h = 0 789 elif self._offaxisangle > np.pi / 2: 790 h = -h 791 sign = -1 792 a = 1 - self.a**2 / self.b**2 793 b = -2 * h 794 c = self.a**2 + h**2 - R**2 795 z = (-b + sign * np.sqrt(b**2 - 4 * a * c)) / (2 * a) 796 if math.isclose(z**2, self.b**2): 797 return np.array([0, 0, -self.b]) 798 x = self.a * np.sqrt(1 - z**2 / self.b**2) 799 centre = Point([x, 0, sign * z]) 800 return centre 801 802 def _zfunc(self, PointArray): 803 x = PointArray[:,0] 804 y = PointArray[:,1] 805 x-= self.r0[0] 806 y-= self.r0[1] 807 z = -np.sqrt(1 - (x / self.a)**2 - (y / self.b)**2) 808 z += self.r0[2] 809 return z 810 811 812# %% Cylindrical mirror definitions 813class MirrorCylindrical(Mirror): 814 """ 815 Cylindrical mirror surface with eqn. $y^2 + z^2 = R^2$, where $R$ is the radius. 816 817 Attributes 818 ---------- 819 radius : float 820 Radius of curvature. A postive value for concave mirror, a negative value for a convex mirror. 821 822 support : ART.ModuleSupport.Support 823 824 type : str 'Cylindrical Mirror'. 825 826 Methods 827 ------- 828 MirrorCylindrical.get_normal(Point) 829 830 MirrorCylindrical.get_centre() 831 832 MirrorCylindrical.get_grid3D(NbPoints) 833 """ 834 835 def __init__(self, Support, Radius, **kwargs): 836 """ 837 Construct a cylindrical mirror. 838 839 Parameters 840 ---------- 841 Radius : float 842 The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror. 843 844 Support : ART.ModuleSupport.Support 845 846 """ 847 super().__init__() 848 if Radius < 0: 849 self.type = "CylindricalCX Mirror" 850 self.curvature = Curvature.CONVEX 851 self.radius = -Radius 852 else: 853 self.type = "CylindricalCC Mirror" 854 self.curvature = Curvature.CONCAVE 855 self.radius = Radius 856 857 self.support = Support 858 859 if "Surface" in kwargs: 860 self.Surface = kwargs["Surface"] 861 862 self.r0 = Point([0.0, 0.0, -self.radius]) 863 864 self.support_normal_ref = Vector([0, 0, 1.0]) 865 self.majoraxis_ref = Vector([1.0, 0, 0]) 866 self.centre_ref = Point([0.0, 0.0, -self.radius]) 867 868 self.add_global_points("centre") 869 self.add_global_vectors("support_normal", "majoraxis") 870 871 def _get_intersection(self, Ray): 872 """Return the intersection point between the Ray and the cylinder.""" 873 uy = Ray.vector[1] 874 uz = Ray.vector[2] 875 yA = Ray.point[1] 876 zA = Ray.point[2] 877 878 a = uy**2 + uz**2 879 b = 2 * (uy * yA + uz * zA) 880 c = yA**2 + zA**2 - self.radius**2 881 882 Solution = mgeo.SolverQuadratic(a, b, c) 883 Solution = mgeo.KeepPositiveSolution(Solution) 884 885 ListPointIntersection = [] 886 for t in Solution: 887 Intersect = Ray.vector * t + Ray.point 888 if Intersect[2] < 0 and Intersect in self.support: 889 ListPointIntersection.append(Intersect) 890 891 return _IntersectionRayMirror(Ray.point, ListPointIntersection) 892 893 def get_local_normal(self, Point): 894 """Return the normal unit vector on the cylinder surface at point P.""" 895 Gradient = Vector([0, -Point[1], -Point[2]]) 896 return Gradient.normalized() 897 898 def get_centre(self): 899 """Return 3D coordinates of the point on the mirror surface at the center of its support.""" 900 return Point([0, 0, -self.radius]) 901 902 def _zfunc(self, PointArray): 903 y = PointArray[:,1] 904 return -np.sqrt(self.radius**2 - y**2) 905 906 907# %% Grazing parabolic mirror definitions 908# A grazing parabola is no different from a parabola, it's just a question of what we consider to be the support. 909# For an ordinary parabola, the support is the xy-plane, for a grazing parabola, the support is parallel to the surface at the optical center. 910 911class GrazingParabola(Mirror): 912 r""" 913 A parabolic mirror with a support parallel to the surface at the optical center. 914 Needs to be completed, currently not functional. 915 TODO 916 """ 917 918 def __init__(self, Support, 919 FocalEffective: float=None, 920 OffAxisAngle: float = None, 921 FocalParent: float = None, 922 RadiusParent: float = None, 923 OffAxisDistance: float = None, 924 MoreThan90: bool = None, 925 **kwargs): 926 """ 927 Initialise a Parabolic mirror. 928 929 Parameters 930 ---------- 931 FocalEffective : float 932 Effective focal length of the parabola in mm. 933 934 OffAxisAngle : float 935 Off-axis angle *in degrees* of the parabola. 936 937 Support : ART.ModuleSupport.Support 938 939 """ 940 super().__init__() 941 self.curvature = Curvature.CONCAVE 942 self.support = Support 943 self.type = "Grazing Parabolic Mirror" 944 945 if "Surface" in kwargs: 946 self.Surface = kwargs["Surface"] 947 948 parameter_calculator = OAP_calculator() 949 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 950 fs=FocalEffective, 951 theta=OffAxisAngle, 952 fp=FocalParent, 953 Rc=RadiusParent, 954 OAD=OffAxisDistance, 955 more_than_90=MoreThan90) 956 self._offaxisangle = values["theta"] 957 self._feff = values["fs"] 958 self._p = values["p"] 959 self._offaxisdistance = values["OAD"] 960 self._fparent = values["fp"] 961 self._rparent = values["Rc"] 962 963 #self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 964 self.r0 = Point([0, 0, 0]) 965 966 self.centre_ref = Point([0, 0, 0]) 967 self.support_normal_ref = Vector([0, 0, 1.0]) 968 self.majoraxis_ref = Vector([1.0, 0, 0]) 969 970 focus_ref = Point([np.sqrt(self._feff**2 - self._offaxisdistance**2), 0, self._offaxisdistance]) # frame where x is as collimated direction 971 # We need to rotate it in the trigonometric direction by the 90°-theta/2 972 angle = np.pi/2 - self._offaxisangle/2 973 self.focus_ref = Point([focus_ref[0]*np.cos(angle)+focus_ref[2]*np.sin(angle),focus_ref[1], -focus_ref[0]*np.sin(angle)+focus_ref[2]*np.cos(angle), ]) 974 975 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 976 towards_collimated_ref = Vector([-1.0, 0, 0]) # Again, we need to rotate it by the same angle 977 self.collimated_ref = Point([-np.cos(angle), 0, np.sin(angle)]) 978 979 self.add_global_points("focus", "centre") 980 self.add_global_vectors("towards_focusing", "towards_collimated", "support_normal", "majoraxis") 981 982 def _get_intersection(self, Ray): 983 """ 984 Return the intersection point between the ray and the parabola. 985 """ 986 pass 987 988 def get_local_normal(self, Point): 989 """ 990 Return the normal unit vector on the paraboloid surface at point Point. 991 """ 992 pass 993 994 def _zfunc(self, PointArray): 995 pass 996 997 998# %% Reflections on mirrors, is it useful to keep this? 999def _ReflectionMirrorRay(Mirror, PointMirror, Ray): 1000 """ 1001 Return the reflected ray according to the law of reflection. 1002 1003 Parameters 1004 ---------- 1005 Mirror : Mirror-objectS 1006 1007 PointMirror : np.ndarray 1008 Point of reflection on the mirror surface. 1009 1010 Ray : Ray-object 1011 1012 """ 1013 PointRay = Ray.point 1014 VectorRay = Ray.vector 1015 NormalMirror = Mirror.get_local_normal(PointMirror) 1016 1017 #VectorRayReflected = mgeo.SymmetricalVector(-VectorRay, NormalMirror) 1018 VectorRayReflected = VectorRay- 2*NormalMirror*np.dot(VectorRay,NormalMirror) # Is it any better than SymmetricalVector? 1019 1020 RayReflected = Ray.copy_ray() 1021 RayReflected.point = PointMirror 1022 RayReflected.vector = VectorRayReflected 1023 RayReflected.incidence = mgeo.AngleBetweenTwoVectors( 1024 -VectorRay, NormalMirror 1025 ) 1026 RayReflected.path = Ray.path + (np.linalg.norm(PointMirror - PointRay),) 1027 1028 return RayReflected 1029 1030 1031def ReflectionMirrorRayList(Mirror, ListRay, IgnoreDefects=False): 1032 """ 1033 Return the the reflected rays according to the law of reflection for the list of incident rays ListRay. 1034 1035 Rays that do not hit the support are not further propagated. 1036 1037 Updates the reflected rays' incidence angle and path. 1038 1039 Parameters 1040 ---------- 1041 Mirror : Mirror-object 1042 1043 ListRay : list[Ray-object] 1044 1045 """ 1046 Deformed = type(Mirror) == DeformedMirror 1047 ListRayReflected = [] 1048 for k in ListRay: 1049 PointMirror = Mirror._get_intersection(k) 1050 1051 if PointMirror is not None: 1052 if Deformed and IgnoreDefects: 1053 M = Mirror.Mirror 1054 else: 1055 M = Mirror 1056 RayReflected = _ReflectionMirrorRay(M, PointMirror, k) 1057 ListRayReflected.append(RayReflected) 1058 return ListRayReflected 1059 1060 1061# %% Deformed mirror definitions, need to be reworked, maybe implemented as coatings? 1062class DeformedMirror(Mirror): 1063 def __init__(self, Mirror, DeformationList): 1064 self.Mirror = Mirror 1065 self.DeformationList = DeformationList 1066 self.type = Mirror.type 1067 self.support = self.Mirror.support 1068 1069 def get_local_normal(self, PointMirror): 1070 base_normal = self.Mirror.get_normal(PointMirror) 1071 C = self.get_centre() 1072 defects_normals = [ 1073 d.get_normal(PointMirror - C) for d in self.DeformationList 1074 ] 1075 for i in defects_normals: 1076 base_normal = mgeo.normal_add(base_normal, i) 1077 base_normal /= np.linalg.norm(base_normal) 1078 return base_normal 1079 1080 def get_centre(self): 1081 return self.Mirror.get_centre() 1082 1083 def get_grid3D(self, NbPoint, **kwargs): 1084 return self.Mirror.get_grid3D(NbPoint, **kwargs) 1085 1086 def _get_intersection(self, Ray): 1087 Intersect = self.Mirror._get_intersection(Ray) 1088 if Intersect is not None: 1089 h = sum( 1090 D.get_offset(Intersect - self.get_centre()) 1091 for D in self.DeformationList 1092 ) 1093 alpha = mgeo.AngleBetweenTwoVectors( 1094 -Ray.vector, self.Mirror.get_normal(Intersect) 1095 ) 1096 Intersect -= Ray.vector * h / np.cos(alpha) 1097 return Intersect
44class Mirror(moe.OpticalElement): 45 """Abstract base class for mirrors.""" 46 vectorised = False 47 def __init__(self): 48 self.Surface = msurf.IdealSurface() 49 super().__init__() 50 51 @abstractmethod 52 def _get_intersection(self, Ray): 53 """ 54 This method should return the intersection point between the ray and the mirror surface. 55 The calculation should be done in the reference frame of the mirror. 56 It essentially defines the mirror surface (but not its normal). 57 """ 58 pass 59 60 @abstractmethod 61 def get_local_normal(self, Point: np.ndarray): 62 """ 63 This method should return the normal unit vector in point 'Point' on the mirror surface. 64 The normal is in the reference frame of the mirror. 65 """ 66 pass 67 68 def propagate_raylist(self, RayList, alignment=False): 69 """ 70 Propagate a list of rays to the mirror and return the reflected rays. 71 72 Parameters 73 ---------- 74 RayList : a RayList (a class defined in ARTcore.ModuleOpticalRay) 75 Rays to propagate through the mirror. 76 77 alignment : bool 78 If True, the rays are not blocked by the support. Not implemented yet 79 80 Returns 81 ------- 82 ReflectedRayList : a RayList 83 The rays that were reflected by the mirror. 84 """ 85 #local_rays = [ray.to_basis(*self.basis) for ray in RayList] 86 local_rays = RayList.to_basis(*self.basis) 87 # So local_rays is a list of rays in the mirror reference frame 88 if self.vectorised and len(local_rays) > 10: 89 # If the class implements the vectorised version of the intersection and normal calculation 90 intersection_points, OK = self._get_intersections(local_rays) 91 # NB: the _get_intersections method should return two things: 92 # - the intersection points in the mirror reference frame with nan's for rays that do not intersect 93 # - a boolean array indicating which rays intersections make sense 94 local_normals = mgeo.VectorArray(np.zeros_like(intersection_points)) 95 local_normals[OK] = self.get_local_normals(intersection_points[OK]) 96 local_reflected_rays = [ 97 self.Surface.reflect_ray(local_rays[i], intersection_points[i], local_normals[i]) 98 for i in range(len(local_rays)) if OK[i]] 99 else: 100 intersection_points, OK = zip(*[self._get_intersection(r) for r in local_rays]) 101 # OK is a boolean list indicating if the ray intersects the mirror (for instance is in the support) 102 local_reflected_rays = [ 103 self.Surface.reflect_ray(local_rays[i], intersection_points[i], self.get_local_normal(intersection_points[i])) 104 for i in range(len(local_rays)) if OK[i]] 105 reflected_rays = mray.RayList.from_list(local_reflected_rays).from_basis(*self.basis) 106 if alignment and len(reflected_rays) == 0: 107 logger.error("Unexpected error occurred: no rays were reflected during alignment procedure.") 108 logger.error("This should not happen.") 109 logger.error(f"Mirror on which error ocurred: {self}") 110 logger.error(f"Alignment ray: {RayList[0]}") 111 logger.error(f"Alignment ray in mirror reference frame: {local_rays[0]}") 112 logger.error(f"Intersection point: {self._get_intersection(local_rays[0])[0]}") 113 logger.error(f"Support: {self.support}") 114 elif len(reflected_rays) == 0: 115 logger.warning("No rays were reflected by the mirror.") 116 logger.debug(f"Mirror: {self}") 117 logger.debug(f"First ray: {RayList[0]}") 118 logger.debug(f"First ray in mirror reference frame: {RayList[0].to_basis(self.r0, self.r, self.q)}") 119 logger.debug(f"First ray intersection point: {self._get_intersection(RayList[0].to_basis(self.r0, self.r, self.q))}") 120 return reflected_rays
Abstract base class for mirrors.
60 @abstractmethod 61 def get_local_normal(self, Point: np.ndarray): 62 """ 63 This method should return the normal unit vector in point 'Point' on the mirror surface. 64 The normal is in the reference frame of the mirror. 65 """ 66 pass
This method should return the normal unit vector in point 'Point' on the mirror surface. The normal is in the reference frame of the mirror.
68 def propagate_raylist(self, RayList, alignment=False): 69 """ 70 Propagate a list of rays to the mirror and return the reflected rays. 71 72 Parameters 73 ---------- 74 RayList : a RayList (a class defined in ARTcore.ModuleOpticalRay) 75 Rays to propagate through the mirror. 76 77 alignment : bool 78 If True, the rays are not blocked by the support. Not implemented yet 79 80 Returns 81 ------- 82 ReflectedRayList : a RayList 83 The rays that were reflected by the mirror. 84 """ 85 #local_rays = [ray.to_basis(*self.basis) for ray in RayList] 86 local_rays = RayList.to_basis(*self.basis) 87 # So local_rays is a list of rays in the mirror reference frame 88 if self.vectorised and len(local_rays) > 10: 89 # If the class implements the vectorised version of the intersection and normal calculation 90 intersection_points, OK = self._get_intersections(local_rays) 91 # NB: the _get_intersections method should return two things: 92 # - the intersection points in the mirror reference frame with nan's for rays that do not intersect 93 # - a boolean array indicating which rays intersections make sense 94 local_normals = mgeo.VectorArray(np.zeros_like(intersection_points)) 95 local_normals[OK] = self.get_local_normals(intersection_points[OK]) 96 local_reflected_rays = [ 97 self.Surface.reflect_ray(local_rays[i], intersection_points[i], local_normals[i]) 98 for i in range(len(local_rays)) if OK[i]] 99 else: 100 intersection_points, OK = zip(*[self._get_intersection(r) for r in local_rays]) 101 # OK is a boolean list indicating if the ray intersects the mirror (for instance is in the support) 102 local_reflected_rays = [ 103 self.Surface.reflect_ray(local_rays[i], intersection_points[i], self.get_local_normal(intersection_points[i])) 104 for i in range(len(local_rays)) if OK[i]] 105 reflected_rays = mray.RayList.from_list(local_reflected_rays).from_basis(*self.basis) 106 if alignment and len(reflected_rays) == 0: 107 logger.error("Unexpected error occurred: no rays were reflected during alignment procedure.") 108 logger.error("This should not happen.") 109 logger.error(f"Mirror on which error ocurred: {self}") 110 logger.error(f"Alignment ray: {RayList[0]}") 111 logger.error(f"Alignment ray in mirror reference frame: {local_rays[0]}") 112 logger.error(f"Intersection point: {self._get_intersection(local_rays[0])[0]}") 113 logger.error(f"Support: {self.support}") 114 elif len(reflected_rays) == 0: 115 logger.warning("No rays were reflected by the mirror.") 116 logger.debug(f"Mirror: {self}") 117 logger.debug(f"First ray: {RayList[0]}") 118 logger.debug(f"First ray in mirror reference frame: {RayList[0].to_basis(self.r0, self.r, self.q)}") 119 logger.debug(f"First ray intersection point: {self._get_intersection(RayList[0].to_basis(self.r0, self.r, self.q))}") 120 return reflected_rays
Propagate a list of rays to the mirror and return the reflected rays.
RayList : a RayList (a class defined in ARTcore.ModuleOpticalRay)
Rays to propagate through the mirror.
alignment : bool
If True, the rays are not blocked by the support. Not implemented yet
ReflectedRayList : a RayList
The rays that were reflected by the mirror.
445def GetClosestSphere(Mirror, Npoints=1000): 446 """ 447 This function calculates the closest sphere to the surface of a mirror. 448 It does so by sampling the surface of the mirror at Npoints points. 449 It then calculates the closest sphere to these points. 450 It returns the radius of the sphere and the center of the sphere. 451 """ 452 Points = mpm.sample_support(Mirror.support, Npoints=1000) 453 Points += Mirror.r0[:2] 454 Z = Mirror._zfunc(Points) 455 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 456 spX, spY, spZ = Points[:, 0], Points[:, 1], Points[:, 2] 457 Center, Radius = BestFitSphere(spX, spY, spZ) 458 return Center, Radius
This function calculates the closest sphere to the surface of a mirror. It does so by sampling the surface of the mirror at Npoints points. It then calculates the closest sphere to these points. It returns the radius of the sphere and the center of the sphere.
460def GetAsphericity(Mirror, Npoints=1000): 461 """ 462 This function calculates the maximum distance of the mirror surface to the closest sphere. 463 """ 464 center, radius = GetClosestSphere(Mirror, Npoints) 465 Points = mpm.sample_support(Mirror.support, Npoints=1000) 466 Points += Mirror.r0[:2] 467 Z = Mirror._zfunc(Points) 468 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 469 Points_centered = Points - center 470 Distance = np.linalg.norm(Points_centered, axis=1) - radius 471 Distance*=1e3 # To convert to µm 472 return np.ptp(Distance)
This function calculates the maximum distance of the mirror surface to the closest sphere.
654def DrawAsphericity(Mirror, Npoints=1000): 655 """ 656 This function displays a map of the asphericity of the mirror. 657 It's a scatter plot of the points of the mirror surface, with the color representing the distance to the closest sphere. 658 The closest sphere is calculated by the function get_closest_sphere, so least square method. 659 660 Parameters 661 ---------- 662 Mirror : Mirror 663 The mirror to analyse. 664 665 Npoints : int, optional 666 The number of points to sample on the mirror surface. The default is 1000. 667 668 Returns 669 ------- 670 fig : Figure 671 The figure of the plot. 672 """ 673 plt.ion() 674 fig = plt.figure() 675 ax = Mirror.support._ContourSupport(fig) 676 center, radius = man.GetClosestSphere(Mirror, Npoints) 677 Points = mpm.sample_support(Mirror.support, Npoints=1000) 678 Points += Mirror.r0[:2] 679 Z = Mirror._zfunc(Points) 680 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 681 X, Y = Points[:, 0] - Mirror.r0[0], Points[:, 1] - Mirror.r0[1] 682 Points_centered = Points - center 683 Distance = np.linalg.norm(Points_centered, axis=1) - radius 684 Distance*=1e3 # To convert to µm 685 p = plt.scatter(X, Y, c=Distance, s=15) 686 divider = man.make_axes_locatable(ax) 687 cax = divider.append_axes("right", size="5%", pad=0.05) 688 cbar = fig.colorbar(p, cax=cax) 689 cbar.set_label("Distance to closest sphere (µm)") 690 ax.set_xlabel("x (mm)") 691 ax.set_ylabel("y (mm)") 692 plt.title("Asphericity map", loc="right") 693 plt.tight_layout() 694 695 bbox = ax.get_position() 696 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 697 ax.set_position(bbox) 698 plt.show() 699 return fig
This function displays a map of the asphericity of the mirror. It's a scatter plot of the points of the mirror surface, with the color representing the distance to the closest sphere. The closest sphere is calculated by the function get_closest_sphere, so least square method.
Mirror : Mirror The mirror to analyse.
Npoints : int, optional The number of points to sample on the mirror surface. The default is 1000.
fig : Figure The figure of the plot.
138class MirrorPlane(Mirror): 139 """ 140 A plane mirror. 141 142 Attributes 143 ---------- 144 support : ART.ModuleSupport.Support 145 Mirror support 146 type : str = 'Plane Mirror' 147 Human readable mirror type 148 Methods 149 ------- 150 MirrorPlane.get_normal(Point) 151 152 MirrorPlane.get_centre() 153 154 MirrorPlane.get_grid3D(NbPoints) 155 """ 156 vectorised = True 157 def __init__(self, Support, **kwargs): 158 """ 159 Create a plane mirror on a given Support. 160 161 Parameters 162 ---------- 163 Support : ART.ModuleSupport.Support 164 165 """ 166 super().__init__() 167 self.support = Support 168 self.type = "Plane Mirror" 169 self.curvature = Curvature.FLAT 170 171 if "Surface" in kwargs: 172 self.Surface = kwargs["Surface"] 173 174 self.r0 = Point([0.0, 0.0, 0.0]) 175 176 self.centre_ref = Point([0.0, 0.0, 0.0]) 177 self.support_normal_ref = Vector([0, 0, 1.0]) 178 self.majoraxis_ref = Vector([1.0, 0, 0]) 179 180 self.add_global_points("centre") 181 self.add_global_vectors("support_normal", "majoraxis") 182 183 def _get_intersection(self, Ray): 184 """Return the intersection point between Ray and the xy-plane.""" 185 t = -Ray.point[2] / Ray.vector[2] 186 PointIntersection = Ray.point + Ray.vector * t 187 return PointIntersection, t > 0 and PointIntersection in self.support 188 189 def get_local_normal(self, Point: np.ndarray): 190 """Return normal unit vector in point 'Point' on the plane mirror.""" 191 return Vector([0, 0, 1]) 192 193 def _get_intersections(self, RayList): 194 """ 195 Vectorised version of the intersection calculation. 196 """ 197 vectors = np.array([ray.vector for ray in RayList]) 198 points = np.array([ray.point for ray in RayList]) 199 200 t = -points[:,2] / vectors[:,2] 201 Points = points + vectors * t[:,np.newaxis] 202 OK = (t > 0) & np.array([p in self.support for p in Points]) 203 return mgeo.PointArray(Points), OK 204 205 def get_local_normals(self, Points): 206 return mgeo.VectorArray(np.zeros_like(Points) + [0, 0, 1]) 207 208 def _zfunc(self, PointArray): 209 x = PointArray[:,0] 210 y = PointArray[:,1] 211 return np.zeros_like(x)
A plane mirror.
support : ART.ModuleSupport.Support
Mirror support
type : str = 'Plane Mirror'
Human readable mirror type
MirrorPlane.get_normal(Point)
MirrorPlane.get_centre()
MirrorPlane.get_grid3D(NbPoints)
157 def __init__(self, Support, **kwargs): 158 """ 159 Create a plane mirror on a given Support. 160 161 Parameters 162 ---------- 163 Support : ART.ModuleSupport.Support 164 165 """ 166 super().__init__() 167 self.support = Support 168 self.type = "Plane Mirror" 169 self.curvature = Curvature.FLAT 170 171 if "Surface" in kwargs: 172 self.Surface = kwargs["Surface"] 173 174 self.r0 = Point([0.0, 0.0, 0.0]) 175 176 self.centre_ref = Point([0.0, 0.0, 0.0]) 177 self.support_normal_ref = Vector([0, 0, 1.0]) 178 self.majoraxis_ref = Vector([1.0, 0, 0]) 179 180 self.add_global_points("centre") 181 self.add_global_vectors("support_normal", "majoraxis")
Create a plane mirror on a given Support.
Support : ART.ModuleSupport.Support
189 def get_local_normal(self, Point: np.ndarray): 190 """Return normal unit vector in point 'Point' on the plane mirror.""" 191 return Vector([0, 0, 1])
Return normal unit vector in point 'Point' on the plane mirror.
234def _RenderMirror(Mirror, Npoints=1000, draw_support = False, draw_points = False, draw_vectors = True , recenter_support = True): 235 """ 236 This function renders a mirror in 3D. 237 """ 238 mesh = Mirror._Render(Npoints) 239 p = pv.Plotter() 240 p.add_mesh(mesh) 241 if draw_support: 242 support = Mirror.support._Render() 243 if recenter_support: 244 support.translate(Mirror.r0,inplace=True) 245 p.add_mesh(support, color="gray", opacity=0.5) 246 247 if draw_vectors: 248 # We draw the important vectors of the optical element 249 # For that, if we have a "vectors" attribute, we use that 250 # (a dictionary with the vector names as keys and the colors as values) 251 # Otherwise we use the default vectors: "support_normal", "majoraxis" 252 if hasattr(Mirror, "vectors"): 253 vectors = Mirror.vectors 254 else: 255 vectors = {"support_normal_ref": "red", "majoraxis_ref": "blue"} 256 for vector, color in vectors.items(): 257 if hasattr(Mirror, vector): 258 p.add_arrows(Mirror.r0, 10*getattr(Mirror, vector), color=color) 259 260 if draw_points: 261 # We draw the important points of the optical element 262 # For that, if we have a "points" attribute, we use that 263 # (a dictionary with the point names as keys and the colors as values) 264 # Otherwise we use the default points: "centre_ref" 265 if hasattr(Mirror, "points"): 266 points = Mirror.points 267 else: 268 points = {"centre_ref": "red"} 269 for point, color in points.items(): 270 if hasattr(Mirror, point): 271 p.add_mesh(pv.Sphere(radius=1, center=getattr(Mirror, point)), color=color) 272 else: 273 p.add_mesh(pv.Sphere(radius=1, center=Mirror.r0), color="red") 274 p.show() 275 return p
This function renders a mirror in 3D.
214class MirrorSpherical(Mirror): 215 """ 216 Spherical mirror surface with eqn. $x^2 + y^2 + z^2 = R^2$, where $R$ is the radius. 217 218  219 220 Attributes 221 ---------- 222 radius : float 223 Radius of curvature. A postive value for concave mirror, a negative value for a convex mirror. 224 225 support : ART.ModuleSupport.Support 226 227 type : str SphericalCC Mirror' or SphericalCX Mirror' 228 229 Methods 230 ------- 231 MirrorSpherical.get_normal(Point) 232 233 MirrorSpherical.get_centre() 234 235 MirrorSpherical.get_grid3D(NbPoints) 236 237 """ 238 def __init__(self, Support, Radius, **kwargs): 239 """ 240 Construct a spherical mirror. 241 242 Parameters 243 ---------- 244 Radius : float 245 The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror. 246 247 Support : ART.ModuleSupport.Support 248 249 """ 250 super().__init__() 251 if Radius < 0: 252 self.type = "SphericalCX Mirror" 253 self.curvature = Curvature.CONVEX 254 self.radius = -Radius 255 elif Radius > 0: 256 self.type = "SphericalCC Mirror" 257 self.curvature = Curvature.CONCAVE 258 self.radius = Radius 259 else: 260 raise ValueError("Radius of curvature must be non-zero") 261 self.support = Support 262 263 if "Surface" in kwargs: 264 self.Surface = kwargs["Surface"] 265 266 self.r0 = Point([0.0, 0, -self.radius]) 267 268 self.centre_ref = Point([0.0, 0, -self.radius]) 269 self.sphere_center_ref = Point([0.0, 0, 0]) 270 self.focus_ref = Point([0.0, 0, -self.radius/2]) 271 272 self.support_normal_ref = Vector([0, 0, 1.0]) 273 self.majoraxis_ref = Vector([1.0, 0, 0]) 274 self.towards_focus_ref = (self.focus_ref - self.centre_ref).normalized() 275 276 self.add_global_points("sphere_center", "focus", "centre") 277 self.add_global_vectors("towards_focus", "majoraxis", "support_normal") 278 279 def _get_intersection(self, Ray): 280 """Return the intersection point between the ray and the sphere.""" 281 a = np.dot(Ray.vector, Ray.vector) 282 b = 2 * np.dot(Ray.vector, Ray.point) 283 c = np.dot(Ray.point, Ray.point) - self.radius**2 284 285 Solution = mgeo.SolverQuadratic(a, b, c) 286 Solution = mgeo.KeepPositiveSolution(Solution) 287 288 for t in Solution: 289 Intersect = Ray.vector * t + Ray.point 290 if Intersect - self.r0 in self.support: 291 return Intersect, True 292 293 return Ray.point, False 294 295 def get_local_normal(self, Point): 296 """Return the normal unit vector on the spherical surface at point Point.""" 297 return -Vector(Point).normalized() 298 299 def _zfunc(self, PointArray): 300 x = PointArray[:,0] 301 y = PointArray[:,1] 302 return -np.sqrt(self.radius**2 - x**2 - y**2)
Spherical mirror surface with eqn. $x^2 + y^2 + z^2 = R^2$, where $R$ is the radius.
radius : float
Radius of curvature. A postive value for concave mirror, a negative value for a convex mirror.
support : ART.ModuleSupport.Support
type : str SphericalCC Mirror' or SphericalCX Mirror'
MirrorSpherical.get_normal(Point)
MirrorSpherical.get_centre()
MirrorSpherical.get_grid3D(NbPoints)
238 def __init__(self, Support, Radius, **kwargs): 239 """ 240 Construct a spherical mirror. 241 242 Parameters 243 ---------- 244 Radius : float 245 The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror. 246 247 Support : ART.ModuleSupport.Support 248 249 """ 250 super().__init__() 251 if Radius < 0: 252 self.type = "SphericalCX Mirror" 253 self.curvature = Curvature.CONVEX 254 self.radius = -Radius 255 elif Radius > 0: 256 self.type = "SphericalCC Mirror" 257 self.curvature = Curvature.CONCAVE 258 self.radius = Radius 259 else: 260 raise ValueError("Radius of curvature must be non-zero") 261 self.support = Support 262 263 if "Surface" in kwargs: 264 self.Surface = kwargs["Surface"] 265 266 self.r0 = Point([0.0, 0, -self.radius]) 267 268 self.centre_ref = Point([0.0, 0, -self.radius]) 269 self.sphere_center_ref = Point([0.0, 0, 0]) 270 self.focus_ref = Point([0.0, 0, -self.radius/2]) 271 272 self.support_normal_ref = Vector([0, 0, 1.0]) 273 self.majoraxis_ref = Vector([1.0, 0, 0]) 274 self.towards_focus_ref = (self.focus_ref - self.centre_ref).normalized() 275 276 self.add_global_points("sphere_center", "focus", "centre") 277 self.add_global_vectors("towards_focus", "majoraxis", "support_normal")
Construct a spherical mirror.
Radius : float
The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror.
Support : ART.ModuleSupport.Support
295 def get_local_normal(self, Point): 296 """Return the normal unit vector on the spherical surface at point Point.""" 297 return -Vector(Point).normalized()
Return the normal unit vector on the spherical surface at point Point.
234def _RenderMirror(Mirror, Npoints=1000, draw_support = False, draw_points = False, draw_vectors = True , recenter_support = True): 235 """ 236 This function renders a mirror in 3D. 237 """ 238 mesh = Mirror._Render(Npoints) 239 p = pv.Plotter() 240 p.add_mesh(mesh) 241 if draw_support: 242 support = Mirror.support._Render() 243 if recenter_support: 244 support.translate(Mirror.r0,inplace=True) 245 p.add_mesh(support, color="gray", opacity=0.5) 246 247 if draw_vectors: 248 # We draw the important vectors of the optical element 249 # For that, if we have a "vectors" attribute, we use that 250 # (a dictionary with the vector names as keys and the colors as values) 251 # Otherwise we use the default vectors: "support_normal", "majoraxis" 252 if hasattr(Mirror, "vectors"): 253 vectors = Mirror.vectors 254 else: 255 vectors = {"support_normal_ref": "red", "majoraxis_ref": "blue"} 256 for vector, color in vectors.items(): 257 if hasattr(Mirror, vector): 258 p.add_arrows(Mirror.r0, 10*getattr(Mirror, vector), color=color) 259 260 if draw_points: 261 # We draw the important points of the optical element 262 # For that, if we have a "points" attribute, we use that 263 # (a dictionary with the point names as keys and the colors as values) 264 # Otherwise we use the default points: "centre_ref" 265 if hasattr(Mirror, "points"): 266 points = Mirror.points 267 else: 268 points = {"centre_ref": "red"} 269 for point, color in points.items(): 270 if hasattr(Mirror, point): 271 p.add_mesh(pv.Sphere(radius=1, center=getattr(Mirror, point)), color=color) 272 else: 273 p.add_mesh(pv.Sphere(radius=1, center=Mirror.r0), color="red") 274 p.show() 275 return p
This function renders a mirror in 3D.
305class MirrorParabolic(Mirror): 306 r""" 307 A paraboloid with vertex at the origin $O=[0,0,0]$ and symmetry axis z: 308 $z = \frac{1}{4f}[x^2 + y^2]$ where $f$ is the focal lenght of the *mother* 309 parabola (i.e. measured from its center at $O$ to the focal point $F$). 310 311 The center of the support is shifted along the x-direction by the off-axis distance $x_c$. 312 This leads to an *effective focal length* $f_\mathrm{eff}$, measured from the shifted center 313 of the support $P$ to the focal point $F$. 314 It is related to the mother focal length by $f = f_\\mathrm{eff} \cos^2(\alpha/2) $, 315 or equivalently $ p = 2f = f_\mathrm{eff} (1+\\cos\\alpha)$, where $\\alpha$ 316 is the off-axis angle, and $p = 2f$ is called the semi latus rectum. 317 318 Another useful relationship is that between the off-axis distance and the resulting 319 off-axis angle: $x_c = 2 f \tan(\alpha/2)$. 320 321 322  323 324 Attributes 325 ---------- 326 offaxisangle : float 327 Off-axis angle of the parabola. Modifying this also updates p, keeping feff constant. 328 Attention: The off-axis angle must be *given in degrees*, but is stored and will be *returned in radian* ! 329 330 feff : float 331 Effective focal length of the parabola in mm. Modifying this also updates p, keeping offaxisangle constant. 332 333 p : float 334 Semi latus rectum of the parabola in mm. Modifying this also updates feff, keeping offaxisangle constant. 335 336 support : ART.ModuleSupport.Support 337 338 type : str 'Parabolic Mirror'. 339 340 Methods 341 ------- 342 MirrorParabolic.get_normal(Point) 343 344 MirrorParabolic.get_centre() 345 346 MirrorParabolic.get_grid3D(NbPoints) 347 348 """ 349 #vectorised = True # Unitl I fix the closest solution thing 350 def __init__(self, Support, 351 FocalEffective: float=None, 352 OffAxisAngle: float = None, 353 FocalParent: float = None, 354 RadiusParent: float = None, 355 OffAxisDistance: float = None, 356 MoreThan90: bool = None, 357 **kwargs): 358 """ 359 Initialise a Parabolic mirror. 360 361 Parameters 362 ---------- 363 FocalEffective : float 364 Effective focal length of the parabola in mm. 365 366 OffAxisAngle : float 367 Off-axis angle *in degrees* of the parabola. 368 369 Support : ART.ModuleSupport.Support 370 371 """ 372 super().__init__() 373 self.curvature = Curvature.CONCAVE 374 self.support = Support 375 self.type = "Parabolic Mirror" 376 377 if "Surface" in kwargs: 378 self.Surface = kwargs["Surface"] 379 380 parameter_calculator = OAP_calculator() 381 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 382 fs=FocalEffective, 383 theta=OffAxisAngle, 384 fp=FocalParent, 385 Rc=RadiusParent, 386 OAD=OffAxisDistance, 387 more_than_90=MoreThan90) 388 self._offaxisangle = values["theta"] 389 self._feff = values["fs"] 390 self._p = values["p"] 391 self._offaxisdistance = values["OAD"] 392 self._fparent = values["fp"] 393 self._rparent = values["Rc"] 394 395 self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 396 397 self.centre_ref = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 398 self.support_normal_ref = Vector([0, 0, 1.0]) 399 self.majoraxis_ref = Vector([1.0, 0, 0]) 400 401 self.focus_ref = Point([0.0, 0, self._fparent]) 402 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 403 self.towards_collimated_ref = Vector([0, 0, 1.0]) 404 405 self.add_global_points("focus", "centre") 406 self.add_global_vectors("towards_focusing", "towards_collimated", "support_normal", "majoraxis") 407 408 def _get_intersection(self, Ray): 409 """Return the intersection point between the ray and the parabola.""" 410 ux, uy, uz = Ray.vector 411 xA, yA, zA = Ray.point 412 413 da = ux**2 + uy**2 414 db = 2 * (ux * xA + uy * yA) - 2 * self._p * uz 415 dc = xA**2 + yA**2 - 2 * self._p * zA 416 417 Solution = mgeo.SolverQuadratic(da, db, dc) 418 Solution = mgeo.KeepPositiveSolution(Solution) 419 IntersectionPoint = [Ray.point + Ray.vector * t for t in Solution if t > 0] 420 distances = [mgeo.Vector(i - self.r0).norm for i in IntersectionPoint] 421 IntersectionPoint = IntersectionPoint[distances.index(min(distances))] 422 return IntersectionPoint, IntersectionPoint-self.r0 in self.support 423 424 def _get_intersections(self, RayList): 425 """ 426 Vectorised version of the intersection calculation. 427 """ 428 vectors = np.array([ray.vector for ray in RayList]) 429 points = np.array([ray.point for ray in RayList]) 430 Solutions = np.zeros(len(RayList), dtype=float) 431 432 da = np.sum(vectors[:,0:2]**2, axis=1) 433 db = 2 * (np.sum(vectors[:,0:2] * points[:,0:2], axis=1) - self._p * vectors[:,2]) 434 dc = np.sum(points[:,0:2]**2, axis=1) - 2 * self._p * points[:,2] 435 436 linear = da == 0 437 Solutions[linear] = -dc[linear] / db[linear] 438 439 centered = (dc==0) & (da!=0) # If equation is of form ax**2 + bx = 0 440 Solutions[centered] = np.maximum(-db[centered] / da[centered], 0) 441 442 unstable = (4*da*dc) < db**2 * 1e-10 # Cases leading to catastrophic cancellation 443 444 Deltas = db**2 - 4 * da * dc 445 OK = Deltas >= 0 446 SolutionsPlus = (-db + np.sqrt(Deltas)) / 2 / da 447 SolutionsMinus = (-db - np.sqrt(Deltas)) / 2 / da 448 SolutionsPlus[unstable] = dc[unstable] / (da[unstable] * SolutionsMinus[unstable]) 449 SolutionsPlus = np.where(SolutionsPlus >= 0, SolutionsPlus, np.inf) 450 SolutionsMinus = np.where(SolutionsMinus >= 0, SolutionsMinus, np.inf) 451 Solutions = np.minimum(SolutionsPlus, SolutionsMinus) 452 Solutions = np.maximum(Solutions, 0) 453 Points = points + vectors * Solutions[:,np.newaxis] 454 OK = OK & (Solutions > 0) & np.array([p-self.r0 in self.support for p in Points]) 455 return mgeo.PointArray(Points), OK 456 457 def get_local_normal(self, Point): 458 """Return the normal unit vector on the paraboloid surface at point Point.""" 459 Gradient = Vector(np.zeros(3)) 460 Gradient[0] = -Point[0] 461 Gradient[1] = -Point[1] 462 Gradient[2] = self._p 463 return Gradient.normalized() 464 465 def get_local_normals(self, Points): 466 """ 467 Vectorised version of the normal calculation. 468 """ 469 Gradients = mgeo.VectorArray(np.zeros_like(Points)) 470 Gradients[:,0] = -Points[:,0] 471 Gradients[:,1] = -Points[:,1] 472 Gradients[:,2] = self._p 473 return Gradients.normalized() 474 475 def _zfunc(self, PointArray): 476 x = PointArray[:,0] 477 y = PointArray[:,1] 478 return (x**2 + y**2) / 2 / self._p
A paraboloid with vertex at the origin $O=[0,0,0]$ and symmetry axis z: $z = \frac{1}{4f}[x^2 + y^2]$ where $f$ is the focal lenght of the mother parabola (i.e. measured from its center at $O$ to the focal point $F$).
The center of the support is shifted along the x-direction by the off-axis distance $x_c$. This leads to an effective focal length $f_\mathrm{eff}$, measured from the shifted center of the support $P$ to the focal point $F$. It is related to the mother focal length by $f = f_\mathrm{eff} \cos^2(\alpha/2) $, or equivalently $ p = 2f = f_\mathrm{eff} (1+\cos\alpha)$, where $\alpha$ is the off-axis angle, and $p = 2f$ is called the semi latus rectum.
Another useful relationship is that between the off-axis distance and the resulting off-axis angle: $x_c = 2 f \tan(\alpha/2)$.
offaxisangle : float
Off-axis angle of the parabola. Modifying this also updates p, keeping feff constant.
Attention: The off-axis angle must be *given in degrees*, but is stored and will be *returned in radian* !
feff : float
Effective focal length of the parabola in mm. Modifying this also updates p, keeping offaxisangle constant.
p : float
Semi latus rectum of the parabola in mm. Modifying this also updates feff, keeping offaxisangle constant.
support : ART.ModuleSupport.Support
type : str 'Parabolic Mirror'.
MirrorParabolic.get_normal(Point)
MirrorParabolic.get_centre()
MirrorParabolic.get_grid3D(NbPoints)
350 def __init__(self, Support, 351 FocalEffective: float=None, 352 OffAxisAngle: float = None, 353 FocalParent: float = None, 354 RadiusParent: float = None, 355 OffAxisDistance: float = None, 356 MoreThan90: bool = None, 357 **kwargs): 358 """ 359 Initialise a Parabolic mirror. 360 361 Parameters 362 ---------- 363 FocalEffective : float 364 Effective focal length of the parabola in mm. 365 366 OffAxisAngle : float 367 Off-axis angle *in degrees* of the parabola. 368 369 Support : ART.ModuleSupport.Support 370 371 """ 372 super().__init__() 373 self.curvature = Curvature.CONCAVE 374 self.support = Support 375 self.type = "Parabolic Mirror" 376 377 if "Surface" in kwargs: 378 self.Surface = kwargs["Surface"] 379 380 parameter_calculator = OAP_calculator() 381 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 382 fs=FocalEffective, 383 theta=OffAxisAngle, 384 fp=FocalParent, 385 Rc=RadiusParent, 386 OAD=OffAxisDistance, 387 more_than_90=MoreThan90) 388 self._offaxisangle = values["theta"] 389 self._feff = values["fs"] 390 self._p = values["p"] 391 self._offaxisdistance = values["OAD"] 392 self._fparent = values["fp"] 393 self._rparent = values["Rc"] 394 395 self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 396 397 self.centre_ref = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 398 self.support_normal_ref = Vector([0, 0, 1.0]) 399 self.majoraxis_ref = Vector([1.0, 0, 0]) 400 401 self.focus_ref = Point([0.0, 0, self._fparent]) 402 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 403 self.towards_collimated_ref = Vector([0, 0, 1.0]) 404 405 self.add_global_points("focus", "centre") 406 self.add_global_vectors("towards_focusing", "towards_collimated", "support_normal", "majoraxis")
Initialise a Parabolic mirror.
FocalEffective : float
Effective focal length of the parabola in mm.
OffAxisAngle : float
Off-axis angle *in degrees* of the parabola.
Support : ART.ModuleSupport.Support
457 def get_local_normal(self, Point): 458 """Return the normal unit vector on the paraboloid surface at point Point.""" 459 Gradient = Vector(np.zeros(3)) 460 Gradient[0] = -Point[0] 461 Gradient[1] = -Point[1] 462 Gradient[2] = self._p 463 return Gradient.normalized()
Return the normal unit vector on the paraboloid surface at point Point.
465 def get_local_normals(self, Points): 466 """ 467 Vectorised version of the normal calculation. 468 """ 469 Gradients = mgeo.VectorArray(np.zeros_like(Points)) 470 Gradients[:,0] = -Points[:,0] 471 Gradients[:,1] = -Points[:,1] 472 Gradients[:,2] = self._p 473 return Gradients.normalized()
Vectorised version of the normal calculation.
234def _RenderMirror(Mirror, Npoints=1000, draw_support = False, draw_points = False, draw_vectors = True , recenter_support = True): 235 """ 236 This function renders a mirror in 3D. 237 """ 238 mesh = Mirror._Render(Npoints) 239 p = pv.Plotter() 240 p.add_mesh(mesh) 241 if draw_support: 242 support = Mirror.support._Render() 243 if recenter_support: 244 support.translate(Mirror.r0,inplace=True) 245 p.add_mesh(support, color="gray", opacity=0.5) 246 247 if draw_vectors: 248 # We draw the important vectors of the optical element 249 # For that, if we have a "vectors" attribute, we use that 250 # (a dictionary with the vector names as keys and the colors as values) 251 # Otherwise we use the default vectors: "support_normal", "majoraxis" 252 if hasattr(Mirror, "vectors"): 253 vectors = Mirror.vectors 254 else: 255 vectors = {"support_normal_ref": "red", "majoraxis_ref": "blue"} 256 for vector, color in vectors.items(): 257 if hasattr(Mirror, vector): 258 p.add_arrows(Mirror.r0, 10*getattr(Mirror, vector), color=color) 259 260 if draw_points: 261 # We draw the important points of the optical element 262 # For that, if we have a "points" attribute, we use that 263 # (a dictionary with the point names as keys and the colors as values) 264 # Otherwise we use the default points: "centre_ref" 265 if hasattr(Mirror, "points"): 266 points = Mirror.points 267 else: 268 points = {"centre_ref": "red"} 269 for point, color in points.items(): 270 if hasattr(Mirror, point): 271 p.add_mesh(pv.Sphere(radius=1, center=getattr(Mirror, point)), color=color) 272 else: 273 p.add_mesh(pv.Sphere(radius=1, center=Mirror.r0), color="red") 274 p.show() 275 return p
This function renders a mirror in 3D.
482class MirrorToroidal(Mirror): 483 r""" 484 Toroidal mirror surface with eqn.$(\sqrt{x^2+z^2}-R)^2 + y^2 = r^2$ where $R$ and $r$ the major and minor radii. 485 486  487 488 Attributes 489 ---------- 490 majorradius : float 491 Major radius of the toroid in mm. 492 493 minorradius : float 494 Minor radius of the toroid in mm. 495 496 support : ART.ModuleSupport.Support 497 498 type : str 'Toroidal Mirror'. 499 500 Methods 501 ------- 502 MirrorToroidal.get_normal(Point) 503 504 MirrorToroidal.get_centre() 505 506 MirrorToroidal.get_grid3D(NbPoints) 507 508 """ 509 510 def __init__(self, Support, MajorRadius, MinorRadius, **kwargs): 511 """ 512 Construct a toroidal mirror. 513 514 Parameters 515 ---------- 516 MajorRadius : float 517 Major radius of the toroid in mm. 518 519 MinorRadius : float 520 Minor radius of the toroid in mm. 521 522 Support : ART.ModuleSupport.Support 523 524 """ 525 super().__init__() 526 self.support = Support 527 self.type = "Toroidal Mirror" 528 529 if "Surface" in kwargs: 530 self.Surface = kwargs["Surface"] 531 532 self.curvature = Curvature.CONCAVE 533 534 self.majorradius = MajorRadius 535 self.minorradius = MinorRadius 536 537 self.r0 = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 538 539 self.centre_ref = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 540 self.support_normal_ref = Vector([0, 0, 1.0]) 541 self.majoraxis_ref = Vector([1.0, 0, 0]) 542 543 self.add_global_vectors("support_normal", "majoraxis") 544 self.add_global_points("centre") 545 546 def _get_intersection(self, Ray): 547 """Return the intersection point between Ray and the toroidal mirror surface. The intersection points are given in the reference frame of the mirror""" 548 ux = Ray.vector[0] 549 uz = Ray.vector[2] 550 xA = Ray.point[0] 551 zA = Ray.point[2] 552 553 G = 4.0 * self.majorradius**2 * (ux**2 + uz**2) 554 H = 8.0 * self.majorradius**2 * (ux * xA + uz * zA) 555 I = 4.0 * self.majorradius**2 * (xA**2 + zA**2) 556 J = np.dot(Ray.vector, Ray.vector) 557 K = 2.0 * np.dot(Ray.vector, Ray.point) 558 L = ( 559 np.dot(Ray.point, Ray.point) 560 + self.majorradius**2 561 - self.minorradius**2 562 ) 563 564 a = J**2 565 b = 2 * J * K 566 c = 2 * J * L + K**2 - G 567 d = 2 * K * L - H 568 e = L**2 - I 569 570 Solution = mgeo.SolverQuartic(a, b, c, d, e) 571 if len(Solution) == 0: 572 return None, False 573 Solution = [t for t in Solution if t > 0] 574 IntersectionPoint = [Ray.point + Ray.vector *i for i in Solution] 575 distances = [mgeo.Vector(i - self.r0).norm for i in IntersectionPoint] 576 IntersectionPoint = IntersectionPoint[distances.index(min(distances))] 577 OK = IntersectionPoint - self.r0 in self.support and np.abs(IntersectionPoint[2]-self.r0[2]) < self.majorradius 578 return IntersectionPoint, OK 579 580 def get_local_normal(self, Point): 581 """Return the normal unit vector on the toroidal surface at point Point in the reference frame of the mirror""" 582 x = Point[0] 583 y = Point[1] 584 z = Point[2] 585 A = self.majorradius**2 - self.minorradius**2 586 587 Gradient = Vector(np.zeros(3)) 588 Gradient[0] = ( 589 4 * (x**3 + x * y**2 + x * z**2 + x * A) 590 - 8 * x * self.majorradius**2 591 ) 592 Gradient[1] = 4 * (y**3 + y * x**2 + y * z**2 + y * A) 593 Gradient[2] = ( 594 4 * (z**3 + z * x**2 + z * y**2 + z * A) 595 - 8 * z * self.majorradius**2 596 ) 597 598 return -Gradient.normalized() 599 600 def _zfunc(self, PointArray): 601 x = PointArray[:,0] 602 y = PointArray[:,1] 603 return -np.sqrt( 604 (np.sqrt(self.minorradius**2 - y**2) + self.majorradius)**2 - x**2 605 )
Toroidal mirror surface with eqn.$(\sqrt{x^2+z^2}-R)^2 + y^2 = r^2$ where $R$ and $r$ the major and minor radii.
majorradius : float
Major radius of the toroid in mm.
minorradius : float
Minor radius of the toroid in mm.
support : ART.ModuleSupport.Support
type : str 'Toroidal Mirror'.
MirrorToroidal.get_normal(Point)
MirrorToroidal.get_centre()
MirrorToroidal.get_grid3D(NbPoints)
510 def __init__(self, Support, MajorRadius, MinorRadius, **kwargs): 511 """ 512 Construct a toroidal mirror. 513 514 Parameters 515 ---------- 516 MajorRadius : float 517 Major radius of the toroid in mm. 518 519 MinorRadius : float 520 Minor radius of the toroid in mm. 521 522 Support : ART.ModuleSupport.Support 523 524 """ 525 super().__init__() 526 self.support = Support 527 self.type = "Toroidal Mirror" 528 529 if "Surface" in kwargs: 530 self.Surface = kwargs["Surface"] 531 532 self.curvature = Curvature.CONCAVE 533 534 self.majorradius = MajorRadius 535 self.minorradius = MinorRadius 536 537 self.r0 = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 538 539 self.centre_ref = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 540 self.support_normal_ref = Vector([0, 0, 1.0]) 541 self.majoraxis_ref = Vector([1.0, 0, 0]) 542 543 self.add_global_vectors("support_normal", "majoraxis") 544 self.add_global_points("centre")
Construct a toroidal mirror.
MajorRadius : float
Major radius of the toroid in mm.
MinorRadius : float
Minor radius of the toroid in mm.
Support : ART.ModuleSupport.Support
580 def get_local_normal(self, Point): 581 """Return the normal unit vector on the toroidal surface at point Point in the reference frame of the mirror""" 582 x = Point[0] 583 y = Point[1] 584 z = Point[2] 585 A = self.majorradius**2 - self.minorradius**2 586 587 Gradient = Vector(np.zeros(3)) 588 Gradient[0] = ( 589 4 * (x**3 + x * y**2 + x * z**2 + x * A) 590 - 8 * x * self.majorradius**2 591 ) 592 Gradient[1] = 4 * (y**3 + y * x**2 + y * z**2 + y * A) 593 Gradient[2] = ( 594 4 * (z**3 + z * x**2 + z * y**2 + z * A) 595 - 8 * z * self.majorradius**2 596 ) 597 598 return -Gradient.normalized()
Return the normal unit vector on the toroidal surface at point Point in the reference frame of the mirror
234def _RenderMirror(Mirror, Npoints=1000, draw_support = False, draw_points = False, draw_vectors = True , recenter_support = True): 235 """ 236 This function renders a mirror in 3D. 237 """ 238 mesh = Mirror._Render(Npoints) 239 p = pv.Plotter() 240 p.add_mesh(mesh) 241 if draw_support: 242 support = Mirror.support._Render() 243 if recenter_support: 244 support.translate(Mirror.r0,inplace=True) 245 p.add_mesh(support, color="gray", opacity=0.5) 246 247 if draw_vectors: 248 # We draw the important vectors of the optical element 249 # For that, if we have a "vectors" attribute, we use that 250 # (a dictionary with the vector names as keys and the colors as values) 251 # Otherwise we use the default vectors: "support_normal", "majoraxis" 252 if hasattr(Mirror, "vectors"): 253 vectors = Mirror.vectors 254 else: 255 vectors = {"support_normal_ref": "red", "majoraxis_ref": "blue"} 256 for vector, color in vectors.items(): 257 if hasattr(Mirror, vector): 258 p.add_arrows(Mirror.r0, 10*getattr(Mirror, vector), color=color) 259 260 if draw_points: 261 # We draw the important points of the optical element 262 # For that, if we have a "points" attribute, we use that 263 # (a dictionary with the point names as keys and the colors as values) 264 # Otherwise we use the default points: "centre_ref" 265 if hasattr(Mirror, "points"): 266 points = Mirror.points 267 else: 268 points = {"centre_ref": "red"} 269 for point, color in points.items(): 270 if hasattr(Mirror, point): 271 p.add_mesh(pv.Sphere(radius=1, center=getattr(Mirror, point)), color=color) 272 else: 273 p.add_mesh(pv.Sphere(radius=1, center=Mirror.r0), color="red") 274 p.show() 275 return p
This function renders a mirror in 3D.
607def ReturnOptimalToroidalRadii( 608 Focal: float, AngleIncidence: float 609) -> (float, float): 610 """ 611 Get optimal parameters for a toroidal mirror. 612 613 Useful helper function to get the optimal major and minor radii for a toroidal mirror to achieve a 614 focal length 'Focal' with an angle of incidence 'AngleIncidence' and with vanishing astigmatism. 615 616 Parameters 617 ---------- 618 Focal : float 619 Focal length in mm. 620 621 AngleIncidence : int 622 Angle of incidence in degrees. 623 624 Returns 625 ------- 626 OptimalMajorRadius, OptimalMinorRadius : float, float. 627 """ 628 AngleIncidenceRadian = AngleIncidence * np.pi / 180 629 OptimalMajorRadius = ( 630 2 631 * Focal 632 * (1 / np.cos(AngleIncidenceRadian) - np.cos(AngleIncidenceRadian)) 633 ) 634 OptimalMinorRadius = 2 * Focal * np.cos(AngleIncidenceRadian) 635 return OptimalMajorRadius, OptimalMinorRadius
Get optimal parameters for a toroidal mirror.
Useful helper function to get the optimal major and minor radii for a toroidal mirror to achieve a focal length 'Focal' with an angle of incidence 'AngleIncidence' and with vanishing astigmatism.
Focal : float
Focal length in mm.
AngleIncidence : int
Angle of incidence in degrees.
OptimalMajorRadius, OptimalMinorRadius : float, float.
639class MirrorEllipsoidal(Mirror): 640 """ 641 Ellipdoidal mirror surface with eqn. $(x/a)^2 + (y/b)^2 + (z/b)^2 = 1$, where $a$ and $b$ are semi major and semi minor axes. 642 643  644 645 Attributes 646 ---------- 647 a : float 648 Semi major axis of the ellipsoid in mm. 649 650 b : float 651 Semi minor axis of the ellipsoid in mm. 652 653 support : ART.ModuleSupport.Support 654 655 type : str 'Ellipsoidal Mirror'. 656 657 Methods 658 ------- 659 MirrorEllipsoidal.get_normal(Point) 660 661 MirrorEllipsoidal.get_centre() 662 663 MirrorEllipsoidal.get_grid3D(NbPoints) 664 665 """ 666 667 def __init__( 668 self, 669 Support, 670 SemiMajorAxis=None, 671 SemiMinorAxis=None, 672 OffAxisAngle=None, 673 f_object=None, 674 f_image=None, 675 IncidenceAngle=None, 676 Magnification=None, 677 DistanceObjectImage=None, 678 Eccentricity=None, 679 **kwargs, 680 ): 681 """ 682 Generate an ellipsoidal mirror with given parameters. 683 684 The angles are given in degrees but converted to radians for internal calculations. 685 You can specify the semi-major and semi-minor axes, the off-axis angle, the object and image focal distances or some other parameters. 686 The constructor uses a magic calculator to determine the missing parameters. 687 688 Parameters 689 ---------- 690 Support : TYPE 691 ART.ModuleSupport.Support. 692 SemiMajorAxis : float (optional) 693 Semi major axis of the ellipsoid in mm.. 694 SemiMinorAxis : float (optional) 695 Semi minor axis of the ellipsoid in mm.. 696 OffAxisAngle : float (optional) 697 Off-axis angle of the mirror in mm. Defined as the angle at the centre of the mirror between the two foci.. 698 f_object : float (optional) 699 Object focal distance in mm. 700 f_image : float (optional) 701 Image focal distance in mm. 702 IncidenceAngle : float (optional) 703 Angle of incidence in degrees. 704 Magnification : float (optional) 705 Magnification. 706 DistanceObjectImage : float (optional) 707 Distance between object and image in mm. 708 Eccentricity : float (optional) 709 Eccentricity of the ellipsoid. 710 """ 711 super().__init__() 712 self.type = "Ellipsoidal Mirror" 713 self.support = Support 714 715 if "Surface" in kwargs: 716 self.Surface = kwargs["Surface"] 717 718 parameter_calculator = EllipsoidalMirrorCalculator() 719 values, steps = parameter_calculator.calculate_values(verify_consistency=True, 720 f1=f_object, 721 f2=f_image, 722 a=SemiMajorAxis, 723 b=SemiMinorAxis, 724 offset_angle=OffAxisAngle, 725 incidence_angle=IncidenceAngle, 726 m=Magnification, 727 l=DistanceObjectImage, 728 e=Eccentricity) 729 self.a = values["a"] 730 self.b = values["b"] 731 self._offaxisangle = np.deg2rad(values["offset_angle"]) 732 self._f_object = values["f1"] 733 self._f_image = values["f2"] 734 735 self.centre_ref = self._get_centre_ref() 736 737 self.r0 = self.centre_ref 738 739 self.support_normal_ref = Vector([0, 0, 1.0]) 740 self.majoraxis_ref = Vector([1.0, 0, 0]) 741 742 self.f1_ref = Point([-self.a, 0,0]) 743 self.f2_ref = Point([ self.a, 0,0]) 744 self.towards_image_ref = (self.f2_ref - self.centre_ref).normalized() 745 self.towards_object_ref = (self.f1_ref - self.centre_ref).normalized() 746 self.centre_normal_ref = self.get_local_normal(self.centre_ref) 747 748 self.add_global_points("f1", "f2", "centre") 749 self.add_global_vectors("towards_image", "towards_object", "centre_normal", "support_normal", "majoraxis") 750 751 def _get_intersection(self, Ray): 752 """Return the intersection point between Ray and the ellipsoidal mirror surface.""" 753 ux, uy, uz = Ray.vector 754 xA, yA, zA = Ray.point 755 756 da = (uy**2 + uz**2) / self.b**2 + (ux / self.a) ** 2 757 db = 2 * ((uy * yA + uz * zA) / self.b**2 + (ux * xA) / self.a**2) 758 dc = (yA**2 + zA**2) / self.b**2 + (xA / self.a) ** 2 - 1 759 760 Solution = mgeo.SolverQuadratic(da, db, dc) 761 Solution = mgeo.KeepPositiveSolution(Solution) 762 763 ListPointIntersection = [] 764 C = self.get_centre_ref() 765 for t in Solution: 766 Intersect = Ray.vector * t + Ray.point 767 if Intersect[2] < 0 and Intersect - C in self.support: 768 ListPointIntersection.append(Intersect) 769 770 return _IntersectionRayMirror(Ray.point, ListPointIntersection) 771 772 def get_local_normal(self, Point): 773 """Return the normal unit vector on the ellipsoidal surface at point Point.""" 774 Gradient = Vector(np.zeros(3)) 775 776 Gradient[0] = -Point[0] / self.a**2 777 Gradient[1] = -Point[1] / self.b**2 778 Gradient[2] = -Point[2] / self.b**2 779 780 return Gradient.normalized() 781 782 def _get_centre_ref(self): 783 """Return 3D coordinates of the point on the mirror surface at the center of its support.""" 784 foci = 2 * np.sqrt(self.a**2 - self.b**2) # distance between the foci 785 h = -foci / 2 / np.tan(self._offaxisangle) 786 R = np.sqrt(foci**2 / 4 + h**2) 787 sign = 1 788 if math.isclose(self._offaxisangle, np.pi / 2): 789 h = 0 790 elif self._offaxisangle > np.pi / 2: 791 h = -h 792 sign = -1 793 a = 1 - self.a**2 / self.b**2 794 b = -2 * h 795 c = self.a**2 + h**2 - R**2 796 z = (-b + sign * np.sqrt(b**2 - 4 * a * c)) / (2 * a) 797 if math.isclose(z**2, self.b**2): 798 return np.array([0, 0, -self.b]) 799 x = self.a * np.sqrt(1 - z**2 / self.b**2) 800 centre = Point([x, 0, sign * z]) 801 return centre 802 803 def _zfunc(self, PointArray): 804 x = PointArray[:,0] 805 y = PointArray[:,1] 806 x-= self.r0[0] 807 y-= self.r0[1] 808 z = -np.sqrt(1 - (x / self.a)**2 - (y / self.b)**2) 809 z += self.r0[2] 810 return z
Ellipdoidal mirror surface with eqn. $(x/a)^2 + (y/b)^2 + (z/b)^2 = 1$, where $a$ and $b$ are semi major and semi minor axes.
a : float
Semi major axis of the ellipsoid in mm.
b : float
Semi minor axis of the ellipsoid in mm.
support : ART.ModuleSupport.Support
type : str 'Ellipsoidal Mirror'.
MirrorEllipsoidal.get_normal(Point)
MirrorEllipsoidal.get_centre()
MirrorEllipsoidal.get_grid3D(NbPoints)
667 def __init__( 668 self, 669 Support, 670 SemiMajorAxis=None, 671 SemiMinorAxis=None, 672 OffAxisAngle=None, 673 f_object=None, 674 f_image=None, 675 IncidenceAngle=None, 676 Magnification=None, 677 DistanceObjectImage=None, 678 Eccentricity=None, 679 **kwargs, 680 ): 681 """ 682 Generate an ellipsoidal mirror with given parameters. 683 684 The angles are given in degrees but converted to radians for internal calculations. 685 You can specify the semi-major and semi-minor axes, the off-axis angle, the object and image focal distances or some other parameters. 686 The constructor uses a magic calculator to determine the missing parameters. 687 688 Parameters 689 ---------- 690 Support : TYPE 691 ART.ModuleSupport.Support. 692 SemiMajorAxis : float (optional) 693 Semi major axis of the ellipsoid in mm.. 694 SemiMinorAxis : float (optional) 695 Semi minor axis of the ellipsoid in mm.. 696 OffAxisAngle : float (optional) 697 Off-axis angle of the mirror in mm. Defined as the angle at the centre of the mirror between the two foci.. 698 f_object : float (optional) 699 Object focal distance in mm. 700 f_image : float (optional) 701 Image focal distance in mm. 702 IncidenceAngle : float (optional) 703 Angle of incidence in degrees. 704 Magnification : float (optional) 705 Magnification. 706 DistanceObjectImage : float (optional) 707 Distance between object and image in mm. 708 Eccentricity : float (optional) 709 Eccentricity of the ellipsoid. 710 """ 711 super().__init__() 712 self.type = "Ellipsoidal Mirror" 713 self.support = Support 714 715 if "Surface" in kwargs: 716 self.Surface = kwargs["Surface"] 717 718 parameter_calculator = EllipsoidalMirrorCalculator() 719 values, steps = parameter_calculator.calculate_values(verify_consistency=True, 720 f1=f_object, 721 f2=f_image, 722 a=SemiMajorAxis, 723 b=SemiMinorAxis, 724 offset_angle=OffAxisAngle, 725 incidence_angle=IncidenceAngle, 726 m=Magnification, 727 l=DistanceObjectImage, 728 e=Eccentricity) 729 self.a = values["a"] 730 self.b = values["b"] 731 self._offaxisangle = np.deg2rad(values["offset_angle"]) 732 self._f_object = values["f1"] 733 self._f_image = values["f2"] 734 735 self.centre_ref = self._get_centre_ref() 736 737 self.r0 = self.centre_ref 738 739 self.support_normal_ref = Vector([0, 0, 1.0]) 740 self.majoraxis_ref = Vector([1.0, 0, 0]) 741 742 self.f1_ref = Point([-self.a, 0,0]) 743 self.f2_ref = Point([ self.a, 0,0]) 744 self.towards_image_ref = (self.f2_ref - self.centre_ref).normalized() 745 self.towards_object_ref = (self.f1_ref - self.centre_ref).normalized() 746 self.centre_normal_ref = self.get_local_normal(self.centre_ref) 747 748 self.add_global_points("f1", "f2", "centre") 749 self.add_global_vectors("towards_image", "towards_object", "centre_normal", "support_normal", "majoraxis")
Generate an ellipsoidal mirror with given parameters.
The angles are given in degrees but converted to radians for internal calculations. You can specify the semi-major and semi-minor axes, the off-axis angle, the object and image focal distances or some other parameters. The constructor uses a magic calculator to determine the missing parameters.
Support : TYPE ART.ModuleSupport.Support. SemiMajorAxis : float (optional) Semi major axis of the ellipsoid in mm.. SemiMinorAxis : float (optional) Semi minor axis of the ellipsoid in mm.. OffAxisAngle : float (optional) Off-axis angle of the mirror in mm. Defined as the angle at the centre of the mirror between the two foci.. f_object : float (optional) Object focal distance in mm. f_image : float (optional) Image focal distance in mm. IncidenceAngle : float (optional) Angle of incidence in degrees. Magnification : float (optional) Magnification. DistanceObjectImage : float (optional) Distance between object and image in mm. Eccentricity : float (optional) Eccentricity of the ellipsoid.
772 def get_local_normal(self, Point): 773 """Return the normal unit vector on the ellipsoidal surface at point Point.""" 774 Gradient = Vector(np.zeros(3)) 775 776 Gradient[0] = -Point[0] / self.a**2 777 Gradient[1] = -Point[1] / self.b**2 778 Gradient[2] = -Point[2] / self.b**2 779 780 return Gradient.normalized()
Return the normal unit vector on the ellipsoidal surface at point Point.
234def _RenderMirror(Mirror, Npoints=1000, draw_support = False, draw_points = False, draw_vectors = True , recenter_support = True): 235 """ 236 This function renders a mirror in 3D. 237 """ 238 mesh = Mirror._Render(Npoints) 239 p = pv.Plotter() 240 p.add_mesh(mesh) 241 if draw_support: 242 support = Mirror.support._Render() 243 if recenter_support: 244 support.translate(Mirror.r0,inplace=True) 245 p.add_mesh(support, color="gray", opacity=0.5) 246 247 if draw_vectors: 248 # We draw the important vectors of the optical element 249 # For that, if we have a "vectors" attribute, we use that 250 # (a dictionary with the vector names as keys and the colors as values) 251 # Otherwise we use the default vectors: "support_normal", "majoraxis" 252 if hasattr(Mirror, "vectors"): 253 vectors = Mirror.vectors 254 else: 255 vectors = {"support_normal_ref": "red", "majoraxis_ref": "blue"} 256 for vector, color in vectors.items(): 257 if hasattr(Mirror, vector): 258 p.add_arrows(Mirror.r0, 10*getattr(Mirror, vector), color=color) 259 260 if draw_points: 261 # We draw the important points of the optical element 262 # For that, if we have a "points" attribute, we use that 263 # (a dictionary with the point names as keys and the colors as values) 264 # Otherwise we use the default points: "centre_ref" 265 if hasattr(Mirror, "points"): 266 points = Mirror.points 267 else: 268 points = {"centre_ref": "red"} 269 for point, color in points.items(): 270 if hasattr(Mirror, point): 271 p.add_mesh(pv.Sphere(radius=1, center=getattr(Mirror, point)), color=color) 272 else: 273 p.add_mesh(pv.Sphere(radius=1, center=Mirror.r0), color="red") 274 p.show() 275 return p
This function renders a mirror in 3D.
814class MirrorCylindrical(Mirror): 815 """ 816 Cylindrical mirror surface with eqn. $y^2 + z^2 = R^2$, where $R$ is the radius. 817 818 Attributes 819 ---------- 820 radius : float 821 Radius of curvature. A postive value for concave mirror, a negative value for a convex mirror. 822 823 support : ART.ModuleSupport.Support 824 825 type : str 'Cylindrical Mirror'. 826 827 Methods 828 ------- 829 MirrorCylindrical.get_normal(Point) 830 831 MirrorCylindrical.get_centre() 832 833 MirrorCylindrical.get_grid3D(NbPoints) 834 """ 835 836 def __init__(self, Support, Radius, **kwargs): 837 """ 838 Construct a cylindrical mirror. 839 840 Parameters 841 ---------- 842 Radius : float 843 The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror. 844 845 Support : ART.ModuleSupport.Support 846 847 """ 848 super().__init__() 849 if Radius < 0: 850 self.type = "CylindricalCX Mirror" 851 self.curvature = Curvature.CONVEX 852 self.radius = -Radius 853 else: 854 self.type = "CylindricalCC Mirror" 855 self.curvature = Curvature.CONCAVE 856 self.radius = Radius 857 858 self.support = Support 859 860 if "Surface" in kwargs: 861 self.Surface = kwargs["Surface"] 862 863 self.r0 = Point([0.0, 0.0, -self.radius]) 864 865 self.support_normal_ref = Vector([0, 0, 1.0]) 866 self.majoraxis_ref = Vector([1.0, 0, 0]) 867 self.centre_ref = Point([0.0, 0.0, -self.radius]) 868 869 self.add_global_points("centre") 870 self.add_global_vectors("support_normal", "majoraxis") 871 872 def _get_intersection(self, Ray): 873 """Return the intersection point between the Ray and the cylinder.""" 874 uy = Ray.vector[1] 875 uz = Ray.vector[2] 876 yA = Ray.point[1] 877 zA = Ray.point[2] 878 879 a = uy**2 + uz**2 880 b = 2 * (uy * yA + uz * zA) 881 c = yA**2 + zA**2 - self.radius**2 882 883 Solution = mgeo.SolverQuadratic(a, b, c) 884 Solution = mgeo.KeepPositiveSolution(Solution) 885 886 ListPointIntersection = [] 887 for t in Solution: 888 Intersect = Ray.vector * t + Ray.point 889 if Intersect[2] < 0 and Intersect in self.support: 890 ListPointIntersection.append(Intersect) 891 892 return _IntersectionRayMirror(Ray.point, ListPointIntersection) 893 894 def get_local_normal(self, Point): 895 """Return the normal unit vector on the cylinder surface at point P.""" 896 Gradient = Vector([0, -Point[1], -Point[2]]) 897 return Gradient.normalized() 898 899 def get_centre(self): 900 """Return 3D coordinates of the point on the mirror surface at the center of its support.""" 901 return Point([0, 0, -self.radius]) 902 903 def _zfunc(self, PointArray): 904 y = PointArray[:,1] 905 return -np.sqrt(self.radius**2 - y**2)
Cylindrical mirror surface with eqn. $y^2 + z^2 = R^2$, where $R$ is the radius.
radius : float
Radius of curvature. A postive value for concave mirror, a negative value for a convex mirror.
support : ART.ModuleSupport.Support
type : str 'Cylindrical Mirror'.
MirrorCylindrical.get_normal(Point)
MirrorCylindrical.get_centre()
MirrorCylindrical.get_grid3D(NbPoints)
836 def __init__(self, Support, Radius, **kwargs): 837 """ 838 Construct a cylindrical mirror. 839 840 Parameters 841 ---------- 842 Radius : float 843 The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror. 844 845 Support : ART.ModuleSupport.Support 846 847 """ 848 super().__init__() 849 if Radius < 0: 850 self.type = "CylindricalCX Mirror" 851 self.curvature = Curvature.CONVEX 852 self.radius = -Radius 853 else: 854 self.type = "CylindricalCC Mirror" 855 self.curvature = Curvature.CONCAVE 856 self.radius = Radius 857 858 self.support = Support 859 860 if "Surface" in kwargs: 861 self.Surface = kwargs["Surface"] 862 863 self.r0 = Point([0.0, 0.0, -self.radius]) 864 865 self.support_normal_ref = Vector([0, 0, 1.0]) 866 self.majoraxis_ref = Vector([1.0, 0, 0]) 867 self.centre_ref = Point([0.0, 0.0, -self.radius]) 868 869 self.add_global_points("centre") 870 self.add_global_vectors("support_normal", "majoraxis")
Construct a cylindrical mirror.
Radius : float
The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror.
Support : ART.ModuleSupport.Support
894 def get_local_normal(self, Point): 895 """Return the normal unit vector on the cylinder surface at point P.""" 896 Gradient = Vector([0, -Point[1], -Point[2]]) 897 return Gradient.normalized()
Return the normal unit vector on the cylinder surface at point P.
899 def get_centre(self): 900 """Return 3D coordinates of the point on the mirror surface at the center of its support.""" 901 return Point([0, 0, -self.radius])
Return 3D coordinates of the point on the mirror surface at the center of its support.
234def _RenderMirror(Mirror, Npoints=1000, draw_support = False, draw_points = False, draw_vectors = True , recenter_support = True): 235 """ 236 This function renders a mirror in 3D. 237 """ 238 mesh = Mirror._Render(Npoints) 239 p = pv.Plotter() 240 p.add_mesh(mesh) 241 if draw_support: 242 support = Mirror.support._Render() 243 if recenter_support: 244 support.translate(Mirror.r0,inplace=True) 245 p.add_mesh(support, color="gray", opacity=0.5) 246 247 if draw_vectors: 248 # We draw the important vectors of the optical element 249 # For that, if we have a "vectors" attribute, we use that 250 # (a dictionary with the vector names as keys and the colors as values) 251 # Otherwise we use the default vectors: "support_normal", "majoraxis" 252 if hasattr(Mirror, "vectors"): 253 vectors = Mirror.vectors 254 else: 255 vectors = {"support_normal_ref": "red", "majoraxis_ref": "blue"} 256 for vector, color in vectors.items(): 257 if hasattr(Mirror, vector): 258 p.add_arrows(Mirror.r0, 10*getattr(Mirror, vector), color=color) 259 260 if draw_points: 261 # We draw the important points of the optical element 262 # For that, if we have a "points" attribute, we use that 263 # (a dictionary with the point names as keys and the colors as values) 264 # Otherwise we use the default points: "centre_ref" 265 if hasattr(Mirror, "points"): 266 points = Mirror.points 267 else: 268 points = {"centre_ref": "red"} 269 for point, color in points.items(): 270 if hasattr(Mirror, point): 271 p.add_mesh(pv.Sphere(radius=1, center=getattr(Mirror, point)), color=color) 272 else: 273 p.add_mesh(pv.Sphere(radius=1, center=Mirror.r0), color="red") 274 p.show() 275 return p
This function renders a mirror in 3D.
912class GrazingParabola(Mirror): 913 r""" 914 A parabolic mirror with a support parallel to the surface at the optical center. 915 Needs to be completed, currently not functional. 916 TODO 917 """ 918 919 def __init__(self, Support, 920 FocalEffective: float=None, 921 OffAxisAngle: float = None, 922 FocalParent: float = None, 923 RadiusParent: float = None, 924 OffAxisDistance: float = None, 925 MoreThan90: bool = None, 926 **kwargs): 927 """ 928 Initialise a Parabolic mirror. 929 930 Parameters 931 ---------- 932 FocalEffective : float 933 Effective focal length of the parabola in mm. 934 935 OffAxisAngle : float 936 Off-axis angle *in degrees* of the parabola. 937 938 Support : ART.ModuleSupport.Support 939 940 """ 941 super().__init__() 942 self.curvature = Curvature.CONCAVE 943 self.support = Support 944 self.type = "Grazing Parabolic Mirror" 945 946 if "Surface" in kwargs: 947 self.Surface = kwargs["Surface"] 948 949 parameter_calculator = OAP_calculator() 950 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 951 fs=FocalEffective, 952 theta=OffAxisAngle, 953 fp=FocalParent, 954 Rc=RadiusParent, 955 OAD=OffAxisDistance, 956 more_than_90=MoreThan90) 957 self._offaxisangle = values["theta"] 958 self._feff = values["fs"] 959 self._p = values["p"] 960 self._offaxisdistance = values["OAD"] 961 self._fparent = values["fp"] 962 self._rparent = values["Rc"] 963 964 #self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 965 self.r0 = Point([0, 0, 0]) 966 967 self.centre_ref = Point([0, 0, 0]) 968 self.support_normal_ref = Vector([0, 0, 1.0]) 969 self.majoraxis_ref = Vector([1.0, 0, 0]) 970 971 focus_ref = Point([np.sqrt(self._feff**2 - self._offaxisdistance**2), 0, self._offaxisdistance]) # frame where x is as collimated direction 972 # We need to rotate it in the trigonometric direction by the 90°-theta/2 973 angle = np.pi/2 - self._offaxisangle/2 974 self.focus_ref = Point([focus_ref[0]*np.cos(angle)+focus_ref[2]*np.sin(angle),focus_ref[1], -focus_ref[0]*np.sin(angle)+focus_ref[2]*np.cos(angle), ]) 975 976 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 977 towards_collimated_ref = Vector([-1.0, 0, 0]) # Again, we need to rotate it by the same angle 978 self.collimated_ref = Point([-np.cos(angle), 0, np.sin(angle)]) 979 980 self.add_global_points("focus", "centre") 981 self.add_global_vectors("towards_focusing", "towards_collimated", "support_normal", "majoraxis") 982 983 def _get_intersection(self, Ray): 984 """ 985 Return the intersection point between the ray and the parabola. 986 """ 987 pass 988 989 def get_local_normal(self, Point): 990 """ 991 Return the normal unit vector on the paraboloid surface at point Point. 992 """ 993 pass 994 995 def _zfunc(self, PointArray): 996 pass
A parabolic mirror with a support parallel to the surface at the optical center. Needs to be completed, currently not functional. TODO
919 def __init__(self, Support, 920 FocalEffective: float=None, 921 OffAxisAngle: float = None, 922 FocalParent: float = None, 923 RadiusParent: float = None, 924 OffAxisDistance: float = None, 925 MoreThan90: bool = None, 926 **kwargs): 927 """ 928 Initialise a Parabolic mirror. 929 930 Parameters 931 ---------- 932 FocalEffective : float 933 Effective focal length of the parabola in mm. 934 935 OffAxisAngle : float 936 Off-axis angle *in degrees* of the parabola. 937 938 Support : ART.ModuleSupport.Support 939 940 """ 941 super().__init__() 942 self.curvature = Curvature.CONCAVE 943 self.support = Support 944 self.type = "Grazing Parabolic Mirror" 945 946 if "Surface" in kwargs: 947 self.Surface = kwargs["Surface"] 948 949 parameter_calculator = OAP_calculator() 950 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 951 fs=FocalEffective, 952 theta=OffAxisAngle, 953 fp=FocalParent, 954 Rc=RadiusParent, 955 OAD=OffAxisDistance, 956 more_than_90=MoreThan90) 957 self._offaxisangle = values["theta"] 958 self._feff = values["fs"] 959 self._p = values["p"] 960 self._offaxisdistance = values["OAD"] 961 self._fparent = values["fp"] 962 self._rparent = values["Rc"] 963 964 #self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 965 self.r0 = Point([0, 0, 0]) 966 967 self.centre_ref = Point([0, 0, 0]) 968 self.support_normal_ref = Vector([0, 0, 1.0]) 969 self.majoraxis_ref = Vector([1.0, 0, 0]) 970 971 focus_ref = Point([np.sqrt(self._feff**2 - self._offaxisdistance**2), 0, self._offaxisdistance]) # frame where x is as collimated direction 972 # We need to rotate it in the trigonometric direction by the 90°-theta/2 973 angle = np.pi/2 - self._offaxisangle/2 974 self.focus_ref = Point([focus_ref[0]*np.cos(angle)+focus_ref[2]*np.sin(angle),focus_ref[1], -focus_ref[0]*np.sin(angle)+focus_ref[2]*np.cos(angle), ]) 975 976 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 977 towards_collimated_ref = Vector([-1.0, 0, 0]) # Again, we need to rotate it by the same angle 978 self.collimated_ref = Point([-np.cos(angle), 0, np.sin(angle)]) 979 980 self.add_global_points("focus", "centre") 981 self.add_global_vectors("towards_focusing", "towards_collimated", "support_normal", "majoraxis")
Initialise a Parabolic mirror.
FocalEffective : float
Effective focal length of the parabola in mm.
OffAxisAngle : float
Off-axis angle *in degrees* of the parabola.
Support : ART.ModuleSupport.Support
989 def get_local_normal(self, Point): 990 """ 991 Return the normal unit vector on the paraboloid surface at point Point. 992 """ 993 pass
Return the normal unit vector on the paraboloid surface at point Point.
1032def ReflectionMirrorRayList(Mirror, ListRay, IgnoreDefects=False): 1033 """ 1034 Return the the reflected rays according to the law of reflection for the list of incident rays ListRay. 1035 1036 Rays that do not hit the support are not further propagated. 1037 1038 Updates the reflected rays' incidence angle and path. 1039 1040 Parameters 1041 ---------- 1042 Mirror : Mirror-object 1043 1044 ListRay : list[Ray-object] 1045 1046 """ 1047 Deformed = type(Mirror) == DeformedMirror 1048 ListRayReflected = [] 1049 for k in ListRay: 1050 PointMirror = Mirror._get_intersection(k) 1051 1052 if PointMirror is not None: 1053 if Deformed and IgnoreDefects: 1054 M = Mirror.Mirror 1055 else: 1056 M = Mirror 1057 RayReflected = _ReflectionMirrorRay(M, PointMirror, k) 1058 ListRayReflected.append(RayReflected) 1059 return ListRayReflected
Return the the reflected rays according to the law of reflection for the list of incident rays ListRay.
Rays that do not hit the support are not further propagated.
Updates the reflected rays' incidence angle and path.
Mirror : Mirror-object
ListRay : list[Ray-object]
1063class DeformedMirror(Mirror): 1064 def __init__(self, Mirror, DeformationList): 1065 self.Mirror = Mirror 1066 self.DeformationList = DeformationList 1067 self.type = Mirror.type 1068 self.support = self.Mirror.support 1069 1070 def get_local_normal(self, PointMirror): 1071 base_normal = self.Mirror.get_normal(PointMirror) 1072 C = self.get_centre() 1073 defects_normals = [ 1074 d.get_normal(PointMirror - C) for d in self.DeformationList 1075 ] 1076 for i in defects_normals: 1077 base_normal = mgeo.normal_add(base_normal, i) 1078 base_normal /= np.linalg.norm(base_normal) 1079 return base_normal 1080 1081 def get_centre(self): 1082 return self.Mirror.get_centre() 1083 1084 def get_grid3D(self, NbPoint, **kwargs): 1085 return self.Mirror.get_grid3D(NbPoint, **kwargs) 1086 1087 def _get_intersection(self, Ray): 1088 Intersect = self.Mirror._get_intersection(Ray) 1089 if Intersect is not None: 1090 h = sum( 1091 D.get_offset(Intersect - self.get_centre()) 1092 for D in self.DeformationList 1093 ) 1094 alpha = mgeo.AngleBetweenTwoVectors( 1095 -Ray.vector, self.Mirror.get_normal(Intersect) 1096 ) 1097 Intersect -= Ray.vector * h / np.cos(alpha) 1098 return Intersect
Abstract base class for mirrors.
1070 def get_local_normal(self, PointMirror): 1071 base_normal = self.Mirror.get_normal(PointMirror) 1072 C = self.get_centre() 1073 defects_normals = [ 1074 d.get_normal(PointMirror - C) for d in self.DeformationList 1075 ] 1076 for i in defects_normals: 1077 base_normal = mgeo.normal_add(base_normal, i) 1078 base_normal /= np.linalg.norm(base_normal) 1079 return base_normal
This method should return the normal unit vector in point 'Point' on the mirror surface. The normal is in the reference frame of the mirror.
Provides the class OpticalChain, which represents the whole optical setup to be simulated, from the bundle of source-Rays through the successive OpticalElements.
Created in Jan 2022
@author: Stefan Haessler
1""" 2Provides the class OpticalChain, which represents the whole optical setup to be simulated, 3from the bundle of source-[Rays](ModuleOpticalRay.html) through the successive [OpticalElements](ModuleOpticalElement.html). 4 5 6 7 8 9Created in Jan 2022 10 11@author: Stefan Haessler 12""" 13# %% Modules 14import copy 15import numpy as np 16import logging 17 18import ARTcore.ModuleProcessing as mp 19import ARTcore.ModuleGeometry as mgeo 20import ARTcore.ModuleOpticalRay as mray 21import ARTcore.ModuleOpticalElement as moe 22import ARTcore.ModuleDetector as mdet 23import ARTcore.ModuleSource as msource 24 25logger = logging.getLogger(__name__) 26 27# %% 28class OpticalChain: 29 """ 30 The OpticalChain represents the whole optical setup to be simulated: 31 Its main attributes are a list source-[Rays](ModuleOpticalRay.html) and 32 a list of successive [OpticalElements](ModuleOpticalElement.html). 33 34 The method OpticalChain.get_output_rays() returns an associated list of lists of 35 [Rays](ModuleOpticalRay.html), each calculated by ray-tracing from one 36 [OpticalElement](ModuleOpticalElement.html) to the next. 37 So OpticalChain.get_output_rays()[i] is the bundle of [Rays](ModuleOpticalRay.html) *after* 38 optical_elements[i]. 39 40 The string "description" can contain a short description of the optical setup, or similar notes. 41 42 The OpticalChain can be visualized quickly with the method OpticalChain.quickshow(), 43 and more nicely with OpticalChain.render(). 44 45 The class also provides methods for (mis-)alignment of the source-[Ray](ModuleOpticalRay.html)-bundle and the 46 [OpticalElements](ModuleOpticalElement.html). 47 48 Attributes 49 ---------- 50 source_rays : list[mray.Ray] 51 List of source rays, which are to be traced. 52 53 optical_elements : list[moe.OpticalElement] 54 List of successive optical elements. 55 56 detector: mdet.Detector (optional) 57 The detector (or list of detectors) to analyse the results. 58 59 description : str 60 A string to describe the optical setup. 61 Methods 62 ---------- 63 copy_chain() 64 65 get_output_rays() 66 67 render() 68 69 ---------- 70 71 shift_source(axis, distance) 72 73 tilt_source(self, axis, angle) 74 75 ---------- 76 77 rotate_OE(OEindx, axis, angle) 78 79 shift_OE(OEindx, axis, distance) 80 81 """ 82 83 def __init__(self, source_rays, optical_elements, detectors, description=""): 84 """ 85 Parameters 86 ---------- 87 source_rays : list[mray.Ray] 88 List of source rays, which are to be traced. 89 90 optical_elements : list[moe.OpticalElement] 91 List of successive optical elements. 92 93 detector: mdet.Detector (optional) 94 The detector (or list of detectors) to analyse the results. 95 96 description : str, optional 97 A string to describe the optical setup. Defaults to ''. 98 99 """ 100 self.source_rays = copy.deepcopy(source_rays) 101 # deepcopy so this object doesn't get changed when the global source_rays change "outside" 102 self.optical_elements = copy.deepcopy(optical_elements) 103 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 104 self.detectors = detectors 105 if isinstance(detectors, mdet.Detector): 106 self.detectors = {"Focus": detectors} 107 self.description = description 108 self._output_rays = None 109 self._last_source_rays_hash = None 110 self._last_optical_elements_hash = None 111 112 def __repr__(self): 113 pretty_str = "Optical setup [OpticalChain]:\n" 114 pretty_str += f" - Description: {self.description}\n" if self.description else "Description: Not provided.\n" 115 pretty_str += " - Contains the following elements:\n" 116 pretty_str += f" - Source with {len(self.source_rays)} rays at coordinate origin\n" 117 prev_pos = np.zeros(3) 118 for i, element in enumerate(self.optical_elements): 119 dist = (element.position - prev_pos).norm 120 if i == 0: 121 prev = "source" 122 else: 123 prev = f"element {i-1}" 124 pretty_str += f" - Element {i}: {element.type} at distance {round(dist)} from {prev}\n" 125 prev_pos = element.position 126 for i in self.detectors.keys(): 127 detector = self.detectors[i] 128 pretty_str += f' - Detector "{i}": {detector.type} at distance {round(detector.distance,3)} from element {detector.index % len(self.optical_elements)}\n' 129 return pretty_str 130 131 def __getitem__(self, i): 132 return self.optical_elements[i] 133 134 def __len__(self): 135 return len(self.optical_elements) 136 137 @property 138 def source_rays(self): 139 return self._source_rays 140 141 @source_rays.setter 142 def source_rays(self, source_rays): 143 if type(source_rays) == mray.RayList: 144 self._source_rays = source_rays 145 else: 146 raise TypeError("Source_rays must be a RayList object.") 147 148 @property 149 def optical_elements(self): 150 return self._optical_elements 151 152 @optical_elements.setter 153 def optical_elements(self, optical_elements): 154 if type(optical_elements) == list and all(isinstance(x, moe.OpticalElement) for x in optical_elements): 155 self._optical_elements = optical_elements 156 else: 157 raise TypeError("Optical_elements must be list of OpticalElement-objects.") 158 159 # %% METHODS ################################################################## 160 161 def __copy__(self): 162 """Return another optical chain with the same source, optical elements and description-string as this one.""" 163 return OpticalChain(self.source_rays, self.optical_elements, self.detectors, self.description) 164 165 def __deepcopy__(self, memo): 166 """Return another optical chain with the same source, optical elements and description-string as this one.""" 167 return OpticalChain(copy.deepcopy(self.source_rays), copy.deepcopy(self.optical_elements), memo+"\n"+copy.copy(self.description)) 168 169 def get_input_rays(self): 170 """ 171 Returns the list of source rays. 172 """ 173 return self.source_rays 174 175 def get_output_rays(self, force=False, **kwargs): 176 """ 177 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 178 or if the source-ray-bundle or anything about the optical elements has changed. 179 180 This is the user-facing method to perform the ray-tracing calculation. 181 """ 182 current_source_rays_hash = hash(self.source_rays) 183 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 184 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 185 print("...ray-tracing...", end="", flush=True) 186 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 187 print( 188 "\r\033[K", end="", flush=True 189 ) # move to beginning of the line with \r and then delete the whole line with \033[K 190 self._last_source_rays_hash = current_source_rays_hash 191 self._last_optical_elements_hash = current_optical_elements_hash 192 193 return self._output_rays 194 195 def get2dPoints(self, Detector = "Focus"): 196 """ 197 Returns the 2D points of the detected rays on the detector. 198 """ 199 if isinstance(Detector, str): 200 if Detector in self.detectors.keys(): 201 Detector = self.detectors[Detector] 202 else: 203 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 204 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0] 205 206 def get3dPoints(self, Detector = "Focus"): 207 """ 208 Returns the 3D points of the detected rays on the detector. 209 """ 210 if isinstance(Detector, str): 211 if Detector in self.detectors.keys(): 212 Detector = self.detectors[Detector] 213 else: 214 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 215 return Detector.get_3D_points(self.get_output_rays()[Detector.index]) 216 217 def getDelays(self, Detector = "Focus"): 218 """ 219 Returns the delays of the detected rays on the detector. 220 """ 221 if isinstance(Detector, str): 222 if Detector in self.detectors.keys(): 223 Detector = self.detectors[Detector] 224 else: 225 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 226 return Detector.get_Delays(self.get_output_rays()[Detector.index]) 227 228 # %% methods to (mis-)align the optical chain; just uses the corresponding methods of the OpticalElement class... 229 230 def shift_source(self, axis: str|np.ndarray, distance: float): 231 """ 232 Shift source ray bundle by distance (in mm) along the 'axis' specified as 233 a lab-frame vector (numpy-array of length 3) or as one of the strings 234 "vert", "horiz", or "random". 235 236 In the latter case, the reference is the incidence plane of the first 237 non-normal-incidence mirror after the source. If there is none, you will 238 be asked to rather specify the axis as a 3D-numpy-array. 239 240 axis = "vert" means the source position is shifted along the axis perpendicular 241 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 242 243 axis = "horiz" means the source direciton is translated along thr axis in that 244 incidence plane and perpendicular to the current source direction, 245 i.e. "horizontally" in the incidence plane, but retaining the same distance 246 of source and first optical element. 247 248 axis = "random" means the the source direction shifted in a random direction 249 within in the plane perpendicular to the current source direction, 250 e.g. simulating a fluctuation of hte transverse source position. 251 252 Parameters 253 ---------- 254 axis : np.ndarray or str 255 Shift axis, specified either as a 3D lab-frame vector or as one 256 of the strings "vert", "horiz", or "random". 257 258 distance : float 259 Shift distance in mm. 260 261 Returns 262 ------- 263 Nothing, just modifies the property 'source_rays'. 264 """ 265 if type(distance) not in [int, float, np.float64]: 266 raise ValueError('The "distance"-argument must be an int or float number.') 267 268 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 269 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 270 271 OEnormal = None 272 for i in mirror_indcs: 273 ith_OEnormal = self.optical_elements[i].normal 274 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 275 OEnormal = ith_OEnormal 276 break 277 if OEnormal is None: 278 raise Exception( 279 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 280 so you should rather give 'axis' as a numpy-array of length 3." 281 ) 282 283 if type(axis) == np.ndarray and len(axis) == 3: 284 translation_vector = axis 285 else: 286 perp_axis = np.cross(central_ray_vector, OEnormal) 287 horiz_axis = np.cross(perp_axis, central_ray_vector) 288 289 if axis == "vert": 290 translation_vector = perp_axis 291 elif axis == "horiz": 292 translation_vector = horiz_axis 293 elif axis == "random": 294 translation_vector = ( 295 np.random.uniform(low=-1, high=1, size=1) * perp_axis 296 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 297 ) 298 else: 299 raise ValueError( 300 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 301 ) 302 303 self.source_rays = mgeo.TranslationRayList(self.source_rays, distance * mgeo.Normalize(translation_vector)) 304 305 def tilt_source(self, axis: str|np.ndarray, angle: float): 306 """ 307 Rotate source ray bundle by angle around an axis, specified as 308 a lab-frame vector (numpy-array of length 3) or as one of the strings 309 "in_plane", "out_plane" or "random" direction. 310 311 In the latter case, the function considers the incidence plane of the first 312 non-normal-incidence mirror after the source. If there is none, you will 313 be asked to rather specify the axis as a 3D-numpy-array. 314 315 axis = "in_plane" means the source direction is rotated about an axis 316 perpendicular to that incidence plane, which tilts the source 317 "horizontally" in the same plane. 318 319 axis = "out_plane" means the source direciton is rotated about an axis 320 in that incidence plane and perpendicular to the current source direction, 321 which tilts the source "vertically" out of the former incidence plane. 322 323 axis = "random" means the the source direction is tilted in a random direction, 324 e.g. simulating a beam pointing fluctuation. 325 326 Attention, "angle" is given in deg, so as to remain consitent with the 327 conventions of other functions, although pointing is mostly talked about 328 in mrad instead. 329 330 Parameters 331 ---------- 332 axis : np.ndarray or str 333 Shift axis, specified either as a 3D lab-frame vector or as one 334 of the strings "in_plane", "out_plane", or "random". 335 336 angle : float 337 Rotation angle in degree. 338 339 Returns 340 ------- 341 Nothing, just modifies the property 'source_rays'. 342 """ 343 if type(angle) not in [int, float, np.float64]: 344 raise ValueError('The "angle"-argument must be an int or float number.') 345 346 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 347 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 348 349 OEnormal = None 350 for i in mirror_indcs: 351 ith_OEnormal = self.optical_elements[i].normal 352 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 353 OEnormal = ith_OEnormal 354 break 355 if OEnormal is None: 356 raise Exception( 357 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 358 so you should rather give 'axis' as a numpy-array of length 3." 359 ) 360 361 if type(axis) == np.ndarray and len(axis) == 3: 362 rot_axis = axis 363 else: 364 rot_axis_in = np.cross(central_ray_vector, OEnormal) 365 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 366 if axis == "in_plane": 367 rot_axis = rot_axis_in 368 elif axis == "out_plane": 369 rot_axis = rot_axis_out 370 elif axis == "random": 371 rot_axis = ( 372 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 373 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 374 ) 375 else: 376 raise ValueError( 377 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 378 ) 379 380 self.source_rays = mgeo.RotationAroundAxisRayList(self.source_rays, rot_axis, np.deg2rad(angle)) 381 382 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 383 """ 384 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 385 OEstart and OEstop (both included). 386 """ 387 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 388 outray = self.get_output_rays()[OEstart-1][0] 389 print(outray) 390 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 391 self.optical_elements[OEstart:OEstop] = new_elements 392 393 # %% 394 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 395 """ 396 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 397 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 398 that takes either of the two values: 399 - "in" 400 - "out" 401 In either case the "axis" can take these values: 402 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 403 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 404 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 405 406 Parameters 407 ---------- 408 OEindx : int 409 Index of the optical element to modify out of OpticalChain.optical_elements. 410 411 ref : str 412 Reference ray used to define the axes of rotation. Can be either: 413 "in" or "out" or "local_normal" (in+out) 414 415 axis : str 416 Rotation axis, specified as one of the strings 417 "pitch", "roll", "yaw" 418 419 angle : float 420 Rotation angle in degree. 421 422 Returns 423 ------- 424 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 425 """ 426 if abs(OEindx) > len(self.optical_elements): 427 raise ValueError( 428 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 429 ) 430 if type(angle) not in [int, float, np.float64]: 431 raise ValueError('The "angle"-argument must be an int or float number.') 432 MasterRay = [mp.FindCentralRay(self.source_rays)] 433 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 434 TracedRay = [MasterRay] + TracedRay 435 match ref: 436 case "out": 437 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 438 case "in": 439 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 440 case "localnormal": 441 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 442 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 443 RefVec = mgeo.Normalize(Out-In) 444 case _: 445 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 446 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 447 match axis: 448 case "pitch": 449 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 450 case "roll": 451 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 452 case "yaw": 453 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 454 case _: 455 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 456 else: 457 #If the normal vector is aligned with the ray 458 match axis: 459 case "pitch": 460 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 461 case "roll": 462 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 463 case "yaw": 464 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 465 case _: 466 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 467 468 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 469 """ 470 Shift the optical element OpticalChain.optical_elements[OEindx] along 471 axis referenced to the master ray. 472 473 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 474 that takes either of the two values: 475 - "in" 476 - "out" 477 In either case the "axis" can take these values: 478 - "along": Translation along the master ray. 479 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 480 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 481 482 Parameters 483 ---------- 484 OEindx : int 485 Index of the optical element to modify out of OpticalChain.optical_elements. 486 487 ref : str 488 Reference ray used to define the axes of rotation. Can be either: 489 "in" or "out". 490 491 axis : str 492 Translation axis, specified as one of the strings 493 "along", "in_plane", "out_plane". 494 495 distance : float 496 Rotation angle in degree. 497 498 Returns 499 ------- 500 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 501 """ 502 if abs(OEindx) >= len(self.optical_elements): 503 raise ValueError( 504 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 505 ) 506 if type(distance) not in [int, float, np.float64]: 507 raise ValueError('The "dist"-argument must be an int or float number.') 508 509 MasterRay = [mp.FindCentralRay(self.source_rays)] 510 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 511 TracedRay = [MasterRay] + TracedRay 512 match ref: 513 case "out": 514 RefVec = TracedRay[OEindx+1][0].vector 515 case "in": 516 RefVec = TracedRay[OEindx][0].vector 517 case _: 518 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 519 match axis: 520 case "along": 521 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 522 case "in_plane": 523 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 524 case "out_plane": 525 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 526 case _: 527 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 528 529 # some function that randomly misalings one, or several or all ?
29class OpticalChain: 30 """ 31 The OpticalChain represents the whole optical setup to be simulated: 32 Its main attributes are a list source-[Rays](ModuleOpticalRay.html) and 33 a list of successive [OpticalElements](ModuleOpticalElement.html). 34 35 The method OpticalChain.get_output_rays() returns an associated list of lists of 36 [Rays](ModuleOpticalRay.html), each calculated by ray-tracing from one 37 [OpticalElement](ModuleOpticalElement.html) to the next. 38 So OpticalChain.get_output_rays()[i] is the bundle of [Rays](ModuleOpticalRay.html) *after* 39 optical_elements[i]. 40 41 The string "description" can contain a short description of the optical setup, or similar notes. 42 43 The OpticalChain can be visualized quickly with the method OpticalChain.quickshow(), 44 and more nicely with OpticalChain.render(). 45 46 The class also provides methods for (mis-)alignment of the source-[Ray](ModuleOpticalRay.html)-bundle and the 47 [OpticalElements](ModuleOpticalElement.html). 48 49 Attributes 50 ---------- 51 source_rays : list[mray.Ray] 52 List of source rays, which are to be traced. 53 54 optical_elements : list[moe.OpticalElement] 55 List of successive optical elements. 56 57 detector: mdet.Detector (optional) 58 The detector (or list of detectors) to analyse the results. 59 60 description : str 61 A string to describe the optical setup. 62 Methods 63 ---------- 64 copy_chain() 65 66 get_output_rays() 67 68 render() 69 70 ---------- 71 72 shift_source(axis, distance) 73 74 tilt_source(self, axis, angle) 75 76 ---------- 77 78 rotate_OE(OEindx, axis, angle) 79 80 shift_OE(OEindx, axis, distance) 81 82 """ 83 84 def __init__(self, source_rays, optical_elements, detectors, description=""): 85 """ 86 Parameters 87 ---------- 88 source_rays : list[mray.Ray] 89 List of source rays, which are to be traced. 90 91 optical_elements : list[moe.OpticalElement] 92 List of successive optical elements. 93 94 detector: mdet.Detector (optional) 95 The detector (or list of detectors) to analyse the results. 96 97 description : str, optional 98 A string to describe the optical setup. Defaults to ''. 99 100 """ 101 self.source_rays = copy.deepcopy(source_rays) 102 # deepcopy so this object doesn't get changed when the global source_rays change "outside" 103 self.optical_elements = copy.deepcopy(optical_elements) 104 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 105 self.detectors = detectors 106 if isinstance(detectors, mdet.Detector): 107 self.detectors = {"Focus": detectors} 108 self.description = description 109 self._output_rays = None 110 self._last_source_rays_hash = None 111 self._last_optical_elements_hash = None 112 113 def __repr__(self): 114 pretty_str = "Optical setup [OpticalChain]:\n" 115 pretty_str += f" - Description: {self.description}\n" if self.description else "Description: Not provided.\n" 116 pretty_str += " - Contains the following elements:\n" 117 pretty_str += f" - Source with {len(self.source_rays)} rays at coordinate origin\n" 118 prev_pos = np.zeros(3) 119 for i, element in enumerate(self.optical_elements): 120 dist = (element.position - prev_pos).norm 121 if i == 0: 122 prev = "source" 123 else: 124 prev = f"element {i-1}" 125 pretty_str += f" - Element {i}: {element.type} at distance {round(dist)} from {prev}\n" 126 prev_pos = element.position 127 for i in self.detectors.keys(): 128 detector = self.detectors[i] 129 pretty_str += f' - Detector "{i}": {detector.type} at distance {round(detector.distance,3)} from element {detector.index % len(self.optical_elements)}\n' 130 return pretty_str 131 132 def __getitem__(self, i): 133 return self.optical_elements[i] 134 135 def __len__(self): 136 return len(self.optical_elements) 137 138 @property 139 def source_rays(self): 140 return self._source_rays 141 142 @source_rays.setter 143 def source_rays(self, source_rays): 144 if type(source_rays) == mray.RayList: 145 self._source_rays = source_rays 146 else: 147 raise TypeError("Source_rays must be a RayList object.") 148 149 @property 150 def optical_elements(self): 151 return self._optical_elements 152 153 @optical_elements.setter 154 def optical_elements(self, optical_elements): 155 if type(optical_elements) == list and all(isinstance(x, moe.OpticalElement) for x in optical_elements): 156 self._optical_elements = optical_elements 157 else: 158 raise TypeError("Optical_elements must be list of OpticalElement-objects.") 159 160 # %% METHODS ################################################################## 161 162 def __copy__(self): 163 """Return another optical chain with the same source, optical elements and description-string as this one.""" 164 return OpticalChain(self.source_rays, self.optical_elements, self.detectors, self.description) 165 166 def __deepcopy__(self, memo): 167 """Return another optical chain with the same source, optical elements and description-string as this one.""" 168 return OpticalChain(copy.deepcopy(self.source_rays), copy.deepcopy(self.optical_elements), memo+"\n"+copy.copy(self.description)) 169 170 def get_input_rays(self): 171 """ 172 Returns the list of source rays. 173 """ 174 return self.source_rays 175 176 def get_output_rays(self, force=False, **kwargs): 177 """ 178 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 179 or if the source-ray-bundle or anything about the optical elements has changed. 180 181 This is the user-facing method to perform the ray-tracing calculation. 182 """ 183 current_source_rays_hash = hash(self.source_rays) 184 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 185 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 186 print("...ray-tracing...", end="", flush=True) 187 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 188 print( 189 "\r\033[K", end="", flush=True 190 ) # move to beginning of the line with \r and then delete the whole line with \033[K 191 self._last_source_rays_hash = current_source_rays_hash 192 self._last_optical_elements_hash = current_optical_elements_hash 193 194 return self._output_rays 195 196 def get2dPoints(self, Detector = "Focus"): 197 """ 198 Returns the 2D points of the detected rays on the detector. 199 """ 200 if isinstance(Detector, str): 201 if Detector in self.detectors.keys(): 202 Detector = self.detectors[Detector] 203 else: 204 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 205 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0] 206 207 def get3dPoints(self, Detector = "Focus"): 208 """ 209 Returns the 3D points of the detected rays on the detector. 210 """ 211 if isinstance(Detector, str): 212 if Detector in self.detectors.keys(): 213 Detector = self.detectors[Detector] 214 else: 215 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 216 return Detector.get_3D_points(self.get_output_rays()[Detector.index]) 217 218 def getDelays(self, Detector = "Focus"): 219 """ 220 Returns the delays of the detected rays on the detector. 221 """ 222 if isinstance(Detector, str): 223 if Detector in self.detectors.keys(): 224 Detector = self.detectors[Detector] 225 else: 226 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 227 return Detector.get_Delays(self.get_output_rays()[Detector.index]) 228 229 # %% methods to (mis-)align the optical chain; just uses the corresponding methods of the OpticalElement class... 230 231 def shift_source(self, axis: str|np.ndarray, distance: float): 232 """ 233 Shift source ray bundle by distance (in mm) along the 'axis' specified as 234 a lab-frame vector (numpy-array of length 3) or as one of the strings 235 "vert", "horiz", or "random". 236 237 In the latter case, the reference is the incidence plane of the first 238 non-normal-incidence mirror after the source. If there is none, you will 239 be asked to rather specify the axis as a 3D-numpy-array. 240 241 axis = "vert" means the source position is shifted along the axis perpendicular 242 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 243 244 axis = "horiz" means the source direciton is translated along thr axis in that 245 incidence plane and perpendicular to the current source direction, 246 i.e. "horizontally" in the incidence plane, but retaining the same distance 247 of source and first optical element. 248 249 axis = "random" means the the source direction shifted in a random direction 250 within in the plane perpendicular to the current source direction, 251 e.g. simulating a fluctuation of hte transverse source position. 252 253 Parameters 254 ---------- 255 axis : np.ndarray or str 256 Shift axis, specified either as a 3D lab-frame vector or as one 257 of the strings "vert", "horiz", or "random". 258 259 distance : float 260 Shift distance in mm. 261 262 Returns 263 ------- 264 Nothing, just modifies the property 'source_rays'. 265 """ 266 if type(distance) not in [int, float, np.float64]: 267 raise ValueError('The "distance"-argument must be an int or float number.') 268 269 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 270 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 271 272 OEnormal = None 273 for i in mirror_indcs: 274 ith_OEnormal = self.optical_elements[i].normal 275 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 276 OEnormal = ith_OEnormal 277 break 278 if OEnormal is None: 279 raise Exception( 280 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 281 so you should rather give 'axis' as a numpy-array of length 3." 282 ) 283 284 if type(axis) == np.ndarray and len(axis) == 3: 285 translation_vector = axis 286 else: 287 perp_axis = np.cross(central_ray_vector, OEnormal) 288 horiz_axis = np.cross(perp_axis, central_ray_vector) 289 290 if axis == "vert": 291 translation_vector = perp_axis 292 elif axis == "horiz": 293 translation_vector = horiz_axis 294 elif axis == "random": 295 translation_vector = ( 296 np.random.uniform(low=-1, high=1, size=1) * perp_axis 297 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 298 ) 299 else: 300 raise ValueError( 301 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 302 ) 303 304 self.source_rays = mgeo.TranslationRayList(self.source_rays, distance * mgeo.Normalize(translation_vector)) 305 306 def tilt_source(self, axis: str|np.ndarray, angle: float): 307 """ 308 Rotate source ray bundle by angle around an axis, specified as 309 a lab-frame vector (numpy-array of length 3) or as one of the strings 310 "in_plane", "out_plane" or "random" direction. 311 312 In the latter case, the function considers the incidence plane of the first 313 non-normal-incidence mirror after the source. If there is none, you will 314 be asked to rather specify the axis as a 3D-numpy-array. 315 316 axis = "in_plane" means the source direction is rotated about an axis 317 perpendicular to that incidence plane, which tilts the source 318 "horizontally" in the same plane. 319 320 axis = "out_plane" means the source direciton is rotated about an axis 321 in that incidence plane and perpendicular to the current source direction, 322 which tilts the source "vertically" out of the former incidence plane. 323 324 axis = "random" means the the source direction is tilted in a random direction, 325 e.g. simulating a beam pointing fluctuation. 326 327 Attention, "angle" is given in deg, so as to remain consitent with the 328 conventions of other functions, although pointing is mostly talked about 329 in mrad instead. 330 331 Parameters 332 ---------- 333 axis : np.ndarray or str 334 Shift axis, specified either as a 3D lab-frame vector or as one 335 of the strings "in_plane", "out_plane", or "random". 336 337 angle : float 338 Rotation angle in degree. 339 340 Returns 341 ------- 342 Nothing, just modifies the property 'source_rays'. 343 """ 344 if type(angle) not in [int, float, np.float64]: 345 raise ValueError('The "angle"-argument must be an int or float number.') 346 347 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 348 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 349 350 OEnormal = None 351 for i in mirror_indcs: 352 ith_OEnormal = self.optical_elements[i].normal 353 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 354 OEnormal = ith_OEnormal 355 break 356 if OEnormal is None: 357 raise Exception( 358 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 359 so you should rather give 'axis' as a numpy-array of length 3." 360 ) 361 362 if type(axis) == np.ndarray and len(axis) == 3: 363 rot_axis = axis 364 else: 365 rot_axis_in = np.cross(central_ray_vector, OEnormal) 366 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 367 if axis == "in_plane": 368 rot_axis = rot_axis_in 369 elif axis == "out_plane": 370 rot_axis = rot_axis_out 371 elif axis == "random": 372 rot_axis = ( 373 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 374 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 375 ) 376 else: 377 raise ValueError( 378 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 379 ) 380 381 self.source_rays = mgeo.RotationAroundAxisRayList(self.source_rays, rot_axis, np.deg2rad(angle)) 382 383 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 384 """ 385 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 386 OEstart and OEstop (both included). 387 """ 388 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 389 outray = self.get_output_rays()[OEstart-1][0] 390 print(outray) 391 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 392 self.optical_elements[OEstart:OEstop] = new_elements 393 394 # %% 395 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 396 """ 397 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 398 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 399 that takes either of the two values: 400 - "in" 401 - "out" 402 In either case the "axis" can take these values: 403 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 404 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 405 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 406 407 Parameters 408 ---------- 409 OEindx : int 410 Index of the optical element to modify out of OpticalChain.optical_elements. 411 412 ref : str 413 Reference ray used to define the axes of rotation. Can be either: 414 "in" or "out" or "local_normal" (in+out) 415 416 axis : str 417 Rotation axis, specified as one of the strings 418 "pitch", "roll", "yaw" 419 420 angle : float 421 Rotation angle in degree. 422 423 Returns 424 ------- 425 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 426 """ 427 if abs(OEindx) > len(self.optical_elements): 428 raise ValueError( 429 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 430 ) 431 if type(angle) not in [int, float, np.float64]: 432 raise ValueError('The "angle"-argument must be an int or float number.') 433 MasterRay = [mp.FindCentralRay(self.source_rays)] 434 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 435 TracedRay = [MasterRay] + TracedRay 436 match ref: 437 case "out": 438 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 439 case "in": 440 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 441 case "localnormal": 442 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 443 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 444 RefVec = mgeo.Normalize(Out-In) 445 case _: 446 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 447 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 448 match axis: 449 case "pitch": 450 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 451 case "roll": 452 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 453 case "yaw": 454 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 455 case _: 456 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 457 else: 458 #If the normal vector is aligned with the ray 459 match axis: 460 case "pitch": 461 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 462 case "roll": 463 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 464 case "yaw": 465 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 466 case _: 467 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 468 469 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 470 """ 471 Shift the optical element OpticalChain.optical_elements[OEindx] along 472 axis referenced to the master ray. 473 474 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 475 that takes either of the two values: 476 - "in" 477 - "out" 478 In either case the "axis" can take these values: 479 - "along": Translation along the master ray. 480 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 481 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 482 483 Parameters 484 ---------- 485 OEindx : int 486 Index of the optical element to modify out of OpticalChain.optical_elements. 487 488 ref : str 489 Reference ray used to define the axes of rotation. Can be either: 490 "in" or "out". 491 492 axis : str 493 Translation axis, specified as one of the strings 494 "along", "in_plane", "out_plane". 495 496 distance : float 497 Rotation angle in degree. 498 499 Returns 500 ------- 501 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 502 """ 503 if abs(OEindx) >= len(self.optical_elements): 504 raise ValueError( 505 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 506 ) 507 if type(distance) not in [int, float, np.float64]: 508 raise ValueError('The "dist"-argument must be an int or float number.') 509 510 MasterRay = [mp.FindCentralRay(self.source_rays)] 511 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 512 TracedRay = [MasterRay] + TracedRay 513 match ref: 514 case "out": 515 RefVec = TracedRay[OEindx+1][0].vector 516 case "in": 517 RefVec = TracedRay[OEindx][0].vector 518 case _: 519 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 520 match axis: 521 case "along": 522 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 523 case "in_plane": 524 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 525 case "out_plane": 526 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 527 case _: 528 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 529 530 # some function that randomly misalings one, or several or all ?
The OpticalChain represents the whole optical setup to be simulated: Its main attributes are a list source-Rays and a list of successive OpticalElements.
The method OpticalChain.get_output_rays() returns an associated list of lists of Rays, each calculated by ray-tracing from one OpticalElement to the next. So OpticalChain.get_output_rays()[i] is the bundle of Rays after optical_elements[i].
The string "description" can contain a short description of the optical setup, or similar notes.
The OpticalChain can be visualized quickly with the method OpticalChain.quickshow(), and more nicely with OpticalChain.render().
The class also provides methods for (mis-)alignment of the source-Ray-bundle and the OpticalElements.
source_rays : list[mray.Ray]
List of source rays, which are to be traced.
optical_elements : list[moe.OpticalElement]
List of successive optical elements.
detector: mdet.Detector (optional)
The detector (or list of detectors) to analyse the results.
description : str
A string to describe the optical setup.
copy_chain()
get_output_rays()
render()
----------
shift_source(axis, distance)
tilt_source(self, axis, angle)
----------
rotate_OE(OEindx, axis, angle)
shift_OE(OEindx, axis, distance)
84 def __init__(self, source_rays, optical_elements, detectors, description=""): 85 """ 86 Parameters 87 ---------- 88 source_rays : list[mray.Ray] 89 List of source rays, which are to be traced. 90 91 optical_elements : list[moe.OpticalElement] 92 List of successive optical elements. 93 94 detector: mdet.Detector (optional) 95 The detector (or list of detectors) to analyse the results. 96 97 description : str, optional 98 A string to describe the optical setup. Defaults to ''. 99 100 """ 101 self.source_rays = copy.deepcopy(source_rays) 102 # deepcopy so this object doesn't get changed when the global source_rays change "outside" 103 self.optical_elements = copy.deepcopy(optical_elements) 104 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 105 self.detectors = detectors 106 if isinstance(detectors, mdet.Detector): 107 self.detectors = {"Focus": detectors} 108 self.description = description 109 self._output_rays = None 110 self._last_source_rays_hash = None 111 self._last_optical_elements_hash = None
source_rays : list[mray.Ray]
List of source rays, which are to be traced.
optical_elements : list[moe.OpticalElement]
List of successive optical elements.
detector: mdet.Detector (optional)
The detector (or list of detectors) to analyse the results.
description : str, optional
A string to describe the optical setup. Defaults to ''.
170 def get_input_rays(self): 171 """ 172 Returns the list of source rays. 173 """ 174 return self.source_rays
Returns the list of source rays.
176 def get_output_rays(self, force=False, **kwargs): 177 """ 178 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 179 or if the source-ray-bundle or anything about the optical elements has changed. 180 181 This is the user-facing method to perform the ray-tracing calculation. 182 """ 183 current_source_rays_hash = hash(self.source_rays) 184 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 185 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 186 print("...ray-tracing...", end="", flush=True) 187 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 188 print( 189 "\r\033[K", end="", flush=True 190 ) # move to beginning of the line with \r and then delete the whole line with \033[K 191 self._last_source_rays_hash = current_source_rays_hash 192 self._last_optical_elements_hash = current_optical_elements_hash 193 194 return self._output_rays
Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, or if the source-ray-bundle or anything about the optical elements has changed.
This is the user-facing method to perform the ray-tracing calculation.
196 def get2dPoints(self, Detector = "Focus"): 197 """ 198 Returns the 2D points of the detected rays on the detector. 199 """ 200 if isinstance(Detector, str): 201 if Detector in self.detectors.keys(): 202 Detector = self.detectors[Detector] 203 else: 204 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 205 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0]
Returns the 2D points of the detected rays on the detector.
207 def get3dPoints(self, Detector = "Focus"): 208 """ 209 Returns the 3D points of the detected rays on the detector. 210 """ 211 if isinstance(Detector, str): 212 if Detector in self.detectors.keys(): 213 Detector = self.detectors[Detector] 214 else: 215 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 216 return Detector.get_3D_points(self.get_output_rays()[Detector.index])
Returns the 3D points of the detected rays on the detector.
218 def getDelays(self, Detector = "Focus"): 219 """ 220 Returns the delays of the detected rays on the detector. 221 """ 222 if isinstance(Detector, str): 223 if Detector in self.detectors.keys(): 224 Detector = self.detectors[Detector] 225 else: 226 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 227 return Detector.get_Delays(self.get_output_rays()[Detector.index])
Returns the delays of the detected rays on the detector.
231 def shift_source(self, axis: str|np.ndarray, distance: float): 232 """ 233 Shift source ray bundle by distance (in mm) along the 'axis' specified as 234 a lab-frame vector (numpy-array of length 3) or as one of the strings 235 "vert", "horiz", or "random". 236 237 In the latter case, the reference is the incidence plane of the first 238 non-normal-incidence mirror after the source. If there is none, you will 239 be asked to rather specify the axis as a 3D-numpy-array. 240 241 axis = "vert" means the source position is shifted along the axis perpendicular 242 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 243 244 axis = "horiz" means the source direciton is translated along thr axis in that 245 incidence plane and perpendicular to the current source direction, 246 i.e. "horizontally" in the incidence plane, but retaining the same distance 247 of source and first optical element. 248 249 axis = "random" means the the source direction shifted in a random direction 250 within in the plane perpendicular to the current source direction, 251 e.g. simulating a fluctuation of hte transverse source position. 252 253 Parameters 254 ---------- 255 axis : np.ndarray or str 256 Shift axis, specified either as a 3D lab-frame vector or as one 257 of the strings "vert", "horiz", or "random". 258 259 distance : float 260 Shift distance in mm. 261 262 Returns 263 ------- 264 Nothing, just modifies the property 'source_rays'. 265 """ 266 if type(distance) not in [int, float, np.float64]: 267 raise ValueError('The "distance"-argument must be an int or float number.') 268 269 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 270 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 271 272 OEnormal = None 273 for i in mirror_indcs: 274 ith_OEnormal = self.optical_elements[i].normal 275 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 276 OEnormal = ith_OEnormal 277 break 278 if OEnormal is None: 279 raise Exception( 280 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 281 so you should rather give 'axis' as a numpy-array of length 3." 282 ) 283 284 if type(axis) == np.ndarray and len(axis) == 3: 285 translation_vector = axis 286 else: 287 perp_axis = np.cross(central_ray_vector, OEnormal) 288 horiz_axis = np.cross(perp_axis, central_ray_vector) 289 290 if axis == "vert": 291 translation_vector = perp_axis 292 elif axis == "horiz": 293 translation_vector = horiz_axis 294 elif axis == "random": 295 translation_vector = ( 296 np.random.uniform(low=-1, high=1, size=1) * perp_axis 297 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 298 ) 299 else: 300 raise ValueError( 301 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 302 ) 303 304 self.source_rays = mgeo.TranslationRayList(self.source_rays, distance * mgeo.Normalize(translation_vector))
Shift source ray bundle by distance (in mm) along the 'axis' specified as a lab-frame vector (numpy-array of length 3) or as one of the strings "vert", "horiz", or "random".
In the latter case, the reference is the incidence plane of the first non-normal-incidence mirror after the source. If there is none, you will be asked to rather specify the axis as a 3D-numpy-array.
axis = "vert" means the source position is shifted along the axis perpendicular to that incidence plane, i.e. "vertically" away from the former incidence plane..
axis = "horiz" means the source direciton is translated along thr axis in that incidence plane and perpendicular to the current source direction, i.e. "horizontally" in the incidence plane, but retaining the same distance of source and first optical element.
axis = "random" means the the source direction shifted in a random direction within in the plane perpendicular to the current source direction, e.g. simulating a fluctuation of hte transverse source position.
axis : np.ndarray or str
Shift axis, specified either as a 3D lab-frame vector or as one
of the strings "vert", "horiz", or "random".
distance : float
Shift distance in mm.
Nothing, just modifies the property 'source_rays'.
306 def tilt_source(self, axis: str|np.ndarray, angle: float): 307 """ 308 Rotate source ray bundle by angle around an axis, specified as 309 a lab-frame vector (numpy-array of length 3) or as one of the strings 310 "in_plane", "out_plane" or "random" direction. 311 312 In the latter case, the function considers the incidence plane of the first 313 non-normal-incidence mirror after the source. If there is none, you will 314 be asked to rather specify the axis as a 3D-numpy-array. 315 316 axis = "in_plane" means the source direction is rotated about an axis 317 perpendicular to that incidence plane, which tilts the source 318 "horizontally" in the same plane. 319 320 axis = "out_plane" means the source direciton is rotated about an axis 321 in that incidence plane and perpendicular to the current source direction, 322 which tilts the source "vertically" out of the former incidence plane. 323 324 axis = "random" means the the source direction is tilted in a random direction, 325 e.g. simulating a beam pointing fluctuation. 326 327 Attention, "angle" is given in deg, so as to remain consitent with the 328 conventions of other functions, although pointing is mostly talked about 329 in mrad instead. 330 331 Parameters 332 ---------- 333 axis : np.ndarray or str 334 Shift axis, specified either as a 3D lab-frame vector or as one 335 of the strings "in_plane", "out_plane", or "random". 336 337 angle : float 338 Rotation angle in degree. 339 340 Returns 341 ------- 342 Nothing, just modifies the property 'source_rays'. 343 """ 344 if type(angle) not in [int, float, np.float64]: 345 raise ValueError('The "angle"-argument must be an int or float number.') 346 347 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 348 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 349 350 OEnormal = None 351 for i in mirror_indcs: 352 ith_OEnormal = self.optical_elements[i].normal 353 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 354 OEnormal = ith_OEnormal 355 break 356 if OEnormal is None: 357 raise Exception( 358 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 359 so you should rather give 'axis' as a numpy-array of length 3." 360 ) 361 362 if type(axis) == np.ndarray and len(axis) == 3: 363 rot_axis = axis 364 else: 365 rot_axis_in = np.cross(central_ray_vector, OEnormal) 366 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 367 if axis == "in_plane": 368 rot_axis = rot_axis_in 369 elif axis == "out_plane": 370 rot_axis = rot_axis_out 371 elif axis == "random": 372 rot_axis = ( 373 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 374 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 375 ) 376 else: 377 raise ValueError( 378 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 379 ) 380 381 self.source_rays = mgeo.RotationAroundAxisRayList(self.source_rays, rot_axis, np.deg2rad(angle))
Rotate source ray bundle by angle around an axis, specified as a lab-frame vector (numpy-array of length 3) or as one of the strings "in_plane", "out_plane" or "random" direction.
In the latter case, the function considers the incidence plane of the first non-normal-incidence mirror after the source. If there is none, you will be asked to rather specify the axis as a 3D-numpy-array.
axis = "in_plane" means the source direction is rotated about an axis perpendicular to that incidence plane, which tilts the source "horizontally" in the same plane.
axis = "out_plane" means the source direciton is rotated about an axis in that incidence plane and perpendicular to the current source direction, which tilts the source "vertically" out of the former incidence plane.
axis = "random" means the the source direction is tilted in a random direction, e.g. simulating a beam pointing fluctuation.
Attention, "angle" is given in deg, so as to remain consitent with the conventions of other functions, although pointing is mostly talked about in mrad instead.
axis : np.ndarray or str
Shift axis, specified either as a 3D lab-frame vector or as one
of the strings "in_plane", "out_plane", or "random".
angle : float
Rotation angle in degree.
Nothing, just modifies the property 'source_rays'.
383 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 384 """ 385 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 386 OEstart and OEstop (both included). 387 """ 388 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 389 outray = self.get_output_rays()[OEstart-1][0] 390 print(outray) 391 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 392 self.optical_elements[OEstart:OEstop] = new_elements
This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements OEstart and OEstop (both included).
395 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 396 """ 397 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 398 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 399 that takes either of the two values: 400 - "in" 401 - "out" 402 In either case the "axis" can take these values: 403 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 404 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 405 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 406 407 Parameters 408 ---------- 409 OEindx : int 410 Index of the optical element to modify out of OpticalChain.optical_elements. 411 412 ref : str 413 Reference ray used to define the axes of rotation. Can be either: 414 "in" or "out" or "local_normal" (in+out) 415 416 axis : str 417 Rotation axis, specified as one of the strings 418 "pitch", "roll", "yaw" 419 420 angle : float 421 Rotation angle in degree. 422 423 Returns 424 ------- 425 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 426 """ 427 if abs(OEindx) > len(self.optical_elements): 428 raise ValueError( 429 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 430 ) 431 if type(angle) not in [int, float, np.float64]: 432 raise ValueError('The "angle"-argument must be an int or float number.') 433 MasterRay = [mp.FindCentralRay(self.source_rays)] 434 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 435 TracedRay = [MasterRay] + TracedRay 436 match ref: 437 case "out": 438 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 439 case "in": 440 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 441 case "localnormal": 442 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 443 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 444 RefVec = mgeo.Normalize(Out-In) 445 case _: 446 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 447 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 448 match axis: 449 case "pitch": 450 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 451 case "roll": 452 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 453 case "yaw": 454 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 455 case _: 456 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 457 else: 458 #If the normal vector is aligned with the ray 459 match axis: 460 case "pitch": 461 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 462 case "roll": 463 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 464 case "yaw": 465 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 466 case _: 467 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].')
Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable that takes either of the two values: - "in" - "out" In either case the "axis" can take these values: - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. - "yaw": Rotation about the vector normal to the incidence plane and to the master ray.
OEindx : int
Index of the optical element to modify out of OpticalChain.optical_elements.
ref : str
Reference ray used to define the axes of rotation. Can be either:
"in" or "out" or "local_normal" (in+out)
axis : str
Rotation axis, specified as one of the strings
"pitch", "roll", "yaw"
angle : float
Rotation angle in degree.
Nothing, just modifies OpticalChain.optical_elements[OEindx].
469 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 470 """ 471 Shift the optical element OpticalChain.optical_elements[OEindx] along 472 axis referenced to the master ray. 473 474 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 475 that takes either of the two values: 476 - "in" 477 - "out" 478 In either case the "axis" can take these values: 479 - "along": Translation along the master ray. 480 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 481 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 482 483 Parameters 484 ---------- 485 OEindx : int 486 Index of the optical element to modify out of OpticalChain.optical_elements. 487 488 ref : str 489 Reference ray used to define the axes of rotation. Can be either: 490 "in" or "out". 491 492 axis : str 493 Translation axis, specified as one of the strings 494 "along", "in_plane", "out_plane". 495 496 distance : float 497 Rotation angle in degree. 498 499 Returns 500 ------- 501 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 502 """ 503 if abs(OEindx) >= len(self.optical_elements): 504 raise ValueError( 505 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 506 ) 507 if type(distance) not in [int, float, np.float64]: 508 raise ValueError('The "dist"-argument must be an int or float number.') 509 510 MasterRay = [mp.FindCentralRay(self.source_rays)] 511 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 512 TracedRay = [MasterRay] + TracedRay 513 match ref: 514 case "out": 515 RefVec = TracedRay[OEindx+1][0].vector 516 case "in": 517 RefVec = TracedRay[OEindx][0].vector 518 case _: 519 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 520 match axis: 521 case "along": 522 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 523 case "in_plane": 524 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 525 case "out_plane": 526 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 527 case _: 528 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 529 530 # some function that randomly misalings one, or several or all ?
Shift the optical element OpticalChain.optical_elements[OEindx] along axis referenced to the master ray.
The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable that takes either of the two values: - "in" - "out" In either case the "axis" can take these values: - "along": Translation along the master ray. - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. - "out_plane": Translation along the vector normal to the incidence plane and to the master ray.
OEindx : int
Index of the optical element to modify out of OpticalChain.optical_elements.
ref : str
Reference ray used to define the axes of rotation. Can be either:
"in" or "out".
axis : str
Translation axis, specified as one of the strings
"along", "in_plane", "out_plane".
distance : float
Rotation angle in degree.
Nothing, just modifies OpticalChain.optical_elements[OEindx].
67def _getETransmission(OpticalChain, IndexIn=0, IndexOut=-1) -> float: 68 """ 69 Calculates the energy transmission from the input to the output of the OpticalChain in percent. 70 71 Parameters 72 ---------- 73 OpticalChain : OpticalChain 74 An object of the ModuleOpticalChain.OpticalChain-class. 75 76 IndexIn : int, optional 77 Index of the input RayList in the OpticalChain, defaults to 0. 78 79 IndexOut : int, optional 80 Index of the output RayList in the OpticalChain, defaults to -1. 81 82 Returns 83 ------- 84 ETransmission : float 85 """ 86 Rays = OpticalChain.get_output_rays() 87 if IndexIn == 0: 88 RayListIn = OpticalChain.get_input_rays() 89 else: 90 RayListIn = Rays[IndexIn] 91 RayListOut = Rays[IndexOut] 92 ETransmission = GetETransmission(RayListIn, RayListOut) 93 return ETransmission
Calculates the energy transmission from the input to the output of the OpticalChain in percent.
OpticalChain : OpticalChain
An object of the ModuleOpticalChain.OpticalChain-class.
IndexIn : int, optional
Index of the input RayList in the OpticalChain, defaults to 0.
IndexOut : int, optional
Index of the output RayList in the OpticalChain, defaults to -1.
ETransmission : float
147def _getResultsSummary(OpticalChain, Detector = "Focus", verbose=False): 148 """ 149 Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation 150 for the given Detector and RayList. 151 If verbose, then also print a summary of the results for the given Detector. 152 153 Parameters 154 ---------- 155 OpticalChain : OpticalChain 156 An object of the ModuleOpticalChain.OpticalChain-class. 157 158 Detector : Detector or str, optional 159 An object of the ModuleDetector.Detector-class or "Focus" to use the focus detector, defaults to "Focus". 160 161 verbose : bool 162 Whether to print a result summary. 163 164 Returns 165 ------- 166 FocalSpotSizeSD : float 167 168 DurationSD : float 169 """ 170 if isinstance(Detector, str): 171 Detector = OpticalChain.detectors[Detector] 172 Index = Detector.index 173 RayListAnalysed = OpticalChain.get_output_rays()[Index] 174 FocalSpotSizeSD, DurationSD = GetResultSummary(Detector, RayListAnalysed, verbose) 175 return FocalSpotSizeSD, DurationSD
Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation for the given Detector and RayList. If verbose, then also print a summary of the results for the given Detector.
OpticalChain : OpticalChain
An object of the ModuleOpticalChain.OpticalChain-class.
Detector : Detector or str, optional
An object of the ModuleDetector.Detector-class or "Focus" to use the focus detector, defaults to "Focus".
verbose : bool
Whether to print a result summary.
FocalSpotSizeSD : float
DurationSD : float
57def DrawSpotDiagram(OpticalChain, 58 Detector = "Focus", 59 DrawAiryAndFourier=False, 60 DrawFocalContour=False, 61 DrawFocal=False, 62 ColorCoded=None, 63 Observer = None) -> plt.Figure: 64 """ 65 Produce an interactive figure with the spot diagram on the selected Detector. 66 The detector distance can be shifted with the left-right cursor keys. Doing so will actually move the detector. 67 If DrawAiryAndFourier is True, a circle with the Airy-spot-size will be shown. 68 If DrawFocalContour is True, the focal contour calculated from some of the rays will be shown. 69 If DrawFocal is True, a heatmap calculated from some of the rays will be shown. 70 The 'spots' can optionally be color-coded by specifying ColorCoded, which can be one of ["Intensity","Incidence","Delay"]. 71 72 Parameters 73 ---------- 74 RayListAnalysed : list(Ray) 75 List of objects of the ModuleOpticalRay.Ray-class. 76 77 Detector : Detector or str, optional 78 An object of the ModuleDetector.Detector-class or the name of the detector. The default is "Focus". 79 80 DrawAiryAndFourier : bool, optional 81 Whether to draw a circle with the Airy-spot-size. The default is False. 82 83 DrawFocalContour : bool, optional 84 Whether to draw the focal contour. The default is False. 85 86 DrawFocal : bool, optional 87 Whether to draw the focal heatmap. The default is False. 88 89 ColorCoded : str, optional 90 Color-code the spots according to one of ["Intensity","Incidence","Delay"]. The default is None. 91 92 Observer : Observer, optional 93 An observer object. If none, then we just create a copy of the detector and move it when pressing left-right. 94 However, if an observer is specified, then we will change the value of the observer and it will issue 95 the required callbacks to update several plots at the same time. 96 97 Returns 98 ------- 99 fig : matlplotlib-figure-handle. 100 Shows the interactive figure. 101 """ 102 if isinstance(Detector, str): 103 Detector = OpticalChain.detectors[Detector] 104 Index = Detector.index 105 movingDetector = copy(Detector) # We will move this detector when pressing left-right 106 if Observer is None: 107 detectorPosition = Observable(movingDetector.distance) # We will observe the distance of this detector 108 else: 109 detectorPosition = Observer 110 movingDetector.distance = detectorPosition.value 111 112 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 113 114 RayListAnalysed = OpticalChain.get_output_rays()[Index] 115 116 NumericalAperture = man.GetNumericalAperture(RayListAnalysed, 1) # NA determined from final ray bundle 117 MaxWavelength = np.max([i.wavelength for i in RayListAnalysed]) 118 if DrawAiryAndFourier: 119 AiryRadius = man.GetAiryRadius(MaxWavelength, NumericalAperture) * 1e3 # in µm 120 else: 121 AiryRadius = 0 122 123 if DrawFocalContour or DrawFocal: 124 X,Y,Z = man.GetDiffractionFocus(OpticalChain, movingDetector, Index) 125 Z/=np.max(Z) 126 127 DectectorPoint2D_Xcoord, DectectorPoint2D_Ycoord, FocalSpotSize, SpotSizeSD = mpu._getDetectorPoints( 128 RayListAnalysed, movingDetector 129 ) 130 131 match ColorCoded: 132 case "Intensity": 133 IntensityList = [k.intensity for k in RayListAnalysed] 134 z = np.asarray(IntensityList) 135 zlabel = "Intensity (arb.u.)" 136 title = "Intensity + Spot Diagram\n press left/right to move detector position" 137 addLine = "" 138 case "Incidence": 139 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # degree 140 z = np.asarray(IncidenceList) 141 zlabel = "Incidence angle (deg)" 142 title = "Ray Incidence + Spot Diagram\n press left/right to move detector position" 143 addLine = "" 144 case "Delay": 145 DelayList = movingDetector.get_Delays(RayListAnalysed) 146 IntensityList = [k.intensity for k in RayListAnalysed] 147 DurationSD = mp.WeightedStandardDeviation(DelayList,IntensityList) 148 #DurationSD = mp.StandardDeviation(DelayList) 149 z = np.asarray(DelayList) 150 zlabel = "Delay (fs)" 151 title = "Delay + Spot Diagram\n press left/right to move detector position" 152 addLine = "\n" + "{:.2f}".format(DurationSD) + " fs SD" 153 case _: 154 z = "red" 155 title = "Spot Diagram\n press left/right to move detector position" 156 addLine = "" 157 158 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 159 160 plt.ion() 161 fig, ax = plt.subplots() 162 if DrawFocal: 163 focal = ax.pcolormesh(X*1e3,Y*1e3,Z) 164 if DrawFocalContour: 165 levels = [1/np.e**2, 0.5] 166 contour = ax.contourf(X*1e3, Y*1e3, Z, levels=levels, cmap='gray') 167 168 if DrawAiryAndFourier: 169 theta = np.linspace(0, 2 * np.pi, 100) 170 x = AiryRadius * np.cos(theta) 171 y = AiryRadius * np.sin(theta) # 172 ax.plot(x, y, c="black") 173 174 175 foo = ax.scatter( 176 DectectorPoint2D_Xcoord, 177 DectectorPoint2D_Ycoord, 178 c=z, 179 s=15, 180 label="{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(SpotSizeSD * 1e3) + " \u03BCm SD" + addLine, 181 ) 182 183 axisLim = 1.1 * max(AiryRadius, 0.5 * FocalSpotSize * 1000) 184 ax.set_xlim(-axisLim, axisLim) 185 ax.set_ylim(-axisLim, axisLim) 186 187 if ColorCoded == "Intensity" or ColorCoded == "Incidence" or ColorCoded == "Delay": 188 cbar = fig.colorbar(foo) 189 cbar.set_label(zlabel) 190 191 ax.legend(loc="upper right") 192 ax.set_xlabel("X (µm)") 193 ax.set_ylabel("Y (µm)") 194 ax.set_title(title) 195 # ax.margins(x=0) 196 197 198 def update_plot(new_value): 199 nonlocal movingDetector, ColorCoded, zlabel, cbar, detectorPosition, foo, distStep, focal, contour, levels, Index, RayListAnalysed 200 201 newDectectorPoint2D_Xcoord, newDectectorPoint2D_Ycoord, newFocalSpotSize, newSpotSizeSD = mpu._getDetectorPoints( 202 RayListAnalysed, movingDetector 203 ) 204 205 if DrawFocal: 206 focal.set_array(Z) 207 if DrawFocalContour: 208 levels = [1/np.e**2, 0.5] 209 for coll in contour.collections: 210 coll.remove() # Remove old contour lines 211 contour = ax.contourf(X * 1e3, Y * 1e3, Z, levels=levels, cmap='gray') 212 213 xy = foo.get_offsets() 214 xy[:, 0] = newDectectorPoint2D_Xcoord 215 xy[:, 1] = newDectectorPoint2D_Ycoord 216 foo.set_offsets(xy) 217 218 219 if ColorCoded == "Delay": 220 newDelayList = np.asarray(movingDetector.get_Delays(RayListAnalysed)) 221 newDurationSD = mp.WeightedStandardDeviation(newDelayList,IntensityList) 222 #newDurationSD = mp.StandardDeviation(newDelayList) 223 newaddLine = "\n" + "{:.2f}".format(newDurationSD) + " fs SD" 224 foo.set_array(newDelayList) 225 foo.set_clim(min(newDelayList), max(newDelayList)) 226 cbar.update_normal(foo) 227 else: 228 newaddLine = "" 229 230 foo.set_label( 231 "{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(newSpotSizeSD * 1e3) + " \u03BCm SD" + newaddLine 232 ) 233 ax.legend(loc="upper right") 234 235 axisLim = 1.1 * max(AiryRadius, 0.5 * newFocalSpotSize * 1000) 236 ax.set_xlim(-axisLim, axisLim) 237 ax.set_ylim(-axisLim, axisLim) 238 239 distStep = min( 240 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 241 ) # in mm 242 243 fig.canvas.draw_idle() 244 245 246 def press(event): 247 nonlocal detectorPosition, distStep 248 if event.key == "right": 249 detectorPosition.value += distStep 250 elif event.key == "left": 251 if detectorPosition.value > 1.5 * distStep: 252 detectorPosition.value -= distStep 253 else: 254 detectorPosition.value = 0.5 * distStep 255 else: 256 return None 257 258 fig.canvas.mpl_connect("key_press_event", press) 259 260 plt.show() 261 262 detectorPosition.register(update_plot) 263 264 265 return fig, detectorPosition
Produce an interactive figure with the spot diagram on the selected Detector. The detector distance can be shifted with the left-right cursor keys. Doing so will actually move the detector. If DrawAiryAndFourier is True, a circle with the Airy-spot-size will be shown. If DrawFocalContour is True, the focal contour calculated from some of the rays will be shown. If DrawFocal is True, a heatmap calculated from some of the rays will be shown. The 'spots' can optionally be color-coded by specifying ColorCoded, which can be one of ["Intensity","Incidence","Delay"].
RayListAnalysed : list(Ray)
List of objects of the ModuleOpticalRay.Ray-class.
Detector : Detector or str, optional
An object of the ModuleDetector.Detector-class or the name of the detector. The default is "Focus".
DrawAiryAndFourier : bool, optional
Whether to draw a circle with the Airy-spot-size. The default is False.
DrawFocalContour : bool, optional
Whether to draw the focal contour. The default is False.
DrawFocal : bool, optional
Whether to draw the focal heatmap. The default is False.
ColorCoded : str, optional
Color-code the spots according to one of ["Intensity","Incidence","Delay"]. The default is None.
Observer : Observer, optional
An observer object. If none, then we just create a copy of the detector and move it when pressing left-right.
However, if an observer is specified, then we will change the value of the observer and it will issue
the required callbacks to update several plots at the same time.
fig : matlplotlib-figure-handle.
Shows the interactive figure.
270def DrawDelaySpots(OpticalChain, 271 DeltaFT: tuple[int, float], 272 Detector = "Focus", 273 DrawAiryAndFourier=False, 274 ColorCoded=None, 275 Observer = None 276 ) -> plt.Figure: 277 """ 278 Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed 279 hitting the Detector, with the ray-delays shown in the 3rd dimension. 280 The detector distance can be shifted with the left-right cursor keys. 281 If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and 282 whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'. 283 284 The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"]. 285 286 Parameters 287 ---------- 288 RayListAnalysed : list(Ray) 289 List of objects of the ModuleOpticalRay.Ray-class. 290 291 Detector : Detector 292 An object of the ModuleDetector.Detector-class. 293 294 DeltaFT : (int, float) 295 The Fourier-limited pulse duration. Just used as a reference to compare the temporal spread 296 induced by the ray-delays. 297 298 DrawAiryAndFourier : bool, optional 299 Whether to draw a cylinder showing the Airy-spot-size and Fourier-limited-duration. 300 The default is False. 301 302 ColorCoded : str, optional 303 Color-code the spots according to one of ["Intensity","Incidence"]. 304 The default is None. 305 306 Returns 307 ------- 308 fig : matlplotlib-figure-handle. 309 Shows the interactive figure. 310 """ 311 if isinstance(Detector, str): 312 Det = OpticalChain.detectors[Detector] 313 else: 314 Det = Detector 315 Index = Det.index 316 Detector = copy(Det) 317 if Observer is None: 318 detectorPosition = Observable(Detector.distance) 319 else: 320 detectorPosition = Observer 321 Detector.distance = detectorPosition.value 322 323 RayListAnalysed = OpticalChain.get_output_rays()[Index] 324 fig, NumericalAperture, AiryRadius, FocalSpotSize = _drawDelayGraph( 325 RayListAnalysed, Detector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded 326 ) 327 328 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 329 330 movingDetector = copy(Detector) 331 332 def update_plot(new_value): 333 nonlocal movingDetector, ColorCoded, detectorPosition, distStep, fig 334 ax = fig.axes[0] 335 cam = [ax.azim, ax.elev, ax._dist] 336 fig, sameNumericalAperture, sameAiryRadius, newFocalSpotSize = _drawDelayGraph( 337 RayListAnalysed, movingDetector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded, fig 338 ) 339 ax = fig.axes[0] 340 ax.azim, ax.elev, ax._dist = cam 341 distStep = min( 342 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 343 ) 344 return fig 345 346 def press(event): 347 nonlocal detectorPosition, distStep, movingDetector, fig 348 if event.key == "right": 349 detectorPosition.value += distStep 350 elif event.key == "left": 351 if detectorPosition.value > 1.5 * distStep: 352 detectorPosition.value -= distStep 353 else: 354 detectorPosition.value = 0.5 * distStep 355 356 fig.canvas.mpl_connect("key_press_event", press) 357 detectorPosition.register(update_plot) 358 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 359 360 return fig, Observable
Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed hitting the Detector, with the ray-delays shown in the 3rd dimension. The detector distance can be shifted with the left-right cursor keys. If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'.
The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"].
RayListAnalysed : list(Ray)
List of objects of the ModuleOpticalRay.Ray-class.
Detector : Detector
An object of the ModuleDetector.Detector-class.
DeltaFT : (int, float)
The Fourier-limited pulse duration. Just used as a reference to compare the temporal spread
induced by the ray-delays.
DrawAiryAndFourier : bool, optional
Whether to draw a cylinder showing the Airy-spot-size and Fourier-limited-duration.
The default is False.
ColorCoded : str, optional
Color-code the spots according to one of ["Intensity","Incidence"].
The default is None.
fig : matlplotlib-figure-handle.
Shows the interactive figure.
441def DrawMirrorProjection(OpticalChain, ReflectionNumber: int, ColorCoded=None, Detector="") -> plt.Figure: 442 """ 443 Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. 444 The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is 445 measured at the Detector. 446 447 Parameters 448 ---------- 449 OpticalChain : OpticalChain 450 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 451 452 ReflectionNumber : int 453 Index specifying the optical element on which you want to see the impact points. 454 455 Detector : Detector, optional 456 Object of the ModuleDetector.Detector-class. Only necessary to project delays. The default is None. 457 458 ColorCoded : str, optional 459 Specifies which ray property to color-code: ["Incidence","Intensity","Delay"]. The default is None. 460 461 Returns 462 ------- 463 fig : matlplotlib-figure-handle. 464 Shows the figure. 465 """ 466 from mpl_toolkits.axes_grid1 import make_axes_locatable 467 if isinstance(Detector, str): 468 if Detector == "": 469 Detector = None 470 else: 471 Detector = OpticalChain.detectors[Detector] 472 473 Position = OpticalChain[ReflectionNumber].position 474 q = OpticalChain[ReflectionNumber].orientation 475 # n = OpticalChain.optical_elements[ReflectionNumber].normal 476 # m = OpticalChain.optical_elements[ReflectionNumber].majoraxis 477 478 RayListAnalysed = OpticalChain.get_output_rays()[ReflectionNumber] 479 # transform rays into the mirror-support reference frame 480 # (same as mirror frame but without the shift by mirror-centre) 481 r0 = OpticalChain[ReflectionNumber].r0 482 RayList = [r.to_basis(*OpticalChain[ReflectionNumber].basis) for r in RayListAnalysed] 483 484 x = np.asarray([k.point[0] for k in RayList]) - r0[0] 485 y = np.asarray([k.point[1] for k in RayList]) - r0[1] 486 if ColorCoded == "Intensity": 487 IntensityList = [k.intensity for k in RayListAnalysed] 488 z = np.asarray(IntensityList) 489 zlabel = "Intensity (arb.u.)" 490 title = "Ray intensity projected on mirror " 491 elif ColorCoded == "Incidence": 492 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # in degree 493 z = np.asarray(IncidenceList) 494 zlabel = "Incidence angle (deg)" 495 title = "Ray incidence projected on mirror " 496 elif ColorCoded == "Delay": 497 if Detector is not None: 498 z = np.asarray(Detector.get_Delays(RayListAnalysed)) 499 zlabel = "Delay (fs)" 500 title = "Ray delay at detector projected on mirror " 501 else: 502 raise ValueError("If you want to project ray delays, you must specify a detector.") 503 else: 504 z = "red" 505 title = "Ray impact points projected on mirror" 506 507 plt.ion() 508 fig = plt.figure() 509 ax = OpticalChain.optical_elements[ReflectionNumber].support._ContourSupport(fig) 510 p = plt.scatter(x, y, c=z, s=15) 511 if ColorCoded == "Delay" or ColorCoded == "Incidence" or ColorCoded == "Intensity": 512 divider = make_axes_locatable(ax) 513 cax = divider.append_axes("right", size="5%", pad=0.05) 514 cbar = fig.colorbar(p, cax=cax) 515 cbar.set_label(zlabel) 516 ax.set_xlabel("x (mm)") 517 ax.set_ylabel("y (mm)") 518 plt.title(title, loc="right") 519 plt.tight_layout() 520 521 bbox = ax.get_position() 522 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 523 ax.set_position(bbox) 524 plt.show() 525 526 return fig
Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is measured at the Detector.
OpticalChain : OpticalChain
List of objects of the ModuleOpticalOpticalChain.OpticalChain-class.
ReflectionNumber : int
Index specifying the optical element on which you want to see the impact points.
Detector : Detector, optional
Object of the ModuleDetector.Detector-class. Only necessary to project delays. The default is None.
ColorCoded : str, optional
Specifies which ray property to color-code: ["Incidence","Intensity","Delay"]. The default is None.
fig : matlplotlib-figure-handle.
Shows the figure.
532def DrawSetup(OpticalChain, 533 EndDistance=None, 534 maxRays=300, 535 OEpoints=2000, 536 draw_mesh=False, 537 cycle_ray_colors = False, 538 impact_points = False, 539 DrawDetectors=True, 540 DetectedRays = False, 541 Observers = dict()): 542 """ 543 Renders an image of the Optical setup and the traced rays. 544 545 Parameters 546 ---------- 547 OpticalChain : OpticalChain 548 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 549 550 EndDistance : float, optional 551 The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). If not specified, 552 this distance is set to that between the source point and the 1st optical element. 553 554 maxRays: int 555 The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog 556 and not required for a nice image. Default is 150. 557 558 OEpoints : int 559 How many little spheres to draw to represent the optical elements. Default is 2000. 560 561 Returns 562 ------- 563 fig : Pyvista-figure-handle. 564 Shows the figure. 565 """ 566 567 RayListHistory = [OpticalChain.source_rays] + OpticalChain.get_output_rays() 568 569 if EndDistance is None: 570 EndDistance = np.linalg.norm(OpticalChain.source_rays[0].point - OpticalChain.optical_elements[0].position) 571 572 print("...rendering image of optical chain...", end="", flush=True) 573 fig = pvqt.BackgroundPlotter(window_size=(1500, 500), notebook=False) # Opening a window 574 fig.set_background('white') 575 576 if cycle_ray_colors: 577 colors = mpu.generate_distinct_colors(len(OpticalChain)+1) 578 else: 579 colors = [[0.7, 0, 0]]*(len(OpticalChain)+1) # Default color: dark red 580 581 # Optics display 582 # For each optic we will send the figure to the function _RenderOpticalElement and it will add the optic to the figure 583 for i,OE in enumerate(OpticalChain.optical_elements): 584 color = pv.Color(colors[i+1]) 585 rgb = color.float_rgb 586 h, l, s = rgb_to_hls(*rgb) 587 s = max(0, min(1, s * 0.3)) # Decrease saturation 588 l = max(0, min(1, l + 0.1)) # Increase lightness 589 new_rgb = hls_to_rgb(h, l, s) 590 darkened_color = pv.Color(new_rgb) 591 mpm._RenderOpticalElement(fig, OE, OEpoints, draw_mesh, darkened_color, index=i) 592 ray_meshes = mpm._RenderRays(RayListHistory, EndDistance, maxRays) 593 for i,ray in enumerate(ray_meshes): 594 color = pv.Color(colors[i]) 595 fig.add_mesh(ray, color=color, name=f"RayBundle_{i}") 596 if impact_points: 597 for i,rays in enumerate(RayListHistory): 598 points = np.array([list(r.point) for r in rays], dtype=np.float32) 599 points = pv.PolyData(points) 600 color = pv.Color(colors[i-1]) 601 fig.add_mesh(points, color=color, point_size=5, name=f"RayImpactPoints_{i}") 602 603 detector_copies = {key: copy(OpticalChain.detectors[key]) for key in OpticalChain.detectors.keys()} 604 detector_meshes_list = [] 605 detectedpoint_meshes = dict() 606 607 if OpticalChain.detectors is not None and DrawDetectors: 608 # Detector display 609 for key in OpticalChain.detectors.keys(): 610 det = detector_copies[key] 611 index = OpticalChain.detectors[key].index 612 if key in Observers: 613 det.distance = Observers[key].value 614 #Observers[key].register_calculation(lambda x: det.set_distance(x)) 615 mpm._RenderDetector(fig, det, name = key, detector_meshes = detector_meshes_list) 616 if DetectedRays: 617 RayListAnalysed = OpticalChain.get_output_rays()[index] 618 points = det.get_3D_points(RayListAnalysed) 619 points = pv.PolyData(points) 620 detectedpoint_meshes[key] = points 621 fig.add_mesh(points, color='purple', point_size=5, name=f"DetectedRays_{key}") 622 detector_meshes = dict(zip(OpticalChain.detectors.keys(), detector_meshes_list)) 623 624 # Now we define a function that will move on the plot the detector with name "detname" when it's called 625 def move_detector(detname, new_value): 626 nonlocal fig, detector_meshes, detectedpoint_meshes, DetectedRays, detectedpoint_meshes, detector_copies, OpticalChain 627 det = detector_copies[detname] 628 index = OpticalChain.detectors[detname].index 629 det_mesh = detector_meshes[detname] 630 translation = det.normal * (det.distance - new_value) 631 det_mesh.translate(translation, inplace=True) 632 det.distance = new_value 633 if DetectedRays: 634 points_mesh = detectedpoint_meshes[detname] 635 points_mesh.points = det.get_3D_points(OpticalChain.get_output_rays()[index]) 636 fig.show() 637 638 # Now we register the function to the observers 639 for key in OpticalChain.detectors.keys(): 640 if key in Observers: 641 Observers[key].register(lambda x: move_detector(key, x)) 642 643 #pv.save_meshio('optics.inp', pointcloud) 644 print( 645 "\r\033[K", end="", flush=True 646 ) # move to beginning of the line with \r and then delete the whole line with \033[K 647 fig.show() 648 return fig
Renders an image of the Optical setup and the traced rays.
OpticalChain : OpticalChain
List of objects of the ModuleOpticalOpticalChain.OpticalChain-class.
EndDistance : float, optional
The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). If not specified,
this distance is set to that between the source point and the 1st optical element.
maxRays: int
The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog
and not required for a nice image. Default is 150.
OEpoints : int
How many little spheres to draw to represent the optical elements. Default is 2000.
fig : Pyvista-figure-handle.
Shows the figure.
704def DrawCaustics(OpticalChain, Range=1, Detector="Focus" , Npoints=1000, Nrays=1000): 705 """ 706 This function displays the caustics of the rays on the detector. 707 To do so, it calculates the intersections of the rays with the detector over a 708 range determined by the parameter Range, and then plots the standard deviation of the 709 positions in the x and y directions. 710 711 Parameters 712 ---------- 713 OpticalChain : OpticalChain 714 The optical chain to analyse. 715 716 DetectorName : str 717 The name of the detector on which the caustics are calculated. 718 719 Range : float 720 The range of the detector over which to calculate the caustics. 721 722 Npoints : int, optional 723 The number of points to sample on the detector. The default is 1000. 724 725 Returns 726 ------- 727 fig : Figure 728 The figure of the plot. 729 """ 730 distances = np.linspace(-Range, Range, Npoints) 731 if isinstance(Detector, str): 732 Det = OpticalChain.detectors[Detector] 733 Index = Det.index 734 Rays = OpticalChain.get_output_rays()[Index] 735 Nrays = min(Nrays, len(Rays)) 736 Rays = np.random.choice(Rays, Nrays, replace=False) 737 LocalRayList = [r.to_basis(*Det.basis) for r in Rays] 738 Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances) 739 x_std = [] 740 y_std = [] 741 for i in range(len(distances)): 742 x_std.append(mp.StandardDeviation(Points[i][:,0])) 743 y_std.append(mp.StandardDeviation(Points[i][:,1])) 744 plt.ion() 745 fig, ax = plt.subplots() 746 ax.plot(distances, x_std, label="x std") 747 ax.plot(distances, y_std, label="y std") 748 ax.set_xlabel("Detector distance (mm)") 749 ax.set_ylabel("Standard deviation (mm)") 750 ax.legend() 751 plt.title("Caustics") 752 plt.show() 753 return fig
This function displays the caustics of the rays on the detector. To do so, it calculates the intersections of the rays with the detector over a range determined by the parameter Range, and then plots the standard deviation of the positions in the x and y directions.
OpticalChain : OpticalChain The optical chain to analyse.
DetectorName : str The name of the detector on which the caustics are calculated.
Range : float The range of the detector over which to calculate the caustics.
Npoints : int, optional The number of points to sample on the detector. The default is 1000.
fig : Figure The figure of the plot.
Provides the class for a general optical element, which serves to connect the “lab-frame”, in which the light rays are traced, to the proper “optic” coordinate frame in which an optic is defined. So this class is what serves to "align" the optics in lab-space, and it provides methods to modify its alignment.
The optic can be a Mirror-object or a Mask-object.
Created in Apr 2020
@author: Anthony Guillaume + Stefan Haessler
1""" 2Provides the class for a general optical element, which serves to connect the “lab-frame”, 3in which the light rays are traced, to the proper “optic” coordinate frame in 4which an optic is defined. So this class is what serves to "align" the optics in lab-space, 5and it provides methods to modify its alignment. 6 7The optic can be a [Mirror-object](ModuleMirror.html) or a [Mask-object](ModuleMask.html). 8 9 10 11 12 13Created in Apr 2020 14 15@author: Anthony Guillaume + Stefan Haessler 16""" 17# %% 18import ARTcore.ModuleGeometry as mgeo 19from ARTcore.ModuleGeometry import Origin 20 21import numpy as np 22from abc import ABC, abstractmethod 23import logging 24 25logger = logging.getLogger(__name__) 26 27 28# %% 29class OpticalElement(ABC): 30 """ 31 An optical element, can be either a Mirror or a Mask. In the future, one could add Gratings, DispersiveMedium etc... 32 33 Attributes: TODO update 34 ---------- 35 position : np.ndarray 36 Coordinate vector of the optical element’s center point in the lab frame. 37 What this center is, depends on the 'type', but it is generally the center of symmetry 38 of the Support of the optic. It is marked by the point 'P' in the drawings in the 39 documentation of the Mirror-classes for example. 40 41 orientation: np.quaternion 42 Orientation of the optic in the lab reference frame. From this, we calculate 43 the normal, the majoraxis and any other interesting vectors that are of 44 interest for a specific OpticalElement subclass. 45 46 normal : np.ndarray 47 Lab-frame vector pointing against the direction that would be considered 48 as normal incidence on the optical element. 49 50 majoraxis : np.ndarray 51 Lab-frame vector of another distinguished axis of non-rotationally symmetric optics, 52 like the major axes of toroidal/elliptical mirrors or the off-axis direction of 53 off-axis parabolas. This fixes the optical element’s rotation about 'normal'. 54 It is required to be perpendicular to 'normal', and is usually the x-axis in the 55 optic's proper coordinate frame. 56 57 What this 'majoraxis' is, e.g. for the different kinds of mirrors, is illustrated 58 in the documentation of the Mirror-classes. 59 60 For pure p or s polarization, the light incidence plane should be the 61 plane spanned by 'normal' and 'majoraxis', so that the incidence angle is varied 62 by rotating the optical element around the cross product 'normal' x 'majoraxis'. 63 64 65 Methods 66 ---------- 67 rotate_pitch_by(angle) 68 69 rotate_roll_by(angle) 70 71 rotate_yaw_by(angle) 72 73 rotate_random_by(angle) 74 75 shift_along_normal(distance) 76 77 shift_along_major(distance) 78 79 shift_along_cross(distance) 80 81 shift_along_random(distance) 82 83 """ 84 # %% Starting 3d orientation definition 85 _description = "Generic Optical Element" 86 87 def __hash__(self): 88 return super().__hash__()+hash(self._r)+hash(self._q) 89 90 @property 91 def description(self): 92 """ 93 Return a description of the optical element. 94 """ 95 return self._description 96 97 @property 98 def r(self): 99 """ 100 Return the offset of the optical element from its reference frame to the lab reference frame. 101 Not that this is **NOT** the position of the optical element's center point. Rather it is the 102 offset of the reference frame of the optical element from the lab reference frame. 103 """ 104 return self._r 105 106 @r.setter 107 def r(self, NewPosition): 108 """ 109 Set the offset of the optical element from its reference frame to the lab reference frame. 110 """ 111 if isinstance(NewPosition, mgeo.Point) and len(NewPosition) == 3: 112 self._r = NewPosition 113 else: 114 raise TypeError("Position must be a 3D mgeo.Point.") 115 @property 116 def q(self): 117 """ 118 Return the orientation of the optical element. 119 The orientation is stored as a unit quaternion representing the rotation from the optic's coordinate frame to the lab frame. 120 """ 121 return self._q 122 123 @q.setter 124 def q(self, NewOrientation): 125 """ 126 Set the orientation of the optical element. 127 This function normalizes the input quaternion before storing it. 128 If the input is not a quaternion, raise a TypeError. 129 """ 130 if isinstance(NewOrientation, np.quaternion): 131 self._q = NewOrientation.normalized() 132 else: 133 raise TypeError("Orientation must be a quaternion.") 134 135 @property 136 def position(self): 137 """ 138 Return the position of the basepoint of the optical element. Often it is the same as the optical centre. 139 This position is the one around which all rotations are performed. 140 """ 141 return self.r0 + self.r 142 143 @property 144 def orientation(self): 145 """ 146 Return the orientation of the optical element. 147 The utility of this method is unclear. 148 """ 149 return self.q 150 151 @property 152 def basis(self): 153 return self.r0, self.r, self.q 154 # %% Start of other methods 155 def __init__(self): 156 self._r = mgeo.Vector([0.0, 0.0, 0.0]) 157 self._q = np.quaternion(1, 0, 0, 0) 158 self.r0 = mgeo.Point([0.0, 0.0, 0.0]) 159 160 @abstractmethod 161 def propagate_raylist(self, RayList, alignment=False): 162 """ 163 This method is used to propagate a list of rays through the optical element. Implement this method in the subclasses. 164 In the case of a mirror, the method should reflect the rays off the mirror surface. 165 In the case of a mask, the method should block the rays that hit the mask. 166 In the case of a grating, the method should diffract the rays according to the grating equation. 167 """ 168 pass 169 170 def add_global_points(self, *args): 171 """ 172 Automatically add global properties for point attributes. 173 As arguments it takes the names of the global point attributes and seeks for the corresponding local point attributes. 174 Then it creates a property for the global point attribute. 175 176 Example: 177 Suppose the optical element has an attribute 'center_ref' that is the center of the optical element in the optic's coordinate frame. 178 Then, calling object.add_global_points('center') will create a property 'center' that returns the global position of the center of the optical element. 179 It takes into account the position and orientation of the optical element. 180 """ 181 for arg in args: 182 local_attr = f"{arg}_ref" 183 if hasattr(self, local_attr): 184 # Dynamically define a property for the global point 185 setattr(self.__class__, arg, property(self._create_global_point_property(local_attr))) 186 187 def add_global_vectors(self, *args): 188 """ 189 Automatically add global properties for vector attributes. 190 As arguments it takes the names of the global vector attributes and seeks for the corresponding local vector attributes. 191 Then it creates a property for the global vector attribute. 192 193 Example: 194 Suppose the optical element has an attribute 'normal_ref' that is the normal of the optical element in the optic's coordinate frame. 195 Then, calling object.add_global_vectors('normal') will create a property 'normal' that returns the global normal of the optical element. 196 """ 197 for arg in args: 198 local_attr = f"{arg}_ref" 199 if hasattr(self, local_attr): 200 # Dynamically define a property for the global vector 201 setattr(self.__class__, arg, property(self._create_global_vector_property(local_attr))) 202 203 def _create_global_point_property(self, local_attr): 204 """ 205 Return a function that computes the global point. 206 This is the actual function that is used to create the global point property. It performs the transformation of the local point to the global reference frame. 207 """ 208 def global_point(self): 209 # Translate the local point to the global reference frame 210 local_point = getattr(self, local_attr) 211 return local_point.from_basis(*self.basis) 212 return global_point 213 214 def _create_global_vector_property(self, local_attr): 215 """ 216 Return a function that computes the global vector. 217 This is the actual function that is used to create the global vector property. It performs the transformation of the local vector to the global reference frame. 218 """ 219 def global_vector(self): 220 # Rotate the local vector to the global reference frame 221 local_vector = getattr(self, local_attr) 222 return local_vector.from_basis(*self.basis) 223 return global_vector 224 # %% Starting 3d misalignment definitions 225 # This section needs to be reworked to be more general and to allow for more flexibility in the alignment of the optical elements. TODO 226 227 def rotate_pitch_by(self, angle): 228 """ 229 Pitch rotation, i.e. rotates the optical element about the axis ('normal' x 'majoraxis'), by the 230 given angle. 231 If the plane spanned by 'normal' and 'majoraxis' is the incidence plane (normally the case 232 in a "clean alignment" situation for pure p or s polarization), then this is simply a modificaiton 233 of the incidence angle by "angle". But in general, if the optical element has some odd orientation, 234 there is not a direct correspondence. 235 236 Parameters 237 ---------- 238 angle : float 239 Rotation angle in *degrees*. 240 """ 241 rotation_axis = np.cross(self.support_normal, self.majoraxis) 242 self.q = mgeo.QRotationAroundAxis(rotation_axis, np.deg2rad(angle))*self.q 243 244 def rotate_roll_by(self, angle): 245 """ 246 Roll rotation, i.e. rotates the optical element about its 'majoraxis' by the given angle. 247 248 Parameters 249 ---------- 250 angle : float 251 Rotation angle in *degrees*. 252 """ 253 254 self.q = mgeo.QRotationAroundAxis(self.majoraxis, np.deg2rad(angle))*self.q 255 256 def rotate_yaw_by(self, angle): 257 """ 258 Yaw rotation, i.e. rotates the optical element about its 'normal' by the given angle. 259 260 Parameters 261 ---------- 262 angle : float 263 Rotation angle in *degrees*. 264 """ 265 self.q = mgeo.QRotationAroundAxis(self.support_normal, np.deg2rad(angle))*self.q 266 267 def rotate_random_by(self, angle): 268 """ 269 Rotates the optical element about a randomly oriented axis by the given angle. 270 271 Parameters 272 ---------- 273 angle : float 274 Rotation angle in *degrees*. 275 """ 276 277 self.q = mgeo.QRotationAroundAxis(np.random.random(3), np.deg2rad(angle))*self.q 278 279 def shift_along_normal(self, distance): 280 """ 281 Shifts the optical element along its 'normal' by the given distance. 282 283 Parameters 284 ---------- 285 distance : float 286 Shift distance in mm. 287 """ 288 self.r = self.r + distance * self.support_normal 289 290 def shift_along_major(self, distance): 291 """ 292 Shifts the optical element along its 'majoraxis' by the given distance. 293 294 Parameters 295 ---------- 296 distance : float 297 Shift distance in mm. 298 """ 299 self.r = self.r + distance * self.majoraxis 300 301 def shift_along_cross(self, distance): 302 """ 303 Shifts the optical element along the axis 'normal'x'majoraxis' 304 (typically normal to the light incidence plane) by the given distance. 305 306 Parameters 307 ---------- 308 distance : float 309 Shift distance in mm. 310 """ 311 self.r = self.r + distance * mgeo.Normalize(np.cross(self.support_normal, self.majoraxis)) 312 313 def shift_along_random(self, distance): 314 """ 315 Shifts the optical element along a random direction by the given distance. 316 317 Parameters 318 ---------- 319 distance : float 320 Shift distance in mm. 321 """ 322 self.r = self.r + distance * mgeo.Normalize(np.random.random(3))
30class OpticalElement(ABC): 31 """ 32 An optical element, can be either a Mirror or a Mask. In the future, one could add Gratings, DispersiveMedium etc... 33 34 Attributes: TODO update 35 ---------- 36 position : np.ndarray 37 Coordinate vector of the optical element’s center point in the lab frame. 38 What this center is, depends on the 'type', but it is generally the center of symmetry 39 of the Support of the optic. It is marked by the point 'P' in the drawings in the 40 documentation of the Mirror-classes for example. 41 42 orientation: np.quaternion 43 Orientation of the optic in the lab reference frame. From this, we calculate 44 the normal, the majoraxis and any other interesting vectors that are of 45 interest for a specific OpticalElement subclass. 46 47 normal : np.ndarray 48 Lab-frame vector pointing against the direction that would be considered 49 as normal incidence on the optical element. 50 51 majoraxis : np.ndarray 52 Lab-frame vector of another distinguished axis of non-rotationally symmetric optics, 53 like the major axes of toroidal/elliptical mirrors or the off-axis direction of 54 off-axis parabolas. This fixes the optical element’s rotation about 'normal'. 55 It is required to be perpendicular to 'normal', and is usually the x-axis in the 56 optic's proper coordinate frame. 57 58 What this 'majoraxis' is, e.g. for the different kinds of mirrors, is illustrated 59 in the documentation of the Mirror-classes. 60 61 For pure p or s polarization, the light incidence plane should be the 62 plane spanned by 'normal' and 'majoraxis', so that the incidence angle is varied 63 by rotating the optical element around the cross product 'normal' x 'majoraxis'. 64 65 66 Methods 67 ---------- 68 rotate_pitch_by(angle) 69 70 rotate_roll_by(angle) 71 72 rotate_yaw_by(angle) 73 74 rotate_random_by(angle) 75 76 shift_along_normal(distance) 77 78 shift_along_major(distance) 79 80 shift_along_cross(distance) 81 82 shift_along_random(distance) 83 84 """ 85 # %% Starting 3d orientation definition 86 _description = "Generic Optical Element" 87 88 def __hash__(self): 89 return super().__hash__()+hash(self._r)+hash(self._q) 90 91 @property 92 def description(self): 93 """ 94 Return a description of the optical element. 95 """ 96 return self._description 97 98 @property 99 def r(self): 100 """ 101 Return the offset of the optical element from its reference frame to the lab reference frame. 102 Not that this is **NOT** the position of the optical element's center point. Rather it is the 103 offset of the reference frame of the optical element from the lab reference frame. 104 """ 105 return self._r 106 107 @r.setter 108 def r(self, NewPosition): 109 """ 110 Set the offset of the optical element from its reference frame to the lab reference frame. 111 """ 112 if isinstance(NewPosition, mgeo.Point) and len(NewPosition) == 3: 113 self._r = NewPosition 114 else: 115 raise TypeError("Position must be a 3D mgeo.Point.") 116 @property 117 def q(self): 118 """ 119 Return the orientation of the optical element. 120 The orientation is stored as a unit quaternion representing the rotation from the optic's coordinate frame to the lab frame. 121 """ 122 return self._q 123 124 @q.setter 125 def q(self, NewOrientation): 126 """ 127 Set the orientation of the optical element. 128 This function normalizes the input quaternion before storing it. 129 If the input is not a quaternion, raise a TypeError. 130 """ 131 if isinstance(NewOrientation, np.quaternion): 132 self._q = NewOrientation.normalized() 133 else: 134 raise TypeError("Orientation must be a quaternion.") 135 136 @property 137 def position(self): 138 """ 139 Return the position of the basepoint of the optical element. Often it is the same as the optical centre. 140 This position is the one around which all rotations are performed. 141 """ 142 return self.r0 + self.r 143 144 @property 145 def orientation(self): 146 """ 147 Return the orientation of the optical element. 148 The utility of this method is unclear. 149 """ 150 return self.q 151 152 @property 153 def basis(self): 154 return self.r0, self.r, self.q 155 # %% Start of other methods 156 def __init__(self): 157 self._r = mgeo.Vector([0.0, 0.0, 0.0]) 158 self._q = np.quaternion(1, 0, 0, 0) 159 self.r0 = mgeo.Point([0.0, 0.0, 0.0]) 160 161 @abstractmethod 162 def propagate_raylist(self, RayList, alignment=False): 163 """ 164 This method is used to propagate a list of rays through the optical element. Implement this method in the subclasses. 165 In the case of a mirror, the method should reflect the rays off the mirror surface. 166 In the case of a mask, the method should block the rays that hit the mask. 167 In the case of a grating, the method should diffract the rays according to the grating equation. 168 """ 169 pass 170 171 def add_global_points(self, *args): 172 """ 173 Automatically add global properties for point attributes. 174 As arguments it takes the names of the global point attributes and seeks for the corresponding local point attributes. 175 Then it creates a property for the global point attribute. 176 177 Example: 178 Suppose the optical element has an attribute 'center_ref' that is the center of the optical element in the optic's coordinate frame. 179 Then, calling object.add_global_points('center') will create a property 'center' that returns the global position of the center of the optical element. 180 It takes into account the position and orientation of the optical element. 181 """ 182 for arg in args: 183 local_attr = f"{arg}_ref" 184 if hasattr(self, local_attr): 185 # Dynamically define a property for the global point 186 setattr(self.__class__, arg, property(self._create_global_point_property(local_attr))) 187 188 def add_global_vectors(self, *args): 189 """ 190 Automatically add global properties for vector attributes. 191 As arguments it takes the names of the global vector attributes and seeks for the corresponding local vector attributes. 192 Then it creates a property for the global vector attribute. 193 194 Example: 195 Suppose the optical element has an attribute 'normal_ref' that is the normal of the optical element in the optic's coordinate frame. 196 Then, calling object.add_global_vectors('normal') will create a property 'normal' that returns the global normal of the optical element. 197 """ 198 for arg in args: 199 local_attr = f"{arg}_ref" 200 if hasattr(self, local_attr): 201 # Dynamically define a property for the global vector 202 setattr(self.__class__, arg, property(self._create_global_vector_property(local_attr))) 203 204 def _create_global_point_property(self, local_attr): 205 """ 206 Return a function that computes the global point. 207 This is the actual function that is used to create the global point property. It performs the transformation of the local point to the global reference frame. 208 """ 209 def global_point(self): 210 # Translate the local point to the global reference frame 211 local_point = getattr(self, local_attr) 212 return local_point.from_basis(*self.basis) 213 return global_point 214 215 def _create_global_vector_property(self, local_attr): 216 """ 217 Return a function that computes the global vector. 218 This is the actual function that is used to create the global vector property. It performs the transformation of the local vector to the global reference frame. 219 """ 220 def global_vector(self): 221 # Rotate the local vector to the global reference frame 222 local_vector = getattr(self, local_attr) 223 return local_vector.from_basis(*self.basis) 224 return global_vector 225 # %% Starting 3d misalignment definitions 226 # This section needs to be reworked to be more general and to allow for more flexibility in the alignment of the optical elements. TODO 227 228 def rotate_pitch_by(self, angle): 229 """ 230 Pitch rotation, i.e. rotates the optical element about the axis ('normal' x 'majoraxis'), by the 231 given angle. 232 If the plane spanned by 'normal' and 'majoraxis' is the incidence plane (normally the case 233 in a "clean alignment" situation for pure p or s polarization), then this is simply a modificaiton 234 of the incidence angle by "angle". But in general, if the optical element has some odd orientation, 235 there is not a direct correspondence. 236 237 Parameters 238 ---------- 239 angle : float 240 Rotation angle in *degrees*. 241 """ 242 rotation_axis = np.cross(self.support_normal, self.majoraxis) 243 self.q = mgeo.QRotationAroundAxis(rotation_axis, np.deg2rad(angle))*self.q 244 245 def rotate_roll_by(self, angle): 246 """ 247 Roll rotation, i.e. rotates the optical element about its 'majoraxis' by the given angle. 248 249 Parameters 250 ---------- 251 angle : float 252 Rotation angle in *degrees*. 253 """ 254 255 self.q = mgeo.QRotationAroundAxis(self.majoraxis, np.deg2rad(angle))*self.q 256 257 def rotate_yaw_by(self, angle): 258 """ 259 Yaw rotation, i.e. rotates the optical element about its 'normal' by the given angle. 260 261 Parameters 262 ---------- 263 angle : float 264 Rotation angle in *degrees*. 265 """ 266 self.q = mgeo.QRotationAroundAxis(self.support_normal, np.deg2rad(angle))*self.q 267 268 def rotate_random_by(self, angle): 269 """ 270 Rotates the optical element about a randomly oriented axis by the given angle. 271 272 Parameters 273 ---------- 274 angle : float 275 Rotation angle in *degrees*. 276 """ 277 278 self.q = mgeo.QRotationAroundAxis(np.random.random(3), np.deg2rad(angle))*self.q 279 280 def shift_along_normal(self, distance): 281 """ 282 Shifts the optical element along its 'normal' by the given distance. 283 284 Parameters 285 ---------- 286 distance : float 287 Shift distance in mm. 288 """ 289 self.r = self.r + distance * self.support_normal 290 291 def shift_along_major(self, distance): 292 """ 293 Shifts the optical element along its 'majoraxis' by the given distance. 294 295 Parameters 296 ---------- 297 distance : float 298 Shift distance in mm. 299 """ 300 self.r = self.r + distance * self.majoraxis 301 302 def shift_along_cross(self, distance): 303 """ 304 Shifts the optical element along the axis 'normal'x'majoraxis' 305 (typically normal to the light incidence plane) by the given distance. 306 307 Parameters 308 ---------- 309 distance : float 310 Shift distance in mm. 311 """ 312 self.r = self.r + distance * mgeo.Normalize(np.cross(self.support_normal, self.majoraxis)) 313 314 def shift_along_random(self, distance): 315 """ 316 Shifts the optical element along a random direction by the given distance. 317 318 Parameters 319 ---------- 320 distance : float 321 Shift distance in mm. 322 """ 323 self.r = self.r + distance * mgeo.Normalize(np.random.random(3))
An optical element, can be either a Mirror or a Mask. In the future, one could add Gratings, DispersiveMedium etc...
position : np.ndarray
Coordinate vector of the optical element’s center point in the lab frame.
What this center is, depends on the 'type', but it is generally the center of symmetry
of the Support of the optic. It is marked by the point 'P' in the drawings in the
documentation of the Mirror-classes for example.
orientation: np.quaternion
Orientation of the optic in the lab reference frame. From this, we calculate
the normal, the majoraxis and any other interesting vectors that are of
interest for a specific OpticalElement subclass.
normal : np.ndarray
Lab-frame vector pointing against the direction that would be considered
as normal incidence on the optical element.
majoraxis : np.ndarray
Lab-frame vector of another distinguished axis of non-rotationally symmetric optics,
like the major axes of toroidal/elliptical mirrors or the off-axis direction of
off-axis parabolas. This fixes the optical element’s rotation about 'normal'.
It is required to be perpendicular to 'normal', and is usually the x-axis in the
optic's proper coordinate frame.
What this 'majoraxis' is, e.g. for the different kinds of mirrors, is illustrated
in the documentation of the Mirror-classes.
For pure p or s polarization, the light incidence plane should be the
plane spanned by 'normal' and 'majoraxis', so that the incidence angle is varied
by rotating the optical element around the cross product 'normal' x 'majoraxis'.
rotate_pitch_by(angle)
rotate_roll_by(angle)
rotate_yaw_by(angle)
rotate_random_by(angle)
shift_along_normal(distance)
shift_along_major(distance)
shift_along_cross(distance)
shift_along_random(distance)
91 @property 92 def description(self): 93 """ 94 Return a description of the optical element. 95 """ 96 return self._description
Return a description of the optical element.
98 @property 99 def r(self): 100 """ 101 Return the offset of the optical element from its reference frame to the lab reference frame. 102 Not that this is **NOT** the position of the optical element's center point. Rather it is the 103 offset of the reference frame of the optical element from the lab reference frame. 104 """ 105 return self._r
Return the offset of the optical element from its reference frame to the lab reference frame. Not that this is NOT the position of the optical element's center point. Rather it is the offset of the reference frame of the optical element from the lab reference frame.
116 @property 117 def q(self): 118 """ 119 Return the orientation of the optical element. 120 The orientation is stored as a unit quaternion representing the rotation from the optic's coordinate frame to the lab frame. 121 """ 122 return self._q
Return the orientation of the optical element. The orientation is stored as a unit quaternion representing the rotation from the optic's coordinate frame to the lab frame.
136 @property 137 def position(self): 138 """ 139 Return the position of the basepoint of the optical element. Often it is the same as the optical centre. 140 This position is the one around which all rotations are performed. 141 """ 142 return self.r0 + self.r
Return the position of the basepoint of the optical element. Often it is the same as the optical centre. This position is the one around which all rotations are performed.
144 @property 145 def orientation(self): 146 """ 147 Return the orientation of the optical element. 148 The utility of this method is unclear. 149 """ 150 return self.q
Return the orientation of the optical element. The utility of this method is unclear.
161 @abstractmethod 162 def propagate_raylist(self, RayList, alignment=False): 163 """ 164 This method is used to propagate a list of rays through the optical element. Implement this method in the subclasses. 165 In the case of a mirror, the method should reflect the rays off the mirror surface. 166 In the case of a mask, the method should block the rays that hit the mask. 167 In the case of a grating, the method should diffract the rays according to the grating equation. 168 """ 169 pass
This method is used to propagate a list of rays through the optical element. Implement this method in the subclasses. In the case of a mirror, the method should reflect the rays off the mirror surface. In the case of a mask, the method should block the rays that hit the mask. In the case of a grating, the method should diffract the rays according to the grating equation.
171 def add_global_points(self, *args): 172 """ 173 Automatically add global properties for point attributes. 174 As arguments it takes the names of the global point attributes and seeks for the corresponding local point attributes. 175 Then it creates a property for the global point attribute. 176 177 Example: 178 Suppose the optical element has an attribute 'center_ref' that is the center of the optical element in the optic's coordinate frame. 179 Then, calling object.add_global_points('center') will create a property 'center' that returns the global position of the center of the optical element. 180 It takes into account the position and orientation of the optical element. 181 """ 182 for arg in args: 183 local_attr = f"{arg}_ref" 184 if hasattr(self, local_attr): 185 # Dynamically define a property for the global point 186 setattr(self.__class__, arg, property(self._create_global_point_property(local_attr)))
Automatically add global properties for point attributes. As arguments it takes the names of the global point attributes and seeks for the corresponding local point attributes. Then it creates a property for the global point attribute.
Example: Suppose the optical element has an attribute 'center_ref' that is the center of the optical element in the optic's coordinate frame. Then, calling object.add_global_points('center') will create a property 'center' that returns the global position of the center of the optical element. It takes into account the position and orientation of the optical element.
188 def add_global_vectors(self, *args): 189 """ 190 Automatically add global properties for vector attributes. 191 As arguments it takes the names of the global vector attributes and seeks for the corresponding local vector attributes. 192 Then it creates a property for the global vector attribute. 193 194 Example: 195 Suppose the optical element has an attribute 'normal_ref' that is the normal of the optical element in the optic's coordinate frame. 196 Then, calling object.add_global_vectors('normal') will create a property 'normal' that returns the global normal of the optical element. 197 """ 198 for arg in args: 199 local_attr = f"{arg}_ref" 200 if hasattr(self, local_attr): 201 # Dynamically define a property for the global vector 202 setattr(self.__class__, arg, property(self._create_global_vector_property(local_attr)))
Automatically add global properties for vector attributes. As arguments it takes the names of the global vector attributes and seeks for the corresponding local vector attributes. Then it creates a property for the global vector attribute.
Example: Suppose the optical element has an attribute 'normal_ref' that is the normal of the optical element in the optic's coordinate frame. Then, calling object.add_global_vectors('normal') will create a property 'normal' that returns the global normal of the optical element.
228 def rotate_pitch_by(self, angle): 229 """ 230 Pitch rotation, i.e. rotates the optical element about the axis ('normal' x 'majoraxis'), by the 231 given angle. 232 If the plane spanned by 'normal' and 'majoraxis' is the incidence plane (normally the case 233 in a "clean alignment" situation for pure p or s polarization), then this is simply a modificaiton 234 of the incidence angle by "angle". But in general, if the optical element has some odd orientation, 235 there is not a direct correspondence. 236 237 Parameters 238 ---------- 239 angle : float 240 Rotation angle in *degrees*. 241 """ 242 rotation_axis = np.cross(self.support_normal, self.majoraxis) 243 self.q = mgeo.QRotationAroundAxis(rotation_axis, np.deg2rad(angle))*self.q
Pitch rotation, i.e. rotates the optical element about the axis ('normal' x 'majoraxis'), by the given angle. If the plane spanned by 'normal' and 'majoraxis' is the incidence plane (normally the case in a "clean alignment" situation for pure p or s polarization), then this is simply a modificaiton of the incidence angle by "angle". But in general, if the optical element has some odd orientation, there is not a direct correspondence.
angle : float
Rotation angle in *degrees*.
245 def rotate_roll_by(self, angle): 246 """ 247 Roll rotation, i.e. rotates the optical element about its 'majoraxis' by the given angle. 248 249 Parameters 250 ---------- 251 angle : float 252 Rotation angle in *degrees*. 253 """ 254 255 self.q = mgeo.QRotationAroundAxis(self.majoraxis, np.deg2rad(angle))*self.q
Roll rotation, i.e. rotates the optical element about its 'majoraxis' by the given angle.
angle : float
Rotation angle in *degrees*.
257 def rotate_yaw_by(self, angle): 258 """ 259 Yaw rotation, i.e. rotates the optical element about its 'normal' by the given angle. 260 261 Parameters 262 ---------- 263 angle : float 264 Rotation angle in *degrees*. 265 """ 266 self.q = mgeo.QRotationAroundAxis(self.support_normal, np.deg2rad(angle))*self.q
Yaw rotation, i.e. rotates the optical element about its 'normal' by the given angle.
angle : float
Rotation angle in *degrees*.
268 def rotate_random_by(self, angle): 269 """ 270 Rotates the optical element about a randomly oriented axis by the given angle. 271 272 Parameters 273 ---------- 274 angle : float 275 Rotation angle in *degrees*. 276 """ 277 278 self.q = mgeo.QRotationAroundAxis(np.random.random(3), np.deg2rad(angle))*self.q
Rotates the optical element about a randomly oriented axis by the given angle.
angle : float
Rotation angle in *degrees*.
280 def shift_along_normal(self, distance): 281 """ 282 Shifts the optical element along its 'normal' by the given distance. 283 284 Parameters 285 ---------- 286 distance : float 287 Shift distance in mm. 288 """ 289 self.r = self.r + distance * self.support_normal
Shifts the optical element along its 'normal' by the given distance.
distance : float
Shift distance in mm.
291 def shift_along_major(self, distance): 292 """ 293 Shifts the optical element along its 'majoraxis' by the given distance. 294 295 Parameters 296 ---------- 297 distance : float 298 Shift distance in mm. 299 """ 300 self.r = self.r + distance * self.majoraxis
Shifts the optical element along its 'majoraxis' by the given distance.
distance : float
Shift distance in mm.
302 def shift_along_cross(self, distance): 303 """ 304 Shifts the optical element along the axis 'normal'x'majoraxis' 305 (typically normal to the light incidence plane) by the given distance. 306 307 Parameters 308 ---------- 309 distance : float 310 Shift distance in mm. 311 """ 312 self.r = self.r + distance * mgeo.Normalize(np.cross(self.support_normal, self.majoraxis))
Shifts the optical element along the axis 'normal'x'majoraxis' (typically normal to the light incidence plane) by the given distance.
distance : float
Shift distance in mm.
314 def shift_along_random(self, distance): 315 """ 316 Shifts the optical element along a random direction by the given distance. 317 318 Parameters 319 ---------- 320 distance : float 321 Shift distance in mm. 322 """ 323 self.r = self.r + distance * mgeo.Normalize(np.random.random(3))
Shifts the optical element along a random direction by the given distance.
distance : float
Shift distance in mm.
Created in Apr 2020
@author: Anthony Guillaume + Stefan Haessler + André Kalouguine
1""" 2Created in Apr 2020 3 4@author: Anthony Guillaume + Stefan Haessler + André Kalouguine 5""" 6# %% Modules 7 8import numpy as np 9import logging 10import ARTcore.ModuleGeometry as mgeo 11logger = logging.getLogger(__name__) 12import time 13from dataclasses import dataclass, replace 14 15# %% Ray class definition 16 17@dataclass(slots=True) 18class Ray: 19 """ 20 Represents an optical ray from geometrical optics. 21 In order to optimize lookups, Rays are represented as dataclasses. 22 The mandatory attributes are: 23 - point: mgeo.Point 24 - vector: mgeo.Vector 25 """ 26 point: mgeo.Point 27 vector: mgeo.Vector 28 path: tuple = (0.0,) 29 number: int = None 30 wavelength: float = None 31 incidence: float = None 32 intensity: float = None 33 34 def to_basis(self, r0, r, q): 35 """ 36 Transforms the ray to the basis defined by the origin r0, the x-axis r and the y-axis q. 37 """ 38 return Ray( 39 self.point.to_basis(r0, r, q), 40 self.vector.to_basis(r0, r, q), 41 self.path, 42 self.number, 43 self.wavelength, 44 self.incidence, 45 self.intensity, 46 ) 47 48 def from_basis(self, r0, r, q): 49 """ 50 Transforms the ray from the basis defined by the origin r0, the x-axis r and the y-axis q. 51 """ 52 return Ray( 53 self.point.from_basis(r0, r, q), 54 self.vector.from_basis(r0, r, q), 55 self.path, 56 self.number, 57 self.wavelength, 58 self.incidence, 59 self.intensity, 60 ) 61 62 def rotate(self, q): 63 """ 64 Rotates the ray by the quaternion q. 65 """ 66 return Ray( 67 self.point.rotate(q), 68 self.vector.rotate(q), 69 self.path, 70 self.number, 71 self.wavelength, 72 self.incidence, 73 self.intensity, 74 ) 75 76 def translate(self, t): 77 """ 78 Rotates the ray by the vector t 79 """ 80 return Ray( 81 self.point.translate(t), 82 self.vector, 83 self.path, 84 self.number, 85 self.wavelength, 86 self.incidence, 87 self.intensity, 88 ) 89 90 def __copy__(self): 91 """ 92 Returns a new OpticalRay object with the same properties. 93 """ 94 return replace(self) 95 96 def __hash__(self): 97 point_tuple = tuple(self.point.reshape(1, -1)[0]) 98 vector_tuple = tuple(self.vector.reshape(1, -1)[0]) 99 return hash( 100 point_tuple + vector_tuple + (self.path, self.number, self.wavelength, self.incidence, self.intensity) 101 ) 102 103 104@dataclass(slots=True) 105class RayList: 106 """ 107 Class representing a list of rays in a form that is well suited to calculations. 108 Specifically, the points, vectors etc are all numpy arrays 109 It can then be converted to a list of Ray objects with the method 110 """ 111 point: mgeo.PointArray 112 vector: mgeo.VectorArray 113 path: np.ndarray 114 number: np.ndarray 115 wavelength: np.ndarray 116 incidence: np.ndarray 117 intensity: np.ndarray 118 N: int = None 119 120 @classmethod 121 def from_list(cls, rays): 122 N = len(rays) 123 if N == 0: 124 return cls( 125 mgeo.PointArray([[np.nan, np.nan, np.nan]]), 126 mgeo.VectorArray([[np.nan,np.nan,np.nan]]), 127 np.zeros((1,1)), 128 np.zeros(1), 129 np.zeros(1), 130 np.zeros(1), 131 np.zeros(1), 132 0, 133 ) 134 N_r = len(rays[0].path) 135 points = np.zeros((N, 3)) 136 vectors = np.zeros((N, 3)) 137 paths = np.zeros((N, N_r)) 138 numbers = np.zeros(N, dtype=int) 139 wavelengths = np.zeros(N) 140 incidences = np.zeros(N) 141 intensities = np.zeros(N) 142 for i, ray in enumerate(rays): 143 points[i:,] = ray.point 144 vectors[i:,] = ray.vector 145 paths[i:,] = ray.path 146 numbers[i] = ray.number 147 wavelengths[i] = ray.wavelength 148 incidences[i] = ray.incidence 149 intensities[i] = ray.intensity 150 return cls( 151 mgeo.PointArray(points), 152 mgeo.VectorArray(vectors), 153 paths, 154 numbers, 155 wavelengths, 156 incidences, 157 intensities, 158 len(rays), 159 ) 160 161 def __getitem__(self, key): 162 if isinstance(key, slice) or isinstance(key, list): 163 return RayList( 164 self.point[key], 165 self.vector[key], 166 self.path[key], 167 self.number[key], 168 self.wavelength[key], 169 self.incidence[key], 170 self.intensity[key], 171 len(key), 172 ) 173 return Ray( 174 self.point[key], 175 self.vector[key], 176 tuple(self.path[key]), 177 self.number[key], 178 self.wavelength[key], 179 self.incidence[key], 180 self.intensity[key], 181 ) 182 183 def __len__(self): 184 return self.N 185 186 def __iter__(self): 187 if self.N == 0: 188 return iter([]) 189 return (Ray(point=self.point[i], 190 vector=self.vector[i], 191 path=tuple(self.path[i]), 192 number=int(self.number[i]), 193 wavelength=float(self.wavelength[i]), 194 incidence=float(self.incidence[i]), 195 intensity=float(self.intensity[i])) for i in range(self.N)) 196 197 def to_basis(self, r0, r, q): 198 """ 199 Transforms the ray to the basis defined by the origin r0, the x-axis r and the y-axis q. 200 """ 201 return RayList( 202 self.point.to_basis(r0, r, q), 203 self.vector.to_basis(r0, r, q), 204 self.path, 205 self.number, 206 self.wavelength, 207 self.incidence, 208 self.intensity, 209 self.N 210 ) 211 212 def from_basis(self, r0, r, q): 213 """ 214 Transforms the ray from the basis defined by the origin r0, the x-axis r and the y-axis q. 215 """ 216 return RayList( 217 self.point.from_basis(r0, r, q), 218 self.vector.from_basis(r0, r, q), 219 self.path, 220 self.number, 221 self.wavelength, 222 self.incidence, 223 self.intensity, 224 self.N 225 ) 226 227 def rotate(self, q): 228 """ 229 Rotates the raylist by the quaternion q. 230 """ 231 return RayList( 232 self.point.rotate(q), 233 self.vector.rotate(q), 234 self.path, 235 self.number, 236 self.wavelength, 237 self.incidence, 238 self.intensity, 239 self.N 240 ) 241 242 def translate(self, t): 243 """ 244 Rotates the ray by the vector t 245 """ 246 return RayList( 247 self.point.translate(t), 248 self.vector, 249 self.path, 250 self.number, 251 self.wavelength, 252 self.incidence, 253 self.intensity, 254 self.N 255 ) 256 257 def __copy__(self): 258 """ 259 Returns a new OpticalRay object with the same properties. 260 """ 261 return RayList( 262 self.point.copy(), 263 self.vector.copy(), 264 self.path.copy(), 265 self.number.copy(), 266 self.wavelength.copy(), 267 self.incidence.copy(), 268 self.intensity.copy(), 269 self.N, 270 ) 271 272 def __hash__(self): 273 points = self.point.reshape(1, -1)[0] 274 vectors = self.vector.reshape(1, -1)[0] 275 paths = self.path.reshape(1, -1)[0] 276 result = np.concatenate((points, vectors, paths, self.number, self.wavelength, self.incidence, self.intensity)) 277 result = result[~np.isnan(result)] 278 result.flags.writeable = False 279 result_hash = hash(result.data.tobytes()) 280 return result_hash 281 282 def __repr__(self) -> str: 283 intro = f"RayList with {self.N} rays\n" 284 average = mgeo.Vector(np.mean(self.vector)).normalized() 285 vectors = self.vector.normalized() 286 angles = np.arccos(np.clip(np.dot(vectors, average), -1.0, 1.0)) 287 avg_string = "On average, the rays are oriented along the vector:\n" + str(average) + "\n" 288 NA = f"Numerical aperture: {np.max(np.sin(angles)):.3f}\n" 289 return intro +avg_string+ NA
18@dataclass(slots=True) 19class Ray: 20 """ 21 Represents an optical ray from geometrical optics. 22 In order to optimize lookups, Rays are represented as dataclasses. 23 The mandatory attributes are: 24 - point: mgeo.Point 25 - vector: mgeo.Vector 26 """ 27 point: mgeo.Point 28 vector: mgeo.Vector 29 path: tuple = (0.0,) 30 number: int = None 31 wavelength: float = None 32 incidence: float = None 33 intensity: float = None 34 35 def to_basis(self, r0, r, q): 36 """ 37 Transforms the ray to the basis defined by the origin r0, the x-axis r and the y-axis q. 38 """ 39 return Ray( 40 self.point.to_basis(r0, r, q), 41 self.vector.to_basis(r0, r, q), 42 self.path, 43 self.number, 44 self.wavelength, 45 self.incidence, 46 self.intensity, 47 ) 48 49 def from_basis(self, r0, r, q): 50 """ 51 Transforms the ray from the basis defined by the origin r0, the x-axis r and the y-axis q. 52 """ 53 return Ray( 54 self.point.from_basis(r0, r, q), 55 self.vector.from_basis(r0, r, q), 56 self.path, 57 self.number, 58 self.wavelength, 59 self.incidence, 60 self.intensity, 61 ) 62 63 def rotate(self, q): 64 """ 65 Rotates the ray by the quaternion q. 66 """ 67 return Ray( 68 self.point.rotate(q), 69 self.vector.rotate(q), 70 self.path, 71 self.number, 72 self.wavelength, 73 self.incidence, 74 self.intensity, 75 ) 76 77 def translate(self, t): 78 """ 79 Rotates the ray by the vector t 80 """ 81 return Ray( 82 self.point.translate(t), 83 self.vector, 84 self.path, 85 self.number, 86 self.wavelength, 87 self.incidence, 88 self.intensity, 89 ) 90 91 def __copy__(self): 92 """ 93 Returns a new OpticalRay object with the same properties. 94 """ 95 return replace(self) 96 97 def __hash__(self): 98 point_tuple = tuple(self.point.reshape(1, -1)[0]) 99 vector_tuple = tuple(self.vector.reshape(1, -1)[0]) 100 return hash( 101 point_tuple + vector_tuple + (self.path, self.number, self.wavelength, self.incidence, self.intensity) 102 )
Represents an optical ray from geometrical optics. In order to optimize lookups, Rays are represented as dataclasses. The mandatory attributes are:
35 def to_basis(self, r0, r, q): 36 """ 37 Transforms the ray to the basis defined by the origin r0, the x-axis r and the y-axis q. 38 """ 39 return Ray( 40 self.point.to_basis(r0, r, q), 41 self.vector.to_basis(r0, r, q), 42 self.path, 43 self.number, 44 self.wavelength, 45 self.incidence, 46 self.intensity, 47 )
Transforms the ray to the basis defined by the origin r0, the x-axis r and the y-axis q.
49 def from_basis(self, r0, r, q): 50 """ 51 Transforms the ray from the basis defined by the origin r0, the x-axis r and the y-axis q. 52 """ 53 return Ray( 54 self.point.from_basis(r0, r, q), 55 self.vector.from_basis(r0, r, q), 56 self.path, 57 self.number, 58 self.wavelength, 59 self.incidence, 60 self.intensity, 61 )
Transforms the ray from the basis defined by the origin r0, the x-axis r and the y-axis q.
105@dataclass(slots=True) 106class RayList: 107 """ 108 Class representing a list of rays in a form that is well suited to calculations. 109 Specifically, the points, vectors etc are all numpy arrays 110 It can then be converted to a list of Ray objects with the method 111 """ 112 point: mgeo.PointArray 113 vector: mgeo.VectorArray 114 path: np.ndarray 115 number: np.ndarray 116 wavelength: np.ndarray 117 incidence: np.ndarray 118 intensity: np.ndarray 119 N: int = None 120 121 @classmethod 122 def from_list(cls, rays): 123 N = len(rays) 124 if N == 0: 125 return cls( 126 mgeo.PointArray([[np.nan, np.nan, np.nan]]), 127 mgeo.VectorArray([[np.nan,np.nan,np.nan]]), 128 np.zeros((1,1)), 129 np.zeros(1), 130 np.zeros(1), 131 np.zeros(1), 132 np.zeros(1), 133 0, 134 ) 135 N_r = len(rays[0].path) 136 points = np.zeros((N, 3)) 137 vectors = np.zeros((N, 3)) 138 paths = np.zeros((N, N_r)) 139 numbers = np.zeros(N, dtype=int) 140 wavelengths = np.zeros(N) 141 incidences = np.zeros(N) 142 intensities = np.zeros(N) 143 for i, ray in enumerate(rays): 144 points[i:,] = ray.point 145 vectors[i:,] = ray.vector 146 paths[i:,] = ray.path 147 numbers[i] = ray.number 148 wavelengths[i] = ray.wavelength 149 incidences[i] = ray.incidence 150 intensities[i] = ray.intensity 151 return cls( 152 mgeo.PointArray(points), 153 mgeo.VectorArray(vectors), 154 paths, 155 numbers, 156 wavelengths, 157 incidences, 158 intensities, 159 len(rays), 160 ) 161 162 def __getitem__(self, key): 163 if isinstance(key, slice) or isinstance(key, list): 164 return RayList( 165 self.point[key], 166 self.vector[key], 167 self.path[key], 168 self.number[key], 169 self.wavelength[key], 170 self.incidence[key], 171 self.intensity[key], 172 len(key), 173 ) 174 return Ray( 175 self.point[key], 176 self.vector[key], 177 tuple(self.path[key]), 178 self.number[key], 179 self.wavelength[key], 180 self.incidence[key], 181 self.intensity[key], 182 ) 183 184 def __len__(self): 185 return self.N 186 187 def __iter__(self): 188 if self.N == 0: 189 return iter([]) 190 return (Ray(point=self.point[i], 191 vector=self.vector[i], 192 path=tuple(self.path[i]), 193 number=int(self.number[i]), 194 wavelength=float(self.wavelength[i]), 195 incidence=float(self.incidence[i]), 196 intensity=float(self.intensity[i])) for i in range(self.N)) 197 198 def to_basis(self, r0, r, q): 199 """ 200 Transforms the ray to the basis defined by the origin r0, the x-axis r and the y-axis q. 201 """ 202 return RayList( 203 self.point.to_basis(r0, r, q), 204 self.vector.to_basis(r0, r, q), 205 self.path, 206 self.number, 207 self.wavelength, 208 self.incidence, 209 self.intensity, 210 self.N 211 ) 212 213 def from_basis(self, r0, r, q): 214 """ 215 Transforms the ray from the basis defined by the origin r0, the x-axis r and the y-axis q. 216 """ 217 return RayList( 218 self.point.from_basis(r0, r, q), 219 self.vector.from_basis(r0, r, q), 220 self.path, 221 self.number, 222 self.wavelength, 223 self.incidence, 224 self.intensity, 225 self.N 226 ) 227 228 def rotate(self, q): 229 """ 230 Rotates the raylist by the quaternion q. 231 """ 232 return RayList( 233 self.point.rotate(q), 234 self.vector.rotate(q), 235 self.path, 236 self.number, 237 self.wavelength, 238 self.incidence, 239 self.intensity, 240 self.N 241 ) 242 243 def translate(self, t): 244 """ 245 Rotates the ray by the vector t 246 """ 247 return RayList( 248 self.point.translate(t), 249 self.vector, 250 self.path, 251 self.number, 252 self.wavelength, 253 self.incidence, 254 self.intensity, 255 self.N 256 ) 257 258 def __copy__(self): 259 """ 260 Returns a new OpticalRay object with the same properties. 261 """ 262 return RayList( 263 self.point.copy(), 264 self.vector.copy(), 265 self.path.copy(), 266 self.number.copy(), 267 self.wavelength.copy(), 268 self.incidence.copy(), 269 self.intensity.copy(), 270 self.N, 271 ) 272 273 def __hash__(self): 274 points = self.point.reshape(1, -1)[0] 275 vectors = self.vector.reshape(1, -1)[0] 276 paths = self.path.reshape(1, -1)[0] 277 result = np.concatenate((points, vectors, paths, self.number, self.wavelength, self.incidence, self.intensity)) 278 result = result[~np.isnan(result)] 279 result.flags.writeable = False 280 result_hash = hash(result.data.tobytes()) 281 return result_hash 282 283 def __repr__(self) -> str: 284 intro = f"RayList with {self.N} rays\n" 285 average = mgeo.Vector(np.mean(self.vector)).normalized() 286 vectors = self.vector.normalized() 287 angles = np.arccos(np.clip(np.dot(vectors, average), -1.0, 1.0)) 288 avg_string = "On average, the rays are oriented along the vector:\n" + str(average) + "\n" 289 NA = f"Numerical aperture: {np.max(np.sin(angles)):.3f}\n" 290 return intro +avg_string+ NA
Class representing a list of rays in a form that is well suited to calculations. Specifically, the points, vectors etc are all numpy arrays It can then be converted to a list of Ray objects with the method
121 @classmethod 122 def from_list(cls, rays): 123 N = len(rays) 124 if N == 0: 125 return cls( 126 mgeo.PointArray([[np.nan, np.nan, np.nan]]), 127 mgeo.VectorArray([[np.nan,np.nan,np.nan]]), 128 np.zeros((1,1)), 129 np.zeros(1), 130 np.zeros(1), 131 np.zeros(1), 132 np.zeros(1), 133 0, 134 ) 135 N_r = len(rays[0].path) 136 points = np.zeros((N, 3)) 137 vectors = np.zeros((N, 3)) 138 paths = np.zeros((N, N_r)) 139 numbers = np.zeros(N, dtype=int) 140 wavelengths = np.zeros(N) 141 incidences = np.zeros(N) 142 intensities = np.zeros(N) 143 for i, ray in enumerate(rays): 144 points[i:,] = ray.point 145 vectors[i:,] = ray.vector 146 paths[i:,] = ray.path 147 numbers[i] = ray.number 148 wavelengths[i] = ray.wavelength 149 incidences[i] = ray.incidence 150 intensities[i] = ray.intensity 151 return cls( 152 mgeo.PointArray(points), 153 mgeo.VectorArray(vectors), 154 paths, 155 numbers, 156 wavelengths, 157 incidences, 158 intensities, 159 len(rays), 160 )
198 def to_basis(self, r0, r, q): 199 """ 200 Transforms the ray to the basis defined by the origin r0, the x-axis r and the y-axis q. 201 """ 202 return RayList( 203 self.point.to_basis(r0, r, q), 204 self.vector.to_basis(r0, r, q), 205 self.path, 206 self.number, 207 self.wavelength, 208 self.incidence, 209 self.intensity, 210 self.N 211 )
Transforms the ray to the basis defined by the origin r0, the x-axis r and the y-axis q.
213 def from_basis(self, r0, r, q): 214 """ 215 Transforms the ray from the basis defined by the origin r0, the x-axis r and the y-axis q. 216 """ 217 return RayList( 218 self.point.from_basis(r0, r, q), 219 self.vector.from_basis(r0, r, q), 220 self.path, 221 self.number, 222 self.wavelength, 223 self.incidence, 224 self.intensity, 225 self.N 226 )
Transforms the ray from the basis defined by the origin r0, the x-axis r and the y-axis q.
228 def rotate(self, q): 229 """ 230 Rotates the raylist by the quaternion q. 231 """ 232 return RayList( 233 self.point.rotate(q), 234 self.vector.rotate(q), 235 self.path, 236 self.number, 237 self.wavelength, 238 self.incidence, 239 self.intensity, 240 self.N 241 )
Rotates the raylist by the quaternion q.
243 def translate(self, t): 244 """ 245 Rotates the ray by the vector t 246 """ 247 return RayList( 248 self.point.translate(t), 249 self.vector, 250 self.path, 251 self.number, 252 self.wavelength, 253 self.incidence, 254 self.intensity, 255 self.N 256 )
Rotates the ray by the vector t
Contains processing functions used for the ray-tracing and automated (pre-)alignment of the optical chains. Usually these don't need to be called by users of ART, but they may be useful.
Created in Apr 2020
@author: Anthony Guillaume + Stefan Haessler
1""" 2Contains processing functions used for the ray-tracing and automated (pre-)alignment of the optical chains. 3Usually these don't need to be called by users of ART, but they may be useful. 4 5 6 7Created in Apr 2020 8 9@author: Anthony Guillaume + Stefan Haessler 10""" 11# %% Modules 12import ARTcore.ModuleGeometry as mgeo 13from ARTcore.ModuleGeometry import Point, Vector, Origin 14import ARTcore.ModuleMirror as mmirror 15import ARTcore.ModuleMask as mmask 16import ARTcore.ModuleSource as msource 17import ARTcore.ModuleOpticalElement as moe 18import ARTcore.ModuleOpticalRay as mray 19import ARTcore.ModuleSupport as msupp 20import ARTcore.ModuleOpticalChain as moc 21 22import os 23import pickle 24 25# import gzip 26import lzma 27from time import perf_counter 28from datetime import datetime 29from copy import copy 30import numpy as np 31import quaternion 32import logging 33 34logger = logging.getLogger(__name__) 35 36 37# %% 38def singleOEPlacement( 39 Optic: moe.OpticalElement, 40 Distance: float, 41 IncidenceAngle: float = 0, 42 IncidencePlaneAngle: float = 0, 43 InputRay: mray.Ray = None, 44 AlignmentVector: str = "", 45 PreviousIncidencePlane: mgeo.Vector = mgeo.Vector([0, 1, 0]) 46): 47 """ 48 Automatic placement and alignment of a single optical element. 49 The arguments are: 50 - The optic to be placed. 51 - The distance from the previous optic. 52 - An incidence angle. 53 - An incidence plane angle. 54 - An input ray. 55 - An alignment vector. 56 57 The alignment procedure is as follows: 58 - The optic is initially placed with its center at the source position (origin point of previous master ray). 59 - It's oriented such that the alignment vector is antiparallel to the master ray. By default, the alignment vector is the support_normal. 60 - The majoraxis of the optic is aligned with the incidence plane. 61 - The optic is rotated around the incidence plane by the incidence angle. 62 - The optic is rotated around the master ray by the incidence plane angle. 63 - The optic is translated along the master ray by the distance from the previous optic. 64 - The master ray is propagated through the optic without any blocking or diffraction effects and the output ray is used as the master ray for the next optic. 65 """ 66 # Convert angles to radian and wrap to 2pi 67 IncidencePlaneAngle = np.deg2rad(IncidencePlaneAngle) % (2 * np.pi) 68 IncidenceAngle = np.deg2rad(IncidenceAngle) % (2 * np.pi) 69 assert InputRay is not None, "InputRay must be provided" 70 OldOpticalElementCentre = InputRay.point 71 MasterRayDirection = InputRay.vector.normalized() 72 OpticalElementCentre = OldOpticalElementCentre + MasterRayDirection * Distance 73 74 logger.debug(f"Old Optical Element Centre: {OldOpticalElementCentre}") 75 logger.debug(f"Master Ray: {InputRay}") 76 logger.debug(f"Optical Element Centre: {OpticalElementCentre}") 77 # for convex mirrors, rotated them by 180° while keeping same incidence plane so we reflect from the "back side" 78 if Optic.curvature == mmirror.Curvature.CONVEX: 79 IncidenceAngle = np.pi - IncidenceAngle 80 81 if hasattr(Optic, AlignmentVector): 82 OpticVector = getattr(Optic, AlignmentVector) 83 else: 84 logger.warning(f"Optical Element {Optic} does not have an attribute {AlignmentVector}. Using support_normal instead.") 85 OpticVector = Optic.support_normal_ref 86 87 MajorAxis = Optic.majoraxis_ref 88 89 IncidencePlane = PreviousIncidencePlane.rotate(mgeo.QRotationAroundAxis(InputRay.vector, -IncidencePlaneAngle)) 90 # We calculate a quaternion that will rotate OpticVector against the master ray and MajorAxis into the incidence plane 91 # The convention is that the MajorAxis vector points right if seen from the source with IncidencePlane pointing up 92 # To do that, we first calculate a vector MinorAxis in the reference frame of the optic that is orthogonal to both OpticVector and MajorAxis 93 # Then we calculate a rotation matrix that will: 94 # - rotate MinorAxis into the IncidencePlane direction 95 # - rotate OpticVector against the master ray direction 96 # - rotate MajorAxis into the direction of the cross product of the two previous vectors 97 MinorAxis = -np.cross(OpticVector, MajorAxis) 98 qIncidenceAngle = mgeo.QRotationAroundAxis(IncidencePlane, -IncidenceAngle) 99 q = mgeo.QRotationVectorPair2VectorPair(MinorAxis, IncidencePlane, OpticVector, -MasterRayDirection) 100 q = qIncidenceAngle*q 101 Optic.q = q 102 logger.debug(f"Optic: {Optic}") 103 logger.debug(f"OpticVector: {OpticVector}") 104 logger.debug(f"Rotated OpticVector: {OpticVector.rotate(q)}") 105 logger.debug(f"MasterRayDirection: {MasterRayDirection}") 106 107 Optic.r = Origin + (OpticalElementCentre - Optic.centre - Optic.r) 108 109 NextRay = Optic.propagate_raylist(mray.RayList.from_list([InputRay]), alignment=True)[0] 110 return NextRay, IncidencePlane 111 112 113def OEPlacement( 114 OpticsList: list, 115 InitialRay:mray.Ray = None, 116 Source: msource.Source = None, 117 InputIncidencePlane: mgeo.Vector = None 118): 119 """ 120 Automatic placement and alignment of the optical elements for one optical chain. 121 Returns a list of optical elements (copied) 122 """ 123 if InitialRay is None: 124 if Source is None: 125 InputRay = InputRay = mray.Ray( 126 point=mgeo.Point([0, 0, 0]), 127 vector=mgeo.Vector([1, 0, 0]), 128 path=(0.0,), 129 number=0, 130 wavelength=800e-9, 131 incidence=0.0, 132 intensity=1.0 133 ) 134 else: 135 InputRay = Source.get_master_ray() 136 else: 137 InputRay = InitialRay.copy_ray() 138 if InputIncidencePlane is None: 139 InputIncidencePlane = mgeo.Vector([0, 1, 0]) 140 logger.debug(f"Initial Ray: {InputRay}") 141 logger.debug(f"Initial Incidence Plane: {InputIncidencePlane}") 142 assert np.linalg.norm(np.dot(InputRay.vector, InputIncidencePlane))<1e-6, "InputIncidencePlane is not orthogonal to InputRay.vector" 143 PreviousIncidencePlane = InputIncidencePlane.copy() 144 OEList = [] 145 for i in range(len(OpticsList)): 146 OEList.append(copy(OpticsList[i]["OpticalElement"])) 147 InputRay, PreviousIncidencePlane = singleOEPlacement( 148 OEList[-1], 149 OpticsList[i]["Distance"], 150 OpticsList[i]["IncidenceAngle"], 151 OpticsList[i]["IncidencePlaneAngle"], 152 InputRay, 153 OpticsList[i]["Alignment"] if "Alignment" in OpticsList[i] else "support_normal", 154 PreviousIncidencePlane 155 ) 156 return OEList 157 158 159 160# %% 161def RayTracingCalculation( 162 source_rays: mray.RayList, optical_elements: list[moe.OpticalElement], **kwargs 163) -> list[list[mray.Ray]]: 164 """ 165 The actual ray-tracing calculation, starting from the list of 'source_rays', 166 and propagating them from one optical element to the next in the order of the 167 items of the list 'optical_elements'. 168 169 170 Parameters 171 ---------- 172 source_rays : list[Ray-objects] 173 List of input rays. 174 175 optical_elements : list[OpticalElement-objects] 176 177 178 Returns 179 ------- 180 output_rays : list[list[Ray-objects]] 181 List of lists of rays, each item corresponding to the ray-bundle *after* 182 the item with the same index in the 'optical_elements'-list. 183 """ 184 output_rays = [] 185 186 for k in range(0, len(optical_elements)): 187 if k == 0: 188 RayList = source_rays 189 else: 190 RayList = output_rays[k - 1] 191 RayList = optical_elements[k].propagate_raylist(RayList, **kwargs) 192 output_rays.append(RayList) 193 194 return output_rays 195 196 197# %% 198def _FindOptimalDistanceBIS(movingDetector, Amplitude, Step, RayList, OptFor, IntensityWeighted): 199 ListSizeSpot = [] 200 ListDuration = [] 201 ListFitness = [] 202 if IntensityWeighted: 203 Weights = [k.intensity for k in RayList] 204 205 movingDetector.shiftByDistance(-Amplitude) 206 n = int(2 * Amplitude / Step) 207 for i in range(n): 208 ListPointDetector2DCentre = movingDetector.get_PointList2DCentre(RayList) 209 if OptFor in ["intensity", "spotsize"]: 210 if IntensityWeighted: 211 SpotSize = WeightedStandardDeviation(ListPointDetector2DCentre, Weights) 212 else: 213 SpotSize = StandardDeviation(ListPointDetector2DCentre) 214 ListSizeSpot.append(SpotSize) 215 216 if OptFor in ["intensity", "duration"]: 217 DelayList = movingDetector.get_Delays(RayList) 218 if IntensityWeighted: 219 Duration = WeightedStandardDeviation(DelayList, Weights) 220 else: 221 Duration = StandardDeviation(DelayList) 222 ListDuration.append(Duration) 223 224 if OptFor == "intensity": 225 Fitness = SpotSize**2 * Duration 226 elif OptFor == "duration": 227 Fitness = Duration 228 elif OptFor == "spotsize": 229 Fitness = SpotSize 230 ListFitness.append(Fitness) 231 232 movingDetector.shiftByDistance(Step) 233 234 FitnessMin = min(ListFitness) 235 ind = ListFitness.index(FitnessMin) 236 if OptFor in ["intensity", "spotsize"]: 237 OptSizeSpot = ListSizeSpot[ind] 238 else: 239 OptSizeSpot = np.nan 240 if OptFor in ["intensity", "duration"]: 241 OptDuration = ListDuration[ind] 242 else: 243 OptDuration = np.nan 244 245 movingDetector.shiftByDistance(-(n - ind) * Step) 246 247 return movingDetector, OptSizeSpot, OptDuration 248 249 250def FindOptimalDistance( 251 Detector, 252 RayList: list[mray.Ray], 253 OptFor="intensity", 254 Amplitude: float = None, 255 Precision: int = 3, 256 IntensityWeighted=False, 257 verbose=False, 258): 259 """ 260 Automatically finds the optimal the 'Detector'-distance for either 261 maximum intensity, or minimal spotsize or duration. 262 263 Parameters 264 ---------- 265 Detector : Detector-object 266 267 RayList : list[Ray-objects] 268 269 OptFor : str, optional 270 "spotsize": minimizes the standard-deviation spot size *d* on the detector. 271 "duration": minimizes the standard-deviation *tau* of the ray-delays. 272 "intensity": Maximizes 1/tau/d^2. 273 Defaults to "intensity". 274 275 Amplitude : float, optional 276 The detector-distances within which the optimization is done will be 277 the distance of 'Detector' +/- Amplitude in mm. 278 279 Precision : int, optional 280 Precision-parameter for the search algorithm. For Precision = n, 281 it will iterate with search amplitudes decreasing until 10^{-n}. 282 Defaults to 3. 283 284 IntensityWeighted : bool, optional 285 Whether to weigh the calculation of spotsize and/or duration by the ray-intensities. 286 Defaults to False. 287 288 verbose : bool, optional 289 Whether to print results to the console. 290 291 Returns 292 ------- 293 OptDetector : Detector-object 294 A new detector-instance with optimized distance. 295 296 OptSpotSize : float 297 Spotsize, calculated as standard deviation in mm of the spot-diagram 298 on the optimized detector. 299 300 OptDuration : float 301 Duration, calculated as standard deviation in fs of the ray-delays 302 on the optimized detector. 303 """ 304 305 if OptFor not in ["intensity", "size", "duration"]: 306 raise NameError( 307 "I don`t recognize what you want to optimize the detector distance for. OptFor must be either 'intensity', 'size' or 'duration'." 308 ) 309 310 FirstDistance = Detector.get_distance() 311 ListPointDetector2DCentre = Detector.get_PointList2DCentre(RayList) 312 SizeSpot = 2 * StandardDeviation(ListPointDetector2DCentre) 313 NumericalAperture = ReturnNumericalAperture(RayList, 1) 314 if Amplitude is None: 315 Amplitude = min(4 * np.ceil(SizeSpot / np.tan(np.arcsin(NumericalAperture))), FirstDistance) 316 Step = Amplitude/10 317 #Step = Amplitude / 5 # pretty good in half the time ;-) 318 319 if verbose: 320 print( 321 f"Searching optimal detector position for *{OptFor}* within [{FirstDistance-Amplitude:.3f}, {FirstDistance+Amplitude:.3f}] mm...", 322 end="", 323 flush=True, 324 ) 325 movingDetector = Detector.copy_detector() 326 for k in range(Precision + 1): 327 movingDetector, OptSpotSize, OptDuration = _FindOptimalDistanceBIS( 328 movingDetector, Amplitude * 0.1**k, Step * 0.1**k, RayList, OptFor, IntensityWeighted 329 ) 330 331 if ( 332 not FirstDistance - Amplitude + 10**-Precision 333 < movingDetector.get_distance() 334 < FirstDistance + Amplitude - 10**-Precision 335 ): 336 print("There`s no minimum-size/duration focus in the searched range.") 337 338 print( 339 "\r\033[K", end="", flush=True 340 ) # move to beginning of the line with \r and then delete the whole line with \033[K 341 return movingDetector, OptSpotSize, OptDuration 342 343 344# %% 345def FindCentralRay(RayList: list[mray.Ray]): 346 """ 347 Out of a the list of Ray-objects, RayList, determine the average direction 348 vector and source point, and the return a Ray-object representing this 349 central ray of the RayList. 350 351 Parameters 352 ---------- 353 RayList : list of Ray-objects 354 355 Returns 356 ------- 357 CentralRay : Ray-object 358 """ 359 360 CentralVector = np.mean( [x.vector for x in RayList], axis=0) 361 CentralPoint = np.mean( [x.point for x in RayList], axis=0) 362 363 return mray.Ray(mgeo.Point(CentralPoint), mgeo.Vector(CentralVector)) 364 365 366def StandardDeviation(List: list[float, np.ndarray]) -> float: 367 """ 368 Return the standard deviation of elements in the supplied list. 369 If the elements are floats, it's simply the root-mean-square over the numbers. 370 If the elements are vectors (represented as numpy-arrays), the root-mean-square 371 of the distance of the points from their mean is calculated. 372 373 Parameters 374 ---------- 375 List : list of floats or of np.ndarray 376 Numbers (in ART, typically delays) 377 or 2D or 3D vectors (in ART typically space coordinates) 378 379 Returns 380 ------- 381 Std : float 382 """ 383 if type(List[0]) in [int, float, np.float64]: 384 return np.std(List) 385 elif len(List[0]) > 1: 386 return np.sqrt(np.var(List, axis=0).sum()) 387 else: 388 raise ValueError("StandardDeviation expects a list of floats or numpy-arrays as input, but got something else.") 389 390 391def WeightedStandardDeviation(List: list[float, np.ndarray], Weights: list[float]) -> float: 392 """ 393 Return the weighted standard deviation of elements in the supplied list. 394 If the elements are floats, it's simply the root-mean-square over the weighted numbers. 395 If the elements are vectors (represented as numpy-arrays), the root-mean-square 396 of the weighted distances of the points from their mean is calculated. 397 398 Parameters 399 ---------- 400 List : list of floats or of np.ndarray 401 Numbers (in ART, typically delays) 402 or 2D or 3D vectors (in ART typically space coordinates) 403 404 Weights : list of floats, same length as List 405 Parameters such as Ray.intensity to be used as weights 406 407 Returns 408 ------- 409 Std : float 410 """ 411 average = np.average(List, axis=0, weights=Weights, returned=False) 412 variance = np.average((List - average) ** 2, axis=0, weights=Weights, returned=False) 413 return np.sqrt(variance.sum()) 414 415 416# %% 417def _hash_list_of_objects(list): 418 """returns a hash value for a list of objects, by just summing up all the individual hashes.""" 419 total_hash = 0 420 for x in list: 421 total_hash += hash(x) 422 return total_hash 423 424 425def _which_indeces(lst): 426 """takes an input-list, and gives you a list of the indeces where the input-list contains another list or a numpy-array""" 427 indexes = [i for i, x in enumerate(lst) if isinstance(x, (list, np.ndarray))] 428 return indexes 429 430 431# %% 432def save_compressed(obj, filename: str = None): 433 """Save (=pickle) an object 'obj' to a compressed file with name 'filename'.""" 434 if not type(filename) == str: 435 filename = "kept_data_" + datetime.now().strftime("%Y-%m-%d-%Hh%M") 436 437 i = 0 438 while os.path.exists(filename + f"_{i}.xz"): 439 i += 1 440 filename = filename + f"_{i}" 441 # with gzip.open(filename + '.gz', 'wb') as f: 442 with lzma.open(filename + ".xz", "wb") as f: 443 pickle.dump(obj, f) 444 print("Saved results to " + filename + ".xz.") 445 print("->To reload from disk do: kept_data = mp.load_compressed('" + filename + "')") 446 447 448def load_compressed(filename: str): 449 """Load (=unpickle) an object 'obj' from a compressed file with name 'filename'.""" 450 # with gzip.open(filename + '.gz', 'rb') as f: 451 with lzma.open(filename + ".xz", "rb") as f: 452 obj = pickle.load(f) 453 return obj 454 455 456# %% tic-toc timer functions 457_tstart_stack = [] 458 459 460def _tic(): 461 _tstart_stack.append(perf_counter()) 462 463 464def _toc(fmt="Elapsed: %s s"): 465 print(fmt % (perf_counter() - _tstart_stack.pop()))
39def singleOEPlacement( 40 Optic: moe.OpticalElement, 41 Distance: float, 42 IncidenceAngle: float = 0, 43 IncidencePlaneAngle: float = 0, 44 InputRay: mray.Ray = None, 45 AlignmentVector: str = "", 46 PreviousIncidencePlane: mgeo.Vector = mgeo.Vector([0, 1, 0]) 47): 48 """ 49 Automatic placement and alignment of a single optical element. 50 The arguments are: 51 - The optic to be placed. 52 - The distance from the previous optic. 53 - An incidence angle. 54 - An incidence plane angle. 55 - An input ray. 56 - An alignment vector. 57 58 The alignment procedure is as follows: 59 - The optic is initially placed with its center at the source position (origin point of previous master ray). 60 - It's oriented such that the alignment vector is antiparallel to the master ray. By default, the alignment vector is the support_normal. 61 - The majoraxis of the optic is aligned with the incidence plane. 62 - The optic is rotated around the incidence plane by the incidence angle. 63 - The optic is rotated around the master ray by the incidence plane angle. 64 - The optic is translated along the master ray by the distance from the previous optic. 65 - The master ray is propagated through the optic without any blocking or diffraction effects and the output ray is used as the master ray for the next optic. 66 """ 67 # Convert angles to radian and wrap to 2pi 68 IncidencePlaneAngle = np.deg2rad(IncidencePlaneAngle) % (2 * np.pi) 69 IncidenceAngle = np.deg2rad(IncidenceAngle) % (2 * np.pi) 70 assert InputRay is not None, "InputRay must be provided" 71 OldOpticalElementCentre = InputRay.point 72 MasterRayDirection = InputRay.vector.normalized() 73 OpticalElementCentre = OldOpticalElementCentre + MasterRayDirection * Distance 74 75 logger.debug(f"Old Optical Element Centre: {OldOpticalElementCentre}") 76 logger.debug(f"Master Ray: {InputRay}") 77 logger.debug(f"Optical Element Centre: {OpticalElementCentre}") 78 # for convex mirrors, rotated them by 180° while keeping same incidence plane so we reflect from the "back side" 79 if Optic.curvature == mmirror.Curvature.CONVEX: 80 IncidenceAngle = np.pi - IncidenceAngle 81 82 if hasattr(Optic, AlignmentVector): 83 OpticVector = getattr(Optic, AlignmentVector) 84 else: 85 logger.warning(f"Optical Element {Optic} does not have an attribute {AlignmentVector}. Using support_normal instead.") 86 OpticVector = Optic.support_normal_ref 87 88 MajorAxis = Optic.majoraxis_ref 89 90 IncidencePlane = PreviousIncidencePlane.rotate(mgeo.QRotationAroundAxis(InputRay.vector, -IncidencePlaneAngle)) 91 # We calculate a quaternion that will rotate OpticVector against the master ray and MajorAxis into the incidence plane 92 # The convention is that the MajorAxis vector points right if seen from the source with IncidencePlane pointing up 93 # To do that, we first calculate a vector MinorAxis in the reference frame of the optic that is orthogonal to both OpticVector and MajorAxis 94 # Then we calculate a rotation matrix that will: 95 # - rotate MinorAxis into the IncidencePlane direction 96 # - rotate OpticVector against the master ray direction 97 # - rotate MajorAxis into the direction of the cross product of the two previous vectors 98 MinorAxis = -np.cross(OpticVector, MajorAxis) 99 qIncidenceAngle = mgeo.QRotationAroundAxis(IncidencePlane, -IncidenceAngle) 100 q = mgeo.QRotationVectorPair2VectorPair(MinorAxis, IncidencePlane, OpticVector, -MasterRayDirection) 101 q = qIncidenceAngle*q 102 Optic.q = q 103 logger.debug(f"Optic: {Optic}") 104 logger.debug(f"OpticVector: {OpticVector}") 105 logger.debug(f"Rotated OpticVector: {OpticVector.rotate(q)}") 106 logger.debug(f"MasterRayDirection: {MasterRayDirection}") 107 108 Optic.r = Origin + (OpticalElementCentre - Optic.centre - Optic.r) 109 110 NextRay = Optic.propagate_raylist(mray.RayList.from_list([InputRay]), alignment=True)[0] 111 return NextRay, IncidencePlane
Automatic placement and alignment of a single optical element. The arguments are:
The alignment procedure is as follows:
114def OEPlacement( 115 OpticsList: list, 116 InitialRay:mray.Ray = None, 117 Source: msource.Source = None, 118 InputIncidencePlane: mgeo.Vector = None 119): 120 """ 121 Automatic placement and alignment of the optical elements for one optical chain. 122 Returns a list of optical elements (copied) 123 """ 124 if InitialRay is None: 125 if Source is None: 126 InputRay = InputRay = mray.Ray( 127 point=mgeo.Point([0, 0, 0]), 128 vector=mgeo.Vector([1, 0, 0]), 129 path=(0.0,), 130 number=0, 131 wavelength=800e-9, 132 incidence=0.0, 133 intensity=1.0 134 ) 135 else: 136 InputRay = Source.get_master_ray() 137 else: 138 InputRay = InitialRay.copy_ray() 139 if InputIncidencePlane is None: 140 InputIncidencePlane = mgeo.Vector([0, 1, 0]) 141 logger.debug(f"Initial Ray: {InputRay}") 142 logger.debug(f"Initial Incidence Plane: {InputIncidencePlane}") 143 assert np.linalg.norm(np.dot(InputRay.vector, InputIncidencePlane))<1e-6, "InputIncidencePlane is not orthogonal to InputRay.vector" 144 PreviousIncidencePlane = InputIncidencePlane.copy() 145 OEList = [] 146 for i in range(len(OpticsList)): 147 OEList.append(copy(OpticsList[i]["OpticalElement"])) 148 InputRay, PreviousIncidencePlane = singleOEPlacement( 149 OEList[-1], 150 OpticsList[i]["Distance"], 151 OpticsList[i]["IncidenceAngle"], 152 OpticsList[i]["IncidencePlaneAngle"], 153 InputRay, 154 OpticsList[i]["Alignment"] if "Alignment" in OpticsList[i] else "support_normal", 155 PreviousIncidencePlane 156 ) 157 return OEList
Automatic placement and alignment of the optical elements for one optical chain. Returns a list of optical elements (copied)
162def RayTracingCalculation( 163 source_rays: mray.RayList, optical_elements: list[moe.OpticalElement], **kwargs 164) -> list[list[mray.Ray]]: 165 """ 166 The actual ray-tracing calculation, starting from the list of 'source_rays', 167 and propagating them from one optical element to the next in the order of the 168 items of the list 'optical_elements'. 169 170 171 Parameters 172 ---------- 173 source_rays : list[Ray-objects] 174 List of input rays. 175 176 optical_elements : list[OpticalElement-objects] 177 178 179 Returns 180 ------- 181 output_rays : list[list[Ray-objects]] 182 List of lists of rays, each item corresponding to the ray-bundle *after* 183 the item with the same index in the 'optical_elements'-list. 184 """ 185 output_rays = [] 186 187 for k in range(0, len(optical_elements)): 188 if k == 0: 189 RayList = source_rays 190 else: 191 RayList = output_rays[k - 1] 192 RayList = optical_elements[k].propagate_raylist(RayList, **kwargs) 193 output_rays.append(RayList) 194 195 return output_rays
The actual ray-tracing calculation, starting from the list of 'source_rays', and propagating them from one optical element to the next in the order of the items of the list 'optical_elements'.
source_rays : list[Ray-objects]
List of input rays.
optical_elements : list[OpticalElement-objects]
output_rays : list[list[Ray-objects]]
List of lists of rays, each item corresponding to the ray-bundle *after*
the item with the same index in the 'optical_elements'-list.
251def FindOptimalDistance( 252 Detector, 253 RayList: list[mray.Ray], 254 OptFor="intensity", 255 Amplitude: float = None, 256 Precision: int = 3, 257 IntensityWeighted=False, 258 verbose=False, 259): 260 """ 261 Automatically finds the optimal the 'Detector'-distance for either 262 maximum intensity, or minimal spotsize or duration. 263 264 Parameters 265 ---------- 266 Detector : Detector-object 267 268 RayList : list[Ray-objects] 269 270 OptFor : str, optional 271 "spotsize": minimizes the standard-deviation spot size *d* on the detector. 272 "duration": minimizes the standard-deviation *tau* of the ray-delays. 273 "intensity": Maximizes 1/tau/d^2. 274 Defaults to "intensity". 275 276 Amplitude : float, optional 277 The detector-distances within which the optimization is done will be 278 the distance of 'Detector' +/- Amplitude in mm. 279 280 Precision : int, optional 281 Precision-parameter for the search algorithm. For Precision = n, 282 it will iterate with search amplitudes decreasing until 10^{-n}. 283 Defaults to 3. 284 285 IntensityWeighted : bool, optional 286 Whether to weigh the calculation of spotsize and/or duration by the ray-intensities. 287 Defaults to False. 288 289 verbose : bool, optional 290 Whether to print results to the console. 291 292 Returns 293 ------- 294 OptDetector : Detector-object 295 A new detector-instance with optimized distance. 296 297 OptSpotSize : float 298 Spotsize, calculated as standard deviation in mm of the spot-diagram 299 on the optimized detector. 300 301 OptDuration : float 302 Duration, calculated as standard deviation in fs of the ray-delays 303 on the optimized detector. 304 """ 305 306 if OptFor not in ["intensity", "size", "duration"]: 307 raise NameError( 308 "I don`t recognize what you want to optimize the detector distance for. OptFor must be either 'intensity', 'size' or 'duration'." 309 ) 310 311 FirstDistance = Detector.get_distance() 312 ListPointDetector2DCentre = Detector.get_PointList2DCentre(RayList) 313 SizeSpot = 2 * StandardDeviation(ListPointDetector2DCentre) 314 NumericalAperture = ReturnNumericalAperture(RayList, 1) 315 if Amplitude is None: 316 Amplitude = min(4 * np.ceil(SizeSpot / np.tan(np.arcsin(NumericalAperture))), FirstDistance) 317 Step = Amplitude/10 318 #Step = Amplitude / 5 # pretty good in half the time ;-) 319 320 if verbose: 321 print( 322 f"Searching optimal detector position for *{OptFor}* within [{FirstDistance-Amplitude:.3f}, {FirstDistance+Amplitude:.3f}] mm...", 323 end="", 324 flush=True, 325 ) 326 movingDetector = Detector.copy_detector() 327 for k in range(Precision + 1): 328 movingDetector, OptSpotSize, OptDuration = _FindOptimalDistanceBIS( 329 movingDetector, Amplitude * 0.1**k, Step * 0.1**k, RayList, OptFor, IntensityWeighted 330 ) 331 332 if ( 333 not FirstDistance - Amplitude + 10**-Precision 334 < movingDetector.get_distance() 335 < FirstDistance + Amplitude - 10**-Precision 336 ): 337 print("There`s no minimum-size/duration focus in the searched range.") 338 339 print( 340 "\r\033[K", end="", flush=True 341 ) # move to beginning of the line with \r and then delete the whole line with \033[K 342 return movingDetector, OptSpotSize, OptDuration
Automatically finds the optimal the 'Detector'-distance for either maximum intensity, or minimal spotsize or duration.
Detector : Detector-object
RayList : list[Ray-objects]
OptFor : str, optional
"spotsize": minimizes the standard-deviation spot size *d* on the detector.
"duration": minimizes the standard-deviation *tau* of the ray-delays.
"intensity": Maximizes 1/tau/d^2.
Defaults to "intensity".
Amplitude : float, optional
The detector-distances within which the optimization is done will be
the distance of 'Detector' +/- Amplitude in mm.
Precision : int, optional
Precision-parameter for the search algorithm. For Precision = n,
it will iterate with search amplitudes decreasing until 10^{-n}.
Defaults to 3.
IntensityWeighted : bool, optional
Whether to weigh the calculation of spotsize and/or duration by the ray-intensities.
Defaults to False.
verbose : bool, optional
Whether to print results to the console.
OptDetector : Detector-object
A new detector-instance with optimized distance.
OptSpotSize : float
Spotsize, calculated as standard deviation in mm of the spot-diagram
on the optimized detector.
OptDuration : float
Duration, calculated as standard deviation in fs of the ray-delays
on the optimized detector.
346def FindCentralRay(RayList: list[mray.Ray]): 347 """ 348 Out of a the list of Ray-objects, RayList, determine the average direction 349 vector and source point, and the return a Ray-object representing this 350 central ray of the RayList. 351 352 Parameters 353 ---------- 354 RayList : list of Ray-objects 355 356 Returns 357 ------- 358 CentralRay : Ray-object 359 """ 360 361 CentralVector = np.mean( [x.vector for x in RayList], axis=0) 362 CentralPoint = np.mean( [x.point for x in RayList], axis=0) 363 364 return mray.Ray(mgeo.Point(CentralPoint), mgeo.Vector(CentralVector))
Out of a the list of Ray-objects, RayList, determine the average direction vector and source point, and the return a Ray-object representing this central ray of the RayList.
RayList : list of Ray-objects
CentralRay : Ray-object
367def StandardDeviation(List: list[float, np.ndarray]) -> float: 368 """ 369 Return the standard deviation of elements in the supplied list. 370 If the elements are floats, it's simply the root-mean-square over the numbers. 371 If the elements are vectors (represented as numpy-arrays), the root-mean-square 372 of the distance of the points from their mean is calculated. 373 374 Parameters 375 ---------- 376 List : list of floats or of np.ndarray 377 Numbers (in ART, typically delays) 378 or 2D or 3D vectors (in ART typically space coordinates) 379 380 Returns 381 ------- 382 Std : float 383 """ 384 if type(List[0]) in [int, float, np.float64]: 385 return np.std(List) 386 elif len(List[0]) > 1: 387 return np.sqrt(np.var(List, axis=0).sum()) 388 else: 389 raise ValueError("StandardDeviation expects a list of floats or numpy-arrays as input, but got something else.")
Return the standard deviation of elements in the supplied list. If the elements are floats, it's simply the root-mean-square over the numbers. If the elements are vectors (represented as numpy-arrays), the root-mean-square of the distance of the points from their mean is calculated.
List : list of floats or of np.ndarray
Numbers (in ART, typically delays)
or 2D or 3D vectors (in ART typically space coordinates)
Std : float
392def WeightedStandardDeviation(List: list[float, np.ndarray], Weights: list[float]) -> float: 393 """ 394 Return the weighted standard deviation of elements in the supplied list. 395 If the elements are floats, it's simply the root-mean-square over the weighted numbers. 396 If the elements are vectors (represented as numpy-arrays), the root-mean-square 397 of the weighted distances of the points from their mean is calculated. 398 399 Parameters 400 ---------- 401 List : list of floats or of np.ndarray 402 Numbers (in ART, typically delays) 403 or 2D or 3D vectors (in ART typically space coordinates) 404 405 Weights : list of floats, same length as List 406 Parameters such as Ray.intensity to be used as weights 407 408 Returns 409 ------- 410 Std : float 411 """ 412 average = np.average(List, axis=0, weights=Weights, returned=False) 413 variance = np.average((List - average) ** 2, axis=0, weights=Weights, returned=False) 414 return np.sqrt(variance.sum())
Return the weighted standard deviation of elements in the supplied list. If the elements are floats, it's simply the root-mean-square over the weighted numbers. If the elements are vectors (represented as numpy-arrays), the root-mean-square of the weighted distances of the points from their mean is calculated.
List : list of floats or of np.ndarray
Numbers (in ART, typically delays)
or 2D or 3D vectors (in ART typically space coordinates)
Weights : list of floats, same length as List
Parameters such as Ray.intensity to be used as weights
Std : float
433def save_compressed(obj, filename: str = None): 434 """Save (=pickle) an object 'obj' to a compressed file with name 'filename'.""" 435 if not type(filename) == str: 436 filename = "kept_data_" + datetime.now().strftime("%Y-%m-%d-%Hh%M") 437 438 i = 0 439 while os.path.exists(filename + f"_{i}.xz"): 440 i += 1 441 filename = filename + f"_{i}" 442 # with gzip.open(filename + '.gz', 'wb') as f: 443 with lzma.open(filename + ".xz", "wb") as f: 444 pickle.dump(obj, f) 445 print("Saved results to " + filename + ".xz.") 446 print("->To reload from disk do: kept_data = mp.load_compressed('" + filename + "')")
Save (=pickle) an object 'obj' to a compressed file with name 'filename'.
449def load_compressed(filename: str): 450 """Load (=unpickle) an object 'obj' from a compressed file with name 'filename'.""" 451 # with gzip.open(filename + '.gz', 'rb') as f: 452 with lzma.open(filename + ".xz", "rb") as f: 453 obj = pickle.load(f) 454 return obj
Load (=unpickle) an object 'obj' from a compressed file with name 'filename'.
Provides a definition of sources of light. A source can be either a composite source (a sum of sources) or a simple source. A simple source is defined by 4 parameters:
Created in 2019
@author: Anthony Guillaume and Stefan Haessler
1""" 2Provides a definition of sources of light. 3A source can be either a composite source (a sum of sources) or a simple source. 4A simple source is defined by 4 parameters: 5- Wavelength (monochromatic) 6- Angular power distribution 7- Ray origins distribution 8- Ray directions distribution 9A reason to use composite sources is for when the power and ray distributions depend on the wavelength. 10Another source option is the source containing just a list of rays that can be prepared manually. 11Finally another source option is one used for alignment, containing a single ray. 12 13 14Created in 2019 15 16@author: Anthony Guillaume and Stefan Haessler 17""" 18 19# %% Modules 20import ARTcore.ModuleOpticalRay as mray 21import ARTcore.ModuleGeometry as mgeo 22import ARTcore.ModuleProcessing as mp 23 24from ARTcore.DepGraphDefinitions import UniformSpectraCalculator 25 26import numpy as np 27from abc import ABC, abstractmethod 28import logging 29 30logger = logging.getLogger(__name__) 31 32# %% Abstract base classes for sources 33class Source(ABC): 34 """ 35 Abstract base class for sources. 36 Fundamentally a source just has to be able to return a list of rays. 37 To do so, we make it callable. 38 """ 39 @abstractmethod 40 def __call__(self,N): 41 pass 42 43class PowerDistribution(ABC): 44 """ 45 Abstract base class for angular power distributions. 46 """ 47 @abstractmethod 48 def __call__(self, Origins, Directions): 49 """ 50 Return the power of the rays coming 51 from the origin points Origins to the directions Directions. 52 """ 53 pass 54class RayOriginsDistribution(ABC): 55 """ 56 Abstract base class for ray origins distributions. 57 """ 58 @abstractmethod 59 def __call__(self, N): 60 """ 61 Return the origins of N rays. 62 """ 63 pass 64 65class RayDirectionsDistribution(ABC): 66 """ 67 Abstract base class for ray directions distributions. 68 """ 69 @abstractmethod 70 def __call__(self, N): 71 """ 72 Return the directions of N rays. 73 """ 74 pass 75 76class Spectrum(ABC): 77 """ 78 Abstract base class for spectra. 79 """ 80 @abstractmethod 81 def __call__(self, N): 82 """ 83 Return the wavelengths of N rays. 84 """ 85 pass 86 87# %% Specific power distributions 88class SpatialGaussianPowerDistribution(PowerDistribution): 89 """ 90 Spatial Gaussian power distribution, depending only on ray origin points, and not on directions. 91 92 Attributes 93 ---------- 94 Power : float 95 Peak power of the distribution in arbitrary units (user can keep track of their own units.) 96 97 W0 : float 98 1/e^2 beam waist in mm 99 """ 100 def __init__(self, Power, W0): 101 self.Power = Power 102 self.W0 = W0 103 104 def __call__(self, Origins, Directions): 105 """ 106 Return the power of the rays coming 107 from the origin points Origins to the directions Directions. 108 109 Parameters 110 ---------- 111 Origins : mgeo.PointArray 112 PointArray as defined in ModuleGeometry, with points of origin of the rays. 113 114 Directions : mgeo.VectorArray 115 VectorArray as defined in ModuleGeometry, with ray vectors. 116 117 Returns 118 ------- 119 Powers: numpy.array 120 Array of powers for each ray. 121 122 """ 123 return self.Power * np.exp(-2 * np.linalg.norm(Origins, axis=1) ** 2 / self.W0 ** 2) 124 125class AngularGaussianPowerDistribution(PowerDistribution): 126 """ 127 Angular Gaussian power distribution, depending only on ray directions, but not origin points. 128 129 Attributes 130 ---------- 131 Power : float 132 Peak power of the distribution in arbitrary units (user can keep track of their own units.) 133 134 Divergence : float 135 1/e^2 divergence half-angle in radians. 136 """ 137 def __init__(self, Power, Divergence): 138 self.Power = Power 139 self.Divergence = Divergence 140 141 def __call__(self, Origins, Directions): 142 """ 143 Return the power of the rays coming 144 from the origin points Origins to the directions Directions. 145 146 Parameters 147 ---------- 148 Origins : mgeo.PointArray 149 PointArray as defined in ModuleGeometry, with points of origin of the rays. 150 151 Directions : mgeo.VectorArray 152 VectorArray as defined in ModuleGeometry, with ray vectors. 153 154 Returns 155 ------- 156 Powers: numpy.array 157 Array of powers for each ray. 158 159 """ 160 return self.Power * np.exp(-2 * np.arccos(np.dot(Directions, [0, 0, 1])) ** 2 / self.Divergence ** 2) 161 162class GaussianPowerDistribution(PowerDistribution): 163 """ 164 Gaussian power distribution, depending on ray origin points and directions. 165 166 Attributes 167 ---------- 168 Power : float 169 Peak power of the distribution in arbitrary units (user can keep track of their own units.) 170 171 W0 : float 172 1/e^2 beam waist in mm 173 174 Divergence : float 175 1/e^2 divergence half-angle in radians. 176 """ 177 def __init__(self, Power, W0, Divergence): 178 self.Power = Power 179 self.W0 = W0 180 self.Divergence = Divergence 181 182 def __call__(self, Origins, Directions): 183 """ 184 Return the power of the rays coming 185 from the origin points Origins to the directions Directions. 186 187 Parameters 188 ---------- 189 Origins : mgeo.PointArray 190 PointArray as defined in ModuleGeometry, with points of origin of the rays. 191 192 Directions : mgeo.VectorArray 193 VectorArray as defined in ModuleGeometry, with ray vectors. 194 195 Returns 196 ------- 197 Powers: numpy.array 198 Array of powers for each ray. 199 200 """ 201 return self.Power * np.exp(-2 * (Origins-mgeo.Origin).norm ** 2 / self.W0 ** 2) * np.exp(-2 * np.array(np.arccos(np.dot(Directions, mgeo.Vector([1, 0, 0])))) ** 2 / self.Divergence ** 2) 202 203class UniformPowerDistribution(PowerDistribution): 204 """ 205 Uniform power distribution. 206 """ 207 def __init__(self, Power): 208 self.Power = Power 209 210 def __call__(self, Origins, Directions): 211 """ 212 Return the power of the rays coming 213 from the origin points Origins to the directions Directions. 214 """ 215 return self.Power * np.ones(len(Origins)) 216 217# %% Specific ray origins distributions 218class PointRayOriginsDistribution(RayOriginsDistribution): 219 """ 220 Point ray origins distribution. 221 """ 222 def __init__(self, Origin): 223 self.Origin = Origin 224 225 def __call__(self, N): 226 """ 227 Return the origins of N rays. 228 """ 229 return mgeo.PointArray([self.Origin for i in range(N)]) 230 231class DiskRayOriginsDistribution(RayOriginsDistribution): 232 """ 233 Disk ray origins distribution. Uses the Vogel spiral to initialize the rays. 234 """ 235 def __init__(self, Origin, Radius, Normal = mgeo.Vector([0, 0, 1])): 236 self.Origin = Origin 237 self.Radius = Radius 238 self.Normal = Normal 239 240 def __call__(self, N): 241 """ 242 Return the origins of N rays. 243 """ 244 MatrixXY = mgeo.SpiralVogel(N, self.Radius) 245 q = mgeo.QRotationVector2Vector(mgeo.Vector([0, 0, 1]), self.Normal) 246 return mgeo.PointArray([self.Origin + mgeo.Vector([MatrixXY[i, 0], MatrixXY[i, 1], 0]) for i in range(N)]).rotate(q) 247 248# %% Specific ray directions distributions 249class UniformRayDirectionsDistribution(RayDirectionsDistribution): 250 """ 251 Uniform ray directions distribution. 252 """ 253 def __init__(self, Direction): 254 self.Direction = Direction 255 256 def __call__(self, N): 257 """ 258 Return the directions of N rays. 259 """ 260 return mgeo.VectorArray([self.Direction for i in range(N)]) 261 262class ConeRayDirectionsDistribution(RayDirectionsDistribution): 263 """ 264 Cone ray directions distribution. Uses the Vogel spiral to initialize the rays. 265 """ 266 def __init__(self, Direction, Angle): 267 self.Direction = Direction 268 self.Angle = Angle 269 270 def __call__(self, N): 271 """ 272 Return the directions of N rays. 273 """ 274 Height = 1 275 Radius = Height * np.tan(self.Angle) 276 MatrixXY = mgeo.SpiralVogel(N, Radius) 277 q = mgeo.QRotationVector2Vector(mgeo.Vector([0, 0, 1]), self.Direction) 278 return mgeo.VectorArray([[MatrixXY[i, 0], MatrixXY[i, 1], Height] for i in range(N)]).rotate(q).normalized() 279 280 281# %% Specific spectra 282class SingleWavelengthSpectrum(Spectrum): 283 """ 284 Single wavelength spectrum. 285 """ 286 def __init__(self, Wavelength): 287 self.Wavelength = Wavelength 288 289 def __call__(self, N): 290 """ 291 Return the wavelengths of N rays. 292 """ 293 return np.ones(N) * self.Wavelength 294 295class UniformSpectrum(Spectrum): 296 """ 297 Uniform spectrum. 298 Can be specified as any combination of min, max, central or width in either eV or nm. 299 """ 300 def __init__(self, eVMax = None, eVMin = None, eVCentral = None, 301 lambdaMax = None, lambdaMin = None, lambdaCentral = None, 302 eVWidth = None, lambdaWidth = None): 303 # Using the DepSolver, we calculate the minimum and maximum wavelengths 304 values, steps = UniformSpectraCalculator().calculate_values( 305 lambda_min = lambdaMin, 306 lambda_max = lambdaMax, 307 lambda_center = lambdaCentral, 308 lambda_width = lambdaWidth, 309 eV_min = eVMin, 310 eV_max = eVMax, 311 eV_center = eVCentral, 312 eV_width = eVWidth 313 ) 314 self.lambdaMin = values['lambda_min'] 315 self.lambdaMax = values['lambda_max'] 316 def __call__(self, N): 317 """ 318 Return the wavelengths of N rays. 319 """ 320 return np.linspace(self.lambdaMin, self.lambdaMax, N) 321 322 323# %% Simple sources 324class SimpleSource(Source): 325 """ 326 A simple monochromatic source defined by 4 parameters: 327 - Wavelength (in nm) 328 - Power distribution 329 - Ray origins distribution 330 - Ray directions distribution 331 """ 332 333 def __init__(self, Wavelength, PowerDistribution, RayOriginsDistribution, RayDirectionsDistribution): 334 self.Wavelength = Wavelength 335 self.PowerDistribution = PowerDistribution 336 self.RayOriginsDistribution = RayOriginsDistribution 337 self.RayDirectionsDistribution = RayDirectionsDistribution 338 339 def __call__(self, N): 340 """ 341 Return a list of N rays from the simple source. 342 """ 343 Origins = self.RayOriginsDistribution(N) 344 rng = np.random.default_rng() 345 rng.shuffle(Origins) 346 Directions = self.RayDirectionsDistribution(N) 347 Powers = self.PowerDistribution(Origins, Directions) 348 349 RayList = [] 350 for i in range(N): 351 RayList.append(mray.Ray(Origins[i], Directions[i], wavelength=self.Wavelength, number=i, intensity=Powers[i])) 352 return mray.RayList.from_list(RayList) 353 354 355class ListSource(Source): 356 """ 357 A source containing just a list of rays that can be prepared manually. 358 """ 359 def __init__(self, Rays): 360 self.Rays = Rays 361 362 def __call__(self, N): 363 """ 364 Return the list of rays. 365 """ 366 if N < len(self.Rays): 367 return self.Rays[:N] 368 elif N>len(self.Rays): 369 logger.warning("Requested number of rays is greater than the number of rays in the source. Returning the whole source.") 370 return self.Rays 371 return mray.RayList.from_list(self.Rays)
34class Source(ABC): 35 """ 36 Abstract base class for sources. 37 Fundamentally a source just has to be able to return a list of rays. 38 To do so, we make it callable. 39 """ 40 @abstractmethod 41 def __call__(self,N): 42 pass
Abstract base class for sources. Fundamentally a source just has to be able to return a list of rays. To do so, we make it callable.
44class PowerDistribution(ABC): 45 """ 46 Abstract base class for angular power distributions. 47 """ 48 @abstractmethod 49 def __call__(self, Origins, Directions): 50 """ 51 Return the power of the rays coming 52 from the origin points Origins to the directions Directions. 53 """ 54 pass
Abstract base class for angular power distributions.
55class RayOriginsDistribution(ABC): 56 """ 57 Abstract base class for ray origins distributions. 58 """ 59 @abstractmethod 60 def __call__(self, N): 61 """ 62 Return the origins of N rays. 63 """ 64 pass
Abstract base class for ray origins distributions.
66class RayDirectionsDistribution(ABC): 67 """ 68 Abstract base class for ray directions distributions. 69 """ 70 @abstractmethod 71 def __call__(self, N): 72 """ 73 Return the directions of N rays. 74 """ 75 pass
Abstract base class for ray directions distributions.
77class Spectrum(ABC): 78 """ 79 Abstract base class for spectra. 80 """ 81 @abstractmethod 82 def __call__(self, N): 83 """ 84 Return the wavelengths of N rays. 85 """ 86 pass
Abstract base class for spectra.
89class SpatialGaussianPowerDistribution(PowerDistribution): 90 """ 91 Spatial Gaussian power distribution, depending only on ray origin points, and not on directions. 92 93 Attributes 94 ---------- 95 Power : float 96 Peak power of the distribution in arbitrary units (user can keep track of their own units.) 97 98 W0 : float 99 1/e^2 beam waist in mm 100 """ 101 def __init__(self, Power, W0): 102 self.Power = Power 103 self.W0 = W0 104 105 def __call__(self, Origins, Directions): 106 """ 107 Return the power of the rays coming 108 from the origin points Origins to the directions Directions. 109 110 Parameters 111 ---------- 112 Origins : mgeo.PointArray 113 PointArray as defined in ModuleGeometry, with points of origin of the rays. 114 115 Directions : mgeo.VectorArray 116 VectorArray as defined in ModuleGeometry, with ray vectors. 117 118 Returns 119 ------- 120 Powers: numpy.array 121 Array of powers for each ray. 122 123 """ 124 return self.Power * np.exp(-2 * np.linalg.norm(Origins, axis=1) ** 2 / self.W0 ** 2)
Spatial Gaussian power distribution, depending only on ray origin points, and not on directions.
Power : float
Peak power of the distribution in arbitrary units (user can keep track of their own units.)
W0 : float
1/e^2 beam waist in mm
126class AngularGaussianPowerDistribution(PowerDistribution): 127 """ 128 Angular Gaussian power distribution, depending only on ray directions, but not origin points. 129 130 Attributes 131 ---------- 132 Power : float 133 Peak power of the distribution in arbitrary units (user can keep track of their own units.) 134 135 Divergence : float 136 1/e^2 divergence half-angle in radians. 137 """ 138 def __init__(self, Power, Divergence): 139 self.Power = Power 140 self.Divergence = Divergence 141 142 def __call__(self, Origins, Directions): 143 """ 144 Return the power of the rays coming 145 from the origin points Origins to the directions Directions. 146 147 Parameters 148 ---------- 149 Origins : mgeo.PointArray 150 PointArray as defined in ModuleGeometry, with points of origin of the rays. 151 152 Directions : mgeo.VectorArray 153 VectorArray as defined in ModuleGeometry, with ray vectors. 154 155 Returns 156 ------- 157 Powers: numpy.array 158 Array of powers for each ray. 159 160 """ 161 return self.Power * np.exp(-2 * np.arccos(np.dot(Directions, [0, 0, 1])) ** 2 / self.Divergence ** 2)
Angular Gaussian power distribution, depending only on ray directions, but not origin points.
Power : float
Peak power of the distribution in arbitrary units (user can keep track of their own units.)
Divergence : float
1/e^2 divergence half-angle in radians.
163class GaussianPowerDistribution(PowerDistribution): 164 """ 165 Gaussian power distribution, depending on ray origin points and directions. 166 167 Attributes 168 ---------- 169 Power : float 170 Peak power of the distribution in arbitrary units (user can keep track of their own units.) 171 172 W0 : float 173 1/e^2 beam waist in mm 174 175 Divergence : float 176 1/e^2 divergence half-angle in radians. 177 """ 178 def __init__(self, Power, W0, Divergence): 179 self.Power = Power 180 self.W0 = W0 181 self.Divergence = Divergence 182 183 def __call__(self, Origins, Directions): 184 """ 185 Return the power of the rays coming 186 from the origin points Origins to the directions Directions. 187 188 Parameters 189 ---------- 190 Origins : mgeo.PointArray 191 PointArray as defined in ModuleGeometry, with points of origin of the rays. 192 193 Directions : mgeo.VectorArray 194 VectorArray as defined in ModuleGeometry, with ray vectors. 195 196 Returns 197 ------- 198 Powers: numpy.array 199 Array of powers for each ray. 200 201 """ 202 return self.Power * np.exp(-2 * (Origins-mgeo.Origin).norm ** 2 / self.W0 ** 2) * np.exp(-2 * np.array(np.arccos(np.dot(Directions, mgeo.Vector([1, 0, 0])))) ** 2 / self.Divergence ** 2)
Gaussian power distribution, depending on ray origin points and directions.
Power : float
Peak power of the distribution in arbitrary units (user can keep track of their own units.)
W0 : float
1/e^2 beam waist in mm
Divergence : float
1/e^2 divergence half-angle in radians.
204class UniformPowerDistribution(PowerDistribution): 205 """ 206 Uniform power distribution. 207 """ 208 def __init__(self, Power): 209 self.Power = Power 210 211 def __call__(self, Origins, Directions): 212 """ 213 Return the power of the rays coming 214 from the origin points Origins to the directions Directions. 215 """ 216 return self.Power * np.ones(len(Origins))
Uniform power distribution.
219class PointRayOriginsDistribution(RayOriginsDistribution): 220 """ 221 Point ray origins distribution. 222 """ 223 def __init__(self, Origin): 224 self.Origin = Origin 225 226 def __call__(self, N): 227 """ 228 Return the origins of N rays. 229 """ 230 return mgeo.PointArray([self.Origin for i in range(N)])
Point ray origins distribution.
232class DiskRayOriginsDistribution(RayOriginsDistribution): 233 """ 234 Disk ray origins distribution. Uses the Vogel spiral to initialize the rays. 235 """ 236 def __init__(self, Origin, Radius, Normal = mgeo.Vector([0, 0, 1])): 237 self.Origin = Origin 238 self.Radius = Radius 239 self.Normal = Normal 240 241 def __call__(self, N): 242 """ 243 Return the origins of N rays. 244 """ 245 MatrixXY = mgeo.SpiralVogel(N, self.Radius) 246 q = mgeo.QRotationVector2Vector(mgeo.Vector([0, 0, 1]), self.Normal) 247 return mgeo.PointArray([self.Origin + mgeo.Vector([MatrixXY[i, 0], MatrixXY[i, 1], 0]) for i in range(N)]).rotate(q)
Disk ray origins distribution. Uses the Vogel spiral to initialize the rays.
250class UniformRayDirectionsDistribution(RayDirectionsDistribution): 251 """ 252 Uniform ray directions distribution. 253 """ 254 def __init__(self, Direction): 255 self.Direction = Direction 256 257 def __call__(self, N): 258 """ 259 Return the directions of N rays. 260 """ 261 return mgeo.VectorArray([self.Direction for i in range(N)])
Uniform ray directions distribution.
263class ConeRayDirectionsDistribution(RayDirectionsDistribution): 264 """ 265 Cone ray directions distribution. Uses the Vogel spiral to initialize the rays. 266 """ 267 def __init__(self, Direction, Angle): 268 self.Direction = Direction 269 self.Angle = Angle 270 271 def __call__(self, N): 272 """ 273 Return the directions of N rays. 274 """ 275 Height = 1 276 Radius = Height * np.tan(self.Angle) 277 MatrixXY = mgeo.SpiralVogel(N, Radius) 278 q = mgeo.QRotationVector2Vector(mgeo.Vector([0, 0, 1]), self.Direction) 279 return mgeo.VectorArray([[MatrixXY[i, 0], MatrixXY[i, 1], Height] for i in range(N)]).rotate(q).normalized()
Cone ray directions distribution. Uses the Vogel spiral to initialize the rays.
283class SingleWavelengthSpectrum(Spectrum): 284 """ 285 Single wavelength spectrum. 286 """ 287 def __init__(self, Wavelength): 288 self.Wavelength = Wavelength 289 290 def __call__(self, N): 291 """ 292 Return the wavelengths of N rays. 293 """ 294 return np.ones(N) * self.Wavelength
Single wavelength spectrum.
296class UniformSpectrum(Spectrum): 297 """ 298 Uniform spectrum. 299 Can be specified as any combination of min, max, central or width in either eV or nm. 300 """ 301 def __init__(self, eVMax = None, eVMin = None, eVCentral = None, 302 lambdaMax = None, lambdaMin = None, lambdaCentral = None, 303 eVWidth = None, lambdaWidth = None): 304 # Using the DepSolver, we calculate the minimum and maximum wavelengths 305 values, steps = UniformSpectraCalculator().calculate_values( 306 lambda_min = lambdaMin, 307 lambda_max = lambdaMax, 308 lambda_center = lambdaCentral, 309 lambda_width = lambdaWidth, 310 eV_min = eVMin, 311 eV_max = eVMax, 312 eV_center = eVCentral, 313 eV_width = eVWidth 314 ) 315 self.lambdaMin = values['lambda_min'] 316 self.lambdaMax = values['lambda_max'] 317 def __call__(self, N): 318 """ 319 Return the wavelengths of N rays. 320 """ 321 return np.linspace(self.lambdaMin, self.lambdaMax, N)
Uniform spectrum. Can be specified as any combination of min, max, central or width in either eV or nm.
301 def __init__(self, eVMax = None, eVMin = None, eVCentral = None, 302 lambdaMax = None, lambdaMin = None, lambdaCentral = None, 303 eVWidth = None, lambdaWidth = None): 304 # Using the DepSolver, we calculate the minimum and maximum wavelengths 305 values, steps = UniformSpectraCalculator().calculate_values( 306 lambda_min = lambdaMin, 307 lambda_max = lambdaMax, 308 lambda_center = lambdaCentral, 309 lambda_width = lambdaWidth, 310 eV_min = eVMin, 311 eV_max = eVMax, 312 eV_center = eVCentral, 313 eV_width = eVWidth 314 ) 315 self.lambdaMin = values['lambda_min'] 316 self.lambdaMax = values['lambda_max']
325class SimpleSource(Source): 326 """ 327 A simple monochromatic source defined by 4 parameters: 328 - Wavelength (in nm) 329 - Power distribution 330 - Ray origins distribution 331 - Ray directions distribution 332 """ 333 334 def __init__(self, Wavelength, PowerDistribution, RayOriginsDistribution, RayDirectionsDistribution): 335 self.Wavelength = Wavelength 336 self.PowerDistribution = PowerDistribution 337 self.RayOriginsDistribution = RayOriginsDistribution 338 self.RayDirectionsDistribution = RayDirectionsDistribution 339 340 def __call__(self, N): 341 """ 342 Return a list of N rays from the simple source. 343 """ 344 Origins = self.RayOriginsDistribution(N) 345 rng = np.random.default_rng() 346 rng.shuffle(Origins) 347 Directions = self.RayDirectionsDistribution(N) 348 Powers = self.PowerDistribution(Origins, Directions) 349 350 RayList = [] 351 for i in range(N): 352 RayList.append(mray.Ray(Origins[i], Directions[i], wavelength=self.Wavelength, number=i, intensity=Powers[i])) 353 return mray.RayList.from_list(RayList)
A simple monochromatic source defined by 4 parameters:
334 def __init__(self, Wavelength, PowerDistribution, RayOriginsDistribution, RayDirectionsDistribution): 335 self.Wavelength = Wavelength 336 self.PowerDistribution = PowerDistribution 337 self.RayOriginsDistribution = RayOriginsDistribution 338 self.RayDirectionsDistribution = RayDirectionsDistribution
356class ListSource(Source): 357 """ 358 A source containing just a list of rays that can be prepared manually. 359 """ 360 def __init__(self, Rays): 361 self.Rays = Rays 362 363 def __call__(self, N): 364 """ 365 Return the list of rays. 366 """ 367 if N < len(self.Rays): 368 return self.Rays[:N] 369 elif N>len(self.Rays): 370 logger.warning("Requested number of rays is greater than the number of rays in the source. Returning the whole source.") 371 return self.Rays 372 return mray.RayList.from_list(self.Rays)
A source containing just a list of rays that can be prepared manually.
Provides classes for different shapes of a support for optics.
A support fixes the spatial extent and outer shape of optics - think of it as a substrate. Technically, the support is a section of the x-y-plane in the element coordinate frame.
The support will become an attribute of Mirror or Mask objects.
Created in Sept 2020
@author: Anthony Guillaume and Stefan Haessler
1""" 2Provides classes for different shapes of a support for optics. 3 4A support fixes the spatial extent and outer shape of optics - think of it as a substrate. 5Technically, the support is a section of the x-y-plane in the element coordinate frame. 6 7The support will become an attribute of Mirror or Mask objects. 8 9 10 11 12 13Created in Sept 2020 14 15@author: Anthony Guillaume and Stefan Haessler 16""" 17 18# %% 19 20import numpy as np 21import math 22import ARTcore.ModuleGeometry as mgeo 23from abc import ABC, abstractmethod 24import logging 25 26logger = logging.getLogger(__name__) 27 28 29# %% Abstract Support Class 30class Support(ABC): 31 """ 32 Abstract base class for optics supports. 33 Supports the `in` operator to test points. 34 """ 35 @abstractmethod 36 def _sdf(self, Point): 37 """ 38 Signed distance function for the support. 39 """ 40 pass 41 42 def __contains__(self, Point): 43 """ 44 Interface allowing the use of the `in` operator to check if a point is within the support. 45 """ 46 return self._sdf(Point) <= 0 47 48 def _estimate_size(self, initial_distance=1000): 49 """ 50 Estimate the size of the support using the signed distance function. 51 """ 52 directions = np.array([[1, 0], [-1, 0], [0, 1], [0, -1]]) # Simple axis-aligned directions 53 points = directions * initial_distance # Generate points on a circle of the current radius 54 distances = np.array([self._sdf(p) for p in points]) 55 avg_distance = np.mean(distances) 56 57 return initial_distance - avg_distance 58 59 60 61 62 63# %% Round Support 64class SupportRound(Support): 65 """ 66 A round support for optics. 67 68 Attributes 69 ---------- 70 radius : float 71 The radius of the support in mm. 72 """ 73 74 def __init__(self, Radius: float): 75 """ 76 Create a round support. 77 78 Parameters 79 ---------- 80 Radius : float 81 The radius of the support in mm. 82 83 """ 84 self.radius = Radius 85 86 def __repr__(self): 87 return f"{type(self).__name__}({self.radius})" 88 89 def _sdf(self, Point): 90 """ 91 Return signed distance from the support. 92 """ 93 return mgeo.SDF_Circle(Point, self.radius) 94 95 def _get_grid(self, NbPoint: int, **kwargs): 96 """ 97 Return a Point array with the coordinates of a number NbPoints of points. 98 The points are distributed as a Vogel-spiral on the support, with the origin in the center of the support. 99 """ 100 MatrixXY = mgeo.SpiralVogel(NbPoint, self.radius) 101 return mgeo.Point(np.hstack((MatrixXY, np.zeros((MatrixXY.shape[0], 1))))) 102 103 104# %% Round Support with Hole 105class SupportRoundHole(Support): 106 """ 107 A round support for optics with a round hole. 108 109 Attributes 110 ---------- 111 radius : float 112 The radius of the support in mm. 113 114 radiushole : float 115 The radius of the hole in mm. 116 117 centerholeX : float 118 The x-cordinate of the hole's centre, in mm. 119 120 centerholeY : float 121 The y-cordinate of the hole's centre, in mm. 122 123 """ 124 125 def __init__(self, Radius: float, RadiusHole: float, CenterHoleX: float, CenterHoleY: float): 126 """ 127 Parameters 128 ---------- 129 Radius : float 130 The radius of the support in mm. 131 132 RadiusHole : float 133 The radius of the hole in mm. 134 135 CenterHoleX : float 136 The x-cordinate of the hole's centre, in mm. 137 138 CenterHoleY : float 139 The y-cordinate of the hole's centre, in mm. 140 141 """ 142 self.radius = Radius 143 self.radiushole = RadiusHole 144 self.centerholeX = CenterHoleX 145 self.centerholeY = CenterHoleY 146 147 def __repr__(self): 148 return f"{type(self).__name__}(Radius={self.radius}, RadiusHole={self.radiushole}, CenterHoleX={self.centerholeX}, CenterHoleY={self.centerholeY})" 149 150 def _sdf(self, Point): 151 """Return signed distance from the support.""" 152 support = mgeo.SDF_Circle(Point, self.radius) 153 hole = mgeo.SDF_Circle(Point[:2] - np.array([self.centerholeX, self.centerholeY]), self.radiushole) 154 return mgeo.Difference_SDF(support, hole) 155 156 def _get_grid(self, NbPoint, **kwargs): 157 """ 158 Returns a list of 2D-numpy-arrays with the coordinates a number NbPoints of points, 159 distributed as a Vogel-spiral on the support, with the origin in the center of the support. 160 """ 161 MatrixXY = mgeo.SpiralVogel(NbPoint, self.radius) 162 return MatrixXY 163 164# %% Rectangular Support 165class SupportRectangle(Support): 166 """ 167 A rectangular support for optics. 168 169 Attributes 170 ---------- 171 dimX : float 172 The dimension in mm along x. 173 174 dimY : float 175 The dimension in mm along y. 176 177 """ 178 179 def __init__(self, DimensionX: float, DimensionY: float): 180 """ 181 Parameters 182 ---------- 183 DimensionX : float 184 The dimension in mm along x. 185 186 DimensionY : float 187 The dimension in mm along y. 188 189 """ 190 self.dimX = DimensionX 191 self.dimY = DimensionY 192 193 def __repr__(self): 194 return f"{type(self).__name__}(DimensionX={self.dimX}, DimensionY={self.dimY})" 195 196 def _sdf(self, Point): 197 """Return signed distance from the support.""" 198 return mgeo.SDF_Rectangle(Point, self.dimX, self.dimY) 199 200 def _get_grid(self, NbPoints: int, **kwargs) -> list[np.ndarray]: 201 """ 202 Returns a list of 2D-numpy-arrays with the coordinates a number NbPoints of points, 203 distributed as a regular grid on the support, with the origin in the center of the support. 204 """ 205 nbx = int( 206 np.sqrt(self.dimX / self.dimY * NbPoints + 0.25 * (self.dimX - self.dimY) ** 2 / self.dimY**2) 207 - 0.5 * (self.dimX - self.dimY) / self.dimY 208 ) 209 nby = int(NbPoints / nbx) 210 x = np.linspace(-self.dimX / 2, self.dimX / 2, nbx) 211 y = np.linspace(-self.dimY / 2, self.dimY / 2, nby) 212 ListCoordXY = [] 213 for i in x: 214 for j in y: 215 ListCoordXY.append(np.array([i, j])) 216 return ListCoordXY 217 218 219# %% Rectangular Support with Hole 220class SupportRectangleHole(Support): 221 """ 222 A rectangular support for optics with a round hole. 223 224 Attributes 225 ---------- 226 dimX : float 227 The dimension in mm along x. 228 229 dimY : float 230 The dimension in mm along y. 231 232 radiushole : float 233 The radius of the hole in mm. 234 235 centerholeX : float 236 The x-cordinate of the hole's centre, in mm. 237 238 centerholeY : float 239 The y-cordinate of the hole's centre, in mm. 240 241 """ 242 243 def __init__(self, DimensionX: float, DimensionY: float, RadiusHole: float, CenterHoleX: float, CenterHoleY: float): 244 """ 245 Parameters 246 ---------- 247 DimensionX : float 248 The dimension in mm along x. 249 250 DimensionY : float 251 The dimension in mm along y. 252 253 RadiusHole : float 254 The radius of the hole in mm. 255 256 CenterHoleX : float 257 The x-cordinate of the hole's centre, in mm. 258 259 CenterHoleY : float 260 The y-cordinate of the hole's centre, in mm. 261 262 """ 263 self.dimX = DimensionX 264 self.dimY = DimensionY 265 self.radiushole = RadiusHole 266 self.centerholeX = CenterHoleX 267 self.centerholeY = CenterHoleY 268 269 def __repr__(self): 270 return f"{type(self).__name__}(DimensionX={self.dimX}, DimensionY={self.dimY}, RadiusHole={self.radiushole}, CenterHoleX={self.centerholeX}, CenterHoleY={self.centerholeY})" 271 272 def _sdf(self, Point): 273 """Return signed distance from the support.""" 274 support = mgeo.SDF_Rectangle(Point, self.dimX, self.dimY) 275 hole = mgeo.SDF_Circle(Point[:2] - np.array([self.centerholeX, self.centerholeY]), self.radiushole) 276 return mgeo.Difference_SDF(support, hole) 277 278 279 280# %% Rectangular Support with Rectangular Hole 281class SupportRectangleRectHole(Support): 282 """ 283 A rectangular support for optics, with a rectangular hole. 284 285 Attributes 286 ---------- 287 dimX : float 288 The dimension in mm along x. 289 290 dimY : float 291 The dimension in mm along y. 292 293 holeX : float 294 The dimension of the hole in mm along x. 295 296 holeY : float 297 The dimension of the hole in mm along y. 298 299 centerholeX : float 300 The x-cordinate of the hole's centre, in mm. 301 302 centerholeY : float 303 The y-cordinate of the hole's centre, in mm. 304 305 """ 306 307 def __init__( 308 self, DimensionX: float, DimensionY: float, HoleX: float, HoleY: float, CenterHoleX: float, CenterHoleY: float 309 ): 310 """ 311 Parameters 312 ---------- 313 DimensionX : float 314 The dimension in mm along x. 315 316 DimensionY : float 317 The dimension in mm along y. 318 319 HoleX : float 320 The dimension of the hole in mm along x. 321 322 HoleY : float 323 The dimension of the hole in mm along y. 324 325 CenterHoleX : float 326 The x-cordinate of the hole's centre, in mm. 327 328 CenterHoleY : float 329 The y-cordinate of the hole's centre, in mm. 330 331 """ 332 self.dimX = DimensionX 333 self.dimY = DimensionY 334 self.holeX = HoleX 335 self.holeY = HoleY 336 self.centerholeX = CenterHoleX 337 self.centerholeY = CenterHoleY 338 339 def __repr__(self): 340 return f"{type(self).__name__}(DimensionX={self.dimX}, DimensionY={self.dimY}, HoleX={self.holeX}, HoleY={self.holeY}, CenterHoleX={self.centerholeX}, CenterHoleY={self.centerholeY})" 341 342 def _sdf(self, Point): 343 """Return signed distance from the support.""" 344 support = mgeo.SDF_Rectangle(Point, self.dimX, self.dimY) 345 hole = mgeo.SDF_Rectangle(Point[:2] - np.array([self.centerholeX, self.centerholeY]), self.holeX, self.holeY) 346 return mgeo.Difference_SDF(support, hole)
31class Support(ABC): 32 """ 33 Abstract base class for optics supports. 34 Supports the `in` operator to test points. 35 """ 36 @abstractmethod 37 def _sdf(self, Point): 38 """ 39 Signed distance function for the support. 40 """ 41 pass 42 43 def __contains__(self, Point): 44 """ 45 Interface allowing the use of the `in` operator to check if a point is within the support. 46 """ 47 return self._sdf(Point) <= 0 48 49 def _estimate_size(self, initial_distance=1000): 50 """ 51 Estimate the size of the support using the signed distance function. 52 """ 53 directions = np.array([[1, 0], [-1, 0], [0, 1], [0, -1]]) # Simple axis-aligned directions 54 points = directions * initial_distance # Generate points on a circle of the current radius 55 distances = np.array([self._sdf(p) for p in points]) 56 avg_distance = np.mean(distances) 57 58 return initial_distance - avg_distance
Abstract base class for optics supports.
Supports the in operator to test points.
65class SupportRound(Support): 66 """ 67 A round support for optics. 68 69 Attributes 70 ---------- 71 radius : float 72 The radius of the support in mm. 73 """ 74 75 def __init__(self, Radius: float): 76 """ 77 Create a round support. 78 79 Parameters 80 ---------- 81 Radius : float 82 The radius of the support in mm. 83 84 """ 85 self.radius = Radius 86 87 def __repr__(self): 88 return f"{type(self).__name__}({self.radius})" 89 90 def _sdf(self, Point): 91 """ 92 Return signed distance from the support. 93 """ 94 return mgeo.SDF_Circle(Point, self.radius) 95 96 def _get_grid(self, NbPoint: int, **kwargs): 97 """ 98 Return a Point array with the coordinates of a number NbPoints of points. 99 The points are distributed as a Vogel-spiral on the support, with the origin in the center of the support. 100 """ 101 MatrixXY = mgeo.SpiralVogel(NbPoint, self.radius) 102 return mgeo.Point(np.hstack((MatrixXY, np.zeros((MatrixXY.shape[0], 1)))))
A round support for optics.
radius : float
The radius of the support in mm.
106class SupportRoundHole(Support): 107 """ 108 A round support for optics with a round hole. 109 110 Attributes 111 ---------- 112 radius : float 113 The radius of the support in mm. 114 115 radiushole : float 116 The radius of the hole in mm. 117 118 centerholeX : float 119 The x-cordinate of the hole's centre, in mm. 120 121 centerholeY : float 122 The y-cordinate of the hole's centre, in mm. 123 124 """ 125 126 def __init__(self, Radius: float, RadiusHole: float, CenterHoleX: float, CenterHoleY: float): 127 """ 128 Parameters 129 ---------- 130 Radius : float 131 The radius of the support in mm. 132 133 RadiusHole : float 134 The radius of the hole in mm. 135 136 CenterHoleX : float 137 The x-cordinate of the hole's centre, in mm. 138 139 CenterHoleY : float 140 The y-cordinate of the hole's centre, in mm. 141 142 """ 143 self.radius = Radius 144 self.radiushole = RadiusHole 145 self.centerholeX = CenterHoleX 146 self.centerholeY = CenterHoleY 147 148 def __repr__(self): 149 return f"{type(self).__name__}(Radius={self.radius}, RadiusHole={self.radiushole}, CenterHoleX={self.centerholeX}, CenterHoleY={self.centerholeY})" 150 151 def _sdf(self, Point): 152 """Return signed distance from the support.""" 153 support = mgeo.SDF_Circle(Point, self.radius) 154 hole = mgeo.SDF_Circle(Point[:2] - np.array([self.centerholeX, self.centerholeY]), self.radiushole) 155 return mgeo.Difference_SDF(support, hole) 156 157 def _get_grid(self, NbPoint, **kwargs): 158 """ 159 Returns a list of 2D-numpy-arrays with the coordinates a number NbPoints of points, 160 distributed as a Vogel-spiral on the support, with the origin in the center of the support. 161 """ 162 MatrixXY = mgeo.SpiralVogel(NbPoint, self.radius) 163 return MatrixXY
A round support for optics with a round hole.
radius : float
The radius of the support in mm.
radiushole : float
The radius of the hole in mm.
centerholeX : float
The x-cordinate of the hole's centre, in mm.
centerholeY : float
The y-cordinate of the hole's centre, in mm.
126 def __init__(self, Radius: float, RadiusHole: float, CenterHoleX: float, CenterHoleY: float): 127 """ 128 Parameters 129 ---------- 130 Radius : float 131 The radius of the support in mm. 132 133 RadiusHole : float 134 The radius of the hole in mm. 135 136 CenterHoleX : float 137 The x-cordinate of the hole's centre, in mm. 138 139 CenterHoleY : float 140 The y-cordinate of the hole's centre, in mm. 141 142 """ 143 self.radius = Radius 144 self.radiushole = RadiusHole 145 self.centerholeX = CenterHoleX 146 self.centerholeY = CenterHoleY
Radius : float The radius of the support in mm.
RadiusHole : float The radius of the hole in mm.
CenterHoleX : float The x-cordinate of the hole's centre, in mm.
CenterHoleY : float The y-cordinate of the hole's centre, in mm.
166class SupportRectangle(Support): 167 """ 168 A rectangular support for optics. 169 170 Attributes 171 ---------- 172 dimX : float 173 The dimension in mm along x. 174 175 dimY : float 176 The dimension in mm along y. 177 178 """ 179 180 def __init__(self, DimensionX: float, DimensionY: float): 181 """ 182 Parameters 183 ---------- 184 DimensionX : float 185 The dimension in mm along x. 186 187 DimensionY : float 188 The dimension in mm along y. 189 190 """ 191 self.dimX = DimensionX 192 self.dimY = DimensionY 193 194 def __repr__(self): 195 return f"{type(self).__name__}(DimensionX={self.dimX}, DimensionY={self.dimY})" 196 197 def _sdf(self, Point): 198 """Return signed distance from the support.""" 199 return mgeo.SDF_Rectangle(Point, self.dimX, self.dimY) 200 201 def _get_grid(self, NbPoints: int, **kwargs) -> list[np.ndarray]: 202 """ 203 Returns a list of 2D-numpy-arrays with the coordinates a number NbPoints of points, 204 distributed as a regular grid on the support, with the origin in the center of the support. 205 """ 206 nbx = int( 207 np.sqrt(self.dimX / self.dimY * NbPoints + 0.25 * (self.dimX - self.dimY) ** 2 / self.dimY**2) 208 - 0.5 * (self.dimX - self.dimY) / self.dimY 209 ) 210 nby = int(NbPoints / nbx) 211 x = np.linspace(-self.dimX / 2, self.dimX / 2, nbx) 212 y = np.linspace(-self.dimY / 2, self.dimY / 2, nby) 213 ListCoordXY = [] 214 for i in x: 215 for j in y: 216 ListCoordXY.append(np.array([i, j])) 217 return ListCoordXY
A rectangular support for optics.
dimX : float
The dimension in mm along x.
dimY : float
The dimension in mm along y.
180 def __init__(self, DimensionX: float, DimensionY: float): 181 """ 182 Parameters 183 ---------- 184 DimensionX : float 185 The dimension in mm along x. 186 187 DimensionY : float 188 The dimension in mm along y. 189 190 """ 191 self.dimX = DimensionX 192 self.dimY = DimensionY
DimensionX : float The dimension in mm along x.
DimensionY : float The dimension in mm along y.
221class SupportRectangleHole(Support): 222 """ 223 A rectangular support for optics with a round hole. 224 225 Attributes 226 ---------- 227 dimX : float 228 The dimension in mm along x. 229 230 dimY : float 231 The dimension in mm along y. 232 233 radiushole : float 234 The radius of the hole in mm. 235 236 centerholeX : float 237 The x-cordinate of the hole's centre, in mm. 238 239 centerholeY : float 240 The y-cordinate of the hole's centre, in mm. 241 242 """ 243 244 def __init__(self, DimensionX: float, DimensionY: float, RadiusHole: float, CenterHoleX: float, CenterHoleY: float): 245 """ 246 Parameters 247 ---------- 248 DimensionX : float 249 The dimension in mm along x. 250 251 DimensionY : float 252 The dimension in mm along y. 253 254 RadiusHole : float 255 The radius of the hole in mm. 256 257 CenterHoleX : float 258 The x-cordinate of the hole's centre, in mm. 259 260 CenterHoleY : float 261 The y-cordinate of the hole's centre, in mm. 262 263 """ 264 self.dimX = DimensionX 265 self.dimY = DimensionY 266 self.radiushole = RadiusHole 267 self.centerholeX = CenterHoleX 268 self.centerholeY = CenterHoleY 269 270 def __repr__(self): 271 return f"{type(self).__name__}(DimensionX={self.dimX}, DimensionY={self.dimY}, RadiusHole={self.radiushole}, CenterHoleX={self.centerholeX}, CenterHoleY={self.centerholeY})" 272 273 def _sdf(self, Point): 274 """Return signed distance from the support.""" 275 support = mgeo.SDF_Rectangle(Point, self.dimX, self.dimY) 276 hole = mgeo.SDF_Circle(Point[:2] - np.array([self.centerholeX, self.centerholeY]), self.radiushole) 277 return mgeo.Difference_SDF(support, hole)
A rectangular support for optics with a round hole.
dimX : float
The dimension in mm along x.
dimY : float
The dimension in mm along y.
radiushole : float
The radius of the hole in mm.
centerholeX : float
The x-cordinate of the hole's centre, in mm.
centerholeY : float
The y-cordinate of the hole's centre, in mm.
244 def __init__(self, DimensionX: float, DimensionY: float, RadiusHole: float, CenterHoleX: float, CenterHoleY: float): 245 """ 246 Parameters 247 ---------- 248 DimensionX : float 249 The dimension in mm along x. 250 251 DimensionY : float 252 The dimension in mm along y. 253 254 RadiusHole : float 255 The radius of the hole in mm. 256 257 CenterHoleX : float 258 The x-cordinate of the hole's centre, in mm. 259 260 CenterHoleY : float 261 The y-cordinate of the hole's centre, in mm. 262 263 """ 264 self.dimX = DimensionX 265 self.dimY = DimensionY 266 self.radiushole = RadiusHole 267 self.centerholeX = CenterHoleX 268 self.centerholeY = CenterHoleY
DimensionX : float The dimension in mm along x.
DimensionY : float The dimension in mm along y.
RadiusHole : float The radius of the hole in mm.
CenterHoleX : float The x-cordinate of the hole's centre, in mm.
CenterHoleY : float The y-cordinate of the hole's centre, in mm.
282class SupportRectangleRectHole(Support): 283 """ 284 A rectangular support for optics, with a rectangular hole. 285 286 Attributes 287 ---------- 288 dimX : float 289 The dimension in mm along x. 290 291 dimY : float 292 The dimension in mm along y. 293 294 holeX : float 295 The dimension of the hole in mm along x. 296 297 holeY : float 298 The dimension of the hole in mm along y. 299 300 centerholeX : float 301 The x-cordinate of the hole's centre, in mm. 302 303 centerholeY : float 304 The y-cordinate of the hole's centre, in mm. 305 306 """ 307 308 def __init__( 309 self, DimensionX: float, DimensionY: float, HoleX: float, HoleY: float, CenterHoleX: float, CenterHoleY: float 310 ): 311 """ 312 Parameters 313 ---------- 314 DimensionX : float 315 The dimension in mm along x. 316 317 DimensionY : float 318 The dimension in mm along y. 319 320 HoleX : float 321 The dimension of the hole in mm along x. 322 323 HoleY : float 324 The dimension of the hole in mm along y. 325 326 CenterHoleX : float 327 The x-cordinate of the hole's centre, in mm. 328 329 CenterHoleY : float 330 The y-cordinate of the hole's centre, in mm. 331 332 """ 333 self.dimX = DimensionX 334 self.dimY = DimensionY 335 self.holeX = HoleX 336 self.holeY = HoleY 337 self.centerholeX = CenterHoleX 338 self.centerholeY = CenterHoleY 339 340 def __repr__(self): 341 return f"{type(self).__name__}(DimensionX={self.dimX}, DimensionY={self.dimY}, HoleX={self.holeX}, HoleY={self.holeY}, CenterHoleX={self.centerholeX}, CenterHoleY={self.centerholeY})" 342 343 def _sdf(self, Point): 344 """Return signed distance from the support.""" 345 support = mgeo.SDF_Rectangle(Point, self.dimX, self.dimY) 346 hole = mgeo.SDF_Rectangle(Point[:2] - np.array([self.centerholeX, self.centerholeY]), self.holeX, self.holeY) 347 return mgeo.Difference_SDF(support, hole)
A rectangular support for optics, with a rectangular hole.
dimX : float
The dimension in mm along x.
dimY : float
The dimension in mm along y.
holeX : float
The dimension of the hole in mm along x.
holeY : float
The dimension of the hole in mm along y.
centerholeX : float
The x-cordinate of the hole's centre, in mm.
centerholeY : float
The y-cordinate of the hole's centre, in mm.
308 def __init__( 309 self, DimensionX: float, DimensionY: float, HoleX: float, HoleY: float, CenterHoleX: float, CenterHoleY: float 310 ): 311 """ 312 Parameters 313 ---------- 314 DimensionX : float 315 The dimension in mm along x. 316 317 DimensionY : float 318 The dimension in mm along y. 319 320 HoleX : float 321 The dimension of the hole in mm along x. 322 323 HoleY : float 324 The dimension of the hole in mm along y. 325 326 CenterHoleX : float 327 The x-cordinate of the hole's centre, in mm. 328 329 CenterHoleY : float 330 The y-cordinate of the hole's centre, in mm. 331 332 """ 333 self.dimX = DimensionX 334 self.dimY = DimensionY 335 self.holeX = HoleX 336 self.holeY = HoleY 337 self.centerholeX = CenterHoleX 338 self.centerholeY = CenterHoleY
DimensionX : float The dimension in mm along x.
DimensionY : float The dimension in mm along y.
HoleX : float The dimension of the hole in mm along x.
HoleY : float The dimension of the hole in mm along y.
CenterHoleX : float The x-cordinate of the hole's centre, in mm.
CenterHoleY : float The y-cordinate of the hole's centre, in mm.
Provides classes for different reflective surfaces.
Created in Nov 2024
@author: Andre Kalouguine
1""" 2Provides classes for different reflective surfaces. 3 4Created in Nov 2024 5 6@author: Andre Kalouguine 7""" 8import ARTcore.ModuleGeometry as mgeo 9 10import numpy as np 11from copy import copy 12from abc import ABC, abstractmethod 13 14 15class Surface(ABC): 16 """ 17 Abstract base class for surfaces. 18 19 This is where we should ideally define the roughness, scratches/digs, spectral response, etc. 20 21 As for the global sag, that might be better defined in the optical elements themselves. 22 """ 23 pass 24 25class IdealSurface(Surface): 26 """ 27 Ideal surface, i.e. no roughness, no scratches, no digs, no spectral response. 28 """ 29 def __init__(self): 30 super().__init__() 31 def reflect_ray(self, ray, point, normal): 32 VectorRay = ray.vector 33 VectorRayReflected = VectorRay-2*normal*np.dot(VectorRay,normal) # Is it any better than SymmetricalVector? 34 local_reflectedray = copy(ray) 35 local_reflectedray.point = point 36 local_reflectedray.vector = VectorRayReflected 37 local_reflectedray.incidence = mgeo.AngleBetweenTwoVectors(-VectorRay, normal) 38 new_length = np.linalg.norm(point - ray.point) 39 local_reflectedray.path = ray.path + (new_length,) 40 return local_reflectedray 41
16class Surface(ABC): 17 """ 18 Abstract base class for surfaces. 19 20 This is where we should ideally define the roughness, scratches/digs, spectral response, etc. 21 22 As for the global sag, that might be better defined in the optical elements themselves. 23 """ 24 pass
Abstract base class for surfaces.
This is where we should ideally define the roughness, scratches/digs, spectral response, etc.
As for the global sag, that might be better defined in the optical elements themselves.
26class IdealSurface(Surface): 27 """ 28 Ideal surface, i.e. no roughness, no scratches, no digs, no spectral response. 29 """ 30 def __init__(self): 31 super().__init__() 32 def reflect_ray(self, ray, point, normal): 33 VectorRay = ray.vector 34 VectorRayReflected = VectorRay-2*normal*np.dot(VectorRay,normal) # Is it any better than SymmetricalVector? 35 local_reflectedray = copy(ray) 36 local_reflectedray.point = point 37 local_reflectedray.vector = VectorRayReflected 38 local_reflectedray.incidence = mgeo.AngleBetweenTwoVectors(-VectorRay, normal) 39 new_length = np.linalg.norm(point - ray.point) 40 local_reflectedray.path = ray.path + (new_length,) 41 return local_reflectedray
Ideal surface, i.e. no roughness, no scratches, no digs, no spectral response.
32 def reflect_ray(self, ray, point, normal): 33 VectorRay = ray.vector 34 VectorRayReflected = VectorRay-2*normal*np.dot(VectorRay,normal) # Is it any better than SymmetricalVector? 35 local_reflectedray = copy(ray) 36 local_reflectedray.point = point 37 local_reflectedray.vector = VectorRayReflected 38 local_reflectedray.incidence = mgeo.AngleBetweenTwoVectors(-VectorRay, normal) 39 new_length = np.linalg.norm(point - ray.point) 40 local_reflectedray.path = ray.path + (new_length,) 41 return local_reflectedray