1use crate::ipopt_cq::IpoptCqHandle;
25use crate::ipopt_data::IpoptDataHandle;
26use pounce_common::diagnostics::{DiagCategory, DiagnosticsState, IterateVariant};
27use pounce_common::types::Number;
28use pounce_linalg::dense_vector::DenseVector;
29use pounce_linalg::Vector;
30use std::fmt::Write as _;
31
32pub(crate) fn emit_record(diag: &DiagnosticsState, data: &IpoptDataHandle, cq: &IpoptCqHandle) {
41 if !diag.want(DiagCategory::Iterate) {
42 return;
43 }
44 let variant = diag.config.iterate_variant;
45 let json = match build_record(diag, data, cq, variant) {
46 Some(s) => s,
47 None => return,
48 };
49 if let Err(e) = diag.append_iterate_line(&json) {
50 tracing::warn!(target: "pounce::diagnostics",
51 "iterate_dump: failed to append iterate row to iterates.jsonl: {} — continuing",
52 e
53 );
54 }
55}
56
57fn build_record(
61 diag: &DiagnosticsState,
62 data: &IpoptDataHandle,
63 cq: &IpoptCqHandle,
64 variant: IterateVariant,
65) -> Option<String> {
66 let iter = diag.current_iter();
67 let restoration = diag.in_restoration();
68 let (alpha_pr, alpha_du, tag, curr_x) = {
69 let d = data.borrow();
70 let curr = d.curr.as_ref()?.clone();
71 (
72 d.info_alpha_primal,
73 d.info_alpha_dual,
74 d.info_alpha_primal_char,
75 curr.x.clone(),
76 )
77 };
78
79 let mu = data.borrow().curr_mu.max(1e-12);
85 let (active_mask_b64, slack_norm_inf, slack_norm_2, slack_vec) =
86 constraint_active_mask_and_slack(cq, mu);
87
88 let x_norm = vec_inf_norm(&*curr_x);
89
90 let mut out = String::with_capacity(256);
91 out.push('{');
92 write!(out, "\"iter\":{}", iter).ok()?;
93 write!(out, ",\"alpha_pr\":{}", json_f64(alpha_pr)).ok()?;
94 write!(out, ",\"alpha_du\":{}", json_f64(alpha_du)).ok()?;
95 write!(out, ",\"tag\":\"{}\"", escape_tag(tag)).ok()?;
96 write!(
97 out,
98 ",\"restoration\":{}",
99 if restoration { "true" } else { "false" }
100 )
101 .ok()?;
102 write!(out, ",\"active_mask\":\"{}\"", active_mask_b64).ok()?;
103 write!(out, ",\"x_norm\":{}", json_f64(x_norm)).ok()?;
104 write!(out, ",\"slack_norm_inf\":{}", json_f64(slack_norm_inf)).ok()?;
105 write!(out, ",\"slack_norm_2\":{}", json_f64(slack_norm_2)).ok()?;
106
107 if matches!(variant, IterateVariant::Full) {
108 out.push_str(",\"x\":");
110 push_vec_json(&mut out, &*curr_x);
111 out.push_str(",\"slack\":");
114 push_slice_json(&mut out, &slack_vec);
115 }
116
117 out.push('}');
118 Some(out)
119}
120
121fn constraint_active_mask_and_slack(
134 cq: &IpoptCqHandle,
135 tol: Number,
136) -> (String, Number, Number, Vec<Number>) {
137 let c = cq.borrow().curr_c();
138 let d_minus_s = cq.borrow().curr_d_minus_s();
139 let n_eq = c.dim() as usize;
140 let n_ineq = d_minus_s.dim() as usize;
141 let m = n_eq + n_ineq;
142
143 let mut slack = Vec::with_capacity(m);
144 extend_vec_values(&mut slack, &*c);
145 extend_vec_values(&mut slack, &*d_minus_s);
146 debug_assert_eq!(slack.len(), m);
147
148 let inf = slack.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
149 let two = slack.iter().fold(0.0_f64, |acc, &v| acc + v * v).sqrt();
150
151 let nbytes = (m + 7) / 8;
155 let mut bits = vec![0u8; nbytes];
156 for (i, &s) in slack.iter().enumerate() {
157 if s.abs() <= tol {
158 bits[i / 8] |= 1 << (i % 8);
159 }
160 }
161 let b64 = base64_encode(&bits);
162 (b64, inf, two, slack)
163}
164
165fn extend_vec_values(out: &mut Vec<Number>, v: &dyn Vector) {
166 if v.dim() == 0 {
167 return;
168 }
169 if let Some(dense) = v.as_any().downcast_ref::<DenseVector>() {
170 let xs = dense.expanded_values();
173 out.extend_from_slice(&xs);
174 return;
175 }
176 let mut tmp = v.make_new();
180 tmp.copy(v);
181 if let Some(dense) = tmp.as_any().downcast_ref::<DenseVector>() {
182 out.extend_from_slice(&dense.expanded_values());
183 return;
184 }
185 out.resize(out.len() + v.dim() as usize, 0.0);
187}
188
189fn vec_inf_norm(v: &dyn Vector) -> Number {
190 if v.dim() == 0 {
191 return 0.0;
192 }
193 v.amax()
194}
195
196fn push_vec_json(out: &mut String, v: &dyn Vector) {
197 let mut buf = Vec::with_capacity(v.dim() as usize);
198 extend_vec_values(&mut buf, v);
199 push_slice_json(out, &buf);
200}
201
202fn push_slice_json(out: &mut String, xs: &[Number]) {
203 out.push('[');
204 let mut first = true;
205 for x in xs {
206 if !first {
207 out.push(',');
208 }
209 first = false;
210 out.push_str(&json_f64(*x));
211 }
212 out.push(']');
213}
214
215fn json_f64(x: Number) -> String {
221 if x.is_finite() {
222 format!("{:?}", x)
225 } else {
226 "null".to_string()
227 }
228}
229
230fn escape_tag(c: char) -> String {
231 match c {
232 ' ' | '\0' => String::new(),
233 '"' => "\\\"".to_string(),
234 '\\' => "\\\\".to_string(),
235 c if (c as u32) < 0x20 => format!("\\u{:04x}", c as u32),
236 c => c.to_string(),
237 }
238}
239
240fn base64_encode(bytes: &[u8]) -> String {
243 const ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
244 let mut out = String::with_capacity(((bytes.len() + 2) / 3) * 4);
245 let mut i = 0;
246 while i + 3 <= bytes.len() {
247 let b0 = bytes[i] as u32;
248 let b1 = bytes[i + 1] as u32;
249 let b2 = bytes[i + 2] as u32;
250 let n = (b0 << 16) | (b1 << 8) | b2;
251 out.push(ALPHA[((n >> 18) & 0x3f) as usize] as char);
252 out.push(ALPHA[((n >> 12) & 0x3f) as usize] as char);
253 out.push(ALPHA[((n >> 6) & 0x3f) as usize] as char);
254 out.push(ALPHA[(n & 0x3f) as usize] as char);
255 i += 3;
256 }
257 let rem = bytes.len() - i;
258 if rem == 1 {
259 let b0 = bytes[i] as u32;
260 let n = b0 << 16;
261 out.push(ALPHA[((n >> 18) & 0x3f) as usize] as char);
262 out.push(ALPHA[((n >> 12) & 0x3f) as usize] as char);
263 out.push('=');
264 out.push('=');
265 } else if rem == 2 {
266 let b0 = bytes[i] as u32;
267 let b1 = bytes[i + 1] as u32;
268 let n = (b0 << 16) | (b1 << 8);
269 out.push(ALPHA[((n >> 18) & 0x3f) as usize] as char);
270 out.push(ALPHA[((n >> 12) & 0x3f) as usize] as char);
271 out.push(ALPHA[((n >> 6) & 0x3f) as usize] as char);
272 out.push('=');
273 }
274 out
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn base64_round_trip_against_known_vectors() {
283 assert_eq!(base64_encode(&[]), "");
284 assert_eq!(base64_encode(&[0x66]), "Zg==");
285 assert_eq!(base64_encode(&[0x66, 0x6f]), "Zm8=");
286 assert_eq!(base64_encode(&[0x66, 0x6f, 0x6f]), "Zm9v");
287 assert_eq!(base64_encode(&[0, 0, 0, 0]), "AAAAAA==");
291 assert_eq!(base64_encode(&[0xff, 0xff, 0x03, 0x00]), "//8DAA==");
292 }
293
294 #[test]
295 fn json_f64_finite_handles_special_cases() {
296 assert_eq!(json_f64(1.0), "1.0");
297 assert_eq!(json_f64(0.0), "0.0");
298 assert_eq!(json_f64(-0.5), "-0.5");
299 assert_eq!(json_f64(f64::NAN), "null");
300 assert_eq!(json_f64(f64::INFINITY), "null");
301 assert_eq!(json_f64(f64::NEG_INFINITY), "null");
302 }
303
304 #[test]
305 fn escape_tag_handles_blank_and_special_chars() {
306 assert_eq!(escape_tag(' '), "");
307 assert_eq!(escape_tag('\0'), "");
308 assert_eq!(escape_tag('f'), "f");
309 assert_eq!(escape_tag('"'), "\\\"");
310 assert_eq!(escape_tag('\\'), "\\\\");
311 }
312}