Energy Scans (XAFS)

xafs_scan() for Straight-Forward XAFS Scans

The xafs_scan() is a bluesky plan meant for scanning energy over a a number of energy ranges, for example the pre-edge, edge, and EXAFS signal of a K-edge.

The function accepts an arbitrary number of parameters for defining the ranges. The parameters are expected to provide energy step sizes (in eV) and exposure times (in sec) between the boundaries of the ranges. They should be passed following the pattern:

energy, step, exposure, energy, step, exposure, energy, ...

An example across the Nickel K-edge at 8333 eV could be:

RE(xafs_scan(8313, 2, 1, 8325, 0.5, 2, 8365, 10, 1.5, 8533))

RE is the bluesky RunEngine, which should already be imported for you in the ipython environment.

Absolute vs. Relative Scans

In many cases, it is more intuitive to describe the energy ranges relative to some absorption edge (E0). If this E0 energy is given directly to xafs_scan() via the E0 argument, then all energy points will be interpreted as relative to this energy. The same scan from above would be:

RE(xafs_scan(-20, 2, 1, -8, 0.5, 2, 32, 10, 1.5, 200, E0=8333))

Defining Scans in K-Space

For extended structure scans (EXAFS), it may be more helpful to define the EXAFS region in terms of the excited electron’s wavenumber (k-space). This can be done with the keyword arguments k_step, k_exposure, and k_max. Providing E0 is necessary, since otherwise wavenumbers will be calculated relative to 0 eV, and will not produce sensible results.

RE(xafs_scan(-20, 2, 1, -8, 0.5, 2, 32, k_step=0.02, k_max=12, k_exposure=1., E0=8333))

Better quality results can sometimes be achieved by setting longer exposure times at higher k. The k_weight parameter will scale the exposure time geometrically with k. k_weight=0 will produce constant exposure times, and if k_weight=1 then exposure will scale linearly with k.

RE(xafs_scan(-20, 2, 1, -8, 0.5, 2, 32, k_step=0.02, k_max=12, k_exposure=1., k_weight=1, E0=8333))

energy_scan() for More Sophisticated Scans

For extra flexibility, use the energy_scan() plan, which accepts a sequence of energies to scan. For example, to scan from 8325 to 8375 eV in 1 eV steps:

energies = range(8325, 8376, step=1)
RE(energy_scan(energies))

Notice the range ends at 8376 eV instead of 8375 eV, since the last value is not included when using a range.

The exposure time can also be given. exposure can either be a single number to be used for all energies, or a sequence of numbers with the same length as energies, and each energy will use the corresponding exposure:

import numpy as np
energies = range(8325, 8376, step=1)
exposures = np.linspace(0.5, 5, num=len(energies))
RE(energy_scan(energies), exposure=exposures)

Building a more complicated set of energies can be made simpler using the ERange helper class:

energies = ERange(8325, 8375, E_step=1).energies()
RE(energy_scan(energies))

To make things even easier, energy_scan() can accept energy range objects directly:

energies = [
    8300, 8320,  # Individual energies are okay too, you can mix and match
    ERange(8325, 8375, E_step=0.5),
    ERange(8375, 8533, E_step=5),
]
RE(energy_scan(energies))

Other than including the ending energy in the list, this usage does not provide considerable value. However, the inclusion of multiple energies with different exposure times makes the value more clear, since energy_scan will automatically replace an ERange instance with the result of the instance’s energies() method, and add equivalent entries into exposure based on the instance’s exposures() method.

energies = [
    ERange(8325, 8375, E_step=0.5, exposure=1.5),
    ERange(8375, 8533, E_step=5, exposure=0.5),
]
RE(energy_scan(energies))

There is also a similar KRange that works similarly except using electron wavenumbers (k) instead of X-ray energy. This allows the energies to be given in a more intuitive way for EXAFS:

energies = [
    ERange(-50, 50, E_step=0.5, exposure=1.5),
    ERange(50, 200, E_step=5, exposure=0.5),
    KRange(200, 14, k_step=0.05, , k_weight=1., exposure=1.),
]
RE(energy_scan(energies, E0=8333))

Notice that the energies are now given relative to the edge energy E0 (the nickel K-edge in this case). This is almost always necessary when using a KRange instance, since otherwise the corresponding energies would be relative to a free, zero-energy electron, instead of core electrons. E0 can also be given as a string, in this case E0="Ni_K".

At this point, we have largely replicated the behavior of xafs_scan() described above. In fact, xafs_scan() is a wrapper around energy_scan() whose main purpose is to take the parameters in the form of (energy, step, exposure, energy, ...), and convert them to ERange and KRange instances.

Changing Detectors or Positioners

For more sophisticated scans, it may be necessary to include additional detectors. By default, xafs_scan() and energy_scan() will measure the ion chambers as detectors (those returned by haven.registry.findall("ion_chambers")). Both plans accept the detectors argument, which can be any of the following:

  1. A list of devices.

  2. A list of names/labels of devices.

  3. A single name/label for devices.

Options 1 and 2 can be intermingled. For example:

eiger = haven.registry.find("eiger")
detectors = [eiger, "ion_chambers"]
plan = haven.xafs_scan(..., detectors=detectors)

Supplying the detectors argument will ensure that the detectors are captured in the data streams, but it may still be necessary to specify positioners for setting the exposure time. By default, only the ion chambers will receive have their exposure time set. This is especially important when using the k_weight parameter to xafs_scan() or the exposure parameter to energy_scan().

Both plans accept a time_positioners argument for this purpose, which should be a list of entries similar to those accepted for detectors described above but with positioners for the various detectors. Extending the above example:

eiger = haven.registry.find("eiger")
detectors = [eiger, "ion_chambers"]
time_positioners = [eiger.cam.acquire_time, "ion_chambers.exposure_time"]
plan = haven.xafs_scan(..., detectors=detectors, time_positioners=time_positioners)

The above example actually uses all of the ion chambers’ exposure times as separate positioners. This will work but produces extra messages and may be confusing. Since counting is handled by the scaler, any of the ion chambers on the same scaler can be used as a time positioner:

ion_chambers = haven.registry.findall("ion_chambers")
time_positioners = [eiger.cam.acquire_time, ion_chambers[0].exposure_time]
plan = haven.xafs_scan(..., time_positioners=time_positioners)

Lastly, we may want to specify a different energy position for example when using a secondary monocrhomator. By default the “energy” positioner is used, which is a pseudo-positioner that controls both the monochromator and the insertion device (if present). This positioner temporariy disables the EPICS-based pseudo-motor in use at sector 25-ID since the done status is not properly reported for the insertion device when using the EPICS implementation.

The energy_positioners argument accepts similar types as the previous options just discussed, and each one will be set to the energy in electron-volts at each point. For example, to scan only the monochromator energy we could do:

mono_energy = haven.registry.find("monochromator.energy")
plan = haven.energy_scan(..., energy_positioners=[mono_energy])

or equivalently:

plan = haven.energy_scan(..., energy_positioners="monochromator.energy")