1use std::{
2 fmt,
3 time::{Duration, Instant},
4};
5
6mod cddlib;
7mod howzat_common;
8mod howzat_dd;
9mod howzat_lrs;
10mod lrslib;
11mod ppl;
12
13use anyhow::{anyhow, ensure};
14use calculo::num::Num;
15use howzat::dd::ConeOptions;
16use hullabaloo::AdjacencyList;
17use hullabaloo::set_family::{ListFamily, SetFamily};
18use hullabaloo::types::AdjacencyOutput;
19use serde::{Deserialize, Serialize};
20
21use crate::inequalities::{
22 HowzatInequalities, RowMajorInequalities, RowMajorInequalitiesI64, InequalitiesF64,
23 InequalitiesI64,
24};
25use crate::vertices::{
26 HomogeneousGeneratorRowsF64, HomogeneousGeneratorRowsI64, HowzatVertices,
27 RowMajorHomogeneousGenerators, RowMajorHomogeneousGeneratorsI64, RowMajorVertices,
28 RowMajorVerticesI64, VerticesF64, VerticesI64,
29};
30
31use howzat_dd::{
32 DEFAULT_HOWZAT_DD_PIPELINE, HowzatDdPipelineSpec, HowzatDdPurifierSpec, HowzatDdUmpire,
33 parse_howzat_dd_pipeline, parse_howzat_dd_purifier,
34};
35
36#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
37enum BackendSpec {
38 CddlibF64,
39 CddlibGmpFloat,
40 CddlibGmpRational,
41 CddlibHlblF64,
42 CddlibHlblGmpFloat,
43 CddlibHlblGmpRational,
44 HowzatDd {
45 umpire: HowzatDdUmpire,
46 purifier: Option<HowzatDdPurifierSpec>,
47 pipeline: HowzatDdPipelineSpec,
48 },
49 HowzatLrsRug,
50 HowzatLrsDashu,
51 LrslibHlblGmpInt,
52 PplHlblGmpInt,
53}
54
55#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
56enum RequestedAdjacency {
57 Default,
58 Dense,
59 Sparse,
60}
61
62impl RequestedAdjacency {
63 fn token(self) -> Option<&'static str> {
64 match self {
65 Self::Default => None,
66 Self::Dense => Some("dense"),
67 Self::Sparse => Some("sparse"),
68 }
69 }
70}
71
72impl BackendSpec {
73 fn supports_dense_adjacency(&self) -> bool {
74 !matches!(
75 self,
76 Self::CddlibF64
77 | Self::CddlibGmpFloat
78 | Self::CddlibGmpRational
79 | Self::CddlibHlblF64
80 | Self::CddlibHlblGmpFloat
81 | Self::CddlibHlblGmpRational
82 )
83 }
84
85 fn supports_sparse_adjacency(&self) -> bool {
86 true
87 }
88
89 fn fmt_kind(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::CddlibF64 | Self::CddlibGmpFloat | Self::CddlibGmpRational => f.write_str("cddlib"),
92 Self::CddlibHlblF64 | Self::CddlibHlblGmpFloat | Self::CddlibHlblGmpRational => {
93 f.write_str("cddlib+hlbl")
94 }
95 Self::HowzatDd { umpire, .. } => {
96 f.write_str("howzat-dd")?;
97 if let Some(token) = umpire.canonical_token() {
98 write!(f, "@{token}")?;
99 }
100 Ok(())
101 }
102 Self::HowzatLrsRug | Self::HowzatLrsDashu => f.write_str("howzat-lrs"),
103 Self::LrslibHlblGmpInt => f.write_str("lrslib+hlbl"),
104 Self::PplHlblGmpInt => f.write_str("ppl+hlbl"),
105 }
106 }
107
108 fn fmt_num(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 match self {
110 Self::CddlibF64 | Self::CddlibHlblF64 => f.write_str("f64"),
111 Self::CddlibGmpFloat | Self::CddlibHlblGmpFloat => f.write_str("gmpfloat"),
112 Self::CddlibGmpRational | Self::CddlibHlblGmpRational => f.write_str("gmprational"),
113 Self::HowzatDd { pipeline, .. } => f.write_str(&pipeline.canonical()),
114 Self::HowzatLrsRug => f.write_str("rug"),
115 Self::HowzatLrsDashu => f.write_str("dashu"),
116 Self::LrslibHlblGmpInt => f.write_str("gmpint"),
117 Self::PplHlblGmpInt => f.write_str("gmpint"),
118 }
119 }
120}
121
122impl fmt::Display for BackendSpec {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 self.fmt_kind(f)?;
125 f.write_str(":")?;
126 self.fmt_num(f)
127 }
128}
129
130impl std::str::FromStr for BackendSpec {
131 type Err = String;
132
133 fn from_str(value: &str) -> Result<Self, Self::Err> {
134 let raw = value.trim();
135 if raw.is_empty() {
136 return Err("backend spec cannot be empty".to_string());
137 }
138
139 let raw = raw.to_ascii_lowercase();
140
141 let (kind, num) = raw
142 .split_once(':')
143 .map(|(k, n)| (k.trim(), Some(n.trim())))
144 .unwrap_or((raw.trim(), None));
145
146 let (kind, umpire) = if let Some((base, selector)) = kind.split_once('@') {
147 if selector.contains('@') {
148 return Err(format!(
149 "backend spec '{value}' contains multiple '@' selectors (expected e.g. howzat-dd@sp:...)"
150 ));
151 }
152 let base = base.trim();
153 let selector = selector.trim();
154 if base.is_empty() || selector.is_empty() {
155 return Err(format!(
156 "backend spec '{value}' has an invalid '@' selector (expected howzat-dd@sp or howzat-dd@int)"
157 ));
158 }
159 let umpire = match selector {
160 "int" => HowzatDdUmpire::Int,
161 "sp" => HowzatDdUmpire::Sp,
162 _ => {
163 return Err(format!(
164 "backend spec '{value}' has an unknown umpire selector '@{selector}' (expected '@int' or '@sp')"
165 ));
166 }
167 };
168 (base, umpire)
169 } else {
170 (kind, HowzatDdUmpire::Default)
171 };
172
173 let spec = match (kind, num) {
174 ("cddlib", None | Some("") | Some("gmprational")) => Self::CddlibGmpRational,
175 ("cddlib", Some("f64")) => Self::CddlibF64,
176 ("cddlib", Some("gmpfloat")) => Self::CddlibGmpFloat,
177 ("cddlib+hlbl", None | Some("") | Some("gmprational")) => Self::CddlibHlblGmpRational,
178 ("cddlib+hlbl", Some("f64")) => Self::CddlibHlblF64,
179 ("cddlib+hlbl", Some("gmpfloat")) => Self::CddlibHlblGmpFloat,
180 ("howzat-dd", None | Some("")) => Self::HowzatDd {
181 umpire,
182 purifier: None,
183 pipeline: parse_howzat_dd_pipeline(DEFAULT_HOWZAT_DD_PIPELINE)?,
184 },
185 ("howzat-dd", Some(spec)) => Self::HowzatDd {
186 umpire,
187 purifier: None,
188 pipeline: parse_howzat_dd_pipeline(spec)?,
189 },
190 ("howzat-lrs", None | Some("") | Some("rug")) => Self::HowzatLrsRug,
191 ("howzat-lrs", Some("dashu")) => Self::HowzatLrsDashu,
192 ("lrslib+hlbl", None | Some("") | Some("gmpint")) => Self::LrslibHlblGmpInt,
193 ("ppl+hlbl", None | Some("") | Some("gmpint")) => Self::PplHlblGmpInt,
194 _ => {
195 return Err(format!(
196 "unknown backend spec '{value}' (see --help for supported values)"
197 ));
198 }
199 };
200
201 if umpire != HowzatDdUmpire::Default && !matches!(spec, Self::HowzatDd { .. }) {
202 return Err(format!(
203 "backend spec '{value}' does not support '@{token}' (only howzat-dd does)",
204 token = umpire
205 .canonical_token()
206 .expect("selector is not Default")
207 ));
208 }
209
210 Ok(spec)
211 }
212}
213
214#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
215pub struct Backend(BackendSpec, RequestedAdjacency);
216
217#[derive(Copy, Clone, Debug, Eq, PartialEq)]
218pub enum BackendAdjacencyMode {
219 Dense,
220 Sparse,
221}
222
223#[derive(Copy, Clone, Debug, Eq, PartialEq)]
224pub enum Representation {
225 EuclideanVertices,
226 HomogeneousGenerators,
227 Inequality,
228}
229
230impl serde::Serialize for Backend {
231 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
232 where
233 S: serde::Serializer,
234 {
235 serializer.serialize_str(&self.to_string())
236 }
237}
238
239impl<'de> serde::Deserialize<'de> for Backend {
240 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241 where
242 D: serde::Deserializer<'de>,
243 {
244 let spec = String::deserialize(deserializer)?;
245 Self::parse(&spec).map_err(serde::de::Error::custom)
246 }
247}
248
249impl Backend {
250 pub fn parse(spec: &str) -> Result<Self, String> {
251 spec.parse()
252 }
253
254 pub fn adjacency_mode(
255 &self,
256 vertex_count: usize,
257 dim: usize,
258 output_adjacency: bool,
259 ) -> BackendAdjacencyMode {
260 match self.choose_adjacency(vertex_count, dim, output_adjacency) {
261 RequestedAdjacency::Dense => BackendAdjacencyMode::Dense,
262 RequestedAdjacency::Sparse => BackendAdjacencyMode::Sparse,
263 RequestedAdjacency::Default => unreachable!("choose_adjacency never returns Default"),
264 }
265 }
266
267 pub fn minimum_output_level(&self) -> BackendOutputLevel {
268 let (minimum, _) = match &self.0 {
269 BackendSpec::HowzatDd { pipeline, .. } => {
270 if pipeline.has_checks() {
271 (
272 BackendOutputLevel::Incidence,
273 Some("howzat-dd pipeline contains Check steps that require incidence"),
274 )
275 } else {
276 (BackendOutputLevel::Representation, None)
277 }
278 }
279 BackendSpec::HowzatLrsRug | BackendSpec::HowzatLrsDashu => (
280 BackendOutputLevel::Incidence,
281 Some("howzat-lrs always computes an incidence certificate before exact resolution"),
282 ),
283 BackendSpec::LrslibHlblGmpInt => (
284 BackendOutputLevel::Incidence,
285 Some("lrslib solve() always produces incidence"),
286 ),
287 BackendSpec::PplHlblGmpInt => (
288 BackendOutputLevel::Incidence,
289 Some("ppl solve() always produces incidence"),
290 ),
291 _ => (BackendOutputLevel::Representation, None),
292 };
293
294 minimum
295 }
296
297 const DEFAULT_DENSE_ADJACENCY_LIMIT_BYTES: u128 = 128 * 1024 * 1024;
298 const DEFAULT_DENSE_ADJACENCY_LIMIT_NODES: u128 = 32768;
299
300 fn choose_adjacency(
301 &self,
302 vertex_count: usize,
303 dim: usize,
304 output_adjacency: bool,
305 ) -> RequestedAdjacency {
306 match self.1 {
307 RequestedAdjacency::Dense => RequestedAdjacency::Dense,
308 RequestedAdjacency::Sparse => RequestedAdjacency::Sparse,
309 RequestedAdjacency::Default => {
310 if !self.0.supports_dense_adjacency() {
311 return RequestedAdjacency::Sparse;
312 }
313
314 fn dense_graph_bytes(node_count: u128) -> u128 {
315 node_count.saturating_mul(node_count).saturating_add(7) / 8
316 }
317
318 fn binom_capped(n: u128, k: u128, cap: u128) -> u128 {
319 if k > n {
320 return 0;
321 }
322 let k = k.min(n - k);
323 if k == 0 {
324 return 1;
325 }
326
327 let mut result = 1u128;
328 for i in 1..=k {
329 let numerator = n - k + i;
330 let Some(product) = result.checked_mul(numerator) else {
331 return cap + 1;
332 };
333 result = product / i;
334 if result > cap {
335 return cap + 1;
336 }
337 }
338 result
339 }
340
341 fn cyclic_facets_capped(vertex_count: usize, dim: usize, cap: u128) -> u128 {
342 let Some(max_dim) = vertex_count.checked_sub(1) else {
343 return 0;
344 };
345 let d = dim.min(max_dim);
346 if d < 2 {
347 return 0;
348 }
349
350 let n = vertex_count as u128;
351 let d = d as u128;
352 let m = d / 2;
353 if m == 0 {
354 return 0;
355 }
356
357 if d % 2 == 0 {
358 let a = binom_capped(n.saturating_sub(m), m, cap);
359 if a > cap {
360 return cap + 1;
361 }
362 let b = binom_capped(n.saturating_sub(m + 1), m - 1, cap);
363 if b > cap {
364 return cap + 1;
365 }
366 a.saturating_add(b).min(cap + 1)
367 } else {
368 let a = binom_capped(n.saturating_sub(m + 1), m, cap);
369 if a > cap {
370 return cap + 1;
371 }
372 a.saturating_mul(2).min(cap + 1)
373 }
374 }
375
376 let n = vertex_count as u128;
377 if dense_graph_bytes(n) > Self::DEFAULT_DENSE_ADJACENCY_LIMIT_BYTES {
378 return RequestedAdjacency::Sparse;
379 }
380
381 if output_adjacency {
382 let max_facets = cyclic_facets_capped(
383 vertex_count,
384 dim,
385 Self::DEFAULT_DENSE_ADJACENCY_LIMIT_NODES,
386 );
387 if max_facets > Self::DEFAULT_DENSE_ADJACENCY_LIMIT_NODES {
388 return RequestedAdjacency::Sparse;
389 }
390 }
391
392 RequestedAdjacency::Dense
393 }
394 }
395 }
396
397 pub fn solve(
398 &self,
399 repr: Representation,
400 data: &[Vec<f64>],
401 config: &BackendRunConfig,
402 ) -> Result<BackendRunAny, anyhow::Error> {
403 match repr {
404 Representation::EuclideanVertices => {
405 let dim = data.first().map_or(0, |v| v.len());
406 match self.choose_adjacency(data.len(), dim, config.output_adjacency) {
407 RequestedAdjacency::Dense => self.solve_dense(data, config).map(BackendRunAny::Dense),
408 RequestedAdjacency::Sparse => {
409 self.solve_sparse(data, config).map(BackendRunAny::Sparse)
410 }
411 RequestedAdjacency::Default => {
412 unreachable!("choose_adjacency never returns Default")
413 }
414 }
415 }
416 Representation::HomogeneousGenerators => {
417 self.ensure_generator_matrix_backend()?;
418 let generators = HomogeneousGeneratorRowsF64::new(data)?;
419 let dim = generators.dim().saturating_sub(1);
420 match self.choose_adjacency(generators.vertex_count(), dim, config.output_adjacency) {
421 RequestedAdjacency::Dense => self
422 .solve_dense(&generators, config)
423 .map(BackendRunAny::Dense),
424 RequestedAdjacency::Sparse => self
425 .solve_sparse(&generators, config)
426 .map(BackendRunAny::Sparse),
427 RequestedAdjacency::Default => {
428 unreachable!("choose_adjacency never returns Default")
429 }
430 }
431 }
432 Representation::Inequality => {
433 let dim = data.first().map_or(0, |row| row.len().saturating_sub(1));
434 match self.choose_adjacency(data.len(), dim, config.output_adjacency) {
435 RequestedAdjacency::Dense => self
436 .solve_dense_inequalities(data, config)
437 .map(BackendRunAny::Dense),
438 RequestedAdjacency::Sparse => self
439 .solve_sparse_inequalities(data, config)
440 .map(BackendRunAny::Sparse),
441 RequestedAdjacency::Default => {
442 unreachable!("choose_adjacency never returns Default")
443 }
444 }
445 }
446 }
447 }
448
449 pub fn solve_row_major(
450 &self,
451 repr: Representation,
452 data: &[f64],
453 rows: usize,
454 cols: usize,
455 config: &BackendRunConfig,
456 ) -> Result<BackendRunAny, anyhow::Error> {
457 match repr {
458 Representation::EuclideanVertices => {
459 let vertices = RowMajorVertices::new(data, rows, cols)?;
460 match self.choose_adjacency(rows, cols, config.output_adjacency) {
461 RequestedAdjacency::Dense => self
462 .solve_dense(&vertices, config)
463 .map(BackendRunAny::Dense),
464 RequestedAdjacency::Sparse => self
465 .solve_sparse(&vertices, config)
466 .map(BackendRunAny::Sparse),
467 RequestedAdjacency::Default => {
468 unreachable!("choose_adjacency never returns Default")
469 }
470 }
471 }
472 Representation::HomogeneousGenerators => {
473 self.ensure_generator_matrix_backend()?;
474 let generators = RowMajorHomogeneousGenerators::new(data, rows, cols)?;
475 let dim = cols.saturating_sub(1);
476 match self.choose_adjacency(rows, dim, config.output_adjacency) {
477 RequestedAdjacency::Dense => self
478 .solve_dense(&generators, config)
479 .map(BackendRunAny::Dense),
480 RequestedAdjacency::Sparse => self
481 .solve_sparse(&generators, config)
482 .map(BackendRunAny::Sparse),
483 RequestedAdjacency::Default => {
484 unreachable!("choose_adjacency never returns Default")
485 }
486 }
487 }
488 Representation::Inequality => {
489 let dim = cols.saturating_sub(1);
490 let inequalities = RowMajorInequalities::new(data, rows, dim)?;
491 match self.choose_adjacency(rows, dim, config.output_adjacency) {
492 RequestedAdjacency::Dense => self
493 .solve_dense_inequalities(&inequalities, config)
494 .map(BackendRunAny::Dense),
495 RequestedAdjacency::Sparse => self
496 .solve_sparse_inequalities(&inequalities, config)
497 .map(BackendRunAny::Sparse),
498 RequestedAdjacency::Default => {
499 unreachable!("choose_adjacency never returns Default")
500 }
501 }
502 }
503 }
504 }
505
506 pub fn solve_exact(
507 &self,
508 repr: Representation,
509 data: &[Vec<i64>],
510 config: &BackendRunConfig,
511 ) -> Result<BackendRunAny, anyhow::Error> {
512 self.ensure_exact_backend()?;
513 match repr {
514 Representation::EuclideanVertices => {
515 let dim = data.first().map_or(0, |v| v.len());
516 match self.choose_adjacency(data.len(), dim, config.output_adjacency) {
517 RequestedAdjacency::Dense => self
518 .solve_exact_dense(data, config)
519 .map(BackendRunAny::Dense),
520 RequestedAdjacency::Sparse => self
521 .solve_exact_sparse(data, config)
522 .map(BackendRunAny::Sparse),
523 RequestedAdjacency::Default => {
524 unreachable!("choose_adjacency never returns Default")
525 }
526 }
527 }
528 Representation::HomogeneousGenerators => {
529 self.ensure_generator_matrix_backend()?;
530 let generators = HomogeneousGeneratorRowsI64::new(data)?;
531 let dim = generators.dim().saturating_sub(1);
532 match self.choose_adjacency(generators.vertex_count(), dim, config.output_adjacency) {
533 RequestedAdjacency::Dense => self
534 .solve_exact_dense(&generators, config)
535 .map(BackendRunAny::Dense),
536 RequestedAdjacency::Sparse => self
537 .solve_exact_sparse(&generators, config)
538 .map(BackendRunAny::Sparse),
539 RequestedAdjacency::Default => {
540 unreachable!("choose_adjacency never returns Default")
541 }
542 }
543 }
544 Representation::Inequality => {
545 let dim = data.first().map_or(0, |row| row.len().saturating_sub(1));
546 match self.choose_adjacency(data.len(), dim, config.output_adjacency) {
547 RequestedAdjacency::Dense => self
548 .solve_exact_dense_inequalities(data, config)
549 .map(BackendRunAny::Dense),
550 RequestedAdjacency::Sparse => self
551 .solve_exact_sparse_inequalities(data, config)
552 .map(BackendRunAny::Sparse),
553 RequestedAdjacency::Default => {
554 unreachable!("choose_adjacency never returns Default")
555 }
556 }
557 }
558 }
559 }
560
561 pub fn solve_row_major_exact(
562 &self,
563 repr: Representation,
564 data: &[i64],
565 rows: usize,
566 cols: usize,
567 config: &BackendRunConfig,
568 ) -> Result<BackendRunAny, anyhow::Error> {
569 self.ensure_exact_backend()?;
570 match repr {
571 Representation::EuclideanVertices => {
572 let vertices = RowMajorVerticesI64::new(data, rows, cols)?;
573 match self.choose_adjacency(rows, cols, config.output_adjacency) {
574 RequestedAdjacency::Dense => self
575 .solve_exact_dense(&vertices, config)
576 .map(BackendRunAny::Dense),
577 RequestedAdjacency::Sparse => self
578 .solve_exact_sparse(&vertices, config)
579 .map(BackendRunAny::Sparse),
580 RequestedAdjacency::Default => {
581 unreachable!("choose_adjacency never returns Default")
582 }
583 }
584 }
585 Representation::HomogeneousGenerators => {
586 self.ensure_generator_matrix_backend()?;
587 let generators = RowMajorHomogeneousGeneratorsI64::new(data, rows, cols)?;
588 let dim = cols.saturating_sub(1);
589 match self.choose_adjacency(rows, dim, config.output_adjacency) {
590 RequestedAdjacency::Dense => self
591 .solve_exact_dense(&generators, config)
592 .map(BackendRunAny::Dense),
593 RequestedAdjacency::Sparse => self
594 .solve_exact_sparse(&generators, config)
595 .map(BackendRunAny::Sparse),
596 RequestedAdjacency::Default => {
597 unreachable!("choose_adjacency never returns Default")
598 }
599 }
600 }
601 Representation::Inequality => {
602 let dim = cols.saturating_sub(1);
603 let inequalities = RowMajorInequalitiesI64::new(data, rows, dim)?;
604 match self.choose_adjacency(rows, dim, config.output_adjacency) {
605 RequestedAdjacency::Dense => self
606 .solve_exact_dense_inequalities(&inequalities, config)
607 .map(BackendRunAny::Dense),
608 RequestedAdjacency::Sparse => self
609 .solve_exact_sparse_inequalities(&inequalities, config)
610 .map(BackendRunAny::Sparse),
611 RequestedAdjacency::Default => {
612 unreachable!("choose_adjacency never returns Default")
613 }
614 }
615 }
616 }
617 }
618
619 pub fn solve_row_major_exact_gmprat(
620 &self,
621 repr: Representation,
622 data: Vec<calculo::num::RugRat>,
623 rows: usize,
624 cols: usize,
625 config: &BackendRunConfig,
626 ) -> Result<BackendRunAny, anyhow::Error> {
627 use howzat::matrix::LpMatrixBuilder;
628 use howzat::dd::{DefaultNormalizer, IntUmpire, SinglePrecisionUmpire as SpUmpire, SnapPurifier as Snap};
629 use hullabaloo::types::{Generator, IncidenceOutput, Inequality};
630
631 self.ensure_exact_backend()?;
632 ensure!(
633 !config.output_adjacency || config.output_incidence,
634 "output_adjacency requires output_incidence"
635 );
636
637 let (dd_umpire, purifier, pipeline) = match &self.0 {
638 BackendSpec::HowzatDd {
639 umpire,
640 purifier,
641 pipeline,
642 } => {
643 (*umpire, *purifier, pipeline)
644 }
645 _ => {
646 return Err(anyhow!(
647 "backend '{self}' does not support exact gmprat input"
648 ));
649 }
650 };
651
652 ensure!(
653 pipeline.canonical() == "gmprat",
654 "{self} only supports exact gmprat input for howzat-dd:gmprat"
655 );
656
657 let start_total = Instant::now();
658 let timing = config.timing_detail;
659 let output_adjacency = config.output_adjacency;
660 let coeff_mode = if config.output_coefficients {
661 CoefficientMode::Exact
662 } else {
663 CoefficientMode::Off
664 };
665
666 let howzat_output_adjacency = if output_adjacency {
667 config.howzat_output_adjacency
668 } else {
669 AdjacencyOutput::Off
670 };
671 let howzat_output_incidence = if config.output_incidence {
672 IncidenceOutput::Set
673 } else {
674 IncidenceOutput::Off
675 };
676
677 let dd = |output_adjacency| howzat::polyhedron::DdConfig {
678 cone: config.howzat_options.clone(),
679 poly: howzat::polyhedron::PolyhedronOptions {
680 output_incidence: howzat_output_incidence,
681 output_adjacency,
682 input_incidence: IncidenceOutput::Off,
683 input_adjacency: AdjacencyOutput::Off,
684 save_basis_and_tableau: false,
685 save_repair_hints: false,
686 profile_adjacency: false,
687 },
688 };
689
690 fn run_howzat_exact_dd<Inc, Adj, R>(
691 spec: Backend,
692 dd_umpire: HowzatDdUmpire,
693 purifier: Option<HowzatDdPurifierSpec>,
694 output_adjacency: bool,
695 timing: bool,
696 coeff_mode: CoefficientMode,
697 dd: howzat::polyhedron::DdConfig,
698 matrix: howzat::matrix::LpMatrix<calculo::num::RugRat, R>,
699 time_matrix: Duration,
700 start_total: Instant,
701 ) -> Result<BackendRun<Inc, Adj>, anyhow::Error>
702 where
703 Inc: From<SetFamily>,
704 Adj: hullabaloo::adjacency::AdjacencyStore,
705 R: hullabaloo::types::DualRepresentation,
706 {
707 let start_dd = Instant::now();
708 let poly = match dd_umpire {
709 HowzatDdUmpire::Default | HowzatDdUmpire::Int => {
710 ensure!(
711 purifier.is_none(),
712 "{spec} does not support purification for exact gmprat input under @int"
713 );
714 let umpire = IntUmpire::new(calculo::num::RugRat::default_eps());
715 howzat::polyhedron::PolyhedronOutput::<calculo::num::RugRat, R>::from_matrix_dd_int_with_umpire(
716 matrix, dd, umpire,
717 )
718 }
719 HowzatDdUmpire::Sp => {
720 let eps = calculo::num::RugRat::default_eps();
721 let normalizer = <calculo::num::RugRat as DefaultNormalizer>::Norm::default();
722 match purifier {
723 None => {
724 let umpire = SpUmpire::with_normalizer(eps, normalizer);
725 howzat::polyhedron::PolyhedronOutput::<calculo::num::RugRat, R>::from_matrix_dd(
726 matrix, dd, umpire,
727 )
728 }
729 Some(HowzatDdPurifierSpec::Snap) => {
730 let umpire = SpUmpire::with_purifier(eps, normalizer, Snap::new());
731 howzat::polyhedron::PolyhedronOutput::<calculo::num::RugRat, R>::from_matrix_dd(
732 matrix, dd, umpire,
733 )
734 }
735 Some(HowzatDdPurifierSpec::UpSnap(_)) => {
736 return Err(anyhow!(
737 "{spec} does not support purify[upsnap[...]] under exact gmprat input"
738 ));
739 }
740 }
741 }
742 }
743 .map_err(|err| anyhow!("howzat-dd failed: {err}"))?;
744 let time_dd = start_dd.elapsed();
745
746 let store_facet_row_indices = coeff_mode != CoefficientMode::Off;
747 let (geometry, extract_detail) = howzat_common::summarize_howzat_geometry::<Inc, Adj, _, _>(
748 &poly,
749 output_adjacency,
750 timing,
751 store_facet_row_indices,
752 )?;
753 let coefficients = if coeff_mode == CoefficientMode::Off {
754 None
755 } else {
756 let Some(facet_row_indices) = geometry.facet_row_indices.as_deref() else {
757 return Err(anyhow!("internal: facet_row_indices missing"));
758 };
759 howzat_common::extract_howzat_coefficients(&poly, facet_row_indices, coeff_mode)?
760 };
761
762 let total = start_total.elapsed();
763 let detail = timing.then(|| {
764 TimingDetail::HowzatDd(HowzatDdTimingDetail {
765 fast_matrix: Duration::ZERO,
766 fast_dd: Duration::ZERO,
767 cert: Duration::ZERO,
768 repair_partial: Duration::ZERO,
769 repair_graph: Duration::ZERO,
770 exact_matrix: time_matrix,
771 exact_dd: time_dd,
772 incidence: extract_detail.incidence,
773 vertex_adjacency: extract_detail.vertex_adjacency,
774 facet_adjacency: extract_detail.facet_adjacency,
775 })
776 });
777
778 Ok(BackendRun {
779 spec,
780 stats: geometry.stats,
781 timing: BackendTiming {
782 total,
783 fast: None,
784 resolve: None,
785 exact: Some(time_matrix + time_dd),
786 },
787 facets: None,
788 coefficients,
789 geometry: BackendGeometry::Input(InputGeometry {
790 vertex_adjacency: geometry.vertex_adjacency,
791 facets_to_vertices: geometry.facets_to_vertices,
792 facet_adjacency: geometry.facet_adjacency,
793 }),
794 fails: 0,
795 fallbacks: 0,
796 error: None,
797 detail,
798 })
799 }
800
801 match repr {
802 Representation::EuclideanVertices => {
803 ensure!(
804 rows
805 .checked_mul(cols)
806 .is_some_and(|len| len == data.len()),
807 "expected {rows}x{cols} coords but got {}",
808 data.len()
809 );
810 let Some(out_cols) = cols.checked_add(1) else {
811 return Err(anyhow!("howzat generator dimension too large"));
812 };
813 let mut input = data.into_iter();
814 let start_matrix = Instant::now();
815 let mut flat: Vec<calculo::num::RugRat> =
816 Vec::with_capacity(rows.saturating_mul(out_cols));
817 for _ in 0..rows {
818 flat.push(calculo::num::RugRat(rug::Rational::from(1)));
819 flat.extend(input.by_ref().take(cols));
820 }
821 let time_matrix = start_matrix.elapsed();
822 let matrix =
823 LpMatrixBuilder::<calculo::num::RugRat, Generator>::from_flat(rows, out_cols, flat)
824 .build();
825 match self.choose_adjacency(rows, cols, output_adjacency) {
826 RequestedAdjacency::Dense => run_howzat_exact_dd::<SetFamily, SetFamily, Generator>(
827 self.clone(),
828 dd_umpire,
829 purifier,
830 output_adjacency,
831 timing,
832 coeff_mode,
833 dd(howzat_output_adjacency),
834 matrix,
835 time_matrix,
836 start_total,
837 )
838 .map(BackendRunAny::Dense),
839 RequestedAdjacency::Sparse => {
840 run_howzat_exact_dd::<ListFamily, AdjacencyList, Generator>(
841 self.clone(),
842 dd_umpire,
843 purifier,
844 output_adjacency,
845 timing,
846 coeff_mode,
847 dd(AdjacencyOutput::Off),
848 matrix,
849 time_matrix,
850 start_total,
851 )
852 .map(BackendRunAny::Sparse)
853 }
854 RequestedAdjacency::Default => unreachable!("choose_adjacency never returns Default"),
855 }
856 }
857 Representation::HomogeneousGenerators => {
858 self.ensure_generator_matrix_backend()?;
859 ensure!(
860 cols > 1,
861 "generator matrix must have at least 2 columns"
862 );
863 ensure!(
864 rows
865 .checked_mul(cols)
866 .is_some_and(|len| len == data.len()),
867 "expected {rows}x{cols} generator entries but got {}",
868 data.len()
869 );
870 let start_matrix = Instant::now();
871 let matrix =
872 LpMatrixBuilder::<calculo::num::RugRat, Generator>::from_flat(rows, cols, data)
873 .build();
874 let time_matrix = start_matrix.elapsed();
875 let dim = cols - 1;
876 match self.choose_adjacency(rows, dim, output_adjacency) {
877 RequestedAdjacency::Dense => run_howzat_exact_dd::<SetFamily, SetFamily, Generator>(
878 self.clone(),
879 dd_umpire,
880 purifier,
881 output_adjacency,
882 timing,
883 coeff_mode,
884 dd(howzat_output_adjacency),
885 matrix,
886 time_matrix,
887 start_total,
888 )
889 .map(BackendRunAny::Dense),
890 RequestedAdjacency::Sparse => {
891 run_howzat_exact_dd::<ListFamily, AdjacencyList, Generator>(
892 self.clone(),
893 dd_umpire,
894 purifier,
895 output_adjacency,
896 timing,
897 coeff_mode,
898 dd(AdjacencyOutput::Off),
899 matrix,
900 time_matrix,
901 start_total,
902 )
903 .map(BackendRunAny::Sparse)
904 }
905 RequestedAdjacency::Default => unreachable!("choose_adjacency never returns Default"),
906 }
907 }
908 Representation::Inequality => {
909 ensure!(
910 cols > 1,
911 "inequality matrix must have at least 2 columns"
912 );
913 ensure!(
914 rows
915 .checked_mul(cols)
916 .is_some_and(|len| len == data.len()),
917 "expected {rows}x{cols} coeffs but got {}",
918 data.len()
919 );
920 let start_matrix = Instant::now();
921 let matrix =
922 LpMatrixBuilder::<calculo::num::RugRat, Inequality>::from_flat(rows, cols, data)
923 .build();
924 let time_matrix = start_matrix.elapsed();
925 let dim = cols - 1;
926 match self.choose_adjacency(rows, dim, output_adjacency) {
927 RequestedAdjacency::Dense => run_howzat_exact_dd::<SetFamily, SetFamily, Inequality>(
928 self.clone(),
929 dd_umpire,
930 purifier,
931 output_adjacency,
932 timing,
933 coeff_mode,
934 dd(howzat_output_adjacency),
935 matrix,
936 time_matrix,
937 start_total,
938 )
939 .map(BackendRunAny::Dense),
940 RequestedAdjacency::Sparse => {
941 run_howzat_exact_dd::<ListFamily, AdjacencyList, Inequality>(
942 self.clone(),
943 dd_umpire,
944 purifier,
945 output_adjacency,
946 timing,
947 coeff_mode,
948 dd(AdjacencyOutput::Off),
949 matrix,
950 time_matrix,
951 start_total,
952 )
953 .map(BackendRunAny::Sparse)
954 }
955 RequestedAdjacency::Default => unreachable!("choose_adjacency never returns Default"),
956 }
957 }
958 }
959 }
960
961 fn ensure_exact_backend(&self) -> Result<(), anyhow::Error> {
962 let is_exact = match &self.0 {
963 BackendSpec::CddlibGmpRational | BackendSpec::CddlibHlblGmpRational => true,
964 BackendSpec::HowzatDd { pipeline, .. } => pipeline.is_exact(),
965 BackendSpec::LrslibHlblGmpInt | BackendSpec::PplHlblGmpInt => true,
966 _ => false,
967 };
968
969 ensure!(
970 is_exact,
971 "backend '{self}' is not exact; choose an exact backend spec or call solve()/solve_row_major() instead"
972 );
973 Ok(())
974 }
975
976 fn ensure_generator_matrix_backend(&self) -> Result<(), anyhow::Error> {
977 ensure!(
978 matches!(
979 &self.0,
980 BackendSpec::HowzatDd { .. } | BackendSpec::HowzatLrsRug | BackendSpec::HowzatLrsDashu
981 ),
982 "backend '{self}' does not support HomogeneousGenerators input; use a howzat backend or pass Euclidean vertices instead"
983 );
984 Ok(())
985 }
986
987 fn solve_exact_dense<V: VerticesI64 + HowzatVertices + ?Sized>(
988 &self,
989 vertices: &V,
990 config: &BackendRunConfig,
991 ) -> Result<BackendRun<SetFamily, SetFamily>, anyhow::Error> {
992 ensure!(
993 !config.output_adjacency || config.output_incidence,
994 "output_adjacency requires output_incidence"
995 );
996
997 let timing = config.timing_detail;
998 let output_incidence = config.output_incidence;
999 let output_adjacency = config.output_adjacency;
1000 let coeff_mode = if config.output_coefficients {
1001 CoefficientMode::Exact
1002 } else {
1003 CoefficientMode::Off
1004 };
1005 let howzat_output_adjacency = if output_adjacency {
1006 config.howzat_output_adjacency
1007 } else {
1008 AdjacencyOutput::Off
1009 };
1010
1011 let run = match &self.0 {
1012 BackendSpec::HowzatDd {
1013 umpire,
1014 purifier,
1015 pipeline,
1016 } => {
1017 howzat_dd::run_howzat_dd_backend::<V, SetFamily, SetFamily>(
1018 self.clone(),
1019 *umpire,
1020 *purifier,
1021 pipeline,
1022 output_incidence,
1023 output_adjacency,
1024 howzat_output_adjacency,
1025 coeff_mode,
1026 vertices,
1027 &config.howzat_options,
1028 timing,
1029 )
1030 }
1031 BackendSpec::LrslibHlblGmpInt => {
1032 lrslib::run_lrslib_hlbl_backend_i64::<V, SetFamily, SetFamily>(
1033 self.clone(),
1034 vertices,
1035 output_incidence,
1036 output_adjacency,
1037 coeff_mode,
1038 timing,
1039 )
1040 }
1041 BackendSpec::PplHlblGmpInt => ppl::run_ppl_hlbl_backend_i64::<V, SetFamily, SetFamily>(
1042 self.clone(),
1043 vertices,
1044 output_incidence,
1045 output_adjacency,
1046 coeff_mode,
1047 timing,
1048 ),
1049 BackendSpec::CddlibGmpRational | BackendSpec::CddlibHlblGmpRational => {
1050 Err(anyhow!("{self} does not support dense adjacency"))
1051 }
1052 _ => Err(anyhow!(
1053 "internal: solve_exact_dense called with non-exact backend"
1054 )),
1055 };
1056
1057 match run {
1058 Ok(run) => Ok(run),
1059 Err(err) => Err(err),
1060 }
1061 }
1062
1063 fn solve_exact_dense_inequalities<I: InequalitiesI64 + HowzatInequalities + ?Sized>(
1064 &self,
1065 inequalities: &I,
1066 config: &BackendRunConfig,
1067 ) -> Result<BackendRun<SetFamily, SetFamily>, anyhow::Error> {
1068 ensure!(
1069 !config.output_adjacency || config.output_incidence,
1070 "output_adjacency requires output_incidence"
1071 );
1072
1073 let timing = config.timing_detail;
1074 let output_incidence = config.output_incidence;
1075 let output_adjacency = config.output_adjacency;
1076 let coeff_mode = if config.output_coefficients {
1077 CoefficientMode::Exact
1078 } else {
1079 CoefficientMode::Off
1080 };
1081 let howzat_output_adjacency = if output_adjacency {
1082 config.howzat_output_adjacency
1083 } else {
1084 AdjacencyOutput::Off
1085 };
1086
1087 let run = match &self.0 {
1088 BackendSpec::HowzatDd {
1089 umpire,
1090 purifier,
1091 pipeline,
1092 } => {
1093 howzat_dd::run_howzat_dd_backend_from_inequalities::<I, SetFamily, SetFamily>(
1094 self.clone(),
1095 *umpire,
1096 *purifier,
1097 pipeline,
1098 output_incidence,
1099 output_adjacency,
1100 howzat_output_adjacency,
1101 coeff_mode,
1102 inequalities,
1103 &config.howzat_options,
1104 timing,
1105 )
1106 }
1107 BackendSpec::HowzatLrsRug | BackendSpec::HowzatLrsDashu => {
1108 howzat_lrs::run_howzat_lrs_backend_from_inequalities::<I, SetFamily, SetFamily>(
1109 self.clone(),
1110 inequalities,
1111 output_incidence,
1112 output_adjacency,
1113 howzat_output_adjacency,
1114 coeff_mode,
1115 timing,
1116 )
1117 }
1118 _ => Err(anyhow!("{self} does not support inequality input")),
1119 };
1120
1121 match run {
1122 Ok(run) => Ok(run),
1123 Err(err) => Err(err),
1124 }
1125 }
1126
1127 fn solve_exact_sparse<V: VerticesI64 + HowzatVertices + ?Sized>(
1128 &self,
1129 vertices: &V,
1130 config: &BackendRunConfig,
1131 ) -> Result<BackendRun<ListFamily, AdjacencyList>, anyhow::Error> {
1132 ensure!(
1133 !config.output_adjacency || config.output_incidence,
1134 "output_adjacency requires output_incidence"
1135 );
1136
1137 let start_run = Instant::now();
1138 let timing = config.timing_detail;
1139 let output_incidence = config.output_incidence;
1140 let output_adjacency = config.output_adjacency;
1141 let coeff_mode = if config.output_coefficients {
1142 CoefficientMode::Exact
1143 } else {
1144 CoefficientMode::Off
1145 };
1146
1147 let run = match &self.0 {
1148 BackendSpec::CddlibGmpRational => {
1149 cddlib::run_cddlib_backend_i64::<cddlib_rs::CddRational, V>(
1150 self.clone(),
1151 vertices,
1152 cddlib_rs::NumberType::Rational,
1153 false,
1154 output_incidence,
1155 output_adjacency,
1156 coeff_mode,
1157 timing,
1158 )
1159 }
1160 BackendSpec::CddlibHlblGmpRational => {
1161 cddlib::run_cddlib_backend_i64::<cddlib_rs::CddRational, V>(
1162 self.clone(),
1163 vertices,
1164 cddlib_rs::NumberType::Rational,
1165 true,
1166 output_incidence,
1167 output_adjacency,
1168 coeff_mode,
1169 timing,
1170 )
1171 }
1172 BackendSpec::HowzatDd {
1173 umpire,
1174 purifier,
1175 pipeline,
1176 } => {
1177 howzat_dd::run_howzat_dd_backend::<V, ListFamily, AdjacencyList>(
1178 self.clone(),
1179 *umpire,
1180 *purifier,
1181 pipeline,
1182 output_incidence,
1183 output_adjacency,
1184 AdjacencyOutput::Off,
1185 coeff_mode,
1186 vertices,
1187 &config.howzat_options,
1188 timing,
1189 )
1190 }
1191 BackendSpec::LrslibHlblGmpInt => {
1192 lrslib::run_lrslib_hlbl_backend_i64::<V, ListFamily, AdjacencyList>(
1193 self.clone(),
1194 vertices,
1195 output_incidence,
1196 output_adjacency,
1197 coeff_mode,
1198 timing,
1199 )
1200 }
1201 BackendSpec::PplHlblGmpInt => {
1202 ppl::run_ppl_hlbl_backend_i64::<V, ListFamily, AdjacencyList>(
1203 self.clone(),
1204 vertices,
1205 output_incidence,
1206 output_adjacency,
1207 coeff_mode,
1208 timing,
1209 )
1210 }
1211 _ => Err(anyhow!(
1212 "internal: solve_exact_sparse called with non-exact backend"
1213 )),
1214 };
1215
1216 match run {
1217 Ok(run) => Ok(run),
1218 Err(err) => {
1219 if matches!(
1220 self.0,
1221 BackendSpec::CddlibF64
1222 | BackendSpec::CddlibGmpFloat
1223 | BackendSpec::CddlibGmpRational
1224 | BackendSpec::CddlibHlblF64
1225 | BackendSpec::CddlibHlblGmpFloat
1226 | BackendSpec::CddlibHlblGmpRational
1227 ) && cddlib::is_cddlib_error_code(
1228 &err,
1229 cddlib_rs::CddErrorCode::NumericallyInconsistent,
1230 )
1231 {
1232 Ok(cddlib::backend_error_run_sparse(
1233 self.clone(),
1234 vertices.dim(),
1235 vertices.vertex_count(),
1236 start_run.elapsed(),
1237 err.to_string(),
1238 ))
1239 } else {
1240 Err(err)
1241 }
1242 }
1243 }
1244 }
1245
1246 fn solve_exact_sparse_inequalities<I: InequalitiesI64 + HowzatInequalities + ?Sized>(
1247 &self,
1248 inequalities: &I,
1249 config: &BackendRunConfig,
1250 ) -> Result<BackendRun<ListFamily, AdjacencyList>, anyhow::Error> {
1251 ensure!(
1252 !config.output_adjacency || config.output_incidence,
1253 "output_adjacency requires output_incidence"
1254 );
1255
1256 self.ensure_exact_backend()?;
1257
1258 let start_run = Instant::now();
1259 let timing = config.timing_detail;
1260 let output_incidence = config.output_incidence;
1261 let output_adjacency = config.output_adjacency;
1262 let coeff_mode = if config.output_coefficients {
1263 CoefficientMode::Exact
1264 } else {
1265 CoefficientMode::Off
1266 };
1267
1268 let run = match &self.0 {
1269 BackendSpec::CddlibGmpRational => cddlib::run_cddlib_backend_inequalities_i64::<
1270 cddlib_rs::CddRational,
1271 I,
1272 >(
1273 self.clone(),
1274 inequalities,
1275 cddlib_rs::NumberType::Rational,
1276 false,
1277 output_incidence,
1278 output_adjacency,
1279 coeff_mode,
1280 timing,
1281 ),
1282 BackendSpec::CddlibHlblGmpRational => cddlib::run_cddlib_backend_inequalities_i64::<
1283 cddlib_rs::CddRational,
1284 I,
1285 >(
1286 self.clone(),
1287 inequalities,
1288 cddlib_rs::NumberType::Rational,
1289 true,
1290 output_incidence,
1291 output_adjacency,
1292 coeff_mode,
1293 timing,
1294 ),
1295 BackendSpec::HowzatDd {
1296 umpire,
1297 purifier,
1298 pipeline,
1299 } => {
1300 howzat_dd::run_howzat_dd_backend_from_inequalities::<I, ListFamily, AdjacencyList>(
1301 self.clone(),
1302 *umpire,
1303 *purifier,
1304 pipeline,
1305 output_incidence,
1306 output_adjacency,
1307 AdjacencyOutput::Off,
1308 coeff_mode,
1309 inequalities,
1310 &config.howzat_options,
1311 timing,
1312 )
1313 }
1314 BackendSpec::HowzatLrsRug | BackendSpec::HowzatLrsDashu => {
1315 howzat_lrs::run_howzat_lrs_backend_from_inequalities::<I, ListFamily, AdjacencyList>(
1316 self.clone(),
1317 inequalities,
1318 output_incidence,
1319 output_adjacency,
1320 AdjacencyOutput::Off,
1321 coeff_mode,
1322 timing,
1323 )
1324 }
1325 _ => Err(anyhow!("{self} does not support inequality input")),
1326 };
1327
1328 match run {
1329 Ok(run) => Ok(run),
1330 Err(err) => {
1331 if matches!(
1332 self.0,
1333 BackendSpec::CddlibF64
1334 | BackendSpec::CddlibGmpFloat
1335 | BackendSpec::CddlibGmpRational
1336 | BackendSpec::CddlibHlblF64
1337 | BackendSpec::CddlibHlblGmpFloat
1338 | BackendSpec::CddlibHlblGmpRational
1339 ) && cddlib::is_cddlib_error_code(
1340 &err,
1341 cddlib_rs::CddErrorCode::NumericallyInconsistent,
1342 )
1343 {
1344 Ok(cddlib::backend_error_run_sparse(
1345 self.clone(),
1346 inequalities.dim(),
1347 inequalities.facet_count(),
1348 start_run.elapsed(),
1349 err.to_string(),
1350 ))
1351 } else {
1352 Err(err)
1353 }
1354 }
1355 }
1356 }
1357
1358 fn solve_dense<V: VerticesF64 + HowzatVertices + ?Sized>(
1359 &self,
1360 vertices: &V,
1361 config: &BackendRunConfig,
1362 ) -> Result<BackendRun<SetFamily, SetFamily>, anyhow::Error> {
1363 ensure!(
1364 !config.output_adjacency || config.output_incidence,
1365 "output_adjacency requires output_incidence"
1366 );
1367
1368 let timing = config.timing_detail;
1369 let output_incidence = config.output_incidence;
1370 let output_adjacency = config.output_adjacency;
1371 let coeff_mode = if config.output_coefficients {
1372 CoefficientMode::F64
1373 } else {
1374 CoefficientMode::Off
1375 };
1376 let howzat_output_adjacency = if output_adjacency {
1377 config.howzat_output_adjacency
1378 } else {
1379 AdjacencyOutput::Off
1380 };
1381
1382 let run = match &self.0 {
1383 BackendSpec::CddlibF64
1384 | BackendSpec::CddlibGmpFloat
1385 | BackendSpec::CddlibGmpRational
1386 | BackendSpec::CddlibHlblF64
1387 | BackendSpec::CddlibHlblGmpFloat
1388 | BackendSpec::CddlibHlblGmpRational => {
1389 Err(anyhow!("{self} does not support dense adjacency"))
1390 }
1391 BackendSpec::HowzatDd {
1392 umpire,
1393 purifier,
1394 pipeline,
1395 } => {
1396 howzat_dd::run_howzat_dd_backend::<V, SetFamily, SetFamily>(
1397 self.clone(),
1398 *umpire,
1399 *purifier,
1400 pipeline,
1401 output_incidence,
1402 output_adjacency,
1403 howzat_output_adjacency,
1404 coeff_mode,
1405 vertices,
1406 &config.howzat_options,
1407 timing,
1408 )
1409 }
1410 BackendSpec::HowzatLrsRug | BackendSpec::HowzatLrsDashu => {
1411 howzat_lrs::run_howzat_lrs_backend::<V, SetFamily, SetFamily>(
1412 self.clone(),
1413 vertices,
1414 output_incidence,
1415 output_adjacency,
1416 howzat_output_adjacency,
1417 coeff_mode,
1418 timing,
1419 )
1420 }
1421 BackendSpec::LrslibHlblGmpInt => {
1422 lrslib::run_lrslib_hlbl_backend::<V, SetFamily, SetFamily>(
1423 self.clone(),
1424 vertices,
1425 output_incidence,
1426 output_adjacency,
1427 coeff_mode,
1428 timing,
1429 )
1430 }
1431 BackendSpec::PplHlblGmpInt => ppl::run_ppl_hlbl_backend::<V, SetFamily, SetFamily>(
1432 self.clone(),
1433 vertices,
1434 output_incidence,
1435 output_adjacency,
1436 coeff_mode,
1437 timing,
1438 ),
1439 };
1440
1441 match run {
1442 Ok(run) => Ok(run),
1443 Err(err) => Err(err),
1444 }
1445 }
1446
1447 fn solve_dense_inequalities<I: InequalitiesF64 + HowzatInequalities + ?Sized>(
1448 &self,
1449 inequalities: &I,
1450 config: &BackendRunConfig,
1451 ) -> Result<BackendRun<SetFamily, SetFamily>, anyhow::Error> {
1452 ensure!(
1453 !config.output_adjacency || config.output_incidence,
1454 "output_adjacency requires output_incidence"
1455 );
1456
1457 let timing = config.timing_detail;
1458 let output_incidence = config.output_incidence;
1459 let output_adjacency = config.output_adjacency;
1460 let coeff_mode = if config.output_coefficients {
1461 CoefficientMode::F64
1462 } else {
1463 CoefficientMode::Off
1464 };
1465 let howzat_output_adjacency = if output_adjacency {
1466 config.howzat_output_adjacency
1467 } else {
1468 AdjacencyOutput::Off
1469 };
1470
1471 let run = match &self.0 {
1472 BackendSpec::HowzatDd {
1473 umpire,
1474 purifier,
1475 pipeline,
1476 } => {
1477 howzat_dd::run_howzat_dd_backend_from_inequalities::<I, SetFamily, SetFamily>(
1478 self.clone(),
1479 *umpire,
1480 *purifier,
1481 pipeline,
1482 output_incidence,
1483 output_adjacency,
1484 howzat_output_adjacency,
1485 coeff_mode,
1486 inequalities,
1487 &config.howzat_options,
1488 timing,
1489 )
1490 }
1491 BackendSpec::HowzatLrsRug | BackendSpec::HowzatLrsDashu => {
1492 howzat_lrs::run_howzat_lrs_backend_from_inequalities::<I, SetFamily, SetFamily>(
1493 self.clone(),
1494 inequalities,
1495 output_incidence,
1496 output_adjacency,
1497 howzat_output_adjacency,
1498 coeff_mode,
1499 timing,
1500 )
1501 }
1502 _ => Err(anyhow!("{self} does not support inequality input")),
1503 };
1504
1505 match run {
1506 Ok(run) => Ok(run),
1507 Err(err) => Err(err),
1508 }
1509 }
1510
1511 fn solve_sparse<V: VerticesF64 + HowzatVertices + ?Sized>(
1512 &self,
1513 vertices: &V,
1514 config: &BackendRunConfig,
1515 ) -> Result<BackendRun<ListFamily, AdjacencyList>, anyhow::Error> {
1516 ensure!(
1517 !config.output_adjacency || config.output_incidence,
1518 "output_adjacency requires output_incidence"
1519 );
1520
1521 let start_run = Instant::now();
1522 let timing = config.timing_detail;
1523 let output_incidence = config.output_incidence;
1524 let output_adjacency = config.output_adjacency;
1525 let coeff_mode = if config.output_coefficients {
1526 CoefficientMode::F64
1527 } else {
1528 CoefficientMode::Off
1529 };
1530
1531 let run = match &self.0 {
1532 BackendSpec::CddlibF64 => cddlib::run_cddlib_backend::<f64, V>(
1533 self.clone(),
1534 vertices,
1535 cddlib_rs::NumberType::Real,
1536 false,
1537 output_incidence,
1538 output_adjacency,
1539 coeff_mode,
1540 timing,
1541 ),
1542 BackendSpec::CddlibGmpFloat => cddlib::run_cddlib_backend::<cddlib_rs::CddFloat, V>(
1543 self.clone(),
1544 vertices,
1545 cddlib_rs::NumberType::Real,
1546 false,
1547 output_incidence,
1548 output_adjacency,
1549 coeff_mode,
1550 timing,
1551 ),
1552 BackendSpec::CddlibGmpRational => {
1553 cddlib::run_cddlib_backend::<cddlib_rs::CddRational, V>(
1554 self.clone(),
1555 vertices,
1556 cddlib_rs::NumberType::Rational,
1557 false,
1558 output_incidence,
1559 output_adjacency,
1560 coeff_mode,
1561 timing,
1562 )
1563 }
1564 BackendSpec::CddlibHlblF64 => cddlib::run_cddlib_backend::<f64, V>(
1565 self.clone(),
1566 vertices,
1567 cddlib_rs::NumberType::Real,
1568 true,
1569 output_incidence,
1570 output_adjacency,
1571 coeff_mode,
1572 timing,
1573 ),
1574 BackendSpec::CddlibHlblGmpFloat => {
1575 cddlib::run_cddlib_backend::<cddlib_rs::CddFloat, V>(
1576 self.clone(),
1577 vertices,
1578 cddlib_rs::NumberType::Real,
1579 true,
1580 output_incidence,
1581 output_adjacency,
1582 coeff_mode,
1583 timing,
1584 )
1585 }
1586 BackendSpec::CddlibHlblGmpRational => {
1587 cddlib::run_cddlib_backend::<cddlib_rs::CddRational, V>(
1588 self.clone(),
1589 vertices,
1590 cddlib_rs::NumberType::Rational,
1591 true,
1592 output_incidence,
1593 output_adjacency,
1594 coeff_mode,
1595 timing,
1596 )
1597 }
1598 BackendSpec::HowzatDd {
1599 umpire,
1600 purifier,
1601 pipeline,
1602 } => {
1603 howzat_dd::run_howzat_dd_backend::<V, ListFamily, AdjacencyList>(
1604 self.clone(),
1605 *umpire,
1606 *purifier,
1607 pipeline,
1608 output_incidence,
1609 output_adjacency,
1610 AdjacencyOutput::Off,
1611 coeff_mode,
1612 vertices,
1613 &config.howzat_options,
1614 timing,
1615 )
1616 }
1617 BackendSpec::HowzatLrsRug | BackendSpec::HowzatLrsDashu => {
1618 howzat_lrs::run_howzat_lrs_backend::<V, ListFamily, AdjacencyList>(
1619 self.clone(),
1620 vertices,
1621 output_incidence,
1622 output_adjacency,
1623 AdjacencyOutput::Off,
1624 coeff_mode,
1625 timing,
1626 )
1627 }
1628 BackendSpec::LrslibHlblGmpInt => {
1629 lrslib::run_lrslib_hlbl_backend::<V, ListFamily, AdjacencyList>(
1630 self.clone(),
1631 vertices,
1632 output_incidence,
1633 output_adjacency,
1634 coeff_mode,
1635 timing,
1636 )
1637 }
1638 BackendSpec::PplHlblGmpInt => {
1639 ppl::run_ppl_hlbl_backend::<V, ListFamily, AdjacencyList>(
1640 self.clone(),
1641 vertices,
1642 output_incidence,
1643 output_adjacency,
1644 coeff_mode,
1645 timing,
1646 )
1647 }
1648 };
1649
1650 match run {
1651 Ok(run) => Ok(run),
1652 Err(err) => {
1653 if matches!(
1654 self.0,
1655 BackendSpec::CddlibF64
1656 | BackendSpec::CddlibGmpFloat
1657 | BackendSpec::CddlibGmpRational
1658 | BackendSpec::CddlibHlblF64
1659 | BackendSpec::CddlibHlblGmpFloat
1660 | BackendSpec::CddlibHlblGmpRational
1661 ) && cddlib::is_cddlib_error_code(
1662 &err,
1663 cddlib_rs::CddErrorCode::NumericallyInconsistent,
1664 )
1665 {
1666 Ok(cddlib::backend_error_run_sparse(
1667 self.clone(),
1668 vertices.dim(),
1669 vertices.vertex_count(),
1670 start_run.elapsed(),
1671 err.to_string(),
1672 ))
1673 } else {
1674 Err(err)
1675 }
1676 }
1677 }
1678 }
1679
1680 fn solve_sparse_inequalities<I: InequalitiesF64 + HowzatInequalities + ?Sized>(
1681 &self,
1682 inequalities: &I,
1683 config: &BackendRunConfig,
1684 ) -> Result<BackendRun<ListFamily, AdjacencyList>, anyhow::Error> {
1685 ensure!(
1686 !config.output_adjacency || config.output_incidence,
1687 "output_adjacency requires output_incidence"
1688 );
1689
1690 let start_run = Instant::now();
1691 let timing = config.timing_detail;
1692 let output_incidence = config.output_incidence;
1693 let output_adjacency = config.output_adjacency;
1694 let coeff_mode = if config.output_coefficients {
1695 CoefficientMode::F64
1696 } else {
1697 CoefficientMode::Off
1698 };
1699
1700 let run = match &self.0 {
1701 BackendSpec::CddlibF64 => cddlib::run_cddlib_backend_inequalities::<f64, I>(
1702 self.clone(),
1703 inequalities,
1704 cddlib_rs::NumberType::Real,
1705 false,
1706 output_incidence,
1707 output_adjacency,
1708 coeff_mode,
1709 timing,
1710 ),
1711 BackendSpec::CddlibGmpFloat => cddlib::run_cddlib_backend_inequalities::<
1712 cddlib_rs::CddFloat,
1713 I,
1714 >(
1715 self.clone(),
1716 inequalities,
1717 cddlib_rs::NumberType::Real,
1718 false,
1719 output_incidence,
1720 output_adjacency,
1721 coeff_mode,
1722 timing,
1723 ),
1724 BackendSpec::CddlibGmpRational => cddlib::run_cddlib_backend_inequalities::<
1725 cddlib_rs::CddRational,
1726 I,
1727 >(
1728 self.clone(),
1729 inequalities,
1730 cddlib_rs::NumberType::Rational,
1731 false,
1732 output_incidence,
1733 output_adjacency,
1734 coeff_mode,
1735 timing,
1736 ),
1737 BackendSpec::CddlibHlblF64 => cddlib::run_cddlib_backend_inequalities::<f64, I>(
1738 self.clone(),
1739 inequalities,
1740 cddlib_rs::NumberType::Real,
1741 true,
1742 output_incidence,
1743 output_adjacency,
1744 coeff_mode,
1745 timing,
1746 ),
1747 BackendSpec::CddlibHlblGmpFloat => cddlib::run_cddlib_backend_inequalities::<
1748 cddlib_rs::CddFloat,
1749 I,
1750 >(
1751 self.clone(),
1752 inequalities,
1753 cddlib_rs::NumberType::Real,
1754 true,
1755 output_incidence,
1756 output_adjacency,
1757 coeff_mode,
1758 timing,
1759 ),
1760 BackendSpec::CddlibHlblGmpRational => cddlib::run_cddlib_backend_inequalities::<
1761 cddlib_rs::CddRational,
1762 I,
1763 >(
1764 self.clone(),
1765 inequalities,
1766 cddlib_rs::NumberType::Rational,
1767 true,
1768 output_incidence,
1769 output_adjacency,
1770 coeff_mode,
1771 timing,
1772 ),
1773 BackendSpec::HowzatDd {
1774 umpire,
1775 purifier,
1776 pipeline,
1777 } => {
1778 howzat_dd::run_howzat_dd_backend_from_inequalities::<I, ListFamily, AdjacencyList>(
1779 self.clone(),
1780 *umpire,
1781 *purifier,
1782 pipeline,
1783 output_incidence,
1784 output_adjacency,
1785 AdjacencyOutput::Off,
1786 coeff_mode,
1787 inequalities,
1788 &config.howzat_options,
1789 timing,
1790 )
1791 }
1792 BackendSpec::HowzatLrsRug | BackendSpec::HowzatLrsDashu => {
1793 howzat_lrs::run_howzat_lrs_backend_from_inequalities::<I, ListFamily, AdjacencyList>(
1794 self.clone(),
1795 inequalities,
1796 output_incidence,
1797 output_adjacency,
1798 AdjacencyOutput::Off,
1799 coeff_mode,
1800 timing,
1801 )
1802 }
1803 _ => Err(anyhow!("{self} does not support inequality input")),
1804 };
1805
1806 match run {
1807 Ok(run) => Ok(run),
1808 Err(err) => {
1809 if matches!(
1810 self.0,
1811 BackendSpec::CddlibF64
1812 | BackendSpec::CddlibGmpFloat
1813 | BackendSpec::CddlibGmpRational
1814 | BackendSpec::CddlibHlblF64
1815 | BackendSpec::CddlibHlblGmpFloat
1816 | BackendSpec::CddlibHlblGmpRational
1817 ) && cddlib::is_cddlib_error_code(
1818 &err,
1819 cddlib_rs::CddErrorCode::NumericallyInconsistent,
1820 )
1821 {
1822 Ok(cddlib::backend_error_run_sparse(
1823 self.clone(),
1824 inequalities.dim(),
1825 inequalities.facet_count(),
1826 start_run.elapsed(),
1827 err.to_string(),
1828 ))
1829 } else {
1830 Err(err)
1831 }
1832 }
1833 }
1834 }
1835}
1836
1837impl fmt::Display for Backend {
1838 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1839 self.0.fmt_kind(f)?;
1840 let mut opened = false;
1841
1842 if let BackendSpec::HowzatDd {
1843 purifier: Some(purifier),
1844 ..
1845 } = &self.0
1846 {
1847 f.write_str("[")?;
1848 opened = true;
1849 write!(f, "purify[{}]", purifier.canonical_token())?;
1850 }
1851
1852 if let Some(token) = self.1.token() {
1853 if !opened {
1854 f.write_str("[")?;
1855 opened = true;
1856 } else {
1857 f.write_str(",")?;
1858 }
1859 write!(f, "adj[{token}]")?;
1860 }
1861
1862 if opened {
1863 f.write_str("]")?;
1864 }
1865
1866 f.write_str(":")?;
1867 self.0.fmt_num(f)?;
1868 Ok(())
1869 }
1870}
1871
1872fn split_backend_bracket_group(raw: &str) -> Result<Option<(&str, &str)>, String> {
1873 let token = raw.trim_end();
1874 if !token.ends_with(']') {
1875 return Ok(None);
1876 }
1877
1878 let mut depth = 0usize;
1879 for (idx, ch) in token.char_indices().rev() {
1880 match ch {
1881 ']' => depth += 1,
1882 '[' => {
1883 depth = depth.saturating_sub(1);
1884 if depth == 0 {
1885 let base = token.get(..idx).unwrap_or("");
1886 let inner = token.get(idx + 1..token.len() - 1).unwrap_or("");
1887 return Ok(Some((base, inner)));
1888 }
1889 }
1890 _ => {}
1891 }
1892 }
1893
1894 Err(format!("backend spec '{raw}' has an unmatched ']'"))
1895}
1896
1897fn split_backend_option_token(token: &str) -> Result<(&str, &str), String> {
1898 let token = token.trim();
1899 if token.is_empty() {
1900 return Err("backend option cannot be empty".to_string());
1901 }
1902
1903 let Some(open_idx) = token.find('[') else {
1904 return Err(format!(
1905 "backend option '{token}' must be written as name[value], e.g. adj[sparse]"
1906 ));
1907 };
1908 let name = token.get(..open_idx).unwrap_or("").trim();
1909 if name.is_empty() {
1910 return Err(format!("backend option '{token}' is missing a name"));
1911 }
1912
1913 let after_open = token.get(open_idx + 1..).unwrap_or("");
1914 let mut depth = 1usize;
1915 let mut close_rel = None;
1916 for (offset, ch) in after_open.char_indices() {
1917 match ch {
1918 '[' => depth += 1,
1919 ']' => {
1920 depth = depth.saturating_sub(1);
1921 if depth == 0 {
1922 close_rel = Some(offset);
1923 break;
1924 }
1925 }
1926 _ => {}
1927 }
1928 }
1929 let Some(close_rel) = close_rel else {
1930 return Err(format!("backend option '{token}' has an unmatched '['"));
1931 };
1932
1933 let arg = after_open.get(..close_rel).unwrap_or("").trim();
1934 let rest = after_open.get(close_rel + 1..).unwrap_or("").trim();
1935 if !rest.is_empty() {
1936 return Err(format!(
1937 "backend option '{token}' contains trailing characters after the closing ']'"
1938 ));
1939 }
1940
1941 Ok((name, arg))
1942}
1943
1944fn parse_backend_options(
1945 raw: &str,
1946) -> Result<(Option<HowzatDdPurifierSpec>, RequestedAdjacency), String> {
1947 let mut purifier = None;
1948 let mut adjacency = RequestedAdjacency::Default;
1949
1950 let mut depth = 0usize;
1951 let mut start = 0usize;
1952 let mut tokens = Vec::new();
1953 for (idx, ch) in raw.char_indices() {
1954 match ch {
1955 '[' => depth += 1,
1956 ']' => depth = depth.saturating_sub(1),
1957 ',' if depth == 0 => {
1958 tokens.push(raw.get(start..idx).unwrap_or("").trim());
1959 start = idx + 1;
1960 }
1961 _ => {}
1962 }
1963 }
1964 tokens.push(raw.get(start..).unwrap_or("").trim());
1965
1966 for token in tokens {
1967 if token.is_empty() {
1968 return Err(format!(
1969 "backend options '{raw}' contains an empty entry (check comma placement)"
1970 ));
1971 }
1972 let (name, arg) = split_backend_option_token(token)?;
1973
1974 if name.eq_ignore_ascii_case("purify") {
1975 if purifier.is_some() {
1976 return Err(format!(
1977 "backend options '{raw}' contains multiple purify[...] entries"
1978 ));
1979 }
1980 purifier = Some(parse_howzat_dd_purifier(arg)?);
1981 } else if name.eq_ignore_ascii_case("adj") {
1982 if adjacency != RequestedAdjacency::Default {
1983 return Err(format!(
1984 "backend options '{raw}' contains multiple adj[...] entries"
1985 ));
1986 }
1987 if arg.eq_ignore_ascii_case("dense") {
1988 adjacency = RequestedAdjacency::Dense;
1989 } else if arg.eq_ignore_ascii_case("sparse") {
1990 adjacency = RequestedAdjacency::Sparse;
1991 } else {
1992 return Err(format!(
1993 "backend option adj[{arg}] is invalid; expected adj[dense] or adj[sparse]"
1994 ));
1995 }
1996 } else {
1997 return Err(format!(
1998 "backend option '{name}' is unknown; supported options: purify[...], adj[dense|sparse]"
1999 ));
2000 }
2001 }
2002
2003 Ok((purifier, adjacency))
2004}
2005
2006impl std::str::FromStr for Backend {
2007 type Err = String;
2008
2009 fn from_str(value: &str) -> Result<Self, Self::Err> {
2010 let raw = value.trim();
2011 if raw.is_empty() {
2012 return Err("backend spec cannot be empty".to_string());
2013 }
2014
2015 let (kind_part, num_part) = raw
2016 .split_once(':')
2017 .map(|(k, n)| (k.trim(), Some(n.trim())))
2018 .unwrap_or((raw.trim(), None));
2019
2020 let mut kind_part = kind_part;
2021 let num_part = num_part;
2022 let mut purifier = None;
2023 let mut adjacency = RequestedAdjacency::Default;
2024
2025 if let Some((candidate_kind, inner)) = split_backend_bracket_group(kind_part)? {
2026 let inner = inner.trim();
2027 if inner.eq_ignore_ascii_case("dense") || inner.eq_ignore_ascii_case("sparse") {
2028 return Err(format!(
2029 "backend spec '{value}' uses legacy adjacency syntax '[{inner}]'; use '[adj[{inner}]]' instead"
2030 ));
2031 }
2032
2033 let candidate_kind = candidate_kind.trim_end();
2034 if candidate_kind.is_empty() {
2035 return Err("backend spec cannot be empty".to_string());
2036 }
2037
2038 (purifier, adjacency) = parse_backend_options(inner)?;
2039 kind_part = candidate_kind;
2040 }
2041
2042 if let Some(num) = num_part {
2043 let lowered = num.to_ascii_lowercase();
2044 if lowered.contains("purify[") || lowered.contains("adj[") {
2045 return Err(format!(
2046 "backend spec '{value}' contains backend options after ':'; \
2047write backend options immediately after the backend kind, before ':' (e.g. 'howzat-dd[purify[snap]]:f64')"
2048 ));
2049 }
2050 if !kind_part.to_ascii_lowercase().starts_with("howzat-dd") && num.contains('[') {
2051 return Err(format!(
2052 "backend spec '{value}' contains an unexpected '[' after ':'; \
2053backend options must appear after the backend kind, before ':'"
2054 ));
2055 }
2056 }
2057
2058 let spec_string = if let Some(num) = num_part {
2059 format!("{kind_part}:{num}")
2060 } else {
2061 kind_part.to_string()
2062 };
2063
2064 let mut spec: BackendSpec = spec_string.parse()?;
2065
2066 if let Some(purifier) = purifier {
2067 match &mut spec {
2068 BackendSpec::HowzatDd {
2069 purifier: spec_purifier,
2070 ..
2071 } => {
2072 *spec_purifier = Some(purifier);
2073 }
2074 _ => {
2075 return Err(format!(
2076 "backend spec '{value}' does not support purify[...] (only howzat-dd does)"
2077 ));
2078 }
2079 }
2080 }
2081
2082 match adjacency {
2083 RequestedAdjacency::Default => {}
2084 RequestedAdjacency::Dense => {
2085 if !spec.supports_dense_adjacency() {
2086 return Err(format!(
2087 "backend spec '{value}' does not support adj[dense] adjacency"
2088 ));
2089 }
2090 }
2091 RequestedAdjacency::Sparse => {
2092 if !spec.supports_sparse_adjacency() {
2093 return Err(format!(
2094 "backend spec '{value}' does not support adj[sparse] adjacency"
2095 ));
2096 }
2097 }
2098 }
2099
2100 Ok(Self(spec, adjacency))
2101 }
2102}
2103
2104#[derive(Clone, Debug, Eq, PartialEq)]
2105pub struct BackendArg {
2106 pub spec: Backend,
2107 pub authoritative: bool,
2108 pub perf_baseline: bool,
2109}
2110
2111impl std::str::FromStr for BackendArg {
2112 type Err = String;
2113
2114 fn from_str(value: &str) -> Result<Self, Self::Err> {
2115 let raw = value.trim();
2116 if raw.is_empty() {
2117 return Err("backend spec cannot be empty".to_string());
2118 }
2119
2120 let mut authoritative = false;
2121 let mut perf_baseline = false;
2122 let mut rest = raw;
2123
2124 loop {
2125 let Some(prefix) = rest.chars().next() else {
2126 break;
2127 };
2128 match prefix {
2129 '^' => {
2130 if authoritative {
2131 return Err(format!(
2132 "backend spec '{value}' contains multiple '^' prefixes"
2133 ));
2134 }
2135 authoritative = true;
2136 rest = &rest['^'.len_utf8()..];
2137 }
2138 '%' => {
2139 if perf_baseline {
2140 return Err(format!(
2141 "backend spec '{value}' contains multiple '%' prefixes"
2142 ));
2143 }
2144 perf_baseline = true;
2145 rest = &rest['%'.len_utf8()..];
2146 }
2147 _ => break,
2148 }
2149 }
2150
2151 let spec: Backend = rest.trim().parse()?;
2152 Ok(Self {
2153 spec,
2154 authoritative,
2155 perf_baseline,
2156 })
2157 }
2158}
2159
2160#[derive(Clone, Debug)]
2161pub struct BackendRunConfig {
2162 pub howzat_options: ConeOptions,
2163 pub output_incidence: bool,
2164 pub output_adjacency: bool,
2165 pub output_coefficients: bool,
2166 pub howzat_output_adjacency: AdjacencyOutput,
2167 pub timing_detail: bool,
2168}
2169
2170#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
2171pub enum BackendOutputLevel {
2172 Representation,
2173 Incidence,
2174 Adjacency,
2175}
2176
2177impl BackendOutputLevel {
2178 pub fn as_str(self) -> &'static str {
2179 match self {
2180 Self::Representation => "representation",
2181 Self::Incidence => "incidence",
2182 Self::Adjacency => "adjacency",
2183 }
2184 }
2185}
2186
2187impl Default for BackendRunConfig {
2188 fn default() -> Self {
2189 Self {
2190 howzat_options: ConeOptions::default(),
2191 output_incidence: true,
2192 output_adjacency: true,
2193 output_coefficients: false,
2194 howzat_output_adjacency: AdjacencyOutput::List,
2195 timing_detail: false,
2196 }
2197 }
2198}
2199
2200#[derive(Copy, Clone, Debug, Eq, PartialEq)]
2201pub enum CoefficientMode {
2202 Off,
2203 F64,
2204 Exact,
2205}
2206
2207#[derive(Debug, Clone)]
2208pub struct RowMajorMatrix<N> {
2209 pub rows: usize,
2210 pub cols: usize,
2211 pub data: Vec<N>,
2212}
2213
2214impl<N> RowMajorMatrix<N> {
2215 pub fn row(&self, row: usize) -> &[N] {
2216 assert!(row < self.rows);
2217 let start = row * self.cols;
2218 &self.data[start..start + self.cols]
2219 }
2220}
2221
2222#[derive(Debug, Clone, Copy)]
2223pub struct RowMajorMatrixView<'a, N> {
2224 pub rows: usize,
2225 pub cols: usize,
2226 pub data: &'a [N],
2227}
2228
2229impl<'a, N> RowMajorMatrixView<'a, N> {
2230 pub fn row(&self, row: usize) -> &'a [N] {
2231 assert!(row < self.rows);
2232 let start = row * self.cols;
2233 &self.data[start..start + self.cols]
2234 }
2235}
2236
2237#[derive(Debug, Clone)]
2238pub enum CoefficientMatrix {
2239 F64(RowMajorMatrix<f64>),
2240 RugFloat128(RowMajorMatrix<calculo::num::RugFloat<128>>),
2241 RugFloat256(RowMajorMatrix<calculo::num::RugFloat<256>>),
2242 RugFloat512(RowMajorMatrix<calculo::num::RugFloat<512>>),
2243 DashuFloat128(RowMajorMatrix<calculo::num::DashuFloat<128>>),
2244 DashuFloat256(RowMajorMatrix<calculo::num::DashuFloat<256>>),
2245 DashuFloat512(RowMajorMatrix<calculo::num::DashuFloat<512>>),
2246 RugRat(RowMajorMatrix<calculo::num::RugRat>),
2247 DashuRat(RowMajorMatrix<calculo::num::DashuRat>),
2248}
2249
2250mod coefficient_scalar_sealed {
2251 pub trait Sealed {}
2252}
2253
2254pub trait CoefficientScalar: calculo::num::Num + coefficient_scalar_sealed::Sealed + 'static {
2255 fn wrap(matrix: RowMajorMatrix<Self>) -> CoefficientMatrix;
2256
2257 fn view(matrix: &CoefficientMatrix) -> Option<RowMajorMatrixView<'_, Self>>;
2258
2259 fn coerce_from_matrix(
2260 matrix: &CoefficientMatrix,
2261 ) -> Result<RowMajorMatrix<Self>, calculo::num::ConversionError>;
2262}
2263
2264fn coerce_matrix_via_f64<N: calculo::num::Num>(
2265 matrix: &CoefficientMatrix,
2266) -> Result<RowMajorMatrix<N>, calculo::num::ConversionError> {
2267 #[inline(always)]
2268 fn push<N: calculo::num::Num>(
2269 out: &mut Vec<N>,
2270 value: f64,
2271 ) -> Result<(), calculo::num::ConversionError> {
2272 if !value.is_finite() {
2273 return Err(calculo::num::ConversionError);
2274 }
2275 out.push(N::try_from_f64(value).ok_or(calculo::num::ConversionError)?);
2276 Ok(())
2277 }
2278
2279 let rows = matrix.rows();
2280 let cols = matrix.cols();
2281 let mut out = Vec::with_capacity(rows.saturating_mul(cols));
2282 match matrix {
2283 CoefficientMatrix::F64(m) => {
2284 for &v in &m.data {
2285 push(&mut out, v)?;
2286 }
2287 }
2288 CoefficientMatrix::RugFloat128(m) => {
2289 for v in &m.data {
2290 push(&mut out, v.to_f64())?;
2291 }
2292 }
2293 CoefficientMatrix::RugFloat256(m) => {
2294 for v in &m.data {
2295 push(&mut out, v.to_f64())?;
2296 }
2297 }
2298 CoefficientMatrix::RugFloat512(m) => {
2299 for v in &m.data {
2300 push(&mut out, v.to_f64())?;
2301 }
2302 }
2303 CoefficientMatrix::DashuFloat128(m) => {
2304 for v in &m.data {
2305 push(&mut out, v.to_f64())?;
2306 }
2307 }
2308 CoefficientMatrix::DashuFloat256(m) => {
2309 for v in &m.data {
2310 push(&mut out, v.to_f64())?;
2311 }
2312 }
2313 CoefficientMatrix::DashuFloat512(m) => {
2314 for v in &m.data {
2315 push(&mut out, v.to_f64())?;
2316 }
2317 }
2318 CoefficientMatrix::RugRat(m) => {
2319 for v in &m.data {
2320 push(&mut out, v.to_f64())?;
2321 }
2322 }
2323 CoefficientMatrix::DashuRat(m) => {
2324 for v in &m.data {
2325 push(&mut out, v.to_f64())?;
2326 }
2327 }
2328 }
2329
2330 Ok(RowMajorMatrix { rows, cols, data: out })
2331}
2332
2333fn dashu_ibig_to_rug_integer(
2334 value: &<calculo::num::DashuRat as calculo::num::Rat>::Int,
2335) -> rug::Integer {
2336 use rug::integer::Order;
2337
2338 let (sign, words) = value.as_sign_words();
2339 let mut out = rug::Integer::from_digits(words, Order::Lsf);
2340 let is_negative: bool = sign.into();
2341 if is_negative {
2342 out = -out;
2343 }
2344 out
2345}
2346
2347fn coerce_matrix_to_rug_rat(
2348 matrix: &CoefficientMatrix,
2349) -> Result<RowMajorMatrix<calculo::num::RugRat>, calculo::num::ConversionError> {
2350 use calculo::num::CoerceFrom as _;
2351 use calculo::num::Rat as _;
2352
2353 fn from_dashu_rat(
2354 value: &calculo::num::DashuRat,
2355 ) -> Result<calculo::num::RugRat, calculo::num::ConversionError> {
2356 let (numer, denom) = value.clone().into_parts();
2357 let numer = dashu_ibig_to_rug_integer(&numer);
2358 let denom = dashu_ibig_to_rug_integer(&denom);
2359 if denom == 0 {
2360 return Err(calculo::num::ConversionError);
2361 }
2362 Ok(calculo::num::RugRat(rug::Rational::from((numer, denom))))
2363 }
2364
2365 let rows = matrix.rows();
2366 let cols = matrix.cols();
2367 let mut out = Vec::with_capacity(rows.saturating_mul(cols));
2368
2369 match matrix {
2370 CoefficientMatrix::F64(m) => {
2371 for &v in &m.data {
2372 if !v.is_finite() {
2373 return Err(calculo::num::ConversionError);
2374 }
2375 out.push(
2376 calculo::num::RugRat::try_from_f64(v).ok_or(calculo::num::ConversionError)?,
2377 );
2378 }
2379 }
2380 CoefficientMatrix::RugFloat128(m) => {
2381 for v in &m.data {
2382 let rat = v.0.to_rational().ok_or(calculo::num::ConversionError)?;
2383 out.push(calculo::num::RugRat(rat));
2384 }
2385 }
2386 CoefficientMatrix::RugFloat256(m) => {
2387 for v in &m.data {
2388 let rat = v.0.to_rational().ok_or(calculo::num::ConversionError)?;
2389 out.push(calculo::num::RugRat(rat));
2390 }
2391 }
2392 CoefficientMatrix::RugFloat512(m) => {
2393 for v in &m.data {
2394 let rat = v.0.to_rational().ok_or(calculo::num::ConversionError)?;
2395 out.push(calculo::num::RugRat(rat));
2396 }
2397 }
2398 CoefficientMatrix::DashuFloat128(m) => {
2399 for v in &m.data {
2400 let dashu_rat = calculo::num::DashuRat::coerce_from(v)?;
2401 out.push(from_dashu_rat(&dashu_rat)?);
2402 }
2403 }
2404 CoefficientMatrix::DashuFloat256(m) => {
2405 for v in &m.data {
2406 let dashu_rat = calculo::num::DashuRat::coerce_from(v)?;
2407 out.push(from_dashu_rat(&dashu_rat)?);
2408 }
2409 }
2410 CoefficientMatrix::DashuFloat512(m) => {
2411 for v in &m.data {
2412 let dashu_rat = calculo::num::DashuRat::coerce_from(v)?;
2413 out.push(from_dashu_rat(&dashu_rat)?);
2414 }
2415 }
2416 CoefficientMatrix::RugRat(m) => out.extend(m.data.iter().cloned()),
2417 CoefficientMatrix::DashuRat(m) => {
2418 for v in &m.data {
2419 out.push(from_dashu_rat(v)?);
2420 }
2421 }
2422 }
2423
2424 Ok(RowMajorMatrix { rows, cols, data: out })
2425}
2426
2427macro_rules! impl_coeff_scalar_via_f64 {
2428 ($ty:ty, $variant:ident) => {
2429 impl coefficient_scalar_sealed::Sealed for $ty {}
2430
2431 impl CoefficientScalar for $ty {
2432 #[inline(always)]
2433 fn wrap(matrix: RowMajorMatrix<Self>) -> CoefficientMatrix {
2434 CoefficientMatrix::$variant(matrix)
2435 }
2436
2437 #[inline(always)]
2438 fn view(matrix: &CoefficientMatrix) -> Option<RowMajorMatrixView<'_, Self>> {
2439 let CoefficientMatrix::$variant(m) = matrix else {
2440 return None;
2441 };
2442 Some(RowMajorMatrixView {
2443 rows: m.rows,
2444 cols: m.cols,
2445 data: &m.data,
2446 })
2447 }
2448
2449 #[inline(always)]
2450 fn coerce_from_matrix(
2451 matrix: &CoefficientMatrix,
2452 ) -> Result<RowMajorMatrix<Self>, calculo::num::ConversionError> {
2453 if let Some(view) = Self::view(matrix) {
2454 return Ok(RowMajorMatrix {
2455 rows: view.rows,
2456 cols: view.cols,
2457 data: view.data.to_vec(),
2458 });
2459 }
2460 coerce_matrix_via_f64(matrix)
2461 }
2462 }
2463 };
2464}
2465
2466impl_coeff_scalar_via_f64!(f64, F64);
2467impl_coeff_scalar_via_f64!(calculo::num::RugFloat<128>, RugFloat128);
2468impl_coeff_scalar_via_f64!(calculo::num::RugFloat<256>, RugFloat256);
2469impl_coeff_scalar_via_f64!(calculo::num::RugFloat<512>, RugFloat512);
2470impl_coeff_scalar_via_f64!(calculo::num::DashuFloat<128>, DashuFloat128);
2471impl_coeff_scalar_via_f64!(calculo::num::DashuFloat<256>, DashuFloat256);
2472impl_coeff_scalar_via_f64!(calculo::num::DashuFloat<512>, DashuFloat512);
2473impl_coeff_scalar_via_f64!(calculo::num::DashuRat, DashuRat);
2474
2475impl coefficient_scalar_sealed::Sealed for calculo::num::RugRat {}
2476
2477impl CoefficientScalar for calculo::num::RugRat {
2478 #[inline(always)]
2479 fn wrap(matrix: RowMajorMatrix<Self>) -> CoefficientMatrix {
2480 CoefficientMatrix::RugRat(matrix)
2481 }
2482
2483 #[inline(always)]
2484 fn view(matrix: &CoefficientMatrix) -> Option<RowMajorMatrixView<'_, Self>> {
2485 let CoefficientMatrix::RugRat(m) = matrix else {
2486 return None;
2487 };
2488 Some(RowMajorMatrixView {
2489 rows: m.rows,
2490 cols: m.cols,
2491 data: &m.data,
2492 })
2493 }
2494
2495 #[inline(always)]
2496 fn coerce_from_matrix(
2497 matrix: &CoefficientMatrix,
2498 ) -> Result<RowMajorMatrix<Self>, calculo::num::ConversionError> {
2499 coerce_matrix_to_rug_rat(matrix)
2500 }
2501}
2502
2503impl CoefficientMatrix {
2504 pub fn rows(&self) -> usize {
2505 match self {
2506 Self::F64(m) => m.rows,
2507 Self::RugFloat128(m) => m.rows,
2508 Self::RugFloat256(m) => m.rows,
2509 Self::RugFloat512(m) => m.rows,
2510 Self::DashuFloat128(m) => m.rows,
2511 Self::DashuFloat256(m) => m.rows,
2512 Self::DashuFloat512(m) => m.rows,
2513 Self::RugRat(m) => m.rows,
2514 Self::DashuRat(m) => m.rows,
2515 }
2516 }
2517
2518 pub fn cols(&self) -> usize {
2519 match self {
2520 Self::F64(m) => m.cols,
2521 Self::RugFloat128(m) => m.cols,
2522 Self::RugFloat256(m) => m.cols,
2523 Self::RugFloat512(m) => m.cols,
2524 Self::DashuFloat128(m) => m.cols,
2525 Self::DashuFloat256(m) => m.cols,
2526 Self::DashuFloat512(m) => m.cols,
2527 Self::RugRat(m) => m.cols,
2528 Self::DashuRat(m) => m.cols,
2529 }
2530 }
2531
2532 pub fn cast<N: CoefficientScalar>(&self) -> RowMajorMatrixView<'_, N> {
2533 N::view(self).unwrap_or_else(|| {
2534 panic!(
2535 "CoefficientMatrix::cast<{}> called on {self:?}",
2536 std::any::type_name::<N>()
2537 )
2538 })
2539 }
2540
2541 pub fn coerce<N: CoefficientScalar>(
2542 &self,
2543 ) -> Result<RowMajorMatrix<N>, calculo::num::ConversionError> {
2544 N::coerce_from_matrix(self)
2545 }
2546
2547 pub fn stringify(&self) -> Result<RowMajorMatrix<String>, calculo::num::ConversionError> {
2548 let m = self.coerce::<calculo::num::RugRat>()?;
2549 Ok(RowMajorMatrix {
2550 rows: m.rows,
2551 cols: m.cols,
2552 data: m.data.iter().map(ToString::to_string).collect(),
2553 })
2554 }
2555
2556 pub fn from_num<N: CoefficientScalar>(rows: usize, cols: usize, data: Vec<N>) -> Self {
2557 N::wrap(RowMajorMatrix { rows, cols, data })
2558 }
2559}
2560
2561impl serde::Serialize for CoefficientMatrix {
2562 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2563 where
2564 S: serde::Serializer,
2565 {
2566 use serde::ser::SerializeSeq;
2567
2568 fn serialize_rows<S>(
2569 rows: usize,
2570 cols: usize,
2571 data: &[f64],
2572 serializer: S,
2573 ) -> Result<S::Ok, S::Error>
2574 where
2575 S: serde::Serializer,
2576 {
2577 let mut seq = serializer.serialize_seq(Some(rows))?;
2578 for row in 0..rows {
2579 let start = row * cols;
2580 seq.serialize_element(&data[start..start + cols])?;
2581 }
2582 seq.end()
2583 }
2584
2585 match self {
2586 Self::F64(m) => serialize_rows(m.rows, m.cols, &m.data, serializer),
2587 _ => {
2588 let m = self.coerce::<f64>().map_err(serde::ser::Error::custom)?;
2589 serialize_rows(m.rows, m.cols, &m.data, serializer)
2590 }
2591 }
2592 }
2593}
2594
2595impl<'de> serde::Deserialize<'de> for CoefficientMatrix {
2596 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2597 where
2598 D: serde::Deserializer<'de>,
2599 {
2600 let rows: Vec<Vec<f64>> = Vec::deserialize(deserializer)?;
2601 let row_count = rows.len();
2602 let col_count = rows.first().map_or(0, Vec::len);
2603
2604 if rows.iter().any(|row| row.len() != col_count) {
2605 return Err(serde::de::Error::custom(
2606 "coefficient matrix is ragged (inconsistent row widths)",
2607 ));
2608 }
2609
2610 let mut data = Vec::with_capacity(row_count.saturating_mul(col_count));
2611 for row in rows {
2612 data.extend(row);
2613 }
2614 Ok(Self::F64(RowMajorMatrix {
2615 rows: row_count,
2616 cols: col_count,
2617 data,
2618 }))
2619 }
2620}
2621
2622#[derive(Debug, Clone)]
2623pub struct AnyPolytopeCoefficients {
2624 pub generators: CoefficientMatrix,
2625 pub inequalities: CoefficientMatrix,
2626}
2627
2628#[derive(Debug, Clone, Copy)]
2629pub struct PolytopeCoefficientsView<'a, N> {
2630 pub generators: RowMajorMatrixView<'a, N>,
2631 pub inequalities: RowMajorMatrixView<'a, N>,
2632}
2633
2634#[derive(Debug, Clone)]
2635pub struct PolytopeCoefficients<N> {
2636 pub generators: RowMajorMatrix<N>,
2637 pub inequalities: RowMajorMatrix<N>,
2638}
2639
2640impl AnyPolytopeCoefficients {
2641 pub fn cast<N: CoefficientScalar>(&self) -> PolytopeCoefficientsView<'_, N> {
2642 PolytopeCoefficientsView {
2643 generators: self.generators.cast::<N>(),
2644 inequalities: self.inequalities.cast::<N>(),
2645 }
2646 }
2647
2648 pub fn coerce<N: CoefficientScalar>(
2649 &self,
2650 ) -> Result<PolytopeCoefficients<N>, calculo::num::ConversionError> {
2651 Ok(PolytopeCoefficients {
2652 generators: self.generators.coerce::<N>()?,
2653 inequalities: self.inequalities.coerce::<N>()?,
2654 })
2655 }
2656
2657 pub fn stringify(&self) -> Result<PolytopeCoefficients<String>, calculo::num::ConversionError> {
2658 Ok(PolytopeCoefficients {
2659 generators: self.generators.stringify()?,
2660 inequalities: self.inequalities.stringify()?,
2661 })
2662 }
2663}
2664
2665#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2666pub struct Stats {
2667 pub dimension: usize,
2668 pub vertices: usize,
2669 pub facets: usize,
2670 pub ridges: usize,
2671}
2672
2673#[derive(Debug, Clone, Serialize, Deserialize)]
2674pub struct BackendTiming {
2675 pub total: Duration,
2676 pub fast: Option<Duration>,
2677 pub resolve: Option<Duration>,
2678 pub exact: Option<Duration>,
2679}
2680
2681#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2682pub struct CddlibTimingDetail {
2683 pub build: Duration,
2684 pub incidence: Duration,
2685 pub vertex_adjacency: Duration,
2686 pub facet_adjacency: Duration,
2687 pub vertex_positions: Duration,
2688 pub post_inc: Duration,
2689 pub post_v_adj: Duration,
2690 pub post_f_adj: Duration,
2691}
2692
2693#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2694pub struct HowzatDdTimingDetail {
2695 pub fast_matrix: Duration,
2696 pub fast_dd: Duration,
2697 pub cert: Duration,
2698 pub repair_partial: Duration,
2699 pub repair_graph: Duration,
2700 pub exact_matrix: Duration,
2701 pub exact_dd: Duration,
2702 pub incidence: Duration,
2703 pub vertex_adjacency: Duration,
2704 pub facet_adjacency: Duration,
2705}
2706
2707#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2708pub struct HowzatLrsTimingDetail {
2709 pub matrix: Duration,
2710 pub lrs: Duration,
2711 pub cert: Duration,
2712 pub incidence: Duration,
2713 pub vertex_adjacency: Duration,
2714 pub facet_adjacency: Duration,
2715}
2716
2717#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2718pub struct LrslibTimingDetail {
2719 pub build: Duration,
2720 pub incidence: Duration,
2721 pub vertex_adjacency: Duration,
2722 pub facet_adjacency: Duration,
2723 pub post_inc: Duration,
2724 pub post_v_adj: Duration,
2725 pub post_f_adj: Duration,
2726}
2727
2728#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2729pub struct PplTimingDetail {
2730 pub build: Duration,
2731 pub incidence: Duration,
2732 pub vertex_adjacency: Duration,
2733 pub facet_adjacency: Duration,
2734 pub post_inc: Duration,
2735 pub post_v_adj: Duration,
2736 pub post_f_adj: Duration,
2737}
2738
2739#[derive(Debug, Clone, Serialize, Deserialize)]
2740pub enum TimingDetail {
2741 Cddlib(CddlibTimingDetail),
2742 HowzatDd(HowzatDdTimingDetail),
2743 HowzatLrs(HowzatLrsTimingDetail),
2744 Lrslib(LrslibTimingDetail),
2745 Ppl(PplTimingDetail),
2746}
2747
2748#[derive(Debug, Clone, Serialize, Deserialize)]
2749pub struct BackendRun<Inc = SetFamily, Adj = SetFamily> {
2750 pub spec: Backend,
2751 pub stats: Stats,
2752 pub timing: BackendTiming,
2753 pub facets: Option<Vec<Vec<f64>>>,
2754 #[serde(skip, default)]
2755 pub coefficients: Option<AnyPolytopeCoefficients>,
2756 pub geometry: BackendGeometry<Inc, Adj>,
2757 pub fails: usize,
2758 pub fallbacks: usize,
2759 pub error: Option<String>,
2760 pub detail: Option<TimingDetail>,
2761}
2762
2763#[derive(Debug, Clone, Serialize, Deserialize)]
2764pub enum BackendGeometry<Inc = SetFamily, Adj = SetFamily> {
2765 Baseline(BaselineGeometry<Inc, Adj>),
2766 Input(InputGeometry<Inc, Adj>),
2767}
2768
2769#[derive(Debug, Clone, Serialize, Deserialize)]
2770pub struct BaselineGeometry<Inc = SetFamily, Adj = SetFamily> {
2771 pub vertex_positions: CoefficientMatrix,
2772 pub vertex_adjacency: Adj,
2773 pub facets_to_vertices: Inc,
2774 pub facet_adjacency: Adj,
2775}
2776
2777#[derive(Debug, Clone, Serialize, Deserialize)]
2778pub struct InputGeometry<Inc = SetFamily, Adj = SetFamily> {
2779 pub vertex_adjacency: Adj,
2780 pub facets_to_vertices: Inc,
2781 pub facet_adjacency: Adj,
2782}
2783
2784#[derive(Debug, Clone, Serialize, Deserialize)]
2785pub enum BackendRunAny {
2786 Dense(BackendRun<SetFamily, SetFamily>),
2787 Sparse(BackendRun<ListFamily, AdjacencyList>),
2788}
2789
2790#[cfg(test)]
2791mod tests {
2792 use super::*;
2793
2794 #[test]
2795 fn parse_backend_accepts_adj_option() {
2796 let backend: Backend = "howzat-dd[adj[dense]]:f64".parse().unwrap();
2797 assert_eq!(backend.to_string(), "howzat-dd[adj[dense]]:f64");
2798 }
2799
2800 #[test]
2801 fn parse_backend_rejects_cddlib_dense_adj() {
2802 assert!("cddlib[adj[dense]]:gmprational".parse::<Backend>().is_err());
2803 }
2804
2805 #[test]
2806 fn parse_backend_accepts_sparse_adj_for_cddlib() {
2807 let backend: Backend = "cddlib[adj[sparse]]:gmprational".parse().unwrap();
2808 assert_eq!(backend.to_string(), "cddlib[adj[sparse]]:gmprational");
2809 }
2810
2811 #[test]
2812 fn parse_backend_spec_accepts_f64_eps_syntax() {
2813 let spec: Backend = "howzat-dd[purify[snap]]:f64[min,eps[1e-12]]"
2814 .parse()
2815 .unwrap();
2816 assert_eq!(
2817 spec.to_string(),
2818 "howzat-dd[purify[snap]]:f64[eps[1e-12],min]"
2819 );
2820
2821 let spec: Backend = "howzat-dd:f64[eps[0.0]]".parse().unwrap();
2822 assert!(matches!(spec.0, BackendSpec::HowzatDd { .. }));
2823 }
2824
2825 #[test]
2826 fn parse_backend_spec_canonicalizes_backend_options() {
2827 let spec: Backend = "howzat-dd[adj[sparse],purify[snap]]:f64".parse().unwrap();
2828 assert_eq!(spec.to_string(), "howzat-dd[purify[snap],adj[sparse]]:f64");
2829 }
2830
2831 #[test]
2832 fn parse_backend_accepts_howzat_dd_umpire_selectors() {
2833 let spec: Backend = "howzat-dd@int:gmprat".parse().unwrap();
2834 assert_eq!(spec.to_string(), "howzat-dd@int:gmprat");
2835
2836 let spec: Backend = "howzat-dd@sp:gmprat".parse().unwrap();
2837 assert_eq!(spec.to_string(), "howzat-dd@sp:gmprat");
2838 }
2839
2840 #[test]
2841 fn parse_backend_arg_accepts_marker_prefixes() {
2842 let arg: BackendArg = "cddlib:f64".parse().unwrap();
2843 assert_eq!(arg.spec.to_string(), "cddlib:f64");
2844 assert!(!arg.authoritative);
2845 assert!(!arg.perf_baseline);
2846
2847 let arg: BackendArg = "^cddlib:f64".parse().unwrap();
2848 assert!(arg.authoritative);
2849 assert!(!arg.perf_baseline);
2850
2851 let arg: BackendArg = "%cddlib:f64".parse().unwrap();
2852 assert!(!arg.authoritative);
2853 assert!(arg.perf_baseline);
2854
2855 let arg: BackendArg = "^%howzat-dd[purify[snap]]:f64".parse().unwrap();
2856 assert_eq!(arg.spec.to_string(), "howzat-dd[purify[snap]]:f64");
2857 assert!(arg.authoritative);
2858 assert!(arg.perf_baseline);
2859
2860 let arg: BackendArg = "%^cddlib:gmprational".parse().unwrap();
2861 assert!(arg.authoritative);
2862 assert!(arg.perf_baseline);
2863 }
2864
2865 #[test]
2866 fn parse_backend_arg_rejects_duplicate_marker_prefixes() {
2867 let err = "^^cddlib:f64".parse::<BackendArg>().unwrap_err();
2868 assert!(
2869 err.contains("multiple '^'"),
2870 "expected '^'-prefix error, got: {err}"
2871 );
2872
2873 let err = "%%cddlib:f64".parse::<BackendArg>().unwrap_err();
2874 assert!(
2875 err.contains("multiple '%'"),
2876 "expected '%'-prefix error, got: {err}"
2877 );
2878 }
2879}