Installation

roqsim consists of multiple packages:

  • roqsim — the ROS-free framework core (engine, plugins API, drivers, the roqsim command tree).

  • roqsim_sensors — generic sensor plugins and models (lidar, OAK-D RGB-D, RealSense, force/torque, fiducial markers).

  • roqsim_assets — the prop library (furniture, containers, fittings) and the scene tools.

  • roqsim_scenes — baked MJCF scenes and the world/floorplan generators.

  • roqsim_mobile — wheeled bases: floorplan, spawn_robot, diff-drive, omni-drive, and the base models (TurtleBot 3/4, Husky, Jackal).

  • roqsim_manipulation — manipulator plugins (spawn_arm, arm_controller, cartesian_admittance); roqsim_manipulation_assets — the arm and gripper models.

  • roqsim_mobile_manipulation — robots that are a base and an arm (frankie, tiago_pro); the one package depending on both families.

  • roqsim_humanoid / roqsim_quadruped — legged robot families; roqsim_walker — kinematic pedestrians (dynamic obstacles, not a robot family).

  • roqsim_scene_builder / roqsim_webctrl — the human-in-the-loop scene windows and the web control UI.

  • scenario_execution_roqsim — the OpenSCENARIO 2 vocabulary (import osc.roqsim): what a scenario can ask a running simulation and what it can break in one. Named for scenario-execution’s own convention rather than ours, because that is the project it plugs into.

  • ros2_ws/ — colcon packages: roqsim_ros_bridge (the ROS 2 bridge) and roqsim_nav2_example (a nav2 example + test).

Install what you need: every family package pulls its own dependencies, and none of them depends on another family. make venv installs them all in editable mode.

Manual install

python3 -m venv --system-site-packages .venv
.venv/bin/pip install -e "roqsim[test]" -e "roqsim_sensors[test]" -e "roqsim_mobile[test]"

ROS 2 (optional)

For the ROS 2 bridge and the nav2 example, source a ROS 2 distribution (Jazzy) and build the workspace:

source /opt/ros/jazzy/setup.bash
make build-ros          # colcon build ros2_ws (only runs when ROS is sourced)
source ros2_ws/install/setup.bash

External assets (fetched + converted, not committed)

Some vendor CAD/mesh sources have unclear redistribution terms, so instead of committing derived files roqsim regenerates them locally from their sources and git-ignores both. They are declared in external/external_assets.yaml — per resource: sources with URLs, a conversion script and its dependencies, and the generated target paths — and driven by:

make external-list                 # show resources, their sources and generated targets
make external-resources            # fetch every source + run its conversion into the targets
make external-resources RESOURCE=livox_mid360_meshes BLENDER=/path/to/blender   # one resource
make external-sync-gitignore       # rewrite the managed .gitignore block from the manifest
make add-external-resource ARGS="--name X --source URL::PATH[::manual] --target PATH ..."

Two resources are declared today, spanning the two shapes the schema supports:

livox_mid360_meshes

Two Livox STEP files (placed under external/sources/livox/) are tessellated with Open CASCADE (cascadio) and processed in Blender into the housing / dome / FOV meshes. Its sources are manual — they sit behind Livox’s product page — so the runner tells you where to put the file rather than downloading it.

spot_locomotion_policy

The NVIDIA-licensed Boston Dynamics Spot policy (spot_policy.pt + spot_env.yaml). A download-only, optional resource: fetched anonymously from NVIDIA’s public bucket, and because it is only needed to run Spot the fetch is fail-soft — a network error warns and skips instead of breaking make venv. make venv fetches it through this system, and python -m roqsim_quadruped.policy.fetch_policy is a thin wrapper around the same runner.

The tool venv (external/.venv-tools/), the fetched sources and the generated outputs are all in the managed .gitignore block; only the manifest, the runner and the conversion scripts under external/convert/ are tracked.

Interpreter note

Because the venv uses --system-site-packages, run ROS entry points with the venv interpreter so roqsim is importable, e.g.:

source /opt/ros/jazzy/setup.bash
source ros2_ws/install/setup.bash
.venv/bin/python -m roqsim_ros_bridge.run_bridge \
    --world ros2_ws/src/roqsim_ros_bridge/worlds/turtlebot_ros2.yaml

Container images

Two images are published to the GitHub Container Registry on every push to main, each as a multi-architecture index covering linux/amd64 and linux/arm64 — so the same reference works on an x86 node and on an arm64 machine (an Apple Silicon laptop, a Graviton/Ampere node, an arm64 robot host) with nothing to select by hand:

ghcr.io/cps-test-lab/roqsim

The lean core: ROS-free, headless MuJoCo, with the sensor / mobile / manipulation (plugins and the arm and gripper models) / mobile-manipulation / scenes / walker packages. ENTRYPOINT is the roqsim command tree, so the image is used the way the CLI is:

docker run --rm ghcr.io/cps-test-lab/roqsim --help
docker run --rm -v "$PWD:/work" -w /work ghcr.io/cps-test-lab/roqsim \
    sim my_world.yaml --headless --seconds 10
ghcr.io/cps-test-lab/roqsim-ros

ROS 2 Jazzy, nav2, MoveIt and rviz2, every roqsim_* package, and a colcon-built ros2_ws (bridge, nav2 example, walker_ros). Its entrypoint sources ROS and the workspace, then execs what you pass:

docker run --rm ghcr.io/cps-test-lab/roqsim-ros ros2 pkg list

Neither image sets MUJOCO_GL: which offscreen backend works is a property of the node the image lands on, not of the image, so both backends are installed and roqsim.gl.select_offscreen_gl() picks at import. Baking a value in would override that choice with a guess — see Architecture & porting playbook.

What is published, and for how long

Only two kinds of tag exist: latest, which follows main, and released versions from a v* git tag (1.4.2 and 1.4). A pull request builds and smoke-tests every architecture but pushes nothing, so no branch or PR tag is ever created.

The package listing also holds untagged manifests, and that is normal rather than debris: each architecture is pushed as its own untagged manifest and a tag is the index that references them, so half of every multi-arch image is untagged by construction. A weekly job (cleanup_untagged_images.yml) removes untagged manifests that no tagged index references and that are older than a grace window — it walks the tagged manifests first, because deleting an index’s children would break the index. The grace window exists because untagged is not the same as unused: anything that pinned an image by digest still needs that digest after the tag has moved on.

Layer caching goes to a roqsim-buildcache package rather than to the GitHub Actions cache, which is capped per repository and scoped per git ref — multi-GB image layers stored there thrash to a zero hit rate and starve the other workflows of the same cache. The cache tag is overwritten on each push, so its orphaned blobs are cleaned up by the same weekly job.

Building them yourself

container/build.sh wraps docker build with the repo root as its context. It builds for the host by default, which is what a local test wants:

./container/build.sh --image roqsim
./container/build.sh --platform linux/arm64 --image roqsim   # one explicit architecture

--multiarch instead builds every architecture the image is published for, reading the list from container/platforms.env. It requires --push and --project: a multi-platform build produces an index, and since the local daemon can hold only one image there is nowhere but a registry for the result to go.

./container/build.sh --multiarch --project ghcr.io/cps-test-lab --push

The architecture policy

container/platforms.env is the single source of truth for which architectures each image is built for, read by both container/build.sh and .github/workflows/image.yml — so a local build and a CI build cannot disagree about what an image is. It also maps each platform to the GitHub runner native to it, because CI builds each architecture on its own hardware and merges the results into one manifest rather than emulating under QEMU.

That is a correctness measure before it is a speed one. Asking buildx for an architecture the base image does not publish does not fail — it takes the base’s only architecture, labels it with the one requested, and pushes an image that dies with Invalid ELF image for this architecture the first time anything execs inside it, at run time and on someone else’s machine. A native build has no second architecture present to mislabel, and CI additionally runs each freshly built image on its own native hardware before pushing, which is the only check that catches this class of fault: a mislabelled image builds, pushes and inspects perfectly.

Adding an architecture is therefore one edit in container/platforms.env (its platform line plus a RUNNER_ mapping) and nothing in the workflow. make check fails if a platform has no runner mapped, or if the workflow starts naming an architecture of its own. The ceiling is what the base image publishes: python:*-slim and the official ros:*-ros-base are both multi-architecture indexes, which is what makes arm64 available here at all.