import numpy as np import tomllib from scipy.optimize import root_scalar import xraylib from transfocator_calcs import lookup_diameter, materials_to_deltas, materials_to_linear_attenuatio, calc_lookup_table MAT_MACRO = 'MAT' NLENS_MACRO = 'NUMLENS' RADIUS_MACRO = 'RADIUS' LOC_MACRO = 'LOC' THICKERR_MACRO = 'THICKERR' ''' Config variables Beam Properties energy : energy in keV L_und : undulator length in m sigmaH_e : Sigma electron source size in H direction in m sigmaV_e : Sigma electron source size in V direction in m sigmaHp : Sigma electron divergence in H direction in rad sigmaVp_e : Sigma electron divergence in V direction in rad Beamline properties d_StoL1 : Source-to-CRL1 distance, in m d_Stof : Source-to-focus distance, in m CRL properties d_min : Minimum thickness at the apex in m stack_d : Stack thickness in m ''' DEFAULT_CONFIG = {'beam':{'energy': 15, 'L_und': 4.7, 'sigmaH_e': 14.8e-6, 'sigmaV_e': 3.7e-6, 'sigmaHp_e': 2.8e-6, 'sigmaVp_e': 1.5e-6}, 'beam_line': {'d_StoL': 51.9, 'd_Stof': 66.2}, 'crl':{'stack_d': 50.0e-3, 'd_min': 3.0e-5}} """ pyDevice TO DO: WHAT inputs change the focal size arrays? Energy, what else? WHAT inputs change the search through the arrays? desired focal size, what else? IOC init functions -get lens stack parameters (# of lenses in each stack, radius, location, thickness, thickness error) -- from substitution file but put into PVs? Update with autosave? -get source info -energy from from ID IOC -hor/vert sizes and divergence (also energy dependent) -lens diameter table? What is it doing? -desired focal size is changed --> what needs updating? --> nothing, just need to search focal size array again -multiple flags: is focal size achievable? is it achievable at sample? recalc function -- should probably be same as init function -energy is updated --> what needs updating? -what else could user/staff change? sample position? """ ''' Update the following to accommodate XS code ''' class singleTF(): def __init__(self, crl_setup = None, beam_config = DEFAULT_CONFIG['beam'], beamline_config = DEFAULT_CONFIG['beamline'], crl_config = DEFAULT_CONFIG['crl'], slits_config = DEFAULT_CONFIG['slits']): if crl_setup is None: beam = beam_config beamline = beamline_config crl = crl_config slits = slits_config else: with open(crl_setup, "rb") as f: config = tomllib.load(f) beam = config['beam'] beamline = config['beamline'] crl = config['crl'] slits = config['slits'] self.setupSource(beam) self.setupBeamline(beamline) self.setupCRL(crl) # TODO is setupSlits necessary? self.setupSlits(slits) # Initialize lens variables -- TODO -- this is done via a subs file -- are any of these needed prior to that loading? self.numlens = np.array([1, 1, 1, 1, 1, 1, 2, 4, 8, 16]) # CRL1 number of lenses in each stack (was L1_n) self.radius = np.array([2.0e-3, 1.0e-3, 5.0e-4, 3.0e-4, 2.0e-4, 1.0e-4, 1.0e-4, 1.0e-4, 1.0e-4, 1.0e-4]) # CRL1 lens radius in each stack (was L1_R) self.materials = np.array(["Be", "Be", "Be", "Be", "Be", "Be", "Be", "Be", "Be", "Be"]) # CRL1 lens material in each stack (was L1_mater) self.lens_loc = np.array([4.5, 3.5, 2.5, 1.5, 0.5, -0.5, -1.5, -2.5, -3.5, -4.5])*stack_d # CRL1 lens stack location relative to center stack, positive means upstream (was L1_Loc) self.lens_thickerr = np.array([1.0e-6, 1.0e-6, 1.0e-6, 1.0e-6, 1.0e-6, 1.0e-6, 1.4e-6, 2.0e-6, 2.8e-6, 4.0e-6]) # CRL1 lens RMS thickness error (was L1_HE) #<----- lens diameter stuff done in transfocator calcs, should this be removed from here? self.Lens_diameter_table = [ (50, 450.0), (100, 632.0), (200, 894.0), (300, 1095.0), (500, 1414.0), (1000, 2000.0), (1500, 2450.0), ] # Convert the lookup table to a dictionary for faster lookup self.Lens_diameter_dict = {int(col1): col2 for col1, col2 in Lens_diameter_table} # end lens diameter stuff -------> self.energy = 0 # gets value from an ao (incoming beam energy) self.focalSize = 0 # get value from an ao (desired focal length) self.lenses = 0 # sets integer (2^12) whose binary representation indicates which lenses are in or out self.num_lense = 12 # Number of lenses in system self.verbosity = True self.lookupTable = [] def setupSource(self, beam_properties): ''' Beam properties can have entries for the following energy : energy in keV L_und : undulator length in m sigmaH_e : Sigma electron source size in H direction in m sigmaV_e : Sigma electron source size in V direction in m sigmaHp : Sigma electron divergence in H direction in rad sigmaVp_e : Sigma electron divergence in V direction in rad ''' self.setEnergy(beam_properites['energy']) self.beam['L_und'] = beam_properties['L_und'] self.beam['sigmaH_e'] = beam_properties['sigmaH_e'] self.beam['sigmaV_e'] = beam_properties['sigmaV_e'] self.beam['sigmaHp_e'] = beam_properties['sigmaHp_e'] self.beam['sigmaVp_e'] = beam_properties['sigmaVp_e'] self.beam['sigmaH'] = (self.sigmaH_e**2 + self.wl*self.L_und/2/np.pi/np.pi)**0.5 self.beam['sigmaV'] = (self.sigmaV_e**2 + self.wl*self.L_und/2/np.pi/np.pi)**0.5 self.beam['sigmaHp'] = (self.sigmaHp_e**2 + self.wl/self.L_und/2)**0.5 self.beam['sigmaVp'] = (self.sigmaVp_e**2 + self.wl/self.L_und/2)**0.5 def setupBeamline(self, beamline_properties): ''' Beamline properties can contain entries for the following d_StoL1 : Source-to-CRL1 distance, in m d_Stof : Source-to-focus distance, in m ''' self.bl['d_StoL'] = beam_properties['d_StoL'] self.bl['d_Stof'] = beam_properties['d_Stof'] def setupCRL(self, crl_properties): ''' CRL properties can contiain entries for the following d_min : Minimum thickness at the apex in m stack_d : Stack thickness in m ''' self.crl['d_min'] = crl_properties['d_min'] self.crl['stack_d'] = crl_properties['stack_d'] def setupSlits(self, slit_properties): ''' Slit properties can contiain entries for the following ''' pass def setLensCount(self, lensCount): self.numLens = def setupLookupTable(self, subs_file, n_lenses, energy = 8.0): ''' lookup table created after IOC startup (after transfocator materials and thicknesses are set ''' print(80*'#') print('Setting up lens control...') self.num_lenses = n_lenses self.energy = energy #read in substitutions file try: subsFile = open(subs_file,"r") except: raise RuntimeError(f"Substiution file ({subsFile}) not found.") subsFileContent = subsFile.readlines() subsFile.close() macros = subsFileContent[2].replace('{','').replace('}','').replace(',','').split() lens_properties = {key: [] for key in macros} # dictionary of lists for i in range(self.num_lenses): try: xx = subsFileContent[3+i].replace('{','').replace('}','').replace(',','').replace('"','').split() lens_properties[macros[0]].append(xx[0]) lens_properties[macros[1]].append(xx[1]) lens_properties[macros[2]].append(xx[2]) lens_properties[macros[3]].append(xx[3]) lens_properties[macros[4]].append(xx[4]) lens_properties[macros[5]].append(xx[5]) lens_properties[macros[6]].append(xx[6]) except: raise RuntimeError(f"Number of lenses ({self.num_lenses}) doesn't match substitution file") self.numlens = [] self.radius = [] self.materials = [] self.lens_loc = [] self.lens_thickerr = [] # get number of lens for each lens from lens properties dictionary-list print('Getting lens materials...') if NLENS_MACRO in macros: self.numlens = lens_properties[NLENS_MACRO] print('Number of lens read in.\n') else: raise RuntimeError(f"Number of lenses macro ({NLENS_MACRO}) not found in substituion file") # get radii for each lens from lens properties dictionary-list print('Getting lens\' radii...') if RAD_MACRO in macros: self.radius = lens_properties[RAD_MACRO] print('Radius of lenses read in.\n') else: raise RuntimeError(f"Radius macro ({RAD_MACRO}) not found in substituion file") # get materials from lens properties dictionary-list print('Getting lens materials...') if MAT_MACRO in macros: self.materials = lens_properties[MAT_MACRO] print('Lens material read in.\n') else: raise RuntimeError(f"Material macro ({MAT_MACRO}) not found in substituion file") # get densities from local definition (for compounds) or from xraylib (for elements) densities = get_densities(self.materials) self.densities = [densities[material] for material in self.materials] # get location of each lens from lens properties dictionary-list print('Getting lens\' locations...') if LOC_MACRO in macros: self.lens_loc = lens_properties[LOC_MACRO]*self.stack_d print('Location of lenses read in.\n') else: raise RuntimeError(f"Location macro ({RAD_MACRO}) not found in substituion file") # get thicknesses errprfrom lens properties dictionary-list print('Getting lens thickness error...') if TERR_MACRO in macros: self.lens_thickerr = [float(i) for i in lens_properties[TERR_MACRO]] print('Lens thickness errors read in.\n') else: raise RuntimeError(f"Thickness errors macro ({TERR_MACRO}) not found in substituion file") print('Constructing lookup table...') self.construct_lookup_table() print('Lookup table calculation complete.\n') print('Transfocator control setup complete.') print(80*'#') def construct_lookup_table(self): self.lookupTable = calc_lookup_table(self.num_configs, self.radius, self.material, self.energy, self.numlens, self.lens_loc, self.beam, self.bl, self.crl, self.slit1_H, self.slit1_V, self.lens_thickerr, flag_HE = self.thickerr_flag) self.cull_lookup_table() def cull_lookup_table(self): ''' Culls the lookup table based on lenses that are locked and or disabled ''' self.culledSize = 2**(self.num_lenses - (self.outMask | self.inMask).bit_count()) if self.verbose: print(f'Operating spaced now at {self.culledSize} configurations') if self.verbose: print(f'Culling table with in mask {self.inMask} and out mask {self.outMask}') self.culledConfigs = np.empty(self.culledSize, dtype=int) self.culledTable = np.empty(self.culledSize) j = 0 for i in range(2**self.num_lenses): if ((i & self.outMask == 0) and (i & self.inMask == self.inMask)): self.culledConfigs[j]=i self.culledTable[j] = self.lookupTable[i] j += 1 self.sort_lookup_table() def sort_lookup_table(self): ''' ''' if self.verbose: print(f'Sorting culled lookup table of length {len(self.culledTable)}') self.sorted_index = np.argsort(self.culledTable) def updateConfig(self, config_BW): ''' When user manually changes lenses, this gets focal size and displays it along with updated RBVs but it doesn't set the config PV ''' self.configIndex = int(config_BW) self.configIndexSorted = self.sorted_index.tolist().index(self.configIndex) self.setFocalSizeActual() self.updateLensRBV() self.updateFocalSizeRBVs() def setInMask(self, inMask): ''' update mask for lenses that are locked in ''' self.inMask = int(inMask) self.cull_lookup_table() if self.verbose: print(f'Converting culled index via in Mask') self.convertCulledIndex() if self.verbose: print(f'Updating transfocator RBV via in Mask') self.updateLensRBV() if self.verbose: print(f'Setting in mask RBV to {self.inMask}') pydev.iointr('new_inMask', int(self.inMask)) def setOutMask(self, outMask): ''' update mask for lenses that must remain out (either disabled or locked) ''' self.outMask = int(outMask) self.cull_lookup_table() if self.verbose: print(f'Converting culled index via out Mask') self.convertCulledIndex() if self.verbose: print(f'Updating transfocator RBV via out Mask') self.updateLensRBV() if self.verbose: print(f'Setting out mask RBV to {self.outMask}') pydev.iointr('new_outMask', int(self.outMask)) def convertCulledIndex(self): ''' When available configs change, need to update index so that tweaks continue to work ''' if self.verbose: print('Converting ...') self.culledIndex = (np.where(self.culledConfigs == self.config))[0][0] if self.verbose: print(f'Culled index is {self.culledIndex}') self.culledIndexSorted = self.sorted_index.tolist().index(self.culledIndex) if self.verbose: print(f'Sorted culled index is {self.culledIndexSorted}') def setFocalSizeActual(self): ''' ''' self.focalSize_actual = self.culledTable[self.culledIndex] def find_config(self): ''' User selected focal size, this function finds nearest acheivable focal size from the lookup table ''' # Code to search lookup table for nearest focal size to desired if self.verbose: print(f'Searching for config closest to {self.focalSize}') self.culledIndex = np.argmin(np.abs(self.culledTable - self.focalSize)) if self.verbose: print(f'Config index found at {self.culledIndex}') self.culledIndexSorted = self.sorted_index.tolist().index(self.culledIndex) if self.verbose: print(f'Sorted config index found at {self.culledIndexSorted}') # Update PVs self.setFocalSizeActual() self.updateLensConfigPV() self.updateLensRBV() self.updateFocalSizeRBVs() def setSlitSize(self, size, slit): ''' Update proper slit size ''' if slit = 'hor': self.slit1_H = float(size) # H slit size before CRL 1 elif slit == 'vert': self.slit1_V = float(size) # V slit size before CRL 1 else # Need error handling break def updateSlitSize(self, size, slit): ''' Slit size updates are propagated to CRL object from EPICS. The beam size lookup table is then recalculated. ''' self.setSlitSize(self, size, slit) # Testing calling the lookup table reconstruction in EPICS instead of python # self.construct_lookup_table() def setEnergy(self, energy): ''' Sets various forms of energy ''' self.energy = float(energy) self.energy_eV = self.energy*1000.0 # Energy in keV self.wl = 1239.84 / (self.energy_eV * 10**9) #Wavelength in nm(?) def updateE(self, energy): ''' Beam energy updates are propagated to CRL object from EPICS. The beam size lookup table is then recalculated. ''' # Energy variable sent from IOC as a string self.setEnergy(energy) self.construct_lookup_table() # Do I need to find what the current config would produce as far as focal size and location? # self.focalSizeRBV = def updateFsize(self, focalSize): ''' User updates desired focal size. Lookup table is traversed to find nearest to desired. ''' # focalPoint variable sent from IOC as a string self.focalSize = float(focalSize) self.find_config() def updateIndex(self, sortedIndex): ''' User has updated desired sorted index ''' self.culledIndexSorted = int(sortedIndex) self.culledIndex = self.sorted_index[self.culledIndexSorted] # Update PVs self.setFocalSizeActual() self.updateLensConfigPV() self.updateLensRBV() self.updateFocalSizeRBVs() def updateThickerrFlag(self, flag): ''' User has updated thickness error flag so that ... ''' self.thickerr_flag = flag def updateLensConfigPV(self): ''' ''' self.config = self.culledConfigs[self.culledIndex] pydev.iointr('new_lenses', int(self.config)) def updateLensRBV(self): ''' ''' pydev.iointr('new_index', int(self.culledIndexSorted)) def updateEnergyRBV(self): ''' ''' pydev.iointr('updated_E', int(self.energy)) def updateSlitSizeRBV(self, size, slit): ''' Update proper slit size ''' if slit = 'hor': pydev.iointr('updated_slitSize_H', int(self.slit1_H)) elif slit == 'vert': pydev.iointr('updated_slitSize_V', int(self.slit1_V)) def updateFocalSizeRBVs(self): ''' ''' pydev.iointr('new_fSize', self.focalSize_actual) def updateVerbosity(self, verbosity): ''' Turn on minor printing ''' print(f'Verbosity set to {verbosity}') self.verbose = int(verbosity)