1use core::panic;
5use std::collections::{HashMap, HashSet};
6use std::fmt::Display;
7use itertools::Itertools;
8
9use super::{Node, Path, Slot};
10
11#[cfg(not(feature = "big-link"))]
12pub type Edge = u8;
13#[cfg(not(feature = "big-link"))]
14pub type StateRepr = u64;
15
16#[cfg(feature = "big-link")]
18pub type Edge = u16;
19#[cfg(feature = "big-link")]
20pub type StateRepr = u128;
21
22pub type State = yui_core::bitseq::BitSeq<StateRepr>;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Link {
26 nodes: Vec<Node>,
27 loops: Vec<Edge>,
28 base_pt: Option<Edge>,
29}
30
31impl Link {
32 pub const MAX_CROSSING: usize = State::MAX_LEN;
35
36 pub fn new(
37 nodes: impl IntoIterator<Item = Node>,
38 loops: impl IntoIterator<Item = Edge>,
39 ) -> Self {
40 let nodes = nodes.into_iter().collect_vec();
41 let loops = loops.into_iter().collect_vec();
42
43 assert!(
44 nodes.len() <= Self::MAX_CROSSING,
45 "too many crossings: {} > MAX_CROSSING = {} (enable the `big-link` feature for up to 128)",
46 nodes.len(), Self::MAX_CROSSING
47 );
48
49 let edge_counts = nodes.iter().flat_map(|x| x.edges()).cloned().counts();
50 let bad = edge_counts.iter()
51 .filter(|&(_, &c)| c != 2)
52 .map(|(&e, &c)| (e, c))
53 .sorted()
54 .collect_vec();
55 assert!(bad.is_empty(), "each edge must appear exactly twice; (edge, count) = {bad:?}");
56
57 let node_edges: HashSet<Edge> = edge_counts.into_keys().collect();
58 let mut loop_set: HashSet<Edge> = HashSet::new();
59 for &e in &loops {
60 assert!(!node_edges.contains(&e), "loop edge {e} is already used in a node");
61 assert!(loop_set.insert(e), "duplicate loop edge: {e}");
62 }
63
64 let base_pt = node_edges.iter().chain(loops.iter()).copied().min();
66
67 let l = Self { nodes, loops, base_pt };
68 l.verify_ori();
69 l
70 }
71
72 pub fn from_nodes(nodes: impl IntoIterator<Item = Node>) -> Self {
73 Self::new(nodes, [])
74 }
75
76 pub fn with_base_pt(mut self, e: Edge) -> Self {
77 let exists = self.nodes.iter().any(|x| x.edges().contains(&e))
78 || self.loops.contains(&e);
79 assert!(exists, "base_pt {e} is not an edge of this link");
80 self.base_pt = Some(e);
81 self
82 }
83
84 pub fn base_pt(&self) -> Option<Edge> {
85 self.base_pt
86 }
87
88 pub fn empty() -> Link {
89 Self::new([], [])
90 }
91
92 pub fn is_empty(&self) -> bool {
93 self.nodes.is_empty() && self.loops.is_empty()
94 }
95
96 pub fn unknot() -> Link {
97 Self::unlink(1)
98 }
99
100 pub fn unlink(n: usize) -> Link {
101 Self::new([], (1..=n).map(|e| e as Edge))
102 }
103
104 pub fn is_knot(&self) -> bool {
105 self.n_comps() == 1
106 }
107
108 pub fn is_oriented(&self) -> bool {
109 self.nodes().all(|n| n.is_oriented())
110 }
111
112 pub(crate) fn normalize_ori(&mut self) {
114 if !self.is_oriented() {
115 self.nodes.iter_mut().for_each(|n|
116 n.set_incoming(None)
117 );
118 }
119 }
120
121 pub fn verify_ori(&self) {
123 let n_ori = self.nodes.iter().filter(|x| x.is_oriented()).count();
124 if n_ori == 0 {
125 return;
126 }
127 assert_eq!(n_ori, self.n_nodes(), "some nodes are oriented and some are not");
128
129 self.nodes.iter().flat_map(|x| {
130 let (p, q) = x.incoming().unwrap();
131 Slot::ALL.map(move |s| (x.edge(s), s == p || s == q))
132 }).into_group_map().into_iter().for_each(|(e, ins)|
133 assert!(
134 matches!(ins[..], [a, b] if a != b),
135 "edge {e} does not run from an outgoing slot to an incoming one"
136 )
137 );
138 }
139
140 pub fn writhe(&self) -> i32 {
141 let (p, n) = self.n_signed_crossings();
142 (p as i32) - (n as i32)
143 }
144
145 pub fn n_nodes(&self) -> usize {
146 self.nodes.len()
147 }
148
149 pub fn nodes(&self) -> impl Iterator<Item = &Node> {
150 self.nodes.iter()
151 }
152
153 pub fn node(&self, i: usize) -> &Node {
154 &self.nodes[i]
155 }
156
157 pub(crate) fn node_mut(&mut self, i: usize) -> &mut Node {
158 &mut self.nodes[i]
159 }
160
161 pub fn crossings(&self) -> impl Iterator<Item = &Node> {
162 self.nodes.iter().filter(|x| x.is_crossing())
163 }
164
165 pub fn n_crossings(&self) -> usize {
166 self.nodes.iter()
167 .filter(|x| x.is_crossing())
168 .count()
169 }
170
171 pub fn n_signed_crossings(&self) -> (usize, usize) {
172 let mut pos = 0;
173 let mut neg = 0;
174 for n in self.nodes.iter() {
175 if n.is_pos() { pos += 1 }
176 else if n.is_neg() { neg += 1}
177 }
178 (pos, neg)
179 }
180
181 pub fn loops(&self) -> &[Edge] {
182 &self.loops
183 }
184
185 pub fn n_loops(&self) -> usize {
186 self.loops.len()
187 }
188
189 pub fn n_edges(&self) -> usize {
190 self.nodes.len() * 2 + self.loops.len()
191 }
192
193 pub fn edges(&self) -> Vec<Edge> {
194 let mut edges: Vec<Edge> = self.nodes.iter()
195 .flat_map(|x| x.edges().iter().copied())
196 .chain(self.loops.iter().copied())
197 .collect();
198 edges.sort();
199 edges.dedup();
200 edges
201 }
202
203 pub fn n_comps(&self) -> usize {
204 let mut count = 0;
205 self.traverse_comps(|c, _, _|
206 if count <= c { count = c + 1 }
207 );
208 count + self.loops.len()
209 }
210
211 pub fn comps(&self) -> Vec<Path> {
212 let mut comps = vec![];
213
214 self.traverse_comps(|c, i, s| {
215 if c == comps.len() {
216 comps.push(vec![]);
217 }
218 comps[c].push(self.node(i).edge(s));
219 });
220
221 let mut result: Vec<Path> = comps.into_iter().map(Path::circ).collect();
222 for &e in &self.loops {
223 result.push(Path::circ(vec![e]));
224 }
225 result
226 }
227
228 pub fn traverse_comps<F>(&self, mut f: F) where
229 F: FnMut(usize, usize, Slot) {
230 let mut c = 0; let mut remain: HashSet<Edge> = self.nodes.iter().flat_map(|x| x.edges().iter().copied()).collect();
232
233 while !remain.is_empty() {
234 let e0 = remain.iter().min().cloned().unwrap();
236
237 let (i0, j0) = if self.is_oriented() {
239 self.edge_ends(e0, true).1
240 } else {
241 self.find_port(|i, s|
242 self.node(i).edge(s) == e0
243 ).unwrap()
244 };
245
246 self.traverse_from((i0, j0), |i, s| {
247 remain.remove(&self.node(i).edge(s));
248 f(c, i, s);
249 });
250
251 c += 1;
253 }
254 }
255
256 pub fn traverse_from<F>(&self, start: (usize, Slot), mut f: F) where
257 F: FnMut(usize, Slot)
258 {
259 let (mut i, mut j) = start;
260
261 f(i, j); loop {
264 let c = self.node(i);
265 let k = c.paired_slot(j);
266 let next = self.traverse_outer(i, k);
267
268 if next == start {
269 break
270 }
271
272 (i, j) = next;
273
274 f(i, j)
275 }
276 }
277
278 fn traverse_outer(&self, n_index: usize, slot: Slot) -> (usize, Slot) {
279 let e = self.nodes[n_index].edge(slot);
280 self.nodes.iter().enumerate().flat_map(|(i, _)|
281 Slot::ALL.map(move |s| (i, s))
282 ).find(|&(i, s)|
283 self.nodes[i].edge(s) == e && (i, s) != (n_index, slot)
284 ).expect("Broken data")
285 }
286
287 pub(crate) fn reorient<F>(&mut self, is_incoming: F) -> bool
293 where F: Fn(usize, Slot) -> bool {
294 let mut incoming: Vec<Vec<Slot>> = vec![vec![]; self.n_nodes()];
295 let mut remain: HashSet<Edge> = self.nodes.iter().flat_map(|x| x.edges().iter().copied()).collect();
296 let mut undetermined = false;
297
298 while !remain.is_empty() {
299 let Some(start) = self.find_port(|i, s|
302 remain.contains(&self.node(i).edge(s)) && is_incoming(i, s)
303 ) else {
304 undetermined = true;
305 break;
306 };
307
308 self.traverse_from(start, |i, s| {
309 remain.remove(&self.node(i).edge(s));
310 let out = self.node(i).paired_slot(s);
311 assert!(
312 is_incoming(i, s) || !is_incoming(i, out),
313 "inconsistent orientation: the strand through node {i} exits at slot {out}, which is claimed incoming"
314 );
315 incoming[i].push(s);
316 });
317 }
318
319 let oris = Iterator::zip(self.nodes.iter(), incoming.iter()).map(|(n, slots)|
322 match slots[..] {
323 [p, q] => Node::orientable(n.node_type(), p, q).then_some((p, q)),
324 _ => None,
325 }
326 ).collect_vec();
327 let coherent = !undetermined && oris.iter().all(Option::is_some);
328
329 self.nodes.iter_mut().zip(oris).for_each(|(n, o)|
330 n.set_incoming(if coherent { o } else { None })
331 );
332
333 coherent
334 }
335
336 pub fn unoriented(&self) -> Self {
337 if !self.is_oriented() {
338 return self.clone();
339 }
340 let mut l = self.clone();
341 l.nodes.iter_mut().for_each(|n|
342 n.set_incoming(None)
343 );
344 l
345 }
346
347 pub fn reindexed(&self, start_edge: Edge, base: Edge) -> Link {
351 assert!(self.is_oriented(), "reindexed needs an orientation to traverse in");
352
353 let mut map: HashMap<Edge, Edge> = HashMap::new();
354 let mut next = base;
355
356 let mut port = if self.loops.contains(&start_edge) {
359 map.insert(start_edge, next);
360 next += 1;
361 None
362 } else {
363 Some(self.edge_ends(start_edge, true).1)
364 };
365
366 while let Some(p) = port.or_else(||
369 self.nodes.iter()
370 .flat_map(|x| x.edges().iter().copied())
371 .filter(|e| !map.contains_key(e))
372 .min()
373 .map(|e| self.edge_ends(e, true).1)
374 ) {
375 self.traverse_from(p, |i, s| {
376 map.entry(self.node(i).edge(s)).or_insert_with(|| {
377 let id = next;
378 next += 1;
379 id
380 });
381 });
382 port = None;
383 }
384
385 let loops = self.loops.iter().map(|&e|
386 *map.entry(e).or_insert_with(|| {
387 let id = next;
388 next += 1;
389 id
390 })
391 ).collect_vec();
392
393 let nodes = self.nodes.iter().map(|x|
394 x.convert_edges(|e| map[&e])
395 );
396 Link::new(nodes, loops).with_base_pt(base)
397 }
398
399 pub fn reindexed_canon(&self) -> Link {
402 let pd = self.edges().into_iter()
403 .map(|e| self.reindexed(e, 1).pd_code())
404 .min()
405 .expect("a knot has at least one edge");
406 Self::from_pd_code(pd)
407 }
408
409 pub(crate) fn edge_ends(&self, e: Edge, directed: bool) -> ((usize, Slot), (usize, Slot)) {
413 assert!(!directed || self.is_oriented(), "directed edge_ends requires an oriented link");
414
415 let (x, y) = self.nodes().enumerate().flat_map(|(i, n)|
416 Slot::ALL.into_iter().filter(move |&s| n.edge(s) == e).map(move |s| (i, s))
417 ).collect_tuple().unwrap_or_else(||
418 panic!("edge {e} must appear exactly twice")
419 );
420 if !directed {
421 return (x, y);
422 }
423
424 let is_in = |(i, s): (usize, Slot)| {
425 let (p, q) = self.node(i).incoming().expect("directed edge_ends requires an oriented link");
426 s == p || s == q
427 };
428 debug_assert!(is_in(x) != is_in(y), "edge {e} must have one head and one tail");
429 if is_in(x) { (y, x) } else { (x, y) }
430 }
431
432 fn find_port(&self, f: impl Fn(usize, Slot) -> bool) -> Option<(usize, Slot)> {
433 (0..self.n_nodes()).flat_map(|i|
434 Slot::ALL.map(move |s| (i, s))
435 ).find(|&(i, s)| f(i, s))
436 }
437}
438
439impl Display for Link {
440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 write!(f, "L[{}]", self.nodes.iter().map(|x| x.to_string()).join(", "))
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use yui_core::bitseq::Bit;
448
449 use super::*;
450
451 #[test]
452 #[should_panic(expected = "(edge, count) = [(4, 1), (5, 3)]")]
453 fn new_names_the_miscounted_edges() {
454 let _ = Link::from_pd_code([[1, 5, 2, 4], [3, 1, 5, 6], [5, 3, 6, 2]]);
456 }
457
458 #[test]
459 #[should_panic(expected = "does not run from an outgoing slot")]
460 fn new_rejects_disagreeing_orientation() {
461 use crate::NodeType::XL;
462 let a = Node::new(XL, Some((Slot::SW, Slot::SE)), [1, 2, 3, 4]);
464 let b = Node::new(XL, Some((Slot::NE, Slot::NW)), [3, 4, 1, 2]);
465 let _ = Link::from_nodes([a, b]);
466 }
467
468 #[test]
469 #[should_panic(expected = "some nodes are oriented and some are not")]
470 fn new_rejects_partial_orientation() {
471 use crate::NodeType::XL;
472 let a = Node::new(XL, Some((Slot::SW, Slot::SE)), [1, 2, 3, 4]);
473 let b = Node::new(XL, None, [3, 4, 1, 2]);
474 let _ = Link::from_nodes([a, b]);
475 }
476
477 #[test]
478 fn link_init() {
479 let l = Link::from_nodes(vec![]);
480 assert_eq!(l.nodes.len(), 0);
481 }
482
483 #[test]
484 fn link_is_empty() {
485 let l = Link::empty();
486 assert!(l.is_empty());
487
488 let l = Link::test_data("unknot_l_twist");
489 assert!(!l.is_empty());
490 }
491
492 #[test]
493 fn link_crossing_num() {
494 let l = Link::empty();
495 assert_eq!(l.n_crossings(), 0);
496
497 let l = Link::test_data("unknot_l_twist");
498 assert_eq!(l.n_crossings(), 1);
499
500 let l = Link::test_data("3_1");
501 assert_eq!(l.n_crossings(), 3);
502 }
503
504 #[test]
505 fn link_next() {
506 let l = Link::test_data("unknot_l_twist");
507
508 let s = |i: usize| Slot::from(i);
509 assert_eq!(l.traverse_outer(0, s(0)), (0, s(1)));
510 assert_eq!(l.traverse_outer(0, s(1)), (0, s(0)));
511 assert_eq!(l.traverse_outer(0, s(2)), (0, s(3)));
512 assert_eq!(l.traverse_outer(0, s(3)), (0, s(2)));
513 }
514
515 #[test]
516 fn link_traverse() {
517 let traverse = |l: &Link, start: (usize, Slot)| {
518 let mut queue = vec![];
519 l.traverse_from(start, |i, s| queue.push((i, s.index())));
520 queue
521 };
522
523 let l = Link::test_data("unknot_l_twist");
524 let path = traverse(&l, (0, Slot::SW));
525
526 assert_eq!(path, [(0, 0), (0, 3)]); }
528
529 #[test]
530 fn link_crossing_signs() {
531 let l = Link::test_data("unknot_l_twist");
532 assert_eq!(l.n_signed_crossings(), (1, 0));
533
534 let l = Link::test_data("unknot_r_twist");
535 assert_eq!(l.n_signed_crossings(), (0, 1));
536
537 let l = Link::test_data("unknot_l_twist").resolve_at(0, Bit::Bit0);
538 assert_eq!(l.n_signed_crossings(), (0, 0));
539 }
540
541 #[test]
542 fn link_writhe() {
543 let l = Link::test_data("unknot_l_twist");
544 assert_eq!(l.writhe(), 1);
545
546 let l = Link::test_data("unknot_r_twist");
547 assert_eq!(l.writhe(), -1);
548
549 let l = Link::test_data("unknot_l_twist").resolve_at(0, Bit::Bit0);
550 assert_eq!(l.writhe(), 0);
551 }
552
553 #[test]
554 fn link_components() {
555 let l = Link::test_data("unknot_l_twist");
556 let comps = l.comps();
557 assert_eq!(comps, vec![ Path::circ(vec![1, 2])]);
558 }
559
560
561
562 #[test]
563 fn empty_link() {
564 let l = Link::empty();
565 assert_eq!(l.n_crossings(), 0);
566 assert_eq!(l.writhe(), 0);
567 assert_eq!(l.n_comps(), 0);
568 }
569
570 #[test]
571 fn unknot() {
572 let l = Link::unknot();
573
574 assert!(!l.is_empty());
575 assert!(l.is_oriented());
576 assert!(l.is_knot());
577
578 assert_eq!(l.n_crossings(), 0);
579 assert_eq!(l.writhe(), 0);
580 assert_eq!(l.n_edges(), 1);
581 assert_eq!(l.n_comps(), 1);
582 assert_eq!(l.n_loops(), 1);
583
584 assert_eq!(l.loops(), &[1]);
585 assert_eq!(l.comps(), vec![Path::circ(vec![1])]);
586 }
587
588 #[test]
589 fn unlink_zero() {
590 let l = Link::unlink(0);
591
592 assert!(l.is_empty());
593 assert!(l.is_oriented());
594 assert!(!l.is_knot());
595
596 assert_eq!(l.n_crossings(), 0);
597 assert_eq!(l.writhe(), 0);
598 assert_eq!(l.n_edges(), 0);
599 assert_eq!(l.n_comps(), 0);
600 assert_eq!(l.n_loops(), 0);
601
602 assert_eq!(l.loops(), &[] as &[Edge]);
603 assert_eq!(l.comps(), vec![] as Vec<Path>);
604 }
605
606 #[test]
607 fn unlink_n() {
608 let l = Link::unlink(3);
609
610 assert!(!l.is_empty());
611 assert!(l.is_oriented());
612 assert!(!l.is_knot());
613
614 assert_eq!(l.n_crossings(), 0);
615 assert_eq!(l.writhe(), 0);
616 assert_eq!(l.n_edges(), 3);
617 assert_eq!(l.n_comps(), 3);
618 assert_eq!(l.n_loops(), 3);
619
620 assert_eq!(l.loops(), &[1, 2, 3]);
621 assert_eq!(
622 l.comps(),
623 vec![Path::circ(vec![1]), Path::circ(vec![2]), Path::circ(vec![3])]
624 );
625 }
626
627
628 #[test]
629 fn trefoil() {
630 let l = Link::test_data("3_1");
631 assert_eq!(l.n_crossings(), 3);
632 assert_eq!(l.writhe(), 3);
633 assert_eq!(l.n_comps(), 1);
634 }
635
636 #[test]
637 fn figure8() {
638 let l = Link::test_data("4_1");
639 assert_eq!(l.n_crossings(), 4);
640 assert_eq!(l.writhe(), 0);
641 assert_eq!(l.n_comps(), 1);
642 }
643
644 #[test]
645 fn hopf_link() {
646 let l = Link::test_data("L2a1");
647 assert_eq!(l.n_crossings(), 2);
648 assert_eq!(l.writhe(), -2);
649 assert_eq!(l.n_comps(), 2);
650 }
651
652 #[test]
653 fn unlink_2() {
654 let l = Link::test_data("unlink2");
656 assert_eq!(l.n_crossings(), 2);
657 assert_eq!(l.writhe(), 0);
658 assert_eq!(l.n_comps(), 2);
659 assert!(!l.is_oriented());
660 }
661
662 #[test]
663 fn unlink_2_r2() {
664 let l = Link::test_data("unlink2_r2");
666 assert_eq!(l.n_crossings(), 2);
667 assert_eq!(l.writhe(), 0);
668 assert_eq!(l.n_comps(), 2);
669 assert!(l.is_oriented());
670 }
671
672 #[test]
673 fn l2x4() {
674 let l = Link::test_data("L4a1");
675 assert_eq!(l.n_crossings(), 4);
676 assert_eq!(l.writhe(), -4);
677 assert_eq!(l.n_comps(), 2);
678 }
679
680
681 #[test]
682 fn base_pt_default_min_edge() {
683 let l = Link::test_data("3_1");
685 assert_eq!(l.base_pt(), Some(1));
686
687 assert_eq!(Link::empty().base_pt(), None);
689 }
690
691 #[test]
692 fn with_base_pt_sets_base_pt() {
693 let l = Link::test_data("3_1").with_base_pt(1);
694 assert_eq!(l.base_pt(), Some(1));
695 }
696
697 #[test]
698 fn with_base_pt_on_loop() {
699 let l = Link::unlink(3).with_base_pt(2);
700 assert_eq!(l.base_pt(), Some(2));
701 }
702
703
704 #[test]
705 #[should_panic]
706 fn with_base_pt_invalid_panics() {
707 let _ = Link::test_data("3_1").with_base_pt(99);
709 }
710
711 #[test]
712 fn unoriented_drops_every_incoming() {
713 let l = Link::test_data("3_1");
714 assert!(l.is_oriented());
715
716 let u = l.unoriented();
717 assert!(!u.is_oriented());
718 assert!(u.nodes().all(|x| x.incoming().is_none()));
719 u.verify_ori();
720
721 assert!(Iterator::zip(u.nodes(), l.nodes()).all(|(a, b)|
723 a.node_type() == b.node_type() && a.edges() == b.edges()
724 ));
725 assert_eq!(u.n_comps(), l.n_comps());
726 }
727
728 #[test]
729 fn unoriented_of_an_unoriented_diagram_is_itself() {
730 let l = Link::test_data("unlink2");
732 assert!(!l.is_oriented());
733 assert_eq!(l.unoriented(), l);
734
735 let u = Link::test_data("3_1").unoriented();
736 assert_eq!(u.unoriented(), u);
737 }
738
739 #[test]
740 fn n_edges_counts_the_edge_set() {
741 for l in [
744 Link::empty(),
745 Link::unknot(),
746 Link::unlink(3),
747 Link::test_data("3_1"),
748 Link::test_data("L4a1"),
749 Link::test_data("unknot_l_twist"),
750 Link::pretzel(1, 3, 5),
751 ] {
752 assert_eq!(l.n_edges(), l.edges().len(), "{l}");
753 }
754 }
755
756 #[test]
757 fn reindexed_numbers_along_the_orientation() {
758 let canon = |k: &Link| k.reindexed_canon();
760
761 for l in [Link::test_data("3_1"), Link::test_data("6_1"), Link::pretzel(1, 3, 5)] {
762 let c = canon(&l);
763 for e in l.edges() {
764 assert_eq!(canon(&l.reindexed(e, 1)), c, "relabelling from edge {e} changed the canonical form");
765 }
766 }
767 }
768
769 #[test]
770 fn reindexed_covers_links_and_loops() {
771 for name in ["L2a1", "L4a1"] {
773 let l = Link::test_data(name);
774 for e in l.edges() {
775 let r = l.reindexed(e, 1);
776 assert_eq!(r.edges(), (1..=l.n_edges() as Edge).collect::<Vec<_>>(), "{name} from edge {e}");
777 assert_eq!(r.n_comps(), l.n_comps(), "{name} from edge {e}");
778 }
779 }
780
781 let r = Link::unknot().reindexed(1, 5);
783 assert_eq!((r.edges(), r.n_comps(), r.base_pt()), (vec![5], 1, Some(5)));
784
785 let l = Link::unlink(3);
786 let r = l.reindexed(l.edges()[1], 1);
787 assert_eq!((r.edges(), r.n_comps(), r.base_pt()), (vec![1, 2, 3], 3, Some(1)));
788 }
789
790 #[test]
791 fn reorient_exits_by_the_node_pairing() {
792 use crate::NodeType;
793
794 let mut l = Link::test_data("3_1").resolve_at(0, Bit::Bit0);
798 assert_eq!(l.node(0).node_type(), NodeType::H);
799
800 let claims = [(0, Slot::NE), (1, Slot::NW)];
801 assert!(l.reorient(|i, s| claims.contains(&(i, s))));
802 assert!(l.is_oriented());
803 }
804}