Autonomous Driving at CUEPIAC 2023
In October 2023 our team took First Prize, second place nationwide, in the autonomous-driving simulation track of the China Undergraduate Engineering Practice and Innovation Ability Competition (CUEPIAC). I led a team of four—Zheng Zhang, Wentao Huang, Zeqi Yao and myself— advised by Prof. Yi Zhong, and wrote the perception, decision, planning and control code described on this page: about 3,900 lines of C++ against the competition's 51Sim-One simulator, with no learned components and no library planner.
What the competition asked
Each entry drives a fixed set of scenarios in 51Sim-One and is scored per scenario: car-following, a stationary blocker in the lane, signalized and unsignalized junctions, pedestrians on crosswalks, cut-ins from neighboring lanes, lane changes, and one long continuous track. The simulator supplies ground-truth perception—every obstacle arrives already tagged with an id, a type and a speed—plus an HD map, and the entry must return throttle, brake and steering every tick.
That framing removes the perception problem and leaves the question of what the car should do given a clean, complete picture of the world. A run scores on more than avoiding a collision: moving off promptly at a green light, stopping at the stop line, and changing lanes into a gap that is actually there all count. The requirements pull against each other—the thresholds that make the car decisive at a junction make it aggressive behind a slow truck—and the work is in resolving that conflict explicitly.
What I built
Four layers plus utilities, each a set of free functions over simulator data and HD-map queries. The call graph is flat, so every decision the car makes can be traced to a named function.
| Layer | Lines | Role |
|---|---|---|
| Perception | 631 | Turn raw simulator obstacle entries into a filtered, lane-attributed obstacle list; find the obstacle ahead, speed-limit signs, junction blockers, crosswalk occupancy |
| Decision | 710 | Answer yes/no questions: is the target lane occupied, is the light green, have we passed the stop line, is a turn legal |
| Planning | 577 | Resolve the nearest lane, build the reference path from the navigation road list, generate lane-change paths, build detection zones |
| Control | 102 | Speed controller with output limits |
| Application | 1,139 | The main loop, the driving state machine, control post-processing |
Reasoning in lane coordinates
I built the decision and planning layers on lane coordinates rather than world coordinates.
Positions are projected onto a lane as (s, t)—distance along the lane and
lateral offset—and detection zones are rectangles expressed in those terms. A question that is
awkward in Cartesian space ("is that car beside me, in the next lane, on a curve?") becomes an interval
test, because the map supplies the geometry. The same trick makes the obstacle-ahead search work along
the target path rather than merely along the current lane, so a car in the lane you are about
to enter counts as an obstacle before you enter it.
The driving state machine
Behavior is decided by one variable taking eight values: Start!, Follow,
ChangeLaneStart, ObstacleAvoid, InChangeLane,
NearIntersection, CarAEB and ObstacleAEB. Junction handling is
a guard that runs before the state dispatch, so it pre-empts whatever state was active.
Follow is where the vehicle spends most of its time, and its gap-keeping law is
speed-dependent rather than a fixed distance:
| Gap to obstacle ahead | Target speed |
|---|---|
| > 30 m | max speed |
| (1.5·v + 15, 30] | max(min(vmax, 1.1·vobs), 5) |
| (1.0·v + 5, 1.5·v + 15] | max(min(vmax, 0.95·vobs), 4) |
| ≤ 1.0·v + 5 | max(0.6·vobs, 2) |
Leaving Follow is where the judgment sits. A slow-moving car ahead is a reason to
change lane; a stationary one is a reason to avoid; and if neither is legal, the
fallback is emergency braking—one state for a car, another for anything else, because their exit
conditions differ: braking for a car releases as soon as that car moves again, whereas a non-car
blocker only releases when the measured distance recovers.
Steering and speed
Path tracking is pure pursuit with a speed-adaptive lookahead: a near point at
max(v · 0.8 s, 5 m) and a far point at max(v · 4 s, 5 m), and the command
steering = -atan2(2 · L · sin α, ld) · 36 / (7π) · 1.7
with wheelbase L = 2.85 m, a factor that converts road-wheel radians into the SDK's
normalized range, and an empirical gain of 1.7. The far lookahead does not steer the car at all: a
large far-angle with a small near-angle means a curve is coming, and the state machine uses that as a
preview signal to decide whether a lane change is safe to start. It is a cheap way to get one bit of
the future out of a purely reactive controller.
Speed is a proportional controller with output limits. Steady-state error is absorbed by the target bands above rather than by integral action, and a post-processing step clamps steering harder as speed rises.
Watching it run
Four scenarios, one per branch of the state machine.
Lane change with adjacent traffic
The lane-change path is generated, the remainder of the global route is spliced onto it, and the plan is held without re-planning until the vehicle is within 3 m of the target point.
Stationary vehicle ahead
The car detects a stopped obstacle on its target path, asks whether a neighboring lane is legal and clear, generates a path around the obstacle's actual position, and rejoins the route.
Traffic-light response
The signalized branch resolves the upcoming light and stop line from the remaining road list, decelerates in three steps to the line, and moves off when the light turns green.
Pedestrian re-crossing
An unsignalized T-junction with a stop line. The vehicle decelerates in stages—5 m/s, then 1 m/s, then full brake—measured on whichever is nearer, the pedestrian or the stop line plus 3 m. The pedestrian steps on, turns back, and the car resumes.