From a36c1f043d852dc87930f48b2fa88e8070fc8905 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:32:54 +0800 Subject: [PATCH 01/10] Add plan for #445: [Model] AdditionalKey --- docs/plans/2026-03-17-additional-key.md | 244 ++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 docs/plans/2026-03-17-additional-key.md diff --git a/docs/plans/2026-03-17-additional-key.md b/docs/plans/2026-03-17-additional-key.md new file mode 100644 index 000000000..ac8470ba0 --- /dev/null +++ b/docs/plans/2026-03-17-additional-key.md @@ -0,0 +1,244 @@ +# Plan: Add AdditionalKey Model (#445) + +## Overview + +Add the `AdditionalKey` satisfaction problem from relational database theory (Garey & Johnson A4 SR27). Given a set of attributes, functional dependencies, a relation scheme, and known keys, determine whether there exists a candidate key not in the known set. + +**Problem type:** Satisfaction (`Metric = bool`) +**Category:** `misc` (unique input structure — relational database theory) +**Type parameters:** None +**Associated rules:** R121 HittingSet → AdditionalKey (inbound only — this model will be an orphan until that rule is implemented) + +## Information Checklist + +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `AdditionalKey` | +| 2 | Mathematical definition | Given attribute set A, functional dependencies F on A, relation R ⊆ A, and known keys K for ⟨R,F⟩, determine if R has a candidate key not in K | +| 3 | Problem type | Satisfaction (decision) | +| 4 | Type parameters | None | +| 5 | Struct fields | `num_attributes: usize`, `dependencies: Vec<(Vec, Vec)>`, `relation_attrs: Vec`, `known_keys: Vec>` | +| 6 | Configuration space | `vec![2; relation_attrs.len()]` — binary selection over relation attributes | +| 7 | Feasibility check | Closure of selected attrs under F covers all relation_attrs, selected set is minimal (no proper subset also covers), and selected set is not in known_keys | +| 8 | Objective function | N/A (satisfaction: returns bool) | +| 9 | Best known exact algorithm | Brute-force enumeration: O(2^num_relation_attrs × num_dependencies × num_attributes) — Beeri & Bernstein (1979) | +| 10 | Solving strategy | BruteForce works directly | +| 11 | Category | `misc` | +| 12 | Expected outcome | Instance 1 (YES): attrs {0,2} is an additional key. Instance 2 (NO): only key {0} is already known | + +## Batch 1: Implementation (Steps 1–5.5) + +### Step 1: Create model file `src/models/misc/additional_key.rs` + +Follow `SubsetSum` as the reference satisfaction problem pattern. + +**Schema registration:** +```rust +inventory::submit! { + ProblemSchemaEntry { + name: "AdditionalKey", + display_name: "Additional Key", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Determine whether a relational schema has a candidate key not in a given set", + fields: &[ + FieldInfo { name: "num_attributes", type_name: "usize", description: "Number of attributes in A" }, + FieldInfo { name: "dependencies", type_name: "Vec<(Vec, Vec)>", description: "Functional dependencies F; each (lhs, rhs)" }, + FieldInfo { name: "relation_attrs", type_name: "Vec", description: "Relation scheme attributes R ⊆ A" }, + FieldInfo { name: "known_keys", type_name: "Vec>", description: "Known candidate keys K" }, + ], + } +} +``` + +**Struct:** +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdditionalKey { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + relation_attrs: Vec, + known_keys: Vec>, +} +``` + +**Constructor:** `new(num_attributes, dependencies, relation_attrs, known_keys)` with validation: +- All attribute indices < num_attributes +- relation_attrs elements are unique and sorted +- known_keys entries are sorted for consistent comparison + +**Size getters:** +- `num_attributes() -> usize` +- `num_dependencies() -> usize` (returns `dependencies.len()`) +- `num_relation_attrs() -> usize` (returns `relation_attrs.len()`) +- `num_known_keys() -> usize` (returns `known_keys.len()`) + +**Core algorithm — closure computation:** +```rust +fn compute_closure(&self, attrs: &HashSet) -> HashSet { + let mut closure = attrs.clone(); + let mut changed = true; + while changed { + changed = false; + for (lhs, rhs) in &self.dependencies { + if lhs.iter().all(|a| closure.contains(a)) { + for &a in rhs { + if closure.insert(a) { + changed = true; + } + } + } + } + } + closure +} +``` + +**Problem trait:** +```rust +impl Problem for AdditionalKey { + const NAME: &'static str = "AdditionalKey"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.relation_attrs.len()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + // 1. Check config length and binary values + // 2. Build selected attribute set from config + // 3. Compute closure under FDs + // 4. Check closure covers all relation_attrs + // 5. Check minimality: no proper subset also has full closure + // 6. Check selected set is not in known_keys + } +} + +impl SatisfactionProblem for AdditionalKey {} +``` + +**Variant declaration:** +```rust +crate::declare_variants! { + default sat AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", +} +``` + +### Step 2: Register in module system + +1. **`src/models/misc/mod.rs`**: Add `mod additional_key;` and `pub use additional_key::AdditionalKey;` +2. **`src/models/mod.rs`**: Add `AdditionalKey` to the `misc` re-export line + +### Step 3: CLI registration + +1. **`problemreductions-cli/src/commands/create.rs`**: Add match arm for `"AdditionalKey"` that parses `--dependencies`, `--relation-attrs`, `--known-keys`, and `--num-attributes` flags. Since this problem has a unique schema, it needs custom argument parsing. + +2. **`problemreductions-cli/src/cli.rs`**: Add CLI flags if not already present: + - `--num-attributes` (usize) + - `--dependencies` (string, e.g., "0,1:2,3;2,3:4,5" for {0,1}→{2,3} and {2,3}→{4,5}) + - `--relation-attrs` (comma-separated usize list) + - `--known-keys` (string, e.g., "0,1;2,3" for [{0,1}, {2,3}]) + +3. **Help text**: Add entry to "Flags by problem type" table + +### Step 4: Add canonical model example + +In `src/models/misc/additional_key.rs`, add: +```rust +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "additional_key", + build: || { + let problem = AdditionalKey::new( + 6, + vec![ + (vec![0, 1], vec![2, 3]), + (vec![2, 3], vec![4, 5]), + (vec![4, 5], vec![0, 1]), + (vec![0, 2], vec![3]), + (vec![3, 5], vec![1]), + ], + vec![0, 1, 2, 3, 4, 5], + vec![vec![0, 1], vec![2, 3], vec![4, 5]], + ); + // Config for key {0, 2}: [1, 0, 1, 0, 0, 0] + crate::example_db::specs::satisfaction_example( + problem, + vec![vec![1, 0, 1, 0, 0, 0]], + ) + }, + }] +} +``` + +Register in `src/models/misc/mod.rs` `canonical_model_example_specs()`. + +### Step 5: Write unit tests + +Create `src/unit_tests/models/misc/additional_key.rs` with: + +1. **`test_additional_key_creation`**: Construct instance, verify getters and dims +2. **`test_additional_key_evaluate_satisfying`**: Instance 1 — verify {0,2} is a valid additional key +3. **`test_additional_key_evaluate_unsatisfying`**: Instance 2 — verify no additional key exists +4. **`test_additional_key_evaluate_non_minimal`**: Verify a superset of a key returns false +5. **`test_additional_key_evaluate_known_key`**: Verify a known key returns false +6. **`test_additional_key_evaluate_not_a_key`**: Verify a non-key (closure ≠ R) returns false +7. **`test_additional_key_wrong_config_length`**: Wrong-length config returns false +8. **`test_additional_key_invalid_variable_value`**: Config with value ≥ 2 returns false +9. **`test_additional_key_brute_force`**: BruteForce finds a satisfying solution +10. **`test_additional_key_brute_force_all`**: All satisfying solutions are valid +11. **`test_additional_key_serialization`**: Round-trip serde test +12. **`test_additional_key_paper_example`**: Verify the paper example instance (same as canonical example) + +Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the model file. + +### Step 5.5: Add trait_consistency entry + +In `src/unit_tests/trait_consistency.rs`: +- Add `check_problem_trait(...)` call for `AdditionalKey` with the small Instance 2 (3 attributes) + +## Batch 2: Paper Entry (Step 6) + +### Step 6: Document in paper + +**File:** `docs/paper/reductions.typ` + +1. **Add display name:** +```typst +"AdditionalKey": [Additional Key], +``` + +2. **Add problem-def:** +```typst +#problem-def("AdditionalKey")[ + Given a set $A$ of attribute names, a collection $F$ of functional dependencies on $A$, + a subset $R subset.eq A$, and a set $K$ of candidate keys for the relational scheme $angle.l R, F angle.r$, + determine whether there exists a subset $R' subset.eq R$ such that $R' in.not K$, + $(R', R) in F^*$, and for no $R'' subset.neq R'$ is $(R'', R) in F^*$. +][ + A classical NP-complete problem from relational database theory @beeri1979. + The problem is central to database normalization: enumerating all candidate keys + is necessary to verify Boyce-Codd Normal Form (BCNF), and the NP-completeness + of Additional Key implies that BCNF testing is intractable in general. + The best known exact algorithm is brute-force enumeration of all $2^(|R|)$ subsets, + checking each for the key property via closure computation under Armstrong's axioms. + #footnote[No algorithm improving on brute-force is known for the Additional Key problem.] + + *Example.* Let $A = {0, 1, 2, 3, 4, 5}$ with functional dependencies + ${0,1} -> {2,3}$, ${2,3} -> {4,5}$, ${4,5} -> {0,1}$, + ${0,2} -> {3}$, ${3,5} -> {1}$, relation $R = A$, + and known keys $K = {{0,1}, {2,3}, {4,5}}$. + The subset ${0,2}$ is an additional key: its closure under $F$ reaches all of $A$ + via ${0,2} -> {3} -> {4,5} -> {0,1}$, it is minimal, and ${0,2} in.not K$. +] +``` + +3. **Add BibTeX entry** for Beeri & Bernstein (1979) if not already present. + +4. **Build and verify:** `make paper` From 8f352b8b04075dc79ffdb0f3c92f68a1c2acd27a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:50:45 +0800 Subject: [PATCH 02/10] Implement AdditionalKey satisfaction problem model (#445) Add the Additional Key problem from relational database theory (Garey & Johnson SR7). Given a relational schema (R, F) and known candidate keys K, determines whether R has a candidate key not in K. - Model file with closure computation, minimality check, and known-key filter - 13 unit tests covering creation, evaluation, edge cases, brute force, serialization - CLI create support with --num-attributes, --dependencies, --relation-attrs, --known-keys - Module registration and re-exports - Canonical example-db entry with regenerated fixtures Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/cli.rs | 13 + problemreductions-cli/src/commands/create.rs | 61 +++- src/example_db/fixtures/examples.json | 1 + src/models/misc/additional_key.rs | 287 +++++++++++++++++++ src/models/misc/mod.rs | 4 + src/models/mod.rs | 6 +- src/unit_tests/models/misc/additional_key.rs | 156 ++++++++++ 7 files changed, 523 insertions(+), 5 deletions(-) create mode 100644 src/models/misc/additional_key.rs create mode 100644 src/unit_tests/models/misc/additional_key.rs diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index e71a9e426..c583412e7 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -243,6 +243,7 @@ Flags by problem type: OptimalLinearArrangement --graph, --bound RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices] + AdditionalKey --num-attributes, --dependencies, --relation-attrs, --known-keys SubgraphIsomorphism --graph (host), --pattern (pattern) LCS --strings FAS --arcs [--weights] [--num-vertices] @@ -456,6 +457,18 @@ pub struct CreateArgs { /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, + /// Number of attributes for AdditionalKey + #[arg(long)] + pub num_attributes: Option, + /// Functional dependencies for AdditionalKey (e.g., "0,1:2,3;2,3:4,5") + #[arg(long)] + pub dependencies: Option, + /// Relation scheme attributes for AdditionalKey (comma-separated, e.g., "0,1,2,3,4,5") + #[arg(long)] + pub relation_attrs: Option, + /// Known candidate keys for AdditionalKey (e.g., "0,1;2,3") + #[arg(long)] + pub known_keys: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 03220f4f6..f2532eb55 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -13,8 +13,9 @@ use problemreductions::models::graph::{ SteinerTree, }; use problemreductions::models::misc::{ - BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, + AdditionalKey, BinPacking, FlowShopScheduling, LongestCommonSubsequence, + MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, + ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -90,6 +91,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.sink_2.is_none() && args.requirement_1.is_none() && args.requirement_2.is_none() + && args.num_attributes.is_none() + && args.dependencies.is_none() + && args.relation_attrs.is_none() + && args.known_keys.is_none() } fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> { @@ -292,6 +297,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "MultipleChoiceBranching" => { "--arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10" } + "AdditionalKey" => "--num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5;4,5:0,1\" --relation-attrs 0,1,2,3,4,5 --known-keys \"0,1;2,3;4,5\"", "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", @@ -932,6 +938,57 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { } } + // AdditionalKey + "AdditionalKey" => { + let usage = "Usage: pred create AdditionalKey --num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5\" --relation-attrs \"0,1,2,3,4,5\" --known-keys \"0,1;2,3\""; + let num_attributes = args.num_attributes.ok_or_else(|| { + anyhow::anyhow!( + "AdditionalKey requires --num-attributes\n\n{usage}" + ) + })?; + let deps_str = args.dependencies.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "AdditionalKey requires --dependencies\n\n{usage}" + ) + })?; + let ra_str = args.relation_attrs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "AdditionalKey requires --relation-attrs\n\n{usage}" + ) + })?; + let dependencies: Vec<(Vec, Vec)> = deps_str + .split(';') + .map(|dep| { + let parts: Vec<&str> = dep.trim().split(':').collect(); + anyhow::ensure!( + parts.len() == 2, + "Invalid dependency format '{}', expected 'lhs:rhs' (e.g., '0,1:2,3')", + dep.trim() + ); + let lhs: Vec = util::parse_comma_list(parts[0].trim())?; + let rhs: Vec = util::parse_comma_list(parts[1].trim())?; + Ok((lhs, rhs)) + }) + .collect::>>()?; + let relation_attrs: Vec = util::parse_comma_list(ra_str)?; + let known_keys: Vec> = match args.known_keys.as_deref() { + Some(s) if !s.is_empty() => s + .split(';') + .map(|k| util::parse_comma_list(k.trim())) + .collect::>>()?, + _ => vec![], + }; + ( + ser(AdditionalKey::new( + num_attributes, + dependencies, + relation_attrs, + known_keys, + ))?, + resolved_variant.clone(), + ) + } + // SubsetSum "SubsetSum" => { let sizes_str = args.sizes.as_deref().ok_or_else(|| { diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 6e52883b2..fe36a1947 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -1,5 +1,6 @@ { "models": [ + {"problem":"AdditionalKey","variant":{},"instance":{"dependencies":[[[0,1],[2,3]],[[2,3],[4,5]],[[4,5],[0,1]],[[0,2],[3]],[[3,5],[1]]],"known_keys":[[0,1],[2,3],[4,5]],"num_attributes":6,"relation_attrs":[0,1,2,3,4,5]},"samples":[{"config":[1,0,1,0,0,0],"metric":true}],"optimal":[{"config":[1,0,0,1,0,1],"metric":true},{"config":[1,0,1,0,0,0],"metric":true}]}, {"problem":"BMF","variant":{},"instance":{"k":2,"m":3,"matrix":[[true,true,false],[true,true,true],[false,true,true]],"n":3},"samples":[{"config":[1,0,1,1,0,1,1,1,0,0,1,1],"metric":{"Valid":0}}],"optimal":[{"config":[0,1,1,1,1,0,0,1,1,1,1,0],"metric":{"Valid":0}},{"config":[1,0,1,1,0,1,1,1,0,0,1,1],"metric":{"Valid":0}}]}, {"problem":"BicliqueCover","variant":{},"instance":{"graph":{"edges":[[0,0],[0,1],[1,1],[1,2]],"left_size":2,"right_size":3},"k":2},"samples":[{"config":[1,0,0,1,1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[0,1,0,1,0,1,0,1,0,1],"metric":{"Valid":5}},{"config":[1,0,1,0,1,0,1,0,1,0],"metric":{"Valid":5}}]}, {"problem":"BoundedComponentSpanningForest","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,5,null],[5,6,null],[6,7,null],[0,7,null],[1,5,null],[2,6,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null]}},"max_components":3,"max_weight":6,"weights":[2,3,1,2,3,1,2,1]},"samples":[{"config":[0,0,1,1,1,2,2,0],"metric":true}],"optimal":[{"config":[0,0,0,1,1,1,2,2],"metric":true},{"config":[0,0,0,1,1,2,2,2],"metric":true},{"config":[0,0,0,2,2,1,1,1],"metric":true},{"config":[0,0,0,2,2,2,1,1],"metric":true},{"config":[0,0,1,1,1,0,2,2],"metric":true},{"config":[0,0,1,1,1,2,2,0],"metric":true},{"config":[0,0,1,1,1,2,2,2],"metric":true},{"config":[0,0,1,1,2,0,1,1],"metric":true},{"config":[0,0,1,1,2,1,1,0],"metric":true},{"config":[0,0,1,1,2,2,1,0],"metric":true},{"config":[0,0,1,1,2,2,1,1],"metric":true},{"config":[0,0,1,1,2,2,2,0],"metric":true},{"config":[0,0,1,2,2,0,1,1],"metric":true},{"config":[0,0,1,2,2,1,1,0],"metric":true},{"config":[0,0,1,2,2,1,1,1],"metric":true},{"config":[0,0,1,2,2,2,1,0],"metric":true},{"config":[0,0,1,2,2,2,1,1],"metric":true},{"config":[0,0,2,1,1,0,2,2],"metric":true},{"config":[0,0,2,1,1,1,2,0],"metric":true},{"config":[0,0,2,1,1,1,2,2],"metric":true},{"config":[0,0,2,1,1,2,2,0],"metric":true},{"config":[0,0,2,1,1,2,2,2],"metric":true},{"config":[0,0,2,2,1,0,2,2],"metric":true},{"config":[0,0,2,2,1,1,1,0],"metric":true},{"config":[0,0,2,2,1,1,2,0],"metric":true},{"config":[0,0,2,2,1,1,2,2],"metric":true},{"config":[0,0,2,2,1,2,2,0],"metric":true},{"config":[0,0,2,2,2,0,1,1],"metric":true},{"config":[0,0,2,2,2,1,1,0],"metric":true},{"config":[0,0,2,2,2,1,1,1],"metric":true},{"config":[0,1,0,2,2,1,0,0],"metric":true},{"config":[0,1,0,2,2,2,0,0],"metric":true},{"config":[0,1,1,1,2,0,0,0],"metric":true},{"config":[0,1,1,1,2,2,0,0],"metric":true},{"config":[0,1,1,1,2,2,2,0],"metric":true},{"config":[0,1,1,2,2,0,0,0],"metric":true},{"config":[0,1,1,2,2,1,0,0],"metric":true},{"config":[0,1,1,2,2,2,0,0],"metric":true},{"config":[0,1,1,2,2,2,1,0],"metric":true},{"config":[0,1,2,2,2,0,0,0],"metric":true},{"config":[0,1,2,2,2,1,0,0],"metric":true},{"config":[0,1,2,2,2,1,1,0],"metric":true},{"config":[0,2,0,1,1,1,0,0],"metric":true},{"config":[0,2,0,1,1,2,0,0],"metric":true},{"config":[0,2,1,1,1,0,0,0],"metric":true},{"config":[0,2,1,1,1,2,0,0],"metric":true},{"config":[0,2,1,1,1,2,2,0],"metric":true},{"config":[0,2,2,1,1,0,0,0],"metric":true},{"config":[0,2,2,1,1,1,0,0],"metric":true},{"config":[0,2,2,1,1,1,2,0],"metric":true},{"config":[0,2,2,1,1,2,0,0],"metric":true},{"config":[0,2,2,2,1,0,0,0],"metric":true},{"config":[0,2,2,2,1,1,0,0],"metric":true},{"config":[0,2,2,2,1,1,1,0],"metric":true},{"config":[1,0,0,0,2,1,1,1],"metric":true},{"config":[1,0,0,0,2,2,1,1],"metric":true},{"config":[1,0,0,0,2,2,2,1],"metric":true},{"config":[1,0,0,2,2,0,1,1],"metric":true},{"config":[1,0,0,2,2,1,1,1],"metric":true},{"config":[1,0,0,2,2,2,0,1],"metric":true},{"config":[1,0,0,2,2,2,1,1],"metric":true},{"config":[1,0,1,2,2,0,1,1],"metric":true},{"config":[1,0,1,2,2,2,1,1],"metric":true},{"config":[1,0,2,2,2,0,0,1],"metric":true},{"config":[1,0,2,2,2,0,1,1],"metric":true},{"config":[1,0,2,2,2,1,1,1],"metric":true},{"config":[1,1,0,0,0,1,2,2],"metric":true},{"config":[1,1,0,0,0,2,2,1],"metric":true},{"config":[1,1,0,0,0,2,2,2],"metric":true},{"config":[1,1,0,0,2,0,0,1],"metric":true},{"config":[1,1,0,0,2,1,0,0],"metric":true},{"config":[1,1,0,0,2,2,0,0],"metric":true},{"config":[1,1,0,0,2,2,0,1],"metric":true},{"config":[1,1,0,0,2,2,2,1],"metric":true},{"config":[1,1,0,2,2,0,0,0],"metric":true},{"config":[1,1,0,2,2,0,0,1],"metric":true},{"config":[1,1,0,2,2,1,0,0],"metric":true},{"config":[1,1,0,2,2,2,0,0],"metric":true},{"config":[1,1,0,2,2,2,0,1],"metric":true},{"config":[1,1,1,0,0,0,2,2],"metric":true},{"config":[1,1,1,0,0,2,2,2],"metric":true},{"config":[1,1,1,2,2,0,0,0],"metric":true},{"config":[1,1,1,2,2,2,0,0],"metric":true},{"config":[1,1,2,0,0,0,2,1],"metric":true},{"config":[1,1,2,0,0,0,2,2],"metric":true},{"config":[1,1,2,0,0,1,2,2],"metric":true},{"config":[1,1,2,0,0,2,2,1],"metric":true},{"config":[1,1,2,0,0,2,2,2],"metric":true},{"config":[1,1,2,2,0,0,0,1],"metric":true},{"config":[1,1,2,2,0,0,2,1],"metric":true},{"config":[1,1,2,2,0,0,2,2],"metric":true},{"config":[1,1,2,2,0,1,2,2],"metric":true},{"config":[1,1,2,2,0,2,2,1],"metric":true},{"config":[1,1,2,2,2,0,0,0],"metric":true},{"config":[1,1,2,2,2,0,0,1],"metric":true},{"config":[1,1,2,2,2,1,0,0],"metric":true},{"config":[1,2,0,0,0,1,1,1],"metric":true},{"config":[1,2,0,0,0,2,1,1],"metric":true},{"config":[1,2,0,0,0,2,2,1],"metric":true},{"config":[1,2,1,0,0,0,1,1],"metric":true},{"config":[1,2,1,0,0,2,1,1],"metric":true},{"config":[1,2,2,0,0,0,1,1],"metric":true},{"config":[1,2,2,0,0,0,2,1],"metric":true},{"config":[1,2,2,0,0,1,1,1],"metric":true},{"config":[1,2,2,0,0,2,1,1],"metric":true},{"config":[1,2,2,2,0,0,0,1],"metric":true},{"config":[1,2,2,2,0,0,1,1],"metric":true},{"config":[1,2,2,2,0,1,1,1],"metric":true},{"config":[2,0,0,0,1,1,1,2],"metric":true},{"config":[2,0,0,0,1,1,2,2],"metric":true},{"config":[2,0,0,0,1,2,2,2],"metric":true},{"config":[2,0,0,1,1,0,2,2],"metric":true},{"config":[2,0,0,1,1,1,0,2],"metric":true},{"config":[2,0,0,1,1,1,2,2],"metric":true},{"config":[2,0,0,1,1,2,2,2],"metric":true},{"config":[2,0,1,1,1,0,0,2],"metric":true},{"config":[2,0,1,1,1,0,2,2],"metric":true},{"config":[2,0,1,1,1,2,2,2],"metric":true},{"config":[2,0,2,1,1,0,2,2],"metric":true},{"config":[2,0,2,1,1,1,2,2],"metric":true},{"config":[2,1,0,0,0,1,1,2],"metric":true},{"config":[2,1,0,0,0,1,2,2],"metric":true},{"config":[2,1,0,0,0,2,2,2],"metric":true},{"config":[2,1,1,0,0,0,1,2],"metric":true},{"config":[2,1,1,0,0,0,2,2],"metric":true},{"config":[2,1,1,0,0,1,2,2],"metric":true},{"config":[2,1,1,0,0,2,2,2],"metric":true},{"config":[2,1,1,1,0,0,0,2],"metric":true},{"config":[2,1,1,1,0,0,2,2],"metric":true},{"config":[2,1,1,1,0,2,2,2],"metric":true},{"config":[2,1,2,0,0,0,2,2],"metric":true},{"config":[2,1,2,0,0,1,2,2],"metric":true},{"config":[2,2,0,0,0,1,1,1],"metric":true},{"config":[2,2,0,0,0,1,1,2],"metric":true},{"config":[2,2,0,0,0,2,1,1],"metric":true},{"config":[2,2,0,0,1,0,0,2],"metric":true},{"config":[2,2,0,0,1,1,0,0],"metric":true},{"config":[2,2,0,0,1,1,0,2],"metric":true},{"config":[2,2,0,0,1,1,1,2],"metric":true},{"config":[2,2,0,0,1,2,0,0],"metric":true},{"config":[2,2,0,1,1,0,0,0],"metric":true},{"config":[2,2,0,1,1,0,0,2],"metric":true},{"config":[2,2,0,1,1,1,0,0],"metric":true},{"config":[2,2,0,1,1,1,0,2],"metric":true},{"config":[2,2,0,1,1,2,0,0],"metric":true},{"config":[2,2,1,0,0,0,1,1],"metric":true},{"config":[2,2,1,0,0,0,1,2],"metric":true},{"config":[2,2,1,0,0,1,1,1],"metric":true},{"config":[2,2,1,0,0,1,1,2],"metric":true},{"config":[2,2,1,0,0,2,1,1],"metric":true},{"config":[2,2,1,1,0,0,0,2],"metric":true},{"config":[2,2,1,1,0,0,1,1],"metric":true},{"config":[2,2,1,1,0,0,1,2],"metric":true},{"config":[2,2,1,1,0,1,1,2],"metric":true},{"config":[2,2,1,1,0,2,1,1],"metric":true},{"config":[2,2,1,1,1,0,0,0],"metric":true},{"config":[2,2,1,1,1,0,0,2],"metric":true},{"config":[2,2,1,1,1,2,0,0],"metric":true},{"config":[2,2,2,0,0,0,1,1],"metric":true},{"config":[2,2,2,0,0,1,1,1],"metric":true},{"config":[2,2,2,1,1,0,0,0],"metric":true},{"config":[2,2,2,1,1,1,0,0],"metric":true}]}, diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs new file mode 100644 index 000000000..248825173 --- /dev/null +++ b/src/models/misc/additional_key.rs @@ -0,0 +1,287 @@ +//! Additional Key problem implementation. +//! +//! Given a relational schema (R, F) and a set K of known candidate keys, +//! determine whether there exists a candidate key of R (under the functional +//! dependencies F) that is not in K. A candidate key is a minimal set of +//! attributes whose closure under F covers all of R. +//! +//! The problem is NP-complete (Garey & Johnson, SR7). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "AdditionalKey", + display_name: "Additional Key", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Determine whether a relational schema has a candidate key not in a given set", + fields: &[ + FieldInfo { name: "num_attributes", type_name: "usize", description: "Number of attributes in A" }, + FieldInfo { name: "dependencies", type_name: "Vec<(Vec, Vec)>", description: "Functional dependencies F; each (lhs, rhs)" }, + FieldInfo { name: "relation_attrs", type_name: "Vec", description: "Relation scheme attributes R ⊆ A" }, + FieldInfo { name: "known_keys", type_name: "Vec>", description: "Known candidate keys K" }, + ], + } +} + +/// The Additional Key problem. +/// +/// Given a set `A` of attributes, a set of functional dependencies `F` over `A`, +/// a relation scheme `R ⊆ A`, and a set `K` of known candidate keys of `R` +/// under `F`, determine whether `R` has a candidate key not in `K`. +/// +/// A **candidate key** is a minimal subset `X ⊆ R` such that the closure of `X` +/// under `F` contains all attributes of `R`. +/// +/// # Representation +/// +/// Each attribute in `R` has a binary variable: `x_i = 1` if the attribute is +/// selected, `0` otherwise. A configuration is satisfying iff the selected +/// attributes form a candidate key of `R` that is not in `K`. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::AdditionalKey; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = AdditionalKey::new( +/// 3, +/// vec![(vec![0], vec![1, 2])], +/// vec![0, 1, 2], +/// vec![], +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdditionalKey { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + relation_attrs: Vec, + known_keys: Vec>, +} + +impl AdditionalKey { + /// Create a new AdditionalKey instance. + /// + /// # Panics + /// + /// Panics if any attribute index is >= `num_attributes`, or if + /// `relation_attrs` contains duplicates. + pub fn new( + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + relation_attrs: Vec, + known_keys: Vec>, + ) -> Self { + // Validate all attribute indices + for &a in &relation_attrs { + assert!( + a < num_attributes, + "relation_attrs element {a} >= num_attributes {num_attributes}" + ); + } + // Validate relation_attrs uniqueness + let mut sorted_ra = relation_attrs.clone(); + sorted_ra.sort_unstable(); + sorted_ra.dedup(); + assert_eq!( + sorted_ra.len(), + relation_attrs.len(), + "relation_attrs contains duplicates" + ); + for (lhs, rhs) in &dependencies { + for &a in lhs { + assert!( + a < num_attributes, + "dependency lhs attribute {a} >= num_attributes {num_attributes}" + ); + } + for &a in rhs { + assert!( + a < num_attributes, + "dependency rhs attribute {a} >= num_attributes {num_attributes}" + ); + } + } + for key in &known_keys { + for &a in key { + assert!( + a < num_attributes, + "known_keys attribute {a} >= num_attributes {num_attributes}" + ); + } + } + // Sort known_keys entries internally for consistent comparison + let known_keys: Vec> = known_keys + .into_iter() + .map(|mut k| { + k.sort_unstable(); + k + }) + .collect(); + Self { + num_attributes, + dependencies, + relation_attrs, + known_keys, + } + } + + /// Returns the number of attributes in the universal set A. + pub fn num_attributes(&self) -> usize { + self.num_attributes + } + + /// Returns the number of functional dependencies. + pub fn num_dependencies(&self) -> usize { + self.dependencies.len() + } + + /// Returns the number of attributes in the relation scheme R. + pub fn num_relation_attrs(&self) -> usize { + self.relation_attrs.len() + } + + /// Returns the number of known candidate keys. + pub fn num_known_keys(&self) -> usize { + self.known_keys.len() + } + + /// Returns the functional dependencies. + pub fn dependencies(&self) -> &[(Vec, Vec)] { + &self.dependencies + } + + /// Returns the relation scheme attributes. + pub fn relation_attrs(&self) -> &[usize] { + &self.relation_attrs + } + + /// Returns the known candidate keys. + pub fn known_keys(&self) -> &[Vec] { + &self.known_keys + } + + /// Compute the closure of a set of attributes under the functional dependencies. + fn compute_closure(&self, attrs: &[bool]) -> Vec { + let mut closure = attrs.to_vec(); + let mut changed = true; + while changed { + 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; + } + } + } + } + } + closure + } +} + +impl Problem for AdditionalKey { + const NAME: &'static str = "AdditionalKey"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.relation_attrs.len()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + // Check config length + if config.len() != self.relation_attrs.len() { + return false; + } + // Check all values are 0 or 1 + if config.iter().any(|&v| v >= 2) { + return false; + } + + // Build selected attribute set + let selected: Vec = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| self.relation_attrs[i]) + .collect(); + + // Empty selection is not a key + if selected.is_empty() { + return false; + } + + // Compute closure of selected attributes + let mut attr_set = vec![false; self.num_attributes]; + for &a in &selected { + attr_set[a] = true; + } + let closure = self.compute_closure(&attr_set); + + // Check closure covers all relation_attrs + if !self.relation_attrs.iter().all(|&a| closure[a]) { + return false; + } + + // Check minimality: removing any single selected attribute should break coverage + for &a in &selected { + let mut reduced = attr_set.clone(); + reduced[a] = false; + let reduced_closure = self.compute_closure(&reduced); + if self.relation_attrs.iter().all(|&ra| reduced_closure[ra]) { + return false; // Not minimal + } + } + + // Build sorted selected vec and check it's not in known_keys + let mut sorted_selected = selected; + sorted_selected.sort_unstable(); + !self.known_keys.contains(&sorted_selected) + } +} + +impl SatisfactionProblem for AdditionalKey {} + +crate::declare_variants! { + default sat AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "additional_key", + build: || { + let problem = AdditionalKey::new( + 6, + vec![ + (vec![0, 1], vec![2, 3]), + (vec![2, 3], vec![4, 5]), + (vec![4, 5], vec![0, 1]), + (vec![0, 2], vec![3]), + (vec![3, 5], vec![1]), + ], + vec![0, 1, 2, 3, 4, 5], + vec![vec![0, 1], vec![2, 3], vec![4, 5]], + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![1, 0, 1, 0, 0, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/additional_key.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index c4b125274..9052484c2 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -1,6 +1,7 @@ //! Miscellaneous problems. //! //! Problems with unique input structures that don't fit other categories: +//! - [`AdditionalKey`]: Determine whether a relational schema has an additional candidate key //! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`Factoring`]: Integer factorization //! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) @@ -12,6 +13,7 @@ //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value +pub(crate) mod additional_key; mod bin_packing; pub(crate) mod factoring; mod flow_shop_scheduling; @@ -23,6 +25,7 @@ mod sequencing_within_intervals; pub(crate) mod shortest_common_supersequence; mod subset_sum; +pub use additional_key::AdditionalKey; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use flow_shop_scheduling::FlowShopScheduling; @@ -42,5 +45,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec AdditionalKey { + AdditionalKey::new( + 6, + vec![ + (vec![0, 1], vec![2, 3]), + (vec![2, 3], vec![4, 5]), + (vec![4, 5], vec![0, 1]), + (vec![0, 2], vec![3]), + (vec![3, 5], vec![1]), + ], + vec![0, 1, 2, 3, 4, 5], + vec![vec![0, 1], vec![2, 3], vec![4, 5]], + ) +} + +/// Instance 2: 3 attributes, single FD {0}->{1,2}, known key [{0}]. +fn instance2() -> AdditionalKey { + AdditionalKey::new( + 3, + vec![(vec![0], vec![1, 2])], + vec![0, 1, 2], + vec![vec![0]], + ) +} + +#[test] +fn test_additional_key_creation() { + let problem = instance1(); + assert_eq!(problem.num_attributes(), 6); + assert_eq!(problem.num_dependencies(), 5); + assert_eq!(problem.num_relation_attrs(), 6); + assert_eq!(problem.num_known_keys(), 3); + assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!(::NAME, "AdditionalKey"); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_additional_key_evaluate_satisfying() { + let problem = instance1(); + // Config [1,0,1,0,0,0] selects attrs {0,2}. + // Closure of {0,2}: {0,2} -> apply (0,2)->3 => {0,2,3} -> apply (2,3)->(4,5) => {0,2,3,4,5} + // -> apply (4,5)->(0,1) => {0,1,2,3,4,5}. Covers all. + // Minimality: remove 0 => {2}, closure of {2} = {2} => does not cover all. OK. + // remove 2 => {0}, closure of {0} = {0} => does not cover all. OK. + // {0,2} sorted is [0,2], not in known_keys [{0,1},{2,3},{4,5}]. + assert!(problem.evaluate(&[1, 0, 1, 0, 0, 0])); +} + +#[test] +fn test_additional_key_evaluate_known_key() { + let problem = instance1(); + // Config [1,1,0,0,0,0] selects attrs {0,1} which IS in known_keys. + assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0])); +} + +#[test] +fn test_additional_key_evaluate_not_a_key() { + let problem = instance1(); + // Config [0,0,0,0,0,1] selects {5}. Closure of {5} = {5}. + // Does not cover all attrs. + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 1])); +} + +#[test] +fn test_additional_key_evaluate_non_minimal() { + let problem = instance1(); + // Config [1,1,1,0,0,0] selects {0,1,2}. + // {0,1} alone determines all attrs (known key), so {0,1,2} is NOT minimal. + assert!(!problem.evaluate(&[1, 1, 1, 0, 0, 0])); +} + +#[test] +fn test_additional_key_no_additional_key() { + let problem = instance2(); + // Only candidate key is {0}, which is already known. + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_additional_key_wrong_config_length() { + let problem = instance1(); + assert!(!problem.evaluate(&[1, 0])); + assert!(!problem.evaluate(&[1, 0, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_additional_key_invalid_variable_value() { + let problem = instance1(); + assert!(!problem.evaluate(&[2, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_additional_key_brute_force() { + let problem = instance1(); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_additional_key_brute_force_all() { + let problem = instance1(); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_additional_key_serialization() { + let problem = instance1(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: AdditionalKey = serde_json::from_value(json.clone()).unwrap(); + assert_eq!(restored.num_attributes(), problem.num_attributes()); + assert_eq!(restored.num_dependencies(), problem.num_dependencies()); + assert_eq!(restored.num_relation_attrs(), problem.num_relation_attrs()); + assert_eq!(restored.num_known_keys(), problem.num_known_keys()); + // Verify round-trip produces same evaluation + assert_eq!( + problem.evaluate(&[1, 0, 1, 0, 0, 0]), + restored.evaluate(&[1, 0, 1, 0, 0, 0]) + ); +} + +#[test] +fn test_additional_key_paper_example() { + let problem = instance1(); + // Verify {0,2} is a valid additional key + assert!(problem.evaluate(&[1, 0, 1, 0, 0, 0])); + // Count all satisfying solutions + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_additional_key_empty_selection() { + let problem = instance1(); + // All zeros = no attributes selected = not a key + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0])); +} From 7fd2866c49baec8fb05edd529811b90403a639fd Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:51:23 +0800 Subject: [PATCH 03/10] Apply rustfmt formatting fixes Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/commands/create.rs | 16 +++++----------- src/models/mod.rs | 6 +++--- src/unit_tests/models/misc/additional_key.rs | 7 +------ 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index f2532eb55..ceb68eb22 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -14,8 +14,8 @@ use problemreductions::models::graph::{ }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, FlowShopScheduling, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, + SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -942,19 +942,13 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "AdditionalKey" => { let usage = "Usage: pred create AdditionalKey --num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5\" --relation-attrs \"0,1,2,3,4,5\" --known-keys \"0,1;2,3\""; let num_attributes = args.num_attributes.ok_or_else(|| { - anyhow::anyhow!( - "AdditionalKey requires --num-attributes\n\n{usage}" - ) + anyhow::anyhow!("AdditionalKey requires --num-attributes\n\n{usage}") })?; let deps_str = args.dependencies.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "AdditionalKey requires --dependencies\n\n{usage}" - ) + anyhow::anyhow!("AdditionalKey requires --dependencies\n\n{usage}") })?; let ra_str = args.relation_attrs.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "AdditionalKey requires --relation-attrs\n\n{usage}" - ) + anyhow::anyhow!("AdditionalKey requires --relation-attrs\n\n{usage}") })?; let dependencies: Vec<(Vec, Vec)> = deps_str .split(';') diff --git a/src/models/mod.rs b/src/models/mod.rs index d3a1318a6..4e7aacb7b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -21,8 +21,8 @@ pub use graph::{ TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use misc::{ - AdditionalKey, BinPacking, Factoring, FlowShopScheduling, Knapsack, - LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, SubsetSum, + AdditionalKey, BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, + MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, + SubsetSum, }; pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; diff --git a/src/unit_tests/models/misc/additional_key.rs b/src/unit_tests/models/misc/additional_key.rs index d135bbbb0..25b59f800 100644 --- a/src/unit_tests/models/misc/additional_key.rs +++ b/src/unit_tests/models/misc/additional_key.rs @@ -20,12 +20,7 @@ fn instance1() -> AdditionalKey { /// Instance 2: 3 attributes, single FD {0}->{1,2}, known key [{0}]. fn instance2() -> AdditionalKey { - AdditionalKey::new( - 3, - vec![(vec![0], vec![1, 2])], - vec![0, 1, 2], - vec![vec![0]], - ) + AdditionalKey::new(3, vec![(vec![0], vec![1, 2])], vec![0, 1, 2], vec![vec![0]]) } #[test] From 61eec60cd0c5d6ce4053348493ff40c5a5435b68 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 01:57:51 +0800 Subject: [PATCH 04/10] Add AdditionalKey model (Batch 1) Implement the AdditionalKey satisfaction problem from relational database theory (Garey & Johnson A4 SR27). Includes model, CLI registration, unit tests, and example-db entry. --- src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 42bc6c927..1167e9b85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,9 +57,9 @@ pub mod prelude { UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ - BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, SubsetSum, + AdditionalKey, BinPacking, Factoring, FlowShopScheduling, Knapsack, + LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, + SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, From cbb53c1563df9da0c9e8195d6a31c6f8924bdb98 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 02:02:01 +0800 Subject: [PATCH 05/10] Add AdditionalKey problem definition to paper Add problem-def entry, display name, and bibliography reference (Beeri & Bernstein, 1979) for the Additional Key problem from relational database theory. Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 23 +++++++++++++++++++++++ docs/paper/references.bib | 11 +++++++++++ 2 files changed, 34 insertions(+) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 01372f7cb..4a8420ada 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -62,6 +62,7 @@ // Problem display names for theorem headers #let display-name = ( + "AdditionalKey": [Additional Key], "MaximumIndependentSet": [Maximum Independent Set], "MinimumVertexCover": [Minimum Vertex Cover], "MaxCut": [Max-Cut], @@ -2326,6 +2327,28 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ) ] +#problem-def("AdditionalKey")[ + Given a set $A$ of attribute names, a collection $F$ of functional dependencies on $A$, + a subset $R subset.eq A$, and a set $K$ of candidate keys for the relational scheme $chevron.l R, F chevron.r$, + determine whether there exists a subset $R' subset.eq R$ such that $R' in.not K$, + the closure $R'^+$ under $F$ equals $R$, and no proper subset of $R'$ also has this property. +][ + A classical NP-complete problem from relational database theory @beeri1979. + Enumerating all candidate keys is necessary to verify Boyce-Codd Normal Form (BCNF), + and the NP-completeness of Additional Key implies that BCNF testing is intractable in general. + The best known exact algorithm is brute-force enumeration of all $2^(|R|)$ subsets, + checking each for the key property via closure computation under Armstrong's axioms. + #footnote[No algorithm improving on brute-force is known for the Additional Key problem.] + + *Example.* Consider attribute set $A = {0, 1, 2, 3, 4, 5}$ with functional dependencies + $F = {{0,1} -> {2,3}, {2,3} -> {4,5}, {4,5} -> {0,1}, {0,2} -> {3}, {3,5} -> {1}}$, + relation $R = A$, and known keys $K = {{0,1}, {2,3}, {4,5}}$. + The subset ${0,2}$ is an additional key: starting from ${0,2}$, we apply ${0,2} -> {3}$ + to get ${0,2,3}$, then ${2,3} -> {4,5}$ to get ${0,2,3,4,5}$, then ${4,5} -> {0,1}$ + to reach $R^+ = A$. The set ${0,2}$ is minimal (neither ${0}$ nor ${2}$ alone determines $A$) + and ${0,2} in.not K$, so the answer is YES. +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 6482b48a2..c20920188 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -104,6 +104,17 @@ @article{lucas2014 year = {2014} } +@article{beeri1979, + author = {Catriel Beeri and Philip A. Bernstein}, + title = {Computational Problems Related to the Design of Normal Form Relational Schemas}, + journal = {ACM Transactions on Database Systems}, + volume = {4}, + number = {1}, + pages = {30--59}, + year = {1979}, + doi = {10.1145/320064.320066} +} + @article{barahona1982, author = {Francisco Barahona}, title = {On the computational complexity of Ising spin glass models}, From 262f635ea2a05f3ef4be8d7942b2166d0773cd0c Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 02:04:23 +0800 Subject: [PATCH 06/10] chore: remove plan file after implementation --- docs/plans/2026-03-17-additional-key.md | 244 ------------------------ 1 file changed, 244 deletions(-) delete mode 100644 docs/plans/2026-03-17-additional-key.md diff --git a/docs/plans/2026-03-17-additional-key.md b/docs/plans/2026-03-17-additional-key.md deleted file mode 100644 index ac8470ba0..000000000 --- a/docs/plans/2026-03-17-additional-key.md +++ /dev/null @@ -1,244 +0,0 @@ -# Plan: Add AdditionalKey Model (#445) - -## Overview - -Add the `AdditionalKey` satisfaction problem from relational database theory (Garey & Johnson A4 SR27). Given a set of attributes, functional dependencies, a relation scheme, and known keys, determine whether there exists a candidate key not in the known set. - -**Problem type:** Satisfaction (`Metric = bool`) -**Category:** `misc` (unique input structure — relational database theory) -**Type parameters:** None -**Associated rules:** R121 HittingSet → AdditionalKey (inbound only — this model will be an orphan until that rule is implemented) - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `AdditionalKey` | -| 2 | Mathematical definition | Given attribute set A, functional dependencies F on A, relation R ⊆ A, and known keys K for ⟨R,F⟩, determine if R has a candidate key not in K | -| 3 | Problem type | Satisfaction (decision) | -| 4 | Type parameters | None | -| 5 | Struct fields | `num_attributes: usize`, `dependencies: Vec<(Vec, Vec)>`, `relation_attrs: Vec`, `known_keys: Vec>` | -| 6 | Configuration space | `vec![2; relation_attrs.len()]` — binary selection over relation attributes | -| 7 | Feasibility check | Closure of selected attrs under F covers all relation_attrs, selected set is minimal (no proper subset also covers), and selected set is not in known_keys | -| 8 | Objective function | N/A (satisfaction: returns bool) | -| 9 | Best known exact algorithm | Brute-force enumeration: O(2^num_relation_attrs × num_dependencies × num_attributes) — Beeri & Bernstein (1979) | -| 10 | Solving strategy | BruteForce works directly | -| 11 | Category | `misc` | -| 12 | Expected outcome | Instance 1 (YES): attrs {0,2} is an additional key. Instance 2 (NO): only key {0} is already known | - -## Batch 1: Implementation (Steps 1–5.5) - -### Step 1: Create model file `src/models/misc/additional_key.rs` - -Follow `SubsetSum` as the reference satisfaction problem pattern. - -**Schema registration:** -```rust -inventory::submit! { - ProblemSchemaEntry { - name: "AdditionalKey", - display_name: "Additional Key", - aliases: &[], - dimensions: &[], - module_path: module_path!(), - description: "Determine whether a relational schema has a candidate key not in a given set", - fields: &[ - FieldInfo { name: "num_attributes", type_name: "usize", description: "Number of attributes in A" }, - FieldInfo { name: "dependencies", type_name: "Vec<(Vec, Vec)>", description: "Functional dependencies F; each (lhs, rhs)" }, - FieldInfo { name: "relation_attrs", type_name: "Vec", description: "Relation scheme attributes R ⊆ A" }, - FieldInfo { name: "known_keys", type_name: "Vec>", description: "Known candidate keys K" }, - ], - } -} -``` - -**Struct:** -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AdditionalKey { - num_attributes: usize, - dependencies: Vec<(Vec, Vec)>, - relation_attrs: Vec, - known_keys: Vec>, -} -``` - -**Constructor:** `new(num_attributes, dependencies, relation_attrs, known_keys)` with validation: -- All attribute indices < num_attributes -- relation_attrs elements are unique and sorted -- known_keys entries are sorted for consistent comparison - -**Size getters:** -- `num_attributes() -> usize` -- `num_dependencies() -> usize` (returns `dependencies.len()`) -- `num_relation_attrs() -> usize` (returns `relation_attrs.len()`) -- `num_known_keys() -> usize` (returns `known_keys.len()`) - -**Core algorithm — closure computation:** -```rust -fn compute_closure(&self, attrs: &HashSet) -> HashSet { - let mut closure = attrs.clone(); - let mut changed = true; - while changed { - changed = false; - for (lhs, rhs) in &self.dependencies { - if lhs.iter().all(|a| closure.contains(a)) { - for &a in rhs { - if closure.insert(a) { - changed = true; - } - } - } - } - } - closure -} -``` - -**Problem trait:** -```rust -impl Problem for AdditionalKey { - const NAME: &'static str = "AdditionalKey"; - type Metric = bool; - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![] - } - - fn dims(&self) -> Vec { - vec![2; self.relation_attrs.len()] - } - - fn evaluate(&self, config: &[usize]) -> bool { - // 1. Check config length and binary values - // 2. Build selected attribute set from config - // 3. Compute closure under FDs - // 4. Check closure covers all relation_attrs - // 5. Check minimality: no proper subset also has full closure - // 6. Check selected set is not in known_keys - } -} - -impl SatisfactionProblem for AdditionalKey {} -``` - -**Variant declaration:** -```rust -crate::declare_variants! { - default sat AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", -} -``` - -### Step 2: Register in module system - -1. **`src/models/misc/mod.rs`**: Add `mod additional_key;` and `pub use additional_key::AdditionalKey;` -2. **`src/models/mod.rs`**: Add `AdditionalKey` to the `misc` re-export line - -### Step 3: CLI registration - -1. **`problemreductions-cli/src/commands/create.rs`**: Add match arm for `"AdditionalKey"` that parses `--dependencies`, `--relation-attrs`, `--known-keys`, and `--num-attributes` flags. Since this problem has a unique schema, it needs custom argument parsing. - -2. **`problemreductions-cli/src/cli.rs`**: Add CLI flags if not already present: - - `--num-attributes` (usize) - - `--dependencies` (string, e.g., "0,1:2,3;2,3:4,5" for {0,1}→{2,3} and {2,3}→{4,5}) - - `--relation-attrs` (comma-separated usize list) - - `--known-keys` (string, e.g., "0,1;2,3" for [{0,1}, {2,3}]) - -3. **Help text**: Add entry to "Flags by problem type" table - -### Step 4: Add canonical model example - -In `src/models/misc/additional_key.rs`, add: -```rust -#[cfg(feature = "example-db")] -pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "additional_key", - build: || { - let problem = AdditionalKey::new( - 6, - vec![ - (vec![0, 1], vec![2, 3]), - (vec![2, 3], vec![4, 5]), - (vec![4, 5], vec![0, 1]), - (vec![0, 2], vec![3]), - (vec![3, 5], vec![1]), - ], - vec![0, 1, 2, 3, 4, 5], - vec![vec![0, 1], vec![2, 3], vec![4, 5]], - ); - // Config for key {0, 2}: [1, 0, 1, 0, 0, 0] - crate::example_db::specs::satisfaction_example( - problem, - vec![vec![1, 0, 1, 0, 0, 0]], - ) - }, - }] -} -``` - -Register in `src/models/misc/mod.rs` `canonical_model_example_specs()`. - -### Step 5: Write unit tests - -Create `src/unit_tests/models/misc/additional_key.rs` with: - -1. **`test_additional_key_creation`**: Construct instance, verify getters and dims -2. **`test_additional_key_evaluate_satisfying`**: Instance 1 — verify {0,2} is a valid additional key -3. **`test_additional_key_evaluate_unsatisfying`**: Instance 2 — verify no additional key exists -4. **`test_additional_key_evaluate_non_minimal`**: Verify a superset of a key returns false -5. **`test_additional_key_evaluate_known_key`**: Verify a known key returns false -6. **`test_additional_key_evaluate_not_a_key`**: Verify a non-key (closure ≠ R) returns false -7. **`test_additional_key_wrong_config_length`**: Wrong-length config returns false -8. **`test_additional_key_invalid_variable_value`**: Config with value ≥ 2 returns false -9. **`test_additional_key_brute_force`**: BruteForce finds a satisfying solution -10. **`test_additional_key_brute_force_all`**: All satisfying solutions are valid -11. **`test_additional_key_serialization`**: Round-trip serde test -12. **`test_additional_key_paper_example`**: Verify the paper example instance (same as canonical example) - -Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the model file. - -### Step 5.5: Add trait_consistency entry - -In `src/unit_tests/trait_consistency.rs`: -- Add `check_problem_trait(...)` call for `AdditionalKey` with the small Instance 2 (3 attributes) - -## Batch 2: Paper Entry (Step 6) - -### Step 6: Document in paper - -**File:** `docs/paper/reductions.typ` - -1. **Add display name:** -```typst -"AdditionalKey": [Additional Key], -``` - -2. **Add problem-def:** -```typst -#problem-def("AdditionalKey")[ - Given a set $A$ of attribute names, a collection $F$ of functional dependencies on $A$, - a subset $R subset.eq A$, and a set $K$ of candidate keys for the relational scheme $angle.l R, F angle.r$, - determine whether there exists a subset $R' subset.eq R$ such that $R' in.not K$, - $(R', R) in F^*$, and for no $R'' subset.neq R'$ is $(R'', R) in F^*$. -][ - A classical NP-complete problem from relational database theory @beeri1979. - The problem is central to database normalization: enumerating all candidate keys - is necessary to verify Boyce-Codd Normal Form (BCNF), and the NP-completeness - of Additional Key implies that BCNF testing is intractable in general. - The best known exact algorithm is brute-force enumeration of all $2^(|R|)$ subsets, - checking each for the key property via closure computation under Armstrong's axioms. - #footnote[No algorithm improving on brute-force is known for the Additional Key problem.] - - *Example.* Let $A = {0, 1, 2, 3, 4, 5}$ with functional dependencies - ${0,1} -> {2,3}$, ${2,3} -> {4,5}$, ${4,5} -> {0,1}$, - ${0,2} -> {3}$, ${3,5} -> {1}$, relation $R = A$, - and known keys $K = {{0,1}, {2,3}, {4,5}}$. - The subset ${0,2}$ is an additional key: its closure under $F$ reaches all of $A$ - via ${0,2} -> {3} -> {4,5} -> {0,1}$, it is minimal, and ${0,2} in.not K$. -] -``` - -3. **Add BibTeX entry** for Beeri & Bernstein (1979) if not already present. - -4. **Build and verify:** `make paper` From 6bb961c55bcb8450ef0921a76fb3b5753904730b Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 01:25:54 +0800 Subject: [PATCH 07/10] Fix AdditionalKey canonical example to use updated ModelExampleSpec API The merge brought in API changes to ModelExampleSpec (removed `build` closure, added direct `instance`/`optimal_config`/`optimal_value` fields). Update AdditionalKey's canonical_model_example_specs to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/models/misc/additional_key.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs index 248825173..8f0a60ea3 100644 --- a/src/models/misc/additional_key.rs +++ b/src/models/misc/additional_key.rs @@ -264,21 +264,20 @@ crate::declare_variants! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "additional_key", - build: || { - let problem = AdditionalKey::new( - 6, - vec![ - (vec![0, 1], vec![2, 3]), - (vec![2, 3], vec![4, 5]), - (vec![4, 5], vec![0, 1]), - (vec![0, 2], vec![3]), - (vec![3, 5], vec![1]), - ], - vec![0, 1, 2, 3, 4, 5], - vec![vec![0, 1], vec![2, 3], vec![4, 5]], - ); - crate::example_db::specs::satisfaction_example(problem, vec![vec![1, 0, 1, 0, 0, 0]]) - }, + instance: Box::new(AdditionalKey::new( + 6, + vec![ + (vec![0, 1], vec![2, 3]), + (vec![2, 3], vec![4, 5]), + (vec![4, 5], vec![0, 1]), + (vec![0, 2], vec![3]), + (vec![3, 5], vec![1]), + ], + vec![0, 1, 2, 3, 4, 5], + vec![vec![0, 1], vec![2, 3], vec![4, 5]], + )), + optimal_config: vec![1, 0, 1, 0, 0, 0], + optimal_value: serde_json::json!(true), }] } From 533af69f723a89490af6fe0dcafd7cfe05313598 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 01:43:53 +0800 Subject: [PATCH 08/10] Address review findings: panic tests, pinned solution count, help text - Add 5 #[should_panic] tests for constructor validation paths - Pin brute-force solution count to 2 (additional keys: {0,2} and {0,3,5}) - Fix --known-keys help text to use [brackets] for optional flag - Remove redundant test_additional_key_paper_example Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/cli.rs | 2 +- src/unit_tests/models/misc/additional_key.rs | 47 +++++++++++++------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index ff5791837..2c8dc9cb9 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -257,7 +257,7 @@ Flags by problem type: OptimalLinearArrangement --graph, --bound RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices] - AdditionalKey --num-attributes, --dependencies, --relation-attrs, --known-keys + AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys] SubgraphIsomorphism --graph (host), --pattern (pattern) LCS --strings, --bound [--alphabet-size] FAS --arcs [--weights] [--num-vertices] diff --git a/src/unit_tests/models/misc/additional_key.rs b/src/unit_tests/models/misc/additional_key.rs index 25b59f800..7b39854fb 100644 --- a/src/unit_tests/models/misc/additional_key.rs +++ b/src/unit_tests/models/misc/additional_key.rs @@ -107,7 +107,8 @@ fn test_additional_key_brute_force_all() { let problem = instance1(); let solver = BruteForce::new(); let solutions = solver.find_all_satisfying(&problem); - assert!(!solutions.is_empty()); + // Exactly 2 additional keys: {0,2} and {0,3,5} + assert_eq!(solutions.len(), 2); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -129,23 +130,39 @@ fn test_additional_key_serialization() { ); } -#[test] -fn test_additional_key_paper_example() { - let problem = instance1(); - // Verify {0,2} is a valid additional key - assert!(problem.evaluate(&[1, 0, 1, 0, 0, 0])); - // Count all satisfying solutions - let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); - assert!(!solutions.is_empty()); - for sol in &solutions { - assert!(problem.evaluate(sol)); - } -} - #[test] fn test_additional_key_empty_selection() { let problem = instance1(); // All zeros = no attributes selected = not a key assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0])); } + +#[test] +#[should_panic(expected = "relation_attrs element")] +fn test_additional_key_panic_relation_attrs_out_of_bounds() { + AdditionalKey::new(3, vec![], vec![0, 1, 5], vec![]); +} + +#[test] +#[should_panic(expected = "relation_attrs contains duplicates")] +fn test_additional_key_panic_relation_attrs_duplicates() { + AdditionalKey::new(3, vec![], vec![0, 1, 1], vec![]); +} + +#[test] +#[should_panic(expected = "dependency lhs attribute")] +fn test_additional_key_panic_dependency_lhs_out_of_bounds() { + AdditionalKey::new(3, vec![(vec![5], vec![0])], vec![0, 1, 2], vec![]); +} + +#[test] +#[should_panic(expected = "dependency rhs attribute")] +fn test_additional_key_panic_dependency_rhs_out_of_bounds() { + AdditionalKey::new(3, vec![(vec![0], vec![5])], vec![0, 1, 2], vec![]); +} + +#[test] +#[should_panic(expected = "known_keys attribute")] +fn test_additional_key_panic_known_keys_out_of_bounds() { + AdditionalKey::new(3, vec![], vec![0, 1, 2], vec![vec![5]]); +} From 76e1efd89e1975c22c0a57b5a82e7f2d937f06c5 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 01:45:28 +0800 Subject: [PATCH 09/10] Fix merge artifacts: fmt, missing fields in test helper Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 10 ++++++---- src/models/mod.rs | 15 +++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 63484e3d8..287237d7c 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -14,10 +14,10 @@ use problemreductions::models::graph::{ MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ - AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, - ConjunctiveBooleanQuery, FlowShopScheduling, LongestCommonSubsequence, - MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, - QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, + AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, + FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, + MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, @@ -4489,6 +4489,8 @@ mod tests { domain_size: None, relations: None, conjuncts_spec: None, + relation_attrs: None, + known_keys: None, costs: None, cut_bound: None, size_bound: None, diff --git a/src/models/mod.rs b/src/models/mod.rs index a1f3b080c..65352833c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -27,14 +27,13 @@ pub use graph::{ }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ - AdditionalKey, BinPacking, CbqRelation, ConjunctiveBooleanQuery, - ConjunctiveQueryFoldability, Factoring, FlowShopScheduling, Knapsack, - LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, - Partition, PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, - ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, Term, + AdditionalKey, BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, + Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, + MultiprocessorScheduling, PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, + SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines, + SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, }; pub use set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, From 8ca70f036fc3f2012869f28d600fb2fe37a8c1d2 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 01:53:10 +0800 Subject: [PATCH 10/10] Cover data getter methods in creation test Co-Authored-By: Claude Opus 4.6 (1M context) --- src/unit_tests/models/misc/additional_key.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/unit_tests/models/misc/additional_key.rs b/src/unit_tests/models/misc/additional_key.rs index 7b39854fb..da348277e 100644 --- a/src/unit_tests/models/misc/additional_key.rs +++ b/src/unit_tests/models/misc/additional_key.rs @@ -33,6 +33,12 @@ fn test_additional_key_creation() { assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2, 2]); assert_eq!(::NAME, "AdditionalKey"); assert_eq!(::variant(), vec![]); + // Data getters + assert_eq!(problem.dependencies().len(), 5); + assert_eq!(problem.dependencies()[0], (vec![0, 1], vec![2, 3])); + assert_eq!(problem.relation_attrs(), &[0, 1, 2, 3, 4, 5]); + assert_eq!(problem.known_keys().len(), 3); + assert_eq!(problem.known_keys()[0], vec![0, 1]); } #[test]