IBLBMFluidNode Specification#
Created 2026-03-24. Prerequisite for T3.C (FSI coupling) and T3.D (step-out demo).
1. Purpose#
IBLBMFluidNode wraps the existing LBM code (d3q19.py, bounce_back.py, rotating_body.py) as a proper MADDENING SimulationNode. This replaces the manual wiring in run_confinement_sweep.py with a node-graph approach where the LBM fluid solver is coupled to RigidBodyNode via edges.
2. MADDENING Architecture Compatibility#
Assessment (2026-03-24)#
No MADDENING extensions are required. The existing SimulationNode interface supports all IBLBMFluidNode requirements:
Requirement |
Interface feature |
Status |
|---|---|---|
1.3 GB f-array state |
|
Supported |
Variable solid mask each step |
Mask is a state array updated in |
Supported |
Quaternion boundary input |
|
Supported |
Circular dependency (LBM ↔ RigidBody) |
|
Supported |
Spatial stencil (streaming) |
|
Supported |
Non-scannable geometry |
|
Supported |
Force/torque output |
|
Supported |
Why run_scan() is not required#
The LBM geometry generation (create_umr_mask) uses JAX operations that ARE traceable, but the Python for-loop over 19 directions in compute_q_values_sdf_sparse gets unrolled during tracing, making compilation expensive. GraphManager.run() with its Python loop is the correct execution path — each step is JIT-compiled, but the loop is in Python. This is identical to the current run_confinement_sweep.py pattern and is performant (measured 0.04s/step LBM overhead at 192^3 on H100).
3. Node Interface#
@stability(StabilityLevel.EXPERIMENTAL)
class IBLBMFluidNode(MimeNode):
meta = NodeMeta(
algorithm_id="MIME-NODE-010",
algorithm_version="1.0.0",
stability=StabilityLevel.EXPERIMENTAL,
description="3D IB-LBM fluid solver with Bouzidi IBB for confined microrobot flows",
governing_equations="BGK-LBM D3Q19, Bouzidi interpolated bounce-back, momentum exchange",
discretization="D3Q19 lattice Boltzmann with BGK collision operator",
...
)
mime_meta = MimeNodeMeta(
role=NodeRole.ENVIRONMENT,
anatomical_regimes=(...), # iliac artery, CSF, etc.
)
3.1 Constructor parameters#
Parameter |
Type |
Description |
|---|---|---|
|
int |
Lattice dimensions |
|
float |
BGK relaxation time |
|
float |
Pipe wall radius in lattice units |
|
dict |
UMR geometry kwargs for |
|
bool |
Enable Bouzidi IBB for body surface |
|
float |
Physical lattice spacing [m] for unit conversion |
3.2 State#
def initial_state(self) -> dict:
return {
"f": init_equilibrium(nx, ny, nz), # (nx, ny, nz, 19) float32
"solid_mask": initial_solid_mask, # (nx, ny, nz) bool
"body_angle": jnp.array(0.0), # current rotation angle
"drag_force": jnp.zeros(3), # (3,) float32
"drag_torque": jnp.zeros(3), # (3,) float32
}
3.3 Boundary inputs#
def boundary_input_spec(self) -> dict[str, BoundaryInputSpec]:
return {
"body_angular_velocity": BoundaryInputSpec(
shape=(3,), description="Body angular velocity [rad/s] from RigidBodyNode",
),
"body_orientation": BoundaryInputSpec(
shape=(4,), description="Body orientation quaternion from RigidBodyNode",
),
}
3.4 Update#
def update(self, state, boundary_inputs, dt):
omega_z = boundary_inputs["body_angular_velocity"][2] # z-component
new_angle = state["body_angle"] + omega_z * dt
# Generate masks at new angle
solid_mask = create_umr_mask(..., rotation_angle=new_angle)
umr_missing = compute_missing_mask(solid_mask)
pipe_missing = compute_missing_mask(pipe_wall) # static, could be cached
# LBM step
f_pre, f_post, rho, u = lbm_step_split(state["f"], tau)
# Two-pass BB
f = apply_bounce_back(f_post, f_pre, pipe_missing, ...) # pipe wall
if use_bouzidi:
q_values = compute_q_values_sdf_sparse(umr_missing, sdf_func)
f = apply_bouzidi_bounce_back(f, f_pre, umr_missing, ..., q_values, ...)
else:
f = apply_bounce_back(f, f_pre, umr_missing, ...)
# Momentum exchange
force = compute_momentum_exchange_force(f_pre, f, umr_missing)
torque = compute_momentum_exchange_torque(f_pre, f, umr_missing, center)
return {
"f": f,
"solid_mask": solid_mask,
"body_angle": new_angle,
"drag_force": force,
"drag_torque": torque,
}
3.5 Boundary fluxes#
def compute_boundary_fluxes(self, state, boundary_inputs, dt):
return {
"drag_force": state["drag_force"], # → RigidBodyNode.drag_force
"drag_torque": state["drag_torque"], # → RigidBodyNode.drag_torque
}
3.6 Properties#
@property
def requires_halo(self) -> bool:
return True # LBM streaming accesses spatial neighbors
4. Bouzidi Q-Value Strategy#
4.1 Why per-step recomputation is required#
For a body rotating at ω = 0.001 rad/step with fin tip radius 29 lu (at 192³), the surface moves 0.029 lu per step. This exceeds the q-value precision needed for O(dx²) Bouzidi accuracy after a single step. Rotating or caching q-values introduces O(dx) errors that degrade Bouzidi to simple BB accuracy — defeating its purpose.
4.2 Sparse q-value computation#
compute_q_values_sdf_sparse reduces the per-step cost by evaluating the SDF bisection only at boundary nodes (~112K at 192³) instead of the full domain (7.1M nodes).
Implementation uses jnp.nonzero(mm_q, size=MAX_LINKS) to gather boundary node indices within JAX’s static-shape requirements. The boundary link count for a rigid body of fixed shape is approximately constant across rotation angles (~±10%), so a fixed size= parameter with 20% padding is safe.
Expected performance: ~0.1s per step at 192³ on H100 (vs ~6s for full-domain). Combined with 0.04s LBM step: ~0.14s total per step.
4.3 Bisection iteration count#
16 iterations give ~10⁻⁵ precision. For Bouzidi with O(dx²) accuracy at 192³ (dx ≈ 0.05 mm), 8 iterations (~10⁻² lu ≈ 0.5 μm) are sufficient. Reducing to 8 iterations halves the sparse q-value cost to ~0.05s per step.
5. Coupling Architecture#
ExternalMagneticFieldNode
↓ field_vector, field_gradient
PermanentMagnetResponseNode
↓ magnetic_torque
RigidBodyNode ←──────────────── IBLBMFluidNode
↓ angular_velocity, orientation ↑ drag_force, drag_torque
└──────────────────────────────────┘
Coupling mode: CouplingGroup with one-step lag (back-edge from IBLBMFluidNode → RigidBodyNode). This is standard IB-LBM practice — the drag from step t drives the orientation at step t+1.
For the confinement sweep (fixed omega, no FSI), the coupling simplifies: angular_velocity is constant, so there is no circular dependency. The IBLBMFluidNode runs standalone.
6. Effort Estimate#
Task |
Effort |
|---|---|
|
2 hours |
|
1 hour |
Wire boundary inputs/outputs + edges |
1 hour |
Verification: torque matches standalone script |
1 hour |
Replace |
2 hours |
Total |
7 hours |
7. Dependencies#
No MADDENING extensions required
Requires
compute_q_values_sdf_sparse(implemented as part of T2.6b fix)Existing nodes:
RigidBodyNode,PermanentMagnetResponseNode,ExternalMagneticFieldNode