Problem 359125 · hard · Level 03 Linear Management & Searching

Design the Cruise Control

PI control · anti-windup · feedforward · setpoint ramp · disturbance rejection · trade-off

Now you are the cruise control. A hidden car drives a hilly 10-minute route, and every half second the simulator calls your function

controller(t, speed, v_set, grade) -> force

with the time t in seconds (0, 0.5, 1, ...), the speedometer reading in m/s, the current speed limit v_set in m/s, and the road's grade from an inclinometer (rise per metre; 0.05 is a 5 % climb). Return the force you want at the wheels, in newtons: positive drives, negative brakes.

What you are up against:

  • the engine delivers at most 2200 N and the brakes at most 4000 N: any force outside -4000..2200 is limited to that range;
  • the car has a mass between 1100 and 1700 kg (passengers and luggage vary; you are not told), air drag of about 0.35 to 0.5 times the square of the air speed, and a rolling resistance of about 1.2 % of its weight;
  • a steady wind of up to 5 m/s blows with or against the car;
  • hills of up to ±8 % pull the car back or push it on: the slope's force is mass * 9.81 * grade;
  • the speed limit changes every 50 to 140 seconds, between 14 and 31 m/s;
  • the speedometer is noisy (a random error of about 0.1 m/s, rounded to 0.01) and the inclinometer slightly so.

The car starts at the first speed limit. You are judged against a reference speed v_ref that starts at the first limit and, after every half-second step, moves towards the current limit by at most 0.5 m/s going up (a comfortable 1 m/s²) and at most 0.75 m/s going down:

v_ref = min(v_set, v_ref + 0.5) if v_ref < v_set else max(v_set, v_ref - 0.75)

Each test calls run_drive(controller, seed) (it is defined for you, so you can try it with Run), which returns a summary of the drive, for example

{"rms_error": 1.006781, "mean_force_change": 92.971, "worst_over_limit": 0.8672,
 "worst_error": 2.2233, "distance_km": 12.53}

for the plain proportional controller lambda t, speed, v_set, grade: 600 * (v_set - speed) on seed 1. Your function may keep state (an integral, a filtered speed) in global variables; t == 0 marks the start of a new drive, so reset your state then.

How this problem is scored

  • A test passes when the car is never more than 2 m/s over the speed limit (from 10 s after each change of the limit, so there is time to brake) and the RMS error is at most 2 m/s.
  • Its quality is 100 - 35 * rms_error - 0.12 * mean_force_change (between 0 and 100). rms_error is the root-mean-square of speed - v_ref over all 1200 steps; mean_force_change is the average of abs(F - previous F) in newtons, the jerking of the throttle and brakes that passengers feel and that wastes fuel. Every 0.1 m/s of RMS error costs 3.5 points, every 10 N of average change 1.2 points.
  • The P controller above scores 54 on seed 1: it never quite reaches the limit uphill, and it passes all the speedometer's noise straight to the engine.

Constraints

  • return a number; the result must not depend on the clock, and each call should take well under a millisecond

Goals

  • Write a speed controller that a simulator calls once per time step
  • Hold a reference speed over hills and wind with a limited engine force
  • Balance tracking against how much the force is jerked about
Starting Python…