"""Implements loading the text file format for MB Scientific analyzers."""
from __future__ import annotations
import warnings
from logging import DEBUG, INFO
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar
import numpy as np
import xarray as xr
from arpes.constants import TWO_DIMENSION
from arpes.debug import setup_logger
from arpes.endstations import HemisphericalEndstation
from arpes.helper import clean_keys
if TYPE_CHECKING:
from _typeshed import Incomplete
from arpes._typing.attrs_property import ScanDesc
__all__ = ("MBSEndstation",)
LOGLEVELS = (DEBUG, INFO)
LOGLEVEL = LOGLEVELS[1]
logger = setup_logger(__name__, LOGLEVEL)
[docs]
class MBSEndstation(HemisphericalEndstation):
"""Implements loading text files from the MB Scientific text file format.
There's not too much metadata here except what comes with the analyzer settings.
"""
PRINCIPAL_NAME = "MBS"
ALIASES: ClassVar[list[str]] = [
"MB Scientific",
]
_TOLERATED_EXTENSIONS: ClassVar[set[str]] = {
".txt",
}
RENAME_KEYS: ClassVar[dict[str, str]] = {
"deflx": "psi",
}
def resolve_frame_locations(
self,
scan_desc: ScanDesc | None = None,
) -> list[Path | str]:
"""There is only a single file for the MBS loader, so this is simple."""
if scan_desc is None:
scan_desc = {}
return [scan_desc.get("path", scan_desc.get("file"))]
def postprocess_final(
self,
data: xr.Dataset,
scan_desc: ScanDesc | None = None,
) -> xr.Dataset:
"""Performs final data normalization.
Because the MBS format does not come from a proper ARPES DAQ setup,
we have to attach a bunch of missing coordinates with blank values
in order to fit the data model.
"""
warnings.warn(
"Loading from text format misses metadata. You will need to supply "
"missing coordinates as appropriate.",
stacklevel=2,
)
data.attrs["psi"] = float(data.attrs["psi"])
for s in [dv for dv in data.data_vars.values() if "eV" in dv.dims]:
s.attrs["psi"] = float(s.attrs["psi"])
defaults = {
"x": np.nan,
"y": np.nan,
"z": np.nan,
"theta": 0,
"beta": 0,
"chi": 0,
"alpha": np.nan,
"hv": np.nan,
}
for k, v in defaults.items():
data.attrs[k] = v
for s in [dv for dv in data.data_vars.values() if "eV" in dv.dims]:
s.attrs[k] = v
return super().postprocess_final(data, scan_desc)
def load_single_frame(
self,
frame_path: str | Path = "",
scan_desc: ScanDesc | None = None,
**kwargs: Incomplete,
) -> xr.Dataset:
"""Load a single frame from an MBS spectrometer.
Most of the complexity here is in header handling and building
the resultant coordinates. Namely, coordinates are stored in an indirect
format using start/stop/step which needs to be hydrated.
"""
if scan_desc:
logger.debug("MBSEndstation.loadl_single_frame:scan_desc is not used")
if kwargs:
for k, v in kwargs.items():
msg = "MBSEndstation.loadl_single_frame:"
msg += f"unused kwargs is detected: k:{k}, v:{v}"
logger.debug(msg)
with Path(frame_path).open() as f:
lines = f.readlines()
lines = [line.strip() for line in lines]
data_index = lines.index("DATA:")
header = lines[:data_index]
data = lines[data_index + 1 :]
data_array = np.array([[float(f) for f in d] for d in [d.split() for d in data]])
del data
headers = [h.split("\t") for h in header]
headers = [h for h in headers if len(h) == len(("item", "value"))]
alt = [h for h in headers if len(h) == len(("only_item",))]
headers.append(["alt", str(alt)])
attrs = clean_keys(dict(headers))
eV_axis = np.linspace(
float(attrs["start_k_e_"]),
float(attrs["end_k_e_"]),
num=int(attrs["no_steps"]),
endpoint=False,
)
n_eV = int(attrs["no_steps"])
idx_eV = data_array.shape.index(n_eV)
if data_array.ndim == TWO_DIMENSION:
phi_axis = np.linspace(
float(attrs["xscalemin"]),
float(attrs["xscalemax"]),
num=data_array.shape[1 if idx_eV == 0 else 0],
endpoint=False,
)
coords = {"phi": np.deg2rad(phi_axis), "eV": eV_axis}
dims = ["eV", "phi"] if idx_eV == 0 else ["phi", "eV"]
else:
coords = {"eV": eV_axis}
dims = ["eV"]
return xr.Dataset(
{
"spectrum": xr.DataArray(
data_array,
coords=coords,
dims=dims,
attrs=attrs,
),
},
attrs=attrs,
)