Source code for coseda.nexus.goniometer
"""Helpers for NeXus goniometer transform chains."""
from __future__ import annotations
from typing import Iterable, Optional, Sequence, Tuple, Union
import re
import h5py
import numpy as np
from coseda.nexus.groups import ensure_nexus_parents
from coseda.nexus.paths import (
GONIOMETER_GROUP_PATH,
GONIOMETER_TRANSFORMATIONS_PATH,
SAMPLE_TRANSFORM_PATH,
)
ORDER_KEY = "goniometer_transform_order"
DEFAULT_ORDER = ("stage_z", "alpha", "beta", "stage_x", "stage_y")
_VALID_KEYS = {"stage_z", "alpha", "beta", "stage_x", "stage_y"}
_TOKEN_MAP = {
"x": "stage_x",
"y": "stage_y",
"z": "stage_z",
"stagepos_x": "stage_x",
"stagepos_x_refined": "stage_x",
"stagepos_y": "stage_y",
"stagepos_y_refined": "stage_y",
"stagepos_z": "stage_z",
"alpha": "alpha",
"alphatilt": "alpha",
"alpha_tilt": "alpha",
"beta": "beta",
"betatilt": "beta",
"beta_tilt": "beta",
}
def _pick_dataset(h5file: h5py.File, names: Iterable[str]) -> Optional[h5py.Dataset]:
for name in names:
path = f"/entry/data/{name}"
if path in h5file:
return h5file[path]
return None
def _link_transform(
trans_group: h5py.Group,
name: str,
source: h5py.Dataset,
transform_type: str,
vector: Tuple[float, float, float],
units: str,
depends_on: str,
) -> None:
if name in trans_group:
del trans_group[name]
trans_group[name] = source
ds = trans_group[name]
ds.attrs["transformation_type"] = transform_type
ds.attrs["vector"] = np.array(vector, dtype=np.float32)
if "units" not in ds.attrs:
ds.attrs["units"] = units
ds.attrs["depends_on"] = depends_on
def _normalize_order(order: Optional[Union[Sequence[str], str]]) -> Optional[Tuple[str, ...]]:
if order is None:
return None
if isinstance(order, str):
tokens = [t for t in re.split(r"[,\s>]+", order) if t]
else:
tokens = list(order)
normalized = []
for token in tokens:
key = str(token).strip().lower()
if not key:
continue
key = _TOKEN_MAP.get(key, key)
if key in _VALID_KEYS and key not in normalized:
normalized.append(key)
return tuple(normalized) if normalized else None
def _read_order(goniometer: h5py.Group) -> Optional[Tuple[str, ...]]:
if "transform_order" in goniometer:
raw = goniometer["transform_order"][()]
elif "transform_order" in goniometer.attrs:
raw = goniometer.attrs["transform_order"]
else:
return None
if isinstance(raw, (bytes, np.bytes_)):
return _normalize_order(raw.decode("utf-8"))
if isinstance(raw, np.ndarray):
items = []
for item in raw:
if isinstance(item, (bytes, np.bytes_)):
items.append(item.decode("utf-8"))
else:
items.append(str(item))
return _normalize_order(items)
return _normalize_order(str(raw))
def _write_order(goniometer: h5py.Group, order: Sequence[str]) -> None:
dtype = h5py.string_dtype(encoding="utf-8")
if "transform_order" in goniometer:
del goniometer["transform_order"]
goniometer.create_dataset("transform_order", data=np.array(order, dtype=object), dtype=dtype)
goniometer.attrs["transform_order"] = ", ".join(order)
[docs]
def get_goniometer_transform_order(config) -> Optional[Tuple[str, ...]]:
for section in ("AcquisitionDetails", "Parameters"):
try:
if config.has_option(section, ORDER_KEY):
return _normalize_order(config.get(section, ORDER_KEY))
except Exception:
continue
return None
[docs]
def ensure_goniometer_transforms(
h5file: h5py.File,
transform_order: Optional[Union[Sequence[str], str]] = None,
) -> None:
"""
Create a goniometer transform chain:
stage_z -> alpha -> beta -> stage_x -> stage_y (all relative to sample_pos).
"""
ensure_nexus_parents(h5file)
goniometer = h5file.require_group(GONIOMETER_GROUP_PATH.lstrip("/"))
goniometer.attrs["NX_class"] = "NXgoniometer"
trans = h5file.require_group(GONIOMETER_TRANSFORMATIONS_PATH.lstrip("/"))
trans.attrs["NX_class"] = "NXtransformations"
order = _normalize_order(transform_order)
if order:
_write_order(goniometer, order)
else:
order = _read_order(goniometer) or DEFAULT_ORDER
chain = []
for step in order:
if step == "stage_z":
stage_z = _pick_dataset(h5file, ("stagepos_z",))
if stage_z is not None:
chain.append(("stage_z", stage_z, "translation", (0.0, 0.0, 1.0), "m"))
elif step == "alpha":
alpha = _pick_dataset(h5file, ("alphatilt",))
if alpha is not None:
chain.append(("alpha", alpha, "rotation", (1.0, 0.0, 0.0), "degree"))
elif step == "beta":
beta = _pick_dataset(h5file, ("betatilt",))
if beta is not None:
chain.append(("beta", beta, "rotation", (0.0, 1.0, 0.0), "degree"))
elif step == "stage_x":
stage_x = _pick_dataset(h5file, ("stagepos_x_refined", "stagepos_x"))
if stage_x is not None:
chain.append(("stage_x", stage_x, "translation", (1.0, 0.0, 0.0), "m"))
elif step == "stage_y":
stage_y = _pick_dataset(h5file, ("stagepos_y_refined", "stagepos_y"))
if stage_y is not None:
chain.append(("stage_y", stage_y, "translation", (0.0, 1.0, 0.0), "m"))
if not chain:
return
depends_on = SAMPLE_TRANSFORM_PATH
for name, source, ttype, vector, units in chain:
_link_transform(trans, name, source, ttype, vector, units, depends_on)
depends_on = f"{GONIOMETER_TRANSFORMATIONS_PATH}/{name}"
sample = h5file["entry/sample"]
sample.attrs["depends_on"] = depends_on