Source code for est.core.io.write_xas

from datetime import datetime
from typing import Any
from typing import List
from typing import Tuple
from typing import Union

from silx.io.dictdump import dicttonx
from silx.io.url import DataUrl

from ..types.spectrum import Spectrum
from ..types.xasobject import XASObject


[docs] def write_xas( output_file: str, xas_obj: Union[dict, XASObject], overwrite: bool = True, output_group_template: str = "1.{}", output_group_start_index: int = 1, ) -> List[str]: """ Save XAS object in HDF5, one group per spectrum. :param output_file: HDF5 file name :param xas_obj: XAS object with data and results to save. :param overwrite: overwrite existing HDF5 content. :param output_group_template: HDF5 group name can have a placeholder `"{}"` for the spectrum index. :param output_group_start_index: Start spectrum index to be used in `output_group_template`. """ if not output_file: raise ValueError("no output filename defined") if isinstance(xas_obj, dict): xas_obj = XASObject.from_dict(xas_obj) if not isinstance(xas_obj, XASObject): raise TypeError(type(xas_obj)) # Prepare saving data_paths = [] data_groups = [] last_top_group = None for i in range(xas_obj.n_spectrum): out_index = output_group_start_index + i parts = output_group_template.format(out_index).split("/") top_group, data_group, data_path = _prepare_nxdict(*parts) data_paths.append(data_path) data_groups.append(data_group) if last_top_group != top_group: last_top_group = top_group dicttonx( top_group, output_file, mode="a", update_mode="add", ) # The output group template does not have a placeholder if len(set(data_paths)) != len(data_paths): data_paths = [ f"{s}_{output_group_start_index+i}" for i, s in enumerate(data_paths, 1) ] # Save results urls = [] for spec, data_group, data_path in zip(xas_obj.spectra, data_groups, data_paths): data_group = dict(data_group) data_group.update(_build_raw(spec)) data_group.update(_build_results(spec)) dicttonx( data_group, output_file, data_path, mode="a", update_mode="replace" if overwrite else "add", ) url = DataUrl(file_path=output_file, data_path=data_path) urls.append(url.path()) return urls
def _prepare_nxdict(*parts: str) -> Tuple[dict, dict, str]: parts = [s for s in parts if s] if not parts: parts = ["entry"] root = {"@NX_class": "NXroot"} imax = len(parts) - 1 parent = root for i, name in enumerate(parts): data_node = {"@NX_class": "NXentry" if i == 0 else "NXcollection"} if i < imax: parent[name] = data_node parent = parent[name] data_path = "/".join(["", *parts]) return root, data_node, data_path def _build_raw(spec: Spectrum) -> dict: data = {"data": _mu_vs_energy(spec)} data = _prune_none_and_empty_dict(data) if data: data["@default"] = "data" return data def _build_results(spec: Spectrum) -> dict: results = { "statistics": _build_statistics(spec), "normalized": _normalized_mu_vs_energy(spec), "raw_enorm": _mu_vs_normalized_energy(spec), "normalized_enorm": _normalized_mu_vs_normalized_energy(spec), "exafs": _exafs(spec), "ft": _ft(spec), "noise": _noise(spec), "larch": _nxparameters(**spec.larch_dict.to_nexus()), "pymca": _nxparameters(**spec.pymca_dict.to_nexus()), } results = _prune_none_and_empty_dict(results) if not results: return {} process = { "@NX_class": "NXprocess", "program": "est", "date": datetime.now().astimezone().isoformat(), **results, } return {"results": process} def _mu_vs_energy(spec: Spectrum) -> dict: return _nxdata(spec, "energy", "mu", "pre_edge", "post_edge") def _mu_vs_normalized_energy(spec: Spectrum) -> dict: return _nxdata( spec, "normalized_energy", "mu", "pre_edge", "post_edge", ) def _normalized_mu_vs_energy(spec: Spectrum) -> dict: return _nxdata(spec, "energy", "flatten_mu", "normalized_mu") def _normalized_mu_vs_normalized_energy(spec: Spectrum) -> dict: return _nxdata( spec, "normalized_energy", "normalized_mu", "flatten_mu", ) def _exafs(spec: Spectrum) -> dict: return _nxdata(spec, "k", "chi", "chi_weighted_k") def _ft(spec: Spectrum) -> dict: data = _nxdata( spec, "ft.radius", "ft.intensity", "ft.real", "ft.imaginary", "ft.phase" ) if not data: return {} data["parameters"] = _nxparameters(**spec.ft.to_nexus("window_weight")) return data def _noise(spec: Spectrum) -> dict: return _nxdata(spec, "energy", "noise_savgol") def _build_statistics(spec: Spectrum) -> dict: parameters = {} parameters.update(spec.to_nexus("e0")) parameters.update(spec.to_nexus("edge_step")) parameters.update(spec.to_nexus("noise_e_min")) parameters.update(spec.to_nexus("noise_e_max")) parameters.update(spec.to_nexus("raw_noise_savgol")) parameters.update(spec.to_nexus("norm_noise_savgol")) if parameters: return _nxparameters(**parameters) return {} def _nxdata(spec: Spectrum, axis: str, signal: str, *auxiliary_signals: str) -> dict: data = spec.to_nexus(axis, signal, *auxiliary_signals) data = _prune_none_and_empty_dict(data) # Extract field name from model path. For example `radius` from `ft.radius` axis = axis.split(".")[-1] signal = signal.split(".")[-1] auxiliary_signals = [s.split(".")[-1] for s in auxiliary_signals] if signal not in data or axis not in data: return {} auxiliary_signals = [s for s in auxiliary_signals if s in data] data = {"@NX_class": "NXdata", "@signal": signal, "@axes": [axis], **data} if auxiliary_signals: data["@auxiliary_signals"] = auxiliary_signals return data def _nxparameters(**parameters: Any) -> dict: parameters = _prune_none_and_empty_dict(parameters) if not parameters: return {} return {"@NX_class": "NXparameters", **parameters} def _prune_none_and_empty_dict(obj: Any) -> Any: if isinstance(obj, dict): cleaned = {} for k, v in obj.items(): v = _prune_none_and_empty_dict(v) if v is None: continue if isinstance(v, dict) and len(v) == 0: continue cleaned[k] = v return cleaned return obj