rust_robotics_control/
feedback_linearization.rs1use rust_robotics_core::{ControlInput, Path2D, Point2D, Pose2D};
7
8#[derive(Debug, Clone, Copy)]
10pub struct FeedbackLinearizationConfig {
11 pub k1: f64,
13 pub k2: f64,
15 pub offset_distance: f64,
17 pub dt: f64,
19 pub goal_tolerance: f64,
21 pub max_steps: usize,
23}
24
25impl Default for FeedbackLinearizationConfig {
26 fn default() -> Self {
27 Self {
28 k1: 1.0,
29 k2: 1.0,
30 offset_distance: 0.1,
31 dt: 0.01,
32 goal_tolerance: 0.05,
33 max_steps: 10_000,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy)]
40pub struct FeedbackLinearizationStep {
41 pub pose: Pose2D,
42 pub control: ControlInput,
43 pub distance_to_goal: f64,
44}
45
46#[derive(Debug, Clone)]
48pub struct FeedbackLinearizationResult {
49 pub steps: Vec<FeedbackLinearizationStep>,
50 pub converged: bool,
51}
52
53impl FeedbackLinearizationResult {
54 pub fn final_pose(&self) -> Pose2D {
55 self.steps
56 .last()
57 .map(|s| s.pose)
58 .unwrap_or_else(Pose2D::origin)
59 }
60
61 pub fn iterations(&self) -> usize {
62 self.steps.len().saturating_sub(1)
63 }
64
65 pub fn path(&self) -> Path2D {
66 Path2D::from_points(
67 self.steps
68 .iter()
69 .map(|s| Point2D::new(s.pose.x, s.pose.y))
70 .collect(),
71 )
72 }
73}
74
75pub struct FeedbackLinearizationController {
77 config: FeedbackLinearizationConfig,
78}
79
80impl FeedbackLinearizationController {
81 pub fn new(config: FeedbackLinearizationConfig) -> Self {
82 Self { config }
83 }
84
85 pub fn config(&self) -> FeedbackLinearizationConfig {
86 self.config
87 }
88
89 pub fn simulate(&self, start: Pose2D, goal: Pose2D) -> FeedbackLinearizationResult {
91 let mut pose = start;
92 let mut steps = vec![FeedbackLinearizationStep {
93 pose,
94 control: ControlInput::zero(),
95 distance_to_goal: dist(pose, goal),
96 }];
97
98 for _ in 0..self.config.max_steps {
99 let l = self.config.offset_distance;
103 let px = pose.x + l * pose.yaw.cos();
104 let py = pose.y + l * pose.yaw.sin();
105 let p_dist = ((goal.x - px).powi(2) + (goal.y - py).powi(2)).sqrt();
106 if p_dist <= self.config.goal_tolerance {
107 return FeedbackLinearizationResult {
108 steps,
109 converged: true,
110 };
111 }
112
113 let control = self.compute_control(pose, goal);
114 pose = integrate_pose(pose, control, self.config.dt);
115 steps.push(FeedbackLinearizationStep {
116 pose,
117 control,
118 distance_to_goal: dist(pose, goal),
119 });
120 }
121
122 FeedbackLinearizationResult {
123 steps,
124 converged: false,
125 }
126 }
127
128 fn compute_control(&self, pose: Pose2D, goal: Pose2D) -> ControlInput {
129 let l = self.config.offset_distance;
130 let (cos_t, sin_t) = (pose.yaw.cos(), pose.yaw.sin());
131
132 let px = pose.x + l * cos_t;
134 let py = pose.y + l * sin_t;
135
136 let ux = -self.config.k1 * (px - goal.x);
138 let uy = -self.config.k2 * (py - goal.y);
139
140 let v = ux * cos_t + uy * sin_t;
142 let omega = (-ux * sin_t + uy * cos_t) / l;
143
144 ControlInput::new(v, omega)
145 }
146}
147
148pub fn feedback_linearization(
150 start: Pose2D,
151 goal: Pose2D,
152 config: FeedbackLinearizationConfig,
153) -> FeedbackLinearizationResult {
154 FeedbackLinearizationController::new(config).simulate(start, goal)
155}
156
157fn integrate_pose(pose: Pose2D, control: ControlInput, dt: f64) -> Pose2D {
158 let yaw = pose.yaw + control.omega * dt;
159 Pose2D::new(
160 pose.x + control.v * yaw.cos() * dt,
161 pose.y + control.v * yaw.sin() * dt,
162 yaw,
163 )
164}
165
166fn dist(pose: Pose2D, goal: Pose2D) -> f64 {
167 ((goal.x - pose.x).powi(2) + (goal.y - pose.y).powi(2)).sqrt()
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_config_defaults() {
176 let cfg = FeedbackLinearizationConfig::default();
177 assert_eq!(cfg.k1, 1.0);
178 assert_eq!(cfg.k2, 1.0);
179 assert_eq!(cfg.offset_distance, 0.1);
180 assert_eq!(cfg.dt, 0.01);
181 assert_eq!(cfg.goal_tolerance, 0.05);
182 assert_eq!(cfg.max_steps, 10_000);
183 }
184
185 #[test]
186 fn test_feedback_linearization_converges() {
187 let start = Pose2D::origin();
188 let goal = Pose2D::new(5.0, 5.0, 0.0);
189 let result = feedback_linearization(start, goal, FeedbackLinearizationConfig::default());
190 let fp = result.final_pose();
191
192 let l = FeedbackLinearizationConfig::default().offset_distance;
193 let px = fp.x + l * fp.yaw.cos();
194 let py = fp.y + l * fp.yaw.sin();
195 assert!(
196 result.converged,
197 "controller did not converge (virtual point distance: {:.4})",
198 ((goal.x - px).powi(2) + (goal.y - py).powi(2)).sqrt()
199 );
200 assert!((px - goal.x).abs() < 0.1);
202 assert!((py - goal.y).abs() < 0.1);
203 }
204
205 #[test]
206 fn test_feedback_linearization_non_empty_path() {
207 let controller =
208 FeedbackLinearizationController::new(FeedbackLinearizationConfig::default());
209 let result = controller.simulate(Pose2D::origin(), Pose2D::new(2.0, 1.0, 0.0));
210 let path = result.path();
211
212 assert!(
213 path.points.len() > 1,
214 "path should have more than one point"
215 );
216 assert_eq!(path.points[0].x, 0.0);
217 assert_eq!(path.points[0].y, 0.0);
218 }
219}