1use crate::core::VarKey;
67use crate::core::variable::{ManifoldVariable, Variable};
68use crate::error::ErrorLogging;
69use crate::observers::{ObserverError, ObserverResult, OptObserver};
70use apex_io as io;
71use apex_manifolds::LieGroup;
72use apex_manifolds::rn::Rn;
73use apex_manifolds::se2::SE2;
74use apex_manifolds::se3::SE3;
75use faer::Mat;
76use faer::sparse;
77use slotmap::{Key, SlotMap};
78use std::cell::{Cell, RefCell};
79use std::collections::HashMap;
80use tracing::{info, warn};
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum VisualizationMode {
106 Iterative,
109
110 #[default]
113 InitialAndFinal,
114}
115
116#[derive(Debug, Clone)]
146pub struct VisualizationConfig {
147 pub show_cameras: bool,
150 pub show_landmarks: bool,
152 pub show_se2_poses: bool,
154 pub show_plots: bool,
156 pub show_matrices: bool,
158
159 pub camera_fov: f32,
162 pub camera_aspect_ratio: f32,
164 pub camera_frustum_scale: f32,
166
167 pub landmark_point_size: f32,
170 pub initial_landmark_color: [u8; 3],
172 pub optimized_landmark_color: [u8; 3],
174
175 pub se2_pose_radius: f32,
178 pub se2_box_half_size: f32,
180 pub initial_se2_color: [u8; 3],
182 pub optimized_se2_color: [u8; 3],
184
185 pub hessian_downsample_size: usize,
188 pub gradient_bar_width: usize,
190
191 pub graph_scale: f32,
194 pub invert_camera_poses: bool,
196 pub visualization_mode: VisualizationMode,
198}
199
200impl Default for VisualizationConfig {
201 fn default() -> Self {
202 Self {
203 show_cameras: true,
205 show_landmarks: true,
206 show_se2_poses: true,
207 show_plots: true,
208 show_matrices: true,
209
210 camera_fov: 0.5,
212 camera_aspect_ratio: 1.0,
213 camera_frustum_scale: 1.0,
214
215 landmark_point_size: 0.02,
217 initial_landmark_color: [100, 150, 255], optimized_landmark_color: [255, 200, 50], se2_pose_radius: 0.5,
222 se2_box_half_size: 0.3,
223 initial_se2_color: [100, 150, 255], optimized_se2_color: [50, 200, 100], hessian_downsample_size: 100,
228 gradient_bar_width: 100,
229
230 graph_scale: 1.0,
232 invert_camera_poses: false,
233 visualization_mode: VisualizationMode::default(),
234 }
235 }
236}
237
238impl VisualizationConfig {
239 pub fn new() -> Self {
241 Self::default()
242 }
243
244 pub fn with_show_cameras(mut self, show: bool) -> Self {
248 self.show_cameras = show;
249 self
250 }
251
252 pub fn with_show_landmarks(mut self, show: bool) -> Self {
254 self.show_landmarks = show;
255 self
256 }
257
258 pub fn with_show_se2_poses(mut self, show: bool) -> Self {
260 self.show_se2_poses = show;
261 self
262 }
263
264 pub fn with_show_plots(mut self, show: bool) -> Self {
266 self.show_plots = show;
267 self
268 }
269
270 pub fn with_show_matrices(mut self, show: bool) -> Self {
272 self.show_matrices = show;
273 self
274 }
275
276 pub fn with_camera_fov(mut self, fov: f32) -> Self {
280 self.camera_fov = fov;
281 self
282 }
283
284 pub fn with_camera_aspect_ratio(mut self, ratio: f32) -> Self {
286 self.camera_aspect_ratio = ratio;
287 self
288 }
289
290 pub fn with_camera_frustum_scale(mut self, scale: f32) -> Self {
292 self.camera_frustum_scale = scale;
293 self
294 }
295
296 pub fn with_landmark_point_size(mut self, size: f32) -> Self {
300 self.landmark_point_size = size;
301 self
302 }
303
304 pub fn with_initial_landmark_color(mut self, rgb: [u8; 3]) -> Self {
306 self.initial_landmark_color = rgb;
307 self
308 }
309
310 pub fn with_optimized_landmark_color(mut self, rgb: [u8; 3]) -> Self {
312 self.optimized_landmark_color = rgb;
313 self
314 }
315
316 pub fn with_se2_pose_radius(mut self, radius: f32) -> Self {
320 self.se2_pose_radius = radius;
321 self
322 }
323
324 pub fn with_se2_box_half_size(mut self, half_size: f32) -> Self {
326 self.se2_box_half_size = half_size;
327 self
328 }
329
330 pub fn with_initial_se2_color(mut self, rgb: [u8; 3]) -> Self {
332 self.initial_se2_color = rgb;
333 self
334 }
335
336 pub fn with_optimized_se2_color(mut self, rgb: [u8; 3]) -> Self {
338 self.optimized_se2_color = rgb;
339 self
340 }
341
342 pub fn with_hessian_downsample_size(mut self, size: usize) -> Self {
346 self.hessian_downsample_size = size;
347 self
348 }
349
350 pub fn with_gradient_bar_width(mut self, width: usize) -> Self {
352 self.gradient_bar_width = width;
353 self
354 }
355
356 pub fn with_graph_scale(mut self, scale: f32) -> Self {
360 self.graph_scale = scale;
361 self
362 }
363
364 pub fn with_invert_camera_poses(mut self, invert: bool) -> Self {
366 self.invert_camera_poses = invert;
367 self
368 }
369
370 pub fn with_visualization_mode(mut self, mode: VisualizationMode) -> Self {
385 self.visualization_mode = mode;
386 self
387 }
388
389 pub fn cameras_only() -> Self {
395 Self::default()
396 .with_show_cameras(true)
397 .with_show_landmarks(false)
398 .with_show_se2_poses(false)
399 }
400
401 pub fn landmarks_only() -> Self {
405 Self::default()
406 .with_show_cameras(false)
407 .with_show_landmarks(true)
408 .with_show_se2_poses(false)
409 }
410
411 pub fn for_bundle_adjustment() -> Self {
416 Self::default()
417 .with_invert_camera_poses(true)
418 .with_show_se2_poses(false)
419 .with_camera_frustum_scale(0.3)
420 }
421
422 pub fn for_pose_graph() -> Self {
427 Self::default()
428 .with_show_landmarks(false)
429 .with_invert_camera_poses(false)
430 }
431}
432
433pub struct RerunObserver {
468 rec: Option<rerun::RecordingStream>,
469 enabled: bool,
470 iteration_metrics: RefCell<IterationMetrics>,
473 config: VisualizationConfig,
475 initial_camera_positions: RefCell<HashMap<String, [f32; 3]>>,
477 initial_landmark_positions: RefCell<HashMap<String, [f32; 3]>>,
478 initial_state_logged: Cell<bool>,
480}
481
482#[derive(Default, Clone)]
487struct IterationMetrics {
488 cost: Option<f64>,
489 gradient_norm: Option<f64>,
490 damping: Option<f64>,
491 step_norm: Option<f64>,
492 step_quality: Option<f64>,
493 hessian: Option<sparse::SparseColMat<usize, f64>>,
494 gradient: Option<Mat<f64>>,
495}
496
497impl RerunObserver {
498 pub fn new(enabled: bool) -> ObserverResult<Self> {
519 Self::new_with_options(enabled, None)
520 }
521
522 pub fn new_with_options(enabled: bool, save_path: Option<&str>) -> ObserverResult<Self> {
544 Self::with_config(enabled, save_path, VisualizationConfig::default())
545 }
546
547 pub fn with_config(
573 enabled: bool,
574 save_path: Option<&str>,
575 config: VisualizationConfig,
576 ) -> ObserverResult<Self> {
577 let rec = if enabled {
578 let rec = if let Some(path) = save_path {
579 info!("Saving visualization to: {}", path);
581 rerun::RecordingStreamBuilder::new("apex-solver-optimization")
582 .save(path)
583 .map_err(|e| {
584 ObserverError::RecordingSaveFailed {
585 path: path.to_string(),
586 reason: format!("{}", e),
587 }
588 .log_with_source(e)
589 })?
590 } else {
591 match rerun::RecordingStreamBuilder::new("apex-solver-optimization").spawn() {
593 Ok(rec) => {
594 info!("Rerun viewer launched successfully");
595 rec
596 }
597 Err(e) => {
598 warn!("Could not launch Rerun viewer: {}", e);
599 warn!("Saving to file 'optimization.rrd' instead");
600 warn!("View it later with: rerun optimization.rrd");
601
602 rerun::RecordingStreamBuilder::new("apex-solver-optimization")
604 .save("optimization.rrd")
605 .map_err(|e2| {
606 ObserverError::RecordingSaveFailed {
607 path: "optimization.rrd".to_string(),
608 reason: format!("{}", e2),
609 }
610 .log_with_source(e2)
611 })?
612 }
613 }
614 };
615
616 Some(rec)
617 } else {
618 None
619 };
620
621 Ok(Self {
622 rec,
623 enabled,
624 iteration_metrics: RefCell::new(IterationMetrics::default()),
625 config,
626 initial_camera_positions: RefCell::new(HashMap::new()),
627 initial_landmark_positions: RefCell::new(HashMap::new()),
628 initial_state_logged: Cell::new(false),
629 })
630 }
631
632 pub fn new_for_bundle_adjustment(
664 enabled: bool,
665 save_path: Option<&str>,
666 invert_camera_poses: bool,
667 ) -> ObserverResult<Self> {
668 let config = VisualizationConfig::for_bundle_adjustment()
669 .with_invert_camera_poses(invert_camera_poses);
670 Self::with_config(enabled, save_path, config)
671 }
672
673 pub fn config(&self) -> &VisualizationConfig {
675 &self.config
676 }
677
678 #[inline(always)]
680 pub fn is_enabled(&self) -> bool {
681 self.enabled && self.rec.is_some()
682 }
683
684 pub fn set_iteration_metrics(
721 &self,
722 cost: f64,
723 gradient_norm: f64,
724 damping: Option<f64>,
725 step_norm: f64,
726 step_quality: Option<f64>,
727 ) {
728 let mut metrics = self.iteration_metrics.borrow_mut();
729 metrics.cost = Some(cost);
730 metrics.gradient_norm = Some(gradient_norm);
731 metrics.damping = damping;
732 metrics.step_norm = Some(step_norm);
733 metrics.step_quality = step_quality;
734 }
735
736 pub fn set_matrix_data(
745 &self,
746 hessian: Option<sparse::SparseColMat<usize, f64>>,
747 gradient: Option<Mat<f64>>,
748 ) {
749 let mut metrics = self.iteration_metrics.borrow_mut();
750 metrics.hessian = hessian;
751 metrics.gradient = gradient;
752 }
753
754 pub fn log_initial_graph(&self, graph: &io::Graph, scale: f32) -> ObserverResult<()> {
764 let rec = self.rec.as_ref().ok_or_else(|| {
765 ObserverError::InvalidState("Recording stream not initialized".to_string())
766 })?;
767
768 if self.config.show_cameras {
770 for (id, vertex) in &graph.vertices_se3 {
771 let (position, rotation) = vertex.to_rerun_transform(scale);
772 let transform = rerun::Transform3D::from_translation_rotation(position, rotation);
773
774 let entity_path = format!("initial_graph/se3_poses/{}", id);
775 rec.log(entity_path.as_str(), &transform).map_err(|e| {
776 ObserverError::LoggingFailed {
777 entity_path: entity_path.clone(),
778 reason: format!("{}", e),
779 }
780 .log_with_source(e)
781 })?;
782
783 rec.log(
785 entity_path.as_str(),
786 &rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
787 self.config.camera_fov,
788 self.config.camera_aspect_ratio,
789 )
790 .with_image_plane_distance(self.config.camera_frustum_scale),
791 )
792 .map_err(|e| {
793 ObserverError::LoggingFailed {
794 entity_path: entity_path.clone(),
795 reason: format!("{}", e),
796 }
797 .log_with_source(e)
798 })?;
799 }
800 }
801
802 if self.config.show_se2_poses && !graph.vertices_se2.is_empty() {
804 let positions: Vec<[f32; 2]> = graph
805 .vertices_se2
806 .values()
807 .map(|vertex| vertex.to_rerun_position_2d(scale))
808 .collect();
809
810 let color = self.config.initial_se2_color;
811 let colors = vec![
812 rerun::components::Color::from_rgb(color[0], color[1], color[2]);
813 positions.len()
814 ];
815
816 rec.log(
817 "initial_graph/se2_poses",
818 &rerun::archetypes::Points2D::new(positions)
819 .with_colors(colors)
820 .with_radii([self.config.se2_pose_radius * scale]),
821 )
822 .map_err(|e| {
823 ObserverError::LoggingFailed {
824 entity_path: "initial_graph/se2_poses".to_string(),
825 reason: format!("{}", e),
826 }
827 .log_with_source(e)
828 })?;
829 }
830
831 Ok(())
832 }
833
834 pub fn log_convergence(&self, status: &str) -> ObserverResult<()> {
842 let rec = self.rec.as_ref().ok_or_else(|| {
843 ObserverError::InvalidState("Recording stream not initialized".to_string())
844 })?;
845
846 rec.log(
848 "optimization/status",
849 &rerun::archetypes::TextDocument::new(status),
850 )
851 .map_err(|e| {
852 ObserverError::LoggingFailed {
853 entity_path: "optimization/status".to_string(),
854 reason: format!("{}", e),
855 }
856 .log_with_source(e)
857 })?;
858
859 Ok(())
860 }
861
862 pub fn log_initial_ba_state(
887 &self,
888 problem: &crate::core::problem::Problem,
889 ) -> ObserverResult<()> {
890 let rec = self.rec.as_ref().ok_or_else(|| {
891 ObserverError::InvalidState("Recording stream not initialized".to_string())
892 })?;
893
894 let mut landmark_positions: Vec<[f32; 3]> = Vec::new();
896 let mut landmark_names: Vec<String> = Vec::new();
897
898 let mut camera_cache = self.initial_camera_positions.borrow_mut();
900 let mut landmark_cache = self.initial_landmark_positions.borrow_mut();
901
902 for (key, var) in problem.variables.iter() {
903 let var_name = format!("var_{:?}", key.data());
904 match var.manifold_type_name() {
905 "SE3" if self.config.show_cameras => {
906 let se3 = SE3::from_param_slice(var.as_param_slice());
908
909 let pose = if self.config.invert_camera_poses {
911 se3.inverse(None)
912 } else {
913 se3
914 };
915
916 let trans = pose.translation();
917 let rot = pose.rotation_quaternion();
918
919 camera_cache.insert(
921 var_name.clone(),
922 [trans.x as f32, trans.y as f32, trans.z as f32],
923 );
924
925 let position = rerun::external::glam::Vec3::new(
926 trans.x as f32,
927 trans.y as f32,
928 trans.z as f32,
929 );
930
931 let nq = rot.as_ref();
932 let rotation = rerun::external::glam::Quat::from_xyzw(
933 nq.i as f32,
934 nq.j as f32,
935 nq.k as f32,
936 nq.w as f32,
937 );
938
939 let transform =
940 rerun::Transform3D::from_translation_rotation(position, rotation);
941
942 let entity_path = format!("initial_graph/cameras/{}", var_name);
943 rec.log(entity_path.as_str(), &transform).map_err(|e| {
944 ObserverError::LoggingFailed {
945 entity_path: entity_path.clone(),
946 reason: format!("{}", e),
947 }
948 .log_with_source(e)
949 })?;
950
951 rec.log(
952 entity_path.as_str(),
953 &rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
954 self.config.camera_fov,
955 self.config.camera_aspect_ratio,
956 )
957 .with_image_plane_distance(self.config.camera_frustum_scale),
958 )
959 .map_err(|e| {
960 ObserverError::LoggingFailed {
961 entity_path: entity_path.clone(),
962 reason: format!("{}", e),
963 }
964 .log_with_source(e)
965 })?;
966 }
967 "Rn" if self.config.show_landmarks && var.dof() == 3 => {
968 let data = var.as_param_slice();
969 let pos = [data[0] as f32, data[1] as f32, data[2] as f32];
970 landmark_positions.push(pos);
971 landmark_names.push(var_name.clone());
972 landmark_cache.insert(var_name.clone(), pos);
973 }
974 _ => {
975 }
977 }
978 }
979
980 if self.config.show_landmarks && !landmark_positions.is_empty() {
982 let color = self.config.initial_landmark_color;
983 rec.log(
984 "initial_graph/landmarks",
985 &rerun::archetypes::Points3D::new(landmark_positions)
986 .with_radii([self.config.landmark_point_size])
987 .with_colors([rerun::components::Color::from_rgb(
988 color[0], color[1], color[2],
989 )]),
990 )
991 .map_err(|e| {
992 ObserverError::LoggingFailed {
993 entity_path: "initial_graph/landmarks".to_string(),
994 reason: format!("{}", e),
995 }
996 .log_with_source(e)
997 })?;
998 }
999
1000 info!(
1001 "Logged initial BA state: {} cameras, {} landmarks",
1002 camera_cache.len(),
1003 landmark_cache.len()
1004 );
1005
1006 Ok(())
1007 }
1008
1009 fn log_final_state(
1020 &self,
1021 values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1022 iterations: usize,
1023 ) -> ObserverResult<()> {
1024 let rec = self.rec.as_ref().ok_or_else(|| {
1025 ObserverError::InvalidState("Recording stream not initialized".to_string())
1026 })?;
1027
1028 rec.set_time_sequence("iteration", iterations as i64);
1030
1031 let mut final_landmark_positions: Vec<[f32; 3]> = Vec::new();
1033 let mut final_se2_positions: Vec<[f32; 2]> = Vec::new();
1035 let mut camera_count = 0;
1036 let mut se2_count = 0;
1037
1038 for (i, (_, var)) in values.iter().enumerate() {
1039 let var_name = format!("var_{i}");
1040 if self.config.show_cameras
1041 && let Some(v) = var.as_any().downcast_ref::<Variable<SE3>>()
1042 {
1043 let pose = if self.config.invert_camera_poses {
1045 v.value.inverse(None)
1046 } else {
1047 v.value.clone()
1048 };
1049
1050 let trans = pose.translation();
1051 let rot = pose.rotation_quaternion();
1052
1053 let position = rerun::external::glam::Vec3::new(
1054 trans.x as f32,
1055 trans.y as f32,
1056 trans.z as f32,
1057 );
1058
1059 let nq = rot.as_ref();
1060 let rotation = rerun::external::glam::Quat::from_xyzw(
1061 nq.i as f32,
1062 nq.j as f32,
1063 nq.k as f32,
1064 nq.w as f32,
1065 );
1066
1067 let transform = rerun::Transform3D::from_translation_rotation(position, rotation);
1068
1069 let entity_path = format!("final_graph/cameras/{}", var_name);
1070 rec.log(entity_path.as_str(), &transform).map_err(|e| {
1071 ObserverError::LoggingFailed {
1072 entity_path: entity_path.clone(),
1073 reason: format!("{}", e),
1074 }
1075 .log_with_source(e)
1076 })?;
1077
1078 rec.log(
1079 entity_path.as_str(),
1080 &rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
1081 self.config.camera_fov,
1082 self.config.camera_aspect_ratio,
1083 )
1084 .with_image_plane_distance(self.config.camera_frustum_scale),
1085 )
1086 .map_err(|e| {
1087 ObserverError::LoggingFailed {
1088 entity_path: entity_path.clone(),
1089 reason: format!("{}", e),
1090 }
1091 .log_with_source(e)
1092 })?;
1093
1094 camera_count += 1;
1095 }
1096 if self.config.show_se2_poses
1097 && let Some(v) = var.as_any().downcast_ref::<Variable<SE2>>()
1098 {
1099 final_se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
1100 se2_count += 1;
1101 }
1102 if self.config.show_landmarks
1103 && let Some(v) = var.as_any().downcast_ref::<Variable<Rn>>()
1104 {
1105 let data = v.value.data();
1107 if data.len() == 3 {
1108 final_landmark_positions.push([data[0] as f32, data[1] as f32, data[2] as f32]);
1109 }
1110 }
1111 }
1112
1113 if self.config.show_landmarks && !final_landmark_positions.is_empty() {
1115 let final_color: [u8; 3] = [50, 200, 100]; rec.log(
1118 "final_graph/landmarks",
1119 &rerun::archetypes::Points3D::new(final_landmark_positions.clone())
1120 .with_radii([self.config.landmark_point_size])
1121 .with_colors([rerun::components::Color::from_rgb(
1122 final_color[0],
1123 final_color[1],
1124 final_color[2],
1125 )]),
1126 )
1127 .map_err(|e| {
1128 ObserverError::LoggingFailed {
1129 entity_path: "final_graph/landmarks".to_string(),
1130 reason: format!("{}", e),
1131 }
1132 .log_with_source(e)
1133 })?;
1134 }
1135
1136 if self.config.show_se2_poses && !final_se2_positions.is_empty() {
1138 let color = self.config.optimized_se2_color;
1139 let hs = self.config.se2_box_half_size;
1140 let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; final_se2_positions.len()];
1141 rec.log(
1142 "final_graph/se2_poses",
1143 &rerun::archetypes::Boxes2D::from_centers_and_half_sizes(
1144 final_se2_positions,
1145 half_sizes,
1146 )
1147 .with_colors([rerun::components::Color::from_rgb(
1148 color[0], color[1], color[2],
1149 )]),
1150 )
1151 .map_err(|e| {
1152 ObserverError::LoggingFailed {
1153 entity_path: "final_graph/se2_poses".to_string(),
1154 reason: format!("{}", e),
1155 }
1156 .log_with_source(e)
1157 })?;
1158 }
1159
1160 info!(
1161 "Logged final state after {} iterations: {} cameras, {} landmarks, {} SE2 poses",
1162 iterations,
1163 camera_count,
1164 final_landmark_positions.len(),
1165 se2_count
1166 );
1167
1168 self.log_displacement_statistics(values)?;
1170
1171 Ok(())
1172 }
1173
1174 fn log_displacement_statistics(
1179 &self,
1180 values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1181 ) -> ObserverResult<()> {
1182 let initial_cameras = self.initial_camera_positions.borrow();
1183 let initial_landmarks = self.initial_landmark_positions.borrow();
1184
1185 let mut camera_displacements: Vec<f32> = Vec::new();
1187 for (i, (_, var)) in values.iter().enumerate() {
1188 let name = format!("var_{i}");
1189 if let Some(v) = var.as_any().downcast_ref::<Variable<SE3>>() {
1190 let pose = if self.config.invert_camera_poses {
1192 v.value.inverse(None)
1193 } else {
1194 v.value.clone()
1195 };
1196
1197 if let Some(initial_pos) = initial_cameras.get(&name) {
1198 let final_pos = pose.translation();
1199 let dx = final_pos.x as f32 - initial_pos[0];
1200 let dy = final_pos.y as f32 - initial_pos[1];
1201 let dz = final_pos.z as f32 - initial_pos[2];
1202 let displacement = (dx * dx + dy * dy + dz * dz).sqrt();
1203 camera_displacements.push(displacement);
1204 }
1205 }
1206 }
1207
1208 let mut landmark_displacements: Vec<f32> = Vec::new();
1210 for (i, (_, var)) in values.iter().enumerate() {
1211 let name = format!("var_{i}");
1212 if let Some(v) = var.as_any().downcast_ref::<Variable<Rn>>() {
1213 let data = v.value.data();
1214 if data.len() == 3
1215 && let Some(initial_pos) = initial_landmarks.get(&name)
1216 {
1217 let dx = data[0] as f32 - initial_pos[0];
1218 let dy = data[1] as f32 - initial_pos[1];
1219 let dz = data[2] as f32 - initial_pos[2];
1220 let displacement = (dx * dx + dy * dy + dz * dz).sqrt();
1221 landmark_displacements.push(displacement);
1222 }
1223 }
1224 }
1225
1226 if !camera_displacements.is_empty() {
1228 let avg = camera_displacements.iter().sum::<f32>() / camera_displacements.len() as f32;
1229 let max = camera_displacements.iter().cloned().fold(0.0f32, f32::max);
1230 let min = camera_displacements
1231 .iter()
1232 .cloned()
1233 .fold(f32::MAX, f32::min);
1234 info!(
1235 "Camera displacement: avg={:.6}, min={:.6}, max={:.6} ({} cameras)",
1236 avg,
1237 min,
1238 max,
1239 camera_displacements.len()
1240 );
1241 }
1242
1243 if !landmark_displacements.is_empty() {
1245 let avg =
1246 landmark_displacements.iter().sum::<f32>() / landmark_displacements.len() as f32;
1247 let max = landmark_displacements
1248 .iter()
1249 .cloned()
1250 .fold(0.0f32, f32::max);
1251 let min = landmark_displacements
1252 .iter()
1253 .cloned()
1254 .fold(f32::MAX, f32::min);
1255 info!(
1256 "Landmark displacement: avg={:.6}, min={:.6}, max={:.6} ({} landmarks)",
1257 avg,
1258 min,
1259 max,
1260 landmark_displacements.len()
1261 );
1262 }
1263
1264 Ok(())
1265 }
1266
1267 fn log_scalars(&self, iteration: usize, metrics: &IterationMetrics) -> ObserverResult<()> {
1273 if !self.config.show_plots {
1274 return Ok(());
1275 }
1276
1277 let rec = self.rec.as_ref().ok_or_else(|| {
1278 ObserverError::InvalidState("Recording stream not initialized".to_string())
1279 })?;
1280 rec.set_time_sequence("iteration", iteration as i64);
1281
1282 if let Some(cost) = metrics.cost {
1284 rec.log("cost_plot/value", &rerun::archetypes::Scalars::new([cost]))
1285 .map_err(|e| {
1286 ObserverError::LoggingFailed {
1287 entity_path: "cost_plot/value".to_string(),
1288 reason: format!("{}", e),
1289 }
1290 .log_with_source(e)
1291 })?;
1292 }
1293
1294 if let Some(gradient_norm) = metrics.gradient_norm {
1295 rec.log(
1296 "gradient_plot/norm",
1297 &rerun::archetypes::Scalars::new([gradient_norm]),
1298 )
1299 .map_err(|e| {
1300 ObserverError::LoggingFailed {
1301 entity_path: "gradient_plot/norm".to_string(),
1302 reason: format!("{}", e),
1303 }
1304 .log_with_source(e)
1305 })?;
1306 }
1307
1308 if let Some(damping) = metrics.damping {
1309 rec.log(
1310 "damping_plot/lambda",
1311 &rerun::archetypes::Scalars::new([damping]),
1312 )
1313 .map_err(|e| {
1314 ObserverError::LoggingFailed {
1315 entity_path: "damping_plot/lambda".to_string(),
1316 reason: format!("{}", e),
1317 }
1318 .log_with_source(e)
1319 })?;
1320 }
1321
1322 if let Some(step_norm) = metrics.step_norm {
1323 rec.log(
1324 "step_plot/norm",
1325 &rerun::archetypes::Scalars::new([step_norm]),
1326 )
1327 .map_err(|e| {
1328 ObserverError::LoggingFailed {
1329 entity_path: "step_plot/norm".to_string(),
1330 reason: format!("{}", e),
1331 }
1332 .log_with_source(e)
1333 })?;
1334 }
1335
1336 if let Some(step_quality) = metrics.step_quality {
1337 rec.log(
1338 "quality_plot/rho",
1339 &rerun::archetypes::Scalars::new([step_quality]),
1340 )
1341 .map_err(|e| {
1342 ObserverError::LoggingFailed {
1343 entity_path: "quality_plot/rho".to_string(),
1344 reason: format!("{}", e),
1345 }
1346 .log_with_source(e)
1347 })?;
1348 }
1349
1350 Ok(())
1351 }
1352
1353 fn log_matrices(&self, iteration: usize, metrics: &IterationMetrics) -> ObserverResult<()> {
1355 if !self.config.show_matrices {
1356 return Ok(());
1357 }
1358
1359 let rec = self.rec.as_ref().ok_or_else(|| {
1360 ObserverError::InvalidState("Recording stream not initialized".to_string())
1361 })?;
1362 rec.set_time_sequence("iteration", iteration as i64);
1363
1364 if let Some(ref hessian) = metrics.hessian
1366 && let Ok(image_data) = self.sparse_hessian_to_image(hessian)
1367 {
1368 rec.log(
1369 "optimization/matrices/hessian",
1370 &rerun::archetypes::Tensor::new(image_data),
1371 )
1372 .map_err(|e| {
1373 ObserverError::LoggingFailed {
1374 entity_path: "optimization/matrices/hessian".to_string(),
1375 reason: format!("{}", e),
1376 }
1377 .log_with_source(e)
1378 })?;
1379 }
1380
1381 if let Some(ref gradient) = metrics.gradient {
1383 let grad_vec: Vec<f64> = (0..gradient.nrows()).map(|i| gradient[(i, 0)]).collect();
1384 if let Ok(image_data) = self.gradient_to_image(&grad_vec) {
1385 rec.log(
1386 "optimization/matrices/gradient",
1387 &rerun::archetypes::Tensor::new(image_data),
1388 )
1389 .map_err(|e| {
1390 ObserverError::LoggingFailed {
1391 entity_path: "optimization/matrices/gradient".to_string(),
1392 reason: format!("{}", e),
1393 }
1394 .log_with_source(e)
1395 })?;
1396 }
1397 }
1398
1399 Ok(())
1400 }
1401
1402 fn log_initial_state(
1409 &self,
1410 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1411 ) -> ObserverResult<()> {
1412 let rec = self.rec.as_ref().ok_or_else(|| {
1413 ObserverError::InvalidState("Recording stream not initialized".to_string())
1414 })?;
1415 rec.set_time_sequence("iteration", 0_i64);
1416
1417 let mut se2_positions: Vec<[f32; 2]> = Vec::new();
1418 let mut landmark_positions: Vec<[f32; 3]> = Vec::new();
1419
1420 for (i, (_, var)) in variables.iter().enumerate() {
1421 let var_name = format!("var_{i}");
1422 if self.config.show_cameras
1423 && let Some(v) = var.as_any().downcast_ref::<Variable<SE3>>()
1424 {
1425 let pose = if self.config.invert_camera_poses {
1426 v.value.inverse(None)
1427 } else {
1428 v.value.clone()
1429 };
1430
1431 let trans = pose.translation();
1432 let rot = pose.rotation_quaternion();
1433
1434 let position = rerun::external::glam::Vec3::new(
1435 trans.x as f32,
1436 trans.y as f32,
1437 trans.z as f32,
1438 );
1439
1440 let nq = rot.as_ref();
1441 let rotation = rerun::external::glam::Quat::from_xyzw(
1442 nq.i as f32,
1443 nq.j as f32,
1444 nq.k as f32,
1445 nq.w as f32,
1446 );
1447
1448 let transform = rerun::Transform3D::from_translation_rotation(position, rotation);
1449
1450 let entity_path = format!("initial_graph/cameras/{}", var_name);
1451 rec.log(entity_path.as_str(), &transform).map_err(|e| {
1452 ObserverError::LoggingFailed {
1453 entity_path: entity_path.clone(),
1454 reason: format!("{}", e),
1455 }
1456 .log_with_source(e)
1457 })?;
1458
1459 rec.log(
1460 entity_path.as_str(),
1461 &rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
1462 self.config.camera_fov,
1463 self.config.camera_aspect_ratio,
1464 )
1465 .with_image_plane_distance(self.config.camera_frustum_scale),
1466 )
1467 .map_err(|e| {
1468 ObserverError::LoggingFailed {
1469 entity_path: entity_path.clone(),
1470 reason: format!("{}", e),
1471 }
1472 .log_with_source(e)
1473 })?;
1474 }
1475 if self.config.show_se2_poses
1476 && let Some(v) = var.as_any().downcast_ref::<Variable<SE2>>()
1477 {
1478 se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
1479 }
1480 if self.config.show_landmarks
1481 && let Some(v) = var.as_any().downcast_ref::<Variable<Rn>>()
1482 {
1483 let data = v.value.data();
1484 if data.len() == 3 {
1485 landmark_positions.push([data[0] as f32, data[1] as f32, data[2] as f32]);
1486 }
1487 }
1488 }
1489
1490 if self.config.show_landmarks && !landmark_positions.is_empty() {
1491 let color = self.config.initial_landmark_color;
1492 rec.log(
1493 "initial_graph/landmarks",
1494 &rerun::archetypes::Points3D::new(landmark_positions)
1495 .with_radii([self.config.landmark_point_size])
1496 .with_colors([rerun::components::Color::from_rgb(
1497 color[0], color[1], color[2],
1498 )]),
1499 )
1500 .map_err(|e| {
1501 ObserverError::LoggingFailed {
1502 entity_path: "initial_graph/landmarks".to_string(),
1503 reason: format!("{}", e),
1504 }
1505 .log_with_source(e)
1506 })?;
1507 }
1508
1509 if self.config.show_se2_poses && !se2_positions.is_empty() {
1510 let color = self.config.initial_se2_color;
1511 let hs = self.config.se2_box_half_size;
1512 let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; se2_positions.len()];
1513 rec.log(
1514 "initial_graph/se2_poses",
1515 &rerun::archetypes::Boxes2D::from_centers_and_half_sizes(se2_positions, half_sizes)
1516 .with_colors([rerun::components::Color::from_rgb(
1517 color[0], color[1], color[2],
1518 )]),
1519 )
1520 .map_err(|e| {
1521 ObserverError::LoggingFailed {
1522 entity_path: "initial_graph/se2_poses".to_string(),
1523 reason: format!("{}", e),
1524 }
1525 .log_with_source(e)
1526 })?;
1527 }
1528
1529 self.initial_state_logged.set(true);
1530 Ok(())
1531 }
1532
1533 fn log_manifolds(
1534 &self,
1535 iteration: usize,
1536 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1537 ) -> ObserverResult<()> {
1538 let rec = self.rec.as_ref().ok_or_else(|| {
1539 ObserverError::InvalidState("Recording stream not initialized".to_string())
1540 })?;
1541 rec.set_time_sequence("iteration", iteration as i64);
1542
1543 let mut landmark_positions: Vec<[f32; 3]> = Vec::new();
1545 let mut se2_positions: Vec<[f32; 2]> = Vec::new();
1547
1548 for (i, (_, var)) in variables.iter().enumerate() {
1549 let var_name = format!("var_{i}");
1550 if self.config.show_cameras
1551 && let Some(v) = var.as_any().downcast_ref::<Variable<SE3>>()
1552 {
1553 let pose = if self.config.invert_camera_poses {
1555 v.value.inverse(None)
1556 } else {
1557 v.value.clone()
1558 };
1559
1560 let trans = pose.translation();
1561 let rot = pose.rotation_quaternion();
1562
1563 let position = rerun::external::glam::Vec3::new(
1564 trans.x as f32,
1565 trans.y as f32,
1566 trans.z as f32,
1567 );
1568
1569 let nq = rot.as_ref();
1570 let rotation = rerun::external::glam::Quat::from_xyzw(
1571 nq.i as f32,
1572 nq.j as f32,
1573 nq.k as f32,
1574 nq.w as f32,
1575 );
1576
1577 let transform = rerun::Transform3D::from_translation_rotation(position, rotation);
1578
1579 let entity_path = format!("optimized_graph/cameras/{}", var_name);
1580 rec.log(entity_path.as_str(), &transform).map_err(|e| {
1581 ObserverError::LoggingFailed {
1582 entity_path: entity_path.clone(),
1583 reason: format!("{}", e),
1584 }
1585 .log_with_source(e)
1586 })?;
1587
1588 rec.log(
1589 entity_path.as_str(),
1590 &rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
1591 self.config.camera_fov,
1592 self.config.camera_aspect_ratio,
1593 )
1594 .with_image_plane_distance(self.config.camera_frustum_scale),
1595 )
1596 .map_err(|e| {
1597 ObserverError::LoggingFailed {
1598 entity_path: entity_path.clone(),
1599 reason: format!("{}", e),
1600 }
1601 .log_with_source(e)
1602 })?;
1603 }
1604 if self.config.show_se2_poses
1605 && let Some(v) = var.as_any().downcast_ref::<Variable<SE2>>()
1606 {
1607 se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
1608 }
1609 if self.config.show_landmarks
1610 && let Some(v) = var.as_any().downcast_ref::<Variable<Rn>>()
1611 {
1612 let data = v.value.data();
1614 if data.len() == 3 {
1615 landmark_positions.push([data[0] as f32, data[1] as f32, data[2] as f32]);
1616 }
1617 }
1619 }
1620
1621 if self.config.show_landmarks && !landmark_positions.is_empty() {
1623 let color = self.config.optimized_landmark_color;
1624 rec.log(
1625 "optimized_graph/landmarks",
1626 &rerun::archetypes::Points3D::new(landmark_positions)
1627 .with_radii([self.config.landmark_point_size])
1628 .with_colors([rerun::components::Color::from_rgb(
1629 color[0], color[1], color[2],
1630 )]),
1631 )
1632 .map_err(|e| {
1633 ObserverError::LoggingFailed {
1634 entity_path: "optimized_graph/landmarks".to_string(),
1635 reason: format!("{}", e),
1636 }
1637 .log_with_source(e)
1638 })?;
1639 }
1640
1641 if self.config.show_se2_poses && !se2_positions.is_empty() {
1643 let color = self.config.optimized_se2_color;
1644 let hs = self.config.se2_box_half_size;
1645 let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; se2_positions.len()];
1646 rec.log(
1647 "optimized_graph/se2_poses",
1648 &rerun::archetypes::Boxes2D::from_centers_and_half_sizes(se2_positions, half_sizes)
1649 .with_colors([rerun::components::Color::from_rgb(
1650 color[0], color[1], color[2],
1651 )]),
1652 )
1653 .map_err(|e| {
1654 ObserverError::LoggingFailed {
1655 entity_path: "optimized_graph/se2_poses".to_string(),
1656 reason: format!("{}", e),
1657 }
1658 .log_with_source(e)
1659 })?;
1660 }
1661
1662 Ok(())
1663 }
1664
1665 fn sparse_hessian_to_image(
1667 &self,
1668 hessian: &sparse::SparseColMat<usize, f64>,
1669 ) -> ObserverResult<rerun::datatypes::TensorData> {
1670 let target_size = self.config.hessian_downsample_size;
1671 let target_rows = target_size;
1672 let target_cols = target_size;
1673
1674 let dense_matrix = Self::downsample_sparse_matrix(hessian, target_rows, target_cols);
1675
1676 let mut min_val = f64::INFINITY;
1677 let mut max_val = f64::NEG_INFINITY;
1678
1679 for &val in &dense_matrix {
1680 if val.is_finite() {
1681 min_val = min_val.min(val);
1682 max_val = max_val.max(val);
1683 }
1684 }
1685
1686 let max_abs = max_val.abs().max(min_val.abs());
1687
1688 let mut rgb_data = Vec::with_capacity(target_rows * target_cols * 3);
1689
1690 for &val in &dense_matrix {
1691 let rgb = Self::value_to_rgb_heatmap(val, max_abs);
1692 rgb_data.extend_from_slice(&rgb);
1693 }
1694
1695 let tensor = rerun::datatypes::TensorData::new(
1696 vec![target_rows as u64, target_cols as u64, 3],
1697 rerun::datatypes::TensorBuffer::U8(rgb_data.into()),
1698 );
1699
1700 Ok(tensor)
1701 }
1702
1703 fn gradient_to_image(&self, gradient: &[f64]) -> ObserverResult<rerun::datatypes::TensorData> {
1705 let n = gradient.len();
1706 let bar_height = 50;
1707 let target_width = self.config.gradient_bar_width;
1708
1709 let max_abs = gradient
1710 .iter()
1711 .map(|&x| x.abs())
1712 .fold(0.0f64, |a, b| a.max(b));
1713
1714 let mut rgb_data = Vec::with_capacity(bar_height * target_width * 3);
1715
1716 for _ in 0..bar_height {
1717 for i in 0..target_width {
1718 let start = (i * n) / target_width;
1719 let end = ((i + 1) * n) / target_width;
1720 let sum: f64 = gradient[start..end].iter().sum();
1721 let val = sum / (end - start).max(1) as f64;
1722
1723 let rgb = Self::value_to_rgb_heatmap(val, max_abs);
1724 rgb_data.extend_from_slice(&rgb);
1725 }
1726 }
1727
1728 let tensor = rerun::datatypes::TensorData::new(
1729 vec![bar_height as u64, target_width as u64, 3],
1730 rerun::datatypes::TensorBuffer::U8(rgb_data.into()),
1731 );
1732
1733 Ok(tensor)
1734 }
1735
1736 fn downsample_sparse_matrix(
1738 sparse: &sparse::SparseColMat<usize, f64>,
1739 target_rows: usize,
1740 target_cols: usize,
1741 ) -> Vec<f64> {
1742 let m = sparse.nrows();
1743 let n = sparse.ncols();
1744
1745 let mut downsampled = vec![0.0; target_rows * target_cols];
1746 let mut counts = vec![0usize; target_rows * target_cols];
1747
1748 let symbolic = sparse.symbolic();
1749
1750 for col in 0..n {
1751 let row_indices = symbolic.row_idx_of_col_raw(col);
1752 let col_values = sparse.val_of_col(col);
1753
1754 for (idx_in_col, &row) in row_indices.iter().enumerate() {
1755 let value = col_values[idx_in_col];
1756
1757 if value.abs() > 1e-12 {
1758 let target_row = (row * target_rows) / m;
1759 let target_col = (col * target_cols) / n;
1760 let idx = target_row * target_cols + target_col;
1761
1762 downsampled[idx] += value;
1763 counts[idx] += 1;
1764 }
1765 }
1766 }
1767
1768 for i in 0..downsampled.len() {
1769 if counts[i] > 0 {
1770 downsampled[i] /= counts[i] as f64;
1771 }
1772 }
1773
1774 downsampled
1775 }
1776
1777 fn value_to_rgb_heatmap(value: f64, max_abs: f64) -> [u8; 3] {
1779 if !value.is_finite() || max_abs == 0.0 {
1780 return [255, 255, 255];
1781 }
1782
1783 let normalized = (value.abs() / max_abs).clamp(0.0, 1.0);
1784
1785 if normalized < 1e-10 {
1786 [255, 255, 255]
1787 } else {
1788 let intensity = (normalized * 255.0) as u8;
1789 let remaining = 255 - intensity;
1790 [remaining, remaining, 255]
1791 }
1792 }
1793}
1794
1795impl OptObserver for RerunObserver {
1800 fn on_step(&self, values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, iteration: usize) {
1816 if !self.is_enabled() {
1817 return;
1818 }
1819
1820 if !self.initial_state_logged.get()
1822 && let Err(e) = self.log_initial_state(values)
1823 {
1824 let _ = e.log();
1825 }
1826
1827 let metrics = self.iteration_metrics.borrow();
1828
1829 if let Err(e) = self.log_scalars(iteration, &metrics) {
1831 let _ = e.log();
1832 }
1833
1834 let should_log_manifolds = match self.config.visualization_mode {
1838 VisualizationMode::Iterative => true,
1839 VisualizationMode::InitialAndFinal => false, };
1841
1842 if should_log_manifolds {
1843 if let Err(e) = self.log_matrices(iteration, &metrics) {
1844 let _ = e.log();
1845 }
1846 if let Err(e) = self.log_manifolds(iteration, values) {
1847 let _ = e.log();
1848 }
1849 }
1850
1851 drop(metrics);
1853 }
1858
1859 fn on_optimization_complete(
1869 &self,
1870 values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1871 iterations: usize,
1872 ) {
1873 if !self.is_enabled() {
1874 return;
1875 }
1876
1877 if let Err(e) = self.log_final_state(values, iterations) {
1879 let _ = e.log();
1880 }
1881 }
1882}
1883
1884impl Default for RerunObserver {
1885 fn default() -> Self {
1886 Self::new(false).unwrap_or_else(|_| Self {
1887 rec: None,
1888 enabled: false,
1889 iteration_metrics: RefCell::new(IterationMetrics::default()),
1890 config: VisualizationConfig::default(),
1891 initial_camera_positions: RefCell::new(HashMap::new()),
1892 initial_landmark_positions: RefCell::new(HashMap::new()),
1893 initial_state_logged: Cell::new(false),
1894 })
1895 }
1896}
1897
1898#[cfg(test)]
1899mod tests {
1900 use super::*;
1901
1902 type TestResult = Result<(), Box<dyn std::error::Error>>;
1903
1904 #[test]
1905 fn test_observer_creation() -> TestResult {
1906 let observer = RerunObserver::new(false)?;
1907 assert!(!observer.is_enabled());
1908 Ok(())
1909 }
1910
1911 #[test]
1912 fn test_rgb_heatmap_conversion() {
1913 let rgb = RerunObserver::value_to_rgb_heatmap(0.0, 1.0);
1914 assert_eq!(rgb, [255, 255, 255]);
1915
1916 let rgb = RerunObserver::value_to_rgb_heatmap(1.0, 1.0);
1917 assert_eq!(rgb, [0, 0, 255]);
1918
1919 let rgb = RerunObserver::value_to_rgb_heatmap(-1.0, 1.0);
1920 assert_eq!(rgb, [0, 0, 255]);
1921
1922 let rgb = RerunObserver::value_to_rgb_heatmap(0.5, 1.0);
1923 assert_eq!(rgb, [128, 128, 255]);
1924 }
1925
1926 #[test]
1927 fn test_set_metrics() -> TestResult {
1928 let observer = RerunObserver::new(false)?;
1929 observer.set_iteration_metrics(1.0, 0.5, Some(0.01), 0.1, Some(0.95));
1930
1931 let metrics = observer.iteration_metrics.borrow();
1932 assert_eq!(metrics.cost, Some(1.0));
1933 assert_eq!(metrics.gradient_norm, Some(0.5));
1934 assert_eq!(metrics.damping, Some(0.01));
1935 assert_eq!(metrics.step_norm, Some(0.1));
1936 assert_eq!(metrics.step_quality, Some(0.95));
1937 Ok(())
1938 }
1939
1940 #[test]
1941 fn test_observer_trait() -> TestResult {
1942 let observer = RerunObserver::new(false)?;
1943 let values: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
1944
1945 observer.on_step(&values, 0);
1947 observer.on_step(&values, 1);
1948 Ok(())
1949 }
1950}