The API may be updated without warning
API stuff TODO
This is the multi-page printable view of this section. Click here to print.
The API may be updated without warning
API stuff TODO
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 wavelength. 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.
RayListIn : list(Ray)
List of incoming rays.
RayListOut : list(Ray)
List of outgoing rays.
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.
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.
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.
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.
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.
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.
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.
Wavelength : float
Light wavelength in mm.
NumericalAperture : float
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 wavelength. 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 wavelength. 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.
Provides functions for analysis of the output ray-bundles calculated through ray-tracing, and also for the ART's standard visualization options.
Implements the following functions: - DrawSpotDiagram: Produces an interactive figure with the spot diagram on the selected Detector. - DrawDelaySpots: Produces a an interactive figure with a spot diagram resulting from the RayListAnalysed - _drawDelayGraph: Auxiliary function for DelayGraph. - DrawMirrorProjection: Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. - DrawSetup: Renders an image of the Optical setup and the traced rays. - DrawAsphericity: Displays a map of the asphericity of the mirror. - DrawCaustics: Displays the caustics of the rays on the detector.
It also implements the following methods: -OpticalChain: - render: Method to render the optical setup. - drawSpotDiagram: Method to draw the spot diagram on the selected detector. - drawDelaySpots: Method to draw the delay spots on the detector. - drawMirrorProjection: Method to draw the mirror projection. - drawCaustics: Method to draw the caustics on the detector. -Mirror: - drawAsphericity: Method to draw the asphericity of the mirror.
Created in Apr 2020
@author: Anthony Guillaume + Stefan Haessler + Andre Kalouguine
1""" 2Provides functions for analysis of the output ray-bundles calculated through ray-tracing, and also for the ART's standard visualization options. 3 4Implements the following functions: 5 - DrawSpotDiagram: Produces an interactive figure with the spot diagram on the selected Detector. 6 - DrawDelaySpots: Produces a an interactive figure with a spot diagram resulting from the RayListAnalysed 7 - _drawDelayGraph: Auxiliary function for DelayGraph. 8 - DrawMirrorProjection: Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. 9 - DrawSetup: Renders an image of the Optical setup and the traced rays. 10 - DrawAsphericity: Displays a map of the asphericity of the mirror. 11 - DrawCaustics: Displays the caustics of the rays on the detector. 12 13It also implements the following methods: 14 -OpticalChain: 15 - render: Method to render the optical setup. 16 - drawSpotDiagram: Method to draw the spot diagram on the selected detector. 17 - drawDelaySpots: Method to draw the delay spots on the detector. 18 - drawMirrorProjection: Method to draw the mirror projection. 19 - drawCaustics: Method to draw the caustics on the detector. 20 -Mirror: 21 - drawAsphericity: Method to draw the asphericity of the mirror. 22 23 24Created in Apr 2020 25 26@author: Anthony Guillaume + Stefan Haessler + Andre Kalouguine 27""" 28# %% Module imports 29import numpy as np 30import matplotlib.pyplot as plt 31from mpl_toolkits.mplot3d import Axes3D 32import pyvista as pv 33import pyvistaqt as pvqt 34import colorcet as cc 35from colorsys import rgb_to_hls, hls_to_rgb 36import logging 37 38logger = logging.getLogger(__name__) 39 40 41import ARTcore.ModuleProcessing as mp 42import ARTcore.ModuleGeometry as mgeo 43import ARTcore.ModuleDetector as mdet 44import ARTcore.ModuleMirror as mmirror 45import ART.ModulePlottingMethods as mpm 46import ART.ModulePlottingUtilities as mpu 47from ART.ModulePlottingUtilities import Observable 48 49import ARTcore.ModuleOpticalChain as moc 50import ART.ModuleAnalysis as man 51import itertools 52from copy import copy 53 54 55# %% Spot diagram on detector 56def DrawSpotDiagram(OpticalChain, 57 Detector = "Focus", 58 DrawAiryAndFourier=False, 59 DrawFocalContour=False, 60 DrawFocal=False, 61 ColorCoded=None, 62 Observer = None) -> plt.Figure: 63 """ 64 Produce an interactive figure with the spot diagram on the selected Detector. 65 The detector distance can be shifted with the left-right cursor keys. Doing so will actually move the detector. 66 If DrawAiryAndFourier is True, a circle with the Airy-spot-size will be shown. 67 If DrawFocalContour is True, the focal contour calculated from some of the rays will be shown. 68 If DrawFocal is True, a heatmap calculated from some of the rays will be shown. 69 The 'spots' can optionally be color-coded by specifying ColorCoded, which can be one of ["Intensity","Incidence","Delay"]. 70 71 Parameters 72 ---------- 73 RayListAnalysed : list(Ray) 74 List of objects of the ModuleOpticalRay.Ray-class. 75 76 Detector : Detector or str, optional 77 An object of the ModuleDetector.Detector-class or the name of the detector. The default is "Focus". 78 79 DrawAiryAndFourier : bool, optional 80 Whether to draw a circle with the Airy-spot-size. The default is False. 81 82 DrawFocalContour : bool, optional 83 Whether to draw the focal contour. The default is False. 84 85 DrawFocal : bool, optional 86 Whether to draw the focal heatmap. The default is False. 87 88 ColorCoded : str, optional 89 Color-code the spots according to one of ["Intensity","Incidence","Delay"]. The default is None. 90 91 Observer : Observer, optional 92 An observer object. If none, then we just create a copy of the detector and move it when pressing left-right. 93 However, if an observer is specified, then we will change the value of the observer and it will issue 94 the required callbacks to update several plots at the same time. 95 96 Returns 97 ------- 98 fig : matlplotlib-figure-handle. 99 Shows the interactive figure. 100 """ 101 if isinstance(Detector, str): 102 Detector = OpticalChain.detectors[Detector] 103 Index = Detector.index 104 movingDetector = copy(Detector) # We will move this detector when pressing left-right 105 if Observer is None: 106 detectorPosition = Observable(movingDetector.distance) # We will observe the distance of this detector 107 else: 108 detectorPosition = Observer 109 movingDetector.distance = detectorPosition.value 110 111 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 112 113 RayListAnalysed = OpticalChain.get_output_rays()[Index] 114 115 NumericalAperture = man.GetNumericalAperture(RayListAnalysed, 1) # NA determined from final ray bundle 116 MaxWavelength = np.max([i.wavelength for i in RayListAnalysed]) 117 if DrawAiryAndFourier: 118 AiryRadius = man.GetAiryRadius(MaxWavelength, NumericalAperture) * 1e3 # in µm 119 else: 120 AiryRadius = 0 121 122 if DrawFocalContour or DrawFocal: 123 X,Y,Z = man.GetDiffractionFocus(OpticalChain, movingDetector, Index) 124 Z/=np.max(Z) 125 126 DectectorPoint2D_Xcoord, DectectorPoint2D_Ycoord, FocalSpotSize, SpotSizeSD = mpu._getDetectorPoints( 127 RayListAnalysed, movingDetector 128 ) 129 130 match ColorCoded: 131 case "Intensity": 132 IntensityList = [k.intensity for k in RayListAnalysed] 133 z = np.asarray(IntensityList) 134 zlabel = "Intensity (arb.u.)" 135 title = "Intensity + Spot Diagram\n press left/right to move detector position" 136 addLine = "" 137 case "Incidence": 138 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # degree 139 z = np.asarray(IncidenceList) 140 zlabel = "Incidence angle (deg)" 141 title = "Ray Incidence + Spot Diagram\n press left/right to move detector position" 142 addLine = "" 143 case "Delay": 144 DelayList = movingDetector.get_Delays(RayListAnalysed) 145 IntensityList = [k.intensity for k in RayListAnalysed] 146 DurationSD = mp.WeightedStandardDeviation(DelayList,IntensityList) 147 #DurationSD = mp.StandardDeviation(DelayList) 148 z = np.asarray(DelayList) 149 zlabel = "Delay (fs)" 150 title = "Delay + Spot Diagram\n press left/right to move detector position" 151 addLine = "\n" + "{:.2f}".format(DurationSD) + " fs SD" 152 case _: 153 z = "red" 154 title = "Spot Diagram\n press left/right to move detector position" 155 addLine = "" 156 157 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 158 159 plt.ion() 160 fig, ax = plt.subplots() 161 if DrawFocal: 162 focal = ax.pcolormesh(X*1e3,Y*1e3,Z) 163 if DrawFocalContour: 164 levels = [1/np.e**2, 0.5] 165 contour = ax.contourf(X*1e3, Y*1e3, Z, levels=levels, cmap='gray') 166 167 if DrawAiryAndFourier: 168 theta = np.linspace(0, 2 * np.pi, 100) 169 x = AiryRadius * np.cos(theta) 170 y = AiryRadius * np.sin(theta) # 171 ax.plot(x, y, c="black") 172 173 174 foo = ax.scatter( 175 DectectorPoint2D_Xcoord, 176 DectectorPoint2D_Ycoord, 177 c=z, 178 s=15, 179 label="{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(SpotSizeSD * 1e3) + " \u03BCm SD" + addLine, 180 ) 181 182 axisLim = 1.1 * max(AiryRadius, 0.5 * FocalSpotSize * 1000) 183 ax.set_xlim(-axisLim, axisLim) 184 ax.set_ylim(-axisLim, axisLim) 185 186 if ColorCoded == "Intensity" or ColorCoded == "Incidence" or ColorCoded == "Delay": 187 cbar = fig.colorbar(foo) 188 cbar.set_label(zlabel) 189 190 ax.legend(loc="upper right") 191 ax.set_xlabel("X (µm)") 192 ax.set_ylabel("Y (µm)") 193 ax.set_title(title) 194 # ax.margins(x=0) 195 196 197 def update_plot(new_value): 198 nonlocal movingDetector, ColorCoded, zlabel, cbar, detectorPosition, foo, distStep, focal, contour, levels, Index, RayListAnalysed 199 200 newDectectorPoint2D_Xcoord, newDectectorPoint2D_Ycoord, newFocalSpotSize, newSpotSizeSD = mpu._getDetectorPoints( 201 RayListAnalysed, movingDetector 202 ) 203 204 if DrawFocal: 205 focal.set_array(Z) 206 if DrawFocalContour: 207 levels = [1/np.e**2, 0.5] 208 for coll in contour.collections: 209 coll.remove() # Remove old contour lines 210 contour = ax.contourf(X * 1e3, Y * 1e3, Z, levels=levels, cmap='gray') 211 212 xy = foo.get_offsets() 213 xy[:, 0] = newDectectorPoint2D_Xcoord 214 xy[:, 1] = newDectectorPoint2D_Ycoord 215 foo.set_offsets(xy) 216 217 218 if ColorCoded == "Delay": 219 newDelayList = np.asarray(movingDetector.get_Delays(RayListAnalysed)) 220 newDurationSD = mp.WeightedStandardDeviation(newDelayList,IntensityList) 221 #newDurationSD = mp.StandardDeviation(newDelayList) 222 newaddLine = "\n" + "{:.2f}".format(newDurationSD) + " fs SD" 223 foo.set_array(newDelayList) 224 foo.set_clim(min(newDelayList), max(newDelayList)) 225 cbar.update_normal(foo) 226 else: 227 newaddLine = "" 228 229 foo.set_label( 230 "{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(newSpotSizeSD * 1e3) + " \u03BCm SD" + newaddLine 231 ) 232 ax.legend(loc="upper right") 233 234 axisLim = 1.1 * max(AiryRadius, 0.5 * newFocalSpotSize * 1000) 235 ax.set_xlim(-axisLim, axisLim) 236 ax.set_ylim(-axisLim, axisLim) 237 238 distStep = min( 239 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 240 ) # in mm 241 242 fig.canvas.draw_idle() 243 244 245 def press(event): 246 nonlocal detectorPosition, distStep 247 if event.key == "right": 248 detectorPosition.value += distStep 249 elif event.key == "left": 250 if detectorPosition.value > 1.5 * distStep: 251 detectorPosition.value -= distStep 252 else: 253 detectorPosition.value = 0.5 * distStep 254 else: 255 return None 256 257 fig.canvas.mpl_connect("key_press_event", press) 258 259 plt.show() 260 261 detectorPosition.register(update_plot) 262 263 264 return fig, detectorPosition 265 266moc.OpticalChain.drawSpotDiagram = DrawSpotDiagram 267 268# %% Delay graph on detector (3D spot diagram) 269def DrawDelaySpots(OpticalChain, 270 DeltaFT: tuple[int, float], 271 Detector = "Focus", 272 DrawAiryAndFourier=False, 273 ColorCoded=None, 274 Observer = None 275 ) -> plt.Figure: 276 """ 277 Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed 278 hitting the Detector, with the ray-delays shown in the 3rd dimension. 279 The detector distance can be shifted with the left-right cursor keys. 280 If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and 281 whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'. 282 283 The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"]. 284 285 Parameters 286 ---------- 287 RayListAnalysed : list(Ray) 288 List of objects of the ModuleOpticalRay.Ray-class. 289 290 Detector : Detector 291 An object of the ModuleDetector.Detector-class. 292 293 DeltaFT : (int, float) 294 The Fourier-limited pulse duration. Just used as a reference to compare the temporal spread 295 induced by the ray-delays. 296 297 DrawAiryAndFourier : bool, optional 298 Whether to draw a cylinder showing the Airy-spot-size and Fourier-limited-duration. 299 The default is False. 300 301 ColorCoded : str, optional 302 Color-code the spots according to one of ["Intensity","Incidence"]. 303 The default is None. 304 305 Returns 306 ------- 307 fig : matlplotlib-figure-handle. 308 Shows the interactive figure. 309 """ 310 if isinstance(Detector, str): 311 Det = OpticalChain.detectors[Detector] 312 else: 313 Det = Detector 314 Index = Det.index 315 Detector = copy(Det) 316 if Observer is None: 317 detectorPosition = Observable(Detector.distance) 318 else: 319 detectorPosition = Observer 320 Detector.distance = detectorPosition.value 321 322 RayListAnalysed = OpticalChain.get_output_rays()[Index] 323 fig, NumericalAperture, AiryRadius, FocalSpotSize = _drawDelayGraph( 324 RayListAnalysed, Detector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded 325 ) 326 327 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 328 329 movingDetector = copy(Detector) 330 331 def update_plot(new_value): 332 nonlocal movingDetector, ColorCoded, detectorPosition, distStep, fig 333 ax = fig.axes[0] 334 cam = [ax.azim, ax.elev, ax._dist] 335 fig, sameNumericalAperture, sameAiryRadius, newFocalSpotSize = _drawDelayGraph( 336 RayListAnalysed, movingDetector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded, fig 337 ) 338 ax = fig.axes[0] 339 ax.azim, ax.elev, ax._dist = cam 340 distStep = min( 341 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 342 ) 343 return fig 344 345 def press(event): 346 nonlocal detectorPosition, distStep, movingDetector, fig 347 if event.key == "right": 348 detectorPosition.value += distStep 349 elif event.key == "left": 350 if detectorPosition.value > 1.5 * distStep: 351 detectorPosition.value -= distStep 352 else: 353 detectorPosition.value = 0.5 * distStep 354 355 fig.canvas.mpl_connect("key_press_event", press) 356 detectorPosition.register(update_plot) 357 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 358 359 return fig, Observable 360 361 362def _drawDelayGraph(RayListAnalysed, Detector, Distance, DeltaFT, DrawAiryAndFourier=False, ColorCoded=None, fig=None): 363 """ 364 Draws the 3D-delay-spot-diagram for a fixed detector. See more doc in the function below. 365 """ 366 NumericalAperture = man.GetNumericalAperture(RayListAnalysed, 1) # NA determined from final ray bundle 367 Wavelength = RayListAnalysed[0].wavelength 368 AiryRadius = man.GetAiryRadius(Wavelength, NumericalAperture) * 1e3 # in µm 369 370 DectectorPoint2D_Xcoord, DectectorPoint2D_Ycoord, FocalSpotSize, SpotSizeSD = mpu._getDetectorPoints( 371 RayListAnalysed, Detector 372 ) 373 374 DelayList = Detector.get_Delays(RayListAnalysed) 375 DurationSD = mp.StandardDeviation(DelayList) 376 377 if ColorCoded == "Intensity": 378 IntensityList = [k.intensity for k in RayListAnalysed] 379 elif ColorCoded == "Incidence": 380 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # in degree 381 382 plt.ion() 383 if fig is None: 384 fig = plt.figure() 385 else: 386 fig.clear(keep_observers=True) 387 388 ax = Axes3D(fig) 389 fig.add_axes(ax) 390 ax.set_xlabel("X (µm)") 391 ax.set_ylabel("Y (µm)") 392 ax.set_zlabel("Delay (fs)") 393 394 labelLine = ( 395 "{:.3f}".format(Distance) 396 + " mm\n" 397 + "{:.1f}".format(SpotSizeSD * 1e3) 398 + " \u03BCm SD\n" 399 + "{:.2f}".format(DurationSD) 400 + " fs SD" 401 ) 402 if ColorCoded == "Intensity": 403 ax.scatter(DectectorPoint2D_Xcoord, DectectorPoint2D_Ycoord, DelayList, s=4, c=IntensityList, label=labelLine) 404 # plt.title('Delay + Intensity graph\n press left/right to move detector position') 405 fig.suptitle("Delay + Intensity graph\n press left/right to move detector position") 406 407 elif ColorCoded == "Incidence": 408 ax.scatter(DectectorPoint2D_Xcoord, DectectorPoint2D_Ycoord, DelayList, s=4, c=IncidenceList, label=labelLine) 409 # plt.title('Delay + Incidence graph\n press left/right to move detector position') 410 fig.suptitle("Delay + Incidence graph\n press left/right to move detector position") 411 else: 412 ax.scatter(DectectorPoint2D_Xcoord, DectectorPoint2D_Ycoord, DelayList, s=4, c=DelayList, label=labelLine) 413 # plt.title('Delay graph\n press left/right to move detector position') 414 fig.suptitle("Delay graph\n press left/right to move detector position") 415 416 ax.legend(loc="upper right") 417 418 if DrawAiryAndFourier: 419 x = np.linspace(-AiryRadius, AiryRadius, 40) 420 z = np.linspace(np.mean(DelayList) - DeltaFT * 0.5, np.mean(DelayList) + DeltaFT * 0.5, 40) 421 x, z = np.meshgrid(x, z) 422 y = np.sqrt(AiryRadius**2 - x**2) 423 ax.plot_wireframe(x, y, z, color="grey", alpha=0.1) 424 ax.plot_wireframe(x, -y, z, color="grey", alpha=0.1) 425 426 axisLim = 1.1 * max(AiryRadius, 0.5 * FocalSpotSize * 1000) 427 ax.set_xlim(-axisLim, axisLim) 428 ax.set_ylim(-axisLim, axisLim) 429 #ax.set_zlim(-axisLim/3*10, axisLim/3*10) #same scaling as spatial axes 430 ax.set_zlim(-DeltaFT, DeltaFT) 431 432 plt.show() 433 fig.canvas.draw() 434 435 return fig, NumericalAperture, AiryRadius, FocalSpotSize 436 437moc.OpticalChain.drawDelaySpots = DrawDelaySpots 438 439# %% Mirror projection 440def DrawMirrorProjection(OpticalChain, ReflectionNumber: int, ColorCoded=None, Detector="") -> plt.Figure: 441 """ 442 Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. 443 The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is 444 measured at the Detector. 445 446 Parameters 447 ---------- 448 OpticalChain : OpticalChain 449 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 450 451 ReflectionNumber : int 452 Index specifying the optical element on which you want to see the impact points. 453 454 Detector : Detector, optional 455 Object of the ModuleDetector.Detector-class. Only necessary to project delays. The default is None. 456 457 ColorCoded : str, optional 458 Specifies which ray property to color-code: ["Incidence","Intensity","Delay"]. The default is None. 459 460 Returns 461 ------- 462 fig : matlplotlib-figure-handle. 463 Shows the figure. 464 """ 465 from mpl_toolkits.axes_grid1 import make_axes_locatable 466 if isinstance(Detector, str): 467 if Detector == "": 468 Detector = None 469 else: 470 Detector = OpticalChain.detectors[Detector] 471 472 Position = OpticalChain[ReflectionNumber].position 473 q = OpticalChain[ReflectionNumber].orientation 474 # n = OpticalChain.optical_elements[ReflectionNumber].normal 475 # m = OpticalChain.optical_elements[ReflectionNumber].majoraxis 476 477 RayListAnalysed = OpticalChain.get_output_rays()[ReflectionNumber] 478 # transform rays into the mirror-support reference frame 479 # (same as mirror frame but without the shift by mirror-centre) 480 r0 = OpticalChain[ReflectionNumber].r0 481 RayList = [r.to_basis(*OpticalChain[ReflectionNumber].basis) for r in RayListAnalysed] 482 483 x = np.asarray([k.point[0] for k in RayList]) - r0[0] 484 y = np.asarray([k.point[1] for k in RayList]) - r0[1] 485 if ColorCoded == "Intensity": 486 IntensityList = [k.intensity for k in RayListAnalysed] 487 z = np.asarray(IntensityList) 488 zlabel = "Intensity (arb.u.)" 489 title = "Ray intensity projected on mirror " 490 elif ColorCoded == "Incidence": 491 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # in degree 492 z = np.asarray(IncidenceList) 493 zlabel = "Incidence angle (deg)" 494 title = "Ray incidence projected on mirror " 495 elif ColorCoded == "Delay": 496 if Detector is not None: 497 z = np.asarray(Detector.get_Delays(RayListAnalysed)) 498 zlabel = "Delay (fs)" 499 title = "Ray delay at detector projected on mirror " 500 else: 501 raise ValueError("If you want to project ray delays, you must specify a detector.") 502 else: 503 z = "red" 504 title = "Ray impact points projected on mirror" 505 506 plt.ion() 507 fig = plt.figure() 508 ax = OpticalChain.optical_elements[ReflectionNumber].support._ContourSupport(fig) 509 p = plt.scatter(x, y, c=z, s=15) 510 if ColorCoded == "Delay" or ColorCoded == "Incidence" or ColorCoded == "Intensity": 511 divider = make_axes_locatable(ax) 512 cax = divider.append_axes("right", size="5%", pad=0.05) 513 cbar = fig.colorbar(p, cax=cax) 514 cbar.set_label(zlabel) 515 ax.set_xlabel("x (mm)") 516 ax.set_ylabel("y (mm)") 517 plt.title(title, loc="right") 518 plt.tight_layout() 519 520 bbox = ax.get_position() 521 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 522 ax.set_position(bbox) 523 plt.show() 524 525 return fig 526 527 528moc.OpticalChain.drawMirrorProjection = DrawMirrorProjection 529 530# %% Setup rendering 531def DrawSetup(OpticalChain, 532 EndDistance=None, 533 maxRays=300, 534 OEpoints=2000, 535 draw_mesh=False, 536 cycle_ray_colors = False, 537 impact_points = False, 538 DrawDetectors=True, 539 DetectedRays = False, 540 Observers = dict()): 541 """ 542 Renders an image of the Optical setup and the traced rays. 543 544 Parameters 545 ---------- 546 OpticalChain : OpticalChain 547 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 548 549 EndDistance : float, optional 550 The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). If not specified, 551 this distance is set to that between the source point and the 1st optical element. 552 553 maxRays: int 554 The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog 555 and not required for a nice image. Default is 150. 556 557 OEpoints : int 558 How many little spheres to draw to represent the optical elements. Default is 2000. 559 560 Returns 561 ------- 562 fig : Pyvista-figure-handle. 563 Shows the figure. 564 """ 565 566 RayListHistory = [OpticalChain.source_rays] + OpticalChain.get_output_rays() 567 568 if EndDistance is None: 569 EndDistance = np.linalg.norm(OpticalChain.source_rays[0].point - OpticalChain.optical_elements[0].position) 570 571 print("...rendering image of optical chain...", end="", flush=True) 572 fig = pvqt.BackgroundPlotter(window_size=(1500, 500), notebook=False) # Opening a window 573 fig.set_background('white') 574 575 if cycle_ray_colors: 576 colors = mpu.generate_distinct_colors(len(OpticalChain)+1) 577 else: 578 colors = [[0.7, 0, 0]]*(len(OpticalChain)+1) # Default color: dark red 579 580 # Optics display 581 # For each optic we will send the figure to the function _RenderOpticalElement and it will add the optic to the figure 582 for i,OE in enumerate(OpticalChain.optical_elements): 583 color = pv.Color(colors[i+1]) 584 rgb = color.float_rgb 585 h, l, s = rgb_to_hls(*rgb) 586 s = max(0, min(1, s * 0.3)) # Decrease saturation 587 l = max(0, min(1, l + 0.1)) # Increase lightness 588 new_rgb = hls_to_rgb(h, l, s) 589 darkened_color = pv.Color(new_rgb) 590 mpm._RenderOpticalElement(fig, OE, OEpoints, draw_mesh, darkened_color, index=i) 591 ray_meshes = mpm._RenderRays(RayListHistory, EndDistance, maxRays) 592 for i,ray in enumerate(ray_meshes): 593 color = pv.Color(colors[i]) 594 fig.add_mesh(ray, color=color, name=f"RayBundle_{i}") 595 if impact_points: 596 for i,rays in enumerate(RayListHistory): 597 points = np.array([list(r.point) for r in rays], dtype=np.float32) 598 points = pv.PolyData(points) 599 color = pv.Color(colors[i-1]) 600 fig.add_mesh(points, color=color, point_size=5, name=f"RayImpactPoints_{i}") 601 602 detector_copies = {key: copy(OpticalChain.detectors[key]) for key in OpticalChain.detectors.keys()} 603 detector_meshes_list = [] 604 detectedpoint_meshes = dict() 605 606 if OpticalChain.detectors is not None and DrawDetectors: 607 # Detector display 608 for key in OpticalChain.detectors.keys(): 609 det = detector_copies[key] 610 index = OpticalChain.detectors[key].index 611 if key in Observers: 612 det.distance = Observers[key].value 613 #Observers[key].register_calculation(lambda x: det.set_distance(x)) 614 mpm._RenderDetector(fig, det, name = key, detector_meshes = detector_meshes_list) 615 if DetectedRays: 616 RayListAnalysed = OpticalChain.get_output_rays()[index] 617 points = det.get_3D_points(RayListAnalysed) 618 points = pv.PolyData(points) 619 detectedpoint_meshes[key] = points 620 fig.add_mesh(points, color='purple', point_size=5, name=f"DetectedRays_{key}") 621 detector_meshes = dict(zip(OpticalChain.detectors.keys(), detector_meshes_list)) 622 623 # Now we define a function that will move on the plot the detector with name "detname" when it's called 624 def move_detector(detname, new_value): 625 nonlocal fig, detector_meshes, detectedpoint_meshes, DetectedRays, detectedpoint_meshes, detector_copies, OpticalChain 626 det = detector_copies[detname] 627 index = OpticalChain.detectors[detname].index 628 det_mesh = detector_meshes[detname] 629 translation = det.normal * (det.distance - new_value) 630 det_mesh.translate(translation, inplace=True) 631 det.distance = new_value 632 if DetectedRays: 633 points_mesh = detectedpoint_meshes[detname] 634 points_mesh.points = det.get_3D_points(OpticalChain.get_output_rays()[index]) 635 fig.show() 636 637 # Now we register the function to the observers 638 for key in OpticalChain.detectors.keys(): 639 if key in Observers: 640 Observers[key].register(lambda x: move_detector(key, x)) 641 642 #pv.save_meshio('optics.inp', pointcloud) 643 print( 644 "\r\033[K", end="", flush=True 645 ) # move to beginning of the line with \r and then delete the whole line with \033[K 646 fig.show() 647 return fig 648 649moc.OpticalChain.render = DrawSetup 650 651# %% Asphericity 652 653def DrawAsphericity(Mirror, Npoints=1000): 654 """ 655 This function displays a map of the asphericity of the mirror. 656 It's a scatter plot of the points of the mirror surface, with the color representing the distance to the closest sphere. 657 The closest sphere is calculated by the function get_closest_sphere, so least square method. 658 659 Parameters 660 ---------- 661 Mirror : Mirror 662 The mirror to analyse. 663 664 Npoints : int, optional 665 The number of points to sample on the mirror surface. The default is 1000. 666 667 Returns 668 ------- 669 fig : Figure 670 The figure of the plot. 671 """ 672 plt.ion() 673 fig = plt.figure() 674 ax = Mirror.support._ContourSupport(fig) 675 center, radius = man.GetClosestSphere(Mirror, Npoints) 676 Points = mpm.sample_support(Mirror.support, Npoints=1000) 677 Points += Mirror.r0[:2] 678 Z = Mirror._zfunc(Points) 679 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 680 X, Y = Points[:, 0] - Mirror.r0[0], Points[:, 1] - Mirror.r0[1] 681 Points_centered = Points - center 682 Distance = np.linalg.norm(Points_centered, axis=1) - radius 683 Distance*=1e3 # To convert to µm 684 p = plt.scatter(X, Y, c=Distance, s=15) 685 divider = man.make_axes_locatable(ax) 686 cax = divider.append_axes("right", size="5%", pad=0.05) 687 cbar = fig.colorbar(p, cax=cax) 688 cbar.set_label("Distance to closest sphere (µm)") 689 ax.set_xlabel("x (mm)") 690 ax.set_ylabel("y (mm)") 691 plt.title("Asphericity map", loc="right") 692 plt.tight_layout() 693 694 bbox = ax.get_position() 695 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 696 ax.set_position(bbox) 697 plt.show() 698 return fig 699 700mmirror.Mirror.drawAsphericity = DrawAsphericity 701# %% Caustics 702 703def DrawCaustics(OpticalChain, Range=1, Detector="Focus" , Npoints=1000, Nrays=1000): 704 """ 705 This function displays the caustics of the rays on the detector. 706 To do so, it calculates the intersections of the rays with the detector over a 707 range determined by the parameter Range, and then plots the standard deviation of the 708 positions in the x and y directions. 709 710 Parameters 711 ---------- 712 OpticalChain : OpticalChain 713 The optical chain to analyse. 714 715 DetectorName : str 716 The name of the detector on which the caustics are calculated. 717 718 Range : float 719 The range of the detector over which to calculate the caustics. 720 721 Npoints : int, optional 722 The number of points to sample on the detector. The default is 1000. 723 724 Returns 725 ------- 726 fig : Figure 727 The figure of the plot. 728 """ 729 distances = np.linspace(-Range, Range, Npoints) 730 if isinstance(Detector, str): 731 Det = OpticalChain.detectors[Detector] 732 Index = Det.index 733 Rays = OpticalChain.get_output_rays()[Index] 734 Nrays = min(Nrays, len(Rays)) 735 Rays = np.random.choice(Rays, Nrays, replace=False) 736 LocalRayList = [r.to_basis(*Det.basis) for r in Rays] 737 Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances) 738 x_std = [] 739 y_std = [] 740 for i in range(len(distances)): 741 x_std.append(mp.StandardDeviation(Points[i][:,0])) 742 y_std.append(mp.StandardDeviation(Points[i][:,1])) 743 plt.ion() 744 fig, ax = plt.subplots() 745 ax.plot(distances, x_std, label="x std") 746 ax.plot(distances, y_std, label="y std") 747 ax.set_xlabel("Detector distance (mm)") 748 ax.set_ylabel("Standard deviation (mm)") 749 ax.legend() 750 plt.title("Caustics") 751 plt.show() 752 return fig 753 754moc.OpticalChain.drawCaustics = DrawCaustics
57def DrawSpotDiagram(OpticalChain, 58 Detector = "Focus", 59 DrawAiryAndFourier=False, 60 DrawFocalContour=False, 61 DrawFocal=False, 62 ColorCoded=None, 63 Observer = None) -> plt.Figure: 64 """ 65 Produce an interactive figure with the spot diagram on the selected Detector. 66 The detector distance can be shifted with the left-right cursor keys. Doing so will actually move the detector. 67 If DrawAiryAndFourier is True, a circle with the Airy-spot-size will be shown. 68 If DrawFocalContour is True, the focal contour calculated from some of the rays will be shown. 69 If DrawFocal is True, a heatmap calculated from some of the rays will be shown. 70 The 'spots' can optionally be color-coded by specifying ColorCoded, which can be one of ["Intensity","Incidence","Delay"]. 71 72 Parameters 73 ---------- 74 RayListAnalysed : list(Ray) 75 List of objects of the ModuleOpticalRay.Ray-class. 76 77 Detector : Detector or str, optional 78 An object of the ModuleDetector.Detector-class or the name of the detector. The default is "Focus". 79 80 DrawAiryAndFourier : bool, optional 81 Whether to draw a circle with the Airy-spot-size. The default is False. 82 83 DrawFocalContour : bool, optional 84 Whether to draw the focal contour. The default is False. 85 86 DrawFocal : bool, optional 87 Whether to draw the focal heatmap. The default is False. 88 89 ColorCoded : str, optional 90 Color-code the spots according to one of ["Intensity","Incidence","Delay"]. The default is None. 91 92 Observer : Observer, optional 93 An observer object. If none, then we just create a copy of the detector and move it when pressing left-right. 94 However, if an observer is specified, then we will change the value of the observer and it will issue 95 the required callbacks to update several plots at the same time. 96 97 Returns 98 ------- 99 fig : matlplotlib-figure-handle. 100 Shows the interactive figure. 101 """ 102 if isinstance(Detector, str): 103 Detector = OpticalChain.detectors[Detector] 104 Index = Detector.index 105 movingDetector = copy(Detector) # We will move this detector when pressing left-right 106 if Observer is None: 107 detectorPosition = Observable(movingDetector.distance) # We will observe the distance of this detector 108 else: 109 detectorPosition = Observer 110 movingDetector.distance = detectorPosition.value 111 112 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 113 114 RayListAnalysed = OpticalChain.get_output_rays()[Index] 115 116 NumericalAperture = man.GetNumericalAperture(RayListAnalysed, 1) # NA determined from final ray bundle 117 MaxWavelength = np.max([i.wavelength for i in RayListAnalysed]) 118 if DrawAiryAndFourier: 119 AiryRadius = man.GetAiryRadius(MaxWavelength, NumericalAperture) * 1e3 # in µm 120 else: 121 AiryRadius = 0 122 123 if DrawFocalContour or DrawFocal: 124 X,Y,Z = man.GetDiffractionFocus(OpticalChain, movingDetector, Index) 125 Z/=np.max(Z) 126 127 DectectorPoint2D_Xcoord, DectectorPoint2D_Ycoord, FocalSpotSize, SpotSizeSD = mpu._getDetectorPoints( 128 RayListAnalysed, movingDetector 129 ) 130 131 match ColorCoded: 132 case "Intensity": 133 IntensityList = [k.intensity for k in RayListAnalysed] 134 z = np.asarray(IntensityList) 135 zlabel = "Intensity (arb.u.)" 136 title = "Intensity + Spot Diagram\n press left/right to move detector position" 137 addLine = "" 138 case "Incidence": 139 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # degree 140 z = np.asarray(IncidenceList) 141 zlabel = "Incidence angle (deg)" 142 title = "Ray Incidence + Spot Diagram\n press left/right to move detector position" 143 addLine = "" 144 case "Delay": 145 DelayList = movingDetector.get_Delays(RayListAnalysed) 146 IntensityList = [k.intensity for k in RayListAnalysed] 147 DurationSD = mp.WeightedStandardDeviation(DelayList,IntensityList) 148 #DurationSD = mp.StandardDeviation(DelayList) 149 z = np.asarray(DelayList) 150 zlabel = "Delay (fs)" 151 title = "Delay + Spot Diagram\n press left/right to move detector position" 152 addLine = "\n" + "{:.2f}".format(DurationSD) + " fs SD" 153 case _: 154 z = "red" 155 title = "Spot Diagram\n press left/right to move detector position" 156 addLine = "" 157 158 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 159 160 plt.ion() 161 fig, ax = plt.subplots() 162 if DrawFocal: 163 focal = ax.pcolormesh(X*1e3,Y*1e3,Z) 164 if DrawFocalContour: 165 levels = [1/np.e**2, 0.5] 166 contour = ax.contourf(X*1e3, Y*1e3, Z, levels=levels, cmap='gray') 167 168 if DrawAiryAndFourier: 169 theta = np.linspace(0, 2 * np.pi, 100) 170 x = AiryRadius * np.cos(theta) 171 y = AiryRadius * np.sin(theta) # 172 ax.plot(x, y, c="black") 173 174 175 foo = ax.scatter( 176 DectectorPoint2D_Xcoord, 177 DectectorPoint2D_Ycoord, 178 c=z, 179 s=15, 180 label="{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(SpotSizeSD * 1e3) + " \u03BCm SD" + addLine, 181 ) 182 183 axisLim = 1.1 * max(AiryRadius, 0.5 * FocalSpotSize * 1000) 184 ax.set_xlim(-axisLim, axisLim) 185 ax.set_ylim(-axisLim, axisLim) 186 187 if ColorCoded == "Intensity" or ColorCoded == "Incidence" or ColorCoded == "Delay": 188 cbar = fig.colorbar(foo) 189 cbar.set_label(zlabel) 190 191 ax.legend(loc="upper right") 192 ax.set_xlabel("X (µm)") 193 ax.set_ylabel("Y (µm)") 194 ax.set_title(title) 195 # ax.margins(x=0) 196 197 198 def update_plot(new_value): 199 nonlocal movingDetector, ColorCoded, zlabel, cbar, detectorPosition, foo, distStep, focal, contour, levels, Index, RayListAnalysed 200 201 newDectectorPoint2D_Xcoord, newDectectorPoint2D_Ycoord, newFocalSpotSize, newSpotSizeSD = mpu._getDetectorPoints( 202 RayListAnalysed, movingDetector 203 ) 204 205 if DrawFocal: 206 focal.set_array(Z) 207 if DrawFocalContour: 208 levels = [1/np.e**2, 0.5] 209 for coll in contour.collections: 210 coll.remove() # Remove old contour lines 211 contour = ax.contourf(X * 1e3, Y * 1e3, Z, levels=levels, cmap='gray') 212 213 xy = foo.get_offsets() 214 xy[:, 0] = newDectectorPoint2D_Xcoord 215 xy[:, 1] = newDectectorPoint2D_Ycoord 216 foo.set_offsets(xy) 217 218 219 if ColorCoded == "Delay": 220 newDelayList = np.asarray(movingDetector.get_Delays(RayListAnalysed)) 221 newDurationSD = mp.WeightedStandardDeviation(newDelayList,IntensityList) 222 #newDurationSD = mp.StandardDeviation(newDelayList) 223 newaddLine = "\n" + "{:.2f}".format(newDurationSD) + " fs SD" 224 foo.set_array(newDelayList) 225 foo.set_clim(min(newDelayList), max(newDelayList)) 226 cbar.update_normal(foo) 227 else: 228 newaddLine = "" 229 230 foo.set_label( 231 "{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(newSpotSizeSD * 1e3) + " \u03BCm SD" + newaddLine 232 ) 233 ax.legend(loc="upper right") 234 235 axisLim = 1.1 * max(AiryRadius, 0.5 * newFocalSpotSize * 1000) 236 ax.set_xlim(-axisLim, axisLim) 237 ax.set_ylim(-axisLim, axisLim) 238 239 distStep = min( 240 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 241 ) # in mm 242 243 fig.canvas.draw_idle() 244 245 246 def press(event): 247 nonlocal detectorPosition, distStep 248 if event.key == "right": 249 detectorPosition.value += distStep 250 elif event.key == "left": 251 if detectorPosition.value > 1.5 * distStep: 252 detectorPosition.value -= distStep 253 else: 254 detectorPosition.value = 0.5 * distStep 255 else: 256 return None 257 258 fig.canvas.mpl_connect("key_press_event", press) 259 260 plt.show() 261 262 detectorPosition.register(update_plot) 263 264 265 return fig, detectorPosition
Produce an interactive figure with the spot diagram on the selected Detector. The detector distance can be shifted with the left-right cursor keys. Doing so will actually move the detector. If DrawAiryAndFourier is True, a circle with the Airy-spot-size will be shown. If DrawFocalContour is True, the focal contour calculated from some of the rays will be shown. If DrawFocal is True, a heatmap calculated from some of the rays will be shown. The 'spots' can optionally be color-coded by specifying ColorCoded, which can be one of ["Intensity","Incidence","Delay"].
RayListAnalysed : list(Ray)
List of objects of the ModuleOpticalRay.Ray-class.
Detector : Detector or str, optional
An object of the ModuleDetector.Detector-class or the name of the detector. The default is "Focus".
DrawAiryAndFourier : bool, optional
Whether to draw a circle with the Airy-spot-size. The default is False.
DrawFocalContour : bool, optional
Whether to draw the focal contour. The default is False.
DrawFocal : bool, optional
Whether to draw the focal heatmap. The default is False.
ColorCoded : str, optional
Color-code the spots according to one of ["Intensity","Incidence","Delay"]. The default is None.
Observer : Observer, optional
An observer object. If none, then we just create a copy of the detector and move it when pressing left-right.
However, if an observer is specified, then we will change the value of the observer and it will issue
the required callbacks to update several plots at the same time.
fig : matlplotlib-figure-handle.
Shows the interactive figure.
270def DrawDelaySpots(OpticalChain, 271 DeltaFT: tuple[int, float], 272 Detector = "Focus", 273 DrawAiryAndFourier=False, 274 ColorCoded=None, 275 Observer = None 276 ) -> plt.Figure: 277 """ 278 Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed 279 hitting the Detector, with the ray-delays shown in the 3rd dimension. 280 The detector distance can be shifted with the left-right cursor keys. 281 If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and 282 whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'. 283 284 The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"]. 285 286 Parameters 287 ---------- 288 RayListAnalysed : list(Ray) 289 List of objects of the ModuleOpticalRay.Ray-class. 290 291 Detector : Detector 292 An object of the ModuleDetector.Detector-class. 293 294 DeltaFT : (int, float) 295 The Fourier-limited pulse duration. Just used as a reference to compare the temporal spread 296 induced by the ray-delays. 297 298 DrawAiryAndFourier : bool, optional 299 Whether to draw a cylinder showing the Airy-spot-size and Fourier-limited-duration. 300 The default is False. 301 302 ColorCoded : str, optional 303 Color-code the spots according to one of ["Intensity","Incidence"]. 304 The default is None. 305 306 Returns 307 ------- 308 fig : matlplotlib-figure-handle. 309 Shows the interactive figure. 310 """ 311 if isinstance(Detector, str): 312 Det = OpticalChain.detectors[Detector] 313 else: 314 Det = Detector 315 Index = Det.index 316 Detector = copy(Det) 317 if Observer is None: 318 detectorPosition = Observable(Detector.distance) 319 else: 320 detectorPosition = Observer 321 Detector.distance = detectorPosition.value 322 323 RayListAnalysed = OpticalChain.get_output_rays()[Index] 324 fig, NumericalAperture, AiryRadius, FocalSpotSize = _drawDelayGraph( 325 RayListAnalysed, Detector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded 326 ) 327 328 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 329 330 movingDetector = copy(Detector) 331 332 def update_plot(new_value): 333 nonlocal movingDetector, ColorCoded, detectorPosition, distStep, fig 334 ax = fig.axes[0] 335 cam = [ax.azim, ax.elev, ax._dist] 336 fig, sameNumericalAperture, sameAiryRadius, newFocalSpotSize = _drawDelayGraph( 337 RayListAnalysed, movingDetector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded, fig 338 ) 339 ax = fig.axes[0] 340 ax.azim, ax.elev, ax._dist = cam 341 distStep = min( 342 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 343 ) 344 return fig 345 346 def press(event): 347 nonlocal detectorPosition, distStep, movingDetector, fig 348 if event.key == "right": 349 detectorPosition.value += distStep 350 elif event.key == "left": 351 if detectorPosition.value > 1.5 * distStep: 352 detectorPosition.value -= distStep 353 else: 354 detectorPosition.value = 0.5 * distStep 355 356 fig.canvas.mpl_connect("key_press_event", press) 357 detectorPosition.register(update_plot) 358 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 359 360 return fig, Observable
Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed hitting the Detector, with the ray-delays shown in the 3rd dimension. The detector distance can be shifted with the left-right cursor keys. If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'.
The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"].
RayListAnalysed : list(Ray)
List of objects of the ModuleOpticalRay.Ray-class.
Detector : Detector
An object of the ModuleDetector.Detector-class.
DeltaFT : (int, float)
The Fourier-limited pulse duration. Just used as a reference to compare the temporal spread
induced by the ray-delays.
DrawAiryAndFourier : bool, optional
Whether to draw a cylinder showing the Airy-spot-size and Fourier-limited-duration.
The default is False.
ColorCoded : str, optional
Color-code the spots according to one of ["Intensity","Incidence"].
The default is None.
fig : matlplotlib-figure-handle.
Shows the interactive figure.
441def DrawMirrorProjection(OpticalChain, ReflectionNumber: int, ColorCoded=None, Detector="") -> plt.Figure: 442 """ 443 Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. 444 The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is 445 measured at the Detector. 446 447 Parameters 448 ---------- 449 OpticalChain : OpticalChain 450 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 451 452 ReflectionNumber : int 453 Index specifying the optical element on which you want to see the impact points. 454 455 Detector : Detector, optional 456 Object of the ModuleDetector.Detector-class. Only necessary to project delays. The default is None. 457 458 ColorCoded : str, optional 459 Specifies which ray property to color-code: ["Incidence","Intensity","Delay"]. The default is None. 460 461 Returns 462 ------- 463 fig : matlplotlib-figure-handle. 464 Shows the figure. 465 """ 466 from mpl_toolkits.axes_grid1 import make_axes_locatable 467 if isinstance(Detector, str): 468 if Detector == "": 469 Detector = None 470 else: 471 Detector = OpticalChain.detectors[Detector] 472 473 Position = OpticalChain[ReflectionNumber].position 474 q = OpticalChain[ReflectionNumber].orientation 475 # n = OpticalChain.optical_elements[ReflectionNumber].normal 476 # m = OpticalChain.optical_elements[ReflectionNumber].majoraxis 477 478 RayListAnalysed = OpticalChain.get_output_rays()[ReflectionNumber] 479 # transform rays into the mirror-support reference frame 480 # (same as mirror frame but without the shift by mirror-centre) 481 r0 = OpticalChain[ReflectionNumber].r0 482 RayList = [r.to_basis(*OpticalChain[ReflectionNumber].basis) for r in RayListAnalysed] 483 484 x = np.asarray([k.point[0] for k in RayList]) - r0[0] 485 y = np.asarray([k.point[1] for k in RayList]) - r0[1] 486 if ColorCoded == "Intensity": 487 IntensityList = [k.intensity for k in RayListAnalysed] 488 z = np.asarray(IntensityList) 489 zlabel = "Intensity (arb.u.)" 490 title = "Ray intensity projected on mirror " 491 elif ColorCoded == "Incidence": 492 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # in degree 493 z = np.asarray(IncidenceList) 494 zlabel = "Incidence angle (deg)" 495 title = "Ray incidence projected on mirror " 496 elif ColorCoded == "Delay": 497 if Detector is not None: 498 z = np.asarray(Detector.get_Delays(RayListAnalysed)) 499 zlabel = "Delay (fs)" 500 title = "Ray delay at detector projected on mirror " 501 else: 502 raise ValueError("If you want to project ray delays, you must specify a detector.") 503 else: 504 z = "red" 505 title = "Ray impact points projected on mirror" 506 507 plt.ion() 508 fig = plt.figure() 509 ax = OpticalChain.optical_elements[ReflectionNumber].support._ContourSupport(fig) 510 p = plt.scatter(x, y, c=z, s=15) 511 if ColorCoded == "Delay" or ColorCoded == "Incidence" or ColorCoded == "Intensity": 512 divider = make_axes_locatable(ax) 513 cax = divider.append_axes("right", size="5%", pad=0.05) 514 cbar = fig.colorbar(p, cax=cax) 515 cbar.set_label(zlabel) 516 ax.set_xlabel("x (mm)") 517 ax.set_ylabel("y (mm)") 518 plt.title(title, loc="right") 519 plt.tight_layout() 520 521 bbox = ax.get_position() 522 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 523 ax.set_position(bbox) 524 plt.show() 525 526 return fig
Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is measured at the Detector.
OpticalChain : OpticalChain
List of objects of the ModuleOpticalOpticalChain.OpticalChain-class.
ReflectionNumber : int
Index specifying the optical element on which you want to see the impact points.
Detector : Detector, optional
Object of the ModuleDetector.Detector-class. Only necessary to project delays. The default is None.
ColorCoded : str, optional
Specifies which ray property to color-code: ["Incidence","Intensity","Delay"]. The default is None.
fig : matlplotlib-figure-handle.
Shows the figure.
532def DrawSetup(OpticalChain, 533 EndDistance=None, 534 maxRays=300, 535 OEpoints=2000, 536 draw_mesh=False, 537 cycle_ray_colors = False, 538 impact_points = False, 539 DrawDetectors=True, 540 DetectedRays = False, 541 Observers = dict()): 542 """ 543 Renders an image of the Optical setup and the traced rays. 544 545 Parameters 546 ---------- 547 OpticalChain : OpticalChain 548 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 549 550 EndDistance : float, optional 551 The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). If not specified, 552 this distance is set to that between the source point and the 1st optical element. 553 554 maxRays: int 555 The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog 556 and not required for a nice image. Default is 150. 557 558 OEpoints : int 559 How many little spheres to draw to represent the optical elements. Default is 2000. 560 561 Returns 562 ------- 563 fig : Pyvista-figure-handle. 564 Shows the figure. 565 """ 566 567 RayListHistory = [OpticalChain.source_rays] + OpticalChain.get_output_rays() 568 569 if EndDistance is None: 570 EndDistance = np.linalg.norm(OpticalChain.source_rays[0].point - OpticalChain.optical_elements[0].position) 571 572 print("...rendering image of optical chain...", end="", flush=True) 573 fig = pvqt.BackgroundPlotter(window_size=(1500, 500), notebook=False) # Opening a window 574 fig.set_background('white') 575 576 if cycle_ray_colors: 577 colors = mpu.generate_distinct_colors(len(OpticalChain)+1) 578 else: 579 colors = [[0.7, 0, 0]]*(len(OpticalChain)+1) # Default color: dark red 580 581 # Optics display 582 # For each optic we will send the figure to the function _RenderOpticalElement and it will add the optic to the figure 583 for i,OE in enumerate(OpticalChain.optical_elements): 584 color = pv.Color(colors[i+1]) 585 rgb = color.float_rgb 586 h, l, s = rgb_to_hls(*rgb) 587 s = max(0, min(1, s * 0.3)) # Decrease saturation 588 l = max(0, min(1, l + 0.1)) # Increase lightness 589 new_rgb = hls_to_rgb(h, l, s) 590 darkened_color = pv.Color(new_rgb) 591 mpm._RenderOpticalElement(fig, OE, OEpoints, draw_mesh, darkened_color, index=i) 592 ray_meshes = mpm._RenderRays(RayListHistory, EndDistance, maxRays) 593 for i,ray in enumerate(ray_meshes): 594 color = pv.Color(colors[i]) 595 fig.add_mesh(ray, color=color, name=f"RayBundle_{i}") 596 if impact_points: 597 for i,rays in enumerate(RayListHistory): 598 points = np.array([list(r.point) for r in rays], dtype=np.float32) 599 points = pv.PolyData(points) 600 color = pv.Color(colors[i-1]) 601 fig.add_mesh(points, color=color, point_size=5, name=f"RayImpactPoints_{i}") 602 603 detector_copies = {key: copy(OpticalChain.detectors[key]) for key in OpticalChain.detectors.keys()} 604 detector_meshes_list = [] 605 detectedpoint_meshes = dict() 606 607 if OpticalChain.detectors is not None and DrawDetectors: 608 # Detector display 609 for key in OpticalChain.detectors.keys(): 610 det = detector_copies[key] 611 index = OpticalChain.detectors[key].index 612 if key in Observers: 613 det.distance = Observers[key].value 614 #Observers[key].register_calculation(lambda x: det.set_distance(x)) 615 mpm._RenderDetector(fig, det, name = key, detector_meshes = detector_meshes_list) 616 if DetectedRays: 617 RayListAnalysed = OpticalChain.get_output_rays()[index] 618 points = det.get_3D_points(RayListAnalysed) 619 points = pv.PolyData(points) 620 detectedpoint_meshes[key] = points 621 fig.add_mesh(points, color='purple', point_size=5, name=f"DetectedRays_{key}") 622 detector_meshes = dict(zip(OpticalChain.detectors.keys(), detector_meshes_list)) 623 624 # Now we define a function that will move on the plot the detector with name "detname" when it's called 625 def move_detector(detname, new_value): 626 nonlocal fig, detector_meshes, detectedpoint_meshes, DetectedRays, detectedpoint_meshes, detector_copies, OpticalChain 627 det = detector_copies[detname] 628 index = OpticalChain.detectors[detname].index 629 det_mesh = detector_meshes[detname] 630 translation = det.normal * (det.distance - new_value) 631 det_mesh.translate(translation, inplace=True) 632 det.distance = new_value 633 if DetectedRays: 634 points_mesh = detectedpoint_meshes[detname] 635 points_mesh.points = det.get_3D_points(OpticalChain.get_output_rays()[index]) 636 fig.show() 637 638 # Now we register the function to the observers 639 for key in OpticalChain.detectors.keys(): 640 if key in Observers: 641 Observers[key].register(lambda x: move_detector(key, x)) 642 643 #pv.save_meshio('optics.inp', pointcloud) 644 print( 645 "\r\033[K", end="", flush=True 646 ) # move to beginning of the line with \r and then delete the whole line with \033[K 647 fig.show() 648 return fig
Renders an image of the Optical setup and the traced rays.
OpticalChain : OpticalChain
List of objects of the ModuleOpticalOpticalChain.OpticalChain-class.
EndDistance : float, optional
The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). If not specified,
this distance is set to that between the source point and the 1st optical element.
maxRays: int
The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog
and not required for a nice image. Default is 150.
OEpoints : int
How many little spheres to draw to represent the optical elements. Default is 2000.
fig : Pyvista-figure-handle.
Shows the figure.
654def DrawAsphericity(Mirror, Npoints=1000): 655 """ 656 This function displays a map of the asphericity of the mirror. 657 It's a scatter plot of the points of the mirror surface, with the color representing the distance to the closest sphere. 658 The closest sphere is calculated by the function get_closest_sphere, so least square method. 659 660 Parameters 661 ---------- 662 Mirror : Mirror 663 The mirror to analyse. 664 665 Npoints : int, optional 666 The number of points to sample on the mirror surface. The default is 1000. 667 668 Returns 669 ------- 670 fig : Figure 671 The figure of the plot. 672 """ 673 plt.ion() 674 fig = plt.figure() 675 ax = Mirror.support._ContourSupport(fig) 676 center, radius = man.GetClosestSphere(Mirror, Npoints) 677 Points = mpm.sample_support(Mirror.support, Npoints=1000) 678 Points += Mirror.r0[:2] 679 Z = Mirror._zfunc(Points) 680 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 681 X, Y = Points[:, 0] - Mirror.r0[0], Points[:, 1] - Mirror.r0[1] 682 Points_centered = Points - center 683 Distance = np.linalg.norm(Points_centered, axis=1) - radius 684 Distance*=1e3 # To convert to µm 685 p = plt.scatter(X, Y, c=Distance, s=15) 686 divider = man.make_axes_locatable(ax) 687 cax = divider.append_axes("right", size="5%", pad=0.05) 688 cbar = fig.colorbar(p, cax=cax) 689 cbar.set_label("Distance to closest sphere (µm)") 690 ax.set_xlabel("x (mm)") 691 ax.set_ylabel("y (mm)") 692 plt.title("Asphericity map", loc="right") 693 plt.tight_layout() 694 695 bbox = ax.get_position() 696 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 697 ax.set_position(bbox) 698 plt.show() 699 return fig
This function displays a map of the asphericity of the mirror. It's a scatter plot of the points of the mirror surface, with the color representing the distance to the closest sphere. The closest sphere is calculated by the function get_closest_sphere, so least square method.
Mirror : Mirror The mirror to analyse.
Npoints : int, optional The number of points to sample on the mirror surface. The default is 1000.
fig : Figure The figure of the plot.
704def DrawCaustics(OpticalChain, Range=1, Detector="Focus" , Npoints=1000, Nrays=1000): 705 """ 706 This function displays the caustics of the rays on the detector. 707 To do so, it calculates the intersections of the rays with the detector over a 708 range determined by the parameter Range, and then plots the standard deviation of the 709 positions in the x and y directions. 710 711 Parameters 712 ---------- 713 OpticalChain : OpticalChain 714 The optical chain to analyse. 715 716 DetectorName : str 717 The name of the detector on which the caustics are calculated. 718 719 Range : float 720 The range of the detector over which to calculate the caustics. 721 722 Npoints : int, optional 723 The number of points to sample on the detector. The default is 1000. 724 725 Returns 726 ------- 727 fig : Figure 728 The figure of the plot. 729 """ 730 distances = np.linspace(-Range, Range, Npoints) 731 if isinstance(Detector, str): 732 Det = OpticalChain.detectors[Detector] 733 Index = Det.index 734 Rays = OpticalChain.get_output_rays()[Index] 735 Nrays = min(Nrays, len(Rays)) 736 Rays = np.random.choice(Rays, Nrays, replace=False) 737 LocalRayList = [r.to_basis(*Det.basis) for r in Rays] 738 Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances) 739 x_std = [] 740 y_std = [] 741 for i in range(len(distances)): 742 x_std.append(mp.StandardDeviation(Points[i][:,0])) 743 y_std.append(mp.StandardDeviation(Points[i][:,1])) 744 plt.ion() 745 fig, ax = plt.subplots() 746 ax.plot(distances, x_std, label="x std") 747 ax.plot(distances, y_std, label="y std") 748 ax.set_xlabel("Detector distance (mm)") 749 ax.set_ylabel("Standard deviation (mm)") 750 ax.legend() 751 plt.title("Caustics") 752 plt.show() 753 return fig
This function displays the caustics of the rays on the detector. To do so, it calculates the intersections of the rays with the detector over a range determined by the parameter Range, and then plots the standard deviation of the positions in the x and y directions.
OpticalChain : OpticalChain The optical chain to analyse.
DetectorName : str The name of the detector on which the caustics are calculated.
Range : float The range of the detector over which to calculate the caustics.
Npoints : int, optional The number of points to sample on the detector. The default is 1000.
fig : Figure The figure of the plot.
Adds plotting and visualisation methods to various classes from ARTcore. Most of the code is simply moved here from the class definitions previously.
Created in July 2024
@author: André Kalouguine + Stefan Haessler + Anthony Guillaume
1""" 2Adds plotting and visualisation methods to various classes from ARTcore. 3Most of the code is simply moved here from the class definitions previously. 4 5Created in July 2024 6 7@author: André Kalouguine + Stefan Haessler + Anthony Guillaume 8""" 9# %% 10import numpy as np 11import matplotlib.pyplot as plt 12from mpl_toolkits.mplot3d import Axes3D 13import matplotlib.patches as patches 14import pyvista as pv 15import pyvistaqt as pvqt 16import colorcet as cc 17 18import ARTcore.ModuleSupport as msup 19import ARTcore.ModuleMirror as mmir 20import ARTcore.ModuleGeometry as mgeo 21import ARTcore.ModuleMask as mmask 22from ARTcore.ModuleGeometry import Point, Vector, Origin 23import itertools 24 25# %% Adding support drawing for mirror projection 26 27def SupportRound_ContourSupport(self, Figure): 28 """Draw support contour in MirrorProjection plots.""" 29 axe = Figure.add_subplot(111, aspect="equal") 30 axe.add_patch(patches.Circle((0, 0), self.radius, alpha=0.08)) 31 return axe 32 33msup.SupportRound._ContourSupport = SupportRound_ContourSupport 34 35def SupportRoundHole_ContourSupport(self, Figure): 36 """Draws support contour in MirrorProjection plots.""" 37 axe = Figure.add_subplot(111, aspect="equal") 38 axe.add_patch(patches.Circle((0, 0), self.radius, alpha=0.08)) 39 axe.add_patch(patches.Circle((self.centerholeX, self.centerholeY), self.radiushole, color="white", alpha=1)) 40 return axe 41 42msup.SupportRoundHole._ContourSupport = SupportRoundHole_ContourSupport 43 44def SupportRectangle_ContourSupport(self, Figure): 45 """Draws support contour in MirrorProjection plots.""" 46 axe = Figure.add_subplot(111, aspect="equal") 47 axe.add_patch(patches.Rectangle((-self.dimX * 0.5, -self.dimY * 0.5), self.dimX, self.dimY, alpha=0.08)) 48 return axe 49 50msup.SupportRectangle._ContourSupport = SupportRectangle_ContourSupport 51 52def SupportRectangleHole_ContourSupport(self, Figure): 53 """Draws support contour in MirrorProjection plots.""" 54 axe = Figure.add_subplot(111, aspect="equal") 55 axe.add_patch(patches.Rectangle((-self.dimX * 0.5, -self.dimY * 0.5), self.dimX, self.dimY, alpha=0.08)) 56 axe.add_patch(patches.Circle((self.centerholeX, self.centerholeY), self.radiushole, color="white", alpha=1)) 57 return axe 58 59msup.SupportRectangleHole._ContourSupport = SupportRectangleHole_ContourSupport 60 61def SupportRectangleRectHole_ContourSupport(self, Figure): 62 """Draws support contour in MirrorProjection plots.""" 63 axe = Figure.add_subplot(111, aspect="equal") 64 axe.add_patch(patches.Rectangle((-self.dimX * 0.5, -self.dimY * 0.5), self.dimX, self.dimY, alpha=0.08)) 65 axe.add_patch( 66 patches.Rectangle( 67 (-self.holeX * 0.5 + self.centerholeX, -self.holeY * 0.5 + self.centerholeY), 68 self.holeX, 69 self.holeY, 70 color="white", 71 alpha=1, 72 ) 73 ) 74 return axe 75 76msup.SupportRectangleRectHole._ContourSupport = SupportRectangleRectHole_ContourSupport 77 78# %% Support sampling and meshing 79 80def sample_support(Support, Npoints): 81 """ 82 This function samples a regular grid on the support and filters out the points that are outside of the support. 83 For that it uses the _estimate_size method of the support. 84 It then generates a regular grid over that area and filters out the points that are outside of the support. 85 If less than half of the points are inside the support, it will increase the number of points by a factor of 2 and try again. 86 """ 87 if hasattr(Support, "size"): 88 size = Support.size 89 else: 90 size_x = Support._estimate_size() 91 size = np.array([2*size_x, 2*size_x]) 92 enough = False 93 while not enough: 94 X = np.linspace(-size[0] * 0.5, size[0] * 0.5, int(np.sqrt(Npoints))) 95 Y = np.linspace(-size[1] * 0.5, size[1] * 0.5, int(np.sqrt(Npoints))) 96 X, Y = np.meshgrid(X, Y) 97 Points = np.array([X.flatten(), Y.flatten()]).T 98 Points = [p for p in Points if p in Support] 99 if len(Points) > Npoints * 0.5: 100 enough = True 101 else: 102 Npoints *= 2 103 return mgeo.PointArray(Points) 104 105def sample_z_values(Support, Npoints): 106 Npoints_init = Npoints 107 if hasattr(Support, "size"): 108 size = Support.size 109 else: 110 size_x = Support._estimate_size() 111 size = np.array([2*size_x, 2*size_x]) 112 enough = False 113 while not enough: 114 X = np.linspace(-size[0] * 0.5, size[0] * 0.5, int(np.sqrt(Npoints))) 115 Y = np.linspace(-size[1] * 0.5, size[1] * 0.5, int(np.sqrt(Npoints))) 116 X, Y = np.meshgrid(X, Y) 117 Points = np.array([X.flatten(), Y.flatten()]).T 118 Points = [p for p in Points if p in Support] 119 if len(Points) > Npoints_init * 0.5: 120 enough = True 121 else: 122 Npoints *= 2 123 Points = np.array([X.flatten(), Y.flatten()]).T 124 mask = np.array([p in Support for p in Points]).reshape(X.shape) 125 return X,Y,mask 126 127 128# %% Mesh/pointcloud generation for different mirror/mask types 129 130# For the mirrors that are already implemented in ARTcore 131# we can use the method _zfunc to render the mirror surface. 132# For that we sample the support and then use the _zfunc method to get the z-values of the surface. 133# We then use the pyvista function add_mesh to render the surface. 134 135def _RenderMirror(Mirror, Npoints=10000): 136 """ 137 This function renders a mirror in 3D. 138 It samples the support of the mirror and uses the _zfunc method to get the z-values of the surface. 139 It draws a small sphere at each point of the support and connects them to form a surface. 140 """ 141 Points = sample_support(Mirror.support, Npoints) 142 Points += Mirror.r0[:2] 143 Z = Mirror._zfunc(Points) 144 Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T 145 Points = Points.from_basis(*Mirror.basis) 146 mesh = pv.PolyData(Points) 147 return mesh 148 149 150def _RenderMirrorSurface(Mirror, Npoints=10000): 151 """ 152 This function renders a mirror in 3D. 153 It samples the support of the mirror and uses the _zfunc method to get the z-values of the surface. 154 It draws a small sphere at each point of the support and connects them to form a surface. 155 """ 156 X,Y,mask = sample_z_values(Mirror.support, Npoints) 157 shape = X.shape 158 X += Mirror.r0[0] 159 Y += Mirror.r0[1] 160 Z = Mirror._zfunc(mgeo.PointArray([X.flatten(), Y.flatten()]).T).reshape(X.shape) 161 #Z += Mirror.r0[2] 162 Z[~mask] = np.nan 163 Points = mgeo.PointArray([X.flatten(), Y.flatten(), Z.flatten()]).T 164 Points = Points.from_basis(*Mirror.basis) 165 X,Y,Z = Points.T 166 X = X.reshape(shape) 167 Y = Y.reshape(shape) 168 Z = Z.reshape(shape) 169 mesh = pv.StructuredGrid(X, Y, Z) 170 return mesh 171 172mmir.MirrorPlane._Render = _RenderMirror 173mmir.MirrorParabolic._Render = _RenderMirror 174mmir.MirrorCylindrical._Render = _RenderMirror 175mmir.MirrorEllipsoidal._Render = _RenderMirror 176mmir.MirrorSpherical._Render = _RenderMirror 177mmir.MirrorToroidal._Render = _RenderMirror 178 179mmask.Mask._Render = _RenderMirror 180 181mmir.MirrorPlane._Render = _RenderMirrorSurface 182mmir.MirrorParabolic._Render = _RenderMirrorSurface 183mmir.MirrorCylindrical._Render = _RenderMirrorSurface 184mmir.MirrorEllipsoidal._Render = _RenderMirrorSurface 185mmir.MirrorSpherical._Render = _RenderMirrorSurface 186mmir.MirrorToroidal._Render = _RenderMirrorSurface 187 188mmask.Mask._Render = _RenderMirrorSurface 189 190# %% Optical element rendering 191def _RenderOpticalElement(fig, OE, OEpoints, draw_mesh = False, color="blue", index=0): 192 """ 193 This function renders the optical elements in the 3D plot. 194 It receives a pyvista figure handle, and draws the optical element on it. 195 """ 196 mesh = OE._Render(OEpoints) 197 fig.add_mesh(mesh, color = color, name = f"{OE.description} {index}") 198 199def _RenderDetector(fig, Detector, size = 40, name = "Focus", detector_meshes = None): 200 """ 201 Unfinished 202 """ 203 Points = [np.array([-size/2,size/2,0]),np.array([size/2,size/2,0]),np.array([size/2,-size/2,0]),np.array([-size/2,-size/2,0])] 204 Points = mgeo.PointArray(Points) 205 Points = Points.from_basis(*Detector.basis) 206 Rect = pv.Rectangle(Points[:3]) 207 if detector_meshes is not None: 208 detector_meshes += [Rect] 209 fig.add_mesh(Rect, color="green", name=f"Detector {name}") 210 211# %% Support rendering using the sdf method 212 213def _RenderSupport(Support, Npoints=10000): 214 """ 215 This function renders a support in 3D. 216 """ 217 X,Y,mask = sample_z_values(Support, Npoints) 218 shape = X.shape 219 Z = np.zeros_like(X) 220 Z[~mask] = np.nan 221 mesh = pv.StructuredGrid(X, Y, Z) 222 return mesh 223 224msup.SupportRound._Render = _RenderSupport 225msup.SupportRoundHole._Render = _RenderSupport 226msup.SupportRectangle._Render = _RenderSupport 227msup.SupportRectangleHole._Render = _RenderSupport 228msup.SupportRectangleRectHole._Render = _RenderSupport 229 230 231# %% Standalone optical element rendering 232 233def _RenderMirror(Mirror, Npoints=1000, draw_support = False, draw_points = False, draw_vectors = True , recenter_support = True): 234 """ 235 This function renders a mirror in 3D. 236 """ 237 mesh = Mirror._Render(Npoints) 238 p = pv.Plotter() 239 p.add_mesh(mesh) 240 if draw_support: 241 support = Mirror.support._Render() 242 if recenter_support: 243 support.translate(Mirror.r0,inplace=True) 244 p.add_mesh(support, color="gray", opacity=0.5) 245 246 if draw_vectors: 247 # We draw the important vectors of the optical element 248 # For that, if we have a "vectors" attribute, we use that 249 # (a dictionary with the vector names as keys and the colors as values) 250 # Otherwise we use the default vectors: "support_normal", "majoraxis" 251 if hasattr(Mirror, "vectors"): 252 vectors = Mirror.vectors 253 else: 254 vectors = {"support_normal_ref": "red", "majoraxis_ref": "blue"} 255 for vector, color in vectors.items(): 256 if hasattr(Mirror, vector): 257 p.add_arrows(Mirror.r0, 10*getattr(Mirror, vector), color=color) 258 259 if draw_points: 260 # We draw the important points of the optical element 261 # For that, if we have a "points" attribute, we use that 262 # (a dictionary with the point names as keys and the colors as values) 263 # Otherwise we use the default points: "centre_ref" 264 if hasattr(Mirror, "points"): 265 points = Mirror.points 266 else: 267 points = {"centre_ref": "red"} 268 for point, color in points.items(): 269 if hasattr(Mirror, point): 270 p.add_mesh(pv.Sphere(radius=1, center=getattr(Mirror, point)), color=color) 271 else: 272 p.add_mesh(pv.Sphere(radius=1, center=Mirror.r0), color="red") 273 p.show() 274 return p 275 276mmir.MirrorPlane.render = _RenderMirror 277mmir.MirrorParabolic.render = _RenderMirror 278mmir.MirrorParabolic.vectors = { 279 "support_normal": "red", 280 "majoraxis": "blue", 281 "towards_focusing_ref": "green" 282} 283mmir.MirrorParabolic.points = { 284 "centre_ref": "red", 285 "focus_ref": "green" 286} 287mmir.MirrorCylindrical.render = _RenderMirror 288mmir.MirrorEllipsoidal.render = _RenderMirror 289mmir.MirrorEllipsoidal.vectors = { 290 "support_normal_ref": "red", 291 "majoraxis_ref": "blue", 292 "towards_image_ref": "green", 293 "towards_object_ref": "green", 294 "centre_normal_ref": "purple"} 295 296mmir.MirrorSpherical.render = _RenderMirror 297mmir.MirrorToroidal.render = _RenderMirror 298 299mmask.Mask.render = _RenderMirror 300 301# %% Ray rendering 302 303def _RenderRays(RayListHistory, EndDistance, maxRays=150): 304 """ 305 Generates a list of Pyvista-meshes representing the rays in RayListHistory. 306 This can then be employed to render the rays in a Pyvista-figure. 307 308 Parameters 309 ---------- 310 RayListHistory : list(list(Ray)) 311 A list of lists of objects of the ModuleOpticalRay.Ray-class. 312 313 EndDistance : float 314 The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). 315 316 maxRays : int, optional 317 The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog 318 and not required for a nice image. Default is 150. 319 320 Returns 321 ------- 322 meshes : list(pvPolyData) 323 List of Pyvista PolyData objects representing the rays 324 """ 325 meshes = [] 326 # Ray display 327 for k in range(len(RayListHistory)): 328 x = [] 329 y = [] 330 z = [] 331 if k != len(RayListHistory) - 1: 332 knums = list( 333 map(lambda x: x.number, RayListHistory[k]) 334 ) # make a list of all ray numbers that are still in the game 335 if len(RayListHistory[k + 1]) > maxRays: 336 rays_to_render = np.random.choice(RayListHistory[k + 1], maxRays, replace=False) 337 else: 338 rays_to_render = RayListHistory[k + 1] 339 340 for j in rays_to_render: 341 indx = knums.index(j.number) 342 i = RayListHistory[k][indx] 343 Point1 = i.point 344 Point2 = j.point 345 x += [Point1[0], Point2[0]] 346 y += [Point1[1], Point2[1]] 347 z += [Point1[2], Point2[2]] 348 349 else: 350 if len(RayListHistory[k]) > maxRays: 351 rays_to_render = np.random.choice(RayListHistory[k], maxRays, replace=False) 352 else: 353 rays_to_render = RayListHistory[k] 354 355 for j in rays_to_render: 356 Point = j.point 357 Vector = j.vector 358 x += [Point[0], Point[0] + Vector[0] * EndDistance] 359 y += [Point[1], Point[1] + Vector[1] * EndDistance] 360 z += [Point[2], Point[2] + Vector[2] * EndDistance] 361 points = np.column_stack((x, y, z)) 362 meshes += [pv.line_segments_from_points(points)] 363 return meshes
28def SupportRound_ContourSupport(self, Figure): 29 """Draw support contour in MirrorProjection plots.""" 30 axe = Figure.add_subplot(111, aspect="equal") 31 axe.add_patch(patches.Circle((0, 0), self.radius, alpha=0.08)) 32 return axe
Draw support contour in MirrorProjection plots.
36def SupportRoundHole_ContourSupport(self, Figure): 37 """Draws support contour in MirrorProjection plots.""" 38 axe = Figure.add_subplot(111, aspect="equal") 39 axe.add_patch(patches.Circle((0, 0), self.radius, alpha=0.08)) 40 axe.add_patch(patches.Circle((self.centerholeX, self.centerholeY), self.radiushole, color="white", alpha=1)) 41 return axe
Draws support contour in MirrorProjection plots.
45def SupportRectangle_ContourSupport(self, Figure): 46 """Draws support contour in MirrorProjection plots.""" 47 axe = Figure.add_subplot(111, aspect="equal") 48 axe.add_patch(patches.Rectangle((-self.dimX * 0.5, -self.dimY * 0.5), self.dimX, self.dimY, alpha=0.08)) 49 return axe
Draws support contour in MirrorProjection plots.
53def SupportRectangleHole_ContourSupport(self, Figure): 54 """Draws support contour in MirrorProjection plots.""" 55 axe = Figure.add_subplot(111, aspect="equal") 56 axe.add_patch(patches.Rectangle((-self.dimX * 0.5, -self.dimY * 0.5), self.dimX, self.dimY, alpha=0.08)) 57 axe.add_patch(patches.Circle((self.centerholeX, self.centerholeY), self.radiushole, color="white", alpha=1)) 58 return axe
Draws support contour in MirrorProjection plots.
62def SupportRectangleRectHole_ContourSupport(self, Figure): 63 """Draws support contour in MirrorProjection plots.""" 64 axe = Figure.add_subplot(111, aspect="equal") 65 axe.add_patch(patches.Rectangle((-self.dimX * 0.5, -self.dimY * 0.5), self.dimX, self.dimY, alpha=0.08)) 66 axe.add_patch( 67 patches.Rectangle( 68 (-self.holeX * 0.5 + self.centerholeX, -self.holeY * 0.5 + self.centerholeY), 69 self.holeX, 70 self.holeY, 71 color="white", 72 alpha=1, 73 ) 74 ) 75 return axe
Draws support contour in MirrorProjection plots.
81def sample_support(Support, Npoints): 82 """ 83 This function samples a regular grid on the support and filters out the points that are outside of the support. 84 For that it uses the _estimate_size method of the support. 85 It then generates a regular grid over that area and filters out the points that are outside of the support. 86 If less than half of the points are inside the support, it will increase the number of points by a factor of 2 and try again. 87 """ 88 if hasattr(Support, "size"): 89 size = Support.size 90 else: 91 size_x = Support._estimate_size() 92 size = np.array([2*size_x, 2*size_x]) 93 enough = False 94 while not enough: 95 X = np.linspace(-size[0] * 0.5, size[0] * 0.5, int(np.sqrt(Npoints))) 96 Y = np.linspace(-size[1] * 0.5, size[1] * 0.5, int(np.sqrt(Npoints))) 97 X, Y = np.meshgrid(X, Y) 98 Points = np.array([X.flatten(), Y.flatten()]).T 99 Points = [p for p in Points if p in Support] 100 if len(Points) > Npoints * 0.5: 101 enough = True 102 else: 103 Npoints *= 2 104 return mgeo.PointArray(Points)
This function samples a regular grid on the support and filters out the points that are outside of the support. For that it uses the _estimate_size method of the support. It then generates a regular grid over that area and filters out the points that are outside of the support. If less than half of the points are inside the support, it will increase the number of points by a factor of 2 and try again.
106def sample_z_values(Support, Npoints): 107 Npoints_init = Npoints 108 if hasattr(Support, "size"): 109 size = Support.size 110 else: 111 size_x = Support._estimate_size() 112 size = np.array([2*size_x, 2*size_x]) 113 enough = False 114 while not enough: 115 X = np.linspace(-size[0] * 0.5, size[0] * 0.5, int(np.sqrt(Npoints))) 116 Y = np.linspace(-size[1] * 0.5, size[1] * 0.5, int(np.sqrt(Npoints))) 117 X, Y = np.meshgrid(X, Y) 118 Points = np.array([X.flatten(), Y.flatten()]).T 119 Points = [p for p in Points if p in Support] 120 if len(Points) > Npoints_init * 0.5: 121 enough = True 122 else: 123 Npoints *= 2 124 Points = np.array([X.flatten(), Y.flatten()]).T 125 mask = np.array([p in Support for p in Points]).reshape(X.shape) 126 return X,Y,mask
Module containing some useful functions for generating various plots. It exists mostly to avoid cluttering the main plotting module.
Created in November 2024
@author: André Kalouguine + Stefan Haessler + Anthony Guillaume
1""" 2Module containing some useful functions for generating various plots. 3It exists mostly to avoid cluttering the main plotting module. 4 5Created in November 2024 6 7@author: André Kalouguine + Stefan Haessler + Anthony Guillaume 8""" 9# %% Module imports 10import weakref 11import numpy as np 12import colorcet as cc 13import matplotlib.pyplot as plt 14 15import ARTcore.ModuleProcessing as mp 16import ARTcore.ModuleGeometry as mgeo 17import ARTcore.ModuleProcessing as mp 18 19 20# %% Definition of observer class to make plots interactive 21class Observable(): 22 """ 23 Observer class to make several plots interactive simultaneously. 24 It encapsulates a single value. 25 When it's modified, it notifies all registered observers. 26 """ 27 def __init__(self, value): 28 self._value = value 29 self._observers = set() 30 self._calculation = set() 31 32 def register_calculation(self, callback): 33 """ 34 Register a calculation to be performed when the value is modified before notifying the observers. 35 For instance, this can be used to perform the ray tracing calculation when the value is modified. 36 The other callbacks will be notified after the calculation is performed and will update the plots. 37 """ 38 self._calculation.add(callback) 39 40 def unregister_calculation(self, callback): 41 self._calculation.discard(callback) 42 43 @property 44 def value(self): 45 return self._value 46 47 @value.setter 48 def value(self, new_value): 49 self._value = new_value 50 for callback in self._calculation: 51 callback(new_value) 52 self.notify(new_value) 53 54 def register(self, callback): 55 self._observers.add(callback) 56 57 def unregister(self, callback): 58 self._observers.discard(callback) 59 60 def notify(self, event): 61 for callback in self._observers: 62 callback(event) 63 64 65# %% Utility functions 66def generate_distinct_colors(num_colors): 67 """ 68 Utility function to generate a list of distinct colors for plotting. 69 70 Parameters 71 ---------- 72 num_colors : int 73 The number of colors to generate. 74 75 Returns 76 ------- 77 distinct_colors : list 78 List of distinct colors. 79 """ 80 # Get a color palette from colorcet 81 palette = cc.glasbey 82 83 # Make sure the number of colors does not exceed the palette length 84 num_colors = min(num_colors, len(palette)) 85 86 # Slice the palette to get the desired number of colors 87 distinct_colors = palette[:num_colors] 88 89 return distinct_colors 90 91 92def _getDetectorPoints(RayListAnalysed, Detector) -> tuple[np.ndarray, np.ndarray, float, float]: 93 """ 94 Prepare the ray impact points on the detector in a format used for the plotting, 95 and along the way also calculate the "spotsize" of this point-cloud on the detector. 96 97 Parameters 98 ---------- 99 RayListAnalysed : list(Ray) 100 A list of objects of the ModuleOpticalRay.Ray-class. 101 102 Detector : Detector 103 An object of the ModuleDetector.Detector-class. 104 105 Returns 106 ------- 107 DectectorPoint2D_Xcoord : np.ndarray with x-coordinates 108 109 DectectorPoint2D_Ycoord : np.ndarray with y-coordinates 110 111 FocalSpotSize : float 112 113 SpotSizeSD : float 114 """ 115 116 Points2D = Detector.get_2D_points(RayListAnalysed) 117 Points2D -= np.mean(Points2D, axis=1) # Centering the points 118 X = Points2D[0][:,0] * 1e3 # To convert to µm 119 Y = Points2D[0][:,1] * 1e3 # To convert to µm 120 121 FocalSpotSize = float(mgeo.DiameterPointArray(Points2D[0])) 122 123 IntensityList = [k.intensity for k in RayListAnalysed] 124 SpotSizeSD = mp.WeightedStandardDeviation(Points2D[0],IntensityList) 125 #SpotSizeSD = mp.StandardDeviation(Points2D[0]) 126 return X, Y, FocalSpotSize, SpotSizeSD 127 128def show(): 129 plt.show(block=False)
22class Observable(): 23 """ 24 Observer class to make several plots interactive simultaneously. 25 It encapsulates a single value. 26 When it's modified, it notifies all registered observers. 27 """ 28 def __init__(self, value): 29 self._value = value 30 self._observers = set() 31 self._calculation = set() 32 33 def register_calculation(self, callback): 34 """ 35 Register a calculation to be performed when the value is modified before notifying the observers. 36 For instance, this can be used to perform the ray tracing calculation when the value is modified. 37 The other callbacks will be notified after the calculation is performed and will update the plots. 38 """ 39 self._calculation.add(callback) 40 41 def unregister_calculation(self, callback): 42 self._calculation.discard(callback) 43 44 @property 45 def value(self): 46 return self._value 47 48 @value.setter 49 def value(self, new_value): 50 self._value = new_value 51 for callback in self._calculation: 52 callback(new_value) 53 self.notify(new_value) 54 55 def register(self, callback): 56 self._observers.add(callback) 57 58 def unregister(self, callback): 59 self._observers.discard(callback) 60 61 def notify(self, event): 62 for callback in self._observers: 63 callback(event)
Observer class to make several plots interactive simultaneously. It encapsulates a single value. When it's modified, it notifies all registered observers.
33 def register_calculation(self, callback): 34 """ 35 Register a calculation to be performed when the value is modified before notifying the observers. 36 For instance, this can be used to perform the ray tracing calculation when the value is modified. 37 The other callbacks will be notified after the calculation is performed and will update the plots. 38 """ 39 self._calculation.add(callback)
Register a calculation to be performed when the value is modified before notifying the observers. For instance, this can be used to perform the ray tracing calculation when the value is modified. The other callbacks will be notified after the calculation is performed and will update the plots.
67def generate_distinct_colors(num_colors): 68 """ 69 Utility function to generate a list of distinct colors for plotting. 70 71 Parameters 72 ---------- 73 num_colors : int 74 The number of colors to generate. 75 76 Returns 77 ------- 78 distinct_colors : list 79 List of distinct colors. 80 """ 81 # Get a color palette from colorcet 82 palette = cc.glasbey 83 84 # Make sure the number of colors does not exceed the palette length 85 num_colors = min(num_colors, len(palette)) 86 87 # Slice the palette to get the desired number of colors 88 distinct_colors = palette[:num_colors] 89 90 return distinct_colors
Utility function to generate a list of distinct colors for plotting.
num_colors : int
The number of colors to generate.
distinct_colors : list
List of distinct colors.
Provides functions for analysing the alignment tolerance of a system.
Mostly it consists in mis-aligning the optical elements of the system and checking the impact on the focal spot and delays.
There are two parts to this module:
The end goal is to have a simple function "GetTolerance" that will return the tolerance of the system to misalignments of each optical element.
Created in Nov 2024
@author: Andre Kalouguine
1""" 2Provides functions for analysing the alignment tolerance of a system. 3 4Mostly it consists in mis-aligning the optical elements of the system and checking the impact on the focal spot and delays. 5 6There are two parts to this module: 7- Misaligning/deteriorating the optical elements of the system 8- Analysing the impact of these misalignments on the focal spot and delays 9 10The end goal is to have a simple function "GetTolerance" that will return the tolerance of the system to misalignments of each optical element. 11 12Created in Nov 2024 13 14@author: Andre Kalouguine 15""" 16import numpy as np 17import matplotlib.pyplot as plt 18from copy import copy 19from scipy.stats import linregress 20 21import ARTcore.ModuleOpticalChain as moc 22 23 24def GetTolerance(OpticalChain, 25 time_error=1.0, 26 Detector = "Focus", 27 n_iter = 5, 28 elements_to_misalign = None): 29 """ 30 Returns the tolerance of the system to misalignments of each optical element. 31 It first constructs a normalisation vector: 32 For each optical element, for each degree of freedom, it iteratively calculates the required amplitude of misalignment to reach the time_error. 33 This gives a n-dimensional vector. The smaller the value, the more sensitive the system is to misalignments of this axis. 34 """ 35 # Define the normalisation vector 36 normalisation_vector = np.zeros(len(OpticalChain.optical_elements) * 6)*np.nan 37 durations = np.zeros(len(OpticalChain.optical_elements) * 6)*np.nan 38 misalignments = ["rotate_roll_by", "rotate_pitch_by", "rotate_yaw_by", "shift_along_normal", "shift_along_major", "shift_along_cross"] 39 if elements_to_misalign is None: 40 elements_to_misalign = range(len(OpticalChain.optical_elements)) 41 if isinstance(Detector, str): 42 Det = OpticalChain.detectors[Detector] 43 else: 44 Det = Detector 45 for i in elements_to_misalign: 46 for j in range(6): 47 # Misalign the optical element 48 amplitude = 1e-3 49 for k in range(n_iter): 50 try: 51 misaligned_optical_chain = copy(OpticalChain) 52 r_before = misaligned_optical_chain[i].r 53 q_before = misaligned_optical_chain[i].q 54 misaligned_optical_chain[i].__getattribute__(misalignments[j])(amplitude) 55 r_after = misaligned_optical_chain[i].r 56 q_after = misaligned_optical_chain[i].q 57 rays = misaligned_optical_chain.get_output_rays() 58 Det.optimise_distance(rays[Det.index], [Det.distance-100, Det.distance+100], Det._spot_size, maxiter=10, tol=1e-10) 59 except: 60 print(f"OE {i} failed to misalign {misalignments[j]} by {amplitude}") 61 amplitude /= 10 62 continue 63 rays = misaligned_optical_chain.get_output_rays(force=True) 64 duration = np.std(Det.get_Delays(rays[Det.index])) 65 if len(rays[Det.index]) <= 50: 66 amplitude /= 2 67 continue 68 if duration > time_error: 69 amplitude /= 3 70 elif duration < time_error / 10: 71 amplitude *= 3 72 elif duration < time_error / 1.5: 73 amplitude *= 1.2 74 else: 75 break 76 if not (time_error/2 < duration < time_error*2): 77 print(f"OE {i} failed to misalign {misalignments[j]} by {amplitude}: duration = {duration}") 78 if duration > time_error: 79 amplitude = np.nan 80 else: 81 amplitude = 0.1 82 normalisation_vector[i*6+j] = amplitude 83 durations[i*6+j] = duration 84 return normalisation_vector, durations
25def GetTolerance(OpticalChain, 26 time_error=1.0, 27 Detector = "Focus", 28 n_iter = 5, 29 elements_to_misalign = None): 30 """ 31 Returns the tolerance of the system to misalignments of each optical element. 32 It first constructs a normalisation vector: 33 For each optical element, for each degree of freedom, it iteratively calculates the required amplitude of misalignment to reach the time_error. 34 This gives a n-dimensional vector. The smaller the value, the more sensitive the system is to misalignments of this axis. 35 """ 36 # Define the normalisation vector 37 normalisation_vector = np.zeros(len(OpticalChain.optical_elements) * 6)*np.nan 38 durations = np.zeros(len(OpticalChain.optical_elements) * 6)*np.nan 39 misalignments = ["rotate_roll_by", "rotate_pitch_by", "rotate_yaw_by", "shift_along_normal", "shift_along_major", "shift_along_cross"] 40 if elements_to_misalign is None: 41 elements_to_misalign = range(len(OpticalChain.optical_elements)) 42 if isinstance(Detector, str): 43 Det = OpticalChain.detectors[Detector] 44 else: 45 Det = Detector 46 for i in elements_to_misalign: 47 for j in range(6): 48 # Misalign the optical element 49 amplitude = 1e-3 50 for k in range(n_iter): 51 try: 52 misaligned_optical_chain = copy(OpticalChain) 53 r_before = misaligned_optical_chain[i].r 54 q_before = misaligned_optical_chain[i].q 55 misaligned_optical_chain[i].__getattribute__(misalignments[j])(amplitude) 56 r_after = misaligned_optical_chain[i].r 57 q_after = misaligned_optical_chain[i].q 58 rays = misaligned_optical_chain.get_output_rays() 59 Det.optimise_distance(rays[Det.index], [Det.distance-100, Det.distance+100], Det._spot_size, maxiter=10, tol=1e-10) 60 except: 61 print(f"OE {i} failed to misalign {misalignments[j]} by {amplitude}") 62 amplitude /= 10 63 continue 64 rays = misaligned_optical_chain.get_output_rays(force=True) 65 duration = np.std(Det.get_Delays(rays[Det.index])) 66 if len(rays[Det.index]) <= 50: 67 amplitude /= 2 68 continue 69 if duration > time_error: 70 amplitude /= 3 71 elif duration < time_error / 10: 72 amplitude *= 3 73 elif duration < time_error / 1.5: 74 amplitude *= 1.2 75 else: 76 break 77 if not (time_error/2 < duration < time_error*2): 78 print(f"OE {i} failed to misalign {misalignments[j]} by {amplitude}: duration = {duration}") 79 if duration > time_error: 80 amplitude = np.nan 81 else: 82 amplitude = 0.1 83 normalisation_vector[i*6+j] = amplitude 84 durations[i*6+j] = duration 85 return normalisation_vector, durations
Returns the tolerance of the system to misalignments of each optical element. It first constructs a normalisation vector: For each optical element, for each degree of freedom, it iteratively calculates the required amplitude of misalignment to reach the time_error. This gives a n-dimensional vector. The smaller the value, the more sensitive the system is to misalignments of this axis.