Skip to content

Commit cdc2b8e

Browse files
committed
Manual integration of control team logic
1 parent 8055e27 commit cdc2b8e

1 file changed

Lines changed: 162 additions & 79 deletions

File tree

GEMstack/onboard/planning/stanley.py

Lines changed: 162 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)