Code owners
Assign users and groups as approvers for specific file changes. Learn more.
pyCRL_system.py 26.97 KiB
from enum import Enum
import numpy as np
import tomllib
import xraylib
from transfocator_calcs import lookup_diameter, materials_to_deltas, materials_to_linear_attenuation
from transfocator_calcs import find_levels, calc_lookup_table, get_densities
from transfocator_calcs import SYSTEM_TYPE
OE_MACRO = 'OE'
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_StoL2 : Source-to-CRL2 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
stacks : number stacks in systems
KB properties
... : ...
... : ...
'''
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},
'beamline': {'d_StoL1': 51.9, 'd_StoL2': 62.1, 'd_Stof': 66.2},
'crl':[{'stacks': 10, 'stack_d': 50.0e-3, 'd_min': 3.0e-5}],
'kb':{'KBH_L': 180.0e-3, 'KBH_q': 380.0e-3, 'KB_theta': 2.5e-3,
'KBV_L': 300.0e-3, 'KBV_q': 640.0e-3, 'KBH_p_limit': 1.0,
'KBV_p_limit': 1.0 }}
def separate_by_oe(property_list, oe_list, desired_oe):
'''
Description:
Lens properties are read in from substitutions file but not separated by
which transfocator they belong to. This functions separates by optical
elemente (i.e. transfocator)
Parameters:
property_list : list containing a property of all lenses
oe_list : list containing transfocator assignement of all lenses
desired_oe : which oe does user wnat properties for
Returns:
list of desired_oe's property values
'''
return [prop for (prop, oe) in zip(property_list, oe_list) if oe == desired_oe]
class focusingSystem():
def __init__(self, crl_setup = None, beam_config = DEFAULT_CONFIG['beam'],
beamline_config = DEFAULT_CONFIG['beamline'],
crl_configs = DEFAULT_CONFIG['crl'],
kb_config = DEFAULT_CONFIG['kb'], sysType = SYSTEM_TYPE.singleCRL):
'''
Description:
Focusing system object -- either single CRL, double CRL or single
CRL + KB.
Parameters:
crl_setup : ...
Default: None
beam_config : ...
Default: DEFAULT_CONFIG['beam']
beamline_config : ...
Default: DEFAULT_CONFIG['beamline']
crl_configs : ...
Default: DEFAULT_CONFIG['crl']
kb_config : ...
Default: DEFAULT_CONFIG['kb']
sysType : ...
Default: SYSTEM_TYPE.singleCRL
'''
self.verbose = True
self.elements = []
self.n_elements = 0
if crl_setup is None:
beam = beam_config
beamline = beamline_config
crl = crl_configs
kb = kb_config
self.sysType = sysType
else:
with open(crl_setup, "rb") as f:
config = tomllib.load(f)
beam = config['beam']
beamline = config['beamline']
#check config for the crl, crl1, crl2, kb and use this to determine system type
crl = []
if "crl" in config:
self.n_elements+=1
crl.append(config['crl'])
self.sysType = SYSTEM_TYPE.singleCRL
self.elements.append('1')
if "kb" in config:
self.n_elements+=1
kb = config['kb']
self.sysType = SYSTEM_TYPE.CRLandKB
self.elements.append('kb')
if "crl1" in config:
self.n_elements+=1
crl.append(config['crl1'])
self.sysType = SYSTEM_TYPE.singleCRL
self.elements.append('1')
if "crl2" in config:
self.n_element+=1
crl.append(config['crl2'])
self.sysType = SYSTEM_TYPE.doubleCRL
self.elements.append('2')
# Setup beam properties
self.beam = {}
self.setupSource(beam)
# Setup beamline position of elements
self.bl = {}
self.setupBeamline(beamline)
# Setup element properties
self.crl = {}
self.setupCRL(elements)
if self.sysType is SYSTEM_TYPE.CRLandKB:
self.setupKB(kb)
# Initialize slit sizes to 0
self.setupSlits()
#<----------------------------------------------------------------------
# Are these needed at initialization
#TODO -- any generalizations? Yes but how? Need to do by elements?
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^10) whose binary representation indicates which lenses are in or out
#TODO -- any generalizations? Yes but how? Need to do by elements?
self.num_stacks = 10 # Number of lenses in system
#---------------------------------------------------------------------->
self.lookupTable = []
self.thickerr_flag = True
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_e : Sigma electron divergence in H direction in rad
sigmaVp_e : Sigma electron divergence in V direction in rad
'''
self.setEnergy(beam_properties['energy'])
self.L_und = beam_properties['L_und']
self.sigmaH_e = beam_properties['sigmaH_e']
self.sigmaV_e = beam_properties['sigmaV_e']
self.sigmaHp_e = beam_properties['sigmaHp_e']
self.sigmaVp_e = beam_properties['sigmaVp_e']
self.setupSourceEnergyDependent()
def setEnergy(self, energy):
'''
Sets various forms of energy
'''
if energy > 0.0001:
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(?)
if self.verbose: print(f'Setting energy to {self.energy} keV')
def setupSourceEnergyDependent(self):
'''
Sets various energy dependent source parameters. Called whenever energy
is updated
'''
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, num=1):
'''
Beamline properties can contain entries for the following
d_StoL1 : Source-to-CRL1 distance, in m
d_StoL2 : Source-to-CRL2 distance, in m
d_Stof : Source-to-sample distance, in m
'''
self.bl['d_StoL1'] = beamline_properties['d_StoL1']
self.bl['d_Stof'] = beamline_properties['d_Stof']
if self.sysType is SYSTEM_TYPE.doubleCRL
self.bl['d_StoL2'] = beamline_properties['d_StoL2']
# if self.sysType is singleCRLandKB # KB doesn't have location???
# self.bl['d_StoKB'] = beamline_properties['d_StoKB']
def setupCRL(self, crl):
'''
Looks through crl (list of transforcators) for entries for the following
d_min : Minimum thickness at the apex in m
stack_d : Stack thickness in m
stacks : number of stacks in system
'''
for elem, tf in enumerate(crl):
self.crl[elem]= {'d_min': tf['d_min'], 'stack_d': tf['stack_d'], 'stacks': tf['stacks']}
def setupKB(self, kb):
'''
Looks through kb for kb properties
KBH_L : KBH length
KBH_q : KBH q
KB_theta : KB mirror angle
KBV_L : KBV length
KBV_q : KBV q
KBH_p_limit : Minimum p that KBH can achieve, so abs(KBH_p) > KBH_p_min
KBV_p_limit : Minimum p that KBV can achieve, so abs(KBV_p) > KBV_p_min
'''
self.kb['KBH_L'] = kb['KBH_L']
self.kb['KBH_q'] = kb['KBH_q']
self.kb['KB_theta'] = kb['KB_theta']
self.kb['KBV_L'] = kb['KBV_L']
self.kb['KBV_q'] = kb['KBV_q']
self.kb['KBH_p_limit'] = kb['KBH_p_limit']
self.kb['KBV_p_limit'] = kb['KBV_p_limit']
def setupSlits(self):
'''
Initializes slit sizes to 0
'''
self.slits['1'] = {'hor':0,'vert':0}
if self.sysType is SYSTEM_TYPE.doubleCRL:
self.slits['2'] = {'hor':0,'vert':0}
if self.sysTYPE is SYSTEM_TYPE.CRLandKB:
self.slits['KB'] = {'hor':0,'vert':0}
def updateSlitSize(self, size, oe, slit):
'''
Slit size updates are propagated to CRL object from EPICS. The beam
size lookup table is then recalculated.
'''
self.slits[oe][slit] = float(size)
if self.verbose: print(f"{oe} {slit} slit is set to {self.slits[oe][slit]}")
def updateSlitSizeRBV(self, oe, slit):
'''
Update proper slit size
'''
oes = oe if isinstance(oe, list) else [oe]
for element in oes:
intr_string = 'updated_slitSize_'+element+'_'+slit
pydev.iointr(intr_string, float(self.slits[element][slit]))
if self.verbose: print(f"{oe} {slit} slit size RBV udpated to {self.slits[element][slit]}")
#TODO how to differentiate tf1 from tf2?
def parseSubsFile(self, subs_file):
'''
Description:
Parameters:
... : ...
Returns:
... : ...
'''
#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_stacks):
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])
lens_properties[macros[6]].append(xx[7])
except:
raise RuntimeError(f"Number of lenses ({self.num_stacks}) doesn't match substitution file")
self.numlens = []
self.radius = []
self.materials = []
self.lens_loc = []
self.lens_thickerr = []
# get number of lens for each lens stack from lens properties dictionary-list
print('Getting OE assignments...')
if OE_MACRO in macros:
self.oe_num = np.array([int(i) for i in lens_properties[NLENS_MACRO]])
print('OE assignments read in.\n')
else:
raise RuntimeError(f"OE assignemnt macro ({OE_MACRO}) not found in substituion file")
# get number of lens for each lens stack from lens properties dictionary-list
print('Getting lens materials...')
if NLENS_MACRO in macros:
self.numlens = np.array([int(i) for i in 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 RADIUS_MACRO in macros:
self.radius = np.array([float(i) for i in lens_properties[RADIUS_MACRO]])
print('Radius of lenses read in.\n')
else:
raise RuntimeError(f"Radius macro ({RADIUS_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 = np.array([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_locations = np.array([float(l)*self.crl[str(self.oe_num[i])]['stack_d'] for i,l in enumerate(lens_properties[LOC_MACRO])])
print('Location of lenses read in.\n')
else:
raise RuntimeError(f"Location macro ({LOC_MACRO}) not found in substituion file")
# get thicknesses errprfrom lens properties dictionary-list
print('Getting lens thickness error...')
if THICKERR_MACRO in macros:
self.lens_thickerr = np.array([float(i) for i in lens_properties[THICKERR_MACRO]])
print('Lens thickness errors read in.\n')
else:
raise RuntimeError(f"Thickness errors macro ({THICKERR_MACRO}) not found in substituion file")
def setupLookupTable(self, subs_file, n_lenses):
'''
lookup table created after IOC startup; called directly by ioc startup file
Note: energy and slit size are updated before this is called but table
calculation is disabled
'''
print(80*'#')
print('Setting up lens control...')
# convert number of lenses to list
self.num_stacks = n_lenses if isinstance(n_lenses, list) else [n_lenses]
self.num_stacks = sum(n_lenses)
self.num_configs = 2**(min(n_lenses))
self.configs = {}
for i, n in enumerate(n_lenses): self.configs[str(i+1)] = np.arange(2**n)
print('Constructing lookup table...')
self.construct_lookup_table()
print('Lookup table calculation complete.\n')
print('Transfocator control setup complete.')
print(80*'#')
self.radii = {}
self.radii['1'] = separate_by_oe(self.radius, self.oe_num, 1)
self.radii['2'] = separate_by_oe(self.radius, self.oe_num, 2)
self.mat = {}
self.mat['1'] = separate_by_oe(self.materials, self.oe_num, 1)
self.mat['2'] = separate_by_oe(self.materials, self.oe_num, 2)
self.lens_loc = {}
self.lens_loc['1'] = separate_by_oe(self.lens_locations, self.oe_num, 1)
self.lens_loc['2'] = separate_by_oe(self.lens_locations, self.oe_num, 2)
self.thickerr = {}
self.thickerr['1'] = separate_by_oe(self.lens_thickerr, self.oe_num, 1)
self.thickerr['2'] = separate_by_oe(self.lens_thickerr, self.oe_num, 2)
def construct_lookup_table(self):
'''
Description:
Parameters:
... : ...
Returns:
... : ...
'''
if self.sysType == SYSTEM_TYPE.singleCRL:
arr_a, dict_b, dict_c = calc_1x_lu_table(self.num_configs,
self.radii['1'], self.mat['1'],
self.energy, self.wl,
self.numlens, self.lens_loc['1'],
self.beam, self.bl, self.crl,
self.slits[0]['H'], self.slits[0]['V'],
self.lens_thickerr['1'],
flag_HE = self.thickerr_flag,
verbose = self.verbose)
elif self.sysType == SYSTEM_TYPE.doubleCRL:
arr_a, dict_b, dict_c, arr_d = calc_2x_lu_table(self.num_configs,
self.radii['1'], self.mat['1'],
self.radii['2'], self.mat['2'],
self.energy, self.wl,
self.numlens, self.lens_loc['1'],
self.lens_loc['2'],
self.beam, self.bl, self.crl,
self.slits,
self.lens_thickerr['1'],
self.lens_thickerr['2'],
flag_HE = self.thickerr_flag,
verbose = self.verbose)
self.index1to2_sorted = arr_d
elif self.sysType == SYSTEM_TYPE.CRLandKB:
arr_a, dict_b, dict_c = calc_kb_lu_table(self.num_configs,
self.radii['1'], self.mat['1'],
self.energy, self.wl,
self.numlens, self.lens_loc['1'],
self.beam, self.bl, self.crl,
self.kb, self.slits,
self.lens_thickerr['1'],
flag_HE = self.thickerr_flag,
verbose = self.verbose)
self.lookupTable = arr_a
self.sorted_invF_index = dict_b
self.sorted_invF = dict_c
self.updateEnergyRBV()
self.updateSlitSizeRBV(self.elements, 'hor')
self.updateSlitSizeRBV(self.elements, 'vert')
self.updateLookupWaveform()
self.updateInvFWaveform()
self.updateLookupConfigs()
def updateEnergyRBV(self):
'''
'''
pydev.iointr('updated_E', float(self.energy))
def updateInvFWaveform(self):
'''
Puts invF lists into waveform PVs
'''
intr_string = 'updated_slitSize_'+oe+'_'+slit
pydev.iointr(intr_string, float(self.slits[oe][slit]))
pydev.iointr('new_invFind_list_1', self.sorted_invF_index['1'].tolist())
pydev.iointr('new_invF_list_1', self.sorted_invF['1'].tolist())
if self.sorted_invF_index['2'] is not None
pydev.iointr('new_invFind_list_2', self.sorted_invF_index['2'].tolist())
pydev.iointr('new_invF_list_2', self.sorted_invF['2'].tolist())
def updateLookupConfigs(self):
'''
Puts lookup table config integers into waveform PV
'''
pydev.iointr('new_configs_1', self.configs['1'].tolist())
if self.sysType = SYSTEM_TYPE.doubleCRL:
pydev.iointr('new_configs_2', self.configs['2'].tolist())
def updateIndex(self, sortedIndex, oe):
'''
User has updated desired sorted index for either CRL1 or CRL2. In double CRL
case, this update (either of CRL1 or CRL2**) moves the system off the lookup table.
If double CRL system's CRL1 index is moved, it will move on lookup table,
so CRL2 will be udpated as well
**In practice, user should only move CRL2 in the double case
'''
self.indexSorted[oe] = int(sortedIndex)
self.index[oe] = self.sorted_invF_index[oe][self.indexSorted]
if oe == '1' and self.sysType == SYSTEM_TYPE.doubleCRL:
self.index['2'] = self.index1to2[self.indexSorted]
# Update PVs
if oe = '2':
self.setFocalSizeActual(offTable = True)
else
self.setFocalSizeActual(offTable = False)
self.updateLensConfigPV()
self.updateLensRBV()
self.updateFocalSizeRBVs()
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()
#TODO look through
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; note the
# lookup table is already sorted by 1/f
if self.verbose: print(f'Searching for config closest to {self.focalSize}')
# simple approach
# self.indexSorted = np.argmin(np.abs(self.lookupTable - self.focalSize))
# XS approach -- can handle nan but in pydev application don't have a good
# way to "transmit" errors (i.e. no solution found) to user.
indices, _ = find_levels(self.lookupTable, self.focalSize, direction='forward')[0]
self.indexSorted = indices[0]
if self.verbose: print(f'1/f-sorted config index found at {self.indexSorted}')
self.index['1'] = self.sorted_invF_index['1'][self.indexSorted]
if self.verbose: print(f'CRL 1 config index found at {self.index['1']}')
if self.sysType is SYSTEM_TYPE.doubleCRL:
self.index['2'] = self.index1to2[self.indexSorted]
if self.verbose: print(f'CRL 2 config index found at {self.index['2']}')
# Update PVs
self.setFocalSizeActual(offTable = False)
self.updateLensConfigPV()
self.updateLensRBV()
self.updateFocalSizeRBVs()
def setFocalSizeActual(self, offTable = False):
'''
offTable case for double CRL when 2nd CRL is changed.
'''
if not offTable:
self.focalSize_actual = self.lookupTable[self.indexSorted]
else
self.focalSize_actual = calc_2xCRL_focus(self.index['1'], self.index['2'],
self.radii['1'], self.mat['1'],
self.radii['2'], self.mat['2'],
self.energy, self.wl,
self.numlens, self.lens_loc['1'],
self.lens_loc['2'],
self.beam, self.bl, self.crl,
self.slits,
self.lens_thickerr['1'],
self.lens_thickerr['2'],
flag_HE = self.thickerr_flag,
verbose = self.verbose)
def updateLensConfigPV(self):
'''
'''
self.config['1'] = self.configs['1'][self.index['1']]
pydev.iointr('new_lenses_1', int(self.config['1']))
if self.sysType is SYSTEM_TYPE.doubleCRL:
self.config['2'] = self.configs['2'][self.index['2']]
pydev.iointr('new_lenses_2', int(self.config['2']))
def updateLensRBV(self):
'''
'''
pydev.iointr('new_index_1', int(self.indexSorted))
if self.sysType is SYSTEM_TYPE.doubleCRL:
pydev.iointr('new_index_2', int(self.indexSorted))
def updateFocalSizeRBVs(self):
'''
'''
pydev.iointr('new_fSize', self.focalSize_actual)
#TODO check
def getPreviewFocalSize(self, sortedIndex):
'''
'''
fSize_preview = self.lookupTable[sortedIndex]
if self.verbose: print(f'Preview focal sizes for {sortedIndex} is {fSize_preview}')
pydev.iointr('new_preview', fSize_preview)
#TODO check
def setThickerrFlag(self, flag):
'''
User has updated thickness error flag so that ...
'''
self.thickerr_flag = int(flag)
if self.verbose: print(f'Thickness Error Flag set to {flag}')
self.updateThickerrFlagRBV()
#TODO check
def updateThickerrFlagRBV(self):
'''
Thickness error flag has been updated
'''
if self.verbose: print(f'Thickness Error Flag RBV set to {self.thickerr_flag}')
pydev.iointr('updated_thickerr_Flag', self.thickerr_flag)
#TODO check
def updateE(self, energy):
'''
Beam energy updates are propagated to CRL object from EPICS. The beam
size lookup table is then recalculated.
'''
if energy > 0.0001:
# Energy variable sent from IOC as a string
self.setEnergy(energy)
# Update beam properties that are dependent on energy
self.setupSourceEnergyDependent()
else:
if verbose: print(f'Invalid energy setting: {energy} kev; staying at {self.energy} keV')
#TODO check
def updateLookupWaveform(self):
'''
Puts lookup table focal sizes into waveform PV
'''
pydev.iointr('new_lookupTable', self.lookupTable.tolist())
#TODO check
def updateVerbosity(self, verbosity):
'''
Turn on minor printing
'''
print(f'Verbosity set to {verbosity}')
self.verbose = int(verbosity)