1use crate::graph_types::GraphSubtypeEntry;
12use crate::rules::cost::PathCostFn;
13use crate::rules::registry::{ReductionEntry, ReductionOverhead};
14use crate::types::ProblemSize;
15use ordered_float::OrderedFloat;
16use petgraph::algo::all_simple_paths;
17use petgraph::graph::{DiGraph, NodeIndex};
18use petgraph::visit::EdgeRef;
19use serde::Serialize;
20use std::any::TypeId;
21use std::cmp::Reverse;
22use std::collections::{BinaryHeap, HashMap, HashSet};
23
24#[derive(Debug, Clone, Serialize)]
26pub struct ReductionGraphJson {
27 pub nodes: Vec<NodeJson>,
29 pub edges: Vec<EdgeJson>,
31}
32
33#[derive(Debug, Clone, Serialize)]
35pub struct NodeJson {
36 pub id: String,
38 pub label: String,
40 pub category: String,
42}
43
44#[derive(Debug, Clone, Serialize)]
46pub struct EdgeJson {
47 pub source: String,
49 pub target: String,
51 pub bidirectional: bool,
53}
54
55#[derive(Debug, Clone)]
57pub struct ReductionPath {
58 pub type_names: Vec<&'static str>,
60}
61
62impl ReductionPath {
63 pub fn len(&self) -> usize {
65 if self.type_names.is_empty() {
66 0
67 } else {
68 self.type_names.len() - 1
69 }
70 }
71
72 pub fn is_empty(&self) -> bool {
74 self.type_names.is_empty()
75 }
76
77 pub fn source(&self) -> Option<&'static str> {
79 self.type_names.first().copied()
80 }
81
82 pub fn target(&self) -> Option<&'static str> {
84 self.type_names.last().copied()
85 }
86}
87
88#[derive(Clone, Debug)]
90pub struct ReductionEdge {
91 pub source_graph: &'static str,
93 pub target_graph: &'static str,
95 pub overhead: ReductionOverhead,
97}
98
99pub struct ReductionGraph {
111 graph: DiGraph<&'static str, ReductionEdge>,
113 name_indices: HashMap<&'static str, NodeIndex>,
115 type_to_name: HashMap<TypeId, &'static str>,
117 graph_hierarchy: HashMap<&'static str, HashSet<&'static str>>,
119}
120
121impl ReductionGraph {
122 pub fn new() -> Self {
124 let mut graph = DiGraph::new();
125 let mut name_indices = HashMap::new();
126 let mut type_to_name = HashMap::new();
127
128 let graph_hierarchy = Self::build_graph_hierarchy();
130
131 Self::register_types(&mut graph, &mut name_indices, &mut type_to_name);
133
134 for entry in inventory::iter::<ReductionEntry> {
136 if !name_indices.contains_key(entry.source_name) {
138 let idx = graph.add_node(entry.source_name);
139 name_indices.insert(entry.source_name, idx);
140 }
141 if !name_indices.contains_key(entry.target_name) {
143 let idx = graph.add_node(entry.target_name);
144 name_indices.insert(entry.target_name, idx);
145 }
146
147 let src = name_indices[entry.source_name];
149 let dst = name_indices[entry.target_name];
150
151 if graph.find_edge(src, dst).is_none() {
153 graph.add_edge(
154 src,
155 dst,
156 ReductionEdge {
157 source_graph: entry.source_graph,
158 target_graph: entry.target_graph,
159 overhead: entry.overhead(),
160 },
161 );
162 }
163 }
164
165 Self::register_reductions(&mut graph, &name_indices);
167
168 Self {
169 graph,
170 name_indices,
171 type_to_name,
172 graph_hierarchy,
173 }
174 }
175
176 fn build_graph_hierarchy() -> HashMap<&'static str, HashSet<&'static str>> {
179 let mut supertypes: HashMap<&'static str, HashSet<&'static str>> = HashMap::new();
180
181 for entry in inventory::iter::<GraphSubtypeEntry> {
183 supertypes.entry(entry.subtype).or_default().insert(entry.supertype);
184 }
185
186 loop {
188 let mut changed = false;
189 let types: Vec<_> = supertypes.keys().copied().collect();
190
191 for sub in &types {
192 let current: Vec<_> = supertypes
193 .get(sub)
194 .map(|s| s.iter().copied().collect())
195 .unwrap_or_default();
196
197 for sup in current {
198 if let Some(sup_supers) = supertypes.get(sup).cloned() {
199 for ss in sup_supers {
200 if supertypes.entry(sub).or_default().insert(ss) {
201 changed = true;
202 }
203 }
204 }
205 }
206 }
207
208 if !changed {
209 break;
210 }
211 }
212
213 supertypes
214 }
215
216 fn register_types(
217 graph: &mut DiGraph<&'static str, ReductionEdge>,
218 name_indices: &mut HashMap<&'static str, NodeIndex>,
219 type_to_name: &mut HashMap<TypeId, &'static str>,
220 ) {
221 macro_rules! register {
224 ($($ty:ty => $base_name:expr),* $(,)?) => {
225 $(
226 type_to_name.insert(TypeId::of::<$ty>(), $base_name);
228
229 if !name_indices.contains_key($base_name) {
231 let idx = graph.add_node($base_name);
232 name_indices.insert($base_name, idx);
233 }
234 )*
235 };
236 }
237
238 use crate::models::graph::*;
239 use crate::models::optimization::*;
240 use crate::models::satisfiability::*;
241 use crate::models::set::*;
242 use crate::models::specialized::*;
243
244 register! {
246 IndependentSet<i32> => "IndependentSet",
248 IndependentSet<f64> => "IndependentSet",
249 VertexCovering<i32> => "VertexCovering",
250 VertexCovering<f64> => "VertexCovering",
251 MaxCut<i32> => "MaxCut",
252 MaxCut<f64> => "MaxCut",
253 Matching<i32> => "Matching",
254 DominatingSet<i32> => "DominatingSet",
255 Coloring => "Coloring",
256 SetPacking<i32> => "SetPacking",
258 SetCovering<i32> => "SetCovering",
259 SpinGlass<i32> => "SpinGlass",
261 SpinGlass<f64> => "SpinGlass",
262 QUBO<f64> => "QUBO",
263 Satisfiability<i32> => "Satisfiability",
265 KSatisfiability<3, i32> => "KSatisfiability",
266 CircuitSAT<i32> => "CircuitSAT",
267 Factoring => "Factoring",
269 }
270 }
271
272 fn register_reductions(
273 graph: &mut DiGraph<&'static str, ReductionEdge>,
274 name_indices: &HashMap<&'static str, NodeIndex>,
275 ) {
276 macro_rules! add_edge {
279 ($src:expr => $dst:expr) => {
280 if let (Some(&src), Some(&dst)) = (name_indices.get($src), name_indices.get($dst)) {
281 if graph.find_edge(src, dst).is_none() {
283 graph.add_edge(
284 src,
285 dst,
286 ReductionEdge {
287 source_graph: "SimpleGraph",
288 target_graph: "SimpleGraph",
289 overhead: ReductionOverhead::default(),
290 },
291 );
292 }
293 }
294 };
295 }
296
297 add_edge!("IndependentSet" => "VertexCovering");
299 add_edge!("VertexCovering" => "IndependentSet");
300 add_edge!("IndependentSet" => "SetPacking");
301 add_edge!("SetPacking" => "IndependentSet");
302 add_edge!("VertexCovering" => "SetCovering");
303 add_edge!("Matching" => "SetPacking");
304
305 add_edge!("SpinGlass" => "QUBO");
307 add_edge!("QUBO" => "SpinGlass");
308 add_edge!("MaxCut" => "SpinGlass");
309 add_edge!("SpinGlass" => "MaxCut");
310
311 add_edge!("Satisfiability" => "KSatisfiability");
313 add_edge!("KSatisfiability" => "Satisfiability");
314 add_edge!("Satisfiability" => "IndependentSet");
315 add_edge!("Satisfiability" => "Coloring");
316 add_edge!("Satisfiability" => "DominatingSet");
317
318 add_edge!("CircuitSAT" => "SpinGlass");
320 add_edge!("Factoring" => "CircuitSAT");
321 }
322
323 pub fn is_graph_subtype(&self, sub: &str, sup: &str) -> bool {
325 sub == sup
326 || self
327 .graph_hierarchy
328 .get(sub)
329 .map(|s| s.contains(sup))
330 .unwrap_or(false)
331 }
332
333 pub fn rule_applicable(
342 &self,
343 want_source_graph: &str,
344 want_target_graph: &str,
345 rule_source_graph: &str,
346 rule_target_graph: &str,
347 ) -> bool {
348 self.is_graph_subtype(want_source_graph, rule_source_graph)
351 && self.is_graph_subtype(rule_target_graph, want_target_graph)
352 }
353
354 pub fn find_cheapest_path<C: PathCostFn>(
367 &self,
368 source: (&str, &str),
369 target: (&str, &str),
370 input_size: &ProblemSize,
371 cost_fn: &C,
372 ) -> Option<ReductionPath> {
373 let src_idx = *self.name_indices.get(source.0)?;
374 let dst_idx = *self.name_indices.get(target.0)?;
375
376 let mut costs: HashMap<NodeIndex, f64> = HashMap::new();
377 let mut sizes: HashMap<NodeIndex, ProblemSize> = HashMap::new();
378 let mut prev: HashMap<NodeIndex, (NodeIndex, petgraph::graph::EdgeIndex)> = HashMap::new();
379 let mut heap = BinaryHeap::new();
380
381 costs.insert(src_idx, 0.0);
382 sizes.insert(src_idx, input_size.clone());
383 heap.push(Reverse((OrderedFloat(0.0), src_idx)));
384
385 while let Some(Reverse((cost, node))) = heap.pop() {
386 if node == dst_idx {
387 return Some(self.reconstruct_path(&prev, src_idx, dst_idx));
388 }
389
390 if cost.0 > *costs.get(&node).unwrap_or(&f64::INFINITY) {
391 continue;
392 }
393
394 let current_size = match sizes.get(&node) {
395 Some(s) => s.clone(),
396 None => continue,
397 };
398
399 for edge_ref in self.graph.edges(node) {
400 let edge = edge_ref.weight();
401 let next = edge_ref.target();
402
403 if !self.rule_applicable(source.1, target.1, edge.source_graph, edge.target_graph) {
405 continue;
406 }
407
408 let edge_cost = cost_fn.edge_cost(&edge.overhead, ¤t_size);
409 let new_cost = cost.0 + edge_cost;
410 let new_size = edge.overhead.evaluate_output_size(¤t_size);
411
412 if new_cost < *costs.get(&next).unwrap_or(&f64::INFINITY) {
413 costs.insert(next, new_cost);
414 sizes.insert(next, new_size);
415 prev.insert(next, (node, edge_ref.id()));
416 heap.push(Reverse((OrderedFloat(new_cost), next)));
417 }
418 }
419 }
420
421 None
422 }
423
424 fn reconstruct_path(
426 &self,
427 prev: &HashMap<NodeIndex, (NodeIndex, petgraph::graph::EdgeIndex)>,
428 src: NodeIndex,
429 dst: NodeIndex,
430 ) -> ReductionPath {
431 let mut path = vec![self.graph[dst]];
432 let mut current = dst;
433
434 while current != src {
435 if let Some(&(prev_node, _)) = prev.get(¤t) {
436 path.push(self.graph[prev_node]);
437 current = prev_node;
438 } else {
439 break;
440 }
441 }
442
443 path.reverse();
444 ReductionPath { type_names: path }
445 }
446
447 pub fn find_paths<S: 'static, T: 'static>(&self) -> Vec<ReductionPath> {
452 let src_name = match self.type_to_name.get(&TypeId::of::<S>()) {
453 Some(&name) => name,
454 None => return vec![],
455 };
456 let dst_name = match self.type_to_name.get(&TypeId::of::<T>()) {
457 Some(&name) => name,
458 None => return vec![],
459 };
460
461 self.find_paths_by_name(src_name, dst_name)
462 }
463
464 pub fn find_paths_by_name(&self, src: &str, dst: &str) -> Vec<ReductionPath> {
466 let src_idx = match self.name_indices.get(src) {
467 Some(&idx) => idx,
468 None => return vec![],
469 };
470 let dst_idx = match self.name_indices.get(dst) {
471 Some(&idx) => idx,
472 None => return vec![],
473 };
474
475 let paths: Vec<Vec<NodeIndex>> =
476 all_simple_paths(&self.graph, src_idx, dst_idx, 0, None).collect();
477
478 paths
479 .into_iter()
480 .map(|path| {
481 let type_names: Vec<&'static str> =
482 path.iter().map(|&idx| self.graph[idx]).collect();
483 ReductionPath { type_names }
484 })
485 .collect()
486 }
487
488 pub fn find_shortest_path<S: 'static, T: 'static>(&self) -> Option<ReductionPath> {
490 let paths = self.find_paths::<S, T>();
491 paths.into_iter().min_by_key(|p| p.len())
492 }
493
494 pub fn find_shortest_path_by_name(&self, src: &str, dst: &str) -> Option<ReductionPath> {
496 let paths = self.find_paths_by_name(src, dst);
497 paths.into_iter().min_by_key(|p| p.len())
498 }
499
500 pub fn has_direct_reduction<S: 'static, T: 'static>(&self) -> bool {
502 let src_name = match self.type_to_name.get(&TypeId::of::<S>()) {
503 Some(&name) => name,
504 None => return false,
505 };
506 let dst_name = match self.type_to_name.get(&TypeId::of::<T>()) {
507 Some(&name) => name,
508 None => return false,
509 };
510
511 self.has_direct_reduction_by_name(src_name, dst_name)
512 }
513
514 pub fn has_direct_reduction_by_name(&self, src: &str, dst: &str) -> bool {
516 if let (Some(&src_idx), Some(&dst_idx)) =
517 (self.name_indices.get(src), self.name_indices.get(dst))
518 {
519 self.graph.find_edge(src_idx, dst_idx).is_some()
520 } else {
521 false
522 }
523 }
524
525 pub fn problem_types(&self) -> Vec<&'static str> {
527 self.name_indices.keys().copied().collect()
528 }
529
530 pub fn num_types(&self) -> usize {
532 self.name_indices.len()
533 }
534
535 pub fn num_reductions(&self) -> usize {
537 self.graph.edge_count()
538 }
539
540 pub fn graph_hierarchy(&self) -> &HashMap<&'static str, HashSet<&'static str>> {
542 &self.graph_hierarchy
543 }
544}
545
546impl Default for ReductionGraph {
547 fn default() -> Self {
548 Self::new()
549 }
550}
551
552impl ReductionGraph {
553 pub fn to_json(&self) -> ReductionGraphJson {
555 let mut edge_set: HashMap<(&str, &str), bool> = HashMap::new();
557
558 for edge in self.graph.edge_indices() {
559 if let Some((src_idx, dst_idx)) = self.graph.edge_endpoints(edge) {
560 let src_name = self.graph[src_idx];
561 let dst_name = self.graph[dst_idx];
562
563 let reverse_key = (dst_name, src_name);
565 if edge_set.contains_key(&reverse_key) {
566 edge_set.insert(reverse_key, true);
568 } else {
569 edge_set.insert((src_name, dst_name), false);
570 }
571 }
572 }
573
574 let nodes: Vec<NodeJson> = self
576 .name_indices
577 .keys()
578 .map(|&name| {
579 let category = Self::categorize_type(name);
580 NodeJson {
581 id: name.to_string(),
582 label: name.to_string(), category: category.to_string(),
584 }
585 })
586 .collect();
587
588 let edges: Vec<EdgeJson> = edge_set
590 .into_iter()
591 .map(|((src, dst), bidirectional)| EdgeJson {
592 source: src.to_string(),
593 target: dst.to_string(),
594 bidirectional,
595 })
596 .collect();
597
598 ReductionGraphJson { nodes, edges }
599 }
600
601 pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
603 let json = self.to_json();
604 serde_json::to_string_pretty(&json)
605 }
606
607 pub fn to_json_file(&self, path: &std::path::Path) -> std::io::Result<()> {
609 let json_string = self
610 .to_json_string()
611 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
612 std::fs::write(path, json_string)
613 }
614
615 fn categorize_type(name: &str) -> &'static str {
617 if name.contains("IndependentSet")
618 || name.contains("VertexCover")
619 || name.contains("MaxCut")
620 || name.contains("Coloring")
621 || name.contains("DominatingSet")
622 || name.contains("Matching")
623 {
624 "graph"
625 } else if name.contains("SetPacking") || name.contains("SetCover") {
626 "set"
627 } else if name.contains("SpinGlass") || name.contains("QUBO") {
628 "optimization"
629 } else if name.contains("Satisfiability") || name.contains("SAT") {
630 "satisfiability"
631 } else if name.contains("Factoring") || name.contains("Circuit") {
632 "specialized"
633 } else {
634 "other"
635 }
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642 use crate::models::graph::{IndependentSet, VertexCovering};
643 use crate::models::set::SetPacking;
644 use crate::rules::cost::MinimizeSteps;
645
646 #[test]
647 fn test_find_direct_path() {
648 let graph = ReductionGraph::new();
649 let paths = graph.find_paths::<IndependentSet<i32>, VertexCovering<i32>>();
650 assert!(!paths.is_empty());
651 assert_eq!(paths[0].type_names.len(), 2);
652 assert_eq!(paths[0].len(), 1); }
654
655 #[test]
656 fn test_find_indirect_path() {
657 let graph = ReductionGraph::new();
658 let paths = graph.find_paths::<IndependentSet<i32>, SetPacking<i32>>();
660 assert!(!paths.is_empty());
661 }
662
663 #[test]
664 fn test_find_shortest_path() {
665 let graph = ReductionGraph::new();
666 let path = graph.find_shortest_path::<IndependentSet<i32>, SetPacking<i32>>();
667 assert!(path.is_some());
668 let path = path.unwrap();
669 assert_eq!(path.len(), 1); }
671
672 #[test]
673 fn test_has_direct_reduction() {
674 let graph = ReductionGraph::new();
675 assert!(graph.has_direct_reduction::<IndependentSet<i32>, VertexCovering<i32>>());
676 assert!(graph.has_direct_reduction::<VertexCovering<i32>, IndependentSet<i32>>());
677 }
678
679 #[test]
680 fn test_no_path() {
681 let graph = ReductionGraph::new();
682 let paths =
684 graph.find_paths::<IndependentSet<i32>, crate::models::optimization::QUBO<f64>>();
685 assert!(paths.is_empty());
686 }
687
688 #[test]
689 fn test_type_erased_paths() {
690 let graph = ReductionGraph::new();
691
692 let paths_i32 = graph.find_paths::<
694 crate::models::graph::MaxCut<i32>,
695 crate::models::optimization::SpinGlass<i32>,
696 >();
697 let paths_f64 = graph.find_paths::<
698 crate::models::graph::MaxCut<f64>,
699 crate::models::optimization::SpinGlass<f64>,
700 >();
701
702 assert!(!paths_i32.is_empty());
704 assert!(!paths_f64.is_empty());
705 assert_eq!(paths_i32[0].type_names, paths_f64[0].type_names);
706 }
707
708 #[test]
709 fn test_find_paths_by_name() {
710 let graph = ReductionGraph::new();
711
712 let paths = graph.find_paths_by_name("MaxCut", "SpinGlass");
713 assert!(!paths.is_empty());
714 assert_eq!(paths[0].len(), 1); let paths = graph.find_paths_by_name("Factoring", "SpinGlass");
717 assert!(!paths.is_empty());
718 assert_eq!(paths[0].len(), 2); }
720
721 #[test]
722 fn test_problem_types() {
723 let graph = ReductionGraph::new();
724 let types = graph.problem_types();
725 assert!(types.len() >= 5);
726 assert!(types.iter().any(|t| t.contains("IndependentSet")));
727 assert!(types.iter().any(|t| t.contains("VertexCovering")));
728 }
729
730 #[test]
731 fn test_graph_statistics() {
732 let graph = ReductionGraph::new();
733 assert!(graph.num_types() >= 5);
734 assert!(graph.num_reductions() >= 6);
735 }
736
737 #[test]
738 fn test_reduction_path_methods() {
739 let graph = ReductionGraph::new();
740 let path = graph
741 .find_shortest_path::<IndependentSet<i32>, VertexCovering<i32>>()
742 .unwrap();
743
744 assert!(!path.is_empty());
745 assert!(path.source().unwrap().contains("IndependentSet"));
746 assert!(path.target().unwrap().contains("VertexCovering"));
747 }
748
749 #[test]
750 fn test_bidirectional_paths() {
751 let graph = ReductionGraph::new();
752
753 let forward = graph.find_paths::<IndependentSet<i32>, VertexCovering<i32>>();
755 assert!(!forward.is_empty());
756
757 let backward = graph.find_paths::<VertexCovering<i32>, IndependentSet<i32>>();
759 assert!(!backward.is_empty());
760 }
761
762 #[test]
763 fn test_to_json() {
764 let graph = ReductionGraph::new();
765 let json = graph.to_json();
766
767 assert!(json.nodes.len() >= 10);
769 assert!(json.nodes.iter().any(|n| n.label == "IndependentSet"));
770 assert!(json.nodes.iter().any(|n| n.category == "graph"));
771 assert!(json.nodes.iter().any(|n| n.category == "optimization"));
772
773 assert!(json.edges.len() >= 10);
775
776 let is_vc_edge = json.edges.iter().find(|e| {
778 (e.source.contains("IndependentSet") && e.target.contains("VertexCovering"))
779 || (e.source.contains("VertexCovering") && e.target.contains("IndependentSet"))
780 });
781 assert!(is_vc_edge.is_some());
782 assert!(is_vc_edge.unwrap().bidirectional);
783 }
784
785 #[test]
786 fn test_to_json_string() {
787 let graph = ReductionGraph::new();
788 let json_string = graph.to_json_string().unwrap();
789
790 assert!(json_string.contains("\"nodes\""));
792 assert!(json_string.contains("\"edges\""));
793 assert!(json_string.contains("IndependentSet"));
794 assert!(json_string.contains("\"category\""));
795 assert!(json_string.contains("\"bidirectional\""));
796 }
797
798 #[test]
799 fn test_categorize_type() {
800 assert_eq!(
802 ReductionGraph::categorize_type("IndependentSet<i32>"),
803 "graph"
804 );
805 assert_eq!(
806 ReductionGraph::categorize_type("VertexCovering<i32>"),
807 "graph"
808 );
809 assert_eq!(ReductionGraph::categorize_type("MaxCut<i32>"), "graph");
810 assert_eq!(ReductionGraph::categorize_type("Coloring"), "graph");
811 assert_eq!(
812 ReductionGraph::categorize_type("DominatingSet<i32>"),
813 "graph"
814 );
815 assert_eq!(ReductionGraph::categorize_type("Matching<i32>"), "graph");
816
817 assert_eq!(ReductionGraph::categorize_type("SetPacking<i32>"), "set");
819 assert_eq!(ReductionGraph::categorize_type("SetCovering<i32>"), "set");
820
821 assert_eq!(
823 ReductionGraph::categorize_type("SpinGlass<i32>"),
824 "optimization"
825 );
826 assert_eq!(ReductionGraph::categorize_type("QUBO<f64>"), "optimization");
827
828 assert_eq!(
830 ReductionGraph::categorize_type("Satisfiability<i32>"),
831 "satisfiability"
832 );
833 assert_eq!(
834 ReductionGraph::categorize_type("KSatisfiability<3, i32>"),
835 "satisfiability"
836 );
837 assert_eq!(
838 ReductionGraph::categorize_type("CircuitSAT<i32>"),
839 "satisfiability"
840 );
841
842 assert_eq!(ReductionGraph::categorize_type("Factoring"), "specialized");
844
845 assert_eq!(ReductionGraph::categorize_type("UnknownProblem"), "other");
847 }
848
849 #[test]
850 fn test_sat_based_reductions() {
851 use crate::models::graph::Coloring;
852 use crate::models::graph::DominatingSet;
853 use crate::models::satisfiability::Satisfiability;
854
855 let graph = ReductionGraph::new();
856
857 assert!(graph.has_direct_reduction::<Satisfiability<i32>, IndependentSet<i32>>());
859
860 assert!(graph.has_direct_reduction::<Satisfiability<i32>, Coloring>());
862
863 assert!(graph.has_direct_reduction::<Satisfiability<i32>, DominatingSet<i32>>());
865 }
866
867 #[test]
868 fn test_circuit_reductions() {
869 use crate::models::optimization::SpinGlass;
870 use crate::models::specialized::{CircuitSAT, Factoring};
871
872 let graph = ReductionGraph::new();
873
874 assert!(graph.has_direct_reduction::<Factoring, CircuitSAT<i32>>());
876
877 assert!(graph.has_direct_reduction::<CircuitSAT<i32>, SpinGlass<f64>>());
879
880 let paths = graph.find_paths::<Factoring, SpinGlass<f64>>();
882 assert!(!paths.is_empty());
883 let shortest = graph
884 .find_shortest_path::<Factoring, SpinGlass<f64>>()
885 .unwrap();
886 assert_eq!(shortest.len(), 2); }
888
889 #[test]
890 fn test_optimization_reductions() {
891 use crate::models::graph::MaxCut;
892 use crate::models::optimization::{SpinGlass, QUBO};
893
894 let graph = ReductionGraph::new();
895
896 assert!(graph.has_direct_reduction::<SpinGlass<f64>, QUBO<f64>>());
898 assert!(graph.has_direct_reduction::<QUBO<f64>, SpinGlass<f64>>());
899
900 assert!(graph.has_direct_reduction::<MaxCut<i32>, SpinGlass<f64>>());
902 assert!(graph.has_direct_reduction::<SpinGlass<f64>, MaxCut<i32>>());
903 }
904
905 #[test]
906 fn test_ksat_reductions() {
907 use crate::models::satisfiability::{KSatisfiability, Satisfiability};
908
909 let graph = ReductionGraph::new();
910
911 assert!(graph.has_direct_reduction::<Satisfiability<i32>, KSatisfiability<3, i32>>());
913 assert!(graph.has_direct_reduction::<KSatisfiability<3, i32>, Satisfiability<i32>>());
914 }
915
916 #[test]
917 fn test_all_categories_present() {
918 let graph = ReductionGraph::new();
919 let json = graph.to_json();
920
921 let categories: std::collections::HashSet<&str> =
922 json.nodes.iter().map(|n| n.category.as_str()).collect();
923
924 assert!(categories.contains("graph"));
925 assert!(categories.contains("set"));
926 assert!(categories.contains("optimization"));
927 assert!(categories.contains("satisfiability"));
928 assert!(categories.contains("specialized"));
929 }
930
931 #[test]
932 fn test_empty_path_source_target() {
933 let path = ReductionPath { type_names: vec![] };
934 assert!(path.is_empty());
935 assert_eq!(path.len(), 0);
936 assert!(path.source().is_none());
937 assert!(path.target().is_none());
938 }
939
940 #[test]
941 fn test_single_node_path() {
942 let path = ReductionPath {
943 type_names: vec!["IndependentSet"],
944 };
945 assert!(!path.is_empty());
946 assert_eq!(path.len(), 0); assert_eq!(path.source(), Some("IndependentSet"));
948 assert_eq!(path.target(), Some("IndependentSet"));
949 }
950
951 #[test]
952 fn test_default_implementation() {
953 let graph1 = ReductionGraph::new();
954 let graph2 = ReductionGraph::default();
955
956 assert_eq!(graph1.num_types(), graph2.num_types());
957 assert_eq!(graph1.num_reductions(), graph2.num_reductions());
958 }
959
960 #[test]
961 fn test_to_json_file() {
962 use std::env;
963 use std::fs;
964
965 let graph = ReductionGraph::new();
966 let file_path = env::temp_dir().join("problemreductions_test_graph.json");
967
968 graph.to_json_file(&file_path).unwrap();
970
971 let content = fs::read_to_string(&file_path).unwrap();
973 assert!(content.contains("\"nodes\""));
974 assert!(content.contains("\"edges\""));
975 assert!(content.contains("IndependentSet"));
976
977 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
979 assert!(!parsed["nodes"].as_array().unwrap().is_empty());
980 assert!(!parsed["edges"].as_array().unwrap().is_empty());
981
982 let _ = fs::remove_file(&file_path);
984 }
985
986 #[test]
987 fn test_has_direct_reduction_unregistered_types() {
988 struct UnregisteredType;
990
991 let graph = ReductionGraph::new();
992
993 assert!(!graph.has_direct_reduction::<UnregisteredType, IndependentSet<i32>>());
995
996 assert!(!graph.has_direct_reduction::<IndependentSet<i32>, UnregisteredType>());
998
999 assert!(!graph.has_direct_reduction::<UnregisteredType, UnregisteredType>());
1001 }
1002
1003 #[test]
1004 fn test_find_paths_unregistered_source() {
1005 struct UnregisteredType;
1006
1007 let graph = ReductionGraph::new();
1008 let paths = graph.find_paths::<UnregisteredType, IndependentSet<i32>>();
1009 assert!(paths.is_empty());
1010 }
1011
1012 #[test]
1013 fn test_find_paths_unregistered_target() {
1014 struct UnregisteredType;
1015
1016 let graph = ReductionGraph::new();
1017 let paths = graph.find_paths::<IndependentSet<i32>, UnregisteredType>();
1018 assert!(paths.is_empty());
1019 }
1020
1021 #[test]
1022 fn test_find_shortest_path_no_path() {
1023 struct UnregisteredType;
1024
1025 let graph = ReductionGraph::new();
1026 let path = graph.find_shortest_path::<UnregisteredType, IndependentSet<i32>>();
1027 assert!(path.is_none());
1028 }
1029
1030 #[test]
1031 fn test_categorize_circuit_as_specialized() {
1032 assert_eq!(
1034 ReductionGraph::categorize_type("CircuitSAT<i32>"),
1035 "satisfiability"
1036 );
1037 }
1040
1041 #[test]
1042 fn test_edge_bidirectionality_detection() {
1043 let graph = ReductionGraph::new();
1044 let json = graph.to_json();
1045
1046 let bidirectional_count = json.edges.iter().filter(|e| e.bidirectional).count();
1048 let unidirectional_count = json.edges.iter().filter(|e| !e.bidirectional).count();
1049
1050 assert!(bidirectional_count > 0, "Should have bidirectional edges");
1052 assert!(unidirectional_count > 0, "Should have unidirectional edges");
1053
1054 let is_vc_bidir = json.edges.iter().any(|e| {
1056 (e.source.contains("IndependentSet") && e.target.contains("VertexCovering")
1057 || e.source.contains("VertexCovering") && e.target.contains("IndependentSet"))
1058 && e.bidirectional
1059 });
1060 assert!(is_vc_bidir, "IS <-> VC should be bidirectional");
1061
1062 let factoring_circuit_unidir = json.edges.iter().any(|e| {
1064 e.source.contains("Factoring")
1065 && e.target.contains("CircuitSAT")
1066 && !e.bidirectional
1067 });
1068 assert!(
1069 factoring_circuit_unidir,
1070 "Factoring -> CircuitSAT should be unidirectional"
1071 );
1072 }
1073
1074 #[test]
1077 fn test_graph_hierarchy_built() {
1078 let graph = ReductionGraph::new();
1079 let hierarchy = graph.graph_hierarchy();
1080
1081 assert!(
1085 hierarchy.get("UnitDiskGraph").map(|s| s.contains("SimpleGraph")).unwrap_or(false),
1086 "UnitDiskGraph should have SimpleGraph as supertype"
1087 );
1088 assert!(
1089 hierarchy.get("PlanarGraph").map(|s| s.contains("SimpleGraph")).unwrap_or(false),
1090 "PlanarGraph should have SimpleGraph as supertype"
1091 );
1092 }
1093
1094 #[test]
1095 fn test_is_graph_subtype_reflexive() {
1096 let graph = ReductionGraph::new();
1097
1098 assert!(graph.is_graph_subtype("SimpleGraph", "SimpleGraph"));
1100 assert!(graph.is_graph_subtype("PlanarGraph", "PlanarGraph"));
1101 assert!(graph.is_graph_subtype("UnitDiskGraph", "UnitDiskGraph"));
1102 }
1103
1104 #[test]
1105 fn test_is_graph_subtype_direct() {
1106 let graph = ReductionGraph::new();
1107
1108 assert!(graph.is_graph_subtype("PlanarGraph", "SimpleGraph"));
1110 assert!(graph.is_graph_subtype("BipartiteGraph", "SimpleGraph"));
1111 assert!(graph.is_graph_subtype("UnitDiskGraph", "PlanarGraph"));
1112 }
1113
1114 #[test]
1115 fn test_is_graph_subtype_transitive() {
1116 let graph = ReductionGraph::new();
1117
1118 assert!(graph.is_graph_subtype("UnitDiskGraph", "SimpleGraph"));
1120 }
1121
1122 #[test]
1123 fn test_is_graph_subtype_not_supertype() {
1124 let graph = ReductionGraph::new();
1125
1126 assert!(!graph.is_graph_subtype("SimpleGraph", "PlanarGraph"));
1128 assert!(!graph.is_graph_subtype("SimpleGraph", "UnitDiskGraph"));
1129 }
1130
1131 #[test]
1132 fn test_rule_applicable_same_graphs() {
1133 let graph = ReductionGraph::new();
1134
1135 assert!(graph.rule_applicable("SimpleGraph", "SimpleGraph", "SimpleGraph", "SimpleGraph"));
1137 }
1138
1139 #[test]
1140 fn test_rule_applicable_subtype_source() {
1141 let graph = ReductionGraph::new();
1142
1143 assert!(graph.rule_applicable("PlanarGraph", "SimpleGraph", "SimpleGraph", "SimpleGraph"));
1146 }
1147
1148 #[test]
1149 fn test_rule_applicable_subtype_target() {
1150 let graph = ReductionGraph::new();
1151
1152 assert!(graph.rule_applicable("SimpleGraph", "SimpleGraph", "SimpleGraph", "PlanarGraph"));
1155 }
1156
1157 #[test]
1158 fn test_rule_not_applicable_wrong_source() {
1159 let graph = ReductionGraph::new();
1160
1161 assert!(!graph.rule_applicable("SimpleGraph", "SimpleGraph", "PlanarGraph", "SimpleGraph"));
1164 }
1165
1166 #[test]
1167 fn test_rule_not_applicable_wrong_target() {
1168 let graph = ReductionGraph::new();
1169
1170 assert!(!graph.rule_applicable("SimpleGraph", "PlanarGraph", "SimpleGraph", "SimpleGraph"));
1173 }
1174
1175 #[test]
1176 fn test_find_cheapest_path_minimize_steps() {
1177 let graph = ReductionGraph::new();
1178 let cost_fn = MinimizeSteps;
1179 let input_size = ProblemSize::new(vec![("n", 10), ("m", 20)]);
1180
1181 let path = graph.find_cheapest_path(
1183 ("IndependentSet", "SimpleGraph"),
1184 ("VertexCovering", "SimpleGraph"),
1185 &input_size,
1186 &cost_fn,
1187 );
1188
1189 assert!(path.is_some());
1190 let path = path.unwrap();
1191 assert_eq!(path.len(), 1); }
1193
1194 #[test]
1195 fn test_find_cheapest_path_multi_step() {
1196 let graph = ReductionGraph::new();
1197 let cost_fn = MinimizeSteps;
1198 let input_size = ProblemSize::new(vec![("num_vertices", 10), ("num_edges", 20)]);
1199
1200 let path = graph.find_cheapest_path(
1204 ("IndependentSet", "SimpleGraph"),
1205 ("SetPacking", "SetSystem"),
1206 &input_size,
1207 &cost_fn,
1208 );
1209
1210 assert!(path.is_some());
1211 let path = path.unwrap();
1212 assert_eq!(path.len(), 1); }
1214
1215 #[test]
1216 fn test_find_cheapest_path_no_path() {
1217 let graph = ReductionGraph::new();
1218 let cost_fn = MinimizeSteps;
1219 let input_size = ProblemSize::new(vec![("n", 10)]);
1220
1221 let path = graph.find_cheapest_path(
1223 ("IndependentSet", "SimpleGraph"),
1224 ("QUBO", "SimpleGraph"),
1225 &input_size,
1226 &cost_fn,
1227 );
1228
1229 assert!(path.is_none());
1230 }
1231
1232 #[test]
1233 fn test_find_cheapest_path_unknown_source() {
1234 let graph = ReductionGraph::new();
1235 let cost_fn = MinimizeSteps;
1236 let input_size = ProblemSize::new(vec![("n", 10)]);
1237
1238 let path = graph.find_cheapest_path(
1239 ("UnknownProblem", "SimpleGraph"),
1240 ("VertexCovering", "SimpleGraph"),
1241 &input_size,
1242 &cost_fn,
1243 );
1244
1245 assert!(path.is_none());
1246 }
1247
1248 #[test]
1249 fn test_find_cheapest_path_unknown_target() {
1250 let graph = ReductionGraph::new();
1251 let cost_fn = MinimizeSteps;
1252 let input_size = ProblemSize::new(vec![("n", 10)]);
1253
1254 let path = graph.find_cheapest_path(
1255 ("IndependentSet", "SimpleGraph"),
1256 ("UnknownProblem", "SimpleGraph"),
1257 &input_size,
1258 &cost_fn,
1259 );
1260
1261 assert!(path.is_none());
1262 }
1263
1264 #[test]
1265 fn test_reduction_edge_struct() {
1266 let edge = ReductionEdge {
1267 source_graph: "PlanarGraph",
1268 target_graph: "SimpleGraph",
1269 overhead: ReductionOverhead::default(),
1270 };
1271
1272 assert_eq!(edge.source_graph, "PlanarGraph");
1273 assert_eq!(edge.target_graph, "SimpleGraph");
1274 }
1275}