Source code for nxtomomill.converter.fluo.blissfluoscan
"""Scan dedicated for bliss fluo-data format - based on h5 files generated by ewoksfluo"""
from __future__ import annotations
import logging
import pint
import numpy
from dataclasses import dataclass, field
import numpy as np
from numpy.typing import NDArray, DTypeLike
import h5py
_ureg = pint.get_application_registry()
_logger = logging.getLogger(__name__)
try:
import tifffile # noqa #F401 needed for later possible lazy loading
except ImportError:
has_tifffile = False
else:
has_tifffile = True
__all__ = ["BlissFluoTomoScanBase", "BlissFluoTomoScan3D", "BlissFluoTomoScan2D"]
[docs]@dataclass
class BlissFluoTomoScanBase:
"""Base class to read XRF data fitted with ewoksfluo."""
ewoksfluo_filename: str
detectors: tuple = ()
dtype: DTypeLike = numpy.float32
verbose: bool = False
angles_deg: list[dtype] | None = None
lines: dict[str, list[str]] = field(default_factory=dict)
pixel_size: float | None = None
energy: float | None = None
@property
def rot_angles_deg(self) -> NDArray:
return self.angles_deg
@property
def rot_angles_rad(self) -> NDArray:
return self.rot_angles_deg.to(_ureg.rad)
[docs]@dataclass
class BlissFluoTomoScan3D(BlissFluoTomoScanBase):
"""Base class to read XRF data fitted with ewoksfluo."""
ewoksfluo_filename: str
detectors: tuple = ()
dtype: DTypeLike = numpy.float32
verbose: bool = False
angles_deg: list[dtype] | None = None
lines: list[str] = field(default_factory=list)
pixel_size: float | None = None
energy: float | None = None
def _check_entry(self, entry, f):
data_shape = f[
f"{entry}/grid/{self.detectors[0]}/results/massfractions/{self.lines[0]}"
][()].shape
flag = self.slow_npoints * self.fast_npoints == data_shape[0] * data_shape[1]
if not flag:
msg = f"Entry {entry} has unexpected data shape {data_shape}, expected {(self.slow_npoints, self.fast_npoints)}. It has been ignored (i.e. not converted)."
_logger.warning(msg)
return flag
def __post_init__(self):
try:
with h5py.File(self.ewoksfluo_filename, "r") as f:
self.entries = sorted(list(f.keys()), key=lambda x: float(x))
entry0 = self.entries[0]
# Get detector names
if len(self.detectors) == 0:
self.detectors = list(f[f"{entry0}/grid"].keys())
# Get fitted line names
detector0 = self.detectors[0]
self.lines = []
for name, ds in f[
f"{entry0}/grid/{detector0}/results/massfractions"
].items():
if ds.ndim == 2:
self.lines.append(name)
# Get projections dimensions
self.slow_npoints = float(
f[f"{entry0}/instrument/fscan_parameters/slow_npoints"][()]
)
self.fast_npoints = float(
f[f"{entry0}/instrument/fscan_parameters/fast_npoints"][()]
)
# Filter out interrupted scans:
self.entries = [
entry for entry in self.entries if self._check_entry(entry, f)
]
# Get rotation angles
angles_tmp = []
for entry in self.entries:
angles_tmp.append(f[f"{entry}/instrument/positioners/somega"][()])
self.angles_deg = np.array(angles_tmp, dtype=self.dtype) * _ureg.deg
# Get pixel size
fast_pixel_size = float(
f[f"{entry0}/instrument/fscan_parameters/fast_step_size"][()]
)
slow_pixel_size = float(
f[f"{entry0}/instrument/fscan_parameters/slow_step_size"][()]
)
if slow_pixel_size == fast_pixel_size:
self.pixel_size = slow_pixel_size * _ureg.um
else:
msg = f"Found slow_pixel_size ({slow_pixel_size}) different from fast_pixel_size ({fast_pixel_size})"
_logger.info(msg)
raise ValueError(msg)
# Get energy
self.energy = (
float(
f[
f"{entry0}/instrument/metadata/InstrumentMonochromator_energy"
][()]
)
* _ureg.keV
)
except FileNotFoundError:
self.entries = None
_logger.info(
f"Detected {len(self.entries)} projections in {self.ewoksfluo_filename}."
)
def load_data(self, det, line):
if self.entries is None:
_logger.info(f"No entry was detected in {self.ewoksfluo_filename}.")
else:
with h5py.File(self.ewoksfluo_filename, "r") as f:
projs_tmp = []
angles_tmp = []
for entry in self.entries:
data = f[f"{entry}/grid/{det}/results/massfractions/{line}"][()]
projs_tmp.append(numpy.nan_to_num(data).astype(self.dtype))
angles_tmp.append(f[f"{entry}/instrument/positioners/somega"][()])
projs_tmp = np.array(projs_tmp)
angles_tmp = np.array(angles_tmp).astype(self.dtype)
if np.allclose(angles_tmp * _ureg.deg, self.angles_deg):
return np.ascontiguousarray(projs_tmp)
else:
msg = f"Angles found for {det}/{line} do not match the angles found at the class level."
_logger.info(msg)
raise ValueError(msg)
[docs]@dataclass
class BlissFluoTomoScan2D(BlissFluoTomoScanBase):
"""Base class to read XRF data fitted with ewoksfluo."""
ewoksfluo_filename: str
detectors: tuple = ()
dtype: DTypeLike = numpy.float32
verbose: bool = False
angles_deg: list[dtype] | None = None
lines: list[str] = field(default_factory=list)
pixel_size: float | None = None
energy: float | None = None