graphlang/
graphoperations.rs

1use log::{debug, trace};
2use rand::seq::IteratorRandom;
3use rand::seq::SliceRandom;
4use rand::thread_rng;
5
6use crate::primitives::{Index, Label};
7use crate::{Graph, Isomorphism, Node};
8// TODO: Graph::neigbours(&Node<I,NL>)
9// TODO: Isomorphism::can_be_safely_merged(&Isomorphism<I>)
10
11/// Tries to find an partial isomorphism from `subgraph` to `graph`. `subgraph` can be a
12/// disconnected graph. If `None` is returned if there is no mapping.
13#[must_use]
14pub fn match_subgraph<I: Index, NL: Label, EL: Label>(
15    graph: &Graph<I, NL, EL>,
16    subgraph: &Graph<I, NL, EL>,
17) -> Option<Isomorphism<I>> {
18    custom(graph, subgraph)
19}
20
21fn custom<I: Index, NL: Label, EL: Label>(
22    graph: &Graph<I, NL, EL>,
23    subgraph: &Graph<I, NL, EL>,
24) -> Option<Isomorphism<I>> {
25    // Random starting node from q_s
26    let q_s = subgraph.nodes.iter().choose(&mut thread_rng())?;
27    trace!("Starting matching with query node: {:?}", &q_s);
28    // For every possible start ...
29    for g_s in graph.nodes.iter().filter(|g| g.is_isomorph_to(q_s)) {
30        trace!("Starting matching with graph node: {:?}", &g_s);
31        let ret = custom_with_initial_nodes_2(graph, subgraph, q_s, g_s, &Isomorphism::default());
32        if ret.is_some() {
33            return ret;
34        }
35    }
36    debug!("No partial isomorphism found");
37    None
38}
39
40/// Combines two lists of nodes and returns all possible mappings, but neglect all nodes,
41/// that are alerdy mapped (i.e. that are in isomorphism).
42fn combine_neighbours<I: Index, NL: Label>(
43    graph: &[Node<I, NL>],
44    query: &[Node<I, NL>],
45    isomorphism: &Isomorphism<I>,
46) -> Isomorphism<I> {
47    let mut iso = Isomorphism::default();
48    for q in query {
49        for g in graph {
50            if q.is_isomorph_to(g)
51                && !isomorphism.contains_nodeid_on_lhs(&q.id)
52                && !isomorphism.contains_nodeid_on_rhs(&g.id)
53            {
54                iso.0.insert(q.id.clone(), g.id.clone());
55            }
56        }
57    }
58    iso
59}
60
61#[test]
62fn combine_neighbours_simple() {
63    let q = vec![Node {
64        id: 1u32,
65        label: "a",
66    }];
67    let g = vec![
68        Node {
69            id: 1u32,
70            label: "a",
71        },
72        Node { id: 3, label: "a" },
73    ];
74    let possible_matches = combine_neighbours(&g, &q, &Isomorphism::default());
75    let reference: Isomorphism<u32> = (&[(1, 1), (1, 3)]).into();
76    assert_eq!(reference, possible_matches);
77}
78
79#[test]
80fn combine_neighbours_with_filter() {
81    let g = vec![
82        Node {
83            id: 1u32,
84            label: "a",
85        },
86        Node { id: 3, label: "a" },
87    ];
88    let q = vec![Node {
89        id: 1u32,
90        label: "a",
91    }];
92
93    let possible_isomorphisms: Isomorphism<u32> = (&[(2, 0)]).into();
94
95    let possible_matches = combine_neighbours(&g, &q, &possible_isomorphisms);
96    let reference: Isomorphism<u32> = (&[(1, 1), (1, 3)]).into();
97    assert_eq!(reference, possible_matches);
98
99    let possible_matches = combine_neighbours(&q, &g, &possible_isomorphisms);
100    let reference: Isomorphism<u32> = (&[(1, 1), (3, 1)]).into();
101    assert_eq!(reference, possible_matches);
102}
103
104#[test]
105fn combine_neighbours_with_filter_2() {
106    let g: Vec<Node<u32, _>> = vec![
107        Node { id: 0, label: "a" },
108        Node { id: 2, label: "a" },
109        Node { id: 3, label: "a" },
110    ];
111    let q: Vec<Node<u32, _>> = vec![Node { id: 0, label: "a" }, Node { id: 2, label: "a" }];
112
113    let possible_isomorphisms: Isomorphism<u32> = (&[(1, 1)]).into();
114    let possible_matches = combine_neighbours(&g, &q, &possible_isomorphisms);
115
116    let reference: Isomorphism<u32> = (&[(0, 0), (0, 2), (0, 3), (2, 0), (2, 2), (2, 3)]).into();
117    assert_eq!(reference, possible_matches);
118}
119
120/// For every mapping in the isomorphism the possible matches are explored
121fn custom_explore_all_options_2<I: Index, NL: Label, EL: Label>(
122    graph: &Graph<I, NL, EL>,
123    subgraph: &Graph<I, NL, EL>,
124    isomorphism: &Isomorphism<I>,
125) -> Option<Isomorphism<I>> {
126    //let (q_s, g_s) = possibilities.iter().choose(&mut thread_rng())?;
127    let mut keys = isomorphism.0.keys().collect::<Vec<_>>();
128    keys.shuffle(&mut thread_rng());
129
130    let mut number_of_hopless_nodes = 0;
131    for q in &keys {
132        let q = *q;
133        let g = isomorphism.0.get(q).unwrap();
134        let g_potentials = graph.neighbours(g);
135        let q_potentials = subgraph.neighbours(q);
136        trace!("");
137        trace!("     Used mapping {:?}", (&q, &g));
138        trace!("      g neigbours {:?}", &g_potentials);
139        trace!("      q neigbours {:?}", &q_potentials);
140        trace!("limiting mappings {:?}", &isomorphism);
141        let possible_matches = combine_neighbours(&g_potentials, &q_potentials, isomorphism);
142        trace!(" possible_matches {:?}", &possible_matches);
143        for (q_s, g_s) in &possible_matches.0 {
144            let q_s = subgraph.get_node_by_id(q_s)?;
145            let g_s = graph.get_node_by_id(g_s)?;
146            let ret = custom_with_initial_nodes_2(graph, subgraph, q_s, g_s, isomorphism);
147            if ret.is_some() {
148                return ret;
149            }
150        }
151        // It could also be a weird start
152        if possible_matches.0.is_empty() {
153            number_of_hopless_nodes += 1;
154        }
155    }
156    if number_of_hopless_nodes != isomorphism.0.len() {
157        return None;
158    }
159    // The query graph must be disconnected!
160
161    // No connection possible -> Has to be disconnected ...
162    let nodes_to_remove = isomorphism.0.keys().cloned().collect::<Vec<_>>();
163    let reduced_query: Graph<I, NL, EL> = subgraph - &nodes_to_remove[..];
164    if reduced_query.nodes.is_empty() {
165        // ... or there is no isomorphism.
166        trace!("This doesn't lead to anything ... we should go back.");
167        return None;
168    }
169    trace!("Match found for connected subgraph, but the following graph'island' is still unmapped:\n{:?}", &reduced_query);
170    // Find a new random start:
171    let q_s = reduced_query.nodes.iter().choose(&mut thread_rng())?;
172    let mut isomorphism = isomorphism.clone();
173    for g_s in graph.nodes.iter().filter(|g| g.is_isomorph_to(q_s)) {
174        let iso = Isomorphism::default();
175        let ret = custom_with_initial_nodes_2(graph, &reduced_query, q_s, g_s, &iso)?;
176        if isomorphism.can_be_safely_merged(&ret) {
177            isomorphism.0.extend(ret.0);
178            return Some(isomorphism.clone());
179        }
180    }
181    // Still doesnt work. We are out of luck.
182    trace!("Disconnected islands don't match. Back we go ...");
183    None
184}
185
186fn custom_with_initial_nodes_2<I: Index, NL: Label, EL: Label>(
187    graph: &Graph<I, NL, EL>,
188    subgraph: &Graph<I, NL, EL>,
189    q_s: &Node<I, NL>,
190    g_s: &Node<I, NL>,
191    isomorphism: &Isomorphism<I>,
192) -> Option<Isomorphism<I>> {
193    let mut isomorphism = isomorphism.clone();
194    isomorphism.0.insert(q_s.id.clone(), g_s.id.clone());
195    trace!("The current isomorphism reads       : {:?}", &isomorphism);
196    if isomorphism.0.len() == subgraph.nodes.len() {
197        debug!("(Sub-)Match found!");
198        return Some(isomorphism);
199    }
200
201    custom_explore_all_options_2(graph, subgraph, &isomorphism)
202}
203
204/// Checks if the two given graphs are (totally) isomorphic to each other, by checking if there
205/// exists a partial isomorphism from lhs to rhs and in the other direcetion. This function is
206/// *very* expensive.
207///
208/// # Examples
209///
210/// ```
211/// use graphlang::{Node,Edge,Graph,are_graphs_isomorph};
212/// let graph_a = Graph {
213///         nodes: vec![ Node::new(0u32, "a"), Node::new(1, "a") ],
214///         edges: vec![ Edge::new_unlabeled(0, 1), ] };
215/// let graph_b = Graph {
216///         nodes: vec![ Node::new(10u32, "a"), Node::new(11, "a") ],
217///         edges: vec![ Edge::new_unlabeled(10, 11), ] };
218/// assert!(are_graphs_isomorph(&graph_a, &graph_b));
219/// ```
220#[must_use]
221pub fn are_graphs_isomorph<I: Index, NL: Label, EL: Label>(
222    lhs: &Graph<I, NL, EL>,
223    rhs: &Graph<I, NL, EL>,
224) -> bool {
225    crate::match_subgraph(lhs, rhs).is_some() && crate::match_subgraph(rhs, lhs).is_some()
226}
227
228#[cfg(test)]
229mod tests {
230    //#[allow()]
231    use super::*;
232    use crate::Edge;
233
234    #[test]
235    fn simple_unique_match() {
236        let _ = pretty_env_logger::try_init_timed();
237        let graph = Graph {
238            nodes: vec![Node::new(0u32, "a"), Node::new(1, "b")],
239            edges: vec![Edge::new_unlabeled(0, 1)],
240        };
241        let query = graph.clone();
242        let possible_isomorphisms: Vec<Isomorphism<u32>> = vec![(&[(0, 0), (1, 1)]).into()];
243        let custom_iso =
244            match_subgraph(&graph, &query).expect("There is at least one possible isomorphism");
245        assert!(possible_isomorphisms.contains(&custom_iso));
246    }
247
248    #[test]
249    fn trivial_disconnected_graphs() {
250        let _ = pretty_env_logger::try_init_timed();
251        let graph = Graph::<_, _, &str> {
252            nodes: vec![Node::new(0u32, "a"), Node::new(1, "b")],
253            edges: vec![],
254        };
255        let query = Graph::<_, _, &str> {
256            nodes: vec![Node::new(10u32, "a"), Node::new(11, "b")],
257            edges: vec![],
258        };
259        let possible_isomorphisms: Vec<Isomorphism<u32>> = vec![(&[(10, 0), (11, 1)]).into()];
260        let custom_iso =
261            match_subgraph(&graph, &query).expect("There is at least one possible isomorphism");
262        assert!(possible_isomorphisms.contains(&custom_iso));
263    }
264
265    #[test]
266    fn simple_nonunique_match() {
267        let _ = pretty_env_logger::try_init_timed();
268        let graph = Graph {
269            nodes: vec![Node::new(0u32, "a"), Node::new(1, "a"), Node::new(2, "b")],
270            edges: vec![Edge::new_unlabeled(0, 2), Edge::new_unlabeled(1, 2)],
271        };
272        let query = Graph {
273            nodes: vec![Node::new(0u32, "a"), Node::new(1, "b")],
274            edges: vec![Edge::new_unlabeled(0, 1)],
275        };
276        let possible_isomorphisms: Vec<Isomorphism<u32>> =
277            vec![(&[(0, 0), (1, 2)]).into(), (&[(0, 1), (1, 2)]).into()];
278        let custom_iso =
279            match_subgraph(&graph, &query).expect("There is at least one possible isomorphism");
280        assert!(possible_isomorphisms.contains(&custom_iso));
281    }
282
283    #[test]
284    fn nonunique_match_with_cycles() {
285        let graph = Graph {
286            nodes: vec![
287                Node::new(0u32, "a"),
288                Node::new(1, "a"),
289                Node::new(2, "a"),
290                Node::new(3, "a"),
291            ],
292            edges: vec![
293                Edge::new_unlabeled(0, 1),
294                Edge::new_unlabeled(1, 2),
295                Edge::new_unlabeled(1, 3),
296                Edge::new_unlabeled(3, 0),
297            ],
298        };
299        let query = Graph {
300            nodes: vec![Node::new(0u32, "a"), Node::new(1, "a"), Node::new(2, "a")],
301            edges: vec![Edge::new_unlabeled(0, 1), Edge::new_unlabeled(1, 2)],
302        };
303        let possible_isomorphisms: Vec<Isomorphism<u32>> = vec![
304            // Path connecting to 2
305            (&[(0, 0), (1, 1), (2, 2)]).into(),
306            (&[(0, 2), (1, 1), (2, 0)]).into(),
307            // Circular path 1 - 2 - 3
308            (&[(0, 0), (1, 1), (2, 3)]).into(),
309            (&[(0, 1), (1, 3), (2, 0)]).into(),
310            (&[(0, 3), (1, 0), (2, 1)]).into(),
311            // Circular path 1 - 2 - 3 in other direction
312            (&[(0, 3), (1, 1), (2, 0)]).into(),
313            (&[(0, 0), (1, 3), (2, 1)]).into(),
314            (&[(0, 1), (1, 0), (2, 3)]).into(),
315        ];
316        for _ in 0..60 {
317            let custom_iso =
318                match_subgraph(&graph, &query).expect("There is at least one possible isomorphism");
319            log::info!("Found isormorphism  : {:#?}", custom_iso);
320            assert!(possible_isomorphisms.contains(&custom_iso));
321        }
322    }
323}