# CLAUDE.md — outram-foam-basic-lib
This crate is a pure-Rust translation of the OpenFOAM C++ primitive and
finite-volume library layer, scoped to the primitives needed to implement
compressible solvers equivalent to **rhoPimpleFoam**, **sonicFoam**, and
**rhoCentralFoam** (Kurganov-Tadmor central-upwind, density-based explicit).
The reference C++ source lives at:
`/home/teddy0/Documents/research/openfoam/`
## Reactor geometry is DRAWN for a human to check before it is trusted (HARD RULE)
**Maintainer direction, 2026-09-25.** Binds this crate. The same rule is in the
`CLAUDE.md` of `outram-mc-libs`, `nee_soon`, every `outram-foam-*` crate and
every crate downstream of them; a crate that newly depends on one of those
takes the rule into its own `CLAUDE.md` (check with `cargo metadata`).
**Whenever you build or change a complex reactor geometry** — CSG cells and
surfaces, lattices, pebble beds, TRISO particles, reflector zones, control-rod
bands, a CFD/FEM mesh, anything a solver will transport or integrate through —
**draw it, as images a human can open (PNG, or JPG/SVG), and hand them over**
before any result computed on it is reported as more than tentative.
- **Draw what the solver sees, not what you meant.** Render from the ASSEMBLED
geometry (cell / material lookup at each pixel, or the mesh itself), never
from the named constants. A picture of the constants hides exactly the
defects this rule exists to catch.
- **Minimum set:** an axial slice (R-Z / x-z) of the whole model; radial (x-y)
slices at the heights that matter; and, for nested geometry, zoomed slices at
every level down to the smallest (pebble, TRISO particle). Colour by
material, with a legend and the key dimensions marked.
- **Commit the images with the change** (beside the V&V record or the
manuscript package) and point the human at them by path in your summary.
Regenerate them whenever the geometry changes.
- **Say what you checked in them, and what you could not** — an image nobody
was told to look at checks nothing.
**Why.** On 2026-09-24/25 the HTR-10 model carried, at once: a bottom reflector
mirrored from the top and up to 107 cm short; a core cavity that grew with the
bed; pebbles interpenetrating by 1.1 cm with 4.8 % of core carbon clipped away
(gh:#309, #310); and a TRISO lattice holding 8240 particles while reporting
8340 (gh:#316, +353 pcm). Every run completed with green diagnostics. Each was
found by looking at the built geometry, not by the eigenvalue.
**Tools.** ~~`outram_mc_libs::geometry::plot` samples a slice of an assembled
CSG geometry; OpenMC-parity image output (PNG/JPG) is the preferred path once
it lands.~~ **UPDATED 2026-09-25 — it has landed (gh:#268):**
`outram_mc_libs::geometry::plot` is a port of OpenMC's plotter that writes PNG
directly — slices, wireframe and solid ray traces — verified pixel-for-pixel
against `openmc --plot`
(`crates/outram-mc-libs/verification_and_validation/geometry_plotting/`).
`render_material_slice` draws a material-coloured slice with a legend and cm
axes in one call; `crates/nee_soon/examples/htr10_geometry_images.rs` is the
worked example. For meshes, plot the mesh itself (cells, patches, zones).
## Maturity: DECLARED MATURE (2026-09-05)
The API-usability rules in the root `CLAUDE.md` ("Human interface layer",
and the Haiku dogfooding hard rule) **are in force for this crate**. See the
maturity gate in that file for what this means and how the bar is revised.
- **2026-09-05 — mature.** Bar: the `vv_*` verification suite passes —
conservation to machine precision (`epsilon = 1e-12`) across cyclic, AMI
matching and AMI non-conformal advection, diffusion and vector-Laplacian
paths; agreement with analytic references on reductions, volume integrals
and the Robin convective boundary; and **observed convergence order matching
theory** for the differentiation schemes. Evidence class: **analytical /
manufactured solution** plus internal consistency, supported by cross-code
comparison against OpenFOAM (563 in-source references, including a test that
the first application is unchanged from the OpenFOAM spelling).
Measured at declaration: **646 tests pass, 0 fail, 22 ignored**.
Note the bar here is deliberately about *the numerics*, not about a physical
benchmark: this crate is the primitive and finite-volume layer, so
conservation and convergence order are the properties that matter. Physical
validation belongs to the solvers built on it.
---
## Why this crate exists
OpenFOAM's C++ codebase is the negative example this port is designed to undo:
- **Runtime type registration** via macros (`addToRunTimeSelectionTable`) hides the
class hierarchy from any static tool — you cannot hover over a type and find its
implementors.
- **Dictionary-driven input** (`fvSolution`, `fvSchemes`) — valid keys exist only in
source comments and forum posts, not in any machine-readable interface.
- **wmake** — a bespoke build system nothing else can consume; OpenFOAM cannot be
used as a library by another project.
- **No units discipline** — `scalar` is `double`; passing pressure where velocity is
expected compiles silently and produces a wrong answer.
This crate replaces each of those with the Rust equivalent:
- Traits make the type hierarchy explicit and statically navigable via rust-analyzer.
- Struct fields with `///` doc comments replace runtime dictionaries — valid "inputs"
are visible on hover.
- Cargo makes this crate a normal library dependency.
- `uom` makes unit errors compile errors.
**The mandatory consequence:** every public item must be navigable with rust-analyzer
alone, by a developer with no prior OpenFOAM knowledge. See the root `CLAUDE.md`
"Human interface layer" section for the full rule.
**Layer 5 (solver logic — PISO/PIMPLE loops, multi-region coupling, turbulence
model registries) is intentionally excluded from this crate.** It belongs in
separate solver crates (`openfoam-icof`, `openfoam-cht`, `openfoam-rho`) that
depend on this crate. See the workspace `CLAUDE.md` for the planned crate list.
---
## Goal and scope
The crate climbs the OpenFOAM stack from the bottom up. Each layer depends
only on the one below it:
```
Layer 5 Solver logic rhoPimpleFoam / sonicFoam / rhoCentralFoam loops
Layer 4 Thermophysics ← THIS CRATE
• Fluid thermo fluidThermo / psiThermo / rhoThermo (fluid_thermo)
Layer 3 FV operators ← THIS CRATE
• fvm:: / fvc:: implicit + explicit operators (fv_operators)
• Source terms fvOptions / fvModels (fv_options)
• Flux limiters TVD limiters (limiters)
Layer 2 Fields + Mesh ← THIS CRATE
• Fields volScalarField, surfaceField (fields)
• Mesh fvMesh, AMI, region interfaces (mesh)
• Matrices lduMatrix, fvMatrix + solvers (ldu_matrix, krylov)
• Case I/O polyMesh / dictionary / field files (io)
Layer 1 Primitives ← THIS CRATE
• Tensor algebra Vector3, Tensor, SymmTensor, SphericalTensor
• Dense matrices scalarSquareMatrix, LU/QR/Cholesky/SVD
• Polynomial math linearEqn, quadraticEqn, cubicEqn, Polynomial<N>
• ODE solvers Euler, RKF45, RKDP45, Rosenbrock23/34, seulex, …
• Interpolation interpolationTable, interpolateXY, spline
• Math functions erfInv, incGamma, invIncGamma, …
• Thermophysics specie-level EOS / thermo / transport kernels (1h)
```
`outram-foam-basic-lib` covers **Layers 1–4**. Layer 5 (the solver loops) lives
in separate crates that depend on this one — see "Layer 5 is intentionally
excluded" above and the workspace `CLAUDE.md` for the planned crate list.
---
## Design rules (see also root CLAUDE.md)
### Enum dispatch for physics models
Every group of interchangeable physics models in this crate uses an enum, not a
trait object. The concrete models for EOS, thermo, and transport are a closed set —
enums give exhaustiveness checking and zero heap allocation.
```rust
// EOS enum — rust-analyzer shows all options; adding one forces all match sites to update
pub enum Eos {
PerfectGas(PerfectGas),
RhoConst(RhoConst),
IcoPolynomial(IcoPolynomial<8>),
PengRobinson(PengRobinsonGas),
}
// Thermo enum wraps an EOS variant
pub enum Thermo {
HConst(HConstThermo<Eos>),
Janaf(JanafThermo<Eos>),
HPolynomial(HPolynomialThermo<Eos, 8>),
}
```
Trait definitions (`EquationOfState`, `ThermoModel`, `TransportModel`) remain as
compiler contracts on each concrete struct — they are not used for `dyn` dispatch.
### No `Box<T>`, no lifetime parameters, no trait objects
Follows the workspace rule. All uom quantity types are `Copy` — own them by value.
Shared mesh or field data: `Arc<FvMesh>`, `Arc<RwLock<VolScalarField>>`.
## Implementation rules
### `extern "C"` policy
`extern "C"` blocks are **permitted if and only if** the called function
compiles and links natively on **all five** target platforms:
| Platform | C runtime | Notes |
|---|---|---|
| Linux | glibc / musl (`libm`) | always available |
| macOS | Apple `libSystem` | always available |
| Windows | MSVC CRT (VS 2013+) or MinGW | always available on modern toolchains |
| Android | Bionic libc | `erf`/`erfc`/`lgamma` since API 9; `tgamma` since API 21 |
| iOS | Apple `libSystem` | always available |
If a function is missing from **any** of these five (e.g. it only exists on
POSIX systems), it must be implemented in pure Rust instead.
~~The current `extern "C"` calls to `erf`, `erfc`, `tgamma`, and `lgamma` in
`inc_gamma.rs` and `inv_inc_gamma.rs` satisfy this rule (minimum Android
API 21 for `tgamma`, which covers ~98% of active devices).~~
**CORRECTED 2026-10-20** (gh:#985) — there are no `extern "C"` calls left in
this crate: `inc_gamma.rs` and `inv_inc_gamma.rs` take `erf`, `erfc`,
`tgamma` and `lgamma` from `petir::real` (pure Rust, checked by grep for
`extern "C"` under `src/`).
~~Prefer Rust stdlib equivalents where they exist — `f64::gamma()` and
`f64::ln_gamma()` (stable since Rust 1.83) can replace `tgamma`/`lgamma`
with no FFI at all.~~ **SUPERSEDED 2026-10-20** (gh:#985, maintainer
direction): every `f64` transcendental goes through PETIR
(`petir::mathf::RealMath`, or `petir::real` for the special functions), never
the platform libm, for cross-platform bit identity. `sqrt`, `abs`, `floor`,
`ceil`, `trunc` and `mul_add` stay on `std` (exact or correctly rounded).
---
## Porting workflow (MANDATORY — follow for every new port)
After adding any new type, function, or module, you MUST do **both**:
### 1. Update `src/prelude.rs`
Add all new public items to the appropriate `pub use` block in `src/prelude.rs`
so that a wildcard import `use outram_foam_basic_lib::prelude::*` exposes them.
### 2. Update `README.md`
Add a row for each newly ported item to the **Ported items** table in
`README.md`. The table lives under the `## What's implemented` heading.
Format: `| Module | Rust type / fn | C++ source | Notes |`.
Verification:
```bash
cargo test -p outram-foam-basic-lib --lib --release # must be green before committing
cargo test -p outram-foam-basic-lib --doc --release # must be green before committing
```
**Rule: always use `--release` for builds and tests.** Never run in debug mode.
### Doc-comment code blocks
Any ```` ```rust ```` block in a doc comment is compiled **and executed** as a
doctest (`cargo test --doc`). Rules:
- **Never use `ignore` to silence a failing doctest.** Fix the code instead.
- `crate::` does not work in doctests — rustdoc compiles each snippet as an
external user of the crate. Use the external crate name instead:
`outram_foam_basic_lib::` (e.g. `use outram_foam_basic_lib::thermophysics::imports::*;`).
- Do not use `crate::` in doc-comment examples at all; always write the
fully-qualified external path so the snippet is self-contained.
- ```` ```rust,no_run ```` is acceptable only for snippets that are genuinely
side-effectful or require external resources at runtime.
---
## Critical translation gotcha — field `name` must not grow
**Never build a field's `name` String compositionally inside an arithmetic
operator.** `Field`/`VolField`/`SurfaceField`'s `Add`/`Sub`/`Neg`/`Mul` must
leave `self.name` as the left operand's name — they must **not** do
`self.name = format!("({} + {})", self.name, rhs.name)`.
A solver repeatedly reassigns a persistent field from an expression containing
that same field (e.g. `rho = rho + div(phi)`). With compositional naming the
`name` string then **doubles in length every timestep** (`2^step` growth) —
invisible in the field *data* but blows the process to tens of GB within ~25
steps. This exact bug hung the rhoPimpleFoam `compressible_lid_cavity` test
(24 GB, SIGTERM). The fix lives in `fields/vol_field.rs` and
`fields/surface_field.rs`; keep the name a short, stable label (matching
OpenFOAM's fixed `IOobject` name), not an audit trail. Full write-up in
`docs/cpp-source-reference.md` (Translation notes).
---
## Reference material (read on demand, not per turn)
- **`docs/cpp-source-reference.md`** — the full C++ source → Rust map for
Layers 1–5, the planned Rust module layout, and all per-type translation
notes. Read the relevant layer's section when porting that layer.
- **`docs/porting-roadmap.md`** — remaining work before the solver crates can
be written (icoFoam / chtMultiRegionFoam prerequisites), the `#[ignore]`-d
known test failures to investigate, and the prioritised test backlog (P0–P2).
## Meshing nexus: outram-blender exports into this crate (GitHub #492, 2026-10-03)
`outram-blender`'s neutral `unstructured::UnstructuredMesh` is the one mesh
description the FV, FE and Monte Carlo solvers are built from. Its `foam-export` feature converts neutral meshes to and from `io::PolyMesh` and to `mesh::FvMesh` (`convert::foam`, through `PolyMesh::to_fv_mesh`, so the FV geometry is computed here). **Since the same day this crate is also the one copy of `FvMesh`, `MeshError`, the 1-D mesher and the tensor primitives** for `tampines-steam-tables` and `outram-park-fork-coolprop`, which re-export them (their copies were code-identical); a change to those items is now a change to both. Solver infrastructure (LDU matrices, Krylov solvers) stays here (maintainer decision).
**This crate must never depend on `outram-blender`** (GitHub #486 dependency
rule 1): blender depends on this crate, optionally, to export into it, and
Cargo counts optional dependencies when it looks for cycles. Mesh types stay
defined here; the converters live in `outram_blender::unstructured::convert`.