UNPKG

@graphty/algorithms

Version:

Graph algorithms library for browser environments implemented in TypeScript

291 lines 8.96 kB
/** * Check if two graphs are isomorphic using VF2 algorithm */ export function isGraphIsomorphic(graph1, graph2, options = {}) { // Quick checks if (graph1.nodeCount !== graph2.nodeCount || graph1.totalEdgeCount !== graph2.totalEdgeCount) { return { isIsomorphic: false }; } if (graph1.isDirected !== graph2.isDirected) { return { isIsomorphic: false }; } const nodes1 = Array.from(graph1.nodes()).map((n) => n.id); const nodes2 = Array.from(graph2.nodes()).map((n) => n.id); // Check degree sequences const degrees1 = nodes1.map((n) => graph1.degree(n)).sort((a, b) => a - b); const degrees2 = nodes2.map((n) => graph2.degree(n)).sort((a, b) => a - b); for (let i = 0; i < degrees1.length; i++) { if (degrees1[i] !== degrees2[i]) { return { isIsomorphic: false }; } } // Initialize VF2 state const state = { core1: new Map(), core2: new Map(), in1: new Map(), in2: new Map(), out1: new Map(), out2: new Map(), depth: 0, }; // Find isomorphism const mappings = []; const found = vf2Recurse(graph1, graph2, state, nodes1, nodes2, options, mappings); if (found && mappings.length > 0) { return { isIsomorphic: true, mapping: mappings[0] ?? new Map(), }; } return { isIsomorphic: false }; } /** * VF2 recursive search */ function vf2Recurse(g1, g2, state, nodes1, nodes2, options, mappings) { // Check if we've found a complete mapping if (state.core1.size === nodes1.length) { mappings.push(new Map(state.core1)); return !options.findAllMappings; // Continue if we want all mappings } // Get candidate pairs const candidates = getCandidatePairs(g1, g2, state, nodes1, nodes2); for (const [node1, node2] of candidates) { if (isFeasible(g1, g2, state, node1, node2, options)) { // Add pair to mapping const newState = addPair(g1, g2, state, node1, node2); if (vf2Recurse(g1, g2, newState, nodes1, nodes2, options, mappings)) { return true; } } } return false; } /** * Get candidate pairs for the next mapping */ function getCandidatePairs(g1, g2, state, nodes1, nodes2) { const pairs = []; // Try to pick from terminal sets first let node1 = null; // Pick from out1 for (const [n] of state.out1) { if (!state.core1.has(n)) { node1 = n; break; } } // If not found, pick from in1 if (!node1) { for (const [n] of state.in1) { if (!state.core1.has(n)) { node1 = n; break; } } } // If still not found, pick any unmapped node if (!node1) { for (const n of nodes1) { if (!state.core1.has(n)) { node1 = n; break; } } } if (!node1) { return pairs; } // Find compatible nodes in G2 for (const node2 of nodes2) { if (!state.core2.has(node2)) { // Basic compatibility check if (g1.degree(node1) === g2.degree(node2)) { pairs.push([node1, node2]); } } } return pairs; } /** * Check if a pair is feasible */ function isFeasible(g1, g2, state, node1, node2, options) { // Node match predicate if (options.nodeMatch && !options.nodeMatch(node1, node2, g1, g2)) { return false; } // Check syntactic feasibility const neighbors1 = new Set(g1.neighbors(node1)); const neighbors2 = new Set(g2.neighbors(node2)); // Check that mapped neighbors correspond for (const [n1, n2] of state.core1) { if (neighbors1.has(n1)) { if (!neighbors2.has(n2)) { return false; } // Edge match predicate if (options.edgeMatch && !options.edgeMatch([node1, n1], [node2, n2], g1, g2)) { return false; } } else if (neighbors2.has(n2)) { return false; } } // Check terminal set sizes let new1In = 0; let new1Out = 0; let term1In = 0; let term1Out = 0; let new2In = 0; let new2Out = 0; let term2In = 0; let term2Out = 0; for (const neighbor of neighbors1) { if (state.core1.has(neighbor)) { // Already mapped } else if (state.in1.has(neighbor)) { term1In++; } else if (state.out1.has(neighbor)) { term1Out++; } else { if (g1.isDirected) { if (g1.hasEdge(neighbor, node1)) { new1In++; } if (g1.hasEdge(node1, neighbor)) { new1Out++; } } else { new1In++; new1Out++; } } } for (const neighbor of neighbors2) { if (state.core2.has(neighbor)) { // Already mapped } else if (state.in2.has(neighbor)) { term2In++; } else if (state.out2.has(neighbor)) { term2Out++; } else { if (g2.isDirected) { if (g2.hasEdge(neighbor, node2)) { new2In++; } if (g2.hasEdge(node2, neighbor)) { new2Out++; } } else { new2In++; new2Out++; } } } return term1In === term2In && term1Out === term2Out && new1In === new2In && new1Out === new2Out; } /** * Add a pair to the mapping and update terminal sets */ function addPair(g1, g2, state, node1, node2) { const newState = { core1: new Map(state.core1), core2: new Map(state.core2), in1: new Map(state.in1), in2: new Map(state.in2), out1: new Map(state.out1), out2: new Map(state.out2), depth: state.depth + 1, }; newState.core1.set(node1, node2); newState.core2.set(node2, node1); // Update terminal sets const neighbors1 = g1.neighbors(node1); for (const neighbor of neighbors1) { if (!newState.core1.has(neighbor) && !newState.in1.has(neighbor) && !newState.out1.has(neighbor)) { if (g1.isDirected) { if (g1.hasEdge(neighbor, node1)) { newState.in1.set(neighbor, newState.depth); } if (g1.hasEdge(node1, neighbor)) { newState.out1.set(neighbor, newState.depth); } } else { newState.in1.set(neighbor, newState.depth); newState.out1.set(neighbor, newState.depth); } } } const neighbors2 = g2.neighbors(node2); for (const neighbor of neighbors2) { if (!newState.core2.has(neighbor) && !newState.in2.has(neighbor) && !newState.out2.has(neighbor)) { if (g2.isDirected) { if (g2.hasEdge(neighbor, node2)) { newState.in2.set(neighbor, newState.depth); } if (g2.hasEdge(node2, neighbor)) { newState.out2.set(neighbor, newState.depth); } } else { newState.in2.set(neighbor, newState.depth); newState.out2.set(neighbor, newState.depth); } } } return newState; } /** * Find all isomorphisms between two graphs */ export function findAllIsomorphisms(graph1, graph2, options = {}) { // Quick checks if (graph1.nodeCount !== graph2.nodeCount || graph1.totalEdgeCount !== graph2.totalEdgeCount) { return []; } if (graph1.isDirected !== graph2.isDirected) { return []; } const nodes1 = Array.from(graph1.nodes()).map((n) => n.id); const nodes2 = Array.from(graph2.nodes()).map((n) => n.id); // Check degree sequences const degrees1 = nodes1.map((n) => graph1.degree(n)).sort((a, b) => a - b); const degrees2 = nodes2.map((n) => graph2.degree(n)).sort((a, b) => a - b); for (let i = 0; i < degrees1.length; i++) { if (degrees1[i] !== degrees2[i]) { return []; } } // Handle empty graphs if (nodes1.length === 0) { return [new Map()]; } // Initialize VF2 state const state = { core1: new Map(), core2: new Map(), in1: new Map(), in2: new Map(), out1: new Map(), out2: new Map(), depth: 0, }; // Find all isomorphisms const mappings = []; vf2Recurse(graph1, graph2, state, nodes1, nodes2, { ...options, findAllMappings: true }, mappings); return mappings; } //# sourceMappingURL=isomorphism.js.map