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
2 changes: 1 addition & 1 deletion .claude/skills/review-pipeline/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Invoke `/review-quality` (file: `.claude/skills/review-quality/SKILL.md`) with t
- Classify as: `confirmed` / `not reproducible in current worktree`
- For confirmed issues, note severity and recommended fix

**Do NOT fix any issues.** Only report them.
**Do NOT fix any issues.** Only report them. When dispatching the agentic-test subagent, explicitly instruct it: "This is a read-only review run. Do NOT offer to fix issues, do NOT select option (a) 'Review together and fix', and do NOT modify any files. Report findings only and stop after generating the report."

### 2. Compose Combined Review Comment

Expand Down
27 changes: 27 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"IsomorphicSpanningTree": [Isomorphic Spanning Tree],
"KthBestSpanningTree": [Kth Best Spanning Tree],
"KColoring": [$k$-Coloring],
"KClique": [$k$-Clique],
"MinimumDominatingSet": [Minimum Dominating Set],
"MaximumMatching": [Maximum Matching],
"TravelingSalesman": [Traveling Salesman],
Expand Down Expand Up @@ -1431,6 +1432,32 @@ is feasible: each set induces a connected subgraph, the component weights are $2
]
]
}
#{
let x = load-model-example("KClique")
let nv = graph-num-vertices(x.instance)
let ne = graph-num-edges(x.instance)
let edges = x.instance.graph.edges
let k = x.instance.k
let sol = (config: x.optimal_config, metric: x.optimal_value)
let K = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
let clique-edges = edges.filter(e => K.contains(e.at(0)) and K.contains(e.at(1)))
[
#problem-def("KClique")[
Given an undirected graph $G = (V, E)$ and an integer $k$, determine whether there exists a subset $K subset.eq V$ with $|K| >= k$ such that every pair of distinct vertices in $K$ is adjacent.
][
$k$-Clique is the classical decision version of Clique, one of Karp's original NP-complete problems @karp1972 and listed as GT19 in Garey and Johnson @garey1979. Unlike Maximum Clique, the threshold $k$ is part of the input, so this formulation is the natural target for decision-to-decision reductions such as $3$SAT $arrow.r$ Clique. The best known exact algorithm matches Maximum Clique via the complement reduction to Maximum Independent Set and runs in $O^*(1.1996^n)$ @xiao2017.

*Example.* Consider the house graph $G$ with $n = #nv$ vertices, $|E| = #ne$ edges, and threshold $k = #k$. The set $K = {#K.map(i => $v_#i$).join(", ")}$ is a valid witness because all three pairs #clique-edges.map(((u, v)) => $(v_#u, v_#v)$).join(", ") are edges, so $|K| = 3 >= #k$ and this is a YES instance. This witness is unique, and no $4$-clique exists because every vertex outside $K$ misses at least one edge to the other selected vertices.

#figure({
let hg = house-graph()
draw-edge-highlight(hg.vertices, hg.edges, clique-edges, K)
},
caption: [The house graph with satisfying witness $K = {#K.map(i => $v_#i$).join(", ")}$ for $k = #k$. The selected vertices and their internal clique edges are highlighted in blue.],
) <fig:house-kclique>
]
]
}
#{
let x = load-model-example("MaximumClique")
let nv = graph-num-vertices(x.instance)
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ Flags by problem type:
QUBO --matrix
SpinGlass --graph, --couplings, --fields
KColoring --graph, --k
KClique --graph, --k
MinimumMultiwayCut --graph, --terminals, --edge-weights
PartitionIntoTriangles --graph
GraphPartitioning --graph
Expand Down
105 changes: 104 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
Some("UnitDiskGraph") => "--positions \"0,0;1,0;0.5,0.8\" --radius 1.5",
_ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1",
},
"KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3",
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
"GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5",
"MinimumCutIntoBoundedSets" => {
Expand Down Expand Up @@ -1376,6 +1377,13 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
util::ser_kcoloring(graph, k)?
}

"KClique" => {
let usage = "Usage: pred create KClique --graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3";
let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
let k = parse_kclique_threshold(args.k, graph.num_vertices(), usage)?;
(ser(KClique::new(graph, k))?, resolved_variant.clone())
}

// SAT
"Satisfiability" => {
let num_vars = args.num_vars.ok_or_else(|| {
Expand Down Expand Up @@ -3403,6 +3411,21 @@ fn ser<T: Serialize>(problem: T) -> Result<serde_json::Value> {
util::ser(problem)
}

fn parse_kclique_threshold(
k_flag: Option<usize>,
num_vertices: usize,
usage: &str,
) -> Result<usize> {
let k = k_flag.ok_or_else(|| anyhow::anyhow!("KClique requires --k\n\n{usage}"))?;
if k == 0 {
bail!("KClique: --k must be positive");
}
if k > num_vertices {
bail!("KClique: k must be <= graph num_vertices");
}
Ok(k)
}

fn variant_map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
util::variant_map(pairs)
}
Expand Down Expand Up @@ -4450,6 +4473,21 @@ fn create_random(
}
}

"KClique" => {
let edge_prob = args.edge_prob.unwrap_or(0.5);
if !(0.0..=1.0).contains(&edge_prob) {
bail!("--edge-prob must be between 0.0 and 1.0");
}
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
let usage =
"Usage: pred create KClique --random --num-vertices 5 [--edge-prob 0.5] [--seed 42] --k 3";
let k = parse_kclique_threshold(args.k, graph.num_vertices(), usage)?;
(
ser(KClique::new(graph, k))?,
variant_map(&[("graph", "SimpleGraph")]),
)
}

// MinimumCutIntoBoundedSets (graph + edge weights + s/t/B/K)
"MinimumCutIntoBoundedSets" => {
let edge_prob = args.edge_prob.unwrap_or(0.5);
Expand Down Expand Up @@ -4702,7 +4740,7 @@ fn create_random(
_ => bail!(
"Random generation is not supported for {canonical}. \
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, KClique, TravelingSalesman, \
SteinerTreeInGraphs, HamiltonianCircuit, SteinerTree, OptimalLinearArrangement, HamiltonianPath, GeneralizedHex)"
),
};
Expand Down Expand Up @@ -5349,4 +5387,69 @@ mod tests {
let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("out of bounds for left partition size 4"));
}

#[test]
fn test_create_kclique() {
use crate::dispatch::ProblemJsonOutput;
use problemreductions::models::graph::KClique;

let mut args = empty_args();
args.problem = Some("KClique".to_string());
args.graph = Some("0-1,0-2,1-3,2-3,2-4,3-4".to_string());
args.k = Some(3);

let output_path =
std::env::temp_dir().join(format!("kclique-create-{}.json", std::process::id()));
let out = OutputConfig {
output: Some(output_path.clone()),
quiet: true,
json: false,
auto_json: false,
};

create(&args, &out).unwrap();

let json = std::fs::read_to_string(&output_path).unwrap();
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
assert_eq!(created.problem_type, "KClique");
assert_eq!(
created.variant.get("graph").map(String::as_str),
Some("SimpleGraph")
);

let problem: KClique<SimpleGraph> = serde_json::from_value(created.data).unwrap();
assert_eq!(problem.k(), 3);
assert_eq!(problem.num_vertices(), 5);
assert!(problem.evaluate(&[0, 0, 1, 1, 1]));

let _ = std::fs::remove_file(output_path);
}

#[test]
fn test_create_kclique_requires_valid_k() {
let mut args = empty_args();
args.problem = Some("KClique".to_string());
args.graph = Some("0-1,0-2,1-3,2-3,2-4,3-4".to_string());
args.k = None;

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err();
assert!(
err.to_string().contains("KClique requires --k"),
"unexpected error: {err}"
);

args.k = Some(6);
let err = create(&args, &out).unwrap_err();
assert!(
err.to_string().contains("k must be <= graph num_vertices"),
"unexpected error: {err}"
);
}
}
43 changes: 40 additions & 3 deletions problemreductions-cli/src/mcp/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::util;
use problemreductions::models::algebraic::QUBO;
use problemreductions::models::formula::{CNFClause, Satisfiability};
use problemreductions::models::graph::{
MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet,
KClique, MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet,
MinimumSumMulticenter, MinimumVertexCover, SpinGlass, TravelingSalesman,
};
use problemreductions::models::misc::Factoring;
Expand Down Expand Up @@ -68,7 +68,7 @@ pub struct CreateProblemParams {
)]
pub problem_type: String,
#[schemars(
description = "Problem parameters as JSON object. Graph problems: {\"edges\": \"0-1,1-2\", \"weights\": \"1,2,3\"}. SAT: {\"num_vars\": 3, \"clauses\": \"1,2;-1,3\"}. QUBO: {\"matrix\": \"1,0.5;0.5,2\"}. KColoring: {\"edges\": \"0-1,1-2\", \"k\": 3}. Factoring: {\"target\": 15, \"bits_m\": 4, \"bits_n\": 4}. Random graph: {\"random\": true, \"num_vertices\": 10, \"edge_prob\": 0.3}. Geometry graphs (use with MIS/KingsSubgraph etc.): {\"positions\": \"0,0;1,0;1,1\"}. UnitDiskGraph: {\"positions\": \"0.0,0.0;1.0,0.0\", \"radius\": 1.5}"
description = "Problem parameters as JSON object. Graph problems: {\"edges\": \"0-1,1-2\", \"weights\": \"1,2,3\"}. SAT: {\"num_vars\": 3, \"clauses\": \"1,2;-1,3\"}. QUBO: {\"matrix\": \"1,0.5;0.5,2\"}. KColoring: {\"edges\": \"0-1,1-2\", \"k\": 3}. KClique: {\"edges\": \"0-1,0-2,1-3,2-3,2-4,3-4\", \"k\": 3}. Factoring: {\"target\": 15, \"bits_m\": 4, \"bits_n\": 4}. Random graph: {\"random\": true, \"num_vertices\": 10, \"edge_prob\": 0.3}. Geometry graphs (use with MIS/KingsSubgraph etc.): {\"positions\": \"0,0;1,0;1,1\"}. UnitDiskGraph: {\"positions\": \"0.0,0.0;1.0,0.0\", \"radius\": 1.5}"
)]
pub params: serde_json::Value,
}
Expand Down Expand Up @@ -406,6 +406,16 @@ impl McpServer {
util::ser_kcoloring(graph, k)?
}

"KClique" => {
let (graph, _) = parse_graph_from_params(params)?;
let k_flag = params.get("k").and_then(|v| v.as_u64()).map(|v| v as usize);
let k = parse_kclique_threshold(k_flag, graph.num_vertices())?;
(
ser(KClique::new(graph, k))?,
variant_map(&[("graph", "SimpleGraph")]),
)
}

// SAT
"Satisfiability" => {
let num_vars = params
Expand Down Expand Up @@ -613,6 +623,22 @@ impl McpServer {
util::validate_k_param(resolved_variant, k_flag, Some(3), "KColoring")?;
util::ser_kcoloring(graph, k)?
}
"KClique" => {
let edge_prob = params
.get("edge_prob")
.and_then(|v| v.as_f64())
.unwrap_or(0.5);
if !(0.0..=1.0).contains(&edge_prob) {
anyhow::bail!("edge_prob must be between 0.0 and 1.0");
}
let graph = util::create_random_graph(num_vertices, edge_prob, seed);
let k_flag = params.get("k").and_then(|v| v.as_u64()).map(|v| v as usize);
let k = parse_kclique_threshold(k_flag, graph.num_vertices())?;
(
ser(KClique::new(graph, k))?,
variant_map(&[("graph", "SimpleGraph")]),
)
}
"MinimumSumMulticenter" => {
let edge_prob = params
.get("edge_prob")
Expand Down Expand Up @@ -644,7 +670,7 @@ impl McpServer {
_ => anyhow::bail!(
"Random generation is not supported for {}. \
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, \
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, KClique, \
TravelingSalesman, MinimumSumMulticenter)",
canonical
),
Expand Down Expand Up @@ -1231,6 +1257,17 @@ fn parse_graph_from_params(params: &serde_json::Value) -> anyhow::Result<(Simple
Ok((SimpleGraph::new(num_vertices, edges), num_vertices))
}

fn parse_kclique_threshold(k_flag: Option<usize>, num_vertices: usize) -> anyhow::Result<usize> {
let k = k_flag.ok_or_else(|| anyhow::anyhow!("KClique requires 'k'"))?;
if k == 0 {
anyhow::bail!("KClique: 'k' must be positive");
}
if k > num_vertices {
anyhow::bail!("KClique: k must be <= graph num_vertices");
}
Ok(k)
}

/// Parse `weights` field from JSON params as vertex weights (i32), defaulting to all 1s.
fn parse_vertex_weights_from_params(
params: &serde_json::Value,
Expand Down
34 changes: 32 additions & 2 deletions scripts/pipeline_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,38 @@
FAILURE_LABELS = {"PoorWritten", "Wrong", "Trivial", "Useless"}


def run_gh(*args: str) -> str:
return subprocess.check_output(["gh", *args], text=True)
def run_gh(*args: str, retries: int = 3, retry_delay: float = 5.0) -> str:
"""Run a ``gh`` CLI command, retrying on transient failures.

The ``gh project`` subcommands occasionally fail with cryptic errors
like "unknown owner type" due to transient API issues or token
refresh races (see cli/cli#7985, cli/cli#8885). Retrying after a
short delay resolves these reliably.
"""
last_exc: subprocess.CalledProcessError | None = None
for attempt in range(retries):
try:
return subprocess.check_output(
["gh", *args], text=True, stderr=subprocess.PIPE,
)
except subprocess.CalledProcessError as exc:
last_exc = exc
stderr = (exc.stderr or "").strip()
if attempt < retries - 1:
print(
f"[run_gh] attempt {attempt + 1}/{retries} failed "
f"(rc={exc.returncode}, stderr={stderr!r}), "
f"retrying in {retry_delay}s…",
file=sys.stderr,
)
time.sleep(retry_delay)
else:
print(
f"[run_gh] all {retries} attempts failed "
f"(stderr={stderr!r})",
file=sys.stderr,
)
raise last_exc # type: ignore[misc]


def _graphql_board_query(project_id: str, page_size: int, cursor: str | None) -> str:
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub mod prelude {
pub use crate::models::graph::{
BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation,
BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GeneralizedHex,
GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IsomorphicSpanningTree,
GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IsomorphicSpanningTree, KClique,
KthBestSpanningTree, LengthBoundedDisjointPaths, SpinGlass, SteinerTree,
StrongConnectivityAugmentation, SubgraphIsomorphism,
};
Expand Down
Loading
Loading