Duplication & Reuse Watch
Part of the OpenRAL public-symbol inventory. Hand-curated;
(LNN)markers are refreshed bytools/refresh_methods_linenos.py.
This is the user-facing deliverable for the goal of "ensure there are no duplication or redundancy of methods". Each item is something a future contributor should look at before adding similar code.
Confirmed redundancy candidates
-
Sensor
_spec()private factory helpers — retired. The seven-way_spec()duplication this item originally tracked is gone: theimu/livox/ouster/hokuyo/slamtecmodules no longer exist underpython/sensors/src/openral_sensors/. The surviving spec factories (e.g.force_torque.robotiq_ft300s_spec) are one-per-file public API, not duplication. -
Three parallel registries with the same lookup-by-string pattern:
python/rskill/src/openral_rskill/loader.py:114—rSkill+InstalledRSkillEntryJSON file registry.python/sensors/src/openral_sensors/catalog.py:85—SensorCatalogin-memory dict.python/sim/src/openral_sim/registry.py:43—_Registry[T]decorator-driven dict.
These are different in lifecycle (file-backed vs. in-memory) and
value type (skill vs. sensor entry vs. factory), so deep
consolidation is not warranted. Worth aligning method names
though — SensorCatalog.list_ids(), _Registry.names(), and
rSkill.list_installed() all answer the same question with
different verbs. A future ADR could standardise on one verb.
-
VLA adapter boundary helpers — resolved.
resolve_device,resolve_rskill_repo_id,run_inference,to_numpy_action,parse_hf_file_uri, andmaterialize_processor_dirnow live inpython/rskill/src/openral_rskill/_vla_core.py. All five eval adapters (smolvla,pi05,xvla,act,diffusion) and the skill-sideChunkedExecutorroute through it. Thediffusion/xvla/pi05adapters now go through the smallpython/sim/src/openral_sim/policies/_processors.py::resolve_processor_dirhelper, which delegates tomaterialize_processor_dirwhen the weights URI resolves to a manifest that declares aprocessorsblock, falling back tosnapshot_downloadfor legacyhf://shapes — the three sister TODOs on the audit closed 2026-05-18. When adding a new VLA family, do NOT re-implement device or rSkill resolution; do NOT wrappolicy.select_actionin your owninference_spanblock — callrun_inferenceso the OTel span fires uniformly. For loading the lerobotPolicyProcessorPipeline, call_processors.resolve_processor_dir(sim-layer) ormaterialize_processor_dir(manifest)(skill-layer) — do NOT callsnapshot_downloaddirectly. (The two remaining directsnapshot_downloadcalls —policies/diffusion.pynorm-stat loading and the exemptedact.pyadapter — are weight/ norm-stat fetches, not processor-dir resolution; they are not regressions of this item.) -
SmolVLA skill-side
SmolVLAAdaptervs eval-side_SmolVLAAdapter— not a duplication target. The two have incompatible input contracts on purpose: the skill takesWorldStateand emits anActioninside the ROS2 lifecycle (Layer 3, S1 runtime); the eval adapter takes a dictObservationand emits a flat numpy array (Layer 8, sim driver). Collapsing them would force either ceremonial Pydantic wrapping in the sim hot loop or wideningSkill.step()to accept dicts (breaks §6.1). With_vla_coreabsorbing the cross-cutting seams, residual overlap (checkpoint load + processor factory, ~30 LOC each side) is below the abstraction-cost threshold. Keep them separate. -
_build_libero_scene/_build_metaworld_scene/_build_mock_sceneinpython/sim/src/openral_sim/{policies,backends}/{libero,metaworld,mock}.pyshare the same structure: lazy-import a backend module, instantiate a_*Simwrapper, return it. Already correctly DRY through theSCENES.register(...)decorator pattern; do not consolidate further. -
Policy load-phase heartbeat — resolved. The original threaded heartbeat (
pi05._heartbeat) lived inline in the pi05 adapter and hard-coded thepi05_*event prefix, the daemon thread plumbing, and the GPU memory probe. It now lives once inpython/rskill/src/openral_rskill/_diagnostics.py::phase_timer(name, *, prefix, gpu_mb, **fields)and the pi05 / smolvla adapters apply it through one-line per-adapter shortcuts (_pi05_phase/_smolvla_phase). When adding a new VLA family, do NOT roll your own heartbeat thread — wrap every load phase (imports/from_pretrained/to_device/processor_dir/make_processors/ family-specific quant or compile phases) with a thin_<family>_phaseshortcut sotools/profile_policy_load.pyandopenral dashboardsee the same event shape across all adapters. -
BEHAVIOR R1Pro wire constants — resolved in-process, mirrored cross-venv. The evaluator's raw observation keys and the 61-D/23-D contract widths were hand-copied in three importable modules; they now live once in
python/sim/src/openral_sim/_behavior_wire.py(STATE_KEY/STATE_DIM/ACTION_DIM/CAMERA_SENSORS/CAMERA_RGB_KEYS/explicit_port), imported by the scene backend, thebehavior_grootpolicy adapter, andopenral_cli.behavior. The sidecar scripts undertools/behavior_*_sidecar.pyrun in isolated venvs that cannot importopenral_sim— their copies are a deliberate wire-contract MIRROR: update them in lockstep with_behavior_wire.
Already correctly DRY (do not flag)
-
SimSensorBridge — the single source for RGB camera publishing + MuJoCo viewer under
deploy sim. All manifest-driven arms route throughopenral_hal.sim_sensor_bridge.SimSensorBridgevia_ManifestHALLifecycleNode. Thepanda_mobilepackage retains its own wiring until the planned dedup refactor lands. Do NOT add per-arm camera or viewer timers in lifecycle subclasses; extendSimSensorBridgeinstead. -
HAL adapters (sim) —
FrankaPandaHAL,UR5eHAL,UR10eHAL,SO100MujocoHAL,Rizon4MujocoHAL,G1MujocoHAL,H1MujocoHAL,AlohaMujocoHAL,OpenArmMujocoHALall extendMujocoArmHAL. Following the bimanual amendment and the 2026-05 cleanup that collapsed each subclass__init__into a single forward toMujocoArmHAL._init_from_description(<DESCRIPTION>, …)), each subclass is now one line of meaningful code — the typed__init__(*, mjcf_path, settle_steps, gravity_enabled, staleness_limit_s)signature is kept so IDEs surface the four user-tunable knobs, but every per-robot constant (MJCF URI, joint→qpos/actuator maps, gripper config, keyframe/seed-ctrl flags) lives entirely in<ROBOT>_DESCRIPTION.sim(SimDescription/SimGripperDescription). The seam isMujocoArmHAL._init_from_description(instance method) → which delegates toMujocoArmHAL._sim_kwargs_for(static method, returning a_MujocoArmInitKwargsTypedDict so the**kwargsunpack into__init__is typed-clean undermypy --strictwith no per-subclass# type: ignore). Per-robot_<robot>_mjcf_pathhelpers were also retired in the same cleanup — every MJCF ref resolves through the centralopenral_core.assets.resolve_assetgrammar (rd:/gym_aloha:/openarm:/menagerie:/file:schemes). New MuJoCo HALs — single-arm, floating-base humanoid, or bimanual — should declare anassets.mjcfref (plus an optionalsim:joint-wiring block) inrobots/<id>/robot.yamland callMujocoArmHAL.from_description(desc). No per-robot Python file is required at all; the existing classes only exist so the explicithal.simstrings ("openral_hal.<robot>:<Class>") some manifests pin keep resolving.H1MujocoHALretains a real subclass body only for its_per_step_updatetorque hook (default no-op inMujocoArmHAL, overridden by H1 to recomputetau = kp*(target-q) - kv*dqevery step) — that PD behavior is H1-specific cerebellar substitute, not arm-data, and stays in code. - Policy adapter loader seams — resolved. The 2026-05 cleanup
pulled three parallel copies of
_load_manifest_for_spec(one each inpolicies/smolvla.py,policies/rldx.py,policies/pi05.py) and one copy of the lerobot lazy-import +ROSConfigErrorinstall hint into a newpython/sim/src/openral_sim/policies/_policy_loading.py—load_manifest_for_spec(spec)andlazy_import_lerobot(adapter_name, *, install_hint=...). Similarly, the four dtype helpers that used to live inpolicies/pi05.py(_manifest_dtype,_normalise_manifest_dtype,_torch_dtype_for,_default_dtype) were lifted intopython/sim/src/openral_sim/_quantization.pyas publicmanifest_dtype,normalise_manifest_dtype,torch_dtype_for,default_dtype_for_device. Theact.pyadapter still carries its own_load_manifest_for_specbecause the rest of its load path is structured around a snapshot of the policy weights; if a fifth adapter ever needs the same shape, route it through_policy_loading.load_manifest_for_spec. - Humanoid contract validators vs useful humanoid sims —
H1MujocoHALand G1's default joint-position path are contract validators. Both robots' floating bases fall without an S0 cerebellar balance controller (CLAUDE.md §6.2); their joint-convergence tests run withgravity_enabled=False. This is the same situation a future GR1 HAL twin (currently still deferred — see below) will be in until the C++ S0 cerebellum lands. Do NOT promote these HALs to "useful humanoid sim" by bolting Python balance heuristics onto them — that path crosses the S0 layer boundary §6.1 reserves for C++. The one sanctioned sanctioned G1 sim exceptions are ADR-0087's kinematic-glide base: the free joint is pinned upright each step and BODY_TWIST Euler-integrates the planar pose — a kinematic navigation stand-in with zero dynamics control, NOT a balance controller, and ADR-0089's exact upstream MuJoCo Playground ONNX policy + matching MJCF. The latter is selected explicitly bywalking_enabled=True, runs only in the sim HAL, and is not a Python balance heuristic or a real-hardware S0. Note thatH1MujocoHAL's software PD position loop is not a balance controller — it's a per-joint Kp/Kd that converts the H1 menagerie's torque actuators into the position-target contract every otherMujocoArmHALsubclass implements, and mirrors whatunitree_sdk2does on real hardware. - Deliberate digital-twin gaps —
SawyerandGR1intentionally ship without a MuJoCo HAL twin: - Sawyer: Rethink Robotics is defunct; no real Sawyer hardware
will ever be plugged in. Sawyer remains only as a MetaWorld
VLA-eval robot (no
SawyerHAL, onlySawyerRealHALskeleton). Twin would be busywork. - GR1: still no Python HAL twin — Fourier GR1 is one humanoid
family along with Unitree G1, and once the C++ S0 cerebellum
lands (M2) it's the natural second consumer of the humanoid
HAL pattern that
G1MujocoHALset up. Currently only exists as anopenral_simrollout robot. These are documented absences, not missing work; do not add HAL twins for them speculatively. - Real-HW manifest derivation — every real-HW adapter publishes a
*_REAL_DESCRIPTIONconstant derived from a sim-side baseline viaopenral_hal._real_description.make_real_description(base, sdk_kind=...). The helper centralises themodel_copy+sdk_kindoverride pattern (thehalentrypoints are shared), so kinematics + safety envelope + capabilities + HAL entrypoints never drift between the sim and real-HW siblings of the same robot. New real-HW adapters MUST go through this helper rather than re-typing the wholeRobotDescriptionconstructor. The UR real-HW module (ur_real.py) uses this helper to deriveUR5e_REAL_DESCRIPTION/UR10e_REAL_DESCRIPTIONfromUR{5,10}e_DESCRIPTION. - HAL adapters (real-HW) — three shapes coexist on purpose:
FrankaPandaRealHALandSawyerRealHALcomposeRosControlHAL(delegating wrapper) and add robot-specific structlog metadata + a vendor-specific recovery / halt topic publish inestop(). This is the intended pattern for any real-HW arm whose vendor stack exposes a singleros2_controljoint trajectory controller plus a separate recovery topic.UR5eRealHAL/UR10eRealHALsubclass a private_URRealHAL(RosControlHAL)base inur_real.pyto share theur_robot_drivercontroller / topic / deadman defaults. Pick subclassing when two adapters share enough defaults to warrant a base; pick composition when each adapter has distinct recovery / metadata semantics. Any future UR variant (UR3e, UR16e, …) is a one-line subclass that swaps theRobotDescription.AlohaHALinlines the publish/state machinery rather than wrappingRosControlHALbecause it splits a single 14-D action across four controllers (two arms + two grippers) — a contract that doesn't matchRosControlHAL's single-controller assumption. Adding a sixth composed-real-HW adapter is the trigger to hoistRosControlHAL-wrapping logic into a_RealHALMixin; adding a second multi-controller adapter is the trigger to hoist AlohaHAL's fan-out into aMultiRosControlHAL.- HIL transport bridges (real-HW HALs) — the single-controller
RosControlHILTransport(tests/hil/_ros_control_transport.py) is the source of truth for the trajectory wiring;AlohaHILTransport(tests/hil/_aloha_ros_transport.py) reuses the module-private_make_trajectory_publisherhelper rather than duplicating theJointTrajectory+ QoS setup four times. Both bridges share the joint-state caching shape (_latestdict,state()projection overjoint_names,wait_for_first_statehelper). Adding a third HIL bridge variant is the trigger to extract the shared subscriber half into a_JointStateCachemixin. - Kernel-twin sim tests — the four
tests/sim/safety/test_kernel_with_<robot>_*.pyfiles (so100_digital_twin,openarm_twin,rizon4_twin,h1_humanoid_twin) used to each open-code the subprocess + lifecycle - ROS-graph envelope around the C++ safety kernel. After the 2026-05
cleanup, all four route through
tests/sim/safety/_kernel_subprocess.py::{start_kernel, activate_kernel_node, build_kernel_envelope, terminate_kernel}and only declare their embodiment-specific joint-name lists + per-test action / state vectors. Adding a fifth robot's kernel-twin test means one new short test file that calls the same four helpers — do NOT re-roll the lifecycle ceremony. - rSkillBase subclasses —
GpuPassthroughSkill,SmolVLAAdapter,SO100SmolVLASkillall override the same five_*_implhooks. The duplicated method names are the contract fromSkillABC; this is inheritance, not redundancy.GpuPassthroughSkill(M8 PR I/10) is the canonical "this skill provably runs on GPU" reference — its_step_implis the right starting point when prototyping a torch.cuda-based Skill that consumes a CPUSensorFrame.data: bytesand needs to be explicit about device placement (raises on missing CUDA rather than silently falling back). - Runtime backends —
NullRuntime,PyTorchRuntime,ONNXRuntime(plusTensorRTRuntimein the privateopenral-pro-trtpackage) all implement theRuntimeProtocol surface (load/infer/quantize/warmup/unload). Same situation as Skill. backends/so100_robosuite/—_So100Liftextendsrobosuite.environments.manipulation.lift.Liftrather than reimplementing the arena / reward / observable / placement scaffolding, and the controller config is the shippedparts/osc_position.jsonwith three knobs overridden (output_max,kp,input_ref_frame) — NOT a custom controller class. The scripted policy is correspondingly tiny (~150 lines, just Cartesian deltas) because OSC owns the IK. The next new robosuite-integrated robot should follow the same pattern: register the robot model + gripper in robosuite's factories, build the env via robosuite's stock manipulation subclasses, pick a stock part controller (osc_position/osc_pose/joint_position) and tune only the gain / output ranges — do not write a JOINT_POSITION + custom-IK stack like the earlyso100_robosuitedrafts did.
Watch list (not yet a problem, but worth tracking)
_validate_action()appears in bothMujocoArmHAL(L296) andRosControlHAL(L250). They validate different invariants today (MuJoCo:joint_targetsrank; ros2_control: control mode). If a third HAL grows a third_validate_action, lift the common parts into a free function inopenral_hal.protocol._require_connected()appears inMujocoArmHAL(L289),SO100FollowerHAL(L386),RosControlHAL(L243), andAlohaHAL(L426). Four is over the threshold — the next HAL adapter that adds a fifth_require_connectedis the trigger to hoist this into a base mixin (openral_hal._lifecycle.RequireConnectedMixin).FrankaPandaRealHAL/SawyerRealHALdeliberately delegate the check to their innerRosControlHALrather than duplicating it.from_yaml(cls, path)classmethods — resolved. The pattern had grown to six copies (RobotDescription,RSkillManifest,DeployScene,SimScene,BenchmarkScene); all now shareopenral_core.schemas._load_yaml_model(cls, path), and the byte-identicalSimScene/BenchmarkSceneoverrides were deleted (they inheritDeployScene.from_yaml, which returnsSelf). A newfrom_yamlon a schemas-module model should be a one-line delegation to_load_yaml_model.- LeRobot SO-ARM unit + cadence conversions in the native MuJoCo
scenes — consolidated into
openral_sim/backends/_so_arm_units.py(steps_per_control_period,lerobot_action_to_radians,radians_to_lerobot_state):so101_eraserandso101_boxeach carried their own copy of the degrees-mode affine and physics-stepping loop, and the copies drifted —so101_boxshipped a single-mj_step-per-action cadence and a gripper-as-degrees mapping thatso101_eraserhad already fixed. A new raw-MuJoCo scene that accepts LeRobot-convention actions must route through this module, not re-derive the conversions. (tabletop_pushkeeps its own_joint_scalesaffine +settle_stepsknob on purpose: it is robot-agnostic, so it cannot assume the SO-ARM "last channel is a [0, 100] gripper" convention.) - Rotation/quaternion math scattered across packages — the yaw
family is now consolidated into
openral_core.geometry(yaw_to_quat_xyzw,yaw_to_quat_wxyz,quat_xyzw_to_yaw): the five former copies inopenral_hal/…/mobile_base_bridge.py,openral_world_state/…/{spatial_memory,grid}.py,openral_sim/…/backends/so101_box/_assets.py, andopenral_runner/…/slam_bridge.pyall route through them. Before adding another yaw↔quat helper, use these. The remaining full-3-DOF conversions are deliberately left in place, each for a concrete reason, not oversight: - quat→matrix in
openral_sim/…/policies/rldx.py(_quat_wxyz_to_mat) is SAPIEN wxyz with its own norm-epsilon, pinned "bit-identical to upstream WidowXBridgeEnv" — a calibration surface, not a duplicate. - rpy→matrix/euler in
openral_safety/…/{mjcf,urdf}_lowering.pyis safety-kernel lowering; touching it needs safety-WG review + a recorded safety-impact update (CLAUDE.md §3), so it does not move on a cleanup PR. - the remaining
_quat_to_matrix(world_cloud_bridge.py, float32) and_rpy_to_*(bucket2_markers.py,depth_height_filter_node.py) are single-caller and return package-specific types; consolidating them would need a typedquat_xyzw_to_matrix/rpy_to_*set inopenral_core.geometryand is only worth it once a second caller appears. Add that set (and route new code through it) at that point.
Generated and curated 2026-05-08 from a single AST pass over
python/, packages/, and tools/. Re-run python3 -c "import ast"-based
extraction whenever a module is added or renamed; this file is hand-edited
afterwards. If a future contributor automates regeneration, mirror the
pattern in tools/schema_export.py.