Skip to main content

rust_igraph/algorithms/properties/
is_planar.rs

1//! Planarity testing via the Left-Right Planarity Test.
2//!
3//! Implements Ulrik Brandes' simplified version (2009) of the
4//! de Fraysseix–Rosenstiehl LR planarity algorithm. Runs in O(V + E).
5//!
6//! A graph is planar iff it can be embedded in the plane without edge
7//! crossings. Equivalently (Kuratowski), a graph is planar iff it has
8//! no `K_5` or `K_{3,3}` subdivision.
9
10use crate::core::{Graph, IgraphResult};
11
12/// Test whether a graph is planar.
13///
14/// Uses the Left-Right Planarity Test (Brandes 2009) which runs in
15/// O(V + E) time. Directed graphs are tested as if undirected.
16/// Self-loops and multi-edges are handled (self-loops are ignored;
17/// multi-edges contribute to the edge count for Euler's criterion).
18///
19/// # Examples
20///
21/// ```
22/// use rust_igraph::{Graph, is_planar};
23///
24/// // K_4 is planar
25/// let mut g = Graph::with_vertices(4);
26/// for i in 0..4u32 {
27///     for j in (i+1)..4 {
28///         g.add_edge(i, j).unwrap();
29///     }
30/// }
31/// assert!(is_planar(&g).unwrap());
32///
33/// // K_5 is NOT planar
34/// let mut g = Graph::with_vertices(5);
35/// for i in 0..5u32 {
36///     for j in (i+1)..5 {
37///         g.add_edge(i, j).unwrap();
38///     }
39/// }
40/// assert!(!is_planar(&g).unwrap());
41/// ```
42pub fn is_planar(graph: &Graph) -> IgraphResult<bool> {
43    let n = graph.vcount() as usize;
44
45    if n <= 4 {
46        return Ok(true);
47    }
48
49    // Build undirected adjacency (deduplicated, no self-loops)
50    let (adj, simple_edge_count) = build_simple_undirected_adj(graph);
51
52    // Euler's formula: planar simple graphs have m <= 3n - 6
53    if simple_edge_count > 3 * n - 6 {
54        return Ok(false);
55    }
56
57    // Test each connected component
58    let mut visited = vec![false; n];
59    for start in 0..n {
60        if visited[start] {
61            continue;
62        }
63        if !test_component_planar(&adj, start, &mut visited) {
64            return Ok(false);
65        }
66    }
67
68    Ok(true)
69}
70
71/// Build undirected adjacency list (simple: no self-loops, no duplicates).
72/// Returns `(adj_list, simple_edge_count)`.
73fn build_simple_undirected_adj(graph: &Graph) -> (Vec<Vec<usize>>, usize) {
74    let n = graph.vcount() as usize;
75    let ecount = graph.ecount();
76    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
77
78    for eid in 0..ecount {
79        #[allow(clippy::cast_possible_truncation)]
80        if let Ok((u, v)) = graph.edge(eid as u32) {
81            let u = u as usize;
82            let v = v as usize;
83            if u != v {
84                adj[u].push(v);
85                adj[v].push(u);
86            }
87        }
88    }
89
90    // Deduplicate each adjacency list
91    for list in &mut adj {
92        list.sort_unstable();
93        list.dedup();
94    }
95
96    let edge_count = adj.iter().map(Vec::len).sum::<usize>() / 2;
97    (adj, edge_count)
98}
99
100/// Sentinel for "no edge".
101const NONE: u32 = u32::MAX;
102
103/// An interval [low, high] of back edges on one side.
104/// `low` and `high` are edge indices into `oriented_edges`.
105/// `NONE` means the interval is empty.
106#[derive(Clone, Copy)]
107struct Interval {
108    low: u32,
109    high: u32,
110}
111
112impl Interval {
113    fn empty_interval() -> Self {
114        Self {
115            low: NONE,
116            high: NONE,
117        }
118    }
119
120    fn new(low: u32, high: u32) -> Self {
121        Self { low, high }
122    }
123
124    fn is_empty(self) -> bool {
125        self.low == NONE && self.high == NONE
126    }
127
128    fn conflicting(self, b: u32, lowpt: &[u32]) -> bool {
129        !self.is_empty() && lowpt[self.high as usize] > lowpt[b as usize]
130    }
131}
132
133/// A conflict pair: two intervals forced to opposite sides.
134#[derive(Clone, Copy)]
135struct ConflictPair {
136    left: Interval,
137    right: Interval,
138}
139
140impl ConflictPair {
141    fn new(left: Interval, right: Interval) -> Self {
142        Self { left, right }
143    }
144
145    fn swap(&mut self) {
146        std::mem::swap(&mut self.left, &mut self.right);
147    }
148
149    fn lowest(&self, lowpt: &[u32]) -> u32 {
150        if self.left.is_empty() {
151            return lowpt[self.right.low as usize];
152        }
153        if self.right.is_empty() {
154            return lowpt[self.left.low as usize];
155        }
156        std::cmp::min(
157            lowpt[self.left.low as usize],
158            lowpt[self.right.low as usize],
159        )
160    }
161}
162
163/// State for the LR planarity test on a single component.
164struct LrState {
165    oriented_edges: Vec<(usize, usize, bool)>,
166    out_edges: Vec<Vec<u32>>,
167    height: Vec<i32>,
168    parent_edge: Vec<u32>,
169    lowpt: Vec<u32>,
170    lowpt2: Vec<u32>,
171}
172
173/// Test planarity of one connected component using the full Brandes LR algorithm.
174#[allow(clippy::too_many_lines)]
175fn test_component_planar(adj: &[Vec<usize>], start: usize, visited: &mut [bool]) -> bool {
176    let n = adj.len();
177    let mut state = orient_and_compute_lowpoints(adj, start, visited, n);
178
179    let num_edges = state.oriented_edges.len();
180    let comp_size = state.oriented_edges.iter().filter(|e| e.2).count() + 1;
181    if comp_size <= 4 {
182        return true;
183    }
184    if num_edges > 3 * comp_size - 6 {
185        return false;
186    }
187
188    // Compute nesting depth and sort adjacency lists
189    let ordered_adjs = compute_nesting_and_sort(&state, n);
190
191    // Phase 2: LR testing
192    lr_testing(&mut state, &ordered_adjs, n)
193}
194
195/// Phase 1: DFS orientation + lowpoint computation.
196#[allow(clippy::too_many_lines)]
197fn orient_and_compute_lowpoints(
198    adj: &[Vec<usize>],
199    start: usize,
200    visited: &mut [bool],
201    n: usize,
202) -> LrState {
203    let mut oriented_edges: Vec<(usize, usize, bool)> = Vec::new();
204    let mut out_edges: Vec<Vec<u32>> = vec![Vec::new(); n];
205    let mut height: Vec<i32> = vec![-1; n];
206    let mut parent_edge: Vec<u32> = vec![NONE; n];
207
208    // Phase 1a: DFS orientation
209    {
210        let mut oriented_set: std::collections::HashSet<(usize, usize)> =
211            std::collections::HashSet::new();
212        let mut stack: Vec<(usize, usize)> = Vec::new();
213        height[start] = 0;
214        visited[start] = true;
215        stack.push((start, 0));
216
217        while let Some(&mut (v, ref mut idx)) = stack.last_mut() {
218            if *idx < adj[v].len() {
219                let w = adj[v][*idx];
220                *idx += 1;
221                let key = if v < w { (v, w) } else { (w, v) };
222                if oriented_set.contains(&key) {
223                    continue;
224                }
225                oriented_set.insert(key);
226                #[allow(clippy::cast_possible_truncation)]
227                let eidx = oriented_edges.len() as u32;
228                out_edges[v].push(eidx);
229                if height[w] == -1 {
230                    oriented_edges.push((v, w, true));
231                    parent_edge[w] = eidx;
232                    height[w] = height[v] + 1;
233                    visited[w] = true;
234                    stack.push((w, 0));
235                } else {
236                    oriented_edges.push((v, w, false));
237                }
238            } else {
239                stack.pop();
240            }
241        }
242    }
243
244    let num_edges = oriented_edges.len();
245
246    // Phase 1b: Compute lowpoints bottom-up
247    #[allow(clippy::cast_sign_loss)]
248    let mut lowpt: Vec<u32> = vec![0; num_edges];
249    let mut lowpt2: Vec<u32> = vec![0; num_edges];
250
251    for eidx in 0..num_edges {
252        let (source, target, is_tree) = oriented_edges[eidx];
253        #[allow(clippy::cast_sign_loss)]
254        if is_tree {
255            lowpt[eidx] = height[source] as u32;
256            lowpt2[eidx] = height[source] as u32;
257        } else {
258            lowpt[eidx] = height[target] as u32;
259            lowpt2[eidx] = height[source] as u32;
260        }
261    }
262
263    // Post-order traversal
264    let comp_size = oriented_edges.iter().filter(|e| e.2).count() + 1;
265    let mut post_order: Vec<usize> = Vec::with_capacity(comp_size);
266    {
267        let mut stack: Vec<(usize, usize)> = vec![(start, 0)];
268        while let Some(&mut (v, ref mut idx)) = stack.last_mut() {
269            if *idx < out_edges[v].len() {
270                let eidx = out_edges[v][*idx] as usize;
271                *idx += 1;
272                let (_, w, is_tree) = oriented_edges[eidx];
273                if is_tree {
274                    stack.push((w, 0));
275                }
276            } else {
277                post_order.push(v);
278                stack.pop();
279            }
280        }
281    }
282
283    // Propagate lowpoints bottom-up
284    for &v in &post_order {
285        let e = parent_edge[v];
286        if e == NONE {
287            continue;
288        }
289        let e_idx = e as usize;
290        for &child_eidx in &out_edges[v] {
291            let cidx = child_eidx as usize;
292            let clp = lowpt[cidx];
293            let clp2 = lowpt2[cidx];
294            match clp.cmp(&lowpt[e_idx]) {
295                std::cmp::Ordering::Less => {
296                    lowpt2[e_idx] = std::cmp::min(lowpt[e_idx], clp2);
297                    lowpt[e_idx] = clp;
298                }
299                std::cmp::Ordering::Greater => {
300                    lowpt2[e_idx] = std::cmp::min(lowpt2[e_idx], clp);
301                }
302                std::cmp::Ordering::Equal => {
303                    lowpt2[e_idx] = std::cmp::min(lowpt2[e_idx], clp2);
304                }
305            }
306        }
307    }
308
309    LrState {
310        oriented_edges,
311        out_edges,
312        height,
313        parent_edge,
314        lowpt,
315        lowpt2,
316    }
317}
318
319/// Compute nesting depth and sort adjacency lists by nesting depth.
320fn compute_nesting_and_sort(state: &LrState, n: usize) -> Vec<Vec<u32>> {
321    let num_edges = state.oriented_edges.len();
322    let mut nesting_depth: Vec<i32> = vec![0; num_edges];
323
324    #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
325    for (eidx, nd) in nesting_depth.iter_mut().enumerate().take(num_edges) {
326        let (source, _, _) = state.oriented_edges[eidx];
327        *nd = 2 * (state.lowpt[eidx] as i32);
328        if state.lowpt2[eidx] < state.height[source] as u32 {
329            *nd += 1;
330        }
331    }
332
333    let mut ordered_adjs: Vec<Vec<u32>> = vec![Vec::new(); n];
334    for (v, slot) in ordered_adjs.iter_mut().enumerate().take(n) {
335        if state.height[v] == -1 {
336            continue;
337        }
338        let mut edges: Vec<u32> = state.out_edges[v].clone();
339        edges.sort_by_key(|&e| nesting_depth[e as usize]);
340        *slot = edges;
341    }
342    ordered_adjs
343}
344
345/// Phase 2: LR constraint testing.
346fn lr_testing(state: &mut LrState, ordered_adjs: &[Vec<u32>], n: usize) -> bool {
347    let num_edges = state.oriented_edges.len();
348    let mut ref_edge: Vec<u32> = vec![NONE; num_edges];
349    let mut stack: Vec<ConflictPair> = Vec::new();
350    let mut stack_bottom: Vec<usize> = vec![0; num_edges];
351    let mut lowpt_edge: Vec<u32> = vec![NONE; num_edges];
352
353    let mut dfs_stack: Vec<usize> = Vec::new();
354    // Find start vertex (first with height == 0)
355    for v in 0..n {
356        if state.height[v] == 0 {
357            dfs_stack.push(v);
358            break;
359        }
360    }
361
362    let mut ind: Vec<usize> = vec![0; n];
363    let mut skip_init_edge: Vec<bool> = vec![false; num_edges];
364
365    while let Some(&v) = dfs_stack.last() {
366        let e = state.parent_edge[v];
367        let mut skip_final = false;
368
369        while ind[v] < ordered_adjs[v].len() {
370            let ei = ordered_adjs[v][ind[v]];
371            let ei_usize = ei as usize;
372
373            if !skip_init_edge[ei_usize] {
374                stack_bottom[ei_usize] = stack.len();
375                let (_, w, is_tree) = state.oriented_edges[ei_usize];
376
377                if is_tree {
378                    skip_init_edge[ei_usize] = true;
379                    dfs_stack.push(w);
380                    skip_final = true;
381                    break;
382                }
383                lowpt_edge[ei_usize] = ei;
384                stack.push(ConflictPair::new(
385                    Interval::empty_interval(),
386                    Interval::new(ei, ei),
387                ));
388            }
389
390            // Integrate new return edges
391            #[allow(clippy::cast_sign_loss)]
392            if state.lowpt[ei_usize] < state.height[v] as u32 {
393                if ind[v] == 0 {
394                    if e != NONE {
395                        lowpt_edge[e as usize] = lowpt_edge[ei_usize];
396                    }
397                } else if e != NONE
398                    && !add_constraints(
399                        ei,
400                        e,
401                        &state.lowpt,
402                        &mut ref_edge,
403                        &mut stack,
404                        &stack_bottom,
405                        &lowpt_edge,
406                    )
407                {
408                    return false;
409                }
410            }
411
412            ind[v] += 1;
413        }
414
415        if !skip_final {
416            dfs_stack.pop();
417            if e != NONE {
418                remove_back_edges(
419                    e,
420                    &state.oriented_edges,
421                    &state.height,
422                    &state.lowpt,
423                    &mut ref_edge,
424                    &mut stack,
425                );
426            }
427        }
428    }
429
430    true
431}
432
433/// Add constraints for edge `ei` against parent tree edge `e`.
434/// Returns false if the graph is non-planar.
435fn add_constraints(
436    ei: u32,
437    e: u32,
438    lowpt: &[u32],
439    ref_edge: &mut [u32],
440    stack: &mut Vec<ConflictPair>,
441    stack_bottom: &[usize],
442    lowpt_edge: &[u32],
443) -> bool {
444    let mut p = ConflictPair::new(Interval::empty_interval(), Interval::empty_interval());
445
446    // Merge return edges of e_i into P.right
447    loop {
448        let Some(mut q) = stack.pop() else {
449            return false;
450        };
451        if !q.left.is_empty() {
452            q.swap();
453        }
454        if !q.left.is_empty() {
455            return false;
456        }
457        if lowpt[q.right.low as usize] > lowpt[e as usize] {
458            if p.right.is_empty() {
459                p.right = q.right;
460            } else {
461                ref_edge[p.right.low as usize] = q.right.high;
462                p.right.low = q.right.low;
463            }
464        } else {
465            ref_edge[q.right.low as usize] = lowpt_edge[e as usize];
466        }
467        if stack.len() == stack_bottom[ei as usize] {
468            break;
469        }
470    }
471
472    // Merge conflicting return edges of e_1,...,e_{i-1} into P.left
473    while let Some(top) = stack.last() {
474        if !top.left.conflicting(ei, lowpt) && !top.right.conflicting(ei, lowpt) {
475            break;
476        }
477        let mut q = stack.pop().unwrap();
478        if q.right.conflicting(ei, lowpt) {
479            q.swap();
480        }
481        if q.right.conflicting(ei, lowpt) {
482            return false;
483        }
484        // Merge Q.right interval into P.right
485        if p.right.low != NONE {
486            ref_edge[p.right.low as usize] = q.right.high;
487        }
488        if q.right.low != NONE {
489            p.right.low = q.right.low;
490        }
491        if p.right.is_empty() && q.right.low != NONE {
492            p.right = Interval::new(q.right.low, q.right.high);
493        }
494
495        // Merge Q.left into P.left
496        if p.left.is_empty() {
497            p.left = q.left;
498        } else {
499            ref_edge[p.left.low as usize] = q.left.high;
500            p.left.low = q.left.low;
501        }
502    }
503
504    if !p.left.is_empty() || !p.right.is_empty() {
505        stack.push(p);
506    }
507    true
508}
509
510/// Remove back edges returning to the parent of tree edge `e`.
511fn remove_back_edges(
512    e: u32,
513    oriented_edges: &[(usize, usize, bool)],
514    height: &[i32],
515    lowpt: &[u32],
516    ref_edge: &mut [u32],
517    stack: &mut Vec<ConflictPair>,
518) {
519    let u = oriented_edges[e as usize].0;
520    #[allow(clippy::cast_sign_loss)]
521    let h_u = height[u] as u32;
522
523    // Trim: drop entire conflict pairs whose lowest return == height[u]
524    while let Some(top) = stack.last() {
525        if top.lowest(lowpt) != h_u {
526            break;
527        }
528        stack.pop();
529    }
530
531    if let Some(p) = stack.last_mut() {
532        // Trim left interval: advance high through ref chain
533        while p.left.high != NONE {
534            let h_edge = p.left.high as usize;
535            let (_, target, _) = oriented_edges[h_edge];
536            if target != u {
537                break;
538            }
539            p.left.high = ref_edge[h_edge];
540        }
541        if p.left.high == NONE && p.left.low != NONE {
542            ref_edge[p.left.low as usize] = p.right.low;
543            p.left.low = NONE;
544        }
545
546        // Trim right interval
547        while p.right.high != NONE {
548            let h_edge = p.right.high as usize;
549            let (_, target, _) = oriented_edges[h_edge];
550            if target != u {
551                break;
552            }
553            p.right.high = ref_edge[h_edge];
554        }
555        if p.right.high == NONE && p.right.low != NONE {
556            ref_edge[p.right.low as usize] = p.left.low;
557            p.right.low = NONE;
558        }
559
560        // If both intervals are now empty, remove the pair
561        if p.left.is_empty() && p.right.is_empty() {
562            stack.pop();
563        }
564    }
565
566    // Set ref for tree edge e
567    if lowpt[e as usize] < h_u {
568        if let Some(top) = stack.last() {
569            let hl = top.left.high;
570            let hr = top.right.high;
571            if hl != NONE && (hr == NONE || lowpt[hl as usize] > lowpt[hr as usize]) {
572                ref_edge[e as usize] = hl;
573            } else if hr != NONE {
574                ref_edge[e as usize] = hr;
575            }
576        }
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn empty_graph() {
586        let g = Graph::with_vertices(0);
587        assert!(is_planar(&g).unwrap());
588    }
589
590    #[test]
591    fn single_vertex() {
592        let g = Graph::with_vertices(1);
593        assert!(is_planar(&g).unwrap());
594    }
595
596    #[test]
597    fn single_edge() {
598        let mut g = Graph::with_vertices(2);
599        g.add_edge(0, 1).unwrap();
600        assert!(is_planar(&g).unwrap());
601    }
602
603    #[test]
604    fn triangle() {
605        let mut g = Graph::with_vertices(3);
606        g.add_edge(0, 1).unwrap();
607        g.add_edge(1, 2).unwrap();
608        g.add_edge(2, 0).unwrap();
609        assert!(is_planar(&g).unwrap());
610    }
611
612    #[test]
613    fn k4_planar() {
614        let mut g = Graph::with_vertices(4);
615        for i in 0..4u32 {
616            for j in (i + 1)..4 {
617                g.add_edge(i, j).unwrap();
618            }
619        }
620        assert!(is_planar(&g).unwrap());
621    }
622
623    #[test]
624    fn k5_not_planar() {
625        let mut g = Graph::with_vertices(5);
626        for i in 0..5u32 {
627            for j in (i + 1)..5 {
628                g.add_edge(i, j).unwrap();
629            }
630        }
631        assert!(!is_planar(&g).unwrap());
632    }
633
634    #[test]
635    fn k33_not_planar() {
636        let mut g = Graph::with_vertices(6);
637        for i in 0..3u32 {
638            for j in 3..6u32 {
639                g.add_edge(i, j).unwrap();
640            }
641        }
642        assert!(!is_planar(&g).unwrap());
643    }
644
645    #[test]
646    fn petersen_not_planar() {
647        let mut g = Graph::with_vertices(10);
648        // Outer cycle
649        for i in 0..5u32 {
650            g.add_edge(i, (i + 1) % 5).unwrap();
651        }
652        // Inner pentagram
653        for i in 0..5u32 {
654            g.add_edge(5 + i, 5 + (i + 2) % 5).unwrap();
655        }
656        // Spokes
657        for i in 0..5u32 {
658            g.add_edge(i, 5 + i).unwrap();
659        }
660        assert!(!is_planar(&g).unwrap());
661    }
662
663    #[test]
664    fn tree_planar() {
665        let mut g = Graph::with_vertices(10);
666        for i in 1..10u32 {
667            g.add_edge(i, i / 2).unwrap();
668        }
669        assert!(is_planar(&g).unwrap());
670    }
671
672    #[test]
673    fn cycle_planar() {
674        let mut g = Graph::with_vertices(20);
675        for i in 0..20u32 {
676            g.add_edge(i, (i + 1) % 20).unwrap();
677        }
678        assert!(is_planar(&g).unwrap());
679    }
680
681    #[test]
682    fn grid_4x4_planar() {
683        let mut g = Graph::with_vertices(16);
684        for r in 0..4u32 {
685            for c in 0..4u32 {
686                let v = r * 4 + c;
687                if c + 1 < 4 {
688                    g.add_edge(v, v + 1).unwrap();
689                }
690                if r + 1 < 4 {
691                    g.add_edge(v, v + 4).unwrap();
692                }
693            }
694        }
695        assert!(is_planar(&g).unwrap());
696    }
697
698    #[test]
699    fn self_loops_ignored() {
700        let mut g = Graph::with_vertices(5);
701        for i in 0..5u32 {
702            for j in (i + 1)..5 {
703                g.add_edge(i, j).unwrap();
704            }
705        }
706        g.add_edge(0, 0).unwrap();
707        assert!(!is_planar(&g).unwrap());
708    }
709
710    #[test]
711    fn disconnected_planar() {
712        let mut g = Graph::with_vertices(6);
713        g.add_edge(0, 1).unwrap();
714        g.add_edge(1, 2).unwrap();
715        g.add_edge(2, 0).unwrap();
716        g.add_edge(3, 4).unwrap();
717        g.add_edge(4, 5).unwrap();
718        g.add_edge(5, 3).unwrap();
719        assert!(is_planar(&g).unwrap());
720    }
721
722    #[test]
723    fn disconnected_non_planar() {
724        let mut g = Graph::with_vertices(6);
725        for i in 0..5u32 {
726            for j in (i + 1)..5 {
727                g.add_edge(i, j).unwrap();
728            }
729        }
730        assert!(!is_planar(&g).unwrap());
731    }
732
733    #[test]
734    fn wheel_5_planar() {
735        let mut g = Graph::with_vertices(6);
736        for i in 1..6u32 {
737            g.add_edge(0, i).unwrap();
738        }
739        for i in 1..5u32 {
740            g.add_edge(i, i + 1).unwrap();
741        }
742        g.add_edge(5, 1).unwrap();
743        assert!(is_planar(&g).unwrap());
744    }
745
746    #[test]
747    fn k5_minus_edge_planar() {
748        let mut g = Graph::with_vertices(5);
749        for i in 0..5u32 {
750            for j in (i + 1)..5 {
751                if !(i == 0 && j == 4) {
752                    g.add_edge(i, j).unwrap();
753                }
754            }
755        }
756        assert!(is_planar(&g).unwrap());
757    }
758
759    #[test]
760    fn k33_minus_edge_planar() {
761        let mut g = Graph::with_vertices(6);
762        for i in 0..3u32 {
763            for j in 3..6u32 {
764                if !(i == 0 && j == 3) {
765                    g.add_edge(i, j).unwrap();
766                }
767            }
768        }
769        assert!(is_planar(&g).unwrap());
770    }
771
772    #[test]
773    fn icosahedron_planar() {
774        let mut g = Graph::with_vertices(12);
775        let edges: &[(u32, u32)] = &[
776            (0, 1),
777            (0, 2),
778            (0, 3),
779            (0, 4),
780            (0, 5),
781            (1, 2),
782            (2, 3),
783            (3, 4),
784            (4, 5),
785            (5, 1),
786            (1, 6),
787            (2, 6),
788            (2, 7),
789            (3, 7),
790            (3, 8),
791            (4, 8),
792            (4, 9),
793            (5, 9),
794            (5, 10),
795            (1, 10),
796            (6, 7),
797            (7, 8),
798            (8, 9),
799            (9, 10),
800            (10, 6),
801            (6, 11),
802            (7, 11),
803            (8, 11),
804            (9, 11),
805            (10, 11),
806        ];
807        for &(u, v) in edges {
808            g.add_edge(u, v).unwrap();
809        }
810        assert!(is_planar(&g).unwrap());
811    }
812}