1use rust_robotics_core::{ControlInput, Path2D, Point2D, Pose2D};
21use std::f64::consts::PI;
22
23#[derive(Debug, Clone, Copy)]
25pub struct BacksteppingConfig {
26 pub k1: f64,
28 pub k2: f64,
30 pub k3: f64,
32 pub dt: f64,
34 pub goal_tolerance: f64,
36 pub max_steps: usize,
38}
39
40impl Default for BacksteppingConfig {
41 fn default() -> Self {
42 Self {
43 k1: 3.0,
44 k2: 8.0,
45 k3: 3.0,
46 dt: 0.01,
47 goal_tolerance: 0.05,
48 max_steps: 10_000,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy)]
55pub struct TimeVaryingBacksteppingConfig {
56 pub base: BacksteppingConfig,
58 pub orbit_radius_scale: f64,
60 pub orbit_frequency: f64,
62 pub orbit_decay_rate: f64,
64}
65
66impl Default for TimeVaryingBacksteppingConfig {
67 fn default() -> Self {
68 Self {
69 base: BacksteppingConfig::default(),
70 orbit_radius_scale: 0.75,
71 orbit_frequency: 2.0,
72 orbit_decay_rate: 0.8,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy)]
79pub struct BacksteppingStep {
80 pub pose: Pose2D,
81 pub control: ControlInput,
82 pub distance_to_goal: f64,
83}
84
85#[derive(Debug, Clone)]
87pub struct BacksteppingResult {
88 pub steps: Vec<BacksteppingStep>,
89 pub converged: bool,
90}
91
92impl BacksteppingResult {
93 pub fn final_pose(&self) -> Pose2D {
95 self.steps
96 .last()
97 .map(|step| step.pose)
98 .unwrap_or_else(Pose2D::origin)
99 }
100
101 pub fn iterations(&self) -> usize {
103 self.steps.len().saturating_sub(1)
104 }
105
106 pub fn path(&self) -> Path2D {
108 Path2D::from_points(
109 self.steps
110 .iter()
111 .map(|step| Point2D::new(step.pose.x, step.pose.y))
112 .collect(),
113 )
114 }
115}
116
117pub struct BacksteppingController {
119 config: BacksteppingConfig,
120}
121
122impl BacksteppingController {
123 pub fn new(config: BacksteppingConfig) -> Self {
125 Self { config }
126 }
127
128 pub fn config(&self) -> BacksteppingConfig {
130 self.config
131 }
132
133 pub fn simulate(&self, start: Pose2D, goal: Pose2D) -> BacksteppingResult {
135 let mut pose = start;
136 let mut steps = vec![BacksteppingStep {
137 pose,
138 control: ControlInput::zero(),
139 distance_to_goal: distance_to_goal(pose, goal),
140 }];
141
142 for _ in 0..self.config.max_steps {
143 if self.is_converged(pose, goal) {
144 return BacksteppingResult {
145 steps,
146 converged: true,
147 };
148 }
149
150 let control = self.compute_control(pose, goal);
151 pose = integrate_pose(pose, control, self.config.dt);
152 steps.push(BacksteppingStep {
153 pose,
154 control,
155 distance_to_goal: distance_to_goal(pose, goal),
156 });
157 }
158
159 BacksteppingResult {
160 steps,
161 converged: false,
162 }
163 }
164
165 pub fn planning(&self, start: (f64, f64, f64), goal: (f64, f64, f64)) -> Vec<(f64, f64)> {
167 self.simulate(
168 Pose2D::new(start.0, start.1, start.2),
169 Pose2D::new(goal.0, goal.1, goal.2),
170 )
171 .path()
172 .points
173 .into_iter()
174 .map(|point| (point.x, point.y))
175 .collect()
176 }
177
178 fn is_converged(&self, pose: Pose2D, goal: Pose2D) -> bool {
179 let position_ok = distance_to_goal(pose, goal) <= self.config.goal_tolerance;
180 let yaw_ok = normalize_angle(goal.yaw - pose.yaw).abs() <= self.config.goal_tolerance;
181 position_ok && yaw_ok
182 }
183
184 fn compute_control(&self, pose: Pose2D, goal: Pose2D) -> ControlInput {
185 let dx = goal.x - pose.x;
186 let dy = goal.y - pose.y;
187 let distance = (dx.powi(2) + dy.powi(2)).sqrt();
188
189 if distance <= self.terminal_region_radius() {
190 return self.compute_terminal_control(pose, goal, distance);
191 }
192
193 self.compute_tracking_control(pose, goal)
194 }
195
196 fn compute_tracking_control(&self, pose: Pose2D, goal: Pose2D) -> ControlInput {
197 let dx = goal.x - pose.x;
198 let dy = goal.y - pose.y;
199
200 let cos_theta = pose.yaw.cos();
201 let sin_theta = pose.yaw.sin();
202
203 let e1 = cos_theta * dx + sin_theta * dy;
204 let e2 = -sin_theta * dx + cos_theta * dy;
205 let e3 = normalize_angle(goal.yaw - pose.yaw);
206
207 let vr = self.config.k1 * (e1.powi(2) + e2.powi(2)).sqrt();
208 let v = vr * e3.cos() + self.config.k1 * e1;
209 let omega = vr * (self.config.k2 * e2 + self.config.k3 * e3.sin());
210
211 ControlInput::new(v, omega)
212 }
213
214 fn compute_terminal_control(&self, pose: Pose2D, goal: Pose2D, distance: f64) -> ControlInput {
215 let alpha = normalize_angle((goal.y - pose.y).atan2(goal.x - pose.x) - pose.yaw);
216 let beta = normalize_angle(goal.yaw - pose.yaw - alpha);
217
218 let mut v = self.config.k1 * distance;
219 if !(-PI / 2.0..=PI / 2.0).contains(&alpha) {
220 v = -v;
221 }
222
223 let omega = self.config.k2 * alpha - self.config.k3 * beta;
224 ControlInput::new(v, omega)
225 }
226
227 fn terminal_region_radius(&self) -> f64 {
228 10.0 * self.config.goal_tolerance
229 }
230}
231
232pub struct TimeVaryingBacksteppingController {
240 config: TimeVaryingBacksteppingConfig,
241 tracker: BacksteppingController,
242}
243
244impl TimeVaryingBacksteppingController {
245 pub fn new(config: TimeVaryingBacksteppingConfig) -> Self {
247 Self {
248 tracker: BacksteppingController::new(config.base),
249 config,
250 }
251 }
252
253 pub fn simulate(&self, start: Pose2D, goal: Pose2D) -> BacksteppingResult {
255 let mut pose = start;
256 let mut steps = vec![BacksteppingStep {
257 pose,
258 control: ControlInput::zero(),
259 distance_to_goal: distance_to_goal(pose, goal),
260 }];
261 let reference_radius = distance_to_goal(start, goal).max(self.config.base.goal_tolerance)
262 * self.config.orbit_radius_scale;
263 let initial_phase = (start.y - goal.y).atan2(start.x - goal.x);
264
265 for step_index in 0..self.config.base.max_steps {
266 if self.is_converged(pose, goal) {
267 return BacksteppingResult {
268 steps,
269 converged: true,
270 };
271 }
272
273 let time = step_index as f64 * self.config.base.dt;
274 let distance = distance_to_goal(pose, goal);
275 let control = if distance <= self.tracker.terminal_region_radius() {
276 self.tracker.compute_terminal_control(pose, goal, distance)
277 } else {
278 let virtual_goal = self.virtual_goal(goal, reference_radius, initial_phase, time);
279 self.tracker.compute_tracking_control(pose, virtual_goal)
280 };
281 pose = integrate_pose(pose, control, self.config.base.dt);
282 steps.push(BacksteppingStep {
283 pose,
284 control,
285 distance_to_goal: distance_to_goal(pose, goal),
286 });
287 }
288
289 BacksteppingResult {
290 steps,
291 converged: false,
292 }
293 }
294
295 fn is_converged(&self, pose: Pose2D, goal: Pose2D) -> bool {
296 let position_ok = distance_to_goal(pose, goal) <= self.config.base.goal_tolerance;
297 let yaw_ok = normalize_angle(goal.yaw - pose.yaw).abs() <= self.config.base.goal_tolerance;
298 position_ok && yaw_ok
299 }
300
301 fn virtual_goal(
302 &self,
303 goal: Pose2D,
304 reference_radius: f64,
305 initial_phase: f64,
306 time: f64,
307 ) -> Pose2D {
308 let amplitude = reference_radius * (-self.config.orbit_decay_rate * time).exp();
309 let phase = initial_phase + self.config.orbit_frequency * time;
310 Pose2D::new(
311 goal.x + amplitude * phase.cos(),
312 goal.y + amplitude * phase.sin(),
313 goal.yaw,
314 )
315 }
316}
317
318pub fn backstepping_control(
320 start: Pose2D,
321 goal: Pose2D,
322 config: BacksteppingConfig,
323) -> BacksteppingResult {
324 BacksteppingController::new(config).simulate(start, goal)
325}
326
327pub fn time_varying_backstepping_control(
329 start: Pose2D,
330 goal: Pose2D,
331 config: TimeVaryingBacksteppingConfig,
332) -> BacksteppingResult {
333 TimeVaryingBacksteppingController::new(config).simulate(start, goal)
334}
335
336pub fn normalize_angle(mut angle: f64) -> f64 {
338 while angle > PI {
339 angle -= 2.0 * PI;
340 }
341 while angle < -PI {
342 angle += 2.0 * PI;
343 }
344 angle
345}
346
347fn integrate_pose(pose: Pose2D, control: ControlInput, dt: f64) -> Pose2D {
348 let x = pose.x + control.v * pose.yaw.cos() * dt;
349 let y = pose.y + control.v * pose.yaw.sin() * dt;
350 let yaw = normalize_angle(pose.yaw + control.omega * dt);
351 Pose2D::new(x, y, yaw)
352}
353
354fn distance_to_goal(pose: Pose2D, goal: Pose2D) -> f64 {
355 ((goal.x - pose.x).powi(2) + (goal.y - pose.y).powi(2)).sqrt()
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_backstepping_config_defaults() {
364 let config = BacksteppingConfig::default();
365 assert_eq!(config.k1, 3.0);
366 assert_eq!(config.k2, 8.0);
367 assert_eq!(config.k3, 3.0);
368 assert_eq!(config.dt, 0.01);
369 assert_eq!(config.goal_tolerance, 0.05);
370 assert_eq!(config.max_steps, 10_000);
371 }
372
373 #[test]
374 fn test_backstepping_converges_to_goal() {
375 let start = Pose2D::origin();
376 let goal = Pose2D::new(2.0, 1.5, PI / 4.0);
377
378 let result = backstepping_control(start, goal, BacksteppingConfig::default());
379 let final_pose = result.final_pose();
380
381 assert!(result.converged);
382 assert!(distance_to_goal(final_pose, goal) < 0.05);
383 assert!(normalize_angle(final_pose.yaw - goal.yaw).abs() < 0.05);
384 }
385
386 #[test]
387 fn test_backstepping_returns_non_empty_path() {
388 let controller = BacksteppingController::new(BacksteppingConfig::default());
389 let result = controller.simulate(Pose2D::origin(), Pose2D::new(1.5, 0.5, 0.0));
390 let path = result.path();
391
392 assert!(path.points.len() > 1);
393 assert_eq!(path.points[0].x, 0.0);
394 assert_eq!(path.points[0].y, 0.0);
395 }
396
397 #[test]
398 fn test_time_varying_backstepping_config_defaults() {
399 let config = TimeVaryingBacksteppingConfig::default();
400 assert_eq!(config.base.goal_tolerance, 0.05);
401 assert_eq!(config.orbit_radius_scale, 0.75);
402 assert_eq!(config.orbit_frequency, 2.0);
403 assert_eq!(config.orbit_decay_rate, 0.8);
404 }
405
406 #[test]
407 fn test_time_varying_backstepping_converges_to_goal() {
408 let start = Pose2D::origin();
409 let goal = Pose2D::new(1.5, 1.0, PI / 2.0);
410 let controller = TimeVaryingBacksteppingController::new(TimeVaryingBacksteppingConfig {
411 base: BacksteppingConfig {
412 k1: 2.5,
413 k2: 6.0,
414 k3: 3.0,
415 dt: 0.01,
416 goal_tolerance: 0.05,
417 max_steps: 12_000,
418 },
419 orbit_radius_scale: 0.75,
420 orbit_frequency: 2.0,
421 orbit_decay_rate: 0.8,
422 });
423
424 let result = controller.simulate(start, goal);
425 let final_pose = result.final_pose();
426
427 assert!(result.converged);
428 assert!(distance_to_goal(final_pose, goal) < 0.05);
429 assert!(normalize_angle(final_pose.yaw - goal.yaw).abs() < 0.05);
430 }
431}