Skip to content

Transfocator

MFXTransfocator

Bases: TransfocatorBase

Class to represent the MFX Transfocator

Source code in tfs/transfocator.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
class MFXTransfocator(TransfocatorBase):
    """
    Class to represent the MFX Transfocator
    """
    interlock = Cpt(TransfocatorInterlock, '')

    # XRT Lenses
    prefocus_top = Cpt(Lens, ":DIA:03")
    prefocus_mid = Cpt(Lens, ":DIA:02")
    prefocus_bot = Cpt(Lens, ":DIA:01")
    xrt_radius = Cpt(EpicsSignalRO, ":BEAM:XRT_RADIUS", kind="normal",
                     doc="XRT effective radius")
    tfs_radius = Cpt(EpicsSignalRO, ":BEAM:TFS_RADIUS", kind="normal",
                     doc="TFS effective radius")

    # TFS Lenses
    tfs_02 = Cpt(Lens, ":TFS:02")
    tfs_03 = Cpt(Lens, ":TFS:03")
    tfs_04 = Cpt(Lens, ":TFS:04")
    tfs_05 = Cpt(Lens, ":TFS:05")
    tfs_06 = Cpt(Lens, ":TFS:06")
    tfs_07 = Cpt(Lens, ":TFS:07")
    tfs_08 = Cpt(Lens, ":TFS:08")
    tfs_09 = Cpt(Lens, ":TFS:09")
    tfs_10 = Cpt(Lens, ":TFS:10")

    # Requested energy
    req_energy = Cpt(EpicsSignal, ":BEAM:REQ_ENERGY")

    # Actual beam energy
    beam_energy = Cpt(EpicsSignal, ":BEAM:ENERGY")

    # Translation
    translation = FormattedComponent(IMS, "MFX:TFS:MMS:21")

    def __init__(self, prefix, *, nominal_sample=399.88103, **kwargs):
        self.nominal_sample = nominal_sample
        super().__init__(prefix, **kwargs)

    @property
    def lenses(self):
        """
        Component lenses
        """
        return [getattr(self, dev) for dev in self._sub_devices
                if isinstance(getattr(self, dev), Lens)]

    @property
    def xrt_lenses(self):
        """
        Lenses in the XRT
        """
        return [lens for lens in self.lenses if 'DIA' in lens.prefix]

    @property
    def tfs_lenses(self):
        """
        Transfocator lenses
        """
        return [lens for lens in self.lenses if 'TFS' in lens.prefix]

    @property
    def current_focus(self):
        """
        The distance from the focus of the Transfocator to nominal_sample

        Note
        ----
        If no lenses are inserted this will retun NaN
        """
        # Find inserted lenses
        inserted = [lens for lens in self.lenses if lens.inserted]
        # Check that we have any inserted lenses at all
        if not inserted:
            logger.warning("No lenses are currently inserted")
            return math.nan
        # Calculate the image from this set of lenses
        return LensConnect(*inserted).image(0.0) - self.nominal_sample
    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()

    def find_best_combo(self, target=None, energy=None, show=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`

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

        kwargs:
            Passed to :meth:`.Calculator.find_solution`
        """
        energy = energy or self.beam_energy.get()
        target = target or self.nominal_sample
        calc = TFS_Calculator(tfs_lenses=self.tfs_lenses, prefocus_lenses=self.xrt_lenses)
        combo, diff = calc.find_solution(target, energy, **kwargs)
        if combo:
            combo.show_info()
            logger.info(f'Difference to desired focus position: {round(diff*1000, 2)} mm')
            radius = combo.tfs_radius
            logger.info(f'Calculated Radius: {round(radius, 2)} um')
            estimate_beam_fwhm(radius=radius, energy=energy)
            focal = focal_length(radius=radius, energy=energy)

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

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

    def set(self, value, **kwargs):
        """
        Set the Transfocator focus

        Parameters
        """
        return self.focus_at(value=value, **kwargs)

    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

current_focus property

The distance from the focus of the Transfocator to nominal_sample

Note

If no lenses are inserted this will retun NaN

lenses property

Component lenses

tfs_lenses property

Transfocator lenses

xrt_lenses property

Lenses in the XRT

find_best_combo(target=None, energy=None, show=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

bool, optional

Print a table of the of the calculated lens combination

kwargs

Passed to :meth:.Calculator.find_solution

Source code in tfs/transfocator.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def find_best_combo(self, target=None, energy=None, show=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`

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

    kwargs:
        Passed to :meth:`.Calculator.find_solution`
    """
    energy = energy or self.beam_energy.get()
    target = target or self.nominal_sample
    calc = TFS_Calculator(tfs_lenses=self.tfs_lenses, prefocus_lenses=self.xrt_lenses)
    combo, diff = calc.find_solution(target, energy, **kwargs)
    if combo:
        combo.show_info()
        logger.info(f'Difference to desired focus position: {round(diff*1000, 2)} mm')
        radius = combo.tfs_radius
        logger.info(f'Calculated Radius: {round(radius, 2)} um')
        estimate_beam_fwhm(radius=radius, energy=energy)
        focal = focal_length(radius=radius, energy=energy)

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

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

focus_at(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

bool, optional

Wait for the motion of the transfocator to complete

float, optional

Timeout for motion

kwargs

All passed to :meth:.find_best_combo

Returns

StateStatus Status that represents whether the move is complete

Source code in tfs/transfocator.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
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

remove_all()

Removes all tfs lenses.

Source code in tfs/transfocator.py
173
174
175
176
177
178
179
180
181
182
183
184
185
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(value, **kwargs)

Set the Transfocator focus

Parameters

Source code in tfs/transfocator.py
221
222
223
224
225
226
227
def set(self, value, **kwargs):
    """
    Set the Transfocator focus

    Parameters
    """
    return self.focus_at(value=value, **kwargs)

TransfocatorEnergyInterrupt

Bases: Exception

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

Source code in tfs/transfocator.py
287
288
289
290
291
292
293
class TransfocatorEnergyInterrupt(Exception):
    """
    Custom exception returned when input beam energy (user defined
    or current measured value) changes significantly during
    calculation
    """
    pass

TransfocatorInterlock

Bases: Device

Device containing signals pertinent to the interlock system.

Source code in tfs/transfocator.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class TransfocatorInterlock(Device):
    """
    Device containing signals pertinent to the interlock system.
    """
    limits = Cpt(
        LensTripLimits, ":ACTIVE",
        doc="Active trip limit settings, based on pre-focus lens"
    )

    # Active limits, predicated on pre-focus lens insertion:
    # no_lens_limit = Cpt(LensTripLimits, ":NO_LENS")
    # lens1_limit = Cpt(LensTripLimits, ":LENS1")
    # lens2_limit = Cpt(LensTripLimits, ":LENS2")
    # lens3_limit = Cpt(LensTripLimits, ":LENS3")

    bypass = Cpt(
        EpicsSignal, ":BYPASS:STATUS", write_pv=":BYPASS:SET",
        doc="Bypass in use?",
    )
    bypass_energy = Cpt(
        EpicsSignal, ":BYPASS:ENERGY",
        doc="Bypass energy",
    )
    ioc_alive = Cpt(
        EpicsSignalRO, ":BEAM:ALIVE",  # string=True,
        doc="IOC alive [active]"
    )
    faulted = Cpt(
        EpicsSignalRO, ":BEAM:FAULTED",  # string=True,
        doc="Fault currently active [active]"
    )
    state_fault = Cpt(
        EpicsSignalRO, ":BEAM:UNKNOWN",  # string=True,
        doc="Lens position unknown [active]"
    )

    violated_fault = Cpt(
        EpicsSignalRO, ":BEAM:VIOLATED",  # string=True,
        doc="Summary fault due to energy/lens combination [active]"
    )
    min_fault = Cpt(
        EpicsSignalRO, ":BEAM:MIN_FAULT",  # string=True,
        doc="Minimum required energy not met for lens combination [active]"
    )
    lens_required_fault = Cpt(
        EpicsSignalRO, ":BEAM:REQ_TFS_FAULT",  # string=True,
        doc="Transfocator lens required for energy/lens combination [active]"
    )
    table_fault = Cpt(
        EpicsSignalRO, ":BEAM:TAB_FAULT",  # string=True,
        doc="Effective radius in table-based disallowed area [active]"
    )

    violated_fault_latch = Cpt(
        EpicsSignalRO, ":BEAM:VIOLATED_LT",  # string=True,
        doc="Summary fault due to energy/lens combination [latched]"
    )
    min_fault_latch = Cpt(
        EpicsSignalRO, ":BEAM:MIN_FAULT_LT",  # string=True,
        doc="Minimum required energy not met for lens combination [latched]"
    )
    lens_required_fault_latch = Cpt(
        EpicsSignalRO, ":BEAM:REQ_TFS_FAULT_LT",  # string=True,
        doc="Transfocator lens required for energy/lens combination [latched]"
    )
    table_fault_latch = Cpt(
        EpicsSignalRO, ":BEAM:TAB_FAULT_LT",  # string=True,
        doc="Effective radius in table-based disallowed area [latched]"
    )

constant_energy(func)

Ensures that requested energy does not change during calculation

Parameters: transfocator_obj: transfocate.transfocator.Transfocator object

string

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

float

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

Source code in tfs/transfocator.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
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