An ESS_butterfly source that emits only where a chopper train lets neutrons through.
Identification
Author: Gregory Tucker
Origin: European Spallation Source ERIC
Date: 2026
Description
`choppers` is a `chopper_parameters` array reinterpreted as `double *`, which is the
only pointer a McStas SETTING PARAMETER can carry. It was four doubles per chopper
before chopper-lib 4.0.0 and is a structure holding a pointer now, so an instrument
that really did hand over a flat array of numbers must build the structures instead:
double edges[] = {-1.8, 1.8};
chopper_parameters pars[] = {{14.0, 0.02, 0.0, 2, edges, 10.0, 4.0}};
chopper_ptr = (double *) pars;
chopper_cnt = sizeof(pars) / sizeof(chopper_parameters);
`edges` is a flat, increasing list of angles in degrees, two per opening, in the disk's
own frame; `beam` is the angle on the beam path at `delay`; and `aperture` is how wide
the beam is on the disk, in degrees about its spindle, which widens every window in time
and is what a mask for anything but a pencil beam needs. Leave it off and the row still
compiles, describing the point beam it described before. See the chopper-lib README.
What the mask is spent on: `resample`
By default a ray the mask excludes is ABSORBed. That is correct -- the discs would have
stopped it -- but a train that passes a percent of the plane then spends ninety-nine
percent of `ncount` on rays that die where they were born, and the run has the statistics
of one a hundredth the size.
`resample = 1` draws the excluded ray again, from inside the allowed region, instead of
throwing it away. This is the same measurement, not an approximation, because the source
samples the two coordinates the mask is drawn in uniformly and independently:
`lambda = Lmin + range * rand01()` with `1/v = lambda V2K / 2 pi`, and
`t = rand01() * tmax_multiplier * ESS_SOURCE_DURATION`. Restricting a uniform draw to a
region and multiplying the weight by that region's share of the whole -- chopper-lib's
`acceptance` -- leaves every downstream estimator unbiased. Every emitted ray is
multiplied by it, the ones that landed in the allowed region unaided included.
Two things this factor is not, both easy to reach for: it is not
`chopper_unmasked_probability`, which is the allowed fraction of the *weighted* signal
and so the transmission rather than the normalisation; and it is not recoverable from
counting rejection attempts, since `E[1/k]` is not `1/E[k]`.
The independence the argument rests on fails under time focusing, and INITIALIZE refuses
`resample` there rather than quietly biasing the answer: the emission time is then drawn
in a window centred on `tfocus_time - tfocus_dist / vz`, which moves with the neutron's
own velocity. No single factor corrects that -- but none is needed, because time focusing
already *is* this trick done exactly. The window traces a band of slope `-tfocus_dist` in
the (1/v, t) plane, which is the shape of a chopper band, and `w_tfocus` is already the
compensating weight. Set the three `tfocus_*` parameters from the pulse-shaping disc and
the source restricts itself.
Under MPI
The three grids this component keeps are summed across the nodes in SAVE and written by
master alone -- every node runs SAVE, and they all share one output directory, so
ungated writes would put one interleaved copy per node into each file. The acceptance is
not summed and must not be: it comes from the mask's geometry, so every node has the same
number already. Nor is anything divided by the node count, because the ray weights carry
one over the *whole* run's ncount: ESS_butterfly reads mcget_ncount() in INITIALIZE,
which mccode_main runs before it slices ncount across the nodes.
MPI is the parallelism this has been run under. The grids are accumulated atomically, so
OpenACC threads are safe too.
Resampling changes only how the surviving rays are drawn, never which region survives.
It inherits the mask's own approximations -- bin quantisation, `mask_grow`, the disc
`aperture` -- exactly, and adds none.
What the grid has to span
Neither of the two coordinates is read straight off the ray, and getting either wrong
absorbs the beam rather than masking it.
Under time focusing the emission time is drawn in a `tfocus_width` window whose centre
slides with the neutron's inverse velocity, so the band it sweeps is
`tfocus_dist * inverse_velocity_range` longer than the window; a grid sized by
`tfocus_width` alone sits around the slowest neutron's window and nowhere near the rest.
And ESS_butterfly adds the offset of a randomly chosen pulse to `t` before this component
sees it, so with `n_pulses > 1` what arrives is not an emission time at all. The grid is
built over the whole swept band clipped to the pulse, and the pulse offset is taken back
off in TRACE -- which needs the emission window to be shorter than the gap between
pulses, so INITIALIZE checks that too.
Input parameters
Parameters in boldface are required;
the others are optional.
Name
Unit
Description
Default
choppers
chopper_parameters,...
information about the chopper train (see above)
chopper_count
1
the number of choppers described
inverse_velocity_bin
s/m
the width of mask bins in 1/v
time_bin
s
the width of emission time bins
filename
str
the base name of any output files, replaced by NAME_CURRENT_COMP if missing
0
noise_fraction
1.
accept out-of-mask rays with this probability
0.0
mask_grow
1
expand the chopper-accepted mask by this many bins in each direction
1
use_mask
1
use (1) or do not use (0) the calculated mask for limiting source emission
1
resample
1
draw (1/v, t) directly from the mask, 100% ray emission from source
0
save_mask
1
output the binary mask as {filename}.mask
1
save_total
1
output the probability drawn from the source before masking as {filename}.total
1
save_emitted
1
output the probability emitted after masking as {filename}.emitted
1
save_count
1
output the number of drawn rays before masking as {filename}.count
1
verify_count
1
report the fraction of drawn rays landing inside the mask, which measures the acceptance that resample=1 corrects every ray weight by, and should match it -- it is counted from the run, where the acceptance comes from the mask geometry alone, so the two agreeing is a real check