Difference between revisions of "Cruise control"
Line 53: | Line 53: | ||
{{Code block|Package initialization}} | {{Code block|Package initialization}} | ||
{{Code start}} | {{Code start}} | ||
− | + | import numpy as np | |
− | + | import matplotlib.pyplot as plt | |
− | + | from math import pi | |
− | + | import control as ct | |
{{Code end}} | {{Code end}} | ||
{{Code block|Vehicle model}} | {{Code block|Vehicle model}} | ||
{{Code start}} | {{Code start}} | ||
− | + | def vehicle_update(t, x, u, params={}): | |
− | + | """Vehicle dynamics for cruise control system. | |
− | + | ||
− | + | Parameters | |
− | + | ---------- | |
− | + | x : array | |
− | + | System state: car velocity in m/s | |
− | + | u : array | |
− | + | System input: [throttle, gear, road_slope], where throttle is | |
− | + | a float between 0 and 1, gear is an integer between 1 and 5, | |
− | + | and road_slope is in rad. | |
− | + | ||
− | + | Returns | |
− | + | ------- | |
− | + | float | |
− | + | Vehicle acceleration | |
− | + | """ | |
− | + | ||
− | + | from math import copysign, sin | |
− | + | sign = lambda x: copysign(1, x) # define the sign() function | |
− | + | ||
− | + | # Set up the system parameters | |
− | + | m = params.get('m', 1600.) # vehicle mass, kg | |
− | + | g = params.get('g', 9.8) # gravitational constant, m/s^2 | |
− | + | Cr = params.get('Cr', 0.01) # coefficient of rolling friction | |
− | + | Cd = params.get('Cd', 0.32) # drag coefficient | |
− | + | rho = params.get('rho', 1.3) # density of air, kg/m^3 | |
− | + | A = params.get('A', 2.4) # car area, m^2 | |
− | + | alpha = params.get( | |
− | + | 'alpha', [40, 25, 16, 12, 10]) # gear ratio / wheel radius | |
− | + | ||
− | + | # Define variables for vehicle state and inputs | |
− | + | v = x[0] # vehicle velocity | |
− | + | throttle = np.clip(u[0], 0, 1) # vehicle throttle | |
− | + | gear = u[1] # vehicle gear | |
− | + | theta = u[2] # road slope | |
− | + | ||
− | + | # Force generated by the engine | |
− | + | omega = alpha[int(gear)-1] * v # engine angular speed | |
− | + | F = alpha[int(gear)-1] * motor_torque(omega, params) * throttle | |
− | + | ||
− | + | # Disturbance forces | |
− | + | # | |
− | + | # The disturbance force Fd has three major components: Fg, the forces due | |
− | + | # to gravity; Fr, the forces due to rolling friction; and Fa, the | |
− | + | # aerodynamic drag. | |
− | + | ||
− | + | # Letting the slope of the road be \theta (theta), gravity gives the | |
− | + | # force Fg = m g sin \theta. | |
− | + | Fg = m * g * sin(theta) | |
− | + | ||
− | + | # A simple model of rolling friction is Fr = m g Cr sgn(v), where Cr is | |
− | + | # the coefficient of rolling friction and sgn(v) is the sign of v (±1) or | |
− | + | # zero if v = 0. | |
− | + | Fr = m * g * Cr * sign(v) | |
− | + | ||
− | + | # The aerodynamic drag is proportional to the square of the speed: Fa = | |
− | + | # 1/2 \rho Cd A |v| v, where \rho is the density of air, Cd is the | |
− | + | # shape-dependent aerodynamic drag coefficient, and A is the frontal area | |
− | + | # of the car. | |
− | + | Fa = 1/2 * rho * Cd * A * abs(v) * v | |
− | + | ||
− | + | # Final acceleration on the car | |
− | + | Fd = Fg + Fr + Fa | |
− | + | dv = (F - Fd) / m | |
− | + | ||
− | + | return dv | |
{{Code end}} | {{Code end}} | ||
{{Code block|Engine model}} | {{Code block|Engine model}} | ||
{{Code start}} | {{Code start}} | ||
− | + | def motor_torque(omega, params={}): | |
− | + | # Set up the system parameters | |
− | + | Tm = params.get('Tm', 190.) # engine torque constant | |
− | + | omega_m = params.get('omega_m', 420.) # peak engine angular speed | |
− | + | beta = params.get('beta', 0.4) # peak engine rolloff | |
− | + | ||
− | + | return np.clip(Tm * (1 - beta * (omega/omega_m - 1)**2), 0, None) | |
{{Code end}} | {{Code end}} | ||
{{Code block|Input/output model for the vehicle system}} | {{Code block|Input/output model for the vehicle system}} | ||
{{Code start}} | {{Code start}} | ||
− | + | vehicle = ct.NonlinearIOSystem( | |
− | + | vehicle_update, None, name='vehicle', | |
− | + | inputs = ('u', 'gear', 'theta'), outputs = ('v'), states=('v')) | |
− | + | {Code end}} | |
{{Code block|Input/output torque curves (plot)}} | {{Code block|Input/output torque curves (plot)}} | ||
{{Code start}} | {{Code start}} | ||
− | + | # Figure 4.2a - single torque curve as function of omega | |
− | + | omega_range = np.linspace(0, 700, 701) | |
− | + | plt.subplot(2, 2, 1) | |
− | + | plt.plot(omega_range, [motor_torque(w) for w in omega_range]) | |
− | + | plt.xlabel('Angular velocity $\omega$ [rad/s]') | |
− | + | plt.ylabel('Torque $T$ [Nm]') | |
− | + | plt.grid(True, linestyle='dotted') | |
− | + | ||
− | + | # Figure 4.2b - torque curves in different gears, as function of velocity | |
− | + | plt.subplot(2, 2, 2) | |
− | + | v_range = np.linspace(0, 70, 71) | |
− | + | alpha = [40, 25, 16, 12, 10] | |
− | + | for gear in range(5): | |
− | + | omega_range = alpha[gear] * v_range | |
− | + | plt.plot(v_range, [motor_torque(w) for w in omega_range], | |
− | + | color='blue', linestyle='solid') | |
− | + | ||
− | + | # Set up the axes and style | |
− | + | plt.axis([0, 70, 100, 200]) | |
− | + | plt.grid(True, linestyle='dotted') | |
− | + | ||
− | + | # Add labels | |
− | + | plt.text(11.5, 120, '$n$=1') | |
− | + | plt.text(24, 120, '$n$=2') | |
− | + | plt.text(42.5, 120, '$n$=3') | |
− | + | plt.text(58.5, 120, '$n$=4') | |
− | + | plt.text(58.5, 185, '$n$=5') | |
− | + | plt.xlabel('Velocity $v$ [m/s]') | |
− | + | plt.ylabel('Torque $T$ [Nm]') | |
− | + | ||
− | + | plt.suptitle('Torque curves for typical car engine'); | |
− | + | plt.tight_layout() | |
{{Code end}} | {{Code end}} | ||
{{Code block|PI controller}} | {{Code block|PI controller}} | ||
{{Code start}} | {{Code start}} | ||
− | + | # Construct a PI controller with rolloff, as a transfer function | |
− | + | Kp = 0.5 # proportional gain | |
− | + | Ki = 0.1 # integral gain | |
− | + | control_pi = ct.tf2io( | |
− | + | ct.TransferFunction([Kp, Ki], [1, 0.01*Ki/Kp]), | |
− | + | name='control', inputs='u', outputs='y') | |
− | + | ||
− | + | cruise_pi = ct.InterconnectedSystem( | |
− | + | (vehicle, control_pi), name='cruise', | |
− | + | connections = [('control.u', '-vehicle.v'), ('vehicle.u', 'control.y')], | |
− | + | inplist = ('control.u', 'vehicle.gear', 'vehicle.theta'), inputs = ('vref', 'gear', 'theta'), | |
− | + | outlist = ('vehicle.v', 'vehicle.u'), outputs = ('v', 'u')) | |
{{Code end}} | {{Code end}} | ||
{{Code block|Plotting function}} | {{Code block|Plotting function}} | ||
{{Code start}} | {{Code start}} | ||
− | + | # Define a generator for creating a "standard" cruise control plot | |
− | + | def cruise_plot(sys, t, y, t_hill=5, vref=20, antiwindup=False, linetype='b-', | |
− | + | subplots=[None, None]): | |
− | + | # Figure out the plot bounds and indices | |
− | + | v_min = vref-1.2; v_max = vref+0.5; v_ind = sys.find_output('v') | |
− | + | u_min = 0; u_max = 2 if antiwindup else 1; u_ind = sys.find_output('u') | |
− | + | ||
− | + | # Make sure the upper and lower bounds on v are OK | |
− | + | while max(y[v_ind]) > v_max: v_max += 1 | |
− | + | while min(y[v_ind]) < v_min: v_min -= 1 | |
− | + | ||
− | + | # Create arrays for return values | |
− | + | subplot_axes = subplots.copy() | |
− | + | ||
− | + | # Velocity profile | |
− | + | if subplot_axes[0] is None: | |
− | + | subplot_axes[0] = plt.subplot(2, 1, 1) | |
− | + | else: | |
− | + | plt.sca(subplots[0]) | |
− | + | plt.plot(t, y[v_ind], linetype) | |
− | + | plt.plot(t, vref*np.ones(t.shape), 'k-') | |
− | + | plt.plot([t_hill, t_hill], [v_min, v_max], 'k--') | |
− | + | plt.axis([0, t[-1], v_min, v_max]) | |
− | + | plt.xlabel('Time $t$ [s]') | |
− | + | plt.ylabel('Velocity $v$ [m/s]') | |
− | + | ||
− | + | # Commanded input profile | |
− | + | if subplot_axes[1] is None: | |
− | + | subplot_axes[1] = plt.subplot(2, 1, 2) | |
− | + | else: | |
− | + | plt.sca(subplots[1]) | |
− | + | plt.plot(t, y[u_ind], 'r--' if antiwindup else linetype) | |
− | + | plt.plot([t_hill, t_hill], [u_min, u_max], 'k--') | |
− | + | plt.axis([0, t[-1], u_min, u_max]) | |
− | + | plt.xlabel('Time $t$ [s]') | |
− | + | plt.ylabel('Throttle $u$') | |
− | + | ||
− | + | # Applied input profile | |
− | + | if antiwindup: | |
− | + | plt.plot(t, np.clip(y[u_ind], 0, 1), linetype) | |
− | + | plt.legend(['Commanded', 'Applied'], frameon=False) | |
− | + | ||
− | + | return subplot_axes | |
{{Code end}} | {{Code end}} | ||
{{Code block|Simulated responses}} | {{Code block|Simulated responses}} | ||
{{Code start}} | {{Code start}} | ||
− | + | # Define the time and input vectors | |
− | + | T = np.linspace(0, 25, 101) | |
− | + | vref = 20 * np.ones(T.shape) | |
− | + | gear = 4 * np.ones(T.shape) | |
− | + | theta0 = np.zeros(T.shape) | |
− | + | ||
− | + | # Now simulate the effect of a hill at t = 5 seconds | |
− | + | plt.figure() | |
− | + | plt.suptitle('Response to change in road slope') | |
− | + | theta_hill = np.array([ | |
− | + | 0 if t <= 5 else | |
− | + | 4./180. * pi * (t-5) if t <= 6 else | |
− | + | 4./180. * pi for t in T]) | |
− | + | ||
− | + | subplots = [None, None] | |
− | + | linecolor = ['red', 'blue', 'green'] | |
− | + | handles = [] | |
− | + | for i, m in enumerate([1200, 1600, 2000]): | |
− | + | # Compute the equilibrium state for the system | |
− | + | X0, U0 = ct.find_eqpt( | |
− | + | cruise_tf, [vref[0], 0], [vref[0], gear[0], theta0[0]], | |
− | + | iu=[1, 2], y0=[vref[0], 0], iy=[0], params={'m':m}) | |
− | + | ||
− | + | t, y = ct.input_output_response( | |
− | + | cruise_tf, T, [vref, gear, theta_hill], X0, params={'m':m}) | |
− | + | ||
− | + | subplots = cruise_plot(cruise_tf, t, y, t_hill=5, subplots=subplots, | |
− | + | linetype=linecolor[i][0] + '-') | |
− | + | handles.append(mlines.Line2D([], [], color=linecolor[i], linestyle='-', | |
− | + | label="m = %d" % m)) | |
− | + | ||
− | + | # Add labels to the plots | |
− | + | plt.sca(subplots[0]) | |
− | + | plt.ylabel('Speed [m/s]') | |
− | + | plt.legend(handles=handles, frameon=False, loc='lower right'); | |
− | + | ||
− | + | plt.sca(subplots[1]) | |
− | + | plt.ylabel('Throttle') | |
− | + | plt.xlabel('Time [s]'); | |
{{Code end}} | {{Code end}} | ||
Revision as of 17:50, 29 December 2020
This page documents the cruise control system that is used as a running example throughout the text. A detailed description of the dynamics of this system are presented in Chapter 4 - Examples. This page contains a description of the system, including the models and commands used to generate some of the plots in the text.
Introduction
Cruise control is the term used to describe a control system that regulates the speed of an automobile. Cruise control was commercially introduced in 1958 as an option on the Chrysler Imperial. The basic operation of a cruise controller is to sense the speed of the vehicle, compare this speed to a desired reference, and then accelerate or decelerate the car as required. The figure to the right shows a block diagram of this feedback system.
A simple control algorithm for controlling the speed is to use a "proportional plus integral" feedback. In this algorithm, we choose the amount of gas flowing to the engine based on both the error between the current and desired speed, and the integral of that error. The plot on the right shows the results of this feedback for a step change in the desired speed and a variety of different masses for the car (which might result from having a different number of passengers or towing a trailer). Notice that independent of the mass (which varies by 25% of the total weight of the car), the steady state speed of the vehicle always approaches the desired speed and achieves that speed within approximately 10-15 seconds. Thus the performance of the system is robust with respect to this uncertainty.
Dynamic model
To develop a mathematical model we start with a force balance for the car body. Let be the speed of the car, the total mass (including passengers), the force generated by the contact of the wheels with the road, and the disturbance force due to gravity, friction and aerodynamic drag. The equation of motion of the car is simply
The force is generated by the engine, whose torque is proportional to the rate of fuel injection, which is itself proportional to a control signal that controls the throttle position. The torque also depends on engine speed . A simple representation of the torque at full throttle is given by the torque curve
where the maximum torque is obtained at engine speed . Typical parameters are Nm, = 420 rad/s (about 4000 RPM) and .
Let be the gear ratio and the wheel radius. The engine speed is related to the velocity through the expression
and the driving force can be written as
Typical values of for gears 1 through 5 are , , , and . The inverse of has a physical interpretation as the effective wheel radius. The figure to the right shows the torque as a function of vehicle speed. The figure shows that the effect of the gear is to "flatten" the torque curve so that an almost full torque can be obtained almost over the whole speed range.
The disturbance force has three major components: , the forces due to gravity; , the forces due to rolling friction; and , the aerodynamic drag. Letting the slope of the road be , gravity gives the force , where is the gravitational constant. A simple model of rolling friction is
where is the coefficient of rolling friction and sgn() is the sign of or zero if . A typical value for the coefficient of rolling friction is . Finally, the aerodynamic drag is proportional to the square of the speed:
where is the density of air, is the shape-dependent aerodynamic drag coefficient and is the frontal area of the car. Typical parameters are 1.3 k/m, and 2.4 m.
Python code
The model for the system above can be built using the Python Control Toolbox. The code blocks in this section can be used to generate the plots above.