diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 66ae6ee6a..dd15461f0 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -124,6 +124,21 @@ Update the CLI dispatch table so `pred` can load, solve, and serialize the new p - Add a lowercase alias mapping in `resolve_alias()` (e.g., `"newproblem" => "NewProblem".to_string()`) - Optionally add short aliases to `ALIASES` array (e.g., `("NP", "NewProblem")`) +## Step 4.5: Add CLI creation support + +Update `problemreductions-cli/src/commands/create.rs` so `pred create ` works: + +1. **Add a match arm** in the `create()` function's main `match canonical.as_str()` block. Parse CLI flags and construct the problem: + - Graph-based problems with vertex weights: add to the `"MaximumIndependentSet" | ... | "MaximalIS"` arm + - Problems with unique fields: add a new arm that parses the required flags and calls the constructor + - See existing arms for patterns (e.g., `"BinPacking"` for simple fields, `"MaximumSetPacking"` for set-based) + +2. **Add CLI flags** in `problemreductions-cli/src/cli.rs` (`CreateArgs` struct) if the problem needs flags not already present. Update `all_data_flags_empty()` accordingly. + +3. **Update help text** in `CreateArgs`'s `after_help` to document the new problem's flags. + +4. **Schema alignment**: The `ProblemSchemaEntry` fields should list **constructor parameters** (what the user provides), not internal derived fields. For example, if `m` and `n` are derived from a matrix, only list `matrix` and `k` in the schema. + ## Step 5: Write unit tests Create `src/unit_tests/models//.rs`: @@ -168,3 +183,5 @@ If running standalone (not inside `make run-plan`), invoke [review-implementatio | Forgetting `declare_variants!` | Required for variant complexity metadata used by the paper's auto-generated table | | Forgetting CLI dispatch | Must add match arms in `dispatch.rs` (`load_problem` + `serialize_any_problem`) | | Forgetting CLI alias | Must add lowercase entry in `problem_name.rs` `resolve_alias()` | +| Forgetting CLI create | Must add creation handler in `commands/create.rs` and flags in `cli.rs` | +| Schema lists derived fields | Schema should list constructor params, not internal fields (e.g., `matrix, k` not `matrix, m, n, k`) | diff --git a/Makefile b/Makefile index 8590cbf94..2e9948c28 100644 --- a/Makefile +++ b/Makefile @@ -270,18 +270,18 @@ cli-demo: cli \ echo ""; \ echo "--- 8. create: build problem instances ---"; \ - $$PRED create MIS --edges 0-1,1-2,2-3,3-4,4-0 -o $(CLI_DEMO_DIR)/mis.json; \ - $$PRED create MIS --edges 0-1,1-2,2-3 --weights 2,1,3,1 -o $(CLI_DEMO_DIR)/mis_weighted.json; \ + $$PRED create MIS --graph 0-1,1-2,2-3,3-4,4-0 -o $(CLI_DEMO_DIR)/mis.json; \ + $$PRED create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o $(CLI_DEMO_DIR)/mis_weighted.json; \ $$PRED create SAT --num-vars 3 --clauses "1,2;-1,3;2,-3" -o $(CLI_DEMO_DIR)/sat.json; \ $$PRED create 3SAT --num-vars 4 --clauses "1,2,3;-1,2,-3;1,-2,3" -o $(CLI_DEMO_DIR)/3sat.json; \ $$PRED create QUBO --matrix "1,-0.5;-0.5,2" -o $(CLI_DEMO_DIR)/qubo.json; \ - $$PRED create KColoring --k 3 --edges 0-1,1-2,2-0 -o $(CLI_DEMO_DIR)/kcol.json; \ - $$PRED create SpinGlass --edges 0-1,1-2 -o $(CLI_DEMO_DIR)/sg.json; \ - $$PRED create MaxCut --edges 0-1,1-2,2-0 -o $(CLI_DEMO_DIR)/maxcut.json; \ - $$PRED create MVC --edges 0-1,1-2,2-3 -o $(CLI_DEMO_DIR)/mvc.json; \ - $$PRED create MaximumMatching --edges 0-1,1-2,2-3 -o $(CLI_DEMO_DIR)/matching.json; \ - $$PRED create Factoring --target 15 --bits-m 4 --bits-n 4 -o $(CLI_DEMO_DIR)/factoring.json; \ - $$PRED create Factoring --target 21 --bits-m 3 --bits-n 3 -o $(CLI_DEMO_DIR)/factoring2.json; \ + $$PRED create KColoring --k 3 --graph 0-1,1-2,2-0 -o $(CLI_DEMO_DIR)/kcol.json; \ + $$PRED create SpinGlass --graph 0-1,1-2 -o $(CLI_DEMO_DIR)/sg.json; \ + $$PRED create MaxCut --graph 0-1,1-2,2-0 -o $(CLI_DEMO_DIR)/maxcut.json; \ + $$PRED create MVC --graph 0-1,1-2,2-3 -o $(CLI_DEMO_DIR)/mvc.json; \ + $$PRED create MaximumMatching --graph 0-1,1-2,2-3 -o $(CLI_DEMO_DIR)/matching.json; \ + $$PRED create Factoring --target 15 --m 4 --n 4 -o $(CLI_DEMO_DIR)/factoring.json; \ + $$PRED create Factoring --target 21 --m 3 --n 3 -o $(CLI_DEMO_DIR)/factoring2.json; \ \ echo ""; \ echo "--- 9. evaluate: test configurations ---"; \ @@ -330,7 +330,7 @@ cli-demo: cli echo ""; \ echo "--- 18. closed-loop: create → reduce → solve → verify ---"; \ echo "Creating a 6-vertex graph..."; \ - $$PRED create MIS --edges 0-1,1-2,2-3,3-4,4-5,0-5,1-4 -o $(CLI_DEMO_DIR)/big.json; \ + $$PRED create MIS --graph 0-1,1-2,2-3,3-4,4-5,0-5,1-4 -o $(CLI_DEMO_DIR)/big.json; \ echo "Solving with ILP..."; \ $$PRED solve $(CLI_DEMO_DIR)/big.json -o $(CLI_DEMO_DIR)/big_sol.json; \ echo "Reducing to QUBO and solving with brute-force..."; \ diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 1aacf242f..91e9bd252 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -203,11 +203,20 @@ TIP: Run `pred create ` (no other flags) to see problem-specific help. Flags by problem type: MIS, MVC, MaxClique, MinDomSet --graph, --weights MaxCut, MaxMatching, TSP --graph, --edge-weights + MaximalIS --graph, --weights SAT, 3SAT/KSAT --num-vars, --clauses [--k] QUBO --matrix SpinGlass --graph, --couplings, --fields KColoring --graph, --k Factoring --target, --m, --n + BinPacking --sizes, --capacity + PaintShop --sequence + MaximumSetPacking --sets [--weights] + MinimumSetCovering --universe, --sets [--weights] + BicliqueCover --left, --right, --biedges, --k + BMF --matrix (0/1), --rank + CVP --basis, --target-vec [--bounds] + ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): KingsSubgraph, TriangularSubgraph --positions (integer x,y pairs) @@ -281,6 +290,42 @@ pub struct CreateArgs { /// Radius for UnitDiskGraph [default: 1.0] #[arg(long)] pub radius: Option, + /// Item sizes for BinPacking (comma-separated, e.g., "3,3,2,2") + #[arg(long)] + pub sizes: Option, + /// Bin capacity for BinPacking + #[arg(long)] + pub capacity: Option, + /// Car paint sequence for PaintShop (comma-separated, each label appears exactly twice, e.g., "a,b,a,c,c,b") + #[arg(long)] + pub sequence: Option, + /// Sets for SetPacking/SetCovering (semicolon-separated, e.g., "0,1;1,2;0,2") + #[arg(long)] + pub sets: Option, + /// Universe size for MinimumSetCovering + #[arg(long)] + pub universe: Option, + /// Bipartite graph edges for BicliqueCover (e.g., "0-0,0-1,1-2" for left-right pairs) + #[arg(long)] + pub biedges: Option, + /// Left partition size for BicliqueCover + #[arg(long)] + pub left: Option, + /// Right partition size for BicliqueCover + #[arg(long)] + pub right: Option, + /// Rank for BMF + #[arg(long)] + pub rank: Option, + /// Lattice basis for CVP (semicolon-separated column vectors, e.g., "1,0;0,1") + #[arg(long)] + pub basis: Option, + /// Target vector for CVP (comma-separated, e.g., "0.5,0.5") + #[arg(long)] + pub target_vec: Option, + /// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10] + #[arg(long, allow_hyphen_values = true)] + pub bounds: Option, } #[derive(clap::Args)] @@ -315,7 +360,7 @@ pub struct SolveArgs { /// Solver: ilp (default) or brute-force #[arg(long, default_value = "ilp")] pub solver: String, - /// Timeout in seconds (0 = no limit) [default: 0] + /// Timeout in seconds (0 = no limit) #[arg(long, default_value = "0")] pub timeout: u64, } @@ -367,7 +412,13 @@ pub struct EvaluateArgs { } /// Print the after_help text for a subcommand on parse error. +/// +/// Only matches the first line of the error message. Without this, +/// bare `pred` (no subcommand) would match "pred solve" in the +/// top-level workflow examples and incorrectly append the solve +/// subcommand's help text. pub fn print_subcommand_help_hint(error_msg: &str) { + let first_line = error_msg.lines().next().unwrap_or(""); let subcmds = [ ("pred solve", "solve"), ("pred reduce", "reduce"), @@ -382,7 +433,7 @@ pub fn print_subcommand_help_hint(error_msg: &str) { ]; let cmd = Cli::command(); for (pattern, name) in subcmds { - if error_msg.contains(pattern) { + if first_line.contains(pattern) { if let Some(sub) = cmd.find_subcommand(name) { if let Some(help) = sub.get_after_help() { eprintln!("\n{help}"); diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index c9a66874f..418bc520f 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -4,10 +4,12 @@ use crate::output::OutputConfig; use crate::problem_name::{parse_problem_spec, resolve_variant}; use crate::util; use anyhow::{bail, Context, Result}; +use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; +use problemreductions::models::misc::{BinPacking, PaintShop}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ - Graph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph, + BipartiteGraph, Graph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph, }; use serde::Serialize; use std::collections::BTreeMap; @@ -31,6 +33,18 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.seed.is_none() && args.positions.is_none() && args.radius.is_none() + && args.sizes.is_none() + && args.capacity.is_none() + && args.sequence.is_none() + && args.sets.is_none() + && args.universe.is_none() + && args.biedges.is_none() + && args.left.is_none() + && args.right.is_none() + && args.rank.is_none() + && args.basis.is_none() + && args.target_vec.is_none() + && args.bounds.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -148,6 +162,17 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { return create_random(args, canonical, &resolved_variant, out); } + // ILP and CircuitSAT have complex input structures not suited for CLI flags. + // Check before the empty-flags help so they get a clear message. + if canonical == "ILP" || canonical == "CircuitSAT" { + bail!( + "CLI creation is not yet supported for {canonical}.\n\n\ + {canonical} instances are typically created via reduction:\n\ + pred create MIS --graph 0-1,1-2 | pred reduce - --to {canonical}\n\n\ + Or use the Rust API for direct construction." + ); + } + // Show schema-driven help when no data flags are provided if all_data_flags_empty(args) { let gt = if graph_type != "SimpleGraph" { @@ -155,7 +180,8 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { } else { None }; - return print_problem_help(canonical, gt); + print_problem_help(canonical, gt)?; + std::process::exit(2); } let (data, variant) = match canonical.as_str() { @@ -272,6 +298,151 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { (ser(Factoring::new(m, n, target))?, resolved_variant.clone()) } + // MaximalIS — same as MIS (graph + vertex weights) + "MaximalIS" => { + create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)? + } + + // BinPacking + "BinPacking" => { + let sizes_str = args.sizes.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "BinPacking requires --sizes and --capacity\n\n\ + Usage: pred create BinPacking --sizes 3,3,2,2 --capacity 5" + ) + })?; + let cap_str = args.capacity.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "BinPacking requires --capacity\n\n\ + Usage: pred create BinPacking --sizes 3,3,2,2 --capacity 5" + ) + })?; + let use_f64 = sizes_str.contains('.') || cap_str.contains('.'); + if use_f64 { + let sizes: Vec = util::parse_comma_list(sizes_str)?; + let capacity: f64 = cap_str.parse()?; + let mut variant = resolved_variant.clone(); + variant.insert("weight".to_string(), "f64".to_string()); + (ser(BinPacking::new(sizes, capacity))?, variant) + } else { + let sizes: Vec = util::parse_comma_list(sizes_str)?; + let capacity: i32 = cap_str.parse()?; + ( + ser(BinPacking::new(sizes, capacity))?, + resolved_variant.clone(), + ) + } + } + + // PaintShop + "PaintShop" => { + let seq_str = args.sequence.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "PaintShop requires --sequence\n\n\ + Usage: pred create PaintShop --sequence a,b,a,c,c,b" + ) + })?; + let sequence: Vec = seq_str.split(',').map(|s| s.trim().to_string()).collect(); + (ser(PaintShop::new(sequence))?, resolved_variant.clone()) + } + + // MaximumSetPacking + "MaximumSetPacking" => { + let sets = parse_sets(args)?; + let num_sets = sets.len(); + let weights = parse_set_weights(args, num_sets)?; + ( + ser(MaximumSetPacking::with_weights(sets, weights))?, + resolved_variant.clone(), + ) + } + + // MinimumSetCovering + "MinimumSetCovering" => { + let universe = args.universe.ok_or_else(|| { + anyhow::anyhow!( + "MinimumSetCovering requires --universe and --sets\n\n\ + Usage: pred create MinimumSetCovering --universe 4 --sets \"0,1;1,2;2,3;0,3\"" + ) + })?; + let sets = parse_sets(args)?; + let num_sets = sets.len(); + let weights = parse_set_weights(args, num_sets)?; + ( + ser(MinimumSetCovering::with_weights(universe, sets, weights))?, + resolved_variant.clone(), + ) + } + + // BicliqueCover + "BicliqueCover" => { + let left = args.left.ok_or_else(|| { + anyhow::anyhow!( + "BicliqueCover requires --left, --right, --biedges, and --k\n\n\ + Usage: pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2" + ) + })?; + let right = args.right.ok_or_else(|| { + anyhow::anyhow!("BicliqueCover requires --right (right partition size)") + })?; + let k = args.k.ok_or_else(|| { + anyhow::anyhow!("BicliqueCover requires --k (number of bicliques)") + })?; + let edges_str = args.biedges.as_deref().ok_or_else(|| { + anyhow::anyhow!("BicliqueCover requires --biedges (e.g., 0-0,0-1,1-1)") + })?; + let edges = util::parse_edge_pairs(edges_str)?; + let graph = BipartiteGraph::new(left, right, edges); + (ser(BicliqueCover::new(graph, k))?, resolved_variant.clone()) + } + + // BMF + "BMF" => { + let matrix = parse_bool_matrix(args)?; + let rank = args.rank.ok_or_else(|| { + anyhow::anyhow!( + "BMF requires --matrix and --rank\n\n\ + Usage: pred create BMF --matrix \"1,0;0,1;1,1\" --rank 2" + ) + })?; + (ser(BMF::new(matrix, rank))?, resolved_variant.clone()) + } + + // ClosestVectorProblem + "ClosestVectorProblem" => { + let basis_str = args.basis.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "CVP requires --basis, --target-vec\n\n\ + Usage: pred create CVP --basis \"1,0;0,1\" --target-vec \"0.5,0.5\"" + ) + })?; + let target_str = args + .target_vec + .as_deref() + .ok_or_else(|| anyhow::anyhow!("CVP requires --target-vec (e.g., \"0.5,0.5\")"))?; + let basis: Vec> = basis_str + .split(';') + .map(|row| util::parse_comma_list(row.trim())) + .collect::>>()?; + let target: Vec = util::parse_comma_list(target_str)?; + let n = basis.len(); + let (lo, hi) = match args.bounds.as_deref() { + Some(s) => { + let parts: Vec = util::parse_comma_list(s)?; + if parts.len() != 2 { + bail!("--bounds expects \"lower,upper\" (e.g., \"-10,10\")"); + } + (parts[0], parts[1]) + } + None => (-10, 10), + }; + let bounds = vec![problemreductions::models::algebraic::VarBounds::bounded(lo, hi); n]; + ( + ser(ClosestVectorProblem::new(basis, target, bounds))?, + resolved_variant.clone(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; @@ -343,13 +514,7 @@ fn create_vertex_weight_problem( ) })?; let weights = parse_vertex_weights(args, n)?; - let data = match canonical { - "MaximumIndependentSet" => ser(MaximumIndependentSet::new(graph, weights))?, - "MinimumVertexCover" => ser(MinimumVertexCover::new(graph, weights))?, - "MaximumClique" => ser(MaximumClique::new(graph, weights))?, - "MinimumDominatingSet" => ser(MinimumDominatingSet::new(graph, weights))?, - _ => unreachable!(), - }; + let data = ser_vertex_weight_problem_with(canonical, graph, weights)?; Ok((data, resolved_variant.clone())) } } @@ -366,6 +531,7 @@ fn ser_vertex_weight_problem_with( "MinimumVertexCover" => ser(MinimumVertexCover::new(graph, weights)), "MaximumClique" => ser(MaximumClique::new(graph, weights)), "MinimumDominatingSet" => ser(MinimumDominatingSet::new(graph, weights)), + "MaximalIS" => ser(MaximalIS::new(graph, weights)), _ => unreachable!(), } } @@ -560,6 +726,67 @@ fn parse_clauses(args: &CreateArgs) -> Result> { .collect() } +/// Parse `--sets` as semicolon-separated sets of comma-separated usize. +/// E.g., "0,1;1,2;0,2" +fn parse_sets(args: &CreateArgs) -> Result>> { + let sets_str = args + .sets + .as_deref() + .ok_or_else(|| anyhow::anyhow!("This problem requires --sets (e.g., \"0,1;1,2;0,2\")"))?; + sets_str + .split(';') + .map(|set| { + set.trim() + .split(',') + .map(|s| { + s.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid set element: {}", e)) + }) + .collect() + }) + .collect() +} + +/// Parse `--weights` for set-based problems (i32), defaulting to all 1s. +fn parse_set_weights(args: &CreateArgs, num_sets: usize) -> Result> { + match &args.weights { + Some(w) => { + let weights: Vec = util::parse_comma_list(w)?; + if weights.len() != num_sets { + bail!("Expected {} weights but got {}", num_sets, weights.len()); + } + Ok(weights) + } + None => Ok(vec![1i32; num_sets]), + } +} + +/// Parse `--matrix` as semicolon-separated rows of comma-separated bool values (0/1). +/// E.g., "1,0;0,1;1,1" +fn parse_bool_matrix(args: &CreateArgs) -> Result>> { + let matrix_str = args + .matrix + .as_deref() + .ok_or_else(|| anyhow::anyhow!("This problem requires --matrix (e.g., \"1,0;0,1;1,1\")"))?; + matrix_str + .split(';') + .map(|row| { + row.trim() + .split(',') + .map(|s| match s.trim() { + "1" | "true" => Ok(true), + "0" | "false" => Ok(false), + other => Err(anyhow::anyhow!( + "Invalid boolean value '{}': expected 0/1 or true/false", + other + )), + }) + .collect() + }) + .collect() +} + /// Parse `--matrix` as semicolon-separated rows of comma-separated f64 values. /// E.g., "1,0.5;0.5,2" fn parse_matrix(args: &CreateArgs) -> Result>> { @@ -605,7 +832,8 @@ fn create_random( "MaximumIndependentSet" | "MinimumVertexCover" | "MaximumClique" - | "MinimumDominatingSet" => { + | "MinimumDominatingSet" + | "MaximalIS" => { let weights = vec![1i32; num_vertices]; match graph_type { "KingsSubgraph" => { @@ -640,13 +868,7 @@ fn create_random( } let graph = util::create_random_graph(num_vertices, edge_prob, args.seed); let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); - let data = match canonical { - "MaximumIndependentSet" => ser(MaximumIndependentSet::new(graph, weights))?, - "MinimumVertexCover" => ser(MinimumVertexCover::new(graph, weights))?, - "MaximumClique" => ser(MaximumClique::new(graph, weights))?, - "MinimumDominatingSet" => ser(MinimumDominatingSet::new(graph, weights))?, - _ => unreachable!(), - }; + let data = ser_vertex_weight_problem_with(canonical, graph, weights)?; (data, variant) } } diff --git a/problemreductions-cli/src/commands/graph.rs b/problemreductions-cli/src/commands/graph.rs index 9d7d8d1ad..665f2cca1 100644 --- a/problemreductions-cli/src/commands/graph.rs +++ b/problemreductions-cli/src/commands/graph.rs @@ -259,7 +259,6 @@ pub(crate) fn variant_to_full_slash(variant: &BTreeMap) -> Strin } } - /// Format a problem node as **bold name/variant** in slash notation. /// This is the single source of truth for "name/variant" display. fn fmt_node(_graph: &ReductionGraph, name: &str, variant: &BTreeMap) -> String { diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 43a5f2c42..2e715a1b1 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -21,6 +21,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("TSP", "TravelingSalesman"), ("BP", "BinPacking"), ("CVP", "ClosestVectorProblem"), + ("MaxMatching", "MaximumMatching"), ]; /// Resolve a short alias to the canonical problem name. @@ -39,7 +40,7 @@ pub fn resolve_alias(input: &str) -> String { "factoring" => "Factoring".to_string(), "maximumindependentset" => "MaximumIndependentSet".to_string(), "maximumclique" => "MaximumClique".to_string(), - "maximummatching" => "MaximumMatching".to_string(), + "maxmatching" | "maximummatching" => "MaximumMatching".to_string(), "minimumdominatingset" => "MinimumDominatingSet".to_string(), "minimumsetcovering" => "MinimumSetCovering".to_string(), "maximumsetpacking" => "MaximumSetPacking".to_string(), diff --git a/problemreductions-cli/src/util.rs b/problemreductions-cli/src/util.rs index 73e43742f..5452c27f7 100644 --- a/problemreductions-cli/src/util.rs +++ b/problemreductions-cli/src/util.rs @@ -227,3 +227,32 @@ pub fn variant_map(pairs: &[(&str, &str)]) -> BTreeMap { .map(|(k, v)| (k.to_string(), v.to_string())) .collect() } + +/// Parse a comma-separated list of values. +pub fn parse_comma_list(s: &str) -> Result> +where + T::Err: std::fmt::Display, +{ + s.split(',') + .map(|v| { + v.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid value '{}': {e}", v.trim())) + }) + .collect() +} + +/// Parse edge pairs like "0-1,1-2,2-3" into Vec<(usize, usize)>. +pub fn parse_edge_pairs(s: &str) -> Result> { + s.split(',') + .map(|pair| { + let parts: Vec<&str> = pair.trim().split('-').collect(); + if parts.len() != 2 { + bail!("Invalid edge '{}': expected format u-v", pair.trim()); + } + let u: usize = parts[0].trim().parse()?; + let v: usize = parts[1].trim().parse()?; + Ok((u, v)) + }) + .collect() +} diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 28396b507..d76028214 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1105,12 +1105,11 @@ fn test_create_unknown_problem() { #[test] fn test_create_no_flags_shows_help() { - // pred create MIS with no data flags shows schema-driven help + // pred create MIS with no data flags shows schema-driven help and exits non-zero let output = pred().args(["create", "MIS"]).output().unwrap(); assert!( - output.status.success(), - "stderr: {}", - String::from_utf8_lossy(&output.stderr) + !output.status.success(), + "should exit non-zero when showing help without data flags" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -1277,13 +1276,13 @@ fn test_path_unknown_cost() { #[test] fn test_path_overall_overhead_text() { // Use a multi-step path so the "Overall" section appears - let output = pred() - .args(["path", "3SAT", "MIS"]) - .output() - .unwrap(); + let output = pred().args(["path", "3SAT", "MIS"]).output().unwrap(); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); - assert!(stdout.contains("Overall"), "multi-step path should show Overall overhead"); + assert!( + stdout.contains("Overall"), + "multi-step path should show Overall overhead" + ); } #[test] @@ -1296,7 +1295,10 @@ fn test_path_overall_overhead_json() { assert!(output.status.success()); let content = std::fs::read_to_string(&tmp).unwrap(); let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert!(json["overall_overhead"].is_array(), "JSON should contain overall_overhead"); + assert!( + json["overall_overhead"].is_array(), + "JSON should contain overall_overhead" + ); let items = json["overall_overhead"].as_array().unwrap(); assert!(!items.is_empty(), "overall_overhead should have entries"); assert!(items[0]["field"].is_string()); @@ -1391,13 +1393,13 @@ fn test_path_all_overall_overhead() { #[test] fn test_path_single_step_no_overall_text() { // Single-step path should NOT show the Overall section - let output = pred() - .args(["path", "MIS", "QUBO"]) - .output() - .unwrap(); + let output = pred().args(["path", "MIS", "QUBO"]).output().unwrap(); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); - assert!(!stdout.contains("Overall"), "single-step path should not show Overall"); + assert!( + !stdout.contains("Overall"), + "single-step path should not show Overall" + ); } #[test] @@ -2557,12 +2559,11 @@ fn test_create_factoring_with_bits() { #[test] fn test_create_factoring_no_flags_shows_help() { - // pred create Factoring with no data flags shows schema-driven help + // pred create Factoring with no data flags shows schema-driven help and exits non-zero let output = pred().args(["create", "Factoring"]).output().unwrap(); assert!( - output.status.success(), - "stderr: {}", - String::from_utf8_lossy(&output.stderr) + !output.status.success(), + "should exit non-zero when showing help without data flags" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -2891,7 +2892,10 @@ fn test_create_kings_subgraph_help() { .args(["create", "MIS/KingsSubgraph"]) .output() .unwrap(); - assert!(output.status.success()); + assert!( + !output.status.success(), + "should exit non-zero when showing help" + ); let stderr = String::from_utf8(output.stderr).unwrap(); assert!( stderr.contains("positions") || stderr.contains("MaximumIndependentSet"), diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index 552f82cc0..113c98294 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -16,8 +16,6 @@ inventory::submit! { description: "Boolean matrix factorization", fields: &[ FieldInfo { name: "matrix", type_name: "Vec>", description: "Target boolean matrix A" }, - FieldInfo { name: "m", type_name: "usize", description: "Number of rows" }, - FieldInfo { name: "n", type_name: "usize", description: "Number of columns" }, FieldInfo { name: "k", type_name: "usize", description: "Factorization rank" }, ], } diff --git a/src/models/formula/circuit.rs b/src/models/formula/circuit.rs index 383f5a6bf..0c3be9bbd 100644 --- a/src/models/formula/circuit.rs +++ b/src/models/formula/circuit.rs @@ -15,7 +15,6 @@ inventory::submit! { description: "Find satisfying input to a boolean circuit", fields: &[ FieldInfo { name: "circuit", type_name: "Circuit", description: "The boolean circuit" }, - FieldInfo { name: "variables", type_name: "Vec", description: "Circuit variable names" }, ], } } diff --git a/src/models/misc/paintshop.rs b/src/models/misc/paintshop.rs index 1fe682724..6f7a847e9 100644 --- a/src/models/misc/paintshop.rs +++ b/src/models/misc/paintshop.rs @@ -17,10 +17,7 @@ inventory::submit! { module_path: module_path!(), description: "Minimize color changes in paint shop sequence", fields: &[ - FieldInfo { name: "sequence_indices", type_name: "Vec", description: "Car sequence as indices" }, - FieldInfo { name: "car_labels", type_name: "Vec", description: "Unique car labels" }, - FieldInfo { name: "is_first", type_name: "Vec", description: "First occurrence flags" }, - FieldInfo { name: "num_cars", type_name: "usize", description: "Number of unique cars" }, + FieldInfo { name: "sequence", type_name: "Vec", description: "Car labels (each must appear exactly twice)" }, ], } }