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# %% More traditional vector operations that don't belong in the classes 153def Normalize(vector): 154 """ 155 Normalize Vector. 156 Obsolete, use use the mgeo.Vector class instead as it has a `normalize` method. 157 """ 158 return vector / np.linalg.norm(vector) 159 160def VectorPerpendicular(vector): 161 """ 162 Find a perpendicular 3D vector in some arbitrary direction 163 Undefined behavior, use with caution. There is no unique perpendicular vector to a 3D vector. 164 """ 165 logger.warning("VectorPerpendicular is undefined behavior. There is no unique perpendicular vector to a 3D vector.") 166 if abs(vector[0]) < 1e-15: 167 return Vector([1, 0, 0]) 168 if abs(vector[1]) < 1e-15: 169 return Vector([0, 1, 0]) 170 if abs(vector[2]) < 1e-15: 171 return Vector([0, 0, 1]) 172 173 # set arbitrarily a = b =1 174 return Vector([1, 1, -1.0 * (vector[0] + vector[1]) / vector[2]]).normalized() 175 176def AngleBetweenTwoVectors(U, V): 177 """ 178 Return the angle in radians between the vectors U and V ; formula from W.Kahan 179 Value in radians between 0 and pi. 180 """ 181 u = np.linalg.norm(U) 182 v = np.linalg.norm(V) 183 return 2 * np.arctan2(np.linalg.norm(U * v - V * u), np.linalg.norm(U * v + V * u)) 184 185def SymmetricalVector(V, SymmetryAxis): 186 """ 187 Return the symmetrical vector to V 188 """ 189 q = QRotationAroundAxis(SymmetryAxis, np.pi) 190 return V.rotate(q) 191 192def normal_add(N1, N2): 193 """ 194 Simple function that takes in two normal vectors of a deformation and calculates 195 the total normal vector if the two deformations were individually applied. 196 Be very careful, this *only* works when the surface is z = f(x,y) and when 197 the deformation is small. 198 Might be made obsolete when shifting to a modernised deformation system. 199 """ 200 normal1 = N1.normalized() 201 normal2 = N2.normalized() 202 grad1 = -normal1[:2] / normal1[2] 203 grad2 = -normal2[:2] / normal2[2] 204 grad = grad1 + grad2 205 total_normal = np.append(-grad, 1) 206 return Vector(total_normal).normalized() 207 208# %% Intersection finding 209def IntersectionLinePlane(A, u, P, n): 210 """ 211 Return the intersection point between a line and a plane. 212 A is a point of the line, u a vector of the line ; P is a point of the plane, n a normal vector 213 Line's equation : OM = u*t + OA , t a real 214 Plane's equation : n.OP - n.OM = 0 215 """ 216 t = np.dot(n, -A + P) / np.dot(u, n) 217 I = u * t + A 218 return I 219 220def IntersectionRayListZPlane(RayList, Z=np.array([0])): 221 """ 222 Return the intersection of a list of rays with a different planes with equiations z = Z[i] 223 Basically, by default it returns the intersection of the rays with the Z=0 plane but you can 224 give it a few values of Z and it should be faster than calling it multiple times. 225 This should let us quickly find the optimal position of the detector as well as trace the caustics. 226 If a ray does not intersect the plane... it should replace that point with a NaN. 227 """ 228 Positions = np.vstack([i.point for i in RayList]) 229 Vectors = np.vstack([i.vector for i in RayList]) 230 non_zero = Vectors[:,2] != 0 231 Positions = Positions[non_zero] 232 Vectors = Vectors[non_zero] 233 Z = Z[:, np.newaxis] 234 A = Positions[:,2]-Z 235 B = -Vectors[:,2] 236 #times = (Positions[:,2]-Z)/Vectors[:,2] 237 #return A,B 238 with np.errstate(divide='ignore', invalid='ignore'): 239 times = np.divide(A, B, where=(B != 0), out=np.full_like(A, np.nan)) 240 #times[times < 0] = np.nan # Set negative results to NaN 241 #return times 242 #positive_times = times >= 0 243 intersect_positions = Positions[:, :2] + times[:, :, np.newaxis] * Vectors[:, :2] 244 result = [] 245 for i in range(Z.shape[0]): 246 # For each plane, we find the intersection points 247 #valid_intersections = intersect_positions[i][positive_times[i]] 248 valid_intersections = intersect_positions[i] 249 result.append(PointArray(valid_intersections)) 250 return result 251 252 253# %% Geometrical utilities for plotting 254def SpiralVogel(NbPoint, Radius): 255 """ 256 Return a NbPoint x 2 matrix of 2D points representative of Vogel's spiral with radius Radius 257 Careful, contrary to most of the code, this is *not* in the 258 ARTcore.Vector or ARTcore.Point format. It is a simple numpy array. 259 The reason is that this is a utility function that can be used both to define directions 260 and to generate grids of points. 261 """ 262 GoldenAngle = np.pi * (3 - np.sqrt(5)) 263 r = np.sqrt(np.arange(NbPoint) / NbPoint) * Radius 264 265 theta = GoldenAngle * np.arange(NbPoint) 266 267 Matrix = np.zeros((NbPoint, 2)) 268 Matrix[:, 0] = np.cos(theta) 269 Matrix[:, 1] = np.sin(theta) 270 Matrix = Matrix * r.reshape((NbPoint, 1)) 271 272 return Matrix 273 274def find_hull(points): 275 """ 276 Find the convex hull of a set of points using a greedy algorithm. 277 This is used to create a polygon that encloses the points. 278 """ 279 # start from leftmost point 280 current_point = min(range(len(points)), key=lambda i: points[i][0]) 281 # initialize hull with current point 282 hull = [current_point] 283 # initialize list of linked points 284 linked = [] 285 # continue until all points have been linked 286 while len(linked) < len(points) - 1: 287 # initialize minimum distance and closest point 288 min_distance = math.inf 289 closest_point = None 290 # find closest unlinked point to current point 291 for i, point in enumerate(points): 292 if i not in linked: 293 distance = math.dist(points[current_point], point) 294 if distance < min_distance: 295 min_distance = distance 296 closest_point = i 297 # add closest point to hull and linked list 298 hull.append(closest_point) 299 linked.append(closest_point) 300 # update current point 301 current_point = closest_point 302 # add link between last point and first point 303 hull.append(hull[0]) 304 # convert hull to a list of pairs of indices 305 indices = [[hull[i], hull[i + 1]] for i in range(len(hull) - 1)] 306 return indices 307 308 309# %% Solvers and utilities for solving equations 310def SolverQuadratic(a, b, c): 311 """ 312 Solve the quadratic equation a*x^2 + b*x +c = 0 ; keep only real solutions 313 """ 314 Solution = np.roots([a, b, c]) 315 RealSolution = [] 316 317 for k in range(len(Solution)): 318 if abs(Solution[k].imag) < 1e-15: 319 RealSolution.append(Solution[k].real) 320 321 return RealSolution 322 323 324def SolverQuartic(a, b, c, d, e): 325 """ 326 Solve the quartic equation a*x^4 + b*x^3 +c*x^2 + d*x + e = 0 ; keep only real solutions 327 """ 328 Solution = np.roots([a, b, c, d, e]) 329 RealSolution = [] 330 331 for k in range(len(Solution)): 332 if abs(Solution[k].imag) < 1e-15: 333 RealSolution.append(Solution[k].real) 334 335 return RealSolution 336 337 338def KeepPositiveSolution(SolutionList): 339 """ 340 Keep only positive solution (numbers) in the list 341 """ 342 PositiveSolutionList = [] 343 epsilon = 1e-12 344 for k in SolutionList: 345 if k > epsilon: 346 PositiveSolutionList.append(k) 347 348 return PositiveSolutionList 349 350 351def KeepNegativeSolution(SolutionList): 352 """ 353 Keep only positive solution (numbers) in the list 354 """ 355 NegativeSolutionList = [] 356 epsilon = -1e-12 357 for k in SolutionList: 358 if k < epsilon: 359 NegativeSolutionList.append(k) 360 361 return NegativeSolutionList 362 363 364# %% Point geometry tools 365def ClosestPoint(A: Point, Points: PointArray): 366 """ 367 Given a reference point A and an array of points, return the index of the point closest to A 368 """ 369 distances = (Points-A).norm 370 return np.argmin(distances) 371 372def DiameterPointArray(Points: PointArray): 373 """ 374 Return the diameter of the smallest circle (for 2D points) 375 or sphere (3D points) including all the points. 376 """ 377 if len(Points) == 0: 378 return None 379 return float(np.ptp(Points, axis=0).max()) 380 381def CentrePointList(Points): 382 """ 383 Shift all 2D-points in PointList so as to center the point-cloud on the origin [0,0]. 384 """ 385 return Points - np.mean(Points, axis=0) 386 387# %% Solid object orientation 388 389def RotateSolid(Object, q): 390 """ 391 Rotate object around basepoint by quaternion q 392 """ 393 Object.q = q*Object.q 394 395def TranslateSolid(Object, T): 396 """ 397 Translate object by vector T 398 """ 399 Object.r = Object.r + T 400 401def RotateSolidAroundInternalPointByQ(Object, q, P): 402 """ 403 Rotate object around P by quaternion q where P is in the object's frame 404 """ 405 pass #TODO 406 407def RotateSolidAroundExternalPointByQ(Object, q, P): 408 """Rotate object around P by quaternion q, where P is in the global frame""" 409 pass #TODO 410 411 412# %% Signed distance functions 413 414def SDF_Rectangle(Point, SizeX, SizeY): 415 """Signed distance function for a rectangle centered at the origin""" 416 d = np.abs(Point[:2]) - np.array([SizeX, SizeY]) / 2 417 return (np.linalg.norm(np.maximum(d, 0)) + np.min(np.max(d, 0)))/2 418 419def SDF_Circle(Point, Radius): 420 """Signed distance function for a circle centered at the origin""" 421 return np.linalg.norm(Point[:2]) - Radius 422 423def Union_SDF(SDF1, SDF2): 424 """Union of two signed distance functions""" 425 return np.minimum(SDF1, SDF2) 426 427def Difference_SDF(SDF1, SDF2): 428 """Difference of two signed distance functions""" 429 return np.maximum(SDF1, -SDF2) 430 431def Intersection_SDF(SDF1, SDF2): 432 """Intersection of two signed distance functions""" 433 return np.maximum(SDF1, SDF2) 434 435 436 437# %% Quaternion calculations 438def QRotationAroundAxis(Axis, Angle): 439 """ 440 Return quaternion for rotation by Angle (in rad) around Axis 441 """ 442 rot_axis = Normalize(np.array([0.0] + Axis)) 443 axis_angle = (Angle * 0.5) * rot_axis 444 qlog = np.quaternion(*axis_angle) 445 q = np.exp(qlog) 446 return q 447 448def QRotationVector2Vector(Vector1, Vector2): 449 """ 450 Return a possible quaternion (among many) that would rotate Vector1 into Vector2. 451 Undefined behavior, use with caution. There is no unique quaternion that rotates one vector into another. 452 """ 453 Vector1 = Normalize(Vector1) 454 Vector2 = Normalize(Vector2) 455 a = np.cross(Vector1, Vector2) 456 return np.quaternion(1 + np.dot(Vector1, Vector2), *a).normalized() 457 458def QRotationVectorPair2VectorPair(InitialVector1, Vector1, InitialVector2, Vector2): 459 """ 460 Return the only quaternion that rotates InitialVector1 to Vector1 and InitialVector2 to Vector2. 461 Please ensure orthogonality two input and two output vectors. 462 """ 463 Vector1 = Normalize(Vector1) 464 Vector2 = Normalize(Vector2) 465 Vector3 = Normalize(np.cross(Vector1,Vector2)) 466 InitialVector1 = Normalize(InitialVector1) 467 InitialVector2 = Normalize(InitialVector2) 468 InitialVector3 = Normalize(np.cross(InitialVector1,InitialVector2)) 469 rot_2Initial = np.zeros((3,3)) 470 rot_2Initial[:,0] = InitialVector1 471 rot_2Initial[:,1] = InitialVector2 472 rot_2Initial[:,2] = InitialVector3 473 rot_2Final = np.zeros((3,3)) 474 rot_2Final[:,0] = Vector1 475 rot_2Final[:,1] = Vector2 476 rot_2Final[:,2] = Vector3 477 q2Init = quaternion.from_rotation_matrix(rot_2Initial) 478 q2Fin = quaternion.from_rotation_matrix(rot_2Final) 479 return (q2Fin/q2Init).normalized() 480 481 482# %% RayList stuff 483def RotationRayList(RayList, q): 484 """Like RotationPointList but with a list of Ray objects""" 485 return [i.rotate(q) for i in RayList] 486 487def TranslationRayList(RayList, T): 488 """Translate a RayList by vector T""" 489 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. 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:
>>> 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. 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:
>>> 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. 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:
>>> 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. 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:
>>> 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])
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)
Normalize Vector.
Obsolete, use use the mgeo.Vector class instead as it has a normalize method.
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()
Find a perpendicular 3D vector in some arbitrary direction Undefined behavior, use with caution. There is no unique perpendicular vector to a 3D vector.
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))
Return the angle in radians between the vectors U and V ; formula from W.Kahan Value in radians between 0 and pi.
186def SymmetricalVector(V, SymmetryAxis): 187 """ 188 Return the symmetrical vector to V 189 """ 190 q = QRotationAroundAxis(SymmetryAxis, np.pi) 191 return V.rotate(q)
Return the symmetrical vector to V
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()
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.
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
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
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
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.
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
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.
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
Find the convex hull of a set of points using a greedy algorithm. This is used to create a polygon that encloses the points.
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
Solve the quadratic equation ax^2 + bx +c = 0 ; keep only real solutions
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
Solve the quartic equation ax^4 + bx^3 +cx^2 + dx + e = 0 ; keep only real solutions
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
Keep only positive solution (numbers) in the list
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
Keep only positive solution (numbers) in the list
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)
Given a reference point A and an array of points, return the index of the point closest to A
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())
Return the diameter of the smallest circle (for 2D points) or sphere (3D points) including all the points.
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)
Shift all 2D-points in PointList so as to center the point-cloud on the origin [0,0].
390def RotateSolid(Object, q): 391 """ 392 Rotate object around basepoint by quaternion q 393 """ 394 Object.q = q*Object.q
Rotate object around basepoint by quaternion q
396def TranslateSolid(Object, T): 397 """ 398 Translate object by vector T 399 """ 400 Object.r = Object.r + T
Translate object by vector T
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
Rotate object around P by quaternion q where P is in the object's frame
408def RotateSolidAroundExternalPointByQ(Object, q, P): 409 """Rotate object around P by quaternion q, where P is in the global frame""" 410 pass #TODO
Rotate object around P by quaternion q, where P is in the global frame
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
Signed distance function for a rectangle centered at the origin
420def SDF_Circle(Point, Radius): 421 """Signed distance function for a circle centered at the origin""" 422 return np.linalg.norm(Point[:2]) - Radius
Signed distance function for a circle centered at the origin
424def Union_SDF(SDF1, SDF2): 425 """Union of two signed distance functions""" 426 return np.minimum(SDF1, SDF2)
Union of two signed distance functions
428def Difference_SDF(SDF1, SDF2): 429 """Difference of two signed distance functions""" 430 return np.maximum(SDF1, -SDF2)
Difference of two signed distance functions
432def Intersection_SDF(SDF1, SDF2): 433 """Intersection of two signed distance functions""" 434 return np.maximum(SDF1, SDF2)
Intersection of two signed distance functions
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
Return quaternion for rotation by Angle (in rad) around Axis
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()
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.
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()
Return the only quaternion that rotates InitialVector1 to Vector1 and InitialVector2 to Vector2. Please ensure orthogonality two input and two output vectors.
484def RotationRayList(RayList, q): 485 """Like RotationPointList but with a list of Ray objects""" 486 return [i.rotate(q) for i in RayList]
Like RotationPointList but with a list of Ray objects
488def TranslationRayList(RayList, T): 489 """Translate a RayList by vector T""" 490 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 ListPointIntersection = [] 288 for t in Solution: 289 Intersect = Ray.vector * t + Ray.point 290 if Intersect[2] < 0 and Intersect in self.support: 291 ListPointIntersection.append(Intersect) 292 293 return _IntersectionRayMirror(Ray.point, ListPointIntersection) 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) 303 304# %% Parabolic mirror definitions 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 479 480 481# %% Toroidal mirror definitions 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 ) 606 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 636 637 638# %% Ellipsoidal mirror definitions 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 811 812 813# %% Cylindrical mirror definitions 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) 906 907 908# %% Grazing parabolic mirror definitions 909# A grazing parabola is no different from a parabola, it's just a question of what we consider to be the support. 910# 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. 911 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 997 998 999# %% Reflections on mirrors, is it useful to keep this? 1000def _ReflectionMirrorRay(Mirror, PointMirror, Ray): 1001 """ 1002 Return the reflected ray according to the law of reflection. 1003 1004 Parameters 1005 ---------- 1006 Mirror : Mirror-objectS 1007 1008 PointMirror : np.ndarray 1009 Point of reflection on the mirror surface. 1010 1011 Ray : Ray-object 1012 1013 """ 1014 PointRay = Ray.point 1015 VectorRay = Ray.vector 1016 NormalMirror = Mirror.get_local_normal(PointMirror) 1017 1018 #VectorRayReflected = mgeo.SymmetricalVector(-VectorRay, NormalMirror) 1019 VectorRayReflected = VectorRay- 2*NormalMirror*np.dot(VectorRay,NormalMirror) # Is it any better than SymmetricalVector? 1020 1021 RayReflected = Ray.copy_ray() 1022 RayReflected.point = PointMirror 1023 RayReflected.vector = VectorRayReflected 1024 RayReflected.incidence = mgeo.AngleBetweenTwoVectors( 1025 -VectorRay, NormalMirror 1026 ) 1027 RayReflected.path = Ray.path + (np.linalg.norm(PointMirror - PointRay),) 1028 1029 return RayReflected 1030 1031 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 1060 1061 1062# %% Deformed mirror definitions, need to be reworked, maybe implemented as coatings? 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
An enumeration.
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.
651def DrawAsphericity(Mirror, Npoints=1000): 652 """ 653 This function displays a map of the asphericity of the mirror. 654 It's a scatter plot of the points of the mirror surface, with the color representing the distance to the closest sphere. 655 The closest sphere is calculated by the function get_closest_sphere, so least square method. 656 657 Parameters 658 ---------- 659 Mirror : Mirror 660 The mirror to analyse. 661 662 Npoints : int, optional 663 The number of points to sample on the mirror surface. The default is 1000. 664 665 Returns 666 ------- 667 fig : Figure 668 The figure of the plot. 669 """ 670 plt.ion() 671 fig = plt.figure() 672 ax = Mirror.support._ContourSupport(fig) 673 center, radius = man.GetClosestSphere(Mirror, Npoints) 674 Points = mpm.sample_support(Mirror.support, Npoints=1000) 675 Points += Mirror.r0[:2] 676 Z = Mirror._zfunc(Points) 677 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 678 X, Y = Points[:, 0] - Mirror.r0[0], Points[:, 1] - Mirror.r0[1] 679 Points_centered = Points - center 680 Distance = np.linalg.norm(Points_centered, axis=1) - radius 681 Distance*=1e3 # To convert to µm 682 p = plt.scatter(X, Y, c=Distance, s=15) 683 divider = man.make_axes_locatable(ax) 684 cax = divider.append_axes("right", size="5%", pad=0.05) 685 cbar = fig.colorbar(p, cax=cax) 686 cbar.set_label("Distance to closest sphere (µm)") 687 ax.set_xlabel("x (mm)") 688 ax.set_ylabel("y (mm)") 689 plt.title("Asphericity map", loc="right") 690 plt.tight_layout() 691 692 bbox = ax.get_position() 693 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 694 ax.set_position(bbox) 695 plt.show() 696 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 ListPointIntersection = [] 289 for t in Solution: 290 Intersect = Ray.vector * t + Ray.point 291 if Intersect[2] < 0 and Intersect in self.support: 292 ListPointIntersection.append(Intersect) 293 294 return _IntersectionRayMirror(Ray.point, ListPointIntersection) 295 296 def get_local_normal(self, Point): 297 """Return the normal unit vector on the spherical surface at point Point.""" 298 return -Vector(Point).normalized() 299 300 def _zfunc(self, PointArray): 301 x = PointArray[:,0] 302 y = PointArray[:,1] 303 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
296 def get_local_normal(self, Point): 297 """Return the normal unit vector on the spherical surface at point Point.""" 298 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.
306class MirrorParabolic(Mirror): 307 r""" 308 A paraboloid with vertex at the origin $O=[0,0,0]$ and symmetry axis z: 309 $z = \frac{1}{4f}[x^2 + y^2]$ where $f$ is the focal lenght of the *mother* 310 parabola (i.e. measured from its center at $O$ to the focal point $F$). 311 312 The center of the support is shifted along the x-direction by the off-axis distance $x_c$. 313 This leads to an *effective focal length* $f_\mathrm{eff}$, measured from the shifted center 314 of the support $P$ to the focal point $F$. 315 It is related to the mother focal length by $f = f_\\mathrm{eff} \cos^2(\alpha/2) $, 316 or equivalently $ p = 2f = f_\mathrm{eff} (1+\\cos\\alpha)$, where $\\alpha$ 317 is the off-axis angle, and $p = 2f$ is called the semi latus rectum. 318 319 Another useful relationship is that between the off-axis distance and the resulting 320 off-axis angle: $x_c = 2 f \tan(\alpha/2)$. 321 322 323  324 325 Attributes 326 ---------- 327 offaxisangle : float 328 Off-axis angle of the parabola. Modifying this also updates p, keeping feff constant. 329 Attention: The off-axis angle must be *given in degrees*, but is stored and will be *returned in radian* ! 330 331 feff : float 332 Effective focal length of the parabola in mm. Modifying this also updates p, keeping offaxisangle constant. 333 334 p : float 335 Semi latus rectum of the parabola in mm. Modifying this also updates feff, keeping offaxisangle constant. 336 337 support : ART.ModuleSupport.Support 338 339 type : str 'Parabolic Mirror'. 340 341 Methods 342 ------- 343 MirrorParabolic.get_normal(Point) 344 345 MirrorParabolic.get_centre() 346 347 MirrorParabolic.get_grid3D(NbPoints) 348 349 """ 350 #vectorised = True # Unitl I fix the closest solution thing 351 def __init__(self, Support, 352 FocalEffective: float=None, 353 OffAxisAngle: float = None, 354 FocalParent: float = None, 355 RadiusParent: float = None, 356 OffAxisDistance: float = None, 357 MoreThan90: bool = None, 358 **kwargs): 359 """ 360 Initialise a Parabolic mirror. 361 362 Parameters 363 ---------- 364 FocalEffective : float 365 Effective focal length of the parabola in mm. 366 367 OffAxisAngle : float 368 Off-axis angle *in degrees* of the parabola. 369 370 Support : ART.ModuleSupport.Support 371 372 """ 373 super().__init__() 374 self.curvature = Curvature.CONCAVE 375 self.support = Support 376 self.type = "Parabolic Mirror" 377 378 if "Surface" in kwargs: 379 self.Surface = kwargs["Surface"] 380 381 parameter_calculator = OAP_calculator() 382 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 383 fs=FocalEffective, 384 theta=OffAxisAngle, 385 fp=FocalParent, 386 Rc=RadiusParent, 387 OAD=OffAxisDistance, 388 more_than_90=MoreThan90) 389 self._offaxisangle = values["theta"] 390 self._feff = values["fs"] 391 self._p = values["p"] 392 self._offaxisdistance = values["OAD"] 393 self._fparent = values["fp"] 394 self._rparent = values["Rc"] 395 396 self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 397 398 self.centre_ref = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 399 self.support_normal_ref = Vector([0, 0, 1.0]) 400 self.majoraxis_ref = Vector([1.0, 0, 0]) 401 402 self.focus_ref = Point([0.0, 0, self._fparent]) 403 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 404 self.towards_collimated_ref = Vector([0, 0, 1.0]) 405 406 self.add_global_points("focus", "centre") 407 self.add_global_vectors("towards_focusing", "towards_collimated", "support_normal", "majoraxis") 408 409 def _get_intersection(self, Ray): 410 """Return the intersection point between the ray and the parabola.""" 411 ux, uy, uz = Ray.vector 412 xA, yA, zA = Ray.point 413 414 da = ux**2 + uy**2 415 db = 2 * (ux * xA + uy * yA) - 2 * self._p * uz 416 dc = xA**2 + yA**2 - 2 * self._p * zA 417 418 Solution = mgeo.SolverQuadratic(da, db, dc) 419 Solution = mgeo.KeepPositiveSolution(Solution) 420 IntersectionPoint = [Ray.point + Ray.vector * t for t in Solution if t > 0] 421 distances = [mgeo.Vector(i - self.r0).norm for i in IntersectionPoint] 422 IntersectionPoint = IntersectionPoint[distances.index(min(distances))] 423 return IntersectionPoint, IntersectionPoint-self.r0 in self.support 424 425 def _get_intersections(self, RayList): 426 """ 427 Vectorised version of the intersection calculation. 428 """ 429 vectors = np.array([ray.vector for ray in RayList]) 430 points = np.array([ray.point for ray in RayList]) 431 Solutions = np.zeros(len(RayList), dtype=float) 432 433 da = np.sum(vectors[:,0:2]**2, axis=1) 434 db = 2 * (np.sum(vectors[:,0:2] * points[:,0:2], axis=1) - self._p * vectors[:,2]) 435 dc = np.sum(points[:,0:2]**2, axis=1) - 2 * self._p * points[:,2] 436 437 linear = da == 0 438 Solutions[linear] = -dc[linear] / db[linear] 439 440 centered = (dc==0) & (da!=0) # If equation is of form ax**2 + bx = 0 441 Solutions[centered] = np.maximum(-db[centered] / da[centered], 0) 442 443 unstable = (4*da*dc) < db**2 * 1e-10 # Cases leading to catastrophic cancellation 444 445 Deltas = db**2 - 4 * da * dc 446 OK = Deltas >= 0 447 SolutionsPlus = (-db + np.sqrt(Deltas)) / 2 / da 448 SolutionsMinus = (-db - np.sqrt(Deltas)) / 2 / da 449 SolutionsPlus[unstable] = dc[unstable] / (da[unstable] * SolutionsMinus[unstable]) 450 SolutionsPlus = np.where(SolutionsPlus >= 0, SolutionsPlus, np.inf) 451 SolutionsMinus = np.where(SolutionsMinus >= 0, SolutionsMinus, np.inf) 452 Solutions = np.minimum(SolutionsPlus, SolutionsMinus) 453 Solutions = np.maximum(Solutions, 0) 454 Points = points + vectors * Solutions[:,np.newaxis] 455 OK = OK & (Solutions > 0) & np.array([p-self.r0 in self.support for p in Points]) 456 return mgeo.PointArray(Points), OK 457 458 def get_local_normal(self, Point): 459 """Return the normal unit vector on the paraboloid surface at point Point.""" 460 Gradient = Vector(np.zeros(3)) 461 Gradient[0] = -Point[0] 462 Gradient[1] = -Point[1] 463 Gradient[2] = self._p 464 return Gradient.normalized() 465 466 def get_local_normals(self, Points): 467 """ 468 Vectorised version of the normal calculation. 469 """ 470 Gradients = mgeo.VectorArray(np.zeros_like(Points)) 471 Gradients[:,0] = -Points[:,0] 472 Gradients[:,1] = -Points[:,1] 473 Gradients[:,2] = self._p 474 return Gradients.normalized() 475 476 def _zfunc(self, PointArray): 477 x = PointArray[:,0] 478 y = PointArray[:,1] 479 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)
351 def __init__(self, Support, 352 FocalEffective: float=None, 353 OffAxisAngle: float = None, 354 FocalParent: float = None, 355 RadiusParent: float = None, 356 OffAxisDistance: float = None, 357 MoreThan90: bool = None, 358 **kwargs): 359 """ 360 Initialise a Parabolic mirror. 361 362 Parameters 363 ---------- 364 FocalEffective : float 365 Effective focal length of the parabola in mm. 366 367 OffAxisAngle : float 368 Off-axis angle *in degrees* of the parabola. 369 370 Support : ART.ModuleSupport.Support 371 372 """ 373 super().__init__() 374 self.curvature = Curvature.CONCAVE 375 self.support = Support 376 self.type = "Parabolic Mirror" 377 378 if "Surface" in kwargs: 379 self.Surface = kwargs["Surface"] 380 381 parameter_calculator = OAP_calculator() 382 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 383 fs=FocalEffective, 384 theta=OffAxisAngle, 385 fp=FocalParent, 386 Rc=RadiusParent, 387 OAD=OffAxisDistance, 388 more_than_90=MoreThan90) 389 self._offaxisangle = values["theta"] 390 self._feff = values["fs"] 391 self._p = values["p"] 392 self._offaxisdistance = values["OAD"] 393 self._fparent = values["fp"] 394 self._rparent = values["Rc"] 395 396 self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 397 398 self.centre_ref = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 399 self.support_normal_ref = Vector([0, 0, 1.0]) 400 self.majoraxis_ref = Vector([1.0, 0, 0]) 401 402 self.focus_ref = Point([0.0, 0, self._fparent]) 403 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 404 self.towards_collimated_ref = Vector([0, 0, 1.0]) 405 406 self.add_global_points("focus", "centre") 407 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
458 def get_local_normal(self, Point): 459 """Return the normal unit vector on the paraboloid surface at point Point.""" 460 Gradient = Vector(np.zeros(3)) 461 Gradient[0] = -Point[0] 462 Gradient[1] = -Point[1] 463 Gradient[2] = self._p 464 return Gradient.normalized()
Return the normal unit vector on the paraboloid surface at point Point.
466 def get_local_normals(self, Points): 467 """ 468 Vectorised version of the normal calculation. 469 """ 470 Gradients = mgeo.VectorArray(np.zeros_like(Points)) 471 Gradients[:,0] = -Points[:,0] 472 Gradients[:,1] = -Points[:,1] 473 Gradients[:,2] = self._p 474 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.
483class MirrorToroidal(Mirror): 484 r""" 485 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. 486 487  488 489 Attributes 490 ---------- 491 majorradius : float 492 Major radius of the toroid in mm. 493 494 minorradius : float 495 Minor radius of the toroid in mm. 496 497 support : ART.ModuleSupport.Support 498 499 type : str 'Toroidal Mirror'. 500 501 Methods 502 ------- 503 MirrorToroidal.get_normal(Point) 504 505 MirrorToroidal.get_centre() 506 507 MirrorToroidal.get_grid3D(NbPoints) 508 509 """ 510 511 def __init__(self, Support, MajorRadius, MinorRadius, **kwargs): 512 """ 513 Construct a toroidal mirror. 514 515 Parameters 516 ---------- 517 MajorRadius : float 518 Major radius of the toroid in mm. 519 520 MinorRadius : float 521 Minor radius of the toroid in mm. 522 523 Support : ART.ModuleSupport.Support 524 525 """ 526 super().__init__() 527 self.support = Support 528 self.type = "Toroidal Mirror" 529 530 if "Surface" in kwargs: 531 self.Surface = kwargs["Surface"] 532 533 self.curvature = Curvature.CONCAVE 534 535 self.majorradius = MajorRadius 536 self.minorradius = MinorRadius 537 538 self.r0 = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 539 540 self.centre_ref = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 541 self.support_normal_ref = Vector([0, 0, 1.0]) 542 self.majoraxis_ref = Vector([1.0, 0, 0]) 543 544 self.add_global_vectors("support_normal", "majoraxis") 545 self.add_global_points("centre") 546 547 def _get_intersection(self, Ray): 548 """Return the intersection point between Ray and the toroidal mirror surface. The intersection points are given in the reference frame of the mirror""" 549 ux = Ray.vector[0] 550 uz = Ray.vector[2] 551 xA = Ray.point[0] 552 zA = Ray.point[2] 553 554 G = 4.0 * self.majorradius**2 * (ux**2 + uz**2) 555 H = 8.0 * self.majorradius**2 * (ux * xA + uz * zA) 556 I = 4.0 * self.majorradius**2 * (xA**2 + zA**2) 557 J = np.dot(Ray.vector, Ray.vector) 558 K = 2.0 * np.dot(Ray.vector, Ray.point) 559 L = ( 560 np.dot(Ray.point, Ray.point) 561 + self.majorradius**2 562 - self.minorradius**2 563 ) 564 565 a = J**2 566 b = 2 * J * K 567 c = 2 * J * L + K**2 - G 568 d = 2 * K * L - H 569 e = L**2 - I 570 571 Solution = mgeo.SolverQuartic(a, b, c, d, e) 572 if len(Solution) == 0: 573 return None, False 574 Solution = [t for t in Solution if t > 0] 575 IntersectionPoint = [Ray.point + Ray.vector *i for i in Solution] 576 distances = [mgeo.Vector(i - self.r0).norm for i in IntersectionPoint] 577 IntersectionPoint = IntersectionPoint[distances.index(min(distances))] 578 OK = IntersectionPoint - self.r0 in self.support and np.abs(IntersectionPoint[2]-self.r0[2]) < self.majorradius 579 return IntersectionPoint, OK 580 581 def get_local_normal(self, Point): 582 """Return the normal unit vector on the toroidal surface at point Point in the reference frame of the mirror""" 583 x = Point[0] 584 y = Point[1] 585 z = Point[2] 586 A = self.majorradius**2 - self.minorradius**2 587 588 Gradient = Vector(np.zeros(3)) 589 Gradient[0] = ( 590 4 * (x**3 + x * y**2 + x * z**2 + x * A) 591 - 8 * x * self.majorradius**2 592 ) 593 Gradient[1] = 4 * (y**3 + y * x**2 + y * z**2 + y * A) 594 Gradient[2] = ( 595 4 * (z**3 + z * x**2 + z * y**2 + z * A) 596 - 8 * z * self.majorradius**2 597 ) 598 599 return -Gradient.normalized() 600 601 def _zfunc(self, PointArray): 602 x = PointArray[:,0] 603 y = PointArray[:,1] 604 return -np.sqrt( 605 (np.sqrt(self.minorradius**2 - y**2) + self.majorradius)**2 - x**2 606 )
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)
511 def __init__(self, Support, MajorRadius, MinorRadius, **kwargs): 512 """ 513 Construct a toroidal mirror. 514 515 Parameters 516 ---------- 517 MajorRadius : float 518 Major radius of the toroid in mm. 519 520 MinorRadius : float 521 Minor radius of the toroid in mm. 522 523 Support : ART.ModuleSupport.Support 524 525 """ 526 super().__init__() 527 self.support = Support 528 self.type = "Toroidal Mirror" 529 530 if "Surface" in kwargs: 531 self.Surface = kwargs["Surface"] 532 533 self.curvature = Curvature.CONCAVE 534 535 self.majorradius = MajorRadius 536 self.minorradius = MinorRadius 537 538 self.r0 = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 539 540 self.centre_ref = Point([0.0, 0.0, -MajorRadius - MinorRadius]) 541 self.support_normal_ref = Vector([0, 0, 1.0]) 542 self.majoraxis_ref = Vector([1.0, 0, 0]) 543 544 self.add_global_vectors("support_normal", "majoraxis") 545 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
581 def get_local_normal(self, Point): 582 """Return the normal unit vector on the toroidal surface at point Point in the reference frame of the mirror""" 583 x = Point[0] 584 y = Point[1] 585 z = Point[2] 586 A = self.majorradius**2 - self.minorradius**2 587 588 Gradient = Vector(np.zeros(3)) 589 Gradient[0] = ( 590 4 * (x**3 + x * y**2 + x * z**2 + x * A) 591 - 8 * x * self.majorradius**2 592 ) 593 Gradient[1] = 4 * (y**3 + y * x**2 + y * z**2 + y * A) 594 Gradient[2] = ( 595 4 * (z**3 + z * x**2 + z * y**2 + z * A) 596 - 8 * z * self.majorradius**2 597 ) 598 599 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.
608def ReturnOptimalToroidalRadii( 609 Focal: float, AngleIncidence: float 610) -> (float, float): 611 """ 612 Get optimal parameters for a toroidal mirror. 613 614 Useful helper function to get the optimal major and minor radii for a toroidal mirror to achieve a 615 focal length 'Focal' with an angle of incidence 'AngleIncidence' and with vanishing astigmatism. 616 617 Parameters 618 ---------- 619 Focal : float 620 Focal length in mm. 621 622 AngleIncidence : int 623 Angle of incidence in degrees. 624 625 Returns 626 ------- 627 OptimalMajorRadius, OptimalMinorRadius : float, float. 628 """ 629 AngleIncidenceRadian = AngleIncidence * np.pi / 180 630 OptimalMajorRadius = ( 631 2 632 * Focal 633 * (1 / np.cos(AngleIncidenceRadian) - np.cos(AngleIncidenceRadian)) 634 ) 635 OptimalMinorRadius = 2 * Focal * np.cos(AngleIncidenceRadian) 636 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.
640class MirrorEllipsoidal(Mirror): 641 """ 642 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. 643 644  645 646 Attributes 647 ---------- 648 a : float 649 Semi major axis of the ellipsoid in mm. 650 651 b : float 652 Semi minor axis of the ellipsoid in mm. 653 654 support : ART.ModuleSupport.Support 655 656 type : str 'Ellipsoidal Mirror'. 657 658 Methods 659 ------- 660 MirrorEllipsoidal.get_normal(Point) 661 662 MirrorEllipsoidal.get_centre() 663 664 MirrorEllipsoidal.get_grid3D(NbPoints) 665 666 """ 667 668 def __init__( 669 self, 670 Support, 671 SemiMajorAxis=None, 672 SemiMinorAxis=None, 673 OffAxisAngle=None, 674 f_object=None, 675 f_image=None, 676 IncidenceAngle=None, 677 Magnification=None, 678 DistanceObjectImage=None, 679 Eccentricity=None, 680 **kwargs, 681 ): 682 """ 683 Generate an ellipsoidal mirror with given parameters. 684 685 The angles are given in degrees but converted to radians for internal calculations. 686 You can specify the semi-major and semi-minor axes, the off-axis angle, the object and image focal distances or some other parameters. 687 The constructor uses a magic calculator to determine the missing parameters. 688 689 Parameters 690 ---------- 691 Support : TYPE 692 ART.ModuleSupport.Support. 693 SemiMajorAxis : float (optional) 694 Semi major axis of the ellipsoid in mm.. 695 SemiMinorAxis : float (optional) 696 Semi minor axis of the ellipsoid in mm.. 697 OffAxisAngle : float (optional) 698 Off-axis angle of the mirror in mm. Defined as the angle at the centre of the mirror between the two foci.. 699 f_object : float (optional) 700 Object focal distance in mm. 701 f_image : float (optional) 702 Image focal distance in mm. 703 IncidenceAngle : float (optional) 704 Angle of incidence in degrees. 705 Magnification : float (optional) 706 Magnification. 707 DistanceObjectImage : float (optional) 708 Distance between object and image in mm. 709 Eccentricity : float (optional) 710 Eccentricity of the ellipsoid. 711 """ 712 super().__init__() 713 self.type = "Ellipsoidal Mirror" 714 self.support = Support 715 716 if "Surface" in kwargs: 717 self.Surface = kwargs["Surface"] 718 719 parameter_calculator = EllipsoidalMirrorCalculator() 720 values, steps = parameter_calculator.calculate_values(verify_consistency=True, 721 f1=f_object, 722 f2=f_image, 723 a=SemiMajorAxis, 724 b=SemiMinorAxis, 725 offset_angle=OffAxisAngle, 726 incidence_angle=IncidenceAngle, 727 m=Magnification, 728 l=DistanceObjectImage, 729 e=Eccentricity) 730 self.a = values["a"] 731 self.b = values["b"] 732 self._offaxisangle = np.deg2rad(values["offset_angle"]) 733 self._f_object = values["f1"] 734 self._f_image = values["f2"] 735 736 self.centre_ref = self._get_centre_ref() 737 738 self.r0 = self.centre_ref 739 740 self.support_normal_ref = Vector([0, 0, 1.0]) 741 self.majoraxis_ref = Vector([1.0, 0, 0]) 742 743 self.f1_ref = Point([-self.a, 0,0]) 744 self.f2_ref = Point([ self.a, 0,0]) 745 self.towards_image_ref = (self.f2_ref - self.centre_ref).normalized() 746 self.towards_object_ref = (self.f1_ref - self.centre_ref).normalized() 747 self.centre_normal_ref = self.get_local_normal(self.centre_ref) 748 749 self.add_global_points("f1", "f2", "centre") 750 self.add_global_vectors("towards_image", "towards_object", "centre_normal", "support_normal", "majoraxis") 751 752 def _get_intersection(self, Ray): 753 """Return the intersection point between Ray and the ellipsoidal mirror surface.""" 754 ux, uy, uz = Ray.vector 755 xA, yA, zA = Ray.point 756 757 da = (uy**2 + uz**2) / self.b**2 + (ux / self.a) ** 2 758 db = 2 * ((uy * yA + uz * zA) / self.b**2 + (ux * xA) / self.a**2) 759 dc = (yA**2 + zA**2) / self.b**2 + (xA / self.a) ** 2 - 1 760 761 Solution = mgeo.SolverQuadratic(da, db, dc) 762 Solution = mgeo.KeepPositiveSolution(Solution) 763 764 ListPointIntersection = [] 765 C = self.get_centre_ref() 766 for t in Solution: 767 Intersect = Ray.vector * t + Ray.point 768 if Intersect[2] < 0 and Intersect - C in self.support: 769 ListPointIntersection.append(Intersect) 770 771 return _IntersectionRayMirror(Ray.point, ListPointIntersection) 772 773 def get_local_normal(self, Point): 774 """Return the normal unit vector on the ellipsoidal surface at point Point.""" 775 Gradient = Vector(np.zeros(3)) 776 777 Gradient[0] = -Point[0] / self.a**2 778 Gradient[1] = -Point[1] / self.b**2 779 Gradient[2] = -Point[2] / self.b**2 780 781 return Gradient.normalized() 782 783 def _get_centre_ref(self): 784 """Return 3D coordinates of the point on the mirror surface at the center of its support.""" 785 foci = 2 * np.sqrt(self.a**2 - self.b**2) # distance between the foci 786 h = -foci / 2 / np.tan(self._offaxisangle) 787 R = np.sqrt(foci**2 / 4 + h**2) 788 sign = 1 789 if math.isclose(self._offaxisangle, np.pi / 2): 790 h = 0 791 elif self._offaxisangle > np.pi / 2: 792 h = -h 793 sign = -1 794 a = 1 - self.a**2 / self.b**2 795 b = -2 * h 796 c = self.a**2 + h**2 - R**2 797 z = (-b + sign * np.sqrt(b**2 - 4 * a * c)) / (2 * a) 798 if math.isclose(z**2, self.b**2): 799 return np.array([0, 0, -self.b]) 800 x = self.a * np.sqrt(1 - z**2 / self.b**2) 801 centre = Point([x, 0, sign * z]) 802 return centre 803 804 def _zfunc(self, PointArray): 805 x = PointArray[:,0] 806 y = PointArray[:,1] 807 x-= self.r0[0] 808 y-= self.r0[1] 809 z = -np.sqrt(1 - (x / self.a)**2 - (y / self.b)**2) 810 z += self.r0[2] 811 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)
668 def __init__( 669 self, 670 Support, 671 SemiMajorAxis=None, 672 SemiMinorAxis=None, 673 OffAxisAngle=None, 674 f_object=None, 675 f_image=None, 676 IncidenceAngle=None, 677 Magnification=None, 678 DistanceObjectImage=None, 679 Eccentricity=None, 680 **kwargs, 681 ): 682 """ 683 Generate an ellipsoidal mirror with given parameters. 684 685 The angles are given in degrees but converted to radians for internal calculations. 686 You can specify the semi-major and semi-minor axes, the off-axis angle, the object and image focal distances or some other parameters. 687 The constructor uses a magic calculator to determine the missing parameters. 688 689 Parameters 690 ---------- 691 Support : TYPE 692 ART.ModuleSupport.Support. 693 SemiMajorAxis : float (optional) 694 Semi major axis of the ellipsoid in mm.. 695 SemiMinorAxis : float (optional) 696 Semi minor axis of the ellipsoid in mm.. 697 OffAxisAngle : float (optional) 698 Off-axis angle of the mirror in mm. Defined as the angle at the centre of the mirror between the two foci.. 699 f_object : float (optional) 700 Object focal distance in mm. 701 f_image : float (optional) 702 Image focal distance in mm. 703 IncidenceAngle : float (optional) 704 Angle of incidence in degrees. 705 Magnification : float (optional) 706 Magnification. 707 DistanceObjectImage : float (optional) 708 Distance between object and image in mm. 709 Eccentricity : float (optional) 710 Eccentricity of the ellipsoid. 711 """ 712 super().__init__() 713 self.type = "Ellipsoidal Mirror" 714 self.support = Support 715 716 if "Surface" in kwargs: 717 self.Surface = kwargs["Surface"] 718 719 parameter_calculator = EllipsoidalMirrorCalculator() 720 values, steps = parameter_calculator.calculate_values(verify_consistency=True, 721 f1=f_object, 722 f2=f_image, 723 a=SemiMajorAxis, 724 b=SemiMinorAxis, 725 offset_angle=OffAxisAngle, 726 incidence_angle=IncidenceAngle, 727 m=Magnification, 728 l=DistanceObjectImage, 729 e=Eccentricity) 730 self.a = values["a"] 731 self.b = values["b"] 732 self._offaxisangle = np.deg2rad(values["offset_angle"]) 733 self._f_object = values["f1"] 734 self._f_image = values["f2"] 735 736 self.centre_ref = self._get_centre_ref() 737 738 self.r0 = self.centre_ref 739 740 self.support_normal_ref = Vector([0, 0, 1.0]) 741 self.majoraxis_ref = Vector([1.0, 0, 0]) 742 743 self.f1_ref = Point([-self.a, 0,0]) 744 self.f2_ref = Point([ self.a, 0,0]) 745 self.towards_image_ref = (self.f2_ref - self.centre_ref).normalized() 746 self.towards_object_ref = (self.f1_ref - self.centre_ref).normalized() 747 self.centre_normal_ref = self.get_local_normal(self.centre_ref) 748 749 self.add_global_points("f1", "f2", "centre") 750 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.
773 def get_local_normal(self, Point): 774 """Return the normal unit vector on the ellipsoidal surface at point Point.""" 775 Gradient = Vector(np.zeros(3)) 776 777 Gradient[0] = -Point[0] / self.a**2 778 Gradient[1] = -Point[1] / self.b**2 779 Gradient[2] = -Point[2] / self.b**2 780 781 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.
815class MirrorCylindrical(Mirror): 816 """ 817 Cylindrical mirror surface with eqn. $y^2 + z^2 = R^2$, where $R$ is the radius. 818 819 Attributes 820 ---------- 821 radius : float 822 Radius of curvature. A postive value for concave mirror, a negative value for a convex mirror. 823 824 support : ART.ModuleSupport.Support 825 826 type : str 'Cylindrical Mirror'. 827 828 Methods 829 ------- 830 MirrorCylindrical.get_normal(Point) 831 832 MirrorCylindrical.get_centre() 833 834 MirrorCylindrical.get_grid3D(NbPoints) 835 """ 836 837 def __init__(self, Support, Radius, **kwargs): 838 """ 839 Construct a cylindrical mirror. 840 841 Parameters 842 ---------- 843 Radius : float 844 The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror. 845 846 Support : ART.ModuleSupport.Support 847 848 """ 849 super().__init__() 850 if Radius < 0: 851 self.type = "CylindricalCX Mirror" 852 self.curvature = Curvature.CONVEX 853 self.radius = -Radius 854 else: 855 self.type = "CylindricalCC Mirror" 856 self.curvature = Curvature.CONCAVE 857 self.radius = Radius 858 859 self.support = Support 860 861 if "Surface" in kwargs: 862 self.Surface = kwargs["Surface"] 863 864 self.r0 = Point([0.0, 0.0, -self.radius]) 865 866 self.support_normal_ref = Vector([0, 0, 1.0]) 867 self.majoraxis_ref = Vector([1.0, 0, 0]) 868 self.centre_ref = Point([0.0, 0.0, -self.radius]) 869 870 self.add_global_points("centre") 871 self.add_global_vectors("support_normal", "majoraxis") 872 873 def _get_intersection(self, Ray): 874 """Return the intersection point between the Ray and the cylinder.""" 875 uy = Ray.vector[1] 876 uz = Ray.vector[2] 877 yA = Ray.point[1] 878 zA = Ray.point[2] 879 880 a = uy**2 + uz**2 881 b = 2 * (uy * yA + uz * zA) 882 c = yA**2 + zA**2 - self.radius**2 883 884 Solution = mgeo.SolverQuadratic(a, b, c) 885 Solution = mgeo.KeepPositiveSolution(Solution) 886 887 ListPointIntersection = [] 888 for t in Solution: 889 Intersect = Ray.vector * t + Ray.point 890 if Intersect[2] < 0 and Intersect in self.support: 891 ListPointIntersection.append(Intersect) 892 893 return _IntersectionRayMirror(Ray.point, ListPointIntersection) 894 895 def get_local_normal(self, Point): 896 """Return the normal unit vector on the cylinder surface at point P.""" 897 Gradient = Vector([0, -Point[1], -Point[2]]) 898 return Gradient.normalized() 899 900 def get_centre(self): 901 """Return 3D coordinates of the point on the mirror surface at the center of its support.""" 902 return Point([0, 0, -self.radius]) 903 904 def _zfunc(self, PointArray): 905 y = PointArray[:,1] 906 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)
837 def __init__(self, Support, Radius, **kwargs): 838 """ 839 Construct a cylindrical mirror. 840 841 Parameters 842 ---------- 843 Radius : float 844 The radius of curvature in mm. A postive value for concave mirror, a negative value for a convex mirror. 845 846 Support : ART.ModuleSupport.Support 847 848 """ 849 super().__init__() 850 if Radius < 0: 851 self.type = "CylindricalCX Mirror" 852 self.curvature = Curvature.CONVEX 853 self.radius = -Radius 854 else: 855 self.type = "CylindricalCC Mirror" 856 self.curvature = Curvature.CONCAVE 857 self.radius = Radius 858 859 self.support = Support 860 861 if "Surface" in kwargs: 862 self.Surface = kwargs["Surface"] 863 864 self.r0 = Point([0.0, 0.0, -self.radius]) 865 866 self.support_normal_ref = Vector([0, 0, 1.0]) 867 self.majoraxis_ref = Vector([1.0, 0, 0]) 868 self.centre_ref = Point([0.0, 0.0, -self.radius]) 869 870 self.add_global_points("centre") 871 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
895 def get_local_normal(self, Point): 896 """Return the normal unit vector on the cylinder surface at point P.""" 897 Gradient = Vector([0, -Point[1], -Point[2]]) 898 return Gradient.normalized()
Return the normal unit vector on the cylinder surface at point P.
900 def get_centre(self): 901 """Return 3D coordinates of the point on the mirror surface at the center of its support.""" 902 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.
913class GrazingParabola(Mirror): 914 r""" 915 A parabolic mirror with a support parallel to the surface at the optical center. 916 Needs to be completed, currently not functional. 917 TODO 918 """ 919 920 def __init__(self, Support, 921 FocalEffective: float=None, 922 OffAxisAngle: float = None, 923 FocalParent: float = None, 924 RadiusParent: float = None, 925 OffAxisDistance: float = None, 926 MoreThan90: bool = None, 927 **kwargs): 928 """ 929 Initialise a Parabolic mirror. 930 931 Parameters 932 ---------- 933 FocalEffective : float 934 Effective focal length of the parabola in mm. 935 936 OffAxisAngle : float 937 Off-axis angle *in degrees* of the parabola. 938 939 Support : ART.ModuleSupport.Support 940 941 """ 942 super().__init__() 943 self.curvature = Curvature.CONCAVE 944 self.support = Support 945 self.type = "Grazing Parabolic Mirror" 946 947 if "Surface" in kwargs: 948 self.Surface = kwargs["Surface"] 949 950 parameter_calculator = OAP_calculator() 951 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 952 fs=FocalEffective, 953 theta=OffAxisAngle, 954 fp=FocalParent, 955 Rc=RadiusParent, 956 OAD=OffAxisDistance, 957 more_than_90=MoreThan90) 958 self._offaxisangle = values["theta"] 959 self._feff = values["fs"] 960 self._p = values["p"] 961 self._offaxisdistance = values["OAD"] 962 self._fparent = values["fp"] 963 self._rparent = values["Rc"] 964 965 #self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 966 self.r0 = Point([0, 0, 0]) 967 968 self.centre_ref = Point([0, 0, 0]) 969 self.support_normal_ref = Vector([0, 0, 1.0]) 970 self.majoraxis_ref = Vector([1.0, 0, 0]) 971 972 focus_ref = Point([np.sqrt(self._feff**2 - self._offaxisdistance**2), 0, self._offaxisdistance]) # frame where x is as collimated direction 973 # We need to rotate it in the trigonometric direction by the 90°-theta/2 974 angle = np.pi/2 - self._offaxisangle/2 975 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), ]) 976 977 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 978 towards_collimated_ref = Vector([-1.0, 0, 0]) # Again, we need to rotate it by the same angle 979 self.collimated_ref = Point([-np.cos(angle), 0, np.sin(angle)]) 980 981 self.add_global_points("focus", "centre") 982 self.add_global_vectors("towards_focusing", "towards_collimated", "support_normal", "majoraxis") 983 984 def _get_intersection(self, Ray): 985 """ 986 Return the intersection point between the ray and the parabola. 987 """ 988 pass 989 990 def get_local_normal(self, Point): 991 """ 992 Return the normal unit vector on the paraboloid surface at point Point. 993 """ 994 pass 995 996 def _zfunc(self, PointArray): 997 pass
A parabolic mirror with a support parallel to the surface at the optical center. Needs to be completed, currently not functional. TODO
920 def __init__(self, Support, 921 FocalEffective: float=None, 922 OffAxisAngle: float = None, 923 FocalParent: float = None, 924 RadiusParent: float = None, 925 OffAxisDistance: float = None, 926 MoreThan90: bool = None, 927 **kwargs): 928 """ 929 Initialise a Parabolic mirror. 930 931 Parameters 932 ---------- 933 FocalEffective : float 934 Effective focal length of the parabola in mm. 935 936 OffAxisAngle : float 937 Off-axis angle *in degrees* of the parabola. 938 939 Support : ART.ModuleSupport.Support 940 941 """ 942 super().__init__() 943 self.curvature = Curvature.CONCAVE 944 self.support = Support 945 self.type = "Grazing Parabolic Mirror" 946 947 if "Surface" in kwargs: 948 self.Surface = kwargs["Surface"] 949 950 parameter_calculator = OAP_calculator() 951 values,steps = parameter_calculator.calculate_values(verify_consistency=True, 952 fs=FocalEffective, 953 theta=OffAxisAngle, 954 fp=FocalParent, 955 Rc=RadiusParent, 956 OAD=OffAxisDistance, 957 more_than_90=MoreThan90) 958 self._offaxisangle = values["theta"] 959 self._feff = values["fs"] 960 self._p = values["p"] 961 self._offaxisdistance = values["OAD"] 962 self._fparent = values["fp"] 963 self._rparent = values["Rc"] 964 965 #self.r0 = Point([self._offaxisdistance, 0, self._offaxisdistance**2 / 2 / self._p]) 966 self.r0 = Point([0, 0, 0]) 967 968 self.centre_ref = Point([0, 0, 0]) 969 self.support_normal_ref = Vector([0, 0, 1.0]) 970 self.majoraxis_ref = Vector([1.0, 0, 0]) 971 972 focus_ref = Point([np.sqrt(self._feff**2 - self._offaxisdistance**2), 0, self._offaxisdistance]) # frame where x is as collimated direction 973 # We need to rotate it in the trigonometric direction by the 90°-theta/2 974 angle = np.pi/2 - self._offaxisangle/2 975 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), ]) 976 977 self.towards_focusing_ref = (self.focus_ref-self.centre_ref).normalized() 978 towards_collimated_ref = Vector([-1.0, 0, 0]) # Again, we need to rotate it by the same angle 979 self.collimated_ref = Point([-np.cos(angle), 0, np.sin(angle)]) 980 981 self.add_global_points("focus", "centre") 982 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
990 def get_local_normal(self, Point): 991 """ 992 Return the normal unit vector on the paraboloid surface at point Point. 993 """ 994 pass
Return the normal unit vector on the paraboloid surface at point Point.
1033def ReflectionMirrorRayList(Mirror, ListRay, IgnoreDefects=False): 1034 """ 1035 Return the the reflected rays according to the law of reflection for the list of incident rays ListRay. 1036 1037 Rays that do not hit the support are not further propagated. 1038 1039 Updates the reflected rays' incidence angle and path. 1040 1041 Parameters 1042 ---------- 1043 Mirror : Mirror-object 1044 1045 ListRay : list[Ray-object] 1046 1047 """ 1048 Deformed = type(Mirror) == DeformedMirror 1049 ListRayReflected = [] 1050 for k in ListRay: 1051 PointMirror = Mirror._get_intersection(k) 1052 1053 if PointMirror is not None: 1054 if Deformed and IgnoreDefects: 1055 M = Mirror.Mirror 1056 else: 1057 M = Mirror 1058 RayReflected = _ReflectionMirrorRay(M, PointMirror, k) 1059 ListRayReflected.append(RayReflected) 1060 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]
1064class DeformedMirror(Mirror): 1065 def __init__(self, Mirror, DeformationList): 1066 self.Mirror = Mirror 1067 self.DeformationList = DeformationList 1068 self.type = Mirror.type 1069 self.support = self.Mirror.support 1070 1071 def get_local_normal(self, PointMirror): 1072 base_normal = self.Mirror.get_normal(PointMirror) 1073 C = self.get_centre() 1074 defects_normals = [ 1075 d.get_normal(PointMirror - C) for d in self.DeformationList 1076 ] 1077 for i in defects_normals: 1078 base_normal = mgeo.normal_add(base_normal, i) 1079 base_normal /= np.linalg.norm(base_normal) 1080 return base_normal 1081 1082 def get_centre(self): 1083 return self.Mirror.get_centre() 1084 1085 def get_grid3D(self, NbPoint, **kwargs): 1086 return self.Mirror.get_grid3D(NbPoint, **kwargs) 1087 1088 def _get_intersection(self, Ray): 1089 Intersect = self.Mirror._get_intersection(Ray) 1090 if Intersect is not None: 1091 h = sum( 1092 D.get_offset(Intersect - self.get_centre()) 1093 for D in self.DeformationList 1094 ) 1095 alpha = mgeo.AngleBetweenTwoVectors( 1096 -Ray.vector, self.Mirror.get_normal(Intersect) 1097 ) 1098 Intersect -= Ray.vector * h / np.cos(alpha) 1099 return Intersect
Abstract base class for mirrors.
1071 def get_local_normal(self, PointMirror): 1072 base_normal = self.Mirror.get_normal(PointMirror) 1073 C = self.get_centre() 1074 defects_normals = [ 1075 d.get_normal(PointMirror - C) for d in self.DeformationList 1076 ] 1077 for i in defects_normals: 1078 base_normal = mgeo.normal_add(base_normal, i) 1079 base_normal /= np.linalg.norm(base_normal) 1080 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 20from ARTcore.ModuleGeometry import Point, Vector, Origin 21import ARTcore.ModuleOpticalRay as mray 22import ARTcore.ModuleOpticalElement as moe 23import ARTcore.ModuleDetector as mdet 24import ARTcore.ModuleSource as msource 25 26logger = logging.getLogger(__name__) 27 28# %% 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 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 quickshow() 68 69 render() 70 71 ---------- 72 73 shift_source(axis, distance) 74 75 tilt_source(self, axis, angle) 76 77 ---------- 78 79 rotate_OE(OEindx, axis, angle) 80 81 shift_OE(OEindx, axis, distance) 82 83 """ 84 85 def __init__(self, source_rays, optical_elements, detectors, description=""): 86 """ 87 Parameters 88 ---------- 89 source_rays : list[mray.Ray] 90 List of source rays, which are to be traced. 91 92 optical_elements : list[moe.OpticalElement] 93 List of successive optical elements. 94 95 detector: mdet.Detector (optional) 96 The detector (or list of detectors) to analyse the results. 97 98 description : str, optional 99 A string to describe the optical setup. Defaults to ''. 100 101 """ 102 self.source_rays = copy.deepcopy(source_rays) 103 # deepcopy so this object doesn't get changed when the global source_rays changes "outside" 104 self.optical_elements = copy.deepcopy(optical_elements) 105 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 106 self.detectors = detectors 107 if isinstance(detectors, mdet.Detector): 108 self.detectors = {"Focus": detectors} 109 self.description = description 110 self._output_rays = None 111 self._last_source_rays_hash = None 112 self._last_optical_elements_hash = None 113 114 def __repr__(self): 115 pretty_str = "Optical setup [OpticalChain]:\n" 116 pretty_str += f" - Description: {self.description}\n" if self.description else "Description: Not provided.\n" 117 pretty_str += " - Contains the following elements:\n" 118 pretty_str += f" - Source with {len(self.source_rays)} rays at coordinate origin\n" 119 prev_pos = np.zeros(3) 120 for i, element in enumerate(self.optical_elements): 121 dist = (element.position - prev_pos).norm 122 if i == 0: 123 prev = "source" 124 else: 125 prev = f"element {i-1}" 126 pretty_str += f" - Element {i}: {element.type} at distance {round(dist)} from {prev}\n" 127 prev_pos = element.position 128 for i in self.detectors.keys(): 129 detector = self.detectors[i] 130 pretty_str += f' - Detector "{i}": {detector.type} at distance {round(detector.distance,3)} from element {detector.index % len(self.optical_elements)}\n' 131 return pretty_str 132 133 def __getitem__(self, i): 134 return self.optical_elements[i] 135 136 def __len__(self): 137 return len(self.optical_elements) 138 139 @property 140 def source_rays(self): 141 return self._source_rays 142 143 @source_rays.setter 144 def source_rays(self, source_rays): 145 if type(source_rays) == mray.RayList: 146 self._source_rays = source_rays 147 else: 148 raise TypeError("Source_rays must be a RayList object.") 149 150 @property 151 def optical_elements(self): 152 return self._optical_elements 153 154 @optical_elements.setter 155 def optical_elements(self, optical_elements): 156 if type(optical_elements) == list and all(isinstance(x, moe.OpticalElement) for x in optical_elements): 157 self._optical_elements = optical_elements 158 else: 159 raise TypeError("Optical_elements must be list of OpticalElement-objects.") 160 161 # %% METHODS ################################################################## 162 163 def __copy__(self): 164 """Return another optical chain with the same source, optical elements and description-string as this one.""" 165 return OpticalChain(self.source_rays, self.optical_elements, self.detectors, self.description) 166 167 def __deepcopy__(self, memo): 168 """Return another optical chain with the same source, optical elements and description-string as this one.""" 169 return OpticalChain(copy.deepcopy(self.source_rays), copy.deepcopy(self.optical_elements), memo+"\n"+copy.copy(self.description)) 170 171 def get_input_rays(self): 172 """ 173 Returns the list of source rays. 174 """ 175 return self.source_rays 176 177 def get_output_rays(self, force=False, **kwargs): 178 """ 179 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 180 or if the source-ray-bundle or anything about the optical elements has changed. 181 182 This is the user-facing method to perform the ray-tracing calculation. 183 """ 184 current_source_rays_hash = hash(self.source_rays) 185 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 186 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 187 print("...ray-tracing...", end="", flush=True) 188 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 189 print( 190 "\r\033[K", end="", flush=True 191 ) # move to beginning of the line with \r and then delete the whole line with \033[K 192 self._last_source_rays_hash = current_source_rays_hash 193 self._last_optical_elements_hash = current_optical_elements_hash 194 195 return self._output_rays 196 197 def get2dPoints(self, Detector = "Focus"): 198 """ 199 Returns the 2D points of the detected rays on the detector. 200 """ 201 if isinstance(Detector, str): 202 if Detector in self.detectors.keys(): 203 Detector = self.detectors[Detector] 204 else: 205 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 206 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0] 207 208 def get3dPoints(self, Detector = "Focus"): 209 """ 210 Returns the 3D points of the detected rays on the detector. 211 """ 212 if isinstance(Detector, str): 213 if Detector in self.detectors.keys(): 214 Detector = self.detectors[Detector] 215 else: 216 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 217 return Detector.get_3D_points(self.get_output_rays()[Detector.index]) 218 219 def getDelays(self, Detector = "Focus"): 220 """ 221 Returns the delays of the detected rays on the detector. 222 """ 223 if isinstance(Detector, str): 224 if Detector in self.detectors.keys(): 225 Detector = self.detectors[Detector] 226 else: 227 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 228 return Detector.get_Delays(self.get_output_rays()[Detector.index]) 229 230 # %% methods to (mis-)align the optical chain; just uses the corresponding methods of the OpticalElement class... 231 232 def shift_source(self, axis: str|np.ndarray, distance: float): 233 """ 234 Shift source ray bundle by distance (in mm) along the 'axis' specified as 235 a lab-frame vector (numpy-array of length 3) or as one of the strings 236 "vert", "horiz", or "random". 237 238 In the latter case, the reference is the incidence plane of the first 239 non-normal-incidence mirror after the source. If there is none, you will 240 be asked to rather specify the axis as a 3D-numpy-array. 241 242 axis = "vert" means the source position is shifted along the axis perpendicular 243 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 244 245 axis = "horiz" means the source direciton is translated along thr axis in that 246 incidence plane and perpendicular to the current source direction, 247 i.e. "horizontally" in the incidence plane, but retaining the same distance 248 of source and first optical element. 249 250 axis = "random" means the the source direction shifted in a random direction 251 within in the plane perpendicular to the current source direction, 252 e.g. simulating a fluctuation of hte transverse source position. 253 254 Parameters 255 ---------- 256 axis : np.ndarray or str 257 Shift axis, specified either as a 3D lab-frame vector or as one 258 of the strings "vert", "horiz", or "random". 259 260 distance : float 261 Shift distance in mm. 262 263 Returns 264 ------- 265 Nothing, just modifies the property 'source_rays'. 266 """ 267 if type(distance) not in [int, float, np.float64]: 268 raise ValueError('The "distance"-argument must be an int or float number.') 269 270 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 271 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 272 273 OEnormal = None 274 for i in mirror_indcs: 275 ith_OEnormal = self.optical_elements[i].normal 276 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 277 OEnormal = ith_OEnormal 278 break 279 if OEnormal is None: 280 raise Exception( 281 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 282 so you should rather give 'axis' as a numpy-array of length 3." 283 ) 284 285 if type(axis) == np.ndarray and len(axis) == 3: 286 translation_vector = axis 287 else: 288 perp_axis = np.cross(central_ray_vector, OEnormal) 289 horiz_axis = np.cross(perp_axis, central_ray_vector) 290 291 if axis == "vert": 292 translation_vector = perp_axis 293 elif axis == "horiz": 294 translation_vector = horiz_axis 295 elif axis == "random": 296 translation_vector = ( 297 np.random.uniform(low=-1, high=1, size=1) * perp_axis 298 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 299 ) 300 else: 301 raise ValueError( 302 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 303 ) 304 305 self.source_rays = mgeo.TranslationRayList(self.source_rays, distance * mgeo.Normalize(translation_vector)) 306 307 def tilt_source(self, axis: str|np.ndarray, angle: float): 308 """ 309 Rotate source ray bundle by angle around an axis, specified as 310 a lab-frame vector (numpy-array of length 3) or as one of the strings 311 "in_plane", "out_plane" or "random" direction. 312 313 In the latter case, the function considers the incidence plane of the first 314 non-normal-incidence mirror after the source. If there is none, you will 315 be asked to rather specify the axis as a 3D-numpy-array. 316 317 axis = "in_plane" means the source direction is rotated about an axis 318 perpendicular to that incidence plane, which tilts the source 319 "horizontally" in the same plane. 320 321 axis = "out_plane" means the source direciton is rotated about an axis 322 in that incidence plane and perpendicular to the current source direction, 323 which tilts the source "vertically" out of the former incidence plane. 324 325 axis = "random" means the the source direction is tilted in a random direction, 326 e.g. simulating a beam pointing fluctuation. 327 328 Attention, "angle" is given in deg, so as to remain consitent with the 329 conventions of other functions, although pointing is mostly talked about 330 in mrad instead. 331 332 Parameters 333 ---------- 334 axis : np.ndarray or str 335 Shift axis, specified either as a 3D lab-frame vector or as one 336 of the strings "in_plane", "out_plane", or "random". 337 338 angle : float 339 Rotation angle in degree. 340 341 Returns 342 ------- 343 Nothing, just modifies the property 'source_rays'. 344 """ 345 if type(angle) not in [int, float, np.float64]: 346 raise ValueError('The "angle"-argument must be an int or float number.') 347 348 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 349 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 350 351 OEnormal = None 352 for i in mirror_indcs: 353 ith_OEnormal = self.optical_elements[i].normal 354 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 355 OEnormal = ith_OEnormal 356 break 357 if OEnormal is None: 358 raise Exception( 359 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 360 so you should rather give 'axis' as a numpy-array of length 3." 361 ) 362 363 if type(axis) == np.ndarray and len(axis) == 3: 364 rot_axis = axis 365 else: 366 rot_axis_in = np.cross(central_ray_vector, OEnormal) 367 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 368 if axis == "in_plane": 369 rot_axis = rot_axis_in 370 elif axis == "out_plane": 371 rot_axis = rot_axis_out 372 elif axis == "random": 373 rot_axis = ( 374 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 375 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 376 ) 377 else: 378 raise ValueError( 379 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 380 ) 381 382 self.source_rays = mgeo.RotationAroundAxisRayList(self.source_rays, rot_axis, np.deg2rad(angle)) 383 384 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 385 """ 386 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 387 OEstart and OEstop (both included). 388 """ 389 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 390 outray = self.get_output_rays()[OEstart-1][0] 391 print(outray) 392 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 393 self.optical_elements[OEstart:OEstop] = new_elements 394 395 # %% 396 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 397 """ 398 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 399 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 400 that takes either of the two values: 401 - "in" 402 - "out" 403 In either case the "axis" can take these values: 404 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 405 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 406 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 407 408 Parameters 409 ---------- 410 OEindx : int 411 Index of the optical element to modify out of OpticalChain.optical_elements. 412 413 ref : str 414 Reference ray used to define the axes of rotation. Can be either: 415 "in" or "out" or "local_normal" (in+out) 416 417 axis : str 418 Rotation axis, specified as one of the strings 419 "pitch", "roll", "yaw" 420 421 angle : float 422 Rotation angle in degree. 423 424 Returns 425 ------- 426 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 427 """ 428 if abs(OEindx) > len(self.optical_elements): 429 raise ValueError( 430 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 431 ) 432 if type(angle) not in [int, float, np.float64]: 433 raise ValueError('The "angle"-argument must be an int or float number.') 434 MasterRay = [mp.FindCentralRay(self.source_rays)] 435 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 436 TracedRay = [MasterRay] + TracedRay 437 match ref: 438 case "out": 439 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 440 case "in": 441 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 442 case "localnormal": 443 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 444 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 445 RefVec = mgeo.Normalize(Out-In) 446 case _: 447 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 448 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 449 match axis: 450 case "pitch": 451 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 452 case "roll": 453 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 454 case "yaw": 455 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 456 case _: 457 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 458 else: 459 #If the normal vector is aligned with the ray 460 match axis: 461 case "pitch": 462 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 463 case "roll": 464 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 465 case "yaw": 466 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 467 case _: 468 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 469 470 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 471 """ 472 Shift the optical element OpticalChain.optical_elements[OEindx] along 473 axis referenced to the master ray. 474 475 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 476 that takes either of the two values: 477 - "in" 478 - "out" 479 In either case the "axis" can take these values: 480 - "along": Translation along the master ray. 481 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 482 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 483 484 Parameters 485 ---------- 486 OEindx : int 487 Index of the optical element to modify out of OpticalChain.optical_elements. 488 489 ref : str 490 Reference ray used to define the axes of rotation. Can be either: 491 "in" or "out". 492 493 axis : str 494 Translation axis, specified as one of the strings 495 "along", "in_plane", "out_plane". 496 497 distance : float 498 Rotation angle in degree. 499 500 Returns 501 ------- 502 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 503 """ 504 if abs(OEindx) >= len(self.optical_elements): 505 raise ValueError( 506 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 507 ) 508 if type(distance) not in [int, float, np.float64]: 509 raise ValueError('The "dist"-argument must be an int or float number.') 510 511 MasterRay = [mp.FindCentralRay(self.source_rays)] 512 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 513 TracedRay = [MasterRay] + TracedRay 514 match ref: 515 case "out": 516 RefVec = TracedRay[OEindx+1][0].vector 517 case "in": 518 RefVec = TracedRay[OEindx][0].vector 519 case _: 520 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 521 match axis: 522 case "along": 523 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 524 case "in_plane": 525 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 526 case "out_plane": 527 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 528 case _: 529 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 530 531 # some function that randomly misalings one, or several or all ?
30class OpticalChain: 31 """ 32 The OpticalChain represents the whole optical setup to be simulated: 33 Its main attributes are a list source-[Rays](ModuleOpticalRay.html) and 34 a list of successive [OpticalElements](ModuleOpticalElement.html). 35 36 The method OpticalChain.get_output_rays() returns an associated list of lists of 37 [Rays](ModuleOpticalRay.html), each calculated by ray-tracing from one 38 [OpticalElement](ModuleOpticalElement.html) to the next. 39 So OpticalChain.get_output_rays()[i] is the bundle of [Rays](ModuleOpticalRay.html) *after* 40 optical_elements[i]. 41 42 The string "description" can contain a short description of the optical setup, or similar notes. 43 44 The OpticalChain can be visualized quickly with the method OpticalChain.quickshow(), 45 and more nicely with OpticalChain.render(). 46 47 The class also provides methods for (mis-)alignment of the source-[Ray](ModuleOpticalRay.html)-bundle and the 48 [OpticalElements](ModuleOpticalElement.html). 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 quickshow() 69 70 render() 71 72 ---------- 73 74 shift_source(axis, distance) 75 76 tilt_source(self, axis, angle) 77 78 ---------- 79 80 rotate_OE(OEindx, axis, angle) 81 82 shift_OE(OEindx, axis, distance) 83 84 """ 85 86 def __init__(self, source_rays, optical_elements, detectors, description=""): 87 """ 88 Parameters 89 ---------- 90 source_rays : list[mray.Ray] 91 List of source rays, which are to be traced. 92 93 optical_elements : list[moe.OpticalElement] 94 List of successive optical elements. 95 96 detector: mdet.Detector (optional) 97 The detector (or list of detectors) to analyse the results. 98 99 description : str, optional 100 A string to describe the optical setup. Defaults to ''. 101 102 """ 103 self.source_rays = copy.deepcopy(source_rays) 104 # deepcopy so this object doesn't get changed when the global source_rays changes "outside" 105 self.optical_elements = copy.deepcopy(optical_elements) 106 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 107 self.detectors = detectors 108 if isinstance(detectors, mdet.Detector): 109 self.detectors = {"Focus": detectors} 110 self.description = description 111 self._output_rays = None 112 self._last_source_rays_hash = None 113 self._last_optical_elements_hash = None 114 115 def __repr__(self): 116 pretty_str = "Optical setup [OpticalChain]:\n" 117 pretty_str += f" - Description: {self.description}\n" if self.description else "Description: Not provided.\n" 118 pretty_str += " - Contains the following elements:\n" 119 pretty_str += f" - Source with {len(self.source_rays)} rays at coordinate origin\n" 120 prev_pos = np.zeros(3) 121 for i, element in enumerate(self.optical_elements): 122 dist = (element.position - prev_pos).norm 123 if i == 0: 124 prev = "source" 125 else: 126 prev = f"element {i-1}" 127 pretty_str += f" - Element {i}: {element.type} at distance {round(dist)} from {prev}\n" 128 prev_pos = element.position 129 for i in self.detectors.keys(): 130 detector = self.detectors[i] 131 pretty_str += f' - Detector "{i}": {detector.type} at distance {round(detector.distance,3)} from element {detector.index % len(self.optical_elements)}\n' 132 return pretty_str 133 134 def __getitem__(self, i): 135 return self.optical_elements[i] 136 137 def __len__(self): 138 return len(self.optical_elements) 139 140 @property 141 def source_rays(self): 142 return self._source_rays 143 144 @source_rays.setter 145 def source_rays(self, source_rays): 146 if type(source_rays) == mray.RayList: 147 self._source_rays = source_rays 148 else: 149 raise TypeError("Source_rays must be a RayList object.") 150 151 @property 152 def optical_elements(self): 153 return self._optical_elements 154 155 @optical_elements.setter 156 def optical_elements(self, optical_elements): 157 if type(optical_elements) == list and all(isinstance(x, moe.OpticalElement) for x in optical_elements): 158 self._optical_elements = optical_elements 159 else: 160 raise TypeError("Optical_elements must be list of OpticalElement-objects.") 161 162 # %% METHODS ################################################################## 163 164 def __copy__(self): 165 """Return another optical chain with the same source, optical elements and description-string as this one.""" 166 return OpticalChain(self.source_rays, self.optical_elements, self.detectors, self.description) 167 168 def __deepcopy__(self, memo): 169 """Return another optical chain with the same source, optical elements and description-string as this one.""" 170 return OpticalChain(copy.deepcopy(self.source_rays), copy.deepcopy(self.optical_elements), memo+"\n"+copy.copy(self.description)) 171 172 def get_input_rays(self): 173 """ 174 Returns the list of source rays. 175 """ 176 return self.source_rays 177 178 def get_output_rays(self, force=False, **kwargs): 179 """ 180 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 181 or if the source-ray-bundle or anything about the optical elements has changed. 182 183 This is the user-facing method to perform the ray-tracing calculation. 184 """ 185 current_source_rays_hash = hash(self.source_rays) 186 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 187 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 188 print("...ray-tracing...", end="", flush=True) 189 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 190 print( 191 "\r\033[K", end="", flush=True 192 ) # move to beginning of the line with \r and then delete the whole line with \033[K 193 self._last_source_rays_hash = current_source_rays_hash 194 self._last_optical_elements_hash = current_optical_elements_hash 195 196 return self._output_rays 197 198 def get2dPoints(self, Detector = "Focus"): 199 """ 200 Returns the 2D points of the detected rays on the detector. 201 """ 202 if isinstance(Detector, str): 203 if Detector in self.detectors.keys(): 204 Detector = self.detectors[Detector] 205 else: 206 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 207 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0] 208 209 def get3dPoints(self, Detector = "Focus"): 210 """ 211 Returns the 3D points of the detected rays on the detector. 212 """ 213 if isinstance(Detector, str): 214 if Detector in self.detectors.keys(): 215 Detector = self.detectors[Detector] 216 else: 217 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 218 return Detector.get_3D_points(self.get_output_rays()[Detector.index]) 219 220 def getDelays(self, Detector = "Focus"): 221 """ 222 Returns the delays of the detected rays on the detector. 223 """ 224 if isinstance(Detector, str): 225 if Detector in self.detectors.keys(): 226 Detector = self.detectors[Detector] 227 else: 228 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 229 return Detector.get_Delays(self.get_output_rays()[Detector.index]) 230 231 # %% methods to (mis-)align the optical chain; just uses the corresponding methods of the OpticalElement class... 232 233 def shift_source(self, axis: str|np.ndarray, distance: float): 234 """ 235 Shift source ray bundle by distance (in mm) along the 'axis' specified as 236 a lab-frame vector (numpy-array of length 3) or as one of the strings 237 "vert", "horiz", or "random". 238 239 In the latter case, the reference is the incidence plane of the first 240 non-normal-incidence mirror after the source. If there is none, you will 241 be asked to rather specify the axis as a 3D-numpy-array. 242 243 axis = "vert" means the source position is shifted along the axis perpendicular 244 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 245 246 axis = "horiz" means the source direciton is translated along thr axis in that 247 incidence plane and perpendicular to the current source direction, 248 i.e. "horizontally" in the incidence plane, but retaining the same distance 249 of source and first optical element. 250 251 axis = "random" means the the source direction shifted in a random direction 252 within in the plane perpendicular to the current source direction, 253 e.g. simulating a fluctuation of hte transverse source position. 254 255 Parameters 256 ---------- 257 axis : np.ndarray or str 258 Shift axis, specified either as a 3D lab-frame vector or as one 259 of the strings "vert", "horiz", or "random". 260 261 distance : float 262 Shift distance in mm. 263 264 Returns 265 ------- 266 Nothing, just modifies the property 'source_rays'. 267 """ 268 if type(distance) not in [int, float, np.float64]: 269 raise ValueError('The "distance"-argument must be an int or float number.') 270 271 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 272 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 273 274 OEnormal = None 275 for i in mirror_indcs: 276 ith_OEnormal = self.optical_elements[i].normal 277 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 278 OEnormal = ith_OEnormal 279 break 280 if OEnormal is None: 281 raise Exception( 282 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 283 so you should rather give 'axis' as a numpy-array of length 3." 284 ) 285 286 if type(axis) == np.ndarray and len(axis) == 3: 287 translation_vector = axis 288 else: 289 perp_axis = np.cross(central_ray_vector, OEnormal) 290 horiz_axis = np.cross(perp_axis, central_ray_vector) 291 292 if axis == "vert": 293 translation_vector = perp_axis 294 elif axis == "horiz": 295 translation_vector = horiz_axis 296 elif axis == "random": 297 translation_vector = ( 298 np.random.uniform(low=-1, high=1, size=1) * perp_axis 299 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 300 ) 301 else: 302 raise ValueError( 303 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 304 ) 305 306 self.source_rays = mgeo.TranslationRayList(self.source_rays, distance * mgeo.Normalize(translation_vector)) 307 308 def tilt_source(self, axis: str|np.ndarray, angle: float): 309 """ 310 Rotate source ray bundle by angle around an axis, specified as 311 a lab-frame vector (numpy-array of length 3) or as one of the strings 312 "in_plane", "out_plane" or "random" direction. 313 314 In the latter case, the function considers the incidence plane of the first 315 non-normal-incidence mirror after the source. If there is none, you will 316 be asked to rather specify the axis as a 3D-numpy-array. 317 318 axis = "in_plane" means the source direction is rotated about an axis 319 perpendicular to that incidence plane, which tilts the source 320 "horizontally" in the same plane. 321 322 axis = "out_plane" means the source direciton is rotated about an axis 323 in that incidence plane and perpendicular to the current source direction, 324 which tilts the source "vertically" out of the former incidence plane. 325 326 axis = "random" means the the source direction is tilted in a random direction, 327 e.g. simulating a beam pointing fluctuation. 328 329 Attention, "angle" is given in deg, so as to remain consitent with the 330 conventions of other functions, although pointing is mostly talked about 331 in mrad instead. 332 333 Parameters 334 ---------- 335 axis : np.ndarray or str 336 Shift axis, specified either as a 3D lab-frame vector or as one 337 of the strings "in_plane", "out_plane", or "random". 338 339 angle : float 340 Rotation angle in degree. 341 342 Returns 343 ------- 344 Nothing, just modifies the property 'source_rays'. 345 """ 346 if type(angle) not in [int, float, np.float64]: 347 raise ValueError('The "angle"-argument must be an int or float number.') 348 349 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 350 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 351 352 OEnormal = None 353 for i in mirror_indcs: 354 ith_OEnormal = self.optical_elements[i].normal 355 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 356 OEnormal = ith_OEnormal 357 break 358 if OEnormal is None: 359 raise Exception( 360 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 361 so you should rather give 'axis' as a numpy-array of length 3." 362 ) 363 364 if type(axis) == np.ndarray and len(axis) == 3: 365 rot_axis = axis 366 else: 367 rot_axis_in = np.cross(central_ray_vector, OEnormal) 368 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 369 if axis == "in_plane": 370 rot_axis = rot_axis_in 371 elif axis == "out_plane": 372 rot_axis = rot_axis_out 373 elif axis == "random": 374 rot_axis = ( 375 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 376 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 377 ) 378 else: 379 raise ValueError( 380 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 381 ) 382 383 self.source_rays = mgeo.RotationAroundAxisRayList(self.source_rays, rot_axis, np.deg2rad(angle)) 384 385 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 386 """ 387 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 388 OEstart and OEstop (both included). 389 """ 390 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 391 outray = self.get_output_rays()[OEstart-1][0] 392 print(outray) 393 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 394 self.optical_elements[OEstart:OEstop] = new_elements 395 396 # %% 397 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 398 """ 399 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 400 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 401 that takes either of the two values: 402 - "in" 403 - "out" 404 In either case the "axis" can take these values: 405 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 406 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 407 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 408 409 Parameters 410 ---------- 411 OEindx : int 412 Index of the optical element to modify out of OpticalChain.optical_elements. 413 414 ref : str 415 Reference ray used to define the axes of rotation. Can be either: 416 "in" or "out" or "local_normal" (in+out) 417 418 axis : str 419 Rotation axis, specified as one of the strings 420 "pitch", "roll", "yaw" 421 422 angle : float 423 Rotation angle in degree. 424 425 Returns 426 ------- 427 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 428 """ 429 if abs(OEindx) > len(self.optical_elements): 430 raise ValueError( 431 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 432 ) 433 if type(angle) not in [int, float, np.float64]: 434 raise ValueError('The "angle"-argument must be an int or float number.') 435 MasterRay = [mp.FindCentralRay(self.source_rays)] 436 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 437 TracedRay = [MasterRay] + TracedRay 438 match ref: 439 case "out": 440 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 441 case "in": 442 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 443 case "localnormal": 444 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 445 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 446 RefVec = mgeo.Normalize(Out-In) 447 case _: 448 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 449 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 450 match axis: 451 case "pitch": 452 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 453 case "roll": 454 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 455 case "yaw": 456 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 457 case _: 458 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 459 else: 460 #If the normal vector is aligned with the ray 461 match axis: 462 case "pitch": 463 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 464 case "roll": 465 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 466 case "yaw": 467 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 468 case _: 469 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 470 471 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 472 """ 473 Shift the optical element OpticalChain.optical_elements[OEindx] along 474 axis referenced to the master ray. 475 476 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 477 that takes either of the two values: 478 - "in" 479 - "out" 480 In either case the "axis" can take these values: 481 - "along": Translation along the master ray. 482 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 483 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 484 485 Parameters 486 ---------- 487 OEindx : int 488 Index of the optical element to modify out of OpticalChain.optical_elements. 489 490 ref : str 491 Reference ray used to define the axes of rotation. Can be either: 492 "in" or "out". 493 494 axis : str 495 Translation axis, specified as one of the strings 496 "along", "in_plane", "out_plane". 497 498 distance : float 499 Rotation angle in degree. 500 501 Returns 502 ------- 503 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 504 """ 505 if abs(OEindx) >= len(self.optical_elements): 506 raise ValueError( 507 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 508 ) 509 if type(distance) not in [int, float, np.float64]: 510 raise ValueError('The "dist"-argument must be an int or float number.') 511 512 MasterRay = [mp.FindCentralRay(self.source_rays)] 513 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 514 TracedRay = [MasterRay] + TracedRay 515 match ref: 516 case "out": 517 RefVec = TracedRay[OEindx+1][0].vector 518 case "in": 519 RefVec = TracedRay[OEindx][0].vector 520 case _: 521 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 522 match axis: 523 case "along": 524 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 525 case "in_plane": 526 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 527 case "out_plane": 528 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 529 case _: 530 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 531 532 # 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()
quickshow()
render()
----------
shift_source(axis, distance)
tilt_source(self, axis, angle)
----------
rotate_OE(OEindx, axis, angle)
shift_OE(OEindx, axis, distance)
86 def __init__(self, source_rays, optical_elements, detectors, description=""): 87 """ 88 Parameters 89 ---------- 90 source_rays : list[mray.Ray] 91 List of source rays, which are to be traced. 92 93 optical_elements : list[moe.OpticalElement] 94 List of successive optical elements. 95 96 detector: mdet.Detector (optional) 97 The detector (or list of detectors) to analyse the results. 98 99 description : str, optional 100 A string to describe the optical setup. Defaults to ''. 101 102 """ 103 self.source_rays = copy.deepcopy(source_rays) 104 # deepcopy so this object doesn't get changed when the global source_rays changes "outside" 105 self.optical_elements = copy.deepcopy(optical_elements) 106 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 107 self.detectors = detectors 108 if isinstance(detectors, mdet.Detector): 109 self.detectors = {"Focus": detectors} 110 self.description = description 111 self._output_rays = None 112 self._last_source_rays_hash = None 113 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 ''.
172 def get_input_rays(self): 173 """ 174 Returns the list of source rays. 175 """ 176 return self.source_rays
Returns the list of source rays.
178 def get_output_rays(self, force=False, **kwargs): 179 """ 180 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 181 or if the source-ray-bundle or anything about the optical elements has changed. 182 183 This is the user-facing method to perform the ray-tracing calculation. 184 """ 185 current_source_rays_hash = hash(self.source_rays) 186 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 187 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 188 print("...ray-tracing...", end="", flush=True) 189 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 190 print( 191 "\r\033[K", end="", flush=True 192 ) # move to beginning of the line with \r and then delete the whole line with \033[K 193 self._last_source_rays_hash = current_source_rays_hash 194 self._last_optical_elements_hash = current_optical_elements_hash 195 196 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.
198 def get2dPoints(self, Detector = "Focus"): 199 """ 200 Returns the 2D points of the detected rays on the detector. 201 """ 202 if isinstance(Detector, str): 203 if Detector in self.detectors.keys(): 204 Detector = self.detectors[Detector] 205 else: 206 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 207 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0]
Returns the 2D points of the detected rays on the detector.
209 def get3dPoints(self, Detector = "Focus"): 210 """ 211 Returns the 3D points of the detected rays on the detector. 212 """ 213 if isinstance(Detector, str): 214 if Detector in self.detectors.keys(): 215 Detector = self.detectors[Detector] 216 else: 217 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 218 return Detector.get_3D_points(self.get_output_rays()[Detector.index])
Returns the 3D points of the detected rays on the detector.
220 def getDelays(self, Detector = "Focus"): 221 """ 222 Returns the delays of the detected rays on the detector. 223 """ 224 if isinstance(Detector, str): 225 if Detector in self.detectors.keys(): 226 Detector = self.detectors[Detector] 227 else: 228 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 229 return Detector.get_Delays(self.get_output_rays()[Detector.index])
Returns the delays of the detected rays on the detector.
233 def shift_source(self, axis: str|np.ndarray, distance: float): 234 """ 235 Shift source ray bundle by distance (in mm) along the 'axis' specified as 236 a lab-frame vector (numpy-array of length 3) or as one of the strings 237 "vert", "horiz", or "random". 238 239 In the latter case, the reference is the incidence plane of the first 240 non-normal-incidence mirror after the source. If there is none, you will 241 be asked to rather specify the axis as a 3D-numpy-array. 242 243 axis = "vert" means the source position is shifted along the axis perpendicular 244 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 245 246 axis = "horiz" means the source direciton is translated along thr axis in that 247 incidence plane and perpendicular to the current source direction, 248 i.e. "horizontally" in the incidence plane, but retaining the same distance 249 of source and first optical element. 250 251 axis = "random" means the the source direction shifted in a random direction 252 within in the plane perpendicular to the current source direction, 253 e.g. simulating a fluctuation of hte transverse source position. 254 255 Parameters 256 ---------- 257 axis : np.ndarray or str 258 Shift axis, specified either as a 3D lab-frame vector or as one 259 of the strings "vert", "horiz", or "random". 260 261 distance : float 262 Shift distance in mm. 263 264 Returns 265 ------- 266 Nothing, just modifies the property 'source_rays'. 267 """ 268 if type(distance) not in [int, float, np.float64]: 269 raise ValueError('The "distance"-argument must be an int or float number.') 270 271 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 272 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 273 274 OEnormal = None 275 for i in mirror_indcs: 276 ith_OEnormal = self.optical_elements[i].normal 277 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 278 OEnormal = ith_OEnormal 279 break 280 if OEnormal is None: 281 raise Exception( 282 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 283 so you should rather give 'axis' as a numpy-array of length 3." 284 ) 285 286 if type(axis) == np.ndarray and len(axis) == 3: 287 translation_vector = axis 288 else: 289 perp_axis = np.cross(central_ray_vector, OEnormal) 290 horiz_axis = np.cross(perp_axis, central_ray_vector) 291 292 if axis == "vert": 293 translation_vector = perp_axis 294 elif axis == "horiz": 295 translation_vector = horiz_axis 296 elif axis == "random": 297 translation_vector = ( 298 np.random.uniform(low=-1, high=1, size=1) * perp_axis 299 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 300 ) 301 else: 302 raise ValueError( 303 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 304 ) 305 306 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'.
308 def tilt_source(self, axis: str|np.ndarray, angle: float): 309 """ 310 Rotate source ray bundle by angle around an axis, specified as 311 a lab-frame vector (numpy-array of length 3) or as one of the strings 312 "in_plane", "out_plane" or "random" direction. 313 314 In the latter case, the function considers the incidence plane of the first 315 non-normal-incidence mirror after the source. If there is none, you will 316 be asked to rather specify the axis as a 3D-numpy-array. 317 318 axis = "in_plane" means the source direction is rotated about an axis 319 perpendicular to that incidence plane, which tilts the source 320 "horizontally" in the same plane. 321 322 axis = "out_plane" means the source direciton is rotated about an axis 323 in that incidence plane and perpendicular to the current source direction, 324 which tilts the source "vertically" out of the former incidence plane. 325 326 axis = "random" means the the source direction is tilted in a random direction, 327 e.g. simulating a beam pointing fluctuation. 328 329 Attention, "angle" is given in deg, so as to remain consitent with the 330 conventions of other functions, although pointing is mostly talked about 331 in mrad instead. 332 333 Parameters 334 ---------- 335 axis : np.ndarray or str 336 Shift axis, specified either as a 3D lab-frame vector or as one 337 of the strings "in_plane", "out_plane", or "random". 338 339 angle : float 340 Rotation angle in degree. 341 342 Returns 343 ------- 344 Nothing, just modifies the property 'source_rays'. 345 """ 346 if type(angle) not in [int, float, np.float64]: 347 raise ValueError('The "angle"-argument must be an int or float number.') 348 349 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 350 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 351 352 OEnormal = None 353 for i in mirror_indcs: 354 ith_OEnormal = self.optical_elements[i].normal 355 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 356 OEnormal = ith_OEnormal 357 break 358 if OEnormal is None: 359 raise Exception( 360 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 361 so you should rather give 'axis' as a numpy-array of length 3." 362 ) 363 364 if type(axis) == np.ndarray and len(axis) == 3: 365 rot_axis = axis 366 else: 367 rot_axis_in = np.cross(central_ray_vector, OEnormal) 368 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 369 if axis == "in_plane": 370 rot_axis = rot_axis_in 371 elif axis == "out_plane": 372 rot_axis = rot_axis_out 373 elif axis == "random": 374 rot_axis = ( 375 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 376 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 377 ) 378 else: 379 raise ValueError( 380 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 381 ) 382 383 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'.
385 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 386 """ 387 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 388 OEstart and OEstop (both included). 389 """ 390 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 391 outray = self.get_output_rays()[OEstart-1][0] 392 print(outray) 393 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 394 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).
397 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 398 """ 399 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 400 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 401 that takes either of the two values: 402 - "in" 403 - "out" 404 In either case the "axis" can take these values: 405 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 406 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 407 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 408 409 Parameters 410 ---------- 411 OEindx : int 412 Index of the optical element to modify out of OpticalChain.optical_elements. 413 414 ref : str 415 Reference ray used to define the axes of rotation. Can be either: 416 "in" or "out" or "local_normal" (in+out) 417 418 axis : str 419 Rotation axis, specified as one of the strings 420 "pitch", "roll", "yaw" 421 422 angle : float 423 Rotation angle in degree. 424 425 Returns 426 ------- 427 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 428 """ 429 if abs(OEindx) > len(self.optical_elements): 430 raise ValueError( 431 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 432 ) 433 if type(angle) not in [int, float, np.float64]: 434 raise ValueError('The "angle"-argument must be an int or float number.') 435 MasterRay = [mp.FindCentralRay(self.source_rays)] 436 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 437 TracedRay = [MasterRay] + TracedRay 438 match ref: 439 case "out": 440 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 441 case "in": 442 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 443 case "localnormal": 444 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 445 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 446 RefVec = mgeo.Normalize(Out-In) 447 case _: 448 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 449 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 450 match axis: 451 case "pitch": 452 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 453 case "roll": 454 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 455 case "yaw": 456 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 457 case _: 458 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 459 else: 460 #If the normal vector is aligned with the ray 461 match axis: 462 case "pitch": 463 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 464 case "roll": 465 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 466 case "yaw": 467 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 468 case _: 469 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].
471 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 472 """ 473 Shift the optical element OpticalChain.optical_elements[OEindx] along 474 axis referenced to the master ray. 475 476 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 477 that takes either of the two values: 478 - "in" 479 - "out" 480 In either case the "axis" can take these values: 481 - "along": Translation along the master ray. 482 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 483 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 484 485 Parameters 486 ---------- 487 OEindx : int 488 Index of the optical element to modify out of OpticalChain.optical_elements. 489 490 ref : str 491 Reference ray used to define the axes of rotation. Can be either: 492 "in" or "out". 493 494 axis : str 495 Translation axis, specified as one of the strings 496 "along", "in_plane", "out_plane". 497 498 distance : float 499 Rotation angle in degree. 500 501 Returns 502 ------- 503 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 504 """ 505 if abs(OEindx) >= len(self.optical_elements): 506 raise ValueError( 507 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 508 ) 509 if type(distance) not in [int, float, np.float64]: 510 raise ValueError('The "dist"-argument must be an int or float number.') 511 512 MasterRay = [mp.FindCentralRay(self.source_rays)] 513 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 514 TracedRay = [MasterRay] + TracedRay 515 match ref: 516 case "out": 517 RefVec = TracedRay[OEindx+1][0].vector 518 case "in": 519 RefVec = TracedRay[OEindx][0].vector 520 case _: 521 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 522 match axis: 523 case "along": 524 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 525 case "in_plane": 526 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 527 case "out_plane": 528 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 529 case _: 530 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 531 532 # 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 DurationSD = mp.StandardDeviation(DelayList) 147 z = np.asarray(DelayList) 148 zlabel = "Delay (fs)" 149 title = "Delay + Spot Diagram\n press left/right to move detector position" 150 addLine = "\n" + "{:.2f}".format(DurationSD) + " fs SD" 151 case _: 152 z = "red" 153 title = "Spot Diagram\n press left/right to move detector position" 154 addLine = "" 155 156 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 157 158 plt.ion() 159 fig, ax = plt.subplots() 160 if DrawFocal: 161 focal = ax.pcolormesh(X*1e3,Y*1e3,Z) 162 if DrawFocalContour: 163 levels = [1/np.e**2, 0.5] 164 contour = ax.contourf(X*1e3, Y*1e3, Z, levels=levels, cmap='gray') 165 166 if DrawAiryAndFourier: 167 theta = np.linspace(0, 2 * np.pi, 100) 168 x = AiryRadius * np.cos(theta) 169 y = AiryRadius * np.sin(theta) # 170 ax.plot(x, y, c="black") 171 172 173 foo = ax.scatter( 174 DectectorPoint2D_Xcoord, 175 DectectorPoint2D_Ycoord, 176 c=z, 177 s=15, 178 label="{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(SpotSizeSD * 1e3) + " \u03BCm SD" + addLine, 179 ) 180 181 axisLim = 1.1 * max(AiryRadius, 0.5 * FocalSpotSize * 1000) 182 ax.set_xlim(-axisLim, axisLim) 183 ax.set_ylim(-axisLim, axisLim) 184 185 if ColorCoded == "Intensity" or ColorCoded == "Incidence" or ColorCoded == "Delay": 186 cbar = fig.colorbar(foo) 187 cbar.set_label(zlabel) 188 189 ax.legend(loc="upper right") 190 ax.set_xlabel("X (µm)") 191 ax.set_ylabel("Y (µm)") 192 ax.set_title(title) 193 # ax.margins(x=0) 194 195 196 def update_plot(new_value): 197 nonlocal movingDetector, ColorCoded, zlabel, cbar, detectorPosition, foo, distStep, focal, contour, levels, Index, RayListAnalysed 198 199 newDectectorPoint2D_Xcoord, newDectectorPoint2D_Ycoord, newFocalSpotSize, newSpotSizeSD = mpu._getDetectorPoints( 200 RayListAnalysed, movingDetector 201 ) 202 203 if DrawFocal: 204 focal.set_array(Z) 205 if DrawFocalContour: 206 levels = [1/np.e**2, 0.5] 207 for coll in contour.collections: 208 coll.remove() # Remove old contour lines 209 contour = ax.contourf(X * 1e3, Y * 1e3, Z, levels=levels, cmap='gray') 210 211 xy = foo.get_offsets() 212 xy[:, 0] = newDectectorPoint2D_Xcoord 213 xy[:, 1] = newDectectorPoint2D_Ycoord 214 foo.set_offsets(xy) 215 216 217 if ColorCoded == "Delay": 218 newDelayList = np.asarray(movingDetector.get_Delays(RayListAnalysed)) 219 newDurationSD = mp.StandardDeviation(newDelayList) 220 newaddLine = "\n" + "{:.2f}".format(newDurationSD) + " fs SD" 221 foo.set_array(newDelayList) 222 foo.set_clim(min(newDelayList), max(newDelayList)) 223 cbar.update_normal(foo) 224 else: 225 newaddLine = "" 226 227 foo.set_label( 228 "{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(newSpotSizeSD * 1e3) + " \u03BCm SD" + newaddLine 229 ) 230 ax.legend(loc="upper right") 231 232 axisLim = 1.1 * max(AiryRadius, 0.5 * newFocalSpotSize * 1000) 233 ax.set_xlim(-axisLim, axisLim) 234 ax.set_ylim(-axisLim, axisLim) 235 236 distStep = min( 237 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 238 ) # in mm 239 240 fig.canvas.draw_idle() 241 242 243 def press(event): 244 nonlocal detectorPosition, distStep 245 if event.key == "right": 246 detectorPosition.value += distStep 247 elif event.key == "left": 248 if detectorPosition.value > 1.5 * distStep: 249 detectorPosition.value -= distStep 250 else: 251 detectorPosition.value = 0.5 * distStep 252 else: 253 return None 254 255 fig.canvas.mpl_connect("key_press_event", press) 256 257 plt.show() 258 259 detectorPosition.register(update_plot) 260 261 262 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.
267def DrawDelaySpots(OpticalChain, 268 DeltaFT: tuple[int, float], 269 Detector = "Focus", 270 DrawAiryAndFourier=False, 271 ColorCoded=None, 272 Observer = None 273 ) -> plt.Figure: 274 """ 275 Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed 276 hitting the Detector, with the ray-delays shown in the 3rd dimension. 277 The detector distance can be shifted with the left-right cursor keys. 278 If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and 279 whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'. 280 281 The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"]. 282 283 Parameters 284 ---------- 285 RayListAnalysed : list(Ray) 286 List of objects of the ModuleOpticalRay.Ray-class. 287 288 Detector : Detector 289 An object of the ModuleDetector.Detector-class. 290 291 DeltaFT : (int, float) 292 The Fourier-limited pulse duration. Just used as a reference to compare the temporal spread 293 induced by the ray-delays. 294 295 DrawAiryAndFourier : bool, optional 296 Whether to draw a cylinder showing the Airy-spot-size and Fourier-limited-duration. 297 The default is False. 298 299 ColorCoded : str, optional 300 Color-code the spots according to one of ["Intensity","Incidence"]. 301 The default is None. 302 303 Returns 304 ------- 305 fig : matlplotlib-figure-handle. 306 Shows the interactive figure. 307 """ 308 if isinstance(Detector, str): 309 Det = OpticalChain.detectors[Detector] 310 else: 311 Det = Detector 312 Index = Det.index 313 Detector = copy(Det) 314 if Observer is None: 315 detectorPosition = Observable(Detector.distance) 316 else: 317 detectorPosition = Observer 318 Detector.distance = detectorPosition.value 319 320 RayListAnalysed = OpticalChain.get_output_rays()[Index] 321 fig, NumericalAperture, AiryRadius, FocalSpotSize = _drawDelayGraph( 322 RayListAnalysed, Detector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded 323 ) 324 325 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 326 327 movingDetector = copy(Detector) 328 329 def update_plot(new_value): 330 nonlocal movingDetector, ColorCoded, detectorPosition, distStep, fig 331 ax = fig.axes[0] 332 cam = [ax.azim, ax.elev, ax._dist] 333 fig, sameNumericalAperture, sameAiryRadius, newFocalSpotSize = _drawDelayGraph( 334 RayListAnalysed, movingDetector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded, fig 335 ) 336 ax = fig.axes[0] 337 ax.azim, ax.elev, ax._dist = cam 338 distStep = min( 339 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 340 ) 341 return fig 342 343 def press(event): 344 nonlocal detectorPosition, distStep, movingDetector, fig 345 if event.key == "right": 346 detectorPosition.value += distStep 347 elif event.key == "left": 348 if detectorPosition.value > 1.5 * distStep: 349 detectorPosition.value -= distStep 350 else: 351 detectorPosition.value = 0.5 * distStep 352 353 fig.canvas.mpl_connect("key_press_event", press) 354 detectorPosition.register(update_plot) 355 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 356 357 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.
438def DrawMirrorProjection(OpticalChain, ReflectionNumber: int, ColorCoded=None, Detector="") -> plt.Figure: 439 """ 440 Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. 441 The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is 442 measured at the Detector. 443 444 Parameters 445 ---------- 446 OpticalChain : OpticalChain 447 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 448 449 ReflectionNumber : int 450 Index specifying the optical element on which you want to see the impact points. 451 452 Detector : Detector, optional 453 Object of the ModuleDetector.Detector-class. Only necessary to project delays. The default is None. 454 455 ColorCoded : str, optional 456 Specifies which ray property to color-code: ["Incidence","Intensity","Delay"]. The default is None. 457 458 Returns 459 ------- 460 fig : matlplotlib-figure-handle. 461 Shows the figure. 462 """ 463 from mpl_toolkits.axes_grid1 import make_axes_locatable 464 if isinstance(Detector, str): 465 if Detector == "": 466 Detector = None 467 else: 468 Detector = OpticalChain.detectors[Detector] 469 470 Position = OpticalChain[ReflectionNumber].position 471 q = OpticalChain[ReflectionNumber].orientation 472 # n = OpticalChain.optical_elements[ReflectionNumber].normal 473 # m = OpticalChain.optical_elements[ReflectionNumber].majoraxis 474 475 RayListAnalysed = OpticalChain.get_output_rays()[ReflectionNumber] 476 # transform rays into the mirror-support reference frame 477 # (same as mirror frame but without the shift by mirror-centre) 478 r0 = OpticalChain[ReflectionNumber].r0 479 RayList = [r.to_basis(*OpticalChain[ReflectionNumber].basis) for r in RayListAnalysed] 480 481 x = np.asarray([k.point[0] for k in RayList]) - r0[0] 482 y = np.asarray([k.point[1] for k in RayList]) - r0[1] 483 if ColorCoded == "Intensity": 484 IntensityList = [k.intensity for k in RayListAnalysed] 485 z = np.asarray(IntensityList) 486 zlabel = "Intensity (arb.u.)" 487 title = "Ray intensity projected on mirror " 488 elif ColorCoded == "Incidence": 489 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # in degree 490 z = np.asarray(IncidenceList) 491 zlabel = "Incidence angle (deg)" 492 title = "Ray incidence projected on mirror " 493 elif ColorCoded == "Delay": 494 if Detector is not None: 495 z = np.asarray(Detector.get_Delays(RayListAnalysed)) 496 zlabel = "Delay (fs)" 497 title = "Ray delay at detector projected on mirror " 498 else: 499 raise ValueError("If you want to project ray delays, you must specify a detector.") 500 else: 501 z = "red" 502 title = "Ray impact points projected on mirror" 503 504 plt.ion() 505 fig = plt.figure() 506 ax = OpticalChain.optical_elements[ReflectionNumber].support._ContourSupport(fig) 507 p = plt.scatter(x, y, c=z, s=15) 508 if ColorCoded == "Delay" or ColorCoded == "Incidence" or ColorCoded == "Intensity": 509 divider = make_axes_locatable(ax) 510 cax = divider.append_axes("right", size="5%", pad=0.05) 511 cbar = fig.colorbar(p, cax=cax) 512 cbar.set_label(zlabel) 513 ax.set_xlabel("x (mm)") 514 ax.set_ylabel("y (mm)") 515 plt.title(title, loc="right") 516 plt.tight_layout() 517 518 bbox = ax.get_position() 519 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 520 ax.set_position(bbox) 521 plt.show() 522 523 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.
529def DrawSetup(OpticalChain, 530 EndDistance=None, 531 maxRays=300, 532 OEpoints=2000, 533 draw_mesh=False, 534 cycle_ray_colors = False, 535 impact_points = False, 536 DrawDetectors=True, 537 DetectedRays = False, 538 Observers = dict()): 539 """ 540 Renders an image of the Optical setup and the traced rays. 541 542 Parameters 543 ---------- 544 OpticalChain : OpticalChain 545 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 546 547 EndDistance : float, optional 548 The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). If not specified, 549 this distance is set to that between the source point and the 1st optical element. 550 551 maxRays: int 552 The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog 553 and not required for a nice image. Default is 150. 554 555 OEpoints : int 556 How many little spheres to draw to represent the optical elements. Default is 2000. 557 558 Returns 559 ------- 560 fig : Pyvista-figure-handle. 561 Shows the figure. 562 """ 563 564 RayListHistory = [OpticalChain.source_rays] + OpticalChain.get_output_rays() 565 566 if EndDistance is None: 567 EndDistance = np.linalg.norm(OpticalChain.source_rays[0].point - OpticalChain.optical_elements[0].position) 568 569 print("...rendering image of optical chain...", end="", flush=True) 570 fig = pvqt.BackgroundPlotter(window_size=(1500, 500), notebook=False) # Opening a window 571 fig.set_background('white') 572 573 if cycle_ray_colors: 574 colors = mpu.generate_distinct_colors(len(OpticalChain)+1) 575 else: 576 colors = [[0.7, 0, 0]]*(len(OpticalChain)+1) # Default color: dark red 577 578 # Optics display 579 # For each optic we will send the figure to the function _RenderOpticalElement and it will add the optic to the figure 580 for i,OE in enumerate(OpticalChain.optical_elements): 581 color = pv.Color(colors[i+1]) 582 rgb = color.float_rgb 583 h, l, s = rgb_to_hls(*rgb) 584 s = max(0, min(1, s * 0.3)) # Decrease saturation 585 l = max(0, min(1, l + 0.1)) # Increase lightness 586 new_rgb = hls_to_rgb(h, l, s) 587 darkened_color = pv.Color(new_rgb) 588 mpm._RenderOpticalElement(fig, OE, OEpoints, draw_mesh, darkened_color, index=i) 589 ray_meshes = mpm._RenderRays(RayListHistory, EndDistance, maxRays) 590 for i,ray in enumerate(ray_meshes): 591 color = pv.Color(colors[i]) 592 fig.add_mesh(ray, color=color, name=f"RayBundle_{i}") 593 if impact_points: 594 for i,rays in enumerate(RayListHistory): 595 points = np.array([list(r.point) for r in rays], dtype=np.float32) 596 points = pv.PolyData(points) 597 color = pv.Color(colors[i-1]) 598 fig.add_mesh(points, color=color, point_size=5, name=f"RayImpactPoints_{i}") 599 600 detector_copies = {key: copy(OpticalChain.detectors[key]) for key in OpticalChain.detectors.keys()} 601 detector_meshes_list = [] 602 detectedpoint_meshes = dict() 603 604 if OpticalChain.detectors is not None and DrawDetectors: 605 # Detector display 606 for key in OpticalChain.detectors.keys(): 607 det = detector_copies[key] 608 index = OpticalChain.detectors[key].index 609 if key in Observers: 610 det.distance = Observers[key].value 611 #Observers[key].register_calculation(lambda x: det.set_distance(x)) 612 mpm._RenderDetector(fig, det, name = key, detector_meshes = detector_meshes_list) 613 if DetectedRays: 614 RayListAnalysed = OpticalChain.get_output_rays()[index] 615 points = det.get_3D_points(RayListAnalysed) 616 points = pv.PolyData(points) 617 detectedpoint_meshes[key] = points 618 fig.add_mesh(points, color='purple', point_size=5, name=f"DetectedRays_{key}") 619 detector_meshes = dict(zip(OpticalChain.detectors.keys(), detector_meshes_list)) 620 621 # Now we define a function that will move on the plot the detector with name "detname" when it's called 622 def move_detector(detname, new_value): 623 nonlocal fig, detector_meshes, detectedpoint_meshes, DetectedRays, detectedpoint_meshes, detector_copies, OpticalChain 624 det = detector_copies[detname] 625 index = OpticalChain.detectors[detname].index 626 det_mesh = detector_meshes[detname] 627 translation = det.normal * (det.distance - new_value) 628 det_mesh.translate(translation, inplace=True) 629 det.distance = new_value 630 if DetectedRays: 631 points_mesh = detectedpoint_meshes[detname] 632 points_mesh.points = det.get_3D_points(OpticalChain.get_output_rays()[index]) 633 fig.show() 634 635 # Now we register the function to the observers 636 for key in OpticalChain.detectors.keys(): 637 if key in Observers: 638 Observers[key].register(lambda x: move_detector(key, x)) 639 640 #pv.save_meshio('optics.inp', pointcloud) 641 print( 642 "\r\033[K", end="", flush=True 643 ) # move to beginning of the line with \r and then delete the whole line with \033[K 644 fig.show() 645 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.
701def DrawCaustics(OpticalChain, Range=1, Detector="Focus" , Npoints=1000, Nrays=1000): 702 """ 703 This function displays the caustics of the rays on the detector. 704 To do so, it calculates the intersections of the rays with the detector over a 705 range determined by the parameter Range, and then plots the standard deviation of the 706 positions in the x and y directions. 707 708 Parameters 709 ---------- 710 OpticalChain : OpticalChain 711 The optical chain to analyse. 712 713 DetectorName : str 714 The name of the detector on which the caustics are calculated. 715 716 Range : float 717 The range of the detector over which to calculate the caustics. 718 719 Npoints : int, optional 720 The number of points to sample on the detector. The default is 1000. 721 722 Returns 723 ------- 724 fig : Figure 725 The figure of the plot. 726 """ 727 distances = np.linspace(-Range, Range, Npoints) 728 if isinstance(Detector, str): 729 Det = OpticalChain.detectors[Detector] 730 Index = Det.index 731 Rays = OpticalChain.get_output_rays()[Index] 732 Nrays = min(Nrays, len(Rays)) 733 Rays = np.random.choice(Rays, Nrays, replace=False) 734 LocalRayList = [r.to_basis(*Det.basis) for r in Rays] 735 Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances) 736 x_std = [] 737 y_std = [] 738 for i in range(len(distances)): 739 x_std.append(mp.StandardDeviation(Points[i][:,0])) 740 y_std.append(mp.StandardDeviation(Points[i][:,1])) 741 plt.ion() 742 fig, ax = plt.subplots() 743 ax.plot(distances, x_std, label="x std") 744 ax.plot(distances, y_std, label="y std") 745 ax.set_xlabel("Detector distance (mm)") 746 ax.set_ylabel("Standard deviation (mm)") 747 ax.legend() 748 plt.title("Caustics") 749 plt.show() 750 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- Spectrum 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 22from ARTcore.ModuleGeometry import Point, Vector, Origin 23import ARTcore.ModuleProcessing as mp 24 25from ARTcore.DepGraphDefinitions import UniformSpectraCalculator 26 27import numpy as np 28from abc import ABC, abstractmethod 29import logging 30 31logger = logging.getLogger(__name__) 32 33# %% Abstract base classes for sources 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 43 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 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 65 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 76 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 87 88# %% Specific power distributions 89class SpatialGaussianPowerDistribution(PowerDistribution): 90 """ 91 Spatial Gaussian power distribution. 92 """ 93 def __init__(self, Power, W0): 94 self.Power = Power 95 self.W0 = W0 96 97 def __call__(self, Origins, Directions): 98 """ 99 Return the power of the rays coming 100 from the origin points Origins to the directions Directions. 101 """ 102 return self.Power * np.exp(-2 * np.linalg.norm(Origins, axis=1) ** 2 / self.W0 ** 2) 103 104class AngularGaussianPowerDistribution(PowerDistribution): 105 """ 106 Angular Gaussian power distribution. 107 """ 108 def __init__(self, Power, Divergence): 109 self.Power = Power 110 self.Divergence = Divergence 111 112 def __call__(self, Origins, Directions): 113 """ 114 Return the power of the rays coming 115 from the origin points Origins to the directions Directions. 116 """ 117 return self.Power * np.exp(-2 * np.arccos(np.dot(Directions, [0, 0, 1])) ** 2 / self.Divergence ** 2) 118 119class GaussianPowerDistribution(PowerDistribution): 120 """ 121 Gaussian power distribution. 122 """ 123 def __init__(self, Power, W0, Divergence): 124 self.Power = Power 125 self.W0 = W0 126 self.Divergence = Divergence 127 128 def __call__(self, Origins, Directions): 129 """ 130 Return the power of the rays coming 131 from the origin points Origins to the directions Directions. 132 """ 133 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) 134 135class UniformPowerDistribution(PowerDistribution): 136 """ 137 Uniform power distribution. 138 """ 139 def __init__(self, Power): 140 self.Power = Power 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 return self.Power * np.ones(len(Origins)) 148 149# %% Specific ray origins distributions 150class PointRayOriginsDistribution(RayOriginsDistribution): 151 """ 152 Point ray origins distribution. 153 """ 154 def __init__(self, Origin): 155 self.Origin = Origin 156 157 def __call__(self, N): 158 """ 159 Return the origins of N rays. 160 """ 161 return mgeo.PointArray([self.Origin for i in range(N)]) 162 163class DiskRayOriginsDistribution(RayOriginsDistribution): 164 """ 165 Disk ray origins distribution. Uses the Vogel spiral to initialize the rays. 166 """ 167 def __init__(self, Origin, Radius, Normal = mgeo.Vector([0, 0, 1])): 168 self.Origin = Origin 169 self.Radius = Radius 170 self.Normal = Normal 171 172 def __call__(self, N): 173 """ 174 Return the origins of N rays. 175 """ 176 MatrixXY = mgeo.SpiralVogel(N, self.Radius) 177 q = mgeo.QRotationVector2Vector(mgeo.Vector([0, 0, 1]), self.Normal) 178 return mgeo.PointArray([self.Origin + mgeo.Vector([MatrixXY[i, 0], MatrixXY[i, 1], 0]) for i in range(N)]).rotate(q) 179 180# %% Specific ray directions distributions 181class UniformRayDirectionsDistribution(RayDirectionsDistribution): 182 """ 183 Uniform ray directions distribution. 184 """ 185 def __init__(self, Direction): 186 self.Direction = Direction 187 188 def __call__(self, N): 189 """ 190 Return the directions of N rays. 191 """ 192 return mgeo.VectorArray([self.Direction for i in range(N)]) 193 194class ConeRayDirectionsDistribution(RayDirectionsDistribution): 195 """ 196 Cone ray directions distribution. Uses the Vogel spiral to initialize the rays. 197 """ 198 def __init__(self, Direction, Angle): 199 self.Direction = Direction 200 self.Angle = Angle 201 202 def __call__(self, N): 203 """ 204 Return the directions of N rays. 205 """ 206 Height = 1 207 Radius = Height * np.tan(self.Angle) 208 MatrixXY = mgeo.SpiralVogel(N, Radius) 209 q = mgeo.QRotationVector2Vector(mgeo.Vector([0, 0, 1]), self.Direction) 210 return mgeo.VectorArray([[MatrixXY[i, 0], MatrixXY[i, 1], Height] for i in range(N)]).rotate(q).normalized() 211 212 213# %% Specific spectra 214class SingleWavelengthSpectrum(Spectrum): 215 """ 216 Single wavelength spectrum. 217 """ 218 def __init__(self, Wavelength): 219 self.Wavelength = Wavelength 220 221 def __call__(self, N): 222 """ 223 Return the wavelengths of N rays. 224 """ 225 return np.ones(N) * self.Wavelength 226 227class UniformSpectrum(Spectrum): 228 """ 229 Uniform spectrum. 230 Can be specified as a r 231 """ 232 def __init__(self, eVMax = None, eVMin = None, eVCentral = None, 233 lambdaMax = None, lambdaMin = None, lambdaCentral = None, 234 eVWidth = None, lambdaWidth = None): 235 # Using the DepSolver, we calculate the minimum and maximum wavelengths 236 values, steps = UniformSpectraCalculator().calculate_values( 237 lambda_min = lambdaMin, 238 lambda_max = lambdaMax, 239 lambda_center = lambdaCentral, 240 lambda_width = lambdaWidth, 241 eV_min = eVMin, 242 eV_max = eVMax, 243 eV_center = eVCentral, 244 eV_width = eVWidth 245 ) 246 self.lambdaMin = values['lambda_min'] 247 self.lambdaMax = values['lambda_max'] 248 def __call__(self, N): 249 """ 250 Return the wavelengths of N rays. 251 """ 252 return np.linspace(self.lambdaMin, self.lambdaMax, N) 253 254 255# %% Simple sources 256class SimpleSource(Source): 257 """ 258 A simple source is defined by 4 parameters: 259 - Spectrum 260 - Power distribution 261 - Ray origins distribution 262 - Ray directions distribution 263 """ 264 265 def __init__(self, Spectrum, PowerDistribution, RayOriginsDistribution, RayDirectionsDistribution): 266 self.Spectrum = Spectrum 267 self.PowerDistribution = PowerDistribution 268 self.RayOriginsDistribution = RayOriginsDistribution 269 self.RayDirectionsDistribution = RayDirectionsDistribution 270 271 def __call__(self, N): 272 """ 273 Return a list of N rays from the simple source. 274 """ 275 Wavelengths = self.Spectrum(N) 276 Origins = self.RayOriginsDistribution(N) 277 Directions = self.RayDirectionsDistribution(N) 278 Powers = self.PowerDistribution(Origins, Directions) 279 RayList = [] 280 for i in range(N): 281 RayList.append(mray.Ray(Origins[i], Directions[i], wavelength=Wavelengths[i], number=i, intensity=Powers[i])) 282 return mray.RayList.from_list(RayList) 283 284 285class ListSource(Source): 286 """ 287 A source containing just a list of rays that can be prepared manually. 288 """ 289 def __init__(self, Rays): 290 self.Rays = Rays 291 292 def __call__(self, N): 293 """ 294 Return the list of rays. 295 """ 296 if N < len(self.Rays): 297 return self.Rays[:N] 298 elif N>len(self.Rays): 299 logger.warning("Requested number of rays is greater than the number of rays in the source. Returning the whole source.") 300 return self.Rays 301 return mray.RayList.from_list(self.Rays)
35class Source(ABC): 36 """ 37 Abstract base class for sources. 38 Fundamentally a source just has to be able to return a list of rays. 39 To do so, we make it callable. 40 """ 41 @abstractmethod 42 def __call__(self,N): 43 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.
45class PowerDistribution(ABC): 46 """ 47 Abstract base class for angular power distributions. 48 """ 49 @abstractmethod 50 def __call__(self, Origins, Directions): 51 """ 52 Return the power of the rays coming 53 from the origin points Origins to the directions Directions. 54 """ 55 pass
Abstract base class for angular power distributions.
56class RayOriginsDistribution(ABC): 57 """ 58 Abstract base class for ray origins distributions. 59 """ 60 @abstractmethod 61 def __call__(self, N): 62 """ 63 Return the origins of N rays. 64 """ 65 pass
Abstract base class for ray origins distributions.
67class RayDirectionsDistribution(ABC): 68 """ 69 Abstract base class for ray directions distributions. 70 """ 71 @abstractmethod 72 def __call__(self, N): 73 """ 74 Return the directions of N rays. 75 """ 76 pass
Abstract base class for ray directions distributions.
78class Spectrum(ABC): 79 """ 80 Abstract base class for spectra. 81 """ 82 @abstractmethod 83 def __call__(self, N): 84 """ 85 Return the wavelengths of N rays. 86 """ 87 pass
Abstract base class for spectra.
90class SpatialGaussianPowerDistribution(PowerDistribution): 91 """ 92 Spatial Gaussian power distribution. 93 """ 94 def __init__(self, Power, W0): 95 self.Power = Power 96 self.W0 = W0 97 98 def __call__(self, Origins, Directions): 99 """ 100 Return the power of the rays coming 101 from the origin points Origins to the directions Directions. 102 """ 103 return self.Power * np.exp(-2 * np.linalg.norm(Origins, axis=1) ** 2 / self.W0 ** 2)
Spatial Gaussian power distribution.
105class AngularGaussianPowerDistribution(PowerDistribution): 106 """ 107 Angular Gaussian power distribution. 108 """ 109 def __init__(self, Power, Divergence): 110 self.Power = Power 111 self.Divergence = Divergence 112 113 def __call__(self, Origins, Directions): 114 """ 115 Return the power of the rays coming 116 from the origin points Origins to the directions Directions. 117 """ 118 return self.Power * np.exp(-2 * np.arccos(np.dot(Directions, [0, 0, 1])) ** 2 / self.Divergence ** 2)
Angular Gaussian power distribution.
120class GaussianPowerDistribution(PowerDistribution): 121 """ 122 Gaussian power distribution. 123 """ 124 def __init__(self, Power, W0, Divergence): 125 self.Power = Power 126 self.W0 = W0 127 self.Divergence = Divergence 128 129 def __call__(self, Origins, Directions): 130 """ 131 Return the power of the rays coming 132 from the origin points Origins to the directions Directions. 133 """ 134 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.
136class UniformPowerDistribution(PowerDistribution): 137 """ 138 Uniform power distribution. 139 """ 140 def __init__(self, Power): 141 self.Power = Power 142 143 def __call__(self, Origins, Directions): 144 """ 145 Return the power of the rays coming 146 from the origin points Origins to the directions Directions. 147 """ 148 return self.Power * np.ones(len(Origins))
Uniform power distribution.
151class PointRayOriginsDistribution(RayOriginsDistribution): 152 """ 153 Point ray origins distribution. 154 """ 155 def __init__(self, Origin): 156 self.Origin = Origin 157 158 def __call__(self, N): 159 """ 160 Return the origins of N rays. 161 """ 162 return mgeo.PointArray([self.Origin for i in range(N)])
Point ray origins distribution.
164class DiskRayOriginsDistribution(RayOriginsDistribution): 165 """ 166 Disk ray origins distribution. Uses the Vogel spiral to initialize the rays. 167 """ 168 def __init__(self, Origin, Radius, Normal = mgeo.Vector([0, 0, 1])): 169 self.Origin = Origin 170 self.Radius = Radius 171 self.Normal = Normal 172 173 def __call__(self, N): 174 """ 175 Return the origins of N rays. 176 """ 177 MatrixXY = mgeo.SpiralVogel(N, self.Radius) 178 q = mgeo.QRotationVector2Vector(mgeo.Vector([0, 0, 1]), self.Normal) 179 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.
182class UniformRayDirectionsDistribution(RayDirectionsDistribution): 183 """ 184 Uniform ray directions distribution. 185 """ 186 def __init__(self, Direction): 187 self.Direction = Direction 188 189 def __call__(self, N): 190 """ 191 Return the directions of N rays. 192 """ 193 return mgeo.VectorArray([self.Direction for i in range(N)])
Uniform ray directions distribution.
195class ConeRayDirectionsDistribution(RayDirectionsDistribution): 196 """ 197 Cone ray directions distribution. Uses the Vogel spiral to initialize the rays. 198 """ 199 def __init__(self, Direction, Angle): 200 self.Direction = Direction 201 self.Angle = Angle 202 203 def __call__(self, N): 204 """ 205 Return the directions of N rays. 206 """ 207 Height = 1 208 Radius = Height * np.tan(self.Angle) 209 MatrixXY = mgeo.SpiralVogel(N, Radius) 210 q = mgeo.QRotationVector2Vector(mgeo.Vector([0, 0, 1]), self.Direction) 211 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.
215class SingleWavelengthSpectrum(Spectrum): 216 """ 217 Single wavelength spectrum. 218 """ 219 def __init__(self, Wavelength): 220 self.Wavelength = Wavelength 221 222 def __call__(self, N): 223 """ 224 Return the wavelengths of N rays. 225 """ 226 return np.ones(N) * self.Wavelength
Single wavelength spectrum.
228class UniformSpectrum(Spectrum): 229 """ 230 Uniform spectrum. 231 Can be specified as a r 232 """ 233 def __init__(self, eVMax = None, eVMin = None, eVCentral = None, 234 lambdaMax = None, lambdaMin = None, lambdaCentral = None, 235 eVWidth = None, lambdaWidth = None): 236 # Using the DepSolver, we calculate the minimum and maximum wavelengths 237 values, steps = UniformSpectraCalculator().calculate_values( 238 lambda_min = lambdaMin, 239 lambda_max = lambdaMax, 240 lambda_center = lambdaCentral, 241 lambda_width = lambdaWidth, 242 eV_min = eVMin, 243 eV_max = eVMax, 244 eV_center = eVCentral, 245 eV_width = eVWidth 246 ) 247 self.lambdaMin = values['lambda_min'] 248 self.lambdaMax = values['lambda_max'] 249 def __call__(self, N): 250 """ 251 Return the wavelengths of N rays. 252 """ 253 return np.linspace(self.lambdaMin, self.lambdaMax, N)
Uniform spectrum. Can be specified as a r
233 def __init__(self, eVMax = None, eVMin = None, eVCentral = None, 234 lambdaMax = None, lambdaMin = None, lambdaCentral = None, 235 eVWidth = None, lambdaWidth = None): 236 # Using the DepSolver, we calculate the minimum and maximum wavelengths 237 values, steps = UniformSpectraCalculator().calculate_values( 238 lambda_min = lambdaMin, 239 lambda_max = lambdaMax, 240 lambda_center = lambdaCentral, 241 lambda_width = lambdaWidth, 242 eV_min = eVMin, 243 eV_max = eVMax, 244 eV_center = eVCentral, 245 eV_width = eVWidth 246 ) 247 self.lambdaMin = values['lambda_min'] 248 self.lambdaMax = values['lambda_max']
257class SimpleSource(Source): 258 """ 259 A simple source is defined by 4 parameters: 260 - Spectrum 261 - Power distribution 262 - Ray origins distribution 263 - Ray directions distribution 264 """ 265 266 def __init__(self, Spectrum, PowerDistribution, RayOriginsDistribution, RayDirectionsDistribution): 267 self.Spectrum = Spectrum 268 self.PowerDistribution = PowerDistribution 269 self.RayOriginsDistribution = RayOriginsDistribution 270 self.RayDirectionsDistribution = RayDirectionsDistribution 271 272 def __call__(self, N): 273 """ 274 Return a list of N rays from the simple source. 275 """ 276 Wavelengths = self.Spectrum(N) 277 Origins = self.RayOriginsDistribution(N) 278 Directions = self.RayDirectionsDistribution(N) 279 Powers = self.PowerDistribution(Origins, Directions) 280 RayList = [] 281 for i in range(N): 282 RayList.append(mray.Ray(Origins[i], Directions[i], wavelength=Wavelengths[i], number=i, intensity=Powers[i])) 283 return mray.RayList.from_list(RayList)
A simple source is defined by 4 parameters:
286class ListSource(Source): 287 """ 288 A source containing just a list of rays that can be prepared manually. 289 """ 290 def __init__(self, Rays): 291 self.Rays = Rays 292 293 def __call__(self, N): 294 """ 295 Return the list of rays. 296 """ 297 if N < len(self.Rays): 298 return self.Rays[:N] 299 elif N>len(self.Rays): 300 logger.warning("Requested number of rays is greater than the number of rays in the source. Returning the whole source.") 301 return self.Rays 302 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