Synthetic runs and degraded statistics
Asymmetry can manufacture a complete synthetic run — per-detector count histograms, grouping, provenance, and a loadable NeXus file — from any fit model, and can resample a measured run to a lower statistics level. Both tools draw Poisson counts at the histogram level, so the synthetic data flow through exactly the same reduction chain (grouping, α balance, error propagation, binning) as beamline data, and everything downstream — fitting, Fourier analysis, MaxEnt — behaves as it would on a real measurement.
Generate a synthetic run
When to use this. Three situations call for synthetic data. Fit validation: before trusting a fit on real data, check that the analysis recovers known parameters — simulate from the fitted model, refit, and compare. Teaching: produce realistic datasets for any relaxation or oscillation function without beam time. Method development: manufacture test inputs with controlled statistics for binning, Fourier or fitting studies. It is not a substitute for calibration measurements — α, t0 and backgrounds in a synthetic run are inputs, not measurements.
Open File → Generate Synthetic Run… with at least one run loaded. The loaded run acts as the instrument template: the synthetic run copies its detector count, time binning, per-detector t0, good-bin window, detector grouping and α. The dialog seeds its model and parameter values from the template run’s current fit when one exists; otherwise pick any time-domain model with Edit Model… (the same builder used by the fit panel).
The Generate Synthetic Run dialog opened with no run loaded, so the Template run list falls back to the two idealised instruments — here Built-in: Ideal pulsed F/B (ISIS-style) . The Model A(t) is a damped Larmor precession (an Oscillatory × Exponential composite) at ν ≈ 22 MHz, a below-Tc EuO signal from the archetype gallery, with its parameter table populated in percent. The Total events budget pre-fills from the chosen instrument, the Generation mode is Forward/backward asymmetry, and Fixed seed keeps the draw regenerable.
Each detector in the forward group accumulates expected counts
with the backward group seeing \(1 - a(t)\), where \(a(t)\) is the chosen model (the dialog works in percent, matching the fit panel), \(\tau_\mu = 2.1969811\) μs, and \(b\) is an optional flat background per bin. The α balance enters as the relative efficiency of the two groups — forward detectors are weighted by \(2\alpha/(1+\alpha)\) and backward by \(2/(1+\alpha)\) — so the reduction \((F - \alpha B)/(F + \alpha B)\) recovers \(a(t)\) with the template’s α restored. The recorded counts are independent Poisson draws of these expectations; bins before t0 contain only the background. Total events sets the expected event budget over the histogram window (the realised total fluctuates, as it would on the beamline), and defaults to the template run’s own statistics so the synthetic noise level matches the data you are used to looking at.
The result appears in the Data Browser as SIM 90001 (and counting),
tinted and carrying a tooltip with its provenance: the generating model,
parameter values, seed, event budget and template run. The run’s metadata
records the same information permanently. Save as NeXus… writes the run
as a standard ISIS muon NeXus file that reloads through Open Data
File(s)… with identical counts — this is also how a synthetic run persists
across sessions, since project files reference data files rather than
storing histograms. A synthetic run that has not been saved this way is held
in memory only; saving a project that still references one raises a warning,
because the project will not be able to reload it.
Reproducibility is exact: the same template, model, parameters, events and seed produce bit-identical counts on any machine. Leave Fixed seed checked when you want a regenerable dataset (the seed is recorded in the provenance either way); untick it to draw a fresh seed per generation.
Two caveats worth knowing. The ISIS NeXus format has no field for α, so a
reloaded synthetic file starts from the loader’s default balance, exactly
as real data would — the generating α is recorded in the file’s
simulation group for reference. And the synthetic counts contain no
deadtime distortion, so the file’s deadtimes are correctly written as zero;
do not enable deadtime correction for synthetic runs.
Generation modes: asymmetry, counts, and periods
When to use this. The Generation dropdown chooses what kind of synthetic run to make. Leave it on the default for asymmetry work; switch it when you need raw single-histogram counts for a count-domain fit, or a red/green period structure.
Forward/backward asymmetry (default) is the run described above: the antisymmetric \(\pm a(t)\) split across the forward and backward groups, reduced to an asymmetry curve. This is what feeds the standard fit panel and the α-free forward/backward count fit, which needs the F/B pair to separate the balance \(\alpha\) from the amplitude.
Count histograms (single-group) imprints the same \(+a(t)\) on every group, so each detector group is an independent single-histogram measurement
with no balancing backward detector. This is the data the single-histogram count fit expects — fit any one group and recover its \(N_0\), the amplitude inside \(a(t)\), and the flat background. Use it for longitudinal-field / zero-field (LF/ZF) single-detector work, or to manufacture a clean test case for the count-domain fit modes.
Two-period (red/green) generates two period histograms in one run, the way a light-on/off or RF-on/off measurement is recorded. Red carries the full model; the Green amplitude (×) factor scales green’s signal — leave it at \(0\) for a flat reference period (the usual light-off case), so the green-minus-red combination \(G - R\) recovers the red signal cleanly. Each period is an independent Poisson draw from one seed. The run lands in the Data Browser with the full red/green machinery: pick the period or the \(G \mp R\) combination in the grouping dialog exactly as for a loaded period-mode file. Save-as-NeXus flattens a two-period run to its red period — period-mode synthetic runs live in memory and through the project, not on disk.
Built-in instrument templates
When to use this. Teaching and quick experiments with no data to hand. The dialog opens without a loaded run and offers two idealised instruments that supply the geometry a real template would, so you can go straight from a model to a synthetic run.
The two built-ins are the contrasting source archetypes of the field. The ideal pulsed F/B mirrors an ISIS-style spectrometer — 32 forward and 32 backward detectors, 16 ns bins over a 32 μs window, and no uncorrelated background, since a pulsed source has essentially none. The ideal continuous F/B is a PSI-style pair with fine 1 ns binning over a short 10 μs window and a flat background of about 10 counts/bin/detector: the time-independent uncorrelated background that characterises a continuous source, where muons arrive one at a time and the time-zero clock runs free. Select either from the Template run list; the event budget and background spinners pre-fill with sensible values for the chosen instrument. Everything downstream — generation, the NeXus round trip, refitting — behaves exactly as for a loaded-run template.
The archetype gallery
When to use this. A one-click route to a physically realistic dataset for a named material, for teaching or for a sanity check that the analysis chain reproduces textbook behaviour. File → Simulate Preset lists each archetype; choosing one generates a badged synthetic run (or a whole scan) from a fixed seed, so the same preset always yields the same data.
The presets and the physics they carry:
Ag — ZF Gaussian Kubo–Toyabe. The canonical static nuclear-dipolar reference, \(\Delta = 0.39\) μs-1.
Ag — LF decoupling series. The same Kubo–Toyabe in longitudinal fields of 0, 10, 25 and 50 G, the textbook test that the dipolar fields are static.
EuO — ferromagnet through Tc. A zero-field temperature scan across \(T_c = 69\) K: below it the muon precesses at a frequency that tracks the magnetic order parameter \((1 - T/T_c)^\beta\); above it the signal is paramagnetic relaxation that broadens through the critical region.
PbF₂ — F-μ-F entanglement. The dipolar beat pattern of a muon bound between two fluorine neighbours at \(r \approx 1.17\) Å.
YBCO — transverse-field precession. Knight-shifted Larmor precession in a 200 G transverse field.
Because each preset is a real synthetic run, you can refit it and recover the input physics. Fitting the Ag preset, for instance, returns \(\Delta = 0.389(1)\) μs-1, and the F-μ-F preset returns \(r_{\mu\mathrm{F}} = 1.170(1)\) Å — the generating values, within the counting errors. The parameter values follow Blundell et al. (see the references); the preset tooltips name the relevant chapter.
Multi-group simulation
When to use this. Synthesising a transverse-field ring whose detector groups see the same precession at different phases — for instance to manufacture test data for the multi-group time-domain fit, or to show how the relative phase around a ring maps onto the geometry. File → Generate Multi-Group Run… opens a per-group table of amplitude, relative phase and relative count rate, over a shared normalised polarisation model.
The forward/backward dialog applies one signal with the α split; here each group \(g\) instead carries
the shared polarisation \(P(t)\) scaled by the group’s own amplitude \(A_g\) and shifted by its own phase \(\phi_g\). The amplitude owns the overall scale and the count-rate weight owns the background, so the shared model is a unit-amplitude, zero-baseline polarisation — exactly the contract the grouped time-domain fit expects. If you have just run a grouped fit on the selected run, the table is seeded from that fit’s per-group amplitudes and phases, so “simulate what I just fitted” is one click. The reduction recovers each group’s seeded signal bin-for-bin in expectation.
Degrade statistics
When to use this. Answering “would half the beam time have been enough?” — for planning proposals, for checking that a marginal feature survives at realistic statistics, or for testing how a fit degrades as counts shrink. Because the thinned run is a new entry beside the original, a single measurement yields a whole family of statistics levels.
Right-click a run in the Data Browser and choose Degrade Statistics…. A factor \(f < 1\) keeps each recorded count independently with probability \(f\) — binomial thinning. Thinning a Poisson process is exactly Poisson, so the result is statistically indistinguishable from a measurement \(f\) times as long: per-bin errors grow as \(1/\sqrt{f}\), with no approximation. The derived run appears beside the source (which is never modified), badged with its factor, seed and parent run, and can be saved as NeXus like any synthetic run.
A factor \(f > 1\) is also accepted but is an extrapolation, not a simulation: drawing \(\mathrm{Poisson}(f k)\) from the recorded \(k\) inflates the variance by roughly \(1 + f\) relative to a genuinely longer measurement. Use it for qualitative “more statistics” sketches only — never for quantitative sensitivity claims.
Validating error bars: the pull distribution
When to use this. After a fit, to ask the sharpest question you can ask of any analysis chain: are the error bars honest? A fitted value is only as trustworthy as its uncertainty, and the only way to test an uncertainty is to repeat the measurement many times and see whether the scatter matches. The pull diagnostic does exactly that, with synthetic repeats.
After a converged single fit, click Pull diagnostic…. The tool re-simulates the fitted run many times at matched statistics, refits each copy over the same window, and for every free parameter forms the pull
where \(\theta_{\mathrm{true}}\) is the value you fitted and
\(\sigma_{\hat\theta}\) the error each refit reports. For a sound chain the
pulls are standard normal. Read the histogram against its \(N(0, 1)\)
overlay two ways: the mean should sit at zero (a non-zero mean is a
bias — the fit systematically misses), and the width should sit at one.
A width below one means the reported errors are too large; above one, too
small. The verdict line states this for each parameter, with the measured
width and its uncertainty, e.g. width 1.02(7) — well-calibrated.
Asymmetry’s reduction uses exact Poisson error propagation for the asymmetry, \(\sigma_A^2 = (1 - A^2)/(F + \alpha B)\)-style rather than the independent-numerator-and-denominator form, so the pull widths centre on one with no fudge factor — the same check the test suite runs on every build.
Scripting
The same machinery is available without the GUI, in
asymmetry.core.simulate:
from asymmetry.core.io import load
from asymmetry.core.io.nexus_writer import write_nexus_v1
from asymmetry.core.simulate import degrade_run, reduce_run_to_dataset, simulate_run
from asymmetry.core.fitting.composite import CompositeModel
template = load("EMU00020722.nxs").run
model = CompositeModel.from_expression("Oscillatory * Exponential")
params = dict(model.param_defaults) | {"frequency": 1.35, "Lambda": 0.21}
run = simulate_run(template, model, params, total_events=40e6, seed=7)
dataset = reduce_run_to_dataset(run) # browser-ready asymmetry curve
write_nexus_v1(run, "SIM90001.nxs") # loadable through load()
half = degrade_run(run, 0.5, seed=1) # "half the beam time"
No run to hand? Build one of the idealised instruments instead of loading a file:
from asymmetry.core.simulate import build_builtin_template, simulate_run
template = build_builtin_template("ideal_pulsed_fb") # or "ideal_continuous_fb"
run = simulate_run(template, model, params, total_events=40e6, seed=7)
The textbook archetypes are one call each, returning the run (or the whole scan family), badged and ready to plot or fit:
from asymmetry.core.simulate_presets import build_preset_runs
ag = build_preset_runs("ag_zf_kt")[0] # single run
euo_scan = build_preset_runs("euo_tscan") # five temperatures
Count-mode and two-period synthesis have their own entry points. Count-mode
returns a run whose per-group histograms feed the single-histogram count fit;
two-period takes a PeriodSpec for red and
green and returns the loadable red/green payload.
from asymmetry.core.simulate import (
PeriodSpec, simulate_count_run, simulate_two_period_run,
)
counts = simulate_count_run(template, model, params, total_events=40e6, seed=7)
# Red carries the full model; green a half-amplitude variant (scale=0 makes
# a flat reference period, so G−R recovers the red signal). Each period can
# instead pass its own model/parameters/alpha/total_events.
red = PeriodSpec(model, params)
green = PeriodSpec(model, params, scale=0.5)
periods = simulate_two_period_run(
template, [red, green], total_events=40e6, seed=7,
)
Multi-group simulations (a different amplitude and phase per detector group —
phases around a transverse-field (TF) ring, for instance) use
asymmetry.core.simulate.simulate_multi_group_run() with a
GroupSignalSpec per group; the
forward/backward dialog exposes only the single-signal α-split case.
from asymmetry.core.simulate import GroupSignalSpec, simulate_multi_group_run
ring = CompositeModel(["Oscillatory"]) # normalised polarisation
specs = [GroupSignalSpec(g, amplitude=0.2, relative_phase=phi)
for g, phi in [(1, 0.0), (2, 1.57), (3, 3.14), (4, 4.71)]]
run = simulate_multi_group_run(template, ring, specs,
total_events=40e6,
base_parameters={"frequency": 2.7})
A pull-distribution check makes the strongest validation pattern, and
asymmetry.core.pull_diagnostic.run_pull_distribution() packages it:
simulate many seeds from one parameter set, refit each, and histogram
\((\hat\theta - \theta_{\mathrm{true}})/\sigma_{\hat\theta}\). For a
healthy analysis chain the pulls are standard normal; widths persistently
below one flag over-estimated errors and above one under-estimated errors.
Asymmetry’s own test suite runs exactly this check.
References
S. J. Blundell, R. De Renzi, T. Lancaster, and F. L. Pratt, Muon Spectroscopy: An Introduction (Oxford University Press, Oxford, 2022), Chs. 14–15.
F. L. Pratt, Physica B 289–290, 710 (2000) — WiMDA, whose Simulate and Degrade Statistics tools this feature parallels.