From 2f23ee7b5f4e0ef62c65a4909ccd2f08168d1dc9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 10:24:04 +0800 Subject: [PATCH 1/5] Add plan for #411: [Model] CapacityAssignment --- docs/plans/2026-03-22-capacity-assignment.md | 257 +++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 docs/plans/2026-03-22-capacity-assignment.md diff --git a/docs/plans/2026-03-22-capacity-assignment.md b/docs/plans/2026-03-22-capacity-assignment.md new file mode 100644 index 000000000..b78b4ae68 --- /dev/null +++ b/docs/plans/2026-03-22-capacity-assignment.md @@ -0,0 +1,257 @@ +# CapacityAssignment Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `CapacityAssignment` model, CLI creation path, canonical example, tests, and paper entry required by issue `#411`. + +**Architecture:** Implement `CapacityAssignment` as a `misc` satisfaction problem with one multi-valued variable per communication link, where each variable selects an index into a shared ordered capacity set. The model owns the capacity levels, per-link cost/delay matrices, and the two budgets; `evaluate()` checks config length/range, then sums cost and delay and returns `true` iff both budgets are met. The issue already names the planned connecting rule `SubsetSum -> CapacityAssignment`, so this model is not an orphan even though no separate open rule issue currently matches the name. + +**Tech Stack:** Rust workspace (`serde`, `inventory`, registry macros), Clap CLI, Typst paper, `make` verification targets. + +--- + +## Batch 1: Model, Registry, Example, CLI + +### Task 1: Write failing model tests for issue #411 behavior + +**Files:** +- Create: `src/unit_tests/models/misc/capacity_assignment.rs` +- Reference: `src/models/misc/multiprocessor_scheduling.rs` +- Reference: `src/unit_tests/models/misc/multiprocessor_scheduling.rs` + +**Step 1: Write the failing test** + +Add focused tests for: +- construction/getters/dimensions on the fixed 3-link issue example +- `evaluate()` returning `true` for the validated witnesses `(2,2,2)` and `(1,2,3)` and `false` for `(1,1,1)` and `(3,3,3)` after converting to zero-based config indices +- invalid configs (wrong length and out-of-range capacity index) returning `false` +- brute-force finding exactly 5 satisfying assignments for the canonical 3-link example +- serde round-trip for the model + +**Step 2: Run test to verify it fails** + +Run: `cargo test capacity_assignment --lib` +Expected: FAIL because `CapacityAssignment` and its test module do not exist yet. + +**Step 3: Write minimal implementation** + +Do not implement production code in this task. Leave the failing test as the red state for the next task. + +**Step 4: Run test to verify it still fails for the expected reason** + +Run: `cargo test capacity_assignment --lib` +Expected: FAIL with missing type/module errors, not unrelated syntax errors. + +**Step 5: Commit** + +Do not commit yet; combine with Task 2 after the model exists and the tests are green. + +### Task 2: Implement the `CapacityAssignment` model and register it in the crate + +**Files:** +- Create: `src/models/misc/capacity_assignment.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Test: `src/unit_tests/models/misc/capacity_assignment.rs` + +**Step 1: Write the failing test** + +Use the red tests from Task 1 as the only driver for this task. + +**Step 2: Run test to verify it fails** + +Run: `cargo test capacity_assignment --lib` +Expected: FAIL for the missing model implementation. + +**Step 3: Write minimal implementation** + +Implement: +- `ProblemSchemaEntry` with constructor-facing fields: `capacities`, `cost`, `delay`, `cost_budget`, `delay_budget` +- `CapacityAssignment` struct with serde derives and constructor validation for: + - non-empty capacity list + - rectangular `cost` and `delay` matrices + - `cost.len() == delay.len()` and each row length matching `capacities.len()` + - monotonicity (`cost[i][j] <= cost[i][j+1]`, `delay[i][j] >= delay[i][j+1]`) +- getters `num_links()`, `num_capacities()`, `capacities()`, `cost()`, `delay()`, `cost_budget()`, `delay_budget()` +- `Problem` impl with `dims() == vec![num_capacities; num_links]` +- `evaluate()` that rejects bad config lengths / indices and checks both budgets +- `SatisfactionProblem` impl +- `declare_variants! { default sat CapacityAssignment => "num_capacities^num_links" }` +- `canonical_model_example_specs()` using the corrected 3-link YES instance from the issue +- module/export wiring in `src/models/misc/mod.rs`, `src/models/mod.rs`, and `src/lib.rs` + +**Step 4: Run test to verify it passes** + +Run: `cargo test capacity_assignment --lib` +Expected: PASS for the new model tests. + +**Step 5: Commit** + +```bash +git add src/models/misc/capacity_assignment.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/capacity_assignment.rs +git commit -m "feat: add CapacityAssignment model" +``` + +### Task 3: Add failing CLI tests for `pred create CapacityAssignment` + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Reference: `problemreductions-cli/src/cli.rs` + +**Step 1: Write the failing test** + +Add `create.rs` unit tests that exercise: +- successful creation with dedicated CapacityAssignment arguments +- informative failures for mismatched row widths / row counts +- rejection of non-monotone cost or delay rows + +Use a concrete CLI shape for this plan: +- `--capacities 1,2,3` +- `--cost-matrix "1,3,6;2,4,7;1,2,5"` +- `--delay-matrix "8,4,1;7,3,1;6,3,1"` +- `--cost-budget 10` +- `--delay-budget 12` + +**Step 2: Run test to verify it fails** + +Run: `cargo test -p problemreductions-cli capacity_assignment` +Expected: FAIL because the CLI flags / match arm do not exist yet. + +**Step 3: Write minimal implementation** + +Do not add production CLI code in this task. Leave the red test in place for Task 4. + +**Step 4: Run test to verify it still fails for the expected reason** + +Run: `cargo test -p problemreductions-cli capacity_assignment` +Expected: FAIL with missing flag / match-arm behavior, not unrelated parser errors. + +**Step 5: Commit** + +Do not commit yet; combine with Task 4 after the CLI path is green. + +### Task 4: Implement CLI creation support and help text + +**Files:** +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Test: `problemreductions-cli/src/commands/create.rs` + +**Step 1: Write the failing test** + +Use the red tests from Task 3 as the driver. + +**Step 2: Run test to verify it fails** + +Run: `cargo test -p problemreductions-cli capacity_assignment` +Expected: FAIL because `pred create CapacityAssignment` is still unsupported. + +**Step 3: Write minimal implementation** + +Implement: +- new `CreateArgs` fields for `--cost-matrix`, `--delay-matrix`, `--cost-budget`, `--delay-budget` +- `all_data_flags_empty()` updates for the new flags +- help-table entry and usage example in `problemreductions-cli/src/cli.rs` +- parsing helpers in `create.rs` for rectangular `u64` matrices using the existing semicolon/comma matrix style +- `create()` match arm constructing `CapacityAssignment::new(...)` +- CLI tests proving valid creation and the targeted validation failures above + +Do not add a short alias; canonical-name lookup is already case-insensitive through the registry. + +**Step 4: Run test to verify it passes** + +Run: `cargo test -p problemreductions-cli capacity_assignment` +Expected: PASS for the new CLI tests. + +**Step 5: Commit** + +```bash +git add problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs +git commit -m "feat: add CapacityAssignment CLI support" +``` + +## Batch 2: Paper Entry + +### Task 5: Add the paper definition and paper-aligned regression test + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `src/unit_tests/models/misc/capacity_assignment.rs` + +**Step 1: Write the failing test** + +Add a `test_capacity_assignment_paper_example` that: +- constructs the exact 3-link paper example +- checks the documented satisfying config +- uses `BruteForce::find_all_satisfying()` to confirm the documented count of 5 satisfying assignments + +**Step 2: Run test to verify it fails** + +Run: `cargo test capacity_assignment_paper_example --lib` +Expected: FAIL because the paper-aligned test is not written yet. + +**Step 3: Write minimal implementation** + +Update `docs/paper/reductions.typ` with: +- display-name entry for `CapacityAssignment` +- `problem-def("CapacityAssignment")` with the formal GJ definition +- brief background and the corrected Van Sickle/Chandy IFIP 1977 citation +- algorithm paragraph noting brute force and pseudo-polynomial DP with citations +- a worked 3-link example consistent with the canonical example data +- `pred-commands()` based on the canonical example export pattern + +Then finish the paper-aligned unit test. + +**Step 4: Run test to verify it passes** + +Run: +- `cargo test capacity_assignment_paper_example --lib` +- `make paper` + +Expected: PASS for the test and a clean Typst build. + +**Step 5: Commit** + +```bash +git add docs/paper/reductions.typ src/unit_tests/models/misc/capacity_assignment.rs +git commit -m "docs: add CapacityAssignment paper entry" +``` + +### Task 6: Final verification before push + +**Files:** +- Verify only; no new files unless generated tracked outputs change legitimately + +**Step 1: Write the failing test** + +No new tests. This task is verification-only. + +**Step 2: Run test to verify it fails** + +Skip. Use fresh full-project verification instead. + +**Step 3: Write minimal implementation** + +No implementation changes unless verification exposes a real failure. If it does, fix the smallest failing scope first and re-run the relevant command before returning to the full suite. + +**Step 4: Run test to verify it passes** + +Run: +- `make test` +- `make clippy` +- `make paper` + +Inspect: +- `git status --short` + +Expected: all commands exit 0; only intended tracked changes remain staged or ready to stage. + +**Step 5: Commit** + +If verification fixes were required: + +```bash +git add -A +git commit -m "chore: finish CapacityAssignment verification" +``` From 2b4da29a5f025c9331bf9dff133a146c5cfea168 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 10:38:29 +0800 Subject: [PATCH 2/5] Implement #411: [Model] CapacityAssignment --- docs/paper/reductions.typ | 34 +++ docs/paper/references.bib | 8 + problemreductions-cli/src/cli.rs | 14 + problemreductions-cli/src/commands/create.rs | 250 +++++++++++++++++- src/lib.rs | 2 +- src/models/misc/capacity_assignment.rs | 200 ++++++++++++++ src/models/misc/mod.rs | 3 + src/models/mod.rs | 9 +- .../models/misc/capacity_assignment.rs | 95 +++++++ 9 files changed, 601 insertions(+), 14 deletions(-) create mode 100644 src/models/misc/capacity_assignment.rs create mode 100644 src/unit_tests/models/misc/capacity_assignment.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 9167a0c15..0b9da56d9 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -113,6 +113,7 @@ "BoundedComponentSpanningForest": [Bounded Component Spanning Forest], "BinPacking": [Bin Packing], "BoyceCoddNormalFormViolation": [Boyce-Codd Normal Form Violation], + "CapacityAssignment": [Capacity Assignment], "ConsistencyOfDatabaseFrequencyTables": [Consistency of Database Frequency Tables], "ClosestVectorProblem": [Closest Vector Problem], "ConsecutiveSets": [Consecutive Sets], @@ -4693,6 +4694,39 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("CapacityAssignment") + [ + #problem-def("CapacityAssignment")[ + Given a finite set $C$ of communication links, an ordered set $M subset ZZ_(> 0)$ of capacities, cost and delay functions $g: C times M -> ZZ_(>= 0)$ and $d: C times M -> ZZ_(>= 0)$ such that for every $c in C$ and $i < j$ in the order of $M$ we have $g(c, i) <= g(c, j)$ and $d(c, i) >= d(c, j)$, and budgets $K, J in ZZ_(>= 0)$, determine whether there exists an assignment $sigma: C -> M$ such that $sum_(c in C) g(c, sigma(c)) <= K$ and $sum_(c in C) d(c, sigma(c)) <= J$. + ][ + Capacity Assignment is the bicriteria communication-network design problem SR7 in Garey & Johnson @garey1979. The original NP-completeness proof, via reduction from Subset Sum, is due to Van Sickle and Chandy @vansicklechandy1977. The model captures discrete provisioning of communication links, where upgrading a link increases installation cost but decreases delay. The direct witness encoding implemented in this repository yields an $O^*(|M|^(|C|))$ exact algorithm by brute-force enumeration#footnote[No algorithm improving on brute-force enumeration is known for the exact witness encoding used in this repository.]. Garey and Johnson also note a pseudo-polynomial dynamic-programming formulation when the budgets are small @garey1979. + + *Example.* Let $C = {c_1, c_2, c_3}$, $M = {1, 2, 3}$, $K = 10$, and $J = 12$. With cost rows $(1, 3, 6)$, $(2, 4, 7)$, $(1, 2, 5)$ and delay rows $(8, 4, 1)$, $(7, 3, 1)$, $(6, 3, 1)$, the assignment $sigma = (2, 2, 2)$ has total cost $3 + 4 + 2 = 9 <= 10$ and total delay $4 + 3 + 3 = 10 <= 12$, so the instance is satisfiable. Brute-force enumeration finds exactly 5 satisfying assignments; for contrast, $sigma = (1, 1, 1)$ violates the delay budget and $sigma = (3, 3, 3)$ violates the cost budget. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o capacity-assignment.json", + "pred solve capacity-assignment.json --solver brute-force", + "pred evaluate capacity-assignment.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure({ + table( + columns: (auto, auto, auto), + inset: 4pt, + align: left, + table.header([*Link*], [*Cost row*], [*Delay row*]), + [$c_1$], [$(1, 3, 6)$], [$(8, 4, 1)$], + [$c_2$], [$(2, 4, 7)$], [$(7, 3, 1)$], + [$c_3$], [$(1, 2, 5)$], [$(6, 3, 1)$], + ) + }, + caption: [Canonical Capacity Assignment instance with budgets $K = 10$ and $J = 12$. Each row lists the cost-delay trade-off for one communication link.], + ) + ] + ] +} + #{ let x = load-model-example("PrecedenceConstrainedScheduling") let n = x.instance.num_tasks diff --git a/docs/paper/references.bib b/docs/paper/references.bib index f24f1a092..769af5ac0 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -180,6 +180,14 @@ @book{garey1979 year = {1979} } +@inproceedings{vansicklechandy1977, + author = {Lawrence Van Sickle and K. Mani Chandy}, + title = {Computational Complexity of Network Design Algorithms}, + booktitle = {IFIP Congress 77}, + pages = {235--239}, + year = {1977} +} + @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 02df552d7..8649a7674 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -239,6 +239,7 @@ Flags by problem type: LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound Factoring --target, --m, --n BinPacking --sizes, --capacity + CapacityAssignment --capacities, --cost-matrix, --delay-matrix, --cost-budget, --delay-budget SubsetSum --sizes, --target SumOfSquaresPartition --sizes, --num-groups, --bound PaintShop --sequence @@ -312,6 +313,7 @@ Examples: pred create MIS --graph 0-1,1-2,2-3 --weights 1,1,1 pred create SAT --num-vars 3 --clauses \"1,2;-1,3\" pred create QUBO --matrix \"1,0.5;0.5,2\" + pred create CapacityAssignment --capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12 pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5 pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10 pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force @@ -356,6 +358,12 @@ pub struct CreateArgs { /// Edge capacities for multicommodity flow problems (e.g., 1,1,2) #[arg(long)] pub capacities: Option, + /// Cost matrix for CapacityAssignment (semicolon-separated rows, e.g., "1,3,6;2,4,7") + #[arg(long)] + pub cost_matrix: Option, + /// Delay matrix for CapacityAssignment (semicolon-separated rows, e.g., "8,4,1;7,3,1") + #[arg(long)] + pub delay_matrix: Option, /// Source vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub source: Option, @@ -513,6 +521,12 @@ pub struct CreateArgs { /// Upper bound on total inter-partition arc cost #[arg(long)] pub cost_bound: Option, + /// Budget on total cost for CapacityAssignment + #[arg(long)] + pub cost_budget: Option, + /// Budget on total delay penalty for CapacityAssignment + #[arg(long)] + pub delay_budget: Option, /// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0) #[arg(long)] pub pattern: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index d03de0341..610b01d2b 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -18,15 +18,16 @@ use problemreductions::models::graph::{ StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ - AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, - ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, FlowShopScheduling, FrequencyTable, - KnownValue, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, - PaintShop, PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, - SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, - SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, TimetableDesign, + AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, + ConjunctiveBooleanQuery, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, + FlowShopScheduling, FrequencyTable, KnownValue, LongestCommonSubsequence, + MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, + QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, + SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, + SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, + ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, + TimetableDesign, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; @@ -50,6 +51,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.edge_weights.is_none() && args.edge_lengths.is_none() && args.capacities.is_none() + && args.cost_matrix.is_none() + && args.delay_matrix.is_none() && args.source.is_none() && args.sink.is_none() && args.num_paths_required.is_none() @@ -102,6 +105,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.length_bound.is_none() && args.weight_bound.is_none() && args.cost_bound.is_none() + && args.cost_budget.is_none() + && args.delay_budget.is_none() && args.pattern.is_none() && args.strings.is_none() && args.costs.is_none() @@ -564,6 +569,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", + "CapacityAssignment" => { + "--capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12" + } "MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10", "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", "SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1", @@ -2610,6 +2618,90 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + "CapacityAssignment" => { + let usage = "Usage: pred create CapacityAssignment --capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12"; + let capacities_str = args.capacities.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "CapacityAssignment requires --capacities, --cost-matrix, --delay-matrix, --cost-budget, and --delay-budget\n\n{usage}" + ) + })?; + let cost_matrix_str = args.cost_matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --cost-matrix\n\n{usage}") + })?; + let delay_matrix_str = args.delay_matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --delay-matrix\n\n{usage}") + })?; + let cost_budget = args.cost_budget.ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --cost-budget\n\n{usage}") + })?; + let delay_budget = args.delay_budget.ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --delay-budget\n\n{usage}") + })?; + + let capacities: Vec = util::parse_comma_list(capacities_str)?; + anyhow::ensure!( + !capacities.is_empty(), + "CapacityAssignment requires at least one capacity value\n\n{usage}" + ); + anyhow::ensure!( + capacities.iter().all(|&capacity| capacity > 0), + "CapacityAssignment capacities must be positive\n\n{usage}" + ); + anyhow::ensure!( + capacities.windows(2).all(|w| w[0] < w[1]), + "CapacityAssignment capacities must be strictly increasing\n\n{usage}" + ); + + let cost = parse_u64_matrix_rows(cost_matrix_str, "cost")?; + let delay = parse_u64_matrix_rows(delay_matrix_str, "delay")?; + anyhow::ensure!( + cost.len() == delay.len(), + "cost matrix row count ({}) must match delay matrix row count ({})\n\n{usage}", + cost.len(), + delay.len() + ); + + for (index, row) in cost.iter().enumerate() { + anyhow::ensure!( + row.len() == capacities.len(), + "cost row {} length ({}) must match capacities length ({})\n\n{usage}", + index, + row.len(), + capacities.len() + ); + anyhow::ensure!( + row.windows(2).all(|w| w[0] <= w[1]), + "cost row {} must be non-decreasing\n\n{usage}", + index + ); + } + for (index, row) in delay.iter().enumerate() { + anyhow::ensure!( + row.len() == capacities.len(), + "delay row {} length ({}) must match capacities length ({})\n\n{usage}", + index, + row.len(), + capacities.len() + ); + anyhow::ensure!( + row.windows(2).all(|w| w[0] >= w[1]), + "delay row {} must be non-increasing\n\n{usage}", + index + ); + } + + ( + ser(CapacityAssignment::new( + capacities, + cost, + delay, + cost_budget, + delay_budget, + ))?, + resolved_variant.clone(), + ) + } + // MinimumMultiwayCut "MinimumMultiwayCut" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -4744,6 +4836,31 @@ fn parse_matrix(args: &CreateArgs) -> Result>> { .collect() } +fn parse_u64_matrix_rows(matrix_str: &str, matrix_name: &str) -> Result>> { + matrix_str + .split(';') + .enumerate() + .map(|(row_index, row)| { + let row = row.trim(); + anyhow::ensure!( + !row.is_empty(), + "{matrix_name} row {row_index} must not be empty" + ); + row.split(',') + .map(|value| { + value.trim().parse::().map_err(|error| { + anyhow::anyhow!( + "Invalid {matrix_name} row {row_index} value {:?}: {}", + value.trim(), + error + ) + }) + }) + .collect() + }) + .collect() +} + /// Parse `--quantifiers` as comma-separated quantifier labels (E/A or Exists/ForAll). /// E.g., "E,A,E" or "Exists,ForAll,Exists" fn parse_quantifiers(args: &CreateArgs, num_vars: usize) -> Result> { @@ -5860,6 +5977,117 @@ mod tests { assert!(err.to_string().contains("GeneralizedHex requires --sink")); } + #[test] + fn test_create_capacity_assignment_serializes_problem_json() { + let output = temp_output_path("capacity_assignment_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3,6;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--cost-budget", + "10", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: cli.output.clone(), + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + create(&args, &out).unwrap(); + + let json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap(); + fs::remove_file(&output).unwrap(); + assert_eq!(json["type"], "CapacityAssignment"); + assert_eq!(json["data"]["capacities"], serde_json::json!([1, 2, 3])); + assert_eq!(json["data"]["cost_budget"], 10); + assert_eq!(json["data"]["delay_budget"], 12); + } + + #[test] + fn test_create_capacity_assignment_rejects_non_monotone_cost_row() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3,2;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--cost-budget", + "10", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("cost row 0")); + assert!(err.contains("non-decreasing")); + } + + #[test] + fn test_create_capacity_assignment_rejects_matrix_width_mismatch() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--cost-budget", + "10", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + let args = match cli.command { + Commands::Create(args) => args, + _ => unreachable!(), + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("cost row 0")); + assert!(err.contains("capacities length")); + } + fn empty_args() -> CreateArgs { CreateArgs { problem: Some("BiconnectivityAugmentation".to_string()), @@ -5871,6 +6099,8 @@ mod tests { edge_weights: None, edge_lengths: None, capacities: None, + cost_matrix: None, + delay_matrix: None, source: None, sink: None, num_paths_required: None, @@ -5923,6 +6153,8 @@ mod tests { length_bound: None, weight_bound: None, cost_bound: None, + cost_budget: None, + delay_budget: None, pattern: None, strings: None, arc_costs: None, diff --git a/src/lib.rs b/src/lib.rs index f9e84dca0..48dd0a9bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,7 @@ pub mod prelude { UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ - AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, + AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, Partition, QueryArg, diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs new file mode 100644 index 000000000..46d889b4c --- /dev/null +++ b/src/models/misc/capacity_assignment.rs @@ -0,0 +1,200 @@ +//! Capacity Assignment problem implementation. +//! +//! Capacity Assignment asks whether each communication link can be assigned +//! one capacity level so that total cost and total delay both stay within +//! their respective budgets. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "CapacityAssignment", + display_name: "Capacity Assignment", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Assign capacities to links while respecting cost and delay budgets", + fields: &[ + FieldInfo { name: "capacities", type_name: "Vec", description: "Ordered capacity levels M" }, + FieldInfo { name: "cost", type_name: "Vec>", description: "Cost matrix g(c, m) for each link and capacity" }, + FieldInfo { name: "delay", type_name: "Vec>", description: "Delay matrix d(c, m) for each link and capacity" }, + FieldInfo { name: "cost_budget", type_name: "u64", description: "Budget K on total cost" }, + FieldInfo { name: "delay_budget", type_name: "u64", description: "Budget J on total delay penalty" }, + ], + } +} + +/// Capacity Assignment feasibility problem. +/// +/// Each variable chooses one capacity index for one communication link. +/// Costs are monotone non-decreasing and delays are monotone non-increasing +/// with respect to the ordered capacity list. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CapacityAssignment { + capacities: Vec, + cost: Vec>, + delay: Vec>, + cost_budget: u64, + delay_budget: u64, +} + +impl CapacityAssignment { + /// Create a new Capacity Assignment instance. + pub fn new( + capacities: Vec, + cost: Vec>, + delay: Vec>, + cost_budget: u64, + delay_budget: u64, + ) -> Self { + assert!(!capacities.is_empty(), "capacities must be non-empty"); + assert!( + capacities.iter().all(|&capacity| capacity > 0), + "capacities must be positive" + ); + assert!( + capacities.windows(2).all(|w| w[0] < w[1]), + "capacities must be strictly increasing" + ); + assert_eq!( + cost.len(), + delay.len(), + "cost and delay must have the same number of links" + ); + + let num_capacities = capacities.len(); + for (link, row) in cost.iter().enumerate() { + assert_eq!( + row.len(), + num_capacities, + "cost row {link} length must match capacities length" + ); + assert!( + row.windows(2).all(|w| w[0] <= w[1]), + "cost row {link} must be non-decreasing" + ); + } + for (link, row) in delay.iter().enumerate() { + assert_eq!( + row.len(), + num_capacities, + "delay row {link} length must match capacities length" + ); + assert!( + row.windows(2).all(|w| w[0] >= w[1]), + "delay row {link} must be non-increasing" + ); + } + + Self { + capacities, + cost, + delay, + cost_budget, + delay_budget, + } + } + + /// Number of communication links. + pub fn num_links(&self) -> usize { + self.cost.len() + } + + /// Number of discrete capacity choices per link. + pub fn num_capacities(&self) -> usize { + self.capacities.len() + } + + /// Ordered capacity levels. + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + /// Cost matrix indexed by link, then capacity. + pub fn cost(&self) -> &[Vec] { + &self.cost + } + + /// Delay matrix indexed by link, then capacity. + pub fn delay(&self) -> &[Vec] { + &self.delay + } + + /// Total cost budget. + pub fn cost_budget(&self) -> u64 { + self.cost_budget + } + + /// Total delay budget. + pub fn delay_budget(&self) -> u64 { + self.delay_budget + } + + fn total_cost_and_delay(&self, config: &[usize]) -> Option<(u128, u128)> { + if config.len() != self.num_links() { + return None; + } + + let num_capacities = self.num_capacities(); + let mut total_cost = 0u128; + let mut total_delay = 0u128; + + for (link, &choice) in config.iter().enumerate() { + if choice >= num_capacities { + return None; + } + total_cost += self.cost[link][choice] as u128; + total_delay += self.delay[link][choice] as u128; + } + + Some((total_cost, total_delay)) + } +} + +impl Problem for CapacityAssignment { + const NAME: &'static str = "CapacityAssignment"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![self.num_capacities(); self.num_links()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let Some((total_cost, total_delay)) = self.total_cost_and_delay(config) else { + return false; + }; + total_cost <= self.cost_budget as u128 && total_delay <= self.delay_budget as u128 + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for CapacityAssignment {} + +crate::declare_variants! { + default sat CapacityAssignment => "num_capacities ^ num_links", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "capacity_assignment", + instance: Box::new(CapacityAssignment::new( + vec![1, 2, 3], + vec![vec![1, 3, 6], vec![2, 4, 7], vec![1, 2, 5]], + vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], + 10, + 12, + )), + optimal_config: vec![1, 1, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/capacity_assignment.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index e3b86f58b..33b0aa36f 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -35,6 +35,7 @@ pub(crate) mod additional_key; mod bin_packing; mod boyce_codd_normal_form_violation; +mod capacity_assignment; pub(crate) mod conjunctive_boolean_query; pub(crate) mod conjunctive_query_foldability; mod consistency_of_database_frequency_tables; @@ -68,6 +69,7 @@ mod timetable_design; pub use additional_key::AdditionalKey; pub use bin_packing::BinPacking; pub use boyce_codd_normal_form_violation::BoyceCoddNormalFormViolation; +pub use capacity_assignment::CapacityAssignment; pub use conjunctive_boolean_query::{ConjunctiveBooleanQuery, QueryArg, Relation as CbqRelation}; pub use conjunctive_query_foldability::{ConjunctiveQueryFoldability, Term}; pub use consistency_of_database_frequency_tables::{ @@ -104,6 +106,7 @@ pub use timetable_design::TimetableDesign; pub(crate) fn canonical_model_example_specs() -> Vec { let mut specs = Vec::new(); specs.extend(boyce_codd_normal_form_violation::canonical_model_example_specs()); + specs.extend(capacity_assignment::canonical_model_example_specs()); specs.extend(consistency_of_database_frequency_tables::canonical_model_example_specs()); specs.extend(conjunctive_boolean_query::canonical_model_example_specs()); specs.extend(conjunctive_query_foldability::canonical_model_example_specs()); diff --git a/src/models/mod.rs b/src/models/mod.rs index d15d4f5d6..0bde4d370 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -33,10 +33,11 @@ pub use graph::{ }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ - AdditionalKey, BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, - ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, Factoring, FlowShopScheduling, - Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, - PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, + AdditionalKey, BinPacking, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, + ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, + Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, + MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, Partition, + PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, diff --git a/src/unit_tests/models/misc/capacity_assignment.rs b/src/unit_tests/models/misc/capacity_assignment.rs new file mode 100644 index 000000000..b7ebc5fc5 --- /dev/null +++ b/src/unit_tests/models/misc/capacity_assignment.rs @@ -0,0 +1,95 @@ +use crate::models::misc::CapacityAssignment; +use crate::solvers::BruteForce; +use crate::traits::Problem; + +fn example_problem() -> CapacityAssignment { + CapacityAssignment::new( + vec![1, 2, 3], + vec![vec![1, 3, 6], vec![2, 4, 7], vec![1, 2, 5]], + vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], + 10, + 12, + ) +} + +#[test] +fn test_capacity_assignment_basic_properties() { + let problem = example_problem(); + assert_eq!(problem.num_links(), 3); + assert_eq!(problem.num_capacities(), 3); + assert_eq!(problem.capacities(), &[1, 2, 3]); + assert_eq!(problem.cost_budget(), 10); + assert_eq!(problem.delay_budget(), 12); + assert_eq!(problem.dims(), vec![3, 3, 3]); + assert_eq!( + ::NAME, + "CapacityAssignment" + ); + assert_eq!(::variant(), Vec::new()); +} + +#[test] +fn test_capacity_assignment_evaluate_yes_and_no_examples() { + let problem = example_problem(); + assert!(problem.evaluate(&[1, 1, 1])); + assert!(problem.evaluate(&[0, 1, 2])); + assert!(!problem.evaluate(&[0, 0, 0])); + assert!(!problem.evaluate(&[2, 2, 2])); +} + +#[test] +fn test_capacity_assignment_rejects_invalid_configs() { + let problem = example_problem(); + assert!(!problem.evaluate(&[1, 1])); + assert!(!problem.evaluate(&[1, 1, 3])); +} + +#[test] +fn test_capacity_assignment_bruteforce_solution_count() { + let problem = example_problem(); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert_eq!(solutions.len(), 5); + assert!(solutions.contains(&vec![1, 1, 1])); + assert!(solutions.contains(&vec![0, 1, 2])); +} + +#[test] +fn test_capacity_assignment_serialization_round_trip() { + let problem = example_problem(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: CapacityAssignment = serde_json::from_value(json).unwrap(); + assert_eq!(restored.capacities(), problem.capacities()); + assert_eq!(restored.cost(), problem.cost()); + assert_eq!(restored.delay(), problem.delay()); + assert_eq!(restored.cost_budget(), problem.cost_budget()); + assert_eq!(restored.delay_budget(), problem.delay_budget()); +} + +#[test] +fn test_capacity_assignment_paper_example() { + let problem = example_problem(); + let config = vec![1, 1, 1]; + assert!(problem.evaluate(&config)); + + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert_eq!(solutions.len(), 5); + assert!(solutions.contains(&config)); +} + +#[test] +fn test_capacity_assignment_rejects_non_increasing_capacities() { + let result = std::panic::catch_unwind(|| { + CapacityAssignment::new(vec![1, 1], vec![vec![1, 2]], vec![vec![2, 1]], 3, 3) + }); + assert!(result.is_err()); +} + +#[test] +fn test_capacity_assignment_rejects_non_monotone_delay_row() { + let result = std::panic::catch_unwind(|| { + CapacityAssignment::new(vec![1, 2], vec![vec![1, 2]], vec![vec![1, 2]], 3, 3) + }); + assert!(result.is_err()); +} From ee98489ca1d870a141d8a7b09a322f4e1dbcbb02 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 10:38:34 +0800 Subject: [PATCH 3/5] chore: remove plan file after implementation --- docs/plans/2026-03-22-capacity-assignment.md | 257 ------------------- 1 file changed, 257 deletions(-) delete mode 100644 docs/plans/2026-03-22-capacity-assignment.md diff --git a/docs/plans/2026-03-22-capacity-assignment.md b/docs/plans/2026-03-22-capacity-assignment.md deleted file mode 100644 index b78b4ae68..000000000 --- a/docs/plans/2026-03-22-capacity-assignment.md +++ /dev/null @@ -1,257 +0,0 @@ -# CapacityAssignment Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `CapacityAssignment` model, CLI creation path, canonical example, tests, and paper entry required by issue `#411`. - -**Architecture:** Implement `CapacityAssignment` as a `misc` satisfaction problem with one multi-valued variable per communication link, where each variable selects an index into a shared ordered capacity set. The model owns the capacity levels, per-link cost/delay matrices, and the two budgets; `evaluate()` checks config length/range, then sums cost and delay and returns `true` iff both budgets are met. The issue already names the planned connecting rule `SubsetSum -> CapacityAssignment`, so this model is not an orphan even though no separate open rule issue currently matches the name. - -**Tech Stack:** Rust workspace (`serde`, `inventory`, registry macros), Clap CLI, Typst paper, `make` verification targets. - ---- - -## Batch 1: Model, Registry, Example, CLI - -### Task 1: Write failing model tests for issue #411 behavior - -**Files:** -- Create: `src/unit_tests/models/misc/capacity_assignment.rs` -- Reference: `src/models/misc/multiprocessor_scheduling.rs` -- Reference: `src/unit_tests/models/misc/multiprocessor_scheduling.rs` - -**Step 1: Write the failing test** - -Add focused tests for: -- construction/getters/dimensions on the fixed 3-link issue example -- `evaluate()` returning `true` for the validated witnesses `(2,2,2)` and `(1,2,3)` and `false` for `(1,1,1)` and `(3,3,3)` after converting to zero-based config indices -- invalid configs (wrong length and out-of-range capacity index) returning `false` -- brute-force finding exactly 5 satisfying assignments for the canonical 3-link example -- serde round-trip for the model - -**Step 2: Run test to verify it fails** - -Run: `cargo test capacity_assignment --lib` -Expected: FAIL because `CapacityAssignment` and its test module do not exist yet. - -**Step 3: Write minimal implementation** - -Do not implement production code in this task. Leave the failing test as the red state for the next task. - -**Step 4: Run test to verify it still fails for the expected reason** - -Run: `cargo test capacity_assignment --lib` -Expected: FAIL with missing type/module errors, not unrelated syntax errors. - -**Step 5: Commit** - -Do not commit yet; combine with Task 2 after the model exists and the tests are green. - -### Task 2: Implement the `CapacityAssignment` model and register it in the crate - -**Files:** -- Create: `src/models/misc/capacity_assignment.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Test: `src/unit_tests/models/misc/capacity_assignment.rs` - -**Step 1: Write the failing test** - -Use the red tests from Task 1 as the only driver for this task. - -**Step 2: Run test to verify it fails** - -Run: `cargo test capacity_assignment --lib` -Expected: FAIL for the missing model implementation. - -**Step 3: Write minimal implementation** - -Implement: -- `ProblemSchemaEntry` with constructor-facing fields: `capacities`, `cost`, `delay`, `cost_budget`, `delay_budget` -- `CapacityAssignment` struct with serde derives and constructor validation for: - - non-empty capacity list - - rectangular `cost` and `delay` matrices - - `cost.len() == delay.len()` and each row length matching `capacities.len()` - - monotonicity (`cost[i][j] <= cost[i][j+1]`, `delay[i][j] >= delay[i][j+1]`) -- getters `num_links()`, `num_capacities()`, `capacities()`, `cost()`, `delay()`, `cost_budget()`, `delay_budget()` -- `Problem` impl with `dims() == vec![num_capacities; num_links]` -- `evaluate()` that rejects bad config lengths / indices and checks both budgets -- `SatisfactionProblem` impl -- `declare_variants! { default sat CapacityAssignment => "num_capacities^num_links" }` -- `canonical_model_example_specs()` using the corrected 3-link YES instance from the issue -- module/export wiring in `src/models/misc/mod.rs`, `src/models/mod.rs`, and `src/lib.rs` - -**Step 4: Run test to verify it passes** - -Run: `cargo test capacity_assignment --lib` -Expected: PASS for the new model tests. - -**Step 5: Commit** - -```bash -git add src/models/misc/capacity_assignment.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/capacity_assignment.rs -git commit -m "feat: add CapacityAssignment model" -``` - -### Task 3: Add failing CLI tests for `pred create CapacityAssignment` - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Reference: `problemreductions-cli/src/cli.rs` - -**Step 1: Write the failing test** - -Add `create.rs` unit tests that exercise: -- successful creation with dedicated CapacityAssignment arguments -- informative failures for mismatched row widths / row counts -- rejection of non-monotone cost or delay rows - -Use a concrete CLI shape for this plan: -- `--capacities 1,2,3` -- `--cost-matrix "1,3,6;2,4,7;1,2,5"` -- `--delay-matrix "8,4,1;7,3,1;6,3,1"` -- `--cost-budget 10` -- `--delay-budget 12` - -**Step 2: Run test to verify it fails** - -Run: `cargo test -p problemreductions-cli capacity_assignment` -Expected: FAIL because the CLI flags / match arm do not exist yet. - -**Step 3: Write minimal implementation** - -Do not add production CLI code in this task. Leave the red test in place for Task 4. - -**Step 4: Run test to verify it still fails for the expected reason** - -Run: `cargo test -p problemreductions-cli capacity_assignment` -Expected: FAIL with missing flag / match-arm behavior, not unrelated parser errors. - -**Step 5: Commit** - -Do not commit yet; combine with Task 4 after the CLI path is green. - -### Task 4: Implement CLI creation support and help text - -**Files:** -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Test: `problemreductions-cli/src/commands/create.rs` - -**Step 1: Write the failing test** - -Use the red tests from Task 3 as the driver. - -**Step 2: Run test to verify it fails** - -Run: `cargo test -p problemreductions-cli capacity_assignment` -Expected: FAIL because `pred create CapacityAssignment` is still unsupported. - -**Step 3: Write minimal implementation** - -Implement: -- new `CreateArgs` fields for `--cost-matrix`, `--delay-matrix`, `--cost-budget`, `--delay-budget` -- `all_data_flags_empty()` updates for the new flags -- help-table entry and usage example in `problemreductions-cli/src/cli.rs` -- parsing helpers in `create.rs` for rectangular `u64` matrices using the existing semicolon/comma matrix style -- `create()` match arm constructing `CapacityAssignment::new(...)` -- CLI tests proving valid creation and the targeted validation failures above - -Do not add a short alias; canonical-name lookup is already case-insensitive through the registry. - -**Step 4: Run test to verify it passes** - -Run: `cargo test -p problemreductions-cli capacity_assignment` -Expected: PASS for the new CLI tests. - -**Step 5: Commit** - -```bash -git add problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs -git commit -m "feat: add CapacityAssignment CLI support" -``` - -## Batch 2: Paper Entry - -### Task 5: Add the paper definition and paper-aligned regression test - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `src/unit_tests/models/misc/capacity_assignment.rs` - -**Step 1: Write the failing test** - -Add a `test_capacity_assignment_paper_example` that: -- constructs the exact 3-link paper example -- checks the documented satisfying config -- uses `BruteForce::find_all_satisfying()` to confirm the documented count of 5 satisfying assignments - -**Step 2: Run test to verify it fails** - -Run: `cargo test capacity_assignment_paper_example --lib` -Expected: FAIL because the paper-aligned test is not written yet. - -**Step 3: Write minimal implementation** - -Update `docs/paper/reductions.typ` with: -- display-name entry for `CapacityAssignment` -- `problem-def("CapacityAssignment")` with the formal GJ definition -- brief background and the corrected Van Sickle/Chandy IFIP 1977 citation -- algorithm paragraph noting brute force and pseudo-polynomial DP with citations -- a worked 3-link example consistent with the canonical example data -- `pred-commands()` based on the canonical example export pattern - -Then finish the paper-aligned unit test. - -**Step 4: Run test to verify it passes** - -Run: -- `cargo test capacity_assignment_paper_example --lib` -- `make paper` - -Expected: PASS for the test and a clean Typst build. - -**Step 5: Commit** - -```bash -git add docs/paper/reductions.typ src/unit_tests/models/misc/capacity_assignment.rs -git commit -m "docs: add CapacityAssignment paper entry" -``` - -### Task 6: Final verification before push - -**Files:** -- Verify only; no new files unless generated tracked outputs change legitimately - -**Step 1: Write the failing test** - -No new tests. This task is verification-only. - -**Step 2: Run test to verify it fails** - -Skip. Use fresh full-project verification instead. - -**Step 3: Write minimal implementation** - -No implementation changes unless verification exposes a real failure. If it does, fix the smallest failing scope first and re-run the relevant command before returning to the full suite. - -**Step 4: Run test to verify it passes** - -Run: -- `make test` -- `make clippy` -- `make paper` - -Inspect: -- `git status --short` - -Expected: all commands exit 0; only intended tracked changes remain staged or ready to stage. - -**Step 5: Commit** - -If verification fixes were required: - -```bash -git add -A -git commit -m "chore: finish CapacityAssignment verification" -``` From 642cdd222e58ac5a03d10b98e058114909b3c200 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 22 Mar 2026 16:32:18 +0800 Subject: [PATCH 4/5] fix: rustfmt after merge with main --- problemreductions-cli/src/commands/create.rs | 5 ++--- src/models/mod.rs | 16 ++++++++-------- .../models/misc/capacity_assignment.rs | 5 +---- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index b10b69374..36ac7a2b8 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -25,9 +25,8 @@ use problemreductions::models::misc::{ QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, - ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, - TimetableDesign, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, TimetableDesign, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; diff --git a/src/models/mod.rs b/src/models/mod.rs index 7001cd04a..07d509252 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -36,14 +36,14 @@ pub use misc::PartiallyOrderedKnapsack; pub use misc::{ AdditionalKey, BinPacking, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, - Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, Partition, - PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, - SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, - SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling, - StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, TimetableDesign, + Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, + MultiprocessorScheduling, PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, + SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, + SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, + StackerCrane, StaffScheduling, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, + Term, TimetableDesign, }; pub use set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/unit_tests/models/misc/capacity_assignment.rs b/src/unit_tests/models/misc/capacity_assignment.rs index b7ebc5fc5..46fcf78bc 100644 --- a/src/unit_tests/models/misc/capacity_assignment.rs +++ b/src/unit_tests/models/misc/capacity_assignment.rs @@ -21,10 +21,7 @@ fn test_capacity_assignment_basic_properties() { assert_eq!(problem.cost_budget(), 10); assert_eq!(problem.delay_budget(), 12); assert_eq!(problem.dims(), vec![3, 3, 3]); - assert_eq!( - ::NAME, - "CapacityAssignment" - ); + assert_eq!(::NAME, "CapacityAssignment"); assert_eq!(::variant(), Vec::new()); } From 967cccff4b17dfa52bbec3136e35409a0e833f10 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 22 Mar 2026 17:46:54 +0800 Subject: [PATCH 5/5] fix: update --capacities help text to cover CapacityAssignment Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 6d901340c..7e71d6bde 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -365,7 +365,7 @@ pub struct CreateArgs { /// Edge lengths (e.g., 2,3,1) [default: all 1s] #[arg(long)] pub edge_lengths: Option, - /// Edge capacities for multicommodity flow problems (e.g., 1,1,2) + /// Capacities (edge capacities for flow problems, capacity levels for CapacityAssignment) #[arg(long)] pub capacities: Option, /// Bundle capacities for IntegralFlowBundles (e.g., 1,1,1)