From d07751a5fff52733e1ff590a4566a30cd5ce49a7 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 12:41:46 +0800 Subject: [PATCH 1/6] Add plan for #301: [Model] MinimumDummyActivitiesPert --- ...026-03-22-minimum-dummy-activities-pert.md | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 docs/plans/2026-03-22-minimum-dummy-activities-pert.md diff --git a/docs/plans/2026-03-22-minimum-dummy-activities-pert.md b/docs/plans/2026-03-22-minimum-dummy-activities-pert.md new file mode 100644 index 000000000..b288d75bc --- /dev/null +++ b/docs/plans/2026-03-22-minimum-dummy-activities-pert.md @@ -0,0 +1,265 @@ +# MinimumDummyActivitiesPert Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `MinimumDummyActivitiesPert` model from issue #301, including registry/CLI/example-db support and a paper entry that matches the canonical issue example. + +**Architecture:** Represent a candidate PERT encoding with one binary decision per precedence arc in the input DAG: either merge the predecessor's finish event with the successor's start event, or keep a dummy arc between them. `evaluate()` will quotient the selected endpoint merges, rebuild the induced event network, reject cyclic or reachability-changing encodings, and return the number of distinct dummy arcs that remain. This keeps `dims()` at `vec![2; num_arcs]`, matches the issue's worked example, and makes the 6-task witness brute-forceable in tests. + +**Tech Stack:** Rust workspace, `DirectedGraph`, `inventory` problem registry, `BruteForce`, `problemreductions-cli`, Typst paper. + +--- + +## Batch 1: Model, registrations, CLI, and tests + +### Task 1: Add failing model tests for the issue example and constructor guards + +**Files:** +- Create: `src/unit_tests/models/graph/minimum_dummy_activities_pert.rs` +- Modify: `src/models/graph/mod.rs` + +**Step 1: Write the failing tests** + +Add tests covering: +- constructor/accessor basics on a small DAG +- constructor rejection of cyclic input graphs +- the issue's 6-task example, with the merge-selection config for `A->C`, `B->E`, `C->F` evaluating to `SolutionSize::Valid(2)` +- an invalid config that merges both incoming arcs to `D`, causing spurious reachability and therefore `SolutionSize::Invalid` +- brute-force optimality on the 6-task example (`find_best()` returns value `2`) +- serde round-trip + +Use the precedence-arc order from `DirectedGraph::arcs()` and build the test helpers from the issue example directly. + +**Step 2: Run the targeted test to verify RED** + +Run: `cargo test minimum_dummy_activities_pert --lib` + +Expected: FAIL because the model module does not exist yet. + +**Step 3: Write minimal implementation** + +Create `src/models/graph/minimum_dummy_activities_pert.rs` with: +- `ProblemSchemaEntry` for `MinimumDummyActivitiesPert` +- struct field `graph: DirectedGraph` +- `try_new(graph) -> Result` enforcing `graph.is_dag()` +- `new(graph)` panicking on invalid input +- getters `graph()`, `num_vertices()`, `num_arcs()` +- helpers: + - ordered precedence arcs (`graph.arcs()`) + - a tiny union-find over the `2 * num_vertices` task endpoints + - event-network construction from a binary merge config + - transitive-reachability checks on both the input DAG and the derived event DAG +- `Problem` impl with `dims() = vec![2; self.graph.num_arcs()]` +- `OptimizationProblem` impl with `Direction::Minimize` +- `declare_variants! { default opt MinimumDummyActivitiesPert => "2^num_arcs" }` +- canonical example spec using the issue's 6-task instance and optimal merge config +- test link at file bottom + +Interpret config bit `1` as "merge this precedence arc" and bit `0` as "keep a dummy arc unless the quotient already identifies those endpoints". Count dummy arcs after quotienting by unique ordered event-class pairs. + +**Step 4: Run the targeted tests to verify GREEN** + +Run: `cargo test minimum_dummy_activities_pert --lib` + +Expected: PASS for the new model tests. + +**Step 5: Commit** + +```bash +git add src/models/graph/minimum_dummy_activities_pert.rs src/unit_tests/models/graph/minimum_dummy_activities_pert.rs src/models/graph/mod.rs +git commit -m "Add MinimumDummyActivitiesPert model" +``` + +### Task 2: Register the model throughout the library and example database + +**Files:** +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` or prelude export surface if the graph models are re-exported there +- Modify: `src/unit_tests/example_db.rs` + +**Step 1: Write the failing example-db / registry test** + +Add a test in `src/unit_tests/example_db.rs` asserting that `find_model_example()` resolves `MinimumDummyActivitiesPert` with an empty variant map and exposes a non-empty optimal config. + +**Step 2: Run the targeted test to verify RED** + +Run: `cargo test test_find_model_example_minimum_dummy_activities_pert --lib` + +Expected: FAIL because the model is not fully exported/registered yet. + +**Step 3: Register the model and canonical example** + +Update module exports so: +- `src/models/graph/mod.rs` declares and re-exports `minimum_dummy_activities_pert` +- `src/models/graph/mod.rs::canonical_model_example_specs()` extends with the new model's example specs +- `src/models/mod.rs` re-exports `MinimumDummyActivitiesPert` +- any public prelude/lib exports remain consistent with the other graph models + +**Step 4: Run the targeted test to verify GREEN** + +Run: `cargo test test_find_model_example_minimum_dummy_activities_pert --lib` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/example_db.rs +git commit -m "Register MinimumDummyActivitiesPert" +``` + +### Task 3: Add CLI create support and CLI-facing tests + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` + +**Step 1: Write the failing CLI tests** + +Add tests in `problemreductions-cli/src/commands/create.rs` covering: +- `all_data_flags_empty()` treats `--arcs` as input for this problem path +- `create()` serializes a `MinimumDummyActivitiesPert` JSON payload from `--arcs "0>2,0>3,1>3,1>4,2>5" --num-vertices 6` +- `create()` rejects cyclic input with the constructor error + +**Step 2: Run the targeted tests to verify RED** + +Run: `cargo test -p problemreductions-cli minimum_dummy_activities_pert` + +Expected: FAIL because the create arm/help text do not exist yet. + +**Step 3: Implement minimal CLI support** + +Add: +- a `create()` match arm using `parse_directed_graph(args.arcs, args.num_vertices)` and `MinimumDummyActivitiesPert::try_new` +- a problem-specific usage string, example usage snippet, and `after_help` entry in `problemreductions-cli/src/cli.rs` +- an example command in the schema-driven help list near the other directed-graph problems + +Use: +`pred create MinimumDummyActivitiesPert --arcs "0>2,0>3,1>3,1>4,2>5" --num-vertices 6` + +**Step 4: Run the targeted tests to verify GREEN** + +Run: `cargo test -p problemreductions-cli minimum_dummy_activities_pert` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs +git commit -m "Add CLI support for MinimumDummyActivitiesPert" +``` + +### Task 4: Add broad verification for Batch 1 + +**Files:** +- No new files + +**Step 1: Run focused library and CLI tests** + +Run: +- `cargo test minimum_dummy_activities_pert --lib` +- `cargo test -p problemreductions-cli minimum_dummy_activities_pert` + +Expected: both PASS. + +**Step 2: Run workspace checks likely to catch registration breakage** + +Run: +- `make test` +- `make clippy` + +Expected: PASS. + +**Step 3: Commit if any cleanup/refactor was needed** + +```bash +git add -A +git commit -m "Polish MinimumDummyActivitiesPert integration" +``` + +Only commit if Batch 1 verification required code changes. + +## Batch 2: Paper entry and paper-example consistency + +### Task 5: Add the paper entry and paper-example coverage + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `src/unit_tests/models/graph/minimum_dummy_activities_pert.rs` + +**Step 1: Write the failing paper-example test** + +Extend the model test file with `test_minimum_dummy_activities_pert_paper_example` that: +- builds the exact 6-task issue example +- evaluates the documented optimal merge config +- asserts `SolutionSize::Valid(2)` +- checks `BruteForce::find_best()` also returns value `2` + +If this is already covered by Task 1's issue-example test, keep the dedicated paper-example test as a named wrapper around the same witness. + +**Step 2: Run the targeted test to verify RED** + +Run: `cargo test test_minimum_dummy_activities_pert_paper_example --lib` + +Expected: FAIL until the named paper-example coverage exists. + +**Step 3: Write the paper entry** + +Update `docs/paper/reductions.typ`: +- add `"MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks],` to `display-name` +- add `#problem-def("MinimumDummyActivitiesPert")[ ... ][ ... ]` +- derive the example from `#let x = load-model-example("MinimumDummyActivitiesPert")` +- explain the 6-task precedence DAG, the three selected merges, and the two remaining dummy arcs +- add a `pred-commands()` block using `problem-spec(x)` +- cite the Garey-Johnson / Krishnamoorthy-Deo background and note the brute-force complexity with a footnote if no sharper exact algorithm is being claimed + +Keep the paper example aligned with the canonical example-db instance, using 1-indexed prose only where mathematically clearer. + +**Step 4: Run the targeted test and paper build to verify GREEN** + +Run: +- `cargo test test_minimum_dummy_activities_pert_paper_example --lib` +- `make paper` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add docs/paper/reductions.typ src/unit_tests/models/graph/minimum_dummy_activities_pert.rs +git commit -m "Document MinimumDummyActivitiesPert in paper" +``` + +### Task 6: Final verification and pipeline handoff + +**Files:** +- No new files + +**Step 1: Run final verification** + +Run: +- `make test` +- `make clippy` +- `make paper` +- `git status --short` + +Expected: +- all commands PASS +- only intended tracked files are modified +- generated `docs/paper/data/examples.json` and other ignored exports remain unstaged + +**Step 2: Prepare the issue-to-pr cleanup** + +After implementation succeeds: +- keep the branch clean +- delete this plan file before the final push +- summarize the implementation in the PR comment, including the merge-vs-dummy encoding choice and any deviations (ideally none) + +**Step 3: Final commit cleanup** + +```bash +git rm docs/plans/2026-03-22-minimum-dummy-activities-pert.md +git commit -m "chore: remove plan file after implementation" +``` From eb46baf3eeaff114d12c2b11a15d2f4dab952638 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 12:56:58 +0800 Subject: [PATCH 2/6] Implement #301: [Model] MinimumDummyActivitiesPert --- docs/paper/reductions.typ | 53 +++ problemreductions-cli/src/cli.rs | 2 + problemreductions-cli/src/commands/create.rs | 76 ++++- src/lib.rs | 3 +- .../graph/minimum_dummy_activities_pert.rs | 304 ++++++++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 3 +- src/unit_tests/example_db.rs | 18 ++ .../graph/minimum_dummy_activities_pert.rs | 88 +++++ 9 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 src/models/graph/minimum_dummy_activities_pert.rs create mode 100644 src/unit_tests/models/graph/minimum_dummy_activities_pert.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 9167a0c15..77589901f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -134,6 +134,7 @@ "MinMaxMulticenter": [Min-Max Multicenter], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], + "MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks], "MinimumSumMulticenter": [Minimum Sum Multicenter], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], "MultipleChoiceBranching": [Multiple Choice Branching], @@ -1845,6 +1846,58 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ] } +#{ + let x = load-model-example("MinimumDummyActivitiesPert") + let nv = x.instance.graph.num_vertices + let arcs = x.instance.graph.arcs + let ne = arcs.len() + let sol = (config: x.optimal_config, metric: x.optimal_value) + let merged = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc) + let dummy = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 0).map(((i, arc)) => arc) + let opt = sol.metric.Valid + let blue = graph-colors.at(0) + [ + #problem-def("MinimumDummyActivitiesPert")[ + Given a precedence DAG $G = (V, A)$, find an activity-on-arc PERT event network with one real activity arc for each task $v in V$, minimizing the number of dummy activity arcs, such that for every ordered pair of tasks $(u, v)$ there is a path from the finish event of $u$ to the start event of $v$ if and only if $v$ is reachable from $u$ in $G$. + ][ + The decision version of minimum dummy activities appears as ND44 in Garey and Johnson's compendium @garey1979. It arises when an activity-on-node precedence DAG must be converted into an activity-on-arc PERT chart: merging compatible finish/start events removes dummy activities, but an over-aggressive merge creates spurious precedence relations between unrelated tasks. The implementation here enumerates, for each direct precedence arc, whether it is realized as an event merge or left as a dummy activity, so brute-force over the $m = #ne$ direct precedences yields a worst-case bound of $O^*(2^m)$. #footnote[No exact algorithm improving on the direct-precedence merge encoding implemented in the codebase is claimed here.] + + *Example.* Consider the canonical precedence DAG on $n = #nv$ tasks with direct precedences #arcs.map(a => $(v_#(a.at(0)), v_#(a.at(1)))$).join(", "). The optimal encoding merges the predecessor-finish/successor-start pairs #merged.map(a => $(v_#(a.at(0)), v_#(a.at(1)))$).join(", "), so those handoffs need no dummy activity at all. The remaining direct precedences #dummy.map(a => $(v_#(a.at(0)), v_#(a.at(1)))$).join(" and ") still require dummy activities, so the optimum is $#opt$. Both unresolved precedences enter $v_3$, and merging either of them would identify unrelated task completions, creating spurious reachability between the two source tasks. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o minimum-dummy-activities-pert.json", + "pred solve minimum-dummy-activities-pert.json", + "pred evaluate minimum-dummy-activities-pert.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure({ + let positions = ((0, 1.0), (0, -0.3), (2.0, 1.3), (2.0, 0.35), (2.0, -0.95), (4.0, 1.3)) + canvas(length: 1cm, { + for (k, pos) in positions.enumerate() { + g-node(pos, name: "v" + str(k), + fill: white, + label: [$v_#k$]) + } + for arc in dummy { + let (u, v) = arc + draw.line("v" + str(u), "v" + str(v), + stroke: (paint: luma(140), thickness: 1pt, dash: "dashed"), + mark: (end: "straight", scale: 0.4)) + } + for arc in merged { + let (u, v) = arc + draw.line("v" + str(u), "v" + str(v), + stroke: 1.7pt + blue, + mark: (end: "straight", scale: 0.45)) + } + }) + }, + caption: [Canonical Minimum Dummy Activities in PERT Networks instance. Blue precedence arcs are encoded by merging the predecessor finish event with the successor start event; dashed gray arcs still require dummy activities. The optimal encoding leaves exactly #opt dummy activities.], + ) + ] + ] +} + #{ let x = load-model-example("MinimumFeedbackVertexSet") let nv = graph-num-vertices(x.instance) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 02df552d7..cee800ce0 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -295,6 +295,7 @@ Flags by problem type: SCS --strings, --bound [--alphabet-size] StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size] D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 + MinimumDummyActivitiesPert --arcs [--num-vertices] CBQ --domain-size, --relations, --conjuncts-spec ILP, CircuitSAT (via reduction only) @@ -322,6 +323,7 @@ Examples: pred create ConsistencyOfDatabaseFrequencyTables --num-objects 6 --attribute-domains \"2,3,2\" --frequency-tables \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\" --known-values \"0,0,0;3,0,1;1,2,1\" pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5 pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1 + pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6 pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\" pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3 diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index d03de0341..5557c16de 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -13,9 +13,9 @@ use problemreductions::models::algebraic::{ use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, - LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, MinimumMultiwayCut, - MixedChinesePostman, MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, + LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, + MinimumDummyActivitiesPert, MinimumMultiwayCut, MixedChinesePostman, + MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, @@ -585,6 +585,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1" } "MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"", + "MinimumDummyActivitiesPert" => "--arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6", "StrongConnectivityAugmentation" => { "--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1" } @@ -3229,6 +3230,25 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // MinimumDummyActivitiesPert + "MinimumDummyActivitiesPert" => { + let usage = "Usage: pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" [--num-vertices N]"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "MinimumDummyActivitiesPert requires --arcs\n\n\ + {usage}" + ) + })?; + let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; + ( + ser( + MinimumDummyActivitiesPert::try_new(graph) + .map_err(|e| anyhow::anyhow!(e))?, + )?, + resolved_variant.clone(), + ) + } + // MixedChinesePostman "MixedChinesePostman" => { let usage = "Usage: pred create MixedChinesePostman --graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-costs 2,3,1,4 --bound 24 [--num-vertices N]"; @@ -6214,6 +6234,56 @@ mod tests { assert!(err.contains("--num-vertices (5) is too small for the arcs")); } + #[test] + fn test_create_minimum_dummy_activities_pert_json() { + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::MinimumDummyActivitiesPert; + + let mut args = empty_args(); + args.problem = Some("MinimumDummyActivitiesPert".to_string()); + args.num_vertices = Some(6); + args.arcs = Some("0>2,0>3,1>3,1>4,2>5".to_string()); + + let output_path = temp_output_path("minimum_dummy_activities_pert"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "MinimumDummyActivitiesPert"); + assert!(created.variant.is_empty()); + + let problem: MinimumDummyActivitiesPert = serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_arcs(), 5); + + let _ = fs::remove_file(output_path); + } + + #[test] + fn test_create_minimum_dummy_activities_pert_rejects_cycles() { + let mut args = empty_args(); + args.problem = Some("MinimumDummyActivitiesPert".to_string()); + args.num_vertices = Some(3); + args.arcs = Some("0>1,1>2,2>0".to_string()); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("requires the input graph to be a DAG")); + } + #[test] fn test_create_balanced_complete_bipartite_subgraph() { use crate::dispatch::ProblemJsonOutput; diff --git a/src/lib.rs b/src/lib.rs index f9e84dca0..9d6c023e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,7 +57,8 @@ pub mod prelude { }; pub use crate::models::graph::{ KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, - MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, + MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, + MinimumDominatingSet, MinimumDummyActivitiesPert, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, diff --git a/src/models/graph/minimum_dummy_activities_pert.rs b/src/models/graph/minimum_dummy_activities_pert.rs new file mode 100644 index 000000000..c654b75e7 --- /dev/null +++ b/src/models/graph/minimum_dummy_activities_pert.rs @@ -0,0 +1,304 @@ +//! Minimum Dummy Activities in PERT Networks. +//! +//! Given a precedence DAG whose vertices are tasks, select which direct +//! precedence constraints can be represented by merging the predecessor's +//! finish event with the successor's start event. The remaining precedence +//! constraints require dummy activities. A configuration is valid when the +//! resulting event network is acyclic and preserves exactly the same +//! task-to-task reachability relation as the original DAG. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::DirectedGraph; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinimumDummyActivitiesPert", + display_name: "Minimum Dummy Activities in PERT Networks", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Find a PERT event network for a precedence DAG minimizing dummy activities", + fields: &[ + FieldInfo { + name: "graph", + type_name: "DirectedGraph", + description: "The precedence DAG G=(V,A) whose vertices are tasks and arcs encode direct precedence constraints", + }, + ], + } +} + +/// Minimum Dummy Activities in PERT Networks. +/// +/// For each precedence arc `u -> v`, the configuration chooses one of two +/// encodings: +/// - `1`: merge `u`'s finish event with `v`'s start event +/// - `0`: keep a dummy activity from `u`'s finish event to `v`'s start event +/// +/// A valid configuration must preserve exactly the same reachability relation +/// between task completions and task starts as the original precedence DAG. +#[derive(Debug, Clone, Serialize)] +pub struct MinimumDummyActivitiesPert { + graph: DirectedGraph, +} + +impl MinimumDummyActivitiesPert { + /// Fallible constructor used by CLI validation and deserialization. + pub fn try_new(graph: DirectedGraph) -> Result { + if !graph.is_dag() { + return Err("MinimumDummyActivitiesPert requires the input graph to be a DAG".into()); + } + Ok(Self { graph }) + } + + /// Create a new instance. + /// + /// # Panics + /// + /// Panics if the input graph is not a DAG. + pub fn new(graph: DirectedGraph) -> Self { + Self::try_new(graph).unwrap_or_else(|msg| panic!("{msg}")) + } + + /// Get the precedence DAG. + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + /// Get the number of tasks. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of direct precedence arcs. + pub fn num_arcs(&self) -> usize { + self.graph.num_arcs() + } + + /// Check whether the merge-selection config encodes a valid PERT network. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate(config).is_valid() + } + + fn precedence_arcs(&self) -> Vec<(usize, usize)> { + self.graph.arcs() + } + + fn build_candidate_network(&self, config: &[usize]) -> Option { + let num_tasks = self.num_vertices(); + let arcs = self.precedence_arcs(); + if config.len() != arcs.len() || config.iter().any(|&bit| bit > 1) { + return None; + } + + let mut uf = UnionFind::new(2 * num_tasks); + for ((u, v), &merge_bit) in arcs.iter().zip(config.iter()) { + if merge_bit == 1 { + uf.union(finish_endpoint(*u), start_endpoint(*v)); + } + } + + let roots: Vec = (0..2 * num_tasks).map(|endpoint| uf.find(endpoint)).collect(); + let mut root_to_dense = BTreeMap::new(); + for &root in &roots { + let next = root_to_dense.len(); + root_to_dense.entry(root).or_insert(next); + } + + let start_events: Vec = (0..num_tasks) + .map(|task| root_to_dense[&roots[start_endpoint(task)]]) + .collect(); + let finish_events: Vec = (0..num_tasks) + .map(|task| root_to_dense[&roots[finish_endpoint(task)]]) + .collect(); + + if start_events + .iter() + .zip(finish_events.iter()) + .any(|(start, finish)| start == finish) + { + return None; + } + + let task_arcs: Vec<(usize, usize)> = (0..num_tasks) + .map(|task| (start_events[task], finish_events[task])) + .collect(); + + let dummy_arcs: BTreeSet<(usize, usize)> = arcs + .iter() + .zip(config.iter()) + .filter_map(|((u, v), &merge_bit)| { + if merge_bit == 1 { + return None; + } + let source = finish_events[*u]; + let target = start_events[*v]; + (source != target).then_some((source, target)) + }) + .collect(); + + let mut event_arcs = task_arcs; + event_arcs.extend(dummy_arcs.iter().copied()); + let event_graph = DirectedGraph::new(root_to_dense.len(), event_arcs); + if !event_graph.is_dag() { + return None; + } + + Some(CandidatePertNetwork { + event_graph, + start_events, + finish_events, + num_dummy_arcs: dummy_arcs.len(), + }) + } +} + +impl Problem for MinimumDummyActivitiesPert { + const NAME: &'static str = "MinimumDummyActivitiesPert"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_arcs()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + let Some(candidate) = self.build_candidate_network(config) else { + return SolutionSize::Invalid; + }; + + let source_reachability = reachability_matrix(&self.graph); + let event_reachability = reachability_matrix(&candidate.event_graph); + + for source in 0..self.num_vertices() { + for target in 0..self.num_vertices() { + let pert_reachable = + candidate.finish_events[source] == candidate.start_events[target] + || event_reachability[candidate.finish_events[source]] + [candidate.start_events[target]]; + if source_reachability[source][target] != pert_reachable { + return SolutionSize::Invalid; + } + } + } + + SolutionSize::Valid( + i32::try_from(candidate.num_dummy_arcs) + .expect("dummy activity count must fit in i32"), + ) + } +} + +impl OptimizationProblem for MinimumDummyActivitiesPert { + type Value = i32; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +crate::declare_variants! { + default opt MinimumDummyActivitiesPert => "2^num_arcs", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "minimum_dummy_activities_pert", + instance: Box::new(MinimumDummyActivitiesPert::new(DirectedGraph::new( + 6, + vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)], + ))), + optimal_config: vec![1, 0, 0, 1, 1], + optimal_value: serde_json::json!({"Valid": 2}), + }] +} + +#[derive(Deserialize)] +struct MinimumDummyActivitiesPertData { + graph: DirectedGraph, +} + +impl<'de> Deserialize<'de> for MinimumDummyActivitiesPert { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let data = MinimumDummyActivitiesPertData::deserialize(deserializer)?; + Self::try_new(data.graph).map_err(serde::de::Error::custom) + } +} + +struct CandidatePertNetwork { + event_graph: DirectedGraph, + start_events: Vec, + finish_events: Vec, + num_dummy_arcs: usize, +} + +#[derive(Debug)] +struct UnionFind { + parent: Vec, +} + +impl UnionFind { + fn new(size: usize) -> Self { + Self { + parent: (0..size).collect(), + } + } + + fn find(&mut self, x: usize) -> usize { + if self.parent[x] != x { + let root = self.find(self.parent[x]); + self.parent[x] = root; + } + self.parent[x] + } + + fn union(&mut self, a: usize, b: usize) { + let root_a = self.find(a); + let root_b = self.find(b); + if root_a != root_b { + self.parent[root_b] = root_a; + } + } +} + +fn start_endpoint(task: usize) -> usize { + 2 * task +} + +fn finish_endpoint(task: usize) -> usize { + 2 * task + 1 +} + +fn reachability_matrix(graph: &DirectedGraph) -> Vec> { + let num_vertices = graph.num_vertices(); + let adjacency: Vec> = (0..num_vertices).map(|vertex| graph.successors(vertex)).collect(); + let mut reachable = vec![vec![false; num_vertices]; num_vertices]; + + for source in 0..num_vertices { + let mut stack = adjacency[source].clone(); + while let Some(vertex) = stack.pop() { + if reachable[source][vertex] { + continue; + } + reachable[source][vertex] = true; + stack.extend(adjacency[vertex].iter().copied()); + } + } + + reachable +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/minimum_dummy_activities_pert.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 06627fb70..20be25a68 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -11,6 +11,7 @@ //! - [`MaxCut`]: Maximum cut on weighted graphs //! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning) //! - [`MinimumCutIntoBoundedSets`]: Minimum cut into bounded sets (Garey & Johnson ND17) +//! - [`MinimumDummyActivitiesPert`]: Minimum dummy activities in activity-on-arc PERT networks //! - [`HamiltonianCircuit`]: Hamiltonian circuit (decision problem) //! - [`IsomorphicSpanningTree`]: Isomorphic spanning tree (satisfaction) //! - [`KClique`]: Clique decision problem with threshold k @@ -70,6 +71,7 @@ pub(crate) mod maximum_matching; pub(crate) mod min_max_multicenter; pub(crate) mod minimum_cut_into_bounded_sets; pub(crate) mod minimum_dominating_set; +pub(crate) mod minimum_dummy_activities_pert; pub(crate) mod minimum_feedback_arc_set; pub(crate) mod minimum_feedback_vertex_set; pub(crate) mod minimum_multiway_cut; @@ -116,6 +118,7 @@ pub use maximum_matching::MaximumMatching; pub use min_max_multicenter::MinMaxMulticenter; pub use minimum_cut_into_bounded_sets::MinimumCutIntoBoundedSets; pub use minimum_dominating_set::MinimumDominatingSet; +pub use minimum_dummy_activities_pert::MinimumDummyActivitiesPert; pub use minimum_feedback_arc_set::MinimumFeedbackArcSet; pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet; pub use minimum_multiway_cut::MinimumMultiwayCut; @@ -159,6 +162,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec DirectedGraph { + DirectedGraph::new(6, vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)]) +} + +fn issue_problem() -> MinimumDummyActivitiesPert { + MinimumDummyActivitiesPert::new(issue_graph()) +} + +fn config_for_merges( + problem: &MinimumDummyActivitiesPert, + merges: &[(usize, usize)], +) -> Vec { + let mut config = vec![0; problem.num_arcs()]; + let arcs = problem.graph().arcs(); + for &(u, v) in merges { + let index = arcs + .iter() + .position(|&(a, b)| a == u && b == v) + .expect("merge arc must exist in issue graph"); + config[index] = 1; + } + config +} + +#[test] +fn test_minimum_dummy_activities_pert_creation() { + let problem = issue_problem(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_arcs(), 5); + assert_eq!(problem.dims(), vec![2; 5]); +} + +#[test] +fn test_minimum_dummy_activities_pert_rejects_cyclic_input() { + let err = MinimumDummyActivitiesPert::try_new(DirectedGraph::new( + 3, + vec![(0, 1), (1, 2), (2, 0)], + )) + .unwrap_err(); + assert!(err.contains("DAG")); +} + +#[test] +fn test_minimum_dummy_activities_pert_issue_example() { + let problem = issue_problem(); + let config = config_for_merges(&problem, &[(0, 2), (1, 4), (2, 5)]); + assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.evaluate(&config), SolutionSize::Valid(2)); + assert!(problem.is_valid_solution(&config)); +} + +#[test] +fn test_minimum_dummy_activities_pert_rejects_spurious_reachability() { + let problem = issue_problem(); + let config = config_for_merges(&problem, &[(0, 3), (1, 3)]); + assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert!(!problem.is_valid_solution(&config)); +} + +#[test] +fn test_minimum_dummy_activities_pert_solver_finds_optimum_two() { + let problem = issue_problem(); + let solution = BruteForce::new().find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); +} + +#[test] +fn test_minimum_dummy_activities_pert_serialization_roundtrip() { + let problem = issue_problem(); + let json = serde_json::to_string(&problem).unwrap(); + let restored: MinimumDummyActivitiesPert = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.graph(), problem.graph()); +} + +#[test] +fn test_minimum_dummy_activities_pert_paper_example() { + let problem = issue_problem(); + let config = config_for_merges(&problem, &[(0, 2), (1, 4), (2, 5)]); + assert_eq!(problem.evaluate(&config), SolutionSize::Valid(2)); + let solution = BruteForce::new().find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); +} From a4d0bcab7629d9e511743d55443382d0b919898f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 12:57:24 +0800 Subject: [PATCH 3/6] chore: remove plan file after implementation --- ...026-03-22-minimum-dummy-activities-pert.md | 265 ------------------ 1 file changed, 265 deletions(-) delete mode 100644 docs/plans/2026-03-22-minimum-dummy-activities-pert.md diff --git a/docs/plans/2026-03-22-minimum-dummy-activities-pert.md b/docs/plans/2026-03-22-minimum-dummy-activities-pert.md deleted file mode 100644 index b288d75bc..000000000 --- a/docs/plans/2026-03-22-minimum-dummy-activities-pert.md +++ /dev/null @@ -1,265 +0,0 @@ -# MinimumDummyActivitiesPert Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `MinimumDummyActivitiesPert` model from issue #301, including registry/CLI/example-db support and a paper entry that matches the canonical issue example. - -**Architecture:** Represent a candidate PERT encoding with one binary decision per precedence arc in the input DAG: either merge the predecessor's finish event with the successor's start event, or keep a dummy arc between them. `evaluate()` will quotient the selected endpoint merges, rebuild the induced event network, reject cyclic or reachability-changing encodings, and return the number of distinct dummy arcs that remain. This keeps `dims()` at `vec![2; num_arcs]`, matches the issue's worked example, and makes the 6-task witness brute-forceable in tests. - -**Tech Stack:** Rust workspace, `DirectedGraph`, `inventory` problem registry, `BruteForce`, `problemreductions-cli`, Typst paper. - ---- - -## Batch 1: Model, registrations, CLI, and tests - -### Task 1: Add failing model tests for the issue example and constructor guards - -**Files:** -- Create: `src/unit_tests/models/graph/minimum_dummy_activities_pert.rs` -- Modify: `src/models/graph/mod.rs` - -**Step 1: Write the failing tests** - -Add tests covering: -- constructor/accessor basics on a small DAG -- constructor rejection of cyclic input graphs -- the issue's 6-task example, with the merge-selection config for `A->C`, `B->E`, `C->F` evaluating to `SolutionSize::Valid(2)` -- an invalid config that merges both incoming arcs to `D`, causing spurious reachability and therefore `SolutionSize::Invalid` -- brute-force optimality on the 6-task example (`find_best()` returns value `2`) -- serde round-trip - -Use the precedence-arc order from `DirectedGraph::arcs()` and build the test helpers from the issue example directly. - -**Step 2: Run the targeted test to verify RED** - -Run: `cargo test minimum_dummy_activities_pert --lib` - -Expected: FAIL because the model module does not exist yet. - -**Step 3: Write minimal implementation** - -Create `src/models/graph/minimum_dummy_activities_pert.rs` with: -- `ProblemSchemaEntry` for `MinimumDummyActivitiesPert` -- struct field `graph: DirectedGraph` -- `try_new(graph) -> Result` enforcing `graph.is_dag()` -- `new(graph)` panicking on invalid input -- getters `graph()`, `num_vertices()`, `num_arcs()` -- helpers: - - ordered precedence arcs (`graph.arcs()`) - - a tiny union-find over the `2 * num_vertices` task endpoints - - event-network construction from a binary merge config - - transitive-reachability checks on both the input DAG and the derived event DAG -- `Problem` impl with `dims() = vec![2; self.graph.num_arcs()]` -- `OptimizationProblem` impl with `Direction::Minimize` -- `declare_variants! { default opt MinimumDummyActivitiesPert => "2^num_arcs" }` -- canonical example spec using the issue's 6-task instance and optimal merge config -- test link at file bottom - -Interpret config bit `1` as "merge this precedence arc" and bit `0` as "keep a dummy arc unless the quotient already identifies those endpoints". Count dummy arcs after quotienting by unique ordered event-class pairs. - -**Step 4: Run the targeted tests to verify GREEN** - -Run: `cargo test minimum_dummy_activities_pert --lib` - -Expected: PASS for the new model tests. - -**Step 5: Commit** - -```bash -git add src/models/graph/minimum_dummy_activities_pert.rs src/unit_tests/models/graph/minimum_dummy_activities_pert.rs src/models/graph/mod.rs -git commit -m "Add MinimumDummyActivitiesPert model" -``` - -### Task 2: Register the model throughout the library and example database - -**Files:** -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` or prelude export surface if the graph models are re-exported there -- Modify: `src/unit_tests/example_db.rs` - -**Step 1: Write the failing example-db / registry test** - -Add a test in `src/unit_tests/example_db.rs` asserting that `find_model_example()` resolves `MinimumDummyActivitiesPert` with an empty variant map and exposes a non-empty optimal config. - -**Step 2: Run the targeted test to verify RED** - -Run: `cargo test test_find_model_example_minimum_dummy_activities_pert --lib` - -Expected: FAIL because the model is not fully exported/registered yet. - -**Step 3: Register the model and canonical example** - -Update module exports so: -- `src/models/graph/mod.rs` declares and re-exports `minimum_dummy_activities_pert` -- `src/models/graph/mod.rs::canonical_model_example_specs()` extends with the new model's example specs -- `src/models/mod.rs` re-exports `MinimumDummyActivitiesPert` -- any public prelude/lib exports remain consistent with the other graph models - -**Step 4: Run the targeted test to verify GREEN** - -Run: `cargo test test_find_model_example_minimum_dummy_activities_pert --lib` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/example_db.rs -git commit -m "Register MinimumDummyActivitiesPert" -``` - -### Task 3: Add CLI create support and CLI-facing tests - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` - -**Step 1: Write the failing CLI tests** - -Add tests in `problemreductions-cli/src/commands/create.rs` covering: -- `all_data_flags_empty()` treats `--arcs` as input for this problem path -- `create()` serializes a `MinimumDummyActivitiesPert` JSON payload from `--arcs "0>2,0>3,1>3,1>4,2>5" --num-vertices 6` -- `create()` rejects cyclic input with the constructor error - -**Step 2: Run the targeted tests to verify RED** - -Run: `cargo test -p problemreductions-cli minimum_dummy_activities_pert` - -Expected: FAIL because the create arm/help text do not exist yet. - -**Step 3: Implement minimal CLI support** - -Add: -- a `create()` match arm using `parse_directed_graph(args.arcs, args.num_vertices)` and `MinimumDummyActivitiesPert::try_new` -- a problem-specific usage string, example usage snippet, and `after_help` entry in `problemreductions-cli/src/cli.rs` -- an example command in the schema-driven help list near the other directed-graph problems - -Use: -`pred create MinimumDummyActivitiesPert --arcs "0>2,0>3,1>3,1>4,2>5" --num-vertices 6` - -**Step 4: Run the targeted tests to verify GREEN** - -Run: `cargo test -p problemreductions-cli minimum_dummy_activities_pert` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs -git commit -m "Add CLI support for MinimumDummyActivitiesPert" -``` - -### Task 4: Add broad verification for Batch 1 - -**Files:** -- No new files - -**Step 1: Run focused library and CLI tests** - -Run: -- `cargo test minimum_dummy_activities_pert --lib` -- `cargo test -p problemreductions-cli minimum_dummy_activities_pert` - -Expected: both PASS. - -**Step 2: Run workspace checks likely to catch registration breakage** - -Run: -- `make test` -- `make clippy` - -Expected: PASS. - -**Step 3: Commit if any cleanup/refactor was needed** - -```bash -git add -A -git commit -m "Polish MinimumDummyActivitiesPert integration" -``` - -Only commit if Batch 1 verification required code changes. - -## Batch 2: Paper entry and paper-example consistency - -### Task 5: Add the paper entry and paper-example coverage - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `src/unit_tests/models/graph/minimum_dummy_activities_pert.rs` - -**Step 1: Write the failing paper-example test** - -Extend the model test file with `test_minimum_dummy_activities_pert_paper_example` that: -- builds the exact 6-task issue example -- evaluates the documented optimal merge config -- asserts `SolutionSize::Valid(2)` -- checks `BruteForce::find_best()` also returns value `2` - -If this is already covered by Task 1's issue-example test, keep the dedicated paper-example test as a named wrapper around the same witness. - -**Step 2: Run the targeted test to verify RED** - -Run: `cargo test test_minimum_dummy_activities_pert_paper_example --lib` - -Expected: FAIL until the named paper-example coverage exists. - -**Step 3: Write the paper entry** - -Update `docs/paper/reductions.typ`: -- add `"MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks],` to `display-name` -- add `#problem-def("MinimumDummyActivitiesPert")[ ... ][ ... ]` -- derive the example from `#let x = load-model-example("MinimumDummyActivitiesPert")` -- explain the 6-task precedence DAG, the three selected merges, and the two remaining dummy arcs -- add a `pred-commands()` block using `problem-spec(x)` -- cite the Garey-Johnson / Krishnamoorthy-Deo background and note the brute-force complexity with a footnote if no sharper exact algorithm is being claimed - -Keep the paper example aligned with the canonical example-db instance, using 1-indexed prose only where mathematically clearer. - -**Step 4: Run the targeted test and paper build to verify GREEN** - -Run: -- `cargo test test_minimum_dummy_activities_pert_paper_example --lib` -- `make paper` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add docs/paper/reductions.typ src/unit_tests/models/graph/minimum_dummy_activities_pert.rs -git commit -m "Document MinimumDummyActivitiesPert in paper" -``` - -### Task 6: Final verification and pipeline handoff - -**Files:** -- No new files - -**Step 1: Run final verification** - -Run: -- `make test` -- `make clippy` -- `make paper` -- `git status --short` - -Expected: -- all commands PASS -- only intended tracked files are modified -- generated `docs/paper/data/examples.json` and other ignored exports remain unstaged - -**Step 2: Prepare the issue-to-pr cleanup** - -After implementation succeeds: -- keep the branch clean -- delete this plan file before the final push -- summarize the implementation in the PR comment, including the merge-vs-dummy encoding choice and any deviations (ideally none) - -**Step 3: Final commit cleanup** - -```bash -git rm docs/plans/2026-03-22-minimum-dummy-activities-pert.md -git commit -m "chore: remove plan file after implementation" -``` From e8a7389da3893fe360ee8160a1b7c310396485fe Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 16:21:23 +0800 Subject: [PATCH 4/6] Fix formatting after merge with main --- problemreductions-cli/src/commands/create.rs | 10 +++------- src/lib.rs | 15 +++++++-------- .../graph/minimum_dummy_activities_pert.rs | 19 +++++++++++-------- src/models/mod.rs | 5 ++--- .../graph/minimum_dummy_activities_pert.rs | 8 +++----- 5 files changed, 26 insertions(+), 31 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 593fe4c8c..8561cd380 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -14,9 +14,8 @@ use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets, - MinimumDummyActivitiesPert, MinimumMultiwayCut, MixedChinesePostman, - MultipleChoiceBranching, PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, + MinimumDummyActivitiesPert, MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, + PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, @@ -3603,10 +3602,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { })?; let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; ( - ser( - MinimumDummyActivitiesPert::try_new(graph) - .map_err(|e| anyhow::anyhow!(e))?, - )?, + ser(MinimumDummyActivitiesPert::try_new(graph).map_err(|e| anyhow::anyhow!(e))?)?, resolved_variant.clone(), ) } diff --git a/src/lib.rs b/src/lib.rs index 0076e60e5..2ca59022c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,14 +58,13 @@ pub mod prelude { }; pub use crate::models::graph::{ KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, - MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, - MinimumDominatingSet, MinimumDummyActivitiesPert, - MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, - MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation, - OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, - PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath, - SteinerTreeInGraphs, TravelingSalesman, UndirectedFlowLowerBounds, - UndirectedTwoCommodityIntegralFlow, + MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, + MinimumDummyActivitiesPert, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, + MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, + MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, + PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman, + ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, + UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, diff --git a/src/models/graph/minimum_dummy_activities_pert.rs b/src/models/graph/minimum_dummy_activities_pert.rs index c654b75e7..3685fd2f2 100644 --- a/src/models/graph/minimum_dummy_activities_pert.rs +++ b/src/models/graph/minimum_dummy_activities_pert.rs @@ -102,7 +102,9 @@ impl MinimumDummyActivitiesPert { } } - let roots: Vec = (0..2 * num_tasks).map(|endpoint| uf.find(endpoint)).collect(); + let roots: Vec = (0..2 * num_tasks) + .map(|endpoint| uf.find(endpoint)) + .collect(); let mut root_to_dense = BTreeMap::new(); for &root in &roots { let next = root_to_dense.len(); @@ -179,10 +181,10 @@ impl Problem for MinimumDummyActivitiesPert { for source in 0..self.num_vertices() { for target in 0..self.num_vertices() { - let pert_reachable = - candidate.finish_events[source] == candidate.start_events[target] - || event_reachability[candidate.finish_events[source]] - [candidate.start_events[target]]; + let pert_reachable = candidate.finish_events[source] + == candidate.start_events[target] + || event_reachability[candidate.finish_events[source]] + [candidate.start_events[target]]; if source_reachability[source][target] != pert_reachable { return SolutionSize::Invalid; } @@ -190,8 +192,7 @@ impl Problem for MinimumDummyActivitiesPert { } SolutionSize::Valid( - i32::try_from(candidate.num_dummy_arcs) - .expect("dummy activity count must fit in i32"), + i32::try_from(candidate.num_dummy_arcs).expect("dummy activity count must fit in i32"), ) } } @@ -282,7 +283,9 @@ fn finish_endpoint(task: usize) -> usize { fn reachability_matrix(graph: &DirectedGraph) -> Vec> { let num_vertices = graph.num_vertices(); - let adjacency: Vec> = (0..num_vertices).map(|vertex| graph.successors(vertex)).collect(); + let adjacency: Vec> = (0..num_vertices) + .map(|vertex| graph.successors(vertex)) + .collect(); let mut reachable = vec![vec![false; num_vertices]; num_vertices]; for source in 0..num_vertices { diff --git a/src/models/mod.rs b/src/models/mod.rs index bd651a401..e7c968d40 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -25,9 +25,8 @@ pub use graph::{ KColoring, KthBestSpanningTree, LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumDummyActivitiesPert, - MinimumFeedbackArcSet, - MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, - MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, + MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, + MinimumVertexCover, MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, SubgraphIsomorphism, diff --git a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs index ce7b3d3b8..760e24731 100644 --- a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs +++ b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs @@ -38,11 +38,9 @@ fn test_minimum_dummy_activities_pert_creation() { #[test] fn test_minimum_dummy_activities_pert_rejects_cyclic_input() { - let err = MinimumDummyActivitiesPert::try_new(DirectedGraph::new( - 3, - vec![(0, 1), (1, 2), (2, 0)], - )) - .unwrap_err(); + let err = + MinimumDummyActivitiesPert::try_new(DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])) + .unwrap_err(); assert!(err.contains("DAG")); } From b1ddcea52f6ca0b368bde625503a2a76126ad0a1 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 17:02:47 +0800 Subject: [PATCH 5/6] Fix dummy arc overcount on transitive arcs, add regression test, fix paper solve command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Subtract task-arc intersections from dummy count: when a non-merged arc's dummy coincides with a task arc, it doesn't add a new arc to the event network and shouldn't be counted. - Add regression test for DAG 0→1, 1→2, 0→2 (optimal = 0 dummies). - Fix paper's pred solve command to include --solver brute-force. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- src/models/graph/minimum_dummy_activities_pert.rs | 5 ++++- .../models/graph/minimum_dummy_activities_pert.rs | 11 +++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 6b42476b1..ecc0580a8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2086,7 +2086,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 #pred-commands( "pred create --example " + problem-spec(x) + " -o minimum-dummy-activities-pert.json", - "pred solve minimum-dummy-activities-pert.json", + "pred solve minimum-dummy-activities-pert.json --solver brute-force", "pred evaluate minimum-dummy-activities-pert.json --config " + x.optimal_config.map(str).join(","), ) diff --git a/src/models/graph/minimum_dummy_activities_pert.rs b/src/models/graph/minimum_dummy_activities_pert.rs index 3685fd2f2..1dff45eac 100644 --- a/src/models/graph/minimum_dummy_activities_pert.rs +++ b/src/models/graph/minimum_dummy_activities_pert.rs @@ -143,6 +143,9 @@ impl MinimumDummyActivitiesPert { }) .collect(); + let task_arc_set: BTreeSet<(usize, usize)> = task_arcs.iter().copied().collect(); + let num_dummy_arcs = dummy_arcs.difference(&task_arc_set).count(); + let mut event_arcs = task_arcs; event_arcs.extend(dummy_arcs.iter().copied()); let event_graph = DirectedGraph::new(root_to_dense.len(), event_arcs); @@ -154,7 +157,7 @@ impl MinimumDummyActivitiesPert { event_graph, start_events, finish_events, - num_dummy_arcs: dummy_arcs.len(), + num_dummy_arcs, }) } } diff --git a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs index 760e24731..1d9415c9b 100644 --- a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs +++ b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs @@ -76,6 +76,17 @@ fn test_minimum_dummy_activities_pert_serialization_roundtrip() { assert_eq!(restored.graph(), problem.graph()); } +#[test] +fn test_minimum_dummy_activities_pert_transitive_arc_zero_dummies() { + // DAG with transitive arc: 0→1, 1→2, 0→2. + // Merging 0+=1- and 1+=2- makes the 0→2 reachability transitively + // satisfied, so the optimal dummy count is 0. + let dag = DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let problem = MinimumDummyActivitiesPert::new(dag); + let solution = BruteForce::new().find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); +} + #[test] fn test_minimum_dummy_activities_pert_paper_example() { let problem = issue_problem(); From 69e123c1251725241d5680b23843672d5087973d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 17:47:22 +0800 Subject: [PATCH 6/6] Fix formatting after merge with main --- problemreductions-cli/src/commands/create.rs | 4 ++-- src/models/mod.rs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index b2e831bdc..b631b2a84 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -15,8 +15,8 @@ use problemreductions::models::graph::{ DisjointConnectingPaths, GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets, MinimumDummyActivitiesPert, MinimumMultiwayCut, MixedChinesePostman, - MultipleChoiceBranching, - PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, + MultipleChoiceBranching, PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, + StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, diff --git a/src/models/mod.rs b/src/models/mod.rs index 9cdcf57dd..7a71c3916 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -25,13 +25,13 @@ pub use graph::{ IsomorphicSpanningTree, KClique, KColoring, KthBestSpanningTree, LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet, - MinimumDummyActivitiesPert, - MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, - MinimumVertexCover, MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, - OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, - PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath, SpinGlass, - SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, SubgraphIsomorphism, - TravelingSalesman, UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, + MinimumDummyActivitiesPert, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, + MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MixedChinesePostman, + MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, + PartitionIntoPathsOfLength2, PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman, + ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, + StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{