1use crate::nl_reader;
44use pounce_common::types::{Number, NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
45use pounce_nlp::tnlp::{BoundsInfo, IndexStyle, SparsityRequest, TNLP};
46use std::path::PathBuf;
47use std::process::ExitCode;
48
49#[derive(Debug, Clone)]
51pub struct VerifyArgs {
52 pub nl: PathBuf,
53 pub sol: PathBuf,
54 pub feas_tol: Number,
56 pub opt_tol: Number,
58 pub json_output: Option<PathBuf>,
60 pub require_optimal: bool,
63}
64
65impl Default for VerifyArgs {
66 fn default() -> Self {
67 VerifyArgs {
68 nl: PathBuf::new(),
69 sol: PathBuf::new(),
70 feas_tol: 1e-6,
71 opt_tol: 1e-6,
72 json_output: None,
73 require_optimal: false,
74 }
75 }
76}
77
78const USAGE: &str = "\
79Usage: pounce verify <problem.nl> <claim.sol> [OPTIONS]
80
81Independently check that the solution in <claim.sol> satisfies the
82constraints and bounds of the canonical problem <problem.nl>. Re-derives
83feasibility from the model itself — it does not trust the .sol's status
84line or rerun the solver.
85
86Arguments:
87 <problem.nl> canonical AMPL .nl problem (the source of truth)
88 <claim.sol> claimed AMPL .sol solution to check
89
90Options:
91 --feas-tol <t> feasibility tolerance (default 1e-6)
92 --opt-tol <t> stationarity tolerance (default 1e-6)
93 --require-optimal also fail if the KKT stationarity residual
94 exceeds --opt-tol (needs duals in the .sol)
95 --json-output <path> write a JSON verification receipt to <path>
96 -h, --help print this message
97
98Exit code: 0 = verified feasible, 20 = violation exceeds tolerance,
992 = usage/IO error.";
100
101pub fn run_from_argv(rest: &[String]) -> ExitCode {
103 let args = match parse_verify_argv(rest) {
104 Ok(Some(a)) => a,
105 Ok(None) => {
106 println!("{USAGE}");
108 return ExitCode::SUCCESS;
109 }
110 Err(msg) => {
111 eprintln!("pounce verify: {msg}");
112 eprintln!("{USAGE}");
113 return ExitCode::from(2);
114 }
115 };
116 run(&args)
117}
118
119fn parse_verify_argv(rest: &[String]) -> Result<Option<VerifyArgs>, String> {
120 let mut a = VerifyArgs::default();
121 let mut positionals: Vec<PathBuf> = Vec::new();
122 let mut it = rest.iter();
123 while let Some(arg) = it.next() {
124 match arg.as_str() {
125 "-h" | "--help" => return Ok(None),
126 "--feas-tol" => {
127 let v = it.next().ok_or("--feas-tol requires a value")?;
128 a.feas_tol = v.parse().map_err(|e| format!("--feas-tol: {e}"))?;
129 }
130 "--opt-tol" => {
131 let v = it.next().ok_or("--opt-tol requires a value")?;
132 a.opt_tol = v.parse().map_err(|e| format!("--opt-tol: {e}"))?;
133 }
134 "--require-optimal" => a.require_optimal = true,
135 "--json-output" => {
136 let v = it.next().ok_or("--json-output requires a value")?;
137 a.json_output = Some(PathBuf::from(v));
138 }
139 other if other.starts_with('-') => {
140 return Err(format!("unknown flag `{other}`"));
141 }
142 _ => positionals.push(PathBuf::from(arg)),
143 }
144 }
145 match positionals.len() {
146 0 | 1 => Err("expected two positional arguments: <problem.nl> <claim.sol>".to_string()),
147 2 => {
148 a.nl = positionals[0].clone();
149 a.sol = positionals[1].clone();
150 Ok(Some(a))
151 }
152 n => Err(format!("expected 2 positional arguments, got {n}")),
153 }
154}
155
156#[derive(Debug)]
159pub struct VerifyOutcome {
160 pub n_vars: usize,
161 pub n_cons: usize,
162 pub nl_sha256: String,
163 pub sol_sha256: String,
164 pub solve_result_num: Option<i32>,
165 pub feas_tol: Number,
166 pub opt_tol: Number,
167 pub max_con_violation: Number,
169 pub worst_con: Option<RowReport>,
170 pub max_bound_violation: Number,
171 pub worst_bound: Option<RowReport>,
172 pub feasible: bool,
173 pub objective: Option<Number>,
175 pub duals_present: bool,
176 pub stationarity: Option<Number>,
177 pub dual_sign: Option<i32>,
178 pub complementarity: Option<Number>,
179 pub optimal: Option<bool>,
180 pub verified: bool,
182}
183
184#[derive(Debug, Clone)]
185pub struct RowReport {
186 pub index: usize,
187 pub name: String,
188 pub value: Number,
189 pub lo: Number,
190 pub hi: Number,
191 pub violation: Number,
192}
193
194fn is_finite_bound(b: Number) -> bool {
195 b > NLP_LOWER_BOUND_INF && b < NLP_UPPER_BOUND_INF
196}
197
198fn box_violation(v: Number, lo: Number, hi: Number) -> Number {
206 if !v.is_finite() {
207 return Number::INFINITY;
208 }
209 let below = if is_finite_bound(lo) {
210 lo - v
211 } else {
212 Number::NEG_INFINITY
213 };
214 let above = if is_finite_bound(hi) {
215 v - hi
216 } else {
217 Number::NEG_INFINITY
218 };
219 below.max(above).max(0.0)
220}
221
222pub fn run(args: &VerifyArgs) -> ExitCode {
223 let outcome = match evaluate(args) {
224 Ok(o) => o,
225 Err(msg) => {
226 eprintln!("pounce verify: {msg}");
227 return ExitCode::from(2);
228 }
229 };
230 print_report(args, &outcome);
231
232 if let Some(path) = &args.json_output {
233 let json = receipt_json(args, &outcome);
234 if let Err(e) = std::fs::write(path, json.as_bytes()) {
235 eprintln!(
236 "pounce verify: failed to write receipt {}: {e}",
237 path.display()
238 );
239 return ExitCode::from(2);
240 }
241 let signed = std::env::var(KEY_ENV)
242 .map(|k| !k.is_empty())
243 .unwrap_or(false);
244 println!(
245 " receipt: {}{}",
246 path.display(),
247 if signed {
248 " (signed: HMAC-SHA256)"
249 } else {
250 ""
251 }
252 );
253 }
254
255 if outcome.verified {
256 ExitCode::SUCCESS
257 } else {
258 ExitCode::from(20)
259 }
260}
261
262fn evaluate(args: &VerifyArgs) -> Result<VerifyOutcome, String> {
263 let nl_bytes =
265 std::fs::read(&args.nl).map_err(|e| format!("cannot read {}: {e}", args.nl.display()))?;
266 let sol_bytes =
267 std::fs::read(&args.sol).map_err(|e| format!("cannot read {}: {e}", args.sol.display()))?;
268 let nl_sha256 = sha256::hex(&nl_bytes);
269 let sol_sha256 = sha256::hex(&sol_bytes);
270
271 let prob = nl_reader::read_nl_file(&args.nl)?;
273 let n = prob.n;
274 let m = prob.m;
275 let con_names = prob.con_names.clone();
276 let var_names = prob.var_names.clone();
277 let mut tnlp = nl_reader::NlTnlp::new(prob);
278
279 let info = tnlp
280 .get_nlp_info()
281 .ok_or("get_nlp_info failed on the .nl")?;
282 let nnz = info.nnz_jac_g.max(0) as usize;
283 let fortran = matches!(info.index_style, IndexStyle::Fortran);
284
285 let sol_text = String::from_utf8_lossy(&sol_bytes);
287 let parsed = parse_sol(&sol_text)?;
288 if parsed.x.len() != n {
289 return Err(format!(
290 "solution has {} primal values but the problem has {n} variables \
291 (is this the right .sol for this .nl?)",
292 parsed.x.len()
293 ));
294 }
295 let x = parsed.x;
296 let duals_present = !parsed.lambda.is_empty();
297 if duals_present && parsed.lambda.len() != m {
298 return Err(format!(
299 "solution carries {} dual values but the problem has {m} constraints",
300 parsed.lambda.len()
301 ));
302 }
303
304 let mut x_l = vec![0.0; n];
306 let mut x_u = vec![0.0; n];
307 let mut g_l = vec![0.0; m];
308 let mut g_u = vec![0.0; m];
309 if !tnlp.get_bounds_info(BoundsInfo {
310 x_l: &mut x_l,
311 x_u: &mut x_u,
312 g_l: &mut g_l,
313 g_u: &mut g_u,
314 }) {
315 return Err("get_bounds_info failed".to_string());
316 }
317
318 let mut max_bound_violation = 0.0_f64;
320 let mut worst_bound: Option<RowReport> = None;
321 for j in 0..n {
322 let viol = box_violation(x[j], x_l[j], x_u[j]);
323 if viol > max_bound_violation {
324 max_bound_violation = viol;
325 worst_bound = Some(RowReport {
326 index: j,
327 name: name_at(&var_names, j, 'x'),
328 value: x[j],
329 lo: x_l[j],
330 hi: x_u[j],
331 violation: viol,
332 });
333 }
334 }
335
336 let mut g = vec![0.0; m];
338 if !tnlp.eval_g(&x, true, &mut g) {
339 return Err("eval_g failed at the claimed solution".to_string());
340 }
341 let mut max_con_violation = 0.0_f64;
342 let mut worst_con: Option<RowReport> = None;
343 for i in 0..m {
344 let viol = box_violation(g[i], g_l[i], g_u[i]);
345 if viol > max_con_violation {
346 max_con_violation = viol;
347 worst_con = Some(RowReport {
348 index: i,
349 name: name_at(&con_names, i, 'c'),
350 value: g[i],
351 lo: g_l[i],
352 hi: g_u[i],
353 violation: viol,
354 });
355 }
356 }
357
358 let feasible = max_con_violation <= args.feas_tol && max_bound_violation <= args.feas_tol;
359
360 let objective = tnlp.eval_f(&x, true);
362
363 let mut stationarity = None;
365 let mut dual_sign = None;
366 let mut complementarity = None;
367 let mut optimal = None;
368 if duals_present {
369 let lambda = &parsed.lambda;
370
371 let mut grad_f = vec![0.0; n];
373 tnlp.eval_grad_f(&x, true, &mut grad_f);
374
375 let mut irow = vec![0i32; nnz];
377 let mut jcol = vec![0i32; nnz];
378 tnlp.eval_jac_g(
379 Some(&x),
380 true,
381 SparsityRequest::Structure {
382 irow: &mut irow,
383 jcol: &mut jcol,
384 },
385 );
386 let mut jval = vec![0.0; nnz];
387 tnlp.eval_jac_g(
388 Some(&x),
389 true,
390 SparsityRequest::Values { values: &mut jval },
391 );
392
393 let (resid_pos, _comp_pos) = stationarity_residual(
398 1.0, &grad_f, &irow, &jcol, &jval, fortran, lambda, &x, &x_l, &x_u,
399 );
400 let (resid_neg, _comp_neg) = stationarity_residual(
401 -1.0, &grad_f, &irow, &jcol, &jval, fortran, lambda, &x, &x_l, &x_u,
402 );
403 let (best_resid, sign) = if resid_pos <= resid_neg {
404 (resid_pos, 1)
405 } else {
406 (resid_neg, -1)
407 };
408 stationarity = Some(best_resid);
409 dual_sign = Some(sign);
410 complementarity = Some(constraint_complementarity(lambda, &g, &g_l, &g_u));
411 optimal = Some(best_resid <= args.opt_tol);
412 }
413
414 let verified = feasible && (!args.require_optimal || optimal.unwrap_or(false));
417
418 Ok(VerifyOutcome {
419 n_vars: n,
420 n_cons: m,
421 nl_sha256,
422 sol_sha256,
423 solve_result_num: parsed.solve_result_num,
424 feas_tol: args.feas_tol,
425 opt_tol: args.opt_tol,
426 max_con_violation,
427 worst_con,
428 max_bound_violation,
429 worst_bound,
430 feasible,
431 objective,
432 duals_present,
433 stationarity,
434 dual_sign,
435 complementarity,
436 optimal,
437 verified,
438 })
439}
440
441#[allow(clippy::too_many_arguments)]
446fn stationarity_residual(
447 sign: Number,
448 grad_f: &[Number],
449 irow: &[i32],
450 jcol: &[i32],
451 jval: &[Number],
452 fortran: bool,
453 lambda: &[Number],
454 x: &[Number],
455 x_l: &[Number],
456 x_u: &[Number],
457) -> (Number, Number) {
458 let n = grad_f.len();
459 let off = if fortran { 1 } else { 0 };
460 let mut s = grad_f.to_vec();
461 for k in 0..jval.len() {
462 let row = (irow[k] as usize).wrapping_sub(off);
463 let col = (jcol[k] as usize).wrapping_sub(off);
464 if row < lambda.len() && col < n {
465 s[col] += sign * jval[k] * lambda[row];
466 }
467 }
468 let mut dual_inf = 0.0_f64;
470 for j in 0..n {
471 let at_lo = is_finite_bound(x_l[j]) && (x[j] - x_l[j]).abs() <= 1e-8 * (1.0 + x_l[j].abs());
472 let at_hi = is_finite_bound(x_u[j]) && (x_u[j] - x[j]).abs() <= 1e-8 * (1.0 + x_u[j].abs());
473 let fixed =
474 is_finite_bound(x_l[j]) && is_finite_bound(x_u[j]) && (x_u[j] - x_l[j]).abs() <= 1e-12;
475 let r = if fixed {
476 0.0
477 } else if at_lo && !at_hi {
478 (-s[j]).max(0.0)
480 } else if at_hi && !at_lo {
481 s[j].max(0.0)
483 } else {
484 s[j].abs()
485 };
486 dual_inf = dual_inf.max(r);
487 }
488 (dual_inf, 0.0)
489}
490
491fn constraint_complementarity(
495 lambda: &[Number],
496 g: &[Number],
497 g_l: &[Number],
498 g_u: &[Number],
499) -> Number {
500 let mut comp = 0.0_f64;
501 for i in 0..lambda.len() {
502 if (g_u[i] - g_l[i]).abs() <= 1e-12 {
503 continue; }
505 let dl = if is_finite_bound(g_l[i]) {
506 (g[i] - g_l[i]).abs()
507 } else {
508 Number::INFINITY
509 };
510 let du = if is_finite_bound(g_u[i]) {
511 (g_u[i] - g[i]).abs()
512 } else {
513 Number::INFINITY
514 };
515 let dist = dl.min(du);
516 if dist.is_finite() {
517 comp = comp.max(lambda[i].abs() * dist);
518 }
519 }
520 comp
521}
522
523fn name_at(names: &[String], i: usize, kind: char) -> String {
524 match names.get(i) {
525 Some(s) if !s.is_empty() => s.clone(),
526 _ => format!("{kind}[{i}]"),
527 }
528}
529
530#[derive(Debug)]
535struct ParsedSol {
536 x: Vec<Number>,
537 lambda: Vec<Number>,
538 solve_result_num: Option<i32>,
539}
540
541fn parse_sol(text: &str) -> Result<ParsedSol, String> {
546 let mut after_options = None;
548 for (i, line) in text.lines().enumerate() {
549 if line.trim() == "Options" {
550 after_options = Some(i);
551 break;
552 }
553 }
554 let start = after_options.ok_or("malformed .sol: no `Options` section found")?;
555 let tail: String = text.lines().skip(start + 1).collect::<Vec<_>>().join(" ");
556 let mut toks = tail.split_whitespace();
557
558 let nopts: usize = toks
559 .next()
560 .ok_or("malformed .sol: missing option count")?
561 .parse()
562 .map_err(|e| format!("malformed .sol: bad option count: {e}"))?;
563 for _ in 0..nopts {
564 toks.next()
565 .ok_or("malformed .sol: truncated option words")?;
566 }
567
568 let next_usize = |toks: &mut std::str::SplitWhitespace, what: &str| -> Result<usize, String> {
569 toks.next()
570 .ok_or_else(|| format!("malformed .sol: missing {what}"))?
571 .parse::<usize>()
572 .map_err(|e| format!("malformed .sol: bad {what}: {e}"))
573 };
574 let n_dual = next_usize(&mut toks, "dual count")?;
575 let _m = next_usize(&mut toks, "constraint count")?;
576 let n_primal = next_usize(&mut toks, "primal count")?;
577 let _n = next_usize(&mut toks, "variable count")?;
578
579 let mut lambda = Vec::with_capacity(n_dual);
580 for k in 0..n_dual {
581 let t = toks
582 .next()
583 .ok_or_else(|| format!("malformed .sol: truncated dual block at {k}"))?;
584 lambda.push(
585 t.parse::<Number>()
586 .map_err(|e| format!("malformed .sol: bad dual {k}: {e}"))?,
587 );
588 }
589 let mut x = Vec::with_capacity(n_primal);
590 for k in 0..n_primal {
591 let t = toks
592 .next()
593 .ok_or_else(|| format!("malformed .sol: truncated primal block at {k}"))?;
594 x.push(
595 t.parse::<Number>()
596 .map_err(|e| format!("malformed .sol: bad primal {k}: {e}"))?,
597 );
598 }
599
600 let mut solve_result_num = None;
602 let rest: Vec<&str> = toks.collect();
603 if let Some(p) = rest.iter().position(|&t| t == "objno") {
604 if let Some(code) = rest.get(p + 2) {
605 solve_result_num = code.parse::<i32>().ok();
606 }
607 }
608
609 Ok(ParsedSol {
610 x,
611 lambda,
612 solve_result_num,
613 })
614}
615
616fn print_report(args: &VerifyArgs, o: &VerifyOutcome) {
621 println!("pounce verify — independent solution check");
622 println!(
623 " problem : {} ({} vars, {} cons)",
624 args.nl.display(),
625 o.n_vars,
626 o.n_cons
627 );
628 println!(" sha256:{}", o.nl_sha256);
629 println!(" solution: {}", args.sol.display());
630 println!(" sha256:{}", o.sol_sha256);
631 if let Some(srn) = o.solve_result_num {
632 println!(" claimed solve_result_num: {srn}");
633 }
634 println!();
635 println!(" feasibility (tol {:.1e}):", o.feas_tol);
636 print_row(
637 "max constraint violation",
638 o.max_con_violation,
639 &o.worst_con,
640 );
641 print_row(
642 "max bound violation ",
643 o.max_bound_violation,
644 &o.worst_bound,
645 );
646 if let Some(obj) = o.objective {
647 println!(" objective at x*: {obj:.10e}");
648 }
649 if o.duals_present {
650 println!();
651 println!(" optimality (tol {:.1e}, duals supplied):", o.opt_tol);
652 if let Some(s) = o.stationarity {
653 let sign = o.dual_sign.unwrap_or(1);
654 println!(" KKT stationarity residual: {s:.3e} (dual sign {sign:+})");
655 }
656 if let Some(c) = o.complementarity {
657 println!(" complementarity residual : {c:.3e}");
658 }
659 } else {
660 println!();
661 println!(" optimality: not checked (.sol carried no duals)");
662 }
663 println!();
664 let verdict = if o.verified {
665 "VERIFIED — solution is feasible for the canonical problem".to_string()
666 } else if !o.feasible {
667 "REJECTED — solution VIOLATES the canonical constraints".to_string()
668 } else if !o.duals_present {
669 "REJECTED — feasible, but --require-optimal needs duals and the .sol \
673 carried none"
674 .to_string()
675 } else {
676 "REJECTED — feasible but not first-order optimal (--require-optimal)".to_string()
677 };
678 println!(" VERDICT: {verdict}");
679}
680
681fn print_row(label: &str, v: Number, worst: &Option<RowReport>) {
682 match worst {
683 Some(r) => println!(
684 " {label}: {v:.3e} at {} (value {:.6e}, bounds [{:.6e}, {:.6e}])",
685 r.name, r.value, r.lo, r.hi
686 ),
687 None => println!(" {label}: {v:.3e}"),
688 }
689}
690
691pub const KEY_ENV: &str = "POUNCE_VERIFY_KEY";
694
695pub fn signing_preimage(o: &VerifyOutcome) -> String {
708 format!(
709 "pounce-verify-receipt/v1\n\
710 verify_version=1\n\
711 nl_sha256={}\n\
712 sol_sha256={}\n\
713 n_vars={}\n\
714 n_cons={}\n\
715 feasible={}\n\
716 verified={}\n\
717 verdict={}\n",
718 o.nl_sha256,
719 o.sol_sha256,
720 o.n_vars,
721 o.n_cons,
722 o.feasible,
723 o.verified,
724 if o.verified { "VERIFIED" } else { "REJECTED" },
725 )
726}
727
728fn receipt_json(args: &VerifyArgs, o: &VerifyOutcome) -> String {
729 use serde_json::json;
730 let worst_con = o.worst_con.as_ref().map(row_json);
731 let worst_bound = o.worst_bound.as_ref().map(row_json);
732 let optimality = if o.duals_present {
733 json!({
734 "available": true,
735 "objective": o.objective,
736 "stationarity_residual": o.stationarity,
737 "dual_sign": o.dual_sign,
738 "complementarity_residual": o.complementarity,
739 "optimal": o.optimal,
740 "note": "bound-projected stationarity (dual infeasibility) using the .sol's \
741 constraint duals; bound multipliers inferred from activity. Sign chosen \
742 to match the supplied dual convention. Feasibility is the rigorous gate."
743 })
744 } else {
745 json!({ "available": false })
746 };
747 let mut receipt = json!({
748 "pounce_verify_version": 1,
749 "solver": format!("pounce {}", env!("CARGO_PKG_VERSION")),
750 "problem": {
751 "path": args.nl.display().to_string(),
752 "sha256": o.nl_sha256,
753 "n_vars": o.n_vars,
754 "n_cons": o.n_cons,
755 },
756 "solution": {
757 "path": args.sol.display().to_string(),
758 "sha256": o.sol_sha256,
759 "claimed_solve_result_num": o.solve_result_num,
760 "duals_present": o.duals_present,
761 },
762 "tolerances": { "feasibility": o.feas_tol, "optimality": o.opt_tol },
763 "feasibility": {
764 "max_constraint_violation": o.max_con_violation,
765 "worst_constraint": worst_con,
766 "max_bound_violation": o.max_bound_violation,
767 "worst_bound": worst_bound,
768 "feasible": o.feasible,
769 },
770 "optimality": optimality,
771 "verdict": if o.verified { "VERIFIED" } else { "REJECTED" },
772 "verified": o.verified,
773 });
774
775 if let Ok(key) = std::env::var(KEY_ENV) {
779 if !key.is_empty() {
780 if let Some(obj) = receipt.as_object_mut() {
781 let sig = sha256::hmac_hex(key.as_bytes(), signing_preimage(o).as_bytes());
782 obj.insert("signature_alg".into(), json!("HMAC-SHA256"));
783 obj.insert(
784 "signed_fields".into(),
785 json!([
786 "verify_version",
787 "nl_sha256",
788 "sol_sha256",
789 "n_vars",
790 "n_cons",
791 "feasible",
792 "verified",
793 "verdict"
794 ]),
795 );
796 obj.insert("signature".into(), json!(sig));
797 }
798 }
799 }
800
801 serde_json::to_string_pretty(&receipt).unwrap_or_else(|_| "{}".to_string())
802}
803
804fn row_json(r: &RowReport) -> serde_json::Value {
805 serde_json::json!({
806 "index": r.index,
807 "name": r.name,
808 "value": r.value,
809 "lower": r.lo,
810 "upper": r.hi,
811 "violation": r.violation,
812 })
813}
814
815pub mod sha256 {
822 const K: [u32; 64] = [
823 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
824 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
825 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
826 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
827 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
828 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
829 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
830 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
831 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
832 0xc67178f2,
833 ];
834
835 pub fn digest(data: &[u8]) -> [u8; 32] {
837 let mut h: [u32; 8] = [
838 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
839 0x5be0cd19,
840 ];
841
842 let bit_len = (data.len() as u64).wrapping_mul(8);
844 let mut msg = data.to_vec();
845 msg.push(0x80);
846 while msg.len() % 64 != 56 {
847 msg.push(0);
848 }
849 msg.extend_from_slice(&bit_len.to_be_bytes());
850
851 let mut w = [0u32; 64];
852 for chunk in msg.chunks_exact(64) {
853 for i in 0..16 {
854 w[i] = u32::from_be_bytes([
855 chunk[4 * i],
856 chunk[4 * i + 1],
857 chunk[4 * i + 2],
858 chunk[4 * i + 3],
859 ]);
860 }
861 for i in 16..64 {
862 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
863 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
864 w[i] = w[i - 16]
865 .wrapping_add(s0)
866 .wrapping_add(w[i - 7])
867 .wrapping_add(s1);
868 }
869
870 let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) =
871 (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
872 for i in 0..64 {
873 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
874 let ch = (e & f) ^ ((!e) & g);
875 let t1 = hh
876 .wrapping_add(s1)
877 .wrapping_add(ch)
878 .wrapping_add(K[i])
879 .wrapping_add(w[i]);
880 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
881 let maj = (a & b) ^ (a & c) ^ (b & c);
882 let t2 = s0.wrapping_add(maj);
883 hh = g;
884 g = f;
885 f = e;
886 e = d.wrapping_add(t1);
887 d = c;
888 c = b;
889 b = a;
890 a = t1.wrapping_add(t2);
891 }
892 h[0] = h[0].wrapping_add(a);
893 h[1] = h[1].wrapping_add(b);
894 h[2] = h[2].wrapping_add(c);
895 h[3] = h[3].wrapping_add(d);
896 h[4] = h[4].wrapping_add(e);
897 h[5] = h[5].wrapping_add(f);
898 h[6] = h[6].wrapping_add(g);
899 h[7] = h[7].wrapping_add(hh);
900 }
901
902 let mut out = [0u8; 32];
903 for (i, word) in h.iter().enumerate() {
904 out[4 * i..4 * i + 4].copy_from_slice(&word.to_be_bytes());
905 }
906 out
907 }
908
909 fn to_hex(bytes: &[u8]) -> String {
910 let mut out = String::with_capacity(bytes.len() * 2);
911 for b in bytes {
912 out.push_str(&format!("{b:02x}"));
913 }
914 out
915 }
916
917 pub fn hex(data: &[u8]) -> String {
919 to_hex(&digest(data))
920 }
921
922 pub fn hmac(key: &[u8], msg: &[u8]) -> [u8; 32] {
924 const BLOCK: usize = 64;
925 let mut k = [0u8; BLOCK];
926 if key.len() > BLOCK {
927 k[..32].copy_from_slice(&digest(key));
928 } else {
929 k[..key.len()].copy_from_slice(key);
930 }
931 let mut ipad = [0x36u8; BLOCK];
932 let mut opad = [0x5cu8; BLOCK];
933 for i in 0..BLOCK {
934 ipad[i] ^= k[i];
935 opad[i] ^= k[i];
936 }
937 let mut inner = Vec::with_capacity(BLOCK + msg.len());
938 inner.extend_from_slice(&ipad);
939 inner.extend_from_slice(msg);
940 let inner_digest = digest(&inner);
941 let mut outer = Vec::with_capacity(BLOCK + 32);
942 outer.extend_from_slice(&opad);
943 outer.extend_from_slice(&inner_digest);
944 digest(&outer)
945 }
946
947 pub fn hmac_hex(key: &[u8], msg: &[u8]) -> String {
949 to_hex(&hmac(key, msg))
950 }
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956 use crate::nl_writer::{format_sol, SolutionFile};
957
958 #[test]
959 fn sha256_known_answers() {
960 assert_eq!(
962 sha256::hex(b""),
963 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
964 );
965 assert_eq!(
966 sha256::hex(b"abc"),
967 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
968 );
969 assert_eq!(
970 sha256::hex(b"The quick brown fox jumps over the lazy dog"),
971 "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"
972 );
973 }
974
975 #[test]
976 fn hmac_sha256_known_answers() {
977 assert_eq!(
979 sha256::hmac_hex(b"Jefe", b"what do ya want for nothing?"),
980 "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
981 );
982 assert_eq!(
984 sha256::hmac_hex(&[0x0b; 20], b"Hi There"),
985 "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
986 );
987 }
988
989 #[test]
990 fn parse_sol_round_trips_writer() {
991 let message = format!(
995 "POUNCE {}: Optimal Solution Found",
996 env!("CARGO_PKG_VERSION")
997 );
998 let payload = SolutionFile {
999 message: &message,
1000 x: &[1.0, 2.5, -0.5, 100.0],
1001 lambda: &[0.1, -0.2],
1002 solve_result_num: 0,
1003 suffixes: &[],
1004 };
1005 let text = format_sol(&payload);
1006 let parsed = parse_sol(&text).expect("parse");
1007 assert_eq!(parsed.x.len(), 4);
1008 assert_eq!(parsed.lambda.len(), 2);
1009 assert!((parsed.x[1] - 2.5).abs() < 1e-15);
1010 assert!((parsed.x[3] - 100.0).abs() < 1e-12);
1011 assert!((parsed.lambda[0] - 0.1).abs() < 1e-15);
1012 assert_eq!(parsed.solve_result_num, Some(0));
1013 }
1014
1015 #[test]
1016 fn parse_sol_handles_no_duals() {
1017 let payload = SolutionFile {
1018 message: "msg",
1019 x: &[3.0, 4.0],
1020 lambda: &[],
1021 solve_result_num: 200,
1022 suffixes: &[],
1023 };
1024 let text = format_sol(&payload);
1025 let parsed = parse_sol(&text).expect("parse");
1026 assert_eq!(parsed.x, vec![3.0, 4.0]);
1027 assert!(parsed.lambda.is_empty());
1028 assert_eq!(parsed.solve_result_num, Some(200));
1029 }
1030
1031 #[test]
1032 fn box_violation_basic() {
1033 assert_eq!(box_violation(5.0, 0.0, 10.0), 0.0);
1035 assert!((box_violation(-2.0, 0.0, 10.0) - 2.0).abs() < 1e-15);
1037 assert!((box_violation(13.0, 0.0, 10.0) - 3.0).abs() < 1e-15);
1039 assert_eq!(box_violation(1e30, 0.0, NLP_UPPER_BOUND_INF), 0.0);
1041 }
1042
1043 #[test]
1044 fn box_violation_rejects_non_finite() {
1045 assert_eq!(box_violation(Number::NAN, 0.0, 10.0), Number::INFINITY);
1050 assert_eq!(
1052 box_violation(Number::INFINITY, 0.0, NLP_UPPER_BOUND_INF),
1053 Number::INFINITY
1054 );
1055 assert_eq!(
1056 box_violation(Number::NEG_INFINITY, NLP_LOWER_BOUND_INF, 10.0),
1057 Number::INFINITY
1058 );
1059 }
1060}