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:
- Apply gravity and predict particle positions.
- Find neighbors with the spatial hash grid.
- Project density constraints iteratively.
- Apply XSPH viscosity, vorticity confinement, and tensile correction.
- 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
Contactsaggregate; - 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:
SPHKernelsfor Poly6, Spiky-gradient, and viscosity kernels;SpatialHashGridfor 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:
For CUDA builds:
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.pyfor simple ViewerGL volume inspection and a finite ViewerNull lifecycle smoke;demo_lbm_robot.pyfor an immersed-boundary FR3 arm;demo_lbm_robot_gripper.pyfor 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.