Skip to content

Commit 7065679

Browse files
authored
Add vehicle_model.py (#1808)
* move * move test too * better
1 parent 27a50f8 commit 7065679

File tree

2 files changed

+297
-0
lines changed

2 files changed

+297
-0
lines changed
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import pytest
2+
import math
3+
4+
import numpy as np
5+
6+
from opendbc.car.honda.interface import CarInterface
7+
from opendbc.car.honda.values import CAR
8+
from opendbc.car.vehicle_model import VehicleModel, dyn_ss_sol, create_dyn_state_matrices
9+
10+
11+
class TestVehicleModel:
12+
def setup_method(self):
13+
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
14+
self.VM = VehicleModel(CP)
15+
16+
def test_round_trip_yaw_rate(self):
17+
# TODO: fix VM to work at zero speed
18+
for u in np.linspace(1, 30, num=10):
19+
for roll in np.linspace(math.radians(-20), math.radians(20), num=11):
20+
for sa in np.linspace(math.radians(-20), math.radians(20), num=11):
21+
yr = self.VM.yaw_rate(sa, u, roll)
22+
new_sa = self.VM.get_steer_from_yaw_rate(yr, u, roll)
23+
24+
assert sa == pytest.approx(new_sa)
25+
26+
def test_dyn_ss_sol_against_yaw_rate(self):
27+
"""Verify that the yaw_rate helper function matches the results
28+
from the state space model."""
29+
30+
for roll in np.linspace(math.radians(-20), math.radians(20), num=11):
31+
for u in np.linspace(1, 30, num=10):
32+
for sa in np.linspace(math.radians(-20), math.radians(20), num=11):
33+
34+
# Compute yaw rate based on state space model
35+
_, yr1 = dyn_ss_sol(sa, u, roll, self.VM)
36+
37+
# Compute yaw rate using direct computations
38+
yr2 = self.VM.yaw_rate(sa, u, roll)
39+
assert float(yr1[0]) == pytest.approx(yr2)
40+
41+
def test_syn_ss_sol_simulate(self):
42+
"""Verifies that dyn_ss_sol matches a simulation"""
43+
44+
for roll in np.linspace(math.radians(-20), math.radians(20), num=11):
45+
for u in np.linspace(1, 30, num=10):
46+
A, B = create_dyn_state_matrices(u, self.VM)
47+
48+
# Convert to discrete time system
49+
dt = 0.01
50+
top = np.hstack((A, B))
51+
full = np.vstack((top, np.zeros_like(top))) * dt
52+
Md = sum([np.linalg.matrix_power(full, k) / math.factorial(k) for k in range(25)])
53+
Ad = Md[:A.shape[0], :A.shape[1]]
54+
Bd = Md[:A.shape[0], A.shape[1]:]
55+
56+
for sa in np.linspace(math.radians(-20), math.radians(20), num=11):
57+
inp = np.array([[sa], [roll]])
58+
59+
# Simulate for 1 second
60+
x1 = np.zeros((2, 1))
61+
for _ in range(100):
62+
x1 = Ad @ x1 + Bd @ inp
63+
64+
# Compute steady state solution directly
65+
x2 = dyn_ss_sol(sa, u, roll, self.VM)
66+
67+
np.testing.assert_almost_equal(x1, x2, decimal=3)

opendbc/car/vehicle_model.py

+230
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Dynamic bicycle model from "The Science of Vehicle Dynamics (2014), M. Guiggiani"
4+
5+
The state is x = [v, r]^T
6+
with v lateral speed [m/s], and r rotational speed [rad/s]
7+
8+
The input u is the steering angle [rad], and roll [rad]
9+
10+
The system is defined by
11+
x_dot = A*x + B*u
12+
13+
A depends on longitudinal speed, u [m/s], and vehicle parameters CP
14+
"""
15+
16+
import numpy as np
17+
from numpy.linalg import solve
18+
19+
from opendbc.car.structs import CarParams
20+
21+
ACCELERATION_DUE_TO_GRAVITY = 9.8
22+
23+
24+
class VehicleModel:
25+
def __init__(self, CP: CarParams):
26+
"""
27+
Args:
28+
CP: Car Parameters
29+
"""
30+
# for math readability, convert long names car params into short names
31+
self.m: float = CP.mass
32+
self.j: float = CP.rotationalInertia
33+
self.l: float = CP.wheelbase
34+
self.aF: float = CP.centerToFront
35+
self.aR: float = CP.wheelbase - CP.centerToFront
36+
self.chi: float = CP.steerRatioRear
37+
38+
self.cF_orig: float = CP.tireStiffnessFront
39+
self.cR_orig: float = CP.tireStiffnessRear
40+
self.update_params(1.0, CP.steerRatio)
41+
42+
def update_params(self, stiffness_factor: float, steer_ratio: float) -> None:
43+
"""Update the vehicle model with a new stiffness factor and steer ratio"""
44+
self.cF: float = stiffness_factor * self.cF_orig
45+
self.cR: float = stiffness_factor * self.cR_orig
46+
self.sR: float = steer_ratio
47+
48+
def steady_state_sol(self, sa: float, u: float, roll: float) -> np.ndarray:
49+
"""Returns the steady state solution.
50+
51+
If the speed is too low we can't use the dynamic model (tire slip is undefined),
52+
we then have to use the kinematic model
53+
54+
Args:
55+
sa: Steering wheel angle [rad]
56+
u: Speed [m/s]
57+
roll: Road Roll [rad]
58+
59+
Returns:
60+
2x1 matrix with steady state solution (lateral speed, rotational speed)
61+
"""
62+
if u > 0.1:
63+
return dyn_ss_sol(sa, u, roll, self)
64+
else:
65+
return kin_ss_sol(sa, u, self)
66+
67+
def calc_curvature(self, sa: float, u: float, roll: float) -> float:
68+
"""Returns the curvature. Multiplied by the speed this will give the yaw rate.
69+
70+
Args:
71+
sa: Steering wheel angle [rad]
72+
u: Speed [m/s]
73+
roll: Road Roll [rad]
74+
75+
Returns:
76+
Curvature factor [1/m]
77+
"""
78+
return (self.curvature_factor(u) * sa / self.sR) + self.roll_compensation(roll, u)
79+
80+
def curvature_factor(self, u: float) -> float:
81+
"""Returns the curvature factor.
82+
Multiplied by wheel angle (not steering wheel angle) this will give the curvature.
83+
84+
Args:
85+
u: Speed [m/s]
86+
87+
Returns:
88+
Curvature factor [1/m]
89+
"""
90+
sf = calc_slip_factor(self)
91+
return (1. - self.chi) / (1. - sf * u**2) / self.l
92+
93+
def get_steer_from_curvature(self, curv: float, u: float, roll: float) -> float:
94+
"""Calculates the required steering wheel angle for a given curvature
95+
96+
Args:
97+
curv: Desired curvature [1/m]
98+
u: Speed [m/s]
99+
roll: Road Roll [rad]
100+
101+
Returns:
102+
Steering wheel angle [rad]
103+
"""
104+
105+
return (curv - self.roll_compensation(roll, u)) * self.sR * 1.0 / self.curvature_factor(u)
106+
107+
def roll_compensation(self, roll: float, u: float) -> float:
108+
"""Calculates the roll-compensation to curvature
109+
110+
Args:
111+
roll: Road Roll [rad]
112+
u: Speed [m/s]
113+
114+
Returns:
115+
Roll compensation curvature [rad]
116+
"""
117+
sf = calc_slip_factor(self)
118+
119+
if abs(sf) < 1e-6:
120+
return 0
121+
else:
122+
return (ACCELERATION_DUE_TO_GRAVITY * roll) / ((1 / sf) - u**2)
123+
124+
def get_steer_from_yaw_rate(self, yaw_rate: float, u: float, roll: float) -> float:
125+
"""Calculates the required steering wheel angle for a given yaw_rate
126+
127+
Args:
128+
yaw_rate: Desired yaw rate [rad/s]
129+
u: Speed [m/s]
130+
roll: Road Roll [rad]
131+
132+
Returns:
133+
Steering wheel angle [rad]
134+
"""
135+
curv = yaw_rate / u
136+
return self.get_steer_from_curvature(curv, u, roll)
137+
138+
def yaw_rate(self, sa: float, u: float, roll: float) -> float:
139+
"""Calculate yaw rate
140+
141+
Args:
142+
sa: Steering wheel angle [rad]
143+
u: Speed [m/s]
144+
roll: Road Roll [rad]
145+
146+
Returns:
147+
Yaw rate [rad/s]
148+
"""
149+
return self.calc_curvature(sa, u, roll) * u
150+
151+
152+
def kin_ss_sol(sa: float, u: float, VM: VehicleModel) -> np.ndarray:
153+
"""Calculate the steady state solution at low speeds
154+
At low speeds the tire slip is undefined, so a kinematic
155+
model is used.
156+
157+
Args:
158+
sa: Steering angle [rad]
159+
u: Speed [m/s]
160+
VM: Vehicle model
161+
162+
Returns:
163+
2x1 matrix with steady state solution
164+
"""
165+
K = np.zeros((2, 1))
166+
K[0, 0] = VM.aR / VM.sR / VM.l * u
167+
K[1, 0] = 1. / VM.sR / VM.l * u
168+
return K * sa
169+
170+
171+
def create_dyn_state_matrices(u: float, VM: VehicleModel) -> tuple[np.ndarray, np.ndarray]:
172+
"""Returns the A and B matrix for the dynamics system
173+
174+
Args:
175+
u: Vehicle speed [m/s]
176+
VM: Vehicle model
177+
178+
Returns:
179+
A tuple with the 2x2 A matrix, and 2x2 B matrix
180+
181+
Parameters in the vehicle model:
182+
cF: Tire stiffness Front [N/rad]
183+
cR: Tire stiffness Front [N/rad]
184+
aF: Distance from CG to front wheels [m]
185+
aR: Distance from CG to rear wheels [m]
186+
m: Mass [kg]
187+
j: Rotational inertia [kg m^2]
188+
sR: Steering ratio [-]
189+
chi: Steer ratio rear [-]
190+
"""
191+
A = np.zeros((2, 2))
192+
B = np.zeros((2, 2))
193+
A[0, 0] = - (VM.cF + VM.cR) / (VM.m * u)
194+
A[0, 1] = - (VM.cF * VM.aF - VM.cR * VM.aR) / (VM.m * u) - u
195+
A[1, 0] = - (VM.cF * VM.aF - VM.cR * VM.aR) / (VM.j * u)
196+
A[1, 1] = - (VM.cF * VM.aF**2 + VM.cR * VM.aR**2) / (VM.j * u)
197+
198+
# Steering input
199+
B[0, 0] = (VM.cF + VM.chi * VM.cR) / VM.m / VM.sR
200+
B[1, 0] = (VM.cF * VM.aF - VM.chi * VM.cR * VM.aR) / VM.j / VM.sR
201+
202+
# Roll input
203+
B[0, 1] = -ACCELERATION_DUE_TO_GRAVITY
204+
205+
return A, B
206+
207+
208+
def dyn_ss_sol(sa: float, u: float, roll: float, VM: VehicleModel) -> np.ndarray:
209+
"""Calculate the steady state solution when x_dot = 0,
210+
Ax + Bu = 0 => x = -A^{-1} B u
211+
212+
Args:
213+
sa: Steering angle [rad]
214+
u: Speed [m/s]
215+
roll: Road Roll [rad]
216+
VM: Vehicle model
217+
218+
Returns:
219+
2x1 matrix with steady state solution
220+
"""
221+
A, B = create_dyn_state_matrices(u, VM)
222+
inp = np.array([[sa], [roll]])
223+
return -solve(A, B) @ inp # type: ignore
224+
225+
226+
def calc_slip_factor(VM: VehicleModel) -> float:
227+
"""The slip factor is a measure of how the curvature changes with speed
228+
it's positive for Oversteering vehicle, negative (usual case) otherwise.
229+
"""
230+
return VM.m * (VM.cF * VM.aF - VM.cR * VM.aR) / (VM.l**2 * VM.cF * VM.cR)

0 commit comments

Comments
 (0)