Skip to content

Walls, Flow, and Microfluidics 馃К - #1

Closed
marpaia wants to merge 55 commits into
masterfrom
marpaia/microfluidics
Closed

Walls, Flow, and Microfluidics 馃К#1
marpaia wants to merge 55 commits into
masterfrom
marpaia/microfluidics

Conversation

@marpaia

@marpaia marpaia commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

marpaia added 30 commits August 16, 2026 19:52
marpaia added 25 commits August 18, 2026 20:49
Endpoint clamping worked in float position space while the sampling bound
is tested in double lattice coordinates, so on an origin and spacing with
no exact float representation a clamped endpoint could still fall outside
the lattice and raise. Sampling now takes an explicit bound: positions are
clamped where the coordinate is computed, so a clamped sample always lands
inside the lattice.

Building a stencil ran the full grid specification validation, which walks
every site once a grid carries an obstacle mask and every site's faces once
it carries a velocity field. Sampling now validates only the lattice - the
extents an index depends on - so the cost is constant per sample instead of
proportional to the grid. Drift over 200 cells on the biopixel grid falls
from tens of milliseconds to 0.03 ms.

Drift also takes the caller's mechanics integration parameters, so its
rotation limit is the configured one rather than a private constant, and
reuses the shared axis-angle rotation instead of repeating it. Direction
finiteness joins position in the pre-mutation check.
Every trap model advertised nutrient-limited growth while consuming
nothing: no model carried an uptake term, so the sampled field stayed at
its inlet value and the Monod law returned a constant. The models now
consume nutrient at a fixed yield tied to realized growth, so growth and
consumption stay consistent and a packed trap draws down the field it
feeds on.

Consumption alone was not enough. Crank-Nicolson judges convergence
against the absolute tolerance plus the relative tolerance times the
right-hand-side norm, and the right-hand side carries the background
concentration. A cell source far below that background leaves a residual
under the threshold, so the solve returned the old field and discarded the
source; the models' AHL secretion never reached the grid either. The grids
now judge convergence on an absolute residual sized above the float32
floor and below one step of uptake.

Device masks now voxelize conservatively: a site is solid only when its
whole voxel lies inside a wall, so the mechanics walls enclose the mask and
the voxel holding any reachable position stays fluid. Without that a cell
resting against the trap's back wall had an all-solid sampling stencil and
the trap models raised partway through a run. Wall tests carry a rounding
margin so a wall drawn on a lattice face resolves deterministically.

The biopixel lattice covers the whole channel height and the far channel
wall, with a one-voxel margin past the floor for the small penetration
contact relaxation allows, and resolves the cavity in two z layers so its
gap-height mobility matches the device's squared gap ratio.
The benchmark script and the test suite each carried their own copy of the
exact plane Poiseuille and two-layer Brinkman solutions, the duct builder,
and the cross-solver comparison. Both now read one shared module of
reference solutions with their citations, so the two cannot drift apart.

Three benchmarks measured less than they claimed. The square duct took the
largest cell-centered sample as the peak, and cell centers straddle the
axis, so about half the reported error was the sampling offset; the peak is
now interpolated to the centerline, which is exact for a parabola. The
two-layer Brinkman case rescaled the exact profile onto the computed one
and gained a refinement step, so its second-order convergence is now
measured rather than asserted. The cross-solver check ran at a tolerance
five times looser than the documented claim and is now held to one percent,
which its measured agreement clears comfortably.

`--fine` left three of six benchmarks byte-identical because they took no
resolution; every benchmark now refines.

The solvers gained the coverage they lacked: reversed flow, a non-y flow
axis, partly blocked inlets, fluid pockets sealed off from the flow, and
anisotropic spacing, all of which worked but were untested. A new test
pins the MAC solver's resolution limit, measuring how a one-voxel gap
over-predicts flux and how that converges as the gap resolves.
Several documented numbers did not match the code. The pillar channel's
far-field plug is the requested 20, not 22, and the explanation offered for
the difference was wrong: the solve normalizes over open inlet faces, so
blocked columns cannot inflate the speed. Its recommended time step
carried cells 1.5 cell radii per step, past the bound the same page states.
The Prindle columns are drawn in pairs rather than on the single 172.5
pitch quoted, which the mask test asserted alongside a span that
contradicts it. The trap heights were listed among the mask-derived
dimensions though they are model defaults, and one table had four rows
under a sentence promising three.

The tutorials now also state what each solver is good for. The MAC solve
reports the fluid voxels across its narrowest channel, and below about four
the depth-averaged closure is the better model of that channel; the closure
in turn solves for a column mean, so a cell near a floor drifts at the mean
rather than at its own streamline. Neither limit was written down.

A new units and timescales section states what a time unit is, that
transport is compressed relative to growth, by roughly how much, and which
two numerical limits force it: the explicit drift step and the Jacobi
transport solve. The Peclet ratio survives the compression and the
separation from growth does not, so trends are meaningful and absolute
timescales are scaled.
The two solvers carried near-identical conjugate gradient loops, the same
twenty-five lines of axis and boundary validation, and two copies of the
Kozeny-Carman drag expression. The Hele-Shaw module now owns one masked
conjugate gradient, one flow-axis validator, and one drag law, and the MAC
solver uses them. Its unread flow-axis attribute, its alias for the edge
slice, and its mask-shifting duplicate of the field shift are gone; one
generic shift serves both.

The device helpers grew a shared base that owns the projection onto engine
inputs, so the mask sweep, the boundary assignment, the flow solve, and the
wall-box construction exist once. The projection is a method rather than a
free function reaching into the device, so the module no longer suppresses
private-usage reporting.

The no-through-flow guards compared the solved inlet speed against a
quantity carrying the units of mobility and spacing, so a device authored
in millimeters or with heavy uniform drag could be rejected though it was
well posed. Both now compare the inlet against the largest speed the same
solve produced anywhere, which no rescaling changes.

The DXF reader assumed the group after a section marker was the section
name; it now checks the group code first and leaves the section unnamed
otherwise.
Planes, spheres, boxes, and cylinders each carried their own copy of the
same thirty-five line contact loop, differing only in the surface a point
sees. One templated pass now takes that surface - a signed distance and an
outward direction - along with the permitted region, and the four kinds
supply only their geometry. A plane is the outside of the half-space its
inward normal points into, which is why it fits the same shape. The
arithmetic is unchanged term for term, and the shared constraint
conformance scenarios confirm it.

Checkpoint validation likewise repeated one ordered-identifier walk per
kind; it now runs once over each list.
Dish founders were drawn with a uniform radius, which crowds them toward
the center of the seeded disc; the square root spreads them over its area.
The dish's caps leave a cell diameter of play rather than pinning a strict
monolayer, which the docstring now says.

The controller unpacks its mechanics parameters by name where it applies
drift, and the coupled-rate kernels record that the host's step validation
is what guarantees their stencils have a fluid corner to renormalize over.
The coupled step's obstacle and velocity-field buffers size themselves from
the grid specification, which was declared below them, so no CUDA build of
this branch compiled. The declaration moves above its first use.

`scripts/run_cuda_compile_check.sh` now passes and registers all 21 tests.
Hardware conformance still needs an NVIDIA host.
The grid scatter kernels asked the obstacle mask about every lattice site
for every cell before computing the trilinear weight that is zero for all
but the eight sites of that cell's stencil. Computing the weight first and
returning on zero cuts the mask reads by a factor of the site count, on
masked and unmasked grids alike, and removes the only path that could
divide zero by zero. The shared coupled-rate conformance scenario, which
scatters five hundred cells across a masked grid, confirms the result is
unchanged.

Implicit models also gained a long-run test. A Crank-Nicolson step can stop
converging once a field has grown, because the residual floor rises with
its magnitude, so the one-step tutorial smoke test cannot see it. Every
model that selects Crank-Nicolson now advances two hundred steps and every
step must converge and commit.
An implicit step was accepted when its residual fell below a threshold
scaled by the field it started from. A field's magnitude says nothing about
how much of it one step has to change, so the threshold grew with the
background: a cell's exchange with a well-stocked field left a residual
under it, the solve returned the old field, and the source was discarded.
That silently zeroed every cell-scattered source in the device models -
nutrient uptake and AHL secretion alike - and would do the same to any
model whose chemistry is small next to its concentration scale.

The relative term now scales the residual the step begins with, which all
three backends already computed before their first iteration. The accuracy
a model receives is the accuracy it asked for, at any concentration scale.

The absolute term is raised to the field's float32 residual floor. A
tolerance below the floor cannot be met, and asking for one used to iterate
to the limit and fail the step outright - one legacy model does exactly
that once its field has grown. The right-hand side carries the step's
operator terms as well as the field, so the floor tracks stiffness too.
That floor is also the honest limit of an implicit step: a source too small
to move the field within its own float32 resolution converges without
being seen, which a test now pins.

The four device models drop the local tolerances they carried to work
around this and use the engine's defaults. Their uptake reaches the field
more completely than the workaround managed, and the result no longer
moves when the tolerance is tightened by two orders of magnitude.
The signal and coupled-rate kernels carried the same lattice geometry
helpers and the same forty-six line face-closure block, which decides
whether each of a site's six faces is closed by a boundary, a periodic wrap
onto solid, or a solid neighbour, and which velocity it carries.

A Metal library is compiled from its source text at runtime, with no
include path, so the fragment cannot be included the way the CUDA kernels
now include theirs. The build embeds it instead: the embedding step takes a
list of sources and concatenates them into one library, with the shared
fragment first, and rejects a source carrying the delimiter that would
break the embedding.

Both kernels shrink by around eighty lines each. Every embedded library
still compiles at backend construction and the shared transport and
coupled-rate conformance scenarios still match the CPU reference.
Contact relaxation can press a cell of a packed trap into a wall, and the
Danino trap does exactly that after a few thousand steps. Drift then
sampled a stencil with no fluid in it and raised, ending the run.

The velocity field is validated zero on every face of a solid site, so zero
is the field's own value there, not a fallback: a cell inside a wall does
not drift. The stencil now reports that it holds no fluid and each caller
decides what that means - concentration has no value inside an obstacle and
sampling one stays an error, which is what the coupled step enforces before
it mutates anything.
The tree carries a .clang-format that nothing enforced, so its sources had
drifted from it. Applying it is mechanical: the native suite and the shared
conformance scenarios pass unchanged.

This is what makes the new hook configuration usable - a first run over an
unformatted tree would otherwise fail every commit.
The circuit could not fire. LuxI and AiiA are driven by the same activation
term, so their ratio, and with it the AHL where production balances
removal, is pinned by their decay constants - at the published constants,
half the Hill threshold. No cell density and no run length could cross it,
so the model sat at its basal state forever and the tutorial's headline
claim was false.

Three constants now carry the clock, each set against something
measurable. The threshold sits inside the window where the response is
steep enough to oscillate. One shared rate scale places the period relative
to a doubling, which is the only timescale the model has. AHL's diffusivity
reaches the order the trap's width and the clock's period demand, since a
patch stays in phase with its neighbours only within about the square root
of their product - below that a trap oscillates in independent patches
rather than as one quorum.

AiiA's removal of AHL moves out of the cells and into the field. It is a
loss proportional to the AHL already there, so transport can take it into
the implicit diagonal, where a step stays positive while the loss times the
step is under two; scattered from the cells it is explicit and needs half
that step, which a synchronized pulse in a packed trap exhausted. The
engine grew the mutation this needs, alongside the one that swaps a
velocity field.

The trap lattice resolves the device rather than approximating it: two z
layers span the six-micrometer depth exactly, where a coarser lattice left
half a voxel of slack on each side and doubled the fluid volume every
concentration is divided by. It reaches a voxel past the walls, which
contact relaxation needs, and the trap's walls now stand proud of the
chamber - level with it, a cell pressed against the ceiling had a
zero-length escape in z that the chamber blocked, and crowding drove it
ever deeper into the wall.

A run at seed 42 measures the result: quiet through growth, first pulse at
about four thousand cells, then a pulse every 1.03 time units - 1.5
doubling times - with the front and back of the trap correlated 0.87 at
zero lag.
Rasterizing AiiA into the grid's loss walked every site in Python on every
step, which cost more than the transport solve it fed. The field follows the
clock, so it is rebuilt with array operations a few dozen times a period
rather than every step; a step of the Danino model drops by a fifth at
small cell counts, where that work was most of it.
@marpaia

marpaia commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by the reviewable stacked drafts in #2, #3, #4, #5, and #6. The final stack tip is tree-identical to this branch, while the large device source files are isolated in #5.

@marpaia marpaia closed this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant