@ferret/flow-solver
flow boxes: a solved 2d slice of air or water round bodies (staggered grid, pressure projection) that publishes its wake into the flow field and pushes bodies by surface pressure.
Read the guide: how to use it →
Classes
FlowBox class
class FlowBoxreadonly def: FlowBoxDef | |
readonly name: string | |
readonly plane: 'xz' | 'xy' | |
grid: FlowGrid | |
cell: number | the cell size actually used (coarser than asked when over maxCells) |
degraded: boolean | true when the box had to coarsen to fit its budget |
sleeping: boolean | a sleeping box adds nothing: the analytic field stays (no body, nobody near it) |
readonly bodies: Map<string, Placed> | |
time: number | the world time of the field in the grid, s |
steps: number | |
readonly oa: number | |
readonly ob: number | |
readonly level: number | |
constructor(def: FlowBoxDef, readonly field: FlowField) | |
get medium(): Medium | |
get cells() | |
dispose() | |
world(a: number, b: number): V3 | |
contains(p: V3): boolean | |
addBody(d: BoxBodyDef): void | |
removeBody(id: string) | |
setBody(id: string, s: { position: [number, number]; angle?: number; velocity?: [number, number]; omega?: number }) | move a body: its position, angle and velocity in the plane |
maxSpeed(): number | The largest speed in the grid, m/s. |
step(dt: number, t: number, maxSub = 4) | Advance by dt at world time t (the box sub-steps so the Courant number stays near one, at most maxSub times). |
force(id: string): [number, number] | The force of the flow on a body, N, in the two axes of the plane (per metre of slice times the body's depth). |
velocityAt(p: V3): V3 | |
pressureAt(p: V3): number | pressure at a point, Pa gauge (zero at the outflow) |
dynamicPressure(p: V3): number | dynamic pressure ½·ρ·v², Pa |
slice(every = 1): Array<{ at: V3; velocity: V3; speed: number; pressure: number; solid: boolean }> | A decimated slice for the inspector: points with velocity, speed and pressure (at most 4096). |
wake() | |
sleep() |
packages/flow-solver/src/box.ts:60
FlowGrid class
class FlowGridreadonly nx: number | |
readonly ny: number | |
readonly h: number | |
readonly density: number | |
nu: number | |
confinement: number | |
iterations: number | |
tolerance: number | |
u: Float64Array | |
v: Float64Array | |
p: Float64Array | |
readonly owner: Uint16Array | 0 fluid, otherwise the id of the body that owns the cell (1 = static colliders) |
readonly sx: Float64Array | velocity of the solid in a solid cell (the wall's velocity) |
readonly sy: Float64Array | |
sides: [Side, Side, Side, Side] | boundary types: left, right, bottom, top; and the velocity prescribed on inflow faces (per face along that side) |
readonly inL: Float64Array | |
readonly inR: Float64Array | |
readonly inB: Float64Array | |
readonly inT: Float64Array | |
divergence: number | divergence left after the last projection (max over fluid cells, 1/s), iterations used, and the residual |
used: number | |
residual: number | |
constructor(o: GridOptions) | |
solid(i: number, j: number): boolean | |
fluid(i: number, j: number): boolean | the cell is fluid and inside the grid |
applyBoundary() | Put the values of the boundaries and of the solids on the faces they own. |
sampU(x: number, y: number): number | u and v at a cell-unit position, as numbers (no allocation: the advection calls these a million times) |
sampV(x: number, y: number): number | |
velocityCells(x: number, y: number): [number, number] | velocity at a point in cell units (x, y from the grid origin / h) |
velocityAt(x: number, y: number): [number, number] | velocity at a point in metres from the grid origin |
pressureAt(x: number, y: number): number | |
centre(i: number, j: number): [number, number] | velocity at the centre of a cell |
step(dt: number) | |
forces(): Map<number, SolidBody> | The force the flow puts on every body (per unit depth, N/m): the pressure on the faces that touch the fluid plus the wall shear. |
packages/flow-solver/src/grid.ts:25
Functions
applyFlowTier function
function applyFlowTier(field: FlowField, tier: { flowSources: number; flowBoxCells: number }): voidGive a field the source budget of a tier.
packages/flow-solver/src/box.ts:274
flowBoxes function
function flowBoxes(fieldOf: (host: PhysicsHost) => FlowField | undefined): FlowBoxCapabilitypackages/flow-solver/src/capability.ts:45
flowBoxForms function
function flowBoxForms(br: FormBridge, cap: FlowBoxCapability): voidpackages/flow-solver/src/capability.ts:135
flowTierBudget function
function flowTierBudget(tier: { flowSources: number; flowBoxCells: number }, every = 1): { maxSources: number; maxCells: number }The budget of a hardware tier for the flow: the sources a field takes and the cells a box may have when stepped every every-th physics step.
packages/flow-solver/src/box.ts:270
validateFlowBox function
function validateFlowBox(d: unknown, path = 'flowBoxes'): string[]packages/flow-solver/src/capability.ts:20
Interfaces
BoxBodyDef interface
interface BoxBodyDefid: string | |
shape: BoxShape | |
depth?: number | extent along the slice normal, m (the force is the force per metre times this); default 1 |
position?: [number, number] | centre in the plane, m (world coordinates of the plane's two axes) |
angle?: number | |
velocity?: [number, number] | |
omega?: number | |
static?: boolean | static colliders are voxelised once |
packages/flow-solver/src/box.ts:15
FlowBoxCapability interface
interface FlowBoxCapability extends Capabilityreadonly boxes: Map<string, { box: FlowBox; def: FlowBoxLevelDef; acc: number }> | |
add(def: FlowBoxLevelDef): FlowBox | |
remove(name: string): void |
packages/flow-solver/src/capability.ts:39
FlowBoxDef interface
interface FlowBoxDefname: string | |
plane?: 'xz' | 'xy' | |
origin: V3 | the corner of the box with the smallest coordinates, [x, y, z]; the coordinate off the plane is the slice's level |
size: [number, number] | extent in the plane, m |
cell: number | cell size, m |
medium?: Medium | |
density?: number | kg/m3 and Pa·s; defaults of the medium |
viscosity?: number | |
margin?: number | cells over which the solved field blends into the analytic one at the edge (default 4) |
maxCells?: number | cells most the box may have; more are coarsened and degraded is set (default 40000) |
confinement?: number | |
iterations?: number | |
bodies?: BoxBodyDef[] | |
forces?: 'solved' | 'coefficients' | solved: the pressure on the surfaces drives the bodies; coefficients: only the field is solved (cheap), the media drag stays |
reach?: number | half-thickness of the slice along its normal beyond which the box adds nothing, m (default 5) |
packages/flow-solver/src/box.ts:29
FlowBoxLevelDef interface
interface FlowBoxLevelDef extends Omit<FlowBoxDef, 'bodies'>The flowbox capability: the flow boxes of a level, stepped in the field phase, with the rigid bodies in them.
Level block physics.flowBoxes: a list of FlowBoxDef where each body may have a node (a rigid body of the level whose position, angle and velocity
in the plane the box takes every step, and which the force of the flow is applied to when the box's forces is solved).
solved: the pressure and shear on the body's surface drive it and the media drag of a body in an air box is switched off (exempt: ['media']),
so nothing is counted twice. coefficients (default): the box only solves the field (the wake for the render and for sails); the media drag stays.
A box sleeps while it has no body and nobody wakes it; every steps it every N-th physics step.
bodies?: Array<BoxBodyDef & { node?: string }> | |
every?: number | step the solver every N-th physics step with the accumulated time (default 1) |
packages/flow-solver/src/capability.ts:14
FormBridge interface
interface FormBridgeSensors for the language (3.9): (pressure-at x y z), (dynamic-pressure x y z) read the box that contains the point.
define(name: string, sig: { args: string[]; returns?: string }, fn: (ctx: unknown, ...args: any[]) => unknown): unknown |
packages/flow-solver/src/capability.ts:134
GridOptions interface
interface GridOptionsnx: number | |
ny: number | |
h: number | |
density: number | |
nu: number | kinematic viscosity m2/s |
confinement?: number | vorticity confinement strength, 0 = off |
iterations?: number | most CG iterations per projection (default 80) and the relative residual to stop at (default 1e-5) |
tolerance?: number |
packages/flow-solver/src/grid.ts:11
SolidBody interface
interface SolidBodyid: number | |
fx: number | |
fy: number | |
cells: number |
packages/flow-solver/src/grid.ts:23
Types
BoxShape type
type BoxShape = | { type: 'circle'; radius: number } | { type: 'box'; half: [number, number] } | { type: 'polygon'; points: Array<[number, number]> }A flow box: a bounded slice of the level where the flow is solved. It takes its inflow from the analytic sources of the level
(@ferret/flow), solves the flow round the bodies in it, and publishes itself back into the field as a solved source, so the media, the vessels,
the character, the rotors and the render read the wake without a change on their side. 2d slices: plane: 'xz' is horizontal (the streets of a
town, a river seen from above) at height level, plane: 'xy' vertical (the section of a roof or a wing) at depth level.
packages/flow-solver/src/box.ts:10
Side type
type Side = 'wall' | 'inflow' | 'outflow'The solver of the flow box: incompressible flow on a staggered (MAC) grid in two dimensions. One step is: boundary values, advection (MacCormack with a limiter, so a coarse grid keeps its eddies), viscosity, optional vorticity confinement, and a pressure projection (conjugate gradient with a modified incomplete Cholesky preconditioner, a fixed iteration cap, residual test on the data only). Everything is a function of the data, never of the clock, so two peers with the same history have the same field.
Units are SI: cell size h in m, velocity m/s, density kg/m3, pressure Pa (gauge: zero at the outflow). u lives on the vertical faces
((nx+1) x ny), v on the horizontal faces (nx x (ny+1)), pressure and the solid flags in the cells.
packages/flow-solver/src/grid.ts:10