Skip to content

feat(pffdtd): add PFFDTD (Brian Hamilton) wave-equation FDTD method#11

Open
Burhanuddin98 wants to merge 1 commit into
choras-org:developfrom
Burhanuddin98:feat/pffdtd-method
Open

feat(pffdtd): add PFFDTD (Brian Hamilton) wave-equation FDTD method#11
Burhanuddin98 wants to merge 1 commit into
choras-org:developfrom
Burhanuddin98:feat/pffdtd-method

Conversation

@Burhanuddin98
Copy link
Copy Markdown

Summary

Adds the upstream PFFDTD wave-equation FDTD solver (bsxfun/pffdtd, MIT) as a new simulation method package, following the canonical copier-template layout that DE / DG / pyroomacoustics use.

Goal: give CHORAS users a wave-equation FDTD option (FDTD complements DG and the diffusion equation by being a phase-coherent wave solver — the right tool for low-frequency room modes and detailed reflection patterns).

Companion PR

A separate PR will be opened against `choras-org/CHORAS` once this lands, to (a) bump the simulation-backend submodule pointer to the merge commit and (b) add the corresponding `pffdtd-method` service to `docker-compose.yml` under `profiles: sim_method`. I'll wait for this PR to merge first so the submodule bump references a SHA that actually exists in this repo.

Files added

```
pffdtd_method/
├── Dockerfile # nvidia/cuda:12.6.0-devel; clones bsxfun/pffdtd@aa319f6; compiles c_cuda
├── pyproject.toml # numpy<2; tqdm/psutil/memory_profiler/plotly/etc. transitive deps
└── pffdtd_interface/
├── init.py, cli.py, main.py # canonical scaffolding (matches DE/DG)
├── definition.py # SimulationMethod ABC (copied from de_method)
└── PFFDTDinterface.py # wrapper: sim_setup -> FDTD -> postprocess -> JSON

example_settings/pffdtd_setting.json # 7 UI knobs: c0, fmax, ppw, IR length, T, RH, GPU on/off
methods-config.json # adds the PFFDTD entry
```

Design notes (the parts a reviewer should sanity-check)

  • CPU-primary, GPU-opportunistic. The wrapper auto-detects whether the container has GPU passthrough (i.e. whether the CHORAS executor passes `--gpus all` to `docker run`). If yes, it subprocesses Hamilton's compiled `fdtd_gpu` binary; if not, falls back to the pure-Python numba CPU engine (`fdtd.sim_fdtd.SimEngine`). So the method works on every host CHORAS supports today; GPU acceleration is a bonus when the executor is later configured to enable it (no executor change in this PR).
  • No `local_executor.py` change required. This PR is self-contained to `simulation-backend`.
  • No ROM. Pure pass-through over upstream PFFDTD.
  • Python 3.11 compat. Monkey-patch `multiprocessing.shared_memory.SharedMemory.close()` to swallow the `BufferError` that 3.11+ raises when memoryview consumers still exist (PFFDTD's voxelizer triggers this; voxelization is already complete by the time `close()` runs).
  • Single-process voxelization. `Nprocs=1` forced — avoids the SharedMemory issue entirely on modern Python. Voxelize step is fast enough that single-proc isn't a bottleneck for typical CHORAS room sizes.
  • numpy<2 pin — PFFDTD predates numpy 2.0 alias removals (`np.bool8`, `np.complex_`, `np.float`). Dockerfile also sed-patches those at install time as belt-and-braces.
  • c_cuda fatbinary — sm_60 through sm_90 plus PTX, so one image runs on any modern NVIDIA card without a rebuild.

Verification

End-to-end run against `MeasurementRoom.obj` from `example_geometries/`:

  1. Voxelize → `vox_out.h5` written
  2. `sim_setup` → `cart_grid.h5`, `comms_out.h5`, `sim_consts.h5`, `sim_mats.h5`
  3. FDTD (CPU numba engine — no GPU passthrough on the test host)
  4. Post-process: HP at 10 Hz, LP at 0.9 × grid Nyquist, resample to 48 kHz
  5. IR written to `results[0].responses[0].receiverResults` (flat samples, DG-style)
  6. Per-band metrics in `parameters` (edt, t20, t30, c80, d50, ts, spl_t0_freq)
  7. Auralization CSV written alongside JSON
  8. Container exits 0; `percentage = 100` in final JSON

Output schema

Matches the slide-10 schema with the DG-style flat IR in `receiverResults`:

```json
{
"results": [{
"percentage": 100,
"resultType": "PFFDTD",
"responses": [{
"x": ..., "y": ..., "z": ...,
"receiverResults": [...IR samples at 48 kHz...],
"receiverResultsUncorrected": [...same...],
"parameters": {
"edt": [...per band...],
"t20": [...], "t30": [...],
"c80": [...], "d50": [...],
"ts": [...], "spl_t0_freq": [...]
}
}]
}]
}
```

Test plan

  • Containerised method builds end-to-end (CUDA devel base + PFFDTD clone + c_cuda compile + pip install all transitive deps)
  • Python imports clean inside the image (`PFFDTDMethod`, `sim_setup`, `SimEngine`, `VoxGrid`, `materials.adm_funcs`)
  • End-to-end run on MeasurementRoom geometry produces a valid 48 kHz IR + per-band metrics + auralization CSV
  • CPU fallback works on hosts without GPU passthrough (current state of CHORAS upstream executor)
  • Review of the GPU-opportunistic design — happy to make GPU explicit-only if you prefer it not auto-detect
  • Review of `numpy<2` pin and the few sed patches in the Dockerfile

Context

This PR comes from following the 2nd CHORAS Developer Workshop walkthrough (Silvin's slides on the copier template + contribution flow). Happy to address any deviation from the conventions DE/DG follow.

Adds the upstream PFFDTD wave-equation FDTD solver (bsxfun/pffdtd) as a new simulation method package, following the canonical copier-template layout that DE / DG / pyroomacoustics use.

What's added:

  pffdtd_method/
    Dockerfile                                  -- nvidia/cuda:12.6.0-devel base; clones bsxfun/pffdtd@aa319f6; compiles
                                                   Hamilton's c_cuda binary as a multi-arch fatbinary (sm_60..sm_90 + PTX);
                                                   sed-patches deprecated numpy aliases (np.bool8, np.complex_, np.float)
    pyproject.toml                              -- numpy<2 pin (PFFDTD predates numpy 2.0 alias removals); transitive deps
                                                   (tqdm, psutil, memory_profiler, plotly, polyscope, trimesh, soundfile,
                                                   gmsh, numba, h5py, scipy, matplotlib, resampy)
    pffdtd_interface/
      __init__.py, __cli__.py, __main__.py      -- canonical scaffolding (matches DE/DG)
      definition.py                             -- SimulationMethod ABC (copied from de_method to avoid cross-package import)
      PFFDTDinterface.py                        -- the actual wrapper; runs sim_setup -> FDTD -> postprocess; produces a
                                                   48 kHz IR in receiverResults plus per-band edt/t20/t30/c80/d50/ts/spl

  example_settings/pffdtd_setting.json          -- 7 user-facing knobs (c0, fmax, ppw, IR length, T, RH, GPU on/off)
  methods-config.json                           -- adds the PFFDTD entry

Design notes:

  - CPU-primary, GPU-opportunistic. The wrapper auto-detects whether the
    container has GPU passthrough (i.e. whether the CHORAS executor is
    configured to pass --gpus all) and only invokes Hamilton's c_cuda binary
    in that case. Otherwise it falls back to the pure-Python numba CPU engine
    (PFFDTD's fdtd.sim_fdtd.SimEngine). So the method works on every host
    CHORAS supports today; GPU acceleration is a bonus when wired.

  - No backend changes. This PR is self-contained to simulation-backend.
    A companion docker-compose.yml entry (plus a submodule-pointer bump
    after this PR merges) will be a follow-up PR to choras-org/CHORAS.

  - No ROM. Pure pass-through over upstream PFFDTD. No surrogate model.

  - Python 3.11 compat. Multiprocessing SharedMemory.close() raises
    BufferError on 3.11+ when memoryview consumers still exist; PFFDTD's
    voxelizer triggers this. Monkey-patched in the wrapper to swallow the
    specific error (data is already on disk by the time close fires).

  - Single-process voxelization. PFFDTD's voxelizer Nprocs forced to 1
    to avoid the SharedMemory issue entirely on modern Python; the
    voxelize step is fast enough that single-proc isn't a bottleneck.

Verified end-to-end against MeasurementRoom.obj from example_geometries/:
voxelize -> sim_setup -> SimEngine.run_all() -> postprocess (HP 10 Hz,
LP 0.9*Nyquist, resample to 48 kHz) -> IR written to receiverResults, per-
band metrics in parameters, auralization CSV written alongside JSON,
container exits 0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant