Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
pyTransfocator_single.py 26.78 KiB
import numpy as np
from scipy.optimize import root_scalar
import xraylib
from transfocator_calcs import lookup_diameter, materials_to_deltas, materials_to_linear_attenuatio, calc_lookup_tablen

MAT_MACRO = 'MAT'
NLENS_MACRO = 'NUMLENS'
RADIUS_MACRO = 'RADIUS'
LOC_MACRO = 'LOC'
THICKERR_MACRO = 'THICKERR'

"""
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?
"""

# Beamline input block
energy = 15000.0            # Energy in eV
energy_keV = energy/1000.0  # Energy in keV
wl = 1239.84 / (energy * 10**9)
d_StoL1 = 51.9              # Source-to-CRL1 distance, in m
d_StoL2 = 62.1              # Source-to-CRL2 distance, in m
d_Stof  = 66.2              # Source-to-focus distance, in m
#slit1_H = 500.0e-6          # H slit size before CRL 1
#slit1_V = 300.0e-6          # V slit size before CRL 1

# CRL input block
d_min   = 3.0e-5            # Minimum thickness at the apex in m
stack_d = 50.0e-3           # Stack thickness in m
L1_n    = np.array([1,      1,      1,      1,      1,      1,      2,      4,      8,      16])                # CRL1 number of lenses in each stack
L1_R    = 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
L1_mater= np.array(["Be",   "Be",   "Be",   "Be",   "Be",   "Be",   "Be",   "Be",   "Be",   "Be"])              # CRL1 lens material in each stack
L1_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
L1_HE   = 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


# Source size input block
L_und = 4.7                 # undulator length
sigmaH_e = 14.8e-6          # Sigma electron source size in H direction in m
sigmaV_e = 3.7e-6           # Sigma electron source size in V direction in m
sigmaHp_e = 2.8e-6          # Sigma electron divergence in H direction in rad
sigmaVp_e = 1.5e-6          # Sigma electron divergence in V direction in rad
sigmaH = (sigmaH_e**2 + wl*L_und/2/np.pi/np.pi)**0.5
sigmaV = (sigmaV_e**2 + wl*L_und/2/np.pi/np.pi)**0.5
sigmaHp = (sigmaHp_e**2 + wl/L_und/2)**0.5
sigmaVp = (sigmaVp_e**2 + wl/L_und/2)**0.5






def absorptionaperture(x, n1mud, sigma, n1mur):
    numerator = np.exp(-(x**2/(2*sigma**2))) * np.exp(-n1mur*(x**2) - n1mud)
    denominator = np.exp(-n1mud)
    return numerator / denominator - 0.5

def find_levels(array, levels, direction='forward'):
    """
    Find the first indices at which the array crosses specified levels and the corresponding crossed values.

    Parameters:
        array (numpy.ndarray): An array of numbers.
        levels (float or numpy.ndarray): A number or an array of levels to find crossings.
        direction (str, optional): The searching direction. Defaults to 'forward'.
                                   Can be either 'forward' or 'backward'.

    Returns:
        tuple: A tuple containing two arrays:
            - An array of first indices at which the array crosses the specified levels.
            - An array of first crossed values at the corresponding indices.
    """

    # Convert a single level to a numpy array
    if isinstance(levels, (int, float)):
        levels = np.array([levels])

    indices = []
    values = []

    # Compute the max and min of the array ignoring NaNs
    max_val = np.nanmax(array)
    min_val = np.nanmin(array)

    for level in levels:
        # If level is out of bounds
        if level > max_val or level < min_val:
            indices.append(-1)
            values.append(np.nan)
            continue

        crossings = []

        if direction == 'forward':
            for i in range(1, len(array)):
                if np.isnan(array[i - 1]) or np.isnan(array[i]):
                    continue
                if (array[i - 1] < level <= array[i]) or (array[i - 1] > level >= array[i]):
                    crossings.append(i - 1)
                    break

        elif direction == 'backward':
            for i in range(len(array) - 2, -1, -1):
                if np.isnan(array[i + 1]) or np.isnan(array[i]):
                    continue
                if (array[i + 1] < level <= array[i]) or (array[i + 1] > level >= array[i]):
                    crossings.append(i)
                    break

        else:
            raise ValueError("Invalid direction. It should be either 'forward' or 'backward'.")

        if len(crossings) > 0:
            idx = crossings[0]
            indices.append(idx)
            values.append(array[idx])
        else:
            # In case no crossing is found within the range
            indices.append(-1)
            values.append(np.nan)

    return np.array(indices), np.array(values)


def Single_CRL2D_control(fsize):

    L1_D        = np.zeros(L1_R.size)                                   # CRL1 diameters for each stack
    for i in range(L1_R.size):
        L1_D[i] = lookup_diameter(L1_R[i])
    L1_delta    = materials_to_deltas(L1_mater, energy_keV)             # delta values for CRL1 stacks
    L1_mu       = materials_to_linear_attenuation(L1_mater, energy_keV) # mu values for CRL1 stacks
    L1_Feq      = L1_R/(2*L1_n*L1_delta) + L1_loc                       # CRL1 equivalent f in m for each stack
    
    L1_index_n  = 2**L1_Feq.size                                        # Total number of combinations for CRL1
    L1_invF_list= np.zeros(L1_index_n)                                  # List of equivalent 1/f in m^-1 for CRL1
    for i in range(L1_index_n):
        L1_invF_list[i] = np.sum(index_to_binary_list(i, L1_Feq.size)/L1_Feq)
    # Sort the L1_invF list (to avoid zigzagging)
    L1_invF_list_sort_indices = np.argsort(L1_invF_list)
    L1_invF_list_sorted       = L1_invF_list[L1_invF_list_sort_indices]
    q1_list  = 1/(L1_invF_list_sorted - 1/d_StoL1)      # focal position of CRL1 for all configurations (sorted)
    dq1_list = q1_list - (d_Stof - d_StoL1)

    # Start generating focal size list as a function of CRL1 setting
    sigma1H         = (sigmaH**2 + (sigmaHp*d_StoL1)**2)**0.5   # sigma beam size before CRL1
    sigma1V         = (sigmaV**2 + (sigmaVp*d_StoL1)**2)**0.5   # sigma beam size before CRL1
    L1_n1mud_list   = np.zeros(L1_index_n)                      # List of n1*mu*d_min for all possible CRL1 configurations
    L1_n1muR_list   = np.zeros(L1_index_n)                      # List of n1*mu/R for all possible CRL1 configurations
    aperL1H_list    = np.zeros(L1_index_n)                      # absorption H aperture of CRL1 for all configurations
    aperL1V_list    = np.zeros(L1_index_n)                      # absorption V aperture of CRL1 for all configurations
    diameter1_list  = np.zeros(L1_index_n)                      # CRL1 diameter for all possible configurations
    FWHM1H_list     = np.zeros(L1_index_n)                      # H focal size at the focus of CRL1
    FWHM1V_list     = np.zeros(L1_index_n)                      # V focal size at the focus of CRL1
    Strehl_list     = np.zeros(L1_index_n)                      # Strehl ratio based on lens thickness error

    for i in range(L1_index_n):
        # absorption aperture is a function of CRL absorption/physical aperture, incident beam size, and physical slits
        L1_n1mud_list[i] = np.sum(index_to_binary_list(L1_invF_list_sort_indices[i], L1_Feq.size)*np.array(L1_mu*L1_n*d_min))
        L1_n1muR_list[i] = np.sum(index_to_binary_list(L1_invF_list_sort_indices[i], L1_Feq.size)*np.array(L1_mu*L1_n/L1_R))
        solution = root_scalar(absorptionaperture, args=(L1_n1mud_list[i], sigma1H, L1_n1muR_list[i]), bracket=[0.0, 2*sigma1H], method='bisect')
        aperL1H_list[i] = solution.root*2.0
        solution = root_scalar(absorptionaperture, args=(L1_n1mud_list[i], sigma1V, L1_n1muR_list[i]), bracket=[0.0, 2*sigma1V], method='bisect')
        aperL1V_list[i] = solution.root*2.0
        mask = (np.array(index_to_binary_list(L1_invF_list_sort_indices[i], L1_Feq.size)) == 1)
        if np.all(mask == False):
            diameter1_list[i] = np.inf
        else:
            diameter1_list[i] = np.min(L1_D[mask])
        aperL1H_list[i] = min(aperL1H_list[i], diameter1_list[i], slit1_H)
        aperL1V_list[i] = min(aperL1V_list[i], diameter1_list[i], slit1_V)
        phase_error_tmp = np.linalg.norm(index_to_binary_list(L1_invF_list_sort_indices[i], L1_Feq.size)*np.array(L1_HE*L1_delta)*2*np.pi/wl)
        Strehl_list[i] = np.exp(-phase_error_tmp**2)

    # FWHMbeam size at CRL1 focus
    FWHM1H_list  = ((0.88*wl*q1_list/aperL1H_list)**2 + (2.355*sigmaH*q1_list/d_StoL1)**2)**0.5
    FWHM1V_list  = ((0.88*wl*q1_list/aperL1V_list)**2 + (2.355*sigmaV*q1_list/d_StoL1)**2)**0.5
    if flag_HE:
        FWHM1H_list *= (Strehl_list)**(-0.5)
        FWHM1V_list *= (Strehl_list)**(-0.5)
    FWHM_list   = (FWHM1H_list*FWHM1V_list)**0.5

    indices, values = find_levels(FWHM_list, fsize, direction='backward')
    index = indices[0]
    if index == -1:
        print(f"Cannot achieve the focal size {fsize*1.0e6:.2f} μm")
    else:
        print("======== Find size at focus ========================================")
        print(f"Energy: {energy_keV} keV")
        print(f"CRL1 configuration index in sorted list is {index}")
        print(f"CRL1 configuration index is {L1_invF_list_sort_indices[index]} or {index_to_binary_list(L1_invF_list_sort_indices[index], L1_Feq.size)}")
        print(f"CRL1 f is {1/L1_invF_list_sorted[index]:.2f} m, focus at q1 = {q1_list[index]:.2f} m")
        print(f"Focal size is {FWHM1H_list[index]*1.0e6:.2f} μm x {FWHM1V_list[index]*1.0e6:.2f} μm at the focal point ({dq1_list[index]*1e3:.1f} mm from sample)")

    FWHM1H_atsample_list = (FWHM1H_list**2 + (aperL1H_list*dq1_list/q1_list)**2)**0.5
    FWHM1V_atsample_list = (FWHM1V_list**2 + (aperL1V_list*dq1_list/q1_list)**2)**0.5
    FWHM_atsample_list   = (FWHM1H_atsample_list*FWHM1V_atsample_list)**0.5
    indices, values = find_levels(FWHM_atsample_list, fsize, direction='forward')
    index2 = indices[0]
    if index2 == -1:
        print(f"Cannot achieve the bame size {fsize*1.0e6:.2f} μm at sample")
    else:
        print("======== Find size at sample =======================================")
        print(f"CRL1 configuration index in sorted list is {index2}")
        print(f"CRL1 configuration index is {L1_invF_list_sort_indices[index2]} or {index_to_binary_list(L1_invF_list_sort_indices[index2], L1_Feq.size)}")
        print(f"CRL1 f is {1/L1_invF_list_sorted[index2]:.2f} m, focus at q1 = {q1_list[index2]:.2f} m ({dq1_list[index2]*1e3:.1f} mm from sample)")
        print(f"Beam size is {FWHM1H_atsample_list[index2]*1.0e6:.2f} μm x {FWHM1V_atsample_list[index2]*1.0e6:.2f} μm at the sample position)")

    indices, values = find_levels(dq1_list, 0.0, direction='backward')
    index3 = indices[0]
    if index == -1:
        print(f"Cannot find combination to focus close to sample")
    else:
        print("======== Find configuration focus close to the sample ==============")
        print(f"CRL1 configuration index in sorted list is {index3}")
        print(f"CRL1 configuration index is {L1_invF_list_sort_indices[index3]} or {index_to_binary_list(L1_invF_list_sort_indices[index3], L1_Feq.size)}")
        print(f"CRL1 f is {1/L1_invF_list_sorted[index3]:.2f} m, focus at q1 = {q1_list[index3]:.2f} m ({dq1_list[index3]*1e3:.1f} mm from sample)")
        print(f"Beam size is {FWHM1H_atsample_list[index3]*1.0e6:.2f} μm x {FWHM1V_atsample_list[index3]*1.0e6:.2f} μm at the sample position)")

    return


if __name__ == "__main__":

    flag_HE = True

    fsize  = 50.0e-6            # Desired focal size in m (area average of h and v size)
    #Single_CRL2D_control(fsize)  # Find the best configuration for a single transfocator system


'''
Update the following to accommodate XS code
'''

class singleTF():
	
	def __init__(self, beam = {}, beamline = {}, crl = {}, slits = {}):
		# Initialize beamline layout variables
		self.d_StoL = 51.9              # Source-to-CRL1 distance, in m
		self.d_Stof  = 66.2              # Source-to-focus distance, in m
			

		self.setSource(beam)
		
		self.setBeamline(beamline)
		
		self.setCRL(crl)
		
		self.setSlits(slits)
		# Initialize lens variables
		self.L1_n    = np.array([1,      1,      1,      1,      1,      1,      2,      4,      8,      16])                # CRL1 number of lenses in each stack
		self.L1_R    = 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
		self.L1_mater= np.array(["Be",   "Be",   "Be",   "Be",   "Be",   "Be",   "Be",   "Be",   "Be",   "Be"])              # CRL1 lens material in each stack
		self.L1_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
		self.L1_HE   = 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

		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}
		
		
		# Initialize pre-CRL slit size
		self.slit1_H = 500.0e-6          # H slit size before CRL 1
		self.slit1_V = 300.0e-6          # V slit size before CRL 1

		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
		'''
		
		#Default values	
		energy = 15				
		L_und = 4.7		        
		sigmaH_e = 14.8e-6      
		sigmaV_e = 3.7e-6       
		sigmaHp = 2.8e-6        
		sigmaVp_e = 1.5e-6      
		
		if 'energy' in beam_properties.keys(): 
			self.setEnergy(beam_properites['energy'])
		else:
			self.setEnergy(energy)
		if 'L_und' in beam_properties.keys(): 
			self.L_und = beam_properties['L_und']
		else:
			self.L_und = L_und
		if 'sigmaH_e' in beam_properties.keys(): 
			self.sigmaH_e = beam_properties['sigmaH_e']
		else:
			self.sigmaH_e = sigmaH_e
		if 'sigmaV_e' in beam_properties.keys(): 
			self.sigmaV_e = beam_properties['sigmaV_e']
		else:
			self.sigmaV_e = sigmaV_e
		if 'sigmaHp_e' in beam_properties.keys(): 
			self.sigmaHp_e = beam_properties['sigmaHp_e']
		else:
			self.sigmaHp_e = sigmaHp_e
		if 'sigmaVp_e' in beam_properties.keys(): 
			self.sigmaVp_e = beam_properties['sigmaVp_e']
		else:
			self.sigmaVp_e = sigmaVp_e
		
		self.sigmaH =  (self.sigmaH_e**2 +  self.wl*self.L_und/2/np.pi/np.pi)**0.5
		self.sigmaV =  (self.sigmaV_e**2 +  self.wl*self.L_und/2/np.pi/np.pi)**0.5
		self.sigmaHp = (self.sigmaHp_e**2 + self.wl/self.L_und/2)**0.5
		self.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
		'''
		
		# Default values	
		d_StoL = 51.9             
		d_Stof  = 66.2            
		
		if 'd_StoL' in beam_properties.keys(): 
			self.d_StoL = beam_properties['d_StoL']
		else:
			self.d_StoL = d_StoL
		if 'd_Stof' in beam_properties.keys(): 
			self.d_Stof = beam_properties['d_Stof']
		else:
			self.d_Stof = 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
		'''
		#Default values	
		stack_d = 50.0e-3
		d_min = 3.0e-5

		if 'd_min' in crl_properties.keys(): 
			self.d_min = crl_properties['d_min'] 
		else:  
			self.d_min = d_min
		if 'stack_d' in crl_properties.keys(): 
			self.stack_d = crl_properties['stack_d']
		else:
			self.stack_d = stack_d
	
	def setupSlits(self, slit_properties):
		'''
		Setting up properties of slits
		'''
		#Default values	

		pass
		
	def setLensCount(self, lensCount):
		self.numLens = 

    def setupLookupTable(self, subs_file, n_lenses, energy = 8.0):
        '''
        lookup table created after IOC startup (after filter 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_filters):
            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('Filter 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.culledTable()

    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 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 filter 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 filter 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 updateSlitSize(self, size, slit):
		'''
		Slit size updates are propagated to CRL object from EPICS.  The beam
		size lookup table is then recalculated.
		'''
		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
		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 calc_lenses(self):
#		self.lenses = (self.energy * self.focalPoint) % 4096
#		pydev.iointr('new_lens_config', self.lenses)

    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 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 = verbosity