# Liquids

# Liquids

Package `@ferret/physics-liquids`. Capabilities: `liquids` (reservoirs and pipes), `surface` (a water surface), `fluid` (particles). Foils and the Magnus force are plain functions.

## Which tool

"Water" in a game means very different things, and the cheapest model that is right for the job should win.

| You need | Use | What it is |
|---|---|---|
| A tank that fills and drains, plumbing, a pump, a valve puzzle | **Reservoirs** (`liquids`) | containers and links: levels from volumes, flows from pressure differences |
| A lake, a harbour or a river with waves, things that float, a current | **Surface** (`surface`) | a shallow-water height field on a grid |
| A splash, a pour, a dam break, a wave over a deck | **Particles** (`fluid`) | particle liquid with a budget |
| Lift and drag of a wing, keel, sail or rudder; a ball that curves | **Foils** | `foilForce`, `thinFoil`, `magnus` |

All of them share the idea of a *liquid material*: `EXAMPLE_LIQUIDS` has water, oil, honey, mercury and alcohol, each with density, viscosity, surface tension and a contact angle per solid. You can add your own.

## Reservoirs and pipes

A `Reservoir` is a container with a volume of liquid. The container can be a box, a cylinder, a sphere, or a profile made from a list of cross-sections (a funnel, a bottle, a ship's hold). The level follows the volume, whatever the shape.

A **link** joins two reservoirs, or a reservoir and the air. It can be a pipe, an orifice, a valve, a pump or a check valve. The flow comes from the pressure difference, laminar or turbulent as the pipe demands. What you get without extra work: levels in two connected tanks equalise with the right time constant, a tank with a hole drains the way Torricelli says it should, and two liquids in a U-tube balance in proportion to their densities.

```ts
import { LiquidNetwork, LiquidLibrary, EXAMPLE_LIQUIDS, boxContainer } from '@ferret/physics-liquids';

const net = new LiquidNetwork(new LiquidLibrary(EXAMPLE_LIQUIDS), 9.80665);
const low = net.addReservoir({ id: 'low', liquid: 'water', container: boxContainer(1, 1, 5), level: 2 });
net.addReservoir({ id: 'high', liquid: 'water', container: boxContainer(1, 1, 5), level: 0.5, position: [0, 3, 0] });
const up = net.addLink({ id: 'up', from: 'low', to: 'high', fromElevation: 0.1, toElevation: 3.1,
                         diameter: 0.05, length: 4, check: true });
up.def.pump = { pressure: 80000 };         // 80 kPa lifts water about 8 m
net.step(0.1);
```

Link settings you will reach for: `diameter`, `length`, `roughness` and `loss` for the plumbing; `valve` from 0 (shut) to 1; `pump` as a pressure or a rate; `check: true` for a one-way valve; `orifice` for a hole; `main` for a supply at fixed pressure; `profile` for a pipe whose bore changes (a Venturi shows the pressure drop where it narrows).

A closed tank holds a cushion of gas that compresses as it fills. A body in a tank raises the level by the volume it displaces. When media (skill `ferret`) is on, the reservoir is also a medium, so wood floats at the ratio of densities and drags like in a lake.

More that comes with it: the force on a vertical gate (`gateForce`, and where it acts), capillary rise in a thin tube (water climbs, mercury is pushed down), a wick that soaks a fabric, and the pressure inside a droplet.

In a world, the capability is `liquids()`. Its commands (`reservoir.add`, `link.add`, `link.valve`, `reservoir.state`, `link.state`, `gate.force`, ...) are enough for an agent to build a plumbing puzzle without writing code.

## A lake, a harbour, a river

`WaterSurface` is a height field on a grid. Waves travel at a speed set by the local depth, so they slow down on a shallow beach, bounce off walls and banks, and fade at the open edges where the surface is allowed to leak out. A steady `current` carries the waves and everything floating on them.

It is deliberately not a flow solver around objects. Bodies push the surface (`disturb`) and read it (`height`, `velocity`).

```ts
import { WaterSurface } from '@ferret/physics-liquids';

const s = new WaterSurface({ nx: 400, nz: 4, dx: 0.5, depth: 2 });
s.step(0.05);
```

In a world, `waterSurface()` owns several surfaces:

```ts
cap.add('river', { nx: 128, nz: 128, dx: 0.5, depth: 2, current: [1, 0], open: ['minX', 'maxX'], spongeCells: 8 });
cap.float({ body, radius: 0.4, push: true });   // the body floats and makes rings
```

A floating body settles at its draft (the depth at which it displaces its own weight) and is carried along by the current. `depth` can be a number or a function of `(x, z)`, so a beach is a depth that reaches zero. `friction` damps waves; `gravity` can be changed. `surface.normals` hands normals to the renderer for shading (see fx (skill `ferret-physics-fx-tuning`)). A 128 × 128 surface fits in the frame.

## Particles

`particleLiquid()` (capability `fluid`) steps particle liquids with the world. A body joins as a moving solid and is pushed back, so a ball floats and a paddle splashes.

A reservoir stays the truth about how much liquid there is. A `pour` takes its volume from a reservoir as particles; a `drain` puts what arrives back. Nothing is made or lost on the way.

```ts
const cap = particleLiquid();
host.register(cap);
host.enable('fluid');

const a = new Reservoir({ id: 'a', liquid: 'water', container: boxContainer(1, 1, 1), volume: 0.01 });
const b = new Reservoir({ id: 'b', liquid: 'water', container: boxContainer(1, 1, 1), volume: 0 });
const f = cap.add('jet', { spacing: 0.01 });                         // 1 cm particles
f.colliders.push({ sdf: planeSdf([0, 1, 0], 0), friction: 0 });      // a floor
cap.pour('jet', { from: a, at: [0, 0.3, 0], velocity: [1, 0, 0], radius: 0.02, rate: 2e-4 });
cap.drain('jet', { into: b, sdf: (x) => 0.3 - x });                  // everything past x = 0.3 goes down the drain
```

The liquid behaves like the real one: a still column presses on the floor with its weight, a dam break runs out at the right speed and never faster than physics allows, honey flows slower than water, and a droplet on a wall holds the contact angle the wall asks for.

**The budget is the main control.** `budget` (6000 by default) is the most particles stepped at full quality per liquid; above it the solver halves its work and says so. The quality tier gives a smaller number (`fluidParticles`: 400 desktop, 200 console, 120 mobile, 60 low), enough for a splash, not a lake. `spacing` sets the particle size: about 2 cm for a splash, 1 cm for detail. `cap.view(name)` returns positions and speeds for a renderer to draw (the `fx` package does).

## Foils and spin

`foilForce(def, velocity, flow, rho)` gives the lift and drag on a wing, sail, keel, fin or rudder. Lift is across the flow and the span, drag along the flow. The shape is described by a **polar**: lift and drag as functions of the angle of attack. `thinFoil({ aspect, Cd0, stall })` builds a plausible one for a flat plate: lift grows with the angle until the stall, drag has a base part and a part that grows with lift.

```ts
const polar = thinFoil({ aspect: 6, Cd0: 0.01, stall: 0.3 });
const { force } = foilForce({ area: 2, polar, chord, span }, [50, 0, 0], [0, 0, 0], 1.2);
```

This is the effect of a wing as a game uses it, not a solved flow around the shape.

`magnus(v, ω, radius, ρ)` is the sideways force on a spinning ball. It acts across the spin and the velocity, grows with the spin and saturates, which is how a football bends and a tennis ball dips.

## Parameters

| Capability | Value | Default | Effect |
|---|---|---|---|
| `liquids` | `liquids` | the examples | add or override liquid materials |
| `fluid` | `budget` | 6000 | particles at full quality per liquid |
| `surface` | surface definition | none | grid `nx, nz, dx`, `level`, `depth`, `current`, `friction`, `open`, `spongeCells`, `gravity` |

## Works with

media (skill `ferret`): a reservoir can serve as a medium for floating and drag. vehicles (skill `ferret-physics-vehicles`): a vessel reads the water level from its scoped values, and a wave field can be its `surface` function. fx (skill `ferret-physics-fx-tuning`): draws the surface mesh and the particle views.

## Cost

Reservoirs are almost free: hundreds are fine. A surface costs one update per cell: 128 × 128 fits in the step; reduce the grid before reducing the step rate. Particles are the expensive one: stay inside the tier's number and let the solver halve itself if it must. When the step runs over budget, the governor switches `fluid` off first, then `surface`.

## When it does not behave

| Symptom | Cause |
|---|---|
| A tank never empties | the orifice is tiny for the head; check units (metres, not centimetres) |
| Water runs uphill | a pump on the link, or wrong elevations: `fromElevation` and `toElevation` are the world heights of each pipe end |
| `reservoir "x" not found` | link ends name reservoirs by id; `'air'` is the only other possible end |
| Waves blow up | the step is over the stability limit; use a smaller step or a larger `dx` |
| Waves never fade | all edges are walls; name the `open` edges or add `friction` |
| Particle liquid looks heavy and slow | over `budget`: the solver halved its work; lower the count or raise the spacing |
| A mesh collider is refused by particles | bodies join by shape; a hull stands in as its bounding box, meshes are refused |

Worked numbers (draining time against Torricelli, pumps and check valves, wave speed, foil lift, Magnus) are in `tests/unit/examples/physics-liquids.test.ts`.

## Reference

`liquids`, `LiquidNetwork`, `Reservoir`, `boxContainer`, `profileContainer`, `EXAMPLE_LIQUIDS`, `WaterSurface`, `waterSurface`, `particleLiquid`, `foilForce`, `thinFoil`, `magnus`.

## Hydraulics (PLAN-DYNAMICS.md stage A11)

`HydraulicCircuit(liquid, { bulk? })` in `@ferret/physics-liquids` is a network of **compressible** nodes (bulk modulus `K`, so a closed line is an oil spring, and a locked cylinder holds). All pressures are found each sub-step by one fully implicit Newton solve (trust region, backtracking, step halving as the fallback), so pumping a stiff chamber conserves the liquid. `circuit.liquidVolume()` shows it.

| part | call |
|---|---|
| tank (atmosphere), line | `tank`, `line(id, volume, pressure?)` |
| pump (constant flow) | `pump(id, node, rate)` |
| orifice, tap, check valve, relief | `link(id, { from, to, cdA | laminar, open?, check?, relief? })`, `relief(id, line, { crack, cdA })`, `setOpen(id, x)` |
| accumulator (gas law, `precharge`, `gas`, `gamma`) | `accumulator(id, …)`, `stored(id)` |
| cylinder (`bore`, `rod`, `stroke`, `mass`, `coulomb`, `viscous`, `leak`, end stops) | `cylinder(id, def)`; nodes `id.cap`, `id.rod` |
| hand pump (lever 0..1, `handleForce` out) | `handPump(id, { bore, stroke, to })` |
| 4/3 directional valve (`spool` -1..1) | `directional(id, { supply, a, b, cdA })` |

A jack from two cylinders is Pascal's law with the stroke volumes (tested: hand force = load · a/A within 12 %). A closed valve locks the cylinder; a relief valve limits the pressure at the end stop.

**In a level:** `physics.hydraulics` is a list of circuits (`CircuitDef`: `lines`, `accumulators`, `pumps`, `cylinders`, `links`, `valves`, `handPumps`, `reliefs`). A cylinder with `attach: { a, b, axis }` has no mass of its own: it reads the extension and speed of body `b` relative to `a` along `axis` and pushes both equally and oppositely. Mistakes (an unknown node, a rod thicker than the bore) are refused at load, naming the node.

**Commands:** `hydraulic.state`, `hydraulic.spool {circuit, valve, value}`, `hydraulic.open {circuit, link, value}`, `hydraulic.lever {circuit, pump, value}`, `edit.physics.hydraulics.set/remove`.

**In the language:** `hydraulicForms(bridge, cap)` gives `(spool "circuit" "valve" :extend|:hold|:retract|x)`, `(valve "circuit" "link" x)`, `(cylinder-position "circuit" "cyl")`, `(cylinder-force "circuit" "cyl")`.
