ModuleAnalysis
Adds various analysis methods.
Contains the following functions: - GetETransmission: Calculates the energy transmission from RayListIn to RayListOut in percent. - GetResultSummary: Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation. - GetPulseProfile: Retrieve the pulse profile from the given RayList and Detector. - GetNumericalAperture: Returns the numerical aperture associated with the supplied ray-bundle. - GetAiryRadius: Returns the radius of the Airy disk. - GetPlaneWaveFocus: Calculates the approximate polychromatic focal spot of a set of rays. - GetDiffractionFocus: Calculates the approximate polychromatic focal spot of a set of rays. - GetClosestSphere: Calculates the closest sphere to the surface of a mirror. - GetAsphericity: Calculates the maximum distance of the mirror surface to the closest sphere. - _best_fit_sphere: Calculates the best sphere to fit a set of points.
Adds the following methods: - OpticalChain: - getETransmission: Calculates the energy transmission from the input to the output of the OpticalChain. - getResultsSummary: Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation. - getPulseProfile: Retrieve the pulse profile from the output of the OpticalChain. - getPlaneWaveFocus: Calculates the approximate polychromatic focal spot of the output of the OpticalChain. - getDiffractionFocus: Calculates the approximate polychromatic focal spot of the output of the OpticalChain. - Mirror: - getClosestSphere: Calculates the closest sphere to the surface of a mirror. - getAsphericity: Calculates the maximum distance of the mirror surface to the closest sphere.
Created in July 2024
@author: André Kalouguine + Stefan Haessler + Anthony Guillaume
1""" 2Adds various analysis methods. 3 4Contains the following functions: 5 - GetETransmission: Calculates the energy transmission from RayListIn to RayListOut in percent. 6 - GetResultSummary: Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation. 7 - GetPulseProfile: Retrieve the pulse profile from the given RayList and Detector. 8 - GetNumericalAperture: Returns the numerical aperture associated with the supplied ray-bundle. 9 - GetAiryRadius: Returns the radius of the Airy disk. 10 - GetPlaneWaveFocus: Calculates the approximate polychromatic focal spot of a set of rays. 11 - GetDiffractionFocus: Calculates the approximate polychromatic focal spot of a set of rays. 12 - GetClosestSphere: Calculates the closest sphere to the surface of a mirror. 13 - GetAsphericity: Calculates the maximum distance of the mirror surface to the closest sphere. 14 - _best_fit_sphere: Calculates the best sphere to fit a set of points. 15 16Adds the following methods: 17 - OpticalChain: 18 - getETransmission: Calculates the energy transmission from the input to the output of the OpticalChain. 19 - getResultsSummary: Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation. 20 - getPulseProfile: Retrieve the pulse profile from the output of the OpticalChain. 21 - getPlaneWaveFocus: Calculates the approximate polychromatic focal spot of the output of the OpticalChain. 22 - getDiffractionFocus: Calculates the approximate polychromatic focal spot of the output of the OpticalChain. 23 - Mirror: 24 - getClosestSphere: Calculates the closest sphere to the surface of a mirror. 25 - getAsphericity: Calculates the maximum distance of the mirror surface to the closest sphere. 26 27Created in July 2024 28 29@author: André Kalouguine + Stefan Haessler + Anthony Guillaume 30""" 31# %% Module imports 32import ARTcore.ModuleGeometry as mgeo 33import ARTcore.ModuleOpticalRay as mray 34import ARTcore.ModuleProcessing as mp 35import ARTcore.ModuleOpticalChain as moc 36import ARTcore.ModuleMirror as mmirror 37import ART.ModulePlottingMethods as mpm 38import matplotlib.pyplot as plt 39from mpl_toolkits.axes_grid1 import make_axes_locatable 40import numpy as np 41import math 42 43LightSpeed = 299792458000 44 45# %% Analysis methods 46def GetETransmission(RayListIn, RayListOut) -> float: 47 """ 48 Calculates the energy transmission from RayListIn to RayListOut in percent by summing up the 49 intensity-property of the individual rays. 50 51 Parameters 52 ---------- 53 RayListIn : list(Ray) 54 List of incoming rays. 55 56 RayListOut : list(Ray) 57 List of outgoing rays. 58 59 Returns 60 ------- 61 ETransmission : float 62 """ 63 ETransmission = 100 * sum(Ray.intensity for Ray in RayListOut) / sum(Ray.intensity for Ray in RayListIn) 64 return ETransmission 65 66def _getETransmission(OpticalChain, IndexIn=0, IndexOut=-1) -> float: 67 """ 68 Calculates the energy transmission from the input to the output of the OpticalChain in percent. 69 70 Parameters 71 ---------- 72 OpticalChain : OpticalChain 73 An object of the ModuleOpticalChain.OpticalChain-class. 74 75 IndexIn : int, optional 76 Index of the input RayList in the OpticalChain, defaults to 0. 77 78 IndexOut : int, optional 79 Index of the output RayList in the OpticalChain, defaults to -1. 80 81 Returns 82 ------- 83 ETransmission : float 84 """ 85 Rays = OpticalChain.get_output_rays() 86 if IndexIn == 0: 87 RayListIn = OpticalChain.get_input_rays() 88 else: 89 RayListIn = Rays[IndexIn] 90 RayListOut = Rays[IndexOut] 91 ETransmission = GetETransmission(RayListIn, RayListOut) 92 return ETransmission 93 94moc.OpticalChain.getETransmission = _getETransmission 95 96def GetResultSummary(Detector, RayListAnalysed, verbose=False): 97 """ 98 Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation 99 for the given Detector and RayList. 100 If verbose, then also print a summary of the results for the given Detector. 101 102 Parameters 103 ---------- 104 Detector : Detector 105 An object of the ModuleDetector.Detector-class. 106 107 RayListAnalysed : list(Ray) 108 List of objects of the ModuleOpticalRay.Ray-class. 109 110 verbose : bool 111 Whether to print a result summary. 112 113 Returns 114 ------- 115 FocalSpotSizeSD : float 116 117 DurationSD : float 118 """ 119 DetectorPointList2DCentre = Detector.get_2D_points(RayListAnalysed) 120 FocalSpotSizeSD = mp.StandardDeviation(DetectorPointList2DCentre) 121 DelayList = Detector.get_Delays(RayListAnalysed) 122 DurationSD = mp.StandardDeviation(DelayList) 123 124 if verbose: 125 FocalSpotSize = mgeo.DiameterPointList(DetectorPointList2DCentre) 126 summarystring = ( 127 "At the detector distance of " 128 + "{:.3f}".format(Detector.get_distance()) 129 + " mm we get:\n" 130 + "Spatial std : " 131 + "{:.3f}".format(FocalSpotSizeSD * 1e3) 132 + " \u03BCm and min-max: " 133 + "{:.3f}".format(FocalSpotSize * 1e3) 134 + " \u03BCm\n" 135 + "Temporal std : " 136 + "{:.3e}".format(DurationSD) 137 + " fs and min-max : " 138 + "{:.3e}".format(max(DelayList) - min(DelayList)) 139 + " fs" 140 ) 141 142 print(summarystring) 143 144 return FocalSpotSizeSD, DurationSD 145 146def _getResultsSummary(OpticalChain, Detector = "Focus", verbose=False): 147 """ 148 Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation 149 for the given Detector and RayList. 150 If verbose, then also print a summary of the results for the given Detector. 151 152 Parameters 153 ---------- 154 OpticalChain : OpticalChain 155 An object of the ModuleOpticalChain.OpticalChain-class. 156 157 Detector : Detector or str, optional 158 An object of the ModuleDetector.Detector-class or "Focus" to use the focus detector, defaults to "Focus". 159 160 verbose : bool 161 Whether to print a result summary. 162 163 Returns 164 ------- 165 FocalSpotSizeSD : float 166 167 DurationSD : float 168 """ 169 if isinstance(Detector, str): 170 Detector = OpticalChain.detectors[Detector] 171 Index = Detector.index 172 RayListAnalysed = OpticalChain.get_output_rays()[Index] 173 FocalSpotSizeSD, DurationSD = GetResultSummary(Detector, RayListAnalysed, verbose) 174 return FocalSpotSizeSD, DurationSD 175 176moc.OpticalChain.getResultsSummary = _getResultsSummary 177 178def GetPulseProfile(Detector, RayList, Nbins=100): 179 """ 180 Retrieve the pulse profile from the given RayList and Detector. 181 The pulse profile is calculated by binning the delays of the rays in the RayList 182 at the Detector. The intensity of the rays is also taken into account. 183 184 Parameters 185 ---------- 186 Detector : Detector 187 An object of the ModuleDetector.Detector-class. 188 189 RayList : list(Ray) 190 List of objects of the ModuleOpticalRay.Ray-class. 191 192 Nbins : int 193 Number of bins to use for the histogram. 194 195 Returns 196 ------- 197 time : numpy.ndarray 198 The bins of the histogram. 199 200 intensity : numpy.ndarray 201 The histogram values. 202 """ 203 DelayList = Detector.get_Delays(RayList) 204 IntensityList = [Ray.intensity for Ray in RayList] 205 intensity, time_edges = np.histogram(DelayList, bins=Nbins, weights=IntensityList) 206 time = 0.5 * (time_edges[1:] + time_edges[:-1]) 207 return time, intensity 208 209def _getPulseProfile(OpticalChain, Detector = "Focus", Nbins=100): 210 """ 211 Retrieve the pulse profile from the output of the OpticalChain. 212 The pulse profile is calculated by binning the delays of the rays in the RayList 213 at the Detector. The intensity of the rays is also taken into account. 214 215 Parameters 216 ---------- 217 OpticalChain : OpticalChain 218 An object of the ModuleOpticalChain.OpticalChain-class. 219 220 Detector : Detector or str, optional 221 An object of the ModuleDetector.Detector-class or "Focus" to use the focus detector, defaults to "Focus". 222 223 Nbins : int 224 Number of bins to use for the histogram. 225 226 Returns 227 ------- 228 time : numpy.ndarray 229 The bins of the histogram. 230 231 intensity : numpy.ndarray 232 The histogram values. 233 """ 234 if isinstance(Detector, str): 235 Detector = OpticalChain.detectors[Detector] 236 Index = Detector.index 237 RayList = OpticalChain.get_output_rays()[Index] 238 time, intensity = GetPulseProfile(Detector, RayList, Nbins) 239 return time, intensity 240 241 242def GetNumericalAperture(RayList: list[mray.Ray], RefractiveIndex: float = 1) -> float: 243 r""" 244 Returns the numerical aperture associated with the supplied ray-bundle 'Raylist'. 245 This is $n\sin\theta$, where $\theta$ is the maximum angle between any of the rays and the central ray, 246 and $n$ is the refractive index of the propagation medium. 247 248 Parameters 249 ---------- 250 RayList : list of Ray-object 251 The ray-bundle of which to determine the NA. 252 253 RefractiveIndex : float, optional 254 Refractive index of the propagation medium, defaults to =1. 255 256 Returns 257 ------- 258 NA : float 259 """ 260 CentralRay = mp.FindCentralRay(RayList) 261 if CentralRay is None: 262 CentralVector = np.array([0, 0, 0]) 263 for k in RayList: 264 CentralVector = CentralVector + k.vector 265 CentralVector = CentralVector / len(RayList) 266 else: 267 CentralVector = CentralRay.vector 268 ListAngleAperture = [] 269 for k in RayList: 270 ListAngleAperture.append(mgeo.AngleBetweenTwoVectors(CentralVector, k.vector)) 271 272 return np.sin(np.amax(ListAngleAperture)) * RefractiveIndex 273 274 275def GetAiryRadius(Wavelength: float, NumericalAperture: float) -> float: 276 r""" 277 Returns the radius of the Airy disk: $r = 1.22 \frac\{\lambda\}\{NA\}$, 278 i.e. the diffraction-limited radius of the focal spot corresponding to a given 279 numerical aperture $NA$ and a light wavelength $\lambda$. 280 281 For very small $NA<10^\{-3\}$, diffraction effects becomes negligible and the Airy Radius becomes meaningless, 282 so in that case, a radius of 0 is returned. 283 284 Parameters 285 ---------- 286 Wavelength : float 287 Light wavelength in mm. 288 289 NumericalAperture : float 290 291 Returns 292 ------- 293 AiryRadius : float 294 """ 295 if NumericalAperture > 1e-3 and Wavelength is not None: 296 return 1.22 * 0.5 * Wavelength / NumericalAperture 297 else: 298 return 0 # for very small numerical apertures, diffraction effects becomes negligible and the Airy Radius becomes meaningless 299 300 301def GetPlaneWaveFocus(OpticalChain, Detector = "Focus", size=None, Nrays=1000, resolution=100): 302 """ 303 This function calculates the approximate polychromatic focal spot of a set of rays. 304 To do so, it positions itself in the detector plane and samples a square area. 305 If the size of the area is not given, it will use twice the Airy radius of the system with the largest wavbelength. 306 Otherwise it uses the size. 307 The resolution of the sampling is given by the resolution parameter. So it samples on a grid resolution x resolution. 308 309 To calculate the intensity, it takes Nrays out of the raylist (to subsample if needed). 310 It assimilates every ray to a plane wave, so it calculates a k-vector for each ray (taking into account the wavelength of the ray). 311 It calculates the phase of each ray from the delay and from the intersection position with the detector. 312 The delay from the non-central position is simply sin(alpha)*distance/c, where alpha is the angle between the ray and the normal to the detector 313 and distance is the distance between the intersection point and the central point of the detector. 314 It then calculates the intensity at each point of the grid by summing the intensity of each plane wave. 315 316 As long as there are not too many rays, this method is faster than doing an FFT. 317 It's also naturally polychromatic. 318 """ 319 if isinstance(Detector, str): 320 Detector = OpticalChain.detectors[Detector] 321 Index = Detector.index 322 RayList = OpticalChain.get_output_rays()[Index] 323 if size is None: 324 Wavelengths = [Ray.wavelength for Ray in RayList] 325 Wavelength = max(Wavelengths) 326 NumericalAperture = GetNumericalAperture(RayList) 327 size = 3 * GetAiryRadius(Wavelength, NumericalAperture) 328 X = np.linspace(-size / 2, size / 2, resolution) 329 Y = np.linspace(-size / 2, size / 2, resolution) 330 X, Y = np.meshgrid(X, Y) 331 # We pick Nrays. if there are less than Nrays, we take all of them. 332 PickedRaysGlobal = np.random.choice(RayList, min(Nrays, len(RayList)), replace=False) 333 # We calculate the k-vector of each ray, taking into account the wavelength of the ray. 334 # The units should be in mm^-1 335 PickedRays = [Ray.to_basis(*Detector.basis) for Ray in PickedRaysGlobal] 336 # The rays are now in the reference plane of the detector whose normal is [0,0,1] 337 wavelengths = np.array([Ray.wavelength for Ray in PickedRays]) 338 vectors = np.array([Ray.vector for Ray in PickedRays]) 339 frequencies = np.array([LightSpeed / Ray.wavelength for Ray in PickedRays]) # mm/s / mm = 1/s 340 k_vectors = 2*np.pi * vectors / wavelengths[:, np.newaxis] 341 angles = np.arccos(np.clip(np.dot(vectors, np.array([0, 0, 1])), -1.0, 1.0)) 342 # We calculate the intersection of the rays with the detector plane 343 Intersections = mgeo.IntersectionRayListZPlane(PickedRays)[0] 344 distances = (Intersections - mgeo.Origin[:2]).norm 345 # We calculate the phase of each ray 346 PathDelays = np.array(Detector.get_Delays(PickedRaysGlobal)) 347 PositionDelays = np.sum(np.array(Intersections._add_dimension()-mgeo.Origin)*vectors, axis=1) / LightSpeed * 1e15 348 Delays = (PathDelays+PositionDelays) / 1e15 349 # We have the delays in fs, we can now calculate the phase, taking into account the wavelength of each ray 350 Phases = np.array([2 * np.pi * frequencies[i] * Delays[i] for i in range(len(Delays))]) 351 # We also need the intensity of each ray 352 RayIntensities = np.array([Ray.intensity for Ray in PickedRays]) 353 # We can now calculate the intensity at each point of the grid 354 Intensity = np.zeros((resolution, resolution)) 355 for i in range(len(PickedRays)): 356 Intensity += RayIntensities[i] * np.cos(k_vectors[i][0] * X + k_vectors[i][1] * Y + Phases[i]) 357 358 return X, Y, Intensity 359 360moc.getPlaneWaveFocus = GetPlaneWaveFocus 361 362def GetDiffractionFocus(OpticalChain, Detector = "Focus", size=None, Nrays=1000, resolution=100): 363 """ 364 This function calculates the approximate polychromatic focal spot of a set of rays. 365 To do so, it positions itself in the detector plane and samples a square area. 366 If the size of the area is not given, it will use twice the Airy radius of the system with the largest wavbelength. 367 Otherwise it uses the size. 368 The resolution of the sampling is given by the resolution parameter. So it samples on a grid resolution x resolution. 369 370 To calculate the intensity, it takes Nrays out of the raylist (to subsample if needed). 371 It assimilates every ray to a plane wave, so it calculates a k-vector for each ray (taking into account the wavelength of the ray). 372 It considers that all the rays are intersecting the detector in the middle and doesn't take into account their phase. 373 374 So it returns the best case scenario for a diffraction limited focus with that numerical aperture 375 """ 376 if isinstance(Detector, str): 377 Detector = OpticalChain.detectors[Detector] 378 Index = Detector.index 379 RayList = OpticalChain.get_output_rays()[Index] 380 if size is None: 381 Wavelengths = [Ray.wavelength for Ray in RayList] 382 Wavelength = max(Wavelengths) 383 NumericalAperture = GetNumericalAperture(RayList) 384 size = 3 * GetAiryRadius(Wavelength, NumericalAperture) 385 X = np.linspace(-size / 2, size / 2, resolution) 386 Y = np.linspace(-size / 2, size / 2, resolution) 387 X, Y = np.meshgrid(X, Y) 388 # We pick Nrays. if there are less than Nrays, we take all of them. 389 PickedRaysGlobal = np.random.choice(RayList, min(Nrays, len(RayList)), replace=False) 390 # We calculate the k-vector of each ray, taking into account the wavelength of the ray. 391 # The units should be in mm^-1 392 PickedRays = [Ray.to_basis(*Detector.basis) for Ray in PickedRaysGlobal] 393 # The rays are now in the reference plane of the detector whose normal is [0,0,1] 394 wavelengths = np.array([Ray.wavelength for Ray in PickedRays]) 395 vectors = np.array([Ray.vector for Ray in PickedRays]) 396 frequencies = np.array([LightSpeed / Ray.wavelength for Ray in PickedRays]) # mm/s / mm = 1/s 397 k_vectors = 2*np.pi * vectors / wavelengths[:, np.newaxis] 398 angles = np.arccos(np.clip(np.dot(vectors, np.array([0, 0, 1])), -1.0, 1.0)) 399 # We calculate the intersection of the rays with the detector plane 400 Intersections = mgeo.IntersectionRayListZPlane(PickedRays)[0] 401 distances = (Intersections - mgeo.Origin[:2]).norm 402 # We also need the intensity of each ray 403 RayIntensities = np.array([Ray.intensity for Ray in PickedRays]) 404 # We can now calculate the intensity at each point of the grid 405 Intensity = np.zeros((resolution, resolution)) 406 for i in range(len(PickedRays)): 407 Intensity += RayIntensities[i] * np.cos(k_vectors[i][0] * X + k_vectors[i][1] * Y) 408 409 return X, Y, Intensity 410 411moc.getDiffractionFocus = GetDiffractionFocus 412 413# %% Asphericity analysis 414# This code is to do asphericity analysis of various surfaces 415# It calculates the closes sphere to the surface of an optical element 416# Then there are two functions. One that simply gives an asphericity value 417# and another one that actually plots the distance the the closest sphere 418# in much the same way as we plot the MirrorProjection 419 420def BestFitSphere(X,Y,Z): 421 """ 422 This function calculates the best sphere to fit a set of points. 423 It uses the least square method to find the center and the radius of the sphere. 424 Cite https://jekel.me/2015/Least-Squares-Sphere-Fit/ 425 """ 426 A = np.zeros((len(X),4)) 427 A[:,0] = X*2 428 A[:,1] = Y*2 429 A[:,2] = Z*2 430 A[:,3] = 1 431 432 # Assemble the f matrix 433 f = np.zeros((len(X),1)) 434 f[:,0] = X**2 + Y**2 + Z**2 435 C, residules, rank, singval = np.linalg.lstsq(A,f) 436 437 # solve for the radius 438 t = (C[0]*C[0])+(C[1]*C[1])+(C[2]*C[2])+C[3] 439 radius = math.sqrt(t) 440 441 return mgeo.Point(C[:3].flatten()),radius 442 443 444def GetClosestSphere(Mirror, Npoints=1000): 445 """ 446 This function calculates the closest sphere to the surface of a mirror. 447 It does so by sampling the surface of the mirror at Npoints points. 448 It then calculates the closest sphere to these points. 449 It returns the radius of the sphere and the center of the sphere. 450 """ 451 Points = mpm.sample_support(Mirror.support, Npoints=1000) 452 Points += Mirror.r0[:2] 453 Z = Mirror._zfunc(Points) 454 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 455 spX, spY, spZ = Points[:, 0], Points[:, 1], Points[:, 2] 456 Center, Radius = BestFitSphere(spX, spY, spZ) 457 return Center, Radius 458 459def GetAsphericity(Mirror, Npoints=1000): 460 """ 461 This function calculates the maximum distance of the mirror surface to the closest sphere. 462 """ 463 center, radius = GetClosestSphere(Mirror, Npoints) 464 Points = mpm.sample_support(Mirror.support, Npoints=1000) 465 Points += Mirror.r0[:2] 466 Z = Mirror._zfunc(Points) 467 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 468 Points_centered = Points - center 469 Distance = np.linalg.norm(Points_centered, axis=1) - radius 470 Distance*=1e3 # To convert to µm 471 return np.ptp(Distance) 472 473mmirror.Mirror.getClosestSphere = GetClosestSphere 474mmirror.Mirror.getAsphericity = GetAsphericity
47def GetETransmission(RayListIn, RayListOut) -> float: 48 """ 49 Calculates the energy transmission from RayListIn to RayListOut in percent by summing up the 50 intensity-property of the individual rays. 51 52 Parameters 53 ---------- 54 RayListIn : list(Ray) 55 List of incoming rays. 56 57 RayListOut : list(Ray) 58 List of outgoing rays. 59 60 Returns 61 ------- 62 ETransmission : float 63 """ 64 ETransmission = 100 * sum(Ray.intensity for Ray in RayListOut) / sum(Ray.intensity for Ray in RayListIn) 65 return ETransmission
Calculates the energy transmission from RayListIn to RayListOut in percent by summing up the intensity-property of the individual rays.
Parameters
RayListIn : list(Ray)
List of incoming rays.
RayListOut : list(Ray)
List of outgoing rays.
Returns
ETransmission : float
97def GetResultSummary(Detector, RayListAnalysed, verbose=False): 98 """ 99 Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation 100 for the given Detector and RayList. 101 If verbose, then also print a summary of the results for the given Detector. 102 103 Parameters 104 ---------- 105 Detector : Detector 106 An object of the ModuleDetector.Detector-class. 107 108 RayListAnalysed : list(Ray) 109 List of objects of the ModuleOpticalRay.Ray-class. 110 111 verbose : bool 112 Whether to print a result summary. 113 114 Returns 115 ------- 116 FocalSpotSizeSD : float 117 118 DurationSD : float 119 """ 120 DetectorPointList2DCentre = Detector.get_2D_points(RayListAnalysed) 121 FocalSpotSizeSD = mp.StandardDeviation(DetectorPointList2DCentre) 122 DelayList = Detector.get_Delays(RayListAnalysed) 123 DurationSD = mp.StandardDeviation(DelayList) 124 125 if verbose: 126 FocalSpotSize = mgeo.DiameterPointList(DetectorPointList2DCentre) 127 summarystring = ( 128 "At the detector distance of " 129 + "{:.3f}".format(Detector.get_distance()) 130 + " mm we get:\n" 131 + "Spatial std : " 132 + "{:.3f}".format(FocalSpotSizeSD * 1e3) 133 + " \u03BCm and min-max: " 134 + "{:.3f}".format(FocalSpotSize * 1e3) 135 + " \u03BCm\n" 136 + "Temporal std : " 137 + "{:.3e}".format(DurationSD) 138 + " fs and min-max : " 139 + "{:.3e}".format(max(DelayList) - min(DelayList)) 140 + " fs" 141 ) 142 143 print(summarystring) 144 145 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.
Parameters
Detector : Detector
An object of the ModuleDetector.Detector-class.
RayListAnalysed : list(Ray)
List of objects of the ModuleOpticalRay.Ray-class.
verbose : bool
Whether to print a result summary.
Returns
FocalSpotSizeSD : float
DurationSD : float
179def GetPulseProfile(Detector, RayList, Nbins=100): 180 """ 181 Retrieve the pulse profile from the given RayList and Detector. 182 The pulse profile is calculated by binning the delays of the rays in the RayList 183 at the Detector. The intensity of the rays is also taken into account. 184 185 Parameters 186 ---------- 187 Detector : Detector 188 An object of the ModuleDetector.Detector-class. 189 190 RayList : list(Ray) 191 List of objects of the ModuleOpticalRay.Ray-class. 192 193 Nbins : int 194 Number of bins to use for the histogram. 195 196 Returns 197 ------- 198 time : numpy.ndarray 199 The bins of the histogram. 200 201 intensity : numpy.ndarray 202 The histogram values. 203 """ 204 DelayList = Detector.get_Delays(RayList) 205 IntensityList = [Ray.intensity for Ray in RayList] 206 intensity, time_edges = np.histogram(DelayList, bins=Nbins, weights=IntensityList) 207 time = 0.5 * (time_edges[1:] + time_edges[:-1]) 208 return time, intensity
Retrieve the pulse profile from the given RayList and Detector. The pulse profile is calculated by binning the delays of the rays in the RayList at the Detector. The intensity of the rays is also taken into account.
Parameters
Detector : Detector
An object of the ModuleDetector.Detector-class.
RayList : list(Ray)
List of objects of the ModuleOpticalRay.Ray-class.
Nbins : int
Number of bins to use for the histogram.
Returns
time : numpy.ndarray
The bins of the histogram.
intensity : numpy.ndarray
The histogram values.
243def GetNumericalAperture(RayList: list[mray.Ray], RefractiveIndex: float = 1) -> float: 244 r""" 245 Returns the numerical aperture associated with the supplied ray-bundle 'Raylist'. 246 This is $n\sin\theta$, where $\theta$ is the maximum angle between any of the rays and the central ray, 247 and $n$ is the refractive index of the propagation medium. 248 249 Parameters 250 ---------- 251 RayList : list of Ray-object 252 The ray-bundle of which to determine the NA. 253 254 RefractiveIndex : float, optional 255 Refractive index of the propagation medium, defaults to =1. 256 257 Returns 258 ------- 259 NA : float 260 """ 261 CentralRay = mp.FindCentralRay(RayList) 262 if CentralRay is None: 263 CentralVector = np.array([0, 0, 0]) 264 for k in RayList: 265 CentralVector = CentralVector + k.vector 266 CentralVector = CentralVector / len(RayList) 267 else: 268 CentralVector = CentralRay.vector 269 ListAngleAperture = [] 270 for k in RayList: 271 ListAngleAperture.append(mgeo.AngleBetweenTwoVectors(CentralVector, k.vector)) 272 273 return np.sin(np.amax(ListAngleAperture)) * RefractiveIndex
Returns the numerical aperture associated with the supplied ray-bundle 'Raylist'. This is $n\sin\theta$, where $\theta$ is the maximum angle between any of the rays and the central ray, and $n$ is the refractive index of the propagation medium.
Parameters
RayList : list of Ray-object
The ray-bundle of which to determine the NA.
RefractiveIndex : float, optional
Refractive index of the propagation medium, defaults to =1.
Returns
NA : float
276def GetAiryRadius(Wavelength: float, NumericalAperture: float) -> float: 277 r""" 278 Returns the radius of the Airy disk: $r = 1.22 \frac\{\lambda\}\{NA\}$, 279 i.e. the diffraction-limited radius of the focal spot corresponding to a given 280 numerical aperture $NA$ and a light wavelength $\lambda$. 281 282 For very small $NA<10^\{-3\}$, diffraction effects becomes negligible and the Airy Radius becomes meaningless, 283 so in that case, a radius of 0 is returned. 284 285 Parameters 286 ---------- 287 Wavelength : float 288 Light wavelength in mm. 289 290 NumericalAperture : float 291 292 Returns 293 ------- 294 AiryRadius : float 295 """ 296 if NumericalAperture > 1e-3 and Wavelength is not None: 297 return 1.22 * 0.5 * Wavelength / NumericalAperture 298 else: 299 return 0 # for very small numerical apertures, diffraction effects becomes negligible and the Airy Radius becomes meaningless
Returns the radius of the Airy disk: $r = 1.22 \frac{\lambda}{NA}$, i.e. the diffraction-limited radius of the focal spot corresponding to a given numerical aperture $NA$ and a light wavelength $\lambda$.
For very small $NA<10^{-3}$, diffraction effects becomes negligible and the Airy Radius becomes meaningless, so in that case, a radius of 0 is returned.
Parameters
Wavelength : float
Light wavelength in mm.
NumericalAperture : float
Returns
AiryRadius : float
302def GetPlaneWaveFocus(OpticalChain, Detector = "Focus", size=None, Nrays=1000, resolution=100): 303 """ 304 This function calculates the approximate polychromatic focal spot of a set of rays. 305 To do so, it positions itself in the detector plane and samples a square area. 306 If the size of the area is not given, it will use twice the Airy radius of the system with the largest wavbelength. 307 Otherwise it uses the size. 308 The resolution of the sampling is given by the resolution parameter. So it samples on a grid resolution x resolution. 309 310 To calculate the intensity, it takes Nrays out of the raylist (to subsample if needed). 311 It assimilates every ray to a plane wave, so it calculates a k-vector for each ray (taking into account the wavelength of the ray). 312 It calculates the phase of each ray from the delay and from the intersection position with the detector. 313 The delay from the non-central position is simply sin(alpha)*distance/c, where alpha is the angle between the ray and the normal to the detector 314 and distance is the distance between the intersection point and the central point of the detector. 315 It then calculates the intensity at each point of the grid by summing the intensity of each plane wave. 316 317 As long as there are not too many rays, this method is faster than doing an FFT. 318 It's also naturally polychromatic. 319 """ 320 if isinstance(Detector, str): 321 Detector = OpticalChain.detectors[Detector] 322 Index = Detector.index 323 RayList = OpticalChain.get_output_rays()[Index] 324 if size is None: 325 Wavelengths = [Ray.wavelength for Ray in RayList] 326 Wavelength = max(Wavelengths) 327 NumericalAperture = GetNumericalAperture(RayList) 328 size = 3 * GetAiryRadius(Wavelength, NumericalAperture) 329 X = np.linspace(-size / 2, size / 2, resolution) 330 Y = np.linspace(-size / 2, size / 2, resolution) 331 X, Y = np.meshgrid(X, Y) 332 # We pick Nrays. if there are less than Nrays, we take all of them. 333 PickedRaysGlobal = np.random.choice(RayList, min(Nrays, len(RayList)), replace=False) 334 # We calculate the k-vector of each ray, taking into account the wavelength of the ray. 335 # The units should be in mm^-1 336 PickedRays = [Ray.to_basis(*Detector.basis) for Ray in PickedRaysGlobal] 337 # The rays are now in the reference plane of the detector whose normal is [0,0,1] 338 wavelengths = np.array([Ray.wavelength for Ray in PickedRays]) 339 vectors = np.array([Ray.vector for Ray in PickedRays]) 340 frequencies = np.array([LightSpeed / Ray.wavelength for Ray in PickedRays]) # mm/s / mm = 1/s 341 k_vectors = 2*np.pi * vectors / wavelengths[:, np.newaxis] 342 angles = np.arccos(np.clip(np.dot(vectors, np.array([0, 0, 1])), -1.0, 1.0)) 343 # We calculate the intersection of the rays with the detector plane 344 Intersections = mgeo.IntersectionRayListZPlane(PickedRays)[0] 345 distances = (Intersections - mgeo.Origin[:2]).norm 346 # We calculate the phase of each ray 347 PathDelays = np.array(Detector.get_Delays(PickedRaysGlobal)) 348 PositionDelays = np.sum(np.array(Intersections._add_dimension()-mgeo.Origin)*vectors, axis=1) / LightSpeed * 1e15 349 Delays = (PathDelays+PositionDelays) / 1e15 350 # We have the delays in fs, we can now calculate the phase, taking into account the wavelength of each ray 351 Phases = np.array([2 * np.pi * frequencies[i] * Delays[i] for i in range(len(Delays))]) 352 # We also need the intensity of each ray 353 RayIntensities = np.array([Ray.intensity for Ray in PickedRays]) 354 # We can now calculate the intensity at each point of the grid 355 Intensity = np.zeros((resolution, resolution)) 356 for i in range(len(PickedRays)): 357 Intensity += RayIntensities[i] * np.cos(k_vectors[i][0] * X + k_vectors[i][1] * Y + Phases[i]) 358 359 return X, Y, Intensity
This function calculates the approximate polychromatic focal spot of a set of rays. To do so, it positions itself in the detector plane and samples a square area. If the size of the area is not given, it will use twice the Airy radius of the system with the largest wavbelength. Otherwise it uses the size. The resolution of the sampling is given by the resolution parameter. So it samples on a grid resolution x resolution.
To calculate the intensity, it takes Nrays out of the raylist (to subsample if needed). It assimilates every ray to a plane wave, so it calculates a k-vector for each ray (taking into account the wavelength of the ray). It calculates the phase of each ray from the delay and from the intersection position with the detector. The delay from the non-central position is simply sin(alpha)*distance/c, where alpha is the angle between the ray and the normal to the detector and distance is the distance between the intersection point and the central point of the detector. It then calculates the intensity at each point of the grid by summing the intensity of each plane wave.
As long as there are not too many rays, this method is faster than doing an FFT. It's also naturally polychromatic.
363def GetDiffractionFocus(OpticalChain, Detector = "Focus", size=None, Nrays=1000, resolution=100): 364 """ 365 This function calculates the approximate polychromatic focal spot of a set of rays. 366 To do so, it positions itself in the detector plane and samples a square area. 367 If the size of the area is not given, it will use twice the Airy radius of the system with the largest wavbelength. 368 Otherwise it uses the size. 369 The resolution of the sampling is given by the resolution parameter. So it samples on a grid resolution x resolution. 370 371 To calculate the intensity, it takes Nrays out of the raylist (to subsample if needed). 372 It assimilates every ray to a plane wave, so it calculates a k-vector for each ray (taking into account the wavelength of the ray). 373 It considers that all the rays are intersecting the detector in the middle and doesn't take into account their phase. 374 375 So it returns the best case scenario for a diffraction limited focus with that numerical aperture 376 """ 377 if isinstance(Detector, str): 378 Detector = OpticalChain.detectors[Detector] 379 Index = Detector.index 380 RayList = OpticalChain.get_output_rays()[Index] 381 if size is None: 382 Wavelengths = [Ray.wavelength for Ray in RayList] 383 Wavelength = max(Wavelengths) 384 NumericalAperture = GetNumericalAperture(RayList) 385 size = 3 * GetAiryRadius(Wavelength, NumericalAperture) 386 X = np.linspace(-size / 2, size / 2, resolution) 387 Y = np.linspace(-size / 2, size / 2, resolution) 388 X, Y = np.meshgrid(X, Y) 389 # We pick Nrays. if there are less than Nrays, we take all of them. 390 PickedRaysGlobal = np.random.choice(RayList, min(Nrays, len(RayList)), replace=False) 391 # We calculate the k-vector of each ray, taking into account the wavelength of the ray. 392 # The units should be in mm^-1 393 PickedRays = [Ray.to_basis(*Detector.basis) for Ray in PickedRaysGlobal] 394 # The rays are now in the reference plane of the detector whose normal is [0,0,1] 395 wavelengths = np.array([Ray.wavelength for Ray in PickedRays]) 396 vectors = np.array([Ray.vector for Ray in PickedRays]) 397 frequencies = np.array([LightSpeed / Ray.wavelength for Ray in PickedRays]) # mm/s / mm = 1/s 398 k_vectors = 2*np.pi * vectors / wavelengths[:, np.newaxis] 399 angles = np.arccos(np.clip(np.dot(vectors, np.array([0, 0, 1])), -1.0, 1.0)) 400 # We calculate the intersection of the rays with the detector plane 401 Intersections = mgeo.IntersectionRayListZPlane(PickedRays)[0] 402 distances = (Intersections - mgeo.Origin[:2]).norm 403 # We also need the intensity of each ray 404 RayIntensities = np.array([Ray.intensity for Ray in PickedRays]) 405 # We can now calculate the intensity at each point of the grid 406 Intensity = np.zeros((resolution, resolution)) 407 for i in range(len(PickedRays)): 408 Intensity += RayIntensities[i] * np.cos(k_vectors[i][0] * X + k_vectors[i][1] * Y) 409 410 return X, Y, Intensity
This function calculates the approximate polychromatic focal spot of a set of rays. To do so, it positions itself in the detector plane and samples a square area. If the size of the area is not given, it will use twice the Airy radius of the system with the largest wavbelength. Otherwise it uses the size. The resolution of the sampling is given by the resolution parameter. So it samples on a grid resolution x resolution.
To calculate the intensity, it takes Nrays out of the raylist (to subsample if needed). It assimilates every ray to a plane wave, so it calculates a k-vector for each ray (taking into account the wavelength of the ray). It considers that all the rays are intersecting the detector in the middle and doesn't take into account their phase.
So it returns the best case scenario for a diffraction limited focus with that numerical aperture
421def BestFitSphere(X,Y,Z): 422 """ 423 This function calculates the best sphere to fit a set of points. 424 It uses the least square method to find the center and the radius of the sphere. 425 Cite https://jekel.me/2015/Least-Squares-Sphere-Fit/ 426 """ 427 A = np.zeros((len(X),4)) 428 A[:,0] = X*2 429 A[:,1] = Y*2 430 A[:,2] = Z*2 431 A[:,3] = 1 432 433 # Assemble the f matrix 434 f = np.zeros((len(X),1)) 435 f[:,0] = X**2 + Y**2 + Z**2 436 C, residules, rank, singval = np.linalg.lstsq(A,f) 437 438 # solve for the radius 439 t = (C[0]*C[0])+(C[1]*C[1])+(C[2]*C[2])+C[3] 440 radius = math.sqrt(t) 441 442 return mgeo.Point(C[:3].flatten()),radius
This function calculates the best sphere to fit a set of points. It uses the least square method to find the center and the radius of the sphere. Cite https://jekel.me/2015/Least-Squares-Sphere-Fit/
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.