Skip to content

Go2 Speed vs Precision testing#2138

Draft
mustafab0 wants to merge 20 commits into
mainfrom
mustafa/task/go2-controller-tuning
Draft

Go2 Speed vs Precision testing#2138
mustafab0 wants to merge 20 commits into
mainfrom
mustafa/task/go2-controller-tuning

Conversation

@mustafab0
Copy link
Copy Markdown
Contributor

@mustafab0 mustafab0 commented May 18, 2026

Problem

Tuning the Go2 base controller was guesswork: the FOPDT plant fit and feedforward gains were hand-vendored, with no single artifact telling an operator "for tolerance X cm, run at speed Y."

Issue: #921


Solution

Two CLI tools producing one versioned config artifact. go2_characterization runs a space-cheap velocity-step system-ID, fits FOPDT per axis (vx/vy/wz), and derives feedforward + a curvature velocity profile; go2_benchmark sweeps the hardcoded baseline controller across a speed ladder and writes back the operating-point map + tolerance→max-safe-speed inversion. One SI harness, sim vs hardware is just which plant the steps drive. Carries only the minimal verified dependency closure from the R&D branch (R&D archived off-repo).


Breaking Changes

None


How to Test

  1. uv run pytest dimos/utils/benchmarking/test_tuning.py -q → 16 pass.
  2. uv run python -m dimos.utils.benchmarking.characterization --mode self-test → artifact, valid_for_tuning=false.
  3. uv run python -m dimos.utils.benchmarking.benchmark --config <self-test artifact> --mode sim --speeds 0.5 → sim pre-check map.
  4. Run dimos run unitree-go2-webrtc-keyboard-teleop to position robot before each test

@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
1842 2 1840 28
View the top 2 failed test(s) by shortest run time
dimos.robot.test_all_blueprints::test_blueprint_is_valid[coordinator-sim-fopdt]
Stack Traces | 0.169s run time
blueprint_name = 'coordinator-sim-fopdt'

    @pytest.mark.parametrize("blueprint_name", UBUNTU_BLUEPRINTS)
    def test_blueprint_is_valid(blueprint_name: str) -> None:
        """Validate blueprints that should import on the ubuntu-latest runner."""
>       _check_blueprint(blueprint_name)

blueprint_name = 'coordinator-sim-fopdt'

dimos/robot/test_all_blueprints.py:99: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/robot/test_all_blueprints.py:75: in _check_blueprint
    blueprint = get_blueprint_by_name(blueprint_name)
        blueprint_name = 'coordinator-sim-fopdt'
        message    = 'Missing required tools: git-lfs.\n\nGit LFS installation instructions: https://git-lfs.github.io/'
dimos/robot/get_all_blueprints.py:47: in get_blueprint_by_name
    module = __import__(module_path, fromlist=[attr])
        attr       = 'coordinator_sim_fopdt'
        module_path = 'dimos.control.blueprints.mobile'
        name       = 'coordinator-sim-fopdt'
.../control/blueprints/mobile.py:186: in <module>
    "paths_dir": str(G1_LOCAL_PLANNER_PRECOMPUTED_PATHS),
        ControlCoordinator = <class 'dimos.control.coordinator.ControlCoordinator'>
        FastLio2   = <class 'dimos.hardware.sensors.lidar.fastlio2.module.FastLio2'>
        FopdtPlantConnection = <class 'dimos.robot.sim.fopdt_plant_connection.FopdtPlantConnection'>
        G1_LOCAL_PLANNER_PRECOMPUTED_PATHS = <[RuntimeError('Missing required tools: git-lfs.\n\nGit LFS installation instructions: https://git-lfs.github.io/') raised in repr()] LfsPath object at 0x7f7ebada42d0>
        HardwareComponent = <class 'dimos.control.components.HardwareComponent'>
        HardwareType = <enum 'HardwareType'>
        JointState = <class 'dimos.msgs.sensor_msgs.JointState.JointState'>
        KeyboardTeleop = <class 'dimos.robot.unitree.keyboard_teleop.KeyboardTeleop'>
        LCMTransport = <class 'dimos.core.transport.LCMTransport'>
        MovementManager = <class 'dimos.navigation.movement_manager.movement_manager.MovementManager'>
        Pose       = <class 'dimos.msgs.geometry_msgs.Pose.Pose'>
        PoseStamped = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
        Quaternion = <class 'dimos.msgs.geometry_msgs.Quaternion.Quaternion'>
        RerunBridgeModule = <class 'dimos.visualization.rerun.bridge.RerunBridgeModule'>
        RerunWebSocketServer = <class 'dimos.visualization.rerun.websocket_server.RerunWebSocketServer'>
        TaskConfig = <class 'dimos.control.coordinator.TaskConfig'>
        Twist      = <class 'dimos.msgs.geometry_msgs.Twist.Twist'>
        Vector3    = <class 'dimos.msgs.geometry_msgs.Vector3.Vector3'>
        __builtins__ = <builtins>
        __cached__ = '.../blueprints/__pycache__/mobile.cpython-312.pyc'
        __doc__    = 'Mobile manipulation coordinator blueprints.\n\nUsage:\n    dimos run coordinator-mock-twist-base                # Moc...drive)\n    dimos run coordinator-sim-fopdt                      # FOPDT sim plant on /go2/cmd_vel|odom (Go2-shaped)\n'
        __file__   = '/home/runner/work/dimos/dimos/.../control/blueprints/mobile.py'
        __loader__ = <_frozen_importlib_external.SourceFileLoader object at 0x7f7eba6e35c0>
        __name__   = 'dimos.control.blueprints.mobile'
        __package__ = 'dimos.control.blueprints'
        __spec__   = ModuleSpec(name='dimos.control.blueprints.mobile', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f7eba6e35c0>, origin='/home/runner/work/dimos/dimos/.../control/blueprints/mobile.py')
        _base_joints = ['base/vx', 'base/vy', 'base/wz']
        _catalog_xarm7 = <function xarm7 at 0x7f7eba91f060>
        _flowbase_mid360_mount = Pose(position=Vector([        0.2        -0.2         0.1]), orientation=Quaternion(0.000000, 0.000000, 0.000000, 1.000000))
        _flowbase_twist_base = <function _flowbase_twist_base at 0x7f7ed4102c00>
        _mock_twist_base = <function _mock_twist_base at 0x7f7eb9b56980>
        annotations = _Feature((3, 7, 0, 'beta', 1), None, 16777216)
        autoconnect = <function autoconnect at 0x7f7fcf5ff600>
        coordinator_flowbase = Blueprint(blueprints=(BlueprintAtom(kwargs={'hardware': [HardwareComponent(hardware_id='base', hardware_type=<Hardware...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        coordinator_flowbase_keyboard_teleop = Blueprint(blueprints=(BlueprintAtom(kwargs={'hardware': [HardwareComponent(hardware_id='base', hardware_type=<Hardware...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        coordinator_mock_twist_base = Blueprint(blueprints=(BlueprintAtom(kwargs={'hardware': [HardwareComponent(hardware_id='base', hardware_type=<Hardware...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        create_nav_stack = <function create_nav_stack at 0x7f7ebace6e80>
        make_twist_base_joints = <function make_twist_base_joints at 0x7f7fd0935a80>
        nav_stack_rerun_config = <function nav_stack_rerun_config at 0x7f7ebac544a0>
        os         = <module 'os' (frozen)>
dimos/utils/data.py:324: in __str__
    return str(self._ensure_downloaded())
        self       = <[RuntimeError('Missing required tools: git-lfs.\n\nGit LFS installation instructions: https://git-lfs.github.io/') raised in repr()] LfsPath object at 0x7f7ebada42d0>
dimos/utils/data.py:302: in _ensure_downloaded
    cache = get_data(filename)
        cache      = None
        filename   = 'unitree_g1_local_planner_precomputed_paths'
        self       = <[RuntimeError('Missing required tools: git-lfs.\n\nGit LFS installation instructions: https://git-lfs.github.io/') raised in repr()] LfsPath object at 0x7f7ebada42d0>
dimos/utils/data.py:259: in get_data
    archive_path = _decompress_archive(_pull_lfs_archive(archive_name))
        archive_name = 'unitree_g1_local_planner_precomputed_paths'
        data_dir   = PosixPath('.../dimos/dimos/data')
        file_path  = PosixPath('.../dimos/dimos/data/unitree_g1_local_planner_precomputed_paths')
        name       = 'unitree_g1_local_planner_precomputed_paths'
        nested_path = None
        path_parts = ('unitree_g1_local_planner_precomputed_paths',)
dimos/utils/data.py:186: in _pull_lfs_archive
    _check_git_lfs_available()
        filename   = 'unitree_g1_local_planner_precomputed_paths'
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def _check_git_lfs_available() -> bool:
        missing = []
    
        # Check if git is available
        try:
            subprocess.run(["git", "--version"], capture_output=True, check=True, text=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            missing.append("git")
    
        # Check if git-lfs is available
        try:
            subprocess.run(["git-lfs", "version"], capture_output=True, check=True, text=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            missing.append("git-lfs")
    
        if missing:
>           raise RuntimeError(
                f"Missing required tools: {', '.join(missing)}.\n\n"
                "Git LFS installation instructions: https://git-lfs.github.io/"
            )
E           RuntimeError: Missing required tools: git-lfs.
E           
E           Git LFS installation instructions: https://git-lfs.github.io/

missing    = ['git-lfs']

dimos/utils/data.py:135: RuntimeError
dimos.project.test_no_sections::test_no_section_markers
Stack Traces | 0.489s run time
def test_no_section_markers():
        """
        Fail if any file contains section-style comment markers.
    
        If a file is too complicated to be understood without sections, then the
        sections should be files. We don't need "subfiles".
        """
        violations = find_section_markers()
        if violations:
            report_lines = [
                f"Found {len(violations)} section marker(s). "
                "If a file is too complicated to be understood without sections, "
                'then the sections should be files. We don\'t need "subfiles".',
                "",
            ]
            for path, lineno, text in violations:
                report_lines.append(f"  {path}:{lineno}: {text.strip()}")
>           raise AssertionError("\n".join(report_lines))
E           AssertionError: Found 42 section marker(s). If a file is too complicated to be understood without sections, then the sections should be files. We don't need "subfiles".
E           
E             .../control/tasks/velocity_profiler.py:79: # ------------------------------------------------------------------
E             .../control/tasks/velocity_profiler.py:81: # ------------------------------------------------------------------
E             .../control/tasks/baseline_path_follower_task.py:137: # ------------------------------------------------------------------
E             .../control/tasks/baseline_path_follower_task.py:139: # ------------------------------------------------------------------
E             .../control/tasks/baseline_path_follower_task.py:213: # ------------------------------------------------------------------
E             .../control/tasks/baseline_path_follower_task.py:215: # ------------------------------------------------------------------
E             .../control/tasks/baseline_path_follower_task.py:289: # ------------------------------------------------------------------
E             .../control/tasks/baseline_path_follower_task.py:291: # ------------------------------------------------------------------
E             .../utils/characterization/trajectories.py:75: # ---------------------------------------------------------------------------
E             .../utils/characterization/trajectories.py:77: # ---------------------------------------------------------------------------
E             .../utils/characterization/trajectories.py:290: # ---------------------------------------------------------------------------
E             .../utils/characterization/trajectories.py:292: # ---------------------------------------------------------------------------
E             .../utils/characterization/trajectories.py:329: # ---------------------------------------------------------------------------
E             .../utils/characterization/trajectories.py:331: # ---------------------------------------------------------------------------
E             .../utils/characterization/trajectories.py:357: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/test_tuning.py:50: # --- DERIVE ---------------------------------------------------------------
E             .../utils/benchmarking/test_tuning.py:137: # --- artifact round-trip --------------------------------------------------
E             .../utils/benchmarking/test_tuning.py:160: # --- tolerance -> max-safe-speed inversion --------------------------------
E             .../utils/benchmarking/plant.py:120: # --- Vendored fitted FOPDT plant for the Go2 base ------------------------
E             .../utils/benchmarking/plant.py:147: # --- Per-robot profile (single source of truth for robot specifics) -----
E             .../utils/benchmarking/characterization.py:105: # --- self-test (in-process FOPDT plant; NOT robot-valid) -----------------
E             .../utils/benchmarking/characterization.py:171: # --- fit-quality graph (the human-facing deliverable) -------------------
E             .../utils/benchmarking/characterization.py:217: # --- hardware SI (real robot over LCM, operator-gated, safe) -------------
E             .../utils/benchmarking/paths.py:78: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/paths.py:80: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/paths.py:190: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/paths.py:192: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/paths.py:284: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/paths.py:286: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/scoring.py:82: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/scoring.py:84: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/scoring.py:129: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/scoring.py:131: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/scoring.py:199: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/scoring.py:201: # ---------------------------------------------------------------------------
E             .../utils/benchmarking/benchmark.py:342: # --- benchmark ----------------------------------------------------------
E             .../utils/benchmarking/tuning.py:53: # --- DERIVE tunable constants (documented; single source of truth) -------
E             .../utils/benchmarking/tuning.py:86: # --- Artifact schema -----------------------------------------------------
E             .../utils/benchmarking/tuning.py:214: # --- serialization ---
E             .../utils/benchmarking/tuning.py:264: # --- helpers -------------------------------------------------------------
E             .../utils/benchmarking/tuning.py:313: # --- DERIVE: pure model -> config ---------------------------------------
E             .../utils/benchmarking/tuning.py:398: # --- tolerance -> max-safe-speed inversion (pure) ------------------------

lineno     = 398
path       = '.../utils/benchmarking/tuning.py'
report_lines = ['Found 42 section marker(s). If a file is too complicated to be understood without sections, then the sections should...l/tasks/baseline_path_follower_task.py:139: # ------------------------------------------------------------------', ...]
text       = '# --- tolerance -> max-safe-speed inversion (pure) ------------------------'
violations = [('.../control/tasks/velocity_profiler.py', 79, '    # -------------------------------------------------------------...baseline_path_follower_task.py', 215, '    # ------------------------------------------------------------------'), ...]

dimos/project/test_no_sections.py:145: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@mustafab0 mustafab0 force-pushed the mustafa/task/go2-controller-tuning branch from 2fb6967 to bfe69aa Compare May 20, 2026 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant