From 4d34574b912ddb987f6d8cbb0afdf764bfcf4f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Le=20Guyader?= <loic.le.guyader@xfel.eu> Date: Tue, 17 Dec 2019 11:18:49 +0100 Subject: [PATCH] Upload New File --- TZPGcalc.py | 409 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 TZPGcalc.py diff --git a/TZPGcalc.py b/TZPGcalc.py new file mode 100644 index 0000000..5ce4ff6 --- /dev/null +++ b/TZPGcalc.py @@ -0,0 +1,409 @@ +# -*- coding: utf-8 -*- +""" TZPG simple calculator for SCS. + + Interactive widget to calculate beam sizes and position at the sample and detector planes for the SCS instrument. + + Copyright (2019) SCS Team. +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle +from matplotlib.colors import hsv_to_rgb + +import ipywidgets as widgets +from ipywidgets import HBox, VBox, Layout +from IPython.display import display + +# zone plate focal length +F = 250*1e-3 # [m] + +# Z position of the zone plate optic from the first interaction point (sample Z stage at 0 mm) +Z0 = 230*1e-3 # [m] + +# zone plate nominal focal sorthened by the KBS focusing +d = 3.3 - Z0 # distance between HFM and TZPG +f1 = 7.3 # HFM focus 2 m behind second interaction point +F = F*(d-f1)/(d-f1-F) + +class TZPGcalc(): + def __init__(self): + self.initFig() + self.initWidgets() + self.UpdateFig() + display(self.control) + + def initFig(self): + """ Creates a figure for the sample plane and detector plane images with all necessary drawings. + """ + + plt.close('TZPGcalc') + fig, (self.ax_sam, self.ax_det) = plt.subplots(1, 2, num='TZPGcalc', figsize=(6,3)) + + self.ax_sam.set_title('Sample plane') + self.ax_det.set_title('Detector plane') + + self.ax_sam.set_aspect('equal') + self.ax_det.set_aspect('equal') + self.ax_sam.set_xlim([-2, 2]) + self.ax_sam.set_ylim([-2, 2]) + self.ax_det.set_xlim([-35, 35]) + self.ax_det.set_ylim([-20, 50]) + + # red and blue shifted color of the beams + c_rr = hsv_to_rgb([0/360, 50/100, 100/100]) + c_rb = hsv_to_rgb([40/360, 50/100, 100/100]) + c_gr = hsv_to_rgb([95/360, 60/100, 100/100]) + c_gb = hsv_to_rgb([145/360, 60/100, 100/100]) + + self.samBeamsL = {'F0G0': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F0G1': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F0G-1': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F1G0': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_rr, alpha=0.7, lw=None)), + 'F1G1': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_gr, alpha=0.7, lw=None)), + 'F1G-1': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_gr, alpha=0.7, lw=None)) + } + + self.detBeamsL = {'F0G0': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F0G1': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F0G-1': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F1G0': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_rr, alpha=0.7, lw=None)), + 'F1G1': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_gr, alpha=0.7, lw=None)), + 'F1G-1': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_gr, alpha=0.7, lw=None)) + } + + self.samBeamsH = {'F0G0': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F0G1': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F0G-1': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F1G0': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_rb, alpha=0.7, lw=None)), + 'F1G1': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_gb, alpha=0.7, lw=None)), + 'F1G-1': self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_gb, alpha=0.7, lw=None)) + } + + self.detBeamsH = {'F0G0': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F0G1': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F0G-1': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor="black", alpha=0.4, lw=None)), + 'F1G0': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_rb, alpha=0.7, lw=None)), + 'F1G1': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_gb, alpha=0.7, lw=None)), + 'F1G-1': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor=c_gb, alpha=0.7, lw=None)) + } + + self.detLines = {'module': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, fill=False, facecolor='k')), + 'Vfilter': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor="blue", alpha=0.4)), + 'Hfilter': self.ax_det.add_patch(Rectangle((0, 0), 1, 1, facecolor="blue", alpha=0.4)), + } + + # 5x5 membranes + self.sampleLines = {} + self.etchLines = {} + for k in range(25): + self.sampleLines[k] = self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, fill=False, facecolor='k')) + self.etchLines[k] = self.ax_sam.add_patch(Rectangle((0, 0), 1, 1, fill=False, facecolor='k', alpha=0.4, ls='--')) + + def RectUpdate(self, rect, xLeft, yBottom, xRight, yTop): + """ Updates the position and size of the given Rectangle. + + rect: Rectangle to update + xLeft: x position of the left corner + yBottom: y position of the bottom corner + xRight: x position of the right corner + yTop: y position of the top corner + """ + scale = 1e3 # displayed distances in [mm] + + xw = np.abs(xLeft - xRight) + yw = np.abs(yTop - yBottom) + + + rect.set_xy((scale*xLeft, scale*yBottom)) + rect.set_height(scale*yw) + rect.set_width(scale*xw) + + def UpdateBeams(self, Beams, Z, conf): + """ Update the position and size of the beams. + + Beams: dictionary of f'F{f}G{g}' Rectangles for f = 0 and 1 zone plate order and + g = +1, 0 and -1 grating order + Z: distance Z between the zone plate and the current imaging plane + conf: dictionnary of distance for calculation {'F', 'TZPGwH', 'TZPGwV', 'TZPGo', 'theta_grating'} + """ + F = conf['F'] + wH = conf['TZPGwH'] + wV = conf['TZPGwV'] + o = conf['TZPGo'] + offaxis = wV/2 + o + + # side view X + Fx = F*np.arctan(conf['theta_grating']) + + Xdg0 = np.abs(Z - F)/F*wH/2 + Xdg1L = Z/F*(Fx - wH/2) + wH/2 + Xdg1 = Z*np.tan(conf['theta_grating']) + Xdg1H = Z/F*(Fx + wH/2) - wH/2 + + # top view Y + YdfL = (o*(Z - F)/F + offaxis) + Ydf = ((o + wV/2)*(Z - F)/F + offaxis) + YdfH = ((o + wV)*(Z - F)/F + offaxis) + + if Z < F: # before zone plate focus, low and high beam edges are swapped + Xdg1L, Xdg1H = (Xdg1H, Xdg1L) + YdfL, YdfH = (YdfH, YdfL) + + self.RectUpdate(Beams['F0G0'], + -wH/2, -wV/2, wH/2, wV/2) + self.RectUpdate(Beams['F0G1'], + Xdg1-wH/2, -wV/2, Xdg1+wH/2, wV/2) + self.RectUpdate(Beams['F0G-1'], + -Xdg1-wH/2, -wV/2, -Xdg1+wH/2, wV/2) + + self.RectUpdate(Beams['F1G0'], + -Xdg0, YdfL, Xdg0, YdfH) + self.RectUpdate(Beams['F1G1'], + Xdg1L, YdfL, Xdg1H, YdfH) + self.RectUpdate(Beams['F1G-1'], + -Xdg1H, YdfL, -Xdg1L, YdfH) + + def DetectorUpdate(self, Xoff, Yoff): + """ Draw DSSC detector module with filter mask. + + Xoff: x offset + Yoff: y offset + """ + # x module axis is vertical, y module axis is horizontal + # the module 15 is +0.91 mm vertical from the beam and 4.233 mm horizontal from the beam + offset_h = 4.233e-3 #[mm] + offset_v = 0.91e-3 #[mm] + + moduleHw = 256*0.236e-3 #[mm] + moduleVw = 128*0.204e-3 #[mm] + + filterW = 7e-3 #[mm] + filterL = 160e-3 #[mm] + + self.RectUpdate(self.detLines['module'], + -moduleHw - offset_h + Xoff, offset_v + Yoff, -offset_h + Xoff, moduleVw + offset_v + Yoff) + self.RectUpdate(self.detLines['Vfilter'], + -filterW/2 + Xoff, -filterL/2 + Yoff, filterW/2 + Xoff, filterL/2 + Yoff) + self.RectUpdate(self.detLines['Hfilter'], + -filterL/2 + Xoff, -filterW/2 + Yoff, filterL/2 + Xoff, filterW/2 + Yoff) + + def SampleUpdate(self, w, p, Xoff, Yoff, thickness=0.525): + """ Draw the sample. + + w: membrane width + p: membrane pitch + Xoff: sample x offset + Yoff: sample y offset + thickness: sample thickness used to calculate the etched facets + """ + # Si etching angle + wp = w +2*thickness/np.tan(np.deg2rad(54.74)) + + j = 0 + for k in range(-2, 3): + for l in range(-2, 3): + self.RectUpdate(self.sampleLines[j], k*p - w/2 + Xoff, l*p - w/2 - Yoff, k*p + w/2 + Xoff, l*p + w/2 - Yoff) + self.RectUpdate(self.etchLines[j], k*p - wp/2 + Xoff, l*p - wp/2 - Yoff, k*p + wp/2 + Xoff, l*p + wp/2 - Yoff) + j+=1 + + def UpdateFig(self): + """ Update the figure with the current slider values. + + """ + + # we calculate the optics for the central wavelength + nrjL, nrjH = self.nrj_slider.value # [eV] + wlL = 1240/nrjL*1e-9 + wlH = 1240/nrjH*1e-9 + wl = 0.5*(wlL + wlH) + + theta_grating = self.grating_slider.value*1e-3 # [rad] + sampleZ = self.samz_slider.value*1e-3 # [m] + detectorZ = self.det_slider.value*1e-3 # [m] + TZPGwH = self.TZPGwH_slider.value*1e-3 #[m] + TZPGwV = self.TZPGwV_slider.value*1e-3 #[m] + TZPGo = self.TZPGoffaxis_slider.value*1e-3 - TZPGwV/2 #[m] + + d_nominal = wl/np.sin(theta_grating) + self.d_label.value = f'Grating Pitch:{int(np.round(d_nominal*1e9))} nm' + + rn = TZPGwV + TZPGo + dr_nominal = wl * F / (2*rn) + self.dr_label.value = f'Outer Zone Plate width dr:{int(np.round(dr_nominal*1e9))} nm' + + # configuration for the low energy and high energy photon + confL = {'F':(2*rn)*dr_nominal/wlL, + 'theta_grating':np.arcsin(wlL/d_nominal), + 'TZPGwH':TZPGwH, 'TZPGwV':TZPGwV, 'TZPGo':TZPGo} + confH = {'F':(2*rn)*dr_nominal/wlH, + 'theta_grating':np.arcsin(wlH/d_nominal), + 'TZPGwH':TZPGwH, 'TZPGwV':TZPGwV, 'TZPGo':TZPGo} + + # update the beams + self.UpdateBeams(self.samBeamsL, Z0 + sampleZ, confL) + self.UpdateBeams(self.detBeamsL, Z0 + detectorZ, confL) + self.UpdateBeams(self.samBeamsH, Z0 + sampleZ, confH) + self.UpdateBeams(self.detBeamsH, Z0 + detectorZ, confH) + + # update the detector + detXoff = self.detX_slider.value*1e-3 #[m] + detYoff = self.detY_slider.value*1e-3 #[m] + self.DetectorUpdate(detXoff, detYoff) + + # update the sample + samw = self.samw_slider.value*1e-3 #[m] + samp = self.samp_slider.value*1e-3 #[m] + samXoff = self.samX_slider.value*1e-3 #[m] + samYoff = self.samY_slider.value*1e-3 #[m] + samthickness = self.samthickness_slider.value*1e-6 #[m] + self.SampleUpdate(samw, samp, samXoff, samYoff, samthickness) + + def initWidgets(self): + """ Creates the necessary interactive widget controls. + """ + self.button = widgets.Button( + description='Update', + ) + + @self.button.on_click + def plot_on_click(b): + self.UpdateFig() + + # TZPG part + self.nrj_slider = widgets.FloatRangeSlider( + value=[840., 880.], + min=450., + max=3000.0, + step=1, + readout_format='.2f', + ) + self.TZPGwH_slider = widgets.FloatSlider( + value=1.0, + min=.1, + max=3.0, + step=0.05, + readout_format='.2f', + ) + self.TZPGwV_slider = widgets.FloatSlider( + value=1.0, + min=.1, + max=3.0, + step=0.05, + readout_format='.2f', + ) + self.TZPGoffaxis_slider = widgets.FloatSlider( + value=0.75, + min=.0, + max=2.0, + step=0.05, + readout_format='.2f', + ) + self.grating_slider = widgets.FloatSlider( + value=3.8, + min=1., + max=10.0, + step=0.05, + readout_format='.2f', + ) + self.dr_label = widgets.Label(value='dr') + self.d_label = widgets.Label(value='dr') + TZPGTab = VBox(children=[HBox([widgets.Label(value='Energy (eV):'), self.nrj_slider]), + HBox([widgets.Label(value=r'Grating $\theta$ (mrad):'), self.grating_slider]), + self.d_label, self.dr_label, + HBox([widgets.Label(value='TZPG horiz. width (mm):'), self.TZPGwH_slider]), + HBox(children=[HBox([widgets.Label(value='TZPG vert. width (mm):'), self.TZPGwV_slider]), + HBox([widgets.Label(value='TZPG off axis (mm):'), self.TZPGoffaxis_slider]) + ])]) + + # sample part + self.samz_slider = widgets.FloatSlider( + value=30., + min=-10., + max=180.0, + step=1, + readout_format='.2f', + ) + self.samw_slider = widgets.FloatSlider( + value=.5, + min=0.01, + max=2.0, + step=.01, + readout_format='.2f', + ) + self.samp_slider = widgets.FloatSlider( + value=1.0, + min=0.01, + max=2.0, + step=.01, + readout_format='.2f', + ) + self.samX_slider = widgets.FloatSlider( + value=0., + min=-10, + max=10, + step=0.01, + readout_format='.2f', + ) + self.samY_slider = widgets.FloatSlider( + value=0., + min=-10, + max=10, + step=0.01, + readout_format='.2f', + ) + self.samthickness_slider = widgets.FloatSlider( + value=381, + min=1, + max=1000, + step=1, + readout_format='.0f', + ) + samTab = VBox(children=[HBox([widgets.Label(value='Sample Z (mm):'), self.samz_slider]), + HBox(children=[HBox([widgets.Label(value='Membrane width (mm):'), self.samw_slider]), + HBox([widgets.Label(value='Membrane pitch (mm):'), self.samp_slider])]), + HBox(children=[HBox([widgets.Label(value='Sample X-Offset (mm):'), self.samX_slider]), + HBox([widgets.Label(value='Sample Y-Offset (mm):'), self.samY_slider])]), + HBox([widgets.Label(value='Substrate thickness (um):'), self.samthickness_slider]) + ]) + + #detector tab + self.det_slider = widgets.FloatSlider( + value=2000., + min=1000, + max=5800, + step=1, + description='', + readout_format='.2f', + ) + self.detX_slider = widgets.FloatSlider( + value=20., + min=-50, + max=50, + step=0.5, + readout_format='.2f', + ) + self.detY_slider = widgets.FloatSlider( + value=0., + min=-50, + max=50, + step=0.5, + readout_format='.2f', + ) + detTab = VBox(children=[HBox([widgets.Label(value='Detector Z (m):'), self.det_slider]), + HBox(children=[HBox([widgets.Label(value='Detector X-Offset (mm):'), self.detX_slider]), + HBox([widgets.Label(value='Detector Y-Offset (mm):'), self.detY_slider])])]) + + tab1 = widgets.Accordion(children=[TZPGTab]) + tab1.set_title(0, 'TZPG') + tab1.selected_index = None + + tab2 = widgets.Accordion(children=[samTab]) + tab2.set_title(0, 'sample') + tab2.selected_index = None + + tab3 = widgets.Accordion(children=[detTab]) + tab3.set_title(0, 'detector') + tab3.selected_index = None + + self.control = VBox(children=[tab1, tab2, tab3, self.button]) \ No newline at end of file -- GitLab