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