Skip to content
Open
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
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Max<V>, Min<V>, Sum<W>, Or, And, Extremum<V>, ExtremumSense
- Decision-equivalence rules map completed `Or` values identically. Decision-to-optimization rules own their feasibility/threshold map; reject target configurations that do not certify YES instead of returning an invalid source witness. Optimization rules decode optimal witnesses and evaluate the source; register a value map only when mathematically defined. Counting and universal rules map completed folds without witnesses. Follow [result mappings](../docs/src/design.md#result-mappings); no mandatory rule-category tags.
- Decode only the reduction's defined mathematical mapping. Reject malformed structure with `ExtractionError`; never panic, truncate, clamp, invent defaults, or add recovery branches. Explicit mathematical alternatives and sentinels are allowed. Test successful decoding and every rejected representation.
- CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`)
- Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph
- Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph, SimpleDigraph, MultiDigraph, MultiMixedGraph
- Weight types: `One` (unit weight marker), `i64`, `f64` — all implement `WeightElement` trait
- `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type
- Weight management via inherent methods (`weights()`, `set_weights()`, `is_weighted()`), not traits
Expand Down
104 changes: 52 additions & 52 deletions docs/paper/reductions.typ

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion docs/src/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,27 @@ nodes in the reduction graph, and explicit reduction rules connect them.

Variant types fall into three categories:

- **Graph type** — `SimpleGraph`, `PlanarGraph`, `BipartiteGraph`, `UnitDiskGraph`, `KingsSubgraph`, `TriangularSubgraph`.
- **Graph type** — `SimpleGraph`, `PlanarGraph`, `BipartiteGraph`, `UnitDiskGraph`, `KingsSubgraph`, `TriangularSubgraph`, `SimpleDigraph`, `MultiDigraph`, `MultiMixedGraph`.
- **Weight type** — `One` (unweighted), `i64`, `f64`.
- **K value** — e.g., `K3` for 3-SAT, `KN` for arbitrary K.

`SimpleGraph` is undirected, loop-free, and has no parallel edges: `(u, v)` and
`(v, u)` are the same edge. Its fallible constructor, `new`, and serde share
validation. `PlanarGraph` wraps it; `BipartiteGraph` also rejects repeated
cross-partition pairs. `UnitDiskGraph`, `KingsSubgraph`, and `TriangularSubgraph`
generate each edge between distinct vertex indices once, including when two
vertices have coincident positions.

`SimpleDigraph` is loop-free and rejects repeated ordered arcs; `(u, v)`
and `(v, u)` are distinct. It validates a wrapped `MultiDigraph` and
delegates graph algorithms to it. `MultiDigraph` allows loops and parallel
arcs; `arc_count(u, v)` exposes multiplicity. `MultiMixedGraph` allows loops and
parallel arcs and undirected edges. In both multi types, arc i is entry i of
`arcs()`; models index their per-arc data by that same order. All three types
validate serde input and retain the JSON shapes `{num_vertices, arcs}` or
`{num_vertices, arcs, edges}`.


<div class="theme-light-only">

![Lattices](static/lattices.svg)
Expand Down
14 changes: 9 additions & 5 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use problemreductions::models::graph::{LabelledArc, LabelledDigraph};
use problemreductions::models::misc::{CbqRelation, FrequencyTable, KnownValue, QueryArg};
use problemreductions::prelude::*;
use problemreductions::topology::{
DirectedGraph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph,
KingsSubgraph, MultiDigraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph,
};
use serde::Serialize;
use std::collections::{BTreeMap, BTreeSet};
Expand Down Expand Up @@ -478,15 +478,15 @@ fn parse_bool_rows(rows_str: &str) -> Result<Vec<Vec<bool>>> {
Ok(matrix)
}

/// Parse `--arcs` as directed arc pairs and build a `DirectedGraph`.
/// Parse `--arcs` as directed arc pairs and build a `MultiDigraph`.
///
/// Returns `(graph, num_arcs)`. Infers vertex count from arc endpoints
/// unless `num_vertices` is provided (which must be >= inferred count).
/// E.g., "0>1,1>2,2>0"
fn parse_directed_graph(
arcs_str: &str,
num_vertices: Option<usize>,
) -> Result<(DirectedGraph, usize)> {
) -> Result<(MultiDigraph, usize)> {
let arcs: Vec<(usize, usize)> = arcs_str
.split(',')
.map(|pair| {
Expand All @@ -506,7 +506,11 @@ fn parse_directed_graph(
.iter()
.flat_map(|&(u, v)| [u, v])
.max()
.map(|m| m + 1)
.map(|m| {
m.checked_add(1)
.ok_or_else(|| anyhow::anyhow!("vertex count overflows usize"))
})
.transpose()?
.unwrap_or(0);
let num_v = match num_vertices {
Some(user_num_v) => {
Expand All @@ -522,7 +526,7 @@ fn parse_directed_graph(
None => inferred_num_v,
};
let num_arcs = arcs.len();
Ok((DirectedGraph::new(num_v, arcs), num_arcs))
Ok((MultiDigraph::try_new(num_v, arcs)?, num_arcs))
}

/// Parse implication rules from semicolon-separated "antecedents>consequent" strings.
Expand Down
22 changes: 15 additions & 7 deletions problemreductions-cli/src/commands/create/schema_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl CreateContext {
.and_then(serde_json::Value::as_array)
.map(Vec::len);
}
"DirectedGraph" => {
"MultiDigraph" | "SimpleDigraph" => {
self.num_vertices = value
.get("num_vertices")
.and_then(serde_json::Value::as_u64)
Expand Down Expand Up @@ -324,7 +324,7 @@ pub(crate) fn create_inputs_for(
for field in schema.fields {
let concrete_type = resolve_schema_field_type(field.type_name, resolved_variant);
match concrete_type.as_str() {
"DirectedGraph" => {
"MultiDigraph" | "SimpleDigraph" => {
insert_create_input(&mut inputs, "arcs", InputValueKind::Text, field.name);
}
_ => {
Expand All @@ -340,7 +340,10 @@ pub(crate) fn create_inputs_for(
}
if schema.fields.iter().any(|field| {
let concrete_type = resolve_schema_field_type(field.type_name, resolved_variant);
matches!(concrete_type.as_str(), "SimpleGraph" | "DirectedGraph")
matches!(
concrete_type.as_str(),
"SimpleGraph" | "MultiDigraph" | "SimpleDigraph"
)
}) {
insert_create_input(
&mut inputs,
Expand Down Expand Up @@ -534,7 +537,7 @@ pub(super) fn parse_field_value(
let normalized_type = normalize_type_name(concrete_type);
let value = match normalized_type.as_str() {
"SimpleGraph" => parse_simple_graph_value(raw, context)?,
"DirectedGraph" => parse_directed_graph_value(raw, context)?,
"MultiDigraph" | "SimpleDigraph" => parse_directed_graph_value(raw, context)?,
"LabelledDigraph" => parse_labelled_digraph_value(raw, field_name)?,
"KingsSubgraph" => parse_grid_subgraph_value(raw, true)?,
"TriangularSubgraph" => parse_grid_subgraph_value(raw, false)?,
Expand Down Expand Up @@ -1106,7 +1109,12 @@ pub(super) fn parse_simple_graph_value(
.iter()
.flat_map(|&(u, v)| [u, v])
.max()
.map(|max_vertex| max_vertex + 1)
.map(|max_vertex| {
max_vertex
.checked_add(1)
.ok_or_else(|| anyhow::anyhow!("vertex count overflows usize"))
})
.transpose()?
.unwrap_or(0);
let num_vertices = match num_vertices {
Some(explicit) => {
Expand All @@ -1118,7 +1126,7 @@ pub(super) fn parse_simple_graph_value(
}
None => inferred_num_vertices,
};
SimpleGraph::new(num_vertices, edges)
SimpleGraph::try_new(num_vertices, edges)?
};
Ok(serde_json::to_value(graph)?)
}
Expand Down Expand Up @@ -1218,7 +1226,7 @@ pub(super) fn problem_help_flag_name(
) -> String {
if field_type == "G" && is_geometry {
"positions".to_string()
} else if field_type == "DirectedGraph" {
} else if matches!(field_type, "MultiDigraph" | "SimpleDigraph") {
"arcs".to_string()
} else {
help_flag_name(field_name)
Expand Down
31 changes: 31 additions & 0 deletions problemreductions-cli/src/mcp/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,34 @@ fn test_solve_bundle_rejects_aggregate_only_path() {
let err = result.unwrap_err().to_string();
assert!(err.contains("witness"), "unexpected error: {err}");
}

#[test]
fn simple_graph_create_rejects_loops_and_parallel_edges() {
let server = McpServer::new();
for (edges, offending) in [
(serde_json::json!([[0, 0]]), "edge (0, 0)"),
(serde_json::json!([[0, 1], [0, 1]]), "edge (0, 1)"),
(serde_json::json!([[0, 1], [1, 0]]), "edge (1, 0)"),
] {
let error = server
.create_problem_inner(
"MaximumIndependentSet/SimpleGraph/One",
&serde_json::json!({"graph": edges, "num_vertices": 3}),
)
.unwrap_err();
assert!(format!("{error:#}").contains(offending), "{error:#}");
}
}

#[test]
fn mixed_chinese_postman_create_accepts_an_undirected_loop() {
let output = McpServer::new().create_problem_inner("MixedChinesePostman/i64", &serde_json::json!({"graph": [[0,0]], "arcs": [[0,0]], "num_vertices": 1, "edge_weights": [3], "arc_weights": [2]})).unwrap();
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
let problem: problemreductions::models::graph::MixedChinesePostman<i64> =
serde_json::from_value(json["data"].clone()).unwrap();
assert_eq!(problem.graph().edges(), vec![(0, 0)]);
assert_eq!(
problemreductions::Problem::evaluate(&problem, &vec![false]).unwrap(),
problemreductions::Min(Some(5))
);
}
Loading
Loading