Back to Blog

Building My First ROV Simulator

A hands-on project log documenting the creation of a basic ROV control simulator using Python and ROS2, applying automotive control theory to underwater vehicle dynamics.

Project Overview

One of the best ways to learn is to build. So I’m building a ROV (Remotely Operated Vehicle) control simulator from scratch. The goal isn’t to create production-grade software — it’s to understand the fundamentals of underwater vehicle control by applying concepts I already know from automotive systems.

Tech Stack:

  • Python 3.11
  • ROS2 (Robot Operating System 2)
  • Matplotlib for visualization
  • NumPy for physics calculations

Why a Simulator?

I don’t have a physical ROV (yet). But I do have a deep understanding of control systems from years of automotive work. A simulator lets me:

  1. Model the physics of underwater movement (buoyancy, drag, thruster dynamics)
  2. Implement control algorithms (PID controllers for depth hold, heading hold)
  3. Visualize telemetry in real-time
  4. Iterate quickly without the cost and risk of hardware testing

This mirrors the approach in automotive development: we simulate before we build.

Step 1: The Vehicle Model

An ROV is essentially a 6-DOF (six degrees of freedom) system — it can move in surge, sway, heave, roll, pitch, and yaw. For this first version, I’m simplifying to 3-DOF: surge (forward/backward), heave (up/down), and yaw (rotation).

The key forces acting on the ROV:

class ROVModel:
    def __init__(self):
        self.mass = 25.0  # kg
        self.buoyancy = 250.0  # N (slightly positive)
        self.drag_coefficients = {
            'surge': 20.0,
            'heave': 30.0,
            'yaw': 5.0,
        }
        self.position = np.zeros(3)  # x, z, theta
        self.velocity = np.zeros(3)
    
    def update(self, thrust_forces, dt):
        # Calculate drag (quadratic)
        drag = -np.sign(self.velocity) * self.drag_coefficients * self.velocity**2
        
        # Net force = thrust + drag + buoyancy
        net_force = thrust_forces + drag
        net_force[1] += (self.buoyancy - self.mass * 9.81)
        
        # Integration (Euler method for now)
        acceleration = net_force / self.mass
        self.velocity += acceleration * dt
        self.position += self.velocity * dt

This is simplified, but it captures the essential dynamics. The quadratic drag model is important — water resistance increases dramatically with speed, unlike air for road vehicles.

Step 2: The PID Controller

Here’s where automotive experience really shines. PID controllers are everywhere in automotive systems — cruise control, idle speed control, temperature regulation. The same principle applies to ROV depth hold:

class DepthHoldController:
    def __init__(self, kp=50.0, ki=5.0, kd=20.0):
        self.kp = kp
        self.ki = ki
        self.kd = kd
        self.integral = 0.0
        self.prev_error = 0.0
    
    def compute(self, target_depth, current_depth, dt):
        error = target_depth - current_depth
        self.integral += error * dt
        derivative = (error - self.prev_error) / dt
        self.prev_error = error
        
        # Anti-windup
        self.integral = np.clip(self.integral, -100, 100)
        
        output = self.kp * error + self.ki * self.integral + self.kd * derivative
        return np.clip(output, -100, 100)  # Thrust percentage

The anti-windup is critical — just like in automotive throttle control. Without it, the integral term accumulates during saturation and causes overshoot.

Step 3: ROS2 Integration

ROS2 provides the communication framework — nodes publish and subscribe to topics, similar to how automotive middleware like AUTOSAR handles inter-component communication. The architecture:

  • Vehicle Node: Runs the physics simulation
  • Controller Node: Implements the PID depth hold
  • Telemetry Node: Logs and visualizes data
  • Pilot Input Node: Accepts manual joystick commands

Current Status

The basic simulator is running. The depth hold controller can maintain target depth within ±0.1m in calm conditions. Next steps:

  • Add ocean current disturbances
  • Implement heading hold controller
  • Add simulated sonar sensor with noise
  • Build a real-time dashboard

Lessons So Far

The biggest realization: underwater control is harder than automotive control. In a car, you mostly deal with 1D control (speed, steering angle). An ROV operates in a fully 3D fluid environment where every axis is coupled. Maintaining depth while the vehicle surges forward requires coordinated multi-axis control.

But the fundamentals are the same. Understand the plant, model the disturbances, tune the controller, validate the response. Automotive engineers have this in their DNA.

More updates coming as the project evolves.