Fork of the ANL MUSIC simulator (originally by Daniel Santiago-Gonzalez). Stopping powers are computed using catima.
Dependencies are managed by Nix.
nix build
./result/bin/musicsim ControlExamples/37Cl_alpha_n/37Cl_alpha_n_bulk.tomlFor iterative development:
nix develop
make
./musicsim ControlExamples/37Cl_alpha_n/37Cl_alpha_n_bulk.tomlControl files are TOML. Sections and keys are case-sensitive. Every section is optional. Anything you don't write will be its compiled-in default. Example:
[gas]
species = "4He"
pressure = 220.0 # Torr
temperature = 293.0 # K
#
[beam]
species = "37Cl"
energy = 92.0 # MeV at the accelerator (before windows)
energy_fwhm = 0.90 # MeV FWHM
dedx_scale = 1.0
#
[target]
species = "4He"
compound = "41K"
#
[windows.entrance]
material = "Ti"
thickness_mg_cm2 = 0.9 # alternatively: thickness_um = X
#
[windows.exit]
material = "Ti"
thickness_mg_cm2 = 1.3
#
[windows.degrader]
material = "Mylar"
thickness_um = 6.0 # alternatively: thickness_mg_cm2 = X
#
[detector]
eloss_bins = 555
max_eloss = 10.0 # MeV
strip_first = 3 # alternatively: strip = -1 for unreacted-beam runs
strip_last = 13
eres = 5.0 # % FWHM of the channel's deposit; scalar broadcasts
# to all 34 anode electrodes. -1 disables. Does NOT
# set cathode noise — use the [detector.eres] table
# form for that.
# Per-channel form (mutually exclusive with the scalar `eres` above — pick
# one). Keys: Cathode, S0, S17, L1..L16, R1..R16 (35 channels total). Any
# missing key keeps its default of -1 (no noise on that channel).
# [detector.eres]
# Cathode = 5.0
# S0 = 4.0
# L1 = 6.0
# R1 = 5.0
# ...
# R16 = 0.05
# S17 = 0.04
#
[[reaction.step]] # add more [[reaction.step]] for decay chains
[reaction.step.evap]
name = "4He"
color = 2
dedx_scale = 1.0
[reaction.step.res]
name = "37Cl"
color = 4
dedx_scale = 1.0
#
[run]
n_events = 10000
threads = 8
wait = 0
update = 0
max_time = 2000.0 # ns
sim_step = 0.001 # cm
method = 0
output = "traces_37Cl_aa_bulk.root"
file_opt = "recreate"
print_opt = 0The per-event beam-energy chain is:
beam.energy ± beam.energy_fwhm (accelerator) → degrader (if any) → entrance
window → Kbi (gas surface). At the degrader and each window the beam
traverses a single thick step, which puts the Vavilov parameter κ comfortably
into the Bohr regime (κ ≫ 10) where Vavilov has already collapsed to a
Gaussian — so straggling there is sampled as a single Gaussian draw from
catima's sigma_E.
The accelerator energy spread, the degrader straggling,
and the entrance-window straggling are independent Gaussians and add in
quadrature naturally.
Particles that exit the downstream face of the gas are propagated through the
exit window before their energy is recorded as Kbeam_exit / Kh_exit[] /
Kl_exit[] on the MC tree.
Inside the active gas the propagator takes ~10 μm steps by default, which puts the
per-step Vavilov parameter κ ≈ 0.05–0.5.
In that band the symmetric Bohr
Gaussian that catima exposes via sigma_E is the wrong distribution shape; using it would induce warnings about unphysical energy loss.
The real per-step energy-loss spectrum is asymmetric, with a long high-loss
tail from δ-electron knock-ons and no negative-loss left tail.
Each step
samples instead from the full Vavilov distribution Φ(λ_V; κ, β²) at the
step's κ and β², standardised against catima's mean dE and σ_E so the first
two moments still match catima exactly.
Per-step samples are clamped to
[0, Ki] (energy can only decrease via dE/dx, never increase).
The sampler uses the convolution decomposition of Yi & Han1 —
Φ(λ_V; κ, β²) factors as Φ(λ_V; (1−β²)κ, 0) ⋆ Φ(λ_V; β²κ, 1), so a draw
at any (κ, β²) is the sum of two independent draws from 1-D κ-indexed
tables.
The tables (src/VavilovSampler.cpp) are pre-filled once at startup
from ROOT's Math::VavilovAccurate (~3 ms of one-time work) and looked up
via bilinear interpolation in the hot path — no per-step VavilovAccurate
construction. Outside the Vavilov band (κ < 10⁻³ or κ > 10) the sampler
falls back to a Gaussian, matching the Landau and Bohr limits respectively.
Set one of: detector.strip = N, or both detector.strip_first and
detector.strip_last. strip = -1 runs unreacted-beam (no reaction vertex).
catima exposes a couple of physics-model switches that can shift dE/dx
predictions noticeably for heavy ions at low velocity. They're set through
an optional [physics] section:
[physics]
z_effective = "atima14" # default
low_energy = "srim_95" # default
straggling_z_effective = "pierce_blann" # default
straggling = true # defaultz_effectivecontrols the projectile-charge-state model. Valid:none,pierce_blann,anthony_landorf,hubert,winger,schiwietz,global,atima14. catima's compiled default ispierce_blann;atima14is used here.low_energycontrols the table used below the Bethe-Bloch regime (where 37Cl in Ti sits at ~0.3 MeV/u, for instance). Valid:srim_85(catima's compiled default) orsrim_95(newer SRIM/Ziegler curves; default here).straggling_z_effectivecontrols the charge-state model used only to obtain the energy-loss straggling σ_E (for the gas, windows, and degrader alike). It is decoupled fromz_effectivebecause catima'satima14model returns σ_E = 0 — that code path computes the mean dE/dx but never populates the straggling variance, so leaving straggling onatima14would silently zero all energy-loss fluctuations (the per-step gas Vavilov width and the window/degrader Gaussians are both scaled by catima's σ_E). σ_E is only weakly charge-model-dependent, so pairing theatima14mean with apierce_blannσ_E is well-behaved. Accepts anyz_effectivevalue exceptatima14(rejected at load with an error).stragglingis the master switch.falsedisables energy-loss straggling everywhere — the per-step Vavilov sampling in the gas and the Gaussian window/degrader smearing — so every energy loss is the catima mean. Useful when[detector.eres]carries measured peak widths that already include the physical fluctuations. Detector noise (eres) is unaffected.
Any of the four physical layers can be turned off independently — the simulator prints a warning at startup and skips both the mean dE/dx and the straggling for that layer.
| Disable | How |
|---|---|
| Gas energy loss + straggling | gas.pressure = 0 (or any non-positive value) |
| Entrance window | Set thickness_mg_cm2 (or thickness_um) to -1 under [windows.entrance] |
| Exit window | Same, under [windows.exit] |
| Degrader | Omit the [windows.degrader] block, or set its thickness to -1 |
Each of the three layer sub-tables ([windows.entrance], [windows.exit],
[windows.degrader]) accepts exactly one of thickness_mg_cm2 (areal
density, mg/cm²) or thickness_um (linear length along the beam axis, μm).
Setting both in the same sub-table is an error.
Pick whichever matches the
spec sheet for the physical layer; the loader dispatches to catima's areal
or linear thickness path accordingly.
run.threads = N fans the event loop out across N worker threads using
std::async.
Each worker is a separate MUSIC_Simulator instance with its
own RNG seed and per-worker ROOT output; the master pre-warms catima's
internal cache and then merges the per-worker output files with TFileMerger
into the configured run.output at the end of the run.
Workers force
update=0/wait=0, so visualization is only available in single-threaded
mode (the default, threads = 1).
reaction.step is an array of tables — add more [[reaction.step]] blocks
for multi-step decay chains; up to MaxNumEvapPart (10) are supported.
evap.dedx_scale and res.dedx_scale are optional and default to 1.0.
Particle names use AEl notation (e.g. 37Cl, 4He, 1H, n).
The pre-TOML .msc format used by the original ANL musicsim isn't accepted by
this fork.
A python converter is included:
tools/legacy_msc_to_toml.py path/to/upstream/file.msc
# writes path/to/upstream/file.toml; per-key comments are preserved.Glob-friendly: tools/legacy_msc_to_toml.py /path/to/musicsim/Examples/**/*.msc.
Kb is mapped to beam.energy with an inline warning — the meaning shifted
from "gas-surface KE" (upstream) to "accelerator KE before windows" (Remix),
so adjust the value by hand if you want the windows modelled.
SRIM-table keys
and AnodeGeom are dropped (catima handles dE/dx; geometry is hardcoded).
Each run produces one ROOT file with two trees, mirroring the branch layout of
the experimental events tree produced by the
MUSIC EventBuilder:
events_MeV— detector-level branchesLeft_0_17_dE[18]andRightdE[18](Float, MeV), plus a scalarCathode.Left_0_17_dE[s]holds the left end of strips 1–16; the single-ended guard strips 0 and 17 carry their full energy inLeft_0_17_dE[0]/Left_0_17_dE[17]withRightdEzero there, so the strip total isLeft_0_17_dE[s] + RightdE[s]everywhere (no separateTotaldEbranch, matching the data tree). Energies are MeV truth (hence the_MeVsuffix vs the experimental ADC-valuedeventstree), so analysis macros should calibrate data to MeV rather than rescaling sim to ADC. The DAQ-onlyHits/Grid/FlagsORbranches of the experimental tree have no truth analog and are not emitted.MC— truth-only branches, friended toevents_MeV: reaction strip, beam kinematics (BeamEnergyAccelat the accelerator,Kbiat the gas surface,Kbrat the reaction point,Kbeam_exitafter the exit window), light/heavy four-vector components, vertex and exit positions, per-evap exit energies (Kl_exit[],Kh_exit[]), and dead-layer energy depositsDeadUS_dE/DeadDS_dE(MeV) for the unread gas regions upstream and downstream of the readout strips. Sentinel values on exit-energy branches:-1= particle stopped in the gas,-2= no such particle for this event (e.g.Kbeam_exiton a reacted event). A literal0means the particle reached the exit window but stopped inside it.
Parts of this fork — the catima migration, std::async multi-threading, window / degrader plumbing, and most boilerplate around them — were written with the help of Claude Code. All changes to the simulation physics (kinematics, energy-loss chain, straggling sampling) are human-reviewed and approved before being committed.
Footnotes
-
Chul-Young Yi, Hyon-Soo Han, A Monte Carlo algorithm for the Vavilov distribution, Nucl. Instr. and Meth. in Phys. Res. B 149(3), 263–271 (1999). doi:10.1016/S0168-583X(98)00803-9. ↩