Skip to main content

howzat_kit/backend/
mod.rs

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}