TraceWin .dst loading¶
The .dst is TraceWin's binary distribution file — used to checkpoint
beams at any point in a simulation, share initial distributions across
machines, and import measured beam data from upstream codes (Toutatis,
PIC simulations, etc.). HELIX reads and writes .dst files
faithfully.
TL;DR¶
from linac_gen.core.config import BeamConfig
from linac_gen.distributions.factory import create_beam
from linac_gen.io.tracewin_dst import write_dst
# Create a .dst to load (normally it comes from TraceWin or a prior
# HELIX run — see "Writing" below):
_src = create_beam(BeamConfig(n_particles=1000, current=5.0), seed=3)
write_dst("initial.dst", _src.particles, current_mA=5.0,
frequency_MHz=352.21, mass_MeV=_src.ref.species.mass,
w_kin_ref=3.0)
beam_cfg = BeamConfig(
species="proton",
source="file",
distribution_file="initial.dst",
)
beam = create_beam(beam_cfg)
When source="file" and the file is a .dst, the file is
authoritative for the beam state: the reference energy and
frequency, plus the sample-derived emittances and Twiss
parameters, override the corresponding BeamConfig values, and the
particle count is the file's count. This follows TraceWin's
convention:
"If the input dst file is specified, the input beam parameters (number of particles, emittances, Twiss parameters, beam centroid, beam current and energy) are automatically extracted from the specified file and used for the calculation." — TraceWin user manual
with one deliberate exception: the header's current is not
applied — BeamConfig.current stays in force, so you control the
space-charge current explicitly. The file's rest mass is checked
against the configured species (a warning is logged if they differ by
more than 1 MeV) but the species comes from BeamConfig.
n_particles and seed have no effect on file loads — every
particle in the file is used, in file order. There is no
subsampling; if you want fewer particles, thin the file yourself
before loading.
Tutorial¶
Reading¶
from linac_gen.io.tracewin_dst import load_dst
particles, header = load_dst("initial.dst")
# particles: ndarray (N, 6) in HELIX conventions:
# [x_mm, xp_mrad, y_mm, yp_mrad, dphi_deg, dW_MeV]
# with dphi / dW as deviations from the bunch centroid.
print(f"Loaded {len(particles)} particles")
print(f" energy: {header['w_kin_ref']:.3f} MeV")
print(f" freq: {header['frequency_MHz']:.1f} MHz")
print(f" mass: {header['mass_MeV']:.3f} MeV")
header also carries n_particles, current_mA, phi_ref_rad, and
the sample-derived emittances / Twiss (emit_x, emit_nx,
alpha_x, beta_x, … for all three planes).
Writing¶
from linac_gen.io.tracewin_dst import write_dst
write_dst(
"final.dst",
beam.alive_particles, # (N, 6) — filtering losses is your job
current_mA=beam.current,
frequency_MHz=beam.ref.frequency,
mass_MeV=beam.ref.species.mass,
w_kin_ref=beam.ref.w_kin,
)
# tidy up the demo files
from pathlib import Path
Path("initial.dst").unlink(missing_ok=True)
Path("final.dst").unlink(missing_ok=True)
The writer takes a raw (N, 6) particle array in HELIX units and
converts back to TraceWin's on-disk conventions. It does not
filter dead particles — pass beam.alive_particles, not
beam.particles.
Layout¶
The .dst is a packed little-endian binary file (no alignment
padding):
Header (23 bytes):
2 × char magic bytes (TraceWin writes 0x7d 0x64)
uint32 Np — number of particles
float64 Ib — beam current [mA]
float64 freq — bunch frequency [MHz]
1 × char separator byte (TraceWin writes 0x7d)
Particle block (Np × 48 bytes):
For each particle, 6 × float64:
x [cm]
x' [rad]
y [cm]
y' [rad]
phi [rad] # absolute, relative to the RF clock
W [MeV] # absolute kinetic energy, not a deviation
Trailer (8 bytes):
float64 mc² — particle rest-mass energy [MeV]
Total file size = 31 + 48·Np bytes. There is no charge-state field; the species is not stored in the file.
On load, HELIX converts to its internal (mm, mrad, deg, MeV)
deviation convention:
- x, y: cm → mm (×10); x', y': rad → mrad (×1000).
- phi, W: the file stores absolute values and no synchronous
particle, so the loader subtracts the bunch centroid
(
mean(phi),mean(W)) and reports the residuals asdphi[deg] /dW[MeV]. The raw centroid is returned in the header asphi_ref_rad/w_kin_refso you can restore absolute values if needed.
API reference¶
linac_gen.io.tracewin_dst.load_dst(path: str) -> (particles, header)
linac_gen.io.tracewin_dst.write_dst(
path: str, particles: np.ndarray,
current_mA: float, frequency_MHz: float,
mass_MeV: float, w_kin_ref: float,
phi_ref_rad: float = 0.0,
) -> None
Source¶
linac_gen/io/tracewin_dst.py:1
As backtracking input¶
A .dst at a downstream plane (a diagnostic export, a TraceWin dump,
or a prior HELIX --write-dst) is the natural input for backward
tracking — reconstructing the distribution that entered the line:
python -m linac_gen backtrack lattice.dat --dst measured_exit.dst \
--species H- --energy 2.1 --freq 162.5 --write-dst entrance.dst
Energy policy: the file's centroid energy is authoritative for the
beam, but the machine's maps are always evaluated on the design
reference (the --energy you pass). If the file energy deviates from
the design exit energy the walk re-centres dW on the design
reference — warning above 0.1 % relative deviation, refusing above
5 % (both thresholds adjustable). A refusal almost always means the
.dst and the lattice/energy pairing don't belong together.
See CLI: backtrack for the full
workflow including the forward-closure --validate check.
See also¶
- Distributions —
from_filedistribution. - Worked example: LEBT + RFQ —
uses a
.dstto chain LEBT output into RFQ input. - CLI: backtrack — reconstruct
the upstream distribution from a downstream
.dst.
← BeamConfig reference · Continue to Space charge → Models →