From bf13790c3ae987aa1bc4f80ff64b6e62be8b7e20 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 18:21:30 +0800 Subject: [PATCH 1/3] Add plan for #513: [Model] ProductionPlanning --- .../2026-03-23-production-planning-model.md | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 docs/plans/2026-03-23-production-planning-model.md diff --git a/docs/plans/2026-03-23-production-planning-model.md b/docs/plans/2026-03-23-production-planning-model.md new file mode 100644 index 00000000..caa99ce2 --- /dev/null +++ b/docs/plans/2026-03-23-production-planning-model.md @@ -0,0 +1,374 @@ +# ProductionPlanning Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `ProductionPlanning` model from issue #513 as a `misc` satisfaction problem with brute-force support, CLI creation, canonical example data, unit tests, and a paper entry, without bundling any reduction-rule work into this PR. + +**Architecture:** Implement `ProductionPlanning` as a per-period bounded-integer decision problem: each configuration chooses production amounts `x_i in {0, ..., c_i}` and `evaluate()` checks capacity bounds, nonnegative prefix inventory, and the total production + holding + setup cost budget. Register it through `ProblemSchemaEntry` and `declare_variants!`, expose a canonical example via `canonical_model_example_specs()`, add `pred create ProductionPlanning` support with dedicated vector flags, and document the corrected SS21 / Florian-Lenstra-Rinnooy Kan (1980) references plus the cleaned example from the issue comments. + +**Tech Stack:** Rust workspace, serde/inventory registry, clap CLI parsing, Typst paper, GitHub issue context, Garey & Johnson SS21, Florian-Lenstra-Rinnooy Kan (1980). + +--- + +## Inputs And Constraints + +- Issue: `#513 [Model] ProductionPlanning` +- Associated rule already exists: `#488 [Rule] Partition to Production Planning`, so this model will not be orphaned +- Category: `src/models/misc/` +- Problem type: satisfaction (`Metric = bool`, `SatisfactionProblem`) +- Constructor shape: + - `num_periods: usize` + - `demands: Vec` + - `capacities: Vec` + - `setup_costs: Vec` + - `production_costs: Vec` + - `inventory_costs: Vec` + - `cost_bound: u64` +- Size getters required by the complexity expression: + - `num_periods() -> usize` + - `max_capacity() -> u64` +- Complexity string: `"(max_capacity + 1)^num_periods"` +- Source-of-truth example for tests and paper: + - demands `[5, 3, 7, 2, 8, 5]` + - capacities `[12, 12, 12, 12, 12, 12]` + - setup costs `[10, 10, 10, 10, 10, 10]` + - production costs `[1, 1, 1, 1, 1, 1]` + - inventory costs `[1, 1, 1, 1, 1, 1]` + - cost bound `80` + - satisfying plan `[8, 0, 10, 0, 12, 0]` +- Keep ILP discussion out of scope for this PR. This issue is a model-only pipeline item; reduction-rule work stays in separate rule issues / PRs. + +## Batch 1: Model, Registration, CLI, Tests + +### Task 1: Scaffold The Model And Core Registration + +**Files:** +- Create: `src/models/misc/production_planning.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Create: `src/unit_tests/models/misc/production_planning.rs` + +**Step 1: Write the failing tests** + +Add initial tests in `src/unit_tests/models/misc/production_planning.rs` for: +- constructor/getter round-trip +- `dims()` equals `capacities[i] + 1` per period +- `num_periods()` and `max_capacity()` getters +- constructor panics on mismatched vector lengths +- constructor panics when any capacity cannot fit into `usize` for `dims()` + +**Step 2: Run the targeted test to verify RED** + +Run: + +```bash +cargo test production_planning --lib +``` + +Expected: +- compile or test failure because the model/module does not exist yet + +**Step 3: Write the minimal implementation** + +Implement `src/models/misc/production_planning.rs` with: +- `inventory::submit!` `ProblemSchemaEntry` +- `ProductionPlanning` struct deriving `Debug, Clone, Serialize, Deserialize` +- `new(...)` constructor that validates: + - `num_periods > 0` + - every per-period vector length equals `num_periods` + - every capacity fits in `usize` and `capacity + 1` fits in `usize` for `dims()` +- accessors for all fields +- size getters `num_periods()` and `max_capacity()` +- `Problem` impl: + - `NAME = "ProductionPlanning"` + - `Metric = bool` + - `variant() = variant_params![]` + - `dims() = capacities.iter().map(|c| (c + 1) as usize).collect()` + - placeholder `evaluate()` logic sufficient for the creation tests to compile +- `SatisfactionProblem` impl +- `declare_variants! { default sat ProductionPlanning => "(max_capacity + 1)^num_periods", }` +- test link at file bottom + +Wire the new model through: +- `src/models/misc/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` prelude exports + +**Step 4: Run the targeted test to verify GREEN** + +Run: + +```bash +cargo test production_planning --lib +``` + +Expected: +- the creation/getter/module wiring tests pass +- semantic tests still fail or remain to be added later + +**Step 5: Commit** + +```bash +git add src/models/misc/production_planning.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/production_planning.rs +git commit -m "Add ProductionPlanning model scaffold" +``` + +### Task 2: Implement The Real Evaluation Semantics + +**Files:** +- Modify: `src/models/misc/production_planning.rs` +- Modify: `src/unit_tests/models/misc/production_planning.rs` + +**Step 1: Write the failing tests** + +Extend the test file with behavior-driven tests for: +- the issue example plan `[8, 0, 10, 0, 12, 0]` evaluates to `true` +- a plan that exceeds a period capacity evaluates to `false` +- a plan that creates negative prefix inventory evaluates to `false` +- a plan that exceeds the total cost bound evaluates to `false` +- wrong config length evaluates to `false` +- `BruteForce::find_satisfying()` returns `Some(_)` on the issue example instance +- serde round-trip preserves all vectors and `cost_bound` + +**Step 2: Run the targeted test to verify RED** + +Run: + +```bash +cargo test production_planning --lib +``` + +Expected: +- the new semantic tests fail because `evaluate()` is incomplete + +**Step 3: Write the minimal implementation** + +Finish `evaluate()` using issue #513 semantics: +- reject wrong config length +- reject any `x_i > capacities[i]` +- compute cumulative production and cumulative demand in `u128` +- reject any prefix where cumulative production `<` cumulative demand +- compute inventory `I_i` as the nonnegative prefix surplus +- compute total cost as: + - `sum_i production_costs[i] * x_i` + - `+ sum_i inventory_costs[i] * I_i` + - `+ sum_{x_i > 0} setup_costs[i]` +- compare against `cost_bound` using `u128` to avoid overflow during intermediate arithmetic +- return `true` iff all constraints hold and total cost is within budget + +Add small private helpers only if they remove duplication cleanly, for example: +- a prefix-balance helper +- a checked `u128` cost accumulator + +**Step 4: Run the targeted test to verify GREEN** + +Run: + +```bash +cargo test production_planning --lib +``` + +Expected: +- all `production_planning` unit tests pass + +**Step 5: Commit** + +```bash +git add src/models/misc/production_planning.rs src/unit_tests/models/misc/production_planning.rs +git commit -m "Implement ProductionPlanning evaluation" +``` + +### Task 3: Add Canonical Example Data And CLI Creation Support + +**Files:** +- Modify: `src/models/misc/production_planning.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` + +**Step 1: Write the failing tests** + +Add or extend CLI tests in `problemreductions-cli/src/commands/create.rs` for: +- `pred create ProductionPlanning --num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-budget 80` +- missing required vectors produce a clear usage error +- mismatched vector lengths produce a clear validation error +- `pred create --example ProductionPlanning` succeeds once the canonical example is registered + +**Step 2: Run the targeted test to verify RED** + +Run: + +```bash +cargo test create::tests::production_planning +``` + +Expected: +- failures because the CLI flags/match arm/example data do not exist yet + +**Step 3: Write the minimal implementation** + +In `src/models/misc/production_planning.rs`: +- add `canonical_model_example_specs()` using the cleaned issue example and satisfying config + +In `src/models/misc/mod.rs`: +- include `production_planning::canonical_model_example_specs()` in the misc example chain + +In `problemreductions-cli/src/cli.rs`: +- add new `CreateArgs` fields: + - `demands` + - `setup_costs` + - `production_costs` + - `inventory_costs` +- include them in `all_data_flags_empty()` +- add a `ProductionPlanning` row to the create help table +- add at least one concrete example command to the create help text + +In `problemreductions-cli/src/commands/create.rs`: +- import `ProductionPlanning` +- add a `ProductionPlanning` match arm +- require: + - `--num-periods` + - `--demands` + - `--capacities` + - `--setup-costs` + - `--production-costs` + - `--inventory-costs` + - `--cost-budget` +- parse the per-period vectors as `Vec` with one shared helper for consistent error messages +- validate that all vector lengths equal `num_periods` +- construct `ProductionPlanning::new(...)` + +Do **not** add a manual `problem_name.rs` alias unless testing proves it is needed. The registry already resolves canonical names case-insensitively. + +**Step 4: Run the targeted test to verify GREEN** + +Run: + +```bash +cargo test create::tests::production_planning +cargo test production_planning --lib +``` + +Expected: +- the new CLI tests pass +- canonical example registration compiles and example-backed creation works + +**Step 5: Commit** + +```bash +git add src/models/misc/production_planning.rs src/models/misc/mod.rs problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs +git commit -m "Add ProductionPlanning CLI support" +``` + +### Task 4: Batch-1 Verification + +**Files:** +- Modify: any files required by verification fixes + +**Step 1: Run the batch verification commands** + +Run: + +```bash +make test clippy +``` + +Expected: +- tests and clippy pass for the implemented model/CLI work + +**Step 2: Fix any failures** + +If verification fails: +- make the minimal correction +- rerun the exact failing command +- keep fixes in scope for `ProductionPlanning` + +**Step 3: Commit any verification-driven fixes** + +```bash +git add -A +git commit -m "Fix ProductionPlanning verification issues" +``` + +Only create this commit if verification required code changes. + +## Batch 2: Paper Entry And Paper-Example Alignment + +### Task 5: Add The Paper Entry And Lock The Paper Example + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `src/unit_tests/models/misc/production_planning.rs` + +**Step 1: Write the failing test** + +Add a `test_production_planning_paper_example` that: +- reconstructs the paper/example-db instance +- checks the documented plan `[8, 0, 10, 0, 12, 0]` evaluates to `true` +- confirms `BruteForce::find_satisfying()` returns at least one satisfying configuration for that instance + +**Step 2: Run the targeted test to verify RED** + +Run: + +```bash +cargo test production_planning_paper_example --lib +``` + +Expected: +- failure because the paper-aligned test or example details are not fully wired yet + +**Step 3: Write the minimal implementation** + +Update `docs/paper/reductions.typ`: +- add `"ProductionPlanning": [Production Planning]` to the display-name dictionary +- add `#problem-def("ProductionPlanning")[...]` in the scheduling/misc section +- use the corrected references and wording: + - Garey & Johnson `SS21` + - Florian, Lenstra, and Rinnooy Kan `(1980)` +- describe the model as a lot-sizing / production-planning feasibility problem with setup, production, and inventory costs +- present the cleaned six-period example from the fixed issue body +- include reproducibility commands using the canonical example helper pattern already used elsewhere in the paper + +Keep the paper content aligned with the implemented model: +- no rule theorem in this PR +- no invented smaller replacement example +- no reintroduction of the old trial-and-error example text + +**Step 4: Run the targeted paper verification** + +Run: + +```bash +cargo test production_planning_paper_example --lib +make paper +``` + +Expected: +- the paper-example test passes +- the Typst paper builds successfully + +**Step 5: Commit** + +```bash +git add docs/paper/reductions.typ src/unit_tests/models/misc/production_planning.rs +git commit -m "Document ProductionPlanning in paper" +``` + +## Final Verification Checklist + +Before handing back to `issue-to-pr` cleanup/push steps, rerun and confirm: + +```bash +make test clippy +make paper +git status --short +``` + +Success criteria: +- `ProductionPlanning` is registered and exported +- `pred create ProductionPlanning ...` works with the new flags +- canonical example data is available +- the issue example plan is encoded consistently across tests and paper +- no reduction rule was bundled into this PR From 9ad8615626be77d0855577ad383978396acb206f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 18:44:13 +0800 Subject: [PATCH 2/3] Implement #513: [Model] ProductionPlanning --- docs/paper/reductions.typ | 52 ++++ docs/paper/references.bib | 10 + problemreductions-cli/src/cli.rs | 14 + problemreductions-cli/src/commands/create.rs | 263 +++++++++++++++++- src/lib.rs | 3 +- src/models/misc/mod.rs | 4 + src/models/misc/production_planning.rs | 226 +++++++++++++++ src/models/mod.rs | 3 +- .../models/misc/production_planning.rs | 143 ++++++++++ 9 files changed, 715 insertions(+), 3 deletions(-) create mode 100644 src/models/misc/production_planning.rs create mode 100644 src/unit_tests/models/misc/production_planning.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 5b94df3f..958a63b6 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -154,6 +154,7 @@ "MultipleCopyFileAllocation": [Multiple Copy File Allocation], "ExpectedRetrievalCost": [Expected Retrieval Cost], "MultiprocessorScheduling": [Multiprocessor Scheduling], + "ProductionPlanning": [Production Planning], "PartitionIntoPathsOfLength2": [Partition into Paths of Length 2], "PartitionIntoTriangles": [Partition Into Triangles], "PrecedenceConstrainedScheduling": [Precedence Constrained Scheduling], @@ -5282,6 +5283,57 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("ProductionPlanning") + let n = x.instance.num_periods + let demands = x.instance.demands + let capacities = x.instance.capacities + let setup-costs = x.instance.setup_costs + let production-costs = x.instance.production_costs + let inventory-costs = x.instance.inventory_costs + let bound = x.instance.cost_bound + let plan = x.optimal_config + let prefix-production = range(n).map(i => plan.slice(0, i + 1).sum()) + let prefix-demand = range(n).map(i => demands.slice(0, i + 1).sum()) + let inventory = range(n).map(i => prefix-production.at(i) - prefix-demand.at(i)) + let production-total = range(n).map(i => production-costs.at(i) * plan.at(i)).sum() + let inventory-total = range(n).map(i => inventory-costs.at(i) * inventory.at(i)).sum() + let setup-total = range(n).filter(i => plan.at(i) > 0).map(i => setup-costs.at(i)).sum() + [ + #problem-def("ProductionPlanning")[ + Given a positive integer $n$, period demands $r_1, dots, r_n in ZZ_(>= 0)$, production capacities $c_1, dots, c_n in ZZ_(>= 0)$, setup costs $b_1, dots, b_n in ZZ_(>= 0)$, per-unit production costs $p_1, dots, p_n in ZZ_(>= 0)$, per-unit inventory costs $h_1, dots, h_n in ZZ_(>= 0)$, and a bound $B in ZZ_(>= 0)$, determine whether there exist production quantities $x_1, dots, x_n$ such that $0 <= x_i <= c_i$ for every period $i$, the inventory prefix $I_i = sum_(j=1)^i (x_j - r_j)$ satisfies $I_i >= 0$ for every $i$, and $sum_(i=1)^n (p_i x_i + h_i I_i) + sum_(i: x_i > 0) b_i <= B$. + ][ + Production Planning is the lot-sizing feasibility problem SS21 in Garey & Johnson @garey1979. Florian, Lenstra, and Rinnooy Kan show that the general problem is NP-complete even under strong restrictions, while also giving pseudo-polynomial dynamic-programming algorithms for capacitated variants @florianLenstraRinnooyKan1980. The implementation in this repository uses one bounded integer variable per period, so the registered exact baseline explores the direct witness space $product_i (c_i + 1)$; under the uniform-capacity bound $C = max_i c_i$, this becomes $O^*((C + 1)^n)$#footnote[This is the search bound induced by the configuration space exposed by the implementation, not a literature-best exact algorithm claim.]. + + *Example.* Consider the canonical instance with #n periods, demands $(#demands.map(str).join(", "))$, capacities $(#capacities.map(str).join(", "))$, setup costs $(#setup-costs.map(str).join(", "))$, production costs $(#production-costs.map(str).join(", "))$, inventory costs $(#inventory-costs.map(str).join(", "))$, and budget $B = #bound$. The satisfying production plan $x = (#plan.map(str).join(", "))$ yields prefix inventories $(#inventory.map(str).join(", "))$. The verifier therefore accepts, and its cost breakdown is $#production-total + #inventory-total + #setup-total = #(production-total + inventory-total + setup-total) <= #bound$. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o production-planning.json", + "pred solve production-planning.json --solver brute-force", + "pred evaluate production-planning.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure({ + table( + columns: n + 1, + align: center, + inset: 4pt, + table.header([*Period*], ..range(n).map(i => [#(i + 1)])), + [$r_i$], ..range(n).map(i => [#demands.at(i)]), + [$c_i$], ..range(n).map(i => [#capacities.at(i)]), + [$b_i$], ..range(n).map(i => [#setup-costs.at(i)]), + [$p_i$], ..range(n).map(i => [#production-costs.at(i)]), + [$h_i$], ..range(n).map(i => [#inventory-costs.at(i)]), + [$x_i$], ..range(n).map(i => [#plan.at(i)]), + [$I_i$], ..range(n).map(i => [#inventory.at(i)]), + ) + }, + caption: [Canonical Production Planning instance from the example DB. The documented plan meets every prefix-demand constraint and stays within the budget $B = #bound$.], + ) + ] + ] +} + #{ let x = load-model-example("CapacityAssignment") [ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 62192cb0..69a3e000 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -180,6 +180,16 @@ @book{garey1979 year = {1979} } +@article{florianLenstraRinnooyKan1980, + author = {M. Florian and J. K. Lenstra and A. H. G. Rinnooy Kan}, + title = {Deterministic Production Planning: Algorithms and Complexity}, + journal = {Management Science}, + volume = {26}, + number = {7}, + pages = {669--679}, + year = {1980} +} + @article{busingstiller2011, author = {Christina Büsing and Sebastian Stiller}, title = {Line planning, path constrained network flow and inapproximability}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 54dcf8ee..2ffbe015 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -247,6 +247,7 @@ Flags by problem type: Factoring --target, --m, --n BinPacking --sizes, --capacity CapacityAssignment --capacities, --cost-matrix, --delay-matrix, --cost-budget, --delay-budget + ProductionPlanning --num-periods, --demands, --capacities, --setup-costs, --production-costs, --inventory-costs, --cost-budget SubsetSum --sizes, --target SumOfSquaresPartition --sizes, --num-groups, --bound ExpectedRetrievalCost --probabilities, --num-sectors, --latency-bound @@ -329,6 +330,7 @@ Examples: pred create SAT --num-vars 3 --clauses \"1,2;-1,3\" pred create QUBO --matrix \"1,0.5;0.5,2\" pred create 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 ProductionPlanning --num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-budget 80 pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5 pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2 pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10 @@ -378,6 +380,18 @@ pub struct CreateArgs { /// Capacities (edge capacities for flow problems, capacity levels for CapacityAssignment) #[arg(long)] pub capacities: Option, + /// Demands for ProductionPlanning (comma-separated, e.g., "5,3,7,2,8,5") + #[arg(long)] + pub demands: Option, + /// Setup costs for ProductionPlanning (comma-separated, e.g., "10,10,10,10,10,10") + #[arg(long)] + pub setup_costs: Option, + /// Per-unit production costs for ProductionPlanning (comma-separated, e.g., "1,1,1,1,1,1") + #[arg(long)] + pub production_costs: Option, + /// Per-unit inventory costs for ProductionPlanning (comma-separated, e.g., "1,1,1,1,1,1") + #[arg(long)] + pub inventory_costs: Option, /// Bundle capacities for IntegralFlowBundles (e.g., 1,1,1) #[arg(long)] pub bundle_capacities: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 9c65f926..9b96a3bb 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -24,7 +24,7 @@ use problemreductions::models::misc::{ ConjunctiveBooleanQuery, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, GroupingBySwapping, KnownValue, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, - PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression, + PartiallyOrderedKnapsack, ProductionPlanning, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, @@ -57,6 +57,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.edge_weights.is_none() && args.edge_lengths.is_none() && args.capacities.is_none() + && args.demands.is_none() + && args.setup_costs.is_none() + && args.production_costs.is_none() + && args.inventory_costs.is_none() && args.bundle_capacities.is_none() && args.cost_matrix.is_none() && args.delay_matrix.is_none() @@ -619,6 +623,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "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" } + "ProductionPlanning" => { + "--num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-budget 80" + } "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", "ExpectedRetrievalCost" => EXPECTED_RETRIEVAL_COST_EXAMPLE_ARGS, @@ -3097,6 +3104,65 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + "ProductionPlanning" => { + let usage = "Usage: pred create ProductionPlanning --num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-budget 80"; + let num_periods = args.num_periods.ok_or_else(|| { + anyhow::anyhow!("ProductionPlanning requires --num-periods\n\n{usage}") + })?; + let demands = + parse_named_u64_list(args.demands.as_deref(), "ProductionPlanning", "--demands", usage)?; + let capacities = parse_named_u64_list( + args.capacities.as_deref(), + "ProductionPlanning", + "--capacities", + usage, + )?; + let setup_costs = parse_named_u64_list( + args.setup_costs.as_deref(), + "ProductionPlanning", + "--setup-costs", + usage, + )?; + let production_costs = parse_named_u64_list( + args.production_costs.as_deref(), + "ProductionPlanning", + "--production-costs", + usage, + )?; + let inventory_costs = parse_named_u64_list( + args.inventory_costs.as_deref(), + "ProductionPlanning", + "--inventory-costs", + usage, + )?; + let cost_bound = args.cost_budget.ok_or_else(|| { + anyhow::anyhow!("ProductionPlanning requires --cost-budget\n\n{usage}") + })?; + + for (flag, len) in [ + ("--demands", demands.len()), + ("--capacities", capacities.len()), + ("--setup-costs", setup_costs.len()), + ("--production-costs", production_costs.len()), + ("--inventory-costs", inventory_costs.len()), + ] { + ensure_named_len(len, num_periods, flag, usage)?; + } + + ( + ser(ProductionPlanning::new( + num_periods, + demands, + capacities, + setup_costs, + production_costs, + inventory_costs, + cost_bound, + ))?, + resolved_variant.clone(), + ) + } + "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(|| { @@ -5476,6 +5542,24 @@ fn parse_requirements(args: &CreateArgs, usage: &str) -> Result> { util::parse_comma_list(requirements_str) } +fn parse_named_u64_list( + raw: Option<&str>, + problem: &str, + flag: &str, + usage: &str, +) -> Result> { + let raw = raw.ok_or_else(|| anyhow::anyhow!("{problem} requires {flag}\n\n{usage}"))?; + util::parse_comma_list(raw).map_err(|err| anyhow::anyhow!("{err}\n\n{usage}")) +} + +fn ensure_named_len(len: usize, expected: usize, flag: &str, usage: &str) -> Result<()> { + anyhow::ensure!( + len == expected, + "{flag} must contain exactly {expected} entries\n\n{usage}" + ); + Ok(()) +} + fn validate_staff_scheduling_args( schedules: &[Vec], requirements: &[u64], @@ -6956,6 +7040,179 @@ mod tests { assert_eq!(json["data"]["delay_budget"], 12); } + #[test] + fn test_create_production_planning_serializes_problem_json() { + let output = temp_output_path("production_planning_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "ProductionPlanning", + "--num-periods", + "6", + "--demands", + "5,3,7,2,8,5", + "--capacities", + "12,12,12,12,12,12", + "--setup-costs", + "10,10,10,10,10,10", + "--production-costs", + "1,1,1,1,1,1", + "--inventory-costs", + "1,1,1,1,1,1", + "--cost-budget", + "80", + ]) + .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"], "ProductionPlanning"); + assert_eq!(json["data"]["num_periods"], 6); + assert_eq!(json["data"]["demands"], serde_json::json!([5, 3, 7, 2, 8, 5])); + assert_eq!( + json["data"]["capacities"], + serde_json::json!([12, 12, 12, 12, 12, 12]) + ); + assert_eq!( + json["data"]["setup_costs"], + serde_json::json!([10, 10, 10, 10, 10, 10]) + ); + assert_eq!( + json["data"]["production_costs"], + serde_json::json!([1, 1, 1, 1, 1, 1]) + ); + assert_eq!( + json["data"]["inventory_costs"], + serde_json::json!([1, 1, 1, 1, 1, 1]) + ); + assert_eq!(json["data"]["cost_bound"], 80); + } + + #[test] + fn test_create_production_planning_requires_all_period_vectors() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "ProductionPlanning", + "--num-periods", + "6", + "--demands", + "5,3,7,2,8,5", + "--capacities", + "12,12,12,12,12,12", + "--setup-costs", + "10,10,10,10,10,10", + "--inventory-costs", + "1,1,1,1,1,1", + "--cost-budget", + "80", + ]) + .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(); + assert!(err + .to_string() + .contains("ProductionPlanning requires --production-costs")); + } + + #[test] + fn test_create_production_planning_rejects_mismatched_period_lengths() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "ProductionPlanning", + "--num-periods", + "6", + "--demands", + "5,3,7,2,8", + "--capacities", + "12,12,12,12,12,12", + "--setup-costs", + "10,10,10,10,10,10", + "--production-costs", + "1,1,1,1,1,1", + "--inventory-costs", + "1,1,1,1,1,1", + "--cost-budget", + "80", + ]) + .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(); + assert!(err + .to_string() + .contains("--demands must contain exactly 6 entries")); + } + + #[test] + fn test_create_example_production_planning_uses_canonical_example() { + let output = temp_output_path("production_planning_example_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "--example", + "ProductionPlanning", + ]) + .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"], "ProductionPlanning"); + assert_eq!(json["data"]["num_periods"], 4); + assert_eq!(json["data"]["demands"], serde_json::json!([2, 1, 3, 2])); + assert_eq!(json["data"]["cost_bound"], 16); + } + #[test] fn test_create_longest_path_serializes_problem_json() { let output = temp_output_path("longest_path_create"); @@ -7231,6 +7488,10 @@ mod tests { edge_weights: None, edge_lengths: None, capacities: None, + demands: None, + setup_costs: None, + production_costs: None, + inventory_costs: None, bundle_capacities: None, cost_matrix: None, delay_matrix: None, diff --git a/src/lib.rs b/src/lib.rs index 56b6902e..59d8f240 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,7 +73,8 @@ pub mod prelude { ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, ExpectedRetrievalCost, Factoring, FlowShopScheduling, GroupingBySwapping, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, - MultiprocessorScheduling, PaintShop, Partition, QueryArg, RectilinearPictureCompression, + MultiprocessorScheduling, PaintShop, Partition, ProductionPlanning, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 2d07dc9b..6991734d 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -19,6 +19,7 @@ //! - [`Partition`]: Partition a multiset into two equal-sum subsets //! - [`PartiallyOrderedKnapsack`]: Knapsack with precedence constraints //! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline +//! - [`ProductionPlanning`]: Meet all period demands within capacity and total-cost bounds //! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles //! - [`ResourceConstrainedScheduling`]: Schedule unit-length tasks on processors with resource constraints //! - [`SchedulingWithIndividualDeadlines`]: Meet per-task deadlines on parallel processors @@ -54,6 +55,7 @@ pub(crate) mod paintshop; pub(crate) mod partially_ordered_knapsack; pub(crate) mod partition; mod precedence_constrained_scheduling; +mod production_planning; mod rectilinear_picture_compression; pub(crate) mod resource_constrained_scheduling; mod scheduling_with_individual_deadlines; @@ -92,6 +94,7 @@ pub use paintshop::PaintShop; pub use partially_ordered_knapsack::PartiallyOrderedKnapsack; pub use partition::Partition; pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling; +pub use production_planning::ProductionPlanning; pub use rectilinear_picture_compression::RectilinearPictureCompression; pub use resource_constrained_scheduling::ResourceConstrainedScheduling; pub use scheduling_with_individual_deadlines::SchedulingWithIndividualDeadlines; @@ -124,6 +127,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Demand r_i for each period" }, + FieldInfo { name: "capacities", type_name: "Vec", description: "Production capacity c_i for each period" }, + FieldInfo { name: "setup_costs", type_name: "Vec", description: "Setup cost b_i incurred when x_i > 0" }, + FieldInfo { name: "production_costs", type_name: "Vec", description: "Per-unit production cost coefficient p_i" }, + FieldInfo { name: "inventory_costs", type_name: "Vec", description: "Per-unit inventory cost coefficient h_i" }, + FieldInfo { name: "cost_bound", type_name: "u64", description: "Total cost bound B" }, + ], + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProductionPlanning { + #[serde(deserialize_with = "positive_usize::deserialize")] + num_periods: usize, + demands: Vec, + capacities: Vec, + setup_costs: Vec, + production_costs: Vec, + inventory_costs: Vec, + cost_bound: u64, +} + +impl ProductionPlanning { + pub fn new( + num_periods: usize, + demands: Vec, + capacities: Vec, + setup_costs: Vec, + production_costs: Vec, + inventory_costs: Vec, + cost_bound: u64, + ) -> Self { + assert!(num_periods > 0, "num_periods must be positive"); + for len in [ + demands.len(), + capacities.len(), + setup_costs.len(), + production_costs.len(), + inventory_costs.len(), + ] { + assert_eq!( + len, num_periods, + "all per-period vectors must have length num_periods" + ); + } + assert!( + capacities.iter().all(|&capacity| { + usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some() + }), + "capacities must fit in usize for dims()" + ); + + Self { + num_periods, + demands, + capacities, + setup_costs, + production_costs, + inventory_costs, + cost_bound, + } + } + + pub fn num_periods(&self) -> usize { + self.num_periods + } + + pub fn demands(&self) -> &[u64] { + &self.demands + } + + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + pub fn setup_costs(&self) -> &[u64] { + &self.setup_costs + } + + pub fn production_costs(&self) -> &[u64] { + &self.production_costs + } + + pub fn inventory_costs(&self) -> &[u64] { + &self.inventory_costs + } + + pub fn cost_bound(&self) -> u64 { + self.cost_bound + } + + pub fn max_capacity(&self) -> u64 { + self.capacities.iter().copied().max().unwrap_or(0) + } +} + +impl Problem for ProductionPlanning { + const NAME: &'static str = "ProductionPlanning"; + type Metric = bool; + + fn dims(&self) -> Vec { + self.capacities + .iter() + .map(|&capacity| { + usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .expect("capacities validated in constructor") + }) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_periods { + return false; + } + + let mut cumulative_production = 0u128; + let mut cumulative_demand = 0u128; + let mut total_cost = 0u128; + let cost_bound = self.cost_bound as u128; + + for (i, &production) in config.iter().enumerate() { + let capacity = match usize::try_from(self.capacities[i]) { + Ok(value) => value, + Err(_) => return false, + }; + if production > capacity { + return false; + } + + let production = production as u128; + cumulative_production += production; + cumulative_demand += self.demands[i] as u128; + + if cumulative_production < cumulative_demand { + return false; + } + + let inventory = cumulative_production - cumulative_demand; + total_cost += self.production_costs[i] as u128 * production; + total_cost += self.inventory_costs[i] as u128 * inventory; + if production > 0 { + total_cost += self.setup_costs[i] as u128; + } + + if total_cost > cost_bound { + return false; + } + } + + total_cost <= cost_bound + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for ProductionPlanning {} + +crate::declare_variants! { + default sat ProductionPlanning => "(max_capacity + 1)^num_periods", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "production_planning", + instance: Box::new(ProductionPlanning::new( + 4, + vec![2, 1, 3, 2], + vec![4, 4, 4, 4], + vec![2, 2, 2, 2], + vec![1, 1, 1, 1], + vec![1, 1, 1, 1], + 16, + )), + optimal_config: vec![3, 0, 4, 1], + optimal_value: serde_json::json!(true), + }] +} + +mod positive_usize { + use serde::de::Error; + use serde::{Deserialize, Deserializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = usize::deserialize(deserializer)?; + if value == 0 { + return Err(D::Error::custom("expected positive integer, got 0")); + } + Ok(value) + } +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/production_planning.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 290cb128..de73d235 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -40,7 +40,8 @@ pub use misc::{ ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, ExpectedRetrievalCost, Factoring, FlowShopScheduling, GroupingBySwapping, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, - Partition, PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, + Partition, PrecedenceConstrainedScheduling, ProductionPlanning, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, diff --git a/src/unit_tests/models/misc/production_planning.rs b/src/unit_tests/models/misc/production_planning.rs new file mode 100644 index 00000000..e2e9e531 --- /dev/null +++ b/src/unit_tests/models/misc/production_planning.rs @@ -0,0 +1,143 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +fn issue_example_problem() -> ProductionPlanning { + ProductionPlanning::new( + 6, + vec![5, 3, 7, 2, 8, 5], + vec![12, 12, 12, 12, 12, 12], + vec![10, 10, 10, 10, 10, 10], + vec![1, 1, 1, 1, 1, 1], + vec![1, 1, 1, 1, 1, 1], + 80, + ) +} + +fn tiny_solver_problem() -> ProductionPlanning { + ProductionPlanning::new( + 3, + vec![1, 1, 1], + vec![2, 1, 1], + vec![1, 1, 1], + vec![1, 1, 1], + vec![0, 0, 0], + 5, + ) +} + +#[test] +fn test_production_planning_creation() { + let problem = issue_example_problem(); + assert_eq!(problem.num_periods(), 6); + assert_eq!(problem.demands(), &[5, 3, 7, 2, 8, 5]); + assert_eq!(problem.capacities(), &[12, 12, 12, 12, 12, 12]); + assert_eq!(problem.setup_costs(), &[10, 10, 10, 10, 10, 10]); + assert_eq!(problem.production_costs(), &[1, 1, 1, 1, 1, 1]); + assert_eq!(problem.inventory_costs(), &[1, 1, 1, 1, 1, 1]); + assert_eq!(problem.cost_bound(), 80); + assert_eq!(problem.max_capacity(), 12); + assert_eq!(problem.dims(), vec![13; 6]); + assert_eq!(::NAME, "ProductionPlanning"); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_production_planning_evaluate_issue_example() { + let problem = issue_example_problem(); + assert!(problem.evaluate(&[8, 0, 10, 0, 12, 0])); +} + +#[test] +fn test_production_planning_rejects_capacity_overflow() { + let problem = issue_example_problem(); + assert!(!problem.evaluate(&[13, 0, 10, 0, 12, 0])); +} + +#[test] +fn test_production_planning_rejects_negative_inventory_prefix() { + let problem = issue_example_problem(); + assert!(!problem.evaluate(&[4, 4, 4, 4, 4, 4])); +} + +#[test] +fn test_production_planning_rejects_budget_overflow() { + let problem = issue_example_problem(); + assert!(!problem.evaluate(&[8, 0, 10, 0, 12, 1])); +} + +#[test] +fn test_production_planning_rejects_wrong_config_length() { + let problem = issue_example_problem(); + assert!(!problem.evaluate(&[8, 0, 10, 0, 12])); +} + +#[test] +fn test_production_planning_bruteforce_finds_satisfying_solution() { + let problem = tiny_solver_problem(); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +} + +#[test] +fn test_production_planning_paper_example() { + let problem = issue_example_problem(); + let plan = vec![8, 0, 10, 0, 12, 0]; + let solver = BruteForce::new(); + + assert!(problem.evaluate(&plan)); + + let satisfying = solver.find_satisfying(&problem); + assert!(satisfying.is_some()); + assert!(problem.evaluate(&satisfying.unwrap())); +} + +#[test] +fn test_production_planning_serialization() { + let problem = issue_example_problem(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: ProductionPlanning = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_periods(), problem.num_periods()); + assert_eq!(restored.demands(), problem.demands()); + assert_eq!(restored.capacities(), problem.capacities()); + assert_eq!(restored.setup_costs(), problem.setup_costs()); + assert_eq!(restored.production_costs(), problem.production_costs()); + assert_eq!(restored.inventory_costs(), problem.inventory_costs()); + assert_eq!(restored.cost_bound(), problem.cost_bound()); +} + +#[test] +#[should_panic(expected = "all per-period vectors must have length num_periods")] +fn test_production_planning_rejects_length_mismatch() { + ProductionPlanning::new( + 2, + vec![1], + vec![1, 1], + vec![1, 1], + vec![1, 1], + vec![1, 1], + 3, + ); +} + +#[test] +#[should_panic(expected = "capacities must fit in usize for dims()")] +fn test_production_planning_rejects_capacity_too_large_for_dims() { + ProductionPlanning::new( + 1, + vec![0], + vec![u64::MAX], + vec![0], + vec![0], + vec![0], + 0, + ); +} + +#[test] +#[should_panic(expected = "num_periods must be positive")] +fn test_production_planning_rejects_zero_periods() { + ProductionPlanning::new(0, vec![], vec![], vec![], vec![], vec![], 0); +} From cea94a7ea9df4f1106ac378720af40d40ab6b4a7 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 18:44:25 +0800 Subject: [PATCH 3/3] chore: remove plan file after implementation --- .../2026-03-23-production-planning-model.md | 374 ------------------ 1 file changed, 374 deletions(-) delete mode 100644 docs/plans/2026-03-23-production-planning-model.md diff --git a/docs/plans/2026-03-23-production-planning-model.md b/docs/plans/2026-03-23-production-planning-model.md deleted file mode 100644 index caa99ce2..00000000 --- a/docs/plans/2026-03-23-production-planning-model.md +++ /dev/null @@ -1,374 +0,0 @@ -# ProductionPlanning Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `ProductionPlanning` model from issue #513 as a `misc` satisfaction problem with brute-force support, CLI creation, canonical example data, unit tests, and a paper entry, without bundling any reduction-rule work into this PR. - -**Architecture:** Implement `ProductionPlanning` as a per-period bounded-integer decision problem: each configuration chooses production amounts `x_i in {0, ..., c_i}` and `evaluate()` checks capacity bounds, nonnegative prefix inventory, and the total production + holding + setup cost budget. Register it through `ProblemSchemaEntry` and `declare_variants!`, expose a canonical example via `canonical_model_example_specs()`, add `pred create ProductionPlanning` support with dedicated vector flags, and document the corrected SS21 / Florian-Lenstra-Rinnooy Kan (1980) references plus the cleaned example from the issue comments. - -**Tech Stack:** Rust workspace, serde/inventory registry, clap CLI parsing, Typst paper, GitHub issue context, Garey & Johnson SS21, Florian-Lenstra-Rinnooy Kan (1980). - ---- - -## Inputs And Constraints - -- Issue: `#513 [Model] ProductionPlanning` -- Associated rule already exists: `#488 [Rule] Partition to Production Planning`, so this model will not be orphaned -- Category: `src/models/misc/` -- Problem type: satisfaction (`Metric = bool`, `SatisfactionProblem`) -- Constructor shape: - - `num_periods: usize` - - `demands: Vec` - - `capacities: Vec` - - `setup_costs: Vec` - - `production_costs: Vec` - - `inventory_costs: Vec` - - `cost_bound: u64` -- Size getters required by the complexity expression: - - `num_periods() -> usize` - - `max_capacity() -> u64` -- Complexity string: `"(max_capacity + 1)^num_periods"` -- Source-of-truth example for tests and paper: - - demands `[5, 3, 7, 2, 8, 5]` - - capacities `[12, 12, 12, 12, 12, 12]` - - setup costs `[10, 10, 10, 10, 10, 10]` - - production costs `[1, 1, 1, 1, 1, 1]` - - inventory costs `[1, 1, 1, 1, 1, 1]` - - cost bound `80` - - satisfying plan `[8, 0, 10, 0, 12, 0]` -- Keep ILP discussion out of scope for this PR. This issue is a model-only pipeline item; reduction-rule work stays in separate rule issues / PRs. - -## Batch 1: Model, Registration, CLI, Tests - -### Task 1: Scaffold The Model And Core Registration - -**Files:** -- Create: `src/models/misc/production_planning.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Create: `src/unit_tests/models/misc/production_planning.rs` - -**Step 1: Write the failing tests** - -Add initial tests in `src/unit_tests/models/misc/production_planning.rs` for: -- constructor/getter round-trip -- `dims()` equals `capacities[i] + 1` per period -- `num_periods()` and `max_capacity()` getters -- constructor panics on mismatched vector lengths -- constructor panics when any capacity cannot fit into `usize` for `dims()` - -**Step 2: Run the targeted test to verify RED** - -Run: - -```bash -cargo test production_planning --lib -``` - -Expected: -- compile or test failure because the model/module does not exist yet - -**Step 3: Write the minimal implementation** - -Implement `src/models/misc/production_planning.rs` with: -- `inventory::submit!` `ProblemSchemaEntry` -- `ProductionPlanning` struct deriving `Debug, Clone, Serialize, Deserialize` -- `new(...)` constructor that validates: - - `num_periods > 0` - - every per-period vector length equals `num_periods` - - every capacity fits in `usize` and `capacity + 1` fits in `usize` for `dims()` -- accessors for all fields -- size getters `num_periods()` and `max_capacity()` -- `Problem` impl: - - `NAME = "ProductionPlanning"` - - `Metric = bool` - - `variant() = variant_params![]` - - `dims() = capacities.iter().map(|c| (c + 1) as usize).collect()` - - placeholder `evaluate()` logic sufficient for the creation tests to compile -- `SatisfactionProblem` impl -- `declare_variants! { default sat ProductionPlanning => "(max_capacity + 1)^num_periods", }` -- test link at file bottom - -Wire the new model through: -- `src/models/misc/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` prelude exports - -**Step 4: Run the targeted test to verify GREEN** - -Run: - -```bash -cargo test production_planning --lib -``` - -Expected: -- the creation/getter/module wiring tests pass -- semantic tests still fail or remain to be added later - -**Step 5: Commit** - -```bash -git add src/models/misc/production_planning.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/production_planning.rs -git commit -m "Add ProductionPlanning model scaffold" -``` - -### Task 2: Implement The Real Evaluation Semantics - -**Files:** -- Modify: `src/models/misc/production_planning.rs` -- Modify: `src/unit_tests/models/misc/production_planning.rs` - -**Step 1: Write the failing tests** - -Extend the test file with behavior-driven tests for: -- the issue example plan `[8, 0, 10, 0, 12, 0]` evaluates to `true` -- a plan that exceeds a period capacity evaluates to `false` -- a plan that creates negative prefix inventory evaluates to `false` -- a plan that exceeds the total cost bound evaluates to `false` -- wrong config length evaluates to `false` -- `BruteForce::find_satisfying()` returns `Some(_)` on the issue example instance -- serde round-trip preserves all vectors and `cost_bound` - -**Step 2: Run the targeted test to verify RED** - -Run: - -```bash -cargo test production_planning --lib -``` - -Expected: -- the new semantic tests fail because `evaluate()` is incomplete - -**Step 3: Write the minimal implementation** - -Finish `evaluate()` using issue #513 semantics: -- reject wrong config length -- reject any `x_i > capacities[i]` -- compute cumulative production and cumulative demand in `u128` -- reject any prefix where cumulative production `<` cumulative demand -- compute inventory `I_i` as the nonnegative prefix surplus -- compute total cost as: - - `sum_i production_costs[i] * x_i` - - `+ sum_i inventory_costs[i] * I_i` - - `+ sum_{x_i > 0} setup_costs[i]` -- compare against `cost_bound` using `u128` to avoid overflow during intermediate arithmetic -- return `true` iff all constraints hold and total cost is within budget - -Add small private helpers only if they remove duplication cleanly, for example: -- a prefix-balance helper -- a checked `u128` cost accumulator - -**Step 4: Run the targeted test to verify GREEN** - -Run: - -```bash -cargo test production_planning --lib -``` - -Expected: -- all `production_planning` unit tests pass - -**Step 5: Commit** - -```bash -git add src/models/misc/production_planning.rs src/unit_tests/models/misc/production_planning.rs -git commit -m "Implement ProductionPlanning evaluation" -``` - -### Task 3: Add Canonical Example Data And CLI Creation Support - -**Files:** -- Modify: `src/models/misc/production_planning.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` - -**Step 1: Write the failing tests** - -Add or extend CLI tests in `problemreductions-cli/src/commands/create.rs` for: -- `pred create ProductionPlanning --num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-budget 80` -- missing required vectors produce a clear usage error -- mismatched vector lengths produce a clear validation error -- `pred create --example ProductionPlanning` succeeds once the canonical example is registered - -**Step 2: Run the targeted test to verify RED** - -Run: - -```bash -cargo test create::tests::production_planning -``` - -Expected: -- failures because the CLI flags/match arm/example data do not exist yet - -**Step 3: Write the minimal implementation** - -In `src/models/misc/production_planning.rs`: -- add `canonical_model_example_specs()` using the cleaned issue example and satisfying config - -In `src/models/misc/mod.rs`: -- include `production_planning::canonical_model_example_specs()` in the misc example chain - -In `problemreductions-cli/src/cli.rs`: -- add new `CreateArgs` fields: - - `demands` - - `setup_costs` - - `production_costs` - - `inventory_costs` -- include them in `all_data_flags_empty()` -- add a `ProductionPlanning` row to the create help table -- add at least one concrete example command to the create help text - -In `problemreductions-cli/src/commands/create.rs`: -- import `ProductionPlanning` -- add a `ProductionPlanning` match arm -- require: - - `--num-periods` - - `--demands` - - `--capacities` - - `--setup-costs` - - `--production-costs` - - `--inventory-costs` - - `--cost-budget` -- parse the per-period vectors as `Vec` with one shared helper for consistent error messages -- validate that all vector lengths equal `num_periods` -- construct `ProductionPlanning::new(...)` - -Do **not** add a manual `problem_name.rs` alias unless testing proves it is needed. The registry already resolves canonical names case-insensitively. - -**Step 4: Run the targeted test to verify GREEN** - -Run: - -```bash -cargo test create::tests::production_planning -cargo test production_planning --lib -``` - -Expected: -- the new CLI tests pass -- canonical example registration compiles and example-backed creation works - -**Step 5: Commit** - -```bash -git add src/models/misc/production_planning.rs src/models/misc/mod.rs problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs -git commit -m "Add ProductionPlanning CLI support" -``` - -### Task 4: Batch-1 Verification - -**Files:** -- Modify: any files required by verification fixes - -**Step 1: Run the batch verification commands** - -Run: - -```bash -make test clippy -``` - -Expected: -- tests and clippy pass for the implemented model/CLI work - -**Step 2: Fix any failures** - -If verification fails: -- make the minimal correction -- rerun the exact failing command -- keep fixes in scope for `ProductionPlanning` - -**Step 3: Commit any verification-driven fixes** - -```bash -git add -A -git commit -m "Fix ProductionPlanning verification issues" -``` - -Only create this commit if verification required code changes. - -## Batch 2: Paper Entry And Paper-Example Alignment - -### Task 5: Add The Paper Entry And Lock The Paper Example - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `src/unit_tests/models/misc/production_planning.rs` - -**Step 1: Write the failing test** - -Add a `test_production_planning_paper_example` that: -- reconstructs the paper/example-db instance -- checks the documented plan `[8, 0, 10, 0, 12, 0]` evaluates to `true` -- confirms `BruteForce::find_satisfying()` returns at least one satisfying configuration for that instance - -**Step 2: Run the targeted test to verify RED** - -Run: - -```bash -cargo test production_planning_paper_example --lib -``` - -Expected: -- failure because the paper-aligned test or example details are not fully wired yet - -**Step 3: Write the minimal implementation** - -Update `docs/paper/reductions.typ`: -- add `"ProductionPlanning": [Production Planning]` to the display-name dictionary -- add `#problem-def("ProductionPlanning")[...]` in the scheduling/misc section -- use the corrected references and wording: - - Garey & Johnson `SS21` - - Florian, Lenstra, and Rinnooy Kan `(1980)` -- describe the model as a lot-sizing / production-planning feasibility problem with setup, production, and inventory costs -- present the cleaned six-period example from the fixed issue body -- include reproducibility commands using the canonical example helper pattern already used elsewhere in the paper - -Keep the paper content aligned with the implemented model: -- no rule theorem in this PR -- no invented smaller replacement example -- no reintroduction of the old trial-and-error example text - -**Step 4: Run the targeted paper verification** - -Run: - -```bash -cargo test production_planning_paper_example --lib -make paper -``` - -Expected: -- the paper-example test passes -- the Typst paper builds successfully - -**Step 5: Commit** - -```bash -git add docs/paper/reductions.typ src/unit_tests/models/misc/production_planning.rs -git commit -m "Document ProductionPlanning in paper" -``` - -## Final Verification Checklist - -Before handing back to `issue-to-pr` cleanup/push steps, rerun and confirm: - -```bash -make test clippy -make paper -git status --short -``` - -Success criteria: -- `ProductionPlanning` is registered and exported -- `pred create ProductionPlanning ...` works with the new flags -- canonical example data is available -- the issue example plan is encoded consistently across tests and paper -- no reduction rule was bundled into this PR