Simulate a chopper cascade with tof¶
tof is a lightweight straight-line Monte Carlo from the
scipp developers, for chopper-cascade diagrams. It assumes neutrons travel in straight
lines and simulates neither absorption nor scattering — it will never replace McStas, and
it answers "what does my chopper train let through?" in a second rather than a coffee
break.
niess.tof builds a ready-to-run tof.Model from an instrument you have already
assembled, so the train does not have to be retyped.
Build a model¶
from mccode_antlr import Flavor from mccode_antlr.assembler import Assembler from niess.teaching import Primary from niess.tof import to_tof_model
assembler = Assembler('teaching', flavor=Flavor.MCSTAS) Primary.from_calibration().to_mccode(assembler)
setup = to_tof_model(assembler, neutrons=20_000)
In a notebook, displaying setup renders the table below. Everything it used came¶
from the instrument, so nothing has to be supplied -- but the knobs are listed so¶
you know which ones there are.¶
print(setup)
result = setup.model.run()
TofSetup: 1 chopper(s), 2 detector(s)
chopper chopper 14.000 Hz anticlockwise 1 opening(s) at 6.8100 m
detector monitor
detector sample_origin
parameters used (override with with_values(...)):
chopperspeed = 14.0 Hz (default) <- chopper.speed
chopperdelay = 0.0 s (default) <- chopper.delay
nothing has to be provided; every value came from the instrument itself.
Every monitor becomes a tof.Detector, and so does the sample — the places a cascade
diagram usually wants a curve. Distances are walked along the beam, following a curved
guide rather than cutting across it, and offset by the source's own distance because tof
measures every component from the same zero as its source.
What you need to provide¶
Usually nothing, which is what the table says. niess declares each chopper's speed and delay as an instrument parameter carrying the calibration's own value, so a model built from a niess instrument is already the machine as calibrated.
The knobs are listed anyway, because knowing which ones exist is the point of asking, and the report names what read each one — so a value that looks wrong can be traced to the component that used it. Turn one with:
faster = setup.with_values(chopperspeed=70.0)
with_values rebuilds from the same instrument, so the report then marks chopperspeed as
given rather than defaulted.
Anything the walk left out — a Fermi chopper, a disc whose description did not reduce to numbers — is listed too, rather than quietly missing.
Simulating more than one pulse¶
A chopper turning at a fraction of the source frequency does nothing visible in a single
pulse, because a single pulse is the one it lets through. pulses= runs several:
setup = to_tof_model(assembler, pulses=2)
counts = setup.model.run().detectors['sample_origin'].toa.data
At ESS' 14 Hz, a bandwidth disc at 14 Hz opens once per pulse and passes them all, while the same disc at 7 Hz opens for every other one and absorbs the rest — the second pulse arrives at a closed disc and the count for it is zero. That is pulse skipping, and seeing it is the reason to ask for more than one pulse.
neutrons= sets how many are sampled from each pulse, and seed= fixes the sampling so
two runs differ by what was changed rather than by which neutrons were drawn. Omitting
pulses takes the count from the source.
Where the numbers come from¶
The same place niess.chopcalc gets them: the emitted instrument, read through niess
provenance. chopcalc extracts a chopper train to narrow a source's wavelength band, and
emits parameter names so the band recomputes at run time. tof configures one specific
machine, so niess.tof reuses that extraction and evaluates it — which means the disc
grouping, the beam-path walk and the opening-angle conventions are shared with chopcalc
rather than written a second time.
The one thing that is not shared is the conversion into tof's own description, because
the two disagree about how a chopper is specified:
| niess | tof |
|
|---|---|---|
| speed | signed, the sign is the direction | non-negative, direction is separate |
| timing | delay, in seconds |
phase, an angle |
| angles | from the disc's zero mark | from the beam |
A delay is a time, so it delays the opening whichever way the disc turns, and the phase
angle it becomes — 360 · |speed| · delay — carries no sign flip. A NeXus phase is an
angle in the disc's rotating frame and does flip, which is why tof.Chopper.from_nexus
negates it for a negative speed. Conflating the two is the inviting mistake, so the
conversion is pinned by checking tof.Chopper.open_close_times() against niess' own rule
for both directions on an asymmetric disc, where nothing lands back on itself.
Staying offline¶
Building the source downloads a pulse profile on first use, cached afterwards. The profile
follows the instrument's own name — bifrost gets ess-bifrost, an instrument without one
falls back to ess. Pass your own source to avoid the download entirely:
The instrument's Lmin/Lmax are then not consulted, and the report says so by not
listing them.