Skip to content

Architecture

NovaPhy separates immutable scene topology from caller-owned runtime data and solver scratch. This is the same solver-primary shape used by Newton:

ModelBuilder (mutable) → Model (immutable) → Solver (step)
                                      ↘ State / Control / Contacts

There is no World orchestrator. The application decides when to collide, step, profile, render, record, reset, or capture a CUDA graph.

Core pipeline

flowchart LR
    B[ModelBuilder<br/>mutable authoring] --> M[Model<br/>immutable topology]
    M --> SI[SimState in]
    M --> SO[SimState out]
    M --> C[Control]
    M --> P[CollisionPipeline]
    P --> K[Contacts]
    M --> S[Solver]
    SI --> P
    SI --> S
    SO --> S
    C --> S
    K --> S
    S --> R[Next state]
  1. ModelBuilder collects bodies, shapes, joints, articulations, particles, materials, sites, and imported scene data.
  2. Model is baked by builder.finalize(). Topology is immutable and may be shared across independent rollouts.
  3. SimState, Control, and Contacts hold runtime data. The caller owns them.
  4. CollisionPipeline performs broadphase/narrowphase and writes into a pipeline-sized Contacts aggregate.
  5. Solver is bound to a model and advances through the five-argument contract.
import novaphy

model = builder.finalize()
solver = novaphy.solvers.SolverSemiImplicit(model)

state_in = model.state()
state_out = model.state()
control = model.control()
pipeline = novaphy.CollisionPipeline(model)
contacts = pipeline.contacts()

state_in.clear_forces()
pipeline.collide(state_in, contacts)
solver.step(state_in, state_out, control, contacts, dt)

state_in and state_out may alias for in-place stepping. Use distinct buffers for checkpointing, observation-before-action pipelines, or state comparisons.

Ownership and lifetime

Object Mutable? Typical lifetime Share across rollouts?
ModelBuilder Yes Scene authoring only No
Model No after finalize() Application / task lifetime Yes
Solver Internal caches mutate One per execution lane or policy Usually no
SimState Yes Rollout / checkpoint lifetime No
Control Yes Rollout lifetime No
CollisionPipeline Internal caches mutate Rollout or worker lifetime Usually no
Contacts Yes Reused each collision step No

Solver scratch follows the Solver Internal Data Pipeline conventions: long-lived allocations are solver-owned, capacity growth happens outside hot loops where possible, and CPU/CUDA storage has the same logical shape.

Solver capability matrix

All solver classes live under novaphy.solvers. Runnable SolverBase implementations accept step(state_in, state_out, control, contacts, dt).

Solver Domain CPU Optional accelerator Status
SolverSemiImplicit Free rigid bodies, PGS contact Runnable
SolverFeatherstone Articulated dynamics CUDA Runnable
SolverXPBD Maximal-coordinate constraints Runnable
SolverMuJoCo Native MuJoCo-style robot dynamics CUDA Runnable
SolverVBD Rigid / soft VBD and AVBD CUDA or DLAN Runnable
SolverPBF Position Based Fluids Runnable
SolverSPH Smoothed Particle Hydrodynamics CUDA Optional build
SolverLBM Sparse-block Lattice Boltzmann fluids CUDA Optional build
SolverIPC IPC contact through libuipc NVIDIA CUDA or CoreX Optional build
SolverMPM Material Point Method parity surface Scaffold

The SolverMPM scaffold exposes metadata for API parity but its step() method raises. Check it with novaphy.is_scaffold(...); do not select it for a simulation.

Every solver also exposes capability metadata:

  • notify_model_changed(SolverNotifyFlags) invalidates supported caches.
  • joint_support() returns a JointSupportMatrix.
  • Python's read-only backend_info property describes device, graph-capture, fixed-timestep, and scaffold properties. The corresponding C++ interface is the backend_info() method.

Choosing a rigid/articulated path

  • Start with SolverSemiImplicit for free rigid bodies and simple contact.
  • Use SolverFeatherstone for generalized-coordinate articulated systems, analytical dynamics evaluators, or its optional CUDA backend.
  • Use SolverXPBD for maximal-coordinate constraint behavior.
  • Use SolverMuJoCo for its native MuJoCo-style constraint/integrator path, robot grids, and optional CUDA batching. See MuJoCo-style Solver.
  • Use SolverVBD for VBD / AVBD rigid or deformable workflows.
  • Use optional SolverIPC when IPC's build and hardware requirements match the application.

Fluid and coupled paths

SolverPBF and SolverSPH use external SimState particle buffers. Two-way PBF/rigid coupling is deliberately a user-level chain:

collide → SolverPBF.step → collide → SolverSemiImplicit.step

The shared Contacts aggregate carries coupling data. See Fluid Simulation and Migration: fluid coupling.

SolverLBM follows the same five-argument step signature but owns its sparse-block fluid state internally. Its numerical path requires NOVAPHY_WITH_LBM_CUDA=ON; construct the solver and inspect solver.has_cuda_backend before seeding or stepping it.

Collision system

Collision is explicit and model-bound for most solvers:

pipeline = novaphy.CollisionPipeline(model)
contacts = pipeline.contacts()
pipeline.collide(state, contacts)

Do this before a solver step whenever NovaPhy contact constraints should be enforced. For those solvers, contacts=None is valid for a deliberately contact-free articulated step, such as a procedural cart-pole rollout. SolverIPC is the exception: libuipc performs collision internally and ignores the NovaPhy Contacts argument.

Broadphase

The public pipeline supports:

  • Explicit pairs precomputed from model filtering data.
  • SAP (sweep and prune) over shape AABBs.
  • NXN / all-pairs checks for small scenes and tests.

Narrowphase

Specialized primitive pairs cover spheres, planes, boxes, capsules, cylinders, heightfields, meshes, and the general contact core used by current shape paths. Consult the Geometry API rather than assuming every shape pair has identical manifold behavior.

Contact convention

  • Rigid contact normals point from body_a to body_b. A positive normal impulse separates the pair.
  • Plane shapes are world-owned and use body_index = -1.

Multi-environment layer

ModelBuilder.replicate(...) appends independent world slices to one model. MultiEnvRunner is a convenience layer over a solver-bound model:

flowchart LR
    E[Source ModelBuilder] --> R[replicate N worlds]
    R --> M[One immutable Model]
    M --> S[One Solver]
    S --> X[MultiEnvRunner]
    X --> A[Per-environment actions / resets / done flags]

It owns convenient state/control/contact buffers and episode bookkeeping, but it is not a replacement World object and does not change the canonical solver API. See Multi-environment Simulation.

Driver-layer services

Services that used to be tempting to hide inside a world object remain explicit:

  • Profiling: wrap only the desired work in with monitor.scoped(): ....
  • Viewer: call begin_frame, log_state, and end_frame through a novaphy.viewer backend.
  • Picking: viewer input writes forces into the caller-owned state/control path.
  • CUDA graph capture: warm and capture at the driver layer after model, state, contacts, and solver scratch are allocated.
  • Fluid/rigid coupling: compose solver steps in the application loop.

This makes timing and ownership visible to RL runners, robotics applications, and testing harnesses.

Data conventions

NovaPhy engine numerics are float32. Use np.float32 for numeric arrays crossing the Python boundary.

Surface Layout
Internal novaphy::SpatialVector [linear; angular]
Python-facing SimState.body_qd [linear; angular]
Python-facing SimState.body_f [force; torque]
Free-joint joint_qd [linear; angular]
Free-joint Control.joint_f [force; torque]

Free-joint coordinates are [px, py, pz, qx, qy, qz, qw]; their six velocity entries are [vx, vy, vz, wx, wy, wz].

File organization

NovaPhy/
├── novaphy/
│   ├── include/
│   │   ├── collision/
│   │   ├── core/
│   │   ├── dynamics/
│   │   │   ├── featherstone/
│   │   │   ├── ipc/
│   │   │   ├── mujoco/
│   │   │   ├── semi_implicit/
│   │   │   ├── vbd/
│   │   │   └── xpbd/
│   │   ├── fluid/
│   │   ├── io/
│   │   ├── math/
│   │   └── sim/
│   ├── src/                 # Mirrors include/
│   └── tests/               # C++ tests
├── python/
│   ├── bindings/            # pybind11 bindings
│   ├── demos/               # Runnable examples and assets
│   ├── novaphy/
│   │   ├── ik/
│   │   ├── solvers/
│   │   └── viewer/
│   └── tests/               # pytest suite
├── docs/                    # MkDocs source
└── cmake/cmake_project/     # CMake integration examples