Walls, Flow, and Microfluidics 馃К - #1
Closed
marpaia wants to merge 55 commits into
Closed
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.