Skip to main content

gam_problem/
custom_family_error.rs

1//! Custom-family error type and its String conversions.
2
3use thiserror::Error;
4
5use crate::{IdentifiabilityAudit, MapUniquenessError};
6
7#[derive(Debug, Clone, Error)]
8pub enum CustomFamilyError {
9    #[error("custom-family invalid input in {context}: {reason}")]
10    InvalidInput {
11        context: &'static str,
12        reason: String,
13    },
14    #[error("custom-family optimization error in {context}: {reason}")]
15    Optimization {
16        context: &'static str,
17        reason: String,
18    },
19    #[error("{reason}")]
20    DimensionMismatch { reason: String },
21    #[error("{reason}")]
22    NumericalFailure { reason: String },
23    #[error("{reason}")]
24    ConstraintViolation { reason: String },
25    #[error("{reason}")]
26    UnsupportedConfiguration { reason: String },
27    #[error("{reason}")]
28    BasisDecompositionFailed { reason: String },
29    /// Pre-fit cross-block identifiability audit refused the fit. The
30    /// joint design across `ParameterBlockSpec`s carries a rank
31    /// deficiency that the post-`joint_null_rotation` absorption did
32    /// not resolve: two or more blocks contribute the same direction,
33    /// or a structural >2-way alias was detected without per-pair
34    /// attribution. The full `IdentifiabilityAudit` is held so
35    /// consumers (logs, structured-error sinks, the seed driver's
36    /// classifier) can extract the alias pairs and the summary string
37    /// without reparsing.
38    #[error("identifiability audit refused the fit: {}", audit.summary)]
39    IdentifiabilityFailure { audit: IdentifiabilityAudit },
40    /// MAP estimate uniqueness condition `ker(J^T W J) ∩ ker(S) = {0}` is
41    /// violated.  A null direction of `J^T W J` carries zero penalty
42    /// curvature, so the posterior is flat along that direction and the
43    /// MAP is non-unique.  The structured [`MapUniquenessError`] names the
44    /// dominant block so the caller can add the missing penalty or remove
45    /// the unpenalised direction.
46    #[error("MAP estimate non-unique: {}", error)]
47    MapUniquenessFailure { error: MapUniquenessError },
48}
49
50impl From<String> for CustomFamilyError {
51    fn from(value: String) -> Self {
52        Self::InvalidInput {
53            context: "custom-family string boundary",
54            reason: value,
55        }
56    }
57}
58
59impl From<CustomFamilyError> for String {
60    fn from(value: CustomFamilyError) -> Self {
61        value.to_string()
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn invalid_input_display_contains_context_and_reason() {
71        let err = CustomFamilyError::InvalidInput {
72            context: "my_context",
73            reason: "something broke".to_string(),
74        };
75        let msg = err.to_string();
76        assert!(msg.contains("my_context"), "message: {msg}");
77        assert!(msg.contains("something broke"), "message: {msg}");
78    }
79
80    #[test]
81    fn optimization_display_contains_context_and_reason() {
82        let err = CustomFamilyError::Optimization {
83            context: "outer_loop",
84            reason: "diverged".to_string(),
85        };
86        let msg = err.to_string();
87        assert!(msg.contains("outer_loop") && msg.contains("diverged"), "message: {msg}");
88    }
89
90    #[test]
91    fn dimension_mismatch_displays_reason() {
92        let err = CustomFamilyError::DimensionMismatch { reason: "3 vs 4".to_string() };
93        assert_eq!(err.to_string(), "3 vs 4");
94    }
95
96    #[test]
97    fn numerical_failure_displays_reason() {
98        let err = CustomFamilyError::NumericalFailure { reason: "NaN detected".to_string() };
99        assert_eq!(err.to_string(), "NaN detected");
100    }
101
102    #[test]
103    fn from_string_creates_invalid_input_with_boundary_context() {
104        let err = CustomFamilyError::from("string error".to_string());
105        assert!(matches!(err, CustomFamilyError::InvalidInput { .. }));
106        assert!(err.to_string().contains("string error"));
107    }
108
109    #[test]
110    fn from_custom_family_error_for_string_uses_display() {
111        let err = CustomFamilyError::NumericalFailure { reason: "singular".to_string() };
112        let s = String::from(err);
113        assert_eq!(s, "singular");
114    }
115}