Learning, and the capstone · University · about 35 min
Least squares in plain Python, and a robot that drives by the model it fitted to itself.
[1 mark]What does this least squares fit of speed against command print?
data = [(20, 3.1), (40, 7.4), (60, 11.2), (80, 15.3), (100, 19.0)] n = len(data) sx = sum(c for c, v in data) sv = sum(v for c, v in data) sxx = sum(c * c for c, v in data) sxv = sum(c * v for c, v in data) slope = (n * sxv - sx * sv) / (n * sxx - sx * sx) intercept = (sv - slope * sx) / n print(round(slope, 4), round(intercept, 3))
0.1985 -0.71
Six sums and a division give a slope of 0.1985 cm/s per unit of command and an intercept of -0.71 cm/s. The negative intercept is a hint of the dead band below the data.
[1 mark]A fit gives v = 0.2 c - 0.8. How many seconds should the robot drive at command 60 to cover 80 cm open loop? Give 2 decimal places.
[1 mark]Speeds are read 0.2 s after each command change. Why is that worse than random noise?
[1 mark]What does taking (forward - backward) / 2 at each command achieve?
[1 mark]A straight line is fitted to commands from 5 to 100, including the region below 15 where the drive does not move. What is the sensible response?
[1 mark]A proportional heading correction asks for 8 percent turn during the sweep, and the robot slowly turns anyway. Why?
[1 mark]Driving for D / v open loop lands close to D despite the 0.25 s lag. Why?
Sweep the command, fit speed against command by least squares, print slope: and intercept:, then use the fit to drive 80 cm open loop and stop there.
from bugbot import * connect() DT = 0.1 COMMANDS = [20, 30, 40, 50, 60, 70, 80, 90, 100] TARGET = 80.0
The hint students can ask for: Sweep the command, and at each one let the robot settle before reading flow(). Run each command forwards and then backwards so the robot stays where it started, and hold the heading square throughout, remembering that a correction below the drive's dead band does nothing. Fit speed against command by least squares, then work out how long to drive at one command to cover 80 cm.
from bugbot import *
connect()
DT = 0.1
COMMANDS = [20, 30, 40, 50, 60, 70, 80, 90, 100]
TARGET = 80.0
CRUISE = 70
def turn_cmd():
"""Hold the robot square: a sweep takes half a minute, and a robot that turns during it fits nonsense."""
err = (imu()[0] + 180) % 360 - 180
if abs(err) < 2.0:
return 0
cmd = max(18.0, min(30.0, abs(1.5 * err))) # below 15 the drive does nothing at all
return -cmd if err > 0 else cmd
def hold(cmd, ticks, collect=False):
vs = []
for i in range(ticks):
drive(cmd, 0, turn_cmd())
if collect:
vs.append(flow()[1])
wait(DT)
return vs
def speed_at(cmd):
"""Mean forward speed at this command, once the motors have come up."""
hold(cmd, 12) # the command reverses between samples, so give it five time constants
vs = hold(cmd, 3, True)
return sum(vs) / len(vs)
# every command forwards and then backwards, so the robot ends where it began
data = []
for c in COMMANDS:
out = speed_at(c)
back = speed_at(-c)
data.append((c, (out - back) / 2.0))
stop()
wait(0.3)
n = len(data)
sx = sum(c for c, v in data)
sv = sum(v for c, v in data)
sxx = sum(c * c for c, v in data)
sxv = sum(c * v for c, v in data)
slope = (n * sxv - sx * sv) / (n * sxx - sx * sx)
intercept = (sv - slope * sx) / n
print("slope:", round(slope, 4))
print("intercept:", round(intercept, 3))
# drive 80 cm open loop, on the model and nothing else
predicted = slope * CRUISE + intercept
seconds = TARGET / predicted
print("predicted speed:", round(predicted, 2), "so", round(seconds, 2), "seconds")
hold(CRUISE, int(round(seconds / DT)))
stop()
wait(0.5)
Any program that meets the task's checks is marked correct in the simulator; this is one way, not the only way.