1use crate::ipopt_cq::IpoptCqHandle;
24use crate::ipopt_data::IpoptDataHandle;
25use pounce_common::types::Number;
26use pounce_linalg::dense_vector::DenseVector;
27use pounce_linalg::Vector;
28use std::fs::File;
29use std::io::{BufWriter, Write};
30use std::path::PathBuf;
31
32pub const MAGIC: &[u8; 8] = b"POUNCEIT";
35pub const FORMAT_VERSION: u32 = 1;
37
38pub const ENV_DUMP_PATH: &str = "IPOPT_ITER_DUMP_PATH";
41pub const ENV_DUMP_NAME: &str = "IPOPT_ITER_DUMP_NAME";
44
45pub(crate) struct IterDumper {
49 writer: BufWriter<File>,
50 header_written: bool,
54 name: String,
55}
56
57impl IterDumper {
58 pub(crate) fn from_env() -> Option<Self> {
63 let path = std::env::var(ENV_DUMP_PATH).ok()?;
64 if path.is_empty() {
65 return None;
66 }
67 let pb = PathBuf::from(&path);
68 let file = match File::create(&pb) {
69 Ok(f) => f,
70 Err(e) => {
71 eprintln!(
72 "iter_dump: failed to open `{}` for writing: {} — dumping disabled",
73 path, e
74 );
75 return None;
76 }
77 };
78 let name = std::env::var(ENV_DUMP_NAME).unwrap_or_default();
79 Some(Self {
80 writer: BufWriter::new(file),
81 header_written: false,
82 name,
83 })
84 }
85
86 fn write_u32(&mut self, v: u32) -> std::io::Result<()> {
87 self.writer.write_all(&v.to_le_bytes())
88 }
89
90 fn write_f64(&mut self, v: Number) -> std::io::Result<()> {
91 self.writer.write_all(&v.to_le_bytes())
92 }
93
94 fn write_vec(&mut self, v: &dyn Vector) -> std::io::Result<()> {
95 let len = v.dim() as u32;
96 self.write_u32(len)?;
97 if len == 0 {
98 return Ok(());
99 }
100 if let Some(dense) = v.as_any().downcast_ref::<DenseVector>() {
104 if dense.is_homogeneous() {
105 let expanded = dense.expanded_values();
106 for x in &expanded {
107 self.writer.write_all(&x.to_le_bytes())?;
108 }
109 return Ok(());
110 }
111 for x in dense.values() {
113 self.writer.write_all(&x.to_le_bytes())?;
114 }
115 return Ok(());
116 }
117 let mut tmp = v.make_new();
121 tmp.copy(v);
122 if let Some(dense) = tmp.as_any().downcast_ref::<DenseVector>() {
123 for x in dense.expanded_values().iter() {
124 self.writer.write_all(&x.to_le_bytes())?;
125 }
126 return Ok(());
127 }
128 for _ in 0..len {
131 self.writer.write_all(&0.0_f64.to_le_bytes())?;
132 }
133 Ok(())
134 }
135
136 fn write_header(&mut self, n: u32, m: u32) -> std::io::Result<()> {
139 debug_assert!(!self.header_written);
140 self.writer.write_all(MAGIC)?;
141 self.write_u32(FORMAT_VERSION)?;
142 self.write_u32(n)?;
143 self.write_u32(m)?;
144 self.write_u32(0)?;
147 self.write_u32(0)?;
148 let name_len = self.name.len();
149 self.write_u32(name_len as u32)?;
150 let name_bytes = self.name.clone();
151 self.writer.write_all(name_bytes.as_bytes())?;
152 self.header_written = true;
153 Ok(())
154 }
155
156 pub(crate) fn write_record(&mut self, data: &IpoptDataHandle, cq: &IpoptCqHandle) {
160 if let Err(e) = self.write_record_inner(data, cq) {
161 eprintln!(
162 "iter_dump: failed to write iteration record: {} — dumping aborted",
163 e
164 );
165 }
166 }
167
168 fn write_record_inner(
169 &mut self,
170 data: &IpoptDataHandle,
171 cq: &IpoptCqHandle,
172 ) -> std::io::Result<()> {
173 let (iter, mu, tau, alpha_pr, alpha_du, delta_x, delta_s, delta_c, delta_d, curr_opt) = {
177 let d = data.borrow();
178 (
179 d.iter_count as u32,
180 d.curr_mu,
181 d.curr_tau,
182 d.info_alpha_primal,
183 d.info_alpha_dual,
184 d.info_regu_x,
185 d.perturbations.delta_s,
186 d.perturbations.delta_c,
187 d.perturbations.delta_d,
188 d.curr.clone(),
189 )
190 };
191 let Some(curr) = curr_opt else {
192 return Ok(());
194 };
195
196 let inf_pr = cq.borrow().curr_primal_infeasibility_max();
199 let inf_du = cq.borrow().curr_dual_infeasibility_max();
200 let constr_viol = cq.borrow().curr_constraint_violation();
201 let dual_inf = inf_du; let complementarity = {
208 let cq_ref = cq.borrow();
209 cq_ref
210 .curr_compl_x_l()
211 .amax()
212 .max(cq_ref.curr_compl_x_u().amax())
213 .max(cq_ref.curr_compl_s_l().amax())
214 .max(cq_ref.curr_compl_s_u().amax())
215 };
216 let f_val = cq.borrow().curr_f();
217
218 if !self.header_written {
220 let n = curr.x.dim() as u32;
221 let m = (curr.y_c.dim() + curr.y_d.dim()) as u32;
222 self.write_header(n, m)?;
223 }
224
225 self.write_u32(iter)?;
227 self.write_u32(0)?; self.write_f64(mu)?;
229 self.write_f64(tau)?;
230 self.write_f64(alpha_pr)?;
231 self.write_f64(alpha_du)?;
232 self.write_f64(delta_x)?;
233 self.write_f64(delta_s)?;
234 self.write_f64(delta_c)?;
235 self.write_f64(delta_d)?;
236 self.write_f64(inf_pr)?;
237 self.write_f64(inf_du)?;
238 self.write_f64(constr_viol)?;
239 self.write_f64(dual_inf)?;
240 self.write_f64(complementarity)?;
241 self.write_f64(f_val)?;
242
243 self.write_vec(&*curr.x)?;
245 self.write_vec(&*curr.s)?;
246 self.write_vec(&*curr.y_c)?;
247 self.write_vec(&*curr.y_d)?;
248 self.write_vec(&*curr.z_l)?;
249 self.write_vec(&*curr.z_u)?;
250 self.write_vec(&*curr.v_l)?;
251 self.write_vec(&*curr.v_u)?;
252
253 self.write_u32(0)?;
255 Ok(())
256 }
257}
258
259impl Drop for IterDumper {
260 fn drop(&mut self) {
261 if let Err(e) = self.writer.flush() {
264 eprintln!("iter_dump: failed to flush trace file on drop: {}", e);
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use crate::ipopt_data::IpoptData;
273 use crate::iterates_vector::IteratesVector;
274 use pounce_linalg::dense_vector::DenseVectorSpace;
275 use std::cell::RefCell;
276 use std::rc::Rc;
277
278 fn dense(n: i32, vals: Option<&[Number]>) -> Rc<dyn Vector> {
279 let space = DenseVectorSpace::new(n);
280 let mut dv = space.make_new_dense();
281 if let Some(v) = vals {
282 dv.set_values(v);
283 }
284 Rc::new(dv) as Rc<dyn Vector>
285 }
286
287 #[test]
288 fn write_vec_emits_len_then_values_little_endian() {
289 let path =
293 std::env::temp_dir().join(format!("pounce_iter_dump_test_{}.bin", std::process::id()));
294 std::env::set_var(ENV_DUMP_PATH, &path);
295 let mut dumper = IterDumper::from_env().expect("dumper");
296 std::env::remove_var(ENV_DUMP_PATH);
297 let v = dense(3, Some(&[1.0_f64, 2.0, 3.0]));
298 dumper.write_vec(&*v).unwrap();
299 drop(dumper);
300 let bytes = std::fs::read(&path).unwrap();
301 assert_eq!(bytes.len(), 4 + 3 * 8);
303 assert_eq!(&bytes[0..4], &3u32.to_le_bytes());
304 assert_eq!(&bytes[4..12], &1.0_f64.to_le_bytes());
305 assert_eq!(&bytes[12..20], &2.0_f64.to_le_bytes());
306 assert_eq!(&bytes[20..28], &3.0_f64.to_le_bytes());
307 let _ = std::fs::remove_file(&path);
308 }
309
310 #[test]
311 fn from_env_returns_none_when_unset() {
312 std::env::remove_var(ENV_DUMP_PATH);
313 assert!(IterDumper::from_env().is_none());
314 }
315
316 #[test]
317 fn header_writes_magic_and_version() {
318 let path =
319 std::env::temp_dir().join(format!("pounce_iter_dump_hdr_{}.bin", std::process::id()));
320 std::env::set_var(ENV_DUMP_PATH, &path);
321 std::env::set_var(ENV_DUMP_NAME, "hs071");
322 let mut dumper = IterDumper::from_env().expect("dumper");
323 std::env::remove_var(ENV_DUMP_PATH);
324 std::env::remove_var(ENV_DUMP_NAME);
325 dumper.write_header(4, 2).unwrap();
326 drop(dumper);
327 let bytes = std::fs::read(&path).unwrap();
328 assert_eq!(&bytes[0..8], MAGIC);
329 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");
336 assert_eq!(bytes.len(), 37);
337 let _ = std::fs::remove_file(&path);
338 }
339
340 #[test]
345 fn iv_dim_matches_record_layout_assumption() {
346 let iv = IteratesVector::new(
347 dense(4, Some(&[1.0, 2.0, 3.0, 4.0])),
348 dense(1, Some(&[0.5])),
349 dense(1, Some(&[1.0])),
350 dense(1, Some(&[1.0])),
351 dense(4, Some(&[1.0, 1.0, 1.0, 1.0])),
352 dense(4, Some(&[1.0, 1.0, 1.0, 1.0])),
353 dense(1, Some(&[1.0])),
354 dense(0, None),
355 );
356 assert_eq!(iv.x.dim(), 4);
358 assert_eq!(iv.v_u.dim(), 0);
359 let mut data = IpoptData::new();
360 data.set_curr(iv);
361 let _h: IpoptDataHandle = Rc::new(RefCell::new(data));
362 }
363}