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 #[allow(dead_code)]
129 pub fn general(msg: impl Into<String>) -> Self {
130 Self::General(msg.into())
131 }
132
133 #[allow(dead_code)]
135 pub fn out_of_range(name: &'static str, value: f64, min: f64, max: f64) -> Self {
136 Self::OutOfRange {
137 name,
138 value,
139 min,
140 max,
141 }
142 }
143
144 #[allow(dead_code)]
146 pub fn invalid_mesh(reason: impl Into<String>) -> Self {
147 Self::InvalidMesh {
148 reason: reason.into(),
149 }
150 }
151
152 #[allow(dead_code)]
154 pub fn length_mismatch(expected: usize, actual: usize) -> Self {
155 Self::LengthMismatch { expected, actual }
156 }
157
158 #[allow(dead_code)]
160 pub fn convergence_failure(operation: &'static str, iterations: usize, residual: f64) -> Self {
161 Self::ConvergenceFailure {
162 operation,
163 iterations,
164 residual,
165 }
166 }
167
168 #[allow(dead_code)]
170 pub fn index_out_of_bounds(index: usize, len: usize) -> Self {
171 Self::IndexOutOfBounds { index, len }
172 }
173
174 #[allow(dead_code)]
176 pub fn too_few_points(required: usize, actual: usize) -> Self {
177 Self::TooFewPoints { required, actual }
178 }
179
180 #[allow(dead_code)]
182 pub fn degenerate_geometry(details: impl Into<String>) -> Self {
183 Self::DegenerateGeometry {
184 details: details.into(),
185 }
186 }
187
188 #[allow(dead_code)]
190 pub fn dimension_mismatch(
191 lhs: &'static str,
192 lhs_len: usize,
193 rhs: &'static str,
194 rhs_len: usize,
195 ) -> Self {
196 Self::DimensionMismatch {
197 lhs,
198 lhs_len,
199 rhs,
200 rhs_len,
201 }
202 }
203
204 #[allow(dead_code)]
206 pub fn unsupported(operation: &'static str, reason: impl Into<String>) -> Self {
207 Self::Unsupported {
208 operation,
209 reason: reason.into(),
210 }
211 }
212
213 #[allow(dead_code)]
215 pub fn io(context: &'static str, message: impl Into<String>) -> Self {
216 Self::Io {
217 context,
218 message: message.into(),
219 }
220 }
221
222 #[allow(dead_code)]
224 pub fn is_general(&self) -> bool {
225 matches!(self, Self::General(_))
226 }
227
228 #[allow(dead_code)]
230 pub fn is_length_mismatch(&self) -> bool {
231 matches!(self, Self::LengthMismatch { .. })
232 }
233
234 #[allow(dead_code)]
236 pub fn is_convergence_failure(&self) -> bool {
237 matches!(self, Self::ConvergenceFailure { .. })
238 }
239
240 #[allow(dead_code)]
242 pub fn is_index_out_of_bounds(&self) -> bool {
243 matches!(self, Self::IndexOutOfBounds { .. })
244 }
245
246 #[allow(dead_code)]
248 pub fn is_degenerate(&self) -> bool {
249 matches!(self, Self::DegenerateGeometry { .. })
250 }
251}
252
253#[allow(dead_code)]
258pub fn check_range(name: &'static str, value: f64, min: f64, max: f64) -> Result<()> {
259 if value >= min && value <= max {
260 Ok(())
261 } else {
262 Err(Error::out_of_range(name, value, min, max))
263 }
264}
265
266#[allow(dead_code)]
268pub fn check_len(expected: usize, actual: usize) -> Result<()> {
269 if actual == expected {
270 Ok(())
271 } else {
272 Err(Error::length_mismatch(expected, actual))
273 }
274}
275
276#[allow(dead_code)]
278pub fn check_index(index: usize, len: usize) -> Result<()> {
279 if index < len {
280 Ok(())
281 } else {
282 Err(Error::index_out_of_bounds(index, len))
283 }
284}
285
286#[allow(dead_code)]
288pub fn check_min_points(required: usize, actual: usize) -> Result<()> {
289 if actual >= required {
290 Ok(())
291 } else {
292 Err(Error::too_few_points(required, actual))
293 }
294}
295
296#[allow(dead_code)]
298pub fn check_dim_match(
299 lhs: &'static str,
300 lhs_len: usize,
301 rhs: &'static str,
302 rhs_len: usize,
303) -> Result<()> {
304 if lhs_len == rhs_len {
305 Ok(())
306 } else {
307 Err(Error::dimension_mismatch(lhs, lhs_len, rhs, rhs_len))
308 }
309}
310
311#[allow(dead_code)]
313pub fn check_positive(name: &'static str, value: f64) -> Result<()> {
314 if value > 0.0 {
315 Ok(())
316 } else {
317 Err(Error::out_of_range(name, value, f64::EPSILON, f64::MAX))
318 }
319}
320
321#[allow(dead_code)]
323pub fn check_non_negative(name: &'static str, value: f64) -> Result<()> {
324 if value >= 0.0 {
325 Ok(())
326 } else {
327 Err(Error::out_of_range(name, value, 0.0, f64::MAX))
328 }
329}
330
331#[allow(dead_code)]
333pub fn check_finite(name: &'static str, value: f64) -> Result<()> {
334 if value.is_finite() {
335 Ok(())
336 } else {
337 Err(Error::general(format!(
338 "parameter '{name}' is not finite: {value}"
339 )))
340 }
341}
342
343#[allow(dead_code)]
345pub fn check_finite_slice(name: &'static str, values: &[f64]) -> Result<()> {
346 for (i, &v) in values.iter().enumerate() {
347 if !v.is_finite() {
348 return Err(Error::general(format!(
349 "parameter '{name}[{i}]' is not finite: {v}"
350 )));
351 }
352 }
353 Ok(())
354}
355
356#[allow(dead_code)]
365pub fn validate_mesh(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> Result<()> {
366 check_min_points(1, vertices.len())?;
367 for (i, tri) in triangles.iter().enumerate() {
368 for &idx in tri {
369 if idx >= vertices.len() {
370 return Err(Error::InvalidMesh {
371 reason: format!(
372 "triangle {i}: index {idx} >= vertex count {}",
373 vertices.len()
374 ),
375 });
376 }
377 }
378 if tri[0] == tri[1] || tri[1] == tri[2] || tri[0] == tri[2] {
379 return Err(Error::DegenerateGeometry {
380 details: format!(
381 "triangle {i} has repeated indices: [{}, {}, {}]",
382 tri[0], tri[1], tri[2]
383 ),
384 });
385 }
386 }
387 Ok(())
388}
389
390#[allow(dead_code)]
395pub fn validate_heightfield(
396 heights: &[f64],
397 rows: usize,
398 cols: usize,
399 scale_x: f64,
400 scale_z: f64,
401) -> Result<()> {
402 if rows < 2 {
403 return Err(Error::TooFewPoints {
404 required: 2,
405 actual: rows,
406 });
407 }
408 if cols < 2 {
409 return Err(Error::TooFewPoints {
410 required: 2,
411 actual: cols,
412 });
413 }
414 check_positive("scale_x", scale_x)?;
415 check_positive("scale_z", scale_z)?;
416 check_len(rows * cols, heights.len())?;
417 check_finite_slice("heights", heights)?;
418 Ok(())
419}
420
421#[allow(dead_code)]
423pub fn validate_ray_dir(dir: [f64; 3]) -> Result<()> {
424 check_finite_slice("ray_dir", &dir)?;
425 let len_sq = dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2];
426 if len_sq < 1e-30 {
427 return Err(Error::DegenerateGeometry {
428 details: "ray direction is zero (or near-zero)".into(),
429 });
430 }
431 Ok(())
432}
433
434#[allow(dead_code)]
436pub fn validate_point_cloud(points: &[[f64; 3]]) -> Result<()> {
437 check_min_points(1, points.len())?;
438 for (i, p) in points.iter().enumerate() {
439 if !p[0].is_finite() || !p[1].is_finite() || !p[2].is_finite() {
440 return Err(Error::general(format!(
441 "point[{i}] contains non-finite coordinate: {p:?}"
442 )));
443 }
444 }
445 Ok(())
446}
447
448#[allow(dead_code)]
453pub trait WithContext<T> {
454 fn with_context(self, ctx: &str) -> Result<T>;
456}
457
458impl<T, E: std::fmt::Display> WithContext<T> for std::result::Result<T, E> {
459 fn with_context(self, ctx: &str) -> Result<T> {
460 self.map_err(|e| Error::General(format!("{ctx}: {e}")))
461 }
462}
463
464#[derive(Debug, Clone)]
469#[allow(dead_code)]
470pub struct ConvergenceTracker {
471 operation: &'static str,
472 max_iterations: usize,
473 tolerance: f64,
474 current_iteration: usize,
475 last_residual: f64,
476}
477
478#[allow(dead_code)]
479impl ConvergenceTracker {
480 pub fn new(operation: &'static str, max_iterations: usize, tolerance: f64) -> Self {
487 Self {
488 operation,
489 max_iterations,
490 tolerance,
491 current_iteration: 0,
492 last_residual: f64::INFINITY,
493 }
494 }
495
496 pub fn update(&mut self, residual: f64) -> Result<bool> {
501 self.last_residual = residual;
502 self.current_iteration += 1;
503 if residual < self.tolerance {
504 return Ok(true);
505 }
506 if self.current_iteration >= self.max_iterations {
507 return Err(Error::convergence_failure(
508 self.operation,
509 self.current_iteration,
510 residual,
511 ));
512 }
513 Ok(false)
514 }
515
516 pub fn iterations(&self) -> usize {
518 self.current_iteration
519 }
520
521 pub fn residual(&self) -> f64 {
523 self.last_residual
524 }
525
526 pub fn reset(&mut self) {
528 self.current_iteration = 0;
529 self.last_residual = f64::INFINITY;
530 }
531}
532
533#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
542 fn test_general_error_display() {
543 let e = Error::general("something went wrong");
544 assert!(e.to_string().contains("something went wrong"));
545 }
546
547 #[test]
548 fn test_out_of_range_display() {
549 let e = Error::out_of_range("radius", -1.0, 0.0, 100.0);
550 let s = e.to_string();
551 assert!(s.contains("radius"), "should mention parameter name");
552 assert!(s.contains("-1"), "should mention actual value");
553 }
554
555 #[test]
556 fn test_length_mismatch_display() {
557 let e = Error::length_mismatch(10, 7);
558 let s = e.to_string();
559 assert!(s.contains("10"));
560 assert!(s.contains("7"));
561 }
562
563 #[test]
564 fn test_convergence_failure_display() {
565 let e = Error::convergence_failure("smoothing", 100, 0.001);
566 let s = e.to_string();
567 assert!(s.contains("smoothing"));
568 assert!(s.contains("100"));
569 }
570
571 #[test]
572 fn test_index_out_of_bounds_display() {
573 let e = Error::index_out_of_bounds(5, 3);
574 let s = e.to_string();
575 assert!(s.contains('5'));
576 assert!(s.contains('3'));
577 }
578
579 #[test]
580 fn test_too_few_points_display() {
581 let e = Error::too_few_points(3, 1);
582 let s = e.to_string();
583 assert!(s.contains('3'));
584 assert!(s.contains('1'));
585 }
586
587 #[test]
588 fn test_degenerate_geometry_display() {
589 let e = Error::degenerate_geometry("zero area triangle");
590 assert!(e.to_string().contains("zero area triangle"));
591 }
592
593 #[test]
594 fn test_dimension_mismatch_display() {
595 let e = Error::dimension_mismatch("positions", 10, "normals", 8);
596 let s = e.to_string();
597 assert!(s.contains("positions"));
598 assert!(s.contains("normals"));
599 }
600
601 #[test]
602 fn test_unsupported_display() {
603 let e = Error::unsupported("CSG union", "non-manifold mesh");
604 let s = e.to_string();
605 assert!(s.contains("CSG union"));
606 assert!(s.contains("non-manifold mesh"));
607 }
608
609 #[test]
610 fn test_io_error_display() {
611 let e = Error::io("serialize heightfield", "disk full");
612 let s = e.to_string();
613 assert!(s.contains("serialize heightfield"));
614 assert!(s.contains("disk full"));
615 }
616
617 #[test]
620 fn test_is_general() {
621 assert!(Error::general("x").is_general());
622 assert!(!Error::length_mismatch(1, 2).is_general());
623 }
624
625 #[test]
626 fn test_is_length_mismatch() {
627 assert!(Error::length_mismatch(3, 4).is_length_mismatch());
628 assert!(!Error::general("x").is_length_mismatch());
629 }
630
631 #[test]
632 fn test_is_convergence_failure() {
633 assert!(Error::convergence_failure("op", 10, 0.1).is_convergence_failure());
634 assert!(!Error::general("x").is_convergence_failure());
635 }
636
637 #[test]
638 fn test_is_degenerate() {
639 assert!(Error::degenerate_geometry("zero vol").is_degenerate());
640 assert!(!Error::general("x").is_degenerate());
641 }
642
643 #[test]
646 fn test_check_range_ok() {
647 assert!(check_range("r", 5.0, 0.0, 10.0).is_ok());
648 }
649
650 #[test]
651 fn test_check_range_below_min() {
652 let r = check_range("r", -1.0, 0.0, 10.0);
653 assert!(r.is_err());
654 assert!(matches!(r.unwrap_err(), Error::OutOfRange { .. }));
655 }
656
657 #[test]
658 fn test_check_range_above_max() {
659 let r = check_range("r", 11.0, 0.0, 10.0);
660 assert!(r.is_err());
661 }
662
663 #[test]
664 fn test_check_len_ok() {
665 assert!(check_len(5, 5).is_ok());
666 }
667
668 #[test]
669 fn test_check_len_mismatch() {
670 assert!(check_len(5, 4).is_err());
671 }
672
673 #[test]
674 fn test_check_index_ok() {
675 assert!(check_index(0, 1).is_ok());
676 assert!(check_index(4, 5).is_ok());
677 }
678
679 #[test]
680 fn test_check_index_equal_to_len_fails() {
681 assert!(check_index(5, 5).is_err());
682 }
683
684 #[test]
685 fn test_check_min_points_ok() {
686 assert!(check_min_points(3, 3).is_ok());
687 assert!(check_min_points(3, 10).is_ok());
688 }
689
690 #[test]
691 fn test_check_min_points_too_few() {
692 assert!(check_min_points(4, 2).is_err());
693 }
694
695 #[test]
696 fn test_check_positive_ok() {
697 assert!(check_positive("s", 0.001).is_ok());
698 }
699
700 #[test]
701 fn test_check_positive_zero_fails() {
702 assert!(check_positive("s", 0.0).is_err());
703 }
704
705 #[test]
706 fn test_check_non_negative_ok() {
707 assert!(check_non_negative("v", 0.0).is_ok());
708 assert!(check_non_negative("v", 1.5).is_ok());
709 }
710
711 #[test]
712 fn test_check_non_negative_negative_fails() {
713 assert!(check_non_negative("v", -0.1).is_err());
714 }
715
716 #[test]
717 fn test_check_finite_ok() {
718 assert!(check_finite("x", 3.125).is_ok());
719 }
720
721 #[test]
722 fn test_check_finite_nan_fails() {
723 assert!(check_finite("x", f64::NAN).is_err());
724 }
725
726 #[test]
727 fn test_check_finite_inf_fails() {
728 assert!(check_finite("x", f64::INFINITY).is_err());
729 }
730
731 #[test]
732 fn test_check_finite_slice_ok() {
733 assert!(check_finite_slice("pts", &[1.0, 2.0, 3.0]).is_ok());
734 }
735
736 #[test]
737 fn test_check_finite_slice_nan_fails() {
738 assert!(check_finite_slice("pts", &[1.0, f64::NAN, 3.0]).is_err());
739 }
740
741 #[test]
742 fn test_check_dim_match_ok() {
743 assert!(check_dim_match("pos", 5, "nrm", 5).is_ok());
744 }
745
746 #[test]
747 fn test_check_dim_match_fail() {
748 let r = check_dim_match("pos", 5, "nrm", 3);
749 assert!(r.is_err());
750 assert!(matches!(r.unwrap_err(), Error::DimensionMismatch { .. }));
751 }
752
753 #[test]
756 fn test_validate_mesh_ok() {
757 let verts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
758 let tris = [[0, 1, 2]];
759 assert!(validate_mesh(&verts, &tris).is_ok());
760 }
761
762 #[test]
763 fn test_validate_mesh_index_out_of_range() {
764 let verts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
765 let tris = [[0, 1, 5]]; assert!(validate_mesh(&verts, &tris).is_err());
767 }
768
769 #[test]
770 fn test_validate_mesh_degenerate_triangle() {
771 let verts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
772 let tris = [[0, 0, 2]]; let r = validate_mesh(&verts, &tris);
774 assert!(r.is_err());
775 assert!(matches!(r.unwrap_err(), Error::DegenerateGeometry { .. }));
776 }
777
778 #[test]
779 fn test_validate_mesh_empty_vertices() {
780 let r = validate_mesh(&[], &[[0, 1, 2]]);
781 assert!(r.is_err());
782 }
783
784 #[test]
787 fn test_validate_heightfield_ok() {
788 let heights = vec![0.0f64; 4 * 4];
789 assert!(validate_heightfield(&heights, 4, 4, 1.0, 1.0).is_ok());
790 }
791
792 #[test]
793 fn test_validate_heightfield_too_few_rows() {
794 let heights = vec![0.0f64; 4];
795 assert!(validate_heightfield(&heights, 1, 4, 1.0, 1.0).is_err());
796 }
797
798 #[test]
799 fn test_validate_heightfield_bad_scale() {
800 let heights = vec![0.0f64; 4 * 4];
801 assert!(validate_heightfield(&heights, 4, 4, 0.0, 1.0).is_err());
802 }
803
804 #[test]
805 fn test_validate_heightfield_len_mismatch() {
806 let heights = vec![0.0f64; 10]; assert!(validate_heightfield(&heights, 4, 4, 1.0, 1.0).is_err());
808 }
809
810 #[test]
811 fn test_validate_heightfield_nan_height() {
812 let mut heights = vec![0.0f64; 4 * 4];
813 heights[5] = f64::NAN;
814 assert!(validate_heightfield(&heights, 4, 4, 1.0, 1.0).is_err());
815 }
816
817 #[test]
820 fn test_validate_ray_dir_ok() {
821 assert!(validate_ray_dir([0.0, -1.0, 0.0]).is_ok());
822 }
823
824 #[test]
825 fn test_validate_ray_dir_zero_fails() {
826 assert!(validate_ray_dir([0.0, 0.0, 0.0]).is_err());
827 }
828
829 #[test]
830 fn test_validate_ray_dir_nan_fails() {
831 assert!(validate_ray_dir([f64::NAN, 0.0, 0.0]).is_err());
832 }
833
834 #[test]
837 fn test_validate_point_cloud_ok() {
838 let pts = [[0.0, 0.0, 0.0], [1.0, 2.0, 3.0]];
839 assert!(validate_point_cloud(&pts).is_ok());
840 }
841
842 #[test]
843 fn test_validate_point_cloud_empty_fails() {
844 assert!(validate_point_cloud(&[]).is_err());
845 }
846
847 #[test]
848 fn test_validate_point_cloud_nan_fails() {
849 let pts = [[f64::NAN, 0.0, 0.0]];
850 assert!(validate_point_cloud(&pts).is_err());
851 }
852
853 #[test]
856 fn test_with_context_ok_passes_through() {
857 let r: std::result::Result<i32, &str> = Ok(42);
858 let r2: Result<i32> = r.with_context("test");
859 assert_eq!(r2.unwrap(), 42);
860 }
861
862 #[test]
863 fn test_with_context_wraps_error() {
864 let r: std::result::Result<i32, &str> = Err("original error");
865 let r2: Result<i32> = r.with_context("loading mesh");
866 let e = r2.unwrap_err();
867 let s = e.to_string();
868 assert!(s.contains("loading mesh"), "context missing: {s}");
869 assert!(s.contains("original error"), "original missing: {s}");
870 }
871
872 #[test]
875 fn test_convergence_tracker_converges() {
876 let mut tracker = ConvergenceTracker::new("test_op", 100, 1e-6);
877 let result = tracker.update(1e-8);
879 assert!(result.is_ok());
880 assert!(result.unwrap(), "should report converged");
881 assert_eq!(tracker.iterations(), 1);
882 }
883
884 #[test]
885 fn test_convergence_tracker_not_yet_converged() {
886 let mut tracker = ConvergenceTracker::new("test_op", 100, 1e-6);
887 let result = tracker.update(0.5);
888 assert!(result.is_ok());
889 assert!(!result.unwrap(), "should not report converged yet");
890 }
891
892 #[test]
893 fn test_convergence_tracker_failure() {
894 let mut tracker = ConvergenceTracker::new("mesh_smooth", 3, 1e-10);
895 let _ = tracker.update(1.0);
896 let _ = tracker.update(0.5);
897 let r = tracker.update(0.3); assert!(r.is_err());
899 assert!(matches!(r.unwrap_err(), Error::ConvergenceFailure { .. }));
900 }
901
902 #[test]
903 fn test_convergence_tracker_residual_tracked() {
904 let mut tracker = ConvergenceTracker::new("op", 100, 1e-6);
905 let _ = tracker.update(0.7);
906 assert!((tracker.residual() - 0.7).abs() < 1e-12);
907 }
908
909 #[test]
910 fn test_convergence_tracker_reset() {
911 let mut tracker = ConvergenceTracker::new("op", 100, 1e-6);
912 let _ = tracker.update(0.5);
913 tracker.reset();
914 assert_eq!(tracker.iterations(), 0);
915 assert!(tracker.residual().is_infinite());
916 }
917
918 #[test]
919 fn test_convergence_tracker_iterations_counted() {
920 let mut tracker = ConvergenceTracker::new("op", 100, 1e-6);
921 for _ in 0..5 {
922 let _ = tracker.update(1.0);
923 }
924 assert_eq!(tracker.iterations(), 5);
925 }
926}