diff --git a/rustworkx-core/src/planar/lr_planar.rs b/rustworkx-core/src/planar/lr_planar.rs index c91b50989..a76c0afc8 100644 --- a/rustworkx-core/src/planar/lr_planar.rs +++ b/rustworkx-core/src/planar/lr_planar.rs @@ -16,11 +16,13 @@ use std::vec::IntoIter; use hashbrown::{hash_map::Entry, HashMap}; use petgraph::{ + graph::NodeIndex, + stable_graph::StableGraph, visit::{ EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdges, IntoNodeIdentifiers, NodeCount, - Visitable, + NodeIndexable, Visitable, }, - Undirected, + Directed, Undirected, }; use crate::traversal::{depth_first_search, DfsEvent}; @@ -191,7 +193,8 @@ where } } -enum Sign { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Sign { Plus, Minus, } @@ -208,17 +211,17 @@ enum LRTestDfsEvent { // An error: graph is *not* planar. struct NonPlanar {} -struct LRState +pub struct LRState where G::NodeId: Hash + Eq, { - graph: G, + pub graph: G, /// roots of the DFS forest. - roots: Vec, + pub roots: Vec, /// distnace from root. height: HashMap, /// parent edge. - eparent: HashMap>, + pub eparent: HashMap>, /// height of lowest return point. lowpt: HashMap, usize>, /// height of next-to-lowest return point. Only used to check if an edge is chordal. @@ -226,23 +229,31 @@ where /// next back edge in traversal with lowest return point. lowpt_edge: HashMap, Edge>, /// proxy for nesting order ≺ given by twice lowpt (plus 1 if chordal). - nesting_depth: HashMap, usize>, + pub nesting_depth: HashMap, isize>, /// stack for conflict pairs. stack: Vec>>, /// marks the top conflict pair when an edge was pushed in the stack. stack_emarker: HashMap, ConflictPair>>, /// edge relative to which side is defined. - eref: HashMap, Edge>, + pub eref: HashMap, Edge>, /// side of edge, or modifier for side of reference edge. - side: HashMap, Sign>, + pub side: HashMap, Sign>, + /// directed graph used to build the embedding + pub dir_graph: StableGraph<(), (), Directed>, } impl LRState where - G: GraphBase + NodeCount + EdgeCount + IntoEdges + Visitable, + G: GraphBase + + NodeCount + + EdgeCount + + IntoNodeIdentifiers + + NodeIndexable + + IntoEdges + + Visitable, G::NodeId: Hash + Eq, { - fn new(graph: G) -> Self { + pub fn new(graph: G) -> Self { let num_nodes = graph.node_count(); let num_edges = graph.edge_count(); @@ -262,6 +273,33 @@ where .edge_references() .map(|e| ((e.source(), e.target()), Sign::Plus)) .collect(), + dir_graph: StableGraph::with_capacity(num_nodes, 0), + } + } + + // Create the directed graph for the embedding in stable format + // to match the original graph. + fn build_dir_graph(&mut self) + where + ::NodeId: Ord, + { + let mut tmp_nodes: Vec = Vec::new(); + let mut count: usize = 0; + for _ in 0..self.graph.node_bound() { + self.dir_graph.add_node(()); + } + for gnode in self.graph.node_identifiers() { + let gidx = self.graph.to_index(gnode); + if gidx != count { + for idx in count..gidx { + tmp_nodes.push(idx); + } + count = gidx; + } + count += 1; + } + for tmp_node in tmp_nodes { + self.dir_graph.remove_node(NodeIndex::new(tmp_node)); } } @@ -274,6 +312,11 @@ where } } DfsEvent::TreeEdge(v, w, _) => { + let v_dir = NodeIndex::new(self.graph.to_index(v)); + let w_dir = NodeIndex::new(self.graph.to_index(w)); + if !self.dir_graph.contains_edge(v_dir, w_dir) { + self.dir_graph.add_edge(v_dir, w_dir, ()); + } let ei = (v, w); let v_height = self.height[&v]; let w_height = v_height + 1; @@ -287,6 +330,11 @@ where DfsEvent::BackEdge(v, w, _) => { // do *not* consider ``(v, w)`` as a back edge if ``(w, v)`` is a tree edge. if Some(&(w, v)) != self.eparent.get(&v) { + let v_dir = NodeIndex::new(self.graph.to_index(v)); + let w_dir = NodeIndex::new(self.graph.to_index(w)); + if !self.dir_graph.contains_edge(v_dir, w_dir) { + self.dir_graph.add_edge(v_dir, w_dir, ()); + } let ei = (v, w); self.lowpt.insert(ei, self.height[&w]); self.lowpt_2.insert(ei, self.height[&v]); @@ -311,9 +359,9 @@ where if self.lowpt_2[&ei] < self.height[&v] { // if it's chordal, add one. - self.nesting_depth.insert(ei, 2 * low + 1); + self.nesting_depth.insert(ei, (2 * low) as isize + 1); } else { - self.nesting_depth.insert(ei, 2 * low); + self.nesting_depth.insert(ei, (2 * low) as isize); } // update lowpoints of parent edge. @@ -656,7 +704,7 @@ where /// # Example: /// ```rust /// use rustworkx_core::petgraph::graph::UnGraph; -/// use rustworkx_core::planar::is_planar; +/// use rustworkx_core::planar::{is_planar_for_layout, LRState}; /// /// let grid = UnGraph::<(), ()>::from_edges(&[ /// // row edges @@ -664,29 +712,39 @@ where /// // col edges /// (0, 3), (3, 6), (1, 4), (4, 7), (2, 5), (5, 8), /// ]); -/// assert!(is_planar(&grid)) +/// let mut lr_state = LRState::new(&grid); +/// assert!(is_planar_for_layout(&grid, Some(&mut lr_state))) /// ``` -pub fn is_planar(graph: G) -> bool +pub fn is_planar_for_layout(graph: G, state: Option<&mut LRState>) -> bool where G: GraphProp + NodeCount + EdgeCount + IntoEdges + + NodeIndexable + IntoNodeIdentifiers + Visitable, - G::NodeId: Hash + Eq, + G::NodeId: Hash + Eq + Ord, { - let mut state = LRState::new(graph); + // If None passed for state, create new LRState + let mut lr_state = LRState::new(graph); + let lr_state = match state { + Some(state) => state, + None => &mut lr_state, + }; + + // Build directed graph for the embedding + lr_state.build_dir_graph(); // Dfs orientation phase depth_first_search(graph, graph.node_identifiers(), |event| { - state.lr_orientation_visitor(event) + lr_state.lr_orientation_visitor(event) }); // Left - Right partition. - for v in state.roots.clone() { - let res = lr_visit_ordered_dfs_tree(&mut state, v, |state, event| { - state.lr_testing_visitor(event) + for v in lr_state.roots.clone() { + let res = lr_visit_ordered_dfs_tree(lr_state, v, |lr_state, event| { + lr_state.lr_testing_visitor(event) }); if res.is_err() { return false; @@ -695,3 +753,40 @@ where true } + +/// Check if an undirected graph is planar. +/// +/// A graph is planar iff it can be drawn in a plane without any edge +/// intersections. +/// +/// The planarity check algorithm is based on the +/// Left-Right Planarity Test: +/// +/// [`Ulrik Brandes: The Left-Right Planarity Test (2009)`](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.217.9208) +/// +/// # Example: +/// ```rust +/// use rustworkx_core::petgraph::graph::UnGraph; +/// use rustworkx_core::planar::is_planar; +/// +/// let grid = UnGraph::<(), ()>::from_edges(&[ +/// // row edges +/// (0, 1), (1, 2), (3, 4), (4, 5), (6, 7), (7, 8), +/// // col edges +/// (0, 3), (3, 6), (1, 4), (4, 7), (2, 5), (5, 8), +/// ]); +/// assert!(is_planar(&grid)) +/// ``` +pub fn is_planar(graph: G) -> bool +where + G: GraphProp + + NodeCount + + EdgeCount + + IntoEdges + + IntoNodeIdentifiers + + NodeIndexable + + Visitable, + G::NodeId: Hash + Eq + Ord, +{ + is_planar_for_layout(graph, None) +} diff --git a/rustworkx-core/src/planar/mod.rs b/rustworkx-core/src/planar/mod.rs index e67dd2775..a7e5ca020 100644 --- a/rustworkx-core/src/planar/mod.rs +++ b/rustworkx-core/src/planar/mod.rs @@ -12,6 +12,6 @@ //! Module for planar graphs. -mod lr_planar; +pub mod lr_planar; -pub use lr_planar::is_planar; +pub use lr_planar::{is_planar, is_planar_for_layout, LRState}; diff --git a/rustworkx-core/tests/test_planar.rs b/rustworkx-core/tests/test_planar.rs new file mode 100644 index 000000000..8a4fa97b7 --- /dev/null +++ b/rustworkx-core/tests/test_planar.rs @@ -0,0 +1,281 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Test module for planar graphs. + +use rustworkx_core::petgraph::graph::UnGraph; +use rustworkx_core::planar::{is_planar_for_layout, LRState}; + +#[test] +fn test_simple_planar_graph() { + let graph = UnGraph::<(), ()>::from_edges(&[ + (1, 2), + (2, 3), + (3, 4), + (4, 6), + (6, 7), + (7, 1), + (1, 5), + (5, 2), + (2, 4), + (4, 5), + (5, 7), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert!(res) +} + +#[test] +fn test_planar_grid_3_3_graph() { + let graph = UnGraph::<(), ()>::from_edges(&[ + // row edges + (0, 1), + (1, 2), + (3, 4), + (4, 5), + (6, 7), + (7, 8), + // col edges + (0, 3), + (3, 6), + (1, 4), + (4, 7), + (2, 5), + (5, 8), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert!(res) +} + +#[test] +fn test_planar_with_self_loop() { + let graph = UnGraph::<(), ()>::from_edges(&[ + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (1, 2), + (1, 3), + (1, 5), + (2, 5), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert!(res) +} + +#[test] +fn test_goldner_harary_planar_graph() { + // test goldner-harary graph (a maximal planar graph) + let graph = UnGraph::<(), ()>::from_edges(&[ + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (1, 7), + (1, 8), + (1, 10), + (1, 11), + (2, 3), + (2, 4), + (2, 6), + (2, 7), + (2, 9), + (2, 10), + (2, 11), + (3, 4), + (4, 5), + (4, 6), + (4, 7), + (5, 7), + (6, 7), + (7, 8), + (7, 9), + (7, 10), + (8, 10), + (9, 10), + (10, 11), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert!(res) +} + +#[test] +fn test_multiple_components_planar_graph() { + let graph = UnGraph::<(), ()>::from_edges(&[(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert!(res) +} + +#[test] +fn test_planar_multi_graph() { + let graph = UnGraph::<(), ()>::from_edges(&[(0, 1), (0, 1), (0, 1), (1, 2), (2, 0)]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert!(res) +} + +#[test] +fn test_k3_3_non_planar() { + let graph = UnGraph::<(), ()>::from_edges(&[ + (0, 3), + (0, 4), + (0, 5), + (1, 3), + (1, 4), + (1, 5), + (2, 3), + (2, 4), + (2, 5), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert_eq!(res, false) +} + +#[test] +fn test_k5_non_planar() { + let graph = UnGraph::<(), ()>::from_edges(&[ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (3, 4), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert_eq!(res, false) +} + +#[test] +fn test_multiple_components_non_planar() { + let graph = UnGraph::<(), ()>::from_edges(&[ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (3, 4), + (6, 7), + (7, 8), + (8, 6), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert_eq!(res, false) +} + +#[test] +fn test_non_planar() { + // tests a graph that has no subgraph directly isomorphic to K5 or K3_3. + let graph = UnGraph::<(), ()>::from_edges(&[ + (1, 5), + (1, 6), + (1, 7), + (2, 6), + (2, 3), + (3, 5), + (3, 7), + (4, 5), + (4, 6), + (4, 7), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert_eq!(res, false) +} + +#[test] +fn test_planar_graph1() { + let graph = UnGraph::<(), ()>::from_edges(&[ + (3, 10), + (2, 13), + (1, 13), + (7, 11), + (0, 8), + (8, 13), + (0, 2), + (0, 7), + (0, 10), + (1, 7), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert!(res) +} + +#[test] +fn test_non_planar_graph2() { + let graph = UnGraph::<(), ()>::from_edges(&[ + (1, 2), + (4, 13), + (0, 13), + (4, 5), + (7, 10), + (1, 7), + (0, 3), + (2, 6), + (5, 6), + (7, 13), + (4, 8), + (0, 8), + (0, 9), + (2, 13), + (6, 7), + (3, 6), + (2, 8), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert_eq!(res, false) +} + +#[test] +fn test_non_planar_graph3() { + let graph = UnGraph::<(), ()>::from_edges(&[ + (0, 7), + (3, 11), + (3, 4), + (8, 9), + (4, 11), + (1, 7), + (1, 13), + (1, 11), + (3, 5), + (5, 7), + (1, 3), + (0, 4), + (5, 11), + (5, 13), + ]); + let mut lr_state = LRState::new(&graph); + let res = is_planar_for_layout(&graph, Some(&mut lr_state)); + assert_eq!(res, false) +} diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index 2943017fc..6ba4d8ebf 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -882,6 +882,21 @@ def random_layout(graph, center=None, seed=None): raise TypeError("Invalid Input Type %s for graph" % type(graph)) +@_rustworkx_dispatch +def planar_layout(graph, scale=None, center=None): + """Generate a planar layout + + :param PyGraph graph: The graph to generate the layout for + :param tuple center: An optional center position. This is a 2 tuple of two + ``float`` values for the center position + :param int seed: An optional seed to set for the random number generator. + + :returns: The planar layout of the graph. + :rtype: Pos2DMapping + """ + raise TypeError("Invalid Input Type %s for graph" % type(graph)) + + @_rustworkx_dispatch def spring_layout( graph, @@ -1817,7 +1832,7 @@ def node_link_json(graph, path=None, graph_attrs=None, node_attrs=None, edge_att """Generate a JSON object representing a graph in a node-link format :param graph: The graph to generate the JSON for. Can either be a - :class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`. + :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`. :param str path: An optional path to write the JSON output to. If specified the function will not return anything and instead will write the JSON to the file specified. diff --git a/src/layout/embedding.rs b/src/layout/embedding.rs new file mode 100644 index 000000000..826ccf027 --- /dev/null +++ b/src/layout/embedding.rs @@ -0,0 +1,732 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +use hashbrown::{HashMap, HashSet}; +use indexmap::{IndexMap, IndexSet}; +use petgraph::prelude::*; +use petgraph::visit::NodeIndexable; +use petgraph::Directed; +use rayon::prelude::*; // For par_sort +use std::fmt::Debug; + +use crate::StablePyGraph; +use rustworkx_core::connectivity::connected_components; +use rustworkx_core::planar::lr_planar::{LRState, Sign}; + +pub type Point = [f64; 2]; + +#[derive(Debug)] +pub struct CwCcw { + cw: Option, + ccw: Option, +} + +impl Default for CwCcw { + fn default() -> Self { + CwCcw { + cw: None, + ccw: None, + } + } +} + +#[allow(dead_code)] +impl CwCcw { + fn new(cw: T, ccw: T) -> Self { + CwCcw { + cw: Some(cw), + ccw: Some(ccw), + } + } +} + +#[derive(Debug)] +pub struct FirstNbr { + pub first_nbr: Option, +} + +impl Default for FirstNbr { + fn default() -> Self { + FirstNbr { first_nbr: None } + } +} + +#[allow(dead_code)] +impl FirstNbr { + fn new(first_nbr: T) -> Self { + FirstNbr { + first_nbr: Some(first_nbr), + } + } +} + +/// The basic embedding to build the structure that will lead to +/// the position coordinates to display. +pub struct PlanarEmbedding { + pub embedding: StableGraph, CwCcw, Directed>, +} + +impl Default for PlanarEmbedding { + fn default() -> Self { + PlanarEmbedding { + embedding: StableGraph::, CwCcw, Directed>::new(), + } + } +} +impl PlanarEmbedding { + pub fn new() -> Self { + PlanarEmbedding { + embedding: StableGraph::, CwCcw, Directed>::new(), + } + } + + pub fn neighbors_cw_order(&mut self, v: NodeIndex) -> Vec { + let mut nbrs: Vec = vec![]; + let first_nbr = self.embedding[v].first_nbr; + + if first_nbr.is_none() { + // v has no neighbors + return nbrs; + } + let start_node = first_nbr.unwrap(); + nbrs.push(start_node); + + let mut current_node = self.get_edge_weight(v, start_node, true).unwrap(); + + while start_node != current_node { + nbrs.push(current_node); + current_node = self.get_edge_weight(v, current_node, true).unwrap(); + } + nbrs + } + + fn add_half_edge_cw( + &mut self, + start_node: NodeIndex, + end_node: NodeIndex, + ref_nbr: Option, + ) { + let cw_weight = CwCcw::::default(); + self.embedding.add_edge(start_node, end_node, cw_weight); + + if let Some(ref_nbr_node) = ref_nbr { + let cw_ref = self + .get_edge_weight(start_node, ref_nbr_node, true) + .unwrap(); + // Alter half-edge data structures + self.update_edge_weight(start_node, ref_nbr_node, end_node, true); + self.update_edge_weight(start_node, end_node, cw_ref, true); + self.update_edge_weight(start_node, cw_ref, end_node, false); + self.update_edge_weight(start_node, end_node, ref_nbr_node, false); + } else { + // The start node has no neighbors + self.update_edge_weight(start_node, end_node, end_node, true); + self.update_edge_weight(start_node, end_node, end_node, false); + self.embedding[start_node].first_nbr = Some(end_node); + } + } + + fn add_half_edge_ccw( + &mut self, + start_node: NodeIndex, + end_node: NodeIndex, + ref_nbr: Option, + ) { + if let Some(ref_nbr_node) = ref_nbr { + let ccw_ref_node = self.get_edge_weight(start_node, ref_nbr_node, false); + self.add_half_edge_cw(start_node, end_node, ccw_ref_node); + if ref_nbr == self.embedding[start_node].first_nbr { + // Update first neighbor + self.embedding[start_node].first_nbr = Some(end_node); + } + } else { + // Start node has no neighbors + let cw_weight = CwCcw::::default(); + self.embedding.add_edge(start_node, end_node, cw_weight); + + self.update_edge_weight(start_node, end_node, end_node, true); + self.update_edge_weight(start_node, end_node, end_node, false); + self.embedding[start_node].first_nbr = Some(end_node); + } + } + + fn add_half_edge_first(&mut self, start_node: NodeIndex, end_node: NodeIndex) { + // Add half edge that's first_nbr or None + let ref_node: Option = if self.embedding.node_bound() >= start_node.index() + && self.embedding[start_node].first_nbr.is_some() + { + self.embedding[start_node].first_nbr + } else { + None + }; + self.add_half_edge_ccw(start_node, end_node, ref_node); + } + + fn next_face_half_edge(&mut self, v: NodeIndex, w: NodeIndex) -> (NodeIndex, NodeIndex) { + let new_node = self.get_edge_weight(w, v, false); + (w, new_node.unwrap()) + } + + fn update_edge_weight(&mut self, v: NodeIndex, w: NodeIndex, new_node: NodeIndex, cw: bool) { + let found_edge = self.embedding.find_edge(v, w); + let found_weight = self.embedding.edge_weight_mut(found_edge.unwrap()); + if cw { + found_weight.unwrap().cw = Some(new_node); + } else { + found_weight.unwrap().ccw = Some(new_node); + } + } + + fn get_edge_weight(&self, v: NodeIndex, w: NodeIndex, cw: bool) -> Option { + let found_edge = self.embedding.find_edge(v, w)?; + let found_weight = self.embedding.edge_weight(found_edge)?; + if cw { + found_weight.cw + } else { + found_weight.ccw + } + } + + fn connect_components(&mut self, v: NodeIndex, w: NodeIndex) { + // If multiple connected_components, connect them + self.add_half_edge_first(v, w); + self.add_half_edge_first(w, v); + } +} + +/// Use the LRState data from is_planar to build an embedding. +pub fn create_embedding( + planar_emb: &mut PlanarEmbedding, + lr_state: &mut LRState<&StablePyGraph>, +) { + let mut ordered_adjs: IndexMap> = + IndexMap::with_capacity(lr_state.graph.node_count()); + + add_nodes_to_embedding(planar_emb, &lr_state.dir_graph); + + // Create the adjacency list for each node + for v in lr_state.dir_graph.node_indices() { + ordered_adjs.insert(v, lr_state.dir_graph.edges(v).map(|e| e.target()).collect()); + } + for v in lr_state.dir_graph.node_indices() { + // Change the sign for nesting_depth + for e in lr_state.dir_graph.edges(v) { + let edge: (NodeIndex, NodeIndex) = (e.source(), e.target()); + let signed_depth: isize = lr_state.nesting_depth[&edge] as isize; + let signed_side = if sign(edge, &mut lr_state.eref, &mut lr_state.side) == Sign::Minus { + -1 + } else { + 1 + }; + lr_state + .nesting_depth + .insert(edge, signed_depth * signed_side); + } + } + // Sort the adjacency list using revised nesting depth as sort order + for (v, adjs) in ordered_adjs.iter_mut() { + adjs.par_sort_by_key(|x| lr_state.nesting_depth[&(*v, *x)]); + } + // Add the initial half edge cw to the embedding using the ordered adjacency list + for v in lr_state.dir_graph.node_indices() { + let mut prev_node: Option = None; + for w in ordered_adjs.get(&v).unwrap().iter() { + planar_emb.add_half_edge_cw(v, *w, prev_node); + prev_node = Some(*w) + } + } + // Start the DFS traversal for the embedding + let mut left_ref: HashMap = HashMap::with_capacity(ordered_adjs.len()); + let mut right_ref: HashMap = HashMap::with_capacity(ordered_adjs.len()); + let mut idx: Vec = vec![0; lr_state.graph.node_bound()]; + + for v in lr_state.roots.iter() { + // Create the stack with an initial entry of v + let mut dfs_stack: Vec = vec![*v]; + + while !dfs_stack.is_empty() { + let v = dfs_stack.pop().unwrap(); + let idx2 = idx[v.index()]; + + // Iterate over the ordered_adjs starting at the saved index until the end + for w in ordered_adjs.get(&v).unwrap()[idx2..].iter() { + idx[v.index()] += 1; + let ei = (v, *w); + if lr_state.eparent.contains_key(w) && ei == lr_state.eparent[w] { + planar_emb.add_half_edge_first(*w, v); + left_ref.insert(v, *w); + right_ref.insert(v, *w); + dfs_stack.push(v); + dfs_stack.push(*w); + break; + } else if !lr_state.side.contains_key(&ei) || lr_state.side[&ei] == Sign::Plus { + planar_emb.add_half_edge_cw(*w, v, Some(right_ref[w])); + } else { + planar_emb.add_half_edge_ccw(*w, v, Some(left_ref[w])); + left_ref.insert(*w, v); + } + } + } + } + + fn add_nodes_to_embedding( + planar_emb: &mut PlanarEmbedding, + dir_graph: &StableGraph<(), (), Directed>, + ) { + let mut tmp_nodes: Vec = Vec::new(); + let mut count: usize = 0; + for _ in 0..dir_graph.node_bound() { + let first_nbr = FirstNbr::::default(); + planar_emb.embedding.add_node(first_nbr); + } + for gnode in dir_graph.node_indices() { + let gidx = gnode.index(); + if gidx != count { + for idx in count..gidx { + tmp_nodes.push(idx); + } + count = gidx; + } + count += 1; + } + for tmp_node in tmp_nodes { + planar_emb.embedding.remove_node(NodeIndex::new(tmp_node)); + } + } + + fn sign( + edge: (NodeIndex, NodeIndex), + eref: &mut HashMap<(NodeIndex, NodeIndex), (NodeIndex, NodeIndex)>, + side: &mut HashMap<(NodeIndex, NodeIndex), Sign>, + ) -> Sign { + // Resolve the relative side of an edge to the absolute side. + + // Create a temp of Plus in case edge not in side. + let temp_side: Sign = if side.contains_key(&edge) { + side[&edge] + } else { + Sign::Plus + }; + if eref.contains_key(&edge) { + if temp_side == sign(eref[&edge], eref, side) { + side.insert(edge, Sign::Plus); + } else { + side.insert(edge, Sign::Minus); + } + eref.remove(&edge); + } + if side.contains_key(&edge) { + side[&edge] + } else { + Sign::Plus + } + } +} + +/// Once the embedding has been created, triangulate the embedding, +/// create a canonical ordering, and convert the embedding to position +/// coordinates. +pub fn embedding_to_pos(planar_emb: &mut PlanarEmbedding) -> Vec { + let mut pos: Vec = vec![[0.0, 0.0]; planar_emb.embedding.node_bound()]; + if planar_emb.embedding.node_bound() < 4 { + return [[0.0, 0.0], [2.0, 0.0], [1.0, 1.0]].to_vec(); + } + let outer_face = triangulate_embedding(planar_emb, false); + + let node_list = canonical_ordering(planar_emb, outer_face); + + let mut right_t_child = HashMap::, Option>::new(); + let mut left_t_child = HashMap::, Option>::new(); + let mut delta_x = HashMap::, isize>::new(); + let mut y_coord = HashMap::, isize>::new(); + + // Set the coordinates for the first 3 nodes. + let v1 = node_list[0].0; + let v2 = node_list[1].0; + let v3 = node_list[2].0; + + delta_x.insert(v1, 0); + y_coord.insert(v1, 0); + right_t_child.insert(v1, v3); + left_t_child.insert(v1, None); + + delta_x.insert(v2, 1); + y_coord.insert(v2, 0); + right_t_child.insert(v2, None); + left_t_child.insert(v2, None); + + delta_x.insert(v3, 1); + y_coord.insert(v3, 1); + right_t_child.insert(v3, v2); + left_t_child.insert(v3, None); + + // Set coordinates for the remaining nodes, adjusting + // positions along the way as needed. + for ordering in node_list.iter().skip(3) { + let vk = ordering.0; + let contour_nbrs = &ordering.1; + + let wp = contour_nbrs[0]; + let wp1 = contour_nbrs[1]; + let wq = contour_nbrs[contour_nbrs.len() - 1]; + let wq1 = contour_nbrs[contour_nbrs.len() - 2]; + + let adds_mult_tri = contour_nbrs.len() > 2; + + let mut delta_wp1_plus = 1; + if delta_x.contains_key(&wp1) { + delta_wp1_plus = delta_x[&wp1] + 1; + } + delta_x.insert(wp1, delta_wp1_plus); + + let mut delta_wq_plus = 1; + if delta_x.contains_key(&wq) { + delta_wq_plus = delta_x[&wq] + 1; + } + delta_x.insert(wq, delta_wq_plus); + + let delta_x_wp_wq = contour_nbrs[1..].iter().map(|x| delta_x[x]).sum::(); + + let y_wp = y_coord[&wp]; + let y_wq = y_coord[&wq]; + delta_x.insert(vk, (delta_x_wp_wq - y_wp + y_wq) / 2_isize); + y_coord.insert(vk, (delta_x_wp_wq + y_wp + y_wq) / 2_isize); + delta_x.insert(wq, delta_x_wp_wq - delta_x[&vk]); + + if adds_mult_tri { + delta_x.insert(wp1, delta_x[&wp1] - delta_x[&vk]); + } + right_t_child.insert(wp, vk); + right_t_child.insert(vk, wq); + if adds_mult_tri { + left_t_child.insert(vk, wp1); + right_t_child.insert(wq1, None); + } else { + left_t_child.insert(vk, None); + } + } + + // Set the position of the next tree child. + fn set_position( + parent: Option, + tree: &HashMap, Option>, + remaining_nodes: &mut Vec>, + delta_x: &HashMap, isize>, + y_coord: &HashMap, isize>, + pos: &mut [Point], + ) { + let child = tree[&parent]; + let parent_node_x = pos[parent.unwrap().index()][0]; + + if let Some(child_un) = child { + let child_x = parent_node_x + (delta_x[&child] as f64); + pos[child_un.index()] = [child_x, (y_coord[&child] as f64)]; + remaining_nodes.push(child); + } + } + pos[v1.unwrap().index()] = [0.0, y_coord[&v1] as f64]; + let mut remaining_nodes = vec![v1]; + + // Set the positions of all the nodes. + while !remaining_nodes.is_empty() { + let parent_node = remaining_nodes.pop().unwrap(); + set_position( + parent_node, + &left_t_child, + &mut remaining_nodes, + &delta_x, + &y_coord, + &mut pos, + ); + set_position( + parent_node, + &right_t_child, + &mut remaining_nodes, + &delta_x, + &y_coord, + &mut pos, + ); + } + pos +} + +fn triangulate_embedding( + planar_emb: &mut PlanarEmbedding, + fully_triangulate: bool, +) -> Vec { + let component_sets = connected_components(&planar_emb.embedding); + for i in 0..(component_sets.len() - 1) { + let v1 = component_sets[i].iter().min().unwrap(); + let v2 = component_sets[i + 1].iter().min().unwrap(); + planar_emb.connect_components(*v1, *v2); + } + let mut outer_face = vec![]; + let mut face_list = vec![]; + let mut edges_counted: HashSet<(NodeIndex, NodeIndex)> = HashSet::new(); + + let indices: Vec = planar_emb.embedding.node_indices().collect(); + for v in indices { + for w in planar_emb.neighbors_cw_order(v) { + let new_face = make_bi_connected(planar_emb, &v, &w, &mut edges_counted); + if !new_face.is_empty() { + face_list.push(new_face.clone()); + if new_face.len() > outer_face.len() { + outer_face = new_face; + } + } + } + } + for face in face_list { + if face != outer_face || fully_triangulate { + triangulate_face(planar_emb, face[0], face[1]); + } + } + if fully_triangulate { + let v1 = outer_face[0]; + let v2 = outer_face[1]; + let v3 = planar_emb.get_edge_weight(v2, v1, false); + outer_face = vec![v1, v2, v3.unwrap()]; + } + outer_face +} + +fn make_bi_connected( + planar_emb: &mut PlanarEmbedding, + start_node: &NodeIndex, + out_node: &NodeIndex, + edges_counted: &mut HashSet<(NodeIndex, NodeIndex)>, +) -> Vec { + // If edge already counted return + if edges_counted.contains(&(*start_node, *out_node)) { + return vec![]; + } + edges_counted.insert((*start_node, *out_node)); + let mut v1 = *start_node; + let mut v2 = *out_node; + let mut face_list: Vec = vec![*start_node]; + let (_, mut v3) = planar_emb.next_face_half_edge(v1, v2); + + while v2 != *start_node || v3 != *out_node { + if face_list.contains(&v2) { + planar_emb.add_half_edge_cw(v1, v3, Some(v2)); + planar_emb.add_half_edge_ccw(v3, v1, Some(v2)); + edges_counted.insert((v2, v3)); + edges_counted.insert((v3, v1)); + v2 = v1; + } else { + face_list.push(v2); + } + v1 = v2; + let edge = planar_emb.next_face_half_edge(v2, v3); + v2 = edge.0; + v3 = edge.1; + edges_counted.insert((v1, v2)); + } + face_list +} + +fn triangulate_face(planar_emb: &mut PlanarEmbedding, mut v1: NodeIndex, mut v2: NodeIndex) { + let (_, mut v3) = planar_emb.next_face_half_edge(v1, v2); + let (_, mut v4) = planar_emb.next_face_half_edge(v2, v3); + if v1 == v2 || v1 == v3 { + return; + } + while v1 != v4 { + if planar_emb.embedding.contains_edge(v1, v3) { + v1 = v2; + v2 = v3; + v3 = v4; + } else { + planar_emb.add_half_edge_cw(v1, v3, Some(v2)); + planar_emb.add_half_edge_ccw(v3, v1, Some(v2)); + v2 = v3; + v3 = v4; + } + let edge = planar_emb.next_face_half_edge(v2, v3); + v4 = edge.1; + } +} + +fn canonical_ordering( + planar_emb: &mut PlanarEmbedding, + outer_face: Vec, +) -> Vec<(Option, Vec>)> { + let v1 = outer_face[0]; + let v2 = outer_face[1]; + let mut chords: HashMap = HashMap::new(); + let mut marked_nodes: HashSet = HashSet::new(); + + let mut ready_to_pick = outer_face.iter().cloned().collect::>(); + ready_to_pick.par_sort(); + + let mut outer_face_cw_nbr: HashMap = + HashMap::with_capacity(outer_face.len()); + let mut outer_face_ccw_nbr: HashMap = + HashMap::with_capacity(outer_face.len()); + + let mut prev_nbr = v2; + for v in outer_face[2..outer_face.len()].iter() { + outer_face_ccw_nbr.insert(prev_nbr, *v); + prev_nbr = *v; + } + outer_face_ccw_nbr.insert(prev_nbr, v1); + + prev_nbr = v1; + for v in outer_face[1..outer_face.len()].iter().rev() { + outer_face_cw_nbr.insert(prev_nbr, *v); + prev_nbr = *v; + } + + fn is_outer_face_nbr( + x: NodeIndex, + y: NodeIndex, + outer_face_cw_nbr: &HashMap, + outer_face_ccw_nbr: &HashMap, + ) -> bool { + if !outer_face_ccw_nbr.contains_key(&x) { + return outer_face_cw_nbr[&x] == y; + } + if !outer_face_cw_nbr.contains_key(&x) { + return outer_face_ccw_nbr[&x] == y; + } + outer_face_cw_nbr[&x] == y || outer_face_ccw_nbr[&x] == y + } + + fn is_on_outer_face( + x: NodeIndex, + v1: NodeIndex, + marked_nodes: &HashSet, + outer_face_ccw_nbr: &HashMap, + ) -> bool { + !marked_nodes.contains(&x) && (outer_face_ccw_nbr.contains_key(&x) || x == v1) + } + + for v in outer_face { + for nbr in planar_emb.neighbors_cw_order(v) { + if is_on_outer_face(nbr, v1, &marked_nodes, &outer_face_ccw_nbr) + && !is_outer_face_nbr(v, nbr, &outer_face_cw_nbr, &outer_face_ccw_nbr) + { + let mut chords_plus = 0; + if chords.contains_key(&v) { + chords_plus = chords[&v]; + } + chords.insert(v, chords_plus + 1); + ready_to_pick.shift_remove(&v); + } + } + } + + let mut canon_order: Vec<(Option, Vec>)> = + vec![(None, vec![]); planar_emb.embedding.node_count()]; + + canon_order[0] = (Some(v1), vec![]); + canon_order[1] = (Some(v2), vec![]); + ready_to_pick.shift_remove(&v1); + ready_to_pick.shift_remove(&v2); + + for k in (2..(planar_emb.embedding.node_count())).rev() { + let v = ready_to_pick[0]; + ready_to_pick.shift_remove(&v); + marked_nodes.insert(v); + + let mut wp: Option = None; + let mut wq: Option = None; + for nbr in planar_emb.neighbors_cw_order(v).iter() { + if marked_nodes.contains(nbr) { + continue; + } + if is_on_outer_face(*nbr, v1, &marked_nodes, &outer_face_ccw_nbr) { + if *nbr == v1 { + wp = Some(v1); + } else if *nbr == v2 { + wq = Some(v2); + } else if outer_face_cw_nbr[nbr] == v { + wp = Some(*nbr); + } else { + wq = Some(*nbr); + } + } + if wp.is_some() && wq.is_some() { + break; + } + } + let mut wp_wq = vec![]; + if let Some(wp_un) = wp { + if let Some(wq_un) = wq { + wp_wq = vec![wp]; + let mut nbr = wp.unwrap(); + while Some(nbr) != wq { + let next_nbr = planar_emb.get_edge_weight(v, nbr, false).unwrap(); + wp_wq.push(Some(next_nbr)); + outer_face_cw_nbr.insert(nbr, next_nbr); + outer_face_ccw_nbr.insert(next_nbr, nbr); + nbr = next_nbr; + } + if wp_wq.len() == 2 { + if chords.contains_key(&wp_un) { + let chords_wp = chords[&wp_un] - 1; + chords.insert(wp_un, chords_wp); + if chords[&wp_un] == 0 { + ready_to_pick.insert(wp_un); + } + } + if chords.contains_key(&wq_un) { + let chords_wq = chords[&wq_un] - 1; + chords.insert(wq_un, chords_wq); + if chords[&wq_un] == 0 { + ready_to_pick.insert(wq_un); + } + } + } else { + let mut new_face_nodes: IndexSet = IndexSet::new(); + if wp_wq.len() > 1 { + for w in &wp_wq[1..(wp_wq.len() - 1)] { + let w_un = w.unwrap(); + new_face_nodes.insert(w_un); + } + for w in &new_face_nodes { + let w_un = *w; + ready_to_pick.insert(w_un); + for nbr in planar_emb.neighbors_cw_order(w_un) { + if is_on_outer_face(nbr, v1, &marked_nodes, &outer_face_ccw_nbr) + && !is_outer_face_nbr( + w_un, + nbr, + &outer_face_cw_nbr, + &outer_face_ccw_nbr, + ) + { + let mut chords_w_plus = 1; + if chords.contains_key(&w_un) { + chords_w_plus = chords[&w_un] + 1; + } + chords.insert(w_un, chords_w_plus); + ready_to_pick.shift_remove(&w_un); + if !new_face_nodes.contains(&nbr) { + let mut chords_plus = 1; + if chords.contains_key(&nbr) { + chords_plus = chords[&nbr] + 1 + } + chords.insert(nbr, chords_plus); + ready_to_pick.shift_remove(&nbr); + } + } + } + } + } + } + } + } + canon_order[k] = (Some(v), wp_wq); + } + canon_order +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 72c4155e8..1b815477b 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -12,6 +12,8 @@ mod bipartite; mod circular; +mod embedding; +mod planar; mod random; mod shell; mod spiral; @@ -195,6 +197,33 @@ pub fn digraph_spring_layout( ) } +/// Generate a planar layout +/// +/// The algorithm first uses Ulrik Brandes: The Left-Right Planarity Test 2009, +/// http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.217.9208 +/// to determine if the graph is planar. If so, then a planar embedding is created +/// and the drawing is created using M. Chrobak and T.H. Payne: A Linear-time Algorithm +/// for Drawing a Planar Graph on a Grid 1989, +/// http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677. +/// +/// :param PyGraph graph: The graph to be used +/// :param float|None scale: Scale factor for positions. If scale is ``None``, +/// no re-scaling is performed. (``default=1.0``) +/// :param tuple|None center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// +/// :returns: A dictionary of positions keyed by node id. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, / scale=1.0, center=None)")] +pub fn graph_planar_layout( + graph: &graph::PyGraph, + scale: Option, + center: Option<[f64; 2]>, +) -> PyResult { + planar::planar_layout(&graph.graph, scale, center) +} + /// Generate a random layout /// /// :param PyGraph graph: The graph to generate the layout for diff --git a/src/layout/planar.rs b/src/layout/planar.rs new file mode 100644 index 000000000..457136e04 --- /dev/null +++ b/src/layout/planar.rs @@ -0,0 +1,75 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +use petgraph::prelude::*; +use petgraph::visit::NodeIndexable; +use pyo3::prelude::*; + +use super::super::GraphNotPlanar; +use super::spring::{recenter, rescale, Point}; +use crate::iterators::Pos2DMapping; +use crate::layout::embedding::{create_embedding, embedding_to_pos, PlanarEmbedding}; +use crate::StablePyGraph; +use rustworkx_core::dictmap::*; +use rustworkx_core::planar::{is_planar_for_layout, LRState}; + +/// If a graph is planar, create a set of position coordinates for a planar +/// layout that can be passed to a drawer. +pub fn planar_layout( + graph: &StablePyGraph, + scale: Option, + center: Option, +) -> PyResult { + let node_num = graph.node_bound(); + if node_num == 0 { + return Ok(Pos2DMapping { + pos_map: DictMap::new(), + }); + } + + // First determine if the graph is planar. + let mut lr_state = LRState::new(graph); + if !is_planar_for_layout(graph, Some(&mut lr_state)) { + Err(GraphNotPlanar::new_err("The input graph is not planar.")) + + // If planar, create the position coordinates. + } else { + let mut planar_emb = PlanarEmbedding::new(); + planar_emb.embedding = StableGraph::with_capacity(node_num, 0); + + // First create the graph embedding + create_embedding(&mut planar_emb, &mut lr_state); + + // Then convert the embedding to position coordinates. + let mut pos = embedding_to_pos(&mut planar_emb); + + if let Some(scale) = scale { + rescale( + &mut pos, + scale, + graph.node_indices().map(|n| n.index()).collect(), + ); + } + if let Some(center) = center { + recenter(&mut pos, center); + } + Ok(Pos2DMapping { + pos_map: graph + .node_indices() + .map(|n| { + let n = n.index(); + (n, pos[n]) + }) + .collect(), + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index cce7c9175..d7674bcf2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -378,6 +378,8 @@ create_exception!(rustworkx, JSONSerializationError, PyException); create_exception!(rustworkx, NegativeCycle, PyException); // Failed to Converge on a solution create_exception!(rustworkx, FailedToConverge, PyException); +// Graph is not planar +create_exception!(rustworkx, GraphNotPlanar, PyException); // Graph is not bipartite create_exception!(rustworkx, GraphNotBipartite, PyException); @@ -408,6 +410,8 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { "GraphNotBipartite", py.get_type_bound::(), )?; + m.add("GraphNotPlanar", py.get_type_bound::())?; + m.add_wrapped(wrap_pyfunction!(bfs_successors))?; m.add_wrapped(wrap_pyfunction!(bfs_predecessors))?; m.add_wrapped(wrap_pyfunction!(graph_bfs_search))?; @@ -548,6 +552,7 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(digraph_core_number))?; m.add_wrapped(wrap_pyfunction!(graph_complement))?; m.add_wrapped(wrap_pyfunction!(digraph_complement))?; + m.add_wrapped(wrap_pyfunction!(graph_planar_layout))?; m.add_wrapped(wrap_pyfunction!(graph_random_layout))?; m.add_wrapped(wrap_pyfunction!(digraph_random_layout))?; m.add_wrapped(wrap_pyfunction!(graph_bipartite_layout))?; diff --git a/tests/graph/test_planar_layout.py b/tests/graph/test_planar_layout.py new file mode 100644 index 000000000..1360fedae --- /dev/null +++ b/tests/graph/test_planar_layout.py @@ -0,0 +1,62 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +import rustworkx + + +class TestPlanarLayout(unittest.TestCase): + def setUp(self): + self.graph = rustworkx.PyGraph() + node_a = self.graph.add_node(1) + node_b = self.graph.add_node(2) + self.graph.add_edge(node_a, node_b, 1) + node_c = self.graph.add_node(3) + self.graph.add_edge(node_a, node_c, 2) + + def test_empty_graph(self): + graph = rustworkx.PyGraph() + res = rustworkx.planar_layout(graph) + self.assertEqual({}, res) + + def test_simple_graph(self): + res = rustworkx.planar_layout(self.graph) + self.assertEqual(len(res), 3) + self.assertEqual(len(res[0]), 2) + self.assertIsInstance(res[0][0], float) + + def test_simple_graph_center(self): + res = rustworkx.planar_layout(self.graph, center=[0.5, 0.5]) + self.assertEqual(len(res), 3) + self.assertEqual(len(res[0]), 2) + self.assertIsInstance(res[0][0], float) + + def test_graph_with_removed_nodes(self): + graph = rustworkx.PyGraph() + nodes = graph.add_nodes_from([0, 1, 2]) + graph.remove_node(nodes[1]) + res = rustworkx.planar_layout(graph) + self.assertEqual(len(res), 2) + self.assertTrue(nodes[0] in res) + self.assertTrue(nodes[2] in res) + self.assertFalse(nodes[1] in res) + + def test_graph_with_more_removed_nodes(self): + graph = rustworkx.PyGraph() + nodes = graph.add_nodes_from([0, 1, 2, 3, 4, 5]) + graph.remove_node(nodes[3]) + res = rustworkx.planar_layout(graph) + self.assertEqual(len(res), 5) + self.assertTrue(nodes[0] in res) + self.assertTrue(nodes[4] in res) + self.assertFalse(nodes[3] in res) diff --git a/tests/rustworkx_tests/graph/test_planar_layout.py b/tests/rustworkx_tests/graph/test_planar_layout.py new file mode 100644 index 000000000..1360fedae --- /dev/null +++ b/tests/rustworkx_tests/graph/test_planar_layout.py @@ -0,0 +1,62 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +import rustworkx + + +class TestPlanarLayout(unittest.TestCase): + def setUp(self): + self.graph = rustworkx.PyGraph() + node_a = self.graph.add_node(1) + node_b = self.graph.add_node(2) + self.graph.add_edge(node_a, node_b, 1) + node_c = self.graph.add_node(3) + self.graph.add_edge(node_a, node_c, 2) + + def test_empty_graph(self): + graph = rustworkx.PyGraph() + res = rustworkx.planar_layout(graph) + self.assertEqual({}, res) + + def test_simple_graph(self): + res = rustworkx.planar_layout(self.graph) + self.assertEqual(len(res), 3) + self.assertEqual(len(res[0]), 2) + self.assertIsInstance(res[0][0], float) + + def test_simple_graph_center(self): + res = rustworkx.planar_layout(self.graph, center=[0.5, 0.5]) + self.assertEqual(len(res), 3) + self.assertEqual(len(res[0]), 2) + self.assertIsInstance(res[0][0], float) + + def test_graph_with_removed_nodes(self): + graph = rustworkx.PyGraph() + nodes = graph.add_nodes_from([0, 1, 2]) + graph.remove_node(nodes[1]) + res = rustworkx.planar_layout(graph) + self.assertEqual(len(res), 2) + self.assertTrue(nodes[0] in res) + self.assertTrue(nodes[2] in res) + self.assertFalse(nodes[1] in res) + + def test_graph_with_more_removed_nodes(self): + graph = rustworkx.PyGraph() + nodes = graph.add_nodes_from([0, 1, 2, 3, 4, 5]) + graph.remove_node(nodes[3]) + res = rustworkx.planar_layout(graph) + self.assertEqual(len(res), 5) + self.assertTrue(nodes[0] in res) + self.assertTrue(nodes[4] in res) + self.assertFalse(nodes[3] in res)