Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## Project Overview
Rust library for NP-hard problem reductions. Implements computational problems with reduction rules for transforming between equivalent formulations.

## Agent Preferences
- 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>"`.

## Skills
These repo-local skills live under `.claude/skills/*/SKILL.md`.

Expand Down
5 changes: 4 additions & 1 deletion .claude/skills/add-rule/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule fil

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()`.

## Step 5: Document in paper
## Step 5: Document in paper (MANDATORY — DO NOT SKIP)

**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.

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.

Expand Down Expand Up @@ -224,5 +226,6 @@ Aggregate-only reductions currently have a narrower CLI surface:
| Missing `extract_solution` mapping state | Store any index maps needed in the ReductionResult struct |
| Not adding canonical example to `example_db` | Add builder in `src/example_db/rule_builders.rs` |
| Not regenerating reduction graph | Run `cargo run --example export_graph` after adding a rule |
| 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.** |
| 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 |
| 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 |
596 changes: 590 additions & 6 deletions docs/paper/reductions.typ

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,17 @@ @book{garey1979
year = {1979}
}

@article{galilMegiddo1977,
author = {Zvi Galil and Nimrod Megiddo},
title = {Cyclic Ordering is NP-Complete},
journal = {Theoretical Computer Science},
volume = {5},
number = {2},
pages = {179--182},
year = {1977},
doi = {10.1016/0304-3975(77)90005-1}
}

@article{florianLenstraRinnooyKan1980,
author = {M. Florian and J. K. Lenstra and A. H. G. Rinnooy Kan},
title = {Deterministic Production Planning: Algorithms and Complexity},
Expand Down
12 changes: 6 additions & 6 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -843,13 +843,13 @@ pub struct CreateArgs {
pub assignment: Option<String>,
/// Coefficient/parameter a for QuadraticCongruences (residue target) or QuadraticDiophantineEquations (coefficient of x²)
#[arg(long)]
pub coeff_a: Option<u64>,
pub coeff_a: Option<String>,
/// Coefficient/parameter b for QuadraticCongruences (modulus) or QuadraticDiophantineEquations (coefficient of y)
#[arg(long)]
pub coeff_b: Option<u64>,
pub coeff_b: Option<String>,
/// Constant c for QuadraticCongruences (search-space bound) or QuadraticDiophantineEquations (right-hand side of ax² + by = c)
#[arg(long)]
pub coeff_c: Option<u64>,
pub coeff_c: Option<String>,
/// Incongruence pairs for SimultaneousIncongruences (semicolon-separated "a,b" pairs, e.g., "2,2;1,3;2,5;3,7")
#[arg(long)]
pub pairs: Option<String>,
Expand Down Expand Up @@ -1095,9 +1095,9 @@ impl CreateArgs {
insert!("expression", self.expression.as_deref());
insert!("equations", self.equations.as_deref());
insert!("assignment", self.assignment.as_deref());
insert!("coeff-a", self.coeff_a);
insert!("coeff-b", self.coeff_b);
insert!("coeff-c", self.coeff_c);
insert!("coeff-a", self.coeff_a.as_deref());
insert!("coeff-b", self.coeff_b.as_deref());
insert!("coeff-c", self.coeff_c.as_deref());
insert!("pairs", self.pairs.as_deref());
insert!("w-sizes", self.w_sizes.as_deref());
insert!("x-sizes", self.x_sizes.as_deref());
Expand Down
219 changes: 160 additions & 59 deletions src/models/algebraic/quadratic_congruences.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
//! Quadratic Congruences problem implementation.
//!
//! Given non-negative integers a, b, c with b > 0 and a < b, determine whether
//! there exists a positive integer x with 1 ≤ x < c such that x² ≡ a (mod b).
//! Given non-negative integers `a`, `b`, and `c` with `b > 0` and `a < b`,
//! determine whether there exists a positive integer `x < c` such that
//! `x² ≡ a (mod b)`.
//!
//! The witness integer `x` is encoded as a little-endian binary vector so the
//! model can represent arbitrarily large instances while still fitting the
//! crate's `Vec<usize>` configuration interface.

use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
use crate::traits::Problem;
use crate::types::Or;
use num_bigint::{BigUint, ToBigUint};
use num_traits::{One, Zero};
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize};

Expand All @@ -18,56 +25,57 @@ inventory::submit! {
module_path: module_path!(),
description: "Decide whether x² ≡ a (mod b) has a solution for x in {1, ..., c-1}",
fields: &[
FieldInfo { name: "a", type_name: "u64", description: "a" },
FieldInfo { name: "b", type_name: "u64", description: "b" },
FieldInfo { name: "c", type_name: "u64", description: "c" },
FieldInfo { name: "a", type_name: "BigUint", description: "a" },
FieldInfo { name: "b", type_name: "BigUint", description: "b" },
FieldInfo { name: "c", type_name: "BigUint", description: "c" },
],
}
}

inventory::submit! {
ProblemSizeFieldEntry {
name: "QuadraticCongruences",
fields: &["c"],
fields: &["bit_length_a", "bit_length_b", "bit_length_c"],
}
}

/// Quadratic Congruences problem.
///
/// Given non-negative integers a, b, c with b > 0 and a < b, determine whether
/// there exists a positive integer x with 1 ≤ x < c such that x² ≡ a (mod b).
///
/// The search space is x ∈ {1, …, c−1}. The configuration variable `config[0]`
/// encodes x as `x = config[0] + 1`.
///
/// # Example
/// Given non-negative integers `a`, `b`, `c` with `b > 0` and `a < b`,
/// determine whether there exists a positive integer `x < c` such that
/// `x² ≡ a (mod b)`.
///
/// ```
/// use problemreductions::models::algebraic::QuadraticCongruences;
/// use problemreductions::{Problem, Solver, BruteForce};
///
/// // a=4, b=15, c=10: x=2 → 4 mod 15 = 4 ✓
/// let problem = QuadraticCongruences::new(4, 15, 10);
/// let solver = BruteForce::new();
/// let witness = solver.find_witness(&problem);
/// assert!(witness.is_some());
/// ```
/// The configuration encodes `x` in little-endian binary:
/// `config[i] ∈ {0,1}` is the coefficient of `2^i`.
#[derive(Debug, Clone, Serialize)]
pub struct QuadraticCongruences {
/// Quadratic residue target.
a: u64,
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
a: BigUint,
/// Modulus.
b: u64,
/// Search-space bound; x ranges over {1, ..., c-1}.
c: u64,
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
b: BigUint,
/// Search-space bound; feasible witnesses satisfy `1 <= x < c`.
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
c: BigUint,
}

fn bit_length(value: &BigUint) -> usize {
if value.is_zero() {
0
} else {
let bytes = value.to_bytes_be();
let msb = *bytes.first().expect("nonzero BigUint has bytes");
8 * (bytes.len() - 1) + (8 - msb.leading_zeros() as usize)
}
}

impl QuadraticCongruences {
fn validate_inputs(a: u64, b: u64, c: u64) -> Result<(), String> {
if b == 0 {
fn validate_inputs(a: &BigUint, b: &BigUint, c: &BigUint) -> Result<(), String> {
if b.is_zero() {
return Err("Modulus b must be positive".to_string());
}
if c == 0 {
if c.is_zero() {
return Err("Bound c must be positive".to_string());
}
if a >= b {
Expand All @@ -78,8 +86,22 @@ impl QuadraticCongruences {

/// Create a new QuadraticCongruences instance, returning an error instead of
/// panicking when the inputs are invalid.
pub fn try_new(a: u64, b: u64, c: u64) -> Result<Self, String> {
Self::validate_inputs(a, b, c)?;
pub fn try_new<A, B, C>(a: A, b: B, c: C) -> Result<Self, String>
where
A: ToBigUint,
B: ToBigUint,
C: ToBigUint,
{
let a = a
.to_biguint()
.ok_or_else(|| "Residue a must be nonnegative".to_string())?;
let b = b
.to_biguint()
.ok_or_else(|| "Modulus b must be nonnegative".to_string())?;
let c = c
.to_biguint()
.ok_or_else(|| "Bound c must be nonnegative".to_string())?;
Self::validate_inputs(&a, &b, &c)?;
Ok(Self { a, b, c })
}

Expand All @@ -88,31 +110,105 @@ impl QuadraticCongruences {
/// # Panics
///
/// Panics if `b == 0`, `c == 0`, or `a >= b`.
pub fn new(a: u64, b: u64, c: u64) -> Self {
pub fn new<A, B, C>(a: A, b: B, c: C) -> Self
where
A: ToBigUint,
B: ToBigUint,
C: ToBigUint,
{
Self::try_new(a, b, c).unwrap_or_else(|msg| panic!("{msg}"))
}

/// Get the quadratic residue target a.
pub fn a(&self) -> u64 {
self.a
/// Get the quadratic residue target `a`.
pub fn a(&self) -> &BigUint {
&self.a
}

/// Get the modulus `b`.
pub fn b(&self) -> &BigUint {
&self.b
}

/// Get the search-space bound `c`.
pub fn c(&self) -> &BigUint {
&self.c
}

/// Get the modulus b.
pub fn b(&self) -> u64 {
self.b
/// Number of bits needed to encode the residue target.
pub fn bit_length_a(&self) -> usize {
bit_length(&self.a)
}

/// Get the search-space bound c.
pub fn c(&self) -> u64 {
self.c
/// Number of bits needed to encode the modulus.
pub fn bit_length_b(&self) -> usize {
bit_length(&self.b)
}

/// Number of bits needed to encode the search bound.
pub fn bit_length_c(&self) -> usize {
bit_length(&self.c)
}

fn witness_bit_length(&self) -> usize {
if self.c <= BigUint::one() {
0
} else {
bit_length(&(&self.c - BigUint::one()))
}
}

/// Encode a witness integer `x` as a little-endian binary configuration.
pub fn encode_witness(&self, x: &BigUint) -> Option<Vec<usize>> {
if x.is_zero() || x >= &self.c {
return None;
}

let num_bits = self.witness_bit_length();
let mut remaining = x.clone();
let mut config = Vec::with_capacity(num_bits);

for _ in 0..num_bits {
config.push(if (&remaining & BigUint::one()).is_zero() {
0
} else {
1
});
remaining >>= 1usize;
}

if remaining.is_zero() {
Some(config)
} else {
None
}
}

/// Decode a little-endian binary configuration into its witness integer `x`.
pub fn decode_witness(&self, config: &[usize]) -> Option<BigUint> {
if config.len() != self.witness_bit_length() || config.iter().any(|&digit| digit > 1) {
return None;
}

let mut value = BigUint::zero();
let mut weight = BigUint::one();
for &digit in config {
if digit == 1 {
value += &weight;
}
weight <<= 1usize;
}
Some(value)
}
}

#[derive(Deserialize)]
struct QuadraticCongruencesData {
a: u64,
b: u64,
c: u64,
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
a: BigUint,
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
b: BigUint,
#[serde(with = "crate::models::misc::biguint_serde::decimal_biguint")]
c: BigUint,
}

impl<'de> Deserialize<'de> for QuadraticCongruences {
Expand All @@ -134,38 +230,43 @@ impl Problem for QuadraticCongruences {
}

fn dims(&self) -> Vec<usize> {
if self.c <= 1 {
// No x in {1, ..., c-1} exists.
return vec![];
let num_bits = self.witness_bit_length();
if num_bits == 0 {
Vec::new()
} else {
vec![2; num_bits]
}
// config[0] ∈ {0, ..., c-2} maps to x = config[0] + 1 ∈ {1, ..., c-1}.
vec![self.c as usize - 1]
}

fn evaluate(&self, config: &[usize]) -> Or {
if self.c <= 1 {
let Some(x) = self.decode_witness(config) else {
return Or(false);
}
if config.len() != 1 {
};

if x.is_zero() || x >= *self.c() {
return Or(false);
}
let x = (config[0] as u64) + 1; // 1-indexed
let satisfies = ((x as u128) * (x as u128)) % (self.b as u128) == (self.a as u128);

let satisfies = (&x * &x) % self.b() == self.a().clone();
Or(satisfies)
}
}

crate::declare_variants! {
default QuadraticCongruences => "c",
default QuadraticCongruences => "2^bit_length_c",
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
let instance = QuadraticCongruences::new(4u32, 15u32, 10u32);
let optimal_config = instance
.encode_witness(&BigUint::from(2u32))
.expect("x=2 should be a valid canonical witness");

vec![crate::example_db::specs::ModelExampleSpec {
id: "quadratic_congruences",
instance: Box::new(QuadraticCongruences::new(4, 15, 10)),
// x=2 (config[0]=1): 2²=4 ≡ 4 (mod 15) ✓
optimal_config: vec![1],
instance: Box::new(instance),
optimal_config,
optimal_value: serde_json::json!(true),
}]
}
Expand Down
Loading
Loading