Skip to content

Commit a88812b

Browse files
GiggleLiuclaude
andauthored
fix: CLI QA improvements — docs, display, SpinGlass f64, auto-JSON (#139)
- Fix docs/src/cli.md: replace all --edges with --graph to match actual CLI flag - Fix docs typo suggestion example to use a close misspelling that triggers the hint - Show full variant slash notation (Name/Graph/Weight) instead of diff-from-default - Remove dead variant_to_slash function and unused HashSet import - Deduplicate format_variant into shared variant_to_full_slash helper - Auto-output JSON when stdout is piped for data commands (reduce, solve, evaluate, inspect) - Add SpinGlass f64 weight inference from float syntax in --couplings/--fields - Remove duplicate [default: 1] in help text (clap already shows it) - Count reachable nodes (not unique problem names) in neighbor output Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4ad5a83 commit a88812b

8 files changed

Lines changed: 149 additions & 121 deletions

File tree

docs/src/cli.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Available backends: `highs` (default), `coin-cbc`, `clarabel`, `scip`, `lpsolve`
4040

4141
```bash
4242
# Create a Maximum Independent Set problem
43-
pred create MIS --edges 0-1,1-2,2-3 -o problem.json
43+
pred create MIS --graph 0-1,1-2,2-3 -o problem.json
4444

4545
# Solve it (auto-reduces to ILP)
4646
pred solve problem.json
@@ -56,8 +56,8 @@ pred reduce problem.json --to QUBO -o reduced.json
5656
pred solve reduced.json --solver brute-force
5757

5858
# Pipe commands together (use - to read from stdin)
59-
pred create MIS --edges 0-1,1-2,2-3 | pred solve -
60-
pred create MIS --edges 0-1,1-2,2-3 | pred reduce - --to QUBO | pred solve -
59+
pred create MIS --graph 0-1,1-2,2-3 | pred solve -
60+
pred create MIS --graph 0-1,1-2,2-3 | pred reduce - --to QUBO | pred solve -
6161
```
6262

6363
## Global Flags
@@ -232,13 +232,13 @@ pred export-graph -o reduction_graph.json # save to file
232232
Construct a problem instance from CLI arguments and save as JSON:
233233

234234
```bash
235-
pred create MIS --edges 0-1,1-2,2-3 -o problem.json
236-
pred create MIS --edges 0-1,1-2,2-3 --weights 2,1,3,1 -o problem.json
235+
pred create MIS --graph 0-1,1-2,2-3 -o problem.json
236+
pred create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o problem.json
237237
pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json
238238
pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json
239-
pred create KColoring --k 3 --edges 0-1,1-2,2-0 -o kcol.json
240-
pred create SpinGlass --edges 0-1,1-2 -o sg.json
241-
pred create MaxCut --edges 0-1,1-2,2-0 -o maxcut.json
239+
pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json
240+
pred create SpinGlass --graph 0-1,1-2 -o sg.json
241+
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
242242
pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json
243243
pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json
244244
```
@@ -254,7 +254,7 @@ pred create MaxCut --random --num-vertices 20 --edge-prob 0.5 -o maxcut.json
254254
Without `-o`, the problem JSON is printed to stdout, which can be piped to other commands:
255255

256256
```bash
257-
pred create MIS --edges 0-1,1-2,2-3 | pred solve -
257+
pred create MIS --graph 0-1,1-2,2-3 | pred solve -
258258
pred create MIS --random --num-vertices 10 | pred inspect -
259259
```
260260

@@ -280,7 +280,7 @@ Valid(2)
280280
Stdin is supported with `-`:
281281

282282
```bash
283-
pred create MIS --edges 0-1,1-2,2-3 | pred evaluate - --config 1,0,1,0
283+
pred create MIS --graph 0-1,1-2,2-3 | pred evaluate - --config 1,0,1,0
284284
```
285285

286286
### `pred inspect` — Inspect a problem file
@@ -297,7 +297,7 @@ Works with reduction bundles and stdin:
297297

298298
```bash
299299
pred inspect bundle.json
300-
pred create MIS --edges 0-1,1-2 | pred inspect -
300+
pred create MIS --graph 0-1,1-2 | pred inspect -
301301
```
302302

303303
### `pred reduce` — Reduce a problem
@@ -317,7 +317,7 @@ pred reduce problem.json --via path.json -o reduced.json
317317
Stdin is supported with `-`:
318318

319319
```bash
320-
pred create MIS --edges 0-1,1-2,2-3 | pred reduce - --to QUBO
320+
pred create MIS --graph 0-1,1-2,2-3 | pred reduce - --to QUBO
321321
```
322322

323323
The bundle contains everything needed to map solutions back:
@@ -346,8 +346,8 @@ pred solve problem.json --timeout 30 # abort after 30 seconds
346346
Stdin is supported with `-`:
347347

348348
```bash
349-
pred create MIS --edges 0-1,1-2,2-3 | pred solve -
350-
pred create MIS --edges 0-1,1-2,2-3 | pred solve - --solver brute-force
349+
pred create MIS --graph 0-1,1-2,2-3 | pred solve -
350+
pred create MIS --graph 0-1,1-2,2-3 | pred solve - --solver brute-force
351351
```
352352

353353
When the problem is not ILP, the solver automatically reduces it to ILP, solves, and maps the solution back. The auto-reduction is shown in the output:
@@ -429,8 +429,10 @@ You can also specify variants with a slash: `MIS/UnitDiskGraph`, `SpinGlass/Simp
429429
If you mistype a problem name, `pred` will suggest the closest match:
430430

431431
```bash
432-
$ pred show MaxIndependentSet
433-
Error: Unknown problem: MaxIndependentSet
434-
Did you mean: MaximumIndependentSet?
435-
Run `pred list` to see all available problem types.
432+
$ pred show MaximumIndependentSe
433+
Error: Unknown problem: MaximumIndependentSe
434+
435+
Did you mean: MaximumIndependentSet?
436+
437+
Run `pred list` to see all available problems.
436438
```

problemreductions-cli/src/cli.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Use `pred from <problem>` for outgoing neighbors (what this reduces to).")]
8181
/// Problem name or alias (e.g., MIS, QUBO, MIS/UnitDiskGraph)
8282
#[arg(value_parser = crate::problem_name::ProblemNameParser)]
8383
problem: String,
84-
/// Number of hops to explore [default: 1]
84+
/// Number of hops to explore
8585
#[arg(long, default_value = "1")]
8686
hops: usize,
8787
},
@@ -98,7 +98,7 @@ Use `pred to <problem>` for incoming neighbors (what reduces to this).")]
9898
/// Problem name or alias (e.g., MIS, QUBO, MIS/UnitDiskGraph)
9999
#[arg(value_parser = crate::problem_name::ProblemNameParser)]
100100
problem: String,
101-
/// Number of hops to explore [default: 1]
101+
/// Number of hops to explore
102102
#[arg(long, default_value = "1")]
103103
hops: usize,
104104
},

problemreductions-cli/src/commands/create.rs

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,26 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
235235
"{e}\n\nUsage: pred create SpinGlass --graph 0-1,1-2 [--couplings 1,1] [--fields 0,0,0]"
236236
)
237237
})?;
238-
let couplings = parse_couplings(args, graph.num_edges())?;
239-
let fields = parse_fields(args, n)?;
240-
(
241-
ser(SpinGlass::from_graph(graph, couplings, fields))?,
242-
resolved_variant.clone(),
243-
)
238+
let use_f64 = resolved_variant.get("weight").is_some_and(|w| w == "f64")
239+
|| has_float_syntax(&args.couplings)
240+
|| has_float_syntax(&args.fields);
241+
if use_f64 {
242+
let couplings = parse_couplings_f64(args, graph.num_edges())?;
243+
let fields = parse_fields_f64(args, n)?;
244+
let mut variant = resolved_variant.clone();
245+
variant.insert("weight".to_string(), "f64".to_string());
246+
(
247+
ser(SpinGlass::from_graph(graph, couplings, fields))?,
248+
variant,
249+
)
250+
} else {
251+
let couplings = parse_couplings(args, graph.num_edges())?;
252+
let fields = parse_fields(args, n)?;
253+
(
254+
ser(SpinGlass::from_graph(graph, couplings, fields))?,
255+
resolved_variant.clone(),
256+
)
257+
}
244258
}
245259

246260
// Factoring
@@ -486,6 +500,45 @@ fn parse_fields(args: &CreateArgs, num_vertices: usize) -> Result<Vec<i32>> {
486500
}
487501
}
488502

503+
/// Check if a CLI string value contains float syntax (a decimal point).
504+
fn has_float_syntax(value: &Option<String>) -> bool {
505+
value.as_ref().is_some_and(|s| s.contains('.'))
506+
}
507+
508+
/// Parse `--couplings` as SpinGlass pairwise couplings (f64), defaulting to all 1.0.
509+
fn parse_couplings_f64(args: &CreateArgs, num_edges: usize) -> Result<Vec<f64>> {
510+
match &args.couplings {
511+
Some(w) => {
512+
let vals: Vec<f64> = w
513+
.split(',')
514+
.map(|s| s.trim().parse::<f64>())
515+
.collect::<std::result::Result<Vec<_>, _>>()?;
516+
if vals.len() != num_edges {
517+
bail!("Expected {} couplings but got {}", num_edges, vals.len());
518+
}
519+
Ok(vals)
520+
}
521+
None => Ok(vec![1.0f64; num_edges]),
522+
}
523+
}
524+
525+
/// Parse `--fields` as SpinGlass on-site fields (f64), defaulting to all 0.0.
526+
fn parse_fields_f64(args: &CreateArgs, num_vertices: usize) -> Result<Vec<f64>> {
527+
match &args.fields {
528+
Some(w) => {
529+
let vals: Vec<f64> = w
530+
.split(',')
531+
.map(|s| s.trim().parse::<f64>())
532+
.collect::<std::result::Result<Vec<_>, _>>()?;
533+
if vals.len() != num_vertices {
534+
bail!("Expected {} fields but got {}", num_vertices, vals.len());
535+
}
536+
Ok(vals)
537+
}
538+
None => Ok(vec![0.0f64; num_vertices]),
539+
}
540+
}
541+
489542
/// Parse `--clauses` as semicolon-separated clauses of comma-separated literals.
490543
/// E.g., "1,2;-1,3;2,-3"
491544
fn parse_clauses(args: &CreateArgs) -> Result<Vec<CNFClause>> {

problemreductions-cli/src/commands/graph.rs

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
44
use problemreductions::registry::collect_schemas;
55
use problemreductions::rules::{Minimize, MinimizeSteps, ReductionGraph, TraversalDirection};
66
use problemreductions::types::ProblemSize;
7-
use std::collections::{BTreeMap, HashSet};
7+
use std::collections::BTreeMap;
88

99
pub fn list(out: &OutputConfig) -> Result<()> {
1010
use crate::output::{format_table, Align};
@@ -250,7 +250,7 @@ pub fn show(problem: &str, out: &OutputConfig) -> Result<()> {
250250

251251
/// Convert a variant BTreeMap to slash notation showing ALL values.
252252
/// E.g., {graph: "SimpleGraph", weight: "i32"} → "/SimpleGraph/i32".
253-
fn variant_to_full_slash(variant: &BTreeMap<String, String>) -> String {
253+
pub(crate) fn variant_to_full_slash(variant: &BTreeMap<String, String>) -> String {
254254
if variant.is_empty() {
255255
String::new()
256256
} else {
@@ -259,34 +259,11 @@ fn variant_to_full_slash(variant: &BTreeMap<String, String>) -> String {
259259
}
260260
}
261261

262-
/// Convert a variant BTreeMap to slash notation showing only non-default values.
263-
/// Given default {graph: "SimpleGraph", weight: "i32"} and variant {graph: "UnitDiskGraph", weight: "i32"},
264-
/// returns "/UnitDiskGraph".
265-
fn variant_to_slash(
266-
variant: &BTreeMap<String, String>,
267-
default: &BTreeMap<String, String>,
268-
) -> String {
269-
let diffs: Vec<&str> = variant
270-
.iter()
271-
.filter(|(k, v)| default.get(*k) != Some(*v))
272-
.map(|(_, v)| v.as_str())
273-
.collect();
274-
if diffs.is_empty() {
275-
String::new()
276-
} else {
277-
format!("/{}", diffs.join("/"))
278-
}
279-
}
280262

281263
/// Format a problem node as **bold name/variant** in slash notation.
282264
/// This is the single source of truth for "name/variant" display.
283-
fn fmt_node(graph: &ReductionGraph, name: &str, variant: &BTreeMap<String, String>) -> String {
284-
let default = graph
285-
.variants_for(name)
286-
.first()
287-
.cloned()
288-
.unwrap_or_default();
289-
let slash = variant_to_slash(variant, &default);
265+
fn fmt_node(_graph: &ReductionGraph, name: &str, variant: &BTreeMap<String, String>) -> String {
266+
let slash = variant_to_full_slash(variant);
290267
crate::output::fmt_problem_name(&format!("{name}{slash}"))
291268
}
292269

@@ -624,11 +601,9 @@ pub fn neighbors(
624601
text.push('\n');
625602
render_tree(&graph, &tree, &mut text, "");
626603

627-
// Count unique problem names
628-
let unique_names: HashSet<&str> = neighbors.iter().map(|n| n.name).collect();
629604
text.push_str(&format!(
630-
"\n{} reachable problems in {} hops\n",
631-
unique_names.len(),
605+
"\n{} reachable nodes in {} hops\n",
606+
neighbors.len(),
632607
max_hops,
633608
));
634609

problemreductions-cli/src/commands/reduce.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ pub fn reduce(
7979
anyhow::bail!(
8080
"Path file starts with {}{} but source problem is {}{}",
8181
first.name,
82-
format_variant(&first.variant),
82+
variant_to_full_slash(&first.variant),
8383
source_name,
84-
format_variant(&source_variant),
84+
variant_to_full_slash(&source_variant),
8585
);
8686
}
8787
// If --to is given, validate it matches the path's target
@@ -205,11 +205,4 @@ pub fn reduce(
205205
Ok(())
206206
}
207207

208-
fn format_variant(v: &BTreeMap<String, String>) -> String {
209-
if v.is_empty() {
210-
String::new()
211-
} else {
212-
let vals: Vec<&str> = v.values().map(|v| v.as_str()).collect();
213-
format!("/{}", vals.join("/"))
214-
}
215-
}
208+
use super::graph::variant_to_full_slash;

problemreductions-cli/src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,17 @@ fn main() -> anyhow::Result<()> {
2929
}
3030
};
3131

32+
// Data-producing commands auto-output JSON when piped
33+
let auto_json = matches!(
34+
cli.command,
35+
Commands::Reduce(_) | Commands::Solve(_) | Commands::Evaluate(_) | Commands::Inspect(_)
36+
);
37+
3238
let out = OutputConfig {
3339
output: cli.output,
3440
quiet: cli.quiet,
3541
json: cli.json,
42+
auto_json,
3643
};
3744

3845
match cli.command {

problemreductions-cli/src/output.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ pub struct OutputConfig {
1212
pub quiet: bool,
1313
/// Output JSON to stdout instead of human-readable text.
1414
pub json: bool,
15+
/// When true, auto-output JSON if stdout is not a TTY (piped).
16+
/// Used for data-producing commands (reduce, solve, evaluate, inspect).
17+
pub auto_json: bool,
1518
}
1619

1720
impl OutputConfig {
@@ -36,7 +39,7 @@ impl OutputConfig {
3639
std::fs::write(path, &content)
3740
.with_context(|| format!("Failed to write {}", path.display()))?;
3841
self.info(&format!("Wrote {}", path.display()));
39-
} else if self.json {
42+
} else if self.json || (self.auto_json && !std::io::stdout().is_terminal()) {
4043
println!(
4144
"{}",
4245
serde_json::to_string_pretty(json_value).context("Failed to serialize JSON")?

0 commit comments

Comments
 (0)