Skip to main content

yui_link/braid/
braid.rs

1//! [`Braid`]: a strand count plus a word in the Artin generators. `closure()`
2//! turns it into a [`Link`](crate::Link); strands touched by no crossing become free loops.
3
4use std::ops::{MulAssign, Mul};
5use auto_impl_ops::auto_ops;
6use delegate::delegate;
7use derive_more::{Display, Debug};
8use itertools::Itertools;
9
10use crate::{Link, LinkBuilder, NodeType};
11
12use super::braid_gen::{BraidGen, from_raw};
13
14#[derive(Clone, PartialEq, Eq, Display, Debug)]
15#[display("{:?}", elements)]
16pub struct Braid {
17    strands: usize,
18    elements: Vec<BraidGen>
19}
20
21impl Braid {
22    pub fn new(strands: usize, elements: Vec<BraidGen>) -> Self {
23        if let Some(g) = elements.iter().find(|g| g.index() >= strands) {
24            panic!("σ{} needs {} strands, but the braid has {strands}", g.index(), g.index() + 1);
25        }
26        Self { strands, elements }
27    }
28
29    pub fn id(strands: usize) -> Self {
30        Self::new(
31            strands,
32            vec![]
33        )
34    }
35
36    pub fn generator(strands: usize, index: usize) -> Self {
37        let val = i8::try_from(index).expect("index must fit in i8");
38        Self::new(strands, vec![BraidGen::new(val)])
39    }
40
41    pub fn strands(&self) -> usize {
42        self.strands
43    }
44
45    pub fn elements(&self) -> &[BraidGen] {
46        &self.elements
47    }
48
49    delegate! {
50        to self.elements {
51            pub fn len(&self) -> usize;
52            #[call(is_empty)]
53            pub fn is_id(&self) -> bool;
54        }
55    }
56
57    pub fn inv(&self) -> Self {
58        Self::new(
59            self.strands,
60            self.elements.iter().rev().map(
61                |g| g.inv()
62            ).collect()
63        )
64    }
65
66    pub fn extend(&self, by: usize) -> Self {
67        Self::new(self.strands + by, self.elements.clone())
68    }
69
70    pub fn reduced(&self) -> Self {
71        let mut stack: Vec<BraidGen> = Vec::new();
72        for g in self.elements.iter().copied() {
73            match stack.last() {
74                Some(&top) if top.inv() == g => { stack.pop(); }
75                _ => stack.push(g),
76            }
77        }
78        Self::new(self.strands, stack)
79    }
80
81    // Plat-free braid closure: one crossing per generator; each strand position is closed by wiring
82    // its visits cyclically (the wrap-around edge is the closure arc, so a single visit wires to
83    // itself), and an unvisited position becomes a free loop. Crossing ports are SW=0, SE=1, NE=2, NW=3.
84    pub fn closure(&self) -> Link {
85        let mut b = LinkBuilder::new();
86
87        let xs: Vec<_> = self.elements.iter().map(|g| {
88            let nt = if g.sign().is_positive() { NodeType::XR } else { NodeType::XL };
89            b.add_crossing(nt)
90        }).collect();
91
92        // the (top, bottom) ports each crossing presents to its two strand positions:
93        // (NW = 3, SW = 0) at position i, (NE = 2, SE = 1) at position i + 1.
94        let strands = self.elements.iter().zip(&xs).fold(
95            vec![vec![]; self.strands],
96            |mut strands, (g, &x)| {
97                let i = g.index() - 1;
98                strands[i]    .push(((x, 3), (x, 0)));
99                strands[i + 1].push(((x, 2), (x, 1)));
100                strands
101            }
102        );
103
104        for visits in strands {
105            if visits.is_empty() {
106                b.add_loop();
107            } else {
108                for (&(_, bot), &(top, _)) in visits.iter().circular_tuple_windows() {
109                    b.connect(bot, top);
110                }
111            }
112        }
113
114        // canonical braid orientation: strands run downward, entering every crossing from the
115        // top ports (NW = 3, NE = 2).
116        b.build_with(|_, j| j >= 2).unwrap()
117    }
118
119    pub fn display(&self) -> String {
120        fn row(strands: usize, g: &BraidGen) -> String {
121            let index = g.index();
122            let sign = g.sign();
123
124            (0..3).map(|r| {
125                (1..=strands).map(|i| {
126                    if i == index {
127                        match r {
128                            0 => "\\ /",
129                            1 => if sign.is_positive() { " / " } else { " \\ " },
130                            _ => "/ \\",
131                        }
132                    } else if i == index + 1 {
133                        " "
134                    } else {
135                        "| "
136                    }
137                }).join("")
138            }).join("\n")
139        }
140
141        self.elements.iter().map(|g|
142            row(self.strands, g)
143        ).join("\n")
144    }
145
146    pub fn load(name: &str) -> Result<Braid, Box<dyn std::error::Error>> {
147        let json = yui_core::util::data_dir::load_json("braid", name)?;
148        let code: Vec<i32> = serde_json::from_str(&json)?;
149        Ok(Braid::from_iter(code))
150    }
151}
152
153macro_rules! impl_from_int {
154    ($($t:ty),* $(,)?) => {
155        $(
156            impl<const N: usize> From<[$t; N]> for Braid {
157                fn from(value: [$t; N]) -> Self {
158                    Self::from_iter(value)
159                }
160            }
161
162            impl FromIterator<$t> for Braid {
163                fn from_iter<T: IntoIterator<Item = $t>>(iter: T) -> Self {
164                    let elements = iter.into_iter().map(|v|
165                        from_raw(i8::try_from(v).expect("BraidGen value must fit in i8"))
166                    ).collect_vec();
167                    let strands = elements.iter().map(|g| g.index() + 1).max().unwrap_or(0);
168                    Self::new(strands, elements)
169                }
170            }
171        )*
172    };
173}
174
175impl_from_int!(i8, i16, i32, i64);
176
177#[auto_ops]
178impl MulAssign<&Braid> for Braid {
179    fn mul_assign(&mut self, rhs: &Braid) {
180        assert_eq!(self.strands, rhs.strands);
181        self.elements.extend(rhs.elements.iter().cloned());
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use yui_core::poly::LPoly;
189    use crate::misc::jones_polynomial;
190
191    type P = LPoly<'q', i32>;
192
193    #[test]
194    fn closure_orientation() {
195        // σ₁⁻⁴ closes to the (2,4) torus link with parallel downward strands. jones detects the
196        // component orientations, so this locks the closure's convention (verified identical to the
197        // pre-LinkBuilder closure; L4a1's PD orientation is the anti-parallel variant, NOT this one).
198        let l = Braid::from([-1, -1, -1, -1]).closure();
199        assert_eq!(l.n_comps(), 2);
200        assert!(l.is_oriented());
201        assert_eq!(l.writhe(), -4);
202
203        let q = P::mono;
204        assert_eq!(jones_polynomial(&l), P::from_iter([(q(-2), 1), (q(-4), 1), (q(-6), 1), (q(-12), 1)]));
205    }
206
207    #[test]
208    fn init_by_code() {
209        let b = Braid::from([1, 1, -2, -1, 3]);
210        assert_eq!(b.strands(), 4);
211        assert_eq!(b.len(), 5);
212    }
213
214    #[test]
215    fn to_string() {
216        let b = Braid::from([1, 1, -2, -1, 3]);
217        assert_eq!(b.to_string(), "[1, 1, -2, -1, 3]");
218    }
219
220    #[test]
221    fn display() {
222        let b = Braid::from([1, 1, -2, -1, 3]);
223        let display = b.display();
224        assert_ne!(display, "")
225    }
226
227    #[test]
228    fn reduced_empty() {
229        let b = Braid::from([] as [i32; 0]);
230        assert_eq!(b.reduced(), b);
231    }
232
233    #[test]
234    fn reduced_no_cancel() {
235        let b = Braid::from([1, 2, 3]);
236        assert_eq!(b.reduced(), b);
237    }
238
239    #[test]
240    fn reduced_single_pair() {
241        let b = Braid::from([1, -1]);
242        let r = b.reduced();
243        assert!(r.is_id());
244        assert_eq!(r.strands(), b.strands());
245    }
246
247    #[test]
248    fn reduced_cascading() {
249        let b = Braid::from([1, 2, -2, -1]);
250        assert!(b.reduced().is_id());
251    }
252
253    #[test]
254    fn reduced_mid_sequence() {
255        let b = Braid::from([1, 2, -2, 3]);
256        assert_eq!(b.reduced(), Braid::new(b.strands(), vec![BraidGen::new(1), BraidGen::new(3)]));
257    }
258
259    #[test]
260    fn reduced_non_inverse_pair() {
261        // σ_1 σ_2^{-1} does not reduce (different indices).
262        let b = Braid::from([1, -2]);
263        assert_eq!(b.reduced(), b);
264    }
265
266    #[test]
267    fn closure() {
268        let b = Braid::test_data("3_1");
269        let l = b.closure();
270
271        assert_eq!(l.n_crossings(), 3);
272        assert_eq!(l.writhe(), 3);
273        assert_eq!(l.n_comps(), 1);
274    }
275
276    #[test]
277    fn extend() {
278        let b = Braid::from([1, 2]).extend(2);
279        assert_eq!(b.strands(), 5);
280        assert_eq!(b.len(), 2);
281    }
282
283    #[test]
284    fn extend_zero() {
285        let b0 = Braid::from([1, 2]);
286        let b1 = b0.extend(0);
287        assert_eq!(b1, b0);
288    }
289
290    #[test]
291    fn extend_closure_adds_loops() {
292        // extending a braid by k adds k free loops to the closure.
293        let b = Braid::from([1, 1, 1]).extend(2);
294        let l = b.closure();
295        assert_eq!(l.n_crossings(), 3);
296        assert_eq!(l.n_loops(), 2);
297        assert_eq!(l.n_comps(), 1 + 2); // original trefoil component + 2 free loops
298    }
299
300    #[test]
301    fn closure_identity_braid() {
302        // closure of the identity braid on n strands is an unlink of n components.
303        let b = Braid::id(3);
304        let l = b.closure();
305
306        assert_eq!(l.n_crossings(), 0);
307        assert_eq!(l.n_loops(), 3);
308        assert_eq!(l.n_comps(), 3);
309    }
310
311    #[test]
312    fn closure_with_free_strand() {
313        // 3 strands, σ_1 once: strands 1-2 form the σ_1 closure (one component),
314        // strand 3 is a free loop.
315        let b = Braid::new(3, vec![BraidGen::new(1)]);
316        let l = b.closure();
317
318        assert_eq!(l.n_crossings(), 1);
319        assert_eq!(l.n_loops(), 1);
320        assert_eq!(l.n_comps(), 2);
321    }
322
323    #[test]
324    #[should_panic(expected = "needs 6 strands")]
325    fn new_rejects_a_generator_beyond_the_strands() {
326        // σ₅ occupies positions 5 and 6, so `closure` would index past the strand list.
327        let _ = Braid::new(2, vec![BraidGen::new(5)]);
328    }
329}