@@ -54,18 +54,16 @@ def __init__(
5454 :param desired_speed: Desired speed (float) or 'path'/'trajectory'.
5555 """
5656
57- # 1) Lower Gains
5857 self .k = control_gain if control_gain is not None else settings .get ('control.stanley.control_gain' )
5958 self .k_soft = softening_gain if softening_gain is not None else settings .get ('control.stanley.softening_gain' )
6059
61- # Typically, this is the max front-wheel steering angle in radians
6260 self .max_steer = settings .get ('vehicle.geometry.max_wheel_angle' )
6361 self .wheelbase = settings .get ('vehicle.geometry.wheelbase' )
6462
65- # Basic longitudinal constraints
6663 self .speed_limit = settings .get ('vehicle.limits.max_speed' )
67- self .max_accel = settings .get ('vehicle.limits.max_acceleration' )
68- self .max_decel = settings .get ('vehicle.limits.max_deceleration' )
64+ self .reverse_speed_limit = settings .get ('vehicle.limits.max_reverse_speed' )
65+ self .max_accel = settings .get ('vehicle.limits.max_acceleration' )
66+ self .max_decel = settings .get ('vehicle.limits.max_deceleration' )
6967
7068 # PID for longitudinal speed tracking
7169 p = settings .get ('control.longitudinal_control.pid_p' )
@@ -91,27 +89,29 @@ def __init__(
9189 d = settings .get ('control.racing.longitudinal_control.pid_d' )
9290 i = settings .get ('control.racing.longitudinal_control.pid_i' )
9391
94- # For path/trajectory
92+ # Initialize state variables
9593 self .path_arg = None
9694 self .path = None
9795 self .trajectory = None
9896 self .current_path_parameter = 0.0
9997 self .current_traj_parameter = 0.0
10098 self .t_last = None
99+ self .reverse = None
100+ self .sharp_turn = False
101101
102102 self .launch_control = launch_control
103103
104104 def set_path (self , path : Path ):
105- """ Sets the path or trajectory to track."""
105+ # Sets the path for the controller.
106106 if path == self .path_arg :
107107 return
108108 self .path_arg = path
109109
110- # If the path has more than 2D, reduce to (x,y)
110+ # Ensure path is 2D
111111 if len (path .points [0 ]) > 2 :
112112 path = path .get_dims ([0 ,1 ])
113113
114- # If no timing info, we can't rely on 'path'/'trajectory' speed
114+ # Parameterize path by arc length
115115 if not isinstance (path , Trajectory ):
116116 self .path = path .arc_length_parameterize ()
117117 self .trajectory = None
@@ -257,24 +257,29 @@ def _check_sharp_turn_ahead(self, lookahead_s=3.0, threshold_angle=np.pi/2.0, nu
257257 return False
258258
259259 def compute (self , state : VehicleState , component : Component = None ):
260- """Compute the control outputs: (longitudinal acceleration, front wheel angle)."""
260+ # Computes the control commands ( acceleration and steering angle).
261261 t = state .pose .t
262262 if self .t_last is None :
263263 self .t_last = t
264- dt = t - self .t_last
264+ dt = 0
265+ else :
266+ dt = t - self .t_last
265267
266268 # Current vehicle states
267269 curr_x = state .pose .x
268270 curr_y = state .pose .y
269271 curr_yaw = state .pose .yaw if state .pose .yaw is not None else 0.0
270272 speed = state .v
271273
274+ # Handle no path case
272275 if self .path is None :
273276 if component :
274277 component .debug_event ("No path provided to Stanley controller. Doing nothing." )
278+ self .t_last = t
279+ self .pid_speed .reset ()
275280 return (0.0 , 0.0 )
276281
277- # Ensure same frame
282+ # Transform path and trajectory to vehicle frame if necessary
278283 if self .path .frame != state .pose .frame :
279284 if component :
280285 component .debug_event (f"Transforming path from { self .path .frame .name } to { state .pose .frame .name } " )
@@ -285,103 +290,190 @@ def compute(self, state: VehicleState, component: Component = None):
285290 component .debug_event (f"Transforming trajectory from { self .trajectory .frame .name } to { state .pose .frame .name } " )
286291 self .trajectory = self .trajectory .to_frame (state .pose .frame , current_pose = state .pose )
287292
288- # 1) Closest point
289- fx , fy = self ._find_front_axle_position (curr_x , curr_y , curr_yaw )
290- search_start = self .current_path_parameter - 10
291- search_end = self .current_path_parameter + 10
293+ # Initialize reverse direction if not set
294+ if self .reverse is None :
295+ self .reverse = self .initialize_state_and_direction (state )
296+
297+ # Find front or rear axle position based on direction
298+ if self .reverse :
299+ fx , fy = self ._find_rear_axle_position (curr_x , curr_y , curr_yaw )
300+ else :
301+ fx , fy = self ._find_front_axle_position (curr_x , curr_y , curr_yaw )
302+
303+ # Find the closest point on the path
304+ search_start = self .current_path_parameter - 10.0
305+ search_end = self .current_path_parameter + 10.0
292306 closest_dist , closest_parameter = self .path .closest_point_local ((fx , fy ), [search_start , search_end ])
293- self .current_path_parameter = closest_parameter
307+ self .current_path_parameter = closest_parameter
294308
295309 # 2) Path heading
296- target_x , target_y = self . path . eval ( closest_parameter )
297- index , u = self .path .time_to_index ( closest_parameter )
298- tangent = self .path .eval_tangent (closest_parameter )
310+ # Get target point and tangent on the path
311+ target_x , target_y = self .path .eval ( self . current_path_parameter )
312+ tangent = self .path .eval_tangent (self . current_path_parameter )
299313 path_yaw = atan2 (tangent [1 ], tangent [0 ])
300- desired_x = target_x
301- desired_y = target_y
302- # 3) Lateral error
314+
303315 dx = fx - target_x
304316 dy = fy - target_y
305- left_vec = np . array ([ sin ( curr_yaw ), - cos ( curr_yaw )])
306- cross_track_error = np . sign ( np . dot ( np . array ([ dx , dy ]), left_vec )) * closest_dist
317+ desired_x = target_x
318+ desired_y = target_y
307319
308- # 4) Heading error
320+ # Check for sharp turn ahead and potentially switch to reverse
321+ if not self .reverse :
322+ try :
323+ is_sharp_turn_ahead = self ._check_sharp_turn_ahead (
324+ lookahead_s = 2.0 ,
325+ threshold_angle = np .pi / 2.0
326+ )
327+ except Exception as e :
328+ is_sharp_turn_ahead = False
329+ if is_sharp_turn_ahead and not self .sharp_turn :
330+ self .sharp_turn = True
331+ use_reverse = self .is_target_behind_vehicle (state .pose , (target_x , target_y ))
332+ if use_reverse and self .sharp_turn :
333+ self .reverse = True
334+ self .sharp_turn = False
335+
336+ # Calculate cross-track error based on direction
337+ if self .reverse :
338+ cross_track_error = dx * (- tangent [1 ]) + dy * tangent [0 ]
339+ else :
340+ left_vec = np .array ([sin (curr_yaw ), - cos (curr_yaw )])
341+ cross_track_error = np .sign (np .dot (np .array ([dx , dy ]), left_vec )) * closest_dist
342+
343+ # Calculate yaw error
309344 yaw_error = normalise_angle (path_yaw - curr_yaw )
310345
311- # 5) Standard Stanley terms
312- cross_term = atan2 (self .k * cross_track_error , self .k_soft + speed )
313- desired_steering_angle = yaw_error + cross_term
314346
315- desired_speed = self .desired_speed
347+ # ~~~~~~~~298
348+ if (self .desired_speed ):
349+ desired_speed = abs (self .desired_speed )
350+ else :
351+ desired_speed = None
316352 feedforward_accel = 0.0
317-
318- if self .trajectory and self .desired_speed_source in ['path' , 'trajectory' , 'racing' ]:
319- if len (self .trajectory .points ) < 2 or self .current_path_parameter >= self .path .domain ()[1 ]:
320- # End of trajectory -> stop
321- if component :
322- component .debug_event ("Stanley: Past the end of trajectory, stopping." )
353+
354+ # Stanley control logic for reverse direction
355+ if self .reverse :
356+ heading_term = - yaw_error
357+ cross_term_input = self .k * (- cross_track_error ) / (self .k_soft + abs (speed ))
358+ cross_term = atan2 (cross_term_input , 1.0 )
359+ desired_steering_angle = heading_term + cross_term
360+
361+ # Determine desired speed from trajectory in reverse
362+ if self .trajectory and self .desired_speed_source in ['path' , 'trajectory' ]:
363+ current_trajectory_param_for_eval = self .current_path_parameter
364+ if hasattr (self .trajectory , 'parameter_to_time' ):
365+ current_trajectory_param_for_eval = self .trajectory .parameter_to_time (self .current_path_parameter )
366+
367+ if self .trajectory is not None :
368+ deriv = self .trajectory .eval_derivative (current_trajectory_param_for_eval )
369+ path_speed_magnitude = np .linalg .norm (deriv )
370+ else :
371+ path_speed_magnitude = 0.0
372+
373+ desired_speed = min (path_speed_magnitude , self .reverse_speed_limit )
374+
375+ desired_speed = desired_speed * - 1.0
376+
377+ # Handle reaching the start in reverse
378+ is_at_start_backward = self .current_path_parameter <= self .path .domain ()[0 ] + 1e-3
379+
380+ if is_at_start_backward :
323381 desired_speed = 0.0
324- feedforward_accel = - 2.0
325- desired_steering_angle = 0.0
382+ feedforward_accel = - 2.0 * - 1.0
326383
384+ # Calculate feedforward acceleration for reverse
327385 else :
328- if self .desired_speed_source == 'path' :
329- current_trajectory_time = self .trajectory .parameter_to_time (self .current_path_parameter )
330- else :
331- self .current_traj_parameter += dt
332- current_trajectory_time = self .current_traj_parameter
333- print (current_trajectory_time )
386+ difference_dt = 0.1
387+ current_speed_abs = abs (speed )
388+ if current_speed_abs < 0.1 : estimated_path_step = 0.1 * difference_dt * - 1.0
389+ else : estimated_path_step = current_speed_abs * difference_dt * - 1.0
334390
335- deriv = self .trajectory . eval_derivative ( current_trajectory_time )
391+ future_parameter = self .current_path_parameter + estimated_path_step
336392
337- # deriv 0 at time 0
338- if np .isnan (deriv [0 ]):
339- desired_speed = 0.0
393+ future_parameter = np .clip (future_parameter , self .path .domain ()[0 ], self .path .domain ()[- 1 ])
394+
395+ future_trajectory_param_for_eval = future_parameter
396+ if hasattr (self .trajectory , 'parameter_to_time' ):
397+ future_trajectory_param_for_eval = self .trajectory .parameter_to_time (future_parameter )
398+
399+ if self .trajectory is not None :
400+ future_deriv = self .trajectory .eval_derivative (future_trajectory_param_for_eval )
401+ next_path_speed_magnitude = min (np .linalg .norm (future_deriv ), self .speed_limit )
340402 else :
341- desired_speed = min (np .linalg .norm (deriv ), self .speed_limit )
403+ next_path_speed_magnitude = 0.0
404+
405+ next_desired_speed = next_path_speed_magnitude * - 1.0
342406
343- difference_dt = 0.1
344- future_t = current_trajectory_time + difference_dt
345- if future_t > self .trajectory .domain ()[1 ]:
346- future_t = self .trajectory .domain ()[1 ]
347- future_deriv = self .trajectory .eval_derivative (future_t )
348- next_desired_speed = min (np .linalg .norm (future_deriv ), self .speed_limit )
349407 feedforward_accel = (next_desired_speed - desired_speed ) / difference_dt
350408 feedforward_accel = np .clip (feedforward_accel , - self .max_decel , self .max_accel )
409+
410+ # Reduce speed based on cross-track error in reverse
411+ desired_speed_magnitude_slowed = abs (desired_speed ) * np .exp (- abs (cross_track_error ) * 0.6 )
412+ desired_speed = desired_speed_magnitude_slowed * - 1.0 if desired_speed_magnitude_slowed != 0 else 0.0
413+
414+ # Stanley control logic for forward direction
351415 else :
352- if desired_speed is None :
353- desired_speed = 4.0
416+ cross_term = atan2 (self .k * cross_track_error , self .k_soft + speed )
417+ desired_steering_angle = yaw_error + cross_term
418+
419+ # Determine desired speed from trajectory in forward
420+ if self .trajectory and self .desired_speed_source in ['path' , 'trajectory' , 'racing' ]:
421+ if len (self .trajectory .points ) < 2 or self .current_path_parameter >= self .path .domain ()[1 ]:
422+ if component :
423+ component .debug_event ("Stanley: Past the end of trajectory, stopping." )
424+ desired_speed = 0.0
425+ feedforward_accel = - 2.0
426+ else :
427+ if self .desired_speed_source == 'path' :
428+ current_trajectory_time = self .trajectory .parameter_to_time (self .current_path_parameter )
429+ else :
430+ self .current_traj_parameter += dt
431+ current_trajectory_time = self .current_traj_parameter
432+
433+ deriv = self .trajectory .eval_derivative (current_trajectory_time )
434+ desired_speed = min (np .linalg .norm (deriv ), self .speed_limit )
354435
355- # Cross-track-based slowdown (less aggressive than before). More aggressive for racing
356- if self .desired_speed_source in ['racing' ]:
357- desired_speed *= np .exp (- abs (cross_track_error ) * 1.5 )
436+ # Calculate feedforward acceleration for forward
437+ difference_dt = 0.1
438+ future_t = current_trajectory_time + difference_dt
439+ if future_t > self .trajectory .domain ()[1 ]:
440+ future_t = self .trajectory .domain ()[1 ]
441+ future_deriv = self .trajectory .eval_derivative (future_t )
442+ next_desired_speed = min (np .linalg .norm (future_deriv ), self .speed_limit )
443+ feedforward_accel = (next_desired_speed - desired_speed ) / difference_dt
444+ feedforward_accel = np .clip (feedforward_accel , - self .max_decel , self .max_accel )
445+ # Use constant desired speed if no trajectory or source is not path/trajectory
358446 else :
447+ if desired_speed is None :
448+ desired_speed = 4.0
449+
450+ # Reduce speed based on cross-track error in forward
359451 desired_speed *= np .exp (- abs (cross_track_error ) * 0.6 )
360452
361- # Clip to speed limit
362- if desired_speed > self .speed_limit :
453+ # Apply speed limits
454+ if self .reverse and abs (desired_speed ) > self .reverse_speed_limit :
455+ desired_speed = self .reverse_speed_limit * - 1.0 if desired_speed != 0 else 0.0
456+ elif not self .reverse and desired_speed > self .speed_limit :
363457 desired_speed = self .speed_limit
364458
365- # PID for longitudinal control
459+ # Longitudinal control using PID
366460 speed_error = desired_speed - speed
367461 output_accel = self .pid_speed .advance (e = speed_error , t = t , feedforward_term = feedforward_accel )
368-
369- # Clip acceleration
462+ # Clamp acceleration to limits
370463 if output_accel > self .max_accel :
371464 output_accel = self .max_accel
372465 elif output_accel < - self .max_decel :
373466 output_accel = - self .max_decel
374467
375- # Avoid negative accel when fully stopped
468+ # Stop acceleration if at target speed and decelerating
376469 if desired_speed == 0 and abs (speed ) < 0.01 and output_accel < 0 :
377470 output_accel = 0.0
378471
379- # Debug
472+ # Debugging information
380473 if component is not None :
381- # component.debug("Stanley: fx, fy", (fx, fy))
382474 component .debug ('curr pt' ,(curr_x ,curr_y ))
383- component .debug ("desired_x" ,desired_x )
384- component .debug ("desired_y" ,desired_y )
475+ component .debug ("desired_x" ,target_x )
476+ component .debug ("desired_y" ,target_y )
385477 component .debug ("Stanley: path param" , self .current_path_parameter )
386478 component .debug ("Stanley: crosstrack dist" , closest_dist )
387479 component .debug ("crosstrack error" , cross_track_error )
@@ -390,15 +482,6 @@ def compute(self, state: VehicleState, component: Component = None):
390482 component .debug ("Stanley: desired_speed (m/s)" , desired_speed )
391483 component .debug ("Stanley: feedforward_accel (m/s^2)" , feedforward_accel )
392484 component .debug ("Stanley: output_accel (m/s^2)" , output_accel )
393- component .debug ('Stanley: current yaw (rad)' , curr_yaw )
394- component .debug ('Stanley: current speed (m/s)' , speed )
395-
396-
397- if output_accel > self .max_accel :
398- output_accel = self .max_accel
399-
400- if output_accel < - self .max_decel :
401- output_accel = - self .max_decel
402485
403486 self .t_last = t
404487 return (output_accel , desired_steering_angle )
0 commit comments