Reference/API

Sim MPC

robokit.helpers.sim_mpc.SimMPC(
config: SimMPCConfig,
sim: Sim,
ctrl_ref: np.ndarray
)
Bases: Task

Receding-horizon MPPI over simulated rollouts, for config.batch_size instances.

Lifecycle

  1. __init__ resizes sim into the rollout and validation worlds and captures their graphs on the first plan.
  2. plan(data, t0) refines every instance's plan from its state and returns the next ctrl_steps commands.
  3. The caller executes them on its own simulator (or execute does on one), then plans again at t0 + ctrl_steps.

Example

python
>>> import mujoco
>>> from robokit.helpers.sim_mpc import SimMPCConfig
>>> from robokit.sim import MjWarpSim
>>> from robokit.terms import SensorTask
>>> xml = (
...     '<mujoco><option timestep="0.01"/><worldbody><body><joint name="x" type="slide" damping="5"/>'
...     '<geom size="0.05" mass="1"/></body></worldbody><actuator><position joint="x" kp="50"/></actuator>'
...     '<sensor><jointpos name="x" joint="x"/></sensor></mujoco>'
... )
>>> model = mujoco.MjModel.from_xml_string(xml)
>>> config = SimMPCConfig(num_samples=32, horizon_steps=20, node_steps=10, ctrl_steps=10, max_iterations=2)
>>> config = config.add(SensorTask(np.full((60, 1), 0.3), {"x": 100.0}))
>>> mpc = SimMPC(config, MjWarpSim(model, 1), np.zeros((60, 1)))
>>> commands, infos = mpc.plan(mujoco.MjData(model), 0)  # execute these, then plan again at step 10
>>> commands.shape, mpc.sims["rollout"].nworld
((10, 1, 1), 31)
>>> (rollout,), (plans,) = mpc.execute(MjWarpSim(model, 1), 40)  # or run the loop on a simulator
>>> rollout["qpos"].shape, len(plans)
((40, 1), 4)
>>> # "auto" picked 4x on the CPU (2x on an RTX 3090): every rollout's first quarter steps at 10 ms, the rest at 40 ms
>>> mpc.stages  # xdoctest: +IGNORE_WANT
[SimMPCStage(iters=2, dt=[(0.25, None), (1.0, 0.04)])]
Parameters
configSimMPCConfigrequired

The settings, with the cost terms (config.add), the node noise (ctrl_std) and command bounds.

simSimrequired

The planning simulator, e.g. MjWarpSim(model, 1); for scenes that differ, one scene per instance.

ctrl_refnp.ndarrayrequired

The reference commands the plans offset, (T, nu) or (batch_size, T, nu).

Members
attributeinherited from Task[source]
SimMPC.SUPPORTS_CUDA_GRAPH: bool = True
attributeinherited from Task[source]
SimMPC.SUPPORTS_AUTODIFF: bool = True

False when the residual kernels have no usable tape adjoint; autodiff then folds in the analytic JᵀR.

attributeinherited from Task[source]
SimMPC.name: str = ''

Label a config gave this term, so config.task(name) finds it again.

attributeinherited from Task[source]
SimMPC.robot: Any = None

The robot a built task reads; None on a template.

attributeinherited from Task[source]
SimMPC.weight: Any = 1.0
propertyinherited from Task[source]
SimMPC.active: bool

Whether the term adds cost; helpers skip templates whose weights are all zero (None weighs 1).

propertyinherited from Task[source]
SimMPC.parallel_safe: bool

Whether the term may run on its own CUDA stream; one that reads another term's results runs all in order.

methodinherited from Task[source]
SimMPC.resolve(
var_values: VarValues,
context: Mapping[Context, Any]
)

Swap each Context placeholder, alone or in a tuple, for its value; subclasses then build for the variable.

Example

python
>>> class Inputs(Context):
...     STEP = 0
>>> class Step(Task):
...     def __init__(self, dt):
...         self.dt = dt
>>> task = Step(dt=(Inputs.STEP, 2.0))
>>> task.resolve(None, {Inputs.STEP: 0.5})  # the base swaps placeholders only
>>> task.dt
(0.5, 2.0)
Parameters
var_valuesVarValuesrequired
contextMapping[Context, Any]required
methodinherited from Task[source]
SimMPC.initial_vars(
num_candidates: int,
device: Any
) -> Dict[Enum, Var]

Variables this task adds beside the helper's, at their nominal values; usually none.

Parameters
num_candidatesintrequired
deviceAnyrequired
Returns
Dict[Enum, Var]
methodinherited from Task[source]
SimMPC.needs() -> set

Return the Context members a template reads, including those of the tasks it wraps.

Example

python
>>> class Inputs(Context):
...     STEP = 0
>>> class Step(Task):
...     def __init__(self, dt):
...         self.dt = dt
>>> Step(dt=(Inputs.STEP, 2.0)).needs() == {Inputs.STEP}
True
Returns
set
propertyinherited from Task[source]
SimMPC.cost_name: str

Return the snake-case task name used in cost reports.

propertyinherited from Task[source]
SimMPC.dense_task: Task

Return the per-pose task this term applies, which is the term itself unless it wraps one.

methodinherited from Task[source]
SimMPC.precompute(
var_values: VarValues,
need_gradient: bool = False
)

Prepare shared state before this term is evaluated.

Parameters
var_valuesVarValuesrequired
need_gradientbool= False
method[source]
SimMPC.compute_weighted_cost(
var_values: Optional[VarValues],
out_cost: wp.array
)

Add each node row's rollout cost: draws in the rollout worlds, moved plans in the validation worlds.

Parameters
var_valuesOptional[VarValues]required
out_costwp.arrayrequired
method[source]
SimMPC.plan(
data: Any,
t0: int
) -> Tuple[wp.array, List[dict]]

Refine every instance's plan from its state at sim step t0; return its next ctrl_steps commands.

Parameters
dataAnyrequired

Each instance's time, qpos, qvel, act and qacc_warmstart, (batch_size, ...) arrays: e.g. the executing simulator's data, or a mujoco.MjData for one instance.

t0intrequired

The state's sim step, which indexes ctrl_ref and the costs' references.

Returns
Tuple[wp.array, List[dict]]

The commands, (ctrl_steps, batch_size, nu) before any control_map, in a buffer the next call reuses, and each instance's plan info (t0, iterations, plan_cost, best_cost, ...).

method[source]
SimMPC.execute(
sim: Sim,
num_steps: int,
callback: Optional[Callable[[List[dict]], None]] = None
) -> Tuple[List[Dict[str, np.ndarray]], List[List[dict]]]

Run the planner in closed loop on sim, one world per instance, for num_steps sim steps: each control period plans from sim's state, then steps sim through the commands, mapped by the config's control_map and clipped to the controls MuJoCo applies, and calls callback with every instance's plan info.

Parameters
simSimrequired
num_stepsintrequired
callbackOptional[Callable[[List[dict]], None]]= None
Returns
Tuple[List[Dict[str, np.ndarray]], List[List[dict]]]

Each instance's rows, time, qpos, qvel, ctrl and sensordata (after the step), each (num_steps, ...), and its plan info per control period.

robokit.helpers.sim_mpc.SimMPCConfig(
terms: List[Tuple[Task, float, Optional[float]]] = list(),
ctrl_std: Any = 0.1,
ctrl_range: Optional[np.ndarray] = None,
qpos_std: Any = 0.0,
qvel_std: Any = 0.0,
control_map: Optional[Callable[[Sim, wp.array], None]] = None,
batch_size: int = 1,
num_samples: int = 512,
num_replicas: int = 1,
horizon_steps: int = 300,
node_steps: int = 50,
ctrl_steps: int = 100,
max_iterations: int = 12,
init_iterations: int = 0,
stages: Optional[List[SimMPCStage]] = None,
min_improvement: float = 0.005,
patience: int = 2,
std_growth: float = 4.0,
noise_decay: float = 1.0,
interpolation: Union[Literal['linear', 'cubic', 'zero'], np.ndarray] = 'linear',
draw_first_node: bool = False,
wide_ratio: float = 0.0,
wide_scale: float = 6.0,
std_range: Optional[Tuple[float, float]] = None,
min_std: float = 0.001,
std_gamma: float = 0.5,
elite_ratio: float = 0.2,
best_draw_margin: float = 4.0,
settle_steps: int = 0,
terminal_scale: float = 10.0,
seed: int = 0,
cuda_graph_mode: Literal['none', 'step'] = 'step',
planner: Literal['mppi', 'ilqr'] = 'mppi',
ilqr: Optional[Any] = None
)

SimMPC's per-task settings; times are in sim steps.

Example

python
>>> config = SimMPCConfig(num_samples=64, horizon_steps=30)
>>> config.num_samples, config.node_steps
(64, 50)
>>> SimMPCConfig(stages=[SimMPCStage(3, dt=0.04), SimMPCStage(1)]).max_iterations  # 3 coarse iterations, 1 fine
4
Parameters
termsList[Tuple[Task, float, Optional[float]]]= list()
ctrl_stdAny= 0.1
ctrl_rangeOptional[np.ndarray]= None
qpos_stdAny= 0.0
qvel_stdAny= 0.0
control_mapOptional[Callable[[Sim, wp.array], None]]= None
batch_sizeint= 1
num_samplesint= 512
num_replicasint= 1
horizon_stepsint= 300
node_stepsint= 50
ctrl_stepsint= 100
max_iterationsint= 12
init_iterationsint= 0
stagesOptional[List[SimMPCStage]]= None
min_improvementfloat= 0.005
patienceint= 2
std_growthfloat= 4.0
noise_decayfloat= 1.0
interpolationUnion[Literal['linear', 'cubic', 'zero'], np.ndarray]= 'linear'
draw_first_nodebool= False
wide_ratiofloat= 0.0
wide_scalefloat= 6.0
std_rangeOptional[Tuple[float, float]]= None
min_stdfloat= 0.001
std_gammafloat= 0.5
elite_ratiofloat= 0.2
best_draw_marginfloat= 4.0
settle_stepsint= 0
terminal_scalefloat= 10.0
seedint= 0
cuda_graph_modeLiteral['none', 'step']= 'step'
plannerLiteral['mppi', 'ilqr']= 'mppi'
ilqrOptional[Any]= None
Members
attribute[source]
SimMPCConfig.terms: List[Tuple[Task, float, Optional[float]]] = field(default_factory=list)

The cost terms with their running and terminal scales, added with add.

attribute[source]
SimMPCConfig.ctrl_std: Any = 0.1

Node noise per command at the first free node, one value or one per command.

attribute[source]
SimMPCConfig.ctrl_range: Optional[np.ndarray] = None

(nu, 2) bounds of the commands; None: the actuator ranges MuJoCo clamps to.

attribute[source]
SimMPCConfig.qpos_std: Any = 0.0

Each replica's start noise on qpos, one value or one per coordinate.

attribute[source]
SimMPCConfig.qvel_std: Any = 0.0

Each replica's start noise on qvel, one value or one per dof.

attribute[source]
SimMPCConfig.control_map: Optional[Callable[[Sim, wp.array], None]] = None

control_map(sim, clock) turns the commands in sim.data.ctrl into actuator controls before every step.

attribute[source]
SimMPCConfig.batch_size: int = 1

Instances planned at once; they share the model and the config.

attribute[source]
SimMPCConfig.num_samples: int = 512

Draws per iteration, one of them reserved to score the moved plan. A sim step costs about the same up to some hundreds of worlds (about 512 on an RTX 3090) and grows past them, so fill that many before adding iterations.

attribute[source]
SimMPCConfig.num_replicas: int = 1

Worlds per draw, which average their costs (e.g. over qpos_std noise).

attribute[source]
SimMPCConfig.horizon_steps: int = 300

Steps each rollout simulates and scores.

attribute[source]
SimMPCConfig.node_steps: int = 50

Steps between nodes, the points the control offsets are interpolated through.

attribute[source]
SimMPCConfig.ctrl_steps: int = 100

Steps executed per plan; a multiple of node_steps keeps shifted nodes exact.

attribute[source]
SimMPCConfig.max_iterations: int = 12

Sampling iterations per plan, at most; with stages, the sum of their iters.

attribute[source]
SimMPCConfig.init_iterations: int = 0

If positive, the first plan's iterations instead of max_iterations: the stages' last ones, the first stage run longer or cut.

attribute[source]
SimMPCConfig.stages: Optional[List[SimMPCStage]] = None

Coarse to fine: the stages each plan's iterations run in order; the moved plans are scored at the simulator's timestep. None: the first rollouts pick [SimMPCStage(max_iterations, [(0.25, None), (1.0, k * timestep)])] for the largest k of 4 and 2 whose draws end within 11% of their spread of the fine ones, else one fine stage.

attribute[source]
SimMPCConfig.min_improvement: float = 0.005

Stop once the best draw beats the plan by under this, patience times running.

attribute[source]
SimMPCConfig.patience: int = 2

Iterations under min_improvement in a row that stop the plan.

attribute[source]
SimMPCConfig.std_growth: float = 4.0

Node noise grows from ctrl_std at the first free node to this multiple at the last.

attribute[source]
SimMPCConfig.noise_decay: float = 1.0

Iteration i scales the node noise by noise_decay**i.

attribute[source]
SimMPCConfig.interpolation: Union[Literal['linear', 'cubic', 'zero'], np.ndarray] = 'linear'

Node interpolation, or a (steps, num_nodes) basis of each step's node weights; "zero" also draws the first node.

attribute[source]
SimMPCConfig.draw_first_node: bool = False

Draw the first node for any interpolation, so a plan can change the command it executes now (predictive sampling, CEM).

attribute[source]
SimMPCConfig.wide_ratio: float = 0.0

Share of each plan's first-round draws taken at wide_scale times the noise, as predictive sampling explores.

attribute[source]
SimMPCConfig.wide_scale: float = 6.0
attribute[source]
SimMPCConfig.std_range: Optional[Tuple[float, float]] = None

If set, a plan of one iteration starts from the last plan's refit noise, as CEM carries its spread: one scale of the configured noise (root mean square over the drawn nodes), clipped to this range.

attribute[source]
SimMPCConfig.min_std: float = 0.001

Floor of the node noise.

attribute[source]
SimMPCConfig.std_gamma: float = 0.5

Share of the draws' weighted spread each iteration moves the node noise toward.

attribute[source]
SimMPCConfig.elite_ratio: float = 0.2

Share of the cheapest draws whose cost spread is the temperature and whose weighted mean is the moved plan.

attribute[source]
SimMPCConfig.best_draw_margin: float = 4.0

Execute the best draw instead of the moved plan when the moved plan costs this many temperatures more.

attribute[source]
SimMPCConfig.settle_steps: int = 0

Unscored zero-control steps before each rollout.

attribute[source]
SimMPCConfig.terminal_scale: float = 10.0

Cost scale at the horizon's last step, unless add sets a term's own.

attribute[source]
SimMPCConfig.seed: int = 0

Seed of the node draws and the replicas' initial-state noise.

attribute[source]
SimMPCConfig.cuda_graph_mode: Literal['none', 'step'] = 'step'

"step" replays one captured graph per sim step; "none" launches every kernel.

attribute[source]
SimMPCConfig.planner: Literal['mppi', 'ilqr'] = 'mppi'

"ilqr": ILQROptimizer refines commands held for node_steps, max_iterations times a plan, instead of nodes.

attribute[source]
SimMPCConfig.ilqr: Optional[Any] = None

"ilqr"'s ILQROptimizerConfig, but max_iter; None: damped (robot costs seldom penalize controls) and cautious.

method[source]
SimMPCConfig.add(
term: Task,
running_scale: float = 1.0,
terminal_scale: Optional[float] = None
) -> SimMPCConfig

Add a cost term, scaled by running_scale per step and by terminal_scale (default: the config's) at the horizon's last step; running_scale=0 makes a terminal-only cost.

Parameters
termTaskrequired
running_scalefloat= 1.0
terminal_scaleOptional[float]= None
Returns
SimMPCConfig
robokit.helpers.sim_mpc.CartesianControl(
model: mujoco.MjModel,
body: str,
position_ref: Sequence[float] = (0, 0, 0),
q_wxyz_ref: Sequence[float] = (1, 0, 0, 0),
position_gain: Sequence[float] = (0, 0, 0),
rotation_gain: Sequence[float] = (0, 0, 0),
damping: float = 0.001
)

A SimMPC control map: world-frame body twists (the first six commands) to joint velocities by damped least squares, with feedback toward position_ref and q_wxyz_ref; needs scalar joint actuators.

Example

python
>>> import mujoco
>>> model = mujoco.MjModel.from_xml_string('<mujoco><worldbody><body name="b"><joint name="j" type="slide"/>'
... '<geom size="0.1"/></body></worldbody><actuator><velocity joint="j"/></actuator></mujoco>')
>>> control = CartesianControl(model, "b", position_gain=[0, 0, 1])
>>> control.dofs.shape
(1,)
Parameters
modelmujoco.MjModelrequired
bodystrrequired
position_refSequence[float]= (0, 0, 0)
q_wxyz_refSequence[float]= (1, 0, 0, 0)
position_gainSequence[float]= (0, 0, 0)
rotation_gainSequence[float]= (0, 0, 0)
dampingfloat= 0.001
function[source]
robokit.helpers.sim_mpc.sim_context(
sim: Sim
) -> Dict[Context, Any]

The SimContext values of a simulator's worlds, with its mjm's layout, except CLOCK, NUM_INSTANCES and WORLDS_PER_INSTANCE.

Parameters
simSimrequired
Returns
Dict[Context, Any]
robokit.sim.Sim
Bases: Protocol

What MjWarpSim and newton.NewtonSim offer: device work only, also in a capture on Warp's current stream.

Members
attribute[source]
Sim.mjm: Any

The mujoco.MjModel whose layout data follows: bodies, joints, sensors (for several scenes, their merge).

attribute[source]
Sim.nworld: int
attribute[source]
Sim.substeps: int
attribute[source]
Sim.stream: Optional[wp.Stream]

The CUDA stream it launches on (None on the CPU); other GPU work on it stays ordered without host syncs.

attribute[source]
Sim.data: Any

Warp arrays named and laid out as in mujoco.MjData, with a leading world axis (nworld, ...).

attribute[source]
Sim.model: Any

mujoco.MjModel's fields (NewtonSim: none), a row per world copied from the shared row on first access.

method[source]
Sim.step()

Advance substeps physics steps; data.substep_sensordata[k]: sensordata after substep k.

method[source]
Sim.forward()

Recompute the derived quantities (xpos, sensordata, ...) from the current state.

method[source]
Sim.reset(
mask: wp.array
)

Return the worlds where mask is set to the initial state.

Parameters
maskwp.arrayrequired
method[source]
Sim.resized(
nworld: int
) -> Sim

A new simulator of the same scenes and settings with nworld worlds.

Parameters
nworldintrequired
Returns
Sim
robokit.sim.MjWarpSim(
model: Union[mujoco.MjModel, Sequence[mujoco.MjSpec]],
nworld: Union[int, Sequence[int]],
data: Optional[mujoco.MjData] = None,
substeps: int = 1,
njmax: Optional[int] = None,
nconmax: Optional[int] = None,
key: Optional[str] = None,
device = None
)

Step nworld worlds of model, or nworld[i] of each scene model[i], substeps physics steps per step().

Example

python
>>> import mujoco, numpy as np, warp as wp
>>> from robokit.sim import MjWarpSim
>>> xml = '<mujoco><worldbody><geom type="plane" size="1 1 .1"/>'
>>> xml += '<body pos="0 0 .5"><freejoint/><geom size=".1"/></body></worldbody></mujoco>'
>>> sim = MjWarpSim(mujoco.MjModel.from_xml_string(xml), nworld=8, substeps=4)
>>> sim.model.geom_friction.shape  # one row per world, ready to randomize
(8, 2)
>>> sim.opt.iterations = 5  # from the next step on
>>> sim.step()
>>> bool((sim.data.qpos.numpy()[:, 2] < 0.5).all()), sim.data.substep_sensordata.shape
(True, (4, 8, 0))
>>> sim.reset(wp.array(np.arange(8) < 4))  # the first four worlds back to the start
>>> z = sim.data.qpos.numpy()[:, 2]
>>> float(z[0]), bool(z[7] < 0.5)
(0.5, True)
>>> big = mujoco.MjSpec.from_string(xml.replace('size=".1"', 'size=".2"'))
>>> mixed = MjWarpSim([mujoco.MjSpec.from_string(xml), big], nworld=[3, 5])  # worlds 3 to 7: the big ball
>>> (mixed.model.geom_size.numpy()[:, 1, 0] > 0.15).tolist()
[False, False, False, True, True, True, True, True]
>>> pos = mixed.model.geom_pos.numpy()
>>> pos[0, 0, 2] = -1.0  # world 0's floor, a static geom, a meter lower ...
>>> mixed.model.geom_pos.assign(pos)
>>> mixed.reset(wp.array(np.arange(8) == 0))  # ... from its next reset
>>> mixed.data.geom_xpos.numpy()[:2, 0, 2].tolist()
[-1.0, 0.0]
Parameters
modelUnion[mujoco.MjModel, Sequence[mujoco.MjSpec]]required

A mujoco.MjModel, or mujoco.MjSpec scenes that may differ in geoms, meshes, inertias, sites, pairs.

nworldUnion[int, Sequence[int]]required

The worlds, or each scene's; each world gets its scene's values.

dataOptional[mujoco.MjData]= None

The state reset() restores, else keyframe key's, else the defaults; several scenes: their defaults.

substepsint= 1
njmaxOptional[int]= None
nconmaxOptional[int]= None
keyOptional[str]= None
device= None

The Warp device; on a GPU each call runs as one cached CUDA graph.

Members
method[source]
MjWarpSim.resized(
nworld: int
) -> MjWarpSim

A new simulator of this one's scenes and settings with nworld worlds, split evenly over several scenes.

Parameters
nworldintrequired
Returns
MjWarpSim
method[source]
MjWarpSim.build_coarse_spec(
spec: mujoco.MjSpec,
config: CoarseConfig
) -> mujoco.MjSpec

A copy of spec with config's cheaper geometry and physics, e.g. for planning or early RL training.

Example

python
>>> spec = mujoco.MjSpec.from_string(
...     '<mujoco><worldbody><body name="d"><freejoint/><geom name="a" size="0.1"/><geom size="0.1"/></body>'
...     '<geom name="b" size="0.1" pos="1 0 0"/><geom name="c" size="0.1" pos="0 0 -0.21"/></worldbody>'
...     '<contact><pair geom1="a" geom2="b"/><pair geom1="a" geom2="c"/></contact></mujoco>'
... )
>>> coarse = MjWarpSim.build_coarse_spec(spec, CoarseConfig(qpos_ref=np.array([[0, 0, 0, 1, 0, 0, 0]])))
>>> coarse.compile().npair  # only a-c comes near
1
>>> config = CoarseConfig(collide="a|c|d", condim=1, condim_geoms="d", timestep=0.01, pyramidal=True)
>>> model = MjWarpSim.build_coarse_spec(spec, config).compile()
>>> model.geom_contype.tolist(), model.geom_condim.tolist()  # b, c, a, d's unnamed: b no longer collides
([0, 1, 1, 1], [3, 3, 3, 1])
>>> model.opt.timestep, model.opt.cone
(0.01, 0)
>>> config = CoarseConfig(min_timeconst=0.01, min_impedance_width=0.002)
>>> model = MjWarpSim.build_coarse_spec(spec, config).compile()
>>> model.geom_solref[:, 0].tolist(), model.geom_solimp[:, 2].tolist()  # from 0.02 and 0.001 by default
([0.02, 0.02, 0.02, 0.02], [0.002, 0.002, 0.002, 0.002])
>>> config = CoarseConfig(iterations=2, ls_iterations=4, integrator="implicitfast")
>>> opt = MjWarpSim.build_coarse_spec(spec, config).option
>>> opt.iterations, opt.ls_iterations, opt.integrator == mujoco.mjtIntegrator.mjINT_IMPLICITFAST
(2, 4, True)
Parameters
specmujoco.MjSpecrequired
configCoarseConfigrequired
Returns
mujoco.MjSpec
method[source]
MjWarpSim.set_const()

Recompute constants derived from masses and inertias, per world, after randomizing them or body_ipos.

method[source]
MjWarpSim.build_entity(
prefix: str,
soft_joint_pos_limit_factor: float = 1.0
) -> Entity

The robokit.sim.mjentity.Entity named prefix*, its default pose the initial qpos.

Example

python
>>> xml = '<mujoco><worldbody><body name="r/base" pos="0 0 .5"><freejoint name="r/root"/><geom size=".1"/>'
>>> xml += '<body><joint name="r/a"/><geom size=".1"/></body></body></worldbody></mujoco>'
>>> robot = MjWarpSim(mujoco.MjModel.from_xml_string(xml), nworld=2).build_entity("r/")
>>> robot.joint_names, robot.data.default_root_state[:3].tolist()
(['a'], [0.0, 0.0, 0.5])
Parameters
prefixstrrequired
soft_joint_pos_limit_factorfloat= 1.0
Returns
Entity
method[source]
MjWarpSim.build_contact_sensor(
name: str,
track_air_time: bool = False
) -> ContactSensor

A robokit.sim.mjsensor.ContactSensor of the contact sensors named {name}_{primary}_{found or force}.

Example

python
>>> xml = '<mujoco><worldbody><body><freejoint/><geom name="ball" size=".1"/></body></worldbody><sensor>'
>>> xml += '<contact name="touch_ball_found" geom1="ball" data="found"/></sensor></mujoco>'
>>> MjWarpSim(mujoco.MjModel.from_xml_string(xml), nworld=2).build_contact_sensor("touch").primary_names
['ball']
Parameters
namestrrequired
track_air_timebool= False
Returns
ContactSensor
method[source]
MjWarpSim.get_sensor(
name: str
) -> Any

(nworld, dim): sensor name's readings, a zero-copy torch view of data.sensordata.

Example

python
>>> xml = '<mujoco><worldbody><body name="b" pos="0 0 .5"><freejoint/><geom size=".1"/></body></worldbody>'
>>> xml += '<sensor><framepos name="p" objtype="body" objname="b"/></sensor></mujoco>'
>>> MjWarpSim(mujoco.MjModel.from_xml_string(xml), nworld=2).get_sensor("p").tolist()
[[0.0, 0.0, 0.5], [0.0, 0.0, 0.5]]
Parameters
namestrrequired
Returns
Any
method[source]
MjWarpSim.compute_rays(
render_context: Any,
rays: Any = {}
)

Refit render_context's BVH to the current poses, then cast rays (mjw.rays arguments) in every world.

Example

python
>>> mjm = mujoco.MjModel.from_xml_string('<mujoco><worldbody><geom type="box" pos="0 0 -.5" size="1 1 .5"/>'
...     '</worldbody></mujoco>')
>>> rc = mjw.create_render_context(mjm, 2, cam_res=[], render_rgb=[], render_depth=[], render_seg=[])
>>> dist, down = wp.zeros((2, 1), dtype=float), wp.array([[[0, 0, -1]]] * 2, dtype=wp.vec3)  # from z = 1 m
>>> MjWarpSim(mjm, nworld=2).compute_rays(rc, pnt=wp.array([[[0, 0, 1]]] * 2, dtype=wp.vec3), vec=down,
...     geomgroup=wp.types.vector(6, wp.float32)(-1, -1, -1, -1, -1, -1), flg_static=True,
...     bodyexclude=wp.array([-1], dtype=int), dist=dist, geomid=wp.zeros((2, 1), dtype=int),
...     normal=wp.zeros((2, 1), dtype=wp.vec3))
>>> dist.numpy().tolist()
[[1.0], [1.0]]
Parameters
render_contextAnyrequired
raysAny= {}
method[source]
MjWarpSim.set_torch_stream()

Run torch on Warp's stream, so torch and the simulator stay ordered without host syncs.

Example

python
>>> import torch
>>> xml = '<mujoco><worldbody><body><freejoint/><geom size=".1"/></body></worldbody></mujoco>'
>>> (sim := MjWarpSim(mujoco.MjModel.from_xml_string(xml), nworld=2)).set_torch_stream()
>>> torch.cuda.current_stream().cuda_stream == sim.stream.cuda_stream
True
method[source]
MjWarpSim.set_random(
field: str,
index: Tuple[Any, ...],
low: Union[float, Sequence[float]],
high: Union[float, Sequence[float]],
log: bool = False,
add: bool = False,
rng: Optional[np.random.Generator] = None
)

Set (add: offset) model.<field>[:, *index] to one uniform draw per world from low to high.

Example

python
>>> xml = '<mujoco><worldbody><body><freejoint/><geom size=".1"/></body></worldbody></mujoco>'
>>> sim = MjWarpSim(mujoco.MjModel.from_xml_string(xml), nworld=8)
>>> sim.set_random("geom_friction", (0, 0), 0.5, 1.0)  # sliding friction
>>> friction = sim.model.geom_friction.numpy()[:, 0, 0]
>>> bool(((friction >= 0.5) & (friction <= 1.0)).all()), len(set(friction.tolist()))
(True, 8)
>>> sim.set_random("body_ipos", (1,), (-0.01, -0.01, 0.0), (0.01, 0.01, 0.0), add=True)  # center of mass
>>> float(abs(sim.model.body_ipos.numpy()[:, 1, 2]).max())
0.0
Parameters
fieldstrrequired

After body_mass, body_inertia, body_ipos or body_iquat, it runs set_const().

indexTuple[Any, ...]required
lowUnion[float, Sequence[float]]required

Lower bound, broadcast to the trailing axes.

highUnion[float, Sequence[float]]required

Upper bound, broadcast to the trailing axes.

logbool= False

Draw log-uniformly.

addbool= False
rngOptional[np.random.Generator]= None
method[source]
MjWarpSim.step()

Advance substeps physics steps into substep_sensordata; derived fields then lag qpos by a substep.

method[source]
MjWarpSim.kinematics()

Recompute xpos, xmat, site_xpos, ... from qpos, e.g. after writing it, as mj_kinematics.

method[source]
MjWarpSim.com_pos()

Recompute subtree_com (subtree centers of mass), cinert and cdof from the positions, as mj_comPos.

method[source]
MjWarpSim.forward()

Recompute every derived quantity, accelerations and sensordata included, as mj_forward.

method[source]
MjWarpSim.reset(
mask: wp.array
)

Return the worlds where mask is set to their initial state, static geoms to their current rows.

Parameters
maskwp.arrayrequired

(nworld,), bool or int; read on the device, with no host syncs.

method[source]
MjWarpSim.set_scene(
mask: wp.array,
scene: wp.array
)

Give the worlds where mask is set scene scene[w]'s model rows; their static geoms move at reset().

Example

python
>>> xml = '<mujoco><worldbody><body><freejoint/><geom size=".1"/></body></worldbody></mujoco>'
>>> scenes = [mujoco.MjSpec.from_string(xml), mujoco.MjSpec.from_string(xml.replace(".1", ".2"))]
>>> sim = MjWarpSim(scenes, nworld=[2, 2])
>>> sim.set_scene(wp.array([True, False, False, False]), wp.array([1, 0, 0, 0], dtype=int))
>>> (sim.model.geom_size.numpy()[:, 0, 0] > 0.15).tolist()  # world 0: the big ball
[True, False, True, True]
Parameters
maskwp.arrayrequired

(nworld,), read on the device, with no host syncs.

scenewp.arrayrequired

(nworld,), int.

robokit.terms.SimContext
Bases: Context

Values a planner gives its terms for one world set (sim_context); arrays are refilled every step.

robokit.terms.StateTask(
qpos_ref: np.ndarray,
qvel_ref: np.ndarray,
dof_weight: np.ndarray,
vel_weight: float = 0.0,
penetration_groups: Optional[Tuple[np.ndarray, np.ndarray]] = None,
penetration_scale: float = 0.0,
margin: float = 0.0,
contacts: Sequence[Tuple[np.ndarray, np.ndarray, np.ndarray, float]] = (),
squared: bool = False,
freeze_reference: bool = False,
pose_tolerance: float = 0.0
)
Bases: SimResidualTask

Per step, the weighted qpos error norm plus vel_weight times the qvel error norm, and optional contact terms; its residuals cover the errors, while the contact terms (penetration, missing contacts) are cost-only, so SimMPC sees them and ILQROptimizer does not.

Example

python
>>> import copy, mujoco
>>> from robokit.helpers.sim_mpc import sim_context
>>> from robokit.sim import MjWarpSim
>>> model = mujoco.MjModel.from_xml_string(
...     '<mujoco><worldbody><body><freejoint/><geom size="0.1"/></body></worldbody></mujoco>'
... )
>>> data = mujoco.MjData(model)
>>> data.qpos[0] = 0.05  # 5 cm off the reference in x
>>> context = sim_context(MjWarpSim(model, 1, data)) | {
...     SimContext.CLOCK: wp.array([0, 0, 1], dtype=int),
...     SimContext.NUM_INSTANCES: 1,
...     SimContext.WORLDS_PER_INSTANCE: 1,
... }
>>> qpos_ref, qvel_ref, out = np.tile(model.qpos0, (2, 1)), np.zeros((2, model.nv)), wp.zeros(2, dtype=float)
>>> for i, tolerance in enumerate([0.0, 0.01]):
...     term = copy.copy(StateTask(qpos_ref, qvel_ref, np.ones(model.nv), pose_tolerance=tolerance))
...     term.resolve(None, context)
...     term.compute_weighted_cost(None, out_cost=out[i : i + 1])
>>> assert np.allclose(out.numpy(), [0.05, 0.04])
Parameters
qpos_refnp.ndarrayrequired
qvel_refnp.ndarrayrequired
dof_weightnp.ndarrayrequired
vel_weightfloat= 0.0
penetration_groupsOptional[Tuple[np.ndarray, np.ndarray]]= None
penetration_scalefloat= 0.0
marginfloat= 0.0
contactsSequence[Tuple[np.ndarray, np.ndarray, np.ndarray, float]]= ()
squaredbool= False
freeze_referencebool= False
pose_tolerancefloat= 0.0
Members
attributeinherited from Task[source]
StateTask.SUPPORTS_CUDA_GRAPH: bool = True
attributeinherited from Task[source]
StateTask.SUPPORTS_AUTODIFF: bool = True

False when the residual kernels have no usable tape adjoint; autodiff then folds in the analytic JᵀR.

attributeinherited from Task[source]
StateTask.var_key: Enum = VarKey.ROBOT
attributeinherited from Task[source]
StateTask.name: str = ''

Label a config gave this term, so config.task(name) finds it again.

attributeinherited from Task[source]
StateTask.robot: Any = None

The robot a built task reads; None on a template.

attributeinherited from Task[source]
StateTask.weight: Any = 1.0
propertyinherited from Task[source]
StateTask.active: bool

Whether the term adds cost; helpers skip templates whose weights are all zero (None weighs 1).

propertyinherited from Task[source]
StateTask.parallel_safe: bool

Whether the term may run on its own CUDA stream; one that reads another term's results runs all in order.

methodinherited from Task[source]
StateTask.initial_vars(
num_candidates: int,
device: Any
) -> Dict[Enum, Var]

Variables this task adds beside the helper's, at their nominal values; usually none.

Parameters
num_candidatesintrequired
deviceAnyrequired
Returns
Dict[Enum, Var]
methodinherited from Task[source]
StateTask.needs() -> set

Return the Context members a template reads, including those of the tasks it wraps.

Example

python
>>> class Inputs(Context):
...     STEP = 0
>>> class Step(Task):
...     def __init__(self, dt):
...         self.dt = dt
>>> Step(dt=(Inputs.STEP, 2.0)).needs() == {Inputs.STEP}
True
Returns
set
propertyinherited from Task[source]
StateTask.cost_name: str

Return the snake-case task name used in cost reports.

propertyinherited from Task[source]
StateTask.dense_task: Task

Return the per-pose task this term applies, which is the term itself unless it wraps one.

methodinherited from Task[source]
StateTask.precompute(
var_values: VarValues,
need_gradient: bool = False
)

Prepare shared state before this term is evaluated.

Parameters
var_valuesVarValuesrequired
need_gradientbool= False
attributeinherited from ResidualTask[source]
StateTask.residual_weight: Optional[Union[float, Sequence[float], np.ndarray]]
attributeinherited from ResidualTask[source]
StateTask.device: Any = None

Device of the task's Warp buffers; None before init_buffers.

methodinherited from ResidualTask[source]
StateTask.init_buffers(
device: wp_device_type
)

Allocate Warp buffers on device; a task without any keeps none.

Parameters
devicewp_device_typerequired
propertyinherited from SimResidualTask[source]
StateTask.residual_dim: int
methodinherited from SimResidualTask[source]
StateTask.compute_weighted_residual(
var_values: Optional[VarValues],
out_residual: wp.array,
row_offset: int = 0
) -> wp.array
Parameters
var_valuesOptional[VarValues]required
out_residualwp.arrayrequired
row_offsetint= 0
Returns
wp.array
methodinherited from ResidualTask[source]
StateTask.compute_weighted_jacobian_analytic(
var_values: VarValues,
args: Any = (),
out_jacobian: Optional[wp.array] = None,
row_offset: int = 0,
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
out_jacobianOptional[wp.array]= None
row_offsetint= 0
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
StateTask.compute_weighted_jacobian(
var_values: VarValues,
args: Any = (),
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
StateTask.accumulate_normal_equations(
var_values: VarValues,
costs: wp.array,
JtJ: Optional[wp.array] = None,
Jtr: Optional[wp.array] = None
)

Accumulate this term into shared normal-equation buffers.

Lifecycle

  1. Prepare term state and reusable buffers.
  2. Evaluate residuals and, for a build step, the Jacobian.
  3. Reduce into cost, JᵀJ, and Jᵀr.
Parameters
var_valuesVarValuesrequired

Optimization variables.

costswp.arrayrequired

Shared per-batch cost buffer.

JtJOptional[wp.array]= None

Optional shared normal matrix. Omit for a cost-only step.

JtrOptional[wp.array]= None

Optional shared normal-vector buffer. Omit for a cost-only step.

method[source]
StateTask.resolve(
var_values: Optional[VarValues],
context: Mapping[Context, Any]
)

Build the device references, contact groups and buffers for the context's worlds.

Parameters
var_valuesOptional[VarValues]required
contextMapping[Context, Any]required
method[source]
StateTask.compute_weighted_cost(
var_values: Optional[VarValues],
out_cost: wp.array
)
Parameters
var_valuesOptional[VarValues]required
out_costwp.arrayrequired
robokit.terms.SensorTask(
sensor_ref: np.ndarray,
weight: Union[Dict[str, Union[float, Sequence[float]]], np.ndarray],
norm: bool = False,
residual: Literal['linear', 'angle', 'quaternion', 'yaw', 'yaw_unwrapped', 'heading'] = 'linear',
hinge: Optional[float] = None,
freeze_reference: bool = False,
relative: Optional[str] = None
)
Bases: SimResidualTask

Per step, sum_s w_s * ||sensordata_s - sensor_ref[k]_s||^2 over the sensors in weight (norm: unsquared).

Example

python
>>> context = {  # what SimMPC gives one world at sim step 0
...     SimContext.CLOCK: wp.array([0, 0, 1], dtype=int, device="cpu"),
...     SimContext.NUM_INSTANCES: 1,
...     SimContext.WORLDS_PER_INSTANCE: 1,
...     SimContext.SENSORDATA: wp.array([[0.5, 0.5, 0.0, 0.0]], dtype=float, device="cpu"),
...     SimContext.SENSORS: [("x", 0, 1), ("p", 1, 3)],
... }
>>> ref, weight = np.zeros((10, 4)), {"x": 2.0, "p": [1.0, 0.0, 0.0]}
>>> out = wp.zeros(2, dtype=float, device="cpu")
>>> for i, norm in enumerate([False, True]):
...     term = SensorTask(ref, weight, norm=norm)
...     term.resolve(None, context)
...     term.compute_weighted_cost(None, out_cost=out[i : i + 1])
>>> assert np.allclose(out.numpy(), [2 * 0.5**2 + 1 * 0.5**2, 2 * 0.5 + 1 * 0.5])
Parameters
sensor_refnp.ndarrayrequired
weightUnion[Dict[str, Union[float, Sequence[float]]], np.ndarray]required
normbool= False
residualLiteral['linear', 'angle', 'quaternion', 'yaw', 'yaw_unwrapped', 'heading']= 'linear'
hingeOptional[float]= None
freeze_referencebool= False
relativeOptional[str]= None
Members
attributeinherited from Task[source]
SensorTask.SUPPORTS_CUDA_GRAPH: bool = True
attributeinherited from Task[source]
SensorTask.SUPPORTS_AUTODIFF: bool = True

False when the residual kernels have no usable tape adjoint; autodiff then folds in the analytic JᵀR.

attributeinherited from Task[source]
SensorTask.var_key: Enum = VarKey.ROBOT
attributeinherited from Task[source]
SensorTask.name: str = ''

Label a config gave this term, so config.task(name) finds it again.

attributeinherited from Task[source]
SensorTask.robot: Any = None

The robot a built task reads; None on a template.

attributeinherited from Task[source]
SensorTask.weight: Any = 1.0
propertyinherited from Task[source]
SensorTask.active: bool

Whether the term adds cost; helpers skip templates whose weights are all zero (None weighs 1).

propertyinherited from Task[source]
SensorTask.parallel_safe: bool

Whether the term may run on its own CUDA stream; one that reads another term's results runs all in order.

methodinherited from Task[source]
SensorTask.initial_vars(
num_candidates: int,
device: Any
) -> Dict[Enum, Var]

Variables this task adds beside the helper's, at their nominal values; usually none.

Parameters
num_candidatesintrequired
deviceAnyrequired
Returns
Dict[Enum, Var]
methodinherited from Task[source]
SensorTask.needs() -> set

Return the Context members a template reads, including those of the tasks it wraps.

Example

python
>>> class Inputs(Context):
...     STEP = 0
>>> class Step(Task):
...     def __init__(self, dt):
...         self.dt = dt
>>> Step(dt=(Inputs.STEP, 2.0)).needs() == {Inputs.STEP}
True
Returns
set
propertyinherited from Task[source]
SensorTask.cost_name: str

Return the snake-case task name used in cost reports.

propertyinherited from Task[source]
SensorTask.dense_task: Task

Return the per-pose task this term applies, which is the term itself unless it wraps one.

methodinherited from Task[source]
SensorTask.precompute(
var_values: VarValues,
need_gradient: bool = False
)

Prepare shared state before this term is evaluated.

Parameters
var_valuesVarValuesrequired
need_gradientbool= False
methodinherited from SimResidualTask[source]
SensorTask.compute_weighted_cost(
var_values: Optional[VarValues],
out_cost: wp.array
)

Add each world's 0.5 |r|^2.

Parameters
var_valuesOptional[VarValues]required
out_costwp.arrayrequired
attributeinherited from ResidualTask[source]
SensorTask.residual_weight: Optional[Union[float, Sequence[float], np.ndarray]]
attributeinherited from ResidualTask[source]
SensorTask.device: Any = None

Device of the task's Warp buffers; None before init_buffers.

methodinherited from ResidualTask[source]
SensorTask.init_buffers(
device: wp_device_type
)

Allocate Warp buffers on device; a task without any keeps none.

Parameters
devicewp_device_typerequired
propertyinherited from SimResidualTask[source]
SensorTask.residual_dim: int
methodinherited from SimResidualTask[source]
SensorTask.compute_weighted_residual(
var_values: Optional[VarValues],
out_residual: wp.array,
row_offset: int = 0
) -> wp.array
Parameters
var_valuesOptional[VarValues]required
out_residualwp.arrayrequired
row_offsetint= 0
Returns
wp.array
methodinherited from ResidualTask[source]
SensorTask.compute_weighted_jacobian_analytic(
var_values: VarValues,
args: Any = (),
out_jacobian: Optional[wp.array] = None,
row_offset: int = 0,
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
out_jacobianOptional[wp.array]= None
row_offsetint= 0
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
SensorTask.compute_weighted_jacobian(
var_values: VarValues,
args: Any = (),
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
SensorTask.accumulate_normal_equations(
var_values: VarValues,
costs: wp.array,
JtJ: Optional[wp.array] = None,
Jtr: Optional[wp.array] = None
)

Accumulate this term into shared normal-equation buffers.

Lifecycle

  1. Prepare term state and reusable buffers.
  2. Evaluate residuals and, for a build step, the Jacobian.
  3. Reduce into cost, JᵀJ, and Jᵀr.
Parameters
var_valuesVarValuesrequired

Optimization variables.

costswp.arrayrequired

Shared per-batch cost buffer.

JtJOptional[wp.array]= None

Optional shared normal matrix. Omit for a cost-only step.

JtrOptional[wp.array]= None

Optional shared normal-vector buffer. Omit for a cost-only step.

method[source]
SensorTask.resolve(
var_values: Optional[VarValues],
context: Mapping[Context, Any]
)

Look up the weighted sensors and build the device weights and reference.

Parameters
var_valuesOptional[VarValues]required
contextMapping[Context, Any]required
robokit.terms.ControlTask(
weight: Union[float, Sequence[float]],
ctrl_ref: Optional[np.ndarray] = None,
norm: bool = False
)
Bases: SimResidualTask

Per step, sum(weight * (ctrl - ctrl_ref[k])**2), or with norm its unsquared norm; ctrl_ref defaults to 0.

Example

python
>>> context = {
...     SimContext.CLOCK: wp.array([0, 0, 1], dtype=int, device="cpu"),
...     SimContext.NUM_INSTANCES: 1,
...     SimContext.WORLDS_PER_INSTANCE: 1,
...     SimContext.CTRL: wp.array([[1.0, 2.0]], dtype=float, device="cpu"),
... }
>>> out = wp.zeros(2, dtype=float, device="cpu")
>>> for i, term in enumerate([ControlTask([0.5, 1.0]), ControlTask(2.0, np.ones((10, 2)), norm=True)]):
...     term.resolve(None, context)
...     term.compute_weighted_cost(None, out_cost=out[i : i + 1])
>>> out.numpy().tolist()
[4.5, 2.0]
Parameters
weightUnion[float, Sequence[float]]required
ctrl_refOptional[np.ndarray]= None
normbool= False
Members
attributeinherited from Task[source]
ControlTask.SUPPORTS_CUDA_GRAPH: bool = True
attributeinherited from Task[source]
ControlTask.SUPPORTS_AUTODIFF: bool = True

False when the residual kernels have no usable tape adjoint; autodiff then folds in the analytic JᵀR.

attributeinherited from Task[source]
ControlTask.var_key: Enum = VarKey.ROBOT
attributeinherited from Task[source]
ControlTask.name: str = ''

Label a config gave this term, so config.task(name) finds it again.

attributeinherited from Task[source]
ControlTask.robot: Any = None

The robot a built task reads; None on a template.

attributeinherited from Task[source]
ControlTask.weight: Any = 1.0
propertyinherited from Task[source]
ControlTask.active: bool

Whether the term adds cost; helpers skip templates whose weights are all zero (None weighs 1).

propertyinherited from Task[source]
ControlTask.parallel_safe: bool

Whether the term may run on its own CUDA stream; one that reads another term's results runs all in order.

methodinherited from Task[source]
ControlTask.initial_vars(
num_candidates: int,
device: Any
) -> Dict[Enum, Var]

Variables this task adds beside the helper's, at their nominal values; usually none.

Parameters
num_candidatesintrequired
deviceAnyrequired
Returns
Dict[Enum, Var]
methodinherited from Task[source]
ControlTask.needs() -> set

Return the Context members a template reads, including those of the tasks it wraps.

Example

python
>>> class Inputs(Context):
...     STEP = 0
>>> class Step(Task):
...     def __init__(self, dt):
...         self.dt = dt
>>> Step(dt=(Inputs.STEP, 2.0)).needs() == {Inputs.STEP}
True
Returns
set
propertyinherited from Task[source]
ControlTask.cost_name: str

Return the snake-case task name used in cost reports.

propertyinherited from Task[source]
ControlTask.dense_task: Task

Return the per-pose task this term applies, which is the term itself unless it wraps one.

methodinherited from Task[source]
ControlTask.precompute(
var_values: VarValues,
need_gradient: bool = False
)

Prepare shared state before this term is evaluated.

Parameters
var_valuesVarValuesrequired
need_gradientbool= False
methodinherited from SimResidualTask[source]
ControlTask.compute_weighted_cost(
var_values: Optional[VarValues],
out_cost: wp.array
)

Add each world's 0.5 |r|^2.

Parameters
var_valuesOptional[VarValues]required
out_costwp.arrayrequired
attributeinherited from ResidualTask[source]
ControlTask.residual_weight: Optional[Union[float, Sequence[float], np.ndarray]]
attributeinherited from ResidualTask[source]
ControlTask.device: Any = None

Device of the task's Warp buffers; None before init_buffers.

methodinherited from ResidualTask[source]
ControlTask.init_buffers(
device: wp_device_type
)

Allocate Warp buffers on device; a task without any keeps none.

Parameters
devicewp_device_typerequired
propertyinherited from SimResidualTask[source]
ControlTask.residual_dim: int
methodinherited from SimResidualTask[source]
ControlTask.compute_weighted_residual(
var_values: Optional[VarValues],
out_residual: wp.array,
row_offset: int = 0
) -> wp.array
Parameters
var_valuesOptional[VarValues]required
out_residualwp.arrayrequired
row_offsetint= 0
Returns
wp.array
methodinherited from ResidualTask[source]
ControlTask.compute_weighted_jacobian_analytic(
var_values: VarValues,
args: Any = (),
out_jacobian: Optional[wp.array] = None,
row_offset: int = 0,
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
out_jacobianOptional[wp.array]= None
row_offsetint= 0
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
ControlTask.compute_weighted_jacobian(
var_values: VarValues,
args: Any = (),
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
ControlTask.accumulate_normal_equations(
var_values: VarValues,
costs: wp.array,
JtJ: Optional[wp.array] = None,
Jtr: Optional[wp.array] = None
)

Accumulate this term into shared normal-equation buffers.

Lifecycle

  1. Prepare term state and reusable buffers.
  2. Evaluate residuals and, for a build step, the Jacobian.
  3. Reduce into cost, JᵀJ, and Jᵀr.
Parameters
var_valuesVarValuesrequired

Optimization variables.

costswp.arrayrequired

Shared per-batch cost buffer.

JtJOptional[wp.array]= None

Optional shared normal matrix. Omit for a cost-only step.

JtrOptional[wp.array]= None

Optional shared normal-vector buffer. Omit for a cost-only step.

method[source]
ControlTask.resolve(
var_values: Optional[VarValues],
context: Mapping[Context, Any]
)

Broadcast the weight to every control and build the device reference.

Parameters
var_valuesOptional[VarValues]required
contextMapping[Context, Any]required
robokit.terms.AlignTask(
origin: str,
sensor: str,
target_ref: np.ndarray,
weight: float,
components: Sequence[int] = (0, 1),
freeze_reference: bool = False
)
Bases: SimResidualTask

Per step, weight * (1 + u.v / (|u||v|)) with u = sensor - origin and v = target_ref[k] - origin.

Example

python
>>> context = {
...     SimContext.CLOCK: wp.array([0, 0, 1], dtype=int, device="cpu"),
...     SimContext.NUM_INSTANCES: 1,
...     SimContext.WORLDS_PER_INSTANCE: 1,
...     SimContext.SENSORDATA: wp.array([[0.0, 0.0, 0.0, 1.0, 0.0, 0.0]], dtype=float, device="cpu"),
...     SimContext.SENSORS: [("o", 0, 3), ("s", 3, 3)],
... }
>>> term, out = AlignTask("o", "s", np.tile([0.0, 1.0, 0.0], (10, 1)), weight=2.0), wp.zeros(1, device="cpu")
>>> term.resolve(None, context)
>>> term.compute_weighted_cost(None, out_cost=out)
>>> assert np.isclose(out.numpy()[0], 2.0)  # u = (1, 0) is perpendicular to v = (0, 1)
Parameters
originstrrequired
sensorstrrequired
target_refnp.ndarrayrequired
weightfloatrequired
componentsSequence[int]= (0, 1)
freeze_referencebool= False
Members
attributeinherited from Task[source]
AlignTask.SUPPORTS_CUDA_GRAPH: bool = True
attributeinherited from Task[source]
AlignTask.SUPPORTS_AUTODIFF: bool = True

False when the residual kernels have no usable tape adjoint; autodiff then folds in the analytic JᵀR.

attributeinherited from Task[source]
AlignTask.var_key: Enum = VarKey.ROBOT
attributeinherited from Task[source]
AlignTask.name: str = ''

Label a config gave this term, so config.task(name) finds it again.

attributeinherited from Task[source]
AlignTask.robot: Any = None

The robot a built task reads; None on a template.

attributeinherited from Task[source]
AlignTask.weight: Any = 1.0
propertyinherited from Task[source]
AlignTask.active: bool

Whether the term adds cost; helpers skip templates whose weights are all zero (None weighs 1).

propertyinherited from Task[source]
AlignTask.parallel_safe: bool

Whether the term may run on its own CUDA stream; one that reads another term's results runs all in order.

methodinherited from Task[source]
AlignTask.initial_vars(
num_candidates: int,
device: Any
) -> Dict[Enum, Var]

Variables this task adds beside the helper's, at their nominal values; usually none.

Parameters
num_candidatesintrequired
deviceAnyrequired
Returns
Dict[Enum, Var]
methodinherited from Task[source]
AlignTask.needs() -> set

Return the Context members a template reads, including those of the tasks it wraps.

Example

python
>>> class Inputs(Context):
...     STEP = 0
>>> class Step(Task):
...     def __init__(self, dt):
...         self.dt = dt
>>> Step(dt=(Inputs.STEP, 2.0)).needs() == {Inputs.STEP}
True
Returns
set
propertyinherited from Task[source]
AlignTask.cost_name: str

Return the snake-case task name used in cost reports.

propertyinherited from Task[source]
AlignTask.dense_task: Task

Return the per-pose task this term applies, which is the term itself unless it wraps one.

methodinherited from Task[source]
AlignTask.precompute(
var_values: VarValues,
need_gradient: bool = False
)

Prepare shared state before this term is evaluated.

Parameters
var_valuesVarValuesrequired
need_gradientbool= False
methodinherited from SimResidualTask[source]
AlignTask.compute_weighted_cost(
var_values: Optional[VarValues],
out_cost: wp.array
)

Add each world's 0.5 |r|^2.

Parameters
var_valuesOptional[VarValues]required
out_costwp.arrayrequired
attributeinherited from ResidualTask[source]
AlignTask.residual_weight: Optional[Union[float, Sequence[float], np.ndarray]]
attributeinherited from ResidualTask[source]
AlignTask.device: Any = None

Device of the task's Warp buffers; None before init_buffers.

methodinherited from ResidualTask[source]
AlignTask.init_buffers(
device: wp_device_type
)

Allocate Warp buffers on device; a task without any keeps none.

Parameters
devicewp_device_typerequired
propertyinherited from SimResidualTask[source]
AlignTask.residual_dim: int
methodinherited from SimResidualTask[source]
AlignTask.compute_weighted_residual(
var_values: Optional[VarValues],
out_residual: wp.array,
row_offset: int = 0
) -> wp.array
Parameters
var_valuesOptional[VarValues]required
out_residualwp.arrayrequired
row_offsetint= 0
Returns
wp.array
methodinherited from ResidualTask[source]
AlignTask.compute_weighted_jacobian_analytic(
var_values: VarValues,
args: Any = (),
out_jacobian: Optional[wp.array] = None,
row_offset: int = 0,
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
out_jacobianOptional[wp.array]= None
row_offsetint= 0
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
AlignTask.compute_weighted_jacobian(
var_values: VarValues,
args: Any = (),
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
AlignTask.accumulate_normal_equations(
var_values: VarValues,
costs: wp.array,
JtJ: Optional[wp.array] = None,
Jtr: Optional[wp.array] = None
)

Accumulate this term into shared normal-equation buffers.

Lifecycle

  1. Prepare term state and reusable buffers.
  2. Evaluate residuals and, for a build step, the Jacobian.
  3. Reduce into cost, JᵀJ, and Jᵀr.
Parameters
var_valuesVarValuesrequired

Optimization variables.

costswp.arrayrequired

Shared per-batch cost buffer.

JtJOptional[wp.array]= None

Optional shared normal matrix. Omit for a cost-only step.

JtrOptional[wp.array]= None

Optional shared normal-vector buffer. Omit for a cost-only step.

method[source]
AlignTask.resolve(
var_values: Optional[VarValues],
context: Mapping[Context, Any]
)

Look up both sensors and build the device target.

Parameters
var_valuesOptional[VarValues]required
contextMapping[Context, Any]required
robokit.terms.ContactTask(
geoms: Sequence[str],
target: np.ndarray,
radius: np.ndarray,
allowed: Optional[np.ndarray] = None,
weight: float = 1.0,
penalty: float = 0.0,
margin: float = 0.0,
box: Optional[Sequence[float]] = None,
height: Optional[Sequence[float]] = None
)
Bases: Task

Reward geoms' contacts inside their target discs; penalize touches outside every allowed disc.

box, half widths (x, y), replaces each target disc with that axis-aligned box around the target; height, (low, high), rewards only contacts at those heights.

Example

python
>>> context = {  # one contact of geom 1 at (0, 0), 0.25 apart, at sim step 1
...     SimContext.CLOCK: wp.array([1, 0, 1], dtype=int, device="cpu"),
...     SimContext.NUM_INSTANCES: 1,
...     SimContext.WORLDS_PER_INSTANCE: 1,
...     SimContext.GEOMS: ["floor", "foot"],
...     SimContext.CONTACTS: tuple(
...         wp.array(x, dtype=t, device="cpu")
...         for x, t in [([1], int), ([0], int), ([[0, 1]], wp.vec2i), ([0.25], float), ([[0, 0, 0]], wp.vec3)]
...     ),
... }
>>> far, allowed = np.ones((1, 1, 2)), np.array([[[[0.0, 0.0, 0.1]]]])  # a disc (x, y, r) around the foot
>>> terms = [
...     ContactTask(["foot"], np.zeros((1, 1, 2)), np.full((1, 2, 1), 0.1), weight=2.0),  # radius over time
...     ContactTask(["foot"], far, np.zeros((1, 1)), penalty=0.5, margin=0.3),
...     ContactTask(["foot"], far, np.zeros((1, 1)), allowed, penalty=0.5, margin=0.3),
...     ContactTask(["foot"], far, np.zeros((1, 1)), weight=2.0, box=(1.0, 1.0)),  # (0, 0) is in the box
...     ContactTask(["foot"], far, np.zeros((1, 1)), weight=2.0, box=(1.0, 1.0), height=(0.1, 1.0)),  # too low
... ]
>>> out = wp.zeros(5, dtype=float, device="cpu")
>>> for i, term in enumerate(terms):
...     term.resolve(None, context)
...     term.compute_weighted_cost(None, out_cost=out[i : i + 1])
>>> assert np.allclose(out.numpy(), [-2.0 * 0.75, 0.5, 0.0, -2.0 * 0.75, 0.0])
Parameters
geomsSequence[str]required
targetnp.ndarrayrequired
radiusnp.ndarrayrequired
allowedOptional[np.ndarray]= None
weightfloat= 1.0
penaltyfloat= 0.0
marginfloat= 0.0
boxOptional[Sequence[float]]= None
heightOptional[Sequence[float]]= None
Members
attributeinherited from Task[source]
ContactTask.SUPPORTS_CUDA_GRAPH: bool = True
attributeinherited from Task[source]
ContactTask.SUPPORTS_AUTODIFF: bool = True

False when the residual kernels have no usable tape adjoint; autodiff then folds in the analytic JᵀR.

attributeinherited from Task[source]
ContactTask.var_key: Enum = VarKey.ROBOT
attributeinherited from Task[source]
ContactTask.name: str = ''

Label a config gave this term, so config.task(name) finds it again.

attributeinherited from Task[source]
ContactTask.robot: Any = None

The robot a built task reads; None on a template.

attributeinherited from Task[source]
ContactTask.weight: Any = 1.0
propertyinherited from Task[source]
ContactTask.active: bool

Whether the term adds cost; helpers skip templates whose weights are all zero (None weighs 1).

propertyinherited from Task[source]
ContactTask.parallel_safe: bool

Whether the term may run on its own CUDA stream; one that reads another term's results runs all in order.

methodinherited from Task[source]
ContactTask.initial_vars(
num_candidates: int,
device: Any
) -> Dict[Enum, Var]

Variables this task adds beside the helper's, at their nominal values; usually none.

Parameters
num_candidatesintrequired
deviceAnyrequired
Returns
Dict[Enum, Var]
methodinherited from Task[source]
ContactTask.needs() -> set

Return the Context members a template reads, including those of the tasks it wraps.

Example

python
>>> class Inputs(Context):
...     STEP = 0
>>> class Step(Task):
...     def __init__(self, dt):
...         self.dt = dt
>>> Step(dt=(Inputs.STEP, 2.0)).needs() == {Inputs.STEP}
True
Returns
set
propertyinherited from Task[source]
ContactTask.cost_name: str

Return the snake-case task name used in cost reports.

propertyinherited from Task[source]
ContactTask.dense_task: Task

Return the per-pose task this term applies, which is the term itself unless it wraps one.

methodinherited from Task[source]
ContactTask.precompute(
var_values: VarValues,
need_gradient: bool = False
)

Prepare shared state before this term is evaluated.

Parameters
var_valuesVarValuesrequired
need_gradientbool= False
method[source]
ContactTask.resolve(
var_values: Optional[VarValues],
context: Mapping[Context, Any]
)

Look up the geoms and build the device targets and discs.

Parameters
var_valuesOptional[VarValues]required
contextMapping[Context, Any]required
method[source]
ContactTask.compute_weighted_cost(
var_values: Optional[VarValues],
out_cost: wp.array
)
Parameters
var_valuesOptional[VarValues]required
out_costwp.arrayrequired
robokit.terms.GraspTask(
gripper: Union[str, int],
obj: Union[str, int],
offset: np.ndarray,
weight: np.ndarray,
tolerance: Union[float, np.ndarray] = 0.0,
object_frame: bool = True
)
Bases: SimResidualTask

Per step, weight[k] times the gripper's distance to body obj plus offset[k], less tolerance, at least 0.

Example

python
>>> context = {  # the hand at (0.1, 0, 0.2), a box at the origin turned 90 degrees about z
...     SimContext.CLOCK: wp.array([0, 0, 1], dtype=int, device="cpu"),
...     SimContext.NUM_INSTANCES: 1,
...     SimContext.WORLDS_PER_INSTANCE: 1,
...     SimContext.BODIES: ["world", "hand", "box"],
...     SimContext.BODY_POSITION: wp.array([[[0, 0, 0], [0.1, 0, 0.2], [0, 0, 0]]], dtype=wp.vec3, device="cpu"),
...     SimContext.BODY_ROTATION: wp.array(
...         [[np.eye(3), np.eye(3), [[0, -1, 0], [1, 0, 0], [0, 0, 1]]]], dtype=wp.mat33, device="cpu"
...     ),
... }
>>> offset, out = np.tile([0.0, -0.1, 0.2], (2, 1)), wp.zeros(2, dtype=float, device="cpu")
>>> terms = [GraspTask("hand", "box", offset, np.ones(2)), GraspTask(1, 2, offset, np.ones(2), 0.01, False)]
>>> for i, term in enumerate(terms):  # in the box's frame the hand is on target
...     term.resolve(None, context)
...     term.compute_weighted_cost(None, out_cost=out[i : i + 1])
>>> assert np.allclose(out.numpy(), [0.0, np.hypot(0.1, 0.1) - 0.01], atol=1e-6)
Parameters
gripperUnion[str, int]required
objUnion[str, int]required
offsetnp.ndarrayrequired
weightnp.ndarrayrequired
toleranceUnion[float, np.ndarray]= 0.0
object_framebool= True
Members
attributeinherited from Task[source]
GraspTask.SUPPORTS_CUDA_GRAPH: bool = True
attributeinherited from Task[source]
GraspTask.SUPPORTS_AUTODIFF: bool = True

False when the residual kernels have no usable tape adjoint; autodiff then folds in the analytic JᵀR.

attributeinherited from Task[source]
GraspTask.var_key: Enum = VarKey.ROBOT
attributeinherited from Task[source]
GraspTask.name: str = ''

Label a config gave this term, so config.task(name) finds it again.

attributeinherited from Task[source]
GraspTask.robot: Any = None

The robot a built task reads; None on a template.

attributeinherited from Task[source]
GraspTask.weight: Any = 1.0
propertyinherited from Task[source]
GraspTask.active: bool

Whether the term adds cost; helpers skip templates whose weights are all zero (None weighs 1).

propertyinherited from Task[source]
GraspTask.parallel_safe: bool

Whether the term may run on its own CUDA stream; one that reads another term's results runs all in order.

methodinherited from Task[source]
GraspTask.initial_vars(
num_candidates: int,
device: Any
) -> Dict[Enum, Var]

Variables this task adds beside the helper's, at their nominal values; usually none.

Parameters
num_candidatesintrequired
deviceAnyrequired
Returns
Dict[Enum, Var]
methodinherited from Task[source]
GraspTask.needs() -> set

Return the Context members a template reads, including those of the tasks it wraps.

Example

python
>>> class Inputs(Context):
...     STEP = 0
>>> class Step(Task):
...     def __init__(self, dt):
...         self.dt = dt
>>> Step(dt=(Inputs.STEP, 2.0)).needs() == {Inputs.STEP}
True
Returns
set
propertyinherited from Task[source]
GraspTask.cost_name: str

Return the snake-case task name used in cost reports.

propertyinherited from Task[source]
GraspTask.dense_task: Task

Return the per-pose task this term applies, which is the term itself unless it wraps one.

methodinherited from Task[source]
GraspTask.precompute(
var_values: VarValues,
need_gradient: bool = False
)

Prepare shared state before this term is evaluated.

Parameters
var_valuesVarValuesrequired
need_gradientbool= False
methodinherited from SimResidualTask[source]
GraspTask.compute_weighted_cost(
var_values: Optional[VarValues],
out_cost: wp.array
)

Add each world's 0.5 |r|^2.

Parameters
var_valuesOptional[VarValues]required
out_costwp.arrayrequired
attributeinherited from ResidualTask[source]
GraspTask.residual_weight: Optional[Union[float, Sequence[float], np.ndarray]]
attributeinherited from ResidualTask[source]
GraspTask.device: Any = None

Device of the task's Warp buffers; None before init_buffers.

methodinherited from ResidualTask[source]
GraspTask.init_buffers(
device: wp_device_type
)

Allocate Warp buffers on device; a task without any keeps none.

Parameters
devicewp_device_typerequired
propertyinherited from SimResidualTask[source]
GraspTask.residual_dim: int
methodinherited from SimResidualTask[source]
GraspTask.compute_weighted_residual(
var_values: Optional[VarValues],
out_residual: wp.array,
row_offset: int = 0
) -> wp.array
Parameters
var_valuesOptional[VarValues]required
out_residualwp.arrayrequired
row_offsetint= 0
Returns
wp.array
methodinherited from ResidualTask[source]
GraspTask.compute_weighted_jacobian_analytic(
var_values: VarValues,
args: Any = (),
out_jacobian: Optional[wp.array] = None,
row_offset: int = 0,
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
out_jacobianOptional[wp.array]= None
row_offsetint= 0
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
GraspTask.compute_weighted_jacobian(
var_values: VarValues,
args: Any = (),
kwargs: Any = {}
) -> wp.array
Parameters
var_valuesVarValuesrequired
argsAny= ()
kwargsAny= {}
Returns
wp.array
methodinherited from ResidualTask[source]
GraspTask.accumulate_normal_equations(
var_values: VarValues,
costs: wp.array,
JtJ: Optional[wp.array] = None,
Jtr: Optional[wp.array] = None
)

Accumulate this term into shared normal-equation buffers.

Lifecycle

  1. Prepare term state and reusable buffers.
  2. Evaluate residuals and, for a build step, the Jacobian.
  3. Reduce into cost, JᵀJ, and Jᵀr.
Parameters
var_valuesVarValuesrequired

Optimization variables.

costswp.arrayrequired

Shared per-batch cost buffer.

JtJOptional[wp.array]= None

Optional shared normal matrix. Omit for a cost-only step.

JtrOptional[wp.array]= None

Optional shared normal-vector buffer. Omit for a cost-only step.

method[source]
GraspTask.resolve(
var_values: Optional[VarValues],
context: Mapping[Context, Any]
)

Look up both bodies and build the device schedule.

Parameters
var_valuesOptional[VarValues]required
contextMapping[Context, Any]required