SRTC Web Viewer — User Guide
Running SRTC from the release bundle. These docs are written against the
srtccommand line. In the distributed bundle that command lives inside the container, so run it through Compose from the unpacked bundle directory:docker compose run --rm -v "$PWD":/io backend srtc --helpFiles you pass in should sit under the mounted
/io, and that is also where to write outputs. On Linux the container runs as an unprivileged user, so add--user "$(id -u):$(id -g)"when writing to a mounted directory or the run fails with a permission error creating the output directory:docker compose run --rm --user "$(id -u):$(id -g)" -v "$PWD":/io backend \ srtc -i examples/fwd_transport/oltaris_al_si.deck -o /io/out/results.out \ --depth-g-cm2 1.0 --histories 100000 --seed 1The physics libraries are already in place, and the shipped example decks resolve against them, so the examples below run as written. See Downloads to get the bundle, and the Viewer guide for the browser GUI, which needs none of this.
The SRTC web viewer is a browser front-end for the whole SRTC workflow: build and
edit CSG geometry in 3D, define the source environment, and launch raytrace and
adjoint Monte Carlo calculations — all from a tabbed side menu. The written
deck/config is always the source of truth: every run assembles a real SRTC deck
and executes the srtc binary.
- Backend —
crates/viewer-server(Rust, axum). Depends only on thegeometrycrate (no HDF5), and runs physics by subprocessingsrtc. - Frontend —
web/(React + react-three-fiber + Vite).
0. Quick start (worked example)
On first load the viewer opens its default geometry:
examples/viewer/al1mm_si_detector.mcnp — a 1 mm aluminium spherical shell with a
silicon point detector at the centre (the canonical "dose behind 1 mm Al to a Si
detector" setup). Walk through it to learn the workflow:
- Geometry tab — the Geometry tree shows the loaded deck under deck geometry:
cell_10 (void)(the cavity) andcell_20 (m182)(the 1 mm Al shell). The detectors section lists★ FC1 (point)at the centre. Click any of them to highlight it in 3D. - Environment tab — the default AP9 proton / AE9 electron 350 km spectra are already loaded and plotted.
- Run ▸ Raytrace — pick the
ap9_proton_al.csvcurve, particle proton, and the FC1 detector (or type0 0 0). Run raytrace: it casts rays from the point detector out through the shell — every ray traverses 0.27 g/cm² = exactly 1 mm Al — and reports a dose (≈0.05 Gy/day for the default proton environment). This is a point-detector calculation end to end. - Assign the detector a Silicon dose response via its dropdown in the tree (or the raytrace detector material) to score dose-to-silicon.
To build your own: add shells/detectors from the Geometry tab, or Load path /
Upload deck a different .mcnp.
1. Running the viewer
SRTC is distributed as a self-contained container bundle — see Downloads. Docker is the only prerequisite.
tar xzf srtc-<version>-<arch>.tar.gz
./srtc-<version>-<arch>/install.sh
# -> http://localhost:8080
That starts the browser GUI and brings the physics libraries with it; nothing else needs installing. To use the command line described below, run it through Compose from the unpacked bundle directory:
docker compose run --rm -v "$PWD":/io backend srtc --help
2. The 3D view
- Orbit: drag. Zoom: scroll. Pan: right-drag.
- View toolbar (top-left):
XY/XZ/YZsnap to axis-aligned views;⟳ Resetframes the whole model (isometric). - Axis gizmo (bottom-right): shows orientation; click an axis to snap.
- Geometry is meshed on the backend to match transport truth exactly: macrobodies
(
RPP/RCC/SPH) and spheres render as crisp primitives; complex/hollow CSG is voxel-meshed (blocky is expected — it reflects the sampling resolution).
Selecting geometry and the context popup
Click any cell to select it. A context popup opens at the cursor with:
- Cell/shape name, material, density.
- Visibility:
solid·wireframe·hidden— set per geometry so you can see inside assemblies (e.g. wireframe the outer shell to view interior cells). - Assign material (GUI-built shapes only): pick any SRTC material; the shape's material id + density update and the deck/runs follow. Base-deck cells show their material read-only (edit the deck to change those).
The bottom-right info panel mirrors the current selection. Detectors render as markers (point detectors) or highlight their region (volume detectors); click a point detector to select it.
3. Geometry tab
Add geometry
Build a primitive with the Add geometry form:
Rect. prism (
RPP), Disc/Cyl (RCC), Sphere (SPH).Coordinates: box/sphere use a centre; cylinder uses a base point + axis vector.
wall (0 = solid): a non-zero wall makes the shape hollow (a boolean subtract of an inner body) — a shell/tube.
material + density: a searchable select over the full material compendium (~411 entries) — type to filter by name; picking a material fills its density (editable after). Each entry shows a fidelity badge:
prebuilt— tabulated hadronic physics (run_materials.txt);adjoint— present inmultigroup_coupled.h5, usable by the 3D adjoint;analytic— elemental-fallback physics (fine for raytrace + 1D forward);user— a material you defined (see below).
Any compendium material can be used in a 3D/adjoint run. If it has no multigroup data yet,
srtc mc3dbuilds it from JENDL before transporting (ensure_multigroup_materials_present) — roughly 1.5 s for the new material plus a rebuild of the ones already in the library, once. The readiness strip says which materials will be built. The rebuild preserves the installed energy mesh (reconstructed from the library's stored group bounds), so adding a material never regrids the ones already there.Before this, a material missing from the library was silently transported with the default material's cross sections (
MultigroupXs::matricesfalls back tomaterials[0]) — an argon sphere ran as aluminium.+ Add to
<group>adds the shape into the currently selected tree group.
User-defined materials (periodic-table builder)
Every material select has a pinned + New material… row that opens the builder:
click elements on the periodic table, set each one's atom % (normalized on
save; the live wt% preview shows the conversion), then a name and bulk density.
Saved materials get ids ≥ 411, live in user_materials.csv /
user_material_elements.csv in the server work dir, and appear in every select
with a user badge. The dialog also lists your materials for deletion.
Physics reach: raytrace and 1D forward dose curves work immediately (the analytic
elemental synthesis covers any composition; the server passes
SRTC_USER_MATERIALS_DIR to every srtc job so the ids resolve). The 3D adjoint
needs multigroup data, which is built automatically before the first run that
uses the material (or on demand via build adjoint data in the builder — a
srtc data build-multigroup --merge job). Either way needs a source checkout with
the nuclear-data inputs. The badge flips to adjoint when the build lands.
Since the Bragg-additivity work, a user material's stopping power is synthesized
from its elemental composition rather than left empty, so its multigroup tables
carry real ionization slowdown. Measured against NIST (srtc-data/tests/bragg_stopping.rs):
| band | agreement |
|---|---|
| electrons, >100 keV | within 10% (Al +7.7%, Si +7.3%, Fe +9.1%, W +8.8%) |
| protons, >1 MeV | within 15% (Al +4.0%, Fe +5.7%, W +12.8%; most <5%) |
| protons, <1 MeV | low by 45-82% — pinned, not fixed |
The sub-MeV proton deficit is the missing shell correction: Bethe's maximum sits later and lower than the true Bragg peak, and the √E branch below it inherits that. Sub-MeV protons range out in under ~15 µm of aluminium, so the error lives in the end-of-range CSDA operator rather than in transport across a shield. Fixing it means adding Andersen-Ziegler per-element low-energy coefficients.
The Adjoint tab flags any run that uses a synthesized material so the provenance is visible in the results, not just in the build log.
Geometry hierarchy tree (CAD-style)
The tree opens with a read-only deck geometry section listing the cells parsed from
the loaded input deck (e.g. cell_10 (void), cell_20 (m182)) — so the hierarchy
reflects the whole geometry, not just GUI-authored shapes. Click a deck cell with the
▧ icon to highlight it in 3D. GUI shapes you add live below it under the model
group.
The Geometry tree also organizes your shapes into nested groups (sub-assemblies):
- Click a group to make it the target for new shapes.
- +grp adds a sub-group; ✕ removes a node.
- Drag & drop a shape or group onto a group to nest it (file-explorer style). A dropped item keeps its local coordinates, now relative to the parent group.
- offset x/y/z on a group translates all its descendants together — move a whole sub-assembly as one unit.
- pos x/y/z on a shape edits its position relative to its parent group.
- Naming: give a shape a name in the Add geometry form, or double-click any shape/group label in the tree to rename it — makes complex models easy to navigate.
Internally the tree is flattened to absolute geometry before it's sent to the backend, so runs and saves always see resolved coordinates.
Detectors in the tree
Detectors appear at the bottom of the tree marked with a ★. Click one to select it (highlighted in 3D). Use its dropdown to assign a dose-response material — the material whose response function converts the detector's flux to dose.
- + add creates a point detector (emitted as an
FC<id> TYPE POINT x y zcard, so save/runs pick it up); edit its x/y/z inline and remove it with ✕. Detectors that came from the loaded deck are read-only here.
Point detectors are fully supported by the transport code. Each FC … TYPE POINT
card becomes a real PointDetector in srtc mc3d (an MCNP-style next-event estimator
scoring fluence/dose) and is the origin the Raytrace tab casts rays from. So a point
detector at the centre of a shield gives a dose end to end — the default deck demonstrates
exactly this (Quick start, §0). Assign it a material to score dose in that material
(e.g. Silicon).
Collapsing side panels
Every side-menu panel has a ▾/▸ header — click it to minimize/expand the panel, so you can collapse menus you're done with while setting up a run.
Save deck
The Save deck panel writes the base deck plus your GUI geometry:
- Save to server writes to a path on the server (
out_path). - Download saves the deck to your machine. Re-loading a saved deck reproduces the geometry exactly (shapes become real cells).
Load geometry
Top bar: Load path (a server-side .mcnp/.deck path) or Upload deck (a local
file). Loading a new base keeps your GUI-added shapes layered on top.
4. Environment tab
Defines the source spectra that feed runs. On startup it loads default AP9 proton and AE9 electron 350 km LEO spectra.
- Load CSV replaces a species' spectrum from a two-column
energy,valuefile (or an IRENE export for electrons). It opens the import prompt below rather than parsing silently. - The inline plot shows both spectra log–log; ⤢ expand opens a large viewer with labelled axes (Energy [MeV] vs dN/dE), a differential ⇄ integral toggle, and a table view (scientific notation) for reading exact values.
- The selected particle's spectrum is passed to the raytrace
SPECTRUMcard.
Spectrum import prompt (delimiter, flux vs fluence)
Picking a file opens a dialog before anything is loaded, because every one of these choices is a silent-wrong-answer if it is guessed:
| Control | Options | Why it matters |
|---|---|---|
| column delimiter | auto-detect, comma, tab, space, semicolon, any whitespace | A space-delimited export read as comma-delimited yields zero points and looks like an empty file. Auto-detect picks whichever delimiter parses the most rows. |
| quantity | fluence (already time-integrated) / flux (rate) | A flux is scaled to SRTC's per-day convention and its dose is reported as rad/day; a fluence is a one-shot exposure and its dose is rad. |
| form | differential (/cm²/MeV) / integral above each energy (/cm²) | An integral table read as differential is wrong by the bin width at every energy. |
| per N ⟨unit⟩ (flux only) | seconds…years | Shown with the exact scale factor applied, e.g. × 8.640e4 → /day. |
The dialog shows the first lines of the file as split under the current delimiter,
the point count and energy span, and every line it could not use with the reason
(only 1 column under this delimiter, non-numeric energy or value, …). It then
plots the spectrum as SRTC will read it, so a wrong form or time base is visible
as a shifted curve before it is committed.
It blocks the load on: no usable rows, a single point (nothing to integrate over), or an all-zero/negative spectrum. It warns (without blocking) on negative values, duplicate energies, skipped lines, an energy range that looks like keV or eV rather than MeV, and a form/monotonicity mismatch — values that rise with energy while declared "integral above" mean the file is really differential.
The declared convention lives with the source, so a later change to it re-normalizes the already-uploaded points; there is no need to re-upload.
Heavy-ion (GCR) environment
Two formats are accepted, detected from the file's content, not its name:
| Format | Recognised by | Axis as written | Flux as written |
|---|---|---|---|
| OLTARIS BON2020 | its … array dimensions and data header cards |
MeV/amu | #/(cm²·day·EU*) |
| CREME96 | anything else | MeV/nucleon | #/(m²·s·sr·MeV/nuc) |
BON2020 already writes SRTC's per-cm²-per-day, omnidirectional convention, so the
only conversion is the per-nucleon energy axis. A CREME96 flux is a directional
rate — per steradian, per second, per m² — and reaching SRTC's units multiplies by
4π sr, by 1e-4 (m²→cm²) and by 86400 (s→day): a factor of ~108.6, which is exactly
the kind of thing that silently produces a two-orders-of-magnitude wrong dose if
guessed. So the units are read out of the file's own header when it states them,
defaulted to CREME96's standard when it does not, and the assumption is printed in
the panel rather than buried (CREME96: #/(m²·s·sr·MeV/nuc) vs MeV/nucleon — ×1.0857e2 to #/(cm²·day·MeV) vs total MeV). Force a reader with srtc hi-environment --format oltaris|creme96 if detection ever gets it wrong.
CREME96 files may be laid out either as per-element blocks (a Z = 26 / Fe /
Fe56 / ION 26 56 header line, then energy flux rows, repeated) or as a wide
table (Energy H He Li … header, then one energy and one flux column per element).
Two-column data appearing before any species header is an error, not a guess. CREME96
tabulates per element, so each Z transports as its most abundant isotope (Z=26 →
Fe-56, Z=28 → Ni-58). test_spectrums/creme96/format_example.flx is a runnable layout
example — illustrative numbers, not a physics reference.
Below the NIEL controls, environment file lists the environment files
under SRTC_HI_ENV_DIR, grouped into Uploaded and Shipped. On startup the
panel loads SRTC_DEFAULT_HI_ENV (the BON2020 boundary flux — free-space GCR),
so heavy ions arrive populated like the AP9/AE9 proton and electron spectra rather
than needing a selection first. The hi_Flux_*gcm2 files in the same directory are
already-transported spectra behind aluminium; using one as a source double-counts
that shielding.
Load file uploads your own .dat/.txt (BON2020) or .flx/.txt (CREME96),
which is then selected and parsed. Uploads land in <work_dir>/hi_env/uploads/ (a
volume in containers) and shadow a shipped file of the same name. Unlike the
proton/electron Load CSV buttons — two-column files the browser parses itself —
a multi-species environment file's reader lives in srtc-rs, so the bytes go to the
server first.
Selecting a file parses it server-side (srtc hi-environment) into per-species
spectra — 28 species for the shipped boundary file — with energies converted from
the file's per-nucleon axis to total MeV per ion.
The parse is cached by (file, mtime, LET) for the session, so re-selecting an
environment is instant, and the LET evaluation loads only em_stopping.h5
(2.7 MB) rather than the whole library set (67 MB, most of it hadronic.h5) —
LET is dE/dx and nothing else. Measured on the shipped boundary file: 0.70 s →
0.16 s cold, 0.01 s cached. If a load is still slow, the time is I/O on the
physics volume or an emulated-architecture container, not the parse — check
docs/DEPLOY_DOCKER.md before looking anywhere else.
- species (n/m) ▾ opens a checkbox dropdown, one row per species with its integral flux, plus an all/none toggle. H, He, C, O, Si and Fe are on by default; the rest stay available rather than crowding a 28-series plot.
- ⤢ inspect spectra opens the large viewer — the practical way to read 28 curves, and where every control that decides what is plotted now lives: species filter, x axis, differential ⇄ integral, flux time base, and the tracking cut. There is no inline plot in the side panel; a 232 px log–log plot of overlapping spectra was unreadable, and keeping one mounted meant rebuilding it on every render of a tab whose job is form entry.
- x axis switches between Energy [MeV/u] (default), Energy [MeV] total per ion, and LET in Si [MeV·cm²/mg]. LET comes from the server's stopping tables — the browser never computes stopping power, only unit conversions.
- Per-nucleon is a change of variable, not a relabelled tick: with x = E/A the density is multiplied by A so ∫ dN/dx dx is unchanged. It is what puts Fe-56 and a trapped proton on the same span instead of two decades apart.
- Flux switches the y axis between particles/cm²/s and /cm²/day. Display only — transport always runs in SRTC's native per-day convention.
LET units
LET is displayed in MeV·cm²/mg in silicon, which is mass stopping power: no density enters the quantity, and "in silicon" names the material whose stopping power it is. This is the unit device SEE cross-section curves σ(LET) are published against, so a spectrum here can be folded against a datasheet without converting.
The internals stay in keV/µm — the LetGrid bins, the # LET section of a curve
CSV, and let_vs_depth.csv from hi-depth-sweep all carry keV/µm column names,
and srtc hi-environment --with-let emits let_si_kev_um. Only the GUI converts,
by 233.0 keV/µm per MeV·cm²/mg (ρ_Si = 2.33 g/cm³). That factor is owned by
let_spectrum::kev_um_per_mev_cm2_mg and pinned by a Rust test that names the
web-side constant, so a compendium density edit fails a test instead of silently
mislabelling every LET axis in the viewer.
The LET spectrum mode bins each selected species' fluence onto the same grid
hi-depth-sweep writes (24 bins/decade), sums them into an "all species" total,
and offers differential (dN/dLET) or integral-above — the latter being the curve an
SEE rate integral consumes. Neutral and leptonic species (photon, neutron,
electron) are excluded from it, the same exclusion raytrace::hi_depth makes:
photons and neutrons have no silicon LET at all, and an electron does, so leaving
it in would quietly inflate the total.
- The cut slider is live: dragging it re-bins in the browser with no server round trip, which is the point — the top of the LET spectrum is made entirely of the ions the cut is deciding about.
- Colors group by Z (H/He, Li–B, CNO, mid, Fe-peak) since no categorical palette resolves 28 series.
Heavy-ion tracking cut (min energy [MeV/u])
min energy [MeV/u] (default 10, the CREME96 convention) is the energy per nucleon below which an ion is neither transported nor counted in any LET spectrum. It is a transport cut, not a display filter: below it the ion is removed from the state and never scored.
The reason is range against LET. Fe-56 at 10 MeV/u has a residual range of ~0.03 g/cm² — it cannot cross a shield, and it cannot reach a sensitive volume from anywhere outside one. But LET rises as an ion slows, so an ion at that energy sits at the very top of the LET spectrum (~8 MeV·cm²/mg), which is precisely the region an SEE rate integral is most sensitive to. Without the cut, tracks that go nowhere dominate the part of the answer that matters most.
Consequences worth knowing:
- Dose from ions below the cut is not scored. That is what "stop transport" means, and it is a small omission: sub-10 MeV/u ions carry a negligible share of GCR dose precisely because they stop so fast.
- Protons are exempt (they keep
TRANSPORT EMIN_MEV): a 10 MeV cut on protons would discard the bulk of shallow proton dose. The cut applies to A ≥ 2. - On the shipped BON2020 boundary environment the default barely does anything — the environment itself starts near 11 MeV/u. It earns its keep on environments that reach lower (a CREME96 spectrum down to 0.1 MeV/u) and on transmitted spectra at depth, where slow ions accumulate.
The same number reaches every consumer: the 1D Transport tab's curve (§4b), the
Raytrace tab's per-ray ion transport, and the deck field
TRANSPORT ION_EMIN_MEV_PER_NUCLEON= for hand-written decks.
The panel states the two approximations inline: no fragment production (so heavy-ion flux at depth is a lower bound) and unrestricted LET (an upper bound for thin sensitive volumes).
Selected species carry over to both run paths, and in both they ride the same
job as the proton/electron pass rather than a job per species: the 1D Transport
tab's dose-depth curve (§4b) and the Raytrace tab (one deck with many SPECTRUM
blocks).
4b. 1D Transport tab
Pushes the environment through slab ladders. One action transports everything loaded on the Environment tab — proton and electron spectra plus, when one is selected, the heavy-ion (GCR) environment — through the same depth ladder, into one curve file. Generating them together is the point: a curve-mode raytrace reads that one file, so a curve built without the ion leg silently carries no GCR dose.
Checkboxes pick which light species contribute; transport N heavy-ion species (GCR) (on by default whenever an environment is selected) adds the ion leg. The depth ladder, shield/detector material and curve naming work as before, and the curve feeds the Raytrace tab's dropdown.
What the ion leg adds to the same curve file:
| Where | Contents |
|---|---|
heavy_ion (rad/day) column |
ion dose at each depth, summed over the selected species, inside the curve's total — so a curve-mode raytrace picks up GCR dose with no extra step |
# HI section |
per depth and species: integral flux, dose, mean silicon LET |
# LET section |
silicon LET spectrum vs depth, differential and integral-above per bin |
# SPECTRA section (only with also spectrum vs depth) |
per-species ion flux vs depth, as ion_<species> alongside the proton/electron/neutron/photon rows |
The curve viewer grows a heavy ions (GCR) vs depth panel for such a curve: a depth picker, the LET spectrum (integral-above by default — the SEE convention, since an event rate is a rate above a threshold) and the per-species table sorted by dose.
The ion leg transports exactly the species ticked on the Environment tab, which by
default includes the environment's own H and He rows. Those are GCR protons and
alphas — a different population from a trapped-proton spectrum loaded as a source, so
counting both is correct. Untick them if your proton source already covers the same
population; the # HI table shows each species' contribution, so a double count is
visible rather than hidden in a total.
A heavy-ion environment is always a rate. If the proton/electron sources are a
fluence (already time-integrated), the ion dose cannot be summed with them without
a mission duration, so the column is still written — honestly labelled rad/day —
but marked [not in total] and left out of the sum.
The min energy [MeV/u] cut is set on the Environment tab (§4) beside the environment itself, so this curve and a raytrace of the same environment can never disagree about which ions exist. The chip here shows the value in force.
Ion transport is stopping power plus Tripathi/Kox attenuation — fragments are not produced, so ion flux at depth is a lower bound and the light ions built up from fragmenting heavies are missing. LET is unrestricted (δ-rays counted at the track), an upper bound for thin sensitive volumes.
Standalone heavy-ion sweep (CLI / API)
The separate per-species CSV bundle is still available outside the GUI, for scripted
use where flux_vs_depth.csv itself is the product:
srtc hi-depth-sweep --environment <file> [--format oltaris|creme96] \
--species Fe56,O16,alpha --material AL --depths-g-cm2 0,1,5,10 --out <dir>
| File | Contents |
|---|---|
flux_vs_depth.csv |
depth, species, z, a, bin_low_MeV, bin_high_MeV, fluence — kept per species so LET can later be recomputed in another target material without re-transporting |
let_vs_depth.csv |
silicon LET per depth, differential and integral-above |
summary_vs_depth.csv |
per depth and species: integral flux, dose, mean LET |
The same numbers, from the same code path (raytrace::compute_hi_depth), as the
sections the curve file carries.
5. Raytrace (Run panel)
Opened from 5 Run in the top menu (or the ▶ 5 Run step in the readiness stepper), then the Raytrace engine chip. The menu's function entries are numbered in workflow order — 1 Geometry · 2 Materials · 3 Detectors · 4 Environment · 5 Run — and the stepper under the menu mirrors them: each step shows a ✓ when complete or how many selections are missing (click for the list; items open the panel that fixes them).
Solid-angle dose from a detector point through the CSG (ray-trace sectoring):
- deterministic spectrum transport (checkbox, default on): instead of a
depth-curve lookup, the Environment spectrum is propagated deterministically
through each ray's actual material stack (per-material CSDA + nuclear
attenuation + secondary production — multi-material aware, e.g.
steel→Al→poly). Outputs per-species doses and transmitted spectra per
detector. Physics mirrors the SRTC 1D forward MC (validated 0.93–1.02
proton, 0.97–1.19 electron against it —
docs/RAYTRACE_DETERMINISTIC.md). With a curve also selected, the classic curve dose appears alongside as a cross-check column. - curve: a dose-depth curve — optional cross-check when deterministic is on,
the sole estimator when off. The default is
ap9_proton_oltaris_al.csv— the OLTARIS-derived AP9 proton dose-to-Si vs Al depth (rad/day vs g/cm²), so curve-mode raytrace reproduces absolute OLTARIS doses. Verified: 0.213 rad/day behind 1 mm Al (0.27 g/cm²) vs OLTARIS 0.207 (the other shipped curves are toy placeholders). - transport N heavy-ion species (checkbox, shown only once a heavy-ion
environment is selected on the Environment tab; requires deterministic
transport, since a dose-depth curve has no ion contribution to look up). The
selected ions ride the first species pass as extra
SPECTRUMblocks, so the whole GCR environment costs one job — attaching them to every pass would count them twice when proton and electron are combined. The results table gains an of which ions column (already included in the dose column), and the run writes a silicon LET spectrum (<out>.let.csv, differential and integral above each bin). - particle: proton/electron (uses the matching Environment spectrum).
- detector: pick a scene point detector or type x/y/z.
- n azimuth / n polar: angular sampling density.
- curve depth is mm Al: on for mm-depth curves; off for the OLTARIS curve (g/cm²), set automatically when an OLTARIS curve is the default.
- Run raytrace launches
srtc raytrace; a progress bar tracks completion, then the result shows dose (rad/day), mean traversed depth, and ray count.
Absolute normalization. The Environment spectra are absolute differential fluence
(per_day_cm2_mev); deterministic doses are physical rad/day directly from them.
In curve mode the OLTARIS curve carries the absolute dose-vs-depth instead. (An OLTARIS
electron curve is not yet shipped — only the proton reference exists in-repo.)
Ray visualization (dose-entry directions)
The Rays (dose entry) panel casts rays over 4π from a point and draws them colored by traversed thickness (a damage heatmap): red = thin (least shielding → most dose/damage), blue = thick (well shielded). A color legend shows the g/cm² range. Trace computes them; the max-thickness slider filters which rays show (default = least-material 10% — the highest-dose directions). Added shielding shapes are reflected in the rays.
6. Adjoint MC (Run panel)
Opened from 5 Run in the top menu, then the Adjoint MC engine chip. Two modes, chosen with the toggle at the top:
scoring volume (surface coupling)
Volume-averaged flux and dose inside a part, obtained in two legs — the scheme Geant4, NOVICE and FASTRAD all offer. Protons, neutrons, photons and electrons transport together (the coupled multigroup group index carries the species).
world sphere (environment)
▲ │
leg 1 │ adjoint │ leg 2 forward
│ ▼
╔═╧══ scoring volume's own surface ══╗
║ scoring volume ║
╚════════════════════════════════════╝
You pick one thing: the volume to score in. Its own boundary is the coupling surface — there is no second surface to choose, and no pair to get wrong.
Leg 1 (adjoint) emits from that surface out to the world sphere, where the
histories are folded against the environment and normalized. That yields the flux
arriving at the surface, with its energy and direction distributions. This is
reciprocity used directly: an adjoint history born on the surface, folded against the
environment where it arrives, estimates the forward flux there. Running it in reverse
costs one pass over the geometry outside the part, rather than transporting the whole
environment inward and keeping the small fraction that reaches it — which is why it pays
off exactly when the part is small and deeply buried. The result is cached to
boundary_flux.h5; the outside of the problem does not change when the inside does.
Leg 2 (forward) takes that distribution and transports it forward through the scoring volume, tallying track-length flux and dose. Unlike the point-detector mode, this gives you the flux spectrum inside the volume, not just a scalar dose.
Controls:
- score in: the part to average flux and dose over. Any CSG shape or imported STEP part works — the server resolves the part to its own bounding surface, so you never name a surface id. A region built as a union of bodies has no single boundary to collect the flux on and is not offered.
- histories: leg 1's budget, spread over (energy group × angle bin) cells and allocated by variance after a short pilot. This is not a count of histories through the problem, and the difference matters: a coupled library gives 600 groups × 10 bins = 6000 cells, so 60 000 histories is ten per cell. Below roughly twenty per cell the collected boundary flux is still drifting and the run says so, naming the number to raise it to. The default is 60 000; a run left at the old 200 was off by 28% on a real geometry and reported nothing.
- forward / adjoint: leg 2 histories per leg 1 history. This changes the uncertainty,
not the answer (
coupled_transport_reproduces_the_direct_adjoint_answerpins that). - An environment must be loaded on the Environment tab: leg 1 folds against it, and without one the run has nothing to score. Every loaded environment goes into the same run — the coupled source vector is indexed by the coupled group and the species blocks are disjoint, so a proton and an electron environment merge by addition rather than needing a run each. This is not the same as summing two separate runs: a merged run also captures the cross-species channel (a proton-born adjoint history that converts to an electron and scores against the electron environment), which two separate runs each discard. On a 2 cm Al shell over a 5 mm Si die that channel is worth ~13% of the volume flux and ~1.7% of the dose.
Run adjoint MC launches srtc mc3d. Results are absolute, in the environment's own
units (per day for AP9/AE9), not per source particle.
- trace particle paths (default on) draws both legs, which is the point of it here — the adjoint leg renders dashed, the forward leg solid, both colored by particle type (photon = yellow, electron = blue, proton = red). The track budget is split between them so leg 1 cannot fill it before leg 2 runs. Forward tracks include secondaries. A Particle tracks legend (bottom) toggles each species on/off. Tip: set the surrounding geometry to wireframe or hidden (click-a-shape popup) to see the tracks clearly.
Two things worth knowing. Leg 2 treats the surface as a black boundary: a particle that leaves through it is done, because its re-entry is already counted in the collected inward current. And the surface has to bound a cell — which a part's own boundary always does, but a hand-written deck can name a surface no cell references, and that one is geometrically inert: the tracker never reports crossing it, so the restart silently would not happen. The run refuses rather than returning a wrong number.
Validation. Two external codes and two closed forms.
Geant4 (tests/comparisons/surface_coupled_geant4/) — the same sphere geometry and
IRENE environment, absolute rad/day on both sides. AP9 protons over 1–20 mm Al agree to
1.02–1.09; AE9 electrons show a depth trend that the direct adjoint shows equally, so
it is SRTC's known electron gap rather than the coupling.
PHITS (tests/comparisons/surface_coupled_phits/) — isotropic AP9 protons above
50 MeV, compared as an Al/void ratio so each code's source normalisation cancels:
0.9508 against 0.9562, i.e. 0.6%.
Closed forms (tests/adjoint_surface_coupling.rs, fast tier, no ENDF) — these need no
reference code at all, which is what lets them catch an error that scales everything
equally: a Lambertian sphere of radius R carrying current I must produce a uniform
interior field of exactly I/(πR²), and in vacuum an isotropic environment J across
area A must be collected as A·J/4. On top of those: coupling on the scoring volume's
own surface reproduces the direct adjoint, the answer does not depend on where the
coupling surface sits, and a volume detector converges onto a point detector as it
shrinks (4 cm → 1.394, 2 cm → 1.107, 1 cm → 1.042, 0.5 cm → 1.026).
Where it is weaker. A near-monoenergetic environment makes the adjoint fold heavy-tailed — seed to seed it moves ~50%, against 2.4% for a broad spectrum — so this is a solver for the broad environments it was built for (AP9, AE9, GCR), not for beam lines. And leg 2 transports only through the part you scored in; collecting on one surface and scoring somewhere deeper is reachable from a hand-written deck, not from here.
point detector (pure adjoint)
Dose to a point detector through a spherical shield (the default deck's setup). This is the validated Shield2Dose-spherical physics — pure adjoint born at the point detector, scored against the environment with a Silicon dose response and a spherical exit correction:
point det: the
FC … POINTdetector; source region: the void the environment fills (e.g. the surrounding space cell); shield (mm Al): the shell thickness for the exit correction; histories.environments: check proton and/or electron — the run applies each loaded Environment spectrum as a source and sums the doses (the adjoint dose is linear in the source). The Results popup shows the total plus a per-species breakdown.
Advanced: the inner (cavity) and shield region ids for the exit correction (default 0 and 1, matching the default deck).
Accuracy note: this is a first-principles Monte Carlo dose (4π spherical), distinct from the ray-trace/OLTARIS curve. Its absolute normalization vs OLTARIS is an open validation item: the validated 3D spherical adjoint already under-predicts OLTARIS by ~50% at these depths, the source-spectrum path adds a further offset (
project_oltaris_slab_spectrum), and the electron adjoint has the known coupled-adjoint limitations (issue #23). For an absolute proton dose today, prefer the Raytrace tab with the OLTARIS curve; use point-adjoint for relative/first-principles studies.Results popup: when a run finishes, a Results panel (top-right) shows each detector's dose ± uncertainty (and per-species breakdown for point mode), and auto-plots a per-species spectrum (electron / photon / proton) with the same differential/integral/table viewer via ⤢ as the Environment tab. No user input — it comes from the run:
- point-detector mode → the transported spectrum at the detector: a forward next-event estimate of the flux reaching the point, per species (proton / electron / secondary photon). It matches the environment for penetrating energies and attenuates below the shield cutoff, so it can be multiplied by DD / SEE response functions. (This forward pass runs regardless of the tracks toggle; the dose still uses the validated adjoint transport.)
- surface→volume mode → the forward transported flux at the volume detector. Minimize (—) or maximize (⤢ on the panel header); close with ✕.
6b. Forward 3D tab
Its own top-level panel, not part of Run. Forward Monte Carlo on the CSG geometry, scoring volumetric cells and an optional 3D mesh.
It is separate because it shares nothing with the raytrace/adjoint forms: the
source is a beam rather than an environment spectrum, the detectors are
volumetric cells rather than points, and the main output is a .vti file opened
in ParaView.
Source — pick a kind (point / pencil / surface / environment), a species (proton, electron, photon, neutron) and an energy. Pencil takes a direction; surface takes a deck surface id, a disc radius for planes, and a cosine or isotropic hemisphere. The three Gaussian sigmas (beam spot, energy width, divergence) are 0 = exact.
Where the beam may start — anywhere. A particle is only transported if some
region claims the point it is born at; a point no cell contains is killed at
birth, transporting nothing and scoring nothing (which_region in
src/mc3d/track.rs), which reads as a clean run with zero dose and no tracks.
GUI-authored geometry has no world cell at all, so for point and pencil sources
the viewer appends a void space region sized to reach the source position. Two
cases it cannot fix for you, both now reported by mc3d as
warning: N particles were born outside every region:
a hand-written deck whose own world does not reach the source, and a source
placed inside a cell with imp=0.
Scoring cells — tick the shapes to score in. Each becomes one volumetric tally: MCNP F4 flux and F6 dose, both per source particle. Users normally add a cube or sphere for the purpose. The selection is held by shape, not by region id, so editing the geometry drops a stale pick rather than silently moving it to a different shape.
3D mesh — an axis-aligned box with its own origin, size and bin counts, independent of the geometry. Written as VTK ImageData for ParaView.
Scoring particles — leave all unchecked to score every species (the usual case); check some to isolate one component in the mesh.
Trace particle paths (debug) — records the first N paths of each species and draws them in the 3D view: electrons red, protons blue, photons green, neutrons yellow, with secondaries under their own species (proton/electron → bremsstrahlung photons → knock-on electrons). Recording runs on the serial transport path, so it is a small subset of the histories — a look at where particles go, never a tally. Off by default. The per-species checkboxes and the ✕ (discard) are in the track legend overlay; wireframe or hide the outer shell to see inside. The "escaping (dose-contributing) only" filter is adjoint-only and is not offered for forward tracks.
Outputs, in the run folder (Results tab): forward3d.toml, geometry.mcnp,
tally.json, forward_summary.csv, <name>.vti, and tracks.json when path
tracing is on. Runs are listed as Forward 3D.
Units are per source particle: flux 1/cm²/source, dose MeV/g/source — the PHITS
convention, so a run compares directly against a mesh=reg PHITS tally.
7. How runs execute (the deck is the source of truth)
Each run assembles inputs into WORK_DIR/<job>/ and runs srtc:
- Raytrace →
geometry.mcnp(from your geometry) +raytrace.deck→srtc raytrace -i raytrace.deck -o out.csv. - Adjoint →
geometry.mcnp+config.toml→srtc mc3d --config config.toml --tally-output tally.json. Jobs run asynchronously; the panel polls status and streams the log. You can inspect the exact deck/config written under the work dir.
8. HTTP API reference
| Method | Path | Body / result |
|---|---|---|
| GET | /api/health |
liveness {status:"ok"} |
| POST | /api/geometry/load |
`{deck_text? |
| POST | /api/geometry/save |
{…, out_path?} → {deck_text, bytes, written_path} |
| POST | /api/geometry/rays |
{…, origin?, n_rays?} → per-ray areal density |
| GET | /api/materials |
SRTC material registry |
| GET | /api/curves |
shipped dose-depth curve names |
| GET | /api/curves/detail |
?name= → {x_label, series[]} (main dose table only) |
| GET | /api/curves/spectra |
?name= → per-(particle, depth) spectrum slots |
| GET | /api/curves/heavy-ions |
?name= → {source, species[], let_spectra[]} from the curve's # HI / # LET sections; empty for a curve built without ions |
| GET | /api/hi-environments |
shipped + uploaded heavy-ion environment files |
| GET | /api/hi-environment |
?file=&with_let= → parsed per-species spectra (format, flux_note) |
| POST | /api/hi-environment/upload |
?name= + raw body (.dat/.txt/.flx) → {file, bytes} |
| POST | /api/jobs/dose_depth_curve |
{sources[], depths_*, hi_environment?, hi_species[]?, …} → {job_id, curve} |
| GET | /api/default-spectra |
{proton[], electron[]} |
| POST | /api/jobs/raytrace |
{…, particle, curve, detector, spectrum[]} → {job_id} |
| POST | /api/jobs/adjoint |
{…, n_histories, surface_detector_id, volume_detector_id} → {job_id} |
| GET | /api/jobs/:id |
{status, log_tail, result, error} |
| GET | /api/jobs |
job summaries |
9. Troubleshooting
- Adjoint fails immediately — physics data missing. Check
SRTC_DATA_DIR(and that the shared volume is populated in Docker). The job's log tail shows thesrtcerror. - Raytrace "requires a curve" — pick a curve in the Raytrace tab; add curves under the curves dir.
- A deck won't load — the geometry parser reports the error verbatim (e.g. an unsupported detector card syntax). Fix the deck line it names.
- Nothing renders / blank canvas — check the browser console; ensure WebGL is enabled. Complex regions mesh on the backend and may take a moment on first load.
- Can't reach it from another machine — the backend binds
0.0.0.0; open the host firewall for the port. In Docker the frontend publishes host8080. - Port already in use — a bare
srtc-vieweranddocker composeboth want host 8080; run only one, or change the port.