Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to GeneLab are recorded here.

## [Unreleased]

## [0.4.0] — 2026-07-03

### Changed

- **Genesis 1.2:** the simulator floor moves to `genesis-world>=1.2.0,<1.3.0`
(from the 1.0.x pin). GeneLab-side adaptations: `SimulationCfg.render_fps` now
maps to `ViewerOptions.refresh_rate` (Genesis split the old `max_FPS` into
`refresh_rate` for repaints and `realtime_factor` for sim pacing; `render_fps=None`
disables the pacer to keep the documented "uncapped" contract), and the ImGui
panel host is discovered through the public `viewer.plugins` property (the
private `_viewer_plugins` attribute was renamed upstream; kept as a fallback).
Note for pre-trained checkpoints: Genesis 1.2 parses kinematic trees depth-first,
so joint/link enumeration order changes on branched robots — GeneLab resolves
joints by name and stays self-consistent, but policies trained on 1.0.x whose
observation/action layouts baked the old order must be re-mapped or re-trained.

## [0.3.3] — 2026-06-05

> Git history was rewritten in this release to purge ~24 MB of accidentally-committed vendored
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/installation.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A focused installation checklist. For the full learning path, start with the
|---|---|
| Python | 3.12 or newer |
| Dependency manager | `uv` |
| Simulator backend | `genesis-world>=1.0.0,<1.1.0` (1.1.x changes APIs GeneLab relies on) |
| Simulator backend | `genesis-world>=1.2.0,<1.3.0` (Genesis changes APIs across minor releases; capped until validated) |
| PyTorch | `torch>=2.8.0` through one `torch-*` extra |

## Syncing the environment
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/installation.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
|---|---|
| Python | 3.12 或更新 |
| 依赖管理 | `uv` |
| 仿真后端 | `genesis-world>=1.0.0,<1.1.0`(1.1.x 改动了 GeneLab 依赖的 API) |
| 仿真后端 | `genesis-world>=1.2.0,<1.3.0`(Genesis 的次版本会改动 API,验证前先设上限) |
| PyTorch | 通过一个 `torch-*` extra 安装 `torch>=2.8.0` |

## 同步环境
Expand Down
2 changes: 1 addition & 1 deletion examples/genelab_examples/src/genelab_examples/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def play(self) -> None:
camera_pos=(0.45, -0.55, 0.35),
camera_lookat=(0.0, 0.0, 0.08),
camera_fov=35,
max_FPS=60,
refresh_rate=60,
),
show_viewer=cfg.simulation.vis,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def play(self) -> None:
camera_pos=(2.6, 0.0, 1.6),
camera_lookat=(0.0, 0.0, 0.4),
camera_fov=40,
max_FPS=sim.render_fps or 60,
refresh_rate=sim.render_fps or 60,
enable_gui=True, # the ImGui overlay that hosts our panels
),
show_viewer=True,
Expand Down
18 changes: 12 additions & 6 deletions examples/wuji/src/genelab_wuji/deploy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
* **Encoder / hardware order** = ``JOINT_NAMES_20`` = ``REORIENT_JOINT_POS`` keys =
``wujihandpy``'s (5, 4) row-major flatten: **finger-major** (finger1_joint1..4,
finger2_joint1..4, ...). This is what ``read_encoders`` / ``write_target`` speak.
* **Policy / Genesis articulation order** = ``POLICY_JOINT_NAMES``: **joint-major**
(finger1..5_joint1, then finger1..5_joint2, ...). Genesis orders the articulation
this way regardless of the MJCF element order, so the trained policy's obs and
action are joint-major.
* **Policy order** = ``POLICY_JOINT_NAMES``: **joint-major** (finger1..5_joint1,
then finger1..5_joint2, ...). Genesis 1.0.x enumerated the articulation
breadth-first — joint-major for this hand — and the shipped checkpoint was
trained under that order, so its obs and action layouts are joint-major.

``DeployController`` remaps encoder->policy on read and policy->encoder on write via
``ENC_TO_POLICY`` / its inverse. ``tests/test_examples_wuji_deploy_joint_order.py``
pins ``POLICY_JOINT_NAMES`` against the actual built env so the constant can't drift.
``ENC_TO_POLICY`` / its inverse.

Genesis 1.2+ parses kinematic trees depth-first, so a *live* articulation now
enumerates finger-major (identical to the encoder order). ``POLICY_JOINT_NAMES``
tracks the trained artifact, not the live env; a policy retrained under Genesis
1.2 has finger-major layouts and needs ``ENC_TO_POLICY`` updated to the identity.
``tests/test_examples_wuji_deploy_joint_order.py`` pins the env's depth-first
enumeration so any future Genesis ordering change is caught there.
"""

from __future__ import annotations
Expand Down
2 changes: 1 addition & 1 deletion examples/wuji/src/genelab_wuji/wuji_hand/sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def run_wuji_hand(config: WujiHandRunConfig) -> None:
camera_pos=(0.35, -0.45, 0.32),
camera_lookat=(0.0, 0.0, 0.08),
camera_fov=35,
max_FPS=60,
refresh_rate=60,
),
vis_options=gs.options.VisOptions(
ambient_light=(0.35, 0.35, 0.35),
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "genelab"
version = "0.3.3"
version = "0.4.0"
description = "Isaac Lab API for RL and robotics research powered by Genesis."
readme = "README.md"
license = "Apache-2.0"
Expand All @@ -9,8 +9,9 @@ requires-python = ">=3.12"
authors = [{ name = "Chenhao Zhang", email = "charles040318@gmail.com" }]
maintainers = [{ name = "Chenhao Zhang", email = "charles040318@gmail.com" }]
dependencies = [
# Pin to Genesis 1.0.x: 1.1.x changes parts of the API GeneLab relies on.
"genesis-world>=1.0.0,<1.1.0",
# Pin to Genesis 1.2.x: Genesis changes APIs GeneLab relies on across minor
# releases (1.1.x/1.2.x both did), so cap at the next minor until validated.
"genesis-world>=1.2.0,<1.3.0",
"typer>=0.12",
"rich>=13",
"questionary>=2",
Expand Down
2 changes: 1 addition & 1 deletion src/genelab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import TYPE_CHECKING

__version__ = "0.3.3"
__version__ = "0.4.0"

__all__ = [
"ManagerBasedEnvCfg",
Expand Down
14 changes: 7 additions & 7 deletions src/genelab/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ class SimulationCfg:
dt: float = 0.01
substeps: int = 4
num_envs: int = 1
# Viewer FPS cap, decoupled from the physics rate (``1/dt``). Forwarded to
# ``gs.options.ViewerOptions(max_FPS=...)`` when ``vis=True``. ``None`` runs the
# viewer uncapped. ``ManagerBasedRlEnv`` only refreshes the viewer on the last tick
# of its decimation loop, so the effective render rate is
# ``min(render_fps, 1/(dt*decimation))``; if the control rate exceeds ``render_fps``,
# the viewer's rate-limit will throttle the env step itself — lower ``render_fps`` or
# raise ``decimation`` if that's not what you want.
# Viewer repaint rate, decoupled from the physics rate (``1/dt``). Forwarded to
# ``gs.options.ViewerOptions(refresh_rate=...)`` when ``vis=True``. Genesis paces a
# viewer-enabled sim to wall-clock real time (``realtime_factor=1.0``) by default;
# ``None`` disables that pacer (``realtime_factor=None``) so the sim runs uncapped,
# with repaints at Genesis's default rate. ``ManagerBasedRlEnv`` only refreshes the
# viewer on the last tick of its decimation loop, so the real-time pacer sleeps once
# per control step rather than once per physics tick.
render_fps: int | None = 60
# Initial viewer camera framing, forwarded to ``gs.options.ViewerOptions(camera_pos=...,
# camera_lookat=...)`` when ``vis=True``. ``camera_lookat`` is also the trackball pivot,
Expand Down
6 changes: 3 additions & 3 deletions src/genelab/envs/manager_based_rl_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,9 +351,9 @@ def step(
action = action.to(self._device, non_blocking=True)
self.action_manager.process_action(action)
# Render only on the final physics tick of the decimation loop. Genesis's viewer
# update calls ``rate.sleep`` at ``ViewerOptions.max_FPS``, so updating every
# tick coupled the wall-clock rate to the physics rate and produced slow-motion
# playback at large decimation values.
# update sleeps in its real-time pacer (``ViewerOptions.realtime_factor``), so
# updating every tick coupled the wall-clock rate to the physics rate and produced
# slow-motion playback at large decimation values.
last_tick = self._decimation - 1
for tick in range(self._decimation):
self.action_manager.apply_action()
Expand Down
33 changes: 23 additions & 10 deletions src/genelab/scene/interactive_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,17 @@ def _viewer_option_kwargs(sim_cfg: "SimulationCfg", *, enable_gui: bool) -> dict
Camera framing is only forwarded when set so an unconfigured scene keeps Genesis' own
default camera; ``camera_lookat`` doubles as the trackball pivot, so framing the subject
is what makes the mouse-wheel zoom close in on it.

Genesis 1.1+ split the old ``max_FPS`` (one knob that throttled both repaints and the
stepping loop) into ``refresh_rate`` (repaint cap) and ``realtime_factor`` (sim pacing,
1.0 = wall-clock real time). ``render_fps`` maps onto ``refresh_rate``; ``render_fps=None``
keeps the documented "uncapped" contract by disabling the real-time pacer instead.
"""
kwargs: dict[str, Any] = {"max_FPS": sim_cfg.render_fps, "enable_gui": enable_gui}
kwargs: dict[str, Any] = {"enable_gui": enable_gui}
if sim_cfg.render_fps is not None:
kwargs["refresh_rate"] = sim_cfg.render_fps
else:
kwargs["realtime_factor"] = None
if sim_cfg.camera_pos is not None:
kwargs["camera_pos"] = sim_cfg.camera_pos
if sim_cfg.camera_lookat is not None:
Expand All @@ -64,12 +73,17 @@ def _viewer_option_kwargs(sim_cfg: "SimulationCfg", *, enable_gui: bool) -> dict
def find_imgui_panel_host(viewer: Any) -> Any | None:
"""Return the viewer plugin exposing ``register_panel`` (Genesis's ImGui overlay), or None.

Genesis appends an ``ImGuiOverlayPlugin`` to ``viewer._viewer_plugins`` when the scene is
built with ``ViewerOptions(enable_gui=True)``. We duck-type rather than import the plugin
class so this stays robust across Genesis point releases — any plugin that exposes a
``register_panel`` callable is a valid host.
Genesis registers an ``ImGuiOverlayPlugin`` on the viewer when the scene is built with
``ViewerOptions(enable_gui=True)``. Genesis 1.2 exposes the registered plugins through the
public ``viewer.plugins`` property (the old ``_viewer_plugins`` attribute was renamed);
we read the public property and keep the legacy attribute as a fallback. We duck-type
rather than import the plugin class so this stays robust across Genesis point releases —
any plugin that exposes a ``register_panel`` callable is a valid host.
"""
for plugin in getattr(viewer, "_viewer_plugins", None) or []:
plugins = getattr(viewer, "plugins", None)
if plugins is None:
plugins = getattr(viewer, "_viewer_plugins", None) or []
for plugin in plugins:
if callable(getattr(plugin, "register_panel", None)):
return plugin
return None
Expand Down Expand Up @@ -576,10 +590,9 @@ def build(self) -> None:
if getattr(self._scene_cfg, "batch_render", False)
else None
)
# Render rate is configured separately from the physics rate: Genesis throttles
# ``_visualizer.update`` via ``ViewerOptions.max_FPS``. We only construct
# ``ViewerOptions`` when the viewer is enabled so headless training stays
# untouched.
# Render rate is configured separately from the physics rate: Genesis caps viewer
# repaints via ``ViewerOptions.refresh_rate``. We only construct ``ViewerOptions``
# when the viewer is enabled so headless training stays untouched.
# A non-empty ``panels`` list implies the ImGui overlay even if ``viewer_imgui`` was
# left False, so "add a panel" is a one-liner that just works.
enable_gui = bool(self._sim_cfg.viewer_imgui) or bool(
Expand Down
31 changes: 24 additions & 7 deletions tests/test_examples_wuji_deploy_joint_order.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
"""Pin the deploy joint-order remap against the real Genesis articulation order.

The encoder / wujihandpy order (``JOINT_NAMES_20``) is finger-major; the policy /
Genesis articulation order (``POLICY_JOINT_NAMES``) is joint-major. ``DeployController``
remaps between them. If they drift, the real hand gets scrambled joint obs + actions and
twitches without manipulating the cube (the real-hand 0%-success bug).
The encoder / wujihandpy order (``JOINT_NAMES_20``) is finger-major; the shipped
policy's order (``POLICY_JOINT_NAMES``) is joint-major — Genesis 1.0.x enumerated
the articulation breadth-first, and the deployed checkpoint was trained under that
order. ``DeployController`` remaps between them. If they drift, the real hand gets
scrambled joint obs + actions and twitches without manipulating the cube (the
real-hand 0%-success bug).

Genesis 1.2 parses kinematic trees depth-first, so the live articulation now
enumerates finger-major (identical to the encoder order). ``POLICY_JOINT_NAMES``
deliberately stays the *trained artifact's* order; the env-order drift guard below
pins the new depth-first enumeration instead, so any future Genesis ordering change
is caught again.
"""

import numpy as np
Expand Down Expand Up @@ -35,15 +43,24 @@ def test_default_policy_is_default_reordered() -> None:
assert np.allclose(dp[np.argsort(ENC_TO_POLICY)], d)


def test_policy_joint_order_matches_env() -> None:
"""Drift guard: POLICY_JOINT_NAMES must equal the built env's articulation order."""
def test_env_joint_order_is_depth_first() -> None:
"""Drift guard: the built env must enumerate joints depth-first (finger-major).

Genesis 1.2 parses kinematic trees depth-first, which for the hand coincides
with the encoder order ``JOINT_NAMES_20``. A policy trained under Genesis 1.2
therefore has finger-major obs/action layouts, while the shipped checkpoint
(trained under 1.0.x, breadth-first) keeps the joint-major ``POLICY_JOINT_NAMES``
— sim2sim replay of that checkpoint needs the name-based remap, and a retrained
policy needs an updated ``ENC_TO_POLICY``. This assert exists so any future
Genesis enumeration change is caught here instead of as scrambled joints.
"""
pytest.importorskip("genesis")
from genelab_wuji.deploy.scripts._env import build_reorient_env

env = None
try:
env = build_reorient_env(num_envs=1)
assert list(env.scene["robot"].joint_names) == list(POLICY_JOINT_NAMES)
assert list(env.scene["robot"].joint_names) == list(JOINT_NAMES_20)
except Exception as exc: # asset download / GPU / display unavailable in minimal CI
if env is None:
pytest.skip(f"reorient env unavailable: {exc}")
Expand Down
Loading