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
3 changes: 2 additions & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ Max<V>, Min<V>, Sum<W>, Or, And, Extremum<V>, ExtremumSense
- `variant_params!` macro implements `Problem::variant()` — e.g., `crate::variant_params![G, W]` for two type params, `crate::variant_params![]` for none (see `src/variant.rs`)
- `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed load/serialize/value-solve/witness-solve metadata. One entry per problem may be marked `default`, and variable names in complexity strings are validated at compile time against actual getter methods. Ordinary models are constructed directly from their construction schema. When user-facing construction differs from persisted JSON, define a model-local `#[derive(CreateSpec)]` DTO plus `TryFrom<CreateSpec>`, use its generated `FIELDS` in `ProblemSchemaEntry`, and register it with `create LocalSpec`; never add model-name branches in CLI or MCP code.
- `decision_problem_meta!` macro registers `DecisionProblemMeta` for a concrete inner type, providing the `DECISION_NAME` constant.
- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (aggregate Decision→Opt + Turing Opt→Decision) for a `Decision<P>` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision<P>` before invoking. Accepts `dims`, `fields`, and `size_getters` parameters for problem-specific size fields.
- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (aggregate Decision→Opt + Turing Opt→Decision) for a `Decision<P>` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision<P>` before invoking. Accepts an explicit structural `category` plus `dims`, `fields`, and `size_getters` parameters for problem-specific size fields.
- Problems parameterized by graph type `G` and optionally weight type `W` (problem-dependent)
- `Solver::solve()` computes the aggregate value for any `Problem` whose `Value` implements `Aggregate`
- `BruteForce::find_witness()` / `find_all_witnesses()` recover witnesses only when `P::Value::supports_witnesses()`
Expand Down Expand Up @@ -204,6 +204,7 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`:

### Extension Points
- New models register dynamic load/serialize/brute-force dispatch through `declare_variants!` in the model file, not by adding manual match arms in the CLI
- **Model category is explicit registry metadata.** Every `ProblemSchemaEntry` declares exactly one of `Algebraic`, `Formula`, `Graph`, `Misc`, or `Set`; catalog behavior never derives it from `module_path!()` or source location.
- **CLI creation is registry-driven and deferred:** `pred create` expands flags only for the selected concrete variant. Ordinary models use `ProblemSchemaEntry.fields` directly. Models whose construction differs from persisted JSON own a typed `CreateSpec` and fallible conversion beside the model; CLI and MCP only normalize transport values and invoke the registered constructor.
- **Each construction input has one name and one concrete type per variant.** Do not add compatibility aliases or infer types from flag names. `CreateSpec` field names render as `snake_case → kebab-case` in CLI and remain `snake_case` in MCP. Add a reusable codec only for a genuinely new transport representation, never a model-name parser branch.
- **Random generation is optional and variant-owned.** Not every model has a useful, well-defined random-instance distribution. Add `RandomGenerate` only when the generator has clear semantics and a concrete use (for example, testing or examples); never invent arbitrary bounds or distributions merely to make every model support `--random`. Implement it beside the model (normally through `impl_random_generate!` and a typed `CreateSpec` input DTO), then add `random` only to the applicable `declare_variants!` entries. CLI and MCP discover the exact variant's inputs and callback; never add a model-name random dispatch or advertise random generation on an unsupported variant.
Expand Down
7 changes: 5 additions & 2 deletions .claude/skills/add-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Read these first to understand the patterns:

Before implementing, make sure the plan explicitly covers these items that structural review checks later:
- Derive numeric implementation types from the mathematical domains in the issue and follow `docs/src/design.md#numeric-types-and-arithmetic`; serde/CLI construction uses the same validation as `new`/`try_new`, and boundary tests cover the supported maximum without requiring impractical allocation
- `ProblemSchemaEntry` metadata is complete for the construction interface (`display_name`, `aliases`, `dimensions`, and `fields`)
- `ProblemSchemaEntry` metadata is complete (`display_name`, `aliases`, `dimensions`, explicit `category`, and construction `fields`)
- `Problem::Value` uses the correct aggregate wrapper and witness support is intentional
- `declare_variants!` is present with exactly one `default` variant when multiple concrete variants exist
- CLI discovery and `pred create <ProblemName>` support are included where applicable
Expand All @@ -92,6 +92,8 @@ Choose the appropriate sub-module under `src/models/`:
- `algebraic/` -- matrices, linear systems, lattices (QUBO, ILP, CVP, BMF)
- `misc/` -- unique input structures that don't fit other categories (BinPacking, PaintShop, Factoring)

Declare the same structural choice explicitly in `ProblemSchemaEntry.category`. This is required metadata and is never inferred from `module_path!()` or the file location.

## Step 1.5: Infer problem size getters

From the **best known exact algorithm** complexity (item 9), infer what problem size getter methods the struct should expose. The variables used in the complexity expression define the natural size metrics.
Expand Down Expand Up @@ -122,7 +124,7 @@ Create `src/models/<category>/<name>.rs`:
```

Key decisions:
- **Schema metadata:** `ProblemSchemaEntry` must reflect the construction interface, including `display_name`, `aliases`, `dimensions`, and `fields`
- **Schema metadata:** `ProblemSchemaEntry` must include the explicit structural `category` and reflect the construction interface through `display_name`, `aliases`, `dimensions`, and `fields`
- **Objective problems:** use `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the model should expose optimization-style witness helpers
- **Witness problems:** use `type Value = Or` for existential feasibility problems
- **Aggregate-only problems:** use a value-only aggregate such as `Sum<_>`, `And`, or a custom `Aggregate` when witnesses are not meaningful
Expand Down Expand Up @@ -313,6 +315,7 @@ Structural and quality review is handled by the `review-pipeline` stage, not her
|---------|-----|
| Implementing weight management as a trait | Use inherent methods: `weights()`, `set_weights()`, `is_weighted()` |
| Forgetting `inventory::submit!` | Every problem needs a `ProblemSchemaEntry` registration |
| Omitting or inferring the model category | Set the required `ProblemSchemaEntry.category` explicitly to one of `Algebraic`, `Formula`, `Graph`, `Misc`, or `Set`; never parse `module_path!()`. |
| Missing `#[path]` test link | Add `#[cfg(test)] #[path = "..."] mod tests;` at file bottom |
| Wrong `dims()` | Must match the actual configuration space (e.g., `vec![2; n]` for binary) |
| Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `bool`, aggregate-only models use a fold value like `Sum` / `And` |
Expand Down
3 changes: 2 additions & 1 deletion problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use problemreductions::registry::ProblemCategory;
use std::path::PathBuf;

pub use crate::create_args::CreateArgs;
Expand Down Expand Up @@ -68,7 +69,7 @@ Examples:

/// Restrict problems to a model category such as graph, set, or misc
#[arg(long, conflicts_with = "rules")]
category: Option<String>,
category: Option<ProblemCategory>,

/// List the complete catalog instead of the summary
#[arg(long)]
Expand Down
49 changes: 23 additions & 26 deletions problemreductions-cli/src/commands/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::output::OutputConfig;
use crate::problem_name::{aliases_for, parse_problem_spec, resolve_problem_ref};
use anyhow::Result;
use problemreductions::registry::collect_schemas;
use problemreductions::registry::ProblemCategory;
use problemreductions::rules::{MeasuredPath, ReductionGraph, ReductionPath, TraversalFlow};
use problemreductions::{Expr, Growth};
use std::any::Any;
Expand All @@ -11,7 +12,7 @@ use std::path::Path;

pub fn list(
query: Option<&str>,
category: Option<&str>,
category: Option<ProblemCategory>,
all: bool,
verbose: bool,
out: &OutputConfig,
Expand All @@ -38,30 +39,25 @@ pub fn list(
}
}
let query = query.map(str::to_lowercase);
let category = category.map(str::to_lowercase);
let selected = catalog
.iter()
.filter(|problem| {
category.as_ref().is_none_or(|wanted| {
problem
.category
.unwrap_or("uncategorized")
.eq_ignore_ascii_case(wanted)
}) && query.as_ref().is_none_or(|needle| {
problem.canonical_name.to_lowercase().contains(needle)
|| problem.display_name.to_lowercase().contains(needle)
|| problem
.aliases
.iter()
.any(|alias| alias.to_lowercase().contains(needle))
|| variant_aliases
.get(problem.canonical_name)
.is_some_and(|aliases| {
aliases
.iter()
.any(|alias| alias.to_lowercase().contains(needle))
})
})
category.is_none_or(|wanted| problem.category == wanted)
&& query.as_ref().is_none_or(|needle| {
problem.canonical_name.to_lowercase().contains(needle)
|| problem.display_name.to_lowercase().contains(needle)
|| problem
.aliases
.iter()
.any(|alias| alias.to_lowercase().contains(needle))
|| variant_aliases
.get(problem.canonical_name)
.is_some_and(|aliases| {
aliases
.iter()
.any(|alias| alias.to_lowercase().contains(needle))
})
})
})
.collect::<Vec<_>>();
let graph = needs_variant_rows.then(ReductionGraph::new);
Expand All @@ -79,6 +75,7 @@ pub fn list(
rules: usize,
/// Best-known complexity
complexity: String,
category: ProblemCategory,
}

let mut rows_data: Vec<VariantRow> = Vec::new();
Expand Down Expand Up @@ -124,16 +121,15 @@ pub fn list(
is_default,
rules: if i == 0 { rules } else { 0 },
complexity,
category: problem.category,
});
}
}
}

let mut category_counts = BTreeMap::new();
for problem in &catalog {
*category_counts
.entry(problem.category.unwrap_or("uncategorized"))
.or_insert(0usize) += 1;
*category_counts.entry(problem.category).or_insert(0usize) += 1;
}

let columns: Vec<(&str, Align, usize)> = vec![
Expand Down Expand Up @@ -201,7 +197,7 @@ pub fn list(
vec![
problem.canonical_name.to_string(),
aliases.join(", "),
problem.category.unwrap_or("uncategorized").to_string(),
problem.category.to_string(),
variant_counts
.get(problem.canonical_name)
.copied()
Expand Down Expand Up @@ -248,6 +244,7 @@ pub fn list(
"default": r.is_default,
"rules": r.rules,
"complexity": r.complexity,
"category": r.category,
})
}).collect::<Vec<_>>(),
});
Expand Down
2 changes: 1 addition & 1 deletion problemreductions-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fn main() -> anyhow::Result<()> {
if rules {
commands::graph::list_rules(query.as_deref(), all, verbose, &out)
} else {
commands::graph::list(query.as_deref(), category.as_deref(), all, verbose, &out)
commands::graph::list(query.as_deref(), category, all, verbose, &out)
}
}
Commands::Show { problem } => commands::graph::show(&problem, &out),
Expand Down
18 changes: 18 additions & 0 deletions problemreductions-cli/src/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ problemreductions::inventory::submit! {
display_name: "CLI test aggregate value source",
aliases: &[],
dimensions: &[],
category: problemreductions::registry::ProblemCategory::Misc,
module_path: module_path!(),
description: "Test-only dynamically discovered construction model",
fields: &[FieldInfo {
Expand All @@ -145,6 +146,23 @@ problemreductions::inventory::submit! {
}
}

problemreductions::inventory::submit! {
ProblemSchemaEntry {
name: AggregateValueTarget::NAME,
display_name: "CLI test aggregate value target",
aliases: &[],
dimensions: &[],
category: problemreductions::registry::ProblemCategory::Misc,
module_path: module_path!(),
description: "Test-only aggregate reduction target",
fields: &[FieldInfo {
name: "base",
type_name: "u64",
description: "Base aggregate value",
}],
}
}

problemreductions::inventory::submit! {
VariantEntry {
name: AggregateValueSource::NAME,
Expand Down
18 changes: 18 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ fn test_list() {
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Registered catalog"));
assert!(stdout.contains("graph"));
for category in ["algebraic", "formula", "graph", "misc", "set"] {
assert!(stdout.contains(category));
}
assert!(!stdout.contains("MaximumIndependentSet"));
assert!(stdout.lines().count() < 30, "default list is too verbose");
}
Expand Down Expand Up @@ -117,11 +120,26 @@ fn test_list_json_respects_category_filter() {
assert!(variants
.iter()
.all(|variant| variant["name"] != "MaximumIndependentSet"));
assert!(variants
.iter()
.all(|variant| variant["category"] == "formula"));
assert!(variants
.iter()
.any(|variant| variant["name"] == "KSatisfiability/K3"));
}

#[test]
fn test_list_category_rejects_unknown_value() {
let output = pred()
.args(["list", "--category", "unknown"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("unknown problem category `unknown`"));
assert!(stderr.contains("algebraic, formula, graph, misc, set"));
}

#[test]
fn test_list_searches_variant_aliases() {
let output = pred().args(["list", "3SAT"]).output().unwrap();
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/algebraic_equations_over_gf2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ inventory::submit! {
display_name: "Algebraic Equations over GF(2)",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Find assignment satisfying multilinear polynomial equations over GF(2)",
fields: &[
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/bmf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ inventory::submit! {
display_name: "BMF",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Boolean matrix factorization",
fields: &[
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/closest_vector_problem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ inventory::submit! {
display_name: "Closest Vector Problem",
aliases: &["CVP"],
dimensions: &[VariantDimension::new("weight", "i32", &["i32", "f64"])],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Find the closest lattice point to a target vector",
fields: ClosestVectorProblemI32CreateSpec::FIELDS,
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/consecutive_block_minimization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ inventory::submit! {
display_name: "Consecutive Block Minimization",
aliases: &["CBM"],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Permute columns of a binary matrix to have at most K consecutive blocks of 1s",
fields: ConsecutiveBlockMinimizationCreateSpec::FIELDS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ inventory::submit! {
display_name: "Consecutive Ones Matrix Augmentation",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Augment a binary matrix with at most K zero-to-one flips so some column permutation has the consecutive ones property",
fields: ConsecutiveOnesMatrixAugmentationCreateSpec::FIELDS,
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/consecutive_ones_submatrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ inventory::submit! {
display_name: "Consecutive Ones Submatrix",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Find K columns of a binary matrix that can be permuted to have the consecutive ones property",
fields: &[
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/equilibrium_point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ inventory::submit! {
display_name: "Equilibrium Point",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Decide whether a pure-strategy Nash equilibrium exists for a multi-player game with polynomial payoff functions",
fields: &[
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/feasible_basis_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ inventory::submit! {
display_name: "Feasible Basis Extension",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Given matrix A, vector a_bar, and required columns S, find a feasible basis extending S",
fields: FeasibleBasisExtensionCreateSpec::FIELDS,
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ inventory::submit! {
display_name: "ILP",
aliases: &[],
dimensions: &[VariantDimension::new("variable", "bool", &["bool", "i32"])],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Optimize linear objective subject to linear constraints",
fields: &[
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/minimum_matrix_cover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ inventory::submit! {
display_name: "Minimum Matrix Cover",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Find sign assignment minimizing quadratic form over nonnegative integer matrix",
fields: &[
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/minimum_matrix_domination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ inventory::submit! {
display_name: "Minimum Matrix Domination",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Find minimum subset of 1-entries in a binary matrix that dominates all other 1-entries by shared row or column",
fields: &[
Expand Down
1 change: 1 addition & 0 deletions src/models/algebraic/minimum_weight_decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ inventory::submit! {
display_name: "Minimum Weight Decoding",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Find minimum Hamming weight binary vector x such that Hx ≡ s (mod 2)",
fields: MinimumWeightDecodingCreateSpec::FIELDS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ inventory::submit! {
display_name: "Minimum Weight Solution to Linear Equations",
aliases: &[],
dimensions: &[],
category: crate::registry::ProblemCategory::Algebraic,
module_path: module_path!(),
description: "Find a rational solution to Ay=b minimizing the number of non-zero entries",
fields: MinimumWeightSolutionCreateSpec::FIELDS,
Expand Down
Loading