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}