use crate::core::problem::VariableEnum;
use crate::observers::{ObserverError, ObserverResult, OptObserver};
use apex_io as io;
use apex_manifolds::se3::SE3;
use apex_manifolds::{LieGroup, ManifoldType};
use faer::Mat;
use faer::sparse;
use nalgebra::DVector;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use tracing::{info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VisualizationMode {
Iterative,
#[default]
InitialAndFinal,
}
#[derive(Debug, Clone)]
pub struct VisualizationConfig {
pub show_cameras: bool,
pub show_landmarks: bool,
pub show_se2_poses: bool,
pub show_plots: bool,
pub show_matrices: bool,
pub camera_fov: f32,
pub camera_aspect_ratio: f32,
pub camera_frustum_scale: f32,
pub landmark_point_size: f32,
pub initial_landmark_color: [u8; 3],
pub optimized_landmark_color: [u8; 3],
pub se2_pose_radius: f32,
pub se2_box_half_size: f32,
pub initial_se2_color: [u8; 3],
pub optimized_se2_color: [u8; 3],
pub hessian_downsample_size: usize,
pub gradient_bar_width: usize,
pub graph_scale: f32,
pub invert_camera_poses: bool,
pub visualization_mode: VisualizationMode,
}
impl Default for VisualizationConfig {
fn default() -> Self {
Self {
show_cameras: true,
show_landmarks: true,
show_se2_poses: true,
show_plots: true,
show_matrices: true,
camera_fov: 0.5,
camera_aspect_ratio: 1.0,
camera_frustum_scale: 1.0,
landmark_point_size: 0.02,
initial_landmark_color: [100, 150, 255], optimized_landmark_color: [255, 200, 50],
se2_pose_radius: 0.5,
se2_box_half_size: 0.3,
initial_se2_color: [100, 150, 255], optimized_se2_color: [50, 200, 100],
hessian_downsample_size: 100,
gradient_bar_width: 100,
graph_scale: 1.0,
invert_camera_poses: false,
visualization_mode: VisualizationMode::default(),
}
}
}
impl VisualizationConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_show_cameras(mut self, show: bool) -> Self {
self.show_cameras = show;
self
}
pub fn with_show_landmarks(mut self, show: bool) -> Self {
self.show_landmarks = show;
self
}
pub fn with_show_se2_poses(mut self, show: bool) -> Self {
self.show_se2_poses = show;
self
}
pub fn with_show_plots(mut self, show: bool) -> Self {
self.show_plots = show;
self
}
pub fn with_show_matrices(mut self, show: bool) -> Self {
self.show_matrices = show;
self
}
pub fn with_camera_fov(mut self, fov: f32) -> Self {
self.camera_fov = fov;
self
}
pub fn with_camera_aspect_ratio(mut self, ratio: f32) -> Self {
self.camera_aspect_ratio = ratio;
self
}
pub fn with_camera_frustum_scale(mut self, scale: f32) -> Self {
self.camera_frustum_scale = scale;
self
}
pub fn with_landmark_point_size(mut self, size: f32) -> Self {
self.landmark_point_size = size;
self
}
pub fn with_initial_landmark_color(mut self, rgb: [u8; 3]) -> Self {
self.initial_landmark_color = rgb;
self
}
pub fn with_optimized_landmark_color(mut self, rgb: [u8; 3]) -> Self {
self.optimized_landmark_color = rgb;
self
}
pub fn with_se2_pose_radius(mut self, radius: f32) -> Self {
self.se2_pose_radius = radius;
self
}
pub fn with_se2_box_half_size(mut self, half_size: f32) -> Self {
self.se2_box_half_size = half_size;
self
}
pub fn with_initial_se2_color(mut self, rgb: [u8; 3]) -> Self {
self.initial_se2_color = rgb;
self
}
pub fn with_optimized_se2_color(mut self, rgb: [u8; 3]) -> Self {
self.optimized_se2_color = rgb;
self
}
pub fn with_hessian_downsample_size(mut self, size: usize) -> Self {
self.hessian_downsample_size = size;
self
}
pub fn with_gradient_bar_width(mut self, width: usize) -> Self {
self.gradient_bar_width = width;
self
}
pub fn with_graph_scale(mut self, scale: f32) -> Self {
self.graph_scale = scale;
self
}
pub fn with_invert_camera_poses(mut self, invert: bool) -> Self {
self.invert_camera_poses = invert;
self
}
pub fn with_visualization_mode(mut self, mode: VisualizationMode) -> Self {
self.visualization_mode = mode;
self
}
pub fn cameras_only() -> Self {
Self::default()
.with_show_cameras(true)
.with_show_landmarks(false)
.with_show_se2_poses(false)
}
pub fn landmarks_only() -> Self {
Self::default()
.with_show_cameras(false)
.with_show_landmarks(true)
.with_show_se2_poses(false)
}
pub fn for_bundle_adjustment() -> Self {
Self::default()
.with_invert_camera_poses(true)
.with_show_se2_poses(false)
.with_camera_frustum_scale(0.3)
}
pub fn for_pose_graph() -> Self {
Self::default()
.with_show_landmarks(false)
.with_invert_camera_poses(false)
}
}
pub struct RerunObserver {
rec: Option<rerun::RecordingStream>,
enabled: bool,
iteration_metrics: RefCell<IterationMetrics>,
config: VisualizationConfig,
initial_camera_positions: RefCell<HashMap<String, [f32; 3]>>,
initial_landmark_positions: RefCell<HashMap<String, [f32; 3]>>,
initial_state_logged: Cell<bool>,
}
#[derive(Default, Clone)]
struct IterationMetrics {
cost: Option<f64>,
gradient_norm: Option<f64>,
damping: Option<f64>,
step_norm: Option<f64>,
step_quality: Option<f64>,
hessian: Option<sparse::SparseColMat<usize, f64>>,
gradient: Option<Mat<f64>>,
}
impl RerunObserver {
pub fn new(enabled: bool) -> ObserverResult<Self> {
Self::new_with_options(enabled, None)
}
pub fn new_with_options(enabled: bool, save_path: Option<&str>) -> ObserverResult<Self> {
Self::with_config(enabled, save_path, VisualizationConfig::default())
}
pub fn with_config(
enabled: bool,
save_path: Option<&str>,
config: VisualizationConfig,
) -> ObserverResult<Self> {
let rec = if enabled {
let rec = if let Some(path) = save_path {
info!("Saving visualization to: {}", path);
rerun::RecordingStreamBuilder::new("apex-solver-optimization")
.save(path)
.map_err(|e| {
ObserverError::RecordingSaveFailed {
path: path.to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?
} else {
match rerun::RecordingStreamBuilder::new("apex-solver-optimization").spawn() {
Ok(rec) => {
info!("Rerun viewer launched successfully");
rec
}
Err(e) => {
warn!("Could not launch Rerun viewer: {}", e);
warn!("Saving to file 'optimization.rrd' instead");
warn!("View it later with: rerun optimization.rrd");
rerun::RecordingStreamBuilder::new("apex-solver-optimization")
.save("optimization.rrd")
.map_err(|e2| {
ObserverError::RecordingSaveFailed {
path: "optimization.rrd".to_string(),
reason: format!("{}", e2),
}
.log_with_source(e2)
})?
}
}
};
Some(rec)
} else {
None
};
Ok(Self {
rec,
enabled,
iteration_metrics: RefCell::new(IterationMetrics::default()),
config,
initial_camera_positions: RefCell::new(HashMap::new()),
initial_landmark_positions: RefCell::new(HashMap::new()),
initial_state_logged: Cell::new(false),
})
}
pub fn new_for_bundle_adjustment(
enabled: bool,
save_path: Option<&str>,
invert_camera_poses: bool,
) -> ObserverResult<Self> {
let config = VisualizationConfig::for_bundle_adjustment()
.with_invert_camera_poses(invert_camera_poses);
Self::with_config(enabled, save_path, config)
}
pub fn config(&self) -> &VisualizationConfig {
&self.config
}
#[inline(always)]
pub fn is_enabled(&self) -> bool {
self.enabled && self.rec.is_some()
}
pub fn set_iteration_metrics(
&self,
cost: f64,
gradient_norm: f64,
damping: Option<f64>,
step_norm: f64,
step_quality: Option<f64>,
) {
let mut metrics = self.iteration_metrics.borrow_mut();
metrics.cost = Some(cost);
metrics.gradient_norm = Some(gradient_norm);
metrics.damping = damping;
metrics.step_norm = Some(step_norm);
metrics.step_quality = step_quality;
}
pub fn set_matrix_data(
&self,
hessian: Option<sparse::SparseColMat<usize, f64>>,
gradient: Option<Mat<f64>>,
) {
let mut metrics = self.iteration_metrics.borrow_mut();
metrics.hessian = hessian;
metrics.gradient = gradient;
}
pub fn log_initial_graph(&self, graph: &io::Graph, scale: f32) -> ObserverResult<()> {
let rec = self.rec.as_ref().ok_or_else(|| {
ObserverError::InvalidState("Recording stream not initialized".to_string())
})?;
if self.config.show_cameras {
for (id, vertex) in &graph.vertices_se3 {
let (position, rotation) = vertex.to_rerun_transform(scale);
let transform = rerun::Transform3D::from_translation_rotation(position, rotation);
let entity_path = format!("initial_graph/se3_poses/{}", id);
rec.log(entity_path.as_str(), &transform).map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
rec.log(
entity_path.as_str(),
&rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
self.config.camera_fov,
self.config.camera_aspect_ratio,
)
.with_image_plane_distance(self.config.camera_frustum_scale),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
}
if self.config.show_se2_poses && !graph.vertices_se2.is_empty() {
let positions: Vec<[f32; 2]> = graph
.vertices_se2
.values()
.map(|vertex| vertex.to_rerun_position_2d(scale))
.collect();
let color = self.config.initial_se2_color;
let colors = vec![
rerun::components::Color::from_rgb(color[0], color[1], color[2]);
positions.len()
];
rec.log(
"initial_graph/se2_poses",
&rerun::archetypes::Points2D::new(positions)
.with_colors(colors)
.with_radii([self.config.se2_pose_radius * scale]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "initial_graph/se2_poses".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
Ok(())
}
pub fn log_convergence(&self, status: &str) -> ObserverResult<()> {
let rec = self.rec.as_ref().ok_or_else(|| {
ObserverError::InvalidState("Recording stream not initialized".to_string())
})?;
rec.log(
"optimization/status",
&rerun::archetypes::TextDocument::new(status),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "optimization/status".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
Ok(())
}
pub fn log_initial_ba_state(
&self,
initial_values: &HashMap<String, (ManifoldType, DVector<f64>)>,
) -> ObserverResult<()> {
let rec = self.rec.as_ref().ok_or_else(|| {
ObserverError::InvalidState("Recording stream not initialized".to_string())
})?;
let mut landmark_positions: Vec<[f32; 3]> = Vec::new();
let mut landmark_names: Vec<String> = Vec::new();
let mut camera_cache = self.initial_camera_positions.borrow_mut();
let mut landmark_cache = self.initial_landmark_positions.borrow_mut();
for (var_name, (manifold_type, data)) in initial_values {
match manifold_type {
ManifoldType::SE3 if self.config.show_cameras => {
let se3 = SE3::from(data.clone());
let pose = if self.config.invert_camera_poses {
se3.inverse(None)
} else {
se3
};
let trans = pose.translation();
let rot = pose.rotation_quaternion();
camera_cache.insert(
var_name.clone(),
[trans.x as f32, trans.y as f32, trans.z as f32],
);
let position = rerun::external::glam::Vec3::new(
trans.x as f32,
trans.y as f32,
trans.z as f32,
);
let nq = rot.as_ref();
let rotation = rerun::external::glam::Quat::from_xyzw(
nq.i as f32,
nq.j as f32,
nq.k as f32,
nq.w as f32,
);
let transform =
rerun::Transform3D::from_translation_rotation(position, rotation);
let entity_path = format!("initial_graph/cameras/{}", var_name);
rec.log(entity_path.as_str(), &transform).map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
rec.log(
entity_path.as_str(),
&rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
self.config.camera_fov,
self.config.camera_aspect_ratio,
)
.with_image_plane_distance(self.config.camera_frustum_scale),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
ManifoldType::RN if self.config.show_landmarks => {
if data.len() == 3 {
let pos = [data[0] as f32, data[1] as f32, data[2] as f32];
landmark_positions.push(pos);
landmark_names.push(var_name.clone());
landmark_cache.insert(var_name.clone(), pos);
}
}
_ => {
}
}
}
if self.config.show_landmarks && !landmark_positions.is_empty() {
let color = self.config.initial_landmark_color;
rec.log(
"initial_graph/landmarks",
&rerun::archetypes::Points3D::new(landmark_positions)
.with_radii([self.config.landmark_point_size])
.with_colors([rerun::components::Color::from_rgb(
color[0], color[1], color[2],
)]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "initial_graph/landmarks".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
info!(
"Logged initial BA state: {} cameras, {} landmarks",
camera_cache.len(),
landmark_cache.len()
);
Ok(())
}
fn log_final_state(
&self,
values: &HashMap<String, VariableEnum>,
iterations: usize,
) -> ObserverResult<()> {
let rec = self.rec.as_ref().ok_or_else(|| {
ObserverError::InvalidState("Recording stream not initialized".to_string())
})?;
rec.set_time_sequence("iteration", iterations as i64);
let mut final_landmark_positions: Vec<[f32; 3]> = Vec::new();
let mut final_se2_positions: Vec<[f32; 2]> = Vec::new();
let mut camera_count = 0;
let mut se2_count = 0;
for (var_name, var) in values {
match var {
VariableEnum::SE3(v) if self.config.show_cameras => {
let pose = if self.config.invert_camera_poses {
v.value.inverse(None)
} else {
v.value.clone()
};
let trans = pose.translation();
let rot = pose.rotation_quaternion();
let position = rerun::external::glam::Vec3::new(
trans.x as f32,
trans.y as f32,
trans.z as f32,
);
let nq = rot.as_ref();
let rotation = rerun::external::glam::Quat::from_xyzw(
nq.i as f32,
nq.j as f32,
nq.k as f32,
nq.w as f32,
);
let transform =
rerun::Transform3D::from_translation_rotation(position, rotation);
let entity_path = format!("final_graph/cameras/{}", var_name);
rec.log(entity_path.as_str(), &transform).map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
rec.log(
entity_path.as_str(),
&rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
self.config.camera_fov,
self.config.camera_aspect_ratio,
)
.with_image_plane_distance(self.config.camera_frustum_scale),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
camera_count += 1;
}
VariableEnum::SE2(v) if self.config.show_se2_poses => {
final_se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
se2_count += 1;
}
VariableEnum::Rn(v) if self.config.show_landmarks => {
let data = v.value.data();
if data.len() == 3 {
final_landmark_positions.push([
data[0] as f32,
data[1] as f32,
data[2] as f32,
]);
}
}
_ => {
}
}
}
if self.config.show_landmarks && !final_landmark_positions.is_empty() {
let final_color: [u8; 3] = [50, 200, 100]; rec.log(
"final_graph/landmarks",
&rerun::archetypes::Points3D::new(final_landmark_positions.clone())
.with_radii([self.config.landmark_point_size])
.with_colors([rerun::components::Color::from_rgb(
final_color[0],
final_color[1],
final_color[2],
)]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "final_graph/landmarks".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
if self.config.show_se2_poses && !final_se2_positions.is_empty() {
let color = self.config.optimized_se2_color;
let hs = self.config.se2_box_half_size;
let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; final_se2_positions.len()];
rec.log(
"final_graph/se2_poses",
&rerun::archetypes::Boxes2D::from_centers_and_half_sizes(
final_se2_positions,
half_sizes,
)
.with_colors([rerun::components::Color::from_rgb(
color[0], color[1], color[2],
)]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "final_graph/se2_poses".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
info!(
"Logged final state after {} iterations: {} cameras, {} landmarks, {} SE2 poses",
iterations,
camera_count,
final_landmark_positions.len(),
se2_count
);
self.log_displacement_statistics(values)?;
Ok(())
}
fn log_displacement_statistics(
&self,
values: &HashMap<String, VariableEnum>,
) -> ObserverResult<()> {
let initial_cameras = self.initial_camera_positions.borrow();
let initial_landmarks = self.initial_landmark_positions.borrow();
let mut camera_displacements: Vec<f32> = Vec::new();
for (name, var) in values {
if let VariableEnum::SE3(v) = var {
let pose = if self.config.invert_camera_poses {
v.value.inverse(None)
} else {
v.value.clone()
};
if let Some(initial_pos) = initial_cameras.get(name) {
let final_pos = pose.translation();
let dx = final_pos.x as f32 - initial_pos[0];
let dy = final_pos.y as f32 - initial_pos[1];
let dz = final_pos.z as f32 - initial_pos[2];
let displacement = (dx * dx + dy * dy + dz * dz).sqrt();
camera_displacements.push(displacement);
}
}
}
let mut landmark_displacements: Vec<f32> = Vec::new();
for (name, var) in values {
if let VariableEnum::Rn(v) = var {
let data = v.value.data();
if data.len() == 3
&& let Some(initial_pos) = initial_landmarks.get(name)
{
let dx = data[0] as f32 - initial_pos[0];
let dy = data[1] as f32 - initial_pos[1];
let dz = data[2] as f32 - initial_pos[2];
let displacement = (dx * dx + dy * dy + dz * dz).sqrt();
landmark_displacements.push(displacement);
}
}
}
if !camera_displacements.is_empty() {
let avg = camera_displacements.iter().sum::<f32>() / camera_displacements.len() as f32;
let max = camera_displacements.iter().cloned().fold(0.0f32, f32::max);
let min = camera_displacements
.iter()
.cloned()
.fold(f32::MAX, f32::min);
info!(
"Camera displacement: avg={:.6}, min={:.6}, max={:.6} ({} cameras)",
avg,
min,
max,
camera_displacements.len()
);
}
if !landmark_displacements.is_empty() {
let avg =
landmark_displacements.iter().sum::<f32>() / landmark_displacements.len() as f32;
let max = landmark_displacements
.iter()
.cloned()
.fold(0.0f32, f32::max);
let min = landmark_displacements
.iter()
.cloned()
.fold(f32::MAX, f32::min);
info!(
"Landmark displacement: avg={:.6}, min={:.6}, max={:.6} ({} landmarks)",
avg,
min,
max,
landmark_displacements.len()
);
}
Ok(())
}
fn log_scalars(&self, iteration: usize, metrics: &IterationMetrics) -> ObserverResult<()> {
if !self.config.show_plots {
return Ok(());
}
let rec = self.rec.as_ref().ok_or_else(|| {
ObserverError::InvalidState("Recording stream not initialized".to_string())
})?;
rec.set_time_sequence("iteration", iteration as i64);
if let Some(cost) = metrics.cost {
rec.log("cost_plot/value", &rerun::archetypes::Scalars::new([cost]))
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "cost_plot/value".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
if let Some(gradient_norm) = metrics.gradient_norm {
rec.log(
"gradient_plot/norm",
&rerun::archetypes::Scalars::new([gradient_norm]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "gradient_plot/norm".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
if let Some(damping) = metrics.damping {
rec.log(
"damping_plot/lambda",
&rerun::archetypes::Scalars::new([damping]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "damping_plot/lambda".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
if let Some(step_norm) = metrics.step_norm {
rec.log(
"step_plot/norm",
&rerun::archetypes::Scalars::new([step_norm]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "step_plot/norm".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
if let Some(step_quality) = metrics.step_quality {
rec.log(
"quality_plot/rho",
&rerun::archetypes::Scalars::new([step_quality]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "quality_plot/rho".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
Ok(())
}
fn log_matrices(&self, iteration: usize, metrics: &IterationMetrics) -> ObserverResult<()> {
if !self.config.show_matrices {
return Ok(());
}
let rec = self.rec.as_ref().ok_or_else(|| {
ObserverError::InvalidState("Recording stream not initialized".to_string())
})?;
rec.set_time_sequence("iteration", iteration as i64);
if let Some(ref hessian) = metrics.hessian
&& let Ok(image_data) = self.sparse_hessian_to_image(hessian)
{
rec.log(
"optimization/matrices/hessian",
&rerun::archetypes::Tensor::new(image_data),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "optimization/matrices/hessian".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
if let Some(ref gradient) = metrics.gradient {
let grad_vec: Vec<f64> = (0..gradient.nrows()).map(|i| gradient[(i, 0)]).collect();
if let Ok(image_data) = self.gradient_to_image(&grad_vec) {
rec.log(
"optimization/matrices/gradient",
&rerun::archetypes::Tensor::new(image_data),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "optimization/matrices/gradient".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
}
Ok(())
}
fn log_initial_state(&self, variables: &HashMap<String, VariableEnum>) -> ObserverResult<()> {
let rec = self.rec.as_ref().ok_or_else(|| {
ObserverError::InvalidState("Recording stream not initialized".to_string())
})?;
rec.set_time_sequence("iteration", 0_i64);
let mut se2_positions: Vec<[f32; 2]> = Vec::new();
let mut landmark_positions: Vec<[f32; 3]> = Vec::new();
for (var_name, var) in variables {
match var {
VariableEnum::SE3(v) if self.config.show_cameras => {
let pose = if self.config.invert_camera_poses {
v.value.inverse(None)
} else {
v.value.clone()
};
let trans = pose.translation();
let rot = pose.rotation_quaternion();
let position = rerun::external::glam::Vec3::new(
trans.x as f32,
trans.y as f32,
trans.z as f32,
);
let nq = rot.as_ref();
let rotation = rerun::external::glam::Quat::from_xyzw(
nq.i as f32,
nq.j as f32,
nq.k as f32,
nq.w as f32,
);
let transform =
rerun::Transform3D::from_translation_rotation(position, rotation);
let entity_path = format!("initial_graph/cameras/{}", var_name);
rec.log(entity_path.as_str(), &transform).map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
rec.log(
entity_path.as_str(),
&rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
self.config.camera_fov,
self.config.camera_aspect_ratio,
)
.with_image_plane_distance(self.config.camera_frustum_scale),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
VariableEnum::SE2(v) if self.config.show_se2_poses => {
se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
}
VariableEnum::Rn(v) if self.config.show_landmarks => {
let data = v.value.data();
if data.len() == 3 {
landmark_positions.push([data[0] as f32, data[1] as f32, data[2] as f32]);
}
}
_ => {}
}
}
if self.config.show_landmarks && !landmark_positions.is_empty() {
let color = self.config.initial_landmark_color;
rec.log(
"initial_graph/landmarks",
&rerun::archetypes::Points3D::new(landmark_positions)
.with_radii([self.config.landmark_point_size])
.with_colors([rerun::components::Color::from_rgb(
color[0], color[1], color[2],
)]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "initial_graph/landmarks".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
if self.config.show_se2_poses && !se2_positions.is_empty() {
let color = self.config.initial_se2_color;
let hs = self.config.se2_box_half_size;
let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; se2_positions.len()];
rec.log(
"initial_graph/se2_poses",
&rerun::archetypes::Boxes2D::from_centers_and_half_sizes(se2_positions, half_sizes)
.with_colors([rerun::components::Color::from_rgb(
color[0], color[1], color[2],
)]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "initial_graph/se2_poses".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
self.initial_state_logged.set(true);
Ok(())
}
fn log_manifolds(
&self,
iteration: usize,
variables: &HashMap<String, VariableEnum>,
) -> ObserverResult<()> {
let rec = self.rec.as_ref().ok_or_else(|| {
ObserverError::InvalidState("Recording stream not initialized".to_string())
})?;
rec.set_time_sequence("iteration", iteration as i64);
let mut landmark_positions: Vec<[f32; 3]> = Vec::new();
let mut se2_positions: Vec<[f32; 2]> = Vec::new();
for (var_name, var) in variables {
match var {
VariableEnum::SE3(v) if self.config.show_cameras => {
let pose = if self.config.invert_camera_poses {
v.value.inverse(None)
} else {
v.value.clone()
};
let trans = pose.translation();
let rot = pose.rotation_quaternion();
let position = rerun::external::glam::Vec3::new(
trans.x as f32,
trans.y as f32,
trans.z as f32,
);
let nq = rot.as_ref();
let rotation = rerun::external::glam::Quat::from_xyzw(
nq.i as f32,
nq.j as f32,
nq.k as f32,
nq.w as f32,
);
let transform =
rerun::Transform3D::from_translation_rotation(position, rotation);
let entity_path = format!("optimized_graph/cameras/{}", var_name);
rec.log(entity_path.as_str(), &transform).map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
rec.log(
entity_path.as_str(),
&rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
self.config.camera_fov,
self.config.camera_aspect_ratio,
)
.with_image_plane_distance(self.config.camera_frustum_scale),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: entity_path.clone(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
VariableEnum::SE2(v) if self.config.show_se2_poses => {
se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
}
VariableEnum::Rn(v) if self.config.show_landmarks => {
let data = v.value.data();
if data.len() == 3 {
landmark_positions.push([data[0] as f32, data[1] as f32, data[2] as f32]);
}
}
_ => {
}
}
}
if self.config.show_landmarks && !landmark_positions.is_empty() {
let color = self.config.optimized_landmark_color;
rec.log(
"optimized_graph/landmarks",
&rerun::archetypes::Points3D::new(landmark_positions)
.with_radii([self.config.landmark_point_size])
.with_colors([rerun::components::Color::from_rgb(
color[0], color[1], color[2],
)]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "optimized_graph/landmarks".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
if self.config.show_se2_poses && !se2_positions.is_empty() {
let color = self.config.optimized_se2_color;
let hs = self.config.se2_box_half_size;
let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; se2_positions.len()];
rec.log(
"optimized_graph/se2_poses",
&rerun::archetypes::Boxes2D::from_centers_and_half_sizes(se2_positions, half_sizes)
.with_colors([rerun::components::Color::from_rgb(
color[0], color[1], color[2],
)]),
)
.map_err(|e| {
ObserverError::LoggingFailed {
entity_path: "optimized_graph/se2_poses".to_string(),
reason: format!("{}", e),
}
.log_with_source(e)
})?;
}
Ok(())
}
fn sparse_hessian_to_image(
&self,
hessian: &sparse::SparseColMat<usize, f64>,
) -> ObserverResult<rerun::datatypes::TensorData> {
let target_size = self.config.hessian_downsample_size;
let target_rows = target_size;
let target_cols = target_size;
let dense_matrix = Self::downsample_sparse_matrix(hessian, target_rows, target_cols);
let mut min_val = f64::INFINITY;
let mut max_val = f64::NEG_INFINITY;
for &val in &dense_matrix {
if val.is_finite() {
min_val = min_val.min(val);
max_val = max_val.max(val);
}
}
let max_abs = max_val.abs().max(min_val.abs());
let mut rgb_data = Vec::with_capacity(target_rows * target_cols * 3);
for &val in &dense_matrix {
let rgb = Self::value_to_rgb_heatmap(val, max_abs);
rgb_data.extend_from_slice(&rgb);
}
let tensor = rerun::datatypes::TensorData::new(
vec![target_rows as u64, target_cols as u64, 3],
rerun::datatypes::TensorBuffer::U8(rgb_data.into()),
);
Ok(tensor)
}
fn gradient_to_image(&self, gradient: &[f64]) -> ObserverResult<rerun::datatypes::TensorData> {
let n = gradient.len();
let bar_height = 50;
let target_width = self.config.gradient_bar_width;
let max_abs = gradient
.iter()
.map(|&x| x.abs())
.fold(0.0f64, |a, b| a.max(b));
let mut rgb_data = Vec::with_capacity(bar_height * target_width * 3);
for _ in 0..bar_height {
for i in 0..target_width {
let start = (i * n) / target_width;
let end = ((i + 1) * n) / target_width;
let sum: f64 = gradient[start..end].iter().sum();
let val = sum / (end - start).max(1) as f64;
let rgb = Self::value_to_rgb_heatmap(val, max_abs);
rgb_data.extend_from_slice(&rgb);
}
}
let tensor = rerun::datatypes::TensorData::new(
vec![bar_height as u64, target_width as u64, 3],
rerun::datatypes::TensorBuffer::U8(rgb_data.into()),
);
Ok(tensor)
}
fn downsample_sparse_matrix(
sparse: &sparse::SparseColMat<usize, f64>,
target_rows: usize,
target_cols: usize,
) -> Vec<f64> {
let m = sparse.nrows();
let n = sparse.ncols();
let mut downsampled = vec![0.0; target_rows * target_cols];
let mut counts = vec![0usize; target_rows * target_cols];
let symbolic = sparse.symbolic();
for col in 0..n {
let row_indices = symbolic.row_idx_of_col_raw(col);
let col_values = sparse.val_of_col(col);
for (idx_in_col, &row) in row_indices.iter().enumerate() {
let value = col_values[idx_in_col];
if value.abs() > 1e-12 {
let target_row = (row * target_rows) / m;
let target_col = (col * target_cols) / n;
let idx = target_row * target_cols + target_col;
downsampled[idx] += value;
counts[idx] += 1;
}
}
}
for i in 0..downsampled.len() {
if counts[i] > 0 {
downsampled[i] /= counts[i] as f64;
}
}
downsampled
}
fn value_to_rgb_heatmap(value: f64, max_abs: f64) -> [u8; 3] {
if !value.is_finite() || max_abs == 0.0 {
return [255, 255, 255];
}
let normalized = (value.abs() / max_abs).clamp(0.0, 1.0);
if normalized < 1e-10 {
[255, 255, 255]
} else {
let intensity = (normalized * 255.0) as u8;
let remaining = 255 - intensity;
[remaining, remaining, 255]
}
}
}
impl OptObserver for RerunObserver {
fn on_step(&self, values: &HashMap<String, VariableEnum>, iteration: usize) {
if !self.is_enabled() {
return;
}
if !self.initial_state_logged.get() {
if let Err(e) = self.log_initial_state(values) {
let _ = e.log();
}
}
let metrics = self.iteration_metrics.borrow();
if let Err(e) = self.log_scalars(iteration, &metrics) {
let _ = e.log();
}
let should_log_manifolds = match self.config.visualization_mode {
VisualizationMode::Iterative => true,
VisualizationMode::InitialAndFinal => false, };
if should_log_manifolds {
if let Err(e) = self.log_matrices(iteration, &metrics) {
let _ = e.log();
}
if let Err(e) = self.log_manifolds(iteration, values) {
let _ = e.log();
}
}
drop(metrics);
}
fn on_optimization_complete(&self, values: &HashMap<String, VariableEnum>, iterations: usize) {
if !self.is_enabled() {
return;
}
if let Err(e) = self.log_final_state(values, iterations) {
let _ = e.log();
}
}
}
impl Default for RerunObserver {
fn default() -> Self {
Self::new(false).unwrap_or_else(|_| Self {
rec: None,
enabled: false,
iteration_metrics: RefCell::new(IterationMetrics::default()),
config: VisualizationConfig::default(),
initial_camera_positions: RefCell::new(HashMap::new()),
initial_landmark_positions: RefCell::new(HashMap::new()),
initial_state_logged: Cell::new(false),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn test_observer_creation() -> TestResult {
let observer = RerunObserver::new(false)?;
assert!(!observer.is_enabled());
Ok(())
}
#[test]
fn test_rgb_heatmap_conversion() {
let rgb = RerunObserver::value_to_rgb_heatmap(0.0, 1.0);
assert_eq!(rgb, [255, 255, 255]);
let rgb = RerunObserver::value_to_rgb_heatmap(1.0, 1.0);
assert_eq!(rgb, [0, 0, 255]);
let rgb = RerunObserver::value_to_rgb_heatmap(-1.0, 1.0);
assert_eq!(rgb, [0, 0, 255]);
let rgb = RerunObserver::value_to_rgb_heatmap(0.5, 1.0);
assert_eq!(rgb, [128, 128, 255]);
}
#[test]
fn test_set_metrics() -> TestResult {
let observer = RerunObserver::new(false)?;
observer.set_iteration_metrics(1.0, 0.5, Some(0.01), 0.1, Some(0.95));
let metrics = observer.iteration_metrics.borrow();
assert_eq!(metrics.cost, Some(1.0));
assert_eq!(metrics.gradient_norm, Some(0.5));
assert_eq!(metrics.damping, Some(0.01));
assert_eq!(metrics.step_norm, Some(0.1));
assert_eq!(metrics.step_quality, Some(0.95));
Ok(())
}
#[test]
fn test_observer_trait() -> TestResult {
let observer = RerunObserver::new(false)?;
let values = HashMap::new();
observer.on_step(&values, 0);
observer.on_step(&values, 1);
Ok(())
}
}