From c5ac50dd8c4d76167e8ead35094ff3f253e11155 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 02:06:06 +0800 Subject: [PATCH 1/8] Add plan for #291: [Model] PathConstrainedNetworkFlow --- ...-22-path-constrained-network-flow-model.md | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 docs/plans/2026-03-22-path-constrained-network-flow-model.md diff --git a/docs/plans/2026-03-22-path-constrained-network-flow-model.md b/docs/plans/2026-03-22-path-constrained-network-flow-model.md new file mode 100644 index 000000000..e5cda431d --- /dev/null +++ b/docs/plans/2026-03-22-path-constrained-network-flow-model.md @@ -0,0 +1,187 @@ +# PathConstrainedNetworkFlow Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `PathConstrainedNetworkFlow` satisfaction model, expose it through registry/CLI/example-db flows, and document the canonical example in the Typst paper for issue #291. + +**Architecture:** Implement the model as a graph problem backed by `DirectedGraph`, explicit arc capacities, designated source/sink vertices, and a prescribed collection of s-t paths encoded as arc-index sequences. Reuse the existing satisfaction-problem + brute-force pattern from `DirectedTwoCommodityIntegralFlow`, then extend CLI creation and paper/example plumbing around that core representation. + +**Tech Stack:** Rust workspace crate, serde/inventory registry metadata, `pred` CLI, Typst paper, existing brute-force solver and example-db fixtures. + +--- + +### Task 1: Lock down the model behavior with failing tests + +**Files:** +- Create: `src/unit_tests/models/graph/path_constrained_network_flow.rs` +- Test: `src/unit_tests/models/graph/path_constrained_network_flow.rs` +- Reference: `src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs` +- Reference: `src/models/graph/directed_two_commodity_integral_flow.rs` + +**Step 1: Write the failing tests** + +Add tests that cover: +- constructor/accessor behavior for a YES instance based on the issue’s 5-path example +- `dims()` using per-path bottleneck capacities +- satisfying evaluation for one known feasible path-flow vector +- unsatisfying evaluation for requirement failure, capacity overflow, invalid config length, and invalid path descriptions +- brute-force solver behavior on the YES and NO instances +- serde round-trip +- paper/example consistency test that checks the canonical YES instance and verifies the expected satisfying config + +**Step 2: Run the targeted test to verify it fails** + +Run: `cargo test path_constrained_network_flow --lib` +Expected: FAIL because `PathConstrainedNetworkFlow` does not exist yet. + +**Step 3: Record the concrete example data in the test helpers** + +Use the fixed issue example from #291 / `/fix-issue`: +- 8 vertices, 10 directed arcs, capacities `[2,1,1,1,1,1,1,1,2,1]` +- 5 prescribed s-t paths encoded as arc-index lists +- YES requirement `3` +- NO requirement `4` + +**Step 4: Re-run the targeted test to confirm the failure is still the missing model, not a broken fixture** + +Run: `cargo test path_constrained_network_flow --lib` +Expected: FAIL with unresolved import / missing type, not malformed test data. + +### Task 2: Implement and register the model + +**Files:** +- Create: `src/models/graph/path_constrained_network_flow.rs` +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Implement the model** + +Create `PathConstrainedNetworkFlow` with: +- `graph: DirectedGraph` +- `capacities: Vec` +- `source: usize` +- `sink: usize` +- `paths: Vec>` +- `requirement: u64` + +Implement: +- `inventory::submit!` schema metadata +- constructor assertions for arc count, terminal bounds, and path validity +- accessors and size getters: `num_vertices`, `num_arcs`, `num_paths`, `max_capacity`, `requirement` +- helper(s) for path bottlenecks and prescribed-path validation +- `Problem` + `SatisfactionProblem` +- `declare_variants!` with `"((max_capacity + 1)^num_paths)"` or equivalent valid expression using the existing getter names +- canonical example spec for the YES instance + +**Step 2: Register the model** + +Wire the new type through: +- `src/models/graph/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` prelude exports +- graph example-db spec chain in `src/models/graph/mod.rs` + +**Step 3: Run the targeted tests** + +Run: `cargo test path_constrained_network_flow --lib` +Expected: PASS for the new model tests. + +**Step 4: Refactor only if needed** + +Keep refactors local to helper extraction inside the new model/test files. Do not broaden scope beyond what the tests demand. + +### Task 3: Add CLI creation support for prescribed paths + +**Files:** +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/problem_name.rs` (only if alias coverage is missing after schema registration) + +**Step 1: Write or extend failing CLI-facing tests first** + +Add focused tests for: +- help text / example text exposing `PathConstrainedNetworkFlow` +- parsing a valid `pred create PathConstrainedNetworkFlow ... --paths ...` +- rejecting malformed `--paths` input and invalid arc references + +**Step 2: Run the targeted CLI tests to verify failure** + +Run: `cargo test create::tests::path_constrained_network_flow --package problemreductions-cli` +Expected: FAIL because the CLI does not yet know the new problem or `--paths`. + +**Step 3: Implement CLI support** + +Add: +- `CreateArgs.paths: Option` +- `all_data_flags_empty()` coverage for the new flag +- help-table entry and example string +- parser/helper for semicolon-separated path lists where each path is a comma-separated arc-index sequence +- `create()` match arm that builds `DirectedGraph` from `--arcs`, parses capacities / source / sink / paths / requirement, and serializes the new model + +Prefer the existing directed-graph conventions: +- use `--arcs`, not `--graph` +- use singular `--requirement` +- use `--paths "0,2,5,8;1,3,6,8;..."` to match the model’s constructor shape + +**Step 4: Re-run the targeted CLI tests** + +Run: `cargo test create::tests::path_constrained_network_flow --package problemreductions-cli` +Expected: PASS. + +### Task 4: Wire the canonical example and paper entry + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Verify via existing example-db exports driven from model registration +- Reference: `.claude/skills/write-model-in-paper/SKILL.md` + +**Step 1: Verify the canonical example is the same issue fixture** + +Use the example from Task 1 / model registration as the single source of truth for: +- `canonical_model_example_specs()` +- paper example text +- `pred-commands()` snippet + +**Step 2: Update the paper** + +Add: +- display-name entry for `PathConstrainedNetworkFlow` +- `problem-def("PathConstrainedNetworkFlow")` +- cited background using Garey & Johnson ND34 plus the published Büsing–Stiller 2011 reference +- a worked YES example that explains the feasible integral path-flow assignment +- `pred-commands()` that uses the canonical example fixture and brute-force solving + +**Step 3: Build the paper to confirm the entry is valid** + +Run: `make paper` +Expected: PASS. + +### Task 5: Full verification, PR summary, and cleanup + +**Files:** +- Modify: PR body/comment generated during pipeline execution + +**Step 1: Run the required verification commands** + +Run: +- `cargo test path_constrained_network_flow --lib` +- `cargo test create::tests::path_constrained_network_flow --package problemreductions-cli` +- `make test` +- `make clippy` +- `make paper` + +**Step 2: Inspect the tree** + +Run: `git status --short` +Expected: only intentional tracked changes; no leftover plan artifacts after cleanup. + +**Step 3: Call out the pipeline-specific deviation** + +In the PR body or implementation summary comment, note: +- `` +- the current repo search found no open rule issue referencing `PathConstrainedNetworkFlow` + +**Step 4: Commit, remove the plan file, push, and post the implementation summary** + +Follow `.claude/skills/issue-to-pr/SKILL.md` Step 7b-7d exactly after the implementation is verified. From fd0e829aefbe7e01e2509088649a441310185885 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 02:24:52 +0800 Subject: [PATCH 2/8] Implement #291: [Model] PathConstrainedNetworkFlow --- docs/paper/reductions.typ | 94 ++++++ docs/paper/references.bib | 11 + problemreductions-cli/src/cli.rs | 7 + problemreductions-cli/src/commands/create.rs | 183 +++++++++++- src/lib.rs | 4 +- src/models/graph/mod.rs | 4 + .../graph/path_constrained_network_flow.rs | 282 ++++++++++++++++++ src/models/mod.rs | 20 +- .../graph/path_constrained_network_flow.rs | 124 ++++++++ 9 files changed, 715 insertions(+), 14 deletions(-) create mode 100644 src/models/graph/path_constrained_network_flow.rs create mode 100644 src/unit_tests/models/graph/path_constrained_network_flow.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 3e8213c9d..3449255d8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -74,6 +74,7 @@ "HamiltonianPath": [Hamiltonian Path], "ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path], "UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow], + "PathConstrainedNetworkFlow": [Path-Constrained Network Flow], "LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths], "IsomorphicSpanningTree": [Isomorphic Spanning Tree], "KthBestSpanningTree": [Kth Best Spanning Tree], @@ -1051,6 +1052,99 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ] ] } +#{ + let x = load-model-example("PathConstrainedNetworkFlow") + let arcs = x.instance.graph.arcs.map(a => (a.at(0), a.at(1))) + let requirement = x.instance.requirement + let p1 = (0, 2, 5, 8) + let p2 = (0, 3, 6, 8) + let p5 = (1, 4, 7, 9) + [ + #problem-def("PathConstrainedNetworkFlow")[ + Given a directed graph $G = (V, A)$, designated vertices $s, t in V$, arc capacities $c: A -> ZZ^+$, a prescribed collection $cal(P)$ of directed simple $s$-$t$ paths, and a requirement $R in ZZ^+$, determine whether there exists an integral path-flow function $g: cal(P) -> ZZ_(>= 0)$ such that $sum_(p in cal(P): a in p) g(p) <= c(a)$ for every arc $a in A$ and $sum_(p in cal(P)) g(p) >= R$. + ][ + Path-Constrained Network Flow appears as problem ND34 in Garey \& Johnson @garey1979. Unlike ordinary single-commodity flow, the admissible routes are fixed in advance: every unit of flow must be assigned to one of the listed $s$-$t$ paths. This prescribed-path viewpoint is standard in line planning and unsplittable routing, and Büsing and Stiller give a modern published NP-completeness and inapproximability treatment for exactly this integral formulation @busingstiller2011. + + The implementation uses one integer variable per prescribed path, bounded by that path's bottleneck capacity. Exhaustive search over those path-flow variables gives the registered worst-case bound $O^*((C + 1)^(|cal(P)|))$, where $C = max_(a in A) c(a)$. #footnote[This is the brute-force bound induced by the representation used in the library; no sharper general exact algorithm is claimed here for the integral prescribed-path formulation.] + + *Example.* The canonical fixture uses the directed network with arcs $(0,1)$, $(0,2)$, $(1,3)$, $(1,4)$, $(2,4)$, $(3,5)$, $(4,5)$, $(4,6)$, $(5,7)$, and $(6,7)$, capacities $(2,1,1,1,1,1,1,1,2,1)$, source $s = 0$, sink $t = 7$, and required flow $R = #requirement$. The prescribed paths are $p_1 = 0 arrow 1 arrow 3 arrow 5 arrow 7$, $p_2 = 0 arrow 1 arrow 4 arrow 5 arrow 7$, $p_3 = 0 arrow 1 arrow 4 arrow 6 arrow 7$, $p_4 = 0 arrow 2 arrow 4 arrow 5 arrow 7$, and $p_5 = 0 arrow 2 arrow 4 arrow 6 arrow 7$. The fixture's satisfying configuration is $g = (#x.optimal_config.at(0), #x.optimal_config.at(1), #x.optimal_config.at(2), #x.optimal_config.at(3), #x.optimal_config.at(4)) = (1, 1, 0, 0, 1)$, so one unit is sent along $p_1$, one along $p_2$, and one along $p_5$. The shared arcs $(0,1)$ and $(5,7)$ each carry exactly two units of flow, matching their capacity 2, while every other used arc carries one unit. Therefore the total flow into $t$ is $3 = R$, so the instance is feasible. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o path-constrained-network-flow.json", + "pred solve path-constrained-network-flow.json --solver brute-force", + "pred evaluate path-constrained-network-flow.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure( + canvas(length: 0.95cm, { + import draw: * + let blue = graph-colors.at(0) + let orange = rgb("#f28e2b") + let teal = rgb("#76b7b2") + let gray = luma(185) + let verts = ( + (0, 0), + (1.4, 1.2), + (1.4, -1.2), + (2.8, 1.9), + (2.8, 0), + (4.2, 1.2), + (4.2, -1.2), + (5.6, 0), + ) + for (u, v) in arcs { + line( + verts.at(u), + verts.at(v), + stroke: 0.8pt + gray, + mark: (end: "straight", scale: 0.45), + ) + } + for idx in p1 { + let (u, v) = arcs.at(idx) + line( + verts.at(u), + verts.at(v), + stroke: 1.8pt + blue, + mark: (end: "straight", scale: 0.5), + ) + } + for idx in p2 { + let (u, v) = arcs.at(idx) + line( + verts.at(u), + verts.at(v), + stroke: (paint: orange, thickness: 1.7pt, dash: "dashed"), + mark: (end: "straight", scale: 0.48), + ) + } + for idx in p5 { + let (u, v) = arcs.at(idx) + line( + verts.at(u), + verts.at(v), + stroke: 1.6pt + teal, + mark: (end: "straight", scale: 0.46), + ) + } + for (i, pos) in verts.enumerate() { + let fill = if i == 0 or i == 7 { rgb("#e15759").lighten(75%) } else { white } + g-node(pos, name: "pcnf-" + str(i), fill: fill, label: [$v_#i$]) + } + content((0.65, 0.78), text(8pt, fill: gray)[$2 / 2$]) + content((4.9, 0.78), text(8pt, fill: gray)[$2 / 2$]) + line((0.2, -2.15), (0.8, -2.15), stroke: 1.8pt + blue, mark: (end: "straight", scale: 0.42)) + content((1.15, -2.15), text(8pt)[$p_1$]) + line((1.95, -2.15), (2.55, -2.15), stroke: (paint: orange, thickness: 1.7pt, dash: "dashed"), mark: (end: "straight", scale: 0.42)) + content((2.9, -2.15), text(8pt)[$p_2$]) + line((3.75, -2.15), (4.35, -2.15), stroke: 1.6pt + teal, mark: (end: "straight", scale: 0.42)) + content((4.7, -2.15), text(8pt)[$p_5$]) + }), + caption: [Canonical YES instance for Path-Constrained Network Flow. Blue, dashed orange, and teal show the three prescribed paths used by $g = (1, 1, 0, 0, 1)$. The labels $2 / 2$ mark the shared arcs $(0,1)$ and $(5,7)$, whose flow exactly saturates capacity 2.], + ) + ] + ] +} #{ let x = load-model-example("IsomorphicSpanningTree") let g-edges = x.instance.graph.edges diff --git a/docs/paper/references.bib b/docs/paper/references.bib index f24f1a092..a08e622fb 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -180,6 +180,17 @@ @book{garey1979 year = {1979} } +@article{busingstiller2011, + author = {Christina Büsing and Sebastian Stiller}, + title = {Line planning, path constrained network flow and inapproximability}, + journal = {Networks}, + volume = {57}, + number = {1}, + pages = {106--113}, + year = {2011}, + doi = {10.1002/net.20386} +} + @article{bruckerGareyJohnson1977, author = {Peter Brucker and Michael R. Garey and David S. Johnson}, title = {Scheduling equal-length tasks under tree-like precedence constraints to minimize maximum lateness}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index f10cb96b4..6dc115573 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -236,6 +236,7 @@ Flags by problem type: IsomorphicSpanningTree --graph, --tree KthBestSpanningTree --graph, --edge-weights, --k, --bound LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound + PathConstrainedNetworkFlow --arcs, --capacities, --source, --sink, --paths, --requirement Factoring --target, --m, --n BinPacking --sizes, --capacity SubsetSum --sizes, --target @@ -364,6 +365,9 @@ pub struct CreateArgs { /// Required number of paths for LengthBoundedDisjointPaths #[arg(long)] pub num_paths_required: Option, + /// Prescribed directed s-t paths as semicolon-separated arc-index sequences (e.g., "0,2,5;1,4,6") + #[arg(long)] + pub paths: Option, /// Pairwise couplings J_ij for SpinGlass (e.g., 1,-1,1) [default: all 1s] #[arg(long)] pub couplings: Option, @@ -434,6 +438,9 @@ pub struct CreateArgs { /// Required flow R_2 for commodity 2 #[arg(long)] pub requirement_2: Option, + /// Required total flow R for PathConstrainedNetworkFlow + #[arg(long)] + pub requirement: Option, /// Item sizes for BinPacking (comma-separated, e.g., "3,3,2,2") #[arg(long)] pub sizes: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 955aa441f..ede305a07 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -14,7 +14,8 @@ use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, LengthBoundedDisjointPaths, MinimumCutIntoBoundedSets, MinimumMultiwayCut, MixedChinesePostman, - MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, + MultipleChoiceBranching, PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, + StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, @@ -52,6 +53,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.source.is_none() && args.sink.is_none() && args.num_paths_required.is_none() + && args.paths.is_none() && args.couplings.is_none() && args.fields.is_none() && args.clauses.is_none() @@ -74,6 +76,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.sink_2.is_none() && args.requirement_1.is_none() && args.requirement_2.is_none() + && args.requirement.is_none() && args.sizes.is_none() && args.capacity.is_none() && args.sequence.is_none() @@ -525,6 +528,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "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" } + "PathConstrainedNetworkFlow" => { + "--arcs \"0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7\" --capacities 2,1,1,1,1,1,1,1,2,1 --source 0 --sink 7 --paths \"0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9\" --requirement 3" + } "IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2", "KthBestSpanningTree" => "--graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3", "BottleneckTravelingSalesman" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { @@ -749,6 +755,9 @@ fn help_flag_hint( ("ConsistencyOfDatabaseFrequencyTables", "known_values") => { "semicolon-separated triples: \"0,0,0;3,0,1;1,2,1\"" } + ("PathConstrainedNetworkFlow", "paths") => { + "semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\"" + } ("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\"" @@ -1469,7 +1478,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))?, @@ -3058,6 +3069,47 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // PathConstrainedNetworkFlow + "PathConstrainedNetworkFlow" => { + let usage = "Usage: pred create PathConstrainedNetworkFlow --arcs \"0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7\" --capacities 2,1,1,1,1,1,1,1,2,1 --source 0 --sink 7 --paths \"0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9\" --requirement 3"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("PathConstrainedNetworkFlow 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: Vec = if let Some(ref s) = args.capacities { + util::parse_comma_list(s)? + } else { + vec![1; num_arcs] + }; + anyhow::ensure!( + capacities.len() == num_arcs, + "capacities length ({}) must match number of arcs ({num_arcs})", + capacities.len() + ); + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("PathConstrainedNetworkFlow requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("PathConstrainedNetworkFlow requires --sink\n\n{usage}") + })?; + let requirement = args.requirement.ok_or_else(|| { + anyhow::anyhow!("PathConstrainedNetworkFlow requires --requirement\n\n{usage}") + })?; + let paths = parse_prescribed_paths(args, num_arcs, usage)?; + ( + ser(PathConstrainedNetworkFlow::new( + graph, + capacities, + source, + sink, + paths, + requirement, + ))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackArcSet "MinimumFeedbackArcSet" => { let arcs_str = args.arcs.as_deref().ok_or_else(|| { @@ -4874,6 +4926,40 @@ fn parse_directed_graph( Ok((DirectedGraph::new(num_v, arcs), num_arcs)) } +fn parse_prescribed_paths( + args: &CreateArgs, + num_arcs: usize, + usage: &str, +) -> Result>> { + let paths_str = args + .paths + .as_deref() + .ok_or_else(|| anyhow::anyhow!("PathConstrainedNetworkFlow requires --paths\n\n{usage}"))?; + + paths_str + .split(';') + .map(|path_str| { + let trimmed = path_str.trim(); + anyhow::ensure!( + !trimmed.is_empty(), + "PathConstrainedNetworkFlow paths must be non-empty\n\n{usage}" + ); + let path: Vec = util::parse_comma_list(trimmed)?; + anyhow::ensure!( + !path.is_empty(), + "PathConstrainedNetworkFlow paths must be non-empty\n\n{usage}" + ); + for &arc_idx in &path { + anyhow::ensure!( + arc_idx < num_arcs, + "Path arc index {arc_idx} out of bounds for {num_arcs} arcs\n\n{usage}" + ); + } + Ok(path) + }) + .collect() +} + fn parse_mixed_graph(args: &CreateArgs, usage: &str) -> Result { let (undirected_graph, num_vertices) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; @@ -5558,6 +5644,90 @@ mod tests { std::fs::remove_file(output_path).unwrap(); } + #[test] + fn test_create_path_constrained_network_flow_outputs_problem_json() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "PathConstrainedNetworkFlow", + "--arcs", + "0>1,0>2,1>3,1>4,2>4,3>5,4>5,4>6,5>7,6>7", + "--capacities", + "2,1,1,1,1,1,1,1,2,1", + "--source", + "0", + "--sink", + "7", + "--paths", + "0,2,5,8;0,3,6,8;0,3,7,9;1,4,6,8;1,4,7,9", + "--requirement", + "3", + ]) + .expect("parse create command"); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let output_path = temp_output_path("path_constrained_network_flow"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).expect("create PathConstrainedNetworkFlow JSON"); + + let created: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output_path).unwrap()).unwrap(); + fs::remove_file(output_path).ok(); + + assert_eq!(created["type"], "PathConstrainedNetworkFlow"); + assert_eq!(created["data"]["source"], 0); + assert_eq!(created["data"]["sink"], 7); + assert_eq!(created["data"]["requirement"], 3); + assert_eq!(created["data"]["paths"][0], serde_json::json!([0, 2, 5, 8])); + } + + #[test] + fn test_create_path_constrained_network_flow_rejects_invalid_paths() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "PathConstrainedNetworkFlow", + "--arcs", + "0>1,1>2,2>3", + "--capacities", + "1,1,1", + "--source", + "0", + "--sink", + "3", + "--paths", + "0,3", + "--requirement", + "1", + ]) + .expect("parse create command"); + + let args = match cli.command { + Commands::Create(args) => args, + _ => panic!("expected create command"), + }; + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("out of bounds") || err.contains("not contiguous")); + } + #[test] fn test_create_staff_scheduling_reports_invalid_schedule_without_panic() { let cli = Cli::try_parse_from([ @@ -5609,6 +5779,13 @@ mod tests { ); } + #[test] + fn test_example_for_path_constrained_network_flow_mentions_paths_flag() { + let example = example_for("PathConstrainedNetworkFlow", None); + assert!(example.contains("--paths")); + assert!(example.contains("--requirement")); + } + #[test] fn test_create_timetable_design_outputs_problem_json() { let cli = Cli::try_parse_from([ @@ -5812,6 +5989,7 @@ mod tests { source: None, sink: None, num_paths_required: None, + paths: None, couplings: None, fields: None, clauses: None, @@ -5835,6 +6013,7 @@ mod tests { sink_2: None, requirement_1: None, requirement_2: None, + requirement: None, sizes: None, capacity: None, sequence: None, diff --git a/src/lib.rs b/src/lib.rs index 5d7f10b32..15e6b9672 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,8 +60,8 @@ pub mod prelude { MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, - PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, - ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, + PartitionIntoPathsOfLength2, PartitionIntoTriangles, PathConstrainedNetworkFlow, + RuralPostman, ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index dbdb632f8..18e6451dd 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -37,6 +37,7 @@ //! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) //! - [`MultipleChoiceBranching`]: Directed branching with partition constraints //! - [`LengthBoundedDisjointPaths`]: Length-bounded internally disjoint s-t paths +//! - [`PathConstrainedNetworkFlow`]: Integral flow on a prescribed collection of directed s-t paths //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) //! - [`MixedChinesePostman`]: Mixed-graph postman tour with bounded total length //! - [`SteinerTree`]: Minimum-weight tree spanning all required terminals @@ -80,6 +81,7 @@ pub(crate) mod multiple_copy_file_allocation; pub(crate) mod optimal_linear_arrangement; pub(crate) mod partition_into_paths_of_length_2; pub(crate) mod partition_into_triangles; +pub(crate) mod path_constrained_network_flow; pub(crate) mod rural_postman; pub(crate) mod shortest_weight_constrained_path; pub(crate) mod spin_glass; @@ -125,6 +127,7 @@ pub use multiple_copy_file_allocation::MultipleCopyFileAllocation; pub use optimal_linear_arrangement::OptimalLinearArrangement; pub use partition_into_paths_of_length_2::PartitionIntoPathsOfLength2; pub use partition_into_triangles::PartitionIntoTriangles; +pub use path_constrained_network_flow::PathConstrainedNetworkFlow; pub use rural_postman::RuralPostman; pub use shortest_weight_constrained_path::ShortestWeightConstrainedPath; pub use spin_glass::SpinGlass; @@ -171,6 +174,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Capacity c(a) for each arc" }, + FieldInfo { name: "source", type_name: "usize", description: "Source vertex s" }, + FieldInfo { name: "sink", type_name: "usize", description: "Sink vertex t" }, + FieldInfo { name: "paths", type_name: "Vec>", description: "Prescribed directed s-t paths as arc-index sequences" }, + FieldInfo { name: "requirement", type_name: "u64", description: "Required total flow R" }, + ], + } +} + +/// Path-Constrained Network Flow. +/// +/// A configuration contains one integer variable per prescribed path. If +/// `config[i] = x`, then `x` units of flow are routed along the i-th prescribed +/// path. A configuration is feasible when: +/// - each path variable stays within its bottleneck capacity +/// - the induced arc loads do not exceed the arc capacities +/// - the total delivered flow reaches the requirement +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PathConstrainedNetworkFlow { + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + paths: Vec>, + requirement: u64, +} + +impl PathConstrainedNetworkFlow { + /// Create a new Path-Constrained Network Flow instance. + /// + /// # Panics + /// + /// Panics if: + /// - `capacities.len() != graph.num_arcs()` + /// - `source` or `sink` are out of range or identical + /// - any prescribed path is not a valid directed simple s-t path + pub fn new( + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + paths: Vec>, + requirement: u64, + ) -> Self { + let num_vertices = graph.num_vertices(); + assert_eq!( + capacities.len(), + graph.num_arcs(), + "capacities length must match graph num_arcs" + ); + assert!( + source < num_vertices, + "source ({source}) >= num_vertices ({num_vertices})" + ); + assert!( + sink < num_vertices, + "sink ({sink}) >= num_vertices ({num_vertices})" + ); + assert_ne!(source, sink, "source and sink must be distinct"); + + for path in &paths { + Self::assert_valid_path(&graph, path, source, sink); + } + + Self { + graph, + capacities, + source, + sink, + paths, + requirement, + } + } + + fn assert_valid_path(graph: &DirectedGraph, path: &[usize], source: usize, sink: usize) { + assert!(!path.is_empty(), "prescribed paths must be non-empty"); + + let arcs = graph.arcs(); + let mut visited_vertices = HashSet::from([source]); + let mut current = source; + + for &arc_idx in path { + let &(tail, head) = arcs + .get(arc_idx) + .unwrap_or_else(|| panic!("path arc index {arc_idx} out of bounds")); + assert_eq!( + tail, current, + "prescribed path is not contiguous: expected arc leaving vertex {current}, got {tail}->{head}" + ); + assert!( + visited_vertices.insert(head), + "prescribed path repeats vertex {head}, so it is not a simple path" + ); + current = head; + } + + assert_eq!( + current, sink, + "prescribed path must end at sink {sink}, ended at {current}" + ); + } + + fn path_bottleneck(&self, path: &[usize]) -> u64 { + path.iter() + .map(|&arc_idx| self.capacities[arc_idx]) + .min() + .unwrap_or(0) + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + /// Get the arc capacities. + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + /// Get the prescribed path collection. + pub fn paths(&self) -> &[Vec] { + &self.paths + } + + /// Get the source vertex. + pub fn source(&self) -> usize { + self.source + } + + /// Get the sink vertex. + pub fn sink(&self) -> usize { + self.sink + } + + /// Get the required total flow. + pub fn requirement(&self) -> u64 { + self.requirement + } + + /// Update the required total flow. + pub fn set_requirement(&mut self, requirement: u64) { + self.requirement = 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 prescribed paths. + pub fn num_paths(&self) -> usize { + self.paths.len() + } + + /// Get the maximum arc capacity. + pub fn max_capacity(&self) -> u64 { + self.capacities.iter().copied().max().unwrap_or(0) + } + + /// Check whether a path-flow assignment is feasible. + pub fn is_feasible(&self, config: &[usize]) -> bool { + if config.len() != self.paths.len() { + return false; + } + + let mut arc_loads = vec![0_u64; self.capacities.len()]; + let mut total_flow = 0_u64; + + for (flow_value, path) in config.iter().copied().zip(&self.paths) { + let path_flow = flow_value as u64; + if path_flow > self.path_bottleneck(path) { + return false; + } + + total_flow += path_flow; + for &arc_idx in path { + arc_loads[arc_idx] += path_flow; + if arc_loads[arc_idx] > self.capacities[arc_idx] { + return false; + } + } + } + + total_flow >= self.requirement + } +} + +impl Problem for PathConstrainedNetworkFlow { + const NAME: &'static str = "PathConstrainedNetworkFlow"; + type Metric = bool; + + fn dims(&self) -> Vec { + self.paths + .iter() + .map(|path| (self.path_bottleneck(path) as usize) + 1) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.is_feasible(config) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for PathConstrainedNetworkFlow {} + +crate::declare_variants! { + default sat PathConstrainedNetworkFlow => "(max_capacity + 1)^num_paths", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "path_constrained_network_flow", + instance: Box::new(PathConstrainedNetworkFlow::new( + DirectedGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (1, 3), + (1, 4), + (2, 4), + (3, 5), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + ], + ), + vec![2, 1, 1, 1, 1, 1, 1, 1, 2, 1], + 0, + 7, + vec![ + vec![0, 2, 5, 8], + vec![0, 3, 6, 8], + vec![0, 3, 7, 9], + vec![1, 4, 6, 8], + vec![1, 4, 7, 9], + ], + 3, + )), + optimal_config: vec![1, 1, 0, 0, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/path_constrained_network_flow.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 2d6ebfe4e..7e710fbac 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -20,16 +20,16 @@ pub use formula::{ pub use graph::{ AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, BottleneckTravelingSalesman, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, - GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, - IsomorphicSpanningTree, KClique, KColoring, KthBestSpanningTree, - LengthBoundedDisjointPaths, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, - MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, - MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, - MinimumVertexCover, MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, - OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, - ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, - UndirectedTwoCommodityIntegralFlow, + GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IsomorphicSpanningTree, + KClique, KColoring, KthBestSpanningTree, LengthBoundedDisjointPaths, MaxCut, MaximalIS, + MaximumClique, MaximumIndependentSet, MaximumMatching, MinMaxMulticenter, + MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackArcSet, + MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, + MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, + OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, + PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath, SpinGlass, + SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, SubgraphIsomorphism, + TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ diff --git a/src/unit_tests/models/graph/path_constrained_network_flow.rs b/src/unit_tests/models/graph/path_constrained_network_flow.rs new file mode 100644 index 000000000..ec0933f6d --- /dev/null +++ b/src/unit_tests/models/graph/path_constrained_network_flow.rs @@ -0,0 +1,124 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::DirectedGraph; +use crate::traits::Problem; + +fn yes_instance() -> PathConstrainedNetworkFlow { + let graph = DirectedGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (1, 3), + (1, 4), + (2, 4), + (3, 5), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + ], + ); + + PathConstrainedNetworkFlow::new( + graph, + vec![2, 1, 1, 1, 1, 1, 1, 1, 2, 1], + 0, + 7, + vec![ + vec![0, 2, 5, 8], + vec![0, 3, 6, 8], + vec![0, 3, 7, 9], + vec![1, 4, 6, 8], + vec![1, 4, 7, 9], + ], + 3, + ) +} + +fn no_instance() -> PathConstrainedNetworkFlow { + let mut problem = yes_instance(); + problem.set_requirement(4); + problem +} + +#[test] +fn test_path_constrained_network_flow_creation() { + let problem = yes_instance(); + assert_eq!(problem.num_vertices(), 8); + assert_eq!(problem.num_arcs(), 10); + assert_eq!(problem.num_paths(), 5); + assert_eq!(problem.max_capacity(), 2); + assert_eq!(problem.requirement(), 3); + assert_eq!(problem.source(), 0); + assert_eq!(problem.sink(), 7); +} + +#[test] +fn test_path_constrained_network_flow_dims_use_path_bottlenecks() { + let problem = yes_instance(); + assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2]); +} + +#[test] +fn test_path_constrained_network_flow_evaluation_satisfying() { + let problem = yes_instance(); + assert!(problem.evaluate(&[1, 1, 0, 0, 1])); + assert!(problem.evaluate(&[1, 0, 1, 1, 0])); +} + +#[test] +fn test_path_constrained_network_flow_evaluation_unsatisfying() { + let problem = yes_instance(); + assert!(!problem.evaluate(&[1, 1, 0, 0, 0])); + assert!(!problem.evaluate(&[1, 1, 1, 0, 0])); + assert!(!problem.evaluate(&[1, 1, 0, 0])); +} + +#[test] +fn test_path_constrained_network_flow_solver_yes_and_no() { + let yes = yes_instance(); + let no = no_instance(); + let solver = BruteForce::new(); + + let satisfying = solver.find_all_satisfying(&yes); + assert_eq!(satisfying.len(), 2); + assert!(satisfying.iter().all(|config| yes.evaluate(config))); + + assert!(solver.find_satisfying(&no).is_none()); +} + +#[test] +fn test_path_constrained_network_flow_serialization() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let restored: PathConstrainedNetworkFlow = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.num_vertices(), 8); + assert_eq!(restored.num_arcs(), 10); + assert_eq!(restored.num_paths(), 5); + assert_eq!(restored.requirement(), 3); +} + +#[test] +fn test_path_constrained_network_flow_rejects_invalid_prescribed_paths() { + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + + let result = std::panic::catch_unwind(|| { + PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1], 0, 3, vec![vec![0, 2]], 1) + }); + + assert!(result.is_err()); +} + +#[test] +fn test_path_constrained_network_flow_paper_example() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let config = vec![1, 1, 0, 0, 1]; + + assert!(problem.evaluate(&config)); + + let all = solver.find_all_satisfying(&problem); + assert_eq!(all.len(), 2); + assert!(all.contains(&config)); +} From c6388c44d26f916bdda5a1d49d467188f406ea73 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 02:24:56 +0800 Subject: [PATCH 3/8] chore: remove plan file after implementation --- ...-22-path-constrained-network-flow-model.md | 187 ------------------ 1 file changed, 187 deletions(-) delete mode 100644 docs/plans/2026-03-22-path-constrained-network-flow-model.md diff --git a/docs/plans/2026-03-22-path-constrained-network-flow-model.md b/docs/plans/2026-03-22-path-constrained-network-flow-model.md deleted file mode 100644 index e5cda431d..000000000 --- a/docs/plans/2026-03-22-path-constrained-network-flow-model.md +++ /dev/null @@ -1,187 +0,0 @@ -# PathConstrainedNetworkFlow Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `PathConstrainedNetworkFlow` satisfaction model, expose it through registry/CLI/example-db flows, and document the canonical example in the Typst paper for issue #291. - -**Architecture:** Implement the model as a graph problem backed by `DirectedGraph`, explicit arc capacities, designated source/sink vertices, and a prescribed collection of s-t paths encoded as arc-index sequences. Reuse the existing satisfaction-problem + brute-force pattern from `DirectedTwoCommodityIntegralFlow`, then extend CLI creation and paper/example plumbing around that core representation. - -**Tech Stack:** Rust workspace crate, serde/inventory registry metadata, `pred` CLI, Typst paper, existing brute-force solver and example-db fixtures. - ---- - -### Task 1: Lock down the model behavior with failing tests - -**Files:** -- Create: `src/unit_tests/models/graph/path_constrained_network_flow.rs` -- Test: `src/unit_tests/models/graph/path_constrained_network_flow.rs` -- Reference: `src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs` -- Reference: `src/models/graph/directed_two_commodity_integral_flow.rs` - -**Step 1: Write the failing tests** - -Add tests that cover: -- constructor/accessor behavior for a YES instance based on the issue’s 5-path example -- `dims()` using per-path bottleneck capacities -- satisfying evaluation for one known feasible path-flow vector -- unsatisfying evaluation for requirement failure, capacity overflow, invalid config length, and invalid path descriptions -- brute-force solver behavior on the YES and NO instances -- serde round-trip -- paper/example consistency test that checks the canonical YES instance and verifies the expected satisfying config - -**Step 2: Run the targeted test to verify it fails** - -Run: `cargo test path_constrained_network_flow --lib` -Expected: FAIL because `PathConstrainedNetworkFlow` does not exist yet. - -**Step 3: Record the concrete example data in the test helpers** - -Use the fixed issue example from #291 / `/fix-issue`: -- 8 vertices, 10 directed arcs, capacities `[2,1,1,1,1,1,1,1,2,1]` -- 5 prescribed s-t paths encoded as arc-index lists -- YES requirement `3` -- NO requirement `4` - -**Step 4: Re-run the targeted test to confirm the failure is still the missing model, not a broken fixture** - -Run: `cargo test path_constrained_network_flow --lib` -Expected: FAIL with unresolved import / missing type, not malformed test data. - -### Task 2: Implement and register the model - -**Files:** -- Create: `src/models/graph/path_constrained_network_flow.rs` -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Implement the model** - -Create `PathConstrainedNetworkFlow` with: -- `graph: DirectedGraph` -- `capacities: Vec` -- `source: usize` -- `sink: usize` -- `paths: Vec>` -- `requirement: u64` - -Implement: -- `inventory::submit!` schema metadata -- constructor assertions for arc count, terminal bounds, and path validity -- accessors and size getters: `num_vertices`, `num_arcs`, `num_paths`, `max_capacity`, `requirement` -- helper(s) for path bottlenecks and prescribed-path validation -- `Problem` + `SatisfactionProblem` -- `declare_variants!` with `"((max_capacity + 1)^num_paths)"` or equivalent valid expression using the existing getter names -- canonical example spec for the YES instance - -**Step 2: Register the model** - -Wire the new type through: -- `src/models/graph/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` prelude exports -- graph example-db spec chain in `src/models/graph/mod.rs` - -**Step 3: Run the targeted tests** - -Run: `cargo test path_constrained_network_flow --lib` -Expected: PASS for the new model tests. - -**Step 4: Refactor only if needed** - -Keep refactors local to helper extraction inside the new model/test files. Do not broaden scope beyond what the tests demand. - -### Task 3: Add CLI creation support for prescribed paths - -**Files:** -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/problem_name.rs` (only if alias coverage is missing after schema registration) - -**Step 1: Write or extend failing CLI-facing tests first** - -Add focused tests for: -- help text / example text exposing `PathConstrainedNetworkFlow` -- parsing a valid `pred create PathConstrainedNetworkFlow ... --paths ...` -- rejecting malformed `--paths` input and invalid arc references - -**Step 2: Run the targeted CLI tests to verify failure** - -Run: `cargo test create::tests::path_constrained_network_flow --package problemreductions-cli` -Expected: FAIL because the CLI does not yet know the new problem or `--paths`. - -**Step 3: Implement CLI support** - -Add: -- `CreateArgs.paths: Option` -- `all_data_flags_empty()` coverage for the new flag -- help-table entry and example string -- parser/helper for semicolon-separated path lists where each path is a comma-separated arc-index sequence -- `create()` match arm that builds `DirectedGraph` from `--arcs`, parses capacities / source / sink / paths / requirement, and serializes the new model - -Prefer the existing directed-graph conventions: -- use `--arcs`, not `--graph` -- use singular `--requirement` -- use `--paths "0,2,5,8;1,3,6,8;..."` to match the model’s constructor shape - -**Step 4: Re-run the targeted CLI tests** - -Run: `cargo test create::tests::path_constrained_network_flow --package problemreductions-cli` -Expected: PASS. - -### Task 4: Wire the canonical example and paper entry - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Verify via existing example-db exports driven from model registration -- Reference: `.claude/skills/write-model-in-paper/SKILL.md` - -**Step 1: Verify the canonical example is the same issue fixture** - -Use the example from Task 1 / model registration as the single source of truth for: -- `canonical_model_example_specs()` -- paper example text -- `pred-commands()` snippet - -**Step 2: Update the paper** - -Add: -- display-name entry for `PathConstrainedNetworkFlow` -- `problem-def("PathConstrainedNetworkFlow")` -- cited background using Garey & Johnson ND34 plus the published Büsing–Stiller 2011 reference -- a worked YES example that explains the feasible integral path-flow assignment -- `pred-commands()` that uses the canonical example fixture and brute-force solving - -**Step 3: Build the paper to confirm the entry is valid** - -Run: `make paper` -Expected: PASS. - -### Task 5: Full verification, PR summary, and cleanup - -**Files:** -- Modify: PR body/comment generated during pipeline execution - -**Step 1: Run the required verification commands** - -Run: -- `cargo test path_constrained_network_flow --lib` -- `cargo test create::tests::path_constrained_network_flow --package problemreductions-cli` -- `make test` -- `make clippy` -- `make paper` - -**Step 2: Inspect the tree** - -Run: `git status --short` -Expected: only intentional tracked changes; no leftover plan artifacts after cleanup. - -**Step 3: Call out the pipeline-specific deviation** - -In the PR body or implementation summary comment, note: -- `` -- the current repo search found no open rule issue referencing `PathConstrainedNetworkFlow` - -**Step 4: Commit, remove the plan file, push, and post the implementation summary** - -Follow `.claude/skills/issue-to-pr/SKILL.md` Step 7b-7d exactly after the implementation is verified. From 9e0cd03d904a4c0ce656adba4b87fefce86bd8c3 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 13:51:03 +0800 Subject: [PATCH 4/8] fix formatting after merge --- problemreductions-cli/src/commands/create.rs | 3 +-- src/lib.rs | 5 ++--- src/models/mod.rs | 7 ++++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index ce83b121c..4bdac45c8 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -15,8 +15,7 @@ use problemreductions::models::graph::{ GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, PathConstrainedNetworkFlow, SteinerTree, - SteinerTreeInGraphs, - StrongConnectivityAugmentation, + SteinerTreeInGraphs, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, diff --git a/src/lib.rs b/src/lib.rs index 02b2fa69c..7510c62bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,9 +61,8 @@ pub mod prelude { MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, - PathConstrainedNetworkFlow, - RuralPostman, ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, - UndirectedTwoCommodityIntegralFlow, + PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath, + SteinerTreeInGraphs, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, diff --git a/src/models/mod.rs b/src/models/mod.rs index 2f20cc2e0..43882ecf1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -27,9 +27,10 @@ pub use graph::{ MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, - PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath, - SpinGlass, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, - SubgraphIsomorphism, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, + PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman, + ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, + StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ From 28026b6df1349db23b868388794b8c143249a3c2 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 14:01:27 +0800 Subject: [PATCH 5/8] improve test coverage for PathConstrainedNetworkFlow Cover getter methods (graph, capacities, paths) and all assertion branches in assert_valid_path: empty path, repeated vertex, and path not ending at sink. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../graph/path_constrained_network_flow.rs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/unit_tests/models/graph/path_constrained_network_flow.rs b/src/unit_tests/models/graph/path_constrained_network_flow.rs index ec0933f6d..0fb6625b7 100644 --- a/src/unit_tests/models/graph/path_constrained_network_flow.rs +++ b/src/unit_tests/models/graph/path_constrained_network_flow.rs @@ -52,6 +52,9 @@ fn test_path_constrained_network_flow_creation() { assert_eq!(problem.requirement(), 3); assert_eq!(problem.source(), 0); assert_eq!(problem.sink(), 7); + assert_eq!(problem.graph().num_vertices(), 8); + assert_eq!(problem.capacities().len(), 10); + assert_eq!(problem.paths().len(), 5); } #[test] @@ -100,13 +103,40 @@ fn test_path_constrained_network_flow_serialization() { } #[test] -fn test_path_constrained_network_flow_rejects_invalid_prescribed_paths() { +fn test_path_constrained_network_flow_rejects_non_contiguous_path() { let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let result = std::panic::catch_unwind(|| { PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1], 0, 3, vec![vec![0, 2]], 1) }); + assert!(result.is_err()); +} + +#[test] +fn test_path_constrained_network_flow_rejects_empty_path() { + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let result = std::panic::catch_unwind(|| { + PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1], 0, 3, vec![vec![]], 1) + }); + assert!(result.is_err()); +} + +#[test] +fn test_path_constrained_network_flow_rejects_path_not_ending_at_sink() { + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let result = std::panic::catch_unwind(|| { + PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1], 0, 3, vec![vec![0, 1]], 1) + }); + assert!(result.is_err()); +} +#[test] +fn test_path_constrained_network_flow_rejects_path_with_repeated_vertex() { + // Graph: 0->1, 1->2, 2->1, 1->3 (arcs 0,1,2,3) + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 1), (1, 3)]); + let result = std::panic::catch_unwind(|| { + // Path [0, 1, 2, 3]: 0->1->2->1->3 revisits vertex 1 + PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1, 1], 0, 3, vec![vec![0, 1, 2, 3]], 1) + }); assert!(result.is_err()); } From 492cccc628e0c8e95617c16f97201db99279c6e7 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 14:08:55 +0800 Subject: [PATCH 6/8] fix formatting after merge --- 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 0d7186575..60bf0d22a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -28,10 +28,9 @@ pub use graph::{ MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, - PathConstrainedNetworkFlow, RuralPostman, - ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, - UndirectedTwoCommodityIntegralFlow, + PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath, SpinGlass, + SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, SubgraphIsomorphism, + TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ From c8950aad16d925a96bf25426d9fd493c3bf99c08 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 14:12:46 +0800 Subject: [PATCH 7/8] fix duplicate requirement field in CLI after merge Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/cli.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 87c82ef7b..5e6c5979d 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -370,7 +370,7 @@ pub struct CreateArgs { /// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub sink: Option, - /// Required sink inflow for IntegralFlowHomologousArcs and IntegralFlowWithMultipliers + /// Required total flow R for IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, and PathConstrainedNetworkFlow #[arg(long)] pub requirement: Option, /// Required number of paths for LengthBoundedDisjointPaths @@ -449,9 +449,6 @@ pub struct CreateArgs { /// Required flow R_2 for commodity 2 #[arg(long)] pub requirement_2: Option, - /// Required total flow R for PathConstrainedNetworkFlow - #[arg(long)] - pub requirement: Option, /// Item sizes for BinPacking (comma-separated, e.g., "3,3,2,2") #[arg(long)] pub sizes: Option, From 872913b72bcd2523f249c13a94af9124b6a48b53 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 14:17:14 +0800 Subject: [PATCH 8/8] fix duplicate requirement field in empty_args() test helper Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index ad264d741..526ddd763 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6283,7 +6283,6 @@ mod tests { sink_2: None, requirement_1: None, requirement_2: None, - requirement: None, sizes: None, capacity: None, sequence: None,