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

Produce an interactive figure with the spot diagram on the selected Detector. The detector distance can be shifted with the left-right cursor keys. Doing so will actually move the detector. If DrawAiryAndFourier is True, a circle with the Airy-spot-size will be shown. If DrawFocalContour is True, the focal contour calculated from some of the rays will be shown. If DrawFocal is True, a heatmap calculated from some of the rays will be shown. The 'spots' can optionally be color-coded by specifying ColorCoded, which can be one of ["Intensity","Incidence","Delay"].

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:
270def DrawDelaySpots(OpticalChain, 
271                DeltaFT: tuple[int, float],
272                Detector = "Focus",
273                DrawAiryAndFourier=False, 
274                ColorCoded=None,
275                Observer = None
276                ) -> plt.Figure:
277    """
278    Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed
279    hitting the Detector, with the ray-delays shown in the 3rd dimension.
280    The detector distance can be shifted with the left-right cursor keys.
281    If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and
282    whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'.
283    
284    The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"].
285
286    Parameters
287    ----------
288        RayListAnalysed : list(Ray)
289            List of objects of the ModuleOpticalRay.Ray-class.
290
291        Detector : Detector
292            An object of the ModuleDetector.Detector-class.
293
294        DeltaFT : (int, float)
295            The Fourier-limited pulse duration. Just used as a reference to compare the temporal spread
296            induced by the ray-delays.
297
298        DrawAiryAndFourier : bool, optional
299            Whether to draw a cylinder showing the Airy-spot-size and Fourier-limited-duration.
300            The default is False.
301
302        ColorCoded : str, optional
303            Color-code the spots according to one of ["Intensity","Incidence"].
304            The default is None.
305
306    Returns
307    -------
308        fig : matlplotlib-figure-handle.
309            Shows the interactive figure.
310    """
311    if isinstance(Detector, str):
312        Det = OpticalChain.detectors[Detector]
313    else:
314        Det = Detector
315    Index = Det.index
316    Detector = copy(Det)
317    if Observer is None:
318        detectorPosition = Observable(Detector.distance)
319    else:
320        detectorPosition = Observer
321        Detector.distance = detectorPosition.value
322    
323    RayListAnalysed = OpticalChain.get_output_rays()[Index]
324    fig, NumericalAperture, AiryRadius, FocalSpotSize = _drawDelayGraph(
325        RayListAnalysed, Detector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded
326    )
327
328    distStep = min(50, max(0.0005, round(FocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000))  # in mm
329
330    movingDetector = copy(Detector)
331
332    def update_plot(new_value):
333        nonlocal movingDetector, ColorCoded, detectorPosition, distStep, fig
334        ax = fig.axes[0]
335        cam = [ax.azim, ax.elev, ax._dist]
336        fig, sameNumericalAperture, sameAiryRadius, newFocalSpotSize = _drawDelayGraph(
337            RayListAnalysed, movingDetector, detectorPosition.value, DeltaFT, DrawAiryAndFourier, ColorCoded, fig
338        )
339        ax = fig.axes[0]
340        ax.azim, ax.elev, ax._dist = cam
341        distStep = min(
342            50, max(0.0005, round(newFocalSpotSize / 8 / np.arcsin(NumericalAperture) * 10000) / 10000)
343        )
344        return fig
345
346    def press(event):
347        nonlocal detectorPosition, distStep, movingDetector, fig
348        if event.key == "right":
349            detectorPosition.value += distStep
350        elif event.key == "left":
351            if detectorPosition.value > 1.5 * distStep:
352                detectorPosition.value -= distStep
353            else:
354                detectorPosition.value = 0.5 * distStep
355
356    fig.canvas.mpl_connect("key_press_event", press)
357    detectorPosition.register(update_plot)
358    detectorPosition.register_calculation(lambda x: movingDetector.set_distance(x))
359
360    return fig, Observable

Produce a an interactive figure with a spot diagram resulting from the RayListAnalysed hitting the Detector, with the ray-delays shown in the 3rd dimension. The detector distance can be shifted with the left-right cursor keys. If DrawAiryAndFourier is True, a cylinder is shown whose diameter is the Airy-spot-size and whose height is the Fourier-limited pulse duration 'given by 'DeltaFT'.

The 'spots' can optionally be color-coded by specifying ColorCoded as ["Intensity","Incidence"].

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:
441def DrawMirrorProjection(OpticalChain, ReflectionNumber: int, ColorCoded=None, Detector="") -> plt.Figure:
442    """
443    Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'.
444    The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is
445    measured at the Detector.
446
447    Parameters
448    ----------
449        OpticalChain : OpticalChain
450           List of objects of the ModuleOpticalOpticalChain.OpticalChain-class.
451
452        ReflectionNumber : int
453            Index specifying the optical element on which you want to see the impact points.
454
455        Detector : Detector, optional
456            Object of the ModuleDetector.Detector-class. Only necessary to project delays. The default is None.
457
458        ColorCoded : str, optional
459            Specifies which ray property to color-code: ["Incidence","Intensity","Delay"]. The default is None.
460
461    Returns
462    -------
463        fig : matlplotlib-figure-handle.
464            Shows the figure.
465    """
466    from mpl_toolkits.axes_grid1 import make_axes_locatable
467    if isinstance(Detector, str):
468        if Detector == "":
469            Detector = None
470        else:
471            Detector = OpticalChain.detectors[Detector]
472
473    Position = OpticalChain[ReflectionNumber].position
474    q = OpticalChain[ReflectionNumber].orientation
475    # n = OpticalChain.optical_elements[ReflectionNumber].normal
476    # m = OpticalChain.optical_elements[ReflectionNumber].majoraxis
477
478    RayListAnalysed = OpticalChain.get_output_rays()[ReflectionNumber]
479    # transform rays into the mirror-support reference frame
480    # (same as mirror frame but without the shift by mirror-centre)
481    r0 = OpticalChain[ReflectionNumber].r0
482    RayList = [r.to_basis(*OpticalChain[ReflectionNumber].basis) for r in RayListAnalysed]
483
484    x = np.asarray([k.point[0] for k in RayList]) - r0[0]
485    y = np.asarray([k.point[1] for k in RayList]) - r0[1]
486    if ColorCoded == "Intensity":
487        IntensityList = [k.intensity for k in RayListAnalysed]
488        z = np.asarray(IntensityList)
489        zlabel = "Intensity (arb.u.)"
490        title = "Ray intensity projected on mirror              "
491    elif ColorCoded == "Incidence":
492        IncidenceList = [np.rad2deg(k.incidence) for k in RayListAnalysed]  # in degree
493        z = np.asarray(IncidenceList)
494        zlabel = "Incidence angle (deg)"
495        title = "Ray incidence projected on mirror              "
496    elif ColorCoded == "Delay":
497        if Detector is not None:
498            z = np.asarray(Detector.get_Delays(RayListAnalysed))
499            zlabel = "Delay (fs)"
500            title = "Ray delay at detector projected on mirror              "
501        else:
502            raise ValueError("If you want to project ray delays, you must specify a detector.")
503    else:
504        z = "red"
505        title = "Ray impact points projected on mirror"
506
507    plt.ion()
508    fig = plt.figure()
509    ax = OpticalChain.optical_elements[ReflectionNumber].support._ContourSupport(fig)
510    p = plt.scatter(x, y, c=z, s=15)
511    if ColorCoded == "Delay" or ColorCoded == "Incidence" or ColorCoded == "Intensity":
512        divider = make_axes_locatable(ax)
513        cax = divider.append_axes("right", size="5%", pad=0.05)
514        cbar = fig.colorbar(p, cax=cax)
515        cbar.set_label(zlabel)
516    ax.set_xlabel("x (mm)")
517    ax.set_ylabel("y (mm)")
518    plt.title(title, loc="right")
519    plt.tight_layout()
520
521    bbox = ax.get_position()
522    bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]]))
523    ax.set_position(bbox)
524    plt.show()
525
526    return fig

Produce a plot of the ray impact points on the optical element with index 'ReflectionNumber'. The points can be color-coded according ["Incidence","Intensity","Delay"], where the ray delay is measured at the Detector.

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={}):
532def DrawSetup(OpticalChain, 
533                   EndDistance=None, 
534                   maxRays=300, 
535                   OEpoints=2000, 
536                   draw_mesh=False, 
537                   cycle_ray_colors = False,
538                   impact_points = False,
539                   DrawDetectors=True,
540                   DetectedRays = False,
541                   Observers = dict()):
542    """
543    Renders an image of the Optical setup and the traced rays.
544
545    Parameters
546    ----------
547        OpticalChain : OpticalChain
548            List of objects of the ModuleOpticalOpticalChain.OpticalChain-class.
549
550        EndDistance : float, optional
551            The rays of the last ray bundle are drawn with a length given by EndDistance (in mm). If not specified,
552            this distance is set to that between the source point and the 1st optical element.
553
554        maxRays: int
555            The maximum number of rays to render. Rendering all the traced rays is a insufferable resource hog
556            and not required for a nice image. Default is 150.
557
558        OEpoints : int
559            How many little spheres to draw to represent the optical elements.  Default is 2000.
560
561    Returns
562    -------
563        fig : Pyvista-figure-handle.
564            Shows the figure.
565    """
566
567    RayListHistory = [OpticalChain.source_rays] + OpticalChain.get_output_rays()
568
569    if EndDistance is None:
570        EndDistance = np.linalg.norm(OpticalChain.source_rays[0].point - OpticalChain.optical_elements[0].position)
571
572    print("...rendering image of optical chain...", end="", flush=True)
573    fig = pvqt.BackgroundPlotter(window_size=(1500, 500), notebook=False) # Opening a window
574    fig.set_background('white')
575    
576    if cycle_ray_colors:
577        colors = mpu.generate_distinct_colors(len(OpticalChain)+1)
578    else:
579        colors = [[0.7, 0, 0]]*(len(OpticalChain)+1) # Default color: dark red
580
581    # Optics display
582    # For each optic we will send the figure to the function _RenderOpticalElement and it will add the optic to the figure
583    for i,OE in enumerate(OpticalChain.optical_elements):
584        color = pv.Color(colors[i+1])
585        rgb = color.float_rgb
586        h, l, s = rgb_to_hls(*rgb)
587        s = max(0, min(1, s * 0.3))  # Decrease saturation
588        l = max(0, min(1, l + 0.1))  # Increase lightness
589        new_rgb = hls_to_rgb(h, l, s)
590        darkened_color = pv.Color(new_rgb)
591        mpm._RenderOpticalElement(fig, OE, OEpoints, draw_mesh, darkened_color, index=i)
592    ray_meshes = mpm._RenderRays(RayListHistory, EndDistance, maxRays)
593    for i,ray in enumerate(ray_meshes):
594        color = pv.Color(colors[i])
595        fig.add_mesh(ray, color=color, name=f"RayBundle_{i}")
596    if impact_points:
597        for i,rays in enumerate(RayListHistory):
598            points = np.array([list(r.point) for r in rays], dtype=np.float32)
599            points = pv.PolyData(points)
600            color = pv.Color(colors[i-1])
601            fig.add_mesh(points, color=color, point_size=5, name=f"RayImpactPoints_{i}")
602    
603    detector_copies = {key: copy(OpticalChain.detectors[key]) for key in OpticalChain.detectors.keys()}
604    detector_meshes_list = []
605    detectedpoint_meshes = dict()
606    
607    if OpticalChain.detectors is not None and DrawDetectors:
608        # Detector display
609        for key in OpticalChain.detectors.keys():
610            det = detector_copies[key]
611            index = OpticalChain.detectors[key].index
612            if key in Observers:
613                det.distance = Observers[key].value
614                #Observers[key].register_calculation(lambda x: det.set_distance(x))
615            mpm._RenderDetector(fig, det, name = key, detector_meshes = detector_meshes_list)
616            if DetectedRays:
617                RayListAnalysed = OpticalChain.get_output_rays()[index]
618                points = det.get_3D_points(RayListAnalysed)
619                points = pv.PolyData(points)
620                detectedpoint_meshes[key] = points
621                fig.add_mesh(points, color='purple', point_size=5, name=f"DetectedRays_{key}")
622    detector_meshes = dict(zip(OpticalChain.detectors.keys(), detector_meshes_list))
623    
624    # Now we define a function that will move on the plot the detector with name "detname" when it's called
625    def move_detector(detname, new_value):
626        nonlocal fig, detector_meshes, detectedpoint_meshes, DetectedRays, detectedpoint_meshes, detector_copies, OpticalChain
627        det = detector_copies[detname]
628        index = OpticalChain.detectors[detname].index
629        det_mesh = detector_meshes[detname]
630        translation = det.normal * (det.distance - new_value)
631        det_mesh.translate(translation, inplace=True)
632        det.distance = new_value
633        if DetectedRays:
634            points_mesh = detectedpoint_meshes[detname]
635            points_mesh.points = det.get_3D_points(OpticalChain.get_output_rays()[index])
636        fig.show()
637    
638    # Now we register the function to the observers
639    for key in OpticalChain.detectors.keys():
640        if key in Observers:
641            Observers[key].register(lambda x: move_detector(key, x))
642
643    #pv.save_meshio('optics.inp', pointcloud)  
644    print(
645        "\r\033[K", end="", flush=True
646    )  # move to beginning of the line with \r and then delete the whole line with \033[K
647    fig.show()
648    return fig

Renders an image of the Optical setup and the traced rays.

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):
654def DrawAsphericity(Mirror, Npoints=1000):
655    """
656    This function displays a map of the asphericity of the mirror.
657    It's a scatter plot of the points of the mirror surface, with the color representing the distance to the closest sphere.
658    The closest sphere is calculated by the function get_closest_sphere, so least square method.
659
660    Parameters
661    ----------
662    Mirror : Mirror
663        The mirror to analyse.
664
665    Npoints : int, optional
666        The number of points to sample on the mirror surface. The default is 1000.
667    
668    Returns
669    -------
670    fig : Figure
671        The figure of the plot.
672    """
673    plt.ion()
674    fig = plt.figure()
675    ax = Mirror.support._ContourSupport(fig)
676    center, radius = man.GetClosestSphere(Mirror, Npoints)
677    Points = mpm.sample_support(Mirror.support, Npoints=1000)
678    Points += Mirror.r0[:2]
679    Z = Mirror._zfunc(Points)
680    Points = mgeo.PointArray([Points[:, 0], Points[:, 1], Z]).T
681    X, Y = Points[:, 0] - Mirror.r0[0], Points[:, 1] - Mirror.r0[1]
682    Points_centered = Points - center
683    Distance = np.linalg.norm(Points_centered, axis=1) - radius
684    Distance*=1e3 # To convert to µm
685    p = plt.scatter(X, Y, c=Distance, s=15)
686    divider = man.make_axes_locatable(ax)
687    cax = divider.append_axes("right", size="5%", pad=0.05)
688    cbar = fig.colorbar(p, cax=cax)
689    cbar.set_label("Distance to closest sphere (µm)")
690    ax.set_xlabel("x (mm)")
691    ax.set_ylabel("y (mm)")
692    plt.title("Asphericity map", loc="right")
693    plt.tight_layout()
694
695    bbox = ax.get_position()
696    bbox.set_points(bbox.get_points() - np.array([[0.01, 0], [0.01, 0]]))
697    ax.set_position(bbox)
698    plt.show()
699    return fig

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

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):
704def DrawCaustics(OpticalChain, Range=1, Detector="Focus" , Npoints=1000, Nrays=1000):
705    """
706    This function displays the caustics of the rays on the detector.
707    To do so, it calculates the intersections of the rays with the detector over a 
708    range determined by the parameter Range, and then plots the standard deviation of the
709    positions in the x and y directions.
710
711    Parameters
712    ----------
713    OpticalChain : OpticalChain
714        The optical chain to analyse.
715
716    DetectorName : str
717        The name of the detector on which the caustics are calculated.
718    
719    Range : float
720        The range of the detector over which to calculate the caustics.
721
722    Npoints : int, optional
723        The number of points to sample on the detector. The default is 1000.
724    
725    Returns
726    -------
727    fig : Figure
728        The figure of the plot.
729    """
730    distances = np.linspace(-Range, Range, Npoints)
731    if isinstance(Detector, str):
732        Det = OpticalChain.detectors[Detector]
733        Index = Det.index
734    Rays = OpticalChain.get_output_rays()[Index]
735    Nrays = min(Nrays, len(Rays))
736    Rays = np.random.choice(Rays, Nrays, replace=False)
737    LocalRayList = [r.to_basis(*Det.basis) for r in Rays]
738    Points = mgeo.IntersectionRayListZPlane(LocalRayList, distances)
739    x_std = []
740    y_std = []
741    for i in range(len(distances)):
742        x_std.append(mp.StandardDeviation(Points[i][:,0]))
743        y_std.append(mp.StandardDeviation(Points[i][:,1]))
744    plt.ion()
745    fig, ax = plt.subplots()
746    ax.plot(distances, x_std, label="x std")
747    ax.plot(distances, y_std, label="y std")
748    ax.set_xlabel("Detector distance (mm)")
749    ax.set_ylabel("Standard deviation (mm)")
750    ax.legend()
751    plt.title("Caustics")
752    plt.show()
753    return fig

This function displays the caustics of the rays on the detector. To do so, it calculates the intersections of the rays with the detector over a range determined by the parameter Range, and then plots the standard deviation of the positions in the x and y directions.

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.