ModuleProcessing
Contains processing functions used for the ray-tracing and automated (pre-)alignment of the optical chains. Usually these don't need to be called by users of ART, but they may be useful.
Created in Apr 2020
@author: Anthony Guillaume + Stefan Haessler
1""" 2Contains processing functions used for the ray-tracing and automated (pre-)alignment of the optical chains. 3Usually these don't need to be called by users of ART, but they may be useful. 4 5 6 7Created in Apr 2020 8 9@author: Anthony Guillaume + Stefan Haessler 10""" 11# %% Modules 12import ARTcore.ModuleGeometry as mgeo 13from ARTcore.ModuleGeometry import Point, Vector, Origin 14import ARTcore.ModuleMirror as mmirror 15import ARTcore.ModuleMask as mmask 16import ARTcore.ModuleSource as msource 17import ARTcore.ModuleOpticalElement as moe 18import ARTcore.ModuleOpticalRay as mray 19import ARTcore.ModuleSupport as msupp 20import ARTcore.ModuleOpticalChain as moc 21 22import os 23import pickle 24 25# import gzip 26import lzma 27from time import perf_counter 28from datetime import datetime 29from copy import copy 30import numpy as np 31import quaternion 32import logging 33 34logger = logging.getLogger(__name__) 35 36 37# %% 38def singleOEPlacement( 39 Optic: moe.OpticalElement, 40 Distance: float, 41 IncidenceAngle: float = 0, 42 IncidencePlaneAngle: float = 0, 43 InputRay: mray.Ray = None, 44 AlignmentVector: str = "", 45 PreviousIncidencePlane: mgeo.Vector = mgeo.Vector([0, 1, 0]) 46): 47 """ 48 Automatic placement and alignment of a single optical element. 49 The arguments are: 50 - The optic to be placed. 51 - The distance from the previous optic. 52 - An incidence angle. 53 - An incidence plane angle. 54 - An input ray. 55 - An alignment vector. 56 57 The alignment procedure is as follows: 58 - The optic is initially placed with its center at the source position (origin point of previous master ray). 59 - It's oriented such that the alignment vector is antiparallel to the master ray. By default, the alignment vector is the support_normal. 60 - The majoraxis of the optic is aligned with the incidence plane. 61 - The optic is rotated around the incidence plane by the incidence angle. 62 - The optic is rotated around the master ray by the incidence plane angle. 63 - The optic is translated along the master ray by the distance from the previous optic. 64 - The master ray is propagated through the optic without any blocking or diffraction effects and the output ray is used as the master ray for the next optic. 65 """ 66 # Convert angles to radian and wrap to 2pi 67 IncidencePlaneAngle = np.deg2rad(IncidencePlaneAngle) % (2 * np.pi) 68 IncidenceAngle = np.deg2rad(IncidenceAngle) % (2 * np.pi) 69 assert InputRay is not None, "InputRay must be provided" 70 OldOpticalElementCentre = InputRay.point 71 MasterRayDirection = InputRay.vector.normalized() 72 OpticalElementCentre = OldOpticalElementCentre + MasterRayDirection * Distance 73 74 logger.debug(f"Old Optical Element Centre: {OldOpticalElementCentre}") 75 logger.debug(f"Master Ray: {InputRay}") 76 logger.debug(f"Optical Element Centre: {OpticalElementCentre}") 77 # for convex mirrors, rotated them by 180° while keeping same incidence plane so we reflect from the "back side" 78 if Optic.curvature == mmirror.Curvature.CONVEX: 79 IncidenceAngle = np.pi - IncidenceAngle 80 81 if hasattr(Optic, AlignmentVector): 82 OpticVector = getattr(Optic, AlignmentVector) 83 else: 84 logger.warning(f"Optical Element {Optic} does not have an attribute {AlignmentVector}. Using support_normal instead.") 85 OpticVector = Optic.support_normal_ref 86 87 MajorAxis = Optic.majoraxis_ref 88 89 IncidencePlane = PreviousIncidencePlane.rotate(mgeo.QRotationAroundAxis(InputRay.vector, -IncidencePlaneAngle)) 90 # We calculate a quaternion that will rotate OpticVector against the master ray and MajorAxis into the incidence plane 91 # The convention is that the MajorAxis vector points right if seen from the source with IncidencePlane pointing up 92 # To do that, we first calculate a vector MinorAxis in the reference frame of the optic that is orthogonal to both OpticVector and MajorAxis 93 # Then we calculate a rotation matrix that will: 94 # - rotate MinorAxis into the IncidencePlane direction 95 # - rotate OpticVector against the master ray direction 96 # - rotate MajorAxis into the direction of the cross product of the two previous vectors 97 MinorAxis = -np.cross(OpticVector, MajorAxis) 98 qIncidenceAngle = mgeo.QRotationAroundAxis(IncidencePlane, -IncidenceAngle) 99 q = mgeo.QRotationVectorPair2VectorPair(MinorAxis, IncidencePlane, OpticVector, -MasterRayDirection) 100 q = qIncidenceAngle*q 101 Optic.q = q 102 logger.debug(f"Optic: {Optic}") 103 logger.debug(f"OpticVector: {OpticVector}") 104 logger.debug(f"Rotated OpticVector: {OpticVector.rotate(q)}") 105 logger.debug(f"MasterRayDirection: {MasterRayDirection}") 106 107 Optic.r = Origin + (OpticalElementCentre - Optic.centre - Optic.r) 108 109 NextRay = Optic.propagate_raylist(mray.RayList.from_list([InputRay]), alignment=True)[0] 110 return NextRay, IncidencePlane 111 112 113def OEPlacement( 114 OpticsList: list, 115 InitialRay:mray.Ray = None, 116 Source: msource.Source = None, 117 InputIncidencePlane: mgeo.Vector = None 118): 119 """ 120 Automatic placement and alignment of the optical elements for one optical chain. 121 Returns a list of optical elements (copied) 122 """ 123 if InitialRay is None: 124 if Source is None: 125 InputRay = InputRay = mray.Ray( 126 point=mgeo.Point([0, 0, 0]), 127 vector=mgeo.Vector([1, 0, 0]), 128 path=(0.0,), 129 number=0, 130 wavelength=800e-9, 131 incidence=0.0, 132 intensity=1.0 133 ) 134 else: 135 InputRay = Source.get_master_ray() 136 else: 137 InputRay = InitialRay.copy_ray() 138 if InputIncidencePlane is None: 139 InputIncidencePlane = mgeo.Vector([0, 1, 0]) 140 logger.debug(f"Initial Ray: {InputRay}") 141 logger.debug(f"Initial Incidence Plane: {InputIncidencePlane}") 142 assert np.linalg.norm(np.dot(InputRay.vector, InputIncidencePlane))<1e-6, "InputIncidencePlane is not orthogonal to InputRay.vector" 143 PreviousIncidencePlane = InputIncidencePlane.copy() 144 OEList = [] 145 for i in range(len(OpticsList)): 146 OEList.append(copy(OpticsList[i]["OpticalElement"])) 147 InputRay, PreviousIncidencePlane = singleOEPlacement( 148 OEList[-1], 149 OpticsList[i]["Distance"], 150 OpticsList[i]["IncidenceAngle"], 151 OpticsList[i]["IncidencePlaneAngle"], 152 InputRay, 153 OpticsList[i]["Alignment"] if "Alignment" in OpticsList[i] else "support_normal", 154 PreviousIncidencePlane 155 ) 156 return OEList 157 158 159 160# %% 161def RayTracingCalculation( 162 source_rays: mray.RayList, optical_elements: list[moe.OpticalElement], **kwargs 163) -> list[list[mray.Ray]]: 164 """ 165 The actual ray-tracing calculation, starting from the list of 'source_rays', 166 and propagating them from one optical element to the next in the order of the 167 items of the list 'optical_elements'. 168 169 170 Parameters 171 ---------- 172 source_rays : list[Ray-objects] 173 List of input rays. 174 175 optical_elements : list[OpticalElement-objects] 176 177 178 Returns 179 ------- 180 output_rays : list[list[Ray-objects]] 181 List of lists of rays, each item corresponding to the ray-bundle *after* 182 the item with the same index in the 'optical_elements'-list. 183 """ 184 output_rays = [] 185 186 for k in range(0, len(optical_elements)): 187 if k == 0: 188 RayList = source_rays 189 else: 190 RayList = output_rays[k - 1] 191 RayList = optical_elements[k].propagate_raylist(RayList, **kwargs) 192 output_rays.append(RayList) 193 194 return output_rays 195 196 197# %% 198def _FindOptimalDistanceBIS(movingDetector, Amplitude, Step, RayList, OptFor, IntensityWeighted): 199 ListSizeSpot = [] 200 ListDuration = [] 201 ListFitness = [] 202 if IntensityWeighted: 203 Weights = [k.intensity for k in RayList] 204 205 movingDetector.shiftByDistance(-Amplitude) 206 n = int(2 * Amplitude / Step) 207 for i in range(n): 208 ListPointDetector2DCentre = movingDetector.get_PointList2DCentre(RayList) 209 if OptFor in ["intensity", "spotsize"]: 210 if IntensityWeighted: 211 SpotSize = WeightedStandardDeviation(ListPointDetector2DCentre, Weights) 212 else: 213 SpotSize = StandardDeviation(ListPointDetector2DCentre) 214 ListSizeSpot.append(SpotSize) 215 216 if OptFor in ["intensity", "duration"]: 217 DelayList = movingDetector.get_Delays(RayList) 218 if IntensityWeighted: 219 Duration = WeightedStandardDeviation(DelayList, Weights) 220 else: 221 Duration = StandardDeviation(DelayList) 222 ListDuration.append(Duration) 223 224 if OptFor == "intensity": 225 Fitness = SpotSize**2 * Duration 226 elif OptFor == "duration": 227 Fitness = Duration 228 elif OptFor == "spotsize": 229 Fitness = SpotSize 230 ListFitness.append(Fitness) 231 232 movingDetector.shiftByDistance(Step) 233 234 FitnessMin = min(ListFitness) 235 ind = ListFitness.index(FitnessMin) 236 if OptFor in ["intensity", "spotsize"]: 237 OptSizeSpot = ListSizeSpot[ind] 238 else: 239 OptSizeSpot = np.nan 240 if OptFor in ["intensity", "duration"]: 241 OptDuration = ListDuration[ind] 242 else: 243 OptDuration = np.nan 244 245 movingDetector.shiftByDistance(-(n - ind) * Step) 246 247 return movingDetector, OptSizeSpot, OptDuration 248 249 250def FindOptimalDistance( 251 Detector, 252 RayList: list[mray.Ray], 253 OptFor="intensity", 254 Amplitude: float = None, 255 Precision: int = 3, 256 IntensityWeighted=False, 257 verbose=False, 258): 259 """ 260 Automatically finds the optimal the 'Detector'-distance for either 261 maximum intensity, or minimal spotsize or duration. 262 263 Parameters 264 ---------- 265 Detector : Detector-object 266 267 RayList : list[Ray-objects] 268 269 OptFor : str, optional 270 "spotsize": minimizes the standard-deviation spot size *d* on the detector. 271 "duration": minimizes the standard-deviation *tau* of the ray-delays. 272 "intensity": Maximizes 1/tau/d^2. 273 Defaults to "intensity". 274 275 Amplitude : float, optional 276 The detector-distances within which the optimization is done will be 277 the distance of 'Detector' +/- Amplitude in mm. 278 279 Precision : int, optional 280 Precision-parameter for the search algorithm. For Precision = n, 281 it will iterate with search amplitudes decreasing until 10^{-n}. 282 Defaults to 3. 283 284 IntensityWeighted : bool, optional 285 Whether to weigh the calculation of spotsize and/or duration by the ray-intensities. 286 Defaults to False. 287 288 verbose : bool, optional 289 Whether to print results to the console. 290 291 Returns 292 ------- 293 OptDetector : Detector-object 294 A new detector-instance with optimized distance. 295 296 OptSpotSize : float 297 Spotsize, calculated as standard deviation in mm of the spot-diagram 298 on the optimized detector. 299 300 OptDuration : float 301 Duration, calculated as standard deviation in fs of the ray-delays 302 on the optimized detector. 303 """ 304 305 if OptFor not in ["intensity", "size", "duration"]: 306 raise NameError( 307 "I don`t recognize what you want to optimize the detector distance for. OptFor must be either 'intensity', 'size' or 'duration'." 308 ) 309 310 FirstDistance = Detector.get_distance() 311 ListPointDetector2DCentre = Detector.get_PointList2DCentre(RayList) 312 SizeSpot = 2 * StandardDeviation(ListPointDetector2DCentre) 313 NumericalAperture = ReturnNumericalAperture(RayList, 1) 314 if Amplitude is None: 315 Amplitude = min(4 * np.ceil(SizeSpot / np.tan(np.arcsin(NumericalAperture))), FirstDistance) 316 Step = Amplitude/10 317 #Step = Amplitude / 5 # pretty good in half the time ;-) 318 319 if verbose: 320 print( 321 f"Searching optimal detector position for *{OptFor}* within [{FirstDistance-Amplitude:.3f}, {FirstDistance+Amplitude:.3f}] mm...", 322 end="", 323 flush=True, 324 ) 325 movingDetector = Detector.copy_detector() 326 for k in range(Precision + 1): 327 movingDetector, OptSpotSize, OptDuration = _FindOptimalDistanceBIS( 328 movingDetector, Amplitude * 0.1**k, Step * 0.1**k, RayList, OptFor, IntensityWeighted 329 ) 330 331 if ( 332 not FirstDistance - Amplitude + 10**-Precision 333 < movingDetector.get_distance() 334 < FirstDistance + Amplitude - 10**-Precision 335 ): 336 print("There`s no minimum-size/duration focus in the searched range.") 337 338 print( 339 "\r\033[K", end="", flush=True 340 ) # move to beginning of the line with \r and then delete the whole line with \033[K 341 return movingDetector, OptSpotSize, OptDuration 342 343 344# %% 345def FindCentralRay(RayList: list[mray.Ray]): 346 """ 347 Out of a the list of Ray-objects, RayList, determine the average direction 348 vector and source point, and the return a Ray-object representing this 349 central ray of the RayList. 350 351 Parameters 352 ---------- 353 RayList : list of Ray-objects 354 355 Returns 356 ------- 357 CentralRay : Ray-object 358 """ 359 360 CentralVector = np.mean( [x.vector for x in RayList], axis=0) 361 CentralPoint = np.mean( [x.point for x in RayList], axis=0) 362 363 return mray.Ray(mgeo.Point(CentralPoint), mgeo.Vector(CentralVector)) 364 365 366def StandardDeviation(List: list[float, np.ndarray]) -> float: 367 """ 368 Return the standard deviation of elements in the supplied list. 369 If the elements are floats, it's simply the root-mean-square over the numbers. 370 If the elements are vectors (represented as numpy-arrays), the root-mean-square 371 of the distance of the points from their mean is calculated. 372 373 Parameters 374 ---------- 375 List : list of floats or of np.ndarray 376 Numbers (in ART, typically delays) 377 or 2D or 3D vectors (in ART typically space coordinates) 378 379 Returns 380 ------- 381 Std : float 382 """ 383 if type(List[0]) in [int, float, np.float64]: 384 return np.std(List) 385 elif len(List[0]) > 1: 386 return np.sqrt(np.var(List, axis=0).sum()) 387 else: 388 raise ValueError("StandardDeviation expects a list of floats or numpy-arrays as input, but got something else.") 389 390 391def WeightedStandardDeviation(List: list[float, np.ndarray], Weights: list[float]) -> float: 392 """ 393 Return the weighted standard deviation of elements in the supplied list. 394 If the elements are floats, it's simply the root-mean-square over the weighted numbers. 395 If the elements are vectors (represented as numpy-arrays), the root-mean-square 396 of the weighted distances of the points from their mean is calculated. 397 398 Parameters 399 ---------- 400 List : list of floats or of np.ndarray 401 Numbers (in ART, typically delays) 402 or 2D or 3D vectors (in ART typically space coordinates) 403 404 Weights : list of floats, same length as List 405 Parameters such as Ray.intensity to be used as weights 406 407 Returns 408 ------- 409 Std : float 410 """ 411 average = np.average(List, axis=0, weights=Weights, returned=False) 412 variance = np.average((List - average) ** 2, axis=0, weights=Weights, returned=False) 413 return np.sqrt(variance.sum()) 414 415 416# %% 417def _hash_list_of_objects(list): 418 """returns a hash value for a list of objects, by just summing up all the individual hashes.""" 419 total_hash = 0 420 for x in list: 421 total_hash += hash(x) 422 return total_hash 423 424 425def _which_indeces(lst): 426 """takes an input-list, and gives you a list of the indeces where the input-list contains another list or a numpy-array""" 427 indexes = [i for i, x in enumerate(lst) if isinstance(x, (list, np.ndarray))] 428 return indexes 429 430 431# %% 432def save_compressed(obj, filename: str = None): 433 """Save (=pickle) an object 'obj' to a compressed file with name 'filename'.""" 434 if not type(filename) == str: 435 filename = "kept_data_" + datetime.now().strftime("%Y-%m-%d-%Hh%M") 436 437 i = 0 438 while os.path.exists(filename + f"_{i}.xz"): 439 i += 1 440 filename = filename + f"_{i}" 441 # with gzip.open(filename + '.gz', 'wb') as f: 442 with lzma.open(filename + ".xz", "wb") as f: 443 pickle.dump(obj, f) 444 print("Saved results to " + filename + ".xz.") 445 print("->To reload from disk do: kept_data = mp.load_compressed('" + filename + "')") 446 447 448def load_compressed(filename: str): 449 """Load (=unpickle) an object 'obj' from a compressed file with name 'filename'.""" 450 # with gzip.open(filename + '.gz', 'rb') as f: 451 with lzma.open(filename + ".xz", "rb") as f: 452 obj = pickle.load(f) 453 return obj 454 455 456# %% tic-toc timer functions 457_tstart_stack = [] 458 459 460def _tic(): 461 _tstart_stack.append(perf_counter()) 462 463 464def _toc(fmt="Elapsed: %s s"): 465 print(fmt % (perf_counter() - _tstart_stack.pop()))
39def singleOEPlacement( 40 Optic: moe.OpticalElement, 41 Distance: float, 42 IncidenceAngle: float = 0, 43 IncidencePlaneAngle: float = 0, 44 InputRay: mray.Ray = None, 45 AlignmentVector: str = "", 46 PreviousIncidencePlane: mgeo.Vector = mgeo.Vector([0, 1, 0]) 47): 48 """ 49 Automatic placement and alignment of a single optical element. 50 The arguments are: 51 - The optic to be placed. 52 - The distance from the previous optic. 53 - An incidence angle. 54 - An incidence plane angle. 55 - An input ray. 56 - An alignment vector. 57 58 The alignment procedure is as follows: 59 - The optic is initially placed with its center at the source position (origin point of previous master ray). 60 - It's oriented such that the alignment vector is antiparallel to the master ray. By default, the alignment vector is the support_normal. 61 - The majoraxis of the optic is aligned with the incidence plane. 62 - The optic is rotated around the incidence plane by the incidence angle. 63 - The optic is rotated around the master ray by the incidence plane angle. 64 - The optic is translated along the master ray by the distance from the previous optic. 65 - The master ray is propagated through the optic without any blocking or diffraction effects and the output ray is used as the master ray for the next optic. 66 """ 67 # Convert angles to radian and wrap to 2pi 68 IncidencePlaneAngle = np.deg2rad(IncidencePlaneAngle) % (2 * np.pi) 69 IncidenceAngle = np.deg2rad(IncidenceAngle) % (2 * np.pi) 70 assert InputRay is not None, "InputRay must be provided" 71 OldOpticalElementCentre = InputRay.point 72 MasterRayDirection = InputRay.vector.normalized() 73 OpticalElementCentre = OldOpticalElementCentre + MasterRayDirection * Distance 74 75 logger.debug(f"Old Optical Element Centre: {OldOpticalElementCentre}") 76 logger.debug(f"Master Ray: {InputRay}") 77 logger.debug(f"Optical Element Centre: {OpticalElementCentre}") 78 # for convex mirrors, rotated them by 180° while keeping same incidence plane so we reflect from the "back side" 79 if Optic.curvature == mmirror.Curvature.CONVEX: 80 IncidenceAngle = np.pi - IncidenceAngle 81 82 if hasattr(Optic, AlignmentVector): 83 OpticVector = getattr(Optic, AlignmentVector) 84 else: 85 logger.warning(f"Optical Element {Optic} does not have an attribute {AlignmentVector}. Using support_normal instead.") 86 OpticVector = Optic.support_normal_ref 87 88 MajorAxis = Optic.majoraxis_ref 89 90 IncidencePlane = PreviousIncidencePlane.rotate(mgeo.QRotationAroundAxis(InputRay.vector, -IncidencePlaneAngle)) 91 # We calculate a quaternion that will rotate OpticVector against the master ray and MajorAxis into the incidence plane 92 # The convention is that the MajorAxis vector points right if seen from the source with IncidencePlane pointing up 93 # To do that, we first calculate a vector MinorAxis in the reference frame of the optic that is orthogonal to both OpticVector and MajorAxis 94 # Then we calculate a rotation matrix that will: 95 # - rotate MinorAxis into the IncidencePlane direction 96 # - rotate OpticVector against the master ray direction 97 # - rotate MajorAxis into the direction of the cross product of the two previous vectors 98 MinorAxis = -np.cross(OpticVector, MajorAxis) 99 qIncidenceAngle = mgeo.QRotationAroundAxis(IncidencePlane, -IncidenceAngle) 100 q = mgeo.QRotationVectorPair2VectorPair(MinorAxis, IncidencePlane, OpticVector, -MasterRayDirection) 101 q = qIncidenceAngle*q 102 Optic.q = q 103 logger.debug(f"Optic: {Optic}") 104 logger.debug(f"OpticVector: {OpticVector}") 105 logger.debug(f"Rotated OpticVector: {OpticVector.rotate(q)}") 106 logger.debug(f"MasterRayDirection: {MasterRayDirection}") 107 108 Optic.r = Origin + (OpticalElementCentre - Optic.centre - Optic.r) 109 110 NextRay = Optic.propagate_raylist(mray.RayList.from_list([InputRay]), alignment=True)[0] 111 return NextRay, IncidencePlane
Automatic placement and alignment of a single optical element. The arguments are:
- The optic to be placed.
- The distance from the previous optic.
- An incidence angle.
- An incidence plane angle.
- An input ray.
- An alignment vector.
The alignment procedure is as follows:
- The optic is initially placed with its center at the source position (origin point of previous master ray).
- It's oriented such that the alignment vector is antiparallel to the master ray. By default, the alignment vector is the support_normal.
- The majoraxis of the optic is aligned with the incidence plane.
- The optic is rotated around the incidence plane by the incidence angle.
- The optic is rotated around the master ray by the incidence plane angle.
- The optic is translated along the master ray by the distance from the previous optic.
- The master ray is propagated through the optic without any blocking or diffraction effects and the output ray is used as the master ray for the next optic.
114def OEPlacement( 115 OpticsList: list, 116 InitialRay:mray.Ray = None, 117 Source: msource.Source = None, 118 InputIncidencePlane: mgeo.Vector = None 119): 120 """ 121 Automatic placement and alignment of the optical elements for one optical chain. 122 Returns a list of optical elements (copied) 123 """ 124 if InitialRay is None: 125 if Source is None: 126 InputRay = InputRay = mray.Ray( 127 point=mgeo.Point([0, 0, 0]), 128 vector=mgeo.Vector([1, 0, 0]), 129 path=(0.0,), 130 number=0, 131 wavelength=800e-9, 132 incidence=0.0, 133 intensity=1.0 134 ) 135 else: 136 InputRay = Source.get_master_ray() 137 else: 138 InputRay = InitialRay.copy_ray() 139 if InputIncidencePlane is None: 140 InputIncidencePlane = mgeo.Vector([0, 1, 0]) 141 logger.debug(f"Initial Ray: {InputRay}") 142 logger.debug(f"Initial Incidence Plane: {InputIncidencePlane}") 143 assert np.linalg.norm(np.dot(InputRay.vector, InputIncidencePlane))<1e-6, "InputIncidencePlane is not orthogonal to InputRay.vector" 144 PreviousIncidencePlane = InputIncidencePlane.copy() 145 OEList = [] 146 for i in range(len(OpticsList)): 147 OEList.append(copy(OpticsList[i]["OpticalElement"])) 148 InputRay, PreviousIncidencePlane = singleOEPlacement( 149 OEList[-1], 150 OpticsList[i]["Distance"], 151 OpticsList[i]["IncidenceAngle"], 152 OpticsList[i]["IncidencePlaneAngle"], 153 InputRay, 154 OpticsList[i]["Alignment"] if "Alignment" in OpticsList[i] else "support_normal", 155 PreviousIncidencePlane 156 ) 157 return OEList
Automatic placement and alignment of the optical elements for one optical chain. Returns a list of optical elements (copied)
162def RayTracingCalculation( 163 source_rays: mray.RayList, optical_elements: list[moe.OpticalElement], **kwargs 164) -> list[list[mray.Ray]]: 165 """ 166 The actual ray-tracing calculation, starting from the list of 'source_rays', 167 and propagating them from one optical element to the next in the order of the 168 items of the list 'optical_elements'. 169 170 171 Parameters 172 ---------- 173 source_rays : list[Ray-objects] 174 List of input rays. 175 176 optical_elements : list[OpticalElement-objects] 177 178 179 Returns 180 ------- 181 output_rays : list[list[Ray-objects]] 182 List of lists of rays, each item corresponding to the ray-bundle *after* 183 the item with the same index in the 'optical_elements'-list. 184 """ 185 output_rays = [] 186 187 for k in range(0, len(optical_elements)): 188 if k == 0: 189 RayList = source_rays 190 else: 191 RayList = output_rays[k - 1] 192 RayList = optical_elements[k].propagate_raylist(RayList, **kwargs) 193 output_rays.append(RayList) 194 195 return output_rays
The actual ray-tracing calculation, starting from the list of 'source_rays', and propagating them from one optical element to the next in the order of the items of the list 'optical_elements'.
Parameters
source_rays : list[Ray-objects]
List of input rays.
optical_elements : list[OpticalElement-objects]
Returns
output_rays : list[list[Ray-objects]]
List of lists of rays, each item corresponding to the ray-bundle *after*
the item with the same index in the 'optical_elements'-list.
251def FindOptimalDistance( 252 Detector, 253 RayList: list[mray.Ray], 254 OptFor="intensity", 255 Amplitude: float = None, 256 Precision: int = 3, 257 IntensityWeighted=False, 258 verbose=False, 259): 260 """ 261 Automatically finds the optimal the 'Detector'-distance for either 262 maximum intensity, or minimal spotsize or duration. 263 264 Parameters 265 ---------- 266 Detector : Detector-object 267 268 RayList : list[Ray-objects] 269 270 OptFor : str, optional 271 "spotsize": minimizes the standard-deviation spot size *d* on the detector. 272 "duration": minimizes the standard-deviation *tau* of the ray-delays. 273 "intensity": Maximizes 1/tau/d^2. 274 Defaults to "intensity". 275 276 Amplitude : float, optional 277 The detector-distances within which the optimization is done will be 278 the distance of 'Detector' +/- Amplitude in mm. 279 280 Precision : int, optional 281 Precision-parameter for the search algorithm. For Precision = n, 282 it will iterate with search amplitudes decreasing until 10^{-n}. 283 Defaults to 3. 284 285 IntensityWeighted : bool, optional 286 Whether to weigh the calculation of spotsize and/or duration by the ray-intensities. 287 Defaults to False. 288 289 verbose : bool, optional 290 Whether to print results to the console. 291 292 Returns 293 ------- 294 OptDetector : Detector-object 295 A new detector-instance with optimized distance. 296 297 OptSpotSize : float 298 Spotsize, calculated as standard deviation in mm of the spot-diagram 299 on the optimized detector. 300 301 OptDuration : float 302 Duration, calculated as standard deviation in fs of the ray-delays 303 on the optimized detector. 304 """ 305 306 if OptFor not in ["intensity", "size", "duration"]: 307 raise NameError( 308 "I don`t recognize what you want to optimize the detector distance for. OptFor must be either 'intensity', 'size' or 'duration'." 309 ) 310 311 FirstDistance = Detector.get_distance() 312 ListPointDetector2DCentre = Detector.get_PointList2DCentre(RayList) 313 SizeSpot = 2 * StandardDeviation(ListPointDetector2DCentre) 314 NumericalAperture = ReturnNumericalAperture(RayList, 1) 315 if Amplitude is None: 316 Amplitude = min(4 * np.ceil(SizeSpot / np.tan(np.arcsin(NumericalAperture))), FirstDistance) 317 Step = Amplitude/10 318 #Step = Amplitude / 5 # pretty good in half the time ;-) 319 320 if verbose: 321 print( 322 f"Searching optimal detector position for *{OptFor}* within [{FirstDistance-Amplitude:.3f}, {FirstDistance+Amplitude:.3f}] mm...", 323 end="", 324 flush=True, 325 ) 326 movingDetector = Detector.copy_detector() 327 for k in range(Precision + 1): 328 movingDetector, OptSpotSize, OptDuration = _FindOptimalDistanceBIS( 329 movingDetector, Amplitude * 0.1**k, Step * 0.1**k, RayList, OptFor, IntensityWeighted 330 ) 331 332 if ( 333 not FirstDistance - Amplitude + 10**-Precision 334 < movingDetector.get_distance() 335 < FirstDistance + Amplitude - 10**-Precision 336 ): 337 print("There`s no minimum-size/duration focus in the searched range.") 338 339 print( 340 "\r\033[K", end="", flush=True 341 ) # move to beginning of the line with \r and then delete the whole line with \033[K 342 return movingDetector, OptSpotSize, OptDuration
Automatically finds the optimal the 'Detector'-distance for either maximum intensity, or minimal spotsize or duration.
Parameters
Detector : Detector-object
RayList : list[Ray-objects]
OptFor : str, optional
"spotsize": minimizes the standard-deviation spot size *d* on the detector.
"duration": minimizes the standard-deviation *tau* of the ray-delays.
"intensity": Maximizes 1/tau/d^2.
Defaults to "intensity".
Amplitude : float, optional
The detector-distances within which the optimization is done will be
the distance of 'Detector' +/- Amplitude in mm.
Precision : int, optional
Precision-parameter for the search algorithm. For Precision = n,
it will iterate with search amplitudes decreasing until 10^{-n}.
Defaults to 3.
IntensityWeighted : bool, optional
Whether to weigh the calculation of spotsize and/or duration by the ray-intensities.
Defaults to False.
verbose : bool, optional
Whether to print results to the console.
Returns
OptDetector : Detector-object
A new detector-instance with optimized distance.
OptSpotSize : float
Spotsize, calculated as standard deviation in mm of the spot-diagram
on the optimized detector.
OptDuration : float
Duration, calculated as standard deviation in fs of the ray-delays
on the optimized detector.
346def FindCentralRay(RayList: list[mray.Ray]): 347 """ 348 Out of a the list of Ray-objects, RayList, determine the average direction 349 vector and source point, and the return a Ray-object representing this 350 central ray of the RayList. 351 352 Parameters 353 ---------- 354 RayList : list of Ray-objects 355 356 Returns 357 ------- 358 CentralRay : Ray-object 359 """ 360 361 CentralVector = np.mean( [x.vector for x in RayList], axis=0) 362 CentralPoint = np.mean( [x.point for x in RayList], axis=0) 363 364 return mray.Ray(mgeo.Point(CentralPoint), mgeo.Vector(CentralVector))
Out of a the list of Ray-objects, RayList, determine the average direction vector and source point, and the return a Ray-object representing this central ray of the RayList.
Parameters
RayList : list of Ray-objects
Returns
CentralRay : Ray-object
367def StandardDeviation(List: list[float, np.ndarray]) -> float: 368 """ 369 Return the standard deviation of elements in the supplied list. 370 If the elements are floats, it's simply the root-mean-square over the numbers. 371 If the elements are vectors (represented as numpy-arrays), the root-mean-square 372 of the distance of the points from their mean is calculated. 373 374 Parameters 375 ---------- 376 List : list of floats or of np.ndarray 377 Numbers (in ART, typically delays) 378 or 2D or 3D vectors (in ART typically space coordinates) 379 380 Returns 381 ------- 382 Std : float 383 """ 384 if type(List[0]) in [int, float, np.float64]: 385 return np.std(List) 386 elif len(List[0]) > 1: 387 return np.sqrt(np.var(List, axis=0).sum()) 388 else: 389 raise ValueError("StandardDeviation expects a list of floats or numpy-arrays as input, but got something else.")
Return the standard deviation of elements in the supplied list. If the elements are floats, it's simply the root-mean-square over the numbers. If the elements are vectors (represented as numpy-arrays), the root-mean-square of the distance of the points from their mean is calculated.
Parameters
List : list of floats or of np.ndarray
Numbers (in ART, typically delays)
or 2D or 3D vectors (in ART typically space coordinates)
Returns
Std : float
392def WeightedStandardDeviation(List: list[float, np.ndarray], Weights: list[float]) -> float: 393 """ 394 Return the weighted standard deviation of elements in the supplied list. 395 If the elements are floats, it's simply the root-mean-square over the weighted numbers. 396 If the elements are vectors (represented as numpy-arrays), the root-mean-square 397 of the weighted distances of the points from their mean is calculated. 398 399 Parameters 400 ---------- 401 List : list of floats or of np.ndarray 402 Numbers (in ART, typically delays) 403 or 2D or 3D vectors (in ART typically space coordinates) 404 405 Weights : list of floats, same length as List 406 Parameters such as Ray.intensity to be used as weights 407 408 Returns 409 ------- 410 Std : float 411 """ 412 average = np.average(List, axis=0, weights=Weights, returned=False) 413 variance = np.average((List - average) ** 2, axis=0, weights=Weights, returned=False) 414 return np.sqrt(variance.sum())
Return the weighted standard deviation of elements in the supplied list. If the elements are floats, it's simply the root-mean-square over the weighted numbers. If the elements are vectors (represented as numpy-arrays), the root-mean-square of the weighted distances of the points from their mean is calculated.
Parameters
List : list of floats or of np.ndarray
Numbers (in ART, typically delays)
or 2D or 3D vectors (in ART typically space coordinates)
Weights : list of floats, same length as List
Parameters such as Ray.intensity to be used as weights
Returns
Std : float
433def save_compressed(obj, filename: str = None): 434 """Save (=pickle) an object 'obj' to a compressed file with name 'filename'.""" 435 if not type(filename) == str: 436 filename = "kept_data_" + datetime.now().strftime("%Y-%m-%d-%Hh%M") 437 438 i = 0 439 while os.path.exists(filename + f"_{i}.xz"): 440 i += 1 441 filename = filename + f"_{i}" 442 # with gzip.open(filename + '.gz', 'wb') as f: 443 with lzma.open(filename + ".xz", "wb") as f: 444 pickle.dump(obj, f) 445 print("Saved results to " + filename + ".xz.") 446 print("->To reload from disk do: kept_data = mp.load_compressed('" + filename + "')")
Save (=pickle) an object 'obj' to a compressed file with name 'filename'.
449def load_compressed(filename: str): 450 """Load (=unpickle) an object 'obj' from a compressed file with name 'filename'.""" 451 # with gzip.open(filename + '.gz', 'rb') as f: 452 with lzma.open(filename + ".xz", "rb") as f: 453 obj = pickle.load(f) 454 return obj
Load (=unpickle) an object 'obj' from a compressed file with name 'filename'.