1use crate::rules::registry::{
14 AggregateReduceFn, EdgeCapabilities, ParameterContractError, ReduceFn,
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 aggregate_view_fn: Option<crate::rules::registry::AggregateViewFn>,
48 pub turing: bool,
49}
50
51impl ReductionEdgeData {
52 fn capabilities(&self) -> EdgeCapabilities {
53 EdgeCapabilities::from_executors(self.reduce_fn, self.reduce_aggregate_fn, self.turing)
54 }
55}
56
57#[derive(Debug, Clone, Serialize)]
59pub(crate) struct ReductionGraphJson {
60 pub(crate) nodes: Vec<NodeJson>,
62 pub(crate) edges: Vec<EdgeJson>,
64}
65
66impl ReductionGraphJson {
67 #[cfg_attr(not(test), allow(dead_code))]
69 pub(crate) fn source_node(&self, edge: &EdgeJson) -> &NodeJson {
70 &self.nodes[edge.source]
71 }
72
73 #[cfg_attr(not(test), allow(dead_code))]
75 pub(crate) fn target_node(&self, edge: &EdgeJson) -> &NodeJson {
76 &self.nodes[edge.target]
77 }
78}
79
80#[derive(Debug, Clone, Serialize)]
82pub(crate) struct NodeJson {
83 pub(crate) name: String,
85 pub(crate) variant: BTreeMap<String, String>,
87 pub(crate) category: crate::registry::ProblemCategory,
89 pub(crate) doc_path: String,
91 pub(crate) complexity: String,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97struct VariantRef {
98 name: String,
99 variant: BTreeMap<String, String>,
100}
101
102#[derive(Debug, Clone, Serialize)]
104pub(crate) struct ParameterFieldJson {
105 pub(crate) field: String,
106 pub(crate) contract: &'static str,
107 pub(crate) formula: Option<String>,
108 pub(crate) reason: Option<String>,
109}
110
111#[derive(Debug, Clone, Serialize)]
113pub(crate) struct EdgeJson {
114 pub(crate) source: usize,
116 pub(crate) target: usize,
118 pub(crate) parameters: Vec<ParameterFieldJson>,
120 pub(crate) parameter_contract_error: Option<String>,
121 pub(crate) doc_path: String,
123 pub(crate) witness: bool,
125 pub(crate) aggregate: bool,
127 pub(crate) turing: bool,
129}
130
131#[derive(Debug, Clone)]
133pub struct ReductionPath {
134 pub steps: Vec<ReductionStep>,
136}
137
138#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
140pub enum ExecutePathsError {
141 #[error("concrete path {path_index} is empty")]
142 EmptyPath { path_index: usize },
143 #[error("concrete path {path_index} contains no reduction edge")]
144 NoEdges { path_index: usize },
145 #[error("concrete path {path_index} starts at a different source node")]
146 DifferentSource { path_index: usize },
147 #[error("concrete path {path_index} references unknown node {problem} {variant:?}")]
148 UnknownNode {
149 path_index: usize,
150 problem: String,
151 variant: BTreeMap<String, String>,
152 },
153 #[error("concrete path {path_index} has no registered edge from {source_problem} to {target_problem}")]
154 MissingEdge {
155 path_index: usize,
156 source_problem: String,
157 target_problem: String,
158 },
159 #[error("concrete path {path_index} edge {source_problem} -> {target_problem} is not witness-executable")]
160 NotWitnessExecutable {
161 path_index: usize,
162 source_problem: String,
163 target_problem: String,
164 },
165 #[error("concrete path {path_index} failed during reduction: {cause}")]
166 Reduction {
167 path_index: usize,
168 #[source]
169 cause: crate::rules::ReductionError,
170 },
171}
172
173#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
175pub enum PathParameterError {
176 #[error("cannot compose an empty reduction path")]
177 EmptyPath,
178 #[error("reduction path references unknown node {problem} {variant:?}")]
179 UnknownNode {
180 problem: String,
181 variant: BTreeMap<String, String>,
182 },
183 #[error(
184 "reduction path contains no registered edge from {source_problem} to {target_problem}"
185 )]
186 MissingEdge {
187 source_problem: String,
188 target_problem: String,
189 },
190 #[error("reduction step {step} ({source_problem} -> {target_problem}) is a multi-query reduction without a query-cost model")]
191 TuringEdge {
192 step: usize,
193 source_problem: String,
194 target_problem: String,
195 },
196 #[error("reduction step {step} ({source_problem} -> {target_problem}) has an invalid parameter contract: {error}")]
197 InvalidContract {
198 step: usize,
199 source_problem: String,
200 target_problem: String,
201 #[source]
202 error: Box<ParameterContractError>,
203 },
204 #[error("reduction step {step} ({source_problem} -> {target_problem}) has no symbolic parameter transform")]
205 Unavailable {
206 step: usize,
207 source_problem: String,
208 target_problem: String,
209 },
210 #[error(
211 "cannot compose reduction step {step} ({source_problem} -> {target_problem}): {error}"
212 )]
213 Step {
214 step: usize,
215 source_problem: String,
216 target_problem: String,
217 #[source]
218 error: Box<crate::parameters::ParameterTransformError>,
219 },
220}
221
222impl ReductionPath {
223 pub fn len(&self) -> usize {
225 if self.steps.is_empty() {
226 0
227 } else {
228 self.steps.len() - 1
229 }
230 }
231
232 pub fn is_empty(&self) -> bool {
234 self.steps.is_empty()
235 }
236
237 pub fn source(&self) -> Option<&str> {
239 self.steps.first().map(|s| s.name.as_str())
240 }
241
242 pub fn target(&self) -> Option<&str> {
244 self.steps.last().map(|s| s.name.as_str())
245 }
246
247 pub fn type_names(&self) -> Vec<&str> {
249 let mut names: Vec<&str> = Vec::new();
250 for step in &self.steps {
251 if names.last() != Some(&step.name.as_str()) {
252 names.push(&step.name);
253 }
254 }
255 names
256 }
257}
258
259impl std::fmt::Display for ReductionPath {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 let mut prev_name = "";
262 for step in &self.steps {
263 if step.name != prev_name {
264 if prev_name.is_empty() {
265 write!(f, "{step}")?;
266 } else {
267 write!(f, " → {step}")?;
268 }
269 prev_name = &step.name;
270 }
271 }
272 Ok(())
273 }
274}
275
276#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize)]
278pub struct ReductionStep {
279 pub name: String,
281 pub variant: BTreeMap<String, String>,
283}
284
285impl std::fmt::Display for ReductionStep {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 write!(f, "{}", self.name)?;
288 if !self.variant.is_empty() {
289 let vars: Vec<_> = self
290 .variant
291 .iter()
292 .map(|(k, v)| format!("{k}: {v:?}"))
293 .collect();
294 write!(f, " {{{}}}", vars.join(", "))?;
295 }
296 Ok(())
297 }
298}
299
300#[derive(Debug, Clone)]
302struct VariantNode {
303 name: &'static str,
304 variant: BTreeMap<String, String>,
305 complexity: &'static str,
306}
307
308#[derive(Debug, Clone)]
310pub struct NeighborInfo {
311 pub name: &'static str,
313 pub variant: BTreeMap<String, String>,
315 pub hops: usize,
317}
318
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321pub enum TraversalFlow {
322 Outgoing,
324 Incoming,
326 Both,
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum ReductionMode {
333 Witness,
334 Aggregate,
335 Turing,
338}
339
340#[derive(Debug, Clone)]
342pub struct NeighborTree {
343 pub name: String,
345 pub variant: BTreeMap<String, String>,
347 pub children: Vec<NeighborTree>,
349}
350
351pub struct ReductionGraph {
360 graph: DiGraph<usize, ReductionEdgeData>,
362 nodes: Vec<VariantNode>,
364 name_to_nodes: HashMap<&'static str, Vec<NodeIndex>>,
366 default_variants: HashMap<String, BTreeMap<String, String>>,
368}
369
370impl ReductionGraph {
371 pub fn new() -> Self {
373 crate::registry::validate_variant_parameter_schemas().unwrap_or_else(|errors| {
374 panic!("invalid problem parameters schemas:\n{}", errors.join("\n"))
375 });
376 crate::rules::registry::validate_reduction_parameter_schemas().unwrap_or_else(|errors| {
377 panic!(
378 "invalid reduction parameter schemas:\n{}",
379 errors.join("\n")
380 )
381 });
382 let mut graph = DiGraph::new();
383 let mut nodes: Vec<VariantNode> = Vec::new();
384 let mut node_index: HashMap<VariantRef, NodeIndex> = HashMap::new();
385 let mut name_to_nodes: HashMap<&'static str, Vec<NodeIndex>> = HashMap::new();
386
387 let ensure_node = |name: &'static str,
389 variant: BTreeMap<String, String>,
390 complexity: &'static str,
391 nodes: &mut Vec<VariantNode>,
392 graph: &mut DiGraph<usize, ReductionEdgeData>,
393 node_index: &mut HashMap<VariantRef, NodeIndex>,
394 name_to_nodes: &mut HashMap<&'static str, Vec<NodeIndex>>|
395 -> NodeIndex {
396 let vref = VariantRef {
397 name: name.to_string(),
398 variant: variant.clone(),
399 };
400 if let Some(&idx) = node_index.get(&vref) {
401 idx
402 } else {
403 let node_id = nodes.len();
404 nodes.push(VariantNode {
405 name,
406 variant,
407 complexity,
408 });
409 let idx = graph.add_node(node_id);
410 node_index.insert(vref, idx);
411 name_to_nodes.entry(name).or_default().push(idx);
412 idx
413 }
414 };
415
416 let mut default_variants: HashMap<String, BTreeMap<String, String>> = HashMap::new();
418
419 for entry in inventory::iter::<crate::registry::VariantEntry> {
421 let variant = Self::variant_to_map(&entry.variant());
422 ensure_node(
423 entry.name,
424 variant.clone(),
425 entry.complexity,
426 &mut nodes,
427 &mut graph,
428 &mut node_index,
429 &mut name_to_nodes,
430 );
431 if entry.is_default {
432 default_variants.insert(entry.name.to_string(), variant);
433 }
434 }
435
436 for entry in crate::rules::registry::reduction_entries() {
438 let source_variant = Self::variant_to_map(&entry.source_variant());
439 let target_variant = Self::variant_to_map(&entry.target_variant());
440
441 let src_idx = node_index[&VariantRef {
442 name: entry.source_name.to_string(),
443 variant: source_variant,
444 }];
445 let dst_idx = node_index[&VariantRef {
446 name: entry.target_name.to_string(),
447 variant: target_variant,
448 }];
449
450 let parameter_contract = entry.parameter_contract();
451 if graph.find_edge(src_idx, dst_idx).is_none() {
452 graph.add_edge(
453 src_idx,
454 dst_idx,
455 ReductionEdgeData {
456 parameter_contract,
457 reduce_fn: entry.reduce_fn,
458 reduce_aggregate_fn: entry.reduce_aggregate_fn,
459 aggregate_view_fn: entry.aggregate_view_fn,
460 turing: entry.turing,
461 },
462 );
463 }
464 }
465
466 Self {
467 graph,
468 nodes,
469 name_to_nodes,
470 default_variants,
471 }
472 }
473
474 pub fn variant_to_map(variant: &[(&str, &str)]) -> BTreeMap<String, String> {
477 variant
478 .iter()
479 .map(|(k, v)| {
480 let value = if *k == "graph" && v.is_empty() {
481 "SimpleGraph".to_string()
482 } else {
483 v.to_string()
484 };
485 (k.to_string(), value)
486 })
487 .collect()
488 }
489
490 fn lookup_node(&self, name: &str, variant: &BTreeMap<String, String>) -> Option<NodeIndex> {
492 let nodes = self.name_to_nodes.get(name)?;
493 nodes
494 .iter()
495 .find(|&&idx| self.nodes[self.graph[idx]].variant == *variant)
496 .copied()
497 }
498
499 fn edge_supports_mode(edge: &ReductionEdgeData, mode: ReductionMode) -> bool {
500 match mode {
501 ReductionMode::Witness => edge.reduce_fn.is_some(),
502 ReductionMode::Aggregate => edge.reduce_aggregate_fn.is_some(),
503 ReductionMode::Turing => edge.turing,
504 }
505 }
506
507 fn ordered_outgoing_edges(
508 &self,
509 node: NodeIndex,
510 mode: ReductionMode,
511 ) -> Vec<(NodeIndex, EdgeIndex)> {
512 let mut edges: Vec<_> = self
513 .graph
514 .edges(node)
515 .filter(|edge| Self::edge_supports_mode(edge.weight(), mode))
516 .map(|edge| (edge.target(), edge.id()))
517 .collect();
518 edges.sort_by(|a, b| {
519 let a = &self.nodes[self.graph[a.0]];
520 let b = &self.nodes[self.graph[b.0]];
521 (a.name, &a.variant).cmp(&(b.name, &b.variant))
522 });
523 edges
524 }
525
526 fn node_path_supports_mode(&self, node_path: &[NodeIndex], mode: ReductionMode) -> bool {
527 node_path.windows(2).all(|pair| {
528 self.graph
529 .find_edge(pair[0], pair[1])
530 .is_some_and(|edge_idx| Self::edge_supports_mode(&self.graph[edge_idx], mode))
531 })
532 }
533
534 fn node_path_to_reduction_path(&self, node_path: &[NodeIndex]) -> ReductionPath {
536 let steps = node_path
537 .iter()
538 .map(|&idx| {
539 let node = &self.nodes[self.graph[idx]];
540 ReductionStep {
541 name: node.name.to_string(),
542 variant: node.variant.clone(),
543 }
544 })
545 .collect();
546 ReductionPath { steps }
547 }
548
549 fn node_path_order_key(&self, node_path: &[NodeIndex]) -> NodePathOrderKey<'_> {
550 (
551 node_path.len().saturating_sub(1),
552 node_path
553 .iter()
554 .map(|&idx| {
555 let node = &self.nodes[self.graph[idx]];
556 (node.name, &node.variant)
557 })
558 .collect(),
559 )
560 }
561
562 #[allow(clippy::too_many_arguments)]
563 fn shortest_node_path(
564 &self,
565 source: NodeIndex,
566 target: NodeIndex,
567 adjacency: &[Vec<NodeIndex>],
568 excluded_nodes: &HashSet<NodeIndex>,
569 excluded_edges: &HashSet<(NodeIndex, NodeIndex)>,
570 max_nodes: usize,
571 ) -> Option<Vec<NodeIndex>> {
572 if excluded_nodes.contains(&source) || max_nodes == 0 {
573 return None;
574 }
575 if source == target {
576 return Some(vec![source]);
577 }
578
579 let mut queue = VecDeque::from([(source, 1usize)]);
580 let mut parents = HashMap::new();
581 let mut visited = HashSet::from([source]);
582
583 while let Some((current, path_nodes)) = queue.pop_front() {
584 if path_nodes == max_nodes {
585 continue;
586 }
587 for &next in &adjacency[current.index()] {
588 if excluded_nodes.contains(&next)
589 || excluded_edges.contains(&(current, next))
590 || !visited.insert(next)
591 {
592 continue;
593 }
594 parents.insert(next, current);
595 if next == target {
596 let mut path = vec![target];
597 let mut node = target;
598 while node != source {
599 node = parents[&node];
600 path.push(node);
601 }
602 path.reverse();
603 return Some(path);
604 }
605 queue.push_back((next, path_nodes + 1));
606 }
607 }
608 None
609 }
610
611 fn find_k_shortest_node_paths(
612 &self,
613 source: NodeIndex,
614 target: NodeIndex,
615 mode: ReductionMode,
616 limit: usize,
617 max_nodes: usize,
618 ) -> Vec<Vec<NodeIndex>> {
619 if source == target || limit == 0 {
620 return Vec::new();
621 }
622
623 let mut adjacency = vec![Vec::new(); self.graph.node_count()];
624 for node in self.graph.node_indices() {
625 adjacency[node.index()] = self
626 .ordered_outgoing_edges(node, mode)
627 .into_iter()
628 .map(|(target, _)| target)
629 .collect();
630 }
631
632 let Some(first) = self.shortest_node_path(
633 source,
634 target,
635 &adjacency,
636 &HashSet::new(),
637 &HashSet::new(),
638 max_nodes,
639 ) else {
640 return Vec::new();
641 };
642
643 let mut accepted = vec![first];
644 let mut candidates = BTreeSet::new();
645
646 while accepted.len() < limit {
647 let previous = accepted.last().expect("accepted path exists");
648 for spur_index in 0..previous.len().saturating_sub(1) {
649 let root = &previous[..=spur_index];
650 let excluded_edges = accepted
651 .iter()
652 .filter(|path| path.len() > spur_index + 1 && path[..=spur_index] == *root)
653 .map(|path| (path[spur_index], path[spur_index + 1]))
654 .collect::<HashSet<_>>();
655 let excluded_nodes = root[..spur_index].iter().copied().collect();
656 let max_spur_nodes = max_nodes.saturating_sub(spur_index);
657 let Some(spur) = self.shortest_node_path(
658 previous[spur_index],
659 target,
660 &adjacency,
661 &excluded_nodes,
662 &excluded_edges,
663 max_spur_nodes,
664 ) else {
665 continue;
666 };
667 let mut candidate = root[..spur_index].to_vec();
668 candidate.extend(spur);
669 if !accepted.contains(&candidate) {
670 let key = self.node_path_order_key(&candidate);
671 candidates.insert((key, candidate));
672 }
673 }
674
675 let Some((_, next)) = candidates.pop_first() else {
676 break;
677 };
678 accepted.push(next);
679 }
680
681 accepted
682 }
683
684 pub fn find_all_paths(
689 &self,
690 source: &str,
691 source_variant: &BTreeMap<String, String>,
692 target: &str,
693 target_variant: &BTreeMap<String, String>,
694 ) -> Vec<ReductionPath> {
695 self.find_all_paths_mode(
696 source,
697 source_variant,
698 target,
699 target_variant,
700 ReductionMode::Witness,
701 )
702 }
703
704 pub fn find_all_paths_mode(
707 &self,
708 source: &str,
709 source_variant: &BTreeMap<String, String>,
710 target: &str,
711 target_variant: &BTreeMap<String, String>,
712 mode: ReductionMode,
713 ) -> Vec<ReductionPath> {
714 let src = match self.lookup_node(source, source_variant) {
715 Some(idx) => idx,
716 None => return vec![],
717 };
718 let dst = match self.lookup_node(target, target_variant) {
719 Some(idx) => idx,
720 None => return vec![],
721 };
722
723 let paths: Vec<Vec<NodeIndex>> = all_simple_paths::<
724 Vec<NodeIndex>,
725 _,
726 std::hash::RandomState,
727 >(&self.graph, src, dst, 0, None)
728 .collect();
729
730 paths
731 .iter()
732 .filter(|p| self.node_path_supports_mode(p, mode))
733 .map(|p| self.node_path_to_reduction_path(p))
734 .collect()
735 }
736
737 pub fn find_paths_up_to(
743 &self,
744 source: &str,
745 source_variant: &BTreeMap<String, String>,
746 target: &str,
747 target_variant: &BTreeMap<String, String>,
748 limit: usize,
749 ) -> Vec<ReductionPath> {
750 self.find_paths_up_to_mode_bounded(
751 source,
752 source_variant,
753 target,
754 target_variant,
755 ReductionMode::Witness,
756 limit,
757 None,
758 )
759 }
760
761 pub fn find_paths_up_to_mode(
764 &self,
765 source: &str,
766 source_variant: &BTreeMap<String, String>,
767 target: &str,
768 target_variant: &BTreeMap<String, String>,
769 mode: ReductionMode,
770 limit: usize,
771 ) -> Vec<ReductionPath> {
772 self.find_paths_up_to_mode_bounded(
773 source,
774 source_variant,
775 target,
776 target_variant,
777 mode,
778 limit,
779 None,
780 )
781 }
782
783 #[allow(clippy::too_many_arguments)]
786 pub fn find_paths_up_to_mode_bounded(
787 &self,
788 source: &str,
789 source_variant: &BTreeMap<String, String>,
790 target: &str,
791 target_variant: &BTreeMap<String, String>,
792 mode: ReductionMode,
793 limit: usize,
794 max_intermediate_nodes: Option<usize>,
795 ) -> Vec<ReductionPath> {
796 let src = match self.lookup_node(source, source_variant) {
797 Some(idx) => idx,
798 None => return vec![],
799 };
800 let dst = match self.lookup_node(target, target_variant) {
801 Some(idx) => idx,
802 None => return vec![],
803 };
804
805 if limit == 0 {
806 return Vec::new();
807 }
808
809 let max_intermediate =
810 max_intermediate_nodes.unwrap_or_else(|| self.graph.node_count().saturating_sub(2));
811 let max_nodes = max_intermediate.saturating_add(2);
812 self.find_k_shortest_node_paths(src, dst, mode, limit, max_nodes)
813 .iter()
814 .map(|path| self.node_path_to_reduction_path(path))
815 .collect()
816 }
817
818 pub fn has_direct_reduction<S: crate::traits::Problem, T: crate::traits::Problem>(
820 &self,
821 ) -> bool {
822 self.has_direct_reduction_by_name(S::NAME, T::NAME)
823 }
824
825 pub fn has_direct_reduction_by_name(&self, src: &str, dst: &str) -> bool {
827 let src_nodes = match self.name_to_nodes.get(src) {
828 Some(nodes) => nodes,
829 None => return false,
830 };
831 let dst_nodes = match self.name_to_nodes.get(dst) {
832 Some(nodes) => nodes,
833 None => return false,
834 };
835
836 let dst_set: HashSet<NodeIndex> = dst_nodes.iter().copied().collect();
837
838 for &src_idx in src_nodes {
839 for edge_ref in self.graph.edges(src_idx) {
840 if dst_set.contains(&edge_ref.target()) {
841 return true;
842 }
843 }
844 }
845
846 false
847 }
848
849 pub fn has_direct_reduction_by_name_mode(
851 &self,
852 src: &str,
853 dst: &str,
854 mode: ReductionMode,
855 ) -> bool {
856 let src_nodes = match self.name_to_nodes.get(src) {
857 Some(nodes) => nodes,
858 None => return false,
859 };
860 let dst_nodes = match self.name_to_nodes.get(dst) {
861 Some(nodes) => nodes,
862 None => return false,
863 };
864
865 let dst_set: HashSet<NodeIndex> = dst_nodes.iter().copied().collect();
866
867 for &src_idx in src_nodes {
868 for edge_ref in self.graph.edges(src_idx) {
869 if dst_set.contains(&edge_ref.target())
870 && Self::edge_supports_mode(edge_ref.weight(), mode)
871 {
872 return true;
873 }
874 }
875 }
876
877 false
878 }
879
880 pub fn has_direct_reduction_mode<S: crate::traits::Problem, T: crate::traits::Problem>(
882 &self,
883 mode: ReductionMode,
884 ) -> bool {
885 self.has_direct_reduction_by_name_mode(S::NAME, T::NAME, mode)
886 }
887
888 pub fn problem_types(&self) -> Vec<&'static str> {
890 self.name_to_nodes.keys().copied().collect()
891 }
892
893 pub fn num_types(&self) -> usize {
895 self.name_to_nodes.len()
896 }
897
898 pub fn num_reductions(&self) -> usize {
900 self.graph.edge_count()
901 }
902
903 pub fn num_variant_nodes(&self) -> usize {
905 self.nodes.len()
906 }
907
908 pub fn path_parameter_transforms(
910 &self,
911 path: &ReductionPath,
912 ) -> Result<Vec<crate::parameters::ParameterTransform>, PathParameterError> {
913 if path.steps.len() <= 1 {
914 return Ok(vec![]);
915 }
916
917 let node_indices: Vec<NodeIndex> = path
918 .steps
919 .iter()
920 .map(|step| {
921 self.lookup_node(&step.name, &step.variant).ok_or_else(|| {
922 PathParameterError::UnknownNode {
923 problem: step.name.clone(),
924 variant: step.variant.clone(),
925 }
926 })
927 })
928 .collect::<Result<_, _>>()?;
929
930 node_indices
931 .windows(2)
932 .enumerate()
933 .map(|(index, pair)| {
934 let edge_idx = self.graph.find_edge(pair[0], pair[1]).ok_or_else(|| {
935 PathParameterError::MissingEdge {
936 source_problem: path.steps[index].name.clone(),
937 target_problem: path.steps[index + 1].name.clone(),
938 }
939 })?;
940 if self.graph[edge_idx].turing {
941 return Err(PathParameterError::TuringEdge {
942 step: index + 1,
943 source_problem: path.steps[index].name.clone(),
944 target_problem: path.steps[index + 1].name.clone(),
945 });
946 }
947 let contract =
948 self.graph[edge_idx]
949 .parameter_contract
950 .as_ref()
951 .map_err(|error| PathParameterError::InvalidContract {
952 step: index + 1,
953 source_problem: path.steps[index].name.clone(),
954 target_problem: path.steps[index + 1].name.clone(),
955 error: Box::new(error.clone()),
956 })?;
957 let mut transform = contract.transform().cloned().unwrap_or_else(|| {
958 crate::parameters::ParameterTransform::new(
959 format!(
960 "{} -> {}",
961 path.steps[index].name,
962 path.steps[index + 1].name
963 ),
964 crate::parameters::ParameterRelation::Exact,
965 Vec::<(&str, crate::expr::Expr)>::new(),
966 )
967 .expect("empty transform is valid")
968 });
969 for field in contract.unavailable() {
970 transform.declare_unavailable(field.field, field.reason);
971 }
972 Ok(transform)
973 })
974 .collect()
975 }
976
977 pub fn compose_path_parameter_transform(
979 &self,
980 path: &ReductionPath,
981 ) -> Result<Option<crate::parameters::ParameterTransform>, PathParameterError> {
982 if path.steps.is_empty() {
983 return Err(PathParameterError::EmptyPath);
984 }
985 if path.steps.len() == 1 {
986 return Ok(None);
987 }
988
989 let mut transforms = self.path_parameter_transforms(path)?.into_iter();
990 let Some(mut composed) = transforms.next() else {
991 return Ok(None);
992 };
993 for (offset, transform) in transforms.enumerate() {
994 let edge_index = offset + 1;
995 composed = composed
996 .compose(
997 &transform,
998 format!(
999 "{} -> {}",
1000 path.steps[0].name,
1001 path.steps[edge_index + 1].name
1002 ),
1003 )
1004 .map_err(|error| PathParameterError::Step {
1005 step: edge_index + 1,
1006 source_problem: path.steps[edge_index].name.clone(),
1007 target_problem: path.steps[edge_index + 1].name.clone(),
1008 error: Box::new(error),
1009 })?;
1010 }
1011 Ok(Some(composed))
1012 }
1013
1014 pub fn variants_for(&self, name: &str) -> Vec<BTreeMap<String, String>> {
1019 let mut variants: Vec<BTreeMap<String, String>> = self
1020 .name_to_nodes
1021 .get(name)
1022 .map(|indices| {
1023 indices
1024 .iter()
1025 .map(|&idx| self.nodes[self.graph[idx]].variant.clone())
1026 .collect()
1027 })
1028 .unwrap_or_default();
1029 variants.sort();
1030 if let Some(default) = self.default_variants.get(name) {
1031 if let Some(index) = variants.iter().position(|variant| variant == default) {
1032 let default = variants.remove(index);
1033 variants.insert(0, default);
1034 }
1035 }
1036 variants
1037 }
1038
1039 pub fn default_variant_for(&self, name: &str) -> Option<BTreeMap<String, String>> {
1046 self.default_variants.get(name).cloned()
1047 }
1048
1049 pub fn variant_complexity(
1051 &self,
1052 name: &str,
1053 variant: &BTreeMap<String, String>,
1054 ) -> Option<&'static str> {
1055 let idx = self.lookup_node(name, variant)?;
1056 let node = &self.nodes[self.graph[idx]];
1057 if node.complexity.is_empty() {
1058 None
1059 } else {
1060 Some(node.complexity)
1061 }
1062 }
1063
1064 pub fn outgoing_reductions(&self, name: &str) -> Vec<ReductionEdgeInfo> {
1066 let Some(indices) = self.name_to_nodes.get(name) else {
1067 return vec![];
1068 };
1069 let index_set: HashSet<NodeIndex> = indices.iter().copied().collect();
1070 self.graph
1071 .edge_references()
1072 .filter(|e| index_set.contains(&e.source()))
1073 .map(|e| {
1074 let src = &self.nodes[self.graph[e.source()]];
1075 let dst = &self.nodes[self.graph[e.target()]];
1076 ReductionEdgeInfo {
1077 source_name: src.name,
1078 source_variant: src.variant.clone(),
1079 target_name: dst.name,
1080 target_variant: dst.variant.clone(),
1081 parameter_contract: self.graph[e.id()].parameter_contract.clone(),
1082 capabilities: self.graph[e.id()].capabilities(),
1083 }
1084 })
1085 .collect()
1086 }
1087
1088 pub fn outgoing_reductions_from(
1094 &self,
1095 name: &str,
1096 variant: &BTreeMap<String, String>,
1097 mode: ReductionMode,
1098 ) -> Vec<ReductionEdgeInfo> {
1099 let source = self
1100 .lookup_node(name, variant)
1101 .unwrap_or_else(|| panic!("registered problem variant not found: {name} {variant:?}"));
1102
1103 self.ordered_outgoing_edges(source, mode)
1104 .into_iter()
1105 .map(|(target, edge)| {
1106 let src = &self.nodes[self.graph[source]];
1107 let dst = &self.nodes[self.graph[target]];
1108 ReductionEdgeInfo {
1109 source_name: src.name,
1110 source_variant: src.variant.clone(),
1111 target_name: dst.name,
1112 target_variant: dst.variant.clone(),
1113 parameter_contract: self.graph[edge].parameter_contract.clone(),
1114 capabilities: self.graph[edge].capabilities(),
1115 }
1116 })
1117 .collect()
1118 }
1119
1120 pub fn parameter_names(&self, name: &str) -> Vec<String> {
1122 inventory::iter::<crate::registry::VariantEntry>
1123 .into_iter()
1124 .find(|entry| entry.name == name)
1125 .map(|entry| {
1126 entry
1127 .parameter_names()
1128 .iter()
1129 .map(|name| (*name).to_string())
1130 .collect()
1131 })
1132 .unwrap_or_default()
1133 }
1134
1135 pub fn compute_problem_parameters(
1137 name: &str,
1138 variant: &BTreeMap<String, String>,
1139 instance: &dyn Any,
1140 ) -> ProblemParameters {
1141 let entry = crate::registry::find_variant_entry(name, variant)
1142 .unwrap_or_else(|| panic!("unregistered exact problem variant `{name}` {variant:?}"));
1143 (entry.parameter_measure_fn)(instance)
1144 }
1145
1146 pub fn incoming_reductions(&self, name: &str) -> Vec<ReductionEdgeInfo> {
1148 let Some(indices) = self.name_to_nodes.get(name) else {
1149 return vec![];
1150 };
1151 let index_set: HashSet<NodeIndex> = indices.iter().copied().collect();
1152 self.graph
1153 .edge_references()
1154 .filter(|e| index_set.contains(&e.target()))
1155 .map(|e| {
1156 let src = &self.nodes[self.graph[e.source()]];
1157 let dst = &self.nodes[self.graph[e.target()]];
1158 ReductionEdgeInfo {
1159 source_name: src.name,
1160 source_variant: src.variant.clone(),
1161 target_name: dst.name,
1162 target_variant: dst.variant.clone(),
1163 parameter_contract: self.graph[e.id()].parameter_contract.clone(),
1164 capabilities: self.graph[e.id()].capabilities(),
1165 }
1166 })
1167 .collect()
1168 }
1169
1170 pub fn k_neighbors(
1175 &self,
1176 name: &str,
1177 variant: &BTreeMap<String, String>,
1178 max_hops: usize,
1179 direction: TraversalFlow,
1180 ) -> Vec<NeighborInfo> {
1181 use std::collections::VecDeque;
1182
1183 let Some(start_idx) = self.lookup_node(name, variant) else {
1184 return vec![];
1185 };
1186
1187 let mut visited: HashSet<NodeIndex> = HashSet::new();
1188 visited.insert(start_idx);
1189 let mut queue: VecDeque<(NodeIndex, usize)> = VecDeque::new();
1190 queue.push_back((start_idx, 0));
1191 let mut results: Vec<NeighborInfo> = Vec::new();
1192
1193 while let Some((node_idx, hops)) = queue.pop_front() {
1194 if hops >= max_hops {
1195 continue;
1196 }
1197
1198 let directions = match direction {
1199 TraversalFlow::Outgoing => vec![petgraph::Outgoing],
1200 TraversalFlow::Incoming => vec![petgraph::Incoming],
1201 TraversalFlow::Both => {
1202 vec![petgraph::Outgoing, petgraph::Incoming]
1203 }
1204 };
1205
1206 for dir in directions {
1207 for neighbor_idx in self.graph.neighbors_directed(node_idx, dir) {
1208 if visited.insert(neighbor_idx) {
1209 let neighbor_node = &self.nodes[self.graph[neighbor_idx]];
1210 results.push(NeighborInfo {
1211 name: neighbor_node.name,
1212 variant: neighbor_node.variant.clone(),
1213 hops: hops + 1,
1214 });
1215 queue.push_back((neighbor_idx, hops + 1));
1216 }
1217 }
1218 }
1219 }
1220
1221 results.sort_by(|a, b| a.hops.cmp(&b.hops).then_with(|| a.name.cmp(b.name)));
1222 results
1223 }
1224
1225 pub fn k_neighbor_tree(
1230 &self,
1231 name: &str,
1232 variant: &BTreeMap<String, String>,
1233 max_hops: usize,
1234 direction: TraversalFlow,
1235 ) -> Vec<NeighborTree> {
1236 use std::collections::VecDeque;
1237
1238 let Some(start_idx) = self.lookup_node(name, variant) else {
1239 return vec![];
1240 };
1241
1242 let mut visited: HashSet<NodeIndex> = HashSet::new();
1243 visited.insert(start_idx);
1244
1245 let mut queue: VecDeque<(NodeIndex, usize)> = VecDeque::new();
1246 queue.push_back((start_idx, 0));
1247
1248 let mut node_children: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
1250
1251 while let Some((node_idx, depth)) = queue.pop_front() {
1252 if depth >= max_hops {
1253 continue;
1254 }
1255
1256 let directions = match direction {
1257 TraversalFlow::Outgoing => vec![petgraph::Outgoing],
1258 TraversalFlow::Incoming => vec![petgraph::Incoming],
1259 TraversalFlow::Both => {
1260 vec![petgraph::Outgoing, petgraph::Incoming]
1261 }
1262 };
1263
1264 let mut children = Vec::new();
1265 for dir in directions {
1266 for neighbor_idx in self.graph.neighbors_directed(node_idx, dir) {
1267 if visited.insert(neighbor_idx) {
1268 children.push(neighbor_idx);
1269 queue.push_back((neighbor_idx, depth + 1));
1270 }
1271 }
1272 }
1273 children.sort_by(|a, b| {
1274 self.nodes[self.graph[*a]]
1275 .name
1276 .cmp(self.nodes[self.graph[*b]].name)
1277 });
1278 node_children.insert(node_idx, children);
1279 }
1280
1281 fn build(
1283 idx: NodeIndex,
1284 node_children: &HashMap<NodeIndex, Vec<NodeIndex>>,
1285 nodes: &[VariantNode],
1286 graph: &DiGraph<usize, ReductionEdgeData>,
1287 ) -> NeighborTree {
1288 let children = node_children
1289 .get(&idx)
1290 .map(|cs| {
1291 cs.iter()
1292 .map(|&c| build(c, node_children, nodes, graph))
1293 .collect()
1294 })
1295 .unwrap_or_default();
1296 let node = &nodes[graph[idx]];
1297 NeighborTree {
1298 name: node.name.to_string(),
1299 variant: node.variant.clone(),
1300 children,
1301 }
1302 }
1303
1304 node_children
1305 .get(&start_idx)
1306 .map(|cs| {
1307 cs.iter()
1308 .map(|&c| build(c, &node_children, &self.nodes, &self.graph))
1309 .collect()
1310 })
1311 .unwrap_or_default()
1312 }
1313}
1314
1315impl Default for ReductionGraph {
1316 fn default() -> Self {
1317 Self::new()
1318 }
1319}
1320
1321impl ReductionGraph {
1322 pub(crate) fn to_json(&self) -> ReductionGraphJson {
1326 use crate::registry::ProblemSchemaEntry;
1327
1328 let schema_metadata: HashMap<&str, (&str, crate::registry::ProblemCategory)> =
1330 inventory::iter::<ProblemSchemaEntry>
1331 .into_iter()
1332 .map(|entry| (entry.name, (entry.module_path, entry.category)))
1333 .collect();
1334
1335 let mut json_nodes: Vec<(usize, NodeJson)> = self
1337 .nodes
1338 .iter()
1339 .enumerate()
1340 .map(|(i, node)| {
1341 let &(module_path, category) =
1342 schema_metadata.get(node.name).unwrap_or_else(|| {
1343 panic!(
1344 "missing problem schema for registered variant `{}`",
1345 node.name
1346 )
1347 });
1348 (
1349 i,
1350 NodeJson {
1351 name: node.name.to_string(),
1352 variant: node.variant.clone(),
1353 category,
1354 doc_path: Self::doc_path_from_module_path(module_path, node.name),
1355 complexity: node.complexity.to_string(),
1356 },
1357 )
1358 })
1359 .collect();
1360 json_nodes.sort_by(|a, b| (&a.1.name, &a.1.variant).cmp(&(&b.1.name, &b.1.variant)));
1361
1362 let mut old_to_new: HashMap<usize, usize> = HashMap::new();
1364 for (new_idx, (old_idx, _)) in json_nodes.iter().enumerate() {
1365 old_to_new.insert(*old_idx, new_idx);
1366 }
1367
1368 let nodes: Vec<NodeJson> = json_nodes.into_iter().map(|(_, n)| n).collect();
1369
1370 let mut edges: Vec<EdgeJson> = Vec::new();
1372 for edge_ref in self.graph.edge_references() {
1373 let src_node_id = self.graph[edge_ref.source()];
1374 let dst_node_id = self.graph[edge_ref.target()];
1375 let contract = &edge_ref.weight().parameter_contract;
1376 let capabilities = edge_ref.weight().capabilities();
1377
1378 let mut parameters = Vec::new();
1379 if let Ok(contract) = contract {
1380 if let Some(transform) = contract.transform() {
1381 parameters.extend(transform.expressions().map(|(field, expression)| {
1382 ParameterFieldJson {
1383 field: field.to_string(),
1384 contract: match transform.relation(field).expect("declared formula") {
1385 crate::parameters::ParameterRelation::Exact => "exact",
1386 crate::parameters::ParameterRelation::UpperBound => "upper_bound",
1387 },
1388 formula: Some(expression.to_string()),
1389 reason: None,
1390 }
1391 }));
1392 }
1393 parameters.extend(contract.unavailable().iter().map(|unavailable| {
1394 ParameterFieldJson {
1395 field: unavailable.field.to_string(),
1396 contract: "unavailable",
1397 formula: None,
1398 reason: Some(unavailable.reason.to_string()),
1399 }
1400 }));
1401 }
1402 let parameter_contract_error = contract.as_ref().err().map(ToString::to_string);
1403
1404 let src_name = self.nodes[src_node_id].name;
1406 let dst_name = self.nodes[dst_node_id].name;
1407 let src_variant = &self.nodes[src_node_id].variant;
1408 let dst_variant = &self.nodes[dst_node_id].variant;
1409
1410 let doc_path = self.find_entry_doc_path(src_name, dst_name, src_variant, dst_variant);
1411
1412 edges.push(EdgeJson {
1413 source: old_to_new[&src_node_id],
1414 target: old_to_new[&dst_node_id],
1415 parameters,
1416 parameter_contract_error,
1417 doc_path,
1418 witness: capabilities.witness,
1419 aggregate: capabilities.aggregate,
1420 turing: capabilities.turing,
1421 });
1422 }
1423
1424 edges.sort_by(|a, b| {
1426 (
1427 &nodes[a.source].name,
1428 &nodes[a.source].variant,
1429 &nodes[a.target].name,
1430 &nodes[a.target].variant,
1431 )
1432 .cmp(&(
1433 &nodes[b.source].name,
1434 &nodes[b.source].variant,
1435 &nodes[b.target].name,
1436 &nodes[b.target].variant,
1437 ))
1438 });
1439
1440 ReductionGraphJson { nodes, edges }
1441 }
1442
1443 fn find_entry_doc_path(
1445 &self,
1446 src_name: &str,
1447 dst_name: &str,
1448 src_variant: &BTreeMap<String, String>,
1449 dst_variant: &BTreeMap<String, String>,
1450 ) -> String {
1451 for entry in crate::rules::registry::reduction_entries() {
1452 if entry.source_name == src_name && entry.target_name == dst_name {
1453 let entry_src = Self::variant_to_map(&entry.source_variant());
1454 let entry_dst = Self::variant_to_map(&entry.target_variant());
1455 if &entry_src == src_variant && &entry_dst == dst_variant {
1456 return Self::module_path_to_doc_path(entry.module_path);
1457 }
1458 }
1459 }
1460 String::new()
1461 }
1462
1463 pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
1465 let json = self.to_json();
1466 serde_json::to_string_pretty(&json)
1467 }
1468
1469 pub fn to_json_value(&self) -> Result<serde_json::Value, serde_json::Error> {
1471 serde_json::to_value(self.to_json())
1472 }
1473
1474 pub fn to_json_file(&self, path: &std::path::Path) -> std::io::Result<()> {
1476 let json_string = self
1477 .to_json_string()
1478 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
1479 std::fs::write(path, json_string)
1480 }
1481
1482 fn module_path_to_doc_path(module_path: &str) -> String {
1486 let stripped = module_path
1487 .strip_prefix("problemreductions::")
1488 .unwrap_or(module_path);
1489 format!("{}/index.html", stripped.replace("::", "/"))
1490 }
1491
1492 fn doc_path_from_module_path(module_path: &str, name: &str) -> String {
1497 let stripped = module_path
1498 .strip_prefix("problemreductions::")
1499 .unwrap_or(module_path);
1500 if let Some(parent) = stripped.rsplit_once("::").map(|(p, _)| p) {
1501 format!("{}/struct.{}.html", parent.replace("::", "/"), name)
1502 } else {
1503 format!("struct.{}.html", name)
1504 }
1505 }
1506
1507 pub fn find_entry(
1512 &self,
1513 source_name: &str,
1514 source_variant: &BTreeMap<String, String>,
1515 target_name: &str,
1516 target_variant: &BTreeMap<String, String>,
1517 ) -> Option<MatchedEntry> {
1518 let source = self.lookup_node(source_name, source_variant)?;
1519 let target = self.lookup_node(target_name, target_variant)?;
1520 let edge = self.graph.find_edge(source, target)?;
1521
1522 Some(MatchedEntry {
1523 source_variant: source_variant.clone(),
1524 target_variant: target_variant.clone(),
1525 parameter_contract: self.graph[edge].parameter_contract.clone(),
1526 })
1527 }
1528}
1529
1530pub struct MatchedEntry {
1532 pub source_variant: BTreeMap<String, String>,
1534 pub target_variant: BTreeMap<String, String>,
1536 pub parameter_contract: Result<ReductionParameterContract, ParameterContractError>,
1538}
1539
1540pub(crate) struct RecoveryStep<'a> {
1542 pub result: &'a dyn DynReductionResult,
1543 pub aggregate_view: Option<crate::rules::registry::AggregateViewFn>,
1544 pub source: &'a dyn crate::registry::DynProblem,
1545}
1546
1547pub(crate) fn recover_completed_result(
1550 steps: &[RecoveryStep<'_>],
1551 target: &dyn crate::registry::DynProblem,
1552 outcome: &crate::solvers::SolveOutcome,
1553) -> crate::rules::ExtractionResult<Option<(Box<dyn Any>, String)>> {
1554 use crate::rules::ExtractionError;
1555 use crate::solvers::SolveOutcome;
1556 let SolveOutcome::Optimal {
1557 solution,
1558 evaluation,
1559 } = outcome
1560 else {
1561 return Ok(None);
1562 };
1563 let mut value = target.evaluate_json(solution)?;
1564 let mut actual = target
1565 .aggregate_witness_evaluation(&value)?
1566 .ok_or_else(|| ExtractionError::invalid("target witness is infeasible"))?;
1567 if evaluation != &actual {
1568 return Err(ExtractionError::invalid(
1569 "target evaluation does not match the witness",
1570 ));
1571 }
1572 let last = steps
1573 .last()
1574 .ok_or_else(|| ExtractionError::invalid("recovery requires a reduction edge"))?;
1575 let mut witness = last.result.target_solution_from_json(solution.clone())?;
1576 for step in steps.iter().rev() {
1577 let mapped = step
1578 .aggregate_view
1579 .map(|view| view(step.result)?.extract_value_dyn(value.clone()))
1580 .transpose()?;
1581 if let Some(mapped_value) = &mapped {
1582 if step
1583 .source
1584 .aggregate_witness_evaluation(mapped_value)?
1585 .is_none()
1586 {
1587 return Ok(None);
1588 }
1589 }
1590 witness = step.result.extract_solution_dyn(witness.as_ref())?;
1591 value = step
1592 .source
1593 .evaluate_json(&step.result.source_solution_json(witness.as_ref())?)?;
1594 actual = step
1595 .source
1596 .aggregate_witness_evaluation(&value)?
1597 .ok_or_else(|| ExtractionError::invalid("extracted solution is infeasible"))?;
1598 if mapped.is_some_and(|mapped| mapped != value) {
1599 return Err(ExtractionError::invalid(
1600 "extracted witness does not realize the mapped aggregate",
1601 ));
1602 }
1603 }
1604 Ok(Some((witness, actual)))
1605}
1606
1607pub struct ReductionChain {
1614 steps: Vec<Box<dyn DynReductionResult>>,
1615 aggregate_views: Vec<Option<crate::rules::registry::AggregateViewFn>>,
1616 path: ReductionPath,
1617}
1618
1619impl ReductionChain {
1620 pub fn extract_result(
1623 &self,
1624 source: &dyn crate::registry::DynProblem,
1625 outcome: &crate::solvers::SolveOutcome,
1626 ) -> crate::rules::ExtractionResult<crate::solvers::SolveOutcome> {
1627 use crate::rules::ExtractionError;
1628 use crate::solvers::SolveOutcome;
1629 let borrow = |index: usize, problem| {
1630 let node = &self.path.steps[index];
1631 crate::registry::find_variant_entry(&node.name, &node.variant)
1632 .and_then(|entry| (entry.borrow_fn)(problem))
1633 .ok_or_else(|| ExtractionError::invalid("intermediate problem type mismatch"))
1634 };
1635 let steps = self
1636 .steps
1637 .iter()
1638 .enumerate()
1639 .map(|(index, step)| {
1640 Ok(RecoveryStep {
1641 result: step.as_ref(),
1642 aggregate_view: self.aggregate_views[index],
1643 source: if index == 0 {
1644 source
1645 } else {
1646 borrow(index, self.steps[index - 1].target_problem_any())?
1647 },
1648 })
1649 })
1650 .collect::<crate::rules::ExtractionResult<Vec<_>>>()?;
1651 let target = borrow(self.steps.len(), self.target_problem_any())?;
1652 match recover_completed_result(&steps, target, outcome)? {
1653 None => Ok(SolveOutcome::Infeasible),
1654 Some((witness, evaluation)) => Ok(SolveOutcome::Optimal {
1655 solution: steps[0].result.source_solution_json(witness.as_ref())?,
1656 evaluation,
1657 }),
1658 }
1659 }
1660
1661 pub fn has_value_mapping(&self) -> bool {
1663 self.aggregate_views.iter().all(Option::is_some)
1664 }
1665
1666 pub fn extract_value(
1670 &self,
1671 target_value: serde_json::Value,
1672 ) -> crate::rules::ExtractionResult<serde_json::Value> {
1673 self.steps.iter().zip(&self.aggregate_views).rev().try_fold(
1674 target_value,
1675 |value, (step, view)| {
1676 let view = view.ok_or_else(|| {
1677 crate::rules::ExtractionError::invalid("reduction has no value mapping")
1678 })?;
1679 view(step.as_ref())?.extract_value_dyn(value)
1680 },
1681 )
1682 }
1683
1684 pub fn target_problem_any(&self) -> &dyn Any {
1686 self.steps
1687 .last()
1688 .expect("ReductionChain has no steps")
1689 .target_problem_any()
1690 }
1691
1692 pub fn target_problem<T: 'static>(&self) -> &T {
1696 self.target_problem_any()
1697 .downcast_ref::<T>()
1698 .expect("ReductionChain target type mismatch")
1699 }
1700
1701 pub fn extract_solution<S: 'static, T: 'static>(
1703 &self,
1704 target_solution: &T,
1705 ) -> crate::rules::ExtractionResult<S> {
1706 let mut steps = self.steps.iter().rev();
1707 let first = steps.next().expect("ReductionChain has no steps");
1708 let mut solution = first.extract_solution_dyn(target_solution)?;
1709 for step in steps {
1710 solution = step.extract_solution_dyn(solution.as_ref())?;
1711 }
1712 solution
1713 .downcast::<S>()
1714 .map(|solution| *solution)
1715 .map_err(|_| crate::rules::ExtractionError::invalid("source solution type mismatch"))
1716 }
1717
1718 pub fn extract_solution_json(
1720 &self,
1721 target_solution: serde_json::Value,
1722 ) -> crate::rules::ExtractionResult<serde_json::Value> {
1723 let last = self.steps.last().expect("ReductionChain has no steps");
1724 let mut solution = last.target_solution_from_json(target_solution)?;
1725 for step in self.steps.iter().rev() {
1726 solution = step.extract_solution_dyn(solution.as_ref())?;
1727 }
1728 self.steps[0].source_solution_json(solution.as_ref())
1729 }
1730}
1731
1732pub struct AggregateReductionChain {
1735 steps: Vec<Box<dyn DynAggregateReductionResult>>,
1736}
1737
1738impl AggregateReductionChain {
1739 pub fn target_problem_any(&self) -> &dyn Any {
1741 self.steps
1742 .last()
1743 .expect("AggregateReductionChain has no steps")
1744 .target_problem_any()
1745 }
1746
1747 pub fn target_problem<T: 'static>(&self) -> &T {
1751 self.target_problem_any()
1752 .downcast_ref::<T>()
1753 .expect("AggregateReductionChain target type mismatch")
1754 }
1755
1756 pub fn extract_value(
1758 &self,
1759 target_value: serde_json::Value,
1760 ) -> crate::rules::ExtractionResult<serde_json::Value> {
1761 self.steps
1762 .iter()
1763 .rev()
1764 .try_fold(target_value, |value, step| step.extract_value_dyn(value))
1765 }
1766}
1767
1768impl ReductionGraph {
1769 fn execute_aggregate_edge(
1770 &self,
1771 edge_idx: EdgeIndex,
1772 input: &dyn Any,
1773 ) -> Result<Option<Box<dyn DynAggregateReductionResult>>, crate::rules::ReductionError> {
1774 let edge = &self.graph[edge_idx];
1775 let Some(reduce) = edge.reduce_aggregate_fn else {
1776 return Ok(None);
1777 };
1778 reduce(input).map(Some)
1779 }
1780
1781 pub fn reduce_along_path(
1797 &self,
1798 path: &ReductionPath,
1799 source: &dyn Any,
1800 ) -> Result<Option<ReductionChain>, crate::rules::ReductionError> {
1801 if path.steps.len() < 2 {
1802 return Ok(None);
1803 }
1804 let mut edge_fns = Vec::new();
1806 let mut aggregate_views = Vec::new();
1807 for window in path.steps.windows(2) {
1808 let Some(src) = self.lookup_node(&window[0].name, &window[0].variant) else {
1809 return Ok(None);
1810 };
1811 let Some(dst) = self.lookup_node(&window[1].name, &window[1].variant) else {
1812 return Ok(None);
1813 };
1814 let Some(edge_idx) = self.graph.find_edge(src, dst) else {
1815 return Ok(None);
1816 };
1817 if !Self::edge_supports_mode(&self.graph[edge_idx], ReductionMode::Witness) {
1818 return Ok(None);
1819 }
1820 let Some(reduce) = self.graph[edge_idx].reduce_fn else {
1821 return Ok(None);
1822 };
1823 edge_fns.push(reduce);
1824 aggregate_views.push(self.graph[edge_idx].aggregate_view_fn);
1825 }
1826 let mut steps: Vec<Box<dyn DynReductionResult>> = Vec::new();
1828 let step = (edge_fns[0])(source)?;
1829 steps.push(step);
1830 for edge_fn in &edge_fns[1..] {
1831 let step = {
1832 let prev_target = steps.last().unwrap().target_problem_any();
1833 edge_fn(prev_target)?
1834 };
1835 steps.push(step);
1836 }
1837 Ok(Some(ReductionChain {
1838 steps,
1839 aggregate_views,
1840 path: path.clone(),
1841 }))
1842 }
1843
1844 pub fn reduce_aggregate_along_path(
1846 &self,
1847 path: &ReductionPath,
1848 source: &dyn Any,
1849 ) -> Result<Option<AggregateReductionChain>, crate::rules::ReductionError> {
1850 if path.steps.len() < 2 {
1851 return Ok(None);
1852 }
1853
1854 let mut edge_indices = Vec::new();
1855 for window in path.steps.windows(2) {
1856 let Some(src) = self.lookup_node(&window[0].name, &window[0].variant) else {
1857 return Ok(None);
1858 };
1859 let Some(dst) = self.lookup_node(&window[1].name, &window[1].variant) else {
1860 return Ok(None);
1861 };
1862 let Some(edge_idx) = self.graph.find_edge(src, dst) else {
1863 return Ok(None);
1864 };
1865 edge_indices.push(edge_idx);
1866 }
1867
1868 let mut steps: Vec<Box<dyn DynAggregateReductionResult>> = Vec::new();
1869 let Some(step) = self.execute_aggregate_edge(edge_indices[0], source)? else {
1870 return Ok(None);
1871 };
1872 steps.push(step);
1873 for &edge_idx in &edge_indices[1..] {
1874 let step = {
1875 let prev_target = steps.last().unwrap().target_problem_any();
1876 let Some(step) = self.execute_aggregate_edge(edge_idx, prev_target)? else {
1877 return Ok(None);
1878 };
1879 step
1880 };
1881 steps.push(step);
1882 }
1883 Ok(Some(AggregateReductionChain { steps }))
1884 }
1885}
1886
1887pub struct ExecutedPath {
1892 pub path: ReductionPath,
1894 steps: Vec<Rc<dyn DynReductionResult>>,
1896}
1897
1898impl ExecutedPath {
1899 pub fn target_problem_any(&self) -> &dyn Any {
1901 self.steps
1902 .last()
1903 .expect("ExecutedPath has no steps")
1904 .target_problem_any()
1905 }
1906
1907 pub fn target_parameters(&self) -> Vec<ProblemParameters> {
1909 self.steps
1910 .iter()
1911 .zip(self.path.steps.iter().skip(1))
1912 .map(|(result, target)| {
1913 ReductionGraph::compute_problem_parameters(
1914 &target.name,
1915 &target.variant,
1916 result.target_problem_any(),
1917 )
1918 })
1919 .collect()
1920 }
1921
1922 pub fn extract_solution<S: 'static, T: 'static>(
1924 &self,
1925 target_solution: &T,
1926 ) -> crate::rules::ExtractionResult<S> {
1927 let mut steps = self.steps.iter().rev();
1928 let first = steps.next().expect("ExecutedPath has no steps");
1929 let mut solution = first.extract_solution_dyn(target_solution)?;
1930 for step in steps {
1931 solution = step.extract_solution_dyn(solution.as_ref())?;
1932 }
1933 solution
1934 .downcast::<S>()
1935 .map(|solution| *solution)
1936 .map_err(|_| crate::rules::ExtractionError::invalid("source solution type mismatch"))
1937 }
1938}
1939
1940impl ReductionGraph {
1941 pub fn execute_paths(
1943 &self,
1944 paths: &[ReductionPath],
1945 source_instance: &dyn Any,
1946 ) -> Result<Vec<ExecutedPath>, ExecutePathsError> {
1947 let mut prefixes: HashMap<Vec<ReductionStep>, Vec<Rc<dyn DynReductionResult>>> =
1948 HashMap::new();
1949 let mut executed = Vec::with_capacity(paths.len());
1950 let mut batch_source: Option<&ReductionStep> = None;
1951 for (path_index, path) in paths.iter().enumerate() {
1952 let source = path
1953 .steps
1954 .first()
1955 .ok_or(ExecutePathsError::EmptyPath { path_index })?;
1956 if path.steps.len() < 2 {
1957 return Err(ExecutePathsError::NoEdges { path_index });
1958 }
1959 if let Some(expected) = batch_source {
1960 if source != expected {
1961 return Err(ExecutePathsError::DifferentSource { path_index });
1962 }
1963 } else {
1964 batch_source = Some(source);
1965 }
1966 let source_prefix = vec![source.clone()];
1967 let mut chain = prefixes.get(&source_prefix).cloned().unwrap_or_default();
1968 prefixes.entry(source_prefix.clone()).or_default();
1969 let mut prefix = source_prefix;
1970 for pair in path.steps.windows(2) {
1971 prefix.push(pair[1].clone());
1972 if let Some(cached) = prefixes.get(&prefix) {
1973 chain = cached.clone();
1974 continue;
1975 }
1976 let source_node = self
1977 .lookup_node(&pair[0].name, &pair[0].variant)
1978 .ok_or_else(|| ExecutePathsError::UnknownNode {
1979 path_index,
1980 problem: pair[0].name.clone(),
1981 variant: pair[0].variant.clone(),
1982 })?;
1983 let target_node_index = self
1984 .lookup_node(&pair[1].name, &pair[1].variant)
1985 .ok_or_else(|| ExecutePathsError::UnknownNode {
1986 path_index,
1987 problem: pair[1].name.clone(),
1988 variant: pair[1].variant.clone(),
1989 })?;
1990 let edge_index = self
1991 .graph
1992 .find_edge(source_node, target_node_index)
1993 .ok_or_else(|| ExecutePathsError::MissingEdge {
1994 path_index,
1995 source_problem: pair[0].name.clone(),
1996 target_problem: pair[1].name.clone(),
1997 })?;
1998 let edge_data = &self.graph[edge_index];
1999 let Some(reduce_fn) = edge_data.reduce_fn else {
2000 return Err(ExecutePathsError::NotWitnessExecutable {
2001 path_index,
2002 source_problem: pair[0].name.clone(),
2003 target_problem: pair[1].name.clone(),
2004 });
2005 };
2006 let current = chain
2007 .last()
2008 .map(|step| step.target_problem_any())
2009 .unwrap_or(source_instance);
2010 let result = reduce_fn(current)
2011 .map_err(|cause| ExecutePathsError::Reduction { path_index, cause })?;
2012 chain.push(Rc::from(result));
2013 prefixes.insert(prefix.clone(), chain.clone());
2014 }
2015 executed.push(ExecutedPath {
2016 path: path.clone(),
2017 steps: chain,
2018 });
2019 }
2020 Ok(executed)
2021 }
2022}
2023
2024#[cfg(test)]
2025impl ReductionGraph {
2026 pub(crate) fn from_test_edges(
2031 node_names: &[&'static str],
2032 edges: &[(&'static str, &'static str, ReductionEdgeData)],
2033 ) -> Self {
2034 Self::from_test_variant_edges(
2035 &node_names
2036 .iter()
2037 .map(|&name| (name, BTreeMap::new()))
2038 .collect::<Vec<_>>(),
2039 edges,
2040 )
2041 }
2042
2043 pub(crate) fn from_test_variant_edges(
2044 test_nodes: &[(&'static str, BTreeMap<String, String>)],
2045 edges: &[(&'static str, &'static str, ReductionEdgeData)],
2046 ) -> Self {
2047 let mut graph: DiGraph<usize, ReductionEdgeData> = DiGraph::new();
2048 let mut nodes: Vec<VariantNode> = Vec::new();
2049 let mut name_to_nodes: HashMap<&'static str, Vec<NodeIndex>> = HashMap::new();
2050 let mut index_of: HashMap<&'static str, NodeIndex> = HashMap::new();
2051
2052 for (name, variant) in test_nodes {
2053 let node_id = nodes.len();
2054 nodes.push(VariantNode {
2055 name,
2056 variant: variant.clone(),
2057 complexity: "",
2058 });
2059 let idx = graph.add_node(node_id);
2060 index_of.insert(name, idx);
2061 name_to_nodes.entry(name).or_default().push(idx);
2062 }
2063
2064 for (src, dst, data) in edges {
2065 let s = index_of[src];
2066 let d = index_of[dst];
2067 graph.add_edge(s, d, data.clone());
2068 }
2069
2070 Self {
2071 graph,
2072 nodes,
2073 name_to_nodes,
2074 default_variants: HashMap::new(),
2075 }
2076 }
2077}
2078
2079#[cfg(test)]
2080#[path = "../unit_tests/rules/graph.rs"]
2081mod tests;
2082
2083#[cfg(test)]
2084#[path = "../unit_tests/rules/reduction_path_parity.rs"]
2085mod reduction_path_parity_tests;
2086
2087#[cfg(test)]
2088#[path = "../unit_tests/rules/maximumindependentset_ilp.rs"]
2089mod maximumindependentset_ilp_path_tests;
2090
2091#[cfg(test)]
2092#[path = "../unit_tests/rules/minimumvertexcover_ilp.rs"]
2093mod minimumvertexcover_ilp_path_tests;
2094
2095#[cfg(test)]
2096#[path = "../unit_tests/rules/maximumindependentset_qubo.rs"]
2097mod maximumindependentset_qubo_path_tests;
2098
2099#[cfg(test)]
2100#[path = "../unit_tests/rules/minimumvertexcover_qubo.rs"]
2101mod minimumvertexcover_qubo_path_tests;