ModuleOpticalChain
Provides the class OpticalChain, which represents the whole optical setup to be simulated, from the bundle of source-Rays through the successive OpticalElements.
Created in Jan 2022
@author: Stefan Haessler
1""" 2Provides the class OpticalChain, which represents the whole optical setup to be simulated, 3from the bundle of source-[Rays](ModuleOpticalRay.html) through the successive [OpticalElements](ModuleOpticalElement.html). 4 5 6 7 8 9Created in Jan 2022 10 11@author: Stefan Haessler 12""" 13# %% Modules 14import copy 15import numpy as np 16import logging 17 18import ARTcore.ModuleProcessing as mp 19import ARTcore.ModuleGeometry as mgeo 20from ARTcore.ModuleGeometry import Point, Vector, Origin 21import ARTcore.ModuleOpticalRay as mray 22import ARTcore.ModuleOpticalElement as moe 23import ARTcore.ModuleDetector as mdet 24import ARTcore.ModuleSource as msource 25 26logger = logging.getLogger(__name__) 27 28# %% 29class OpticalChain: 30 """ 31 The OpticalChain represents the whole optical setup to be simulated: 32 Its main attributes are a list source-[Rays](ModuleOpticalRay.html) and 33 a list of successive [OpticalElements](ModuleOpticalElement.html). 34 35 The method OpticalChain.get_output_rays() returns an associated list of lists of 36 [Rays](ModuleOpticalRay.html), each calculated by ray-tracing from one 37 [OpticalElement](ModuleOpticalElement.html) to the next. 38 So OpticalChain.get_output_rays()[i] is the bundle of [Rays](ModuleOpticalRay.html) *after* 39 optical_elements[i]. 40 41 The string "description" can contain a short description of the optical setup, or similar notes. 42 43 The OpticalChain can be visualized quickly with the method OpticalChain.quickshow(), 44 and more nicely with OpticalChain.render(). 45 46 The class also provides methods for (mis-)alignment of the source-[Ray](ModuleOpticalRay.html)-bundle and the 47 [OpticalElements](ModuleOpticalElement.html). 48 Attributes 49 ---------- 50 source_rays : list[mray.Ray] 51 List of source rays, which are to be traced. 52 53 optical_elements : list[moe.OpticalElement] 54 List of successive optical elements. 55 56 detector: mdet.Detector (optional) 57 The detector (or list of detectors) to analyse the results. 58 59 description : str 60 A string to describe the optical setup. 61 Methods 62 ---------- 63 copy_chain() 64 65 get_output_rays() 66 67 quickshow() 68 69 render() 70 71 ---------- 72 73 shift_source(axis, distance) 74 75 tilt_source(self, axis, angle) 76 77 ---------- 78 79 rotate_OE(OEindx, axis, angle) 80 81 shift_OE(OEindx, axis, distance) 82 83 """ 84 85 def __init__(self, source_rays, optical_elements, detectors, description=""): 86 """ 87 Parameters 88 ---------- 89 source_rays : list[mray.Ray] 90 List of source rays, which are to be traced. 91 92 optical_elements : list[moe.OpticalElement] 93 List of successive optical elements. 94 95 detector: mdet.Detector (optional) 96 The detector (or list of detectors) to analyse the results. 97 98 description : str, optional 99 A string to describe the optical setup. Defaults to ''. 100 101 """ 102 self.source_rays = copy.deepcopy(source_rays) 103 # deepcopy so this object doesn't get changed when the global source_rays changes "outside" 104 self.optical_elements = copy.deepcopy(optical_elements) 105 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 106 self.detectors = detectors 107 if isinstance(detectors, mdet.Detector): 108 self.detectors = {"Focus": detectors} 109 self.description = description 110 self._output_rays = None 111 self._last_source_rays_hash = None 112 self._last_optical_elements_hash = None 113 114 def __repr__(self): 115 pretty_str = "Optical setup [OpticalChain]:\n" 116 pretty_str += f" - Description: {self.description}\n" if self.description else "Description: Not provided.\n" 117 pretty_str += " - Contains the following elements:\n" 118 pretty_str += f" - Source with {len(self.source_rays)} rays at coordinate origin\n" 119 prev_pos = np.zeros(3) 120 for i, element in enumerate(self.optical_elements): 121 dist = (element.position - prev_pos).norm 122 if i == 0: 123 prev = "source" 124 else: 125 prev = f"element {i-1}" 126 pretty_str += f" - Element {i}: {element.type} at distance {round(dist)} from {prev}\n" 127 prev_pos = element.position 128 for i in self.detectors.keys(): 129 detector = self.detectors[i] 130 pretty_str += f' - Detector "{i}": {detector.type} at distance {round(detector.distance,3)} from element {detector.index % len(self.optical_elements)}\n' 131 return pretty_str 132 133 def __getitem__(self, i): 134 return self.optical_elements[i] 135 136 def __len__(self): 137 return len(self.optical_elements) 138 139 @property 140 def source_rays(self): 141 return self._source_rays 142 143 @source_rays.setter 144 def source_rays(self, source_rays): 145 if type(source_rays) == mray.RayList: 146 self._source_rays = source_rays 147 else: 148 raise TypeError("Source_rays must be a RayList object.") 149 150 @property 151 def optical_elements(self): 152 return self._optical_elements 153 154 @optical_elements.setter 155 def optical_elements(self, optical_elements): 156 if type(optical_elements) == list and all(isinstance(x, moe.OpticalElement) for x in optical_elements): 157 self._optical_elements = optical_elements 158 else: 159 raise TypeError("Optical_elements must be list of OpticalElement-objects.") 160 161 # %% METHODS ################################################################## 162 163 def __copy__(self): 164 """Return another optical chain with the same source, optical elements and description-string as this one.""" 165 return OpticalChain(self.source_rays, self.optical_elements, self.detectors, self.description) 166 167 def __deepcopy__(self, memo): 168 """Return another optical chain with the same source, optical elements and description-string as this one.""" 169 return OpticalChain(copy.deepcopy(self.source_rays), copy.deepcopy(self.optical_elements), memo+"\n"+copy.copy(self.description)) 170 171 def get_input_rays(self): 172 """ 173 Returns the list of source rays. 174 """ 175 return self.source_rays 176 177 def get_output_rays(self, force=False, **kwargs): 178 """ 179 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 180 or if the source-ray-bundle or anything about the optical elements has changed. 181 182 This is the user-facing method to perform the ray-tracing calculation. 183 """ 184 current_source_rays_hash = hash(self.source_rays) 185 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 186 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 187 print("...ray-tracing...", end="", flush=True) 188 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 189 print( 190 "\r\033[K", end="", flush=True 191 ) # move to beginning of the line with \r and then delete the whole line with \033[K 192 self._last_source_rays_hash = current_source_rays_hash 193 self._last_optical_elements_hash = current_optical_elements_hash 194 195 return self._output_rays 196 197 def get2dPoints(self, Detector = "Focus"): 198 """ 199 Returns the 2D points of the detected rays on the detector. 200 """ 201 if isinstance(Detector, str): 202 if Detector in self.detectors.keys(): 203 Detector = self.detectors[Detector] 204 else: 205 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 206 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0] 207 208 def get3dPoints(self, Detector = "Focus"): 209 """ 210 Returns the 3D points of the detected rays on the detector. 211 """ 212 if isinstance(Detector, str): 213 if Detector in self.detectors.keys(): 214 Detector = self.detectors[Detector] 215 else: 216 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 217 return Detector.get_3D_points(self.get_output_rays()[Detector.index]) 218 219 def getDelays(self, Detector = "Focus"): 220 """ 221 Returns the delays of the detected rays on the detector. 222 """ 223 if isinstance(Detector, str): 224 if Detector in self.detectors.keys(): 225 Detector = self.detectors[Detector] 226 else: 227 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 228 return Detector.get_Delays(self.get_output_rays()[Detector.index]) 229 230 # %% methods to (mis-)align the optical chain; just uses the corresponding methods of the OpticalElement class... 231 232 def shift_source(self, axis: str|np.ndarray, distance: float): 233 """ 234 Shift source ray bundle by distance (in mm) along the 'axis' specified as 235 a lab-frame vector (numpy-array of length 3) or as one of the strings 236 "vert", "horiz", or "random". 237 238 In the latter case, the reference is the incidence plane of the first 239 non-normal-incidence mirror after the source. If there is none, you will 240 be asked to rather specify the axis as a 3D-numpy-array. 241 242 axis = "vert" means the source position is shifted along the axis perpendicular 243 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 244 245 axis = "horiz" means the source direciton is translated along thr axis in that 246 incidence plane and perpendicular to the current source direction, 247 i.e. "horizontally" in the incidence plane, but retaining the same distance 248 of source and first optical element. 249 250 axis = "random" means the the source direction shifted in a random direction 251 within in the plane perpendicular to the current source direction, 252 e.g. simulating a fluctuation of hte transverse source position. 253 254 Parameters 255 ---------- 256 axis : np.ndarray or str 257 Shift axis, specified either as a 3D lab-frame vector or as one 258 of the strings "vert", "horiz", or "random". 259 260 distance : float 261 Shift distance in mm. 262 263 Returns 264 ------- 265 Nothing, just modifies the property 'source_rays'. 266 """ 267 if type(distance) not in [int, float, np.float64]: 268 raise ValueError('The "distance"-argument must be an int or float number.') 269 270 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 271 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 272 273 OEnormal = None 274 for i in mirror_indcs: 275 ith_OEnormal = self.optical_elements[i].normal 276 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 277 OEnormal = ith_OEnormal 278 break 279 if OEnormal is None: 280 raise Exception( 281 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 282 so you should rather give 'axis' as a numpy-array of length 3." 283 ) 284 285 if type(axis) == np.ndarray and len(axis) == 3: 286 translation_vector = axis 287 else: 288 perp_axis = np.cross(central_ray_vector, OEnormal) 289 horiz_axis = np.cross(perp_axis, central_ray_vector) 290 291 if axis == "vert": 292 translation_vector = perp_axis 293 elif axis == "horiz": 294 translation_vector = horiz_axis 295 elif axis == "random": 296 translation_vector = ( 297 np.random.uniform(low=-1, high=1, size=1) * perp_axis 298 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 299 ) 300 else: 301 raise ValueError( 302 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 303 ) 304 305 self.source_rays = mgeo.TranslationRayList(self.source_rays, distance * mgeo.Normalize(translation_vector)) 306 307 def tilt_source(self, axis: str|np.ndarray, angle: float): 308 """ 309 Rotate source ray bundle by angle around an axis, specified as 310 a lab-frame vector (numpy-array of length 3) or as one of the strings 311 "in_plane", "out_plane" or "random" direction. 312 313 In the latter case, the function considers the incidence plane of the first 314 non-normal-incidence mirror after the source. If there is none, you will 315 be asked to rather specify the axis as a 3D-numpy-array. 316 317 axis = "in_plane" means the source direction is rotated about an axis 318 perpendicular to that incidence plane, which tilts the source 319 "horizontally" in the same plane. 320 321 axis = "out_plane" means the source direciton is rotated about an axis 322 in that incidence plane and perpendicular to the current source direction, 323 which tilts the source "vertically" out of the former incidence plane. 324 325 axis = "random" means the the source direction is tilted in a random direction, 326 e.g. simulating a beam pointing fluctuation. 327 328 Attention, "angle" is given in deg, so as to remain consitent with the 329 conventions of other functions, although pointing is mostly talked about 330 in mrad instead. 331 332 Parameters 333 ---------- 334 axis : np.ndarray or str 335 Shift axis, specified either as a 3D lab-frame vector or as one 336 of the strings "in_plane", "out_plane", or "random". 337 338 angle : float 339 Rotation angle in degree. 340 341 Returns 342 ------- 343 Nothing, just modifies the property 'source_rays'. 344 """ 345 if type(angle) not in [int, float, np.float64]: 346 raise ValueError('The "angle"-argument must be an int or float number.') 347 348 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 349 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 350 351 OEnormal = None 352 for i in mirror_indcs: 353 ith_OEnormal = self.optical_elements[i].normal 354 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 355 OEnormal = ith_OEnormal 356 break 357 if OEnormal is None: 358 raise Exception( 359 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 360 so you should rather give 'axis' as a numpy-array of length 3." 361 ) 362 363 if type(axis) == np.ndarray and len(axis) == 3: 364 rot_axis = axis 365 else: 366 rot_axis_in = np.cross(central_ray_vector, OEnormal) 367 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 368 if axis == "in_plane": 369 rot_axis = rot_axis_in 370 elif axis == "out_plane": 371 rot_axis = rot_axis_out 372 elif axis == "random": 373 rot_axis = ( 374 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 375 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 376 ) 377 else: 378 raise ValueError( 379 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 380 ) 381 382 self.source_rays = mgeo.RotationAroundAxisRayList(self.source_rays, rot_axis, np.deg2rad(angle)) 383 384 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 385 """ 386 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 387 OEstart and OEstop (both included). 388 """ 389 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 390 outray = self.get_output_rays()[OEstart-1][0] 391 print(outray) 392 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 393 self.optical_elements[OEstart:OEstop] = new_elements 394 395 # %% 396 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 397 """ 398 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 399 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 400 that takes either of the two values: 401 - "in" 402 - "out" 403 In either case the "axis" can take these values: 404 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 405 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 406 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 407 408 Parameters 409 ---------- 410 OEindx : int 411 Index of the optical element to modify out of OpticalChain.optical_elements. 412 413 ref : str 414 Reference ray used to define the axes of rotation. Can be either: 415 "in" or "out" or "local_normal" (in+out) 416 417 axis : str 418 Rotation axis, specified as one of the strings 419 "pitch", "roll", "yaw" 420 421 angle : float 422 Rotation angle in degree. 423 424 Returns 425 ------- 426 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 427 """ 428 if abs(OEindx) > len(self.optical_elements): 429 raise ValueError( 430 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 431 ) 432 if type(angle) not in [int, float, np.float64]: 433 raise ValueError('The "angle"-argument must be an int or float number.') 434 MasterRay = [mp.FindCentralRay(self.source_rays)] 435 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 436 TracedRay = [MasterRay] + TracedRay 437 match ref: 438 case "out": 439 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 440 case "in": 441 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 442 case "localnormal": 443 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 444 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 445 RefVec = mgeo.Normalize(Out-In) 446 case _: 447 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 448 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 449 match axis: 450 case "pitch": 451 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 452 case "roll": 453 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 454 case "yaw": 455 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 456 case _: 457 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 458 else: 459 #If the normal vector is aligned with the ray 460 match axis: 461 case "pitch": 462 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 463 case "roll": 464 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 465 case "yaw": 466 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 467 case _: 468 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 469 470 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 471 """ 472 Shift the optical element OpticalChain.optical_elements[OEindx] along 473 axis referenced to the master ray. 474 475 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 476 that takes either of the two values: 477 - "in" 478 - "out" 479 In either case the "axis" can take these values: 480 - "along": Translation along the master ray. 481 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 482 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 483 484 Parameters 485 ---------- 486 OEindx : int 487 Index of the optical element to modify out of OpticalChain.optical_elements. 488 489 ref : str 490 Reference ray used to define the axes of rotation. Can be either: 491 "in" or "out". 492 493 axis : str 494 Translation axis, specified as one of the strings 495 "along", "in_plane", "out_plane". 496 497 distance : float 498 Rotation angle in degree. 499 500 Returns 501 ------- 502 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 503 """ 504 if abs(OEindx) >= len(self.optical_elements): 505 raise ValueError( 506 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 507 ) 508 if type(distance) not in [int, float, np.float64]: 509 raise ValueError('The "dist"-argument must be an int or float number.') 510 511 MasterRay = [mp.FindCentralRay(self.source_rays)] 512 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 513 TracedRay = [MasterRay] + TracedRay 514 match ref: 515 case "out": 516 RefVec = TracedRay[OEindx+1][0].vector 517 case "in": 518 RefVec = TracedRay[OEindx][0].vector 519 case _: 520 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 521 match axis: 522 case "along": 523 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 524 case "in_plane": 525 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 526 case "out_plane": 527 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 528 case _: 529 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 530 531 # some function that randomly misalings one, or several or all ?
30class OpticalChain: 31 """ 32 The OpticalChain represents the whole optical setup to be simulated: 33 Its main attributes are a list source-[Rays](ModuleOpticalRay.html) and 34 a list of successive [OpticalElements](ModuleOpticalElement.html). 35 36 The method OpticalChain.get_output_rays() returns an associated list of lists of 37 [Rays](ModuleOpticalRay.html), each calculated by ray-tracing from one 38 [OpticalElement](ModuleOpticalElement.html) to the next. 39 So OpticalChain.get_output_rays()[i] is the bundle of [Rays](ModuleOpticalRay.html) *after* 40 optical_elements[i]. 41 42 The string "description" can contain a short description of the optical setup, or similar notes. 43 44 The OpticalChain can be visualized quickly with the method OpticalChain.quickshow(), 45 and more nicely with OpticalChain.render(). 46 47 The class also provides methods for (mis-)alignment of the source-[Ray](ModuleOpticalRay.html)-bundle and the 48 [OpticalElements](ModuleOpticalElement.html). 49 Attributes 50 ---------- 51 source_rays : list[mray.Ray] 52 List of source rays, which are to be traced. 53 54 optical_elements : list[moe.OpticalElement] 55 List of successive optical elements. 56 57 detector: mdet.Detector (optional) 58 The detector (or list of detectors) to analyse the results. 59 60 description : str 61 A string to describe the optical setup. 62 Methods 63 ---------- 64 copy_chain() 65 66 get_output_rays() 67 68 quickshow() 69 70 render() 71 72 ---------- 73 74 shift_source(axis, distance) 75 76 tilt_source(self, axis, angle) 77 78 ---------- 79 80 rotate_OE(OEindx, axis, angle) 81 82 shift_OE(OEindx, axis, distance) 83 84 """ 85 86 def __init__(self, source_rays, optical_elements, detectors, description=""): 87 """ 88 Parameters 89 ---------- 90 source_rays : list[mray.Ray] 91 List of source rays, which are to be traced. 92 93 optical_elements : list[moe.OpticalElement] 94 List of successive optical elements. 95 96 detector: mdet.Detector (optional) 97 The detector (or list of detectors) to analyse the results. 98 99 description : str, optional 100 A string to describe the optical setup. Defaults to ''. 101 102 """ 103 self.source_rays = copy.deepcopy(source_rays) 104 # deepcopy so this object doesn't get changed when the global source_rays changes "outside" 105 self.optical_elements = copy.deepcopy(optical_elements) 106 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 107 self.detectors = detectors 108 if isinstance(detectors, mdet.Detector): 109 self.detectors = {"Focus": detectors} 110 self.description = description 111 self._output_rays = None 112 self._last_source_rays_hash = None 113 self._last_optical_elements_hash = None 114 115 def __repr__(self): 116 pretty_str = "Optical setup [OpticalChain]:\n" 117 pretty_str += f" - Description: {self.description}\n" if self.description else "Description: Not provided.\n" 118 pretty_str += " - Contains the following elements:\n" 119 pretty_str += f" - Source with {len(self.source_rays)} rays at coordinate origin\n" 120 prev_pos = np.zeros(3) 121 for i, element in enumerate(self.optical_elements): 122 dist = (element.position - prev_pos).norm 123 if i == 0: 124 prev = "source" 125 else: 126 prev = f"element {i-1}" 127 pretty_str += f" - Element {i}: {element.type} at distance {round(dist)} from {prev}\n" 128 prev_pos = element.position 129 for i in self.detectors.keys(): 130 detector = self.detectors[i] 131 pretty_str += f' - Detector "{i}": {detector.type} at distance {round(detector.distance,3)} from element {detector.index % len(self.optical_elements)}\n' 132 return pretty_str 133 134 def __getitem__(self, i): 135 return self.optical_elements[i] 136 137 def __len__(self): 138 return len(self.optical_elements) 139 140 @property 141 def source_rays(self): 142 return self._source_rays 143 144 @source_rays.setter 145 def source_rays(self, source_rays): 146 if type(source_rays) == mray.RayList: 147 self._source_rays = source_rays 148 else: 149 raise TypeError("Source_rays must be a RayList object.") 150 151 @property 152 def optical_elements(self): 153 return self._optical_elements 154 155 @optical_elements.setter 156 def optical_elements(self, optical_elements): 157 if type(optical_elements) == list and all(isinstance(x, moe.OpticalElement) for x in optical_elements): 158 self._optical_elements = optical_elements 159 else: 160 raise TypeError("Optical_elements must be list of OpticalElement-objects.") 161 162 # %% METHODS ################################################################## 163 164 def __copy__(self): 165 """Return another optical chain with the same source, optical elements and description-string as this one.""" 166 return OpticalChain(self.source_rays, self.optical_elements, self.detectors, self.description) 167 168 def __deepcopy__(self, memo): 169 """Return another optical chain with the same source, optical elements and description-string as this one.""" 170 return OpticalChain(copy.deepcopy(self.source_rays), copy.deepcopy(self.optical_elements), memo+"\n"+copy.copy(self.description)) 171 172 def get_input_rays(self): 173 """ 174 Returns the list of source rays. 175 """ 176 return self.source_rays 177 178 def get_output_rays(self, force=False, **kwargs): 179 """ 180 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 181 or if the source-ray-bundle or anything about the optical elements has changed. 182 183 This is the user-facing method to perform the ray-tracing calculation. 184 """ 185 current_source_rays_hash = hash(self.source_rays) 186 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 187 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 188 print("...ray-tracing...", end="", flush=True) 189 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 190 print( 191 "\r\033[K", end="", flush=True 192 ) # move to beginning of the line with \r and then delete the whole line with \033[K 193 self._last_source_rays_hash = current_source_rays_hash 194 self._last_optical_elements_hash = current_optical_elements_hash 195 196 return self._output_rays 197 198 def get2dPoints(self, Detector = "Focus"): 199 """ 200 Returns the 2D points of the detected rays on the detector. 201 """ 202 if isinstance(Detector, str): 203 if Detector in self.detectors.keys(): 204 Detector = self.detectors[Detector] 205 else: 206 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 207 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0] 208 209 def get3dPoints(self, Detector = "Focus"): 210 """ 211 Returns the 3D points of the detected rays on the detector. 212 """ 213 if isinstance(Detector, str): 214 if Detector in self.detectors.keys(): 215 Detector = self.detectors[Detector] 216 else: 217 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 218 return Detector.get_3D_points(self.get_output_rays()[Detector.index]) 219 220 def getDelays(self, Detector = "Focus"): 221 """ 222 Returns the delays of the detected rays on the detector. 223 """ 224 if isinstance(Detector, str): 225 if Detector in self.detectors.keys(): 226 Detector = self.detectors[Detector] 227 else: 228 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 229 return Detector.get_Delays(self.get_output_rays()[Detector.index]) 230 231 # %% methods to (mis-)align the optical chain; just uses the corresponding methods of the OpticalElement class... 232 233 def shift_source(self, axis: str|np.ndarray, distance: float): 234 """ 235 Shift source ray bundle by distance (in mm) along the 'axis' specified as 236 a lab-frame vector (numpy-array of length 3) or as one of the strings 237 "vert", "horiz", or "random". 238 239 In the latter case, the reference is the incidence plane of the first 240 non-normal-incidence mirror after the source. If there is none, you will 241 be asked to rather specify the axis as a 3D-numpy-array. 242 243 axis = "vert" means the source position is shifted along the axis perpendicular 244 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 245 246 axis = "horiz" means the source direciton is translated along thr axis in that 247 incidence plane and perpendicular to the current source direction, 248 i.e. "horizontally" in the incidence plane, but retaining the same distance 249 of source and first optical element. 250 251 axis = "random" means the the source direction shifted in a random direction 252 within in the plane perpendicular to the current source direction, 253 e.g. simulating a fluctuation of hte transverse source position. 254 255 Parameters 256 ---------- 257 axis : np.ndarray or str 258 Shift axis, specified either as a 3D lab-frame vector or as one 259 of the strings "vert", "horiz", or "random". 260 261 distance : float 262 Shift distance in mm. 263 264 Returns 265 ------- 266 Nothing, just modifies the property 'source_rays'. 267 """ 268 if type(distance) not in [int, float, np.float64]: 269 raise ValueError('The "distance"-argument must be an int or float number.') 270 271 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 272 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 273 274 OEnormal = None 275 for i in mirror_indcs: 276 ith_OEnormal = self.optical_elements[i].normal 277 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 278 OEnormal = ith_OEnormal 279 break 280 if OEnormal is None: 281 raise Exception( 282 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 283 so you should rather give 'axis' as a numpy-array of length 3." 284 ) 285 286 if type(axis) == np.ndarray and len(axis) == 3: 287 translation_vector = axis 288 else: 289 perp_axis = np.cross(central_ray_vector, OEnormal) 290 horiz_axis = np.cross(perp_axis, central_ray_vector) 291 292 if axis == "vert": 293 translation_vector = perp_axis 294 elif axis == "horiz": 295 translation_vector = horiz_axis 296 elif axis == "random": 297 translation_vector = ( 298 np.random.uniform(low=-1, high=1, size=1) * perp_axis 299 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 300 ) 301 else: 302 raise ValueError( 303 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 304 ) 305 306 self.source_rays = mgeo.TranslationRayList(self.source_rays, distance * mgeo.Normalize(translation_vector)) 307 308 def tilt_source(self, axis: str|np.ndarray, angle: float): 309 """ 310 Rotate source ray bundle by angle around an axis, specified as 311 a lab-frame vector (numpy-array of length 3) or as one of the strings 312 "in_plane", "out_plane" or "random" direction. 313 314 In the latter case, the function considers the incidence plane of the first 315 non-normal-incidence mirror after the source. If there is none, you will 316 be asked to rather specify the axis as a 3D-numpy-array. 317 318 axis = "in_plane" means the source direction is rotated about an axis 319 perpendicular to that incidence plane, which tilts the source 320 "horizontally" in the same plane. 321 322 axis = "out_plane" means the source direciton is rotated about an axis 323 in that incidence plane and perpendicular to the current source direction, 324 which tilts the source "vertically" out of the former incidence plane. 325 326 axis = "random" means the the source direction is tilted in a random direction, 327 e.g. simulating a beam pointing fluctuation. 328 329 Attention, "angle" is given in deg, so as to remain consitent with the 330 conventions of other functions, although pointing is mostly talked about 331 in mrad instead. 332 333 Parameters 334 ---------- 335 axis : np.ndarray or str 336 Shift axis, specified either as a 3D lab-frame vector or as one 337 of the strings "in_plane", "out_plane", or "random". 338 339 angle : float 340 Rotation angle in degree. 341 342 Returns 343 ------- 344 Nothing, just modifies the property 'source_rays'. 345 """ 346 if type(angle) not in [int, float, np.float64]: 347 raise ValueError('The "angle"-argument must be an int or float number.') 348 349 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 350 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 351 352 OEnormal = None 353 for i in mirror_indcs: 354 ith_OEnormal = self.optical_elements[i].normal 355 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 356 OEnormal = ith_OEnormal 357 break 358 if OEnormal is None: 359 raise Exception( 360 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 361 so you should rather give 'axis' as a numpy-array of length 3." 362 ) 363 364 if type(axis) == np.ndarray and len(axis) == 3: 365 rot_axis = axis 366 else: 367 rot_axis_in = np.cross(central_ray_vector, OEnormal) 368 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 369 if axis == "in_plane": 370 rot_axis = rot_axis_in 371 elif axis == "out_plane": 372 rot_axis = rot_axis_out 373 elif axis == "random": 374 rot_axis = ( 375 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 376 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 377 ) 378 else: 379 raise ValueError( 380 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 381 ) 382 383 self.source_rays = mgeo.RotationAroundAxisRayList(self.source_rays, rot_axis, np.deg2rad(angle)) 384 385 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 386 """ 387 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 388 OEstart and OEstop (both included). 389 """ 390 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 391 outray = self.get_output_rays()[OEstart-1][0] 392 print(outray) 393 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 394 self.optical_elements[OEstart:OEstop] = new_elements 395 396 # %% 397 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 398 """ 399 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 400 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 401 that takes either of the two values: 402 - "in" 403 - "out" 404 In either case the "axis" can take these values: 405 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 406 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 407 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 408 409 Parameters 410 ---------- 411 OEindx : int 412 Index of the optical element to modify out of OpticalChain.optical_elements. 413 414 ref : str 415 Reference ray used to define the axes of rotation. Can be either: 416 "in" or "out" or "local_normal" (in+out) 417 418 axis : str 419 Rotation axis, specified as one of the strings 420 "pitch", "roll", "yaw" 421 422 angle : float 423 Rotation angle in degree. 424 425 Returns 426 ------- 427 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 428 """ 429 if abs(OEindx) > len(self.optical_elements): 430 raise ValueError( 431 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 432 ) 433 if type(angle) not in [int, float, np.float64]: 434 raise ValueError('The "angle"-argument must be an int or float number.') 435 MasterRay = [mp.FindCentralRay(self.source_rays)] 436 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 437 TracedRay = [MasterRay] + TracedRay 438 match ref: 439 case "out": 440 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 441 case "in": 442 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 443 case "localnormal": 444 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 445 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 446 RefVec = mgeo.Normalize(Out-In) 447 case _: 448 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 449 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 450 match axis: 451 case "pitch": 452 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 453 case "roll": 454 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 455 case "yaw": 456 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 457 case _: 458 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 459 else: 460 #If the normal vector is aligned with the ray 461 match axis: 462 case "pitch": 463 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 464 case "roll": 465 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 466 case "yaw": 467 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 468 case _: 469 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 470 471 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 472 """ 473 Shift the optical element OpticalChain.optical_elements[OEindx] along 474 axis referenced to the master ray. 475 476 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 477 that takes either of the two values: 478 - "in" 479 - "out" 480 In either case the "axis" can take these values: 481 - "along": Translation along the master ray. 482 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 483 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 484 485 Parameters 486 ---------- 487 OEindx : int 488 Index of the optical element to modify out of OpticalChain.optical_elements. 489 490 ref : str 491 Reference ray used to define the axes of rotation. Can be either: 492 "in" or "out". 493 494 axis : str 495 Translation axis, specified as one of the strings 496 "along", "in_plane", "out_plane". 497 498 distance : float 499 Rotation angle in degree. 500 501 Returns 502 ------- 503 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 504 """ 505 if abs(OEindx) >= len(self.optical_elements): 506 raise ValueError( 507 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 508 ) 509 if type(distance) not in [int, float, np.float64]: 510 raise ValueError('The "dist"-argument must be an int or float number.') 511 512 MasterRay = [mp.FindCentralRay(self.source_rays)] 513 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 514 TracedRay = [MasterRay] + TracedRay 515 match ref: 516 case "out": 517 RefVec = TracedRay[OEindx+1][0].vector 518 case "in": 519 RefVec = TracedRay[OEindx][0].vector 520 case _: 521 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 522 match axis: 523 case "along": 524 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 525 case "in_plane": 526 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 527 case "out_plane": 528 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 529 case _: 530 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 531 532 # some function that randomly misalings one, or several or all ?
The OpticalChain represents the whole optical setup to be simulated: Its main attributes are a list source-Rays and a list of successive OpticalElements.
The method OpticalChain.get_output_rays() returns an associated list of lists of Rays, each calculated by ray-tracing from one OpticalElement to the next. So OpticalChain.get_output_rays()[i] is the bundle of Rays after optical_elements[i].
The string "description" can contain a short description of the optical setup, or similar notes.
The OpticalChain can be visualized quickly with the method OpticalChain.quickshow(), and more nicely with OpticalChain.render().
The class also provides methods for (mis-)alignment of the source-Ray-bundle and the OpticalElements.
Attributes
source_rays : list[mray.Ray]
List of source rays, which are to be traced.
optical_elements : list[moe.OpticalElement]
List of successive optical elements.
detector: mdet.Detector (optional)
The detector (or list of detectors) to analyse the results.
description : str
A string to describe the optical setup.
Methods
copy_chain()
get_output_rays()
quickshow()
render()
----------
shift_source(axis, distance)
tilt_source(self, axis, angle)
----------
rotate_OE(OEindx, axis, angle)
shift_OE(OEindx, axis, distance)
86 def __init__(self, source_rays, optical_elements, detectors, description=""): 87 """ 88 Parameters 89 ---------- 90 source_rays : list[mray.Ray] 91 List of source rays, which are to be traced. 92 93 optical_elements : list[moe.OpticalElement] 94 List of successive optical elements. 95 96 detector: mdet.Detector (optional) 97 The detector (or list of detectors) to analyse the results. 98 99 description : str, optional 100 A string to describe the optical setup. Defaults to ''. 101 102 """ 103 self.source_rays = copy.deepcopy(source_rays) 104 # deepcopy so this object doesn't get changed when the global source_rays changes "outside" 105 self.optical_elements = copy.deepcopy(optical_elements) 106 # deepcopy so this object doesn't get changed when the global optical_elements changes "outside" 107 self.detectors = detectors 108 if isinstance(detectors, mdet.Detector): 109 self.detectors = {"Focus": detectors} 110 self.description = description 111 self._output_rays = None 112 self._last_source_rays_hash = None 113 self._last_optical_elements_hash = None
Parameters
source_rays : list[mray.Ray]
List of source rays, which are to be traced.
optical_elements : list[moe.OpticalElement]
List of successive optical elements.
detector: mdet.Detector (optional)
The detector (or list of detectors) to analyse the results.
description : str, optional
A string to describe the optical setup. Defaults to ''.
172 def get_input_rays(self): 173 """ 174 Returns the list of source rays. 175 """ 176 return self.source_rays
Returns the list of source rays.
178 def get_output_rays(self, force=False, **kwargs): 179 """ 180 Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, 181 or if the source-ray-bundle or anything about the optical elements has changed. 182 183 This is the user-facing method to perform the ray-tracing calculation. 184 """ 185 current_source_rays_hash = hash(self.source_rays) 186 current_optical_elements_hash = mp._hash_list_of_objects(self.optical_elements) 187 if (current_source_rays_hash != self._last_source_rays_hash) or (current_optical_elements_hash != self._last_optical_elements_hash) or force: 188 print("...ray-tracing...", end="", flush=True) 189 self._output_rays = mp.RayTracingCalculation(self.source_rays, self.optical_elements, **kwargs) 190 print( 191 "\r\033[K", end="", flush=True 192 ) # move to beginning of the line with \r and then delete the whole line with \033[K 193 self._last_source_rays_hash = current_source_rays_hash 194 self._last_optical_elements_hash = current_optical_elements_hash 195 196 return self._output_rays
Returns the list of (lists of) output rays, calculate them if this hasn't been done yet, or if the source-ray-bundle or anything about the optical elements has changed.
This is the user-facing method to perform the ray-tracing calculation.
198 def get2dPoints(self, Detector = "Focus"): 199 """ 200 Returns the 2D points of the detected rays on the detector. 201 """ 202 if isinstance(Detector, str): 203 if Detector in self.detectors.keys(): 204 Detector = self.detectors[Detector] 205 else: 206 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 207 return Detector.get_2D_points(self.get_output_rays()[Detector.index])[0]
Returns the 2D points of the detected rays on the detector.
209 def get3dPoints(self, Detector = "Focus"): 210 """ 211 Returns the 3D points of the detected rays on the detector. 212 """ 213 if isinstance(Detector, str): 214 if Detector in self.detectors.keys(): 215 Detector = self.detectors[Detector] 216 else: 217 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 218 return Detector.get_3D_points(self.get_output_rays()[Detector.index])
Returns the 3D points of the detected rays on the detector.
220 def getDelays(self, Detector = "Focus"): 221 """ 222 Returns the delays of the detected rays on the detector. 223 """ 224 if isinstance(Detector, str): 225 if Detector in self.detectors.keys(): 226 Detector = self.detectors[Detector] 227 else: 228 raise ValueError('The "Detector"-argument must be a name of an existing detector or a Detector-object.') 229 return Detector.get_Delays(self.get_output_rays()[Detector.index])
Returns the delays of the detected rays on the detector.
233 def shift_source(self, axis: str|np.ndarray, distance: float): 234 """ 235 Shift source ray bundle by distance (in mm) along the 'axis' specified as 236 a lab-frame vector (numpy-array of length 3) or as one of the strings 237 "vert", "horiz", or "random". 238 239 In the latter case, the reference is the incidence plane of the first 240 non-normal-incidence mirror after the source. If there is none, you will 241 be asked to rather specify the axis as a 3D-numpy-array. 242 243 axis = "vert" means the source position is shifted along the axis perpendicular 244 to that incidence plane, i.e. "vertically" away from the former incidence plane.. 245 246 axis = "horiz" means the source direciton is translated along thr axis in that 247 incidence plane and perpendicular to the current source direction, 248 i.e. "horizontally" in the incidence plane, but retaining the same distance 249 of source and first optical element. 250 251 axis = "random" means the the source direction shifted in a random direction 252 within in the plane perpendicular to the current source direction, 253 e.g. simulating a fluctuation of hte transverse source position. 254 255 Parameters 256 ---------- 257 axis : np.ndarray or str 258 Shift axis, specified either as a 3D lab-frame vector or as one 259 of the strings "vert", "horiz", or "random". 260 261 distance : float 262 Shift distance in mm. 263 264 Returns 265 ------- 266 Nothing, just modifies the property 'source_rays'. 267 """ 268 if type(distance) not in [int, float, np.float64]: 269 raise ValueError('The "distance"-argument must be an int or float number.') 270 271 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 272 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 273 274 OEnormal = None 275 for i in mirror_indcs: 276 ith_OEnormal = self.optical_elements[i].normal 277 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 278 OEnormal = ith_OEnormal 279 break 280 if OEnormal is None: 281 raise Exception( 282 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 283 so you should rather give 'axis' as a numpy-array of length 3." 284 ) 285 286 if type(axis) == np.ndarray and len(axis) == 3: 287 translation_vector = axis 288 else: 289 perp_axis = np.cross(central_ray_vector, OEnormal) 290 horiz_axis = np.cross(perp_axis, central_ray_vector) 291 292 if axis == "vert": 293 translation_vector = perp_axis 294 elif axis == "horiz": 295 translation_vector = horiz_axis 296 elif axis == "random": 297 translation_vector = ( 298 np.random.uniform(low=-1, high=1, size=1) * perp_axis 299 + np.random.uniform(low=-1, high=1, size=1) * horiz_axis 300 ) 301 else: 302 raise ValueError( 303 'The shift direction must be specified by "axis" as one of ["vert", "horiz", "random"].' 304 ) 305 306 self.source_rays = mgeo.TranslationRayList(self.source_rays, distance * mgeo.Normalize(translation_vector))
Shift source ray bundle by distance (in mm) along the 'axis' specified as a lab-frame vector (numpy-array of length 3) or as one of the strings "vert", "horiz", or "random".
In the latter case, the reference is the incidence plane of the first non-normal-incidence mirror after the source. If there is none, you will be asked to rather specify the axis as a 3D-numpy-array.
axis = "vert" means the source position is shifted along the axis perpendicular to that incidence plane, i.e. "vertically" away from the former incidence plane..
axis = "horiz" means the source direciton is translated along thr axis in that incidence plane and perpendicular to the current source direction, i.e. "horizontally" in the incidence plane, but retaining the same distance of source and first optical element.
axis = "random" means the the source direction shifted in a random direction within in the plane perpendicular to the current source direction, e.g. simulating a fluctuation of hte transverse source position.
Parameters
axis : np.ndarray or str
Shift axis, specified either as a 3D lab-frame vector or as one
of the strings "vert", "horiz", or "random".
distance : float
Shift distance in mm.
Returns
Nothing, just modifies the property 'source_rays'.
308 def tilt_source(self, axis: str|np.ndarray, angle: float): 309 """ 310 Rotate source ray bundle by angle around an axis, specified as 311 a lab-frame vector (numpy-array of length 3) or as one of the strings 312 "in_plane", "out_plane" or "random" direction. 313 314 In the latter case, the function considers the incidence plane of the first 315 non-normal-incidence mirror after the source. If there is none, you will 316 be asked to rather specify the axis as a 3D-numpy-array. 317 318 axis = "in_plane" means the source direction is rotated about an axis 319 perpendicular to that incidence plane, which tilts the source 320 "horizontally" in the same plane. 321 322 axis = "out_plane" means the source direciton is rotated about an axis 323 in that incidence plane and perpendicular to the current source direction, 324 which tilts the source "vertically" out of the former incidence plane. 325 326 axis = "random" means the the source direction is tilted in a random direction, 327 e.g. simulating a beam pointing fluctuation. 328 329 Attention, "angle" is given in deg, so as to remain consitent with the 330 conventions of other functions, although pointing is mostly talked about 331 in mrad instead. 332 333 Parameters 334 ---------- 335 axis : np.ndarray or str 336 Shift axis, specified either as a 3D lab-frame vector or as one 337 of the strings "in_plane", "out_plane", or "random". 338 339 angle : float 340 Rotation angle in degree. 341 342 Returns 343 ------- 344 Nothing, just modifies the property 'source_rays'. 345 """ 346 if type(angle) not in [int, float, np.float64]: 347 raise ValueError('The "angle"-argument must be an int or float number.') 348 349 central_ray_vector = mp.FindCentralRay(self.source_rays).vector 350 mirror_indcs = [i for i, OE in enumerate(self.optical_elements) if "Mirror" in OE.type.type] 351 352 OEnormal = None 353 for i in mirror_indcs: 354 ith_OEnormal = self.optical_elements[i].normal 355 if np.linalg.norm(np.cross(central_ray_vector, ith_OEnormal)) > 1e-10: 356 OEnormal = ith_OEnormal 357 break 358 if OEnormal is None: 359 raise Exception( 360 "There doesn't seem to be a non-normal-incidence mirror in this optical chain, \ 361 so you should rather give 'axis' as a numpy-array of length 3." 362 ) 363 364 if type(axis) == np.ndarray and len(axis) == 3: 365 rot_axis = axis 366 else: 367 rot_axis_in = np.cross(central_ray_vector, OEnormal) 368 rot_axis_out = np.cross(rot_axis_in, central_ray_vector) 369 if axis == "in_plane": 370 rot_axis = rot_axis_in 371 elif axis == "out_plane": 372 rot_axis = rot_axis_out 373 elif axis == "random": 374 rot_axis = ( 375 np.random.uniform(low=-1, high=1, size=1) * rot_axis_in 376 + np.random.uniform(low=-1, high=1, size=1) * rot_axis_out 377 ) 378 else: 379 raise ValueError( 380 'The tilt axis must be specified by as one of ["in_plane", "out_plane", "random"] or as a numpy-array of length 3.' 381 ) 382 383 self.source_rays = mgeo.RotationAroundAxisRayList(self.source_rays, rot_axis, np.deg2rad(angle))
Rotate source ray bundle by angle around an axis, specified as a lab-frame vector (numpy-array of length 3) or as one of the strings "in_plane", "out_plane" or "random" direction.
In the latter case, the function considers the incidence plane of the first non-normal-incidence mirror after the source. If there is none, you will be asked to rather specify the axis as a 3D-numpy-array.
axis = "in_plane" means the source direction is rotated about an axis perpendicular to that incidence plane, which tilts the source "horizontally" in the same plane.
axis = "out_plane" means the source direciton is rotated about an axis in that incidence plane and perpendicular to the current source direction, which tilts the source "vertically" out of the former incidence plane.
axis = "random" means the the source direction is tilted in a random direction, e.g. simulating a beam pointing fluctuation.
Attention, "angle" is given in deg, so as to remain consitent with the conventions of other functions, although pointing is mostly talked about in mrad instead.
Parameters
axis : np.ndarray or str
Shift axis, specified either as a 3D lab-frame vector or as one
of the strings "in_plane", "out_plane", or "random".
angle : float
Rotation angle in degree.
Returns
Nothing, just modifies the property 'source_rays'.
385 def partial_realign(self, OEstart: int, OEstop: int, DistanceList: list, IncidenceAngleList, IncidencePlaneAngleList): 386 """ 387 This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements 388 OEstart and OEstop (both included). 389 """ 390 OpticsList = [i.type for i in self.optical_elements[OEstart:OEstop]] 391 outray = self.get_output_rays()[OEstart-1][0] 392 print(outray) 393 new_elements = mp.OEPlacement(OpticsList, DistanceList, IncidenceAngleList, IncidencePlaneAngleList, outray) 394 self.optical_elements[OEstart:OEstop] = new_elements
This as-of-yet not-implemented method will realign only the parts of the optical chain that are between the elements OEstart and OEstop (both included).
397 def rotate_OE(self, OEindx: int, ref: str, axis: str, angle: float): 398 """ 399 Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. 400 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 401 that takes either of the two values: 402 - "in" 403 - "out" 404 In either case the "axis" can take these values: 405 - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise 406 - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. 407 - "yaw": Rotation about the vector normal to the incidence plane and to the master ray. 408 409 Parameters 410 ---------- 411 OEindx : int 412 Index of the optical element to modify out of OpticalChain.optical_elements. 413 414 ref : str 415 Reference ray used to define the axes of rotation. Can be either: 416 "in" or "out" or "local_normal" (in+out) 417 418 axis : str 419 Rotation axis, specified as one of the strings 420 "pitch", "roll", "yaw" 421 422 angle : float 423 Rotation angle in degree. 424 425 Returns 426 ------- 427 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 428 """ 429 if abs(OEindx) > len(self.optical_elements): 430 raise ValueError( 431 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 432 ) 433 if type(angle) not in [int, float, np.float64]: 434 raise ValueError('The "angle"-argument must be an int or float number.') 435 MasterRay = [mp.FindCentralRay(self.source_rays)] 436 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 437 TracedRay = [MasterRay] + TracedRay 438 match ref: 439 case "out": 440 RefVec = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 441 case "in": 442 RefVec = mgeo.Normalize(TracedRay[OEindx][0].vector) 443 case "localnormal": 444 In = mgeo.Normalize(TracedRay[OEindx][0].vector) 445 Out = mgeo.Normalize(TracedRay[OEindx+1][0].vector) 446 RefVec = mgeo.Normalize(Out-In) 447 case _: 448 raise ValueError('The "ref"-argument must be a string out of ["in", "out", "localnormal].') 449 if 1 - np.dot(self[OEindx].normal,RefVec) > 1e-2: 450 match axis: 451 case "pitch": 452 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 453 case "roll": 454 self[OEindx].normal = mgeo.RotationAroundAxis(RefVec,np.deg2rad(angle),self[OEindx].normal) 455 case "yaw": 456 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 457 case _: 458 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].') 459 else: 460 #If the normal vector is aligned with the ray 461 match axis: 462 case "pitch": 463 self[OEindx].normal = mgeo.RotationAroundAxis(self[OEindx].majoraxis,np.deg2rad(angle),self[OEindx].normal) 464 case "roll": 465 self[OEindx].majoraxis = mgeo.RotationAroundAxis(self[OEindx].normal,np.deg2rad(angle),self[OEindx].majoraxis) 466 case "yaw": 467 self[OEindx].normal = mgeo.RotationAroundAxis(mgeo.Normalize(np.cross(RefVec, self[OEindx].majoraxis)),np.deg2rad(angle),self[OEindx].normal) 468 case _: 469 raise ValueError('The "axis"-argument must be a string out of ["pitch", "roll", "yaw"].')
Rotate the optical element OpticalChain.optical_elements[OEindx] with an axis defined relative to the master ray. The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable that takes either of the two values: - "in" - "out" In either case the "axis" can take these values: - "roll": Rotation about the master ray. Looking in the same direction as light propagation, positive is counterclockwise - "pitch": Rotation about the vector in the incidence plane that is normal to the master ray. - "yaw": Rotation about the vector normal to the incidence plane and to the master ray.
Parameters
OEindx : int
Index of the optical element to modify out of OpticalChain.optical_elements.
ref : str
Reference ray used to define the axes of rotation. Can be either:
"in" or "out" or "local_normal" (in+out)
axis : str
Rotation axis, specified as one of the strings
"pitch", "roll", "yaw"
angle : float
Rotation angle in degree.
Returns
Nothing, just modifies OpticalChain.optical_elements[OEindx].
471 def shift_OE(self, OEindx: int, ref: str, axis: str, distance: float): 472 """ 473 Shift the optical element OpticalChain.optical_elements[OEindx] along 474 axis referenced to the master ray. 475 476 The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable 477 that takes either of the two values: 478 - "in" 479 - "out" 480 In either case the "axis" can take these values: 481 - "along": Translation along the master ray. 482 - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. 483 - "out_plane": Translation along the vector normal to the incidence plane and to the master ray. 484 485 Parameters 486 ---------- 487 OEindx : int 488 Index of the optical element to modify out of OpticalChain.optical_elements. 489 490 ref : str 491 Reference ray used to define the axes of rotation. Can be either: 492 "in" or "out". 493 494 axis : str 495 Translation axis, specified as one of the strings 496 "along", "in_plane", "out_plane". 497 498 distance : float 499 Rotation angle in degree. 500 501 Returns 502 ------- 503 Nothing, just modifies OpticalChain.optical_elements[OEindx]. 504 """ 505 if abs(OEindx) >= len(self.optical_elements): 506 raise ValueError( 507 'The "OEnumber"-argument is out of range compared to the length of OpticalChain.optical_elements.' 508 ) 509 if type(distance) not in [int, float, np.float64]: 510 raise ValueError('The "dist"-argument must be an int or float number.') 511 512 MasterRay = [mp.FindCentralRay(self.source_rays)] 513 TracedRay = mp.RayTracingCalculation(MasterRay, self.optical_elements) 514 TracedRay = [MasterRay] + TracedRay 515 match ref: 516 case "out": 517 RefVec = TracedRay[OEindx+1][0].vector 518 case "in": 519 RefVec = TracedRay[OEindx][0].vector 520 case _: 521 raise ValueError('The "ref"-argument must be a string out of ["in", "out"].') 522 match axis: 523 case "along": 524 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(RefVec) 525 case "in_plane": 526 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, np.cross(RefVec, self[OEindx].normal))) 527 case "out_plane": 528 self[OEindx].position = self[OEindx].position + distance * mgeo.Normalize(np.cross(RefVec, self[OEindx].normal)) 529 case _: 530 raise ValueError('The "axis"-argument must be a string out of ["along", "in_plane", "out_plane"].') 531 532 # some function that randomly misalings one, or several or all ?
Shift the optical element OpticalChain.optical_elements[OEindx] along axis referenced to the master ray.
The axis can either be relative to the incoming master ray or the outgoing one. This is defined by the "ref" variable that takes either of the two values: - "in" - "out" In either case the "axis" can take these values: - "along": Translation along the master ray. - "in_plane": Translation along the vector in the incidence plane that is normal to the master ray. - "out_plane": Translation along the vector normal to the incidence plane and to the master ray.
Parameters
OEindx : int
Index of the optical element to modify out of OpticalChain.optical_elements.
ref : str
Reference ray used to define the axes of rotation. Can be either:
"in" or "out".
axis : str
Translation axis, specified as one of the strings
"along", "in_plane", "out_plane".
distance : float
Rotation angle in degree.
Returns
Nothing, just modifies OpticalChain.optical_elements[OEindx].
67def _getETransmission(OpticalChain, IndexIn=0, IndexOut=-1) -> float: 68 """ 69 Calculates the energy transmission from the input to the output of the OpticalChain in percent. 70 71 Parameters 72 ---------- 73 OpticalChain : OpticalChain 74 An object of the ModuleOpticalChain.OpticalChain-class. 75 76 IndexIn : int, optional 77 Index of the input RayList in the OpticalChain, defaults to 0. 78 79 IndexOut : int, optional 80 Index of the output RayList in the OpticalChain, defaults to -1. 81 82 Returns 83 ------- 84 ETransmission : float 85 """ 86 Rays = OpticalChain.get_output_rays() 87 if IndexIn == 0: 88 RayListIn = OpticalChain.get_input_rays() 89 else: 90 RayListIn = Rays[IndexIn] 91 RayListOut = Rays[IndexOut] 92 ETransmission = GetETransmission(RayListIn, RayListOut) 93 return ETransmission
Calculates the energy transmission from the input to the output of the OpticalChain in percent.
Parameters
OpticalChain : OpticalChain
An object of the ModuleOpticalChain.OpticalChain-class.
IndexIn : int, optional
Index of the input RayList in the OpticalChain, defaults to 0.
IndexOut : int, optional
Index of the output RayList in the OpticalChain, defaults to -1.
Returns
ETransmission : float
147def _getResultsSummary(OpticalChain, Detector = "Focus", verbose=False): 148 """ 149 Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation 150 for the given Detector and RayList. 151 If verbose, then also print a summary of the results for the given Detector. 152 153 Parameters 154 ---------- 155 OpticalChain : OpticalChain 156 An object of the ModuleOpticalChain.OpticalChain-class. 157 158 Detector : Detector or str, optional 159 An object of the ModuleDetector.Detector-class or "Focus" to use the focus detector, defaults to "Focus". 160 161 verbose : bool 162 Whether to print a result summary. 163 164 Returns 165 ------- 166 FocalSpotSizeSD : float 167 168 DurationSD : float 169 """ 170 if isinstance(Detector, str): 171 Detector = OpticalChain.detectors[Detector] 172 Index = Detector.index 173 RayListAnalysed = OpticalChain.get_output_rays()[Index] 174 FocalSpotSizeSD, DurationSD = GetResultSummary(Detector, RayListAnalysed, verbose) 175 return FocalSpotSizeSD, DurationSD
Calculate and return FocalSpotSize-standard-deviation and Duration-standard-deviation for the given Detector and RayList. If verbose, then also print a summary of the results for the given Detector.
Parameters
OpticalChain : OpticalChain
An object of the ModuleOpticalChain.OpticalChain-class.
Detector : Detector or str, optional
An object of the ModuleDetector.Detector-class or "Focus" to use the focus detector, defaults to "Focus".
verbose : bool
Whether to print a result summary.
Returns
FocalSpotSizeSD : float
DurationSD : float
57def DrawSpotDiagram(OpticalChain, 58 Detector = "Focus", 59 DrawAiryAndFourier=False, 60 DrawFocalContour=False, 61 DrawFocal=False, 62 ColorCoded=None, 63 Observer = None) -> plt.Figure: 64 """ 65 Produce an interactive figure with the spot diagram on the selected Detector. 66 The detector distance can be shifted with the left-right cursor keys. Doing so will actually move the detector. 67 If DrawAiryAndFourier is True, a circle with the Airy-spot-size will be shown. 68 If DrawFocalContour is True, the focal contour calculated from some of the rays will be shown. 69 If DrawFocal is True, a heatmap calculated from some of the rays will be shown. 70 The 'spots' can optionally be color-coded by specifying ColorCoded, which can be one of ["Intensity","Incidence","Delay"]. 71 72 Parameters 73 ---------- 74 RayListAnalysed : list(Ray) 75 List of objects of the ModuleOpticalRay.Ray-class. 76 77 Detector : Detector or str, optional 78 An object of the ModuleDetector.Detector-class or the name of the detector. The default is "Focus". 79 80 DrawAiryAndFourier : bool, optional 81 Whether to draw a circle with the Airy-spot-size. The default is False. 82 83 DrawFocalContour : bool, optional 84 Whether to draw the focal contour. The default is False. 85 86 DrawFocal : bool, optional 87 Whether to draw the focal heatmap. The default is False. 88 89 ColorCoded : str, optional 90 Color-code the spots according to one of ["Intensity","Incidence","Delay"]. The default is None. 91 92 Observer : Observer, optional 93 An observer object. If none, then we just create a copy of the detector and move it when pressing left-right. 94 However, if an observer is specified, then we will change the value of the observer and it will issue 95 the required callbacks to update several plots at the same time. 96 97 Returns 98 ------- 99 fig : matlplotlib-figure-handle. 100 Shows the interactive figure. 101 """ 102 if isinstance(Detector, str): 103 Detector = OpticalChain.detectors[Detector] 104 Index = Detector.index 105 movingDetector = copy(Detector) # We will move this detector when pressing left-right 106 if Observer is None: 107 detectorPosition = Observable(movingDetector.distance) # We will observe the distance of this detector 108 else: 109 detectorPosition = Observer 110 movingDetector.distance = detectorPosition.value 111 112 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 113 114 RayListAnalysed = OpticalChain.get_output_rays()[Index] 115 116 NumericalAperture = man.GetNumericalAperture(RayListAnalysed, 1) # NA determined from final ray bundle 117 MaxWavelength = np.max([i.wavelength for i in RayListAnalysed]) 118 if DrawAiryAndFourier: 119 AiryRadius = man.GetAiryRadius(MaxWavelength, NumericalAperture) * 1e3 # in µm 120 else: 121 AiryRadius = 0 122 123 if DrawFocalContour or DrawFocal: 124 X,Y,Z = man.GetDiffractionFocus(OpticalChain, movingDetector, Index) 125 Z/=np.max(Z) 126 127 DectectorPoint2D_Xcoord, DectectorPoint2D_Ycoord, FocalSpotSize, SpotSizeSD = mpu._getDetectorPoints( 128 RayListAnalysed, movingDetector 129 ) 130 131 match ColorCoded: 132 case "Intensity": 133 IntensityList = [k.intensity for k in RayListAnalysed] 134 z = np.asarray(IntensityList) 135 zlabel = "Intensity (arb.u.)" 136 title = "Intensity + Spot Diagram\n press left/right to move detector position" 137 addLine = "" 138 case "Incidence": 139 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # degree 140 z = np.asarray(IncidenceList) 141 zlabel = "Incidence angle (deg)" 142 title = "Ray Incidence + Spot Diagram\n press left/right to move detector position" 143 addLine = "" 144 case "Delay": 145 DelayList = movingDetector.get_Delays(RayListAnalysed) 146 DurationSD = mp.StandardDeviation(DelayList) 147 z = np.asarray(DelayList) 148 zlabel = "Delay (fs)" 149 title = "Delay + Spot Diagram\n press left/right to move detector position" 150 addLine = "\n" + "{:.2f}".format(DurationSD) + " fs SD" 151 case _: 152 z = "red" 153 title = "Spot Diagram\n press left/right to move detector position" 154 addLine = "" 155 156 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 157 158 plt.ion() 159 fig, ax = plt.subplots() 160 if DrawFocal: 161 focal = ax.pcolormesh(X*1e3,Y*1e3,Z) 162 if DrawFocalContour: 163 levels = [1/np.e**2, 0.5] 164 contour = ax.contourf(X*1e3, Y*1e3, Z, levels=levels, cmap='gray') 165 166 if DrawAiryAndFourier: 167 theta = np.linspace(0, 2 * np.pi, 100) 168 x = AiryRadius * np.cos(theta) 169 y = AiryRadius * np.sin(theta) # 170 ax.plot(x, y, c="black") 171 172 173 foo = ax.scatter( 174 DectectorPoint2D_Xcoord, 175 DectectorPoint2D_Ycoord, 176 c=z, 177 s=15, 178 label="{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(SpotSizeSD * 1e3) + " \u03BCm SD" + addLine, 179 ) 180 181 axisLim = 1.1 * max(AiryRadius, 0.5 * FocalSpotSize * 1000) 182 ax.set_xlim(-axisLim, axisLim) 183 ax.set_ylim(-axisLim, axisLim) 184 185 if ColorCoded == "Intensity" or ColorCoded == "Incidence" or ColorCoded == "Delay": 186 cbar = fig.colorbar(foo) 187 cbar.set_label(zlabel) 188 189 ax.legend(loc="upper right") 190 ax.set_xlabel("X (µm)") 191 ax.set_ylabel("Y (µm)") 192 ax.set_title(title) 193 # ax.margins(x=0) 194 195 196 def update_plot(new_value): 197 nonlocal movingDetector, ColorCoded, zlabel, cbar, detectorPosition, foo, distStep, focal, contour, levels, Index, RayListAnalysed 198 199 newDectectorPoint2D_Xcoord, newDectectorPoint2D_Ycoord, newFocalSpotSize, newSpotSizeSD = mpu._getDetectorPoints( 200 RayListAnalysed, movingDetector 201 ) 202 203 if DrawFocal: 204 focal.set_array(Z) 205 if DrawFocalContour: 206 levels = [1/np.e**2, 0.5] 207 for coll in contour.collections: 208 coll.remove() # Remove old contour lines 209 contour = ax.contourf(X * 1e3, Y * 1e3, Z, levels=levels, cmap='gray') 210 211 xy = foo.get_offsets() 212 xy[:, 0] = newDectectorPoint2D_Xcoord 213 xy[:, 1] = newDectectorPoint2D_Ycoord 214 foo.set_offsets(xy) 215 216 217 if ColorCoded == "Delay": 218 newDelayList = np.asarray(movingDetector.get_Delays(RayListAnalysed)) 219 newDurationSD = mp.StandardDeviation(newDelayList) 220 newaddLine = "\n" + "{:.2f}".format(newDurationSD) + " fs SD" 221 foo.set_array(newDelayList) 222 foo.set_clim(min(newDelayList), max(newDelayList)) 223 cbar.update_normal(foo) 224 else: 225 newaddLine = "" 226 227 foo.set_label( 228 "{:.3f}".format(detectorPosition.value) + " mm\n" + "{:.1f}".format(newSpotSizeSD * 1e3) + " \u03BCm SD" + newaddLine 229 ) 230 ax.legend(loc="upper right") 231 232 axisLim = 1.1 * max(AiryRadius, 0.5 * newFocalSpotSize * 1000) 233 ax.set_xlim(-axisLim, axisLim) 234 ax.set_ylim(-axisLim, axisLim) 235 236 distStep = min( 237 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 238 ) # in mm 239 240 fig.canvas.draw_idle() 241 242 243 def press(event): 244 nonlocal detectorPosition, distStep 245 if event.key == "right": 246 detectorPosition.value += distStep 247 elif event.key == "left": 248 if detectorPosition.value > 1.5 * distStep: 249 detectorPosition.value -= distStep 250 else: 251 detectorPosition.value = 0.5 * distStep 252 else: 253 return None 254 255 fig.canvas.mpl_connect("key_press_event", press) 256 257 plt.show() 258 259 detectorPosition.register(update_plot) 260 261 262 return fig, detectorPosition
Produce an interactive figure with the spot diagram on the selected Detector. The detector distance can be shifted with the left-right cursor keys. Doing so will actually move the detector. If DrawAiryAndFourier is True, a circle with the Airy-spot-size will be shown. If DrawFocalContour is True, the focal contour calculated from some of the rays will be shown. If DrawFocal is True, a heatmap calculated from some of the rays will be shown. The 'spots' can optionally be color-coded by specifying ColorCoded, which can be one of ["Intensity","Incidence","Delay"].
Parameters
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.
Returns
fig : matlplotlib-figure-handle.
Shows the interactive figure.
267def DrawDelaySpots(OpticalChain, 268 DeltaFT: tuple[int, float], 269 Detector = "Focus", 270 DrawAiryAndFourier=False, 271 ColorCoded=None, 272 Observer = None 273 ) -> plt.Figure: 274 """ 275 Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed 276 hitting the Detector, with the ray-delays shown in the 3rd dimension. 277 The detector distance can be shifted with the left-right cursor keys. 278 If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and 279 whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'. 280 281 The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"]. 282 283 Parameters 284 ---------- 285 RayListAnalysed : list(Ray) 286 List of objects of the ModuleOpticalRay.Ray-class. 287 288 Detector : Detector 289 An object of the ModuleDetector.Detector-class. 290 291 DeltaFT : (int, float) 292 The Fourier-limited pulse duration. Just used as a reference to compare the temporal spread 293 induced by the ray-delays. 294 295 DrawAiryAndFourier : bool, optional 296 Whether to draw a cylinder showing the Airy-spot-size and Fourier-limited-duration. 297 The default is False. 298 299 ColorCoded : str, optional 300 Color-code the spots according to one of ["Intensity","Incidence"]. 301 The default is None. 302 303 Returns 304 ------- 305 fig : matlplotlib-figure-handle. 306 Shows the interactive figure. 307 """ 308 if isinstance(Detector, str): 309 Det = OpticalChain.detectors[Detector] 310 else: 311 Det = Detector 312 Index = Det.index 313 Detector = copy(Det) 314 if Observer is None: 315 detectorPosition = Observable(Detector.distance) 316 else: 317 detectorPosition = Observer 318 Detector.distance = detectorPosition.value 319 320 RayListAnalysed = OpticalChain.get_output_rays()[Index] 321 fig, NumericalAperture, AiryRadius, FocalSpotSize = _drawDelayGraph( 322 RayListAnalysed, Detector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded 323 ) 324 325 distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)) # in mm 326 327 movingDetector = copy(Detector) 328 329 def update_plot(new_value): 330 nonlocal movingDetector, ColorCoded, detectorPosition, distStep, fig 331 ax = fig.axes[0] 332 cam = [ax.azim, ax.elev, ax._dist] 333 fig, sameNumericalAperture, sameAiryRadius, newFocalSpotSize = _drawDelayGraph( 334 RayListAnalysed, movingDetector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded, fig 335 ) 336 ax = fig.axes[0] 337 ax.azim, ax.elev, ax._dist = cam 338 distStep = min( 339 50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000) 340 ) 341 return fig 342 343 def press(event): 344 nonlocal detectorPosition, distStep, movingDetector, fig 345 if event.key == "right": 346 detectorPosition.value += distStep 347 elif event.key == "left": 348 if detectorPosition.value > 1.5 * distStep: 349 detectorPosition.value -= distStep 350 else: 351 detectorPosition.value = 0.5 * distStep 352 353 fig.canvas.mpl_connect("key_press_event", press) 354 detectorPosition.register(update_plot) 355 detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x)) 356 357 return fig, Observable
Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed hitting the Detector, with the ray-delays shown in the 3rd dimension. The detector distance can be shifted with the left-right cursor keys. If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'.
The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"].
Parameters
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.
Returns
fig : matlplotlib-figure-handle.
Shows the interactive figure.
438def DrawMirrorProjection(OpticalChain, ReflectionNumber: int, ColorCoded=None, Detector="") -> plt.Figure: 439 """ 440 Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. 441 The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is 442 measured at the Detector. 443 444 Parameters 445 ---------- 446 OpticalChain : OpticalChain 447 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 448 449 ReflectionNumber : int 450 Index specifying the optical element on which you want to see the impact points. 451 452 Detector : Detector, optional 453 Object of the ModuleDetector.Detector-class. Only necessary to project delays. The default is None. 454 455 ColorCoded : str, optional 456 Specifies which ray property to color-code: ["Incidence","Intensity","Delay"]. The default is None. 457 458 Returns 459 ------- 460 fig : matlplotlib-figure-handle. 461 Shows the figure. 462 """ 463 from mpl_toolkits.axes_grid1 import make_axes_locatable 464 if isinstance(Detector, str): 465 if Detector == "": 466 Detector = None 467 else: 468 Detector = OpticalChain.detectors[Detector] 469 470 Position = OpticalChain[ReflectionNumber].position 471 q = OpticalChain[ReflectionNumber].orientation 472 # n = OpticalChain.optical_elements[ReflectionNumber].normal 473 # m = OpticalChain.optical_elements[ReflectionNumber].majoraxis 474 475 RayListAnalysed = OpticalChain.get_output_rays()[ReflectionNumber] 476 # transform rays into the mirror-support reference frame 477 # (same as mirror frame but without the shift by mirror-centre) 478 r0 = OpticalChain[ReflectionNumber].r0 479 RayList = [r.to_basis(*OpticalChain[ReflectionNumber].basis) for r in RayListAnalysed] 480 481 x = np.asarray([k.point[0] for k in RayList]) - r0[0] 482 y = np.asarray([k.point[1] for k in RayList]) - r0[1] 483 if ColorCoded == "Intensity": 484 IntensityList = [k.intensity for k in RayListAnalysed] 485 z = np.asarray(IntensityList) 486 zlabel = "Intensity (arb.u.)" 487 title = "Ray intensity projected on mirror " 488 elif ColorCoded == "Incidence": 489 IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed] # in degree 490 z = np.asarray(IncidenceList) 491 zlabel = "Incidence angle (deg)" 492 title = "Ray incidence projected on mirror " 493 elif ColorCoded == "Delay": 494 if Detector is not None: 495 z = np.asarray(Detector.get_Delays(RayListAnalysed)) 496 zlabel = "Delay (fs)" 497 title = "Ray delay at detector projected on mirror " 498 else: 499 raise ValueError("If you want to project ray delays, you must specify a detector.") 500 else: 501 z = "red" 502 title = "Ray impact points projected on mirror" 503 504 plt.ion() 505 fig = plt.figure() 506 ax = OpticalChain.optical_elements[ReflectionNumber].support._ContourSupport(fig) 507 p = plt.scatter(x, y, c=z, s=15) 508 if ColorCoded == "Delay" or ColorCoded == "Incidence" or ColorCoded == "Intensity": 509 divider = make_axes_locatable(ax) 510 cax = divider.append_axes("right", size="5%", pad=0.05) 511 cbar = fig.colorbar(p, cax=cax) 512 cbar.set_label(zlabel) 513 ax.set_xlabel("x (mm)") 514 ax.set_ylabel("y (mm)") 515 plt.title(title, loc="right") 516 plt.tight_layout() 517 518 bbox = ax.get_position() 519 bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]])) 520 ax.set_position(bbox) 521 plt.show() 522 523 return fig
Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is measured at the Detector.
Parameters
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.
Returns
fig : matlplotlib-figure-handle.
Shows the figure.
529def DrawSetup(OpticalChain, 530 EndDistance=None, 531 maxRays=300, 532 OEpoints=2000, 533 draw_mesh=False, 534 cycle_ray_colors = False, 535 impact_points = False, 536 DrawDetectors=True, 537 DetectedRays = False, 538 Observers = dict()): 539 """ 540 Renders an image of the Optical setup and the traced rays. 541 542 Parameters 543 ---------- 544 OpticalChain : OpticalChain 545 List of objects of the ModuleOpticalOpticalChain.OpticalChain-class. 546 547 EndDistance : float, optional 548 The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). If not specified, 549 this distance is set to that between the source point and the 1st optical element. 550 551 maxRays: int 552 The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog 553 and not required for a nice image. Default is 150. 554 555 OEpoints : int 556 How many little spheres to draw to represent the optical elements. Default is 2000. 557 558 Returns 559 ------- 560 fig : Pyvista-figure-handle. 561 Shows the figure. 562 """ 563 564 RayListHistory = [OpticalChain.source_rays] + OpticalChain.get_output_rays() 565 566 if EndDistance is None: 567 EndDistance = np.linalg.norm(OpticalChain.source_rays[0].point - OpticalChain.optical_elements[0].position) 568 569 print("...rendering image of optical chain...", end="", flush=True) 570 fig = pvqt.BackgroundPlotter(window_size=(1500, 500), notebook=False) # Opening a window 571 fig.set_background('white') 572 573 if cycle_ray_colors: 574 colors = mpu.generate_distinct_colors(len(OpticalChain)+1) 575 else: 576 colors = [[0.7, 0, 0]]*(len(OpticalChain)+1) # Default color: dark red 577 578 # Optics display 579 # For each optic we will send the figure to the function _RenderOpticalElement and it will add the optic to the figure 580 for i,OE in enumerate(OpticalChain.optical_elements): 581 color = pv.Color(colors[i+1]) 582 rgb = color.float_rgb 583 h, l, s = rgb_to_hls(*rgb) 584 s = max(0, min(1, s * 0.3)) # Decrease saturation 585 l = max(0, min(1, l + 0.1)) # Increase lightness 586 new_rgb = hls_to_rgb(h, l, s) 587 darkened_color = pv.Color(new_rgb) 588 mpm._RenderOpticalElement(fig, OE, OEpoints, draw_mesh, darkened_color, index=i) 589 ray_meshes = mpm._RenderRays(RayListHistory, EndDistance, maxRays) 590 for i,ray in enumerate(ray_meshes): 591 color = pv.Color(colors[i]) 592 fig.add_mesh(ray, color=color, name=f"RayBundle_{i}") 593 if impact_points: 594 for i,rays in enumerate(RayListHistory): 595 points = np.array([list(r.point) for r in rays], dtype=np.float32) 596 points = pv.PolyData(points) 597 color = pv.Color(colors[i-1]) 598 fig.add_mesh(points, color=color, point_size=5, name=f"RayImpactPoints_{i}") 599 600 detector_copies = {key: copy(OpticalChain.detectors[key]) for key in OpticalChain.detectors.keys()} 601 detector_meshes_list = [] 602 detectedpoint_meshes = dict() 603 604 if OpticalChain.detectors is not None and DrawDetectors: 605 # Detector display 606 for key in OpticalChain.detectors.keys(): 607 det = detector_copies[key] 608 index = OpticalChain.detectors[key].index 609 if key in Observers: 610 det.distance = Observers[key].value 611 #Observers[key].register_calculation(lambda x: det.set_distance(x)) 612 mpm._RenderDetector(fig, det, name = key, detector_meshes = detector_meshes_list) 613 if DetectedRays: 614 RayListAnalysed = OpticalChain.get_output_rays()[index] 615 points = det.get_3D_points(RayListAnalysed) 616 points = pv.PolyData(points) 617 detectedpoint_meshes[key] = points 618 fig.add_mesh(points, color='purple', point_size=5, name=f"DetectedRays_{key}") 619 detector_meshes = dict(zip(OpticalChain.detectors.keys(), detector_meshes_list)) 620 621 # Now we define a function that will move on the plot the detector with name "detname" when it's called 622 def move_detector(detname, new_value): 623 nonlocal fig, detector_meshes, detectedpoint_meshes, DetectedRays, detectedpoint_meshes, detector_copies, OpticalChain 624 det = detector_copies[detname] 625 index = OpticalChain.detectors[detname].index 626 det_mesh = detector_meshes[detname] 627 translation = det.normal * (det.distance - new_value) 628 det_mesh.translate(translation, inplace=True) 629 det.distance = new_value 630 if DetectedRays: 631 points_mesh = detectedpoint_meshes[detname] 632 points_mesh.points = det.get_3D_points(OpticalChain.get_output_rays()[index]) 633 fig.show() 634 635 # Now we register the function to the observers 636 for key in OpticalChain.detectors.keys(): 637 if key in Observers: 638 Observers[key].register(lambda x: move_detector(key, x)) 639 640 #pv.save_meshio('optics.inp', pointcloud) 641 print( 642 "\r\033[K", end="", flush=True 643 ) # move to beginning of the line with \r and then delete the whole line with \033[K 644 fig.show() 645 return fig
Renders an image of the Optical setup and the traced rays.
Parameters
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.
Returns
fig : Pyvista-figure-handle.
Shows the figure.
701def DrawCaustics(OpticalChain, Range=1, Detector="Focus" , Npoints=1000, Nrays=1000): 702 """ 703 This function displays the caustics of the rays on the detector. 704 To do so, it calculates the intersections of the rays with the detector over a 705 range determined by the parameter Range, and then plots the standard deviation of the 706 positions in the x and y directions. 707 708 Parameters 709 ---------- 710 OpticalChain : OpticalChain 711 The optical chain to analyse. 712 713 DetectorName : str 714 The name of the detector on which the caustics are calculated. 715 716 Range : float 717 The range of the detector over which to calculate the caustics. 718 719 Npoints : int, optional 720 The number of points to sample on the detector. The default is 1000. 721 722 Returns 723 ------- 724 fig : Figure 725 The figure of the plot. 726 """ 727 distances = np.linspace(-Range, Range, Npoints) 728 if isinstance(Detector, str): 729 Det = OpticalChain.detectors[Detector] 730 Index = Det.index 731 Rays = OpticalChain.get_output_rays()[Index] 732 Nrays = min(Nrays, len(Rays)) 733 Rays = np.random.choice(Rays, Nrays, replace=False) 734 LocalRayList = [r.to_basis(*Det.basis) for r in Rays] 735 Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances) 736 x_std = [] 737 y_std = [] 738 for i in range(len(distances)): 739 x_std.append(mp.StandardDeviation(Points[i][:,0])) 740 y_std.append(mp.StandardDeviation(Points[i][:,1])) 741 plt.ion() 742 fig, ax = plt.subplots() 743 ax.plot(distances, x_std, label="x std") 744 ax.plot(distances, y_std, label="y std") 745 ax.set_xlabel("Detector distance (mm)") 746 ax.set_ylabel("Standard deviation (mm)") 747 ax.legend() 748 plt.title("Caustics") 749 plt.show() 750 return fig
This function displays the caustics of the rays on the detector. To do so, it calculates the intersections of the rays with the detector over a range determined by the parameter Range, and then plots the standard deviation of the positions in the x and y directions.
Parameters
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.
Returns
fig : Figure The figure of the plot.