From 868931859bb9c38e248c413cf1d296c87afd2a05 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 03:03:39 +0800 Subject: [PATCH 1/5] Add plan for #292: [Model] IntegralFlowHomologousArcs --- ...-22-integral-flow-homologous-arcs-model.md | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md diff --git a/docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md b/docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md new file mode 100644 index 00000000..40fb658f --- /dev/null +++ b/docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md @@ -0,0 +1,226 @@ +# IntegralFlowHomologousArcs Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. +> +> **Codex note:** `run-pipeline` normally hands execution to subagents, but this session does not have explicit delegation approval. Execute this plan locally in the same order, following TDD for every code change. + +**Goal:** Add the `IntegralFlowHomologousArcs` satisfaction model, register it for library/CLI/example-db/paper workflows, and validate the canonical YES/NO homologous-constraint instances from issue `#292`. + +**Architecture:** Model the instance as a `DirectedGraph` plus per-arc capacities, one source/sink pair, one required sink inflow, and homologous pairs encoded as arc-index pairs in graph arc order. A configuration stores one integer flow value per arc; validity checks enforce per-arc bounds, conservation at non-terminals, homologous equalities, and sink inflow `>= requirement`. For `declare_variants!`, use the general-capacity brute-force bound `"(max_capacity + 1)^num_arcs"` and document `2^num_arcs` as the unit-capacity special case mentioned in the issue. + +**Tech Stack:** Rust workspace (`problemreductions`, `problemreductions-cli`), serde, inventory registry metadata, Typst paper docs, existing brute-force solver. + +--- + +## Issue Checklist + +| Item | Decision | +|---|---| +| Problem name | `IntegralFlowHomologousArcs` | +| Problem type | Satisfaction (`Metric = bool`) | +| Category | `src/models/graph/` | +| Core fields | `graph`, `capacities`, `source`, `sink`, `requirement`, `homologous_pairs` | +| Config space | One variable per arc, domain `{0, ..., c(a)}` | +| Feasibility | Capacity, conservation, homologous-equality, sink inflow threshold | +| Example outcome | Use the YES and NO instances from issue `#292` / fix-issue changelog | +| Associated rules | Incoming: `#732`, `#365`; outgoing: `#733` | + +## Batch 1: Model, Registry, CLI, Tests + +### Task 1: Add failing model tests first + +**Files:** +- Create: `src/unit_tests/models/graph/integral_flow_homologous_arcs.rs` +- Modify: `src/models/graph/integral_flow_homologous_arcs.rs` (test link will fail until the file exists) + +**Step 1: Write the failing tests** + +Cover these behaviors in the new test file: +- constructor/accessors/dimension sizes for the issue YES instance +- `evaluate()` accepts the YES configuration from the issue +- `evaluate()` rejects the issue NO instance because the homologous constraint makes it infeasible +- `evaluate()` rejects capacity, conservation, homologous-pair, and wrong-length violations +- `BruteForce::find_satisfying()` returns `Some(_)` for the YES instance and `None` for the NO instance +- serde round-trip and a `test_integral_flow_homologous_arcs_paper_example` + +**Step 2: Run the focused test target and verify RED** + +Run: `cargo test integral_flow_homologous_arcs --lib` + +Expected: compile/test failure because the model module and test link do not exist yet. + +**Step 3: Do not add production code yet** + +Stop after confirming the failing state. The model implementation belongs to Task 2. + +### Task 2: Implement the model and wire it into the library + +**Files:** +- Create: `src/models/graph/integral_flow_homologous_arcs.rs` +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Implement the minimal model to satisfy Task 1** + +In `src/models/graph/integral_flow_homologous_arcs.rs`: +- add `ProblemSchemaEntry` with constructor-facing fields +- add optional `ProblemSizeFieldEntry` for `num_vertices` and `num_arcs` +- define `IntegralFlowHomologousArcs` with `#[derive(Debug, Clone, Serialize, Deserialize)]` +- use `DirectedGraph` plus `Vec` capacities and `Vec<(usize, usize)>` homologous arc-index pairs +- validate constructor invariants: + - capacities length matches `graph.num_arcs()` + - source/sink are in range + - every homologous pair references valid arc indices + - each capacity fits into `usize` for `dims()` +- expose getters: `graph()`, `capacities()`, `source()`, `sink()`, `requirement()`, `homologous_pairs()`, `num_vertices()`, `num_arcs()`, `max_capacity()` +- implement `Problem`, `SatisfactionProblem`, `declare_variants!`, and `canonical_model_example_specs()` +- use `"(max_capacity + 1)^num_arcs"` in `declare_variants!` + +In the library exports: +- register the module in `src/models/graph/mod.rs` +- extend the graph re-exports and `canonical_model_example_specs()` chain +- add the type to `src/models/mod.rs` and the graph prelude exports in `src/lib.rs` + +**Step 2: Run the focused model tests and verify GREEN** + +Run: `cargo test integral_flow_homologous_arcs --lib` + +Expected: the new model tests pass. + +**Step 3: Refactor only if needed** + +Keep helper functions small and local. Prefer explicit balance calculations over generic abstractions. + +### Task 3: Add CLI creation support with tests first + +**Files:** +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/tests/cli_tests.rs` + +**Step 1: Write the failing CLI tests** + +Add coverage for: +- successful `pred create IntegralFlowHomologousArcs --arcs ... --capacities ... --source ... --sink ... --requirement ... --homologous-pairs ...` +- helpful usage errors when `--homologous-pairs` or required terminals are missing +- schema/help wiring so `pred create IntegralFlowHomologousArcs` surfaces the new fields + +Use `--homologous-pairs "2=5;4=3"` as the planned CLI format for arc-index equalities. + +**Step 2: Run the focused CLI tests and verify RED** + +Run: `cargo test -p problemreductions-cli integral_flow_homologous_arcs` + +Expected: failure because the CLI flags, parser, and create arm do not exist yet. + +**Step 3: Implement the minimal CLI support** + +In `problemreductions-cli/src/cli.rs`: +- add `IntegralFlowHomologousArcs` to the "Flags by problem type" and examples +- add `--homologous-pairs` to `CreateArgs` + +In `problemreductions-cli/src/commands/create.rs`: +- include `homologous_pairs` in both `all_data_flags_empty()` checks +- parse the new flag into `Vec<(usize, usize)>` +- add a `create()` match arm that builds `IntegralFlowHomologousArcs` +- reuse existing `--arcs`, `--capacities`, `--source`, `--sink`, and `--requirement` + +**Step 4: Re-run the focused CLI tests and verify GREEN** + +Run: `cargo test -p problemreductions-cli integral_flow_homologous_arcs` + +Expected: the targeted CLI tests pass. + +### Task 4: Add supporting regression coverage and trait/example checks + +**Files:** +- Modify: `src/unit_tests/trait_consistency.rs` +- Modify: `src/models/graph/integral_flow_homologous_arcs.rs` +- Modify: `problemreductions-cli/tests/cli_tests.rs` (if Task 3 only added minimal error coverage) + +**Step 1: Add failing regression checks** + +Add or extend tests so the new model is covered by: +- trait-consistency name checks +- canonical example-db expectations +- at least one end-to-end CLI create/solve or create/evaluate smoke path if still missing + +**Step 2: Run focused tests and verify RED** + +Run: `cargo test trait_consistency integral_flow_homologous_arcs` + +Expected: failure until the missing registrations or example values are wired correctly. + +**Step 3: Implement the minimal missing wiring** + +Only add the registrations/tests needed to satisfy the new coverage. + +**Step 4: Re-run focused tests and verify GREEN** + +Run: `cargo test trait_consistency integral_flow_homologous_arcs` + +Expected: the focused regression checks pass. + +## Batch 2: Paper Entry + +### Task 5: Add the paper documentation after implementation is stable + +**Files:** +- Modify: `docs/paper/reductions.typ` + +**Step 1: Write the failing paper-adjacent check** + +If the model tests do not already assert the issue example used in the paper, extend `test_integral_flow_homologous_arcs_paper_example` first so the paper instance is executable in Rust before editing Typst. + +**Step 2: Implement the paper entry** + +Add: +- display-name dictionary entry for `IntegralFlowHomologousArcs` +- `#problem-def("IntegralFlowHomologousArcs")[...]` with self-contained notation +- background that cites Garey-Johnson / Sahni and notes the LP-equivalent fractional variant +- a worked example matching the canonical YES instance +- `pred-commands()` using the canonical example-db entry rather than a hand-written bare alias +- wording that records `2^num_arcs` as the unit-capacity special case while the code registers the general-capacity bound + +Use the existing directed-flow paper entries as the style reference. + +**Step 3: Run the paper build and verify GREEN** + +Run: `make paper` + +Expected: Typst compiles without warnings/errors attributable to the new entry. + +## Final Verification + +### Task 6: Run repo verification and prepare the branch for PR push + +**Files:** +- Modify only as needed from prior tasks + +**Step 1: Run the required verification** + +Run: +- `cargo test integral_flow_homologous_arcs` +- `cargo test -p problemreductions-cli integral_flow_homologous_arcs` +- `make test` +- `make clippy` +- `make paper` + +**Step 2: Inspect the tree** + +Run: `git status --short` + +Expected: only intentional tracked changes remain; ignored generated docs under `docs/src/reductions/` may appear but must not be staged accidentally. + +**Step 3: Commit coherent implementation changes** + +Suggested messages: +- `Add IntegralFlowHomologousArcs model and CLI support` +- `Document IntegralFlowHomologousArcs in paper` + +**Step 4: Remove this plan file before final push** + +Run: +- `git rm docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md` +- `git commit -m "chore: remove plan file after implementation"` From fc7b2b81f8d6ad1d1de9efab26ee506de137dece Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 03:25:50 +0800 Subject: [PATCH 2/5] Implement #292: [Model] IntegralFlowHomologousArcs --- docs/paper/reductions.typ | 84 ++++++ problemreductions-cli/src/cli.rs | 8 + problemreductions-cli/src/commands/create.rs | 143 ++++++++++ problemreductions-cli/tests/cli_tests.rs | 102 ++++++++ src/lib.rs | 6 +- .../graph/integral_flow_homologous_arcs.rs | 245 ++++++++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 7 +- src/unit_tests/example_db.rs | 15 ++ src/unit_tests/graph_models.rs | 36 +++ .../graph/integral_flow_homologous_arcs.rs | 128 +++++++++ 11 files changed, 772 insertions(+), 6 deletions(-) create mode 100644 src/models/graph/integral_flow_homologous_arcs.rs create mode 100644 src/unit_tests/models/graph/integral_flow_homologous_arcs.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 9167a0c1..f5853f1a 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -131,6 +131,7 @@ "ConsecutiveBlockMinimization": [Consecutive Block Minimization], "ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], + "IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs], "MinMaxMulticenter": [Min-Max Multicenter], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], @@ -5204,6 +5205,89 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("IntegralFlowHomologousArcs") + let arcs = x.instance.graph.arcs + let sol = x.optimal_config + let source = x.instance.source + let sink = x.instance.sink + let requirement = x.instance.requirement + [ + #problem-def("IntegralFlowHomologousArcs")[ + Given a directed graph $G = (V, A)$ with source $s in V$, sink $t in V$, arc capacities $c: A -> ZZ^+$, requirement $R in ZZ^+$, and a set $H subset.eq A times A$ of homologous arc pairs, determine whether there exists an integral flow function $f: A -> ZZ_(>= 0)$ such that $f(a) <= c(a)$ for every $a in A$, flow is conserved at every vertex in $V backslash {s, t}$, $f(a) = f(a')$ for every $(a, a') in H$, and the net flow into $t$ is at least $R$. + ][ + Integral Flow with Homologous Arcs is the single-commodity equality-constrained flow problem listed as ND35 in Garey & Johnson @garey1979. Their catalog records the NP-completeness result attributed to Sahni and notes that the unit-capacity restriction remains hard, while the corresponding non-integral relaxation is polynomial-time equivalent to linear programming @garey1979. + + The implementation uses one integer variable per arc, so exhaustive search over the induced configuration space runs in $O((C + 1)^m)$ for $m = |A|$ and $C = max_(a in A) c(a)$#footnote[This is the exact search bound induced by the implemented per-arc domains $f(a) in {0, dots, c(a)}$. In the unit-capacity special case, it simplifies to $O(2^m)$.]. + + *Example.* The canonical fixture instance has source $s = v_#source$, sink $t = v_#sink$, unit capacities on all eight arcs, requirement $R = #requirement$, and homologous pairs $(a_2, a_5)$ and $(a_4, a_3)$. The stored satisfying configuration routes one unit along $0 -> 1 -> 3 -> 5$ and one unit along $0 -> 2 -> 4 -> 5$. Thus the paired arcs $(1,3)$ and $(2,4)$ both carry 1, while $(1,4)$ and $(2,3)$ both carry 0. Every nonterminal vertex has equal inflow and outflow, and the sink receives two units of flow, so the verifier returns true. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o integral-flow-homologous-arcs.json", + "pred solve integral-flow-homologous-arcs.json --solver brute-force", + "pred evaluate integral-flow-homologous-arcs.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 red = rgb("#e15759") + let gray = luma(185) + let positions = ( + (0, 0), + (1.6, 1.1), + (1.6, -1.1), + (3.2, 1.1), + (3.2, -1.1), + (4.8, 0), + ) + let labels = ( + [$s = v_0$], + [$v_1$], + [$v_2$], + [$v_3$], + [$v_4$], + [$t = v_5$], + ) + for (idx, (u, v)) in arcs.enumerate() { + let stroke = if idx == 3 or idx == 4 { + (paint: orange, thickness: 1.3pt, dash: "dashed") + } else if sol.at(idx) == 1 { + (paint: blue, thickness: 1.8pt) + } else { + (paint: gray, thickness: 0.7pt) + } + line( + positions.at(u), + positions.at(v), + stroke: stroke, + mark: (end: "straight", scale: 0.5), + ) + } + for (i, pos) in positions.enumerate() { + let fill = if i == source { blue } else if i == sink { red } else { white } + g-node( + pos, + name: "ifha-" + str(i), + fill: fill, + label: if i == source or i == sink { + text(fill: white)[#labels.at(i)] + } else { + labels.at(i) + }, + ) + } + content((2.4, 1.55), text(8pt, fill: blue)[$f(a_2) = f(a_5) = 1$]) + content((2.4, -1.55), text(8pt, fill: orange)[$f(a_4) = f(a_3) = 0$]) + }), + caption: [Canonical YES instance for Integral Flow with Homologous Arcs. Solid blue arcs carry the satisfying integral flow; dashed orange arcs form the second homologous pair, constrained to equal zero.], + ) + ] + ] +} + #problem-def("DirectedTwoCommodityIntegralFlow")[ Given a directed graph $G = (V, A)$ with arc capacities $c: A -> ZZ^+$, two source-sink pairs $(s_1, t_1)$ and $(s_2, t_2)$, and requirements $R_1, R_2 in ZZ^+$, determine whether there exist two integral flow functions $f_1, f_2: A -> ZZ_(>= 0)$ such that (1) $f_1(a) + f_2(a) <= c(a)$ for all $a in A$, (2) flow $f_i$ is conserved at every vertex except $s_1, s_2, t_1, t_2$, and (3) the net flow into $t_i$ under $f_i$ is at least $R_i$ for $i in {1, 2}$. ][ diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 02df552d..e31834c3 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -234,6 +234,7 @@ Flags by problem type: LongestCircuit --graph, --edge-weights, --bound BoundedComponentSpanningForest --graph, --weights, --k, --bound UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 + IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs IsomorphicSpanningTree --graph, --tree KthBestSpanningTree --graph, --edge-weights, --k, --bound LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound @@ -323,6 +324,7 @@ Examples: pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5 pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1 pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 + pred create IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\" pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\" pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3 pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2 @@ -362,6 +364,9 @@ pub struct CreateArgs { /// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub sink: Option, + /// Required sink inflow for IntegralFlowHomologousArcs + #[arg(long)] + pub requirement: Option, /// Required number of paths for LengthBoundedDisjointPaths #[arg(long)] pub num_paths_required: Option, @@ -528,6 +533,9 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Arc-index equality constraints for IntegralFlowHomologousArcs (semicolon-separated, e.g., "2=5;4=3") + #[arg(long)] + pub homologous_pairs: Option, /// Quantifiers for QBF (comma-separated, E=Exists, A=ForAll, e.g., "E,A,E") #[arg(long)] pub quantifiers: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index d03de034..e9307034 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -52,6 +52,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.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() @@ -107,6 +108,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.costs.is_none() && args.arc_costs.is_none() && args.arcs.is_none() + && args.homologous_pairs.is_none() && args.quantifiers.is_none() && args.usage.is_none() && args.storage.is_none() @@ -149,6 +151,8 @@ 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.homologous_pairs.is_none() && args.num_attributes.is_none() && args.dependencies.is_none() && args.relation_attrs.is_none() @@ -523,6 +527,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "UndirectedTwoCommodityIntegralFlow" => { "--graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1" }, + "IntegralFlowHomologousArcs" => { + "--arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\"" + } "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" } @@ -745,6 +752,9 @@ fn help_flag_hint( } ("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"", ("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"", + ("IntegralFlowHomologousArcs", "homologous_pairs") => { + "semicolon-separated arc-index equalities: \"2=5;4=3\"" + } ("ConsistencyOfDatabaseFrequencyTables", "attribute_domains") => { "comma-separated domain sizes: 2,3,2" } @@ -3103,6 +3113,83 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // IntegralFlowHomologousArcs + "IntegralFlowHomologousArcs" => { + let usage = "Usage: pred create IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\""; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs 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 { + s.split(',') + .map(|token| { + let trimmed = token.trim(); + trimmed + .parse::() + .with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}")) + }) + .collect::>>()? + } else { + vec![1; num_arcs] + }; + anyhow::ensure!( + capacities.len() == num_arcs, + "Expected {} capacities but got {}\n\n{}", + num_arcs, + capacities.len(), + usage + ); + for (arc_index, &capacity) in capacities.iter().enumerate() { + let fits = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(); + anyhow::ensure!( + fits, + "capacity {} at arc index {} is too large for this platform\n\n{}", + capacity, + arc_index, + usage + ); + } + let num_vertices = graph.num_vertices(); + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --sink\n\n{usage}") + })?; + let requirement = args.requirement.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --requirement\n\n{usage}") + })?; + validate_vertex_index("source", source, num_vertices, usage)?; + validate_vertex_index("sink", sink, num_vertices, usage)?; + let homologous_pairs = + parse_homologous_pairs(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + for &(a, b) in &homologous_pairs { + anyhow::ensure!( + a < num_arcs && b < num_arcs, + "homologous pair ({}, {}) references arc >= num_arcs ({})\n\n{}", + a, + b, + num_arcs, + usage + ); + } + ( + ser(IntegralFlowHomologousArcs::new( + graph, + capacities, + source, + sink, + requirement, + homologous_pairs, + ))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackArcSet "MinimumFeedbackArcSet" => { let arcs_str = args.arcs.as_deref().ok_or_else(|| { @@ -4322,6 +4409,35 @@ fn parse_named_sets(sets_str: Option<&str>, flag: &str) -> Result .collect() } +fn parse_homologous_pairs(args: &CreateArgs) -> Result> { + let pairs = args.homologous_pairs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "IntegralFlowHomologousArcs requires --homologous-pairs (e.g., \"2=5;4=3\")" + ) + })?; + + pairs + .split(';') + .filter(|entry| !entry.trim().is_empty()) + .map(|entry| { + let entry = entry.trim(); + let (left, right) = entry.split_once('=').ok_or_else(|| { + anyhow::anyhow!( + "Invalid homologous pair '{}': expected format u=v (e.g., 2=5)", + entry + ) + })?; + let left = left.trim().parse::().with_context(|| { + format!("Invalid homologous pair '{}': expected format u=v", entry) + })?; + let right = right.trim().parse::().with_context(|| { + format!("Invalid homologous pair '{}': expected format u=v", entry) + })?; + Ok((left, right)) + }) + .collect() +} + /// Parse a dependency string as semicolon-separated `lhs>rhs` pairs. /// E.g., "0,1>2,3;2,3>0,1" fn parse_deps(s: &str) -> Result, Vec)>> { @@ -5873,6 +5989,7 @@ mod tests { capacities: None, source: None, sink: None, + requirement: None, num_paths_required: None, couplings: None, fields: None, @@ -5971,6 +6088,7 @@ mod tests { usage: None, storage: None, quantifiers: None, + homologous_pairs: None, } } @@ -5988,6 +6106,13 @@ mod tests { assert!(!all_data_flags_empty(&args)); } + #[test] + fn test_all_data_flags_empty_treats_homologous_pairs_as_input() { + let mut args = empty_args(); + args.homologous_pairs = Some("2=5;4=3".to_string()); + assert!(!all_data_flags_empty(&args)); + } + #[test] fn test_parse_potential_edges() { let mut args = empty_args(); @@ -6016,6 +6141,24 @@ mod tests { assert_eq!(parse_budget(&args).unwrap(), 7); } + #[test] + fn test_parse_homologous_pairs() { + let mut args = empty_args(); + args.homologous_pairs = Some("2=5;4=3".to_string()); + + assert_eq!(parse_homologous_pairs(&args).unwrap(), vec![(2, 5), (4, 3)]); + } + + #[test] + fn test_parse_homologous_pairs_rejects_invalid_token() { + let mut args = empty_args(); + args.homologous_pairs = Some("2-5".to_string()); + + let err = parse_homologous_pairs(&args).unwrap_err().to_string(); + + assert!(err.contains("u=v")); + } + #[test] fn test_parse_graph_respects_explicit_num_vertices() { let mut args = empty_args(); diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 5c38df24..d2dfbb12 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -33,6 +33,18 @@ fn test_list_includes_undirected_two_commodity_integral_flow() { assert!(stdout.contains("UndirectedTwoCommodityIntegralFlow")); } +#[test] +fn test_list_includes_integral_flow_homologous_arcs() { + let output = pred().args(["list"]).output().unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("IntegralFlowHomologousArcs")); +} + #[test] fn test_solve_help_mentions_string_to_string_correction_bruteforce() { let output = pred().args(["solve", "--help"]).output().unwrap(); @@ -656,6 +668,96 @@ 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_homologous_arcs() { + let output_file = std::env::temp_dir().join("pred_test_create_integral_flow_homologous_arcs.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "IntegralFlowHomologousArcs", + "--arcs", + "0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5", + "--capacities", + "1,1,1,1,1,1,1,1", + "--source", + "0", + "--sink", + "5", + "--requirement", + "2", + "--homologous-pairs", + "2=5;4=3", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(output_file.exists()); + + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "IntegralFlowHomologousArcs"); + + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_integral_flow_homologous_arcs_requires_homologous_pairs() { + let output = pred() + .args([ + "create", + "IntegralFlowHomologousArcs", + "--arcs", + "0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5", + "--capacities", + "1,1,1,1,1,1,1,1", + "--source", + "0", + "--sink", + "5", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("requires --homologous-pairs")); + assert!(stderr.contains("Usage: pred create IntegralFlowHomologousArcs")); +} + +#[test] +fn test_create_integral_flow_homologous_arcs_rejects_invalid_pair_token() { + let output = pred() + .args([ + "create", + "IntegralFlowHomologousArcs", + "--arcs", + "0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5", + "--capacities", + "1,1,1,1,1,1,1,1", + "--source", + "0", + "--sink", + "5", + "--requirement", + "2", + "--homologous-pairs", + "2-5;4=3", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("u=v")); + assert!(stderr.contains("Usage: pred create IntegralFlowHomologousArcs")); +} + #[test] fn test_create_consecutive_block_minimization_rejects_ragged_matrix() { let output = pred() diff --git a/src/lib.rs b/src/lib.rs index f9e84dca..817d03f2 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, IntegralFlowHomologousArcs, 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_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs new file mode 100644 index 00000000..75fc4cf7 --- /dev/null +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -0,0 +1,245 @@ +//! Integral Flow with Homologous Arcs problem implementation. +//! +//! Given a directed capacitated network with a source, sink, and pairs of arcs +//! that must carry equal flow, determine whether an integral flow meeting the +//! required sink inflow exists. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; +use crate::topology::DirectedGraph; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "IntegralFlowHomologousArcs", + display_name: "Integral Flow with Homologous Arcs", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Integral flow feasibility with arc-pair equality constraints", + fields: &[ + FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed graph G = (V, A)" }, + FieldInfo { name: "capacities", type_name: "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: "requirement", type_name: "u64", description: "Required net inflow R at the sink" }, + FieldInfo { name: "homologous_pairs", type_name: "Vec<(usize, usize)>", description: "Arc-index pairs (a, a') with f(a) = f(a')" }, + ], + } +} + +inventory::submit! { + ProblemSizeFieldEntry { + name: "IntegralFlowHomologousArcs", + fields: &["num_vertices", "num_arcs"], + } +} + +/// Integral flow with homologous arcs. +/// +/// A configuration stores one non-negative integer flow value for each arc in +/// the graph's arc order. The assignment is feasible when it respects arc +/// capacities, flow conservation at non-terminal vertices, every homologous-pair +/// equality constraint, and the required net inflow at the sink. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegralFlowHomologousArcs { + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + requirement: u64, + homologous_pairs: Vec<(usize, usize)>, +} + +impl IntegralFlowHomologousArcs { + pub fn new( + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + requirement: u64, + homologous_pairs: Vec<(usize, usize)>, + ) -> Self { + let num_vertices = graph.num_vertices(); + let num_arcs = graph.num_arcs(); + + assert_eq!( + capacities.len(), + num_arcs, + "capacities length must match graph.num_arcs()" + ); + assert!( + source < num_vertices, + "source ({source}) must be less than num_vertices ({num_vertices})" + ); + assert!( + sink < num_vertices, + "sink ({sink}) must be less than num_vertices ({num_vertices})" + ); + + for &(a, b) in &homologous_pairs { + assert!(a < num_arcs, "homologous arc index {a} out of range"); + assert!(b < num_arcs, "homologous arc index {b} out of range"); + } + + for &capacity in &capacities { + assert!( + usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(), + "capacities must fit into usize for dims()" + ); + } + + Self { + graph, + capacities, + source, + sink, + requirement, + homologous_pairs, + } + } + + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + pub fn source(&self) -> usize { + self.source + } + + pub fn sink(&self) -> usize { + self.sink + } + + pub fn requirement(&self) -> u64 { + self.requirement + } + + pub fn homologous_pairs(&self) -> &[(usize, usize)] { + &self.homologous_pairs + } + + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + pub fn num_arcs(&self) -> usize { + self.graph.num_arcs() + } + + pub fn max_capacity(&self) -> u64 { + self.capacities.iter().copied().max().unwrap_or(0) + } + + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate(config) + } + + fn domain_size(capacity: u64) -> usize { + usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .expect("capacity already validated to fit into usize") + } +} + +impl Problem for IntegralFlowHomologousArcs { + const NAME: &'static str = "IntegralFlowHomologousArcs"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + self.capacities + .iter() + .map(|&capacity| Self::domain_size(capacity)) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_arcs() { + return false; + } + + for (&(a, b), _) in self.homologous_pairs.iter().zip(std::iter::repeat(())) { + if config[a] != config[b] { + return false; + } + } + + let mut balances = vec![0_i128; self.num_vertices()]; + for (arc_index, ((u, v), &capacity)) in self + .graph + .arcs() + .into_iter() + .zip(self.capacities.iter()) + .enumerate() + { + let Ok(flow) = u64::try_from(config[arc_index]) else { + return false; + }; + if flow > capacity { + return false; + } + let flow = i128::from(flow); + balances[u] -= flow; + balances[v] += flow; + } + + for (vertex, &balance) in balances.iter().enumerate() { + if vertex != self.source && vertex != self.sink && balance != 0 { + return false; + } + } + + balances[self.sink] >= i128::from(self.requirement) + } +} + +impl SatisfactionProblem for IntegralFlowHomologousArcs {} + +crate::declare_variants! { + default sat IntegralFlowHomologousArcs => "(max_capacity + 1)^num_arcs", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "integral_flow_homologous_arcs", + instance: Box::new(IntegralFlowHomologousArcs::new( + DirectedGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (1, 4), + (2, 4), + (3, 5), + (4, 5), + ], + ), + vec![1; 8], + 0, + 5, + 2, + vec![(2, 5), (4, 3)], + )), + optimal_config: vec![1, 1, 1, 0, 0, 1, 1, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/integral_flow_homologous_arcs.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 06627fb7..1f51fe3a 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) +//! - [`IntegralFlowHomologousArcs`]: Integral flow with arc-pair equality constraints //! - [`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_homologous_arcs; 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_homologous_arcs::IntegralFlowHomologousArcs; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kclique::KClique; pub use kcoloring::KColoring; @@ -181,6 +184,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec IntegralFlowHomologousArcs { + let graph = DirectedGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (1, 4), + (2, 4), + (3, 5), + (4, 5), + ], + ); + IntegralFlowHomologousArcs::new(graph, vec![1; 8], 0, 5, 2, vec![(2, 5), (4, 3)]) +} + +fn no_instance() -> IntegralFlowHomologousArcs { + let graph = DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]); + IntegralFlowHomologousArcs::new(graph, vec![1; 4], 0, 3, 1, vec![(0, 1)]) +} + +#[test] +fn test_integral_flow_homologous_arcs_creation() { + let problem = yes_instance(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_arcs(), 8); + assert_eq!(problem.source(), 0); + assert_eq!(problem.sink(), 5); + assert_eq!(problem.requirement(), 2); + assert_eq!(problem.max_capacity(), 1); + assert_eq!(problem.homologous_pairs(), &[(2, 5), (4, 3)]); + assert_eq!(problem.dims(), vec![2; 8]); +} + +#[test] +fn test_integral_flow_homologous_arcs_evaluate_yes_instance() { + let problem = yes_instance(); + let config = vec![1, 1, 1, 0, 0, 1, 1, 1]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_integral_flow_homologous_arcs_evaluate_no_instance() { + let problem = no_instance(); + assert!(!problem.evaluate(&[0, 0, 0, 0])); +} + +#[test] +fn test_integral_flow_homologous_arcs_rejects_homologous_violation() { + let problem = yes_instance(); + let config = vec![1, 1, 1, 0, 0, 0, 1, 1]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_integral_flow_homologous_arcs_rejects_capacity_violation() { + let problem = yes_instance(); + let config = vec![2, 0, 0, 0, 0, 0, 0, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_integral_flow_homologous_arcs_rejects_conservation_violation() { + let problem = yes_instance(); + let config = vec![1, 0, 0, 0, 0, 0, 0, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_integral_flow_homologous_arcs_wrong_config_length_is_invalid() { + let problem = yes_instance(); + assert!(!problem.evaluate(&[0; 7])); + assert!(!problem.evaluate(&[0; 9])); +} + +#[test] +fn test_integral_flow_homologous_arcs_solver_yes() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +} + +#[test] +fn test_integral_flow_homologous_arcs_solver_no() { + let problem = no_instance(); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_integral_flow_homologous_arcs_serialization() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let restored: IntegralFlowHomologousArcs = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.num_vertices(), 6); + assert_eq!(restored.num_arcs(), 8); + assert_eq!(restored.requirement(), 2); + assert_eq!(restored.homologous_pairs(), &[(2, 5), (4, 3)]); +} + +#[test] +fn test_integral_flow_homologous_arcs_problem_name() { + assert_eq!( + ::NAME, + "IntegralFlowHomologousArcs" + ); +} + +#[test] +fn test_integral_flow_homologous_arcs_paper_example() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let config = vec![1, 1, 1, 0, 0, 1, 1, 1]; + + assert!(problem.evaluate(&config)); + + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + assert!(solutions.iter().all(|solution| problem.evaluate(solution))); +} From c98a7e8da00b45dbc38e058087f0e9b23773d1ab Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 03:26:01 +0800 Subject: [PATCH 3/5] chore: remove plan file after implementation --- ...-22-integral-flow-homologous-arcs-model.md | 226 ------------------ 1 file changed, 226 deletions(-) delete mode 100644 docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md diff --git a/docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md b/docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md deleted file mode 100644 index 40fb658f..00000000 --- a/docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md +++ /dev/null @@ -1,226 +0,0 @@ -# IntegralFlowHomologousArcs Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. -> -> **Codex note:** `run-pipeline` normally hands execution to subagents, but this session does not have explicit delegation approval. Execute this plan locally in the same order, following TDD for every code change. - -**Goal:** Add the `IntegralFlowHomologousArcs` satisfaction model, register it for library/CLI/example-db/paper workflows, and validate the canonical YES/NO homologous-constraint instances from issue `#292`. - -**Architecture:** Model the instance as a `DirectedGraph` plus per-arc capacities, one source/sink pair, one required sink inflow, and homologous pairs encoded as arc-index pairs in graph arc order. A configuration stores one integer flow value per arc; validity checks enforce per-arc bounds, conservation at non-terminals, homologous equalities, and sink inflow `>= requirement`. For `declare_variants!`, use the general-capacity brute-force bound `"(max_capacity + 1)^num_arcs"` and document `2^num_arcs` as the unit-capacity special case mentioned in the issue. - -**Tech Stack:** Rust workspace (`problemreductions`, `problemreductions-cli`), serde, inventory registry metadata, Typst paper docs, existing brute-force solver. - ---- - -## Issue Checklist - -| Item | Decision | -|---|---| -| Problem name | `IntegralFlowHomologousArcs` | -| Problem type | Satisfaction (`Metric = bool`) | -| Category | `src/models/graph/` | -| Core fields | `graph`, `capacities`, `source`, `sink`, `requirement`, `homologous_pairs` | -| Config space | One variable per arc, domain `{0, ..., c(a)}` | -| Feasibility | Capacity, conservation, homologous-equality, sink inflow threshold | -| Example outcome | Use the YES and NO instances from issue `#292` / fix-issue changelog | -| Associated rules | Incoming: `#732`, `#365`; outgoing: `#733` | - -## Batch 1: Model, Registry, CLI, Tests - -### Task 1: Add failing model tests first - -**Files:** -- Create: `src/unit_tests/models/graph/integral_flow_homologous_arcs.rs` -- Modify: `src/models/graph/integral_flow_homologous_arcs.rs` (test link will fail until the file exists) - -**Step 1: Write the failing tests** - -Cover these behaviors in the new test file: -- constructor/accessors/dimension sizes for the issue YES instance -- `evaluate()` accepts the YES configuration from the issue -- `evaluate()` rejects the issue NO instance because the homologous constraint makes it infeasible -- `evaluate()` rejects capacity, conservation, homologous-pair, and wrong-length violations -- `BruteForce::find_satisfying()` returns `Some(_)` for the YES instance and `None` for the NO instance -- serde round-trip and a `test_integral_flow_homologous_arcs_paper_example` - -**Step 2: Run the focused test target and verify RED** - -Run: `cargo test integral_flow_homologous_arcs --lib` - -Expected: compile/test failure because the model module and test link do not exist yet. - -**Step 3: Do not add production code yet** - -Stop after confirming the failing state. The model implementation belongs to Task 2. - -### Task 2: Implement the model and wire it into the library - -**Files:** -- Create: `src/models/graph/integral_flow_homologous_arcs.rs` -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Implement the minimal model to satisfy Task 1** - -In `src/models/graph/integral_flow_homologous_arcs.rs`: -- add `ProblemSchemaEntry` with constructor-facing fields -- add optional `ProblemSizeFieldEntry` for `num_vertices` and `num_arcs` -- define `IntegralFlowHomologousArcs` with `#[derive(Debug, Clone, Serialize, Deserialize)]` -- use `DirectedGraph` plus `Vec` capacities and `Vec<(usize, usize)>` homologous arc-index pairs -- validate constructor invariants: - - capacities length matches `graph.num_arcs()` - - source/sink are in range - - every homologous pair references valid arc indices - - each capacity fits into `usize` for `dims()` -- expose getters: `graph()`, `capacities()`, `source()`, `sink()`, `requirement()`, `homologous_pairs()`, `num_vertices()`, `num_arcs()`, `max_capacity()` -- implement `Problem`, `SatisfactionProblem`, `declare_variants!`, and `canonical_model_example_specs()` -- use `"(max_capacity + 1)^num_arcs"` in `declare_variants!` - -In the library exports: -- register the module in `src/models/graph/mod.rs` -- extend the graph re-exports and `canonical_model_example_specs()` chain -- add the type to `src/models/mod.rs` and the graph prelude exports in `src/lib.rs` - -**Step 2: Run the focused model tests and verify GREEN** - -Run: `cargo test integral_flow_homologous_arcs --lib` - -Expected: the new model tests pass. - -**Step 3: Refactor only if needed** - -Keep helper functions small and local. Prefer explicit balance calculations over generic abstractions. - -### Task 3: Add CLI creation support with tests first - -**Files:** -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/tests/cli_tests.rs` - -**Step 1: Write the failing CLI tests** - -Add coverage for: -- successful `pred create IntegralFlowHomologousArcs --arcs ... --capacities ... --source ... --sink ... --requirement ... --homologous-pairs ...` -- helpful usage errors when `--homologous-pairs` or required terminals are missing -- schema/help wiring so `pred create IntegralFlowHomologousArcs` surfaces the new fields - -Use `--homologous-pairs "2=5;4=3"` as the planned CLI format for arc-index equalities. - -**Step 2: Run the focused CLI tests and verify RED** - -Run: `cargo test -p problemreductions-cli integral_flow_homologous_arcs` - -Expected: failure because the CLI flags, parser, and create arm do not exist yet. - -**Step 3: Implement the minimal CLI support** - -In `problemreductions-cli/src/cli.rs`: -- add `IntegralFlowHomologousArcs` to the "Flags by problem type" and examples -- add `--homologous-pairs` to `CreateArgs` - -In `problemreductions-cli/src/commands/create.rs`: -- include `homologous_pairs` in both `all_data_flags_empty()` checks -- parse the new flag into `Vec<(usize, usize)>` -- add a `create()` match arm that builds `IntegralFlowHomologousArcs` -- reuse existing `--arcs`, `--capacities`, `--source`, `--sink`, and `--requirement` - -**Step 4: Re-run the focused CLI tests and verify GREEN** - -Run: `cargo test -p problemreductions-cli integral_flow_homologous_arcs` - -Expected: the targeted CLI tests pass. - -### Task 4: Add supporting regression coverage and trait/example checks - -**Files:** -- Modify: `src/unit_tests/trait_consistency.rs` -- Modify: `src/models/graph/integral_flow_homologous_arcs.rs` -- Modify: `problemreductions-cli/tests/cli_tests.rs` (if Task 3 only added minimal error coverage) - -**Step 1: Add failing regression checks** - -Add or extend tests so the new model is covered by: -- trait-consistency name checks -- canonical example-db expectations -- at least one end-to-end CLI create/solve or create/evaluate smoke path if still missing - -**Step 2: Run focused tests and verify RED** - -Run: `cargo test trait_consistency integral_flow_homologous_arcs` - -Expected: failure until the missing registrations or example values are wired correctly. - -**Step 3: Implement the minimal missing wiring** - -Only add the registrations/tests needed to satisfy the new coverage. - -**Step 4: Re-run focused tests and verify GREEN** - -Run: `cargo test trait_consistency integral_flow_homologous_arcs` - -Expected: the focused regression checks pass. - -## Batch 2: Paper Entry - -### Task 5: Add the paper documentation after implementation is stable - -**Files:** -- Modify: `docs/paper/reductions.typ` - -**Step 1: Write the failing paper-adjacent check** - -If the model tests do not already assert the issue example used in the paper, extend `test_integral_flow_homologous_arcs_paper_example` first so the paper instance is executable in Rust before editing Typst. - -**Step 2: Implement the paper entry** - -Add: -- display-name dictionary entry for `IntegralFlowHomologousArcs` -- `#problem-def("IntegralFlowHomologousArcs")[...]` with self-contained notation -- background that cites Garey-Johnson / Sahni and notes the LP-equivalent fractional variant -- a worked example matching the canonical YES instance -- `pred-commands()` using the canonical example-db entry rather than a hand-written bare alias -- wording that records `2^num_arcs` as the unit-capacity special case while the code registers the general-capacity bound - -Use the existing directed-flow paper entries as the style reference. - -**Step 3: Run the paper build and verify GREEN** - -Run: `make paper` - -Expected: Typst compiles without warnings/errors attributable to the new entry. - -## Final Verification - -### Task 6: Run repo verification and prepare the branch for PR push - -**Files:** -- Modify only as needed from prior tasks - -**Step 1: Run the required verification** - -Run: -- `cargo test integral_flow_homologous_arcs` -- `cargo test -p problemreductions-cli integral_flow_homologous_arcs` -- `make test` -- `make clippy` -- `make paper` - -**Step 2: Inspect the tree** - -Run: `git status --short` - -Expected: only intentional tracked changes remain; ignored generated docs under `docs/src/reductions/` may appear but must not be staged accidentally. - -**Step 3: Commit coherent implementation changes** - -Suggested messages: -- `Add IntegralFlowHomologousArcs model and CLI support` -- `Document IntegralFlowHomologousArcs in paper` - -**Step 4: Remove this plan file before final push** - -Run: -- `git rm docs/plans/2026-03-22-integral-flow-homologous-arcs-model.md` -- `git commit -m "chore: remove plan file after implementation"` From f24becb752529f156aba7797f7b0c574be1ff35a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 13:55:58 +0800 Subject: [PATCH 4/5] fix: resolve formatting after merge with main --- problemreductions-cli/tests/cli_tests.rs | 3 ++- src/lib.rs | 6 +++--- src/unit_tests/example_db.rs | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 7676be89..7cc95c1c 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -670,7 +670,8 @@ fn test_create_undirected_two_commodity_integral_flow_rejects_out_of_range_termi #[test] fn test_create_integral_flow_homologous_arcs() { - let output_file = std::env::temp_dir().join("pred_test_create_integral_flow_homologous_arcs.json"); + let output_file = + std::env::temp_dir().join("pred_test_create_integral_flow_homologous_arcs.json"); let output = pred() .args([ "-o", diff --git a/src/lib.rs b/src/lib.rs index 757ec76e..1b26c905 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,9 +52,9 @@ pub mod prelude { BiconnectivityAugmentation, BottleneckTravelingSalesman, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, - IsomorphicSpanningTree, KClique, - KthBestSpanningTree, LengthBoundedDisjointPaths, MixedChinesePostman, SpinGlass, - SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, + IsomorphicSpanningTree, KClique, KthBestSpanningTree, LengthBoundedDisjointPaths, + MixedChinesePostman, SpinGlass, SteinerTree, StrongConnectivityAugmentation, + SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 39dc584a..102ca120 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -140,7 +140,10 @@ fn test_find_model_example_integral_flow_homologous_arcs() { assert_eq!(example.problem, "IntegralFlowHomologousArcs"); assert_eq!(example.variant, problem.variant); assert_eq!(example.instance["requirement"], 2); - assert_eq!(example.instance["homologous_pairs"], serde_json::json!([[2, 5], [4, 3]])); + assert_eq!( + example.instance["homologous_pairs"], + serde_json::json!([[2, 5], [4, 3]]) + ); assert_eq!(example.optimal_config, vec![1, 1, 1, 0, 0, 1, 1, 1]); } From 1bdede01825204da721c3495ad04e5687c02b233 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 14:02:26 +0800 Subject: [PATCH 5/5] fix: simplify homologous-pair iteration and add non-unit-capacity test --- .../graph/integral_flow_homologous_arcs.rs | 2 +- .../graph/integral_flow_homologous_arcs.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index 75fc4cf7..51ae2b0c 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -170,7 +170,7 @@ impl Problem for IntegralFlowHomologousArcs { return false; } - for (&(a, b), _) in self.homologous_pairs.iter().zip(std::iter::repeat(())) { + for &(a, b) in &self.homologous_pairs { if config[a] != config[b] { return false; } diff --git a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs index 948d3787..e9cfb245 100644 --- a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs +++ b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs @@ -114,6 +114,22 @@ fn test_integral_flow_homologous_arcs_problem_name() { ); } +#[test] +fn test_integral_flow_homologous_arcs_non_unit_capacity() { + // s=0 -> 1 -> 2=t, with capacities [3, 3], homologous pair (0,1) so both arcs carry + // equal flow. R=2 is satisfiable: f=[2,2]. + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = IntegralFlowHomologousArcs::new(graph, vec![3, 3], 0, 2, 2, vec![(0, 1)]); + assert_eq!(problem.dims(), vec![4, 4]); + assert_eq!(problem.max_capacity(), 3); + assert!(problem.evaluate(&[2, 2])); + assert!(problem.evaluate(&[3, 3])); + assert!(!problem.evaluate(&[2, 3])); // homologous violation + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert_eq!(solutions.len(), 2); // [2,2] and [3,3] +} + #[test] fn test_integral_flow_homologous_arcs_paper_example() { let problem = yes_instance();