apex_solver/core/variable.rs
1//! Variables for optimization on manifolds.
2//!
3//! This module provides the `Variable` struct, which represents optimization variables that
4//! live on manifolds (Lie groups like SE2, SE3, SO2, SO3, or Euclidean spaces Rn). Variables
5//! support manifold operations (plus/minus in tangent space), constraints (fixed indices,
6//! bounds), and covariance estimation.
7//!
8//! # Key Concepts
9//!
10//! ## Manifolds vs. Vector Spaces
11//!
12//! Unlike standard optimization that operates in Euclidean space (Rⁿ), many robotics problems
13//! involve variables on manifolds:
14//! - **Poses**: SE(2) or SE(3) - rigid body transformations
15//! - **Rotations**: SO(2) or SO(3) - rotation matrices/quaternions
16//! - **Landmarks**: R³ - 3D points in Euclidean space
17//!
18//! Manifolds require special handling:
19//! - Updates happen in the tangent space (local linearization)
20//! - Plus operation (⊞): manifold × tangent → manifold
21//! - Minus operation (⊟): manifold × manifold → tangent
22//!
23//! ## Tangent Space Updates
24//!
25//! During optimization, updates are computed as tangent vectors and applied via the plus
26//! operation:
27//!
28//! ```text
29//! x_new = x_old ⊞ δx
30//! ```
31//!
32//! where `δx` is a tangent vector (e.g., 6D for SE(3), 3D for SE(2)).
33//!
34//! ## Constraints
35//!
36//! Variables support two types of constraints:
37//! - **Fixed indices**: Specific DOF held constant during optimization
38//! - **Bounds**: Box constraints (min/max) on tangent space components
39//!
40//! ## Covariance
41//!
42//! After optimization, the `Variable` can store a covariance matrix representing uncertainty
43//! in the tangent space. For SE(3), this is a 6×6 matrix; for SE(2), a 3×3 matrix.
44//!
45//! # Example: SE(2) Variable
46//!
47//! ```
48//! use apex_solver::core::variable::Variable;
49//! use apex_solver::manifold::se2::{SE2, SE2Tangent};
50//! use nalgebra::DVector;
51//!
52//! // Create a 2D pose variable
53//! let initial_pose = SE2::from_xy_angle(1.0, 2.0, 0.5);
54//! let mut var = Variable::new(initial_pose);
55//!
56//! // Apply a tangent space update: [dx, dy, dtheta]
57//! let delta = SE2Tangent::from(DVector::from_vec(vec![0.1, 0.2, 0.05]));
58//! let updated_pose = var.plus(&delta);
59//! var.set_value(updated_pose);
60//!
61//! println!("Updated pose at ({}, {}, {})",
62//! var.value.translation().x,
63//! var.value.translation().y,
64//! var.value.angle()
65//! );
66//! ```
67//!
68//! # Example: Variable with Constraints
69//!
70//! ```
71//! use apex_solver::core::variable::Variable;
72//! use apex_solver::manifold::rn::Rn;
73//! use nalgebra::DVector;
74//!
75//! // Create a 3D point variable
76//! let mut landmark = Variable::new(Rn::new(DVector::from_vec(vec![0.0, 0.0, 0.0])));
77//!
78//! // Fix the z-coordinate (index 2)
79//! landmark.fixed_indices.insert(2);
80//!
81//! // Constrain x to [-10, 10]
82//! landmark.bounds.insert(0, (-10.0, 10.0));
83//!
84//! // Apply update (will respect constraints)
85//! let update = DVector::from_vec(vec![15.0, 5.0, 100.0]); // Large update
86//! landmark.update_variable(update);
87//!
88//! let result = landmark.to_vector();
89//! assert_eq!(result[0], 10.0); // Clamped to upper bound
90//! assert_eq!(result[1], 5.0); // Unconstrained
91//! assert_eq!(result[2], 0.0); // Fixed at original value
92//! ```
93
94use std::collections;
95
96use crate::manifold;
97use faer;
98use nalgebra;
99
100/// Generic Variable struct that uses static dispatch with any manifold type.
101///
102/// This struct represents optimization variables that live on manifolds and provides
103/// type-safe operations for updating variables with tangent space perturbations.
104///
105/// # Type Parameters
106/// * `M` - The manifold type that implements the LieGroup trait
107///
108/// # Examples
109/// ```
110/// use apex_solver::core::variable::Variable;
111/// use apex_solver::manifold::se2::SE2;
112/// use apex_solver::manifold::rn::Rn;
113///
114/// // Create a Variable for SE2 manifold
115/// let se2_value = SE2::from_xy_angle(1.0, 2.0, 0.5);
116/// let se2_var = Variable::new(se2_value);
117///
118/// // Create a Variable for Euclidean space
119/// let rn_value = Rn::from_vec(vec![1.0, 2.0, 3.0]);
120/// let rn_var = Variable::new(rn_value);
121/// ```
122#[derive(Clone, Debug)]
123pub struct Variable<M: manifold::LieGroup> {
124 /// The manifold value
125 pub value: M,
126 /// Indices that should remain fixed during optimization
127 pub fixed_indices: collections::HashSet<usize>,
128 /// Bounds constraints on the tangent space representation
129 pub bounds: collections::HashMap<usize, (f64, f64)>,
130 /// Covariance matrix in the tangent space (uncertainty estimation)
131 ///
132 /// This is `None` if covariance has not been computed.
133 /// When present, it's a square matrix of size `tangent_dim x tangent_dim`
134 /// representing the uncertainty in the optimized variable's tangent space.
135 ///
136 /// For example, for SE3 this would be a 6×6 matrix representing uncertainty
137 /// in [translation_x, translation_y, translation_z, rotation_x, rotation_y, rotation_z].
138 pub covariance: Option<faer::Mat<f64>>,
139}
140
141impl<M> Variable<M>
142where
143 M: manifold::LieGroup + Clone + 'static,
144 M::TangentVector: manifold::Tangent<M>,
145{
146 /// Create a new Variable from a manifold value.
147 ///
148 /// # Arguments
149 /// * `value` - The initial manifold value
150 ///
151 /// # Examples
152 /// ```
153 /// use apex_solver::core::variable::Variable;
154 /// use apex_solver::manifold::se2::SE2;
155 ///
156 /// let se2_value = SE2::from_xy_angle(1.0, 2.0, 0.5);
157 /// let variable = Variable::new(se2_value);
158 /// ```
159 pub fn new(value: M) -> Self {
160 Variable {
161 value,
162 fixed_indices: collections::HashSet::new(),
163 bounds: collections::HashMap::new(),
164 covariance: None,
165 }
166 }
167
168 /// Set the manifold value.
169 ///
170 /// # Arguments
171 /// * `value` - The new manifold value
172 pub fn set_value(&mut self, value: M) {
173 self.value = value;
174 }
175
176 /// Get the degrees of freedom (tangent space dimension) of the variable.
177 ///
178 /// This returns the dimension of the tangent space, which is the number of
179 /// parameters that can be optimized for this manifold type.
180 ///
181 /// # Returns
182 /// The tangent space dimension (degrees of freedom)
183 pub fn get_size(&self) -> usize {
184 self.value.tangent_dim()
185 }
186
187 /// Plus operation: apply tangent space perturbation to the manifold value.
188 ///
189 /// This method takes a tangent vector and returns a new manifold value by applying
190 /// the manifold's plus operation (typically the exponential map).
191 ///
192 /// # Arguments
193 /// * `tangent` - The tangent vector to apply as a perturbation
194 ///
195 /// # Returns
196 /// A new manifold value after applying the tangent perturbation
197 ///
198 /// # Examples
199 /// ```
200 /// use apex_solver::core::variable::Variable;
201 /// use apex_solver::manifold::se2::{SE2, SE2Tangent};
202 /// use nalgebra as na;
203 ///
204 /// let se2_value = SE2::from_xy_angle(1.0, 2.0, 0.0);
205 /// let variable = Variable::new(se2_value);
206 ///
207 /// // Create a tangent vector: [dx, dy, dtheta]
208 /// let tangent = SE2Tangent::from(na::DVector::from(vec![0.1, 0.1, 0.1]));
209 /// let new_value = variable.plus(&tangent);
210 /// ```
211 pub fn plus(&self, tangent: &M::TangentVector) -> M {
212 self.value.plus(tangent, None, None)
213 }
214
215 /// Minus operation: compute tangent space difference between two manifold values.
216 ///
217 /// This method computes the tangent vector that would transform this variable's
218 /// value to the other variable's value using the manifold's minus operation
219 /// (typically the logarithmic map).
220 ///
221 /// # Arguments
222 /// * `other` - The other variable to compute the difference to
223 ///
224 /// # Returns
225 /// A tangent vector representing the difference in tangent space
226 ///
227 /// # Examples
228 /// ```
229 /// use apex_solver::core::variable::Variable;
230 /// use apex_solver::manifold::se2::SE2;
231 ///
232 /// let se2_1 = SE2::from_xy_angle(2.0, 3.0, 0.5);
233 /// let se2_2 = SE2::from_xy_angle(1.0, 2.0, 0.0);
234 /// let var1 = Variable::new(se2_1);
235 /// let var2 = Variable::new(se2_2);
236 ///
237 /// let difference = var1.minus(&var2);
238 /// ```
239 pub fn minus(&self, other: &Self) -> M::TangentVector {
240 self.value.minus(&other.value, None, None)
241 }
242
243 /// Get the covariance matrix for this variable (if computed).
244 ///
245 /// Returns `None` if covariance has not been computed.
246 ///
247 /// # Returns
248 /// Reference to the covariance matrix in tangent space
249 pub fn get_covariance(&self) -> Option<&faer::Mat<f64>> {
250 self.covariance.as_ref()
251 }
252
253 /// Set the covariance matrix for this variable.
254 ///
255 /// The covariance matrix should be square with dimension equal to
256 /// the tangent space dimension of this variable.
257 ///
258 /// # Arguments
259 /// * `cov` - Covariance matrix in tangent space
260 pub fn set_covariance(&mut self, cov: faer::Mat<f64>) {
261 self.covariance = Some(cov);
262 }
263
264 /// Clear the covariance matrix.
265 pub fn clear_covariance(&mut self) {
266 self.covariance = None;
267 }
268}
269
270// Extension implementation for Rn manifold (special case since it's Euclidean)
271use crate::manifold::rn::Rn;
272
273impl Variable<Rn> {
274 /// Convert the Rn variable to a vector representation.
275 pub fn to_vector(&self) -> nalgebra::DVector<f64> {
276 self.value.data().clone()
277 }
278
279 /// Create an Rn variable from a vector representation.
280 pub fn from_vector(values: nalgebra::DVector<f64>) -> Self {
281 Self::new(Rn::new(values))
282 }
283
284 /// Update the Rn variable with bounds and fixed constraints.
285 pub fn update_variable(&mut self, mut tangent_delta: nalgebra::DVector<f64>) {
286 // bound
287 for (&idx, &(lower, upper)) in &self.bounds {
288 tangent_delta[idx] = tangent_delta[idx].max(lower).min(upper);
289 }
290
291 // fix
292 for &index_to_fix in &self.fixed_indices {
293 tangent_delta[index_to_fix] = self.value.data()[index_to_fix];
294 }
295
296 self.value = Rn::new(tangent_delta);
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::manifold::{rn::Rn, se2::SE2, se3::SE3, so2::SO2, so3::SO3};
304 use nalgebra;
305 use std;
306
307 #[test]
308 fn test_variable_creation_rn() {
309 let vec_data = nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
310 let rn_value = Rn::new(vec_data);
311 let variable = Variable::new(rn_value);
312
313 // Use get_size for Rn manifold (returns dynamic size)
314 assert_eq!(variable.get_size(), 5);
315 assert!(variable.fixed_indices.is_empty());
316 assert!(variable.bounds.is_empty());
317 }
318
319 #[test]
320 fn test_variable_creation_se2() {
321 let se2 = SE2::from_xy_angle(1.0, 2.0, 0.5);
322 let variable = Variable::new(se2);
323
324 assert_eq!(variable.get_size(), SE2::DOF);
325 assert!(variable.fixed_indices.is_empty());
326 assert!(variable.bounds.is_empty());
327 }
328
329 #[test]
330 fn test_variable_creation_se3() {
331 let se3 = SE3::from_translation_quaternion(
332 nalgebra::Vector3::new(1.0, 2.0, 3.0),
333 nalgebra::Quaternion::new(1.0, 0.0, 0.0, 0.0),
334 );
335 let variable = Variable::new(se3);
336
337 assert_eq!(variable.get_size(), SE3::DOF);
338 assert!(variable.fixed_indices.is_empty());
339 assert!(variable.bounds.is_empty());
340 }
341
342 #[test]
343 fn test_variable_creation_so2() {
344 let so2 = SO2::from_angle(0.5);
345 let variable = Variable::new(so2);
346
347 assert_eq!(variable.get_size(), SO2::DOF);
348 assert!(variable.fixed_indices.is_empty());
349 assert!(variable.bounds.is_empty());
350 }
351
352 #[test]
353 fn test_variable_creation_so3() {
354 let so3 = SO3::from_euler_angles(0.1, 0.2, 0.3);
355 let variable = Variable::new(so3);
356
357 assert_eq!(variable.get_size(), SO3::DOF);
358 assert!(variable.fixed_indices.is_empty());
359 assert!(variable.bounds.is_empty());
360 }
361
362 #[test]
363 fn test_variable_set_value() {
364 let initial_vec = nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0]);
365 let mut variable = Variable::new(Rn::new(initial_vec));
366
367 let new_vec = nalgebra::DVector::from_vec(vec![4.0, 5.0, 6.0, 7.0]);
368 variable.set_value(Rn::new(new_vec));
369 assert_eq!(variable.get_size(), 4);
370
371 let se2_initial = SE2::from_xy_angle(0.0, 0.0, 0.0);
372 let mut se2_variable = Variable::new(se2_initial);
373
374 let se2_new = SE2::from_xy_angle(1.0, 2.0, std::f64::consts::PI / 4.0);
375 se2_variable.set_value(se2_new);
376 assert_eq!(se2_variable.get_size(), SE2::DOF);
377 }
378
379 #[test]
380 fn test_variable_plus_minus_operations() {
381 // Test SE2 manifold plus/minus operations
382 let se2_1 = SE2::from_xy_angle(2.0, 3.0, std::f64::consts::PI / 2.0);
383 let se2_2 = SE2::from_xy_angle(1.0, 1.0, std::f64::consts::PI / 4.0);
384 let var1 = Variable::new(se2_1);
385 let var2 = Variable::new(se2_2);
386
387 let diff_tangent = var1.minus(&var2);
388 let var2_updated = var2.plus(&diff_tangent);
389 let final_diff = var1.minus(&Variable::new(var2_updated));
390
391 assert!(nalgebra::DVector::from(final_diff).norm() < 1e-10);
392 }
393
394 #[test]
395 fn test_variable_rn_plus_minus_operations() {
396 // Test Rn manifold plus/minus operations
397 let rn_1 = Rn::new(nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0]));
398 let rn_2 = Rn::new(nalgebra::DVector::from_vec(vec![4.0, 5.0, 6.0]));
399 let var1 = Variable::new(rn_1);
400 let var2 = Variable::new(rn_2);
401
402 // Test minus operation
403 let diff_tangent = var1.minus(&var2);
404 assert_eq!(
405 diff_tangent.to_vector(),
406 nalgebra::DVector::from_vec(vec![-3.0, -3.0, -3.0])
407 );
408
409 // Test plus operation
410 let var2_updated = var2.plus(&diff_tangent);
411 assert_eq!(
412 var2_updated.data(),
413 &nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0])
414 );
415
416 // Test roundtrip consistency
417 let final_diff = var1.minus(&Variable::new(var2_updated));
418 assert!(final_diff.to_vector().norm() < 1e-10);
419 }
420
421 #[test]
422 fn test_variable_update_with_bounds() {
423 let vec_data = nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
424 let mut variable = Variable::new(Rn::new(vec_data));
425
426 variable.bounds.insert(0, (-1.0, 1.0));
427 variable.bounds.insert(2, (0.0, 5.0));
428
429 let new_values = nalgebra::DVector::from_vec(vec![-5.0, 10.0, -3.0, 20.0, 30.0, 40.0]);
430 variable.update_variable(new_values);
431
432 let result_vec = variable.to_vector();
433 assert!(result_vec.len() == 6);
434 }
435
436 #[test]
437 fn test_variable_update_with_fixed_indices() {
438 let vec_data = nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
439 let mut variable = Variable::new(Rn::new(vec_data.clone()));
440
441 variable.fixed_indices.insert(1);
442 variable.fixed_indices.insert(4);
443
444 let delta_values =
445 nalgebra::DVector::from_vec(vec![9.0, 18.0, 27.0, 36.0, 45.0, 54.0, 63.0, 72.0]);
446 variable.update_variable(delta_values);
447
448 let result_vec = variable.to_vector();
449 assert_eq!(result_vec[1], 2.0);
450 assert_eq!(result_vec[4], 5.0);
451 assert!(result_vec.len() == 8);
452 }
453
454 #[test]
455 fn test_variable_combined_bounds_and_fixed() {
456 let vec_data = nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
457 let mut variable = Variable::new(Rn::new(vec_data.clone()));
458
459 variable.bounds.insert(0, (-2.0, 2.0));
460 variable.bounds.insert(3, (-1.0, 1.0));
461 variable.fixed_indices.insert(1);
462 variable.fixed_indices.insert(5);
463
464 let delta_values =
465 nalgebra::DVector::from_vec(vec![-5.0, 100.0, 30.0, 10.0, 50.0, 600.0, 70.0]);
466 variable.update_variable(delta_values);
467
468 let result = variable.to_vector();
469 assert_eq!(result[1], 2.0);
470 assert_eq!(result[5], 6.0);
471 assert!(result.len() == 7);
472 }
473
474 #[test]
475 fn test_variable_type_safety() {
476 let se2_var = Variable::new(SE2::from_xy_angle(1.0, 2.0, 0.5));
477 let se3_var = Variable::new(SE3::from_translation_quaternion(
478 nalgebra::Vector3::new(1.0, 2.0, 3.0),
479 nalgebra::Quaternion::new(1.0, 0.0, 0.0, 0.0),
480 ));
481 let so2_var = Variable::new(SO2::from_angle(0.5));
482 let so3_var = Variable::new(SO3::from_euler_angles(0.1, 0.2, 0.3));
483 let rn_var = Variable::new(Rn::new(nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0])));
484
485 assert_eq!(se2_var.get_size(), SE2::DOF);
486 assert_eq!(se3_var.get_size(), SE3::DOF);
487 assert_eq!(so2_var.get_size(), SO2::DOF);
488 assert_eq!(so3_var.get_size(), SO3::DOF);
489 assert_eq!(rn_var.get_size(), 3);
490 }
491
492 #[test]
493 fn test_variable_vector_conversion_roundtrip() {
494 let original_data = nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
495 let rn_var = Variable::new(Rn::new(original_data.clone()));
496 let vec_repr = rn_var.to_vector();
497 assert_eq!(vec_repr, original_data);
498
499 let reconstructed_var = Variable::<Rn>::from_vector(vec_repr);
500 assert_eq!(reconstructed_var.to_vector(), original_data);
501 }
502
503 #[test]
504 fn test_variable_manifold_operations_consistency() {
505 // Test Rn manifold operations (has vector conversion methods)
506 let rn_initial = Rn::new(nalgebra::DVector::from_vec(vec![1.0, 2.0, 3.0]));
507 let mut rn_var = Variable::new(rn_initial);
508 let rn_new_values = nalgebra::DVector::from_vec(vec![2.0, 3.0, 4.0]);
509 rn_var.update_variable(rn_new_values);
510
511 let rn_result = rn_var.to_vector();
512 assert_eq!(rn_result, nalgebra::DVector::from_vec(vec![2.0, 3.0, 4.0]));
513
514 // Test SE2 manifold plus/minus operations (core functionality)
515 let se2_1 = SE2::from_xy_angle(2.0, 3.0, std::f64::consts::PI / 2.0);
516 let se2_2 = SE2::from_xy_angle(1.0, 1.0, std::f64::consts::PI / 4.0);
517 let var1 = Variable::new(se2_1);
518 let var2 = Variable::new(se2_2);
519
520 let diff_tangent = var1.minus(&var2);
521 let var2_updated = var2.plus(&diff_tangent);
522 let final_diff = var1.minus(&Variable::new(var2_updated));
523
524 // The final difference should be small (close to identity in tangent space)
525 assert!(nalgebra::DVector::from(final_diff).norm() < 1e-10);
526 }
527
528 #[test]
529 fn test_variable_constraints_interaction() {
530 let rn_data = nalgebra::DVector::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0]);
531 let mut rn_var = Variable::new(Rn::new(rn_data));
532
533 rn_var.bounds.insert(0, (-1.0, 1.0));
534 rn_var.bounds.insert(2, (-10.0, 10.0));
535 rn_var.fixed_indices.insert(1);
536 rn_var.fixed_indices.insert(4);
537
538 let large_delta = nalgebra::DVector::from_vec(vec![5.0, 100.0, 15.0, 20.0, 200.0]);
539 rn_var.update_variable(large_delta);
540
541 let result = rn_var.to_vector();
542
543 assert_eq!(result[0], 1.0);
544 assert_eq!(result[1], 0.0);
545 assert_eq!(result[2], 10.0);
546 assert_eq!(result[3], 20.0);
547 assert_eq!(result[4], 0.0);
548 }
549}