diff --git a/agent_context/topics/robot-system/robot-system.md b/agent_context/topics/robot-system/robot-system.md
index 139f64de5..bf7e4ce9a 100644
--- a/agent_context/topics/robot-system/robot-system.md
+++ b/agent_context/topics/robot-system/robot-system.md
@@ -33,7 +33,7 @@ ObjectBaseCfg uid, init_pos, init_rot, init_local_pose
│ disable_self_collision, init_qpos, body_scale,
│ build_pk_chain, use_usd_properties
└─ RobotCfg control_parts, urdf_cfg, solver_cfg, drive_pros (override default to "force")
- ├─ DexforceW1Cfg version, arm_kind, with_default_eef
+ ├─ DexforceW1Cfg version, hand_versions, with_default_eef
└─ CobotMagicCfg (dual-arm defaults)
```
@@ -46,13 +46,13 @@ Key fields on `RobotCfg`:
| `solver_cfg` | `SolverCfg \| Dict[str, SolverCfg] \| None` | IK solver config; dict keys must match `control_parts` keys |
| `drive_pros` | `JointDrivePropertiesCfg` | Default drive type is `"force"` (overrides Articulation's `"none"`) |
| `attrs` | `RigidBodyAttributesCfg` | Rigid-body physics attributes (mass, friction, damping, ...) |
-| variant fields | `enum \| str \| bool` | Optional subclass fields (e.g. `version`, `arm_kind`, `with_default_eef`) |
+| variant fields | `enum \| str \| bool` | Optional subclass fields (e.g. `version`, `with_default_eef`) |
| `_pk_urdf_path` | `property \| method → str` | URDF for the FK/IK serial chain (one source, so it can't drift from sim) |
## The robot config protocol
-Every robot config subclasses `RobotCfg` and overrides two hooks. `from_dict` is a
-3-line template — do not reimplement it:
+Every robot config subclasses `RobotCfg` and overrides the construction hooks.
+The default `from_dict` implementation is this 3-line template:
```python
@classmethod
@@ -69,8 +69,26 @@ def from_dict(cls, init_dict):
reading the PK URDF from a single `_pk_urdf_path` source (a property for
constant-path robots, a method when the path depends on a variant).
-Serialization (`to_dict` / `to_string` / `save_to_file`) is **inherited** from
-`RobotCfg` and round-trips: `RobotCfg.from_dict(cfg.to_dict())` reproduces the cfg.
+Serialization (`to_dict` / `to_string` / `save_to_file`) is inherited from
+`RobotCfg` unless the config stores version-derived runtime transforms.
+`DexforceW1Cfg` is the current exception: serialized hand transforms and solver
+TCPs are raw end-effector values, while the in-memory values include the selected
+W1 revision offset. Its `to_dict` removes that derived offset and `from_dict`
+restores it. Every config, including this exception, must satisfy
+`type(cfg).from_dict(cfg.to_dict())` without changing the selected components or
+applying a derived transform twice.
+
+W1 robot and hand releases use separate types and registries:
+
+- `DexforceW1Version` selects body/arm assets, kinematics, and flange calibration
+ through `specs.py`.
+- `DexforceW1HandVersion` selects external hand/gripper assets, joint metadata,
+ and raw mounting transforms through `hand_specs.py`.
+- The current default is hand V021 for every W1 robot version. Never infer a
+ hand version from `DexforceW1Version`.
+- `DexforceW1Cfg` always represents a complete dual-arm W1. Structural
+ `include_*`, `arm_sides`, and mixed `component_versions` options are not part
+ of its public protocol.
.. note::
`merge_robot_cfg` calls the base `RobotCfg.from_dict` internally, so the
@@ -119,7 +137,7 @@ Full guide: `docs/source/tutorial/add_robot.rst` · Quick reference: `docs/sourc
Minimal checklist:
1. Create a `@configclass` inheriting `RobotCfg`.
2. Override `_build_defaults(self, init_dict=None)` — read variant fields from `init_dict`, then populate `urdf_cfg`, `control_parts`, `solver_cfg`, `drive_pros` and `attrs`.
-3. Keep `from_dict` as the 3-line template (`cls()` → `_build_defaults` → `merge_robot_cfg`); do not reimplement.
+3. Keep `from_dict` as the 3-line template (`cls()` → `_build_defaults` → `merge_robot_cfg`) unless version-derived state requires an explicitly documented post-merge step.
4. Define `control_parts` mapping part names to joint name lists.
5. Configure `solver_cfg` (one `SolverCfg` per control part).
6. Implement `build_pk_serial_chain` reading from `_pk_urdf_path` (property for constant paths, method for variant-dependent).
@@ -128,13 +146,15 @@ Minimal checklist:
9. Add robot docs in `docs/source/resources/robot/` and update `docs/source/resources/robot/index.rst`.
10. Test — a `__main__` smoke test + the DOF drift guard + `preview-asset` CLI.
-Serialization (`to_dict` / `save_to_file`) is inherited — no need to implement it.
+Serialization (`to_dict` / `save_to_file`) is normally inherited. A robot-specific
+override requires documented raw/final semantics and regression tests for default,
+custom-transform, component-version, and public-builder round-trips.
## Available Robots
| Robot | Config Class | Module | Structure | Notes |
|---|---|---|---|---|
-| DexForce W1 | `DexforceW1Cfg` | `embodichain/lab/sim/robots/dexforce_w1/` | Package (`cfg.py`, `types.py`, `params.py`, `utils.py`) | Humanoid; versions: V021; arm kinds: ANTHROPOMORPHIC, INDUSTRIAL; component types: chassis, torso, eyes, head, left/right arm/hand |
+| DexForce W1 | `DexforceW1Cfg` | `embodichain/lab/sim/robots/dexforce_w1/` | Package (`cfg.py`, `types.py`, `specs.py`, `hand_specs.py`, `params.py`, `utils.py`) | Humanoid; robot and hand versions are independently registered |
| CobotMagic | `CobotMagicCfg` | `embodichain/lab/sim/robots/cobotmagic.py` | Single file | Dual-arm; 6-DOF arms + 2-DOF grippers; uses OPW solver |
## Common Failure Modes
@@ -148,4 +168,4 @@ Serialization (`to_dict` / `save_to_file`) is inherited — no need to implement
- **`all` instead of `__all__`** — lowercase `all` does not work with `from module import *`; use `__all__`.
- **`solver_cfg` set in multiple places** — set it once in `_build_defaults` only; setting it elsewhere (e.g. a build helper) gets overwritten and is dead code.
- **PK URDF drifts from the sim URDF** — route `build_pk_serial_chain` through `_pk_urdf_path` and keep the DOF drift-guard test so silent drift is caught.
-- **Reimplementing `from_dict`** — keep the 3-line template; put construction logic in `_build_defaults`. (Making the base `RobotCfg.from_dict` call `merge_robot_cfg` would infinite-recurse, since `merge_robot_cfg` calls `RobotCfg.from_dict`.)
+- **Reimplementing `from_dict` without a serialization protocol** — keep the 3-line template by default. If derived version state requires post-merge processing, document the raw/final values and test round-trips. (Making the base `RobotCfg.from_dict` call `merge_robot_cfg` would infinite-recurse, since `merge_robot_cfg` calls `RobotCfg.from_dict`.)
diff --git a/docs/source/_static/robots/dexforcew1_anthropomorphic.jpg b/docs/source/_static/robots/dexforcew1.jpg
similarity index 100%
rename from docs/source/_static/robots/dexforcew1_anthropomorphic.jpg
rename to docs/source/_static/robots/dexforcew1.jpg
diff --git a/docs/source/_static/robots/dexforcew1_industrial.jpg b/docs/source/_static/robots/dexforcew1_industrial.jpg
deleted file mode 100644
index a3c4b9bbc..000000000
Binary files a/docs/source/_static/robots/dexforcew1_industrial.jpg and /dev/null differ
diff --git a/docs/source/features/workspace_analyzer/cli.md b/docs/source/features/workspace_analyzer/cli.md
index c1698fc7f..2e53a2de6 100644
--- a/docs/source/features/workspace_analyzer/cli.md
+++ b/docs/source/features/workspace_analyzer/cli.md
@@ -39,19 +39,19 @@ to `left_arm`/`right_arm` or the first available part).
| Argument | Description |
|----------|-------------|
| `--robot NAME` | Predefined robot. Choices: `franka_panda`, `cobotmagic`, `dexforce_w1`, `ur`. |
-| `--robot-params JSON` | JSON dict of variant overrides, e.g. `{"robot_type":"ur5"}` or `{"version":"v021","arm_kind":"industrial"}`. |
+| `--robot-params JSON` | JSON dict of variant overrides, e.g. `{"robot_type":"ur5"}` or `{"version":"v025","with_default_eef":false}`. |
| `--control-part NAME` | Control part to analyze (e.g. `arm`, `left_arm`, `right_arm`). Optional; auto-selected if omitted. |
```bash
embodichain analyze-workspace \
--robot dexforce_w1 \
- --robot-params '{"version":"v021","arm_kind":"industrial"}' \
+ --robot-params '{"version":"v025","with_default_eef":false}' \
--control-part left_arm --mode joint_space --num-samples 20000
```
Available control parts per robot: `franka_panda` (`arm`, `hand`), `cobotmagic`
(`left_arm`, `left_eef`, `right_arm`, `right_eef`), `dexforce_w1`
-(`left_arm`, `right_arm` for industrial), `ur` (`arm`).
+(`left_arm`, `left_eef`, `right_arm`, `right_eef`), `ur` (`arm`).
## Generic asset (`--asset`)
diff --git a/docs/source/features/workspace_analyzer/workspace_analyzer.md b/docs/source/features/workspace_analyzer/workspace_analyzer.md
index 8213dd10d..2e49ff0e0 100644
--- a/docs/source/features/workspace_analyzer/workspace_analyzer.md
+++ b/docs/source/features/workspace_analyzer/workspace_analyzer.md
@@ -33,8 +33,7 @@ sim = SimulationManager(SimulationManagerCfg(headless=False, sim_device="cpu"))
# Add robot
robot = sim.add_robot(DexforceW1Cfg.from_dict({
"uid": "dexforce_w1",
- "version": "v021",
- "arm_kind": "industrial"
+ "version": "v021"
}))
# Quick analysis with defaults
@@ -175,7 +174,7 @@ sim = SimulationManager(SimulationManagerCfg(headless=False, sim_device="cpu"))
# Add robot
robot = sim.add_robot(DexforceW1Cfg.from_dict({
- "uid": "dexforce_w1", "version": "v021", "arm_kind": "industrial"
+ "uid": "dexforce_w1", "version": "v021"
}))
# 1. Joint Space Analysis
diff --git a/docs/source/guides/add_robot.rst b/docs/source/guides/add_robot.rst
index 5a4d01386..f6557f60e 100644
--- a/docs/source/guides/add_robot.rst
+++ b/docs/source/guides/add_robot.rst
@@ -73,7 +73,7 @@ Key parameters
| ``attrs`` | RigidBodyAttributesCfg | Rigid-body physics attributes |
+---------------------+----------------------------------+----------------------------------+
| variant fields | enum / str / bool | Optional subclass fields |
-| | | (e.g. ``version``, ``arm_kind``) |
+| | | (e.g. ``version``) |
+---------------------+----------------------------------+----------------------------------+
| ``_pk_urdf_path`` | property or method → str | URDF for the FK/IK serial chain |
+---------------------+----------------------------------+----------------------------------+
diff --git a/docs/source/overview/sim/solvers/srs_solver.md b/docs/source/overview/sim/solvers/srs_solver.md
index 0cd7350dc..8e2d73563 100644
--- a/docs/source/overview/sim/solvers/srs_solver.md
+++ b/docs/source/overview/sim/solvers/srs_solver.md
@@ -31,7 +31,6 @@ SRSSolver is configured via the `SRSSolverCfg` class, allowing detailed control
from embodichain.data import get_data_path
from embodichain.lab.sim.robots.dexforce_w1.types import (
DexforceW1ArmSide,
- DexforceW1ArmKind,
DexforceW1Version,
)
from embodichain.lab.sim.robots.dexforce_w1.params import (
@@ -41,7 +40,6 @@ from embodichain.lab.sim.solvers.srs_solver import SRSSolver, SRSSolverCfg
arm_params = W1ArmKineParams(
arm_side=DexforceW1ArmSide.RIGHT,
- arm_kind=DexforceW1ArmKind.ANTHROPOMORPHIC,
version=DexforceW1Version.V021,
)
diff --git a/docs/source/resources/robot/dexforce_w1.md b/docs/source/resources/robot/dexforce_w1.md
index 7160825bb..6acf8e3da 100644
--- a/docs/source/resources/robot/dexforce_w1.md
+++ b/docs/source/resources/robot/dexforce_w1.md
@@ -1,68 +1,49 @@
# Dexforce W1
-Dexforce W1 is a versatile robot developed by DexForce Technology Co., Ltd., supporting both industrial and anthropomorphic arm types. It is suitable for various simulation and real-world application scenarios.
+Dexforce W1 is a dual-arm humanoid robot developed by DexForce Technology Co., Ltd.
-
- Anthropomorphic Version
-
-
-
- Industrial Version
+
+ Dexforce W1
## Key Features
-- Supports multiple arm types (industrial, anthropomorphic)
+- Supports dual 7-DOF arms
+- Supports version-owned asset layouts and calibration parameters
- Configurable left/right hand brand and version
- Flexible URDF assembly and simulation configuration
- Compatible with SimulationManager simulation environment
-## Method 1: Fine-grained configuration with `build_dexforce_w1_cfg`
+## Configuration with `DexforceW1Cfg.from_dict`
-This method allows you to specify detailed parameters for each arm and hand. Recommended for advanced users who need full control over robot hardware options.
+`DexforceW1Cfg.from_dict` is the single public construction entry point. A W1
+configuration always describes the complete chassis, torso, head, eyes, wrist
+cameras, and both arms for one robot version.
**Parameters:**
-- `arm_kind`: Arm type, e.g., `DexforceW1ArmKind.ANTHROPOMORPHIC` or `DexforceW1ArmKind.INDUSTRIAL`.
- `hand_types`: Dict specifying hand brand for each arm side (`LEFT`/`RIGHT`).
- `hand_versions`: Dict specifying hand version for each arm side.
-
-```python
-hand_types = {
- DexforceW1ArmSide.LEFT: DexforceW1HandBrand.BRAINCO_HAND,
- DexforceW1ArmSide.RIGHT: DexforceW1HandBrand.BRAINCO_HAND,
-}
-hand_versions = {
- DexforceW1ArmSide.LEFT: DexforceW1Version.V021,
- DexforceW1ArmSide.RIGHT: DexforceW1Version.V021,
-}
-cfg = build_dexforce_w1_cfg(
- arm_kind=DexforceW1ArmKind.ANTHROPOMORPHIC,
- hand_types=hand_types,
- hand_versions=hand_versions,
-)
-robot = sim.add_robot(cfg=cfg)
-print("DexforceW1 robot added to the simulation.")
-```
-
-## Method 2: Quick configuration with `DexforceW1Cfg.from_dict`
-
-This method allows fast setup using a dictionary, suitable for simple scenarios or when default options are sufficient. Recommended for rapid prototyping or when only basic parameters are needed.
-
-**Parameters:**
-
-- `uid`: Unique robot identifier (string).
-- `version`: Robot version, e.g., `v021`.
-- `arm_kind`: Arm type, e.g., `anthropomorphic` or `industrial` (string).
+- `with_default_eef`: Whether to install the registered default end effectors.
+- `hand_attach_xposes`: Optional hand-specific mounting transforms.
```python
from embodichain.lab.sim.robots import DexforceW1Cfg
+
cfg = DexforceW1Cfg.from_dict(
- {"uid": "dexforce_w1", "version": "v021", "arm_kind": "anthropomorphic"}
+ {
+ "uid": "dexforce_w1",
+ "version": "v025",
+ "hand_types": {
+ "left": "BRAINCO_HAND",
+ "right": "BRAINCO_HAND",
+ },
+ "hand_versions": {"left": "v021", "right": "v021"},
+ }
)
robot = sim.add_robot(cfg=cfg)
print("DexforceW1 robot added to the simulation.")
@@ -84,21 +65,107 @@ robot.set_qpos(qpos=[0, np.pi/4, 0.0, np.pi/2, np.pi/4, 0.0, 0.0], joint_ids=rob
This mirrored design simplifies motion planning and ensures that both arms can perform coordinated or symmetric actions efficiently.
-## Configuration Method Selection
-
-Choose `build_dexforce_w1_cfg` for maximum flexibility and hardware customization. Use `DexforceW1Cfg.from_dict` for quick setup and prototyping. Both methods produce a configuration object (`cfg`) that can be passed to `sim.add_robot(cfg=cfg)` to add the robot to the simulation.
-
-**Note:**
-
-- Ensure parameter types match the expected enums or strings.
-- For advanced simulation scenarios, prefer the fine-grained method.
-- For most demos or simple tasks, the quick method is sufficient.
-
## Type Descriptions
| Type | Options / Values | Description |
|-------------------------|-------------------------------------------------------|------------------------------------|
-| `DexforceW1ArmKind` | `ANTHROPOMORPHIC`, `INDUSTRIAL` | Arm type |
| `DexforceW1HandBrand` | `BRAINCO_HAND`, `DH_PGC_GRIPPER`, `DH_PGC_GRIPPER_M` | Hand brand |
-| `DexforceW1Version` | `V021` | Release version |
+| `DexforceW1Version` | `V021`, `V022`, `V025` | Release version |
+| `DexforceW1HandVersion` | `V021` | External hand/gripper asset version |
| `DexforceW1ArmSide` | `LEFT`, `RIGHT` | Left/right hand identifier |
+
+## Unified asset layout and version extension
+
+V022 and V025 use one unified Hugging Face archive per release:
+
+```text
+dexforce_w1//w1.zip
+└── w1/
+ ├── robot.urdf
+ ├── chassis.urdf
+ ├── torso.urdf
+ ├── head.urdf
+ ├── left_arm.urdf
+ ├── right_arm.urdf
+ ├── visual/
+ └── collision/
+```
+
+The runtime downloads each release archive once. Direct FK/IK uses `robot.urdf`
+or the arm URDFs, while configurable robot assembly reads all components from
+the same extracted directory. V022 and V025 assets are resolved through the
+registered Hugging Face dataset archives and the shared asset cache.
+
+### Version-owned end-effector offset
+
+Different arm revisions may place the physical mounting surface at different
+positions relative to the arm `ee` frame. This difference belongs to the W1
+revision, not to a BrainCo hand, DH gripper, PIKA gripper, or any other
+end-effector.
+
+`W1VersionSpec.default_eef_attach_xpos` is the single source of truth for this
+revision offset:
+
+| Version | Left arm | Right arm |
+|---------|----------|-----------|
+| V021 | Identity | Identity |
+| V022 | Identity (provisional; calibration required) | Identity (provisional; calibration required) |
+| V025 | `+0.012 m` along the `ee` frame Z axis | `+0.012 m` along the `ee` frame Z axis |
+
+The final assembly transform and solver TCP are derived as follows:
+
+```python
+final_attach_xpos = version_attach_xpos @ eef_attach_xpos
+final_tcp = version_attach_xpos @ solver_tcp
+```
+
+Therefore, the V025 offset affects both the assembled end-effector position and
+FK/IK results. The offset is also applied when callers provide a custom
+`hand_attach_xposes` value, a custom `left_hand`/`right_hand` component
+transform, or an explicit arm TCP through `DexforceW1Cfg.from_dict`.
+Serialization removes the derived offset and restores it on loading, so a
+configuration round trip does not apply the offset twice.
+
+Do not manually add the 12 mm correction to an end-effector transform or TCP.
+Those values must describe the end-effector relative to the standard mounting
+surface; the W1 version layer adds the robot revision correction.
+
+One `DexforceW1Version` selects the complete W1 robot release. Mixed body/arm
+versions are intentionally unsupported because their assets, kinematics, TCP,
+and camera calibration must remain consistent.
+
+Robot and hand versions are independent. `DexforceW1Version` selects the W1
+body, arm assets, kinematics, and flange calibration. `DexforceW1HandVersion`
+selects an external hand or gripper asset from
+`embodichain/lab/sim/robots/dexforce_w1/hand_specs.py`.
+
+The currently released BrainCo hand and DH gripper registrations all use
+`DexforceW1HandVersion.V021`. Therefore W1 V021, V022, and V025 select hand V021
+by default. This is an explicit default, not a compatibility alias keyed by the
+robot version. A future hand release is added by registering a new hand version
+and hand spec; it does not require adding aliases for every W1 robot version.
+
+To add another W1 revision:
+
+1. Register the new `DexforceW1Version` value and dataset archive.
+2. Add one `W1VersionSpec` entry in
+ `embodichain/lab/sim/robots/dexforce_w1/specs.py`.
+3. Register component URDF paths and the full-robot URDF path.
+4. Set the arm kinematic parameters and
+ `default_eef_attach_xpos` for both sides. Use identity only after confirming
+ that the arm `ee` frame is already at the physical mounting surface.
+5. Verify that the same version offset is present in both the assembled
+ end-effector pose and the final FK/IK TCP.
+6. Validate hand/gripper mounting, wrist cameras, FK/IK, VR teleoperation, and
+ real2sim task regression.
+
+To add another hand release:
+
+1. Add a `DexforceW1HandVersion` value.
+2. Register each supported brand and side in `hand_specs.py`, including its
+ URDF, joint names, root/end links, and mounting transform.
+3. Select it explicitly through `hand_versions`; an unregistered version fails
+ during config construction.
+
+Control parts, assembly, TCP selection, analytical parameters and FK/IK then use
+the version specification without revision-specific branches.
diff --git a/docs/source/tutorial/modular_env.rst b/docs/source/tutorial/modular_env.rst
index af4d14992..f79ee6d36 100644
--- a/docs/source/tutorial/modular_env.rst
+++ b/docs/source/tutorial/modular_env.rst
@@ -105,7 +105,6 @@ The main environment configuration inherits from :class:`envs.EmbodiedEnvCfg` an
Uses the pre-configured :class:`DexforceW1Cfg` with customizations:
- **Version**: Specific robot variant (v021)
-- **Arm Type**: Anthropomorphic configuration
- **Position**: Initial placement in the scene
**Sensor Configuration**
@@ -241,4 +240,4 @@ This tutorial demonstrates the full power of EmbodiChain's modular environment s
**Using an AI coding agent?** These skills can help you build on this tutorial:
- **/add-task-env** — Scaffold a new task environment with the correct file structure, ``@register_env`` decorator, base class methods, ``__init__.py`` update, and test stub.
- - **/add-functor** — Add observation, reward, event, or randomization functors with the correct signature and module placement.
\ No newline at end of file
+ - **/add-functor** — Add observation, reward, event, or randomization functors with the correct signature and module placement.
diff --git a/embodichain/data/assets/w1_assets.py b/embodichain/data/assets/w1_assets.py
index d6ec94ce8..2fff2d0d3 100644
--- a/embodichain/data/assets/w1_assets.py
+++ b/embodichain/data/assets/w1_assets.py
@@ -28,8 +28,10 @@
#
# Main Asset:
# - DexforceW1V021:
-# Represents the complete humanoid robot asset,
-# including both industrial arms and anthropomorphic arms.
+# Represents the complete humanoid robot asset.
+# - DexforceW1V022 / DexforceW1V025:
+# Unified release archives containing the full robot and all component
+# URDFs used by runtime assembly.
#
# Component Assets:
# - DexforceW1ChassisV021: Chassis component
@@ -38,10 +40,8 @@
# - DexforceW1HeadV021: Head component
#
# Arm Assets:
-# - DexforceW1LeftArm1V021 / DexforceW1RightArm1V021:
-# Anthropomorphic (human-like) arms, left and right.
-# - DexforceW1LeftArm2V021 / DexforceW1RightArm2V021:
-# Industrial arms, left and right.
+# - DexforceW1LeftArmV021 / DexforceW1RightArmV021:
+# Left and right arms.
#
# All classes inherit from EmbodiChainDataset and are responsible for
# downloading and managing the data resources for their respective components.
@@ -60,7 +60,6 @@ class DexforceW1V021(EmbodiChainDataset):
Example usage:
>>> from embodichain.data import get_data_path
>>> print(get_data_path("DexforceW1V021/DexforceW1_v02_1.urdf"))
- >>> print(get_data_path("DexforceW1V021/DexforceW1_v02_2.urdf"))
"""
def __init__(self, data_root: str = None):
@@ -74,15 +73,19 @@ def __init__(self, data_root: str = None):
super().__init__(prefix, data_descriptor, path)
-class DexforceW1V021_INDUSTRIAL_DH_PGC_GRIPPER_M(EmbodiChainDataset):
- """Dataset class for the industrial Dexforce W1 V021 with DH_PGC_gripper.
+class DexforceW1V022(EmbodiChainDataset):
+ """Dataset class for the unified Dexforce W1 V022 release archive.
- Directory structure:
- DexforceW1V021_INDUSTRIAL_DH_PGC_GRIPPER_M/DexforceW1V021.urdf
+ Expected Hugging Face path and archive layout::
+
+ dexforce_w1/v022/w1.zip
+ w1/robot.urdf
+ w1/chassis.urdf
+ w1/torso.urdf
+ w1/head.urdf
+ w1/left_arm.urdf
+ w1/right_arm.urdf
- Example usage:
- >>> from embodichain.data import get_data_path
- >>> print(get_data_path("DexforceW1V021_INDUSTRIAL_DH_PGC_GRIPPER_M/DexforceW1V021.urdf"))
"""
def __init__(self, data_root: str = None):
@@ -90,9 +93,11 @@ def __init__(self, data_root: str = None):
os.path.join(
EMBODICHAIN_DOWNLOAD_PREFIX,
w1_assets,
- "DexforceW1V021_INDUSTRIAL_DH_PGC_GRIPPER_M.zip",
+ "v022",
+ "w1.zip",
),
- "06ec5dfa76dc69160d7ff9bc537a6a7b",
+ # Recalculate this if the archive is cleaned or rebuilt before upload.
+ "867f52d684b8cf5161f9c8e53ff493d2",
)
prefix = type(self).__name__
path = EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root
@@ -100,15 +105,21 @@ def __init__(self, data_root: str = None):
super().__init__(prefix, data_descriptor, path)
-class DexforceW1V021_ANTHROPOMORPHIC_BRAINCO_HAND_REVO1(EmbodiChainDataset):
- """Dataset class for the anthropomorphic Dexforce W1 V021 with BrainCo_hand_revo_1.
+class DexforceW1V025(EmbodiChainDataset):
+ """Dataset class for the unified Dexforce W1 V025 release archive.
- Directory structure:
- DexforceW1V021_ANTHROPOMORPHIC_BRAINCO_HAND_REVO1/DexforceW1V021.urdf
+ Expected Hugging Face path and archive layout::
- Example usage:
- >>> from embodichain.data import get_data_path
- >>> print(get_data_path("DexforceW1V021_ANTHROPOMORPHIC_BRAINCO_HAND_REVO1/DexforceW1V021.urdf"))
+ dexforce_w1/v025/w1.zip
+ w1/robot.urdf
+ w1/chassis.urdf
+ w1/torso.urdf
+ w1/head.urdf
+ w1/left_arm.urdf
+ w1/right_arm.urdf
+
+ The same archive supports both direct full-robot loading and component
+ assembly, so runtime code does not download duplicate component archives.
"""
def __init__(self, data_root: str = None):
@@ -116,9 +127,10 @@ def __init__(self, data_root: str = None):
os.path.join(
EMBODICHAIN_DOWNLOAD_PREFIX,
w1_assets,
- "DexforceW1V021_ANTHROPOMORPHIC_BRAINCO_HAND_REVO1.zip",
+ "v025",
+ "w1.zip",
),
- "ef19d247799e79233863b558c47b32cd",
+ "a983814a05b20ba12fce02883cfd1d7e",
)
prefix = type(self).__name__
path = EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root
@@ -174,7 +186,7 @@ def __init__(self, data_root: str = None):
super().__init__(prefix, data_descriptor, path)
-class DexforceW1LeftArm1V021(EmbodiChainDataset):
+class DexforceW1LeftArmV021(EmbodiChainDataset):
def __init__(self, data_root: str = None):
data_descriptor = o3d.data.DataDescriptor(
os.path.join(
@@ -188,7 +200,7 @@ def __init__(self, data_root: str = None):
super().__init__(prefix, data_descriptor, path)
-class DexforceW1RightArm1V021(EmbodiChainDataset):
+class DexforceW1RightArmV021(EmbodiChainDataset):
def __init__(self, data_root: str = None):
data_descriptor = o3d.data.DataDescriptor(
os.path.join(
@@ -200,31 +212,3 @@ def __init__(self, data_root: str = None):
path = EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root
super().__init__(prefix, data_descriptor, path)
-
-
-class DexforceW1LeftArm2V021(EmbodiChainDataset):
- def __init__(self, data_root: str = None):
- data_descriptor = o3d.data.DataDescriptor(
- os.path.join(
- EMBODICHAIN_DOWNLOAD_PREFIX, w1_assets, "W1_LeftArm_2_v021.zip"
- ),
- "b99bd0587cc9a36fed3cdaa4f9fd62e7",
- )
- prefix = type(self).__name__
- path = EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root
-
- super().__init__(prefix, data_descriptor, path)
-
-
-class DexforceW1RightArm2V021(EmbodiChainDataset):
- def __init__(self, data_root: str = None):
- data_descriptor = o3d.data.DataDescriptor(
- os.path.join(
- EMBODICHAIN_DOWNLOAD_PREFIX, w1_assets, "W1_RightArm_2_v021.zip"
- ),
- "d9f25b2d5244ca5a859040327273a99e",
- )
- prefix = type(self).__name__
- path = EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root
-
- super().__init__(prefix, data_descriptor, path)
diff --git a/embodichain/lab/scripts/analyze_workspace.py b/embodichain/lab/scripts/analyze_workspace.py
index ee1c22301..7f1649c14 100644
--- a/embodichain/lab/scripts/analyze_workspace.py
+++ b/embodichain/lab/scripts/analyze_workspace.py
@@ -29,7 +29,7 @@
embodichain analyze-workspace \\
--robot dexforce_w1 \\
- --robot-params '{"version":"v021","arm_kind":"industrial"}' \\
+ --robot-params '{"version":"v025","with_default_eef":false}' \\
--control-part left_arm --mode joint_space
# Generic URDF/USD asset -- requires --ee-link (and --joints for grippers)
@@ -247,7 +247,7 @@ def build_preset_robot_cfg(
Uses the robot's built-in ``control_parts`` and ``solver_cfg``, so the
end-effector link and joint names come from the preset. Pass
``--robot-params`` (JSON) for variant overrides such as
- ``{"robot_type": "ur5"}`` or ``{"version": "v021", "arm_kind": "industrial"}``.
+ ``{"robot_type": "ur5"}`` or ``{"version": "v025", "with_default_eef": false}``.
Args:
args: Parsed CLI arguments with ``args.robot`` and optionally
@@ -775,7 +775,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
type=str,
default=None,
help="JSON dict of variant overrides for --robot, e.g. "
- '{"robot_type":"ur5"} or {"version":"v021","arm_kind":"industrial"}.',
+ '{"robot_type":"ur5"} or {"version":"v025","with_default_eef":false}.',
)
robot.add_argument(
"--control-part",
diff --git a/embodichain/lab/sim/cfg.py b/embodichain/lab/sim/cfg.py
index 5f35cabe6..815008c25 100644
--- a/embodichain/lab/sim/cfg.py
+++ b/embodichain/lab/sim/cfg.py
@@ -1263,8 +1263,7 @@ class URDFCfg:
"""Case normalization policy applied to joint/link names during URDF assembly.
Supported values per key are ``"upper"``, ``"lower"`` or ``"original"``
- (legacy alias ``"none"``). The default upper-cases joints and lower-cases
- links. Set ``{"joint": "original"}`` to preserve the source URDF casing.
+ (legacy alias ``"none"``). The default preserves source URDF casing.
"""
def __init__(
@@ -1843,6 +1842,8 @@ def to_dict(self):
def serialize(obj, _visited=None):
if _visited is None:
_visited = set()
+ if isinstance(obj, enum.Enum):
+ return obj.value
if isinstance(obj, (dict, object)) and not isinstance(
obj, (str, int, float, bool, type(None))
):
@@ -1851,12 +1852,15 @@ def serialize(obj, _visited=None):
return None
_visited.add(obj_id)
- if isinstance(obj, enum.Enum):
- return obj.value
if isinstance(obj, np.ndarray):
return obj.tolist()
if isinstance(obj, dict):
- return {str(k): serialize(v, _visited) for k, v in obj.items()}
+ return {
+ (k.value if isinstance(k, enum.Enum) else str(k)): serialize(
+ v, _visited
+ )
+ for k, v in obj.items()
+ }
if isinstance(obj, (list, tuple)):
return [serialize(v, _visited) for v in obj]
if hasattr(obj, "to_dict") and obj is not self:
diff --git a/embodichain/lab/sim/robots/dexforce_w1/cfg.py b/embodichain/lab/sim/robots/dexforce_w1/cfg.py
index ae0b53c5d..85ace00f9 100644
--- a/embodichain/lab/sim/robots/dexforce_w1/cfg.py
+++ b/embodichain/lab/sim/robots/dexforce_w1/cfg.py
@@ -16,30 +16,34 @@
from __future__ import annotations
-import numpy as np
import typing
+
+import numpy as np
import torch
from typing import TYPE_CHECKING, Dict
from embodichain.lab.sim.robots.dexforce_w1.types import (
- DexforceW1HandBrand,
DexforceW1ArmSide,
- DexforceW1ArmKind,
+ DexforceW1HandBrand,
+ DexforceW1HandVersion,
DexforceW1Version,
)
from embodichain.lab.sim.robots.dexforce_w1.utils import (
- build_dexforce_w1_cfg,
+ build_dexforce_w1_assembly_urdf_cfg,
+ build_dexforce_w1_control_parts,
+)
+from embodichain.lab.sim.robots.dexforce_w1.hand_specs import (
+ get_default_w1_hand_version,
)
-from embodichain.lab.sim.solvers import SolverCfg
+from embodichain.lab.sim.robots.dexforce_w1.specs import get_w1_version_spec
from embodichain.lab.sim.cfg import (
RobotCfg,
JointDrivePropertiesCfg,
RigidBodyAttributesCfg,
)
from embodichain.lab.sim.utility.cfg_utils import merge_robot_cfg
-from embodichain.data import get_data_path
-from embodichain.utils import configclass, logger
+from embodichain.utils import configclass
if TYPE_CHECKING:
import pytorch_kinematics as pk
@@ -50,8 +54,10 @@ class DexforceW1Cfg(RobotCfg):
"""DexforceW1 specific configuration, inherits from RobotCfg and allows custom parameters."""
version: DexforceW1Version = DexforceW1Version.V021
- arm_kind: DexforceW1ArmKind = DexforceW1ArmKind.ANTHROPOMORPHIC
with_default_eef: bool = True
+ hand_types: dict | None = None
+ hand_versions: dict[DexforceW1ArmSide, DexforceW1HandVersion] | None = None
+ hand_attach_xposes: dict | None = None
@classmethod
def from_dict(
@@ -66,162 +72,181 @@ def from_dict(
A DexforceW1Cfg instance. Defaults are built via
:meth:`_build_defaults`, then ``init_dict`` overrides are merged.
"""
+ unknown_fields = set(init_dict) - set(cls.__dataclass_fields__)
+ if unknown_fields:
+ unknown = ", ".join(sorted(unknown_fields))
+ raise ValueError(f"Unknown DexforceW1 configuration fields: {unknown}")
+
cfg = cls()
cfg._build_defaults(init_dict)
- return merge_robot_cfg(cfg, init_dict)
+ cfg = merge_robot_cfg(cfg, init_dict)
+ cfg._compose_user_eef_overrides(init_dict)
+ return cfg
+
+ def _compose_user_eef_overrides(self, init_dict: dict) -> None:
+ """Apply the version offset to user-supplied EEF transforms and TCPs."""
+ side_by_component = {
+ "left_hand": DexforceW1ArmSide.LEFT,
+ "right_hand": DexforceW1ArmSide.RIGHT,
+ }
+ urdf_cfg = init_dict.get("urdf_cfg")
+ components = (
+ urdf_cfg.get("components", []) if isinstance(urdf_cfg, dict) else []
+ )
+ if isinstance(components, dict):
+ component_names = set(components)
+ else:
+ component_names = {
+ component.get("component_type")
+ for component in components
+ if isinstance(component, dict)
+ }
+
+ for component_name, arm_side in side_by_component.items():
+ if component_name not in component_names:
+ continue
+ component = self.urdf_cfg.components.get(component_name)
+ if component is None:
+ continue
+ spec = get_w1_version_spec(self.version)
+ component["transform"] = spec.compose_eef_attach_xpos(
+ arm_side, component["transform"]
+ )
+
+ solver_cfg = init_dict.get("solver_cfg")
+ if not isinstance(solver_cfg, dict):
+ return
+ for arm_side in DexforceW1ArmSide:
+ part_name = f"{arm_side.value}_arm"
+ part_override = solver_cfg.get(part_name)
+ if not isinstance(part_override, dict) or "tcp" not in part_override:
+ continue
+ solver = self.solver_cfg.get(part_name)
+ if solver is None:
+ continue
+ spec = get_w1_version_spec(self.version)
+ solver.tcp = spec.compose_eef_attach_xpos(arm_side, solver.tcp)
+
+ def to_dict(self):
+ """Serialize EEF-specific transforms without the derived version offset."""
+ data = super().to_dict()
+ side_by_component = {
+ "left_hand": DexforceW1ArmSide.LEFT,
+ "right_hand": DexforceW1ArmSide.RIGHT,
+ }
+ components = data.get("urdf_cfg", {}).get("components", {})
+ for component_name, arm_side in side_by_component.items():
+ component = components.get(component_name)
+ if not isinstance(component, dict) or component.get("transform") is None:
+ continue
+ spec = get_w1_version_spec(self.version)
+ offset_inv = np.linalg.inv(spec.eef_attach_xpos(arm_side))
+ component["transform"] = (
+ offset_inv @ np.asarray(component["transform"], dtype=float)
+ ).tolist()
+
+ solver_cfg = data.get("solver_cfg", {})
+ for arm_side in DexforceW1ArmSide:
+ part_name = f"{arm_side.value}_arm"
+ solver = solver_cfg.get(part_name)
+ if not isinstance(solver, dict) or solver.get("tcp") is None:
+ continue
+ spec = get_w1_version_spec(self.version)
+ offset_inv = np.linalg.inv(spec.eef_attach_xpos(arm_side))
+ solver["tcp"] = (
+ offset_inv @ np.asarray(solver["tcp"], dtype=float)
+ ).tolist()
+ return data
def _build_defaults(self, init_dict: dict | None = None) -> None:
"""Build default urdf/control/solver/physics from variant fields.
- Reads ``version``/``arm_kind``/``with_default_eef`` from ``init_dict``,
+ Reads ``version``/``with_default_eef`` from ``init_dict``,
sets them on ``self``, then populates ``urdf_cfg``, ``control_parts``,
``solver_cfg``, ``drive_pros`` and ``attrs``.
"""
init_dict = init_dict or {}
- version = init_dict.get("version", DexforceW1Version.V021)
- arm_kind = init_dict.get("arm_kind", DexforceW1ArmKind.ANTHROPOMORPHIC)
- with_default_eef = init_dict.get("with_default_eef", True)
-
- self.version = (
- DexforceW1Version(version) if isinstance(version, str) else version
- )
- self.arm_kind = (
- DexforceW1ArmKind(arm_kind) if isinstance(arm_kind, str) else arm_kind
+ self.version = DexforceW1Version.parse(
+ init_dict.get("version", DexforceW1Version.V021)
)
- self.with_default_eef = with_default_eef
+ self.with_default_eef = bool(init_dict.get("with_default_eef", True))
- # urdf_cfg + control_parts (build_dexforce_w1_cfg no longer sets solver_cfg)
- if self.arm_kind == DexforceW1ArmKind.INDUSTRIAL:
- hand_types = {
- DexforceW1ArmSide.LEFT: DexforceW1HandBrand.DH_PGC_GRIPPER_M,
- DexforceW1ArmSide.RIGHT: DexforceW1HandBrand.DH_PGC_GRIPPER_M,
- }
- else:
- hand_types = {
- DexforceW1ArmSide.LEFT: DexforceW1HandBrand.BRAINCO_HAND,
- DexforceW1ArmSide.RIGHT: DexforceW1HandBrand.BRAINCO_HAND,
- }
- hand_versions = {
- DexforceW1ArmSide.LEFT: self.version,
- DexforceW1ArmSide.RIGHT: self.version,
+ self.hand_types = {
+ DexforceW1ArmSide.parse(side): DexforceW1HandBrand.parse(brand)
+ for side, brand in init_dict.get("hand_types", {}).items()
+ }
+ configured_hand_versions = {
+ DexforceW1ArmSide.parse(side): DexforceW1HandVersion.parse(version)
+ for side, version in init_dict.get("hand_versions", {}).items()
+ }
+ self.hand_versions = {
+ side: configured_hand_versions.get(
+ side,
+ get_default_w1_hand_version(
+ self.hand_types.get(side, DexforceW1HandBrand.BRAINCO_HAND)
+ ),
+ )
+ for side in DexforceW1ArmSide
+ }
+ self.hand_attach_xposes = {
+ DexforceW1ArmSide.parse(side): np.asarray(transform, dtype=float)
+ for side, transform in init_dict.get("hand_attach_xposes", {}).items()
}
- base_cfg = build_dexforce_w1_cfg(
- arm_kind=self.arm_kind,
- hand_types=hand_types,
- hand_versions=hand_versions,
- include_hand=with_default_eef,
+
+ self.urdf_cfg = build_dexforce_w1_assembly_urdf_cfg(
+ version=self.version,
+ hand_types=self.hand_types,
+ hand_versions=self.hand_versions,
+ hand_attach_xposes=self.hand_attach_xposes,
+ include_hand=self.with_default_eef,
+ )
+ self.control_parts = build_dexforce_w1_control_parts(
+ version=self.version,
+ hand_types=self.hand_types,
+ hand_versions=self.hand_versions,
+ include_hand=self.with_default_eef,
)
- self.urdf_cfg = base_cfg.urdf_cfg
- self.control_parts = base_cfg.control_parts
# physics
physics = self._build_default_physics_cfgs(
- arm_kind=self.arm_kind, with_default_eef=with_default_eef
+ with_default_eef=self.with_default_eef
)
for key, value in physics.items():
setattr(self, key, value)
# solver (set exactly once -- was previously double-set)
- self.solver_cfg = self._build_default_solver_cfg(arm_kind=self.arm_kind)
-
- def _build_default_solver_cfg(self, arm_kind: DexforceW1ArmKind):
- """Build the default SRS solver config for the given arm kind.
+ self.solver_cfg = self._build_default_solver_cfg()
- Note: the W1ArmKineParams below intentionally use DexforceW1Version.V021
- (matching the original behavior -- version does not flow into the solver).
- """
+ def _build_default_solver_cfg(self):
+ """Build the version-matched default SRS solver configuration."""
from embodichain.lab.sim.solvers import SRSSolverCfg
from embodichain.lab.sim.robots.dexforce_w1.params import (
W1ArmKineParams,
)
- if arm_kind == DexforceW1ArmKind.INDUSTRIAL:
- w1_left_arm_params = W1ArmKineParams(
- arm_side=DexforceW1ArmSide.LEFT,
- arm_kind=DexforceW1ArmKind.INDUSTRIAL,
- version=DexforceW1Version.V021,
- )
- w1_right_arm_params = W1ArmKineParams(
- arm_side=DexforceW1ArmSide.RIGHT,
- arm_kind=DexforceW1ArmKind.INDUSTRIAL,
- version=DexforceW1Version.V021,
- )
- left_arm_tcp = np.array(
- [
- [1.0, 0.0, 0.0, 0.0],
- [0.0, 1.0, 0.0, 0.0],
- [0.0, 0.0, 1.0, 0.15],
- [0.0, 0.0, 0.0, 1.0],
- ]
- )
- right_arm_tcp = np.array(
- [
- [1.0, 0.0, 0.0, 0.0],
- [0.0, 1.0, 0.0, 0.0],
- [0.0, 0.0, 1.0, 0.15],
- [0.0, 0.0, 0.0, 1.0],
- ]
- )
- else:
- w1_left_arm_params = W1ArmKineParams(
- arm_side=DexforceW1ArmSide.LEFT,
- arm_kind=DexforceW1ArmKind.ANTHROPOMORPHIC,
- version=DexforceW1Version.V021,
- )
- w1_right_arm_params = W1ArmKineParams(
- arm_side=DexforceW1ArmSide.RIGHT,
- arm_kind=DexforceW1ArmKind.ANTHROPOMORPHIC,
- version=DexforceW1Version.V021,
- )
- left_arm_tcp = np.array(
- [
- [-1.0, 0.0, 0.0, 0.012],
- [0.0, 0.0, 1.0, 0.0675],
- [0.0, 1.0, 0.0, 0.127],
- [0.0, 0.0, 0.0, 1.0],
- ]
- )
- right_arm_tcp = np.array(
- [
- [1.0, 0.0, 0.0, 0.012],
- [0.0, 0.0, -1.0, -0.0675],
- [0.0, 1.0, 0.0, 0.127],
- [0.0, 0.0, 0.0, 1.0],
- ]
+ solver_cfg = {}
+ for arm_side in DexforceW1ArmSide:
+ params = W1ArmKineParams(arm_side=arm_side, version=self.version)
+ part_name = f"{arm_side.value}_arm"
+ solver_cfg[part_name] = SRSSolverCfg(
+ end_link_name=f"{arm_side.value}_ee",
+ root_link_name=f"{arm_side.value}_arm_base",
+ dh_params=params.dh_params,
+ user_qpos_limits=params.qpos_limits,
+ T_e_oe=params.T_e_oe,
+ T_b_ob=params.T_b_ob,
+ link_lengths=params.link_lengths,
+ rotation_directions=params.rotation_directions,
+ tcp=get_w1_version_spec(self.version).tcp(arm_side),
)
-
- return {
- "right_arm": SRSSolverCfg(
- end_link_name="right_ee",
- root_link_name="right_arm_base",
- dh_params=w1_right_arm_params.dh_params,
- user_qpos_limits=w1_right_arm_params.qpos_limits,
- T_e_oe=w1_right_arm_params.T_e_oe,
- T_b_ob=w1_right_arm_params.T_b_ob,
- link_lengths=w1_right_arm_params.link_lengths,
- rotation_directions=w1_right_arm_params.rotation_directions,
- tcp=right_arm_tcp,
- ),
- "left_arm": SRSSolverCfg(
- end_link_name="left_ee",
- root_link_name="left_arm_base",
- dh_params=w1_left_arm_params.dh_params,
- user_qpos_limits=w1_left_arm_params.qpos_limits,
- T_e_oe=w1_left_arm_params.T_e_oe,
- T_b_ob=w1_left_arm_params.T_b_ob,
- link_lengths=w1_left_arm_params.link_lengths,
- rotation_directions=w1_left_arm_params.rotation_directions,
- tcp=left_arm_tcp,
- ),
- }
+ return solver_cfg
def _build_default_physics_cfgs(
- self, arm_kind: DexforceW1ArmKind, with_default_eef: bool = True
+ self, with_default_eef: bool = True
) -> typing.Dict[str, typing.Any]:
"""Build default physics configurations for DexforceW1.
Args:
- arm_kind: The arm kind enum.
with_default_eef: Whether to include default end-effector configurations
Returns:
@@ -236,7 +261,6 @@ def _build_default_physics_cfgs(
DEFAULT_EEF_HAND_JOINT_NAMES = (
"(LEFT|RIGHT)_HAND_(THUMB[12]|INDEX|MIDDLE|RING|PINKY)"
)
- DEFAULT_EEF_GRIPPER_JOINT_NAMES = "(LEFT|RIGHT)_FINGER[1-2]"
ARM_JOINTS = "(RIGHT|LEFT)_J[0-9]"
BODY_JOINTS = "(ANKLE|KNEE|BUTTOCK|WAIST)"
HEAD_JOINTS = "(NECK1|NECK2)"
@@ -253,11 +277,7 @@ def _build_default_physics_cfgs(
drive_pros = JointDrivePropertiesCfg(**joint_params)
if with_default_eef:
- eef_joint_names = (
- DEFAULT_EEF_HAND_JOINT_NAMES
- if arm_kind == DexforceW1ArmKind.ANTHROPOMORPHIC
- else DEFAULT_EEF_GRIPPER_JOINT_NAMES
- )
+ eef_joint_names = DEFAULT_EEF_HAND_JOINT_NAMES
drive_pros.stiffness.update(
{eef_joint_names: DEFAULT_EEF_JOINT_DRIVE_PARAMS["stiffness"]}
)
@@ -281,16 +301,16 @@ def _build_default_physics_cfgs(
# to_dict, to_string, save_to_file inherited from RobotCfg
- def _pk_urdf_path(self) -> str:
- """URDF used for FK/IK serial chains, by arm kind.
+ def _pk_urdf_path(self, arm_side: DexforceW1ArmSide) -> str:
+ """Return the selected arm component URDF for FK/IK.
.. attention::
The root_link->end_link kinematics here must match the arms in the
simulation (assembled) URDF. A DOF drift guard in the tests checks this.
"""
- if self.arm_kind == DexforceW1ArmKind.INDUSTRIAL:
- return get_data_path("DexforceW1V021/DexforceW1_v02_2.urdf")
- return get_data_path("DexforceW1V021/DexforceW1_v02_1.urdf")
+ from embodichain.lab.sim.robots.dexforce_w1.utils import arm_manager
+
+ return arm_manager.get_urdf(side=arm_side, version=self.version)
def build_pk_serial_chain(
self, device: torch.device = torch.device("cpu"), **kwargs
@@ -299,24 +319,14 @@ def build_pk_serial_chain(
create_pk_serial_chain,
)
- urdf_path = self._pk_urdf_path()
-
- left_arm_chain = create_pk_serial_chain(
- urdf_path=urdf_path,
- device=device,
- end_link_name="left_ee",
- root_link_name="left_arm_base",
- )
- right_arm_chain = create_pk_serial_chain(
- urdf_path=urdf_path,
- device=device,
- end_link_name="right_ee",
- root_link_name="right_arm_base",
- )
-
return {
- "left_arm": left_arm_chain,
- "right_arm": right_arm_chain,
+ f"{arm_side.value}_arm": create_pk_serial_chain(
+ urdf_path=self._pk_urdf_path(arm_side),
+ device=device,
+ end_link_name=f"{arm_side.value}_ee",
+ root_link_name=f"{arm_side.value}_arm_base",
+ )
+ for arm_side in DexforceW1ArmSide
}
@@ -326,16 +336,11 @@ def build_pk_serial_chain(
np.set_printoptions(precision=5, suppress=True)
from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
- from embodichain.lab.sim.robots.dexforce_w1.types import (
- DexforceW1ArmKind,
- )
config = SimulationManagerCfg(headless=True, sim_device="cpu", num_envs=4)
sim = SimulationManager(config)
- cfg = DexforceW1Cfg.from_dict(
- {"uid": "dexforce_w1", "version": "v021", "arm_kind": "anthropomorphic"}
- )
+ cfg = DexforceW1Cfg.from_dict({"uid": "dexforce_w1", "version": "v021"})
robot = sim.add_robot(cfg=cfg)
sim.update(step=1)
diff --git a/embodichain/lab/sim/robots/dexforce_w1/hand_specs.py b/embodichain/lab/sim/robots/dexforce_w1/hand_specs.py
new file mode 100644
index 000000000..7df0d66a2
--- /dev/null
+++ b/embodichain/lab/sim/robots/dexforce_w1/hand_specs.py
@@ -0,0 +1,199 @@
+# ----------------------------------------------------------------------------
+# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ----------------------------------------------------------------------------
+
+"""Asset and mounting specifications for W1 external end effectors."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from dataclasses import dataclass
+from types import MappingProxyType
+
+import numpy as np
+from scipy.spatial.transform import Rotation as R
+
+from .types import (
+ DexforceW1ArmSide,
+ DexforceW1HandBrand,
+ DexforceW1HandVersion,
+)
+
+__all__ = [
+ "W1HandSideSpec",
+ "W1HandSpec",
+ "get_default_w1_hand_version",
+ "get_w1_hand_spec",
+]
+
+
+def _attach_xpos(
+ rotation_xyz_degrees: tuple[float, float, float],
+ translation: tuple[float, float, float] = (0.0, 0.0, 0.0),
+) -> tuple[tuple[float, ...], ...]:
+ transform = np.eye(4)
+ transform[:3, :3] = R.from_euler(
+ "xyz", rotation_xyz_degrees, degrees=True
+ ).as_matrix()
+ transform[:3, 3] = translation
+ return tuple(tuple(float(value) for value in row) for row in transform)
+
+
+@dataclass(frozen=True)
+class W1HandSideSpec:
+ """Side-specific asset metadata for one hand release."""
+
+ urdf_path: str
+ joint_names: tuple[str, ...]
+ end_link_name: str
+ root_link_name: str
+ attach_xpos: tuple[tuple[float, ...], ...]
+
+
+@dataclass(frozen=True)
+class W1HandSpec:
+ """A hand release independent of the W1 body/arm version."""
+
+ brand: DexforceW1HandBrand
+ version: DexforceW1HandVersion
+ sides: Mapping[DexforceW1ArmSide, W1HandSideSpec]
+
+ def __post_init__(self) -> None:
+ object.__setattr__(self, "sides", MappingProxyType(dict(self.sides)))
+
+ def for_side(self, side: DexforceW1ArmSide | str) -> W1HandSideSpec:
+ side = DexforceW1ArmSide.parse(side)
+ try:
+ return self.sides[side]
+ except KeyError as exc:
+ raise ValueError(
+ f"Hand {self.brand.value} {self.version.value} has no "
+ f"{side.value} asset registered"
+ ) from exc
+
+
+def _brainco_side(side: DexforceW1ArmSide) -> W1HandSideSpec:
+ prefix = "LEFT" if side == DexforceW1ArmSide.LEFT else "RIGHT"
+ side_name = "Left" if side == DexforceW1ArmSide.LEFT else "Right"
+ rotation = (90.0, 0.0, 180.0 if side == DexforceW1ArmSide.LEFT else 0.0)
+ return W1HandSideSpec(
+ urdf_path=(
+ f"BrainCoHandRevo1/BrainCo{side_name}Hand/" f"BrainCo{side_name}Hand.urdf"
+ ),
+ joint_names=(
+ f"{prefix}_HAND_THUMB1",
+ f"{prefix}_HAND_THUMB2",
+ f"{prefix}_HAND_INDEX",
+ f"{prefix}_HAND_MIDDLE",
+ f"{prefix}_HAND_RING",
+ f"{prefix}_HAND_PINKY",
+ ),
+ end_link_name=f"{side.value}_hand_base",
+ root_link_name=f"{side.value}_thumb_dist",
+ attach_xpos=_attach_xpos(rotation),
+ )
+
+
+def _dh_gripper_side(
+ side: DexforceW1ArmSide,
+ modified: bool,
+) -> W1HandSideSpec:
+ prefix = "LEFT" if side == DexforceW1ArmSide.LEFT else "RIGHT"
+ suffix = "_M" if modified else ""
+ joint_suffixes = (
+ ("FINGER1", "FINGER2")
+ if modified
+ else (
+ "FINGER1_JOINT",
+ "FINGER2_JOINT",
+ )
+ )
+ return W1HandSideSpec(
+ urdf_path=f"DH_PGC_140_50{suffix}/DH_PGC_140_50{suffix}.urdf",
+ joint_names=tuple(f"{prefix}_{name}" for name in joint_suffixes),
+ end_link_name=f"{side.value}_base_link_1",
+ root_link_name=(
+ f"{side.value}_finger2" if modified else f"{side.value}_finger2_link"
+ ),
+ attach_xpos=_attach_xpos(
+ (0.0, 0.0, 90.0),
+ (0.0, 0.0, 0.0 if modified else 0.015),
+ ),
+ )
+
+
+_W1_HAND_SPECS = {
+ (
+ DexforceW1HandBrand.BRAINCO_HAND,
+ DexforceW1HandVersion.V021,
+ ): W1HandSpec(
+ brand=DexforceW1HandBrand.BRAINCO_HAND,
+ version=DexforceW1HandVersion.V021,
+ sides={side: _brainco_side(side) for side in DexforceW1ArmSide},
+ ),
+ (
+ DexforceW1HandBrand.DH_PGC_GRIPPER,
+ DexforceW1HandVersion.V021,
+ ): W1HandSpec(
+ brand=DexforceW1HandBrand.DH_PGC_GRIPPER,
+ version=DexforceW1HandVersion.V021,
+ sides={side: _dh_gripper_side(side, False) for side in DexforceW1ArmSide},
+ ),
+ (
+ DexforceW1HandBrand.DH_PGC_GRIPPER_M,
+ DexforceW1HandVersion.V021,
+ ): W1HandSpec(
+ brand=DexforceW1HandBrand.DH_PGC_GRIPPER_M,
+ version=DexforceW1HandVersion.V021,
+ sides={side: _dh_gripper_side(side, True) for side in DexforceW1ArmSide},
+ ),
+}
+
+_DEFAULT_W1_HAND_VERSIONS = MappingProxyType(
+ {
+ DexforceW1HandBrand.BRAINCO_HAND: DexforceW1HandVersion.V021,
+ DexforceW1HandBrand.DH_PGC_GRIPPER: DexforceW1HandVersion.V021,
+ DexforceW1HandBrand.DH_PGC_GRIPPER_M: DexforceW1HandVersion.V021,
+ }
+)
+
+
+def get_default_w1_hand_version(
+ brand: DexforceW1HandBrand | str,
+) -> DexforceW1HandVersion:
+ """Return the explicitly selected default release for a hand brand."""
+ brand = DexforceW1HandBrand.parse(brand)
+ try:
+ return _DEFAULT_W1_HAND_VERSIONS[brand]
+ except KeyError as exc:
+ raise ValueError(
+ f"No default hand version registered for {brand.value}"
+ ) from exc
+
+
+def get_w1_hand_spec(
+ brand: DexforceW1HandBrand | str,
+ version: DexforceW1HandVersion | str,
+) -> W1HandSpec:
+ """Return an explicitly registered hand release."""
+ brand = DexforceW1HandBrand.parse(brand)
+ version = DexforceW1HandVersion.parse(version)
+ try:
+ return _W1_HAND_SPECS[(brand, version)]
+ except KeyError as exc:
+ raise ValueError(
+ f"No hand asset registered for brand={brand.value}, "
+ f"version={version.value}"
+ ) from exc
diff --git a/embodichain/lab/sim/robots/dexforce_w1/params.py b/embodichain/lab/sim/robots/dexforce_w1/params.py
index 472c8a775..da759c2d2 100644
--- a/embodichain/lab/sim/robots/dexforce_w1/params.py
+++ b/embodichain/lab/sim/robots/dexforce_w1/params.py
@@ -19,24 +19,21 @@
from dataclasses import dataclass, field
from embodichain.lab.sim.robots.dexforce_w1.types import (
- DexforceW1HandBrand,
DexforceW1ArmSide,
- DexforceW1ArmKind,
DexforceW1Version,
)
+from embodichain.lab.sim.robots.dexforce_w1.specs import get_w1_version_spec
@dataclass
class W1ArmKineParams:
- """Kinematics parameters for W1 arm variants.
+ """Kinematics parameters for a W1 arm.
- - arm_kind and W1Version enum types expected to be defined elsewhere.
- dh_params stored as numpy array of shape (7,4).
- qpos_limits stored as numpy array of shape (7,2) in radians.
"""
arm_side: "DexforceW1ArmSide"
- arm_kind: "DexforceW1ArmKind"
version: "DexforceW1Version" = field(default_factory=lambda: DexforceW1Version.V021)
# (initialized in __post_init__)
@@ -54,18 +51,16 @@ class W1ArmKineParams:
qpos_limits: np.ndarray = field(init=False)
def __post_init__(self):
- if self.version == DexforceW1Version.V021:
- self.d_list = np.array([0.0, 0.0, 0.260, 0.0, 0.166, 0.098, 0.0])
- self.link_lengths = np.array(
- [
- self.d_list[0] + self.d_list[1],
- self.d_list[2] + self.d_list[3],
- self.d_list[4] + self.d_list[5],
- self.d_list[6],
- ]
- )
- else:
- raise ValueError(f"W1Version {self.version} are not supported.")
+ spec = get_w1_version_spec(self.version)
+ self.d_list = np.asarray(spec.arm_d_list, dtype=float)
+ self.link_lengths = np.array(
+ [
+ self.d_list[0] + self.d_list[1],
+ self.d_list[2] + self.d_list[3],
+ self.d_list[4] + self.d_list[5],
+ self.d_list[6],
+ ]
+ )
# helpers: create DH rows and clamp limits
def dh_row(d, alpha, a, theta):
@@ -78,104 +73,52 @@ def deg2rad_list(list_of_pairs):
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
- [0.0, 0.0, 1.0, 0.1025],
+ [0.0, 0.0, 1.0, spec.arm_base_z],
[0.0, 0.0, 0.0, 1.0],
]
)
- # Build parameters per arm_kind and side, minimizing duplication
- if self.arm_kind == DexforceW1ArmKind.INDUSTRIAL:
- # default tcp for industrial
- T_e_oe = np.array(
- [
- [-1.0, 0.0, 0.0, 0.0],
- [0.0, -1.0, 0.0, 0.0],
- [0.0, 0.0, 1.0, 0.066],
- [0.0, 0.0, 0.0, 1.0],
- ]
- )
-
- # fmt: off
- dh = [
- dh_row(self.link_lengths[0], -np.pi / 2, 0.0, 0.0),
- dh_row(0.0, np.pi / 2, 0.0, 0.0),
- dh_row(self.link_lengths[1], np.pi / 2, 0.0, np.pi / 2),
- dh_row(0.0, -np.pi / 2, 0.0, 0.0),
- dh_row(self.link_lengths[2], -np.pi / 2, 0.0, 0.0),
- dh_row(0.0, np.pi / 2, 0.0, 0.0),
- dh_row(self.link_lengths[3], 0.0, 0.0, 0.0),
+ T_e_oe = np.array(
+ [
+ [0.0, 0.0, -1.0, -0.066],
+ [0.0, 1.0, 0.0, 0.0],
+ [1.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0],
]
-
- # fmt: on
- if self.arm_side == DexforceW1ArmSide.LEFT:
- limits = [
- [-170.0, 170.0],
- [-120.0, 90.0],
- [-170.0, 170.0],
- [-135.0, 90.0],
- [-170.0, 170.0],
- [-90.0, 90.0],
- [-170.0, 170.0],
- ]
- rotation_directions = np.array([1, 1, 1, 1, 1, -1, 1])
- else:
- limits = [
- [-170.0, 170.0],
- [-90.0, 120.0],
- [-170.0, 170.0],
- [-90.0, 135.0],
- [-170.0, 170.0],
- [-90.0, 90.0],
- [-170.0, 170.0],
- ]
- rotation_directions = np.array([1, 1, 1, -1, 1, 1, 1])
-
- self.T_e_oe = T_e_oe
-
- elif self.arm_kind == DexforceW1ArmKind.ANTHROPOMORPHIC:
- T_e_oe = np.array(
- [
- [0.0, 0.0, -1.0, -0.066],
- [0.0, 1.0, 0.0, 0.0],
- [1.0, 0.0, 0.0, 0.0],
- [0.0, 0.0, 0.0, 1.0],
- ]
- )
- # fmt: off
- dh = [
- dh_row(self.link_lengths[0], -np.pi / 2, 0.0, 0.0),
- dh_row(0.0, np.pi / 2, 0.0, 0.0),
- dh_row(self.link_lengths[1], np.pi / 2, 0.0, np.pi / 2),
- dh_row(0.0, -np.pi / 2, 0.0, 0.0),
- dh_row(self.link_lengths[2], -np.pi / 2, 0.0, 0.0),
- dh_row(0.0, np.pi / 2, 0.0, np.pi / 2),
- dh_row(self.link_lengths[3], 0.0, 0.0, 0.0),
+ )
+ # fmt: off
+ dh = [
+ dh_row(self.link_lengths[0], -np.pi / 2, 0.0, 0.0),
+ dh_row(0.0, np.pi / 2, 0.0, 0.0),
+ dh_row(self.link_lengths[1], np.pi / 2, 0.0, np.pi / 2),
+ dh_row(0.0, -np.pi / 2, 0.0, 0.0),
+ dh_row(self.link_lengths[2], -np.pi / 2, 0.0, 0.0),
+ dh_row(0.0, np.pi / 2, 0.0, np.pi / 2),
+ dh_row(self.link_lengths[3], 0.0, 0.0, 0.0),
+ ]
+ # fmt: on
+
+ if self.arm_side == DexforceW1ArmSide.LEFT:
+ limits = [
+ [-170.0, 170.0],
+ [-120.0, 90.0],
+ [-170.0, 170.0],
+ [-135.0, 90.0],
+ [-170.0, 170.0],
+ [-45.0, 45.0],
+ [-90.0, 60.0],
]
- # fmt: on
-
- if self.arm_side == DexforceW1ArmSide.LEFT:
- limits = [
- [-170.0, 170.0],
- [-120.0, 90.0],
- [-170.0, 170.0],
- [-135.0, 90.0],
- [-170.0, 170.0],
- [-45.0, 45.0],
- [-90.0, 60.0],
- ]
- rotation_directions = np.array([1, 1, 1, 1, 1, -1, 1])
- else:
- limits = [
- [-170.0, 170.0],
- [-90.0, 120.0],
- [-170.0, 170.0],
- [-90.0, 135.0],
- [-170.0, 170.0],
- [-45.0, 45.0],
- [-60.0, 90.0],
- ]
- rotation_directions = np.array([1, 1, 1, -1, 1, 1, 1])
+ rotation_directions = np.array([1, 1, 1, 1, 1, -1, 1])
else:
- raise ValueError(f"Unsupported arm_kind: {self.arm_kind}")
+ limits = [
+ [-170.0, 170.0],
+ [-90.0, 120.0],
+ [-170.0, 170.0],
+ [-90.0, 135.0],
+ [-170.0, 170.0],
+ [-45.0, 45.0],
+ [-60.0, 90.0],
+ ]
+ rotation_directions = np.array([1, 1, 1, -1, 1, 1, 1])
self.T_b_ob = T_b_ob
self.T_e_oe = T_e_oe
@@ -192,7 +135,6 @@ def deg2rad_list(list_of_pairs):
def as_dict(self) -> dict:
return {
"arm_side": self.arm_side.name,
- "arm_kind": self.arm_kind.name,
"version": self.version.name,
"link_lengths": self.link_lengths.tolist(),
"T_b_ob": self.T_b_ob.tolist(),
@@ -204,23 +146,9 @@ def as_dict(self) -> dict:
@classmethod
def from_dict(cls, data: dict) -> "W1ArmKineParams":
- arm_side = (
- DexforceW1ArmSide[data["arm_side"]]
- if isinstance(data.get("arm_side"), str)
- else data.get("arm_side")
- )
-
- arm_kind = (
- DexforceW1ArmKind[data["arm_kind"]]
- if isinstance(data.get("arm_kind"), str)
- else data.get("arm_kind")
- )
- version = (
- DexforceW1Version[data["version"]]
- if isinstance(data.get("version"), str)
- else data.get("version", DexforceW1Version.V021)
- )
- inst = cls(arm_side=arm_side, arm_kind=arm_kind, version=version)
+ arm_side = DexforceW1ArmSide.parse(data["arm_side"])
+ version = DexforceW1Version.parse(data.get("version", DexforceW1Version.V021))
+ inst = cls(arm_side=arm_side, version=version)
# allow overriding computed arrays if provided
if "dh_params" in data:
diff --git a/embodichain/lab/sim/robots/dexforce_w1/specs.py b/embodichain/lab/sim/robots/dexforce_w1/specs.py
new file mode 100644
index 000000000..986bb7819
--- /dev/null
+++ b/embodichain/lab/sim/robots/dexforce_w1/specs.py
@@ -0,0 +1,245 @@
+# ----------------------------------------------------------------------------
+# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ----------------------------------------------------------------------------
+
+"""Single source of truth for released Dexforce W1 hardware revisions."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from dataclasses import dataclass
+from types import MappingProxyType
+
+import numpy as np
+from scipy.spatial.transform import Rotation as R
+
+from .types import (
+ DexforceW1ArmSide,
+ DexforceW1Type,
+ DexforceW1Version,
+)
+
+__all__ = [
+ "W1VersionSpec",
+ "get_w1_version_spec",
+]
+
+
+_LEFT_TCP = (
+ (-1.0, 0.0, 0.0, 0.012),
+ (0.0, 0.0, 1.0, 0.0675),
+ (0.0, 1.0, 0.0, 0.127),
+ (0.0, 0.0, 0.0, 1.0),
+)
+_RIGHT_TCP = (
+ (1.0, 0.0, 0.0, 0.012),
+ (0.0, 0.0, -1.0, -0.0675),
+ (0.0, 1.0, 0.0, 0.127),
+ (0.0, 0.0, 0.0, 1.0),
+)
+_DEFAULT_TCP = {
+ DexforceW1ArmSide.LEFT: _LEFT_TCP,
+ DexforceW1ArmSide.RIGHT: _RIGHT_TCP,
+}
+
+
+@dataclass(frozen=True)
+class W1VersionSpec:
+ """Asset layout and calibrated defaults belonging to one W1 revision."""
+
+ version: DexforceW1Version
+ component_urdfs: Mapping[DexforceW1Type, str]
+ full_robot_urdf_path: str
+ arm_d_list: tuple[float, ...]
+ arm_base_z: float
+ default_eef_attach_xpos: Mapping[DexforceW1ArmSide, tuple]
+ solver_tcp: Mapping[DexforceW1ArmSide, tuple]
+ eyes_attach_xpos: tuple[tuple[float, ...], ...]
+ wrist_camera_rpy: tuple[float, float, float]
+ wrist_camera_xyz: tuple[float, float, float]
+ head_contains_eyes: bool = False
+
+ def __post_init__(self) -> None:
+ object.__setattr__(
+ self, "component_urdfs", MappingProxyType(dict(self.component_urdfs))
+ )
+ object.__setattr__(
+ self,
+ "default_eef_attach_xpos",
+ MappingProxyType(dict(self.default_eef_attach_xpos)),
+ )
+ object.__setattr__(self, "solver_tcp", MappingProxyType(dict(self.solver_tcp)))
+
+ @property
+ def assembly_name(self) -> str:
+ return f"DexforceW1V{self.version.value.removeprefix('v')}"
+
+ def component_urdf(self, component_type: DexforceW1Type) -> str:
+ try:
+ return self.component_urdfs[component_type]
+ except KeyError as exc:
+ raise ValueError(
+ f"W1 {self.version.value} has no registered "
+ f"{component_type.value} component asset."
+ ) from exc
+
+ def full_robot_urdf(self) -> str:
+ return self.full_robot_urdf_path
+
+ def eef_attach_xpos(self, arm_side: DexforceW1ArmSide) -> np.ndarray:
+ """Return the version-owned transform applied before every EEF."""
+ return np.asarray(self.default_eef_attach_xpos[arm_side], dtype=float).copy()
+
+ def compose_eef_attach_xpos(
+ self,
+ arm_side: DexforceW1ArmSide,
+ eef_xpos: np.ndarray,
+ ) -> np.ndarray:
+ """Compose the W1 revision offset with an EEF-specific transform."""
+ eef_xpos = np.asarray(eef_xpos, dtype=float)
+ if eef_xpos.shape != (4, 4):
+ raise ValueError(
+ f"EEF transform must have shape (4, 4), got {eef_xpos.shape}."
+ )
+ return self.eef_attach_xpos(arm_side) @ eef_xpos
+
+ def tcp(self, arm_side: DexforceW1ArmSide) -> np.ndarray:
+ """Return the final EE-to-TCP transform for this W1 revision."""
+ return self.compose_eef_attach_xpos(
+ arm_side,
+ np.asarray(self.solver_tcp[arm_side], dtype=float),
+ )
+
+ def eyes_xpos(self) -> np.ndarray:
+ return np.asarray(self.eyes_attach_xpos, dtype=float).copy()
+
+ def wrist_camera_xpos(self, arm_side: DexforceW1ArmSide) -> np.ndarray:
+ attach_xpos = np.eye(4)
+ attach_xpos[:3, :3] = R.from_euler("xyz", self.wrist_camera_rpy).as_matrix()
+ attach_xpos[:3, 3] = self.wrist_camera_xyz
+
+ frame_xpos = np.eye(4)
+ yaw = -90 if arm_side == DexforceW1ArmSide.LEFT else 90
+ frame_xpos[:3, :3] = R.from_rotvec([0, 0, yaw], degrees=True).as_matrix()
+ return frame_xpos @ attach_xpos
+
+
+_V021_COMPONENT_URDFS = {
+ DexforceW1Type.CHASSIS: "DexforceW1ChassisV021/chassis.urdf",
+ DexforceW1Type.TORSO: "DexforceW1TorsoV021/torso.urdf",
+ DexforceW1Type.EYES: "DexforceW1EyesV021/eyes.urdf",
+ DexforceW1Type.HEAD: "DexforceW1HeadV021/head.urdf",
+ DexforceW1Type.LEFT_ARM: "DexforceW1LeftArmV021/left_arm.urdf",
+ DexforceW1Type.RIGHT_ARM: "DexforceW1RightArmV021/right_arm.urdf",
+}
+_V022_COMPONENT_URDFS = {
+ DexforceW1Type.CHASSIS: "DexforceW1V022/w1/chassis.urdf",
+ DexforceW1Type.TORSO: "DexforceW1V022/w1/torso.urdf",
+ DexforceW1Type.HEAD: "DexforceW1V022/w1/head.urdf",
+ DexforceW1Type.LEFT_ARM: "DexforceW1V022/w1/left_arm.urdf",
+ DexforceW1Type.RIGHT_ARM: "DexforceW1V022/w1/right_arm.urdf",
+}
+_V025_COMPONENT_URDFS = {
+ DexforceW1Type.CHASSIS: "DexforceW1V025/w1/chassis.urdf",
+ DexforceW1Type.TORSO: "DexforceW1V025/w1/torso.urdf",
+ DexforceW1Type.HEAD: "DexforceW1V025/w1/head.urdf",
+ DexforceW1Type.LEFT_ARM: "DexforceW1V025/w1/left_arm.urdf",
+ DexforceW1Type.RIGHT_ARM: "DexforceW1V025/w1/right_arm.urdf",
+}
+# Verified against the released V022 and V025 left/right arm URDF joint origins.
+_SHARED_D_LIST = (0.0, 0.0, 0.260, 0.0, 0.166, 0.098, 0.0)
+_EYES_ATTACH_XPOS = (
+ (-0.0, 0.25959, -0.96572, 0.091),
+ (0.0, -0.96572, -0.25959, -0.051),
+ (-1.0, -0.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+)
+_WRIST_CAMERA_RPY = (2.79252648, 0.0, 1.57079633)
+_WRIST_CAMERA_XYZ = (0.08, 0.0, 0.06)
+_IDENTITY_XPOS = (
+ (1.0, 0.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+)
+_V021_DEFAULT_EEF_ATTACH_XPOS = {
+ DexforceW1ArmSide.LEFT: _IDENTITY_XPOS,
+ DexforceW1ArmSide.RIGHT: _IDENTITY_XPOS,
+}
+# Provisional V022 baseline. Replace this with the measured arm-flange
+# transform before declaring the release calibrated.
+_V022_DEFAULT_EEF_ATTACH_XPOS = {
+ DexforceW1ArmSide.LEFT: _IDENTITY_XPOS,
+ DexforceW1ArmSide.RIGHT: _IDENTITY_XPOS,
+}
+# Calibrated outward flange offset shared by every V025 end effector.
+_V025_EEF_ATTACH_XPOS = (
+ (1.0, 0.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0, 0.012),
+ (0.0, 0.0, 0.0, 1.0),
+)
+_V025_DEFAULT_EEF_ATTACH_XPOS = {
+ DexforceW1ArmSide.LEFT: _V025_EEF_ATTACH_XPOS,
+ DexforceW1ArmSide.RIGHT: _V025_EEF_ATTACH_XPOS,
+}
+
+_W1_VERSION_SPECS = {
+ DexforceW1Version.V021: W1VersionSpec(
+ version=DexforceW1Version.V021,
+ component_urdfs=_V021_COMPONENT_URDFS,
+ full_robot_urdf_path="DexforceW1V021/DexforceW1_v02_1.urdf",
+ arm_d_list=_SHARED_D_LIST,
+ arm_base_z=0.1025,
+ default_eef_attach_xpos=_V021_DEFAULT_EEF_ATTACH_XPOS,
+ solver_tcp=_DEFAULT_TCP,
+ eyes_attach_xpos=_EYES_ATTACH_XPOS,
+ wrist_camera_rpy=_WRIST_CAMERA_RPY,
+ wrist_camera_xyz=_WRIST_CAMERA_XYZ,
+ ),
+ DexforceW1Version.V022: W1VersionSpec(
+ version=DexforceW1Version.V022,
+ component_urdfs=_V022_COMPONENT_URDFS,
+ full_robot_urdf_path="DexforceW1V022/w1/robot.urdf",
+ # Verified from the V022 left/right arm URDF joint origins.
+ arm_d_list=_SHARED_D_LIST,
+ arm_base_z=0.1025,
+ default_eef_attach_xpos=_V022_DEFAULT_EEF_ATTACH_XPOS,
+ solver_tcp=_DEFAULT_TCP,
+ eyes_attach_xpos=_EYES_ATTACH_XPOS,
+ wrist_camera_rpy=_WRIST_CAMERA_RPY,
+ wrist_camera_xyz=_WRIST_CAMERA_XYZ,
+ # Verified from V022 head.urdf: it contains the eyes link and EYES joint.
+ head_contains_eyes=True,
+ ),
+ DexforceW1Version.V025: W1VersionSpec(
+ version=DexforceW1Version.V025,
+ component_urdfs=_V025_COMPONENT_URDFS,
+ full_robot_urdf_path="DexforceW1V025/w1/robot.urdf",
+ # Verified against left_arm.urdf/right_arm.urdf in the V025 release.
+ arm_d_list=_SHARED_D_LIST,
+ arm_base_z=0.1025,
+ default_eef_attach_xpos=_V025_DEFAULT_EEF_ATTACH_XPOS,
+ solver_tcp=_DEFAULT_TCP,
+ eyes_attach_xpos=_EYES_ATTACH_XPOS,
+ wrist_camera_rpy=_WRIST_CAMERA_RPY,
+ wrist_camera_xyz=_WRIST_CAMERA_XYZ,
+ head_contains_eyes=True,
+ ),
+}
+
+
+def get_w1_version_spec(version: DexforceW1Version | str) -> W1VersionSpec:
+ return _W1_VERSION_SPECS[DexforceW1Version.parse(version)]
diff --git a/embodichain/lab/sim/robots/dexforce_w1/types.py b/embodichain/lab/sim/robots/dexforce_w1/types.py
index fa65d07e0..27bcf756e 100644
--- a/embodichain/lab/sim/robots/dexforce_w1/types.py
+++ b/embodichain/lab/sim/robots/dexforce_w1/types.py
@@ -15,53 +15,76 @@
# ----------------------------------------------------------------------------
import enum
+import re
+from typing import TypeVar
__all__ = [
"DexforceW1Version",
- "DexforceW1ArmKind",
+ "DexforceW1HandVersion",
"DexforceW1ArmSide",
"DexforceW1Type",
"DexforceW1HandBrand",
]
+_W1EnumT = TypeVar("_W1EnumT", bound="_W1Enum")
-class DexforceW1Version(enum.Enum):
- """Versioning for DexforceW1 components."""
+
+class _W1Enum(enum.Enum):
+ @classmethod
+ def _parse_label(cls) -> str:
+ name = cls.__name__.removeprefix("DexforceW1")
+ words = re.sub(r"(? _W1EnumT:
+ """Parse an enum instance, member name, or serialized value."""
+ if isinstance(value, cls):
+ return value
+ if isinstance(value, str):
+ normalized = value.lower()
+ for member in cls:
+ if normalized in (member.name.lower(), str(member.value).lower()):
+ return member
+ raise ValueError(f"Invalid {cls._parse_label()}: {value!r}")
+
+
+class DexforceW1Version(_W1Enum):
+ """Released version of the W1 robot body and arms."""
V021 = "v021"
+ V022 = "v022"
+ V025 = "v025"
-class DexforceW1ArmKind(enum.Enum):
- """Arm type for DexforceW1: anthropomorphic or industrial."""
+class DexforceW1HandVersion(_W1Enum):
+ """Released version of an external W1 hand or gripper asset."""
- ANTHROPOMORPHIC = "anthropomorphic"
- INDUSTRIAL = "industrial"
+ V021 = "v021"
-class DexforceW1ArmSide(enum.Enum):
+class DexforceW1ArmSide(_W1Enum):
"""Arm side for DexforceW1: left or right."""
LEFT = "left"
RIGHT = "right"
-class DexforceW1Type(enum.Enum):
+class DexforceW1Type(_W1Enum):
"""Component type for DexforceW1."""
CHASSIS = "chassis"
TORSO = "torso"
EYES = "eyes"
HEAD = "head"
- LEFT_ARM1 = "left_arm" # Anthropomorphic left arm
- RIGHT_ARM1 = "right_arm" # Anthropomorphic right arm
- LEFT_ARM2 = "left_arm2" # Industrial left arm
- RIGHT_ARM2 = "right_arm2" # Industrial right arm
+ LEFT_ARM = "left_arm"
+ RIGHT_ARM = "right_arm"
LEFT_HAND = "left_hand"
RIGHT_HAND = "right_hand"
FULL_BODY = "full_body" # Full robot
-class DexforceW1HandBrand(enum.Enum):
+class DexforceW1HandBrand(_W1Enum):
BRAINCO_HAND = "BRAINCO_HAND"
DH_PGC_GRIPPER = "DH_PGC_GRIPPER"
DH_PGC_GRIPPER_M = "DH_PGC_GRIPPER_M"
diff --git a/embodichain/lab/sim/robots/dexforce_w1/utils.py b/embodichain/lab/sim/robots/dexforce_w1/utils.py
index 9402b5657..1a6d73c1b 100644
--- a/embodichain/lab/sim/robots/dexforce_w1/utils.py
+++ b/embodichain/lab/sim/robots/dexforce_w1/utils.py
@@ -13,20 +13,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
+import xml.etree.ElementTree as ET
+
import numpy as np
-from scipy.spatial.transform import Rotation as R
-from typing import List, Dict
from embodichain.lab.sim.robots.dexforce_w1.types import (
- DexforceW1ArmKind,
DexforceW1Type,
DexforceW1ArmSide,
DexforceW1Version,
DexforceW1HandBrand,
+ DexforceW1HandVersion,
+)
+from embodichain.lab.sim.robots.dexforce_w1.hand_specs import (
+ get_default_w1_hand_version,
+ get_w1_hand_spec,
)
from embodichain.data import get_data_path
-from embodichain.lab.sim.solvers import SolverCfg
-from embodichain.lab.sim.cfg import RobotCfg, URDFCfg
+from embodichain.lab.sim.cfg import URDFCfg
+from embodichain.lab.sim.robots.dexforce_w1.specs import (
+ get_w1_version_spec,
+)
__all__ = [
"ChassisManager",
@@ -36,18 +42,14 @@
"HandManager",
"EyesManager",
"build_dexforce_w1_assembly_urdf_cfg",
- "build_dexforce_w1_cfg",
]
class ChassisManager:
- def __init__(self):
- self._urdf_rel_paths = {
- DexforceW1Version.V021: "DexforceW1ChassisV021/chassis.urdf",
- }
-
def get_urdf(self, version=DexforceW1Version.V021):
- return get_data_path(self._urdf_rel_paths[version])
+ return get_data_path(
+ get_w1_version_spec(version).component_urdf(DexforceW1Type.CHASSIS)
+ )
def get_config(self, version=DexforceW1Version.V021):
return {
@@ -60,13 +62,12 @@ def get_config(self, version=DexforceW1Version.V021):
class TorsoManager:
def __init__(self):
- self._urdf_rel_paths = {
- DexforceW1Version.V021: "DexforceW1TorsoV021/torso.urdf",
- }
self.joint_names = ["ANKLE", "KNEE", "BUTTOCK", "WAIST"]
def get_urdf(self, version=DexforceW1Version.V021):
- return get_data_path(self._urdf_rel_paths[version])
+ return get_data_path(
+ get_w1_version_spec(version).component_urdf(DexforceW1Type.TORSO)
+ )
def get_config(self, version=DexforceW1Version.V021):
return {
@@ -79,13 +80,12 @@ def get_config(self, version=DexforceW1Version.V021):
class HeadManager:
def __init__(self):
- self._urdf_rel_paths = {
- DexforceW1Version.V021: "DexforceW1HeadV021/head.urdf",
- }
self.joint_names = ["NECK1", "NECK2"]
def get_urdf(self, version=DexforceW1Version.V021):
- return get_data_path(self._urdf_rel_paths[version])
+ return get_data_path(
+ get_w1_version_spec(version).component_urdf(DexforceW1Type.HEAD)
+ )
def get_config(self, version=DexforceW1Version.V021):
return {
@@ -97,13 +97,10 @@ def get_config(self, version=DexforceW1Version.V021):
class EyesManager:
- def __init__(self):
- self._urdf_rel_paths = {
- DexforceW1Version.V021: "DexforceW1EyesV021/eyes.urdf",
- }
-
def get_urdf(self, version=DexforceW1Version.V021):
- return get_data_path(self._urdf_rel_paths[version])
+ return get_data_path(
+ get_w1_version_spec(version).component_urdf(DexforceW1Type.EYES)
+ )
def get_config(self, version=DexforceW1Version.V021):
return {
@@ -115,37 +112,22 @@ def get_config(self, version=DexforceW1Version.V021):
class ArmManager:
- def __init__(self):
- self._urdf_rel_paths = {
- (
- DexforceW1ArmKind.ANTHROPOMORPHIC,
- DexforceW1ArmSide.LEFT,
- DexforceW1Version.V021,
- ): "DexforceW1LeftArm1V021/left_arm.urdf",
- (
- DexforceW1ArmKind.ANTHROPOMORPHIC,
- DexforceW1ArmSide.RIGHT,
- DexforceW1Version.V021,
- ): "DexforceW1RightArm1V021/right_arm.urdf",
- (
- DexforceW1ArmKind.INDUSTRIAL,
- DexforceW1ArmSide.LEFT,
- DexforceW1Version.V021,
- ): "DexforceW1LeftArm2V021/left_arm.urdf",
- (
- DexforceW1ArmKind.INDUSTRIAL,
- DexforceW1ArmSide.RIGHT,
- DexforceW1Version.V021,
- ): "DexforceW1RightArm2V021/right_arm.urdf",
- }
-
- def get_urdf(self, kind, side, version=DexforceW1Version.V021):
- return get_data_path(self._urdf_rel_paths[(kind, side, version)])
+ def get_urdf(self, side, version=DexforceW1Version.V021):
+ spec = get_w1_version_spec(version)
+ return get_data_path(spec.component_urdf(self.get_component_type(side)))
+
+ @staticmethod
+ def get_component_type(side):
+ return (
+ DexforceW1Type.LEFT_ARM
+ if side == DexforceW1ArmSide.LEFT
+ else DexforceW1Type.RIGHT_ARM
+ )
- def get_config(self, kind, side, version=DexforceW1Version.V021):
+ def get_config(self, side, version=DexforceW1Version.V021):
prefix = "LEFT" if side == DexforceW1ArmSide.LEFT else "RIGHT"
return {
- "urdf_path": self.get_urdf(kind, side, version),
+ "urdf_path": self.get_urdf(side, version),
"joint_names": [f"{prefix}_J{i}" for i in range(1, 8)],
"end_link_name": f"{prefix.lower()}_ee",
"root_link_name": f"{prefix.lower()}_arm_base",
@@ -153,132 +135,40 @@ def get_config(self, kind, side, version=DexforceW1Version.V021):
class HandManager:
- def __init__(self):
- self._urdf_rel_paths = {
- (
- DexforceW1HandBrand.BRAINCO_HAND,
- DexforceW1ArmSide.LEFT,
- DexforceW1Version.V021,
- ): "BrainCoHandRevo1/BrainCoLeftHand/BrainCoLeftHand.urdf",
- (
- DexforceW1HandBrand.BRAINCO_HAND,
- DexforceW1ArmSide.RIGHT,
- DexforceW1Version.V021,
- ): "BrainCoHandRevo1/BrainCoRightHand/BrainCoRightHand.urdf",
- (
- DexforceW1HandBrand.DH_PGC_GRIPPER,
- DexforceW1ArmSide.LEFT,
- DexforceW1Version.V021,
- ): "DH_PGC_140_50/DH_PGC_140_50.urdf",
- (
- DexforceW1HandBrand.DH_PGC_GRIPPER,
- DexforceW1ArmSide.RIGHT,
- DexforceW1Version.V021,
- ): "DH_PGC_140_50/DH_PGC_140_50.urdf",
- (
- DexforceW1HandBrand.DH_PGC_GRIPPER_M,
- DexforceW1ArmSide.LEFT,
- DexforceW1Version.V021,
- ): "DH_PGC_140_50_M/DH_PGC_140_50_M.urdf",
- (
- DexforceW1HandBrand.DH_PGC_GRIPPER_M,
- DexforceW1ArmSide.RIGHT,
- DexforceW1Version.V021,
- ): "DH_PGC_140_50_M/DH_PGC_140_50_M.urdf",
- }
-
def get_config(
self,
brand: DexforceW1HandBrand,
side: DexforceW1ArmSide,
- version: DexforceW1Version = DexforceW1Version.V021,
+ version: DexforceW1HandVersion | None = None,
):
- prefix = "LEFT" if side == DexforceW1ArmSide.LEFT else "RIGHT"
- if brand == DexforceW1HandBrand.BRAINCO_HAND:
- if side == DexforceW1ArmSide.LEFT:
- base_link_name = f"{prefix.lower()}_hand_base"
- root_link_name = f"{prefix.lower()}_thumb_dist"
- joint_names = [
- f"{prefix}_HAND_THUMB1", # Left thumb flexion
- f"{prefix}_HAND_THUMB2", # Left thumb abduction/adduction
- f"{prefix}_HAND_INDEX", # Left index finger flexion
- f"{prefix}_HAND_MIDDLE", # Left middle finger flexion
- f"{prefix}_HAND_RING", # Left ring finger flexion
- f"{prefix}_HAND_PINKY", # Left pinky finger flexion
- ]
- else:
- base_link_name = f"{prefix.lower()}_hand_base"
- root_link_name = f"{prefix.lower()}_thumb_dist"
- joint_names = [
- f"{prefix}_HAND_THUMB1", # Right thumb flexion
- f"{prefix}_HAND_THUMB2", # Right thumb abduction/adduction
- f"{prefix}_HAND_INDEX", # Right index finger flexion
- f"{prefix}_HAND_MIDDLE", # Right middle finger flexion
- f"{prefix}_HAND_RING", # Right ring finger flexion
- f"{prefix}_HAND_PINKY", # Right pinky finger flexion
- ]
- elif brand == DexforceW1HandBrand.DH_PGC_GRIPPER:
- base_link_name = f"{prefix.lower()}_base_link_1"
- root_link_name = f"{prefix.lower()}_finger2_link"
- joint_names = [f"{prefix}_FINGER1_JOINT", f"{prefix}_FINGER2_JOINT"]
- elif brand == DexforceW1HandBrand.DH_PGC_GRIPPER_M:
- base_link_name = f"{prefix.lower()}_base_link_1"
- root_link_name = f"{prefix.lower()}_finger2"
- joint_names = [f"{prefix}_FINGER1", f"{prefix}_FINGER2"]
- else:
- raise ValueError(f"Unknown hand brand: {brand}")
-
+ version = version or get_default_w1_hand_version(brand)
+ side_spec = get_w1_hand_spec(brand, version).for_side(side)
return {
- "urdf_path": self.get_urdf(brand, side, version),
- "joint_names": joint_names,
- "end_link_name": base_link_name,
- "root_link_name": root_link_name,
+ "urdf_path": get_data_path(side_spec.urdf_path),
+ "joint_names": list(side_spec.joint_names),
+ "end_link_name": side_spec.end_link_name,
+ "root_link_name": side_spec.root_link_name,
}
def get_urdf(
self,
brand: DexforceW1HandBrand,
side: DexforceW1ArmSide,
- version: DexforceW1Version = DexforceW1Version.V021,
+ version: DexforceW1HandVersion | None = None,
):
- return get_data_path(self._urdf_rel_paths[(brand, side, version)])
+ version = version or get_default_w1_hand_version(brand)
+ side_spec = get_w1_hand_spec(brand, version).for_side(side)
+ return get_data_path(side_spec.urdf_path)
def get_attach_xpos(
self,
brand: DexforceW1HandBrand,
- arm_kind: DexforceW1ArmKind = DexforceW1ArmKind.INDUSTRIAL,
- is_left: bool = True,
+ arm_side: DexforceW1ArmSide = DexforceW1ArmSide.LEFT,
+ version: DexforceW1HandVersion | None = None,
):
- if brand == DexforceW1HandBrand.BRAINCO_HAND:
- rot_params = {
- (DexforceW1ArmKind.INDUSTRIAL, True): [90, 0, 0],
- (DexforceW1ArmKind.INDUSTRIAL, False): [90, 0, 180],
- (DexforceW1ArmKind.ANTHROPOMORPHIC, True): [90, 0, 180],
- (DexforceW1ArmKind.ANTHROPOMORPHIC, False): [90, 0, 0],
- }
- attach_xpos = np.eye(4)
- rot = R.from_euler("xyz", rot_params[(arm_kind, is_left)], degrees=True)
- attach_xpos[:3, :3] = rot.as_matrix()
- attach_xpos[2, 3] = 0.0
- return attach_xpos
- elif brand == DexforceW1HandBrand.DH_PGC_GRIPPER:
- attach_xpos = np.array(
- [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0.015], [0, 0, 0, 1]]
- )
- attach_xpos[:3, :3] = (
- attach_xpos[:3, :3]
- @ R.from_rotvec([0, 0, 90], degrees=True).as_matrix()
- )
- return attach_xpos
- elif brand == DexforceW1HandBrand.DH_PGC_GRIPPER_M:
- attach_xpos = np.eye(4)
- attach_xpos[:3, :3] = (
- attach_xpos[:3, :3]
- @ R.from_rotvec([0, 0, 90], degrees=True).as_matrix()
- )
- return attach_xpos
- else:
- raise ValueError(f"Unknown brand: {brand}")
+ version = version or get_default_w1_hand_version(brand)
+ side_spec = get_w1_hand_spec(brand, version).for_side(arm_side)
+ return np.asarray(side_spec.attach_xpos, dtype=float).copy()
eyes_manager = EyesManager()
@@ -290,86 +180,60 @@ def get_attach_xpos(
def build_dexforce_w1_assembly_urdf_cfg(
- arm_kind: DexforceW1ArmKind,
- arm_sides: List[DexforceW1ArmSide] = [
- DexforceW1ArmSide.LEFT,
- DexforceW1ArmSide.RIGHT,
- ],
- fname: str | None = "DexforceW1V021",
+ version: DexforceW1Version = DexforceW1Version.V021,
+ fname: str | None = None,
hand_types: dict[DexforceW1ArmSide, DexforceW1HandBrand] | None = None,
- hand_versions: dict[DexforceW1ArmSide, DexforceW1Version] | None = None,
+ hand_versions: dict[DexforceW1ArmSide, DexforceW1HandVersion] | None = None,
hand_attach_xposes: dict[DexforceW1ArmSide, np.ndarray] | None = None,
- include_chassis: bool = True,
- include_torso: bool = True,
- include_head: bool = True,
include_hand: bool = True,
- include_eyes: bool = True,
- include_wrist_cameras: bool = True,
- component_versions: dict[DexforceW1Type, DexforceW1Version] | None = None,
) -> URDFCfg:
"""
Assemble DexforceW1 robot urdf configuration.
Args:
- arm_kind: Arm type (anthropomorphic or industrial).
- arm_sides: List of arm sides to include (left/right). Default both sides.
- fname: Output configuration name. Default "DexforceW1V021".
+ version: W1 version used by every robot component.
+ fname: Output configuration name. Defaults to the version assembly name.
hand_types: Dict specifying hand brand (DexforceW1HandBrand) for each arm side. Default None, which uses the default brand.
- hand_versions: Dict specifying hand version for each arm side. Default None, which uses the default version.
+ hand_versions: Hand asset version for each side. Defaults to hand V021,
+ independently of the W1 robot version.
hand_attach_xposes: Dict specifying hand attachment pose for each arm side. Default None, which uses the default attachment pose.
- include_chassis: Whether to include chassis. Default True.
- include_torso: Whether to include torso. Default True.
- include_head: Whether to include head. Default True.
include_hand: Whether to include hand. Default True.
- include_wrist_cameras: Whether to include wrist cameras. Default True.
- component_versions: Dict specifying version for each robot component. Default all V021.
Returns:
URDFCfg: Assembled URDF configuration.
"""
- def get_version(t, default=DexforceW1Version.V021):
- return (component_versions or {}).get(t, default)
+ version = DexforceW1Version.parse(version)
+ hand_versions = {
+ DexforceW1ArmSide.parse(side): DexforceW1HandVersion.parse(hand_version)
+ for side, hand_version in (hand_versions or {}).items()
+ }
- components = []
- if include_chassis:
- components.append(
- {
- "component_type": "chassis",
- "urdf_path": chassis_manager.get_urdf(
- get_version(DexforceW1Type.CHASSIS)
- ),
- }
- )
- if include_torso:
- components.append(
- {
- "component_type": "torso",
- "urdf_path": torso_manager.get_urdf(get_version(DexforceW1Type.TORSO)),
- }
- )
- if include_head:
- components.append(
- {
- "component_type": "head",
- "urdf_path": head_manager.get_urdf(get_version(DexforceW1Type.HEAD)),
- }
- )
+ if fname is None:
+ fname = get_w1_version_spec(version).assembly_name
+
+ components = [
+ {
+ "component_type": "chassis",
+ "urdf_path": chassis_manager.get_urdf(version),
+ },
+ {
+ "component_type": "torso",
+ "urdf_path": torso_manager.get_urdf(version),
+ },
+ {
+ "component_type": "head",
+ "urdf_path": head_manager.get_urdf(version),
+ },
+ ]
sensors = []
- if include_eyes:
+ head_spec = get_w1_version_spec(version)
+ head_contains_eyes = head_spec.head_contains_eyes
+ if not head_contains_eyes:
# TODO: Support user-defined eye transforms
- import xml.etree.ElementTree as ET
-
- attach_xpos = np.array(
- [
- [-0.0, 0.25959, -0.96572, 0.091],
- [0.0, -0.96572, -0.25959, -0.051],
- [-1.0, -0.0, 0.0, 0.0],
- [0.0, 0.0, 0.0, 1.0],
- ]
- )
+ attach_xpos = head_spec.eyes_xpos()
joint_xml = """
@@ -402,73 +266,43 @@ def get_version(t, default=DexforceW1Version.V021):
"sensor_type": "camera",
}
)
- if include_wrist_cameras:
- for arm_side in arm_sides:
- # TODO: Support user-defined eye transforms
- import xml.etree.ElementTree as ET
-
- if arm_side == DexforceW1ArmSide.LEFT:
- rpy = [2.79252648, 0.0, 1.57079633]
- xyz = [0.08, 0.0, 0.06]
- tf_xpos = np.eye(4)
- tf_xpos[:3, :3] = R.from_rotvec([0, 0, -90], degrees=True).as_matrix()
- else:
- rpy = [2.79252648, 0.0, 1.57079633]
- xyz = [0.08, 0.0, 0.06]
- tf_xpos = np.eye(4)
- tf_xpos[:3, :3] = R.from_rotvec([0, 0, 90], degrees=True).as_matrix()
-
- attach_xpos = np.eye(4)
- attach_xpos[:3, :3] = R.from_euler("xyz", rpy).as_matrix()
- attach_xpos[:3, 3] = xyz
- attach_xpos = tf_xpos @ attach_xpos
-
- joint_xml = f"""
-
-
-
-
-
- """
-
- link_xml = f"""
-
-
-
-
-
-
-
- """
-
- joint_elem = ET.fromstring(joint_xml)
- link_elem = ET.fromstring(link_xml)
- sensors.append(
- {
- "sensor_name": f"{arm_side.value.lower()}_wrist_camera",
- "sensor_source": ([link_elem], [joint_elem]),
- "parent_component": f"{arm_side.value}_arm",
- "parent_link": f"{arm_side.value}_ee",
- "transform": attach_xpos,
- "sensor_type": "camera",
- }
- )
+ for arm_side in DexforceW1ArmSide:
+ camera_spec = get_w1_version_spec(version)
+ attach_xpos = camera_spec.wrist_camera_xpos(arm_side)
- for arm_side in arm_sides:
- if arm_kind == DexforceW1ArmKind.ANTHROPOMORPHIC:
- arm_type = (
- DexforceW1Type.LEFT_ARM1
- if arm_side == DexforceW1ArmSide.LEFT
- else DexforceW1Type.RIGHT_ARM1
- )
- else:
- arm_type = (
- DexforceW1Type.LEFT_ARM2
- if arm_side == DexforceW1ArmSide.LEFT
- else DexforceW1Type.RIGHT_ARM2
- )
- arm_version = get_version(arm_type)
- arm_cfg = arm_manager.get_config(arm_kind, arm_side, arm_version)
+ joint_xml = f"""
+
+
+
+
+
+ """
+
+ link_xml = f"""
+
+
+
+
+
+
+
+ """
+
+ joint_elem = ET.fromstring(joint_xml)
+ link_elem = ET.fromstring(link_xml)
+ sensors.append(
+ {
+ "sensor_name": f"{arm_side.value.lower()}_wrist_camera",
+ "sensor_source": ([link_elem], [joint_elem]),
+ "parent_component": f"{arm_side.value}_arm",
+ "parent_link": f"{arm_side.value}_ee",
+ "transform": attach_xpos,
+ "sensor_type": "camera",
+ }
+ )
+
+ for arm_side in DexforceW1ArmSide:
+ arm_cfg = arm_manager.get_config(arm_side, version)
components.append(
{
"component_type": f"{arm_side.value}_arm",
@@ -477,27 +311,30 @@ def get_version(t, default=DexforceW1Version.V021):
)
if include_hand:
- for arm_side in arm_sides:
+ for arm_side in DexforceW1ArmSide:
# hand_brand: DexforceW1HandBrand
hand_brand = (hand_types or {}).get(
arm_side, DexforceW1HandBrand.BRAINCO_HAND
)
- hand_version = (hand_versions or {}).get(
- arm_side,
- get_version(
- DexforceW1Type.LEFT_HAND
- if arm_side == DexforceW1ArmSide.LEFT
- else DexforceW1Type.RIGHT_HAND
- ),
+ hand_version = hand_versions.get(
+ arm_side, get_default_w1_hand_version(hand_brand)
)
urdf_path = hand_manager.get_urdf(hand_brand, arm_side, hand_version)
- attach_xpos = (hand_attach_xposes or {}).get(
- arm_side,
- hand_manager.get_attach_xpos(
- hand_brand, arm_kind, arm_side == DexforceW1ArmSide.LEFT
- ),
- )
+ custom_attach_xpos = (hand_attach_xposes or {}).get(arm_side)
+ if custom_attach_xpos is None:
+ hand_attach_xpos = hand_manager.get_attach_xpos(
+ hand_brand, arm_side, hand_version
+ )
+ arm_spec = get_w1_version_spec(version)
+ attach_xpos = arm_spec.compose_eef_attach_xpos(
+ arm_side, hand_attach_xpos
+ )
+ else:
+ arm_spec = get_w1_version_spec(version)
+ attach_xpos = arm_spec.compose_eef_attach_xpos(
+ arm_side, custom_attach_xpos
+ )
components.append(
{
"component_type": f"{arm_side.value}_hand",
@@ -505,239 +342,62 @@ def get_version(t, default=DexforceW1Version.V021):
"transform": attach_xpos,
}
)
- return URDFCfg(components=components, sensors=sensors, fname=fname)
-
-
-def build_dexforce_w1_solver_cfg(
- arm_kind: DexforceW1ArmKind,
- arm_sides: List[DexforceW1ArmSide] = [
- DexforceW1ArmSide.LEFT,
- DexforceW1ArmSide.RIGHT,
- ],
- component_versions: dict[DexforceW1Type, DexforceW1Version] | None = None,
- urdf_cfg: URDFCfg | None = None,
-) -> Dict[str, SolverCfg]:
- """
- Build DexforceW1 solver configuration dict.
-
- Args:
- arm_kind: Arm type.
- arm_sides: Included arm sides. Optional, default both sides.
- component_versions: Component version dict. Optional, default all V021.
- urdf_cfg: Optional, URDFCfg object from build_dexforce_w1_assembly_urdf_cfg.
-
- Returns:
- Dict[str, SolverCfg]: solver config keyed by control part name
- (e.g. ``"left_arm"``, ``"full_body"``).
- """
-
- def get_version(t, default=DexforceW1Version.V021):
- return (component_versions or {}).get(t, default)
-
- solver_cfg = {}
-
- for arm_side in arm_sides:
- if arm_kind == DexforceW1ArmKind.ANTHROPOMORPHIC:
- arm_type = (
- DexforceW1Type.LEFT_ARM1
- if arm_side == DexforceW1ArmSide.LEFT
- else DexforceW1Type.RIGHT_ARM1
- )
- else:
- arm_type = (
- DexforceW1Type.LEFT_ARM2
- if arm_side == DexforceW1ArmSide.LEFT
- else DexforceW1Type.RIGHT_ARM2
- )
- arm_version = get_version(arm_type)
- arm_cfg = arm_manager.get_config(arm_kind, arm_side, arm_version)
- # Use control_parts-aligned key (e.g. "left_arm") so init_solver
- # can match this entry to the corresponding control part.
- solver_key = f"{arm_side.value}_arm"
- solver_cfg[solver_key] = SolverCfg.from_dict(
- {
- "class_type": "PytorchSolver",
- "urdf_path": arm_cfg["urdf_path"],
- "joint_names": arm_cfg["joint_names"],
- "end_link_name": arm_cfg["end_link_name"],
- "root_link_name": arm_cfg["root_link_name"],
- }
- )
-
- # Use urdf_cfg.fpath if provided, otherwise fallback to default path
- full_body_urdf_path = (
- urdf_cfg.fpath or get_data_path("DexforceW1FullBodyV021/full_body.urdf")
- if urdf_cfg is not None
- else get_data_path("DexforceW1FullBodyV021/full_body.urdf")
+ # W1 exposes stable uppercase joint names and lowercase link names
+ # independently of the casing used by each source component URDF.
+ return URDFCfg(
+ components=components,
+ sensors=sensors,
+ fname=fname,
+ name_case={"joint": "upper", "link": "lower"},
)
- solver_cfg[DexforceW1Type.FULL_BODY.value] = SolverCfg.from_dict(
- {
- "class_type": "PytorchSolver",
- "urdf_path": full_body_urdf_path,
- "joint_names": [
- "ANKLE",
- "KNEE",
- "BUTTOCK",
- "WAIST",
- "NECK1",
- "NECK2",
- "LEFT_J1",
- "LEFT_J2",
- "LEFT_J3",
- "LEFT_J4",
- "LEFT_J5",
- "LEFT_J6",
- "LEFT_J7",
- "RIGHT_J1",
- "RIGHT_J2",
- "RIGHT_J3",
- "RIGHT_J4",
- "RIGHT_J5",
- "RIGHT_J6",
- "RIGHT_J7",
- ],
- "end_link_name": "right_ee",
- "root_link_name": "base_link",
- }
- )
- return solver_cfg
+def build_dexforce_w1_control_parts(
+ version: DexforceW1Version,
+ hand_types: dict[DexforceW1ArmSide, DexforceW1HandBrand] | None,
+ hand_versions: dict[DexforceW1ArmSide, DexforceW1HandVersion] | None,
+ include_hand: bool,
+) -> dict[str, list[str]]:
+ """Build control-part joint lists for a complete dual-arm W1."""
+ version = DexforceW1Version.parse(version)
+ hand_versions = {
+ DexforceW1ArmSide.parse(side): DexforceW1HandVersion.parse(hand_version)
+ for side, hand_version in (hand_versions or {}).items()
+ }
-def build_dexforce_w1_cfg(
- arm_kind: DexforceW1ArmKind,
- arm_sides: List[DexforceW1ArmSide] = [
- DexforceW1ArmSide.LEFT,
- DexforceW1ArmSide.RIGHT,
- ],
- hand_types: dict[DexforceW1ArmSide, DexforceW1HandBrand] | None = None,
- hand_versions: dict[DexforceW1ArmSide, DexforceW1Version] | None = None,
- hand_attach_xposes: dict[DexforceW1ArmSide, np.ndarray] | None = None,
- include_chassis: bool = True,
- include_torso: bool = True,
- include_head: bool = True,
- include_hand: bool = True,
- component_versions: dict[DexforceW1Type, DexforceW1Version] | None = None,
- solver_cfg: dict[DexforceW1Type, SolverCfg] | None = None,
-) -> "DexforceW1Cfg":
- """
- Build DexforceW1 robot configuration object.
+ arm_joints = {}
+ for arm_side in DexforceW1ArmSide:
+ arm_joints[arm_side] = arm_manager.get_config(arm_side, version)["joint_names"]
- Args:
- arm_kind: Arm type (anthropomorphic or industrial).
- arm_sides: List of arm sides to include (left/right). Default both sides.
- hand_types: Dict specifying hand brand (DexforceW1HandBrand) for each arm side. Default None, which uses the default brand.
- hand_versions: Dict specifying hand version for each arm side. Default None, which uses the default version.
- hand_attach_xposes: Dict specifying hand attachment pose for each arm side. Default None, which uses the default attachment pose.
- include_chassis: Whether to include chassis. Optional, default True.
- include_torso: Whether to include torso. Optional, default True.
- include_head: Whether to include head. Optional, default True.
- include_hand: Whether to include hand. Optional, default True.
- include_wrist_cameras: Whether to include wrist cameras. Optional, default True.
- component_versions: Dict specifying version for each robot component.
- solver_cfg: Optional, pre-defined solver configuration dict.
-
- Returns:
- DexforceW1Cfg: Robot configuration object.
- """
- urdf_cfg = build_dexforce_w1_assembly_urdf_cfg(
- arm_kind=arm_kind,
- arm_sides=arm_sides,
- hand_types=hand_types,
- hand_versions=hand_versions,
- hand_attach_xposes=hand_attach_xposes,
- include_chassis=include_chassis,
- include_torso=include_torso,
- include_head=include_head,
- include_hand=include_hand,
- component_versions=component_versions,
- )
+ torso_joints = torso_manager.get_config(version)["joint_names"]
+ head_joints = head_manager.get_config(version)["joint_names"]
- left_arm_joints = []
- right_arm_joints = []
- for arm_side in arm_sides:
- if arm_kind == DexforceW1ArmKind.ANTHROPOMORPHIC:
- arm_type = (
- DexforceW1Type.LEFT_ARM1
- if arm_side == DexforceW1ArmSide.LEFT
- else DexforceW1Type.RIGHT_ARM1
- )
- else:
- arm_type = (
- DexforceW1Type.LEFT_ARM2
- if arm_side == DexforceW1ArmSide.LEFT
- else DexforceW1Type.RIGHT_ARM2
- )
- arm_version = (component_versions or {}).get(arm_type, DexforceW1Version.V021)
- arm_cfg = arm_manager.get_config(arm_kind, arm_side, arm_version)
- if arm_side == DexforceW1ArmSide.LEFT:
- left_arm_joints = arm_cfg["joint_names"]
- elif arm_side == DexforceW1ArmSide.RIGHT:
- right_arm_joints = arm_cfg["joint_names"]
-
- torso_joints = []
- head_joints = []
- left_hand_joints = []
- right_hand_joints = []
-
- if include_torso:
- torso_joints = torso_manager.get_config()["joint_names"]
- if include_head:
- head_joints = head_manager.get_config()["joint_names"]
+ hand_joints = {}
if include_hand:
- if DexforceW1ArmSide.LEFT in arm_sides:
- left_hand_brand = (hand_types or {}).get(
- DexforceW1ArmSide.LEFT, DexforceW1HandBrand.BRAINCO_HAND
- )
- left_hand_version = (hand_versions or {}).get(
- DexforceW1ArmSide.LEFT, DexforceW1Version.V021
- )
- left_hand_cfg = hand_manager.get_config(
- left_hand_brand, DexforceW1ArmSide.LEFT, left_hand_version
- )
- left_hand_joints = left_hand_cfg["joint_names"]
- if DexforceW1ArmSide.RIGHT in arm_sides:
- right_hand_brand = (hand_types or {}).get(
- DexforceW1ArmSide.RIGHT, DexforceW1HandBrand.BRAINCO_HAND
- )
- right_hand_version = (hand_versions or {}).get(
- DexforceW1ArmSide.RIGHT, DexforceW1Version.V021
+ for arm_side in DexforceW1ArmSide:
+ hand_brand = (hand_types or {}).get(
+ arm_side, DexforceW1HandBrand.BRAINCO_HAND
)
- right_hand_cfg = hand_manager.get_config(
- right_hand_brand, DexforceW1ArmSide.RIGHT, right_hand_version
+ hand_version = hand_versions.get(
+ arm_side, get_default_w1_hand_version(hand_brand)
)
- right_hand_joints = right_hand_cfg["joint_names"]
+ hand_joints[arm_side] = hand_manager.get_config(
+ hand_brand, arm_side, hand_version
+ )["joint_names"]
+ left_arm_joints = arm_joints.get(DexforceW1ArmSide.LEFT, [])
+ right_arm_joints = arm_joints.get(DexforceW1ArmSide.RIGHT, [])
control_parts = {}
-
- if torso_joints:
- control_parts["torso"] = torso_joints
- if head_joints:
- control_parts["head"] = head_joints
- if left_arm_joints:
- control_parts["left_arm"] = left_arm_joints
- if right_arm_joints:
- control_parts["right_arm"] = right_arm_joints
- if left_arm_joints and right_arm_joints:
- control_parts["dual_arm"] = left_arm_joints + right_arm_joints
- if left_hand_joints:
- control_parts["left_eef"] = left_hand_joints
- if right_hand_joints:
- control_parts["right_eef"] = right_hand_joints
-
- if torso_joints and head_joints and left_arm_joints and right_arm_joints:
- control_parts["full_body"] = (
- torso_joints + head_joints + left_arm_joints + right_arm_joints
- )
-
- from embodichain.lab.sim.robots.dexforce_w1.cfg import DexforceW1Cfg
-
- cfg = DexforceW1Cfg()
- cfg.arm_kind = arm_kind
- cfg.urdf_cfg = urdf_cfg
- cfg.control_parts = control_parts
-
- if solver_cfg is not None:
- cfg.solver_cfg = solver_cfg
-
- return cfg
+ control_parts["torso"] = torso_joints
+ control_parts["head"] = head_joints
+ control_parts["left_arm"] = left_arm_joints
+ control_parts["right_arm"] = right_arm_joints
+ control_parts["dual_arm"] = left_arm_joints + right_arm_joints
+ if DexforceW1ArmSide.LEFT in hand_joints:
+ control_parts["left_eef"] = hand_joints[DexforceW1ArmSide.LEFT]
+ if DexforceW1ArmSide.RIGHT in hand_joints:
+ control_parts["right_eef"] = hand_joints[DexforceW1ArmSide.RIGHT]
+ control_parts["full_body"] = (
+ torso_joints + head_joints + left_arm_joints + right_arm_joints
+ )
+ return control_parts
diff --git a/embodichain/lab/sim/utility/cfg_utils.py b/embodichain/lab/sim/utility/cfg_utils.py
index 12246e848..51ce7d028 100644
--- a/embodichain/lab/sim/utility/cfg_utils.py
+++ b/embodichain/lab/sim/utility/cfg_utils.py
@@ -84,7 +84,7 @@ def merge_robot_cfg(base_cfg: RobotCfg, override_cfg_dict: dict[str, any]) -> Ro
"""
# Only parse keys the base RobotCfg recognizes, so subclass-only variant
- # fields (version, arm_kind, ...) set by _build_defaults don't trigger
+ # fields (version, ...) set by _build_defaults don't trigger
# spurious "Key not found in RobotCfg" warnings from the base from_dict.
# NOTE: check RobotCfg.__dataclass_fields__ (not hasattr(base_cfg, k))
# because base_cfg is the subclass instance which has subclass-only fields,
@@ -104,9 +104,12 @@ def merge_robot_cfg(base_cfg: RobotCfg, override_cfg_dict: dict[str, any]) -> Ro
# 2. Part dict lacks "class_type" → attribute overrides for an
# existing solver part (e.g. {"tcp": ..., "stiffness": ...}).
provided_solver_cfg = override_cfg_dict.get("solver_cfg")
- if provided_solver_cfg and isinstance(provided_solver_cfg, dict):
+ if isinstance(provided_solver_cfg, dict):
if base_cfg.solver_cfg is None:
base_cfg.solver_cfg = {}
+ if not provided_solver_cfg:
+ base_cfg.solver_cfg = {}
+ continue
for part, item in provided_solver_cfg.items():
if isinstance(item, dict) and "class_type" in item:
# New or replacement solver part — use the deserialized
@@ -217,7 +220,7 @@ def merge_robot_cfg(base_cfg: RobotCfg, override_cfg_dict: dict[str, any]) -> Ro
)
else:
# Only apply keys the base RobotCfg.from_dict recognized.
- # Subclass-only variant fields (e.g. version, arm_kind) are not
+ # Subclass-only variant fields (e.g. version) are not
# present on a plain RobotCfg and are already set by _build_defaults;
# skip them instead of raising AttributeError.
if hasattr(robot_cfg, key):
diff --git a/embodichain/lab/sim/workspace/analyzer.py b/embodichain/lab/sim/workspace/analyzer.py
index e9f427698..c2decb5af 100644
--- a/embodichain/lab/sim/workspace/analyzer.py
+++ b/embodichain/lab/sim/workspace/analyzer.py
@@ -2102,17 +2102,34 @@ def _build_cache_key_metadata(self, num_samples: int) -> Dict[str, Any]:
robot_name = Path(fpath).stem
else:
robot_name = config_class.removesuffix("Cfg")
+
+ def serialize_parameter(value):
+ if isinstance(value, Enum):
+ return value.value
+ if isinstance(value, np.ndarray):
+ return value.tolist()
+ if isinstance(value, dict):
+ return {
+ serialize_parameter(key): serialize_parameter(item)
+ for key, item in value.items()
+ }
+ if isinstance(value, (list, tuple)):
+ return [serialize_parameter(item) for item in value]
+ return value
+
robot_parameters = {}
for parameter_name in (
"robot_type",
"version",
- "arm_kind",
"with_default_eef",
+ "hand_types",
+ "hand_versions",
+ "hand_attach_xposes",
):
if not hasattr(self.robot.cfg, parameter_name):
continue
value = getattr(self.robot.cfg, parameter_name)
- robot_parameters[parameter_name] = getattr(value, "value", value)
+ robot_parameters[parameter_name] = serialize_parameter(value)
robot_info = {
"name": robot_name,
"config_class": config_class,
diff --git a/examples/sim/demo/grasp_cup_to_caffe.py b/examples/sim/demo/grasp_cup_to_caffe.py
index 536549c76..a36f68925 100644
--- a/examples/sim/demo/grasp_cup_to_caffe.py
+++ b/examples/sim/demo/grasp_cup_to_caffe.py
@@ -33,6 +33,7 @@
from embodichain.lab.sim.cfg import (
RenderCfg,
LightCfg,
+ MarkerCfg,
JointDrivePropertiesCfg,
RigidObjectCfg,
RigidBodyAttributesCfg,
@@ -97,6 +98,7 @@ def create_robot(sim: SimulationManager) -> Robot:
cfg = DexforceW1Cfg.from_dict(
{
"uid": "dexforce_w1",
+ "version": "v025",
"init_pos": [0.4, -0.5, 0.0],
}
)
@@ -423,6 +425,7 @@ def main():
table = create_table(sim)
caffe = create_caffe(sim)
cup = create_cup(sim)
+
sim.update(step=1)
# apply random perturbation
diff --git a/examples/sim/gizmo/gizmo_scene.py b/examples/sim/gizmo/gizmo_scene.py
index 011f5814e..554517a83 100644
--- a/examples/sim/gizmo/gizmo_scene.py
+++ b/examples/sim/gizmo/gizmo_scene.py
@@ -72,9 +72,7 @@ def main():
sim.set_manual_update(False)
# Get DexForce W1 URDF path
- urdf_path = get_data_path(
- "DexforceW1V021_INDUSTRIAL_DH_PGC_GRIPPER_M/DexforceW1V021.urdf"
- )
+ urdf_path = get_data_path("DexforceW1V021/DexforceW1_v02_1.urdf")
# Create DexForce W1 robot
robot_cfg = RobotCfg(
diff --git a/examples/sim/robot/dexforce_w1.py b/examples/sim/robot/dexforce_w1.py
index bf97ed486..9a4e78383 100644
--- a/examples/sim/robot/dexforce_w1.py
+++ b/examples/sim/robot/dexforce_w1.py
@@ -43,7 +43,6 @@ def main(visualization: VisualizationCfg | None = None) -> None:
{
"uid": "dexforce_w1",
"version": "v021",
- "arm_kind": "anthropomorphic",
"with_default_eef": False,
"control_parts": {
"left_eef": ["LEFT_FINGER1_JOINT", "LEFT_FINGER2_JOINT"],
diff --git a/examples/sim/scene/scene_demo.py b/examples/sim/scene/scene_demo.py
index ad0c8e62b..aeb39a3a5 100644
--- a/examples/sim/scene/scene_demo.py
+++ b/examples/sim/scene/scene_demo.py
@@ -169,7 +169,6 @@ def main():
{
"uid": "dexforce_w1",
"version": "v021",
- "arm_kind": "anthropomorphic",
"init_pos": [-1, -0.5, 0],
"init_rot": [0, 0, 90],
}
diff --git a/examples/sim/workspace/analyze_cartesian_workspace.py b/examples/sim/workspace/analyze_cartesian_workspace.py
index e5f0efa49..fb9160067 100644
--- a/examples/sim/workspace/analyze_cartesian_workspace.py
+++ b/examples/sim/workspace/analyze_cartesian_workspace.py
@@ -97,7 +97,6 @@ def main() -> None:
{
"uid": "dexforce_w1",
"version": "v021",
- "arm_kind": "industrial",
"with_default_eef": False,
}
)
diff --git a/examples/sim/workspace/analyze_joint_workspace.py b/examples/sim/workspace/analyze_joint_workspace.py
index 2566fa17e..3695bdb79 100644
--- a/examples/sim/workspace/analyze_joint_workspace.py
+++ b/examples/sim/workspace/analyze_joint_workspace.py
@@ -92,7 +92,6 @@ def main() -> None:
{
"uid": "dexforce_w1",
"version": "v021",
- "arm_kind": "industrial",
# Workspace analysis only needs the arms. Excluding the grippers
# avoids loading unrelated joints and assets.
"with_default_eef": False,
diff --git a/examples/sim/workspace/analyze_plane_workspace.py b/examples/sim/workspace/analyze_plane_workspace.py
index 96e5e31cb..95e381e1e 100644
--- a/examples/sim/workspace/analyze_plane_workspace.py
+++ b/examples/sim/workspace/analyze_plane_workspace.py
@@ -97,7 +97,6 @@ def main() -> None:
{
"uid": "dexforce_w1",
"version": "v021",
- "arm_kind": "industrial",
"with_default_eef": False,
}
)
diff --git a/scripts/tutorials/gym/modular_env.py b/scripts/tutorials/gym/modular_env.py
index 37e186524..c882b258d 100644
--- a/scripts/tutorials/gym/modular_env.py
+++ b/scripts/tutorials/gym/modular_env.py
@@ -105,7 +105,6 @@ class ExampleCfg(EmbodiedEnvCfg):
{
"uid": "dexforce_w1",
"version": "v021",
- "arm_kind": "anthropomorphic",
"init_pos": [0.0, 0, 0.0],
}
)
diff --git a/tests/sim/objects/test_robot.py b/tests/sim/objects/test_robot.py
index b3a37837b..876781a60 100644
--- a/tests/sim/objects/test_robot.py
+++ b/tests/sim/objects/test_robot.py
@@ -63,7 +63,6 @@ def setup_simulation(cls, sim_device):
{
"uid": "dexforce_w1",
"version": "v021",
- "arm_kind": "anthropomorphic",
}
)
@@ -409,7 +408,6 @@ def test_configured_qpos_limits_sync_to_solver_after_initialization(self):
{
"uid": "dexforce_w1_solver_limit_sync",
"version": "v021",
- "arm_kind": "anthropomorphic",
"qpos_limits": {"LEFT_J[1-7]": configured_limits},
}
)
diff --git a/tests/sim/objects/test_robot_cfg.py b/tests/sim/objects/test_robot_cfg.py
index 6ccd89ec7..750588300 100644
--- a/tests/sim/objects/test_robot_cfg.py
+++ b/tests/sim/objects/test_robot_cfg.py
@@ -26,34 +26,305 @@
)
from embodichain.lab.sim.workspace import RobotWorkspaceCfg
from embodichain.lab.sim.robots.dexforce_w1 import DexforceW1Cfg
+from embodichain.lab.sim.robots.dexforce_w1.params import W1ArmKineParams
from embodichain.lab.sim.robots.dexforce_w1.types import (
- DexforceW1ArmKind,
+ DexforceW1ArmSide,
+ DexforceW1HandBrand,
+ DexforceW1HandVersion,
+ DexforceW1Type,
DexforceW1Version,
)
+from embodichain.lab.sim.robots.dexforce_w1.hand_specs import (
+ get_default_w1_hand_version,
+ get_w1_hand_spec,
+)
+from embodichain.lab.sim.robots.dexforce_w1.specs import get_w1_version_spec
from embodichain.lab.sim.solvers import SRSSolverCfg
from embodichain.utils import configclass
from embodichain.lab.sim.utility.cfg_utils import merge_robot_cfg
+def _mock_w1_asset_paths(monkeypatch, tmp_path):
+ import embodichain.lab.sim.cfg as sim_cfg
+ from embodichain.lab.sim.robots.dexforce_w1 import utils as w1_utils
+
+ def resolve(path):
+ resolved = tmp_path / path
+ resolved.parent.mkdir(parents=True, exist_ok=True)
+ resolved.write_text("")
+ return str(resolved)
+
+ monkeypatch.setattr(w1_utils, "get_data_path", resolve)
+ monkeypatch.setattr(sim_cfg, "get_data_path", resolve)
+
+
def test_dexforce_w1_roundtrip():
- cfg = DexforceW1Cfg.from_dict(
- {"uid": "dexforce_w1", "version": "v021", "arm_kind": "anthropomorphic"}
- )
+ cfg = DexforceW1Cfg.from_dict({"uid": "dexforce_w1", "version": "v021"})
d = cfg.to_dict()
assert d["uid"] == "dexforce_w1"
- assert d["arm_kind"] == "anthropomorphic"
cfg2 = DexforceW1Cfg.from_dict(d)
assert cfg2.uid == "dexforce_w1"
- assert cfg2.arm_kind == DexforceW1ArmKind.ANTHROPOMORPHIC
assert cfg2.version == DexforceW1Version.V021
def test_dexforce_w1_solver_cfg_is_srs_and_set_once():
- cfg = DexforceW1Cfg.from_dict({"arm_kind": "industrial"})
+ cfg = DexforceW1Cfg.from_dict({})
assert isinstance(cfg.solver_cfg["left_arm"], SRSSolverCfg)
assert isinstance(cfg.solver_cfg["right_arm"], SRSSolverCfg)
+def test_dexforce_w1_rejects_unknown_fields():
+ with pytest.raises(ValueError, match="Unknown DexforceW1 configuration fields"):
+ DexforceW1Cfg.from_dict({"unsupported_variant": "value"})
+
+
+@pytest.mark.parametrize(
+ "removed_field",
+ [
+ "arm_sides",
+ "include_chassis",
+ "include_torso",
+ "include_head",
+ "include_eyes",
+ "include_wrist_cameras",
+ "component_versions",
+ ],
+)
+def test_dexforce_w1_rejects_removed_builder_fields(removed_field):
+ with pytest.raises(ValueError, match="Unknown DexforceW1 configuration fields"):
+ DexforceW1Cfg.from_dict({removed_field: None})
+
+
+def test_w1_v025_eef_offset_applies_to_attach_and_tcp():
+ v021 = get_w1_version_spec(DexforceW1Version.V021)
+ v025 = get_w1_version_spec(DexforceW1Version.V025)
+ expected_offset = np.eye(4)
+ expected_offset[2, 3] = 0.012
+ hand_spec = get_w1_hand_spec(
+ DexforceW1HandBrand.BRAINCO_HAND, DexforceW1HandVersion.V021
+ )
+
+ for arm_side in DexforceW1ArmSide:
+ np.testing.assert_allclose(v021.eef_attach_xpos(arm_side), np.eye(4))
+ np.testing.assert_allclose(v025.eef_attach_xpos(arm_side), expected_offset)
+ np.testing.assert_allclose(
+ v025.tcp(arm_side),
+ expected_offset @ v021.tcp(arm_side),
+ )
+ np.testing.assert_allclose(
+ v025.compose_eef_attach_xpos(
+ arm_side,
+ hand_spec.for_side(arm_side).attach_xpos,
+ ),
+ expected_offset @ np.asarray(hand_spec.for_side(arm_side).attach_xpos),
+ )
+
+
+def test_w1_v025_eef_offset_composes_with_custom_attach():
+ spec = get_w1_version_spec(DexforceW1Version.V025)
+ custom_attach = np.eye(4)
+ custom_attach[:3, 3] = [0.01, -0.02, 0.03]
+
+ result = spec.compose_eef_attach_xpos(DexforceW1ArmSide.RIGHT, custom_attach)
+
+ np.testing.assert_allclose(result[:3, 3], [0.01, -0.02, 0.042])
+ np.testing.assert_allclose(result[:3, :3], np.eye(3))
+
+
+def test_w1_v022_version_spec_is_registered():
+ spec = get_w1_version_spec("v022")
+
+ assert spec.version == DexforceW1Version.V022
+ assert spec.assembly_name == "DexforceW1V022"
+ assert spec.full_robot_urdf() == "DexforceW1V022/w1/robot.urdf"
+ assert (
+ spec.component_urdf(DexforceW1Type.LEFT_ARM)
+ == "DexforceW1V022/w1/left_arm.urdf"
+ )
+ assert (
+ spec.component_urdf(DexforceW1Type.RIGHT_ARM)
+ == "DexforceW1V022/w1/right_arm.urdf"
+ )
+
+
+def test_w1_v022_provisional_eef_baseline_is_composed_consistently():
+ v021 = get_w1_version_spec(DexforceW1Version.V021)
+ v022 = get_w1_version_spec(DexforceW1Version.V022)
+
+ for arm_side in DexforceW1ArmSide:
+ np.testing.assert_allclose(v022.eef_attach_xpos(arm_side), np.eye(4))
+ np.testing.assert_allclose(v022.tcp(arm_side), v021.tcp(arm_side))
+
+
+def test_w1_v022_cfg_uses_registered_asset_paths(tmp_path, monkeypatch):
+ import embodichain.lab.sim.cfg as sim_cfg
+ from embodichain.lab.sim.robots.dexforce_w1 import utils as w1_utils
+
+ registered_paths = []
+
+ def resolve_registered_path(path):
+ registered_paths.append(path)
+ resolved = tmp_path / path
+ resolved.parent.mkdir(parents=True, exist_ok=True)
+ resolved.write_text("")
+ return str(resolved)
+
+ monkeypatch.setattr(w1_utils, "get_data_path", resolve_registered_path)
+ monkeypatch.setattr(sim_cfg, "get_data_path", resolve_registered_path)
+ cfg = DexforceW1Cfg.from_dict(
+ {
+ "uid": "dexforce_w1_v022",
+ "version": "v022",
+ "with_default_eef": False,
+ }
+ )
+
+ assert cfg.version == DexforceW1Version.V022
+ assert cfg.uid == "dexforce_w1_v022"
+ assert cfg.urdf_cfg.fname == "DexforceW1V022"
+ assert "DexforceW1V022/w1/left_arm.urdf" in registered_paths
+ assert "DexforceW1V022/w1/right_arm.urdf" in registered_paths
+
+
+def test_w1_cfg_builds_complete_dual_arm_robot(monkeypatch, tmp_path):
+ _mock_w1_asset_paths(monkeypatch, tmp_path)
+ cfg = DexforceW1Cfg.from_dict({"version": "v021", "with_default_eef": False})
+
+ assert set(cfg.urdf_cfg.components) == {
+ "chassis",
+ "torso",
+ "head",
+ "left_arm",
+ "right_arm",
+ }
+ assert set(cfg.control_parts) == {
+ "torso",
+ "head",
+ "left_arm",
+ "right_arm",
+ "dual_arm",
+ "full_body",
+ }
+ assert set(cfg.solver_cfg) == {"left_arm", "right_arm"}
+
+
+def test_w1_hand_version_is_independent_of_robot_version(monkeypatch, tmp_path):
+ _mock_w1_asset_paths(monkeypatch, tmp_path)
+ from embodichain.lab.sim.robots.dexforce_w1 import utils as w1_utils
+
+ selected_versions = []
+ original_get_urdf = w1_utils.hand_manager.get_urdf
+
+ def capture_version(brand, side, version):
+ selected_versions.append((side, version))
+ return original_get_urdf(brand, side, version)
+
+ monkeypatch.setattr(w1_utils.hand_manager, "get_urdf", capture_version)
+ DexforceW1Cfg.from_dict(
+ {
+ "version": "v025",
+ }
+ )
+
+ assert selected_versions
+ assert all(
+ version == DexforceW1HandVersion.V021 for _, version in selected_versions
+ )
+
+
+@pytest.mark.parametrize("brand", list(DexforceW1HandBrand))
+def test_w1_hand_brand_defaults_are_explicitly_registered(brand):
+ version = get_default_w1_hand_version(brand)
+
+ assert version == DexforceW1HandVersion.V021
+ assert get_w1_hand_spec(brand, version).brand == brand
+
+
+def test_w1_hand_version_roundtrip(monkeypatch, tmp_path):
+ _mock_w1_asset_paths(monkeypatch, tmp_path)
+ cfg = DexforceW1Cfg.from_dict(
+ {"version": "v025", "hand_versions": {"left": "v021"}}
+ )
+
+ data = cfg.to_dict()
+ restored = DexforceW1Cfg.from_dict(data)
+
+ assert data["hand_versions"] == {"left": "v021", "right": "v021"}
+ assert restored.hand_versions == {
+ DexforceW1ArmSide.LEFT: DexforceW1HandVersion.V021,
+ DexforceW1ArmSide.RIGHT: DexforceW1HandVersion.V021,
+ }
+
+
+def test_w1_rejects_unregistered_hand_version():
+ with pytest.raises(ValueError, match="Invalid Dexforce W1 hand version"):
+ DexforceW1Cfg.from_dict({"hand_versions": {"left": "v025"}})
+
+
+def test_w1_rejects_robot_version_as_hand_version():
+ with pytest.raises(ValueError, match="Invalid Dexforce W1 hand version"):
+ DexforceW1Cfg.from_dict({"hand_versions": {"left": DexforceW1Version.V021}})
+
+
+@pytest.mark.parametrize("version", ["v025", "V025", DexforceW1Version.V025])
+def test_w1_kine_params_accept_consistent_version_forms(version):
+ params = W1ArmKineParams.from_dict({"arm_side": "left", "version": version})
+
+ assert params.arm_side == DexforceW1ArmSide.LEFT
+ assert params.version == DexforceW1Version.V025
+
+
+def test_w1_version_spec_mappings_are_immutable():
+ spec = get_w1_version_spec("v025")
+
+ with pytest.raises(TypeError):
+ spec.solver_tcp[DexforceW1ArmSide.LEFT] = np.eye(4)
+ with pytest.raises(TypeError):
+ spec.component_urdfs[DexforceW1Type.LEFT_ARM] = "other.urdf"
+
+
+def test_w1_v025_custom_eef_and_tcp_roundtrip(monkeypatch, tmp_path):
+ _mock_w1_asset_paths(monkeypatch, tmp_path)
+ baseline = DexforceW1Cfg.from_dict({"version": "v025"})
+ raw_attach = np.eye(4)
+ raw_attach[:3, 3] = [0.01, -0.02, 0.03]
+ raw_tcp = np.eye(4)
+ raw_tcp[:3, 3] = [0.04, 0.05, 0.06]
+ cfg = DexforceW1Cfg.from_dict(
+ {
+ "version": "v025",
+ "urdf_cfg": {
+ "components": {
+ "left_hand": {
+ "urdf_path": baseline.urdf_cfg.components["left_hand"][
+ "urdf_path"
+ ],
+ "transform": raw_attach.tolist(),
+ }
+ }
+ },
+ "solver_cfg": {"left_arm": {"tcp": raw_tcp.tolist()}},
+ }
+ )
+
+ expected_attach = get_w1_version_spec("v025").compose_eef_attach_xpos(
+ DexforceW1ArmSide.LEFT, raw_attach
+ )
+ expected_tcp = get_w1_version_spec("v025").compose_eef_attach_xpos(
+ DexforceW1ArmSide.LEFT, raw_tcp
+ )
+ restored = DexforceW1Cfg.from_dict(cfg.to_dict())
+
+ np.testing.assert_allclose(
+ cfg.urdf_cfg.components["left_hand"]["transform"], expected_attach
+ )
+ np.testing.assert_allclose(cfg.solver_cfg["left_arm"].tcp, expected_tcp)
+ np.testing.assert_allclose(
+ restored.urdf_cfg.components["left_hand"]["transform"], expected_attach
+ )
+ np.testing.assert_allclose(restored.solver_cfg["left_arm"].tcp, expected_tcp)
+
+
class _RoundTripVariant(enum.Enum):
A = "a"
B = "b"
@@ -162,7 +433,7 @@ def _dof_of_pk_chain(chain) -> int:
def test_dexforce_w1_pk_dof_matches_control_parts():
pytest.importorskip("pytorch_kinematics")
- cfg = DexforceW1Cfg.from_dict({"arm_kind": "anthropomorphic"})
+ cfg = DexforceW1Cfg.from_dict({})
try:
chains = cfg.build_pk_serial_chain()
except Exception as exc:
diff --git a/tests/sim/solvers/test_srs_solver.py b/tests/sim/solvers/test_srs_solver.py
index 83cd4a61b..540814987 100644
--- a/tests/sim/solvers/test_srs_solver.py
+++ b/tests/sim/solvers/test_srs_solver.py
@@ -31,7 +31,6 @@
from embodichain.lab.sim.solvers.srs_solver import SRSSolver, SRSSolverCfg
from embodichain.lab.sim.robots.dexforce_w1.types import (
DexforceW1ArmSide,
- DexforceW1ArmKind,
DexforceW1Version,
)
from embodichain.lab.sim.robots.dexforce_w1.params import (
@@ -44,23 +43,17 @@ class BaseSolverTest:
def get_arm_config(self):
return [
- (DexforceW1ArmSide.LEFT, DexforceW1ArmKind.ANTHROPOMORPHIC, "left_arm"),
- (DexforceW1ArmSide.RIGHT, DexforceW1ArmKind.ANTHROPOMORPHIC, "right_arm"),
- (DexforceW1ArmSide.LEFT, DexforceW1ArmKind.INDUSTRIAL, "left_arm"),
- (DexforceW1ArmSide.RIGHT, DexforceW1ArmKind.INDUSTRIAL, "right_arm"),
+ (DexforceW1ArmSide.LEFT, "left_arm"),
+ (DexforceW1ArmSide.RIGHT, "right_arm"),
]
def setup_solver(self, solver_type: str, device: str = "cpu"):
- for arm_side, arm_kind, arm_name in self.get_arm_config():
+ for arm_side, arm_name in self.get_arm_config():
arm_params = W1ArmKineParams(
arm_side=arm_side,
- arm_kind=arm_kind,
version=DexforceW1Version.V021,
)
- if arm_kind == DexforceW1ArmKind.ANTHROPOMORPHIC:
- urdf = get_data_path("DexforceW1V021/DexforceW1_v02_1.urdf")
- else:
- urdf = get_data_path("DexforceW1V021/DexforceW1_v02_2.urdf")
+ urdf = get_data_path("DexforceW1V021/DexforceW1_v02_1.urdf")
cfg = SRSSolverCfg()
cfg.joint_names = [
@@ -83,24 +76,18 @@ def setup_solver(self, solver_type: str, device: str = "cpu"):
cfg.link_lengths = arm_params.link_lengths
cfg.rotation_directions = arm_params.rotation_directions
- solver_key = f"{arm_name}_{arm_kind.name}"
- self.solver[solver_key] = SRSSolver(cfg=cfg, num_envs=1, device=device)
+ self.solver[arm_name] = SRSSolver(cfg=cfg, num_envs=1, device=device)
@pytest.mark.parametrize(
- "arm_side, arm_kind, arm_name",
+ "arm_side, arm_name",
[
- (DexforceW1ArmSide.LEFT, DexforceW1ArmKind.ANTHROPOMORPHIC, "left_arm"),
- (DexforceW1ArmSide.RIGHT, DexforceW1ArmKind.ANTHROPOMORPHIC, "right_arm"),
- (DexforceW1ArmSide.LEFT, DexforceW1ArmKind.INDUSTRIAL, "left_arm"),
- (DexforceW1ArmSide.RIGHT, DexforceW1ArmKind.INDUSTRIAL, "right_arm"),
+ (DexforceW1ArmSide.LEFT, "left_arm"),
+ (DexforceW1ArmSide.RIGHT, "right_arm"),
],
)
- def test_ik(
- self, arm_side: DexforceW1ArmSide, arm_kind: DexforceW1ArmKind, arm_name: str
- ):
+ def test_ik(self, arm_side: DexforceW1ArmSide, arm_name: str):
# Test inverse kinematics (IK) with a 1x4x4 homogeneous matrix pose and a joint_seed
- solver_key = f"{arm_name}_{arm_kind.name}"
- device = self.solver[solver_key].device
+ device = self.solver[arm_name].device
qpos_fk = torch.tensor(
[[0.0, 0.0, 0.0, -np.pi / 4, 0.0, 0.0, 0.0]],
@@ -108,15 +95,15 @@ def test_ik(
device=device,
)
- fk_xpos = self.solver[solver_key].get_fk(qpos=qpos_fk)
+ fk_xpos = self.solver[arm_name].get_fk(qpos=qpos_fk)
- _, ik_qpos = self.solver[solver_key].get_ik(fk_xpos, return_all_solutions=False)
+ _, ik_qpos = self.solver[arm_name].get_ik(fk_xpos, return_all_solutions=False)
- ik_xpos = self.solver[solver_key].get_fk(qpos=ik_qpos[:, 0, :])
+ ik_xpos = self.solver[arm_name].get_fk(qpos=ik_qpos[:, 0, :])
assert torch.allclose(
fk_xpos, ik_xpos, atol=1e-3, rtol=1e-3
- ), f"FK and IK results do not match for {solver_key}"
+ ), f"FK and IK results do not match for {arm_name}"
def test_update_with_robot_limit_intersects_existing_solver_limits(self):
"""Test robot limit sync only tightens solver limits and never widens them."""
@@ -212,12 +199,10 @@ def setup_simulation(self, solver_type: str, device: str = "cpu"):
w1_left_arm_params = W1ArmKineParams(
arm_side=DexforceW1ArmSide.LEFT,
- arm_kind=DexforceW1ArmKind.ANTHROPOMORPHIC,
version=DexforceW1Version.V021,
)
w1_right_arm_params = W1ArmKineParams(
arm_side=DexforceW1ArmSide.RIGHT,
- arm_kind=DexforceW1ArmKind.ANTHROPOMORPHIC,
version=DexforceW1Version.V021,
)