1use crate::error::{StatsError, StatsResult};
7use std::fmt::Display;
8
9#[derive(Debug)]
11pub struct EnhancedError {
12 pub error: StatsError,
14 pub context: String,
16 pub suggestions: Vec<String>,
18 pub see_also: Vec<String>,
20}
21
22impl EnhancedError {
23 pub fn new(error: StatsError, context: impl Into<String>) -> Self {
25 Self {
26 error,
27 context: context.into(),
28 suggestions: Vec::new(),
29 see_also: Vec::new(),
30 }
31 }
32
33 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
35 self.suggestions.push(suggestion.into());
36 self
37 }
38
39 pub fn with_suggestions(
41 mut self,
42 suggestions: impl IntoIterator<Item = impl Into<String>>,
43 ) -> Self {
44 self.suggestions
45 .extend(suggestions.into_iter().map(|s| s.into()));
46 self
47 }
48
49 pub fn see_also(mut self, reference: impl Into<String>) -> Self {
51 self.see_also.push(reference.into());
52 self
53 }
54
55 pub fn into_error(self) -> StatsError {
57 let mut message = format!("{}\nContext: {}", self.error, self.context);
58
59 if !self.suggestions.is_empty() {
60 message.push_str("\n\nSuggestions:");
61 for (i, suggestion) in self.suggestions.iter().enumerate() {
62 message.push_str(&format!("\n {}. {}", i + 1, suggestion));
63 }
64 }
65
66 if !self.see_also.is_empty() {
67 message.push_str("\n\nSee also:");
68 for reference in &self.see_also {
69 message.push_str(&format!("\n - {}", reference));
70 }
71 }
72
73 StatsError::computation(message)
74 }
75}
76
77pub mod enhanced_validation {
79 use super::*;
80 use scirs2_core::numeric::Float;
81
82 pub fn validate_distribution_params<F: Float + Display>(
84 params: &[(F, &str, ParamType)],
85 distribution_name: &str,
86 ) -> StatsResult<()> {
87 for &(value, name, param_type) in params {
88 match param_type {
89 ParamType::Positive => {
90 if value <= F::zero() {
91 return Err(EnhancedError::new(
92 StatsError::domain(format!("{} must be positive, got {}", name, value)),
93 format!("Invalid {} parameter for {} distribution", name, distribution_name),
94 )
95 .with_suggestions(vec![
96 format!("Ensure {} > 0", name),
97 "Check your data preprocessing steps".to_string(),
98 "Consider using a different distribution if negative values are expected".to_string(),
99 ])
100 .see_also(format!("distributions::{}", distribution_name.to_lowercase()))
101 .into_error());
102 }
103 }
104 ParamType::NonNegative => {
105 if value < F::zero() {
106 return Err(EnhancedError::new(
107 StatsError::domain(format!(
108 "{} must be non-negative, got {}",
109 name, value
110 )),
111 format!(
112 "Invalid {} parameter for {} distribution",
113 name, distribution_name
114 ),
115 )
116 .with_suggestions(vec![
117 format!("Ensure {} >= 0", name),
118 "Check for data entry errors".to_string(),
119 ])
120 .into_error());
121 }
122 }
123 ParamType::Probability => {
124 if value < F::zero() || value > F::one() {
125 return Err(EnhancedError::new(
126 StatsError::domain(format!(
127 "{} must be in [0, 1], got {}",
128 name, value
129 )),
130 format!(
131 "Invalid probability parameter '{}' for {} distribution",
132 name, distribution_name
133 ),
134 )
135 .with_suggestions(vec![
136 "Ensure probability is between 0 and 1 (inclusive)",
137 "Check if you're using a proportion instead of a percentage",
138 "Verify your probability calculations",
139 ])
140 .into_error());
141 }
142 }
143 ParamType::Integer => {
144 if value.floor() != value {
145 return Err(EnhancedError::new(
146 StatsError::domain(format!(
147 "{} must be an integer, got {}",
148 name, value
149 )),
150 format!(
151 "Invalid {} parameter for {} distribution",
152 name, distribution_name
153 ),
154 )
155 .with_suggestions(vec![
156 "Round to the nearest integer if appropriate",
157 "Check if you're using the correct distribution",
158 ])
159 .into_error());
160 }
161 }
162 ParamType::PositiveInteger => {
163 if value.floor() != value || value <= F::zero() {
164 return Err(EnhancedError::new(
165 StatsError::domain(format!(
166 "{} must be a positive integer, got {}",
167 name, value
168 )),
169 format!(
170 "Invalid {} parameter for {} distribution",
171 name, distribution_name
172 ),
173 )
174 .with_suggestions(vec![
175 "Ensure the value is a positive whole number",
176 "Check your counting or indexing logic",
177 ])
178 .into_error());
179 }
180 }
181 }
182 }
183 Ok(())
184 }
185
186 #[derive(Debug, Copy, Clone)]
188 pub enum ParamType {
189 Positive,
190 NonNegative,
191 Probability,
192 Integer,
193 PositiveInteger,
194 }
195}
196
197pub mod numerical {
199 use super::*;
200
201 pub fn handle_overflow(operation: &str, values: &[impl Display]) -> StatsError {
203 let value_str = values
204 .iter()
205 .map(|v| v.to_string())
206 .collect::<Vec<_>>()
207 .join(", ");
208
209 EnhancedError::new(
210 StatsError::computation("Numerical overflow"),
211 format!(
212 "Overflow occurred during {} with values: [{}]",
213 operation, value_str
214 ),
215 )
216 .with_suggestions(vec![
217 "Scale your input data to smaller magnitudes",
218 "Use logarithmic transformations if appropriate",
219 "Consider using higher precision data types (f64 instead of f32)",
220 "Check for extreme outliers in your data",
221 ])
222 .see_also("numerical_stability")
223 .into_error()
224 }
225
226 pub fn handle_convergence_failure(
228 algorithm: &str,
229 iterations: usize,
230 tolerance: f64,
231 ) -> StatsError {
232 EnhancedError::new(
233 StatsError::computation("Algorithm failed to converge"),
234 format!(
235 "{} failed to converge after {} iterations (tolerance: {})",
236 algorithm, iterations, tolerance
237 ),
238 )
239 .with_suggestions(vec![
240 "Increase the maximum number of iterations",
241 "Relax the convergence tolerance",
242 "Check if your data is well-conditioned",
243 "Try different initial values",
244 "Consider using a different algorithm",
245 ])
246 .into_error()
247 }
248
249 pub fn handle_singular_matrix(context: &str) -> StatsError {
251 EnhancedError::new(
252 StatsError::computation("Matrix is singular or near-singular"),
253 format!("Singular matrix encountered in {}", context),
254 )
255 .with_suggestions(vec![
256 "Check for linear dependencies in your data",
257 "Remove collinear features",
258 "Add regularization to your model",
259 "Ensure you have more observations than features",
260 "Check for duplicate rows or columns",
261 ])
262 .see_also("linear_algebra")
263 .into_error()
264 }
265}
266
267pub mod data_validation {
269 use super::*;
270 use scirs2_core::numeric::Float;
271
272 pub fn validatedata_quality<T>(data: &[T], context: &str, allow_empty: bool) -> StatsResult<()>
274 where
275 T: Float + Display,
276 {
277 if data.is_empty() && !allow_empty {
278 return Err(EnhancedError::new(
279 StatsError::invalid_argument("Empty data array"),
280 format!("Empty input data for {}", context),
281 )
282 .with_suggestions(vec![
283 "Ensure your data loading process completed successfully",
284 "Check if filters removed all data points",
285 "Verify the data source is not _empty",
286 ])
287 .into_error());
288 }
289
290 let nan_count = data.iter().filter(|&&x| x.is_nan()).count();
292 let inf_count = data.iter().filter(|&&x| x.is_infinite()).count();
293
294 if nan_count > 0 {
295 return Err(EnhancedError::new(
296 StatsError::invalid_argument(format!("Found {} NaN values", nan_count)),
297 format!("Invalid data values in {}", context),
298 )
299 .with_suggestions(vec![
300 "Use dropna() or similar to remove NaN values",
301 "Check for division by zero in calculations",
302 "Verify data import didn't introduce NaN values",
303 "Consider imputation methods if appropriate",
304 ])
305 .see_also("data_preprocessing")
306 .into_error());
307 }
308
309 if inf_count > 0 {
310 return Err(EnhancedError::new(
311 StatsError::invalid_argument(format!("Found {} infinite values", inf_count)),
312 format!("Invalid data values in {}", context),
313 )
314 .with_suggestions(vec![
315 "Check for numerical overflow in calculations",
316 "Apply bounds checking before operations",
317 "Consider log transformations for large values",
318 "Remove or cap extreme outliers",
319 ])
320 .into_error());
321 }
322
323 Ok(())
324 }
325
326 pub fn validateshape_compatibility(
328 actualshape: &[usize],
329 expectedshape: &[Option<usize>],
330 array_name: &str,
331 ) -> StatsResult<()> {
332 if actualshape.len() != expectedshape.len() {
333 return Err(EnhancedError::new(
334 StatsError::dimension_mismatch(format!(
335 "Expected {}-dimensional array, got {}-dimensional",
336 expectedshape.len(),
337 actualshape.len()
338 )),
339 format!("Shape mismatch for {}", array_name),
340 )
341 .with_suggestions(vec![
342 "Reshape your array using reshape() or similar",
343 "Check if you're passing the correct variable",
344 "Verify data preprocessing steps maintained correct dimensions",
345 ])
346 .into_error());
347 }
348
349 for (i, (&actual, &expected)) in actualshape.iter().zip(expectedshape.iter()).enumerate() {
350 if let Some(expected_dim) = expected {
351 if actual != expected_dim {
352 return Err(EnhancedError::new(
353 StatsError::dimension_mismatch(format!(
354 "Dimension {} mismatch: expected {}, got {}",
355 i, expected_dim, actual
356 )),
357 format!("Invalid shape for {}", array_name),
358 )
359 .with_suggestions(vec![
360 format!("Ensure dimension {} has size {}", i, expected_dim),
361 "Check array slicing or indexing operations".to_string(),
362 ])
363 .into_error());
364 }
365 }
366 }
367
368 Ok(())
369 }
370}