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 if let Some(v) = var.as_any().downcast_ref::<Variable<SE3>>() {
1042 let pose = if self.config.invert_camera_poses {
1044 v.value.inverse(None)
1045 } else {
1046 v.value.clone()
1047 };
1048
1049 let trans = pose.translation();
1050 let rot = pose.rotation_quaternion();
1051
1052 let position = rerun::external::glam::Vec3::new(
1053 trans.x as f32,
1054 trans.y as f32,
1055 trans.z as f32,
1056 );
1057
1058 let nq = rot.as_ref();
1059 let rotation = rerun::external::glam::Quat::from_xyzw(
1060 nq.i as f32,
1061 nq.j as f32,
1062 nq.k as f32,
1063 nq.w as f32,
1064 );
1065
1066 let transform =
1067 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 }
1097 if self.config.show_se2_poses {
1098 if let Some(v) = var.as_any().downcast_ref::<Variable<SE2>>() {
1099 final_se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
1100 se2_count += 1;
1101 }
1102 }
1103 if self.config.show_landmarks {
1104 if let Some(v) = var.as_any().downcast_ref::<Variable<Rn>>() {
1105 let data = v.value.data();
1107 if data.len() == 3 {
1108 final_landmark_positions.push([
1109 data[0] as f32,
1110 data[1] as f32,
1111 data[2] as f32,
1112 ]);
1113 }
1114 }
1115 }
1116 }
1117
1118 if self.config.show_landmarks && !final_landmark_positions.is_empty() {
1120 let final_color: [u8; 3] = [50, 200, 100]; rec.log(
1123 "final_graph/landmarks",
1124 &rerun::archetypes::Points3D::new(final_landmark_positions.clone())
1125 .with_radii([self.config.landmark_point_size])
1126 .with_colors([rerun::components::Color::from_rgb(
1127 final_color[0],
1128 final_color[1],
1129 final_color[2],
1130 )]),
1131 )
1132 .map_err(|e| {
1133 ObserverError::LoggingFailed {
1134 entity_path: "final_graph/landmarks".to_string(),
1135 reason: format!("{}", e),
1136 }
1137 .log_with_source(e)
1138 })?;
1139 }
1140
1141 if self.config.show_se2_poses && !final_se2_positions.is_empty() {
1143 let color = self.config.optimized_se2_color;
1144 let hs = self.config.se2_box_half_size;
1145 let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; final_se2_positions.len()];
1146 rec.log(
1147 "final_graph/se2_poses",
1148 &rerun::archetypes::Boxes2D::from_centers_and_half_sizes(
1149 final_se2_positions,
1150 half_sizes,
1151 )
1152 .with_colors([rerun::components::Color::from_rgb(
1153 color[0], color[1], color[2],
1154 )]),
1155 )
1156 .map_err(|e| {
1157 ObserverError::LoggingFailed {
1158 entity_path: "final_graph/se2_poses".to_string(),
1159 reason: format!("{}", e),
1160 }
1161 .log_with_source(e)
1162 })?;
1163 }
1164
1165 info!(
1166 "Logged final state after {} iterations: {} cameras, {} landmarks, {} SE2 poses",
1167 iterations,
1168 camera_count,
1169 final_landmark_positions.len(),
1170 se2_count
1171 );
1172
1173 self.log_displacement_statistics(values)?;
1175
1176 Ok(())
1177 }
1178
1179 fn log_displacement_statistics(
1184 &self,
1185 values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1186 ) -> ObserverResult<()> {
1187 let initial_cameras = self.initial_camera_positions.borrow();
1188 let initial_landmarks = self.initial_landmark_positions.borrow();
1189
1190 let mut camera_displacements: Vec<f32> = Vec::new();
1192 for (i, (_, var)) in values.iter().enumerate() {
1193 let name = format!("var_{i}");
1194 if let Some(v) = var.as_any().downcast_ref::<Variable<SE3>>() {
1195 let pose = if self.config.invert_camera_poses {
1197 v.value.inverse(None)
1198 } else {
1199 v.value.clone()
1200 };
1201
1202 if let Some(initial_pos) = initial_cameras.get(&name) {
1203 let final_pos = pose.translation();
1204 let dx = final_pos.x as f32 - initial_pos[0];
1205 let dy = final_pos.y as f32 - initial_pos[1];
1206 let dz = final_pos.z as f32 - initial_pos[2];
1207 let displacement = (dx * dx + dy * dy + dz * dz).sqrt();
1208 camera_displacements.push(displacement);
1209 }
1210 }
1211 }
1212
1213 let mut landmark_displacements: Vec<f32> = Vec::new();
1215 for (i, (_, var)) in values.iter().enumerate() {
1216 let name = format!("var_{i}");
1217 if let Some(v) = var.as_any().downcast_ref::<Variable<Rn>>() {
1218 let data = v.value.data();
1219 if data.len() == 3
1220 && let Some(initial_pos) = initial_landmarks.get(&name)
1221 {
1222 let dx = data[0] as f32 - initial_pos[0];
1223 let dy = data[1] as f32 - initial_pos[1];
1224 let dz = data[2] as f32 - initial_pos[2];
1225 let displacement = (dx * dx + dy * dy + dz * dz).sqrt();
1226 landmark_displacements.push(displacement);
1227 }
1228 }
1229 }
1230
1231 if !camera_displacements.is_empty() {
1233 let avg = camera_displacements.iter().sum::<f32>() / camera_displacements.len() as f32;
1234 let max = camera_displacements.iter().cloned().fold(0.0f32, f32::max);
1235 let min = camera_displacements
1236 .iter()
1237 .cloned()
1238 .fold(f32::MAX, f32::min);
1239 info!(
1240 "Camera displacement: avg={:.6}, min={:.6}, max={:.6} ({} cameras)",
1241 avg,
1242 min,
1243 max,
1244 camera_displacements.len()
1245 );
1246 }
1247
1248 if !landmark_displacements.is_empty() {
1250 let avg =
1251 landmark_displacements.iter().sum::<f32>() / landmark_displacements.len() as f32;
1252 let max = landmark_displacements
1253 .iter()
1254 .cloned()
1255 .fold(0.0f32, f32::max);
1256 let min = landmark_displacements
1257 .iter()
1258 .cloned()
1259 .fold(f32::MAX, f32::min);
1260 info!(
1261 "Landmark displacement: avg={:.6}, min={:.6}, max={:.6} ({} landmarks)",
1262 avg,
1263 min,
1264 max,
1265 landmark_displacements.len()
1266 );
1267 }
1268
1269 Ok(())
1270 }
1271
1272 fn log_scalars(&self, iteration: usize, metrics: &IterationMetrics) -> ObserverResult<()> {
1278 if !self.config.show_plots {
1279 return Ok(());
1280 }
1281
1282 let rec = self.rec.as_ref().ok_or_else(|| {
1283 ObserverError::InvalidState("Recording stream not initialized".to_string())
1284 })?;
1285 rec.set_time_sequence("iteration", iteration as i64);
1286
1287 if let Some(cost) = metrics.cost {
1289 rec.log("cost_plot/value", &rerun::archetypes::Scalars::new([cost]))
1290 .map_err(|e| {
1291 ObserverError::LoggingFailed {
1292 entity_path: "cost_plot/value".to_string(),
1293 reason: format!("{}", e),
1294 }
1295 .log_with_source(e)
1296 })?;
1297 }
1298
1299 if let Some(gradient_norm) = metrics.gradient_norm {
1300 rec.log(
1301 "gradient_plot/norm",
1302 &rerun::archetypes::Scalars::new([gradient_norm]),
1303 )
1304 .map_err(|e| {
1305 ObserverError::LoggingFailed {
1306 entity_path: "gradient_plot/norm".to_string(),
1307 reason: format!("{}", e),
1308 }
1309 .log_with_source(e)
1310 })?;
1311 }
1312
1313 if let Some(damping) = metrics.damping {
1314 rec.log(
1315 "damping_plot/lambda",
1316 &rerun::archetypes::Scalars::new([damping]),
1317 )
1318 .map_err(|e| {
1319 ObserverError::LoggingFailed {
1320 entity_path: "damping_plot/lambda".to_string(),
1321 reason: format!("{}", e),
1322 }
1323 .log_with_source(e)
1324 })?;
1325 }
1326
1327 if let Some(step_norm) = metrics.step_norm {
1328 rec.log(
1329 "step_plot/norm",
1330 &rerun::archetypes::Scalars::new([step_norm]),
1331 )
1332 .map_err(|e| {
1333 ObserverError::LoggingFailed {
1334 entity_path: "step_plot/norm".to_string(),
1335 reason: format!("{}", e),
1336 }
1337 .log_with_source(e)
1338 })?;
1339 }
1340
1341 if let Some(step_quality) = metrics.step_quality {
1342 rec.log(
1343 "quality_plot/rho",
1344 &rerun::archetypes::Scalars::new([step_quality]),
1345 )
1346 .map_err(|e| {
1347 ObserverError::LoggingFailed {
1348 entity_path: "quality_plot/rho".to_string(),
1349 reason: format!("{}", e),
1350 }
1351 .log_with_source(e)
1352 })?;
1353 }
1354
1355 Ok(())
1356 }
1357
1358 fn log_matrices(&self, iteration: usize, metrics: &IterationMetrics) -> ObserverResult<()> {
1360 if !self.config.show_matrices {
1361 return Ok(());
1362 }
1363
1364 let rec = self.rec.as_ref().ok_or_else(|| {
1365 ObserverError::InvalidState("Recording stream not initialized".to_string())
1366 })?;
1367 rec.set_time_sequence("iteration", iteration as i64);
1368
1369 if let Some(ref hessian) = metrics.hessian
1371 && let Ok(image_data) = self.sparse_hessian_to_image(hessian)
1372 {
1373 rec.log(
1374 "optimization/matrices/hessian",
1375 &rerun::archetypes::Tensor::new(image_data),
1376 )
1377 .map_err(|e| {
1378 ObserverError::LoggingFailed {
1379 entity_path: "optimization/matrices/hessian".to_string(),
1380 reason: format!("{}", e),
1381 }
1382 .log_with_source(e)
1383 })?;
1384 }
1385
1386 if let Some(ref gradient) = metrics.gradient {
1388 let grad_vec: Vec<f64> = (0..gradient.nrows()).map(|i| gradient[(i, 0)]).collect();
1389 if let Ok(image_data) = self.gradient_to_image(&grad_vec) {
1390 rec.log(
1391 "optimization/matrices/gradient",
1392 &rerun::archetypes::Tensor::new(image_data),
1393 )
1394 .map_err(|e| {
1395 ObserverError::LoggingFailed {
1396 entity_path: "optimization/matrices/gradient".to_string(),
1397 reason: format!("{}", e),
1398 }
1399 .log_with_source(e)
1400 })?;
1401 }
1402 }
1403
1404 Ok(())
1405 }
1406
1407 fn log_initial_state(
1414 &self,
1415 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1416 ) -> ObserverResult<()> {
1417 let rec = self.rec.as_ref().ok_or_else(|| {
1418 ObserverError::InvalidState("Recording stream not initialized".to_string())
1419 })?;
1420 rec.set_time_sequence("iteration", 0_i64);
1421
1422 let mut se2_positions: Vec<[f32; 2]> = Vec::new();
1423 let mut landmark_positions: Vec<[f32; 3]> = Vec::new();
1424
1425 for (i, (_, var)) in variables.iter().enumerate() {
1426 let var_name = format!("var_{i}");
1427 if self.config.show_cameras {
1428 if let Some(v) = var.as_any().downcast_ref::<Variable<SE3>>() {
1429 let pose = if self.config.invert_camera_poses {
1430 v.value.inverse(None)
1431 } else {
1432 v.value.clone()
1433 };
1434
1435 let trans = pose.translation();
1436 let rot = pose.rotation_quaternion();
1437
1438 let position = rerun::external::glam::Vec3::new(
1439 trans.x as f32,
1440 trans.y as f32,
1441 trans.z as f32,
1442 );
1443
1444 let nq = rot.as_ref();
1445 let rotation = rerun::external::glam::Quat::from_xyzw(
1446 nq.i as f32,
1447 nq.j as f32,
1448 nq.k as f32,
1449 nq.w as f32,
1450 );
1451
1452 let transform =
1453 rerun::Transform3D::from_translation_rotation(position, rotation);
1454
1455 let entity_path = format!("initial_graph/cameras/{}", var_name);
1456 rec.log(entity_path.as_str(), &transform).map_err(|e| {
1457 ObserverError::LoggingFailed {
1458 entity_path: entity_path.clone(),
1459 reason: format!("{}", e),
1460 }
1461 .log_with_source(e)
1462 })?;
1463
1464 rec.log(
1465 entity_path.as_str(),
1466 &rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
1467 self.config.camera_fov,
1468 self.config.camera_aspect_ratio,
1469 )
1470 .with_image_plane_distance(self.config.camera_frustum_scale),
1471 )
1472 .map_err(|e| {
1473 ObserverError::LoggingFailed {
1474 entity_path: entity_path.clone(),
1475 reason: format!("{}", e),
1476 }
1477 .log_with_source(e)
1478 })?;
1479 }
1480 }
1481 if self.config.show_se2_poses {
1482 if let Some(v) = var.as_any().downcast_ref::<Variable<SE2>>() {
1483 se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
1484 }
1485 }
1486 if self.config.show_landmarks {
1487 if let Some(v) = var.as_any().downcast_ref::<Variable<Rn>>() {
1488 let data = v.value.data();
1489 if data.len() == 3 {
1490 landmark_positions.push([data[0] as f32, data[1] as f32, data[2] as f32]);
1491 }
1492 }
1493 }
1494 }
1495
1496 if self.config.show_landmarks && !landmark_positions.is_empty() {
1497 let color = self.config.initial_landmark_color;
1498 rec.log(
1499 "initial_graph/landmarks",
1500 &rerun::archetypes::Points3D::new(landmark_positions)
1501 .with_radii([self.config.landmark_point_size])
1502 .with_colors([rerun::components::Color::from_rgb(
1503 color[0], color[1], color[2],
1504 )]),
1505 )
1506 .map_err(|e| {
1507 ObserverError::LoggingFailed {
1508 entity_path: "initial_graph/landmarks".to_string(),
1509 reason: format!("{}", e),
1510 }
1511 .log_with_source(e)
1512 })?;
1513 }
1514
1515 if self.config.show_se2_poses && !se2_positions.is_empty() {
1516 let color = self.config.initial_se2_color;
1517 let hs = self.config.se2_box_half_size;
1518 let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; se2_positions.len()];
1519 rec.log(
1520 "initial_graph/se2_poses",
1521 &rerun::archetypes::Boxes2D::from_centers_and_half_sizes(se2_positions, half_sizes)
1522 .with_colors([rerun::components::Color::from_rgb(
1523 color[0], color[1], color[2],
1524 )]),
1525 )
1526 .map_err(|e| {
1527 ObserverError::LoggingFailed {
1528 entity_path: "initial_graph/se2_poses".to_string(),
1529 reason: format!("{}", e),
1530 }
1531 .log_with_source(e)
1532 })?;
1533 }
1534
1535 self.initial_state_logged.set(true);
1536 Ok(())
1537 }
1538
1539 fn log_manifolds(
1540 &self,
1541 iteration: usize,
1542 variables: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1543 ) -> ObserverResult<()> {
1544 let rec = self.rec.as_ref().ok_or_else(|| {
1545 ObserverError::InvalidState("Recording stream not initialized".to_string())
1546 })?;
1547 rec.set_time_sequence("iteration", iteration as i64);
1548
1549 let mut landmark_positions: Vec<[f32; 3]> = Vec::new();
1551 let mut se2_positions: Vec<[f32; 2]> = Vec::new();
1553
1554 for (i, (_, var)) in variables.iter().enumerate() {
1555 let var_name = format!("var_{i}");
1556 if self.config.show_cameras {
1557 if let Some(v) = var.as_any().downcast_ref::<Variable<SE3>>() {
1558 let pose = if self.config.invert_camera_poses {
1560 v.value.inverse(None)
1561 } else {
1562 v.value.clone()
1563 };
1564
1565 let trans = pose.translation();
1566 let rot = pose.rotation_quaternion();
1567
1568 let position = rerun::external::glam::Vec3::new(
1569 trans.x as f32,
1570 trans.y as f32,
1571 trans.z as f32,
1572 );
1573
1574 let nq = rot.as_ref();
1575 let rotation = rerun::external::glam::Quat::from_xyzw(
1576 nq.i as f32,
1577 nq.j as f32,
1578 nq.k as f32,
1579 nq.w as f32,
1580 );
1581
1582 let transform =
1583 rerun::Transform3D::from_translation_rotation(position, rotation);
1584
1585 let entity_path = format!("optimized_graph/cameras/{}", var_name);
1586 rec.log(entity_path.as_str(), &transform).map_err(|e| {
1587 ObserverError::LoggingFailed {
1588 entity_path: entity_path.clone(),
1589 reason: format!("{}", e),
1590 }
1591 .log_with_source(e)
1592 })?;
1593
1594 rec.log(
1595 entity_path.as_str(),
1596 &rerun::archetypes::Pinhole::from_fov_and_aspect_ratio(
1597 self.config.camera_fov,
1598 self.config.camera_aspect_ratio,
1599 )
1600 .with_image_plane_distance(self.config.camera_frustum_scale),
1601 )
1602 .map_err(|e| {
1603 ObserverError::LoggingFailed {
1604 entity_path: entity_path.clone(),
1605 reason: format!("{}", e),
1606 }
1607 .log_with_source(e)
1608 })?;
1609 }
1610 }
1611 if self.config.show_se2_poses {
1612 if let Some(v) = var.as_any().downcast_ref::<Variable<SE2>>() {
1613 se2_positions.push([v.value.x() as f32, v.value.y() as f32]);
1614 }
1615 }
1616 if self.config.show_landmarks {
1617 if let Some(v) = var.as_any().downcast_ref::<Variable<Rn>>() {
1618 let data = v.value.data();
1620 if data.len() == 3 {
1621 landmark_positions.push([data[0] as f32, data[1] as f32, data[2] as f32]);
1622 }
1623 }
1625 }
1626 }
1627
1628 if self.config.show_landmarks && !landmark_positions.is_empty() {
1630 let color = self.config.optimized_landmark_color;
1631 rec.log(
1632 "optimized_graph/landmarks",
1633 &rerun::archetypes::Points3D::new(landmark_positions)
1634 .with_radii([self.config.landmark_point_size])
1635 .with_colors([rerun::components::Color::from_rgb(
1636 color[0], color[1], color[2],
1637 )]),
1638 )
1639 .map_err(|e| {
1640 ObserverError::LoggingFailed {
1641 entity_path: "optimized_graph/landmarks".to_string(),
1642 reason: format!("{}", e),
1643 }
1644 .log_with_source(e)
1645 })?;
1646 }
1647
1648 if self.config.show_se2_poses && !se2_positions.is_empty() {
1650 let color = self.config.optimized_se2_color;
1651 let hs = self.config.se2_box_half_size;
1652 let half_sizes: Vec<[f32; 2]> = vec![[hs, hs]; se2_positions.len()];
1653 rec.log(
1654 "optimized_graph/se2_poses",
1655 &rerun::archetypes::Boxes2D::from_centers_and_half_sizes(se2_positions, half_sizes)
1656 .with_colors([rerun::components::Color::from_rgb(
1657 color[0], color[1], color[2],
1658 )]),
1659 )
1660 .map_err(|e| {
1661 ObserverError::LoggingFailed {
1662 entity_path: "optimized_graph/se2_poses".to_string(),
1663 reason: format!("{}", e),
1664 }
1665 .log_with_source(e)
1666 })?;
1667 }
1668
1669 Ok(())
1670 }
1671
1672 fn sparse_hessian_to_image(
1674 &self,
1675 hessian: &sparse::SparseColMat<usize, f64>,
1676 ) -> ObserverResult<rerun::datatypes::TensorData> {
1677 let target_size = self.config.hessian_downsample_size;
1678 let target_rows = target_size;
1679 let target_cols = target_size;
1680
1681 let dense_matrix = Self::downsample_sparse_matrix(hessian, target_rows, target_cols);
1682
1683 let mut min_val = f64::INFINITY;
1684 let mut max_val = f64::NEG_INFINITY;
1685
1686 for &val in &dense_matrix {
1687 if val.is_finite() {
1688 min_val = min_val.min(val);
1689 max_val = max_val.max(val);
1690 }
1691 }
1692
1693 let max_abs = max_val.abs().max(min_val.abs());
1694
1695 let mut rgb_data = Vec::with_capacity(target_rows * target_cols * 3);
1696
1697 for &val in &dense_matrix {
1698 let rgb = Self::value_to_rgb_heatmap(val, max_abs);
1699 rgb_data.extend_from_slice(&rgb);
1700 }
1701
1702 let tensor = rerun::datatypes::TensorData::new(
1703 vec![target_rows as u64, target_cols as u64, 3],
1704 rerun::datatypes::TensorBuffer::U8(rgb_data.into()),
1705 );
1706
1707 Ok(tensor)
1708 }
1709
1710 fn gradient_to_image(&self, gradient: &[f64]) -> ObserverResult<rerun::datatypes::TensorData> {
1712 let n = gradient.len();
1713 let bar_height = 50;
1714 let target_width = self.config.gradient_bar_width;
1715
1716 let max_abs = gradient
1717 .iter()
1718 .map(|&x| x.abs())
1719 .fold(0.0f64, |a, b| a.max(b));
1720
1721 let mut rgb_data = Vec::with_capacity(bar_height * target_width * 3);
1722
1723 for _ in 0..bar_height {
1724 for i in 0..target_width {
1725 let start = (i * n) / target_width;
1726 let end = ((i + 1) * n) / target_width;
1727 let sum: f64 = gradient[start..end].iter().sum();
1728 let val = sum / (end - start).max(1) as f64;
1729
1730 let rgb = Self::value_to_rgb_heatmap(val, max_abs);
1731 rgb_data.extend_from_slice(&rgb);
1732 }
1733 }
1734
1735 let tensor = rerun::datatypes::TensorData::new(
1736 vec![bar_height as u64, target_width as u64, 3],
1737 rerun::datatypes::TensorBuffer::U8(rgb_data.into()),
1738 );
1739
1740 Ok(tensor)
1741 }
1742
1743 fn downsample_sparse_matrix(
1745 sparse: &sparse::SparseColMat<usize, f64>,
1746 target_rows: usize,
1747 target_cols: usize,
1748 ) -> Vec<f64> {
1749 let m = sparse.nrows();
1750 let n = sparse.ncols();
1751
1752 let mut downsampled = vec![0.0; target_rows * target_cols];
1753 let mut counts = vec![0usize; target_rows * target_cols];
1754
1755 let symbolic = sparse.symbolic();
1756
1757 for col in 0..n {
1758 let row_indices = symbolic.row_idx_of_col_raw(col);
1759 let col_values = sparse.val_of_col(col);
1760
1761 for (idx_in_col, &row) in row_indices.iter().enumerate() {
1762 let value = col_values[idx_in_col];
1763
1764 if value.abs() > 1e-12 {
1765 let target_row = (row * target_rows) / m;
1766 let target_col = (col * target_cols) / n;
1767 let idx = target_row * target_cols + target_col;
1768
1769 downsampled[idx] += value;
1770 counts[idx] += 1;
1771 }
1772 }
1773 }
1774
1775 for i in 0..downsampled.len() {
1776 if counts[i] > 0 {
1777 downsampled[i] /= counts[i] as f64;
1778 }
1779 }
1780
1781 downsampled
1782 }
1783
1784 fn value_to_rgb_heatmap(value: f64, max_abs: f64) -> [u8; 3] {
1786 if !value.is_finite() || max_abs == 0.0 {
1787 return [255, 255, 255];
1788 }
1789
1790 let normalized = (value.abs() / max_abs).clamp(0.0, 1.0);
1791
1792 if normalized < 1e-10 {
1793 [255, 255, 255]
1794 } else {
1795 let intensity = (normalized * 255.0) as u8;
1796 let remaining = 255 - intensity;
1797 [remaining, remaining, 255]
1798 }
1799 }
1800}
1801
1802impl OptObserver for RerunObserver {
1807 fn on_step(&self, values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, iteration: usize) {
1823 if !self.is_enabled() {
1824 return;
1825 }
1826
1827 if !self.initial_state_logged.get() {
1829 if let Err(e) = self.log_initial_state(values) {
1830 let _ = e.log();
1831 }
1832 }
1833
1834 let metrics = self.iteration_metrics.borrow();
1835
1836 if let Err(e) = self.log_scalars(iteration, &metrics) {
1838 let _ = e.log();
1839 }
1840
1841 let should_log_manifolds = match self.config.visualization_mode {
1845 VisualizationMode::Iterative => true,
1846 VisualizationMode::InitialAndFinal => false, };
1848
1849 if should_log_manifolds {
1850 if let Err(e) = self.log_matrices(iteration, &metrics) {
1851 let _ = e.log();
1852 }
1853 if let Err(e) = self.log_manifolds(iteration, values) {
1854 let _ = e.log();
1855 }
1856 }
1857
1858 drop(metrics);
1860 }
1862
1863 fn on_optimization_complete(
1873 &self,
1874 values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
1875 iterations: usize,
1876 ) {
1877 if !self.is_enabled() {
1878 return;
1879 }
1880
1881 if let Err(e) = self.log_final_state(values, iterations) {
1883 let _ = e.log();
1884 }
1885 }
1886}
1887
1888impl Default for RerunObserver {
1889 fn default() -> Self {
1890 Self::new(false).unwrap_or_else(|_| Self {
1891 rec: None,
1892 enabled: false,
1893 iteration_metrics: RefCell::new(IterationMetrics::default()),
1894 config: VisualizationConfig::default(),
1895 initial_camera_positions: RefCell::new(HashMap::new()),
1896 initial_landmark_positions: RefCell::new(HashMap::new()),
1897 initial_state_logged: Cell::new(false),
1898 })
1899 }
1900}
1901
1902#[cfg(test)]
1903mod tests {
1904 use super::*;
1905
1906 type TestResult = Result<(), Box<dyn std::error::Error>>;
1907
1908 #[test]
1909 fn test_observer_creation() -> TestResult {
1910 let observer = RerunObserver::new(false)?;
1911 assert!(!observer.is_enabled());
1912 Ok(())
1913 }
1914
1915 #[test]
1916 fn test_rgb_heatmap_conversion() {
1917 let rgb = RerunObserver::value_to_rgb_heatmap(0.0, 1.0);
1918 assert_eq!(rgb, [255, 255, 255]);
1919
1920 let rgb = RerunObserver::value_to_rgb_heatmap(1.0, 1.0);
1921 assert_eq!(rgb, [0, 0, 255]);
1922
1923 let rgb = RerunObserver::value_to_rgb_heatmap(-1.0, 1.0);
1924 assert_eq!(rgb, [0, 0, 255]);
1925
1926 let rgb = RerunObserver::value_to_rgb_heatmap(0.5, 1.0);
1927 assert_eq!(rgb, [128, 128, 255]);
1928 }
1929
1930 #[test]
1931 fn test_set_metrics() -> TestResult {
1932 let observer = RerunObserver::new(false)?;
1933 observer.set_iteration_metrics(1.0, 0.5, Some(0.01), 0.1, Some(0.95));
1934
1935 let metrics = observer.iteration_metrics.borrow();
1936 assert_eq!(metrics.cost, Some(1.0));
1937 assert_eq!(metrics.gradient_norm, Some(0.5));
1938 assert_eq!(metrics.damping, Some(0.01));
1939 assert_eq!(metrics.step_norm, Some(0.1));
1940 assert_eq!(metrics.step_quality, Some(0.95));
1941 Ok(())
1942 }
1943
1944 #[test]
1945 fn test_observer_trait() -> TestResult {
1946 let observer = RerunObserver::new(false)?;
1947 let values: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
1948
1949 observer.on_step(&values, 0);
1951 observer.on_step(&values, 1);
1952 Ok(())
1953 }
1954}