Skip to content

Add scene engine pipeline - #436

Open
MuziWong wants to merge 25 commits into
mainfrom
muzi/feat_scene_engine
Open

Add scene engine pipeline#436
MuziWong wants to merge 25 commits into
mainfrom
muzi/feat_scene_engine

Conversation

@MuziWong

@MuziWong MuziWong commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR adds the image to tabletop scene generation pipeline, including semantic understanding, image segmentation and verification, geometry generation, layout refinement, gravity settling, Gym export, and preview tooling.

It also uses VHACD as the default simulation collision-decomposition method.

Dependencies: Existing scene-engine service endpoints and simulation dependencies.

Type of change

  • Bug fix
  • Enhancement
  • New feature
  • Breaking change
  • Documentation update

Screenshots

N/A

Checklist

  • I have run the black . command to format the code base. (black is unavailable in the current
    environment; the branch includes its existing RAN black commit.)
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective
  • Dependencies have been updated, if applicable.

Copilot AI review requested due to automatic review settings July 29, 2026 03:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new “scene engine” image-to-tabletop-scene generation pipeline under embodichain/gen_sim/scene_engine, covering semantic scene understanding, image segmentation, coarse geometry/layout generation, heuristic layout refinement + gravity settling, Gym export, and CLI preview tooling. It also standardizes use of VHACD for collision decomposition in the simulation-facing parts of the pipeline.

Changes:

  • Added core scene data structures (table/assets/scene) plus stage modules for understanding, segmentation, generation/refinement, and Gym export.
  • Added service clients/config loaders for an OpenAI-compatible VLM endpoint, an image-segmentation service, and a geometry-generation service.
  • Added CLI entry points to run the pipeline and preview the generated Gym export in SimulationManager.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
embodichain/gen_sim/scene_engine/utils/logger.py Adds simple stage logging helpers for the pipeline.
embodichain/gen_sim/scene_engine/utils/init.py Initializes the utils package.
embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py Implements RLE mask decode/merge and mask visualization helpers.
embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py Adds geometry/layout utilities: transforms, table support heuristics, packing, and gravity-settling helpers.
embodichain/gen_sim/scene_engine/pipeline/utils/init.py Initializes pipeline utils package.
embodichain/gen_sim/scene_engine/pipeline/scene_understanding.py VLM-driven semantic parsing + strict JSON validation for table/assets.
embodichain/gen_sim/scene_engine/pipeline/scene_segmentation.py Service-driven segmentation with VLM validation/assignment and mask outputs.
embodichain/gen_sim/scene_engine/pipeline/scene_generation.py Orchestrates coarse geometry download, SimReady conversion, refinement, and scene updates.
embodichain/gen_sim/scene_engine/pipeline/gym_export.py Exports final scene + meshes into a Gym config + mesh assets layout.
embodichain/gen_sim/scene_engine/pipeline/generate.py End-to-end pipeline entrypoint tying all stages together.
embodichain/gen_sim/scene_engine/pipeline/init.py Initializes pipeline package.
embodichain/gen_sim/scene_engine/llms/openai_compatible_client.py Implements an OpenAI-compatible multimodal chat-completions client.
embodichain/gen_sim/scene_engine/llms/load_config.py Loads VLM config with environment-variable overrides.
embodichain/gen_sim/scene_engine/llms/init.py Initializes llms package.
embodichain/gen_sim/scene_engine/core/table.py Defines the Table dataclass and serialization.
embodichain/gen_sim/scene_engine/core/scene.py Defines the Scene dataclass and serialization.
embodichain/gen_sim/scene_engine/core/asset.py Defines the Asset dataclass and serialization.
embodichain/gen_sim/scene_engine/core/init.py Initializes core package.
embodichain/gen_sim/scene_engine/configs/scene_engine_config.json Adds a unified config template for VLM/segmentation/geometry services.
embodichain/gen_sim/scene_engine/configs/init.py Initializes configs package.
embodichain/gen_sim/scene_engine/clients/image_segmentation.py Adds image segmentation service client + config loader.
embodichain/gen_sim/scene_engine/clients/geometry_generation.py Adds geometry generation service client + response parsing + downloads.
embodichain/gen_sim/scene_engine/clients/init.py Initializes clients package.
embodichain/gen_sim/scene_engine/cli/start.py Adds CLI entry to run the full pipeline from an image.
embodichain/gen_sim/scene_engine/cli/preview.py Adds CLI tool to preview exported Gym scenes in simulation.
embodichain/gen_sim/scene_engine/cli/init.py Initializes cli package.
Comments suppressed due to low confidence (1)

embodichain/gen_sim/scene_engine/pipeline/generate.py:106

  • If generate_scene_and_refine() (or check_health()) raises, GeometryGenerationClient.close() is skipped, leaking the underlying requests.Session. Use try/finally to ensure cleanup.
    # 3. Objects + Coarse Layout Generation
    log_stage_start("Objects + Coarse Layout Generation")
    # Load the config and fail if the Geometry Generation Server is unavailable.
    geometry_generation_client = GeometryGenerationClient.from_config(
        geometry_generation_config_path
    )
    geometry_generation_client.check_health()

    scene = generate_scene_and_refine(
        image_path=image_path,
        output_root=resolved_output_root,
        scene=scene,
        vlm_client=vlm_client,
        geometry_generation_client=geometry_generation_client,
    )
    geometry_generation_client.close()  # Kill the session.
    log_stage_end("Objects + Coarse Layout Generation")

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py Outdated
Comment on lines +73 to +88
# 2. Scene Segmentation
log_stage_start("Scene Segmentation")
# Load the config and fail if the Image Segmentation Server is unavailable.
image_segmentation_client = ImageSegmentationClient.from_config(
image_segmentation_config_path
)
image_segmentation_client.check_health() # Error raising will happen internally.
scene = segment_scene(
image_path=image_path,
output_root=resolved_output_root,
scene=scene,
vlm_client=vlm_client,
image_segmentation_client=image_segmentation_client,
)
image_segmentation_client.close() # Kill the session.
log_stage_end("Scene Segmentation")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have already fixed this part.

Comment on lines +88 to +90
finally:
sim.destroy()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have already fixed this part.

Comment thread embodichain/gen_sim/scene_engine/clients/geometry_generation.py
Comment on lines +215 to +217
rendered_mask = ( # If weuse outline, then need to do some another processings.
mask if mask_style == "fill" else _mask_outer_outline(mask, image.size)
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have changed weuse into we use.

Comment on lines +177 to +180
if (
scene.table.id != "table"
): # Currently it will always return true. For we hardcode the table id to "table".
raise ValueError("Scene table id must be 'table'.")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have already shorten this into one line.

Comment on lines +47 to +55
def generate_scene_from_image(
image_path: str | Path,
output_root: str | Path,
*,
llm_config_path: str | Path | None = None,
image_segmentation_config_path: str | Path | None = None,
geometry_generation_config_path: str | Path | None = None,
) -> Scene:
"""Generate the initial core scene state from an input image."""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for reviewing!

print("Successfully completed!")


def main() -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] 请将新增命令注册到统一 CLI。当前项目公开入口是 embodichain console script,但 embodichain.__main__.COMMANDS 中没有 Scene Engine 或预览命令;因此安装后用户无法从统一 CLI 发现或调用这里的 main(),只能依赖内部模块路径。请在 COMMANDS 中注册 Scene Engine 和 preview 对应的子命令。

@MuziWong MuziWong Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Resolved by registering scene-engine and preview-scene in embodichain.__main__.COMMANDS.

The unified dispatcher lazily imports the selected command handler and forwards the remaining command-line arguments to its main(argv) function. Both Scene Engine CLI entry points accept the forwarded argv, allowing their own argument parsers to process the expected options instead of relying on the process-wide sys.argv.

Installed users can invoke Scene Engine through the public CLI:

embodichain scene-engine \
  --image <image> \
  --output_root <output_root> \
  --config <scene_engine_config>

embodichain preview-scene \
  --output_root <output_root>

preview-scene opens the native preview window by default. To use the browser-based Viser preview:

embodichain preview-scene \
  --output_root <output_root> \
  --viser \
  --viser-host 0.0.0.0 \
  --viser-port 9000

The corresponding module entry points are:

python -m embodichain.gen_sim.scene_engine.cli.start \
  --image <image> \
  --output_root <output_root> \
  --config <scene_engine_config>

python -m embodichain.gen_sim.scene_engine.cli.preview \
  --output_root <output_root>

I also verified that:

  • scene-engine and preview-scene appear in embodichain --help;
  • embodichain scene-engine --help and embodichain preview-scene --help dispatch to their command-specific parsers correctly;
  • preview-scene --help exposes the required --output_root argument and the optional Viser-related flags;
  • the relevant CLI and Scene Engine unit tests pass (21 passed).

resolved_output_root = Path(output_root).expanduser().resolve()
resolved_output_root.mkdir(parents=True, exist_ok=True)

generate_scene_from_image(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Pass service configuration through the generation CLI. As written, users cannot run this command without editing the JSON installed with the package: this call forwards only the image and output paths, while the packaged service URLs are empty. The LLM endpoint can be overridden through environment variables, but the segmentation and geometry services have no CLI or environment-variable entry point, so generation fails during base_url validation. Please accept a config path or explicit service URL options here and forward them into the pipeline.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Resolved by adding an optional --config argument to the generation CLI. The provided Scene Engine JSON configuration is propagated to the LLM, image segmentation, and geometry generation clients, allowing users to override service endpoints without modifying the package-installed default configuration.

for scene_object in scene_objects
}
gym_config = {
"id": f"Prompt2Scene-{int(time.time() * 1000)}-v0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Export a registered environment ID. embodichain run-env passes this generated ID directly to gymnasium.make, but the codebase does not register any Prompt2Scene-* environment. Consequently, every exported config fails immediately with NameNotFound. Please export an environment ID that is already registered with the runner, or provide and invoke the corresponding registration logic.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Resolved by changing the artifact from a Gym environment configuration to an explicit scene export (embodichain.scene-export/v1).

The generated identifier is now scene_id, which uniquely identifies the exported scene data. It is not a Gymnasium environment ID and is never passed to gymnasium.make(). The exported scene is loaded through the Scene Engine preview path instead of embodichain run-env, so no Gym environment registration is required.

"max_episodes": 10,
"max_episode_steps": 300,
"env": {"events": {}, "observations": {}, "dataset": {}},
"robot": {},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Supply a valid robot configuration in the Gym export. Even after the environment ID is fixed, EmbodiedEnv parses this empty object as RobotCfg(fpath=None); _setup_robot then receives None from add_robot and proceeds to access robot joints. If this file is intended to load directly through run-env, export a valid robot configuration. Otherwise, target an explicitly scene-only environment that supports running without a robot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Resolved by changing the artifact from a Gym environment configuration to an explicit scene export (embodichain.scene-export/v1).

The generated scene does not infer a robot model, robot placement, controller, or sensor setup from the input image, so exporting a default robot configuration would produce an incomplete and potentially misleading task setup. The scene export therefore omits the robot field entirely and is loaded through the Scene Engine preview path, which directly creates a SimulationManager for the table and assets without constructing an EmbodiedEnv.

A downstream task or environment layer can attach an explicit robot configuration when one is available, keeping scene generation independent from task-specific execution.

destination_glb_path = mesh_assets_root / object_id / f"{object_id}.glb"
destination_glb_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source_glb_path, destination_glb_path)
return destination_glb_path.relative_to(mesh_assets_root.parent).as_posix()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Emit mesh paths that the Gym loader can resolve. When this config is loaded through run-env, config_to_cfg passes the relative Mesh path to get_data_path, which interprets mesh_assets as a dataset name and raises Dataset class 'mesh_assets' not found. Only the custom preview resolves this path relative to the config directory. Please export an absolute or data-root-relative path, or update the common loader to resolve Mesh paths relative to the config file.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Resolved by changing this artifact from a run-env Gym configuration to an explicit scene export (embodichain.scene-export/v1). The output is now written as scene_export/scene_config.json and is loaded through the Scene Engine preview path, which resolves mesh_assets/... relative to the config file directory.

It is no longer passed to config_to_cfg() or get_data_path(), so exporting absolute paths or modifying the common Gym loader is unnecessary here. Keeping config-relative paths also preserves portability when the complete scene_export/ directory is moved.

Copilot AI review requested due to automatic review settings July 29, 2026 06:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (13)

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:510

  • Calling the private SimulationManager method _deferred_destroy() bypasses the public teardown path and can become brittle if the internal cleanup mechanism changes. Prefer using destroy(exit_process=False) plus SimulationManager.flush_cleanup_queue() to run the deferred cleanup without forcibly exiting the process.
    finally:
        sim._deferred_destroy()

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:485

  • RigidObjectCfg.max_convex_hull_num / acd_method are deprecated in favor of the shape-level MeshCfg fields. Moving these to MeshCfg(...) avoids relying on deprecated overrides and keeps the config consistent with the rest of the sim stack.
                    body_scale=tuple(asset_info["y_up_scale"]),
                    body_type="dynamic",
                    max_convex_hull_num=max_convex_hull_num,
                    acd_method="vhacd",  # Use vhacd by default.

embodichain/gen_sim/scene_engine/cli/preview.py:89

  • SimulationManager.destroy() may call os._exit(0) by default (via EMBODICHAIN_SIM_EXIT_PROCESS), which is surprising for a CLI preview tool. Also, the sim manager uses deferred cleanup; flushing the cleanup queue avoids leaked C++ resources/segfaults on interpreter shutdown (see embodichain/lab/sim/sim_manager.py:2676-2797).
    finally:
        sim.destroy()

embodichain/gen_sim/scene_engine/pipeline/generate.py:88

  • If segment_scene(...) raises, the ImageSegmentationClient session never closes. Wrap usage in a try/finally so the HTTP session is closed on all error paths.
    image_segmentation_client = ImageSegmentationClient.from_config(
        image_segmentation_config_path
    )
    image_segmentation_client.check_health()  # Error raising will happen internally.
    scene = segment_scene(
        image_path=image_path,
        output_root=resolved_output_root,
        scene=scene,
        vlm_client=vlm_client,
        image_segmentation_client=image_segmentation_client,
    )
    image_segmentation_client.close()  # Kill the session.
    log_stage_end("Scene Segmentation")

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:216

  • Spelling/grammar in the inline comment: "weuse" / "some another processings".
        rendered_mask = (  # If weuse outline, then need to do some another processings.
            mask if mask_style == "fill" else _mask_outer_outline(mask, image.size)

embodichain/gen_sim/scene_engine/pipeline/generate.py:54

  • This PR introduces a new end-to-end scene-engine pipeline (VLM parsing/validation, RLE decoding, mask unioning, geometry/layout refinement, gym export) but adds no tests. There is existing gen_sim test coverage (e.g. tests/gen_sim/simready_pipeline/*), so adding unit tests for the pure functions (JSON schema validators, RLE encode/decode, IoU/union logic, layout transform helpers) and lightweight integration tests with mocked clients would help prevent regressions.
def generate_scene_from_image(
    image_path: str | Path,
    output_root: str | Path,
    *,
    llm_config_path: str | Path | None = None,
    image_segmentation_config_path: str | Path | None = None,
    geometry_generation_config_path: str | Path | None = None,
) -> Scene:

embodichain/gen_sim/scene_engine/cli/preview.py:160

  • RigidObjectCfg.max_convex_hull_num / acd_method are deprecated (they override the shape-level settings). Set convex-decomposition settings on MeshCfg instead, so preview matches the supported config path.
                max_convex_hull_num=max_convex_hull_num,
                acd_method="vhacd",  # Use vhacd by default.
            )

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:468

  • RigidObjectCfg.max_convex_hull_num / acd_method are deprecated in favor of MeshCfg.max_convex_hull_num / MeshCfg.acd_method (see embodichain/lab/sim/cfg.py:963-984). Set these on the MeshCfg instead to match the supported configuration surface.

This issue also appears on line 482 of the same file.

                body_scale=tuple(table_y_up_scale),
                body_type="static",
                max_convex_hull_num=max_convex_hull_num,
                acd_method="vhacd",  # Use vhacd by default.

embodichain/gen_sim/scene_engine/cli/preview.py:85

  • The preview loop opens a window and then only sleeps; if physics is manually updated (the common case), the viewer may not render / respond because the simulation never steps. Stepping once per loop keeps the window responsive and ensures the scene is actually rendered.

This issue also appears on line 158 of the same file.

        sim.open_window()
        while True:
            time.sleep(0.1)

embodichain/gen_sim/scene_engine/pipeline/generate.py:106

  • If generate_scene_and_refine(...) raises, the GeometryGenerationClient session is left open. Use try/finally to guarantee .close() and avoid leaking connections.
    geometry_generation_client = GeometryGenerationClient.from_config(
        geometry_generation_config_path
    )
    geometry_generation_client.check_health()

    scene = generate_scene_and_refine(
        image_path=image_path,
        output_root=resolved_output_root,
        scene=scene,
        vlm_client=vlm_client,
        geometry_generation_client=geometry_generation_client,
    )
    geometry_generation_client.close()  # Kill the session.
    log_stage_end("Objects + Coarse Layout Generation")

embodichain/gen_sim/scene_engine/clients/geometry_generation.py:67

  • check_health() ignores the configured timeout_s by hardcoding timeout=10. If the intention is to cap the timeout for health checks, use min(self._timeout_s, 10) so the config is still respected when it is smaller.
                response = self._session.get(
                    self._url(self._health_path),
                    # timeout=self._timeout_s,
                    timeout=10,  # Use a shorter timeout for avoiding long waits.
                )

embodichain/gen_sim/scene_engine/pipeline/scene_generation.py:188

  • Spelling/wording in comments: "assets(includes table)", "seperately".
    # Simready all the assets(includes table).
    # Treat table and assets seperately.
    # Notice that, currently the simready process is only

embodichain/gen_sim/scene_engine/utils/logger.py:22

  • Logger naming is inconsistent with the rest of the codebase, which generally uses logging.getLogger(__name__) (e.g. embodichain/utils/logger.py:24, embodichain/lab/gym/utils/registration.py:41). Using __name__ keeps the logger hierarchy aligned with the module path and makes filtering/configuration easier.
_LOGGER = logging.getLogger("embodichain.scene_engine")

Copilot AI review requested due to automatic review settings July 30, 2026 03:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (8)

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:479

  • Same as the table config above: RigidObjectCfg.max_convex_hull_num / acd_method are deprecated; for new code set max_convex_hull_num/acd_method on the MeshCfg shape so collision decomposition settings live with the mesh (and avoid legacy overrides).
                RigidObjectCfg(
                    uid=asset_id,
                    shape=MeshCfg(fpath=str(asset_info["mesh_path"])),
                    init_pos=tuple(rigid_layout["pos"]),
                    init_rot=tuple(

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:511

  • Calling the private SimulationManager._deferred_destroy() couples this pipeline to internal cleanup implementation details and bypasses the supported destroy() API. Since destroy() can already avoid os._exit() via exit_process=False, prefer the public method and then flush the cleanup queue for deterministic teardown inside the pipeline.
    finally:
        sim._deferred_destroy()

embodichain/gen_sim/scene_engine/pipeline/scene_understanding.py:180

  • The inline comment is incorrect/misleading: since the table id is hardcoded to "table" in validate_scene_understanding_json, this condition will normally be false (it won't "always return true"). Consider rewording so readers don't misinterpret the control flow.
    if (
        scene.table.id != "table"
    ):  # Currently it will always return true. For we hardcode the table id to "table".
        raise ValueError("Scene table id must be 'table'.")

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:216

  • Spelling/grammar in the comment makes it harder to read ("weuse", "processings").
        rendered_mask = (  # If weuse outline, then need to do some another processings.
            mask if mask_style == "fill" else _mask_outer_outline(mask, image.size)

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:469

  • RigidObjectCfg.max_convex_hull_num / acd_method are marked deprecated in embodichain.lab.sim.cfg.RigidObjectCfg (prefer MeshCfg.max_convex_hull_num / MeshCfg.acd_method). Since this is new code, set these on MeshCfg instead of the top-level cfg to match the current API and avoid relying on legacy overrides.

This issue also appears on line 475 of the same file.

            RigidObjectCfg(
                uid=table_id,
                shape=MeshCfg(fpath=str(table_mesh_path)),
                init_pos=tuple(table_rigid_layout["pos"]),
                init_rot=tuple(

embodichain/gen_sim/scene_engine/cli/preview.py:160

  • RigidObjectCfg.max_convex_hull_num / acd_method are deprecated in favor of MeshCfg.max_convex_hull_num / MeshCfg.acd_method (see embodichain.lab.sim.cfg.RigidObjectCfg). For new code, attach VHACD settings to the MeshCfg so they stay with the shape config.
            RigidObjectCfg(
                uid=uid,
                shape=MeshCfg(fpath=str(mesh_path)),
                # Keep every preview body static: exported poses are already the
                # final gravity-settled poses and should not be simulated again.

embodichain/gen_sim/scene_engine/pipeline/gym_export.py:189

  • PR description says VHACD is the default collision decomposition method, but the exported gym config does not specify any acd_method. In EmbodiChain, MeshCfg.acd_method defaults to "coacd" (see embodichain/lab/sim/shapes.py), so consumers loading this gym config via RigidObjectCfg.from_dict() will still decompose with CoACD unless explicitly overridden.
        "shape": {
            "shape_type": "Mesh",
            "fpath": asset_relative_path,
            "compute_uv": False,
        },

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:55

  • This PR introduces substantial new parsing/validation logic (e.g., RLE decoding, IoU-based mask merging, strict VLM JSON parsing) but does not add unit tests. The repo already has tests/gen_sim/ coverage for similar geometry/pipeline utilities, so adding focused tests would help prevent regressions (e.g., RLE decode/encode round-trip, union_overlapping_mask_candidates grouping, and error handling for malformed payloads).
def decode_rle_mask(mask_rle: dict[str, Any]) -> Image.Image:
    """Decode an uncompressed RLE mask into a binary image."""

    # Check the return value's format.
    size = mask_rle.get("size")
    counts = mask_rle.get("counts")
    if (
        not isinstance(size, list)
        or len(size) != 2
        or not all(isinstance(value, int) and value > 0 for value in size)
    ):
        raise ValueError("Image Segmentation Server RLE needs size=[height, width].")
    if not isinstance(counts, list):
        raise ValueError("Image Segmentation Server RLE counts must be a list.")

Copilot AI review requested due to automatic review settings July 30, 2026 07:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (9)

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:215

  • Typo/grammar in the inline comment ("weuse" / "processings"). This comment is user-facing for future maintainers; please correct it for clarity.
        rendered_mask = (  # If weuse outline, then need to do some another processings.

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:195

  • Image.open(...).convert(...) leaves the file handle open until the Image is closed. Wrap the open in a context manager so repeated calls don't leak descriptors.
    image = Image.open(image_path).convert("RGBA")

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:510

  • Calling SimulationManager's private _deferred_destroy() ties this pipeline to an internal cleanup implementation. Prefer using the public destroy(exit_process=False) plus SimulationManager.flush_cleanup_queue() to guarantee cleanup without relying on a private method.
    finally:
        sim._deferred_destroy()

embodichain/gen_sim/scene_engine/pipeline/generate.py:80

  • image_segmentation_client.close() is not in a finally block, so an exception during segmentation/validation will leak the underlying requests.Session. Wrap the segmentation call in try/finally to always close the client.
    image_segmentation_client = ImageSegmentationClient.from_config(
        image_segmentation_config_path
    )
    image_segmentation_client.check_health()  # Error raising will happen internally.
    scene = segment_scene(

embodichain/gen_sim/scene_engine/pipeline/scene_understanding.py:180

  • The inline comment says the condition "will always return true", but the code hardcodes id="table", so the condition should never be true. Updating the comment avoids misleading future debugging.
    if (
        scene.table.id != "table"
    ):  # Currently it will always return true. For we hardcode the table id to "table".
        raise ValueError("Scene table id must be 'table'.")

embodichain/gen_sim/scene_engine/pipeline/generate.py:54

  • This PR adds a large new scene-engine pipeline (VLM JSON validation, RLE decoding, layout transforms, export schema) but does not add any tests. The repo already has tests/gen_sim/simready_pipeline/*, so adding focused unit tests (e.g., RLE decode/encode round-trip, transform_matrix<->layout_object invariants, exported scene_config schema) would prevent regressions.
def generate_scene_from_image(
    image_path: str | Path,
    output_root: str | Path,
    *,
    llm_config_path: str | Path | None = None,
    image_segmentation_config_path: str | Path | None = None,
    geometry_generation_config_path: str | Path | None = None,
) -> Scene:

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:170

  • Image files are opened without a context manager here (and in the mask loop). PIL keeps the underlying file handle open until the Image is closed, which can exhaust file descriptors in batch runs. Use with Image.open(...) and keep only the converted in-memory image.

This issue also appears on line 195 of the same file.

    image = Image.open(image_path).convert("RGB")
    ignored_mask = Image.new("L", image.size, 0)
    for mask_path in mask_paths:
        mask = Image.open(mask_path).convert("L")
        _require_image_size(mask, image.size)

embodichain/gen_sim/scene_engine/pipeline/generate.py:106

  • geometry_generation_client.close() is not protected by finally, so failures during geometry generation/refinement can leak the requests.Session. Use try/finally around the client usage to ensure it is always closed.
    geometry_generation_client = GeometryGenerationClient.from_config(
        geometry_generation_config_path
    )
    geometry_generation_client.check_health()

    scene = generate_scene_and_refine(

embodichain/gen_sim/scene_engine/cli/preview.py:90

  • In interactive mode the preview loop never advances the simulation (sim.update(...)). If physics is manually stepped (the common case in SimulationManager), the window may never render/refresh. Stepping at a low rate keeps the viewer responsive and consistent with the headless path.
        sim.open_window()
        while True:
            time.sleep(0.1)

Copilot AI review requested due to automatic review settings July 30, 2026 08:22
Copilot AI review requested due to automatic review settings July 31, 2026 03:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 38 out of 38 changed files in this pull request and generated no new comments.

Suppressed comments (4)

embodichain/gen_sim/scene_engine/cli/preview.py:112

  • The preview loop only sleeps and never advances the simulation after opening the window / starting Viser. SimulationManager.open_window() does not run a render/update loop by itself, so the preview will appear frozen. Call sim.update(step=1) inside the loop (optionally with a small sleep to throttle).
        print("Close with Ctrl-C.")
        while True:
            time.sleep(0.1)

docs/source/features/generative_sim/scene_engine.md:70

  • The documented segment_single_object_path does not match the packaged Scene Engine config (embodichain/gen_sim/scene_engine/configs/scene_engine_config.json uses /predict). This can cause copy/paste configs from the docs to fail against the default service settings.
    "max_attempts": 3,
    "health_path": "/health",
    "segment_single_object_path": "/segment_single_object"
  },

docs/source/features/generative_sim/scene_engine.md:77

  • The documented generate_objects_path does not match the packaged Scene Engine config (embodichain/gen_sim/scene_engine/configs/scene_engine_config.json uses /generate_multiple_objects). Aligning the doc example with the packaged default reduces setup friction.
    "max_attempts": 3,
    "health_path": "/health",
    "generate_objects_path": "/generate_objects"
  }

embodichain/gen_sim/scene_engine/llms/load_config.py:65

  • This line is likely over Black's default line length and will be reformatted (or may fail format checks if CI enforces black). Split it across lines to keep the file Black-stable.
    base_url = os.getenv("OPENAI_BASE_URL") or llm_config.get("base_url", "")
    default_query = llm_config.get("default_query", {})
    max_attempts = os.getenv("OPENAI_MAX_ATTEMPTS") or llm_config.get("max_attempts", 3)

Comment thread embodichain/gen_sim/scene_engine/utils/logger.py Outdated
Copilot AI review requested due to automatic review settings July 31, 2026 04:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.

Suppressed comments (2)

embodichain/gen_sim/scene_engine/pipeline/scene_understanding.py:180

  • The inline comment is incorrect: since the table id is hard-coded to "table" in validate_scene_understanding_json(), this condition should normally be false. The current wording (“always return true”) is misleading for future debugging.
    if (
        scene.table.id != "table"
    ):  # Currently it will always return true. For we hardcode the table id to "table".
        raise ValueError("Scene table id must be 'table'.")

embodichain/gen_sim/scene_engine/cli/preview.py:112

  • The preview loop never advances the SimulationManager, so neither the native window nor the Viser backend will receive regular updates. Other preview tooling keeps the visualizer alive by calling sim.update(step=1) in the main loop (e.g., embodichain/lab/scripts/preview_asset.py:255-258).
        while True:
            time.sleep(0.1)

Copilot AI review requested due to automatic review settings July 31, 2026 06:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.

Suppressed comments (5)

embodichain/gen_sim/scene_engine/cli/start.py:86

  • The CLI flag --config is described as optional, but the pipeline always loads ImageSegmentationClient.from_config() / GeometryGenerationClient.from_config() which reject empty base_url and other required keys. With the packaged config containing placeholders, running without --config will fail immediately, so the argument should be required (or the code should provide an alternative config source).
    parser.add_argument(
        "--config",
        type=Path,
        default=None,
        help=(
            "Optional Scene Engine JSON config containing the llm, "
            "image_segmentation, and geometry_generation service settings."
        ),
    )

docs/source/guides/cli.md:102

  • This section says the command requires a config, but the argument table lists --config defaulting to a “packaged config”. The packaged config file is a template with required fields left blank, so it is not a usable default. Update the table to mark --config as required (or clarify that the packaged config is only a template).
| ``--image`` | *(required)* | Input ``.jpg``, ``.jpeg``, or ``.png`` scene image |
| ``--output_root`` | *(required)* | Directory that receives intermediate artifacts and ``scene_export/`` |
| ``--config`` | packaged config | Scene Engine JSON config containing ``llm``, ``image_segmentation``, and ``geometry_generation`` settings |

docs/source/features/generative_sim/scene_engine.md:70

  • The example config uses segment_single_object_path: "/segment_single_object", but the packaged Scene Engine config shipped with this PR uses "/predict". Either align the example with the packaged default, or explicitly call out that these paths are service-specific placeholders so users don't copy/paste a non-working endpoint.
  "image_segmentation": {
    "base_url": "http://segmentation-host:port",
    "timeout_s": 120,
    "max_attempts": 3,
    "health_path": "/health",
    "segment_single_object_path": "/segment_single_object"
  },

docs/source/features/generative_sim/scene_engine.md:77

  • Similarly, the example config uses generate_objects_path: "/generate_objects", but the packaged config in this PR uses "/generate_multiple_objects". Aligning them avoids users copying a config that won't match the default client settings.
  "geometry_generation": {
    "base_url": "http://geometry-host:port",
    "timeout_s": 600,
    "max_attempts": 3,
    "health_path": "/health",
    "generate_objects_path": "/generate_objects"
  }

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:217

  • Minor grammar in the inline comment (“some another processings”) makes this section harder to read/maintain while debugging mask rendering.
        rendered_mask = (  # If we use outline, then need to do some another processings.
            mask if mask_style == "fill" else _mask_outer_outline(mask, image.size)

Copilot AI review requested due to automatic review settings July 31, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.

Suppressed comments (4)

embodichain/gen_sim/scene_engine/pipeline/scene_understanding.py:180

  • The inline comment here is inaccurate/unclear (the table id is hard-coded to "table" in validate_scene_understanding_json(), so this condition should normally be false). It’s better to remove the comment to avoid confusion during debugging.
    if (
        scene.table.id != "table"
    ):  # Currently it will always return false. For we hardcode the table id to "table".
        raise ValueError("Scene table id must be 'table'.")

embodichain/gen_sim/scene_engine/cli/preview.py:112

  • The preview loop never calls sim.update(), so the simulation window / Viser session may not render or process events after opening. This effectively turns the command into an idle sleep loop.
        while True:
            time.sleep(0.1)

embodichain/gen_sim/scene_engine/llms/load_config.py:64

  • This line exceeds typical Black formatting and is the only long unwrapped assignment in this file. Wrapping it improves consistency and avoids future formatting churn.
    max_attempts = os.getenv("OPENAI_MAX_ATTEMPTS") or llm_config.get("max_attempts", 3)

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:38

  • Calling matplotlib.use("Agg") at import time can raise if pyplot has already been imported elsewhere, and it also globally overrides the user's chosen Matplotlib backend (e.g., notebooks). Consider guarding this so it only runs when safe/needed.
matplotlib.use("Agg")

Copilot AI review requested due to automatic review settings July 31, 2026 08:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

embodichain/gen_sim/scene_engine/cli/preview.py:112

  • The preview loop never advances the simulation/rendering; it only sleeps. Unlike preview_asset.py, which continuously calls sim.update(step=1), this will typically leave the window/visualization frozen after the first frame (and may not publish updates to Viser). Replace the sleep-only loop with a loop that steps the simulation.
            print(f"Previewing: {config_path}")
            sim.open_window()
        print("Close with Ctrl-C.")
        while True:
            time.sleep(0.1)

embodichain/gen_sim/scene_engine/clients/image_segmentation.py:125

  • The error check response_data.get("ok") is False is too narrow: it will treat ok=0 (or other non-True values) as success and silently return empty masks. If the server returns an ok field, it should be treated as a strict success indicator and raise for anything other than True.
                if not isinstance(response_data, dict):
                    raise RuntimeError(
                        "Image Segmentation Server response must be a JSON object."
                    )
                if response_data.get("ok") is False:
                    raise RuntimeError(
                        "Image Segmentation Server request failed: "
                        f"{response_data.get('error', 'unknown error')}"
                    )
                return _extract_rle_masks(response_data)

docs/source/guides/cli.md:102

  • The CLI docs claim --config defaults to a "packaged config", but the packaged template has empty service URLs and the CLI will fail without a user-provided config. Update this default/description to reflect the actual behavior (e.g., mark required, or clarify that the packaged file is only a template and must be filled in).
| Argument | Default | Description |
|---|---|---|
| ``--image`` | *(required)* | Input ``.jpg``, ``.jpeg``, or ``.png`` scene image |
| ``--output_root`` | *(required)* | Directory that receives intermediate artifacts and ``scene_export/`` |
| ``--config`` | packaged config | Scene Engine JSON config containing ``llm``, ``image_segmentation``, and ``geometry_generation`` settings |

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:34

  • matplotlib.use("Agg") is executed at import time, which globally overrides the Matplotlib backend for the entire Python process whenever scene_generation_utils is imported (the pipeline imports it unconditionally). This can break interactive usage (e.g., Qt backends) in downstream code. Prefer setting the backend only inside the debug-rendering helper(s), or guarding it so it does not override a user-selected backend.
matplotlib.use("Agg")

Comment on lines +78 to +86
parser.add_argument(
"--config",
type=Path,
default=None,
help=(
"Optional Scene Engine JSON config containing the llm, "
"image_segmentation, and geometry_generation service settings."
),
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for your reviewing, I have already fix this problem and updated relevant docs :)

Copilot AI review requested due to automatic review settings July 31, 2026 10:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 38 out of 38 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:196

  • render_numbered_mask_candidates opens the source image without closing it. Using a with Image.open(...) block avoids leaking file descriptors when this helper is called repeatedly.
    image = Image.open(image_path).convert("RGBA")
    overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))

embodichain/gen_sim/scene_engine/pipeline/utils/scene_segmentation_utils.py:170

  • Image.open(...) is used without a context manager in render_image_without_masks. PIL keeps file handles open until the image object is closed/GC’d, which can leak descriptors when processing many images/masks.

This issue also appears on line 195 of the same file.

    image = Image.open(image_path).convert("RGB")
    ignored_mask = Image.new("L", image.size, 0)
    for mask_path in mask_paths:
        mask = Image.open(mask_path).convert("L")
        _require_image_size(mask, image.size)

embodichain/gen_sim/scene_engine/pipeline/scene_understanding.py:179

  • The inline comment is grammatically incorrect ("For we hardcode...") and makes the intent harder to read.
    ):  # Currently it will always return false. For we hardcode the table id to "table".

Comment on lines +152 to +153
output_path = resolved_output_root / f"{object_id}.glb"
self._download_glb(response_object["mesh"], output_path)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you, dear Copilot~ I have fix this problem and updated the test script.

Comment on lines +167 to +169
mesh_path = (config_dir / shape["fpath"]).resolve()
if not mesh_path.is_file():
raise FileNotFoundError(f"Gym mesh for {uid!r} not found: {mesh_path}")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for your review. I have fix this problem and updated the test script.

Copilot AI review requested due to automatic review settings July 31, 2026 10:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.

Suppressed comments (3)

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:484

  • RigidObjectCfg.max_convex_hull_num and RigidObjectCfg.acd_method are deprecated (MeshCfg owns these settings). Keeping them on RigidObjectCfg risks the convex-decomposition settings being ignored if/when the deprecated fields are removed.

Move these parameters onto the MeshCfg for each asset body.

                    max_convex_hull_num=max_convex_hull_num,
                    acd_method="vhacd",  # Use vhacd by default.

embodichain/gen_sim/scene_engine/cli/preview.py:202

  • RigidObjectCfg.max_convex_hull_num and RigidObjectCfg.acd_method are marked deprecated in the simulation config (use MeshCfg.max_convex_hull_num / MeshCfg.acd_method instead). Setting the deprecated fields here may be ignored in future refactors and duplicates the same settings across two config objects.

Consider moving these settings onto MeshCfg and removing the deprecated RigidObjectCfg fields.

                max_convex_hull_num=max_convex_hull_num,
                acd_method="vhacd",  # Use vhacd by default.

embodichain/gen_sim/scene_engine/pipeline/utils/scene_generation_utils.py:469

  • RigidObjectCfg.max_convex_hull_num and RigidObjectCfg.acd_method are deprecated (the active fields live on MeshCfg). To avoid relying on deprecated configuration that may be removed/ignored, set max_convex_hull_num/acd_method on the MeshCfg instance instead.

This issue also appears on line 483 of the same file.

                max_convex_hull_num=max_convex_hull_num,
                acd_method="vhacd",  # Use vhacd by default.

@yuecideng yuecideng left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The overall layering is strong: the staged pipeline, strict service-boundary validation, explicit coordinate-frame handling, debug artifacts, and portable scene export form a solid end-to-end MVP. The inline comments below focus on the gaps that should be addressed before treating this as a robust scene reconstruction engine. P0 marks scene-correctness risks; P1 marks reliability and maintainability risks.

## Quick Start

Install EmbodiChain with the generative-simulation dependencies. See
[Installation (gensim extra)](../../quick_start/install.md#optional-generative-simulation-gensim).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] The gensim extra currently declares only bpy and pyrender, while Scene Engine directly imports requests, Pillow, SciPy, trimesh, Open3D, and matplotlib. A clean embodichain[gensim] installation therefore relies on undeclared transitive dependencies. Please declare these dependencies directly, possibly under a dedicated scene-engine extra, and exercise that installation path in CI.

# Absolute path to the canonicalized GLB used by the final simulation.
simready_glb_path: str | None = None
# Final y-up layout after scene refinement and gravity settling.
rot: list[float] | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] This mutable dataclass represents every pipeline stage through nullable fields, while Table duplicates almost the same shape. That makes invalid state combinations representable until a late export-time check. Please consider a shared SceneObject model with typed pose/frame/artifact fields, or stage-specific validated models. The scene IR will also need support relations and confidence/provenance if stacked or contained objects are to be preserved.

*,
llm_config_path: str | Path | None = None,
image_segmentation_config_path: str | Path | None = None,
geometry_generation_config_path: str | Path | None = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] One Scene Engine JSON file is exposed here as three independently parsed paths, while important behavior such as IoU thresholds, retry counts, packing clearances, settle steps, physics settings, and hull counts remains hard-coded elsewhere. Please load and validate one project-style @configclass SceneEngineCfg, then inject the relevant sub-config into each stage/client. This would also make runs reproducible through a single config snapshot.

# Create stage output directory.
stage_output_root = Path(output_root).expanduser().resolve() / "scene_generation"
if stage_output_root.exists():
shutil.rmtree(stage_output_root)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] This removes the previous, potentially expensive generation artifacts before a replacement has completed. Any service or simulation failure leaves a partial stage and prevents resuming. Please build each stage in a temporary directory, validate it, write an input/config/model manifest, and atomically replace the completed stage. A --resume/--from-stage path would be especially valuable for this multi-service pipeline.

:, :2
] # Ignore z, for we wanna get the x-y plane projection.
try:
projected_hull = ConvexHull(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P0] This is the convex hull of the projection of the entire table mesh, not the actual top support surface. It fills concavities and holes and may include footprints from legs or overhanging geometry, so the packer can place assets over unsupported space. Please extract near-top, upward-facing triangles (or a support height field/polygon) and validate candidate footprints with downward raycasts. The existing comment about L-shaped tables indicates this is a known correctness limitation.


# All assets share this one simulation, so they can collide with the
# table and with one another while settling.
sim.update(step=settle_steps)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P0] A fixed number of simulation steps is not a convergence or validity criterion. Before exporting, please require linear/angular velocities to remain below thresholds for several frames and validate finite poses, table support, footprint containment, and unacceptable penetration/fall-off. If those postconditions fail, the pipeline should retry layout/settling or return a clear quality failure rather than exporting the pose.

"dynamic_friction": 0.9,
"restitution": 0.01,
}
_ASSET_PHYSICS_ATTRS = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] The settling simulation uses RigidObjectCfg defaults and 32 convex hulls, while the exported assets use these different physical attributes and 16 hulls. The final dynamic scene may therefore not reproduce the behavior under which its pose was settled. Please share the same object physics and collision-decomposition configuration between settlement, export, and downstream loading.


export_root = Path(output_root).expanduser().resolve() / "scene_export"
mesh_assets_root = export_root / "mesh_assets"
mesh_assets_root.mkdir(parents=True, exist_ok=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] scene_export is neither refreshed nor written atomically. Re-running into the same output root with fewer objects leaves stale mesh directories, and a copy/write failure can leave a config and asset set from different runs. Please assemble a fresh temporary export, validate all entries, then atomically replace the previous directory.

object_masks: list[tuple[str, Path]],
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
last_error: Exception | None = None
for _ in range(self._max_attempts):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Retrying the entire submission immediately can create duplicate expensive geometry jobs when the server accepted the request but the response was lost. Please separate submit/poll/download retry policies, add exponential backoff and jitter, and use an idempotency key or persisted request ID. The polling endpoint, interval, and 600-second deadline should also come from configuration.

)


def test_layout_transform_round_trip_preserves_pose_and_scale() -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] The current tests cover a transform round trip and a simple AABB happy path, but not the high-risk behavior introduced here. Please add fixtures for concave/rotated/asymmetric tables, stacked or contained assets, dense/infeasible packing, gravity convergence and fall-off checks, queued geometry jobs, and a mocked end-to-end pipeline. Golden-scene metrics such as support validity and relation retention would make scene quality regressions visible.

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.

4 participants