# -*- coding: utf-8 -*- """ Geometric beam calculator for SCS. Copyright (2021) SCS Team. """ import numpy as np import matplotlib.pyplot as plt from sympy.physics.optics import GeometricRay, FreeSpace, ThinLens from sympy.solvers import solve from sympy import symbols, simplify from sympy.utilities.lambdify import lambdify class GeoBeams: """The Beams object to compute beam propagation from source point to sample. Inputs ------ elems : dict dictionnaries of numerical values for the different parameters. """ def __init__(self): # all distances in meters self.elems = { 'dIHF': -56, # Intermediate Horizontal Focus position 'dEX': -30, # Exit Slit position 'dHFM': -3.35, # Horizontal Focusing Mirror position 'fHFM': 5.74, # HFM focal length 'dVFM': -2, # Vertical Focusing Mirror position 'fVFM': 5.05, # VFM focal length 'dBOZ': -0.23, # Beam splitting Off axis Zone plate position 'fBOZ_x': 0.25, # BOZ horizontal focal length 'fBOZ_y': 0.25, # BOZ vertical focal distance 'theta_grating': 0, # grating deflection angle in rad 'dSAMZ': 0.0, # Sample position 'ddetZ': 2.0, # Detector position 'WH': 0.8e-3, # BOZ horizontal width 'WV': 0.8e-3, # BOZ vertical width 'offaxis': -0.55e-3, # BOZ center to optical axis 'EXw': 100e-6, # Exit Slit width 'IHFw': 200e-6, # IHF source width 'x1': 0, # horizontal beam height at source 'theta1_x': 0, # horizontal beam angle at source 'x2': 0, # horizontal beam height at BOZ 'y1': 0, # vertical beam height at source 'theta1_y': 0, # vertical beam angle at source 'y2': 0 # vertical beam height at BOZ } self.compute_eqs() def compute_eqs(self): """Compute the beam propagation equation between source and zone plate. eq_x, eq_y: tuple, the horizontal x and vertical y equations to compute the initial angle from the source to pass by the zone plate position x2, given the source position at x1. """ dEX, dIHF, dVFM, dHFM, dBOZ = symbols('dEX, dIHF, dVFM, dHFM, dBOZ') fVFM, fHFM, fBOZ_x, fBOZ_y = symbols('fVFM, fHFM, fBOZ_x, fBOZ_y') x1, theta1_x, x2 = symbols('x1, theta1_x, x2') y1, theta1_y, y2 = symbols('y1, theta1_y, y2') # horizontal and vertical beam propagation from the # source to the zone plate self.HFM = simplify( FreeSpace(-dHFM+dBOZ)*ThinLens(fHFM) * FreeSpace(-dIHF+dHFM)*GeometricRay(x1, theta1_x)) self.VFM = simplify( FreeSpace(-dVFM+dBOZ)*ThinLens(fVFM) * FreeSpace(-dEX+dVFM)*GeometricRay(y1, theta1_y)) self.HFM_f = lambdify(self.elems.keys(), self.HFM, ['numpy']) self.VFM_f = lambdify(self.elems.keys(), self.VFM, ['numpy']) # beams for the zone plate first order (0th order is unfocused) self.HBOZ = simplify(ThinLens(fBOZ_x)*self.HFM) self.VBOZ = simplify(ThinLens(fBOZ_y)*self.VFM) self.HBOZ_f = lambdify(self.elems.keys(), self.HBOZ, ['numpy']) self.VBOZ_f = lambdify(self.elems.keys(), self.VBOZ, ['numpy']) # solve which beam angle at the source theta1 ends on the # zone plate at x2, given x1 self.theta1_x = solve(self.HFM[0] - x2, theta1_x)[0] self.theta1_y = solve(self.VFM[0] - y2, theta1_y)[0] self.theta1_x_f = lambdify(self.elems.keys(), self.theta1_x, ['numpy']) self.theta1_y_f = lambdify(self.elems.keys(), self.theta1_y, ['numpy']) def path(self): """Compute the horizontal and vertical beam propagation. """ x = [] z_x = [] # source x1 = self.elems['x1'] angle1 = self.elems['theta1_x'] x += [x1] z_x += [self.elems['dIHF']] ray = GeometricRay(x1, angle1) # KBS ray = (ThinLens(self.elems['fHFM']) * FreeSpace(-self.elems['dIHF'] + self.elems['dHFM'])*ray) z_x += [self.elems['dHFM']] x += [ray[0].evalf()] # BOZ ray = (ThinLens(self.elems['fBOZ_x']) * FreeSpace(-self.elems['dHFM'] + self.elems['dBOZ'])*ray) z_x += [self.elems['dBOZ']] x += [ray[0].evalf()] # detector ray = FreeSpace(-self.elems['dBOZ']+self.elems['ddetZ'])*ray z_x += [self.elems['ddetZ']] x += [ray[0].evalf()] y = [] z_y = [] # source y1 = self.elems['y1'] angle1 = self.elems['theta1_y'] y += [y1] z_y += [self.elems['dEX']] ray = GeometricRay(y1, angle1) # KBS ray = (ThinLens(self.elems['fVFM']) * FreeSpace(-self.elems['dEX']+self.elems['dVFM'])*ray) z_y += [self.elems['dVFM']] y += [ray[0].evalf()] # BOZ ray = (ThinLens(self.elems['fBOZ_y']) * FreeSpace(-self.elems['dVFM']+self.elems['dBOZ'])*ray) z_y += [self.elems['dBOZ']] y += [ray[0].evalf()] # detector ray = FreeSpace(-self.elems['dBOZ']+self.elems['ddetZ'])*ray z_y += [self.elems['ddetZ']] y += [ray[0].evalf()] return z_x, x, z_y, y def plot(self): fig, ax = plt.subplots(2, 1, figsize=(6, 4), sharex=True) xmin, xmax = [np.Inf, -np.Inf] ymin, ymax = [np.Inf, -np.Inf] xs = np.empty((4,4)) ys = np.empty((4,4)) for k, (x2, y2) in enumerate(zip( [self.elems['WH']/2, -self.elems['WH']/2], np.array([self.elems['WV']/2, -self.elems['WV']/2]) + self.elems['offaxis'])): for kk, (x1, y1) in enumerate(zip( [self.elems['IHFw']/2, -self.elems['IHFw']/2], [self.elems['EXw']/2, -self.elems['EXw']/2])): c = f'C{3*k+kk}' self.elems['x1'] = x1 self.elems['x2'] = x2 self.elems['y1'] = y1 self.elems['y2'] = y2 self.elems['theta1_x'] = self.theta1_x_f( *list(self.elems.values())) self.elems['theta1_y'] = self.theta1_y_f( *list(self.elems.values())) z_x, x, z_y, y = self.path() xs[2*k + kk, :] = 1e3*np.array(x) ys[2*k + kk, :] = 1e3*np.array(y) if xmin > x[-1]: xmin = x[-1] if xmax < x[-1]: xmax = x[-1] if ymin > y[-1]: ymin = y[-1] if ymax < y[-1]: ymax = y[-1] ax0in = ax[0].inset_axes([0.1, 0.65, 0.2, 0.25]) ax1in = ax[1].inset_axes([0.1, 0.65, 0.2, 0.25]) # fill_between to show bea255s for k in range(4): for kk in range(k+1, 4): ax[0].fill_between(z_x, xs[k,:], xs[kk, :], color='C0') ax0in.fill_between(z_x, xs[k,:], xs[kk, :], color='C0') ax[1].fill_between(z_y, ys[k,:], ys[kk, :], color='C0') ax1in.fill_between(z_y, ys[k,:], ys[kk, :], color='C0') ax0in.set_xlim([-0.02, 0.1]) ax0in.set_ylim([-0.10, 0.10]) ax1in.set_xlim([-0.02, 0.1]) ax1in.set_ylim([-0.05, 0.15]) ax[0].set_ylabel('x (mm)') ax[1].set_ylabel('y (mm)') ax[1].set_xlabel('z (m)') ax[0].axvline(self.elems['dSAMZ'], ls='--', c='k', alpha=0.5) ax0in.axvline(self.elems['dSAMZ'], ls='--', c='k', alpha=0.5) ax[1].axvline(self.elems['dSAMZ'], ls='--', c='k', alpha=0.5) ax1in.axvline(self.elems['dSAMZ'], ls='--', c='k', alpha=0.5) return fig def plane_image(self, p0, n, ZPorder, Gorder=0): """Compute the BOZ image on a plane. Inputs ------ p0: [x, y, z] point on the imaging plane n: [X, Y, Z] plane normal vector ZPorder: int, order of the zone plate, i.e. 0 or 1 Gorder: int, grating order, in [-1, 0, 1] Returns ------- list of 4 corners [x,y] """ xmin, xmax = [np.Inf, -np.Inf] ymin, ymax = [np.Inf, -np.Inf] for k, (x2, y2) in enumerate(zip( [self.elems['WH']/2, -self.elems['WH']/2], np.array([self.elems['WV']/2, -self.elems['WV']/2]) + self.elems['offaxis'])): for kk, (x1, y1) in enumerate(zip( [self.elems['IHFw']/2, -self.elems['IHFw']/2], [self.elems['EXw']/2, -self.elems['EXw']/2])): self.elems['x1'] = x1 self.elems['y1'] = y1 self.elems['x2'] = x2 self.elems['y2'] = y2 self.elems['theta1_x'] = self.theta1_x_f( *list(self.elems.values())) self.elems['theta1_y'] = self.theta1_y_f( *list(self.elems.values())) # evaluate HFM/VFM or HBOZ/VBOZ if ZPorder == 1: bx = self.HBOZ_f(*list(self.elems.values())) by = self.VBOZ_f(*list(self.elems.values())) elif ZPorder == 0: bx = self.HFM_f(*list(self.elems.values())) by = self.VFM_f(*list(self.elems.values())) else: raise ValueError( 'ZPorder other than 0 or 1 not implemented') # intersection beam with plane l1 = np.array([x2, y2, self.elems['dBOZ']]) l12 = np.array([bx[1][0] + Gorder*self.elems['theta_grating'], by[1][0], 1]) p = self.LinePlaneIntersection(p0, n, l1, l12=l12) if xmin > p[0]: xmin = p[0] if xmax < p[0]: xmax = p[0] if ymin > p[1]: ymin = p[1] if ymax < p[1]: ymax = p[1] return np.array([[xmax, ymax], [xmax, ymin], [xmin, ymin], [xmin, ymax]]) def LinePlaneIntersection(self, p0, n, l1, *, l2=None, l12=None): """ Calculate the intersection point in space between a line (beam) passing through 2 points l1 and l2 and a (sample) plane passing by p0 with normal n l1: [x,y,z] point on line l2: [x,y,z] point on line p0: [x,y,z] point on plane n: plane normal vector """ assert not((l2 is None) and (l12 is None)), ( "Either l2 or l12 must be defined") # plane parametrized as (p - p0).n = 0 # line parametrized as p = l1 + l12*d with d Real if l12 is None: l12 = l2 - l1 if np.dot(l12, n) == 0: return [0, 0, 0] # line is either in the plane or outside of it else: d = np.dot((p0 - l1), n)/np.dot(l12, n) return l1 + l12*d