Skip to main content

hyperdrone

Drone simulation stack for RLtools: raytracing renderer, L2F multirotor dynamics, and environment setup under one Python package.

Subpackage What it is
hyperdrone.render Raytracing renderer (OptiX / Metal / Vulkan / generic CPU). Self-contained — no dependency on drone dynamics.
hyperdrone.dynamics Vectorized, stateful L2F multirotor simulator (Sim); cpu and cuda variants.
hyperdrone.env The RL environment: the C++ MultiEnvironment<hyperdrone::World> driven through the exact rl_tools batch verbs.
hyperdrone.gym Optional Gymnasium VectorEnv adapter over hyperdrone.env (pip install "hyperdrone[gym]").

Native components are JIT-compiled per set of compile-time constants (resolution, camera count, drone count, ...) into a persistent cache; later uses load the cached library directly. render and dynamics are peers that never import each other — the coupling surface is a plain DLPack tensor of packed camera bases, device-resident on OptiX + CUDA. Rendering uses a shared scene module and a directly bound Python extension per renderer configuration; scene and asset-pool objects can be reused across configurations. Environment configurations are also directly bound extensions, sharing their C++ implementation with the golden-rollout generator.

Notebooks

Notebook Open in Colab
Overview Open in Colab
HyperDrone Open in Colab
Benchmark Open in Colab
Renderer Open in Colab
Orbiter Open in Colab

Install

From an rl-tools checkout (development):

.venv/bin/pip install -e interface/python/hyperdrone

From an sdist (self-contained — bundles the rl-tools headers and raytracing sources, no checkout needed):

pip install hyperdrone-<version>.tar.gz

Install the dependencies used by the packaged examples with:

pip install "hyperdrone[examples]"

Requirements: CMake >= 3.24, a C++17 compiler, and git for dependency downloads. GLB loading uses the Assimp 6.0.4 release archive with a pinned SHA-256, shared by render and env builds. Cached archives are reused offline and verified by CMake before extraction. CUDA + OptiX driver is required for the OptiX render backend and cuda dynamics, Vulkan dev + glslang for the VULKAN backend. The WEBGPU backend fetches a hash-pinned wgpu-native prebuilt at first build (no extra system packages). Nothing beyond the build toolchain for GENERIC + cpu.

Environment

Variable Meaning
HYPERDRONE_RENDER_BACKEND OPTIX | METAL | VULKAN | WEBGPU | GENERIC | AUTO (AUTO preference: OPTIX > METAL > VULKAN > WEBGPU > GENERIC)
HYPERDRONE_CACHE_DIR root for CMake build trees and downloaded build dependencies (default ~/.cache/hyperdrone)
HYPERDRONE_RLTOOLS_ROOT rl-tools source root override (default: enclosing checkout, else the vendored tree)
HYPERDRONE_PROCTHOR_PATH examples only: local ProcTHOR scene override (otherwise downloaded and cached)
HYPERDRONE_SKIP_BUILD skip the CMake staleness check when the artifacts already exist
HYPERDRONE_OFFLINE forbid network during builds (requires seeded/vendored dependencies)
HYPERDRONE_BUILD_JOBS parallel build jobs (default 5)

Each component, variant and native CPU instruction set gets its own build tree (render-optix-*, dynamics-cuda-*, ...); switching never invalidates another's cache. All configure/build steps run under a per-tree file lock, so many worker processes can share one cache safely. CMake FetchContent sources and native build state live in <cache>/.dependencies; the native build does not write generated files into an editable checkout or installed package. CUDA trees also distinguish the native GPU architecture or explicit CUDAARCHS target.

At the default verbosity, each successful renderer allocation reports the selected backend on stderr, for example #rl_tools::rendering::raytracing: backend=metal. The Renderer.backend and MultiEnvironment.render_backend properties provide the same lowercase name from the compiled renderer type. hyperdrone.set_verbosity(level) selects the native log level, error, warning, info (default, includes the backend line) or debug, for loaded and future native modules alike. Changes apply to subsequent diagnostic checks, including during native work on other threads; a diagnostic already checked may finish at its previous level. Application stdout is independent of diagnostic verbosity.

Rendering

import numpy as np
from hyperdrone import render
from hyperdrone.examples.data import procthor_scene_path

scene = render.load_scene(procthor_scene_path(), fidelity="high")

renderer = render.Renderer(width=320, height=240, num_cameras=4, output=("rgb", "depth"), fidelity="high")
renderer.init(scene)

camera = renderer.camera(position=(0, 0, 1.5), look_at=(1, 0, 1.5), fov=80)
renderer.set_cameras(np.repeat(camera[None], 4, axis=0))
renderer.render()

rgb   = renderer.frame()    # (4, 240, 320, 4) uint8
depth = renderer.depth()    # (4, 240, 320) float32

The renderer operates in the L2F FLU frame: +X forward, +Y left, +Z up; GLB assets are swizzled at load time. Procedural scenes (render.Scene() + render.Object / render.Mesh / render.SceneLight), segmentation output, overlays (per-camera dynamic content via AssetPool + spawn/attach), motion blur, and anti-aliasing follow the same API as before under hyperdrone.render.*.

Converted ProcTHOR scenes carry ambient (0.6, 0.6, 0.6) and a black background in scenes[].extras.rl_tools.environment (mode: "solid", ambient and background are nonnegative linear RGB triples). Scene loading imports these settings; importing objects or assemblies leaves the parent environment alone. Ambient affects high and veryhigh, approximating indirect illumination without light bounces. Override imported settings with scene.set_environment(...) before renderer.init(scene).

render() produces all image outputs selected at construction; render_launch() and render_sync() split submission from waiting. Collision probes run separately through probe() or probe_launch() / probe_sync(), after generate_probe_directions(), and are read with collisions(). num_probes sets their count per camera (default 1); zero compiles the probe pass out and makes probe operations raise. Probe 0 follows the camera's principal axis; the remaining directions are a fixed Fibonacci set in the camera's FLU frame (x forward, y image left, z image up), so the pattern turns with the camera.

Zero-copy I/O (DLPack)

renderer.frame()                  # snapshot copy (safe to keep)
renderer.frame(out=rgba_array)    # fill an existing writable, aligned, C-contiguous uint8 array
rgba = torch.from_dlpack(renderer.frame_dlpack())
# live uint8 (num_cameras, height, width, 4), zero-copy GPU tensor on OptiX
packed = torch.from_dlpack(renderer.frame_raw_dlpack())
# same memory as packed uint32 (num_cameras, height, width)

Inputs accept any DLPack producer. CUDA-resident camera input (OptiX): set_cameras dispatches on __dlpack_device__, so a CUDA tensor — a torch GPU tensor, a CuPy/JAX CUDA array, or Sim.camera_bases_dlpack(aspect=renderer.aspect) — is handed over device-to-device on the render stream, fully async; stream= takes the producer's cudaStream_t handle for event-ordered hand-off. The renderer retains asynchronous inputs until the corresponding render/probe synchronization; do not mutate their contents until that work finishes.

Dynamics

from hyperdrone import dynamics

sim = dynamics.Sim(4096, parameters={"dynamics": {"dynamics": {"model": "x500_sim"}}}, seed=0, device="cuda")
sim.reset()                          # nominal state; reset(sample_states=True) samples the configured distribution
sim.step(actions)                    # (N, 4) float32 in [-1, 1], host or CUDA tensor
sim.state["position"]                # independent host copy
sim.state_dlpack("position")         # live backend buffer
sim.observe()                        # (N, 18): position, row-major body-to-world rotation, velocities
sim.set_instance_parameters({"dynamics": {"dynamics": {"mass": 1.2}}}, mask=selected)
sim.instance_parameters()            # per-drone values; overrides last until each drone's next reset
sim.parameters                       # fresh copy of the constructor's resolved shared parameters

cameras = sim.camera_bases_dlpack(fov=100, aspect=renderer.aspect)
renderer.set_cameras(cameras, stream=sim.stream)

Sim defaults to device="cpu"; choose "cuda" or "auto" explicitly. Seed at construction, then reset(mask=None, *, sample_states=False) advances the sampling sequence only for selected drones. Observation noise uses a separate stream from stepping. The shared parameter schema matches the environment's "dynamics" tree; integration dt is fixed at construction and available as read-only sim.dt. Per-drone overrides affect dynamics.dynamics; a reset restores that drone's nominal parameters. motor_command is the rotor command in the model's action-limit units, not measured RPM. A process can load one dynamics variant.

Host accessors return independent snapshots; names ending in _dlpack() expose live storage. Drive each object on its construction CUDA device, and finish consuming a borrowed buffer before the producer overwrites it. Sim orders DLPack exports onto the consumer's stream. Camera bases have shape (N, 12) and concatenate position, dir_00, dir_du, and dir_dv world-FLU vectors; the pixel-centre ray is normalize(dir_00 + (x+0.5)/width * dir_du + (y+0.5)/height * dir_dv); aspect is required when constructing them.

RL environment

import numpy as np
from hyperdrone.env import EnvConfig, MultiEnvironment

config = EnvConfig(num_environments=1, instances_per_environment=1024, width=32, height=32, task="target_frame")
env = MultiEnvironment("scenes/", config=config, seed=0)

mask = np.ones(env.total_instances, dtype=bool)
env.reset(mask)                    # resample parameters + states where mask is set
observations = env.observe()       # (total, observation_dim) float32
env.step(actions)                  # (total, action_dim) float32 in [-1, 1]
rewards, terminated = env.rewards(), env.terminated()
critic_input = env.observe_privileged()
env.rotate_scene()                 # deterministic scene rotation; reset all instances after
env.observation_layout             # named blocks: which channels/values mean what

reset(mask) samples parameters and states immediately. observe() renders as needed, and each observation is acquired at most once per simulation step. Frame history advances once per simulation step, and repeated reads reuse that step's frame; skipped reads do not stretch camera exposure beyond shutter_fraction * dt. The training loop owns episode counters and chooses when to reset; the Gymnasium adapter below supplies same-step autoreset and time limits.

On the OptiX backend the World runs on the GPU (env.device == "cuda"): its RNG and every instance tensor live there, and every verb is enqueued on one CUDA stream, env.stream, which you bind to the stream your tensors live on. CUDA arrays (any DLPack producer on env.device_id) are then read in place and outputs written into out=, in stream order, with no copies or host synchronization:

import torch
env = MultiEnvironment("scenes/", config=config, seed=0, stream=torch.cuda.current_stream().cuda_stream)
observations = torch.empty((T, env.total_instances, env.observation_dim), device="cuda")
rewards = torch.empty((T, env.total_instances), device="cuda")
terminated = torch.empty((T, env.total_instances), dtype=torch.bool, device="cuda")
reset_mask = torch.ones(env.total_instances, dtype=torch.bool, device="cuda")
for t in range(T):
    env.reset(reset_mask)                     # CUDA bool mask
    env.observe(out=observations[t])          # written into the rollout buffer
    env.step(policy(observations[t]))         # CUDA float32 actions
    env.rewards(out=rewards[t])
    env.terminated(out=terminated[t])
    reset_mask = terminated[t]                # plus your episode limits

Order data produced on another stream yourself (env_stream.wait_stream(producer)), as between any two torch streams; setting env.stream orders the new stream after the old one's work. numpy arrays work with every World: they are staged through the environment and the call synchronizes. EnvConfig(device="cpu") keeps the World on the host; the visual-inertial localization task (no CUDA verbs yet) and spec_header Worlds (whose verbs may be host loops) default to it.

EnvConfig is the compile-time part (the JIT key); the runtime part is MultiEnvironment(..., parameters={"dynamics": {"dynamics": {"model": "x500", "mass": 2.1}}, "cameras": [{"fov": 90}]}), a partial dict merged over the nominal parameters as the base of every drone (the specification's randomization ranges apply on top at reset). env.parameters returns drone zero's resolved nominal parameters, including its entry-specific overrides: "dynamics" is the l2f parameter set, where a "model" key loads an l2f registry entry (hyperdrone.dynamics.MODELS) before the rest is merged, and each entry in "cameras" holds mount.pose (position and orientation, a (w, x, y, z) quaternion mapping the camera's FLU axes into the body frame) and the horizontal fov in degrees. env.instance_parameters() returns what each instance is using in the current episode (after randomization), and env.set_instance_parameters(overrides, mask) changes it until that instance's next reset (one dict for all selected instances or one per instance); only the drone itself varies (dynamics.dynamics, dynamics.imu, cameras, and "drone", which starts from another table entry's nominal and swaps the rendered body).

Several drones fly in one environment through the drone table, MultiEnvironment(..., drones=[{"asset": x500, "parameters": {...}}, {"asset": crazyflie, "parameters": {"dynamics": {"dynamics": {"model": "crazyflie"}}}}, ...]): every agent samples one entry uniformly at each group reset, independently of the scene its environment shows, so every scene meets every drone. An entry pairs the body/prop_* GLB rendered into the instance's cameras with a partial parameter dict merged over the shared constructor base (the per-drone keys dynamics.dynamics, dynamics.imu, cameras); the same GLB may back several dynamics entries, distinct GLBs load once and all of them need the same part count (an agent's body swaps in place at reset, on the GPU for a CUDA World). The table holds up to env.max_drone_types entries (8, a spec_header World sets its own), env.drones returns the resolved native table, and each agent's "drone" value in env.instance_parameters() indexes it. env.self_visible and env.max_drone_types report the compiled world's capabilities, including custom spec_header worlds. drone_asset=X is the one-entry table. The six rigged assemblies in hyperdrone.examples.data.RIGGED_MODELS use matching l2f registry entries; each prop spins with the dynamics rotor at its position whatever rotor order the entry uses.

With EnvConfig(preset="x500_fpv", num_agents=7, num_camera_agents=7), each instance is a group of seven mutually visible drones; groups share scene geometry but remain isolated episodes. instance_parameters() returns {"agents": [...]} per group, and set_instance_parameters({"agents": [{"drone": i} for i in choices]}, mask) assigns models independently ({} leaves an agent unchanged). Single-agent entries keep the flat schema. Scene placement, exposure, shutter fraction, reset, reward and termination remain group-wide; all agents share the integration timestep.

num_camera_agents defaults to 1 and selects agents [0, num_camera_agents) to produce images; all num_agents drones still receive actions and appear in state, privileged observations and rendered geometry. EnvConfig(num_agents=7, num_camera_agents=1, preset="x500_fpv", num_cameras_per_agent=2) gives agent 0 two synchronized cameras. Set mounts and horizontal FOVs with parameters={"cameras": [fpv, chase]}; each agent retains a list of exactly num_cameras_per_agent entries, and {} preserves a camera’s existing values in a partial override. All cameras share resolution, output channels and capture timing. env.num_views is env.num_camera_agents * env.num_cameras_per_agent, ordered by agent then camera. Base-world frames() tiles these views vertically; env.frames().reshape(env.total_instances, env.num_camera_agents, env.num_cameras_per_agent, H, W, 3) exposes the axes without copying.

All environment semantics — reset, reward, termination, scene scheduling, observation composition — live on the C++ side (rl_tools::rl::environments::hyperdrone::MultiEnvironment<World>); the binding marshals tensors and nothing else, and a seeded host-World rollout is pinned bit-exact against the C++ verbs by a golden test. The scenes argument is a directory of .glb scenes (sorted corpus) or a list of references — .glb paths, conta:HASH strings, or conta store manifest entries {"description": ..., "hash": ...} — used as the corpus in list order and partitioned across environments; conta references (scenes, drone_asset=, assets={"gate": reference}) resolve through the conta package into the shared content-addressed cache, so a config pins the exact corpus on any machine. Configuration follows the C++ extension ladder: preset= names the platform ("crazyflie", "x500_fpv" — SELF_VISIBLE, pass a body/prop_* GLB via drone_asset=; hyperdrone.env.Rotorcraft(assembly, rotor_positions=..., rotor_torque_directions=...) is the same rig as a render.Rig for manual composition: every prop_* part spins about its hub, and given the dynamics rotor layout (sim.parameters["dynamics"]["dynamics"]) coordinate i is the phase of rotor i spinning against its reaction torque, so sim.state["motor_command"] supplies motor command values for a chosen visual spin calibration; without rotors the props follow part order and the Quad-X convention, front-right CCW — and "x500_fpv_imu", the same platform stepping at IMU rate), task= names the wrapper ("target_frame", "moving_gate", "visual_inertial_localization"), num_agents= enables multi-agent, num_camera_agents= selects the prefix with cameras, num_cameras_per_agent= sets their camera count, and EnvConfig(spec_header=...) pins an arbitrary C++ specification (a header defining hyperdrone_env_user::WORLD, hashed into the JIT key); its final C++ specification must define N_CAMERA_AGENTS explicitly. A spec header can go beyond constants to a full user-authored task wrapper — verb overloads registering and moving entities, compiled reward, termination: hyperdrone/examples/Orbiter.ipynb walks through one, and tests/env/test_orbiter.py compiles and flies its header. The environment's raytracing backend follows HYPERDRONE_RENDER_BACKEND like the renderer.

Entity wrappers share their own pose writes in prepare_entity_poses: render calls it before forwarding acquisition, and step calls it before forwarding when a frame is pending and history or flow requires its poses. The base World separates input production (prepare_render_inputs), temporal reset (reset_render_state, using prepared poses for flow) and advancement (advance_flow), and acquisition (render, which publishes transforms, updates acceleration, and scatters images into history). Pose helpers neither render nor call update; CUDA may fuse temporal bookkeeping into existing kernels without changing these semantics.

The target-frame task requires one agent with one camera and pre-renders its episode setups: a bank of one setup per instance (drone, scene placement, camera, brightness and the target perturbations) is rendered in one pass every TARGET_BANK_REFRESH_STEPS simulation steps (a compile-time task constant, default 32) and after a scene change, and resets adopt the bank's setups in turn and copy their frames, so a reset adds no render pass of its own; resets beyond the instance count within one refresh reuse setups. Each episode's drone and camera come from its setup, so set_instance_parameters rejects drone and camera overrides for this task; a replayed spawn re-renders its target frame.

EnvConfig(motion_blur_samples=8) enables camera and propeller motion blur for presets and tasks; valid counts are 1 (default, disabled), 2, 4, 8, 16, and 32. Custom spec headers control ENABLE_MOTION_BLUR and MOTION_BLUR_SAMPLES directly.

For repeatable shots, capture points = env.spawn_points() after a reset and save the returned dictionaries, then use env.reset(mask, spawn_points=points) with one entry per selected instance (a host mask). Each entry pins the active scene by content hash, one drone-table index per agent in "drone", scene translation/yaw, per-agent pose (xyz, wxyz), and shutter fraction. The reset uses constructor-supplied drone dynamics and camera mounts; velocity/motor initialization and task-specific randomization still follow their configured ranges. Scene changes remain explicit through rotate_scene(), which requires resetting every instance before further use; a shot for a different active scene is rejected.

Manual composition of Sim and Renderer (no MDP — rendering research, data generation) remains first-class: see python -m hyperdrone.examples.drone_flythrough for the wiring. ClearanceSampler lives in hyperdrone.render — it rides the renderer's collision probes and is deterministic given a seed.

Native spy

env.spy() opens a live camera mosaic and an independent spectator camera. Call update() on the main thread after your usual observe() or render() and before changing the environment; the spy reads existing observations without producing new ones. GUI events are processed only by update(), so call it regularly even when training is paused. Closing its window leaves the environment running; update() then returns False.

env.reset()
with env.spy() as spy:
    for _ in range(1000):
        observations = env.observe()
        spy.update()
        env.step(policy(observations))
        env.reset(env.terminated())
env.close()

Click a camera tile to select its member, instance, agent and camera, then click the spectator for mouse look. WASD moves, Q/E moves down/up, Shift accelerates, and Esc releases the mouse; the spectator pose is retained for each world independently. The spectator has its own renderer; camera capture and spectator refresh are capped at 20 Hz, and GUI work at 60 Hz. They still consume GPU bandwidth and rendering capacity. Use with or explicitly close the spy on the main thread before environment teardown. The Gymnasium adapter also exposes env.spy(); call update() after reset() or step().

The optional native dependencies are GLFW, desktop OpenGL and Dear ImGui (on Ubuntu: libglfw3-dev, libgl1-mesa-dev, libimgui-dev). Alternatively, supply an existing ImGui source checkout with RL_TOOLS_IMGUI_SOURCE_DIR=/path/to/imgui before the first JIT configure. After adding dependencies to an existing build, run cmake /path/to/cache/env-<backend>-<tag> (add -DRL_TOOLS_IMGUI_SOURCE_DIR=/path/to/imgui for a source checkout), unset HYPERDRONE_SKIP_BUILD, and restart Python; constructing the next environment rebuilds its native module. The build directory lives under HYPERDRONE_CACHE_DIR (default ~/.cache/hyperdrone); CMake reports HyperDrone spy availability. Ordinary environments remain usable when these dependencies are absent; env.spy() then explains the missing build feature. No display or window is opened until env.spy() is called.

Run a local-scene example with python -m hyperdrone.examples.spy /path/to/scene.glb.

C++ uses hyperdrone::Spy<hyperdrone::spy::Config<ENVIRONMENT>> with device-first malloc, init, update and free; link RLtools::HyperDroneSpy and close the spy before freeing its environment. The C++ example builds as example_hyperdrone_spy when the GUI dependencies are available. Both interfaces share the same capture, spectator and window implementation.

Visual-inertial localization

task="visual_inertial_localization" on preset="x500_fpv_imu" is the localization benchmark: fixed-length synchronized episodes stepping at IMU rate (env.dt), a camera frame every env.frame_stride steps, and the IMU sample of each step from env.observe_imu() (named by env.observation_layout_imu: accelerometer, gyroscope, frame age, new-frame flag). The task is autonomous: the RAPTOR autopilot built into it flies a waypoint route (its checkpoint is pinned by content hash and fetched through conta on first use), so action_dim is 0 and step() takes no actions. The privileged observation carries the current waypoint next to the dynamics state; the drone's own body is not rendered, matching the C++ benchmark harness. Resets must be all-or-none per environment.

config = EnvConfig(instances_per_environment=2, width=160, height=120, preset="x500_fpv_imu", task="visual_inertial_localization")
env = MultiEnvironment(scenes, config=config, seed=0)
env.reset()
for step in range(env.episode_step_limit):
    if step % env.frame_stride == 0:
        frames = env.frames()          # the estimator's camera frame
    env.step()
    imu = env.observe_imu()            # the estimator's IMU sample for this step
    truth = env.observe_privileged()   # position / rotation matrix / velocities / waypoint

python -m hyperdrone.examples.visual_inertial_localization runs an episode and scores IMU dead reckoning against the ground truth, the same protocol as the C++ demo.

Gymnasium

from hyperdrone.gym import VectorEnv   # pip install "hyperdrone[gym]"
env = VectorEnv("scenes/", config=EnvConfig(instances_per_environment=1024), seed=0)
observations, infos = env.reset()
observations, rewards, terminations, truncations, infos = env.step(actions)

The adapter resets instances that terminate or reach episode_step_limit within the same step. Returned observations start the new episode for those instances; no final observation is exposed. truncations flags time limits that are not terminations. The core packages never import gymnasium.

End-to-end example: python -m hyperdrone.examples.drone_flythrough; renderer benchmark: python -m hyperdrone.examples.benchmark (flag-compatible with the C++ benchmark counterpart).

Publishing

From the repository root, build and upload a release with:

interface/python/hyperdrone/scripts/publish.sh

Set the release version in hyperdrone/__init__.py; package metadata and JIT caches use that single source. The script uses the repository's .venv (creating it if needed), installs the build and upload tools, builds a bundled source archive and a py3-none-any wheel from that archive, checks both with Twine, and uploads both to PyPI. Authenticate through your existing Twine configuration (~/.pypirc, keyring, or TWINE_PASSWORD), or enter a PyPI API token when prompted.

Use interface/python/hyperdrone/scripts/publish.sh --build-only to build and check without uploading. Each run keeps its two artifacts in a fresh interface/python/hyperdrone/dist/release.* directory, so older builds are never included in an upload. Native components remain JIT-compiled on first use.

Tests

Use the repository .venv with editable interface/python/hyperdrone[gym] installed; point CONTA_ROOT at the local test assets.

HYPERDRONE_RENDER_BACKEND=GENERIC .venv/bin/python -m pytest interface/python/hyperdrone/tests -v

With the spy dependencies installed, run HYPERDRONE_TEST_SPY=1 xvfb-run -a .venv/bin/python -m pytest interface/python/hyperdrone/tests/env/test_spy.py to check the native GUI lifecycle and exact simulation equivalence; omit xvfb-run -a when using a desktop display.

tests/jit exercises the build infrastructure against a toy component in seconds; tests/test_architecture.py enforces the import DAG (render and dynamics never see each other); scripts/test_sdist.sh is the self-containment rot guard (build sdist → clean venv → vendored-tree render).

render.datasets.Train200() exposes the header-pinned 200-scene ProcTHOR training subset (scene IDs 0–199). Catalog access loads the native core without resolving assets; use dataset["ProcTHOR-Train-1"].reference with render.load_scene, or dataset.references[:4] with MultiEnvironment. The RLtools commit identifies the catalog's membership, order, and hashes. This collection's Train-1 artifact differs from the scene pinned by examples.data.procthor_scene_path() and the rendering goldens.

from hyperdrone import render

dataset = render.datasets.Train200()
scene = render.load_scene(dataset["ProcTHOR-Train-1"].reference)

Select any combination with output=("rgb", "depth", "segmentation", "normals", "flow"). Outputs other than RGB are:

Accessor dtype Per-camera shape Contract
depth() float32 (H, W) Camera-forward depth from the pixel-centre shutter-close ray; miss +inf
segmentation() uint32 (H, W) Instance IDs or semantic classes; miss 0xFFFFFFFF
normals() float32 (H, W, 3) World-FLU geometric unit normal facing against the ray; miss zero
flow() float32 (H, W, 2) Current-to-previous (dx, dy) displacement in pixels, positive along image columns/rows
flow_status() uint32 (H, W) render.FlowStatus: VALID, MISS, BEHIND, OUT_OF_FRAME, OCCLUDED

Renderer prepends num_cameras; host accessors return snapshots or fill an exact-shape/dtype out= array, and *_dlpack() exposes the live backend buffer. Read after render() or render_sync(). Depth, segmentation, normals and flow use pixel centres at shutter close, independently of RGB anti-aliasing and motion blur. MISS and BEHIND flow vectors are zero; OUT_OF_FRAME and OCCLUDED retain geometric displacement, so use flow_status() == render.FlowStatus.VALID as the mask.

renderer = render.Renderer(width=128, height=128, output=("rgb", "depth", "normals", "flow"))
renderer.init(scene)
renderer.set_cameras(previous_cameras)
renderer.render()                       # first capture seeds previous=current
renderer.advance_flow()                 # snapshot camera AND object poses
renderer.set_cameras(current_cameras)   # update object transforms here too, then update()
renderer.render()
flow, status = renderer.flow(), renderer.flow_status()

EnvConfig(output=("rgb", "depth", "segmentation", "normals", "flow")) enables the same auxiliary channels in MultiEnvironment, with RGB policy observations. Its accessors accept out= (NumPy, or CUDA arrays on a CUDA World) and return (total_instances, num_views, H, W[, channels]). They render pending frames as observe() does; repeated reads reuse the capture. Flow retains each due capture even when it is unread, follows task frame cadence, and resets per instance or on scene changes. The first/reset capture has zero motion. semantic_segmentation=True selects classes, and env.outputs / env.segmentation_mode describe the compiled configuration. Normals use the renderer’s scene FLU frame; instance IDs are local to each environment’s active renderer. Custom spec_header Worlds declare their renderer OUTPUTS list, including float RGB for policy observations and Probes for scene annotations; leave the corresponding Python options at their defaults.

EnvConfig(dataset="glb") (the default) uses the taxonomy authored in the GLBs; dataset="procthor" requires each scene to declare ai2thor-hab when semantic output is enabled. Dataset selection is compiled into the JIT module and also applies with spec_header. In semantic mode, initialization validates all scenes and pooled drone/gate assets against one taxonomy, then shares its immutable dictionary across them. Conflicting names or complete class mappings fail initialization; IDs are preserved. Scene rotation does not change the dictionary.

config = EnvConfig(dataset="procthor", output=("rgb", "segmentation"), semantic_segmentation=True)
env = MultiEnvironment(scenes, config=config)
classes = env.segmentation_classes  # dict[int, str], an independent copy
taxonomy = env.segmentation_taxonomy  # "ai2thor-hab"
env.reset()
labels = env.segmentation()
names_present = {int(id): classes.get(int(id), "Background") for id in np.unique(labels)}
env.close()

These properties return {} and "" when semantic output is disabled or all classes are unnamed. Unnamed objects in a named taxonomy must use IDs in that dictionary. The reserved background ID 0xFFFFFFFF is excluded from the dictionary.

For labelled ProcTHOR rendering, preserve GLB root instances and select the output meaning:

scene = render.load_scene(procthor_scene_path(), rgb=False, preserve_instances=True)
renderer = render.Renderer(width=512, height=512, output="segmentation",
                           semantic_segmentation=True)  # False selects instance IDs
renderer.init(scene)
# Set cameras and render as usual.
labels = renderer.segmentation()  # uint32; renderer.segmentation_mode identifies the mode
classes = scene.segmentation_classes  # {class_id: name}, taxonomy: scene.segmentation_taxonomy

Semantic IDs come from AI2THOR-Hab's taxonomy: unknown is 0 (including Ceiling, which has no dataset category); background is 0xFFFFFFFF in both modes. Instance IDs identify scene instances: use scene.instance_object(id) then scene.object_name(object_index) for static hits. Overlay instance IDs are scene.num_instances + placement.overlay * renderer.config.max_overlay_instances + placement.first_slot + part. spawn() returns an OverlayPlacement; transform and despawn methods reject stale, foreign and forged handles. Semantic IDs must never be used as object indices. Loading remains welded by default for RGB; semantic rendering rejects a welded object containing multiple classes. Assemblies and asset-pool copies retain labels and the taxonomy. Composing conflicting taxonomies requires an explicit remapping before semantic rendering.

Assign labels before Renderer.init(): object.segmentation_class = id, scene.set_object_segmentation_class(index, id), assembly.set_segmentation_class(index, id), or pool.set_object_segmentation_class(asset, object_index, id) retains the current taxonomy and explicitly makes that object's class valid, including welded objects. Assignment rejects IDs absent from that taxonomy. object.assign_segmentation_class(id, taxonomy="custom", classes={0: "Unknown", 7: "Furniture"}) replaces the taxonomy; omitting taxonomy/classes selects unnamed numeric IDs.

Objects, scenes, assemblies and pools expose remap_segmentation_classes(mapping, taxonomy="custom", classes={...}); pools also expose remap_asset_segmentation_classes(asset, mapping, ...). Omitting taxonomy/classes selects unnamed numeric IDs. Mappings must cover every used class, and validation failures leave labels unchanged. Remapping preserves valid class boundaries and rejects mixed-class welded objects; explicitly assign one class first if collapsing their labels is intentional. Remap conflicting sources separately before composition. The C++ equivalents are device-first assign_segmentation_class and remap_segmentation_classes in rendering/segmentation_cpu.h, with a shared immutable target taxonomy (or nullptr for unnamed IDs).

Articulated assets

render.Rig is a rigid-link tree: world_link = world_parent @ origin @ joint(q) and world_geometry = world_link @ offset, with links declared parent before child (parent -1 is the base pose) and joints fixed, revolute (radians about axis), prismatic (metres along axis) or spherical (wxyz quaternion). rig.coordinate_slices locates each link's coordinates, rig.default_coordinates is the neutral state. Rig.from_assembly(assembly, joints={"slider": ("prismatic", (0, 1, 0))}) builds one link per part at its rest frame and articulates parts by name; load_assembly(path, part_nodes=[...]) keeps nested GLB nodes as independent parts (an enclosing node first) and records their parent for that. Procedural assets grow with ObjectAssembly.add_part(object, transform). The drone rig the environment renders is hyperdrone.env.Rotorcraft, a Rig built from the body/prop_* convention (see the RL environment section).

renderer.set_transform(placement, body, rig=rig, coordinates=phases)
renderer.set_transform_pair(placement, body_open, body_close, rig=rig,
                            coordinates_open=phases_open, coordinates_close=phases_close)
renderer.update()
renderer.render()

With a rig, the rigid verbs take the rig's base pose and joint coordinates (default: the neutral state) and place the parts at its geometry-to-world poses; the pair form samples the shutter (the base follows the constant-twist screw, the coordinates interpolate before the kinematics, so a revolute joint keeps every revolution). The same poses are available as arrays: rig.evaluate(base, q) is (num_parts, 3, 4) for set_transforms, rig.sample(base_open, base_close, q_open, q_close, renderer.motion_blur_samples) the (samples, parts, 3, 4) motion plus the shutter-close poses for set_motion_transforms. Batches of identical assets, one per overlay, submit the whole tensor at once with set_all_transforms(poses) of shape (num_overlays, max_overlay_instances, 3, 4) (and set_all_motion_transforms); rig.evaluate accepts batched (B, 3, 4) bases for that. On OptiX the bulk transform setters also take CUDA tensors (stream= orders the copy after the producer), and renderer.transforms_dlpack() / transforms_motion_dlpack() expose the live input tensors for in-place producers ordered against renderer.render_stream; publish pending host writes with update() before writing them. python -m hyperdrone.examples.articulation renders a spinning hinge and a sliding block with motion blur; hyperdrone/examples/Renderer.ipynb flies the x500 with spinning props.

Stability

The release contract covers documented names, single-agent shapes and documented HYPERDRONE_* variables. Multi-agent parameter/spawn shapes, observation layouts (including agent<i>/ block prefixes), team-mean rewards and any-agent termination remain experimental. The shipped tasks are single-agent. spec_header constants and their shadowing form the stable extension tier; custom verb-overload wrappers are experimental and should pin the package version. The JIT key hashes the top-level header only; changes to included headers rebuild that tree in place.

Reproducibility is per seed, configuration, reset masks and call sequence on one host build (-O3 -march=native -ffast-math). RNG streams may change in minor releases. Environment IMU noise and shutter sampling still share the physics RNG. The Train200 catalog is immutable; cite its RLtools commit for paper datasets and identify the examples and golden scenes separately as fixtures.

Presets name platforms and default to a neutral MDP: zero reward and disabled termination. Configure the task through parameters["dynamics"]["mdp"]; moving_gate adds +10 for a pass and terminates on gate crashes. Preset and task names are stable. Privileged observations are ground truth, with row-major body-to-world rotation matrices. Views are ordered (instance, agent, camera) and stacked along image height; all cameras share resolution.

All RGB fidelity tiers store sRGB; alpha is always 255 and reserved. Medium uses a checker background. Probe misses return +inf with boolean hit flags; ClearanceSampler accepts candidates whose sampled rays have no hit closer than the clearance radius; it does not compute the environment's free-space annotation table.

Metadata

Release files for hyperlux 0.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hyperlux 0.0.1
File Size Uploaded
hyperlux-0.0.1.tar.gz 1.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for hyperlux 0.0.1
File Interpreter ABI Platform
hyperlux-0.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 2.6 MB

Release files / hyperlux-0.0.1.tar.gz

Download URL hyperlux-0.0.1.tar.gz
Size 1.1 MB
Tags Source
SHA-256 checksum
How to use checksums
b43c0194f15e53a77f6c94e805559dc2efbce65cc70ee30ac39b2cbc1724b9b7
BLAKE2b-256 checksum
How to use checksums
96ef2047c8e186590f85ce7e6cf137c2cac6ce340e85a05a7557968fce90ec69
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.8

Release files / hyperlux-0.0.1-py3-none-any.whl

Download URL hyperlux-0.0.1-py3-none-any.whl
Size 1.6 MB
Tags Python 3
SHA-256 checksum
How to use checksums
18b717d524d9001cc0cb28b590206e5166835d707388cb087b7ea0e55aff4318
BLAKE2b-256 checksum
How to use checksums
a161d08fa6c0ade8300cde250a8ef4d250706c43e4f6796cf50fd6fe3ca83e61
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.8

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page