Skip to content

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 appliedBeamConfig.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 as dphi [deg] / dW [MeV]. The raw centroid is returned in the header as phi_ref_rad / w_kin_ref so you can restore absolute values if needed.

API reference

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

  • Distributionsfrom_file distribution.
  • Worked example: LEBT + RFQ — uses a .dst to chain LEBT output into RFQ input.
  • CLI: backtrack — reconstruct the upstream distribution from a downstream .dst.

BeamConfig reference · Continue to Space charge → Models →