Statistical acceptance in mctest
← back to overview · mctest
Statistical acceptance in mctest
mctest can judge a test value against its target using the Monte Carlo error
bars that every McStas and McXtrace run prints, instead of only a fixed ±20 %
window. This note explains the rule, the assumptions behind it, and where it can
mislead.
TL;DR
- Nothing changes unless you ask for it. Without new options, mctest still
passes a value within ±20 % of its target.
mctest --nsigma 5 also passes a value that is within 5 error bars of its
target. This helps noisy, low-statistics tests that miss ±20 % by bad luck. It
never fails anything ±20 % would pass, as long as targets carry no error bar
(and none do yet).
- Error bars from too few particles are ignored. Fewer than ~100 effective
events, and mctest falls back to ±20 %, so a nearly empty run can't slip through.
--pvalue is the same threshold for people who think in p-values:
--pvalue 5.7e-7 is --nsigma 5.
--statfactor F runs every test with F times the particles: 0.1 for a
quick check, 100 for reference-quality numbers.
- Use 5, not 3. A full test run judges thousands of values, and at 3σ a few
of them would fail by chance alone every time.
- Error bars can lie. Instruments using
SPLIT report error bars that are
too small, and some targets are a few per cent off for non-statistical
reasons. That's why ±20 % stays in place for now.
What a test compares
Each test definition in an instrument header gives a target intensity for one
monitor, either as a single value (%Test:, also accepted under its older name
%Example:) or as one value per point of a parameter scan (%TestScan:). A test
run produces, for that monitor, the intensity I and its error bar ERR, as in
the simulation's own summary line:
Detector: transmitted_I=2.00752e+08 transmitted_ERR=2.28872e+06 transmitted_N=50752 "transmitted.L_U1"
mctest reads I and ERR from the monitor file's # values: line, from this
stdout line for NeXus output, or from the NAME_I and NAME_ERR columns of a
scan's mccode.dat. In McCode, I = Σ pi over the N neutron or
photon weights pi reaching the monitor, and
ERR² = N/(N−1)·(Σ pi² − (Σ pi/N)²), which is
Σ pi² to a very good approximation for any N worth testing. A single
event gives ERR = I.
A target may also carry its own error bar, written the same way:
Detector: NAME_I=… NAME_ERR=… on a %Test: line, or NAME_ERR={…} after the
NAME_I={…} list of a %TestScan: line. No instrument does this yet.
The acceptance rule
With Δ = Itest − Itarget and a threshold n given by
--nsigma n (or --pvalue p, see below), each value, or each point of a scan,
is accepted as follows.
| Threshold given |
Target line |
Error bars usable |
Accepted when |
| no |
any |
not used |
within ±20 % of the target |
| yes |
no _ERR |
yes |
within ±20 % or |Δ| ≤ n·√(1+f)·ERRtest |
| yes |
has _ERR |
yes |
|Δ| ≤ n·√(ERRtest² + ERRtarget²), error bars alone |
| yes |
any |
no |
within ±20 % of the target |
Here f is the --statfactor (default 1, giving √2·ERRtest). A scan
passes only if every point passes and the number of points matches the target
list. mcviewtest colours its report by the same rule, using the threshold and
factor stored with each result.
The ±20 % rule is kept as an equal alternative while targets carry no _ERR, so
an error-bar threshold can only widen acceptance, never narrow it. It is dropped
for a target that states its own error bar: such a target is a deliberate
statistical statement and is judged by the error bars alone.
The target's error bar, when it is not recorded
A comparison of two Monte Carlo estimates needs both variances:
σΔ² = σtest² + σtarget². Today's targets were
recorded without their error bar. mctest assumes the target was produced at the
unscaled ncount of the test, so that its error bar is that of the test run
scaled back by the statistics factor:
σ_target = σ_test · √f ⇒ σ_Δ = σ_test · √(1 + f)
Without --statfactor this is σΔ = √2·σtest. Most targets
were in fact made at higher statistics than the default test, so the assumption
usually overstates σtarget and errs towards accepting: it reduces
false alarms, while a real regression still shows up as a large deviation. If a
target came from fewer events than the test, the test becomes stricter than
intended. Recording _ERR on the target line replaces the assumption with the
real value.
When error bars are not trusted
An error bar from a handful of particles is itself very uncertain, and a large
one would let almost any value through. For example, with only a hundred
neutrons, a test value at 28 % of its target is only 2.2σ away. mctest therefore
uses error bars only from enough effective events:
N_eff = (I / ERR)² ≈ (Σ p_i)² / Σ p_i² ≥ 100
Neff is Kish's effective sample size of the weighted particles
reaching the monitor: the number of unweighted events that would give the same
relative error, which is then 1/√Neff, at most 10 % here. Below the
limit, the value is judged by the ±20 % rule, and mctest says so in its log. For
a target with _ERR, its own Neff must pass the limit as well. A zero
result, or a missing error bar, always falls back to the ±20 % rule.
Choosing the threshold
For a Gaussian-distributed Δ, accepting |Δ| ≤ n·σΔ is the same test as
accepting a two-sided p-value of at least p = 2·(1 − Φ(n)). --pvalue p is
converted to n with this relation, so the two options are equivalent ways to
set one threshold.
| n (σ) |
Two-sided p |
Coverage |
Expected false failures per 2000 values |
| 2.17 |
0.03 |
97 % |
60 |
| 3 |
2.7 × 10⁻³ |
99.73 % |
5.4 |
| 4 |
6.3 × 10⁻⁵ |
99.994 % |
0.13 |
| 5 |
5.7 × 10⁻⁷ |
99.99994 % |
0.001 |
The number of comparisons decides the threshold. A full test suite judges a few
thousand values once every scan point is counted, so a per-value threshold that
sounds strict still produces several spurious failures per run at 3σ. The CI
test suites therefore run with --nsigma 5, which keeps the expected number of
false alarms per run far below one.
Scaling the statistics
--statfactor f multiplies the ncount of every test by f: the general ncount
(-n, default 10⁶) for single-value tests, and the per-point ncount of each
scan (the scan line's own -n, else at most 10⁵ per point). Small factors give
quick, rough runs; large factors give runs good enough to record new targets
with their error bars. Run timeouts grow with f, and the factor is part of the
result directory's name.
The test run's own ERR follows the reduced or increased statistics
automatically, so nothing needs to be back-projected from the target. Only the
unrecorded target error depends on f, through the √(1+f) above.
Known limitations
- Correlated weights. ERR assumes that the particle histories are
independent. Instruments that use
SPLIT reuse each history several times,
so their error bars understate the real scatter between runs. Measured
examples: SSRL_bl_11_2_white_src (two SPLIT 10 crystals) scattered 8–12 %
between seeds at 10⁶ against reported errors of 3–5 %; LLB_6T2 scattered
5–10 % against a reported 0.2 %. For such instruments, a pure error-bar test
with a recorded target _ERR would fail spuriously.
- Systematic differences. A target that is a few per cent off for reasons
other than statistics, such as an older code version, fails a pure error-bar
test as soon as the statistics are high. Test_KDSource at full statistics is
3 % below its target, which is −13σ and −42σ for its high-statistics variants.
Such tests pass today only through the ±20 % alternative. Record a target's
_ERR only together with a freshly generated, high-statistics target value.
- Tests whose statistics do not depend on ncount. Instruments that read
their particles from a file (MCPL or KDSource input) give the same value and
error bar whatever the ncount, so
--statfactor changes only the assumed
target error for them. The √(1+f) assumption does not apply there.
- Random streams. With MPI, each rank seeds from the base seed plus its
rank, and scans give each point its own seed. Results therefore differ between
rank counts and from serial runs even with a fixed
--seed, which is why
acceptance must allow for statistical scatter in the first place.
- Gaussian approximation. The threshold assumes Δ is Gaussian. That holds
well above the Neff limit, but not for a handful of events, which is
one reason those fall back to the ±20 % rule.
Results on the CI basic tests
The McStas basic-test instruments at 10⁶ particles with MPI, judged with
--nsigma 5. Deviations use the √2·ERR assumption, and only the tests where the
two rules disagree, or come close, are shown.
| Test |
% of target |
Deviation |
Neff |
±20 % |
Used rule |
| BNL_H8_simple |
76 % |
−1.0σ |
≈19 |
fail |
fail (below Neff limit, ±20 %) |
| Test_KDSource_3 |
97 % |
−13σ |
≈4×10⁵ |
pass |
pass (±20 %) |
| Test_KDSource_4 |
97 % |
−42σ |
≈4×10⁶ |
pass |
pass (±20 %) |
| ESS_BEER_MCPL |
109 % |
+4.6σ |
≈6×10³ |
pass |
pass |
| BNL_H8 |
96 % |
−4.2σ |
≈2×10⁴ |
pass |
pass |
All other basic-test values, McStas and McXtrace, are within 3.7σ and 9 % of
their targets. BNL_H8_simple has too few effective events at 10⁶ to be judged
by its error bar and is a genuinely noisy test; it needs a higher ncount in its
own test line.
With scaled statistics
The same McStas tests plus the 41-point SE_example scans, at
--statfactor 0.1 (10⁵ particles per value, 10⁴ per scan point) and
--statfactor 10 (10⁷ and 10⁶), with --nsigma 5.
| Factor |
Result |
What happened |
| 0.1 |
5 tests fail |
BNL_H8_simple got zero counts, templateSANS_Mantid came out at 149 %, and both SE_example scans had points outside ±20 %. All of these fell below the Neff limit and were judged by the ±20 % rule: at this factor the tests lack the statistics to be judged, which the rule reports instead of hiding. |
| 10 |
all pass |
BNL_H8_simple, which fails ±20 % at the default ncount, comes out at 100.8 %. The scans pass with every point within 11 % of target. |
Test_KDSource and ESS_BEER_MCPL return identical values at every factor, because
they read a fixed particle file. Their reported deviation still changes with f
(Test_KDSource_3: −17.9σ at 0.1, −13σ at 1, −5.7σ at 10), purely through the
assumed target error σtest·√f. This is the limitation for file-input
tests described above.
Usage
mctest --nsigma 5 # error bars, else ±20 %
mctest --pvalue 5.7e-7 # the same threshold as a p-value
mctest --nsigma 5 --statfactor 0.1 # quick run, 10× fewer particles
mctest --nsigma 5 --statfactor 100 # reference run, e.g. to record targets with _ERR
Without --nsigma or --pvalue, mctest behaves as before: every value is
judged by the ±20 % rule.
Last Modified: Friday, 23-Oct-2026 18:32:09 CEST