Skip to main content

hullabaloo_core/
adjacency.rs

1use ahash::AHashMap;
2use smallvec::SmallVec;
3
4use crate::adjacency_list::AdjacencyList;
5use crate::set_family::{SetFamily, SetFamilyBuilder};
6use crate::types::{RowId, RowSet};
7
8type Key = SmallVec<[usize; 16]>;
9
10/// Read-only view of an undirected adjacency structure.
11pub trait Graph {
12    fn node_count(&self) -> usize;
13    fn degree(&self, node: usize) -> usize;
14    fn contains(&self, node: usize, neighbor: usize) -> bool;
15    fn neighbors(&self, node: usize) -> impl Iterator<Item = usize> + '_;
16}
17
18/// Marker trait for adjacency structures that can be built by the hullabaloo adjacency engine.
19pub trait AdjacencyStore: Graph + Sized {
20    type Builder: AdjacencyBuilder<Output = Self>;
21}
22
23/// A sink for building an undirected adjacency structure.
24///
25/// This is generic so the core FR/adjacency algorithm can write into either an edge-list style
26/// adjacency (`AdjacencyList`) or a bitset-based adjacency (e.g. `howzat::SetFamily`) without paying for
27/// conversions.
28pub trait AdjacencyBuilder {
29    type Output;
30
31    fn new(node_count: usize) -> Self;
32    fn add_undirected_edge(&mut self, a: usize, b: usize);
33    fn finish(self) -> Self::Output;
34}
35
36#[derive(Clone, Debug)]
37pub struct AdjacencyListBuilder {
38    adjacency: Vec<Vec<usize>>,
39}
40
41impl AdjacencyBuilder for AdjacencyListBuilder {
42    type Output = AdjacencyList;
43
44    #[inline]
45    fn new(node_count: usize) -> Self {
46        Self {
47            adjacency: vec![Vec::new(); node_count],
48        }
49    }
50
51    #[inline]
52    fn add_undirected_edge(&mut self, a: usize, b: usize) {
53        self.adjacency[a].push(b);
54        self.adjacency[b].push(a);
55    }
56
57    #[inline]
58    fn finish(mut self) -> Self::Output {
59        for neigh in &mut self.adjacency {
60            neigh.sort_unstable();
61            neigh.dedup();
62        }
63        AdjacencyList::from_sorted_adjacency_lists(self.adjacency)
64    }
65}
66
67impl Graph for AdjacencyList {
68    #[inline]
69    fn node_count(&self) -> usize {
70        self.num_vertices()
71    }
72
73    #[inline]
74    fn degree(&self, node: usize) -> usize {
75        self.adjacency_lists()[node].len()
76    }
77
78    #[inline]
79    fn contains(&self, node: usize, neighbor: usize) -> bool {
80        self.adjacency_lists()[node]
81            .binary_search(&neighbor)
82            .is_ok()
83    }
84
85    #[inline]
86    fn neighbors(&self, node: usize) -> impl Iterator<Item = usize> + '_ {
87        self.adjacency_lists()[node].iter().copied()
88    }
89}
90
91impl AdjacencyStore for AdjacencyList {
92    type Builder = AdjacencyListBuilder;
93}
94
95impl AdjacencyBuilder for SetFamilyBuilder {
96    type Output = SetFamily;
97
98    #[inline]
99    fn new(node_count: usize) -> Self {
100        SetFamily::builder(node_count, node_count)
101    }
102
103    #[inline]
104    fn add_undirected_edge(&mut self, a: usize, b: usize) {
105        self.insert_into_set(a, RowId::new(b));
106        self.insert_into_set(b, RowId::new(a));
107    }
108
109    #[inline]
110    fn finish(self) -> Self::Output {
111        self.build()
112    }
113}
114
115impl Graph for SetFamily {
116    #[inline]
117    fn node_count(&self) -> usize {
118        self.family_size()
119    }
120
121    #[inline]
122    fn degree(&self, node: usize) -> usize {
123        self.sets()[node].cardinality()
124    }
125
126    #[inline]
127    fn contains(&self, node: usize, neighbor: usize) -> bool {
128        self.sets()[node].contains(neighbor)
129    }
130
131    #[inline]
132    fn neighbors(&self, node: usize) -> impl Iterator<Item = usize> + '_ {
133        self.sets()[node].iter().raw()
134    }
135}
136
137impl AdjacencyStore for SetFamily {
138    type Builder = SetFamilyBuilder;
139}
140
141#[derive(Clone, Debug)]
142struct SparseRowMembershipIndex {
143    row_offsets: Vec<usize>,
144    members: Vec<usize>,
145}
146
147impl SparseRowMembershipIndex {
148    fn new<R: AsRef<[usize]>>(rows_by_node: &[R], row_capacity: usize) -> Self {
149        let mut degrees: Vec<usize> = vec![0; row_capacity];
150        for rows in rows_by_node {
151            for &row in rows.as_ref() {
152                if row < row_capacity {
153                    degrees[row] = degrees[row].saturating_add(1);
154                }
155            }
156        }
157
158        let mut row_offsets: Vec<usize> = vec![0; row_capacity.saturating_add(1)];
159        for row in 0..row_capacity {
160            row_offsets[row + 1] = row_offsets[row].saturating_add(degrees[row]);
161        }
162
163        let mut members: Vec<usize> = vec![0; row_offsets[row_capacity]];
164        let mut cursor = row_offsets.clone();
165        for (node, rows) in rows_by_node.iter().enumerate() {
166            for &row in rows.as_ref() {
167                if row >= row_capacity {
168                    continue;
169                }
170                let pos = cursor[row];
171                if pos < members.len() {
172                    members[pos] = node;
173                    cursor[row] = pos + 1;
174                }
175            }
176        }
177
178        Self {
179            row_offsets,
180            members,
181        }
182    }
183
184    #[inline]
185    fn members_of_row(&self, row: usize) -> &[usize] {
186        let Some((&start, &end)) = self
187            .row_offsets
188            .get(row)
189            .zip(self.row_offsets.get(row.saturating_add(1)))
190        else {
191            return &[];
192        };
193        self.members.get(start..end).unwrap_or(&[])
194    }
195
196    #[inline]
197    fn row_contains_node(&self, row: usize, node: usize) -> bool {
198        self.members_of_row(row).binary_search(&node).is_ok()
199    }
200}
201
202#[inline]
203fn intersect_sorted_rows(a: &[usize], b: &[usize], out: &mut Vec<usize>) {
204    out.clear();
205    let mut i = 0usize;
206    let mut j = 0usize;
207    while i < a.len() && j < b.len() {
208        match a[i].cmp(&b[j]) {
209            std::cmp::Ordering::Less => i += 1,
210            std::cmp::Ordering::Greater => j += 1,
211            std::cmp::Ordering::Equal => {
212                out.push(a[i]);
213                i += 1;
214                j += 1;
215            }
216        }
217    }
218}
219
220fn has_third_node_containing_face(
221    membership: &SparseRowMembershipIndex,
222    face_rows: &[usize],
223    node_a: usize,
224    node_b: usize,
225    non_excluded_nodes: usize,
226) -> bool {
227    if face_rows.is_empty() {
228        // The empty face is contained in every node; only adjacent when no third node exists.
229        return non_excluded_nodes > 2;
230    }
231
232    let mut base_row = face_rows[0];
233    let mut base_len = membership.members_of_row(base_row).len();
234    for &row in face_rows.iter().skip(1) {
235        let len = membership.members_of_row(row).len();
236        if len < base_len {
237            base_row = row;
238            base_len = len;
239            if base_len <= 2 {
240                break;
241            }
242        }
243    }
244
245    for &candidate in membership.members_of_row(base_row) {
246        if candidate == node_a || candidate == node_b {
247            continue;
248        }
249        let mut ok = true;
250        for &row in face_rows {
251            if row == base_row {
252                continue;
253            }
254            if !membership.row_contains_node(row, candidate) {
255                ok = false;
256                break;
257            }
258        }
259        if ok {
260            return true;
261        }
262    }
263    false
264}
265
266/// Options for building an adjacency graph from incidence sets.
267#[derive(Clone, Copy, Debug, Default)]
268pub struct IncidenceAdjacencyOptions<'a> {
269    /// Nodes to exclude from adjacency computation (e.g. linearity generators).
270    ///
271    /// When `None`, all nodes are considered active.
272    pub excluded_nodes: Option<&'a [bool]>,
273
274    /// Active universe rows; rows with `false` are ignored.
275    ///
276    /// When `None`, all rows are active.
277    pub active_rows: Option<&'a [bool]>,
278
279    /// Candidate edges to test (pairs of node indices). When `None`, the builder enumerates
280    /// candidate edges using the sparse membership index.
281    pub candidate_edges: Option<&'a [(usize, usize)]>,
282
283    /// If `true`, skip degeneracy containment checks.
284    pub assume_nondegenerate: bool,
285}
286
287/// Options for building adjacency from pre-filtered per-node incidence rows.
288#[derive(Clone, Copy, Debug, Default)]
289pub struct RowsByNodeAdjacencyOptions<'a> {
290    /// Nodes to exclude from adjacency computation (e.g. linearity generators).
291    ///
292    /// When `None`, all nodes are considered active.
293    pub excluded_nodes: Option<&'a [bool]>,
294
295    /// Candidate edges to test (pairs of node indices). When `None`, the builder enumerates
296    /// candidate edges using the sparse membership index.
297    pub candidate_edges: Option<&'a [(usize, usize)]>,
298
299    /// If `true`, skip degeneracy containment checks.
300    pub assume_nondegenerate: bool,
301}
302
303pub fn input_adjacency_from_incidence_set_family(
304    incidence: &SetFamily,
305    redundant_nodes: &RowSet,
306    dominant_nodes: &RowSet,
307) -> SetFamily {
308    let family_size = incidence.family_size();
309    if redundant_nodes.len() != family_size {
310        panic!(
311            "redundant_nodes length mismatch (redundant_nodes={} family_size={})",
312            redundant_nodes.len(),
313            family_size
314        );
315    }
316    if dominant_nodes.len() != family_size {
317        panic!(
318            "dominant_nodes length mismatch (dominant_nodes={} family_size={})",
319            dominant_nodes.len(),
320            family_size
321        );
322    }
323
324    let row_capacity = incidence.set_capacity();
325    let mut rows_by_node: Vec<Vec<usize>> = Vec::with_capacity(family_size);
326
327    for idx in 0..family_size {
328        if redundant_nodes.contains(idx) || dominant_nodes.contains(idx) {
329            rows_by_node.push(Vec::new());
330            continue;
331        }
332        let set = incidence
333            .set(idx)
334            .unwrap_or_else(|| panic!("incidence SetFamily missing set for index {idx}"));
335        rows_by_node.push(set.iter().map(|id| id.as_index()).collect());
336    }
337
338    input_adjacency_from_rows_by_node_with::<SetFamilyBuilder>(
339        &rows_by_node,
340        row_capacity,
341        redundant_nodes,
342        dominant_nodes,
343    )
344}
345
346pub fn input_adjacency_from_rows_by_node_with<S: AdjacencyBuilder>(
347    rows_by_node: &[impl AsRef<[usize]>],
348    row_capacity: usize,
349    redundant_nodes: &RowSet,
350    dominant_nodes: &RowSet,
351) -> S::Output {
352    let family_size = rows_by_node.len();
353    if family_size == 0 {
354        return S::new(0).finish();
355    }
356    if redundant_nodes.len() != family_size {
357        panic!(
358            "redundant_nodes length mismatch (redundant_nodes={} family_size={})",
359            redundant_nodes.len(),
360            family_size
361        );
362    }
363    if dominant_nodes.len() != family_size {
364        panic!(
365            "dominant_nodes length mismatch (dominant_nodes={} family_size={})",
366            dominant_nodes.len(),
367            family_size
368        );
369    }
370
371    let mut excluded_nodes = vec![false; family_size];
372
373    let mut has_dominant = false;
374    let mut normal_first = usize::MAX;
375    let mut normal_second = usize::MAX;
376    let mut normal_count = 0usize;
377
378    for (idx, excluded_slot) in excluded_nodes.iter_mut().enumerate() {
379        let is_dominant = dominant_nodes.contains(idx);
380        has_dominant |= is_dominant;
381        let excluded = redundant_nodes.contains(idx) || is_dominant;
382        *excluded_slot = excluded;
383        if !excluded {
384            normal_count += 1;
385            if normal_count == 1 {
386                normal_first = idx;
387            } else if normal_count == 2 {
388                normal_second = idx;
389            }
390        }
391    }
392
393    let mut sink = S::new(family_size);
394    adjacency_from_rows_by_node_into(
395        &mut sink,
396        rows_by_node,
397        row_capacity,
398        3,
399        RowsByNodeAdjacencyOptions {
400            excluded_nodes: Some(&excluded_nodes),
401            candidate_edges: None,
402            assume_nondegenerate: false,
403        },
404    );
405
406    if normal_count == 2 {
407        sink.add_undirected_edge(normal_first, normal_second);
408    }
409
410    if has_dominant {
411        let mut nonredundant: Vec<usize> = Vec::new();
412        for idx in 0..family_size {
413            if !redundant_nodes.contains(idx) {
414                nonredundant.push(idx);
415            }
416        }
417
418        for dom in dominant_nodes.iter().map(|id| id.as_index()) {
419            if redundant_nodes.contains(dom) {
420                continue;
421            }
422            for &other in &nonredundant {
423                if other == dom {
424                    continue;
425                }
426                sink.add_undirected_edge(dom, other);
427            }
428        }
429    }
430
431    sink.finish()
432}
433
434/// Build an undirected adjacency graph from a family of incidence sets.
435///
436/// The criterion matches the "ridge adjacency" / "dual graph" logic:
437/// - Let `face = S[i] ∩ S[j]` (restricted to `active_rows`).
438/// - Require `|face| >= adj_dim - 2`.
439/// - In degenerate mode, reject `(i,j)` if there exists any third node `k` whose set contains `face`.
440///
441/// `adj_dim` should be the dimension of the homogenized cone / system (typically `d+1` for polytopes).
442///
443/// # Panics
444///
445/// Panics if dimension mismatches occur in `excluded_nodes`, `active_rows`, or row indices.
446pub fn adjacency_from_incidence(
447    sets: &[Vec<usize>],
448    universe_size: usize,
449    adj_dim: usize,
450    options: IncidenceAdjacencyOptions<'_>,
451) -> AdjacencyList {
452    adjacency_from_incidence_with::<AdjacencyListBuilder>(sets, universe_size, adj_dim, options)
453}
454
455/// Build adjacency from incidence sets, writing into an arbitrary sink.
456///
457/// # Panics
458///
459/// Panics if dimension mismatches occur in `excluded_nodes`, `active_rows`, or row indices.
460pub fn adjacency_from_incidence_with<S: AdjacencyBuilder>(
461    sets: &[Vec<usize>],
462    universe_size: usize,
463    adj_dim: usize,
464    options: IncidenceAdjacencyOptions<'_>,
465) -> S::Output {
466    let family_size = sets.len();
467    if family_size == 0 {
468        return S::new(0).finish();
469    }
470
471    let excluded = options.excluded_nodes;
472    if let Some(mask) = excluded {
473        assert!(
474            mask.len() == family_size,
475            "excluded_nodes length mismatch (excluded_nodes={} family_size={})",
476            mask.len(),
477            family_size
478        );
479    }
480    let is_excluded = |idx: usize| excluded.is_some_and(|m| m[idx]);
481
482    let active = options.active_rows;
483    if let Some(mask) = active {
484        assert!(
485            mask.len() == universe_size,
486            "active_rows length mismatch (active_rows={} universe_size={})",
487            mask.len(),
488            universe_size
489        );
490    }
491
492    let mut rows_by_node: Vec<Vec<usize>> = Vec::with_capacity(family_size);
493    for (idx, rows) in sets.iter().enumerate() {
494        if is_excluded(idx) {
495            rows_by_node.push(Vec::new());
496            continue;
497        }
498
499        let mut filtered: Vec<usize> = Vec::new();
500        if let Some(active) = active {
501            for &row in rows {
502                debug_assert!(
503                    row < universe_size,
504                    "row index out of range (node={idx} row={row} universe_size={universe_size})"
505                );
506                if active[row] {
507                    filtered.push(row);
508                }
509            }
510        } else {
511            for &row in rows {
512                debug_assert!(
513                    row < universe_size,
514                    "row index out of range (node={idx} row={row} universe_size={universe_size})"
515                );
516                filtered.push(row);
517            }
518        }
519
520        debug_assert!(
521            filtered.windows(2).all(|w| w[0] < w[1]),
522            "adjacency_from_incidence expects per-node sets to be sorted + deduped"
523        );
524        rows_by_node.push(filtered);
525    }
526
527    adjacency_from_rows_by_node_with::<S>(
528        &rows_by_node,
529        universe_size,
530        adj_dim,
531        RowsByNodeAdjacencyOptions {
532            excluded_nodes: excluded,
533            candidate_edges: options.candidate_edges,
534            assume_nondegenerate: options.assume_nondegenerate,
535        },
536    )
537}
538
539/// Build adjacency from pre-filtered per-node incidence rows.
540///
541/// `rows_by_node[i]` must be sorted + deduped, and contain only row indices `< row_capacity`.
542///
543/// # Panics
544///
545/// Panics if `excluded_nodes` length doesn't match family size.
546pub fn adjacency_from_rows_by_node_with<S: AdjacencyBuilder>(
547    rows_by_node: &[impl AsRef<[usize]>],
548    row_capacity: usize,
549    adj_dim: usize,
550    options: RowsByNodeAdjacencyOptions<'_>,
551) -> S::Output {
552    let family_size = rows_by_node.len();
553    if family_size == 0 {
554        return S::new(0).finish();
555    }
556
557    let mut sink = S::new(family_size);
558    adjacency_from_rows_by_node_into(&mut sink, rows_by_node, row_capacity, adj_dim, options);
559    sink.finish()
560}
561
562fn adjacency_from_rows_by_node_into<S, Rows>(
563    sink: &mut S,
564    rows_by_node: &[Rows],
565    row_capacity: usize,
566    adj_dim: usize,
567    options: RowsByNodeAdjacencyOptions<'_>,
568) where
569    S: AdjacencyBuilder,
570    Rows: AsRef<[usize]>,
571{
572    let family_size = rows_by_node.len();
573    if family_size == 0 {
574        return;
575    }
576
577    let excluded = options.excluded_nodes;
578    if let Some(mask) = excluded {
579        assert!(
580            mask.len() == family_size,
581            "excluded_nodes length mismatch (excluded_nodes={} family_size={})",
582            mask.len(),
583            family_size
584        );
585    }
586    let is_excluded = |idx: usize| excluded.is_some_and(|m| m[idx]);
587
588    let required = adj_dim.saturating_sub(2);
589    let non_excluded = (0..family_size).filter(|&i| !is_excluded(i)).count();
590    if non_excluded < 2 {
591        return;
592    }
593
594    // Edge case: when `required == 0` (adj_dim <= 2), overlap-based candidate generation can't see
595    // empty intersections. Handle directly.
596    if options.candidate_edges.is_none() && required == 0 {
597        let non_excluded_indices: Vec<usize> =
598            (0..family_size).filter(|&i| !is_excluded(i)).collect();
599        if options.assume_nondegenerate || non_excluded_indices.len() == 2 {
600            for (pos, &i) in non_excluded_indices.iter().enumerate() {
601                for &j in non_excluded_indices.iter().skip(pos + 1) {
602                    sink.add_undirected_edge(i, j);
603                }
604            }
605        }
606        return;
607    }
608
609    // If nodes are simplicial (`|rows| == required + 1`), adjacency reduces to hashing their
610    // (required)-subfaces ("ridges"). This avoids the overlap sweep whose cost can blow up with
611    // high row-membership (e.g. vertex degrees in facet adjacency).
612    //
613    // In mixed cases, we still use ridge hashing to add all simplicial↔simplicial edges and then
614    // run the generic test only for non-simplicial nodes.
615    let mut simplicial_facet_dimension: Option<usize> = None;
616    if options.candidate_edges.is_none() && required >= 1 {
617        let facet_dimension = required + 1;
618        let simplicial_count = simplicial_ridge_hash_into_sink(
619            rows_by_node,
620            facet_dimension,
621            excluded,
622            options.assume_nondegenerate,
623            sink,
624        );
625        if simplicial_count == non_excluded {
626            return;
627        }
628        simplicial_facet_dimension = Some(facet_dimension);
629    }
630
631    let membership = SparseRowMembershipIndex::new(rows_by_node, row_capacity);
632    let mut face_rows: Vec<usize> = Vec::new();
633
634    if let Some(edges) = options.candidate_edges {
635        for &(i, j) in edges {
636            if i == j || i >= family_size || j >= family_size {
637                continue;
638            }
639            if is_excluded(i) || is_excluded(j) {
640                continue;
641            }
642            intersect_sorted_rows(
643                rows_by_node[i].as_ref(),
644                rows_by_node[j].as_ref(),
645                &mut face_rows,
646            );
647            if face_rows.len() < required {
648                continue;
649            }
650            if !options.assume_nondegenerate
651                && has_third_node_containing_face(&membership, &face_rows, i, j, non_excluded)
652            {
653                continue;
654            }
655            sink.add_undirected_edge(i, j);
656        }
657        return;
658    }
659
660    let mut counts: Vec<u32> = vec![0; family_size];
661    let mut touched: Vec<usize> = Vec::new();
662
663    for i in 0..family_size {
664        if is_excluded(i) {
665            continue;
666        }
667        if let Some(d) = simplicial_facet_dimension {
668            if rows_by_node[i].as_ref().len() == d {
669                continue;
670            }
671        }
672
673        touched.clear();
674        for &row in rows_by_node[i].as_ref() {
675            debug_assert!(row < row_capacity, "row index out of range in rows_by_node");
676            for &j in membership.members_of_row(row) {
677                let c = counts[j];
678                if c == 0 {
679                    touched.push(j);
680                }
681                counts[j] = c + 1;
682            }
683        }
684
685        for &j in &touched {
686            if is_excluded(j) {
687                continue;
688            }
689            if let Some(d) = simplicial_facet_dimension {
690                if rows_by_node[j].as_ref().len() != d && j <= i {
691                    continue;
692                }
693            } else if j <= i {
694                continue;
695            }
696            if (counts[j] as usize) < required {
697                continue;
698            }
699            if !options.assume_nondegenerate {
700                intersect_sorted_rows(
701                    rows_by_node[i].as_ref(),
702                    rows_by_node[j].as_ref(),
703                    &mut face_rows,
704                );
705                if face_rows.len() < required {
706                    continue;
707                }
708                if has_third_node_containing_face(&membership, &face_rows, i, j, non_excluded) {
709                    continue;
710                }
711            }
712            sink.add_undirected_edge(i, j);
713        }
714
715        for &j in &touched {
716            counts[j] = 0;
717        }
718    }
719}
720
721fn simplicial_ridge_hash_into_sink<S: AdjacencyBuilder>(
722    facets: &[impl AsRef<[usize]>],
723    facet_dimension: usize,
724    excluded_nodes: Option<&[bool]>,
725    assume_nondegenerate: bool,
726    sink: &mut S,
727) -> usize {
728    if facet_dimension < 2 {
729        return 0;
730    }
731
732    let is_excluded = |idx: usize| excluded_nodes.is_some_and(|m| m[idx]);
733
734    let mut ridge_to_facets: AHashMap<Key, SmallVec<[usize; 2]>> = AHashMap::new();
735    let mut simplicial_count: usize = 0;
736    let ridge_len = facet_dimension - 1;
737    for (facet_idx, facet) in facets.iter().enumerate() {
738        if is_excluded(facet_idx) {
739            continue;
740        }
741        let facet = facet.as_ref();
742        if facet.len() != facet_dimension {
743            continue;
744        }
745        simplicial_count += 1;
746        for drop_pos in 0..facet_dimension {
747            let mut ridge = Key::with_capacity(ridge_len);
748            ridge.extend(facet[..drop_pos].iter().copied());
749            ridge.extend(facet[(drop_pos + 1)..].iter().copied());
750            ridge_to_facets.entry(ridge).or_default().push(facet_idx);
751        }
752    }
753
754    if assume_nondegenerate {
755        for incident in ridge_to_facets.values() {
756            for (pos, &a) in incident.iter().enumerate() {
757                for &b in incident.iter().skip(pos + 1) {
758                    if a != b {
759                        sink.add_undirected_edge(a, b);
760                    }
761                }
762            }
763        }
764    } else {
765        for incident in ridge_to_facets.values() {
766            if let [a, b] = incident.as_slice() {
767                if a != b {
768                    sink.add_undirected_edge(*a, *b);
769                }
770            }
771        }
772    }
773
774    simplicial_count
775}
776
777#[cfg(test)]
778mod tests {
779    use super::{IncidenceAdjacencyOptions, adjacency_from_incidence};
780
781    #[test]
782    fn square_facets_form_cycle() {
783        // Unit square facets each contain two vertices (0..4).
784        // Facet 0: {0,1}, facet 1: {1,3}, facet 2: {2,3}, facet 3: {0,2}
785        let facets = vec![vec![0, 1], vec![1, 3], vec![2, 3], vec![0, 2]];
786        let g = adjacency_from_incidence(&facets, 4, 3, IncidenceAdjacencyOptions::default());
787        let degrees: Vec<usize> = g.adjacency_lists().iter().map(|n| n.len()).collect();
788        assert_eq!(degrees, vec![2, 2, 2, 2]);
789    }
790}