Skip to content

Commit af71633

Browse files
committed
test
1 parent 9111107 commit af71633

1 file changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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

Comments
 (0)