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
123 changes: 123 additions & 0 deletions .greptile/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"commentTypes": [
"logic",
"syntax"
],
"instructions": "Review against the conventions in .greptile/rules.md. Formatting, import order, line length, license headers, spelling, and debug statements are already enforced by pre-commit (black/isort/flake8/pyupgrade/codespell) - do not comment on them, except under scripts/ which pre-commit skips. Prioritize, in order: (1) runtime bugs and cross-script CLI/config mismatches, (2) unvectorized, device-mismatched, or non-deterministic tensor code in mdp terms, (3) gym registration and cfg entry points that do not resolve, (4) missing extension version + CHANGELOG bumps, (5) machine-specific paths or committed artifacts. Be concise; include a code suggestion when the fix is local.",
"ignorePatterns": [
"docs/licenses/**",
"docs/source/_static/**",
".github/workflows/license-exceptions.json",
"CONTRIBUTORS.md"
],
"rules": [
{
"id": "extension-version-changelog",
"rule": "Any change to code under source/<ext>/<ext>/ must bump `version` in source/<ext>/config/extension.toml (semver) and add a matching entry to source/<ext>/docs/CHANGELOG.rst with the same version, a date, and Added/Changed/Deprecated/Removed/Fixed sub-sections written in past tense. Flag PRs that touch an extension's package without both, or where the two versions disagree.",
"severity": "high",
"scope": [
"source/**"
]
},
{
"id": "cli-flag-consistency",
"rule": "When a script launches another script (subprocess, os.system, shell), every flag it passes must be defined by the target's argparse parser or Hydra config, and opt-out flags such as --no_video, --headless, or --disable_* must actually change behavior in every code path that reads them. Flag passed-but-undefined flags and flags that are parsed but ignored.",
"severity": "high",
"scope": [
"scripts/**",
"scripts_v2/**",
"source/**"
]
},
{
"id": "vectorized-mdp-terms",
"rule": "Observation, reward, event, termination, curriculum, and command terms under **/mdp/ must be batched over the env dimension: operate on tensors shaped (num_envs, ...) or on the given env_ids, allocate tensors with device=env.device, and never loop in Python over environments, call .item()/.cpu()/.numpy() inside per-step logic, or mix CPU and CUDA tensors. Flag per-env Python loops and host round-trips in these files.",
"severity": "high",
"scope": [
"source/**/mdp/**"
]
},
{
"id": "per-env-sampling-uses-torch",
"rule": "Per-environment randomness in mdp terms (anything that differs across env_ids: poses, velocities, physics parameters, noise) must be sampled with torch on env.device (torch.rand/randn/randint, math_utils.sample_uniform) in one batched call, not with Python `random` or `np.random` in a loop. Scalar scene-wide choices shared by all envs (e.g. picking one HDRI or a camera jitter applied to the whole scene) may use `random`, which the env seed also seeds.",
"severity": "medium",
"scope": [
"source/uwlab_tasks/**",
"source/uwlab/**"
]
},
{
"id": "task-registration",
"rule": "A new environment cfg class in uwlab_tasks must be registered with gym.register in its package __init__.py, and every entry-point string in the registration (env_cfg_entry_point, *_cfg_entry_point) must resolve to an existing module:attribute or agents/ file. Flag registrations whose targets do not exist, duplicate task ids, and env cfgs that are never registered.",
"severity": "high",
"scope": [
"source/uwlab_tasks/**"
]
},
{
"id": "task-package-self-contained",
"rule": "A new task lives in its own package under source/uwlab_tasks/uwlab_tasks/<manager_based|direct>/<category>/<task>/ (reference layout: manipulation/omnireset with __init__.py doing gym.register, *_env_cfg.py, mdp/, config/<robot>/ or agents/). By default everything the task needs - mdp terms, recorders, utilities, cfgs - stays inside that package. Changes outside it (the uwlab core extension, uwlab_rl, uwlab_assets, shared uwlab_tasks utilities, or another task's package) are sometimes correct but must be justified: the PR description or code comment should say why the code cannot live in the task package (generic and used by more than one task, a framework bug fix, a new asset). Flag out-of-package changes that carry no such justification, task-specific code added to another task's package (e.g. extending omnireset/mdp for a different task), and behaviour flags added to another task's env cfg instead of a separate env cfg.",
"severity": "high",
"scope": [
"source/**"
]
},
{
"id": "new-task-docs",
"rule": "When a PR registers a new task id, it should also list it in docs/source/overview/uw_environments.rst. Work that ships as a publication (own Quick Start, checkpoints, paper) additionally gets docs/source/publications/<name>/index.rst and a bullet under 'Getting Started' in README.md. Mention this once as a note; do not block on it.",
"severity": "medium",
"scope": [
"source/uwlab_tasks/**"
]
},
{
"id": "optional-research-dependencies",
"rule": "Research-only or heavy dependencies (e.g. diffusion_policy, robomimic, policy-training frameworks) must not be added to any extension's install_requires or extension.toml [python.pipapi] requirements, so a default UWLab install stays light. Ship them as an extras_require group (pattern: EXTRAS_REQUIRE in source/uwlab_rl/setup.py), a git submodule, or documented install steps on the relevant docs/source/publications/<name>/ page. Nothing under source/ may import them at module import time.",
"severity": "high"
},
{
"id": "submodule-policy",
"rule": "A new .gitmodules entry must point at a lab-owned repository (github.com/UW-Lab or github.com/WEIRDLabUW, not a personal fork), pin a branch or commit, be optional to initialize (nothing under source/ imports it at module import time), and come with docs saying when to run `git submodule update --init`. If docs previously told users to clone that repo manually (e.g. docs/source/publications/omnireset/distillation.rst), the PR must update them.",
"severity": "high"
},
{
"id": "no-machine-specific-paths",
"rule": "Committed code must not contain absolute filesystem paths (/home/<user>, /mnt, /data, /scratch), personal usernames, hard-coded conda env names, wandb entities, or local checkpoint and dataset paths. Use CLI arguments, environment variables, Hydra config, or paths relative to the repository or the extension's data directory.",
"severity": "high"
},
{
"id": "no-committed-artifacts",
"rule": "Do not commit training outputs or generated data: checkpoints (*.pt, *.pth, *.jit, *.onnx), logs, videos, hdf5/npz datasets, USD files, __pycache__, or wandb/tensorboard directories. Binary assets that legitimately belong in uwlab_assets must go through git LFS per .gitattributes. Trained checkpoints and datasets are published to the Hugging Face dataset UW-Lab/uwlab-assets (checkpoints under Policies/) via a fork + PR and linked from the docs, never stored in this repo.",
"severity": "medium"
},
{
"id": "type-hints-signature-only",
"rule": "Public functions and methods in source/ carry type hints in the signature using PEP 604 syntax (`int | None`, not `Optional[int]`), omit `-> None`, and use Google-style docstrings whose Args entries are `name: description` with no type repeated in the docstring. Flag missing hints on new public APIs and types duplicated in docstrings.",
"severity": "medium",
"scope": [
"source/**"
]
},
{
"id": "type-checking-imports-only-for-cfg",
"rule": "Imports live at module top. `typing.TYPE_CHECKING` guarded imports are allowed only to break the cfg <-> implementation cycle (a cfg referencing its class_type/func, or an implementation annotating its cfg). The only other accepted exception is standalone scripts that must import isaaclab/uwlab modules after AppLauncher has started the app. Flag other function-local or TYPE_CHECKING imports.",
"severity": "medium",
"scope": [
"source/**"
]
},
{
"id": "shell-scripts-need-bash",
"rule": "Shell scripts that use bash-only syntax (arrays, [[ ]], ${!arr[@]}, (( )), `local`) must start with `#!/usr/bin/env bash`, not `#!/bin/sh`.",
"severity": "medium",
"scope": [
"**/*.sh"
]
},
{
"id": "new-dependencies-need-justification",
"rule": "Adding a package to any setup.py install_requires, extension.toml [python.pipapi] requirements, environment.yml, or pyproject.toml needs a stated reason in the PR and a license on the allowlist (MIT, Apache, BSD, ISC, zlib, PSF, BSL, MPL) or an entry in .github/workflows/license-exceptions.json. Prefer implementing with torch, numpy, or existing dependencies. Research-only or heavy dependencies (diffusion_policy, robomimic, policy-training frameworks) must not go in install_requires at all - see optional-research-dependencies.",
"severity": "medium"
}
]
}
16 changes: 16 additions & 0 deletions .greptile/files.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"files": [
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"description": "PR template. The items that matter for review are the pre-commit run and the CHANGELOG + extension.toml version bump; tests are optional and should not be requested."
},
{
"path": ".pre-commit-config.yaml",
"description": "Formatting and lint checks that CI already enforces; do not duplicate these in review comments. Note scripts/ is excluded from pre-commit."
},
{
"path": ".gitattributes",
"description": "File types that must be stored via git LFS."
}
]
}
192 changes: 192 additions & 0 deletions .greptile/rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# UW Lab review conventions

UW Lab extends [Isaac Lab](https://github.com/isaac-sim/IsaacLab) and follows its coding
conventions (Google Python style, PEP 8, PEP 484/585 type hints, Google docstrings). This file
is review context for Greptile. Rules that pre-commit already enforces are listed so the reviewer
does not repeat them.

## Already enforced by pre-commit (do not comment on these)

`./uwlab.sh -f` runs: black (line length 120), isort (black profile, custom sections in
`pyproject.toml`), flake8 (+simplify, +return; see `.flake8` for ignored codes), pyupgrade
(`--py310-plus`), codespell, trailing whitespace / EOF fixes, license header insertion,
debug-statement check, and a 2 MB file-size cap. Everything under `scripts/` is **excluded**
from pre-commit, so basic hygiene issues there are fair game.

## Repository layout

Four Omniverse-style extensions live under `source/`, each with the same shape:

```
source/<ext>/
├── config/extension.toml # version (semver) + metadata + pip requirements
├── docs/CHANGELOG.rst # one entry per version, must match extension.toml
├── <ext>/ # the python package
├── test/ # pytest tests
└── setup.py
```

- `uwlab` – framework core (envs, mdp terms, sensors, utilities). Depends on `isaaclab`.
- `uwlab_assets` – robot / object / sensor cfg instances. Large binaries go through git LFS.
- `uwlab_tasks` – task definitions: `manager_based/` and `direct/`, each task is a package with
`__init__.py` (gym registration), `*_env_cfg.py`, `mdp/`, and `agents/` (RL library cfgs).
- `uwlab_rl` – RL library wrappers and runners.

Standalone entry points: `scripts/` mirrors the Isaac Lab layout (`reinforcement_learning/`,
`imitation_learning/`, `tools/`, `tutorials/`); `scripts_v2/tools/` holds newer standalone tools.

## Standalone scripts

Any script that touches Isaac Sim must create the `AppLauncher` first and import `isaaclab` /
`uwlab*` modules only after `simulation_app` exists:

```python
"""Launch Isaac Sim Simulator first."""
from isaaclab.app import AppLauncher

app_launcher = AppLauncher(args_cli)
simulation_app = app_launcher.app

"""Rest everything follows."""
import torch
from isaaclab.envs import ManagerBasedRLEnv
```

Scripts take configuration from argparse / Hydra, never from hard-coded absolute paths.
When one script shells out to another, the flags it passes must exist in the callee.

## Python file and class structure

Within a file: imports → constants → public functions → public classes → private functions →
private classes. Within a class: constants → `ClassVar` attributes → `__init__`/`__del__` →
`__repr__`/`__str__` → properties → public instance/class/static methods → private methods,
ordered the way a user would call them (`initialize`, `reset`, `update`, `close`).

Private helpers are prefixed with `_`. Imports stay at module top; the only sanctioned
`typing.TYPE_CHECKING` use is breaking the cfg ↔ implementation cycle (a cfg's `class_type` /
`func` default referencing the implementation, or an implementation annotating its cfg type).

## Type hints and docstrings

- Type hints in the signature, PEP 604 unions (`torch.Tensor | None`), no `-> None`.
- Google docstrings. `Args:` entries are `name: description` — the type is never repeated.
- Physical quantities state SI units and shape: `"""Joint positions [rad], shape (num_envs, num_joints)."""`
- Docstrings explain *why* and non-obvious design choices, not just *what*.

## Manager-based environments

- Configs are `@configclass` dataclasses (`*Cfg`), usually alongside the implementation.
- MDP terms are plain functions with the Isaac Lab signature:

```python
def term(env: ManagerBasedRLEnv, ..., asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
```

Observation / reward / termination terms return a tensor of shape `(num_envs,)` or
`(num_envs, D)`. Event terms receive `env_ids: torch.Tensor | None` and must only touch those
environments. Stateful terms subclass `ManagerTermBase`.
- Everything is batched over the env dimension on `env.device`. No Python loops over envs, no
`.item()` / `.cpu()` / numpy round-trips in per-step code, no CPU/CUDA tensor mixing.
- Per-env randomness (anything that differs across `env_ids`) is sampled with torch on
`env.device` in one batched call (`torch.rand(..., device=env.device)`,
`math_utils.sample_uniform`). Python `random` is fine for scalar scene-wide choices shared by
every env (one HDRI, one camera jitter); the env seed seeds it too. Either way results must be
reproducible under `env_cfg.seed`, which `test_environment_determinism.py` checks.
- Scene entities are resolved through `SceneEntityCfg` (joint/body ids are resolved once, then
indexed), not by string lookups every step.

## Task registration

New environments are registered in the task package's `__init__.py`:

```python
gym.register(
id="UW-<Task>-<Robot>-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.<task>_env_cfg:<Task>EnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:<Task>PPORunnerCfg",
},
)
```

Every entry-point string must resolve; `-Play-v0` variants normally reuse the training cfg with
`num_envs`, randomization, and curriculum reduced. New tasks are picked up by
`source/uwlab_tasks/test/test_environments.py`, so they must construct and step headless.

## Adding a new task or publication

- One package per task under `source/uwlab_tasks/uwlab_tasks/manager_based/<category>/<task>/`
(`direct/` for direct-workflow envs). `manipulation/omnireset/` is the reference layout:
`__init__.py` (gym registration), `*_env_cfg.py`, `mdp/` (terms and utils; its `__init__`
re-exports `isaaclab.envs.mdp` and `uwlab.envs.mdp`, then the local modules), and
`config/<robot>/` or `agents/` for robot-specific cfgs and RL library cfgs.
- By default everything the task needs - new mdp terms, recorders, helpers, cfgs - lives inside
that package. Do not extend another task's package (e.g. add cube logic to `omnireset/mdp/`) and
do not add flags to another task's env cfg; create a separate env cfg.
- Changes outside the task package (`uwlab` core, `uwlab_rl`, `uwlab_assets`, shared `uwlab_tasks`
utilities, another task's package) are sometimes the right call, but they must be justified in
the PR: why can this not live in the task package? Good reasons are code that is generic and used
by more than one task, a framework bug fix, or a new shared asset. If the justification is
missing, ask for it rather than rejecting the change.
- Docs: add the task id to `docs/source/overview/uw_environments.rst`. Publication-tier work also
gets `docs/source/publications/<name>/index.rst` (Quick Start first; see `omnireset/index.rst`)
and a bullet under **Getting Started** in `README.md`.
- Checkpoints and datasets go to the Hugging Face dataset `UW-Lab/uwlab-assets` (`Policies/` for
checkpoints) via fork + PR, and the docs link to them. They are never committed here.
- Heavy or research-only dependencies (diffusion_policy, robomimic, ...) are not added to core
install requirements, so a default install stays light. Use an `extras_require` group
(`EXTRAS_REQUIRE` in `source/uwlab_rl/setup.py` is the pattern), a git submodule, or install
steps on the publication's docs page. Nothing under `source/` may import them at module import
time.
- Submodules point at a lab-owned repo (`github.com/UW-Lab`, `github.com/WEIRDLabUW`), are pinned
to a branch or commit, are optional to initialize, and are documented (when to run
`git submodule update --init`). If docs previously said to clone the repo manually, update them
in the same PR.

## Versioning and changelog

Every PR that changes an extension's package bumps that extension's version in
`config/extension.toml` (patch for fixes, minor for features, major for breaking changes) and adds
a matching entry at the top of `docs/CHANGELOG.rst`:

```rst
0.13.9 (2026-08-28)
~~~~~~~~~~~~~~~~~~~

Added
^^^^^

* Added :func:`~uwlab_tasks.manager_based.manipulation.omnireset.mdp.cube_reset` event for cube reset states.

Fixed
^^^^^

* Fixed ``--no_video`` being ignored in :mod:`scripts.ASTEROID.run_asteroid`.
```

Bullets are past tense, concise, and say why when it is not obvious. Sub-sections are limited to
Added / Changed / Deprecated / Removed / Fixed. Breaking changes go under Changed, prefixed with
`**Breaking:**`, with migration guidance.

## Dependencies

Prefer torch, numpy, and what is already installed. A new package must be added to the owning
extension's `setup.py` and `extension.toml`, have an allowlisted license (MIT / Apache / BSD /
ISC / zlib / PSF / BSL / MPL — CI fails otherwise unless listed in
`.github/workflows/license-exceptions.json`), and be justified in the PR description.

## Tests

pytest under `source/<ext>/test/`, run with `./uwlab.sh -t` or
`./uwlab.sh -p -m pytest <path>`. Tests that need the simulator create the `AppLauncher` at
module top (headless) and are marked `@pytest.mark.isaacsim_ci`. Tests are **not required** for a
PR — do not ask for them. Do flag changes that would break the existing suite (e.g. a new task
that cannot be constructed headless, or a determinism regression).

## Portability

No absolute paths, usernames, hard-coded conda environments, wandb entities, or local
checkpoint/dataset locations in committed code. Use CLI args, environment variables, Hydra, or
paths relative to the repo / extension data directory.
Loading
Loading