Arsitektur CI untuk Validasi Robotika

Perubahan deskripsi fisik robot (URDF/Xacro) tanpa validasi otomatis kerap merusak kontroler kinematika, simulasi dinamika, dan algoritma path planning. Penerapan Continuous Integration (CI) pada repositori robotika memastikan setiap modifikasi deskripsi link, joint limit, tensor inersia, dan visual/collision mesh terverifikasi sebelum masuk ke branch utama.

Pipeline validasi terbagi menjadi dua tier: linting statis berbasis CPU untuk struktur dan kinematika, serta dynamic regression test berbasis physics engine headless (PyBullet atau Isaac Sim) untuk mengukur performa task benchmark.

Validasi Statis: Linting URDF dan Mesh Checking

Langkah pertama memverifikasi dokumen XML dari Xacro atau URDF mentah. Gunakan check_urdf dari paket liburdfdom-tools untuk memvalidasi tree struktur link dan joint, serta script Python untuk memverifikasi file STL/DAE/OBJ yang direferensikan.

Eksekusi CLI Linting

# Konversi Xacro ke URDF jika menggunakan parameter dinamis
xacro robot.urdf.xacro -o robot.urdf

# Validasi struktur tree dan parse XML
check_urdf robot.urdf

Skrip Verifikasi Mesh dan Tensor Inersia

Berikut skrip Python lint_robot.py untuk mendeteksi missing mesh file dan mengecek validitas matriks inersia (massa positif dan kondisi non-zero inertia tensor):

import os
import sys
import xml.etree.ElementTree as ET

def validate_urdf(urdf_path):
    tree = ET.parse(urdf_path)
    root = tree.getroot()
    errors = []

    for link in root.findall("link"):
        link_name = link.attrib.get("name", "unknown")
        
        # Validasi inersia jika link memiliki massa
        inertial = link.find("inertial")
        if inertial is not None:
            mass = inertial.find("mass")
            if mass is not None and float(mass.attrib.get("value", 0)) <= 0:
                errors.append(f"Link {link_name}: Massa harus > 0")
            
            inertia = inertial.find("inertia")
            if inertia is not None:
                ixx = float(inertia.attrib.get("ixx", 0))
                iyy = float(inertia.attrib.get("iyy", 0))
                izz = float(inertia.attrib.get("izz", 0))
                if ixx <= 0 or iyy <= 0 or izz <= 0:
                    errors.append(f"Link {link_name}: Diagonal inersia harus positif")

        # Validasi path visual/collision mesh
        for mesh in link.iter("mesh"):
            uri = mesh.attrib.get("filename", "")
            clean_path = uri.replace("package://", "").replace("file://", "")
            # Resolve relative path sesuai konteks workspace
            if not os.path.exists(clean_path):
                errors.append(f"Link {link_name}: Mesh file tidak ditemukan: {uri}")

    if errors:
        print("\n".join(errors))
        sys.exit(1)
    print("URDF & Mesh Check Passed.")

if __name__ == "__main__":
    validate_urdf(sys.argv[1])

Headless Regression Benchmark

Linting statis tidak dapat memverifikasi efek dynamika, slip, atau joint limits saat robot diberi kontrol torsi atau posisi. Regression testing dijalankan secara headless menggunakan PyBullet untuk menguji eksekusi trajectory dan mendeteksi deviasi posisi end-effector dari target ground-truth.

Skrip Benchmark Trajectory

Skrip evaluate_trajectory.py memuat URDF, menjalankan serangkaian command trajectory, dan menghitung Mean Squared Error (MSE) antara posisi aktual dengan expected waypoint:

import sys
import numpy as np
import pybullet as p
import pybullet_data

def run_benchmark(urdf_path, threshold_mse=0.015):
    physics_client = p.connect(p.DIRECT)  # Mode headless tanpa GUI
    p.setAdditionalSearchPath(pybullet_data.getDataPath())
    p.setGravity(0, 0, -9.81)
    
    plane_id = p.loadURDF("plane.urdf")
    robot_id = p.loadURDF(urdf_path, [0, 0, 0], useFixedBase=True)
    
    num_joints = p.getNumJoints(robot_id)
    active_joints = [j for j in range(num_joints) if p.getJointInfo(robot_id, j)[2] != p.JOINT_FIXED]
    
    # Generate trajectory target sintetis (step input per joint)
    target_positions = [0.5] * len(active_joints)
    actual_positions = []
    
    for step in range(240):  # Simulasi 1 detik (240 Hz)
        p.setJointMotorControlArray(
            robot_id,
            active_joints,
            controlMode=p.POSITION_CONTROL,
            targetPositions=target_positions,
            forces=[50.0] * len(active_joints)
        )
        p.stepSimulation()
    
    states = p.getJointStates(robot_id, active_joints)
    final_positions = [state[0] for state in states]
    
    p.disconnect()
    
    mse = np.mean((np.array(target_positions) - np.array(final_positions)) ** 2)
    print(f"Trajectory MSE: {mse:.6f} (Threshold: {threshold_mse})")
    
    if mse > threshold_mse:
        print("Deviasi melebihi batas toleransi!")
        sys.exit(1)
    print("Benchmark Passed.")

if __name__ == "__main__":
    run_benchmark(sys.argv[1])

Konfigurasi GitHub Actions Workflow

Konfigurasi workflow berikut menggabungkan stage linting dan execution benchmark. Untuk pipeline yang menggunakan Isaac Sim atau renderer berat, self-hosted runner berbasis GPU diperlukan. Skrip menggunakan Docker layer caching dan cache dependency untuk meminimalkan durasi eksekusi.

name: Robot Model CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  lint-urdf:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Lint Tools
        run: |
          sudo apt-get update
          sudo apt-get install -y liburdfdom-tools xacro

      - name: Generate and Check URDF
        run: |
          xacro models/robot.urdf.xacro -o /tmp/robot.urdf
          check_urdf /tmp/robot.urdf

      - name: Run Python Mesh and Inertia Linter
        run: |
          python3 scripts/lint_robot.py /tmp/robot.urdf

  trajectory-benchmark:
    needs: lint-urdf
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
          cache: 'pip'

      - name: Install Physics Dependencies
        run: |
          python -m pip install --upgrade pip
          pip install numpy pybullet

      - name: Run Headless Benchmark
        run: |
          python scripts/evaluate_trajectory.py models/robot.urdf

Mitigasi Runner Timeout pada Job Simulasi Berat

Simulasi fisika resolusi tinggi rentan terkena timeout jika dioperasikan pada GitHub-hosted runners standard (2 CPU cores, tanpa GPU). Terapkan strategi berikut untuk menjaga runtime CI tetap rendah:

  • Batasi Step Simulasi: Hanya simulasikan durasi minimal yang dibutuhkan kontroler untuk mencapai steady state (misalnya 1-3 detik pada timestep 240 Hz).
  • Gunakan Direct Mode (Headless): Hindari inisialisasi context OpenGL visualisasi kecuali sedang menguji visual perception pipeline. Gunakan opsi headless bawaan seperti p.DIRECT di PyBullet atau flag --headless di Isaac Sim.
  • Subsampling Collision Meshes: Untuk model simulasi CI, gunakan simplified convex hull collision mesh (.obj/.stl ter-reduksi) alih-alih high-poly visual mesh asli.
  • Self-Hosted GPU Runner dengan Caching: Jika beralih ke Omniverse Isaac Sim, jalankan job pada self-hosted runner dengan driver NVIDIA terpasang dan pasang cache Docker container image agar layer simulasi (~15GB+) tidak diunduh ulang di setiap commit.