Skip to content
Snippets Groups Projects
pyTransfocator_single.py 18.9 KiB
Newer Older
mwyman's avatar
mwyman committed
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
mwyman's avatar
mwyman committed

MAT_MACRO = 'MAT'
NLENS_MACRO = 'NUMLENS'
RADIUS_MACRO = 'RADIUS'
LOC_MACRO = 'LOC'
THICKERR_MACRO = 'THICKERR'
        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
        d_StoL1 : Source-to-CRL1 distance, in m
        d_Stof  : Source-to-focus distance, in m
        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 = 
mwyman's avatar
mwyman committed

    def setupLookupTable(self, subs_file, n_lenses, energy = 8.0):
        '''
        lookup table created after IOC startup (after transfocator materials and 
mwyman's avatar
mwyman committed
        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):
mwyman's avatar
mwyman committed
            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
mwyman's avatar
mwyman committed
        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
mwyman's avatar
mwyman committed
        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")
mwyman's avatar
mwyman committed
        # 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
mwyman's avatar
mwyman committed
        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.')
mwyman's avatar
mwyman committed
        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()
mwyman's avatar
mwyman committed

    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()
mwyman's avatar
mwyman committed
    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')
mwyman's avatar
mwyman committed
        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')
mwyman's avatar
mwyman committed
        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] 
mwyman's avatar
mwyman committed
        User selected focal size, this function finds nearest acheivable focal 
        size from the lookup table
        '''
mwyman's avatar
mwyman committed
        # Code to search lookup table for nearest focal size to desired
mwyman's avatar
mwyman committed
        if self.verbose: print(f'Searching for config closest to {self.focalSize}')
mwyman's avatar
mwyman committed
        self.culledIndex = np.argmin(np.abs(self.culledTable - self.focalSize))
        if self.verbose: print(f'Config index found at {self.culledIndex}')
mwyman's avatar
mwyman committed

mwyman's avatar
mwyman committed
        self.culledIndexSorted = self.sorted_index.tolist().index(self.culledIndex)
mwyman's avatar
mwyman committed
        if self.verbose: print(f'Sorted config index found at {self.culledIndexSorted}')

        # Update PVs
        self.setFocalSizeActual()
        self.updateLensConfigPV()
        self.updateLensRBV()
    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)
mwyman's avatar
mwyman committed

        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):
mwyman's avatar
mwyman committed
		'''
		User has updated thickness error flag so that ...
mwyman's avatar
mwyman committed
		'''
mwyman's avatar
mwyman committed
    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))

mwyman's avatar
mwyman committed
        
    def updateFocalSizeRBVs(self):
        '''
        
        '''
        pydev.iointr('new_fSize', self.focalSize_actual)
  
mwyman's avatar
mwyman committed
    def updateVerbosity(self, verbosity):
        '''
        Turn on minor printing
        '''
        print(f'Verbosity set to {verbosity}')