From 70ea1725be4102055e143104d0e29e899fb201e6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 01:01:42 +0800 Subject: [PATCH 1/6] Add plan for #290: [Model] IntegralFlowWithMultipliers --- ...26-03-22-integral-flow-with-multipliers.md | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 docs/plans/2026-03-22-integral-flow-with-multipliers.md diff --git a/docs/plans/2026-03-22-integral-flow-with-multipliers.md b/docs/plans/2026-03-22-integral-flow-with-multipliers.md new file mode 100644 index 000000000..11ed13fc8 --- /dev/null +++ b/docs/plans/2026-03-22-integral-flow-with-multipliers.md @@ -0,0 +1,284 @@ +# IntegralFlowWithMultipliers Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `IntegralFlowWithMultipliers` graph model, including registry metadata, CLI creation/inspection support, canonical example coverage, and a paper entry, for issue `#290`. + +**Architecture:** Implement this as a fixed-variant satisfaction model over `DirectedGraph` with one integer variable per arc. Feasibility checks enforce capacity bounds, multiplier-scaled conservation at non-terminal vertices, and sink net inflow `>= requirement`; metadata and examples follow the issue/comment decisions (`u64` data, no misleading generalized-flow alias, concrete complexity `(max_capacity + 1)^num_arcs`). Execute the paper work in a separate batch after the Rust/example-db work is complete so the paper can load the final canonical example data. + +**Tech Stack:** Rust workspace, registry inventory metadata, Clap CLI, example-db, Typst paper, `make` verification targets. + +--- + +## Issue Context + +- Issue: `#290` `[Model] IntegralFlowWithMultipliers` +- Pipeline state: Ready -> claimed to In progress by `run-pipeline` +- `issue-context` result: `Good` label present, action = `create-pr`, no PR to resume +- Companion rule issue exists: `#363` `[Rule] PARTITION to INTEGRAL FLOW WITH MULTIPLIERS` +- Use the repaired issue body/comment decisions as source of truth: + - store `multipliers`, `capacities`, `requirement` as `u64` + - `multipliers.len() == num_vertices`, with source/sink entries ignored + - no `Generalized Flow` alias + - use complexity string `"(max_capacity + 1)^num_arcs"` + - use the cleaned YES instance as the canonical worked example + - keep the repaired diamond-graph NO instance in tests only + +## Batch Layout + +- **Batch 1:** add-model Steps 1-5.5 + - Rust model, registry wiring, CLI creation/help, canonical example, non-paper tests +- **Batch 2:** add-model Step 6 + - `docs/paper/reductions.typ` entry + paper/example verification + +## Reference Files + +- Model pattern: `src/models/graph/directed_two_commodity_integral_flow.rs` +- Metadata/size-field pattern: `src/models/graph/undirected_two_commodity_integral_flow.rs` +- Trait smoke coverage: `src/unit_tests/trait_consistency.rs` +- CLI creation pattern: `problemreductions-cli/src/commands/create.rs` +- Paper pattern: `docs/paper/reductions.typ` entries for `DirectedTwoCommodityIntegralFlow` and `MaximumIndependentSet` + +## Batch 1 + +### Task 1: Add the failing model/unit tests first + +**Files:** +- Create: `src/unit_tests/models/graph/integral_flow_with_multipliers.rs` + +**Step 1: Write the failing tests** + +Add tests that cover: +- creation/accessors/dims/size getters +- a satisfying assignment for the repaired YES instance +- an unsatisfying assignment for the repaired NO instance +- multiplier-scaled conservation failure +- sink net-inflow check uses `>= requirement` +- wrong config length returns `false` +- serde round-trip +- brute-force solver finds a satisfying config for the YES instance and none for the NO instance + +Also add a paper-example-oriented test scaffold that can be finalized once the canonical example is wired. + +**Step 2: Run the focused tests and confirm they fail** + +Run: + +```bash +cargo test integral_flow_with_multipliers --lib +``` + +Expected: compilation/test failures because the model type and module do not exist yet. + +**Step 3: Commit the red state only if it is helpful** + +Optional; skip if the branch policy prefers keeping the red state local. + +### Task 2: Implement the Rust model and registry wiring + +**Files:** +- Create: `src/models/graph/integral_flow_with_multipliers.rs` +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Modify: `src/unit_tests/trait_consistency.rs` + +**Step 1: Implement the model with the smallest code that satisfies Task 1** + +Add `IntegralFlowWithMultipliers` with: +- `ProblemSchemaEntry` metadata +- `ProblemSizeFieldEntry` declaring at least `num_vertices`, `num_arcs`, `max_capacity`, and `requirement` +- fields: `graph`, `source`, `sink`, `multipliers`, `capacities`, `requirement` +- constructor validation: + - `capacities.len() == graph.num_arcs()` + - `multipliers.len() == graph.num_vertices()` + - `source`/`sink` in bounds and distinct + - non-terminal multipliers are positive + - each capacity fits into `usize` for `dims()` +- accessors/getters: `graph()`, `capacities()`, `multipliers()`, `source()`, `sink()`, `requirement()`, `num_vertices()`, `num_arcs()`, `max_capacity()` +- feasibility helper using `i128` balance accumulation per vertex + - enforce `0 <= f(a) <= c(a)` implicitly from `dims()`/config decoding + - for each non-terminal `v`, require `h(v) * inflow(v) == outflow(v)` + - require sink net inflow `incoming - outgoing >= requirement` +- `Problem` impl: + - `NAME = "IntegralFlowWithMultipliers"` + - `Metric = bool` + - `variant() = variant_params![]` + - `dims() = capacities.iter().map(|c| c + 1)` + - `evaluate()` delegates to feasibility +- `SatisfactionProblem` impl +- `declare_variants! { default sat IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs", }` +- canonical example spec using the repaired YES instance from the issue +- test module link at the bottom + +Wire the new model into: +- `src/models/graph/mod.rs` docs/mod exports/example-spec chain +- `src/models/mod.rs` +- `src/lib.rs` prelude exports +- `src/unit_tests/trait_consistency.rs` + +**Step 2: Run the focused library tests and make them green** + +Run: + +```bash +cargo test integral_flow_with_multipliers --lib +cargo test trait_consistency --lib +``` + +Expected: the new model tests and trait smoke test pass. + +**Step 3: Refactor only after green** + +Keep helper methods local to the model file; do not generalize flow utilities prematurely. + +### Task 3: Add CLI creation/help coverage and example-db wiring + +**Files:** +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/tests/cli_tests.rs` + +**Step 1: Write/extend failing CLI tests first** + +Add tests that cover: +- `pred create IntegralFlowWithMultipliers` with `--arcs`, `--capacities`, `--multipliers`, `--source`, `--sink`, `--requirement` +- `pred inspect`/`pred show` exposing the new size fields and schema fields +- error cases for missing `--multipliers` or wrong multiplier/capacity lengths +- `pred create --example IntegralFlowWithMultipliers` returning the canonical JSON shape + +**Step 2: Run the focused CLI tests and confirm they fail** + +Run: + +```bash +cargo test -p problemreductions-cli integral_flow_with_multipliers +``` + +Expected: failures because the CLI does not yet know the problem/flags. + +**Step 3: Implement the minimal CLI support** + +Add: +- new `CreateArgs` field for `--multipliers` +- new `CreateArgs` field for singular `--requirement` +- `all_data_flags_empty()` coverage for both new fields +- after-help table/examples entry in `problemreductions-cli/src/cli.rs` +- `example_for()` entry in `problemreductions-cli/src/commands/create.rs` +- create-arm in `problemreductions-cli/src/commands/create.rs` using `parse_directed_graph(...)` + - require `--arcs` + - parse `--capacities` (default all ones if omitted only if that matches existing CLI norms; otherwise require explicitly) + - require `--multipliers`, `--source`, `--sink`, `--requirement` + - validate vector lengths and vertex bounds + - construct `IntegralFlowWithMultipliers::new(...)` + +If the registry alias machinery already handles the canonical name, do **not** add a made-up short alias. + +**Step 4: Run the focused CLI tests and make them green** + +Run: + +```bash +cargo test -p problemreductions-cli integral_flow_with_multipliers +``` + +### Task 4: Run the Batch 1 verification set + +**Files:** +- None beyond the files above + +**Step 1: Run the verification commands** + +Run: + +```bash +cargo test integral_flow_with_multipliers +make fmt +make clippy +``` + +If `make clippy` is too broad while iterating, use targeted `cargo clippy --all-targets --all-features -- -D warnings` and finish with the repo target before closing Batch 1. + +**Step 2: Commit the Batch 1 implementation** + +Suggested message: + +```bash +git add src/models/graph/integral_flow_with_multipliers.rs \ + src/models/graph/mod.rs src/models/mod.rs src/lib.rs \ + src/unit_tests/models/graph/integral_flow_with_multipliers.rs \ + src/unit_tests/trait_consistency.rs \ + problemreductions-cli/src/cli.rs \ + problemreductions-cli/src/commands/create.rs \ + problemreductions-cli/tests/cli_tests.rs +git commit -m "Add IntegralFlowWithMultipliers model" +``` + +## Batch 2 + +### Task 5: Add the paper entry and paper-example validation + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `src/unit_tests/models/graph/integral_flow_with_multipliers.rs` + +**Step 1: Write the failing paper/example test first** + +Complete the paper-example test in the model unit test file so it: +- builds the canonical YES instance +- evaluates the documented satisfying config +- confirms the brute-force solver finds at least one satisfying config + +**Step 2: Run the focused test and confirm red if needed** + +Run: + +```bash +cargo test integral_flow_with_multipliers_paper_example --lib +``` + +Expected: failure until the final documented example/config is aligned. + +**Step 3: Implement the paper entry** + +In `docs/paper/reductions.typ`: +- add display name entry for `IntegralFlowWithMultipliers` +- add `problem-def("IntegralFlowWithMultipliers")` +- use the issue-approved formulation: directed graph, vertex multipliers on non-terminals, sink requirement `>= R` +- cite the Sahni 1974 and Jewell 1962 references +- explain the polynomial special case when all multipliers are 1 / continuous-flow relaxation +- use the canonical YES example from the issue and load it from example-db +- include a small directed-network figure and `pred-commands()` snippet derived from the canonical example data + +**Step 4: Run the paper/example verification** + +Run: + +```bash +cargo test integral_flow_with_multipliers_paper_example --lib +make paper +``` + +**Step 5: Commit the paper batch** + +Suggested message: + +```bash +git add docs/paper/reductions.typ src/unit_tests/models/graph/integral_flow_with_multipliers.rs +git commit -m "Document IntegralFlowWithMultipliers" +``` + +## Final Verification + +Run before cleanup/push: + +```bash +make check +git status --short +``` + +Success criteria: +- new model is discoverable through the registry/CLI +- `pred create IntegralFlowWithMultipliers ...` works +- canonical example exists and is used by tests/paper +- the plan file can be deleted before the final push, per `issue-to-pr` From b7cade1dac27d9f5aa73983f0ee438696b3fcf34 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 01:15:18 +0800 Subject: [PATCH 2/6] Add IntegralFlowWithMultipliers model --- problemreductions-cli/src/cli.rs | 8 + problemreductions-cli/src/commands/create.rs | 100 ++++++- problemreductions-cli/tests/cli_tests.rs | 147 ++++++++++ src/lib.rs | 6 +- .../graph/integral_flow_with_multipliers.rs | 257 ++++++++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 2 +- .../graph/integral_flow_with_multipliers.rs | 139 ++++++++++ 8 files changed, 658 insertions(+), 5 deletions(-) create mode 100644 src/models/graph/integral_flow_with_multipliers.rs create mode 100644 src/unit_tests/models/graph/integral_flow_with_multipliers.rs diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index f10cb96b4..72a154822 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -229,6 +229,7 @@ Flags by problem type: PartitionIntoTriangles --graph GraphPartitioning --graph GeneralizedHex --graph, --source, --sink + IntegralFlowWithMultipliers --arcs, --capacities, --source, --sink, --multipliers, --requirement MinimumCutIntoBoundedSets --graph, --edge-weights, --source, --sink, --size-bound, --cut-bound HamiltonianCircuit, HC --graph BoundedComponentSpanningForest --graph, --weights, --k, --bound @@ -312,6 +313,7 @@ Examples: pred create SAT --num-vars 3 --clauses \"1,2;-1,3\" pred create QUBO --matrix \"1,0.5;0.5,2\" pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5 + pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2 pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10 pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\" @@ -355,12 +357,18 @@ pub struct CreateArgs { /// Edge capacities for multicommodity flow problems (e.g., 1,1,2) #[arg(long)] pub capacities: Option, + /// Vertex multipliers in vertex order (e.g., 1,2,3,1) + #[arg(long)] + pub multipliers: Option, /// Source vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub source: Option, /// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub sink: Option, + /// Required sink inflow for IntegralFlowWithMultipliers + #[arg(long)] + pub requirement: Option, /// Required number of paths for LengthBoundedDisjointPaths #[arg(long)] pub num_paths_required: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 955aa441f..4225f7474 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -49,8 +49,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.edge_weights.is_none() && args.edge_lengths.is_none() && args.capacities.is_none() + && args.multipliers.is_none() && args.source.is_none() && args.sink.is_none() + && args.requirement.is_none() && args.num_paths_required.is_none() && args.couplings.is_none() && args.fields.is_none() @@ -512,6 +514,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3", "GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3", "GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5", + "IntegralFlowWithMultipliers" => { + "--arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2" + } "MinimumCutIntoBoundedSets" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 3 --size-bound 3 --cut-bound 1" } @@ -1093,6 +1098,95 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // IntegralFlowWithMultipliers (directed arcs + capacities + source/sink + multipliers + requirement) + "IntegralFlowWithMultipliers" => { + let usage = "Usage: pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --arcs\n\n{usage}") + })?; + let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let capacities_str = args.capacities.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --capacities\n\n{usage}") + })?; + let capacities: Vec = util::parse_comma_list(capacities_str) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + if capacities.len() != num_arcs { + bail!( + "Expected {} capacities but got {}\n\n{}", + num_arcs, + capacities.len(), + usage + ); + } + for (arc_index, &capacity) in capacities.iter().enumerate() { + let fits = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(); + if !fits { + bail!( + "capacity {} at arc index {} is too large for this platform\n\n{}", + capacity, + arc_index, + usage + ); + } + } + + let num_vertices = graph.num_vertices(); + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --sink\n\n{usage}") + })?; + validate_vertex_index("source", source, num_vertices, usage)?; + validate_vertex_index("sink", sink, num_vertices, usage)?; + if source == sink { + bail!( + "IntegralFlowWithMultipliers requires distinct --source and --sink\n\n{}", + usage + ); + } + + let multipliers_str = args.multipliers.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --multipliers\n\n{usage}") + })?; + let multipliers: Vec = util::parse_comma_list(multipliers_str) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + if multipliers.len() != num_vertices { + bail!( + "Expected {} multipliers but got {}\n\n{}", + num_vertices, + multipliers.len(), + usage + ); + } + if multipliers + .iter() + .enumerate() + .any(|(vertex, &multiplier)| vertex != source && vertex != sink && multiplier == 0) + { + bail!("non-terminal multipliers must be positive\n\n{usage}"); + } + + let requirement = args.requirement.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --requirement\n\n{usage}") + })?; + ( + ser(IntegralFlowWithMultipliers::new( + graph, + source, + sink, + multipliers, + capacities, + requirement, + ))?, + resolved_variant.clone(), + ) + } + // Minimum cut into bounded sets (graph + edge weights + s/t/B/K) "MinimumCutIntoBoundedSets" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -1469,7 +1563,9 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { })?; let edge_weights = parse_edge_weights(args, graph.num_edges())?; let data = match canonical { - "BottleneckTravelingSalesman" => ser(BottleneckTravelingSalesman::new(graph, edge_weights))?, + "BottleneckTravelingSalesman" => { + ser(BottleneckTravelingSalesman::new(graph, edge_weights))? + } "MaxCut" => ser(MaxCut::new(graph, edge_weights))?, "MaximumMatching" => ser(MaximumMatching::new(graph, edge_weights))?, "TravelingSalesman" => ser(TravelingSalesman::new(graph, edge_weights))?, @@ -5809,8 +5905,10 @@ mod tests { edge_weights: None, edge_lengths: None, capacities: None, + multipliers: None, source: None, sink: None, + requirement: None, num_paths_required: None, couplings: None, fields: None, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 6388acdd2..a73c7195d 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -656,6 +656,101 @@ fn test_create_undirected_two_commodity_integral_flow_rejects_out_of_range_termi assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } +#[test] +fn test_create_integral_flow_with_multipliers() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,0>3,0>4,0>5,0>6,1>7,2>7,3>7,4>7,5>7,6>7", + "--capacities", + "1,1,1,1,1,1,2,3,4,5,6,4", + "--source", + "0", + "--sink", + "7", + "--multipliers", + "1,2,3,4,5,6,4,1", + "--requirement", + "12", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "IntegralFlowWithMultipliers"); + assert_eq!(json["variant"], serde_json::json!({})); + assert_eq!(json["data"]["source"], 0); + assert_eq!(json["data"]["sink"], 7); + assert_eq!(json["data"]["requirement"], 12); + assert_eq!( + json["data"]["multipliers"], + serde_json::json!([1, 2, 3, 4, 5, 6, 4, 1]) + ); + assert_eq!( + json["data"]["capacities"], + serde_json::json!([1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4]) + ); +} + +#[test] +fn test_create_integral_flow_with_multipliers_missing_multipliers_shows_usage() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,1>3,2>3", + "--capacities", + "1,1,2,2", + "--source", + "0", + "--sink", + "3", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("requires --multipliers")); + assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); +} + +#[test] +fn test_create_integral_flow_with_multipliers_rejects_wrong_multiplier_count() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,1>3,2>3", + "--capacities", + "1,1,2,2", + "--source", + "0", + "--sink", + "3", + "--multipliers", + "1,2,3", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Expected 4 multipliers but got 3")); + assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); +} + #[test] fn test_create_consecutive_block_minimization_rejects_ragged_matrix() { let output = pred() @@ -5588,6 +5683,58 @@ fn test_inspect_undirected_two_commodity_integral_flow_reports_size_fields() { std::fs::remove_file(&result_file).ok(); } +#[test] +fn test_inspect_integral_flow_with_multipliers_reports_size_fields() { + let problem_file = std::env::temp_dir().join("pred_test_ifwm_inspect_in.json"); + let result_file = std::env::temp_dir().join("pred_test_ifwm_inspect_out.json"); + let create_out = pred() + .args([ + "-o", + problem_file.to_str().unwrap(), + "create", + "--example", + "IntegralFlowWithMultipliers", + ]) + .output() + .unwrap(); + assert!( + create_out.status.success(), + "stderr: {}", + String::from_utf8_lossy(&create_out.stderr) + ); + + let output = pred() + .args([ + "-o", + result_file.to_str().unwrap(), + "inspect", + problem_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let content = std::fs::read_to_string(&result_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + let size_fields: Vec<&str> = json["size_fields"] + .as_array() + .expect("size_fields should be an array") + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert!(size_fields.contains(&"num_vertices")); + assert!(size_fields.contains(&"num_arcs")); + assert!(size_fields.contains(&"max_capacity")); + assert!(size_fields.contains(&"requirement")); + + std::fs::remove_file(&problem_file).ok(); + std::fs::remove_file(&result_file).ok(); +} + #[test] fn test_inspect_acyclic_partition_reports_size_fields() { let problem_file = std::env::temp_dir().join("pred_test_acyclic_partition_inspect_in.json"); diff --git a/src/lib.rs b/src/lib.rs index 5d7f10b32..96dc4d897 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,9 +51,9 @@ pub mod prelude { AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, BottleneckTravelingSalesman, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GeneralizedHex, GraphPartitioning, HamiltonianCircuit, - HamiltonianPath, IsomorphicSpanningTree, KClique, KthBestSpanningTree, - LengthBoundedDisjointPaths, MixedChinesePostman, SpinGlass, SteinerTree, - StrongConnectivityAugmentation, SubgraphIsomorphism, + HamiltonianPath, IntegralFlowWithMultipliers, IsomorphicSpanningTree, KClique, + KthBestSpanningTree, LengthBoundedDisjointPaths, MixedChinesePostman, SpinGlass, + SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs new file mode 100644 index 000000000..4f25d731e --- /dev/null +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -0,0 +1,257 @@ +//! Integral Flow With Multipliers problem implementation. +//! +//! Given a directed graph with arc capacities, vertex multipliers on +//! non-terminals, and a sink demand, determine whether there exists an +//! integral flow satisfying multiplier-scaled conservation. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; +use crate::topology::DirectedGraph; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "IntegralFlowWithMultipliers", + display_name: "Integral Flow With Multipliers", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Integral flow feasibility on a directed graph with multiplier-scaled conservation at non-terminal vertices", + fields: &[ + FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed graph G = (V, A)" }, + FieldInfo { name: "source", type_name: "usize", description: "Source vertex s" }, + FieldInfo { name: "sink", type_name: "usize", description: "Sink vertex t" }, + FieldInfo { name: "multipliers", type_name: "Vec", description: "Vertex multipliers h(v) in vertex order; source/sink entries are ignored" }, + FieldInfo { name: "capacities", type_name: "Vec", description: "Arc capacities c(a) in graph arc order" }, + FieldInfo { name: "requirement", type_name: "u64", description: "Required net inflow R at the sink" }, + ], + } +} + +inventory::submit! { + ProblemSizeFieldEntry { + name: "IntegralFlowWithMultipliers", + fields: &["num_vertices", "num_arcs", "max_capacity", "requirement"], + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegralFlowWithMultipliers { + graph: DirectedGraph, + source: usize, + sink: usize, + multipliers: Vec, + capacities: Vec, + requirement: u64, +} + +impl IntegralFlowWithMultipliers { + pub fn new( + graph: DirectedGraph, + source: usize, + sink: usize, + multipliers: Vec, + capacities: Vec, + requirement: u64, + ) -> Self { + assert_eq!( + capacities.len(), + graph.num_arcs(), + "capacities length must match graph num_arcs" + ); + assert_eq!( + multipliers.len(), + graph.num_vertices(), + "multipliers length must match graph num_vertices" + ); + + let num_vertices = graph.num_vertices(); + assert!( + source < num_vertices, + "source ({source}) must be less than num_vertices ({num_vertices})" + ); + assert!( + sink < num_vertices, + "sink ({sink}) must be less than num_vertices ({num_vertices})" + ); + assert_ne!(source, sink, "source and sink must be distinct"); + + for (vertex, &multiplier) in multipliers.iter().enumerate() { + if vertex != source && vertex != sink { + assert!(multiplier > 0, "non-terminal multipliers must be positive"); + } + } + + for &capacity in &capacities { + let domain = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)); + assert!( + domain.is_some(), + "arc capacities must fit into usize for dims()" + ); + } + + Self { + graph, + source, + sink, + multipliers, + capacities, + requirement, + } + } + + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + pub fn source(&self) -> usize { + self.source + } + + pub fn sink(&self) -> usize { + self.sink + } + + pub fn multipliers(&self) -> &[u64] { + &self.multipliers + } + + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + pub fn requirement(&self) -> u64 { + self.requirement + } + + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + pub fn num_arcs(&self) -> usize { + self.graph.num_arcs() + } + + pub fn max_capacity(&self) -> u64 { + self.capacities.iter().copied().max().unwrap_or(0) + } + + fn domain_size(capacity: u64) -> usize { + usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .expect("capacity already validated to fit into usize") + } + + pub fn is_feasible(&self, config: &[usize]) -> bool { + if config.len() != self.num_arcs() { + return false; + } + + let num_vertices = self.num_vertices(); + let mut inflow = vec![0_i128; num_vertices]; + let mut outflow = vec![0_i128; num_vertices]; + + for (arc_index, ((u, v), &capacity)) in self + .graph + .arcs() + .into_iter() + .zip(self.capacities.iter()) + .enumerate() + { + let Some(flow_usize) = config.get(arc_index).copied() else { + return false; + }; + let Ok(flow_u64) = u64::try_from(flow_usize) else { + return false; + }; + if flow_u64 > capacity { + return false; + } + let flow = i128::from(flow_u64); + outflow[u] += flow; + inflow[v] += flow; + } + + for vertex in 0..num_vertices { + if vertex == self.source || vertex == self.sink { + continue; + } + let multiplier = i128::from(self.multipliers[vertex]); + let Some(expected_outflow) = inflow[vertex].checked_mul(multiplier) else { + return false; + }; + if expected_outflow != outflow[vertex] { + return false; + } + } + + let sink_net_flow = inflow[self.sink] - outflow[self.sink]; + sink_net_flow >= i128::from(self.requirement) + } +} + +impl Problem for IntegralFlowWithMultipliers { + const NAME: &'static str = "IntegralFlowWithMultipliers"; + type Metric = bool; + + fn dims(&self) -> Vec { + self.capacities + .iter() + .map(|&capacity| Self::domain_size(capacity)) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.is_feasible(config) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for IntegralFlowWithMultipliers {} + +crate::declare_variants! { + default sat IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "integral_flow_with_multipliers", + instance: Box::new(IntegralFlowWithMultipliers::new( + DirectedGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (0, 6), + (1, 7), + (2, 7), + (3, 7), + (4, 7), + (5, 7), + (6, 7), + ], + ), + 0, + 7, + vec![1, 2, 3, 4, 5, 6, 4, 1], + vec![1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4], + 12, + )), + optimal_config: vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/integral_flow_with_multipliers.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index dbdb632f8..4da017f6a 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -42,6 +42,7 @@ //! - [`SteinerTree`]: Minimum-weight tree spanning all required terminals //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) //! - [`DirectedTwoCommodityIntegralFlow`]: Directed two-commodity integral flow (satisfaction) +//! - [`IntegralFlowWithMultipliers`]: Integral flow with vertex multipliers on a directed graph //! - [`UndirectedTwoCommodityIntegralFlow`]: Two-commodity integral flow on undirected graphs //! - [`StrongConnectivityAugmentation`]: Strong connectivity augmentation with weighted candidate arcs @@ -56,6 +57,7 @@ pub(crate) mod generalized_hex; pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_circuit; pub(crate) mod hamiltonian_path; +pub(crate) mod integral_flow_with_multipliers; pub(crate) mod isomorphic_spanning_tree; pub(crate) mod kclique; pub(crate) mod kcoloring; @@ -101,6 +103,7 @@ pub use generalized_hex::GeneralizedHex; pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_circuit::HamiltonianCircuit; pub use hamiltonian_path::HamiltonianPath; +pub use integral_flow_with_multipliers::IntegralFlowWithMultipliers; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kclique::KClique; pub use kcoloring::KColoring; @@ -145,6 +148,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec IntegralFlowWithMultipliers { + let graph = DirectedGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (0, 6), + (1, 7), + (2, 7), + (3, 7), + (4, 7), + (5, 7), + (6, 7), + ], + ); + IntegralFlowWithMultipliers::new( + graph, + 0, + 7, + vec![1, 2, 3, 4, 5, 6, 4, 1], + vec![1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4], + 12, + ) +} + +fn yes_config() -> Vec { + vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0] +} + +fn no_instance() -> IntegralFlowWithMultipliers { + let graph = DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2)]); + IntegralFlowWithMultipliers::new(graph, 0, 3, vec![1, 2, 3, 1], vec![2, 1, 2, 5, 1], 7) +} + +#[test] +fn test_integral_flow_with_multipliers_creation_accessors_and_dimensions() { + let problem = yes_instance(); + assert_eq!(problem.graph().num_vertices(), 8); + assert_eq!(problem.num_arcs(), 12); + assert_eq!(problem.source(), 0); + assert_eq!(problem.sink(), 7); + assert_eq!(problem.requirement(), 12); + assert_eq!(problem.max_capacity(), 6); + assert_eq!(problem.multipliers(), &[1, 2, 3, 4, 5, 6, 4, 1]); + assert_eq!(problem.capacities(), &[1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4]); + assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 5]); +} + +#[test] +fn test_integral_flow_with_multipliers_evaluate_yes_instance() { + assert!(yes_instance().evaluate(&yes_config())); +} + +#[test] +fn test_integral_flow_with_multipliers_evaluate_no_instance() { + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&no_instance()).is_none()); +} + +#[test] +fn test_integral_flow_with_multipliers_rejects_multiplier_conservation_violation() { + let config = vec![1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; + assert!(!yes_instance().evaluate(&config)); +} + +#[test] +fn test_integral_flow_with_multipliers_sink_requirement_is_at_least() { + let config = vec![0, 0, 1, 1, 1, 0, 0, 0, 4, 5, 6, 0]; + assert!(yes_instance().evaluate(&config)); +} + +#[test] +fn test_integral_flow_with_multipliers_rejects_wrong_config_length() { + let problem = yes_instance(); + assert!(!problem.evaluate(&[0; 11])); + assert!(!problem.evaluate(&[0; 13])); + assert!(!problem.evaluate(&[])); +} + +#[test] +fn test_integral_flow_with_multipliers_serialization_round_trip() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let decoded: IntegralFlowWithMultipliers = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.source(), problem.source()); + assert_eq!(decoded.sink(), problem.sink()); + assert_eq!(decoded.requirement(), problem.requirement()); + assert_eq!(decoded.multipliers(), problem.multipliers()); + assert_eq!(decoded.capacities(), problem.capacities()); +} + +#[test] +fn test_integral_flow_with_multipliers_solver_yes_instance() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem).unwrap(); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_integral_flow_with_multipliers_problem_name_and_size_fields() { + assert_eq!( + ::NAME, + "IntegralFlowWithMultipliers" + ); + let fields: HashSet<&'static str> = declared_size_fields("IntegralFlowWithMultipliers") + .into_iter() + .collect(); + assert_eq!( + fields, + HashSet::from(["max_capacity", "num_arcs", "num_vertices", "requirement"]) + ); +} + +#[cfg(feature = "example-db")] +#[test] +fn test_integral_flow_with_multipliers_canonical_example_spec() { + let specs = canonical_model_example_specs(); + assert_eq!(specs.len(), 1); + let spec = &specs[0]; + assert_eq!(spec.id, "integral_flow_with_multipliers"); + assert_eq!(spec.optimal_config, yes_config()); + assert_eq!(spec.optimal_value, serde_json::json!(true)); +} + +#[test] +fn test_integral_flow_with_multipliers_paper_example() { + assert!(yes_instance().evaluate(&yes_config())); +} From 4feeb5522de3ec59db88858da179bccf95e32216 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 01:27:37 +0800 Subject: [PATCH 3/6] Polish IntegralFlowWithMultipliers paper and tests --- docs/paper/reductions.typ | 66 +++++++++++++++++++ docs/paper/references.bib | 21 ++++++ problemreductions-cli/tests/cli_tests.rs | 54 +++++++++++++++ .../graph/integral_flow_with_multipliers.rs | 12 +++- 4 files changed, 152 insertions(+), 1 deletion(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 3e8213c9d..8ab9da4bc 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -130,6 +130,7 @@ "ConsecutiveBlockMinimization": [Consecutive Block Minimization], "ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], + "IntegralFlowWithMultipliers": [Integral Flow With Multipliers], "MinMaxMulticenter": [Min-Max Multicenter], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], @@ -5176,6 +5177,71 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ) ] +#{ + let x = load-model-example("IntegralFlowWithMultipliers") + let config = x.optimal_config + [ + #problem-def("IntegralFlowWithMultipliers")[ + Given a directed graph $G = (V, A)$, distinguished vertices $s, t in V$, arc capacities $c: A -> ZZ^+$, vertex multipliers $h: V backslash {s, t} -> ZZ^+$, and a requirement $R in ZZ^+$, determine whether there exists an integral flow function $f: A -> ZZ_(>= 0)$ such that (1) $f(a) <= c(a)$ for every $a in A$, (2) for each nonterminal vertex $v in V backslash {s, t}$, the value $h(v)$ times the total inflow into $v$ equals the total outflow from $v$, and (3) the net flow into $t$ is at least $R$. + ][ + Integral Flow With Multipliers is Garey and Johnson's gain/loss network problem ND33 @garey1979. Sahni includes the same integral vertex-multiplier formulation among his computationally related problems, where partition-style reductions show that adding discrete gain factors destroys the ordinary max-flow structure @sahni1974. The key wrinkle is that conservation is no longer symmetric: one unit entering a vertex may force several units to leave, so the feasible integral solutions behave more like multiplicative gadgets than classical flow balances. + + When every multiplier equals $1$, the model collapses to ordinary single-commodity max flow and becomes polynomial-time solvable by the standard network-flow machinery summarized in Garey and Johnson @garey1979. Jewell studies a different continuous flow-with-gains model in which gain factors live on arcs and the flow may be fractional @jewell1962. That continuous relaxation remains polynomially tractable, so it should not be conflated with the NP-complete integral vertex-multiplier decision problem catalogued here. In this implementation the witness stores one bounded integer variable per arc, giving the direct exact-search bound $O((C + 1)^m)$ where $m = |A|$ and $C = max_(a in A) c(a)$. + + *Example.* The canonical fixture encodes the Partition multiset ${2, 3, 4, 5, 6, 4}$ using source $s = v_0$, sink $t = v_7$, six unit-capacity arcs out of $s$, six sink arcs with capacities $(2, 3, 4, 5, 6, 4)$, and multipliers $(2, 3, 4, 5, 6, 4)$ on the intermediate vertices. Setting the source arcs to $v_1$, $v_3$, and $v_5$ to $1$ forces outgoing sink arcs of $2$, $4$, and $6$, respectively. The sink therefore receives net inflow $2 + 4 + 6 = 12$, exactly meeting the requirement $R = 12$. + + #pred-commands( + "pred create --example IntegralFlowWithMultipliers -o integral-flow-with-multipliers.json", + "pred solve integral-flow-with-multipliers.json", + "pred evaluate integral-flow-with-multipliers.json --config " + config.map(str).join(","), + ) + + #figure( + canvas(length: 0.9cm, { + import draw: * + let blue = graph-colors.at(0) + let gray = luma(180) + let source = (0, 0) + let sink = (6, 0) + let mids = ( + (2.4, 2.5), + (2.4, 1.5), + (2.4, 0.5), + (2.4, -0.5), + (2.4, -1.5), + (2.4, -2.5), + ) + let labels = ( + [$v_1, h = 2$], + [$v_2, h = 3$], + [$v_3, h = 4$], + [$v_4, h = 5$], + [$v_5, h = 6$], + [$v_6, h = 4$], + ) + let active = (0, 2, 4) + + for (i, pos) in mids.enumerate() { + let chosen = active.contains(i) + let color = if chosen { blue } else { gray } + let thickness = if chosen { 1.3pt } else { 0.6pt } + line(source, pos, stroke: (paint: color, thickness: thickness), mark: (end: "straight", scale: 0.45)) + line(pos, sink, stroke: (paint: color, thickness: thickness), mark: (end: "straight", scale: 0.45)) + circle(pos, radius: 0.22, fill: if chosen { blue.lighten(75%) } else { white }, stroke: 0.6pt) + content((pos.at(0) + 0.85, pos.at(1)), text(6.5pt, labels.at(i))) + } + + circle(source, radius: 0.24, fill: blue.lighten(75%), stroke: 0.6pt) + circle(sink, radius: 0.24, fill: blue.lighten(75%), stroke: 0.6pt) + content(source, text(7pt, [$s = v_0$])) + content(sink, text(7pt, [$t = v_7$])) + }), + caption: [Integral Flow With Multipliers: the blue branches send one unit from $s$ into $v_1$, $v_3$, and $v_5$, forcing sink inflow $2 + 4 + 6 = 12$ at $t$.], + ) + ] + ] +} + #problem-def("AdditionalKey")[ Given a set $A$ of attribute names, a collection $F$ of functional dependencies on $A$, a subset $R subset.eq A$, and a set $K$ of candidate keys for the relational scheme $chevron.l R, F chevron.r$, diff --git a/docs/paper/references.bib b/docs/paper/references.bib index f24f1a092..0d9948cfa 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -255,6 +255,27 @@ @article{evenItaiShamir1976 doi = {10.1137/0205048} } +@article{sahni1974, + author = {Sartaj Sahni}, + title = {Computationally Related Problems}, + journal = {SIAM Journal on Computing}, + volume = {3}, + number = {4}, + pages = {262--279}, + year = {1974} +} + +@article{jewell1962, + author = {William S. Jewell}, + title = {Optimal Flow Through Networks with Gains}, + journal = {Operations Research}, + volume = {10}, + number = {4}, + pages = {476--499}, + year = {1962}, + doi = {10.1287/opre.10.4.476} +} + @article{abdelWahabKameda1978, author = {H. M. Abdel-Wahab and T. Kameda}, title = {Scheduling to Minimize Maximum Cumulative Cost Subject to Series-Parallel Precedence Constraints}, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index a73c7195d..35e3fed2d 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -751,6 +751,60 @@ fn test_create_integral_flow_with_multipliers_rejects_wrong_multiplier_count() { assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); } +#[test] +fn test_create_integral_flow_with_multipliers_rejects_zero_nonterminal_multiplier() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,1>3,2>3", + "--capacities", + "1,1,2,2", + "--source", + "0", + "--sink", + "3", + "--multipliers", + "1,0,3,1", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("non-terminal multipliers must be positive")); + assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); +} + +#[test] +fn test_create_integral_flow_with_multipliers_rejects_identical_source_and_sink() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,1>3,2>3", + "--capacities", + "1,1,2,2", + "--source", + "0", + "--sink", + "0", + "--multipliers", + "1,2,3,1", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("requires distinct --source and --sink")); + assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); +} + #[test] fn test_create_consecutive_block_minimization_rejects_ragged_matrix() { let output = pred() diff --git a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs index 37f4e8f1d..d2dc7a65a 100644 --- a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs +++ b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs @@ -135,5 +135,15 @@ fn test_integral_flow_with_multipliers_canonical_example_spec() { #[test] fn test_integral_flow_with_multipliers_paper_example() { - assert!(yes_instance().evaluate(&yes_config())); + let problem = yes_instance(); + let config = yes_config(); + let solver = BruteForce::new(); + + assert!(problem.evaluate(&config)); + assert_eq!([config[0], config[2], config[4]], [1, 1, 1]); + assert_eq!([config[6], config[8], config[10]], [2, 4, 6]); + assert_eq!(config[6] + config[8] + config[10], 12); + + let all_solutions = solver.find_all_satisfying(&problem); + assert!(all_solutions.iter().any(|solution| solution == &config)); } From d90e6c0b8582b1b3f78c8be4d64a88fc36486990 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 01:27:47 +0800 Subject: [PATCH 4/6] chore: remove plan file after implementation --- ...26-03-22-integral-flow-with-multipliers.md | 284 ------------------ 1 file changed, 284 deletions(-) delete mode 100644 docs/plans/2026-03-22-integral-flow-with-multipliers.md diff --git a/docs/plans/2026-03-22-integral-flow-with-multipliers.md b/docs/plans/2026-03-22-integral-flow-with-multipliers.md deleted file mode 100644 index 11ed13fc8..000000000 --- a/docs/plans/2026-03-22-integral-flow-with-multipliers.md +++ /dev/null @@ -1,284 +0,0 @@ -# IntegralFlowWithMultipliers Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `IntegralFlowWithMultipliers` graph model, including registry metadata, CLI creation/inspection support, canonical example coverage, and a paper entry, for issue `#290`. - -**Architecture:** Implement this as a fixed-variant satisfaction model over `DirectedGraph` with one integer variable per arc. Feasibility checks enforce capacity bounds, multiplier-scaled conservation at non-terminal vertices, and sink net inflow `>= requirement`; metadata and examples follow the issue/comment decisions (`u64` data, no misleading generalized-flow alias, concrete complexity `(max_capacity + 1)^num_arcs`). Execute the paper work in a separate batch after the Rust/example-db work is complete so the paper can load the final canonical example data. - -**Tech Stack:** Rust workspace, registry inventory metadata, Clap CLI, example-db, Typst paper, `make` verification targets. - ---- - -## Issue Context - -- Issue: `#290` `[Model] IntegralFlowWithMultipliers` -- Pipeline state: Ready -> claimed to In progress by `run-pipeline` -- `issue-context` result: `Good` label present, action = `create-pr`, no PR to resume -- Companion rule issue exists: `#363` `[Rule] PARTITION to INTEGRAL FLOW WITH MULTIPLIERS` -- Use the repaired issue body/comment decisions as source of truth: - - store `multipliers`, `capacities`, `requirement` as `u64` - - `multipliers.len() == num_vertices`, with source/sink entries ignored - - no `Generalized Flow` alias - - use complexity string `"(max_capacity + 1)^num_arcs"` - - use the cleaned YES instance as the canonical worked example - - keep the repaired diamond-graph NO instance in tests only - -## Batch Layout - -- **Batch 1:** add-model Steps 1-5.5 - - Rust model, registry wiring, CLI creation/help, canonical example, non-paper tests -- **Batch 2:** add-model Step 6 - - `docs/paper/reductions.typ` entry + paper/example verification - -## Reference Files - -- Model pattern: `src/models/graph/directed_two_commodity_integral_flow.rs` -- Metadata/size-field pattern: `src/models/graph/undirected_two_commodity_integral_flow.rs` -- Trait smoke coverage: `src/unit_tests/trait_consistency.rs` -- CLI creation pattern: `problemreductions-cli/src/commands/create.rs` -- Paper pattern: `docs/paper/reductions.typ` entries for `DirectedTwoCommodityIntegralFlow` and `MaximumIndependentSet` - -## Batch 1 - -### Task 1: Add the failing model/unit tests first - -**Files:** -- Create: `src/unit_tests/models/graph/integral_flow_with_multipliers.rs` - -**Step 1: Write the failing tests** - -Add tests that cover: -- creation/accessors/dims/size getters -- a satisfying assignment for the repaired YES instance -- an unsatisfying assignment for the repaired NO instance -- multiplier-scaled conservation failure -- sink net-inflow check uses `>= requirement` -- wrong config length returns `false` -- serde round-trip -- brute-force solver finds a satisfying config for the YES instance and none for the NO instance - -Also add a paper-example-oriented test scaffold that can be finalized once the canonical example is wired. - -**Step 2: Run the focused tests and confirm they fail** - -Run: - -```bash -cargo test integral_flow_with_multipliers --lib -``` - -Expected: compilation/test failures because the model type and module do not exist yet. - -**Step 3: Commit the red state only if it is helpful** - -Optional; skip if the branch policy prefers keeping the red state local. - -### Task 2: Implement the Rust model and registry wiring - -**Files:** -- Create: `src/models/graph/integral_flow_with_multipliers.rs` -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Modify: `src/unit_tests/trait_consistency.rs` - -**Step 1: Implement the model with the smallest code that satisfies Task 1** - -Add `IntegralFlowWithMultipliers` with: -- `ProblemSchemaEntry` metadata -- `ProblemSizeFieldEntry` declaring at least `num_vertices`, `num_arcs`, `max_capacity`, and `requirement` -- fields: `graph`, `source`, `sink`, `multipliers`, `capacities`, `requirement` -- constructor validation: - - `capacities.len() == graph.num_arcs()` - - `multipliers.len() == graph.num_vertices()` - - `source`/`sink` in bounds and distinct - - non-terminal multipliers are positive - - each capacity fits into `usize` for `dims()` -- accessors/getters: `graph()`, `capacities()`, `multipliers()`, `source()`, `sink()`, `requirement()`, `num_vertices()`, `num_arcs()`, `max_capacity()` -- feasibility helper using `i128` balance accumulation per vertex - - enforce `0 <= f(a) <= c(a)` implicitly from `dims()`/config decoding - - for each non-terminal `v`, require `h(v) * inflow(v) == outflow(v)` - - require sink net inflow `incoming - outgoing >= requirement` -- `Problem` impl: - - `NAME = "IntegralFlowWithMultipliers"` - - `Metric = bool` - - `variant() = variant_params![]` - - `dims() = capacities.iter().map(|c| c + 1)` - - `evaluate()` delegates to feasibility -- `SatisfactionProblem` impl -- `declare_variants! { default sat IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs", }` -- canonical example spec using the repaired YES instance from the issue -- test module link at the bottom - -Wire the new model into: -- `src/models/graph/mod.rs` docs/mod exports/example-spec chain -- `src/models/mod.rs` -- `src/lib.rs` prelude exports -- `src/unit_tests/trait_consistency.rs` - -**Step 2: Run the focused library tests and make them green** - -Run: - -```bash -cargo test integral_flow_with_multipliers --lib -cargo test trait_consistency --lib -``` - -Expected: the new model tests and trait smoke test pass. - -**Step 3: Refactor only after green** - -Keep helper methods local to the model file; do not generalize flow utilities prematurely. - -### Task 3: Add CLI creation/help coverage and example-db wiring - -**Files:** -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/tests/cli_tests.rs` - -**Step 1: Write/extend failing CLI tests first** - -Add tests that cover: -- `pred create IntegralFlowWithMultipliers` with `--arcs`, `--capacities`, `--multipliers`, `--source`, `--sink`, `--requirement` -- `pred inspect`/`pred show` exposing the new size fields and schema fields -- error cases for missing `--multipliers` or wrong multiplier/capacity lengths -- `pred create --example IntegralFlowWithMultipliers` returning the canonical JSON shape - -**Step 2: Run the focused CLI tests and confirm they fail** - -Run: - -```bash -cargo test -p problemreductions-cli integral_flow_with_multipliers -``` - -Expected: failures because the CLI does not yet know the problem/flags. - -**Step 3: Implement the minimal CLI support** - -Add: -- new `CreateArgs` field for `--multipliers` -- new `CreateArgs` field for singular `--requirement` -- `all_data_flags_empty()` coverage for both new fields -- after-help table/examples entry in `problemreductions-cli/src/cli.rs` -- `example_for()` entry in `problemreductions-cli/src/commands/create.rs` -- create-arm in `problemreductions-cli/src/commands/create.rs` using `parse_directed_graph(...)` - - require `--arcs` - - parse `--capacities` (default all ones if omitted only if that matches existing CLI norms; otherwise require explicitly) - - require `--multipliers`, `--source`, `--sink`, `--requirement` - - validate vector lengths and vertex bounds - - construct `IntegralFlowWithMultipliers::new(...)` - -If the registry alias machinery already handles the canonical name, do **not** add a made-up short alias. - -**Step 4: Run the focused CLI tests and make them green** - -Run: - -```bash -cargo test -p problemreductions-cli integral_flow_with_multipliers -``` - -### Task 4: Run the Batch 1 verification set - -**Files:** -- None beyond the files above - -**Step 1: Run the verification commands** - -Run: - -```bash -cargo test integral_flow_with_multipliers -make fmt -make clippy -``` - -If `make clippy` is too broad while iterating, use targeted `cargo clippy --all-targets --all-features -- -D warnings` and finish with the repo target before closing Batch 1. - -**Step 2: Commit the Batch 1 implementation** - -Suggested message: - -```bash -git add src/models/graph/integral_flow_with_multipliers.rs \ - src/models/graph/mod.rs src/models/mod.rs src/lib.rs \ - src/unit_tests/models/graph/integral_flow_with_multipliers.rs \ - src/unit_tests/trait_consistency.rs \ - problemreductions-cli/src/cli.rs \ - problemreductions-cli/src/commands/create.rs \ - problemreductions-cli/tests/cli_tests.rs -git commit -m "Add IntegralFlowWithMultipliers model" -``` - -## Batch 2 - -### Task 5: Add the paper entry and paper-example validation - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `src/unit_tests/models/graph/integral_flow_with_multipliers.rs` - -**Step 1: Write the failing paper/example test first** - -Complete the paper-example test in the model unit test file so it: -- builds the canonical YES instance -- evaluates the documented satisfying config -- confirms the brute-force solver finds at least one satisfying config - -**Step 2: Run the focused test and confirm red if needed** - -Run: - -```bash -cargo test integral_flow_with_multipliers_paper_example --lib -``` - -Expected: failure until the final documented example/config is aligned. - -**Step 3: Implement the paper entry** - -In `docs/paper/reductions.typ`: -- add display name entry for `IntegralFlowWithMultipliers` -- add `problem-def("IntegralFlowWithMultipliers")` -- use the issue-approved formulation: directed graph, vertex multipliers on non-terminals, sink requirement `>= R` -- cite the Sahni 1974 and Jewell 1962 references -- explain the polynomial special case when all multipliers are 1 / continuous-flow relaxation -- use the canonical YES example from the issue and load it from example-db -- include a small directed-network figure and `pred-commands()` snippet derived from the canonical example data - -**Step 4: Run the paper/example verification** - -Run: - -```bash -cargo test integral_flow_with_multipliers_paper_example --lib -make paper -``` - -**Step 5: Commit the paper batch** - -Suggested message: - -```bash -git add docs/paper/reductions.typ src/unit_tests/models/graph/integral_flow_with_multipliers.rs -git commit -m "Document IntegralFlowWithMultipliers" -``` - -## Final Verification - -Run before cleanup/push: - -```bash -make check -git status --short -``` - -Success criteria: -- new model is discoverable through the registry/CLI -- `pred create IntegralFlowWithMultipliers ...` works -- canonical example exists and is used by tests/paper -- the plan file can be deleted before the final push, per `issue-to-pr` From cf511c4bdaaa4d84c67896f6652775e0acf3c415 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 13:38:55 +0800 Subject: [PATCH 5/6] Fix formatting after merge conflict resolution Co-Authored-By: Claude Opus 4.6 (1M context) --- src/models/mod.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/models/mod.rs b/src/models/mod.rs index ad922045f..b9847eaed 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -26,10 +26,9 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MixedChinesePostman, MultipleChoiceBranching, - MultipleCopyFileAllocation, - OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, - ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, + PartitionIntoTriangles, RuralPostman, ShortestWeightConstrainedPath, SpinGlass, SteinerTree, + SteinerTreeInGraphs, StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; From a1ffbd5c5f37ed63448acbe2c159d55547a62cb1 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 13:42:48 +0800 Subject: [PATCH 6/6] Fix paper solve command: add --solver brute-force (no ILP path for this model) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b0b598711..6e9e51401 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -5267,7 +5267,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #pred-commands( "pred create --example IntegralFlowWithMultipliers -o integral-flow-with-multipliers.json", - "pred solve integral-flow-with-multipliers.json", + "pred solve integral-flow-with-multipliers.json --solver brute-force", "pred evaluate integral-flow-with-multipliers.json --config " + config.map(str).join(","), )