apex_solver/observers/mod.rs
1//! Observer pattern for optimization monitoring.
2//!
3//! This module provides a clean observer pattern for monitoring optimization progress.
4//! Observers can be registered with any optimizer and will be notified at each iteration,
5//! enabling real-time visualization, logging, metrics collection, and custom analysis.
6//!
7//! # Design Philosophy
8//!
9//! The observer pattern provides complete separation between optimization algorithms
10//! and monitoring/visualization logic:
11//!
12//! - **Decoupling**: Optimization logic is independent of how progress is monitored
13//! - **Extensibility**: Easy to add new observers (Rerun, CSV, metrics, dashboards)
14//! - **Composability**: Multiple observers can run simultaneously
15//! - **Zero overhead**: When no observers are registered, notification is a no-op
16//!
17//! # Architecture
18//!
19//! ```text
20//! ┌─────────────────┐
21//! │ Optimizer │
22//! │ (LM/GN/DogLeg) │
23//! └────────┬────────┘
24//! │ observers.notify(values, iteration)
25//! ├──────────────┬──────────────┬──────────────┐
26//! ▼ ▼ ▼ ▼
27//! ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
28//! │ Rerun │ │ CSV │ │ Metrics │ │ Custom │
29//! │ Observer │ │ Observer │ │ Observer │ │ Observer │
30//! └──────────┘ └──────────┘ └──────────┘ └──────────┘
31//! ```
32//!
33//! # Examples
34//!
35//! ## Single Observer
36//!
37//! ```no_run
38//! use apex_solver::{LevenbergMarquardt, LevenbergMarquardtConfig};
39//! use apex_solver::observers::OptObserver;
40//! # use apex_solver::core::problem::Problem;
41//! # use apex_solver::JacobianMode;
42//!
43//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
44//! # let mut problem = Problem::new(JacobianMode::Sparse);
45//!
46//! let config = LevenbergMarquardtConfig::new().with_max_iterations(100);
47//! let mut solver = LevenbergMarquardt::with_config(config);
48//!
49//! #[cfg(feature = "visualization")]
50//! {
51//! use apex_solver::observers::RerunObserver;
52//! let rerun_observer = RerunObserver::new(true)?;
53//! solver.add_observer(rerun_observer);
54//! }
55//!
56//! let result = solver.optimize(&mut problem)?;
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! ## Multiple Observers
62//!
63//! ```no_run
64//! # use apex_solver::{LevenbergMarquardt, LevenbergMarquardtConfig};
65//! # use apex_solver::core::problem::Problem;
66//! # use apex_solver::core::variable::ManifoldVariable;
67//! # use apex_solver::core::VarKey;
68//! # use apex_solver::observers::OptObserver;
69//! # use apex_solver::JacobianMode;
70//! # use slotmap::SlotMap;
71//!
72//! // Custom observer that logs to CSV
73//! struct CsvObserver {
74//! file: std::fs::File,
75//! }
76//!
77//! impl OptObserver for CsvObserver {
78//! fn on_step(&self, _values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, _iteration: usize) {
79//! // Write iteration data to CSV
80//! // ... implementation ...
81//! }
82//! }
83//!
84//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
85//! # let mut problem = Problem::new(JacobianMode::Sparse);
86//! let mut solver = LevenbergMarquardt::new();
87//!
88//! // Add Rerun visualization
89//! #[cfg(feature = "visualization")]
90//! {
91//! use apex_solver::observers::RerunObserver;
92//! solver.add_observer(RerunObserver::new(true)?);
93//! }
94//!
95//! // Add CSV logging
96//! // solver.add_observer(CsvObserver { file: ... });
97//!
98//! let result = solver.optimize(&mut problem)?;
99//! # Ok(())
100//! # }
101//! ```
102//!
103//! ## Custom Observer
104//!
105//! ```no_run
106//! use apex_solver::observers::OptObserver;
107//! use apex_solver::core::variable::ManifoldVariable;
108//! use apex_solver::core::VarKey;
109//! use slotmap::SlotMap;
110//!
111//! struct MetricsObserver {
112//! max_variables_seen: std::cell::RefCell<usize>,
113//! }
114//!
115//! impl OptObserver for MetricsObserver {
116//! fn on_step(&self, values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, _iteration: usize) {
117//! let count = values.len();
118//! let mut max = self.max_variables_seen.borrow_mut();
119//! *max = (*max).max(count);
120//! }
121//! }
122//! ```
123
124// Visualization-specific submodules (feature-gated)
125#[cfg(feature = "visualization")]
126pub mod conversions;
127#[cfg(feature = "visualization")]
128pub mod visualization;
129
130// Re-export RerunObserver when visualization is enabled
131#[cfg(feature = "visualization")]
132pub use visualization::{RerunObserver, VisualizationConfig, VisualizationMode};
133
134// Re-export conversion traits for ergonomic use
135#[cfg(feature = "visualization")]
136pub use conversions::{CollectRerun2D, CollectRerun3D, RerunConvert2D, RerunConvert3D};
137
138use crate::core::VarKey;
139use crate::core::variable::ManifoldVariable;
140use faer::Mat;
141use faer::sparse;
142use slotmap::SlotMap;
143use thiserror::Error;
144
145/// Observer-specific error types for apex-solver
146#[derive(Debug, Clone, Error)]
147pub enum ObserverError {
148 /// Failed to initialize Rerun recording stream
149 #[error("Failed to initialize Rerun recording stream: {0}")]
150 RerunInitialization(String),
151
152 /// Failed to spawn Rerun viewer process
153 #[error("Failed to spawn Rerun viewer: {0}")]
154 ViewerSpawnFailed(String),
155
156 /// Failed to save recording to file
157 #[error("Failed to save recording to file '{path}': {reason}")]
158 RecordingSaveFailed { path: String, reason: String },
159
160 /// Failed to log data to Rerun
161 #[error("Failed to log data to Rerun at '{entity_path}': {reason}")]
162 LoggingFailed { entity_path: String, reason: String },
163
164 /// Failed to convert matrix to visualization format
165 #[error("Failed to convert matrix to image: {0}")]
166 MatrixVisualizationFailed(String),
167
168 /// Failed to convert tensor data
169 #[error("Failed to create tensor data: {0}")]
170 TensorConversionFailed(String),
171
172 /// Recording stream is in invalid state
173 #[error("Recording stream is in invalid state: {0}")]
174 InvalidState(String),
175
176 /// Mutex was poisoned (thread panicked while holding lock)
177 #[error("Mutex poisoned in {context}: {reason}")]
178 MutexPoisoned { context: String, reason: String },
179}
180
181/// Result type for observer operations
182pub type ObserverResult<T> = Result<T, ObserverError>;
183
184/// Observer trait for monitoring optimization progress.
185///
186/// Implement this trait to create custom observers that are notified at each
187/// optimization iteration. Observers receive the current variable values and
188/// iteration number, enabling real-time monitoring, visualization, logging,
189/// or custom analysis.
190///
191/// # Design Notes
192///
193/// - Observers should be lightweight and non-blocking
194/// - Errors in observers should not crash optimization (handle internally)
195/// - For expensive operations (file I/O, network), consider buffering
196/// - Observers receive immutable references (cannot modify optimization state)
197///
198/// # Thread Safety
199///
200/// Observers must be `Send` to support parallel optimization in the future.
201/// Use interior mutability (`RefCell`, `Mutex`) if you need to mutate state.
202pub trait OptObserver: Send {
203 /// Called after each optimization iteration.
204 ///
205 /// # Arguments
206 ///
207 /// * `values` - Current variable values (manifold states)
208 /// * `iteration` - Current iteration number (0 = initial values, 1+ = after steps)
209 ///
210 /// # Implementation Guidelines
211 ///
212 /// - Keep this method fast to avoid slowing optimization
213 /// - Handle errors internally (log warnings, don't panic)
214 /// - Don't mutate `values` (you receive `&HashMap`)
215 /// - Consider buffering expensive operations
216 ///
217 /// # Examples
218 ///
219 /// ```no_run
220 /// use apex_solver::observers::OptObserver;
221 /// use apex_solver::core::variable::ManifoldVariable;
222 /// use apex_solver::core::VarKey;
223 /// use slotmap::SlotMap;
224 ///
225 /// struct SimpleLogger;
226 ///
227 /// impl OptObserver for SimpleLogger {
228 /// fn on_step(&self, _values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, _iteration: usize) {
229 /// // Track optimization progress
230 /// }
231 /// }
232 /// ```
233 fn on_step(&self, values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, iteration: usize);
234
235 /// Set iteration metrics for visualization and monitoring.
236 ///
237 /// This method is called before `on_step` to provide optimization metrics
238 /// such as cost, gradient norm, damping parameter, etc. Observers can use
239 /// this data for visualization, logging, or analysis.
240 ///
241 /// # Arguments
242 ///
243 /// * `cost` - Current cost function value
244 /// * `gradient_norm` - L2 norm of the gradient vector
245 /// * `damping` - Damping parameter (for Levenberg-Marquardt, may be None for other solvers)
246 /// * `step_norm` - L2 norm of the parameter update step
247 /// * `step_quality` - Step quality metric (e.g., rho for trust region methods)
248 ///
249 /// # Default Implementation
250 ///
251 /// The default implementation does nothing, allowing simple observers to ignore metrics.
252 fn set_iteration_metrics(
253 &self,
254 _cost: f64,
255 _gradient_norm: f64,
256 _damping: Option<f64>,
257 _step_norm: f64,
258 _step_quality: Option<f64>,
259 ) {
260 // Default implementation does nothing
261 }
262
263 /// Set matrix data for advanced visualization.
264 ///
265 /// This method provides access to the Hessian matrix and gradient vector
266 /// for observers that want to visualize matrix structure or perform
267 /// advanced analysis.
268 ///
269 /// # Arguments
270 ///
271 /// * `hessian` - Sparse Hessian matrix (J^T * J)
272 /// * `gradient` - Gradient vector (J^T * r)
273 ///
274 /// # Default Implementation
275 ///
276 /// The default implementation does nothing, allowing simple observers to ignore matrices.
277 fn set_matrix_data(
278 &self,
279 _hessian: Option<sparse::SparseColMat<usize, f64>>,
280 _gradient: Option<Mat<f64>>,
281 ) {
282 // Default implementation does nothing
283 }
284
285 /// Called when optimization completes.
286 ///
287 /// This method is called once at the end of optimization, after all iterations
288 /// are complete. Use this for final visualization, cleanup, or summary logging.
289 ///
290 /// # Arguments
291 ///
292 /// * `values` - Final optimized variable values
293 /// * `iterations` - Total number of iterations performed
294 ///
295 /// # Default Implementation
296 ///
297 /// The default implementation does nothing, allowing simple observers to ignore completion.
298 ///
299 /// # Examples
300 ///
301 /// ```no_run
302 /// use apex_solver::observers::OptObserver;
303 /// use apex_solver::core::variable::ManifoldVariable;
304 /// use apex_solver::core::VarKey;
305 /// use slotmap::SlotMap;
306 ///
307 /// struct FinalStateLogger;
308 ///
309 /// impl OptObserver for FinalStateLogger {
310 /// fn on_step(&self, _values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, _iteration: usize) {}
311 ///
312 /// fn on_optimization_complete(&self, values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, iterations: usize) {
313 /// println!("Optimization completed after {} iterations with {} variables",
314 /// iterations, values.len());
315 /// }
316 /// }
317 /// ```
318 fn on_optimization_complete(
319 &self,
320 _values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
321 _iterations: usize,
322 ) {
323 // Default implementation does nothing
324 }
325}
326
327/// Collection of observers for optimization monitoring.
328///
329/// This struct manages a vector of observers and provides a convenient
330/// `notify()` method to call all observers at once. Optimizers use this
331/// internally to manage their observers.
332///
333/// # Usage
334///
335/// Typically you don't create this directly - use the `add_observer()` method
336/// on optimizers. However, you can use it for custom optimization algorithms:
337///
338/// ```no_run
339/// use apex_solver::observers::{OptObserver, OptObserverVec};
340/// use apex_solver::core::variable::ManifoldVariable;
341/// use apex_solver::core::VarKey;
342/// use slotmap::SlotMap;
343///
344/// struct MyOptimizer {
345/// observers: OptObserverVec,
346/// // ... other fields ...
347/// }
348///
349/// impl MyOptimizer {
350/// fn step(&mut self, values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, iteration: usize) {
351/// // ... optimization logic ...
352///
353/// // Notify all observers
354/// self.observers.notify(values, iteration);
355/// }
356/// }
357/// ```
358#[derive(Default)]
359pub struct OptObserverVec {
360 observers: Vec<Box<dyn OptObserver>>,
361}
362
363impl OptObserverVec {
364 /// Create a new empty observer collection.
365 pub fn new() -> Self {
366 Self {
367 observers: Vec::new(),
368 }
369 }
370
371 /// Add an observer to the collection.
372 ///
373 /// The observer will be called at each optimization iteration in the order
374 /// it was added.
375 ///
376 /// # Arguments
377 ///
378 /// * `observer` - Any type implementing `OptObserver`
379 ///
380 /// # Examples
381 ///
382 /// ```no_run
383 /// use apex_solver::observers::{OptObserver, OptObserverVec};
384 /// use apex_solver::core::variable::ManifoldVariable;
385 /// use apex_solver::core::VarKey;
386 /// use slotmap::SlotMap;
387 ///
388 /// struct MyObserver;
389 /// impl OptObserver for MyObserver {
390 /// fn on_step(&self, _values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, _iteration: usize) {
391 /// // Handle optimization step
392 /// }
393 /// }
394 ///
395 /// let mut observers = OptObserverVec::new();
396 /// observers.add(MyObserver);
397 /// ```
398 pub fn add(&mut self, observer: impl OptObserver + 'static) {
399 self.observers.push(Box::new(observer));
400 }
401
402 /// Set iteration metrics for all observers.
403 ///
404 /// Calls `set_iteration_metrics()` on each registered observer. This should
405 /// be called before `notify()` to provide optimization metrics.
406 ///
407 /// # Arguments
408 ///
409 /// * `cost` - Current cost function value
410 /// * `gradient_norm` - L2 norm of the gradient vector
411 /// * `damping` - Damping parameter (may be None)
412 /// * `step_norm` - L2 norm of the parameter update step
413 /// * `step_quality` - Step quality metric (may be None)
414 #[inline]
415 pub fn set_iteration_metrics(
416 &self,
417 cost: f64,
418 gradient_norm: f64,
419 damping: Option<f64>,
420 step_norm: f64,
421 step_quality: Option<f64>,
422 ) {
423 for observer in &self.observers {
424 observer.set_iteration_metrics(cost, gradient_norm, damping, step_norm, step_quality);
425 }
426 }
427
428 /// Set matrix data for all observers.
429 ///
430 /// Calls `set_matrix_data()` on each registered observer. This should
431 /// be called before `notify()` to provide matrix data for visualization.
432 ///
433 /// # Arguments
434 ///
435 /// * `hessian` - Sparse Hessian matrix
436 /// * `gradient` - Gradient vector
437 #[inline]
438 pub fn set_matrix_data(
439 &self,
440 hessian: Option<sparse::SparseColMat<usize, f64>>,
441 gradient: Option<Mat<f64>>,
442 ) {
443 for observer in &self.observers {
444 observer.set_matrix_data(hessian.clone(), gradient.clone());
445 }
446 }
447
448 /// Notify all observers with current optimization state.
449 ///
450 /// Calls `on_step()` on each registered observer in order. If no observers
451 /// are registered, this is a no-op with zero overhead.
452 ///
453 /// # Arguments
454 ///
455 /// * `values` - Current variable values
456 /// * `iteration` - Current iteration number
457 ///
458 /// # Examples
459 ///
460 /// ```no_run
461 /// use apex_solver::observers::OptObserverVec;
462 /// use apex_solver::core::variable::ManifoldVariable;
463 /// use apex_solver::core::VarKey;
464 /// use slotmap::SlotMap;
465 ///
466 /// let observers = OptObserverVec::new();
467 /// let values: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
468 ///
469 /// // Notify all observers (safe even if empty)
470 /// observers.notify(&values, 0);
471 /// ```
472 #[inline]
473 pub fn notify(&self, values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, iteration: usize) {
474 for observer in &self.observers {
475 observer.on_step(values, iteration);
476 }
477 }
478
479 /// Notify all observers that optimization is complete.
480 ///
481 /// Calls `on_optimization_complete()` on each registered observer. This should
482 /// be called once at the end of optimization, after all iterations are done.
483 ///
484 /// # Arguments
485 ///
486 /// * `values` - Final optimized variable values
487 /// * `iterations` - Total number of iterations performed
488 ///
489 /// # Examples
490 ///
491 /// ```no_run
492 /// use apex_solver::observers::OptObserverVec;
493 /// use apex_solver::core::variable::ManifoldVariable;
494 /// use apex_solver::core::VarKey;
495 /// use slotmap::SlotMap;
496 ///
497 /// let observers = OptObserverVec::new();
498 /// let values: SlotMap<VarKey, Box<dyn ManifoldVariable>> = SlotMap::with_key();
499 ///
500 /// // Notify all observers that optimization is complete
501 /// observers.notify_complete(&values, 50);
502 /// ```
503 #[inline]
504 pub fn notify_complete(
505 &self,
506 values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
507 iterations: usize,
508 ) {
509 for observer in &self.observers {
510 observer.on_optimization_complete(values, iterations);
511 }
512 }
513
514 /// Check if any observers are registered.
515 ///
516 /// Useful for conditional logic or debugging.
517 #[inline]
518 pub fn is_empty(&self) -> bool {
519 self.observers.is_empty()
520 }
521
522 /// Get the number of registered observers.
523 #[inline]
524 pub fn len(&self) -> usize {
525 self.observers.len()
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use crate::error::ErrorLogging;
533 use std::sync::{Arc, Mutex};
534
535 fn empty_vars() -> SlotMap<VarKey, Box<dyn ManifoldVariable>> {
536 SlotMap::with_key()
537 }
538
539 #[derive(Clone)]
540 struct TestObserver {
541 calls: Arc<Mutex<Vec<usize>>>,
542 }
543
544 impl OptObserver for TestObserver {
545 fn on_step(&self, _values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, iteration: usize) {
546 // In test code, we log and ignore mutex poisoning errors since they indicate test bugs
547 if let Ok(mut guard) = self.calls.lock().map_err(|e| {
548 ObserverError::MutexPoisoned {
549 context: "TestObserver::on_step".to_string(),
550 reason: e.to_string(),
551 }
552 .log()
553 }) {
554 guard.push(iteration);
555 }
556 }
557 }
558
559 #[test]
560 fn test_empty_observers() {
561 let observers = OptObserverVec::new();
562 assert!(observers.is_empty());
563 assert_eq!(observers.len(), 0);
564
565 // Should not panic with no observers
566 observers.notify(&empty_vars(), 0);
567 }
568
569 #[test]
570 fn test_single_observer() -> Result<(), ObserverError> {
571 let calls = Arc::new(Mutex::new(Vec::new()));
572 let observer = TestObserver {
573 calls: calls.clone(),
574 };
575
576 let mut observers = OptObserverVec::new();
577 observers.add(observer);
578
579 assert_eq!(observers.len(), 1);
580
581 observers.notify(&empty_vars(), 0);
582 observers.notify(&empty_vars(), 1);
583 observers.notify(&empty_vars(), 2);
584
585 let guard = calls.lock().map_err(|e| {
586 ObserverError::MutexPoisoned {
587 context: "test_single_observer".to_string(),
588 reason: e.to_string(),
589 }
590 .log()
591 })?;
592 assert_eq!(*guard, vec![0, 1, 2]);
593 Ok(())
594 }
595
596 #[test]
597 fn test_multiple_observers() -> Result<(), ObserverError> {
598 let calls1 = Arc::new(Mutex::new(Vec::new()));
599 let calls2 = Arc::new(Mutex::new(Vec::new()));
600
601 let observer1 = TestObserver {
602 calls: calls1.clone(),
603 };
604 let observer2 = TestObserver {
605 calls: calls2.clone(),
606 };
607
608 let mut observers = OptObserverVec::new();
609 observers.add(observer1);
610 observers.add(observer2);
611
612 assert_eq!(observers.len(), 2);
613
614 observers.notify(&empty_vars(), 5);
615
616 let guard1 = calls1.lock().map_err(|e| {
617 ObserverError::MutexPoisoned {
618 context: "test_multiple_observers (calls1)".to_string(),
619 reason: e.to_string(),
620 }
621 .log()
622 })?;
623 assert_eq!(*guard1, vec![5]);
624
625 let guard2 = calls2.lock().map_err(|e| {
626 ObserverError::MutexPoisoned {
627 context: "test_multiple_observers (calls2)".to_string(),
628 reason: e.to_string(),
629 }
630 .log()
631 })?;
632 assert_eq!(*guard2, vec![5]);
633 Ok(())
634 }
635
636 // -------------------------------------------------------------------------
637 // ObserverError Display — one per variant
638 // -------------------------------------------------------------------------
639
640 #[test]
641 fn test_observer_error_rerun_initialization_display() {
642 let e = ObserverError::RerunInitialization("init fail".into());
643 assert!(e.to_string().contains("init fail"));
644 }
645
646 #[test]
647 fn test_observer_error_viewer_spawn_failed_display() {
648 let e = ObserverError::ViewerSpawnFailed("spawn fail".into());
649 assert!(e.to_string().contains("spawn fail"));
650 }
651
652 #[test]
653 fn test_observer_error_recording_save_failed_display() {
654 let e = ObserverError::RecordingSaveFailed {
655 path: "/tmp/out.rrd".into(),
656 reason: "disk full".into(),
657 };
658 let s = e.to_string();
659 assert!(s.contains("/tmp/out.rrd"), "{s}");
660 assert!(s.contains("disk full"), "{s}");
661 }
662
663 #[test]
664 fn test_observer_error_logging_failed_display() {
665 let e = ObserverError::LoggingFailed {
666 entity_path: "world/points".into(),
667 reason: "timeout".into(),
668 };
669 let s = e.to_string();
670 assert!(s.contains("world/points"), "{s}");
671 assert!(s.contains("timeout"), "{s}");
672 }
673
674 #[test]
675 fn test_observer_error_matrix_visualization_failed_display() {
676 let e = ObserverError::MatrixVisualizationFailed("bad dims".into());
677 assert!(e.to_string().contains("bad dims"));
678 }
679
680 #[test]
681 fn test_observer_error_tensor_conversion_failed_display() {
682 let e = ObserverError::TensorConversionFailed("nan values".into());
683 assert!(e.to_string().contains("nan values"));
684 }
685
686 #[test]
687 fn test_observer_error_invalid_state_display() {
688 let e = ObserverError::InvalidState("stream closed".into());
689 assert!(e.to_string().contains("stream closed"));
690 }
691
692 #[test]
693 fn test_observer_error_mutex_poisoned_display() {
694 let e = ObserverError::MutexPoisoned {
695 context: "on_step".into(),
696 reason: "thread panicked".into(),
697 };
698 let s = e.to_string();
699 assert!(s.contains("on_step"), "{s}");
700 assert!(s.contains("thread panicked"), "{s}");
701 }
702
703 // -------------------------------------------------------------------------
704 // log() / log_with_source() return self
705 // -------------------------------------------------------------------------
706
707 #[test]
708 fn test_observer_error_log_returns_self() {
709 let e = ObserverError::InvalidState("log_test".into());
710 let returned = e.log();
711 assert!(returned.to_string().contains("log_test"));
712 }
713
714 #[test]
715 fn test_observer_error_log_with_source_returns_self() {
716 let e = ObserverError::MatrixVisualizationFailed("src_test".into());
717 let source = std::io::Error::other("src");
718 let returned = e.log_with_source(source);
719 assert!(returned.to_string().contains("src_test"));
720 }
721
722 // -------------------------------------------------------------------------
723 // OptObserverVec — set_iteration_metrics, set_matrix_data, notify_complete
724 // -------------------------------------------------------------------------
725
726 #[test]
727 fn test_set_iteration_metrics_no_panic() {
728 let mut observers = OptObserverVec::new();
729 observers.add(TestObserver {
730 calls: Arc::new(Mutex::new(Vec::new())),
731 });
732 // Should not panic whether empty or not
733 observers.set_iteration_metrics(1.5, 1e-3, Some(1e-4), 0.01, Some(0.9));
734 }
735
736 #[test]
737 fn test_set_iteration_metrics_empty_no_panic() {
738 let observers = OptObserverVec::new();
739 observers.set_iteration_metrics(0.0, 0.0, None, 0.0, None);
740 }
741
742 #[test]
743 fn test_set_matrix_data_no_panic() {
744 let mut observers = OptObserverVec::new();
745 observers.add(TestObserver {
746 calls: Arc::new(Mutex::new(Vec::new())),
747 });
748 // Pass None for both hessian and gradient
749 observers.set_matrix_data(None, None);
750 }
751
752 // Observer that counts on_optimization_complete calls
753 #[derive(Clone)]
754 struct CompleteObserver {
755 complete_calls: Arc<Mutex<usize>>,
756 }
757
758 impl OptObserver for CompleteObserver {
759 fn on_step(&self, _values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, _iteration: usize) {
760 }
761
762 fn on_optimization_complete(
763 &self,
764 _values: &SlotMap<VarKey, Box<dyn ManifoldVariable>>,
765 _iterations: usize,
766 ) {
767 if let Ok(mut guard) = self.complete_calls.lock() {
768 *guard += 1;
769 }
770 }
771 }
772
773 #[test]
774 fn test_notify_complete_calls_on_optimization_complete() {
775 let complete_calls = Arc::new(Mutex::new(0usize));
776 let observer = CompleteObserver {
777 complete_calls: complete_calls.clone(),
778 };
779
780 let mut observers = OptObserverVec::new();
781 observers.add(observer);
782 observers.notify_complete(&empty_vars(), 10);
783
784 let count = *complete_calls.lock().unwrap_or_else(|e| e.into_inner());
785 assert_eq!(count, 1);
786 }
787
788 #[test]
789 fn test_notify_complete_empty_no_panic() {
790 let observers = OptObserverVec::new();
791 observers.notify_complete(&empty_vars(), 5);
792 }
793
794 #[test]
795 fn test_default_trait_methods_no_panic() {
796 // TestObserver only overrides on_step; the default impls are exercised here
797 let observer = TestObserver {
798 calls: Arc::new(Mutex::new(Vec::new())),
799 };
800 // Default implementations should be no-ops
801 observer.set_iteration_metrics(1.0, 1e-3, None, 0.0, None);
802 observer.set_matrix_data(None, None);
803 observer.on_optimization_complete(&empty_vars(), 5);
804 }
805}