From 7012937cb159b4f683fc7a3af5d5dda6c27bf895 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:51:34 +0800 Subject: [PATCH 1/6] Add plan for #444: MinimumCardinalityKey model --- .../2026-03-17-minimum-cardinality-key.md | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 docs/plans/2026-03-17-minimum-cardinality-key.md diff --git a/docs/plans/2026-03-17-minimum-cardinality-key.md b/docs/plans/2026-03-17-minimum-cardinality-key.md new file mode 100644 index 000000000..1fa573e3e --- /dev/null +++ b/docs/plans/2026-03-17-minimum-cardinality-key.md @@ -0,0 +1,103 @@ +# Plan: Add MinimumCardinalityKey Model (Issue #444) + +## Information Checklist + +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `MinimumCardinalityKey` | +| 2 | Mathematical definition | Given attribute set A, functional dependencies F, and bound M, is there a candidate key K ⊆ A with \|K\| ≤ M? A key K must: (1) determine all of A under closure F*, and (2) be minimal (no proper subset also determines A). | +| 3 | Problem type | Satisfaction (`Metric = bool`) | +| 4 | Type parameters | None | +| 5 | Struct fields | `num_attributes: usize`, `dependencies: Vec<(Vec, Vec)>`, `bound_k: usize` | +| 6 | Configuration space | `vec![2; num_attributes]` — binary per attribute (1 = included in K) | +| 7 | Feasibility check | K = selected attributes; compute closure(K) under F; check closure = A, \|K\| ≤ bound_k, and K is minimal | +| 8 | Objective function | N/A (satisfaction) — returns `true` iff valid candidate key of bounded size | +| 9 | Best known exact algorithm | O(2^num_attributes) brute-force enumeration (Lucchesi & Osborn 1978) | +| 10 | Solving strategy | BruteForce (enumerate subsets, check key property) | +| 11 | Category | `set/` (attribute sets + functional dependencies) | +| 12 | Expected outcome | Instance 1 (YES): A={0..5}, F={({0,1}→{2}), ({0,2}→{3}), ({1,3}→{4}), ({2,4}→{5})}, M=2. Key {0,1} determines all of A. Instance 2 (NO): same A, F={({0,1,2}→{3}), ({3,4}→{5})}, M=2. No 2-element key exists. | + +## Associated Rules + +- R120: Vertex Cover → MinimumCardinalityKey (this model is target) +- R122: MinimumCardinalityKey → PrimeAttributeName (this model is source) + +## Batch 1: Implementation (Steps 1–5.5) + +All tasks in this batch are independent and can run in parallel. + +### Task 1: Implement model + register + declare variants + +**Files:** `src/models/set/minimum_cardinality_key.rs`, `src/models/set/mod.rs`, `src/models/mod.rs` + +Follow `SetBasis` as the template (satisfaction problem, no type parameters, set/ category). + +1. Create `src/models/set/minimum_cardinality_key.rs`: + - `inventory::submit!` with `ProblemSchemaEntry` (name: "MinimumCardinalityKey", display_name: "Minimum Cardinality Key", aliases: &[], fields: num_attributes/usize, dependencies/Vec<(Vec, Vec)>, bound_k/usize) + - Struct `MinimumCardinalityKey` with `#[derive(Debug, Clone, Serialize, Deserialize)]` + - Fields: `num_attributes: usize`, `dependencies: Vec<(Vec, Vec)>`, `bound_k: usize` + - Constructor `new(num_attributes, dependencies, bound_k)` — validate all attribute indices in dependencies are < num_attributes + - Getter methods: `num_attributes()`, `num_dependencies()`, `bound_k()`, `dependencies()` + - Helper: `compute_closure(attrs: &[bool]) -> Vec` — iteratively apply FDs until fixpoint + - Helper: `is_minimal_key(config: &[usize]) -> bool` — for each selected attribute, check removing it breaks the key property + - `Problem` impl: NAME = "MinimumCardinalityKey", Metric = bool, dims = vec![2; num_attributes], variant = variant_params![] + - `evaluate()`: (a) K = selected attrs, (b) |K| ≤ bound_k, (c) closure(K) = A, (d) K is minimal + - `SatisfactionProblem` impl (marker) + - `declare_variants! { default sat MinimumCardinalityKey => "2^num_attributes" }` + - `#[cfg(test)] #[path]` link to unit tests + +2. Update `src/models/set/mod.rs`: + - Add `pub(crate) mod minimum_cardinality_key;` + - Add `pub use minimum_cardinality_key::MinimumCardinalityKey;` + - Add `specs.extend(minimum_cardinality_key::canonical_model_example_specs());` + +3. Update `src/models/mod.rs`: + - Add `MinimumCardinalityKey` to the set re-export line + +### Task 2: CLI registration + +**Files:** `problemreductions-cli/src/problem_name.rs`, `problemreductions-cli/src/commands/create.rs`, `problemreductions-cli/src/cli.rs` + +1. `problem_name.rs`: Add `"minimumcardinalitykey" => "MinimumCardinalityKey"` to `resolve_alias()` +2. `commands/create.rs`: Add match arm for "MinimumCardinalityKey" that parses `--num-attributes`, `--dependencies` (JSON array of [lhs, rhs] pairs), `--bound-k` +3. `cli.rs`: Add new CLI flags if needed (`--dependencies`, `--bound-k`) and update `CreateArgs` help table + +### Task 3: Canonical example in example_db + +**File:** `src/example_db/model_builders.rs` (aggregator) + `src/models/set/minimum_cardinality_key.rs` (specs function) + +Add `canonical_model_example_specs()` function in the model file returning a `ModelExampleSpec` with id "minimum_cardinality_key". Use Instance 1 from the issue (num_attributes=6, the 4 FDs, bound_k=2) and a known satisfying solution [1,1,0,0,0,0] (K={0,1}). + +### Task 4: Unit tests + +**File:** `src/unit_tests/models/set/minimum_cardinality_key.rs` + +Tests (≥3 required): +1. `test_minimum_cardinality_key_creation` — constructor, getters, dims +2. `test_minimum_cardinality_key_evaluation_yes` — Instance 1: config [1,1,0,0,0,0] evaluates to true +3. `test_minimum_cardinality_key_evaluation_no` — Instance 2: no config of size ≤2 is a key +4. `test_minimum_cardinality_key_non_minimal_rejected` — a superset of a key (e.g., {0,1,2} when {0,1} suffices) returns false +5. `test_minimum_cardinality_key_solver` — BruteForce finds all satisfying configs for Instance 1 +6. `test_minimum_cardinality_key_serialization` — serde roundtrip +7. `test_minimum_cardinality_key_closure_computation` — verify closure works correctly +8. `test_minimum_cardinality_key_invalid_config` — wrong length, out-of-range values +9. `test_minimum_cardinality_key_empty_dependencies` — edge case: no FDs, only trivial keys +10. `test_minimum_cardinality_key_paper_example` — (placeholder, finalized after Step 6) + +Also update `src/unit_tests/models/set/mod.rs` to include the new test module. + +### Task 5: Build verification + +Run `make test clippy` to verify everything compiles and passes. + +## Batch 2: Paper Entry (Step 6) + +Depends on Batch 1 completion (needs working model for exports). + +### Task 6: Write paper entry in docs/paper/reductions.typ + +1. Add display name: `"MinimumCardinalityKey": [Minimum Cardinality Key],` +2. Write `#problem-def("MinimumCardinalityKey")[def][body]`: + - **Definition:** Given a set A of attribute names, functional dependencies F on A, and positive integer M, determine whether there exists a candidate key K ⊆ A with |K| ≤ M such that (K,A) ∈ F* and K is minimal. + - **Body:** Background on relational database theory and Armstrong's axioms. NP-completeness via Vertex Cover (Lucchesi & Osborn 1978). Best known: brute-force O(2^|A|). CeTZ diagram showing the example instance with attributes and FD arrows. +3. Run `make paper` to verify compilation. From 2ed0101a152e0f32ce6b474d10e136aa0f53d0e6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:13:37 +0800 Subject: [PATCH 2/6] Implement #444: MinimumCardinalityKey model - Add MinimumCardinalityKey satisfaction problem (set/ category) - Functional dependency closure under Armstrong's axioms - CLI create support with --num-attributes, --dependencies, --k flags - Paper entry with problem definition and worked example - 11 unit tests covering creation, evaluation, solver, serialization - Regenerate example-db fixtures --- docs/paper/reductions.typ | 24 ++ docs/paper/references.bib | 11 + problemreductions-cli/src/cli.rs | 10 +- problemreductions-cli/src/commands/create.rs | 66 ++++++ src/example_db/fixtures/examples.json | 1 + src/models/mod.rs | 4 +- src/models/set/minimum_cardinality_key.rs | 205 ++++++++++++++++++ src/models/set/mod.rs | 3 + .../models/set/minimum_cardinality_key.rs | 132 +++++++++++ 9 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 src/models/set/minimum_cardinality_key.rs create mode 100644 src/unit_tests/models/set/minimum_cardinality_key.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 4910b9b4f..5a029764a 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -78,6 +78,7 @@ "MaximumSetPacking": [Maximum Set Packing], "MinimumSetCovering": [Minimum Set Covering], "SetBasis": [Set Basis], + "MinimumCardinalityKey": [Minimum Cardinality Key], "SpinGlass": [Spin Glass], "QUBO": [QUBO], "ILP": [Integer Linear Programming], @@ -1261,6 +1262,29 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("MinimumCardinalityKey") + let n = x.instance.num_attributes + let deps = x.instance.dependencies + let m = deps.len() + let bound = x.instance.bound_k + let sample = x.samples.at(0) + let key-attrs = range(n).filter(i => sample.config.at(i) == 1) + let sat-count = x.optimal.len() + let fmt-set(s) = "${" + s.map(e => str(e)).join(", ") + "}$" + let fmt-fd(d) = fmt-set(d.at(0)) + " $arrow.r$ " + fmt-set(d.at(1)) + [ + #problem-def("MinimumCardinalityKey")[ + Given a set $A$ of attribute names, a collection $F$ of functional dependencies (ordered pairs of subsets of $A$), and a positive integer $M$, does there exist a candidate key $K subset.eq A$ with $|K| <= M$, i.e., a minimal subset $K$ such that the closure of $K$ under $F^*$ equals $A$? + ][ + The Minimum Cardinality Key problem arises in relational database theory, where identifying the smallest candidate key determines the most efficient way to uniquely identify rows in a relation. It was shown NP-complete by Lucchesi and Osborn (1978) @lucchesi1978keys via transformation from Vertex Cover. The problem appears as SR26 in Garey & Johnson (A4) @garey1979. The closure $F^*$ is defined by Armstrong's axioms: reflexivity ($B subset.eq C$ implies $C arrow.r B$), transitivity, and union. The best known exact algorithm is brute-force enumeration of all subsets of $A$, giving $O^*(2^(|A|))$ time#footnote[Lucchesi and Osborn give an output-polynomial algorithm for enumerating all candidate keys, but the number of keys can be exponential.]. + + *Example.* Let $A = {0, 1, ..., #(n - 1)}$ ($|A| = #n$) with $M = #bound$ and functional dependencies $F = {#deps.enumerate().map(((i, d)) => fmt-fd(d)).join(", ")}$. + The candidate key $K = #fmt-set(key-attrs)$ has $|K| = #key-attrs.len() <= #bound$. Its closure: start with ${0, 1}$; apply ${0, 1} arrow.r {2}$ to get ${0, 1, 2}$; apply ${0, 2} arrow.r {3}$ to get ${0, 1, 2, 3}$; apply ${1, 3} arrow.r {4}$ to get ${0, 1, 2, 3, 4}$; apply ${2, 4} arrow.r {5}$ to get $A$. Neither ${0}$ nor ${1}$ alone determines $A$, so $K$ is minimal. There are #sat-count satisfying encodings in total. + ] + ] +} + == Optimization Problems #{ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 6482b48a2..d13842dee 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -638,6 +638,17 @@ @article{lucchesi1978 doi = {10.1112/jlms/s2-17.3.369} } +@article{lucchesi1978keys, + author = {Cl\'audio L. Lucchesi and Sylvia L. Osborn}, + title = {Candidate Keys for Relations}, + journal = {Journal of Computer and System Sciences}, + volume = {17}, + number = {2}, + pages = {270--279}, + year = {1978}, + doi = {10.1016/0022-0000(78)90009-0} +} + @article{lenstra1976, author = {Jan Karel Lenstra and Alexander H. G. Rinnooy Kan}, title = {On General Routing Problems}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 4f0ff53e3..c37922544 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -234,6 +234,7 @@ Flags by problem type: MinimumSetCovering --universe, --sets [--weights] X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) SetBasis --universe, --sets, --k + MinimumCardinalityKey --num-attributes, --dependencies, --k BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank SteinerTree --graph, --edge-weights, --terminals @@ -272,7 +273,8 @@ Examples: pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1 pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\" - pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3")] + pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3 + pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2")] pub struct CreateArgs { /// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example. #[arg(value_parser = crate::problem_name::ProblemNameParser)] @@ -448,6 +450,12 @@ pub struct CreateArgs { /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, + /// Functional dependencies for MinimumCardinalityKey (semicolon-separated "lhs>rhs" pairs, e.g., "0,1>2;0,2>3") + #[arg(long)] + pub dependencies: Option, + /// Number of attributes for MinimumCardinalityKey + #[arg(long)] + pub num_attributes: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 47e5c682f..960916aad 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -77,6 +77,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.deadline.is_none() && args.num_processors.is_none() && args.alphabet_size.is_none() + && args.dependencies.is_none() + && args.num_attributes.is_none() && args.capacities.is_none() && args.source_1.is_none() && args.sink_1.is_none() @@ -300,6 +302,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", "SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3", + "MinimumCardinalityKey" => { + "--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2" + } "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", _ => "", } @@ -978,6 +983,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // MinimumCardinalityKey + "MinimumCardinalityKey" => { + let num_attributes = args.num_attributes.ok_or_else(|| { + anyhow::anyhow!( + "MinimumCardinalityKey requires --num-attributes (or --num-vertices), --dependencies, and --k\n\n\ + Usage: pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2" + ) + })?; + let k = args.k.ok_or_else(|| { + anyhow::anyhow!("MinimumCardinalityKey requires --k (bound on key cardinality)") + })?; + let deps_str = args.dependencies.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "MinimumCardinalityKey requires --dependencies (e.g., \"0,1>2;0,2>3\")" + ) + })?; + let dependencies = parse_dependencies(deps_str)?; + ( + ser(problemreductions::models::set::MinimumCardinalityKey::new( + num_attributes, + dependencies, + k, + ))?, + resolved_variant.clone(), + ) + } + // BicliqueCover "BicliqueCover" => { let left = args.left.ok_or_else(|| { @@ -1849,6 +1881,40 @@ fn parse_sets(args: &CreateArgs) -> Result>> { .collect() } +/// Parse `--dependencies` as semicolon-separated "lhs>rhs" pairs. +/// E.g., "0,1>2;0,2>3;1,3>4;2,4>5" means {0,1}->{2}, {0,2}->{3}, etc. +fn parse_dependencies(input: &str) -> Result, Vec)>> { + input + .split(';') + .map(|dep| { + let parts: Vec<&str> = dep.trim().split('>').collect(); + if parts.len() != 2 { + bail!( + "Invalid dependency format: expected 'lhs>rhs', got '{}'", + dep.trim() + ); + } + let lhs: Vec = parts[0] + .split(',') + .map(|s| { + s.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid attribute index: {}", e)) + }) + .collect::>()?; + let rhs: Vec = parts[1] + .split(',') + .map(|s| { + s.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid attribute index: {}", e)) + }) + .collect::>()?; + Ok((lhs, rhs)) + }) + .collect() +} + /// Parse `--partition` as semicolon-separated groups of comma-separated arc indices. /// E.g., "0,1;2,3;4,7;5,6" fn parse_partition_groups(args: &CreateArgs, num_arcs: usize) -> Result>> { diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index ea07c1daa..6369ffdb8 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -20,6 +20,7 @@ {"problem":"MaximumIndependentSet","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null],[4,0,null],[5,7,null],[7,9,null],[9,6,null],[6,8,null],[8,5,null],[0,5,null],[1,6,null],[2,7,null],[3,8,null],[4,9,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null,null,null]}},"weights":[5,1,1,1,1,3,1,1,1,3]},"samples":[{"config":[1,0,1,0,0,0,0,0,1,1],"metric":{"Valid":10}}],"optimal":[{"config":[1,0,1,0,0,0,0,0,1,1],"metric":{"Valid":10}}]}, {"problem":"MaximumMatching","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,0,0,1,0],"metric":{"Valid":2}}],"optimal":[{"config":[0,0,1,0,1,0],"metric":{"Valid":2}},{"config":[0,1,0,0,0,1],"metric":{"Valid":2}},{"config":[0,1,1,0,0,0],"metric":{"Valid":2}},{"config":[1,0,0,0,0,1],"metric":{"Valid":2}},{"config":[1,0,0,0,1,0],"metric":{"Valid":2}},{"config":[1,0,0,1,0,0],"metric":{"Valid":2}}]}, {"problem":"MaximumSetPacking","variant":{"weight":"i32"},"instance":{"sets":[[0,1],[1,2],[2,3],[3,4]],"weights":[1,1,1,1]},"samples":[{"config":[1,0,1,0],"metric":{"Valid":2}}],"optimal":[{"config":[0,1,0,1],"metric":{"Valid":2}},{"config":[1,0,0,1],"metric":{"Valid":2}},{"config":[1,0,1,0],"metric":{"Valid":2}}]}, + {"problem":"MinimumCardinalityKey","variant":{},"instance":{"bound_k":2,"dependencies":[[[0,1],[2]],[[0,2],[3]],[[1,3],[4]],[[2,4],[5]]],"num_attributes":6},"samples":[{"config":[1,1,0,0,0,0],"metric":true}],"optimal":[{"config":[1,1,0,0,0,0],"metric":true}]}, {"problem":"MinimumDominatingSet","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[0,0,1,1,0],"metric":{"Valid":2}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":2}},{"config":[0,1,0,0,1],"metric":{"Valid":2}},{"config":[0,1,0,1,0],"metric":{"Valid":2}},{"config":[0,1,1,0,0],"metric":{"Valid":2}},{"config":[1,0,0,0,1],"metric":{"Valid":2}},{"config":[1,0,0,1,0],"metric":{"Valid":2}},{"config":[1,0,1,0,0],"metric":{"Valid":2}}]}, {"problem":"MinimumFeedbackVertexSet","variant":{"weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,0,null],[0,3,null],[3,4,null],[4,1,null],[4,2,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[1,0,0,0,0],"metric":{"Valid":1}}],"optimal":[{"config":[0,0,1,0,0],"metric":{"Valid":1}},{"config":[1,0,0,0,0],"metric":{"Valid":1}}]}, {"problem":"MinimumSetCovering","variant":{"weight":"i32"},"instance":{"sets":[[0,1,2],[1,3],[2,3,4]],"universe_size":5,"weights":[1,1,1]},"samples":[{"config":[1,0,1],"metric":{"Valid":2}}],"optimal":[{"config":[1,0,1],"metric":{"Valid":2}}]}, diff --git a/src/models/mod.rs b/src/models/mod.rs index 5469c3a06..208fd02e3 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -24,4 +24,6 @@ pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, }; -pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; +pub use set::{ + ExactCoverBy3Sets, MaximumSetPacking, MinimumCardinalityKey, MinimumSetCovering, SetBasis, +}; diff --git a/src/models/set/minimum_cardinality_key.rs b/src/models/set/minimum_cardinality_key.rs new file mode 100644 index 000000000..a4a765cdf --- /dev/null +++ b/src/models/set/minimum_cardinality_key.rs @@ -0,0 +1,205 @@ +//! Minimum Cardinality Key problem implementation. +//! +//! Given a set of attribute names, functional dependencies, and a bound M, +//! determine whether there exists a candidate key of cardinality at most M. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinimumCardinalityKey", + display_name: "Minimum Cardinality Key", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Determine whether a relational system has a candidate key of bounded cardinality", + fields: &[ + FieldInfo { name: "num_attributes", type_name: "usize", description: "Number of attributes in the relation" }, + FieldInfo { name: "dependencies", type_name: "Vec<(Vec, Vec)>", description: "Functional dependencies as (lhs, rhs) pairs" }, + FieldInfo { name: "bound_k", type_name: "usize", description: "Upper bound on key cardinality" }, + ], + } +} + +/// The Minimum Cardinality Key decision problem. +/// +/// Given a set of attributes `A = {0, ..., n-1}`, a set of functional +/// dependencies `F` (each a pair `(X, Y)` where `X, Y` are subsets of `A`), +/// and a positive integer `k`, determine whether there exists a candidate key +/// (a minimal set of attributes that functionally determines all of `A`) of +/// cardinality at most `k`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimumCardinalityKey { + /// Number of attributes (elements are `0..num_attributes`). + num_attributes: usize, + /// Functional dependencies as `(lhs, rhs)` pairs. + dependencies: Vec<(Vec, Vec)>, + /// Upper bound on key cardinality. + bound_k: usize, +} + +impl MinimumCardinalityKey { + /// Create a new Minimum Cardinality Key instance. + /// + /// # Panics + /// + /// Panics if any attribute index in a dependency lies outside the attribute set. + pub fn new( + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + bound_k: usize, + ) -> Self { + let mut dependencies = dependencies; + for (dep_index, (lhs, rhs)) in dependencies.iter_mut().enumerate() { + lhs.sort_unstable(); + lhs.dedup(); + rhs.sort_unstable(); + rhs.dedup(); + for &attr in lhs.iter().chain(rhs.iter()) { + assert!( + attr < num_attributes, + "Dependency {} contains attribute {} which is outside attribute set of size {}", + dep_index, + attr, + num_attributes + ); + } + } + + Self { + num_attributes, + dependencies, + bound_k, + } + } + + /// Return the number of attributes. + pub fn num_attributes(&self) -> usize { + self.num_attributes + } + + /// Return the number of functional dependencies. + pub fn num_dependencies(&self) -> usize { + self.dependencies.len() + } + + /// Return the upper bound on key cardinality. + pub fn bound_k(&self) -> usize { + self.bound_k + } + + /// Return the functional dependencies. + pub fn dependencies(&self) -> &[(Vec, Vec)] { + &self.dependencies + } + + /// Compute the attribute closure of the selected attributes under the + /// functional dependencies. Starts with the selected set and repeatedly + /// applies each FD: if all lhs attributes are in the closure, add all rhs + /// attributes. Repeats until no change. + fn compute_closure(&self, selected: &[bool]) -> Vec { + let mut closure = selected.to_vec(); + loop { + let mut changed = false; + for (lhs, rhs) in &self.dependencies { + if lhs.iter().all(|&a| closure[a]) { + for &a in rhs { + if !closure[a] { + closure[a] = true; + changed = true; + } + } + } + } + if !changed { + break; + } + } + closure + } + + /// Check whether the selected attributes form a key (their closure equals + /// the full attribute set). + fn is_key(&self, selected: &[bool]) -> bool { + let closure = self.compute_closure(selected); + closure.iter().all(|&v| v) + } + + /// Check whether the selected attributes form a minimal key: they are a + /// key, and removing any single selected attribute breaks the key property. + fn is_minimal_key(&self, selected: &[bool]) -> bool { + if !self.is_key(selected) { + return false; + } + for i in 0..self.num_attributes { + if selected[i] { + let mut reduced = selected.to_vec(); + reduced[i] = false; + if self.is_key(&reduced) { + return false; + } + } + } + true + } +} + +impl Problem for MinimumCardinalityKey { + const NAME: &'static str = "MinimumCardinalityKey"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![2; self.num_attributes] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { + return false; + } + + let selected: Vec = config.iter().map(|&v| v == 1).collect(); + let count = selected.iter().filter(|&&v| v).count(); + + if count == 0 || count > self.bound_k { + return false; + } + + self.is_minimal_key(&selected) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for MinimumCardinalityKey {} + +crate::declare_variants! { + default sat MinimumCardinalityKey => "2^num_attributes", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "minimum_cardinality_key", + build: || { + let problem = MinimumCardinalityKey::new( + 6, + vec![ + (vec![0, 1], vec![2]), + (vec![0, 2], vec![3]), + (vec![1, 3], vec![4]), + (vec![2, 4], vec![5]), + ], + 2, + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![1, 1, 0, 0, 0, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/set/minimum_cardinality_key.rs"] +mod tests; diff --git a/src/models/set/mod.rs b/src/models/set/mod.rs index fb8ee7cd8..e04ff519f 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -7,11 +7,13 @@ pub(crate) mod exact_cover_by_3_sets; pub(crate) mod maximum_set_packing; +pub(crate) mod minimum_cardinality_key; pub(crate) mod minimum_set_covering; pub(crate) mod set_basis; pub use exact_cover_by_3_sets::ExactCoverBy3Sets; pub use maximum_set_packing::MaximumSetPacking; +pub use minimum_cardinality_key::MinimumCardinalityKey; pub use minimum_set_covering::MinimumSetCovering; pub use set_basis::SetBasis; @@ -21,6 +23,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec{2}, {0,2}->{3}, +/// {1,3}->{4}, {2,4}->{5}. K={0,1} is a candidate key of size 2. +fn instance1(bound_k: usize) -> MinimumCardinalityKey { + MinimumCardinalityKey::new( + 6, + vec![ + (vec![0, 1], vec![2]), + (vec![0, 2], vec![3]), + (vec![1, 3], vec![4]), + (vec![2, 4], vec![5]), + ], + bound_k, + ) +} + +/// Instance 2 from the issue: 6 attributes, FDs {0,1,2}->{3}, {3,4}->{5}. +/// No 2-element subset determines all attributes. +fn instance2() -> MinimumCardinalityKey { + MinimumCardinalityKey::new(6, vec![(vec![0, 1, 2], vec![3]), (vec![3, 4], vec![5])], 2) +} + +#[test] +fn test_minimum_cardinality_key_creation() { + let problem = instance1(2); + assert_eq!(problem.num_attributes(), 6); + assert_eq!(problem.num_dependencies(), 4); + assert_eq!(problem.bound_k(), 2); + assert_eq!(problem.num_variables(), 6); + assert_eq!(problem.dims(), vec![2; 6]); +} + +#[test] +fn test_minimum_cardinality_key_evaluation_yes() { + let problem = instance1(2); + // K={0,1}: closure under FDs reaches all 6 attributes, and it is minimal. + assert!(problem.evaluate(&[1, 1, 0, 0, 0, 0])); +} + +#[test] +fn test_minimum_cardinality_key_evaluation_no_instance() { + let problem = instance2(); + // No 2-element subset is a key for instance 2. + assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0])); + assert!(!problem.evaluate(&[1, 0, 1, 0, 0, 0])); + assert!(!problem.evaluate(&[0, 0, 0, 1, 1, 0])); +} + +#[test] +fn test_minimum_cardinality_key_non_minimal_rejected() { + let problem = instance1(3); + // K={0,1,2}: closure reaches all attributes, but {0,1} is a proper subset + // that is also a key, so {0,1,2} is NOT minimal. + assert!(!problem.evaluate(&[1, 1, 1, 0, 0, 0])); +} + +#[test] +fn test_minimum_cardinality_key_exceeds_bound() { + let problem = instance1(1); + // K={0,1} has |K|=2 > bound_k=1, so it must be rejected. + assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0])); +} + +#[test] +fn test_minimum_cardinality_key_solver() { + let problem = instance1(2); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + let solution_set: HashSet> = solutions.iter().cloned().collect(); + + assert!(!solutions.is_empty()); + assert!(solution_set.contains(&vec![1, 1, 0, 0, 0, 0])); + assert!(solutions.iter().all(|sol| problem.evaluate(sol))); +} + +#[test] +fn test_minimum_cardinality_key_serialization() { + let problem = instance1(2); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: MinimumCardinalityKey = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.num_attributes(), problem.num_attributes()); + assert_eq!(deserialized.num_dependencies(), problem.num_dependencies()); + assert_eq!(deserialized.bound_k(), problem.bound_k()); + assert_eq!(deserialized.dependencies(), problem.dependencies()); +} + +#[test] +fn test_minimum_cardinality_key_invalid_config() { + let problem = instance1(2); + // Wrong length. + assert!(!problem.evaluate(&[1, 1, 0, 0, 0])); + // Value > 1. + assert!(!problem.evaluate(&[2, 1, 0, 0, 0, 0])); +} + +#[test] +fn test_minimum_cardinality_key_empty_deps() { + // No FDs: closure(K) = K. Only K = {0,1,2} determines all attributes. + // It is minimal because removing any element gives a set that does not + // cover all 3 attributes. + let problem = MinimumCardinalityKey::new(3, vec![], 3); + assert!(problem.evaluate(&[1, 1, 1])); + // Any proper subset fails (not a key). + assert!(!problem.evaluate(&[1, 1, 0])); + assert!(!problem.evaluate(&[1, 0, 0])); + assert!(!problem.evaluate(&[0, 0, 0])); +} + +#[test] +#[should_panic(expected = "outside attribute set")] +fn test_minimum_cardinality_key_panics_on_invalid_index() { + MinimumCardinalityKey::new(3, vec![(vec![0, 3], vec![1])], 2); +} + +#[test] +fn test_minimum_cardinality_key_paper_example() { + let problem = instance1(2); + let solution = vec![1, 1, 0, 0, 0, 0]; + assert!(problem.evaluate(&solution)); + + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + let solution_set: HashSet> = solutions.iter().cloned().collect(); + assert!(solution_set.contains(&solution)); + // All returned solutions must be valid. + assert!(solutions.iter().all(|sol| problem.evaluate(sol))); +} From d48ec2566de9afe1eba900565421da8e5773c589 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:13:44 +0800 Subject: [PATCH 3/6] chore: remove plan file after implementation --- .../2026-03-17-minimum-cardinality-key.md | 103 ------------------ 1 file changed, 103 deletions(-) delete mode 100644 docs/plans/2026-03-17-minimum-cardinality-key.md diff --git a/docs/plans/2026-03-17-minimum-cardinality-key.md b/docs/plans/2026-03-17-minimum-cardinality-key.md deleted file mode 100644 index 1fa573e3e..000000000 --- a/docs/plans/2026-03-17-minimum-cardinality-key.md +++ /dev/null @@ -1,103 +0,0 @@ -# Plan: Add MinimumCardinalityKey Model (Issue #444) - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `MinimumCardinalityKey` | -| 2 | Mathematical definition | Given attribute set A, functional dependencies F, and bound M, is there a candidate key K ⊆ A with \|K\| ≤ M? A key K must: (1) determine all of A under closure F*, and (2) be minimal (no proper subset also determines A). | -| 3 | Problem type | Satisfaction (`Metric = bool`) | -| 4 | Type parameters | None | -| 5 | Struct fields | `num_attributes: usize`, `dependencies: Vec<(Vec, Vec)>`, `bound_k: usize` | -| 6 | Configuration space | `vec![2; num_attributes]` — binary per attribute (1 = included in K) | -| 7 | Feasibility check | K = selected attributes; compute closure(K) under F; check closure = A, \|K\| ≤ bound_k, and K is minimal | -| 8 | Objective function | N/A (satisfaction) — returns `true` iff valid candidate key of bounded size | -| 9 | Best known exact algorithm | O(2^num_attributes) brute-force enumeration (Lucchesi & Osborn 1978) | -| 10 | Solving strategy | BruteForce (enumerate subsets, check key property) | -| 11 | Category | `set/` (attribute sets + functional dependencies) | -| 12 | Expected outcome | Instance 1 (YES): A={0..5}, F={({0,1}→{2}), ({0,2}→{3}), ({1,3}→{4}), ({2,4}→{5})}, M=2. Key {0,1} determines all of A. Instance 2 (NO): same A, F={({0,1,2}→{3}), ({3,4}→{5})}, M=2. No 2-element key exists. | - -## Associated Rules - -- R120: Vertex Cover → MinimumCardinalityKey (this model is target) -- R122: MinimumCardinalityKey → PrimeAttributeName (this model is source) - -## Batch 1: Implementation (Steps 1–5.5) - -All tasks in this batch are independent and can run in parallel. - -### Task 1: Implement model + register + declare variants - -**Files:** `src/models/set/minimum_cardinality_key.rs`, `src/models/set/mod.rs`, `src/models/mod.rs` - -Follow `SetBasis` as the template (satisfaction problem, no type parameters, set/ category). - -1. Create `src/models/set/minimum_cardinality_key.rs`: - - `inventory::submit!` with `ProblemSchemaEntry` (name: "MinimumCardinalityKey", display_name: "Minimum Cardinality Key", aliases: &[], fields: num_attributes/usize, dependencies/Vec<(Vec, Vec)>, bound_k/usize) - - Struct `MinimumCardinalityKey` with `#[derive(Debug, Clone, Serialize, Deserialize)]` - - Fields: `num_attributes: usize`, `dependencies: Vec<(Vec, Vec)>`, `bound_k: usize` - - Constructor `new(num_attributes, dependencies, bound_k)` — validate all attribute indices in dependencies are < num_attributes - - Getter methods: `num_attributes()`, `num_dependencies()`, `bound_k()`, `dependencies()` - - Helper: `compute_closure(attrs: &[bool]) -> Vec` — iteratively apply FDs until fixpoint - - Helper: `is_minimal_key(config: &[usize]) -> bool` — for each selected attribute, check removing it breaks the key property - - `Problem` impl: NAME = "MinimumCardinalityKey", Metric = bool, dims = vec![2; num_attributes], variant = variant_params![] - - `evaluate()`: (a) K = selected attrs, (b) |K| ≤ bound_k, (c) closure(K) = A, (d) K is minimal - - `SatisfactionProblem` impl (marker) - - `declare_variants! { default sat MinimumCardinalityKey => "2^num_attributes" }` - - `#[cfg(test)] #[path]` link to unit tests - -2. Update `src/models/set/mod.rs`: - - Add `pub(crate) mod minimum_cardinality_key;` - - Add `pub use minimum_cardinality_key::MinimumCardinalityKey;` - - Add `specs.extend(minimum_cardinality_key::canonical_model_example_specs());` - -3. Update `src/models/mod.rs`: - - Add `MinimumCardinalityKey` to the set re-export line - -### Task 2: CLI registration - -**Files:** `problemreductions-cli/src/problem_name.rs`, `problemreductions-cli/src/commands/create.rs`, `problemreductions-cli/src/cli.rs` - -1. `problem_name.rs`: Add `"minimumcardinalitykey" => "MinimumCardinalityKey"` to `resolve_alias()` -2. `commands/create.rs`: Add match arm for "MinimumCardinalityKey" that parses `--num-attributes`, `--dependencies` (JSON array of [lhs, rhs] pairs), `--bound-k` -3. `cli.rs`: Add new CLI flags if needed (`--dependencies`, `--bound-k`) and update `CreateArgs` help table - -### Task 3: Canonical example in example_db - -**File:** `src/example_db/model_builders.rs` (aggregator) + `src/models/set/minimum_cardinality_key.rs` (specs function) - -Add `canonical_model_example_specs()` function in the model file returning a `ModelExampleSpec` with id "minimum_cardinality_key". Use Instance 1 from the issue (num_attributes=6, the 4 FDs, bound_k=2) and a known satisfying solution [1,1,0,0,0,0] (K={0,1}). - -### Task 4: Unit tests - -**File:** `src/unit_tests/models/set/minimum_cardinality_key.rs` - -Tests (≥3 required): -1. `test_minimum_cardinality_key_creation` — constructor, getters, dims -2. `test_minimum_cardinality_key_evaluation_yes` — Instance 1: config [1,1,0,0,0,0] evaluates to true -3. `test_minimum_cardinality_key_evaluation_no` — Instance 2: no config of size ≤2 is a key -4. `test_minimum_cardinality_key_non_minimal_rejected` — a superset of a key (e.g., {0,1,2} when {0,1} suffices) returns false -5. `test_minimum_cardinality_key_solver` — BruteForce finds all satisfying configs for Instance 1 -6. `test_minimum_cardinality_key_serialization` — serde roundtrip -7. `test_minimum_cardinality_key_closure_computation` — verify closure works correctly -8. `test_minimum_cardinality_key_invalid_config` — wrong length, out-of-range values -9. `test_minimum_cardinality_key_empty_dependencies` — edge case: no FDs, only trivial keys -10. `test_minimum_cardinality_key_paper_example` — (placeholder, finalized after Step 6) - -Also update `src/unit_tests/models/set/mod.rs` to include the new test module. - -### Task 5: Build verification - -Run `make test clippy` to verify everything compiles and passes. - -## Batch 2: Paper Entry (Step 6) - -Depends on Batch 1 completion (needs working model for exports). - -### Task 6: Write paper entry in docs/paper/reductions.typ - -1. Add display name: `"MinimumCardinalityKey": [Minimum Cardinality Key],` -2. Write `#problem-def("MinimumCardinalityKey")[def][body]`: - - **Definition:** Given a set A of attribute names, functional dependencies F on A, and positive integer M, determine whether there exists a candidate key K ⊆ A with |K| ≤ M such that (K,A) ∈ F* and K is minimal. - - **Body:** Background on relational database theory and Armstrong's axioms. NP-completeness via Vertex Cover (Lucchesi & Osborn 1978). Best known: brute-force O(2^|A|). CeTZ diagram showing the example instance with attributes and FD arrows. -3. Run `make paper` to verify compilation. From b47eb067b776060a72d7f6cd467ba99e04551acc Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 05:06:15 +0800 Subject: [PATCH 4/6] Fix MinimumCardinalityKey review feedback --- docs/src/cli.md | 10 ++- problemreductions-cli/src/commands/create.rs | 37 ++++++----- problemreductions-cli/tests/cli_tests.rs | 64 +++++++++++++++++++ src/models/set/minimum_cardinality_key.rs | 2 +- .../models/set/minimum_cardinality_key.rs | 10 +++ 5 files changed, 104 insertions(+), 19 deletions(-) diff --git a/docs/src/cli.md b/docs/src/cli.md index 5c595e905..073389a34 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -297,6 +297,7 @@ pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 - pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json 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" -o x3c.json +pred create MinimumCardinalityKey --num-attributes 6 --dependencies "0,1>2;0,2>3;1,3>4;2,4>5" --k 2 -o mck.json pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json ``` @@ -460,10 +461,17 @@ Source evaluation: Valid(2) ``` > **Note:** The ILP solver requires a reduction path from the target problem to ILP. -> Some problems (e.g., BoundedComponentSpanningForest, LengthBoundedDisjointPaths) do not currently have one, so use +> Some problems (e.g., BoundedComponentSpanningForest, LengthBoundedDisjointPaths, MinimumCardinalityKey) do not currently have one, so use > `pred solve --solver brute-force` for these. > For other problems, use `pred path ILP` to check whether an ILP reduction path exists. +For example, the canonical Minimum Cardinality Key instance can be created and solved with: + +```bash +pred create MinimumCardinalityKey --num-attributes 6 --dependencies "0,1>2;0,2>3;1,3>4;2,4>5" --k 2 -o mck.json +pred solve mck.json --solver brute-force +``` + ## Shell Completions Enable tab completion by adding one line to your shell config: diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index fde03a2bf..17f70206b 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -228,6 +228,9 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"", _ => "edge list: 0-1,1-2,2-3", }, + "Vec<(Vec, Vec)>" => { + "semicolon-separated dependencies: \"0,1>2;0,2>3\"" + } "Vec" => "comma-separated integers: 1,1,2", "Vec" => "comma-separated: 1,2,3", "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", @@ -310,6 +313,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { match (canonical, field_name) { ("BoundedComponentSpanningForest", "max_components") => return "k".to_string(), ("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(), + ("MinimumCardinalityKey", "bound_k") => return "k".to_string(), _ => {} } // General field-name overrides (previously in cli_flag_name) @@ -1082,7 +1086,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "MinimumCardinalityKey" => { let num_attributes = args.num_attributes.ok_or_else(|| { anyhow::anyhow!( - "MinimumCardinalityKey requires --num-attributes (or --num-vertices), --dependencies, and --k\n\n\ + "MinimumCardinalityKey requires --num-attributes, --dependencies, and --k\n\n\ Usage: pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2" ) })?; @@ -2004,6 +2008,19 @@ fn parse_sets(args: &CreateArgs) -> Result>> { /// Parse `--dependencies` as semicolon-separated "lhs>rhs" pairs. /// E.g., "0,1>2;0,2>3;1,3>4;2,4>5" means {0,1}->{2}, {0,2}->{3}, etc. fn parse_dependencies(input: &str) -> Result, Vec)>> { + fn parse_dependency_side(side: &str) -> Result> { + if side.trim().is_empty() { + return Ok(vec![]); + } + side.split(',') + .map(|s| { + s.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid attribute index: {}", e)) + }) + .collect() + } + input .split(';') .map(|dep| { @@ -2014,22 +2031,8 @@ fn parse_dependencies(input: &str) -> Result, Vec)>> { dep.trim() ); } - let lhs: Vec = parts[0] - .split(',') - .map(|s| { - s.trim() - .parse::() - .map_err(|e| anyhow::anyhow!("Invalid attribute index: {}", e)) - }) - .collect::>()?; - let rhs: Vec = parts[1] - .split(',') - .map(|s| { - s.trim() - .parse::() - .map_err(|e| anyhow::anyhow!("Invalid attribute index: {}", e)) - }) - .collect::>()?; + let lhs = parse_dependency_side(parts[0])?; + let rhs = parse_dependency_side(parts[1])?; Ok((lhs, rhs)) }) .collect() diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 9c35fd001..6b2452580 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1045,6 +1045,70 @@ fn test_create_set_basis_rejects_out_of_range_elements() { assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } +#[test] +fn test_create_minimum_cardinality_key_problem_help_uses_supported_flags() { + let output = pred().args(["create", "MinimumCardinalityKey"]).output().unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--num-attributes"), "stderr: {stderr}"); + assert!(stderr.contains("--dependencies"), "stderr: {stderr}"); + assert!(stderr.contains("--k"), "stderr: {stderr}"); + assert!( + stderr.contains("semicolon-separated dependencies"), + "stderr: {stderr}" + ); + assert!(!stderr.contains("--bound-k"), "stderr: {stderr}"); +} + +#[test] +fn test_create_minimum_cardinality_key_allows_empty_lhs_dependency() { + let output = pred() + .args([ + "create", + "MinimumCardinalityKey", + "--num-attributes", + "1", + "--dependencies", + ">0", + "--k", + "1", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "MinimumCardinalityKey"); + assert_eq!(json["data"]["num_attributes"], 1); + assert_eq!(json["data"]["bound_k"], 1); + assert_eq!(json["data"]["dependencies"][0][0], serde_json::json!([])); + assert_eq!(json["data"]["dependencies"][0][1], serde_json::json!([0])); +} + +#[test] +fn test_create_minimum_cardinality_key_missing_num_attributes_message() { + let output = pred() + .args([ + "create", + "MinimumCardinalityKey", + "--dependencies", + "0>0", + "--k", + "1", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("MinimumCardinalityKey requires --num-attributes")); + assert!(!stderr.contains("--num-vertices"), "stderr: {stderr}"); +} + #[test] fn test_create_then_evaluate() { // Create a problem diff --git a/src/models/set/minimum_cardinality_key.rs b/src/models/set/minimum_cardinality_key.rs index a4a765cdf..c9d6fc5b0 100644 --- a/src/models/set/minimum_cardinality_key.rs +++ b/src/models/set/minimum_cardinality_key.rs @@ -162,7 +162,7 @@ impl Problem for MinimumCardinalityKey { let selected: Vec = config.iter().map(|&v| v == 1).collect(); let count = selected.iter().filter(|&&v| v).count(); - if count == 0 || count > self.bound_k { + if count > self.bound_k { return false; } diff --git a/src/unit_tests/models/set/minimum_cardinality_key.rs b/src/unit_tests/models/set/minimum_cardinality_key.rs index b64fc2d2c..36d8841ab 100644 --- a/src/unit_tests/models/set/minimum_cardinality_key.rs +++ b/src/unit_tests/models/set/minimum_cardinality_key.rs @@ -111,6 +111,16 @@ fn test_minimum_cardinality_key_empty_deps() { assert!(!problem.evaluate(&[0, 0, 0])); } +#[test] +fn test_minimum_cardinality_key_empty_key_candidate() { + let problem = MinimumCardinalityKey::new(1, vec![(vec![], vec![0])], 1); + assert!(problem.evaluate(&[0])); + assert!(!problem.evaluate(&[1])); + + let solver = BruteForce::new(); + assert_eq!(solver.find_all_satisfying(&problem), vec![vec![0]]); +} + #[test] #[should_panic(expected = "outside attribute set")] fn test_minimum_cardinality_key_panics_on_invalid_index() { From 7c97a179f005619653bee2bbc98fc6a1e539ebea Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 18 Mar 2026 16:15:07 +0800 Subject: [PATCH 5/6] Fix formatting after merge conflict resolution Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 4 +--- problemreductions-cli/tests/cli_tests.rs | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index aaeec1e54..db65c252d 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -241,9 +241,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"", _ => "edge list: 0-1,1-2,2-3", }, - "Vec<(Vec, Vec)>" => { - "semicolon-separated dependencies: \"0,1>2;0,2>3\"" - } + "Vec<(Vec, Vec)>" => "semicolon-separated dependencies: \"0,1>2;0,2>3\"", "Vec" => "comma-separated integers: 4,5,3,2,6", "Vec" => "comma-separated: 1,2,3", "Vec" => "comma-separated indices: 0,2,4", diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 31174a206..bffb1a1b7 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1363,7 +1363,10 @@ fn test_create_set_basis_rejects_out_of_range_elements() { #[test] fn test_create_minimum_cardinality_key_problem_help_uses_supported_flags() { - let output = pred().args(["create", "MinimumCardinalityKey"]).output().unwrap(); + let output = pred() + .args(["create", "MinimumCardinalityKey"]) + .output() + .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("--num-attributes"), "stderr: {stderr}"); From 8f07479231890d98842fa0366d93bad7f4357398 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 18 Mar 2026 16:22:57 +0800 Subject: [PATCH 6/6] Add missing fields to empty_args() test helper after merge Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index db65c252d..e5e865f85 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -3307,6 +3307,8 @@ mod tests { deadline: None, num_processors: None, alphabet_size: None, + dependencies: None, + num_attributes: None, source_string: None, target_string: None, schedules: None,