diff --git a/.claude/skills/review-pipeline/SKILL.md b/.claude/skills/review-pipeline/SKILL.md index f8511f88f..9849a5432 100644 --- a/.claude/skills/review-pipeline/SKILL.md +++ b/.claude/skills/review-pipeline/SKILL.md @@ -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 diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 53188f3f8..0c2a242ac 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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], @@ -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.], + ) + ] + ] +} #{ let x = load-model-example("MaximumClique") let nv = graph-num-vertices(x.instance) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 9fdeea3da..254fbdce2 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -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 diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index f04afb8cb..b0e4eb3a0 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -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" => { @@ -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(|| { @@ -3403,6 +3411,21 @@ fn ser(problem: T) -> Result { util::ser(problem) } +fn parse_kclique_threshold( + k_flag: Option, + num_vertices: usize, + usage: &str, +) -> Result { + 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 { util::variant_map(pairs) } @@ -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); @@ -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)" ), }; @@ -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 = 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}" + ); + } } diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 37fbb236f..9240123b8 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -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; @@ -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, } @@ -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 @@ -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") @@ -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 ), @@ -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, num_vertices: usize) -> anyhow::Result { + 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, diff --git a/scripts/pipeline_board.py b/scripts/pipeline_board.py index f75a3c9b5..4f4e86cd4 100644 --- a/scripts/pipeline_board.py +++ b/scripts/pipeline_board.py @@ -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: diff --git a/src/lib.rs b/src/lib.rs index 973c8476e..f0993eadb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, }; diff --git a/src/models/graph/kclique.rs b/src/models/graph/kclique.rs new file mode 100644 index 000000000..d16e2911c --- /dev/null +++ b/src/models/graph/kclique.rs @@ -0,0 +1,145 @@ +//! KClique problem implementation. +//! +//! KClique is the decision version of Clique: determine whether a graph +//! contains a clique of size at least `k`. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "KClique", + display_name: "k-Clique", + aliases: &["Clique"], + dimensions: &[VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"])], + module_path: module_path!(), + description: "Determine whether a graph contains a clique of size at least k", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, + FieldInfo { name: "k", type_name: "usize", description: "Minimum clique size threshold" }, + ], + } +} + +/// The k-Clique decision problem. +/// +/// Given a graph `G = (V, E)` and a positive integer `k`, determine whether +/// there exists a subset `K ⊆ V` of size at least `k` such that every pair of +/// distinct vertices in `K` is adjacent. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KClique { + graph: G, + k: usize, +} + +impl KClique { + /// Create a new k-Clique problem instance. + pub fn new(graph: G, k: usize) -> Self { + assert!(k > 0, "k must be positive"); + assert!(k <= graph.num_vertices(), "k must be <= graph num_vertices"); + Self { graph, k } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the clique-size threshold. + pub fn k(&self) -> usize { + self.k + } + + /// Get the number of vertices in the underlying graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges in the underlying graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Check whether a configuration is a valid witness. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + is_kclique_config(&self.graph, config, self.k) + } +} + +impl Problem for KClique +where + G: Graph + crate::variant::VariantParam, +{ + const NAME: &'static str = "KClique"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_vertices()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + is_kclique_config(&self.graph, config, self.k) + } +} + +impl SatisfactionProblem for KClique where G: Graph + crate::variant::VariantParam {} + +fn is_kclique_config(graph: &G, config: &[usize], k: usize) -> bool { + if config.len() != graph.num_vertices() { + return false; + } + + let selected: Vec = match config + .iter() + .enumerate() + .map(|(index, &value)| match value { + 0 => Ok(None), + 1 => Ok(Some(index)), + _ => Err(()), + }) + .collect::, _>>() + { + Ok(values) => values.into_iter().flatten().collect(), + Err(()) => return false, + }; + + if selected.len() < k { + return false; + } + + for i in 0..selected.len() { + for j in (i + 1)..selected.len() { + if !graph.has_edge(selected[i], selected[j]) { + return false; + } + } + } + true +} + +crate::declare_variants! { + default sat KClique => "1.1996^num_vertices", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "kclique_simplegraph", + instance: Box::new(KClique::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), + 3, + )), + optimal_config: vec![0, 0, 1, 1, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/kclique.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index a1973da55..c88017482 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -12,6 +12,7 @@ //! - [`MinimumCutIntoBoundedSets`]: Minimum cut into bounded sets (Garey & Johnson ND17) //! - [`HamiltonianCircuit`]: Hamiltonian circuit (decision problem) //! - [`IsomorphicSpanningTree`]: Isomorphic spanning tree (satisfaction) +//! - [`KClique`]: Clique decision problem with threshold k //! - [`KthBestSpanningTree`]: K distinct bounded spanning trees (satisfaction) //! - [`KColoring`]: K-vertex coloring //! - [`PartitionIntoTriangles`]: Partition vertices into triangles @@ -51,6 +52,7 @@ pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_circuit; pub(crate) mod hamiltonian_path; pub(crate) mod isomorphic_spanning_tree; +pub(crate) mod kclique; pub(crate) mod kcoloring; pub(crate) mod kth_best_spanning_tree; pub(crate) mod length_bounded_disjoint_paths; @@ -92,6 +94,7 @@ pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_circuit::HamiltonianCircuit; pub use hamiltonian_path::HamiltonianPath; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; +pub use kclique::KClique; pub use kcoloring::KColoring; pub use kth_best_spanning_tree::KthBestSpanningTree; pub use length_bounded_disjoint_paths::LengthBoundedDisjointPaths; @@ -133,6 +136,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec SimpleGraph { + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]) +} + +fn issue_witness() -> Vec { + vec![0, 0, 1, 1, 1] +} + +#[test] +fn test_kclique_creation() { + let problem = KClique::new(issue_graph(), 3); + + assert_eq!(problem.graph().num_vertices(), 5); + assert_eq!(problem.graph().num_edges(), 6); + assert_eq!(problem.k(), 3); + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 6); + assert_eq!(problem.dims(), vec![2; 5]); +} + +#[test] +fn test_kclique_evaluate_yes_instance() { + let problem = KClique::new(issue_graph(), 3); + + assert!(problem.evaluate(&issue_witness())); + assert!(problem.is_valid_solution(&issue_witness())); +} + +#[test] +fn test_kclique_evaluate_rejects_non_clique() { + let problem = KClique::new(issue_graph(), 3); + + assert!(!problem.evaluate(&[1, 0, 1, 1, 0])); + assert!(!problem.is_valid_solution(&[1, 0, 1, 1, 0])); +} + +#[test] +fn test_kclique_evaluate_rejects_too_small_clique() { + let problem = KClique::new(issue_graph(), 3); + + assert!(!problem.evaluate(&[1, 0, 1, 0, 0])); + assert!(!problem.evaluate(&[0, 0, 1, 1, 0])); +} + +#[test] +fn test_kclique_solver_finds_unique_witness() { + let problem = KClique::new(issue_graph(), 3); + let solver = BruteForce::new(); + + assert_eq!(solver.find_satisfying(&problem), Some(issue_witness())); + assert_eq!(solver.find_all_satisfying(&problem), vec![issue_witness()]); +} + +#[test] +fn test_kclique_serialization_round_trip() { + let problem = KClique::new(issue_graph(), 3); + let json = serde_json::to_string(&problem).unwrap(); + let restored: KClique = serde_json::from_str(&json).unwrap(); + + assert_eq!(restored.graph().edges(), problem.graph().edges()); + assert_eq!(restored.k(), 3); + assert!(restored.evaluate(&issue_witness())); +} + +#[test] +fn test_kclique_paper_example() { + let problem = KClique::new(issue_graph(), 3); + let solver = BruteForce::new(); + + assert!(problem.evaluate(&issue_witness())); + assert_eq!(solver.find_all_satisfying(&problem), vec![issue_witness()]); +}