use thiserror::Error;
pub mod between_factor;
pub mod prior_factor;
pub mod projection_factor;
pub use between_factor::BetweenFactor;
pub use prior_factor::PriorFactor;
pub use projection_factor::ProjectionFactor;
#[derive(Debug, Clone, Copy, Default)]
pub struct OptimizeParams<const POSE: bool, const LANDMARK: bool, const INTRINSIC: bool>;
impl<const P: bool, const L: bool, const I: bool> OptimizeParams<P, L, I> {
pub const POSE: bool = P;
pub const LANDMARK: bool = L;
pub const INTRINSIC: bool = I;
}
pub type BundleAdjustment = OptimizeParams<true, true, false>;
pub type SelfCalibration = OptimizeParams<true, true, true>;
pub type OnlyIntrinsics = OptimizeParams<false, false, true>;
pub type OnlyPose = OptimizeParams<true, false, false>;
pub type OnlyLandmarks = OptimizeParams<false, true, false>;
pub type PoseAndIntrinsics = OptimizeParams<true, false, true>;
pub type LandmarksAndIntrinsics = OptimizeParams<false, true, true>;
pub mod camera {
pub use apex_camera_models::*;
}
#[derive(Debug, Clone, Error)]
pub enum FactorError {
#[error("Invalid dimension: expected {expected}, got {actual}")]
InvalidDimension { expected: usize, actual: usize },
#[error("Invalid projection: {0}")]
InvalidProjection(String),
#[error("Jacobian computation failed: {0}")]
JacobianFailed(String),
#[error("Invalid parameter values: {0}")]
InvalidParameters(String),
#[error("Numerical instability: {0}")]
NumericalInstability(String),
}
pub type FactorResult<T> = Result<T, FactorError>;
pub trait Factor: Send + Sync {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
);
fn residual_dim(&self) -> usize;
fn jacobian_shape(&self) -> (usize, usize);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorLogging;
use faer::prelude::ReborrowMut;
use nalgebra::dvector;
#[test]
fn test_optimize_params_bundle_adjustment_flags() {
const { assert!(BundleAdjustment::POSE) };
const { assert!(BundleAdjustment::LANDMARK) };
const { assert!(!BundleAdjustment::INTRINSIC) };
}
#[test]
fn test_optimize_params_self_calibration_flags() {
const { assert!(SelfCalibration::POSE) };
const { assert!(SelfCalibration::LANDMARK) };
const { assert!(SelfCalibration::INTRINSIC) };
}
#[test]
fn test_optimize_params_only_intrinsics_flags() {
const { assert!(!OnlyIntrinsics::POSE) };
const { assert!(!OnlyIntrinsics::LANDMARK) };
const { assert!(OnlyIntrinsics::INTRINSIC) };
}
#[test]
fn test_optimize_params_only_pose_flags() {
const { assert!(OnlyPose::POSE) };
const { assert!(!OnlyPose::LANDMARK) };
const { assert!(!OnlyPose::INTRINSIC) };
}
#[test]
fn test_optimize_params_only_landmarks_flags() {
const { assert!(!OnlyLandmarks::POSE) };
const { assert!(OnlyLandmarks::LANDMARK) };
const { assert!(!OnlyLandmarks::INTRINSIC) };
}
#[test]
fn test_optimize_params_pose_and_intrinsics_flags() {
const { assert!(PoseAndIntrinsics::POSE) };
const { assert!(!PoseAndIntrinsics::LANDMARK) };
const { assert!(PoseAndIntrinsics::INTRINSIC) };
}
#[test]
fn test_optimize_params_landmarks_and_intrinsics_flags() {
const { assert!(!LandmarksAndIntrinsics::POSE) };
const { assert!(LandmarksAndIntrinsics::LANDMARK) };
const { assert!(LandmarksAndIntrinsics::INTRINSIC) };
}
#[test]
fn test_factor_error_invalid_dimension_display() {
let e = FactorError::InvalidDimension {
expected: 3,
actual: 6,
};
let s = e.to_string();
assert!(s.contains("3"), "{s}");
assert!(s.contains("6"), "{s}");
}
#[test]
fn test_factor_error_invalid_projection_display() {
let e = FactorError::InvalidProjection("behind camera".into());
assert!(e.to_string().contains("behind camera"));
}
#[test]
fn test_factor_error_jacobian_failed_display() {
let e = FactorError::JacobianFailed("singular".into());
assert!(e.to_string().contains("singular"));
}
#[test]
fn test_factor_error_invalid_parameters_display() {
let e = FactorError::InvalidParameters("nan detected".into());
assert!(e.to_string().contains("nan detected"));
}
#[test]
fn test_factor_error_numerical_instability_display() {
let e = FactorError::NumericalInstability("overflow".into());
assert!(e.to_string().contains("overflow"));
}
#[test]
fn test_factor_error_log_returns_self() {
let e = FactorError::JacobianFailed("test_log".into());
let returned = e.log();
assert!(returned.to_string().contains("test_log"));
}
#[test]
fn test_factor_error_log_with_source_returns_self() {
let e = FactorError::InvalidProjection("proj_log".into());
let source = std::io::Error::other("src");
let returned = e.log_with_source(source);
assert!(returned.to_string().contains("proj_log"));
}
struct ConstantFactor {
value: f64,
}
impl Factor for ConstantFactor {
fn linearize(
&self,
params: &[&[f64]],
residual: &mut [f64],
jacobian: Option<faer::mat::MatMut<'_, f64>>,
) {
residual[0] = params[0][0] - self.value;
if let Some(mut jac) = jacobian {
*jac.rb_mut().get_mut(0, 0) = 1.0;
}
}
fn residual_dim(&self) -> usize {
1
}
fn jacobian_shape(&self) -> (usize, usize) {
(1, 1)
}
}
#[test]
fn test_factor_compute_with_jacobian() {
let f = ConstantFactor { value: 3.0 };
let p = dvector![5.0];
let params: Vec<&[f64]> = vec![p.as_slice()];
let mut residual = vec![0.0f64; 1];
let mut jac_buf = vec![0.0f64; 1];
let jac_mut = faer::mat::MatMut::from_column_major_slice_mut(&mut jac_buf, 1, 1);
f.linearize(¶ms, &mut residual, Some(jac_mut));
assert!((residual[0] - 2.0).abs() < 1e-12);
assert!((jac_buf[0] - 1.0).abs() < 1e-12);
}
#[test]
fn test_factor_compute_without_jacobian() {
let f = ConstantFactor { value: 3.0 };
let p = dvector![5.0];
let params: Vec<&[f64]> = vec![p.as_slice()];
let mut residual = vec![0.0f64; 1];
f.linearize(¶ms, &mut residual, None);
assert!((residual[0] - 2.0).abs() < 1e-12);
}
#[test]
fn test_factor_residual_dim() {
let f = ConstantFactor { value: 0.0 };
assert_eq!(f.residual_dim(), 1);
}
#[test]
fn test_factor_result_ok() {
let r: FactorResult<f64> = Ok(1.0);
assert!(r.is_ok());
}
#[test]
fn test_factor_result_err() {
let r: FactorResult<f64> = Err(FactorError::InvalidParameters("bad".into()));
assert!(r.is_err());
}
}