Robotics and Inverse Kinematics¶
NovaPhy's robotics path uses the same immutable-model and caller-owned-buffer contract as the rest of the engine:
Inverse kinematics is a CPU/NumPy utility that produces generalized coordinates. Forward dynamics remains an explicit solver step; IK does not apply motor targets or advance simulation by itself.
Import a robot¶
For the usual URDF workflow, append directly to a builder:
import novaphy
builder = novaphy.ModelBuilder(up_axis=novaphy.Axis.Z)
builder.add_urdf(
"robot.urdf",
floating=False,
enable_self_collisions=False,
)
model = builder.finalize()
Use floating=True for a floating base. A fixed-base serial manipulator is
the simplest supported IK input because its revolute/prismatic joints have
joint_coord_count == joint_dof_count.
The other current import entry points are:
| Asset | Entry point | Result |
|---|---|---|
| URDF | ModelBuilder.add_urdf(path, ...) |
Appends directly to a mutable builder. |
| MJCF | ModelBuilder.add_mjcf(path) or add_mjcf_string(xml) |
Appends MuJoCo XML bodies and joints. |
| Parsed URDF | UrdfParser + SceneBuilderEngine.build_from_urdf(...) |
Finalized model plus scene metadata. |
| PhysX USDA articulation | ModelBuilder.add_usd(path, ...) |
Appends bodies, joints, and collision shapes directly to a mutable builder. |
| USDA text snapshot | OpenUsdImporter + SceneBuilderEngine.build_from_openusd(...) |
Parses a smaller line-oriented subset into a finalized model; current metadata remains empty. |
Use the parsed scene-builder path when link/joint/shape metadata and asset auditing are part of the application. See the I/O API for format-specific coverage and limitations.
Configure and send controls¶
Joint drive mode, gains, and default targets belong to the builder/model.
Configure them before finalize():
import numpy as np
n = len(builder.joint_target_pos)
builder.joint_target_mode[:n] = [novaphy.JointTargetMode.POSITION] * n
builder.joint_target_pos[:n] = np.zeros(n, dtype=np.float32)
builder.joint_target_ke[:n] = np.full(n, 100.0, dtype=np.float32)
builder.joint_target_kd[:n] = np.full(n, 10.0, dtype=np.float32)
model = builder.finalize()
control = model.control() # initialized from the Model defaults
Runtime commands live in Control:
control.joint_target_pos— position target per joint DOF.control.joint_target_vel— velocity target per joint DOF.control.joint_f— feed-forward force/torque per joint DOF.control.joint_act— optional solver-specific activation input.
The buffers are device-aware arrays. Replace their contents with assign()
rather than assuming a NumPy slice is a writable view:
control.joint_target_pos.assign(q_target)
control.joint_target_vel.fill_zero()
control.joint_f.assign(feed_forward_effort)
novaphy.actuators.ControllerPD and Actuator provide a small
application-side PD layer that accumulates effort into control.joint_f.
They are not a replacement for the model's built-in drive modes. See the
actuator API.
Then drive the selected solver with the canonical five-argument call:
solver = novaphy.solvers.SolverFeatherstone(model)
state_in = model.state()
state_out = model.state()
collision_pipeline = novaphy.CollisionPipeline(model)
contacts = collision_pipeline.contacts()
collision_pipeline.collide(state_in, contacts)
solver.step(state_in, state_out, control, contacts, 1.0 / 120.0)
SolverMuJoCo is the native MuJoCo-style articulated/contact solver;
SolverFeatherstone is the articulated-body path. Both remain explicit
solver objects—there is no World orchestrator. See
Articulated Bodies and SolverMuJoCo.
Class-style IK API¶
The current public names match the Newton-style class surface:
| Symbol | Purpose |
|---|---|
IKSolver |
Sampling, optimization, best-seed selection, and cost reporting. |
IKObjective |
Base class for composable residual blocks. |
IKObjectivePosition |
Three-row end-effector position objective. |
IKObjectiveRotation |
Three-row quaternion/axis-angle rotation objective. |
IKObjectiveJointLimit |
Soft per-coordinate joint-limit penalty. |
IKOptimizer |
Enum: LM or LBFGS. |
IKOptimizerLM |
Levenberg-Marquardt implementation. |
IKOptimizerLBFGS |
L-BFGS implementation with Wolfe line search. |
IKJacobianType |
Enum: ANALYTIC or FINITE_DIFF. |
IKSampler |
Enum: NONE, GAUSS, UNIFORM, or ROBERTS. |
The objective class names are IKObjectivePosition,
IKObjectiveRotation, and IKObjectiveJointLimit—not the older planned
IKPositionObjective, IKRotationObjective, or
IKJointLimitObjective names.
Position IK from a finalized model¶
This example assumes a fixed-base model containing only fixed, revolute, and prismatic joints:
import numpy as np
from novaphy import ik
target = np.array([[0.45, 0.10, 0.35]], dtype=np.float32)
position_objective = ik.IKObjectivePosition(
link_index=end_effector_body,
link_offset=np.zeros(3, dtype=np.float32),
target_positions=target,
)
limit_objective = ik.IKObjectiveJointLimit(
model.joint_limit_lower,
model.joint_limit_upper,
weight=0.1,
)
ik_solver = ik.IKSolver(
model=model,
n_problems=1,
objectives=[position_objective, limit_objective],
optimizer=ik.IKOptimizer.LM,
jacobian_mode=ik.IKJacobianType.FINITE_DIFF,
)
q_in = np.asarray(model.joint_q, dtype=np.float32).reshape(1, -1).copy()
q_out = np.empty_like(q_in)
ik_solver.step(q_in, q_out, iterations=50)
control.joint_target_pos.assign(q_out[0])
IKSolver.step() writes into q_out and returns None. The input/output
shape is (n_problems, model.joint_coord_count). The arrays may alias for
in-place tracking:
position_objective.set_target_position(0, next_target)
ik_solver.step(q_in, q_in, iterations=20)
cost = float(ik_solver.costs[0])
The class solver runs a fixed number of iterations; it has no tolerance-based
early exit or convergence Boolean. Evaluate the task-space error or
ik_solver.costs in the application when a success decision is required.
Pose targets and multiple seeds¶
Rotation targets and offsets use [x, y, z, w] quaternions:
rotation_objective = ik.IKObjectiveRotation(
link_index=end_effector_body,
link_offset_rotation=np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float32),
target_rotations=np.array([[0.0, 0.0, 0.0, 1.0]], dtype=np.float32),
canonicalize_quat_err=True,
weight=0.5,
)
ik_solver = ik.IKSolver(
model=model,
n_problems=1,
objectives=[position_objective, rotation_objective, limit_objective],
optimizer=ik.IKOptimizer.LM,
jacobian_mode=ik.IKJacobianType.FINITE_DIFF,
sampler=ik.IKSampler.GAUSS,
n_seeds=8,
noise_std=0.15,
rng_seed=12345,
)
n_seeds expands each problem internally and gathers the lowest-cost row.
IKSampler.NONE requires n_seeds == 1. GAUSS keeps the first input seed;
UNIFORM and ROBERTS sample bounded coordinates from the model limits.
Jacobian and model limitations¶
The following boundaries are part of the current implementation:
- NovaPhy has no autodiff IK backend.
IKJacobianTypecontains onlyANALYTICandFINITE_DIFF; there is noAUTODIFForMIXEDmember. - IK is CPU/NumPy today.
n_problems > 1and seed expansion are processed serially, not as a CUDA batch. - The optimizer currently adds an
n_dofsupdate directly to ann_qcoordinate vector. In practice, high-level IK requiresjoint_coord_count == joint_dof_count. Ball,Free, and D6 joints need a quaternion/manifold retraction and are not supported by the optimizer. Do not include free-jointed props in an IK model. Build a robot-only kinematic model and keep the full scene for simulation, aspython/demos/mujoco/demo_robot_panda_hydro.pydoes.- A raw finalized
Modeldoes not retain the PythonJointdescriptors needed by the hand-written spatial Jacobian. Asking forANALYTICwith a raw model currently falls back to finite differences. - The true analytic path is available with the Python-only
JointBodyBundle, and only Revolute/Prismatic columns are implemented. Ball/Free/D6 columns remain zero in both bundle Jacobian modes. IKObjectiveJointLimitis a soft penalty. The class solver does not hard clamp every accepted optimizer step.
These constraints are why the raw-model examples select FINITE_DIFF
explicitly, even though IKSolver defaults to ANALYTIC.
JointBodyBundle and analytic IK¶
JointBodyBundle(joints, bodies) is a backwards-compatible Python bridge
for list-based robot descriptions. It builds and caches a temporary
NovaPhy Model, while preserving the Python joint axis/parent metadata used
by the analytic Jacobian:
bundle = ik.JointBodyBundle(joints, bodies)
ik_solver = ik.IKSolver(
model=bundle,
n_problems=1,
objectives=[position_objective],
jacobian_mode=ik.IKJacobianType.ANALYTIC,
)
Use it only when joints and bodies contain one parent-before-child entry
per link and the moving joints are Revolute or Prismatic. It is not a C++
articulation container, a GPU batch representation, or a way to make
multi-DOF quaternion joints optimizer-compatible.
Legacy function API¶
The earlier functions remain exported directly from novaphy.ik:
q_solution, converged = ik.solve_ik(bundle, q0, target_position)
q_pose, converged = ik.solve_ik_pose(
bundle,
q0,
target_position,
target_rotation_matrix,
)
solve_ik, solve_ik_pose, and solve_ik_pose_best_of_seeds are now
compatibility wrappers around IKSolver with LM. They run the requested
fixed iteration count and compute the returned convergence flag afterward.
When explicit limits are supplied, the wrappers add the soft limit objective
and apply a final hard clip. New tracking and batched code should construct
IKSolver directly.
The low-level compatibility helpers
compute_jacobian_position(),
compute_jacobian_angular_velocity(), get_ee_position(),
get_ee_rotation(), get_link_transforms(), and
rotation_error_axis_angle() also remain public.
Coordinate conventions¶
Python-facing flat buffers are linear-first:
- free-joint
q = [px, py, pz, qx, qy, qz, qw] - free-joint
qd = [vx, vy, vz, wx, wy, wz] - free-joint
joint_f = [fx, fy, fz, tx, ty, tz] state.body_qd = [linear; angular]state.body_f = [force; torque]
C++ novaphy::SpatialVector and the public spatial-algebra helpers use the same
linear-first layout, so no component reordering is required at that boundary.
Demos¶
python/demos/mujoco/demo_robot_panda_hydro.pycombines the class-style IK solver, runtime joint targets, native dynamics, and unified viewers.python/demos/ik_arm_demo/benchmark_compare.pycompares LM finite differences, LM analytic Jacobians, and L-BFGS on the same arm.python/demos/demo_ik_arm.pyusesViewerGL.log_gizmo()for a draggable position target, renders the URDF DAE visuals in a Z-up viewer model, and writes name-mapped IK FK results into its standardSimState. Use--headlessfor a one-shot solve or--viewer null --num-frames 120to exercise the interactive lifecycle without a window.