""" Validation utilities for figure quality control """ import os import numpy as np from scipy import stats def validate_figure_output(fig_num, output_files, computed_metrics, validation_flags, config): """ Validate figure output against quality thresholds Args: fig_num: Figure number (1-15) output_files: List of generated file paths computed_metrics: Dictionary of computed metrics validation_flags: Dictionary of boolean validation flags config: Configuration dictionary with validation thresholds Returns: dict: Validation result with pass/fail status """ result = { "figure": f"Fig{fig_num:02d}", "output_files": output_files, "metrics": computed_metrics, "flags": validation_flags, "pass": True, "errors": [] } # Check file existence for filepath in output_files: if not os.path.exists(filepath): result["pass"] = False result["errors"].append(f"File not found: {filepath}") elif os.path.getsize(filepath) == 0: result["pass"] = False result["errors"].append(f"Empty file: {filepath}") # Figure-specific validation validation_thresholds = config.get('validation', {}) if fig_num == 3: # OCV fitting r2 = computed_metrics.get('r2', 0) r2_min = validation_thresholds.get('fig03_r2_min', 0.99) if r2 < r2_min: result["pass"] = False result["errors"].append(f"R² too low: {r2:.4f} < {r2_min}") elif fig_num == 7: # Baseline validation v_i_corr = computed_metrics.get('v_i_correlation', 0) corr_max = validation_thresholds.get('fig07_v_i_corr_max', -0.5) if v_i_corr > corr_max: result["pass"] = False result["errors"].append(f"V-I correlation not negative enough: {v_i_corr:.3f} > {corr_max}") elif fig_num == 9: # Scenario comparison if 'delta_tte_match' in validation_flags: if not validation_flags['delta_tte_match']: result["pass"] = False result["errors"].append("ΔTTE annotation mismatch") elif fig_num == 13: # Survival curve if 'survival_monotonic' in validation_flags: if not validation_flags['survival_monotonic']: result["pass"] = False result["errors"].append("Survival curve not monotonic") return result def compute_r2(y_true, y_pred): """Compute R-squared coefficient of determination""" ss_res = np.sum((y_true - y_pred) ** 2) ss_tot = np.sum((y_true - np.mean(y_true)) ** 2) return 1 - (ss_res / ss_tot) def check_monotonic_decreasing(arr): """Check if array is monotonically decreasing""" return np.all(np.diff(arr) <= 0)