from matplotlib.pylab import *
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
from matplotlib.lines import Line2D
π = pi
style.use(['dark_background', 'bmh'])
%matplotlib widget
Car-trailer diagram (inverted image res/car-trainer-k.png
available as well):
Car-trailer equation: \begin{align} \dot x &= s \cos \theta_0 \\ \dot y &= s \sin \theta_0 \\ \dot \theta_0 &= \frac{s}{L} \tan \phi \\ \dot \theta_1 &= \frac{s}{d_1} \sin(\theta_1 - \theta_0) \end{align} where $s$: signed speed, $\phi$: negative steering angle,
class Truck:
def __init__(self, display=False):
self.W = 1 # car and trailer width, for drawing only
self.L = 1 * self.W # car length
self.d = 4 * self.L # d_1
self.s = -0.1 # speed
self.display = display
self.box = [0, 40, -10, 10]
if self.display:
self.f = figure(figsize=(10, 5), num='The truck backer-upper', facecolor='none')
self.ax = self.f.add_axes([0.01, 0.01, 0.98, 0.98], facecolor='black')
self.patches = list()
self.ax.axis('equal')
b = self.box
self.ax.axis([b[0] - 1, b[1], b[2], b[3]])
self.ax.set_xticks([]); self.ax.set_yticks([])
self.ax.axhline(); self.ax.axvline()
self.reset()
def reset(self, ϕ=0):
self.ϕ = ϕ # car initial steering angle
# self.θ0 = deg2rad(30) # car initial direction
# self.θ1 = deg2rad(-30) # trailer initial direction
# self.x, self.y = 20, -5 # initial car coordinates
self.θ0 = random() * 2 * π # 0 <= ϑ₀ < 2π
self.θ1 = (random() - 0.5) * π / 2 + self.θ0 # -π/4 <= ϑ₁ - ϑ₀ < π/4
self.x = (random() * .75 + 0.25) * self.box[1]
self.y = (random() - 0.5) * (self.box[3] - self.box[2])
# If poorly initialise, then re-initialise
if not self.valid():
self.reset(ϕ)
# Draw, if display is True
if self.display: self.draw()
def step(self, ϕ=0, dt=1):
# Check for illegal conditions
if self.is_jackknifed():
print('The truck is jackknifed!')
return
if self.is_offscreen():
print('The car or trailer is off screen')
return
self.ϕ = ϕ
x, y, W, L, d, s, θ0, θ1, ϕ = self._get_atributes()
# Perform state update
self.x += s * cos(θ0) * dt
self.y += s * sin(θ0) * dt
self.θ0 += s / L * tan(ϕ) * dt
self.θ1 += s / d * sin(θ0 - θ1) * dt
return (self.x, self.y, self.θ0, *self._traler_xy(), self.θ1)
def state(self):
return (self.x, self.y, self.θ0, *self._traler_xy(), self.θ1)
def _get_atributes(self):
return (
self.x, self.y, self.W, self.L, self.d, self.s,
self.θ0, self.θ1, self.ϕ
)
def _traler_xy(self):
x, y, W, L, d, s, θ0, θ1, ϕ = self._get_atributes()
return x - d * cos(θ1), y - d * sin(θ1)
def is_jackknifed(self):
x, y, W, L, d, s, θ0, θ1, ϕ = self._get_atributes()
return abs(θ0 - θ1) * 180 / π > 90
def is_offscreen(self):
x, y, W, L, d, s, θ0, θ1, ϕ = self._get_atributes()
x1, y1 = x + 1.5 * L * cos(θ0), y + 1.5 * L * sin(θ0)
x2, y2 = self._traler_xy()
b = self.box
return not (
b[0] <= x1 <= b[1] and b[2] <= y1 <= b[3] and
b[0] <= x2 <= b[1] and b[2] <= y2 <= b[3]
)
def valid(self):
return not self.is_jackknifed() and not self.is_offscreen()
def draw(self):
if not self.display: return
if self.patches: self.clear()
self._draw_car()
self._draw_trailer()
self.f.canvas.draw()
def clear(self):
for p in self.patches:
p.remove()
self.patches = list()
def _draw_car(self):
x, y, W, L, d, s, θ0, θ1, ϕ = self._get_atributes()
ax = self.ax
x1, y1 = x + L / 2 * cos(θ0), y + L / 2 * sin(θ0)
bar = Line2D((x, x1), (y, y1), lw=5, color='C2', alpha=0.8)
ax.add_line(bar)
car = Rectangle(
(x1, y1 - W / 2), L, W, color='C2', alpha=0.8, transform=
matplotlib.transforms.Affine2D().rotate_deg_around(x1, y1, θ0 * 180 / π) +
ax.transData
)
ax.add_patch(car)
x2, y2 = x1 + L / 2 ** 0.5 * cos(θ0 + π / 4), y1 + L / 2 ** 0.5 * sin(θ0 + π / 4)
left_wheel = Line2D(
(x2 - L / 4 * cos(θ0 + ϕ), x2 + L / 4 * cos(θ0 + ϕ)),
(y2 - L / 4 * sin(θ0 + ϕ), y2 + L / 4 * sin(θ0 + ϕ)),
lw=3, color='C5', alpha=1)
ax.add_line(left_wheel)
x3, y3 = x1 + L / 2 ** 0.5 * cos(π / 4 - θ0), y1 - L / 2 ** 0.5 * sin(π / 4 - θ0)
right_wheel = Line2D(
(x3 - L / 4 * cos(θ0 + ϕ), x3 + L / 4 * cos(θ0 + ϕ)),
(y3 - L / 4 * sin(θ0 + ϕ), y3 + L / 4 * sin(θ0 + ϕ)),
lw=3, color='C5', alpha=1)
ax.add_line(right_wheel)
self.patches += [car, bar, left_wheel, right_wheel]
def _draw_trailer(self):
x, y, W, L, d, s, θ0, θ1, ϕ = self._get_atributes()
ax = self.ax
x, y = x - d * cos(θ1), y - d * sin(θ1) - W / 2
trailer = Rectangle(
(x, y), d, W, color='C0', alpha=0.8, transform=
matplotlib.transforms.Affine2D().rotate_deg_around(x, y + W/2, θ1 * 180 / π) +
ax.transData
)
ax.add_patch(trailer)
self.patches += [trailer]
truck = Truck(display=True)
ϕ = deg2rad(-35) # positive left, negative right
truck.step(ϕ)
truck.draw()
truck.reset()
import torch
import torch.nn as nn
from torch.optim import SGD
from tqdm import tqdm
# Build expert data set
episodes = 10
inputs = list()
outputs = list()
# truck = Truck(); episodes = 10_000 # uncomment for creating the data set
for episode in tqdm(range(episodes)):
truck.reset()
while truck.valid():
initial_state = truck.state()
ϕ = (random() - 0.5) * π / 2
inputs.append((ϕ, *initial_state))
outputs.append(truck.step(ϕ))
truck.draw()
len(inputs), len(outputs)
state_size = 6
steering_size = 1
hidden_units_e = 45
emulator = nn.Sequential(
nn.Linear(steering_size + state_size, hidden_units_e),
nn.ReLU(),
nn.Linear(hidden_units_e, state_size)
)
optimiser_e = SGD(emulator.parameters(), lr=0.005)
criterion = nn.MSELoss()
tensor_inputs = torch.Tensor(inputs)
tensor_outputs = torch.Tensor(outputs)
mean = tensor_inputs.mean(0)
std = tensor_inputs.std(0)
tensor_inputs = (tensor_inputs - mean) / std
tensor_outputs = (tensor_outputs - mean[1:]) / std[1:]
# Split the data into 80:20 for test:train.
test_size = int(len(tensor_inputs) * 0.8)
print(len(tensor_inputs), test_size)
train_inputs = tensor_inputs[:test_size]
train_outputs = tensor_outputs[:test_size]
test_inputs = tensor_inputs[test_size:]
test_outputs = tensor_outputs[test_size:]
len(train_inputs)
# Emulator training
cnt = 0
for i in torch.randperm(len(train_inputs)):
ϕ_state = train_inputs[i]
next_state_prediction = emulator(ϕ_state)
next_state = train_outputs[i]
loss = criterion(next_state_prediction, next_state)
optimiser_e.zero_grad()
loss.backward()
optimiser_e.step()
if cnt == 0 or (cnt + 1) % 1000 == 0:
print(f'{cnt + 1:4d} / {len(train_inputs)}, {loss.item():.10f}')
cnt += 1
# Test
total_loss = 0
with torch.no_grad():
for idx, ϕ_state in enumerate(test_inputs):
next_state_prediction = emulator(ϕ_state)
next_state = test_outputs[idx]
total_loss += criterion(next_state_prediction, next_state).item()
ave_test_loss = total_loss/test_size
print(f'Test loss: {ave_test_loss:.10f}')
# Here you need to insert the code for training the controller
# by using the emulator for backpropagation
# If you succeed, feel free to send a PR