Skip to content

[Proposal] Optimization of default built solver_cfg for dexforceW1 #120

@yhnsu

Description

@yhnsu

Proposal: Refactor DexforceW1Cfg solver initialization (PytorchSolver vs SRSSolver overwrite)

File: embodichain/lab/sim/robots/dexforce_w1/cfg.py
Class: DexforceW1Cfg


Current behavior

@configclass
class DexforceW1Cfg(RobotCfg):
    ...

    @classmethod
    def from_dict(
        cls, init_dict: Dict[str, str | float | tuple | dict]
    ) -> DexforceW1Cfg:
        ...
        init_dict_m = init_dict.copy()
        version = init_dict_m.get("version", "v021")
        arm_kind = init_dict_m.get("arm_kind", "anthropomorphic")
        with_default_eef = init_dict_m.get("with_default_eef", True)
        init_dict_m.pop("version", None)
        init_dict_m.pop("arm_kind", None)
        init_dict_m.pop("with_default_eef", None)

        # 1) Build base cfg via utils (this already initializes PytorchSolver solver_cfg based on URDF + control_parts)
        cfg: DexforceW1Cfg = cls()._build_default_cfg(
            version=version, arm_kind=arm_kind, with_default_eef=with_default_eef
        )

        # 2) Merge default physics configs
        default_physics_cfgs = cls()._build_default_physics_cfgs(
            arm_kind=arm_kind, with_default_eef=with_default_eef
        )
        for key, value in default_physics_cfgs.items():
            setattr(cfg, key, value)

        # 3) Build SRSSolver configs for both arms
        default_solver_cfg = cls()._build_default_solver_cfg(
            is_industrial=(arm_kind == "industrial")
        )

        # 4) ⚠️ This line overwrites the PytorchSolver-based solver_cfg created in _build_default_cfg
        cfg.solver_cfg = default_solver_cfg

        # 5) Finally merge external overrides
        cfg = merge_robot_cfg(cfg, init_dict_m)

        return cfg

Notes:

  • _build_default_cfg() internally calls build_dexforce_w1_cfg() (in dexforce_w1/utils.py), which already builds a complete solver_cfg based on the assembled URDF and control_parts, including:

    • full_body
    • left_arm_to_torso
    • right_arm_to_torso
    • other whole-body / torso-related control parts as PytorchSolverCfg.
  • _build_default_solver_cfg() only returns SRSSolver configs for the two 7-DoF arms:

@staticmethod
def _build_default_solver_cfg(is_industrial: bool) -> SolverCfg:
    from embodichain.lab.sim.solvers import SRSSolverCfg
    from embodichain.lab.sim.robots.dexforce_w1.params import W1ArmKineParams

    if is_industrial:
        ...
    else:
        ...

    return {
        "right_arm": SRSSolverCfg(...),
        "left_arm": SRSSolverCfg(...),
    }

As a result:

  • build_dexforce_w1_cfg() first initializes a rich set of PytorchSolver entries (full_body / *_arm_to_torso / ...).
  • Then cfg.solver_cfg = default_solver_cfg replaces the entire dict with a much smaller one that only contains "left_arm" and "right_arm" SRSSolver entries.
  • All PytorchSolver-based configs built in utils.build_dexforce_w1_solver_cfg are effectively discarded; their initialization work becomes meaningless and the final behavior is non‑obvious to users.

Why this is problematic

DexforceW1 is designed to support two complementary IK solver families:

  • URDF-based PytorchSolver:

    • Used for whole-body / torso-related control parts such as full_body, left_arm_to_torso, right_arm_to_torso, etc.
  • DH-based SRSSolver:

    • Used for the 7-DoF arms: left_arm, right_arm (high-quality, analytic-like IK).

The current from_dict() implementation:

  • discards the PytorchSolver-based entries created in _build_default_cfg / build_dexforce_w1_cfg,
  • keeps only the SRSSolver entries for left_arm / right_arm,
  • breaks the intended layering: “whole-body PytorchSolver” + “per-arm SRSSolver”.

It also makes the code path harder to reason about:
logging solver_cfg right after build_dexforce_w1_cfg() shows rich PytorchSolver configs, but the final DexforceW1Cfg.solver_cfg that Robot.init_solver sees is a different, reduced set.


Proposed improvement

Goals:

  • Preserve the PytorchSolver-based solver configs produced by build_dexforce_w1_cfg() for:

    • full_body
    • left_arm_to_torso
    • right_arm_to_torso
    • etc.
  • Still provide SRSSolver-based defaults for:

    • "left_arm"
    • "right_arm"
  • Allow external robot.solver_cfg in user configs to override individual fields (e.g., tcp, ik_nearest_weight) instead of replacing the entire solver_cfg dict.

Suggested change (only in the solver initialization part of from_dict):

@classmethod
def from_dict(
    cls, init_dict: Dict[str, str | float | tuple | dict]
) -> DexforceW1Cfg:
    ...
    cfg: DexforceW1Cfg = cls()._build_default_cfg(
        version=version, arm_kind=arm_kind, with_default_eef=with_default_eef
    )

    default_physics_cfgs = cls()._build_default_physics_cfgs(
        arm_kind=arm_kind, with_default_eef=with_default_eef
    )
    for key, value in default_physics_cfgs.items():
        setattr(cfg, key, value)

    # Build SRSSolver defaults for left/right arms
    default_solver_cfg = cls()._build_default_solver_cfg(
        is_industrial=(arm_kind == "industrial")
    )

    # ✅ Proposed: merge instead of overwrite.
    # If build_dexforce_w1_cfg did not set solver_cfg, just use SRSSolver defaults.
    # Otherwise, only update "left_arm"/"right_arm" with SRSSolver, keeping
    # full_body / left_arm_to_torso / right_arm_to_torso PytorchSolver entries.
    if cfg.solver_cfg is None:
        cfg.solver_cfg = default_solver_cfg
    else:
        cfg.solver_cfg.update(default_solver_cfg)

    cfg = merge_robot_cfg(cfg, init_dict_m)
    return cfg

With this:

  • build_dexforce_w1_cfg() + build_dexforce_w1_solver_cfg() are responsible for:

    • default control_parts layout (torso, head, left_arm, right_arm, full_body, ...);
    • URDF-based PytorchSolverCfg for full_body, left_arm_to_torso, right_arm_to_torso, etc.
  • _build_default_solver_cfg() is responsible only for:

    • providing SRSSolverCfg for "left_arm" and "right_arm" (used to overwrite or supplement those keys).
  • External init_dict["solver_cfg"]:

    • can safely override fields (e.g., tcp, ik_nearest_weight) via merge_robot_cfg without unintentionally wiping out all other solver entries.

Summary

Currently, the PytorchSolver-based solver_cfg built in _build_default_cfg (via build_dexforce_w1_cfg) is entirely overwritten in from_dict by _build_default_solver_cfg, making the earlier initialization effectively useless.

By changing the logic to merge (cfg.solver_cfg.update(default_solver_cfg)) instead of blindly overwriting, we can:

  • keep whole-body / torso PytorchSolver IK intact,
  • still use SRSSolver for arm-specific IK,
  • and make the configuration behavior much easier to understand and extend.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrobotModule related to robotsolver

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions