# Pressure Wave
Simulates blast overpressure and impulse on a spherical sensor (a human brain)
placed inside MICH-helmet and SAPI-plate geometry (front and back torso plates),
using Friedlander waveforms and ray tracing with specular reflection. It is a
simplified model: it does **not** approximate human body composition around the
sensor.
The numerical core runs on **[NVIDIA Warp](https://nvidia.github.io/warp/)** — one
GPU thread per ray, each marching its own bounce path with real early-exit over a
hardware BVH (`wp.mesh_query_ray`). Rays are cast toward the sensor for every
point in a configurable blast cube, through five armor configurations
(`full_armor` = helmet + both plates, `front_plate_helmet` = helmet + front
plate, `helmet_only`, `vest_only` = front plate, `no_armor`); the
solid-angle-weighted total impulse is written to CSV.
# Getting Started
Environment management is via [`uv`](https://docs.astral.sh/uv/); all simulation
parameters live in a YAML file (`config.yaml`).
## Install
Host dependencies (NumPy, trimesh, PyYAML, SciPy) are the same everywhere:
$ uv sync
The GPU backend is NVIDIA Warp, installed via the `cuda` extra:
$ uv sync --extra cuda
**On the DGX Spark / GB10** (aarch64 + CUDA 13), if the PyPI wheel (CUDA 12.9)
doesn't cover your stack, install the official aarch64 CUDA-13 wheel directly:
$ uv pip install \
https://github.com/NVIDIA/warp/releases/download/v1.15.0/warp_lang-1.15.0+cu13-py3-none-manylinux_2_34_aarch64.whl
**Smoke-test Warp before anything else** — the GB10 is new hardware:
$ uv run python -c "import warp as wp; wp.init(); print(wp.get_cuda_devices())"
[Device('cuda:0')]
An empty list means Warp fell back to CPU (no CUDA device seen); set
`compute.device: cpu` to run on CPU deliberately.
## Run
$ uv run simulate_blasts.py --config config.yaml
Every parameter lives in the YAML file. Override individual keys on the command
line with repeatable dotted `--set` flags:
$ uv run simulate_blasts.py --config config.yaml \
--set blast.cube_segments=15 \
--set blast.cube_extents_m=20 \
--set rays.resolution_arcmin=2
## Testing
$ uv run pytest
The suite checks the physical invariants and cross-checks the `no_armor` column
against an independent NumPy computation. It runs on whatever `compute.device`
resolves to — the GPU, or the Warp CPU backend as a fallback.
## Configuration
See `config.yaml` for the full, commented schema. The knobs, by group:
| Group | Key | Meaning |
|-------|-----|---------|
| `blast` | `cube_center_m`, `cube_extents_m`, `cube_segments`, `min_dist_m` | blast-cube center, **side length**, **samples per axis (sampling density)**, and the minimum blast-to-sensor distance |
| `rays` | `resolution_arcmin`, `max_bounces`, `max_length_m`, `fan_mode` | **angular ray spacing (ray density)**, reflection depth, trace length, and whether the ray fan spans the sensor disc (`sensor_cone`) or widens to the armor (`geometry`) |
| `physics` | `initial_peak_pressure_kpa`, `positive_phase_duration_s`, `decay_alpha`, `reflection_loss` | Friedlander/blast constants |
| `sensor` | `diameter_m` | sensor sphere diameter (radius = diameter / 2) |
| `geometry` | `helmet`, `vest` | mesh paths |
| `compute` | `device` | `cuda` (GPU) or `cpu` (Warp CPU backend, for testing) |
| `output` | `file` | output CSV path (`null` → timestamped) |
Number of explosions = `cube_segments³` (minus points closer than `min_dist_m`).
Output CSV columns: `bp_x, bp_y, bp_z, full_armor, front_plate_helmet,
helmet_only, vest_only, no_armor`.
> **Data-compatibility note:** CSVs generated before the back plate was added
> (including those committed in `data/`) have four armor columns, and their
> `full_armor` corresponds to today's `front_plate_helmet` (helmet + front
> plate only).
## Edge diffraction (optional, off by default)
Pure ray tracing casts sharp geometric shadows: a blast point the armor occludes
contributes exactly zero. But the pulse is acoustically *large* — the positive
phase spans `c·t_d ≈ 3.4 m`, roughly ten times the helmet — so real blast energy
diffracts around the shell into those shadows. Setting `diffraction.enabled: true`
(or `--set diffraction.enabled=true`) adds **first-order edge diffraction** so
the geometric shadows fill in physically, which is primarily a robustness check:
does the armor-configuration ranking survive the model's single largest
omission?
The method is **Biot–Tolstoy–Medwin (BTM)**, not GTD/UTD. Impulse is the
zero-frequency (DC) component of pressure, and the high-frequency GTD/UTD
diffraction coefficient *diverges* at DC (it scales `1/√k`); BTM's time-domain
response is integrable, giving a finite DC edge gain. The diffracted impulse is
the incident impulse times that gain, folded into the same accumulator as the
direct/reflected rays so **geometric-optics + diffraction is continuous across
the shadow boundary** by construction.
The DC coefficient is validated in `tests/test_diffraction.py` against the exact
static rigid-wedge Green's function (a Legendre-Q modal series, itself checked
against image sums to 1e-9), across wedge angles including the screen (the rim
case), and independently by the shadow-boundary continuity condition. The
coupling constant `1/(16π²)` is fixed by those checks, not fitted.
Because MICH.obj is a smooth watertight shell with no rim edge, the helmet's
diffracting edge is sourced procedurally — the intersection of the shell with a
horizontal (constant-Z) plane (`diffraction.edge_mode: rim_plane`); the torso
plates use their perimeter feature edges. See the `diffraction:` block in
`config.yaml`.
**Scope and caveats.** First-order only (no double diffraction, no
reflection×diffraction); *edge* diffraction, not smooth-surface creeping waves;
impulse-only (DC), linear acoustics (no Mach stem). The diffraction term is
signed, so where the discrete ray-traced geometric field under-resolves a
near-zero-field transition cell the naive sum can dip slightly negative (worst
case a few percent of the data scale, at cells whose geometric field is ~0); the
physical impulse is clamped at zero and the count logged, and the effect shrinks
with finer `rays.resolution_arcmin`. Treat diffraction-on results as a
comparative robustness bound, not absolute dosimetry.
## Precision and performance notes
- Warp's mesh queries run in **float32**; the per-explosion impulse is accumulated
in **float64**. Expect ~5–6 significant digits vs a float64 reference — the last
couple of CSV decimals, and cone-edge rays, can differ.
- Memory is trivial (~0.5 GB even at 9k points / 2 arcmin) — rays are generated
in-kernel, never materialized.
- Warp uses a **software BVH on CUDA cores** (not the GB10's RT cores). If peak
throughput at very fine resolution is needed, the RT-core path is
[Mitsuba 3 / Dr.Jit](https://www.mitsuba-renderer.org/) — a further ~3–9×.
# Analysis
`analysis.R` performs an ANOVA followed by paired t-tests with a Holm adjustment:
$ R -q -s --no-save < analysis.R
`visualize.R` contains ggplot/rgl snippets for looking at the data (source
`analysis.R` first — it prepares the data frames those snippets use).
`Exploration.ipynb` is an interactive walkthrough of the full pipeline —
configuration → geometry → ray-fan sampling → Friedlander physics → a small
Warp run with plots and an independent NumPy cross-check. It runs in a couple
of minutes on the Warp CPU fallback (no GPU required):
$ uv sync --group notebook --extra cuda
$ uv run jupyter lab Exploration.ipynb
# Project Notes
The geometry is oriented so the helmet faces the negative Y direction. The
environment is Z-up, X-left, Y-rearward. The front SAPI plate sits at
`[0, -0.12, -0.5]` (just in front of and below the origin sensor, torso
height); the back plate is the same asset mirrored about Z at `[0, 0.18, -0.5]`
— 0.3 m behind the front plate, concave face toward the body.
# License
Copyright (C) 2025 SWGY, Inc
This project — including source code, the geometry assets in `assets/`, the
simulation outputs in `data/`, and the `Exploration.ipynb` notebook — is
licensed under the GNU Affero General Public License, version 3 or (at your
option) any later version. See the [`LICENSE`](./LICENSE) file for the full
text, or <https://www.gnu.org/licenses/agpl-3.0.html>.
`SPDX-License-Identifier: AGPL-3.0-or-later`