Loading Data
Asymmetry reads the major muSR raw-data formats through a common
load() API and a uniform Grouping dialog: ISIS
muon NeXus (legacy V1 and V2), PSI BIN/MDU, and MusrRoot/LEM ROOT. The
practical consequence is that data from different facilities can be
compared in one session without manual format-by-format pre-processing.
Each loader is implemented against the same authoritative reference used
by the facility’s own software — musrfit’s PRunDataHandler for PSI
BIN/MDU and MusrRoot/LEM ROOT, with Mantid’s LoadPSIMuonBin used as a
PSI-BIN cross-check — so format-specific subtleties (bin-index
conventions, per-detector \(t_0\) offsets, multi-period ISIS files,
PSI temperature sidecars) are handled consistently with what those
packages do.
If you are loading a new dataset for the first time, the subsections below document the metadata each loader extracts and the common failure modes. Period-mode files (e.g. photo-μSR light-OFF/ON, RF on/off, avoided-level-crossing) can be navigated with the scriptable period-selection API — see Selecting periods (Red / Green) below.
For an end-to-end walk-through that starts with loading, see Temperature scan through a magnetic transition.
Supported formats
ISIS Muon NeXus (.nxs, .nexus)
Asymmetry supports ISIS muon NeXus files (legacy V1 and modern V2), including
multi-period runs. Multi-period files return a list of MuonDataset values.
Scalar sample temperature is read from the NeXus sample header. NXlog-style
temperature groups with time plus value/values datasets are also
summarised in Get Info; the Data Browser keeps the scalar header temperature
unless Options → Use temperature from log or the Temperature (K)
include checkbox is ticked, in which case the log mean is shown while that
selection remains active. The Options menu choice is global and resets all
loaded runs to the same behaviour. The Get Info checkbox affects only the
target run, making it useful for per-dataset overrides. Log-derived temperature
values are shown with red text in the Data Browser.
The applied field is read from the sample/magnetic_field header and stored
in gauss, matching the convention the rest of Asymmetry (and the PSI and
MusrRoot loaders) uses. Most ISIS files store the value in gauss with no unit,
which is taken as-is. When the dataset carries a units attribute the loader
honours it: T / tesla is converted by a factor of 104 and
mT / millitesla by a factor of 10. An absent, blank, gauss, or
unrecognised unit is passed through unchanged — the loader never guesses a
conversion the file did not declare.
For NeXus good-data windows, Asymmetry treats integer bin metadata as
canonical (first_good_bin, last_good_bin, and t0_bin). When
first_good_time / last_good_time are present, they are used only as a
fallback if the corresponding integer bin attributes are missing.
Asymmetry’s internal Histogram and grouping bin indices are always
zero-based array indices. Real ISIS files (both V1 and V2) often encode
integer bin metadata using one-based centre-bin numbering. The loader compares
explicit t0_bin values with the time axis; when all available detector
t0 values point one sample past t = 0, it subtracts one from
t0_bin, first_good_bin, and last_good_bin and records
bin_index_base = 1 in grouping metadata. Otherwise it leaves the integers
unchanged with bin_index_base = 0. For V1, this good-data window and
t0_bin are read from the attributes of the counts dataset (where ISIS
stores them), matching the V2 behaviour.
HDF4 container (legacy .nxs)
The V1 /run muonTD schema is the format WiMDA reads natively, and ISIS
historically wrote it inside an HDF4 container (pre-~2015 runs) rather than
HDF5. Asymmetry detects the container from the file magic and reads HDF4 V1
files directly — no manual pre-conversion to HDF5 is required. The same schema
reader is used for both containers, so an HDF4 .nxs and its HDF5-converted
twin reduce to identical asymmetry, counts, grouping, and metadata.
HDF4 reading needs the optional pyhdf dependency, installed with the
hdf4 extra:
pip install asymmetry[hdf4]
On Linux the pyhdf wheel bundles the HDF4 C library. On macOS
(Apple Silicon) the PyPI wheel does too (Intel Macs: use conda-forge
pyhdf or build from source). On Windows, pyhdf’s wheel does not
bundle the HDF4 runtime: it also needs hdf.dll / mfhdf.dll (for
example from the conda-forge hdf4 package, as Mantid uses, or via
packaging/windows/fetch_hdf4_dlls.py). Point the ASYMMETRY_HDF4_DLL_DIR
environment variable at the directory holding those DLLs. The pre-built Windows
and Apple Silicon macOS desktop releases bundle HDF4. When pyhdf (or the
Windows runtime) is absent, opening an HDF4 .nxs raises a clear error
naming the hdf4 extra; HDF5 .nxs loading is unaffected.
PSI BIN/MDU (.bin, .mdu)
Asymmetry supports PSI raw histogram files in the classic PSI-BIN and PSI-MDU formats. These files are loaded into normal raw histogram runs, so they use the same full Grouping dialog as ISIS NeXus data.
This loader is intentionally based on established PSI readers rather than a
new file interpretation. The BIN/MDU binary layout and metadata offsets follow
the musrfit PSI reader, especially PRunDataHandler::ReadPsiBinFile and the
MuSR_td_PSI_bin/MDU structures it uses. Mantid’s LoadPSIMuonBin was
also checked for PSI-BIN behaviour. Asymmetry keeps the implementation in pure
Python and maps the result into the existing Run/Histogram data model.
PSI files can store different t0 values per detector. Asymmetry preserves
those detector-specific offsets and aligns histograms by their own t0 when
regrouping.
For PSI-BIN, musrfit’s MuSR_td_PSI_bin reader uses the stored t0 and
first good values directly as C++ vector indices, so Asymmetry treats them
as zero-based. The last good value has an end-bin convention: some PSI-BIN
headers store the histogram length itself, for example 8192 for an
8192-bin histogram. musrfit uses this effectively as an exclusive stop when it
extracts good-bin ranges. Asymmetry stores the corresponding valid inclusive
array index, so that example becomes 8191 internally.
For PSI-MDU, the tag metadata are converted to the same zero-based internal
coordinate system used by musrfit’s public reader methods. The converted
t0, first good, and last good values from the musrfit reader match
the values stored by Asymmetry.
PSI label metadata is used to build initial grouping suggestions. BIN labels
such as Forw/Back and MDU positron labels such as F1/B1 become
group names and forward/backward defaults where possible. Users can then edit
the grouping in the same full Grouping dialog used for raw NeXus runs.
Repeated labels are not collapsed: each histogram remains visible as its own
group, with suffixes such as Forw 2 added where needed.
PSI labels define forward/backward along the beam direction, whereas Asymmetry’s pair-asymmetry controls use forward/backward relative to the initial muon spin. For PSI data, the analysis forward/backward selections are therefore initialised with the beam-forward and beam-backward detector groups swapped. The detector layout editor still displays the PSI detector convention.
If the explicit field entry is missing or zero but the PSI comment/title text
contains a recognisable field such as LF 32G or Bz=150 G, the GUI
offers to apply that comment-derived value as the run field. This prompt is
available for PSI BIN/MDU and MusrRoot/LEM ROOT files.
PSI-BIN temperature metadata has two sources. The scalar run temperature is
read from the BIN header using the musrfit-compatible offsets. Optional
temperature-log sidecars use Mantid’s LoadPSIMuonBin convention: Asymmetry
searches from the BIN file directory, to three directory levels below it, for
all .mon files whose filename contains the run number. The .mon header
date/title, equipment name, and backslash-delimited rows are parsed into
plottable nexus_time_series entries named like
psi_temperature/Temp_<channel> for classic logs or
psi_temperature/<equipment>/<channel> for FLAME tlog files. When FLAME
logs are present, flamesam0/SAM_ts is marked as the primary sample
temperature to match the MusrRoot slow-control convention; flamedil0/DIL_T_mix
and variox0/Variox are retained as secondary sample-temperature traces.
The Get Info window shows these logs in the summary/advanced tables and records
the sidecar paths plus Mantid LoadPSIMuonBin-compatible provenance in
psi_temperature_log metadata.
MusrRoot / LEM ROOT (.root)
Asymmetry supports ROOT files that follow the PSI/MusrRoot layout used by
musrfit. The loader reads the ROOT objects with uproot and maps the result
into the same raw Run/Histogram model as ISIS NeXus and PSI BIN/MDU
files, so ROOT runs use the full Grouping dialog.
The implementation follows musrfit’s PRunDataHandler::ReadRootFile rather
than inventing a new interpretation. In particular:
MusrRoot files are detected from
RunHeaderandhDecay%03dhistograms. Both the documentedRunHeaderTFolderlayout and the newerTDirectorylayout are supported. musrfit added theTDirectory-based layout in a 2025 commit series, and the current MusrRoot specification defines it as canonical (theTFolderstreaming is deprecated). PSI’s 2026 FLAME DAQ writes this layout; other PSI bulk-µSR instruments are expected to follow, since the layout is instrument-agnostic.Histogram search follows musrfit’s paths:
histos/hDecay%03dfor folder files andhistos/DecayAnaModule/hDecay%03dfor directory files.RunInfovalues such as run number, title, laboratory, instrument, sample, temperature, field, time resolution, number of histograms, and red/green offsets are read from the header when present. Instrument names are matched case-insensitively (FLAME writes the lowercaseflame).DetectorInfoentries provide detector labels, histogram numbers, per-detectort0bins, and detector-specific good-bin ranges when the file supplies them.In the
TDirectorylayout,RunHeaderand its subfolders (RunInfo,DetectorInfo/DetectorNNN,SampleEnvironmentInfo,MagneticFieldEnvironmentInfo,BeamlineInfo,RunSummary) are themselvesTDirectorys, and every leaf is aTObjStringencoding both its key name and its payload as"NNN - Label: Value -@type"(a physical-quantity value carries an optional error, unit, set-point, and free-text description, for exampleTime Resolution: 0.09765625 ns; SiPM). TheNNNprefix is a single counter shared across every subfolder, so numbering within one subfolder is not contiguous; Asymmetry parses these entries by label, never by number. The free-textRunSummaryblock — which musrfit itself does not read — is attached verbatim to the loaded run asmetadata["musrroot_run_summary"], so it is preserved as provenance rather than discarded.MusrRoot slow-control histograms in
histos/SCAnaModuleare imported as plottablenexus_time_serieslogs. In particular,hSampleTemperatureis summarised as an average temperature in Get Info while the scalarRunInfo/Sample Temperatureremains the default Data Browser value until Options → Use temperature from log or the Temperature (K) include checkbox is ticked. The Options menu choice applies to every loaded run, while the Get Info checkbox overrides only that run. Log-derived temperature values are shown with red text. FLAME ROOT files may use EPICS-style sensor names such asSAM_ts_valueinstead of a literalhSampleTemperaturehistogram; Asymmetry follows theSens=...pointer inRunInfo/Sample Temperatureso the Get Info temperature row still opens the corresponding log plot.
Legacy LEM ROOT files without a MusrRoot RunHeader are supported only when
their RunInfo and histogram objects are readable through uproot. The
full PyROOT/TLemRunHeader object model used by very old files is not
reimplemented.
The optional uproot dependency is required for ROOT loading. Install with
the root extra if it is not already present.
Deadtime correction remains controlled by the Grouping dialog. File mode
uses per-detector deadtime values already supplied by a run file, while
Manual, Cal, and Estimate resolve their deadtime payload in the
Grouping dialog and then apply that same payload to all selected runs. The
resolved payload is also reused for future datasets loaded into the same
project, matching the existing grouping-template workflow.
PSI BIN/MDU and MusrRoot/LEM ROOT files do not normally contain NeXus-style
file deadtime constants, so File mode is often unavailable there. Those
formats can still use manual per-detector values, the WiMDA-style Cal
per-detector calibration routine, or the reference-run deadtime estimate.
Background correction is a separate Grouping dialog toggle for PSI-style data
(.bin, .mdu, and PSI/LEM .root). It subtracts a count background
from grouped raw forward/backward histograms before asymmetry calculation,
following musrfit PRunAsymmetry. The toggle is off by default and disabled
for ISIS/NeXus data, keeping the correction split explicit: ISIS uses
file-based deadtime correction when available, while PSI uses grouped
background correction.
Reference provenance
The format and correction behaviour is split by source so that the application does not invent format rules:
PSI BIN/MDU parsing follows musrfit’s PSI raw-data reader, with Mantid’s
LoadPSIMuonBinused as a cross-check for BIN files.MusrRoot/LEM ROOT parsing follows musrfit
PRunDataHandler::ReadRootFileand the MusrRoot documentation:RunHeadermetadata plushDecayhistograms are mapped into Asymmetry’s raw run model.Per-detector
t0handling follows the PSI file metadata exposed by those readers; Asymmetry stores these asdetector_t0_binsand aligns detector histograms before grouping.Background correction follows musrfit
PRunAsymmetry: group forward/backward histograms, subtract fixed values or mean background from inclusive bin ranges, then calculate asymmetry. If no ranges are supplied, the fallback range is0.1 * t0to0.6 * t0. The corrected-count uncertainty follows musrfit-style propagation, while Asymmetry retains its standard alpha convention withalphaapplied to the backward group.File-based deadtime correction uses the non-paralyzable formula implemented by musrfit
PRunBase::DeadTimeCorrectionand MantidApplyDeadTimeCorr:N_corr = N / (1 - N * dead_time / (time_bin * good_frames)). Thegood_framesnormalisation is read from the file’s top-levelgood_frames/goodfrmwhen present; legacy ISIS HDF4 / NeXus-V1 files (e.g. HiFi runs) omit it, so Asymmetry falls back toinstrument/beam(frames_periodper period, then theframes_good/framesrun total). Without that fallback the count defaults to 1 and the correction over-normalises by orders of magnitude.The Grouping dialog also exposes WiMDA-style manual, calibrated, and estimated deadtime workflows alongside file-provided deadtimes.
Estimateis calculated from the selected reference run only and then broadcast to all selected detectors and runs.Calfits each detector separately on that reference run and persists the resulting explicit per-detector table through the manual detector-value editor.Mantid’s PSI-BIN loader can emit a deadtime table, but it fills the PSI-BIN table with zeros. Asymmetry therefore treats PSI deadtime as absent and uses
Filemode only when non-zero file values are actually present.PSI-BIN
.montemperature sidecar loading follows MantidLoadPSIMuonBinrather than musrfit, because musrfit was found to read embedded PSI temperature fields but not these external log files.
Grouping behaviour by format
In the GUI, supported raw data runs use the full Grouping dialog:
NeXus runs use the full Grouping dialog.
- PSI BIN/MDU runs use the full Grouping dialog, with PSI label-derived
forward/backward defaults when labels such as
Forw/BackorF1/B1are present.
- MusrRoot/LEM ROOT runs use the full Grouping dialog. Detector names from
DetectorInfoor ROOT histogram titles become the initial group names, and ROOT per-detectort0values are handled like PSI BIN/MDUt0metadata.
Basic usage
Loading a single file
from asymmetry.core.io import load
dataset = load("path/to/data.nxs")
print(dataset.summary())
The same load API can be used with NeXus files:
dataset_or_periods = load("path/to/HIFI00206453.nxs")
if isinstance(dataset_or_periods, list):
print(f"Loaded {len(dataset_or_periods)} periods")
And with PSI raw files:
dataset = load("deltat_pta_gps_3110.bin")
dataset = load("tdc_hifi_2014_00153.mdu")
And with MusrRoot/LEM ROOT files:
dataset = load("lem15_his_2994.root")
The returned MuonDataset contains:
time: Time axis in microsecondsasymmetry: Asymmetry valueserror: Error barsmetadata: Run metadata (temperature, field, etc.)run: Reference to the original Run object
Selecting periods (Red / Green)
Period-mode runs record several period histograms in one file — for example
light-OFF and light-ON in a photo-μSR experiment, RF on/off, or ALC
steps. Use the period-selection API to pull out a single period as an ordinary
MuonDataset with its own provenance (t0, good-bin window, grouping,
temperature, field, per-period good_frames and deadtime):
The Map Periods dialog (Grouping window ▸ Map periods…) assigns a multi-period run’s periods to the red and green sets; for two-period runs the RG Mode radios in the Grouping window select the period directly.
from asymmetry.core.io import load, select_period, period_count, period_labels
run = load("HIFI00103277.nxs") # two-period photo-µSR run
print(period_count(run)) # 2
print(period_labels(run)) # ['red', 'green']
light_on = select_period(run, "red") # period 1
light_off = select_period(run, "green") # period 2
# ...or select at load time:
light_off = load("HIFI00103277.nxs", period="green")
For the common two-period case the first period is labelled "red" and the
second "green" (the same convention as the GUI “RG box”). You can also pass
a 1-based integer period number, which is the way to address files with three
or more periods. In a photo-μSR experiment the convention is light-OFF =
Green (period 2) and light-ON = Red (period 1); confirm this against the
relaxation for your instrument. Out-of-range numbers and unknown labels raise a
clear error at the boundary. The GUI red/green selector calls this same core
API, so scripts and the desktop app agree on the per-period spectra.
Accessing metadata
dataset = load("data.nxs")
print(f"Run number: {dataset.run_number}")
print(f"Temperature: {dataset.metadata.get('temperature')} K")
print(f"Field: {dataset.metadata.get('field')} G")
print(f"Title: {dataset.metadata.get('title')}")
In the GUI the same preserved provenance is surfaced by right-clicking a run in the Data Browser and choosing Get Info, which opens the run-information dialog:
The Run Parameters table in the Get Info dialog for a loaded transverse-field run, with each field selectable for inclusion in the Data Browser via the Include in Data Browser column. The loader keeps the experiment provenance explicit — instrument, title, temperature, field, and the raw histogram geometry (four detector histograms, 2400 bins, 0.005 μs bin width, and the summed counts) — alongside the free-text comment and run start/end timestamps.
Loading multiple files
from pathlib import Path
data_dir = Path("data")
datasets = []
for nexus_file in data_dir.glob("*.nxs"):
dataset = load(str(nexus_file))
datasets.append(dataset)
print(f"Loaded {len(datasets)} datasets")
Loading a run range
A contiguous run series — a field or temperature scan such as BiSCCO 1276–1289 — can be loaded in one step from a folder plus a first/last run number, instead of selecting every file by hand.
Why this exists. The native Open dialog’s File name field holds only a bounded number of characters (~256), so a long quoted list of ~15 file names is silently truncated and the load has to be split into batches. The run-range path resolves the files itself and never passes them through that field, so a whole scan loads at once.
In the GUI. Choose File → Load Run Range…. Pick the folder that holds
the run files; the dialog prefills the run Prefix (e.g. MUSR) and the
first/last run numbers from the files it finds there. Adjust the range if you
want a subset, then click OK. The matching runs are loaded through the
ordinary multi-file path (duplicate prompts, auto-grouping, and the missing-run
gap warning all apply). The log records how many of the requested runs were
found, e.g. Loading run range 1276–1289: 14 of 14 runs found.
The prefill scan is capped at 20,000 directory entries so a huge facility folder (often on a slow network mount) cannot stall the dialog. When the cap is hit, a warning appears under the folder field — “This folder has too many files to scan in full — showing the first N run files found. Adjust the range by hand if runs are missing.” — and the prefilled first/last numbers may cover only part of the folder; type the range you want and the load proceeds normally.
In scripts. resolve_run_range() is the pure,
GUI-free resolver behind the dialog. It scans a folder and returns the existing
files for an inclusive run range, sorted by run number:
from asymmetry.core.io import load, resolve_run_range
files = resolve_run_range("data/BiSCCO", 1276, 1289, prefix="MUSR")
datasets = [load(str(path)) for path in files]
Resolver semantics:
Inclusive range. Both
firstandlastare included. The result is sorted ascending by run number.Padding-agnostic. The run number is parsed from the trailing digits of each file name, so any zero-pad width matches —
MUSR00001276.nxsresolves to run1276.Prefix.
prefix(the leading text before the run-number digits) is matched case-insensitively. When omitted it is auto-detected: if the files in range all share one prefix it is used, and if several prefixes are present aValueErrorlists them so you can pass an explicitprefix=.Extensions. Only files with a loader-registered extension are considered (so sidecar logs such as
.mon/.txtare ignored). Passext="nxs"to restrict the scan to a single format.Missing runs are skipped. Gaps in the range are not an error — the resolver returns the runs that exist and omits the rest. The GUI separately warns when the loaded runs are non-contiguous.
Errors. A missing or non-directory folder, or
first > last, raisesValueError. A valid folder with no matching runs in range returns an empty list (the GUI reports this as “no run files found”).
Direct file format access
For advanced users, you can access the low-level file loaders. For NeXus files:
from asymmetry.core.io.nexus import NexusLoader
loader = NexusLoader()
dataset_or_periods = loader.load("HIFI00206453.nxs")
For ROOT files:
from asymmetry.core.io.root import RootLoader
loader = RootLoader()
dataset = loader.load("lem15_his_2994.root")
Loader registry and custom formats
LoaderRegistry maps file extensions to loader classes.
from asymmetry.core.io.base import LoaderRegistry
print(LoaderRegistry.supported_extensions())
print(LoaderRegistry.file_dialog_filter())
You can register custom loader classes at runtime for additional formats.
Runnable examples
See the executable scripts:
examples/basic_dataset_loading.pyexamples/custom_loader.py