1+ import numpy as np
2+ from math import sin , cos , atan2
3+
4+ # These imports match your existing project structure
5+ from ...mathutils .control import PID
6+ from ...utils import settings
7+ from ...mathutils import transforms
8+ from ...knowledge .vehicle .geometry import front2steer
9+ from ...knowledge .vehicle .dynamics import acceleration_to_pedal_positions
10+ from ...state .vehicle import VehicleState , ObjectFrameEnum
11+ from ...state .trajectory import Path , Trajectory
12+ from ..interface .gem import GEMVehicleCommand
13+ from ..component import Component
14+
15+ #####################################
16+ # 1. Angle normalization
17+ #####################################
18+ def normalise_angle (angle ):
19+ """Wraps an angle to the [-pi, pi] range."""
20+ while angle > np .pi :
21+ angle -= 2.0 * np .pi
22+ while angle < - np .pi :
23+ angle += 2.0 * np .pi
24+ return angle
25+
26+ #####################################
27+ # 2. Stanley-based controller with longitudinal PID
28+ #####################################
29+ class Stanley (object ):
30+ """
31+ A Stanley controller that handles lateral control (steering)
32+ plus a basic longitudinal control (PID + optional feedforward).
33+ It has been modified to reduce oscillations by:
34+ 1) Lower gains
35+ 2) Steering damping
36+ 3) Low-pass filter on steering
37+ 4) Gentler speed logic when cornering
38+ """
39+
40+ def __init__ (
41+ self ,
42+ control_gain = None ,
43+ softening_gain = None ,
44+ desired_speed = None
45+ ):
46+ """
47+ :param control_gain: Stanley lateral control gain k (lowered from the default to reduce overshoot).
48+ :param softening_gain: Softening gain k_soft.
49+ :param yaw_rate_gain: Yaw-rate gain k_yaw_rate (helps reduce oscillations).
50+ :param steering_damp_gain: Steering damping gain k_damp_steer.
51+ :param desired_speed: Desired speed (float) or 'path'/'trajectory'.
52+ """
53+
54+ # 1) Lower Gains
55+ self .k = control_gain if control_gain is not None else settings .get ('control.stanley.control_gain' )
56+ self .k_soft = softening_gain if softening_gain is not None else settings .get ('control.stanley.softening_gain' )
57+
58+ # Typically, this is the max front-wheel steering angle in radians
59+ self .max_steer = settings .get ('vehicle.geometry.max_wheel_angle' )
60+ self .wheelbase = settings .get ('vehicle.geometry.wheelbase' )
61+
62+ # Basic longitudinal constraints
63+ self .speed_limit = settings .get ('vehicle.limits.max_speed' )
64+ self .max_accel = settings .get ('vehicle.limits.max_acceleration' )
65+ self .max_decel = settings .get ('vehicle.limits.max_deceleration' )
66+
67+ # PID for longitudinal speed tracking
68+ p = settings .get ('control.longitudinal_control.pid_p' )
69+ d = settings .get ('control.longitudinal_control.pid_d' )
70+ i = settings .get ('control.longitudinal_control.pid_i' )
71+ self .pid_speed = PID (p , d , i , windup_limit = 20 )
72+
73+ # Speed source: numeric or derived from path/trajectory
74+ if desired_speed is not None :
75+ self .desired_speed_source = desired_speed
76+ else :
77+ self .desired_speed_source = settings .get ('control.stanley.desired_speed' , 'path' )
78+
79+ if isinstance (self .desired_speed_source , (int , float )):
80+ self .desired_speed = self .desired_speed_source
81+ else :
82+ self .desired_speed = None
83+
84+ # For path/trajectory
85+ self .path_arg = None
86+ self .path = None
87+ self .trajectory = None
88+ self .current_path_parameter = 0.0
89+ self .current_traj_parameter = 0.0
90+ self .t_last = None
91+
92+ def set_path (self , path : Path ):
93+ """Sets the path or trajectory to track."""
94+ if path == self .path_arg :
95+ return
96+ self .path_arg = path
97+
98+ # If the path has more than 2D, reduce to (x,y)
99+ if len (path .points [0 ]) > 2 :
100+ path = path .get_dims ([0 ,1 ])
101+
102+ # If no timing info, we can't rely on 'path'/'trajectory' speed
103+ if not isinstance (path , Trajectory ):
104+ self .path = path .arc_length_parameterize ()
105+ self .trajectory = None
106+ self .current_traj_parameter = 0.0
107+ if self .desired_speed_source in ['path' , 'trajectory' ]:
108+ raise ValueError ("Stanley: Provided path has no timing. Either provide a Trajectory or set a constant desired_speed." )
109+ else :
110+ self .path = path .arc_length_parameterize ()
111+ self .trajectory = path
112+ self .current_traj_parameter = self .trajectory .domain ()[0 ]
113+
114+ self .current_path_parameter = 0.0
115+
116+ def _find_front_axle_position (self , x , y , yaw ):
117+ """Compute front-axle world position from the center/rear and yaw."""
118+ fx = x + self .wheelbase * cos (yaw )
119+ fy = y + self .wheelbase * sin (yaw )
120+ return fx , fy
121+
122+ def compute (self , state : VehicleState , component : Component = None ):
123+ """Compute the control outputs: (longitudinal acceleration, front wheel angle)."""
124+ t = state .pose .t
125+ if self .t_last is None :
126+ self .t_last = t
127+ dt = t - self .t_last
128+
129+ # Current vehicle states
130+ curr_x = state .pose .x
131+ curr_y = state .pose .y
132+ curr_yaw = state .pose .yaw if state .pose .yaw is not None else 0.0
133+ speed = state .v
134+
135+ if self .path is None :
136+ if component :
137+ component .debug_event ("No path provided to Stanley controller. Doing nothing." )
138+ return (0.0 , 0.0 )
139+
140+ # Ensure same frame
141+ if self .path .frame != state .pose .frame :
142+ if component :
143+ component .debug_event (f"Transforming path from { self .path .frame .name } to { state .pose .frame .name } " )
144+ self .path = self .path .to_frame (state .pose .frame , current_pose = state .pose )
145+
146+ if self .trajectory and self .trajectory .frame != state .pose .frame :
147+ if component :
148+ component .debug_event (f"Transforming trajectory from { self .trajectory .frame .name } to { state .pose .frame .name } " )
149+ self .trajectory = self .trajectory .to_frame (state .pose .frame , current_pose = state .pose )
150+
151+ # 1) Closest point
152+ fx , fy = self ._find_front_axle_position (curr_x , curr_y , curr_yaw )
153+ search_start = self .current_path_parameter - 5.0
154+ search_end = self .current_path_parameter + 5.0
155+ closest_dist , closest_parameter = self .path .closest_point_local ((fx , fy ), [search_start , search_end ])
156+ self .current_path_parameter = closest_parameter
157+
158+ # 2) Path heading
159+ target_x , target_y = self .path .eval (closest_parameter )
160+ tangent = self .path .eval_tangent (closest_parameter )
161+ path_yaw = atan2 (tangent [1 ], tangent [0 ])
162+ desired_x = target_x
163+ desired_y = target_y
164+ # 3) Lateral error
165+ dx = fx - target_x
166+ dy = fy - target_y
167+ left_vec = np .array ([sin (curr_yaw ), - cos (curr_yaw )])
168+ cross_track_error = np .sign (np .dot (np .array ([dx , dy ]), left_vec )) * closest_dist
169+
170+ # 4) Heading error
171+ yaw_error = normalise_angle (path_yaw - curr_yaw )
172+
173+ # 5) Standard Stanley terms
174+ cross_term = atan2 (self .k * cross_track_error , self .k_soft + speed )
175+ desired_steering_angle = yaw_error + cross_term
176+
177+ desired_speed = self .desired_speed
178+ feedforward_accel = 0.0
179+
180+ if self .trajectory and self .desired_speed_source in ['path' , 'trajectory' ]:
181+ if len (self .trajectory .points ) < 2 or self .current_path_parameter >= self .path .domain ()[1 ]:
182+ # End of trajectory -> stop
183+ if component :
184+ component .debug_event ("Stanley: Past the end of trajectory, stopping." )
185+ desired_speed = 0.0
186+ feedforward_accel = - 2.0
187+ else :
188+ if self .desired_speed_source == 'path' :
189+ current_trajectory_time = self .trajectory .parameter_to_time (self .current_path_parameter )
190+ else :
191+ self .current_traj_parameter += dt
192+ current_trajectory_time = self .current_traj_parameter
193+
194+ deriv = self .trajectory .eval_derivative (current_trajectory_time )
195+ desired_speed = min (np .linalg .norm (deriv ), self .speed_limit )
196+
197+ difference_dt = 0.1
198+ future_t = current_trajectory_time + difference_dt
199+ if future_t > self .trajectory .domain ()[1 ]:
200+ future_t = self .trajectory .domain ()[1 ]
201+ future_deriv = self .trajectory .eval_derivative (future_t )
202+ next_desired_speed = min (np .linalg .norm (future_deriv ), self .speed_limit )
203+ feedforward_accel = (next_desired_speed - desired_speed ) / difference_dt
204+ feedforward_accel = np .clip (feedforward_accel , - self .max_decel , self .max_accel )
205+ else :
206+ if desired_speed is None :
207+ desired_speed = 4.0
208+
209+ # Cross-track-based slowdown (less aggressive than before).
210+ desired_speed *= np .exp (- abs (cross_track_error ) * 0.6 )
211+
212+ # Clip to speed limit
213+ if desired_speed > self .speed_limit :
214+ desired_speed = self .speed_limit
215+
216+ # PID for longitudinal control
217+ speed_error = desired_speed - speed
218+ output_accel = self .pid_speed .advance (e = speed_error , t = t , feedforward_term = feedforward_accel )
219+
220+ # Clip acceleration
221+ if output_accel > self .max_accel :
222+ output_accel = self .max_accel
223+ elif output_accel < - self .max_decel :
224+ output_accel = - self .max_decel
225+
226+ # Avoid negative accel when fully stopped
227+ if desired_speed == 0 and abs (speed ) < 0.01 and output_accel < 0 :
228+ output_accel = 0.0
229+
230+ # Debug
231+ if component is not None :
232+ # component.debug("Stanley: fx, fy", (fx, fy))
233+ component .debug ('curr pt' ,(curr_x ,curr_y ))
234+ component .debug ("desired_x" ,desired_x )
235+ component .debug ("desired_y" ,desired_y )
236+ component .debug ("Stanley: path param" , self .current_path_parameter )
237+ component .debug ("Stanley: crosstrack dist" , closest_dist )
238+ component .debug ("crosstrack error" , cross_track_error )
239+ component .debug ("Stanley: yaw_error" , yaw_error )
240+ component .debug ('steering_angle' , desired_steering_angle )
241+ component .debug ("Stanley: desired_speed (m/s)" , desired_speed )
242+ component .debug ("Stanley: feedforward_accel (m/s^2)" , feedforward_accel )
243+ component .debug ("Stanley: output_accel (m/s^2)" , output_accel )
244+
245+ if output_accel > self .max_accel :
246+ output_accel = self .max_accel
247+
248+ if output_accel < - self .max_decel :
249+ output_accel = - self .max_decel
250+
251+ self .t_last = t
252+ return (output_accel , desired_steering_angle )
253+
254+ #####################################
255+ # 3. Tracker component
256+ #####################################
257+ class StanleyTrajectoryTracker (Component ):
258+ """
259+ A trajectory-tracking Component that uses the above Stanley controller
260+ for lateral control plus PID-based longitudinal control.
261+ It now includes measures to mitigate oscillations.
262+ """
263+
264+ def __init__ (self , vehicle_interface = None , ** kwargs ):
265+ """
266+ :param vehicle_interface: The low-level interface to send commands to the vehicle.
267+ :param kwargs: Optional parameters to pass into the Stanley(...) constructor.
268+ """
269+ self .stanley = Stanley (** kwargs )
270+ self .vehicle_interface = vehicle_interface
271+
272+ def rate (self ):
273+ """Control frequency in Hz."""
274+ return 50.0
275+
276+ def state_inputs (self ):
277+ """
278+ Required state inputs:
279+ - Vehicle state
280+ - Trajectory
281+ """
282+ return ["vehicle" , "trajectory" ]
283+
284+ def state_outputs (self ):
285+ """No direct output state here."""
286+ return []
287+
288+ def update (self , vehicle : VehicleState , trajectory : Trajectory ):
289+ """
290+ Per control cycle:
291+ 1) Set the path/trajectory
292+ 2) Compute (acceleration, front wheel angle)
293+ 3) Convert front wheel angle to steering wheel angle (if necessary)
294+ 4) Send command to the vehicle
295+ """
296+ self .stanley .set_path (trajectory )
297+ accel , f_delta = self .stanley .compute (vehicle , self )
298+
299+ # If your low-level interface expects steering wheel angle:
300+ steering_angle = front2steer (f_delta )
301+ steering_angle = np .clip (
302+ steering_angle ,
303+ settings .get ('vehicle.geometry.min_steering_angle' , - 0.5 ),
304+ settings .get ('vehicle.geometry.max_steering_angle' , 0.5 )
305+ )
306+
307+ cmd = self .vehicle_interface .simple_command (accel , steering_angle , vehicle )
308+ self .vehicle_interface .send_command (cmd )
309+
310+ def healthy (self ):
311+ """Optional: check if the controller has a valid path."""
312+ return self .stanley .path is not None
0 commit comments