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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ run-plan:
@NL=$$'\n'; \
BRANCH=$$(git branch --show-current); \
if [ "$(AGENT_TYPE)" = "claude" ]; then \
PROCESS="1. Read the plan file$${NL}2. Use /subagent-driven-development to execute tasks$${NL}3. Push: git push origin $$BRANCH$${NL}4. Post summary"; \
PROCESS="1. Read the plan file$${NL}2. Use /subagent-driven-development to execute tasks$${NL}3. Push: git push origin $$BRANCH$${NL}4. Create a pull request"; \
else \
PROCESS="1. Read the plan file$${NL}2. Execute the tasks step by step. For each task, implement and test before moving on.$${NL}3. Push: git push origin $$BRANCH$${NL}4. Post summary"; \
PROCESS="1. Read the plan file$${NL}2. Execute the tasks step by step. For each task, implement and test before moving on.$${NL}3. Push: git push origin $$BRANCH$${NL}4. Create a pull request"; \
fi; \
PROMPT="Execute the plan in '$${PLAN_FILE}'."; \
if [ -n "$(INSTRUCTIONS)" ]; then \
Expand Down
204 changes: 55 additions & 149 deletions docs/src/design.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/src/reductions/reduction_graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,17 @@
],
"doc_path": "rules/spinglass_qubo/index.html"
},
{
"source": 21,
"target": 0,
"overhead": [
{
"field": "num_variables",
"formula": "num_variables + num_clauses + 1"
}
],
"doc_path": "rules/sat_circuitsat/index.html"
},
{
"source": 21,
"target": 3,
Expand Down
51 changes: 51 additions & 0 deletions examples/chained_reduction_factoring_to_spinglass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// # Chained Reduction: Factoring -> SpinGlass
//
// Mirrors Julia's examples/Ising.jl — reduces a Factoring problem
// to SpinGlass via the reduction graph, then solves and extracts the factors.
// Uses ILPSolver for the solve step (Julia uses GenericTensorNetworks).

// ANCHOR: imports
use problemreductions::prelude::*;
use problemreductions::rules::{MinimizeSteps, ReductionGraph};
use problemreductions::solvers::ILPSolver;
use problemreductions::topology::SimpleGraph;
use problemreductions::types::ProblemSize;
// ANCHOR_END: imports

pub fn run() {
// ANCHOR: example
let graph = ReductionGraph::new();

// Find reduction path: Factoring -> ... -> SpinGlass
let src_var = ReductionGraph::variant_to_map(&Factoring::variant());
let dst_var =
ReductionGraph::variant_to_map(&SpinGlass::<SimpleGraph, f64>::variant());
let rpath = graph
.find_cheapest_path(
"Factoring",
&src_var,
"SpinGlass",
&dst_var,
&ProblemSize::new(vec![]),
&MinimizeSteps,
)
.unwrap();
println!("Reduction path: {:?}", rpath.type_names());

// Create: factor 6 = p × q with 2-bit factors (mirrors Julia's Factoring(2, 2, 6))
let factoring = Factoring::new(2, 2, 6);

// Solve Factoring via ILP
let solver = ILPSolver::new();
let solution = solver.solve_reduced(&factoring).unwrap();

// Extract and display the factors
let (p, q) = factoring.read_factors(&solution);
println!("{} = {} × {}", factoring.target(), p, q);
assert_eq!(p * q, 6, "Factors should multiply to 6");
// ANCHOR_END: example
}

fn main() {
run()
}
238 changes: 237 additions & 1 deletion scripts/jl/generate_testdata.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Generate JSON test fixtures from ProblemReductions.jl for Rust parity testing.
# Run: cd scripts/jl && julia --project=. generate_testdata.jl

using ProblemReductions, Graphs, JSON
using ProblemReductions, Graphs, JSON, Random

const OUTDIR = joinpath(@__DIR__, "..", "..", "tests", "data", "jl")
mkpath(OUTDIR)
Expand Down Expand Up @@ -274,6 +274,165 @@ function export_setcovering(sc, label)
return make_instance(label, inst, sc)
end

"""Evaluate BicliqueCover with flat binary config (matching Rust convention).

Config layout: for each vertex v (0-based) and biclique b (0-based),
config[v*k + b + 1] == 1 means vertex v is in biclique b.
Returns (is_valid, size) where is_valid means all edges covered,
and size is the total number of 1s in the config.
"""
function biclique_cover_evaluate(left_size, right_size, edges_0based, k, config)
n = left_size + right_size
# Check all edges are covered
for (l, r) in edges_0based
covered = false
for b in 0:k-1
l_in = config[l * k + b + 1] == 1
r_in = config[r * k + b + 1] == 1
if l_in && r_in
covered = true
break
end
end
if !covered
return (false, 0)
end
end
return (true, sum(config))
end

function export_biclique_cover(graph, left_part, k, label)
left_size = length(left_part)
right_size = nv(graph) - left_size
edges_0 = graph_to_edges(graph)
n = nv(graph)
num_vars = n * k

inst = Dict(
"num_vertices" => n,
"edges" => edges_0,
"left_size" => left_size,
"right_size" => right_size,
"k" => k,
)

# Sample configs
configs = Set{Vector{Int}}()
push!(configs, zeros(Int, num_vars))
push!(configs, ones(Int, num_vars))
while length(configs) < min(10, 2^num_vars)
push!(configs, [rand(0:1) for _ in 1:num_vars])
end
configs = collect(configs)

# Evaluate configs
evals = []
for cfg in configs
(valid, sz) = biclique_cover_evaluate(left_size, right_size, edges_0, k, cfg)
push!(evals, Dict("config" => cfg, "is_valid" => valid, "size" => valid ? sz : 0))
end

# Brute force: find all best (minimize size among valid covers)
best_size = typemax(Int)
best_configs = Vector{Int}[]
for bits in 0:(2^num_vars - 1)
cfg = [(bits >> i) & 1 for i in 0:num_vars-1]
(valid, sz) = biclique_cover_evaluate(left_size, right_size, edges_0, k, cfg)
if valid
if sz < best_size
best_size = sz
best_configs = [cfg]
elseif sz == best_size
push!(best_configs, cfg)
end
end
end

return Dict(
"label" => label,
"instance" => inst,
"evaluations" => evals,
"best_solutions" => best_configs,
)
end

"""Evaluate BMF with flat binary config (matching Rust convention).

Config layout: first m*k bits are B (row-major), next k*n bits are C (row-major).
Returns hamming distance between A and boolean_product(B, C).
All configs are valid.
"""
function bmf_evaluate(A, m, n, k, config)
# Extract B (m x k)
B = zeros(Bool, m, k)
for i in 1:m, j in 1:k
B[i,j] = config[(i-1)*k + j] == 1
end
b_size = m * k
# Extract C (k x n)
C = zeros(Bool, k, n)
for i in 1:k, j in 1:n
C[i,j] = config[b_size + (i-1)*n + j] == 1
end
# Boolean product
product = zeros(Bool, m, n)
for i in 1:m, j in 1:n
product[i,j] = any(kk -> B[i,kk] && C[kk,j], 1:k)
end
# Hamming distance
return sum(A .!= product)
end

function export_bmf(A, k, label)
m, n = size(A)
num_vars = m * k + k * n

inst = Dict(
"matrix" => [[Int(A[i,j]) for j in 1:n] for i in 1:m],
"m" => m,
"n" => n,
"k" => k,
)

# Sample configs
configs = Set{Vector{Int}}()
push!(configs, zeros(Int, num_vars))
push!(configs, ones(Int, num_vars))
while length(configs) < min(10, 2^num_vars)
push!(configs, [rand(0:1) for _ in 1:num_vars])
end
configs = collect(configs)

# Evaluate configs
evals = []
for cfg in configs
dist = bmf_evaluate(A, m, n, k, cfg)
# All configs are valid for BMF; size = hamming distance
push!(evals, Dict("config" => cfg, "is_valid" => true, "size" => dist))
end

# Brute force: find all best (minimize hamming distance)
best_dist = typemax(Int)
best_configs = Vector{Int}[]
for bits in 0:(2^num_vars - 1)
cfg = [(bits >> i) & 1 for i in 0:num_vars-1]
dist = bmf_evaluate(A, m, n, k, cfg)
if dist < best_dist
best_dist = dist
best_configs = [cfg]
elseif dist == best_dist
push!(best_configs, cfg)
end
end

return Dict(
"label" => label,
"instance" => inst,
"evaluations" => evals,
"best_solutions" => best_configs,
)
end

# ── reduction exports ────────────────────────────────────────────────

function export_reduction(source, target_type, source_label)
Expand Down Expand Up @@ -306,6 +465,7 @@ end
# ── main ─────────────────────────────────────────────────────────────

function main()
Random.seed!(42) # pin seed so re-runs produce identical fixtures
println("Generating Julia parity test data...")

# ── Build test instances (matching Julia test/rules/rules.jl) ──
Expand Down Expand Up @@ -370,6 +530,16 @@ function main()
# SetCovering docstring
doc_sc = SetCovering([[1, 2, 3], [2, 4], [1, 4]], [1, 2, 3])

# BicliqueCover: 6-vertex bipartite graph, 2 bicliques (from Julia test)
doc_bc_graph = SimpleGraph(6)
for (i,j) in [(1,5), (1,4), (2,5), (2,4), (3,6)]
add_edge!(doc_bc_graph, i, j)
end
doc_bc = BicliqueCover(doc_bc_graph, [1,2,3], 2)

# BMF: 3x3 all-ones matrix, rank 2 (from Julia test)
doc_bmf = BinaryMatrixFactorization(trues(3, 3), 2)

# ── Individual rule test instances (from test/rules/*.jl) ──
rule_graph4 = SimpleGraph(Graphs.SimpleEdge.([(1, 2), (1, 3), (3, 4), (2, 3)]))

Expand Down Expand Up @@ -547,6 +717,16 @@ function main()
export_setcovering(doc_sc, "doc_3subsets"),
]))

# BicliqueCover
write_fixture("biclique_cover.json", model_fixture("BicliqueCover", [
export_biclique_cover(doc_bc_graph, [1,2,3], 2, "doc_6vertex"),
]))

# BMF
write_fixture("bmf.json", model_fixture("BMF", [
export_bmf(trues(3, 3), 2, "doc_3x3_ones"),
]))

# ── Export reduction fixtures ──
println("Exporting reduction fixtures...")

Expand Down Expand Up @@ -622,6 +802,62 @@ function main()
write_fixture(filename, data)
end

# ── Export reduction path fixtures (deterministic, skip if already exist) ──
println("Exporting reduction path fixtures...")

g = reduction_graph()

if !isfile(joinpath(OUTDIR, "path_maxcut_to_spinglass.json"))
# MaxCut → SpinGlass path
mc_source = MaxCut(smallgraph(:petersen))
mc_paths = reduction_paths(g, MaxCut, SpinGlass)
mc_res = reduceto(mc_paths[1], mc_source)
mc_best_source = findbest(mc_source, BruteForce())
mc_best_target = findbest(target_problem(mc_res), BruteForce())
mc_extracted = sort(unique(extract_solution.(Ref(mc_res), mc_best_target)))

write_fixture("path_maxcut_to_spinglass.json", Dict(
"path" => string.(typeof.(mc_paths[1].nodes)),
"best_source" => mc_best_source,
"best_target" => mc_best_target,
"extracted" => mc_extracted,
))

# MaxCut → QUBO path (uses same mc_source/mc_best_source)
mc_qubo_paths = reduction_paths(g, MaxCut, QUBO)
mc_qubo_res = reduceto(mc_qubo_paths[1], mc_source)
mc_qubo_best_target = findbest(target_problem(mc_qubo_res), BruteForce())
mc_qubo_extracted = sort(unique(extract_solution.(Ref(mc_qubo_res), mc_qubo_best_target)))

write_fixture("path_maxcut_to_qubo.json", Dict(
"path" => string.(typeof.(mc_qubo_paths[1].nodes)),
"best_source" => mc_best_source,
"extracted" => mc_qubo_extracted,
))
else
println(" skipping path_maxcut_to_*.json (already exist)")
end

if !isfile(joinpath(OUTDIR, "path_factoring_to_spinglass.json"))
# Factoring → SpinGlass path (slow BruteForce on SpinGlass target)
fact = Factoring(2, 1, 3)
fact_paths = reduction_paths(g, Factoring, SpinGlass)
fact_res = reduceto(fact_paths[1], fact)
fact_best_target = findbest(target_problem(fact_res), BruteForce())
fact_extracted = sort(unique(filter(
sol -> solution_size(fact, sol) == SolutionSize(0, true),
extract_solution.(Ref(fact_res), fact_best_target)
)))

write_fixture("path_factoring_to_spinglass.json", Dict(
"path" => string.(typeof.(fact_paths[1].nodes)),
"best_source" => findbest(fact, BruteForce()),
"extracted" => fact_extracted,
))
else
println(" skipping path_factoring_to_spinglass.json (already exists)")
end

println("Done! Generated fixtures in $OUTDIR")
end

Expand Down
Loading
Loading