Water flow in a reef tank, modeled before the tank is wet. Two wavemakers, a return nozzle, a sand bed and a rock structure, run through FluidX3D (a GPU lattice-Boltzmann solver) and drawn as nine cuts through the water: looking down at three heights, through the front glass at three depths, from the side at three stations. Plus a tool for shaping the rock in MS Paint and seeing what the shape does to the flow.
Built for one 90 gallon rimless tank (35.5 × 23.5 in inside, 22.1 in of water, 2.5 in of sand). The tank numbers sit in
one block at the top of src/cfd_tank.py; the holes in tank/holes.csv. Changing them to your tank is a short job for
you or your AI; see AGENTS.md.
This is a hobby project, shared because it was fun and might save someone the same 10-15 hours. It runs; it has not been checked against a real tank. Treat the numbers as "which layout is better", not "what speed is this coral seeing".
Open tank, pumps only, 20 s average, looking down 1 in over the sand:
The same pumps with the rock in. Side cut through the main rock, looking from the right pane. The solid tan is the rock the cut passes through; the pale wash is all the rock behind the glass, the far one outlined solid, the near one dotted:
The rock itself, shaped as 1 in square rods and rendered in Blender:
Each case is one folder in cases/: rock/ (the rock's views and renders), rock.stl, and flow/ (nine views plus
numbers.txt). 3_nsa_test is an open, holey scape copied by eye from a photo, run the same way for comparison: big
openings carry 2-3 in/s, about what that spot gets with no rock at all.
The averages above hide most of what the water does. src/flow_anim.py draws every 0.2 s of a run and joins the
frames: 20 s of tank time played at 2x speed, one 10 s video per view. The "starved" patches turn out to get swept every few
seconds; the front-glass jet split wanders. Videos are not in git; these five are hosted by GitHub.
Side cut through the main rock (far):
side3_far.mp4
Side cut through the overhang (middle):
side2_middle.mp4
Side cut through the right rock (near):
side1_near.mp4
Through the front glass, mid-depth:
front2_middle.mp4
Looking down, 1 in over the sand:
top3_low.mp4
- A GPU with OpenCL (any recent NVIDIA, AMD or Intel). A 90 s run on a 4 mm grid is about 3 minutes on a mid-range card.
- FluidX3D, cloned beside this folder (or anywhere, with
FLUIDX3Dset to its path), and whatever builds it on your machine (Visual Studio Build Tools on Windows;makeelsewhere). - Python 3.12 with
pip install -r requirements.txt. - Blender 4 or 5 for the rock renders (
BLENDERenv var, or on PATH). - Windows was the build machine. Nothing is Windows-only on purpose except the default Blender path and the Arial font the rod tool draws with.
python src/cfd_tank.py prepare # points FluidX3D's setup.cpp at src/cfd_tank.cpp, sets its defines
# build FluidX3D (Release, x64)
python src/cfd_tank.py run --case open_tank --t 90 --pumps "3:forward,32.5:left" --rfg-tilt 20
python src/cfd_tank.py views --case open_tank --out cases/1_open_tank/flow --title "Open tank"
python src/rock_rods.py draw 2_two_bommies # the rock's views to paint on, and the Blender corners
python src/rock_rods.py stl 2_two_bommies
python src/cfd_tank.py run --case rock --t 60 --warm open_tank --pumps "3:forward,32.5:left" --rfg-tilt 20 --stl cases/2_two_bommies/rock.stl
python src/cfd_tank.py views --case rock --out cases/2_two_bommies/flow --title "With the rock"
Raw fields land in FluidX3D/bin/reef_out/<case>/ (about 140 MB a run). --warm starts a run from another's end
instead of still water: half the time, same answer.
src/rock_rods.py treats the rock as a grid of 1 in square rods on the sand, each its own height. It draws a top view,
a front view and a side view, with a copy of each to paint on in MS Paint. Eyedrop a color off the drawing and paint:
green 3 in, yellow 5, orange 8, red 10, purple 13; white cuts a rod out; a color on open sand adds a rod. Then
python src/rock_rods.py update NAME reads the paint, redraws, and rerenders the Blender corners. Undercuts (ledges,
overhangs) are set in words: set NAME --x 14:18 --y 8:17 --bottom 3.5. A floating check refuses rock with nothing
holding it up. stl writes the shape for the flow run.
cases/2_two_bommies/rock/ is the shape in the pictures: heights.csv and bottoms.csv are the whole record. A new
case starts as a copy of an old case folder, renamed.
To move or turn a rock without touching it, edit the case's place.json:
{
"move_mm": [15, 0, 0],
"turn_deg": 10
}
move_mm is right, back, up; turn_deg turns it about its own center, counterclockwise seen from above. The
rock.stl files are millimetres around the middle of the tank, so any STL viewer shows them centered at true size.
- Color: 20 s mean speed, in inches per second, in five bands: starved (under 2), low (2-4), LPS (4-8), SPS (8-12), hot (over 12). Rules of thumb from reef-keeping, not measurements.
- Arrows: mean direction in the plane of the cut; motion through the page does not show. Length goes as the square root of speed so slow water still shows its way.
- Red arrow or ring: a pump and the way it points. Pale tan: rock. Deeper tan: the sand bed.
numbers.txt: tank mean, sand band, fastest water over the sand (sand starts to move at about 13 in/s), dead sand.
src/ cfd_tank.py (prepare, run, views), cfd_tank.cpp (the FluidX3D setup), rock_rods.py (+ rock_rods_blender.py),
flow_anim.py (videos), nsa_rock.py (builds case 3's rock), render_stl.py (renders any rock.stl in Blender)
tank/ holes.csv: return and drain positions
notes/ how the pump and nozzle geometry were measured off vendor pictures, with the scripts
cases/ one folder per scenario: 1_open_tank, 2_two_bommies, 3_nsa_test; each rock/ (rods, views, renders),
rock.stl (what the solver loads), flow/ (nine views, numbers.txt; videos ignored). Data only; code is in src/
MIT for everything here (see LICENSE). FluidX3D is not included and has its own license: no commercial use, no
military use, publish altered source with published results. src/cfd_tank.cpp and the defines prepare sets are the
altered source; that is why they are here.


