Architecture & Design
A single unified solver runs on CPU or GPU by injecting a MathBackend. Where an
abstraction earns its keep it lives in a co-located base.py (HeatSolver,
MathBackend, LaserPath); single-implementation interfaces are deliberately
avoided.
Directory Structure
spectral_galerkin_heat/
├── pyproject.toml # Package definition and dependencies
├── README.md
├── src/
│ └── spectral_galerkin_heat/
│ ├── __init__.py # Package version + public API
│ ├── runner.py # StandaloneHeatRunner: simulation loop orchestrator
│ ├── core/
│ │ ├── parameters.py # Data classes: SimulationContext, NumParams, MaterialParams, …
│ │ ├── laser.py # LaserState dataclass + LaserPath ABC
│ │ └── vector.py # Vec3: immutable (x, y, z) value type
│ ├── backends/
│ │ ├── base.py # MathBackend: bundles array module (xp) + kernels
│ │ ├── numpy_backend.py # NumpyBackend (CPU)
│ │ ├── cupy_backend.py # CupyBackend (GPU)
│ │ ├── _registry.py # backend registry: register_backend + get_backend
│ │ └── __init__.py # get_backend(name); registers built-in backends
│ ├── solvers/
│ │ ├── base.py # HeatSolver ABC
│ │ ├── spectral.py # SpectralSolver (unified CPU/GPU, backend-injected)
│ │ ├── spectral_cpu_linear.py
│ │ └── __init__.py # build_solver(context): solver selector
│ ├── physics/
│ │ ├── spectral_state.py # Backend-agnostic state (SpectralGrid, FineMeshState, …)
│ │ ├── spectral_ops.py # Backend-agnostic free functions
│ │ ├── spectral_cpu_kernels.py # Low-level CPU spectral kernels (NumPy / Numba)
│ │ ├── spectral_gpu_kernels.py # Low-level GPU spectral kernels (CuPy / CUDA)
│ │ └── spectral_helpers.py # DCT reconstruction utilities
│ └── io_utils/
│ ├── spectral_fs_io.py # LocalFSIOManager (HDF5 + XDMF)
│ ├── xdmf_io.py # XdmfBuilder + structured/unstructured readers
| ...
├── simulations/
│ ├── main.py # CLI entry point: parses YAML, builds solver, runs simulation
│ └── examples/ # Example YAML configs (increasing complexity) + orchestrator.py
│ └── paths/ # G-code laser paths for the examples
└── docs/
└── ARCHITECTURE.md
Component Descriptions
CLI (
simulations/main.py): Parses a YAML config, builds the solver viabuild_solver(context), and delegates execution toStandaloneHeatRunner.Runner (
runner.py):StandaloneHeatRunner— the simulation loop orchestrator. Owns thewhile t < t_endloop; receives aHeatSolverand aLocalFSIOManagerby injection (the I/O manager defaults to a freshLocalFSIOManager), delegating all physics to the former and all I/O to the latter.Solver selector (
solvers/__init__.py):build_solver(context)— a single dispatch onbackendthat returns the configuredHeatSolver. It replaces the formerSimulationFactoryhierarchy, which only ever differed in this one choice.Solver base (
solvers/base.py):HeatSolverABC — declaresinitialize(),step(), andfinalize(). The unifiedSpectralSolverruns on CPU or GPU purely through its injectedMathBackend;SpectralSolverCPULinearis a separate linear variant.Backends (
backends/):MathBackendbundles an array module (xp, NumPy or CuPy) with its matching physics-kernel module. A name→factory registry (_registry.py) resolves backends:get_backend(name)returnsNumpyBackendorCupyBackend, so the same solver code targets either device. Extra backends register themselves with the@register_backend("name")decorator, so adding one never requires editingget_backend.I/O (
io_utils/spectral_fs_io.py):LocalFSIOManager— the concrete filesystem I/O handler (HDF5 + XDMF). It owns the full I/O contract (initialize,process_step,process_end,finalize,save_step, …);save_stepdispatches per output type to small_save_*helpers.Laser (
core/laser.py):LaserStatedataclass andLaserPathABC. Used by the solver (to query laser position/power) andio_utils/gcode_path.py.Physics (
physics/): Backend-agnostic state and free functions (spectral_state,spectral_ops) plus the pure numerical kernels (spectral_cpu_kernelsNumba /spectral_gpu_kernelsCuPy).
Adding a backend (PyTorch, JAX, …)
The solver doesn’t need a specific array library to run on. It only uses
backend.xp (the array module) and backend.kernels (the math functions), and
almost all the physics code already works with any NumPy-like xp. So a new
backend is mostly plumbing — supplying four things:
1. An array module xp. It needs to behave like NumPy: zeros, arange,
exp, sum, in-place writes (multiply(..., out=), a[0,0,0] = v),
.astype, etc. NumPy and CuPy work out of the box. PyTorch and JAX could be used by writing wrappers that rename a few things (and JAX arrays
can’t be modified in place, so those writes need adapting).
2. A kernels module. Most functions are reused as-is from spectral_ops and
spectral_state — don’t rewrite them. You only need to provide the ones that
truly depend on the library:
the cosine transform (
DCT_II/IDCT_II);the sub-pixel shift (
_ndshift);a couple of simple per-cell formulas (the evaporation and latent-heat source terms, and the Gaussian flux
core.laser.super_gaussian_flux).Then subclass
SpectralSolverState, bind your ownxp, and give those primitives toBackendHooks. Copyspectral_cpu_kernels.pyas a template.
3. A MathBackend and registration. Subclass MathBackend with your name,
xp, and kernels, and add @register_backend("mybackend").
4. A config name. Hook the simulation.backend string up to your backend in
solvers/__init__.py (build_solver).
Keep the kernels working for both float32 and float64, and check your backend
against the CPU one with test_cpu_gpu_equivalence_e2e in
tests/integration/test_pipeline.py.
Architecture Diagram
flowchart TD
%% --- Styling ---
classDef client fill:#ff7675,stroke:#d63031,stroke-width:2px,color:#000;
classDef abstract fill:#81ecec,stroke:#00cec9,stroke-width:2px,stroke-dasharray:5 5,color:#000;
classDef concrete fill:#55efc4,stroke:#00b894,stroke-width:2px,color:#000;
classDef product fill:#fdcb6e,stroke:#e17055,stroke-width:2px,color:#000;
classDef core fill:#a29bfe,stroke:#6c5ce7,stroke-width:2px,color:#000;
%% --- Nodes ---
CLI["simulations/main.py"]:::client
Runner["runner.py\nStandaloneHeatRunner"]:::client
Context["core/parameters.py\nSimulationContext"]:::core
Laser["core/laser.py\nLaserState · LaserPath"]:::core
Build["solvers/__init__.py\nbuild_solver()"]:::client
ISolv["solvers/base.py\n≪abstract≫ HeatSolver"]:::abstract
IBack["backends/base.py\n≪abstract≫ MathBackend"]:::abstract
Solver["solvers/spectral.py\nSpectralSolver"]:::product
CPUBack["backends/numpy_backend.py\nNumpyBackend"]:::concrete
GPUBack["backends/cupy_backend.py\nCupyBackend"]:::concrete
FSIO["io_utils/spectral_fs_io.py\nLocalFSIOManager"]:::product
%% --- Relationships ---
CLI -->|"creates context"| Context
CLI -->|"build_solver(context)"| Build
CLI -->|"instantiates with solver + io"| Runner
Build -->|"returns"| Solver
Runner -->|"calls"| ISolv
Runner -->|"calls"| FSIO
Solver -.->|"implements"| ISolv
Solver -->|"injected with"| IBack
IBack -.->|"implemented by"| CPUBack
IBack -.->|"implemented by"| GPUBack
Solver -->|"queries"| Laser