Skip to main content

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 apex_solver::manifold::Tangent;
51//! use nalgebra::DVector;
52//!
53//! // Create a 2D pose variable
54//! let initial_pose = SE2::from_xy_angle(1.0, 2.0, 0.5);
55//! let mut var = Variable::new(initial_pose);
56//!
57//! // Apply a tangent space update: [dx, dy, dtheta]
58//! let delta = SE2Tangent::from_slice(&[0.1, 0.2, 0.05]);
59//! let updated_pose = var.plus(&delta);
60//! var.set_value(updated_pose);
61//! ```
62//!
63//! # Example: Variable with Constraints
64//!
65//! ```
66//! use apex_solver::core::variable::Variable;
67//! use apex_solver::manifold::rn::Rn;
68//! use nalgebra::DVector;
69//!
70//! // Create a 3D point variable
71//! let mut landmark = Variable::new(Rn::new(DVector::from_vec(vec![0.0, 0.0, 0.0])));
72//!
73//! // Fix the z-coordinate (index 2)
74//! landmark.fixed_indices.insert(2);
75//!
76//! // Constrain x to [-10, 10]
77//! landmark.bounds.insert(0, (-10.0, 10.0));
78//!
79//! // Apply update (will respect constraints)
80//! let update = DVector::from_vec(vec![15.0, 5.0, 100.0]); // Large update
81//! landmark.update_variable(update);
82//!
83//! let result = landmark.to_vector();
84//! assert_eq!(result[0], 10.0);   // Clamped to upper bound
85//! assert_eq!(result[1], 5.0);    // Unconstrained
86//! assert_eq!(result[2], 0.0);    // Fixed at original value
87//! ```
88
89use std::any::Any;
90use std::collections::{HashMap, HashSet};
91
92use apex_manifolds::{LieGroup, Tangent};
93use faer::Mat;
94use nalgebra::DVector;
95
96/// Generic Variable struct that uses static dispatch with any manifold type.
97///
98/// This struct represents optimization variables that live on manifolds and provides
99/// type-safe operations for updating variables with tangent space perturbations.
100///
101/// # Type Parameters
102/// * `M` - The manifold type that implements the LieGroup trait
103///
104/// # Examples
105/// ```
106/// use apex_solver::core::variable::Variable;
107/// use apex_solver::manifold::se2::SE2;
108/// use apex_solver::manifold::rn::Rn;
109///
110/// // Create a Variable for SE2 manifold
111/// let se2_value = SE2::from_xy_angle(1.0, 2.0, 0.5);
112/// let se2_var = Variable::new(se2_value);
113///
114/// // Create a Variable for Euclidean space
115/// let rn_value = Rn::from_vec(vec![1.0, 2.0, 3.0]);
116/// let rn_var = Variable::new(rn_value);
117/// ```
118#[derive(Clone, Debug)]
119pub struct Variable<M: LieGroup> {
120    /// The manifold value
121    pub value: M,
122    /// Indices that should remain fixed during optimization
123    pub fixed_indices: HashSet<usize>,
124    /// Bounds constraints on the tangent space representation
125    pub bounds: HashMap<usize, (f64, f64)>,
126    /// Covariance matrix in the tangent space (uncertainty estimation)
127    ///
128    /// This is `None` if covariance has not been computed.
129    /// When present, it's a square matrix of size `tangent_dim x tangent_dim`
130    /// representing the uncertainty in the optimized variable's tangent space.
131    ///
132    /// For example, for SE3 this would be a 6×6 matrix representing uncertainty
133    /// in [translation_x, translation_y, translation_z, rotation_x, rotation_y, rotation_z].
134    pub covariance: Option<faer::Mat<f64>>,
135}
136
137impl<M> Variable<M>
138where
139    M: LieGroup + Clone + 'static,
140    M::TangentVector: Tangent<M>,
141{
142    /// Create a new Variable from a manifold value.
143    ///
144    /// # Arguments
145    /// * `value` - The initial manifold value
146    ///
147    /// # Examples
148    /// ```
149    /// use apex_solver::core::variable::Variable;
150    /// use apex_solver::manifold::se2::SE2;
151    ///
152    /// let se2_value = SE2::from_xy_angle(1.0, 2.0, 0.5);
153    /// let variable = Variable::new(se2_value);
154    /// ```
155    pub fn new(value: M) -> Self {
156        Variable {
157            value,
158            fixed_indices: HashSet::new(),
159            bounds: HashMap::new(),
160            covariance: None,
161        }
162    }
163
164    /// Set the manifold value.
165    ///
166    /// # Arguments
167    /// * `value` - The new manifold value
168    pub fn set_value(&mut self, value: M) {
169        self.value = value;
170    }
171
172    /// Get the degrees of freedom (tangent space dimension) of the variable.
173    ///
174    /// This returns the dimension of the tangent space, which is the number of
175    /// parameters that can be optimized for this manifold type.
176    ///
177    /// # Returns
178    /// The tangent space dimension (degrees of freedom)
179    pub fn get_size(&self) -> usize {
180        self.value.tangent_dim()
181    }
182
183    /// Plus operation: apply tangent space perturbation to the manifold value.
184    ///
185    /// This method takes a tangent vector and returns a new manifold value by applying
186    /// the manifold's plus operation (typically the exponential map).
187    ///
188    /// # Arguments
189    /// * `tangent` - The tangent vector to apply as a perturbation
190    ///
191    /// # Returns
192    /// A new manifold value after applying the tangent perturbation
193    ///
194    /// # Examples
195    /// ```
196    /// use apex_solver::core::variable::Variable;
197    /// use apex_solver::manifold::se2::{SE2, SE2Tangent};
198    /// use apex_solver::manifold::Tangent;
199    /// use nalgebra as na;
200    ///
201    /// let se2_value = SE2::from_xy_angle(1.0, 2.0, 0.0);
202    /// let variable = Variable::new(se2_value);
203    ///
204    /// // Create a tangent vector: [dx, dy, dtheta]
205    /// let tangent = SE2Tangent::from_slice(&[0.1, 0.1, 0.1]);
206    /// let new_value = variable.plus(&tangent);
207    /// ```
208    pub fn plus(&self, tangent: &M::TangentVector) -> M {
209        self.value.plus(tangent, None, None)
210    }
211
212    /// Minus operation: compute tangent space difference between two manifold values.
213    ///
214    /// This method computes the tangent vector that would transform this variable's
215    /// value to the other variable's value using the manifold's minus operation
216    /// (typically the logarithmic map).
217    ///
218    /// # Arguments
219    /// * `other` - The other variable to compute the difference to
220    ///
221    /// # Returns
222    /// A tangent vector representing the difference in tangent space
223    ///
224    /// # Examples
225    /// ```
226    /// use apex_solver::core::variable::Variable;
227    /// use apex_solver::manifold::se2::SE2;
228    ///
229    /// let se2_1 = SE2::from_xy_angle(2.0, 3.0, 0.5);
230    /// let se2_2 = SE2::from_xy_angle(1.0, 2.0, 0.0);
231    /// let var1 = Variable::new(se2_1);
232    /// let var2 = Variable::new(se2_2);
233    ///
234    /// let difference = var1.minus(&var2);
235    /// ```
236    pub fn minus(&self, other: &Self) -> M::TangentVector {
237        self.value.minus(&other.value, None, None)
238    }
239
240    /// Get the covariance matrix for this variable (if computed).
241    ///
242    /// Returns `None` if covariance has not been computed.
243    ///
244    /// # Returns
245    /// Reference to the covariance matrix in tangent space
246    pub fn get_covariance(&self) -> Option<&Mat<f64>> {
247        self.covariance.as_ref()
248    }
249
250    /// Set the covariance matrix for this variable.
251    ///
252    /// The covariance matrix should be square with dimension equal to
253    /// the tangent space dimension of this variable.
254    ///
255    /// # Arguments
256    /// * `cov` - Covariance matrix in tangent space
257    pub fn set_covariance(&mut self, cov: Mat<f64>) {
258        self.covariance = Some(cov);
259    }
260
261    /// Clear the covariance matrix.
262    pub fn clear_covariance(&mut self) {
263        self.covariance = None;
264    }
265}
266
267// Extension implementation for Rn manifold (special case since it's Euclidean)
268use apex_manifolds::rn::Rn;
269
270impl Variable<Rn> {
271    /// Convert the Rn variable to a vector representation.
272    pub fn to_vector(&self) -> DVector<f64> {
273        self.value.data().clone()
274    }
275
276    /// Create an Rn variable from a vector representation.
277    pub fn from_vector(values: DVector<f64>) -> Self {
278        Self::new(Rn::new(values))
279    }
280
281    /// Update the Rn variable with bounds and fixed constraints.
282    pub fn update_variable(&mut self, mut tangent_delta: DVector<f64>) {
283        // bound
284        for (&idx, &(lower, upper)) in &self.bounds {
285            tangent_delta[idx] = tangent_delta[idx].max(lower).min(upper);
286        }
287
288        // fix
289        for &index_to_fix in &self.fixed_indices {
290            tangent_delta[index_to_fix] = self.value.data()[index_to_fix];
291        }
292
293        self.value = Rn::new(tangent_delta);
294    }
295}
296
297// ============================================================================
298// ManifoldVariable — object-safe trait for heterogeneous variable collections
299// ============================================================================
300
301/// Object-safe trait covering all manifold variable types.
302///
303/// Replaces `VariableEnum` as the common type for `HashMap<String, Box<dyn ManifoldVariable>>`.
304/// One blanket impl covers all 8 manifold types; no per-manifold match arms needed.
305///
306/// # Hot-path contract
307/// `as_param_slice`, `dof`, and `apply_tangent_step` must never heap-allocate.
308pub trait ManifoldVariable: Send + Sync + 'static {
309    // ── Hot path (zero allocation) ──────────────────────────────────────
310    /// Zero-copy borrow of the parameter storage backing this variable.
311    fn as_param_slice(&self) -> &[f64];
312    /// Tangent space dimension (degrees of freedom).
313    fn dof(&self) -> usize;
314    /// Apply a tangent-space step in-place: x ← x ⊞ δx.
315    /// Fixed indices are zeroed before the retraction.
316    fn apply_tangent_step(&mut self, step: &[f64]);
317
318    // ── Build-time configuration ─────────────────────────────────────────
319    fn set_fixed_indices(&mut self, indices: HashSet<usize>);
320    fn get_fixed_indices(&self) -> &HashSet<usize>;
321    fn set_bounds(&mut self, bounds: HashMap<usize, (f64, f64)>);
322    fn get_bounds(&self) -> &HashMap<usize, (f64, f64)>;
323    /// Static name of the underlying manifold type ("SE3", "SO3", …).
324    fn manifold_type_name(&self) -> &'static str;
325
326    // ── Fixed/free column layout (ISSUE-0003) ───────────────────────────
327    // Fixed tangent coordinates must never occupy a column in the linear
328    // solve — solving the full system and zeroing the fixed entries of the
329    // step afterward is not equivalent, since free and fixed columns
330    // generally couple through JᵀJ. These map a variable's local tangent
331    // indices to a compacted "free-column" space instead.
332    /// Number of un-fixed tangent coordinates.
333    fn free_dof(&self) -> usize {
334        self.dof() - self.get_fixed_indices().len()
335    }
336
337    /// Map a local tangent index (`0..dof()`) to its offset within this
338    /// variable's free-column block, or `None` if that index is fixed.
339    fn local_free_offset(&self, local: usize) -> Option<usize> {
340        let fixed = self.get_fixed_indices();
341        if fixed.is_empty() {
342            // Fast path: identity mapping, no HashSet touch.
343            return Some(local);
344        }
345        if fixed.contains(&local) {
346            return None;
347        }
348        Some(local - fixed.iter().filter(|&&i| i < local).count())
349    }
350
351    /// Expand a free-space step (length `free_dof()`) into a `dof()`-length
352    /// buffer, writing `0.0` at every fixed local index in ascending order.
353    fn expand_free_step(&self, free_step: &[f64], out: &mut [f64]) {
354        let fixed = self.get_fixed_indices();
355        let dof = self.dof();
356        if fixed.is_empty() {
357            out[..dof].copy_from_slice(&free_step[..dof]);
358            return;
359        }
360        let mut free_idx = 0usize;
361        for (local, slot) in out.iter_mut().enumerate().take(dof) {
362            *slot = if fixed.contains(&local) {
363                0.0
364            } else {
365                let v = free_step.get(free_idx).copied().unwrap_or(0.0);
366                free_idx += 1;
367                v
368            };
369        }
370    }
371
372    // ── Covariance ───────────────────────────────────────────────────────
373    fn set_covariance(&mut self, cov: faer::Mat<f64>);
374    fn covariance(&self) -> Option<&faer::Mat<f64>>;
375    fn clear_covariance(&mut self);
376
377    // ── I/O (NOT hot path — allocates) ───────────────────────────────────
378    fn to_dvector(&self) -> DVector<f64>;
379
380    // ── Downcast support ─────────────────────────────────────────────────
381    fn as_any(&self) -> &dyn Any;
382    fn as_any_mut(&mut self) -> &mut dyn Any;
383    fn clone_box(&self) -> Box<dyn ManifoldVariable>;
384}
385
386impl Clone for Box<dyn ManifoldVariable> {
387    fn clone(&self) -> Self {
388        self.clone_box()
389    }
390}
391
392impl<M> ManifoldVariable for Variable<M>
393where
394    M: LieGroup + Clone + Send + Sync + 'static,
395    M::TangentVector: Tangent<M> + Send + Sync + 'static,
396{
397    fn as_param_slice(&self) -> &[f64] {
398        self.value.as_param_slice()
399    }
400
401    fn dof(&self) -> usize {
402        self.value.tangent_dim()
403    }
404
405    fn apply_tangent_step(&mut self, step: &[f64]) {
406        let dof = self.dof();
407        // Stack buffer for fixed-size manifolds (all current types have DOF ≤ 10).
408        // Fall back to heap Vec only for large dynamic Rn variables.
409        if dof <= 16 {
410            let mut buf = [0f64; 16];
411            buf[..dof].copy_from_slice(&step[..dof]);
412            for &idx in &self.fixed_indices {
413                if idx < dof {
414                    buf[idx] = 0.0;
415                }
416            }
417            let tangent = M::TangentVector::from_slice(&buf[..dof]);
418            self.value = self.value.plus(&tangent, None, None);
419        } else {
420            let mut v = step[..dof].to_vec();
421            for &idx in &self.fixed_indices {
422                if idx < dof {
423                    v[idx] = 0.0;
424                }
425            }
426            let tangent = M::TangentVector::from_slice(&v);
427            self.value = self.value.plus(&tangent, None, None);
428        }
429    }
430
431    fn manifold_type_name(&self) -> &'static str {
432        M::NAME
433    }
434
435    fn set_fixed_indices(&mut self, indices: HashSet<usize>) {
436        self.fixed_indices = indices;
437    }
438
439    fn get_fixed_indices(&self) -> &HashSet<usize> {
440        &self.fixed_indices
441    }
442
443    fn set_bounds(&mut self, bounds: HashMap<usize, (f64, f64)>) {
444        self.bounds = bounds;
445    }
446
447    fn get_bounds(&self) -> &HashMap<usize, (f64, f64)> {
448        &self.bounds
449    }
450
451    fn set_covariance(&mut self, cov: faer::Mat<f64>) {
452        self.covariance = Some(cov);
453    }
454
455    fn covariance(&self) -> Option<&faer::Mat<f64>> {
456        self.covariance.as_ref()
457    }
458
459    fn clear_covariance(&mut self) {
460        self.covariance = None;
461    }
462
463    fn to_dvector(&self) -> DVector<f64> {
464        DVector::from_column_slice(self.value.as_param_slice())
465    }
466
467    fn as_any(&self) -> &dyn Any {
468        self
469    }
470
471    fn as_any_mut(&mut self) -> &mut dyn Any {
472        self
473    }
474
475    fn clone_box(&self) -> Box<dyn ManifoldVariable> {
476        Box::new(self.clone())
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use apex_manifolds::{rn::Rn, se2::SE2, se3::SE3, so2::SO2, so3::SO3};
484    use nalgebra::{DVector, Quaternion, Vector3};
485    use std;
486
487    #[test]
488    fn test_variable_creation_rn() {
489        let vec_data = DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
490        let rn_value = Rn::new(vec_data);
491        let variable = Variable::new(rn_value);
492
493        // Use get_size for Rn manifold (returns dynamic size)
494        assert_eq!(variable.get_size(), 5);
495        assert!(variable.fixed_indices.is_empty());
496        assert!(variable.bounds.is_empty());
497    }
498
499    #[test]
500    fn test_variable_creation_se2() {
501        let se2 = SE2::from_xy_angle(1.0, 2.0, 0.5);
502        let variable = Variable::new(se2);
503
504        assert_eq!(variable.get_size(), SE2::DOF);
505        assert!(variable.fixed_indices.is_empty());
506        assert!(variable.bounds.is_empty());
507    }
508
509    #[test]
510    fn test_variable_creation_se3() {
511        let se3 = SE3::from_translation_quaternion(
512            Vector3::new(1.0, 2.0, 3.0),
513            Quaternion::new(1.0, 0.0, 0.0, 0.0),
514        );
515        let variable = Variable::new(se3);
516
517        assert_eq!(variable.get_size(), SE3::DOF);
518        assert!(variable.fixed_indices.is_empty());
519        assert!(variable.bounds.is_empty());
520    }
521
522    #[test]
523    fn test_variable_creation_so2() {
524        let so2 = SO2::from_angle(0.5);
525        let variable = Variable::new(so2);
526
527        assert_eq!(variable.get_size(), SO2::DOF);
528        assert!(variable.fixed_indices.is_empty());
529        assert!(variable.bounds.is_empty());
530    }
531
532    #[test]
533    fn test_variable_creation_so3() {
534        let so3 = SO3::from_euler_angles(0.1, 0.2, 0.3);
535        let variable = Variable::new(so3);
536
537        assert_eq!(variable.get_size(), SO3::DOF);
538        assert!(variable.fixed_indices.is_empty());
539        assert!(variable.bounds.is_empty());
540    }
541
542    #[test]
543    fn test_variable_set_value() {
544        let initial_vec = DVector::from_vec(vec![1.0, 2.0, 3.0]);
545        let mut variable = Variable::new(Rn::new(initial_vec));
546
547        let new_vec = DVector::from_vec(vec![4.0, 5.0, 6.0, 7.0]);
548        variable.set_value(Rn::new(new_vec));
549        assert_eq!(variable.get_size(), 4);
550
551        let se2_initial = SE2::from_xy_angle(0.0, 0.0, 0.0);
552        let mut se2_variable = Variable::new(se2_initial);
553
554        let se2_new = SE2::from_xy_angle(1.0, 2.0, std::f64::consts::PI / 4.0);
555        se2_variable.set_value(se2_new);
556        assert_eq!(se2_variable.get_size(), SE2::DOF);
557    }
558
559    #[test]
560    fn test_variable_plus_minus_operations() {
561        // Test SE2 manifold plus/minus operations
562        let se2_1 = SE2::from_xy_angle(2.0, 3.0, std::f64::consts::PI / 2.0);
563        let se2_2 = SE2::from_xy_angle(1.0, 1.0, std::f64::consts::PI / 4.0);
564        let var1 = Variable::new(se2_1);
565        let var2 = Variable::new(se2_2);
566
567        let diff_tangent = var1.minus(&var2);
568        let var2_updated = var2.plus(&diff_tangent);
569        let final_diff = var1.minus(&Variable::new(var2_updated));
570
571        assert!(DVector::from_column_slice(final_diff.as_slice()).norm() < 1e-10);
572    }
573
574    #[test]
575    fn test_variable_rn_plus_minus_operations() {
576        // Test Rn manifold plus/minus operations
577        let rn_1 = Rn::new(DVector::from_vec(vec![1.0, 2.0, 3.0]));
578        let rn_2 = Rn::new(DVector::from_vec(vec![4.0, 5.0, 6.0]));
579        let var1 = Variable::new(rn_1);
580        let var2 = Variable::new(rn_2);
581
582        // Test minus operation
583        let diff_tangent = var1.minus(&var2);
584        assert_eq!(
585            diff_tangent.to_vector(),
586            DVector::from_vec(vec![-3.0, -3.0, -3.0])
587        );
588
589        // Test plus operation
590        let var2_updated = var2.plus(&diff_tangent);
591        assert_eq!(var2_updated.data(), &DVector::from_vec(vec![1.0, 2.0, 3.0]));
592
593        // Test roundtrip consistency
594        let final_diff = var1.minus(&Variable::new(var2_updated));
595        assert!(final_diff.to_vector().norm() < 1e-10);
596    }
597
598    #[test]
599    fn test_variable_update_with_bounds() {
600        let vec_data = DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
601        let mut variable = Variable::new(Rn::new(vec_data));
602
603        variable.bounds.insert(0, (-1.0, 1.0));
604        variable.bounds.insert(2, (0.0, 5.0));
605
606        let new_values = DVector::from_vec(vec![-5.0, 10.0, -3.0, 20.0, 30.0, 40.0]);
607        variable.update_variable(new_values);
608
609        let result_vec = variable.to_vector();
610        assert!(result_vec.len() == 6);
611    }
612
613    #[test]
614    fn test_variable_update_with_fixed_indices() {
615        let vec_data = DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
616        let mut variable = Variable::new(Rn::new(vec_data.clone()));
617
618        variable.fixed_indices.insert(1);
619        variable.fixed_indices.insert(4);
620
621        let delta_values = DVector::from_vec(vec![9.0, 18.0, 27.0, 36.0, 45.0, 54.0, 63.0, 72.0]);
622        variable.update_variable(delta_values);
623
624        let result_vec = variable.to_vector();
625        assert_eq!(result_vec[1], 2.0);
626        assert_eq!(result_vec[4], 5.0);
627        assert!(result_vec.len() == 8);
628    }
629
630    #[test]
631    fn test_variable_combined_bounds_and_fixed() {
632        let vec_data = DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
633        let mut variable = Variable::new(Rn::new(vec_data.clone()));
634
635        variable.bounds.insert(0, (-2.0, 2.0));
636        variable.bounds.insert(3, (-1.0, 1.0));
637        variable.fixed_indices.insert(1);
638        variable.fixed_indices.insert(5);
639
640        let delta_values = DVector::from_vec(vec![-5.0, 100.0, 30.0, 10.0, 50.0, 600.0, 70.0]);
641        variable.update_variable(delta_values);
642
643        let result = variable.to_vector();
644        assert_eq!(result[1], 2.0);
645        assert_eq!(result[5], 6.0);
646        assert!(result.len() == 7);
647    }
648
649    #[test]
650    fn test_variable_type_safety() {
651        let se2_var = Variable::new(SE2::from_xy_angle(1.0, 2.0, 0.5));
652        let se3_var = Variable::new(SE3::from_translation_quaternion(
653            Vector3::new(1.0, 2.0, 3.0),
654            Quaternion::new(1.0, 0.0, 0.0, 0.0),
655        ));
656        let so2_var = Variable::new(SO2::from_angle(0.5));
657        let so3_var = Variable::new(SO3::from_euler_angles(0.1, 0.2, 0.3));
658        let rn_var = Variable::new(Rn::new(DVector::from_vec(vec![1.0, 2.0, 3.0])));
659
660        assert_eq!(se2_var.get_size(), SE2::DOF);
661        assert_eq!(se3_var.get_size(), SE3::DOF);
662        assert_eq!(so2_var.get_size(), SO2::DOF);
663        assert_eq!(so3_var.get_size(), SO3::DOF);
664        assert_eq!(rn_var.get_size(), 3);
665    }
666
667    #[test]
668    fn test_variable_vector_conversion_roundtrip() {
669        let original_data = DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
670        let rn_var = Variable::new(Rn::new(original_data.clone()));
671        let vec_repr = rn_var.to_vector();
672        assert_eq!(vec_repr, original_data);
673
674        let reconstructed_var = Variable::<Rn>::from_vector(vec_repr);
675        assert_eq!(reconstructed_var.to_vector(), original_data);
676    }
677
678    #[test]
679    fn test_variable_manifold_operations_consistency() {
680        // Test Rn manifold operations (has vector conversion methods)
681        let rn_initial = Rn::new(DVector::from_vec(vec![1.0, 2.0, 3.0]));
682        let mut rn_var = Variable::new(rn_initial);
683        let rn_new_values = DVector::from_vec(vec![2.0, 3.0, 4.0]);
684        rn_var.update_variable(rn_new_values);
685
686        let rn_result = rn_var.to_vector();
687        assert_eq!(rn_result, DVector::from_vec(vec![2.0, 3.0, 4.0]));
688
689        // Test SE2 manifold plus/minus operations (core functionality)
690        let se2_1 = SE2::from_xy_angle(2.0, 3.0, std::f64::consts::PI / 2.0);
691        let se2_2 = SE2::from_xy_angle(1.0, 1.0, std::f64::consts::PI / 4.0);
692        let var1 = Variable::new(se2_1);
693        let var2 = Variable::new(se2_2);
694
695        let diff_tangent = var1.minus(&var2);
696        let var2_updated = var2.plus(&diff_tangent);
697        let final_diff = var1.minus(&Variable::new(var2_updated));
698
699        // The final difference should be small (close to identity in tangent space)
700        assert!(DVector::from_column_slice(final_diff.as_slice()).norm() < 1e-10);
701    }
702
703    #[test]
704    fn test_variable_constraints_interaction() {
705        let rn_data = DVector::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0]);
706        let mut rn_var = Variable::new(Rn::new(rn_data));
707
708        rn_var.bounds.insert(0, (-1.0, 1.0));
709        rn_var.bounds.insert(2, (-10.0, 10.0));
710        rn_var.fixed_indices.insert(1);
711        rn_var.fixed_indices.insert(4);
712
713        let large_delta = DVector::from_vec(vec![5.0, 100.0, 15.0, 20.0, 200.0]);
714        rn_var.update_variable(large_delta);
715
716        let result = rn_var.to_vector();
717
718        assert_eq!(result[0], 1.0);
719        assert_eq!(result[1], 0.0);
720        assert_eq!(result[2], 10.0);
721        assert_eq!(result[3], 20.0);
722        assert_eq!(result[4], 0.0);
723    }
724}