1use crate::nl_tape::Tape;
35use pounce_common::types::{Index, Number};
36use pounce_nlp::tnlp::{
37 BoundsInfo, IndexStyle, IpoptCq, IpoptData, Linearity, MetaData, NlpInfo, Solution,
38 SparsityRequest, StartingPoint, IDX_NAMES, TNLP,
39};
40use std::cell::RefCell;
41use std::collections::{BTreeMap, BTreeSet, HashMap};
42use std::path::Path;
43use std::rc::Rc;
44
45#[derive(Debug, Clone)]
46pub enum Expr {
47 Const(Number),
49 Var(usize),
51 Binary(BinOp, Box<Expr>, Box<Expr>),
53 Unary(UnaryOp, Box<Expr>),
55 Sum(Vec<Expr>),
58 Cse(Rc<Expr>),
66 Funcall { id: usize, args: Vec<FuncallArg> },
70 Compare(CmpOp, Box<Expr>, Box<Expr>),
76 And(Box<Expr>, Box<Expr>),
79 Or(Box<Expr>, Box<Expr>),
82 Not(Box<Expr>),
85 Cond {
91 cond: Box<Expr>,
92 then_: Box<Expr>,
93 else_: Box<Expr>,
94 },
95 MinList(Vec<Expr>),
101 MaxList(Vec<Expr>),
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum CmpOp {
111 Lt,
112 Le,
113 Eq,
114 Ge,
115 Gt,
116 Ne,
117}
118
119#[derive(Debug, Clone)]
123pub enum FuncallArg {
124 Real(Expr),
125 Str(String),
126}
127
128#[derive(Debug, Clone)]
131pub struct ImportedFunc {
132 pub id: usize,
133 pub kind: usize,
135 pub nargs: i64,
137 pub name: String,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum BinOp {
142 Add,
143 Sub,
144 Mul,
145 Div,
146 Pow,
147 Atan2,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum UnaryOp {
153 Neg,
154 Sqrt,
155 Log,
156 Exp,
157 Abs,
158 Sin,
159 Cos,
160 Log10,
161 Tan,
162 Atan,
163 Acos,
164 Sinh,
165 Cosh,
166 Tanh,
167 Asin,
168 Acosh,
169 Asinh,
170 Atanh,
171}
172
173#[derive(Debug, Clone)]
175pub struct NlProblem {
176 pub n: usize,
177 pub m: usize,
178 pub num_obj: usize,
179 pub minimize: bool,
180 pub obj_nonlinear: Expr,
181 pub obj_linear: Vec<(usize, Number)>,
182 pub obj_constant: Number,
183 pub con_nonlinear: Vec<Expr>,
185 pub con_linear: Vec<Vec<(usize, Number)>>,
187 pub x_l: Vec<Number>,
188 pub x_u: Vec<Number>,
189 pub g_l: Vec<Number>,
190 pub g_u: Vec<Number>,
191 pub x0: Vec<Number>,
192 pub lambda0: Vec<Number>,
193 pub suffixes: NlSuffixes,
201 pub imported_funcs: Vec<ImportedFunc>,
205 pub var_names: Vec<String>,
216 pub con_names: Vec<String>,
220}
221
222#[derive(Debug, Clone, Default)]
227pub struct NlSuffixes {
228 pub var_int: BTreeMap<String, Vec<Index>>,
231 pub con_int: BTreeMap<String, Vec<Index>>,
233 pub obj_int: BTreeMap<String, Vec<Index>>,
235 pub problem_int: BTreeMap<String, Index>,
237 pub var_real: BTreeMap<String, Vec<Number>>,
239 pub con_real: BTreeMap<String, Vec<Number>>,
241 pub obj_real: BTreeMap<String, Vec<Number>>,
243 pub problem_real: BTreeMap<String, Number>,
245}
246
247pub fn read_nl_file(path: &Path) -> Result<NlProblem, String> {
258 let txt = std::fs::read_to_string(path)
259 .map_err(|e| format!("could not read {}: {}", path.display(), e))?;
260 let mut prob = parse_nl_text(&txt)?;
261 prob.var_names = read_name_file(&path.with_extension("col"), prob.n);
262 prob.con_names = read_name_file(&path.with_extension("row"), prob.m);
263 Ok(prob)
264}
265
266fn read_name_file(path: &Path, expected: usize) -> Vec<String> {
276 let Ok(txt) = std::fs::read_to_string(path) else {
277 return Vec::new();
278 };
279 let names: Vec<String> = txt.lines().take(expected).map(str::to_owned).collect();
280 if names.len() == expected {
281 names
282 } else {
283 Vec::new()
284 }
285}
286
287pub fn parse_nl_text(txt: &str) -> Result<NlProblem, String> {
289 let mut p = Parser::new(txt);
290 p.parse_header()?;
291 let n = p.n;
292 let m = p.m;
293 let num_obj = p.num_obj;
294
295 let mut con_nonlinear: Vec<Expr> = (0..m).map(|_| Expr::Const(0.0)).collect();
296 let mut obj_nonlinear = Expr::Const(0.0);
297 let mut minimize = true;
298 let mut obj_linear: Vec<(usize, Number)> = Vec::new();
299 let mut con_linear: Vec<Vec<(usize, Number)>> = vec![Vec::new(); m];
300 let mut x_l = vec![-1e19; n];
301 let mut x_u = vec![1e19; n];
302 let mut g_l = vec![-1e19; m];
303 let mut g_u = vec![1e19; m];
304 let mut x0 = vec![0.0; n];
305 let mut lambda0 = vec![0.0; m];
306 let mut suffixes = NlSuffixes::default();
307 let mut imported_funcs: Vec<ImportedFunc> = Vec::new();
308
309 while let Some(line) = p.peek_segment_line() {
310 let tag = line
311 .trim_start()
312 .chars()
313 .next()
314 .ok_or("unexpected blank segment header")?;
315 match tag {
316 'C' => {
317 let (_hdr, rest) = p.eat_segment_header()?;
318 let _ = rest;
319 let idx = parse_segment_index(&_hdr, 'C')?;
320 if idx >= m {
321 return Err(format!("C{idx} out of range; m={m}"));
322 }
323 con_nonlinear[idx] = p.parse_expr()?;
324 }
325 'O' => {
326 let (hdr, _rest) = p.eat_segment_header()?;
327 let parts: Vec<&str> = hdr.split_whitespace().collect();
328 if parts.len() < 2 {
329 return Err(format!("malformed O-segment header: {hdr}"));
330 }
331 let idx = parse_segment_index(parts[0], 'O')?;
332 let kind: i32 = parts[1].parse().map_err(|e| format!("O kind: {e}"))?;
333 if idx == 0 {
334 minimize = kind == 0;
335 obj_nonlinear = p.parse_expr()?;
336 } else {
337 let _ = p.parse_expr()?;
339 }
340 }
341 'r' => {
342 p.eat_segment_header()?;
343 for i in 0..m {
344 let line = p.next_data_line()?;
345 let (lo, hi) = parse_bound_line(&line)?;
346 g_l[i] = lo;
347 g_u[i] = hi;
348 }
349 }
350 'b' => {
351 p.eat_segment_header()?;
352 for i in 0..n {
353 let line = p.next_data_line()?;
354 let (lo, hi) = parse_bound_line(&line)?;
355 x_l[i] = lo;
356 x_u[i] = hi;
357 }
358 }
359 'k' => {
360 p.eat_segment_header()?;
363 let count = if n == 0 { 0 } else { n - 1 };
364 for _ in 0..count {
365 p.next_data_line()?;
366 }
367 }
368 'J' => {
369 let (hdr, _) = p.eat_segment_header()?;
370 let parts: Vec<&str> = hdr.split_whitespace().collect();
371 if parts.len() < 2 {
372 return Err(format!("malformed J-segment header: {hdr}"));
373 }
374 let row = parse_segment_index(parts[0], 'J')?;
375 let nz: usize = parts[1].parse().map_err(|e| format!("J nz: {e}"))?;
376 if row >= m {
377 return Err(format!("J{row} out of range"));
378 }
379 for _ in 0..nz {
380 let line = p.next_data_line()?;
381 let (var, coef) = parse_var_coef(&line)?;
382 con_linear[row].push((var, coef));
383 }
384 }
385 'G' => {
386 let (hdr, _) = p.eat_segment_header()?;
387 let parts: Vec<&str> = hdr.split_whitespace().collect();
388 if parts.len() < 2 {
389 return Err(format!("malformed G-segment header: {hdr}"));
390 }
391 let idx = parse_segment_index(parts[0], 'G')?;
392 let nz: usize = parts[1].parse().map_err(|e| format!("G nz: {e}"))?;
393 let mut acc = Vec::with_capacity(nz);
394 for _ in 0..nz {
395 let line = p.next_data_line()?;
396 let (var, coef) = parse_var_coef(&line)?;
397 acc.push((var, coef));
398 }
399 if idx == 0 {
400 obj_linear = acc;
401 }
402 }
403 'x' => {
404 let (hdr, _) = p.eat_segment_header()?;
405 let parts: Vec<&str> = hdr.split_whitespace().collect();
406 let nx: usize = parts
407 .first()
408 .and_then(|s| s.trim_start_matches('x').parse().ok())
409 .ok_or_else(|| format!("malformed x-segment header: {hdr}"))?;
410 for _ in 0..nx {
411 let line = p.next_data_line()?;
412 let (idx, val) = parse_var_coef(&line)?;
413 if idx < n {
414 x0[idx] = val;
415 }
416 }
417 }
418 'd' => {
419 let (hdr, _) = p.eat_segment_header()?;
420 let parts: Vec<&str> = hdr.split_whitespace().collect();
421 let nd: usize = parts
422 .first()
423 .and_then(|s| s.trim_start_matches('d').parse().ok())
424 .ok_or_else(|| format!("malformed d-segment header: {hdr}"))?;
425 for _ in 0..nd {
426 let line = p.next_data_line()?;
427 let (idx, val) = parse_var_coef(&line)?;
428 if idx < m {
429 lambda0[idx] = val;
430 }
431 }
432 }
433 'V' => p.parse_v_segment()?,
434 'S' => {
435 parse_suffix_segment(&mut p, n, m, num_obj, &mut suffixes)?;
436 }
437 'F' => {
438 let (hdr, _rest) = p.eat_segment_header()?;
441 let parts: Vec<&str> = hdr.split_whitespace().collect();
442 if parts.is_empty() {
443 return Err(format!("malformed F-segment header: '{hdr}'"));
444 }
445 let id = parse_segment_index(parts[0], 'F')?;
446 let kind: usize = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
447 let nargs: i64 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
448 let name = parts.get(3).copied().unwrap_or("").to_string();
449 imported_funcs.push(ImportedFunc {
450 id,
451 kind,
452 nargs,
453 name,
454 });
455 }
456 other => return Err(format!("unknown .nl segment tag '{other}'")),
457 }
458 }
459
460 Ok(NlProblem {
461 n,
462 m,
463 num_obj,
464 minimize,
465 obj_nonlinear,
466 obj_linear,
467 obj_constant: 0.0,
468 con_nonlinear,
469 con_linear,
470 x_l,
471 x_u,
472 g_l,
473 g_u,
474 x0,
475 lambda0,
476 suffixes,
477 imported_funcs,
478 var_names: Vec::new(),
481 con_names: Vec::new(),
482 })
483}
484
485fn parse_suffix_segment(
502 p: &mut Parser,
503 n: usize,
504 m: usize,
505 num_obj: usize,
506 out: &mut NlSuffixes,
507) -> Result<(), String> {
508 let (hdr, _) = p.eat_segment_header()?;
509 let parts: Vec<&str> = hdr.split_whitespace().collect();
510 if parts.len() < 3 {
511 return Err(format!(
512 "malformed S-segment header: '{hdr}' (expected `S<kind> <n> <name>`)"
513 ));
514 }
515 let kind_str = parts[0].trim_start_matches('S');
516 let kind: u32 = kind_str
517 .parse()
518 .map_err(|e| format!("S kind '{kind_str}': {e}"))?;
519 let nentries: usize = parts[1].parse().map_err(|e| format!("S nentries: {e}"))?;
520 let name = parts[2].to_string();
521
522 let is_real = (kind & 0x4) != 0;
523 let target = kind & 0x3;
524 let target_dim = match target {
525 0 => n,
526 1 => m,
527 2 => num_obj,
528 3 => 0, _ => unreachable!("kind & 0x3 is in 0..=3"),
530 };
531
532 let mut int_buf: Vec<Index> = if !is_real && target != 3 {
536 vec![0; target_dim]
537 } else {
538 Vec::new()
539 };
540 let mut real_buf: Vec<Number> = if is_real && target != 3 {
541 vec![0.0; target_dim]
542 } else {
543 Vec::new()
544 };
545 let mut problem_int: Index = 0;
546 let mut problem_real: Number = 0.0;
547
548 for _ in 0..nentries {
549 let line = p.next_data_line()?;
550 let parts: Vec<&str> = line.split_whitespace().collect();
551 if parts.len() < 2 {
552 return Err(format!(
553 "malformed S-segment entry '{line}' (expected `<idx> <value>`)"
554 ));
555 }
556 let idx: usize = parts[0]
557 .parse()
558 .map_err(|e| format!("S entry idx '{}': {e}", parts[0]))?;
559 if target != 3 && idx >= target_dim {
560 return Err(format!(
561 "S-suffix '{name}' index {idx} out of range for target dim {target_dim}"
562 ));
563 }
564 if is_real {
565 let v: Number = parts[1]
566 .parse()
567 .map_err(|e| format!("S real entry value '{}': {e}", parts[1]))?;
568 if target == 3 {
569 problem_real = v;
570 } else {
571 real_buf[idx] = v;
572 }
573 } else {
574 let v: Index = parts[1]
575 .parse()
576 .map_err(|e| format!("S int entry value '{}': {e}", parts[1]))?;
577 if target == 3 {
578 problem_int = v;
579 } else {
580 int_buf[idx] = v;
581 }
582 }
583 }
584
585 match (target, is_real) {
586 (0, false) => {
587 out.var_int.insert(name, int_buf);
588 }
589 (1, false) => {
590 out.con_int.insert(name, int_buf);
591 }
592 (2, false) => {
593 out.obj_int.insert(name, int_buf);
594 }
595 (3, false) => {
596 out.problem_int.insert(name, problem_int);
597 }
598 (0, true) => {
599 out.var_real.insert(name, real_buf);
600 }
601 (1, true) => {
602 out.con_real.insert(name, real_buf);
603 }
604 (2, true) => {
605 out.obj_real.insert(name, real_buf);
606 }
607 (3, true) => {
608 out.problem_real.insert(name, problem_real);
609 }
610 _ => unreachable!(),
611 }
612 Ok(())
613}
614
615fn parse_segment_index(s: &str, tag: char) -> Result<usize, String> {
616 let trimmed = s.trim_start_matches(tag);
617 trimmed
618 .parse()
619 .map_err(|e| format!("malformed {tag}-segment index '{s}': {e}"))
620}
621
622fn parse_bound_line(line: &str) -> Result<(Number, Number), String> {
623 let parts: Vec<&str> = line.split_whitespace().collect();
624 if parts.is_empty() {
625 return Err("empty bound line".into());
626 }
627 let kind: i32 = parts[0].parse().map_err(|e| format!("bound kind: {e}"))?;
628 let lo;
629 let hi;
630 match kind {
631 0 => {
632 if parts.len() < 3 {
634 return Err(format!("bound kind 0 needs 2 values: '{line}'"));
635 }
636 lo = parts[1].parse().map_err(|e| format!("lo: {e}"))?;
637 hi = parts[2].parse().map_err(|e| format!("hi: {e}"))?;
638 }
639 1 => {
640 if parts.len() < 2 {
642 return Err(format!("bound kind 1 needs 1 value: '{line}'"));
643 }
644 lo = -1e19;
645 hi = parts[1].parse().map_err(|e| format!("hi: {e}"))?;
646 }
647 2 => {
648 if parts.len() < 2 {
650 return Err(format!("bound kind 2 needs 1 value: '{line}'"));
651 }
652 lo = parts[1].parse().map_err(|e| format!("lo: {e}"))?;
653 hi = 1e19;
654 }
655 3 => {
656 lo = -1e19;
658 hi = 1e19;
659 }
660 4 => {
661 if parts.len() < 2 {
663 return Err(format!("bound kind 4 needs 1 value: '{line}'"));
664 }
665 let v: Number = parts[1].parse().map_err(|e| format!("eq: {e}"))?;
666 lo = v;
667 hi = v;
668 }
669 5 => return Err("complementarity (kind 5) bounds are not supported".into()),
670 other => return Err(format!("unknown bound kind {other}")),
671 }
672 Ok((lo, hi))
673}
674
675fn parse_var_coef(line: &str) -> Result<(usize, Number), String> {
676 let parts: Vec<&str> = line.split_whitespace().collect();
677 if parts.len() < 2 {
678 return Err(format!("malformed var/coef line: '{line}'"));
679 }
680 let v: usize = parts[0].parse().map_err(|e| format!("var idx: {e}"))?;
681 let c: Number = parts[1].parse().map_err(|e| format!("coef: {e}"))?;
682 Ok((v, c))
683}
684
685struct Parser<'a> {
686 lines: Vec<&'a str>,
687 pos: usize,
688 n: usize,
689 m: usize,
690 num_obj: usize,
691 n_funcs: usize,
693 cses: Vec<Rc<Expr>>,
696}
697
698impl<'a> Parser<'a> {
699 fn new(txt: &'a str) -> Self {
700 let lines: Vec<&str> = txt.lines().collect();
701 Self {
702 lines,
703 pos: 0,
704 n: 0,
705 m: 0,
706 num_obj: 0,
707 n_funcs: 0,
708 cses: Vec::new(),
709 }
710 }
711
712 fn next_line(&mut self) -> Option<&'a str> {
713 while self.pos < self.lines.len() {
714 let l = self.lines[self.pos];
715 self.pos += 1;
716 let trimmed = strip_comment(l).trim();
720 if !trimmed.is_empty() {
721 return Some(l);
722 }
723 }
724 None
725 }
726
727 fn next_data_line(&mut self) -> Result<String, String> {
728 let raw = self
729 .next_line()
730 .ok_or_else(|| "unexpected end of file in data line".to_string())?;
731 Ok(strip_comment(raw).trim().to_string())
732 }
733
734 fn parse_header(&mut self) -> Result<(), String> {
735 let line0 = self.next_line().ok_or("empty .nl file")?;
736 let trimmed = strip_comment(line0).trim();
737 let first = trimmed.chars().next().ok_or("empty header line")?;
738 if first != 'g' {
739 return Err(format!(
740 "only ASCII (g-) .nl files supported; got header '{trimmed}'"
741 ));
742 }
743
744 let l2 = self.next_data_line()?;
746 let nums: Vec<&str> = l2.split_whitespace().collect();
747 if nums.len() < 3 {
748 return Err(format!("malformed line 2: '{l2}'"));
749 }
750 self.n = nums[0].parse().map_err(|e| format!("n: {e}"))?;
751 self.m = nums[1].parse().map_err(|e| format!("m: {e}"))?;
752 self.num_obj = nums[2].parse().map_err(|e| format!("num_obj: {e}"))?;
753
754 for _ in 0..3 {
756 self.next_data_line()?;
757 }
758 let l5 = self.next_data_line()?;
760 let nums5: Vec<&str> = l5.split_whitespace().collect();
761 self.n_funcs = nums5.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
762 for _ in 0..4 {
764 self.next_data_line()?;
765 }
766 Ok(())
767 }
768
769 fn peek_segment_line(&mut self) -> Option<&'a str> {
770 let saved = self.pos;
771 let l = self.next_line()?;
772 self.pos = saved;
773 Some(l)
774 }
775
776 fn eat_segment_header(&mut self) -> Result<(String, String), String> {
779 let raw = self
780 .next_line()
781 .ok_or_else(|| "expected segment header".to_string())?;
782 let (hdr, comment) = split_comment(raw);
783 Ok((hdr.trim().to_string(), comment.trim().to_string()))
784 }
785
786 fn parse_expr(&mut self) -> Result<Expr, String> {
787 let raw = self
788 .next_line()
789 .ok_or_else(|| "expected expression token".to_string())?;
790 let tok = strip_comment(raw).trim().to_string();
791 if tok.is_empty() {
792 return Err("empty expression token".into());
793 }
794 let first = tok.chars().next().ok_or("empty expression token")?;
795 match first {
796 'n' => {
797 let v: Number = tok[1..]
798 .trim()
799 .parse()
800 .map_err(|e| format!("n value: {e}"))?;
801 Ok(Expr::Const(v))
802 }
803 'v' => {
804 let i: usize = tok[1..]
805 .trim()
806 .parse()
807 .map_err(|e| format!("v index: {e}"))?;
808 Ok(self.var_or_cse(i)?)
809 }
810 'o' => {
811 let code: i32 = tok[1..]
812 .trim()
813 .parse()
814 .map_err(|e| format!("opcode: {e}"))?;
815 self.parse_opcode(code)
816 }
817 'f' => {
818 let rest = &tok[1..];
821 let mut parts = rest.split_whitespace();
822 let id_str = parts
823 .next()
824 .ok_or_else(|| format!("missing function id in '{tok}'"))?;
825 let nargs_str = parts
826 .next()
827 .ok_or_else(|| format!("missing nargs in '{tok}'"))?;
828 let id: usize = id_str
829 .parse()
830 .map_err(|e| format!("bad function id '{id_str}': {e}"))?;
831 let nargs: usize = nargs_str
832 .parse()
833 .map_err(|e| format!("bad funcall nargs '{nargs_str}': {e}"))?;
834 let mut args: Vec<FuncallArg> = Vec::with_capacity(nargs);
835 for _ in 0..nargs {
836 args.push(self.parse_funcall_arg()?);
837 }
838 Ok(Expr::Funcall { id, args })
839 }
840 't' | 'u' => Err(format!("unsupported expression token '{tok}'")),
841 other => Err(format!(
842 "unexpected expression token start '{other}': '{tok}'"
843 )),
844 }
845 }
846
847 fn parse_funcall_arg(&mut self) -> Result<FuncallArg, String> {
853 let saved = self.pos;
855 let raw = self
856 .next_line()
857 .ok_or_else(|| "expected funcall argument".to_string())?;
858 let tok = strip_comment(raw).trim().to_string();
859 let first = tok.chars().next().ok_or("empty funcall arg token")?;
860 if first == 'h' {
861 let rest = &tok[1..];
863 let s = match rest.find(':') {
864 Some(i) => rest[i + 1..].to_string(),
865 None => String::new(),
866 };
867 Ok(FuncallArg::Str(s))
868 } else {
869 self.pos = saved;
871 Ok(FuncallArg::Real(self.parse_expr()?))
872 }
873 }
874
875 fn parse_opcode(&mut self, code: i32) -> Result<Expr, String> {
876 match code {
877 0 => {
878 let a = self.parse_expr()?;
879 let b = self.parse_expr()?;
880 Ok(Expr::Binary(BinOp::Add, Box::new(a), Box::new(b)))
881 }
882 1 => {
883 let a = self.parse_expr()?;
884 let b = self.parse_expr()?;
885 Ok(Expr::Binary(BinOp::Sub, Box::new(a), Box::new(b)))
886 }
887 2 => {
888 let a = self.parse_expr()?;
889 let b = self.parse_expr()?;
890 Ok(Expr::Binary(BinOp::Mul, Box::new(a), Box::new(b)))
891 }
892 3 => {
893 let a = self.parse_expr()?;
894 let b = self.parse_expr()?;
895 Ok(Expr::Binary(BinOp::Div, Box::new(a), Box::new(b)))
896 }
897 5 => {
898 let a = self.parse_expr()?;
899 let b = self.parse_expr()?;
900 Ok(Expr::Binary(BinOp::Pow, Box::new(a), Box::new(b)))
901 }
902 15 => Ok(Expr::Unary(UnaryOp::Abs, Box::new(self.parse_expr()?))),
903 16 => Ok(Expr::Unary(UnaryOp::Neg, Box::new(self.parse_expr()?))),
904 39 => Ok(Expr::Unary(UnaryOp::Sqrt, Box::new(self.parse_expr()?))),
905 41 => Ok(Expr::Unary(UnaryOp::Sin, Box::new(self.parse_expr()?))),
906 42 => Ok(Expr::Unary(UnaryOp::Log10, Box::new(self.parse_expr()?))),
907 43 => Ok(Expr::Unary(UnaryOp::Log, Box::new(self.parse_expr()?))),
908 44 => Ok(Expr::Unary(UnaryOp::Exp, Box::new(self.parse_expr()?))),
909 46 => Ok(Expr::Unary(UnaryOp::Cos, Box::new(self.parse_expr()?))),
910 38 => Ok(Expr::Unary(UnaryOp::Tan, Box::new(self.parse_expr()?))),
911 49 => Ok(Expr::Unary(UnaryOp::Atan, Box::new(self.parse_expr()?))),
912 53 => Ok(Expr::Unary(UnaryOp::Acos, Box::new(self.parse_expr()?))),
913 40 => Ok(Expr::Unary(UnaryOp::Sinh, Box::new(self.parse_expr()?))),
914 45 => Ok(Expr::Unary(UnaryOp::Cosh, Box::new(self.parse_expr()?))),
915 37 => Ok(Expr::Unary(UnaryOp::Tanh, Box::new(self.parse_expr()?))),
916 51 => Ok(Expr::Unary(UnaryOp::Asin, Box::new(self.parse_expr()?))),
917 52 => Ok(Expr::Unary(UnaryOp::Acosh, Box::new(self.parse_expr()?))),
918 50 => Ok(Expr::Unary(UnaryOp::Asinh, Box::new(self.parse_expr()?))),
919 47 => Ok(Expr::Unary(UnaryOp::Atanh, Box::new(self.parse_expr()?))),
920 48 => {
922 let a = self.parse_expr()?;
923 let b = self.parse_expr()?;
924 Ok(Expr::Binary(BinOp::Atan2, Box::new(a), Box::new(b)))
925 }
926 22 => self.parse_compare(CmpOp::Lt),
929 23 => self.parse_compare(CmpOp::Le),
930 24 => self.parse_compare(CmpOp::Eq),
931 28 => self.parse_compare(CmpOp::Ge),
932 29 => self.parse_compare(CmpOp::Gt),
933 30 => self.parse_compare(CmpOp::Ne),
934 20 => {
936 let a = self.parse_expr()?;
937 let b = self.parse_expr()?;
938 Ok(Expr::Or(Box::new(a), Box::new(b)))
939 }
940 21 => {
941 let a = self.parse_expr()?;
942 let b = self.parse_expr()?;
943 Ok(Expr::And(Box::new(a), Box::new(b)))
944 }
945 34 => Ok(Expr::Not(Box::new(self.parse_expr()?))),
946 35 => {
948 let cond = self.parse_expr()?;
949 let then_ = self.parse_expr()?;
950 let else_ = self.parse_expr()?;
951 Ok(Expr::Cond {
952 cond: Box::new(cond),
953 then_: Box::new(then_),
954 else_: Box::new(else_),
955 })
956 }
957 54 => {
958 let count_line = self.next_data_line()?;
960 let count: usize = count_line
961 .split_whitespace()
962 .next()
963 .ok_or_else(|| "missing variadic count".to_string())?
964 .parse()
965 .map_err(|e| format!("variadic count: {e}"))?;
966 let mut args = Vec::with_capacity(count);
967 for _ in 0..count {
968 args.push(self.parse_expr()?);
969 }
970 Ok(Expr::Sum(args))
971 }
972 11 | 12 => {
975 let count_line = self.next_data_line()?;
976 let count: usize = count_line
977 .split_whitespace()
978 .next()
979 .ok_or_else(|| "missing min/max list count".to_string())?
980 .parse()
981 .map_err(|e| format!("min/max list count: {e}"))?;
982 let mut args = Vec::with_capacity(count);
983 for _ in 0..count {
984 args.push(self.parse_expr()?);
985 }
986 if code == 11 {
987 Ok(Expr::MinList(args))
988 } else {
989 Ok(Expr::MaxList(args))
990 }
991 }
992 other => Err(format!("unsupported opcode o{other}")),
993 }
994 }
995
996 fn parse_compare(&mut self, op: CmpOp) -> Result<Expr, String> {
999 let a = self.parse_expr()?;
1000 let b = self.parse_expr()?;
1001 Ok(Expr::Compare(op, Box::new(a), Box::new(b)))
1002 }
1003
1004 fn var_or_cse(&self, i: usize) -> Result<Expr, String> {
1007 if i < self.n {
1008 Ok(Expr::Var(i))
1009 } else {
1010 let local = i - self.n;
1011 self.cses
1012 .get(local)
1013 .map(|rc| Expr::Cse(rc.clone()))
1014 .ok_or_else(|| {
1015 format!(
1016 "v{i} references CSE {local} but only {} have been defined",
1017 self.cses.len()
1018 )
1019 })
1020 }
1021 }
1022
1023 fn parse_v_segment(&mut self) -> Result<(), String> {
1027 let (hdr, _) = self.eat_segment_header()?;
1028 let parts: Vec<&str> = hdr.split_whitespace().collect();
1029 if parts.len() < 2 {
1030 return Err(format!("malformed V-segment header: {hdr}"));
1031 }
1032 let cse_idx = parse_segment_index(parts[0], 'V')?;
1033 let nlin: usize = parts[1].parse().map_err(|e| format!("V nlin: {e}"))?;
1034 let mut linear: Vec<(usize, Number)> = Vec::with_capacity(nlin);
1036 for _ in 0..nlin {
1037 let line = self.next_data_line()?;
1038 let (var, coef) = parse_var_coef(&line)?;
1039 linear.push((var, coef));
1040 }
1041 let nonlin = self.parse_expr()?;
1042 let mut combined = nonlin;
1045 for (var, coef) in linear {
1046 let v_expr = self.var_or_cse(var)?;
1047 let term = if coef == 1.0 {
1048 v_expr
1049 } else {
1050 Expr::Binary(BinOp::Mul, Box::new(Expr::Const(coef)), Box::new(v_expr))
1051 };
1052 combined = Expr::Binary(BinOp::Add, Box::new(combined), Box::new(term));
1053 }
1054 if cse_idx < self.n {
1055 return Err(format!("V{cse_idx} below n={}", self.n));
1056 }
1057 let local = cse_idx - self.n;
1058 if local != self.cses.len() {
1059 return Err(format!(
1060 "V-segment index V{cse_idx} out of order; expected V{}",
1061 self.n + self.cses.len()
1062 ));
1063 }
1064 self.cses.push(Rc::new(combined));
1065 Ok(())
1066 }
1067}
1068
1069fn strip_comment(s: &str) -> &str {
1070 match s.find('#') {
1071 Some(i) => &s[..i],
1072 None => s,
1073 }
1074}
1075
1076fn split_comment(s: &str) -> (&str, &str) {
1077 match s.find('#') {
1078 Some(i) => (&s[..i], &s[i + 1..]),
1079 None => (s, ""),
1080 }
1081}
1082
1083pub fn eval_expr(e: &Expr, x: &[Number]) -> Number {
1091 match e {
1092 Expr::Const(c) => *c,
1093 Expr::Var(i) => x[*i],
1094 Expr::Binary(op, a, b) => {
1095 let va = eval_expr(a, x);
1096 let vb = eval_expr(b, x);
1097 match op {
1098 BinOp::Add => va + vb,
1099 BinOp::Sub => va - vb,
1100 BinOp::Mul => va * vb,
1101 BinOp::Div => va / vb,
1102 BinOp::Pow => va.powf(vb),
1103 BinOp::Atan2 => va.atan2(vb),
1104 }
1105 }
1106 Expr::Unary(op, a) => {
1107 let va = eval_expr(a, x);
1108 match op {
1109 UnaryOp::Neg => -va,
1110 UnaryOp::Sqrt => va.sqrt(),
1111 UnaryOp::Log => va.ln(),
1112 UnaryOp::Log10 => va.log10(),
1113 UnaryOp::Exp => va.exp(),
1114 UnaryOp::Abs => va.abs(),
1115 UnaryOp::Sin => va.sin(),
1116 UnaryOp::Cos => va.cos(),
1117 UnaryOp::Tan => va.tan(),
1118 UnaryOp::Atan => va.atan(),
1119 UnaryOp::Acos => va.acos(),
1120 UnaryOp::Sinh => va.sinh(),
1121 UnaryOp::Cosh => va.cosh(),
1122 UnaryOp::Tanh => va.tanh(),
1123 UnaryOp::Asin => va.asin(),
1124 UnaryOp::Acosh => va.acosh(),
1125 UnaryOp::Asinh => va.asinh(),
1126 UnaryOp::Atanh => va.atanh(),
1127 }
1128 }
1129 Expr::Sum(args) => args.iter().map(|a| eval_expr(a, x)).sum(),
1130 Expr::MinList(args) => args
1131 .iter()
1132 .map(|a| eval_expr(a, x))
1133 .fold(Number::INFINITY, Number::min),
1134 Expr::MaxList(args) => args
1135 .iter()
1136 .map(|a| eval_expr(a, x))
1137 .fold(Number::NEG_INFINITY, Number::max),
1138 Expr::Compare(op, a, b) => {
1139 let va = eval_expr(a, x);
1140 let vb = eval_expr(b, x);
1141 let truth = match op {
1142 CmpOp::Lt => va < vb,
1143 CmpOp::Le => va <= vb,
1144 CmpOp::Eq => va == vb,
1145 CmpOp::Ge => va >= vb,
1146 CmpOp::Gt => va > vb,
1147 CmpOp::Ne => va != vb,
1148 };
1149 if truth {
1150 1.0
1151 } else {
1152 0.0
1153 }
1154 }
1155 Expr::And(a, b) => {
1156 if eval_expr(a, x) != 0.0 && eval_expr(b, x) != 0.0 {
1157 1.0
1158 } else {
1159 0.0
1160 }
1161 }
1162 Expr::Or(a, b) => {
1163 if eval_expr(a, x) != 0.0 || eval_expr(b, x) != 0.0 {
1164 1.0
1165 } else {
1166 0.0
1167 }
1168 }
1169 Expr::Not(a) => {
1170 if eval_expr(a, x) == 0.0 {
1171 1.0
1172 } else {
1173 0.0
1174 }
1175 }
1176 Expr::Cond { cond, then_, else_ } => {
1177 if eval_expr(cond, x) != 0.0 {
1178 eval_expr(then_, x)
1179 } else {
1180 eval_expr(else_, x)
1181 }
1182 }
1183 Expr::Cse(body) => eval_expr(body, x),
1184 Expr::Funcall { .. } => panic!(
1185 "eval_expr: AMPL imported function called without an external resolver; \
1186 evaluate through the tape AD path (Tape::build_with_externals) instead"
1187 ),
1188 }
1189}
1190
1191fn argmin_argmax(args: &[Expr], x: &[Number], want_min: bool) -> Option<usize> {
1196 let mut best: Option<(usize, Number)> = None;
1197 for (i, a) in args.iter().enumerate() {
1198 let v = eval_expr(a, x);
1199 match best {
1200 None => best = Some((i, v)),
1201 Some((_, bv)) => {
1202 if (want_min && v < bv) || (!want_min && v > bv) {
1206 best = Some((i, v));
1207 }
1208 }
1209 }
1210 }
1211 best.map(|(i, _)| i)
1212}
1213
1214pub fn grad_expr(e: &Expr, x: &[Number], seed: Number, grad: &mut [Number]) {
1216 match e {
1217 Expr::Const(_) => {}
1218 Expr::Var(i) => grad[*i] += seed,
1219 Expr::Binary(op, a, b) => {
1220 let va = eval_expr(a, x);
1221 let vb = eval_expr(b, x);
1222 match op {
1223 BinOp::Add => {
1224 grad_expr(a, x, seed, grad);
1225 grad_expr(b, x, seed, grad);
1226 }
1227 BinOp::Sub => {
1228 grad_expr(a, x, seed, grad);
1229 grad_expr(b, x, -seed, grad);
1230 }
1231 BinOp::Mul => {
1232 grad_expr(a, x, seed * vb, grad);
1233 grad_expr(b, x, seed * va, grad);
1234 }
1235 BinOp::Div => {
1236 grad_expr(a, x, seed / vb, grad);
1237 grad_expr(b, x, -seed * va / (vb * vb), grad);
1238 }
1239 BinOp::Pow => {
1240 let dpa = vb * va.powf(vb - 1.0);
1242 grad_expr(a, x, seed * dpa, grad);
1243 if va > 0.0 {
1245 let dpb = va.powf(vb) * va.ln();
1246 grad_expr(b, x, seed * dpb, grad);
1247 }
1248 }
1249 BinOp::Atan2 => {
1250 let d = va * va + vb * vb;
1252 grad_expr(a, x, seed * vb / d, grad);
1253 grad_expr(b, x, -seed * va / d, grad);
1254 }
1255 }
1256 }
1257 Expr::Unary(op, a) => {
1258 let va = eval_expr(a, x);
1259 let d = match op {
1260 UnaryOp::Neg => -1.0,
1261 UnaryOp::Sqrt => 0.5 / va.sqrt(),
1262 UnaryOp::Log => 1.0 / va,
1263 UnaryOp::Log10 => 1.0 / (va * std::f64::consts::LN_10),
1264 UnaryOp::Exp => va.exp(),
1265 UnaryOp::Abs => {
1266 if va > 0.0 {
1267 1.0
1268 } else if va < 0.0 {
1269 -1.0
1270 } else {
1271 0.0
1272 }
1273 }
1274 UnaryOp::Sin => va.cos(),
1275 UnaryOp::Cos => -va.sin(),
1276 UnaryOp::Tan => {
1277 let t = va.tan();
1278 1.0 + t * t
1279 }
1280 UnaryOp::Atan => 1.0 / (1.0 + va * va),
1281 UnaryOp::Acos => -1.0 / (1.0 - va * va).sqrt(),
1282 UnaryOp::Sinh => va.cosh(),
1283 UnaryOp::Cosh => va.sinh(),
1284 UnaryOp::Tanh => {
1285 let t = va.tanh();
1286 1.0 - t * t
1287 }
1288 UnaryOp::Asin => 1.0 / (1.0 - va * va).sqrt(),
1289 UnaryOp::Acosh => 1.0 / (va * va - 1.0).sqrt(),
1290 UnaryOp::Asinh => 1.0 / (va * va + 1.0).sqrt(),
1291 UnaryOp::Atanh => 1.0 / (1.0 - va * va),
1292 };
1293 grad_expr(a, x, seed * d, grad);
1294 }
1295 Expr::Sum(args) => {
1296 for arg in args {
1297 grad_expr(arg, x, seed, grad);
1298 }
1299 }
1300 Expr::MinList(args) => {
1305 if let Some(k) = argmin_argmax(args, x, true) {
1306 grad_expr(&args[k], x, seed, grad);
1307 }
1308 }
1309 Expr::MaxList(args) => {
1310 if let Some(k) = argmin_argmax(args, x, false) {
1311 grad_expr(&args[k], x, seed, grad);
1312 }
1313 }
1314 Expr::Compare(_, _, _) | Expr::And(_, _) | Expr::Or(_, _) | Expr::Not(_) => {}
1317 Expr::Cond { cond, then_, else_ } => {
1320 if eval_expr(cond, x) != 0.0 {
1321 grad_expr(then_, x, seed, grad);
1322 } else {
1323 grad_expr(else_, x, seed, grad);
1324 }
1325 }
1326 Expr::Cse(body) => grad_expr(body, x, seed, grad),
1327 Expr::Funcall { .. } => {
1328 panic!("grad_expr: AMPL imported function called without an external resolver")
1329 }
1330 }
1331}
1332
1333pub fn collect_vars(e: &Expr, out: &mut BTreeSet<usize>) {
1335 match e {
1336 Expr::Const(_) => {}
1337 Expr::Var(i) => {
1338 out.insert(*i);
1339 }
1340 Expr::Binary(_, a, b) => {
1341 collect_vars(a, out);
1342 collect_vars(b, out);
1343 }
1344 Expr::Unary(_, a) => collect_vars(a, out),
1345 Expr::Sum(args) | Expr::MinList(args) | Expr::MaxList(args) => {
1346 for a in args {
1347 collect_vars(a, out);
1348 }
1349 }
1350 Expr::Compare(_, a, b) | Expr::And(a, b) | Expr::Or(a, b) => {
1356 collect_vars(a, out);
1357 collect_vars(b, out);
1358 }
1359 Expr::Not(a) => collect_vars(a, out),
1360 Expr::Cond { cond, then_, else_ } => {
1361 collect_vars(cond, out);
1362 collect_vars(then_, out);
1363 collect_vars(else_, out);
1364 }
1365 Expr::Cse(body) => collect_vars(body, out),
1366 Expr::Funcall { args, .. } => {
1367 for a in args {
1368 if let FuncallArg::Real(e) = a {
1369 collect_vars(e, out);
1370 }
1371 }
1372 }
1373 }
1374}
1375
1376#[derive(Debug, Clone)]
1388struct ColorWrite {
1389 row: u32,
1390 hess_idx: u32,
1391}
1392
1393#[derive(Debug)]
1394pub struct NlTnlp {
1395 prob: NlProblem,
1396 obj_tapes: Vec<Tape>,
1399 con_tapes: Vec<Vec<Tape>>,
1402 h_irow: Vec<i32>,
1405 h_jcol: Vec<i32>,
1406 jac_cols: Vec<Vec<usize>>,
1408 jac_nnz: usize,
1409 seeds: Vec<Vec<f64>>,
1416 decoding: Vec<Vec<ColorWrite>>,
1420 obj_tape_colors: Vec<Vec<u32>>,
1424 con_tape_colors: Vec<Vec<Vec<u32>>>,
1426 final_x: Option<Vec<Number>>,
1427 final_obj: Number,
1428 scratch_row_grad: Vec<f64>,
1430 vals_scratch: Vec<f64>,
1433 dot_scratch: Vec<f64>,
1434 adj_scratch: Vec<f64>,
1435 adj_dot_scratch: Vec<f64>,
1436 compressed: Vec<Vec<f64>>,
1439}
1440
1441const P_ADD: u8 = 10;
1458const P_MUL: u8 = 20;
1459const P_NEG: u8 = 30;
1460const P_POW: u8 = 40;
1461const P_ATOM: u8 = 100;
1462
1463fn fmt_num(x: Number) -> String {
1466 if x.is_finite() && x == x.trunc() && x.abs() < 1e15 {
1467 format!("{}", x as i64)
1468 } else {
1469 format!("{x}")
1470 }
1471}
1472
1473fn var_label(i: usize, var_names: &[String]) -> String {
1476 match var_names.get(i) {
1477 Some(s) if !s.is_empty() => s.clone(),
1478 _ => format!("x[{i}]"),
1479 }
1480}
1481
1482fn expr_prec(e: &Expr) -> u8 {
1484 match e {
1485 Expr::Binary(BinOp::Add, ..) | Expr::Binary(BinOp::Sub, ..) | Expr::Sum(_) => P_ADD,
1486 Expr::Binary(BinOp::Mul, ..) | Expr::Binary(BinOp::Div, ..) => P_MUL,
1487 Expr::Unary(UnaryOp::Neg, _) => P_NEG,
1488 Expr::Binary(BinOp::Pow, ..) => P_POW,
1489 Expr::Cse(inner) => expr_prec(inner),
1490 _ => P_ATOM,
1492 }
1493}
1494
1495fn render_prec(e: &Expr, min_prec: u8, vn: &[String], funcs: &[ImportedFunc]) -> String {
1498 let s = render_expr(e, vn, funcs);
1499 if expr_prec(e) < min_prec {
1500 format!("({s})")
1501 } else {
1502 s
1503 }
1504}
1505
1506fn unary_name(op: UnaryOp) -> &'static str {
1507 match op {
1508 UnaryOp::Neg => "-",
1509 UnaryOp::Sqrt => "sqrt",
1510 UnaryOp::Log => "log",
1511 UnaryOp::Exp => "exp",
1512 UnaryOp::Abs => "abs",
1513 UnaryOp::Sin => "sin",
1514 UnaryOp::Cos => "cos",
1515 UnaryOp::Log10 => "log10",
1516 UnaryOp::Tan => "tan",
1517 UnaryOp::Atan => "atan",
1518 UnaryOp::Acos => "acos",
1519 UnaryOp::Sinh => "sinh",
1520 UnaryOp::Cosh => "cosh",
1521 UnaryOp::Tanh => "tanh",
1522 UnaryOp::Asin => "asin",
1523 UnaryOp::Acosh => "acosh",
1524 UnaryOp::Asinh => "asinh",
1525 UnaryOp::Atanh => "atanh",
1526 }
1527}
1528
1529fn cmp_sym(op: CmpOp) -> &'static str {
1530 match op {
1531 CmpOp::Lt => "<",
1532 CmpOp::Le => "<=",
1533 CmpOp::Eq => "==",
1534 CmpOp::Ge => ">=",
1535 CmpOp::Gt => ">",
1536 CmpOp::Ne => "!=",
1537 }
1538}
1539
1540fn push_additive(out: &mut String, rendered: &str, first: bool) {
1545 if first {
1546 out.push_str(rendered);
1547 } else if let Some(rest) = rendered.strip_prefix('-') {
1548 out.push_str(" - ");
1549 out.push_str(rest);
1550 } else {
1551 out.push_str(" + ");
1552 out.push_str(rendered);
1553 }
1554}
1555
1556fn render_expr(e: &Expr, vn: &[String], funcs: &[ImportedFunc]) -> String {
1558 match e {
1559 Expr::Const(c) => fmt_num(*c),
1560 Expr::Var(i) => var_label(*i, vn),
1561 Expr::Binary(op, l, r) => match op {
1562 BinOp::Add => {
1563 let mut s = render_prec(l, P_ADD, vn, funcs);
1564 push_additive(&mut s, &render_prec(r, P_ADD, vn, funcs), false);
1565 s
1566 }
1567 BinOp::Sub => format!(
1569 "{} - {}",
1570 render_prec(l, P_ADD, vn, funcs),
1571 render_prec(r, P_ADD + 1, vn, funcs)
1572 ),
1573 BinOp::Mul => format!(
1574 "{}*{}",
1575 render_prec(l, P_MUL, vn, funcs),
1576 render_prec(r, P_MUL, vn, funcs)
1577 ),
1578 BinOp::Div => format!(
1579 "{}/{}",
1580 render_prec(l, P_MUL, vn, funcs),
1581 render_prec(r, P_MUL + 1, vn, funcs)
1582 ),
1583 BinOp::Pow => format!(
1585 "{}^{}",
1586 render_prec(l, P_POW + 1, vn, funcs),
1587 render_prec(r, P_POW, vn, funcs)
1588 ),
1589 BinOp::Atan2 => format!(
1590 "atan2({}, {})",
1591 render_expr(l, vn, funcs),
1592 render_expr(r, vn, funcs)
1593 ),
1594 },
1595 Expr::Unary(UnaryOp::Neg, a) => format!("-{}", render_prec(a, P_NEG, vn, funcs)),
1596 Expr::Unary(op, a) => format!("{}({})", unary_name(*op), render_expr(a, vn, funcs)),
1597 Expr::Sum(xs) => {
1598 if xs.is_empty() {
1599 "0".to_string()
1600 } else {
1601 let mut s = String::new();
1602 for (k, x) in xs.iter().enumerate() {
1603 push_additive(&mut s, &render_prec(x, P_ADD, vn, funcs), k == 0);
1604 }
1605 s
1606 }
1607 }
1608 Expr::Cse(inner) => render_expr(inner, vn, funcs),
1609 Expr::Funcall { id, args } => {
1610 let name = funcs
1611 .iter()
1612 .find(|f| f.id == *id)
1613 .map(|f| f.name.clone())
1614 .unwrap_or_else(|| format!("extern#{id}"));
1615 let parts: Vec<String> = args
1616 .iter()
1617 .map(|a| match a {
1618 FuncallArg::Real(x) => render_expr(x, vn, funcs),
1619 FuncallArg::Str(s) => format!("{s:?}"),
1620 })
1621 .collect();
1622 format!("{name}({})", parts.join(", "))
1623 }
1624 Expr::Compare(op, a, b) => format!(
1625 "({} {} {})",
1626 render_expr(a, vn, funcs),
1627 cmp_sym(*op),
1628 render_expr(b, vn, funcs)
1629 ),
1630 Expr::And(a, b) => format!(
1631 "({} && {})",
1632 render_expr(a, vn, funcs),
1633 render_expr(b, vn, funcs)
1634 ),
1635 Expr::Or(a, b) => format!(
1636 "({} || {})",
1637 render_expr(a, vn, funcs),
1638 render_expr(b, vn, funcs)
1639 ),
1640 Expr::Not(a) => format!("!({})", render_expr(a, vn, funcs)),
1641 Expr::Cond { cond, then_, else_ } => format!(
1642 "if({}, {}, {})",
1643 render_expr(cond, vn, funcs),
1644 render_expr(then_, vn, funcs),
1645 render_expr(else_, vn, funcs)
1646 ),
1647 Expr::MinList(xs) => format!(
1648 "min({})",
1649 xs.iter()
1650 .map(|x| render_expr(x, vn, funcs))
1651 .collect::<Vec<_>>()
1652 .join(", ")
1653 ),
1654 Expr::MaxList(xs) => format!(
1655 "max({})",
1656 xs.iter()
1657 .map(|x| render_expr(x, vn, funcs))
1658 .collect::<Vec<_>>()
1659 .join(", ")
1660 ),
1661 }
1662}
1663
1664fn render_linear(linear: &[(usize, Number)], vn: &[String]) -> String {
1667 let mut out = String::new();
1668 let mut first = true;
1673 for (var, coef) in linear {
1674 if *coef == 0.0 {
1675 continue;
1676 }
1677 let neg = *coef < 0.0;
1678 let mag = coef.abs();
1679 let term = if mag == 1.0 {
1680 var_label(*var, vn)
1681 } else {
1682 format!("{}*{}", fmt_num(mag), var_label(*var, vn))
1683 };
1684 if first {
1685 if neg {
1686 out.push('-');
1687 }
1688 out.push_str(&term);
1689 first = false;
1690 } else {
1691 out.push_str(if neg { " - " } else { " + " });
1692 out.push_str(&term);
1693 }
1694 }
1695 out
1696}
1697
1698fn render_body(linear: &[(usize, Number)], nonlinear: &Expr, prob: &NlProblem) -> String {
1700 let mut s = render_linear(linear, &prob.var_names);
1701 let nl_is_zero = matches!(nonlinear, Expr::Const(c) if *c == 0.0);
1702 if !nl_is_zero {
1703 let nl = render_prec(nonlinear, P_ADD, &prob.var_names, &prob.imported_funcs);
1704 if s.is_empty() {
1705 s = nl;
1706 } else {
1707 push_additive(&mut s, &nl, false);
1708 }
1709 }
1710 if s.is_empty() {
1711 s = "0".to_string();
1712 }
1713 s
1714}
1715
1716pub fn render_constraint_equation(prob: &NlProblem, k: usize) -> String {
1720 let body = render_body(&prob.con_linear[k], &prob.con_nonlinear[k], prob);
1721 let lo = prob.g_l[k];
1722 let hi = prob.g_u[k];
1723 const INF: Number = 1.0e19;
1724 let has_lo = lo > -INF;
1725 let has_hi = hi < INF;
1726 match (has_lo, has_hi) {
1727 (true, true) if lo == hi => format!("{body} = {}", fmt_num(lo)),
1728 (true, true) => format!("{} <= {body} <= {}", fmt_num(lo), fmt_num(hi)),
1729 (true, false) => format!("{body} >= {}", fmt_num(lo)),
1730 (false, true) => format!("{body} <= {}", fmt_num(hi)),
1731 (false, false) => format!("{body} (free)"),
1732 }
1733}
1734
1735pub fn render_all_constraint_equations(prob: &NlProblem) -> Vec<String> {
1738 (0..prob.m)
1739 .map(|k| render_constraint_equation(prob, k))
1740 .collect()
1741}
1742
1743pub fn constraint_jacobian_sparsity(prob: &NlProblem) -> (Vec<Index>, Vec<Index>) {
1757 let mut irow: Vec<Index> = Vec::new();
1758 let mut jcol: Vec<Index> = Vec::new();
1759 let mut support: BTreeSet<usize> = BTreeSet::new();
1760 for k in 0..prob.m {
1761 support.clear();
1762 for &(j, _coef) in &prob.con_linear[k] {
1763 support.insert(j);
1764 }
1765 collect_vars(&prob.con_nonlinear[k], &mut support);
1766 for &j in &support {
1767 irow.push(k as Index);
1768 jcol.push(j as Index);
1769 }
1770 }
1771 (irow, jcol)
1772}
1773
1774fn split_top_sums(expr: &Expr) -> Vec<Expr> {
1801 let mut out = Vec::new();
1802 fn push_leaf(e: &Expr, factor: f64, out: &mut Vec<Expr>) {
1803 if factor == 1.0 {
1804 out.push(e.clone());
1805 } else if factor == -1.0 {
1806 out.push(Expr::Unary(UnaryOp::Neg, Box::new(e.clone())));
1807 } else {
1808 out.push(Expr::Binary(
1809 BinOp::Mul,
1810 Box::new(Expr::Const(factor)),
1811 Box::new(e.clone()),
1812 ));
1813 }
1814 }
1815 fn go(e: &Expr, factor: f64, out: &mut Vec<Expr>) {
1816 match e {
1817 Expr::Sum(terms) => {
1818 for t in terms {
1819 go(t, factor, out);
1820 }
1821 }
1822 Expr::Binary(BinOp::Add, l, r) => {
1823 go(l, factor, out);
1824 go(r, factor, out);
1825 }
1826 Expr::Binary(BinOp::Sub, l, r) => {
1827 go(l, factor, out);
1828 go(r, -factor, out);
1829 }
1830 Expr::Unary(UnaryOp::Neg, x) => {
1831 go(x, -factor, out);
1832 }
1833 Expr::Binary(BinOp::Mul, l, r) => match (l.as_ref(), r.as_ref()) {
1836 (Expr::Const(c), _) => go(r, factor * c, out),
1837 (_, Expr::Const(c)) => go(l, factor * c, out),
1838 _ => push_leaf(e, factor, out),
1839 },
1840 Expr::Binary(BinOp::Div, l, r) => match r.as_ref() {
1841 Expr::Const(c) if *c != 0.0 => go(l, factor / c, out),
1842 _ => push_leaf(e, factor, out),
1843 },
1844 _ => push_leaf(e, factor, out),
1845 }
1846 }
1847 go(expr, 1.0, &mut out);
1848 if out.is_empty() {
1849 out.push(Expr::Const(0.0));
1850 }
1851 out
1852}
1853
1854fn greedy_hessian_coloring(n: usize, lower_pairs: &[(usize, usize)]) -> (Vec<u32>, usize) {
1870 if n == 0 {
1871 return (Vec::new(), 0);
1872 }
1873
1874 let mut col_rows: Vec<Vec<u32>> = vec![Vec::new(); n];
1879 let mut row_cols: Vec<Vec<u32>> = vec![Vec::new(); n];
1880 for &(i, j) in lower_pairs {
1881 col_rows[j].push(i as u32);
1882 row_cols[i].push(j as u32);
1883 if i != j {
1884 col_rows[i].push(j as u32);
1885 row_cols[j].push(i as u32);
1886 }
1887 }
1888
1889 let mut var_color = vec![u32::MAX; n];
1890 let mut forbidden = vec![u32::MAX; n + 1];
1891 let mut n_colors: u32 = 0;
1892
1893 for j in 0..n {
1894 if col_rows[j].is_empty() {
1896 continue;
1897 }
1898 for &r in &col_rows[j] {
1902 for &c in &row_cols[r as usize] {
1903 if c as usize == j {
1904 continue;
1905 }
1906 let cc = var_color[c as usize];
1907 if cc != u32::MAX {
1908 forbidden[cc as usize] = j as u32;
1909 }
1910 }
1911 }
1912 let mut chosen: u32 = 0;
1914 while (chosen as usize) < forbidden.len() && forbidden[chosen as usize] == j as u32 {
1915 chosen += 1;
1916 }
1917 var_color[j] = chosen;
1918 if chosen + 1 > n_colors {
1919 n_colors = chosen + 1;
1920 }
1921 }
1922
1923 (var_color, n_colors as usize)
1924}
1925
1926impl NlTnlp {
1927 pub fn new(prob: NlProblem) -> Self {
1936 Self::try_new(prob)
1937 .unwrap_or_else(|e| panic!("failed to resolve AMPL external functions: {e}"))
1938 }
1939
1940 pub fn try_new(prob: NlProblem) -> Result<Self, String> {
1945 let mut referenced: BTreeSet<usize> = BTreeSet::new();
1951 super::nl_external::collect_funcall_ids(&prob.obj_nonlinear, &mut referenced);
1952 for c in &prob.con_nonlinear {
1953 super::nl_external::collect_funcall_ids(c, &mut referenced);
1954 }
1955 let resolver = if referenced.is_empty() {
1956 super::nl_external::ExternalResolver::default()
1957 } else {
1958 super::nl_external::ExternalResolver::build_for_problem(
1959 &prob.imported_funcs,
1960 &referenced,
1961 )?
1962 };
1963
1964 let obj_summands = split_top_sums(&prob.obj_nonlinear);
1970 let obj_tapes: Vec<Tape> = obj_summands
1971 .iter()
1972 .map(|e| Tape::build_with_externals(e, &resolver))
1973 .collect();
1974
1975 let mut con_tapes: Vec<Vec<Tape>> = Vec::with_capacity(prob.m);
1976 for k in 0..prob.m {
1977 let summands = split_top_sums(&prob.con_nonlinear[k]);
1978 con_tapes.push(
1979 summands
1980 .iter()
1981 .map(|e| Tape::build_with_externals(e, &resolver))
1982 .collect(),
1983 );
1984 }
1985
1986 let mut pairs: BTreeSet<(usize, usize)> = BTreeSet::new();
1989 for t in &obj_tapes {
1990 pairs.extend(t.hessian_sparsity());
1991 }
1992 for row in &con_tapes {
1993 for t in row {
1994 pairs.extend(t.hessian_sparsity());
1995 }
1996 }
1997 let mut h_irow = Vec::with_capacity(pairs.len());
1998 let mut h_jcol = Vec::with_capacity(pairs.len());
1999 let mut hess_map = HashMap::with_capacity(pairs.len());
2000 for (k, (hi, lo)) in pairs.iter().enumerate() {
2001 h_irow.push(*hi as i32);
2002 h_jcol.push(*lo as i32);
2003 hess_map.insert((*hi, *lo), k);
2004 }
2005
2006 let lower_pairs: Vec<(usize, usize)> = pairs.iter().copied().collect();
2011 let (var_color, n_colors) = greedy_hessian_coloring(prob.n, &lower_pairs);
2012
2013 let mut seeds: Vec<Vec<f64>> = vec![vec![0.0; prob.n]; n_colors];
2016 for (k, &c) in var_color.iter().enumerate() {
2017 if c != u32::MAX {
2018 seeds[c as usize][k] = 1.0;
2019 }
2020 }
2021
2022 let mut decoding: Vec<Vec<ColorWrite>> = vec![Vec::new(); n_colors];
2028 for (&(i, j), &idx) in hess_map.iter() {
2029 let c = var_color[j];
2030 debug_assert!(
2031 c != u32::MAX,
2032 "column {j} has Hessian pair {idx} but no color"
2033 );
2034 decoding[c as usize].push(ColorWrite {
2035 row: i as u32,
2036 hess_idx: idx as u32,
2037 });
2038 }
2039
2040 let tape_colors = |t: &Tape| -> Vec<u32> {
2044 let mut s: BTreeSet<u32> = BTreeSet::new();
2045 for v in t.variables() {
2046 let c = var_color[v];
2047 if c != u32::MAX {
2048 s.insert(c);
2049 }
2050 }
2051 s.into_iter().collect()
2052 };
2053 let obj_tape_colors: Vec<Vec<u32>> = obj_tapes.iter().map(tape_colors).collect();
2054 let con_tape_colors: Vec<Vec<Vec<u32>>> = con_tapes
2055 .iter()
2056 .map(|row| row.iter().map(tape_colors).collect())
2057 .collect();
2058
2059 let mut jac_cols: Vec<Vec<usize>> = Vec::with_capacity(prob.m);
2062 let mut jac_nnz = 0;
2063 for i in 0..prob.m {
2064 let mut set: BTreeSet<usize> = BTreeSet::new();
2065 for t in &con_tapes[i] {
2066 for v in t.variables() {
2067 set.insert(v);
2068 }
2069 }
2070 for (v, _) in &prob.con_linear[i] {
2071 set.insert(*v);
2072 }
2073 let cols: Vec<usize> = set.into_iter().collect();
2074 jac_nnz += cols.len();
2075 jac_cols.push(cols);
2076 }
2077
2078 let mut max_tape_n: usize = 0;
2079 for t in &obj_tapes {
2080 max_tape_n = max_tape_n.max(t.ops.len());
2081 }
2082 for row in &con_tapes {
2083 for t in row {
2084 max_tape_n = max_tape_n.max(t.ops.len());
2085 }
2086 }
2087
2088 if std::env::var("POUNCE_DBG_TAPE_STATS").is_ok() {
2089 let n_obj = obj_tapes.len();
2090 let n_con: usize = con_tapes.iter().map(|r| r.len()).sum();
2091 let total = n_obj + n_con;
2092 let mut sum_ops: usize = 0;
2093 for t in &obj_tapes {
2094 sum_ops += t.ops.len();
2095 }
2096 for row in &con_tapes {
2097 for t in row {
2098 sum_ops += t.ops.len();
2099 }
2100 }
2101 let t = total.max(1);
2102 let nnz_h = h_irow.len();
2103 let avg_decode =
2104 decoding.iter().map(|d| d.len()).sum::<usize>() as f64 / n_colors.max(1) as f64;
2105 eprintln!(
2106 "[tape stats] summands={total} (obj={n_obj} con={n_con}) \
2107 total_ops={sum_ops} avg_ops={:.1} max_ops={max_tape_n} \
2108 n_colors={n_colors} avg_decode_per_color={avg_decode:.1} nnz_h={nnz_h}",
2109 sum_ops as f64 / t as f64,
2110 );
2111 }
2112
2113 let compressed: Vec<Vec<f64>> = vec![vec![0.0; prob.n]; n_colors];
2114
2115 Ok(Self {
2116 prob,
2117 obj_tapes,
2118 con_tapes,
2119 h_irow,
2120 h_jcol,
2121 jac_cols,
2122 jac_nnz,
2123 seeds,
2124 decoding,
2125 obj_tape_colors,
2126 con_tape_colors,
2127 final_x: None,
2128 final_obj: 0.0,
2129 scratch_row_grad: Vec::new(),
2130 vals_scratch: vec![0.0; max_tape_n],
2131 dot_scratch: vec![0.0; max_tape_n],
2132 adj_scratch: vec![0.0; max_tape_n],
2133 adj_dot_scratch: vec![0.0; max_tape_n],
2134 compressed,
2135 })
2136 }
2137
2138 pub fn final_x(&self) -> Option<&[Number]> {
2139 self.final_x.as_deref()
2140 }
2141
2142 pub fn final_obj(&self) -> Number {
2143 self.final_obj
2144 }
2145}
2146
2147impl pounce_nlp::expression_provider::ExpressionProvider for NlTnlp {
2148 fn constraint_expression(&self, i: usize) -> Option<pounce_nlp::FbbtTape> {
2153 let nonlinear = self.prob.con_nonlinear.get(i)?;
2154 let linear = self
2155 .prob
2156 .con_linear
2157 .get(i)
2158 .map(|v| v.as_slice())
2159 .unwrap_or(&[]);
2160 crate::nl_fbbt_translate::translate_constraint(nonlinear, linear)
2161 }
2162
2163 fn variable_name(&self, i: usize) -> Option<&str> {
2166 self.prob.var_names.get(i).map(String::as_str)
2167 }
2168
2169 fn constraint_name(&self, i: usize) -> Option<&str> {
2172 self.prob.con_names.get(i).map(String::as_str)
2173 }
2174}
2175
2176impl TNLP for NlTnlp {
2177 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2178 Some(NlpInfo {
2179 n: self.prob.n as Index,
2180 m: self.prob.m as Index,
2181 nnz_jac_g: self.jac_nnz as Index,
2182 nnz_h_lag: self.h_irow.len() as Index,
2183 index_style: IndexStyle::C,
2184 })
2185 }
2186
2187 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2188 b.x_l.copy_from_slice(&self.prob.x_l);
2189 b.x_u.copy_from_slice(&self.prob.x_u);
2190 if !self.prob.g_l.is_empty() {
2191 b.g_l.copy_from_slice(&self.prob.g_l);
2192 b.g_u.copy_from_slice(&self.prob.g_u);
2193 }
2194 true
2195 }
2196
2197 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2198 sp.x.copy_from_slice(&self.prob.x0);
2199 true
2200 }
2201
2202 fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
2203 let mut nl: Number = 0.0;
2204 for t in &self.obj_tapes {
2205 nl += t.eval(x);
2206 }
2207 let lin: Number = self.prob.obj_linear.iter().map(|(i, c)| c * x[*i]).sum();
2208 let v = self.prob.obj_constant + nl + lin;
2209 let signed = if self.prob.minimize { v } else { -v };
2210 Some(signed)
2211 }
2212
2213 fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
2214 grad.fill(0.0);
2215 for t in &self.obj_tapes {
2216 t.gradient_seed(x, 1.0, grad);
2217 }
2218 for (i, c) in &self.prob.obj_linear {
2219 grad[*i] += c;
2220 }
2221 if !self.prob.minimize {
2222 for g in grad.iter_mut() {
2223 *g = -*g;
2224 }
2225 }
2226 true
2227 }
2228
2229 fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2230 for i in 0..self.prob.m {
2231 let mut nl: Number = 0.0;
2232 for t in &self.con_tapes[i] {
2233 nl += t.eval(x);
2234 }
2235 let lin: Number = self.prob.con_linear[i].iter().map(|(j, c)| c * x[*j]).sum();
2236 g[i] = nl + lin;
2237 }
2238 true
2239 }
2240
2241 fn eval_jac_g(
2242 &mut self,
2243 x: Option<&[Number]>,
2244 _new_x: bool,
2245 mode: SparsityRequest<'_>,
2246 ) -> bool {
2247 match mode {
2248 SparsityRequest::Structure { irow, jcol } => {
2249 let mut k = 0;
2250 for i in 0..self.prob.m {
2251 for &j in &self.jac_cols[i] {
2252 irow[k] = i as Index;
2253 jcol[k] = j as Index;
2254 k += 1;
2255 }
2256 }
2257 true
2258 }
2259 SparsityRequest::Values { values } => {
2260 let n = self.prob.n;
2261 let xs = x.unwrap_or(&self.prob.x0);
2262 if self.scratch_row_grad.len() < n {
2263 self.scratch_row_grad.resize(n, 0.0);
2264 }
2265 let mut k = 0;
2266 for i in 0..self.prob.m {
2267 for &j in &self.jac_cols[i] {
2268 self.scratch_row_grad[j] = 0.0;
2269 }
2270 for t in &self.con_tapes[i] {
2271 t.gradient_seed(xs, 1.0, &mut self.scratch_row_grad);
2272 }
2273 for &(v, c) in &self.prob.con_linear[i] {
2274 self.scratch_row_grad[v] += c;
2275 }
2276 for &j in &self.jac_cols[i] {
2277 values[k] = self.scratch_row_grad[j];
2278 k += 1;
2279 }
2280 }
2281 true
2282 }
2283 }
2284 }
2285
2286 fn eval_h(
2287 &mut self,
2288 x: Option<&[Number]>,
2289 _new_x: bool,
2290 obj_factor: Number,
2291 lambda: Option<&[Number]>,
2292 _new_lambda: bool,
2293 mode: SparsityRequest<'_>,
2294 ) -> bool {
2295 match mode {
2296 SparsityRequest::Structure { irow, jcol } => {
2297 irow.copy_from_slice(&self.h_irow);
2298 jcol.copy_from_slice(&self.h_jcol);
2299 true
2300 }
2301 SparsityRequest::Values { values } => {
2302 let x = x.unwrap_or(&self.prob.x0);
2303 values.fill(0.0);
2304
2305 let obj_seed = if self.prob.minimize {
2306 obj_factor
2307 } else {
2308 -obj_factor
2309 };
2310 for buf in &mut self.compressed {
2319 buf.fill(0.0);
2320 }
2321
2322 if obj_seed != 0.0 {
2323 for (ti, t) in self.obj_tapes.iter().enumerate() {
2324 if t.ops.is_empty() {
2325 continue;
2326 }
2327 t.forward_into(x, &mut self.vals_scratch);
2328 for &c in &self.obj_tape_colors[ti] {
2329 t.hessian_directional(
2330 &self.vals_scratch,
2331 &self.seeds[c as usize],
2332 obj_seed,
2333 &mut self.compressed[c as usize],
2334 &mut self.dot_scratch,
2335 &mut self.adj_scratch,
2336 &mut self.adj_dot_scratch,
2337 );
2338 }
2339 }
2340 }
2341
2342 if let Some(lam) = lambda {
2343 for k in 0..self.prob.m {
2344 let w = lam[k];
2345 if w == 0.0 {
2346 continue;
2347 }
2348 for (ti, t) in self.con_tapes[k].iter().enumerate() {
2349 if t.ops.is_empty() {
2350 continue;
2351 }
2352 t.forward_into(x, &mut self.vals_scratch);
2353 for &c in &self.con_tape_colors[k][ti] {
2354 t.hessian_directional(
2355 &self.vals_scratch,
2356 &self.seeds[c as usize],
2357 w,
2358 &mut self.compressed[c as usize],
2359 &mut self.dot_scratch,
2360 &mut self.adj_scratch,
2361 &mut self.adj_dot_scratch,
2362 );
2363 }
2364 }
2365 }
2366 }
2367
2368 for (c, table) in self.decoding.iter().enumerate() {
2371 let comp = &self.compressed[c];
2372 for w in table {
2373 values[w.hess_idx as usize] += comp[w.row as usize];
2374 }
2375 }
2376 true
2377 }
2378 }
2379 }
2380
2381 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
2382 self.final_x = Some(sol.x.to_vec());
2383 self.final_obj = sol.obj_value;
2384 }
2385
2386 fn get_var_con_metadata(&mut self, var: &mut MetaData, con: &mut MetaData) -> bool {
2396 let mut any = false;
2397 if !self.prob.var_names.is_empty() {
2398 var.strings
2399 .insert(IDX_NAMES.to_string(), self.prob.var_names.clone());
2400 any = true;
2401 }
2402 if !self.prob.con_names.is_empty() {
2403 con.strings
2404 .insert(IDX_NAMES.to_string(), self.prob.con_names.clone());
2405 any = true;
2406 }
2407 any
2408 }
2409
2410 fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
2411 for (i, t) in types.iter_mut().enumerate() {
2415 *t = match &self.prob.con_nonlinear[i] {
2416 Expr::Const(c) if *c == 0.0 => Linearity::Linear,
2417 _ => Linearity::NonLinear,
2418 };
2419 }
2420 true
2421 }
2422}
2423
2424pub fn load_nl_as_tnlp(path: &Path) -> Result<Rc<RefCell<dyn TNLP>>, String> {
2426 let prob = read_nl_file(path)?;
2427 Ok(Rc::new(RefCell::new(NlTnlp::new(prob))))
2428}
2429
2430#[cfg(test)]
2431mod tests {
2432 use super::*;
2433
2434 const SIMPLE: &str = "g3 0 1 0
24522 0 1 0 0
24530 1
24540 0
24550 2 0
24560 0 0 1
24570 0 0 0 0
24580 0
24590 0
24600 0 0 0 0
2461O0 0
2462o0
2463o5
2464o1
2465v0
2466n1
2467n2
2468o5
2469o1
2470v1
2471n2
2472n2
2473b
24743
24753
2476";
2477
2478 #[test]
2479 fn parses_simple_quadratic() {
2480 let p = parse_nl_text(SIMPLE).expect("parse");
2481 assert_eq!(p.n, 2);
2482 assert_eq!(p.m, 0);
2483 assert_eq!(p.num_obj, 1);
2484 let f = eval_expr(&p.obj_nonlinear, &[0.0, 0.0]);
2486 assert!((f - 5.0).abs() < 1e-12);
2487 let f = eval_expr(&p.obj_nonlinear, &[1.0, 2.0]);
2489 assert!(f.abs() < 1e-12);
2490 }
2491
2492 #[test]
2493 fn gradient_matches_analytic() {
2494 let p = parse_nl_text(SIMPLE).expect("parse");
2495 let x = [0.5, 1.0];
2496 let mut g = [0.0_f64; 2];
2497 grad_expr(&p.obj_nonlinear, &x, 1.0, &mut g);
2498 assert!((g[0] - (-1.0)).abs() < 1e-12);
2501 assert!((g[1] - (-2.0)).abs() < 1e-12);
2502 }
2503
2504 const EQ_LIN: &str = "g3 0 1 0
25232 1 1 0 0
25240 1
25250 0
25260 2 0
25270 0 0 1
25280 0 0 0 0
25292 0
25300 0
25310 0 0 0 0
2532C0
2533n0
2534O0 0
2535o0
2536o5
2537v0
2538n2
2539o5
2540v1
2541n2
2542r
25434 1
2544b
25453
25463
2547k1
25482
2549J0 2
25500 1
25511 1
2552";
2553
2554 #[test]
2555 fn parses_constrained_problem() {
2556 let p = parse_nl_text(EQ_LIN).expect("parse");
2557 assert_eq!(p.n, 2);
2558 assert_eq!(p.m, 1);
2559 assert!((p.g_l[0] - 1.0).abs() < 1e-12);
2561 assert!((p.g_u[0] - 1.0).abs() < 1e-12);
2562 assert_eq!(p.con_linear[0], vec![(0, 1.0), (1, 1.0)]);
2564 }
2565
2566 #[test]
2567 fn constrained_tnlp_eval_g_jac_h() {
2568 let p = parse_nl_text(EQ_LIN).expect("parse");
2569 let mut t = NlTnlp::new(p);
2570 let info = t.get_nlp_info().unwrap();
2571 assert_eq!(info.m, 1);
2572 assert_eq!(info.nnz_jac_g, 2);
2573
2574 let mut g = [0.0_f64; 1];
2576 assert!(t.eval_g(&[0.3, 0.4], true, &mut g));
2577 assert!((g[0] - 0.7).abs() < 1e-12);
2578
2579 let mut irow = [0_i32; 2];
2581 let mut jcol = [0_i32; 2];
2582 assert!(t.eval_jac_g(
2583 None,
2584 true,
2585 SparsityRequest::Structure {
2586 irow: &mut irow,
2587 jcol: &mut jcol
2588 }
2589 ));
2590 assert_eq!(irow, [0, 0]);
2591 assert_eq!(jcol, [0, 1]);
2592
2593 let mut vals = [0.0_f64; 2];
2595 assert!(t.eval_jac_g(
2596 Some(&[0.3, 0.4]),
2597 true,
2598 SparsityRequest::Values { values: &mut vals }
2599 ));
2600 assert!((vals[0] - 1.0).abs() < 1e-12);
2601 assert!((vals[1] - 1.0).abs() < 1e-12);
2602
2603 assert_eq!(info.nnz_h_lag, 2);
2608 let mut hirow = [0_i32; 2];
2609 let mut hjcol = [0_i32; 2];
2610 assert!(t.eval_h(
2611 None,
2612 true,
2613 1.0,
2614 None,
2615 true,
2616 SparsityRequest::Structure {
2617 irow: &mut hirow,
2618 jcol: &mut hjcol
2619 }
2620 ));
2621 assert_eq!(hirow, [0, 1]);
2622 assert_eq!(hjcol, [0, 1]);
2623 let mut hvals = [0.0_f64; 2];
2624 assert!(t.eval_h(
2625 Some(&[0.3, 0.4]),
2626 true,
2627 1.0,
2628 Some(&[0.5]),
2629 true,
2630 SparsityRequest::Values { values: &mut hvals }
2631 ));
2632 assert!((hvals[0] - 2.0).abs() < 1e-12);
2633 assert!((hvals[1] - 2.0).abs() < 1e-12);
2634 }
2635
2636 const CSE_OBJ: &str = "g3 0 1 0
26402 0 1 0 0
26410 1
26420 0
26430 2 0
26440 0 0 1
26450 0 0 0 0
26460 0
26470 0
26480 1 0 0 0
2649V2 0 0
2650o0
2651v0
2652v1
2653O0 0
2654o0
2655o5
2656v2
2657n2
2658v2
2659b
26603
26613
2662";
2663
2664 #[test]
2665 fn parses_v_segment_cse() {
2666 let p = parse_nl_text(CSE_OBJ).expect("parse");
2667 assert_eq!(p.n, 2);
2668 let f = eval_expr(&p.obj_nonlinear, &[1.0, 2.0]);
2670 assert!((f - 12.0).abs() < 1e-12, "got {f}");
2671 let mut g = [0.0_f64; 2];
2673 grad_expr(&p.obj_nonlinear, &[1.0, 2.0], 1.0, &mut g);
2674 assert!((g[0] - 7.0).abs() < 1e-12, "g[0]={}", g[0]);
2675 assert!((g[1] - 7.0).abs() < 1e-12, "g[1]={}", g[1]);
2676 let mut vs = BTreeSet::new();
2678 collect_vars(&p.obj_nonlinear, &mut vs);
2679 assert_eq!(vs.into_iter().collect::<Vec<_>>(), vec![0, 1]);
2680 }
2681
2682 const WITH_SUFFIXES: &str = "g3 0 1 0
26881 0 1 0 0
26890 1
26900 0
26910 1 0
26920 0 0 1
26930 0 0 0 0
26940 0
26950 0
26960 0 0 0 0
2697O0 0
2698o5
2699o1
2700v0
2701n1
2702n2
2703b
27043
2705S0 1 sens_state_1
27060 7
2707S4 1 sens_state_value_1
27080 4.5
2709";
2710
2711 #[test]
2712 fn parses_var_int_and_var_real_suffixes() {
2713 let p = parse_nl_text(WITH_SUFFIXES).expect("parse");
2714 let v = p.suffixes.var_int.get("sens_state_1").expect("var_int");
2716 assert_eq!(v.as_slice(), &[7]);
2717 let r = p
2719 .suffixes
2720 .var_real
2721 .get("sens_state_value_1")
2722 .expect("var_real");
2723 assert_eq!(r.len(), 1);
2724 assert!((r[0] - 4.5).abs() < 1e-12);
2725 assert!(p.suffixes.con_int.is_empty());
2727 assert!(p.suffixes.con_real.is_empty());
2728 }
2729
2730 const WITH_CON_SUFFIX: &str = "g3 0 1 0
27332 2 1 0 0
27340 0
27350 0
27360 2 0
27370 0 0 1
27380 0 0 0 0
27392 0
27400 0
27410 0 0 0 0 0
2742C0
2743n0
2744C1
2745n0
2746O0 0
2747n0
2748r
27494 0.0
27504 0.0
2751b
27523
27533
2754k1
27550
2756J0 2
27570 1
27581 1
2759J1 2
27600 1
27611 -1
2762S1 2 sens_init_constr
27630 1
27641 2
2765";
2766
2767 #[test]
2768 fn parses_con_int_suffix() {
2769 let p = parse_nl_text(WITH_CON_SUFFIX).expect("parse");
2770 let s = p.suffixes.con_int.get("sens_init_constr").expect("con_int");
2771 assert_eq!(s.as_slice(), &[1, 2]);
2773 }
2774
2775 #[test]
2776 fn rejects_suffix_with_out_of_range_index() {
2777 let bad = WITH_CON_SUFFIX.replace("1 2\n", "5 2\n"); let err = parse_nl_text(&bad).expect_err("must reject");
2779 assert!(
2780 err.contains("out of range"),
2781 "expected out-of-range error, got: {err}"
2782 );
2783 }
2784
2785 #[test]
2786 fn tnlp_round_trip_solves() {
2787 let p = parse_nl_text(SIMPLE).expect("parse");
2788 let mut tnlp = NlTnlp::new(p);
2789 let info = tnlp.get_nlp_info().unwrap();
2790 assert_eq!(info.n, 2);
2791 assert_eq!(info.m, 0);
2792 let f0 = tnlp.eval_f(&[0.0, 0.0], true).unwrap();
2793 assert!((f0 - 5.0).abs() < 1e-12);
2794 let mut g = [0.0_f64; 2];
2795 tnlp.eval_grad_f(&[0.0, 0.0], true, &mut g);
2796 assert!((g[0] - (-2.0)).abs() < 1e-12);
2798 assert!((g[1] - (-4.0)).abs() < 1e-12);
2799 }
2800
2801 use pounce_nlp::expression_provider::ExpressionProvider;
2808 use std::sync::atomic::{AtomicUsize, Ordering};
2809
2810 fn scratch_dir(tag: &str) -> std::path::PathBuf {
2812 static N: AtomicUsize = AtomicUsize::new(0);
2813 let seq = N.fetch_add(1, Ordering::Relaxed);
2814 let dir = std::env::temp_dir().join(format!(
2815 "pounce_nlnames_{}_{}_{}",
2816 std::process::id(),
2817 tag,
2818 seq
2819 ));
2820 std::fs::create_dir_all(&dir).expect("create scratch dir");
2821 dir
2822 }
2823
2824 #[test]
2825 fn read_name_file_reads_in_order() {
2826 let dir = scratch_dir("col_order");
2827 let p = dir.join("m.col");
2828 std::fs::write(&p, "x_in\nT_reactor\nflow\n").unwrap();
2829 assert_eq!(read_name_file(&p, 3), vec!["x_in", "T_reactor", "flow"]);
2830 }
2831
2832 #[test]
2833 fn read_name_file_truncates_extra_lines() {
2834 let dir = scratch_dir("row_obj");
2838 let p = dir.join("m.row");
2839 std::fs::write(&p, "mass_balance\nenergy_balance\nobj\n").unwrap();
2840 assert_eq!(
2841 read_name_file(&p, 2),
2842 vec!["mass_balance", "energy_balance"]
2843 );
2844 }
2845
2846 #[test]
2847 fn read_name_file_empty_on_short_or_missing() {
2848 let dir = scratch_dir("short");
2849 let short = dir.join("m.col");
2850 std::fs::write(&short, "only_one\n").unwrap();
2851 assert!(read_name_file(&short, 3).is_empty());
2853 assert!(read_name_file(&dir.join("absent.col"), 2).is_empty());
2855 }
2856
2857 #[test]
2858 fn read_nl_file_captures_sibling_names() {
2859 let dir = scratch_dir("sibling");
2862 let nl = dir.join("m.nl");
2863 std::fs::write(&nl, SIMPLE).unwrap();
2864 std::fs::write(dir.join("m.col"), "alpha\nbeta\n").unwrap();
2865
2866 let prob = read_nl_file(&nl).expect("parse + name capture");
2867 assert_eq!(prob.var_names, vec!["alpha", "beta"]);
2868 assert!(prob.con_names.is_empty()); let tnlp = NlTnlp::new(prob);
2871 assert_eq!(tnlp.variable_name(0), Some("alpha"));
2872 assert_eq!(tnlp.variable_name(1), Some("beta"));
2873 assert_eq!(tnlp.variable_name(2), None); }
2875
2876 #[test]
2877 fn read_nl_file_without_names_yields_empty() {
2878 let dir = scratch_dir("noname");
2879 let nl = dir.join("m.nl");
2880 std::fs::write(&nl, SIMPLE).unwrap();
2881 let prob = read_nl_file(&nl).expect("parse");
2882 assert!(prob.var_names.is_empty());
2883 assert!(prob.con_names.is_empty());
2884 let tnlp = NlTnlp::new(prob);
2885 assert_eq!(tnlp.variable_name(0), None);
2886 }
2887
2888 fn names(v: &[&str]) -> Vec<String> {
2891 v.iter().map(|s| s.to_string()).collect()
2892 }
2893
2894 #[test]
2895 fn render_uses_variable_names_when_present() {
2896 let e = Expr::Binary(BinOp::Mul, Box::new(Expr::Var(0)), Box::new(Expr::Var(1)));
2897 assert_eq!(render_expr(&e, &names(&["T", "flow"]), &[]), "T*flow");
2898 assert_eq!(render_expr(&e, &[], &[]), "x[0]*x[1]");
2900 }
2901
2902 #[test]
2903 fn render_parenthesizes_by_precedence() {
2904 let sum = Expr::Binary(BinOp::Add, Box::new(Expr::Var(0)), Box::new(Expr::Var(1)));
2906 let e = Expr::Binary(BinOp::Mul, Box::new(sum), Box::new(Expr::Var(2)));
2907 assert_eq!(render_expr(&e, &[], &[]), "(x[0] + x[1])*x[2]");
2908
2909 let mul = Expr::Binary(BinOp::Mul, Box::new(Expr::Var(1)), Box::new(Expr::Var(2)));
2911 let e2 = Expr::Binary(BinOp::Add, Box::new(Expr::Var(0)), Box::new(mul));
2912 assert_eq!(render_expr(&e2, &[], &[]), "x[0] + x[1]*x[2]");
2913 }
2914
2915 #[test]
2916 fn render_subtraction_right_assoc_parens() {
2917 let inner = Expr::Binary(BinOp::Sub, Box::new(Expr::Var(1)), Box::new(Expr::Var(2)));
2919 let e = Expr::Binary(BinOp::Sub, Box::new(Expr::Var(0)), Box::new(inner));
2920 assert_eq!(render_expr(&e, &[], &[]), "x[0] - (x[1] - x[2])");
2921 }
2922
2923 #[test]
2924 fn render_functions_and_pow() {
2925 let sq = Expr::Binary(
2926 BinOp::Pow,
2927 Box::new(Expr::Var(0)),
2928 Box::new(Expr::Const(2.0)),
2929 );
2930 let e = Expr::Unary(UnaryOp::Exp, Box::new(sq));
2931 assert_eq!(render_expr(&e, &names(&["q"]), &[]), "exp(q^2)");
2932 }
2933
2934 #[test]
2935 fn render_linear_signs_are_tidy() {
2936 let lin = vec![(0usize, 1.0), (1, -2.0), (2, 1.0)];
2938 assert_eq!(render_linear(&lin, &names(&["a", "b", "c"])), "a - 2*b + c");
2939 }
2940
2941 #[test]
2942 fn render_linear_skips_zero_coefficients() {
2943 let lin = vec![(0usize, 1.0), (1, 0.0), (2, -3.0)];
2946 assert_eq!(render_linear(&lin, &names(&["a", "b", "c"])), "a - 3*c");
2947 let lin = vec![(0usize, 0.0), (1, 2.0)];
2949 assert_eq!(render_linear(&lin, &names(&["a", "b"])), "2*b");
2950 }
2951
2952 #[test]
2953 fn render_sum_folds_negative_terms() {
2954 let sq = |i| {
2956 Expr::Binary(
2957 BinOp::Pow,
2958 Box::new(Expr::Var(i)),
2959 Box::new(Expr::Const(2.0)),
2960 )
2961 };
2962 let neg = |i| {
2963 Expr::Binary(
2964 BinOp::Mul,
2965 Box::new(Expr::Const(-1.0)),
2966 Box::new(Expr::Var(i)),
2967 )
2968 };
2969 let e = Expr::Sum(vec![
2970 sq(0),
2971 neg(1),
2972 Expr::Unary(UnaryOp::Neg, Box::new(Expr::Var(2))),
2973 ]);
2974 assert_eq!(
2975 render_expr(&e, &names(&["a", "b", "c"]), &[]),
2976 "a^2 - 1*b - c"
2977 );
2978 }
2979
2980 #[test]
2981 fn render_constraint_equation_forms() {
2982 let mut prob = parse_nl_text(SIMPLE).unwrap();
2984 prob.n = 2;
2986 prob.m = 2;
2987 prob.var_names = names(&["mass_in", "mass_out"]);
2988 prob.con_names = names(&["balance", "window"]);
2989 prob.con_linear = vec![
2990 vec![(0, 1.0), (1, -1.0)], vec![(0, 1.0)], ];
2993 prob.con_nonlinear = vec![Expr::Const(0.0), Expr::Const(0.0)];
2994 prob.g_l = vec![0.0, 0.0];
2995 prob.g_u = vec![0.0, 500.0];
2996
2997 assert_eq!(
2998 render_constraint_equation(&prob, 0),
2999 "mass_in - mass_out = 0"
3000 );
3001 assert_eq!(render_constraint_equation(&prob, 1), "0 <= mass_in <= 500");
3002
3003 let all = render_all_constraint_equations(&prob);
3004 assert_eq!(all.len(), 2);
3005 assert_eq!(all[1], "0 <= mass_in <= 500");
3006 }
3007
3008 #[test]
3009 fn constraint_jacobian_sparsity_unions_linear_and_nonlinear() {
3010 let mut prob = parse_nl_text(SIMPLE).unwrap();
3011 prob.n = 3;
3012 prob.m = 2;
3013 prob.con_linear = vec![vec![(1, 4.0)], vec![(2, 1.0)]];
3016 prob.con_nonlinear = vec![
3017 Expr::Binary(BinOp::Mul, Box::new(Expr::Var(0)), Box::new(Expr::Var(2))),
3018 Expr::Const(0.0),
3019 ];
3020 prob.g_l = vec![0.0, 0.0];
3021 prob.g_u = vec![0.0, 0.0];
3022
3023 let (irow, jcol) = constraint_jacobian_sparsity(&prob);
3024 assert_eq!(irow, vec![0, 0, 0, 1]);
3026 assert_eq!(jcol, vec![0, 1, 2, 2]);
3027 }
3028}