1use crate::rules::registry::{
14 AggregateReduceFn, EdgeCapabilities, ParameterContractError, ReduceFn, ReductionEntry,
15 ReductionParameterContract,
16};
17use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult};
18use crate::types::ProblemParameters;
19use petgraph::algo::all_simple_paths;
20use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex};
21use petgraph::visit::EdgeRef;
22use serde::Serialize;
23use std::any::Any;
24use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
25use std::rc::Rc;
26
27type NodePathOrderKey<'a> = (usize, Vec<(&'static str, &'a BTreeMap<String, String>)>);
28
29#[derive(Debug, Clone)]
32pub struct ReductionEdgeInfo {
33 pub source_name: &'static str,
34 pub source_variant: BTreeMap<String, String>,
35 pub target_name: &'static str,
36 pub target_variant: BTreeMap<String, String>,
37 pub parameter_contract: Result<ReductionParameterContract, ParameterContractError>,
38 pub capabilities: EdgeCapabilities,
39}
40
41#[derive(Clone)]
43pub(crate) struct ReductionEdgeData {
44 pub parameter_contract: Result<ReductionParameterContract, ParameterContractError>,
45 pub reduce_fn: Option<ReduceFn>,
46 pub reduce_aggregate_fn: Option<AggregateReduceFn>,
47 pub turing: bool,
48}
49
50impl ReductionEdgeData {
51 fn capabilities(&self) -> EdgeCapabilities {
52 EdgeCapabilities::from_executors(self.reduce_fn, self.reduce_aggregate_fn, self.turing)
53 }
54}
55
56#[derive(Debug, Clone, Serialize)]
58pub(crate) struct ReductionGraphJson {
59 pub(crate) nodes: Vec<NodeJson>,
61 pub(crate) edges: Vec<EdgeJson>,
63}
64
65impl ReductionGraphJson {
66 #[cfg_attr(not(test), allow(dead_code))]
68 pub(crate) fn source_node(&self, edge: &EdgeJson) -> &NodeJson {
69 &self.nodes[edge.source]
70 }
71
72 #[cfg_attr(not(test), allow(dead_code))]
74 pub(crate) fn target_node(&self, edge: &EdgeJson) -> &NodeJson {
75 &self.nodes[edge.target]
76 }
77}
78
79#[derive(Debug, Clone, Serialize)]
81pub(crate) struct NodeJson {
82 pub(crate) name: String,
84 pub(crate) variant: BTreeMap<String, String>,
86 pub(crate) category: crate::registry::ProblemCategory,
88 pub(crate) doc_path: String,
90 pub(crate) complexity: String,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96struct VariantRef {
97 name: String,
98 variant: BTreeMap<String, String>,
99}
100
101#[derive(Debug, Clone, Serialize)]
103pub(crate) struct ParameterFieldJson {
104 pub(crate) field: String,
105 pub(crate) contract: &'static str,
106 pub(crate) formula: Option<String>,
107 pub(crate) reason: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize)]
112pub(crate) struct EdgeJson {
113 pub(crate) source: usize,
115 pub(crate) target: usize,
117 pub(crate) parameters: Vec<ParameterFieldJson>,
119 pub(crate) parameter_contract_error: Option<String>,
120 pub(crate) doc_path: String,
122 pub(crate) witness: bool,
124 pub(crate) aggregate: bool,
126 pub(crate) turing: bool,
128}
129
130#[derive(Debug, Clone)]
132pub struct ReductionPath {
133 pub steps: Vec<ReductionStep>,
135}
136
137#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
139pub enum ExecutePathsError {
140 #[error("concrete path {path_index} is empty")]
141 EmptyPath { path_index: usize },
142 #[error("concrete path {path_index} contains no reduction edge")]
143 NoEdges { path_index: usize },
144 #[error("concrete path {path_index} starts at a different source node")]
145 DifferentSource { path_index: usize },
146 #[error("concrete path {path_index} references unknown node {problem} {variant:?}")]
147 UnknownNode {
148 path_index: usize,
149 problem: String,
150 variant: BTreeMap<String, String>,
151 },
152 #[error("concrete path {path_index} has no registered edge from {source_problem} to {target_problem}")]
153 MissingEdge {
154 path_index: usize,
155 source_problem: String,
156 target_problem: String,
157 },
158 #[error("concrete path {path_index} edge {source_problem} -> {target_problem} is not witness-executable")]
159 NotWitnessExecutable {
160 path_index: usize,
161 source_problem: String,
162 target_problem: String,
163 },
164 #[error("concrete path {path_index} failed during reduction: {cause}")]
165 Reduction {
166 path_index: usize,
167 #[source]
168 cause: crate::rules::ReductionError,
169 },
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
174pub enum PathParameterError {
175 #[error("cannot compose an empty reduction path")]
176 EmptyPath,
177 #[error("reduction path references unknown node {problem} {variant:?}")]
178 UnknownNode {
179 problem: String,
180 variant: BTreeMap<String, String>,
181 },
182 #[error(
183 "reduction path contains no registered edge from {source_problem} to {target_problem}"
184 )]
185 MissingEdge {
186 source_problem: String,
187 target_problem: String,
188 },
189 #[error("reduction step {step} ({source_problem} -> {target_problem}) is a multi-query reduction without a query-cost model")]
190 TuringEdge {
191 step: usize,
192 source_problem: String,
193 target_problem: String,
194 },
195 #[error("reduction step {step} ({source_problem} -> {target_problem}) has an invalid parameter contract: {error}")]
196 InvalidContract {
197 step: usize,
198 source_problem: String,
199 target_problem: String,
200 #[source]
201 error: Box<ParameterContractError>,
202 },
203 #[error("reduction step {step} ({source_problem} -> {target_problem}) has no symbolic parameter transform")]
204 Unavailable {
205 step: usize,
206 source_problem: String,
207 target_problem: String,
208 },
209 #[error(
210 "cannot compose reduction step {step} ({source_problem} -> {target_problem}): {error}"
211 )]
212 Step {
213 step: usize,
214 source_problem: String,
215 target_problem: String,
216 #[source]
217 error: Box<crate::parameters::ParameterTransformError>,
218 },
219}
220
221impl ReductionPath {
222 pub fn len(&self) -> usize {
224 if self.steps.is_empty() {
225 0
226 } else {
227 self.steps.len() - 1
228 }
229 }
230
231 pub fn is_empty(&self) -> bool {
233 self.steps.is_empty()
234 }
235
236 pub fn source(&self) -> Option<&str> {
238 self.steps.first().map(|s| s.name.as_str())
239 }
240
241 pub fn target(&self) -> Option<&str> {
243 self.steps.last().map(|s| s.name.as_str())
244 }
245
246 pub fn type_names(&self) -> Vec<&str> {
248 let mut names: Vec<&str> = Vec::new();
249 for step in &self.steps {
250 if names.last() != Some(&step.name.as_str()) {
251 names.push(&step.name);
252 }
253 }
254 names
255 }
256}
257
258impl std::fmt::Display for ReductionPath {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 let mut prev_name = "";
261 for step in &self.steps {
262 if step.name != prev_name {
263 if prev_name.is_empty() {
264 write!(f, "{step}")?;
265 } else {
266 write!(f, " → {step}")?;
267 }
268 prev_name = &step.name;
269 }
270 }
271 Ok(())
272 }
273}
274
275#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize)]
277pub struct ReductionStep {
278 pub name: String,
280 pub variant: BTreeMap<String, String>,
282}
283
284impl std::fmt::Display for ReductionStep {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 write!(f, "{}", self.name)?;
287 if !self.variant.is_empty() {
288 let vars: Vec<_> = self
289 .variant
290 .iter()
291 .map(|(k, v)| format!("{k}: {v:?}"))
292 .collect();
293 write!(f, " {{{}}}", vars.join(", "))?;
294 }
295 Ok(())
296 }
297}
298
299#[derive(Debug, Clone)]
301struct VariantNode {
302 name: &'static str,
303 variant: BTreeMap<String, String>,
304 complexity: &'static str,
305}
306
307#[derive(Debug, Clone)]
309pub struct NeighborInfo {
310 pub name: &'static str,
312 pub variant: BTreeMap<String, String>,
314 pub hops: usize,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum TraversalFlow {
321 Outgoing,
323 Incoming,
325 Both,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum ReductionMode {
332 Witness,
333 Aggregate,
334 Turing,
337}
338
339#[derive(Debug, Clone)]
341pub struct NeighborTree {
342 pub name: String,
344 pub variant: BTreeMap<String, String>,
346 pub children: Vec<NeighborTree>,
348}
349
350pub struct ReductionGraph {
359 graph: DiGraph<usize, ReductionEdgeData>,
361 nodes: Vec<VariantNode>,
363 name_to_nodes: HashMap<&'static str, Vec<NodeIndex>>,
365 default_variants: HashMap<String, BTreeMap<String, String>>,
367}
368
369impl ReductionGraph {
370 pub fn new() -> Self {
372 crate::registry::validate_variant_parameter_schemas().unwrap_or_else(|errors| {
373 panic!("invalid problem parameters schemas:\n{}", errors.join("\n"))
374 });
375 crate::rules::registry::validate_reduction_parameter_schemas().unwrap_or_else(|errors| {
376 panic!(
377 "invalid reduction parameter schemas:\n{}",
378 errors.join("\n")
379 )
380 });
381 let mut graph = DiGraph::new();
382 let mut nodes: Vec<VariantNode> = Vec::new();
383 let mut node_index: HashMap<VariantRef, NodeIndex> = HashMap::new();
384 let mut name_to_nodes: HashMap<&'static str, Vec<NodeIndex>> = HashMap::new();
385
386 let ensure_node = |name: &'static str,
388 variant: BTreeMap<String, String>,
389 complexity: &'static str,
390 nodes: &mut Vec<VariantNode>,
391 graph: &mut DiGraph<usize, ReductionEdgeData>,
392 node_index: &mut HashMap<VariantRef, NodeIndex>,
393 name_to_nodes: &mut HashMap<&'static str, Vec<NodeIndex>>|
394 -> NodeIndex {
395 let vref = VariantRef {
396 name: name.to_string(),
397 variant: variant.clone(),
398 };
399 if let Some(&idx) = node_index.get(&vref) {
400 idx
401 } else {
402 let node_id = nodes.len();
403 nodes.push(VariantNode {
404 name,
405 variant,
406 complexity,
407 });
408 let idx = graph.add_node(node_id);
409 node_index.insert(vref, idx);
410 name_to_nodes.entry(name).or_default().push(idx);
411 idx
412 }
413 };
414
415 let mut default_variants: HashMap<String, BTreeMap<String, String>> = HashMap::new();
417
418 for entry in inventory::iter::<crate::registry::VariantEntry> {
420 let variant = Self::variant_to_map(&entry.variant());
421 ensure_node(
422 entry.name,
423 variant.clone(),
424 entry.complexity,
425 &mut nodes,
426 &mut graph,
427 &mut node_index,
428 &mut name_to_nodes,
429 );
430 if entry.is_default {
431 default_variants.insert(entry.name.to_string(), variant);
432 }
433 }
434
435 for entry in inventory::iter::<ReductionEntry> {
437 let source_variant = Self::variant_to_map(&entry.source_variant());
438 let target_variant = Self::variant_to_map(&entry.target_variant());
439
440 let src_idx = node_index[&VariantRef {
441 name: entry.source_name.to_string(),
442 variant: source_variant,
443 }];
444 let dst_idx = node_index[&VariantRef {
445 name: entry.target_name.to_string(),
446 variant: target_variant,
447 }];
448
449 let parameter_contract = entry.parameter_contract();
450 if graph.find_edge(src_idx, dst_idx).is_none() {
451 graph.add_edge(
452 src_idx,
453 dst_idx,
454 ReductionEdgeData {
455 parameter_contract,
456 reduce_fn: entry.reduce_fn,
457 reduce_aggregate_fn: entry.reduce_aggregate_fn,
458 turing: entry.turing,
459 },
460 );
461 }
462 }
463
464 Self {
465 graph,
466 nodes,
467 name_to_nodes,
468 default_variants,
469 }
470 }
471
472 pub fn variant_to_map(variant: &[(&str, &str)]) -> BTreeMap<String, String> {
475 variant
476 .iter()
477 .map(|(k, v)| {
478 let value = if *k == "graph" && v.is_empty() {
479 "SimpleGraph".to_string()
480 } else {
481 v.to_string()
482 };
483 (k.to_string(), value)
484 })
485 .collect()
486 }
487
488 fn lookup_node(&self, name: &str, variant: &BTreeMap<String, String>) -> Option<NodeIndex> {
490 let nodes = self.name_to_nodes.get(name)?;
491 nodes
492 .iter()
493 .find(|&&idx| self.nodes[self.graph[idx]].variant == *variant)
494 .copied()
495 }
496
497 fn edge_supports_mode(edge: &ReductionEdgeData, mode: ReductionMode) -> bool {
498 match mode {
499 ReductionMode::Witness => edge.reduce_fn.is_some(),
500 ReductionMode::Aggregate => edge.reduce_aggregate_fn.is_some(),
501 ReductionMode::Turing => edge.turing,
502 }
503 }
504
505 fn ordered_outgoing_edges(
506 &self,
507 node: NodeIndex,
508 mode: ReductionMode,
509 ) -> Vec<(NodeIndex, EdgeIndex)> {
510 let mut edges: Vec<_> = self
511 .graph
512 .edges(node)
513 .filter(|edge| Self::edge_supports_mode(edge.weight(), mode))
514 .map(|edge| (edge.target(), edge.id()))
515 .collect();
516 edges.sort_by(|a, b| {
517 let a = &self.nodes[self.graph[a.0]];
518 let b = &self.nodes[self.graph[b.0]];
519 (a.name, &a.variant).cmp(&(b.name, &b.variant))
520 });
521 edges
522 }
523
524 fn node_path_supports_mode(&self, node_path: &[NodeIndex], mode: ReductionMode) -> bool {
525 node_path.windows(2).all(|pair| {
526 self.graph
527 .find_edge(pair[0], pair[1])
528 .is_some_and(|edge_idx| Self::edge_supports_mode(&self.graph[edge_idx], mode))
529 })
530 }
531
532 fn node_path_to_reduction_path(&self, node_path: &[NodeIndex]) -> ReductionPath {
534 let steps = node_path
535 .iter()
536 .map(|&idx| {
537 let node = &self.nodes[self.graph[idx]];
538 ReductionStep {
539 name: node.name.to_string(),
540 variant: node.variant.clone(),
541 }
542 })
543 .collect();
544 ReductionPath { steps }
545 }
546
547 fn node_path_order_key(&self, node_path: &[NodeIndex]) -> NodePathOrderKey<'_> {
548 (
549 node_path.len().saturating_sub(1),
550 node_path
551 .iter()
552 .map(|&idx| {
553 let node = &self.nodes[self.graph[idx]];
554 (node.name, &node.variant)
555 })
556 .collect(),
557 )
558 }
559
560 #[allow(clippy::too_many_arguments)]
561 fn shortest_node_path(
562 &self,
563 source: NodeIndex,
564 target: NodeIndex,
565 adjacency: &[Vec<NodeIndex>],
566 excluded_nodes: &HashSet<NodeIndex>,
567 excluded_edges: &HashSet<(NodeIndex, NodeIndex)>,
568 max_nodes: usize,
569 ) -> Option<Vec<NodeIndex>> {
570 if excluded_nodes.contains(&source) || max_nodes == 0 {
571 return None;
572 }
573 if source == target {
574 return Some(vec![source]);
575 }
576
577 let mut queue = VecDeque::from([(source, 1usize)]);
578 let mut parents = HashMap::new();
579 let mut visited = HashSet::from([source]);
580
581 while let Some((current, path_nodes)) = queue.pop_front() {
582 if path_nodes == max_nodes {
583 continue;
584 }
585 for &next in &adjacency[current.index()] {
586 if excluded_nodes.contains(&next)
587 || excluded_edges.contains(&(current, next))
588 || !visited.insert(next)
589 {
590 continue;
591 }
592 parents.insert(next, current);
593 if next == target {
594 let mut path = vec![target];
595 let mut node = target;
596 while node != source {
597 node = parents[&node];
598 path.push(node);
599 }
600 path.reverse();
601 return Some(path);
602 }
603 queue.push_back((next, path_nodes + 1));
604 }
605 }
606 None
607 }
608
609 fn find_k_shortest_node_paths(
610 &self,
611 source: NodeIndex,
612 target: NodeIndex,
613 mode: ReductionMode,
614 limit: usize,
615 max_nodes: usize,
616 ) -> Vec<Vec<NodeIndex>> {
617 if source == target || limit == 0 {
618 return Vec::new();
619 }
620
621 let mut adjacency = vec![Vec::new(); self.graph.node_count()];
622 for node in self.graph.node_indices() {
623 adjacency[node.index()] = self
624 .ordered_outgoing_edges(node, mode)
625 .into_iter()
626 .map(|(target, _)| target)
627 .collect();
628 }
629
630 let Some(first) = self.shortest_node_path(
631 source,
632 target,
633 &adjacency,
634 &HashSet::new(),
635 &HashSet::new(),
636 max_nodes,
637 ) else {
638 return Vec::new();
639 };
640
641 let mut accepted = vec![first];
642 let mut candidates = BTreeSet::new();
643
644 while accepted.len() < limit {
645 let previous = accepted.last().expect("accepted path exists");
646 for spur_index in 0..previous.len().saturating_sub(1) {
647 let root = &previous[..=spur_index];
648 let excluded_edges = accepted
649 .iter()
650 .filter(|path| path.len() > spur_index + 1 && path[..=spur_index] == *root)
651 .map(|path| (path[spur_index], path[spur_index + 1]))
652 .collect::<HashSet<_>>();
653 let excluded_nodes = root[..spur_index].iter().copied().collect();
654 let max_spur_nodes = max_nodes.saturating_sub(spur_index);
655 let Some(spur) = self.shortest_node_path(
656 previous[spur_index],
657 target,
658 &adjacency,
659 &excluded_nodes,
660 &excluded_edges,
661 max_spur_nodes,
662 ) else {
663 continue;
664 };
665 let mut candidate = root[..spur_index].to_vec();
666 candidate.extend(spur);
667 if !accepted.contains(&candidate) {
668 let key = self.node_path_order_key(&candidate);
669 candidates.insert((key, candidate));
670 }
671 }
672
673 let Some((_, next)) = candidates.pop_first() else {
674 break;
675 };
676 accepted.push(next);
677 }
678
679 accepted
680 }
681
682 pub fn find_all_paths(
687 &self,
688 source: &str,
689 source_variant: &BTreeMap<String, String>,
690 target: &str,
691 target_variant: &BTreeMap<String, String>,
692 ) -> Vec<ReductionPath> {
693 self.find_all_paths_mode(
694 source,
695 source_variant,
696 target,
697 target_variant,
698 ReductionMode::Witness,
699 )
700 }
701
702 pub fn find_all_paths_mode(
705 &self,
706 source: &str,
707 source_variant: &BTreeMap<String, String>,
708 target: &str,
709 target_variant: &BTreeMap<String, String>,
710 mode: ReductionMode,
711 ) -> Vec<ReductionPath> {
712 let src = match self.lookup_node(source, source_variant) {
713 Some(idx) => idx,
714 None => return vec![],
715 };
716 let dst = match self.lookup_node(target, target_variant) {
717 Some(idx) => idx,
718 None => return vec![],
719 };
720
721 let paths: Vec<Vec<NodeIndex>> = all_simple_paths::<
722 Vec<NodeIndex>,
723 _,
724 std::hash::RandomState,
725 >(&self.graph, src, dst, 0, None)
726 .collect();
727
728 paths
729 .iter()
730 .filter(|p| self.node_path_supports_mode(p, mode))
731 .map(|p| self.node_path_to_reduction_path(p))
732 .collect()
733 }
734
735 pub fn find_paths_up_to(
741 &self,
742 source: &str,
743 source_variant: &BTreeMap<String, String>,
744 target: &str,
745 target_variant: &BTreeMap<String, String>,
746 limit: usize,
747 ) -> Vec<ReductionPath> {
748 self.find_paths_up_to_mode_bounded(
749 source,
750 source_variant,
751 target,
752 target_variant,
753 ReductionMode::Witness,
754 limit,
755 None,
756 )
757 }
758
759 pub fn find_paths_up_to_mode(
762 &self,
763 source: &str,
764 source_variant: &BTreeMap<String, String>,
765 target: &str,
766 target_variant: &BTreeMap<String, String>,
767 mode: ReductionMode,
768 limit: usize,
769 ) -> Vec<ReductionPath> {
770 self.find_paths_up_to_mode_bounded(
771 source,
772 source_variant,
773 target,
774 target_variant,
775 mode,
776 limit,
777 None,
778 )
779 }
780
781 #[allow(clippy::too_many_arguments)]
784 pub fn find_paths_up_to_mode_bounded(
785 &self,
786 source: &str,
787 source_variant: &BTreeMap<String, String>,
788 target: &str,
789 target_variant: &BTreeMap<String, String>,
790 mode: ReductionMode,
791 limit: usize,
792 max_intermediate_nodes: Option<usize>,
793 ) -> Vec<ReductionPath> {
794 let src = match self.lookup_node(source, source_variant) {
795 Some(idx) => idx,
796 None => return vec![],
797 };
798 let dst = match self.lookup_node(target, target_variant) {
799 Some(idx) => idx,
800 None => return vec![],
801 };
802
803 if limit == 0 {
804 return Vec::new();
805 }
806
807 let max_intermediate =
808 max_intermediate_nodes.unwrap_or_else(|| self.graph.node_count().saturating_sub(2));
809 let max_nodes = max_intermediate.saturating_add(2);
810 self.find_k_shortest_node_paths(src, dst, mode, limit, max_nodes)
811 .iter()
812 .map(|path| self.node_path_to_reduction_path(path))
813 .collect()
814 }
815
816 pub fn has_direct_reduction<S: crate::traits::Problem, T: crate::traits::Problem>(
818 &self,
819 ) -> bool {
820 self.has_direct_reduction_by_name(S::NAME, T::NAME)
821 }
822
823 pub fn has_direct_reduction_by_name(&self, src: &str, dst: &str) -> bool {
825 let src_nodes = match self.name_to_nodes.get(src) {
826 Some(nodes) => nodes,
827 None => return false,
828 };
829 let dst_nodes = match self.name_to_nodes.get(dst) {
830 Some(nodes) => nodes,
831 None => return false,
832 };
833
834 let dst_set: HashSet<NodeIndex> = dst_nodes.iter().copied().collect();
835
836 for &src_idx in src_nodes {
837 for edge_ref in self.graph.edges(src_idx) {
838 if dst_set.contains(&edge_ref.target()) {
839 return true;
840 }
841 }
842 }
843
844 false
845 }
846
847 pub fn has_direct_reduction_by_name_mode(
849 &self,
850 src: &str,
851 dst: &str,
852 mode: ReductionMode,
853 ) -> bool {
854 let src_nodes = match self.name_to_nodes.get(src) {
855 Some(nodes) => nodes,
856 None => return false,
857 };
858 let dst_nodes = match self.name_to_nodes.get(dst) {
859 Some(nodes) => nodes,
860 None => return false,
861 };
862
863 let dst_set: HashSet<NodeIndex> = dst_nodes.iter().copied().collect();
864
865 for &src_idx in src_nodes {
866 for edge_ref in self.graph.edges(src_idx) {
867 if dst_set.contains(&edge_ref.target())
868 && Self::edge_supports_mode(edge_ref.weight(), mode)
869 {
870 return true;
871 }
872 }
873 }
874
875 false
876 }
877
878 pub fn has_direct_reduction_mode<S: crate::traits::Problem, T: crate::traits::Problem>(
880 &self,
881 mode: ReductionMode,
882 ) -> bool {
883 self.has_direct_reduction_by_name_mode(S::NAME, T::NAME, mode)
884 }
885
886 pub fn problem_types(&self) -> Vec<&'static str> {
888 self.name_to_nodes.keys().copied().collect()
889 }
890
891 pub fn num_types(&self) -> usize {
893 self.name_to_nodes.len()
894 }
895
896 pub fn num_reductions(&self) -> usize {
898 self.graph.edge_count()
899 }
900
901 pub fn num_variant_nodes(&self) -> usize {
903 self.nodes.len()
904 }
905
906 pub fn path_parameter_transforms(
908 &self,
909 path: &ReductionPath,
910 ) -> Result<Vec<crate::parameters::ParameterTransform>, PathParameterError> {
911 if path.steps.len() <= 1 {
912 return Ok(vec![]);
913 }
914
915 let node_indices: Vec<NodeIndex> = path
916 .steps
917 .iter()
918 .map(|step| {
919 self.lookup_node(&step.name, &step.variant).ok_or_else(|| {
920 PathParameterError::UnknownNode {
921 problem: step.name.clone(),
922 variant: step.variant.clone(),
923 }
924 })
925 })
926 .collect::<Result<_, _>>()?;
927
928 node_indices
929 .windows(2)
930 .enumerate()
931 .map(|(index, pair)| {
932 let edge_idx = self.graph.find_edge(pair[0], pair[1]).ok_or_else(|| {
933 PathParameterError::MissingEdge {
934 source_problem: path.steps[index].name.clone(),
935 target_problem: path.steps[index + 1].name.clone(),
936 }
937 })?;
938 if self.graph[edge_idx].turing {
939 return Err(PathParameterError::TuringEdge {
940 step: index + 1,
941 source_problem: path.steps[index].name.clone(),
942 target_problem: path.steps[index + 1].name.clone(),
943 });
944 }
945 let contract =
946 self.graph[edge_idx]
947 .parameter_contract
948 .as_ref()
949 .map_err(|error| PathParameterError::InvalidContract {
950 step: index + 1,
951 source_problem: path.steps[index].name.clone(),
952 target_problem: path.steps[index + 1].name.clone(),
953 error: Box::new(error.clone()),
954 })?;
955 contract
956 .transform()
957 .cloned()
958 .ok_or_else(|| PathParameterError::Unavailable {
959 step: index + 1,
960 source_problem: path.steps[index].name.clone(),
961 target_problem: path.steps[index + 1].name.clone(),
962 })
963 })
964 .collect()
965 }
966
967 pub fn compose_path_parameter_transform(
969 &self,
970 path: &ReductionPath,
971 ) -> Result<Option<crate::parameters::ParameterTransform>, PathParameterError> {
972 if path.steps.is_empty() {
973 return Err(PathParameterError::EmptyPath);
974 }
975 if path.steps.len() == 1 {
976 return Ok(None);
977 }
978
979 let mut transforms = self.path_parameter_transforms(path)?.into_iter();
980 let Some(mut composed) = transforms.next() else {
981 return Ok(None);
982 };
983 for (offset, transform) in transforms.enumerate() {
984 let edge_index = offset + 1;
985 composed = composed
986 .compose(
987 &transform,
988 format!(
989 "{} -> {}",
990 path.steps[0].name,
991 path.steps[edge_index + 1].name
992 ),
993 )
994 .map_err(|error| PathParameterError::Step {
995 step: edge_index + 1,
996 source_problem: path.steps[edge_index].name.clone(),
997 target_problem: path.steps[edge_index + 1].name.clone(),
998 error: Box::new(error),
999 })?;
1000 }
1001 Ok(Some(composed))
1002 }
1003
1004 pub fn variants_for(&self, name: &str) -> Vec<BTreeMap<String, String>> {
1009 let mut variants: Vec<BTreeMap<String, String>> = self
1010 .name_to_nodes
1011 .get(name)
1012 .map(|indices| {
1013 indices
1014 .iter()
1015 .map(|&idx| self.nodes[self.graph[idx]].variant.clone())
1016 .collect()
1017 })
1018 .unwrap_or_default();
1019 variants.sort();
1020 if let Some(default) = self.default_variants.get(name) {
1021 if let Some(index) = variants.iter().position(|variant| variant == default) {
1022 let default = variants.remove(index);
1023 variants.insert(0, default);
1024 }
1025 }
1026 variants
1027 }
1028
1029 pub fn default_variant_for(&self, name: &str) -> Option<BTreeMap<String, String>> {
1036 self.default_variants.get(name).cloned()
1037 }
1038
1039 pub fn variant_complexity(
1041 &self,
1042 name: &str,
1043 variant: &BTreeMap<String, String>,
1044 ) -> Option<&'static str> {
1045 let idx = self.lookup_node(name, variant)?;
1046 let node = &self.nodes[self.graph[idx]];
1047 if node.complexity.is_empty() {
1048 None
1049 } else {
1050 Some(node.complexity)
1051 }
1052 }
1053
1054 pub fn outgoing_reductions(&self, name: &str) -> Vec<ReductionEdgeInfo> {
1056 let Some(indices) = self.name_to_nodes.get(name) else {
1057 return vec![];
1058 };
1059 let index_set: HashSet<NodeIndex> = indices.iter().copied().collect();
1060 self.graph
1061 .edge_references()
1062 .filter(|e| index_set.contains(&e.source()))
1063 .map(|e| {
1064 let src = &self.nodes[self.graph[e.source()]];
1065 let dst = &self.nodes[self.graph[e.target()]];
1066 ReductionEdgeInfo {
1067 source_name: src.name,
1068 source_variant: src.variant.clone(),
1069 target_name: dst.name,
1070 target_variant: dst.variant.clone(),
1071 parameter_contract: self.graph[e.id()].parameter_contract.clone(),
1072 capabilities: self.graph[e.id()].capabilities(),
1073 }
1074 })
1075 .collect()
1076 }
1077
1078 pub fn outgoing_reductions_from(
1084 &self,
1085 name: &str,
1086 variant: &BTreeMap<String, String>,
1087 mode: ReductionMode,
1088 ) -> Vec<ReductionEdgeInfo> {
1089 let source = self
1090 .lookup_node(name, variant)
1091 .unwrap_or_else(|| panic!("registered problem variant not found: {name} {variant:?}"));
1092
1093 self.ordered_outgoing_edges(source, mode)
1094 .into_iter()
1095 .map(|(target, edge)| {
1096 let src = &self.nodes[self.graph[source]];
1097 let dst = &self.nodes[self.graph[target]];
1098 ReductionEdgeInfo {
1099 source_name: src.name,
1100 source_variant: src.variant.clone(),
1101 target_name: dst.name,
1102 target_variant: dst.variant.clone(),
1103 parameter_contract: self.graph[edge].parameter_contract.clone(),
1104 capabilities: self.graph[edge].capabilities(),
1105 }
1106 })
1107 .collect()
1108 }
1109
1110 pub fn parameter_names(&self, name: &str) -> Vec<String> {
1112 inventory::iter::<crate::registry::VariantEntry>
1113 .into_iter()
1114 .find(|entry| entry.name == name)
1115 .map(|entry| {
1116 entry
1117 .parameter_names()
1118 .iter()
1119 .map(|name| (*name).to_string())
1120 .collect()
1121 })
1122 .unwrap_or_default()
1123 }
1124
1125 pub fn compute_problem_parameters(
1127 name: &str,
1128 variant: &BTreeMap<String, String>,
1129 instance: &dyn Any,
1130 ) -> ProblemParameters {
1131 let entry = crate::registry::find_variant_entry(name, variant)
1132 .unwrap_or_else(|| panic!("unregistered exact problem variant `{name}` {variant:?}"));
1133 (entry.parameter_measure_fn)(instance)
1134 }
1135
1136 pub fn incoming_reductions(&self, name: &str) -> Vec<ReductionEdgeInfo> {
1138 let Some(indices) = self.name_to_nodes.get(name) else {
1139 return vec![];
1140 };
1141 let index_set: HashSet<NodeIndex> = indices.iter().copied().collect();
1142 self.graph
1143 .edge_references()
1144 .filter(|e| index_set.contains(&e.target()))
1145 .map(|e| {
1146 let src = &self.nodes[self.graph[e.source()]];
1147 let dst = &self.nodes[self.graph[e.target()]];
1148 ReductionEdgeInfo {
1149 source_name: src.name,
1150 source_variant: src.variant.clone(),
1151 target_name: dst.name,
1152 target_variant: dst.variant.clone(),
1153 parameter_contract: self.graph[e.id()].parameter_contract.clone(),
1154 capabilities: self.graph[e.id()].capabilities(),
1155 }
1156 })
1157 .collect()
1158 }
1159
1160 pub fn k_neighbors(
1165 &self,
1166 name: &str,
1167 variant: &BTreeMap<String, String>,
1168 max_hops: usize,
1169 direction: TraversalFlow,
1170 ) -> Vec<NeighborInfo> {
1171 use std::collections::VecDeque;
1172
1173 let Some(start_idx) = self.lookup_node(name, variant) else {
1174 return vec![];
1175 };
1176
1177 let mut visited: HashSet<NodeIndex> = HashSet::new();
1178 visited.insert(start_idx);
1179 let mut queue: VecDeque<(NodeIndex, usize)> = VecDeque::new();
1180 queue.push_back((start_idx, 0));
1181 let mut results: Vec<NeighborInfo> = Vec::new();
1182
1183 while let Some((node_idx, hops)) = queue.pop_front() {
1184 if hops >= max_hops {
1185 continue;
1186 }
1187
1188 let directions = match direction {
1189 TraversalFlow::Outgoing => vec![petgraph::Outgoing],
1190 TraversalFlow::Incoming => vec![petgraph::Incoming],
1191 TraversalFlow::Both => {
1192 vec![petgraph::Outgoing, petgraph::Incoming]
1193 }
1194 };
1195
1196 for dir in directions {
1197 for neighbor_idx in self.graph.neighbors_directed(node_idx, dir) {
1198 if visited.insert(neighbor_idx) {
1199 let neighbor_node = &self.nodes[self.graph[neighbor_idx]];
1200 results.push(NeighborInfo {
1201 name: neighbor_node.name,
1202 variant: neighbor_node.variant.clone(),
1203 hops: hops + 1,
1204 });
1205 queue.push_back((neighbor_idx, hops + 1));
1206 }
1207 }
1208 }
1209 }
1210
1211 results.sort_by(|a, b| a.hops.cmp(&b.hops).then_with(|| a.name.cmp(b.name)));
1212 results
1213 }
1214
1215 pub fn k_neighbor_tree(
1220 &self,
1221 name: &str,
1222 variant: &BTreeMap<String, String>,
1223 max_hops: usize,
1224 direction: TraversalFlow,
1225 ) -> Vec<NeighborTree> {
1226 use std::collections::VecDeque;
1227
1228 let Some(start_idx) = self.lookup_node(name, variant) else {
1229 return vec![];
1230 };
1231
1232 let mut visited: HashSet<NodeIndex> = HashSet::new();
1233 visited.insert(start_idx);
1234
1235 let mut queue: VecDeque<(NodeIndex, usize)> = VecDeque::new();
1236 queue.push_back((start_idx, 0));
1237
1238 let mut node_children: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
1240
1241 while let Some((node_idx, depth)) = queue.pop_front() {
1242 if depth >= max_hops {
1243 continue;
1244 }
1245
1246 let directions = match direction {
1247 TraversalFlow::Outgoing => vec![petgraph::Outgoing],
1248 TraversalFlow::Incoming => vec![petgraph::Incoming],
1249 TraversalFlow::Both => {
1250 vec![petgraph::Outgoing, petgraph::Incoming]
1251 }
1252 };
1253
1254 let mut children = Vec::new();
1255 for dir in directions {
1256 for neighbor_idx in self.graph.neighbors_directed(node_idx, dir) {
1257 if visited.insert(neighbor_idx) {
1258 children.push(neighbor_idx);
1259 queue.push_back((neighbor_idx, depth + 1));
1260 }
1261 }
1262 }
1263 children.sort_by(|a, b| {
1264 self.nodes[self.graph[*a]]
1265 .name
1266 .cmp(self.nodes[self.graph[*b]].name)
1267 });
1268 node_children.insert(node_idx, children);
1269 }
1270
1271 fn build(
1273 idx: NodeIndex,
1274 node_children: &HashMap<NodeIndex, Vec<NodeIndex>>,
1275 nodes: &[VariantNode],
1276 graph: &DiGraph<usize, ReductionEdgeData>,
1277 ) -> NeighborTree {
1278 let children = node_children
1279 .get(&idx)
1280 .map(|cs| {
1281 cs.iter()
1282 .map(|&c| build(c, node_children, nodes, graph))
1283 .collect()
1284 })
1285 .unwrap_or_default();
1286 let node = &nodes[graph[idx]];
1287 NeighborTree {
1288 name: node.name.to_string(),
1289 variant: node.variant.clone(),
1290 children,
1291 }
1292 }
1293
1294 node_children
1295 .get(&start_idx)
1296 .map(|cs| {
1297 cs.iter()
1298 .map(|&c| build(c, &node_children, &self.nodes, &self.graph))
1299 .collect()
1300 })
1301 .unwrap_or_default()
1302 }
1303}
1304
1305impl Default for ReductionGraph {
1306 fn default() -> Self {
1307 Self::new()
1308 }
1309}
1310
1311impl ReductionGraph {
1312 pub(crate) fn to_json(&self) -> ReductionGraphJson {
1316 use crate::registry::ProblemSchemaEntry;
1317
1318 let schema_metadata: HashMap<&str, (&str, crate::registry::ProblemCategory)> =
1320 inventory::iter::<ProblemSchemaEntry>
1321 .into_iter()
1322 .map(|entry| (entry.name, (entry.module_path, entry.category)))
1323 .collect();
1324
1325 let mut json_nodes: Vec<(usize, NodeJson)> = self
1327 .nodes
1328 .iter()
1329 .enumerate()
1330 .map(|(i, node)| {
1331 let &(module_path, category) =
1332 schema_metadata.get(node.name).unwrap_or_else(|| {
1333 panic!(
1334 "missing problem schema for registered variant `{}`",
1335 node.name
1336 )
1337 });
1338 (
1339 i,
1340 NodeJson {
1341 name: node.name.to_string(),
1342 variant: node.variant.clone(),
1343 category,
1344 doc_path: Self::doc_path_from_module_path(module_path, node.name),
1345 complexity: node.complexity.to_string(),
1346 },
1347 )
1348 })
1349 .collect();
1350 json_nodes.sort_by(|a, b| (&a.1.name, &a.1.variant).cmp(&(&b.1.name, &b.1.variant)));
1351
1352 let mut old_to_new: HashMap<usize, usize> = HashMap::new();
1354 for (new_idx, (old_idx, _)) in json_nodes.iter().enumerate() {
1355 old_to_new.insert(*old_idx, new_idx);
1356 }
1357
1358 let nodes: Vec<NodeJson> = json_nodes.into_iter().map(|(_, n)| n).collect();
1359
1360 let mut edges: Vec<EdgeJson> = Vec::new();
1362 for edge_ref in self.graph.edge_references() {
1363 let src_node_id = self.graph[edge_ref.source()];
1364 let dst_node_id = self.graph[edge_ref.target()];
1365 let contract = &edge_ref.weight().parameter_contract;
1366 let capabilities = edge_ref.weight().capabilities();
1367
1368 let mut parameters = Vec::new();
1369 if let Ok(contract) = contract {
1370 if let Some(transform) = contract.transform() {
1371 let relation = match transform.relation() {
1372 crate::parameters::ParameterRelation::Exact => "exact",
1373 crate::parameters::ParameterRelation::UpperBound => "upper_bound",
1374 };
1375 parameters.extend(transform.expressions().map(|(field, expression)| {
1376 ParameterFieldJson {
1377 field: field.to_string(),
1378 contract: relation,
1379 formula: Some(expression.to_string()),
1380 reason: None,
1381 }
1382 }));
1383 }
1384 parameters.extend(contract.unavailable().iter().map(|unavailable| {
1385 ParameterFieldJson {
1386 field: unavailable.field.to_string(),
1387 contract: "unavailable",
1388 formula: None,
1389 reason: Some(unavailable.reason.to_string()),
1390 }
1391 }));
1392 }
1393 let parameter_contract_error = contract.as_ref().err().map(ToString::to_string);
1394
1395 let src_name = self.nodes[src_node_id].name;
1397 let dst_name = self.nodes[dst_node_id].name;
1398 let src_variant = &self.nodes[src_node_id].variant;
1399 let dst_variant = &self.nodes[dst_node_id].variant;
1400
1401 let doc_path = self.find_entry_doc_path(src_name, dst_name, src_variant, dst_variant);
1402
1403 edges.push(EdgeJson {
1404 source: old_to_new[&src_node_id],
1405 target: old_to_new[&dst_node_id],
1406 parameters,
1407 parameter_contract_error,
1408 doc_path,
1409 witness: capabilities.witness,
1410 aggregate: capabilities.aggregate,
1411 turing: capabilities.turing,
1412 });
1413 }
1414
1415 edges.sort_by(|a, b| {
1417 (
1418 &nodes[a.source].name,
1419 &nodes[a.source].variant,
1420 &nodes[a.target].name,
1421 &nodes[a.target].variant,
1422 )
1423 .cmp(&(
1424 &nodes[b.source].name,
1425 &nodes[b.source].variant,
1426 &nodes[b.target].name,
1427 &nodes[b.target].variant,
1428 ))
1429 });
1430
1431 ReductionGraphJson { nodes, edges }
1432 }
1433
1434 fn find_entry_doc_path(
1436 &self,
1437 src_name: &str,
1438 dst_name: &str,
1439 src_variant: &BTreeMap<String, String>,
1440 dst_variant: &BTreeMap<String, String>,
1441 ) -> String {
1442 for entry in inventory::iter::<ReductionEntry> {
1443 if entry.source_name == src_name && entry.target_name == dst_name {
1444 let entry_src = Self::variant_to_map(&entry.source_variant());
1445 let entry_dst = Self::variant_to_map(&entry.target_variant());
1446 if &entry_src == src_variant && &entry_dst == dst_variant {
1447 return Self::module_path_to_doc_path(entry.module_path);
1448 }
1449 }
1450 }
1451 String::new()
1452 }
1453
1454 pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
1456 let json = self.to_json();
1457 serde_json::to_string_pretty(&json)
1458 }
1459
1460 pub fn to_json_value(&self) -> Result<serde_json::Value, serde_json::Error> {
1462 serde_json::to_value(self.to_json())
1463 }
1464
1465 pub fn to_json_file(&self, path: &std::path::Path) -> std::io::Result<()> {
1467 let json_string = self
1468 .to_json_string()
1469 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
1470 std::fs::write(path, json_string)
1471 }
1472
1473 fn module_path_to_doc_path(module_path: &str) -> String {
1477 let stripped = module_path
1478 .strip_prefix("problemreductions::")
1479 .unwrap_or(module_path);
1480 format!("{}/index.html", stripped.replace("::", "/"))
1481 }
1482
1483 fn doc_path_from_module_path(module_path: &str, name: &str) -> String {
1488 let stripped = module_path
1489 .strip_prefix("problemreductions::")
1490 .unwrap_or(module_path);
1491 if let Some(parent) = stripped.rsplit_once("::").map(|(p, _)| p) {
1492 format!("{}/struct.{}.html", parent.replace("::", "/"), name)
1493 } else {
1494 format!("struct.{}.html", name)
1495 }
1496 }
1497
1498 pub fn find_entry(
1503 &self,
1504 source_name: &str,
1505 source_variant: &BTreeMap<String, String>,
1506 target_name: &str,
1507 target_variant: &BTreeMap<String, String>,
1508 ) -> Option<MatchedEntry> {
1509 let source = self.lookup_node(source_name, source_variant)?;
1510 let target = self.lookup_node(target_name, target_variant)?;
1511 let edge = self.graph.find_edge(source, target)?;
1512
1513 Some(MatchedEntry {
1514 source_variant: source_variant.clone(),
1515 target_variant: target_variant.clone(),
1516 parameter_contract: self.graph[edge].parameter_contract.clone(),
1517 })
1518 }
1519}
1520
1521pub struct MatchedEntry {
1523 pub source_variant: BTreeMap<String, String>,
1525 pub target_variant: BTreeMap<String, String>,
1527 pub parameter_contract: Result<ReductionParameterContract, ParameterContractError>,
1529}
1530
1531pub struct ReductionChain {
1537 steps: Vec<Box<dyn DynReductionResult>>,
1538}
1539
1540impl ReductionChain {
1541 pub fn target_problem_any(&self) -> &dyn Any {
1543 self.steps
1544 .last()
1545 .expect("ReductionChain has no steps")
1546 .target_problem_any()
1547 }
1548
1549 pub fn target_problem<T: 'static>(&self) -> &T {
1553 self.target_problem_any()
1554 .downcast_ref::<T>()
1555 .expect("ReductionChain target type mismatch")
1556 }
1557
1558 pub fn extract_solution<S: 'static, T: 'static>(
1560 &self,
1561 target_solution: &T,
1562 ) -> crate::rules::ExtractionResult<S> {
1563 let mut steps = self.steps.iter().rev();
1564 let first = steps.next().expect("ReductionChain has no steps");
1565 let mut solution = first.extract_solution_dyn(target_solution)?;
1566 for step in steps {
1567 solution = step.extract_solution_dyn(solution.as_ref())?;
1568 }
1569 solution
1570 .downcast::<S>()
1571 .map(|solution| *solution)
1572 .map_err(|_| crate::rules::ExtractionError::invalid("source solution type mismatch"))
1573 }
1574
1575 pub fn extract_solution_json(
1577 &self,
1578 target_solution: serde_json::Value,
1579 ) -> crate::rules::ExtractionResult<serde_json::Value> {
1580 let last = self.steps.last().expect("ReductionChain has no steps");
1581 let mut solution = last.target_solution_from_json(target_solution)?;
1582 for step in self.steps.iter().rev() {
1583 solution = step.extract_solution_dyn(solution.as_ref())?;
1584 }
1585 self.steps[0].source_solution_json(solution.as_ref())
1586 }
1587}
1588
1589pub struct AggregateReductionChain {
1592 steps: Vec<Box<dyn DynAggregateReductionResult>>,
1593}
1594
1595impl AggregateReductionChain {
1596 pub fn target_problem_any(&self) -> &dyn Any {
1598 self.steps
1599 .last()
1600 .expect("AggregateReductionChain has no steps")
1601 .target_problem_any()
1602 }
1603
1604 pub fn target_problem<T: 'static>(&self) -> &T {
1608 self.target_problem_any()
1609 .downcast_ref::<T>()
1610 .expect("AggregateReductionChain target type mismatch")
1611 }
1612
1613 pub fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value {
1615 self.steps
1616 .iter()
1617 .rev()
1618 .fold(target_value, |value, step| step.extract_value_dyn(value))
1619 }
1620}
1621
1622impl ReductionGraph {
1623 fn execute_aggregate_edge(
1624 &self,
1625 edge_idx: EdgeIndex,
1626 input: &dyn Any,
1627 ) -> Result<Option<Box<dyn DynAggregateReductionResult>>, crate::rules::ReductionError> {
1628 let edge = &self.graph[edge_idx];
1629 if !Self::edge_supports_mode(edge, ReductionMode::Aggregate) {
1630 return Ok(None);
1631 }
1632
1633 let Some(reduce) = edge.reduce_aggregate_fn else {
1634 return Ok(None);
1635 };
1636 reduce(input).map(Some)
1637 }
1638
1639 pub fn reduce_along_path(
1655 &self,
1656 path: &ReductionPath,
1657 source: &dyn Any,
1658 ) -> Result<Option<ReductionChain>, crate::rules::ReductionError> {
1659 if path.steps.len() < 2 {
1660 return Ok(None);
1661 }
1662 let mut edge_fns = Vec::new();
1664 for window in path.steps.windows(2) {
1665 let Some(src) = self.lookup_node(&window[0].name, &window[0].variant) else {
1666 return Ok(None);
1667 };
1668 let Some(dst) = self.lookup_node(&window[1].name, &window[1].variant) else {
1669 return Ok(None);
1670 };
1671 let Some(edge_idx) = self.graph.find_edge(src, dst) else {
1672 return Ok(None);
1673 };
1674 if !Self::edge_supports_mode(&self.graph[edge_idx], ReductionMode::Witness) {
1675 return Ok(None);
1676 }
1677 let Some(reduce) = self.graph[edge_idx].reduce_fn else {
1678 return Ok(None);
1679 };
1680 edge_fns.push(reduce);
1681 }
1682 let mut steps: Vec<Box<dyn DynReductionResult>> = Vec::new();
1684 let step = (edge_fns[0])(source)?;
1685 steps.push(step);
1686 for edge_fn in &edge_fns[1..] {
1687 let step = {
1688 let prev_target = steps.last().unwrap().target_problem_any();
1689 edge_fn(prev_target)?
1690 };
1691 steps.push(step);
1692 }
1693 Ok(Some(ReductionChain { steps }))
1694 }
1695
1696 pub fn reduce_aggregate_along_path(
1698 &self,
1699 path: &ReductionPath,
1700 source: &dyn Any,
1701 ) -> Result<Option<AggregateReductionChain>, crate::rules::ReductionError> {
1702 if path.steps.len() < 2 {
1703 return Ok(None);
1704 }
1705
1706 let mut edge_indices = Vec::new();
1707 for window in path.steps.windows(2) {
1708 let Some(src) = self.lookup_node(&window[0].name, &window[0].variant) else {
1709 return Ok(None);
1710 };
1711 let Some(dst) = self.lookup_node(&window[1].name, &window[1].variant) else {
1712 return Ok(None);
1713 };
1714 let Some(edge_idx) = self.graph.find_edge(src, dst) else {
1715 return Ok(None);
1716 };
1717 edge_indices.push(edge_idx);
1718 }
1719
1720 let mut steps: Vec<Box<dyn DynAggregateReductionResult>> = Vec::new();
1721 let Some(step) = self.execute_aggregate_edge(edge_indices[0], source)? else {
1722 return Ok(None);
1723 };
1724 steps.push(step);
1725 for &edge_idx in &edge_indices[1..] {
1726 let step = {
1727 let prev_target = steps.last().unwrap().target_problem_any();
1728 let Some(step) = self.execute_aggregate_edge(edge_idx, prev_target)? else {
1729 return Ok(None);
1730 };
1731 step
1732 };
1733 steps.push(step);
1734 }
1735 Ok(Some(AggregateReductionChain { steps }))
1736 }
1737}
1738
1739pub struct ExecutedPath {
1744 pub path: ReductionPath,
1746 steps: Vec<Rc<dyn DynReductionResult>>,
1748}
1749
1750impl ExecutedPath {
1751 pub fn target_problem_any(&self) -> &dyn Any {
1753 self.steps
1754 .last()
1755 .expect("ExecutedPath has no steps")
1756 .target_problem_any()
1757 }
1758
1759 pub fn target_parameters(&self) -> Vec<ProblemParameters> {
1761 self.steps
1762 .iter()
1763 .zip(self.path.steps.iter().skip(1))
1764 .map(|(result, target)| {
1765 ReductionGraph::compute_problem_parameters(
1766 &target.name,
1767 &target.variant,
1768 result.target_problem_any(),
1769 )
1770 })
1771 .collect()
1772 }
1773
1774 pub fn extract_solution<S: 'static, T: 'static>(
1776 &self,
1777 target_solution: &T,
1778 ) -> crate::rules::ExtractionResult<S> {
1779 let mut steps = self.steps.iter().rev();
1780 let first = steps.next().expect("ExecutedPath has no steps");
1781 let mut solution = first.extract_solution_dyn(target_solution)?;
1782 for step in steps {
1783 solution = step.extract_solution_dyn(solution.as_ref())?;
1784 }
1785 solution
1786 .downcast::<S>()
1787 .map(|solution| *solution)
1788 .map_err(|_| crate::rules::ExtractionError::invalid("source solution type mismatch"))
1789 }
1790}
1791
1792impl ReductionGraph {
1793 pub fn execute_paths(
1795 &self,
1796 paths: &[ReductionPath],
1797 source_instance: &dyn Any,
1798 ) -> Result<Vec<ExecutedPath>, ExecutePathsError> {
1799 let mut prefixes: HashMap<Vec<ReductionStep>, Vec<Rc<dyn DynReductionResult>>> =
1800 HashMap::new();
1801 let mut executed = Vec::with_capacity(paths.len());
1802 let mut batch_source: Option<&ReductionStep> = None;
1803 for (path_index, path) in paths.iter().enumerate() {
1804 let source = path
1805 .steps
1806 .first()
1807 .ok_or(ExecutePathsError::EmptyPath { path_index })?;
1808 if path.steps.len() < 2 {
1809 return Err(ExecutePathsError::NoEdges { path_index });
1810 }
1811 if let Some(expected) = batch_source {
1812 if source != expected {
1813 return Err(ExecutePathsError::DifferentSource { path_index });
1814 }
1815 } else {
1816 batch_source = Some(source);
1817 }
1818 let source_prefix = vec![source.clone()];
1819 let mut chain = prefixes.get(&source_prefix).cloned().unwrap_or_default();
1820 prefixes.entry(source_prefix.clone()).or_default();
1821 let mut prefix = source_prefix;
1822 for pair in path.steps.windows(2) {
1823 prefix.push(pair[1].clone());
1824 if let Some(cached) = prefixes.get(&prefix) {
1825 chain = cached.clone();
1826 continue;
1827 }
1828 let source_node = self
1829 .lookup_node(&pair[0].name, &pair[0].variant)
1830 .ok_or_else(|| ExecutePathsError::UnknownNode {
1831 path_index,
1832 problem: pair[0].name.clone(),
1833 variant: pair[0].variant.clone(),
1834 })?;
1835 let target_node_index = self
1836 .lookup_node(&pair[1].name, &pair[1].variant)
1837 .ok_or_else(|| ExecutePathsError::UnknownNode {
1838 path_index,
1839 problem: pair[1].name.clone(),
1840 variant: pair[1].variant.clone(),
1841 })?;
1842 let edge_index = self
1843 .graph
1844 .find_edge(source_node, target_node_index)
1845 .ok_or_else(|| ExecutePathsError::MissingEdge {
1846 path_index,
1847 source_problem: pair[0].name.clone(),
1848 target_problem: pair[1].name.clone(),
1849 })?;
1850 let edge_data = &self.graph[edge_index];
1851 let Some(reduce_fn) = edge_data.reduce_fn else {
1852 return Err(ExecutePathsError::NotWitnessExecutable {
1853 path_index,
1854 source_problem: pair[0].name.clone(),
1855 target_problem: pair[1].name.clone(),
1856 });
1857 };
1858 let current = chain
1859 .last()
1860 .map(|step| step.target_problem_any())
1861 .unwrap_or(source_instance);
1862 let result = reduce_fn(current)
1863 .map_err(|cause| ExecutePathsError::Reduction { path_index, cause })?;
1864 chain.push(Rc::from(result));
1865 prefixes.insert(prefix.clone(), chain.clone());
1866 }
1867 executed.push(ExecutedPath {
1868 path: path.clone(),
1869 steps: chain,
1870 });
1871 }
1872 Ok(executed)
1873 }
1874}
1875
1876#[cfg(test)]
1877impl ReductionGraph {
1878 pub(crate) fn from_test_edges(
1883 node_names: &[&'static str],
1884 edges: &[(&'static str, &'static str, ReductionEdgeData)],
1885 ) -> Self {
1886 Self::from_test_variant_edges(
1887 &node_names
1888 .iter()
1889 .map(|&name| (name, BTreeMap::new()))
1890 .collect::<Vec<_>>(),
1891 edges,
1892 )
1893 }
1894
1895 pub(crate) fn from_test_variant_edges(
1896 test_nodes: &[(&'static str, BTreeMap<String, String>)],
1897 edges: &[(&'static str, &'static str, ReductionEdgeData)],
1898 ) -> Self {
1899 let mut graph: DiGraph<usize, ReductionEdgeData> = DiGraph::new();
1900 let mut nodes: Vec<VariantNode> = Vec::new();
1901 let mut name_to_nodes: HashMap<&'static str, Vec<NodeIndex>> = HashMap::new();
1902 let mut index_of: HashMap<&'static str, NodeIndex> = HashMap::new();
1903
1904 for (name, variant) in test_nodes {
1905 let node_id = nodes.len();
1906 nodes.push(VariantNode {
1907 name,
1908 variant: variant.clone(),
1909 complexity: "",
1910 });
1911 let idx = graph.add_node(node_id);
1912 index_of.insert(name, idx);
1913 name_to_nodes.entry(name).or_default().push(idx);
1914 }
1915
1916 for (src, dst, data) in edges {
1917 let s = index_of[src];
1918 let d = index_of[dst];
1919 graph.add_edge(s, d, data.clone());
1920 }
1921
1922 Self {
1923 graph,
1924 nodes,
1925 name_to_nodes,
1926 default_variants: HashMap::new(),
1927 }
1928 }
1929}
1930
1931#[cfg(test)]
1932#[path = "../unit_tests/rules/graph.rs"]
1933mod tests;
1934
1935#[cfg(test)]
1936#[path = "../unit_tests/rules/reduction_path_parity.rs"]
1937mod reduction_path_parity_tests;
1938
1939#[cfg(test)]
1940#[path = "../unit_tests/rules/maximumindependentset_ilp.rs"]
1941mod maximumindependentset_ilp_path_tests;
1942
1943#[cfg(test)]
1944#[path = "../unit_tests/rules/minimumvertexcover_ilp.rs"]
1945mod minimumvertexcover_ilp_path_tests;
1946
1947#[cfg(test)]
1948#[path = "../unit_tests/rules/maximumindependentset_qubo.rs"]
1949mod maximumindependentset_qubo_path_tests;
1950
1951#[cfg(test)]
1952#[path = "../unit_tests/rules/minimumvertexcover_qubo.rs"]
1953mod minimumvertexcover_qubo_path_tests;