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.

The continuous-track scenario. A four-minute run through curves, junctions and mixed traffic, shown at the simulator's fast-forward. The panel at the right is the simulator's live readout of speed, throttle, steering, brake and gear—every value there is a decision the stack made that tick.

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

One tick of the control loop: perception feeds decision and planning, the driving state machine chooses a target speed and rebuilds the target path, and control produces throttle and steering
One tick of the control loop.

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
Two cyclists ahead of the vehicle, each labeled by the simulator with an id and a speed, beside a stop sign The ego vehicle model in the scenario editor, a compact hatchback shown on a grid
Left: what the stack is given—every obstacle already tagged with an id, a type and a speed. Right: the ego vehicle, whose 2.85 m wheelbase is the one physical constant the steering law depends on.

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

The eight driving states and the conditions that move the vehicle between them
Eight states. Every transition is an explicit predicate over map and obstacle geometry.

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.

Resources