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 NonBlockColumnFree,
42}
43
44#[derive(Debug, Clone, Default, Copy)]
46pub struct StageTimings {
47 pub incidence_ms: u128,
48 pub matching_ms: u128,
49 pub dm_ms: u128,
50 pub components_ms: u128,
51 pub btf_ms: u128,
52 pub block_solve_ms: u128,
53 pub residual_check_ms: u128,
54}
55
56#[derive(Debug, Clone, Default, Copy)]
59pub struct ClassCounts {
60 pub pure_equality: Index,
61 pub objective_coupled: Index,
62 pub inequality_coupled: Index,
63 pub objective_and_inequality_coupled: Index,
64}
65
66#[derive(Debug, Clone, Default)]
80pub struct AuxiliaryPreprocessingDiagnostics {
81 pub blocks_eliminated: Index,
83 pub candidate_blocks: Index,
85 pub vars_eliminated: Index,
87 pub rows_eliminated: Index,
89 pub total_time_ms: u128,
91 pub stage_time_ms: StageTimings,
93 pub class_counts: ClassCounts,
95 pub max_block_residual: Number,
98 pub max_accepted_block_dim: Index,
100 pub rejection_reasons: Vec<AuxiliaryRejectionReason>,
102 pub trivially_fixed_vars: Index,
106 pub trivially_free_rows: Index,
109 pub trivially_slack_rows: Index,
113 pub inequality_coupled_accepted_via_projection: Index,
118}
119
120impl fmt::Display for AuxiliaryPreprocessingDiagnostics {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 writeln!(
123 f,
124 "auxiliary-preprocessing: {} of {} candidate block(s) eliminated, \
125 fixing {} variable(s) and dropping {} row(s) in {} ms",
126 self.blocks_eliminated,
127 self.candidate_blocks,
128 self.vars_eliminated,
129 self.rows_eliminated,
130 self.total_time_ms,
131 )?;
132 if self.blocks_eliminated > 0 {
133 writeln!(
134 f,
135 " max block dim: {}, max residual: {:.3e}",
136 self.max_accepted_block_dim, self.max_block_residual
137 )?;
138 }
139 let cc = &self.class_counts;
140 if cc.pure_equality
141 + cc.objective_coupled
142 + cc.inequality_coupled
143 + cc.objective_and_inequality_coupled
144 > 0
145 {
146 writeln!(
147 f,
148 " candidates by coupling class: pure={}, obj={}, ineq={}, both={}",
149 cc.pure_equality,
150 cc.objective_coupled,
151 cc.inequality_coupled,
152 cc.objective_and_inequality_coupled,
153 )?;
154 }
155 if !self.rejection_reasons.is_empty() {
156 writeln!(f, " rejections ({}):", self.rejection_reasons.len())?;
157 let mut by_reason: std::collections::BTreeMap<&str, usize> =
159 std::collections::BTreeMap::new();
160 for r in &self.rejection_reasons {
161 let key = match r {
162 AuxiliaryRejectionReason::BlockTooLarge => "block-too-large",
163 AuxiliaryRejectionReason::CouplingDisallowed => "coupling-disallowed",
164 AuxiliaryRejectionReason::BlockSolveDiverged => "block-solve-diverged",
165 AuxiliaryRejectionReason::ResidualCheckFailed => "residual-check-failed",
166 AuxiliaryRejectionReason::OutOfBounds => "out-of-bounds",
167 AuxiliaryRejectionReason::NonBlockColumnFree => "non-block-column-free",
168 };
169 *by_reason.entry(key).or_insert(0) += 1;
170 }
171 for (reason, count) in by_reason {
172 writeln!(f, " {reason}: {count}")?;
173 }
174 }
175 Ok(())
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn diagnostics_default_is_empty() {
185 let d = AuxiliaryPreprocessingDiagnostics::default();
186 assert_eq!(d.blocks_eliminated, 0);
187 assert_eq!(d.candidate_blocks, 0);
188 assert_eq!(d.vars_eliminated, 0);
189 assert_eq!(d.rows_eliminated, 0);
190 assert_eq!(d.total_time_ms, 0);
191 assert_eq!(d.max_block_residual, 0.0);
192 assert_eq!(d.max_accepted_block_dim, 0);
193 assert_eq!(d.stage_time_ms.matching_ms, 0);
194 assert_eq!(d.class_counts.pure_equality, 0);
195 assert!(d.rejection_reasons.is_empty());
196 }
197
198 #[test]
199 fn display_empty_diagnostics() {
200 let d = AuxiliaryPreprocessingDiagnostics::default();
201 let s = format!("{d}");
202 assert!(s.contains("0 of 0 candidate block(s) eliminated"));
203 assert!(!s.contains("rejections"));
205 assert!(!s.contains("coupling"));
206 }
207
208 #[test]
209 fn display_populated_diagnostics() {
210 let mut d = AuxiliaryPreprocessingDiagnostics {
211 blocks_eliminated: 2,
212 candidate_blocks: 3,
213 vars_eliminated: 4,
214 rows_eliminated: 4,
215 total_time_ms: 12,
216 max_block_residual: 1.5e-13,
217 max_accepted_block_dim: 2,
218 ..Default::default()
219 };
220 d.class_counts.pure_equality = 2;
221 d.class_counts.inequality_coupled = 1;
222 d.rejection_reasons
223 .push(AuxiliaryRejectionReason::CouplingDisallowed);
224 let s = format!("{d}");
225 assert!(s.contains("2 of 3 candidate block(s) eliminated"));
226 assert!(s.contains("max block dim: 2"));
227 assert!(s.contains("max residual: 1.500e-13"));
228 assert!(s.contains("candidates by coupling class"));
229 assert!(s.contains("pure=2"));
230 assert!(s.contains("ineq=1"));
231 assert!(s.contains("coupling-disallowed: 1"));
232 }
233}