Skip to content

Commit fbce255

Browse files
committed
Integrate graph type migration and update numeric cast fixtures
2 parents 85175e3 + 7126de6 commit fbce255

591 files changed

Lines changed: 2851 additions & 3748 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Max<V>, Min<V>, Sum<W>, Or, And, Extremum<V>, ExtremumSense
142142
- 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.
143143
- 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.
144144
- CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`)
145-
- Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph
145+
- Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph, SimpleDigraph, MultiDigraph, MultiMixedGraph
146146
- Weight types: `One` (unit weight marker), `i64`, `f64` — all implement `WeightElement` trait
147147
- `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type
148148
- Weight management via inherent methods (`weights()`, `set_weights()`, `is_weighted()`), not traits

‎docs/paper/reductions.typ‎

Lines changed: 54 additions & 54 deletions
Large diffs are not rendered by default.

‎docs/src/design.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,27 @@ nodes in the reduction graph, and explicit reduction rules connect them.
147147

148148
Variant types fall into three categories:
149149

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

154+
`SimpleGraph` is undirected, loop-free, and has no parallel edges: `(u, v)` and
155+
`(v, u)` are the same edge. Its fallible constructor, `new`, and serde share
156+
validation. `PlanarGraph` wraps it; `BipartiteGraph` also rejects repeated
157+
cross-partition pairs. `UnitDiskGraph`, `KingsSubgraph`, and `TriangularSubgraph`
158+
generate each edge between distinct vertex indices once, including when two
159+
vertices have coincident positions.
160+
161+
`SimpleDigraph` is loop-free and rejects repeated ordered arcs; `(u, v)`
162+
and `(v, u)` are distinct. It validates a wrapped `MultiDigraph` and
163+
delegates graph algorithms to it. `MultiDigraph` allows loops and parallel
164+
arcs; `arc_count(u, v)` exposes multiplicity. `MultiMixedGraph` allows loops and
165+
parallel arcs and undirected edges. In both multi types, arc i is entry i of
166+
`arcs()`; models index their per-arc data by that same order. All three types
167+
validate serde input and retain the JSON shapes `{num_vertices, arcs}` or
168+
`{num_vertices, arcs, edges}`.
169+
170+
154171
<div class="theme-light-only">
155172

156173
![Lattices](static/lattices.svg)

‎problemreductions-cli/src/bin/pred_sym.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ fn main() {
122122
})
123123
.collect();
124124

125-
// Check for unbound variables
126125
let expr_vars = parsed.variables();
127126
let bound_vars: std::collections::HashSet<&str> =
128127
bindings.iter().map(|(name, _)| name.as_str()).collect();

‎problemreductions-cli/src/commands/create.rs‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use problemreductions::models::graph::{LabelledArc, LabelledDigraph};
1111
use problemreductions::models::misc::{CbqRelation, FrequencyTable, KnownValue, QueryArg};
1212
use problemreductions::prelude::*;
1313
use problemreductions::topology::{
14-
DirectedGraph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph,
14+
KingsSubgraph, MultiDigraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph,
1515
};
1616
use serde::Serialize;
1717
use std::collections::{BTreeMap, BTreeSet};
@@ -353,9 +353,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
353353
return create_registered_random(args, canonical, &resolved_variant, out);
354354
}
355355

356-
// ILP and CircuitSAT have complex input structures
357-
// not suited for CLI flags. Check before the empty-flags help so they get a
358-
// clear message.
356+
// ILP and CircuitSAT have complex input structures not suited for CLI flags.
359357
if canonical == "ILP" || canonical == "CircuitSAT" {
360358
bail!(
361359
"CLI creation is not yet supported for {canonical}.\n\n\
@@ -478,15 +476,15 @@ fn parse_bool_rows(rows_str: &str) -> Result<Vec<Vec<bool>>> {
478476
Ok(matrix)
479477
}
480478

481-
/// Parse `--arcs` as directed arc pairs and build a `DirectedGraph`.
479+
/// Parse `--arcs` as directed arc pairs and build a `MultiDigraph`.
482480
///
483481
/// Returns `(graph, num_arcs)`. Infers vertex count from arc endpoints
484482
/// unless `num_vertices` is provided (which must be >= inferred count).
485483
/// E.g., "0>1,1>2,2>0"
486484
fn parse_directed_graph(
487485
arcs_str: &str,
488486
num_vertices: Option<usize>,
489-
) -> Result<(DirectedGraph, usize)> {
487+
) -> Result<(MultiDigraph, usize)> {
490488
let arcs: Vec<(usize, usize)> = arcs_str
491489
.split(',')
492490
.map(|pair| {
@@ -506,7 +504,11 @@ fn parse_directed_graph(
506504
.iter()
507505
.flat_map(|&(u, v)| [u, v])
508506
.max()
509-
.map(|m| m + 1)
507+
.map(|m| {
508+
m.checked_add(1)
509+
.ok_or_else(|| anyhow::anyhow!("vertex count overflows usize"))
510+
})
511+
.transpose()?
510512
.unwrap_or(0);
511513
let num_v = match num_vertices {
512514
Some(user_num_v) => {
@@ -522,7 +524,7 @@ fn parse_directed_graph(
522524
None => inferred_num_v,
523525
};
524526
let num_arcs = arcs.len();
525-
Ok((DirectedGraph::new(num_v, arcs), num_arcs))
527+
Ok((MultiDigraph::try_new(num_v, arcs)?, num_arcs))
526528
}
527529

528530
/// Parse implication rules from semicolon-separated "antecedents>consequent" strings.

‎problemreductions-cli/src/commands/create/schema_support.rs‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ impl CreateContext {
6161
.and_then(serde_json::Value::as_array)
6262
.map(Vec::len);
6363
}
64-
"DirectedGraph" => {
64+
"MultiDigraph" | "SimpleDigraph" => {
6565
self.num_vertices = value
6666
.get("num_vertices")
6767
.and_then(serde_json::Value::as_u64)
@@ -324,7 +324,7 @@ pub(crate) fn create_inputs_for(
324324
for field in schema.fields {
325325
let concrete_type = resolve_schema_field_type(field.type_name, resolved_variant);
326326
match concrete_type.as_str() {
327-
"DirectedGraph" => {
327+
"MultiDigraph" | "SimpleDigraph" => {
328328
insert_create_input(&mut inputs, "arcs", InputValueKind::Text, field.name);
329329
}
330330
_ => {
@@ -340,7 +340,10 @@ pub(crate) fn create_inputs_for(
340340
}
341341
if schema.fields.iter().any(|field| {
342342
let concrete_type = resolve_schema_field_type(field.type_name, resolved_variant);
343-
matches!(concrete_type.as_str(), "SimpleGraph" | "DirectedGraph")
343+
matches!(
344+
concrete_type.as_str(),
345+
"SimpleGraph" | "MultiDigraph" | "SimpleDigraph"
346+
)
344347
}) {
345348
insert_create_input(
346349
&mut inputs,
@@ -534,7 +537,7 @@ pub(super) fn parse_field_value(
534537
let normalized_type = normalize_type_name(concrete_type);
535538
let value = match normalized_type.as_str() {
536539
"SimpleGraph" => parse_simple_graph_value(raw, context)?,
537-
"DirectedGraph" => parse_directed_graph_value(raw, context)?,
540+
"MultiDigraph" | "SimpleDigraph" => parse_directed_graph_value(raw, context)?,
538541
"LabelledDigraph" => parse_labelled_digraph_value(raw, field_name)?,
539542
"KingsSubgraph" => parse_grid_subgraph_value(raw, true)?,
540543
"TriangularSubgraph" => parse_grid_subgraph_value(raw, false)?,
@@ -1106,7 +1109,12 @@ pub(super) fn parse_simple_graph_value(
11061109
.iter()
11071110
.flat_map(|&(u, v)| [u, v])
11081111
.max()
1109-
.map(|max_vertex| max_vertex + 1)
1112+
.map(|max_vertex| {
1113+
max_vertex
1114+
.checked_add(1)
1115+
.ok_or_else(|| anyhow::anyhow!("vertex count overflows usize"))
1116+
})
1117+
.transpose()?
11101118
.unwrap_or(0);
11111119
let num_vertices = match num_vertices {
11121120
Some(explicit) => {
@@ -1118,7 +1126,7 @@ pub(super) fn parse_simple_graph_value(
11181126
}
11191127
None => inferred_num_vertices,
11201128
};
1121-
SimpleGraph::new(num_vertices, edges)
1129+
SimpleGraph::try_new(num_vertices, edges)?
11221130
};
11231131
Ok(serde_json::to_value(graph)?)
11241132
}
@@ -1218,7 +1226,7 @@ pub(super) fn problem_help_flag_name(
12181226
) -> String {
12191227
if field_type == "G" && is_geometry {
12201228
"positions".to_string()
1221-
} else if field_type == "DirectedGraph" {
1229+
} else if matches!(field_type, "MultiDigraph" | "SimpleDigraph") {
12221230
"arcs".to_string()
12231231
} else {
12241232
help_flag_name(field_name)

‎problemreductions-cli/src/commands/create/tests.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,6 @@ fn test_create_staff_scheduling_reports_invalid_schedule_without_panic() {
778778
let result = std::panic::catch_unwind(|| create(&args, &out));
779779
assert!(result.is_ok(), "create should return an error, not panic");
780780
let err = result.unwrap().unwrap_err().to_string();
781-
// parse_bool_rows catches ragged rows before validate_staff_scheduling_args
782781
assert!(
783782
err.contains("All rows") || err.contains("schedule 1 has 6 periods, expected 7"),
784783
"expected row-length validation error, got: {err}"

‎problemreductions-cli/src/commands/graph.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1259,7 +1259,6 @@ pub fn neighbors(
12591259
TraversalFlow::Both => "both directions",
12601260
};
12611261

1262-
// Build tree structure via BFS with parent tracking
12631262
let tree = graph.k_neighbor_tree(&spec_name, &variant, max_hops, direction);
12641263

12651264
out.emit(

‎problemreductions-cli/src/commands/inspect.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ pub fn inspect(input: &Path, out: &OutputConfig) -> Result<()> {
1111
let content = read_input(input)?;
1212
let json: serde_json::Value = serde_json::from_str(&content)?;
1313

14-
// Detect if it's a bundle or a problem
1514
if json.get("source").is_some() && json.get("target").is_some() && json.get("path").is_some() {
1615
let bundle: ReductionBundle = serde_json::from_value(json)?;
1716
inspect_bundle(&bundle, out)

‎problemreductions-cli/src/mcp/tests.rs‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,6 @@ fn deterministic_solver_dispatch_mcp_output_is_repeatable_for_each_solver_class(
473473
fn test_solve_bundle() {
474474
let server = McpServer::new();
475475
let problem_json = create_test_mis(&server);
476-
// Reduce first, then solve the bundle
477476
let bundle_json = server
478477
.reduce_inner(
479478
&problem_json,
@@ -719,3 +718,34 @@ fn test_solve_bundle_rejects_aggregate_only_path() {
719718
let err = result.unwrap_err().to_string();
720719
assert!(err.contains("witness"), "unexpected error: {err}");
721720
}
721+
722+
#[test]
723+
fn simple_graph_create_rejects_loops_and_parallel_edges() {
724+
let server = McpServer::new();
725+
for (edges, offending) in [
726+
(serde_json::json!([[0, 0]]), "edge (0, 0)"),
727+
(serde_json::json!([[0, 1], [0, 1]]), "edge (0, 1)"),
728+
(serde_json::json!([[0, 1], [1, 0]]), "edge (1, 0)"),
729+
] {
730+
let error = server
731+
.create_problem_inner(
732+
"MaximumIndependentSet/SimpleGraph/One",
733+
&serde_json::json!({"graph": edges, "num_vertices": 3}),
734+
)
735+
.unwrap_err();
736+
assert!(format!("{error:#}").contains(offending), "{error:#}");
737+
}
738+
}
739+
740+
#[test]
741+
fn mixed_chinese_postman_create_accepts_an_undirected_loop() {
742+
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();
743+
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
744+
let problem: problemreductions::models::graph::MixedChinesePostman<i64> =
745+
serde_json::from_value(json["data"].clone()).unwrap();
746+
assert_eq!(problem.graph().edges(), vec![(0, 0)]);
747+
assert_eq!(
748+
problemreductions::Problem::evaluate(&problem, &vec![false]).unwrap(),
749+
problemreductions::Min(Some(5))
750+
);
751+
}

0 commit comments

Comments
 (0)