Upgrade guide: world.step → solver.step¶
NovaPhy 0.4.0 realigned the stepping API with Newton's
solver.step(state_in, state_out, control, contacts, dt) contract — see
newton/_src/solvers/solver.py.
If you are coming from a 0.3.x codebase that constructed novaphy.World,
this page lists the call-site changes you need to make.
Why¶
Newton's primary forward-dynamics entry is:
class SolverBase:
def __init__(self, model: Model):
self.model = model
def step(self, state_in, state_out, control, contacts, dt) -> None:
...
There is no World concept — solvers consume Model + State +
Control + Contacts directly. NovaPhy historically funneled stepping
through World.step(dt), which:
- hid the canonical Newton entry behind a wrapper;
- held its own
gravity_field that could drift fromModel.gravity; - rolled multi-solver fluid + rigid orchestration into one opaque call.
The 0.4.0 refactor removed World and made solver.step the canonical
forward-dynamics entry.
Recommended pattern¶
import novaphy
builder = novaphy.ModelBuilder()
# ... add bodies, shapes, joints, fluid blocks ...
model = builder.finalize()
config = novaphy.solvers.SolverSemiImplicit.Config()
solver = novaphy.solvers.SolverSemiImplicit(model, config)
state = model.state()
control = model.control()
collision_pipeline = novaphy.CollisionPipeline(model)
contacts = collision_pipeline.contacts()
dt = 1.0 / 240.0
for _ in range(steps):
solver.step(state, state, control, contacts, dt)
state_in and state_out may alias (in-place stepping). Pass two
distinct SimState objects when the input buffer must be preserved:
state_a = model.state()
state_b = model.state()
for _ in range(steps):
solver.step(state_a, state_b, control, contacts, dt)
state_a, state_b = state_b, state_a # swap
Call-site mapping¶
| Before | After |
|---|---|
world = novaphy.World(model) |
config = novaphy.solvers.SolverSemiImplicit.Config()solver = novaphy.solvers.SolverSemiImplicit(model, config) |
world = novaphy.World(model, multibody_settings=mbs) |
config = novaphy.solvers.SolverFeatherstone.Config()copy mbs fields onto config, then solver = novaphy.solvers.SolverFeatherstone(model, config) |
world = novaphy.World.make_xpbd(model) |
config = novaphy.solvers.SolverXPBD.Config()solver = novaphy.solvers.SolverXPBD(model, config) |
world.step(dt) |
solver.step(state, state, control, contacts, dt) |
world.step(world.state, world.state, None, None, dt) |
solver.step(state, state, None, contacts, dt) |
world.state |
the SimState you passed in |
world.gravity |
model.gravity |
world.set_gravity(g) |
model.gravity = g then solver.notify_model_changed(SolverNotifyFlags.ModelProperties) |
world.apply_force(i, f) |
state.apply_force(i, f) |
world.contacts |
the Contacts you passed in (after step) |
fluid_world.step(dt) |
explicit pbf_solver.step(...) + solver.step(...) chain (see below) |
world.performance_monitor |
monitor = novaphy.PerformanceMonitor() then with monitor.scoped(): solver.step(...) |
Solver constructors¶
NovaPhy solver constructors use a C++-backed nested Config class. The
shape is the same for every solver; each solver's config carries its own
attribute set.
config = novaphy.solvers.SolverSemiImplicit.Config()
solver = novaphy.solvers.SolverSemiImplicit(model, config) # semi-implicit Euler + PGS
config = novaphy.solvers.SolverXPBD.Config()
config.iterations = 20
solver = novaphy.solvers.SolverXPBD(model, config) # XPBD maximal-coordinate
config = novaphy.solvers.SolverFeatherstone.Config()
config.angular_damping = 0.05
solver = novaphy.solvers.SolverFeatherstone(model, config) # ABA + PGS articulated
config = novaphy.solvers.SolverPBF.Config()
solver = novaphy.solvers.SolverPBF(model, config) # then initialize_state
config = novaphy.solvers.SolverSPH.Config()
solver = novaphy.solvers.SolverSPH(model, config) # reads/writes state.particle_*
config = novaphy.solvers.SolverIPC.Config()
solver = novaphy.solvers.SolverIPC(model, config) # CUDA, libuipc-backed
The config-object construction path is preferred for solver-specific option bundles. The config classes are pybind-exposed C++ structs, so each solver can carry its own attribute set without introducing Python dataclasses::
pbf_options = novaphy.solvers.SolverPBF.Config()
pbf_options.solver_iterations = 8
pbf = novaphy.solvers.SolverPBF(model, pbf_options)
sph_options = novaphy.solvers.SolverSPH.Config()
sph_options.substeps = 4
sph = novaphy.solvers.SolverSPH(model, sph_options)
mpm_options = novaphy.solvers.SolverMPM.Config()
mpm = novaphy.solvers.SolverMPM(model, mpm_options)
Legacy top-level config names remain compatibility aliases:
PBFConfig is SolverPBF.Config, SPHConfig is SolverSPH.Config, and
IPCConfig is SolverIPC.Config when IPC is built.
Power-user post-construction tuning remains available for rigid solvers::
config = novaphy.solvers.SolverSemiImplicit.Config()
config.velocity_iterations = 30
config.warm_starting = True
config.sleep_enabled = True
solver = novaphy.solvers.SolverSemiImplicit(model, config)
config = novaphy.solvers.SolverXPBD.Config()
config.iterations = 20
config.velocity_damping = 0.99
solver = novaphy.solvers.SolverXPBD(model, config)
config = novaphy.solvers.SolverFeatherstone.Config()
config.pgs_iterations = 50
config.pgs_slop = 0.001
solver = novaphy.solvers.SolverFeatherstone(model, config)
solver.model returns the bound model (Newton-aligned attribute).
Common pitfalls¶
- Aliased state. Passing the same
SimStatefor bothstate_inandstate_outis the in-place case; pass distinct buffers if you need the input preserved. control=Noneuses the defaults baked into theModel(matching Newton). Pass an explicitmodel.control()if you want to feed injoint_f,joint_target_pos, orjoint_target_vel. Body forces and torques live onSimState.body_f/state.apply_force(...)/state.apply_torque(...).contacts=Nonemeans the solver runs without contact constraints. PassCollisionPipeline.contacts()and callcollision_pipeline.collide(state, contacts)beforesolver.step(...)when you want rigid contacts.- Gravity changes — mutate
model.gravity(or per-world viamodel.gravity_per_world[i] = g) and then callsolver.notify_model_changed(novaphy.solvers.SolverNotifyFlags.ModelProperties). - No top-level shortcut. Mirroring Newton 1:1, NovaPhy does not
expose
novaphy.SolverXXXat the top level — every solver, configuration object and notify-flag enum lives undernovaphy.solvers.*only. Comparenewton/__init__.py: it exportsModel/ModelBuilder/State/Control/Contacts/eval_fketc. but noSolverXXX;newton.solvers.SolverXXX(model)is the single entry.
Runtimeshims removed. Earlier preview builds shippednovaphy.SolverRuntime/novaphy.FeatherstoneRuntime/XPBDSolverRuntimeas demo wrappers that bundled the quintet behind a singleruntime.step(dt=...)call. They have been deleted to match Newton's surface 1:1 — every demo, test, and production caller now constructs(model, solver, state, control, contacts)directly and drivessolver.step(state_in, state_out, control, contacts, dt).
Fluid + rigid coupling¶
The unified PBF + Akinci coupling + rigid orchestration that previously
lived inside World::step is now an explicit user-level chain:
pbf_config = novaphy.solvers.SolverPBF.Config()
rigid_config = novaphy.solvers.SolverSemiImplicit.Config()
pbf = novaphy.solvers.SolverPBF(model, pbf_config)
solver = novaphy.solvers.SolverSemiImplicit(model, rigid_config)
state = model.state()
collision_pipeline = novaphy.CollisionPipeline(model)
contacts = collision_pipeline.contacts()
pbf.initialize_state(state)
for _ in range(steps):
collision_pipeline.collide(state, contacts)
pbf.step(state, state, None, contacts, dt)
collision_pipeline.collide(state, contacts)
solver.step(state, state, None, contacts, dt)
collision_pipeline.collide writes both rigid_contact_* and
soft_contact_* channels. SolverPBF consumes the soft-point channel, and
the rigid solver consumes the rigid contact rows, so the two passes compose
through the same aggregate.
Profiling¶
World.performance_monitor is replaced by a standalone monitor driven
externally with a thread-local context manager (Newton's event_scope
pattern):
monitor = novaphy.PerformanceMonitor()
monitor.enabled = True
monitor.trace_enabled = True
for _ in range(steps):
with monitor.scoped():
solver.step(state, state, control, contacts, dt)
for stat in monitor.phase_stats():
print(stat.name, stat.avg_ms)
monitor.write_trace_json("trace.json")
Outside the with block every C++ phase scope is a zero-cost no-op.