build_hierarchy(cfg, [], wall_aware=True) left both base wall rows ACTIVE;
the refined path already marked them OUTER. Add the regression test.
Found by native review of eeb90ac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lbm/hierarchy.py loops the existing pair logic (FixedPatch2D/FixedPatchSet2D
for L0->L1, nest_patch/nest_patch_set for deeper pairs, _middle_config for
the per-level config chain) and returns validated per-level configs, pair
tables and composed kinds for any depth. Legacy scalar window forms are
accepted so existing public shapes are unchanged. Validates each config copy
because LBMConfig.omega is only computed by validate(). Not yet used by the
stepper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prepare_fine_boundary accepts a FixedPatchSet2D: objects are projected once
onto the level-wide fine grid (FixedPatchSet2D.fine_origin = (0, 0), same
coarse cell-edge -> fine cell transform), each circle must lie inside one
patch, coarse transfer cells are checked against the covered union, and body
ids stay object positions, so the existing jet one-body-per-jet, overlap and
(receiver, dir) writer checks run once over the whole level (caller obj_id
cannot bypass them). The stepper no longer rejects fine_objects/jets with
several patches; blocked link tables, BFL and the jet amplitude cache are
reused unchanged. No kernel change. README/docstring scope updated.
Tests: CPU translation/ids/rejections (outside one patch, one jet over two
bodies at zero amplitude); GPU one body via the new path == single-patch path
(both modes), two bodies (+jets, amplitude switch) == tiled single (bitwise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FixedPatchStepper(..., child=(x0,y0,x1,y1), child_block=(by,bx)) adds an L2
patch in L1 window cells (requires fine_block/base_block, one L1 patch, no
bodies/jets). nest_patch() builds the L1->L2 FixedPatch2D (2*nu on L1) and
the composed L1 kind (inherited INNER/OUTER kept, child-covered ACTIVE made
OUTER), rejecting from the actual tables: coverage outside ACTIVE, donors or
receivers not uncovered ACTIVE, surviving ACTIVE pulls of covered/OUTER cells
without a child-supplied arrival, and overlap with L1's restriction footprint.
Step order per L1 hop: explode L1(src)->L2, two L2 hops (halo, step), L1 halo,
receiver-local L2->L1 staging into src, L1 step. The second hop thus feeds L2
from the new L1 G; L2 is never refilled from L0; L0 stays once per step (1:2:4).
Existing kernels only; a small _Pair holds the child transfer bindings (explode
from the child module, parent window stride). Two-level API unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FixedPatchSet2D.fine_layout rejects non-positive, float, bool or wrong-length
block sizes with ValueError before any modulo (was ZeroDivisionError/TypeError).
The face gap-4 GPU test now checks raw owner cores directly by kind/covered
for every patch (ACTIVE finite, OUTER/holes/covered NaN; INNER not asserted),
not only the masked download and reversed-order parity. README documents the
body-free multipatch call, scope and level-wide fine shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One rectangle via the multipatch path is bit-identical to the old
single-patch blocked path (both modes, raw masks); S1 two-patch 16x32 equals
the 16x16 one-patch run tiled (bitwise), in both rectangle orders; face
gap-4 pair is order-invariant with exact covered/hole masks. Mass printed
as a diagnostic only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several separated (>= 4 coarse cells) body-free 2:1 patches share one
level-wide fine grid stored by a sparse BlockLayout2D over the patch
supports. FixedPatchSet2D re-addresses each FixedPatch2D's tables to level
coordinates (same rows/child order, canonical patch order). The stepper
accepts a sequence of rectangles as bounds (requires fine_block and
base_block; rejects fine_objects/jets), reusing the existing blocked step
sequence and kernels. Helpers use a covered mask instead of one rectangle
and skip unstored fine holes. No kernel changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c397e08 moved the 64-bit capacity check after np.arange(nby*nbx), so an
over-limit regular layout allocated before rejecting. Check arithmetic
block count (regular) or validated selected count (sparse) first; mock
regression asserts no np.arange before the ValueError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holes are filled with NaN, which bool/str silently mangled and integers
rejected with a conversion error. Sparse layouts now raise a clear
ValueError for non-floating arrays; full layouts keep any dtype.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An int64 C-contiguous origins array was kept by reference (and frozen in
the caller's hands), so mutating it later changed layout.origins and
block_cell without changing owner/halos. Copy before marking read-only;
regression checks origins, owner and block_cell invariance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture listed (fluid_idx=13, dir=3) twice with different q, so the
existing repeated-link rejection fired before the sparse checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build(..., origins=None|[(y0, x0), ...]) stores only grid-aligned selected
blocks (unique, strictly row-major, no silent reordering). Cells of absent
blocks own layout.hole (= nblocks*L, one past the last block cell) so an
unguarded index fails rather than wrapping like -1; layout.owned is the
dense mask. Halo pairs never read holes, to_blocked ignores hole values,
to_dense returns NaN there, map_cut_links rejects receivers, solid
neighbors or q<.5 donors in absent blocks, blocked_link_tables stores
owned flags only. origins=None or every origin reproduces the regular
layout exactly. Storage only: no CUDA or stepper changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Caller obj_id 99 spanning 2x2 fine blocks: driven rows carry the single
remapped owner 0 across several block ids, accepted; source id unchanged.
Caller ids (99, 7) under one tube are rejected at amplitude 0 and .002
on the 32x24 fixture; source ids unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synthetic rows: one jet over non-contiguous rows of one body is accepted;
the same rows with a second body id are rejected (Grok case 2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
surface_link_velocity (the shared host check used by CurvedLinkSoA,
ObjectManager sync, prepare_fine_boundary and FixedPatchStepper) now
rejects a jet whose selected cut links carry more than one body id, at
any amplitude, before uploads. Identity is the scene-wide body id of the
physical cut-link list, so one body spanning storage blocks stays valid.
No body selector; empty-selection semantics unchanged.
Tests: two bodies under one jet rejected (amplitude 2e-3 and 0) on the
Simulation/manager path and on the fine path; separate jets on separate
bodies accepted on both; one fine body crossing 2x2 block seams accepted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
19bd308 computed slot = 3*self.cfg.dim unconditionally, so a default
manager without cfg raised even when empty. Use the existing fallback
(cfg.dim if cfg else 2). Regression for empty and one-body managers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test-only. Rotated 30-degree tube: lateral .999 in, 1.001 out, along -0.1
and 4.1 out, Parallel(30 deg) gives the axis unit vector. Separate
axis-aligned exact-edge check: |s| = 1 excluded, Tube along 0/length and
Sector r_min/r_max included. Synthetic hit sampling on a flat face x=10
(no non-circle geometry or kernel coverage): spatial selection |y| <= 1
and off-centre radial vectors from (5, 0) rather than the face normal.
Re-written from Grok's stated cases; the original text was clipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ObjectManager._upload_action_async is the single entry for every action
path (setters, direct edits of the public action array, run()). It now
also rejects nonzero vx/vy/omega slots of Jet-driven bodies via
validate_jet_motion, before any device copy. Regression: each motion
slot edited directly is rejected with no device write; zero motion
uploads. Simulation.set_jet_amplitude documents that a device error
requires close/recreate, as for other CUDA exceptions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FixedPatchStepper.set_jet_amplitude validates index/amplitude and builds
the new rows before touching the device; invalid input leaves the
instance usable. A copy/sync error marks it failed (as initialize/step/
download do) and re-raises; host state (_jets, blocked velocity table)
commits only after a successful upload. CurvedLinkSoA.set_jet_amplitude
likewise uploads a new array first and commits jets/surface_velocity
afterwards. Focused mocked-driver tests for dense and blocked copy
failure, invalid input and IndexError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Taken from Grok's inline patch (Slack ts 1792052057.423679); the selector
fix in that patch was already applied in b1cd555. The rotated-tube and
non-circle-wall cases were clipped in transit and are not included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FixedPatchStepper(..., jets=()) passes jets (coarse cell-edge frame, as
fine_objects) to prepare_fine_boundary, which runs the shared assignment
checks before CUDA. Dense fine path: assign_surface with jets and the
boundary's true hits. Blocked path: surface_link_velocity with jets/hits;
unit-amplitude shapes cached. set_jet_amplitude (new/ready only) validates
first, then rewrites the dense SoA column or the blocked jet rows and
re-uploads only the velocity buffer after a stream sync. Scheduling,
kernels and launch ABI unchanged.
tests/validation/multigrid/test_refinement_jets.py: mocked-driver CPU
checks and frozen GPU gates G1-G3 (layout identity, jet vs legacy <=1e-6,
amplitude switch).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move surface_link_velocity (pure host code) from lbm/curved_links.py to
body/jets.py, re-exported by curved_links, so refinement.py can reuse it
without importing PyCUDA. prepare_fine_boundary now runs the same
assignment validation as the steppers when jets are given, rejecting
jet-vs-SurfaceActuation overlap (root's C(12.25,12.25) R2.15 case) as
well as empty/overlapping jets. Test the overlap and a disjoint pair.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the Jet motion check into ObjectManager.validate_jet_motion (uses the
synced field's cut-link jet_id) and call it from both Simulation.set_body
and the direct ObjectManager.set_body_state entry, before any state or
action change. Test both entries, vx too, and that action is untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sector/Tube/Radial store origin as a validated float 2-tuple, so caller
lists/arrays cannot change a registered definition. Simulation.set_body
rejects nonzero motion for bodies with Jet-driven links once initialized
(before initialize the sync-time stationary check applies).
prepare_fine_boundary accepts jets in the same coarse cell-edge frame as
fine objects, records read-only FP64 edge-frame hits (FineBoundary2D
.curved_hits, default None) and rejects empty or overlapping selections
before CUDA. FixedPatchStepper wiring is not included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace fine_hits_to_physical with fine_hits_to_edge_frame (patch
cell-edge frame, coarse centers at I+1/2, as FixedPatchStepper uses) and
edge_to_node_frame (Simulation integer-node frame, shift -1/2). Test one
physical circle through both frames: fine hits converted to node frame
lie on the Simulation circle and one node-frame Jet selects the same arc
on coarse and fine links; relabelling without conversion is detected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jet is a frozen dataclass; with_amplitude returns a validated copy, so
region/field/profile cannot drift from the cached per-link shapes and
amplitude cannot bypass validation. CurvedLinkSoA.set_jet_amplitude and
Simulation.set_jet_amplitude replace the Jet (Simulation also updates the
registered list so later geometry syncs keep the amplitude). Reject
non-finite hits; document edge rule (|s|<1 exclusive, radial/along
bounds inclusive). Fix the clipped-q test selector to receiver (9,5)
dir 8, as in test_surface_actuation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ObjectManager.jets holds scene-level Jets. build_compact_lists_for
collects true FP64 hits (SimObject.get_curved_list hits_out) row-aligned
with the cut-link columns; BodySyncPlan carries them as curved_hits, and
sync_to_gpu / apply_sync_plan / the pre-sync check pass jets+hits to
assign_surface. Jets require D2Q9/FP32/unshifted/double_buffer and may
only drive links of stationary bodies. Simulation.add_jet (before
initialize) and set_jet_amplitude (velocity column re-upload only);
save_checkpoint refuses scenes with jets. Fine-path wiring not included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jet(region, field, profile, amplitude) with Sector/Tube regions,
Radial/Parallel fields and uniform/parabolic/quartic profiles. Selection
uses true FP64 hit points over all scene cut links, no body association.
Overlap between jets or with a SurfaceActuation aperture is rejected.
CurvedLinkSoA caches unit-amplitude shapes; set_jet_amplitude rewrites
and re-uploads only the surface-velocity column. SurfaceActuation.as_jet
is the legacy adapter (equivalence measured in tests). Kernel and SoA
geometry columns unchanged. Manager/Simulation/fine-path wiring pending.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The blocked actuated path called the helper with an unvalidated fine
config (nq=0), so receiver*nq+dir collapsed distinct writers and the
unique-writer guard fired. Validate after wall-aware sanitization, as the
dense path does before assign_surface; the test's fine_cfg helper too.
Unique-writer guard unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asserts 21 aperture links, all with nonzero velocity, and 3 halo-writer
plus 2 halo-donor aperture rows in each decomposed layout (actuation
coverage across seams, not only geometric cut-link coverage). Velocity
is given in coarse units; prepare_fine_boundary scales it once. The GPU
gate remains storage equivalence; no zero-mass-change claim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- curved_links.surface_link_velocity: pure host helper extracted from
CurvedLinkSoA.assign_surface (which now calls it; dense path unchanged).
Rows follow physical cut-link order.
- blocks.cu BlockCurvedBoundary: one nullable trailing surface_velocity
argument (2 floats/link), same convention as CurvedBoundaryKernel; no
exchange output. Both call sites pass it explicitly (PeriodicBlockStepper
passes null; single-level behaviour unchanged).
- FixedPatchStepper blocked mode accepts actuated stationary circles,
validated against the physical fine_cfg; uploads the velocity table only
when any actuation spec exists and fails before BFL if that buffer is
missing (even at zero amplitude). Blocked exchange diagnostics are
unavailable; dense exchange behaviour unchanged. No force aggregation,
no motion, no new numerical formula.
Tests: 1x1 seam (0,0), receivers in 4 blocks, all q<.5 links fallback 0;
dense SoA never built in blocked mode; launch order incl. null vs velocity
pointer; missing-buffer guard at 0 and 0.002; helper == assign_surface;
aperture crosses a seam; GPU: periodic and slot_reservoir, +/-0.002,
8 coarse steps, dense vs 1x1/2x2/4x2 exact values + NaN masks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FixedPatchStepper(fine_block=..., fine_objects=[circle]) now accepts
passive stationary circles; any surface actuation is rejected before CUDA
(dense fine path unchanged). prepare_fine_boundary still decides legality
on the dense fine grid; its SoA is mapped by the new array-based storage
adapter blocks.blocked_link_tables (map_cut_links: unique receiver owner
and local writer, row order kept; blocked flags sized nblocks*L), which
PeriodicBlockStepper._static_circles now also uses (single-level legality
builder unchanged). Per fine hop on the same buffer: halo -> existing
BlockCurvedBoundary -> BlockStep(flags, kind). No CUDA changes.
Tests: fixture 76 links / 68 q<.5 and 8 seam writes + 8 seam donors per
decomposed layout (both modes); adapter flags sizing; mocked launch-order
contract; GPU: periodic and slot_reservoir, 8 coarse steps, dense fine
body vs 1x1/2x2/4x2 fine blocks, exact values + NaN masks on masked
downloads and raw final arrays, ACTIVE fluid finite, solid/OUTER NaN.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GPU test: after hop 2 some INNER_GHOST populations are NaN (pulled from
OUTER poisoned on hop 1; 236 slots in pinned CPUREF); assert ACTIVE
finite and OUTER NaN, keep the full dense-vs-blocked raw value/NaN-mask
comparison.
- A 1x1 non-periodic fine layout has zero halo pairs: skip the halo table
uploads (CUDA rejects mem_alloc(0)); _fine_halo already skips launch.
CPU guard test with driver doubles.
- Drop the class-level fine_layout default; the followup fake backend now
sets fine_layout = None explicitly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fine_block=(by, bx) partitions the ENTIRE fine_shape (incl. the four-cell
patch ghost band) with a non-periodic one-cell-halo BlockLayout2D; body-
free only (fine_objects rejected in blocked mode; dense fine-body and
actuation path unchanged). Per coarse step on the same stream:
BlockExplode into ghost owner cores -> gather -> BlockStep(kind) ->
gather -> BlockStep(kind) -> unchanged PatchCoarseStep reading remapped
owner-core child slots in the original row/child order. Public arrays
keep the dense fine shape; only the private allocation is blocked.
blocks.cu: explicit BlockExplode; BlockStep gains nullable kind
(OUTER skip pull + NaN, INNER pull only, ACTIVE pull + SRT).
PeriodicBlockStepper passes an explicit null kind (raw launch ABI).
refinement.cu/refinement.py and coarse numerics unchanged.
Tests: CPU rejections and table remap (dense (q,cell) and child order
preserved, owner cores, kinds); GPU gate: periodic and slot_reservoir,
8 coarse steps, dense vs 1x1/2x2/4x2 fine blocks bit-identical in
masked downloads and raw coarse/unpacked fine incl. ghost NaN masks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At the half-integer center the -0.1/-0.5 circles were already rejected
as unresolved, so the case did not exercise the finite/positive-radius
guard; an integer center makes the squared radius mark one solid cell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_static_circles now checks finite center/radius and radius > 0 before
build_flag_mask (a negative radius squared to a one-cell solid), matching
prepare_fine_boundary. objects is converted to a tuple before the
emptiness test so an empty iterable takes the body-free path.
CPU tests: negative/NaN radius and nonfinite center rejected before CUDA;
empty iterator skips circle preparation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
blocks.cu: BlockCurvedBoundary passes the receiver-local core cell and the
block-offset fi to the unchanged apply_bouzidi_link (Uw=Vw=0, obs=nullptr),
so k_s and the q<1/2 donor follow the window image the next pull reads.
BlockStep takes nullable block-cell flags; solid cores store NaN each step.
Adds core/flags.cuh, core/obs.cuh and boundary/curved_boundary.cuh.
block_stepper.py: objects=() accepts stationary passive circles only; host
preparation rejects before CUDA: non-circle/moving/actuated/sensor roles,
overlap, domain-edge contact, and any fluid core (all rows/columns, periodic
neighbors) whose pull from a solid lacks exactly one cut link. Uses
map_cut_links for unique receiver owner/local writer. Order per step:
halo copy -> BFL -> pull/collide, no copy in between. Fluid-only
initialize validation; solids NaN.
Tests: CPU rejections/tables; GPU 32x24, nu .1, 8 steps, circle
(15.5,11.5) R4.3 with halo-write and halo-donor presence asserted,
1x1/2x2/4x2 bit-identical, finite fluid/NaN solid, FP64 BFL+pull/SRT
<= 3e-6 on fluid; drift reported. Body-free gate unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
close() now clears _module (initialized to None) inside the active
context together with the function references; lifecycle test asserts
it is retained on failed activation and cleared after close.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
step() reads _dst/_src before the launch; without them the failed-launch
test raised AttributeError instead of the intended RuntimeError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>