Skip to content

The sim

The sim is MuJoCo standing where the drives stand, and pip install 'anvil-robotics[sim]' brings it. The same script runs against it and against a real arm: anvil.hw.SimHw takes the place of anvil.hw.DamiaoHw, and nothing above the hw interface can tell which it got.

It is a robot, not a step function

The world comes first, as the workcell does on the bench: anvil.sim.World starts a sim, or joins one already running, and stands your robot in a scene with its drives switched off, on the clock you choose. SimHw then takes the robot over, the way DamiaoHw opens its CAN links:

world = anvil.sim.World(robot=robot, scene="table")
hw = anvil.hw.SimHw(world=world)              # on metal: anvil.hw.DamiaoHw(buses=anvil.hw.buses(robot=robot))

Left without a scene, a world stands the robot on what its workcell stands on, RobotConfig.stand: a shipped workcell without leaders on the lift, a yam pair on the bracket it carries, whose pole camera, pole_cam, the sim renders as any other.

anvil.sim.scenes() lists the scenes the sim ships. Two set out things to play with on a workbench before the robot, standing on the lift: "workbench" has Anvil's Devbox 2.0, a leader handle and two camera modules, to pick up or push off the edge, and "fruit_bowl" a bowl of fruit and a cutting board. Each prop is a loose object the arms can grasp, knock over and drop, and that World.place puts back. The bench stands 0.9 m tall, and the world raises the lift as far as RobotConfig.lift_raised_m says: a shipped workcell lowers it all the way under an OpenArm, whose hands hang from its shoulders, and raises it all the way under yams, which work from their base, so both reach down onto the bench. A yam pair stands on the bracket its workcell's stand carries, in these scenes as on the bare lift.

A world holds one robot; another robot is another world. The sim behaves as the drives do, so code that only works because a sim is forgiving fails here first:

  • the drives come up disabled and have to be enabled;
  • each drive runs its own impedance law on the last setpoint it was sent, and holds it if nothing new arrives;
  • a setpoint past what a drive's frame can carry arrives clipped, and a torque past what its motor delivers arrives at that maximum;
  • the robot collides, with itself and with the scene it stands in.

A loop that stops leaves the drives disabled and the robot where it falls, as an arm does when its loop stops; the world goes on. The next SimHw takes it over as it stands.

The sim models no friction and no rotor inertia yet, so it is optimistic about back-driving an arm and about starting one from rest.

Two clocks

The world's clock is chosen when it is set up:

  • Free-running, the default and what the hardware keeps: the world steps against wall time whatever the loop is doing, so a loop that goes quiet is a robot still being commanded.
  • Lockstep, World(lockstep_hz=...): the world steps once per command and not otherwise, so the same commands give the same run twice, whatever else the machine is doing. A test wants this one.

A runtime over a lockstep world still ticks against the wall clock, at the world's rate, unless it is built stepped: then it ticks only when advance asks. A call that waits on the loop runs no ticks of its own, so on a stepped runtime play times out unless another thread advances the loop meanwhile.

Cameras

Every camera link of the robot's URDF is a camera in the sim, looking out of the lens the meshes draw, watching from the moment the world is set up. anvil.camera.SimCamera streams one as JPEG frames, as a USB camera would. A frame falls on a grid of the world's time and is stamped with the tick it shows, so a recording places it beside that tick. Free-running, a frame the sim has no time to render is dropped, and the gap in the frame's sequence number says so; lockstep renders every one, so a run sends the same frames twice.

Rendering is offscreen, through EGL, which needs no display and no GPU. On a machine with neither, install Mesa's:

sudo apt install libegl1 libegl-mesa0 libgl1-mesa-dri libopengl0

The cameras and the window draw the robot as Anvil Forge does: each part shaded by its finish under a studio's lights, with its reflections, against a dark backdrop, and a line on every crease. Where OpenGL renders on the CPU, as Mesa's does, the key light casts no shadow: drawing the robot a second time for it would cost each camera most of its frame rate there. Renderer.MUJOCO draws with MuJoCo's own renderer instead. Name it as a World starts its server, World(robot=..., renderer=Renderer.MUJOCO); as a server is started by hand, anvil-robotics sim --renderer mujoco; or, for a server started by code you do not change, with ANVIL_SIM_RENDERER=mujoco in the environment it starts from. A renderer named to the World or on the command line wins over the environment.

Watching the world

MuJoCo's own window shows the whole world, the robot and the scene it stands in: orbit, pan and zoom with the mouse, and pick a body with a double-click to push it with Ctrl and a drag. World(viewer=True) opens it as the world is set up, and World.show_window opens it on a world already running, or again after it was closed. It opens on the machine the sim server runs on, so that machine needs a desktop session; without one the call says so and the world runs on.

A sim on another machine

The sim stands in for metal, so the machine that would drive the metal need not also simulate it, and the window is nicer on the machine you are sitting at. Start a sim there:

anvil-robotics sim --listen 0.0.0.0:7447 --viewer

and give its address to World on the machine running the loop, with the same release of anvil on both:

world = anvil.sim.World(robot=robot, address="10.0.0.4:7447", scene="table")

What the world answers

The world answers questions beside whatever drives it, never costing the loop a tick: World.contacts says what is touching now and everything that has touched since the world was set up, looked at every physics step, so "nothing collided during the whole sweep" is one question asked at the end; World.place puts a loose object of the scene, a block on the table, where a run wants it; and World.hold holds joints of the robot still where a run says, a hand on an arm nothing drives, which is how a run moves a leader the operator would move by hand.