Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions problemreductions-cli/src/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use crate::dispatch::{
};
use crate::output::OutputConfig;
use anyhow::Result;
use problemreductions::rules::ReductionGraph;
use problemreductions::rules::{ReductionGraph, ReductionMode};
use std::collections::BTreeMap;
use std::path::Path;

pub fn inspect(input: &Path, out: &OutputConfig) -> Result<()> {
Expand Down Expand Up @@ -59,8 +60,7 @@ fn inspect_problem(pj: &ProblemJson, out: &OutputConfig) -> Result<()> {
}

// Reductions
let outgoing = graph.outgoing_reductions(name);
let targets = targets_deduped(&outgoing);
let targets = executable_reduction_targets(&graph, name, &variant);
if !targets.is_empty() {
text.push_str(&format!("Reduces to: {}\n", targets.join(", ")));
}
Expand Down Expand Up @@ -100,8 +100,29 @@ fn inspect_bundle(bundle: &ReductionBundle, out: &OutputConfig) -> Result<()> {
out.emit_with_default_name("", &text, &json_val)
}

fn targets_deduped(outgoing: &[problemreductions::rules::ReductionEdgeInfo]) -> Vec<String> {
let mut targets: Vec<String> = outgoing.iter().map(|e| e.target_name.to_string()).collect();
pub(crate) fn executable_reduction_targets(
graph: &ReductionGraph,
name: &str,
variant: &BTreeMap<String, String>,
) -> Vec<String> {
let mut targets: Vec<String> = graph
.outgoing_reductions_from(name, variant, ReductionMode::Witness)
.into_iter()
.map(|edge| {
let default_variant = graph
.default_variant_for(edge.target_name)
.unwrap_or_else(|| panic!("default variant not found for {}", edge.target_name));
if default_variant == edge.target_variant {
edge.target_name.to_string()
} else {
format!(
"{}{}",
edge.target_name,
crate::commands::graph::variant_to_full_slash(&edge.target_variant)
)
}
})
.collect();
targets.sort();
targets.dedup();
targets
Expand Down
6 changes: 2 additions & 4 deletions problemreductions-cli/src/mcp/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,10 +846,8 @@ impl McpServer {

let size_fields = graph.size_field_names(name);

let outgoing = graph.outgoing_reductions(name);
let mut targets: Vec<String> = outgoing.iter().map(|e| e.target_name.to_string()).collect();
targets.sort();
targets.dedup();
let targets =
crate::commands::inspect::executable_reduction_targets(&graph, name, &variant);
let solver_view = solver_capabilities_view(&problem)?;

let result = serde_json::json!({
Expand Down
126 changes: 126 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6092,6 +6092,132 @@ fn test_inspect_problem() {
std::fs::remove_file(&problem_file).ok();
}

#[test]
fn test_inspect_reports_only_executable_reductions_for_exact_variant() {
let unit_file = std::env::temp_dir().join("pred_test_inspect_exact_variant_unit.json");
let weighted_file = std::env::temp_dir().join("pred_test_inspect_exact_variant_weighted.json");

let unit_create = pred()
.args([
"create",
"MIS",
"--graph",
"0-1,1-2,2-3",
"-o",
unit_file.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
unit_create.status.success(),
"stderr: {}",
String::from_utf8_lossy(&unit_create.stderr)
);

let weighted_create = pred()
.args([
"create",
"MIS/SimpleGraph/i32",
"--graph",
"0-1,1-2,2-3",
"--weights",
"3,1,2,1",
"-o",
weighted_file.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
weighted_create.status.success(),
"stderr: {}",
String::from_utf8_lossy(&weighted_create.stderr)
);

for (source, expected, excluded) in [
(&unit_file, "MaximumSetPacking", "IntegralFlowBundles"),
(
&weighted_file,
"IntegralFlowBundles",
"MaximumIndependentSet/KingsSubgraph/One",
),
] {
let inspect = pred()
.args(["inspect", source.to_str().unwrap(), "--json"])
.output()
.unwrap();
assert!(
inspect.status.success(),
"stderr: {}",
String::from_utf8_lossy(&inspect.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&inspect.stdout).unwrap();
let targets = json["reduces_to"].as_array().unwrap();
assert!(targets.iter().any(|target| target == expected));
assert!(!targets.iter().any(|target| target == excluded));

for (index, target) in targets.iter().enumerate() {
let target = target.as_str().unwrap();
let bundle = std::env::temp_dir().join(format!(
"pred_test_inspect_exact_variant_bundle_{index}.json"
));
let reduce = pred()
.args([
"reduce",
source.to_str().unwrap(),
"--to",
target,
"-o",
bundle.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
reduce.status.success(),
"inspect advertised non-executable target {target}: {}",
String::from_utf8_lossy(&reduce.stderr)
);
std::fs::remove_file(bundle).unwrap();
}
}

std::fs::remove_file(unit_file).unwrap();
std::fs::remove_file(weighted_file).unwrap();
}

#[test]
fn test_inspect_excludes_non_witness_reductions() {
let problem_file = std::env::temp_dir().join("pred_test_inspect_witness_reductions_only.json");
let create = pred()
.args([
"create",
"--example",
"MinimumDominatingSet",
"-o",
problem_file.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
create.status.success(),
"stderr: {}",
String::from_utf8_lossy(&create.stderr)
);

let inspect = pred()
.args(["inspect", problem_file.to_str().unwrap(), "--json"])
.output()
.unwrap();
assert!(
inspect.status.success(),
"stderr: {}",
String::from_utf8_lossy(&inspect.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&inspect.stdout).unwrap();
assert_eq!(json["reduces_to"], serde_json::json!(["ILP"]));

std::fs::remove_file(problem_file).unwrap();
}

#[test]
fn test_inspect_minmaxmulticenter_lists_ilp_and_bruteforce() {
let problem_file = std::env::temp_dir().join("pred_test_inspect_minmaxmulticenter.json");
Expand Down
32 changes: 32 additions & 0 deletions src/rules/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,38 @@ impl ReductionGraph {
.collect()
}

/// Get executable outgoing reductions from one exact problem variant.
///
/// # Panics
///
/// Panics if `name` and `variant` do not identify an exactly registered problem variant.
pub fn outgoing_reductions_from(
&self,
name: &str,
variant: &BTreeMap<String, String>,
mode: ReductionMode,
) -> Vec<ReductionEdgeInfo> {
let source = self
.lookup_node(name, variant)
.unwrap_or_else(|| panic!("registered problem variant not found: {name} {variant:?}"));

self.ordered_outgoing_edges(source, mode)
.into_iter()
.map(|(target, edge)| {
let src = &self.nodes[self.graph[source]];
let dst = &self.nodes[self.graph[target]];
ReductionEdgeInfo {
source_name: src.name,
source_variant: src.variant.clone(),
target_name: dst.name,
target_variant: dst.variant.clone(),
overhead: self.graph[edge].overhead.clone(),
capabilities: self.graph[edge].capabilities(),
}
})
.collect()
}

/// Get the problem size field names for a problem type.
///
/// Derives size fields from the overhead expressions of reduction entries
Expand Down
49 changes: 49 additions & 0 deletions src/unit_tests/rules/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,55 @@ fn test_compute_source_size_uses_exact_variant_executor() {
assert_eq!(size.get("num_edges"), Some(3));
}

#[test]
fn test_outgoing_reductions_from_uses_exact_variant_and_mode() {
let graph = ReductionGraph::new();
let unit =
ReductionGraph::variant_to_map(&MaximumIndependentSet::<SimpleGraph, One>::variant());
let weighted =
ReductionGraph::variant_to_map(&MaximumIndependentSet::<SimpleGraph, i32>::variant());

let unit_targets =
graph.outgoing_reductions_from("MaximumIndependentSet", &unit, ReductionMode::Witness);
assert!(unit_targets
.iter()
.all(|edge| edge.source_variant == unit && edge.capabilities.witness));
assert!(unit_targets.iter().any(|edge| {
edge.target_name == "MaximumSetPacking"
&& edge.target_variant.get("weight").map(String::as_str) == Some("One")
}));
assert!(!unit_targets
.iter()
.any(|edge| edge.target_name == "IntegralFlowBundles"));

let weighted_targets =
graph.outgoing_reductions_from("MaximumIndependentSet", &weighted, ReductionMode::Witness);
assert!(weighted_targets
.iter()
.all(|edge| edge.source_variant == weighted && edge.capabilities.witness));
assert!(weighted_targets
.iter()
.any(|edge| edge.target_name == "IntegralFlowBundles"));
assert!(!weighted_targets.iter().any(|edge| {
edge.target_name == "MaximumIndependentSet"
&& edge.target_variant.get("graph").map(String::as_str) == Some("KingsSubgraph")
}));
}

#[test]
#[should_panic(expected = "registered problem variant not found")]
fn test_outgoing_reductions_from_rejects_unknown_exact_variant() {
let graph = ReductionGraph::new();
graph.outgoing_reductions_from(
"MaximumIndependentSet",
&BTreeMap::from([
("graph".to_string(), "SimpleGraph".to_string()),
("weight".to_string(), "i64".to_string()),
]),
ReductionMode::Witness,
);
}

#[test]
fn test_compute_source_size_unknown_problem() {
let problem = 42u32;
Expand Down