1use thiserror::Error;
12
13#[derive(Debug, Error)]
17pub enum Error {
18 #[error("{0}")]
20 General(String),
21
22 #[error("parameter '{name}' = {value} is out of range [{min}, {max}]")]
24 OutOfRange {
25 name: &'static str,
27 value: f64,
29 min: f64,
31 max: f64,
33 },
34
35 #[error("mesh validation failed: {reason}")]
37 InvalidMesh {
38 reason: String,
40 },
41
42 #[error("buffer length mismatch: expected {expected}, got {actual}")]
44 LengthMismatch {
45 expected: usize,
47 actual: usize,
49 },
50
51 #[error(
53 "numerical computation '{operation}' did not converge after {iterations} iterations (residual {residual:.3e})"
54 )]
55 ConvergenceFailure {
56 operation: &'static str,
58 iterations: usize,
60 residual: f64,
62 },
63
64 #[error("index out of bounds: {index} >= {len}")]
66 IndexOutOfBounds {
67 index: usize,
69 len: usize,
71 },
72
73 #[error("shape requires at least {required} vertices/points, got {actual}")]
75 TooFewPoints {
76 required: usize,
78 actual: usize,
80 },
81
82 #[error("degenerate geometry: {details}")]
84 DegenerateGeometry {
85 details: String,
87 },
88
89 #[error("array dimension mismatch: '{lhs}' has {lhs_len} elements, '{rhs}' has {rhs_len}")]
91 DimensionMismatch {
92 lhs: &'static str,
94 lhs_len: usize,
96 rhs: &'static str,
98 rhs_len: usize,
100 },
101
102 #[error("unsupported operation '{operation}': {reason}")]
104 Unsupported {
105 operation: &'static str,
107 reason: String,
109 },
110
111 #[error("I/O error in '{context}': {message}")]
113 Io {
114 context: &'static str,
116 message: String,
118 },
119}
120
121pub type Result<T> = std::result::Result<T, Error>;
123
124impl Error {
127 pub fn general(msg: impl Into<String>) -> Self {
129 Self::General(msg.into())
130 }
131
132 pub fn out_of_range(name: &'static str, value: f64, min: f64, max: f64) -> Self {
134 Self::OutOfRange {
135 name,
136 value,
137 min,
138 max,
139 }
140 }
141
142 pub fn invalid_mesh(reason: impl Into<String>) -> Self {
144 Self::InvalidMesh {
145 reason: reason.into(),
146 }
147 }
148
149 pub fn length_mismatch(expected: usize, actual: usize) -> Self {
151 Self::LengthMismatch { expected, actual }
152 }
153
154 pub fn convergence_failure(operation: &'static str, iterations: usize, residual: f64) -> Self {
156 Self::ConvergenceFailure {
157 operation,
158 iterations,
159 residual,
160 }
161 }
162
163 pub fn index_out_of_bounds(index: usize, len: usize) -> Self {
165 Self::IndexOutOfBounds { index, len }
166 }
167
168 pub fn too_few_points(required: usize, actual: usize) -> Self {
170 Self::TooFewPoints { required, actual }
171 }
172
173 pub fn degenerate_geometry(details: impl Into<String>) -> Self {
175 Self::DegenerateGeometry {
176 details: details.into(),
177 }
178 }
179
180 pub fn dimension_mismatch(
182 lhs: &'static str,
183 lhs_len: usize,
184 rhs: &'static str,
185 rhs_len: usize,
186 ) -> Self {
187 Self::DimensionMismatch {
188 lhs,
189 lhs_len,
190 rhs,
191 rhs_len,
192 }
193 }
194
195 pub fn unsupported(operation: &'static str, reason: impl Into<String>) -> Self {
197 Self::Unsupported {
198 operation,
199 reason: reason.into(),
200 }
201 }
202
203 pub fn io(context: &'static str, message: impl Into<String>) -> Self {
205 Self::Io {
206 context,
207 message: message.into(),
208 }
209 }
210
211 pub fn is_general(&self) -> bool {
213 matches!(self, Self::General(_))
214 }
215
216 pub fn is_length_mismatch(&self) -> bool {
218 matches!(self, Self::LengthMismatch { .. })
219 }
220
221 pub fn is_convergence_failure(&self) -> bool {
223 matches!(self, Self::ConvergenceFailure { .. })
224 }
225
226 pub fn is_index_out_of_bounds(&self) -> bool {
228 matches!(self, Self::IndexOutOfBounds { .. })
229 }
230
231 pub fn is_degenerate(&self) -> bool {
233 matches!(self, Self::DegenerateGeometry { .. })
234 }
235}
236
237pub fn check_range(name: &'static str, value: f64, min: f64, max: f64) -> Result<()> {
242 if value >= min && value <= max {
243 Ok(())
244 } else {
245 Err(Error::out_of_range(name, value, min, max))
246 }
247}
248
249pub fn check_len(expected: usize, actual: usize) -> Result<()> {
251 if actual == expected {
252 Ok(())
253 } else {
254 Err(Error::length_mismatch(expected, actual))
255 }
256}
257
258pub fn check_index(index: usize, len: usize) -> Result<()> {
260 if index < len {
261 Ok(())
262 } else {
263 Err(Error::index_out_of_bounds(index, len))
264 }
265}
266
267pub fn check_min_points(required: usize, actual: usize) -> Result<()> {
269 if actual >= required {
270 Ok(())
271 } else {
272 Err(Error::too_few_points(required, actual))
273 }
274}
275
276pub fn check_dim_match(
278 lhs: &'static str,
279 lhs_len: usize,
280 rhs: &'static str,
281 rhs_len: usize,
282) -> Result<()> {
283 if lhs_len == rhs_len {
284 Ok(())
285 } else {
286 Err(Error::dimension_mismatch(lhs, lhs_len, rhs, rhs_len))
287 }
288}
289
290pub fn check_positive(name: &'static str, value: f64) -> Result<()> {
292 if value > 0.0 {
293 Ok(())
294 } else {
295 Err(Error::out_of_range(name, value, f64::EPSILON, f64::MAX))
296 }
297}
298
299pub fn check_non_negative(name: &'static str, value: f64) -> Result<()> {
301 if value >= 0.0 {
302 Ok(())
303 } else {
304 Err(Error::out_of_range(name, value, 0.0, f64::MAX))
305 }
306}
307
308pub fn check_finite(name: &'static str, value: f64) -> Result<()> {
310 if value.is_finite() {
311 Ok(())
312 } else {
313 Err(Error::general(format!(
314 "parameter '{name}' is not finite: {value}"
315 )))
316 }
317}
318
319pub fn check_finite_slice(name: &'static str, values: &[f64]) -> Result<()> {
321 for (i, &v) in values.iter().enumerate() {
322 if !v.is_finite() {
323 return Err(Error::general(format!(
324 "parameter '{name}[{i}]' is not finite: {v}"
325 )));
326 }
327 }
328 Ok(())
329}
330
331pub fn validate_mesh(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> Result<()> {
340 check_min_points(1, vertices.len())?;
341 for (i, tri) in triangles.iter().enumerate() {
342 for &idx in tri {
343 if idx >= vertices.len() {
344 return Err(Error::InvalidMesh {
345 reason: format!(
346 "triangle {i}: index {idx} >= vertex count {}",
347 vertices.len()
348 ),
349 });
350 }
351 }
352 if tri[0] == tri[1] || tri[1] == tri[2] || tri[0] == tri[2] {
353 return Err(Error::DegenerateGeometry {
354 details: format!(
355 "triangle {i} has repeated indices: [{}, {}, {}]",
356 tri[0], tri[1], tri[2]
357 ),
358 });
359 }
360 }
361 Ok(())
362}
363
364pub fn validate_heightfield(
369 heights: &[f64],
370 rows: usize,
371 cols: usize,
372 scale_x: f64,
373 scale_z: f64,
374) -> Result<()> {
375 if rows < 2 {
376 return Err(Error::TooFewPoints {
377 required: 2,
378 actual: rows,
379 });
380 }
381 if cols < 2 {
382 return Err(Error::TooFewPoints {
383 required: 2,
384 actual: cols,
385 });
386 }
387 check_positive("scale_x", scale_x)?;
388 check_positive("scale_z", scale_z)?;
389 check_len(rows * cols, heights.len())?;
390 check_finite_slice("heights", heights)?;
391 Ok(())
392}
393
394pub fn validate_ray_dir(dir: [f64; 3]) -> Result<()> {
396 check_finite_slice("ray_dir", &dir)?;
397 let len_sq = dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2];
398 if len_sq < 1e-30 {
399 return Err(Error::DegenerateGeometry {
400 details: "ray direction is zero (or near-zero)".into(),
401 });
402 }
403 Ok(())
404}
405
406pub fn validate_point_cloud(points: &[[f64; 3]]) -> Result<()> {
408 check_min_points(1, points.len())?;
409 for (i, p) in points.iter().enumerate() {
410 if !p[0].is_finite() || !p[1].is_finite() || !p[2].is_finite() {
411 return Err(Error::general(format!(
412 "point[{i}] contains non-finite coordinate: {p:?}"
413 )));
414 }
415 }
416 Ok(())
417}
418
419pub trait WithContext<T> {
424 fn with_context(self, ctx: &str) -> Result<T>;
426}
427
428impl<T, E: std::fmt::Display> WithContext<T> for std::result::Result<T, E> {
429 fn with_context(self, ctx: &str) -> Result<T> {
430 self.map_err(|e| Error::General(format!("{ctx}: {e}")))
431 }
432}
433
434#[derive(Debug, Clone)]
439pub struct ConvergenceTracker {
440 operation: &'static str,
441 max_iterations: usize,
442 tolerance: f64,
443 current_iteration: usize,
444 last_residual: f64,
445}
446
447impl ConvergenceTracker {
448 pub fn new(operation: &'static str, max_iterations: usize, tolerance: f64) -> Self {
455 Self {
456 operation,
457 max_iterations,
458 tolerance,
459 current_iteration: 0,
460 last_residual: f64::INFINITY,
461 }
462 }
463
464 pub fn update(&mut self, residual: f64) -> Result<bool> {
469 self.last_residual = residual;
470 self.current_iteration += 1;
471 if residual < self.tolerance {
472 return Ok(true);
473 }
474 if self.current_iteration >= self.max_iterations {
475 return Err(Error::convergence_failure(
476 self.operation,
477 self.current_iteration,
478 residual,
479 ));
480 }
481 Ok(false)
482 }
483
484 pub fn iterations(&self) -> usize {
486 self.current_iteration
487 }
488
489 pub fn residual(&self) -> f64 {
491 self.last_residual
492 }
493
494 pub fn reset(&mut self) {
496 self.current_iteration = 0;
497 self.last_residual = f64::INFINITY;
498 }
499}
500
501#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
510 fn test_general_error_display() {
511 let e = Error::general("something went wrong");
512 assert!(e.to_string().contains("something went wrong"));
513 }
514
515 #[test]
516 fn test_out_of_range_display() {
517 let e = Error::out_of_range("radius", -1.0, 0.0, 100.0);
518 let s = e.to_string();
519 assert!(s.contains("radius"), "should mention parameter name");
520 assert!(s.contains("-1"), "should mention actual value");
521 }
522
523 #[test]
524 fn test_length_mismatch_display() {
525 let e = Error::length_mismatch(10, 7);
526 let s = e.to_string();
527 assert!(s.contains("10"));
528 assert!(s.contains("7"));
529 }
530
531 #[test]
532 fn test_convergence_failure_display() {
533 let e = Error::convergence_failure("smoothing", 100, 0.001);
534 let s = e.to_string();
535 assert!(s.contains("smoothing"));
536 assert!(s.contains("100"));
537 }
538
539 #[test]
540 fn test_index_out_of_bounds_display() {
541 let e = Error::index_out_of_bounds(5, 3);
542 let s = e.to_string();
543 assert!(s.contains('5'));
544 assert!(s.contains('3'));
545 }
546
547 #[test]
548 fn test_too_few_points_display() {
549 let e = Error::too_few_points(3, 1);
550 let s = e.to_string();
551 assert!(s.contains('3'));
552 assert!(s.contains('1'));
553 }
554
555 #[test]
556 fn test_degenerate_geometry_display() {
557 let e = Error::degenerate_geometry("zero area triangle");
558 assert!(e.to_string().contains("zero area triangle"));
559 }
560
561 #[test]
562 fn test_dimension_mismatch_display() {
563 let e = Error::dimension_mismatch("positions", 10, "normals", 8);
564 let s = e.to_string();
565 assert!(s.contains("positions"));
566 assert!(s.contains("normals"));
567 }
568
569 #[test]
570 fn test_unsupported_display() {
571 let e = Error::unsupported("CSG union", "non-manifold mesh");
572 let s = e.to_string();
573 assert!(s.contains("CSG union"));
574 assert!(s.contains("non-manifold mesh"));
575 }
576
577 #[test]
578 fn test_io_error_display() {
579 let e = Error::io("serialize heightfield", "disk full");
580 let s = e.to_string();
581 assert!(s.contains("serialize heightfield"));
582 assert!(s.contains("disk full"));
583 }
584
585 #[test]
588 fn test_is_general() {
589 assert!(Error::general("x").is_general());
590 assert!(!Error::length_mismatch(1, 2).is_general());
591 }
592
593 #[test]
594 fn test_is_length_mismatch() {
595 assert!(Error::length_mismatch(3, 4).is_length_mismatch());
596 assert!(!Error::general("x").is_length_mismatch());
597 }
598
599 #[test]
600 fn test_is_convergence_failure() {
601 assert!(Error::convergence_failure("op", 10, 0.1).is_convergence_failure());
602 assert!(!Error::general("x").is_convergence_failure());
603 }
604
605 #[test]
606 fn test_is_degenerate() {
607 assert!(Error::degenerate_geometry("zero vol").is_degenerate());
608 assert!(!Error::general("x").is_degenerate());
609 }
610
611 #[test]
614 fn test_check_range_ok() {
615 assert!(check_range("r", 5.0, 0.0, 10.0).is_ok());
616 }
617
618 #[test]
619 fn test_check_range_below_min() {
620 let r = check_range("r", -1.0, 0.0, 10.0);
621 assert!(r.is_err());
622 assert!(matches!(r.unwrap_err(), Error::OutOfRange { .. }));
623 }
624
625 #[test]
626 fn test_check_range_above_max() {
627 let r = check_range("r", 11.0, 0.0, 10.0);
628 assert!(r.is_err());
629 }
630
631 #[test]
632 fn test_check_len_ok() {
633 assert!(check_len(5, 5).is_ok());
634 }
635
636 #[test]
637 fn test_check_len_mismatch() {
638 assert!(check_len(5, 4).is_err());
639 }
640
641 #[test]
642 fn test_check_index_ok() {
643 assert!(check_index(0, 1).is_ok());
644 assert!(check_index(4, 5).is_ok());
645 }
646
647 #[test]
648 fn test_check_index_equal_to_len_fails() {
649 assert!(check_index(5, 5).is_err());
650 }
651
652 #[test]
653 fn test_check_min_points_ok() {
654 assert!(check_min_points(3, 3).is_ok());
655 assert!(check_min_points(3, 10).is_ok());
656 }
657
658 #[test]
659 fn test_check_min_points_too_few() {
660 assert!(check_min_points(4, 2).is_err());
661 }
662
663 #[test]
664 fn test_check_positive_ok() {
665 assert!(check_positive("s", 0.001).is_ok());
666 }
667
668 #[test]
669 fn test_check_positive_zero_fails() {
670 assert!(check_positive("s", 0.0).is_err());
671 }
672
673 #[test]
674 fn test_check_non_negative_ok() {
675 assert!(check_non_negative("v", 0.0).is_ok());
676 assert!(check_non_negative("v", 1.5).is_ok());
677 }
678
679 #[test]
680 fn test_check_non_negative_negative_fails() {
681 assert!(check_non_negative("v", -0.1).is_err());
682 }
683
684 #[test]
685 fn test_check_finite_ok() {
686 assert!(check_finite("x", 3.125).is_ok());
687 }
688
689 #[test]
690 fn test_check_finite_nan_fails() {
691 assert!(check_finite("x", f64::NAN).is_err());
692 }
693
694 #[test]
695 fn test_check_finite_inf_fails() {
696 assert!(check_finite("x", f64::INFINITY).is_err());
697 }
698
699 #[test]
700 fn test_check_finite_slice_ok() {
701 assert!(check_finite_slice("pts", &[1.0, 2.0, 3.0]).is_ok());
702 }
703
704 #[test]
705 fn test_check_finite_slice_nan_fails() {
706 assert!(check_finite_slice("pts", &[1.0, f64::NAN, 3.0]).is_err());
707 }
708
709 #[test]
710 fn test_check_dim_match_ok() {
711 assert!(check_dim_match("pos", 5, "nrm", 5).is_ok());
712 }
713
714 #[test]
715 fn test_check_dim_match_fail() {
716 let r = check_dim_match("pos", 5, "nrm", 3);
717 assert!(r.is_err());
718 assert!(matches!(r.unwrap_err(), Error::DimensionMismatch { .. }));
719 }
720
721 #[test]
724 fn test_validate_mesh_ok() {
725 let verts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
726 let tris = [[0, 1, 2]];
727 assert!(validate_mesh(&verts, &tris).is_ok());
728 }
729
730 #[test]
731 fn test_validate_mesh_index_out_of_range() {
732 let verts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
733 let tris = [[0, 1, 5]]; assert!(validate_mesh(&verts, &tris).is_err());
735 }
736
737 #[test]
738 fn test_validate_mesh_degenerate_triangle() {
739 let verts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
740 let tris = [[0, 0, 2]]; let r = validate_mesh(&verts, &tris);
742 assert!(r.is_err());
743 assert!(matches!(r.unwrap_err(), Error::DegenerateGeometry { .. }));
744 }
745
746 #[test]
747 fn test_validate_mesh_empty_vertices() {
748 let r = validate_mesh(&[], &[[0, 1, 2]]);
749 assert!(r.is_err());
750 }
751
752 #[test]
755 fn test_validate_heightfield_ok() {
756 let heights = vec![0.0f64; 4 * 4];
757 assert!(validate_heightfield(&heights, 4, 4, 1.0, 1.0).is_ok());
758 }
759
760 #[test]
761 fn test_validate_heightfield_too_few_rows() {
762 let heights = vec![0.0f64; 4];
763 assert!(validate_heightfield(&heights, 1, 4, 1.0, 1.0).is_err());
764 }
765
766 #[test]
767 fn test_validate_heightfield_bad_scale() {
768 let heights = vec![0.0f64; 4 * 4];
769 assert!(validate_heightfield(&heights, 4, 4, 0.0, 1.0).is_err());
770 }
771
772 #[test]
773 fn test_validate_heightfield_len_mismatch() {
774 let heights = vec![0.0f64; 10]; assert!(validate_heightfield(&heights, 4, 4, 1.0, 1.0).is_err());
776 }
777
778 #[test]
779 fn test_validate_heightfield_nan_height() {
780 let mut heights = vec![0.0f64; 4 * 4];
781 heights[5] = f64::NAN;
782 assert!(validate_heightfield(&heights, 4, 4, 1.0, 1.0).is_err());
783 }
784
785 #[test]
788 fn test_validate_ray_dir_ok() {
789 assert!(validate_ray_dir([0.0, -1.0, 0.0]).is_ok());
790 }
791
792 #[test]
793 fn test_validate_ray_dir_zero_fails() {
794 assert!(validate_ray_dir([0.0, 0.0, 0.0]).is_err());
795 }
796
797 #[test]
798 fn test_validate_ray_dir_nan_fails() {
799 assert!(validate_ray_dir([f64::NAN, 0.0, 0.0]).is_err());
800 }
801
802 #[test]
805 fn test_validate_point_cloud_ok() {
806 let pts = [[0.0, 0.0, 0.0], [1.0, 2.0, 3.0]];
807 assert!(validate_point_cloud(&pts).is_ok());
808 }
809
810 #[test]
811 fn test_validate_point_cloud_empty_fails() {
812 assert!(validate_point_cloud(&[]).is_err());
813 }
814
815 #[test]
816 fn test_validate_point_cloud_nan_fails() {
817 let pts = [[f64::NAN, 0.0, 0.0]];
818 assert!(validate_point_cloud(&pts).is_err());
819 }
820
821 #[test]
824 fn test_with_context_ok_passes_through() {
825 let r: std::result::Result<i32, &str> = Ok(42);
826 let r2: Result<i32> = r.with_context("test");
827 assert_eq!(r2.unwrap(), 42);
828 }
829
830 #[test]
831 fn test_with_context_wraps_error() {
832 let r: std::result::Result<i32, &str> = Err("original error");
833 let r2: Result<i32> = r.with_context("loading mesh");
834 let e = r2.unwrap_err();
835 let s = e.to_string();
836 assert!(s.contains("loading mesh"), "context missing: {s}");
837 assert!(s.contains("original error"), "original missing: {s}");
838 }
839
840 #[test]
843 fn test_convergence_tracker_converges() {
844 let mut tracker = ConvergenceTracker::new("test_op", 100, 1e-6);
845 let result = tracker.update(1e-8);
847 assert!(result.is_ok());
848 assert!(result.unwrap(), "should report converged");
849 assert_eq!(tracker.iterations(), 1);
850 }
851
852 #[test]
853 fn test_convergence_tracker_not_yet_converged() {
854 let mut tracker = ConvergenceTracker::new("test_op", 100, 1e-6);
855 let result = tracker.update(0.5);
856 assert!(result.is_ok());
857 assert!(!result.unwrap(), "should not report converged yet");
858 }
859
860 #[test]
861 fn test_convergence_tracker_failure() {
862 let mut tracker = ConvergenceTracker::new("mesh_smooth", 3, 1e-10);
863 let _ = tracker.update(1.0);
864 let _ = tracker.update(0.5);
865 let r = tracker.update(0.3); assert!(r.is_err());
867 assert!(matches!(r.unwrap_err(), Error::ConvergenceFailure { .. }));
868 }
869
870 #[test]
871 fn test_convergence_tracker_residual_tracked() {
872 let mut tracker = ConvergenceTracker::new("op", 100, 1e-6);
873 let _ = tracker.update(0.7);
874 assert!((tracker.residual() - 0.7).abs() < 1e-12);
875 }
876
877 #[test]
878 fn test_convergence_tracker_reset() {
879 let mut tracker = ConvergenceTracker::new("op", 100, 1e-6);
880 let _ = tracker.update(0.5);
881 tracker.reset();
882 assert_eq!(tracker.iterations(), 0);
883 assert!(tracker.residual().is_infinite());
884 }
885
886 #[test]
887 fn test_convergence_tracker_iterations_counted() {
888 let mut tracker = ConvergenceTracker::new("op", 100, 1e-6);
889 for _ in 0..5 {
890 let _ = tracker.update(1.0);
891 }
892 assert_eq!(tracker.iterations(), 5);
893 }
894}