Skip to main content

yui_link/link/
link.rs

1//! [`Link`]: a knot or link as a planar diagram — a `Vec<Node>` plus the free
2//! loops and an optional base point — with its accessors, components and traversal.
3
4use 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// `big-link`: diagrams with up to 128 crossings (256 edge labels).
17#[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    /// The maximum number of crossings a `Link` can carry, bounded by the `State` width
33    /// (64 by default, 128 under the `big-link` feature).
34    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        // Default base_pt to the minimal edge (if any).
65        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    // An orientation belongs to the whole diagram: once any node has lost it, drop it everywhere.
113    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    // Oriented throughout or not at all, every edge from an outgoing slot to an incoming one.
122    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; // component counter
231        let mut remain: HashSet<Edge> = self.nodes.iter().flat_map(|x| x.edges().iter().copied()).collect();
232
233        while !remain.is_empty() {
234            // Take minimal edge-id.
235            let e0 = remain.iter().min().cloned().unwrap();
236
237            // Find node & point having edge e0, entering at its head so the walk runs forward.
238            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            // Onto next component.
252            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); // call starting point
262
263        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    // Re-derive each crossing's orientation by traversing components. `is_incoming(i, j)` tells
288    // whether port j of node i is known to receive an incoming strand (PD codes: j == 0). The first
289    // claimed port met by a tentative traversal fixes the component's direction; a component claiming
290    // no port is undetermined (cf. `unlink2`) and the whole link is left unoriented. A fixed direction
291    // contradicting `is_incoming` (an odd PD code) panics. Returns whether the link is now oriented.
292    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            // start at a claimed port of an untraversed component, so the direction is correct
300            // from the outset. Components claiming no port are undetermined (cf. `unlink2`).
301            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        // a node is oriented by its two incoming slots, provided they lie on different strands;
320        // if any node fails that, or some component is undetermined, the whole link is unoriented.
321        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    // Renumber the edges base..base+n in the order they are met traversing from `start_edge`
348    // (a knot's one traversal covers every edge), keeping the diagram. base_pt becomes `base`
349    // (base = 1 gives the usual 1-based numbering of knot theory).
350    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        // `start_edge` takes `base`. A free loop carries no ports, so it is numbered outright;
357        // otherwise the walk starts there. Note `traverse_from` runs forward from an in-port.
358        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        // then the remaining components, each from its least unnumbered edge, so a link is
367        // renumbered deterministically.
368        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    // The canonical relabelling: the least `reindexed(e, 1)` over all start edges. Rebuilt from that
400    // PD code so the node order is canonical too, making `==` equality up to relabelling.
401    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    // The two (node, slot) ends of edge `e`. When `directed`, they are ordered as (tail, head)
410    // along the orientation — the strand exits at the tail and enters at the head (cf.
411    // `Node::incoming`); otherwise the order carries no meaning.
412    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        // the trefoil's symmetric PD with edge 4 mistyped as 5
455        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        // both nodes take edge 1 as incoming, so it would enter at both of its ends.
463        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)]); // loop
527    }
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        // the over-component has no under-anchor, so the PD code leaves the link unoriented.
655        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        // the R2 pair alternates over/under, so both components are under-anchored.
665        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        // Defaults to the minimal edge of the link.
684        let l = Link::test_data("3_1");
685        assert_eq!(l.base_pt(), Some(1));
686
687        // Empty link has no edge, so base_pt is None.
688        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        // Edge 99 is not in the trefoil's edge set.
708        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        // the diagram itself is untouched — only the orientation is gone.
722        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        // `unlink2` loads unoriented (its over-component has no under-anchor in the PD code).
731        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        // `n_edges` is O(1) off the "each node-edge appears exactly twice" invariant, so it must
742        // agree with the deduped edge list — including when free loops are present.
743        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        // Renumbering depends only on (diagram, start edge), so the least code is a canonical form.
759        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        // more than one component: the walk continues into the rest, so every edge is renumbered.
772        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        // free loops carry no ports, so they are numbered outright rather than traversed.
782        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        // An `H` node pairs NE<->NW, so a strand entering at NE exits at NW — not at the opposite
795        // corner SW, which is a *crossing*'s pairing. These two claims agree with the diagram's own
796        // orientation; reading the exit as the opposite corner made them look contradictory.
797        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}