Skip to content

Comments

Add parity tests against Julia ProblemReductions.jl#65

Merged
GiggleLiu merged 16 commits intomainfrom
jg/issue-64-test-against-jl
Feb 14, 2026
Merged

Add parity tests against Julia ProblemReductions.jl#65
GiggleLiu merged 16 commits intomainfrom
jg/issue-64-test-against-jl

Conversation

@GiggleLiu
Copy link
Contributor

Summary

Resolves #64

  • Adds a Julia test data generation script (scripts/jl/generate_testdata.jl) that exercises ProblemReductions.jl to produce JSON fixtures for problem construction, evaluation, and reductions
  • Generates 26 JSON fixture files covering 10 problem types (IndependentSet, VertexCovering, MaxCut, SpinGlass, QUBO, Satisfiability, KSatisfiability, SetPacking, Matching, Factoring) and 16 reduction paths
  • Adds comprehensive Rust parity tests (tests/suites/jl_parity.rs) that deserialize the Julia fixtures and verify that the Rust implementations produce identical problem dimensions, evaluations, optimal solutions, and reduction round-trip results
  • Includes a make jl-testdata target to regenerate fixtures from Julia when needed
  • Adds a Julia local environment (scripts/jl/Project.toml, Manifest.toml) pinning ProblemReductions.jl dependencies

Test plan

  • make test passes (includes the new jl_parity test suite)
  • make clippy passes
  • Verify make jl-testdata regenerates fixtures (requires Julia + ProblemReductions.jl)

🤖 Generated with Claude Code

GiggleLiu and others added 6 commits February 14, 2026 02:42
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generates JSON fixtures from ProblemReductions.jl for parity testing:
- 10 model fixtures (IndependentSet, SpinGlass, MaxCut, QUBO, SAT, KSat,
  VertexCovering, SetPacking, Matching, Factoring)
- 17 reduction fixtures covering all Julia test/rules/rules.jl pairs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
22 active tests covering:
- 10 model evaluations (IS, SpinGlass, MaxCut, QUBO, SAT, KSat,
  VertexCover, SetPacking, Matching, Factoring)
- 12 reduction closed-loop tests (IS↔SetPacking, IS→VC, VC→SetCovering,
  SpinGlass↔MaxCut, SpinGlass↔QUBO, SAT↔KSat, CircuitSAT→SpinGlass,
  Factoring→CircuitSAT)
- 4 ignored stubs for unimplemented reductions (SAT→Coloring,
  SAT→IS, SAT→DominatingSet, Matching→SetPacking)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 99.65675% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.06%. Comparing base (2eb68fb) to head (79e9386).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...unit_tests/models/graph/maximum_independent_set.rs 95.83% 1 Missing ⚠️
...rc/unit_tests/models/graph/minimum_vertex_cover.rs 95.65% 1 Missing ⚠️
src/unit_tests/models/set/maximum_set_packing.rs 95.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #65      +/-   ##
==========================================
- Coverage   97.08%   97.06%   -0.02%     
==========================================
  Files         186      187       +1     
  Lines       25643    25150     -493     
==========================================
- Hits        24895    24412     -483     
+ Misses        748      738      -10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

"label": "circuit",
"extracted_single": [
[
1,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no line break in the dumped json file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b28ea0e — JSON fixtures now use compact format and files reorganized to tests/data/jl/.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements comprehensive parity testing between the Rust problemreductions crate and Julia's ProblemReductions.jl library to ensure behavioral consistency. The implementation includes a Julia test data generation script that produces JSON fixtures and corresponding Rust integration tests that verify matching results.

Changes:

  • Added Julia local environment with ProblemReductions.jl dependencies
  • Implemented Julia script to generate 26 JSON test fixtures covering 10 problem types and 16 reduction paths
  • Added comprehensive Rust parity tests for model evaluations, solver results, and reduction round-trips
  • Integrated new test suite into existing test infrastructure with make jl-testdata target

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/jl/Project.toml Julia project dependencies for ProblemReductions.jl, Graphs, and JSON
scripts/jl/Manifest.toml Locked Julia dependency versions
scripts/jl/generate_testdata.jl Julia script to generate test fixtures with proper 1→0-based index conversion
tests/suites/jl_parity.rs Rust parity tests for 10 problem types and 12 active reduction paths
tests/main.rs Integration of jl_parity test module
tests/data/jl_*.json 26 JSON fixture files (10 model fixtures, 16 reduction fixtures)
Makefile Added jl-testdata target for fixture regeneration
docs/plans/*.md Design and implementation plan documents

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 13 to 102
fn parse_edges(instance: &serde_json::Value) -> Vec<(usize, usize)> {
instance["edges"]
.as_array()
.unwrap()
.iter()
.map(|e| {
let arr = e.as_array().unwrap();
(
arr[0].as_u64().unwrap() as usize,
arr[1].as_u64().unwrap() as usize,
)
})
.collect()
}

fn parse_weighted_edges(instance: &serde_json::Value) -> Vec<(usize, usize, i32)> {
let edges = parse_edges(instance);
let weights: Vec<i32> = instance["weights"]
.as_array()
.unwrap()
.iter()
.map(|w| w.as_i64().unwrap() as i32)
.collect();
edges
.into_iter()
.zip(weights)
.map(|((u, v), w)| (u, v, w))
.collect()
}

fn parse_config(val: &serde_json::Value) -> Vec<usize> {
val.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect()
}

fn parse_configs_set(val: &serde_json::Value) -> HashSet<Vec<usize>> {
val.as_array()
.unwrap()
.iter()
.map(parse_config)
.collect()
}

fn parse_i32_vec(val: &serde_json::Value) -> Vec<i32> {
val.as_array()
.unwrap()
.iter()
.map(|v| v.as_i64().unwrap() as i32)
.collect()
}

fn parse_sets(val: &serde_json::Value) -> Vec<Vec<usize>> {
val.as_array()
.unwrap()
.iter()
.map(|s| {
s.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect()
})
.collect()
}

fn parse_sat_clauses(instance: &serde_json::Value) -> (usize, Vec<CNFClause>) {
let num_vars = instance["num_variables"].as_u64().unwrap() as usize;
let clauses: Vec<CNFClause> = instance["clauses"]
.as_array()
.unwrap()
.iter()
.map(|clause| {
let literals: Vec<i32> = clause["literals"]
.as_array()
.unwrap()
.iter()
.map(|lit| {
let var = lit["variable"].as_u64().unwrap() as i32 + 1; // Convert to 1-indexed
let negated = lit["negated"].as_bool().unwrap();
if negated { -var } else { var }
})
.collect();
CNFClause::new(literals)
})
.collect();
(num_vars, clauses)
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test functions extensively use .unwrap() for JSON deserialization and parsing. While this is acceptable for tests (where panics are tolerable), consider adding more descriptive error messages to help debug when fixtures don't match expected format. For example, instance["instance"]["num_vertices"].as_u64().expect("num_vertices should be a u64") would be more helpful than just .unwrap() when the JSON structure is incorrect.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b28ea0e — replaced .unwrap() with descriptive .expect() messages in all JSON parsing helpers (parse_edges, parse_config, parse_i32_vec, etc.).

Comment on lines 8 to 27
true,
false
],
[
true,
true
]
],
"extracted_multiple": [
[
true,
false
],
[
true,
true
],
[
false,
true
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extracted_single and extracted_multiple fields in this fixture contain boolean values (true/false) instead of integers (1/0). This is inconsistent with other fixtures and will cause parsing errors in the Rust tests, which expect integer arrays via parse_config. This fixture appears to not be used by any Rust test (SAT → IndependentSet is marked as #[ignore]), so it doesn't cause immediate failures, but should be fixed for consistency.

Suggested change
true,
false
],
[
true,
true
]
],
"extracted_multiple": [
[
true,
false
],
[
true,
true
],
[
false,
true
1,
0
],
[
1,
1
]
],
"extracted_multiple": [
[
1,
0
],
[
1,
1
],
[
0,
1

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b28ea0e — added bool_to_int conversion in export_reduction() so extract_solution results serialize as 0/1 integers instead of true/false. SAT→IndependentSet test is now active (no longer #[ignore]).

@GiggleLiu
Copy link
Contributor Author

Systematic Comparison: Julia ProblemReductions.jl vs Rust Parity Tests

Model Evaluation Coverage (16 Julia problems)

Julia Problem Rust Type Doc Examples Parity Test Status
IndependentSet MaximumIndependentSet ✅ 4-vertex + diamond + petersen
VertexCovering MinimumVertexCover ✅ 4-vertex + petersen
Matching MaximumMatching ✅ petersen
SetPacking MaximumSetPacking ✅ five_sets
SetCovering MinimumSetCovering ✅ 3-subsets ✅ NEW
MaxCut MaxCut ✅ K3 + petersen
DominatingSet MinimumDominatingSet ✅ path_graph(5) ✅ NEW
MaximalIS MaximalIS ✅ 4-vertex ✅ NEW
SpinGlass SpinGlass ✅ 4-vertex + petersen
QUBO QUBO ✅ identity + 3x3
Satisfiability Satisfiability
KSatisfiability KSatisfiability
CircuitSAT CircuitSAT ✅ (reduction)
Factoring Factoring ✅ (2,2,6) + 3 others
PaintShop PaintShop ✅ "abaccb" ✅ NEW
Coloring{3} KColoring<3> ✅ petersen ✅ NEW
BicliqueCover BicliqueCover ❌ missing
BMF BMF ❌ missing

14/16 problems covered. BicliqueCover and BMF have no Julia doc examples to extract.

Reduction Coverage (Julia has ~14 reduction rules)

Reduction Julia Test Fixture Rust Parity Test Status
IS → SetPacking
SetPacking → IS
IS ↔ VertexCovering
VertexCovering → SetCovering
SpinGlass ↔ MaxCut
SpinGlass ↔ QUBO
SAT ↔ KSat{3}
CircuitSAT → SpinGlass
Factoring → CircuitSAT
IS → SetPacking (doc 4-vertex) ✅ NEW
SAT → Coloring{3} ✅ fixture exists #[ignore] ❌ not impl in Rust
SAT → IndependentSet ✅ fixture exists #[ignore] ❌ not impl in Rust
SAT → DominatingSet ✅ fixture exists #[ignore] ❌ not impl in Rust
Matching → SetPacking ✅ fixture exists #[ignore] ❌ not impl in Rust

10/14 reductions fully tested. 4 have fixtures generated but are #[ignore] pending Rust rule implementation.

Semantic Mapping Notes

  • SpinGlass: Spin convention differs (Julia 0→+1, Rust 0→−1). Tests flip configs before comparison.
  • KColoring: Julia EXTREMA counts valid edges; Rust returns bool. Test maps jl_size == num_edges ↔ Rust true.
  • SAT/KSat: Julia EXTREMA counts satisfied clauses; Rust returns bool. Test maps jl_size == num_clauses ↔ Rust true.
  • QUBO: Julia uses full symmetric matrix; Rust upper-triangle. Test converts Q_rust[i][j] = Q_jl[i][j] + Q_jl[j][i].
  • Factoring: Julia uses is_valid flag; Rust minimizes distance. Test maps jl_valid ↔ rust_value == 0.

GiggleLiu and others added 2 commits February 14, 2026 13:31
- Reorganize fixtures: tests/data/jl_*.json → tests/data/jl/*.json with compact JSON
- Add doc example instances for 5 new problem types (DominatingSet, MaximalIS,
  PaintShop, KColoring, SetCovering) and doc instances for existing problems
- Add 22 individual rule test instances from Julia test/rules/*.jl covering
  spinglass↔maxcut, qubo→spinglass, vc→setcovering, is→setpacking,
  matching→setpacking, sat→ksat/coloring/independentset/dominatingset
- Implement 11 new reduction parity tests (replaced 4 #[ignore] stubs)
- Use ILP solver for SAT→Coloring test (brute force was 185s, now 0.02s)
- Handle unsatisfiable SAT instances in evaluation test

39 passing, 1 ignored (SAT→CircuitSAT not in Rust)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix boolean values in extracted solutions (true/false → 0/1 integers)
- Replace .unwrap() with descriptive .expect() messages in JSON parsers
- Compact JSON already addressed in previous commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GiggleLiu and others added 7 commits February 14, 2026 14:21
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 40 Julia ProblemReductions.jl parity tests (model evaluations and
reduction round-trips) are now in src/unit_tests/jl_parity.rs, removing
duplication with the existing unit test suite. The integration test file
tests/suites/jl_parity.rs is reduced to a stub comment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Distribute Julia parity tests from tests/suites/jl_parity.rs into
the corresponding unit test files (15 model + 12 rule files). Tests
use shared JSON parsing helpers via include!("../jl_helpers.rs").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove ~95 hand-written evaluate/brute_force tests from model and rule
unit test files that are fully covered by the JL fixture-based parity
tests. Keep construction, utility, trait compliance, edge case, and
relationship tests that verify distinct concerns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 89 out of 90 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 85 to 97
let num_vars = instance["num_variables"].as_u64().unwrap() as usize;
let clauses = instance["clauses"]
.as_array()
.unwrap()
.iter()
.map(|clause| {
let literals: Vec<i32> = clause["literals"]
.as_array()
.unwrap()
.iter()
.map(|lit| {
let var = lit["variable"].as_u64().unwrap() as i32 + 1;
let negated = lit["negated"].as_bool().unwrap();
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jl_parse_sat_clauses function uses .unwrap() in several places (lines 85-97) instead of .expect() with descriptive messages, which is inconsistent with the rest of the helper functions that use .expect(). This makes debugging more difficult when JSON fixtures don't match the expected format. Consider replacing .unwrap() with .expect("...") to provide helpful error messages when the JSON structure is incorrect.

Suggested change
let num_vars = instance["num_variables"].as_u64().unwrap() as usize;
let clauses = instance["clauses"]
.as_array()
.unwrap()
.iter()
.map(|clause| {
let literals: Vec<i32> = clause["literals"]
.as_array()
.unwrap()
.iter()
.map(|lit| {
let var = lit["variable"].as_u64().unwrap() as i32 + 1;
let negated = lit["negated"].as_bool().unwrap();
let num_vars = instance["num_variables"]
.as_u64()
.expect("num_variables should be a u64") as usize;
let clauses = instance["clauses"]
.as_array()
.expect("clauses should be an array")
.iter()
.map(|clause| {
let literals: Vec<i32> = clause["literals"]
.as_array()
.expect("clause.literals should be an array")
.iter()
.map(|lit| {
let var = lit["variable"]
.as_u64()
.expect("literal.variable should be a u64") as i32
+ 1;
let negated = lit["negated"]
.as_bool()
.expect("literal.negated should be a bool");

Copilot uses AI. Check for mistakes.
Comment on lines 125 to 127
.unwrap()
.iter()
.find(|inst| inst["label"].as_str().unwrap() == label)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jl_find_instance_by_label function also uses .unwrap() on lines 124-127 instead of .expect(), which is inconsistent with the error handling pattern established in other helper functions. Consider using .expect() with a descriptive message for the array access to maintain consistency and improve debugging.

Suggested change
.unwrap()
.iter()
.find(|inst| inst["label"].as_str().unwrap() == label)
.expect("instances should be an array")
.iter()
.find(|inst| inst["label"].as_str().expect("instance label should be a string") == label)

Copilot uses AI. Check for mistakes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@GiggleLiu GiggleLiu merged commit 2b5f0fd into main Feb 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Test against ProblemReductions.jl

1 participant