1use pounce_convex::{ConeSpec, QpProblem, Triplet};
35use std::fmt;
36
37#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct ConeDecl {
41 pub kind: ConeKind,
42 pub dim: usize,
43 pub alpha: Option<f64>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ConeKind {
54 Free,
56 Zero,
58 Nonneg,
60 Nonpos,
62 Exp,
64 SecondOrder,
66 Pow,
69}
70
71impl ConeKind {
72 fn parse(tok: &str) -> Option<ConeKind> {
75 Some(match tok {
76 "F" => ConeKind::Free,
77 "L=" => ConeKind::Zero,
78 "L+" => ConeKind::Nonneg,
79 "L-" => ConeKind::Nonpos,
80 "EXP" => ConeKind::Exp,
81 "Q" => ConeKind::SecondOrder,
82 _ => return None,
83 })
84 }
85}
86
87#[derive(Debug, Clone)]
90pub struct CbfModel {
91 pub minimize: bool,
93 pub num_var: usize,
94 pub var_cones: Vec<ConeDecl>,
95 pub num_con: usize,
96 pub con_cones: Vec<ConeDecl>,
97 pub c: Vec<f64>,
99 pub c0: f64,
101 pub a: Vec<(usize, usize, f64)>,
103 pub b: Vec<f64>,
105 pub psdcon_dims: Vec<usize>,
109 pub hcoord: Vec<(usize, usize, usize, usize, f64)>,
113 pub dcoord: Vec<(usize, usize, usize, f64)>,
116}
117
118#[derive(Debug, Clone)]
123pub struct ConicProgram {
124 pub prob: QpProblem,
125 pub cones: Vec<ConeSpec>,
126 pub obj_constant: f64,
127}
128
129impl ConicProgram {
130 pub fn cbf_objective(&self, pounce_obj: f64, minimize: bool) -> f64 {
134 if minimize {
135 pounce_obj + self.obj_constant
136 } else {
137 -pounce_obj + self.obj_constant
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq)]
144pub enum CbfError {
145 Malformed(String),
147 UnsupportedCone(String),
149 BadExpDim(usize),
151}
152
153impl fmt::Display for CbfError {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 match self {
156 CbfError::Malformed(s) => write!(f, "malformed CBF: {s}"),
157 CbfError::UnsupportedCone(s) => write!(f, "unsupported CBF cone '{s}'"),
158 CbfError::BadExpDim(d) => write!(f, "EXP cone must have dim 3, got {d}"),
159 }
160 }
161}
162
163impl std::error::Error for CbfError {}
164
165struct Lines<'a> {
167 rows: Vec<&'a str>,
168 pos: usize,
169}
170
171impl<'a> Lines<'a> {
172 fn new(text: &'a str) -> Self {
173 let rows = text
174 .lines()
175 .map(str::trim)
176 .filter(|l| !l.is_empty() && !l.starts_with('#'))
177 .collect();
178 Lines { rows, pos: 0 }
179 }
180
181 fn next(&mut self) -> Option<&'a str> {
182 let row = self.rows.get(self.pos).copied();
183 if row.is_some() {
184 self.pos += 1;
185 }
186 row
187 }
188
189 fn require(&mut self, what: &str) -> Result<&'a str, CbfError> {
190 self.next()
191 .ok_or_else(|| CbfError::Malformed(format!("expected {what}, got end of file")))
192 }
193}
194
195fn parse_usize(tok: &str, what: &str) -> Result<usize, CbfError> {
196 tok.parse()
197 .map_err(|_| CbfError::Malformed(format!("expected integer for {what}, got '{tok}'")))
198}
199
200fn parse_f64(tok: &str, what: &str) -> Result<f64, CbfError> {
201 tok.parse()
202 .map_err(|_| CbfError::Malformed(format!("expected number for {what}, got '{tok}'")))
203}
204
205fn parse_cone_token(
210 tok: &str,
211 pow_params: &[Vec<f64>],
212) -> Result<(ConeKind, Option<f64>), CbfError> {
213 if let Some(rest) = tok.strip_prefix('@') {
214 let (idx, kind) = rest
216 .split_once(':')
217 .ok_or_else(|| CbfError::Malformed(format!("bad parametric cone '{tok}'")))?;
218 if kind != "POW" {
219 return Err(CbfError::UnsupportedCone(format!("@{idx}:{kind}")));
220 }
221 let k = parse_usize(idx, "POW reference index")?;
222 let params = pow_params
223 .get(k)
224 .ok_or_else(|| CbfError::Malformed(format!("POW references @{k}, not declared")))?;
225 if params.len() != 2 {
226 return Err(CbfError::UnsupportedCone(format!(
227 "POW with {} parameters (only the 3-D power cone, 2 parameters, is supported)",
228 params.len()
229 )));
230 }
231 let alpha = params[0] / (params[0] + params[1]);
232 Ok((ConeKind::Pow, Some(alpha)))
233 } else {
234 let kind =
235 ConeKind::parse(tok).ok_or_else(|| CbfError::UnsupportedCone(tok.to_string()))?;
236 Ok((kind, None))
237 }
238}
239
240fn parse_cone_block(
243 lines: &mut Lines,
244 what: &str,
245 pow_params: &[Vec<f64>],
246) -> Result<(usize, Vec<ConeDecl>), CbfError> {
247 let header = lines.require(what)?;
248 let mut it = header.split_whitespace();
249 let total = parse_usize(it.next().unwrap_or(""), &format!("{what} total"))?;
250 let k = parse_usize(it.next().unwrap_or(""), &format!("{what} cone count"))?;
251 let mut cones = Vec::with_capacity(k);
252 let mut sum = 0;
253 for _ in 0..k {
254 let line = lines.require(&format!("{what} cone"))?;
255 let mut t = line.split_whitespace();
256 let tok = t.next().unwrap_or("");
257 let (kind, alpha) = parse_cone_token(tok, pow_params)?;
258 let dim = parse_usize(t.next().unwrap_or(""), &format!("{what} cone dim"))?;
259 if kind == ConeKind::Exp && dim != 3 {
260 return Err(CbfError::BadExpDim(dim));
261 }
262 if kind == ConeKind::Pow && dim != 3 {
263 return Err(CbfError::Malformed(format!(
264 "{what}: only the 3-D power cone is supported, got POW dim {dim}"
265 )));
266 }
267 sum += dim;
268 cones.push(ConeDecl { kind, dim, alpha });
269 }
270 if sum != total {
271 return Err(CbfError::Malformed(format!(
272 "{what} cone dims sum to {sum}, header says {total}"
273 )));
274 }
275 Ok((total, cones))
276}
277
278pub fn parse(text: &str) -> Result<CbfModel, CbfError> {
281 let mut lines = Lines::new(text);
282
283 let mut minimize = true;
284 let mut num_var = 0usize;
285 let mut var_cones = Vec::new();
286 let mut num_con = 0usize;
287 let mut con_cones = Vec::new();
288 let mut c = Vec::new();
289 let mut c0 = 0.0;
290 let mut a = Vec::new();
291 let mut b = Vec::new();
292 let mut pow_params: Vec<Vec<f64>> = Vec::new();
293 let mut psdcon_dims: Vec<usize> = Vec::new();
294 let mut hcoord: Vec<(usize, usize, usize, usize, f64)> = Vec::new();
295 let mut dcoord: Vec<(usize, usize, usize, f64)> = Vec::new();
296 let mut seen_var = false;
297
298 while let Some(kw) = lines.next() {
299 match kw {
300 "VER" => {
301 lines.require("VER value")?;
302 }
303 "POWCONES" => {
307 let header = lines.require("POWCONES header")?;
308 let mut it = header.split_whitespace();
309 let ncones = parse_usize(it.next().unwrap_or(""), "POWCONES count")?;
310 let _total = parse_usize(it.next().unwrap_or(""), "POWCONES total")?;
311 for _ in 0..ncones {
312 let len = parse_usize(lines.require("POWCONES cone length")?, "POWCONES len")?;
313 let mut params = Vec::with_capacity(len);
314 for _ in 0..len {
315 params.push(parse_f64(
316 lines.require("POWCONES alpha")?,
317 "POWCONES alpha",
318 )?);
319 }
320 pow_params.push(params);
321 }
322 }
323 "PSDCON" => {
326 let count = parse_usize(lines.require("PSDCON count")?, "PSDCON count")?;
327 for _ in 0..count {
328 psdcon_dims.push(parse_usize(lines.require("PSDCON dim")?, "PSDCON dim")?);
329 }
330 }
331 "HCOORD" => {
333 let nnz = parse_usize(lines.require("HCOORD nnz")?, "HCOORD nnz")?;
334 for _ in 0..nnz {
335 let line = lines.require("HCOORD entry")?;
336 let mut t = line.split_whitespace();
337 let con = parse_usize(t.next().unwrap_or(""), "HCOORD con")?;
338 let var = parse_usize(t.next().unwrap_or(""), "HCOORD var")?;
339 let i = parse_usize(t.next().unwrap_or(""), "HCOORD i")?;
340 let j = parse_usize(t.next().unwrap_or(""), "HCOORD j")?;
341 let val = parse_f64(t.next().unwrap_or(""), "HCOORD val")?;
342 hcoord.push((con, var, i, j, val));
343 }
344 }
345 "DCOORD" => {
347 let nnz = parse_usize(lines.require("DCOORD nnz")?, "DCOORD nnz")?;
348 for _ in 0..nnz {
349 let line = lines.require("DCOORD entry")?;
350 let mut t = line.split_whitespace();
351 let con = parse_usize(t.next().unwrap_or(""), "DCOORD con")?;
352 let i = parse_usize(t.next().unwrap_or(""), "DCOORD i")?;
353 let j = parse_usize(t.next().unwrap_or(""), "DCOORD j")?;
354 let val = parse_f64(t.next().unwrap_or(""), "DCOORD val")?;
355 dcoord.push((con, i, j, val));
356 }
357 }
358 "OBJSENSE" => {
359 let s = lines.require("OBJSENSE value")?;
360 minimize = match s {
361 "MIN" => true,
362 "MAX" => false,
363 other => {
364 return Err(CbfError::Malformed(format!("bad OBJSENSE '{other}'")));
365 }
366 };
367 }
368 "VAR" => {
369 let (n, cones) = parse_cone_block(&mut lines, "VAR", &pow_params)?;
370 num_var = n;
371 var_cones = cones;
372 c = vec![0.0; n];
373 seen_var = true;
374 }
375 "CON" => {
376 let (m, cones) = parse_cone_block(&mut lines, "CON", &pow_params)?;
377 num_con = m;
378 con_cones = cones;
379 b = vec![0.0; m];
380 }
381 "OBJACOORD" => {
382 if !seen_var {
383 return Err(CbfError::Malformed("OBJACOORD before VAR".into()));
384 }
385 let nnz = parse_usize(lines.require("OBJACOORD nnz")?, "OBJACOORD nnz")?;
386 for _ in 0..nnz {
387 let line = lines.require("OBJACOORD entry")?;
388 let mut t = line.split_whitespace();
389 let col = parse_usize(t.next().unwrap_or(""), "OBJACOORD col")?;
390 let val = parse_f64(t.next().unwrap_or(""), "OBJACOORD val")?;
391 if col >= num_var {
392 return Err(CbfError::Malformed(format!("OBJACOORD col {col} ≥ n")));
393 }
394 c[col] += val;
395 }
396 }
397 "OBJBCOORD" => {
398 c0 = parse_f64(lines.require("OBJBCOORD value")?, "OBJBCOORD")?;
399 }
400 "ACOORD" => {
401 let nnz = parse_usize(lines.require("ACOORD nnz")?, "ACOORD nnz")?;
402 a.reserve(nnz);
403 for _ in 0..nnz {
404 let line = lines.require("ACOORD entry")?;
405 let mut t = line.split_whitespace();
406 let row = parse_usize(t.next().unwrap_or(""), "ACOORD row")?;
407 let col = parse_usize(t.next().unwrap_or(""), "ACOORD col")?;
408 let val = parse_f64(t.next().unwrap_or(""), "ACOORD val")?;
409 a.push((row, col, val));
410 }
411 }
412 "BCOORD" => {
413 if b.is_empty() && num_con > 0 {
414 b = vec![0.0; num_con];
415 }
416 let nnz = parse_usize(lines.require("BCOORD nnz")?, "BCOORD nnz")?;
417 for _ in 0..nnz {
418 let line = lines.require("BCOORD entry")?;
419 let mut t = line.split_whitespace();
420 let row = parse_usize(t.next().unwrap_or(""), "BCOORD row")?;
421 let val = parse_f64(t.next().unwrap_or(""), "BCOORD val")?;
422 if row >= num_con {
423 return Err(CbfError::Malformed(format!("BCOORD row {row} ≥ m")));
424 }
425 b[row] += val;
426 }
427 }
428 "INT" => {
431 let nnz = parse_usize(lines.require("INT count")?, "INT count")?;
432 for _ in 0..nnz {
433 lines.require("INT entry")?;
434 }
435 }
436 other => {
437 return Err(CbfError::UnsupportedCone(format!("section '{other}'")));
438 }
439 }
440 }
441
442 if !seen_var {
443 return Err(CbfError::Malformed("no VAR section".into()));
444 }
445
446 Ok(CbfModel {
447 minimize,
448 num_var,
449 var_cones,
450 num_con,
451 con_cones,
452 c,
453 c0,
454 a,
455 b,
456 psdcon_dims,
457 hcoord,
458 dcoord,
459 })
460}
461
462impl CbfModel {
463 fn rows_of_a(&self) -> Vec<Vec<(usize, f64)>> {
466 let mut rows = vec![Vec::new(); self.num_con];
467 for &(r, col, val) in &self.a {
468 rows[r].push((col, val));
469 }
470 rows
471 }
472
473 pub fn to_conic(&self) -> Result<ConicProgram, CbfError> {
479 let n = self.num_var;
480 let a_rows = self.rows_of_a();
481
482 let mut g: Vec<Triplet> = Vec::new();
483 let mut h: Vec<f64> = Vec::new();
484 let mut cones: Vec<ConeSpec> = Vec::new();
485 let mut a_eq: Vec<Triplet> = Vec::new();
486 let mut b_eq: Vec<f64> = Vec::new();
487
488 let push_row =
492 |g: &mut Vec<Triplet>, h: &mut Vec<f64>, coeffs: &[(usize, f64)], constant: f64| {
493 let r = h.len();
494 for &(col, val) in coeffs {
495 g.push(Triplet::new(r, col, -val));
496 }
497 h.push(constant);
498 };
499
500 let mut v = 0usize; for cone in &self.var_cones {
503 match cone.kind {
504 ConeKind::Free => {}
505 ConeKind::Nonneg => {
506 for j in 0..cone.dim {
507 push_row(&mut g, &mut h, &[(v + j, 1.0)], 0.0);
508 }
509 cones.push(ConeSpec::Nonneg(cone.dim));
510 }
511 ConeKind::Nonpos => {
512 for j in 0..cone.dim {
514 push_row(&mut g, &mut h, &[(v + j, -1.0)], 0.0);
515 }
516 cones.push(ConeSpec::Nonneg(cone.dim));
517 }
518 ConeKind::SecondOrder => {
519 for j in 0..cone.dim {
520 push_row(&mut g, &mut h, &[(v + j, 1.0)], 0.0);
521 }
522 cones.push(ConeSpec::SecondOrder(cone.dim));
523 }
524 ConeKind::Exp => {
525 for j in (0..3).rev() {
527 push_row(&mut g, &mut h, &[(v + j, 1.0)], 0.0);
528 }
529 cones.push(ConeSpec::Exponential);
530 }
531 ConeKind::Pow => {
532 let alpha = cone.alpha.ok_or_else(|| {
536 CbfError::Malformed("POW cone missing its exponent".into())
537 })?;
538 push_row(&mut g, &mut h, &[(v + 2, 1.0)], 0.0); push_row(&mut g, &mut h, &[(v, 1.0)], 0.0); push_row(&mut g, &mut h, &[(v + 1, 1.0)], 0.0); cones.push(ConeSpec::Power(alpha));
542 }
543 ConeKind::Zero => {
544 for j in 0..cone.dim {
546 a_eq.push(Triplet::new(b_eq.len(), v + j, 1.0));
547 b_eq.push(0.0);
548 }
549 }
550 }
551 v += cone.dim;
552 }
553
554 let mut r = 0usize; for cone in &self.con_cones {
557 match cone.kind {
558 ConeKind::Zero => {
559 for i in 0..cone.dim {
561 let row = r + i;
562 for &(col, val) in &a_rows[row] {
563 a_eq.push(Triplet::new(b_eq.len(), col, val));
564 }
565 b_eq.push(-self.b[row]);
566 }
567 }
568 ConeKind::Nonneg => {
569 for i in 0..cone.dim {
571 let row = r + i;
572 push_row(&mut g, &mut h, &a_rows[row], self.b[row]);
573 }
574 cones.push(ConeSpec::Nonneg(cone.dim));
575 }
576 ConeKind::Nonpos => {
577 for i in 0..cone.dim {
579 let row = r + i;
580 let neg: Vec<(usize, f64)> =
581 a_rows[row].iter().map(|&(c, v)| (c, -v)).collect();
582 push_row(&mut g, &mut h, &neg, -self.b[row]);
583 }
584 cones.push(ConeSpec::Nonneg(cone.dim));
585 }
586 ConeKind::SecondOrder => {
587 for i in 0..cone.dim {
588 let row = r + i;
589 push_row(&mut g, &mut h, &a_rows[row], self.b[row]);
590 }
591 cones.push(ConeSpec::SecondOrder(cone.dim));
592 }
593 ConeKind::Exp => {
594 for i in (0..3).rev() {
596 let row = r + i;
597 push_row(&mut g, &mut h, &a_rows[row], self.b[row]);
598 }
599 cones.push(ConeSpec::Exponential);
600 }
601 ConeKind::Pow => {
602 let alpha = cone.alpha.ok_or_else(|| {
604 CbfError::Malformed("POW cone missing its exponent".into())
605 })?;
606 for &i in &[2usize, 0, 1] {
607 let row = r + i;
608 push_row(&mut g, &mut h, &a_rows[row], self.b[row]);
609 }
610 cones.push(ConeSpec::Power(alpha));
611 }
612 ConeKind::Free => {} }
614 r += cone.dim;
615 }
616
617 if !self.psdcon_dims.is_empty() {
622 use std::collections::HashMap;
623 let r2 = std::f64::consts::SQRT_2;
624 let mut h_by: HashMap<(usize, usize, usize), Vec<(usize, f64)>> = HashMap::new();
625 for &(con, var, i, j, val) in &self.hcoord {
626 h_by.entry((con, i, j)).or_default().push((var, val));
627 }
628 let mut d_by: HashMap<(usize, usize, usize), f64> = HashMap::new();
629 for &(con, i, j, val) in &self.dcoord {
630 *d_by.entry((con, i, j)).or_insert(0.0) += val;
631 }
632 for (con, &dim) in self.psdcon_dims.iter().enumerate() {
633 for j in 0..dim {
635 for i in j..dim {
636 let scale = if i == j { 1.0 } else { r2 };
637 let constant = scale * d_by.get(&(con, i, j)).copied().unwrap_or(0.0);
638 let coeffs: Vec<(usize, f64)> = h_by
639 .get(&(con, i, j))
640 .map(|v| v.iter().map(|&(var, val)| (var, scale * val)).collect())
641 .unwrap_or_default();
642 push_row(&mut g, &mut h, &coeffs, constant);
643 }
644 }
645 cones.push(ConeSpec::Psd(dim));
646 }
647 }
648
649 let c: Vec<f64> = if self.minimize {
651 self.c.clone()
652 } else {
653 self.c.iter().map(|v| -v).collect()
654 };
655
656 let prob = QpProblem {
657 n,
658 p_lower: Vec::new(),
659 c,
660 a: a_eq,
661 b: b_eq,
662 g,
663 h,
664 lb: Vec::new(),
665 ub: Vec::new(),
666 };
667 Ok(ConicProgram {
668 prob,
669 cones,
670 obj_constant: self.c0,
671 })
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 const TINY_GP: &str = "\
680VER
6812
682
683OBJSENSE
684MIN
685
686VAR
6874 2
688F 1
689EXP 3
690
691CON
6921 1
693L= 1
694
695OBJACOORD
6961
6970 1.0
698
699ACOORD
7002
7010 1 1.0
7020 3 -1.0
703
704BCOORD
7051
7060 -2.0
707";
708
709 #[test]
710 fn parses_sections() {
711 let m = parse(TINY_GP).unwrap();
712 assert!(m.minimize);
713 assert_eq!(m.num_var, 4);
714 assert_eq!(m.var_cones.len(), 2);
715 assert_eq!(m.var_cones[0].kind, ConeKind::Free);
716 assert_eq!(m.var_cones[1].kind, ConeKind::Exp);
717 assert_eq!(m.num_con, 1);
718 assert_eq!(m.con_cones[0].kind, ConeKind::Zero);
719 assert_eq!(m.c, vec![1.0, 0.0, 0.0, 0.0]);
720 assert_eq!(m.b, vec![-2.0]);
721 assert_eq!(m.a.len(), 2);
722 }
723
724 #[test]
725 fn rejects_bad_exp_dim() {
726 let bad = TINY_GP.replace("EXP 3", "EXP 2");
727 assert!(matches!(parse(&bad), Err(CbfError::BadExpDim(2))));
728 }
729
730 #[test]
731 fn rejects_unsupported_cone() {
732 let bad = TINY_GP.replace("EXP 3", "POW 3");
733 assert!(matches!(parse(&bad), Err(CbfError::UnsupportedCone(_))));
734 }
735
736 #[test]
737 fn cone_dim_sum_is_checked() {
738 let bad = TINY_GP.replace("4 2", "5 2");
739 assert!(matches!(parse(&bad), Err(CbfError::Malformed(_))));
740 }
741
742 #[test]
743 fn to_conic_builds_exp_and_equality() {
744 let m = parse(TINY_GP).unwrap();
745 let cp = m.to_conic().unwrap();
746 assert_eq!(cp.cones, vec![ConeSpec::Exponential]);
748 assert_eq!(cp.prob.m_eq(), 1); assert_eq!(cp.prob.m_ineq(), 3); assert_eq!(cp.obj_constant, 0.0);
751 let row0: Vec<_> = cp.prob.g.iter().filter(|t| t.row == 0).collect();
755 assert_eq!(row0.len(), 1);
756 assert_eq!(row0[0].col, 3);
757 }
758
759 const TINY_POW: &str = "\
760VER
7612
762
763OBJSENSE
764MAX
765
766POWCONES
7671 2
7682
7693.0
7701.0
771
772VAR
7733 1
774@0:POW 3
775
776CON
7770 0
778
779OBJACOORD
7801
7812 1.0
782";
783
784 #[test]
785 fn parses_powcones_and_resolves_alpha() {
786 let m = parse(TINY_POW).unwrap();
787 assert_eq!(m.var_cones.len(), 1);
788 assert_eq!(m.var_cones[0].kind, ConeKind::Pow);
789 let a = m.var_cones[0].alpha.unwrap();
791 assert!((a - 0.75).abs() < 1e-12, "alpha {a}");
792 }
793
794 #[test]
795 fn to_conic_builds_power_cone_with_permutation() {
796 let m = parse(TINY_POW).unwrap();
797 let cp = m.to_conic().unwrap();
798 assert_eq!(cp.cones, vec![ConeSpec::Power(0.75)]);
799 assert_eq!(cp.prob.m_ineq(), 3); let row0: Vec<_> = cp.prob.g.iter().filter(|t| t.row == 0).collect();
802 assert_eq!(row0[0].col, 2);
803 let row1: Vec<_> = cp.prob.g.iter().filter(|t| t.row == 1).collect();
804 assert_eq!(row1[0].col, 0);
805 let row2: Vec<_> = cp.prob.g.iter().filter(|t| t.row == 2).collect();
806 assert_eq!(row2[0].col, 1);
807 }
808
809 #[test]
810 fn pow_reference_to_undeclared_set_errors() {
811 let bad = TINY_POW.replace("@0:POW", "@5:POW");
812 assert!(matches!(parse(&bad), Err(CbfError::Malformed(_))));
813 }
814
815 const TINY_SDP: &str = "\
816VER
8172
818
819OBJSENSE
820MAX
821
822VAR
8231 1
824F 1
825
826PSDCON
8271
8282
829
830OBJACOORD
8311
8320 1.0
833
834HCOORD
8352
8360 0 0 0 -1.0
8370 0 1 1 -1.0
838
839DCOORD
8402
8410 0 0 2.0
8420 1 1 5.0
843";
844
845 #[test]
846 fn parses_psdcon_hcoord_dcoord() {
847 let m = parse(TINY_SDP).unwrap();
848 assert_eq!(m.psdcon_dims, vec![2]);
849 assert_eq!(m.hcoord.len(), 2);
850 assert_eq!(m.dcoord.len(), 2);
851 }
852
853 #[test]
854 fn to_conic_builds_psd_constraint() {
855 let m = parse(TINY_SDP).unwrap();
856 let cp = m.to_conic().unwrap();
857 assert_eq!(cp.cones, vec![ConeSpec::Psd(2)]);
859 assert_eq!(cp.prob.m_ineq(), 3);
860 assert_eq!(cp.prob.h, vec![2.0, 0.0, 5.0]);
863 let row0: Vec<_> = cp.prob.g.iter().filter(|t| t.row == 0).collect();
864 assert_eq!(row0.len(), 1);
865 assert!((row0[0].val - 1.0).abs() < 1e-12); }
867}