Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
309c562
feat: SetSplitting to NAE-SAT clause-per-subset reduction (#1100)
peda-cos Oct 12, 2026
ccad796
fix: account for incongruence search period in complexity
isPANN Oct 22, 2026
55823fb
Merge remote-tracking branch 'origin/feat/graph-explorer-overhead-str…
isPANN Oct 22, 2026
e46a40e
Add direct QUBO reductions for scheduling and assignment
isPANN Oct 22, 2026
37aa878
Reject source arithmetic overflow in direct QUBO reductions
isPANN Oct 22, 2026
ecc0ad5
Add verified Phase 2 direct QUBO reductions
isPANN Oct 22, 2026
01f6e3a
Add conditional Phase 3 direct QUBO reductions
isPANN Oct 22, 2026
109985f
Defer direct QUBO rules with unresolved target timeouts
isPANN Oct 22, 2026
0dd7448
Add verified direct decision QUBO reduction for integer staff scheduling
isPANN Oct 22, 2026
3d3b020
Tighten direct QUBO bounds and replace costly encodings
isPANN Oct 23, 2026
c6bfae6
Add binary decision ILP to SAT reduction
isPANN Oct 23, 2026
0daa9a8
Fix solution length validation and remove redundant test scaffolding
isPANN Oct 23, 2026
4da646d
Count ILP nonzeros per constraint family instead of rows times columns
isPANN Oct 27, 2026
f95447a
Check ILP nonzero bounds on dense sources
isPANN Oct 27, 2026
4ce1d38
Bound scheduling QUBO sizes by worker bits and start windows
isPANN Oct 27, 2026
0801132
Restate StaffScheduling and SWRTD QUBO sizes in the paper and tests
isPANN Oct 27, 2026
8523caa
Reject negative ConsecutiveOnesSubmatrix bounds at construction
isPANN Oct 27, 2026
3de4a67
Declare exact parameter counts the constructions always produce
isPANN Oct 27, 2026
b8867cf
Keep 3-SAT literal-dependent counts as upper bounds
isPANN Oct 27, 2026
0318bfd
Test exact construction counts on degenerate graphs
isPANN Oct 28, 2026
f7be538
Reduce negative-weight MaxCut to MinimumMatrixCover with an edge gadget
isPANN Oct 28, 2026
ef18c76
Simplify the MaxCut gadget, retype the COS bound, and clean up docs
isPANN Oct 28, 2026
5e512c7
Remove the KSatisfiability KN variant and short-clause constructors
isPANN Oct 28, 2026
86d6808
Declare MonochromaticTriangle counts exact and drop stale short-claus…
isPANN Oct 28, 2026
eafd02a
Bound KSatisfiability by FixedK and tidy the K-SAT reductions
isPANN Oct 28, 2026
3c2f9bd
Drop the FixedK trait
isPANN Oct 28, 2026
3898a42
Merge current main before SetSplitting review fixes
GiggleLiu Oct 29, 2026
40a445d
Fix SetSplitting NAE bounds and checked construction
GiggleLiu Oct 29, 2026
9b281d7
Align K3 reduction proofs with exact-width clauses
GiggleLiu Oct 29, 2026
ce0da09
Integrate reviewed scheduling bounds into safe reduction stack
GiggleLiu Oct 29, 2026
c90f206
Integrate reviewed graph explorer fixes and current main into safe stack
GiggleLiu Oct 29, 2026
4810b63
Integrate reviewed SetSplitting NAE reduction into safe stack
GiggleLiu Oct 29, 2026
2226ebe
Reject unrepresentable interval QUBO allocations
GiggleLiu Oct 29, 2026
c5c05ee
Integrate tested interval QUBO allocation guard
GiggleLiu Oct 29, 2026
ffd9805
Merge pull request #1195 from peda-cos/fix-set-splitting-naesat-1100
GiggleLiu Oct 29, 2026
3571ec1
Merge remote-tracking branch 'origin/main' into HEAD
GiggleLiu Oct 29, 2026
52bd171
Check matrix-cover objective range before reduction
GiggleLiu Oct 29, 2026
8ab56f2
Integrate checked matrix-cover objective bounds
GiggleLiu Oct 29, 2026
affe211
Align comparison fixture with integrated route certificates
GiggleLiu Oct 29, 2026
abb96e7
Fix website wording checks and patched URI dependency
GiggleLiu Oct 29, 2026
4e5b346
Keep unavailable growth UI coverage independent of catalog bounds
GiggleLiu Oct 29, 2026
3372d66
Merge pull request #1225 from CodingThrust/refactor/remove-ksat-kn
GiggleLiu Oct 29, 2026
5cab940
Merge pull request #1224 from CodingThrust/feat/exact-tight-bounds
GiggleLiu Oct 29, 2026
b4a4e06
Merge pull request #1222 from CodingThrust/fix/ilp-nonzeros-bounds
GiggleLiu Oct 29, 2026
b84842c
Merge pull request #1223 from CodingThrust/fix/qubo-scheduling-bounds
GiggleLiu Oct 29, 2026
2cd3218
Merge pull request #1218 from CodingThrust/feat/binary-ilp-to-sat
GiggleLiu Oct 29, 2026
5e137d3
Merge pull request #1217 from CodingThrust/issue-1213-direct-qubo-phase4
GiggleLiu Oct 29, 2026
5cd4819
Merge pull request #1216 from CodingThrust/issue-1213-direct-qubo-phase3
GiggleLiu Oct 29, 2026
004ad17
Merge pull request #1215 from CodingThrust/issue-1213-direct-qubo-phase2
GiggleLiu Oct 29, 2026
5c79557
Merge pull request #1214 from CodingThrust/feat/direct-scheduling-qubo
GiggleLiu Oct 29, 2026
59d5ea0
Merge branch 'fix/simultaneous-incongruences-complexity' of https://g…
GiggleLiu Oct 29, 2026
da5f7f7
Render Typst math vectors without filtering and isolate provenance ch…
GiggleLiu Oct 29, 2026
2fc8669
Merge commit 'da5f7f74e1f7c16edde8a679fc5f94fbf5e0f94d' into HEAD
GiggleLiu Oct 29, 2026
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
606 changes: 557 additions & 49 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 @@ -2332,3 +2332,14 @@ @article{axler2019
year = {2019},
url = {https://www.maths.tcd.ie/EMIS/journals/JIS/VOL22/Axler/axler17.pdf}
}

@article{een2006,
author = {Niklas E{\'e}n and Niklas S{\"o}rensson},
title = {Translating Pseudo-Boolean Constraints into {SAT}},
journal = {Journal on Satisfiability, Boolean Modeling and Computation},
year = {2006},
volume = {2},
number = {1--4},
pages = {1--26},
doi = {10.3233/SAT190014},
}
4 changes: 4 additions & 0 deletions docs/website/src/styles/details.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
.typst-detail h3 { margin: 24px 0 10px; font-size: 17px; font-weight: 600; }
.typst-detail p { margin: 10px 0; }
.typst-detail svg { max-width: 100%; filter: invert(.9); }
/* Direct glyph colors avoid WebKit rasterizing small filtered math into blocks. */
.typst-detail .typst-math > svg { filter: none; }
.typst-detail .typst-math [fill="#000000"] { fill: var(--ink); }
.typst-detail .typst-math [stroke="#000000"] { stroke: var(--ink); }
.typst-math > svg { display: inline-block !important; vertical-align: baseline; }
div.typst-math { overflow-x: auto; margin: 14px 0; }
.typst-detail figure { margin: 20px 0; }
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,17 +507,17 @@ mod tests {
let cli = Cli::try_parse_from([
"pred",
"create",
"3SAT",
"2SAT",
"--num-vars",
"3",
"--clauses",
"1,2,3",
"1,2",
])
.expect("variant alias parses");
let Commands::Create(args) = cli.command else {
panic!("expected create command");
};
assert_eq!(args.problem.as_deref(), Some("KSatisfiability/K3"));
assert_eq!(args.problem.as_deref(), Some("KSatisfiability/K2"));
assert_eq!(args.raw("num-vars"), Some("3"));
}

Expand Down
20 changes: 4 additions & 16 deletions problemreductions-cli/src/commands/create/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@ fn test_create_path_constrained_network_flow_rejects_invalid_paths() {
}

#[test]
fn test_create_staff_scheduling_reports_invalid_schedule_without_panic() {
fn test_create_staff_scheduling_rejects_ragged_schedule() {
let cli = Cli::try_parse_from([
"pred",
"create",
Expand Down Expand Up @@ -775,13 +775,7 @@ fn test_create_staff_scheduling_reports_invalid_schedule_without_panic() {
auto_json: false,
};

let result = std::panic::catch_unwind(|| create(&args, &out));
assert!(result.is_ok(), "create should return an error, not panic");
let err = result.unwrap().unwrap_err().to_string();
assert!(
err.contains("All rows") || err.contains("schedule 1 has 6 periods, expected 7"),
"expected row-length validation error, got: {err}"
);
assert!(create(&args, &out).is_err());
}

#[test]
Expand Down Expand Up @@ -954,7 +948,7 @@ fn test_create_timetable_design_outputs_problem_json() {
}

#[test]
fn test_create_timetable_design_reports_invalid_matrix_without_panic() {
fn test_create_timetable_design_rejects_invalid_matrix() {
let cli = Cli::try_parse_from([
"pred",
"create",
Expand Down Expand Up @@ -986,13 +980,7 @@ fn test_create_timetable_design_reports_invalid_matrix_without_panic() {
auto_json: false,
};

let result = std::panic::catch_unwind(|| create(&args, &out));
assert!(result.is_ok(), "create should return an error, not panic");
let err = result.unwrap().unwrap_err().to_string();
assert!(
err.contains("--craftsman-avail"),
"expected timetable matrix validation error, got: {err}"
);
assert!(create(&args, &out).is_err());
}

#[test]
Expand Down
29 changes: 19 additions & 10 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ fn path_text_preserves_variant_conversion_steps() {

#[test]
fn polynomial_comparison_default_layout_and_opt_in_proofs() {
let args = ["path", "MIS", "QUBO", "--compare", "--limit", "3"];
// The first two clique routes are equivalent; set-packing and feedback-arc routes add comparisons.
let args = ["path", "MIS", "QUBO", "--compare", "--limit", "4"];
let output = pred().args(args).output().unwrap();
assert!(output.status.success());
let text = String::from_utf8(output.stdout).unwrap();
Expand All @@ -63,14 +64,14 @@ fn polynomial_comparison_default_layout_and_opt_in_proofs() {
assert!(text.contains("Path 1") && text.contains("num_vars"));
assert!(!text.contains("[upper bound]") && !text.contains("[exact]"));
assert!(text[pairs..unresolved]
.contains("0 dominated, 1 equivalent, 2 proven incomparable, 0 unresolved"));
.contains("2 dominated, 1 equivalent, 3 proven incomparable, 0 unresolved"));
assert!(!text.contains("worst case:") && !text.contains("Generic candidate front"));
assert!(text.contains("Paths 1, 2: proven incomparable"));
assert!(text.contains("Paths 2, 3: Path 2 ≡ Path 3"));
assert!(text.contains("Paths 1, 3: proven incomparable"));
assert!(text.contains("Paths 1, 2: Path 1 ≡ Path 2"));
assert!(
text.contains("Ignores exact-cancellation families") && text.contains("bounded strata")
);
assert!(text.contains("2 proven incomparable, 0 unresolved"));
assert!(text.contains("3 proven incomparable, 0 unresolved"));
assert!(
!text.contains("Field proofs") && !text.contains("Witness {") && !text.contains("f/g → ∞")
);
Expand All @@ -79,8 +80,8 @@ fn polynomial_comparison_default_layout_and_opt_in_proofs() {
assert!(output.status.success());
let text = String::from_utf8(output.stdout).unwrap();
assert!(
text.contains("Paths 1, 2: proven incomparable")
&& text.contains("Paths 2, 3: Path 2 ≡ Path 3")
text.contains("Paths 1, 3: proven incomparable")
&& text.contains("Paths 1, 2: Path 1 ≡ Path 2")
);
assert!(text.contains("Field proofs") && text.contains("f/g → ∞"));
assert!(text.contains("f ≡ g after expansion"));
Expand All @@ -89,17 +90,25 @@ fn polynomial_comparison_default_layout_and_opt_in_proofs() {
let output = pred().args(args).arg("--json").output().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
for (route, step, name) in [
(0, 0, "MaximumClique"),
(1, 1, "MaximumClique"),
(2, 1, "MaximumSetPacking"),
(3, 2, "MinimumFeedbackArcSet"),
] {
assert_eq!(json["paths"][route]["path"][step]["to"]["name"], name);
}
let view = &json["polynomial_comparison"];
assert_eq!(view["schema_version"], 5);
assert_eq!(view["mode"], "generic");
assert_eq!(view["counts"]["Incomparable"], 2);
assert_eq!(view["counts"]["Incomparable"], 3);
assert!(view.get("generic_counts").is_none());
assert!(view.get("generic_candidate_front").is_none());
assert!(view["pairs"][0]["fields"]["num_vars"]["forward"]["certificate"].is_object());
let identity = &view["pairs"][2]["fields"]["num_vars"]["forward"]["certificate"];
let identity = &view["pairs"][0]["fields"]["num_vars"]["forward"]["certificate"];
assert_eq!(identity["kind"], "identity");
assert_eq!(identity.as_object().unwrap().len(), 1);
assert!(view["pairs"][2]["fields"]["num_vars"]
assert!(view["pairs"][0]["fields"]["num_vars"]
.get("generic")
.is_none());
let output = pred()
Expand Down
32 changes: 0 additions & 32 deletions problemreductions-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,38 +1328,6 @@ mod tests {
assert_eq!(error.to_string(), "duplicate `create` clause");
}

#[test]
fn declare_variants_generates_model_dispatch_without_solver_dispatch() {
let input: DeclareVariantsInput = syn::parse_quote! {
default Foo => "1",
};
let tokens = generate_declare_variants(&input).unwrap().to_string();
assert!(tokens.contains("factory :"), "expected factory field");
assert!(
tokens.contains("serialize_fn :"),
"expected serialize_fn field"
);
assert!(!tokens.contains("solve_typed_fn :"));
assert!(!tokens.contains("solve_fn :"));
assert!(
!tokens.contains("factory : None"),
"factory should not be None"
);
assert!(
!tokens.contains("serialize_fn : None"),
"serialize_fn should not be None"
);
assert!(!tokens.contains("find_cartesian"));
assert!(
!tokens.contains("find_best"),
"did not expect legacy find_best in tokens"
);
assert!(
!tokens.contains("SolutionSize :: Invalid"),
"did not expect legacy invalid fallback in tokens"
);
}

#[test]
fn reduction_rejects_unexpected_attribute() {
let extra_attr = syn::Ident::new("extra", proc_macro2::Span::call_site());
Expand Down
79 changes: 72 additions & 7 deletions scripts/test_website.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,45 @@ def test_graph_growth_labels_are_conservative_for_exact_signed_formulas(self):
expect(self.page.locator('.path')).to_have_count(2)
expect(self.page.get_by_role('button', name='Hide other routes', exact=True)).to_be_visible()

def test_typst_math_keeps_vector_glyphs_and_colors_in_both_themes(self):
self.visit('problem/KSatisfiability?variant=k=K3')
expect(self.page.locator('article.typst-detail')).to_be_visible()
formula = self.page.locator('.typst-math > svg').filter(has=self.page.locator('g + g use')).first
expect(formula).to_be_visible()
self.page.evaluate('document.fonts.ready')
# Include an intentional color and a fraction-like stroke alongside the real Typst glyphs.
formula.locator('use').first.evaluate("node => node.setAttribute('fill', '#e15759')")
formula.evaluate('''svg => {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
line.setAttribute('d', 'M0 .1H4'); line.setAttribute('stroke', '#000000');
line.setAttribute('stroke-width', '.1'); svg.append(line);
}''')
for theme, ink, paper in [('dark', '#eef3e9', '#0b110d'), ('light', '#112011', '#ffffff')]:
with self.subTest(theme=theme):
self.page.evaluate('''({ink, paper}) => {
document.documentElement.style.setProperty('--ink', ink);
document.documentElement.style.setProperty('--paper', paper);
}''', {'ink': ink, 'paper': paper})
expect(formula.locator('use').first).to_have_css('fill', 'rgb(225, 87, 89)')
actual = formula.screenshot()
# Reference the same vectors with explicit theme paint and no rasterizing filter.
saved = formula.evaluate('''(svg, ink) => {
const painted = [...svg.querySelectorAll('[fill="#000000"], [stroke="#000000"]')];
const saved = [svg.getAttribute('style'), painted.map(node => node.getAttribute('style'))];
svg.style.filter = 'none';
for (const node of painted) {
if (node.getAttribute('fill') === '#000000') node.style.fill = ink;
if (node.getAttribute('stroke') === '#000000') node.style.stroke = ink;
}
return saved;
}''', ink)
self.assertEqual(actual, formula.screenshot(), 'Theme styling changed the rendered math vectors')
formula.evaluate('''(svg, [style, paints]) => {
const restore = (node, value) => value === null ? node.removeAttribute('style') : node.setAttribute('style', value);
restore(svg, style);
[...svg.querySelectorAll('[fill="#000000"], [stroke="#000000"]')].forEach((node, i) => restore(node, paints[i]));
}''', saved)

def test_accessibility_atlas(self):
self.visit('atlas')
expect(self.page.locator('.atlas-row').first).to_be_visible()
Expand Down Expand Up @@ -390,7 +429,7 @@ def test_graph_place_card_hands_a_reachable_problem_to_directions(self):
self.page.locator('.act.primary').click()
self.page.locator('#target').fill('QUBO')
self.page.locator('#suggest button', has_text='QUBO').first.click()
expect(self.page.locator('#results .summary strong')).to_contain_text('best route')
expect(self.page.locator('#results .summary strong')).to_contain_text('size-bound candidate')

def test_accessibility_graph_desktop_drawer(self):
self.page.goto(self.base + 'graph.html#directions')
Expand Down Expand Up @@ -525,7 +564,7 @@ def test_explore_defers_path_engine_until_directions(self):
expect(self.page.locator('.path')).to_have_count(0)
self.choose_endpoint('source', 'SAT')
self.choose_endpoint('target', 'QUBO')
expect(self.page.locator('#results .summary strong')).to_contain_text('best route')
expect(self.page.locator('#results .summary strong')).to_contain_text('size-bound candidate')
route = self.copy_route()
self.assertEqual(route['path'][0]['from']['name'], 'Satisfiability')
self.assertEqual(route['path'][-1]['to']['name'], 'QUBO')
Expand Down Expand Up @@ -1026,12 +1065,38 @@ def test_graph_sat_size_badges_finish_promptly_after_routes_arrive(self):
expect(self.page.locator('.path').first.locator('.size')).to_contain_text('n')

def test_graph_unavailable_parameters_keep_size_badges_unknown(self):
# Keep unavailable-field UI coverage independent of improvements to registry bounds.
nodes = [{'name': name, 'category': 'formula', 'variant': {}, 'default': True}
for name in ['Satisfiability', 'QUBO']]
known = {'certificate': 'num_vars^3', 'accuracy': {'kind': 'upper_bound'}}
unavailable = {'certificate': None, 'accuracy': {'kind': 'unavailable',
'reason': {'kind': 'unavailable', 'detail': 'Synthetic unavailable parameter'}}}
fields = {'num_vars': known, 'num_quadratic_terms': unavailable}
scene = {'nodes': nodes, 'edges': [{'source': 0, 'target': 1, 'witness': True,
'aggregate': False, 'turing': False, 'fields': fields}], 'aliases': {},
'layout': {'Satisfiability': {'x': 0, 'y': 0}, 'QUBO': {'x': 100, 'y': 70}}}
self.page.route('**/graph-scene.json.gz', lambda route: route.fulfill(
body=gzip.compress(json.dumps(scene).encode()), content_type='application/gzip'))
endpoint = lambda i: {'name': nodes[i]['name'], 'variant': {}}
paths = {'truncated': False, 'paths': [{'steps': 1,
'path': [{'from': endpoint(0), 'to': endpoint(1)}]}]}
comparison = {'profiles': [{'source_coordinates': ['num_vars'], 'fields': fields}],
'front': [1], 'counts': {'Dominated': 0, 'Equivalent': 0, 'Incomparable': 0, 'Unknown': 0}}
worker = 'self.onmessage = () => {' + ''.join('self.postMessage(' + json.dumps(reply) + ');'
for reply in [{'phase': 'paths', 'result': paths}, {'phase': 'comparison', 'result': comparison}]) + '};'
self.page.route('**/path-worker.js', lambda route: route.fulfill(body=worker, content_type='text/javascript'))
self.page.goto(self.base + 'graph.html#directions')
self.choose_endpoint('source', 'MVC')
self.page.locator('#source-options [aria-label="weight"]').get_by_role('button', name='One', exact=True).click()
self.choose_endpoint('target', 'LongestCommonSubsequence')
expect(self.page.locator('#results .summary strong')).to_contain_text('size-bound candidate', timeout=3000)
expect(self.page.locator('.path').first.locator('.size')).to_have_text('?', timeout=500)
expect(self.page.locator('#results .summary strong')).to_have_text('1 size-bound candidate')
expect(self.page.locator('.size.pending')).to_have_count(0)
expect(self.page.locator('.path .size')).to_have_text('?')
expect(self.page.locator('.path .size')).to_have_attribute('title', 'Size unavailable for one or more parameters')
summaries = self.page.locator('.growth').evaluate('''dl => Object.fromEntries(
[...dl.querySelectorAll('dt')].map(term=>[term.textContent,term.nextElementSibling.textContent]))''')
self.assertEqual(summaries, {'vars': 'O(vars³)', 'quadratic terms': 'unavailable'})
expect(self.page.locator('.steps .rule-link .oh')).to_have_text('?')
edge = self.page.locator('.scene-edge[data-edge="Satisfiability→QUBO"]')
expect(edge).to_have_count(1)
self.assertTrue({'route', 'e-unknown'} <= set(edge.get_attribute('class').split()))

def test_directions_preloads_engine_before_graph_initialization(self):
self.page.goto(self.base + 'graph.html#directions')
Expand Down
9 changes: 8 additions & 1 deletion scripts/test_website_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sys
import tempfile
import unittest
from unittest.mock import patch
from urllib.request import build_opener, ProxyHandler
from urllib.parse import urlsplit

Expand Down Expand Up @@ -74,7 +75,13 @@ def test_canceled_asset_request_does_not_print_a_server_traceback(self):
def test_verification_only_report_does_not_claim_the_artifact_build_source(self):
with tempfile.TemporaryDirectory() as directory:
site = Path(directory)
with self.assertRaises(FileNotFoundError):
# This case checks failed-artifact provenance; the full verifier exercises these tools.
run = subprocess.run
def skip_tool_checks(command, **kwargs):
if command[:2] in [["cargo", "test"], ["npm", "run"]]:
return subprocess.CompletedProcess(command, 0)
return run(command, **kwargs)
with patch("check_website.subprocess.run", side_effect=skip_tool_checks), self.assertRaises(FileNotFoundError):
check_website.check(site, build=False, open_questions=None)
report = json.loads((site / "verification.json").read_text())
self.assertEqual(report["verification"]["status"], "failed")
Expand Down
Loading