OTOM Logo

OTOM CFD WebGPU

Advanced Simulation, 3D Raymarching, & AI Orchestration

Table of Contents

1. System Overview & Architecture

OTOM CFD is a high-performance computational fluid dynamics solver running entirely in the browser via WebGPU. It is heavily inspired by the methodologies popularized by the WaterLily.jl Julia package, specifically bringing Cartesian-grid, boundary-data immersion methods to massive parallel compute shaders.

The software supports both 2D and 3D incompressible Navier-Stokes simulations, a dedicated 2D compressible Euler engine (HLLC Riemann solver) for shock-bearing high-speed flows, multi-phase flows, and high-fidelity volumetric raymarching—all orchestrated through an interactive GUI and an integrated LLM-powered AI assistant.

2. Comprehensive Parameter Guide

The simulation is driven by a uniform buffer passed to the GPU every frame. Parameters are split between physical and visual properties.

ParameterDomainDescription / Physical Meaning
Engine Solver2DIncompressible (projection Navier-Stokes, low speed) or Compressible (Euler/HLLC, shocks & high speed). See §4.
Grid Resolution2D / 3DNumber of computational cells (e.g., 910x512 in 2D, 64³ to 256³ in 3D). Dictates spatial frequency limits.
Flow Mode2D / 3DVelocity enforces a constant inlet speed. Pressure drives flow via boundary pressure gradients (\(\Delta P\)).
Flow Speed (\(U_\infty\))2D / 3DInlet velocity in grid cells per second (or m/s if SI units enabled).
Lattice dt (\(\Delta t\))2D / 3DTime advanced per sub-step. Governs the CFL condition (\(CFL = U \frac{\Delta t}{\Delta x}\)).
Physics Steps2D / 3DNumber of compute passes executed per visual render frame. Increases stability for high-speed flows.
Jacobi Itr2D / 3DIterations of the Poisson pressure solver. Higher values ensure stricter incompressibility (\(\nabla \cdot \mathbf{u} = 0\)).
Viscosity (\(\nu\))2D / 3DKinematic viscosity. Defines fluid thickness. Air ~0.015, Water ~0.001.
Visual ParameterDomainDescription
View Mode2D / 3DSelects the Eulerian field to display: Vorticity, Velocity, Pressure, Phase, Schlieren (Pressure Gradient), Numerical Schlieren, or Dye Streams.
Dye Streams2D (Incompressible)2–16 separately coloured passive tracer streams, so you can follow where each one goes around and behind the body. Release: Inlet Streams (continuous lanes from every inflow edge, or the painted inlet in Control Volume mode), Initial Bands (the domain starts striped), or Both. Stream Thickness is the dyed fraction of each lane (1 = bands touch). Where streams mix, their colours average. Changing a setting or pressing Restart Dye re-seeds the dye without resetting the flow. Tip: turn off Liquid Mode for the cleanest view.
Dye Streams (3D)3D (Incompressible)Same idea in 3D (Field → Dye Streams). Shape: Tubes (a rake of round coloured filaments across the −X inlet face, like a smoke rake in a wind tunnel) or Sheets (horizontal coloured layers). Rendered by the normal volume / isosurface / slice views; Threshold (~0.1–0.3) sets how faint dye still shows. A closed domain (no inlet flow) starts filled instead. Same Release / Thickness / Restart Dye controls as 2D.
Gain / VScale2D / 3DMultiplicative factor applied before colormapping. Adjusts the sensitivity of the visualizer.
Theme2D / 3DColormap applied to the main viewport field: Seismic, Smoky Seismic, Jet, Hot, Viridis, Turbo, Magma, Inferno, Plasma. See below.
Isoline Density2D / 3DFrequency of contour lines superimposed on the field to visualize gradients.
Arrow Density2D / 3DSpacing of the vector field arrows. In 3D, this samples a specific 2D slice defined by Slice Depth.

Colormap Themes

Jet and Hot are visually similar names but genuinely different palettes: Jet is the classic MATLAB-style rainbow spectrum — dark blue → blue → cyan → green → yellow → red → dark red, spanning the field's full normalized range — while Hot is a black/red/yellow ramp driven by the signed field value rather than a spatial position. Both live in the same shared colormap() WGSL function, so any 2D or 3D view (main field, Force Display overlay, particle color-by-field) can use either. Force Theme (Aerodynamics tab, 2D and 3D) is a second, independent colormap selector for the pressure/force overlay — it can be set differently from the main Theme, e.g. Seismic for the flow field with Jet reserved for the Force Display, so the two visual channels never fight for the same color language.

Colormap Window, Auto Range & Colour Scale

Colormap Min/Max defines the window mapped onto the colormap's two ends — field values at Min land on the cold end, values at Max on the hot end. The historical fixed mapping is just the default window (\(-1\), \(1\)); many fields don't naturally sit in that range (an incompressible pressure field, for instance, floats at an arbitrary additive gauge offset with no fixed natural range at all), so a mismatched window can make a field with real structure look like a single flat colour.

Auto Range removes the guesswork: while enabled, the app periodically measures the field actually on screen (its 1st/99th percentile, robust against a single outlier cell) and fits Colormap Min/Max — and, in 3D, the render Threshold — to it automatically. The read-only Measured line shows the fitted percentiles alongside the field's true min/max/mean — useful on its own for telling a field that "looks flat" (genuinely tiny values) apart from one that's merely mis-scaled (a large mean with real but small variation riding on top of it, the exact signature of the pressure gauge-offset case above).

Colour Scale (Linear or Log) is a separate, complementary control: it doesn't change the window, it redistributes contrast within it. Log applies a signed, sign-preserving logarithmic curve after the Min/Max window is applied, so weak structure near the middle of the range becomes visible without clipping the strong values at the ends — the window's two ends still map to exactly \(\pm 1\) either way. Fit the window with Auto Range first, then switch to Log if fine detail is still hard to see.

Both 2D and 3D pressure sampling honour this pipeline identically, and in 3D compressible mode the window/scale apply to the same decoded velocity/pressure textures the shared volume renderer already uses — Auto Range and Log work in the Compressible engine exactly as they do in Incompressible.

Physical Calibration & SI Units 2D3D

By default, every number in the sidebar is a lattice unit — self-consistent relative to the grid, not inherently meters or seconds. Flipping SI Units Mode (2D: "Physical Calibration" folder; 3D: its own "Physical Calibration" folder, right below Simulation — the two are independent, so turning it on in one tab never silently recalibrates the other) reinterprets Flow speed/Viscosity as real physical quantities (m/s, m²/s) and converts them into the lattice units the solver actually runs on:

$$u_{lattice} = u_{SI} \cdot \frac{\Delta t_{SI}}{\Delta x}, \qquad \nu_{lattice} = \nu_{SI} \cdot \frac{\Delta t_{SI}}{\Delta x^2}, \qquad \Delta x = \frac{\text{Domain Width (m)}}{\text{Resolution}}$$

where Domain Width (m) and Real-world Time Step (s) are the two calibration inputs you provide, and Lattice dt is fixed to 1.0 internally once SI mode is on (the real timestep is already folded into the conversion above). In 3D, Domain Width (m) is calibrated for Length (X) = 1.0 (the domain's default ±1 world extent) and automatically rescales if you change the Length (X) slider, so resizing the non-cubic control volume doesn't silently invalidate your calibration. Height (Y)/Depth (Z) can still differ from Length(X) (the domain doesn't have to be a cube) — each axis then has a physically different cell size, which the solver's diffusion/gradient stencils don't individually correct for (they operate uniformly in normalized grid-cell space); this only matters if your domain is significantly non-cubic and you need diffusion to be quantitatively exact along the stretched axis, not just qualitatively correct.

Sanity-checking your inputs: both the 2D and 3D Physical Calibration folders show three read-only numbers — Cell Size (mm) (the physical size of one grid cell), Effective CFL (the Courant number of the actual lattice velocity that reaches the GPU, in either units mode — named "Effective" rather than "CFL Number" to avoid confusion with the Compressible engine's own CFL Number parameter, which is a different thing: a user-set target for the acoustic timestep, not a live diagnostic), and Est. Reynolds (computed from Flow speed, Viscosity, and a characteristic length derived from Domain Width — named "Est." rather than "Reynolds (Re)" to avoid confusion with the Aerodynamics folder's own Reynolds display, which only updates while "Live Analysis" is on; this one is always live). If Effective CFL climbs too high, a warning banner appears above the canvas — lower Flow speed, raise Domain Width (m), or shrink Real-world Time Step. A CFL warning means the fluid is moving more than about one grid cell per step, which produces visibly wrong (oscillating, exploding, or NaN) results — this is the first thing to check before trusting any result once SI units are on.

Turning SI Units Mode on auto-corrects Viscosity for the selected Fluid Material (unless it's set to "Custom"): the lattice-tuned viscosity values (e.g. Air's default 0.015) were chosen to look right as abstract lattice numbers, not as real physical properties — reinterpreting that same number as literal m²/s the instant SI mode turns on would imply an object roughly 1,000× more viscous than real air. To prevent a jarring change in flow behaviour, the app substitutes the real 20°C kinematic viscosity for the current material (Air ≈ 1.48×10⁻⁵ m²/s, Water ≈ 1.0×10⁻⁶, Engine Oil ≈ 1.2×10⁻⁴, Glycerin ≈ 1.18×10⁻³ m²/s) and widens the Viscosity slider's range/precision to represent these much smaller numbers usefully. Toggling SI Units Mode back off restores the original lattice-tuned value. Switching Fluid Material while SI is on picks from the real-value table instead of the lattice one.

Why the flow can still look "strange" right after enabling SI units even with this correction: real physical parameters at a coarse simulation grid very easily imply a Reynolds number in the hundreds of thousands or millions — e.g. real air, a modest few-m/s breeze, and a few-meter domain is already Re in the millions, which no 64³–256³ grid can resolve without looking chaotic and rapidly-changing, LES turbulence modelling or not. This is not a bug: it's the same "under-resolved turbulence" limitation every CFD tool has when asked to simulate a too-high-Re flow on too-coarse a grid. The Est. Reynolds readout crossing roughly 5,000 while SI Units Mode is on triggers a "HIGH REYNOLDS" banner explaining this and suggesting concrete fixes — raise Viscosity (a physically thicker fluid, or accept a coarser numerical/eddy viscosity), lower Flow speed, or shrink the domain (Domain Width and Real-world Time Step alone won't fix this if changed proportionally to each other, since that leaves the implied physical scenario — and therefore the Reynolds number — unchanged).

The Ground Truth tab (§10) remains the recommended way to validate results once your CFL is healthy and Reynolds number is in a resolvable range: it compares measured Cd/Cl against textbook correlations for canonical shapes at the current Reynolds regime.

3. Core Numerics & Theory Solver

The core fluid solver uses a staggered Cartesian grid with a fractional-step (Projection) method.

Advection & Projection

1. Advection: The velocity field is self-advected using a semi-Lagrangian back-tracing scheme. To reduce numerical dissipation, we utilize high-order filtering.
2. Divergence: The divergence of the intermediate velocity field (\(\mathbf{u}^*\)) is calculated.
3. Pressure Poisson: A Jacobi iterative solver resolves the pressure field needed to counteract the divergence.
4. Gradient Subtraction: The velocity is corrected: \(\mathbf{u}^{n+1} = \mathbf{u}^* - \Delta t \nabla p\).

Boundary Data Immersion Method (BDIM)

Instead of conforming a mesh to an obstacle, OTOM CFD uses Brinkman Penalization. Solid objects are defined by exact analytical Signed Distance Fields (SDFs). The velocity inside the SDF (\(d \le 0\)) is forced to match the object's velocity, allowing for arbitrary, moving, and overlapping geometries at near-zero computational cost.

4. Compressible Flow Solver Euler / HLLC

Alongside the default incompressible projection solver, OTOM CFD provides a dedicated compressible engine (set Engine Solver = Compressible) for high-speed, transonic, and supersonic flows where density variation and shock waves are physically significant. Instead of enforcing incompressibility, it solves the 2D compressible Euler equations in conservative finite-volume form, so it naturally captures shocks, expansion fans, and contact discontinuities.

Governing Equations

The conserved state stored in every cell is \(\mathbf{Q} = [\rho,\ \rho u,\ \rho v,\ E]^T\) (density, x/y momentum, total energy), evolved by the hyperbolic conservation law:

$$\frac{\partial \mathbf{Q}}{\partial t} + \frac{\partial \mathbf{F}(\mathbf{Q})}{\partial x} + \frac{\partial \mathbf{G}(\mathbf{Q})}{\partial y} = 0$$

The system is closed by the ideal-gas equation of state, where \(\gamma\) is the ratio of specific heats and \(a\) the local speed of sound:

$$p = (\gamma - 1)\left(E - \tfrac{1}{2}\rho(u^2 + v^2)\right), \qquad a = \sqrt{\frac{\gamma p}{\rho}}$$

Godunov Finite-Volume Scheme & HLLC Riemann Solver

Each cell is advanced by the net flux across its four faces. The flux at every interface is obtained by solving a local Riemann problem with the HLLC (Harten–Lax–van Leer–Contact) approximate solver, which restores the central contact/shear wave that the simpler HLL solver smears out—essential for clean slip lines and wakes:

$$\mathbf{Q}^{n+1}_{i,j} = \mathbf{Q}^{n}_{i,j} - \frac{\Delta t}{\Delta x}\!\left(\mathbf{F}_{i+\frac12} - \mathbf{F}_{i-\frac12}\right) - \frac{\Delta t}{\Delta y}\!\left(\mathbf{G}_{j+\frac12} - \mathbf{G}_{j-\frac12}\right)$$

The full stencil—four face fluxes plus the conservative update—is fused into a single WebGPU compute pass for robustness and throughput. The scheme is first-order in space and time; a positivity guard floors density and internal energy each step to guarantee \(\rho>0\) and \(p>0\), preventing the NaN blow-up that otherwise occurs across strong shocks.

Boundary & Obstacle Treatment

  • Inlet (left): Dirichlet free-stream state fixed by the user's Inlet Mach, Inlet Density, and Inlet Pressure.
  • Outlet (right): Zero-gradient (transmissive) extrapolation.
  • Top / bottom: Inviscid slip walls (\(v = 0\)).
  • Obstacles: Treated as inviscid reflecting slip walls. The wall-face flux carries pressure only, \(\mathbf{F}_{wall} = [\,0,\ p\,n_x,\ p\,n_y,\ 0\,]^T\), which produces detached bow shocks ahead of blunt bodies and oblique shocks off wedges.

Stability, Parameters & Visualization

The explicit time step is bounded by the acoustic CFL condition \(\Delta t < \Delta x / (|u| + a)\). If a strong shock destabilizes the field, lower Lattice dt or raise Physics Steps. The engine exposes four parameters (active only in Compressible mode):

ParameterRangePhysical Meaning
Gamma (\(\gamma\))1.1 – 2.0Ratio of specific heats (1.4 for diatomic air).
Inlet Mach0 – 5.0Free-stream Mach number \(M = u/a\). Values > 1 are supersonic.
Inlet Density0.1 – 5.0Free-stream density \(\rho_\infty\).
Inlet Pressure0.1 – 5.0Free-stream static pressure \(p_\infty\).

Recommended view modes: Schlieren (density gradient \(|\nabla \rho|\), the classic shock-visualization technique), Velocity (rendered as Mach number), or Pressure. Lagrangian particles and streamlines are advected directly by the reconstructed velocity \(\mathbf{u} = (\rho u,\ \rho v)/\rho\), so they stream through and bend around shocks just like the field itself.

5. Turbulence & Fidelity LES

Large Eddy Simulation (LES)

The Smagorinsky model calculates sub-grid scale (SGS) stresses. It derives an "eddy viscosity" \(\nu_t\) from the resolved strain rate tensor \(\bar{S}_{ij}\), dissipating energy at the smallest resolvable scales.

$$\nu_{t} = (C_s \Delta)^2 \sqrt{2 \bar{S}_{ij} \bar{S}_{ij}}$$

Vorticity Confinement

To counteract the inherent diffusion of the semi-Lagrangian advection, Vorticity Confinement applies a body force that pushes fluid towards local vorticity maxima, keeping vortices sharp and persistent.

$\mathbf{f}_{conf} = \epsilon \Delta x (\mathbf{N} \times \omega), \quad \mathbf{N} = \frac{\nabla |\omega|}{|\nabla |\omega||}$

Off by default in 3D. Confinement is an artificial force, not physics: it re-energises swirl the grid has smeared. Measured on the drone demo it doubled the kinetic energy of the flow. Use it for looks; leave it off when the numbers matter. When it is on it runs before the pressure projection, so it can no longer break mass conservation.

6. Multi-Phase & Control Volumes

OTOM CFD supports multi-phase simulations (e.g., air and water interaction) by advecting a scalar phase field \(\phi \in [0,1]\).

7. 3D Simulation & Volumetric Rendering WebGPU 3D

The 3D solver extends the 2D logic into a texture_3d environment. Rendering is achieved via a single-pass Raymarching fragment shader.

Physically Based Raymarching

The camera casts rays through the domain bounding box. The shader performs two primary operations per ray:

  1. Sphere Tracing: Finds intersections with 3D solid obstacles using the get_sdf3d function. If hit, it calculates surface normals and applies PBR lighting (Metallic/Roughness/Ambient).
  2. Volume Accumulation: If the ray misses the solid (or before it hits), it steps through the 3D texture, sampling the chosen field (e.g., Vorticity). Color and alpha are accumulated via alpha blending (\(C = C_{src}\alpha + C_{dst}(1-\alpha)\)).

Advanced 3D Features

  • Isosurface Mode: Instead of fuzzy clouds, the volume renderer searches for a specific threshold value, calculates the 3D gradient normal of the fluid field, and renders it as a solid, shaded surface complete with volumetric soft shadows. The solid obstacle body is always composited as an opaque backing layer behind the isosurface, so the geometry remains visible even when high field values (e.g. stagnation zone) exist upstream of the surface.
  • Cutting Planes: Users can define a plane (\(\mathbf{n} \cdot \mathbf{x} - d = 0\)) to clip the volume or strictly render a 2D cross-section slice inside the 3D domain.

Obstacle Spin & Local Axis Gizmo 3D

Every 3D obstacle — the primary analytical shape (Sphere/Box/Cylinder/Wing/Extrude 2D), the STL mesh, and each of up to 4 secondary "Extra Obstacles" — can spin continuously about all 3 axes, the 3D analogue of the 2D Torque slider. Spin X/Y/Z (deg/frame) is a rotation rate: each rendered frame, the obstacle's current absolute orientation angle (Rot X/Y/Z, directly editable — set it to pose a fixed starting tilt with spin left at zero, e.g. pitching a blade before it starts turning) advances by the rate on each axis independently (wrapped to \([-180°,180°]\)), then that angle is fed into the SDF evaluation. Setting a rate to 0 simply holds the current orientation. Spin About Local Axes (per obstacle) applies the three rates in the obstacle's own rotated frame instead of world axes, so a tilted shape keeps spinning about its own axis instead of the components mixing across world axes as it tilts.

Mechanically, the rotation is applied as the exact inverse of the STL transform's own rotation (stl_inv_rotate, \(R^{-1} = R_x(-r_x)R_y(-r_y)R_z(-r_z)\)) to the query point before the primitive's local-space SDF runs, and the resulting distance is otherwise unaffected — so spin composes correctly with the Wing's own Angle-of-Attack slider (AoA still tilts the chord inside the wing's own frame, after the outer spin has already oriented the whole obstacle in world space). Because get_sdf3d/get_sdf_extra are the single shared functions consumed by rendering, particle/streamline collision, and the aerodynamic force integral, a spinning obstacle is mechanically consistent everywhere at once — no separate plumbing was needed for each consumer.

Show Local Axes (a checkbox per obstacle — one for the primary obstacle, one for STL if selected, one for each Extra Obstacle) draws that obstacle's local X (red) / Y (green) / Z (blue) axis as a short line from its centre, rotated by the same spin, so the user can always see which way "local X" currently points as the shape spins.

Compressible engine caveat: spin for the primary obstacle and Extra Obstacles is only applied when Engine Solver is Incompressible. The Compressible (HLLC) solver's own wall boundary condition uses a separate, unrotated copy of the SDF for its compute kernels; rotating the shared render-path SDF without also rotating that copy would make the rendered obstacle visually disagree with what the solver treats as solid, so rotation is forced back to identity while Compressible is active. STL spin is unaffected by this caveat and works in both engines, since the STL transform was already fully mirrored into the compressible solver's SDF copy before this feature existed.

Spin drives real flow — it is not a cosmetic rotation

A spinning obstacle's solid cells carry the rigid-body velocity \(\boldsymbol{\omega} \times \mathbf{r}\) (about the obstacle's own centre) instead of the historical zero — a genuine moving no-slip wall, exactly like the physical surface of a real spinning object. This is what lets a rotating shape actually drag the surrounding fluid, shed vorticity, and pump flow, rather than presenting fluid with a stationary wall that merely changes orientation between frames. The same mechanism drives the swirl/downwash behind a spinning box, sphere, or cylinder, and the suction/pressure-side split across a genuinely pitched blade (see below).

Blade pitch matters for thrust. A symmetric shape spinning about an axis through its own centre — a Box, a Sphere, an unpitched flat plate — mostly produces swirl and a weak centrifugal low-pressure core along the spin axis, not a net thrust-generating pressure differential between its faces. Real thrust needs the blade angled relative to its own rotation plane, exactly like a real propeller's twist: the Wing shape's AoA provides this directly, or an STL mesh with real blade pitch built into its geometry. Measure it with Torque (Aerodynamics tab, below) alongside the existing Drag/Lift: Drag/Lift give the thrust a spinning, pitched blade produces; Torque gives what it cost to produce it (shaft power is Torque × angular speed) — together they're what let a user read a spinning blade's aerodynamic efficiency off the simulation.

Primitive Sizing 3D

Beyond the uniform Scale slider, the primary Box and Cylinder shapes have independent per-axis controls, shown only when that shape is selected: Box Size X/Y/Z (ratios, default 1.0 each, multiplying the historical fixed 0.25 half-extent per axis) let a Box be stretched into a slab or a beam rather than staying a cube; Cylinder Radius / Cylinder Height (ratios, default 1.0, multiplying the historical fixed \(r=0.2\), \(h=0.4\)) do the same for a Cylinder. Both compose with Scale, which still resizes the whole result uniformly afterward.

Extrude 2D takes its footprint from whatever is currently painted on the flat 2D canvas (Draw Mode) — it is not a parametric shape, and the 2D canvas is the actual UI for its outline. Its Extrusion Length slider controls the extruded half-length along Z (ratio, default 1.0 = the historical fixed 0.85 half-span), independent of Scale (which would otherwise also zoom the 2D footprint along with the length).

Extra Obstacles (3D) & STL Instancing

The "Extra Obstacles (3D)" folder lets a scene hold up to 4 secondary obstacles in addition to the primary one, each with an independent Shape, position, scale, spin, and local-axis toggle (added/removed via + Add Obstacle / Remove). The Shape choices are Sphere, Box, Cylinder, Wing, and STL.

STL as an Extra Obstacle is an instance, not a second upload. There is only one voxelized STL SDF texture in the whole scene — the mesh loaded via the primary "Obstacle" folder's Upload STL button. Choosing STL for an Extra Obstacle samples that same voxel data at this slot's own independent position, scale, and rotation, exactly the same technique the primary obstacle itself uses to place the mesh. This is useful for repeating one part at several locations (e.g. an array of identical pillars, or a duplicate of a car body at a different offset) without the cost of a second voxelization pass or a second GPU texture. If no STL file has been uploaded yet, an STL-typed Extra Obstacle simply renders as nothing until one is.

The previous fifth shape option, a built-in simplified car body (the standard automotive benchmark shape with a slanted rear window), has been replaced by STL instancing: any imported car or vehicle mesh can now be used, and repeated, instead.

Non-Cubic Control Volume (Domain Resize) 3D

The 3D domain is no longer constrained to a unit cube. Three independent half-extent sliders — Length (X), Height (Y), and Depth (Z) — let users reshape the control volume to a rectangular cuboid. Default value 1.0 corresponds to the original ±1 extent in each axis. All internal coordinate transformations (world-to-UV, raymarching box, particle seeding, streamline reset bounds) scale with these parameters via three uniform floats (domainX, domainY, domainZ) pushed to the GPU every frame.

SliderRangeEffect
Length (X)0.5 – 4.0Stretches / compresses the streamwise (flow) direction.
Height (Y)0.5 – 4.0Stretches / compresses the vertical extent.
Depth (Z)0.5 – 4.0Stretches / compresses the spanwise direction.

Changing any dimension triggers a full simulation re-initialisation (initSim()) so that the velocity field and obstacle mask are consistent with the new geometry.

Velocity Perturbation Contour 3D Viz

In View Mode = Velocity, the 3D volume renderer displays the signed perturbation from free-stream rather than raw speed. The formula is:

$$ v_{display} = \left(\frac{|\mathbf{u}|}{U_\infty} - 1\right) \times G $$

where \(G\) is the user's Gain (VScale) slider × 10. The undisturbed free-stream maps to zero (transparent), regions faster than free-stream (e.g. flow accelerating around the sides) appear red, and the stagnation / wake (slower) regions appear blue. A bilateral threshold on \(|v_{display}|\) means both acceleration and deceleration contribute to opacity. To suppress numerical checkerboard noise (a typical artifact of collocated-grid pressure solvers) the velocity is sampled at the corner between 8 neighbouring cells so the GPU's trilinear filter averages them to zero for any ±δ alternating pattern — at no extra texture read cost.

3D Solver Accuracy: Step Order, Walls and Validation 3D

Each 3D substep runs in the standard fractional-step order: transport and body forces (advection, fans, buoyancy, rotating frame, inlet) → optional vorticity confinement → viscosity / LES → pressure projection. The projection comes last because it is the only stage that makes the velocity divergence-free, and its result is what the renderer, the particles, the force integrals and the next substep all read. (Earlier versions projected before confinement and viscosity, so every consumer saw a field those two stages had pushed off mass conservation again.)

Domain walls are impermeable. On this collocated grid the velocity on a face is the mean of the two cells either side, so the ghost value beyond a wall mirrors the normal velocity (\(u_{ghost} = -u_n\)) and the wall face carries no flow; tangential velocity keeps a zero-gradient ghost (free-slip walls). The +X face is a wall when Open Outlet is off, and a zero-gradient outflow with \(p = 0\) when it is on. Without the mirrored ghost a jet aimed at the floor drained through it, and a sealed box lost about 20% of its circulation through the floor every step.

Measured, before → after these fixes:

TestBeforeAfter
Sealed box, fan blowing down: net flux through interior planes (must be 0)−0.22 (20% of the mean speed)≈ 0.003
Wind tunnel with sphere: mass flux at the outlet (inlet = 30)28.3 (−6%)30.0
Sphere drag, Re 100 (Schiller–Naumann 1.09)0.84, of which pressure 0.031.11 (+2%)
Sphere drag, Re 300 (Schiller–Naumann 0.68)—0.78 (+14%)
Actuator disk: far-wake speed vs momentum theory \(2v_i = 2\sqrt{T/2\rho A}\)—10.24 vs 10.75 (−5%)

The sphere cases used a 16-cell diameter on a 128³ grid (1% blockage). Accuracy drops on coarser grids and at high Reynolds numbers, where the boundary layer is far thinner than a cell and no wall model is present.

Rotors: Actuator Disk vs. Resolved Blades (Solid in Simulation) 3D

A spinning blade is a moving wall in the solver. That works while the blade tip moves less than about one cell per step. Faster, the blade jumps over cells each step and flings its wall speed into the air: on the drone demo the props reached 7 cells per step, produced almost no thrust, and caused most of the jitter around the rotors. The standard model at this resolution is the actuator disk: a Ventilation Fan Disk on each rotor adds the momentum, and the blades are only drawn.

Every obstacle (the primary and each Extra Obstacle) has a Solid in Simulation checkbox. Unticked, the body is visual only: still drawn and spinning, but not part of the flow, the force integrals or the particles' collisions. Measured on the drone demo (thrust from the four disks): the downwash under the rotors became 3the downwash under the rotors became 3–10× steadier (frame-to-frame spread ±0.08–0.14 instead of ±0.4–1.0)ndash;10the downwash under each rotor became about 10× steadier (frame-to-frame spread ±0.1 instead of ±1.0)times; steadier (frame-to-frame spread the downwash under each rotor became about 10× steadier (frame-to-frame spread ±0.1 instead of ±1.0)plusmn;0.08the downwash under each rotor became about 10× steadier (frame-to-frame spread ±0.1 instead of ±1.0)ndash;0.14 instead of the downwash under each rotor became about 10× steadier (frame-to-frame spread ±0.1 instead of ±1.0)plusmn;0.4the downwash under each rotor became about 10× steadier (frame-to-frame spread ±0.1 instead of ±1.0)ndash;1.0), the weakest rotor's jet went from slightly upward (wrong) to clearly downward, and the strongest jets got 35and the strongest jets got 35–50% strongerndash;50% stronger — at the same frame rate.

Keep bodies solid when the flow around them is the point (a wing, a car, a slowly turning stirrer): there the solid wall is the physics. Make them visual only when a fan disk already supplies their effect.

8. Lagrangian Particles & Trails

Both 2D and 3D modes support massless Lagrangian particles advected by the velocity field.

Sub-Step Tunneling Prevention (3D) 3D

At high flow speeds a particle's displacement in one frame can exceed the obstacle diameter, causing it to pass through solid geometry without detection (tunneling). The 3D advection kernel solves this by splitting each full step into 4 sub-steps of \(\tfrac{1}{4} \Delta t_{vis}\) each. The SDF is tested after every sub-step; the particle is guaranteed to be caught within one cell-diameter of travel.

Obstacle Surface Interaction (3D) 3D

When a particle's sub-step would carry it inside an obstacle (SDF < 0.01), two strategies are applied in sequence rather than teleporting the particle back to the inlet:

  1. Tangential sliding: The sub-step velocity is projected onto the surface tangent plane using the SDF normal \(\hat{\mathbf{n}} = \nabla(\text{SDF})\) at the last-valid position:
    $$ \mathbf{v}_{tang} = \mathbf{v} - (\mathbf{v} \cdot \hat{\mathbf{n}})\,\hat{\mathbf{n}} $$
    The particle slides along the surface with this tangential component. If the slid position is still inside the SDF the particle is frozen at the last valid position for that sub-step.
  2. Persistence: Particles that become stuck on an obstacle surface remain visible at that position rather than disappearing. They resume motion once the flow carries them off the surface.

Field-Colored Particles 2D3D

Toggle Color by Field (in the Streamlines/Particles or 3D Particles folder) to color each particle and trail segment by the local flow field value at its current position, using the active colorbar theme.

  • Vorticity view: particles show local rotation intensity — strong vortices appear at the colormap extremes, irrotational flow is near-neutral.
  • Velocity view: particles range from blue (below free-stream speed) through neutral to red (above free-stream).
  • Pressure view: high-pressure stagnation regions vs. low-pressure suction regions are directly visible on each particle.

When disabled, particles render as white (dark theme) or black (light theme). The field value is sampled in the GPU vertex shader at zero extra texture-fetch cost, so performance impact is negligible.

Temperature (Buoyancy) Model 3D

An optional Temperature (Buoyancy) folder (above Performance in the 3D sidebar) adds Boussinesq natural-convection physics to the 3D incompressible solver — designed for greenhouse ventilation, stack-effect studies, and any scenario where a temperature difference between zones drives airflow.

Physics: the Boussinesq approximation

The solver remains fully incompressible. Density is assumed uniform everywhere except inside one vertical body-force term. The acceleration added each timestep is:

$$\mathbf{f}_{buoy} = g\beta\,T(\mathbf{x})\,\hat{y}$$

where \(T\) is the local temperature deviation from ambient (ambient = 0), \(\beta \approx 1/T_{abs} \approx 0.0034\,\mathrm{K}^{-1}\) for air, and \(g\beta\) is the single Buoyancy slider. This force is applied before the pressure-projection step, so the pressure Poisson solve produces the inside/outside pressure difference that drives flow through windows and vents — the physics is not prescribed but emerges from the equations, making the model self-consistent with any wind-driven inlet flow.

Temperature field and zone sources

A 3D scalar field \(T(\mathbf{x},t)\) is advected and diffused by the velocity field each substep (semi-Lagrangian advection + explicit Laplacian diffusion). Inlet planes always supply ambient air (\(T=0\)). A user-defined box zone injects heat in one of two modes:

  • Fixed Temperature (Dirichlet): the zone is held at the set overheat each step — analogous to a surface at constant temperature (warm soil, heated floor). Use Zone Temp (dT) to set the value.
  • Heat Flux (volumetric source): the zone temperature increases at a constant rate each step — analogous to solar gain or an electric heater continuously adding energy. Use Flux Rate. This mode has no direction: it is a W/m³-style volumetric source, not a directional beam. Direction appears only in the subsequent advection (carried by the wind), diffusion (spread isotropically), and buoyancy (warm air rises in +Y).

Negative values make a cooling zone (chilled wall, evaporative cooling) — the resulting cold air sinks.

Zone Temp units

Zone Temp is a normalized overheat above ambient, not a Celsius value. The user defines the mapping, for example "Zone Temp 1.0 = 20 °C overheat". The buoyancy force at each cell is Buoyancy × T_local (pointwise), so the slider acts on whatever temperature gradient the field has developed.

Buoyancy slider and the Richardson number

The Buoyancy slider encodes \(g\beta\) in simulation units. Its relationship to the physical Richardson number \(Ri = g\beta\Delta T\,H / U^2\) (the ratio of stack-effect strength to wind strength) is approximately:

$$\text{Buoyancy} \approx \frac{7 \cdot Ri}{T_{zone}}$$

Practical guidance for a greenhouse (height \(\approx 3\,\text{m}\), 10 °C overheat, Zone Temp = 0.6):

  • Breezy day (\(\geq\)3 m/s, \(Ri \approx 0.1\)) → Buoyancy 1–2 (wind dominates)
  • Light wind (\(\approx\)1 m/s, \(Ri \approx 1\)) → Buoyancy 8–15 (default 10; wind and stack comparable)
  • Near calm (\(\approx\)0.3–0.5 m/s, \(Ri \approx 4\text{–}10\)) → Buoyancy 40–50 (stack dominates)

Persistent field and the "Clear Heat" button

The temperature field is a GPU volume that persists across frames. Setting Zone Temp or Flux Rate to 0 stops injecting new heat but does not remove heat already in the domain — residual warmth floating in recirculation zones continues to drive buoyancy. This is physically correct behaviour (real greenhouse air does not instantly return to ambient when a heater is turned off). Press Clear Heat (back to ambient) to instantly zero the entire field. The field also resets automatically on sim re-init.

Unsteady behaviour is normal

Natural convection with residual heat is inherently unsteady — thermal plumes flicker and meander (like smoke above a candle). Only the time-averaged field is steady. Use the Steady Average toggle in the Visualization folder to observe the converged mean plume, exactly as a CFD engineer would report time-averaged natural-convection results. Setting Buoyancy = 0, or pressing Clear Heat while sources are off, produces a truly neutral (isothermal, no-buoyancy) flow.

Temperature view mode

Select Temperature in the Field dropdown (Visualization folder) to render the thermal plume directly. Ambient air (T = 0) is transparent; warmer regions render red, cooler regions render blue. The ghost box (Show Zone) draws the source zone as a translucent orange (heating) or blue (cooling) box — occluded by the STL obstacle — to help with positioning.

Ventilation Fan Model 3D

An optional Ventilation Fan folder (directly below Temperature (Buoyancy) in the 3D sidebar) lets the user place up to 4 independent fan zones — for cross-ventilation studies, forced-convection cooling, or modelling a spinning rotor's downwash without resolving actual blade geometry. Each fan has a Shape: Box (a straight-through vent) or Disk (an actuator-disk rotor/propeller).

Physics: two different actuator models

Box is a hard velocity clamp — the standard simplified-CFD treatment for a duct/desk/extractor fan: inside the zone, velocity is hard-set every step to a fixed directional jet,

$$\mathbf{u}(\mathbf{x}) = P \, \hat{d} \quad \text{for } \mathbf{x} \in \text{zone}$$

where \(P\) is the fan's Power (Jet Speed) and \(\hat{d}\) is its unit direction vector — a Dirichlet velocity boundary condition, the same technique the domain inlet itself uses. This behaves like a section of duct: the flow inside simply is that speed, which suits a straight-through vent but cannot, by itself, distinguish an upstream (suction) side from a downstream (discharge) side. Incompressible engine only.

Disk instead adds momentum to whatever air passes through it, every step:

$$\mathbf{u}(\mathbf{x}) \mathrel{+}= P \, \hat{d} \, \Delta t \quad \text{for } \mathbf{x} \in \text{disk}$$

This single change — an increment rather than a replacement — is what makes a Disk fan behave like an actual rotor: because the zone no longer forces a fixed value, the pressure/continuity solve is free to draw replacement air in from behind the disk (suction) and build pressure up just ahead of it (discharge), a genuine upstream/downstream pressure difference — which is aerodynamic thrust — instead of a section of duct. Disk works in both engine modes: the compressible solver applies the equivalent source term directly to its conservative momentum and energy variables at the end of each Runge-Kutta stage (adding the matching kinetic-energy delta alongside momentum, so the derived pressure doesn't silently jump). Box has no clean equivalent as a source term in a conservative finite-volume scheme, so it remains incompressible-only.

Where two Box zones overlap, the higher-numbered fan's velocity wins (a simple sequential override). Where two Disk zones overlap, their momentum contributions sum — the physically correct combination for multiple momentum sources acting on the same air.

The zone itself is oriented — not just the velocity inside it

Both shapes' zones are built in a local frame derived directly from the direction vector \(\hat{d}\) (one axis along \(\hat{d}\), the other two spanning the plane perpendicular to it) — so tilting Direction Yaw/Pitch rotates the zone's own shape bodily, not just the velocity direction inside a fixed footprint. For a Box this means its own "outlet face" always faces wherever it's aimed, exactly like an oriented duct segment; for a Disk it means the disk's flat face is always perpendicular to its thrust axis, exactly like a real rotor plane. At the default direction (\(0°,0°\), blowing toward \(+X\)) this local frame reduces exactly to world \(X\)/\(Y\)/\(Z\), so an un-rotated fan looks identical to the original world-axis-aligned box.

Zone Size X/Y/Z follow that same local frame rather than world axes: Size X is always the half-extent along the thrust direction (thickness), and Size Y/Size Z are the two extents across it. For Disk, Size Y is the radius and Size Z is unused. Zone Center X/Y/Z and all sizes are normalized (\(-1\) to \(+1\) for center, \(0.02\) to \(1\) for size) and scaled by the domain half-extents, the same convention as the Temperature zone.

Multiple fans

Fans are added and removed independently via + Add Fan / Remove buttons, each opening its own sub-folder ("Fan 1", "Fan 2", ...) with its own shape, position, size, direction, and power — the same add/remove pattern as the Extra Obstacles (3D) folder. The Enable Fans toggle is a single master switch for all fans at once: turning it off zeroes every fan's effect on the next step without deleting any of their configurations, so settings are preserved if re-enabled.

Visualizing direction: the ghost zone and arrow

Show Zone + Arrow draws, for every active fan, a translucent teal ghost (a different colour from the Temperature zone's orange/blue, so the two are never confused) marking its true oriented extent — an oriented box for Box fans, an actual tilted cylinder (not a bounding box) for Disk fans, both computed with a proper ray intersection in the zone's own local frame so the ghost always matches what the solver is really doing — plus a shaft-and-arrowhead glyph spanning the zone and pointing exactly along the direction it blows. The glyph reuses the same rendering technique as the Aerodynamics tab's Force Vectors arrows.

No persistent field

Unlike the Temperature model, a fan has no memory: it is re-applied fresh every step, with nothing to advect or diffuse. There is therefore no "Clear Fan" button — toggling Enable Fans off removes the effect immediately on the next frame.

Modelling a drone/propeller blade's downwash

A Disk fan is the recommended way to model a spinning rotor's induced flow: real rotor RPM is far higher than this explicit-timestep solver could resolve for actual rotating blade geometry without an impractically small CFL-limited timestep and a finer grid than the 3D solver's 256³ cap — an actuator disk sidesteps this entirely by imposing the induced velocity directly, the same simplification real rotor-aerodynamics tools use. The alternative — a literally spinning, blade-pitched STL mesh (see Obstacle Spin below) — gives real blade-tip vortices and a measurable Torque, but is only realistic at low RPM.

9. Integrated AI Agent Architecture LLM

OTOM CFD includes a built-in CFDChatbot that allows users to alter the simulation using natural language. The architecture works as follows:

Function Calling & Parameter Mapping

The agent communicates with the Gemini API using strict JSON schemas (Function Calling). When a user asks "Make the fluid thicker" or "Add a large spinning wing," the LLM maps intent to two primary functions:

  • update_settings: Maps semantic concepts to internal variables (e.g., "thicker" \(\rightarrow\) viscosity: 0.05, "slower" \(\rightarrow\) inletVel: 5.0).
  • set_obstacles / modify_existing_obstacles: Translates geometry requests into SDF parameters (e.g., obsType: 'Engineering', engShape: 'NACA 0012', angle: 15, scale: 2.0).

The Chatbot class intercepts these tool calls and updates the Lil-GUI controllers, which in turn flush the updated data to the WebGPU uniform and storage buffers.

Array-valued 3D features without a dedicated action — the Ventilation Fan's fans3D list and the Extra Obstacles (3D) list — are set through the same update_settings call, passing the whole array as one of the "changes". Because update_settings replaces whichever keys it touches rather than merging into them, asking the agent to add one more fan requires it to resend every existing fan alongside the new one; it has no live read-back of the current scene and relies on the conversation history to know what is already there.

House tab action. A fourth action, house, drives the House Ventilation tab (§15): it opens the tab, loads the example or a template, switches between imported plans, sets the wind speed, direction and \(C_p\) values, sets each window or door to Inlet / Outlet / Closed / Auto (interior doors Open / Closed), changes room heights and resolution, undoes or resets plan edits, and starts or stops Rank Each Opening — e.g. { "action": "house", "changes": { "windFrom": 270, "openings": { "W1": "Inlet", "D2": "Closed" } } }. While the tab is on screen, the scene snapshot the agent receives lists every opening with its role and live flow, plus the measured ACH and the last ranking, so it reports measured numbers instead of guessing. Walls are edited by the user on the map; the agent cannot move or draw them. Claude Code reaches the same action through the cfd_house MCP tool.

10. Aerodynamics & Analytics

The software calculates Drag (\(C_d\)) and Lift (\(C_l\)) coefficients by integrating pressure and viscous shear forces over the surface of the obstacles.

The Reynolds number is calculated dynamically based on the largest obstacle's chord length \(L\):

$$Re = \frac{U_\infty L}{\nu}$$

Analytics can be exported as structured text or baked into PDF reports with canvas screenshots using the GUI tools.

How the 3D force integral is normalised 3D

The 3D surface integral sums \(-p\,\mathbf{n}\,dA\) and the wall shear over the cells within one cell of the body surface. Three details make it quantitative: the projection stores \(p\,\Delta t\), so the pressure is divided by the time step; each band cell is weighted by a smoothed delta function \(\tfrac12\big(1+\cos(\pi\,d/\Delta x)\big)\) of its signed distance \(d\), so the band adds up to exactly one surface rather than two; and the wall shear uses each cell's real distance from the wall. \(C_d\) and \(C_l\) are divided by \(\tfrac12 \rho U^2 A_{ref}\), with \(A_{ref}\) the body's measured frontal area (shown as Ref. Area (frontal)), and the Reynolds number uses the matching equivalent diameter \(\sqrt{4A_{ref}/\pi}\). Earlier versions used a fixed reference area and a grid-based length, integrated the raw \(p\,\Delta t\), and counted the surface twice: the sphere's pressure drag came out about 18× too small.

Torque — measuring a spinning blade's efficiency 3D

When the 3D obstacle is spinning (see Obstacle Spin above), the same surface integral that produces Drag/Lift also accumulates Torque about whichever spin axis is currently active — the shaft torque a motor would need to supply to hold that rotation rate:

$$\tau = \oint \big(\mathbf{r} \times (-p\,\mathbf{n})\big) \cdot \hat{\boldsymbol{\omega}} \; dA$$

where \(\mathbf{r}\) is measured from the obstacle's own centre and \(\hat{\boldsymbol{\omega}}\) is its current spin axis — exactly the same axis/centre solid_vel3d uses for the wall-velocity boundary condition, so the reading is about the same rotation actually driving the flow. Torque reads exactly 0 whenever the obstacle isn't spinning (there is no single shaft axis to report a torque about). Combined with Drag/Lift, this is what lets Cd/Cl (thrust) and Torque (cost) together describe a spinning blade's aerodynamic efficiency — shaft power is Torque × angular speed, though the app displays the raw torque rather than computing that product.

Surface Friction — the pressure/friction drag split 3D

The 3D surface integral now resolves the viscous component of the wall force alongside the pressure component. Wall shear stress is evaluated as

$$\boldsymbol{\tau} = \mu \frac{\partial \mathbf{u}_t}{\partial n} \approx \mu \,\big(\mathbf{u}_{\text{fluid}} - \mathbf{u}_{\text{wall}}\big)_t$$

sampling the fluid velocity one cell along the outward normal and differencing it against the wall's own velocity — the separation is exactly one cell and velocity is stored in cells/step, so the finite-difference denominator is unity. Using \(\mathbf{u}_{\text{wall}}\) rather than zero is what keeps this correct for a spinning obstacle: the shear a rotating blade feels is set by the fluid's velocity relative to the moving surface, which for a well-driven rotor is far smaller than the absolute velocity and would otherwise be grossly overestimated.

The Surface Friction sub-folder reports Cd — Pressure, Cd — Friction (which sum to the headline total \(C_d\)) and the area-weighted Mean Wall Shear. The ratio is a useful shape diagnostic: pressure drag dominates for bluff bodies, friction drag for slender bodies aligned with the flow. Friction also contributes to the reported Torque, which matters for rotors — omitting it would systematically under-report required shaft power.

Propulsion in real units — Thrust, Power, RPM 3D · SI

The Propulsion (SI) sub-folder converts the lattice-unit force and torque integrals into Newtons and Watts. The conversion is derived from the same relation the solver already uses for velocity in SI Units Mode (\(u_{\text{lat}} = u_{SI}\,\Delta t / \Delta x\)), so one lattice velocity unit is \(\Delta x/\Delta t\) m/s. Because the projection step computes \(\mathbf{u} \mathrel{-}= \nabla p\) with velocity in cells/step, the solver's pressure is a kinematic pressure \(p/\rho\) carrying units of velocity squared — so converting it to Pascals requires \(\rho\,(\Delta x/\Delta t)^2\) with no extra length factor:

$$F_{SI} = \rho \left(\frac{\Delta x}{\Delta t}\right)^{2} m_{\text{world}}^{2}\; F_{\text{lat}}, \qquad \tau_{SI} = F_{SI}\, m_{\text{world}}$$

where \(m_{\text{world}}\) is metres per world unit. RPM follows from the spin sliders (degrees per frame, converted through Physics Steps and Real-world Time Step), thrust is the force projected onto the spin axis when rotating (streamwise \(+X\) otherwise), and shaft power is \(P = |\tau|\,\omega\). Thrust / Power (N/W) is reported rather than a "figure of merit" because it needs no disk-area assumption and is the metric rotor designers actually compare.

These fields require SI Units Mode (and Live Analysis). Lattice numbers carry no physical scale whatsoever, so rather than showing a fabricated Newton value the fields state "needs SI Units Mode". The physical scale is set entirely by Domain Width (m) and Real-world Time Step, so those must describe your actual case for the output to mean anything.

Wind Noise — aeroacoustic proxy 3D

The Wind Noise folder (directly below Ventilation Fan) estimates flow-generated noise from the rate of change of surface pressure on the obstacle. This is the source term of Curle's acoustic analogy: at low Mach number the dominant noise mechanism is not the turbulence itself (a weak quadrupole, scaling as \(M^8\)) but the fluctuating pressure on solid surfaces, radiating as a dipole that scales as \(M^6\) — which is why a body in a flow is far louder than the same flow without it.

$$p'(r) \;\sim\; \frac{1}{4\pi c\, r}\sqrt{\;\int_S \left(\frac{\partial p}{\partial t}\right)^{2} A_c \; dS }$$

The integrand is accumulated once per solver substep — not on the ~2 Hz analysis timer, which is far too slow to resolve the unsteadiness that actually radiates. Noise Index (dB rel.) is a relative scale with an arbitrary zero point, always available and the right number for A/B comparisons. Est. SPL is an absolute estimate re \(20\,\mu\text{Pa}\) at the chosen Observer Dist, and requires SI Units Mode; it takes the surface correlation area \(A_c\) as one cell face, the standard resolved-scale assumption.

What this is not. There is no acoustic wave equation, no propagation delay, no directivity, no reflection or diffraction, and turbulence finer than the grid radiates nothing at all. The number is a source-strength indicator for ranking designs — genuinely useful for answering "is this shape quieter than that one?" — and must not be presented as a certifiable or measured sound-pressure level. Enabling the panel also costs one extra full-grid pass per substep plus a \(\text{res}_{3D}^3\) history texture pair, so it is off by default.

Ground Truth — Reference Comparison Truth tab

The Ground Truth tab (fact_check icon in the left sidebar) cross-checks the live simulation against textbook and empirical correlations for the active canonical shape. It auto-detects the current geometry — 2D Circle/Square/Rectangle/Triangle/Hexagon/Pentagon and Engineering shapes (NACA airfoils, Flat Plate, Ellipse, Tear Drop, Wedge), or 3D Sphere/Cube/Cylinder/Wing — and presents a Measured | Reference | Δ% table (colour-coded by error), a reference plot with the current operating point marked, and an engineering assessment.

Reference correlations include the circular cylinder fit \(C_d \approx 1 + 10\,Re^{-2/3}\) (White) with Strouhal \(St \approx 0.20\); the sphere Schiller–Naumann/Morrison drag law; the laminar flat plate \(C_d = 1.328/\sqrt{Re}\) (Blasius, parallel) or \(C_d \approx 1.98\) (normal); a generic sharp-edged bluff-body fit for prisms (e.g. square \(\approx 2.05\) face-on, \(\approx 1.55\) corner-on; triangle \(\approx 1.55\) apex-upstream, \(\approx 2.0\) base-upstream); and thin-airfoil theory \(C_l = 2\pi\sin\alpha\) closed by a parabolic drag polar \(C_d = C_{d0} + k\,C_l^2\). The reference plot is \(C_d\) vs \(Re\) (log axis) for bluff bodies, or the \(C_l\) vs \(\alpha\) lift line for airfoils.

Honesty of the measured column: in 3D the “Measured” values are a genuine GPU surface-force integral. In 2D they are presently an illustrative placeholder (marked with an asterisk) pending a dedicated 2D force kernel; only the Reference column is authoritative in 2D. Hand-drawn or imported (STL) geometry has no closed-form solution, and the panel states this rather than fabricating a number.

Engineering Judgement

Below the table, a scene-aware assessment translates the live observables into plain-language guidance with colour-coded markers (✓ good, ! caution, • info): the solver-vs-theory agreement, the Reynolds flow regime and expected wake behaviour (creeping → steady recirculation → laminar von Kármán street → subcritical → drag crisis), vortex-shedding side-force and vortex-induced-vibration warnings, airfoil stall (incidence beyond \(\sim\!12\text{--}15^\circ\)) and lift-to-drag ratio, and the compressibility regime (incompressible/subsonic/transonic/supersonic, including the Mach angle \(\mu = \arcsin(1/M)\)).

Local AI Scene Explanation & Chat WebLLM · in-browser

The Ground Truth tab includes an optional “Explain this scene” assistant that runs a small large-language model entirely in the browser on the user’s GPU via WebLLM (MLC). It produces a plain-language explanation of the current scene and then supports a multi-turn conversation for follow-up questions. The model is grounded on the structured scene data — the \(C_d\), \(C_l\), \(Re\), Mach, regime and judgement values come from the solver and reference correlations, not the model — so it narrates and reasons but does not invent coefficients.

A dropdown selects the local model (Llama 3.2 1B, Gemma 2 2B default, Qwen2.5 3B, Llama 3.2 3B, or DeepSeek-R1 Distill 7B). The chosen model is downloaded from the MLC CDN on first use and cached in-browser thereafter; everything runs locally, so no scene data leaves the device. On hardware without WebGPU (or if a model cannot be allocated), it transparently falls back to the cloud assistant. The simulation auto-pauses during generation so the language model and the renderer do not contend for the GPU.

11. AI Surrogate Models Experimental · ML

The AI tab (brain icon in the left sidebar, directly below Demo) provides a machine-learning workflow for training and running neural-network surrogate models that approximate the physics solver. A surrogate learns the solver's behaviour from data and, once trained, can advance the flow with a single network evaluation instead of the full iterative solve. This is offered as an experimental research feature: surrogates trade physical fidelity for speed, and are most interesting in 3D where the iterative pressure solve is the dominant cost.

Concept: Learning the Per-Step Residual

Rather than predicting the next flow field directly, the networks are trained to predict the residual — the change in the field over one solver step — which the simulation then integrates:

$$ \mathbf{q}_{t+\Delta t} = \mathbf{q}_{t} + \mathcal{N}_\theta(\mathbf{q}_{t}, \, \text{mask}, \, Re) $$

where \( \mathbf{q} \) is the stacked velocity/pressure state, \( \text{mask} \) is the obstacle occupancy, \( Re \) is the (normalised) Reynolds number, and \( \mathcal{N}_\theta \) is the trained network. Predicting the residual keeps the targets small and well-conditioned and improves step-to-step stability. All inputs and outputs are normalised internally by fixed reference scales so the network operates on \(\mathcal{O}(1)\) values.

Resolution lock: a network's weights encode the physical scale of the grid it trained on, so each model is bound to a single grid resolution. Applying a model automatically sets the simulation to that grid. To run at a different resolution you train a separate model — resolution is part of a model's identity, not a runtime knob.

2D Training — Train New Model

Trains a compact U-Net (three encoder/decoder levels with skip connections, ~tens of thousands of parameters) on the 2D incompressible solver using TensorFlow.js. The data pipeline drives the real GPU solver headlessly across randomised parameters, captures (state, next-state) pairs near quasi-steady state, and converts them to normalised residual targets. Controls:

  • Model name — identifier used for saving / loading.
  • Obstacle — Circle (the 2D v1 shape).
  • Re min / Re max, U_in min / U_in max — the Reynolds-number and inlet-velocity ranges sampled during data collection.
  • Samples — number of randomised simulation runs; each contributes several training pairs.
  • Epochs — passes over the collected dataset (Adam optimiser, mean-squared-error loss).
  • Status — live progress: Collecting…, then Epoch n — loss …, then Saved ….

2D inference uses a CPU round-trip (read the field off the GPU, evaluate the network in TensorFlow.js, write the result back). At small 2D grids the data volume is tiny, so this stays real-time.

3D Training — Train 3D Model

Trains a small fully-convolutional 3D CNN (a flat stack of 3×3×3 convolutions, 8 → 16 → 16 channels, plus a 1×1×1 output convolution — deliberately not a U-Net so the GPU inference path needs only one kernel type). The data pipeline runs the real 3D incompressible solver headlessly, reads back the velocity and pressure volumes, and bakes the obstacle's occupancy mask as an additional input channel. Controls:

  • Model name.
  • Grid (³) — cubic resolution: 32, 64, 128, 256, 512 (the same set the 3D solver offers, so an applied model always lands on a valid sim resolution).
  • Obstacle — a dropdown: choose a single shape (Sphere, Box, Cylinder, Wing) or All to train one model that generalises across every shape. Multi-shape generalisation is possible because the obstacle's signed-distance / occupancy mask is supplied to the network as a dedicated input channel.
  • Re / U_in ranges, Samples, Epochs, Status — as in the 2D trainer (3D uses tiny batch sizes because the volumes are large).

Input channels (6): \(v_x, v_y, v_z\), pressure, obstacle occupancy (SDF), normalised Reynolds number. Output channels (4): residual \(\Delta v_x, \Delta v_y, \Delta v_z, \Delta p\).

⚠ Memory & cost at large grids. Cost scales with the number of cells, which grows as the cube of the resolution. Grids 16–64 are practical; 128 is heavy; 256 and 512 are extreme and may exhaust GPU or CPU memory during data collection, dataset assembly, or training (a single 256³ activation tensor is on the order of 1 GB; 512³ is ~8× larger). These options are provided for completeness and experimentation — for a first run prefer Grid 16–32, one shape, ~8–12 Samples, ~10 Epochs, and increase only once it trains successfully.

3D Inference — GPU-Resident Forward Pass

Unlike the 2D path, the trained 3D network runs entirely on the GPU. Its weights are uploaded to GPU buffers and the forward pass executes as a chain of WebGPU compute shaders operating directly on the simulation's velocity/pressure volumes:

$$ \text{pack} \;\rightarrow\; \text{conv3d} \times 4 \;\rightarrow\; \text{write-back} $$

The pack stage gathers the velocity, pressure, and obstacle-mask channels into a packed input buffer; each conv3d stage performs one zero-padded ("same") convolution with ReLU; the write-back stage integrates the predicted residual and writes the new velocity and pressure back to the volume textures (zeroing velocity inside solid cells). Because there is no CPU round-trip, the surrogate can in principle outrun the iterative solver — whose 3D cost is dominated by the many Jacobi pressure-Poisson iterations per step. The advantage is largest at higher resolutions; at small grids the physics solver is already inexpensive, so the surrogate may show little or no speed-up.

Correctness self-test. When a 3D model is applied, a one-off check runs the GPU forward pass and compares it against the reference TensorFlow.js evaluation on the identical input, logging the maximum absolute difference to the browser console. A small value (≈\(10^{-3}\) or below) confirms the GPU kernels reproduce the trained network.

Managing & Applying Models — My Models / Apply Model

My Models lists every saved surrogate with its parameter ranges and final training loss; each row has a trash icon to delete it. Models (weights plus a metadata record) persist in the browser through IndexedDB, so they survive page reloads.

Apply Model selects an Active Model and toggles Enable AI Solver. Enabling a model swaps the surrogate in for the physics solver: a 2D model switches the simulation to 2D incompressible at the model's grid; a 3D model switches to 3D incompressible at the model's grid (open the 3D tab to view the result). Disabling the toggle — or selecting None — instantly restores the real physics solver.

Scope, Accuracy & Practical Tips

  • Experimental: surrogates approximate the solver and are not a substitute for it when accuracy matters. A small network trained on few samples can drift or become unstable over many steps; if the field degrades, disable the AI Solver to recover.
  • Improving quality: increase Samples (raises the achievable accuracy ceiling), increase Epochs (reaches that ceiling), and keep the Re/U_in ranges as narrow as your use-case allows (less variety means the network has less to generalise over). Watch the loss in Status: if it is still falling at the last epoch, add epochs; if it has plateaued, add samples or accept the limit.
  • Speed comparison: to judge whether the surrogate is actually faster, watch the FPS readout in the Performance folder with the AI Solver on versus off at the same grid.
  • Supported obstacles (3D training): Sphere, Box, Cylinder, Wing (the analytical primitives whose occupancy can be computed directly). STL meshes and extruded 2D profiles are not yet covered by the 3D trainer.

13. Fly-Through Camera Mode (3D) Navigation

Inspired by the CATIA V5 Fly Mode, the 3D view supports a first-person fly-through camera that lets you navigate inside the control volume — go forward, backward, turn left/right, look up/down — independently of the normal orbit/zoom interaction.

Activating & Deactivating

  • Activate: Click the middle mouse button (scroll wheel) while the 3D view is active. A HUD overlay appears with a crosshair and a blue directional arrow pointing in the flight direction.
  • Deactivate: Click the middle mouse button again, or press Esc.
  • On exit the orbit camera is automatically re-centred at the position where you stopped, so normal orbit/zoom continues smoothly from there.

Controls

InputAction
Mouse movementSteer — left/right yaws the view; up/down pitches. Moving the mouse to the left turns the camera left (intuitive first-person mapping).
Scroll wheel upDecrease forward speed.
Scroll wheel downIncrease forward speed.
Right mouse button (hold)Fly backward while held; release to resume forward flight.
Middle-click or EscExit fly mode.

HUD Overlay

While fly mode is active, a semi-transparent overlay is drawn on top of the 3D canvas:

  • A ring crosshair at the screen centre with four orthogonal tick marks.
  • A blue chevron arrow above the crosshair indicating the flight direction (always pointing forward into the scene).
  • A status bar at the bottom of the screen showing the current speed (e.g. FLY MODE | Speed: 0.20) and a reminder of the key controls.

Default Speed & Range

The default forward speed is 0.2 world units / second. The control volume spans ±1 unit per axis by default, so the camera crosses it in about 10 seconds at default speed. Speed is adjustable in real time via the scroll wheel over the range 0.05 – 10 world units/s. If the Pointer Lock API is available in the browser, the cursor is hidden during fly mode and movement is captured as raw deltas for smoother steering.

14. Session Persistence UX

All simulation settings and the current 3D camera state are automatically saved to localStorage under the key wl_session_v1. The session is restored when the page is reloaded — you continue from exactly where you left off.

What is Saved

  • All GUI-exposed parameters (inlet velocity, viscosity, obstacle type, domain size, rendering options, etc.).
  • The 3D camera state (azimuth θ, elevation φ, radius r).
  • Whether the 2D or 3D tab was active when the session ended.

Ephemeral display values (FPS, frame counter, live Cd/Cl/Reynolds, STL triangle count) are intentionally excluded so that restored values are always meaningful.

Save Triggers

  • On page unload (beforeunload event) — fires when you close the tab, navigate away, or refresh.
  • Every 30 seconds — a periodic autosave guards against browser crashes.

To start fresh, clear localStorage in your browser's developer tools (Application → Storage → Local Storage) and reload.

Backward-Compatible Loading

The GUI grows new per-item fields over time (e.g. the Spin X/Y/Z and Show Local Axes controls added to Extra Obstacles and Ventilation Fans). A session, saved scene file, or Demo written before such a field existed won't have it — restoring that old data verbatim would leave the corresponding slider with no value to bind to and fail to build the sidebar. Every load path (session restore, Load button, Demo, and the AI assistant's scene edits) therefore backfills any missing per-item field with its default before rebuilding the GUI, so older saves keep working after an update instead of failing to load.

Play/Pause and Reset All Settings

The bottom of the sidebars ends with these controls, below Load/Save:

  • Play/Pause — every tab (Fluid, 3D, Study, House, Guide, Demo, AI, Truth, Bench) ends with the same Pause / Play simulation bar, kept in view at the bottom of the panel: blue while the simulation runs, green while it is paused. Only one simulation runs at a time, so all the bars control the same state and always show the same label. ⏭ Step 1 Frame and Reset All Settings sit side by side above it in the Fluid and 3D panels. (The Bench bar is locked while a benchmark is timing frames.)
  • Reset All Settings — restores every setting, in both the 2D and 3D panels, back to the software's original defaults: obstacles, Extra Obstacles, Ventilation Fans, the Temperature/Buoyancy zone, colormap themes, engine mode, everything. This is distinct from RESET FIELDS / Reset 3D Simulation, which only re-seed the flow field and leave your configuration untouched. Because it discards the current setup with no undo, it asks for confirmation before proceeding.

15. House Ventilation (Floor Plans) House tab2D

The House tab (home icon in the left sidebar, below Study) shows how the wind ventilates a building, room by room. It is meant for architects, designers and householders who want to know which windows and doors actually bring fresh air in, which ones let it out, and what happens when one of them is closed, moved or added. The plan is not drawn as an overlay: walls and door leaves become real solid cells in the 2D solver (rendered as concrete), the yard around the house is ground, and the indoor air is the flow.

Floor Plans: DXF Import, Example and Templates

  • Import DXF Plans… — load one or more .dxf files at once (drawn in CADCraft, AutoCAD or any CAD program that saves DXF). Each becomes an entry in Active Plan, so several houses or variants can be compared in one session.
  • Example: Apartment (CADCraft) — a bundled apartment plan from the CADCraft project (MIT licence). It loads automatically the first time the tab is opened.
  • Template: One Room / Two Rooms — simple plans to start from and edit.
  • Drawing Units — Auto reads the DXF $INSUNITS header and otherwise guesses from the plan size; override with mm, cm, m, inches or feet if the size shown under Plan Size is wrong.

How the drawing is read. The importer understands LINE, LWPOLYLINE (including arc bulges), POLYLINE, ARC, CIRCLE and nested block INSERTs. Walls, windows and doors are recognised by their layer or block name — e.g. A-WALL, A-GLAZ, A-DOOR, WALL, WINDOW, DOOR, plus common names in other languages. Wall thickness comes from the two faces of each wall; a door's width comes from its swing arc and leaf. Every opening gets an id (W1, W2… for windows, D1, D2… for doors) and is classed as exterior (between a room and outside, with the facade it sits on: N, E, S, W…) or interior (between two rooms).

Wind Model: Facade Pressure Coefficients

The wind is not simulated around the building; it is represented by the pressure it puts on each facade, the usual approach in natural-ventilation design. For wind speed \(U\) and a facade pressure coefficient \(C_p\), the pressure at an opening is

$ p = \tfrac{1}{2}\,\rho\,U^2\,C_p $

with defaults \(C_p = +0.6\) on the windward facade, \(-0.6\) on the side walls and \(-0.3\) on the leeward facade (all three are sliders in the Wind folder). Wind From is the direction the wind comes from in degrees, 0° = North = the top of the plan. Openings are driven at two pressure levels: all inlets at the average \(C_p\) of the inlet facades, all outlets at the average \(C_p\) of the outlet facades; the Results box shows both pressures in Pa. If every open window is on facades with the same \(C_p\), there is no pressure difference and the panel says so: no cross-flow is expected.

Wind at the building. By default Wind Speed is a weather-station wind at 10 m in open country (Wind Speed Is = "Weather station (10 m)"). The \(C_p\) values refer to the wind at the building, so it is converted with the BS 5925 / CIBSE method, \(U_H = U_{10}\,K\,H^a\), using Site Terrain (open flat country \(K\)=0.68, \(a\)=0.17; country with scattered windbreaks 0.52, 0.20; suburban / urban 0.35, 0.25; city centre 0.21, 0.33) and Building Height. A 4 m/s station wind is about 2 m/s at a low house in a suburb; the result is shown as Wind at Building. Choose "At the building" to enter that speed directly. Outside Windows Are (Windows & Doors folder): Fully open (purge), Half open or Tilted (kiep, ~10%); the 2D model applies it as the windows' effective open height, so flows scale with it (interior and outside doors are not reduced).

Internally the scene is a Control-Volume, pressure-driven, SI-unit incompressible case (air, \(\nu = 1.48\times10^{-5}\) m²/s, LES on): inlet openings are painted as CV inlet cells and outlet openings as CV outlet cells, so the pressure difference, not a prescribed velocity, decides how much air goes where. The time step follows the wind, \(\Delta t = 0.8\,\Delta x / u_{max}\) with \(u_{max} = U\sqrt{C_{p,windward} - C_{p,side}}\), so the fastest jet moves less than one cell per step.

Choosing Inlets, Outlets and Closed Openings

  • Auto (by wind) — the default for outside openings: windward ones become inlets, the others outlets.
  • Inlet / Outlet / Closed — force any outside window or door to let air in, let it out, or stay shut.
  • Interior doors — Open or Closed, individually or all at once with All Interior Doors.

Set them in the Windows & Doors folder, or simply click a window or door on the map: a small menu opens at that spot with the same choices. Labels on the map show each opening's id and live airflow, coloured green (inlet), blue (outlet) or grey (closed); the arrow at the top right shows the wind. Changes apply to the air that is already moving — the simulation is not restarted, so the effect of opening or closing a window can be watched as it develops.

Editing the Plan on the Map

The Edit Plan (on the map) folder turns the viewport into a simple plan editor:

ToolWhat it does
PickClick a window or door to open its menu: set its role (Air in / Air out / Closed / Auto), slide it along its wall in 10 cm steps (◀ ▶ or ▲ ▼), make it narrower or wider in 10 cm steps (− / +, growing evenly on both sides, with the width shown), set the Wind at that window or door (0.5 m/s steps, ↺ back to the global Wind Speed), or press Drag on the map to switch to the Move tool. The menu stays open while you click, and the flow updates at once. Panning the view still works.
MoveDrag a window or door to slide it along its wall (it stays on the wall); drag one of the round handles at its ends to make it wider or narrower (5 cm steps, at least 0.4 m; the width is shown while dragging). Both stop at the end of the wall and 5 cm before a neighbouring opening. Drag a wall corner, or drag the side of a wall to push the whole wall — both faces and the windows in it move together and connected walls stretch.
Draw WallDrag to draw a wall of the Drawn Wall Thickness; snaps to 5 cm, to horizontal/vertical and to existing corners, and shows its length.
Add Window / Add DoorClick on a wall to put an opening there. It takes the thickness of the wall it sits in.
DeleteClick a wall, window or door to remove it.

Edits are re-rasterised on a fixed layout, so the map never jumps, and swapped into the running solver immediately. Undo (Ctrl+Z, up to 50 steps), Reset Plan to Original and Download Edited Plan (DXF) complete the loop: the exported DXF (metres; layers A-WALL, A-GLAZ, A-DOOR; doors as leaf + swing arc) opens in CADCraft or AutoCAD and re-imports to exactly the same walls, windows and doors.

Results: Airflow per Opening, Fresh Air and Air Changes per Hour

Every ~0.6 s the tab reads the velocity field and integrates the flow across the air side of each opening. In 2D this is a flow per metre of height, \(q_{2D}\) (m²/s); it is turned into a volume flow with the opening height from Room Data (window or door height, capped by the ceiling):

$ Q = q_{2D}\,h, \qquad \mathrm{ACH} = \frac{3600\,Q_{in}}{A_{floor}\,H_{ceiling}} $

The Results box shows ACH, fresh air in L/s, and the flow through every opening (in, out or through). Because indoor air is incompressible, the air coming in and going out should match; the remaining difference (typically 10–15% once the flow has settled) is shown as the measurement uncertainty, and a larger difference is flagged as "still settling".

Each Opening's Effect → Rank Each Opening runs a short study: a baseline with everything as set, then each outside opening closed in turn. Each case settles for one to three flow-through times (Study Length Quick / Standard / Long) and is averaged over 8 samples. The table lists, for every opening, how much the fresh-air rate drops when it is closed — the share each window or door contributes. Your own settings are restored afterwards.

Reference numbers (bundled apartment, wind 4 m/s from the north): about 75 ACH and 4.5–4.6 m³/s through the house; a simple orifice-equation estimate for the same openings gives 4.7 m³/s, and in and out agree within about 12%. Closing window W5 drops it to about 51 ACH; the ranking shows W4 and W5 carrying most of the cross-flow and the south windows contributing 7–11% each.

Scope and Limits

  • 2D plan model. Each opening acts over its full height, and L/s and ACH scale the 2D flow by opening and ceiling height. Use it to compare layouts and opening choices, not as a certified ventilation figure.
  • Wind only. No stack (thermal buoyancy) effect, no surrounding buildings or trees, and the \(C_p\) values are generic: for a sheltered or unusually shaped building, adjust them from a \(C_p\) database or wind-tunnel data.
  • Turbulent room air. A "high Reynolds" notice is expected here; room airflow is turbulent and is handled by the LES model.
  • Saving. The flow settings are saved with the session, but the list of imported plans and your edits are not — use Download Edited Plan (DXF) to keep an edited plan.

The House tab can also be driven by the AI assistant (§9): ask it to load the example, change the wind, set windows to inlet/outlet/closed, or rank the openings, and it reads the live per-opening flows and ACH back to you.

16. Assisted Setup, Checks and Learning Tools New

Tools that catch setup mistakes before they produce a wrong answer, explain results in plain language, and turn photos, scans and meshes into usable geometry. The checks are deterministic rules (the numbers they quote come from the same formulas the solver uses); the AI assistant is given their findings so it can explain and apply them, but does not invent them. The two learned models used are small, open-licence and run on your device.

Setup Check and Blow-up Guard

The Setup Check button (checklist icon, top right next to Undo/Redo) shows a badge with the number of findings and opens a panel listing each one, why it matters, and — where a safe fix exists — a one-click fix. Fixes go through the normal settings path, so they run each control's own update and can be undone with Ctrl+Z. Ask AI to explain hands the findings to the assistant.

CheckRuleFix offered
Time step (CFL)2D: warning above 2.2, note above 1.5 (the 2D scheme stays stable but smears). 3D: error above 2.2, warning above 1.5. In SI mode the real-world time step is checked against the cell size.Time step for CFL ≈ 0.8
DNS resolutionLES off while the grid is coarser than the DNS estimateTurn LES on
Forces with Vorticity ConfinementConfinement adds artificial spin and biases Cd/ClTurn it off
Cells across the body2D under 20, 3D under 14 (the validated Re-100 sphere has 16)Finer grid
BlockageFrontal size over 10% of the channel (2D) or area over 5% of the tunnel section (3D)Smaller body
Run-up and wakeBody closer than 1 body length to the inlet, or under 3 lengths of wake (only while forces are measured; not for spinning rotors)Move the body (only if it then still fits)
Shape mismatch (3D)The shape menu and the shape the solver runs disagreeRe-select the shape
Other 3DCompressible below Mach 0.3, Multigrid off while measuring forces, Temperature drawn as a fogging volume, fans on with no power, inflow with the outlet closedMatching one-click fix

The blow-up guard reads one slice of the velocity field every ~2 s. Any NaN pauses the run (a NaN field never recovers) and opens the Setup Check with lower the time step and restart. A cell moving more than 8 cells per step — far beyond any stable flow — is reported as a runaway without pausing.

Mesh Check (3D import)

Runs on every STL/OBJ/3DS import, before the (slow) voxelizer: triangle count, size and likely units, watertightness (open and non-manifold edges — inside/outside is decided by ray counting, so holes leak), zero-area triangles and heavy meshes. It proposes an orientation — the longest side along the flow (+X), up = Z for STL/3DS and Y for OBJ — with side and top previews and buttons to change the flow axis, the up axis, turn it front-to-back or upside down, or keep the file orientation. Options: size it for the wind tunnel (about 5% blockage and at most 35% of the tunnel length), place it 30% along the tunnel, and close small gaps with a thin shell. The chosen rotation is stored with the mesh and re-applied when a session is restored. Import as-is skips everything.

Report Summary

The HTML report (2D Aerodynamics folder) now opens with a written Summary: what was simulated; the time-averaged Cd and Cl with their RMS fluctuation and whether the average had settled; the Ground Truth reference and the percentage difference (with the known 20–30% low reading of the 2D solver in shedding flow); a trust level (Good / Moderate / Low) with its reasons; and next steps. The Setup Check findings at report time are listed too. Every number in the summary also appears in the report's tables. The optional AI-written summary (off by default) adds a paragraph written by your selected AI provider from the report's numbers only; the screenshot is not sent.

Guided Tours (Demo tab)

Short lessons that load demos, let the flow develop and explain what to look at, with Back/Next and Ask AI about this: Shock waves (Sod tube, its exact solution in the Truth tab, a Mach 2 and a Mach 3 wedge — theory 47.8° and 34.0° for this 16.7° wedge), Vortex shedding & Reynolds number (honey Re 5 vs a von Kármán street at Re 200), Measuring drag you can trust (sphere at Re 100: Cd ≈ 1.11 vs 1.09), Seeing vortices in 3D (Q-criterion and Lambda-2) and Heat and ventilation. The AI assistant can start them ("teach me shock waves").

Smoke Glow volume style (3D) and the Demo groups

3D View → Smoke Glow (volume) renders the field as glowing smoke, the style of production CFD volume renders (for example Flow360's vorticity images). Opacity is a band: zero for calm air, rising above the Threshold, and falling back to zero at Glow Band Top (a fraction of the colour window), so the strongest cores stay see-through and every filament shows a glowing edge instead of filling in like fog. The colormap is spread across that band (Magma or Inferno look best), Volume Opacity is the opacity of one grid cell of glow (so the look does not change with Render Scale), and Fade Near Body fades the glow to zero within that distance of the body so the boundary layer does not speckle its surface. The march takes twice the samples in this style. Environment → Dark Studio gives a dark backdrop whatever the menu theme. The Swept Wing Tip Vortices 3D demo uses it on vorticity at Re ≈ 15,000, 160³.

The Demo tab groups the scenes into Aerodynamics & Vortices, Machines & Rotating Parts, Shocks & Compressible Flow and Heat, Ventilation & Buildings (assigned from words in the file name); each group, the Teaching Labs and the Guided Tours open with a click and remember whether they were left open.

Teaching Labs (Demo tab)

A lab session done end to end for a teacher: one press sets up a classic experiment in the 2D solver, lets the flow develop, measures it, compares every measurement with published data and downloads a student worksheet (aim, theory, prediction questions with blank answer boxes, method, the measured data and charts, analysis questions) and an instructor answer key (the same pack with every answer worked from the measured numbers), plus Data (CSV). Optional Course and School fields go on the header.

  • Flow past a cylinder: drag & vortex shedding (Quick: Re 40, 100, about 4 min; Full: Re 20, 40, 100, 200, about 9 min). Same setup the solver's accuracy was validated on (768 × 384 grid, D ≈ 46 cells, ν = 0.4, 200 pressure iterations). Each case develops for 35 (steady) or 100 (shedding) convective times D/U and is measured for 20 or 50 more: time-mean Cd ± fluctuation from the surface force; the Strouhal number from the cross-stream velocity at a probe 3 D behind the cylinder, read every frame and timed by its crossings of the mean; and, for the steady cases, the recirculation length L/D from the centre-line velocity. References: 2D laminar Cd (Tritton; Dennis & Chang; Henderson), St = 0.2731 − 1.1129/√Re + 0.4821/Re (Williamson & Brown 1998), L/D (Coutanceau & Bouard; Dennis & Chang).
  • Shock tube (Sod problem) (about 1 min): runs the bundled Sod demo until just before a wave reaches an end of the tube and compares the density profile with the exact Riemann solution: shock position and speed, shock Mach number, the two plateau densities, shock and contact widths and the L1 error.

The worksheet states the tool's measured accuracy (within about 10% for steady, resolved flow; time-mean drag 20–30% low once the wake sheds) so students treat the numbers as a measurement with an error to discuss. The AI assistant and Claude Code can run a lab with {"action": "lab", "changes": {"run": "cylinder", "set": "quick"}} and download the files with {"download": "instructor"}.

House Tab: Rooms, Advice and Plan Pictures

  • Rooms are found automatically: the indoor air splits into connected regions at the doors. Each room gets an id (R1 = largest), its area, a cautious type guess (hall/corridor, main room, small room) and its live mean air speed. Rooms with no window to the outside, or with stagnant air (under a quarter of the house's average speed), are flagged on the map.
  • ✨ Suggest Which Openings to Open runs Rank Each Opening if needed and writes advice: openings to keep open (each carries over 15% of the fresh air), worth keeping (5–15%), can be closed (under 5%), and any whose closing increases the fresh air (the flow short-circuits through them), plus advice for each flagged room. Apply: Fewest Openings and Apply: Most Fresh Air close the corresponding openings and simulate the result — closing several openings is not the sum of closing them one by one — with an Undo link.
  • Import Plan Picture reads a scanned or exported plan (PNG/JPG). Thick dark strokes are walls; thin lines (text, dimensions, furniture, door swings, glazing) are dropped by a distance-transform stroke-width test. Door- and window-sized gaps in the walls (0.5–2.6 m, as thin as a wall) become openings: a gap crossed by thin glazing lines is a window, an empty one a doorway. Picture: House Width (m) sets the scale. It works best on plans with solid walls; on a single-line drawing every line is read as a wall. Check the result on the map and correct it with the Edit Plan tools.
  • 🏠 Example Houses loads a ready plan: the CADCraft apartment (DXF), a studio flat (windows on one facade only), a terraced house (party walls, front and back openings), a 3-bedroom bungalow (central corridor), an open-plan loft and an L-shaped house, plus the one- and two-room templates. They are built from the same walls, partitions and openings a DXF import produces.
  • 🛋 Furniture (decoration): every plan is furnished when it first opens — sofa, rug and coffee table in the largest room, beds with nightstands and wardrobes, a kitchen counter and dining set, bathroom fittings in small windowless rooms, plants in corners, nothing in front of a door. 🎲 Decorate Randomly gives a new arrangement; Show Furniture, Furniture Opacity and Clear Furniture control it. Realistic Furniture (on by default) draws every piece from above the way a styled interior plan does — cream upholstery with soft volume, oak and walnut with grain, a marble worktop, ceramic fittings, bedding with velvet cushions and a knit throw, plants in terracotta pots, lamps, books and vases — each with its own soft shadow; untick it for the simple flat drawings. The furniture is drawn for the look only and does not enter the flow: in a 2D plan model every solid acts up to the ceiling, so a sofa would be simulated as a wall.
  • 🎨 Plan Look → Illustrated (same folder, now Furniture & Garden): turns the plan into a presentation drawing seen from above — a mown lawn round the house with trees and shrubs casting soft shadows, flower beds along the facade, stepping-stone paths from the outside doors, a paved patio with a bistro set at the main room's door, and the house's own shadow on the grass. Indoors, faint wood floors (tiles in kitchens and bathrooms) and pastel furniture with soft contact shadows. Floor Opacity sets how strongly the floors show; keep it low so the airflow colours stay readable. Like the furniture, it is decoration only: the garden covers the yard, which is solid ground to the flow anyway, and nothing in it changes the simulation.
  • View → Show: Air speed (colour = speed) or Air paths (one colour per window), the Dye Streams field: every window or door to the outside releases its own colour, so you can follow whose fresh air reaches which room; a room that stays white gets no fresh air from that wind. The choice is kept when another plan is loaded.
  • Assessment report: how rooms are judged. A room is credited with the air passing through it, but never more than the fresh air entering the whole house in that wind direction (air circulating between rooms through interior doors is not fresh air), and it is judged on the lower of the simulation and the textbook orifice-network calculation, so a disagreement between the two can only make the verdict stricter. Target Preset offers the Bbl purge-ventilation figures (spuiventilatie, art. 4.131: 3 dm³/s per m² for a verblijfsruimte, 6 for a verblijfsgebied), converted to air changes per hour at the ceiling height; the room table also shows each room's worst fresh air in dm³/s per m². This is an orientation, not the NEN 1087 check. Report Language: English or Nederlands; a finished study can be downloaded again in the other language.
  • House demos: the Demo tab lists House Ventilation Bungalow (wind from SW), L-shaped House (wind from W) and Terraced House (wind from N). Each opens the House tab with that example plan and wind.
  • Wind per window or door: each opening to the outside can have its own local wind speed (its menu → Wind), e.g. a window sheltered by a neighbouring building or a hedge. The solver holds every opening at its own wind pressure ½·ρ·Ui²·Cp (Cp of its inlet/outlet group); with all openings at the global speed this is exactly the two-level model. Note the sign: on a windward facade (Cp > 0) more wind pushes more air in, but on a sheltered or side facade (Cp < 0) the wind sucks, so more wind there pulls more air out — the menu says which applies and shows the pressure. The orifice cross-check in the Ventilation Assessment uses the same per-opening pressures.
  • Play / Pause sits at the bottom of the House menu and stays in view while the menu scrolls.

House Tab: Ventilation Assessment (report)

📋 Ventilation Assessment → Run Assessment + Report does the whole study in one press and downloads a report you can hand to a client. The plan is run for each wind direction (Wind Directions: 4, 8 or the current wind only) at the set Wind Speed and Cp values, with the same settle-and-sample schedule as Rank Each Opening (Study Length). For every room and direction it measures the through-flow air changes per hour,

$ \mathrm{ACH}_{room} = \frac{3600 \cdot \tfrac12 \sum_{open} |Q_i|}{A_{room}\,H_{ceiling}} $

(half the sum of the flows through all its open windows and doors, since what comes in also goes out), and compares it with Target per Room (ACH). The default of 4 ACH is the usual purge-ventilation figure for a habitable room. Each direction has its own uncertainty band: the larger of its measured in/out mismatch and twice its sample fluctuation, never below 10%. A direction whose in and out flows differ by more than 25% is flagged. If its samples still vary, the flow had not settled and the report suggests a longer Study Length. If they are steady, it is a bias in the outflow measurement: ACH is taken from the inflow (on the example apartment, wind from E, it stayed within 1% across run lengths and grid sizes while the mismatch did not improve), and that direction is judged with the wider band. A room within that band of the target is reported Marginal rather than pass or fail. Corridors and small windowless rooms (from the room-type guess) are listed but not judged.

The report (self-contained HTML, prints to PDF) contains: an overall verdict, a polar chart of whole-house ACH by wind direction against the target, rule-based recommendations, a room × direction table (green / amber / red), whole-house and per-opening flows per direction, a flow snapshot for each direction, and the method and its limits (2D plan model, generic Cp, no single-sided or stack ventilation, design-stage tool). Cross-check: every direction is also recalculated by hand-calculation methods that do not use the simulation, with the same opening sizes and facade pressures: an orifice network (every room a zone at its own pressure, every open window and door, interior doors included, an orifice \(Q = C_d A \sqrt{2\Delta p/\rho}\), room pressures solved by Newton's method so each room's flows balance — the multizone method of CIBSE AM10 and CONTAM) and the single-zone BS 5925 formula \(Q = C_d A_{eff} U \sqrt{\Delta C_p}\), \(1/A_{eff}^2 = 1/A_{in}^2 + 1/A_{out}^2\), with \(C_d = 0.6\). The simulation agrees when its fresh air is within the \(C_d\) 0.5–0.7 range of the network figure, widened by that direction's uncertainty band. A direction that disagrees is named in the recommendations (above: usually an inlet jet crossing straight to an outlet; below: an undeveloped flow or a sharply bending air path); the verdict itself is never changed by the cross-check. The report shows both figures per direction and the simulated vs network ACH per room. Download Results (CSV) gives the same numbers as a spreadsheet. The recommendations are fixed rules applied to the measured numbers, so the report never contains a figure the simulation did not produce. The AI agent and Claude Code can start it with {"assess": true} and read the verdict from the scene state.

Example reports (open in a new tab; each is the file the tab downloads, unchanged):

  • Apartment (CADCraft example), 8 wind directions — all three habitable rooms meet a 4 ACH target; shows the orientation advice, the draught warning and a cross-check that disagrees for four directions (the verdict then uses the stricter textbook figure).
  • Terraced house, 4 wind directions, Bbl purge target — 4 m/s weather-station wind converted to the house (suburban terrain), windows fully open, target 3 dm³/s per m² (Bbl verblijfsruimte). Wind along the street gives no cross-flow, so the house only partly meets the target: an honest example of what the report says when ventilation depends on the wind direction.

AI Cut-out for Uploaded Pictures (2D)

Upload Image opens a cut-out dialog: click the object to keep (more clicks add parts), right-click to mark background. It uses SlimSAM, a distilled Segment Anything model (Apache-2.0, ~14 MB, downloaded once, runs on the CPU in the browser; the picture never leaves the device). The mask is cleaned for use as a body: specks removed, small gaps closed and enclosed holes filled (a car's window is not a hole through the car). Use brightness threshold instead keeps the original silhouette method; the AI Cut-out on Upload checkbox turns the dialog off. The model only decides which pixels are the body; it does not alter the flow or its rendering.

Bubbles (Sparger) — gas in a stirred tank (3D)

With Enable Bubbles every 3D particle becomes a bubble parcel: released at a ring or disk sparger, carried by the liquid plus its own rise (slip) speed \(v_s\), burst at the Liquid Surface height and released again. It moves with the physical time step, not the Particle Speed display multiplier. The coupling is two-way: the bubbles in each cell are counted every substep, and the liquid receives an upward acceleration proportional to the local count (averaged with the six neighbouring cells), applied before the pressure projection like buoyancy:

$ a_y = k\,\bar{n}_{cell}, \qquad k = G\,\frac{v_s^2}{H_p}\,\frac{V_p}{N} $

where \(N\) is the number of bubble parcels, \(H_p\) the plume height (sparger to surface) and \(V_p\) the plume volume (sparger radius × height). This is calibrated on bubble-column behaviour — the liquid dragged up by a bubble plume moves at about the bubbles' rise speed — so Gas Push \(G = 1\) gives a plume about as fast as \(v_s\), independent of the bubble count and the grid. In a still tank (impellers stopped) the bubbles alone drive a plume up to ≈1.8× \(v_s\) and the return flow down the walls. Not modelled: bubble break-up and merging, size distribution, gas dissolving into the liquid (mass transfer), and a free liquid surface. Demo: Stirred Tank with Sparger 3D.

17. Demo Gallery Slides PDF

The demo scenes in the DEMO tab are collected in a slide deck: for each scene, a Blender (Cycles) render of a run recorded in the app, what the picture shows, the physics and solver settings, and the result, tagged VALIDATED (compared with a reference), PARTLY or SHOWCASE (demonstrates a feature; numbers not validated). It ends with a table of the measured values against their references.

Open the slides

OTOM CFD Demo Gallery (PDF, 39 slides) — also linked at the top of the DEMO tab, and online in the OTOMcomposite image gallery.

Your browser cannot show the PDF here; use the link above.

Validated scenes

SceneMeasured vs reference
Sphere Drag at Re 100 (3D)Cd ≈ 1.11 vs 1.09 Schiller–Naumann (≈ 3%)
Supersonic Wedge, Mach 2Shock angle ≈ 47–50° vs 47.8° oblique-shock theory
Sod Shock TubeDensity, velocity and pressure profiles on the exact Riemann solution
Kármán Vortex Street (Re 200)Shedding reproduced; Cd ≈ 0.9–1.0 vs ≈ 1.3 (known 20–30% low bias of the 2D solver in shedding flow)

12. Academic References & Theoretical Foundations

The numerical methods and boundary immersion techniques driving this WebGPU solver heavily draw upon academic research published in top-tier Elsevier journals. For further reading on the mathematics of WaterLily-style models, consult the following literature:

Weymouth, G. D., & Yue, D. K. P. (2011).
Boundary data immersion method for Cartesian-grid simulations of fluid-body interaction problems.
Journal of Computational Physics, 230(22), 6233-6247. [Elsevier]
Establishes the BDIM framework used for obstacle rendering and interaction without conformal meshing.
Maertens, A. P., & Weymouth, G. D. (2015).
Accurate Cartesian-grid simulations of near-body flows at intermediate Reynolds numbers.
Computer Methods in Applied Mechanics and Engineering, 283, 106-129. [Elsevier]
Expands on projection methods and pressure Poisson solvers over non-conformal grids, foundational for the Jacobi iterations used herein.
Laizet, S., & Lamballais, E. (2009).
High-order compact schemes for incompressible flows: A simple and efficient method with quasi-spectral accuracy.
Journal of Computational Physics, 228(15), 5989-6015. [Elsevier]
Provides context on high-order spatial discretizations necessary for maintaining stability in Cartesian solvers at high Reynolds numbers.
Toro, E. F., Spruce, M., & Speares, W. (1994).
Restoration of the contact surface in the HLL-Riemann solver.
Shock Waves, 4(1), 25-34. [Springer]
Introduces the HLLC approximate Riemann solver used by the compressible Euler engine to capture shocks, contact surfaces, and shear waves.
Antoniadis, A. F., Drikakis, D., Farmakis, P. S., et al. (2022).
UCNS3D: An open-source high-order finite-volume unstructured CFD solver.
Computer Physics Communications, 279, 108453. [Elsevier]
High-order finite-volume methodology (MUSCL/WENO reconstruction with Riemann solvers) that informs the compressible engine's flux-reconstruction roadmap.
Smagorinsky, J. (1963).
General circulation experiments with the primitive equations.
Monthly Weather Review (Historical context; modern implementations reviewed widely in Elsevier's Computers & Fluids).
The basis for the Large Eddy Simulation (LES) sub-grid scale modeling implemented in the software's turbulence settings.