use crate::ipopt_cq::IpoptCqHandle;
use crate::ipopt_data::IpoptDataHandle;
use pounce_common::diagnostics::{DiagCategory, DiagnosticsState, IterateVariant};
use pounce_common::types::Number;
use pounce_linalg::dense_vector::DenseVector;
use pounce_linalg::Vector;
use std::fmt::Write as _;
pub(crate) fn emit_record(diag: &DiagnosticsState, data: &IpoptDataHandle, cq: &IpoptCqHandle) {
if !diag.want(DiagCategory::Iterate) {
return;
}
let variant = diag.config.iterate_variant;
let json = match build_record(diag, data, cq, variant) {
Some(s) => s,
None => return,
};
if let Err(e) = diag.append_iterate_line(&json) {
tracing::warn!(target: "pounce::diagnostics",
"iterate_dump: failed to append iterate row to iterates.jsonl: {} — continuing",
e
);
}
}
fn build_record(
diag: &DiagnosticsState,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
variant: IterateVariant,
) -> Option<String> {
let iter = diag.current_iter();
let restoration = diag.in_restoration();
let (alpha_pr, alpha_du, tag, curr_x) = {
let d = data.borrow();
let curr = d.curr.as_ref()?.clone();
(
d.info_alpha_primal,
d.info_alpha_dual,
d.info_alpha_primal_char,
curr.x.clone(),
)
};
let mu = data.borrow().curr_mu.max(1e-12);
let (active_mask_b64, slack_norm_inf, slack_norm_2, slack_vec) =
constraint_active_mask_and_slack(cq, mu);
let x_norm = vec_inf_norm(&*curr_x);
let mut out = String::with_capacity(256);
out.push('{');
write!(out, "\"iter\":{}", iter).ok()?;
write!(out, ",\"alpha_pr\":{}", json_f64(alpha_pr)).ok()?;
write!(out, ",\"alpha_du\":{}", json_f64(alpha_du)).ok()?;
write!(out, ",\"tag\":\"{}\"", escape_tag(tag)).ok()?;
write!(
out,
",\"restoration\":{}",
if restoration { "true" } else { "false" }
)
.ok()?;
write!(out, ",\"active_mask\":\"{}\"", active_mask_b64).ok()?;
write!(out, ",\"x_norm\":{}", json_f64(x_norm)).ok()?;
write!(out, ",\"slack_norm_inf\":{}", json_f64(slack_norm_inf)).ok()?;
write!(out, ",\"slack_norm_2\":{}", json_f64(slack_norm_2)).ok()?;
if matches!(variant, IterateVariant::Full) {
out.push_str(",\"x\":");
push_vec_json(&mut out, &*curr_x);
out.push_str(",\"slack\":");
push_slice_json(&mut out, &slack_vec);
}
out.push('}');
Some(out)
}
fn constraint_active_mask_and_slack(
cq: &IpoptCqHandle,
tol: Number,
) -> (String, Number, Number, Vec<Number>) {
let c = cq.borrow().curr_c();
let d_minus_s = cq.borrow().curr_d_minus_s();
let n_eq = c.dim() as usize;
let n_ineq = d_minus_s.dim() as usize;
let m = n_eq + n_ineq;
let mut slack = Vec::with_capacity(m);
extend_vec_values(&mut slack, &*c);
extend_vec_values(&mut slack, &*d_minus_s);
debug_assert_eq!(slack.len(), m);
let inf = slack.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
let two = slack.iter().fold(0.0_f64, |acc, &v| acc + v * v).sqrt();
let nbytes = (m + 7) / 8;
let mut bits = vec![0u8; nbytes];
for (i, &s) in slack.iter().enumerate() {
if s.abs() <= tol {
bits[i / 8] |= 1 << (i % 8);
}
}
let b64 = base64_encode(&bits);
(b64, inf, two, slack)
}
fn extend_vec_values(out: &mut Vec<Number>, v: &dyn Vector) {
if v.dim() == 0 {
return;
}
if let Some(dense) = v.as_any().downcast_ref::<DenseVector>() {
let xs = dense.expanded_values();
out.extend_from_slice(&xs);
return;
}
let mut tmp = v.make_new();
tmp.copy(v);
if let Some(dense) = tmp.as_any().downcast_ref::<DenseVector>() {
out.extend_from_slice(&dense.expanded_values());
return;
}
out.resize(out.len() + v.dim() as usize, 0.0);
}
fn vec_inf_norm(v: &dyn Vector) -> Number {
if v.dim() == 0 {
return 0.0;
}
v.amax()
}
fn push_vec_json(out: &mut String, v: &dyn Vector) {
let mut buf = Vec::with_capacity(v.dim() as usize);
extend_vec_values(&mut buf, v);
push_slice_json(out, &buf);
}
fn push_slice_json(out: &mut String, xs: &[Number]) {
out.push('[');
let mut first = true;
for x in xs {
if !first {
out.push(',');
}
first = false;
out.push_str(&json_f64(*x));
}
out.push(']');
}
fn json_f64(x: Number) -> String {
if x.is_finite() {
format!("{:?}", x)
} else {
"null".to_string()
}
}
fn escape_tag(c: char) -> String {
match c {
' ' | '\0' => String::new(),
'"' => "\\\"".to_string(),
'\\' => "\\\\".to_string(),
c if (c as u32) < 0x20 => format!("\\u{:04x}", c as u32),
c => c.to_string(),
}
}
fn base64_encode(bytes: &[u8]) -> String {
const ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(((bytes.len() + 2) / 3) * 4);
let mut i = 0;
while i + 3 <= bytes.len() {
let b0 = bytes[i] as u32;
let b1 = bytes[i + 1] as u32;
let b2 = bytes[i + 2] as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHA[((n >> 18) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 12) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 6) & 0x3f) as usize] as char);
out.push(ALPHA[(n & 0x3f) as usize] as char);
i += 3;
}
let rem = bytes.len() - i;
if rem == 1 {
let b0 = bytes[i] as u32;
let n = b0 << 16;
out.push(ALPHA[((n >> 18) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 12) & 0x3f) as usize] as char);
out.push('=');
out.push('=');
} else if rem == 2 {
let b0 = bytes[i] as u32;
let b1 = bytes[i + 1] as u32;
let n = (b0 << 16) | (b1 << 8);
out.push(ALPHA[((n >> 18) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 12) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 6) & 0x3f) as usize] as char);
out.push('=');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_round_trip_against_known_vectors() {
assert_eq!(base64_encode(&[]), "");
assert_eq!(base64_encode(&[0x66]), "Zg==");
assert_eq!(base64_encode(&[0x66, 0x6f]), "Zm8=");
assert_eq!(base64_encode(&[0x66, 0x6f, 0x6f]), "Zm9v");
assert_eq!(base64_encode(&[0, 0, 0, 0]), "AAAAAA==");
assert_eq!(base64_encode(&[0xff, 0xff, 0x03, 0x00]), "//8DAA==");
}
#[test]
fn json_f64_finite_handles_special_cases() {
assert_eq!(json_f64(1.0), "1.0");
assert_eq!(json_f64(0.0), "0.0");
assert_eq!(json_f64(-0.5), "-0.5");
assert_eq!(json_f64(f64::NAN), "null");
assert_eq!(json_f64(f64::INFINITY), "null");
assert_eq!(json_f64(f64::NEG_INFINITY), "null");
}
#[test]
fn escape_tag_handles_blank_and_special_chars() {
assert_eq!(escape_tag(' '), "");
assert_eq!(escape_tag('\0'), "");
assert_eq!(escape_tag('f'), "f");
assert_eq!(escape_tag('"'), "\\\"");
assert_eq!(escape_tag('\\'), "\\\\");
}
}