# Advanced Robotics at UW

Advanced Robotics at UW · Oct 2025 – present
Tools: C++, Python, ROS 2, Kalman filter, LQR, motion capture
Page: https://ericjgoossen.com/projects/aruw/
Links: [ARUW](https://aruw.org/) [Team code (GitLab)](https://gitlab.com/aruw/controls/aruw-mcb) [Odin characterization](https://github.com/EricJGoossen/odin-characterization)

Summary: Localization and control for ARUW's competition robots, from Kalman-filter odometry and LQR path tracking to measuring the sentry's localization against motion capture.
Lines of code Eric wrote: 10,204 (the sum of the entries below)

![Our red robot fighting two blue robots on the capture point in round 3 of the 3v3 match against RoboGrinder at the 2026 ARC Championship, with the broadcast's damage numbers over them](https://ericjgoossen.com/projects/aruw/images/hero-match.jpg)

## Overview

I joined Advanced Robotics at UW, the university's RoboMaster team, as a control systems programmer: I reworked the chassis's Kalman-filter odometry for new odometry pods and led a rewrite of its path tracking from pure pursuit to LQR. At the ARC robotics competition, the sentry, our autonomous robot, didn't navigate as well as it needed to, so I switched to the vision team to be the one to fix that, starting by measuring exactly how good its localization is against motion capture. This year I own the sentry's path planning and state machine, and I want to look into learning-based control, so it can adapt to opponents as the match unfolds.

## Entries (6, newest first)

### The same check for a 2D lidar

Date: 2026-10-09 · URL: https://ericjgoossen.com/projects/aruw/#ld19-lidar
Summary: The same characterization for a 2D lidar for obstacle mapping: steady, low noise, and good enough for our maps.
Lines of code Eric wrote: 769 | Language: Python | Tools: ROS 2 | Hardware: LD19 lidar | Role: Solo | Status: Done
Skills: ROS 2, State estimation
Links: [Code](https://github.com/EricJGoossen/odin-characterization/tree/5244122/odin_char/sensors/ld19)

I ran the same characterization on a WayPonDEV LD19 2D lidar, for obstacle mapping, with the Odin providing the pose. It scans at a steady 10 Hz with 8–14 ms of jitter, about 503 points per scan 0.72° apart, and skips 1–2% of scans. Range noise is 3–6 mm at 1–2 m, 6–11 mm at 2–3 m and 7–28 mm at 3–5 m: well under a 5 cm map cell, even at 5 m. At most 0.2% of returns are isolated false points and 1–2.4% are outliers, mostly at object edges where a beam half-hits the edge and half the background, which a filter removes easily. It never dropped off USB in the 40 minutes it ran, and the longest gap between scans was one skipped scan. The LD19 was good enough for our use, but we bought a 3D lidar anyway: the Odin's field of view is small, and a 3D lidar sees much more of the area around the robot.

![Occupancy map of a cluttered room, about 7 by 12 meters, with the robot's pushed path drawn in red as two loops](https://ericjgoossen.com/projects/aruw/images/ld19-map.png)
Caption: The room mapped with the LD19's scans, placed by the Odin's poses (5 cm cells), with the 24.5 m path the robot was pushed along in red.

![Top-down diagram of the sentry's sensor coverage: the Odin's lidar covers a 126 degree wedge to the front, the LD19 a 196 degree arc behind, and a 39 degree wedge between them is covered by neither](https://ericjgoossen.com/projects/aruw/images/sensor-coverage.png)
Caption: What the two sensors see on the sentry, from the measured mounting: a 39° wedge is covered by neither.

### An error model for the Odin (featured)

Date: 2026-10-08 · URL: https://ericjgoossen.com/projects/aruw/#odin-error-model
Summary: An error model for the Odin, validated on a held-out session by Monte Carlo, that showed the sentry could rely on it alone.
Lines of code Eric wrote: 691 | Language: Python | Tools: NumPy, SciPy | Hardware: Odin1 | Role: Solo | Status: Done
Skills: State estimation, Experiment design, Python
Links: [Analysis code](https://github.com/EricJGoossen/odin-characterization/tree/2cf113f)

The question wasn't how accurate the Odin is but whether the sentry could rely on it alone, and one accuracy number can't answer that: the nav stack needs to know how the error behaves, whether it grows, and what happens when something goes wrong. So I designed the October 3 campaign around those questions (figure-8 laps, a long hold, repeated relocalizations and covered-sensor trials, all against mocap) and fit a small model to it: the true pose, plus an offset drawn each time the Odin relocalizes into its map, plus first-order Gauss-Markov noise: correlated over time, but decaying.

[Equations: see this entry on its page]

Splitting the error this way was the key decision. The offset from each relocalization turned out to be the largest term, which one overall noise figure would have blurred, and modelling the noise as decaying showed the error stays bounded instead of drifting. To check the model wasn't just describing the data it was fit to, I refit it without one session, simulated it many times along that session's real path (a Monte Carlo check), and asked whether the real error fell where the simulations said it would. It did: its real error there, 5.1 cm horizontal and under half a degree, matched the prediction on all six axes (roll came in just under its predicted range, still within the test). That made the recommendation clear: the Odin can be the sentry's only localization, kept on the turret major, where it swivels independently of the turret and the chassis. We tried it in several mount locations and found the occlusions made little difference. The nav stack only has to handle a rare wrong relocalization, and the output pausing while the sensor is covered.

![Depth image from the Odin, colored from yellow for near to purple for far, in its round fisheye frame: a wall close by on the left, a person sitting in a chair, and boxes and furniture across the room](https://ericjgoossen.com/projects/aruw/images/odin-depth.jpg)
Caption: What the Odin sees: a depth image from its recordings, near in yellow and far in purple.

![Chart: for each axis, the Odin's real error on a held-out session as a dot against the model's predicted range as a bar; along-track, cross-track and vertical position, and roll, pitch and yaw](https://ericjgoossen.com/projects/aruw/images/odin-holdout.svg)
Caption: The hold-out check: the model, refitted without one session, predicts that session's error. Every axis passes; roll comes in just under its predicted range.

### A test platform against motion capture (featured)

Date: 2026-10-02 · URL: https://ericjgoossen.com/projects/aruw/#odin-characterization
Summary: A test platform that measures the sentry's lidar-inertial localization against motion capture, from guided driving to a fixed campaign protocol.
Lines of code Eric wrote: 5,959 | Language: Python | Tools: OptiTrack | Hardware: Odin1, sentry | Role: Solo | Status: Done
Skills: Experiment design, Python
Links: [Code](https://github.com/EricJGoossen/odin-characterization/tree/aa1cefe)

The sentry localizes with an Odin1, a lidar-inertial sensor that relocalizes into a prebuilt map. Before the team decided whether the whole localization system could rely on it alone, we wanted to know how accurate it is and where it is weak. So I built [a test platform](https://github.com/EricJGoossen/odin-characterization) that measures it against the motion capture room, whose OptiTrack tracks the robot to a fraction of a millimeter. It starts and stops the sensor, records every run, guides whoever is driving with live mocap feedback, and can also drive the robot itself through the Jetson and the robot's controller, with a watchdog and speed caps. A campaign day runs a fixed protocol: calibration spins, a mapping drive, figure-8 laps at set speeds, ten-minute holds, back-and-forth motion to measure latency, cold starts, and outages where someone covers the sensor.

![Diagram of the system: motion capture and the Odin1 sensor feed the characterization scripts, which record every run and write a report; in autonomous mode they send chassis velocities over UDP to the Jetson, which forwards them over UART to the sentry's motor control board](https://ericjgoossen.com/projects/aruw/images/odin-system.svg)
Caption: The three parts, and the default human-driven mode.

![Bar timeline of day 1: preflight 8 minutes, calibration 3, mapping 5, figure-8 laps 6, holds 41, latency 3, cold starts 16, outages 11; 93 minutes in all](https://ericjgoossen.com/projects/aruw/images/odin-day1.svg)
Caption: Day 1, step by step, from the campaign's time estimate.

### Kalman-filter odometry for three dead wheels (featured)

Date: 2026-06-30 · URL: https://ericjgoossen.com/projects/aruw/#kalman-filter
Summary: The engineer robot's Kalman-filter odometry rewritten for three dead wheels, split into three small filters and tuned offline from logged drives.
Lines of code Eric wrote: 905 | Language: C++, Python | Tools: aruw-mcb | Hardware: Engineer robot | Role: Solo | Status: Used at competition
Skills: State estimation, C++
Links: [8-state filter](https://gitlab.com/aruw/controls/aruw-mcb/-/commit/57dd3eb62f9330f57eea4b856fb0c3f8f6fe5a7d) [Split filters](https://gitlab.com/aruw/controls/aruw-mcb/-/commit/32612de587558ed10e7d0453618395ba06c242d3)

Our robots know where they are from dead wheels: small unpowered tracking wheels with encoders that roll along the floor. When the engineer robot moved to new odometry pods with three of them, I [rewrote the team's Kalman-filter odometry](https://gitlab.com/aruw/controls/aruw-mcb/-/commit/57dd3eb62f9330f57eea4b856fb0c3f8f6fe5a7d) for them; I had taught myself Kalman filters from *Kalman Filter from the Ground Up*. The first version was one 8-state filter fusing the three wheels with the IMU. Its covariance between heading and the x and y motion turned out to be negligible, so I [split it](https://gitlab.com/aruw/controls/aruw-mcb/-/commit/32612de587558ed10e7d0453618395ba06c242d3) into three independent two-state filters, for x, y and heading, each updated by one measurement: every step now multiplies 2×2 matrices and divides by a number, instead of working through 8×8 matrices and inverting a 7×7 one, and the wheels alone turned out to be enough without the IMU. To tune it, I drove fixed paths (straight forward, straight sideways, spins and a square), logged the raw wheels, and replayed them offline to fit each pod's position, angle and wheel radius and the filter's noise, checking the result against the IMU's heading. Our target was under 10 cm of error over the engineer's path, the most our vision system could correct for, and the tuned odometry met it.

Video: Top-down replay on the field: the robot's footprint drives from the third cube pickup at the left across the field to the third drop-off at the right, leaving a blue trail
File: https://ericjgoossen.com/projects/aruw/images/kf-competition-run.mp4; still frame (an image you can view): https://ericjgoossen.com/projects/aruw/images/kf-competition-run.jpg
Caption: The engineer driving itself at competition, from the third cube pickup to its drop-off: the filter's own logged pose, replayed at real time from just before it moves.

Video: A logged 5 meter sideways push replayed through the filter while the pods' parameters move from their design values to the tuned ones: the estimated path swings from a wide curve onto the dashed path the IMU gives, and in a plot below the wheels' heading comes down onto the IMU's
File: https://ericjgoossen.com/projects/aruw/images/kf-tuning.mp4; still frame (an image you can view): https://ericjgoossen.com/projects/aruw/images/kf-tuning.jpg
Caption: Tuning offline: a logged 5 m sideways push, replayed through the filter as the pods move from their design values to the tuned ones. With one pod 2.7° off straight, the design values read the push as a 79° turn; tuned, it comes to 4.99 m and follows the path the IMU's heading gives, ending within 1° of it.

### Path tracking: from pure pursuit to LQR (featured)

Date: 2026-05-16 · URL: https://ericjgoossen.com/projects/aruw/#lqr-path-tracking
Summary: I led replacing the chassis's pure pursuit with LQR path tracking that models the robot's accelerations: within 3.2 cm of the path in simulation.
Lines of code Eric wrote: 1,880 | Language: C++ | Tools: Eigen | Hardware: Engineer robot | Role: Lead | Status: In simulation
Skills: Control systems, Technical leadership, C++
Links: [Code](https://github.com/EricJGoossen/LQRPathing/tree/999e391) [The LQR study](https://ieeexplore.ieee.org/document/9517448)

Our chassis followed paths with pure pursuit: aim at a point a fixed distance down the path and ramp the speed up toward it. The robot's accelerations stay implicit in that, so it has no idea how hard it can brake or how fast it can take a corner, and at speed it swings wide of turns and overshoots where it should stop. I proposed replacing it with path tracking that models those accelerations explicitly, and led the feature from idea to implementation: I wrote the design and gathered the resources behind it, from spline fitting and arc-length reparametrization to the Riccati equation and [a study](https://ieeexplore.ieee.org/document/9517448) that found LQR tracks more accurately than pure pursuit. First, a cubic spline through the waypoints is reparametrized by arc length, the speed at each point is capped by its curvature, and a forward and a backward pass keep every speed-up and every stop within what the robot can deliver. Then an LQR tracks that profile on a world-frame model of the chassis, where its dynamics stay constant, and its force is rotated into the robot's frame. I designed it and built the framework; the comparison here runs the finished MVP against our pure pursuit in simulation.

Video: Two dots racing along an S-shaped path with hairpin turns: the blue one, LQR, stays on the path; the orange one, pure pursuit, swings out in a loop at every hairpin and finishes later
File: https://ericjgoossen.com/projects/aruw/images/lqr-vs-pure-pursuit.mp4; still frame (an image you can view): https://ericjgoossen.com/projects/aruw/images/lqr-vs-pure-pursuit.jpg
Caption: The same slalom, the same simulated chassis and the same 2 m/s top speed: LQR (blue) against the team's pure pursuit (orange), in real time.

![Bar charts for three paths: LQR finishes each sooner and stays within about 3 cm of the path, against 33 to 46 cm for pure pursuit](https://ericjgoossen.com/projects/aruw/images/lqr-vs-pure-pursuit.svg)
Caption: Three paths: LQR finishes 1.5 to 2.1 s sooner and stays within 3.2 cm of the path; pure pursuit strays up to 46 cm.

### The competition and our robots

Date: 2025-10-01 · URL: https://ericjgoossen.com/projects/aruw/#the-competition
Summary: What ARC is, the eight robots we build each season, and how we placed at the 2026 championship.
Language: C++ | Hardware: Engineer, sentry | Role: Team | Status: 3rd in the 3v3

Links: [Robot reveal](https://www.youtube.com/watch?v=FGh1yQdJGeM) [Championship match](https://www.youtube.com/watch?v=PqSzhmRSAZA) [ARUW](https://aruw.org/)

Advanced Robotics at UW competes in ARC, DJI's robotics competition for universities in North America (RoboMaster North America until 2025). Every season the team designs and builds a fleet of eight robots, most of them new ([this year's reveal](https://www.youtube.com/watch?v=FGh1yQdJGeM)), for what the team calls a deathmatch somewhere between paintball and League of Legends. Operators drive their robots through an onboard camera and fire 17 mm and 42 mm projectiles at the pressure-sensitive armor plates on the other team's robots, while fighting king-of-the-hill style for a capture point in the middle of the field, in matches that are each a best of three. The engineer can't shoot: it collects minerals to trade for coins that buy supplies during the match. The sentry is the only fully autonomous robot on the field, finding its own way around and aiming on its own. ARUW won the competition every year from 2021 to 2024. At the 2026 ARC Championship at Purdue we placed third in the 3v3, second in the dart event and third in the drone event; for it I worked on the engineer's odometry and path tracking, and since then I've moved to the sentry's localization and navigation.

![Three of ARUW's 2026 robots side by side on the grass, two with long launcher barrels and the black-shelled sentry on the right, with visitors' legs in the background](https://ericjgoossen.com/projects/aruw/images/fleet-lineup.jpg)
Caption: The sentry (right) and two of our other 2026 robots on the Quad in cherry blossom season.

[YouTube video: Our 2026 robot reveal](https://www.youtube.com/watch?v=FGh1yQdJGeM) (still frame: https://ericjgoossen.com/projects/aruw/images/reveal.jpg)

