use crate::ipopt_cq::IpoptCqHandle;
use crate::ipopt_data::IpoptDataHandle;
use pounce_common::types::Number;
use pounce_linalg::dense_vector::DenseVector;
use pounce_linalg::Vector;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
pub const MAGIC: &[u8; 8] = b"POUNCEIT";
pub const FORMAT_VERSION: u32 = 1;
pub const ENV_DUMP_PATH: &str = "IPOPT_ITER_DUMP_PATH";
pub const ENV_DUMP_NAME: &str = "IPOPT_ITER_DUMP_NAME";
pub(crate) struct IterDumper {
writer: BufWriter<File>,
header_written: bool,
name: String,
}
impl IterDumper {
pub(crate) fn from_env() -> Option<Self> {
let path = std::env::var(ENV_DUMP_PATH).ok()?;
if path.is_empty() {
return None;
}
let pb = PathBuf::from(&path);
let file = match File::create(&pb) {
Ok(f) => f,
Err(e) => {
eprintln!(
"iter_dump: failed to open `{}` for writing: {} — dumping disabled",
path, e
);
return None;
}
};
let name = std::env::var(ENV_DUMP_NAME).unwrap_or_default();
Some(Self {
writer: BufWriter::new(file),
header_written: false,
name,
})
}
fn write_u32(&mut self, v: u32) -> std::io::Result<()> {
self.writer.write_all(&v.to_le_bytes())
}
fn write_f64(&mut self, v: Number) -> std::io::Result<()> {
self.writer.write_all(&v.to_le_bytes())
}
fn write_vec(&mut self, v: &dyn Vector) -> std::io::Result<()> {
let len = v.dim() as u32;
self.write_u32(len)?;
if len == 0 {
return Ok(());
}
if let Some(dense) = v.as_any().downcast_ref::<DenseVector>() {
if dense.is_homogeneous() {
let expanded = dense.expanded_values();
for x in &expanded {
self.writer.write_all(&x.to_le_bytes())?;
}
return Ok(());
}
for x in dense.values() {
self.writer.write_all(&x.to_le_bytes())?;
}
return Ok(());
}
let mut tmp = v.make_new();
tmp.copy(v);
if let Some(dense) = tmp.as_any().downcast_ref::<DenseVector>() {
for x in dense.expanded_values().iter() {
self.writer.write_all(&x.to_le_bytes())?;
}
return Ok(());
}
for _ in 0..len {
self.writer.write_all(&0.0_f64.to_le_bytes())?;
}
Ok(())
}
fn write_header(&mut self, n: u32, m: u32) -> std::io::Result<()> {
debug_assert!(!self.header_written);
self.writer.write_all(MAGIC)?;
self.write_u32(FORMAT_VERSION)?;
self.write_u32(n)?;
self.write_u32(m)?;
self.write_u32(0)?;
self.write_u32(0)?;
let name_len = self.name.len();
self.write_u32(name_len as u32)?;
let name_bytes = self.name.clone();
self.writer.write_all(name_bytes.as_bytes())?;
self.header_written = true;
Ok(())
}
pub(crate) fn write_record(&mut self, data: &IpoptDataHandle, cq: &IpoptCqHandle) {
if let Err(e) = self.write_record_inner(data, cq) {
eprintln!(
"iter_dump: failed to write iteration record: {} — dumping aborted",
e
);
}
}
fn write_record_inner(
&mut self,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
) -> std::io::Result<()> {
let (iter, mu, tau, alpha_pr, alpha_du, delta_x, delta_s, delta_c, delta_d, curr_opt) = {
let d = data.borrow();
(
d.iter_count as u32,
d.curr_mu,
d.curr_tau,
d.info_alpha_primal,
d.info_alpha_dual,
d.info_regu_x,
d.perturbations.delta_s,
d.perturbations.delta_c,
d.perturbations.delta_d,
d.curr.clone(),
)
};
let Some(curr) = curr_opt else {
return Ok(());
};
let inf_pr = cq.borrow().curr_primal_infeasibility_max();
let inf_du = cq.borrow().curr_dual_infeasibility_max();
let constr_viol = cq.borrow().curr_constraint_violation();
let dual_inf = inf_du; let complementarity = {
let cq_ref = cq.borrow();
cq_ref
.curr_compl_x_l()
.amax()
.max(cq_ref.curr_compl_x_u().amax())
.max(cq_ref.curr_compl_s_l().amax())
.max(cq_ref.curr_compl_s_u().amax())
};
let f_val = cq.borrow().curr_f();
if !self.header_written {
let n = curr.x.dim() as u32;
let m = (curr.y_c.dim() + curr.y_d.dim()) as u32;
self.write_header(n, m)?;
}
self.write_u32(iter)?;
self.write_u32(0)?; self.write_f64(mu)?;
self.write_f64(tau)?;
self.write_f64(alpha_pr)?;
self.write_f64(alpha_du)?;
self.write_f64(delta_x)?;
self.write_f64(delta_s)?;
self.write_f64(delta_c)?;
self.write_f64(delta_d)?;
self.write_f64(inf_pr)?;
self.write_f64(inf_du)?;
self.write_f64(constr_viol)?;
self.write_f64(dual_inf)?;
self.write_f64(complementarity)?;
self.write_f64(f_val)?;
self.write_vec(&*curr.x)?;
self.write_vec(&*curr.s)?;
self.write_vec(&*curr.y_c)?;
self.write_vec(&*curr.y_d)?;
self.write_vec(&*curr.z_l)?;
self.write_vec(&*curr.z_u)?;
self.write_vec(&*curr.v_l)?;
self.write_vec(&*curr.v_u)?;
self.write_u32(0)?;
Ok(())
}
}
impl Drop for IterDumper {
fn drop(&mut self) {
if let Err(e) = self.writer.flush() {
eprintln!("iter_dump: failed to flush trace file on drop: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ipopt_data::IpoptData;
use crate::iterates_vector::IteratesVector;
use pounce_linalg::dense_vector::DenseVectorSpace;
use std::cell::RefCell;
use std::rc::Rc;
fn dense(n: i32, vals: Option<&[Number]>) -> Rc<dyn Vector> {
let space = DenseVectorSpace::new(n);
let mut dv = space.make_new_dense();
if let Some(v) = vals {
dv.set_values(v);
}
Rc::new(dv) as Rc<dyn Vector>
}
#[test]
fn write_vec_emits_len_then_values_little_endian() {
let path =
std::env::temp_dir().join(format!("pounce_iter_dump_test_{}.bin", std::process::id()));
std::env::set_var(ENV_DUMP_PATH, &path);
let mut dumper = IterDumper::from_env().expect("dumper");
std::env::remove_var(ENV_DUMP_PATH);
let v = dense(3, Some(&[1.0_f64, 2.0, 3.0]));
dumper.write_vec(&*v).unwrap();
drop(dumper);
let bytes = std::fs::read(&path).unwrap();
assert_eq!(bytes.len(), 4 + 3 * 8);
assert_eq!(&bytes[0..4], &3u32.to_le_bytes());
assert_eq!(&bytes[4..12], &1.0_f64.to_le_bytes());
assert_eq!(&bytes[12..20], &2.0_f64.to_le_bytes());
assert_eq!(&bytes[20..28], &3.0_f64.to_le_bytes());
let _ = std::fs::remove_file(&path);
}
#[test]
fn from_env_returns_none_when_unset() {
std::env::remove_var(ENV_DUMP_PATH);
assert!(IterDumper::from_env().is_none());
}
#[test]
fn header_writes_magic_and_version() {
let path =
std::env::temp_dir().join(format!("pounce_iter_dump_hdr_{}.bin", std::process::id()));
std::env::set_var(ENV_DUMP_PATH, &path);
std::env::set_var(ENV_DUMP_NAME, "hs071");
let mut dumper = IterDumper::from_env().expect("dumper");
std::env::remove_var(ENV_DUMP_PATH);
std::env::remove_var(ENV_DUMP_NAME);
dumper.write_header(4, 2).unwrap();
drop(dumper);
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[0..8], MAGIC);
assert_eq!(&bytes[8..12], &1u32.to_le_bytes()); assert_eq!(&bytes[12..16], &4u32.to_le_bytes()); assert_eq!(&bytes[16..20], &2u32.to_le_bytes()); assert_eq!(&bytes[20..24], &0u32.to_le_bytes()); assert_eq!(&bytes[24..28], &0u32.to_le_bytes()); assert_eq!(&bytes[28..32], &5u32.to_le_bytes()); assert_eq!(&bytes[32..37], b"hs071");
assert_eq!(bytes.len(), 37);
let _ = std::fs::remove_file(&path);
}
#[test]
fn iv_dim_matches_record_layout_assumption() {
let iv = IteratesVector::new(
dense(4, Some(&[1.0, 2.0, 3.0, 4.0])),
dense(1, Some(&[0.5])),
dense(1, Some(&[1.0])),
dense(1, Some(&[1.0])),
dense(4, Some(&[1.0, 1.0, 1.0, 1.0])),
dense(4, Some(&[1.0, 1.0, 1.0, 1.0])),
dense(1, Some(&[1.0])),
dense(0, None),
);
assert_eq!(iv.x.dim(), 4);
assert_eq!(iv.v_u.dim(), 0);
let mut data = IpoptData::new();
data.set_curr(iv);
let _h: IpoptDataHandle = Rc::new(RefCell::new(data));
}
}