1use std::fmt;
10
11use pounce_common::types::{Index, Number};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum AuxiliaryRejectionReason {
20 BlockTooLarge,
23 CouplingDisallowed,
26 BlockSolveDiverged,
28 ResidualCheckFailed,
31 OutOfBounds,
35}
36
37#[derive(Debug, Clone, Default, Copy)]
39pub struct StageTimings {
40 pub incidence_ms: u128,
41 pub matching_ms: u128,
42 pub dm_ms: u128,
43 pub components_ms: u128,
44 pub btf_ms: u128,
45 pub block_solve_ms: u128,
46 pub residual_check_ms: u128,
47}
48
49#[derive(Debug, Clone, Default, Copy)]
52pub struct ClassCounts {
53 pub pure_equality: Index,
54 pub objective_coupled: Index,
55 pub inequality_coupled: Index,
56 pub objective_and_inequality_coupled: Index,
57}
58
59#[derive(Debug, Clone, Default)]
73pub struct AuxiliaryPreprocessingDiagnostics {
74 pub blocks_eliminated: Index,
76 pub candidate_blocks: Index,
78 pub vars_eliminated: Index,
80 pub rows_eliminated: Index,
82 pub total_time_ms: u128,
84 pub stage_time_ms: StageTimings,
86 pub class_counts: ClassCounts,
88 pub max_block_residual: Number,
91 pub max_accepted_block_dim: Index,
93 pub rejection_reasons: Vec<AuxiliaryRejectionReason>,
95 pub trivially_fixed_vars: Index,
99 pub trivially_free_rows: Index,
102 pub trivially_slack_rows: Index,
106 pub inequality_coupled_accepted_via_projection: Index,
111}
112
113impl fmt::Display for AuxiliaryPreprocessingDiagnostics {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 writeln!(
116 f,
117 "auxiliary-preprocessing: {} of {} candidate block(s) eliminated, \
118 fixing {} variable(s) and dropping {} row(s) in {} ms",
119 self.blocks_eliminated,
120 self.candidate_blocks,
121 self.vars_eliminated,
122 self.rows_eliminated,
123 self.total_time_ms,
124 )?;
125 if self.blocks_eliminated > 0 {
126 writeln!(
127 f,
128 " max block dim: {}, max residual: {:.3e}",
129 self.max_accepted_block_dim, self.max_block_residual
130 )?;
131 }
132 let cc = &self.class_counts;
133 if cc.pure_equality
134 + cc.objective_coupled
135 + cc.inequality_coupled
136 + cc.objective_and_inequality_coupled
137 > 0
138 {
139 writeln!(
140 f,
141 " candidates by coupling class: pure={}, obj={}, ineq={}, both={}",
142 cc.pure_equality,
143 cc.objective_coupled,
144 cc.inequality_coupled,
145 cc.objective_and_inequality_coupled,
146 )?;
147 }
148 if !self.rejection_reasons.is_empty() {
149 writeln!(f, " rejections ({}):", self.rejection_reasons.len())?;
150 let mut by_reason: std::collections::BTreeMap<&str, usize> =
152 std::collections::BTreeMap::new();
153 for r in &self.rejection_reasons {
154 let key = match r {
155 AuxiliaryRejectionReason::BlockTooLarge => "block-too-large",
156 AuxiliaryRejectionReason::CouplingDisallowed => "coupling-disallowed",
157 AuxiliaryRejectionReason::BlockSolveDiverged => "block-solve-diverged",
158 AuxiliaryRejectionReason::ResidualCheckFailed => "residual-check-failed",
159 AuxiliaryRejectionReason::OutOfBounds => "out-of-bounds",
160 };
161 *by_reason.entry(key).or_insert(0) += 1;
162 }
163 for (reason, count) in by_reason {
164 writeln!(f, " {reason}: {count}")?;
165 }
166 }
167 Ok(())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn diagnostics_default_is_empty() {
177 let d = AuxiliaryPreprocessingDiagnostics::default();
178 assert_eq!(d.blocks_eliminated, 0);
179 assert_eq!(d.candidate_blocks, 0);
180 assert_eq!(d.vars_eliminated, 0);
181 assert_eq!(d.rows_eliminated, 0);
182 assert_eq!(d.total_time_ms, 0);
183 assert_eq!(d.max_block_residual, 0.0);
184 assert_eq!(d.max_accepted_block_dim, 0);
185 assert_eq!(d.stage_time_ms.matching_ms, 0);
186 assert_eq!(d.class_counts.pure_equality, 0);
187 assert!(d.rejection_reasons.is_empty());
188 }
189
190 #[test]
191 fn display_empty_diagnostics() {
192 let d = AuxiliaryPreprocessingDiagnostics::default();
193 let s = format!("{d}");
194 assert!(s.contains("0 of 0 candidate block(s) eliminated"));
195 assert!(!s.contains("rejections"));
197 assert!(!s.contains("coupling"));
198 }
199
200 #[test]
201 fn display_populated_diagnostics() {
202 let mut d = AuxiliaryPreprocessingDiagnostics {
203 blocks_eliminated: 2,
204 candidate_blocks: 3,
205 vars_eliminated: 4,
206 rows_eliminated: 4,
207 total_time_ms: 12,
208 max_block_residual: 1.5e-13,
209 max_accepted_block_dim: 2,
210 ..Default::default()
211 };
212 d.class_counts.pure_equality = 2;
213 d.class_counts.inequality_coupled = 1;
214 d.rejection_reasons
215 .push(AuxiliaryRejectionReason::CouplingDisallowed);
216 let s = format!("{d}");
217 assert!(s.contains("2 of 3 candidate block(s) eliminated"));
218 assert!(s.contains("max block dim: 2"));
219 assert!(s.contains("max residual: 1.500e-13"));
220 assert!(s.contains("candidates by coupling class"));
221 assert!(s.contains("pure=2"));
222 assert!(s.contains("ineq=1"));
223 assert!(s.contains("coupling-disallowed: 1"));
224 }
225}