ModuleAnalysisAndPlots

Provides functions for analysis of the output ray-bundles calculated through ray-tracing, and also for the ART's standard visualization options.

Implements the following functions: - DrawSpotDiagram: Produces an interactive figure with the spot diagram on the selected Detector. - DrawDelaySpots: Produces a an interactive figure with a spot diagram resulting from the RayListAnalysed - _drawDelayGraph: Auxiliary function for DelayGraph. - DrawMirrorProjection: Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. - DrawSetup: Renders an image of the Optical setup and the traced rays. - DrawAsphericity: Displays a map of the asphericity of the mirror. - DrawCaustics: Displays the caustics of the rays on the detector.

It also implements the following methods: -OpticalChain: - render: Method to render the optical setup. - drawSpotDiagram: Method to draw the spot diagram on the selected detector. - drawDelaySpots: Method to draw the delay spots on the detector. - drawMirrorProjection: Method to draw the mirror projection. - drawCaustics: Method to draw the caustics on the detector. -Mirror: - drawAsphericity: Method to draw the asphericity of the mirror.

Created in Apr 2020

@author: Anthony Guillaume + Stefan Haessler + Andre Kalouguine

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

This function displays a map of the asphericity of the mirror. It's a scatter plot of the points of the mirror surface, with the color representing the distance to the closest sphere. The closest sphere is calculated by the function get_closest_sphere, so least square method.

Parameters

Mirror : Mirror The mirror to analyse.

Npoints : int, optional The number of points to sample on the mirror surface. The default is 1000.

Returns

fig : Figure The figure of the plot.

def DrawCaustics(OpticalChain, Range=1, Detector='Focus', Npoints=1000, Nrays=1000):
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.