outram-foam-basic-lib 0.1.7

OpenFOAM primitive functions translated to Rust -- an independent OUTRAM PARK fork, not the official OpenFOAM software
Documentation
# 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`.