|
| 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