Skip to content

Fluid Simulation

NovaPhy 0.4.0 has three fluid paths:

Solver Method Default Optional Rigid coupling
SolverPBF Position Based Fluids CPU Akinci boundary particles
SolverSPH Smoothed Particle Hydrodynamics CUDA Boundary particles
SolverLBM Sparse-block Lattice Boltzmann Method CUDA Immersed boundary

SolverMPM remains an API scaffold in the current release. It exposes solver metadata for parity work, but its step() method raises and it must not be selected for a simulation.

Position Based Fluids

The PBF path follows the usual predict/project/update loop:

  1. Apply gravity and predict particle positions.
  2. Find neighbors with the spatial hash grid.
  3. Project density constraints iteratively.
  4. Apply XSPH viscosity, vorticity confinement, and tensile correction.
  5. Update final positions and velocities.

Create a fluid block

import numpy as np
import novaphy

builder = novaphy.ModelBuilder()
builder.add_ground_plane(y=0.0)

block = novaphy.FluidBlockDef()
block.lower = np.array([0.0, 0.1, 0.0], dtype=np.float32)
block.upper = np.array([1.0, 1.1, 1.0], dtype=np.float32)
block.particle_spacing = 0.05

positions = novaphy.generate_fluid_block(block)
mass = block.rest_density * block.particle_spacing**3
radius = 0.5 * block.particle_spacing

builder.add_particles(
    positions,
    [block.initial_velocity] * len(positions),
    [mass] * len(positions),
    [radius] * len(positions),
)
model = builder.finalize()

Configure and step PBF

config = novaphy.solvers.SolverPBF.Config()
config.kernel_radius = 4.0 * block.particle_spacing
config.solver_iterations = 4

solver = novaphy.solvers.SolverPBF(model, config)
state = model.state()
solver.initialize_state(state)

pipeline = novaphy.CollisionPipeline(model)
contacts = pipeline.contacts()

for _ in range(500):
    state.clear_forces()
    pipeline.collide(state, contacts)
    solver.step(state, state, None, contacts, 1.0 / 120.0)

control=None is valid here because the PBF-only loop has no rigid joint control to apply.

Rigid-fluid coupling

NovaPhy uses Akinci-style sampled boundary particles for two-way PBF/rigid interaction:

  • sampled rigid surfaces contribute to fluid density;
  • particle/boundary contact forces are accumulated through the shared Contacts aggregate;
  • the rigid solver consumes the resulting reaction forces.

There is no hidden fluid world. Compose the stages explicitly:

pbf = novaphy.solvers.SolverPBF(model, config)
rigid = novaphy.solvers.SolverSemiImplicit(model)

state = model.state()
pbf.initialize_state(state)
control = model.control()
pipeline = novaphy.CollisionPipeline(model)
contacts = pipeline.contacts()
dt = 1.0 / 120.0

for _ in range(500):
    state.clear_forces()
    pipeline.collide(state, contacts)
    pbf.step(state, state, None, contacts, dt)

    # Refresh rigid contacts after the fluid position update.
    pipeline.collide(state, contacts)
    rigid.step(state, state, control, contacts, dt)

The explicit chain makes the coupling order, profiling boundary, and contact refresh visible to the application.

SPH

SolverSPH is CUDA-only in the current implementation. The public class and configuration types are present in a standard build, but a non-empty step requires NOVAPHY_WITH_SPH_CUDA=ON. There is no CPU backend to fall back to. Check novaphy.has_sph_cuda() before constructing a runnable SPH workflow.

NovaPhy also exposes reusable SPH building blocks:

  • SPHKernels for Poly6, Spiky-gradient, and viscosity kernels;
  • SpatialHashGrid for neighbor lookup;
  • SPHState, ParticleState, and boundary-particle helpers;
  • sample_model_boundaries(...) and contact-reaction utilities.

Start with the runnable demo because it keeps particle sizing, stability parameters, boundary options, and backend selection together:

python python/demos/demo_sph_fluid.py --headless --steps 100

For CUDA builds:

if not novaphy.has_sph_cuda():
    raise RuntimeError("Rebuild with NOVAPHY_WITH_SPH_CUDA=ON")

Sparse-block LBM

SolverLBM owns its sparse-block fluid state internally while preserving the common SolverBase.step(state_in, state_out, control, contacts, dt) call shape. The class, SolverLBM.Config, and capability metadata are present in a standard build, but the numerical backend requires NOVAPHY_WITH_LBM_CUDA=ON. There is no top-level has_lbm_cuda() helper; construct the model-bound solver and inspect has_cuda_backend:

config = novaphy.solvers.SolverLBM.Config()
config.resolution = 0.01
config.enable_immersed_boundary_coupling = True

lbm = novaphy.solvers.SolverLBM(model, config)
if not lbm.has_cuda_backend:
    raise RuntimeError("Rebuild with NOVAPHY_WITH_LBM_CUDA=ON")

lbm.seed_box(
    np.array([-0.25, 0.0, -0.25], dtype=np.float32),
    np.array([0.25, 0.5, 0.25], dtype=np.float32),
)
lbm.step(state, state, None, None, 1.0 / 120.0)

The retained runtime supports dense-grid exports for visualization and optional immersed-boundary coupling to rigid geometry. It currently supports one model world per solver. The canonical examples are:

  • demo_lbm_volume.py for simple ViewerGL volume inspection and a finite ViewerNull lifecycle smoke;
  • demo_lbm_robot.py for an immersed-boundary FR3 arm;
  • demo_lbm_robot_gripper.py for the FR3 underwater payload transfer.

ViewerNull can exercise demo_lbm_volume.py without CUDA, but that mode skips fluid steps; all actual LBM simulation and both robot demos require the LBM CUDA build.

Scaffolded fluid API

The MPM name is visible under novaphy.solvers for API compatibility and capability discovery:

mpm = novaphy.solvers.SolverMPM(model)
assert novaphy.is_scaffold(mpm)
print(novaphy.scaffold_reason(mpm))

Do not describe SolverMPM as an available fluid backend in 0.4.0.

Demos

Demo Solver / behavior Requirements
demo_dam_break.py PBF block collapse CPU
demo_fluid_coupling.py Multiple rigid shapes coupled to PBF CPU
demo_sph_fluid.py SPH smoke, boundaries, and timing SPH CUDA build
demo_lbm_volume.py Sparse-block speed volume LBM CUDA for fluid steps; ViewerNull lifecycle without CUDA
demo_lbm_robot.py Immersed-boundary FR3 arm LBM CUDA + viewer
demo_lbm_robot_gripper.py FR3 underwater payload transfer LBM CUDA + viewer

Run paths from the repository root, for example:

python python/demos/demo_fluid_coupling.py
python python/demos/demo_sph_fluid.py --headless --steps 200 --boundary
python python/demos/demo_lbm_volume.py --viewer null --num-frames 2

# Interactive; both require NOVAPHY_WITH_LBM_CUDA=ON.
python python/demos/demo_lbm_robot.py
python python/demos/demo_lbm_robot_gripper.py

# Finite offscreen ViewerGL verification.
python python/demos/demo_lbm_robot.py --headless 2
python python/demos/demo_lbm_robot_gripper.py --headless 2

See Demos for viewer/headless conventions and Build from Source for backend flags.