Skip to content

Transfocator

transfocator

Attributes

logger module-attribute

logger = getLogger(__name__)

Classes

MFXTransfocator

MFXTransfocator(prefix, *, nominal_sample=400.37, **kwargs)

Bases: TransfocatorBase

Class to represent the MFX Transfocator

Source code in tfs/transfocator.py
def __init__(self, prefix, *, nominal_sample=400.37, **kwargs):
    self.nominal_sample = nominal_sample
    super().__init__(prefix, **kwargs)
Attributes
beam_energy class-attribute instance-attribute
beam_energy = Component(EpicsSignal, ':BEAM:ENERGY')
current_focus property
current_focus

The distance from the focus of the Transfocator to nominal_sample

Note

If no lenses are inserted this will retun NaN

interlock class-attribute instance-attribute
interlock = Component(TransfocatorInterlock, '')
lenses property
lenses

Component lenses

nominal_sample instance-attribute
nominal_sample = nominal_sample
prefocus_bot class-attribute instance-attribute
prefocus_bot = Component(MFXLens, ':DIA:01')
prefocus_mid class-attribute instance-attribute
prefocus_mid = Component(MFXLens, ':DIA:02')
prefocus_top class-attribute instance-attribute
prefocus_top = Component(MFXLens, ':DIA:03')
req_energy class-attribute instance-attribute
req_energy = Component(EpicsSignal, ':BEAM:REQ_ENERGY')
tfs_02 class-attribute instance-attribute
tfs_02 = Component(MFXLens, ':TFS:02')
tfs_03 class-attribute instance-attribute
tfs_03 = Component(MFXLens, ':TFS:03')
tfs_04 class-attribute instance-attribute
tfs_04 = Component(MFXLens, ':TFS:04')
tfs_05 class-attribute instance-attribute
tfs_05 = Component(MFXLens, ':TFS:05')
tfs_06 class-attribute instance-attribute
tfs_06 = Component(MFXLens, ':TFS:06')
tfs_07 class-attribute instance-attribute
tfs_07 = Component(MFXLens, ':TFS:07')
tfs_08 class-attribute instance-attribute
tfs_08 = Component(MFXLens, ':TFS:08')
tfs_09 class-attribute instance-attribute
tfs_09 = Component(MFXLens, ':TFS:09')
tfs_10 class-attribute instance-attribute
tfs_10 = Component(MFXLens, ':TFS:10')
tfs_lenses property
tfs_lenses

Transfocator lenses

tfs_radius class-attribute instance-attribute
tfs_radius = Component(EpicsSignalRO, ':BEAM:TFS_RADIUS', kind='normal', doc='TFS effective radius')
translation class-attribute instance-attribute
translation = FormattedComponent(IMS, 'MFX:TFS:MMS:21')
xrt_lenses property
xrt_lenses

Lenses in the XRT

xrt_radius class-attribute instance-attribute
xrt_radius = Component(EpicsSignalRO, ':BEAM:XRT_RADIUS', kind='normal', doc='XRT effective radius')
Functions
find_best_combo
find_best_combo(target=None, energy_eV=None, n=4, z_obj=0, show=True, exclusions=[], avoid_forbidden=False, enable_prefocus=True, **kwargs)

Calculate the best lens array to hit the nominal sample point

Parameters:

Name Type Description Default
target float

The target image of the lens array. By default this is nominal_sample i.e. 400.37

None
energy_eV int

Select the energy in eV. Default uses the beam energy given by acr which is usually wrong

None
n int

The maximum number of lenses in a valid combination. This saves time by avoiding calculating the focal plane of combinations with a large number of lenses, default n=4

4
z_obj float

The source point of the beam, default halfway through range at 150.0

0
show bool

Print a table of the of the calculated lens combination

True
exclusions list

Select your lenses to excluclde from tfs lenes #2-10 as list recommend [8,10]

[]
avoid_forbidden bool

Avoids forbidden TFS configurations. True by default

False
enable_prefocus bool

Allows the solver to use the prefocusing lenses in the XRT. Default is True since the prefocusing lenses are usually required to hit the target plane at MFX. Setting this to False will force the solver to find a solution using only the TFS lenses.

True
kwargs

Passed to :meth:.Calculator.find_solution

{}
Source code in tfs/transfocator.py
def find_best_combo(
        self,
        target=None,
        energy_eV=None,
        n=4,
        z_obj=0,
        show=True,
        exclusions=[],
        avoid_forbidden=False,
        enable_prefocus=True,
        **kwargs):
    """
    Calculate the best lens array to hit the nominal sample point

    Parameters
    ----------
    target : float, optional
        The target image of the lens array. By default this is
        `nominal_sample i.e. 400.37`

    energy_eV : int, optional
        Select the energy in eV.
        Default uses the beam energy given by acr which is usually wrong

    n : int, optional
        The maximum number of lenses in a valid combination. This saves
        time by avoiding calculating the focal plane of combinations with a
        large number of lenses, default n=4

    z_obj : float, optional
        The source point of the beam, default halfway through range at 150.0

    show : bool, optional
        Print a table of the of the calculated lens combination

    exclusions : list, optional
        Select your lenses to excluclde from tfs lenes #2-10 as list recommend [8,10]

    avoid_forbidden : bool, optional
        Avoids forbidden TFS configurations. True by default

    enable_prefocus : bool, optional
        Allows the solver to use the prefocusing lenses in the XRT. Default is True since
        the prefocusing lenses are usually required to hit the target plane at MFX.
        Setting this to False will force the solver to find a solution using only
        the TFS lenses.

    kwargs:
        Passed to :meth:`.Calculator.find_solution`
    """
    if 'energy' in kwargs:
        raise ValueError('energy is no longer the correct input variable. Please use energy_eV. Thank Fred.')

    energy = energy_eV or self.beam_energy.get()
    try:
        assert energy > 1000
    except AssertionError:
        logging.warning(f"please double-check that {energy} is in eV, not keV.")
    target = target or self.nominal_sample
    exclusions = [x - 1 for x in exclusions]
    calc = TFS_Calculator(tfs_lenses=self.tfs_lenses, prefocus_lenses=self.xrt_lenses, exclusions=exclusions)
    combo, diff = calc.find_solution(target, energy, n, z_obj, avoid_forbidden=avoid_forbidden,enable_prefocus=enable_prefocus, **kwargs)
    if combo:
        print(combo)
        combo.show_info()
        logger.info(f'Difference to desired focus position: {round(diff*1000, 2)} mm')
        radius = combo.tfs_radius
        logger.info(f'Given Energy: {energy} eV')
        logger.info(f'Given Sample Position: {target} mm')
        logger.info(f'Calculated Radius: {round(radius, 2)} um')
        estimate_beam_fwhm(radius=radius, energy=energy)
        focal = focal_length(radius=radius, energy=energy)
        if avoid_forbidden:
            logger.info("TFS combo is by default allowed")
        else:
            logger.error("TFS combo may be forbidden. Please check")

        logger.info(f'Calculated Focal Length: {focal} m\n')

    else:
        logger.error("Unable to find a valid solution for target")
    return combo
focus_at
focus_at(value=None, wait=False, timeout=None, **kwargs)

Calculate a combination and insert the lenses

Parameters:

Name Type Description Default
value

Chosen focal plane. Nominal sample by default

None
wait bool

Wait for the motion of the transfocator to complete

False
timeout

Timeout for motion

None
kwargs

All passed to :meth:.find_best_combo

{}

Returns:

Type Description
StateStatus

Status that represents whether the move is complete

Source code in tfs/transfocator.py
def focus_at(self, value=None, wait=False, timeout=None, **kwargs):
    """
    Calculate a combination and insert the lenses

    Parameters
    ----------
    value: float, optional
        Chosen focal plane. Nominal sample by default

    wait : bool, optional
        Wait for the motion of the transfocator to complete

    timeout: float, optional
        Timeout for motion

    kwargs:
        All passed to :meth:`.find_best_combo`

    Returns
    -------
    StateStatus
        Status that represents whether the move is complete
    """
    # Find the best combination of lenses to match the target image
    plane = value or self.nominal_sample
    best_combo = self.find_best_combo(target=plane, **kwargs)
    # Collect status to combine
    statuses = list()
    # Only tell one XRT lens to insert
    prefocused = False
    for lens in self.xrt_lenses:
        if lens in best_combo.lenses:
            statuses.append(lens.insert(timeout=timeout))
            prefocused = True
            break
    # If we have no XRT lenses one remove will do
    if not prefocused:
        statuses.append(self.xrt_lenses[0].remove(timeout=timeout))
    # Ensure all Transfocator lenses are correct
    for lens in self.tfs_lenses:
        if lens in best_combo.lenses:
            statuses.append(lens.insert(timeout=timeout))
        else:
            statuses.append(lens.remove(timeout=timeout))
    # Conglomerate all status objects
    status = statuses.pop(0)
    for st in statuses:
        status = status & st
    # Wait if necessary
    if wait:
        status_wait(status, timeout=timeout)
    return status
get_stage_limits
get_stage_limits(margin_mm)
Source code in tfs/transfocator.py
def get_stage_limits(self, margin_mm):
    stage = self.translation
    z_high_mm = stage.high_limit
    z_low_mm = stage.low_limit
    z_max_mm = z_high_mm - margin_mm
    z_min_mm = z_low_mm + margin_mm
    print(f"Stage limits: low={z_low_mm:.3f} mm, high={z_high_mm:.3f} mm, margin={margin_mm:.3f} mm")
    return z_min_mm, z_max_mm
get_z_stage_target
get_z_stage_target(energy_eV, combo, ref_focal_length_um, ref_z_stage_mm)
Source code in tfs/transfocator.py
def get_z_stage_target(self, energy_eV, combo, ref_focal_length_um, ref_z_stage_mm):
    focal_length_um = focal_length(combo.tfs_radius, energy=energy_eV)
    z_stage_target_mm = ref_z_stage_mm - (focal_length_um - ref_focal_length_um) * 1000
    print(f"Energy {energy_eV:.2f} eV: computed focal length = {focal_length_um:.3f} um, target z = {z_stage_target_mm:.3f} mm.")
    return z_stage_target_mm
mv_stage_to_pos
mv_stage_to_pos(z_mm)
Source code in tfs/transfocator.py
def mv_stage_to_pos(self, z_mm):
    stage = self.translation
    print(f"Moving stage to position: z={z_mm:.3f}mm")
    stage.mv(z_mm)
    print(f"Stage moved to position: z={z_mm:.3f}mm")
    return z_mm
mv_stage_to_target_pos
mv_stage_to_target_pos(energy_eV, combo, target_z_mm, track_record)
Source code in tfs/transfocator.py
def mv_stage_to_target_pos(self, energy_eV, combo, target_z_mm, track_record):
    stage = self.translation
    print(f"Moving stage to {target_z_mm:.3f} mm.")
    stage.mv(target_z_mm)
    track_record.append({
        "energy": energy_eV,
        "inserted_lenses": [lens.prefix for lens in combo.lenses],
        "z_position": target_z_mm
    })
plan_energy_schedule
plan_energy_schedule(low_eV, high_eV, step_eV=10.0, *, target=None, n=4, z_obj=0.0, show=False)

Plan a schedule of lens insert/remove actions and stage offsets over an energy interval without moving hardware.

Parameters:

Name Type Description Default
low_eV float

Starting energy in eV.

required
high_eV float

Ending energy in eV.

required
step_eV float

Energy increment in eV (default 10 eV).

10.0
target float

Desired focal plane (defaults to nominal sample).

None
n int

Max number of TFS lenses in combo (passed to solver).

4
z_obj float

Source point in solver.

0.0
show bool

If True, print combo info for each energy.

False

Returns:

Type Description
list of dict

For each energy step, returns an entry with keys: - 'energy_eV': energy value in eV - 'lenses': list of lens prefixes in the planned combo - 'actions': {'insert': [...], 'remove': [...]} compared to previous step - 'image_target_delta': signed difference (image - target) - 'stage_offset_mm': signed offset in mm to place focus at target

Source code in tfs/transfocator.py
def plan_energy_schedule(self, low_eV, high_eV, step_eV=10.0, *, target=None,
                         n=4, z_obj=0.0, show=False):
    """
    Plan a schedule of lens insert/remove actions and stage offsets
    over an energy interval without moving hardware.

    Parameters
    ----------
    low_eV : float
        Starting energy in eV.
    high_eV : float
        Ending energy in eV.
    step_eV : float, optional
        Energy increment in eV (default 10 eV).
    target : float, optional
        Desired focal plane (defaults to nominal sample).
    n : int, optional
        Max number of TFS lenses in combo (passed to solver).
    z_obj : float, optional
        Source point in solver.
    show : bool, optional
        If True, print combo info for each energy.

    Returns
    -------
    list of dict
        For each energy step, returns an entry with keys:
          - 'energy_eV': energy value in eV
          - 'lenses': list of lens prefixes in the planned combo
          - 'actions': {'insert': [...], 'remove': [...]} compared to previous step
          - 'image_target_delta': signed difference (image - target)
          - 'stage_offset_mm': signed offset in mm to place focus at target
    """
    if step_eV is None or step_eV == 0:
        raise ValueError("step_eV must be non-zero")

    tgt = target or self.nominal_sample
    # Energy sequence inclusive of high_eV
    num_steps = int((high_eV - low_eV) // step_eV)
    energies = [low_eV + i * step_eV for i in range(num_steps + 1)]
    if energies[-1] < high_eV:
        energies.append(high_eV)

    calc = TFS_Calculator(tfs_lenses=self.tfs_lenses, prefocus_lenses=self.xrt_lenses)
    schedule = []
    prev_lenses = set()

    for energy in energies:
        combo, _diff_abs, pre_focus_lens = calc.find_solution(tgt, energy, n=n, z_obj=z_obj)
        if combo is None:
            schedule.append({
                'energy_eV': energy,
                'lenses': [],
                'actions': {'insert': [], 'remove': []},
                'image_target_delta': None,
                'stage_offset_mm': None,
            })
            continue

        if show:
            combo.show_info()

        # Determine planned lenses as prefixes for readability
        lens_list = [lens.prefix for lens in combo.lenses]
        lens_set = set(lens_list)

        # Signed difference between image and target
        image_pos = combo.image(z_obj, energy)
        delta = image_pos - tgt
        stage_offset_mm = delta * 1000.0 # convert from meters to mm

        # Actions relative to previous step
        to_insert = sorted(list(lens_set - prev_lenses))
        to_remove = sorted(list(prev_lenses - lens_set))

        schedule.append({
            'energy_eV': energy,
            'lenses': lens_list,
            'actions': {'insert': to_insert, 'remove': to_remove},
            'image_target_delta': delta,
            'stage_offset_mm': stage_offset_mm,
        })

        prev_lenses = lens_set

    return schedule
plot_focus_track
plot_focus_track(json_file_path)
Source code in tfs/transfocator.py
def plot_focus_track(self, json_file_path):
    # Load the data from the JSON file
    with open(json_file_path, 'r') as f:
        data = json.load(f)

    # Extract energy, z_position, and inserted_lenses values
    energies = [entry['energy'] for entry in data]
    z_positions = [entry['z_position'] for entry in data]
    inserted_lenses = [entry['inserted_lenses'] for entry in data]

    # Create the plot
    plt.figure(figsize=(12, 8))
    plt.plot(energies, z_positions, marker='o', linestyle='-', color='b')

    # Add labels and title
    plt.title('Z Position vs Energy')
    plt.xlabel('Energy (eV)')
    plt.ylabel('Z Position (units)')

    # Variable to keep track of the last inserted lenses shown
    last_displayed_lenses = None

    # Annotate only the first occurrence of each unique set of inserted_lenses
    for energy, z_position, lenses in zip(energies, z_positions, inserted_lenses):
        # Convert list of lenses to a tuple for easier comparison
        lenses_tuple = tuple(lenses)

        if lenses_tuple != last_displayed_lenses:
            plt.annotate(', '.join(lenses),
                        (energy, z_position),
                        textcoords="offset points",
                        xytext=(0, 10),
                        ha='center',
                        fontsize=8,
                        color='red',
                        arrowprops=dict(arrowstyle='->', color='red', lw=0.5))
            last_displayed_lenses = lenses_tuple  # Update the last_displayed_lenses
    plt.grid(True)
    plt.show()
remove_all
remove_all()

Removes all tfs lenses.

Source code in tfs/transfocator.py
def remove_all(self):
    """
    Removes all tfs lenses.
    """
    self.tfs_02.remove()
    self.tfs_03.remove()
    self.tfs_04.remove()
    self.tfs_05.remove()
    self.tfs_06.remove()
    self.tfs_07.remove()
    self.tfs_08.remove()
    self.tfs_09.remove()
    self.tfs_10.remove()
set
set(value, **kwargs)

Set the Transfocator focus

Parameters

Source code in tfs/transfocator.py
def set(self, value, **kwargs):
    """
    Set the Transfocator focus

    Parameters
    """
    return self.focus_at(value=value, **kwargs)
set_reference_combo
set_reference_combo(energy_eV, show=False, **kwargs)

kwargs: Passed to :meth:.Calculator.find_solution

Source code in tfs/transfocator.py
def set_reference_combo(self, energy_eV, show=False, **kwargs):
    """
    kwargs:
        Passed to :meth:`.Calculator.find_solution`
    """
    combo = self.find_best_combo(energy_eV=energy_eV, show=show,  **kwargs)
    ref_focal_length_um = focal_length(combo.tfs_radius, energy=energy_eV)
    print(f"Reference energy: {energy_eV:.2f} eV, reference focal length: {ref_focal_length_um:.3f} um")
    return combo, ref_focal_length_um
track_focus
track_focus(energies, *, margin_mm=10.0, show=False, ref_focal_length_um=None, ref_z_stage_mm=None, display=True, shrinking_rate=4, enable_prefocus=True, lens_beam_energy_offset=0.0, **kwargs)

Keep the focal length fixed over a provided list of energies by compensating with the translation stage. Lenses are NOT actuated.

Workflow: - Move stage to high limit minus a small margin. - Compute initial lens combo and reference focal length. - For each next energy, compute focal length for the current combo and move the stage to compensate. - If the move would exceed the stage low limit, return to the top position and recompute the lens combo at that energy, then continue.

kwargs: Passed to :meth:.Calculator.find_solution

Source code in tfs/transfocator.py
def track_focus(self, energies, *, margin_mm=10.0, show=False,
                ref_focal_length_um=None, ref_z_stage_mm=None,
                display=True, shrinking_rate=4, enable_prefocus=True,
                lens_beam_energy_offset=0.0, **kwargs):
    """
    Keep the focal length fixed over a provided list of energies by
    compensating with the translation stage. Lenses are NOT actuated.

    Workflow:
    - Move stage to high limit minus a small margin.
    - Compute initial lens combo and reference focal length.
    - For each next energy, compute focal length for the current combo and
      move the stage to compensate.
    - If the move would exceed the stage low limit, return to the top
      position and recompute the lens combo at that energy, then continue.

    kwargs:
        Passed to :meth:`.Calculator.find_solution`
    """
    if len(energies) == 0:
        print("No energies provided.")
        return None
    # cast energies to float to avoid json serialization issues
    energies = [float(energy) for energy in energies]

    enable_prefocus_save = enable_prefocus

    min_z_stage_mm, max_z_stage_mm = self.get_stage_limits(margin_mm)

    ref_zs_mm = self.mv_stage_to_pos(max_z_stage_mm)
    combo, ref_fl_um = self.set_reference_combo(energies[0], show=show, **kwargs)
    if ref_focal_length_um is None:
        ref_focal_length_um = ref_fl_um
    if ref_z_stage_mm is None:
        ref_z_stage_mm = ref_zs_mm

    track_record = []

    for energy in energies:
        enable_prefocus = enable_prefocus_save
        target_z_stage_mm = self.get_z_stage_target(
            energy, combo, ref_focal_length_um, ref_z_stage_mm
        )
        if min_z_stage_mm < target_z_stage_mm <= max_z_stage_mm:
            self.mv_stage_to_target_pos(
                energy, combo, target_z_stage_mm, track_record
            )
        else:
            found_combo = False
            prefocus_fallback = True
            while prefocus_fallback:
                shrinking_max_z_stage_mm = max_z_stage_mm
                while shrinking_max_z_stage_mm > min_z_stage_mm:
                    self.mv_stage_to_pos(shrinking_max_z_stage_mm)
                    combo = self.find_best_combo(
                        energy_eV=energy, show=show, enable_prefocus=enable_prefocus, **kwargs
                    )
                    if combo:
                        new_target_z_stage_mm = self.get_z_stage_target(
                            energy, combo, ref_focal_length_um, ref_z_stage_mm
                        )
                        if min_z_stage_mm < new_target_z_stage_mm <= max_z_stage_mm:
                            self.mv_stage_to_target_pos(
                                energy, combo, new_target_z_stage_mm, track_record
                            )
                            found_combo = True
                            # check compatibility with lens_beam_energy
                            if 'DIA' in combo.lenses[0].prefix:
                                prefocus_lens_radius = combo.lenses[0].radius
                                lens_beam_energy = energy + lens_beam_energy_offset
                                #str(os.popen("caget MFX:LENS:BEAM:ENERGY | awk '{print $2}'").read().strip())
                                radius = combo.tfs_radius
                                from tfs.offline_calculator import TFS_Calculator as TFSCalc
                                calc = TFSCalc(combo.lenses)
                                forbidden = calc.check_forbidden(prefocus_lens_radius,
                                                     lens_beam_energy,
                                                     radius)
                                if forbidden:
                                    found_combo = False
                            if found_combo:
                                break
                    shrinking_max_z_stage_mm -= shrinking_rate*margin_mm
                if found_combo:
                    break
                else:
                    print("Stage out of travel. Cannot compensate further...")
                    if enable_prefocus:
                        print("Disabling prefocus for this energy.")
                        enable_prefocus = False
                    else:
                        prefocus_fallback = False

    print(f"Tracking complete. Final energy: {track_record[-1]['energy']:.2f} eV, stage position: {track_record[-1]['z_position']:.3f} mm.")
    print(f"Lenses currently inserted: {track_record[-1]['inserted_lenses']}")

    save_path = Path.home() / "track_focus_results.json"
    with open(save_path, "w") as f:
        json.dump(track_record, f, indent=4)
    print(f"Tracking results saved to {save_path}")
    if display:
        self.plot_focus_track(save_path)
    return track_record
try_combo
try_combo(target=400.37, energy=None, show=True, prefocus=None, tfs=[], **kwargs)

Calculates the focus based on the lens combo you select

Parameters:

Name Type Description Default
target float

The target image of the lens array. By default this is nominal_sample i.e. 399.88

400.37
energy int

Select the energy in eV. Default uses the beam energy given by acr which is usually wrong

None
show bool

Print a table of the of the calculated lens combination

True
prefocus int

Select either 333, 428, or 750 um radius lens

None
tfs list

Select your lens combination from tfs lenes #2-10 as list i.e. [2,6,8,10]

[]
kwargs

Passed to :meth:.Calculator.find_solution

{}
Source code in tfs/transfocator.py
def try_combo(
        self,
        target=400.37,
        energy=None,
        show=True,
        prefocus = None,
        tfs = [],
        **kwargs):
    """
    Calculates the focus based on the lens combo you select

    Parameters
    ----------
    target : float, optional
        The target image of the lens array. By default this is
        `nominal_sample i.e. 399.88`

    energy : int, optional
        Select the energy in eV.
        Default uses the beam energy given by acr which is usually wrong

    show : bool, optional
        Print a table of the of the calculated lens combination

    prefocus : int, optional
        Select either 333, 428, or 750 um radius lens

    tfs : list, optional
        Select your lens combination from tfs lenes #2-10 as list i.e. [2,6,8,10]

    kwargs:
        Passed to :meth:`.Calculator.find_solution`
    """
    energy = energy or self.beam_energy.get()
    target = target or self.nominal_sample

    for e_range, lens in MFX_prefocus_energy_range.items():
        if energy >= e_range[0] and energy < e_range[1]:
            prefocus_rec = lens[1]

    if prefocus_rec != prefocus:
        logging.error(
            f'{prefocus_rec} um prefocusing lens is reccommended for {energy} eV '
            f'You are not using the recommended prefocusing lens.')

    if prefocus == 750:
        prefocus_idx = 2
    elif prefocus == 428:
        prefocus_idx = 1
    elif prefocus == 333:
        prefocus_idx = 0
    elif prefocus is None:
        prefocus_idx = None
    else:
        logging.error(
            'No proper prefocusing lens selected. '
            'Select either 333, 428, or 750 um radius lens (as int)')

    if prefocus_idx is None:
        tfs_combo = []
    else:
        tfs_combo = [self.xrt_lenses[prefocus_idx]]

    for lens in tfs:
        tfs_combo.append(self.tfs_lenses[int(lens) - 2])

    combo = LensConnect(*tfs_combo)

    if combo:
        combo.show_info()
        radius = combo.tfs_radius
        logger.info(f'Given Energy: {energy} eV')
        logger.info(f'Given Sample Position: {target} mm')
        logger.info(f'Calculated Radius: {round(radius, 2)} um')
        estimate_beam_fwhm(radius=radius, energy=energy)
        focal = focal_length(radius=radius, energy=energy)
        calc = TFS_Calculator(tfs_lenses=self.tfs_lenses, prefocus_lenses=self.xrt_lenses)
        if prefocus_idx is not None:
            forbidden = calc.check_forbidden(prefocus_idx, energy, radius)
            log_level = logger.error if forbidden else logger.info
            log_level(f"TFS Configuration is {'Forbidden' if forbidden else 'Allowed'}")

        logger.info(f'Calculated Focal Length: {focal} um\n')

    else:
        logger.error("Unable to find a valid solution for target")
    return combo

Transfocator

Transfocator(prefix, *, nominal_sample=400.37, **kwargs)

Bases: MFXTransfocator

Source code in tfs/transfocator.py
def __init__(self, prefix, *, nominal_sample=400.37, **kwargs):
    self.nominal_sample = nominal_sample
    super().__init__(prefix, **kwargs)

TransfocatorBase

TransfocatorBase(prefix, *args, **kwargs)

Bases: Device

Source code in tfs/transfocator.py
def __init__(self, prefix, *args, **kwargs):
    super().__init__(prefix, **kwargs)
    return
Functions

TransfocatorEnergyInterrupt

Bases: Exception

Custom exception returned when input beam energy (user defined or current measured value) changes significantly during calculation

TransfocatorInterlock

Bases: Device

Device containing signals pertinent to the interlock system.

Attributes
bypass class-attribute instance-attribute
bypass = Component(EpicsSignal, ':BYPASS:STATUS', write_pv=':BYPASS:SET', doc='Bypass in use?')
bypass_energy class-attribute instance-attribute
bypass_energy = Component(EpicsSignal, ':BYPASS:ENERGY', doc='Bypass energy')
faulted class-attribute instance-attribute
faulted = Component(EpicsSignalRO, ':BEAM:FAULTED', doc='Fault currently active [active]')
ioc_alive class-attribute instance-attribute
ioc_alive = Component(EpicsSignalRO, ':BEAM:ALIVE', doc='IOC alive [active]')
lens_required_fault class-attribute instance-attribute
lens_required_fault = Component(EpicsSignalRO, ':BEAM:REQ_TFS_FAULT', doc='Transfocator lens required for energy/lens combination [active]')
lens_required_fault_latch class-attribute instance-attribute
lens_required_fault_latch = Component(EpicsSignalRO, ':BEAM:REQ_TFS_FAULT_LT', doc='Transfocator lens required for energy/lens combination [latched]')
limits class-attribute instance-attribute
limits = Component(LensTripLimits, ':ACTIVE', doc='Active trip limit settings, based on pre-focus lens')
min_fault class-attribute instance-attribute
min_fault = Component(EpicsSignalRO, ':BEAM:MIN_FAULT', doc='Minimum required energy not met for lens combination [active]')
min_fault_latch class-attribute instance-attribute
min_fault_latch = Component(EpicsSignalRO, ':BEAM:MIN_FAULT_LT', doc='Minimum required energy not met for lens combination [latched]')
state_fault class-attribute instance-attribute
state_fault = Component(EpicsSignalRO, ':BEAM:UNKNOWN', doc='Lens position unknown [active]')
table_fault class-attribute instance-attribute
table_fault = Component(EpicsSignalRO, ':BEAM:TAB_FAULT', doc='Effective radius in table-based disallowed area [active]')
table_fault_latch class-attribute instance-attribute
table_fault_latch = Component(EpicsSignalRO, ':BEAM:TAB_FAULT_LT', doc='Effective radius in table-based disallowed area [latched]')
violated_fault class-attribute instance-attribute
violated_fault = Component(EpicsSignalRO, ':BEAM:VIOLATED', doc='Summary fault due to energy/lens combination [active]')
violated_fault_latch class-attribute instance-attribute
violated_fault_latch = Component(EpicsSignalRO, ':BEAM:VIOLATED_LT', doc='Summary fault due to energy/lens combination [latched]')

Functions

constant_energy

constant_energy(func)

Ensures that requested energy does not change during calculation

Parameters: transfocator_obj: transfocate.transfocator.Transfocator object

energy_type: string input string specifying 'req_energy' or 'beam_energy' to be monitored during calculation

tolerance: float energy (in eV) for which current beam energy can change during calculation and still assumed constant

Source code in tfs/transfocator.py
def constant_energy(func):
    """
    Ensures that requested energy does not change during calculation

    Parameters:
    transfocator_obj: transfocate.transfocator.Transfocator object

    energy_type: string
        input string specifying 'req_energy' or 'beam_energy'
        to be monitored during calculation

    tolerance: float
        energy (in eV) for which current beam energy can change during
        calculation and still assumed constant
    """
    @wraps(func)
    def with_constant_energy(transfocator_obj, energy_type, tolerance, *args, **kwargs):
        try:
            energy_signal = getattr(transfocator_obj, energy_type)
        except Exception as e:
            raise AttributeError("input 'energy_type' not defined") from e
        energy_before = energy_signal.get()
        result = func(transfocator_obj, *args, **kwargs)
        energy_after = energy_signal.get()
        if not math.isclose(energy_before, energy_after, abs_tol=tolerance):
            raise TransfocatorEnergyInterrupt("The beam energy changed significantly during the calculation")
        return result
    return with_constant_energy