Skip to content

feat(genesis): add Genesis physics simulation environment family#32

Closed
Shr1ftyy wants to merge 1 commit intomainfrom
feat/genesis
Closed

feat(genesis): add Genesis physics simulation environment family#32
Shr1ftyy wants to merge 1 commit intomainfrom
feat/genesis

Conversation

@Shr1ftyy
Copy link
Collaborator

@Shr1ftyy Shr1ftyy commented Feb 7, 2026

Summary

  • Adds a new genesis environment family powered by the Genesis physics simulation engine
  • Framework-based architecture: GenesisBaseEnvironment handles all boilerplate (engine init, scene building, first-person ego camera attached to robot torso, PD control pipeline), thin subclasses define per-robot behavior
  • First concrete environment: Unitree G1 humanoid (43 actuated DOFs) with procedural scene generation (flat/hilly/stepped terrain, 3-6 primitive objects) and four task types (navigate, pickup, place, push)
  • Single Docker image (kinitro/genesis:v1) serves all genesis/* env_ids
  • Registers genesis/g1-v0 in the environment registry, CLI, and executor config

New files

  • kinitro/environments/genesis/ — task types, robot config, scene generator, task generator, base environment, G1 subclass
  • environments/genesis/ — Dockerfile, Actor (env.py), metadata, requirements

Test plan

  • uv run python -m ruff check passes
  • uv run python -m pytest tests/ -x -q passes (116 tests)
  • kinitro env build genesis builds Docker image successfully
  • Local render test produces valid RGB + depth images from first-person ego camera

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Genesis physics engine environment support with G1 humanoid robot, featuring navigation and manipulation tasks (pickup, placement, pushing).
    • Integrated procedurally generated scenes and dynamic task generation capabilities.
  • Infrastructure

    • Added Docker container support and configuration for Genesis evaluation environments.
    • Extended CLI build command to support Genesis environment family.

@Shr1ftyy Shr1ftyy marked this pull request as draft February 7, 2026 05:45
@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
📝 Walkthrough
📝 Walkthrough
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(genesis): add Genesis physics simulation environment family' directly and clearly describes the main change: introduction of a new Genesis environment family to the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/genesis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
kinitro/environments/registry.py (1)

120-131: ⚠️ Potential issue | 🟡 Minor

Stale docstring: "(currently only 'metaworld')".

Line 126 says family: Environment family (currently only 'metaworld') — this was already outdated with ProcTHOR and is now further outdated with Genesis. Update to reflect all supported families or remove the parenthetical.

Proposed fix
-        family: Environment family (currently only 'metaworld')
+        family: Environment family (e.g., 'metaworld', 'procthor', 'genesis')
🤖 Fix all issues with AI agents
In `@environments/genesis/Dockerfile`:
- Around line 21-76: Add a non-root user in the Dockerfile, create a dedicated
user/group (e.g., "appuser"), chown the runtime directories (/app and
/opt/menagerie) and any files created at build time, and then switch to that
user with a USER directive before the final image is emitted; specifically:
after installing system deps and copying files (after pip install and the git
clone), add commands to create the user/group (useradd or addgroup/adduser), run
chown -R appuser:appuser /app /opt/menagerie (and adjust ownership of any other
runtime paths), and add USER appuser so the container runs non-root while
keeping existing ENV/PYTHONPATH settings intact.

In `@environments/genesis/env.py`:
- Around line 294-310: The returned metadata currently includes the miner's
base_url inside the "extra" dict (key "base_url"), which may leak sensitive
deployment tokens; remove the "base_url" entry (or replace it with a
redacted/boolean flag) from the dict returned by the function that constructs
this result (the block building the "extra" dict including task_id, seed,
env_id, timesteps, total_reward, mean_action_time, max_action_time, model,
base_url) so logs no longer contain the raw URL, and update any callers/tests
that expect base_url accordingly.
- Around line 134-148: The base_url None check currently raises a raw ValueError
before the surrounding try/except and should instead return a structured error
like the env_id check; modify the genesis environment handler so that when
base_url is None it calls self._build_error_result(...) with the same keys
(env_id, task_id, seed or task_id, start_time=time.time(), and an explanatory
error message) or move the base_url validation inside the existing try block so
that any missing-base_url condition is caught and converted into the structured
error result; update the check near env_id/seed handling that references env_id,
base_url, seed, task_id and uses _build_error_result to ensure consistent error
behavior.

In `@kinitro/cli/env/commands.py`:
- Line 24: The build_env docstring for the function build_env should be updated
to mention the new "genesis" family under the "Environment families" section;
add a short description for Genesis (e.g., "Genesis — custom benchmark
environments") and an approximate image size similar to the other entries (e.g.,
"~1.2GB") so the help text matches AVAILABLE_ENV_FAMILIES which now includes
"genesis".

In `@kinitro/environments/genesis/base.py`:
- Around line 534-548: The code slices joint_action to n but doesn't handle when
joint_action has fewer than n elements, causing a NumPy shape mismatch at
target_pos = default_pos + joint_action * action_scale; modify the logic around
joint_action (after obtaining joint_action and computing n) to check its length
and if shorter than n pad it with zeros (as float32) to length n before clipping
and mapping; ensure the padded joint_action matches the dtype and shape of
default_pos and action_scale so target_pos computation (using joint_action,
default_pos, action_scale) succeeds without errors.

In `@kinitro/environments/genesis/scene_generator.py`:
- Around line 219-239: The stepped-terrain height math in _get_terrain_height
contradicts the nested-box geometry created in build_scene: _get_terrain_height
currently increases height with radial distance while build_scene stacks boxes
of increasing size (and height) from center outward, causing center points to be
under the tallest outer box. Fix by making the two match: either modify
build_scene to invert the step stacking so inner boxes are taller (so height
decreases with radius), or update _get_terrain_height to compute heights that
match the nested-box geometry (e.g., derive step index from box size ordering
used in build_scene or compute height as total_steps - step_idx times
step_height). Update the logic in the functions _get_terrain_height and/or
build_scene accordingly and ensure step_width/step_height params remain
consistent between them.
- Around line 248-251: The CI linter flags the runtime-only import "import
genesis as gs" as unresolved; fix it by updating that import to suppress the
type checker—change the line "import genesis as gs  # noqa: PLC0415" to include
a mypy ignore like "import genesis as gs  # noqa: PLC0415  # type:
ignore[import-unresolved]" (or alternatively implement a conditional import
guard or small local "genesis" stub) so the linter stops reporting the
unresolved import while keeping the runtime behavior intact.
- Around line 186-217: The fallback in _generate_object_position currently
returns a hardcoded [1.5, 0.0, 0.05] which can place objects below the surface
on non-flat terrain; change the fallback to compute terrain height using
_get_terrain_height(1.5, 0.0, terrain_type, terrain_params) and add the same
small offset (0.05) before returning so the final fallback is [1.5, 0.0,
terrain_height + 0.05]; keep the existing signature and semantics of
_generate_object_position unchanged.

In `@kinitro/environments/genesis/task_generator.py`:
- Around line 120-131: The current loop silently overrides an explicitly passed
task_type on retries; change the logic in the method containing the loop so that
if the caller provided a non-None task_type you do not replace it on subsequent
attempts—only call _random_choice(available_types, rng) when the initial
task_type was None. Concretely: capture a flag like "explicit_type_provided =
(task_type is not None)" before the loop, call self._try_generate_task(objects,
rng, task_type, robot_config) up to self._max_attempts keeping the same
task_type when explicit_type_provided is True, and only re-select task_type from
available_types via _random_choice when explicit_type_provided is False; ensure
the function returns None after exhausting attempts. Reference symbols:
task_type, available_types, _random_choice, _try_generate_task,
self._max_attempts.

In `@kinitro/environments/genesis/task_types.py`:
- Around line 104-146: check_task_feasibility currently verifies destination for
TaskType.PLACE but doesn't guard against target == destination; update the
TaskType.PLACE branch in check_task_feasibility to mirror the PUSH checks by
returning infeasible when destination is None or when target.object_id ==
destination.object_id (include a clear reason string like "Cannot place object
at its own location"), referencing the TaskType.PLACE branch and the
target/destination object_id comparisons to locate the change.
🧹 Nitpick comments (16)
environments/genesis/requirements.txt (1)

1-8: Consider pinning dependency versions for reproducible Docker builds.

Most packages here are unpinned. In particular, torch and genesis-world are fast-moving libraries where minor version bumps can introduce breaking changes or significantly change behavior. Since this file is used in a Dockerfile for evaluation, reproducibility matters. At minimum, pin genesis-world and torch to known-good versions.

environments/genesis/Dockerfile (1)

56-62: Pin the MuJoCo Menagerie clone to a specific commit or tag.

git clone --depth 1 fetches whatever is at HEAD, so builds are non-reproducible and an upstream breaking change to the G1 MJCF files could silently break the image. Pin to a known-good commit hash or tagged release.

Example with a pinned commit
-RUN git clone --depth 1 --filter=blob:none --sparse \
-    https://github.com/google-deepmind/mujoco_menagerie.git /opt/menagerie && \
-    cd /opt/menagerie && \
-    git sparse-checkout set unitree_g1 && \
-    echo "Menagerie assets installed"
+ARG MENAGERIE_COMMIT=<pinned_commit_hash>
+RUN git clone --filter=blob:none --sparse \
+    https://github.com/google-deepmind/mujoco_menagerie.git /opt/menagerie && \
+    cd /opt/menagerie && \
+    git checkout ${MENAGERIE_COMMIT} && \
+    git sparse-checkout set unitree_g1 && \
+    echo "Menagerie assets installed at ${MENAGERIE_COMMIT}"
kinitro/environments/registry.py (1)

48-58: LGTM — consider a safeguard on getattr.

The dynamic getattr(envs, env_cls_name) approach is more flexible than dedicated per-class imports and scales well for adding more Genesis variants. The trade-off is that a typo in env_cls_name surfaces as an AttributeError only at first instantiation time rather than at import time. Since the string is a literal at the call site, this is acceptable, but you might want to add a brief guard for a clearer error message:

Optional: friendlier error on missing class
         cls = getattr(envs, env_cls_name)
+        if cls is None:
+            raise ImportError(f"Genesis environment class '{env_cls_name}' not found in envs module")

Or use getattr(envs, env_cls_name, None) with a check.

kinitro/environments/genesis/task_generator.py (2)

53-58: _random_choice uses TypeVar("T") but consider simplifying with a generic.

The TypeVar("T") at line 18 is only used here. This is fine, though in Python 3.12+ you could use the [T] syntax on the function. Minor nit — no action needed now.


66-68: Remove unused helper function _get_landmark_objects or document its intended purpose.

This function is defined but never called anywhere in the codebase. Per YAGNI, remove it unless there's a documented future use case.

kinitro/environments/genesis/envs/g1_humanoid.py (1)

35-81: target_pos computed on line 48 is unused for non-NAVIGATE branches.

Lines 47–48 unconditionally compute robot_pos and target_pos, but target_pos is only used inside the NAVIGATE branch (line 52). For PICKUP, PLACE, and PUSH the method uses obj_pos and dest_pos instead. This is harmless (cheap computation), but pulling target_pos into the NAVIGATE block would improve readability.

kinitro/environments/genesis/robot_config.py (1)

8-36: Consider using list[TaskType] instead of list[str] for supported_task_types.

supported_task_types stores string values (e.g., "navigate") that must match TaskType.value. This stringly-typed coupling is visible in task_generator.py line 111 (t.value in robot_config.supported_task_types). Using list[TaskType] directly would provide type safety and eliminate the risk of drift if enum values change.

That said, keeping it as strings may be intentional for serialization or decoupling — so flagging as optional.

kinitro/environments/genesis/scene_generator.py (1)

17-17: logger is defined but never used in this module.

environments/genesis/env.py (2)

60-85: New httpx.AsyncClient per call creates connection overhead on the hot path.

Each of the up to 500 /act calls creates a new AsyncClient, establishing a fresh connection pool. The comment explains this is to avoid event loop binding issues with affinetes, which is a valid concern.

Consider creating the client once at the start of _run_evaluation and reusing it for all calls within that evaluation, then closing it at the end. This avoids both the per-call overhead and the cross-event-loop issue.


174-220: Miner reset is called before env reset — consider ordering implications.

Line 204 calls the miner's /reset endpoint, then line 220 calls env.reset(task_config). If env.reset() fails (e.g., Genesis engine crash), the miner has already been reset and is waiting for observations that will never arrive. Consider reversing the order: reset the simulation first (which is more likely to fail), then notify the miner.

kinitro/environments/genesis/base.py (6)

33-38: Log the GPU initialization failure before falling back to CPU.

When GPU init fails, the exception is silently swallowed. Logging it at debug or warning level would help troubleshoot GPU driver/availability issues in production.

🔧 Proposed fix
     try:
         gs.init(backend=getattr(gs, "gpu"))
         logger.info("genesis_initialized", backend="gpu")
-    except Exception:
+    except Exception as e:
+        logger.debug("genesis_gpu_init_failed", error=str(e))
         gs.init(backend=getattr(gs, "cpu"))
         logger.info("genesis_initialized", backend="cpu")

77-80: Add brief comments explaining the Any type hints.

Per coding guidelines: "Avoid Any type hints unless required; if used, explain why in code comments." These are Any because the genesis module is only available at runtime inside Docker, so the concrete types (gs.Scene, gs.Entity, gs.Camera) can't be referenced at import/lint time.

📝 Proposed fix
         # Lazy-initialized components
-        self._scene: Any = None
-        self._robot: Any = None
-        self._camera: Any = None
-        self._object_entities: list[Any] = []
+        # Genesis types (gs.Scene, gs.Entity, gs.Camera) are unavailable at
+        # import time — the engine is a Docker-only runtime dependency.
+        self._scene: Any = None  # gs.Scene
+        self._robot: Any = None  # gs.Entity (MJCF morph)
+        self._camera: Any = None  # gs.Camera
+        self._object_entities: list[Any] = []  # list[gs.Entity]

As per coding guidelines, "Avoid Any type hints unless required; if used, explain why in code comments."


222-226: Redundant local re-import of SceneConfig.

SceneConfig is already imported at the module level (line 14). Only SceneObjectConfig needs a local import here.

📝 Proposed fix
-        from kinitro.environments.genesis.scene_generator import (  # noqa: PLC0415
-            SceneConfig,
-            SceneObjectConfig,
-        )
+        from kinitro.environments.genesis.scene_generator import SceneObjectConfig  # noqa: PLC0415

278-296: Consider the success + fallen interaction in the same step.

If _check_success returns True but the robot has also fallen, _episode_success is latched to True while the reward is overwritten to -1.0. Downstream, get_success() will report success despite a penalty. If this is intentional (task completion is "sticky" regardless of final posture), a brief comment would clarify the design choice for future maintainers.


379-383: Extract the hardcoded floating-base DOF count (6) into a named constant.

The magic number 6 (floating-base DOFs) appears in four places: here, _read_robot_state (lines 445–446), and _apply_action (line 551). If a fixed-base robot is ever added, all locations must be updated in lockstep. A class constant (or a RobotConfig field) would make this explicit and prevent drift.

♻️ Proposed fix

Add a class-level constant:

     # Rendering settings
     IMAGE_SIZE = 84
     SIM_DT = 0.01  # 100 Hz physics
     CONTROL_DT = 0.02  # 50 Hz control (2 physics steps per control step)
+    FLOATING_BASE_DOFS = 6  # 3 translation + 3 rotation for free-floating robots

Then replace all occurrences, e.g.:

-        actuated_dof_idx = list(range(6, 6 + n))
+        actuated_dof_idx = list(range(self.FLOATING_BASE_DOFS, self.FLOATING_BASE_DOFS + n))

463-467: Silent exception swallowing hides object-state read failures.

The bare except Exception: pass makes it impossible to diagnose issues where object positions can't be read (e.g., entity destroyed, tensor device mismatch). At minimum, log a warning so failures are visible.

🔧 Proposed fix
                 try:
                     pos = entity.get_pos().cpu().numpy().flatten()
                     states[obj_id] = pos
-                except Exception:
-                    pass
+                except Exception as e:
+                    logger.debug("object_state_read_failed", object_id=obj_id, error=str(e))

Comment on lines +21 to +76
FROM python:3.11-slim

WORKDIR /app

# Install system dependencies for Genesis rendering (EGL/OpenGL headless)
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
# OpenGL/Mesa libraries for headless rendering
libgl1 \
libgl1-mesa-dri \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
libgomp1 \
libegl1 \
libegl-mesa0 \
libglvnd0 \
libglx0 \
libglx-mesa0 \
mesa-utils \
&& rm -rf /var/lib/apt/lists/*

# EGL for headless rendering
ENV PYOPENGL_PLATFORM=egl
ENV NVIDIA_DRIVER_CAPABILITIES=all

# Force software rendering when no GPU available
ENV LIBGL_ALWAYS_SOFTWARE=1
ENV MESA_GL_VERSION_OVERRIDE=3.3

# Copy requirements and install dependencies
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r /app/requirements.txt

# Clone MuJoCo Menagerie (sparse checkout -- only robot dirs needed)
# This provides MJCF assets for supported robots (G1, Go2, Franka, etc.)
RUN git clone --depth 1 --filter=blob:none --sparse \
https://github.com/google-deepmind/mujoco_menagerie.git /opt/menagerie && \
cd /opt/menagerie && \
git sparse-checkout set unitree_g1 && \
echo "Menagerie assets installed"

ENV GENESIS_MENAGERIE_PATH=/opt/menagerie

# Copy the kinitro environments package (copied by env build command)
# This is a subset of the main kinitro package - only environments/ is needed
COPY kinitro /app/kinitro

# Set PYTHONPATH so kinitro package is importable
ENV PYTHONPATH="/app:${PYTHONPATH}"

# Copy the eval environment Actor
COPY env.py /app/

# Affinetes will inject its HTTP server - do not set CMD/ENTRYPOINT
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a non-root USER directive.

Trivy flags DS002: the container runs everything as root. Even though Affinetes manages the container lifecycle, running as a non-root user is a security best practice that limits the blast radius if the container is compromised.

Proposed fix
 # Affinetes will inject its HTTP server - do not set CMD/ENTRYPOINT
+
+# Run as non-root for security
+RUN useradd --create-home appuser
+USER appuser
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies for Genesis rendering (EGL/OpenGL headless)
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
# OpenGL/Mesa libraries for headless rendering
libgl1 \
libgl1-mesa-dri \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
libgomp1 \
libegl1 \
libegl-mesa0 \
libglvnd0 \
libglx0 \
libglx-mesa0 \
mesa-utils \
&& rm -rf /var/lib/apt/lists/*
# EGL for headless rendering
ENV PYOPENGL_PLATFORM=egl
ENV NVIDIA_DRIVER_CAPABILITIES=all
# Force software rendering when no GPU available
ENV LIBGL_ALWAYS_SOFTWARE=1
ENV MESA_GL_VERSION_OVERRIDE=3.3
# Copy requirements and install dependencies
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r /app/requirements.txt
# Clone MuJoCo Menagerie (sparse checkout -- only robot dirs needed)
# This provides MJCF assets for supported robots (G1, Go2, Franka, etc.)
RUN git clone --depth 1 --filter=blob:none --sparse \
https://github.com/google-deepmind/mujoco_menagerie.git /opt/menagerie && \
cd /opt/menagerie && \
git sparse-checkout set unitree_g1 && \
echo "Menagerie assets installed"
ENV GENESIS_MENAGERIE_PATH=/opt/menagerie
# Copy the kinitro environments package (copied by env build command)
# This is a subset of the main kinitro package - only environments/ is needed
COPY kinitro /app/kinitro
# Set PYTHONPATH so kinitro package is importable
ENV PYTHONPATH="/app:${PYTHONPATH}"
# Copy the eval environment Actor
COPY env.py /app/
# Affinetes will inject its HTTP server - do not set CMD/ENTRYPOINT
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies for Genesis rendering (EGL/OpenGL headless)
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
# OpenGL/Mesa libraries for headless rendering
libgl1 \
libgl1-mesa-dri \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
libgomp1 \
libegl1 \
libegl-mesa0 \
libglvnd0 \
libglx0 \
libglx-mesa0 \
mesa-utils \
&& rm -rf /var/lib/apt/lists/*
# EGL for headless rendering
ENV PYOPENGL_PLATFORM=egl
ENV NVIDIA_DRIVER_CAPABILITIES=all
# Force software rendering when no GPU available
ENV LIBGL_ALWAYS_SOFTWARE=1
ENV MESA_GL_VERSION_OVERRIDE=3.3
# Copy requirements and install dependencies
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r /app/requirements.txt
# Clone MuJoCo Menagerie (sparse checkout -- only robot dirs needed)
# This provides MJCF assets for supported robots (G1, Go2, Franka, etc.)
RUN git clone --depth 1 --filter=blob:none --sparse \
https://github.com/google-deepmind/mujoco_menagerie.git /opt/menagerie && \
cd /opt/menagerie && \
git sparse-checkout set unitree_g1 && \
echo "Menagerie assets installed"
ENV GENESIS_MENAGERIE_PATH=/opt/menagerie
# Copy the kinitro environments package (copied by env build command)
# This is a subset of the main kinitro package - only environments/ is needed
COPY kinitro /app/kinitro
# Set PYTHONPATH so kinitro package is importable
ENV PYTHONPATH="/app:${PYTHONPATH}"
# Copy the eval environment Actor
COPY env.py /app/
# Run as non-root for security
RUN useradd --create-home appuser && chown -R appuser:appuser /app
USER appuser
# Affinetes will inject its HTTP server - do not set CMD/ENTRYPOINT
🤖 Prompt for AI Agents
In `@environments/genesis/Dockerfile` around lines 21 - 76, Add a non-root user in
the Dockerfile, create a dedicated user/group (e.g., "appuser"), chown the
runtime directories (/app and /opt/menagerie) and any files created at build
time, and then switch to that user with a USER directive before the final image
is emitted; specifically: after installing system deps and copying files (after
pip install and the git clone), add commands to create the user/group (useradd
or addgroup/adduser), run chown -R appuser:appuser /app /opt/menagerie (and
adjust ownership of any other runtime paths), and add USER appuser so the
container runs non-root while keeping existing ENV/PYTHONPATH settings intact.

Comment on lines +134 to +148
# Validate env_id is a genesis environment
if not env_id.startswith("genesis/"):
return self._build_error_result(
env_id=env_id,
task_id=task_id,
seed=seed or task_id,
start_time=time.time(),
error=f"Invalid env_id for Genesis container: {env_id}. Must start with 'genesis/'",
)

if base_url is None:
raise ValueError("base_url (miner endpoint) is required")

if seed is None:
seed = task_id
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing base_url validation raises ValueError outside the try/except block.

Line 145 raises ValueError("base_url (miner endpoint) is required") before the try/except on line 152. This means a None base_url causes a raw exception to propagate to the caller, while all other errors return a structured error dict. This inconsistency could crash the calling framework.

Move the check inside the try/except, or return a structured error result like the env_id check above.

Proposed fix
         if base_url is None:
-            raise ValueError("base_url (miner endpoint) is required")
+            return self._build_error_result(
+                env_id=env_id,
+                task_id=task_id,
+                seed=seed or task_id,
+                start_time=time.time(),
+                error="base_url (miner endpoint) is required",
+            )
🤖 Prompt for AI Agents
In `@environments/genesis/env.py` around lines 134 - 148, The base_url None check
currently raises a raw ValueError before the surrounding try/except and should
instead return a structured error like the env_id check; modify the genesis
environment handler so that when base_url is None it calls
self._build_error_result(...) with the same keys (env_id, task_id, seed or
task_id, start_time=time.time(), and an explanatory error message) or move the
base_url validation inside the existing try block so that any missing-base_url
condition is caught and converted into the structured error result; update the
check near env_id/seed handling that references env_id, base_url, seed, task_id
and uses _build_error_result to ensure consistent error behavior.

Comment on lines +294 to +310
return {
"task_name": f"robotics:{env_id}",
"score": score,
"success": success,
"time_taken": time.time() - start_time,
"extra": {
"task_id": task_id,
"seed": seed,
"env_id": env_id,
"timesteps": timesteps,
"total_reward": float(total_reward),
"mean_action_time": float(np.mean(action_times)) if action_times else 0.0,
"max_action_time": float(np.max(action_times)) if action_times else 0.0,
"model": model,
"base_url": base_url,
},
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

base_url is included in the result metadata.

Line 308 includes the miner's base_url in the returned extra dict. If miner URLs contain tokens or sensitive deployment details, this could leak information into evaluation logs. Consider whether this is appropriate for your logging/storage pipeline, or omit it.

🤖 Prompt for AI Agents
In `@environments/genesis/env.py` around lines 294 - 310, The returned metadata
currently includes the miner's base_url inside the "extra" dict (key
"base_url"), which may leak sensitive deployment tokens; remove the "base_url"
entry (or replace it with a redacted/boolean flag) from the dict returned by the
function that constructs this result (the block building the "extra" dict
including task_id, seed, env_id, timesteps, total_reward, mean_action_time,
max_action_time, model, base_url) so logs no longer contain the raw URL, and
update any callers/tests that expect base_url accordingly.


# Available environment families for build command
AVAILABLE_ENV_FAMILIES = ["metaworld", "procthor"]
AVAILABLE_ENV_FAMILIES = ["metaworld", "procthor", "genesis"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the build_env docstring to include Genesis.

AVAILABLE_ENV_FAMILIES now includes "genesis", but the docstring at lines 57–60 only lists metaworld and procthor under "Environment families". Add a line for Genesis (e.g., description and approximate image size) to keep the help text consistent.

🤖 Prompt for AI Agents
In `@kinitro/cli/env/commands.py` at line 24, The build_env docstring for the
function build_env should be updated to mention the new "genesis" family under
the "Environment families" section; add a short description for Genesis (e.g.,
"Genesis — custom benchmark environments") and an approximate image size similar
to the other entries (e.g., "~1.2GB") so the help text matches
AVAILABLE_ENV_FAMILIES which now includes "genesis".

Comment on lines +475 to +489
try:
# Update camera pose to follow attached link
if self._camera._attached_link is not None:
self._camera.move_to_attach()

# camera.render() returns (rgb, depth, segmentation, normal)
rgb, depth, _seg, _normal = self._camera.render(rgb=True, depth=True)

# Ensure uint8 for RGB
if rgb is not None and rgb.dtype != np.uint8:
rgb = (np.clip(rgb, 0, 1) * 255).astype(np.uint8)

return rgb, depth
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Two concerns: private attribute access and depth image encoding.

  1. _attached_link is a private attribute (line 477). This couples to Genesis internals and may break on library updates. Consider storing the attachment state locally (e.g., a self._camera_attached: bool flag set in _attach_ego_camera).

  2. Depth array may not be suitable for encode_image (line 487). RGB is normalized to uint8, but depth is returned raw (likely float32). If encode_image performs PNG/JPEG encoding expecting uint8, the depth data will be corrupted or cause an error. Verify that encode_image handles float depth arrays correctly, or apply normalization here.

#!/bin/bash
# Check encode_image implementation to see if it handles float32 depth arrays
ast-grep --pattern $'def encode_image($$$) {
  $$$
}'
rg -n -A 15 'def encode_image' --type=py

Comment on lines 186 to 166
def _generate_object_position(
self,
rng: np.random.Generator,
terrain_type: str,
terrain_params: dict[str, Any],
pickupable: bool,
) -> list[float]:
"""Generate a valid position for an object, avoiding robot spawn area."""
half_arena = self._arena_size / 2.0
min_dist_from_center = 0.8 # Avoid robot spawn area

for _ in range(50):
x = float(rng.uniform(-half_arena, half_arena))
y = float(rng.uniform(-half_arena, half_arena))

dist = np.sqrt(x**2 + y**2)
if dist < min_dist_from_center:
continue

# Pickupable objects should be closer (reachable)
if pickupable and dist > half_arena * 0.7:
continue

# Compute z (height) based on terrain
z = self._get_terrain_height(x, y, terrain_type, terrain_params)
# Place object on top of terrain
z += 0.05 # Small offset above ground

return [x, y, z]

# Fallback position
return [1.5, 0.0, 0.05]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fallback position ignores terrain height.

Line 217 returns [1.5, 0.0, 0.05] as a hardcoded fallback, which doesn't call _get_terrain_height. On hilly or stepped terrain, this could place the object below the surface. Given that 50 attempts almost certainly succeed for the configured arena, this is low-risk but easy to fix.

Proposed fix
         # Fallback position
-        return [1.5, 0.0, 0.05]
+        z = self._get_terrain_height(1.5, 0.0, terrain_type, terrain_params) + 0.05
+        return [1.5, 0.0, z]
🤖 Prompt for AI Agents
In `@kinitro/environments/genesis/scene_generator.py` around lines 186 - 217, The
fallback in _generate_object_position currently returns a hardcoded [1.5, 0.0,
0.05] which can place objects below the surface on non-flat terrain; change the
fallback to compute terrain height using _get_terrain_height(1.5, 0.0,
terrain_type, terrain_params) and add the same small offset (0.05) before
returning so the final fallback is [1.5, 0.0, terrain_height + 0.05]; keep the
existing signature and semantics of _generate_object_position unchanged.

Comment on lines 219 to 239
def _get_terrain_height(
self,
x: float,
y: float,
terrain_type: str,
terrain_params: dict[str, Any],
) -> float:
"""Get terrain height at (x, y) for object placement."""
if terrain_type == "flat":
return 0.0
elif terrain_type == "hilly":
freq = terrain_params.get("frequency", 0.5)
amp = terrain_params.get("amplitude", 0.2)
return float(amp * (np.sin(freq * x) + np.cos(freq * y)) * 0.5)
elif terrain_type == "stepped":
step_width = terrain_params.get("step_width", 1.0)
step_height = terrain_params.get("step_height", 0.1)
dist = np.sqrt(x**2 + y**2)
step_idx = int(dist / step_width)
return float(step_idx * step_height)
return 0.0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stepped terrain: height model in _get_terrain_height contradicts the geometry built by build_scene.

_get_terrain_height computes height as increasing with distance from center (step_idx = int(dist / step_width) → height grows outward). However, build_scene creates nested boxes that all share origin (0,0) with increasing size and height — so every inner box is overlapped by all outer (taller) boxes. The actual surface height at the center equals step_height * num_steps (the tallest box), while _get_terrain_height returns 0.0 there.

This means objects placed near the center will clip through the stepped geometry (placed at z ≈ 0.05 when the actual surface is at step_height * num_steps), and objects placed further out may also be at incorrect heights.

The fix should either:

  1. Invert the step geometry in build_scene so that inner steps are taller (matching the height model), or
  2. Fix _get_terrain_height to match the actual nested-box geometry.

Also applies to: 268-283

🤖 Prompt for AI Agents
In `@kinitro/environments/genesis/scene_generator.py` around lines 219 - 239, The
stepped-terrain height math in _get_terrain_height contradicts the nested-box
geometry created in build_scene: _get_terrain_height currently increases height
with radial distance while build_scene stacks boxes of increasing size (and
height) from center outward, causing center points to be under the tallest outer
box. Fix by making the two match: either modify build_scene to invert the step
stacking so inner boxes are taller (so height decreases with radius), or update
_get_terrain_height to compute heights that match the nested-box geometry (e.g.,
derive step index from box size ordering used in build_scene or compute height
as total_steps - step_idx times step_height). Update the logic in the functions
_get_terrain_height and/or build_scene accordingly and ensure
step_width/step_height params remain consistent between them.

Comment on lines +248 to +178
Returns:
List of Genesis entity references for object tracking
"""
import genesis as gs # noqa: PLC0415
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pipeline failure: genesis import is unresolved.

The CI linter flags import genesis as gs as unresolved. Since Genesis is only available at runtime in the Docker container, consider suppressing the lint with # type: ignore[import-unresolved] in addition to the existing # noqa: PLC0415, or adding a genesis stub/conditional import guard.

Proposed fix
-        import genesis as gs  # noqa: PLC0415
+        import genesis as gs  # noqa: PLC0415  # type: ignore[import-unresolved]
🧰 Tools
🪛 GitHub Actions: ci

[error] 251-251: Cannot resolve imported module genesis (unresolved-import) in scene generation. Ensure the Genesis module is available in the Python environment or add it to dependencies.

🤖 Prompt for AI Agents
In `@kinitro/environments/genesis/scene_generator.py` around lines 248 - 251, The
CI linter flags the runtime-only import "import genesis as gs" as unresolved;
fix it by updating that import to suppress the type checker—change the line
"import genesis as gs  # noqa: PLC0415" to include a mypy ignore like "import
genesis as gs  # noqa: PLC0415  # type: ignore[import-unresolved]" (or
alternatively implement a conditional import guard or small local "genesis"
stub) so the linter stops reporting the unresolved import while keeping the
runtime behavior intact.

Comment on lines +120 to +131
if task_type is None:
task_type = _random_choice(available_types, rng)

for _ in range(self._max_attempts):
if task_type is None:
return None
task = self._try_generate_task(objects, rng, task_type, robot_config)
if task is not None:
return task

# Try a different task type
task_type = _random_choice(available_types, rng)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Explicit task_type is silently overridden on retry.

When the caller passes an explicit task_type (e.g., TaskType.PICKUP), the first attempt uses it, but if it fails, line 131 replaces it with a random type from available_types. This means the caller's explicit request is silently ignored after a single failed attempt, potentially returning a task of a completely different type.

If the caller explicitly specified a type, retries should probably stay on that type (varying object selection), or the method should return None if the specified type is infeasible.

Proposed fix
+        # If caller explicitly requested a type, only retry that type
+        explicit_type = task_type is not None
+
         if task_type is None:
             task_type = _random_choice(available_types, rng)
 
         for _ in range(self._max_attempts):
             if task_type is None:
                 return None
             task = self._try_generate_task(objects, rng, task_type, robot_config)
             if task is not None:
                 return task
 
-            # Try a different task type
-            task_type = _random_choice(available_types, rng)
+            if not explicit_type:
+                # Try a different task type
+                task_type = _random_choice(available_types, rng)
🤖 Prompt for AI Agents
In `@kinitro/environments/genesis/task_generator.py` around lines 120 - 131, The
current loop silently overrides an explicitly passed task_type on retries;
change the logic in the method containing the loop so that if the caller
provided a non-None task_type you do not replace it on subsequent attempts—only
call _random_choice(available_types, rng) when the initial task_type was None.
Concretely: capture a flag like "explicit_type_provided = (task_type is not
None)" before the loop, call self._try_generate_task(objects, rng, task_type,
robot_config) up to self._max_attempts keeping the same task_type when
explicit_type_provided is True, and only re-select task_type from
available_types via _random_choice when explicit_type_provided is False; ensure
the function returns None after exhausting attempts. Reference symbols:
task_type, available_types, _random_choice, _try_generate_task,
self._max_attempts.

Comment on lines +104 to +146
def check_task_feasibility(
task_type: TaskType,
target: SceneObject,
destination: SceneObject | None = None,
robot_supported_tasks: list[str] | None = None,
) -> tuple[bool, str]:
"""
Check if a task is feasible given the target object and robot capabilities.

Returns:
Tuple of (is_feasible, reason)
"""
# Check robot supports this task type
if robot_supported_tasks is not None:
if task_type.value not in robot_supported_tasks:
return False, f"Robot does not support task type {task_type.value}"

# Check required properties
required_props = TASK_REQUIRED_PROPERTIES[task_type]
for prop in required_props:
if not getattr(target, prop, False):
return False, f"Object {target.object_type} is not {prop}"

# Task-specific checks
if task_type == TaskType.PICKUP:
if target.is_picked_up:
return False, f"Object {target.object_type} is already picked up"

if task_type == TaskType.PLACE:
if destination is None:
return False, "Place task requires a destination"

if task_type == TaskType.PUSH:
if destination is None:
return False, "Push task requires a destination"
if target.object_id == destination.object_id:
return False, "Cannot push object to itself"

if task_type == TaskType.NAVIGATE:
# Navigate is always feasible if target exists
pass

return True, "Task is feasible"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

PLACE feasibility doesn't guard against target == destination (unlike PUSH).

Lines 136–140 validate that PUSH requires destination is not None and target.object_id != destination.object_id. The PLACE check (lines 132–134) only validates the destination exists but doesn't prevent placing an object at its own location. While the current TaskGenerator naturally prevents this (it picks from disjoint pickupable/non-pickupable sets), the feasibility function should be self-consistent.

Proposed fix
     if task_type == TaskType.PLACE:
         if destination is None:
             return False, "Place task requires a destination"
+        if target.object_id == destination.object_id:
+            return False, "Cannot place object at itself"
🤖 Prompt for AI Agents
In `@kinitro/environments/genesis/task_types.py` around lines 104 - 146,
check_task_feasibility currently verifies destination for TaskType.PLACE but
doesn't guard against target == destination; update the TaskType.PLACE branch in
check_task_feasibility to mirror the PUSH checks by returning infeasible when
destination is None or when target.object_id == destination.object_id (include a
clear reason string like "Cannot place object at its own location"), referencing
the TaskType.PLACE branch and the target/destination object_id comparisons to
locate the change.

@Shr1ftyy Shr1ftyy force-pushed the feat/genesis branch 3 times, most recently from af55012 to 846280e Compare February 7, 2026 07:54
Adds a new environment family powered by the Genesis simulation engine,
with a framework-based architecture where GenesisBaseEnvironment handles
all boilerplate (engine init, scene building, first-person ego camera,
PD control pipeline) and thin subclasses define per-robot behavior.

First concrete environment: Unitree G1 humanoid (43 actuated DOFs)
with procedural scene generation (flat ground, 3-6 primitive objects)
and four task types (navigate, pickup, place, push).

Includes CLI --viewer and --seed flags for `kinitro env test`, registry
**kwargs passthrough for env factory options, and 72 unit tests covering
all pure-logic modules.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant