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:
A list of devices.
A list of names/labels of devices.
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")