diff --git a/examples/matching_trace.rs b/examples/matching_trace.rs new file mode 100644 index 0000000..8f994f8 --- /dev/null +++ b/examples/matching_trace.rs @@ -0,0 +1,232 @@ +use open_hypergraphs::lax::matching::{MatchEvent, MatchTrace}; +use open_hypergraphs::lax::Hypergraph; +use std::cell::{Cell, RefCell}; +use std::collections::BTreeMap; + +struct PrintTrace { + indent: Cell, + edge_decision: RefCell>, + node_decision: RefCell>>, + edge_assign: RefCell>, + node_assign: RefCell>, + frame_edges: RefCell>>, + frame_nodes: RefCell>>, + frame_stack: RefCell>, +} + +impl PrintTrace { + fn new() -> Self { + Self { + indent: Cell::new(0), + edge_decision: RefCell::new(BTreeMap::new()), + node_decision: RefCell::new(BTreeMap::new()), + edge_assign: RefCell::new(BTreeMap::new()), + node_assign: RefCell::new(BTreeMap::new()), + frame_edges: RefCell::new(BTreeMap::new()), + frame_nodes: RefCell::new(BTreeMap::new()), + frame_stack: RefCell::new(Vec::new()), + } + } + + fn indent(&self) -> usize { + self.indent.get() * 2 + } + + fn print_solution(&self) { + let edge_assign = self.edge_assign.borrow(); + let node_assign = self.node_assign.borrow(); + let mut parts = Vec::new(); + for (p_edge, t_edge) in edge_assign.iter() { + parts.push(format!("e{p_edge}->t{t_edge}")); + } + for (p_node, t_node) in node_assign.iter() { + parts.push(format!("n{p_node}->t{t_node}")); + } + + if parts.is_empty() { + println!("{:indent$}solution", "", indent = self.indent()); + } else { + println!( + "{:indent$}solution [{}]", + "", + parts.join(", "), + indent = self.indent() + ); + } + } +} + +impl MatchTrace for PrintTrace { + fn on_event(&self, event: MatchEvent) { + match event { + MatchEvent::EnterFrame { depth: _, frame_id } => { + println!( + "{:indent$}enter frame #{frame_id}", + "", + indent = self.indent() + ); + self.indent.set(self.indent.get() + 1); + self.frame_stack.borrow_mut().push(frame_id); + } + MatchEvent::ExitFrame { depth: _, frame_id } => { + let indent = self.indent.get().saturating_sub(1); + self.indent.set(indent); + println!( + "{:indent$}exit frame #{frame_id}", + "", + indent = self.indent() + ); + if let Some(top) = self.frame_stack.borrow_mut().pop() { + self.edge_decision.borrow_mut().remove(&top); + self.node_decision.borrow_mut().remove(&top); + if let Some(edges) = self.frame_edges.borrow_mut().remove(&top) { + let mut edge_assign = self.edge_assign.borrow_mut(); + for p_edge in edges { + edge_assign.remove(&p_edge); + } + } + if let Some(nodes) = self.frame_nodes.borrow_mut().remove(&top) { + let mut node_assign = self.node_assign.borrow_mut(); + for p_node in nodes { + node_assign.remove(&p_node); + } + } + } + } + MatchEvent::Decision { + pattern_edge, + pattern_node, + choice_features, + candidate_count, + heuristic_tag, + depth: _, + } => { + let current = self.frame_stack.borrow().last().copied(); + if let Some(frame_id) = current { + if let Some(edge) = pattern_edge { + self.edge_decision.borrow_mut().insert(frame_id, edge); + } + if let Some(node) = pattern_node { + self.node_decision + .borrow_mut() + .entry(frame_id) + .or_default() + .push(node); + } + } + println!( + "{:indent$}decision pattern_edge={pattern_edge:?} pattern_node={pattern_node:?} candidates={candidate_count} features={choice_features} heuristic={heuristic_tag}", + "", + indent = self.indent() + ); + } + MatchEvent::Branch { + target_edge, + target_node, + depth: _, + } => { + let current = self.frame_stack.borrow().last().copied(); + if let Some(frame_id) = current { + if let Some(edge) = target_edge { + if let Some(p_edge) = self.edge_decision.borrow().get(&frame_id) { + self.edge_assign.borrow_mut().insert(*p_edge, edge); + self.frame_edges + .borrow_mut() + .entry(frame_id) + .or_default() + .push(*p_edge); + } + } + if let Some(node) = target_node { + let mut pending = self.node_decision.borrow_mut(); + if let Some(stack) = pending.get_mut(&frame_id) { + if let Some(p_node) = stack.pop() { + self.node_assign.borrow_mut().insert(p_node, node); + self.frame_nodes + .borrow_mut() + .entry(frame_id) + .or_default() + .push(p_node); + } + } + } + } + let frame_id = self.frame_stack.borrow().last().copied(); + let edge_map = target_edge.and_then(|edge| { + let frame_id = frame_id?; + let edge_decision = self.edge_decision.borrow(); + let p_edge = edge_decision.get(&frame_id)?; + Some(format!("map e{p_edge}->t{edge}")) + }); + let node_map = target_node.and_then(|node| { + let frame_id = frame_id?; + let node_decision = self.node_decision.borrow(); + let p_node = node_decision.get(&frame_id)?.last().copied()?; + Some(format!("map n{p_node}->t{node}")) + }); + let map_note = match (edge_map, node_map) { + (Some(edge), Some(node)) => format!("{edge}, {node}"), + (Some(edge), None) => edge, + (None, Some(node)) => node, + (None, None) => String::new(), + }; + println!( + "{:indent$}branch target_edge={target_edge:?} target_node={target_node:?} {map_note}", + "", + indent = self.indent() + ); + } + MatchEvent::PropagationSummary { depth: _ } => { + println!("{:indent$}propagation done", "", indent = self.indent()); + } + MatchEvent::Prune { + reason, + detail, + depth: _, + } => { + println!( + "{:indent$}prune: {reason} ({detail})", + "", + indent = self.indent() + ); + } + MatchEvent::Solution => { + self.print_solution(); + } + } + } +} + +fn build_target() -> Hypergraph { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(0); + target.new_edge('f', (vec![n0], vec![n1])); + target.new_edge('f', (vec![n1], vec![n0])); + target.new_edge('f', (vec![n0], vec![n0])); + target.new_edge('f', (vec![n1], vec![n1])); + target +} + +fn build_pattern() -> Hypergraph { + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(0); + pattern.new_edge('f', (vec![p0], vec![p1])); + pattern +} + +fn main() { + let target = build_target(); + let pattern = build_pattern(); + let trace = PrintTrace::new(); + + println!("=== isomorphisms ==="); + let iso = target.find_subgraph_isomorphisms(&pattern, Some(&trace)); + println!("isomorphisms: {}", iso.len()); + + println!(); + println!("=== homomorphisms ==="); + let homo = target.find_subgraph_homomorphisms(&pattern, Some(&trace)); + println!("homomorphisms: {}", homo.len()); +} diff --git a/src/lax/matching.rs b/src/lax/matching.rs new file mode 100644 index 0000000..962534b --- /dev/null +++ b/src/lax/matching.rs @@ -0,0 +1,800 @@ +use super::hypergraph::{EdgeId, Hypergraph, NodeId}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Morphism { + node_map: Vec, + edge_map: Vec, +} + +impl Morphism { + pub fn node_map(&self) -> &[NodeId] { + &self.node_map + } + + pub fn edge_map(&self) -> &[EdgeId] { + &self.edge_map + } +} + +pub trait MatchTrace { + fn on_event(&self, _event: MatchEvent) {} +} + +pub struct NoopTrace; + +impl MatchTrace for NoopTrace {} + +static NOOP_TRACE: NoopTrace = NoopTrace; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MatchEvent { + EnterFrame { + depth: usize, + frame_id: usize, + }, + Decision { + pattern_edge: Option, + pattern_node: Option, + choice_features: &'static str, + candidate_count: usize, + heuristic_tag: &'static str, + depth: usize, + }, + Branch { + target_edge: Option, + target_node: Option, + depth: usize, + }, + PropagationSummary { + depth: usize, + }, + Prune { + reason: &'static str, + detail: &'static str, + depth: usize, + }, + Solution, + ExitFrame { + depth: usize, + frame_id: usize, + }, +} + +impl Hypergraph { + /// Find all subgraph isomorphisms from `pattern` into `self`. + /// + /// This uses an edge-first backtracking search specialized to hypergraphs. + /// The quotient map is ignored; run `quotient` first if you want strict matching. + pub fn find_subgraph_isomorphisms_by( + &self, + pattern: &Hypergraph, + node_eq: FN, + edge_eq: FE, + trace: Option<&dyn MatchTrace>, + ) -> Vec + where + FN: Fn(&OP, &O) -> bool, + FE: Fn(&AP, &A) -> bool, + { + let trace = trace.unwrap_or(&NOOP_TRACE); + find_subgraph_isomorphisms_impl(self, pattern, &node_eq, &edge_eq, trace) + } + + /// Find all subgraph homomorphisms from `pattern` into `self`. + /// + /// This uses an edge-first backtracking search specialized to hypergraphs, + /// but does not enforce injectivity (mono) on nodes or edges. + /// The quotient map is ignored; run `quotient` first if you want strict matching. + pub fn find_subgraph_homomorphisms_by( + &self, + pattern: &Hypergraph, + node_eq: FN, + edge_eq: FE, + trace: Option<&dyn MatchTrace>, + ) -> Vec + where + FN: Fn(&OP, &O) -> bool, + FE: Fn(&AP, &A) -> bool, + { + let trace = trace.unwrap_or(&NOOP_TRACE); + find_subgraph_homomorphisms_impl(self, pattern, &node_eq, &edge_eq, trace) + } +} + +impl Hypergraph { + /// Find all subgraph isomorphisms from `pattern` into `self` by label equality. + pub fn find_subgraph_isomorphisms( + &self, + pattern: &Hypergraph, + trace: Option<&dyn MatchTrace>, + ) -> Vec { + self.find_subgraph_isomorphisms_by(pattern, |a, b| a == b, |a, b| a == b, trace) + } + + /// Find all subgraph homomorphisms from `pattern` into `self` by label equality. + pub fn find_subgraph_homomorphisms( + &self, + pattern: &Hypergraph, + trace: Option<&dyn MatchTrace>, + ) -> Vec { + self.find_subgraph_homomorphisms_by(pattern, |a, b| a == b, |a, b| a == b, trace) + } +} + +fn find_subgraph_homomorphisms_impl( + target: &Hypergraph, + pattern: &Hypergraph, + node_eq: &FN, + edge_eq: &FE, + trace: &dyn MatchTrace, +) -> Vec +where + FN: Fn(&OP, &O) -> bool, + FE: Fn(&AP, &A) -> bool, +{ + find_subgraph_matches_impl(target, pattern, node_eq, edge_eq, false, trace) +} + +fn find_subgraph_isomorphisms_impl( + target: &Hypergraph, + pattern: &Hypergraph, + node_eq: &FN, + edge_eq: &FE, + trace: &dyn MatchTrace, +) -> Vec +where + FN: Fn(&OP, &O) -> bool, + FE: Fn(&AP, &A) -> bool, +{ + find_subgraph_matches_impl(target, pattern, node_eq, edge_eq, true, trace) +} + +fn find_subgraph_matches_impl( + target: &Hypergraph, + pattern: &Hypergraph, + node_eq: &FN, + edge_eq: &FE, + injective: bool, + trace: &dyn MatchTrace, +) -> Vec +where + FN: Fn(&OP, &O) -> bool, + FE: Fn(&AP, &A) -> bool, +{ + let options = MatchOptions { injective }; + if !cardinality_feasible(pattern, target, &options) { + return Vec::new(); + } + + // Precompute candidate target edges for each pattern edge. + // `edge_candidates[p]` is the list of target edge indices that match `p` by label and arity. + let mut edge_candidates = Vec::with_capacity(pattern.edges.len()); + for (p_edge_idx, p_label) in pattern.edges.iter().enumerate() { + let p_adj = &pattern.adjacency[p_edge_idx]; + let mut candidates = Vec::new(); + for (t_edge_idx, t_label) in target.edges.iter().enumerate() { + if !edge_eq(p_label, t_label) { + continue; + } + let t_adj = &target.adjacency[t_edge_idx]; + if p_adj.sources.len() != t_adj.sources.len() + || p_adj.targets.len() != t_adj.targets.len() + { + continue; + } + candidates.push(t_edge_idx); + } + if candidates.is_empty() && !pattern.edges.is_empty() { + return Vec::new(); + } + edge_candidates.push(candidates); + } + + // Precompute degrees for pruning in the injective case. + let (pattern_in, pattern_out) = node_degrees(pattern); + let (target_in, target_out) = node_degrees(target); + + // Explore edges with fewer candidates first (and higher arity as a tie-breaker). + // Rationale: "fail fast" ordering reduces backtracking when constraints are tight. + let mut edge_order: Vec = (0..pattern.edges.len()).collect(); + edge_order.sort_by_key(|&edge_idx| { + let arity = + pattern.adjacency[edge_idx].sources.len() + pattern.adjacency[edge_idx].targets.len(); + (edge_candidates[edge_idx].len(), std::cmp::Reverse(arity)) + }); + + // Track isolated nodes so we can assign them after edge mapping. + let mut node_in_edge = vec![false; pattern.nodes.len()]; + for edge in &pattern.adjacency { + for node in edge.sources.iter().chain(edge.targets.iter()) { + node_in_edge[node.0] = true; + } + } + let isolated_nodes: Vec = node_in_edge + .iter() + .enumerate() + .filter_map(|(idx, used)| if *used { None } else { Some(idx) }) + .collect(); + + let mut state = MatchState::new(pattern, target); + let context = MatchContext::new( + target, + pattern, + node_eq, + &edge_order, + &edge_candidates, + &isolated_nodes, + &pattern_in, + &pattern_out, + &target_in, + &target_out, + &options, + trace, + ); + let mut matches = Vec::new(); + + backtrack_edges(&context, 0, &mut state, &mut matches); + + matches +} + +fn cardinality_feasible( + pattern: &Hypergraph, + target: &Hypergraph, + options: &MatchOptions, +) -> bool { + if !options.injective { + return true; + } + pattern.nodes.len() <= target.nodes.len() && pattern.edges.len() <= target.edges.len() +} + +struct MatchOptions { + injective: bool, +} + +struct MatchContext<'a, OP, AP, O, A, FN> +where + FN: Fn(&OP, &O) -> bool, +{ + target: &'a Hypergraph, + pattern: &'a Hypergraph, + node_eq: &'a FN, + edge_order: &'a [usize], + edge_candidates: &'a [Vec], + isolated_nodes: &'a [usize], + pattern_in: &'a [usize], + pattern_out: &'a [usize], + target_in: &'a [usize], + target_out: &'a [usize], + options: &'a MatchOptions, + trace: &'a dyn MatchTrace, +} + +impl<'a, OP, AP, O, A, FN> MatchContext<'a, OP, AP, O, A, FN> +where + FN: Fn(&OP, &O) -> bool, +{ + fn new( + target: &'a Hypergraph, + pattern: &'a Hypergraph, + node_eq: &'a FN, + edge_order: &'a [usize], + edge_candidates: &'a [Vec], + isolated_nodes: &'a [usize], + pattern_in: &'a [usize], + pattern_out: &'a [usize], + target_in: &'a [usize], + target_out: &'a [usize], + options: &'a MatchOptions, + trace: &'a dyn MatchTrace, + ) -> Self { + Self { + target, + pattern, + node_eq, + edge_order, + edge_candidates, + isolated_nodes, + pattern_in, + pattern_out, + target_in, + target_out, + options, + trace, + } + } +} + +struct MatchState { + node_map: Vec>, + edge_map: Vec>, + used_target_nodes: Vec, + used_target_edges: Vec, + pattern_mapped_in: Vec, + pattern_mapped_out: Vec, + target_mapped_in: Vec, + target_mapped_out: Vec, + next_frame_id: usize, +} + +impl MatchState { + fn new(pattern: &Hypergraph, target: &Hypergraph) -> Self { + Self { + node_map: vec![None; pattern.nodes.len()], + edge_map: vec![None; pattern.edges.len()], + used_target_nodes: vec![false; target.nodes.len()], + used_target_edges: vec![false; target.edges.len()], + pattern_mapped_in: vec![0usize; pattern.nodes.len()], + pattern_mapped_out: vec![0usize; pattern.nodes.len()], + target_mapped_in: vec![0usize; target.nodes.len()], + target_mapped_out: vec![0usize; target.nodes.len()], + next_frame_id: 0, + } + } + + fn enter_frame(&mut self, trace: &dyn MatchTrace, depth: usize) -> usize { + let frame_id = self.next_frame_id; + self.next_frame_id += 1; + trace.on_event(MatchEvent::EnterFrame { depth, frame_id }); + frame_id + } + + fn exit_frame(&self, trace: &dyn MatchTrace, depth: usize, frame_id: usize) { + trace.on_event(MatchEvent::ExitFrame { depth, frame_id }); + } + + fn commit_edge_mapping( + &mut self, + p_edge_idx: usize, + t_edge_idx: usize, + p_sources: &[NodeId], + p_targets: &[NodeId], + t_sources: &[NodeId], + t_targets: &[NodeId], + options: &MatchOptions, + ) { + // Record the edge mapping and update incremental counters if injective. + self.edge_map[p_edge_idx] = Some(EdgeId(t_edge_idx)); + if options.injective { + self.used_target_edges[t_edge_idx] = true; + add_edge_incidence( + p_sources, + p_targets, + &mut self.pattern_mapped_in, + &mut self.pattern_mapped_out, + ); + add_edge_incidence( + t_sources, + t_targets, + &mut self.target_mapped_in, + &mut self.target_mapped_out, + ); + } + } + + fn rollback_edge_mapping( + &mut self, + p_edge_idx: usize, + t_edge_idx: usize, + p_sources: &[NodeId], + p_targets: &[NodeId], + t_sources: &[NodeId], + t_targets: &[NodeId], + options: &MatchOptions, + ) { + // Undo the edge mapping and counters. + self.edge_map[p_edge_idx] = None; + if options.injective { + self.used_target_edges[t_edge_idx] = false; + remove_edge_incidence( + p_sources, + p_targets, + &mut self.pattern_mapped_in, + &mut self.pattern_mapped_out, + ); + remove_edge_incidence( + t_sources, + t_targets, + &mut self.target_mapped_in, + &mut self.target_mapped_out, + ); + } + } + + fn rollback_new_nodes(&mut self, newly_mapped: Vec, options: &MatchOptions) { + // Undo node bindings created while exploring a candidate edge. + for p_node_idx in newly_mapped { + let t_node_idx = self.node_map[p_node_idx].unwrap().0; + self.node_map[p_node_idx] = None; + if options.injective { + self.used_target_nodes[t_node_idx] = false; + } + } + } + + fn commit_edge_nodes( + &mut self, + context: &MatchContext<'_, OP, AP, O, A, FN>, + p_adj: &super::hypergraph::Hyperedge, + t_adj: &super::hypergraph::Hyperedge, + depth: usize, + ) -> Option> + where + FN: Fn(&OP, &O) -> bool, + { + let mut newly_mapped = Vec::new(); + for (p_node, t_node) in p_adj.sources.iter().zip(t_adj.sources.iter()) { + if !try_map_node( + context, + p_node.0, + t_node.0, + 0, + 1, + self, + &mut newly_mapped, + depth, + ) { + self.rollback_new_nodes(newly_mapped, context.options); + return None; + } + } + for (p_node, t_node) in p_adj.targets.iter().zip(t_adj.targets.iter()) { + if !try_map_node( + context, + p_node.0, + t_node.0, + 1, + 0, + self, + &mut newly_mapped, + depth, + ) { + self.rollback_new_nodes(newly_mapped, context.options); + return None; + } + } + Some(newly_mapped) + } +} + +fn backtrack_edges( + context: &MatchContext<'_, OP, AP, O, A, FN>, + edge_index: usize, + state: &mut MatchState, + matches: &mut Vec, +) where + FN: Fn(&OP, &O) -> bool, +{ + let frame_id = state.enter_frame(context.trace, edge_index); + // If all edges are mapped, fill in remaining isolated nodes. + if edge_index == context.edge_order.len() { + context.trace.on_event(MatchEvent::Decision { + pattern_edge: None, + pattern_node: None, + choice_features: "no_more_pattern_edges", + candidate_count: 0, + heuristic_tag: "edge_order", + depth: edge_index, + }); + backtrack_isolated_nodes(context, 0, state, matches); + state.exit_frame(context.trace, edge_index, frame_id); + return; + } + + let p_edge_idx = context.edge_order[edge_index]; + let p_adj = &context.pattern.adjacency[p_edge_idx]; + context.trace.on_event(MatchEvent::Decision { + pattern_edge: Some(p_edge_idx), + pattern_node: None, + choice_features: "edge_order", + candidate_count: context.edge_candidates[p_edge_idx].len(), + heuristic_tag: "min_candidates_then_arity", + depth: edge_index, + }); + + for &t_edge_idx in &context.edge_candidates[p_edge_idx] { + context.trace.on_event(MatchEvent::Branch { + target_edge: Some(t_edge_idx), + target_node: None, + depth: edge_index, + }); + if context.options.injective && state.used_target_edges[t_edge_idx] { + context.trace.on_event(MatchEvent::Prune { + reason: "edge_used", + detail: "injective_edge_reuse", + depth: edge_index, + }); + continue; + } + let t_adj = &context.target.adjacency[t_edge_idx]; + + let Some(newly_mapped) = state.commit_edge_nodes(context, p_adj, t_adj, edge_index) else { + context.trace.on_event(MatchEvent::Prune { + reason: "node_mapping_failed", + detail: "edge_incidence_conflict", + depth: edge_index, + }); + continue; + }; + + state.commit_edge_mapping( + p_edge_idx, + t_edge_idx, + &p_adj.sources, + &p_adj.targets, + &t_adj.sources, + &t_adj.targets, + context.options, + ); + context + .trace + .on_event(MatchEvent::PropagationSummary { depth: edge_index }); + + backtrack_edges(context, edge_index + 1, state, matches); + + state.rollback_edge_mapping( + p_edge_idx, + t_edge_idx, + &p_adj.sources, + &p_adj.targets, + &t_adj.sources, + &t_adj.targets, + context.options, + ); + + // Roll back any provisional node bindings from this edge attempt. + state.rollback_new_nodes(newly_mapped, context.options); + } + state.exit_frame(context.trace, edge_index, frame_id); +} + +fn backtrack_isolated_nodes( + context: &MatchContext<'_, OP, AP, O, A, FN>, + idx: usize, + state: &mut MatchState, + matches: &mut Vec, +) where + FN: Fn(&OP, &O) -> bool, +{ + let frame_id = state.enter_frame(context.trace, idx); + if idx == context.isolated_nodes.len() { + let node_map = state + .node_map + .iter() + .map(|node| node.expect("pattern nodes must be mapped")) + .collect(); + let edge_map = state + .edge_map + .iter() + .map(|edge| edge.expect("pattern edges must be mapped")) + .collect(); + matches.push(Morphism { node_map, edge_map }); + context.trace.on_event(MatchEvent::Solution); + state.exit_frame(context.trace, idx, frame_id); + return; + } + + let p_node_idx = context.isolated_nodes[idx]; + context.trace.on_event(MatchEvent::Decision { + pattern_edge: None, + pattern_node: Some(p_node_idx), + choice_features: "isolated_nodes", + candidate_count: context.target.nodes.len(), + heuristic_tag: "isolated_nodes_order", + depth: idx, + }); + for t_node_idx in 0..context.target.nodes.len() { + if context.options.injective && state.used_target_nodes[t_node_idx] { + context.trace.on_event(MatchEvent::Prune { + reason: "node_used", + detail: "injective_node_reuse", + depth: idx, + }); + continue; + } + if !degree_feasible(context, state, p_node_idx, t_node_idx, 0, 0) { + context.trace.on_event(MatchEvent::Prune { + reason: "degree_infeasible", + detail: "degree_capacity", + depth: idx, + }); + continue; + } + if !(context.node_eq)( + &context.pattern.nodes[p_node_idx], + &context.target.nodes[t_node_idx], + ) { + context.trace.on_event(MatchEvent::Prune { + reason: "label_mismatch", + detail: "node_label", + depth: idx, + }); + continue; + } + + state.node_map[p_node_idx] = Some(NodeId(t_node_idx)); + if context.options.injective { + state.used_target_nodes[t_node_idx] = true; + } + context.trace.on_event(MatchEvent::Branch { + target_edge: None, + target_node: Some(t_node_idx), + depth: idx, + }); + context + .trace + .on_event(MatchEvent::PropagationSummary { depth: idx }); + + backtrack_isolated_nodes(context, idx + 1, state, matches); + + if context.options.injective { + state.used_target_nodes[t_node_idx] = false; + } + state.node_map[p_node_idx] = None; + context.trace.on_event(MatchEvent::Branch { + target_edge: None, + target_node: Some(t_node_idx), + depth: idx, + }); + } + state.exit_frame(context.trace, idx, frame_id); +} + +#[allow(clippy::too_many_arguments)] +fn try_map_node( + context: &MatchContext<'_, OP, AP, O, A, FN>, + p_node_idx: usize, + t_node_idx: usize, + add_in: usize, + add_out: usize, + state: &mut MatchState, + newly_mapped: &mut Vec, + depth: usize, +) -> bool +where + FN: Fn(&OP, &O) -> bool, +{ + context.trace.on_event(MatchEvent::Decision { + pattern_edge: None, + pattern_node: Some(p_node_idx), + choice_features: "edge_incidence", + candidate_count: 1, + heuristic_tag: "edge_incidence", + depth, + }); + if let Some(existing) = state.node_map[p_node_idx] { + if existing.0 != t_node_idx { + context.trace.on_event(MatchEvent::Prune { + reason: "node_mapped_conflict", + detail: "edge_incidence_conflict", + depth, + }); + return false; + } + if context.options.injective { + return degree_feasible(context, state, p_node_idx, t_node_idx, add_in, add_out); + } + return true; + } + if context.options.injective && state.used_target_nodes[t_node_idx] { + context.trace.on_event(MatchEvent::Prune { + reason: "node_used", + detail: "injective_node_reuse", + depth, + }); + return false; + } + if !(context.node_eq)( + &context.pattern.nodes[p_node_idx], + &context.target.nodes[t_node_idx], + ) { + context.trace.on_event(MatchEvent::Prune { + reason: "label_mismatch", + detail: "node_label", + depth, + }); + return false; + } + if !degree_feasible(context, state, p_node_idx, t_node_idx, add_in, add_out) { + context.trace.on_event(MatchEvent::Prune { + reason: "degree_infeasible", + detail: "degree_capacity", + depth, + }); + return false; + } + + state.node_map[p_node_idx] = Some(NodeId(t_node_idx)); + if context.options.injective { + state.used_target_nodes[t_node_idx] = true; + } + newly_mapped.push(p_node_idx); + context.trace.on_event(MatchEvent::Branch { + target_edge: None, + target_node: Some(t_node_idx), + depth, + }); + context + .trace + .on_event(MatchEvent::PropagationSummary { depth }); + true +} + +fn node_degrees(graph: &Hypergraph) -> (Vec, Vec) { + let mut in_deg = vec![0usize; graph.nodes.len()]; + let mut out_deg = vec![0usize; graph.nodes.len()]; + for edge in &graph.adjacency { + for node in &edge.sources { + out_deg[node.0] += 1; + } + for node in &edge.targets { + in_deg[node.0] += 1; + } + } + (in_deg, out_deg) +} + +fn add_edge_incidence( + sources: &[NodeId], + targets: &[NodeId], + mapped_in: &mut [usize], + mapped_out: &mut [usize], +) { + for node in sources { + mapped_out[node.0] += 1; + } + for node in targets { + mapped_in[node.0] += 1; + } +} + +fn remove_edge_incidence( + sources: &[NodeId], + targets: &[NodeId], + mapped_in: &mut [usize], + mapped_out: &mut [usize], +) { + for node in sources { + mapped_out[node.0] -= 1; + } + for node in targets { + mapped_in[node.0] -= 1; + } +} + +fn degree_feasible( + context: &MatchContext<'_, OP, AP, O, A, FN>, + state: &MatchState, + p_node_idx: usize, + t_node_idx: usize, + add_in: usize, + add_out: usize, +) -> bool +where + FN: Fn(&OP, &O) -> bool, +{ + if !context.options.injective { + return true; + } + // Basic degree bound: a pattern node cannot map to a target node with fewer in/out edges. + if context.pattern_in[p_node_idx] > context.target_in[t_node_idx] + || context.pattern_out[p_node_idx] > context.target_out[t_node_idx] + { + return false; + } + + // Remaining incident edges on the pattern node after this tentative assignment. + let pattern_remaining_in = + context.pattern_in[p_node_idx].saturating_sub(state.pattern_mapped_in[p_node_idx] + add_in); + let pattern_remaining_out = context.pattern_out[p_node_idx] + .saturating_sub(state.pattern_mapped_out[p_node_idx] + add_out); + // Remaining capacity on the target node to host those edges. + let target_remaining_in = + context.target_in[t_node_idx].saturating_sub(state.target_mapped_in[t_node_idx] + add_in); + let target_remaining_out = context.target_out[t_node_idx] + .saturating_sub(state.target_mapped_out[t_node_idx] + add_out); + + // Feasible if the target has enough unused incident capacity to fit the pattern. + pattern_remaining_in <= target_remaining_in && pattern_remaining_out <= target_remaining_out +} diff --git a/src/lax/mod.rs b/src/lax/mod.rs index 4cf746c..ba09ce6 100644 --- a/src/lax/mod.rs +++ b/src/lax/mod.rs @@ -74,6 +74,7 @@ pub mod category; pub mod functor; pub mod hypergraph; +pub mod matching; pub mod mut_category; pub mod open_hypergraph; diff --git a/tests/lax/matching.rs b/tests/lax/matching.rs new file mode 100644 index 0000000..41d5a25 --- /dev/null +++ b/tests/lax/matching.rs @@ -0,0 +1,367 @@ +use open_hypergraphs::lax::{EdgeId, Hypergraph, NodeId}; + +fn assert_is_morphism( + target: &Hypergraph, + pattern: &Hypergraph, + morphism: &open_hypergraphs::lax::matching::Morphism, + node_eq: impl Fn(&OP, &O) -> bool, + edge_eq: impl Fn(&AP, &A) -> bool, +) { + // Node labels must be preserved. + for (p_idx, p_label) in pattern.nodes.iter().enumerate() { + let t_idx = morphism.node_map()[p_idx].0; + assert!(node_eq(p_label, &target.nodes[t_idx])); + } + + // Edge labels and incidence (ordered sources/targets) must be preserved. + for (p_edge_idx, p_label) in pattern.edges.iter().enumerate() { + let t_edge_idx = morphism.edge_map()[p_edge_idx].0; + assert!(edge_eq(p_label, &target.edges[t_edge_idx])); + + let p_adj = &pattern.adjacency[p_edge_idx]; + let t_adj = &target.adjacency[t_edge_idx]; + assert_eq!(p_adj.sources.len(), t_adj.sources.len()); + assert_eq!(p_adj.targets.len(), t_adj.targets.len()); + + for (p_node, t_node) in p_adj.sources.iter().zip(t_adj.sources.iter()) { + let mapped = morphism.node_map()[p_node.0]; + assert_eq!(mapped, *t_node); + } + for (p_node, t_node) in p_adj.targets.iter().zip(t_adj.targets.iter()) { + let mapped = morphism.node_map()[p_node.0]; + assert_eq!(mapped, *t_node); + } + } +} + +#[test] +fn test_subgraph_isomorphisms_single_edge() { + let mut target = Hypergraph::empty(); + let t0 = target.new_node(0); + let t1 = target.new_node(1); + let t2 = target.new_node(0); + target.new_edge('f', (vec![t0], vec![t1])); + target.new_edge('f', (vec![t2], vec![t1])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + pattern.new_edge('f', (vec![p0], vec![p1])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 2); + assert!(matches.iter().all(|m| m.node_map()[1] == NodeId(1))); + + let mut sources = matches + .iter() + .map(|m| m.node_map()[0].0) + .collect::>(); + sources.sort(); + assert_eq!(sources, vec![0, 2]); + + for m in matches { + assert_is_morphism(&target, &pattern, &m, |a, b| a == b, |a, b| a == b); + if m.node_map()[0] == NodeId(0) { + assert_eq!(m.edge_map()[0], EdgeId(0)); + } else { + assert_eq!(m.edge_map()[0], EdgeId(1)); + } + } +} + +#[test] +fn test_subgraph_isomorphisms_order_sensitive() { + let mut target = Hypergraph::empty(); + let t0 = target.new_node(0); + let t1 = target.new_node(1); + target.new_edge('f', (vec![t0, t1], vec![])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + pattern.new_edge('f', (vec![p1, p0], vec![])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert!(matches.is_empty()); +} + +#[test] +fn test_subgraph_isomorphisms_isolated_nodes() { + let mut target: Hypergraph = Hypergraph::empty(); + target.new_node(1); + target.new_node(2); + target.new_node(1); + + let mut pattern: Hypergraph = Hypergraph::empty(); + pattern.new_node(1); + pattern.new_node(2); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + // The pattern's 2-label must map to the unique 2 in the target; the 1-label can map to either 1. + assert_eq!(matches.len(), 2); + assert!(matches.iter().all(|m| m.node_map()[1] == NodeId(1))); + + let mut sources = matches + .iter() + .map(|m| m.node_map()[0].0) + .collect::>(); + sources.sort(); + assert_eq!(sources, vec![0, 2]); + for m in matches { + assert_is_morphism(&target, &pattern, &m, |a, b| a == b, |a, b| a == b); + } +} + +#[test] +fn test_subgraph_isomorphisms_shared_nodes() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(1); + let n2 = target.new_node(2); + target.new_edge('g', (vec![n0], vec![n1])); + target.new_edge('h', (vec![n1], vec![n2])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + let p2 = pattern.new_node(2); + pattern.new_edge('g', (vec![p0], vec![p1])); + pattern.new_edge('h', (vec![p1], vec![p2])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 1); +} + +#[test] +fn test_subgraph_isomorphisms_arity_mismatch() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(1); + target.new_edge('f', (vec![n0], vec![n1])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + let p2 = pattern.new_node(2); + pattern.new_edge('f', (vec![p0, p1], vec![p2])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert!(matches.is_empty()); +} + +#[test] +fn test_subgraph_isomorphisms_degree_feasible_prune() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(1); + target.new_edge('a', (vec![n0], vec![n1])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + let p2 = pattern.new_node(2); + pattern.new_edge('a', (vec![p0], vec![p1])); + pattern.new_edge('b', (vec![p0], vec![p2])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert!(matches.is_empty()); +} + +#[test] +fn test_subgraph_isomorphisms_empty_pattern() { + let mut target = Hypergraph::empty(); + target.new_node(1); + target.new_node(2); + + let pattern: Hypergraph = Hypergraph::empty(); + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 1); + assert!(matches[0].node_map().is_empty()); + assert!(matches[0].edge_map().is_empty()); +} + +#[test] +fn test_subgraph_isomorphisms_multi_incidence_sources() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(1); + target.new_edge('f', (vec![n0, n0], vec![n1])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + pattern.new_edge('f', (vec![p0, p0], vec![p1])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 1); + assert_is_morphism(&target, &pattern, &matches[0], |a, b| a == b, |a, b| a == b); + assert_eq!(matches[0].node_map()[0], n0); + assert_eq!(matches[0].node_map()[1], n1); + assert_eq!(matches[0].edge_map()[0], EdgeId(0)); +} + +#[test] +fn test_subgraph_isomorphisms_multiple_matches_complex_target() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(0); + let n2 = target.new_node(1); + let n3 = target.new_node(1); + let n4 = target.new_node(2); + target.new_edge('f', (vec![n0], vec![n2])); + target.new_edge('f', (vec![n0], vec![n3])); + target.new_edge('f', (vec![n1], vec![n2])); + target.new_edge('f', (vec![n1], vec![n3])); + target.new_edge('g', (vec![n2], vec![n4])); + target.new_edge('g', (vec![n3], vec![n4])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + let p2 = pattern.new_node(2); + pattern.new_edge('f', (vec![p0], vec![p1])); + pattern.new_edge('g', (vec![p1], vec![p2])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 4); + assert!(matches.iter().all(|m| m.node_map()[2] == n4)); +} + +#[test] +fn test_subgraph_isomorphisms_node_in_sources_and_targets() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + target.new_edge('g', (vec![n0], vec![n0])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + pattern.new_edge('g', (vec![p0], vec![p0])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 1); + assert_is_morphism(&target, &pattern, &matches[0], |a, b| a == b, |a, b| a == b); + assert_eq!(matches[0].node_map()[0], n0); + assert_eq!(matches[0].edge_map()[0], EdgeId(0)); +} + +#[test] +fn test_subgraph_isomorphisms_identical_edges_injective() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(1); + target.new_edge('h', (vec![n0], vec![n1])); + target.new_edge('h', (vec![n0], vec![n1])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + pattern.new_edge('h', (vec![p0], vec![p1])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 2); + let mut edge_ids = matches + .iter() + .map(|m| m.edge_map()[0].0) + .collect::>(); + edge_ids.sort(); + assert_eq!(edge_ids, vec![0, 1]); + for m in matches { + assert_is_morphism(&target, &pattern, &m, |a, b| a == b, |a, b| a == b); + } +} + +#[test] +fn test_subgraph_isomorphisms_two_identical_edges_bijective() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(1); + target.new_edge('h', (vec![n0], vec![n1])); + target.new_edge('h', (vec![n0], vec![n1])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(1); + pattern.new_edge('h', (vec![p0], vec![p1])); + pattern.new_edge('h', (vec![p0], vec![p1])); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 2); + let mut edge_maps = matches + .iter() + .map(|m| (m.edge_map()[0].0, m.edge_map()[1].0)) + .collect::>(); + edge_maps.sort(); + assert_eq!(edge_maps, vec![(0, 1), (1, 0)]); + for m in matches { + assert_is_morphism(&target, &pattern, &m, |a, b| a == b, |a, b| a == b); + } +} + +#[test] +fn test_subgraph_isomorphisms_isolated_nodes_duplicate_labels() { + let mut target: Hypergraph = Hypergraph::empty(); + target.new_node(1); + target.new_node(1); + target.new_node(1); + + let mut pattern: Hypergraph = Hypergraph::empty(); + pattern.new_node(1); + pattern.new_node(1); + + let matches = target.find_subgraph_isomorphisms(&pattern, None); + assert_eq!(matches.len(), 6); +} + +#[test] +fn test_subgraph_homomorphisms_allow_node_merging() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + target.new_edge('f', (vec![n0], vec![n0])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(0); + pattern.new_edge('f', (vec![p0], vec![p1])); + + let iso_matches = target.find_subgraph_isomorphisms(&pattern, None); + assert!(iso_matches.is_empty()); + + let homo_matches = target.find_subgraph_homomorphisms(&pattern, None); + assert_eq!(homo_matches.len(), 1); + assert_eq!(homo_matches[0].node_map()[0], n0); + assert_eq!(homo_matches[0].node_map()[1], n0); + assert_is_morphism( + &target, + &pattern, + &homo_matches[0], + |a, b| a == b, + |a, b| a == b, + ); +} + +#[test] +fn test_subgraph_homomorphisms_allow_edge_merging() { + let mut target = Hypergraph::empty(); + let n0 = target.new_node(0); + let n1 = target.new_node(0); + target.new_edge('h', (vec![n0], vec![n1])); + + let mut pattern = Hypergraph::empty(); + let p0 = pattern.new_node(0); + let p1 = pattern.new_node(0); + pattern.new_edge('h', (vec![p0], vec![p1])); + pattern.new_edge('h', (vec![p0], vec![p1])); + + let iso_matches = target.find_subgraph_isomorphisms(&pattern, None); + assert!(iso_matches.is_empty()); + + let homo_matches = target.find_subgraph_homomorphisms(&pattern, None); + assert_eq!(homo_matches.len(), 1); + assert_is_morphism( + &target, + &pattern, + &homo_matches[0], + |a, b| a == b, + |a, b| a == b, + ); +} diff --git a/tests/lax/mod.rs b/tests/lax/mod.rs index 2e1d7c6..8b295a7 100644 --- a/tests/lax/mod.rs +++ b/tests/lax/mod.rs @@ -1,3 +1,4 @@ pub mod eval; pub mod hypergraph; +pub mod matching; pub mod open_hypergraph;