From 12941029e76aa9004fa601d21138a28dba4a0a96 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 04:04:55 +0800 Subject: [PATCH 1/6] Add plan for #293: [Model] IntegralFlowBundles --- .../plans/2026-03-22-integral-flow-bundles.md | 328 ++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 docs/plans/2026-03-22-integral-flow-bundles.md diff --git a/docs/plans/2026-03-22-integral-flow-bundles.md b/docs/plans/2026-03-22-integral-flow-bundles.md new file mode 100644 index 000000000..dbf4f2703 --- /dev/null +++ b/docs/plans/2026-03-22-integral-flow-bundles.md @@ -0,0 +1,328 @@ +# IntegralFlowBundles Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add `IntegralFlowBundles` for issue `#293`, including the model, direct ILP solver reduction, CLI/example-db support, and paper coverage. + +**Architecture:** Implement `IntegralFlowBundles` as a `SatisfactionProblem` under `src/models/misc/` with one integer decision variable per directed arc. Each arc domain is bounded by the minimum bundle capacity among bundles containing that arc; evaluation enforces bundle capacities, flow conservation at nonterminals, and required net inflow at the sink. Because the issue explicitly promises ILP solving, also add a direct `IntegralFlowBundles -> ILP` reduction with a canonical rule example and paper theorem in the same branch. + +**Tech Stack:** Rust workspace, inventory registry, Clap CLI, example-db fixtures, Typst paper, optional `ilp-solver` feature. + +--- + +## Batch 1: Implement the model, solver rule, tests, and CLI + +### Task 1: Add failing model tests and the minimal model scaffold + +**Files:** +- Create: `src/models/misc/integral_flow_bundles.rs` +- Create: `src/unit_tests/models/misc/integral_flow_bundles.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Write the failing test** + +Add model tests that pin down the issue-backed example and the repo review checklist: +- `test_integral_flow_bundles_creation_and_getters` +- `test_integral_flow_bundles_dims_use_tight_arc_bounds` +- `test_integral_flow_bundles_evaluate_yes_and_no_examples` +- `test_integral_flow_bundles_rejects_bad_bundle_sum_or_conservation` +- `test_integral_flow_bundles_serialization` + +The yes-instance should be the issue example: +- `num_vertices = 4` +- `arcs = [(0,1), (0,2), (1,3), (2,3), (1,2), (2,1)]` +- `bundles = [[0,1], [2,5], [3,4]]` +- `bundle_capacities = [1,1,1]` +- `source = 0`, `sink = 3`, `requirement = 1` +- satisfying config: `[1,0,1,0,0,0]` + +The no-instance should reuse the same data with `requirement = 2`. + +**Step 2: Run test to verify it fails** + +Run: +```bash +cargo test test_integral_flow_bundles_creation_and_getters --lib +``` + +Expected: FAIL because `IntegralFlowBundles` does not exist yet. + +**Step 3: Write minimal implementation** + +Create `src/models/misc/integral_flow_bundles.rs` with: +- `ProblemSchemaEntry` and `ProblemSizeFieldEntry` +- `IntegralFlowBundles` struct with fields: + - `num_vertices: usize` + - `arcs: Vec<(usize, usize)>` + - `source: usize` + - `sink: usize` + - `bundles: Vec>` + - `bundle_capacities: Vec` + - `requirement: u64` +- constructor validation: + - bundle count matches capacity count + - every arc index in every bundle is in bounds + - every arc appears in at least one bundle + - `source`/`sink` are valid and distinct + - each per-arc upper bound fits `usize` for `dims()` +- getters: + - `num_vertices()` + - `num_arcs()` + - `num_bundles()` + - accessors for all stored fields +- `Problem` impl: + - `Metric = bool` + - `dims()` returns one domain size per arc: `min(capacity over containing bundles) + 1` + - `evaluate()` checks config length, domain bounds, bundle sums, flow conservation, and net sink inflow +- `SatisfactionProblem` impl +- `declare_variants! { default sat IntegralFlowBundles => "2^num_arcs" }` +- `canonical_model_example_specs()` using the yes-instance above +- `#[cfg(test)]` link to the new unit test file + +Register the type in: +- `src/models/misc/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` + +**Step 4: Run test to verify it passes** + +Run: +```bash +cargo test integral_flow_bundles --lib +``` + +Expected: PASS for the new model tests. + +**Step 5: Commit** + +```bash +git add src/models/misc/integral_flow_bundles.rs src/unit_tests/models/misc/integral_flow_bundles.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs +git commit -m "Add IntegralFlowBundles model" +``` + +### Task 2: Add the direct ILP reduction and rule tests + +**Files:** +- Create: `src/rules/integralflowbundles_ilp.rs` +- Create: `src/unit_tests/rules/integralflowbundles_ilp.rs` +- Modify: `src/rules/mod.rs` + +**Step 1: Write the failing test** + +Add rule tests that cover both structure and closed-loop solving: +- `test_reduction_creates_valid_ilp` +- `test_integral_flow_bundles_to_ilp_closed_loop` +- `test_extract_solution_returns_arc_flows` +- `test_solve_reduced_integral_flow_bundles` + +Target behavior: +- one ILP variable per arc, integer and lower-bounded by `0` +- one bundle-capacity constraint per bundle +- one conservation equality per nonterminal vertex +- one sink-inflow inequality enforcing `>= requirement` +- objective can be zero/minimize-zero because the source problem is a satisfaction problem + +Use the canonical yes-instance from Task 1 as the closed-loop fixture. + +**Step 2: Run test to verify it fails** + +Run: +```bash +cargo test --features ilp-solver integralflowbundles_ilp +``` + +Expected: FAIL because the reduction does not exist yet. + +**Step 3: Write minimal implementation** + +Create `src/rules/integralflowbundles_ilp.rs`: +- reduce `IntegralFlowBundles` to `ILP` +- use one integer ILP variable `f_a` per arc +- add bundle constraints `sum_{a in I_j} f_a <= c_j` +- add conservation equalities for each `v != source, sink` +- add a sink-requirement inequality using incoming minus outgoing sink flow +- return identity extraction (`target_solution.to_vec()`) +- register overhead with: + - `num_vars = "num_arcs"` + - `num_constraints = "num_bundles + num_vertices - 2 + 1"` +- add `canonical_rule_example_specs()` based on the issue example + +Register the rule in `src/rules/mod.rs`. + +**Step 4: Run test to verify it passes** + +Run: +```bash +cargo test --features ilp-solver integralflowbundles_ilp +``` + +Expected: PASS for the new reduction tests. + +**Step 5: Commit** + +```bash +git add src/rules/integralflowbundles_ilp.rs src/unit_tests/rules/integralflowbundles_ilp.rs src/rules/mod.rs +git commit -m "Add IntegralFlowBundles to ILP reduction" +``` + +### Task 3: Add CLI creation support and example-db coverage + +**Files:** +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/tests/cli_tests.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `src/rules/mod.rs` + +**Step 1: Write the failing test** + +Add CLI tests for: +- `pred create IntegralFlowBundles --arcs "0>1,0>2,1>3,2>3,1>2,2>1" --bundles "0,1;2,5;3,4" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4` +- missing `--bundles` or `--bundle-capacities` +- `pred create --example IntegralFlowBundles` + +Also add or extend example-db assertions if needed so both: +- `find_model_example("IntegralFlowBundles")` +- `find_rule_example("IntegralFlowBundles" -> "ILP")` +remain covered by existing generic tests. + +**Step 2: Run test to verify it fails** + +Run: +```bash +cargo test -p problemreductions-cli integral_flow_bundles +``` + +Expected: FAIL because the CLI flags and create arm do not exist yet. + +**Step 3: Write minimal implementation** + +Update `problemreductions-cli/src/cli.rs`: +- add help-table entries for `IntegralFlowBundles` +- add `bundles: Option` and `bundle_capacities: Option` to `CreateArgs` +- treat both flags as data-bearing in `all_data_flags_empty()` + +Update `problemreductions-cli/src/commands/create.rs`: +- add usage/example strings for `IntegralFlowBundles` +- parse `--arcs` with the existing directed-arc helper +- parse `--bundles` as semicolon-separated groups of comma-separated arc indices +- parse `--bundle-capacities` as comma-separated nonnegative integers +- add the `IntegralFlowBundles` create arm and serialize the new problem +- verify `pred create --example IntegralFlowBundles` works via the example-db registration from Tasks 1 and 2 + +Use the registry-backed alias flow; do not add manual alias tables unless a concrete failing test proves they are still required. + +**Step 4: Run test to verify it passes** + +Run: +```bash +cargo test -p problemreductions-cli integral_flow_bundles +cargo test example_db --features "example-db ilp-solver" +``` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/tests/cli_tests.rs src/models/misc/mod.rs src/rules/mod.rs +git commit -m "Wire IntegralFlowBundles through CLI and example db" +``` + +## Batch 2: Paper and final verification + +### Task 4: Document the model and ILP reduction in the paper + +**Files:** +- Modify: `docs/paper/reductions.typ` + +**Step 1: Write the failing test** + +Add the paper content first, then use the paper build as the failing/passing gate: +- display name entry for `IntegralFlowBundles` +- `problem-def("IntegralFlowBundles")` with the formal bundled-flow definition +- example narrative tied to the canonical yes-instance and its satisfying config +- `pred-commands(...)` block using `pred create --example IntegralFlowBundles` +- `reduction-rule("IntegralFlowBundles", "ILP")` explaining the per-arc-variable ILP formulation + +**Step 2: Run test to verify it fails or exposes missing pieces** + +Run: +```bash +make paper +``` + +Expected: FAIL until the paper entry, citations, and example wiring are complete. + +**Step 3: Write minimal implementation** + +Add: +- `display-name["IntegralFlowBundles"]` +- model background with Garey-Johnson/Sahni citations +- the issue’s yes/no example, explicitly checking bundle sums and sink inflow +- a short ILP reduction theorem referencing the new solver rule + +Mirror the style of: +- `UndirectedTwoCommodityIntegralFlow` +- `BinPacking -> ILP` + +**Step 4: Run test to verify it passes** + +Run: +```bash +make paper +``` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add docs/paper/reductions.typ +git commit -m "Document IntegralFlowBundles and its ILP reduction" +``` + +### Task 5: Final repo verification before push + +**Files:** +- Modify: none expected + +**Step 1: Run the focused verification suite** + +Run: +```bash +cargo test integral_flow_bundles --lib +cargo test --features ilp-solver integralflowbundles_ilp +cargo test -p problemreductions-cli integral_flow_bundles +cargo test example_db --features "example-db ilp-solver" +``` + +Expected: PASS. + +**Step 2: Run the broader safety net** + +Run: +```bash +make check +make paper +``` + +Expected: PASS. + +**Step 3: Inspect the tree** + +Run: +```bash +git status --short +``` + +Expected: only intended tracked changes; no leftover `docs/plans/*.md` after the later cleanup commit. + +**Step 4: Commit any last adjustments** + +```bash +git add -A +git commit -m "Polish IntegralFlowBundles implementation" +``` From 49f00db7db25b281e2bebc024bc01a8e4f824b29 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 04:11:51 +0800 Subject: [PATCH 2/6] Add IntegralFlowBundles model --- src/lib.rs | 6 +- src/models/graph/integral_flow_bundles.rs | 278 ++++++++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 8 +- .../models/graph/integral_flow_bundles.rs | 97 ++++++ 5 files changed, 386 insertions(+), 7 deletions(-) create mode 100644 src/models/graph/integral_flow_bundles.rs create mode 100644 src/unit_tests/models/graph/integral_flow_bundles.rs diff --git a/src/lib.rs b/src/lib.rs index f9e84dca0..623665d0c 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, IntegralFlowBundles, IsomorphicSpanningTree, KClique, + KthBestSpanningTree, LengthBoundedDisjointPaths, MixedChinesePostman, SpinGlass, + SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs new file mode 100644 index 000000000..ca7ea9c1b --- /dev/null +++ b/src/models/graph/integral_flow_bundles.rs @@ -0,0 +1,278 @@ +//! Integral Flow with Bundles problem implementation. +//! +//! Given a directed graph with overlapping bundle-capacity constraints on arcs, +//! determine whether an integral flow can deliver a required amount to the sink. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; +use crate::topology::DirectedGraph; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +inventory::submit! { + ProblemSchemaEntry { + name: "IntegralFlowBundles", + display_name: "Integral Flow with Bundles", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Integral flow feasibility on a directed graph with overlapping bundle capacities", + 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: "bundles", type_name: "Vec>", description: "Bundles of arc indices covering A" }, + FieldInfo { name: "bundle_capacities", type_name: "Vec", description: "Capacity c_j for each bundle I_j" }, + FieldInfo { name: "requirement", type_name: "u64", description: "Required net inflow R at the sink" }, + ], + } +} + +inventory::submit! { + ProblemSizeFieldEntry { + name: "IntegralFlowBundles", + fields: &["num_vertices", "num_arcs", "num_bundles"], + } +} + +/// Integral Flow with Bundles (Garey & Johnson ND36). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegralFlowBundles { + graph: DirectedGraph, + source: usize, + sink: usize, + bundles: Vec>, + bundle_capacities: Vec, + requirement: u64, +} + +impl IntegralFlowBundles { + /// Create a new Integral Flow with Bundles instance. + pub fn new( + graph: DirectedGraph, + source: usize, + sink: usize, + bundles: Vec>, + bundle_capacities: Vec, + requirement: u64, + ) -> Self { + let num_vertices = graph.num_vertices(); + let num_arcs = graph.num_arcs(); + + assert!(source < num_vertices, "source ({source}) >= num_vertices ({num_vertices})"); + assert!(sink < num_vertices, "sink ({sink}) >= num_vertices ({num_vertices})"); + assert!(source != sink, "source and sink must be distinct"); + assert_eq!( + bundles.len(), + bundle_capacities.len(), + "bundles length must match bundle_capacities length" + ); + assert!(requirement > 0, "requirement must be positive"); + + let mut arc_covered = vec![false; num_arcs]; + let mut arc_upper_bounds = vec![u64::MAX; num_arcs]; + + for (bundle_index, (bundle, &capacity)) in bundles.iter().zip(&bundle_capacities).enumerate() { + assert!(capacity > 0, "bundle capacity at index {bundle_index} must be positive"); + + let mut seen = BTreeSet::new(); + for &arc_index in bundle { + assert!( + arc_index < num_arcs, + "bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}" + ); + assert!( + seen.insert(arc_index), + "bundle {bundle_index} contains duplicate arc index {arc_index}" + ); + arc_covered[arc_index] = true; + arc_upper_bounds[arc_index] = arc_upper_bounds[arc_index].min(capacity); + } + } + + for (arc_index, covered) in arc_covered.iter().copied().enumerate() { + assert!(covered, "arc {arc_index} must belong to at least one bundle"); + let domain = usize::try_from(arc_upper_bounds[arc_index]) + .ok() + .and_then(|bound| bound.checked_add(1)); + assert!( + domain.is_some(), + "bundle-derived upper bound for arc {arc_index} must fit into usize for dims()" + ); + } + + Self { + graph, + source, + sink, + bundles, + bundle_capacities, + requirement, + } + } + + /// Get the underlying directed graph. + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + /// Get the source vertex. + pub fn source(&self) -> usize { + self.source + } + + /// Get the sink vertex. + pub fn sink(&self) -> usize { + self.sink + } + + /// Get the bundles. + pub fn bundles(&self) -> &[Vec] { + &self.bundles + } + + /// Get the bundle capacities. + pub fn bundle_capacities(&self) -> &[u64] { + &self.bundle_capacities + } + + /// Get the required net inflow at the sink. + pub fn requirement(&self) -> u64 { + self.requirement + } + + /// Get the number of vertices. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of arcs. + pub fn num_arcs(&self) -> usize { + self.graph.num_arcs() + } + + /// Get the number of bundles. + pub fn num_bundles(&self) -> usize { + self.bundles.len() + } + + /// Check whether a configuration is feasible. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate(config) + } + + fn arc_upper_bounds(&self) -> Vec { + let mut upper_bounds = vec![u64::MAX; self.num_arcs()]; + for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) { + for &arc_index in bundle { + upper_bounds[arc_index] = upper_bounds[arc_index].min(capacity); + } + } + upper_bounds + } + + fn vertex_balance(&self, config: &[usize], vertex: usize) -> Option { + let mut balance = 0i128; + for (arc_index, (u, v)) in self.graph.arcs().into_iter().enumerate() { + let flow = i128::from(u64::try_from(*config.get(arc_index)?).ok()?); + if vertex == u { + balance -= flow; + } + if vertex == v { + balance += flow; + } + } + Some(balance) + } +} + +impl Problem for IntegralFlowBundles { + const NAME: &'static str = "IntegralFlowBundles"; + type Metric = bool; + + fn dims(&self) -> Vec { + self.arc_upper_bounds() + .into_iter() + .map(|bound| { + usize::try_from(bound) + .ok() + .and_then(|bound| bound.checked_add(1)) + .expect("bundle-derived arc upper bounds are validated in the constructor") + }) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_arcs() { + return false; + } + + let upper_bounds = self.arc_upper_bounds(); + for (&value, &upper_bound) in config.iter().zip(&upper_bounds) { + if u64::try_from(value).map_or(true, |value| value > upper_bound) { + return false; + } + } + + for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) { + let mut total = 0u64; + for &arc_index in bundle { + let Ok(flow) = u64::try_from(config[arc_index]) else { + return false; + }; + let Some(next_total) = total.checked_add(flow) else { + return false; + }; + total = next_total; + } + if total > capacity { + return false; + } + } + + for vertex in 0..self.num_vertices() { + if vertex == self.source || vertex == self.sink { + continue; + } + if self.vertex_balance(config, vertex) != Some(0) { + return false; + } + } + + matches!( + self.vertex_balance(config, self.sink), + Some(balance) if balance >= i128::from(self.requirement) + ) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for IntegralFlowBundles {} + +crate::declare_variants! { + default sat IntegralFlowBundles => "2^num_arcs", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "integral_flow_bundles", + instance: Box::new(IntegralFlowBundles::new( + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + 0, + 3, + vec![vec![0, 1], vec![2, 5], vec![3, 4]], + vec![1, 1, 1], + 1, + )), + optimal_config: vec![1, 0, 1, 0, 0, 0], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/integral_flow_bundles.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 06627fb70..1398df63c 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) +//! - [`IntegralFlowBundles`]: Integral flow feasibility with overlapping bundle capacities //! - [`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_bundles; pub(crate) mod isomorphic_spanning_tree; pub(crate) mod kclique; pub(crate) mod kcoloring; @@ -102,6 +104,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_bundles::IntegralFlowBundles; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kclique::KClique; pub use kcoloring::KColoring; @@ -147,6 +150,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec IntegralFlowBundles { + IntegralFlowBundles::new( + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + 0, + 3, + vec![vec![0, 1], vec![2, 5], vec![3, 4]], + vec![1, 1, 1], + 1, + ) +} + +fn no_instance() -> IntegralFlowBundles { + IntegralFlowBundles::new( + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + 0, + 3, + vec![vec![0, 1], vec![2, 5], vec![3, 4]], + vec![1, 1, 1], + 2, + ) +} + +fn satisfying_config() -> Vec { + vec![1, 0, 1, 0, 0, 0] +} + +#[test] +fn test_integral_flow_bundles_creation_and_getters() { + let problem = yes_instance(); + assert_eq!(problem.num_vertices(), 4); + assert_eq!(problem.num_arcs(), 6); + assert_eq!(problem.num_bundles(), 3); + assert_eq!(problem.source(), 0); + assert_eq!(problem.sink(), 3); + assert_eq!(problem.requirement(), 1); + assert_eq!(problem.bundle_capacities(), &[1, 1, 1]); + assert_eq!(problem.graph().arcs().len(), 6); +} + +#[test] +fn test_integral_flow_bundles_dims_use_tight_arc_bounds() { + let problem = yes_instance(); + assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2, 2]); +} + +#[test] +fn test_integral_flow_bundles_evaluate_yes_and_no_examples() { + let yes = yes_instance(); + let no = no_instance(); + let config = satisfying_config(); + assert!(yes.evaluate(&config)); + assert!(!no.evaluate(&config)); + assert!(yes.is_valid_solution(&config)); +} + +#[test] +fn test_integral_flow_bundles_rejects_bad_bundle_sum_or_conservation() { + let problem = yes_instance(); + + let mut bundle_violation = satisfying_config(); + bundle_violation[1] = 1; + assert!(!problem.evaluate(&bundle_violation)); + + let conservation_violation = vec![1, 0, 0, 0, 0, 0]; + assert!(!problem.evaluate(&conservation_violation)); +} + +#[test] +fn test_integral_flow_bundles_solver_and_paper_example() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let all = solver.find_all_satisfying(&problem); + assert!(!all.is_empty()); + assert!(all.contains(&satisfying_config())); + assert!(problem.evaluate(&satisfying_config())); +} + +#[test] +fn test_integral_flow_bundles_serialization() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let roundtrip: IntegralFlowBundles = serde_json::from_str(&json).unwrap(); + assert_eq!(roundtrip.num_vertices(), 4); + assert_eq!(roundtrip.num_arcs(), 6); + assert_eq!(roundtrip.num_bundles(), 3); + assert_eq!(roundtrip.requirement(), 1); +} + +#[test] +fn test_integral_flow_bundles_problem_name() { + assert_eq!(::NAME, "IntegralFlowBundles"); +} From bcf92ff267fd6ef7aea7c484681de62721fec98e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 04:15:04 +0800 Subject: [PATCH 3/6] Add IntegralFlowBundles to ILP reduction --- src/rules/integralflowbundles_ilp.rs | 117 ++++++++++++++++++ src/rules/mod.rs | 3 + .../rules/integralflowbundles_ilp.rs | 110 ++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 src/rules/integralflowbundles_ilp.rs create mode 100644 src/unit_tests/rules/integralflowbundles_ilp.rs diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs new file mode 100644 index 000000000..fe0abf2ae --- /dev/null +++ b/src/rules/integralflowbundles_ilp.rs @@ -0,0 +1,117 @@ +//! Reduction from Integral Flow with Bundles to ILP. +//! +//! Each directed arc gets one non-negative integer ILP variable. The ILP keeps +//! the bundle-capacity inequalities, flow-conservation equalities at +//! nonterminals, and the sink inflow lower bound from the source problem. + +use crate::models::algebraic::{ILP, LinearConstraint, ObjectiveSense}; +use crate::models::graph::IntegralFlowBundles; +use crate::reduction; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +/// Result of reducing IntegralFlowBundles to ILP. +#[derive(Debug, Clone)] +pub struct ReductionIFBToILP { + target: ILP, +} + +impl ReductionResult for ReductionIFBToILP { + type Source = IntegralFlowBundles; + type Target = ILP; + + fn target_problem(&self) -> &ILP { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +#[reduction( + overhead = { + num_vars = "num_arcs", + num_constraints = "num_bundles + num_vertices - 1", + } +)] +impl ReduceTo> for IntegralFlowBundles { + type Result = ReductionIFBToILP; + + fn reduce_to(&self) -> Self::Result { + let arcs = self.graph().arcs(); + let mut constraints = Vec::with_capacity(self.num_bundles() + self.num_vertices() - 1); + + for (bundle, &capacity) in self.bundles().iter().zip(self.bundle_capacities()) { + let terms = bundle.iter().map(|&arc_index| (arc_index, 1.0)).collect(); + constraints.push(LinearConstraint::le(terms, capacity as f64)); + } + + for vertex in 0..self.num_vertices() { + if vertex == self.source() || vertex == self.sink() { + continue; + } + + let mut terms = Vec::new(); + for (arc_index, (u, v)) in arcs.iter().copied().enumerate() { + if vertex == u { + terms.push((arc_index, -1.0)); + } + if vertex == v { + terms.push((arc_index, 1.0)); + } + } + constraints.push(LinearConstraint::eq(terms, 0.0)); + } + + let mut sink_terms = Vec::new(); + for (arc_index, (u, v)) in arcs.iter().copied().enumerate() { + if self.sink() == u { + sink_terms.push((arc_index, -1.0)); + } + if self.sink() == v { + sink_terms.push((arc_index, 1.0)); + } + } + constraints.push(LinearConstraint::ge( + sink_terms, + self.requirement() as f64, + )); + + ReductionIFBToILP { + target: ILP::new(self.num_arcs(), constraints, vec![], ObjectiveSense::Minimize), + } + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::export::SolutionPair; + use crate::topology::DirectedGraph; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "integralflowbundles_to_ilp", + build: || { + crate::example_db::specs::rule_example_with_witness::<_, ILP>( + IntegralFlowBundles::new( + DirectedGraph::new( + 4, + vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)], + ), + 0, + 3, + vec![vec![0, 1], vec![2, 5], vec![3, 4]], + vec![1, 1, 1], + 1, + ), + SolutionPair { + source_config: vec![1, 0, 1, 0, 0, 0], + target_config: vec![1, 0, 1, 0, 0, 0], + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/integralflowbundles_ilp.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index d7929687b..5a9c6ba70 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -60,6 +60,8 @@ pub(crate) mod factoring_ilp; #[cfg(feature = "ilp-solver")] pub(crate) mod graphpartitioning_ilp; #[cfg(feature = "ilp-solver")] +pub(crate) mod integralflowbundles_ilp; +#[cfg(feature = "ilp-solver")] mod ilp_bool_ilp_i32; #[cfg(feature = "ilp-solver")] pub(crate) mod ilp_qubo; @@ -134,6 +136,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec IntegralFlowBundles { + IntegralFlowBundles::new( + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + 0, + 3, + vec![vec![0, 1], vec![2, 5], vec![3, 4]], + vec![1, 1, 1], + 1, + ) +} + +fn no_instance() -> IntegralFlowBundles { + IntegralFlowBundles::new( + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + 0, + 3, + vec![vec![0, 1], vec![2, 5], vec![3, 4]], + vec![1, 1, 1], + 2, + ) +} + +fn satisfying_config() -> Vec { + vec![1, 0, 1, 0, 0, 0] +} + +#[test] +fn test_integral_flow_bundles_to_ilp_structure() { + let problem = yes_instance(); + let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem); + let ilp = reduction.target_problem(); + + assert_eq!(ilp.num_vars, 6); + assert_eq!(ilp.constraints.len(), 6); + assert_eq!(ilp.sense, ObjectiveSense::Minimize); + assert!(ilp.objective.is_empty()); + assert_eq!( + ilp.constraints + .iter() + .filter(|constraint| constraint.cmp == Comparison::Le) + .count(), + 3 + ); + assert_eq!( + ilp.constraints + .iter() + .filter(|constraint| constraint.cmp == Comparison::Eq) + .count(), + 2 + ); + assert_eq!( + ilp.constraints + .iter() + .filter(|constraint| constraint.cmp == Comparison::Ge) + .count(), + 1 + ); +} + +#[test] +fn test_integral_flow_bundles_to_ilp_closed_loop() { + let problem = yes_instance(); + let direct = BruteForce::new() + .find_satisfying(&problem) + .expect("source instance should be satisfiable"); + assert!(problem.evaluate(&direct)); + + let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem); + let ilp_solution = ILPSolver::new() + .solve(reduction.target_problem()) + .expect("ILP should be feasible"); + let extracted = reduction.extract_solution(&ilp_solution); + + assert!(problem.evaluate(&extracted)); +} + +#[test] +fn test_integral_flow_bundles_to_ilp_extract_solution_is_identity() { + let problem = yes_instance(); + let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem); + assert_eq!(reduction.extract_solution(&satisfying_config()), satisfying_config()); +} + +#[test] +fn test_integral_flow_bundles_to_ilp_unsat_instance_is_infeasible() { + let problem = no_instance(); + let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem); + assert!(ILPSolver::new().solve(reduction.target_problem()).is_none()); +} + +#[test] +fn test_integral_flow_bundles_to_ilp_sink_requirement_constraint() { + let problem = yes_instance(); + let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem); + let ilp = reduction.target_problem(); + + let sink_constraint = ilp + .constraints + .iter() + .find(|constraint| constraint.cmp == Comparison::Ge) + .expect("expected one sink inflow lower bound"); + assert_eq!(sink_constraint.rhs, 1.0); + assert_eq!(sink_constraint.terms, vec![(2, 1.0), (3, 1.0)]); +} From 0bd5d4a8ab7db880cbd1ce7dbff87f0ac3db96dd Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 04:21:02 +0800 Subject: [PATCH 4/6] Wire IntegralFlowBundles through CLI and example db --- problemreductions-cli/src/cli.rs | 10 ++ problemreductions-cli/src/commands/create.rs | 147 ++++++++++++++++++- problemreductions-cli/tests/cli_tests.rs | 145 ++++++++++++++++++ src/unit_tests/example_db.rs | 35 +++++ 4 files changed, 334 insertions(+), 3 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 02df552d7..a90b0b8ff 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -233,6 +233,7 @@ Flags by problem type: HamiltonianCircuit, HC --graph LongestCircuit --graph, --edge-weights, --bound BoundedComponentSpanningForest --graph, --weights, --k, --bound + IntegralFlowBundles --arcs, --bundles, --bundle-capacities, --source, --sink, --requirement [--num-vertices] UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 IsomorphicSpanningTree --graph, --tree KthBestSpanningTree --graph, --edge-weights, --k, --bound @@ -356,12 +357,18 @@ pub struct CreateArgs { /// Edge capacities for multicommodity flow problems (e.g., 1,1,2) #[arg(long)] pub capacities: Option, + /// Bundle capacities for IntegralFlowBundles (e.g., 1,1,1) + #[arg(long)] + pub bundle_capacities: 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 IntegralFlowBundles + #[arg(long)] + pub requirement: Option, /// Required number of paths for LengthBoundedDisjointPaths #[arg(long)] pub num_paths_required: Option, @@ -462,6 +469,9 @@ pub struct CreateArgs { /// Partition groups for arc-index partitions (semicolon-separated, e.g., "0,1;2,3") #[arg(long)] pub partition: Option, + /// Arc bundles for IntegralFlowBundles (semicolon-separated groups of arc indices, e.g., "0,1;2,5;3,4") + #[arg(long)] + pub bundles: Option, /// Universe size for set-system problems such as MinimumHittingSet, MinimumSetCovering, and ComparativeContainment #[arg(long)] pub universe: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index d03de0341..b8d8d3632 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -13,9 +13,9 @@ use problemreductions::models::algebraic::{ use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, - LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, MinimumMultiwayCut, - MixedChinesePostman, MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, + IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, + MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, SteinerTree, + SteinerTreeInGraphs, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, @@ -50,8 +50,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.edge_weights.is_none() && args.edge_lengths.is_none() && args.capacities.is_none() + && args.bundle_capacities.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() @@ -84,6 +86,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.r_weights.is_none() && args.s_weights.is_none() && args.partition.is_none() + && args.bundles.is_none() && args.universe.is_none() && args.biedges.is_none() && args.left.is_none() @@ -513,6 +516,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", + "IntegralFlowBundles" => { + "--arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4" + } "MinimumCutIntoBoundedSets" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 3 --size-bound 3 --cut-bound 1" } @@ -754,6 +760,10 @@ fn help_flag_hint( ("ConsistencyOfDatabaseFrequencyTables", "known_values") => { "semicolon-separated triples: \"0,0,0;3,0,1;1,2,1\"" } + ("IntegralFlowBundles", "bundles") => { + "semicolon-separated groups: \"0,1;2,5;3,4\"" + } + ("IntegralFlowBundles", "bundle_capacities") => "comma-separated capacities: 1,1,1", ("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"", ("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => { "semicolon-separated 0/1 rows: \"1,1,0;0,1,1\"" @@ -1375,6 +1385,45 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // IntegralFlowBundles (directed graph + bundles + source/sink + requirement) + "IntegralFlowBundles" => { + let usage = "Usage: pred create IntegralFlowBundles --arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowBundles 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 bundles = parse_bundles(args, num_arcs, usage)?; + let bundle_capacities = parse_bundle_capacities(args, bundles.len(), usage)?; + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowBundles requires --source\n\n{usage}") + })?; + let sink = args + .sink + .ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --sink\n\n{usage}"))?; + let requirement = args.requirement.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowBundles requires --requirement\n\n{usage}") + })?; + validate_vertex_index("source", source, graph.num_vertices(), usage)?; + validate_vertex_index("sink", sink, graph.num_vertices(), usage)?; + anyhow::ensure!( + source != sink, + "IntegralFlowBundles requires distinct --source and --sink\n\n{usage}" + ); + + ( + ser(IntegralFlowBundles::new( + graph, + source, + sink, + bundles, + bundle_capacities, + requirement, + ))?, + resolved_variant.clone(), + ) + } + // LengthBoundedDisjointPaths (graph + source + sink + path count + bound) "LengthBoundedDisjointPaths" => { let usage = "Usage: pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3"; @@ -4204,6 +4253,48 @@ fn parse_capacities(args: &CreateArgs, num_edges: usize, usage: &str) -> Result< Ok(capacities) } +fn parse_bundle_capacities(args: &CreateArgs, num_bundles: usize, usage: &str) -> Result> { + let capacities = args.bundle_capacities.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowBundles requires --bundle-capacities\n\n{usage}") + })?; + let capacities: Vec = capacities + .split(',') + .map(|s| { + let trimmed = s.trim(); + trimmed + .parse::() + .with_context(|| format!("Invalid bundle capacity `{trimmed}`\n\n{usage}")) + }) + .collect::>>()?; + anyhow::ensure!( + capacities.len() == num_bundles, + "Expected {} bundle capacities but got {}\n\n{}", + num_bundles, + capacities.len(), + usage + ); + for (bundle_index, &capacity) in capacities.iter().enumerate() { + let fits = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(); + anyhow::ensure!( + fits, + "bundle capacity {} at bundle index {} is too large for this platform\n\n{}", + capacity, + bundle_index, + usage + ); + anyhow::ensure!( + capacity > 0, + "bundle capacity at bundle index {} must be positive\n\n{}", + bundle_index, + usage + ); + } + Ok(capacities) +} + /// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s. fn parse_couplings(args: &CreateArgs, num_edges: usize) -> Result> { match &args.couplings { @@ -4446,6 +4537,53 @@ fn parse_partition_groups(args: &CreateArgs, num_arcs: usize) -> Result Result>> { + let bundles_str = args.bundles.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowBundles requires --bundles\n\n{usage}") + })?; + + let bundles: Vec> = bundles_str + .split(';') + .map(|bundle| { + let bundle = bundle.trim(); + anyhow::ensure!( + !bundle.is_empty(), + "IntegralFlowBundles does not allow empty bundle entries\n\n{usage}" + ); + bundle + .split(',') + .map(|s| { + s.trim().parse::().with_context(|| { + format!("Invalid bundle arc index `{}`\n\n{usage}", s.trim()) + }) + }) + .collect::>>() + }) + .collect::>()?; + + let mut seen_overall = vec![false; num_arcs]; + for (bundle_index, bundle) in bundles.iter().enumerate() { + let mut seen_in_bundle = BTreeSet::new(); + for &arc_index in bundle { + anyhow::ensure!( + arc_index < num_arcs, + "bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}\n\n{usage}" + ); + anyhow::ensure!( + seen_in_bundle.insert(arc_index), + "bundle {bundle_index} contains duplicate arc index {arc_index}\n\n{usage}" + ); + seen_overall[arc_index] = true; + } + } + anyhow::ensure!( + seen_overall.iter().all(|covered| *covered), + "bundles must cover every arc at least once\n\n{usage}" + ); + + Ok(bundles) +} + fn parse_multiple_choice_branching_threshold(args: &CreateArgs, usage: &str) -> Result { let raw_bound = args .bound @@ -5871,8 +6009,10 @@ mod tests { edge_weights: None, edge_lengths: None, capacities: None, + bundle_capacities: None, source: None, sink: None, + requirement: None, num_paths_required: None, couplings: None, fields: None, @@ -5906,6 +6046,7 @@ mod tests { r_weights: None, s_weights: None, partition: None, + bundles: None, universe: None, biedges: None, left: None, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 5c38df249..17f7e25cf 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -656,6 +656,151 @@ 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_bundles() { + let output = pred() + .args([ + "create", + "IntegralFlowBundles", + "--arcs", + "0>1,0>2,1>3,2>3,1>2,2>1", + "--bundles", + "0,1;2,5;3,4", + "--bundle-capacities", + "1,1,1", + "--source", + "0", + "--sink", + "3", + "--requirement", + "1", + "--num-vertices", + "4", + ]) + .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"], "IntegralFlowBundles"); + assert_eq!(json["variant"], serde_json::json!({})); + assert_eq!(json["data"]["graph"]["num_vertices"], 4); + assert_eq!(json["data"]["graph"]["arcs"].as_array().unwrap().len(), 6); + assert_eq!(json["data"]["bundles"], serde_json::json!([[0, 1], [2, 5], [3, 4]])); + assert_eq!(json["data"]["bundle_capacities"], serde_json::json!([1, 1, 1])); + assert_eq!(json["data"]["source"], 0); + assert_eq!(json["data"]["sink"], 3); + assert_eq!(json["data"]["requirement"], 1); +} + +#[test] +fn test_create_integral_flow_bundles_missing_bundles_shows_usage() { + let output = pred() + .args([ + "create", + "IntegralFlowBundles", + "--arcs", + "0>1,0>2,1>3,2>3,1>2,2>1", + "--bundle-capacities", + "1,1,1", + "--source", + "0", + "--sink", + "3", + "--requirement", + "1", + "--num-vertices", + "4", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("requires --bundles")); + assert!(stderr.contains("Usage: pred create IntegralFlowBundles")); +} + +#[test] +fn test_create_integral_flow_bundles_rejects_wrong_bundle_capacity_count() { + let output = pred() + .args([ + "create", + "IntegralFlowBundles", + "--arcs", + "0>1,0>2,1>3,2>3,1>2,2>1", + "--bundles", + "0,1;2,5;3,4", + "--bundle-capacities", + "1,1", + "--source", + "0", + "--sink", + "3", + "--requirement", + "1", + "--num-vertices", + "4", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Expected 3 bundle capacities but got 2")); + assert!(stderr.contains("Usage: pred create IntegralFlowBundles")); +} + +#[test] +fn test_create_integral_flow_bundles_rejects_out_of_range_bundle_arc() { + let output = pred() + .args([ + "create", + "IntegralFlowBundles", + "--arcs", + "0>1,0>2,1>3,2>3,1>2,2>1", + "--bundles", + "0,1;2,7;3,4", + "--bundle-capacities", + "1,1,1", + "--source", + "0", + "--sink", + "3", + "--requirement", + "1", + "--num-vertices", + "4", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("bundle 1 references arc 7")); + assert!(stderr.contains("Usage: pred create IntegralFlowBundles")); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + +#[test] +fn test_create_integral_flow_bundles_example() { + let output = pred() + .args(["create", "--example", "IntegralFlowBundles"]) + .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"], "IntegralFlowBundles"); + assert_eq!(json["data"]["requirement"], 1); + assert_eq!(json["data"]["bundles"].as_array().unwrap().len(), 3); +} + #[test] fn test_create_consecutive_block_minimization_rejects_ragged_matrix() { let output = pred() diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 5350da092..47d6bd4c7 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -112,6 +112,21 @@ fn test_find_model_example_multiprocessor_scheduling() { ); } +#[test] +fn test_find_model_example_integral_flow_bundles() { + let problem = ProblemRef { + name: "IntegralFlowBundles".to_string(), + variant: BTreeMap::new(), + }; + + let example = find_model_example(&problem).expect("IntegralFlowBundles example exists"); + assert_eq!(example.problem, "IntegralFlowBundles"); + assert_eq!(example.variant, problem.variant); + assert_eq!(example.instance["graph"]["num_vertices"], 4); + assert_eq!(example.instance["requirement"], 1); + assert_eq!(example.optimal_config, vec![1, 0, 1, 0, 0, 0]); +} + #[test] fn test_find_model_example_strong_connectivity_augmentation() { let problem = ProblemRef { @@ -176,6 +191,26 @@ fn test_find_rule_example_sat_to_kcoloring_contains_full_instances() { ); } +#[cfg(feature = "ilp-solver")] +#[test] +fn test_find_rule_example_integral_flow_bundles_to_ilp_contains_full_instances() { + let source = ProblemRef { + name: "IntegralFlowBundles".to_string(), + variant: BTreeMap::new(), + }; + let target = ProblemRef { + name: "ILP".to_string(), + variant: BTreeMap::from([("variable".to_string(), "i32".to_string())]), + }; + + let example = find_rule_example(&source, &target).expect("IntegralFlowBundles -> ILP exists"); + assert_eq!(example.source.problem, "IntegralFlowBundles"); + assert_eq!(example.target.problem, "ILP"); + assert!(example.source.instance.get("graph").is_some()); + assert_eq!(example.solutions[0].source_config, vec![1, 0, 1, 0, 0, 0]); + assert_eq!(example.solutions[0].target_config, vec![1, 0, 1, 0, 0, 0]); +} + #[test] fn test_build_rule_db_has_unique_structural_keys() { let db = build_rule_db().expect("rule db should build"); From 7157469613e8cf5a4718f596d68a7187537d1f35 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 04:31:40 +0800 Subject: [PATCH 5/6] Add IntegralFlowBundles paper entry and polish formatting --- docs/paper/reductions.typ | 80 +++++++++++++++++++ docs/paper/references.bib | 11 +++ problemreductions-cli/src/commands/create.rs | 26 +++--- problemreductions-cli/tests/cli_tests.rs | 10 ++- src/lib.rs | 6 +- src/models/graph/integral_flow_bundles.rs | 24 ++++-- src/rules/integralflowbundles_ilp.rs | 19 +++-- src/rules/mod.rs | 4 +- .../models/graph/integral_flow_bundles.rs | 5 +- .../rules/integralflowbundles_ilp.rs | 5 +- 10 files changed, 153 insertions(+), 37 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 9167a0c15..9fc767fd9 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -72,6 +72,7 @@ "HamiltonianCircuit": [Hamiltonian Circuit], "BiconnectivityAugmentation": [Biconnectivity Augmentation], "HamiltonianPath": [Hamiltonian Path], + "IntegralFlowBundles": [Integral Flow with Bundles], "LongestCircuit": [Longest Circuit], "ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path], "UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow], @@ -5251,6 +5252,64 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ) ] +#{ + let x = load-model-example("IntegralFlowBundles") + let source = x.instance.source + let sink = x.instance.sink + [ + #problem-def("IntegralFlowBundles")[ + Given a directed graph $G = (V, A)$, specified vertices $s, t in V$, a family of arc bundles $I_1, dots, I_k subset.eq A$ whose union covers $A$, positive bundle capacities $c_1, dots, c_k$, and a requirement $R in ZZ^+$, determine whether there exists an integral flow $f: A -> ZZ_(>= 0)$ such that (1) $sum_(a in I_j) f(a) <= c_j$ for every bundle $j$, (2) flow is conserved at every vertex in $V backslash {s, t}$, and (3) the net flow into $t$ is at least $R$. + ][ + Integral Flow with Bundles is the shared-capacity single-commodity flow problem listed as ND36 in Garey \& Johnson @garey1979. Sahni introduced it as one of a family of computationally related network problems and showed that the bundled-capacity variant is NP-complete even in a very sparse unit-capacity regime @sahni1974. + + The implementation keeps one non-negative integer variable per directed arc. Unlike ordinary max-flow, the usable range of an arc is not determined by an intrinsic per-arc capacity; it is bounded instead by the smallest bundle capacity among the bundles that contain that arc. The registered $O(2^m)$ catalog bound therefore reflects the unit-capacity case with $m = |A|$, which is exactly the regime highlighted by Garey \& Johnson and Sahni.#footnote[No exact worst-case algorithm improving on brute-force is claimed here for the bundled-capacity formulation.] + + *Example.* The canonical YES instance has source $s = v_#source$, sink $t = v_#sink$, and arcs $(0,1)$, $(0,2)$, $(1,3)$, $(2,3)$, $(1,2)$, $(2,1)$. The three bundles are $I_1 = {(0,1), (0,2)}$, $I_2 = {(1,3), (2,1)}$, and $I_3 = {(2,3), (1,2)}$, each with capacity 1. Sending one unit along the path $0 -> 1 -> 3$ yields the flow vector $(1, 0, 1, 0, 0, 0)$: bundle $I_1$ contributes $1 + 0 = 1$, bundle $I_2$ contributes $1 + 0 = 1$, bundle $I_3$ contributes $0 + 0 = 0$, and the only nonterminal vertices $v_1, v_2$ satisfy conservation. If the requirement is raised from $R = 1$ to $R = 2$, the same gadget becomes infeasible because $I_1$ caps the total outflow leaving the source at one unit. + + #pred-commands( + "pred create --example IntegralFlowBundles -o integral-flow-bundles.json", + "pred solve integral-flow-bundles.json", + "pred evaluate integral-flow-bundles.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure( + canvas(length: 1cm, { + import draw: * + let blue = graph-colors.at(0) + let orange = rgb("#f28e2b") + let teal = rgb("#76b7b2") + let gray = luma(185) + let positions = ( + (0, 0), + (2.2, 1.3), + (2.2, -1.3), + (4.4, 0), + ) + + line(positions.at(0), positions.at(1), stroke: (paint: blue, thickness: 2pt), mark: (end: "straight", scale: 0.5)) + line(positions.at(0), positions.at(2), stroke: (paint: blue.lighten(35%), thickness: 1.0pt), mark: (end: "straight", scale: 0.5)) + line(positions.at(1), positions.at(3), stroke: (paint: orange, thickness: 2pt), mark: (end: "straight", scale: 0.5)) + line(positions.at(2), positions.at(3), stroke: (paint: teal, thickness: 1.0pt), mark: (end: "straight", scale: 0.5)) + line((2.0, 1.0), (3.0, 0.0), (2.0, -1.0), stroke: (paint: teal, thickness: 1.0pt), mark: (end: "straight", scale: 0.5)) + line((2.4, -1.0), (1.4, 0.0), (2.4, 1.0), stroke: (paint: orange, thickness: 1.0pt), mark: (end: "straight", scale: 0.5)) + + for (i, pos) in positions.enumerate() { + let fill = if i == source { blue } else if i == sink { rgb("#e15759") } else { white } + g-node(pos, name: "ifb-" + str(i), fill: fill, label: if i == source or i == sink { text(fill: white)[$v_#i$] } else { [$v_#i$] }) + } + + content((1.0, 1.0), text(8pt, fill: blue)[$I_1, c = 1$]) + content((3.3, 1.0), text(8pt, fill: orange)[$I_2, c = 1$]) + content((3.3, -1.0), text(8pt, fill: teal)[$I_3, c = 1$]) + content((2.2, 1.8), text(8pt)[$f(0,1) = 1$]) + content((3.4, 1.55), text(8pt)[$f(1,3) = 1$]) + }), + caption: [Canonical YES instance for Integral Flow with Bundles. Thick blue/orange arcs carry the satisfying flow $0 -> 1 -> 3$, while the lighter arcs show the two unused alternatives coupled into bundles $I_1$, $I_2$, and $I_3$.], + ) + ] + ] +} + #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$, @@ -6658,6 +6717,27 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ For each item $i$, find the unique $j$ with $x_(i j) = 1$; assign item $i$ to bin $j$. ] +#reduction-rule("IntegralFlowBundles", "ILP")[ + The feasibility conditions are already linear: one integer variable per arc, one inequality per bundle, one conservation equality per nonterminal vertex, and one lower bound on sink inflow. +][ + _Construction._ Given Integral Flow with Bundles instance $(G = (V, A), s, t, (I_j, c_j)_(j=1)^k, R)$ with arc set $A = {a_0, dots, a_(m-1)}$, create one non-negative integer variable $x_i$ for each arc $a_i$. The ILP therefore has $m$ variables. + + _Bundle constraints._ For every bundle $I_j$, add + $sum_(a_i in I_j) x_i <= c_j$. + + _Flow conservation._ For every nonterminal vertex $v in V backslash {s, t}$, add + $sum_(a_i = (u, v) in A) x_i - sum_(a_i = (v, w) in A) x_i = 0$. + + _Requirement constraint._ Add the sink inflow lower bound + $sum_(a_i = (u, t) in A) x_i - sum_(a_i = (t, w) in A) x_i >= R$. + + _Objective._ Minimize 0. The target is a pure feasibility ILP, so any constant objective works. + + _Correctness._ ($arrow.r.double$) Any satisfying bundled flow assigns a non-negative integer to each arc, satisfies every bundle inequality by definition, satisfies every nonterminal conservation equality, and yields sink inflow at least $R$, so it is a feasible ILP solution. ($arrow.l.double$) Any feasible ILP solution gives non-negative integral arc values obeying the same bundle, conservation, and sink-inflow constraints, hence it is a satisfying solution to the original Integral Flow with Bundles instance. + + _Solution extraction._ Identity: read the ILP vector $(x_0, dots, x_(m-1))$ directly as the arc-flow vector of the source problem. +] + #reduction-rule("SequencingToMinimizeWeightedCompletionTime", "ILP")[ Completion times are natural integer variables, precedence constraints compare those completion times directly, and one binary order variable per task pair enforces that a single machine cannot overlap two jobs. ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index f24f1a092..b9b5e9bc7 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -255,6 +255,17 @@ @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}, + doi = {10.1137/0203021} +} + @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/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index b8d8d3632..ee39b82b4 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -12,10 +12,10 @@ use problemreductions::models::algebraic::{ }; use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ - GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, - IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, - MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, SteinerTree, - SteinerTreeInGraphs, StrongConnectivityAugmentation, + GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles, + LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, MinimumMultiwayCut, + MixedChinesePostman, MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs, + StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, @@ -760,9 +760,7 @@ fn help_flag_hint( ("ConsistencyOfDatabaseFrequencyTables", "known_values") => { "semicolon-separated triples: \"0,0,0;3,0,1;1,2,1\"" } - ("IntegralFlowBundles", "bundles") => { - "semicolon-separated groups: \"0,1;2,5;3,4\"" - } + ("IntegralFlowBundles", "bundles") => "semicolon-separated groups: \"0,1;2,5;3,4\"", ("IntegralFlowBundles", "bundle_capacities") => "comma-separated capacities: 1,1,1", ("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"", ("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => { @@ -1388,9 +1386,10 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // IntegralFlowBundles (directed graph + bundles + source/sink + requirement) "IntegralFlowBundles" => { let usage = "Usage: pred create IntegralFlowBundles --arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!("IntegralFlowBundles requires --arcs\n\n{usage}") - })?; + let arcs_str = args + .arcs + .as_deref() + .ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles 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 bundles = parse_bundles(args, num_arcs, usage)?; @@ -4538,9 +4537,10 @@ fn parse_partition_groups(args: &CreateArgs, num_arcs: usize) -> Result Result>> { - let bundles_str = args.bundles.as_deref().ok_or_else(|| { - anyhow::anyhow!("IntegralFlowBundles requires --bundles\n\n{usage}") - })?; + let bundles_str = args + .bundles + .as_deref() + .ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --bundles\n\n{usage}"))?; let bundles: Vec> = bundles_str .split(';') diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 17f7e25cf..eb60896dd 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -690,8 +690,14 @@ fn test_create_integral_flow_bundles() { assert_eq!(json["variant"], serde_json::json!({})); assert_eq!(json["data"]["graph"]["num_vertices"], 4); assert_eq!(json["data"]["graph"]["arcs"].as_array().unwrap().len(), 6); - assert_eq!(json["data"]["bundles"], serde_json::json!([[0, 1], [2, 5], [3, 4]])); - assert_eq!(json["data"]["bundle_capacities"], serde_json::json!([1, 1, 1])); + assert_eq!( + json["data"]["bundles"], + serde_json::json!([[0, 1], [2, 5], [3, 4]]) + ); + assert_eq!( + json["data"]["bundle_capacities"], + serde_json::json!([1, 1, 1]) + ); assert_eq!(json["data"]["source"], 0); assert_eq!(json["data"]["sink"], 3); assert_eq!(json["data"]["requirement"], 1); diff --git a/src/lib.rs b/src/lib.rs index 623665d0c..5b7700028 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, IntegralFlowBundles, IsomorphicSpanningTree, KClique, - KthBestSpanningTree, LengthBoundedDisjointPaths, MixedChinesePostman, SpinGlass, - SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, + HamiltonianPath, IntegralFlowBundles, IsomorphicSpanningTree, KClique, KthBestSpanningTree, + LengthBoundedDisjointPaths, MixedChinesePostman, SpinGlass, SteinerTree, + StrongConnectivityAugmentation, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index ca7ea9c1b..383c2171d 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -59,8 +59,14 @@ impl IntegralFlowBundles { let num_vertices = graph.num_vertices(); let num_arcs = graph.num_arcs(); - assert!(source < num_vertices, "source ({source}) >= num_vertices ({num_vertices})"); - assert!(sink < num_vertices, "sink ({sink}) >= num_vertices ({num_vertices})"); + assert!( + source < num_vertices, + "source ({source}) >= num_vertices ({num_vertices})" + ); + assert!( + sink < num_vertices, + "sink ({sink}) >= num_vertices ({num_vertices})" + ); assert!(source != sink, "source and sink must be distinct"); assert_eq!( bundles.len(), @@ -72,8 +78,13 @@ impl IntegralFlowBundles { let mut arc_covered = vec![false; num_arcs]; let mut arc_upper_bounds = vec![u64::MAX; num_arcs]; - for (bundle_index, (bundle, &capacity)) in bundles.iter().zip(&bundle_capacities).enumerate() { - assert!(capacity > 0, "bundle capacity at index {bundle_index} must be positive"); + for (bundle_index, (bundle, &capacity)) in + bundles.iter().zip(&bundle_capacities).enumerate() + { + assert!( + capacity > 0, + "bundle capacity at index {bundle_index} must be positive" + ); let mut seen = BTreeSet::new(); for &arc_index in bundle { @@ -91,7 +102,10 @@ impl IntegralFlowBundles { } for (arc_index, covered) in arc_covered.iter().copied().enumerate() { - assert!(covered, "arc {arc_index} must belong to at least one bundle"); + assert!( + covered, + "arc {arc_index} must belong to at least one bundle" + ); let domain = usize::try_from(arc_upper_bounds[arc_index]) .ok() .and_then(|bound| bound.checked_add(1)); diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index fe0abf2ae..1fd15c1fd 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -4,7 +4,7 @@ //! the bundle-capacity inequalities, flow-conservation equalities at //! nonterminals, and the sink inflow lower bound from the source problem. -use crate::models::algebraic::{ILP, LinearConstraint, ObjectiveSense}; +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowBundles; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -72,13 +72,15 @@ impl ReduceTo> for IntegralFlowBundles { sink_terms.push((arc_index, 1.0)); } } - constraints.push(LinearConstraint::ge( - sink_terms, - self.requirement() as f64, - )); + constraints.push(LinearConstraint::ge(sink_terms, self.requirement() as f64)); ReductionIFBToILP { - target: ILP::new(self.num_arcs(), constraints, vec![], ObjectiveSense::Minimize), + target: ILP::new( + self.num_arcs(), + constraints, + vec![], + ObjectiveSense::Minimize, + ), } } } @@ -93,10 +95,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( IntegralFlowBundles::new( - DirectedGraph::new( - 4, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)], - ), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), 0, 3, vec![vec![0, 1], vec![2, 5], vec![3, 4]], diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 5a9c6ba70..e24c1f74c 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -60,12 +60,12 @@ pub(crate) mod factoring_ilp; #[cfg(feature = "ilp-solver")] pub(crate) mod graphpartitioning_ilp; #[cfg(feature = "ilp-solver")] -pub(crate) mod integralflowbundles_ilp; -#[cfg(feature = "ilp-solver")] mod ilp_bool_ilp_i32; #[cfg(feature = "ilp-solver")] pub(crate) mod ilp_qubo; #[cfg(feature = "ilp-solver")] +pub(crate) mod integralflowbundles_ilp; +#[cfg(feature = "ilp-solver")] pub(crate) mod knapsack_ilp; #[cfg(feature = "ilp-solver")] pub(crate) mod longestcommonsubsequence_ilp; diff --git a/src/unit_tests/models/graph/integral_flow_bundles.rs b/src/unit_tests/models/graph/integral_flow_bundles.rs index 5614c7420..67a6595e5 100644 --- a/src/unit_tests/models/graph/integral_flow_bundles.rs +++ b/src/unit_tests/models/graph/integral_flow_bundles.rs @@ -93,5 +93,8 @@ fn test_integral_flow_bundles_serialization() { #[test] fn test_integral_flow_bundles_problem_name() { - assert_eq!(::NAME, "IntegralFlowBundles"); + assert_eq!( + ::NAME, + "IntegralFlowBundles" + ); } diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index b3af4904c..10455d9c7 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -84,7 +84,10 @@ fn test_integral_flow_bundles_to_ilp_closed_loop() { fn test_integral_flow_bundles_to_ilp_extract_solution_is_identity() { let problem = yes_instance(); let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem); - assert_eq!(reduction.extract_solution(&satisfying_config()), satisfying_config()); + assert_eq!( + reduction.extract_solution(&satisfying_config()), + satisfying_config() + ); } #[test] From 56ef4a3b4e66a173d780cef929fa90b39944e0e6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 04:32:02 +0800 Subject: [PATCH 6/6] chore: remove plan file after implementation --- .../plans/2026-03-22-integral-flow-bundles.md | 328 ------------------ 1 file changed, 328 deletions(-) delete mode 100644 docs/plans/2026-03-22-integral-flow-bundles.md diff --git a/docs/plans/2026-03-22-integral-flow-bundles.md b/docs/plans/2026-03-22-integral-flow-bundles.md deleted file mode 100644 index dbf4f2703..000000000 --- a/docs/plans/2026-03-22-integral-flow-bundles.md +++ /dev/null @@ -1,328 +0,0 @@ -# IntegralFlowBundles Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add `IntegralFlowBundles` for issue `#293`, including the model, direct ILP solver reduction, CLI/example-db support, and paper coverage. - -**Architecture:** Implement `IntegralFlowBundles` as a `SatisfactionProblem` under `src/models/misc/` with one integer decision variable per directed arc. Each arc domain is bounded by the minimum bundle capacity among bundles containing that arc; evaluation enforces bundle capacities, flow conservation at nonterminals, and required net inflow at the sink. Because the issue explicitly promises ILP solving, also add a direct `IntegralFlowBundles -> ILP` reduction with a canonical rule example and paper theorem in the same branch. - -**Tech Stack:** Rust workspace, inventory registry, Clap CLI, example-db fixtures, Typst paper, optional `ilp-solver` feature. - ---- - -## Batch 1: Implement the model, solver rule, tests, and CLI - -### Task 1: Add failing model tests and the minimal model scaffold - -**Files:** -- Create: `src/models/misc/integral_flow_bundles.rs` -- Create: `src/unit_tests/models/misc/integral_flow_bundles.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Write the failing test** - -Add model tests that pin down the issue-backed example and the repo review checklist: -- `test_integral_flow_bundles_creation_and_getters` -- `test_integral_flow_bundles_dims_use_tight_arc_bounds` -- `test_integral_flow_bundles_evaluate_yes_and_no_examples` -- `test_integral_flow_bundles_rejects_bad_bundle_sum_or_conservation` -- `test_integral_flow_bundles_serialization` - -The yes-instance should be the issue example: -- `num_vertices = 4` -- `arcs = [(0,1), (0,2), (1,3), (2,3), (1,2), (2,1)]` -- `bundles = [[0,1], [2,5], [3,4]]` -- `bundle_capacities = [1,1,1]` -- `source = 0`, `sink = 3`, `requirement = 1` -- satisfying config: `[1,0,1,0,0,0]` - -The no-instance should reuse the same data with `requirement = 2`. - -**Step 2: Run test to verify it fails** - -Run: -```bash -cargo test test_integral_flow_bundles_creation_and_getters --lib -``` - -Expected: FAIL because `IntegralFlowBundles` does not exist yet. - -**Step 3: Write minimal implementation** - -Create `src/models/misc/integral_flow_bundles.rs` with: -- `ProblemSchemaEntry` and `ProblemSizeFieldEntry` -- `IntegralFlowBundles` struct with fields: - - `num_vertices: usize` - - `arcs: Vec<(usize, usize)>` - - `source: usize` - - `sink: usize` - - `bundles: Vec>` - - `bundle_capacities: Vec` - - `requirement: u64` -- constructor validation: - - bundle count matches capacity count - - every arc index in every bundle is in bounds - - every arc appears in at least one bundle - - `source`/`sink` are valid and distinct - - each per-arc upper bound fits `usize` for `dims()` -- getters: - - `num_vertices()` - - `num_arcs()` - - `num_bundles()` - - accessors for all stored fields -- `Problem` impl: - - `Metric = bool` - - `dims()` returns one domain size per arc: `min(capacity over containing bundles) + 1` - - `evaluate()` checks config length, domain bounds, bundle sums, flow conservation, and net sink inflow -- `SatisfactionProblem` impl -- `declare_variants! { default sat IntegralFlowBundles => "2^num_arcs" }` -- `canonical_model_example_specs()` using the yes-instance above -- `#[cfg(test)]` link to the new unit test file - -Register the type in: -- `src/models/misc/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` - -**Step 4: Run test to verify it passes** - -Run: -```bash -cargo test integral_flow_bundles --lib -``` - -Expected: PASS for the new model tests. - -**Step 5: Commit** - -```bash -git add src/models/misc/integral_flow_bundles.rs src/unit_tests/models/misc/integral_flow_bundles.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs -git commit -m "Add IntegralFlowBundles model" -``` - -### Task 2: Add the direct ILP reduction and rule tests - -**Files:** -- Create: `src/rules/integralflowbundles_ilp.rs` -- Create: `src/unit_tests/rules/integralflowbundles_ilp.rs` -- Modify: `src/rules/mod.rs` - -**Step 1: Write the failing test** - -Add rule tests that cover both structure and closed-loop solving: -- `test_reduction_creates_valid_ilp` -- `test_integral_flow_bundles_to_ilp_closed_loop` -- `test_extract_solution_returns_arc_flows` -- `test_solve_reduced_integral_flow_bundles` - -Target behavior: -- one ILP variable per arc, integer and lower-bounded by `0` -- one bundle-capacity constraint per bundle -- one conservation equality per nonterminal vertex -- one sink-inflow inequality enforcing `>= requirement` -- objective can be zero/minimize-zero because the source problem is a satisfaction problem - -Use the canonical yes-instance from Task 1 as the closed-loop fixture. - -**Step 2: Run test to verify it fails** - -Run: -```bash -cargo test --features ilp-solver integralflowbundles_ilp -``` - -Expected: FAIL because the reduction does not exist yet. - -**Step 3: Write minimal implementation** - -Create `src/rules/integralflowbundles_ilp.rs`: -- reduce `IntegralFlowBundles` to `ILP` -- use one integer ILP variable `f_a` per arc -- add bundle constraints `sum_{a in I_j} f_a <= c_j` -- add conservation equalities for each `v != source, sink` -- add a sink-requirement inequality using incoming minus outgoing sink flow -- return identity extraction (`target_solution.to_vec()`) -- register overhead with: - - `num_vars = "num_arcs"` - - `num_constraints = "num_bundles + num_vertices - 2 + 1"` -- add `canonical_rule_example_specs()` based on the issue example - -Register the rule in `src/rules/mod.rs`. - -**Step 4: Run test to verify it passes** - -Run: -```bash -cargo test --features ilp-solver integralflowbundles_ilp -``` - -Expected: PASS for the new reduction tests. - -**Step 5: Commit** - -```bash -git add src/rules/integralflowbundles_ilp.rs src/unit_tests/rules/integralflowbundles_ilp.rs src/rules/mod.rs -git commit -m "Add IntegralFlowBundles to ILP reduction" -``` - -### Task 3: Add CLI creation support and example-db coverage - -**Files:** -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/tests/cli_tests.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `src/rules/mod.rs` - -**Step 1: Write the failing test** - -Add CLI tests for: -- `pred create IntegralFlowBundles --arcs "0>1,0>2,1>3,2>3,1>2,2>1" --bundles "0,1;2,5;3,4" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4` -- missing `--bundles` or `--bundle-capacities` -- `pred create --example IntegralFlowBundles` - -Also add or extend example-db assertions if needed so both: -- `find_model_example("IntegralFlowBundles")` -- `find_rule_example("IntegralFlowBundles" -> "ILP")` -remain covered by existing generic tests. - -**Step 2: Run test to verify it fails** - -Run: -```bash -cargo test -p problemreductions-cli integral_flow_bundles -``` - -Expected: FAIL because the CLI flags and create arm do not exist yet. - -**Step 3: Write minimal implementation** - -Update `problemreductions-cli/src/cli.rs`: -- add help-table entries for `IntegralFlowBundles` -- add `bundles: Option` and `bundle_capacities: Option` to `CreateArgs` -- treat both flags as data-bearing in `all_data_flags_empty()` - -Update `problemreductions-cli/src/commands/create.rs`: -- add usage/example strings for `IntegralFlowBundles` -- parse `--arcs` with the existing directed-arc helper -- parse `--bundles` as semicolon-separated groups of comma-separated arc indices -- parse `--bundle-capacities` as comma-separated nonnegative integers -- add the `IntegralFlowBundles` create arm and serialize the new problem -- verify `pred create --example IntegralFlowBundles` works via the example-db registration from Tasks 1 and 2 - -Use the registry-backed alias flow; do not add manual alias tables unless a concrete failing test proves they are still required. - -**Step 4: Run test to verify it passes** - -Run: -```bash -cargo test -p problemreductions-cli integral_flow_bundles -cargo test example_db --features "example-db ilp-solver" -``` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/tests/cli_tests.rs src/models/misc/mod.rs src/rules/mod.rs -git commit -m "Wire IntegralFlowBundles through CLI and example db" -``` - -## Batch 2: Paper and final verification - -### Task 4: Document the model and ILP reduction in the paper - -**Files:** -- Modify: `docs/paper/reductions.typ` - -**Step 1: Write the failing test** - -Add the paper content first, then use the paper build as the failing/passing gate: -- display name entry for `IntegralFlowBundles` -- `problem-def("IntegralFlowBundles")` with the formal bundled-flow definition -- example narrative tied to the canonical yes-instance and its satisfying config -- `pred-commands(...)` block using `pred create --example IntegralFlowBundles` -- `reduction-rule("IntegralFlowBundles", "ILP")` explaining the per-arc-variable ILP formulation - -**Step 2: Run test to verify it fails or exposes missing pieces** - -Run: -```bash -make paper -``` - -Expected: FAIL until the paper entry, citations, and example wiring are complete. - -**Step 3: Write minimal implementation** - -Add: -- `display-name["IntegralFlowBundles"]` -- model background with Garey-Johnson/Sahni citations -- the issue’s yes/no example, explicitly checking bundle sums and sink inflow -- a short ILP reduction theorem referencing the new solver rule - -Mirror the style of: -- `UndirectedTwoCommodityIntegralFlow` -- `BinPacking -> ILP` - -**Step 4: Run test to verify it passes** - -Run: -```bash -make paper -``` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add docs/paper/reductions.typ -git commit -m "Document IntegralFlowBundles and its ILP reduction" -``` - -### Task 5: Final repo verification before push - -**Files:** -- Modify: none expected - -**Step 1: Run the focused verification suite** - -Run: -```bash -cargo test integral_flow_bundles --lib -cargo test --features ilp-solver integralflowbundles_ilp -cargo test -p problemreductions-cli integral_flow_bundles -cargo test example_db --features "example-db ilp-solver" -``` - -Expected: PASS. - -**Step 2: Run the broader safety net** - -Run: -```bash -make check -make paper -``` - -Expected: PASS. - -**Step 3: Inspect the tree** - -Run: -```bash -git status --short -``` - -Expected: only intended tracked changes; no leftover `docs/plans/*.md` after the later cleanup commit. - -**Step 4: Commit any last adjustments** - -```bash -git add -A -git commit -m "Polish IntegralFlowBundles implementation" -```