use std::collections::{BTreeMap, HashMap};
use std::fmt::Display;
use petgraph::stable_graph::{StableDiGraph, NodeIndex, EdgeIndex};
use yui_core::algo::UnionFind;
use crate::{Link, Node, NodeType, Edge, Slot};
pub type Port = (NodeIndex, usize);
#[derive(Debug, Default)]
pub struct LinkBuilder {
graph: StableDiGraph<NodeType, (usize, usize)>,
loops: usize,
}
impl LinkBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn n_nodes(&self) -> usize {
self.graph.node_count()
}
pub fn add_node(&mut self, node_type: NodeType) -> NodeIndex {
self.graph.add_node(node_type)
}
pub fn add_crossing(&mut self, node_type: NodeType) -> NodeIndex {
assert!(matches!(node_type, NodeType::XL | NodeType::XR), "add_crossing expects XL or XR, got {node_type}");
self.add_node(node_type)
}
pub fn connect(&mut self, a: Port, b: Port) {
assert!(a.1 < 4 && b.1 < 4, "slots must be 0..4, got {} and {}", a.1, b.1);
self.graph.add_edge(a.0, b.0, (a.1, b.1));
}
pub fn add_loop(&mut self) {
self.loops += 1;
}
pub fn add_v_twist(&mut self, tt: NodeType, k: usize) -> (Port, Port, Port, Port) {
assert!(k >= 1, "a twist needs at least one crossing");
let v: Vec<_> = (0..k).map(|_| self.add_crossing(tt)).collect();
for w in v.windows(2) {
self.connect((w[0], 3), (w[1], 0));
self.connect((w[0], 2), (w[1], 1));
}
((v[0], 0), (v[0], 1), (v[k - 1], 2), (v[k - 1], 3))
}
pub fn add_h_twist(&mut self, tt: NodeType, k: usize) -> (Port, Port, Port, Port) {
assert!(k >= 1, "a twist needs at least one crossing");
let h: Vec<_> = (0..k).map(|_| self.add_crossing(tt)).collect();
for w in h.windows(2) {
self.connect((w[0], 1), (w[1], 0));
self.connect((w[0], 2), (w[1], 3));
}
((h[0], 0), (h[k - 1], 1), (h[k - 1], 2), (h[0], 3))
}
pub fn add_link(&mut self, l: &Link) -> Vec<NodeIndex> {
let verts: Vec<NodeIndex> = l.nodes().map(|x| self.add_node(x.node_type())).collect();
let occ = l.nodes().enumerate().flat_map(|(i, x)| {
let v = verts[i];
Slot::ALL.map(move |s| (x.edge(s), (v, s.index())))
}).fold(BTreeMap::<Edge, Vec<Port>>::new(), |mut occ, (e, p)| {
occ.entry(e).or_default().push(p);
occ
});
for ports in occ.values() {
self.connect(ports[0], ports[1]);
}
for _ in l.loops() {
self.add_loop();
}
verts
}
pub fn disconnect(&mut self, p: Port) {
let e = self.graph.edge_indices().find(|&e| {
let (a, b) = self.ends(e);
a == p || b == p
}).unwrap_or_else(||
panic!("port (node {}, slot {}) has no edge to disconnect", p.0.index(), p.1)
);
self.graph.remove_edge(e);
}
pub fn edge_at(&self, p: Port) -> Option<Edge> {
self.graph.edge_indices().enumerate().find_map(|(i, e)| {
let (a, b) = self.ends(e);
(a == p || b == p).then_some(i as Edge + 1)
})
}
pub fn build(self) -> Result<Link, LinkError> {
self.build_with(|_, _| true) }
pub fn build_with<F>(self, is_incoming: F) -> Result<Link, LinkError>
where F: Fn(usize, usize) -> bool {
self.validate()?;
let edge_at: HashMap<Port, Edge> = self.graph.edge_indices().enumerate().flat_map(|(i, e)| {
let (a, b) = self.ends(e);
let id = i as Edge + 1;
[(a, id), (b, id)]
}).collect();
let nodes = self.graph.node_indices().map(|v|
Node::new(self.graph[v], None, [0, 1, 2, 3].map(|s| edge_at[&(v, s)]))
);
let e0 = self.graph.edge_count() as Edge + 1;
let loops = e0 .. e0 + self.loops as Edge;
let mut l = Link::new(nodes.collect::<Vec<_>>(), loops);
l.reorient(|i, s| is_incoming(i, s.index()));
Ok(l)
}
fn validate(&self) -> Result<(), LinkError> {
let alpha = self.edge_pairing()?;
if self.is_planar_with(&alpha) {
Ok(())
} else {
Err(LinkError::NonPlanar)
}
}
pub fn is_planar(&self) -> bool {
self.edge_pairing().map(|alpha|
self.is_planar_with(&alpha)
).unwrap_or(false)
}
fn is_planar_with(&self, alpha: &HashMap<Port, Port>) -> bool {
let (v, e) = (self.graph.node_count(), self.graph.edge_count());
let dart = |(n, s): &Port| 4 * n.index() + s;
let phi = |p: &Port| {
let (n, s) = alpha[p];
(n, (s + 1) % 4)
};
let mut faces = UnionFind::new(4 * v);
let mut comps = UnionFind::new(v);
for p in alpha.keys() {
faces.union(dart(p), dart(&phi(p)));
comps.union(p.0.index(), alpha[p].0.index());
}
let (f, c) = (faces.into_disjoint().len(), comps.into_disjoint().len());
(v + f) as isize - e as isize == 2 * c as isize
}
fn edge_pairing(&self) -> Result<HashMap<Port, Port>, LinkError> {
let mut alpha = HashMap::new();
for e in self.graph.edge_indices() {
let (a, b) = self.ends(e);
for (p, q) in [(a, b), (b, a)] {
if alpha.insert(p, q).is_some() {
return Err(LinkError::DuplicatePort(p));
}
}
}
let open = self.graph.node_indices().flat_map(|v|
(0..4).map(move |s| (v, s))
).find(|p|
!alpha.contains_key(p)
);
match open {
Some(p) => Err(LinkError::UnconnectedPort(p)),
None => Ok(alpha),
}
}
fn ends(&self, e: EdgeIndex) -> (Port, Port) {
let (s, t) = self.graph.edge_endpoints(e).unwrap();
let &(fs, ts) = self.graph.edge_weight(e).unwrap();
((s, fs), (t, ts))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinkError {
UnconnectedPort(Port),
DuplicatePort(Port),
NonPlanar,
}
impl Display for LinkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LinkError::UnconnectedPort((v, s)) =>
write!(f, "port (node {}, slot {s}) is not connected", v.index()),
LinkError::DuplicatePort((v, s)) =>
write!(f, "port (node {}, slot {s}) is connected more than once", v.index()),
LinkError::NonPlanar =>
write!(f, "the wiring is non-planar (genus > 0)"),
}
}
}
impl std::error::Error for LinkError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::misc::jones_polynomial;
fn rebuild(l: &Link) -> Link {
let mut b = LinkBuilder::new();
b.add_link(l);
b.build().unwrap()
}
#[test]
fn round_trip_preserves_knot() {
for name in ["3_1", "4_1", "5_2", "6_2"] {
let l = Link::test_data(name);
let r = rebuild(&l);
assert_eq!(r.n_comps(), l.n_comps());
assert!(r.is_oriented());
assert_eq!(jones_polynomial(&r), jones_polynomial(&l), "round-trip changed {name}");
}
}
#[test]
fn round_trip_orients_unlink2() {
let l = Link::test_data("unlink2");
assert!(!l.is_oriented());
let r = rebuild(&l);
assert_eq!(r.n_comps(), 2);
assert!(r.is_oriented());
assert_eq!(jones_polynomial(&r), jones_polynomial(&Link::unlink(2)));
}
#[test]
fn is_planar_detects_genus() {
let mut planar = LinkBuilder::new();
let x = planar.add_crossing(NodeType::XR);
planar.connect((x, 0), (x, 1));
planar.connect((x, 2), (x, 3));
assert!(planar.is_planar());
let mut torus = LinkBuilder::new();
let y = torus.add_crossing(NodeType::XR);
torus.connect((y, 0), (y, 2));
torus.connect((y, 1), (y, 3));
assert!(!torus.is_planar());
assert!(matches!(torus.build(), Err(LinkError::NonPlanar)));
}
#[test]
fn is_planar_accepts_real_knots() {
for name in ["3_1", "4_1", "5_1", "5_2", "6_1", "6_2", "6_3", "7_1", "7_2"] {
let mut b = LinkBuilder::new();
b.add_link(&Link::test_data(name));
assert!(b.is_planar(), "{name} is planar but is_planar returned false");
}
}
#[test]
fn build_with_free_loop() {
let mut b = LinkBuilder::new();
b.add_loop();
let l = b.build().unwrap();
assert_eq!(l.n_comps(), 1);
assert_eq!(l.n_crossings(), 0);
}
#[test]
fn unconnected_port_errors() {
let mut b = LinkBuilder::new();
b.add_crossing(NodeType::XR);
assert!(matches!(b.build(), Err(LinkError::UnconnectedPort(_))));
}
}