network_isomorphism_solver/lib.rs
1#![allow(clippy::similar_names)]
2
3//! Network Isomorphism Solver using Links Theory
4//!
5//! This library provides algorithms for determining if two networks (represented as links)
6//! are isomorphic, meaning they are structurally identical via node relabeling.
7//!
8//! Based on [Links Theory](https://habr.com/en/articles/895896), this implementation
9//! uses doublet-links (ordered pairs of references) as the fundamental data structure,
10//! replacing traditional graph theory concepts of vertices and edges with a unified
11//! link concept where "a link connects links".
12//!
13//! This library uses the [`links-notation`](https://crates.io/crates/links-notation) crate
14//! for parsing Links Notation (`LiNo`) format, which is the standard notation for describing
15//! link structures.
16//!
17//! # Real-World Applications
18//!
19//! - **Drug Discovery**: Compare molecular structures to identify similar compounds
20//! - **Circuit Verification**: Check if two electronic circuit designs are equivalent
21//! - **Computer Vision**: Match patterns in image structures for object recognition
22//! - **Social Network Analysis**: Detect isomorphic substructures in user networks
23//! - **Cryptography**: Zero-knowledge proofs relying on graph isomorphism hardness
24//!
25//! # Links Notation (`LiNo`) Examples
26//!
27//! Networks can be described using Links Notation from the
28//! [links-notation](https://github.com/link-foundation/links-notation) library:
29//!
30//! ```text
31//! // Doublet format (source target)
32//! (1 2) (2 3) (3 1)
33//!
34//! // Named links
35//! (bond1: C1 C2) (bond2: C2 C3)
36//!
37//! // Nested structures
38//! ((A B) (B C) (C A))
39//! ```
40//!
41//! # Example
42//!
43//! ```
44//! use network_isomorphism_solver::{LinkNetwork, is_isomorphic};
45//!
46//! let mut network1 = LinkNetwork::new();
47//! network1.add_link(1, 2);
48//! network1.add_link(2, 3);
49//! network1.add_link(3, 1);
50//!
51//! let mut network2 = LinkNetwork::new();
52//! network2.add_link(10, 20);
53//! network2.add_link(20, 30);
54//! network2.add_link(30, 10);
55//!
56//! assert!(is_isomorphic(&network1, &network2));
57//! ```
58
59mod algorithm;
60
61use std::collections::{HashMap, HashSet};
62
63use algorithm::{
64 compute_network_hash, compute_wl_signatures, find_all_automorphisms, find_isomorphism_mapping,
65 subnetwork_backtrack,
66};
67
68// Re-export links-notation crate for users who want to parse LiNo directly
69pub use links_notation;
70use links_notation::{parse_lino, LiNo, ParseError};
71
72/// Package version (matches Cargo.toml version).
73pub const VERSION: &str = env!("CARGO_PKG_VERSION");
74
75/// A unique identifier for a link in the network.
76/// In Links Theory, each link has its own reference enabling cross-references.
77pub type LinkId = u64;
78
79/// A doublet-link representing a connection between two link references.
80/// This is the fundamental building block in Links Theory: L → L²
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub struct DoubletLink {
83 /// The source link reference
84 pub source: LinkId,
85 /// The target link reference
86 pub target: LinkId,
87}
88
89impl DoubletLink {
90 /// Creates a new doublet-link.
91 ///
92 /// # Arguments
93 ///
94 /// * `source` - The source link reference
95 /// * `target` - The target link reference
96 #[must_use]
97 pub const fn new(source: LinkId, target: LinkId) -> Self {
98 Self { source, target }
99 }
100}
101
102/// A network of links representing interconnected structures.
103///
104/// In Links Theory, this replaces the traditional graph concept with a unified
105/// link-based model where vertices and edges are both represented as links.
106#[derive(Debug, Clone, Default)]
107pub struct LinkNetwork {
108 /// All doublet-links in the network
109 links: Vec<DoubletLink>,
110 /// Set of all unique link references (nodes)
111 nodes: HashSet<LinkId>,
112 /// Adjacency list: node -> set of connected nodes
113 adjacency: HashMap<LinkId, HashSet<LinkId>>,
114}
115
116impl LinkNetwork {
117 /// Creates a new empty link network.
118 #[must_use]
119 pub fn new() -> Self {
120 Self::default()
121 }
122
123 /// Creates a link network from Links Notation string.
124 ///
125 /// Parses simple triplet notation where each line represents a link:
126 /// `source relationship target`
127 ///
128 /// # Arguments
129 ///
130 /// * `notation` - Links Notation string describing the network
131 ///
132 /// # Returns
133 ///
134 /// A new `LinkNetwork` parsed from the notation
135 ///
136 /// # Example
137 ///
138 /// ```
139 /// use network_isomorphism_solver::LinkNetwork;
140 ///
141 /// let network = LinkNetwork::from_notation("
142 /// A connects B
143 /// B connects C
144 /// C connects A
145 /// ");
146 /// assert_eq!(network.node_count(), 3);
147 /// assert_eq!(network.link_count(), 3);
148 /// ```
149 #[must_use]
150 pub fn from_notation(notation: &str) -> Self {
151 let mut network = Self::new();
152 let mut name_to_id: HashMap<String, LinkId> = HashMap::new();
153 let mut next_id: LinkId = 1;
154
155 for line in notation.lines() {
156 let line = line.trim();
157 if line.is_empty() || line.starts_with('#') {
158 continue;
159 }
160
161 let parts: Vec<&str> = line.split_whitespace().collect();
162 if parts.len() >= 3 {
163 // Triplet format: source relationship target
164 let source_name = parts[0];
165 let target_name = parts[parts.len() - 1];
166
167 let source_id = *name_to_id
168 .entry(source_name.to_string())
169 .or_insert_with(|| {
170 let id = next_id;
171 next_id += 1;
172 id
173 });
174
175 let target_id = *name_to_id
176 .entry(target_name.to_string())
177 .or_insert_with(|| {
178 let id = next_id;
179 next_id += 1;
180 id
181 });
182
183 network.add_link(source_id, target_id);
184 } else if parts.len() == 2 {
185 // Doublet format: source target
186 let source_name = parts[0];
187 let target_name = parts[1];
188
189 let source_id = *name_to_id
190 .entry(source_name.to_string())
191 .or_insert_with(|| {
192 let id = next_id;
193 next_id += 1;
194 id
195 });
196
197 let target_id = *name_to_id
198 .entry(target_name.to_string())
199 .or_insert_with(|| {
200 let id = next_id;
201 next_id += 1;
202 id
203 });
204
205 network.add_link(source_id, target_id);
206 }
207 }
208
209 network
210 }
211
212 /// Creates a link network from Links Notation (`LiNo`) string using the
213 /// [`links-notation`](https://crates.io/crates/links-notation) crate parser.
214 ///
215 /// This method parses proper `LiNo` format: `(source target)` doublets.
216 /// For nested structures, it extracts the top-level doublets.
217 ///
218 /// # Arguments
219 ///
220 /// * `lino` - Links Notation string in `LiNo` format
221 ///
222 /// # Returns
223 ///
224 /// `Ok(LinkNetwork)` if parsing succeeds, `Err(ParseError)` otherwise
225 ///
226 /// # Example
227 ///
228 /// ```
229 /// use network_isomorphism_solver::LinkNetwork;
230 ///
231 /// // Parse LiNo doublet format
232 /// let network = LinkNetwork::from_lino("(1 2) (2 3) (3 1)").unwrap();
233 /// assert_eq!(network.node_count(), 3);
234 /// assert_eq!(network.link_count(), 3);
235 ///
236 /// // Parse named links
237 /// let network2 = LinkNetwork::from_lino("(link1: A B) (link2: B C)").unwrap();
238 /// assert_eq!(network2.node_count(), 3);
239 /// assert_eq!(network2.link_count(), 2);
240 /// ```
241 ///
242 /// # Errors
243 ///
244 /// Returns `ParseError` if the `LiNo` string cannot be parsed
245 pub fn from_lino(lino: &str) -> Result<Self, ParseError> {
246 let parsed = parse_lino(lino)?;
247 let mut network = Self::new();
248 let mut name_to_id: HashMap<String, LinkId> = HashMap::new();
249 let mut next_id: LinkId = 1;
250
251 // Helper to get or create an ID for a reference name
252 let mut get_id = |name: &str| -> LinkId {
253 *name_to_id.entry(name.to_string()).or_insert_with(|| {
254 let id = next_id;
255 next_id += 1;
256 id
257 })
258 };
259
260 // Extract doublets from the parsed LiNo structure
261 Self::extract_doublets_from_lino(&parsed, &mut network, &mut get_id);
262
263 Ok(network)
264 }
265
266 /// Recursively extracts doublet links from a `LiNo` structure.
267 fn extract_doublets_from_lino<F>(lino: &LiNo<String>, network: &mut Self, get_id: &mut F)
268 where
269 F: FnMut(&str) -> LinkId,
270 {
271 match lino {
272 LiNo::Ref(_) => {
273 // Single reference, not a link
274 }
275 LiNo::Link { values, .. } => {
276 // Check if this is a doublet (exactly 2 values that are both refs)
277 if values.len() == 2 {
278 if let (LiNo::Ref(source), LiNo::Ref(target)) = (&values[0], &values[1]) {
279 let source_id = get_id(source);
280 let target_id = get_id(target);
281 network.add_link(source_id, target_id);
282 return;
283 }
284 }
285
286 // For triplets (source rel target), extract as doublet (source target)
287 if values.len() == 3 {
288 if let (LiNo::Ref(source), LiNo::Ref(_), LiNo::Ref(target)) =
289 (&values[0], &values[1], &values[2])
290 {
291 let source_id = get_id(source);
292 let target_id = get_id(target);
293 network.add_link(source_id, target_id);
294 return;
295 }
296 }
297
298 // Recursively process nested links
299 for value in values {
300 Self::extract_doublets_from_lino(value, network, get_id);
301 }
302 }
303 }
304 }
305
306 /// Adds a doublet-link to the network.
307 ///
308 /// # Arguments
309 ///
310 /// * `source` - The source link reference
311 /// * `target` - The target link reference
312 pub fn add_link(&mut self, source: LinkId, target: LinkId) {
313 self.links.push(DoubletLink::new(source, target));
314 self.nodes.insert(source);
315 self.nodes.insert(target);
316
317 self.adjacency.entry(source).or_default().insert(target);
318 self.adjacency.entry(target).or_default().insert(source);
319 }
320
321 /// Returns the number of nodes (unique link references) in the network.
322 #[must_use]
323 pub fn node_count(&self) -> usize {
324 self.nodes.len()
325 }
326
327 /// Returns the number of links in the network.
328 #[must_use]
329 pub fn link_count(&self) -> usize {
330 self.links.len()
331 }
332
333 /// Returns all nodes in the network.
334 #[must_use]
335 pub fn nodes(&self) -> &HashSet<LinkId> {
336 &self.nodes
337 }
338
339 /// Returns all links in the network.
340 #[must_use]
341 pub fn links(&self) -> &[DoubletLink] {
342 &self.links
343 }
344
345 /// Returns the degree (number of connections) of a node.
346 #[must_use]
347 pub fn degree(&self, node: LinkId) -> usize {
348 self.adjacency.get(&node).map_or(0, HashSet::len)
349 }
350
351 /// Returns the neighbors of a node.
352 #[must_use]
353 pub fn neighbors(&self, node: LinkId) -> Option<&HashSet<LinkId>> {
354 self.adjacency.get(&node)
355 }
356
357 /// Computes the degree sequence of the network (sorted list of degrees).
358 #[must_use]
359 pub fn degree_sequence(&self) -> Vec<usize> {
360 let mut degrees: Vec<usize> = self.nodes.iter().map(|&n| self.degree(n)).collect();
361 degrees.sort_unstable();
362 degrees
363 }
364}
365
366/// Result of an isomorphism check, containing the mapping if isomorphic.
367#[derive(Debug, Clone)]
368pub struct IsomorphismResult {
369 /// Whether the networks are isomorphic
370 pub is_isomorphic: bool,
371 /// The node mapping from network1 to network2 (if isomorphic)
372 pub mapping: Option<HashMap<LinkId, LinkId>>,
373}
374
375impl IsomorphismResult {
376 /// Creates a result indicating the networks are not isomorphic.
377 #[must_use]
378 const fn not_isomorphic() -> Self {
379 Self {
380 is_isomorphic: false,
381 mapping: None,
382 }
383 }
384
385 /// Creates a result indicating the networks are isomorphic with the given mapping.
386 #[must_use]
387 fn isomorphic(mapping: HashMap<LinkId, LinkId>) -> Self {
388 Self {
389 is_isomorphic: true,
390 mapping: Some(mapping),
391 }
392 }
393}
394
395/// Checks if two link networks are isomorphic.
396///
397/// Uses a combination of fast invariant checks and the Weisfeiler-Lehman algorithm
398/// with backtracking refinement for accurate isomorphism detection.
399///
400/// # Arguments
401///
402/// * `network1` - The first link network
403/// * `network2` - The second link network
404///
405/// # Returns
406///
407/// `true` if the networks are isomorphic, `false` otherwise
408///
409/// # Example
410///
411/// ```
412/// use network_isomorphism_solver::{LinkNetwork, is_isomorphic};
413///
414/// // Create two isomorphic triangle networks
415/// let mut net1 = LinkNetwork::new();
416/// net1.add_link(1, 2);
417/// net1.add_link(2, 3);
418/// net1.add_link(3, 1);
419///
420/// let mut net2 = LinkNetwork::new();
421/// net2.add_link(100, 200);
422/// net2.add_link(200, 300);
423/// net2.add_link(300, 100);
424///
425/// assert!(is_isomorphic(&net1, &net2));
426/// ```
427#[must_use]
428pub fn is_isomorphic(network1: &LinkNetwork, network2: &LinkNetwork) -> bool {
429 check_isomorphism(network1, network2).is_isomorphic
430}
431
432/// Checks if two link networks are isomorphic and returns the mapping.
433///
434/// # Arguments
435///
436/// * `network1` - The first link network
437/// * `network2` - The second link network
438///
439/// # Returns
440///
441/// An `IsomorphismResult` containing whether the networks are isomorphic
442/// and the node mapping if they are
443#[must_use]
444pub fn check_isomorphism(network1: &LinkNetwork, network2: &LinkNetwork) -> IsomorphismResult {
445 // Quick structural checks
446 if network1.node_count() != network2.node_count() {
447 return IsomorphismResult::not_isomorphic();
448 }
449
450 if network1.link_count() != network2.link_count() {
451 return IsomorphismResult::not_isomorphic();
452 }
453
454 // Empty networks are isomorphic
455 if network1.node_count() == 0 {
456 return IsomorphismResult::isomorphic(HashMap::new());
457 }
458
459 // Check degree sequences
460 if network1.degree_sequence() != network2.degree_sequence() {
461 return IsomorphismResult::not_isomorphic();
462 }
463
464 // Check WL hashes
465 let hash1 = compute_network_hash(network1);
466 let hash2 = compute_network_hash(network2);
467
468 if hash1 != hash2 {
469 return IsomorphismResult::not_isomorphic();
470 }
471
472 // Group nodes by their WL signatures for both networks
473 let sig1 = compute_wl_signatures(network1, 3);
474 let sig2 = compute_wl_signatures(network2, 3);
475
476 let mut label_to_nodes1: HashMap<u64, Vec<LinkId>> = HashMap::new();
477 for (&node, &label) in &sig1 {
478 label_to_nodes1.entry(label).or_default().push(node);
479 }
480
481 let mut label_to_nodes2: HashMap<u64, Vec<LinkId>> = HashMap::new();
482 for (&node, &label) in &sig2 {
483 label_to_nodes2.entry(label).or_default().push(node);
484 }
485
486 // Verify partitions match
487 let mut partition1: Vec<usize> = label_to_nodes1.values().map(Vec::len).collect();
488 let mut partition2: Vec<usize> = label_to_nodes2.values().map(Vec::len).collect();
489 partition1.sort_unstable();
490 partition2.sort_unstable();
491
492 if partition1 != partition2 {
493 return IsomorphismResult::not_isomorphic();
494 }
495
496 // Try to find a valid mapping using backtracking
497 // Sort nodes by degree (ascending) for better pruning - start with boundary nodes
498 let mut nodes1: Vec<LinkId> = network1.nodes().iter().copied().collect();
499 nodes1.sort_by_key(|&n| network1.degree(n));
500 let mut nodes2: Vec<LinkId> = network2.nodes().iter().copied().collect();
501 nodes2.sort_by_key(|&n| network2.degree(n));
502
503 if let Some(mapping) = find_isomorphism_mapping(network1, network2, &nodes1, &nodes2) {
504 return IsomorphismResult::isomorphic(mapping);
505 }
506
507 IsomorphismResult::not_isomorphic()
508}
509
510/// Finds all automorphisms of a network.
511///
512/// An automorphism is an isomorphism from a network to itself,
513/// representing its symmetries.
514///
515/// # Arguments
516///
517/// * `network` - The link network to analyze
518///
519/// # Returns
520///
521/// A vector of all automorphism mappings
522#[must_use]
523pub fn find_automorphisms(network: &LinkNetwork) -> Vec<HashMap<LinkId, LinkId>> {
524 let nodes: Vec<LinkId> = network.nodes().iter().copied().collect();
525 let sig = compute_wl_signatures(network, 3);
526 let mut automorphisms = Vec::new();
527 let mut mapping: HashMap<LinkId, LinkId> = HashMap::new();
528 let mut used: HashSet<LinkId> = HashSet::new();
529
530 find_all_automorphisms(
531 network,
532 &nodes,
533 &sig,
534 0,
535 &mut mapping,
536 &mut used,
537 &mut automorphisms,
538 );
539
540 automorphisms
541}
542
543/// Checks if a network contains a subnetwork isomorphic to the pattern.
544///
545/// Useful for pattern matching in networks, such as finding molecular
546/// substructures or detecting specific network motifs.
547///
548/// # Arguments
549///
550/// * `network` - The network to search in
551/// * `pattern` - The pattern network to find
552///
553/// # Returns
554///
555/// `true` if the pattern exists as a subnetwork
556#[must_use]
557pub fn contains_subnetwork(network: &LinkNetwork, pattern: &LinkNetwork) -> bool {
558 if pattern.node_count() > network.node_count() {
559 return false;
560 }
561
562 if pattern.link_count() > network.link_count() {
563 return false;
564 }
565
566 if pattern.node_count() == 0 {
567 return true;
568 }
569
570 let pattern_nodes: Vec<LinkId> = pattern.nodes().iter().copied().collect();
571 let network_nodes: Vec<LinkId> = network.nodes().iter().copied().collect();
572
573 let mut mapping: HashMap<LinkId, LinkId> = HashMap::new();
574 let mut used: HashSet<LinkId> = HashSet::new();
575
576 subnetwork_backtrack(
577 network,
578 pattern,
579 &pattern_nodes,
580 &network_nodes,
581 0,
582 &mut mapping,
583 &mut used,
584 )
585}