Tools for Fluorescence Yield (FY) NEXAFS data reduction collected at Beamline 11.0.1.2 (RSOXS) at the Advanced Light Source (ALS).
pip install -e .For development dependencies (linting, type checking, tests):
pip install -e ".[dev,test]"A typical reduction workflow has three steps:
- Load an I0 reference scan from an SMS text file
- Load a sample as a series of FITS image files
- Define an ROI and call
reduce()to get the normalized FY spectrum
The I0 reference is a Single Motor Scan (SMS) text file collected without a
sample in the beam. Load it with load_sms_file:
from pathlib import Path
from fyn_reduce import load_sms_file
i0 = load_sms_file(Path("/data/i0_scan.txt"))The returned DataFrame contains one row per energy point. The columns used
during reduction are 'Beamline Energy', 'Photodiode', and 'AI 3 Izero'.
Sample data is stored as a series of FITS images, one per energy point plus any
dark frames. Pass all files for a single sample to FYLoader:
from pathlib import Path
from fyn_reduce import FYLoader, load_sms_file
i0 = load_sms_file(Path("/data/i0_scan.txt"))
files = sorted(Path("/data/sampleA_001/").glob("*.fits"))
loader = FYLoader(files=files, i0=i0, name="sampleA_001")Files are loaded automatically on construction (autoload=True by default).
To defer loading:
loader = FYLoader(files=files, i0=i0, name="sampleA_001", autoload=False)
# ... do other work ...
loader.load()Dark frames (CCD shutter closed) are identified automatically from the
CCD Camera Shutter Inhibit flag in each FITS header and subtracted during
reduction. If you have a separately collected dark image, pass it as
external_dark:
from fyn_reduce import load_fits_file
dark_image, _ = load_fits_file(Path("/data/dark_001.fits"))
spectrum = loader.reduce(external_dark=dark_image)When external_dark is provided the embedded dark frames in the series are
ignored.
The ROI controls which pixels on the detector are summed to compute the FY signal. It is defined as a center point and a size in detector pixel coordinates.
loader.roi_center = (512, 512) # (x, y) center pixel
loader.roi_size = (500, 500) # (x_range, y_range) in pixelsThe defaults — center (512, 512), size (500, 500) — cover most of a
1024 × 1024 detector. A smaller ROI isolates signal from background:
# Tight ROI around the fluorescence spot
loader.roi_center = (480, 530)
loader.roi_size = (200, 200)The ROI is always square. The actual pixel slice applied is:
x: [ roi_center[0] - roi_size[0]/2, roi_center[0] + roi_size[0]/2 ]
y: [ roi_center[1] - roi_size[1]/2, roi_center[1] + roi_size[1]/2 ]
To find good ROI coordinates, inspect a representative frame:
from fyn_reduce import load_fits_file
import matplotlib.pyplot as plt
image, meta = load_fits_file(files[0])
fig, ax = plt.subplots()
ax.imshow(image, origin="lower", cmap="viridis")
plt.show()Use the cursor readout in the matplotlib window to identify the center and extent of the fluorescence spot.
Hot pixels (cosmic rays, detector artifacts) are removed before the ROI sum using a median filter. Two parameters control sensitivity:
loader.diz_threshold = 10.0 # ratio above which a pixel is replaced
loader.diz_size = 3 # median filter kernel size (pixels)A lower diz_threshold removes more pixels; a higher value is more
conservative. The default of 10.0 is suitable for most data.
Call reduce() after setting the ROI and dezinger parameters:
spectrum = loader.reduce()The result is a pandas.DataFrame with three columns:
| Column | Description |
|---|---|
Energy |
Beamline energy (eV) |
FY |
Normalized fluorescence yield (arb. units) |
FY_err |
Poisson error on FY (same units) |
If the beamline energy axis needs a calibration correction:
loader.energy_offset = 0.3 # eV; added to every frame energy before reduction
spectrum = loader.reduce()Save as a tab-delimited .dat file with a lowercase header row:
spectrum.rename(columns={"Energy": "energy", "FY": "fy", "FY_err": "fy_err"}) \
.to_csv("sampleA_001.dat", sep="\t", index=False)from pathlib import Path
import matplotlib.pyplot as plt
from fyn_reduce import FYLoader, load_sms_file
data_dir = Path("/data/20260507")
# Load I0 reference
i0 = load_sms_file(data_dir / "i0_scan.txt")
# Load sample
files = sorted((data_dir / "sampleA_001").glob("*.fits"))
loader = FYLoader(files=files, i0=i0, name="sampleA_001")
# Set ROI
loader.roi_center = (480, 530)
loader.roi_size = (200, 200)
# Reduce
spectrum = loader.reduce()
# Plot
fig, ax = plt.subplots()
ax.errorbar(spectrum["Energy"], spectrum["FY"], yerr=spectrum["FY_err"], fmt="o-")
ax.set_xlabel("Beamline Energy (eV)")
ax.set_ylabel("FY (arb. units)")
ax.set_title("sampleA_001")
plt.show()
# Save
spectrum.rename(columns={"Energy": "energy", "FY": "fy", "FY_err": "fy_err"}) \
.to_csv(data_dir / "sampleA_001.dat", sep="\t", index=False)