Skip to content

Commit 90fcc85

Browse files
GiggleLiuclaude
andauthored
feat: add 11 medium-confidence reduction rules (#1010)
* docs: add schema-driven create.rs refactor design spec Design for replacing the 11K-line create.rs with a schema-driven generic dispatch using existing registry factory functions. Targets ~73% line reduction by eliminating the 5,400-line match statement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * update skills * feat: add minimumvertexcover to lcs reduction * Add ThreeDimensionalMatching to ThreePartition reduction * Add NAE-SAT to Partition Into Perfect Matchings reduction * feat: add SetSplitting to Betweenness reduction * Add 3SAT to QuadraticCongruences reduction * Add 3SAT to preemptive scheduling reduction * Add SAT to integral flow homologous arcs reduction * style: rustfmt fixes for NAE-SAT reduction tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add KSatisfiability to TimetableDesign reduction * Add KSatisfiability to CyclicOrdering reduction * feat: add KSatisfiability to OneInThreeSatisfiability reduction (#862) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add 3sat to acyclic partition reduction * Add direct ThreeDimensionalMatching to ILP reduction * fix review --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 22c4b05 commit 90fcc85

36 files changed

Lines changed: 6551 additions & 97 deletions

.claude/CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
## Project Overview
44
Rust library for NP-hard problem reductions. Implements computational problems with reduction rules for transforming between equivalent formulations.
55

6+
## Agent Preferences
7+
- For tasks with complex logic (implementing reductions, fixing CI, issue-to-pr, check-issue, etc.), prefer invoking the `codex` CLI (if installed) with the latest model: `codex exec -c model="gpt-5.4" "<prompt>"`.
8+
69
## Skills
710
These repo-local skills live under `.claude/skills/*/SKILL.md`.
811

.claude/skills/add-rule/SKILL.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule fil
121121

122122
Add a builder function in `src/example_db/rule_builders.rs` that constructs a small, canonical instance for this reduction. Follow the existing patterns in that file. Register the builder in `build_rule_examples()`.
123123

124-
## Step 5: Document in paper
124+
## Step 5: Document in paper (MANDATORY — DO NOT SKIP)
125+
126+
**This step is NOT optional.** Every reduction rule MUST have a corresponding `reduction-rule` entry in the paper. Skipping documentation is a blocking error — the PR will be rejected in review. Do not proceed to Step 6 until the paper entry is written and `make paper` compiles.
125127

126128
Write a `reduction-rule` entry in `docs/paper/reductions.typ`. **Reference example:** search for `reduction-rule("KColoring", "QUBO"` to see the gold-standard entry — use it as a template. For a minimal example, see MinimumVertexCover -> MaximumIndependentSet.
127129

@@ -224,5 +226,6 @@ Aggregate-only reductions currently have a narrower CLI surface:
224226
| Missing `extract_solution` mapping state | Store any index maps needed in the ReductionResult struct |
225227
| Not adding canonical example to `example_db` | Add builder in `src/example_db/rule_builders.rs` |
226228
| Not regenerating reduction graph | Run `cargo run --example export_graph` after adding a rule |
229+
| Skipping Step 5 (paper documentation) | **Every rule MUST have a `reduction-rule` entry in the paper. This is mandatory, not optional. PRs without documentation will be rejected.** |
227230
| Source/target model not fully registered | Both problems must already have `declare_variants!`, aliases as needed, and CLI create support -- use `add-model` skill first |
228231
| Treating a direct-to-ILP rule as a toy stub | Direct ILP reductions need exact overhead metadata and strong semantic regression tests, just like other production ILP rules |

docs/paper/reductions.typ

Lines changed: 590 additions & 6 deletions
Large diffs are not rendered by default.

docs/paper/references.bib

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,17 @@ @book{garey1979
242242
year = {1979}
243243
}
244244

245+
@article{galilMegiddo1977,
246+
author = {Zvi Galil and Nimrod Megiddo},
247+
title = {Cyclic Ordering is NP-Complete},
248+
journal = {Theoretical Computer Science},
249+
volume = {5},
250+
number = {2},
251+
pages = {179--182},
252+
year = {1977},
253+
doi = {10.1016/0304-3975(77)90005-1}
254+
}
255+
245256
@article{florianLenstraRinnooyKan1980,
246257
author = {M. Florian and J. K. Lenstra and A. H. G. Rinnooy Kan},
247258
title = {Deterministic Production Planning: Algorithms and Complexity},

problemreductions-cli/src/cli.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -843,13 +843,13 @@ pub struct CreateArgs {
843843
pub assignment: Option<String>,
844844
/// Coefficient/parameter a for QuadraticCongruences (residue target) or QuadraticDiophantineEquations (coefficient of x²)
845845
#[arg(long)]
846-
pub coeff_a: Option<u64>,
846+
pub coeff_a: Option<String>,
847847
/// Coefficient/parameter b for QuadraticCongruences (modulus) or QuadraticDiophantineEquations (coefficient of y)
848848
#[arg(long)]
849-
pub coeff_b: Option<u64>,
849+
pub coeff_b: Option<String>,
850850
/// Constant c for QuadraticCongruences (search-space bound) or QuadraticDiophantineEquations (right-hand side of ax² + by = c)
851851
#[arg(long)]
852-
pub coeff_c: Option<u64>,
852+
pub coeff_c: Option<String>,
853853
/// Incongruence pairs for SimultaneousIncongruences (semicolon-separated "a,b" pairs, e.g., "2,2;1,3;2,5;3,7")
854854
#[arg(long)]
855855
pub pairs: Option<String>,
@@ -1095,9 +1095,9 @@ impl CreateArgs {
10951095
insert!("expression", self.expression.as_deref());
10961096
insert!("equations", self.equations.as_deref());
10971097
insert!("assignment", self.assignment.as_deref());
1098-
insert!("coeff-a", self.coeff_a);
1099-
insert!("coeff-b", self.coeff_b);
1100-
insert!("coeff-c", self.coeff_c);
1098+
insert!("coeff-a", self.coeff_a.as_deref());
1099+
insert!("coeff-b", self.coeff_b.as_deref());
1100+
insert!("coeff-c", self.coeff_c.as_deref());
11011101
insert!("pairs", self.pairs.as_deref());
11021102
insert!("w-sizes", self.w_sizes.as_deref());
11031103
insert!("x-sizes", self.x_sizes.as_deref());

src/models/algebraic/quadratic_congruences.rs

Lines changed: 160 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
//! Quadratic Congruences problem implementation.
22
//!
3-
//! Given non-negative integers a, b, c with b > 0 and a < b, determine whether
4-
//! there exists a positive integer x with 1 ≤ x < c such that x² ≡ a (mod b).
3+
//! Given non-negative integers `a`, `b`, and `c` with `b > 0` and `a < b`,
4+
//! determine whether there exists a positive integer `x < c` such that
5+
//! `x² ≡ a (mod b)`.
6+
//!
7+
//! The witness integer `x` is encoded as a little-endian binary vector so the
8+
//! model can represent arbitrarily large instances while still fitting the
9+
//! crate's `Vec<usize>` configuration interface.
510
611
use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
712
use crate::traits::Problem;
813
use crate::types::Or;
14+
use num_bigint::{BigUint, ToBigUint};
15+
use num_traits::{One, Zero};
916
use serde::de::Error as _;
1017
use serde::{Deserialize, Deserializer, Serialize};
1118

@@ -18,56 +25,57 @@ inventory::submit! {
1825
module_path: module_path!(),
1926
description: "Decide whether x² ≡ a (mod b) has a solution for x in {1, ..., c-1}",
2027
fields: &[
21-
FieldInfo { name: "a", type_name: "u64", description: "a" },
22-
FieldInfo { name: "b", type_name: "u64", description: "b" },
23-
FieldInfo { name: "c", type_name: "u64", description: "c" },
28+
FieldInfo { name: "a", type_name: "BigUint", description: "a" },
29+
FieldInfo { name: "b", type_name: "BigUint", description: "b" },
30+
FieldInfo { name: "c", type_name: "BigUint", description: "c" },
2431
],
2532
}
2633
}
2734

2835
inventory::submit! {
2936
ProblemSizeFieldEntry {
3037
name: "QuadraticCongruences",
31-
fields: &["c"],
38+
fields: &["bit_length_a", "bit_length_b", "bit_length_c"],
3239
}
3340
}
3441

3542
/// Quadratic Congruences problem.
3643
///
37-
/// Given non-negative integers a, b, c with b > 0 and a < b, determine whether
38-
/// there exists a positive integer x with 1 ≤ x < c such that x² ≡ a (mod b).
39-
///
40-
/// The search space is x ∈ {1, …, c−1}. The configuration variable `config[0]`
41-
/// encodes x as `x = config[0] + 1`.
42-
///
43-
/// # Example
44+
/// Given non-negative integers `a`, `b`, `c` with `b > 0` and `a < b`,
45+
/// determine whether there exists a positive integer `x < c` such that
46+
/// `x² ≡ a (mod b)`.
4447
///
45-
/// ```
46-
/// use problemreductions::models::algebraic::QuadraticCongruences;
47-
/// use problemreductions::{Problem, Solver, BruteForce};
48-
///
49-
/// // a=4, b=15, c=10: x=2 → 4 mod 15 = 4 ✓
50-
/// let problem = QuadraticCongruences::new(4, 15, 10);
51-
/// let solver = BruteForce::new();
52-
/// let witness = solver.find_witness(&problem);
53-
/// assert!(witness.is_some());
54-
/// ```
48+
/// The configuration encodes `x` in little-endian binary:
49+
/// `config[i] ∈ {0,1}` is the coefficient of `2^i`.
5550
#[derive(Debug, Clone, Serialize)]
5651
pub struct QuadraticCongruences {
5752
/// Quadratic residue target.
58-
a: u64,
53+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
54+
a: BigUint,
5955
/// Modulus.
60-
b: u64,
61-
/// Search-space bound; x ranges over {1, ..., c-1}.
62-
c: u64,
56+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
57+
b: BigUint,
58+
/// Search-space bound; feasible witnesses satisfy `1 <= x < c`.
59+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
60+
c: BigUint,
61+
}
62+
63+
fn bit_length(value: &BigUint) -> usize {
64+
if value.is_zero() {
65+
0
66+
} else {
67+
let bytes = value.to_bytes_be();
68+
let msb = *bytes.first().expect("nonzero BigUint has bytes");
69+
8 * (bytes.len() - 1) + (8 - msb.leading_zeros() as usize)
70+
}
6371
}
6472

6573
impl QuadraticCongruences {
66-
fn validate_inputs(a: u64, b: u64, c: u64) -> Result<(), String> {
67-
if b == 0 {
74+
fn validate_inputs(a: &BigUint, b: &BigUint, c: &BigUint) -> Result<(), String> {
75+
if b.is_zero() {
6876
return Err("Modulus b must be positive".to_string());
6977
}
70-
if c == 0 {
78+
if c.is_zero() {
7179
return Err("Bound c must be positive".to_string());
7280
}
7381
if a >= b {
@@ -78,8 +86,22 @@ impl QuadraticCongruences {
7886

7987
/// Create a new QuadraticCongruences instance, returning an error instead of
8088
/// panicking when the inputs are invalid.
81-
pub fn try_new(a: u64, b: u64, c: u64) -> Result<Self, String> {
82-
Self::validate_inputs(a, b, c)?;
89+
pub fn try_new<A, B, C>(a: A, b: B, c: C) -> Result<Self, String>
90+
where
91+
A: ToBigUint,
92+
B: ToBigUint,
93+
C: ToBigUint,
94+
{
95+
let a = a
96+
.to_biguint()
97+
.ok_or_else(|| "Residue a must be nonnegative".to_string())?;
98+
let b = b
99+
.to_biguint()
100+
.ok_or_else(|| "Modulus b must be nonnegative".to_string())?;
101+
let c = c
102+
.to_biguint()
103+
.ok_or_else(|| "Bound c must be nonnegative".to_string())?;
104+
Self::validate_inputs(&a, &b, &c)?;
83105
Ok(Self { a, b, c })
84106
}
85107

@@ -88,31 +110,105 @@ impl QuadraticCongruences {
88110
/// # Panics
89111
///
90112
/// Panics if `b == 0`, `c == 0`, or `a >= b`.
91-
pub fn new(a: u64, b: u64, c: u64) -> Self {
113+
pub fn new<A, B, C>(a: A, b: B, c: C) -> Self
114+
where
115+
A: ToBigUint,
116+
B: ToBigUint,
117+
C: ToBigUint,
118+
{
92119
Self::try_new(a, b, c).unwrap_or_else(|msg| panic!("{msg}"))
93120
}
94121

95-
/// Get the quadratic residue target a.
96-
pub fn a(&self) -> u64 {
97-
self.a
122+
/// Get the quadratic residue target `a`.
123+
pub fn a(&self) -> &BigUint {
124+
&self.a
125+
}
126+
127+
/// Get the modulus `b`.
128+
pub fn b(&self) -> &BigUint {
129+
&self.b
130+
}
131+
132+
/// Get the search-space bound `c`.
133+
pub fn c(&self) -> &BigUint {
134+
&self.c
98135
}
99136

100-
/// Get the modulus b.
101-
pub fn b(&self) -> u64 {
102-
self.b
137+
/// Number of bits needed to encode the residue target.
138+
pub fn bit_length_a(&self) -> usize {
139+
bit_length(&self.a)
103140
}
104141

105-
/// Get the search-space bound c.
106-
pub fn c(&self) -> u64 {
107-
self.c
142+
/// Number of bits needed to encode the modulus.
143+
pub fn bit_length_b(&self) -> usize {
144+
bit_length(&self.b)
145+
}
146+
147+
/// Number of bits needed to encode the search bound.
148+
pub fn bit_length_c(&self) -> usize {
149+
bit_length(&self.c)
150+
}
151+
152+
fn witness_bit_length(&self) -> usize {
153+
if self.c <= BigUint::one() {
154+
0
155+
} else {
156+
bit_length(&(&self.c - BigUint::one()))
157+
}
158+
}
159+
160+
/// Encode a witness integer `x` as a little-endian binary configuration.
161+
pub fn encode_witness(&self, x: &BigUint) -> Option<Vec<usize>> {
162+
if x.is_zero() || x >= &self.c {
163+
return None;
164+
}
165+
166+
let num_bits = self.witness_bit_length();
167+
let mut remaining = x.clone();
168+
let mut config = Vec::with_capacity(num_bits);
169+
170+
for _ in 0..num_bits {
171+
config.push(if (&remaining & BigUint::one()).is_zero() {
172+
0
173+
} else {
174+
1
175+
});
176+
remaining >>= 1usize;
177+
}
178+
179+
if remaining.is_zero() {
180+
Some(config)
181+
} else {
182+
None
183+
}
184+
}
185+
186+
/// Decode a little-endian binary configuration into its witness integer `x`.
187+
pub fn decode_witness(&self, config: &[usize]) -> Option<BigUint> {
188+
if config.len() != self.witness_bit_length() || config.iter().any(|&digit| digit > 1) {
189+
return None;
190+
}
191+
192+
let mut value = BigUint::zero();
193+
let mut weight = BigUint::one();
194+
for &digit in config {
195+
if digit == 1 {
196+
value += &weight;
197+
}
198+
weight <<= 1usize;
199+
}
200+
Some(value)
108201
}
109202
}
110203

111204
#[derive(Deserialize)]
112205
struct QuadraticCongruencesData {
113-
a: u64,
114-
b: u64,
115-
c: u64,
206+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
207+
a: BigUint,
208+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
209+
b: BigUint,
210+
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
211+
c: BigUint,
116212
}
117213

118214
impl<'de> Deserialize<'de> for QuadraticCongruences {
@@ -134,38 +230,43 @@ impl Problem for QuadraticCongruences {
134230
}
135231

136232
fn dims(&self) -> Vec<usize> {
137-
if self.c <= 1 {
138-
// No x in {1, ..., c-1} exists.
139-
return vec![];
233+
let num_bits = self.witness_bit_length();
234+
if num_bits == 0 {
235+
Vec::new()
236+
} else {
237+
vec![2; num_bits]
140238
}
141-
// config[0] ∈ {0, ..., c-2} maps to x = config[0] + 1 ∈ {1, ..., c-1}.
142-
vec![self.c as usize - 1]
143239
}
144240

145241
fn evaluate(&self, config: &[usize]) -> Or {
146-
if self.c <= 1 {
242+
let Some(x) = self.decode_witness(config) else {
147243
return Or(false);
148-
}
149-
if config.len() != 1 {
244+
};
245+
246+
if x.is_zero() || x >= *self.c() {
150247
return Or(false);
151248
}
152-
let x = (config[0] as u64) + 1; // 1-indexed
153-
let satisfies = ((x as u128) * (x as u128)) % (self.b as u128) == (self.a as u128);
249+
250+
let satisfies = (&x * &x) % self.b() == self.a().clone();
154251
Or(satisfies)
155252
}
156253
}
157254

158255
crate::declare_variants! {
159-
default QuadraticCongruences => "c",
256+
default QuadraticCongruences => "2^bit_length_c",
160257
}
161258

162259
#[cfg(feature = "example-db")]
163260
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
261+
let instance = QuadraticCongruences::new(4u32, 15u32, 10u32);
262+
let optimal_config = instance
263+
.encode_witness(&BigUint::from(2u32))
264+
.expect("x=2 should be a valid canonical witness");
265+
164266
vec![crate::example_db::specs::ModelExampleSpec {
165267
id: "quadratic_congruences",
166-
instance: Box::new(QuadraticCongruences::new(4, 15, 10)),
167-
// x=2 (config[0]=1): 2²=4 ≡ 4 (mod 15) ✓
168-
optimal_config: vec![1],
268+
instance: Box::new(instance),
269+
optimal_config,
169270
optimal_value: serde_json::json!(true),
170271
}]
171272
}

0 commit comments

Comments
 (0)