diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 63928ab40..438584ded 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -25,7 +25,7 @@ These repo-local skills live under `.claude/skills/*/SKILL.md`. - [review-quality](skills/review-quality/SKILL.md) -- Generic code quality review: DRY, KISS, cohesion/coupling, test quality, HCI. Read-only, no code changes. Called by `review-pipeline`. - [fix-pr](skills/fix-pr/SKILL.md) -- Resolve PR review comments, fix CI failures, and address codecov coverage gaps. Uses `gh api` for codecov (not local `cargo-llvm-cov`). - [write-model-in-paper](skills/write-model-in-paper/SKILL.md) -- Write or improve a problem-def entry in the Typst paper (standalone, for improving existing entries). Core instructions are inlined in `add-model` Step 6. -- [write-rule-in-paper](skills/write-rule-in-paper/SKILL.md) -- Write or improve a reduction-rule entry in the Typst paper (standalone, for improving existing entries). Core instructions are inlined in `add-rule` Step 5. +- [write-rule-in-paper](skills/write-rule-in-paper/SKILL.md) -- Write or improve a reduction-rule entry in the Typst paper (standalone, for improving existing entries). Core instructions are inlined in `add-rule` Step 6. - [release](skills/release/SKILL.md) -- Create a new crate release. Determines version bump from diff, verifies tests/clippy, then runs `make release`. - [check-issue](skills/check-issue/SKILL.md) -- Quality gate for `[Rule]` and `[Model]` issues. Checks usefulness, non-triviality, correctness of literature, and writing quality. Posts structured report and adds failure labels. - [fix-issue](skills/fix-issue/SKILL.md) -- Fix quality issues found by check-issue — auto-fixes mechanical problems, brainstorms substantive issues with human, then re-checks and moves to Ready. @@ -59,7 +59,7 @@ make fmt-check # Check code formatting make clippy # Run clippy lints make doc # Build mdBook documentation (includes reduction graph export) make mdbook # Build and serve mdBook with live reload -make paper # Build Typst paper from checked-in example fixtures +make paper # Generate example data and build the Typst paper make coverage # Generate coverage report (>95% required) make check # Quick pre-commit check (fmt + clippy + test) make rust-export # Generate Julia parity test data (mapping stages) @@ -107,7 +107,7 @@ make papers-pull # Pull PDFs from shared remote - Run `pred list` for the full catalog of problems, variants, and reductions; `pred show ` for details on a specific problem - `src/rules/` - Reduction rules + inventory registration - `src/models/decision.rs` - Generic `Decision

` wrapper converting optimization problems to decision problems -- `src/solvers/` - BruteForce solver for aggregate values plus witness recovery when supported, ILP solver (feature-gated, witness-only), decision search (binary search via Decision queries). To check if a problem supports ILP solving via a witness-capable reduction path, run `pred path ILP` +- `src/solvers/` - BruteForce reference solver returning problem solutions, ILP solver (feature-gated), decision search (binary search via Decision queries), and the exact-variant solver capability registry. Solver dispatch uses only registered customized implementations and fixed ILP pipelines; reduction-graph reachability does not imply solver availability. Run `pred inspect ` to see the registered capabilities for that instance. - `src/traits.rs` - `Problem` trait - `src/rules/traits.rs` - `ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult` traits - `src/registry/` - Compile-time reduction metadata collection @@ -124,19 +124,26 @@ make papers-pull # Pull PDFs from shared remote Problem (core trait — all problems must implement) │ ├── const NAME: &'static str // e.g., "MaximumIndependentSet" -├── type Value: Clone // aggregate value: Max/Min/Sum/Or/And/Extremum/... -├── fn dims(&self) -> Vec // config space: [2, 2, 2] for 3 binary variables -├── fn evaluate(&self, config) -> Value -├── fn variant() -> Vec<(&str, &str)> // e.g., [("graph","SimpleGraph"), ("weight","i32")] -├── fn num_variables(&self) -> usize // default: dims().len() +├── type Solution // mathematical witness representation +├── type Value: Clone // per-solution evaluation value +├── fn parameter_names() // canonical problem-owned parameter schema +├── fn parameters(&self) -> ProblemParameters // concrete instance parameter values +├── fn evaluate(&self, solution) -> Result +├── fn variant() -> Vec<(&str, &str)> // e.g., [("graph","SimpleGraph"), ("weight","i64")] └── fn problem_type() -> ProblemType // catalog bridge: registry lookup by NAME ``` -**Witness-capable objective problems** (e.g., `MaximumIndependentSet`) typically use `Value = Max`, `Min`, or `Extremum`. +`BruteForceProblem` is a separate reference-solver capability. Its +`dimensions()` method describes only the finite Cartesian coordinate space used +by the registered brute-force implementation. -**Witness-capable feasibility problems** (e.g., `Satisfiability`) typically use `Value = Or`. +**Objective problems** (e.g., `MaximumIndependentSet`) typically use `Value = Max`, `Min`, or `Extremum`. -**Aggregate-only problems** use fold values such as `Sum` or `And`; these solve to a value but have no representative witness configuration. +**Feasibility problems** (e.g., `Satisfiability`) typically use `Value = Or`. + +A successful `Problem` solve always returns `Problem::Solution`. Global counting +or statistics without a representative solution are not modeled as `Problem` +solves. **Decision problems** wrap an optimization problem with a bound: `Decision

` where `P::Value: OptimizationValue`. Evaluates to `Or(true)` when the inner objective meets the bound (≤ for Min, ≥ for Max). @@ -150,36 +157,40 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `Max`: meets bound when value ≥ bound ### Key Patterns +- Keep failure phases typed and separate: public construction paths return `ConstructionError`, `Problem::evaluate()` returns `EvaluationError`, and reduction paths return `ReductionError`. A reduction preserves target construction failures as `ReductionError::Construction`; none of these paths returns or creates an error as a bare `String`. - `variant_params!` macro implements `Problem::variant()` — e.g., `crate::variant_params![G, W]` for two type params, `crate::variant_params![]` for none (see `src/variant.rs`) -- `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed load/serialize/value-solve/witness-solve metadata. One entry per problem may be marked `default`, and variable names in complexity strings are validated at compile time against actual getter methods. +- `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed load/serialize/solution-solve metadata. One entry per problem may be marked `default`, and variable names in complexity strings are validated against the problem-owned parameter schema. Ordinary models are constructed directly from their construction schema. When user-facing construction differs from persisted JSON, define a model-local `#[derive(CreateSpec)]` DTO plus `TryFrom`, use its generated `FIELDS` in `ProblemSchemaEntry`, and register it with `create LocalSpec`; never add model-name branches in CLI or MCP code. - `decision_problem_meta!` macro registers `DecisionProblemMeta` for a concrete inner type, providing the `DECISION_NAME` constant. -- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (aggregate Decision→Opt + Turing Opt→Decision) for a `Decision

` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision

` before invoking. Accepts `dims`, `fields`, and `size_getters` parameters for problem-specific size fields. +- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (aggregate Decision→Opt + Turing Opt→Decision) for a `Decision

` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision

` before invoking. Accepts an explicit structural `category` plus `dims`, `fields`, and `parameter_getters` parameters for problem-specific parameters. - Problems parameterized by graph type `G` and optionally weight type `W` (problem-dependent) -- `Solver::solve()` computes the aggregate value for any `Problem` whose `Value` implements `Aggregate` -- `BruteForce::find_witness()` / `find_all_witnesses()` recover witnesses only when `P::Value::supports_witnesses()` +- `BruteForce::solve()` returns `Result, SolveError>`; `None` means exhaustive search proved infeasibility +- `BruteForce::find_all_witnesses()` is a reference-testing helper for collecting every optimal or satisfying solution - `ReductionResult` provides `target_problem()` and `extract_solution()` for witness/config workflows; `AggregateReductionResult` provides `extract_value()` for aggregate/value workflows +- Every direct `extract_solution()` must call `validate_target_solution()` once before decoding; composed extractors delegate validation to the first direct decoder. +- Decode only the reduction's defined mathematical mapping. Reject malformed structure with `ExtractionError`; never panic, truncate, clamp, invent defaults, or add recovery branches. Explicit mathematical alternatives and sentinels are allowed. Test successful decoding and every rejected representation. - CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`) - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph -- Weight types: `One` (unit weight marker), `i32`, `f64` — all implement `WeightElement` trait +- Weight types: `One` (unit weight marker), `i64`, `f64` — all implement `WeightElement` trait - `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type - Weight management via inherent methods (`weights()`, `set_weights()`, `is_weighted()`), not traits - `NumericSize` supertrait bundles common numeric bounds (`Clone + Default + PartialOrd + Num + Zero + Bounded + AddAssign + 'static`) -### Overhead System -Reduction overhead is expressed using `Expr` AST (in `src/expr.rs`) with the `#[reduction]` macro. The `overhead` attribute is **required** — omitting it is a compile error: +### Parameter Relations +Each reduction declares one rule-level parameter relation using the `Expr` AST in `src/expr.rs`. The `transform` declaration is required: ```rust -#[reduction(overhead = { +#[reduction(transform = upper_bound { num_vertices = "num_vertices + num_clauses", num_edges = "3 * num_clauses", })] impl ReduceTo for Source { ... } ``` - Expression strings are parsed at compile time by a Pratt parser in the proc macro crate -- Variable names are validated against actual getter methods on the source type — typos cause compile errors -- Each problem type provides inherent getter methods (e.g., `num_vertices()`, `num_edges()`) that the overhead expressions reference -- **Overhead expressions describe scaling (asymptotic upper bounds), not exact sizes.** To determine the actual target problem size for a specific instance, read the `reduce_to()` construction code and count the actual variables/constraints/vertices built. -- `ReductionOverhead` stores `Vec<(&'static str, Expr)>` — field name to symbolic expression mappings -- `ReductionEntry` has both symbolic (`overhead_fn`) and compiled (`overhead_eval_fn`) evaluation — the compiled version calls getters directly +- Variable names are validated against the source problem's canonical parameter schema +- Use `transform = exact { ... }` when every formula is an equality and `transform = upper_bound { ... }` when every formula is only an upper bound. One expression block cannot mix relations. +- Use `transform = unavailable { ... }` when no formula is representable, or an auxiliary `unavailable = { ... }` block for omitted target parameters. +- Every target parameter must appear exactly once as a formula or as unavailable with a non-empty reason. +- `ParameterTransform` evaluates and composes formulas with exact rational and arbitrary-precision integer arithmetic. Unsafe upper-bound composition becomes unavailable; it never performs budget pruning or path ranking. +- Concrete instance parameters come from each endpoint instance's `Problem::parameters()` implementation; `ReductionEntry` stores only the symbolic parameter relation. - `VariantEntry` has both a complexity string and compiled `complexity_eval_fn` — same pattern - Expressions support: constants, variables, `+`, `-`, `*`, `/`, `^`, `exp()`, `log()`, `sqrt()`, `factorial()` - Complexity strings must use **concrete numeric values only** (e.g., `"2^(2.372 * num_vertices / 3)"`, not `"2^(omega * num_vertices / 3)"`) @@ -190,32 +201,53 @@ Problem types use explicit optimization prefixes (`Maximum...`, `Minimum...`) or ### Problem Variants Reduction graph nodes use variant key-value pairs from `Problem::variant()`: -- Base: `MaximumIndependentSet` (empty variant = defaults) +- Default: `MaximumIndependentSet {graph: "SimpleGraph", weight: "One"}` - Graph variant: `MaximumIndependentSet {graph: "KingsSubgraph", weight: "One"}` -- Weight variant: `MaximumIndependentSet {graph: "SimpleGraph", weight: "f64"}` -- Default variant ranking: `SimpleGraph`, `One`, `KN` are considered default values; variants with the most default values sort first -- Nodes come exclusively from `#[reduction]` registrations; natural edges between same-name variants are inferred from the graph/weight subtype partial order +- Weight variant: `MaximumIndependentSet {graph: "SimpleGraph", weight: "i64"}` +- Each problem declares one default concrete variant through `declare_variants!`; variant listings place that declaration first +- Nodes come from concrete `declare_variants!` registrations +- Same-name variant relations are explicit `#[reduction]` registrations - Each primitive reduction is determined by the exact `(source_variant, target_variant)` endpoint pair - Reduction edges carry `EdgeCapabilities { witness, aggregate, turing }`; graph search defaults to witness mode, aggregate mode is available through `ReductionMode::Aggregate`, and Turing (multi-query) mode via `ReductionMode::Turing` -- `#[reduction]` accepts only `overhead = { ... }` and currently registers witness/config reductions; aggregate-only and Turing edges require manual `ReductionEntry` registration +- `#[reduction]` requires one `transform = exact`, `transform = upper_bound`, or `transform = unavailable` declaration and currently registers witness/config reductions; aggregate-only and Turing edges require manual `ReductionEntry` registration - `Decision

→ P` is an aggregate-only edge (solve optimization, compare to bound); `P → Decision

` is a Turing edge (binary search over decision bound) ### Extension Points -- New models register dynamic load/serialize/brute-force dispatch through `declare_variants!` in the model file, not by adding manual match arms in the CLI -- **CLI creation is schema-driven:** `pred create` automatically maps `ProblemSchemaEntry` fields to CLI flags via `snake_case → kebab-case` convention. New models need only: (1) matching CLI flags in `CreateArgs` + `flag_map()`, and (2) type parser support in `parse_field_value()` if using a new field type. No match arm in `create.rs` is needed. -- **CLI flag names must match schema field names.** The canonical name for a CLI flag is the schema field name in kebab-case (e.g., schema field `universe_size` → `--universe-size`, field `subsets` → `--subsets`). Old aliases (e.g., `--universe`, `--sets`) may exist as clap `alias` for backward compatibility at the clap level, but `flag_map()`, help text, error messages, and documentation must use the schema-derived name. Do not add new backward-compat aliases; if a field is renamed in the schema, update the CLI flag name to match. -- **Decision variants** of optimization problems use `Decision

` wrapper. Add via: (1) `decision_problem_meta!` for the inner type, (2) inherent methods on `Decision`, (3) `register_decision_variant!` with `dims`, `fields`, `size_getters`. Schema-driven CLI creation auto-restructures flat JSON into `{inner: {...}, bound}`. -- Aggregate-only models are first-class in `declare_variants!`; aggregate-only and Turing reduction edges still need manual `ReductionEntry` wiring because `#[reduction]` only registers witness/config reductions today +- New models register dynamic load/serialize metadata through `declare_variants!` and, when finite enumeration exists, register it separately through `register_brute_force!`; neither belongs in CLI match arms +- **Model category is explicit registry metadata.** Every `ProblemSchemaEntry` declares exactly one of `Algebraic`, `Formula`, `Graph`, `Misc`, or `Set`; catalog behavior never derives it from `module_path!()` or source location. +- **CLI creation is registry-driven and two-stage:** the static parser discovers the requested problem spec without registering model subcommands, then a second parse adds flags only for the selected concrete variant. Ordinary models use `ProblemSchemaEntry.fields` directly. Models whose construction differs from persisted JSON own a typed `CreateSpec` and fallible conversion beside the model; CLI and MCP only normalize transport values and invoke the registered constructor. +- **Each construction input has one name and one concrete type per variant.** Do not add compatibility aliases or infer types from flag names. `CreateSpec` field names render as `snake_case → kebab-case` in CLI and remain `snake_case` in MCP. Add a reusable codec only for a genuinely new transport representation, never a model-name parser branch. +- **Random generation is optional and variant-owned.** Not every model has a useful, well-defined random-instance distribution. Add `RandomGenerate` only when the generator has clear semantics and a concrete use (for example, testing or examples); never invent arbitrary bounds or distributions merely to make every model support `--random`. Implement it beside the model (normally through `impl_random_generate!` and a typed `CreateSpec` input DTO), then add `random` only to the applicable `declare_variants!` entries. CLI and MCP discover the exact variant's inputs and callback; never add a model-name random dispatch or advertise random generation on an unsupported variant. +- **Decision variants** of optimization problems use `Decision

` wrapper. Add via: (1) `decision_problem_meta!` for the inner type, (2) inherent methods on `Decision`, (3) `register_decision_variant!` with `dims`, `fields`, `parameter_getters`. The generated construction spec accepts flat inner fields plus `bound`; persisted JSON remains `{inner: {...}, bound}`. `Decision

` delegates canonical parameters to `P`; its objective bound is semantic instance data, not a problem parameter. +- Aggregate-only and Turing reduction edges still need manual `ReductionEntry` wiring because `#[reduction]` only registers solution-mapping reductions today; this edge capability does not imply that a problem may solve successfully without a `Solution` - Exact registry dispatch lives in `src/registry/`; alias resolution and partial/default variant resolution live in `problemreductions-cli/src/problem_name.rs` - `pred create` schema-driven dispatch lives in `problemreductions-cli/src/commands/create.rs` (`create_schema_driven()`) -- Canonical paper and CLI examples live in `src/example_db/model_builders.rs` and `src/example_db/rule_builders.rs` +- Canonical model examples live in `src/example_db/model_builders.rs`; rule examples live beside their rules and are collected by `src/rules/mod.rs` ## Conventions +### Numeric Contract + +Follow the [numeric types and arithmetic standard](../docs/src/design.md#numeric-types-and-arithmetic) +for every model and reduction. `usize` is reserved for in-memory indices, +collection lengths, and brute-force dimensions; canonical problem parameters +are `u64`; signed mathematical integers use `i64`; and approximate +real values use finite `f64`. Before implementation, identify each numeric +input and domain, each computed total and result type, the largest supported +value, every range/sign-changing conversion, overflow behavior, and whether +arithmetic is exact or approximate. Use `TryFrom` at range boundaries and +checked arithmetic for derived values that may overflow. Rust construction, +serde, CLI, and MCP must enforce the same range. + +Issue contributors provide the mathematical definition, domains, and +constraints; implementers derive the Rust representation. Do not require issue +authors to choose implementation types or add implementation-specific numeric +fields to issue templates. Changes to issue templates require user approval. + ### File Naming - Reduction files: `src/rules/_.rs` (e.g., `maximumindependentset_qubo.rs`) - Model files: `src/models//.rs` — category is by input structure: `graph/` (graph input), `formula/` (boolean formula/circuit), `set/` (universe + subsets), `algebraic/` (matrix/linear system/lattice), `misc/` (other) -- Canonical examples: builder functions in `src/example_db/rule_builders.rs` and `src/example_db/model_builders.rs` +- Canonical examples: model builders in `src/example_db/model_builders.rs`; rule-local `canonical_rule_example_specs()` functions collected by `src/rules/mod.rs` - Example binaries in `examples/`: utility/export tools and pedagogical demos only (not per-reduction files) - Test naming: `test__to__closed_loop` @@ -233,8 +265,8 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: **Reference implementations — read these first:** - **Reduction test:** `src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs` — closed-loop pattern - **Model test:** `src/unit_tests/models/graph/maximum_independent_set.rs` — evaluation, serialization -- **Solver test:** `src/unit_tests/solvers/brute_force.rs` — aggregate `solve()` plus witness recovery helpers -- **Trait definitions:** `src/traits.rs` (`Problem`), `src/solvers/mod.rs` (`Solver`) +- **Solver test:** `src/unit_tests/solvers/brute_force.rs` — solution-returning `solve()` plus all-solution helpers +- **Core definitions:** `src/traits.rs` (`Problem`), `src/solvers/brute_force.rs` (`BruteForce`) ### Coverage @@ -261,7 +293,7 @@ Model review automation checks for a dedicated test file under `src/unit_tests/m - `.claude/` — Claude Code instructions and skills - `docs/book/` — mdBook user documentation (built with `make doc`) - `docs/paper/reductions.typ` — Typst paper with problem definitions and reduction theorems -- `src/example_db/` — Canonical model/rule examples: `model_builders.rs`, `rule_builders.rs` (in-memory builders), `specs.rs` (per-module invariant specs), consumed by `pred create --example` and paper exports +- `src/example_db/` — Model builders, shared example specs, and rule-example aggregation consumed by `pred create --example` and paper exports - `examples/` — Export utilities, graph-analysis helpers, and pedagogical demos ## Documentation Requirements @@ -309,10 +341,10 @@ The complexity string represents the **worst-case time complexity of the best kn 5. Use only concrete numeric values — no symbolic constants (epsilon, omega); inline the actual numbers with citations 6. Variable names must match getter methods on the problem type (enforced at compile time) -### Reduction Overhead (`#[reduction(overhead = {...})]`) -Overhead expressions describe how target problem size relates to source problem size. To verify correctness: +### Reduction Parameter Relation (`#[reduction(transform = exact|upper_bound {...})]`) +Parameter expressions describe how target problem parameters relate to source problem parameters. To verify correctness: 1. Read the `reduce_to()` implementation and count the actual output sizes 2. Check that each field (e.g., `num_vertices`, `num_edges`, `num_sets`) matches the constructed target problem 3. Watch for common errors: universe elements mismatch (edge indices vs vertex indices), worst-case edge counts in intersection graphs (quadratic, not linear), constant factors in circuit constructions -4. Test with concrete small instances: construct a source problem, run the reduction, and compare target sizes against the formula +4. Test with concrete small instances: construct a source problem, run the reduction, and compare target parameters against the formula 5. Ensure there is only one primitive reduction registration for each exact source/target variant pair; wrap shared helpers instead of registering duplicate endpoints diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 54f4c2292..6cf14b947 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -68,14 +68,16 @@ Read these first to understand the patterns: - **Model tests:** `src/unit_tests/models/graph/maximum_independent_set.rs` - **Trait definitions / aggregate types:** `src/traits.rs` (`Problem`), `src/types.rs` (`Aggregate`, `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`) - **Registry dispatch boundary:** `src/registry/mod.rs`, `src/registry/variant.rs` -- **CLI aliases:** `problemreductions-cli/src/problem_name.rs` -- **CLI creation:** `problemreductions-cli/src/commands/create.rs` +- **CLI and MCP construction:** discovered from the model's registry entry; no frontend model-name dispatch - **Canonical model examples:** `src/example_db/model_builders.rs` ## Pre-review Checklist Before implementing, make sure the plan explicitly covers these items that structural review checks later: -- `ProblemSchemaEntry` metadata is complete for the current schema shape (`display_name`, `aliases`, `dimensions`, and constructor-facing `fields`) +- Follow `docs/src/design.md#numeric-types-and-arithmetic`: `usize` is for in-memory indices, collection lengths, and brute-force dimensions; registered problem size parameters use `u64`; signed mathematical integers use `i64`; Boolean data uses `bool`; and approximate real or rational data uses finite `f64`. Use another format only when required by the mathematical problem or schema, such as `BigUint` for arbitrary-precision problems or `One` for unit weights; implementation convenience is not sufficient, and there is no `i32` model or I/O format. Implementation-local values are outside this contract. +- Keep failure phases explicit: fallible constructors, create specs, serde-facing validation, and random generation return `ConstructionError`; `evaluate()` returns `EvaluationError`; no public model path returns `Result<_, String>`. Stored-field arithmetic and evaluation arithmetic are checked and reported in their own phase. +- Serde/CLI construction uses the same validation as `new`/`try_new`, and boundary tests cover the supported maximum without requiring impractical allocation. +- `ProblemSchemaEntry` metadata is complete (`display_name`, `aliases`, `dimensions`, explicit `category`, and construction `fields`) - `Problem::Value` uses the correct aggregate wrapper and witness support is intentional - `declare_variants!` is present with exactly one `default` variant when multiple concrete variants exist - CLI discovery and `pred create ` support are included where applicable @@ -92,6 +94,8 @@ Choose the appropriate sub-module under `src/models/`: - `algebraic/` -- matrices, linear systems, lattices (QUBO, ILP, CVP, BMF) - `misc/` -- unique input structures that don't fit other categories (BinPacking, PaintShop, Factoring) +Declare the same structural choice explicitly in `ProblemSchemaEntry.category`. This is required metadata and is never inferred from `module_path!()` or the file location. + ## Step 1.5: Infer problem size getters From the **best known exact algorithm** complexity (item 9), infer what problem size getter methods the struct should expose. The variables used in the complexity expression define the natural size metrics. @@ -122,14 +126,14 @@ Create `src/models//.rs`: ``` Key decisions: -- **Schema metadata:** `ProblemSchemaEntry` must reflect the current registry schema shape, including `display_name`, `aliases`, `dimensions`, and constructor-facing `fields` +- **Schema metadata:** `ProblemSchemaEntry` must include the explicit structural `category` and reflect the construction interface through `display_name`, `aliases`, `dimensions`, and `fields` - **Objective problems:** use `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the model should expose optimization-style witness helpers - **Witness problems:** use `type Value = Or` for existential feasibility problems - **Aggregate-only problems:** use a value-only aggregate such as `Sum<_>`, `And`, or a custom `Aggregate` when witnesses are not meaningful - **Weight management:** use inherent methods (`weights()`, `set_weights()`, `is_weighted()`), NOT traits - **`dims()`:** returns the configuration space dimensions (e.g., `vec![2; n]` for binary variables) -- **`evaluate()`:** must return the per-configuration aggregate value. For models with invalid configs, check feasibility first and return the appropriate invalid/false contribution -- **`variant()`:** use the `variant_params!` macro — e.g., `crate::variant_params![G, W]` for `Problem`, or `crate::variant_params![]` for problems with no type parameters. Each type parameter must implement `VariantParam` (already done for standard types like `SimpleGraph`, `i32`, `One`). See `src/variant.rs`. +- **`evaluate()`:** must return `Result`. Invalid configurations remain the aggregate's invalid/false contribution; arithmetic overflow and non-finite computed values are errors. +- **`variant()`:** use the `variant_params!` macro — e.g., `crate::variant_params![G, W]` for `Problem`, or `crate::variant_params![]` for problems with no type parameters. Each type parameter must implement `VariantParam` (already done for standard types like `SimpleGraph`, `i64`, `One`). See `src/variant.rs`. - **Solve surface:** `Solver::solve()` always computes the aggregate value. `pred solve problem.json` prints a `Solution` only when a witness exists; `pred solve bundle.json` and `--solver ilp` remain witness-only workflows ## Step 2.5: Register variant complexity @@ -138,7 +142,7 @@ Add `declare_variants!` at the bottom of the model file (after the trait impls, ```rust crate::declare_variants! { - ProblemName => "1.1996^num_vertices", + ProblemName => "1.1996^num_vertices", default ProblemName => "1.1996^num_vertices", } ``` @@ -168,22 +172,32 @@ The CLI now loads, serializes, and brute-force solves problems through the core 1. **Registry-backed dispatch comes from `declare_variants!`:** - Make sure every concrete variant you want the CLI to load is listed in `declare_variants!` - Mark the intended default variant with `default` when applicable + - Declare well-established problem aliases in `ProblemSchemaEntry.aliases` and variant-specific aliases in `declare_variants!`; CLI and MCP discover both from the registry + +## Step 4.5: Add construction support + +CLI and MCP construction are registry-driven. Do not edit either frontend to recognize a model name. + +1. If user-facing inputs exactly match persisted JSON fields, do nothing. The ordinary `declare_variants!` entry uses `ProblemSchemaEntry.fields` as required construction inputs and deserializes the model directly. + +2. If construction has derived fields, renamed inputs, defaults depending on other inputs, or a composite value assembled from multiple inputs, define a model-local DTO with `#[derive(Deserialize, CreateSpec)]`. Its named fields are the complete public construction contract. Use `Option` only for genuinely optional inputs, doc comments for help text, and `#[create(codec = "...")]` when the transport syntax cannot be inferred from the Rust type. Set `ProblemSchemaEntry.fields` to `LocalCreateSpec::FIELDS` so the catalog and executable constructor share the derived metadata. -2. **`problemreductions-cli/src/problem_name.rs`:** - - Add a lowercase alias mapping in `resolve_alias()` (e.g., `"newproblem" => "NewProblem".to_string()`) - - Only add short aliases to the `ALIASES` array if the abbreviation is **well-established in the literature** (e.g., MIS, MVC, SAT, TSP, CVP are standard; "KS" for Knapsack or "BP" for BinPacking are NOT — do not invent new abbreviations) +3. Implement `TryFrom for Model` with `ConstructionError`. The direct constructor and serde path must share validation. Use `Conversion` for contract violations, `IntegerOverflow` for stored integer arithmetic, and `NonFiniteFloat` for stored non-finite values; do not call a panicking constructor. -## Step 4.5: Add CLI creation support +4. Register the spec on each applicable variant: `default Model => "..." create LocalCreateSpec`. Both frontends then discover the inputs automatically and serialize the constructed typed model back to canonical persisted JSON. -CLI creation is **schema-driven** — `pred create ` automatically maps `ProblemSchemaEntry` fields to CLI flags via `snake_case → kebab-case` convention. No match arm in `create.rs` is needed. +5. A new reusable external syntax may add one transport codec. It must dispatch by codec/type, never by canonical model name. Unknown or missing inputs are rejected by the core construction contract. -1. **Ensure CLI flags exist** in `problemreductions-cli/src/cli.rs` (`CreateArgs` struct) for each field in your `ProblemSchemaEntry`. The flag name must match the field name via `snake_case → kebab-case` (e.g., field `edge_weights` → flag `--edge-weights`). If a flag already exists with the right name, you're done. +### Optional random generation -2. **Add new CLI flags** only if the problem needs flags not already present. Add them to `CreateArgs` and update `all_data_flags_empty()` accordingly. Also add entries to the `flag_map()` method on `CreateArgs`. +Random generation is an optional model capability, not a model-completeness requirement. Many models do not have a natural or useful probability distribution over instances; leave random generation unregistered for those models. Do not invent arbitrary size limits, value ranges, or distributions merely to make `--random` available. -3. **Add type parser support** if the field uses a type not yet handled by `parse_field_value()` in `create.rs`. Check the existing type dispatch table — most standard types (`Vec`, `Vec`, `Vec<(usize, usize)>`, graph types, etc.) are already covered. Only add a new parser for genuinely new types. +When the model does have a well-defined generator with a concrete testing or example use, random generation is registry-driven and belongs beside the model. Do not edit CLI or MCP dispatch code. -4. **Schema alignment**: The `ProblemSchemaEntry` fields should list **constructor parameters** (what the user provides), not internal derived fields. For example, if `m` and `n` are derived from a matrix, only list `matrix` and `k` in the schema. Field names must match the struct field names exactly (used for JSON serialization and CLI flag mapping). +1. Define a typed random input DTO with `#[derive(Deserialize, CreateSpec)]`, or reuse a matching shared spec from `crate::random`. +2. Implement `RandomGenerate` with `crate::impl_random_generate!(ConcreteModel, RandomSpec, |spec| { ... })`. Validate values and return `Result`; do not round, clamp, or silently replace invalid inputs. +3. Add `random` only to the exact `declare_variants!` entries that implement the trait: `default Model => "..." create LocalCreateSpec random`. +4. The generated problem must have the same canonical name and variant as the selected registry entry. Use the concrete variant's actual graph and numeric types instead of attaching requested metadata to a different concrete instance. ## Step 4.6: Add canonical model example to example_db @@ -303,6 +317,7 @@ Structural and quality review is handled by the `review-pipeline` stage, not her |---------|-----| | Implementing weight management as a trait | Use inherent methods: `weights()`, `set_weights()`, `is_weighted()` | | Forgetting `inventory::submit!` | Every problem needs a `ProblemSchemaEntry` registration | +| Omitting or inferring the model category | Set the required `ProblemSchemaEntry.category` explicitly to one of `Algebraic`, `Formula`, `Graph`, `Misc`, or `Set`; never parse `module_path!()`. | | Missing `#[path]` test link | Add `#[cfg(test)] #[path = "..."] mod tests;` at file bottom | | Wrong `dims()` | Must match the actual configuration space (e.g., `vec![2; n]` for binary) | | Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `bool`, aggregate-only models use a fold value like `Sum` / `And` | @@ -310,12 +325,12 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Forgetting `declare_variants!` | Required for variant complexity metadata and registry-backed load/serialize/solve dispatch | | Wrong aggregate wrapper | Use `Max` / `Min` / `Extremum` for objective problems, `Or` for existential witness problems, and `Sum` / `And` (or a custom aggregate) for value-only folds | | Wrong `declare_variants!` syntax | Entries no longer use `opt` / `sat`; one entry per problem may be marked `default` | -| Forgetting CLI alias | Must add lowercase entry in `problem_name.rs` `resolve_alias()` | +| Adding aliases in CLI code | Declare problem aliases in `ProblemSchemaEntry.aliases` and variant aliases in `declare_variants!` | | Adding a hand-written decision model | Use `Decision

` wrapper instead — see `decision_problem_meta!` + `register_decision_variant!` in `src/models/graph/minimum_vertex_cover.rs` for the pattern | | Inventing short aliases | Only use well-established literature abbreviations (MIS, SAT, TSP); do NOT invent new ones | -| Forgetting CLI flags | Schema-driven create needs matching CLI flags in `CreateArgs` for each `ProblemSchemaEntry` field (snake_case → kebab-case). Also add to `flag_map()`. | -| Missing type parser | If the problem uses a new field type, add a handler in `parse_field_value()` in `create.rs` | -| Schema lists derived fields | Schema should list constructor params, not internal fields (e.g., `matrix, k` not `matrix, m, n, k`) | +| Adding frontend model-name branches | Construction is model-owned. Use a local `CreateSpec` and register it with `declare_variants!`; CLI and MCP must discover it. | +| Hand-maintaining custom construction fields twice | Derive `CreateSpec`, use `LocalCreateSpec::FIELDS` in `ProblemSchemaEntry`, and register the same type in `declare_variants!`. | +| Calling a panicking constructor from `TryFrom` | Share a fallible constructor and preserve its `ConstructionError`. | | Missing canonical model example | Add a builder in `src/example_db/model_builders.rs` and keep it aligned with paper/example workflows | | Paper example not tested | Must include `test__paper_example` that verifies the exact instance, solution, and solution count shown in the paper | | Claiming direct ILP solving but leaving ` -> ILP` for later | If the issue promises a direct ILP path, implement that rule in the same PR with exact overhead metadata and production-level ILP tests | diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 33a303af6..3097be526 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -24,12 +24,12 @@ Before any implementation, collect all required information. If called from `iss | # | Item | Description | Example | |---|------|-------------|---------| -| 1 | **Source problem** | The problem being reduced FROM (must already exist) | `MinimumVertexCover` | -| 2 | **Target problem** | The problem being reduced TO (must already exist) | `MaximumIndependentSet` | +| 1 | **Source problem** | The problem being reduced FROM (must already exist) | `MinimumVertexCover` | +| 2 | **Target problem** | The problem being reduced TO (must already exist) | `MaximumIndependentSet` | | 3 | **Reduction algorithm** | How to transform source instance to target | "Copy graph and weights; IS on same graph as VC" | | 4 | **Solution extraction** | How to map target solution back to source | "Complement: `1 - x` for each variable" | | 5 | **Correctness argument** | Why the reduction preserves optimality | "S is independent set iff V\S is vertex cover" | -| 6 | **Size overhead** | How target size relates to source size | `num_vertices = "num_vertices", num_edges = "num_edges"` | +| 6 | **Parameter transform** | How target size relates to source size | `num_vertices = "num_vertices", num_edges = "num_edges"` | | 7 | **Concrete example** | A small worked-out instance (tutorial style, clear intuition) | "Triangle graph: VC={0,1} -> IS={2}" | | 8 | **Solving strategy** | How to solve the target problem | "BruteForce, or existing ILP reduction" | | 9 | **Reference** | Paper, textbook, or URL for the reduction | URL or citation | @@ -56,6 +56,28 @@ grep "type Value = " src/models/*/.rs src/models/*/.rs If incompatible, STOP and comment on the issue explaining the type mismatch and options. Do NOT proceed. +## Numeric Safety Gate + +Read `docs/src/design.md#numeric-types-and-arithmetic`. Derive implementation +types, supported ranges, and checked conversions from the mathematical source, +target, and reduction algorithm. Use `usize` for in-memory indices, collection +lengths, and brute-force dimensions; `u64` for registered problem size +parameters; `i64` for signed mathematical integers; `bool` for Boolean data; +and finite `f64` for real or rational data. Another format needs mathematical +or target-schema justification; there is no `i32` boundary format. +Temporary reduction calculations are outside this format contract, but fields +written into the target must use the target model's format. + +Ask the contributor only when a mathematical domain or constraint is ambiguous; +do not ask them to choose Rust types. Do not use `as` for range/sign changes. +Check target-size arithmetic and auxiliary identifiers before constructing the +target, verify serde/CLI uses the same ranges, and add focused boundary tests. +The public reduction returns `ReductionError`: preserve a target constructor's +`ConstructionError` as `ReductionError::Construction`, and report reduction +arithmetic directly as the corresponding `ReductionError`; do not stringify or +silently handle either error. Convert model-derived `i64` values to `f64` only +through `i64_to_exact_f64`. + ## Reference Implementations Read these first to understand the patterns: @@ -106,13 +128,19 @@ impl ReductionResult for ReductionXToY { type Source = SourceType; type Target = TargetType; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - // Map target solution back to source solution - // If Step 1 ran: translate the verified Python extract_solution() logic + fn extract_solution( + &self, + target_solution: &[usize], + ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + let source_solution = /* translate the verified mathematical mapping exactly */; + Ok(source_solution) } } ``` +Every direct extractor must call `validate_target_solution()` once before decoding. It checks only length and value domains, not feasibility, optimality, or rule-specific structure; reject malformed structure with `ExtractionError`. + **ReduceTo with `#[reduction]` macro** (overhead is **required**): ```rust #[reduction(overhead = { @@ -156,6 +184,8 @@ Additional recommended tests: - Edge cases (empty graph, single vertex, etc.) - Weight preservation (if applicable) +Test every malformed representation distinguished by the decoder (for example, zero or multiple one-hot selections, or duplicate permutation entries). The canonical example supplies shared wrong-length and out-of-domain tests. + For aggregate-only reductions, replace the closed-loop witness test with value-chain tests: - Solve the target with `Solver::solve()` - Map the aggregate value back with `extract_value()` @@ -163,9 +193,9 @@ For aggregate-only reductions, replace the closed-loop witness test with value-c Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule file. -## Step 5: Add canonical example to example_db +## Step 5: Add canonical example -Add a builder function in `src/example_db/rule_builders.rs` that constructs a small, canonical instance for this reduction. Follow the existing patterns in that file. Register the builder in `build_rule_examples()`. +Define `canonical_rule_example_specs()` in the rule module and include it from `src/rules/mod.rs::canonical_rule_example_specs()`. This enrolls the rule in shared round-trip, wrong-length, and out-of-domain extraction tests. ## Step 6: Document in paper (MANDATORY — DO NOT SKIP) @@ -231,11 +261,11 @@ Checklist: notation self-contained, complexity cited, overhead consistent, examp ```bash cargo run --example export_graph # Generate reduction_graph.json for docs/paper builds cargo run --example export_schemas # Generate problem schemas for docs/paper builds -make regenerate-fixtures # Regenerate example_db/fixtures/examples.json (slow, needs ILP) +cargo run --features "example-db" --example export_examples make test clippy # Must pass ``` -`make regenerate-fixtures` is required so the paper can load the new rule's example data from `src/example_db/fixtures/examples.json`. Without it, the `reduction-rule` entry in Step 6 will reference missing fixture data. +`export_examples` refreshes the gitignored `docs/paper/data/examples.json` used by the paper. Structural and quality review is handled by the `review-pipeline` stage, not here. The run stage just needs to produce working code. @@ -249,7 +279,9 @@ Structural and quality review is handled by the `review-pipeline` stage, not her ## CLI Impact -Adding a witness-preserving reduction rule does NOT require CLI changes -- the reduction graph is auto-generated from `#[reduction]` macros and the CLI discovers paths dynamically. However, both source and target models must already be fully registered through their model files (`declare_variants!`), aliases as needed in `problem_name.rs`, and `pred create` support where applicable (see `add-model` skill). +Adding a witness-preserving reduction rule does NOT require CLI changes -- the reduction graph is auto-generated from `#[reduction]` macros and the CLI discovers paths dynamically. However, both source and target models must already be fully registered through their model files (`ProblemSchemaEntry` and `declare_variants!`), including any aliases and `pred create` construction contract (see `add-model` skill). + +`ExtractionError` already propagates through `pred extract` and bundle `pred solve`; add a rule-specific CLI test only when the CLI surface changes. Aggregate-only reductions currently have a narrower CLI surface: - `pred solve ` can still compute direct aggregate values for aggregate-only problems @@ -261,7 +293,7 @@ Aggregate-only reductions currently have a narrower CLI surface: - Rule file: `src/rules/_.rs` -- no underscores within a problem name - e.g., `maximumindependentset_qubo.rs`, `minimumvertexcover_maximumindependentset.rs` - Test file: `src/unit_tests/rules/_.rs` -- Canonical example: builder function in `src/example_db/rule_builders.rs` +- Canonical example: `canonical_rule_example_specs()` in the rule module, included from `src/rules/mod.rs` ## Common Mistakes @@ -272,9 +304,10 @@ Aggregate-only reductions currently have a narrower CLI surface: | Wrong overhead expression | Must accurately reflect the size relationship | | Adding extra reduction metadata or duplicate primitive endpoint registration | Keep one primitive registration per endpoint pair and use only the `overhead` form of `#[reduction]` | | Missing `extract_solution` mapping state | Store any index maps needed in the ReductionResult struct | -| Not adding canonical example to `example_db` | Add builder in `src/example_db/rule_builders.rs` | +| Permissive extraction | Validate first, then map exactly or return `ExtractionError` | +| Not adding a canonical example | Add the rule-local spec and include it from `src/rules/mod.rs` | | Not regenerating reduction graph | Run `cargo run --example export_graph` after adding a rule | -| Skipping Step 5 (paper documentation) | **Every rule MUST have a `reduction-rule` entry in the paper. This is mandatory, not optional. PRs without documentation will be rejected.** | -| Source/target model not fully registered | Both problems must already have `declare_variants!`, aliases as needed, and CLI create support -- use `add-model` skill first | +| Skipping Step 6 (paper documentation) | **Every rule MUST have a `reduction-rule` entry in the paper. This is mandatory, not optional. PRs without documentation will be rejected.** | +| Source/target model not fully registered | Both problems must already have `ProblemSchemaEntry`, `declare_variants!`, registry aliases as needed, and a construction contract -- use `add-model` skill first | | Treating a direct-to-ILP rule as a toy stub | Direct ILP reductions need exact overhead metadata and strong semantic regression tests, just like other production ILP rules | | Skipping verification for complex reductions | Verification is default for a reason — `--no-verify` is for trivial identity/complement reductions only | diff --git a/.claude/skills/check-issue/SKILL.md b/.claude/skills/check-issue/SKILL.md index 983e6fee0..e63957476 100644 --- a/.claude/skills/check-issue/SKILL.md +++ b/.claude/skills/check-issue/SKILL.md @@ -102,7 +102,7 @@ Read the "Reduction Algorithm" section and flag as **Fail** if: - **Variable substitution only:** The mapping is a 1-to-1 relabeling (e.g., `x_i → 1 - x_i` for complement problems). A valid reduction must construct new constraints, objectives, or graph structure. - **Subtype coercion:** The reduction merely casts to a more general type within an existing variant hierarchy (e.g., UnitDiskGraph → SimpleGraph) with no structural change to the problem instance. -- **Same-problem identity:** Reducing between variants of the same problem with no insight (e.g., `MIS` → `MIS` by setting all weights to 1). +- **Same-problem identity:** Reducing between variants of the same problem with no insight (e.g., `MIS` → `MIS` by setting all weights to 1). - **Insufficient detail:** The algorithm is a hand-wave ("map variables accordingly", "follows from the definition") — not a step-by-step procedure a programmer could implement. This is also a **Fail**. If the construction involves gadgets, penalty terms, auxiliary variables, or non-trivial structural transformation → **Pass**. diff --git a/.claude/skills/final-review/SKILL.md b/.claude/skills/final-review/SKILL.md index 633d86ad8..ced146b7d 100644 --- a/.claude/skills/final-review/SKILL.md +++ b/.claude/skills/final-review/SKILL.md @@ -168,12 +168,12 @@ Use `AskUserQuestion` with your recommendation: Scan the PR diff for dangerous actions: -- **Blacklisted files**: If the diff touches `docs/src/reductions/reduction_graph.json`, `docs/src/reductions/problem_schemas.json`, or `src/example_db/fixtures/examples.json` (legacy, no longer exists), **block merge**. These files are auto-generated and must not be committed in PRs — they are rebuilt by CI/`make doc`/`make paper`. Flag immediately and recommend OnHold. +- **Blacklisted files**: If the diff touches `docs/src/reductions/reduction_graph.json` or `docs/src/reductions/problem_schemas.json`, **block merge**. These files are auto-generated and must not be committed in PRs — they are rebuilt by CI/`make doc`/`make paper`. Flag immediately and recommend OnHold. - **Removed features**: Any existing model, rule, test, or example deleted? - **Unrelated changes**: Files modified that don't belong to this PR (e.g., changes to unrelated models/rules, CI config, Cargo.toml dependency changes not needed for this PR) - **Force push indicators**: Any sign of history rewriting - **Broad modifications**: Changes to core traits, macros, or shared infrastructure that could affect other features -- **No committed `examples.json`**: The example database is generated on demand by `make paper` (via `export_examples`). PRs should not commit `src/example_db/fixtures/examples.json` (legacy path, deleted) or `docs/paper/data/examples.json` (current output path) — both are gitignored build artifacts. +- **No committed `examples.json`**: The example database is generated on demand by `make paper` (via `export_examples`). Do not commit the gitignored `docs/paper/data/examples.json` build artifact. Report findings with fix options for each concern: diff --git a/.claude/skills/find-problem/SKILL.md b/.claude/skills/find-problem/SKILL.md index 3bcd9953e..d1695524a 100644 --- a/.claude/skills/find-problem/SKILL.md +++ b/.claude/skills/find-problem/SKILL.md @@ -54,16 +54,16 @@ Never skip step 1 or 3. **Goal:** Get the user's model name and solver complexity. -**If invoked as `/find-problem `:** validate with `pred show `. If it exists, show the output (including size fields), then ask for solver complexity. +**If invoked as `/find-problem `:** validate with `pred show `. If it exists, show the output (including parameters), then ask for solver complexity. **If invoked as `/find-problem`:** ask using `AskUserQuestion`: "Which problem model does your solver handle?" Validate the answer with `pred show`. **Ask for complexity** using `AskUserQuestion`: "What is your solver's time complexity? Use the size field names from the output above (e.g., `O(1.1996^num_vertices)`, `O(2^(num_variables/3))`)." -- Variable names should match the model's size fields shown in `pred show` output +- Variable names should match the model's parameters shown in `pred show` output - If the user gives informal notation (e.g., "exponential in n"), help them formalize it using the model's actual size field names -**Exit condition:** Validated model name + complexity expression with variables matching the model's size fields. Proceed to Step 2. +**Exit condition:** Validated model name + complexity expression with variables matching the model's parameters. Proceed to Step 2. --- @@ -91,7 +91,7 @@ Never skip step 1 or 3. - **Better** — effective complexity has a smaller base or exponent than best-known - **Similar** — comparable asymptotic behavior - **Worse** — effective complexity exceeds best-known (reduction overhead makes it impractical) - - **When effective and best-known use different variables** (e.g., `O(1.5^num_subsets)` vs `O(2^universe_size)`): this happens when a problem has multiple independent size fields and the best-known algorithm's dominant variable differs from the reduction overhead's. In this case, use `pred-sym eval` at representative concrete values to determine the comparison. State the result conditionally: "Better when num_subsets ≤ c·universe_size" with the crossover ratio. + - **When effective and best-known use different variables** (e.g., `O(1.5^num_subsets)` vs `O(2^universe_size)`): this happens when a problem has multiple independent parameters and the best-known algorithm's dominant variable differs from the reduction overhead's. In this case, use `pred-sym eval` at representative concrete values to determine the comparison. State the result conditionally: "Better when num_subsets ≤ c·universe_size" with the crossover ratio. 5. **Web search** only the **Better** and **Similar** candidates for real-world applications (not the Worse ones). Use `WebSearch` tool with query " real-world applications". @@ -194,5 +194,5 @@ pred solve bundle.json --solver ilp --timeout 60 - **Conversational tone.** Guided consultation, not a lecture. - **Live execution.** Every `pred` command runs for real. No fake output. - **Graceful fallbacks.** If `pred to` returns no results (no incoming reductions), suggest trying with more hops or a different model. If `pred path` fails for a specific source, skip it and note it in the table. -- **Help with complexity notation.** If the user gives informal complexity, show `pred show ` size fields and help them write a formal expression. +- **Help with complexity notation.** If the user gives informal complexity, show `pred show ` parameters and help them write a formal expression. - **Cap results at 10.** If discovery returns many problems, show top 10 by effective complexity and offer to show more. diff --git a/.claude/skills/find-solver/SKILL.md b/.claude/skills/find-solver/SKILL.md index c9d221e36..704c76ced 100644 --- a/.claude/skills/find-solver/SKILL.md +++ b/.claude/skills/find-solver/SKILL.md @@ -86,9 +86,9 @@ Use `AskUserQuestion` for each question. Format options as **(a)**/**(b)**/**(c) 1. **Web search** the clarified problem description together with terms like "NP-hard", "computational complexity", or "reduction" to find formal problem names and known relationships in the literature. Use `WebSearch` tool. -2. **Run `pred list`** to get the full catalog of available models. Copy-paste the full output into your response. +2. **Search the catalog** with `pred list `. Use `pred list --json` when exhaustive machine-readable discovery is needed. Do not paste the full catalog into the response. -3. **Cross-reference** the web search results against the `pred list` catalog. For each candidate model that exists in the library (3-5 max), present a table: +3. **Cross-reference** the web search results against the catalog. For each candidate model that exists in the library (3-5 max), present a table: | # | Model | Why it might match | Caveat | |---|-------|--------------------|--------| diff --git a/.claude/skills/issue-to-pr/SKILL.md b/.claude/skills/issue-to-pr/SKILL.md index 2573bedd5..87734784e 100644 --- a/.claude/skills/issue-to-pr/SKILL.md +++ b/.claude/skills/issue-to-pr/SKILL.md @@ -92,12 +92,12 @@ Write implementation plan to `docs/plans/YYYY-MM-DD-.md` using `superpower The plan MUST reference the appropriate implementation skill and follow its steps: - **For ordinary `[Model]` issues:** Follow [add-model](../add-model/SKILL.md) Steps 1-7 as the action pipeline -- **For `[Model]` issues that explicitly claim direct ILP solving:** Follow [add-model](../add-model/SKILL.md) Steps 1-7 **and** [add-rule](../add-rule/SKILL.md) Steps 1-6 for the direct ` -> ILP` rule in the same plan / PR +- **For `[Model]` issues that explicitly claim direct ILP solving:** Follow [add-model](../add-model/SKILL.md) Steps 1-7 **and** [add-rule](../add-rule/SKILL.md) Steps 1-7 for the direct ` -> ILP` rule in the same plan / PR - **For `[Rule]` issues:** Follow [add-rule](../add-rule/SKILL.md) Steps 1-7 as the action pipeline. By default, `/add-rule` runs mathematical verification (Step 1) before implementation. If `--no-verify` was passed, include `--no-verify` when invoking `/add-rule` to skip verification. Include the concrete details from the issue (problem definition, reduction algorithm, example, etc.) mapped onto each step. -**Plan batching:** The paper writing step (add-model Step 6 / add-rule Step 5) MUST be in a **separate batch** from the implementation steps, so it gets its own subagent with fresh context. It depends on the implementation being complete (needs exports). Example batch structure for a `[Model]` plan: +**Plan batching:** The paper writing step (add-model Step 6 / add-rule Step 6) MUST be in a **separate batch** from the implementation steps, so it gets its own subagent with fresh context. It depends on the implementation being complete (needs exports). Example batch structure for a `[Model]` plan: - Batch 1: Steps 1-5.5 (implement model, register, CLI, tests) - Batch 2: Step 6 (write paper entry — depends on batch 1 for exports) @@ -112,8 +112,8 @@ For a `[Model]` issue with an explicit direct ILP claim, use: - Otherwise, ensure the information provided is enough to implement a solver. **Example rules:** -- Implement the user-provided example instance in the canonical `example_db` path for the issue (`src/example_db/model_builders.rs` or `src/example_db/rule_builders.rs`, as appropriate). -- Run the relevant export and fixture regeneration steps; verify the generated example data against the user-provided information. +- Implement the user-provided example in `src/example_db/model_builders.rs` for a model, or in the rule-local `canonical_rule_example_specs()` for a rule. +- Run the relevant exports and verify the generated example data against the user-provided information. - Present in `docs/paper/reductions.typ` in tutorial style with clear intuition (see KColoring->QUBO section for reference). ### 6. Create PR (or Resume Existing) diff --git a/.claude/skills/propose/SKILL.md b/.claude/skills/propose/SKILL.md index bc392dadb..5024e0af5 100644 --- a/.claude/skills/propose/SKILL.md +++ b/.claude/skills/propose/SKILL.md @@ -117,7 +117,7 @@ Right after the user picks model or rule, **study at least one existing case** i If no keyword match, just read the most recent closed model issue to see the template conventions. 3. **Note internally** (do not dump raw output to the user): - - What fields / size fields the similar problem has + - What fields / parameters the similar problem has - How the issue defines variables, schema, complexity - What level of mathematical detail is expected in examples - How the "Reduction Rule Crossref" section is structured @@ -176,7 +176,7 @@ Right after the user picks model or rule, **study at least one existing case** i 3. **Note internally**: - How the reduction algorithm is structured (numbered steps, symbol definitions) - - How the size overhead table is formatted (field names, formulas) + - How the parameter transform table is formatted (field names, formulas) - How the example is worked through (source → construction → target → solution) - What references and validation methods are used @@ -567,8 +567,8 @@ If the reduction is well-known, use the literature to **pre-fill** answers in St ``` If the user asks for more or less detail, revise and re-present. -4. **Size overhead** — Compute overhead from the algorithm using the target's size fields from `pred show --json`. Present the overhead table and ask for confirmation: - > "Based on the algorithm, the size overhead is: [table]. Does this look correct?" +4. **Parameter transform** — Compute overhead from the algorithm using the target's parameters from `pred show --json`. Present the overhead table and ask for confirmation: + > "Based on the algorithm, the parameter transform is: [table]. Does this look correct?" 5. **Example** — Generate **at least 3** candidate examples yourself (varying in size and structure), then present via `AskUserQuestion`. **3 options is the minimum — never fewer.** Always include a "Generate new batch" escape hatch: @@ -830,7 +830,7 @@ Print all issue URLs when done. - **Don't skip confirmation for textbook reductions.** Even if SubsetSum → Knapsack is in Garey & Johnson, still present each brainstorming step with pre-filled answers for the user to confirm or revise. Never jump straight to the draft. - **Don't rebuild `pred` unnecessarily.** Use `command -v pred` to check if it's installed before running `make cli` (which takes >1 minute). - **Don't ask all questions at once.** One `AskUserQuestion` call per message. -- **Don't use programming jargon.** Say "list of weights" not "Vec". Say "graph" not "SimpleGraph". Say "integer" not "i32". +- **Don't use programming jargon.** Say "list of weights" not "Vec". Say "graph" not "SimpleGraph". Say "integer" not "i64". - **Don't skip the reduction crossref.** An orphan model will be rejected. - **Don't file without user approval.** Always show the draft first. - **Don't implement anything.** The output is issues, not code. diff --git a/.claude/skills/review-paper/SKILL.md b/.claude/skills/review-paper/SKILL.md index d1c98cdf6..e5a8e2378 100644 --- a/.claude/skills/review-paper/SKILL.md +++ b/.claude/skills/review-paper/SKILL.md @@ -46,7 +46,7 @@ For each of the 10 entries, read the full entry text and evaluate against the ch | M3. Self-contained notation | Every symbol in `def` is defined before first use | | M4. Background text | Body contains at least 2 sentences of background/motivation | | M5. Example present | Body contains `*Example.*` or `Example.` | -| M6. Example from fixture | Example data matches `src/example_db/fixtures/examples.json` (not invented) — check by loading the JSON and comparing | +| M6. Example from fixture | Example data matches `docs/paper/data/examples.json` (not invented) — check by loading the JSON and comparing | | M7. Figure present | Body contains `#figure(` | | M8. Pred commands | Body contains `pred-commands(` or `pred create` | | M9. Algorithm citation | Complexity claims have `@citation` or a footnote explaining absence | @@ -72,7 +72,7 @@ For each of the 10 entries, read the full entry text and evaluate against the ch | M3. Proof length | Proof is at least 3 sentences (not just "trivial" or a one-liner) | | M4. Overhead documented | Overhead is auto-generated from JSON (verify edge exists in `reduction_graph.json`) | | M5. Example present | `example: true` and example renders correctly | -| M6. Example from fixture | Example data matches `src/example_db/fixtures/examples.json` | +| M6. Example from fixture | Example data matches `docs/paper/data/examples.json` | | M7. Pred commands | Example section contains `pred-commands(` with create/reduce/evaluate pipeline | | M8. Both directions | If the reverse rule also exists in the graph, check it has its own entry | diff --git a/.claude/skills/review-pipeline/SKILL.md b/.claude/skills/review-pipeline/SKILL.md index 9849a5432..51930ca79 100644 --- a/.claude/skills/review-pipeline/SKILL.md +++ b/.claude/skills/review-pipeline/SKILL.md @@ -175,7 +175,7 @@ Invoke `/review-quality` (file: `.claude/skills/review-quality/SKILL.md`) with t 2. **Invoke `/agentic-tests:test-feature`** (file: `~/.claude/commands/agentic-tests:test-feature.md`) with the identified feature. This simulates a downstream user exercising the feature from docs and examples. **Minimum test checklist** for the agentic tester: - - `pred list` — verify the new model/rule appears in the catalog + - For models, `pred list `; for rules, `pred list --rules ` — verify the new catalog entry appears - `pred show ` — verify details display correctly - `pred create --example ` — verify example instance creation works - `pred solve ` — verify solving works on the example diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index 5c30e15b6..d291a7e02 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -61,11 +61,12 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 9 | Registered in `{C}/mod.rs` | `Grep("mod {F}", "src/models/{C}/mod.rs")` | | 10 | Re-exported in `models/mod.rs` | `Grep("{P}", "src/models/mod.rs")` | | 11 | Variant registration exists | `Grep("declare_variants!|VariantEntry", file)` | -| 12 | CLI `resolve_alias` entry | `Grep("{P}", "problemreductions-cli/src/problem_name.rs")` | -| 13 | CLI `create` support | Schema-driven: verify each `ProblemSchemaEntry` field has a matching CLI flag in `CreateArgs` (field `snake_case` → flag `kebab-case`). Check `flag_map()` includes the flag. If the field type is unusual, verify `parse_field_value()` handles it. | +| 12 | Alias registration | If aliases are claimed, verify problem aliases are in `ProblemSchemaEntry.aliases` and variant aliases are in `declare_variants!`; no frontend alias branch | +| 13 | CLI `create` support | Run `pred create --help` for the concrete variant. Verify its flags and types come from the registered construction inputs (`ProblemSchemaEntry.fields` or the model-local `CreateSpec`), with a reusable codec for any unusual transport syntax. | | 14 | Canonical model example registered | `Grep("{P}", "src/example_db/model_builders.rs")` | | 15 | Paper `display-name` entry | `Grep('"{P}"', "docs/paper/reductions.typ")` | | 16 | Paper `problem-def` block | `Grep('problem-def.*"{P}"', "docs/paper/reductions.typ")` | +| 17 | Numeric and error contracts | Derive the expected boundary representation from the mathematical definition, then compare schema types, Rust fields, aggregate/total type, constructor and serde validation, conversions, overflow behavior, and boundary tests against `docs/src/design.md#numeric-types-and-arithmetic`. Verify construction paths return `ConstructionError`, `evaluate()` returns `EvaluationError`, and no public model path returns `Result<_, String>`. | ### Rule Checklist @@ -81,16 +82,17 @@ Only run if review type includes "rule". Given: source `S`, target `T`, rule fil | 6 | Test file exists | `Glob("src/unit_tests/rules/{R}.rs")` | | 7 | Closed-loop test present | `Grep("fn test_.*closed_loop\|fn test_.*to_.*basic", test_file)` | | 8 | Registered in `rules/mod.rs` | `Grep("mod {R}", "src/rules/mod.rs")` | -| 9 | Canonical rule example registered | `Grep("{S}|{T}|{R}", "src/example_db/rule_builders.rs")` | +| 9 | Canonical rule example registered | `Grep("canonical_rule_example_specs", rule file)` and verify it is included by `src/rules/mod.rs` | | 10 | Example-db lookup tests exist | `Grep("find_rule_example|build_rule_db", "src/unit_tests/example_db.rs")` | | 11 | Paper `reduction-rule` entry | `Grep('reduction-rule.*"{S}".*"{T}"', "docs/paper/reductions.typ")` | +| 12 | Extraction contract | Direct decoders call `validate_target_solution()`, enforce rule-specific structure, and test malformed cases; the helper does not establish feasibility or optimality. Composed extractors may delegate. | +| 13 | Numeric and error contracts | Compare source/target boundary types, size arithmetic, coefficients, bounds, auxiliary IDs, conversions, overflow behavior, and boundary tests against `docs/src/design.md#numeric-types-and-arithmetic`. Verify public reduction paths return `ReductionError`, preserve target `ConstructionError` as its construction cause, and never stringify or silently handle either failure. | ## Step 2b: Blacklisted File Check Scan the PR's changed files for auto-generated files that must never be committed: - `docs/src/reductions/reduction_graph.json` - `docs/src/reductions/problem_schemas.json` -- `src/example_db/fixtures/examples.json` (legacy path, deleted on main) - `docs/paper/data/examples.json` (current output path, gitignored) If any of these files appear in the diff, report **FAIL — blacklisted auto-generated file committed**. These files are rebuilt by CI/`make doc`/`make paper` and must not be in PRs. @@ -111,12 +113,14 @@ Report pass/fail. If tests fail, identify which tests. **Do NOT fix anything** 2. **`dims()` correctness** — Does it return the actual configuration space? (e.g., `vec![2; n]` for binary) 3. **Size getter consistency** — Do inherent getter methods (e.g., `num_vertices()`, `num_edges()`) match names used in overhead expressions? 4. **Weight handling** — Are weights managed via inherent methods, not traits? +5. **Numeric safety** — Are element and total types distinct where required, do serde and constructors enforce the same range, and are overflow and non-finite values rejected explicitly? ### For Rules: -1. **`extract_solution` correctness** — Does it correctly invert the reduction? Does the returned solution have the right length (source dimensions)? +1. **`extract_solution` correctness** — Does it implement the mathematical inverse? Is every branch either a defined mathematical case or an `ExtractionError`, with no defaulting, truncation, clamping, panic, or recovery? 2. **Overhead accuracy** — Does `overhead = { field = "expr" }` reflect the actual size relationship? 3. **Example quality** — Is it tutorial-style? Does the JSON export include both source and target data? 4. **Paper quality** — Is the reduction-rule statement precise? Is the proof sketch sound? +5. **Numeric safety** — Are target sizes and auxiliary IDs checked before construction, with no unchecked narrowing or exact-to-`f64` shortcut? ## Step 5: Issue Compliance Review diff --git a/.claude/skills/run-pipeline/SKILL.md b/.claude/skills/run-pipeline/SKILL.md index 1a0229a66..731b7e550 100644 --- a/.claude/skills/run-pipeline/SKILL.md +++ b/.claude/skills/run-pipeline/SKILL.md @@ -1,6 +1,6 @@ --- name: run-pipeline -description: Pick a Ready issue from the GitHub Project board, move it through In Progress -> issue-to-pr -> Review pool +description: Pick a Ready issue from the GitHub Project board, move it from In Progress through issue-to-pr into Review pool --- # Run Pipeline @@ -79,7 +79,7 @@ Score only **eligible** issues on three criteria. For `[Model]` issues, extract | Criterion | Weight | How to Assess | |-----------|--------|---------------| | **C1: Industrial/Theoretical Importance** | 3 | Read the report's issue summary for each eligible issue. Score 0-2: **2** = widely used in industry or foundational in complexity theory (e.g., ILP, SAT, MaxFlow, TSP, GraphColoring); **1** = moderately important or well-studied (e.g., SubsetSum, SetCover, Knapsack); **0** = niche or primarily academic | -| **C2: Related to Existing Problems** | 2 | Use the report's Ready/In-progress context plus `pred list` if needed. Score 0-2: **2** = directly related (shares input structure or has known reductions to/from ≥2 existing problems, but is NOT a trivial variant of an existing one); **1** = loosely related (same domain, connects to 1 existing problem); **0** = isolated or is essentially a variant/renaming of an existing problem | +| **C2: Related to Existing Problems** | 2 | Use the report's Ready/In-progress context plus `pred list ` or `pred list --json` if needed. Score 0-2: **2** = directly related (shares input structure or has known reductions to/from ≥2 existing problems, but is NOT a trivial variant of an existing one); **1** = loosely related (same domain, connects to 1 existing problem); **0** = isolated or is essentially a variant/renaming of an existing problem | | **C3: Unblocks Pending Rules** | 2 | Read the `Pending rules unblocked` count already printed in the report for each eligible issue. Score 0-2: **2** = unblocks ≥2 pending rules; **1** = unblocks 1 pending rule; **0** = does not unblock any pending rule | **Final score** = C1 × 3 + C2 × 2 + C3 × 2 (max = 12) diff --git a/.claude/skills/topology-sanity-check/SKILL.md b/.claude/skills/topology-sanity-check/SKILL.md index ba160ce0c..12c405ede 100644 --- a/.claude/skills/topology-sanity-check/SKILL.md +++ b/.claude/skills/topology-sanity-check/SKILL.md @@ -53,7 +53,7 @@ Parse the output and produce: | # | Problem | Variants | Category | |---|---------|----------|----------| -| 1 | ProblemName | 2 (f64, i32) | algebraic | +| 1 | ProblemName | 2 (f64, i64) | algebraic | ### Existing Issues @@ -144,7 +144,7 @@ This runs the analysis from `src/rules/analysis.rs` which: Always report rules with full variant-qualified endpoints, not just base names. Use the same display style as `ReductionStep`, e.g. -`MaximumIndependentSet {graph: "SimpleGraph", weight: "One"} -> MaximumIndependentSet {graph: "KingsSubgraph", weight: "i32"}`. +`MaximumIndependentSet {graph: "SimpleGraph", weight: "One"} -> MaximumIndependentSet {graph: "KingsSubgraph", weight: "i64"}`. Base-name-only summaries are ambiguous and can hide cast-only paths. Parse the test output and report: diff --git a/.claude/skills/verify-reduction/SKILL.md b/.claude/skills/verify-reduction/SKILL.md index 76f1246f6..91765ce3f 100644 --- a/.claude/skills/verify-reduction/SKILL.md +++ b/.claude/skills/verify-reduction/SKILL.md @@ -1,6 +1,6 @@ --- name: verify-reduction -description: Standalone mathematical verification of a reduction rule — generates Typst proof, constructor Python script (>=5000 checks), and adversary Python script (>=5000 independent checks). Reports verdict. No artifacts saved. +description: Standalone mathematical verification of a reduction rule — generates a Typst proof plus constructor and independent adversary scripts with at least 5000 checks each. Reports a verdict without saving artifacts. --- # Verify Reduction @@ -36,22 +36,61 @@ pred show --json ### Type compatibility gate — MANDATORY -Check source/target `Value` types before any work: +Check source/target `Value` types before any work. The `grep` only locates the definitions; it does +not resolve generic parameters or associated types: ```bash grep "type Value = " src/models/*/.rs src/models/*/.rs ``` +Resolve both concrete types completely before declaring compatibility: + +1. Substitute every concrete generic argument from the proposed rule. +2. Follow every type alias and associated type to its defining `impl`. +3. Record the substitution chain and the source file evidence in the verification report. +4. If any generic or associated type remains unresolved, run a compile-backed temporary Rust probe + using `std::any::type_name::<::Value>()`. Build the probe from `/tmp` + with a path dependency on this repository; do not modify the repository. + +Never infer a Rust value type from the mathematical problem name, from unit-weight terminology, or +from the Python verifier's integer representation. In particular, arbitrary-precision Python +integers do not establish that a Rust objective type is `usize` or that it is closed under all +legal source instances. + +Required report format: + +```text +TYPE RESOLUTION: + Source syntax: Min + Substitutions: W = One; ::Sum = i64 + Source resolved: Min + Target syntax: Min + Target resolved: Min + Full-domain compatibility: FAILED +``` + **Compatible pairs for `ReduceTo` (witness-capable):** -- `Or`->`Or`, `Min`->`Min`, `Max`->`Max` (same type) +- `Or`->`Or` +- `Min`->`Min`, `Max`->`Max` (identical resolved inner type) - `Or`->`Min`, `Or`->`Max` (feasibility embeds into optimization) +`Min`->`Min` or `Max`->`Max` with `S != T` is not automatically compatible. Proceed +only if the rule or source model declares a bound covering every legal source instance and the +verification proves a total, order-preserving conversion over that full declared domain. Otherwise +STOP and report a value-domain mismatch. + **Incompatible — STOP if any of these:** - `Min`->`Or` or `Max`->`Or` — optimization source has no threshold K; needs a decision-variant source model - `Max`->`Min` or `Min`->`Max` — opposite optimization directions; needs `ReduceToAggregate` or a decision-variant wrapper - `Or`->`Sum` or `Min`->`Sum` — Sum is aggregate-only; needs `ReduceToAggregate` - Any pair involving `And` or `Sum` on the target side +**Regression case:** `MinimumDominatingSet` resolves to `Min` because +`::Sum = i64`; `MinimumHittingSet` resolves to `Min`. Report +`Min -> Min`, not `Min -> Min`. Without an explicit source-size bound, +the full-domain type gate fails even though the classical cardinality reduction is mathematically +correct and exhaustive small-instance checks pass. + If incompatible, STOP and report the type mismatch and options. Do NOT proceed. ### If compatible @@ -199,6 +238,10 @@ Every item must be YES. If any is NO, go back and fix. - [ ] Zero hand-waving language - [ ] Zero scratch work +### Type gate +- [ ] Concrete Rust `Value` types fully resolved with substitution evidence +- [ ] Different numeric domains either rejected or covered by an explicit full-domain range proof + ### Constructor Python - [ ] 0 failures, >=5,000 total checks - [ ] All 7 sections present and non-empty diff --git a/.claude/skills/write-model-in-paper/SKILL.md b/.claude/skills/write-model-in-paper/SKILL.md index 9ded09507..d8ca47ba2 100644 --- a/.claude/skills/write-model-in-paper/SKILL.md +++ b/.claude/skills/write-model-in-paper/SKILL.md @@ -126,16 +126,16 @@ achieves $O^*(2^n)$ @bjorklund2009. ### 3c. Example with Visualization -A concrete small instance that illustrates the problem. **The example must use data from the checked-in canonical fixture DB**, not an independently invented instance. +A concrete small instance that illustrates the problem. **Use the generated canonical example data**, not an independently invented instance. #### Sourcing example data -1. If you changed example builders/specs, run `make regenerate-fixtures` to refresh `src/example_db/fixtures/examples.json`. -2. Find the problem's entry in `src/example_db/fixtures/examples.json` under `models` — it contains the canonical `instance`, `samples`, and `optimal` fields. +1. If you changed example builders/specs, run `cargo run --features "example-db" --example export_examples`. +2. Find the problem's entry in `docs/paper/data/examples.json` under `models` — it contains the canonical `instance`, `samples`, and `optimal` fields. 3. Use the values from `instance` in the paper example (translating 0-indexed code values to 1-indexed math notation where conventional, e.g., vertices {0,...,n-1} → {1,...,n}). 4. Use `optimal` configurations to show the solution. -**Do not invent a different instance.** If the canonical example is too large or not pedagogically ideal, fix it in `canonical_model_example_specs()` first, re-run `make regenerate-fixtures`, then write the paper entry from the updated JSON. +**Do not invent a different instance.** If the canonical example is unsuitable, fix it in `canonical_model_example_specs()`, re-run `export_examples`, then use the updated JSON. #### Requirements @@ -176,8 +176,6 @@ Add a `pred-commands()` block after the `*Example.*` paragraph and before the `# If `docs/paper/reductions.typ` already defines a shared `problem-spec()` helper, reuse it instead of reintroducing it locally. Do **not** guess whether the default variant matches the canonical example; canonical fixtures may live on non-default variants, and handwritten bare aliases can silently produce broken commands. -For satisfaction problems, replace `pred solve` with `pred solve .json --solver brute-force` if the problem has no ILP reduction path. - **For graph problems**, use the paper's existing graph helpers: - `petersen-graph()`, `house-graph()` or define custom vertex/edge lists - `canvas(length: ..., { ... })` with `g-node()` and `g-edge()` @@ -206,7 +204,7 @@ make paper - [ ] **Notation self-contained**: every symbol in `def` is defined before first use - [ ] **Background present**: historical context, applications, or structural properties - [ ] **Algorithms cited**: every complexity claim has `@citation` or footnote warning -- [ ] **Example from JSON**: instance data matches `src/example_db/fixtures/examples.json` canonical example (not independently invented) +- [ ] **Example from JSON**: instance data matches the canonical entry in `docs/paper/data/examples.json` - [ ] **Evaluation shown**: objective/verifier computed on the example solution - [ ] **Diagram included**: figure with caption and label for graph/matrix/set visualization - [ ] **Paper compiles**: `make paper` succeeds without errors diff --git a/.claude/skills/write-rule-in-paper/SKILL.md b/.claude/skills/write-rule-in-paper/SKILL.md index 0d9755b1b..e7d3c884b 100644 --- a/.claude/skills/write-rule-in-paper/SKILL.md +++ b/.claude/skills/write-rule-in-paper/SKILL.md @@ -7,7 +7,7 @@ description: Use when writing or improving a reduction-rule entry in the Typst p Full authoring guide for writing a `reduction-rule` entry in `docs/paper/reductions.typ`. Covers Typst mechanics, writing quality, and verification. -> **Note:** This content is also inlined in `add-rule` Step 5 (condensed form). This standalone version has more detail and is useful for improving existing entries. +> **Note:** This content is also inlined in `add-rule` Step 6 (condensed form). This standalone version has more detail and is useful for improving existing entries. ## Reference Example @@ -17,14 +17,14 @@ Full authoring guide for writing a `reduction-rule` entry in `docs/paper/reducti Before using this skill, ensure: - The reduction is implemented and tested (`src/rules/_.rs`) -- A canonical example exists in `src/example_db/rule_builders.rs` -- If the canonical example changed, fixtures are regenerated (`make regenerate-fixtures`) +- A rule-local `canonical_rule_example_specs()` exists and is included by `src/rules/mod.rs` +- If the canonical example changed, regenerate the paper data with `cargo run --features "example-db" --example export_examples` - The reduction graph and schemas are up to date (`cargo run --example export_graph && cargo run --example export_schemas`) ## Source Material For mathematical content (theorems, proofs, examples), consult these sources in priority order: -1. **GitHub issue** for the rule (`gh issue view `): contains the verified reduction algorithm, correctness proof, size overhead, and worked examples written during issue creation +1. **GitHub issue** for the rule (`gh issue view `): contains the verified reduction algorithm, correctness proof, parameter transform, and worked examples written during issue creation 2. **Derivation documents** (if available): e.g., `~/Downloads/reduction_derivations_*.typ` — these contain batch-verified proofs with explicit theorem/proof blocks 3. **The implementation** (`src/rules/_.rs`): the code is the ground truth for the construction @@ -38,7 +38,7 @@ Do NOT invent proofs — always cross-check against the issue and derivation sou ``` Where: -- `load-example(source, target, ...)` looks up the canonical rule entry from `src/example_db/fixtures/examples.json` +- `load-example(source, target, ...)` looks up the canonical rule entry from `docs/paper/data/examples.json` - The returned record contains `source`, `target`, and `solutions` - Access fields: `src_tgt.source.instance`, `src_tgt.target.instance`, `src_tgt_sol.source_config`, `src_tgt_sol.target_config` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e703c4fcd..3ef1a9fd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,16 +49,65 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 - name: Run clippy - run: cargo clippy --all-targets --features ilp-highs -- -D warnings + run: cargo clippy --all-targets --features example-db -- -D warnings - # Cross-compile the portable Rust surface to RISC-V Linux, then execute the - # CLI under QEMU user-mode emulation to verify runtime behavior (not just - # that the binary links). + # Build and exercise the HiGHS-backed CLI natively on Apple Silicon. + macos-arm64: + name: macOS ARM64 build & run + runs-on: macos-15 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Verify ARM64 host + run: "rustc -vV | grep 'host: aarch64-apple-darwin'" + - name: Build workspace + run: cargo build --workspace + - name: Run CLI and ILP smoke test + run: | + target/debug/pred list | head -5 + target/debug/pred create MaximumIndependentSet --graph 0-1,1-2,2-3,3-4,4-0 -o mis.json + target/debug/pred solve mis.json --solver ilp | tee solve.out + grep -q '"kind": "ilp"' solve.out + grep -q '"evaluation": "Max(2)"' solve.out + + # Build and exercise the HiGHS-backed CLI natively on 64-bit Windows. + windows-x86_64: + name: Windows x86_64 build & run + runs-on: windows-2025 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Verify x86_64 host + shell: pwsh + run: | + $hostInfo = rustc -vV | Out-String + if ($hostInfo -notmatch 'host: x86_64-pc-windows-msvc') { + throw "Unexpected Rust host:`n$hostInfo" + } + - name: Build workspace + run: cargo build --workspace + - name: Run CLI and ILP smoke test + shell: pwsh + run: | + $pred = 'target/debug/pred.exe' + & $pred list | Select-Object -First 5 + & $pred create MaximumIndependentSet --graph 0-1,1-2,2-3,3-4,4-0 -o mis.json + & $pred solve mis.json --solver ilp | Tee-Object -FilePath solve.out + $solveOutput = Get-Content solve.out -Raw + if ($solveOutput -notmatch '"kind": "ilp"') { + throw 'Expected the ILP solver to run' + } + if ($solveOutput -notmatch '"evaluation": "Max\(2\)"') { + throw 'Expected the maximum independent set value to be 2' + } + + # Cross-compile the full HiGHS-backed CLI to RISC-V Linux, then execute an + # ILP solve under QEMU user-mode emulation (not just a link check). riscv: name: RISC-V build & run runs-on: ubuntu-latest - env: - FEATURES: "ilp-lp-solvers" steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable @@ -68,11 +117,13 @@ jobs: - name: Install RISC-V cross tools and QEMU run: | sudo apt-get update - sudo apt-get install -y gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu qemu-user + sudo apt-get install -y gcc-riscv64-linux-gnu g++-riscv64-linux-gnu binutils-riscv64-linux-gnu qemu-user - name: Build workspace for RISC-V env: CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc - run: cargo build --workspace --no-default-features --features "$FEATURES" --target riscv64gc-unknown-linux-gnu + CC_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-gcc + CXX_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-g++ + run: cargo build --workspace --target riscv64gc-unknown-linux-gnu - name: Verify RISC-V executable run: | riscv64-linux-gnu-readelf -h target/riscv64gc-unknown-linux-gnu/debug/pred \ @@ -83,9 +134,10 @@ jobs: run: | # Registry loads and the catalog renders on RISC-V. $PRED list | head -5 - # End-to-end create + brute-force solve: MIS of a 5-cycle is 2. + # End-to-end reduction and HiGHS solve: MIS of a 5-cycle is 2. $PRED create MaximumIndependentSet --graph 0-1,1-2,2-3,3-4,4-0 -o mis.json - $PRED solve mis.json --solver brute-force | tee solve.out + $PRED solve mis.json --solver ilp | tee solve.out + grep -q '"kind": "ilp"' solve.out grep -q '"evaluation": "Max(2)"' solve.out # Build, test (nextest), doc tests, and paper. @@ -95,7 +147,7 @@ jobs: # Single feature set across compile + test + doctest so artifacts are reused # (no redundant full recompile between steps). env: - FEATURES: "ilp-highs example-db" + FEATURES: "example-db" steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable @@ -109,14 +161,6 @@ jobs: - name: Compile tests run: cargo nextest run --no-run --workspace --features "$FEATURES" - # The subprocess example tests (tests/suites/examples.rs) shell out to - # `cargo run --example … --features ilp-highs`. Pre-build those example - # binaries with that exact feature set so the subprocess reuses artifacts - # instead of recompiling the whole crate mid-test (which otherwise adds - # 60s+ to a single test's wall-clock and would trip the nextest timeout). - - name: Build examples (for subprocess tests) - run: cargo build --examples --features ilp-highs - - name: Run tests run: cargo nextest run --workspace --features "$FEATURES" @@ -127,8 +171,8 @@ jobs: - name: Build paper run: make paper - # Coverage. Feature set intentionally matches the historical coverage gate - # (ilp-highs only) to keep the codecov baseline stable. + # Coverage intentionally excludes the optional example database to keep the + # historical codecov baseline stable. coverage: name: Code Coverage runs-on: ubuntu-latest @@ -142,7 +186,7 @@ jobs: tool: cargo-llvm-cov,nextest - uses: Swatinem/rust-cache@v2 - name: Generate coverage - run: cargo llvm-cov nextest --features ilp-highs --workspace --lcov --output-path lcov.info + run: cargo llvm-cov nextest --workspace --lcov --output-path lcov.info - name: Upload to codecov.io uses: codecov/codecov-action@v5 with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a262a1d0a..60aa9272e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -61,7 +61,7 @@ jobs: run: typst compile --root . docs/paper/reductions.typ book/reductions.pdf - name: Build rustdoc - run: RUSTDOCFLAGS="--default-theme=dark" cargo doc --features ilp-highs --no-deps + run: RUSTDOCFLAGS="--default-theme=dark" cargo doc --no-deps - name: Combine documentation run: | diff --git a/Cargo.toml b/Cargo.toml index 3b0066232..1fa1906fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] -members = [".", "problemreductions-macros", "problemreductions-cli"] +members = [ + ".", + "problemreductions-expr", + "problemreductions-macros", + "problemreductions-cli", +] [package] name = "problemreductions" @@ -12,41 +17,47 @@ keywords = ["np-hard", "optimization", "reduction", "sat", "graph"] categories = ["algorithms", "science"] [features] -default = ["ilp-highs"] example-db = [] -ilp = ["ilp-highs"] # backward compat shorthand -ilp-solver = [] # marker: enables ILP solver code -ilp-highs = ["ilp-solver", "dep:good_lp", "good_lp/highs"] -ilp-cplex = ["ilp-solver", "dep:good_lp", "good_lp/cplex-rs"] -ilp-lp-solvers = ["ilp-solver", "dep:good_lp", "good_lp/lp-solvers"] +benchmarks = ["dep:criterion"] [dependencies] petgraph = { version = "0.8", features = ["serde-1"] } bitvec = "1.0" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +# Persisted problem and reduction data must preserve every finite f64 on replay. +serde_json = { version = "1.0", features = ["float_roundtrip"] } thiserror = "2.0" num-bigint = "0.4" +num-rational = "0.4" num-traits = "0.2" -good_lp = { version = "=1.14.2", default-features = false, optional = true } +good_lp = { version = "=1.14.2", default-features = false, features = ["highs"] } inventory = "0.3" ordered-float = "5.0" rand = "0.10" +criterion = { version = "0.8", optional = true } problemreductions-macros = { version = "0.6.0", path = "problemreductions-macros" } +problemreductions-expr = { version = "0.6.0", path = "problemreductions-expr" } [dev-dependencies] proptest = "1.0" -criterion = "0.8" [[bench]] name = "solver_benchmarks" harness = false +required-features = ["benchmarks"] [[example]] name = "export_examples" path = "examples/export_examples.rs" required-features = ["example-db"] +[profile.dev] +debug = "line-tables-only" + +[profile.debug-dev] +inherits = "dev" +debug = "full" + [profile.release] lto = true codegen-units = 1 diff --git a/Makefile b/Makefile index c18221e67..1cb637f32 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ # Makefile for problemreductions -.PHONY: help build test mcp-test fmt clippy doc mdbook website paper clean coverage rust-export compare qubo-testdata export-schemas release run-plan run-issue run-pipeline run-pipeline-forever run-review run-review-forever board-next board-claim board-ack board-move issue-context issue-guards pr-context pr-wait-ci worktree-issue worktree-pr diagrams jl-testdata cli cli-demo copilot-review papers papers-lookup papers-download papers-scihub papers-status papers-push papers-pull papers-index +.PHONY: help build test bench mcp-test fmt clippy doc mdbook website paper clean coverage rust-export compare qubo-testdata export-schemas release run-plan run-issue run-pipeline run-pipeline-forever run-review run-review-forever board-next board-claim board-ack board-move issue-context issue-guards pr-context pr-wait-ci worktree-issue worktree-pr diagrams jl-testdata cli cli-demo copilot-review papers papers-lookup papers-download papers-scihub papers-status papers-push papers-pull papers-index RUNNER ?= codex CLAUDE_MODEL ?= opus CODEX_MODEL ?= gpt-5.4 +TEST_FEATURES := example-db # Cross-platform sed in-place: macOS needs -i '', Linux needs -i SED_I := sed -i$(shell if [ "$$(uname)" = "Darwin" ]; then echo " ''"; fi) @@ -14,6 +15,7 @@ help: @echo "Available targets:" @echo " build - Build the project" @echo " test - Run all tests" + @echo " bench - Run solver benchmarks" @echo " mcp-test - Run MCP server tests" @echo " fmt - Format code with rustfmt" @echo " fmt-check - Check code formatting" @@ -22,7 +24,7 @@ help: @echo " diagrams - Generate SVG diagrams from Typst (light + dark)" @echo " mdbook - Build and serve mdBook (with live reload)" @echo " website - Build the research website and documentation" - @echo " paper - Build Typst paper from checked-in fixtures (requires typst)" + @echo " paper - Generate example data and build the Typst paper (requires typst)" @echo " coverage - Generate coverage report (requires cargo-llvm-cov)" @echo " clean - Clean build artifacts" @echo " check - Quick check (fmt + clippy + test)" @@ -63,11 +65,15 @@ help: # Build the project build: - cargo build --features ilp-highs + cargo build # Run all workspace tests (including ignored tests) test: - cargo test --features "ilp-highs example-db" --workspace -- --include-ignored + cargo test --features "$(TEST_FEATURES)" --workspace -- --include-ignored + +# Compile Criterion only when benchmarks are requested +bench: + cargo bench --features benchmarks # Run MCP server tests mcp-test: ## Run MCP server tests @@ -83,7 +89,7 @@ fmt-check: # Run clippy clippy: - cargo clippy --all-targets --features ilp-highs -- -D warnings + cargo clippy --all-targets --features "$(TEST_FEATURES)" -- -D warnings node_modules/elkjs/package.json: package.json package-lock.json npm ci @@ -97,7 +103,7 @@ doc: node_modules/elkjs/package.json bash scripts/generate_doc_snippets.sh target/debug/pred mdbook build python3 scripts/build_website.py - RUSTDOCFLAGS="--default-theme=dark" cargo doc --features ilp-highs --no-deps + RUSTDOCFLAGS="--default-theme=dark" cargo doc --no-deps rm -rf book/api cp -r target/doc book/api @@ -133,7 +139,7 @@ mdbook: node_modules/elkjs/package.json @cargo build -p problemreductions-cli --bin pred @bash scripts/generate_doc_snippets.sh target/debug/pred @echo "Building API docs..." - @RUSTDOCFLAGS="--default-theme=dark" cargo doc --features ilp-highs --no-deps 2>&1 | tail -1 + @RUSTDOCFLAGS="--default-theme=dark" cargo doc --no-deps 2>&1 | tail -1 @echo "Building mdBook..." @mdbook build @python3 scripts/build_website.py @@ -151,16 +157,16 @@ export-schemas: # Build Typst paper (generates example data on demand) paper: - cargo run --features "example-db" --example export_examples - cargo run --example export_petersen_mapping - cargo run --example export_graph - cargo run --example export_schemas + cargo run --features "$(TEST_FEATURES)" --example export_examples + cargo run --features "$(TEST_FEATURES)" --example export_petersen_mapping + cargo run --features "$(TEST_FEATURES)" --example export_graph + cargo run --features "$(TEST_FEATURES)" --example export_schemas typst compile --root . docs/paper/reductions.typ docs/paper/reductions.pdf # Generate coverage report (requires: cargo install cargo-llvm-cov) coverage: @command -v cargo-llvm-cov >/dev/null 2>&1 || { echo "Installing cargo-llvm-cov..."; cargo install cargo-llvm-cov; } - cargo llvm-cov --features ilp-highs --workspace --html --open + cargo llvm-cov --workspace --html --open # Clean build artifacts clean: @@ -223,14 +229,17 @@ compare: rust-export echo ""; \ echo "=== $$graph ==="; \ echo "-- unweighted --"; \ - echo "Julia: $$(jq '{nodes: .num_grid_nodes, overhead: .mis_overhead, tape: .num_tape_entries}' tests/data/$${graph}_unweighted_trace.json)"; \ - echo "Rust: $$(jq '{nodes: .stages[3].num_nodes, overhead: .total_overhead, tape: ((.crossing_tape | length) + (.simplifier_tape | length))}' tests/data/$${graph}_rust_unweighted.json)"; \ + julia=$$(jq -c '{nodes: .num_grid_nodes, overhead: .mis_overhead, tape: (.tape | length)}' tests/data/$${graph}_unweighted_trace.json); \ + rust=$$(jq -c '{nodes: .stages[3].num_nodes, overhead: .total_overhead, tape: ((.crossing_tape | length) + (.simplifier_tape | length))}' tests/julia/$${graph}_rust_unweighted.json); \ + echo "Julia: $$julia"; echo "Rust: $$rust"; test "$$julia" = "$$rust" || exit 1; \ echo "-- weighted --"; \ - echo "Julia: $$(jq '{nodes: .num_grid_nodes, overhead: .mis_overhead, tape: .num_tape_entries}' tests/data/$${graph}_weighted_trace.json)"; \ - echo "Rust: $$(jq '{nodes: .stages[3].num_nodes, overhead: .total_overhead, tape: ((.crossing_tape | length) + (.simplifier_tape | length))}' tests/data/$${graph}_rust_weighted.json)"; \ + julia=$$(jq -c '{nodes: .num_grid_nodes, overhead: .mis_overhead, tape: (.tape | length)}' tests/data/$${graph}_weighted_trace.json); \ + rust=$$(jq -c '{nodes: .stages[3].num_nodes, overhead: .total_overhead, tape: ((.crossing_tape | length) + (.simplifier_tape | length))}' tests/julia/$${graph}_rust_weighted.json); \ + echo "Julia: $$julia"; echo "Rust: $$rust"; test "$$julia" = "$$rust" || exit 1; \ echo "-- triangular --"; \ - echo "Julia: $$(jq '{nodes: .num_grid_nodes, overhead: .mis_overhead, tape: .num_tape_entries}' tests/data/$${graph}_triangular_trace.json)"; \ - echo "Rust: $$(jq '{nodes: .stages[3].num_nodes, overhead: .total_overhead, tape: ((.crossing_tape | length) + (.simplifier_tape | length))}' tests/data/$${graph}_rust_triangular.json)"; \ + julia=$$(jq -c '{nodes: .num_grid_nodes, overhead: .mis_overhead, tape: (.tape | length)}' tests/data/$${graph}_triangular_trace.json); \ + rust=$$(jq -c '{nodes: .stages[3].num_nodes, overhead: .total_overhead, tape: ((.crossing_tape | length) + (.simplifier_tape | length))}' tests/julia/$${graph}_rust_triangular.json); \ + echo "Julia: $$julia"; echo "Rust: $$rust"; test "$$julia" = "$$rust" || exit 1; \ done # Run a plan with Codex or Claude @@ -300,16 +309,12 @@ cli-demo: cli $$PRED from QUBO --hops 1; \ \ echo ""; \ - echo "--- 5. path: find reduction paths ---"; \ + echo "--- 5. path: symbolic path enumeration ---"; \ $$PRED path MIS QUBO; \ - $$PRED path MIS QUBO -o $(CLI_DEMO_DIR)/path_mis_qubo.json; \ $$PRED path Factoring SpinGlass; \ - $$PRED path MIS QUBO --cost minimize:num_variables; \ - \ - echo ""; \ - echo "--- 6. path --all: enumerate all paths ---"; \ - $$PRED path MIS QUBO --all; \ - $$PRED path MIS QUBO --all -o $(CLI_DEMO_DIR)/all_paths/; \ + echo "--- 5b. explicitly choose one route from the path set ---"; \ + $$PRED path MIS QUBO -o $(CLI_DEMO_DIR)/paths_mis_qubo.json; \ + jq -e 'first(.paths[] | select(([.path[0].from.name] + [.path[].to.name]) == ["MaximumIndependentSet", "MaximumIndependentSet", "MaximumSetPacking", "MaximumSetPacking", "QUBO"]))' $(CLI_DEMO_DIR)/paths_mis_qubo.json > $(CLI_DEMO_DIR)/path_mis_qubo.json; \ \ echo ""; \ echo "--- 7. export-graph: full reduction graph ---"; \ @@ -318,7 +323,7 @@ cli-demo: cli echo ""; \ echo "--- 8. create: build problem instances ---"; \ $$PRED create MIS --graph 0-1,1-2,2-3,3-4,4-0 -o $(CLI_DEMO_DIR)/mis.json; \ - $$PRED create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o $(CLI_DEMO_DIR)/mis_weighted.json; \ + $$PRED create MaximumIndependentSet/SimpleGraph/i64 --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o $(CLI_DEMO_DIR)/mis_weighted.json; \ $$PRED create SAT --num-vars 3 --clauses "1,2;-1,3;2,-3" -o $(CLI_DEMO_DIR)/sat.json; \ $$PRED create 3SAT --num-vars 4 --clauses "1,2,3;-1,2,-3;1,-2,3" -o $(CLI_DEMO_DIR)/3sat.json; \ $$PRED create QUBO --matrix "1,-0.5;-0.5,2" -o $(CLI_DEMO_DIR)/qubo.json; \ @@ -351,8 +356,8 @@ cli-demo: cli $$PRED solve $(CLI_DEMO_DIR)/mis_weighted.json; \ \ echo ""; \ - echo "--- 13. reduce: MIS → QUBO (auto-discover path) ---"; \ - $$PRED reduce $(CLI_DEMO_DIR)/mis.json --to QUBO -o $(CLI_DEMO_DIR)/bundle_qubo.json; \ + echo "--- 13. reduce: MIS → QUBO along the explicitly chosen route ---"; \ + $$PRED reduce $(CLI_DEMO_DIR)/mis.json --via $(CLI_DEMO_DIR)/path_mis_qubo.json -o $(CLI_DEMO_DIR)/bundle_qubo.json; \ \ echo ""; \ echo "--- 14. solve bundle: brute-force on reduced QUBO ---"; \ @@ -364,7 +369,9 @@ cli-demo: cli \ echo ""; \ echo "--- 16. solve bundle with ILP: MIS → MVC → ILP ---"; \ - $$PRED reduce $(CLI_DEMO_DIR)/mis.json --to MVC -o $(CLI_DEMO_DIR)/bundle_mvc.json; \ + $$PRED path MIS MVC -o $(CLI_DEMO_DIR)/paths_mis_mvc.json; \ + jq -e 'first(.paths[] | select(([.path[0].from.name] + [.path[].to.name]) == ["MaximumIndependentSet", "MaximumIndependentSet", "MinimumVertexCover"]))' $(CLI_DEMO_DIR)/paths_mis_mvc.json > $(CLI_DEMO_DIR)/path_mis_mvc.json; \ + $$PRED reduce $(CLI_DEMO_DIR)/mis.json --via $(CLI_DEMO_DIR)/path_mis_mvc.json -o $(CLI_DEMO_DIR)/bundle_mvc.json; \ $$PRED solve $(CLI_DEMO_DIR)/bundle_mvc.json --solver ilp; \ \ echo ""; \ @@ -381,7 +388,7 @@ cli-demo: cli echo "Solving with ILP..."; \ $$PRED solve $(CLI_DEMO_DIR)/big.json -o $(CLI_DEMO_DIR)/big_sol.json; \ echo "Reducing to QUBO and solving with brute-force..."; \ - $$PRED reduce $(CLI_DEMO_DIR)/big.json --to QUBO -o $(CLI_DEMO_DIR)/big_qubo.json; \ + $$PRED reduce $(CLI_DEMO_DIR)/big.json --via $(CLI_DEMO_DIR)/path_mis_qubo.json -o $(CLI_DEMO_DIR)/big_qubo.json; \ $$PRED solve $(CLI_DEMO_DIR)/big_qubo.json --solver brute-force -o $(CLI_DEMO_DIR)/big_qubo_sol.json; \ echo "Verifying both solutions have the same evaluation..."; \ ILP_EVAL=$$(jq -r '.evaluation' $(CLI_DEMO_DIR)/big_sol.json); \ diff --git a/benches/solver_benchmarks.rs b/benches/solver_benchmarks.rs index 69572e1d5..72dffb2e4 100644 --- a/benches/solver_benchmarks.rs +++ b/benches/solver_benchmarks.rs @@ -17,11 +17,11 @@ fn bench_independent_set(c: &mut Criterion) { for n in [4, 6, 8, 10].iter() { // Create a path graph with n vertices let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); - let problem = MaximumIndependentSet::new(SimpleGraph::new(*n, edges), vec![1i32; *n]); + let problem = MaximumIndependentSet::new(SimpleGraph::new(*n, edges), vec![1i64; *n]); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_witness(black_box(&problem))) + b.iter(|| solver.solve(black_box(&problem))) }); } @@ -34,11 +34,11 @@ fn bench_vertex_covering(c: &mut Criterion) { for n in [4, 6, 8, 10].iter() { let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); - let problem = MinimumVertexCover::new(SimpleGraph::new(*n, edges), vec![1i32; *n]); + let problem = MinimumVertexCover::new(SimpleGraph::new(*n, edges), vec![1i64; *n]); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_witness(black_box(&problem))) + b.iter(|| solver.solve(black_box(&problem))) }); } @@ -51,12 +51,12 @@ fn bench_max_cut(c: &mut Criterion) { for n in [4, 6, 8, 10].iter() { let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); - let weights = vec![1i32; edges.len()]; + let weights = vec![1i64; edges.len()]; let problem = MaxCut::new(SimpleGraph::new(*n, edges), weights); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_witness(black_box(&problem))) + b.iter(|| solver.solve(black_box(&problem))) }); } @@ -72,9 +72,9 @@ fn bench_satisfiability(c: &mut Criterion) { let clauses: Vec = (0..*num_vars) .map(|i| { CNFClause::new(vec![ - (i as i32 + 1), - -((i + 1) as i32 % *num_vars as i32 + 1), - ((i + 2) as i32 % *num_vars as i32 + 1), + (i as i64 + 1), + -((i + 1) as i64 % *num_vars as i64 + 1), + ((i + 2) as i64 % *num_vars as i64 + 1), ]) }) .collect(); @@ -100,11 +100,11 @@ fn bench_spin_glass(c: &mut Criterion) { .map(|i| ((i, i + 1), if i % 2 == 0 { 1.0 } else { -1.0 })) .collect(); let onsite: Vec = vec![0.1; *n]; - let problem = SpinGlass::new(*n, interactions, onsite); + let problem = SpinGlass::new(*n, interactions, onsite).unwrap(); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("chain", n), n, |b, _| { - b.iter(|| solver.find_witness(black_box(&problem))) + b.iter(|| solver.solve(black_box(&problem))) }); } @@ -120,13 +120,13 @@ fn bench_set_covering(c: &mut Criterion) { let sets: Vec> = (0..*num_sets) .map(|i| vec![i, (i + 1) % *num_sets, (i + 2) % *num_sets]) .collect(); - let problem = MinimumSetCovering::::new(*num_sets, sets); + let problem = MinimumSetCovering::::new(*num_sets, sets); let solver = BruteForce::new(); group.bench_with_input( BenchmarkId::new("overlapping", num_sets), num_sets, - |b, _| b.iter(|| solver.find_witness(black_box(&problem))), + |b, _| b.iter(|| solver.solve(black_box(&problem))), ); } @@ -156,12 +156,12 @@ fn bench_matching(c: &mut Criterion) { for n in [4, 6, 8, 10].iter() { let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); - let weights = vec![1i32; edges.len()]; + let weights = vec![1i64; edges.len()]; let problem = MaximumMatching::new(SimpleGraph::new(*n, edges), weights); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_witness(black_box(&problem))) + b.iter(|| solver.solve(black_box(&problem))) }); } @@ -182,7 +182,7 @@ fn bench_paintshop(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("sequential", n), n, |b, _| { - b.iter(|| solver.find_witness(black_box(&problem))) + b.iter(|| solver.solve(black_box(&problem))) }); } @@ -198,10 +198,10 @@ fn bench_comparison(c: &mut Criterion) { // MaximumIndependentSet with 8 vertices let is_problem = MaximumIndependentSet::new( SimpleGraph::new(8, vec![(0, 1), (2, 3), (4, 5), (6, 7)]), - vec![1i32; 8], + vec![1i64; 8], ); group.bench_function("MaximumIndependentSet", |b| { - b.iter(|| solver.find_witness(black_box(&is_problem))) + b.iter(|| solver.solve(black_box(&is_problem))) }); // SAT with 8 variables @@ -223,9 +223,10 @@ fn bench_comparison(c: &mut Criterion) { 8, vec![((0, 1), 1.0), ((2, 3), -1.0), ((4, 5), 1.0), ((6, 7), -1.0)], vec![0.0; 8], - ); + ) + .unwrap(); group.bench_function("SpinGlass", |b| { - b.iter(|| solver.find_witness(black_box(&sg_problem))) + b.iter(|| solver.solve(black_box(&sg_problem))) }); // MaxCut with 8 vertices @@ -234,7 +235,7 @@ fn bench_comparison(c: &mut Criterion) { vec![1, 1, 1, 1], ); group.bench_function("MaxCut", |b| { - b.iter(|| solver.find_witness(black_box(&mc_problem))) + b.iter(|| solver.solve(black_box(&mc_problem))) }); group.finish(); diff --git a/docs/agent-profiles/FEATURES.md b/docs/agent-profiles/FEATURES.md index 7980ec62f..c4a03956e 100644 --- a/docs/agent-profiles/FEATURES.md +++ b/docs/agent-profiles/FEATURES.md @@ -6,5 +6,5 @@ - [Reduction Graph] — Automatic shortest-path search through registered reductions between problem types - [BruteForce Solver] — Enumerate all configurations to find optimal or satisfying solutions - [Variant System] — Graph/weight type parameterization with compile-time complexity registration -- [Overhead System] — Symbolic expressions describing how target problem size relates to source after reduction +- [Parameter Analysis] — Explain how canonical problem parameters transform along a path and measure complete instances - [Serialization] — JSON schema export and serde-based serialization for all problem types diff --git a/docs/agent-profiles/SKILLS.md b/docs/agent-profiles/SKILLS.md index b7c7a6e3a..df3ffde16 100644 --- a/docs/agent-profiles/SKILLS.md +++ b/docs/agent-profiles/SKILLS.md @@ -1,20 +1,20 @@ # Skills -Example generation now goes through the example catalog and checked-in fixture DB. +Example generation goes through the example catalog and generated paper data. When a workflow needs a paper/example instance, prefer the catalog path over ad hoc `examples/reduction_*.rs` binaries: -- use `src/example_db/fixtures/examples.json` directly for paper/example data -- use `make regenerate-fixtures` when canonical examples change +- use `docs/paper/data/examples.json` directly for paper/example data +- run `cargo run --features "example-db" --example export_examples` when canonical examples change - use `pred create --example ` to materialize a canonical model example as normal problem JSON - use `pred create --example --to ` to materialize a canonical rule example as normal problem JSON - when adding new example coverage, register a catalog entry instead of creating a new standalone reduction example file Post-refactor extension points: -- new model load/serialize/brute-force dispatch comes from `declare_variants!` in the model file, with explicit `opt` or `sat` markers and an optional `default` +- new model load/serialize/brute-force dispatch comes from `declare_variants!` in the model file, with an optional `default` - alias resolution lives in `problemreductions-cli/src/problem_name.rs` - `pred create` UX lives in `problemreductions-cli/src/commands/create.rs` -- canonical examples live in `src/example_db/model_builders.rs` and `src/example_db/rule_builders.rs` +- model examples live in `src/example_db/model_builders.rs`; rule examples live beside their rules and are collected by `src/rules/mod.rs` - [issue-to-pr] — Convert a GitHub issue into a PR with an implementation plan - [add-model] — Add a new problem model to the codebase diff --git a/docs/agent-profiles/pred-sym-prof-yuki-tanaka.md b/docs/agent-profiles/pred-sym-prof-yuki-tanaka.md index 10ad103ed..bb0dccce7 100644 --- a/docs/agent-profiles/pred-sym-prof-yuki-tanaka.md +++ b/docs/agent-profiles/pred-sym-prof-yuki-tanaka.md @@ -6,7 +6,7 @@ pred-sym (symbolic expression CLI) ## Use Case Three combined scenarios: 1. **Complexity comparison** — Compare algorithm complexity expressions to determine asymptotic equivalence (e.g., O(n^2 + n) == O(n^2), O(n log n) != O(n^2)). -2. **Reduction overhead audit** — Parse and simplify overhead expressions from reduction rules to verify they match expected growth (e.g., '3*num_vertices + num_edges^2'). +2. **Reduction size-contract audit** — Parse and simplify each rule's exact or upper-bound size relation, and verify it against constructed examples. 3. **Teaching complexity notation** — Use pred-sym as a learning/demonstration tool to explore how expressions simplify, evaluate at concrete sizes, and compare growth rates. ## Expected Outcome diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 88cd07556..ee531965e 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -8,7 +8,8 @@ target: e.target, source-name: graph-data.nodes.at(e.source).name, target-name: graph-data.nodes.at(e.target).name, - overhead: e.overhead, + parameters: e.parameters, + parameter-contract-error: e.parameter_contract_error, )) #let _edges-by-source-name = { @@ -65,7 +66,7 @@ #show: thmrules.with(qed-symbol: $square$) // === Example JSON helpers === -// Load canonical example database directly from the checked-in fixture file. +// Load the generated canonical example database. #let example-db = json("data/examples.json") // Pre-index rules by (source, target) and models by name so lookups are O(bucket) @@ -139,6 +140,21 @@ } } +#let cli-config(solution) = "'" + json.encode(solution) + "'" +#let bool-bit(value) = if value { 1 } else { 0 } +#let display-value(value) = { + if value == none { + "none" + } else if type(value) == bool { + str(bool-bit(value)) + } else if type(value) == array { + "[" + value.map(display-value).join(", ") + "]" + } else { + str(value) + } +} +#let fmt-values(values) = values.map(display-value).join(", ") + #let graph-instance(instance) = { if "graph" in instance { instance @@ -169,6 +185,7 @@ "CyclicOrdering": [Cyclic Ordering], "AcyclicPartition": [Acyclic Partition], "MaximumIndependentSet": [Maximum Independent Set], + "DecisionMaximumIndependentSet": [Decision Maximum Independent Set], "MaximumLeafSpanningTree": [Maximum Leaf Spanning Tree], "MinimumVertexCover": [Minimum Vertex Cover], "MaxCut": [Max-Cut], @@ -262,7 +279,6 @@ "ClosestSubstring": [Closest Substring], "ExactCoverBy3Sets": [Exact Cover by 3-Sets], "ThreeDimensionalMatching": [Three-Dimensional Matching], - "ThreeMatroidIntersection": [Three-Matroid Intersection], "SubsetProduct": [Subset Product], "SubsetSum": [Subset Sum], "CosineProductIntegration": [Cosine Product Integration], @@ -496,12 +512,6 @@ ] } -// Format target problem spec for pred reduce --to (handles empty variant dicts) -#let target-spec(data) = { - if data.target.variant.len() == 0 { data.target.problem } - else { data.target.problem + "/" + data.target.variant.values().join("/") } -} - // Format a canonical example's problem spec for pred create --example #let problem-spec(data) = { if data.variant.len() == 0 { data.problem } @@ -551,17 +561,21 @@ parts.push(node.variant.graph) } if "weight" in node.variant { - if node.variant.weight == "i32" { parts.push("weighted") } + if node.variant.weight == "i64" { parts.push("weighted") } else if node.variant.weight == "f64" { parts.push("real-weighted") } } if "k" in node.variant { parts.push[$k$-ary] } if parts.len() > 0 { [#base (#parts.join(", "))] } else { base } } -// Format overhead fields as inline text -#let format-overhead(overhead) = { - let parts = overhead.map(o => raw(o.field + " = " + o.formula)) - [_Overhead:_ #parts.join(", ").] +// Format explicitly classified parameters as inline text. +#let format-parameter-contract(fields) = { + let parts = fields.map(o => { + if o.contract == "exact" { raw(o.field + " = " + o.formula) } + else if o.contract == "bound-only" { raw(o.field + " <= " + o.formula) } + else { raw(o.field + " unavailable: " + o.reason) } + }) + [_Parameter contract:_ #parts.join(", ").] } // Unified function for reduction rules: theorem + proof + optional example @@ -582,7 +596,7 @@ else { display-name.at(target) } let src-lbl = label("def:" + source) let tgt-lbl = label("def:" + target) - let overhead = if edge != none and edge.overhead.len() > 0 { edge.overhead } else { none } + let parameters = if edge != none and edge.parameters.len() > 0 { edge.parameters } else { none } let thm-lbl = label("thm:" + source + "-to-" + target) covered-rules.update(old => old + ((source, target),)) @@ -590,7 +604,7 @@ #v(1em) #theorem[ *(*#context { if query(src-lbl).len() > 0 { link(src-lbl)[#src-disp] } else [#src-disp] }* #arrow *#context { if query(tgt-lbl).len() > 0 { link(tgt-lbl)[#tgt-disp] } else [#tgt-disp] }*)* #theorem-body - #if overhead != none { linebreak(); format-overhead(overhead) } + #if parameters != none { linebreak(); format-parameter-contract(parameters) } ] #thm-lbl] proof[#proof-body] @@ -767,7 +781,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ne = graph-num-edges(x.instance) // Pick optimal config = {v1, v3, v5, v9} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let S = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let alpha = metric-value(sol.metric) [ #problem-def("MaximumIndependentSet")[ @@ -780,7 +794,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MIS -o mis.json", "pred solve mis.json", - "pred evaluate mis.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mis.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -801,7 +815,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let weights = x.instance.weights let k = x.instance.bound_k let sol = (config: x.optimal_config, metric: x.optimal_value) - let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let S = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let wS = metric-value(sol.metric) [ #problem-def("MaximumCoKPlex")[ @@ -814,7 +828,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o co-k-plex.json", "pred solve co-k-plex.json", - "pred evaluate co-k-plex.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate co-k-plex.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -885,7 +899,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MaximumCommonEdgeSubgraph -o mces.json", "pred solve mces.json --solver brute-force", - "pred evaluate mces.json --config " + f.map(str).join(","), + "pred evaluate mces.json --config " + cli-config(f), ) #figure({ @@ -988,7 +1002,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MaximumContactMapOverlap -o cmo.json", "pred solve cmo.json --solver brute-force", - "pred evaluate cmo.json --config " + config.map(str).join(","), + "pred evaluate cmo.json --config " + cli-config(config), ) #figure({ @@ -1059,7 +1073,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edge-weights = x.instance.edge_weights let k = x.instance.k let sol = (config: x.optimal_config, metric: x.optimal_value) - let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let S = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let wS = metric-value(sol.metric) let edge-strs = edges.zip(edge-weights).map(((e, w)) => [$w_(#e.at(0)#e.at(1)) = #w$]).join(", ") [ @@ -1075,7 +1089,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o k-clique.json", "pred solve k-clique.json", - "pred evaluate k-clique.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate k-clique.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -1100,9 +1114,9 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let nv = 4 let edges = ((0, 1), (0, 2), (1, 2), (2, 3)) let ne = edges.len() - let config = (0, 0, 0, 1) - let deleted = edges.zip(config).filter(((e, b)) => b == 1).map(((e, _)) => e) - let surviving = edges.zip(config).filter(((e, b)) => b == 0).map(((e, _)) => e) + let config = (false, false, false, true) + let deleted = edges.zip(config).filter(((e, deleted)) => deleted).map(((e, _)) => e) + let surviving = edges.zip(config).filter(((e, deleted)) => not deleted).map(((e, _)) => e) let cluster = (0, 1, 2) let opt-val = deleted.len() [ @@ -1118,7 +1132,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example HighlyConnectedDeletion -o hcd.json", "pred solve hcd.json", - "pred evaluate hcd.json --config " + config.map(str).join(","), + "pred evaluate hcd.json --config " + cli-config(config), ) #figure({ @@ -1154,7 +1168,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example EulerianPath -o eulerian.json", "pred solve eulerian.json", - "pred evaluate eulerian.json --config " + pi.map(str).join(","), + "pred evaluate eulerian.json --config " + cli-config(pi), ) #figure( @@ -1231,10 +1245,11 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let beta = x.instance.beta let omega = x.instance.omega let config = x.optimal_config - // Configuration layout: first nv bits are vertex selectors x_v, next ne bits are y_e. - let selected-verts = range(nv).filter(v => config.at(v) == 1) - let omitted-verts = range(nv).filter(v => config.at(v) == 0) - let selected-edge-indices = range(ne).filter(i => config.at(nv + i) == 1) + let vertex-config = config.at(0) + let edge-config = config.at(1) + let selected-verts = range(nv).filter(v => vertex-config.at(v)) + let omitted-verts = range(nv).filter(v => not vertex-config.at(v)) + let selected-edge-indices = range(ne).filter(i => edge-config.at(i)) let selected-edges = selected-edge-indices.map(i => edges.at(i)) let omitted-prize-sum = omitted-verts.map(v => vertex-prizes.at(v)).fold(0, (a, b) => a + b) let edge-cost-sum = selected-edge-indices.map(i => edge-costs.at(i)).fold(0, (a, b) => a + b) @@ -1253,7 +1268,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PrizeCollectingSteinerForest -o pcsf.json", "pred solve pcsf.json --solver brute-force", - "pred evaluate pcsf.json --config " + config.map(str).join(","), + "pred evaluate pcsf.json --config " + cli-config(config), ) #figure({ @@ -1295,9 +1310,9 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edges = x.instance.graph.edges // Pick optimal config = {v0, v3, v4} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - let cover = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let cover = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let wS = metric-value(sol.metric) - let complement = sol.config.enumerate().filter(((i, v)) => v == 0).map(((i, _)) => i) + let complement = sol.config.enumerate().filter(((i, v)) => not v).map(((i, _)) => i) let alpha = complement.len() [ #problem-def("MinimumVertexCover")[ @@ -1313,7 +1328,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MVC -o mvc.json", "pred solve mvc.json", - "pred evaluate mvc.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mvc.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -1333,7 +1348,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ne = graph-num-edges(x.instance) let k = x.instance.bound let sol = x.optimal_config - let cover = sol.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let cover = sol.enumerate().filter(((i, v)) => v).map(((i, _)) => i) [ #problem-def("DecisionMinimumVertexCover")[ Given an undirected graph $G = (V, E)$ with vertex weights $w: V -> RR_(gt.eq 0)$ and an integer bound $k$, determine whether there exists a vertex cover $S subset.eq V$ with $sum_(v in S) w(v) <= k$ such that every edge has at least one endpoint in $S$. @@ -1345,7 +1360,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example DecisionMinimumVertexCover -o vc.json", "pred solve vc.json", - "pred evaluate vc.json --config " + sol.map(str).join(","), + "pred evaluate vc.json --config " + cli-config(sol), ) #figure({ @@ -1360,14 +1375,14 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } #{ - let x = load-model-example("MaxCut", variant: (graph: "SimpleGraph", weight: "i32")) + let x = load-model-example("MaxCut", variant: (graph: "SimpleGraph", weight: "i64")) let nv = graph-num-vertices(x.instance) let ne = graph-num-edges(x.instance) let edges = x.instance.graph.edges // Pick optimal config = S={v0, v3} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - let side-s = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let side-sbar = sol.config.enumerate().filter(((i, v)) => v == 0).map(((i, _)) => i) + let side-s = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) + let side-sbar = sol.config.enumerate().filter(((i, v)) => not v).map(((i, _)) => i) let cut-val = metric-value(sol.metric) let cut-edges = edges.filter(e => side-s.contains(e.at(0)) != side-s.contains(e.at(1))) let uncut-edges = edges.filter(e => side-s.contains(e.at(0)) == side-s.contains(e.at(1))) @@ -1382,7 +1397,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MaxCut -o maxcut.json", "pred solve maxcut.json", - "pred evaluate maxcut.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate maxcut.json --config " + cli-config(x.optimal_config), ) #figure(canvas(length: 1cm, { @@ -1417,8 +1432,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let nv = graph-num-vertices(x.instance) let ne = graph-num-edges(x.instance) let edges = x.instance.graph.edges - let side-a = x.optimal_config.enumerate().filter(((i, v)) => v == 0).map(((i, _)) => i) - let side-b = x.optimal_config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let side-a = x.optimal_config.enumerate().filter(((i, v)) => not v).map(((i, _)) => i) + let side-b = x.optimal_config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let cut-edges = edges.filter(e => x.optimal_config.at(e.at(0)) != x.optimal_config.at(e.at(1))) let cut-val = metric-value(x.optimal_value) [ @@ -1434,7 +1449,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example GraphPartitioning -o gp.json", "pred solve gp.json", - "pred evaluate gp.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate gp.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -1450,7 +1465,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] } #{ - let x = load-model-example("MinimumCutIntoBoundedSets", variant: (graph: "SimpleGraph", weight: "i32")) + let x = load-model-example("MinimumCutIntoBoundedSets", variant: (graph: "SimpleGraph", weight: "i64")) let nv = graph-num-vertices(x.instance) let edges = x.instance.graph.edges let ew = x.instance.edge_weights @@ -1458,8 +1473,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let snk = x.instance.sink let B = x.instance.size_bound let config = x.optimal_config - let V1 = range(nv).filter(i => config.at(i) == 0) - let V2 = range(nv).filter(i => config.at(i) == 1) + let V1 = range(nv).filter(i => not config.at(i)) + let V2 = range(nv).filter(i => config.at(i)) let cut-idx = edges.enumerate().filter(((i, e)) => config.at(e.at(0)) != config.at(e.at(1))).map(((i, _)) => i) let cut-weight = metric-value(x.optimal_value) [ @@ -1476,7 +1491,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumCutIntoBoundedSets -o mcibs.json", "pred solve mcibs.json", - "pred evaluate mcibs.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mcibs.json --config " + cli-config(x.optimal_config), ) // Layout matches the checked-in canonical instance (8 vertices, 12 edges); see @@ -1513,7 +1528,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| content((mx, my), text(6.5pt, fill: rgb("#333333"))[#str(int(ew.at(i)))]) } for (k, pos) in vpos.enumerate() { - let in-v1 = config.at(k) == 0 + let in-v1 = not config.at(k) g-node(pos, name: "v" + str(k), fill: if in-v1 { graph-colors.at(0) } else { graph-colors.at(1) }, label: text(fill: white)[$v_#k$]) @@ -1525,13 +1540,13 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] } #{ - let x = load-model-example("BiconnectivityAugmentation", variant: (graph: "SimpleGraph", weight: "i32")) + let x = load-model-example("BiconnectivityAugmentation", variant: (graph: "SimpleGraph", weight: "i64")) let nv = x.instance.graph.num_vertices let path-edges-j = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) let candidates = x.instance.potential_weights.map(c => (u: c.at(0), v: c.at(1), w: c.at(2))) let budget = x.instance.budget let config = x.optimal_config - let sel-idx = range(candidates.len()).filter(i => config.at(i) == 1) + let sel-idx = range(candidates.len()).filter(i => config.at(i)) let sel-weight = sel-idx.map(i => candidates.at(i).w).sum(default: 0) [ #problem-def("BiconnectivityAugmentation")[ @@ -1544,7 +1559,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example BiconnectivityAugmentation -o biaug.json", "pred solve biaug.json", - "pred evaluate biaug.json --config " + config.map(str).join(","), + "pred evaluate biaug.json --config " + cli-config(config), ) #figure( @@ -1604,7 +1619,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example HC -o hc.json", "pred solve hc.json", - "pred evaluate hc.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -1646,7 +1661,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ne = edges.len() let edge-lengths = x.instance.edge_lengths let config = x.optimal_config - let selected = range(ne).filter(i => config.at(i) == 1) + let selected = range(ne).filter(i => config.at(i)) let total-length = selected.map(i => edge-lengths.at(i)).sum() let cycle-order = (0, 1, 4, 5, 2, 3) [ @@ -1662,7 +1677,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o longest-circuit.json", "pred solve longest-circuit.json", - "pred evaluate longest-circuit.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate longest-circuit.json --config " + cli-config(x.optimal_config), ) #figure( @@ -1679,7 +1694,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| }) for (ei, (u, v)) in edges.enumerate() { - let is-selected = config.at(ei) == 1 + let is-selected = config.at(ei) let col = if is-selected { colors.selected } else { colors.unused } let thickness = if is-selected { 1.3pt } else { 0.5pt } let dash = if is-selected { "solid" } else { "dashed" } @@ -1720,7 +1735,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #{ - let x = load-model-example("BoundedComponentSpanningForest", variant: (graph: "SimpleGraph", weight: "i32")) + let x = load-model-example("BoundedComponentSpanningForest", variant: (graph: "SimpleGraph", weight: "i64")) let nv = x.instance.graph.num_vertices let edges-j = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) let weights = x.instance.weights @@ -1736,12 +1751,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Bounded Component Spanning Forest appears as ND10 in Garey and Johnson @garey1979. It asks for a decomposition into a bounded number of connected pieces, each with bounded total weight, so it naturally captures contiguous districting and redistricting-style constraints where each district must remain connected while respecting a population cap. A direct exhaustive search over component labels gives an $O^*(K^n)$ baseline, but subset-DP techniques via inclusion-exclusion improve the exact running time to $O^*(3^n)$ @bjorklund2009. - *Example.* Consider the graph on $n = #nv$ vertices ${v_0, dots, v_#(nv - 1)}$ with $|E| = #{edges-j.len()}$ edges, vertex weights $(#weights.map(str).join(", "))$, component limit $K = #K$, and bound $B = #B$. The partition #range(num-parts).map(p => $V_#(p + 1) = {#parts.at(p).map(i => $v_#i$).join(", ")}$).join(", ") is feasible: each set induces a connected subgraph, and the component weights #range(num-parts).map(p => $#{parts.at(p).map(i => str(weights.at(i))).join(" + ")} = #part-weights.at(p)$).join(", ") all respect $B = #B$. Therefore this instance is a YES instance. + *Example.* Consider the graph on $n = #nv$ vertices ${v_0, dots, v_#(nv - 1)}$ with $|E| = #{edges-j.len()}$ edges, vertex weights $(#fmt-values(weights))$, component limit $K = #K$, and bound $B = #B$. The partition #range(num-parts).map(p => $V_#(p + 1) = {#parts.at(p).map(i => $v_#i$).join(", ")}$).join(", ") is feasible: each set induces a connected subgraph, and the component weights #range(num-parts).map(p => $#{parts.at(p).map(i => str(weights.at(i))).join(" + ")} = #part-weights.at(p)$).join(", ") all respect $B = #B$. Therefore this instance is a YES instance. #pred-commands( "pred create --example BoundedComponentSpanningForest -o bcsf.json", "pred solve bcsf.json", - "pred evaluate bcsf.json --config " + partition.map(str).join(","), + "pred evaluate bcsf.json --config " + cli-config(partition), ) #figure( @@ -1772,7 +1787,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| content(lpos, text(7pt)[$w = #(weights.at(k))$]) } }), - caption: [Bounded Component Spanning Forest on #nv vertices with $K = #K$ and $B = #B$. The partition #range(num-parts).map(p => $V_#(p+1) = {#parts.at(p).map(i => $v_#i$).join(", ")}$).join(", ") (weights #part-weights.map(str).join(", ")) is feasible. Bold colored edges are intra-component; gray edges cross components.], + caption: [Bounded Component Spanning Forest on #nv vertices with $K = #K$ and $B = #B$. The partition #range(num-parts).map(p => $V_#(p+1) = {#parts.at(p).map(i => $v_#i$).join(", ")}$).join(", ") (weights #fmt-values(part-weights)) is feasible. Bold colored edges are intra-component; gray edges cross components.], ) ] ] @@ -1790,14 +1805,14 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #problem-def("LengthBoundedDisjointPaths")[ Given an undirected graph $G = (V, E)$, distinct terminals $s, t in V$, and a positive integer $K$, maximize the number of pairwise internally vertex-disjoint paths from $s$ to $t$, each using at most $K$ edges. ][ - Length-Bounded Disjoint Paths is the bounded-routing version of the classical disjoint-path problem, with applications in network routing and VLSI where multiple connections must fit simultaneously under quality-of-service limits. Garey & Johnson list it as ND41 and summarize the sharp threshold proved by Itai, Perl, and Shiloach: the problem is NP-complete for every fixed $K >= 5$, polynomial-time solvable for $K <= 4$, and becomes polynomial again when the length bound is removed entirely @garey1979. The implementation here uses $M dot |V|$ binary variables where $M = min(deg(s), deg(t))$ is an upper bound on the number of vertex-disjoint $s$-$t$ paths, so brute-force search over configurations runs in $O^*(2^(M dot |V|))$. + Length-Bounded Disjoint Paths is the bounded-routing version of the classical disjoint-path problem, with applications in network routing and VLSI where multiple connections must fit simultaneously under quality-of-service limits. Garey & Johnson list it as ND41 and summarize the sharp threshold proved by Itai, Perl, and Shiloach: the problem is NP-complete for every fixed $K >= 5$, polynomial-time solvable for $K <= 4$, and becomes polynomial again when the length bound is removed entirely @garey1979. The implementation here uses $M dot |E|$ binary variables where $M = min(deg(s), deg(t))$ is an upper bound on the number of vertex-disjoint $s$-$t$ paths, so brute-force search over configurations runs in $O^*(2^(M dot |E|))$. Each row selects the edges of one path in the input graph's edge order; an all-zero row is unused. *Example.* Consider the graph $G$ with $n = #nv$ vertices, $|E| = #ne$ edges, terminals $s = v_#s$, $t = v_#t$, and $K = #K$. Here $M = #M$ path slots are available. The three paths $P_1 = v_0 arrow v_1 arrow v_4$, $P_2 = v_0 arrow v_2 arrow v_4$, and $P_3 = v_0 arrow v_3 arrow v_4$ are each of length 2 (at most $K = 3$), and their internal vertex sets ${v_1}$, ${v_2}$, and ${v_3}$ are pairwise disjoint. The optimal value is therefore $3$. #pred-commands( "pred create --example LengthBoundedDisjointPaths -o length-bounded-disjoint-paths.json", "pred solve length-bounded-disjoint-paths.json", - "pred evaluate length-bounded-disjoint-paths.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate length-bounded-disjoint-paths.json --config " + cli-config(x.optimal_config), ) #figure( @@ -1874,7 +1889,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example DisjointConnectingPaths -o disjoint-connecting-paths.json", "pred solve disjoint-connecting-paths.json", - "pred evaluate disjoint-connecting-paths.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate disjoint-connecting-paths.json --config " + cli-config(x.optimal_config), ) #figure( @@ -1944,7 +1959,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example GeneralizedHex -o generalized-hex.json", "pred solve generalized-hex.json", - "pred evaluate generalized-hex.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate generalized-hex.json --config " + cli-config(x.optimal_config), ) #figure( @@ -2014,7 +2029,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example HamiltonianPath -o hamiltonian-path.json", "pred solve hamiltonian-path.json", - "pred evaluate hamiltonian-path.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate hamiltonian-path.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2063,7 +2078,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example HamiltonianPathBetweenTwoVertices -o hpbtv.json", "pred solve hpbtv.json", - "pred evaluate hpbtv.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate hpbtv.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2118,7 +2133,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example DirectedHamiltonianPath -o dhp.json", "pred solve dhp.json", - "pred evaluate dhp.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate dhp.json --config " + cli-config(x.optimal_config), ) #figure( @@ -2166,7 +2181,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let x = load-model-example("Kernel") let nv = x.instance.graph.num_vertices let arcs = x.instance.graph.arcs - let in-kernel(k) = x.optimal_config.at(k) == 1 + let in-kernel(k) = x.optimal_config.at(k) [ #problem-def("Kernel")[ Given a directed graph $G = (V, A)$, find a _kernel_ $V' subset.eq V$ such that (1) $V'$ is _independent_ — no arc joins any two vertices in $V'$ — and (2) $V'$ is _absorbing_ — every vertex $u in.not V'$ has an arc $(u, v) in A$ for some $v in V'$. @@ -2180,7 +2195,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example Kernel -o kernel.json", "pred solve kernel.json", - "pred evaluate kernel.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate kernel.json --config " + cli-config(x.optimal_config), ) #figure( @@ -2257,7 +2272,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let t = x.instance.target_vertex let path-config = x.optimal_config let path-order = (0, 1, 3, 2, 4, 5, 6) - let path-edges = edges.enumerate().filter(((idx, _)) => path-config.at(idx) == 1).map(((idx, e)) => e) + let path-edges = edges.enumerate().filter(((idx, _)) => path-config.at(idx)).map(((idx, e)) => e) [ #problem-def("LongestPath")[ Given an undirected graph $G = (V, E)$ with positive edge lengths $l: E -> ZZ^+$ and designated vertices $s, t in V$, find a simple path $P$ from $s$ to $t$ maximizing $sum_(e in P) l(e)$. @@ -2269,7 +2284,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example LongestPath -o longest-path.json", "pred solve longest-path.json", - "pred evaluate longest-path.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate longest-path.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2282,7 +2297,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // so route them as quadratic Bezier arcs (below / above the main layout). let arc-ctrl = ("7": (3.75, -0.35), "9": (3.1, 2.85)) for (idx, (u, v)) in edges.enumerate() { - let on-path = path-config.at(idx) == 1 + let on-path = path-config.at(idx) let st = if on-path { 2pt + blue } else { 1pt + gray } let key = str(idx) if key in arc-ctrl { @@ -2339,12 +2354,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Undirected Flow with Lower Bounds appears as ND37 in Garey and Johnson's catalog @garey1979. Itai proved that even this single-commodity undirected feasibility problem is NP-complete, contrasting sharply with the directed lower-bounded case, which reduces to ordinary max-flow machinery @itai1978. - *Example.* The canonical fixture uses source $s = v_#s$, sink $t = v_#t$, requirement $R = #R$, edges ${#edges.map(((u, v)) => $(v_#u, v_#v)$).join(", ")}$, and lower/upper pairs ${#range(edges.len()).map(i => $(#lower.at(i), #caps.at(i))$).join(", ")}$ in that order. Under the all-zero orientation config, a feasible witness sends flows $(#witness.map(str).join(", "))$ along those edges respectively: $2$ on $(v_0, v_1)$, $1$ on $(v_0, v_2)$, $1$ on $(v_1, v_3)$, $1$ on $(v_2, v_3)$, $1$ on $(v_1, v_4)$, $2$ on $(v_3, v_5)$, and $1$ on $(v_4, v_5)$. Every lower bound is satisfied, each nonterminal vertex has equal inflow and outflow, and the sink receives $2 + 1 = 3 >= R$, so the instance evaluates to true. + *Example.* The canonical fixture uses source $s = v_#s$, sink $t = v_#t$, requirement $R = #R$, edges ${#edges.map(((u, v)) => $(v_#u, v_#v)$).join(", ")}$, and lower/upper pairs ${#range(edges.len()).map(i => $(#lower.at(i), #caps.at(i))$).join(", ")}$ in that order. Under the all-zero orientation config, a feasible witness sends flows $(#fmt-values(witness))$ along those edges respectively: $2$ on $(v_0, v_1)$, $1$ on $(v_0, v_2)$, $1$ on $(v_1, v_3)$, $1$ on $(v_2, v_3)$, $1$ on $(v_1, v_4)$, $2$ on $(v_3, v_5)$, and $1$ on $(v_4, v_5)$. Every lower bound is satisfied, each nonterminal vertex has equal inflow and outflow, and the sink receives $2 + 1 = 3 >= R$, so the instance evaluates to true. #pred-commands( "pred create --example UndirectedFlowLowerBounds -o undirected-flow-lower-bounds.json", "pred solve undirected-flow-lower-bounds.json", - "pred evaluate undirected-flow-lower-bounds.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate undirected-flow-lower-bounds.json --config " + cli-config(x.optimal_config), ) #figure( @@ -2419,7 +2434,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example UndirectedTwoCommodityIntegralFlow -o undirected-two-commodity-integral-flow.json", "pred solve undirected-two-commodity-integral-flow.json", - "pred evaluate undirected-two-commodity-integral-flow.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate undirected-two-commodity-integral-flow.json --config " + cli-config(x.optimal_config), ) #figure( @@ -2483,7 +2498,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o path-constrained-network-flow.json", "pred solve path-constrained-network-flow.json --solver brute-force", - "pred evaluate path-constrained-network-flow.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate path-constrained-network-flow.json --config " + cli-config(x.optimal_config), ) #figure( @@ -2601,7 +2616,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example IsomorphicSpanningTree -o isomorphic-spanning-tree.json", "pred solve isomorphic-spanning-tree.json", - "pred evaluate isomorphic-spanning-tree.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate isomorphic-spanning-tree.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2649,7 +2664,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let t = x.instance.target_vertex let W = x.instance.weight_bound let path-config = x.optimal_config - let path-edges = edges.enumerate().filter(((idx, _)) => path-config.at(idx) == 1).map(((idx, e)) => e) + let path-edges = edges.enumerate().filter(((idx, _)) => path-config.at(idx)).map(((idx, e)) => e) let path-order = (0, 2, 3, 5) [ #problem-def("ShortestWeightConstrainedPath")[ @@ -2664,7 +2679,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ShortestWeightConstrainedPath -o shortest-weight-constrained-path.json", "pred solve shortest-weight-constrained-path.json", - "pred evaluate shortest-weight-constrained-path.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate shortest-weight-constrained-path.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2677,7 +2692,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // labels toward opposite ends of each edge so they don't collide. let label-t = ("3": 0.3, "7": 0.7) for (idx, (u, v)) in edges.enumerate() { - let on-path = path-config.at(idx) == 1 + let on-path = path-config.at(idx) g-edge(verts.at(u), verts.at(v), stroke: if on-path { 2pt + blue } else { 1pt + gray }) let pu = verts.at(u) let pv = verts.at(v) @@ -2721,6 +2736,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #problem-def("KColoring")[ Given $G = (V, E)$ and a positive integer $k$, determine whether there exists a proper $k$-coloring $c: V -> {1, ..., k}$ such that $c(u) eq.not c(v)$ for every $(u, v) in E$. ][ + Fixed variants derive their color count from the type parameter; a serialized `num_colors` that disagrees with that fixed count is rejected. The `KN` variant stores its color count at runtime. + Graph coloring arises in register allocation, frequency assignment, and scheduling @garey1979. Deciding $k$-colorability is NP-complete for $k >= 3$ but solvable in $O(n+m)$ for $k=2$ via bipartiteness testing. For $k = 3$, the best known algorithm runs in $O^*(1.3289^n)$ @beigel2005; for $k = 4$ in $O^*(1.7159^n)$ @wu2024; for $k = 5$ in $O^*((2-epsilon)^n)$ @zamir2021. In general, inclusion-exclusion achieves $O^*(2^n)$ @bjorklund2009. *Example.* Consider the house graph $G$ with $k = #k$ colors. The coloring #range(nv).map(i => $c(v_#i) = #(coloring.at(i) + 1)$).join(", ") is proper: no adjacent pair shares a color, so the number of conflicts is 0. The house graph has chromatic number $chi(G) = #k$ because the triangle $(v_2, v_3, v_4)$ requires #k colors. @@ -2728,7 +2745,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o kcoloring.json", "pred solve kcoloring.json", - "pred evaluate kcoloring.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate kcoloring.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2760,7 +2777,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MaximumAchromaticNumber -o achromatic.json", "pred solve achromatic.json", - "pred evaluate achromatic.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate achromatic.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2782,7 +2799,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edges = x.instance.graph.edges // Pick optimal config = {v2, v3} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let S = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let wS = metric-value(sol.metric) // Compute neighbors dominated by each vertex in S let dominated = S.map(s => { @@ -2804,7 +2821,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumDominatingSet -o minimum-dominating-set.json", "pred solve minimum-dominating-set.json", - "pred evaluate minimum-dominating-set.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-dominating-set.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2821,7 +2838,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let n = x.instance.points.len() let B = x.instance.radius let sol = (config: x.optimal_config, metric: x.optimal_value) - let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let S = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let wS = metric-value(sol.metric) [ #problem-def("MinimumGeometricConnectedDominatingSet")[ @@ -2834,7 +2851,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumGeometricConnectedDominatingSet -o mgcds.json", "pred solve mgcds.json", - "pred evaluate mgcds.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mgcds.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2905,7 +2922,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumCoveringByCliques -o covering-by-cliques.json", "pred solve covering-by-cliques.json", - "pred evaluate covering-by-cliques.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate covering-by-cliques.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2949,7 +2966,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumIntersectionGraphBasis -o intersection-basis.json", "pred solve intersection-basis.json", - "pred evaluate intersection-basis.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate intersection-basis.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -2986,7 +3003,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edges = x.instance.graph.edges // Pick optimal config [1,0,0,0,1,0] = edges {(0,1),(2,4)} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - let matched-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) + let matched-edges = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => edges.at(i)) let wM = metric-value(sol.metric) // Collect matched vertices let matched-verts = () @@ -3006,7 +3023,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MaximumMatching -o maximum-matching.json", "pred solve maximum-matching.json", - "pred evaluate maximum-matching.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate maximum-matching.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3025,7 +3042,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edges = x.instance.graph.edges let ew = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) - let tour-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) + let tour-edges = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => edges.at(i)) let bottleneck = metric-value(sol.metric) let tour-weights = tour-edges.map(((u, v)) => { let idx = edges.position(e => e == (u, v) or e == (v, u)) @@ -3059,7 +3076,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example BottleneckTravelingSalesman -o btsp.json", "pred solve btsp.json", - "pred evaluate btsp.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate btsp.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3121,7 +3138,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edges = x.instance.graph.edges let ew = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) - let tour-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) + let tour-edges = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => edges.at(i)) let tour-cost = metric-value(sol.metric) // Build ordered tour from tour-edges starting at vertex 0 let tour-order = (0,) @@ -3152,7 +3169,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example TSP -o tsp.json", "pred solve tsp.json", - "pred evaluate tsp.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate tsp.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3190,7 +3207,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let weights = x.instance.edge_weights let terminals = x.instance.terminals let sol = (config: x.optimal_config, metric: x.optimal_value) - let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let tree-edges = tree-edge-indices.map(i => edges.at(i)) let cost = metric-value(sol.metric) // Steiner vertices: in tree but not terminals @@ -3223,7 +3240,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SteinerTree -o steiner-tree.json", "pred solve steiner-tree.json", - "pred evaluate steiner-tree.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate steiner-tree.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3262,7 +3279,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let candidates = x.instance.candidate_arcs let bound = x.instance.bound let sol = (config: x.optimal_config, metric: x.optimal_value) - let chosen = candidates.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc) + let chosen = candidates.enumerate().filter(((i, _)) => sol.config.at(i)).map(((i, arc)) => arc) let total-weight = chosen.map(a => a.at(2)).sum() let blue = graph-colors.at(0) [ @@ -3276,7 +3293,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example StrongConnectivityAugmentation -o strong-connectivity-augmentation.json", "pred solve strong-connectivity-augmentation.json", - "pred evaluate strong-connectivity-augmentation.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate strong-connectivity-augmentation.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3338,7 +3355,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let weights = x.instance.edge_weights let terminals = x.instance.terminals let sol = (config: x.optimal_config, metric: x.optimal_value) - let cut-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let cut-edge-indices = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let cut-edges = cut-edge-indices.map(i => edges.at(i)) let cost = metric-value(sol.metric) [ @@ -3352,7 +3369,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumMultiwayCut -o minimum-multiway-cut.json", "pred solve minimum-multiway-cut.json", - "pred evaluate minimum-multiway-cut.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-multiway-cut.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3401,7 +3418,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example OptimalLinearArrangement -o optimal-linear-arrangement.json", "pred solve optimal-linear-arrangement.json", - "pred evaluate optimal-linear-arrangement.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate optimal-linear-arrangement.json --config " + cli-config(x.optimal_config), ) // Build inverse mapping: pos[p] = vertex placed at position p @@ -3466,7 +3483,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example RootedTreeArrangement -o rooted-tree-arrangement.json", "pred solve rooted-tree-arrangement.json --solver brute-force", - "pred evaluate rooted-tree-arrangement.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate rooted-tree-arrangement.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3520,7 +3537,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| 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 K = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let clique-edges = edges.filter(e => K.contains(e.at(0)) and K.contains(e.at(1))) [ #problem-def("KClique")[ @@ -3533,7 +3550,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example KClique -o kclique.json", "pred solve kclique.json", - "pred evaluate kclique.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate kclique.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3552,7 +3569,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edges = x.instance.graph.edges // optimal config = {v2, v3, v4} let sol = (config: x.optimal_config, metric: x.optimal_value) - let K = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let K = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let omega = metric-value(sol.metric) // Edges within the clique let clique-edges = edges.filter(e => K.contains(e.at(0)) and K.contains(e.at(1))) @@ -3567,7 +3584,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MaximumClique -o maximum-clique.json", "pred solve maximum-clique.json", - "pred evaluate maximum-clique.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate maximum-clique.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3586,7 +3603,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edges = x.instance.graph.edges // optimal config = {v0,v2,v4} with w=3 (maximum-weight maximal IS) let opt = (config: x.optimal_config, metric: x.optimal_value) - let S-opt = opt.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let S-opt = opt.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let w-opt = metric-value(opt.metric) // Suboptimal maximal IS {v1,v3} with w=2 (hardcoded — no longer in fixture) let S-sub = (1, 3) @@ -3602,7 +3619,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MaximalIS -o maximal-is.json", "pred solve maximal-is.json", - "pred evaluate maximal-is.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate maximal-is.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3620,7 +3637,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ne = graph-num-edges(x.instance) let edges = x.instance.graph.edges let sol = (config: x.optimal_config, metric: x.optimal_value) - let matched-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) + let matched-edges = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => edges.at(i)) let sz = metric-value(sol.metric) [ #problem-def("MinimumMaximalMatching")[ @@ -3633,7 +3650,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumMaximalMatching -o mmm.json", "pred solve mmm.json", - "pred evaluate mmm.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mmm.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3658,8 +3675,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let arcs = x.instance.graph.arcs let ne = arcs.len() let sol = (config: x.optimal_config, metric: x.optimal_value) - let merged = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc) - let dummy = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 0).map(((i, arc)) => arc) + let merged = arcs.enumerate().filter(((i, _)) => sol.config.at(i)).map(((i, arc)) => arc) + let dummy = arcs.enumerate().filter(((i, _)) => not sol.config.at(i)).map(((i, arc)) => arc) let opt = metric-value(sol.metric) let blue = graph-colors.at(0) [ @@ -3673,7 +3690,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o minimum-dummy-activities-pert.json", "pred solve minimum-dummy-activities-pert.json --solver brute-force", - "pred evaluate minimum-dummy-activities-pert.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-dummy-activities-pert.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3705,13 +3722,13 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } #{ - let x = load-model-example("MinimumFeedbackVertexSet") + let x = load-model-example("MinimumFeedbackVertexSet", variant: (weight: "i64")) let nv = graph-num-vertices(x.instance) let ne = x.instance.graph.arcs.len() let arcs = x.instance.graph.arcs // Pick optimal config = {v0} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let S = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let wS = metric-value(sol.metric) [ #problem-def("MinimumFeedbackVertexSet")[ @@ -3724,7 +3741,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumFeedbackVertexSet -o minimum-feedback-vertex-set.json", "pred solve minimum-feedback-vertex-set.json", - "pred evaluate minimum-feedback-vertex-set.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-feedback-vertex-set.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3778,7 +3795,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PartitionIntoPathsOfLength2 -o partition-paths2.json", "pred solve partition-paths2.json", - "pred evaluate partition-paths2.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate partition-paths2.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3802,7 +3819,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let sol = (config: x.optimal_config, metric: x.optimal_value) let opt-weight = metric-value(sol.metric) // Derive tree edges from optimal config - let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let tree-edges = tree-edge-indices.map(i => edges.at(i)) // Steiner vertices: non-terminal vertices that appear in tree edges let steiner-verts = range(nv).filter(v => not terminals.contains(v) and tree-edges.any(e => e.at(0) == v or e.at(1) == v)) @@ -3817,7 +3834,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SteinerTreeInGraphs -o steiner-tree-in-graphs.json", "pred solve steiner-tree-in-graphs.json", - "pred evaluate steiner-tree-in-graphs.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate steiner-tree-in-graphs.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3869,7 +3886,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let opt-cost = metric-value(x.optimal_value) // Pick optimal config = {v2, v5} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - let centers = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let centers = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) [ #problem-def("MinimumSumMulticenter")[ Given a graph $G = (V, E)$ with vertex weights $w: V -> ZZ_(>= 0)$, edge lengths $l: E -> ZZ_(>= 0)$, and a positive integer $K <= |V|$, find a set $P subset.eq V$ of $K$ vertices (centers) that minimizes the total weighted distance $sum_(v in V) w(v) dot d(v, P)$, where $d(v, P) = min_(p in P) d(v, p)$ is the shortest-path distance from $v$ to the nearest center in $P$. @@ -3883,7 +3900,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumSumMulticenter -o minimum-sum-multicenter.json", "pred solve minimum-sum-multicenter.json", - "pred evaluate minimum-sum-multicenter.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-sum-multicenter.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3916,7 +3933,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let K = x.instance.k let opt = x.optimal_value let sol = (config: x.optimal_config, metric: x.optimal_value) - let centers = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let centers = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) [ #problem-def("MinMaxMulticenter")[ Given a graph $G = (V, E)$ with vertex weights $w: V -> ZZ_(>= 0)$, edge lengths $l: E -> ZZ_(>= 0)$, and a positive integer $K <= |V|$, find $S subset.eq V$ with $|S| = K$ that minimizes $max_(v in V) w(v) dot d(v, S)$, where $d(v, S) = min_(s in S) d(v, s)$ is the shortest weighted-path distance from $v$ to the nearest vertex in $S$. @@ -3930,7 +3947,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinMaxMulticenter -o min-max-multicenter.json", "pred solve min-max-multicenter.json", - "pred evaluate min-max-multicenter.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate min-max-multicenter.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -3963,7 +3980,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let usage = x.instance.usage let storage = x.instance.storage let sol = (config: x.optimal_config, metric: x.optimal_value) - let copies = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let copies = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let opt = metric-value(sol.metric) let s-cost = copies.map(i => storage.at(i)).sum() let a-cost = opt - s-cost @@ -3975,12 +3992,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Multiple Copy File Allocation appears in the storage-and-retrieval section of Garey and Johnson (SR6) @garey1979. The model combines two competing costs: each chosen copy vertex incurs a storage charge, while every vertex pays an access cost weighted by its demand and graph distance to the nearest copy. Applications include content distribution networks (placing cache servers), database replication across data centers, and distributed file systems. Garey and Johnson record the problem as NP-hard in the strong sense, even when usage and storage costs are uniform @garey1979. It generalizes Uncapacitated Facility Location when the network topology is arbitrary. - *Example.* Consider the path $P_#nv$ with usage $u = (#usage.map(str).join(", "))$ and storage costs $s = (#storage.map(str).join(", "))$. The endpoints $v_0, v_5$ have high demand ($u = 5$) but expensive storage ($s = 6$), while $v_1, v_4$ are cheap server locations ($s = 2$). Placing copies at $V' = {#copies.map(i => $v_#i$).join(", ")}$ gives storage cost $#copies.map(i => str(storage.at(i))).join(" + ") = #s-cost$ and access cost $5 dot 1 + 1 dot 0 + 1 dot 1 + 1 dot 1 + 1 dot 0 + 5 dot 1 = #a-cost$, for a total of $#opt$. Adding a copy at $v_0$ would save $5 dot 1 = 5$ in access but cost $6$ in storage — a net loss. This tradeoff between placement cost and proximity drives the problem's NP-hardness. + *Example.* Consider the path $P_#nv$ with usage $u = (#fmt-values(usage))$ and storage costs $s = (#fmt-values(storage))$. The endpoints $v_0, v_5$ have high demand ($u = 5$) but expensive storage ($s = 6$), while $v_1, v_4$ are cheap server locations ($s = 2$). Placing copies at $V' = {#copies.map(i => $v_#i$).join(", ")}$ gives storage cost $#copies.map(i => str(storage.at(i))).join(" + ") = #s-cost$ and access cost $5 dot 1 + 1 dot 0 + 1 dot 1 + 1 dot 1 + 1 dot 0 + 5 dot 1 = #a-cost$, for a total of $#opt$. Adding a copy at $v_0$ would save $5 dot 1 = 5$ in access but cost $6$ in storage — a net loss. This tradeoff between placement cost and proximity drives the problem's NP-hardness. #pred-commands( "pred create --example MultipleCopyFileAllocation -o multiple-copy-file-allocation.json", "pred solve multiple-copy-file-allocation.json", - "pred evaluate multiple-copy-file-allocation.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate multiple-copy-file-allocation.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -4022,7 +4039,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let U-size = all-elems.len() // Pick optimal config = {S1, S3} (0-indexed: sets 0, 2) to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let selected = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let wP = metric-value(sol.metric) // Format a set as {e1+1, e2+1, ...} (1-indexed) let fmt-set(s) = "{" + s.map(e => str(e + 1)).join(", ") + "}" @@ -4037,7 +4054,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o maximum-set-packing.json", "pred solve maximum-set-packing.json", - "pred evaluate maximum-set-packing.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate maximum-set-packing.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4065,7 +4082,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let m = sets.len() let U-size = x.instance.universe_size let sol = (config: x.optimal_config, metric: x.optimal_value) - let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let selected = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let wC = metric-value(sol.metric) let fmt-set(s) = "{" + s.map(e => str(e + 1)).join(", ") + "}" [ @@ -4079,7 +4096,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumSetCovering -o minimum-set-covering.json", "pred solve minimum-set-covering.json", - "pred evaluate minimum-set-covering.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-set-covering.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4110,7 +4127,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let m = sets.len() let U-size = x.instance.universe_size let sol = (config: x.optimal_config, metric: x.optimal_value) - let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let selected = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let hit-size = metric-value(sol.metric) let fmt-set(s) = if s.len() == 0 { $emptyset$ @@ -4130,7 +4147,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumHittingSet -o minimum-hitting-set.json", "pred solve minimum-hitting-set.json", - "pred evaluate minimum-hitting-set.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-hitting-set.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4173,8 +4190,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let m = subsets.len() let n = x.instance.universe_size let sol = (config: x.optimal_config, metric: x.optimal_value) - let part0 = sol.config.enumerate().filter(((i, v)) => v == 0).map(((i, _)) => i) - let part1 = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let part0 = sol.config.enumerate().filter(((i, v)) => not v).map(((i, _)) => i) + let part1 = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let fmt-set(s) = "{" + s.map(e => str(e + 1)).join(", ") + "}" [ #problem-def("SetSplitting")[ @@ -4187,7 +4204,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SetSplitting -o set-splitting.json", "pred solve set-splitting.json", - "pred evaluate set-splitting.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate set-splitting.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4243,7 +4260,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ConsecutiveSets -o consecutive-sets.json", "pred solve consecutive-sets.json", - "pred evaluate consecutive-sets.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate consecutive-sets.json --config " + cli-config(x.optimal_config), ) // Subset span data: (start_pos, end_pos) in the solution string @@ -4276,7 +4293,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| content((x1 + 0.45, y), text(7pt, fill: color, $Sigma_#(si + 1)$)) } }), - caption: [Consecutive Sets: the string $w = (#sol.map(str).join(", "))$ with each subset $Sigma_i$ occupying a contiguous block. Colored bars below indicate the span of each subset.] + caption: [Consecutive Sets: the string $w = (#fmt-values(sol))$ with each subset $Sigma_i$ occupying a contiguous block. Colored bars below indicate the span of each subset.] ) ] ] @@ -4292,7 +4309,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Format a 0-indexed triple as 1-indexed set notation: {a+1, b+1, c+1} let fmt-triple(t) = "{" + t.map(e => str(e + 1)).join(", ") + "}" // Collect indices of selected subsets (1-indexed) - let selected = sol.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let selected = sol.enumerate().filter(((i, v)) => v).map(((i, _)) => i) [ #problem-def("ExactCoverBy3Sets")[ @@ -4305,7 +4322,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ExactCoverBy3Sets -o exact-cover-by-3-sets.json", "pred solve exact-cover-by-3-sets.json", - "pred evaluate exact-cover-by-3-sets.json --config " + x3c.optimal_config.map(str).join(","), + "pred evaluate exact-cover-by-3-sets.json --config " + cli-config(x3c.optimal_config), ) #figure( @@ -4357,7 +4374,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Format a triple as (w+1, x+1, y+1) using 1-indexed notation let fmt-triple(t) = $(#(t.at(0) + 1), #(t.at(1) + 1), #(t.at(2) + 1))$ // Collect indices of selected triples (0-indexed) - let selected = sol.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let selected = sol.enumerate().filter(((i, v)) => v).map(((i, _)) => i) [ #problem-def("ThreeDimensionalMatching")[ @@ -4370,7 +4387,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ThreeDimensionalMatching -o three-dimensional-matching.json", "pred solve three-dimensional-matching.json", - "pred evaluate three-dimensional-matching.json --config " + tdm.optimal_config.map(str).join(","), + "pred evaluate three-dimensional-matching.json --config " + cli-config(tdm.optimal_config), ) // Tripartite layout: W (left), X (center), Y (right) with triples as hyperedges @@ -4413,71 +4430,6 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] } -#{ - let tmi = load-model-example("ThreeMatroidIntersection") - let n = tmi.instance.ground_set_size - let parts = tmi.instance.partitions - let K = tmi.instance.bound - let sol = tmi.optimal_config - let selected = sol.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let fmt-set(items) = if items.len() == 0 { - $emptyset$ - } else { - "{" + items.map(e => str(e)).join(", ") + "}" - } - let fmt-group(g) = $#("{" + g.map(e => str(e)).join(", ") + "}")$ - [ - #problem-def("ThreeMatroidIntersection")[ - Given three partition matroids $(E, cal(F)_1)$, $(E, cal(F)_2)$, $(E, cal(F)_3)$ on a common ground set $E$ with $|E| = n$, and a positive integer $K <= n$, does there exist a subset $E' subset.eq E$ with $|E'| = K$ that is independent in all three matroids? A partition matroid partitions $E$ into groups; a set $S$ is independent if $|S inter G| <= 1$ for every group $G$. - ][ - Three-Matroid Intersection is problem SP11 in Garey & Johnson @garey1979 (section A3). While 2-matroid intersection is solvable in polynomial time (Edmonds, 1970) @edmonds1970, the jump to three matroids captures NP-hardness. NP-completeness is established by transformation from Three-Dimensional Matching, where each dimension induces a partition matroid. The restriction to partition matroids suffices for NP-completeness. - - Doron-Arad, Kulik, and Shachnai (2024) @doron2024 showed that brute force essentially cannot be beaten: any algorithm requires $Omega(2^(n - 5 sqrt(n) log n))$ oracle queries. A marginal improvement to $2^(n - Omega(log^2 n))$ exists via Monotone Local Search @fomin2019. The direct brute-force algorithm runs in $O^*(2^n)$ time where $n = |E|$. - - *Example.* Let $E = {0, 1, dots, #(n - 1)}$ with $K = #K$. The three partition matroids have groups: $cal(F)_1$: #parts.at(0).map(fmt-group).join(", "); $cal(F)_2$: #parts.at(1).map(fmt-group).join(", "); $cal(F)_3$: #parts.at(2).map(fmt-group).join(", "). The subset $E' = #fmt-set(selected)$ is a valid common independent set of size $#K$: each matroid has at most one selected element per group. - - #pred-commands( - "pred create --example ThreeMatroidIntersection -o three-matroid-intersection.json", - "pred solve three-matroid-intersection.json", - "pred evaluate three-matroid-intersection.json --config " + tmi.optimal_config.map(str).join(","), - ) - - // Three rows of partition groups, elements shown in each - #figure( - canvas(length: 1cm, { - import draw: * - let matroid-labels = ($cal(F)_1$, $cal(F)_2$, $cal(F)_3$) - let row-y = (1.6, 0.0, -1.6) - let group-colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#59a14f")) - for (mi, partition) in parts.enumerate() { - let y = row-y.at(mi) - let total-groups = partition.len() - // Label - content((-3.2, y), text(9pt, matroid-labels.at(mi)), anchor: "east") - // Draw each group as a rounded rectangle with elements inside - for (gi, group) in partition.enumerate() { - let gx = (gi - (total-groups - 1) / 2) * 2.2 - let w = group.len() * 0.55 + 0.3 - rect((gx - w / 2, y - 0.35), (gx + w / 2, y + 0.35), - stroke: 0.8pt + group-colors.at(mi), radius: 4pt) - // Elements inside the group - for (ei, elem) in group.enumerate() { - let ex = gx + (ei - (group.len() - 1) / 2) * 0.5 - let is-sel = selected.contains(elem) - circle((ex, y), radius: 0.18, - fill: if is-sel { graph-colors.at(0) } else { white }, - stroke: 0.6pt + black) - content((ex, y), text(7pt, fill: if is-sel { white } else { black }, str(elem))) - } - } - } - }), - caption: [Three-Matroid Intersection: each row shows one partition matroid's groups (rounded boxes). The selected elements $E' = #fmt-set(selected)$ (blue) place at most one element per group in all three matroids.] - ) - ] - ] -} - #{ let x = load-model-example("ComparativeContainment") let n = x.instance.universe_size @@ -4485,8 +4437,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let S = x.instance.s_sets let r-weights = x.instance.r_weights let s-weights = x.instance.s_weights - let selected = x.optimal_config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let satisfiers = ((config: x.optimal_config, metric: x.optimal_value),).map(sol => sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)) + let selected = x.optimal_config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) + let satisfiers = ((config: x.optimal_config, metric: x.optimal_value),).map(sol => sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i)) let contains-selected(family-set) = selected.all(i => family-set.contains(i)) let r-active = range(R.len()).filter(i => contains-selected(R.at(i))) let s-active = range(S.len()).filter(i => contains-selected(S.at(i))) @@ -4511,7 +4463,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ComparativeContainment -o comparative-containment.json", "pred solve comparative-containment.json", - "pred evaluate comparative-containment.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate comparative-containment.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4580,7 +4532,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let k = x.instance.k let sat-count = 1 let basis = range(k).map(i => - range(U-size).filter(j => x.optimal_config.at(i * U-size + j) == 1) + range(U-size).filter(j => x.optimal_config.at(i).at(j)) ) let fmt-set(s) = "{" + s.map(e => str(e + 1)).join(", ") + "}" [ @@ -4594,7 +4546,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SetBasis -o set-basis.json", "pred solve set-basis.json", - "pred evaluate set-basis.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate set-basis.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4636,7 +4588,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let n = x.instance.num_attributes let deps = x.instance.dependencies let q = x.instance.query_attribute - let key = x.optimal_config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let key = x.optimal_config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let num-sat = 2 // candidate keys containing query attribute: {2,3} and {0,3} // Format a set as {e0, e1, ...} (0-indexed); no `$` — embed inside `$...$` or use `fmt-set-math` alias. let fmt-set(s) = "{" + s.map(e => str(e)).join(", ") + "}" @@ -4662,7 +4614,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PrimeAttributeName -o prime-attribute-name.json", "pred solve prime-attribute-name.json", - "pred evaluate prime-attribute-name.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate prime-attribute-name.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4719,7 +4671,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let n = x.instance.num_attributes let deps = x.instance.dependencies let m = deps.len() - let key-attrs = range(n).filter(i => x.optimal_config.at(i) == 1) + let key-attrs = range(n).filter(i => x.optimal_config.at(i)) let fmt-set(s) = "{" + s.map(e => str(e)).join(", ") + "}" let fmt-fd(d) = $#fmt-set(d.at(0)) arrow #fmt-set(d.at(1))$ [ @@ -4734,7 +4686,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumCardinalityKey -o minimum-cardinality-key.json", "pred solve minimum-cardinality-key.json", - "pred evaluate minimum-cardinality-key.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-cardinality-key.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4798,12 +4750,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Rooted Tree Storage Assignment is the storage-and-retrieval problem SR5 in Garey and Johnson @garey1979. Their catalog credits a reduction from Rooted Tree Arrangement, framing the problem as hierarchical file organization: pick a rooted tree on the records so every request set can be completed to a single root-to-leaf path using only a limited number of extra records. The implementation here uses one parent variable per element of $X$, so the direct exhaustive bound is $|X|^(|X|)$ candidate parent arrays, filtered down to valid rooted trees#footnote[No exact algorithm improving on the direct parent-array search bound is claimed here for the general formulation.]. - *Example.* Let $X = {0, 1, dots, #(n - 1)}$, $K = #K$, and $cal(C) = {#range(m).map(i => $X_#(i + 1)$).join(", ")}$ with #subsets.enumerate().map(((i, s)) => $X_#(i + 1) = #fmt-set(s)$).join(", "). The satisfying parent array $p = (#config.map(str).join(", "))$ encodes the rooted tree with arcs #edges.map(((u, v)) => $(#u, #v)$).join(", "). In this tree, $X_1 = {0, 2}$, $X_2 = {1, 3}$, and $X_4 = {2, 4}$ are already directed paths. The only extension is $X_3 = {0, 4}$, which becomes $X_3' = {0, 2, 4}$ along the path $0 -> 2 -> 4$, so the total extension cost is exactly $1 = K$. + *Example.* Let $X = {0, 1, dots, #(n - 1)}$, $K = #K$, and $cal(C) = {#range(m).map(i => $X_#(i + 1)$).join(", ")}$ with #subsets.enumerate().map(((i, s)) => $X_#(i + 1) = #fmt-set(s)$).join(", "). The satisfying parent array $p = (#fmt-values(config))$ encodes the rooted tree with arcs #edges.map(((u, v)) => $(#u, #v)$).join(", "). In this tree, $X_1 = {0, 2}$, $X_2 = {1, 3}$, and $X_4 = {2, 4}$ are already directed paths. The only extension is $X_3 = {0, 4}$, which becomes $X_3' = {0, 2, 4}$ along the path $0 -> 2 -> 4$, so the total extension cost is exactly $1 = K$. #pred-commands( "pred create --example " + problem-spec(x) + " -o rooted-tree-storage-assignment.json", "pred solve rooted-tree-storage-assignment.json --solver brute-force", - "pred evaluate rooted-tree-storage-assignment.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate rooted-tree-storage-assignment.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4839,7 +4791,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| content(pos, if highlighted { text(fill: white)[$#vertex$] } else { [$#vertex$] }) } }), - caption: [Rooted Tree Storage Assignment example. The rooted tree encoded by $p = (#config.map(str).join(", "))$ is shown; the blue path $0 -> 2 -> 4$ is the unique extension needed to realize $X_3 = {0, 4}$ within total cost $K = #K$.], + caption: [Rooted Tree Storage Assignment example. The rooted tree encoded by $p = (#fmt-values(config))$ is shown; the blue path $0 -> 2 -> 4$ is the unique extension needed to realize $X_3 = {0, 4}$ within total cost $K = #K$.], ) ] ] @@ -4869,7 +4821,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example TwoDimensionalConsecutiveSets -o two-dimensional-consecutive-sets.json", "pred solve two-dimensional-consecutive-sets.json", - "pred evaluate two-dimensional-consecutive-sets.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate two-dimensional-consecutive-sets.json --config " + cli-config(x.optimal_config), ) #figure( @@ -4950,10 +4902,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let n = spin-num-spins(x.instance) let edges = x.instance.graph.edges let ne = edges.len() - // Pick optimal config = (+,-,+,+,-) to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) - // Convert config (0=+1, 1=-1) to spin values - let spins = sol.config.map(v => if v == 0 { 1 } else { -1 }) + let spins = sol.config let H = metric-value(sol.metric) let spin-str = spins.map(s => if s > 0 { "+" } else { "-" }).join(", ") // Count satisfied and frustrated edges @@ -4970,7 +4920,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SpinGlass -o spinglass.json", "pred solve spinglass.json", - "pred evaluate spinglass.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate spinglass.json --config " + cli-config(x.optimal_config), ) #figure( @@ -5008,27 +4958,27 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| if v == vi { str(vi) } else { str(v) } }).join(", ")).join("; ") // Collect indices where x*_i = 1 (1-indexed) - let selected = xstar.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => $x_#(i + 1)$) + let selected = xstar.enumerate().filter(((i, v)) => v).map(((i, _)) => $x_#(i + 1)$) let unselected-pairs = () for i in range(n) { for j in range(i + 1, n) { - if Q.at(i).at(j) != 0 and (xstar.at(i) == 0 or xstar.at(j) == 0) { + if Q.at(i).at(j) != 0 and (not xstar.at(i) or not xstar.at(j)) { unselected-pairs.push($#(int(Q.at(i).at(j))) x_#(i + 1) x_#(j + 1)$) } } } [ #problem-def("QUBO")[ - Given $n$ binary variables $x_i in {0, 1}$, upper-triangular matrix $Q in RR^(n times n)$, minimize $f(bold(x)) = sum_(i=1)^n Q_(i i) x_i + sum_(i < j) Q_(i j) x_i x_j$ (using $x_i^2 = x_i$ for binary variables). + Given $n$ binary variables $x_i in {0, 1}$ and an upper-triangular matrix $Q$, minimize $f(bold(x)) = sum_(i=1)^n Q_(i i) x_i + sum_(i < j) Q_(i j) x_i x_j$ (using $x_i^2 = x_i$ for binary variables). The registered variants use either exact integer coefficients $Q in ZZ^(n times n)$ (the default) or finite floating-point coefficients $Q in RR^(n times n)$. ][ - Equivalent to the Ising model via the linear substitution $s_i = 2x_i - 1$. The native formulation for quantum annealing hardware (e.g., D-Wave) and a standard target for penalty-method reductions @glover2019. QUBO unifies many combinatorial problems into a single unconstrained binary framework, making it a universal intermediate representation for quantum and classical optimization. The best known general algorithm runs in $O^*(2^n)$ by brute-force enumeration#footnote[QUBO inherits the Ising model's complexity; no algorithm improving on brute-force is known for the general case.]. + Equivalent to the Ising model via the linear substitution $s_i = 2x_i - 1$. The native formulation for quantum annealing hardware (e.g., D-Wave) and a standard target for penalty-method reductions @glover2019. QUBO unifies many combinatorial problems into a single unconstrained binary framework, making it a universal intermediate representation for quantum and classical optimization. The integer variant stores exact coefficients, the floating-point variant stores finite real coefficients, and their explicit reduction converts exactly representable integers through `i64_to_exact_f64`. The best known general algorithm runs in $O^*(2^n)$ by brute-force enumeration#footnote[QUBO inherits the Ising model's complexity; no algorithm improving on brute-force is known for the general case.]. - *Example.* Consider $n = #n$ with $Q = mat(#mat-rows)$. The objective is $f(bold(x)) = -x_1 - x_2 - x_3 + 2x_1 x_2 + 2x_2 x_3$. Evaluating all $2^#n$ assignments: $f(0,0,0) = 0$, $f(1,0,0) = -1$, $f(0,1,0) = -1$, $f(0,0,1) = -1$, $f(1,1,0) = 0$, $f(0,1,1) = 0$, $f(1,0,1) = -2$, $f(1,1,1) = 1$. The minimum is $f^* = #fstar$ at $bold(x)^* = (#xstar.map(v => str(v)).join(", "))$: selecting #selected.join(" and ") avoids the penalty terms #unselected-pairs.join(" and "). + *Example.* Consider $n = #n$ with $Q = mat(#mat-rows)$. The objective is $f(bold(x)) = -x_1 - x_2 - x_3 + 2x_1 x_2 + 2x_2 x_3$. Evaluating all $2^#n$ assignments: $f(0,0,0) = 0$, $f(1,0,0) = -1$, $f(0,1,0) = -1$, $f(0,0,1) = -1$, $f(1,1,0) = 0$, $f(0,1,1) = 0$, $f(1,0,1) = -2$, $f(1,1,1) = 1$. The minimum is $f^* = #fstar$ at $bold(x)^* = (#fmt-values(xstar))$: selecting #selected.join(" and ") avoids the penalty terms #unselected-pairs.join(" and "). #pred-commands( "pred create --example QUBO -o qubo.json", "pred solve qubo.json", - "pred evaluate qubo.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate qubo.json --config " + cli-config(x.optimal_config), ) ] ] @@ -5036,7 +4986,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #{ let x = load-model-example("ILP") - let nv = x.instance.num_vars + let nv = x.instance.variables.len() let obj = x.instance.objective let constraints = x.instance.constraints let sol = (config: x.optimal_config, metric: x.optimal_value) @@ -5074,7 +5024,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o ilp.json", "pred solve ilp.json", - "pred evaluate ilp.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate ilp.json --config " + cli-config(x.optimal_config), ) #figure( @@ -5178,7 +5128,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example QuadraticAssignment -o quadratic-assignment.json", "pred solve quadratic-assignment.json", - "pred evaluate quadratic-assignment.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate quadratic-assignment.json --config " + cli-config(x.optimal_config), ) #figure( @@ -5246,12 +5196,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| *Example.* Consider $n = #n$ variables and #(eqs.len()) equations over $"GF"(2)$: $ #eqs.enumerate().map(((i, eq)) => $p_#(i+1): #render-eq(eq)$).join($, quad$) $ - The assignment $(x_0, x_1, x_2) = (#(config.map(str).join(", ")))$ satisfies all equations: + The assignment $(x_0, x_1, x_2) = (#fmt-values(config))$ satisfies all equations: #eqs.enumerate().map(((i, eq)) => { // Evaluate each monomial under the assignment let vals = eq.map(mono => { if mono.len() == 0 { 1 } - else { mono.fold(1, (acc, j) => acc * config.at(j)) } + else { mono.fold(1, (acc, j) => acc * bool-bit(config.at(j))) } }) let xor-sum = vals.fold(0, (acc, v) => calc.rem(acc + v, 2)) let lhs = eq.zip(vals).map(((mono, v)) => { @@ -5265,7 +5215,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example AlgebraicEquationsOverGF2 -o agf2.json", "pred solve agf2.json --solver brute-force", - "pred evaluate agf2.json --config " + config.map(str).join(","), + "pred evaluate agf2.json --config " + cli-config(config), ) ] ] @@ -5296,7 +5246,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example QuadraticCongruences -o qc.json", "pred solve qc.json --solver brute-force", - "pred evaluate qc.json --config " + config.map(str).join(","), + "pred evaluate qc.json --config " + cli-config(config), ) #align(center, table( @@ -5343,7 +5293,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example QuadraticDiophantineEquations -o qde.json", "pred solve qde.json --solver brute-force", - "pred evaluate qde.json --config " + config.map(str).join(","), + "pred evaluate qde.json --config " + cli-config(config), ) #align(center, table( @@ -5367,7 +5317,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let x = load-model-example("SimultaneousIncongruences") let pairs = x.instance.pairs let config = x.optimal_config - let xval = config.at(0) + let xval = config // Build table rows: for each pair (a, b), compute x mod b and check ≠ a mod b let rows = pairs.map(pair => { let a = pair.at(0) @@ -5396,12 +5346,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Simultaneous Incongruences is an NP-complete problem @garey1979. It asks whether the complement of a system of congruences — a _covering system_ — can be simultaneously avoided. A _covering system_ is a finite collection of congruences $\{a_i space (op("mod") space b_i)\}$ that covers every integer; when the system is a covering system there is no valid $x$ and the instance is a "no" instance. The problem generalises checking whether a given set of congruences is a covering system, which has connections to Erdős's covering conjecture and sieve methods in analytic number theory. - *Example.* Let $n = #pairs.len()$ with pairs #pairs.map(p => $(#p.at(0), #p.at(1))$).join(", "). The full period is $L = op("lcm")(#moduli.map(str).join(", ")) = #lcm-val$. We test $x = #xval$: + *Example.* Let $n = #pairs.len()$ with pairs #pairs.map(p => $(#p.at(0), #p.at(1))$).join(", "). The full period is $L = op("lcm")(#fmt-values(moduli)) = #lcm-val$. We test $x = #xval$: #pred-commands( "pred create --example SimultaneousIncongruences -o si.json", "pred solve si.json --solver brute-force", - "pred evaluate si.json --config " + config.map(str).join(","), + "pred evaluate si.json --config " + cli-config(config), ) #align(center, table( @@ -5470,12 +5420,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| F_2 = #fmt-payoff(polynomials.at(1)), quad F_3 = #fmt-payoff(polynomials.at(2)) $ - The assignment $bold(y) = (#assignment.map(str).join(", "))$ is a Nash equilibrium: #range(n).map(i => {let v = eval-payoff(i); let ii = i + 1; $F_#ii (bold(y)) = #v$}).join(", "), and no player can strictly improve their payoff by deviating. + The assignment $bold(y) = (#fmt-values(assignment))$ is a Nash equilibrium: #range(n).map(i => {let v = eval-payoff(i); let ii = i + 1; $F_#ii (bold(y)) = #v$}).join(", "), and no player can strictly improve their payoff by deviating. #pred-commands( "pred create --example EquilibriumPoint -o ep.json", "pred solve ep.json --solver brute-force", - "pred evaluate ep.json --config " + config.map(str).join(","), + "pred evaluate ep.json --config " + cli-config(config), ) ] ] @@ -5485,11 +5435,9 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let x = load-model-example("ClosestVectorProblem") let basis = x.instance.basis let target = x.instance.target - let bounds = x.instance.bounds let sol = (config: x.optimal_config, metric: x.optimal_value) let dist = metric-value(sol.metric) - // Config encodes offset from lower bound; recover actual integer coordinates - let coords = sol.config.enumerate().map(((i, v)) => v + bounds.at(i).lower) + let coords = sol.config // Compute B*x: sum over j of coords[j] * basis[j] let dim = basis.at(0).len() let bx = range(dim).map(d => coords.enumerate().fold(0.0, (acc, (j, c)) => acc + c * basis.at(j).at(d))) @@ -5498,16 +5446,16 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let dist-rounded = calc.round(dist, digits: 3) [ #problem-def("ClosestVectorProblem")[ - Given a lattice basis $bold(B) in RR^(m times n)$ (columns $bold(b)_1, dots, bold(b)_n in RR^m$ spanning lattice $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$) and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2$. + Given a full-column-rank integer lattice basis $bold(B) in ZZ^(m times n)$, whose columns span $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$, and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2$. ][ - The Closest Vector Problem is a fundamental lattice problem, proven NP-hard by van Emde Boas @vanemde1981. CVP appears in lattice-based cryptography, coding theory, and integer programming @lenstra1983. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$ using Voronoi cell computations, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. + The Closest Vector Problem is a fundamental lattice problem @micciancio2002 and is NP-hard @vanemde1981. The implementation provides an integer-target variant for exact reduction data and a finite-`f64` target variant for real input; both keep the lattice basis integral and place no bounds on $bold(x)$. Its reference solver uses exact rational Gram--Schmidt projections and sphere-enumeration bounds following the recursive enumeration structure of Fincke and Pohst @fincke1985. Finite `f64` targets are interpreted as their exact binary rational values. The solver is intended for small instances. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. - *Example.* Consider the 2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", ") and target $bold(t) = #fmt-vec(target)$. The lattice points near $bold(t)$ include $bold(B)(1, 0)^top = (2, 0)^top$, $bold(B)(0, 1)^top = (1, 2)^top$, and $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$. The closest is $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ with distance $norm(bold(B)(#coords.map(c => str(c)).join(","))^top - bold(t))_2 approx #dist-rounded$. + *Example.* Consider the 2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", ") and target $bold(t) = #fmt-vec(target)$. The point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ equals the target, so it is a closest lattice point with distance #dist-rounded. #pred-commands( "pred create --example ClosestVectorProblem -o closest-vector-problem.json", "pred solve closest-vector-problem.json", - "pred evaluate closest-vector-problem.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate closest-vector-problem.json --config " + cli-config(x.optimal_config), ) #figure( @@ -5556,20 +5504,20 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let fmt-lit(l) = if l > 0 { $x_#l$ } else { $not x_#(-l)$ } // Format a clause as (l1 or l2 or ...) let fmt-clause(c) = $paren.l #c.literals.map(fmt-lit).join($or$) paren.r$ - // Evaluate a literal under assignment: positive l -> assign[l-1], negative l -> 1-assign[|l|-1] - let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { 1 - assign.at(-l - 1) } + // Evaluate a literal under the Boolean assignment. + let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { not assign.at(-l - 1) } [ #problem-def("Satisfiability")[ Given a CNF formula $phi = and.big_(j=1)^m C_j$ with $m$ clauses over $n$ Boolean variables, where each clause $C_j = or.big_i ell_(j i)$ is a disjunction of literals, find an assignment $bold(x) in {0, 1}^n$ such that $phi(bold(x)) = 1$ (all clauses satisfied). ][ The Boolean Satisfiability Problem (SAT) is the first problem proven NP-complete @cook1971. SAT serves as the foundation of NP-completeness theory: showing a new problem NP-hard typically proceeds by reduction from SAT or one of its variants. Despite worst-case hardness, conflict-driven clause learning (CDCL) solvers handle industrial instances with millions of variables. The Strong Exponential Time Hypothesis (SETH) @impagliazzo2001 conjectures that no $O^*((2-epsilon)^n)$ algorithm exists for general CNF-SAT, and the best known algorithm runs in $O^*(2^n)$ by brute-force enumeration#footnote[SETH conjectures this is optimal; no $O^*((2-epsilon)^n)$ algorithm is known.]. - *Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",") ) = (#assign.map(v => str(v)).join(", "))$ satisfies all clauses: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #c.literals.map(l => str(eval-lit(l))).join($or$) paren.r = 1$).join(", "). Hence $phi(#assign.map(v => str(v)).join(", ")) = 1$. + *Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",") ) = (#fmt-values(assign))$ satisfies all clauses: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #c.literals.map(l => str(bool-bit(eval-lit(l)))).join($or$) paren.r = 1$).join(", "). Hence $phi(#fmt-values(assign)) = 1$. #pred-commands( "pred create --example SAT -o sat.json", "pred solve sat.json", - "pred evaluate sat.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(x.optimal_config), ) ] ] @@ -5582,23 +5530,23 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let clauses = x.instance.clauses let sol = (config: x.optimal_config, metric: x.optimal_value) let assign = sol.config - let complement = assign.map(v => 1 - v) + let complement = assign.map(v => not v) let fmt-lit(l) = if l > 0 { $x_#l$ } else { $not x_#(-l)$ } let fmt-clause(c) = $paren.l #c.literals.map(fmt-lit).join($or$) paren.r$ - let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { 1 - assign.at(-l - 1) } - let clause-values(c) = c.literals.map(l => str(eval-lit(l))) + let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { not assign.at(-l - 1) } + let clause-values(c) = c.literals.map(l => str(bool-bit(eval-lit(l)))) [ #problem-def("NAESatisfiability")[ Given a CNF formula $phi = and.big_(j=1)^m C_j$ with $m$ clauses over $n$ Boolean variables, where each clause $C_j = or.big_i ell_(j i)$ is a disjunction of literals, find an assignment $bold(x) in {0, 1}^n$ such that every clause contains at least one true literal and at least one false literal. ][ Not-All-Equal Satisfiability (NAE-SAT) is a canonical variant in Schaefer's dichotomy theorem @schaefer1978. Unlike ordinary SAT, each clause forbids the all-true and all-false patterns, giving the problem a complement symmetry: if an assignment is NAE-satisfying, then flipping every bit is also NAE-satisfying. This makes NAE-SAT a natural intermediate for cut and partition reductions such as Max-Cut. A straightforward exact algorithm enumerates all $2^n$ assignments; complement symmetry can halve the search space in practice by fixing one variable, but the asymptotic worst-case bound remains $O^*(2^n)$. - *Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#assign.map(v => str(v)).join(", "))$ is NAE-satisfying because each clause evaluates to a tuple containing both $0$ and $1$: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #clause-values(c).join(", ") paren.r$).join(", "). The complementary assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#complement.map(v => str(v)).join(", "))$ is therefore also NAE-satisfying, illustrating the paired-solution structure characteristic of NAE-SAT. + *Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#fmt-values(assign))$ is NAE-satisfying because each clause evaluates to a tuple containing both $0$ and $1$: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #clause-values(c).join(", ") paren.r$).join(", "). The complementary assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#fmt-values(complement))$ is therefore also NAE-satisfying, illustrating the paired-solution structure characteristic of NAE-SAT. #pred-commands( "pred create --example NAESatisfiability -o nae-satisfiability.json", "pred solve nae-satisfiability.json", - "pred evaluate nae-satisfiability.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate nae-satisfiability.json --config " + cli-config(x.optimal_config), ) ] ] @@ -5615,19 +5563,19 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let assign = sol.config let fmt-lit(l) = if l > 0 { $x_#l$ } else { $not x_#(-l)$ } let fmt-clause(c) = $paren.l #c.literals.map(fmt-lit).join($or$) paren.r$ - let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { 1 - assign.at(-l - 1) } + let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { not assign.at(-l - 1) } [ #problem-def("KSatisfiability")[ SAT with exactly $k$ literals per clause. ][ The restriction of SAT to exactly $k$ literals per clause reveals a sharp complexity transition: 2-SAT is polynomial-time solvable via implication graph SCC decomposition @aspvall1979 in $O(n+m)$, while $k$-SAT for $k >= 3$ is NP-complete. Random $k$-SAT exhibits a satisfiability threshold at clause density $m slash n approx 2^k ln 2$, a key phenomenon in computational phase transitions. The best known algorithm for 3-SAT runs in $O^*(1.307^n)$ via biased-PPSZ @hansen2019. Under SETH, $k$-SAT requires time $O^*(c_k^n)$ with $c_k -> 2$ as $k -> infinity$. - *Example.* Consider the #{k}-SAT formula $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses, each containing exactly #k literals. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#assign.map(v => str(v)).join(", "))$ satisfies all clauses: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #c.literals.map(l => str(eval-lit(l))).join($or$) paren.r = 1$).join(", "). + *Example.* Consider the #{k}-SAT formula $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses, each containing exactly #k literals. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#fmt-values(assign))$ satisfies all clauses: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #c.literals.map(l => str(bool-bit(eval-lit(l)))).join($or$) paren.r = 1$).join(", "). #pred-commands( "pred create --example " + problem-spec(x) + " -o ksat.json", "pred solve ksat.json", - "pred evaluate ksat.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(x.optimal_config), ) ] ] @@ -5642,19 +5590,19 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let assign = sol.config let fmt-lit(l) = if l > 0 { $x_#l$ } else { $not x_#(-l)$ } let fmt-clause(c) = $paren.l #c.literals.map(fmt-lit).join($or$) paren.r$ - let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { 1 - assign.at(-l - 1) } + let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { not assign.at(-l - 1) } [ #problem-def("Planar3Satisfiability")[ Given a 3-CNF formula $phi = and.big_(j=1)^m C_j$ with $m$ clauses over $n$ Boolean variables, where each clause $C_j$ contains exactly 3 literals, and the variable-clause incidence graph $H(phi)$ is planar, find a satisfying assignment $bold(x) in {0, 1}^n$. ][ Planar 3-SAT is a restricted variant of 3-SAT introduced by Lichtenstein @lichtenstein1982, who proved it NP-complete. The incidence graph $H(phi)$ is bipartite with variable nodes and clause nodes, connected by edges when a variable appears in a clause. Requiring $H(phi)$ to be planar is a strong structural constraint that enables reductions to geometric and planar problems (e.g., rectilinear Steiner tree, planar vertex cover). The best known algorithm shares the 3-SAT bound of $O^*(1.307^n)$ via biased-PPSZ @hansen2019, since any Planar 3-SAT instance is also a valid 3-SAT instance. - *Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#assign.map(v => str(v)).join(", "))$ satisfies all clauses: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #c.literals.map(l => str(eval-lit(l))).join($or$) paren.r = 1$).join(", "). + *Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#fmt-values(assign))$ satisfies all clauses: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #c.literals.map(l => str(bool-bit(eval-lit(l)))).join($or$) paren.r = 1$).join(", "). #pred-commands( "pred create --example Planar3Satisfiability -o planar3sat.json", "pred solve planar3sat.json", - "pred evaluate planar3sat.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate planar3sat.json --config " + cli-config(x.optimal_config), ) ] ] @@ -5669,20 +5617,20 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let assign = sol.config let fmt-lit(l) = if l > 0 { $x_#l$ } else { $not x_#(-l)$ } let fmt-clause(c) = $paren.l #c.literals.map(fmt-lit).join($or$) paren.r$ - let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { 1 - assign.at(-l - 1) } - let count-true(c) = c.literals.map(eval-lit).sum() + let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { not assign.at(-l - 1) } + let count-true(c) = c.literals.map(l => bool-bit(eval-lit(l))).sum() [ #problem-def("OneInThreeSatisfiability")[ Given a CNF formula $phi = and.big_(j=1)^m C_j$ with $m$ clauses over $n$ Boolean variables, where each clause $C_j$ contains exactly 3 literals, find a truth assignment $bold(x) in {0, 1}^n$ such that each clause has _exactly one_ true literal. ][ One-in-Three Satisfiability (1-in-3 SAT) was introduced by Schaefer @schaefer1978 as part of his dichotomy theorem for generalized satisfiability. Unlike standard 3-SAT which requires at least one true literal per clause, 1-in-3 SAT requires exactly one. The problem is NP-complete even for monotone instances (no negations). The best known algorithm runs in $O^*(1.307^n)$ time via biased-PPSZ @hansen2019, since every 1-in-3 SAT instance reduces trivially to 3-SAT. - *Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#assign.map(v => str(v)).join(", "))$ satisfies the 1-in-3 condition: #clauses.enumerate().map(((j, c)) => $C_#(j + 1)$+ " has " + str(count-true(c)) + " true literal").join(", "). + *Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#fmt-values(assign))$ satisfies the 1-in-3 condition: #clauses.enumerate().map(((j, c)) => $C_#(j + 1)$+ " has " + str(count-true(c)) + " true literal").join(", "). #pred-commands( "pred create --example OneInThreeSatisfiability -o 1in3sat.json", "pred solve 1in3sat.json", - "pred evaluate 1in3sat.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate 1in3sat.json --config " + cli-config(x.optimal_config), ) ] ] @@ -5697,8 +5645,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let assign = sol.config let fmt-lit(l) = if l > 0 { $x_#l$ } else { $not x_#(-l)$ } let fmt-clause(c) = $paren.l #c.literals.map(fmt-lit).join($or$) paren.r$ - let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { 1 - assign.at(-l - 1) } - let clause-sat(c) = c.literals.map(eval-lit).any(v => v == 1) + let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { not assign.at(-l - 1) } + let clause-sat(c) = c.literals.map(eval-lit).any(v => v) let sat-count = clauses.filter(clause-sat).len() [ #problem-def("Maximum2Satisfiability")[ @@ -5706,12 +5654,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Maximum 2-Satisfiability (MAX-2-SAT) is one of the fundamental NP-hard optimization problems. While the decision version of 2-SAT is solvable in linear time by implication-graph analysis, the optimization variant---maximizing the number of satisfied clauses---is NP-hard @garey1979. The best known exact algorithm by Williams @williams2005 runs in $O^*(2^(0.7905n))$ time by reducing to a maximum-weight triangle problem and applying fast matrix multiplication. - *Example.* Consider $m = #m$ clauses over $n = #n$ variables: $#clauses.map(fmt-clause).join($and$)$. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#assign.map(v => str(v)).join(", "))$ satisfies #sat-count out of #m clauses. + *Example.* Consider $m = #m$ clauses over $n = #n$ variables: $#clauses.map(fmt-clause).join($and$)$. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#fmt-values(assign))$ satisfies #sat-count out of #m clauses. #pred-commands( "pred create --example Maximum2Satisfiability -o max2sat.json", "pred solve max2sat.json", - "pred evaluate max2sat.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate max2sat.json --config " + cli-config(x.optimal_config), ) ] ] @@ -5725,20 +5673,20 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let assign = sol.config let fmt-lit(l) = if l > 0 { $x_#l$ } else { $not x_#(-l)$ } let fmt-disjunct(d) = $paren.l #d.map(fmt-lit).join($and$) paren.r$ - let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { 1 - assign.at(-l - 1) } - let disjunct-true(d) = d.map(eval-lit).all(v => v == 1) + let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { not assign.at(-l - 1) } + let disjunct-true(d) = d.map(eval-lit).all(v => v) [ #problem-def("NonTautology")[ Given a Boolean formula in DNF $phi = or.big_(j=1)^m D_j$ with $m$ disjuncts over $n$ Boolean variables, where each disjunct $D_j$ is a conjunction of literals, find a truth assignment $bold(x) in {0, 1}^n$ such that $phi(bold(x)) = 0$ (i.e., every disjunct is false). ][ The Non-Tautology problem asks whether a given DNF formula is _not_ a tautology, by finding a falsifying assignment. A disjunct $D_j = ell_1 and dots and ell_k$ is false when at least one of its literals evaluates to false; the formula is false when all disjuncts are false. The problem is coNP-complete in general and closely related to SAT through De Morgan duality: a DNF formula $phi$ is a tautology iff $not phi$ (a CNF formula) is unsatisfiable. - *Example.* Consider $phi = #disjuncts.map(fmt-disjunct).join($or$)$ with $n = #n$ variables and $m = #m$ disjuncts. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#assign.map(v => str(v)).join(", "))$ falsifies the formula: #disjuncts.enumerate().map(((j, d)) => $D_#(j + 1)$+ " is " + if disjunct-true(d) { "true" } else { "false" }).join(", "). + *Example.* Consider $phi = #disjuncts.map(fmt-disjunct).join($or$)$ with $n = #n$ variables and $m = #m$ disjuncts. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#fmt-values(assign))$ falsifies the formula: #disjuncts.enumerate().map(((j, d)) => $D_#(j + 1)$+ " is " + if disjunct-true(d) { "true" } else { "false" }).join(", "). #pred-commands( "pred create --example NonTautology -o nontaut.json", "pred solve nontaut.json", - "pred evaluate nontaut.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate nontaut.json --config " + cli-config(x.optimal_config), ) ] ] @@ -5814,7 +5762,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } // Build initial env from satisfying assignment #let init-env = inputs.enumerate().fold((:), (env, pair) => { - let (i, v) = pair; env.insert(v, sat-assigns.at(0).at(i)); env + let (i, v) = pair; env.insert(v, bool-bit(sat-assigns.at(0).at(i))); env }) // Evaluate gates in order, accumulating (env, steps) #let result = gates.fold((env: init-env, steps: ()), (acc, a) => { @@ -5829,12 +5777,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #let final-val = eval-expr(output-gate.expr, result.env) *Example.* Consider the circuit with $n = #n$ inputs and $g = #g$ gates: #gate-defs.join(", "), giving $C(#inputs.map(fmt-input).join(", ")) = #circuit-expr$. - The assignment $(#inputs.map(fmt-input).join(", ")) = (#sat-assigns.at(0).map(str).join(", "))$ is satisfying: #result.steps.join(", "), so $C = #final-val$. + The assignment $(#inputs.map(fmt-input).join(", ")) = (#fmt-values(sat-assigns.at(0)))$ is satisfying: #result.steps.join(", "), so $C = #final-val$. #pred-commands( "pred create --example CircuitSAT -o circuitsat.json", "pred solve circuitsat.json", - "pred evaluate circuitsat.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate circuitsat.json --config " + cli-config(x.optimal_config), ) #figure( @@ -5855,7 +5803,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| draw.content((-0.3, 0.975), text(8pt)[$x_1$]) draw.content((-0.3, -0.975), text(8pt)[$x_2$]) }), - caption: [Circuit $C(#inputs.map(fmt-input).join(", ")) = #circuit-expr$. Junction dots mark where inputs fork to both gates. Satisfying assignments: #sat-assigns.map(a => $paren.l #a.map(v => str(v)).join(", ") paren.r$).join(" and ").], + caption: [Circuit $C(#inputs.map(fmt-input).join(", ")) = #circuit-expr$. Junction dots mark where inputs fork to both gates. Satisfying assignments: #sat-assigns.map(a => $paren.l #fmt-values(a) paren.r$).join(" and ").], ) ] ] @@ -5896,7 +5844,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ConjunctiveQueryFoldability -o cqf.json", "pred solve cqf.json", - "pred evaluate cqf.json --config " + config.map(str).join(","), + "pred evaluate cqf.json --config " + cli-config(config), ) #figure( @@ -5984,7 +5932,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let yset = if yop.kind == "elem" { (yop.val,) } else { z-sets.at(yop.val) } z-sets.push((xset + yset).dedup().sorted()) } - let fmt-elem-set(s) = "{" + s.map(str).join(", ") + "}" + let fmt-elem-set(s) = "{" + fmt-values(s) + "}" let fmt-op(op) = if op.kind == "elem" { "{" + str(op.val) + "}" } else { @@ -6002,7 +5950,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example EnsembleComputation -o ensemble.json", "pred solve ensemble.json", - "pred evaluate ensemble.json --config " + config.map(str).join(","), + "pred evaluate ensemble.json --config " + cli-config(config), ) ] ] @@ -6014,21 +5962,20 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let mb = x.instance.m let nb = x.instance.n let sol = x.optimal_config - // First mb bits encode p, next nb bits encode q - let p = range(mb).fold(0, (acc, i) => acc + sol.at(i) * calc.pow(2, i)) + 2 - let q = range(nb).fold(0, (acc, i) => acc + sol.at(mb + i) * calc.pow(2, i)) + 2 + let p = sol.at(0).at(0) + let q = sol.at(1).at(0) [ #problem-def("Factoring")[ - Given a composite integer $N$ and bit sizes $m, n$, find integers $p in [2, 2^m - 1]$ and $q in [2, 2^n - 1]$ such that $p times q = N$. Here $p$ has $m$ bits and $q$ has $n$ bits. + Given an integer $N >= 2$ and optional maximum factor widths $m <= n$, find integers $p in [0, 2^m - 1]$ and $q in [0, 2^n - 1]$ such that $p <= q$ and $p times q = N$. If the widths are omitted, let $b$ be the bit length of $N$ and use $m = ceil(b slash 2)$ and $n = b - 1$. ][ The hardness of integer factorization underpins RSA cryptography and other public-key systems. Unlike most problems in this collection, Factoring is not known to be NP-complete; it lies in NP $inter$ co-NP, suggesting it may be of intermediate complexity. The best classical algorithm is the General Number Field Sieve @lenstra1993 running in sub-exponential time $e^(O(b^(1 slash 3)(log b)^(2 slash 3)))$ where $b$ is the bit length. Shor's algorithm @shor1994 solves Factoring in polynomial time on a quantum computer. - *Example.* Let $N = #N$ with $m = #mb$ bits and $n = #nb$ bits, so $p in [2, #(calc.pow(2, mb) - 1)]$ and $q in [2, #(calc.pow(2, nb) - 1)]$. The solution is $p = #p$, $q = #q$, since $#p times #q = #N = N$. Note $p = #p$ fits in #mb bits and $q = #q$ fits in #nb bits. The alternative factorization $#q times #p$ requires $m = #nb$, $n = #mb$. + *Example.* Let $N = #N$. Its bit length is 4, so the default bounds are $m = #mb$ and $n = #nb$. The canonical solution is $p = #p$, $q = #q$, since $#p <= #q$ and $#p times #q = #N = N$. Explicit wider bounds may also admit a trivial factorization such as $1 times N$. #pred-commands( "pred create --example Factoring -o factoring.json", "pred solve factoring.json", - "pred evaluate factoring.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate factoring.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6054,7 +6001,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example QuantifiedBooleanFormulas -o quantified-boolean-formulas.json", "pred solve quantified-boolean-formulas.json", - "pred evaluate quantified-boolean-formulas.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate quantified-boolean-formulas.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6081,7 +6028,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ExpectedRetrievalCost -o expected-retrieval-cost.json", "pred solve expected-retrieval-cost.json --solver brute-force", - "pred evaluate expected-retrieval-cost.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate expected-retrieval-cost.json --config " + cli-config(x.optimal_config), ) #figure( @@ -6114,7 +6061,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumDiscretePlanarInverseKinematics -o ik.json", "pred solve ik.json --solver brute-force", - "pred evaluate ik.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate ik.json --config " + cli-config(x.optimal_config), ) #figure( @@ -6141,12 +6088,9 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let k = x.instance.k let A = x.instance.matrix let fs = metric-value(x.optimal_value) - // Decode B and C from optimal config - // Config layout: B is m*k values, then C is k*n values let cfg = x.optimal_config - let B = range(mr).map(i => range(k).map(j => cfg.at(i * k + j))) - let C = range(k).map(i => range(nc).map(j => cfg.at(mr * k + i * nc + j))) - // Convert A from bool to int for display + let B = cfg.at(0).map(row => row.map(bool-bit)) + let C = cfg.at(1).map(row => row.map(bool-bit)) let A-int = A.map(row => row.map(v => if v { 1 } else { 0 })) // Format matrix as math.mat with proper rows let fmt-mat(m) = math.mat(..m.map(row => row.map(v => $#v$))) @@ -6161,7 +6105,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example BMF -o bmf.json", "pred solve bmf.json", - "pred evaluate bmf.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate bmf.json --config " + cli-config(x.optimal_config), ) #figure( @@ -6244,7 +6188,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ConsecutiveBlockMinimization -o consecutive-block-minimization.json", "pred solve consecutive-block-minimization.json", - "pred evaluate consecutive-block-minimization.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate consecutive-block-minimization.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6261,10 +6205,10 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let num-changes = metric-value(sol.metric) // Build the full sequence of car labels let seq-labels = seq-indices.map(i => labels.at(i)) - // Build color sequence: for each position, if is_first[pos] then color = assign[car], else 1-assign[car] + // Build the color sequence, flipping the second occurrence of each car. let color-seq = range(seq-indices.len()).map(pos => { let car = seq-indices.at(pos) - if is-first.at(pos) { assign.at(car) } else { 1 - assign.at(car) } + if is-first.at(pos) { assign.at(car) } else { not assign.at(car) } }) [ #problem-def("PaintShop")[ @@ -6272,12 +6216,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ NP-hard and APX-hard @epping2004. Arises in automotive manufacturing where color changes between consecutive cars on an assembly line require costly purging of paint nozzles. Each car appears twice in the sequence (two coats), and each car's two occurrences must receive opposite colors (one per side). A natural benchmark for quantum annealing due to its binary structure and industrial relevance. The best known algorithm runs in $O^*(2^n)$ by brute-force enumeration#footnote[No algorithm improving on brute-force is known for general Paint Shop.]. - *Example.* Consider $n = #n-cars$ cars with sequence $(#seq-labels.join(", "))$. Each car gets one occurrence colored 0 and the other colored 1. The assignment #labels.zip(assign).map(((l, c)) => [#l: #c\/#(1 - c)]).join(", ") yields color sequence $(#color-seq.map(c => str(c)).join(", "))$ with #num-changes color changes. The minimum is #num-changes changes. + *Example.* Consider $n = #n-cars$ cars with sequence $(#seq-labels.join(", "))$. Each car gets one occurrence colored 0 and the other colored 1. The assignment #labels.zip(assign).map(((l, c)) => [#l: #bool-bit(c)\/#bool-bit(not c)]).join(", ") yields color sequence $(#fmt-values(color-seq))$ with #num-changes color changes. The minimum is #num-changes changes. #pred-commands( "pred create --example PaintShop -o paint-shop.json", "pred solve paint-shop.json", - "pred evaluate paint-shop.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate paint-shop.json --config " + cli-config(x.optimal_config), ) #figure( @@ -6295,11 +6239,11 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| items.push(box(width: 0.15cm)) // spacer } } - let fill = if c == 0 { white } else { blue.transparentize(40%) } + let fill = if not c { white } else { blue.transparentize(40%) } items.push(stack(dir: ttb, spacing: 0.08cm, box(width: 0.55cm, height: 0.55cm, fill: fill, stroke: 0.5pt + luma(120), align(center + horizon, text(8pt, weight: "bold", car))), - text(6pt, fill: luma(100), str(c)), + text(6pt, fill: luma(100), str(bool-bit(c))), )) } align(center, stack(dir: ltr, spacing: 0pt, ..items)) @@ -6330,7 +6274,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example BicliqueCover -o biclique-cover.json", "pred solve biclique-cover.json", - "pred evaluate biclique-cover.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate biclique-cover.json --config " + cli-config(x.optimal_config), ) #figure( @@ -6363,8 +6307,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let k = x.instance.k let bip-edges = x.instance.graph.edges let sol = (config: x.optimal_config, metric: x.optimal_value) - let left-selected = range(left-size).filter(i => sol.config.at(i) == 1) - let right-selected = range(right-size).filter(i => sol.config.at(left-size + i) == 1) + let left-selected = range(left-size).filter(i => sol.config.at(i)) + let right-selected = range(right-size).filter(i => sol.config.at(left-size + i)) let selected-edges = bip-edges.filter(e => left-selected.contains(e.at(0)) and right-selected.contains(e.at(1)) ) @@ -6380,7 +6324,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example BalancedCompleteBipartiteSubgraph -o balanced-complete-bipartite-subgraph.json", "pred solve balanced-complete-bipartite-subgraph.json", - "pred evaluate balanced-complete-bipartite-subgraph.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate balanced-complete-bipartite-subgraph.json --config " + cli-config(x.optimal_config), ) #figure( @@ -6450,7 +6394,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PartitionIntoTriangles -o partition-into-triangles.json", "pred solve partition-into-triangles.json", - "pred evaluate partition-into-triangles.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate partition-into-triangles.json --config " + cli-config(x.optimal_config), ) #figure( @@ -6494,7 +6438,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PartitionIntoForests -o partition-into-forests.json", "pred solve partition-into-forests.json", - "pred evaluate partition-into-forests.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate partition-into-forests.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6507,7 +6451,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let edges = x.instance.graph.edges let K = x.instance.max_degree let sol = (config: x.optimal_config, metric: x.optimal_value) - let selected-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let selected-edges = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) [ #problem-def("DegreeConstrainedSpanningTree")[ Given an undirected graph $G = (V, E)$ and a positive integer $K$, determine whether $G$ contains a spanning tree $T$ in which every vertex has degree at most $K$. @@ -6519,7 +6463,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example DegreeConstrainedSpanningTree -o dcst.json", "pred solve dcst.json", - "pred evaluate dcst.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate dcst.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6534,7 +6478,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let D = x.instance.diameter_bound let ew = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) - let selected-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let selected-edges = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let total-weight = selected-edges.map(i => ew.at(i)).sum() [ #problem-def("BoundedDiameterSpanningTree")[ @@ -6547,7 +6491,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example BoundedDiameterSpanningTree -o bdst.json", "pred solve bdst.json", - "pred evaluate bdst.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate bdst.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6559,7 +6503,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ne = graph-num-edges(x.instance) let edges = x.instance.graph.edges let sol = (config: x.optimal_config, metric: x.optimal_value) - let tree-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let tree-edges = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let leaf-count = sol.metric // compute degrees in the tree let degrees = range(nv).map(v => tree-edges.map(i => edges.at(i)).filter(((u, w)) => u == v or w == v).len()) @@ -6575,7 +6519,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MaximumLeafSpanningTree -o mlst.json", "pred solve mlst.json", - "pred evaluate mlst.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mlst.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6597,7 +6541,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MonochromaticTriangle -o monochromatic-triangle.json", "pred solve monochromatic-triangle.json", - "pred evaluate monochromatic-triangle.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate monochromatic-triangle.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6622,7 +6566,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PartitionIntoCliques -o partition-into-cliques.json", "pred solve partition-into-cliques.json", - "pred evaluate partition-into-cliques.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate partition-into-cliques.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6647,7 +6591,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PartitionIntoPerfectMatchings -o partition-into-perfect-matchings.json", "pred solve partition-into-perfect-matchings.json", - "pred evaluate partition-into-perfect-matchings.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate partition-into-perfect-matchings.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6676,7 +6620,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example BinPacking -o bin-packing.json", "pred solve bin-packing.json", - "pred evaluate bin-packing.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate bin-packing.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -6718,7 +6662,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let n = weights.len() let config = x.optimal_config let opt-val = metric-value(x.optimal_value) - let selected = range(n).filter(i => config.at(i) == 1) + let selected = range(n).filter(i => config.at(i)) let total-w = selected.map(i => weights.at(i)).sum() let total-v = selected.map(i => values.at(i)).sum() [ @@ -6732,7 +6676,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example Knapsack -o knapsack.json", "pred solve knapsack.json", - "pred evaluate knapsack.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate knapsack.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6759,7 +6703,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example IntegerKnapsack -o ik.json", "pred solve ik.json", - "pred evaluate ik.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate ik.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6797,7 +6741,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example RectilinearPictureCompression -o rectilinear-picture-compression.json", "pred solve rectilinear-picture-compression.json", - "pred evaluate rectilinear-picture-compression.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate rectilinear-picture-compression.json --config " + cli-config(x.optimal_config), ) #figure( @@ -6854,7 +6798,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example FeasibleRegisterAssignment -o feasible-register-assignment.json", "pred solve feasible-register-assignment.json --solver brute-force", - "pred evaluate feasible-register-assignment.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate feasible-register-assignment.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6873,12 +6817,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Minimum Register Sufficiency for Loops is problem SS20 in Garey & Johnson @garey1979. It is equivalent to minimum coloring of circular arc graphs. NP-complete via reduction from Chromatic Number. No algorithm improving on brute-force $O(n^n)$ enumeration is known for arbitrary circular arc instances. - *Example.* Let $N = #N$ timesteps and $n = #nv$ variables with arcs: #vars.enumerate().map(((i, v)) => $x_#i: [#(v.at(0)), #(v.at(0)) + #(v.at(1)))$).join(", ") mod $#N$. All pairs of arcs overlap (each arc covers half the circle and any two arcs share at least one timestep), forming a complete conflict graph $K_#nv$. The assignment $(#config.map(str).join(", "))$ uses #num-regs distinct registers, which is optimal. + *Example.* Let $N = #N$ timesteps and $n = #nv$ variables with arcs: #vars.enumerate().map(((i, v)) => $x_#i: [#(v.at(0)), #(v.at(0)) + #(v.at(1)))$).join(", ") mod $#N$. All pairs of arcs overlap (each arc covers half the circle and any two arcs share at least one timestep), forming a complete conflict graph $K_#nv$. The assignment $(#fmt-values(config))$ uses #num-regs distinct registers, which is optimal. #pred-commands( "pred create --example MinimumRegisterSufficiencyForLoops -o mrsfl.json", "pred solve mrsfl.json --solver brute-force", - "pred evaluate mrsfl.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mrsfl.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6905,7 +6849,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example RegisterSufficiency -o register-sufficiency.json", "pred solve register-sufficiency.json --solver brute-force", - "pred evaluate register-sufficiency.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate register-sufficiency.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6937,7 +6881,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumCodeGenerationOneRegister -o mcgor.json", "pred solve mcgor.json --solver brute-force", - "pred evaluate mcgor.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mcgor.json --config " + cli-config(x.optimal_config), ) ] ] @@ -6970,7 +6914,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumCodeGenerationUnlimitedRegisters -o mcgur.json", "pred solve mcgur.json --solver brute-force", - "pred evaluate mcgur.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mcgur.json --config " + cli-config(x.optimal_config), ) ] ] @@ -7003,7 +6947,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumCodeGenerationParallelAssignments -o mcgpa.json", "pred solve mcgpa.json --solver brute-force", - "pred evaluate mcgpa.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mcgpa.json --config " + cli-config(x.optimal_config), ) ] ] @@ -7057,7 +7001,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumDecisionTree -o mdt.json", "pred solve mdt.json", - "pred evaluate mdt.json --config " + sol.map(str).join(","), + "pred evaluate mdt.json --config " + cli-config(sol), ) ] ] @@ -7086,7 +7030,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| $paren.l #bits.join(",") paren.r$ } // Selected prime implicants - let selected = cfg.enumerate().filter(((_, v)) => v == 1).map(((i, _)) => i) + let selected = cfg.enumerate().filter(((_, selected)) => selected).map(((i, _)) => i) [ #problem-def("MinimumDisjunctiveNormalForm")[ Given $n$ Boolean variables and a Boolean function $f: {0,1}^n -> {0,1}$ specified by its truth table, find a disjunctive normal form (DNF) formula with the minimum number of terms (disjuncts) that is equivalent to $f$. @@ -7136,7 +7080,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example RuralPostman -o rural-postman.json", "pred solve rural-postman.json", - "pred evaluate rural-postman.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate rural-postman.json --config " + cli-config(x.optimal_config), ) #figure( @@ -7180,14 +7124,14 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } #{ - let x = load-model-example("MixedChinesePostman", variant: (weight: "i32")) + let x = load-model-example("MixedChinesePostman", variant: (weight: "i64")) let nv = x.instance.graph.num_vertices let arcs = x.instance.graph.arcs let edges = x.instance.graph.edges let arc-weights = x.instance.arc_weights let edge-weights = x.instance.edge_weights let config = x.optimal_config - let oriented = edges.enumerate().map(((i, e)) => if config.at(i) == 0 { e } else { (e.at(1), e.at(0)) }) + let oriented = edges.enumerate().map(((i, e)) => if not config.at(i) { e } else { (e.at(1), e.at(0)) }) let base-cost = arc-weights.sum() + edge-weights.sum() let total-cost = x.optimal_value [ @@ -7196,12 +7140,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Mixed Chinese Postman is the mixed-graph arc-routing problem ND25 in Garey and Johnson @garey1979. Papadimitriou proved the mixed case NP-complete even when all lengths are 1, the graph is planar, and the maximum degree is 3 @papadimitriou1976edge. In contrast, the pure undirected and pure directed cases are polynomial-time solvable via matching / circulation machinery @edmondsjohnson1973. The implementation here uses one binary variable per undirected edge orientation, so the search space contributes the $2^|E|$ factor visible in the registered exact bound. - *Example.* Consider the instance on #nv vertices with directed arcs $(v_0, v_1)$, $(v_1, v_2)$, $(v_2, v_3)$, $(v_3, v_0)$ of lengths $2, 3, 1, 4$ and undirected edges $\{v_0, v_2\}$, $\{v_1, v_3\}$, $\{v_0, v_4\}$, $\{v_4, v_2\}$ of lengths $2, 3, 1, 2$. The config $(#config.map(str).join(", "))$ orients those edges as $(v_2, v_0)$, $(v_3, v_1)$, $(v_0, v_4)$, and $(v_4, v_2)$, producing a strongly connected digraph. The base traversal cost is #base-cost, and the minimum balancing cost brings the total to #total-cost. + *Example.* Consider the instance on #nv vertices with directed arcs $(v_0, v_1)$, $(v_1, v_2)$, $(v_2, v_3)$, $(v_3, v_0)$ of lengths $2, 3, 1, 4$ and undirected edges $\{v_0, v_2\}$, $\{v_1, v_3\}$, $\{v_0, v_4\}$, $\{v_4, v_2\}$ of lengths $2, 3, 1, 2$. The config $(#fmt-values(config))$ orients those edges as $(v_2, v_0)$, $(v_3, v_1)$, $(v_0, v_4)$, and $(v_4, v_2)$, producing a strongly connected digraph. The base traversal cost is #base-cost, and the minimum balancing cost brings the total to #total-cost. #pred-commands( - "pred create --example MixedChinesePostman/i32 -o mixed-chinese-postman.json", + "pred create --example MixedChinesePostman/i64 -o mixed-chinese-postman.json", "pred solve mixed-chinese-postman.json --solver brute-force", - "pred evaluate mixed-chinese-postman.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mixed-chinese-postman.json --config " + cli-config(x.optimal_config), ) #figure( @@ -7325,12 +7269,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| A configuration is a permutation of the required arcs, interpreted as the order in which those arcs are forced into the tour. The verifier traverses each chosen arc, then inserts the shortest available connector path from that arc's head to the tail of the next arc, wrapping around at the end to close the walk. - *Example.* The canonical instance has 6 vertices, 5 required arcs, and 7 undirected edges. The optimal configuration $[#config.map(str).join(", ")]$ orders the required arcs as $a_0, a_2, a_1, a_4, a_3$. Traversing those arcs contributes 17 units of required-arc length, and the shortest connector paths contribute $1 + 1 + 1 + 0 + 0 = 3$, so the resulting closed walk has minimum total length $20$. + *Example.* The canonical instance has 6 vertices, 5 required arcs, and 7 undirected edges. The optimal configuration $[#fmt-values(config)]$ orders the required arcs as $a_0, a_2, a_1, a_4, a_3$. Traversing those arcs contributes 17 units of required-arc length, and the shortest connector paths contribute $1 + 1 + 1 + 0 + 0 = 3$, so the resulting closed walk has minimum total length $20$. #pred-commands( "pred create --example " + problem-spec(x) + " -o stacker-crane.json", "pred solve stacker-crane.json --solver brute-force", - "pred evaluate stacker-crane.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate stacker-crane.json --config " + cli-config(x.optimal_config), ) #figure( @@ -7389,7 +7333,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SubgraphIsomorphism -o subgraph-isomorphism.json", "pred solve subgraph-isomorphism.json", - "pred evaluate subgraph-isomorphism.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate subgraph-isomorphism.json --config " + cli-config(x.optimal_config), ) #{ @@ -7488,12 +7432,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Grouping by Swapping is the storage-and-retrieval problem SR21 in Garey and Johnson @garey1979. It asks whether a string can be locally reorganized, using only adjacent transpositions, until equal symbols coalesce into blocks. The implementation in this crate uses a fixed-length swap program with one slot per allowed operation, so the direct brute-force search explores $O(|x|^K)$ configurations.#footnote[This is the exact search bound induced by the fixed-length witness encoding implemented in the codebase; no sharper exact worst-case bound is claimed here.] - *Example.* Let $Sigma = {#alpha-map.join(", ")}$, $x = #source-str$, and $K = #budget$. The configuration $p = (#config.map(str).join(", "))$ performs adjacent swaps at positions $(2, 3)$, $(1, 2)$, and $(3, 4)$, then uses two trailing no-op slots. The resulting string is $y = #step3-str$, so every symbol now appears in one contiguous block and the verifier returns YES. + *Example.* Let $Sigma = {#alpha-map.join(", ")}$, $x = #source-str$, and $K = #budget$. The configuration $p = (#fmt-values(config))$ performs adjacent swaps at positions $(2, 3)$, $(1, 2)$, and $(3, 4)$, then uses two trailing no-op slots. The resulting string is $y = #step3-str$, so every symbol now appears in one contiguous block and the verifier returns YES. #pred-commands( "pred create --example " + problem-spec(x) + " -o grouping-by-swapping.json", "pred solve grouping-by-swapping.json --solver brute-force", - "pred evaluate grouping-by-swapping.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate grouping-by-swapping.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -7522,7 +7466,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ), )) }, - caption: [Grouping by Swapping on $x = #source-str$: three effective adjacent swaps turn the alternating string into $y = #step3-str$. The remaining two slots in $p = (#config.map(str).join(", "))$ are no-ops at position 5.], + caption: [Grouping by Swapping on $x = #source-str$: three effective adjacent swaps turn the alternating string into $y = #step3-str$. The remaining two slots in $p = (#fmt-values(config))$ are no-ops at position 5.], ) The final row has exactly one block of $a$, one block of $b$, and one block of $c$, so it satisfies the grouping constraint within the allotted budget. @@ -7534,8 +7478,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let x = load-model-example("LongestCommonSubsequence") let strings = x.instance.strings let alphabet-size = x.instance.alphabet_size - // optimal_config includes padding symbols; extract the non-padding prefix - let witness = x.optimal_config.filter(c => c < alphabet-size) + let witness = x.optimal_config.filter(c => c != none) let fmt-str(s) = "\"" + s.map(c => str(c)).join("") + "\"" let string-list = strings.map(fmt-str).join(", ") let find-embed(target, candidate) = { @@ -7561,7 +7504,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example LongestCommonSubsequence -o longest-common-subsequence.json", "pred solve longest-common-subsequence.json", - "pred evaluate longest-common-subsequence.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate longest-common-subsequence.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -7629,7 +7572,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ClosestString -o closest-string.json", "pred solve closest-string.json --solver brute-force", - "pred evaluate closest-string.json --config " + center.map(str).join(","), + "pred evaluate closest-string.json --config " + cli-config(center), ) ] ] @@ -7674,7 +7617,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ClosestSubstring -o closest-substring.json", "pred solve closest-substring.json --solver brute-force", - "pred evaluate closest-substring.json --config " + config.map(str).join(","), + "pred evaluate closest-substring.json --config " + cli-config(config), ) ] ] @@ -7686,7 +7629,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let target = x.instance.target let n = sizes.len() let config = x.optimal_config - let selected = range(n).filter(i => config.at(i) == 1) + let selected = range(n).filter(i => config.at(i)) let sel-sizes = selected.map(i => sizes.at(i)) [ #problem-def("SubsetSum")[ @@ -7699,7 +7642,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SubsetSum -o subset-sum.json", "pred solve subset-sum.json", - "pred evaluate subset-sum.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate subset-sum.json --config " + cli-config(x.optimal_config), ) ] ] @@ -7711,7 +7654,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let target = x.instance.target let n = sizes.len() let config = x.optimal_config - let selected = range(n).filter(i => config.at(i) == 1) + let selected = range(n).filter(i => config.at(i)) let sel-sizes = selected.map(i => sizes.at(i)) [ #problem-def("SubsetProduct")[ @@ -7724,7 +7667,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SubsetProduct -o subset-product.json", "pred solve subset-product.json", - "pred evaluate subset-product.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate subset-product.json --config " + cli-config(x.optimal_config), ) ] ] @@ -7752,7 +7695,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ResourceConstrainedScheduling -o rcs.json", "pred solve rcs.json", - "pred evaluate rcs.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate rcs.json --config " + cli-config(x.optimal_config), ) ] ] @@ -7764,7 +7707,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let fds = x.instance.functional_deps let target = x.instance.target_subset let config = x.optimal_config - let X = range(num-attrs).filter(i => config.at(i) == 1) + let X = range(num-attrs).filter(i => config.at(i)) // Compute closure of X under fds let closure = { let cur = X @@ -7805,7 +7748,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example BoyceCoddNormalFormViolation -o bcnf.json", "pred solve bcnf.json", - "pred evaluate bcnf.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate bcnf.json --config " + cli-config(x.optimal_config), ) ] ] @@ -7866,7 +7809,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ConsistencyOfDatabaseFrequencyTables -o consistency-of-database-frequency-tables.json", "pred solve consistency-of-database-frequency-tables.json", - "pred evaluate consistency-of-database-frequency-tables.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate consistency-of-database-frequency-tables.json --config " + cli-config(x.optimal_config), ) ] ] @@ -7890,15 +7833,15 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Problem SP19 in Garey and Johnson @garey1979. NP-complete in the strong sense, so no pseudo-polynomial time algorithm exists unless $P = "NP"$. For fixed $K$, a dynamic-programming algorithm runs in $O(n S^(K-1))$ pseudo-polynomial time, where $S = sum s(a)$. The problem remains NP-complete when the exponent 2 is replaced by any fixed rational $alpha > 1$. #footnote[No algorithm improving on brute-force $O(K^n)$ enumeration is known for the general case.] The squared objective penalizes imbalanced partitions, connecting it to variance minimization, load balancing, and $k$-means clustering. Sum of Squares Partition generalizes Partition ($K = 2$, $J = S^2 slash 2$). - *Example.* Let $A = {#sizes.map(str).join(", ")}$ ($n = #n-elem$) and $K = #K$ groups. The optimal partition is #groups.enumerate().map(((g, idxs)) => { + *Example.* Let $A = {#fmt-values(sizes)}$ ($n = #n-elem$) and $K = #K$ groups. The optimal partition is #groups.enumerate().map(((g, idxs)) => { let elems = idxs.map(i => str(sizes.at(i))) [$A_#(g+1) = {#elems.join(", ")}$] - }).join(", ") with group sums #group-sums.map(str).join(", ") and sum of squares $#group-sq.map(str).join(" + ") = #opt-val$. + }).join(", ") with group sums #fmt-values(group-sums) and sum of squares $#group-sq.map(str).join(" + ") = #opt-val$. #pred-commands( "pred create --example SumOfSquaresPartition -o sosp.json", "pred solve sosp.json --solver brute-force", - "pred evaluate sosp.json --config " + config.map(str).join(","), + "pred evaluate sosp.json --config " + cli-config(config), ) ] ] @@ -7921,12 +7864,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ 3-Partition is Garey and Johnson's strongly NP-complete benchmark SP15 @garey1979. Unlike ordinary Partition, the strict size window forces every feasible block to contain exactly three elements, making the problem the canonical source for strong NP-completeness reductions to scheduling, packing, and layout models. The implementation in this repository uses one group-assignment variable per element, so the exported exact-search baseline is $O^*(3^n)$#footnote[This is the direct worst-case bound induced by the implementation's configuration space and matches the registered catalog expression `3^num_elements`; no sharper general exact bound was independently verified while preparing this entry.]. - *Example.* Let $B = #bound$ and consider the #(sizes.len())-element instance with sizes $(#sizes.map(str).join(", "))$. The witness triples #groups.enumerate().map(((i, g)) => [$A_#(i+1) = {#g.map(str).join(", ")}$]).join([ and ]) both sum to $#bound$, so this instance is satisfiable. + *Example.* Let $B = #bound$ and consider the #(sizes.len())-element instance with sizes $(#fmt-values(sizes))$. The witness triples #groups.enumerate().map(((i, g)) => [$A_#(i+1) = {#fmt-values(g)}$]).join([ and ]) both sum to $#bound$, so this instance is satisfiable. #pred-commands( "pred create --example ThreePartition -o three-partition.json", "pred solve three-partition.json", - "pred evaluate three-partition.json --config " + config.map(str).join(","), + "pred evaluate three-partition.json --config " + cli-config(config), ) #align(center, table( @@ -7934,7 +7877,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| align: center, table.header([Triple], [Elements], [Sum]), ..groups.enumerate().map(((i, g)) => ( - [$A_#(i+1)$], [$#(g.map(str).join(", "))$], [$#bound$], + [$A_#(i+1)$], [$#(fmt-values(g))$], [$#bound$], )).flatten(), )) ] @@ -7953,16 +7896,16 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let y-perm = config.slice(m) [ #problem-def("Numerical3DimensionalMatching")[ - Given disjoint sets $W$, $X$, $Y$ each with $m$ elements, positive integer sizes $s(a)$ with $B\/4 < s(a) < B\/2$ for every element, and a bound $B in ZZ^+$ such that $sum s(a) = m B$, determine whether $W union X union Y$ can be partitioned into $m$ triples, each containing one element from $W$, $X$, and $Y$, with each triple summing to exactly $B$. + Given disjoint sets $W$, $X$, $Y$ each with $m >= 1$ elements, positive integer sizes $s(a)$, and a bound $B in ZZ^+$ such that $sum s(a) = m B$, determine whether $W union X union Y$ can be partitioned into $m$ triples, each containing one element from $W$, $X$, and $Y$, with each triple summing to exactly $B$. ][ - Numerical 3-Dimensional Matching is strongly NP-complete (SP16 in Garey and Johnson @garey1979). The strict size window $B\/4 < s(a) < B\/2$ forces every feasible triple to contain exactly one element from each set. The problem is a key intermediate in strong NP-completeness reductions to bin packing, scheduling, and layout problems. Brute-force enumeration runs in $O^*(m^(2m))$ time. + Numerical 3-Dimensional Matching is strongly NP-complete (SP16 in Garey and Johnson @garey1979). This model requires the necessary total-sum equality as an input precondition; membership in $W$, $X$, and $Y$ determines the three elements of each triple. The problem is a key intermediate in strong NP-completeness reductions to bin packing, scheduling, and layout problems. Brute-force enumeration runs in $O^*(m^(2m))$ time. - *Example.* Let $m = #m$ and $B = #bound$. The sizes are $W = (#sw.map(str).join(", "))$, $X = (#sx.map(str).join(", "))$, $Y = (#sy.map(str).join(", "))$. The matching pairs each $w_i$ with $x_(pi(i))$ and $y_(sigma(i))$: #range(m).map(i => [$w_#i + x_#(x-perm.at(i)) + y_#(y-perm.at(i)) = #(sw.at(i) + sx.at(x-perm.at(i)) + sy.at(y-perm.at(i)))$]).join(", "), all equal to $B$. + *Example.* Let $m = #m$ and $B = #bound$. The sizes are $W = (#fmt-values(sw))$, $X = (#fmt-values(sx))$, $Y = (#fmt-values(sy))$. The matching pairs each $w_i$ with $x_(pi(i))$ and $y_(sigma(i))$: #range(m).map(i => [$w_#i + x_#(x-perm.at(i)) + y_#(y-perm.at(i)) = #(sw.at(i) + sx.at(x-perm.at(i)) + sy.at(y-perm.at(i)))$]).join(", "), all equal to $B$. #pred-commands( "pred create --example Numerical3DimensionalMatching -o n3dm.json", "pred solve n3dm.json", - "pred evaluate n3dm.json --config " + config.map(str).join(","), + "pred evaluate n3dm.json --config " + cli-config(config), ) ] ] @@ -7980,12 +7923,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Numerical Matching with Target Sums is NP-complete in the strong sense (SP17 in Garey and Johnson @garey1979). It generalizes bipartite perfect matching by imposing sum constraints on each pair. Brute-force enumeration runs in $O^*(2^m)$ time by trying all $m!$ permutations. - *Example.* Let $m = #m$, $X = (#sx.map(str).join(", "))$, $Y = (#sy.map(str).join(", "))$, targets $= (#targets.map(str).join(", "))$. The matching $pi = (#config.map(str).join(", "))$ yields sums #range(m).map(i => [$#(sx.at(i)) + #(sy.at(config.at(i))) = #(sx.at(i) + sy.at(config.at(i)))$]).join(", "), which as a multiset equals the targets. + *Example.* Let $m = #m$, $X = (#fmt-values(sx))$, $Y = (#fmt-values(sy))$, targets $= (#fmt-values(targets))$. The matching $pi = (#fmt-values(config))$ yields sums #range(m).map(i => [$#(sx.at(i)) + #(sy.at(config.at(i))) = #(sx.at(i) + sy.at(config.at(i)))$]).join(", "), which as a multiset equals the targets. #pred-commands( "pred create --example NumericalMatchingWithTargetSums -o nmts.json", "pred solve nmts.json", - "pred evaluate nmts.json --config " + config.map(str).join(","), + "pred evaluate nmts.json --config " + cli-config(config), ) ] ] @@ -8001,12 +7944,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Non-Liveness of free-choice Petri nets is NP-complete (Garey and Johnson @garey1979). A Petri net is _free-choice_ if every two transitions sharing an input place have identical presets. The implementation explores the bounded reachability graph (capped at the initial token sum per place) and checks whether any transition becomes permanently dead. - *Example.* A chain net with $#np$ places and $#nt$ transitions: $t_0$ moves a token from $s_0$ to $s_1$, $t_1$ from $s_1$ to $s_2$, $t_2$ from $s_2$ to $s_3$. Starting from $M_0 = (1, 0, 0, 0)$, after all transitions fire once the net reaches deadlock at $(0, 0, 0, 1)$ and all transitions are permanently dead. The witness configuration $(#config.map(str).join(", "))$ confirms all transitions are globally dead. + *Example.* A chain net with $#np$ places and $#nt$ transitions: $t_0$ moves a token from $s_0$ to $s_1$, $t_1$ from $s_1$ to $s_2$, $t_2$ from $s_2$ to $s_3$. Starting from $M_0 = (1, 0, 0, 0)$, after all transitions fire once the net reaches deadlock at $(0, 0, 0, 1)$ and all transitions are permanently dead. The witness configuration $(#fmt-values(config))$ confirms all transitions are globally dead. #pred-commands( "pred create --example NonLivenessFreePetriNet -o petri.json", "pred solve petri.json --solver brute-force", - "pred evaluate petri.json --config " + config.map(str).join(","), + "pred evaluate petri.json --config " + cli-config(config), ) ] ] @@ -8019,7 +7962,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let nt = ts.len() let imps = x.instance.implications let config = x.optimal_config - let selected = range(nt).filter(i => config.at(i) == 1) + let selected = range(nt).filter(i => config.at(i)) let sel-labels = selected.map(i => str(ts.at(i))) [ #problem-def("MinimumAxiomSet")[ @@ -8037,7 +7980,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumAxiomSet -o axiom.json", "pred solve axiom.json --solver brute-force", - "pred evaluate axiom.json --config " + config.map(str).join(","), + "pred evaluate axiom.json --config " + cli-config(config), ) ] ] @@ -8054,12 +7997,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Betweenness is problem MS1 in Garey and Johnson @garey1979. It arises in seriation, archaeological sequencing, and DNA physical mapping. The problem is NP-complete even when restricted to dense constraint sets. The implementation represents a solution as a permutation $f$ where $f(i)$ is the position assigned to element $i$. - *Example.* Consider $n = #n$ elements with triples #triples.map(t => [$(#t.at(0), #t.at(1), #t.at(2))$]).join(", "). The witness ordering $f = (#config.map(str).join(", "))$ (the identity permutation) satisfies all constraints: each middle element of every triple lies between the other two in the ordering. + *Example.* Consider $n = #n$ elements with triples #triples.map(t => [$(#t.at(0), #t.at(1), #t.at(2))$]).join(", "). The witness ordering $f = (#fmt-values(config))$ (the identity permutation) satisfies all constraints: each middle element of every triple lies between the other two in the ordering. #pred-commands( "pred create --example Betweenness -o betweenness.json", "pred solve betweenness.json", - "pred evaluate betweenness.json --config " + config.map(str).join(","), + "pred evaluate betweenness.json --config " + cli-config(config), ) ] ] @@ -8076,12 +8019,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Cyclic Ordering is problem MS2 in Garey and Johnson @garey1979. It is closely related to Betweenness (MS1) but enforces a cyclic rather than linear ordering constraint. The problem is NP-complete. The implementation represents a solution as a permutation $f$ where $f(i)$ is the position assigned to element $i$. - *Example.* Consider $n = #n$ elements with triples #triples.map(t => [$(#t.at(0), #t.at(1), #t.at(2))$]).join(", "). The witness ordering $f = (#config.map(str).join(", "))$ satisfies all constraints: each triple's elements appear in cyclic order under $f$. + *Example.* Consider $n = #n$ elements with triples #triples.map(t => [$(#t.at(0), #t.at(1), #t.at(2))$]).join(", "). The witness ordering $f = (#fmt-values(config))$ satisfies all constraints: each triple's elements appear in cyclic order under $f$. #pred-commands( "pred create --example CyclicOrdering -o cyclic_ordering.json", "pred solve cyclic_ordering.json", - "pred evaluate cyclic_ordering.json --config " + config.map(str).join(","), + "pred evaluate cyclic_ordering.json --config " + cli-config(config), ) ] ] @@ -8099,12 +8042,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Clustering is a fundamental problem in unsupervised learning and data analysis. The variant considered here is the diameter-bounded formulation, which is NP-complete. No algorithm improving on brute-force ($K^n$ enumeration) is known for the general case.#footnote[No algorithm improving on brute-force is known for general diameter-bounded clustering.] - *Example.* Consider $n = #n$ elements with $K = #K$ clusters and diameter bound $B = #B$. The distance matrix has two tight groups ${0,1,2}$ and ${3,4,5}$ with intra-group distance 1 and inter-group distance 3. The witness assignment $(#config.map(str).join(", "))$ partitions elements into clusters ${0,1,2}$ and ${3,4,5}$; each cluster has maximum pairwise distance $1 <= #B$. + *Example.* Consider $n = #n$ elements with $K = #K$ clusters and diameter bound $B = #B$. The distance matrix has two tight groups ${0,1,2}$ and ${3,4,5}$ with intra-group distance 1 and inter-group distance 3. The witness assignment $(#fmt-values(config))$ partitions elements into clusters ${0,1,2}$ and ${3,4,5}$; each cluster has maximum pairwise distance $1 <= #B$. #pred-commands( "pred create --example Clustering -o clustering.json", "pred solve clustering.json", - "pred evaluate clustering.json --config " + config.map(str).join(","), + "pred evaluate clustering.json --config " + cli-config(config), ) ] ] @@ -8122,12 +8065,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Dynamic Storage Allocation is Garey and Johnson's SR2 @garey1979 and models memory allocation for processes with known lifetimes. It generalises strip-packing and bin-packing with time constraints. The implementation encodes each item's starting address as a single variable with domain $D - s(a) + 1$. - *Example.* Let $D = #D$ and consider #n items with $(r, d, s)$ tuples #items.map(t => $(#t.at(0), #t.at(1), #t.at(2))$).join(", "). The witness assignment $sigma = (#config.map(str).join(", "))$ places every item within $[0, #(D - 1)]$ and ensures no two time-overlapping items share memory cells. + *Example.* Let $D = #D$ and consider #n items with $(r, d, s)$ tuples #items.map(t => $(#t.at(0), #t.at(1), #t.at(2))$).join(", "). The witness assignment $sigma = (#fmt-values(config))$ places every item within $[0, #(D - 1)]$ and ensures no two time-overlapping items share memory cells. #pred-commands( "pred create --example DynamicStorageAllocation -o dynamic-storage-allocation.json", "pred solve dynamic-storage-allocation.json", - "pred evaluate dynamic-storage-allocation.json --config " + config.map(str).join(","), + "pred evaluate dynamic-storage-allocation.json --config " + cli-config(config), ) #align(center, table( @@ -8147,7 +8090,6 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let sets = x.instance.sets let k = x.instance.k let bound = x.instance.bound - let config = x.optimal_config let m = sets.len() // Count qualifying tuples by enumerating the Cartesian product let total = sets.fold(1, (acc, s) => acc * s.len()) @@ -8157,12 +8099,11 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ The $K$th Largest $m$-Tuple problem is MP10 in Garey and Johnson's appendix @garey1979. It is _not known to be in NP_, because a "yes" certificate may need to exhibit $K$ qualifying tuples and $K$ can be exponentially large. The problem is PP-complete under polynomial-time Turing reductions @haase2016, though the special case $m = 2$, $K = 1$ is NP-complete via reduction from Subset Sum. In the general case, the only known exact approach is brute-force enumeration of all $product_(i=1)^m |X_i|$ tuples, so the registered catalog complexity is `total_tuples * num_sets`#footnote[No algorithm improving on brute-force is known for the general $K$th Largest $m$-Tuple problem.]. - *Example.* Let $m = #m$, $B = #bound$, and $K = #k$ with sets #sets.enumerate().map(((i, s)) => [$X_#(i+1) = {#s.map(str).join(", ")}$]).join([, ]). The Cartesian product has $#total$ tuples. For instance, the tuple $(#config.enumerate().map(((i, c)) => str(sets.at(i).at(c))).join(", "))$ has sum $#config.enumerate().map(((i, c)) => sets.at(i).at(c)).sum() >= #bound$, contributing 1 to the count. In total, #k of the #total tuples satisfy the bound, so the answer is _yes_ (count $= K$). + *Example.* Let $m = #m$, $B = #bound$, and $K = #k$ with sets #sets.enumerate().map(((i, s)) => [$X_#(i+1) = {#fmt-values(s)}$]).join([, ]). The Cartesian product has $#total$ tuples. Exactly #k tuples have sum at least #bound, so the answer is _yes_ (count $= K$). The evaluator enumerates the Cartesian product internally and stops once it has found $K$ qualifying tuples. #pred-commands( "pred create --example KthLargestMTuple -o kth-largest-m-tuple.json", "pred solve kth-largest-m-tuple.json --solver brute-force", - "pred evaluate kth-largest-m-tuple.json --config " + config.map(str).join(","), ) ] ] @@ -8175,13 +8116,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let release = x.instance.release_times let deadline = x.instance.deadlines let sol = (config: x.optimal_config, metric: x.optimal_value) - // Decode Lehmer code to permutation - let available = range(n) - let perm = () - for c in sol.config { - perm = perm + (available.at(c),) - available = available.slice(0, c) + available.slice(c + 1) - } + let perm = sol.config // Compute start times by simulating the schedule (build (task_idx, start) pairs) let current = 0 let schedule = () @@ -8210,7 +8145,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SequencingWithReleaseTimesAndDeadlines -o sequencing-with-release-times-and-deadlines.json", "pred solve sequencing-with-release-times-and-deadlines.json", - "pred evaluate sequencing-with-release-times-and-deadlines.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sequencing-with-release-times-and-deadlines.json --config " + cli-config(x.optimal_config), ) ] ] @@ -8222,8 +8157,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let n = sizes.len() let total = sizes.sum() let config = x.optimal_config - let sel = range(n).filter(i => config.at(i) == 1) - let unsel = range(n).filter(i => config.at(i) == 0) + let sel = range(n).filter(i => config.at(i)) + let unsel = range(n).filter(i => not config.at(i)) let sel-sum = sel.map(i => sizes.at(i)).sum(default: 0) let unsel-sum = unsel.map(i => sizes.at(i)).sum(default: 0) [ @@ -8232,12 +8167,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ One of Karp's 21 NP-complete problems @karp1972, listed as SP12 in Garey & Johnson @garey1979. Partition is the special case of Subset Sum where the target equals half the total sum. Though NP-complete, it is only _weakly_ NP-hard: a dynamic-programming algorithm runs in $O(n dot B_"total")$ pseudo-polynomial time, where $B_"total" = sum_i s(a_i)$. The best known exact algorithm is the $O^*(2^(n slash 2))$ meet-in-the-middle approach of Schroeppel and Shamir (1981). - *Example.* Let $A = {#sizes.map(s => str(s)).join(", ")}$ ($n = #n$, total sum $= #total$). Setting $A' = {#sel.map(i => str(sizes.at(i))).join(", ")}$ (indices #sel.map(str).join(", ")) gives sum $#sel.map(i => str(sizes.at(i))).join(" + ") = #sel-sum = #total slash 2$, and $A without A' = {#unsel.map(i => str(sizes.at(i))).join(", ")}$ also sums to $#unsel-sum$. Hence a balanced partition exists. + *Example.* Let $A = {#sizes.map(s => str(s)).join(", ")}$ ($n = #n$, total sum $= #total$). Setting $A' = {#sel.map(i => str(sizes.at(i))).join(", ")}$ (indices #fmt-values(sel)) gives sum $#sel.map(i => str(sizes.at(i))).join(" + ") = #sel-sum = #total slash 2$, and $A without A' = {#unsel.map(i => str(sizes.at(i))).join(", ")}$ also sums to $#unsel-sum$. Hence a balanced partition exists. #pred-commands( "pred create --example Partition -o partition.json", "pred solve partition.json", - "pred evaluate partition.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(x.optimal_config), ) ] ] @@ -8260,9 +8195,9 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Alphabet mapping: 0->a, 1->b, 2->c, ... let alpha-map = range(alpha-size).map(i => str.from-unicode(97 + i)) let fmt-str(s) = "\"" + s.map(c => alpha-map.at(c)).join("") + "\"" - // Optimal config includes padding; extract non-padding prefix + // The optional entries after the witness are padding. let sol = (config: x.optimal_config, metric: x.optimal_value) - let w-cfg = sol.config.filter(c => c < alpha-size) + let w-cfg = sol.config.filter(c => c != none) let w = w-cfg.map(c => alpha-map.at(c)) let w-str = fmt-str(w-cfg) let w-len = w.len() @@ -8295,7 +8230,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ShortestCommonSupersequence -o shortest-common-supersequence.json", "pred solve shortest-common-supersequence.json", - "pred evaluate shortest-common-supersequence.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate shortest-common-supersequence.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -8346,7 +8281,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let fmt-str(s) = "\"" + s.map(c => alpha-map.at(c)).join("") + "\"" // Optimal config includes padding; extract non-padding prefix let sol = (config: x.optimal_config, metric: x.optimal_value) - let w-cfg = sol.config.filter(c => c < alpha-size) + let w-cfg = sol.config.filter(c => c != none) let w = w-cfg.map(c => alpha-map.at(c)) let w-str = fmt-str(w-cfg) let w-len = w.len() @@ -8383,7 +8318,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ShortestCommonSuperstring -o shortest-common-superstring.json", "pred solve shortest-common-superstring.json", - "pred evaluate shortest-common-superstring.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate shortest-common-superstring.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -8455,7 +8390,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example StringToStringCorrection -o string-to-string-correction.json", "pred solve string-to-string-correction.json", - "pred evaluate string-to-string-correction.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate string-to-string-correction.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -8558,7 +8493,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumExternalMacroDataCompression -o min-emdc.json", "pred solve min-emdc.json", - "pred evaluate min-emdc.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate min-emdc.json --config " + cli-config(x.optimal_config), ) ] } @@ -8585,7 +8520,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumInternalMacroDataCompression -o min-imdc.json", "pred solve min-imdc.json", - "pred evaluate min-imdc.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate min-imdc.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -8632,7 +8567,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let gates = x.instance.gate_types let ws = x.instance.arc_weights let cfg = x.optimal_config - let sel-arcs = range(arcs.len()).filter(i => cfg.at(i) == 1) + let sel-arcs = range(arcs.len()).filter(i => cfg.at(i)) let total = sel-arcs.map(i => ws.at(i)).sum() [ #problem-def("MinimumWeightAndOrGraph")[ @@ -8648,7 +8583,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumWeightAndOrGraph -o mwaog.json", "pred solve mwaog.json --solver brute-force", - "pred evaluate mwaog.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mwaog.json --config " + cli-config(x.optimal_config), ) ] ] @@ -8663,7 +8598,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ni = inputs.len() let no = outputs.len() let cfg = x.optimal_config - let sel-pairs = range(cfg.len()).filter(i => cfg.at(i) == 1) + let sel-pairs = cfg.flatten().enumerate().filter(((i, selected)) => selected).map(((i, _)) => i) let count = sel-pairs.len() [ #problem-def("MinimumFaultDetectionTestSet")[ @@ -8671,12 +8606,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Fault detection test sets arise in hardware testing: each input-output path through a circuit's DAG representation exercises the internal components it traverses, while the boundary pins themselves are fixed sources and sinks. The problem therefore asks for the fewest test pairs whose induced paths cover all internal vertices. It generalises Set Cover over a structured family of subsets induced by DAG reachability.#footnote[No algorithm improving on brute-force enumeration of all $2^(|I| dot |O|)$ input-output pair subsets is known for the general case.] - *Example.* Consider $n = #n$ vertices with inputs $I = {#inputs.map(str).join(", ")}$ and outputs $O = {#outputs.map(str).join(", ")}$. The internal vertices are ${2, 3, 4}$. Arcs: #{arcs.map(a => $#(a.at(0)) arrow.r #(a.at(1))$).join(", ")}. Selecting pair $(#(inputs.at(0)), #(outputs.at(0)))$ covers internal vertices ${2, 3}$, and pair $(#(inputs.at(1)), #(outputs.at(1)))$ covers internal vertices ${3, 4}$. Their union is all internal vertices, giving an optimal count of $#count$. + *Example.* Consider $n = #n$ vertices with inputs $I = {#fmt-values(inputs)}$ and outputs $O = {#fmt-values(outputs)}$. The internal vertices are ${2, 3, 4}$. Arcs: #{arcs.map(a => $#(a.at(0)) arrow.r #(a.at(1))$).join(", ")}. Selecting pair $(#(inputs.at(0)), #(outputs.at(0)))$ covers internal vertices ${2, 3}$, and pair $(#(inputs.at(1)), #(outputs.at(1)))$ covers internal vertices ${3, 4}$. Their union is all internal vertices, giving an optimal count of $#count$. #pred-commands( "pred create --example MinimumFaultDetectionTestSet -o mfdts.json", "pred solve mfdts.json --solver brute-force", - "pred evaluate mfdts.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mfdts.json --config " + cli-config(x.optimal_config), ) ] ] @@ -8690,7 +8625,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let weights = x.instance.weights let config = x.optimal_config let opt-val = metric-value(x.optimal_value) - let removed = range(na).filter(i => config.at(i) == 1) + let removed = range(na).filter(i => config.at(i)) [ #problem-def("MinimumFeedbackArcSet")[ Given a directed graph $G = (V, A)$, find a minimum-size subset $A' subset.eq A$ such that $G - A'$ is a directed acyclic graph (DAG). Equivalently, $A'$ must contain at least one arc from every directed cycle in $G$. @@ -8702,7 +8637,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumFeedbackArcSet -o minimum-feedback-arc-set.json", "pred solve minimum-feedback-arc-set.json", - "pred evaluate minimum-feedback-arc-set.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-feedback-arc-set.json --config " + cli-config(x.optimal_config), ) ] ] @@ -8716,7 +8651,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let K = x.instance.budget let L = x.instance.max_cycle_length let config = x.optimal_config - let removed-indices = config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let removed-indices = config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) let removed-edges = removed-indices.map(i => edges.at(i)) let blue = graph-colors.at(0) let gray = luma(180) @@ -8733,7 +8668,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PartialFeedbackEdgeSet -o partial-feedback-edge-set.json", "pred solve partial-feedback-edge-set.json", - "pred evaluate partial-feedback-edge-set.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate partial-feedback-edge-set.json --config " + cli-config(x.optimal_config), ) #figure( @@ -8771,7 +8706,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let x = load-model-example("MultipleChoiceBranching") let nv = graph-num-vertices(x.instance) let arcs = x.instance.graph.arcs - let chosen = x.optimal_config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let chosen = x.optimal_config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) [ #problem-def("MultipleChoiceBranching")[ Given a directed graph $G = (V, A)$, arc weights $w: A -> ZZ^+$, a partition $A_1, A_2, dots, A_m$ of $A$, and a threshold $K in ZZ^+$, determine whether there exists a subset $A' subset.eq A$ with $sum_(a in A') w(a) >= K$ such that every vertex has in-degree at most one in $(V, A')$, the selected subgraph $(V, A')$ is acyclic, and $|A' inter A_i| <= 1$ for every partition group. @@ -8785,7 +8720,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MultipleChoiceBranching -o multiple-choice-branching.json", "pred solve multiple-choice-branching.json", - "pred evaluate multiple-choice-branching.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate multiple-choice-branching.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -8840,7 +8775,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example AcyclicPartition -o acyclic-partition.json", "pred solve acyclic-partition.json", - "pred evaluate acyclic-partition.json --config " + config.map(str).join(","), + "pred evaluate acyclic-partition.json --config " + cli-config(config), ) #figure({ @@ -8879,17 +8814,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let task-lengths = x.instance.task_lengths let n = task-lengths.len() let D = x.instance.deadline - let lehmer = x.optimal_config - // Decode Lehmer code to job permutation - let job-order = { - let avail = range(n) - let result = () - for c in lehmer { - result.push(avail.at(c)) - avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) - } - result - } + let job-order = x.optimal_config // Compute Gantt schedule greedily let machine-end = range(m).map(_ => 0) let job-end = range(n).map(_ => 0) @@ -8918,7 +8843,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example FlowShopScheduling -o flow-shop-scheduling.json", "pred solve flow-shop-scheduling.json", - "pred evaluate flow-shop-scheduling.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate flow-shop-scheduling.json --config " + cli-config(x.optimal_config), ) #figure( @@ -9081,7 +9006,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o job-shop-scheduling.json", "pred solve job-shop-scheduling.json --solver brute-force", - "pred evaluate job-shop-scheduling.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate job-shop-scheduling.json --config " + cli-config(x.optimal_config), ) #figure( @@ -9137,43 +9062,8 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let m = x.instance.num_machines let n = p.len() let cfg = x.optimal_config - // Decode per-machine orderings: cfg[i*n..(i+1)*n] is machine i's job order - let orders = range(m).map(i => cfg.slice(i * n, (i + 1) * n)) - - // Greedy simulation to compute start times - let machine-avail = range(m).map(_ => 0) - let job-avail = range(n).map(_ => 0) - let next-on = range(m).map(_ => 0) - let start-times = range(n).map(_ => range(m).map(_ => 0)) - let finish-times = range(n).map(_ => range(m).map(_ => 0)) - - let total-tasks = n * m - let scheduled = 0 - while scheduled < total-tasks { - // Find machine with earliest next start - let best-start = 999999 - let best-machine = -1 - for i in range(m) { - if next-on.at(i) < n { - let j = orders.at(i).at(next-on.at(i)) - let s = calc.max(machine-avail.at(i), job-avail.at(j)) - if s < best-start or (s == best-start and (best-machine == -1 or i < best-machine)) { - best-start = s - best-machine = i - } - } - } - let i = best-machine - let j = orders.at(i).at(next-on.at(i)) - let s = calc.max(machine-avail.at(i), job-avail.at(j)) - let f = s + p.at(j).at(i) - start-times.at(j).at(i) = s - finish-times.at(j).at(i) = f - machine-avail.at(i) = f - job-avail.at(j) = f - next-on.at(i) += 1 - scheduled += 1 - } + let start-times = range(n).map(j => cfg.slice(j * m, (j + 1) * m)) + let finish-times = range(n).map(j => range(m).map(i => start-times.at(j).at(i) + p.at(j).at(i))) let makespan = calc.max(..range(n).map(j => calc.max(..range(m).map(i => finish-times.at(j).at(i))))) @@ -9184,23 +9074,23 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| 2. *Job constraint:* Each job occupies at most one machine at a time. Unlike flow-shop or job-shop scheduling, there is no prescribed order for a job's tasks across machines. ][ - Open Shop Scheduling is problem SS14 in Garey and Johnson's catalog @garey1979 (decision version: does a schedule exist with makespan $<= D$?). NP-completeness for $m >= 3$ machines was established by Gonzalez and Sahni via reduction from Partition @gonzalez1976. The problem is solvable in polynomial time for $m = 2$ and also for the preemptive variant with any $m$ @gonzalez1976. This codebase evaluates a candidate schedule by simulating a greedy active schedule: for each step, the machine with the earliest feasible next-job start is processed next. The configuration encodes one permutation of jobs per machine (direct indices), giving $(n!)^m$ candidate orderings. + Open Shop Scheduling is problem SS14 in Garey and Johnson's catalog @garey1979 (decision version: does a schedule exist with makespan $<= D$?). NP-completeness for $m >= 3$ machines was established by Gonzalez and Sahni via reduction from Partition @gonzalez1976. The problem is solvable in polynomial time for $m = 2$ and also for the preemptive variant with any $m$ @gonzalez1976. A configuration directly records the nonnegative start time $sigma(j,i)$ of every operation in job-major order. This represents both machine-side and job-side ordering decisions without requiring a greedy reconstruction. *Example.* Let $m = #m$ machines and $n = #n$ jobs with processing times #align(center, math.equation([$P = #math.mat(..p.map(row => row.map(v => [#v])))$])) - The canonical optimal orderings are: + The canonical optimal start-time matrix is: #align(center, table( - columns: 2, - align: (left, left), - table.header([Machine], [Job order]), - ..range(m).map(i => ([M#(i+1)], orders.at(i).map(j => [$J_#(j+1)$]).join[$,$])).flatten() + columns: m + 1, + align: center, + table.header([Job], ..range(m).map(i => [M#(i+1)])), + ..range(n).map(j => ([J#(j+1)], ..start-times.at(j).map(s => [#s]))).flatten() )) giving the Gantt chart in @fig:openshop and makespan *#makespan*. #pred-commands( "pred create --example " + problem-spec(x) + " -o open-shop-scheduling.json", "pred solve open-shop-scheduling.json", - "pred evaluate open-shop-scheduling.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate open-shop-scheduling.json --config " + cli-config(x.optimal_config), ) #figure( @@ -9272,12 +9162,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ([$c_#(i+1)$],) + schedules.at(i).map(b => [#bit(b)]) + ([#config.at(i)],) }) + (([$overline(R)$],) + reqs.map(r => [*#r*]) + ([],),)).flatten(), )) - This uses $sum f = #total-workers <= #n-workers$ workers. The coverage $(#coverage.map(str).join(", "))$ meets $overline(R)$ component-wise, so the instance is feasible. + This uses $sum f = #total-workers <= #n-workers$ workers. The coverage $(#fmt-values(coverage))$ meets $overline(R)$ component-wise, so the instance is feasible. #pred-commands( "pred create --example StaffScheduling -o staff.json", "pred solve staff.json", - "pred evaluate staff.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate staff.json --config " + cli-config(x.optimal_config), ) #figure( @@ -9301,7 +9191,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #{ let x = load-model-example("TimetableDesign") - let assignments = x.optimal_config.enumerate().filter(((idx, value)) => value == 1).map(((idx, value)) => ( + let assignments = x.optimal_config.flatten().enumerate().filter(((idx, selected)) => selected).map(((idx, _)) => ( calc.floor(idx / (x.instance.num_tasks * x.instance.num_periods)), calc.floor(calc.rem(idx, x.instance.num_tasks * x.instance.num_periods) / x.instance.num_periods), calc.rem(idx, x.instance.num_periods), @@ -9373,7 +9263,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MultiprocessorScheduling -o multiprocessor-scheduling.json", "pred solve multiprocessor-scheduling.json", - "pred evaluate multiprocessor-scheduling.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate multiprocessor-scheduling.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -9448,12 +9338,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Production Planning is the lot-sizing feasibility problem SS21 in Garey & Johnson @garey1979. Florian, Lenstra, and Rinnooy Kan show that the general problem is NP-complete even under strong restrictions, while also giving pseudo-polynomial dynamic-programming algorithms for capacitated variants @florianLenstraRinnooyKan1980. The implementation in this repository uses one bounded integer variable per period, so the registered exact baseline explores the direct witness space $product_i (c_i + 1)$; under the uniform-capacity bound $C = max_i c_i$, this becomes $O^*((C + 1)^n)$#footnote[This is the search bound induced by the configuration space exposed by the implementation, not a literature-best exact algorithm claim.]. - *Example.* Consider the canonical instance with #n periods, demands $(#demands.map(str).join(", "))$, capacities $(#capacities.map(str).join(", "))$, setup costs $(#setup-costs.map(str).join(", "))$, production costs $(#production-costs.map(str).join(", "))$, inventory costs $(#inventory-costs.map(str).join(", "))$, and budget $B = #bound$. The satisfying production plan $x = (#plan.map(str).join(", "))$ yields prefix inventories $(#inventory.map(str).join(", "))$. The verifier therefore accepts, and its cost breakdown is $#production-total + #inventory-total + #setup-total = #(production-total + inventory-total + setup-total) <= #bound$. + *Example.* Consider the canonical instance with #n periods, demands $(#fmt-values(demands))$, capacities $(#fmt-values(capacities))$, setup costs $(#fmt-values(setup-costs))$, production costs $(#fmt-values(production-costs))$, inventory costs $(#fmt-values(inventory-costs))$, and budget $B = #bound$. The satisfying production plan $x = (#fmt-values(plan))$ yields prefix inventories $(#fmt-values(inventory))$. The verifier therefore accepts, and its cost breakdown is $#production-total + #inventory-total + #setup-total = #(production-total + inventory-total + setup-total) <= #bound$. #pred-commands( "pred create --example " + problem-spec(x) + " -o production-planning.json", "pred solve production-planning.json --solver brute-force", - "pred evaluate production-planning.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate production-planning.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -9490,7 +9380,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o capacity-assignment.json", "pred solve capacity-assignment.json --solver brute-force", - "pred evaluate capacity-assignment.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate capacity-assignment.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -9532,7 +9422,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example PrecedenceConstrainedScheduling -o precedence-constrained-scheduling.json", "pred solve precedence-constrained-scheduling.json", - "pred evaluate precedence-constrained-scheduling.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate precedence-constrained-scheduling.json --config " + cli-config(x.optimal_config), ) ] ] @@ -9565,7 +9455,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SchedulingWithIndividualDeadlines -o scheduling-with-individual-deadlines.json", "pred solve scheduling-with-individual-deadlines.json", - "pred evaluate scheduling-with-individual-deadlines.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate scheduling-with-individual-deadlines.json --config " + cli-config(x.optimal_config), ) #figure( @@ -9627,9 +9517,9 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let precs = x.instance.precedences let d_max = lengths.fold(0, (acc, l) => acc + l) let cfg = x.optimal_config - // For each task t, collect active time slots from the flat binary config + // For each task, collect its active time slots. let active-slots = range(n).map(t => - range(d_max).filter(u => cfg.at(t * d_max + u) == 1) + range(d_max).filter(u => cfg.at(t).at(u)) ) let makespan = x.optimal_value [ @@ -9640,14 +9530,14 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| The makespan is $max_{t in T} (max S(t) + 1)$. ][ - Preemptive Scheduling is problem A5 SS6 in Garey & Johnson @garey1979. NP-complete in general; the special case without precedences ($m$ arbitrary) is solvable in polynomial time (McNaughton's wrap-around algorithm), and the preemptive open-shop variant is also polynomial. The configuration representation is a binary vector of length $n dot D_"max"$ encoding per-slot assignments. + Preemptive Scheduling is problem A5 SS6 in Garey & Johnson @garey1979. NP-complete in general; the special case without precedences ($m$ arbitrary) is solvable in polynomial time (McNaughton's wrap-around algorithm), and the preemptive open-shop variant is also polynomial. The configuration is an $n times D_"max"$ Boolean matrix encoding per-slot assignments. - *Example.* Let $n = #n$ tasks with lengths $(#lengths.map(str).join(", "))$, $m = #m$ processors, and precedences #{precs.map(p => $t_#(p.at(0)) prec t_#(p.at(1))$).join(", ")}. Optimal makespan: $#makespan$. Schedule: #range(n).map(t => [$t_#t$ at slots $[#active-slots.at(t).map(str).join(", ")]$]).join("; "). + *Example.* Let $n = #n$ tasks with lengths $(#fmt-values(lengths))$, $m = #m$ processors, and precedences #{precs.map(p => $t_#(p.at(0)) prec t_#(p.at(1))$).join(", ")}. Optimal makespan: $#makespan$. Schedule: #range(n).map(t => [$t_#t$ at slots $[#active-slots.at(t).map(str).join(", ")]$]).join("; "). #pred-commands( "pred create --example PreemptiveScheduling -o preemptive-scheduling.json", "pred solve preemptive-scheduling.json", - "pred evaluate preemptive-scheduling.json --config " + cfg.map(str).join(","), + "pred evaluate preemptive-scheduling.json --config " + cli-config(cfg), ) ] ] @@ -9669,7 +9559,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Scheduling to Minimize Weighted Completion Time is problem A5 SS13 in Garey & Johnson @garey1979. NP-complete for $m = 2$ by reduction from Partition @lenstra1977, and NP-complete in the strong sense for arbitrary $m$. For a fixed assignment of tasks to processors, Smith's rule gives the optimal ordering on each processor, reducing the search space to $m^n$ processor assignments @smith1956. The problem is solvable in polynomial time when all lengths are equal or when all weights are equal @conway1967 @horn1973. - *Example.* Let $T = {t_1, dots, t_#ntasks}$ with lengths $(#lengths.map(str).join(", "))$, weights $(#weights.map(str).join(", "))$, and $m = #m$ processors. The optimal assignment $(#sigma.map(v => str(v + 1)).join(", "))$ achieves total weighted completion time #x.optimal_value: + *Example.* Let $T = {t_1, dots, t_#ntasks}$ with lengths $(#fmt-values(lengths))$, weights $(#fmt-values(weights))$, and $m = #m$ processors. The optimal assignment $(#sigma.map(v => str(v + 1)).join(", "))$ achieves total weighted completion time #x.optimal_value: #for p in range(m) [ - Processor #(p + 1): ${#tasks-by-proc.at(p).map(i => $t_#(i + 1)$).join(", ")}$#if tasks-by-proc.at(p).len() > 0 { let proc-tasks = tasks-by-proc.at(p) @@ -9686,7 +9576,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o scheduling-wct.json", "pred solve scheduling-wct.json --solver brute-force", - "pred evaluate scheduling-wct.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate scheduling-wct.json --config " + cli-config(x.optimal_config), ) #figure({ @@ -9744,7 +9634,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let max-t = calc.max(..range(ntasks).map(i => deadline.at(i))) [ #problem-def("SequencingWithinIntervals")[ - Given a finite set $T$ of tasks and, for each $t in T$, a release time $r(t) >= 0$, a deadline $d(t) >= 0$, and a processing length $ell(t) in ZZ^+$ satisfying $r(t) + ell(t) <= d(t)$, determine whether there exists a feasible schedule $sigma: T -> ZZ_(>= 0)$ such that for each $t in T$: (1) $sigma(t) >= r(t)$, (2) $sigma(t) + ell(t) <= d(t)$, and (3) for all $t' in T backslash {t}$, either $sigma(t') + ell(t') <= sigma(t)$ or $sigma(t') >= sigma(t) + ell(t)$. + Given a finite set $T$ of tasks and, for each $t in T$, a release time $r(t) >= 0$, a deadline $d(t) >= 0$, and a processing length $ell(t) in ZZ^+$, determine whether there exists a feasible schedule $sigma: T -> ZZ_(>= 0)$ such that for each $t in T$: (1) $sigma(t) >= r(t)$, (2) $sigma(t) + ell(t) <= d(t)$, and (3) for all $t' in T backslash {t}$, either $sigma(t') + ell(t') <= sigma(t)$ or $sigma(t') >= sigma(t) + ell(t)$. ][ Sequencing Within Intervals is problem SS1 in Garey & Johnson @garey1979, proved NP-complete via reduction from Partition (Theorem 3.8). Each task $t$ must execute non-preemptively during the interval $[r(t), d(t))$, occupying $ell(t)$ consecutive time units on a single machine, and no two tasks may overlap. @@ -9761,7 +9651,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SequencingWithinIntervals -o sequencing-within-intervals.json", "pred solve sequencing-within-intervals.json", - "pred evaluate sequencing-within-intervals.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sequencing-within-intervals.json --config " + cli-config(x.optimal_config), ) Each task can only start within its window $[r(t), d(t) - ell(t)]$, and the windows overlap, so finding a non-overlapping assignment is non-trivial. One feasible schedule places the tasks at #range(ntasks).map(i => $[#starts.at(i), #(starts.at(i) + lengths.at(i)))$).join($,$): @@ -9821,17 +9711,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let precs = x.instance.precedences let sol = (config: x.optimal_config, metric: x.optimal_value) let tardy-count = metric-value(sol.metric) - // Decode Lehmer code to permutation (schedule order) - let lehmer = sol.config - let schedule = { - let avail = range(ntasks) - let result = () - for c in lehmer { - result.push(avail.at(c)) - avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) - } - result - } + let schedule = sol.config // Compute inverse: task-pos[task] = position let task-pos = range(ntasks).map(task => { schedule.enumerate().filter(((p, t)) => t == task).at(0).at(0) @@ -9851,7 +9731,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example MinimumTardinessSequencing -o minimum-tardiness-sequencing.json", "pred solve minimum-tardiness-sequencing.json", - "pred evaluate minimum-tardiness-sequencing.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate minimum-tardiness-sequencing.json --config " + cli-config(x.optimal_config), ) #figure( @@ -9904,16 +9784,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ntasks = lengths.len() let sol = (config: x.optimal_config, metric: x.optimal_value) let opt = metric-value(sol.metric) - let lehmer = sol.config - let schedule = { - let avail = range(ntasks) - let result = () - for c in lehmer { - result.push(avail.at(c)) - avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) - } - result - } + let schedule = sol.config let starts = () let finishes = () let elapsed = 0 @@ -9934,7 +9805,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SequencingToMinimizeWeightedCompletionTime -o sequencing-to-minimize-weighted-completion-time.json", "pred solve sequencing-to-minimize-weighted-completion-time.json", - "pred evaluate sequencing-to-minimize-weighted-completion-time.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sequencing-to-minimize-weighted-completion-time.json --config " + cli-config(x.optimal_config), ) #figure( @@ -9975,16 +9846,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let deadlines = x.instance.deadlines let bound = x.instance.bound let njobs = lengths.len() - let lehmer = x.optimal_config - let schedule = { - let avail = range(njobs) - let result = () - for c in lehmer { - result.push(avail.at(c)) - avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) - } - result - } + let schedule = x.optimal_config let completions = { let t = 0 let result = () @@ -10013,7 +9875,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SequencingToMinimizeWeightedTardiness -o sequencing-to-minimize-weighted-tardiness.json", "pred solve sequencing-to-minimize-weighted-tardiness.json", - "pred evaluate sequencing-to-minimize-weighted-tardiness.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sequencing-to-minimize-weighted-tardiness.json --config " + cli-config(x.optimal_config), ) #figure( @@ -10059,16 +9921,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let costs = x.instance.costs let precs = x.instance.precedences let ntasks = costs.len() - let lehmer = x.optimal_config - let schedule = { - let avail = range(ntasks) - let result = () - for c in lehmer { - result.push(avail.at(c)) - avail = avail.enumerate().filter(((i, v)) => i != c).map(((i, v)) => v) - } - result - } + let schedule = x.optimal_config let prefix-sums = { let running = 0 let result = () @@ -10092,7 +9945,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SequencingToMinimizeMaximumCumulativeCost -o sequencing-to-minimize-maximum-cumulative-cost.json", "pred solve sequencing-to-minimize-maximum-cumulative-cost.json", - "pred evaluate sequencing-to-minimize-maximum-cumulative-cost.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sequencing-to-minimize-maximum-cumulative-cost.json --config " + cli-config(x.optimal_config), ) #figure( @@ -10166,7 +10019,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SequencingToMinimizeTardyTaskWeight -o sequencing-to-minimize-tardy-task-weight.json", "pred solve sequencing-to-minimize-tardy-task-weight.json", - "pred evaluate sequencing-to-minimize-tardy-task-weight.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sequencing-to-minimize-tardy-task-weight.json --config " + cli-config(x.optimal_config), ) ] ] @@ -10207,7 +10060,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example SequencingWithDeadlinesAndSetUpTimes -o sequencing-with-deadlines-and-set-up-times.json", "pred solve sequencing-with-deadlines-and-set-up-times.json", - "pred evaluate sequencing-with-deadlines-and-set-up-times.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sequencing-with-deadlines-and-set-up-times.json --config " + cli-config(x.optimal_config), ) ] ] @@ -10233,7 +10086,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o integral-flow-homologous-arcs.json", "pred solve integral-flow-homologous-arcs.json --solver brute-force", - "pred evaluate integral-flow-homologous-arcs.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate integral-flow-homologous-arcs.json --config " + cli-config(x.optimal_config), ) #figure( @@ -10345,12 +10198,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| NP-completeness was proved by Even, Itai, and Shamir via reduction from 3-SAT @even1976. The problem remains NP-complete even when all arc capacities are 1 and $R_1 = 1$. No sub-exponential exact algorithm is known; brute-force enumeration over $(C + 1)^(2|A|)$ flow assignments dominates, where $C = max_(a in A) c(a)$.#footnote[No algorithm improving on brute-force is known for Directed Two-Commodity Integral Flow.] - *Example.* Consider a directed graph with $n = #nv$ vertices and $|A| = #m$ arcs with capacities $(#caps.map(str).join(", "))$, sources $s_1 = v_#s1$, $s_2 = v_#s2$, sinks $t_1 = v_#t1$, $t_2 = v_#t2$, and requirements $R_1 = #R1$, $R_2 = #R2$. Commodity 1 routes along #c1-arcs.map(i => $(v_#(arcs-j.at(i).at(0)), v_#(arcs-j.at(i).at(1)))$).join(", ") and commodity 2 along #c2-arcs.map(i => $(v_#(arcs-j.at(i).at(0)), v_#(arcs-j.at(i).at(1)))$).join(", "), satisfying all capacity and conservation constraints. + *Example.* Consider a directed graph with $n = #nv$ vertices and $|A| = #m$ arcs with capacities $(#fmt-values(caps))$, sources $s_1 = v_#s1$, $s_2 = v_#s2$, sinks $t_1 = v_#t1$, $t_2 = v_#t2$, and requirements $R_1 = #R1$, $R_2 = #R2$. Commodity 1 routes along #c1-arcs.map(i => $(v_#(arcs-j.at(i).at(0)), v_#(arcs-j.at(i).at(1)))$).join(", ") and commodity 2 along #c2-arcs.map(i => $(v_#(arcs-j.at(i).at(0)), v_#(arcs-j.at(i).at(1)))$).join(", "), satisfying all capacity and conservation constraints. #pred-commands( "pred create --example DirectedTwoCommodityIntegralFlow -o d2cif.json", "pred solve d2cif.json", - "pred evaluate d2cif.json --config " + config.map(str).join(","), + "pred evaluate d2cif.json --config " + cli-config(config), ) #figure( @@ -10414,12 +10267,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| The brute-force bound $(C + 1)^(|A|)$ arises from enumerating all possible integral flow vectors, where $C = max_(a in A) c(a)$.#footnote[No sub-exponential exact algorithm is known for Minimum Edge-Cost Flow.] - *Example.* Consider a directed graph with $n = #nv$ vertices, source $s = v_#src$, sink $t = v_#snk$, requirement $R = #R$, and $|A| = #{arcs-j.len()}$ arcs with capacities $(#caps.map(str).join(", "))$ and prices $(#prices-j.map(str).join(", "))$. The optimal flow $f = (#flow.map(str).join(", "))$ activates the arcs #opt-arc-idx.map(i => $(v_#(arcs-j.at(i).at(0)), v_#(arcs-j.at(i).at(1)))$).join(", ") for a total edge cost of $#opt-arc-idx.map(i => str(prices-j.at(i))).join(" + ") = #cost$. + *Example.* Consider a directed graph with $n = #nv$ vertices, source $s = v_#src$, sink $t = v_#snk$, requirement $R = #R$, and $|A| = #{arcs-j.len()}$ arcs with capacities $(#fmt-values(caps))$ and prices $(#fmt-values(prices-j))$. The optimal flow $f = (#fmt-values(flow))$ activates the arcs #opt-arc-idx.map(i => $(v_#(arcs-j.at(i).at(0)), v_#(arcs-j.at(i).at(1)))$).join(", ") for a total edge cost of $#opt-arc-idx.map(i => str(prices-j.at(i))).join(" + ") = #cost$. #pred-commands( "pred create --example MinimumEdgeCostFlow -o mecf.json", "pred solve mecf.json", - "pred evaluate mecf.json --config " + flow.map(str).join(","), + "pred evaluate mecf.json --config " + cli-config(flow), ) #figure( @@ -10501,12 +10354,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| The registered polynomial bound $(|V| + |A|)^6$ is a conservative placeholder honoring the polynomial-time solvability via the linear-programming formulation; sharper strongly-polynomial bounds are available for specific algorithms (e.g., Orlin's enhanced capacity-scaling minimum-cost flow algorithm). - *Example.* On the canonical instance ($n = 4$ vertices, source $s = v_#src$, sink $t = v_#snk$, $|A| = #{arcs-j.len()}$ arcs with capacities $(#caps.map(str).join(", "))$ and costs $(#costs-j.map(str).join(", "))$), the source out-capacity bounds the flow value at $|f| <= 2 + 1 = 3$, and this is achievable: routing 2 units along $v_0 -> v_1$ and 1 unit along $v_0 -> v_2$, balanced by 1 unit through the lateral $v_1 -> v_2$ and 1 unit on $v_1 -> v_3$, gives $f = (#flow.map(str).join(", "))$ with $|f| = 3$ and total cost $2 dot 1 + 1 dot 0 + 1 dot 0 + 1 dot 1 + 2 dot 2 = 7$. The scalar score is $M dot (B - 3) + 7 = 8 dot 4 + 7 = 39$ where $B = 7$ and $M = 8$. + *Example.* On the canonical instance ($n = 4$ vertices, source $s = v_#src$, sink $t = v_#snk$, $|A| = #{arcs-j.len()}$ arcs with capacities $(#fmt-values(caps))$ and costs $(#fmt-values(costs-j))$), the source out-capacity bounds the flow value at $|f| <= 2 + 1 = 3$, and this is achievable: routing 2 units along $v_0 -> v_1$ and 1 unit along $v_0 -> v_2$, balanced by 1 unit through the lateral $v_1 -> v_2$ and 1 unit on $v_1 -> v_3$, gives $f = (#fmt-values(flow))$ with $|f| = 3$ and total cost $2 dot 1 + 1 dot 0 + 1 dot 0 + 1 dot 1 + 2 dot 2 = 7$. The scalar score is $M dot (B - 3) + 7 = 8 dot 4 + 7 = 39$ where $B = 7$ and $M = 8$. #pred-commands( "pred create --example MinimumCostMaximumFlow -o mcmf.json", "pred solve mcmf.json", - "pred evaluate mcmf.json --config " + flow.map(str).join(","), + "pred evaluate mcmf.json --config " + cli-config(flow), ) ] ] @@ -10528,12 +10381,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| The registered polynomial bound $(|V| + |A|)^6$ is a conservative placeholder honoring polynomial-time solvability via the linear-programming formulation; sharper strongly-polynomial bounds are available for specific min-cost-flow algorithms. - *Example.* On the canonical instance ($n = #{arcs-j.fold(0, (acc, a) => calc.max(acc, a.at(0), a.at(1))) + 1}$ vertices, $|A| = #{arcs-j.len()}$ arcs with capacities $(#caps.map(str).join(", "))$ and signed costs $(#costs-j.map(str).join(", "))$) there are two competing cycles through vertex $v_0$: cycle $A = (v_0 -> v_1 -> v_0)$ has per-unit cost $2 + (-3) = -1$ and capacity $2$, while cycle $B = (v_0 -> v_2 -> v_0)$ has per-unit cost $1 + (-4) = -3$ and capacity $1$. Cycle $B$ is cheaper per unit but smaller; cycle $A$ is more expensive per unit but larger. Both reduce cost, so the optimum pushes each cycle to capacity, giving $g = (#circ.map(str).join(", "))$ with total cost $2 dot 2 + 2 dot (-3) + 1 dot 1 + 1 dot (-4) = -5$. By comparison, running only cycle $A$ gives cost $-2$, only cycle $B$ gives $-3$, and the zero circulation gives $0$, so the optimum $-5$ strictly beats every alternative. + *Example.* On the canonical instance ($n = #{arcs-j.fold(0, (acc, a) => calc.max(acc, a.at(0), a.at(1))) + 1}$ vertices, $|A| = #{arcs-j.len()}$ arcs with capacities $(#fmt-values(caps))$ and signed costs $(#fmt-values(costs-j))$) there are two competing cycles through vertex $v_0$: cycle $A = (v_0 -> v_1 -> v_0)$ has per-unit cost $2 + (-3) = -1$ and capacity $2$, while cycle $B = (v_0 -> v_2 -> v_0)$ has per-unit cost $1 + (-4) = -3$ and capacity $1$. Cycle $B$ is cheaper per unit but smaller; cycle $A$ is more expensive per unit but larger. Both reduce cost, so the optimum pushes each cycle to capacity, giving $g = (#fmt-values(circ))$ with total cost $2 dot 2 + 2 dot (-3) + 1 dot 1 + 1 dot (-4) = -5$. By comparison, running only cycle $A$ gives cost $-2$, only cycle $B$ gives $-3$, and the zero circulation gives $0$, so the optimum $-5$ strictly beats every alternative. #pred-commands( "pred create --example MinimumCostCirculation -o mcc.json", "pred solve mcc.json", - "pred evaluate mcc.json --config " + circ.map(str).join(","), + "pred evaluate mcc.json --config " + cli-config(circ), ) ] ] @@ -10556,7 +10409,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example IntegralFlowBundles -o integral-flow-bundles.json", "pred solve integral-flow-bundles.json", - "pred evaluate integral-flow-bundles.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate integral-flow-bundles.json --config " + cli-config(x.optimal_config), ) #figure( @@ -10637,7 +10490,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example IntegralFlowWithMultipliers -o integral-flow-with-multipliers.json", "pred solve integral-flow-with-multipliers.json --solver brute-force", - "pred evaluate integral-flow-with-multipliers.json --config " + config.map(str).join(","), + "pred evaluate integral-flow-with-multipliers.json --config " + cli-config(config), ) #figure( @@ -10705,7 +10558,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let relation = x.instance.relation_attrs let known = x.instance.known_keys let config = x.optimal_config - let Rprime = range(num-attrs).filter(i => config.at(i) == 1) + let Rprime = range(num-attrs).filter(i => config.at(i)) let fmt-set(s) = ${#s.map(i => $#i$).join($,$)}$ let fmt-fd(fd) = { let lhs = fmt-set(fd.at(0)) @@ -10731,7 +10584,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example AdditionalKey -o additional-key.json", "pred solve additional-key.json", - "pred evaluate additional-key.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate additional-key.json --config " + cli-config(x.optimal_config), ) ] ] @@ -10799,7 +10652,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ConjunctiveBooleanQuery -o conjunctive-boolean-query.json", "pred solve conjunctive-boolean-query.json", - "pred evaluate conjunctive-boolean-query.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate conjunctive-boolean-query.json --config " + cli-config(x.optimal_config), ) ] ] @@ -10843,7 +10696,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ConsecutiveOnesMatrixAugmentation -o consecutive-ones-matrix-augmentation.json", "pred solve consecutive-ones-matrix-augmentation.json --solver brute-force", - "pred evaluate consecutive-ones-matrix-augmentation.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate consecutive-ones-matrix-augmentation.json --config " + cli-config(x.optimal_config), ) #figure( @@ -10865,7 +10718,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Use the canonical witness {0, 1, 3} let cfg = x.optimal_config // Selected column indices - let selected = cfg.enumerate().filter(((i, v)) => v == 1).map(((i, v)) => i) + let selected = cfg.enumerate().filter(((i, v)) => v).map(((i, v)) => i) [ #problem-def("ConsecutiveOnesSubmatrix")[ Given an $m times n$ binary matrix $A$ and an integer $K$ with $0 <= K <= n$, determine whether there exists a subset of $K$ columns of $A$ whose columns can be permuted so that in each row all 1's occur consecutively (the _consecutive ones property_). @@ -10877,7 +10730,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example ConsecutiveOnesSubmatrix -o consecutive-ones-submatrix.json", "pred solve consecutive-ones-submatrix.json", - "pred evaluate consecutive-ones-submatrix.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate consecutive-ones-submatrix.json --config " + cli-config(x.optimal_config), ) #figure( @@ -10889,7 +10742,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| for i in range(m) { for j in range(n) { let val = A-int.at(i).at(j) - let is-selected = cfg.at(j) == 1 + let is-selected = cfg.at(j) let f = if val == 1 { if is-selected { graph-colors.at(0).transparentize(30%) } else { luma(200) } } else { white } @@ -10948,12 +10801,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Sparse Matrix Compression appears as problem SR13 in Garey and Johnson @garey1979. It models row-overlay compression for sparse lookup tables: rows may share storage positions only when their shifted 1-entries never demand different row labels from the same slot. The implementation in this crate searches over row shifts only, then reconstructs the implied storage vector internally. This yields the direct exact bound $O(K^m dot m dot n)$ for $m$ rows and $n$ columns.#footnote[The storage vector is not enumerated as part of the configuration space. Once the shifts are fixed, every occupied slot is forced by the 1-entries of the shifted rows.] - *Example.* Let $A = #math.mat(..A-int.map(row => row.map(v => $#v$)))$ and $K = #K$. The stored config $(#cfg.map(str).join(", "))$ encodes the one-based shifts $s = (#shifts.map(str).join(", "))$. These shifts place the four row supports at positions $\{2, 5\}$, $\{3\}$, $\{4\}$, and $\{1\}$ respectively, so the supports are pairwise disjoint. The implied overlay vector is therefore $b = (#storage.map(str).join(", "))$, and this is the unique satisfying shift assignment among the $2^4 = 16$ configs in the canonical fixture. + *Example.* Let $A = #math.mat(..A-int.map(row => row.map(v => $#v$)))$ and $K = #K$. The stored config $(#fmt-values(cfg))$ encodes the one-based shifts $s = (#fmt-values(shifts))$. These shifts place the four row supports at positions $\{2, 5\}$, $\{3\}$, $\{4\}$, and $\{1\}$ respectively, so the supports are pairwise disjoint. The implied overlay vector is therefore $b = (#fmt-values(storage))$, and this is the unique satisfying shift assignment among the $2^4 = 16$ configs in the canonical fixture. #pred-commands( "pred create --example " + problem-spec(x) + " -o sparse-matrix-compression.json", "pred solve sparse-matrix-compression.json", - "pred evaluate sparse-matrix-compression.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate sparse-matrix-compression.json --config " + cli-config(x.optimal_config), ) #figure( @@ -11032,19 +10885,19 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let A = x.instance.matrix let n = A.len() let cfg = x.optimal_config - let signs = cfg.map(v => if v == 0 { $-1$ } else { $+1$ }) + let signs = cfg.map(v => if not v { $-1$ } else { $+1$ }) [ #problem-def("MinimumMatrixCover")[ Given an $n times n$ nonnegative integer matrix $A$, find a function $f: \{1, dots, n\} -> \{-1, +1\}$ minimizing $sum_(i,j) a_(i j) dot f(i) dot f(j)$. ][ Minimum Matrix Cover asks for a sign assignment to rows (equivalently columns) of a square matrix that minimizes the resulting quadratic form. Each binary variable $x_i in \{0, 1\}$ encodes a sign $f(i) = 2 x_i - 1$. Since $f(i)^2 = 1$, diagonal entries contribute a constant $sum_i a_(i i)$; the optimization depends only on off-diagonal structure. The brute-force complexity is $O(2^n)$ where $n$ is the matrix dimension.#footnote[No algorithm improving on brute-force enumeration of all $2^n$ sign assignments is known for the general case.] - *Example.* Let $A$ be the #(n)$times$#(n) symmetric matrix with zero diagonal shown below. The optimal config $(#cfg.map(str).join(", "))$ assigns signs $(#signs.join(", "))$, yielding value $= #x.optimal_value$. + *Example.* Let $A$ be the #(n)$times$#(n) symmetric matrix with zero diagonal shown below. The optimal config $(#fmt-values(cfg))$ assigns signs $(#signs.join(", "))$, yielding value $= #x.optimal_value$. #pred-commands( "pred create --example " + problem-spec(x) + " -o mmc.json", "pred solve mmc.json", - "pred evaluate mmc.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mmc.json --config " + cli-config(x.optimal_config), ) ] ] @@ -11057,19 +10910,19 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let n = if m > 0 { M.at(0).len() } else { 0 } let ones = x.instance.ones let cfg = x.optimal_config - let selected-ones = ones.enumerate().filter(((k, _)) => cfg.at(k) == 1).map(((_, pos)) => pos) + let selected-ones = ones.enumerate().filter(((k, _)) => cfg.at(k)).map(((_, pos)) => pos) [ #problem-def("MinimumMatrixDomination")[ Given an $m times n$ binary matrix $M$, find a minimum-cardinality subset $C$ of 1-entries such that every 1-entry not in $C$ shares a row or column with some entry in $C$. ][ Minimum Matrix Domination is a matrix analogue of the dominating set problem. Each binary variable corresponds to a 1-entry in row-major order; the evaluator checks that every unselected 1-entry shares a row or column with at least one selected entry. The brute-force complexity is $O(2^k)$ where $k$ is the number of 1-entries. - *Example.* Let $M$ be the #(m)$times$#(n) adjacency matrix of $P_6$ (the path on 6 vertices), which has #(ones.len()) non-zero entries. The optimal config $(#cfg.map(str).join(", "))$ selects entries at positions #selected-ones.map(((r, c)) => [(#r, #c)]).join(", "), yielding value $= #x.optimal_value$. + *Example.* Let $M$ be the #(m)$times$#(n) adjacency matrix of $P_6$ (the path on 6 vertices), which has #(ones.len()) non-zero entries. The optimal config $(#fmt-values(cfg))$ selects entries at positions #selected-ones.map(((r, c)) => [(#r, #c)]).join(", "), yielding value $= #x.optimal_value$. #pred-commands( "pred create --example " + problem-spec(x) + " -o mmd.json", "pred solve mmd.json", - "pred evaluate mmd.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mmd.json --config " + cli-config(x.optimal_config), ) ] ] @@ -11091,12 +10944,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| The best known algorithms for general instances use information set decoding techniques, achieving $O(2^(0.0494 n))$ where $n$ is the block length. - *Example.* Let $H$ be the #(n)$times$#(m) binary matrix and $s = (#s.map(v => if v { "1" } else { "0" }).join(", "))$. The optimal config $(#cfg.map(str).join(", "))$ has Hamming weight $#wt$. + *Example.* Let $H$ be the #(n)$times$#(m) binary matrix and $s = (#fmt-values(s))$. The optimal config $(#fmt-values(cfg))$ has Hamming weight $#wt$. #pred-commands( "pred create --example " + problem-spec(x) + " -o mwd.json", "pred solve mwd.json", - "pred evaluate mwd.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mwd.json --config " + cli-config(x.optimal_config), ) ] ] @@ -11108,19 +10961,19 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let n = A.len() let m = if n > 0 { A.at(0).len() } else { 0 } let cfg = x.optimal_config - let selected-cols = cfg.enumerate().filter(((_, v)) => v == 1).map(((j, _)) => j) + let selected-cols = cfg.enumerate().filter(((_, v)) => v).map(((j, _)) => j) [ #problem-def("MinimumWeightSolutionToLinearEquations")[ Given an $n times m$ integer matrix $A$ and an integer vector $b in ZZ^n$, find a rational vector $y in QQ^m$ satisfying $A y = b$ that minimizes $||y||_0$ (the number of non-zero entries of $y$). ][ Minimum Weight Solution to Linear Equations is a sparsity-seeking variant of solving linear systems. Each binary variable $x_j$ indicates whether the $j$-th component of $y$ may be non-zero; the evaluator forms the restricted submatrix $A'$ from the selected columns and checks whether $b$ lies in its column space via integer Gaussian elimination (using i128 arithmetic for exact rational consistency). If the restricted system $A' y' = b$ is consistent, the value is the number of selected columns; otherwise the configuration is infeasible. - *Example.* Let $A$ be the $#n times #m$ matrix $A = #math.mat(..A.map(row => row.map(v => $#v$)))$ with $b = (#b.map(str).join(", "))$. The optimal config $(#cfg.map(str).join(", "))$ selects columns #selected-cols.map(str).join(", "), yielding value $= #x.optimal_value$. + *Example.* Let $A$ be the $#n times #m$ matrix $A = #math.mat(..A.map(row => row.map(v => $#v$)))$ with $b = (#fmt-values(b))$. The optimal config $(#fmt-values(cfg))$ selects columns #fmt-values(selected-cols), yielding value $= #x.optimal_value$. #pred-commands( "pred create --example " + problem-spec(x) + " -o mwsle.json", "pred solve mwsle.json", - "pred evaluate mwsle.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mwsle.json --config " + cli-config(x.optimal_config), ) ] ] @@ -11139,7 +10992,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o iem.json", "pred solve iem.json --solver brute-force", - "pred evaluate iem.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate iem.json --config " + cli-config(x.optimal_config), ) #figure( @@ -11200,7 +11053,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Free column indices (those not in S) let free-cols = range(n).filter(j => j not in S) // Selected free columns from config - let selected = cfg.enumerate().filter(((i, v)) => v == 1).map(((i, v)) => free-cols.at(i)) + let selected = cfg.enumerate().filter(((i, v)) => v).map(((i, v)) => free-cols.at(i)) // Full basis: required + selected let basis = S + selected [ @@ -11209,12 +11062,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ The Feasible Basis Extension problem arises in linear programming theory and the study of simplex method pivoting rules. It was shown NP-complete by Murty @Murty1972 via a reduction from Hamiltonian Circuit, establishing that determining whether a partial basis can be extended to a feasible one is computationally intractable in general. The problem is closely related to the question of whether a given linear program has a feasible basic solution containing specified variables. The best known exact algorithm is brute-force enumeration of all $binom(n - |S|, m - |S|)$ candidate extensions, testing each for nonsingularity and non-negativity of the solution in $O(m^3)$ time.#footnote[No algorithm improving on brute-force enumeration is known for the general Feasible Basis Extension problem.] - *Example.* Consider the $#m times #n$ matrix $A = #math.mat(..A.map(row => row.map(v => $#v$)))$ with $overline(a) = (#rhs.map(str).join(", "))^top$ and required columns $S = \{#S.map(str).join(", ")\}$. We need $#(m - S.len())$ additional column from the free set $\{#free-cols.map(str).join(", ")\}$. Selecting column #selected.at(0) gives basis $B = \{#basis.map(str).join(", ")\}$, which yields $A_B^(-1) overline(a) = (4, 5, 3)^top >= 0$. Column 4 makes $A_B$ singular, and column 5 produces a negative component. + *Example.* Consider the $#m times #n$ matrix $A = #math.mat(..A.map(row => row.map(v => $#v$)))$ with $overline(a) = (#fmt-values(rhs))^top$ and required columns $S = \{#fmt-values(S)\}$. We need $#(m - S.len())$ additional column from the free set $\{#fmt-values(free-cols)\}$. Selecting column #selected.at(0) gives basis $B = \{#fmt-values(basis)\}$, which yields $A_B^(-1) overline(a) = (4, 5, 3)^top >= 0$. Column 4 makes $A_B$ singular, and column 5 produces a negative component. #pred-commands( "pred create --example " + problem-spec(x) + " -o feasible-basis-extension.json", "pred solve feasible-basis-extension.json --solver brute-force", - "pred evaluate feasible-basis-extension.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate feasible-basis-extension.json --config " + cli-config(x.optimal_config), ) #figure( @@ -11278,7 +11131,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ) } }), - caption: [Feasible Basis Extension instance ($#m times #n$). Orange columns are required ($S = \{#S.map(str).join(", ")\}$), blue column is the selected extension. Together they form a nonsingular basis with non-negative solution.], + caption: [Feasible Basis Extension instance ($#m times #n$). Orange columns are required ($S = \{#fmt-values(S)\}$), blue column is the selected extension. Together they form a nonsingular basis with non-negative solution.], ) ] ] @@ -11311,7 +11164,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o mlr.json", "pred solve mlr.json --solver brute-force", - "pred evaluate mlr.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate mlr.json --config " + cli-config(x.optimal_config), ) #figure( @@ -11385,7 +11238,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #pred-commands( "pred create --example " + problem-spec(x) + " -o ocst.json", "pred solve ocst.json --solver brute-force", - "pred evaluate ocst.json --config " + x.optimal_config.map(str).join(","), + "pred evaluate ocst.json --config " + cli-config(x.optimal_config), ) ] ] @@ -11403,12 +11256,12 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ][ Square Tiling (also known as Bounded Wang Tiling) is problem GP13 in Garey and Johnson @garey1979. It was shown NP-complete via transformation from Directed Hamiltonian Path. The infinite variant (tiling the entire plane) is famously undecidable (Berger, 1966). The best known exact approach enumerates all $|T|^(N^2)$ assignments. - *Example.* Consider $|C| = #nc$ colors, $|T| = #tiles.len()$ tiles, and grid size $N = #n$. The tiles are #tiles.enumerate().map(((i, t)) => [$t_#i = chevron.l #t.at(0), #t.at(1), #t.at(2), #t.at(3) chevron.r$]).join(", "). The witness assignment $(#config.map(str).join(", "))$ places $t_#config.at(0), t_#config.at(1)$ in row 0 and $t_#config.at(2), t_#config.at(3)$ in row 1, satisfying all edge-color constraints. + *Example.* Consider $|C| = #nc$ colors, $|T| = #tiles.len()$ tiles, and grid size $N = #n$. The tiles are #tiles.enumerate().map(((i, t)) => [$t_#i = chevron.l #t.at(0), #t.at(1), #t.at(2), #t.at(3) chevron.r$]).join(", "). The witness assignment $(#fmt-values(config))$ places $t_#config.at(0), t_#config.at(1)$ in row 0 and $t_#config.at(2), t_#config.at(3)$ in row 1, satisfying all edge-color constraints. #pred-commands( "pred create --example SquareTiling -o square_tiling.json", "pred solve square_tiling.json", - "pred evaluate square_tiling.json --config " + config.map(str).join(","), + "pred evaluate square_tiling.json --config " + cli-config(config), ) ] ] @@ -11434,7 +11287,10 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| = Reductions -Each reduction is presented as a *Rule* (with linked problem names and overhead from the graph data), followed by a *Proof* (construction, correctness, variable mapping, solution extraction), and optionally a *Concrete Example* (a small instance with verified solution). Problem names in the rule title link back to their definitions in @sec:problems. +Each reduction is presented as a *Rule* (with linked problem names and explicit parameter contracts from the graph data), followed by a *Proof* (construction, correctness, variable mapping, solution extraction), and optionally a *Concrete Example* (a small instance with verified solution). Problem names in the rule title link back to their definitions in @sec:problems. + +The command blocks assume `route.json` contains the explicitly chosen direct route for +the displayed rule, extracted from the corresponding `pred path` entry. #let max2sat_mc = load-example("Maximum2Satisfiability", "MaxCut") @@ -11445,16 +11301,16 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead extra: [ #pred-commands( "pred create --example " + problem-spec(max2sat_mc.source) + " -o max2sat.json", - "pred reduce max2sat.json --to " + target-spec(max2sat_mc) + " -o bundle.json", + "pred reduce max2sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate max2sat.json --config " + max2sat_mc_sol.source_config.map(str).join(","), + "pred evaluate max2sat.json --config " + cli-config(max2sat_mc_sol.source_config), ) - *Step 1 -- Source instance.* The canonical source has $n = #max2sat_mc.source.instance.num_vars$ variables and #max2sat_mc.source.instance.clauses.len() two-literal clauses. The stored optimal assignment is $(#max2sat_mc_sol.source_config.map(str).join(", "))$, which satisfies all five clauses. + *Step 1 -- Source instance.* The canonical source has $n = #max2sat_mc.source.instance.num_vars$ variables and #max2sat_mc.source.instance.clauses.len() two-literal clauses. The stored optimal assignment is $(#fmt-values(max2sat_mc_sol.source_config))$, which satisfies all five clauses. *Step 2 -- Accumulate the cut weights.* Introduce the reference vertex $s = v_0$ and variable vertices $v_1, v_2, v_3$. After summing the per-clause contributions and deleting zero-weight edges, the target graph has the four signed edges $(s, v_2)$ with weight $-1$, $(s, v_3)$ with weight $-1$, $(v_1, v_2)$ with weight $2$, and $(v_2, v_3)$ with weight $-1$. - *Step 3 -- Verify the witness.* The target witness $(#max2sat_mc_sol.target_config.map(str).join(", "))$ puts $v_2$ and $v_3$ on the same side as $s$ and $v_1$ on the opposite side, so extraction recovers $(#max2sat_mc_sol.source_config.map(str).join(", "))$. Only edge $(v_1, v_2)$ crosses, so the cut value is $2$ and the affine objective identity certifies optimality #sym.checkmark. + *Step 3 -- Verify the witness.* The target witness $(#fmt-values(max2sat_mc_sol.target_config))$ puts $v_2$ and $v_3$ on the same side as $s$ and $v_1$ on the opposite side, so extraction recovers $(#fmt-values(max2sat_mc_sol.source_config))$. Only edge $(v_1, v_2)$ crosses, so the cut value is $2$ and the affine objective identity certifies optimality #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. Flipping every target bit yields the complementary cut partition but extracts the same source assignment because extraction compares each variable vertex to $s$. ], @@ -11496,15 +11352,15 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead extra: [ #pred-commands( "pred create --example Maximum2Satisfiability -o max2sat.json", - "pred reduce max2sat.json --to " + target-spec(max2sat_ilp) + " -o bundle.json", + "pred reduce max2sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate max2sat.json --config " + max2sat_ilp_sol.source_config.map(str).join(","), + "pred evaluate max2sat.json --config " + cli-config(max2sat_ilp_sol.source_config), ) *Step 1 -- Source instance.* The canonical MAX-2-SAT instance has $n = #max2sat_ilp.source.instance.num_vars$ Boolean variables and $m = #max2sat_ilp.source.instance.clauses.len()$ clauses. *Step 2 -- Build the binary ILP.* Introduce $n$ binary truth variables $y_0, dots, y_(n-1) in {0,1}$ and $m$ binary clause-indicator variables $z_0, dots, z_(m-1) in {0,1}$. The objective is $ max sum_(j=0)^(m-1) z_j $ subject to one constraint per clause $j$: $z_j <= l_1' + l_2'$ where $l_i' = y_i$ for a positive literal and $l_i' = 1 - y_i$ for a negated literal. The resulting ILP has $n + m = #(max2sat_ilp.source.instance.num_vars + max2sat_ilp.source.instance.clauses.len())$ variables and $m = #max2sat_ilp.source.instance.clauses.len()$ constraints. - *Step 3 -- Verify a solution.* The ILP optimum extracts the first $n$ variables as the truth assignment $bold(y)^* = (#max2sat_ilp_sol.source_config.map(str).join(", "))$, satisfying #max2sat_ilp_sol.source_config.len() source variables #sym.checkmark. + *Step 3 -- Verify a solution.* The ILP optimum extracts the first $n$ variables as the truth assignment $bold(y)^* = (#fmt-values(max2sat_ilp_sol.source_config))$, satisfying #max2sat_ilp_sol.source_config.len() source variables #sym.checkmark. ], )[ A MAX-2-SAT instance maps directly to a binary ILP @garey1979: each Boolean variable becomes a binary decision variable, each clause gets a binary indicator variable, and a single linear inequality per clause links the indicator to its literals. The objective maximizes the sum of clause indicators, so the ILP optimum equals the maximum number of satisfiable clauses. @@ -11658,12 +11514,12 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead extra: [ #pred-commands( "pred create --example MVC -o mvc.json", - "pred reduce mvc.json --to " + target-spec(mvc_mis) + " -o bundle.json", + "pred reduce mvc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mvc.json --config " + mvc_mis_sol.source_config.map(str).join(","), + "pred evaluate mvc.json --config " + cli-config(mvc_mis_sol.source_config), ) - Source VC: $C = {#mvc_mis_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ (size #mvc_mis_sol.source_config.filter(x => x == 1).len()) #h(1em) - Target IS: $S = {#mvc_mis_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ (size #mvc_mis_sol.target_config.filter(x => x == 1).len()) \ + Source VC: $C = {#mvc_mis_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ")}$ (size #mvc_mis_sol.source_config.filter(x => x).len()) #h(1em) + Target IS: $S = {#mvc_mis_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ")}$ (size #mvc_mis_sol.target_config.filter(x => x).len()) \ $|"VC"| + |"IS"| = #graph-num-vertices(mvc_mis.source.instance) = |V|$ #sym.checkmark ], )[ @@ -11687,65 +11543,69 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead example: true, example-source-variant: (graph: "SimpleGraph", weight: "One"), example-target-variant: (graph: "SimpleGraph", weight: "One"), - example-caption: [6-vertex unit graph: dominating set of size 2 equals a 2-center of radius 1], + example-caption: [6-vertex source: two auxiliary centers encode the domination threshold], extra: [ #pred-commands( "pred create --example " + problem-spec(dmds_mmmc.source) + " -o dmds.json", - "pred reduce dmds.json --to " + target-spec(dmds_mmmc) + " -o bundle.json", + "pred reduce dmds.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate dmds.json --config " + dmds_mmmc_sol.source_config.map(str).join(","), + "pred evaluate dmds.json --config " + cli-config(dmds_mmmc_sol.source_config), ) - *Step 1 -- Source instance.* The source graph has vertices ${0, 1, 2, 3, 4, 5}$, edges #{dmds_mmmc.source.instance.inner.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, and bound $K = #dmds_mmmc.source.instance.bound$. The stored dominating-set witness is $D = {#dmds_mmmc_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => str(i)).join(", ")}$. + *Step 1 -- Source instance.* The source graph has vertices ${0, 1, 2, 3, 4, 5}$, edges #{dmds_mmmc.source.instance.inner.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, and bound $K = #dmds_mmmc.source.instance.bound$. The stored dominating-set witness is $D = {#dmds_mmmc_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$. - *Step 2 -- Build the target instance.* Keep the graph unchanged, assign weight $1$ to every vertex, assign length $1$ to every edge, and set the number of centers to $k = #dmds_mmmc.target.instance.k$. The target therefore still has $#graph-num-vertices(dmds_mmmc.target.instance)$ vertices and $#graph-num-edges(dmds_mmmc.target.instance)$ edges. + *Step 2 -- Build the target instance.* Append two isolated vertices, assign weight $1$ to every vertex and length $1$ to every edge, and set the number of centers to $k = #dmds_mmmc.target.instance.k$. The target therefore has $#graph-num-vertices(dmds_mmmc.target.instance)$ vertices and $#graph-num-edges(dmds_mmmc.target.instance)$ edges. - *Step 3 -- Verify a witness.* Choosing centers $P = {#dmds_mmmc_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => str(i)).join(", ")}$ yields distances $(0, 1, 1, 0, 1, 1)$ to the nearest center, so the maximum weighted distance is $1$. The extracted source witness is the same indicator vector, hence a dominating set of size $2$ #sym.checkmark + *Step 3 -- Verify a witness.* Choosing centers $P = {#dmds_mmmc_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$ yields distances $(0, 1, 1, 0, 1, 1, 0, 0)$ to the nearest center, so the maximum weighted distance is $1$. Discarding the two auxiliary center bits recovers a dominating set of size $2$ #sym.checkmark ], )[ - This $O(n + m)$ parameter-setting reduction @garey1979[ND50] keeps the graph unchanged, replaces all vertex weights and edge lengths by $1$, and copies the decision budget $K$ into the target center count $k$. On such unit graphs, a $k$-center solution of radius at most $1$ exists exactly when every vertex is itself chosen or adjacent to a chosen vertex, which is the dominating-set condition. + The radius-threshold relation between centers and dominating sets @hochbaumshmoys1985 is extended here to all signed source bounds using two mandatory isolated centers. This $O(n+m+1)$ construction preserves the existing endpoint variants. ][ - _Construction._ Given a unit-weight decision dominating-set instance $(G = (V, E), K)$, build a Min-Max Multicenter instance on the same graph $G$. Set $w(v) = 1$ for every vertex, set $l(e) = 1$ for every edge, and set the number of centers to $k = K$. + _Construction._ For source graph $G=(V,E)$ with $n$ vertices and integer bound $K$, set $q=max(-1,min(K,n))$. Add isolated vertices $a=n$ and $b=n+1$, leaving every original edge record unchanged. Give all vertices and edges unit weights and lengths, and require exactly $k=q+2$ centers. Then $1<=k<=n+2$ for every input, including an empty graph. - _Correctness._ ($arrow.r.double$) If $D subset.eq V$ is a dominating set with $|D| <= K$, pad $D$ with arbitrary additional vertices until exactly $K$ centers are chosen. Every vertex is then either a center (distance $0$) or adjacent to one (distance $1$), so the target maximum weighted distance is at most $1$. ($arrow.l.double$) If a set $P subset.eq V$ of exactly $K$ centers has maximum weighted distance at most $1$, then every vertex lies at graph distance $0$ or $1$ from some vertex of $P$. Hence every vertex is either in $P$ or adjacent to a vertex of $P$, so $P$ is a dominating set of size $K$. + _Correctness._ Every finite target placement must select both isolated vertices. If a source dominating set $D$ has $|D|<=K$, then $q>=0$ and $|D|<=q<=n$. Extend $D$ to $q$ original vertices and add $a,b$. This placement has $k$ centers and radius at most $1$, proving the forward direction. Conversely, a target placement of radius at most $1$ selects both isolates and exactly $q$ original vertices. Each original vertex is within one original edge of a selected vertex, so those $q<=K$ vertices dominate $G$. For $K<0$, $k=1$ cannot cover both isolates and the target has no finite placement. For $n=0,K>=0$, the two isolates form a radius-zero placement. Loops and repeated edges preserve this reasoning. - _Solution extraction._ Return the same indicator vector: every chosen target center becomes a chosen source dominating-set vertex. + _Solution extraction and NO instances._ Evaluate the full target indicator first. A finite radius at most $1$ permits extraction of its first $n$ bits. Any larger radius or infeasible placement is rejected. The formal aggregate map sends an optimum $r<=1$ to true, and an optimum $r>1$ or infeasibility to false. In particular, a four-vertex path with $K=1$ produces optimum radius $2$, not an infeasible target. Checked parameter arithmetic precedes allocation; unrepresentable counts return the formal numeric error. Target sizes are exactly $n+2$ vertices and $m$ edge records. ] #let dmds_msmc = load-example( "DecisionMinimumDominatingSet", "MinimumSumMulticenter", source-variant: (graph: "SimpleGraph", weight: "One"), - target-variant: (graph: "SimpleGraph", weight: "i32"), + target-variant: (graph: "SimpleGraph", weight: "i64"), ) #let dmds_msmc_sol = dmds_msmc.solutions.at(0) #reduction-rule("DecisionMinimumDominatingSet", "MinimumSumMulticenter", example: true, example-source-variant: (graph: "SimpleGraph", weight: "One"), - example-target-variant: (graph: "SimpleGraph", weight: "i32"), + example-target-variant: (graph: "SimpleGraph", weight: "i64"), example-caption: [6-vertex unit graph: dominating set of size 2 gives total distance 4], extra: [ #pred-commands( "pred create --example " + problem-spec(dmds_msmc.source) + " -o dmds.json", - "pred reduce dmds.json --to " + target-spec(dmds_msmc) + " -o bundle.json", + "pred reduce dmds.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate dmds.json --config " + dmds_msmc_sol.source_config.map(str).join(","), + "pred evaluate dmds.json --config " + cli-config(dmds_msmc_sol.source_config), ) - *Step 1 -- Source instance.* The source graph has vertices ${0, 1, 2, 3, 4, 5}$, edges #{dmds_msmc.source.instance.inner.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, and decision bound $K = #dmds_msmc.source.instance.bound$. The stored dominating-set witness is $D = {#dmds_msmc_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => str(i)).join(", ")}$. + *Step 1 -- Source instance.* The source graph has vertices ${0, 1, 2, 3, 4, 5}$, edges #{dmds_msmc.source.instance.inner.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, and decision bound $K = #dmds_msmc.source.instance.bound$. The stored dominating-set witness is $D = {#dmds_msmc_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$. - *Step 2 -- Build the target instance.* Keep the graph unchanged, assign vertex weight $1$ everywhere, assign edge length $1$ everywhere, and set the target center count to $k = #dmds_msmc.target.instance.k$. The comparison threshold is $B = |V| - K = 6 - 2 = 4$. + *Step 2 -- Build the target instance.* Add one isolated vertex $z$, assign vertex weight $1$ everywhere, assign edge length $1$ everywhere, and set the target center count to $k = #dmds_msmc.target.instance.k$. The comparison threshold is $B = |V| - K = 6 - 2 = 4$. - *Step 3 -- Verify a witness.* Choosing centers $P = {#dmds_msmc_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => str(i)).join(", ")}$ yields distances $(0, 1, 1, 0, 1, 1)$ to the nearest center, so the total weighted distance is $4 = B$. The extracted source witness is the same indicator vector, hence a valid YES witness for the original decision instance #sym.checkmark + *Step 3 -- Verify a witness.* Choosing centers $P = {#dmds_msmc_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$ yields distances $(0, 1, 1, 0, 1, 1, 0)$ to the nearest center, so the total weighted distance is $4 = B$. The extracted source witness removes the coordinate of $z$, hence a valid YES witness for the original decision instance #sym.checkmark ], )[ - This $O(n + m)$ parameter-setting reduction @garey1979[ND51] keeps the graph unchanged, sets every vertex weight and edge length to $1$, copies the decision budget $K$ into the target center count $k$, and compares the target optimum against $B = |V| - K$. On such unit graphs, every exact-$K$ center placement has total distance at least $n - K$, with equality exactly when every non-center vertex is adjacent to a center. + This $O(n + m)$ reduction uses unit vertex weights and edge lengths. For $K >= 0$, let $q = min(K, n)$, add one isolated vertex $z$, and request exactly $q + 1$ centers. The source answer is YES exactly when the target optimum is $B = n - q$. For $K < 0$, add two isolated vertices and request one center, producing an infeasible target. ][ - _Construction._ Given a decision dominating-set instance $(G = (V, E), K)$ with unit vertex weights, build a Minimum Sum Multicenter instance on the same graph $G$. Set $w(v) = 1$ for every vertex, set $l(e) = 1$ for every edge, and set the number of centers to $k = K$. Let $n = |V|$, and define the decision threshold $B = n - K$ for the target optimum. + _Construction and domain._ Let $n = |V|$. For nonnegative $K$, replacing $K$ by $q = min(K, n)$ preserves the decision problem because every vertex subset has size at most $n$. The auxiliary isolate makes $1 <= q + 1 <= n + 1$, so the target is defined even for $K = 0$ and the empty source graph. All original edges are retained. For negative $K$, no source subset can satisfy the bound, and two auxiliary isolates cannot both be served by one center. The target has at most $n + 2$ vertices and exactly $m$ edges. + + _Correctness for nonnegative bounds._ Every finite-cost placement must select $z$, leaving exactly $q$ original centers. Each of the $n - q$ original non-centers has distance at least one, so every finite target cost is at least $B = n - q$. + + ($arrow.r.double$) A source dominating set of size at most $K$ also has size at most $q$. Pad it to size $q$ and add $z$. Each original non-center has distance exactly one; the resulting target cost equals $B$ and is optimal. - _Correctness._ ($arrow.r.double$) If $D subset.eq V$ is a dominating set with $|D| <= K$, pad $D$ with arbitrary additional vertices until exactly $K$ centers are chosen. Every chosen center contributes distance $0$, and every non-center vertex is adjacent to at least one chosen center, so every non-center contributes distance $1$. Hence the total weighted distance is exactly $n - K = B$. + ($arrow.l.double$) A target placement of cost $B$ must select $z$. Equality in the distance lower bound forces every original non-center to have distance one from an original center. Removing $z$ therefore gives a dominating set of size $q <= K$. - ($arrow.l.double$) Suppose a set $P subset.eq V$ of exactly $K$ centers has total weighted distance at most $B = n - K$. Every non-center vertex has distance at least $1$ from $P$, so any exact-$K$ center placement has total distance at least $n - K$. Therefore total distance at most $n - K$ forces equality, meaning every non-center contributes exactly $1$. Thus every non-center vertex is adjacent to some center in $P$, so $P$ is a dominating set of size $K$. + _Boundary cases._ If $K = 0 < n$, one center cannot serve both the isolate and the original graph, so the target is infeasible. If $n = 0$ and $K >= 0$, the sole vertex $z$ is selected and the cost is zero, correctly certifying the empty dominating set. If $K >= n$, selecting all target vertices gives cost zero and extracts all original vertices. Negative bounds give infeasibility as shown above. - _Solution extraction._ Return the same indicator vector: every chosen target center becomes a chosen source dominating-set vertex. + _Value and solution extraction._ Map a finite target optimum equal to $B$ to YES; map any other optimum or infeasibility to NO. For negative bounds use comparison value $-1$, which no finite nonnegative target cost can equal. Extract a source witness only from a placement whose cost equals the comparison value, by removing the auxiliary coordinates. Reject every other placement; an optimal target solution with cost greater than $B$ is not a source YES witness. ] #let mvc_mmm = load-example("MinimumVertexCover", "MinimumMaximalMatching") @@ -11756,8 +11616,8 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead extra: [ #{ let target-edges = mvc_mmm.target.instance.graph.edges - let source-cover = mvc_mmm_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) - let matching = mvc_mmm_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => target-edges.at(i)) + let source-cover = mvc_mmm_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) + let matching = mvc_mmm_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => target-edges.at(i)) let fmt-edge(e) = "(" + str(e.at(0)) + ", " + str(e.at(1)) + ")" [ #pred-commands( @@ -11769,7 +11629,7 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead *Step 1 -- Shared instance.* Both problems use the same 5-cycle, so $n = #graph-num-vertices(mvc_mmm.source.instance)$ and $|E| = #graph-num-edges(mvc_mmm.source.instance)$. - *Step 2 -- Source optimum.* The canonical minimum vertex cover is $C = {#source-cover.map(str).join(", ")}$, so $"mvc"(C_5) = #source-cover.len() = 3$. + *Step 2 -- Source optimum.* The canonical minimum vertex cover is $C = {#fmt-values(source-cover)}$, so $"mvc"(C_5) = #source-cover.len() = 3$. *Step 3 -- Target optimum.* The canonical minimum maximal matching is $M = {#matching.map(fmt-edge).join(", ")}$, so $"mmm"(C_5) = #matching.len() = 2$. @@ -11782,7 +11642,7 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead )[ This size-preserving identity map records the forward implication used in the classical NP-hardness proof for Minimum Maximal Matching (equivalently, Minimum Edge Dominating Set) on bounded-degree graphs: every unit-weight vertex cover of $G$ can be greedily converted into a maximal matching of size at most the cover size. The converse loses a factor of two in general, so the edge is documented but intentionally disabled for runtime reduction search. ][ - _Construction._ Given a unit-weight Minimum Vertex Cover instance $(G = (V, E), K)$, build the Minimum Maximal Matching instance on the same graph $G$. The target uses one binary variable per source edge, so the graph structure and size fields are unchanged. + _Construction._ Given a unit-weight Minimum Vertex Cover instance $(G = (V, E), K)$, build the Minimum Maximal Matching instance on the same graph $G$. The target uses one binary variable per source edge, so the graph structure and parameters are unchanged. _Correctness._ ($arrow.r.double$) Let $C subset.eq V$ be a vertex cover with $|C| lt.eq K$. Start with $M = emptyset$ and process the vertices of $C$ in arbitrary order. Whenever $v in C$ is unmatched, choose any edge $\{v, u\} in E$ whose other endpoint $u$ is also unmatched, add that edge to $M$, and mark both endpoints matched. Because only unmatched endpoints are paired, $M$ is a matching. If some edge $\{x, y\} in E$ were disjoint from every edge of $M$ at the end, then both $x$ and $y$ would still be unmatched. Since $C$ covers every edge, at least one endpoint, say $x$, lies in $C$, and when the algorithm processed $x$ it could have added $\{x, y\}$, a contradiction. Hence $M$ is maximal and $|M| lt.eq |C| lt.eq K$. @@ -11798,25 +11658,25 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead example-caption: [Path graph $P_4$: VC $arrow.r$ LCS via vertex symbols], extra: [ #{ - let source-cover = mvc_lcs_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) + let source-cover = mvc_lcs_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) let target-config = mvc_lcs_sol.target_config - let witness = target-config.filter(x => x < mvc_lcs.target.instance.alphabet_size) + let witness = target-config.filter(x => x != none) let num-strings = mvc_lcs.target.instance.strings.len() let total-length = mvc_lcs.target.instance.strings.map(s => s.len()).fold(0, (acc, n) => acc + n) - let fmt-seq(xs) = "(" + xs.map(str).join(", ") + ")" + let fmt-seq(xs) = "(" + fmt-values(xs) + ")" [ #pred-commands( "pred create --example " + problem-spec(mvc_lcs.source) + " -o mvc.json", - "pred reduce mvc.json --to " + target-spec(mvc_lcs) + " -o bundle.json", + "pred reduce mvc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mvc.json --config " + mvc_lcs_sol.source_config.map(str).join(","), + "pred evaluate mvc.json --config " + cli-config(mvc_lcs_sol.source_config), ) - *Step 1 -- Source instance.* Path graph $P_4$ with $n = #graph-num-vertices(mvc_lcs.source.instance)$ vertices and $|E| = #graph-num-edges(mvc_lcs.source.instance)$ edges. The canonical minimum vertex cover is $C = {#source-cover.map(str).join(", ")}$. + *Step 1 -- Source instance.* Path graph $P_4$ with $n = #graph-num-vertices(mvc_lcs.source.instance)$ vertices and $|E| = #graph-num-edges(mvc_lcs.source.instance)$ edges. The canonical minimum vertex cover is $C = {#fmt-values(source-cover)}$. *Step 2 -- Construct the LCS instance.* Alphabet $Sigma = {0, dots, #((mvc_lcs.target.instance.alphabet_size) - 1)}$ and $#num-strings$ strings: $S_0 = #fmt-seq(mvc_lcs.target.instance.strings.at(0))$, $S_1 = #fmt-seq(mvc_lcs.target.instance.strings.at(1))$, $S_2 = #fmt-seq(mvc_lcs.target.instance.strings.at(2))$, $S_3 = #fmt-seq(mvc_lcs.target.instance.strings.at(3))$. The target has `max_length` $#mvc_lcs.target.instance.max_length$ and total input length $#total-length$. - *Step 3 -- Verify the witness.* The stored target config is #fmt-seq(target-config), so the non-padding common subsequence is $w = #fmt-seq(witness)$ and the corresponding independent set is ${#witness.map(str).join(", ")}$. Taking the complement gives $V backslash w = {#source-cover.map(str).join(", ")} = C$ #sym.checkmark. + *Step 3 -- Verify the witness.* The stored target config is #fmt-seq(target-config), so the non-padding common subsequence is $w = #fmt-seq(witness)$ and the corresponding independent set is ${#fmt-values(witness)}$. Taking the complement gives $V backslash w = {#fmt-values(source-cover)} = C$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ] @@ -11840,20 +11700,20 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead #let mvc_fvs = load-example("MinimumVertexCover", "MinimumFeedbackVertexSet") #let mvc_fvs_sol = mvc_fvs.solutions.at(0) -#let mvc_fvs_cover = mvc_fvs_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) -#let mvc_fvs_fvs = mvc_fvs_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let mvc_fvs_cover = mvc_fvs_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) +#let mvc_fvs_fvs = mvc_fvs_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #reduction-rule("MinimumVertexCover", "MinimumFeedbackVertexSet", example: true, example-caption: [7-vertex graph: each source edge becomes a directed 2-cycle], extra: [ #pred-commands( "pred create --example MVC -o mvc.json", - "pred reduce mvc.json --to " + target-spec(mvc_fvs) + " -o bundle.json", + "pred reduce mvc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mvc.json --config " + mvc_fvs_sol.source_config.map(str).join(","), + "pred evaluate mvc.json --config " + cli-config(mvc_fvs_sol.source_config), ) - Source VC: $C = {#mvc_fvs_cover.map(str).join(", ")}$ (size #mvc_fvs_cover.len()) on a graph with $n = #graph-num-vertices(mvc_fvs.source.instance)$ vertices and $|E| = #graph-num-edges(mvc_fvs.source.instance)$ edges \ - Target FVS: $F = {#mvc_fvs_fvs.map(str).join(", ")}$ (size #mvc_fvs_fvs.len()) on a digraph with the same $n = #graph-num-vertices(mvc_fvs.target.instance)$ vertices and $|A| = #mvc_fvs.target.instance.graph.arcs.len() = 2 |E|$ arcs \ + Source VC: $C = {#fmt-values(mvc_fvs_cover)}$ (size #mvc_fvs_cover.len()) on a graph with $n = #graph-num-vertices(mvc_fvs.source.instance)$ vertices and $|E| = #graph-num-edges(mvc_fvs.source.instance)$ edges \ + Target FVS: $F = {#fmt-values(mvc_fvs_fvs)}$ (size #mvc_fvs_fvs.len()) on a digraph with the same $n = #graph-num-vertices(mvc_fvs.target.instance)$ vertices and $|A| = #mvc_fvs.target.instance.graph.arcs.len() = 2 |E|$ arcs \ Canonical witness is preserved exactly: $C = F$ #sym.checkmark ], )[ @@ -11880,24 +11740,24 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead #let mis_clique = load-example( "MaximumIndependentSet", "MaximumClique", - source-variant: (graph: "SimpleGraph", weight: "i32"), - target-variant: (graph: "SimpleGraph", weight: "i32"), + source-variant: (graph: "SimpleGraph", weight: "i64"), + target-variant: (graph: "SimpleGraph", weight: "i64"), ) #let mis_clique_sol = mis_clique.solutions.at(0) #reduction-rule("MaximumIndependentSet", "MaximumClique", example: true, - example-source-variant: (graph: "SimpleGraph", weight: "i32"), - example-target-variant: (graph: "SimpleGraph", weight: "i32"), + example-source-variant: (graph: "SimpleGraph", weight: "i64"), + example-target-variant: (graph: "SimpleGraph", weight: "i64"), example-caption: [Path graph $P_5$: IS $arrow.r$ Clique via complement], extra: [ #pred-commands( "pred create --example MIS -o mis.json", - "pred reduce mis.json --to " + target-spec(mis_clique) + " -o bundle.json", + "pred reduce mis.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mis.json --config " + mis_clique_sol.source_config.map(str).join(","), + "pred evaluate mis.json --config " + cli-config(mis_clique_sol.source_config), ) - Source IS: $S = {#mis_clique_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ (size #mis_clique_sol.source_config.filter(x => x == 1).len()) #h(1em) - Target Clique: $C = {#mis_clique_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ (size #mis_clique_sol.target_config.filter(x => x == 1).len()) \ + Source IS: $S = {#mis_clique_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ")}$ (size #mis_clique_sol.source_config.filter(x => x).len()) #h(1em) + Target Clique: $C = {#mis_clique_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ")}$ (size #mis_clique_sol.target_config.filter(x => x).len()) \ Source $|E| = #graph-num-edges(mis_clique.source.instance)$, complement $|overline(E)| = #graph-num-edges(mis_clique.target.instance)$ #sym.checkmark ], )[ @@ -11948,54 +11808,94 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead extra: [ #pred-commands( "pred create --example " + problem-spec(dmvc_cc.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(dmvc_cc) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + dmvc_cc_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(dmvc_cc_sol.source_config), ) - Source VC witness $(#dmvc_cc_sol.source_config.map(str).join(", "))$, target containment indicator $(#dmvc_cc_sol.target_config.map(str).join(", "))$. + Source VC witness $(#fmt-values(dmvc_cc_sol.source_config))$, target containment indicator $(#fmt-values(dmvc_cc_sol.target_config))$. ], )[ - Plaisted's reduction @plaisted1976 encodes a unit-weight Decision Vertex Cover instance $(G = (V, E), K)$ as a Comparative Containment instance on universe $X = V$. Each vertex contributes a complement set with unit reward; each edge contributes a complement-of-edge penalty set with weight $|V| + 1$ that dominates the total reward whenever the edge is uncovered; and a single budget set with weight $|V| - K$ enforces the cardinality bound. + This signed-weight extension of the complement-set encoding for Comparative Containment (Garey--Johnson SP10 @garey1979, credited there to @plaisted1976) preserves the existing `i64` Decision Vertex Cover endpoint. It handles negative and zero vertex weights without changing the target's positive-weight definition. ][ - _Construction._ Given a unit-weight VC instance $(G = (V, E), K)$ with $n = |V|$, set the universe $X = V$ and define: - - For each vertex $v in V$, the reward set $R_v = V without {v}$ with weight $w(R_v) = 1$. Then $Y subset.eq R_v$ iff $v in.not Y$, so $sum_(Y subset.eq R_v) w(R_v) = n - |Y|$. - - For each edge $e = {u, v} in E$, the edge-penalty set $S_e = V without {u, v}$ with weight $w(S_e) = n + 1$. Then $Y subset.eq S_e$ iff neither $u$ nor $v$ lies in $Y$, i.e.\ iff $e$ is uncovered. - - A budget set $S_0 = V$ with weight $w(S_0) = n - K$. Since $Y subset.eq V$ always holds, this set contributes the constant penalty $n - K$. + _Construction._ For vertex weights $w_v$ and threshold $K$, define $U=sum_v max(w_v,0)$, $L=sum_v min(w_v,0)$, $W=U+L$, $B=min(K,U)$, $C=B-W$, and $P=U-L+1$. Use universe $X=V$. Process the signed terms $(V without {v},w_v)$ in vertex order and then $(V,C)$: put positive terms in $R$, negative terms in $S$ with their negated coefficients, and omit zero terms. Finally, for each edge record $(u,v)$, append $(V without {u,v},P)$ to $S$ in edge order. - The containment inequality becomes $n - |Y| >= (n + 1) dot (\#"uncovered edges") + (n - K)$, which simplifies to - $ K - |Y| >= (n + 1) dot (\#"uncovered edges"). $ + _Correctness._ For subset $Y$, let $q(Y)$ count uncovered edge records and $w(Y)=sum_(v in Y)w_v$. The difference between the two containment totals is + $ D(Y)=C+sum_(v in.not Y)w_v-P q(Y)=B-w(Y)-P q(Y). $ + Every subset has $L<=w(Y)<=U$, so the thresholds $K$ and $B$ are equivalent. A cover meeting the source bound has $q(Y)=0$ and $D(Y)>=0$, proving the forward direction. Conversely, an uncovered edge would imply $D(Y)<=U-L-P=-1$, contradicting target feasibility. Thus any feasible target subset covers all edges and satisfies $w(Y)<=B<=K$. This also forces selection of looped vertices; repeated edges repeat the same constraint. Empty graphs use the same construction. - _Correctness._ ($arrow.r.double$) If $Y$ is a vertex cover with $|Y| <= K$, the right-hand side equals $0$ and the inequality $K - |Y| >= 0$ holds. ($arrow.l.double$) Suppose the inequality holds for some $Y$. If $Y$ leaves an edge uncovered, the right-hand side is at least $n + 1 > n >= K - |Y|$, a contradiction. Hence $Y$ is a vertex cover and $K - |Y| >= 0$, i.e.\ $|Y| <= K$. + _Solution extraction and arithmetic._ Validate target feasibility through the target API and return the identical indicator vector. All derived coefficients and both complete target-family sums use checked `i64` arithmetic; an unrepresentable construction returns the formal numeric error. Positive family weights ensure that checking their complete sums bounds every target evaluation. No source solve or replacement witness is used. There are exactly $n$ universe elements, at most $n+1$ R-sets, and at most $n+m+1$ S-sets; explicit complement construction takes $O(n(n+m+1))$ time and space. +] - _Solution extraction._ The indicator vector of $Y subset.eq X$ over the universe $X = V$ is read off as the source vertex-cover indicator. Two corner cases are emitted as trivial instances: when $K >= n$ every cover satisfies the bound, so the target is the empty Comparative Containment instance whose unique configuration is trivially feasible; when $K < 0$ the bound is unattainable, and the target is a fixed unsatisfiable instance with a single penalty set. +#let ec_ilp = load-example("EnsembleComputation", "ILP") +#let ec_ilp_sol = ec_ilp.solutions.at(0) +#reduction-rule("EnsembleComputation", "ILP", + example: true, + example-caption: [Three-slot disjoint-union circuit on four atoms], + extra: [ + #pred-commands( + "pred create --example " + problem-spec(ec_ilp.source) + " -o ensemble.json", + "pred reduce ensemble.json --via route.json -o bundle.json", + "pred solve bundle.json", + "pred evaluate ensemble.json --config " + cli-config(ec_ilp_sol.source_config), + ) + The source has universe size #ec_ilp.source.instance.universe_size, budget #ec_ilp.source.instance.budget, and targets #ec_ilp.source.instance.subsets. Its stored witness uses #(ec_ilp_sol.source_config.len() / 2) union operations. The ILP selector variables recover the same ordered operand pairs, and evaluating the extracted source configuration gives #(ec_ilp_sol.source_config.len() / 2) #sym.checkmark. + ], +)[ + This polynomial-size slot encoding follows the circuit viewpoint used by Järvisalo, Kaski, Koivisto, and Korhonen @jarvisaloEtAl2012. It represents at most $J$ ordered disjoint-union gates without enumerating the $2^|A|$ possible sets. +][ + _Construction._ Let $A={0,dots,u-1}$, let $cal(C)=(C_1,dots,C_t)$, and number operation slots $k=0,dots,J-1$. Binary $a_k$ activates slot $k$, with $a_k >= a_(k+1)$. Two one-hot selector families choose each active slot's left and right operands from the $u$ singleton atoms and results of earlier active slots; because union is commutative, require the left operand index to be smaller than the right operand index. For a selector $s$ and earlier result bit $r_(h,e)$, a binary auxiliary $v=s r_(h,e)$ is enforced by $v<=s$, $v<=r_(h,e)$, and $v>=s+r_(h,e)-1$. Thus the selected operand-membership bits $lambda_(k,e)$ and $rho_(k,e)$ are linear expressions. Impose + $ + lambda_(k,e)+rho_(k,e) <= 1, quad + r_(k,e)=lambda_(k,e)+rho_(k,e) + $ + for every $k,e$. Match variable $m_(c,k)$ may equal one only if $a_k=1$ and every result bit $r_(k,e)$ equals the membership of $e$ in $C_c$; require $sum_k m_(c,k)>=1$. Minimize $sum_k a_k$. The construction uses + $ + 3 J u + J(J-1)(u+1) + t J + J + $ + variables and + $ + (5 J-1)+J(J-1)(1+3 u)+2 J u+t J(u+2)+t + $ + constraints. + + _Correctness._ ($arrow.r.double$) A valid $K<=J$ operation program activates its first $K$ slots, copies each operand reference into the corresponding selector, and assigns the computed membership and match bits; all constraints hold with objective $K$. ($arrow.l.double$) In any feasible ILP solution the active slots form a prefix, each operand is either an atom or an earlier active result, the McCormick constraints recover its exact membership, and the disjointness and union equations make every active slot a legal operation. Match constraints ensure that every required set occurs as a result. Therefore the optimum is the minimum feasible operation count within budget $J$. Empty and singleton targets remain infeasible because atoms are operands, not computed results. + + _Solution extraction._ Read the two one-hot operand selectors from active slots in increasing order. Preserve atom indices and encode earlier result $h$ as operand index $u+h$; fill unused suffix slots with a harmless disjoint atom pair because source evaluation stops once all targets have appeared. ] #reduction-rule("MinimumVertexCover", "EnsembleComputation")[ - This $O(|V| + |E|)$ reduction @garey1979 encodes the unit-weight vertex-cover problem as an ensemble-computation minimization over disjoint unions. A fresh element $a_0$ is introduced, and each edge becomes a 3-element target subset. The minimum sequence length equals $K^* + |E|$, where $K^*$ is the minimum vertex cover size. + This reduction @garey1979 (Theorem 3.6, pp. 66--68) maps unit-weight vertex cover to disjoint-union ensemble computation. If $tau$ is the minimum cover size and $m$ the number of edges, the target optimum is $m+tau$. The extraction below applies to every optimal target program, without assuming that the solver returns a normalized program. ][ - _Construction._ Given a unit-weight VC instance $G = (V, E)$, let $a_0$ be a fresh element not in $V$. Set the universe $A = V union {a_0}$ with $|A| = |V| + 1$. For each edge ${u, v} in E$, add the subset ${a_0, u, v}$ to the collection $C$. Set the search-space bound $J = |V| + |E|$. + _Construction._ For a simple graph $G=(V,E)$ with $n=|V|$ and $m=|E|$, give the original vertices indices $0,dots,n-1$ and introduce a fresh atom $a_0$ with index $n$. Set $A=V union {a_0}$ and require the $m$ distinct triples ${a_0,u,v}$ for ${u,v} in E$. Use operation budget $J=max(1,n+m)$, including the positive-budget representation of an empty computation. Thus the universe size is $n+1$, the number of required sets is $m$, and $J<=n+m+1$. + + _Forward bound._ For a vertex cover $C$, first compute ${a_0,v}$ for each $v in C$. For each edge, choose an endpoint $v in C$ and unite ${a_0,v}$ with its other endpoint singleton. The resulting program computes every required triple with at most $|C|+m$ operations, within budget $J$. In particular the optimum $L^*$ satisfies $L^*<=tau+m$. - _Correctness._ ($arrow.r.double$) If $C'$ is a vertex cover of size $K$, label its elements $v_1, dots, v_K$ and the edges $e_1, dots, e_m$. Since $C'$ covers every edge, each $e_j = {u_j, v_(r[j])}$ where $v_(r[j]) in C'$. The sequence of $K + m$ operations $z_i = {a_0} union {v_i}$ for $i = 1, dots, K$ followed by $z_(K+j) = {u_j} union z_(r[j])$ for $j = 1, dots, m$ produces every target subset in exactly $K + |E|$ steps. ($arrow.l.double$) An exchange argument (Garey & Johnson, PO9) shows that any minimum-length sequence can be normalized to use only ${a_0} union {u}$ and ${v} union z_k$ forms. Each edge contributes exactly one operation of the second form, so the number of first-form operations equals the sequence length minus $|E|$. Since the first-form vertices must cover all edges, the minimum sequence length is $K^* + |E|$. + _Backward bound._ Let $L$ be the evaluated prefix length of any valid target program. Every computed set has at least two elements, because all initial operands are singletons and every union is disjoint. Therefore a two-element set is produced exactly by two singleton operands. Each required triple must be formed using an earlier two-element set and a singleton. For its edge ${u,v}$, this pair is one of ${a_0,u}$, ${a_0,v}$, or ${u,v}$. Select respectively $u$, $v$, or $min(u,v)$ from the operation producing that pair. Each selection covers the corresponding edge. Applying this selection to every pair-producing operation in the prefix gives a vertex cover $S$. + + At least $m$ distinct operations produce the required triples; none of these produces a pair. Consequently $|S|<=L-m$, even if the program repeats intermediate sets or contains unused operations and larger sets. At an optimum, $tau<=|S|<=L^*-m<=tau$, proving both $L^*=m+tau$ and optimality of the extracted cover. + + _Solution extraction._ Use the existing target validation API's evaluated prefix length and ignore padding after that prefix. Whenever both operands are singleton indices, select their smaller index. The fresh atom has the largest singleton index, so this implements all three mathematical choices above. Operations with a computed operand contribute no selected vertex. Extraction takes $O(n+L)$ time after target validation and does not perform normalization or source optimization. With no required sets the evaluated prefix is empty and the extracted cover is empty. + + The same extraction also covers native graph representations with loops or repeated edges. A loop at $u$ requires the pair ${a_0,u}$ and therefore forces selection of $u$. Repeated required sets need only be computed once. With $d$ distinct non-loop edges, the preceding bounds become $|S|<=L-d$ and $L^*=tau+d$; the unchanged budget based on all edge records remains sufficient. - _Solution extraction._ From an optimal witness, collect all vertices appearing as singleton operands (indices $< |V|$). In a minimum-length normalized sequence, exactly the $K^*$ cover vertices appear as ${a_0}$-paired singletons. ] #let mvc_aog = load-example("MinimumVertexCover", "MinimumWeightAndOrGraph") #let mvc_aog_sol = mvc_aog.solutions.at(0) -#let mvc_aog_cover = mvc_aog_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let mvc_aog_cover = mvc_aog_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #reduction-rule("MinimumVertexCover", "MinimumWeightAndOrGraph", example: true, example-caption: [Path $P_3$: vertex cover ${1}$ maps to an AND/OR graph of weight 5], extra: [ #pred-commands( "pred create --example MVC -o mvc.json", - "pred reduce mvc.json --to " + target-spec(mvc_aog) + " -o bundle.json", + "pred reduce mvc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mvc.json --config " + mvc_aog_sol.source_config.map(str).join(","), + "pred evaluate mvc.json --config " + cli-config(mvc_aog_sol.source_config), ) - Source VC: $C = {#mvc_aog_cover.map(str).join(", ")}$ (size #mvc_aog_cover.len()) on graph with $n = #graph-num-vertices(mvc_aog.source.instance)$ vertices, $m = #graph-num-edges(mvc_aog.source.instance)$ edges \ + Source VC: $C = {#fmt-values(mvc_aog_cover)}$ (size #mvc_aog_cover.len()) on graph with $n = #graph-num-vertices(mvc_aog.source.instance)$ vertices, $m = #graph-num-edges(mvc_aog.source.instance)$ edges \ Target AND/OR graph: #mvc_aog.target.instance.num_vertices vertices, source $v_#mvc_aog.target.instance.source$ (AND), arcs: #{range(mvc_aog.target.instance.arcs.len()).map(i => {let a = mvc_aog.target.instance.arcs.at(i); $v_#(a.at(0)) arrow.r v_#(a.at(1))$}).join(", ")} \ - Selected arcs: #{mvc_aog_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => {let a = mvc_aog.target.instance.arcs.at(i); $v_#(a.at(0)) arrow.r v_#(a.at(1))$}).join(", ")} (weight #{mvc_aog_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => mvc_aog.target.instance.arc_weights.at(i)).sum()}) #sym.checkmark + Selected arcs: #{mvc_aog_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, _)) => {let a = mvc_aog.target.instance.arcs.at(i); $v_#(a.at(0)) arrow.r v_#(a.at(1))$}).join(", ")} (weight #{mvc_aog_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, _)) => mvc_aog.target.instance.arc_weights.at(i)).sum()}) #sym.checkmark ], )[ This reduction encodes vertex cover as a minimum-weight solution subgraph problem on a three-layer AND/OR DAG. The root AND gate requires all edges to be covered; each edge becomes an OR gate selecting which endpoint covers it; and each vertex becomes a sink whose arc weight equals the vertex weight. The minimum-weight solution subgraph selects exactly the arcs corresponding to a minimum vertex cover. @@ -12035,21 +11935,28 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead _Solution extraction._ Convert spins to binary: $x_i = (s_i + 1) \/ 2$, i.e.\ $s_i = +1 arrow.r x_i = 1$, $s_i = -1 arrow.r x_i = 0$. ] -#let sg_qubo = load-example("SpinGlass", "QUBO") +#let sg_qubo = load-example( + "SpinGlass", + "QUBO", + source-variant: (graph: "SimpleGraph", weight: "f64"), + target-variant: (weight: "f64"), +) #let sg_qubo_sol = sg_qubo.solutions.at(0) #reduction-rule("SpinGlass", "QUBO", example: true, + example-source-variant: (graph: "SimpleGraph", weight: "f64"), + example-target-variant: (weight: "f64"), example-caption: [10-spin Ising model on Petersen graph], extra: [ #pred-commands( "pred create --example SpinGlass -o spinglass.json", - "pred reduce spinglass.json --to " + target-spec(sg_qubo) + " -o bundle.json", + "pred reduce spinglass.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate spinglass.json --config " + sg_qubo_sol.source_config.map(str).join(","), + "pred evaluate spinglass.json --config " + cli-config(sg_qubo_sol.source_config), ) Source: $n = #spin-num-spins(sg_qubo.source.instance)$ spins, $h_i = 0$, couplings $J_(i j) in {plus.minus 1}$ \ Mapping: $s_i = 2x_i - 1$ converts spins ${-1, +1}$ to binary ${0, 1}$ \ - Canonical ground-state witness: $bold(x) = (#sg_qubo_sol.target_config.map(str).join(", "))$ #sym.checkmark + Canonical ground-state witness: $bold(x) = (#fmt-values(sg_qubo_sol.target_config))$ #sym.checkmark ], )[ The Ising model and QUBO are both quadratic functions over finite domains: spins ${-1,+1}$ and binary variables ${0,1}$, respectively. The affine map $s_i = 2x_i - 1$ establishes a bijection between the two domains and preserves the quadratic structure. Substituting into the Ising Hamiltonian yields a QUBO objective that differs from the original energy by a constant, so ground states correspond exactly. @@ -12069,17 +11976,15 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead #let cvp_qubo_sol = cvp_qubo.solutions.at(0) #{ let basis = cvp_qubo.source.instance.basis - let bounds = cvp_qubo.source.instance.bounds let target = cvp_qubo.source.instance.target - let offsets = cvp_qubo_sol.source_config - let coords = offsets.enumerate().map(((i, off)) => off + bounds.at(i).lower) + let coords = cvp_qubo_sol.source_config let matrix = cvp_qubo.target.instance.matrix let bits = cvp_qubo_sol.target_config - let lo = bounds.map(b => b.lower) - let anchor = range(target.len()).map(d => lo.enumerate().fold(0.0, (acc, (i, x)) => acc + x * basis.at(i).at(d))) + let lower = (-23, -14) + let anchor = range(target.len()).map(d => lower.enumerate().fold(0.0, (acc, (i, x)) => acc + x * basis.at(i).at(d))) let constant = range(target.len()).fold(0.0, (acc, d) => acc + calc.pow(anchor.at(d) - target.at(d), 2)) - let qubo-value = range(bits.len()).fold(0.0, (acc, i) => acc + if bits.at(i) == 0 { 0.0 } else { - range(bits.len() - i).fold(0.0, (row-acc, delta) => row-acc + if bits.at(i + delta) == 0 { 0.0 } else { matrix.at(i).at(i + delta) }) + let qubo-value = range(bits.len()).fold(0.0, (acc, i) => acc + if bits.at(i) == false { 0.0 } else { + range(bits.len() - i).fold(0.0, (row-acc, delta) => row-acc + if bits.at(i + delta) == false { 0.0 } else { matrix.at(i).at(i + delta) }) }) let fmt-vec(v) = $paren.l #v.map(e => str(e)).join(", ") paren.r^top$ let rounded-constant = calc.round(constant, digits: 2) @@ -12088,44 +11993,38 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead [ #reduction-rule("ClosestVectorProblem", "QUBO", example: true, - example-caption: [2D bounded CVP with two 3-bit exact-range encodings], + example-caption: [2D standard CVP with a coefficient box derived by the reduction], extra: [ #pred-commands( "pred create --example CVP -o cvp.json", - "pred reduce cvp.json --to " + target-spec(cvp_qubo) + " -o bundle.json", + "pred reduce cvp.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate cvp.json --config " + cvp_qubo_sol.source_config.map(str).join(","), + "pred evaluate cvp.json --config " + cli-config(cvp_qubo_sol.source_config), ) - *Step 1 -- Source instance.* The canonical CVP example uses basis columns $bold(b)_1 = #fmt-vec(basis.at(0))$ and $bold(b)_2 = #fmt-vec(basis.at(1))$, target $bold(t) = #fmt-vec(target)$, and bounds $x_1, x_2 in [#bounds.at(0).lower, #bounds.at(0).upper]$. + *Step 1 -- Source instance.* The canonical CVP example has basis columns $bold(b)_1=#fmt-vec(basis.at(0))$ and $bold(b)_2=#fmt-vec(basis.at(1))$ and target $bold(t)=#fmt-vec(target)$. The source model supplies no coefficient bounds. - *Step 2 -- Exact bounded encoding.* Each variable has #bounds.at(0).upper - bounds.at(0).lower + 1 admissible values, so the implementation uses the capped binary basis $(1, 2, 3)$ rather than $(1, 2, 4)$: the first two bits are powers of two, and the last weight is capped so every bit pattern reconstructs an offset in ${0, dots, 6}$. Thus - $ x_1 = #bounds.at(0).lower + z_0 + 2 z_1 + 3 z_2, quad x_2 = #bounds.at(1).lower + z_3 + 2 z_4 + 3 z_5 $ - giving #cvp_qubo.target.instance.num_vars QUBO variables in total. + *Step 2 -- Derive a safe box.* Here $A=((2,1),(0,2))$, $norm(bold(t))_1=5$, and the selected-row bounds are $bold(C)=(8,7)$. Since $op("adj")(A)=((2,-1),(0,2))$, the reduction obtains $M_1=23$ and $M_2=14$. - *Step 3 -- Build the QUBO.* For this instance, $G = A^top A = ((4, 2), (2, 5))$ and $h = A^top bold(t) = (5.6, 5.8)^top$. Expanding the shifted quadratic form yields the exported upper-triangular matrix with representative entries $Q_(0,0) = #matrix.at(0).at(0)$, $Q_(0,1) = #matrix.at(0).at(1)$, $Q_(0,2) = #matrix.at(0).at(2)$, $Q_(2,5) = #matrix.at(2).at(5)$, and $Q_(5,5) = #matrix.at(5).at(5)$. + *Step 3 -- Encode and expand.* The exact-range weights are $(1,2,4,8,16,15)$ for $x_1+23 in [0,46]$ and $(1,2,4,8,13)$ for $x_2+14 in [0,28]$, giving #cvp_qubo.target.instance.num_vars variables. With $G=B^top B=((4,2),(2,5))$ and $h=B^top bold(t)=(6,7)^top$, representative coefficients are $Q_(0,0)=#matrix.at(0).at(0)$, $Q_(0,1)=#matrix.at(0).at(1)$, $Q_(0,6)=#matrix.at(0).at(6)$, and $Q_(6,6)=#matrix.at(6).at(6)$. - *Step 4 -- Verify a solution.* The fixture stores the canonical witness $bold(z) = (#bits.map(str).join(", "))$, which extracts to source offsets $bold(c) = (#offsets.map(str).join(", "))$ and actual lattice coordinates $bold(x) = (#coords.map(str).join(", "))$. The QUBO value is $bold(z)^top Q bold(z) = #rounded-qubo$; adding back the dropped constant #rounded-constant yields the original squared distance #(rounded-distance-sq), so the extracted point is the closest lattice vector #sym.checkmark. + *Step 4 -- Verify a solution.* The fixture stores $bold(z)=(#fmt-values(bits))$, which decodes to $bold(x)=(#fmt-values(coords))$. The QUBO value is #rounded-qubo; adding the dropped constant #rounded-constant gives squared CVP distance #rounded-distance-sq, so $B bold(x)=bold(t)$ #sym.checkmark. - *Multiplicity.* Offset $3$ has two bit encodings ($(0, 0, 1)$ and $(1, 1, 0)$), so the fixture stores one canonical witness even though the QUBO has multiple optimal binary assignments representing the same CVP solution. + *Multiplicity.* Residual final weights make some offsets have multiple encodings, so the fixture stores one canonical bit vector although other optimal QUBO witnesses can decode to the same $bold(x)$. ], )[ - A bounded Closest Vector Problem instance already supplies a finite integer box $x_i in [ell_i, u_i]$ for each coefficient. Following the direct quadratic-form reduction of Canale, Qureshi, and Viola @canale2023qubo, encoding each offset $c_i = x_i - ell_i$ with an exact in-range binary basis turns the squared-distance objective into an unconstrained quadratic over binary variables. Unlike penalty-method encodings, no auxiliary feasibility penalty is needed: every bit pattern decodes to a legal coefficient vector by construction. + Following the quadratic formulation of Canale, Qureshi, and Viola @canale2023qubo, this rule derives a finite box containing a global minimizer of standard CVP, then encodes that box and expands the squared-distance objective. ][ - _Construction._ Let $A in ZZ^(m times n)$ be the basis matrix with columns $bold(a)_1, dots, bold(a)_n$, let $bold(t) in RR^m$ be the target, and let $x_i in [ell_i, u_i]$ with range $r_i = u_i - ell_i$. Define $L_i = ceil(log_2(r_i + 1))$ when $r_i > 0$ and omit bits when $r_i = 0$. For each variable, introduce binary variables $z_(i,0), dots, z_(i,L_i-1)$ with exact-range weights - $ w_(i,p) = 2^p quad (0 <= p < L_i - 1), quad w_(i,L_i-1) = r_i + 1 - 2^(L_i - 1) $ - so that every bit vector represents an offset in ${0, dots, r_i}$. Then - $ x_i = ell_i + sum_(p=0)^(L_i-1) w_(i,p) z_(i,p) $ - and the total number of QUBO variables is $N = sum_i L_i$, exactly the exported overhead `num_vars = num_encoding_bits`. + _Construction._ Let $B in ZZ^(m times n)$ have full column rank and $bold(t) in ZZ^m$. Select $n$ rows forming an invertible matrix $A$, with source row indices $r_j$. Since zero is a candidate, every minimizer $bold(x)^*$ satisfies $norm(B bold(x)^*-bold(t))_2 <= norm(bold(t))_2$. For $bold(y)=A bold(x)^*$, define $C_j=abs(t_(r_j))+norm(bold(t))_1$. Then $abs(y_j)<=C_j$, and $bold(x)^*=op("adj")(A)bold(y)/det(A)$ gives + $ abs(x_i^*) <= M_i = sum_j abs(op("adj")(A)_(i,j)) C_j $ + because the nonzero integer determinant has magnitude at least one. - Let $G = A^top A$ and $h = A^top bold(t)$. Writing $bold(x) = bold(ell) + B bold(z)$ for the encoding matrix $B in RR^(n times N)$ gives - $ norm(A bold(x) - bold(t))_2^2 = bold(z)^top (B^top G B) bold(z) + 2 bold(z)^top B^top (G bold(ell) - h) + "const" $ - where the constant $norm(A bold(ell) - bold(t))_2^2$ is dropped. Therefore the QUBO coefficients are - $ Q_(u,u) = (B^top G B)_(u,u) + 2 (B^top (G bold(ell) - h))_u, quad Q_(u,v) = 2 (B^top G B)_(u,v) quad (u < v) $ - using the usual upper-triangular convention. + Encode $x_i+M_i in [0,2M_i]$ with powers of two and one capped final weight. If $W$ maps the resulting bits to coefficient offsets, $G=B^top B$, $h=B^top bold(t)$, and $bold(ell)=-bold(M)$, then + $ norm(B bold(x)-bold(t))_2^2 = bold(z)^top(W^top G W)bold(z) + 2 bold(z)^top W^top(G bold(ell)-h) + "const". $ + The constant is dropped. The exact bit count depends on concrete entries, so its symbolic transform is unavailable. - _Correctness._ ($arrow.r.double$) Every binary vector $bold(z) in {0,1}^N$ decodes to a coefficient vector $bold(x)$ inside the prescribed bounds because each exact-range basis reaches only offsets in ${0, dots, r_i}$. Substituting this decoding into the CVP objective yields $bold(z)^top Q bold(z) + "const"$, so any QUBO minimizer maps to a bounded CVP minimizer. ($arrow.l.double$) Every bounded CVP solution $bold(x)$ has at least one bit encoding for each coordinate offset, hence at least one binary vector $bold(z)$ with the same objective value up to the dropped constant. Thus the minimizers correspond exactly, although several binary witnesses may decode to the same CVP solution. + _Correctness._ ($arrow.r.double$) Every bit vector decodes inside the derived box and has QUBO value equal to its CVP squared distance minus one common constant, so a QUBO minimizer is best within the box. ($arrow.l.double$) The derived box contains a global CVP minimizer, and every point in the box has an exact-range encoding. Therefore the best encoded point is globally optimal for CVP. - _Solution extraction._ For each source variable, sum its selected encoding weights to recover the source configuration offset $c_i = x_i - ell_i$. This is exactly the configuration format expected by the `ClosestVectorProblem` model. + _Solution extraction._ Sum the selected weights for each coefficient and subtract $M_i$. ] ] } @@ -12144,9 +12043,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(kc_qubo.source) + " -o kcoloring.json", - "pred reduce kcoloring.json --to " + target-spec(kc_qubo) + " -o bundle.json", + "pred reduce kcoloring.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate kcoloring.json --config " + kc_qubo_sol.source_config.map(str).join(","), + "pred evaluate kcoloring.json --config " + cli-config(kc_qubo_sol.source_config), ) #{ let hg = house-graph() @@ -12162,76 +12061,63 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 1 -- Encode each color choice as a binary variable.* A coloring assigns each vertex one of $k$ colors. To express this in binary, introduce $k$ indicator variables per vertex: $x_(v,c) = 1$ means "vertex $v$ gets color $c$." For the house graph with $k = 3$, this gives $n k = 5 times 3 = 15$ QUBO variables: $ underbrace(x_(0,0) x_(0,1) x_(0,2), "vertex 0") #h(4pt) underbrace(x_(1,0) x_(1,1) x_(1,2), "vertex 1") #h(4pt) dots.c #h(4pt) underbrace(x_(4,0) x_(4,1) x_(4,2), "vertex 4") $ - *Step 2 -- Penalize invalid color assignments (one-hot constraint).* A valid coloring requires each vertex to have _exactly one_ color, i.e.\ $sum_c x_(v,c) = 1$. The penalty $(1 - sum_c x_(v,c))^2$ equals zero when exactly one variable is 1, and is positive otherwise. Weighted by $P_1 = 1 + n = 6$, this contributes diagonal entries $Q_(v k+c, v k+c) = -6$ and off-diagonal entries $Q_(v k+c_1, v k+c_2) = 12$ between colors of the same vertex. These form the $5 times 5$ diagonal blocks of $Q$.\ + *Step 2 -- Penalize invalid color assignments (one-hot constraint).* A valid coloring requires each vertex to have _exactly one_ color, i.e.\ $sum_c x_(v,c) = 1$. The penalty $(1 - sum_c x_(v,c))^2$ equals zero when exactly one variable is 1, and is positive otherwise. We multiply the entire former half-integral objective by 2. With $P = 1 + n = 6$, the one-hot term $2P(1 - sum_c x_(v,c))^2$ contributes diagonal entries $Q_(v k+c, v k+c) = -12$ and off-diagonal entries $Q_(v k+c_1, v k+c_2) = 24$ between colors of the same vertex. These form the $5 times 5$ diagonal blocks of $Q$.\ - *Step 3 -- Penalize same-color neighbors (edge conflict).* For each edge $(u,v) in E$ and each color $c$, the product $x_(u,c) x_(v,c) = 1$ iff both endpoints receive color $c$ — exactly the coloring conflict we want to forbid. The penalty $P_2 dot x_(u,c) x_(v,c)$ with $P_2 = P_1 slash 2 = 3$ makes such conflicts costly. The house has 6 edges, each contributing 3 color-conflict penalties $arrow.r$ 18 off-diagonal entries of value $3$ in $Q$.\ + *Step 3 -- Penalize same-color neighbors (edge conflict).* For each edge $(u,v) in E$ and each color $c$, the product $x_(u,c) x_(v,c) = 1$ iff both endpoints receive color $c$ — exactly the coloring conflict we want to forbid. In the scaled integer objective, each conflict has coefficient $P = 6$. The house has 6 edges, each contributing 3 color-conflict penalties $arrow.r$ 18 off-diagonal entries of value $6$ in $Q$.\ - *Step 4 -- Verify a solution.* The first valid 3-coloring is $(c_0, ..., c_4) = (#kc_qubo_sol.source_config.map(str).join(", "))$, shown in the figure above. The one-hot encoding is $bold(x) = (#kc_qubo_sol.target_config.map(str).join(", "))$. Check: each 3-bit group has exactly one 1 (valid one-hot #sym.checkmark), and for every edge the two endpoints have different colors (e.g.\ edge $0 dash 1$: colors $#kc_qubo_sol.source_config.at(0), #kc_qubo_sol.source_config.at(1)$ #sym.checkmark).\ + *Step 4 -- Verify a solution.* The first valid 3-coloring is $(c_0, ..., c_4) = (#fmt-values(kc_qubo_sol.source_config))$, shown in the figure above. The one-hot encoding is $bold(x) = (#fmt-values(kc_qubo_sol.target_config))$. Check: each 3-bit group has exactly one 1 (valid one-hot #sym.checkmark), and for every edge the two endpoints have different colors (e.g.\ edge $0 dash 1$: colors $#kc_qubo_sol.source_config.at(0), #kc_qubo_sol.source_config.at(1)$ #sym.checkmark).\ *Multiplicity:* The fixture stores one canonical coloring witness. The house graph has $3! times 3 = 18$ valid colorings overall: the triangle $2 dash 3 dash 4$ forces 3 distinct colors ($3! = 6$ permutations), and for each, the base vertices $0, 1$ have exactly $3$ compatible ordered pairs. ], )[ - The $k$-coloring problem has two requirements: each vertex gets exactly one color, and adjacent vertices get different colors. Both can be expressed as quadratic penalties over binary variables. Introduce $n k$ binary variables $x_(v,c) in {0,1}$ (indexed by $v dot k + c$), where $x_(v,c) = 1$ means vertex $v$ receives color $c$. The first requirement becomes a _one-hot constraint_ penalizing vertices with zero or multiple colors; the second becomes an _edge conflict penalty_ penalizing same-color neighbors. The combined QUBO matrix $Q in RR^(n k times n k)$ encodes both penalties. + The $k$-coloring problem has two requirements: each vertex gets exactly one color, and adjacent vertices get different colors. Both can be expressed as quadratic penalties over binary variables. Introduce $n k$ binary variables $x_(v,c) in {0,1}$ (indexed by $v dot k + c$), where $x_(v,c) = 1$ means vertex $v$ receives color $c$. The first requirement becomes a _one-hot constraint_ penalizing vertices with zero or multiple colors; the second becomes an _edge conflict penalty_ penalizing same-color neighbors. The combined integer QUBO matrix $Q in ZZ^(n k times n k)$ encodes both penalties. ][ _Construction._ Applying the penalty method (@sec:penalty-method), the two requirements translate into two penalty terms: - $ f(bold(x)) = underbrace(P_1 sum_(v in V) (1 - sum_(c=1)^k x_(v,c))^2, "one-hot: exactly one color per vertex") + underbrace(P_2 sum_((u,v) in E) sum_(c=1)^k x_(u,c) x_(v,c), "edge conflict: neighbors differ") $ + $ f(bold(x)) = underbrace(2P sum_(v in V) (1 - sum_(c=1)^k x_(v,c))^2, "one-hot: exactly one color per vertex") + underbrace(P sum_((u,v) in E) sum_(c=1)^k x_(u,c) x_(v,c), "edge conflict: neighbors differ") $ _One-hot expansion._ The constraint $(1 - sum_c x_(v,c))^2$ penalizes any vertex with $!= 1$ active color. Expanding using $x_(v,c)^2 = x_(v,c)$ (binary variables): $ (1 - sum_c x_(v,c))^2 = 1 - sum_c x_(v,c) + 2 sum_(c_1 < c_2) x_(v,c_1) x_(v,c_2) $ - Reading off the QUBO coefficients: diagonal $Q_(v k+c, v k+c) = -P_1$ (favors assigning a color) and intra-vertex off-diagonal $Q_(v k+c_1, v k+c_2) = 2 P_1$ for $c_1 < c_2$ (discourages multiple colors). + Reading off the QUBO coefficients: diagonal $Q_(v k+c, v k+c) = -2P$ (favors assigning a color) and intra-vertex off-diagonal $Q_(v k+c_1, v k+c_2) = 4P$ for $c_1 < c_2$ (discourages multiple colors). - _Edge conflict._ For each edge $(u,v)$ and color $c$, the product $x_(u,c) x_(v,c)$ equals 1 iff both endpoints share color $c$. The penalty $P_2 x_(u,c) x_(v,c)$ adds $P_2$ to $Q_(u k+c, v k+c)$ (with appropriate index ordering). + _Edge conflict._ For each edge $(u,v)$ and color $c$, the product $x_(u,c) x_(v,c)$ equals 1 iff both endpoints share color $c$. The penalty $P x_(u,c) x_(v,c)$ adds $P$ to $Q_(u k+c, v k+c)$ (with appropriate index ordering). - In our implementation, $P_1 = P = 1 + n$ and $P_2 = P\/2$. The penalty $P_1$ exceeds the number of vertices, ensuring that any constraint violation outweighs any objective gain. + In our implementation, $P = 1 + n$ and the stored integer objective is twice the equivalent half-integral formulation: one-hot coefficients are scaled from $(-P, 2P)$ to $(-2P, 4P)$ and edge-conflict coefficients from $P\/2$ to $P$. Multiplication by the positive constant 2 preserves the complete argmin set. - _Correctness._ ($arrow.r.double$) If $bold(x)$ violates any one-hot constraint (some vertex has 0 or $>= 2$ colors), the penalty $P_1 > n$ exceeds the objective range, so $bold(x)$ is not a minimizer. ($arrow.l.double$) Among valid one-hot encodings, $f$ reduces to the edge conflict term, minimized when no two adjacent vertices share a color — exactly the $k$-coloring objective. + _Correctness._ The stored QUBO energy is $E(bold(x)) = f(bold(x)) - 2P n$. Every term of $f$ is nonnegative, so $E(bold(x)) >= -2P n$, with equality precisely when every vertex has one color and every edge has differently colored endpoints (@lucas2014, Section 6.1). + ($arrow.r.double$) Given a proper coloring, set exactly its indicated color bit at each vertex. Both penalty sums vanish, so its QUBO energy is $-2P n$, attaining the global lower bound. + ($arrow.l.double$) If a target configuration has energy $-2P n$, each nonnegative penalty vanishes. Every vertex therefore has a unique selected color, and the edge penalties imply a proper source coloring. If the graph has no proper coloring, every target configuration has energy strictly greater than $-2P n$; an optimal target configuration alone is not a coloring certificate. - _Solution extraction._ For each vertex $v$, find $c$ with $x_(v,c) = 1$. + _Aggregation and extraction._ Map a finite target optimum equal to $-2P n$ to true, and every other target value to false. Validate a target configuration once and apply this same equality test before reading its unique selected color in each row. In particular, reject one-hot configurations with monochromatic edges, as well as rows with zero or multiple selected colors. The omitted constant and matrix dimensions are computed with checked integer arithmetic before allocation. For $n = 0$ the empty coloring attains energy zero, including $k = 0$; for $n > 0$, $k = 0$, the empty target configuration has energy zero greater than the negative threshold and certifies no coloring. ] #reduction-rule("MaximumSetPacking", "QUBO")[ - Set packing selects mutually disjoint sets of maximum total weight. Two sets conflict if and only if they share a universe element — the same adjacency structure as an independent set on the _intersection graph_. This reduction builds the intersection graph implicitly and applies the IS penalty method directly: each set becomes a QUBO variable, diagonal entries reward selection, and off-diagonal entries penalize pairs of overlapping sets with a penalty large enough to forbid any overlap. + Encode set selection with one binary variable per set and penalize overlapping pairs. The construction supports positive, zero, and negative weights. ][ - _Construction._ Given sets $S_1, ..., S_m$ with weights $w_1, ..., w_m$, introduce binary variables $x_i in {0,1}$ for each set. Two sets $S_i, S_j$ _conflict_ iff $S_i inter S_j != emptyset$. The packing objective is: maximize $sum_i w_i x_i$ subject to $x_i x_j = 0$ for every conflicting pair. Applying the penalty method (@sec:penalty-method): - $ f(bold(x)) = -sum_i w_i x_i + P sum_(S_i inter S_j != emptyset, thin i < j) x_i x_j $ - with $P = 1 + sum_i w_i$. The QUBO coefficients are: diagonal $Q_(i i) = -w_i$ (reward for selecting set $S_i$), off-diagonal $Q_(i j) = P$ for each conflicting pair $i < j$ (penalty for overlap). + _Construction._ Given sets $S_1, ..., S_m$ with finite real weights $w_1, ..., w_m$, let $M = max({1} union {w_i : 1 <= i <= m})$ and $P = 2M$. Introduce binary variables $x_i$ and minimize + $ f(bold(x)) = -sum_i w_i x_i + P sum_(i < j, thin S_i inter S_j != emptyset) x_i x_j. $ + Thus $Q_(i i) = -w_i$ and $Q_(i j) = P$ for conflicting pairs $i < j$, with all other entries zero. The floating-point implementation rejects a non-finite computed penalty; it never emits an infinite coefficient. As with the source and target real-weight models, numerical evaluation uses finite approximate arithmetic. + + _Correctness._ First suppose an assignment selects a set $S_i$ that conflicts with $d >= 1$ other selected sets. Removing $S_i$ changes the energy by $w_i - P d < 0$, since $P > max(0, w_i)$. Therefore every minimizer is a packing, including when some weights are zero or negative. Repeating such removals turns any conflicting assignment into a packing of strictly lower energy. - _Correctness._ ($arrow.r.double$) If $bold(x)$ encodes a maximum-weight packing, all selected sets are mutually disjoint, so all penalty terms vanish and $f(bold(x)) = -sum_(i in cal(P)) w_i$. Any assignment selecting overlapping sets incurs penalty $P > sum_i w_i$, making it suboptimal. ($arrow.l.double$) Among feasible assignments (no overlapping sets selected), the penalty terms vanish and $f(bold(x)) = -sum_(i in cal(P)) w_i$, minimized exactly when $cal(P)$ is a maximum-weight packing. + ($arrow.r.double$) A maximum-weight packing has energy equal to the negative optimum packing weight. Every other packing has at least this energy, and every conflicting assignment has higher energy than some packing. Hence every maximum-weight packing minimizes $f$. - _Solution extraction._ Return $bold(x)$ directly — each $x_i = 1$ indicates set $S_i$ is in the packing. + ($arrow.l.double$) Every minimizer is conflict-free by the deletion argument. On conflict-free assignments $f(bold(x)) = -sum_i w_i x_i$, so a minimizer must maximize the packing weight. This holds for every target minimizer, not just one choice among ties. Empty sets and an empty family require no special construction. + + _Solution extraction._ Return $bold(x)$ directly. There are exactly $m$ target variables. ] #reduction-rule("KSatisfiability", "QUBO")[ - Each clause in a $k$-SAT formula is falsified by exactly one assignment to its literals. For $k = 2$, this falsifying pattern is a product of two (possibly complemented) binary variables — already quadratic, so each clause maps directly to QUBO terms. For $k = 3$, the falsifying pattern $y_1 y_2 y_3$ is cubic; Rosenberg quadratization replaces the product $y_1 y_2$ with an auxiliary variable $a$, enforced by a penalty that makes $a != y_1 y_2$ suboptimal. The total QUBO counts unsatisfied clauses, so minimizers maximize satisfiability. + Clause falsification penalties become a quadratic objective using Rosenberg quadratization. Retain its omitted constant to decode the SAT decision, rather than interpreting an arbitrary QUBO configuration as a satisfying assignment. ][ - *Case $k = 2$.* - - _Construction._ Each 2-literal clause has exactly one falsifying assignment (both literals false). The penalty for that assignment is a quadratic function of $x_i, x_j$: + _Construction._ Let $n$ be the number of source variables and $m$ the clause count. For each literal let $y$ be its falsity indicator: $y=1-x$ for a positive literal and $y=x$ for a negative one. For widths zero, one and two, the clause penalty is respectively $1$, $y_1$, and $y_1 y_2$. For width three use + $ H = a y_3 + 2(y_1 y_2 - 2 y_1 a - 2 y_2 a + 3a). $ + Rosenberg's penalty is zero when $a=y_1 y_2$ and positive otherwise @borosgruber2014quadratization. Its multiplier 2 strictly exceeds the possible unit gain from the $a y_3$ term. Thus minimizing over each independent auxiliary gives exactly the clause falsification indicator, including repeated or complementary literals. Expand with $x^2=x$ into an upper-triangular QUBO matrix, and retain the omitted constant $C$. - #table( - columns: (auto, auto, auto, auto), - inset: 4pt, - align: left, - table.header([*Clause*], [*Falsified when*], [*Penalty*], [*QUBO contributions*]), - [$x_i or x_j$], [$x_i=0, x_j=0$], [$(1-x_i)(1-x_j)$], [$Q_(i i) -= 1, Q_(j j) -= 1, Q_(i j) += 1$], - [$overline(x_i) or x_j$], [$x_i=1, x_j=0$], [$x_i(1-x_j)$], [$Q_(i i) += 1, Q_(i j) -= 1$], - [$x_i or overline(x_j)$], [$x_i=0, x_j=1$], [$(1-x_i)x_j$], [$Q_(j j) += 1, Q_(i j) -= 1$], - [$overline(x_i) or overline(x_j)$], [$x_i=1, x_j=1$], [$x_i x_j$], [$Q_(i j) += 1$], - ) + _Correctness._ For every source assignment, the minimum target energy over auxiliaries is the number of falsified clauses minus $C$. Every clause expression is nonnegative before subtracting $C$. Hence the formula is satisfiable iff the global target minimum is exactly $-C$. A satisfying assignment lifts by setting each cubic auxiliary to $y_1 y_2$; every zero-penalty target configuration projects to a satisfying source assignment. If an empty clause occurs, its constant penalty 1 prevents the threshold from being attained. Empty formulas have $C=0$ and every source assignment satisfies them. - Summing over all clauses, $f(bold(x)) = sum_j "penalty"_j (bold(x))$ counts falsified clauses. + _Extraction._ Formal target validation precedes decoding. The registered aggregate decoder maps `Min(E)` to `Or(E == Some(-C))`; direct witness extraction rejects other energies and reads the first $n$ coordinates only at the threshold. It does not solve SAT or repair auxiliary assignments. Target optimality must be established before interpreting an aggregate result as the source decision. - _Correctness._ ($arrow.r.double$) Each penalty term is non-negative and equals 1 exactly when its clause is falsified. If $bold(x)$ satisfies all clauses, $f(bold(x)) = 0$. ($arrow.l.double$) Any minimizer of $f$ achieves the fewest falsified clauses, hence maximizes satisfiability. - - *Case $k = 3$ (Rosenberg quadratization).* - - _Construction._ For each clause $(ell_1 or ell_2 or ell_3)$, define complement variables $y_i = overline(ell_i)$ (so $y_i = x_i$ if the literal is negated, $y_i = 1 - x_i$ if positive). The clause is violated when $y_1 y_2 y_3 = 1$. This cubic penalty is reduced to quadratic form by introducing an auxiliary variable $a$ and the substitution $a = y_1 y_2$, enforced via a Rosenberg penalty with weight $M$: - $ H = a dot y_3 + M (y_1 y_2 - 2 y_1 a - 2 y_2 a + 3a) $ - where $M = 2$ suffices. Each clause adds one auxiliary variable (indices $n, n+1, ..., n+m-1$), so the total QUBO has $n + m$ variables. - - _Correctness._ ($arrow.r.double$) If $a = y_1 y_2$, the Rosenberg penalty term vanishes and $H = y_1 y_2 y_3$ counts the clause violation faithfully. ($arrow.l.double$) If $a != y_1 y_2$, the penalty $M(dots.c) >= 1$ strictly exceeds the clause-counting contribution (at most 1), so any minimizer must have $a = y_1 y_2$ for every clause. Among such assignments, $H$ counts unsatisfied clauses, and minimizers maximize satisfiability. - - _Solution extraction._ Discard auxiliary variables: return $bold(x)[0..n]$. + _Domain and overhead._ The K2 variant has $n$ variables; K3 reserves $n+m$, with unused auxiliaries mathematically free for short clauses. Native `new_allow_less` permits widths up to K; CLI and serde still require exactly K per actual clause. Source `Or` maps to target `Min` with an explicitly stored signed threshold. Matrix and constant accumulation use checked arithmetic; literal indices come from the formal `CNFClause::variables` API. Neither endpoint nor variant changes. ] #let ksat_qc = load-example("KSatisfiability", "QuadraticCongruences") @@ -12242,8 +12128,8 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_qc.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_qc) + " -o bundle.json", - "pred evaluate ksat.json --config " + ksat_qc_sol.source_config.map(str).join(","), + "pred reduce ksat.json --via route.json -o bundle.json", + "pred evaluate ksat.json --config " + cli-config(ksat_qc_sol.source_config), ) #{ @@ -12252,49 +12138,58 @@ where $P$ is a penalty weight large enough that any constraint violation costs m let c = ksat_qc.target.instance.c let witness-bits = ksat_qc_sol.target_config.len() [ - *Step 1 -- Source instance.* The canonical formula is the single clause $(x_1 or x_2 or x_3)$, witnessed by the satisfying assignment $(#ksat_qc_sol.source_config.map(str).join(", "))$. + *Step 1 -- Source instance.* The canonical formula is the single clause $(x_1 or x_2 or x_3)$, witnessed by the satisfying assignment $(#fmt-values(ksat_qc_sol.source_config))$. *Step 2 -- Enumerate standard clauses.* With $l = 3$ active variables, the construction lists all $M = 8$ signed 3-clauses on ${x_1, x_2, x_3}$. This yields $N = 2M + l = 19$ lifted coefficients in the doubled knapsack encoding. *Step 3 -- Lift by CRT.* Using $N+1 = 20$ odd primes starting at 13, the reduction builds the CRT gadgets $theta_0, dots, theta_N$ and outputs $(a, b, c)$. In the canonical fixture these numbers have $#a.len()$, $#b.len()$, and $#c.len()$ decimal digits respectively, so the paper reports their sizes rather than expanding them inline. - *Step 4 -- Verify the stored witness.* The example DB keeps the target witness in binary using $#witness-bits$ bits. Evaluating that witness satisfies $x^2 equiv a mod b$ with $1 <= x < c$, and extraction recovers the original source assignment $(#ksat_qc_sol.source_config.map(str).join(", "))$ #sym.checkmark. + *Step 4 -- Verify the stored witness.* The example DB keeps the target witness in binary using $#witness-bits$ bits. Evaluating that witness satisfies $x^2 equiv a mod b$ with $1 <= x < c$, and extraction recovers the original source assignment $(#fmt-values(ksat_qc_sol.source_config))$ #sym.checkmark. ] } *Multiplicity:* The fixture stores one canonical satisfying witness. ], )[ - Manders and Adleman's number-theoretic reduction encodes a 3-SAT assignment as a pattern of signs $alpha_j in {-1, +1}$ in a bounded knapsack-style congruence, then uses carefully chosen prime powers and the Chinese Remainder Theorem to realize those signs as divisibility conditions on $H - x$ and $H + x$. Squaring removes the sign ambiguity and yields one quadratic congruence $x^2 equiv a mod b$ together with an explicit bound $x < c$. The bound is essential: without it, the composite-modulus quadratic residuosity problem becomes much easier once the factorization of $b$ is known. + The bounded quadratic-congruence construction of Manders and Adleman @mandersAdleman1976 converts an odd signed-knapsack target into a square congruence using prime powers and the Chinese Remainder Theorem. We give base-eight positions only to the actual normalized clauses. The same slack equation handles every native clause width from zero through three. ][ - _Construction._ Given a 3-CNF formula $phi$ with $n$ variables, first deduplicate clauses and restrict to the active variables. Enumerate all signed 3-clauses over those variables as $sigma_1, dots, sigma_M$. Define the base-8 clause weight $8^j$ for $sigma_j$, form $tau_phi = -sum_(sigma_j in phi) 8^j$, and for each variable compute the positive and negative occurrence sums $f_i^+$ and $f_i^-$. In doubled form, set $N = 2M + l$ and coefficients - $ d_0 = 2, quad d_(2k-1) = -8^k, quad d_(2k) = -2 dot 8^k, quad d_(2M+i) = f_i^+ - f_i^- $ - together with - $ tau_2 = 2 tau_phi + sum_(j=0)^N d_j + 2 sum_(i=1)^l f_i^- $ - modulo $2 dot 8^(M+1)$. + _Construction._ Delete tautologies and duplicate literals and clauses, retaining empty clauses. Compact the $l$ occurring variables, recording their original positions. Let $C_1, dots, C_m$ be the remaining clauses, $r_j = |C_j|$, $N = 2m+l+1$, and $D=8^(m+1)$. Let $f_i^+$ and $f_i^-$ be the sums of $8^j$ over clauses containing variable $i$ positively and negatively. Define $N$ integer coefficients and an odd target: + $ c_0=1, quad c_(2j-1)=-8^j/2, quad c_(2j)=-8^j, quad c_(2m+i)=(f_i^+-f_i^-)/2, $ + $ tau=1+sum_(j=1)^m (r_j-5)8^j/2. $ + Take the first $N$ primes starting at $13$, indexed from zero, and put $q_i=p_i^N$ and $K=product_i q_i$. Let $theta_i$ be the least positive integer satisfying $theta_i equiv c_i mod D$, $theta_i equiv 0 mod (K/q_i)$, and $p_i$ not dividing $theta_i$. CRT gives a residue modulo $D K/q_i$; choosing the first positive representative and, if necessary, adding that modulus once gives $theta_i$. Set $H=sum_i theta_i$ and + $ b=2D K, quad a=(2D+K)^(-1)(K tau^2+2D H^2) mod b, quad c=H+1. $ + The target's formal domain is $0= 13$ and let $K = product_(j=0)^N p_j^(N+1)$. For each $j$, construct $theta_j$ so that - $ theta_j equiv d_j mod 2 dot 8^(M+1), quad theta_j equiv 0 mod product_(i != j) p_i^(N+1) $ - and $p_j$ does not divide $theta_j$. Set $H = sum_j theta_j$, $b = 2 dot 8^(M+1) dot K$, and - $ a = (2 dot 8^(M+1) + K)^(-1) (K tau_2^2 + 2 dot 8^(M+1) H^2) mod b, quad c = H + 1. $ + _Magnitude and square-root lemmas._ Each $theta_i<=2D K/q_i$. For $m>=1$, $N>=2m+1>=3$ and $13^N>4N D$, giving $2H0$, and witnesses $0=0$. In particular $h^2>=a$, so neither NO branch applies. Since $x^2<=h^2 0$, which requires $1 <= x <= h = c - 1$. + _Backward direction._ For a positive solution of the nonconstant target, $x^2=C-b y equiv a mod b$ and + $ x^2<=C-b<=h^2, $ + hence $1<=x<=h 0$, and $y = p - k$ is a positive integer. So $(x_0, y)$ is a solution to $x^2 + b y = c'$. ($arrow.l.double$) If $(x, y)$ satisfies $x^2 + b y = c'$ with $x, y >= 1$, then $x^2 = c' - b y equiv c' mod b equiv a mod b$ (since $c' = a + b p$). Also $x^2 < c' = a + b p <= h^2 + b$, and since $y >= 1$ we have $x^2 = c' - b y <= c' - b < h^2 + b - b = h^2$, so $x <= h < c$. Thus $x$ is a valid congruence witness, and the original formula is satisfiable. + _Endpoint equality._ The inequality $C-b<=h^2$ can be equality and must not be replaced by a strict inequality. For the empty source formula, the upstream instance is $(a,b,c)=(1,208,2)$; the equation is $x^2+208y=209$. Its solution $x=y=1$ has $x=h$ and is valid at both endpoints. - _Solution extraction._ Decode the Diophantine witness $x$ from its little-endian binary encoding. Then extract a 3-SAT assignment by passing $x$ through the congruence-to-SAT extraction (sign recovery from divisibility by prime powers). + _Solution extraction and bounds._ Pass the native `BigUint` witness directly to the validated congruence extractor; no binary-vector conversion is part of the reduction. Its positive-$y$ check and the displayed inequalities cover arbitrary target witnesses, including $x=h$, zero, and out-of-bound integers. If the preceding common bit-length bound is $L=64(2c_s+s+1)^2+3c_s+4$, the new coefficient bit lengths are bounded by $1$, $L$, and $2L+1$, since $C<=h^2+b$; the fixed NO instance obeys the same bounds. Here $s,c_s$ are the original variable and clause counts. All computed integers have arbitrary precision, and no endpoint or variant changes. ] #let ksat_ss = load-example("KSatisfiability", "SubsetSum") @@ -12305,9 +12200,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_ss.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_ss) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_ss_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_ss_sol.source_config), ) Source: $n = #ksat_ss.source.instance.num_vars$ variables, $m = #sat-num-clauses(ksat_ss.source.instance)$ clauses \ Target: #subsetsum-num-elements(ksat_ss.target.instance) elements, target $= #ksat_ss.target.instance.target$ \ @@ -12346,35 +12241,35 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example SubsetSum -o subsetsum.json", - "pred reduce subsetsum.json --to " + target-spec(ss-cvp) + " -o bundle.json", + "pred reduce subsetsum.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate subsetsum.json --config " + ss-cvp-sol.source_config.map(str).join(","), + "pred evaluate subsetsum.json --config " + cli-config(ss-cvp-sol.source_config), ) - *Step 1 -- Source instance.* The canonical Subset Sum instance has sizes $(#ss-cvp-sizes.map(str).join(", "))$ and target $B = #ss-cvp-target$. + *Step 1 -- Source instance.* The canonical Subset Sum instance has sizes $(#fmt-values(ss-cvp-sizes))$ and target $B = #ss-cvp-target$. *Step 2 -- Build the lattice.* The reduction creates the basis - $ bold(B) = #to-mat(ss-cvp-basis) $ - together with target $ bold(t) = (#ss-cvp-target-vec.map(str).join(", "))^top $ - and binary bounds $x_i in {0,1}$ for all $#ss-cvp-n$ coordinates. + $ bold(B)^top = #to-mat(ss-cvp-basis) $ + together with target $ bold(t) = (#fmt-values(ss-cvp-target-vec))^top $ + in the standard CVP model, with no coefficient bounds. - *Step 3 -- Verify the canonical witness.* The fixture stores $bold(x) = (#ss-cvp-x.map(str).join(", "))$, which selects sizes $3$ and $8$ and therefore satisfies $3 + 8 = #ss-cvp-target$. Since $bold(B) bold(x) = (1, 0, 0, 1, #ss-cvp-target)^top$, the difference vector is $(0.5, -0.5, -0.5, 0.5, 0)^top$ and the Euclidean distance is $sqrt(#ss-cvp-n / 4) = 1$. + *Step 3 -- Verify the canonical witness.* The fixture stores coefficients $(#fmt-values(ss-cvp-x))$. Its first four entries select sizes $3$ and $8$, and the final three are carry coefficients. The first coordinate block has residual $(1,0,0,1)$, the second has $(0,-1,-1,0)$, and all bit-equation residuals are zero. Thus the Euclidean distance is $sqrt(4) = 2$. *Witness semantics.* The example DB stores one canonical minimizer. This source instance also has another satisfying subset, $(1, 1, 1, 0)$, so the reduction has multiple optimal CVP witnesses even though only one is serialized. ], )[ - Classical lattice embedding for Subset Sum following Lagarias and Odlyzko @lagarias1985, with the $1/2$-target CVP formulation in the style of Coster et al. @coster1992. For an instance with $n$ elements, the reduction produces $n$ basis vectors in ambient dimension $n + 1$: the first $n$ coordinates enforce binary structure and the last coordinate records the subset sum error. + The classical exact-distance embedding @micciancio2001 enforces binary coefficients through a minimum squared-distance threshold. To retain arbitrary-precision source integers in the existing integer-basis CVP model, this construction uses paired coordinates and binary carry equations. Its basis entries are only $-2$, $0$, and $1$. ][ - _Construction._ Given sizes $s_0, dots, s_(n-1) in ZZ^+$ and target $B in ZZ^+$, define one basis vector per element: - $ bold(b)_i = bold(e)_i + s_i bold(e)_(n+1) $ - for $i in {0, dots, n-1}$. Equivalently, the basis matrix has columns $bold(b)_0, dots, bold(b)_(n-1)$, so its first $n$ rows form the identity matrix and its last row is $(s_0, dots, s_(n-1))$. Set the target vector to - $ bold(t) = (1/2, dots, 1/2, B)^top $ - and restrict every CVP variable to $x_i in {0, 1}$. + _Construction._ Let $n$ be the number of items, and let $b >= 1$ be the maximum bit length of their nonnegative sizes and target $T$. Write $s_(i,j), t_j in {0,1}$ for bit $j$ of size $s_i$ and target $T$. Introduce integer coefficients $x_0, dots, x_(n-1)$ and carries $c_1, dots, c_(b-1)$, with fixed boundary values $c_0=c_b=0$. The displacement vector consists of $x_i$ for all items, then $x_i-1$ for all items, then residuals + $ r_j = sum_(i=0)^(n-1) s_(i,j) x_i + c_j - 2 c_(j+1) - t_j $ + in descending bit order. These linear expressions define the integer basis columns and a target containing only zeros and ones. Carry columns are also ordered by descending bit index. The first $n$ coordinate rows form an identity on item columns; the remaining carry block has unit pivots in this order. Consequently the full-column-rank check has no exponentially growing pivots. + + _Correctness._ Every integer vector satisfies + $ norm(bold(B) bold(z)-bold(t))_2^2 = sum_i (x_i^2 + (x_i-1)^2) + sum_j r_j^2 >= n. $ + ($arrow.r.double$) For a binary subset summing to $T$, ordinary integer addition gives carries $0 <= c_j <= n$ satisfying all bit equations and both boundaries. Its squared distance equals $n$. ($arrow.l.double$) Squared distance at most $n$ forces each $x_i in {0,1}$ and each $r_j=0$. Multiplying the bit equations by $2^j$ and summing cancels the internal carries, yielding $sum_i s_i x_i=T$. Thus the optimum is $sqrt(n)$ exactly for YES instances. Empty item lists and target zero use the same construction. - _Correctness._ ($arrow.r.double$) If $bold(x) in {0,1}^n$ is a satisfying Subset Sum solution, then $sum_i s_i x_i = B$ and - $ norm(bold(B) bold(x) - bold(t))_2^2 = sum_(i=0)^(n-1) (x_i - 1/2)^2 + (sum_i s_i x_i - B)^2 = n/4. $ - Hence every satisfying subset becomes a CVP solution at distance $sqrt(n / 4)$. ($arrow.l.double$) Conversely, binary bounds force every CVP candidate to lie in ${0,1}^n$. The first $n$ coordinates always contribute exactly $n/4$ to the squared distance, so a CVP minimizer attains distance $sqrt(n/4)$ if and only if the last coordinate contributes $0$, i.e. $sum_i s_i x_i = B$. When the Subset Sum instance is unsatisfiable, every binary vector has strictly larger distance. + _Solution extraction._ Validate the target configuration once and require a finite distance exactly $sqrt(n)$ through the formal aggregate certificate. Return the first $n$ coefficients as Boolean selections, accepting one as true; the remaining coefficients are the specified carries. A larger optimal distance proves NO and provides no source witness. - _Solution extraction._ Return the binary CVP vector unchanged. + _Representation._ The target has $2n+b$ coordinates and $n+b-1$ basis columns. Since bit length is not a registered Subset Sum parameter, the symbolic relations are marked unavailable with that reason. Dimensions and the total dense basis byte count are checked before allocation. On a 64-bit platform this bounds $n < 2^30$; the threshold and the unit squared-distance gap remain distinguishable in the target's floating-point evaluation. The paired coordinates and boundary carry equations also ensure every threshold witness has exactly evaluated small integer residuals. The solver uses exact rational sphere-enumeration bounds; runtime limitations are separate from the mathematical equivalence. ] ] } @@ -12382,21 +12277,19 @@ where $P$ is a penalty weight large enough that any constraint violation costs m // Removed: SubsetSum → CapacityAssignment (unsound reduction, #1006) #reduction-rule("ILP", "QUBO")[ - A binary ILP optimizes a linear objective over binary variables subject to linear constraints. The penalty method converts each equality constraint $bold(a)_k^top bold(x) = b_k$ into the quadratic penalty $(bold(a)_k^top bold(x) - b_k)^2$, which is zero if and only if the constraint is satisfied. Inequality constraints are first converted to equalities using binary slack variables with powers-of-two coefficients. The resulting unconstrained quadratic over binary variables is a QUBO whose matrix $Q$ combines the negated objective (as diagonal terms) with the expanded constraint penalties (as a Gram matrix $A^top A$). + Binary ILP constraints are encoded by squared integer residuals with binary slack variables, following the penalty construction discussed by Lucas @lucas2014[Section 3]. The rule retains the omitted constant and uses a certified energy interval to recover the original objective direction or report infeasibility. No assumption that the ILP is feasible is needed. ][ - _Construction._ First, normalize all constraints to equalities. Inequalities $bold(a)_k^top bold(x) <= b_k$ become $bold(a)_k^top bold(x) + sum_(s=0)^(S_k - 1) 2^s y_(k,s) = b_k$ where $S_k = ceil(log_2 (b_k + 1))$ binary slack bits. For $>=$ constraints, the slack has a negative sign. The extended system is $A' bold(x)' = bold(b)$ with $bold(x)' = (bold(x), bold(y)) in {0,1}^(n')$. For minimization, negate $bold(c)$ to convert to maximization. + _Construction._ Let the normalized source have $n$ binary variables, rows $a_k x diamond_k b_k$, and original objective coefficients $g_i$. Define the minimization-oriented objective $d=g$ for a minimizing source and $d=-g$ for a maximizing source. For a $<=$ row set $R_k=b_k-sum_i min(a_(k i),0)$, and for a $>=$ row set $R_k=sum_i max(a_(k i),0)-b_k$. Use $S_k=ceil(log_2(R_k+1))$ bits if $R_k>0$, and zero bits otherwise; equality rows have zero slack bits. Add powers-of-two slack with positive sign for $<=$ and negative sign for $>=$. A negative range means the row is impossible on binary assignments; leaving it as an equality still has no zero-residual assignment. A zero range makes equality equivalent to the inequality. A positive range represents every slack needed by a feasible source assignment. Larger representable slacks cannot create spurious zero-residual assignments. + + Write the extended rows as $A' z=b$, where the first $n$ bits of $z$ are $x$ and the remaining bits are row-specific slack. Set $P=1+sum_i |d_i|+sum_k |b_k|$, $C=P sum_k b_k^2$, $L=sum_i min(d_i,0)$, and $U=sum_i max(d_i,0)$. The upper-triangular QUBO matrix has diagonal $Q_(i i)=d_i+P sum_k ((a'_(k i))^2-2 b_k a'_(k i))$ and off-diagonal $Q_(i j)=2P sum_k a'_(k i)a'_(k j)$ for $i= 1$ and the penalty $P > ||bold(c)||_1$ exceeds the entire objective range, so $bold(x)'$ cannot be a QUBO minimizer. Among feasible assignments (all penalties zero), $f$ reduces to $-bold(c')^top bold(x)'$, minimized at the ILP optimum. + _Backward direction and optimization._ For any configuration with nonzero residual, integrality gives a squared-residual sum at least one. Thus $E+C>=L+P>U$, since $P>U-L=sum_i |d_i|$. Such a configuration cannot lie in the stored interval or beat a zero-residual configuration. An energy in the interval therefore has zero residual and projects to a feasible source assignment. When a feasible source exists, minimizing target energy minimizes $d^top x$, which gives the original optimum after reversing the sign for a maximizing source. When no feasible source exists, every target configuration lies above the interval; a finite QUBO optimum is then correctly mapped to an absent source value. - _Solution extraction._ Discard slack variables: return $bold(x)' [0..n]$. + _Aggregate and solution extraction._ The concrete source value is `Extremum` and the target is `Min`. The custom aggregate accepts only energies in $[L-C,U-C]$, adds $C$, and reverses the sign for a maximizing source; it returns the corresponding absent `Extremum` otherwise. Checked interval construction guarantees the addition is representable; the strict objective-magnitude penalty bound also makes sign reversal safe. The direct extractor evaluates the target exactly once, reuses this predicate, and returns the first $n$ bits as integer zero/one values only for a zero-residual certificate. Wrong slack choices and infeasible source projections are rejected with a formal extraction error. Target evaluation errors remain errors, not infeasibility claims. ] #let part_cpi = load-example("Partition", "CosineProductIntegration") @@ -12423,7 +12316,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m #let part_ks_n = part_ks_sizes.len() #let part_ks_total = part_ks_sizes.fold(0, (a, b) => a + b) #let part_ks_capacity = part_ks.target.instance.capacity -#let part_ks_selected = part_ks_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let part_ks_selected = part_ks_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let part_ks_selected_sizes = part_ks_selected.map(i => part_ks_sizes.at(i)) #let part_ks_selected_sum = part_ks_selected_sizes.fold(0, (a, b) => a + b) #reduction-rule("Partition", "Knapsack", @@ -12432,16 +12325,16 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(part_ks.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_ks) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_ks_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_ks_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#part_ks_sizes.map(str).join(", "))$ with total sum $S = #part_ks_total$, so a balanced witness must hit exactly $S / 2 = #part_ks_capacity$. + *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#fmt-values(part_ks_sizes))$ with total sum $S = #part_ks_total$, so a balanced witness must hit exactly $S / 2 = #part_ks_capacity$. - *Step 2 -- Build the knapsack instance.* The reduction copies each size into both the weight and the value list, producing weights $(#part_ks.target.instance.weights.map(str).join(", "))$, values $(#part_ks.target.instance.values.map(str).join(", "))$, and capacity $C = #part_ks_capacity$. No auxiliary variables are introduced, so the target has the same $#part_ks_n$ binary coordinates as the source. + *Step 2 -- Build the knapsack instance.* The reduction copies each size into both the weight and the value list, producing weights $(#fmt-values(part_ks.target.instance.weights))$, values $(#fmt-values(part_ks.target.instance.values))$, and capacity $C = #part_ks_capacity$. No auxiliary variables are introduced, so the target has the same $#part_ks_n$ binary coordinates as the source. - *Step 3 -- Verify the canonical witness.* The serialized witness uses the same binary vector on both sides, $bold(x) = (#part_ks_sol.source_config.map(str).join(", "))$. It selects elements at indices $\{#part_ks_selected.map(str).join(", ")\}$ with sizes $(#part_ks_selected_sizes.map(str).join(", "))$, so the chosen subset has total weight and value $#part_ks_selected_sum = #part_ks_capacity$. Hence the knapsack solution saturates the capacity and certifies a balanced partition. + *Step 3 -- Verify the canonical witness.* The serialized witness uses the same binary vector on both sides, $bold(x) = (#fmt-values(part_ks_sol.source_config))$. It selects elements at indices $\{#fmt-values(part_ks_selected)\}$ with sizes $(#fmt-values(part_ks_selected_sizes))$, so the chosen subset has total weight and value $#part_ks_selected_sum = #part_ks_capacity$. Hence the knapsack solution saturates the capacity and certifies a balanced partition. *Witness semantics.* The example DB stores one canonical balanced subset. This instance has multiple balanced partitions because several different subsets sum to $#part_ks_capacity$, but one witness is enough to demonstrate the reduction. ], @@ -12465,7 +12358,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m #let part_ss_n = part_ss_sizes.len() #let part_ss_total = part_ss_sizes.fold(0, (a, b) => a + b) #let part_ss_target = part_ss_total / 2 -#let part_ss_selected = part_ss_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let part_ss_selected = part_ss_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let part_ss_selected_sizes = part_ss_selected.map(i => part_ss_sizes.at(i)) #let part_ss_selected_sum = part_ss_selected_sizes.fold(0, (a, b) => a + b) #reduction-rule("Partition", "SubsetSum", @@ -12474,16 +12367,16 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(part_ss.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_ss) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_ss_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_ss_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#part_ss_sizes.map(str).join(", "))$ with total sum $S = #part_ss_total$, so a balanced witness must hit exactly $S / 2 = #part_ss_target$. + *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#fmt-values(part_ss_sizes))$ with total sum $S = #part_ss_total$, so a balanced witness must hit exactly $S / 2 = #part_ss_target$. - *Step 2 -- Build the Subset Sum instance.* The reduction copies the sizes directly: $(#part_ss_sizes.map(str).join(", "))$, and sets the target $B = S / 2 = #part_ss_target$. The number of binary variables is unchanged ($n = #part_ss_n$). + *Step 2 -- Build the Subset Sum instance.* The reduction copies the sizes directly: $(#fmt-values(part_ss_sizes))$, and sets the target $B = S / 2 = #part_ss_target$. The number of binary variables is unchanged ($n = #part_ss_n$). - *Step 3 -- Verify the canonical witness.* The serialized witness uses the same binary vector on both sides, $bold(x) = (#part_ss_sol.source_config.map(str).join(", "))$. It selects elements at indices $\{#part_ss_selected.map(str).join(", ")\}$ with sizes $(#part_ss_selected_sizes.map(str).join(", "))$, so the chosen subset sums to $#part_ss_selected_sum = #part_ss_target = B$ #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The serialized witness uses the same binary vector on both sides, $bold(x) = (#fmt-values(part_ss_sol.source_config))$. It selects elements at indices $\{#fmt-values(part_ss_selected)\}$ with sizes $(#fmt-values(part_ss_selected_sizes))$, so the chosen subset sums to $#part_ss_selected_sum = #part_ss_target = B$ #sym.checkmark. *Witness semantics.* The example DB stores one canonical balanced subset. Multiple subsets may sum to $B$, but one witness suffices to demonstrate the reduction. ], @@ -12505,7 +12398,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m #let part_ifwm_n = part_ifwm_sizes.len() #let part_ifwm_total = part_ifwm_sizes.fold(0, (a, b) => a + b) #let part_ifwm_half = part_ifwm_total / 2 -#let part_ifwm_selected = part_ifwm_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let part_ifwm_selected = part_ifwm_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let part_ifwm_selected_sizes = part_ifwm_selected.map(i => part_ifwm_sizes.at(i)) #let part_ifwm_source_arcs = part_ifwm_sol.target_config.slice(0, part_ifwm_n) #let part_ifwm_relay_arcs = part_ifwm_sol.target_config.slice(part_ifwm_n, 2 * part_ifwm_n) @@ -12516,16 +12409,16 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(part_ifwm.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_ifwm) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_ifwm_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_ifwm_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Partition multiset is $(#part_ifwm_sizes.map(str).join(", "))$, so the total is $S = #part_ifwm_total$ and any balanced witness must sum to $S / 2 = #part_ifwm_half$. + *Step 1 -- Source instance.* The canonical Partition multiset is $(#fmt-values(part_ifwm_sizes))$, so the total is $S = #part_ifwm_total$ and any balanced witness must sum to $S / 2 = #part_ifwm_half$. - *Step 2 -- Build the relay network.* The reduction creates vertices $s$, one item vertex $v_i$ per element, a relay vertex $w$, and sink $t$. It adds unit-capacity arcs $(s, v_i)$, item arcs $(v_i, w)$ with capacities $(#part_ifwm_sizes.map(str).join(", "))$, and one bottleneck arc $(w, t)$ with capacity $#part_ifwm_half$. The target witness therefore has $#part_ifwm_sol.target_config.len()$ arc-flow coordinates ordered as source arcs, relay arcs, then the bottleneck arc. + *Step 2 -- Build the relay network.* The reduction creates vertices $s$, one item vertex $v_i$ per element, a relay vertex $w$, and sink $t$. It adds unit-capacity arcs $(s, v_i)$, item arcs $(v_i, w)$ with capacities $(#fmt-values(part_ifwm_sizes))$, and one bottleneck arc $(w, t)$ with capacity $#part_ifwm_half$. The target witness therefore has $#part_ifwm_sol.target_config.len()$ arc-flow coordinates ordered as source arcs, relay arcs, then the bottleneck arc. - *Step 3 -- Verify the canonical witness.* The source witness $bold(x) = (#part_ifwm_sol.source_config.map(str).join(", "))$ selects item indices $\{#part_ifwm_selected.map(str).join(", ")\}$ with sizes $(#part_ifwm_selected_sizes.map(str).join(", "))$, summing to $#part_ifwm_half$. On the target side, the source arcs carry $(#part_ifwm_source_arcs.map(str).join(", "))$, the relay arcs carry $(#part_ifwm_relay_arcs.map(str).join(", "))$, and the bottleneck arc carries $#part_ifwm_bottleneck$. Thus the relay receives $#part_ifwm_selected_sizes.map(str).join(" + ") = #part_ifwm_half$ units and the sink inflow equals the requirement #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The source witness $bold(x) = (#fmt-values(part_ifwm_sol.source_config))$ selects item indices $\{#fmt-values(part_ifwm_selected)\}$ with sizes $(#fmt-values(part_ifwm_selected_sizes))$, summing to $#part_ifwm_half$. On the target side, the source arcs carry $(#fmt-values(part_ifwm_source_arcs))$, the relay arcs carry $(#fmt-values(part_ifwm_relay_arcs))$, and the bottleneck arc carries $#part_ifwm_bottleneck$. Thus the relay receives $#part_ifwm_selected_sizes.map(str).join(" + ") = #part_ifwm_half$ units and the sink inflow equals the requirement #sym.checkmark. *Witness semantics.* The fixture stores one canonical balanced subset. Other balanced subsets may exist, but every feasible target witness still extracts by reading the first $n$ unit-capacity source arcs. ], @@ -12550,7 +12443,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m #let ks_qubo_num_items = ks_qubo.source.instance.weights.len() #let ks_qubo_num_slack = ks_qubo.target.instance.num_vars - ks_qubo_num_items #let ks_qubo_penalty = 1 + ks_qubo.source.instance.values.fold(0, (a, b) => a + b) -#let ks_qubo_selected = ks_qubo_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let ks_qubo_selected = ks_qubo_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let ks_qubo_sel_weight = ks_qubo_selected.fold(0, (a, i) => a + ks_qubo.source.instance.weights.at(i)) #let ks_qubo_sel_value = ks_qubo_selected.fold(0, (a, i) => a + ks_qubo.source.instance.values.at(i)) #reduction-rule("Knapsack", "QUBO", @@ -12559,11 +12452,11 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example Knapsack -o knapsack.json", - "pred reduce knapsack.json --to " + target-spec(ks_qubo) + " -o bundle.json", + "pred reduce knapsack.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate knapsack.json --config " + ks_qubo_sol.source_config.map(str).join(","), + "pred evaluate knapsack.json --config " + cli-config(ks_qubo_sol.source_config), ) - *Step 1 -- Source instance.* The canonical knapsack instance has weights $(#ks_qubo.source.instance.weights.map(str).join(", "))$, values $(#ks_qubo.source.instance.values.map(str).join(", "))$, and capacity $C = #ks_qubo.source.instance.capacity$. + *Step 1 -- Source instance.* The canonical knapsack instance has weights $(#fmt-values(ks_qubo.source.instance.weights))$, values $(#fmt-values(ks_qubo.source.instance.values))$, and capacity $C = #ks_qubo.source.instance.capacity$. *Step 2 -- Introduce slack variables.* The inequality $sum_i w_i x_i lt.eq C$ becomes an equality by adding $B = #ks_qubo_num_slack$ binary slack bits that encode unused capacity: $ #ks_qubo.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) + #range(ks_qubo_num_slack).map(j => $#calc.pow(2, j) s_#j$).join($+$) = #ks_qubo.source.instance.capacity $ @@ -12573,9 +12466,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m $ H = -(#ks_qubo.source.instance.values.enumerate().map(((i, v)) => $#v x_#i$).join($+$)) + #ks_qubo_penalty (#ks_qubo.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) + #range(ks_qubo_num_slack).map(j => $#calc.pow(2, j) s_#j$).join($+$) - #ks_qubo.source.instance.capacity)^2 $ so any violation of the equality is more expensive than the entire knapsack value range. - *Step 4 -- Verify a solution.* The QUBO ground state $bold(z) = (#ks_qubo_sol.target_config.map(str).join(", "))$ extracts to the knapsack choice $bold(x) = (#ks_qubo_sol.source_config.map(str).join(", "))$. This selects items $\{#ks_qubo_selected.map(str).join(", ")\}$ with total weight $#ks_qubo_selected.map(i => str(ks_qubo.source.instance.weights.at(i))).join(" + ") = #ks_qubo_sel_weight$ and total value $#ks_qubo_selected.map(i => str(ks_qubo.source.instance.values.at(i))).join(" + ") = #ks_qubo_sel_value$, so the slack bits are all zero and the penalty term vanishes #sym.checkmark. + *Step 4 -- Verify a solution.* The QUBO ground state $bold(z) = (#fmt-values(ks_qubo_sol.target_config))$ extracts to the knapsack choice $bold(x) = (#fmt-values(ks_qubo_sol.source_config))$. This selects items $\{#fmt-values(ks_qubo_selected)\}$ with total weight $#ks_qubo_selected.map(i => str(ks_qubo.source.instance.weights.at(i))).join(" + ") = #ks_qubo_sel_weight$ and total value $#ks_qubo_selected.map(i => str(ks_qubo.source.instance.values.at(i))).join(" + ") = #ks_qubo_sel_value$, so the slack bits are all zero and the penalty term vanishes #sym.checkmark. - *Uniqueness:* The fixture stores one canonical optimal witness. The source optimum is unique because items $\{#ks_qubo_selected.map(str).join(", ")\}$ are the only feasible selection achieving value #ks_qubo_sel_value. + *Uniqueness:* The fixture stores one canonical optimal witness. The source optimum is unique because items $\{#fmt-values(ks_qubo_selected)\}$ are the only feasible selection achieving value #ks_qubo_sel_value. ], )[ For a standard 0-1 Knapsack instance with nonnegative weights, nonnegative values, and nonnegative capacity, the inequality $sum_i w_i x_i lt.eq C$ is converted to equality using binary slack variables that encode the unused capacity. When $C > 0$, one can take $B = floor(log_2 C) + 1$ slack bits; when $C = 0$, a single slack bit also suffices. The penalty method (@sec:penalty-method) combines the negated value objective with a quadratic constraint penalty, producing a QUBO with $n + B$ binary variables. @@ -12600,9 +12493,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example MinimumDiscretePlanarInverseKinematics -o ik.json", - "pred reduce ik.json --to " + target-spec(mdpik_qubo) + " -o bundle.json", + "pred reduce ik.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ik.json --config " + mdpik_qubo_sol.source_config.map(str).join(","), + "pred evaluate ik.json --config " + cli-config(mdpik_qubo_sol.source_config), ) *Step 1 -- Source instance.* The canonical instance has link lengths $(2, 1)$, target $g = (2, 1)$, sampled orientations $Phi_1 = Phi_2 = {0, pi / 2}$, and admissible pair set $A_2 = {(0,0), (0,1), (1,1)}$. @@ -12614,7 +12507,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m $ (2 y_(1,0) + y_(2,0) - 2)^2 + (2 y_(1,1) + y_(2,1) - 1)^2. $ The implementation adds the same safe penalty to every one-hot violation and every forbidden pair, here penalizing the single forbidden adjacency $(1, 0)$ between the two blocks. - *Step 4 -- Verify a solution.* The QUBO ground state $bold(y) = (#mdpik_qubo_sol.target_config.map(str).join(", "))$ decodes to source configuration $(#mdpik_qubo_sol.source_config.map(str).join(", "))$, i.e. link 1 uses angle $0$ and link 2 uses angle $pi / 2$. The end-effector reaches $(2, 1)$ exactly, so the squared distance is $0$ #sym.checkmark. + *Step 4 -- Verify a solution.* The QUBO ground state $bold(y) = (#fmt-values(mdpik_qubo_sol.target_config))$ decodes to source configuration $(#fmt-values(mdpik_qubo_sol.source_config))$, i.e. link 1 uses angle $0$ and link 2 uses angle $pi / 2$. The end-effector reaches $(2, 1)$ exactly, so the squared distance is $0$ #sym.checkmark. ], )[ Discrete planar inverse kinematics is already close to QUBO form: once each sampled orientation is lifted to a binary selector, both end-effector coordinates become linear functions of those selectors, and the squared Euclidean error expands to a quadratic polynomial. Adding quadratic one-hot penalties and quadratic penalties for forbidden consecutive orientation pairs yields a QUBO with $sum_(j=1)^n m_j$ variables @salloum2025ikqubo. @@ -12645,19 +12538,19 @@ where $P$ is a penalty weight large enough that any constraint violation costs m #let mwc_qubo_k = mwc_qubo_terminals.len() #let mwc_qubo_nq = mwc_qubo_n * mwc_qubo_k #let mwc_qubo_alpha = mwc_qubo_weights.fold(0, (a, w) => a + w) + 1 -#let mwc_qubo_cut_indices = mwc_qubo_sol.source_config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) +#let mwc_qubo_cut_indices = mwc_qubo_sol.source_config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) #let mwc_qubo_cut_cost = mwc_qubo_cut_indices.fold(0, (a, i) => a + mwc_qubo_weights.at(i)) #reduction-rule("MinimumMultiwayCut", "QUBO", example: true, - example-caption: [$n = #mwc_qubo_n$ vertices, $k = #mwc_qubo_k$ terminals $T = {#mwc_qubo_terminals.map(str).join(", ")}$, $|E| = #mwc_qubo_edges.len()$ edges], + example-caption: [$n = #mwc_qubo_n$ vertices, $k = #mwc_qubo_k$ terminals $T = {#fmt-values(mwc_qubo_terminals)}$, $|E| = #mwc_qubo_edges.len()$ edges], extra: [ #pred-commands( "pred create --example MinimumMultiwayCut -o minimummultiwaycut.json", - "pred reduce minimummultiwaycut.json --to " + target-spec(mwc_qubo) + " -o bundle.json", + "pred reduce minimummultiwaycut.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate minimummultiwaycut.json --config " + mwc_qubo_sol.source_config.map(str).join(","), + "pred evaluate minimummultiwaycut.json --config " + cli-config(mwc_qubo_sol.source_config), ) - *Step 1 -- Source instance.* The canonical graph has $n = #mwc_qubo_n$ vertices, $m = #mwc_qubo_edges.len()$ edges with weights $(#mwc_qubo_weights.map(str).join(", "))$, and $k = #mwc_qubo_k$ terminals $T = {#mwc_qubo_terminals.map(str).join(", ")}$. + *Step 1 -- Source instance.* The canonical graph has $n = #mwc_qubo_n$ vertices, $m = #mwc_qubo_edges.len()$ edges with weights $(#fmt-values(mwc_qubo_weights))$, and $k = #mwc_qubo_k$ terminals $T = {#fmt-values(mwc_qubo_terminals)}$. *Step 2 -- Introduce binary variables.* Assign $k = #mwc_qubo_k$ indicator variables per vertex: $x_(u,t) = 1$ means vertex $u$ belongs to terminal $t$'s component. This gives $n k = #mwc_qubo_n times #mwc_qubo_k = #mwc_qubo_nq$ QUBO variables: $ underbrace(x_(0,0) x_(0,1) x_(0,2), "vertex 0") #h(4pt) underbrace(x_(1,0) x_(1,1) x_(1,2), "vertex 1") #h(4pt) dots.c #h(4pt) underbrace(x_(4,0) x_(4,1) x_(4,2), "vertex 4") $ @@ -12668,7 +12561,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 5 -- Build $H_B$ (cut cost).* For each edge $(u,v)$ with weight $w$ and each pair $s != t$, add $w$ to $Q_(u k+s, v k+t)$. For example, edge $(0,1)$ with weight $2$ contributes $2$ to positions $(x_(0,0), x_(1,1))$, $(x_(0,0), x_(1,2))$, $(x_(0,1), x_(1,0))$, $(x_(0,1), x_(1,2))$, $(x_(0,2), x_(1,0))$, and $(x_(0,2), x_(1,1))$.\ - *Step 6 -- Verify a solution.* The QUBO ground state $bold(x) = (#mwc_qubo_sol.target_config.map(str).join(", "))$ decodes to the partition: vertex 0 in component 0, vertices 1--3 in component 1, vertex 4 in component 2. Cut edges: $\{#mwc_qubo_cut_indices.map(i => "(" + str(mwc_qubo_edges.at(i).at(0)) + "," + str(mwc_qubo_edges.at(i).at(1)) + ")").join(", ")\}$ with total weight #mwc_qubo_cut_indices.map(i => str(mwc_qubo_weights.at(i))).join(" + ") $= #mwc_qubo_cut_cost$ #sym.checkmark. + *Step 6 -- Verify a solution.* The QUBO ground state $bold(x) = (#fmt-values(mwc_qubo_sol.target_config))$ decodes to the partition: vertex 0 in component 0, vertices 1--3 in component 1, vertex 4 in component 2. Cut edges: $\{#mwc_qubo_cut_indices.map(i => "(" + str(mwc_qubo_edges.at(i).at(0)) + "," + str(mwc_qubo_edges.at(i).at(1)) + ")").join(", ")\}$ with total weight #mwc_qubo_cut_indices.map(i => str(mwc_qubo_weights.at(i))).join(" + ") $= #mwc_qubo_cut_cost$ #sym.checkmark. ], )[ The multiway cut problem requires a partition of vertices into $k$ components — one per terminal — minimizing the total weight of edges crossing components. The penalty method (@sec:penalty-method) encodes two constraints as QUBO penalties: (1) each vertex belongs to exactly one component (one-hot), and (2) each terminal is pinned to its own component. The cut-cost Hamiltonian counts edge weight across distinct components. Reference: @Heidari2022. @@ -12704,21 +12597,28 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ The $n$-variable QUBO assignment directly encodes the vertex-side assignment: $x_v = 0$ means side $A$, $x_v = 1$ means side $B$. ] -#let qubo_ilp = load-example("QUBO", "ILP") +#let qubo_ilp = load-example( + "QUBO", + "ILP", + source-variant: (weight: "f64"), + target-variant: (coefficient: "f64", variable: "bool"), +) #let qubo_ilp_sol = qubo_ilp.solutions.at(0) #reduction-rule("QUBO", "ILP", example: true, + example-source-variant: (weight: "f64"), + example-target-variant: (coefficient: "f64", variable: "bool"), example-caption: [4-variable QUBO with 3 quadratic terms], extra: [ #pred-commands( - "pred create --example QUBO -o qubo.json", - "pred reduce qubo.json --to " + target-spec(qubo_ilp) + " -o bundle.json", + "pred create --example QUBO/f64 -o qubo.json", + "pred reduce qubo.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate qubo.json --config " + qubo_ilp_sol.source_config.map(str).join(","), + "pred evaluate qubo.json --config " + cli-config(qubo_ilp_sol.source_config), ) Source: $n = #qubo_ilp.source.instance.num_vars$ binary variables, 3 off-diagonal terms \ - Target: #qubo_ilp.target.instance.num_vars ILP variables ($#qubo_ilp.source.instance.num_vars$ original $+ #(qubo_ilp.target.instance.num_vars - qubo_ilp.source.instance.num_vars)$ auxiliary), #qubo_ilp.target.instance.constraints.len() McCormick constraints \ - Canonical optimal witness: $bold(x) = (#qubo_ilp_sol.source_config.map(str).join(", "))$ #sym.checkmark + Target: #qubo_ilp.target.instance.variables.len() ILP variables ($#qubo_ilp.source.instance.num_vars$ original $+ #(qubo_ilp.target.instance.variables.len() - qubo_ilp.source.instance.num_vars)$ auxiliary), #qubo_ilp.target.instance.constraints.len() McCormick constraints \ + Canonical optimal witness: $bold(x) = (#fmt-values(qubo_ilp_sol.source_config))$ #sym.checkmark ], )[ QUBO minimizes a quadratic form $bold(x)^top Q bold(x)$ over binary variables. Every quadratic term $Q_(i j) x_i x_j$ can be _linearized_ by introducing an auxiliary variable $y_(i j)$ constrained to equal the product $x_i x_j$ via three McCormick inequalities. Diagonal terms $Q_(i i) x_i^2 = Q_(i i) x_i$ are already linear for binary $x_i$. The result is a binary ILP with a linear objective and $3 m$ constraints (where $m$ is the number of non-zero off-diagonal entries), whose minimizer corresponds exactly to the QUBO minimizer. @@ -12754,23 +12654,24 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example CircuitSAT -o circuitsat.json", - "pred reduce circuitsat.json --to " + target-spec(cs_ilp) + " -o bundle.json", + "pred reduce circuitsat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate circuitsat.json --config " + cs_ilp_sol.source_config.map(str).join(","), + "pred evaluate circuitsat.json --config " + cli-config(cs_ilp_sol.source_config), ) Circuit: #circuit-num-gates(cs_ilp.source.instance) gates (2 XOR, 2 AND, 1 OR), #circuit-num-variables(cs_ilp.source.instance) variables \ - Target: #cs_ilp.target.instance.num_vars ILP variables (circuit vars $+$ auxiliary), trivial objective \ + Target: #cs_ilp.target.instance.variables.len() ILP variables (circuit vars $+$ auxiliary), trivial objective \ Canonical feasible witness shown ($2^3$ valid input combinations exist for the full adder) #sym.checkmark ], )[ - Each boolean gate (AND, OR, NOT, XOR) has a truth table that can be captured exactly by a small set of linear inequalities over binary variables. By Tseitin-style flattening, each internal expression node gets an auxiliary ILP variable constrained to match its gate's output, so the conjunction of all gate constraints is feasible if and only if the circuit is satisfiable. The ILP has a trivial objective (minimize 0), making it a pure feasibility problem. + Each boolean gate (AND, OR, NOT, XOR) has a truth table that can be captured exactly by a small set of linear inequalities over binary variables. By Tseitin-style flattening, expression outputs and binary XOR fold steps get auxiliary ILP variables constrained to match their values, so the conjunction of all gate constraints is feasible if and only if the circuit is satisfiable. The ILP has a trivial objective (minimize 0), making it a pure feasibility problem. ][ - _Construction._ Recursively assign an ILP variable to each expression node. Named circuit variables keep their identity; internal nodes get auxiliary variables. + _Construction._ Pre-register named circuit variables in the source order. Recursively lower expression trees, allocating outputs for constants, NOT, AND and OR, and one output per binary XOR fold step. Multi-input XOR is a left fold; a single input is unchanged and an empty XOR uses the existing constant-false encoding. All variable allocations use checked index arithmetic. _Gate encodings_ (output $c$, inputs $a_1, ..., a_k$, all binary): - NOT: $c + a = 1$ - AND: $c <= a_i$ ($forall i$), $c >= sum a_i - (k - 1)$ - OR: $c >= a_i$ ($forall i$), $c <= sum a_i$ + - Constants: $c=0$ or $c=1$. Empty AND and OR are already covered by their formulas: $c>=1$ and $c<=0$, respectively. - XOR (binary, chained pairwise): $c <= a + b$, $c >= a - b$, $c >= b - a$, $c <= 2 - a - b$ _Objective._ Minimize $0$ (feasibility problem): any feasible solution satisfies the circuit. @@ -12790,9 +12691,13 @@ where $P$ is a penalty weight large enough that any constraint violation costs m & "all gate and input variables are binary". $ - _Correctness._ ($arrow.r.double$) Each gate encoding is the convex hull of the gate's truth table rows (viewed as binary vectors), so a satisfying circuit assignment satisfies all constraints. ($arrow.l.double$) Any binary feasible solution respects every gate's input-output relation, and since gates are composed in topological order, the full circuit evaluates to true. + For each named output $y$ of an assignment with expression result $c$, add $y=c$ unless the two already share a variable. No output is implicitly forced true. The standard AND, OR and NOT linearizations are documented in the MOSEK Modeling Cookbook, Section 9.1.9; the four XOR inequalities can be checked on the four input pairs. + + _Correctness._ ($arrow.r.double$) Fix a satisfying assignment of all source names, evaluate each expression tree, and give every auxiliary its expression or partial XOR value. Every gate row holds, and the named-output equalities hold by source feasibility. ($arrow.l.double$) With binary inputs, the gate rows determine their outputs exactly: AND is one precisely when every input is one, OR is zero precisely when every input is zero, and the four XOR rows force output zero on equal inputs and one on unequal inputs. Structural induction on each expression tree, followed by the named-output equalities, proves every source assignment constraint. Named assignments may form cycles; no topological ordering of them is assumed. + + _Solution extraction._ Validate the target assignment once and reject it if the ILP evaluation is infeasible. Otherwise read all named source variables in their original order. Since the objective is zero, every feasible ILP assignment is an admissible witness. - _Solution extraction._ Return values of the named circuit variables. + _Overhead._ For $v$ named variables, $t$ expression nodes and $o$ named outputs, the target has at most $v+2t$ variables and $5t+o$ constraints. Non-XOR outputs and empty XOR outputs contribute at most $t$ auxiliaries. Binary XOR steps contribute at most the number of expression-tree edges, hence at most $t$. Base gate rows contribute at most $t$, while input inequalities and binary XOR rows contribute at most four times the number of tree edges. Output equalities add at most $o$ rows. This accounts for arbitrarily many constant inputs: three constants under one XOR use five auxiliaries, exceeding the former one-auxiliary-per-node bound of four. ] == Non-Trivial Reductions @@ -12805,16 +12710,16 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example SAT -o sat.json", - "pred reduce sat.json --to " + target-spec(sat_mis) + " -o bundle.json", + "pred reduce sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate sat.json --config " + sat_mis_sol.source_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(sat_mis_sol.source_config), ) - SAT assignment: $(x_1, ..., x_5) = (#sat_mis_sol.source_config.map(str).join(", "))$ \ + SAT assignment: $(x_1, ..., x_5) = (#fmt-values(sat_mis_sol.source_config))$ \ IS graph: #graph-num-vertices(sat_mis.target.instance) vertices ($= 3 times #sat-num-clauses(sat_mis.source.instance)$ literals), #graph-num-edges(sat_mis.target.instance) edges \ IS of size #sat-num-clauses(sat_mis.source.instance) $= m$: one vertex per clause $arrow.r$ satisfying assignment #sym.checkmark ], )[ - @karp1972 A satisfying assignment must make at least one literal true in every clause, and different clauses cannot assign contradictory values to the same variable. These two requirements map naturally to an independent set problem: _intra-clause cliques_ force exactly one literal per clause to be selected, while _conflict edges_ between complementary literals across clauses enforce consistency. The target IS size equals the number of clauses $m$, so an IS of size $m$ exists iff the formula is satisfiable. + @karp1972 A satisfying assignment must make at least one literal true in every clause, and different clauses cannot assign contradictory values to the same variable. These two requirements map naturally to an independent set problem: _intra-clause cliques_ allow at most one literal per clause to be selected, while _conflict edges_ between complementary literals across clauses enforce consistency. The target IS size equals the number of clauses $m$, so an IS of size $m$ exists iff the formula is satisfiable. ][ _Construction._ For $phi = and.big_(j=1)^m C_j$ with $C_j = (ell_(j,1) or ... or ell_(j,k_j))$: @@ -12824,7 +12729,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Correctness._ ($arrow.r.double$) A satisfying assignment selects one true literal per clause; these vertices form an IS of size $m$ (no clause edges by selection, no conflict edges by consistency). ($arrow.l.double$) An IS of size $m$ must contain exactly one vertex per clause (by clause cliques); the corresponding literals are consistent (by conflict edges) and satisfy $phi$. - _Solution extraction._ For $v_(j,i) in S$ with literal $x_k$: set $x_k = 1$; for $overline(x_k)$: set $x_k = 0$. + _Solution extraction._ Validate the target configuration and require a finite independent-set value exactly $m$ before decoding. For $v_(j,i) in S$ with literal $x_k$, set $x_k = 1$; for $overline(x_k)$, set $x_k = 0$. Unselected variables may be set to false. The aggregate map returns true precisely at value $m$. An optimal value below $m$ proves that the source formula is unsatisfiable; it does not provide a satisfying assignment. An empty formula has threshold zero; any empty clause makes the threshold unattainable. + + _Representation and size._ The implementation checks that $m$ fits the exact target score type. Every feasible independent set has size at most $m$, so all partial sums of its unit weights also fit. Repeated literals remain distinct vertices. Complementary occurrences within a clause may add a duplicate edge; this does not affect feasibility. With $L = sum_j k_j$, the stored edge count is at most $2 binom(L, 2) <= L^2$, and the vertex count is exactly $L$. ] #let sat_kc = load-example("Satisfiability", "KColoring") @@ -12835,11 +12742,11 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example SAT -o sat.json", - "pred reduce sat.json --to " + target-spec(sat_kc) + " -o bundle.json", + "pred reduce sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate sat.json --config " + sat_kc_sol.source_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(sat_kc_sol.source_config), ) - SAT assignment: $(x_1, ..., x_5) = (#sat_kc_sol.source_config.map(str).join(", "))$ \ + SAT assignment: $(x_1, ..., x_5) = (#fmt-values(sat_kc_sol.source_config))$ \ Construction: 3 base + $2 times #sat_kc.source.instance.num_vars$ variable gadgets + OR-gadgets $arrow.r$ #graph-num-vertices(sat_kc.target.instance) vertices, #graph-num-edges(sat_kc.target.instance) edges \ Canonical 3-coloring witness shown (the construction also has the expected color-symmetry multiplicity for satisfying assignments) #sym.checkmark ], @@ -12863,22 +12770,24 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example SAT -o sat.json", - "pred reduce sat.json --to " + target-spec(sat_ds) + " -o bundle.json", + "pred reduce sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate sat.json --config " + sat_ds_sol.source_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(sat_ds_sol.source_config), ) - SAT assignment: $(x_1, ..., x_5) = (#sat_ds_sol.source_config.map(str).join(", "))$ \ + SAT assignment: $(x_1, ..., x_5) = (#fmt-values(sat_ds_sol.source_config))$ \ Vertex structure: $#graph-num-vertices(sat_ds.target.instance) = 3 times #sat_ds.source.instance.num_vars + #sat-num-clauses(sat_ds.source.instance)$ (variable triangles + clause vertices) \ Dominating set of size $n = #sat_ds.source.instance.num_vars$: one vertex per variable triangle #sym.checkmark ], )[ - @garey1979 Each variable is represented by a triangle whose three vertices correspond to the positive literal, negative literal, and a dummy. Any dominating set must include at least one vertex from each triangle to dominate the dummy. The clause vertices are connected only to the literal vertices that appear in the clause, so a dominating set of minimum size $n$ (one vertex per triangle) dominates all clause vertices iff the chosen literals satisfy every clause. + @garey1979 Each variable that occurs in the formula is represented by a triangle with positive, negative, and dummy vertices. Let $q$ be the number of occurring variables. Every dominating set needs at least one vertex from each triangle to dominate its dummy. A dominating set of size $q$ exists exactly when the formula is satisfiable. ][ - _Construction._ (1) _Variable triangle_ for $x_i$: vertices $"pos"_i = 3i$, $"neg"_i = 3i+1$, $"dum"_i = 3i+2$ forming a triangle. The dummy vertex $"dum"_i$ is adjacent only to $"pos"_i$ and $"neg"_i$, so it can only be dominated by a vertex from its own triangle. (2) _Clause vertex_ $c_j = 3n+j$ connected to $"pos"_i$ if $x_i in C_j$, to $"neg"_i$ if $overline(x_i) in C_j$. + _Construction._ List the occurring source variable indices in increasing order as $u_0, dots, u_(q-1)$. Variables absent from every clause require no gadget. For each dense index $i$, create $"pos"_i = 3i$, $"neg"_i = 3i+1$, and $"dum"_i = 3i+2$, joining them as a triangle. For clause $C_j$, create $c_j = 3q+j$ and join it to the positive or negative vertex of each literal occurrence in the clause. All vertex weights are one; repeated occurrences retain duplicate edge entries. + + _Correctness._ ($arrow.r.double$) Given a satisfying assignment, select one literal vertex per occurring variable according to its truth value. Each triangle is dominated, and every clause vertex has a selected true-literal neighbor. The set has size $q$. ($arrow.l.double$) To dominate the private dummy of each triangle, any dominating set must select at least one vertex from that triangle. A set of size $q$ therefore selects exactly one per triangle and no clause vertices. Every clause must consequently have a selected literal neighbor. Set a variable true precisely when its positive vertex is selected, and false when its negative or dummy vertex is selected. Every selected literal remains true, so all clauses are satisfied. Absent variables may also be set false because no clause refers to them. - _Correctness._ ($arrow.r.double$) Given a satisfying assignment, select $"pos"_i$ if $x_i = 1$, else $"neg"_i$. This dominates all triangle vertices (each triangle has one selected vertex adjacent to both others). Each clause $C_j$ has at least one true literal, so $c_j$ is adjacent to at least one selected vertex. Total size: $n$. ($arrow.l.double$) Any dominating set needs $>= 1$ vertex per triangle (to dominate $"dum"_i$). A set of size $n$ has exactly one per triangle. If $"dum"_i$ is selected, it does not dominate any clause vertex; but it does dominate $"pos"_i$ and $"neg"_i$, which still need to cover clauses. Since $"dum"_i$ has no clause neighbors, we can swap it for $"pos"_i$ or $"neg"_i$ without losing domination of the triangle. After swapping, each clause vertex $c_j$ must be dominated by some $"pos"_i$ or $"neg"_i$, defining a consistent satisfying assignment. + _Solution extraction._ Evaluate the target configuration once and require a finite value equal to $q$, using the same certificate as the aggregate map. Decode through the stored map from source indices to dense gadget indices. Invalid dominating configurations and values above $q$ provide no satisfying witness and are rejected. The empty formula has $q=m=0$ and an empty target; an empty clause creates an isolated clause vertex, forcing the optimum above $q$. - _Solution extraction._ Set $x_i = 1$ if $"pos"_i$ selected; $x_i = 0$ if $"neg"_i$ selected. + _Size and arithmetic._ For $n$ declared variables, $m$ clauses, and $L$ literal occurrences, the actual target has $3q+m <= 3n+m$ vertices and $3q+L <= 3n+L$ stored edges. Thus the registered formulas are upper bounds. Checked arithmetic computes the vertex count and verifies that every possible sum of unit weights fits #raw("i64"), including the all-selected configuration. Construction depends on occurring variables, so a large declared index alone does not allocate unused triangles. Producing the source assignment still requires its declared length. ] #let sat_ifha = load-example("Satisfiability", "IntegralFlowHomologousArcs") @@ -12889,11 +12798,11 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(sat_ifha.source) + " -o sat.json", - "pred reduce sat.json --to " + target-spec(sat_ifha) + " -o bundle.json", + "pred reduce sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate sat.json --config " + sat_ifha_sol.source_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(sat_ifha_sol.source_config), ) - SAT assignment: $(x_1, x_2, x_3) = (#sat_ifha_sol.source_config.map(str).join(", "))$ \ + SAT assignment: $(x_1, x_2, x_3) = (#fmt-values(sat_ifha_sol.source_config))$ \ Target network: $#sat_ifha.target.instance.graph.num_vertices$ vertices, $#sat_ifha.target.instance.graph.arcs.len()$ arcs, #sat_ifha.target.instance.homologous_pairs.len() homologous pairs, and $R = #sat_ifha.target.instance.requirement$ \ The stored flow witness gives bottleneck loads $1, 1, 1, 0$ across the four clause stages, so every stage respects its unit capacity #sym.checkmark @@ -12945,13 +12854,13 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example SAT -o sat.json", - "pred reduce sat.json --to " + target-spec(sat_ksat) + " -o bundle.json", + "pred reduce sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate sat.json --config " + sat_ksat_sol.source_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(sat_ksat_sol.source_config), ) Source: #sat_ksat.source.instance.num_vars variables, #sat-num-clauses(sat_ksat.source.instance) clauses (sizes 1, 2, 3, 3, 4, 5) \ Target 3-SAT: $#sat_ksat.target.instance.num_vars = #sat_ksat.source.instance.num_vars + 7$ variables, #sat-num-clauses(sat_ksat.target.instance) clauses (small padded, large split) \ - First solution: $(x_1, ..., x_5) = (#sat_ksat_sol.source_config.map(str).join(", "))$, auxiliary vars are don't-cares #sym.checkmark + First solution: $(x_1, ..., x_5) = (#fmt-values(sat_ksat_sol.source_config))$, auxiliary vars are don't-cares #sym.checkmark ], )[ @cook1971 @garey1979 Clauses shorter than $k$ can be padded with a complementary pair $y, overline(y)$ that is always satisfiable; clauses longer than $k$ can be split into a chain of width-$k$ clauses linked by auxiliary variables that propagate truth values. Both transformations preserve satisfiability while enforcing uniform clause width. @@ -12976,16 +12885,16 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(sat_max2sat.source) + " -o sat.json", - "pred reduce sat.json --to " + target-spec(sat_max2sat) + " -o bundle.json", + "pred reduce sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate sat.json --config " + sat_max2sat_sol.source_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(sat_max2sat_sol.source_config), ) *Step 1 -- Source instance.* The canonical SAT formula has $n = #sat_max2sat.source.instance.num_vars$ variables and $m = #sat-num-clauses(sat_max2sat.source.instance)$ clauses: $ C_1 = (x_1 or overline(x_2) or x_3), quad C_2 = (overline(x_1) or x_2). $ - The stored satisfying assignment is $(x_1, x_2, x_3) = (#sat_max2sat_sol.source_config.map(str).join(", "))$. + The stored satisfying assignment is $(x_1, x_2, x_3) = (#fmt-values(sat_max2sat_sol.source_config))$. *Step 2 -- Normalize to 3-CNF.* Clause $C_1$ already has width $3$. Clause $C_2$ introduces one auxiliary variable $y_1$ and becomes $ @@ -12994,7 +12903,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m $ The normalized formula therefore has $4$ variables and $3$ clauses. - *Step 3 -- Build the MAX-2-SAT gadgets.* Introduce one gadget variable per normalized clause, so the target has $#sat_max2sat.target.instance.num_vars$ variables and #sat_max2sat.target.instance.clauses.len() clauses. The stored witness is $(x_1, x_2, x_3, y_1, w_1, w_2, w_3) = (#sat_max2sat_sol.target_config.map(str).join(", "))$. With $(y_1, w_1, w_2, w_3) = (0, 1, 0, 1)$, each of the three gadgets satisfies exactly $7$ clauses, so the target objective reaches $21 = 7 times 3$ #sym.checkmark. + *Step 3 -- Build the MAX-2-SAT gadgets.* Introduce one gadget variable per normalized clause, so the target has $#sat_max2sat.target.instance.num_vars$ variables and #sat_max2sat.target.instance.clauses.len() clauses. The stored witness is $(x_1, x_2, x_3, y_1, w_1, w_2, w_3) = (#fmt-values(sat_max2sat_sol.target_config))$. With $(y_1, w_1, w_2, w_3) = (0, 1, 0, 1)$, each of the three gadgets satisfies exactly $7$ clauses, so the target objective reaches $21 = 7 times 3$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical optimum. Auxiliary variables such as $y_1$ can vary across optimal witnesses, but truncating any optimal target assignment to the first $3$ coordinates still yields a satisfying assignment of the original SAT formula. ], @@ -13030,7 +12939,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m $ ($arrow.r.double$) If $phi$ is satisfiable, then the normalized formula $psi$ is satisfiable by Step 1. Every normalized clause is true, so each gadget attains value $7$ and the target optimum is $7 m'$. ($arrow.l.double$) If the target optimum is $7 m'$, then every gadget must contribute $7$, so every normalized clause $D_t$ is true. Hence $psi$ is satisfiable, and the normalization in Step 1 implies that the restriction to the original variables satisfies $phi$. If $phi$ is unsatisfiable, then $psi$ is unsatisfiable, so every assignment leaves at least one normalized clause false and the target optimum is strictly less than $7 m'$. - _Solution extraction._ Discard all auxiliary normalization variables and all gadget variables $w_t$; return only the first $n$ Boolean variables $(x_1, dots, x_n)$. + _Solution extraction._ Evaluate the target assignment exactly once through the formal validator. Accept only the exact score $7m'$, then return its prefix on the original variables. Since every gadget contributes at most seven, this score forces every normalized clause to hold, and the normalization proof guarantees that the prefix satisfies the original formula. An optimal score below $7m'$ represents source infeasibility and is rejected as a witness. The custom aggregate maps exactly $7m'$ to true and every other value, including an infeasible value, to false. The empty formula has $m'=0$ and accepts its zero-score assignments; an empty source clause creates a contradictory normalized pair and prevents attaining the threshold. + + _Arithmetic._ Checked construction bounds the complete target clause count $10m'$ in `i64`, so every satisfied-clause count is representable. The exact score is computed as $(10m' / 10) dot 7$, dividing first to stay within that bound. The existing variable allocator validates literal indices, and the target is constructed through its fallible public API. ] #let sat_cs = load-example("Satisfiability", "CircuitSAT") @@ -13041,9 +12952,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example SAT -o sat.json", - "pred reduce sat.json --to " + target-spec(sat_cs) + " -o bundle.json", + "pred reduce sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate sat.json --config " + sat_cs_sol.source_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(sat_cs_sol.source_config), ) ], )[ @@ -13074,9 +12985,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example " + problem-spec(cs_sat.source) + " -o circuitsat.json", - "pred reduce circuitsat.json --to " + target-spec(cs_sat) + " -o bundle.json", + "pred reduce circuitsat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate circuitsat.json --config " + cs_sat_sol.source_config.map(str).join(","), + "pred evaluate circuitsat.json --config " + cli-config(cs_sat_sol.source_config), ) Circuit: #circuit-num-gates(cs_sat.source.instance) assignment, #circuit-num-variables(cs_sat.source.instance) named variables \ Target: #cs_sat.target.instance.num_vars SAT variables, #cs_sat.target.instance.clauses.len() clauses \ @@ -13085,7 +12996,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 2 -- Emit clauses.* Add three clauses for $a = x_1 and x_2$, two for $b = not x_3$, three for $c = b and x_4$, three for $d = a or c$, and two clauses for the output identity $r equiv d$. The CNF therefore has #cs_sat.target.instance.clauses.len() clauses. - *Step 3 -- Verify a witness.* The fixture stores source config #cs_sat_sol.source_config.map(str).join(", "), meaning $(r, x_1, x_2, x_3, x_4) = (1, 1, 1, 0, 1)$. Extending with $(a, b, c, d) = (1, 1, 1, 1)$ gives the SAT witness #cs_sat_sol.target_config.map(str).join(", "), which satisfies every gate-definition clause and the two clauses enforcing $r equiv d$. + *Step 3 -- Verify a witness.* The fixture stores source config #fmt-values(cs_sat_sol.source_config), meaning $(r, x_1, x_2, x_3, x_4) = (1, 1, 1, 0, 1)$. Extending with $(a, b, c, d) = (1, 1, 1, 1)$ gives the SAT witness #fmt-values(cs_sat_sol.target_config), which satisfies every gate-definition clause and the two clauses enforcing $r equiv d$. *Multiplicity:* The fixture stores one canonical consistent assignment. Other satisfying assignments may exist whenever the circuit equations leave some named variables unconstrained. ], @@ -13114,26 +13025,31 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example CircuitSAT -o circuitsat.json", - "pred reduce circuitsat.json --to " + target-spec(cs_sg) + " -o bundle.json", + "pred reduce circuitsat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate circuitsat.json --config " + cs_sg_sol.source_config.map(str).join(","), + "pred evaluate circuitsat.json --config " + cli-config(cs_sg_sol.source_config), ) Circuit: #circuit-num-gates(cs_sg.source.instance) gates (2 XOR, 2 AND, 1 OR), #circuit-num-variables(cs_sg.source.instance) variables \ Target: #spin-num-spins(cs_sg.target.instance) spins (each gate allocates I/O + auxiliary spins) \ Canonical ground-state witness shown ($2^3$ valid input combinations exist for the full adder) #sym.checkmark ], )[ - @whitfield2012 @lucas2014 Each logic gate can be represented as an Ising gadget --- a small set of spins with couplings $J_(i j)$ and fields $h_i$ chosen so that the gadget's ground states correspond exactly to the gate's truth table rows. Composing gadgets for all gates in the circuit yields a spin glass whose ground states encode precisely the satisfying assignments of the circuit. The energy gap between valid and invalid I/O patterns ensures that any global ground state respects every gate's logic. + Each gate contributes a Hamiltonian bounded below by a known constant. The circuit is satisfiable exactly when the target optimum attains the sum of those constants. A ground state above this threshold is not a satisfying witness. This uses the nonnegative-penalty principle described by @bian2010ising. ][ - _Construction._ + _Construction._ Boolean bits $x$ use spins $s=2x-1$. The repository energy convention is $H=sum_((i,j)) J_(i j)s_i s_j + sum_i h_i s_i$, including any diagonal terms created by identified inputs. Use the gadgets in @tab:gadgets; a new expression output is tied to each named assignment output by a coupling $-4 s_u s_v$ whenever the two spins differ. Constants use fields $+1$ for false and $-1$ for true. Multi-input gates are binary folds; a single input is unchanged. Empty AND is true, while empty OR and XOR are false, as in the source evaluator. No final output is implicitly forced true: the source asks that every named assignment equal its expression. + + _Local bounds._ In bits $x,y$ with output $z$, the AND and OR energies are respectively $4(x y-2x z-2y z+3z)-3$ and $4(x+y+z+x y-2x z-2y z)-3$. Their minima are $-3$, attained exactly on valid truth-table rows. The XOR gadget with auxiliary bit $a$ has energy $2(x+y-z-2a)^2-4$, with minimum $-4$ exactly when $z=x " XOR " y$ and the auxiliary is consistent. NOT has energy $2(x+z-1)^2-1$, with minimum $-1$. Each constant has minimum $-1$; each equality coupling has minimum $-4$ exactly on equal spins. + + Let $b$ be the sum of these local minima over the gadgets and equality constraints actually emitted. Store $b$ using checked signed integer arithmetic. Then $H-b$ is a sum of nonnegative local penalties, even when expressions share variables, inputs repeat, or named assignments form cycles. + + _Forward direction._ Given a satisfying source assignment, evaluate each finite expression tree under that assignment, set its fresh output spins accordingly, and choose each XOR auxiliary to satisfy $x+y=z+2a$. Every equality to a named output holds because the source assignment satisfies all constraints. Thus every local term attains its lower bound and $H=b$. - _Spin mapping:_ Boolean variables $sigma in {0,1}$ map to Ising spins $s = 2sigma - 1 in {-1, +1}$. Each circuit variable is assigned a unique spin index; gate gadgets reference these indices for their inputs and outputs. + _Backward direction._ A target state with $H=b$ has zero total penalty, so every nonnegative local penalty is zero. Induction on each expression tree gives its correct output; the equality terms identify it with all corresponding named output variables. Reading named spins therefore satisfies every source assignment constraint. This proof does not assume that an inconsistent circuit can simultaneously minimize its local terms. - _Gate gadgets_ (inputs 0,1; output 2; auxiliary 3 for XOR) are listed in @tab:gadgets. For each gate, instantiate the gadget's couplings and fields. The total Hamiltonian is the sum over all gadgets: $H = -sum_(i < j) J_(i j) s_i s_j - sum_i h_i s_i$. + _Value and witness extraction._ Convert the proved target minimum $E$ to source feasibility by $E=b$. The witness extractor validates the target once, rejects states whose energy differs from $b$, and returns every source variable in the model's canonical order using $x=(s+1)/2$. The empty circuit has $b=0$ and the empty witness. - _Correctness._ ($arrow.r.double$) A satisfying circuit assignment maps to a spin configuration where every gadget is in a ground state (valid I/O), so the total energy is minimized. ($arrow.l.double$) Any global ground state must minimize each gadget's contribution. Since each gadget's ground states match its gate's truth table, the spin configuration encodes a valid circuit evaluation. The output spin is constrained to $+1$ (true), so the circuit is satisfied. + _Overhead._ For $v$ source variables, $t$ expression-tree nodes, and $o$ assignment outputs, there are at most $v+3t$ spins and $6t+o$ stored interactions. There are at most $t$ constant and NOT output spins, and binary folds allocate at most two spins per expression-tree edge, of which there are at most $t$. Merging interactions can only reduce the latter count. All coefficient accumulation and threshold addition use checked integer arithmetic. - _Solution extraction._ Map spins back to Boolean: $sigma_i = (s_i + 1) / 2$. Return the circuit input variables. ] #figure( @@ -13151,30 +13067,26 @@ where $P$ is a penalty weight large enough that any constraint violation costs m ) #let fact_cs = load-example("Factoring", "CircuitSAT") -#let fact-decode(config, start, count) = { - let pow2 = (1, 2, 4, 8, 16, 32) - range(count).fold(0, (acc, i) => acc + config.at(start + i) * pow2.at(i)) -} #let fact_cs_sol = fact_cs.solutions.at(0) #let fact-nbf = fact_cs.source.instance.m #let fact-nbs = fact_cs.source.instance.n -#let fact-p = fact-decode(fact_cs_sol.source_config, 0, fact-nbf) -#let fact-q = fact-decode(fact_cs_sol.source_config, fact-nbf, fact-nbs) +#let fact-p = fact_cs_sol.source_config.at(0).at(0) +#let fact-q = fact_cs_sol.source_config.at(1).at(0) #reduction-rule("Factoring", "CircuitSAT", example: true, example-caption: [Factor $N = #fact_cs.source.instance.target$], extra: [ #pred-commands( "pred create --example Factoring -o factoring.json", - "pred reduce factoring.json --to " + target-spec(fact_cs) + " -o bundle.json", + "pred reduce factoring.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate factoring.json --config " + fact_cs_sol.source_config.map(str).join(","), + "pred evaluate factoring.json --config " + cli-config(fact_cs_sol.source_config), ) Circuit: $#fact-nbf times #fact-nbs$ array multiplier with #circuit-num-gates(fact_cs.target.instance) gates, #circuit-num-variables(fact_cs.target.instance) variables \ Canonical witness: $#fact-p times #fact-q = #fact_cs.source.instance.target$ #sym.checkmark ], )[ - Integer multiplication can be implemented as a boolean circuit: an $m times n$ array multiplier computes $p times q$ using only AND, XOR, and OR gates arranged in a grid of full adders. Constraining the output bits to match $N$ turns the circuit into a satisfiability problem --- the circuit is satisfiable iff $N = p times q$ for some $p, q$ within the given bit widths. _(Folklore; no canonical reference.)_ + Integer multiplication can be implemented as a boolean circuit: an $m times n$ array multiplier computes $p times q$ using only AND, XOR, and OR gates arranged in a grid of full adders. Constraining the output bits to match $N$ turns the circuit into a satisfiability problem --- the circuit is satisfiable iff $N = p times q$ for some $p, q$ within the given bit widths. The partial-product and array-adder construction follows @dally2006ee108[Section 10.4, Figures 10.15–10.16]; the zero-width extension is justified below. ][ _Construction._ Build $m times n$ array multiplier for $p times q$: @@ -13182,11 +13094,18 @@ where $P$ is a penalty weight large enough that any constraint violation costs m $ a := p_i and q_j, quad t_1 := a xor s_"prev", quad s_(i,j) := t_1 xor c_"prev" $ $ t_2 := t_1 and c_"prev", quad t_3 := a and s_"prev", quad c_(i,j) := t_2 or t_3 $ - _Output constraint:_ Fix output wires to the binary representation of $N$: $M_k := "bit"_k(N)$ for $k = 1, dots, m+n$. + _Accumulator and output:_ Start with a zero accumulator, expressing its $n$ retained bits as $q_j and 0$. These expressions retain every second-factor input even when $m = 0$. Each row emits its low sum bit; after all rows, the remaining accumulator bits give the high product bits. Define $m+n$ named output wires by these actual expressions, then fix output wires to the binary representation of $N$: $M_k := "bit"_k(N)$ for $k = 1, dots, m+n$. If $N$ has a set bit beyond this width, add two contradictory assignments to one wire. If both widths are zero, there are no product wires and this same overflow check rejects every nonzero target. + + _Correctness._ Let $Q$ be the second factor, $p_i$ the zero-indexed first-factor bits, $R_i$ the integer represented by row $i$'s sum and final carry, and $L_i$ the integer encoded by its first $i$ emitted low bits. Set $R_0 = L_0 = 0$. Full-adder equations give $R_i = p_(i-1) Q + floor(R_(i-1)/2)$. Induction gives + $L_i + 2^i floor(R_i/2) = Q sum_(j=0)^(i-1) p_j 2^j.$ + Thus the emitted bits followed by the residual bits represent exactly $P Q$, including the empty sum when $m = 0$. + + ($arrow.r.double$) For any source factorization, assign the factor inputs and evaluate the acyclic arithmetic assignments in order. The invariant makes all output equalities hold, and a representable product cannot trigger the overflow contradiction. ($arrow.l.double$) Every satisfying assignment obeys each full-adder equation, so induction forces its named product bits to equal the actual input product. The output constants and high-bit rejection then force $P Q = N$. - _Correctness._ ($arrow.r.double$) If $N = p times q$ with $p < 2^m$ and $q < 2^n$, setting the input bits to the binary representations of $p$ and $q$ produces output bits matching $N$, satisfying all constraints. ($arrow.l.double$) Any satisfying assignment to the circuit computes a valid multiplication (the gates enforce arithmetic correctness), and the output constraint ensures the product equals $N$. + _Solution extraction._ Evaluate the target certificate once and reject it unless all equations hold. Read off factor bits $p = sum_i p_i 2^(i-1)$ and $q = sum_j q_j 2^(j-1)$, then return $(min(p,q), max(p,q))$. The source requires $m <= n$. Sorting preserves the asymmetric bounds: if inputs are exchanged, the new smaller factor is below the old first factor, while both inputs fit the larger width. + + _Size and arithmetic._ Product width and assignment capacity are checked before allocation. At most $6 m n + 2(m+n) + 2$ assignments and $6 m n + 2(m+n) + 1$ variables are generated. Factors and products use exact arbitrary-precision integers; the circuit enforces bit arithmetic without floating-point conversions. The cell and output assignments use only the existing Boolean expression API. - _Solution extraction._ Read off factor bits: $p = sum_i p_i 2^(i-1)$, $q = sum_j q_j 2^(j-1)$. ] #let mc_sg = load-example("MaxCut", "SpinGlass") @@ -13198,12 +13117,12 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example MaxCut -o maxcut.json", - "pred reduce maxcut.json --to " + target-spec(mc_sg) + " -o bundle.json", + "pred reduce maxcut.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate maxcut.json --config " + mc_sg_sol.source_config.map(str).join(","), + "pred evaluate maxcut.json --config " + cli-config(mc_sg_sol.source_config), ) Direct 1:1 mapping: vertices $arrow.r$ spins, $J_(i j) = w_(i j) = 1$, $h_i = 0$ \ - Partition: $S = {#mc_sg_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ vs $overline(S) = {#mc_sg_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => str(i)).join(", ")}$ \ + Partition: $S = {#mc_sg_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ")}$ vs $overline(S) = {#mc_sg_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => str(i)).join(", ")}$ \ Cut value $= #mc_sg_cut$ (canonical witness shown) #sym.checkmark ], )[ @@ -13224,9 +13143,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m extra: [ #pred-commands( "pred create --example SpinGlass -o spinglass.json", - "pred reduce spinglass.json --to " + target-spec(sg_mc) + " -o bundle.json", + "pred reduce spinglass.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate spinglass.json --config " + sg_mc_sol.source_config.map(str).join(","), + "pred evaluate spinglass.json --config " + cli-config(sg_mc_sol.source_config), ) All $h_i = 0$: no ancilla needed, direct 1:1 vertex mapping \ Edge weights $w_(i j) = J_(i j) in {plus.minus 1}$ (alternating couplings) \ @@ -13267,13 +13186,17 @@ where $P$ is a penalty weight large enough that any constraint violation costs m ] #reduction-rule("KColoring", "TwoDimensionalConsecutiveSets")[ - @lipski1977fct A graph 3-coloring can be encoded as a partition problem on an alphabet. Each edge becomes a size-3 subset containing the two endpoint symbols plus a unique dummy symbol, and a valid 3-coloring corresponds to partitioning the alphabet into 3 groups where each edge-subset spans exactly 3 consecutive groups with one element per group. The reduction uses $n + m$ alphabet symbols and $m$ subsets for a graph with $n$ vertices and $m$ edges. + A fixed three-coloring problem reduces to the ordered-partition problem of @lipski1978consecutive (Problem 5). For a nonempty loopless graph with $n$ vertices and $m$ stored edges, use one vertex symbol and one private symbol per edge; every edge becomes a three-element subset. The construction and decoding below also cover empty graphs and native self-loops. ][ - _Construction._ Given $G = (V, E)$ with $|V| = n$ and $|E| = m$, build alphabet $Sigma = V union {d_e : e in E}$ of size $n + m$. For each edge $e = {u, v} in E$, define subset $Sigma_e = {u, v, d_e}$. The collection is $cal(C) = {Sigma_e : e in E}$ with $|cal(C)| = m$ subsets, each of size 3. + _Construction._ The source is `KColoring`: the fixed type requires exactly three available colors, and persisted `num_colors` must agree. If any source edge is a self-loop, return alphabet ${0,1,2}$ and subsets ${0,1},{1,2},{0,2}$. Otherwise label vertices $0,...,n-1$ and stored edges $e_0,...,e_(m-1)$. Use alphabet size $max(1,n+m)$ and, for $e_i={u,v}$, subset ${u,v,n+i}$. Construct through the target's fallible constructor. Repeated source edges receive distinct private symbols. + + _Forward direction._ For a loopless graph, a proper coloring assigns adjacent vertices distinct colors from ${0,1,2}$. Assign each edge's private symbol the remaining third color. Each triple occupies all three consecutive nonempty color groups, one symbol per group. If there are no edges, all group assignments satisfy the empty family of constraints; in particular the empty source maps to one unconstrained symbol. A looped source has no proper coloring. - _Correctness._ ($arrow.r.double$) Given a valid 3-coloring $chi: V arrow {1, 2, 3}$, define partition groups $X_c = {v in V : chi(v) = c}$ for $c in {1, 2, 3}$. For each edge $e = {u, v}$, assign dummy $d_e$ to the unique third color $c^* in {1, 2, 3} backslash {chi(u), chi(v)}$ (which exists since $chi(u) != chi(v)$). Then $Sigma_e = {u, v, d_e}$ has its three elements in three distinct groups ${X_(chi(u)), X_(chi(v)), X_(c^*)} = {X_1, X_2, X_3}$, which are consecutive with one element per group. ($arrow.l.double$) If a valid partition into $k$ groups exists, each size-3 subset ${u, v, d_e}$ must occupy 3 distinct consecutive groups. In particular, $u$ and $v$ are in different groups. Mapping groups to colors gives a valid 3-coloring. + _Backward direction._ In any feasible target partition for an ordinary graph, an edge triple occupies three distinct consecutive nonempty groups. Its endpoint groups are therefore distinct and differ by at most two in the ordered partition. Remove groups containing only private symbols and rank the remaining vertex groups starting at zero. Deleting groups cannot increase the distance between endpoint ranks, so every edge has rank difference 1 or 2. Assigning each vertex its group rank modulo 3 gives different colors on every edge, regardless of the total number of groups. For the loop sentinel, all three symbols would have to be pairwise distinct and every pair consecutive. The first and last groups cannot be adjacent, proving target infeasibility. - _Solution extraction._ The first $n$ symbols in the target configuration correspond to the graph vertices. Their group assignments, compressed to $0, 1, 2$, yield the 3-coloring. + _Solution extraction._ Validate the target once, require `Or(true)`, and then return the ranks modulo 3 of the first $n$ symbols. Infeasible and malformed certificates are rejected. The loop sentinel never reaches prefix decoding, even when its alphabet is smaller than $n$. The empty-source prefix is empty and is a valid coloring. + + _Domain and overhead._ The actual alphabet and subset counts are bounded by $n+m+3$ and $m+3$. Source graph storage contains nonzero-sized petgraph node and edge records. With $I=$ `isize::MAX`, their vector allocation bounds imply $n<=floor(I/8)$ and $m<=floor(I/16)$ for the native unit-weight, `u32`-indexed graph, so $n+m$ and every private-symbol index fit `usize`. Group ranks are at most the alphabet size minus one; modulo 3 yields exactly the source color domain. Target allocation failures remain errors. This proof uses the ordered-set definition; it does not assume that every target witness uses only three groups. ] #reduction-rule("Factoring", "ILP")[ @@ -13305,13 +13228,33 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Correctness._ The McCormick constraints enforce $z_(i j) = p_i dot q_j$ for binary variables. The bit equations encode $p times q = N$ via carry propagation, matching array multiplier semantics. - _Solution extraction._ Read $p = sum_i p_i 2^i$ and $q = sum_j q_j 2^j$ from the binary variables. + _Solution extraction._ Read $p = sum_i p_i 2^i$ and $q = sum_j q_j 2^j$ from the binary variables, then return $(min(p,q), max(p,q))$. ] == ILP Formulations The following reductions to Integer Linear Programming are straightforward formulations where problem constraints map directly to linear inequalities. +#reduction-rule("MultipleChoiceBranching", "ILP")[ + A topological-order formulation makes the branching acyclicity condition linear while retaining the source indegree, partition, and weight inequalities directly. +][ + _Construction._ For every arc $a$ introduce a nonnegative integer $x_a <= 1$, and for every vertex $v$ introduce an integer order $0 <= p_v <= n-1$. Add $sum_(a in A_i)x_a <= 1$ for each partition group, $sum_(a in delta^-(v))x_a <= 1$ for each vertex, and $sum_a w_a x_a >= K$. For every arc $a=(u,v)$ add $p_u-p_v+n x_a <= n-1$. The target has exactly $m+n$ variables and $2m+2n+g+1$ constraints for $g$ partition groups. + + _Correctness._ A source branching admits a topological order, which satisfies the order rows. Conversely, selecting $(u,v)$ forces $p_v >= p_u+1$, so selected arcs cannot contain a directed cycle. All remaining source conditions are represented verbatim by their corresponding rows. + + _Solution extraction._ Return arc $a$ exactly when $x_a=1$. +] + +#reduction-rule("PartitionIntoCliques", "ILP")[ + The standard one-hot coloring formulation applied to the complement graph assigns every source vertex to one of the available clique labels and forbids non-adjacent vertices from sharing a label. +][ + _Construction._ Introduce $x_(v,c) in {0,1}$ for every vertex $v$ and clique label $c$. Require $sum_c x_(v,c)=1$ for every vertex. For every source non-edge ${u,v}$ and label $c$, require $x_(u,c)+x_(v,c) <= 1$. Use the zero objective. Since $K <= n$, the target has at most $n^2$ variables and $n+n^3$ constraints. + + _Correctness._ A clique partition satisfies all one-hot rows, and no non-edge has both endpoints in one part. Conversely, the unique selected label of each vertex defines a partition; every two vertices sharing a label must be adjacent, so each part is a clique. + + _Solution extraction._ For each vertex $v$, return the unique label $c$ with $x_(v,c)=1$. +] + #reduction-rule("MaximumSetPacking", "ILP")[ Each set is either selected or not, and every universe element may belong to at most one selected set -- an element-based constraint that is directly linear in binary indicator variables. ][ @@ -13380,9 +13323,9 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(mfdts_ilp.source) + " -o mfdts.json", - "pred reduce mfdts.json --to " + target-spec(mfdts_ilp) + " -o bundle.json", + "pred reduce mfdts.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mfdts.json --config " + mfdts_ilp_sol.source_config.map(str).join(","), + "pred evaluate mfdts.json --config " + cli-config(mfdts_ilp_sol.source_config), ) #{ @@ -13391,11 +13334,11 @@ The following reductions to Integer Linear Programming are straightforward formu let source-config = mfdts_ilp_sol.source_config let target-config = mfdts_ilp_sol.target_config [ - *Step 1 -- Source instance.* The canonical DAG has $#source.num_vertices$ vertices and arcs #source.arcs.map(((u, v)) => [$(#u, #v)$]).join(", "). Inputs are ${#source.inputs.map(str).join(", ")}$, outputs are ${#source.outputs.map(str).join(", ")}$, so the internal vertices are ${2, 3, 4}$. The source configuration therefore has $#source.inputs.len() dot #source.outputs.len() = #target.num_vars$ input-output pair bits. + *Step 1 -- Source instance.* The canonical DAG has $#source.num_vertices$ vertices and arcs #source.arcs.map(((u, v)) => [$(#u, #v)$]).join(", "). Inputs are ${#fmt-values(source.inputs)}$, outputs are ${#fmt-values(source.outputs)}$, so the internal vertices are ${2, 3, 4}$. The source configuration therefore has $#source.inputs.len() dot #source.outputs.len() = #target.variables.len()$ input-output pair bits. - *Step 2 -- Build the covering ILP.* Order the pair variables as $(#source.inputs.at(0), #source.outputs.at(0))$, $(#source.inputs.at(0), #source.outputs.at(1))$, $(#source.inputs.at(1), #source.outputs.at(0))$, and $(#source.inputs.at(1), #source.outputs.at(1))$. Their internal coverage sets are ${2, 3}$, ${3}$, ${3}$, and ${3, 4}$, so the target has #target.num_vars binary variables, #target.constraints.len() covering constraints, and objective $min (x_0 + x_1 + x_2 + x_3)$. The exported constraints are exactly $x_0 >= 1$, $x_0 + x_1 + x_2 + x_3 >= 1$, and $x_3 >= 1$. + *Step 2 -- Build the covering ILP.* Order the pair variables as $(#source.inputs.at(0), #source.outputs.at(0))$, $(#source.inputs.at(0), #source.outputs.at(1))$, $(#source.inputs.at(1), #source.outputs.at(0))$, and $(#source.inputs.at(1), #source.outputs.at(1))$. Their internal coverage sets are ${2, 3}$, ${3}$, ${3}$, and ${3, 4}$, so the target has #target.variables.len() binary variables, #target.constraints.len() covering constraints, and objective $min (x_0 + x_1 + x_2 + x_3)$. The exported constraints are exactly $x_0 >= 1$, $x_0 + x_1 + x_2 + x_3 >= 1$, and $x_3 >= 1$. - *Step 3 -- Verify the canonical witness.* The stored ILP witness is $(#target-config.map(str).join(", "))$. Because extraction is identity, the source witness is the same vector $(#source-config.map(str).join(", "))$, which selects pairs $(#source.inputs.at(0), #source.outputs.at(0))$ and $(#source.inputs.at(1), #source.outputs.at(1))$. These two pairs cover internal vertices ${2, 3}$ and ${3, 4}$ respectively, so their union covers every internal vertex and the optimum value is $2$ #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The stored ILP witness is $(#fmt-values(target-config))$. Because extraction is identity, the source witness is the same vector $(#fmt-values(source-config))$, which selects pairs $(#source.inputs.at(0), #source.outputs.at(0))$ and $(#source.inputs.at(1), #source.outputs.at(1))$. These two pairs cover internal vertices ${2, 3}$ and ${3, 4}$ respectively, so their union covers every internal vertex and the optimum value is $2$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. Any feasible solution must include $x_0 = 1$ to cover internal vertex 2 and $x_3 = 1$ to cover internal vertex 4, so the unique optimum is $(1, 0, 0, 1)$. ] @@ -13446,34 +13389,32 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ $S = {v : x_v = 1}$. ] -#let fvs_cg = load-example("MinimumFeedbackVertexSet", "MinimumCodeGenerationUnlimitedRegisters") +#let fvs_cg = load-example("MinimumFeedbackVertexSet", "MinimumCodeGenerationUnlimitedRegisters", source-variant: (weight: "One")) #let fvs_cg_sol = fvs_cg.solutions.at(0) -#let fvs_cg_fvs = fvs_cg_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let fvs_cg_fvs = fvs_cg_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #reduction-rule("MinimumFeedbackVertexSet", "MinimumCodeGenerationUnlimitedRegisters", example: true, + example-source-variant: (weight: "One"), example-caption: [3-cycle digraph: FVS of size 1 maps to an expression DAG needing 1 LOAD], extra: [ #pred-commands( - "pred create --example MinimumFeedbackVertexSet -o fvs.json", - "pred reduce fvs.json --to " + target-spec(fvs_cg) + " -o bundle.json", + "pred create --example MinimumFeedbackVertexSet/One -o fvs.json", + "pred reduce fvs.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate fvs.json --config " + fvs_cg_sol.source_config.map(str).join(","), + "pred evaluate fvs.json --config " + cli-config(fvs_cg_sol.source_config), ) - Source FVS: $F = {#fvs_cg_fvs.map(str).join(", ")}$ (size #fvs_cg_fvs.len()) on a digraph with $n = #fvs_cg.source.instance.graph.num_vertices$ vertices and $m = #fvs_cg.source.instance.graph.arcs.len()$ arcs \ + Source FVS: $F = {#fmt-values(fvs_cg_fvs)}$ (size #fvs_cg_fvs.len()) on a digraph with $n = #fvs_cg.source.instance.graph.num_vertices$ vertices and $m = #fvs_cg.source.instance.graph.arcs.len()$ arcs \ Target DAG: #fvs_cg.target.instance.num_vertices vertices, left arcs $L$: #{fvs_cg.target.instance.left_arcs.map(a => $#(a.at(0)) arrow.r #(a.at(1))$).join(", ")}, right arcs $R$: #{fvs_cg.target.instance.right_arcs.map(a => $#(a.at(0)) arrow.r #(a.at(1))$).join(", ")} \ - Target evaluation order: $(#fvs_cg_sol.target_config.map(str).join(", "))$ with #fvs_cg_sol.target_config.len() instructions #sym.checkmark + Target evaluation order: $(#fmt-values(fvs_cg_sol.target_config))$ with #(fvs_cg_sol.target_config.len() + fvs_cg_fvs.len()) instructions #sym.checkmark ], )[ - The Aho--Johnson--Ullman chain gadget construction @ahoJohnsonUllman1977 encodes a feedback vertex set problem as a code generation problem on an expression DAG with unlimited registers and 2-address instructions. Each source vertex becomes a leaf (input register), and each outgoing arc becomes an internal chain node. The number of LOAD (copy) instructions needed in an optimal program equals the size of a minimum feedback vertex set. + For the unit-weight variant, minimum feedback vertex set reduces to code generation on a leaf-sharing expression DAG with unlimited registers and 2-address instructions. The cardinality reduction of Aho--Johnson--Ullman @ahoJohnsonUllman1977 motivates the construction; separate start operations below extend it uniformly to self-loops. Arbitrary vertex weights are not represented by unit-cost copies and are excluded from this rule. If the source has $n$ vertices, $m$ arc records, and minimum feedback vertex set size $tau$, the target optimum is $n + m + tau$. ][ - _Construction._ Given a directed graph $G = (V, A)$ with $n = |V|$ vertices and $m = |A|$ arcs, build an expression DAG $D$ with $n + m$ vertices as follows. Vertices $0, dots, n-1$ are _leaves_ (one per source vertex), each stored in its own register. For each source vertex $x$ with outgoing arcs $(x, y_1), dots, (x, y_d)$, create a chain of $d$ internal nodes $x^1, dots, x^d$ where: - - $x^1$ has left child $x^0$ (the leaf for $x$) and right child $y_1^0$ (the leaf for $y_1$), - - $x^i$ ($i >= 2$) has left child $x^(i-1)$ (previous chain node) and right child $y_i^0$ (the leaf for $y_i$). - The left operand's register is destroyed by the OP instruction; a LOAD (copy) is needed whenever a leaf register must survive past its destruction. + _Construction._ Introduce one leaf $x^0$ per source vertex and a right-only dummy leaf $q$. For every vertex $x$, including sinks and isolated vertices, create a start operation $s_x$ with operands $(x^0, q)$. For each outgoing arc $(x, y_i)$, append an operation $c_(x,i)$ with the preceding operation in this chain as left operand and $y_i^0$ as right operand. Preserve the source arc order within each chain. There are $n + 1$ leaves and $N = n + m$ operations, hence $2n + m + 1$ vertices and $N$ operand arcs of each kind. The construction is linear in $n + m + 1$ and always yields a binary expression DAG. - _Correctness._ ($arrow.r.double$) If $F subset.eq V$ is a feedback vertex set of size $k$, then $G[V backslash F]$ is a DAG. The topological order of $G[V backslash F]$ induces an evaluation order of the chain nodes such that each leaf $x^0$ with $x in.not F$ is consumed (as a left child) only after all chain nodes that reference it as a right child have been evaluated. Only leaves corresponding to vertices in $F$ need a LOAD instruction (their register is destroyed before some right-child usage). Hence the program uses exactly $n + m - n + k = m + k$ instructions, of which $k$ are LOADs. ($arrow.l.double$) If an optimal program uses $k$ LOAD instructions, the $k$ leaves that require LOADs form a set $F$: removing $F$ from $G$ leaves a DAG (otherwise a directed cycle $v_1 -> dots -> v_l -> v_1$ would require each $v_i^0$ to be consumed before $v_(i+1)^0$, creating a circular register dependency that demands at least one additional LOAD for each cycle). Thus $F$ is a feedback vertex set of size $k$. + _Extraction and backward direction._ In any valid evaluation order, select $x$ exactly when some right-use of $x^0$ occurs after $s_x$. Each original leaf is overwritten only at its start, the dummy leaf is never overwritten, and each internal result has at most one use. Therefore the instruction count is exactly $N + |F|$ for the extracted set $F$. For every source arc $(x,y)$ with $y in.not F$, the order satisfies $s_x < c_(x,i) < s_y$. Thus start positions strictly increase on every arc induced by $V backslash F$, which is acyclic. A self-loop forces its vertex into $F$ by the same inequality. This argument covers arbitrary interleavings and parallel arcs. - _Solution extraction._ Given a target evaluation order (permutation of internal nodes), identify which leaves require a LOAD: leaf $x^0$ needs a LOAD iff the chain-start node $x^1$ is evaluated before some other internal node that uses $x^0$ as a right child. Set $c_x = 1$ for such vertices and $c_x = 0$ otherwise. + _Forward direction and optimality._ Given a feedback vertex set $F$, execute complete chains for its vertices first, then complete chains for the remaining vertices in a topological order of $G[V backslash F]$. Every right-use of an unselected leaf precedes its start, so this order costs at most $N + |F|$. Combined with the backward bound, this proves that the target optimum is $N + tau$ and that every optimal target order extracts a minimum feedback vertex set. The empty source produces one dummy leaf and an empty order of cost zero. ] #reduction-rule("MaximumClique", "ILP")[ @@ -13494,23 +13435,23 @@ The following reductions to Integer Linear Programming are straightforward formu #let mckp_ilp = load-example( "MaximumCoKPlex", "ILP", - source-variant: (graph: "SimpleGraph", k: "KN", weight: "i32"), - target-variant: (variable: "bool"), + source-variant: (graph: "SimpleGraph", k: "KN", weight: "i64"), + target-variant: (coefficient: "i64", variable: "bool"), ) #let mckp_ilp_sol = mckp_ilp.solutions.at(0) #reduction-rule("MaximumCoKPlex", "ILP", example: true, - example-source-variant: (graph: "SimpleGraph", k: "KN", weight: "i32"), - example-target-variant: (variable: "bool"), + example-source-variant: (graph: "SimpleGraph", k: "KN", weight: "i64"), + example-target-variant: (coefficient: "i64", variable: "bool"), example-caption: [Weighted 5-cycle ($n = 5$), $k = 2$], extra: [ #pred-commands( "pred create --example " + problem-spec(mckp_ilp.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(mckp_ilp) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + mckp_ilp_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(mckp_ilp_sol.source_config), ) - Source co-$k$-plex witness $(#mckp_ilp_sol.source_config.map(str).join(", "))$, target ILP witness $(#mckp_ilp_sol.target_config.map(str).join(", "))$. + Source co-$k$-plex witness $(#fmt-values(mckp_ilp_sol.source_config))$, target ILP witness $(#fmt-values(mckp_ilp_sol.target_config))$. ], )[ This direct binary ILP formulation introduces one variable per source vertex and one induced-degree cap per source vertex @Hernandez2016MolecularSimilarity. @@ -13530,21 +13471,21 @@ The following reductions to Integer Linear Programming are straightforward formu #let mces_ilp = load-example( "MaximumCommonEdgeSubgraph", "ILP", - target-variant: (variable: "bool"), + target-variant: (coefficient: "i64", variable: "bool"), ) #let mces_ilp_sol = mces_ilp.solutions.at(0) #reduction-rule("MaximumCommonEdgeSubgraph", "ILP", example: true, - example-target-variant: (variable: "bool"), + example-target-variant: (coefficient: "i64", variable: "bool"), example-caption: [Two labelled 3-vertex digraphs with 2 arcs each], extra: [ #pred-commands( "pred create --example " + problem-spec(mces_ilp.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(mces_ilp) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + mces_ilp_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(mces_ilp_sol.source_config), ) - Source mapping witness $(#mces_ilp_sol.source_config.map(str).join(", "))$, target ILP witness $(#mces_ilp_sol.target_config.map(str).join(", "))$. + Source mapping witness $(#fmt-values(mces_ilp_sol.source_config))$, target ILP witness $(#fmt-values(mces_ilp_sol.target_config))$. ], )[ Encode a partial injective vertex map with row and column inequalities and linearize each label-compatible source/target arc pair with a McCormick product variable @Bahiense2012MCES. @@ -13568,21 +13509,21 @@ The following reductions to Integer Linear Programming are straightforward formu #let cmo_ilp = load-example( "MaximumContactMapOverlap", "ILP", - target-variant: (variable: "bool"), + target-variant: (coefficient: "i64", variable: "bool"), ) #let cmo_ilp_sol = cmo_ilp.solutions.at(0) #reduction-rule("MaximumContactMapOverlap", "ILP", example: true, - example-target-variant: (variable: "bool"), + example-target-variant: (coefficient: "i64", variable: "bool"), example-caption: [$|V_1| = #cmo_ilp.source.instance.num_vertices_1$, $|E_1| = #cmo_ilp.source.instance.contacts_1.len()$, $|V_2| = #cmo_ilp.source.instance.num_vertices_2$, $|E_2| = #cmo_ilp.source.instance.contacts_2.len()$], extra: [ #pred-commands( "pred create --example " + problem-spec(cmo_ilp.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(cmo_ilp) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + cmo_ilp_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(cmo_ilp_sol.source_config), ) - Source alignment witness $(#cmo_ilp_sol.source_config.map(str).join(", "))$ (each entry is the matched index in $V_2$ shifted by $1$, with $0$ meaning unmatched), target ILP witness $(#cmo_ilp_sol.target_config.map(str).join(", "))$. + Source alignment witness $(#fmt-values(cmo_ilp_sol.source_config))$ (each entry is the matched index in $V_2$ shifted by $1$, with $0$ meaning unmatched), target ILP witness $(#fmt-values(cmo_ilp_sol.target_config))$. ], )[ Encode the order-preserving partial injective alignment $V_1 -> V_2$ by binary match variables $x_(i,j)$ with row, column, and crossing-forbidding inequalities, then linearize each pair of source/target contacts with a binary product variable $y_(i,k,j,l)$ to count preserved contacts @AndonovMalodDogninYanev2011CMO @XieSahinidis2007CMO. @@ -13606,23 +13547,23 @@ The following reductions to Integer Linear Programming are straightforward formu #let mewkc_ilp = load-example( "MaximumEdgeWeightedKClique", "ILP", - source-variant: (weight: "i32"), - target-variant: (variable: "bool"), + source-variant: (weight: "i64"), + target-variant: (coefficient: "i64", variable: "bool"), ) #let mewkc_ilp_sol = mewkc_ilp.solutions.at(0) #reduction-rule("MaximumEdgeWeightedKClique", "ILP", example: true, - example-source-variant: (weight: "i32"), - example-target-variant: (variable: "bool"), + example-source-variant: (weight: "i64"), + example-target-variant: (coefficient: "i64", variable: "bool"), example-caption: [$n = 4$ vertices, $m = 5$ edges, $k = 3$], extra: [ #pred-commands( "pred create --example " + problem-spec(mewkc_ilp.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(mewkc_ilp) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + mewkc_ilp_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(mewkc_ilp_sol.source_config), ) - Source $k$-clique witness $(#mewkc_ilp_sol.source_config.map(str).join(", "))$, target ILP witness $(#mewkc_ilp_sol.target_config.map(str).join(", "))$. + Source $k$-clique witness $(#fmt-values(mewkc_ilp_sol.source_config))$, target ILP witness $(#fmt-values(mewkc_ilp_sol.target_config))$. ], )[ Binary vertex selectors with an exact-cardinality constraint, non-edge clique constraints, and McCormick edge-product variables linearize the induced edge-weight sum @ParkLeePark1996EWClique @GouveiaMartins2015EWClique. @@ -13645,7 +13586,7 @@ The following reductions to Integer Linear Programming are straightforward formu #let ks_ilp = load-example("Knapsack", "ILP") #let ks_ilp_sol = ks_ilp.solutions.at(0) -#let ks_ilp_selected = ks_ilp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let ks_ilp_selected = ks_ilp_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let ks_ilp_sel_weight = ks_ilp_selected.fold(0, (a, i) => a + ks_ilp.source.instance.weights.at(i)) #let ks_ilp_sel_value = ks_ilp_selected.fold(0, (a, i) => a + ks_ilp.source.instance.values.at(i)) #reduction-rule("Knapsack", "ILP", @@ -13654,11 +13595,11 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example Knapsack -o knapsack.json", - "pred reduce knapsack.json --to " + target-spec(ks_ilp) + " -o bundle.json", + "pred reduce knapsack.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate knapsack.json --config " + ks_ilp_sol.source_config.map(str).join(","), + "pred evaluate knapsack.json --config " + cli-config(ks_ilp_sol.source_config), ) - *Step 1 -- Source instance.* The canonical knapsack instance has weights $(#ks_ilp.source.instance.weights.map(str).join(", "))$, values $(#ks_ilp.source.instance.values.map(str).join(", "))$, and capacity $C = #ks_ilp.source.instance.capacity$. + *Step 1 -- Source instance.* The canonical knapsack instance has weights $(#fmt-values(ks_ilp.source.instance.weights))$, values $(#fmt-values(ks_ilp.source.instance.values))$, and capacity $C = #ks_ilp.source.instance.capacity$. *Step 2 -- Build the binary ILP.* Introduce one binary variable per item: $#range(ks_ilp.source.instance.weights.len()).map(i => $x_#i$).join(", ") in {0,1}$. @@ -13667,9 +13608,9 @@ The following reductions to Integer Linear Programming are straightforward formu subject to the single capacity inequality $ #ks_ilp.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) <= #ks_ilp.source.instance.capacity. $ - *Step 3 -- Verify a solution.* The ILP optimum $bold(x)^* = (#ks_ilp_sol.target_config.map(str).join(", "))$ extracts directly to the knapsack selection $bold(x)^* = (#ks_ilp_sol.source_config.map(str).join(", "))$, choosing items $\{#ks_ilp_selected.map(str).join(", ")\}$. Their total weight is $#ks_ilp_selected.map(i => str(ks_ilp.source.instance.weights.at(i))).join(" + ") = #ks_ilp_sel_weight$ and their total value is $#ks_ilp_selected.map(i => str(ks_ilp.source.instance.values.at(i))).join(" + ") = #ks_ilp_sel_value$ #sym.checkmark. + *Step 3 -- Verify a solution.* The ILP optimum $bold(x)^* = (#fmt-values(ks_ilp_sol.target_config))$ extracts directly to the knapsack selection $bold(x)^* = (#fmt-values(ks_ilp_sol.source_config))$, choosing items $\{#fmt-values(ks_ilp_selected)\}$. Their total weight is $#ks_ilp_selected.map(i => str(ks_ilp.source.instance.weights.at(i))).join(" + ") = #ks_ilp_sel_weight$ and their total value is $#ks_ilp_selected.map(i => str(ks_ilp.source.instance.values.at(i))).join(" + ") = #ks_ilp_sel_value$ #sym.checkmark. - *Uniqueness:* The fixture stores one canonical optimal witness. For this instance the optimum is unique: items $\{#ks_ilp_selected.map(str).join(", ")\}$ are the only feasible choice achieving value #ks_ilp_sel_value. + *Uniqueness:* The fixture stores one canonical optimal witness. For this instance the optimum is unique: items $\{#fmt-values(ks_ilp_selected)\}$ are the only feasible choice achieving value #ks_ilp_sel_value. ], )[ A 0-1 Knapsack instance is already a binary Integer Linear Program @papadimitriou-steiglitz1982: each item-selection bit becomes a binary variable, the capacity condition is a single linear inequality, and the value objective is linear. The reduction preserves the number of decision variables exactly, producing an ILP with $n$ variables and one constraint. @@ -13704,20 +13645,20 @@ The following reductions to Integer Linear Programming are straightforward formu [ #pred-commands( "pred create --example " + problem-spec(ik_ilp.source) + " -o integer-knapsack.json", - "pred reduce integer-knapsack.json --to " + target-spec(ik_ilp) + " -o bundle.json", + "pred reduce integer-knapsack.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate integer-knapsack.json --config " + ik_ilp_sol.source_config.map(str).join(","), + "pred evaluate integer-knapsack.json --config " + cli-config(ik_ilp_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Integer Knapsack instance has sizes $(#sizes.map(str).join(", "))$, values $(#values.map(str).join(", "))$, and capacity $B = #B$. + *Step 1 -- Source instance.* The canonical Integer Knapsack instance has sizes $(#fmt-values(sizes))$, values $(#fmt-values(values))$, and capacity $B = #B$. *Step 2 -- Build the ILP.* Introduce one integer variable per item multiplicity: $#range(sizes.len()).map(i => $c_#i$).join(", ") in NN$. The capacity constraint is $ #sizes.enumerate().map(((i, s)) => $#s c_#i$).join($+$) <= #B, $ - and the explicit upper bounds are $(#upper.map(str).join(", "))$, i.e. $c_i <= floor.l B / s_i floor.r$ for every item. + and the explicit upper bounds are $(#fmt-values(upper))$, i.e. $c_i <= floor.l B / s_i floor.r$ for every item. - *Step 3 -- Verify the canonical witness.* The ILP optimum is $(#ik_ilp_sol.target_config.map(str).join(", "))$, which extracts identically to the source multiplicities $(#ik_ilp_sol.source_config.map(str).join(", "))$. The selected terms contribute total size $#total_size <= B$ and total value $#total_value$ #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The ILP optimum is $(#fmt-values(ik_ilp_sol.target_config))$, which extracts identically to the source multiplicities $(#fmt-values(ik_ilp_sol.source_config))$. The selected terms contribute total size $#total_size <= B$ and total value $#total_value$ #sym.checkmark. *Uniqueness:* The fixture stores one canonical optimum, here $(0, 0, 2)$. ] @@ -13747,21 +13688,21 @@ The following reductions to Integer Linear Programming are straightforward formu #let clique_mis = load-example( "MaximumClique", "MaximumIndependentSet", - source-variant: (graph: "SimpleGraph", weight: "i32"), - target-variant: (graph: "SimpleGraph", weight: "i32"), + source-variant: (graph: "SimpleGraph", weight: "i64"), + target-variant: (graph: "SimpleGraph", weight: "i64"), ) #let clique_mis_sol = clique_mis.solutions.at(0) #reduction-rule("MaximumClique", "MaximumIndependentSet", example: true, - example-source-variant: (graph: "SimpleGraph", weight: "i32"), - example-target-variant: (graph: "SimpleGraph", weight: "i32"), + example-source-variant: (graph: "SimpleGraph", weight: "i64"), + example-target-variant: (graph: "SimpleGraph", weight: "i64"), example-caption: [Path graph $P_4$: clique in $G$ maps to independent set in complement $overline(G)$.], extra: [ #pred-commands( "pred create --example MaximumClique -o maximumclique.json", - "pred reduce maximumclique.json --to " + target-spec(clique_mis) + " -o bundle.json", + "pred reduce maximumclique.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate maximumclique.json --config " + clique_mis_sol.source_config.map(str).join(","), + "pred evaluate maximumclique.json --config " + cli-config(clique_mis_sol.source_config), ) ], )[ @@ -13836,11 +13777,11 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(ola_seqmwct.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(ola_seqmwct) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + ola_seqmwct_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(ola_seqmwct_sol.source_config), ) - Source arrangement $pi = (#ola_seqmwct_sol.source_config.map(str).join(", "))$, target schedule $(#ola_seqmwct_sol.target_config.map(str).join(", "))$. + Source arrangement $pi = (#fmt-values(ola_seqmwct_sol.source_config))$, target schedule $(#fmt-values(ola_seqmwct_sol.target_config))$. ], )[ @lawler1978 This $O(n + m)$ reduction turns each vertex into a unit-length job, each edge into a zero-length job, and uses precedences so that every edge job completes exactly when its later endpoint does. The weighted completion-time objective then equals the linear-arrangement objective plus the fixed shift $d_"max" n (n + 1) / 2$. @@ -13872,22 +13813,23 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(dola_c1ma.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(dola_c1ma) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + dola_c1ma_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(dola_c1ma_sol.source_config), ) - The source decision bound is $K = #dola_c1ma.source.instance.bound$, so the target augmentation bound is $K - m = #dola_c1ma.source.instance.bound - #dola_c1ma.target.instance.matrix.len() = #dola_c1ma.target.instance.bound$. Source arrangement $f = (#dola_c1ma_sol.source_config.map(str).join(", "))$ corresponds to target column permutation $(#dola_c1ma_sol.target_config.map(str).join(", "))$. + The source decision bound is $K = #dola_c1ma.source.instance.bound$, so the target augmentation bound is $K - m = #dola_c1ma.source.instance.bound - #dola_c1ma.target.instance.matrix.len() = #dola_c1ma.target.instance.bound$. Source arrangement $f = (#fmt-values(dola_c1ma_sol.source_config))$ corresponds to target column permutation $(#fmt-values(dola_c1ma_sol.target_config))$. ], )[ - @garey1979[SR16] @booth1987 This $O(n m)$ reduction maps a decision Optimal Linear Arrangement instance $(G, K)$ to the edge-vertex incidence matrix of $G$ with augmentation bound $K - |E|$. A column permutation is exactly a vertex ordering; making each edge row consecutive costs one flip per interior gap, so the cheapest augmentation under a fixed ordering equals (total edge length) $- |E|$. + The incidence construction of @booth1975[Theorem 4.19, pp. 106–108], transposed to use edge rows and vertex columns, gives an $O(1 + n (m + 1))$ reduction. Let $ell$ count non-loop edges, including multiplicity. The augmentation cost for any vertex ordering equals its total edge length minus $ell$. ][ - _Construction._ Let $G = (V, E)$ with $n = |V|$, $m = |E|$, and decision bound $K$. Build the $m times n$ binary matrix $A$ with rows indexed by edges and columns by vertices: for edge $e_i = {u, v}$ set $A[i][u] = A[i][v] = 1$ and all other entries of row $i$ to $0$. Each row has exactly two $1$'s. Set the augmentation bound to $"bound" = K - m$. + _Construction._ If $K < ell$, emit the fixed matrix $[[1,1,0],[0,1,1],[1,0,1]]$ with bound zero. Otherwise, form the edge-vertex incidence matrix and use augmentation bound $K - ell$. Retain duplicate edge rows and single-one loop rows. When there are no edges, use one all-zero row of length $n$ to retain all vertex columns; this row is empty when $n = 0$. + + _Correctness._ Every non-loop edge contributes at least one to arrangement length, so $K < ell$ implies NO, including negative bounds. The fixed matrix is NO because among three column pairs at least one pair is not adjacent. In the incidence case, each non-loop row requires exactly $|f(u) - f(v)| - 1$ flips, each loop row requires zero flips, and the additional zero row requires zero flips. Thus $A(f) = S(f) - ell$, where $S$ is arrangement length. Consequently $A(f) <= K - ell$ holds exactly when $S(f) <= K$. The column permutation and the source arrangement are inverse bijections, proving both directions, including empty graphs and repeated edges. The extension to loops and empty graphs follows from this identity; these cases are not premises of the cited two-ones-per-edge proof. - _Correctness._ A column permutation is a bijection $f : V -> {1, dots, n}$. In the row for edge ${u, v}$ the two $1$'s sit at positions $f(u)$ and $f(v)$; making the row consecutive forces filling every zero strictly between them, i.e.\ $|f(u) - f(v)| - 1$ flips. Summed over all rows the total augmentation cost is $sum_({u,v} in E) (|f(u) - f(v)| - 1) = (sum_({u,v} in E) |f(u) - f(v)|) - m$. ($arrow.r.double$) If $G$ admits an arrangement of total length at most $K$, using it as the column permutation costs at most $K - m = "bound"$ flips, so the target is YES. ($arrow.l.double$) If $A$ can be made C1P with at most $"bound"$ flips, the witnessing column permutation gives an arrangement of total length at most $"bound" + m = K$, so the source is YES. + _Solution extraction._ Evaluate the target certificate once and reject it unless it is a permutation meeting the augmentation budget. Return its inverse permutation. No certificate passes for the NO matrix, so there is no source representative to invent in that branch. - _Edge inputs._ If $m = 0$ every arrangement has length $0 <= K$, so emit the always-YES $1 times 1$ matrix $[[0]]$ with bound $max(0, K)$. If $K < m$ the source is NO (every arrangement costs at least $m$), so emit the fixed $3 times 3$ cyclic-overlap matrix $[[1,1,0],[0,1,1],[1,0,1]]$ with bound $0$, which is NO under all $6$ column permutations. + _Arithmetic and size._ The non-loop edge count is converted to an exact signed integer. In the incidence branch $0 <= ell <= K <= 2^63 - 1$, so $K - ell$ is representable. Any accepted certificate has $S(f) <= K$, and its nonnegative edge-length partial sums are also representable. The target has at most $m + 3$ rows and $n + 3$ columns, covering both the incidence representation and the fixed NO matrix. - _Solution extraction._ Read the C1P column permutation $sigma$ (column $sigma(p)$ sits at position $p$) and return the inverse: vertex $v$ receives arrangement position $sigma^(-1)(v)$. ] #reduction-rule("SequencingToMinimizeWeightedCompletionTime", "ILP")[ @@ -13930,23 +13872,23 @@ The following reductions to Integer Linear Programming are straightforward formu #let hc_tsp_target_weights = hc_tsp.target.instance.edge_weights #let hc_tsp_weight_one = hc_tsp_target_edges.enumerate().filter(((i, _)) => hc_tsp_target_weights.at(i) == 1).map(((i, e)) => (e.at(0), e.at(1))) #let hc_tsp_weight_two = hc_tsp_target_edges.enumerate().filter(((i, _)) => hc_tsp_target_weights.at(i) == 2).map(((i, e)) => (e.at(0), e.at(1))) -#let hc_tsp_selected_edges = hc_tsp_target_edges.enumerate().filter(((i, _)) => hc_tsp_sol.target_config.at(i) == 1).map(((i, e)) => (e.at(0), e.at(1))) +#let hc_tsp_selected_edges = hc_tsp_target_edges.enumerate().filter(((i, _)) => hc_tsp_sol.target_config.at(i)).map(((i, e)) => (e.at(0), e.at(1))) #reduction-rule("HamiltonianCircuit", "TravelingSalesman", example: true, example-caption: [Cycle graph on $#hc_tsp_n$ vertices to weighted $K_#hc_tsp_n$], extra: [ #pred-commands( "pred create --example " + problem-spec(hc_tsp.source) + " -o hc.json", - "pred reduce hc.json --to " + target-spec(hc_tsp) + " -o bundle.json", + "pred reduce hc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hc.json --config " + hc_tsp_sol.source_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(hc_tsp_sol.source_config), ) - *Step 1 -- Start from the source graph.* The canonical source fixture is the cycle on vertices ${0, 1, 2, 3}$ with edges #hc_tsp_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The stored Hamiltonian-circuit witness is the permutation $[#hc_tsp_sol.source_config.map(str).join(", ")]$.\ + *Step 1 -- Start from the source graph.* The canonical source fixture is the cycle on vertices ${0, 1, 2, 3}$ with edges #hc_tsp_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The stored Hamiltonian-circuit witness is the permutation $[#fmt-values(hc_tsp_sol.source_config)]$.\ *Step 2 -- Complete the graph and encode adjacency by weights.* The target keeps the same $#hc_tsp_n$ vertices but adds the missing diagonals, so it becomes $K_#hc_tsp_n$ with $#graph-num-edges(hc_tsp.target.instance)$ undirected edges. The original cycle edges #hc_tsp_weight_one.map(e => $(#e.at(0), #e.at(1))$).join(", ") receive weight 1, while the diagonals #hc_tsp_weight_two.map(e => $(#e.at(0), #e.at(1))$).join(", ") receive weight 2.\ - *Step 3 -- Verify the canonical witness.* The stored target configuration $[#hc_tsp_sol.target_config.map(str).join(", ")]$ selects the tour edges #hc_tsp_selected_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). Its total cost is $1 + 1 + 1 + 1 = #hc_tsp_n$, so every chosen edge is a weight-1 source edge, and traversing the selected cycle recovers the Hamiltonian circuit $[#hc_tsp_sol.source_config.map(str).join(", ")]$.\ + *Step 3 -- Verify the canonical witness.* The stored target configuration $[#fmt-values(hc_tsp_sol.target_config)]$ selects the tour edges #hc_tsp_selected_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). Its total cost is $1 + 1 + 1 + 1 = #hc_tsp_n$, so every chosen edge is a weight-1 source edge, and traversing the selected cycle recovers the Hamiltonian circuit $[#fmt-values(hc_tsp_sol.source_config)]$.\ *Multiplicity:* The fixture stores one canonical witness. For the 4-cycle there are $4 times 2 = 8$ Hamiltonian-circuit permutations (choice of start vertex and direction), but they all induce the same undirected target edge set. ], @@ -13968,9 +13910,9 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example TSP -o tsp.json", - "pred reduce tsp.json --to " + target-spec(tsp_ilp) + " -o bundle.json", + "pred reduce tsp.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate tsp.json --config " + tsp_ilp_sol.source_config.map(str).join(","), + "pred evaluate tsp.json --config " + cli-config(tsp_ilp_sol.source_config), ) ], )[ @@ -14014,15 +13956,15 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example LongestPath -o longest-path.json", - "pred reduce longest-path.json --to " + target-spec(lp_ilp) + " -o bundle.json", + "pred reduce longest-path.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate longest-path.json --config " + lp_ilp_sol.source_config.map(str).join(","), + "pred evaluate longest-path.json --config " + cli-config(lp_ilp_sol.source_config), ) *Step 1 -- Orient each undirected edge.* The canonical witness has two source edges, so the reduction creates four directed-arc variables. The optimal witness sets $x_(0,1) = 1$ and $x_(1,2) = 1$, leaving the reverse directions at 0.\ - *Step 2 -- Add order variables.* The target has #lp_ilp.target.instance.num_vars variables and #lp_ilp.target.instance.constraints.len() constraints in total. The order block $bold(o) = (#lp_ilp_sol.target_config.slice(4, 7).map(str).join(", "))$ certifies the increasing path positions $0 < 1 < 2$.\ + *Step 2 -- Add order variables.* The target has #lp_ilp.target.instance.variables.len() variables and #lp_ilp.target.instance.constraints.len() constraints in total. The order block $bold(o) = (#lp_ilp_sol.target_config.slice(4, 7).map(str).join(", "))$ certifies the increasing path positions $0 < 1 < 2$.\ - *Step 3 -- Check the objective.* The target witness $bold(z) = (#lp_ilp_sol.target_config.map(str).join(", "))$ selects lengths $2$ and $3$, so the ILP objective is $5$, matching the source optimum. #sym.checkmark + *Step 3 -- Check the objective.* The target witness $bold(z) = (#fmt-values(lp_ilp_sol.target_config))$ selects lengths $2$ and $3$, so the ILP objective is $5$, matching the source optimum. #sym.checkmark ], )[ A simple $s$-$t$ path can be represented as one unit of directed flow from $s$ to $t$ on oriented copies of the undirected edges. Integer order variables then force the selected arcs to move strictly forward, which forbids detached directed cycles. @@ -14059,9 +14001,9 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example TSP -o tsp.json", - "pred reduce tsp.json --to " + target-spec(tsp_qubo) + " -o bundle.json", + "pred reduce tsp.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate tsp.json --config " + tsp_qubo_sol.source_config.map(str).join(","), + "pred evaluate tsp.json --config " + cli-config(tsp_qubo_sol.source_config), ) *Step 1 -- Encode each tour position as a binary variable.* A tour is a permutation of $n$ vertices. Introduce $n^2 = #tsp_qubo.target.instance.num_vars$ binary variables $x_(v,p)$: vertex $v$ is at position $p$. $ underbrace(x_(0,0) x_(0,1) x_(0,2), "vertex 0") #h(4pt) underbrace(x_(1,0) x_(1,1) x_(1,2), "vertex 1") #h(4pt) underbrace(x_(2,0) x_(2,1) x_(2,2), "vertex 2") $ @@ -14070,7 +14012,7 @@ The following reductions to Integer Linear Programming are straightforward formu *Step 3 -- Encode edge costs.* For each edge $(u,v)$ and position $p$, the products $x_(u,p) x_(v,(p+1) mod 3)$ and $x_(v,p) x_(u,(p+1) mod 3)$ add the edge weight $w_(u v)$ when vertices $u,v$ are consecutive in the tour. Since $K_3$ is complete, all pairs are edges with their actual weights.\ - *Step 4 -- Verify a solution.* The QUBO ground state $bold(x) = (#tsp_qubo_sol.target_config.map(str).join(", "))$ encodes a valid tour. Reading the permutation: each 3-bit group has exactly one 1 (valid permutation #sym.checkmark). The tour cost equals $w_(01) + w_(02) + w_(12) = 1 + 2 + 3 = 6$.\ + *Step 4 -- Verify a solution.* The QUBO ground state $bold(x) = (#fmt-values(tsp_qubo_sol.target_config))$ encodes a valid tour. Reading the permutation: each 3-bit group has exactly one 1 (valid permutation #sym.checkmark). The tour cost equals $w_(01) + w_(02) + w_(12) = 1 + 2 + 3 = 6$.\ *Multiplicity:* The fixture stores one canonical optimal witness. On $K_3$ with distinct edge weights $1, 2, 3$, every Hamiltonian cycle has cost $1 + 2 + 3 = 6$ (all edges used), and 3 cyclic tours $times$ 2 directions yield $6$ permutation matrices overall. ], @@ -14096,13 +14038,13 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example LCS -o lcs.json", - "pred reduce lcs.json --to " + target-spec(lcs_mis) + " -o bundle.json", + "pred reduce lcs.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate lcs.json --config " + lcs_mis_sol.source_config.map(str).join(","), + "pred evaluate lcs.json --config " + cli-config(lcs_mis_sol.source_config), ) - Source LCS: config $(#lcs_mis_sol.source_config.map(str).join(", "))$ \ - Target MIS: $S = {#lcs_mis_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ (size #lcs_mis_sol.target_config.filter(x => x == 1).len()) \ - MIS size $=$ LCS length $= #lcs_mis_sol.target_config.filter(x => x == 1).len()$ #sym.checkmark + Source LCS: config $(#fmt-values(lcs_mis_sol.source_config))$ \ + Target MIS: $S = {#lcs_mis_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ")}$ (size #lcs_mis_sol.target_config.filter(x => x).len()) \ + MIS size $=$ LCS length $= #lcs_mis_sol.target_config.filter(x => x).len()$ #sym.checkmark ], )[ A match-node construction transforms a $k$-string LCS instance into a Maximum Independent Set problem on a conflict graph. Each vertex represents a $k$-tuple of positions (one per string) that all share the same character, and edges connect pairs that cannot coexist in any valid common subsequence. The MIS of this graph equals the LCS length. @@ -14121,21 +14063,21 @@ The following reductions to Integer Linear Programming are straightforward formu #let cs_ilp_str = load-example( "ClosestString", "ILP", - target-variant: (variable: "i32"), + target-variant: (coefficient: "i64", variable: "i64"), ) #let cs_ilp_str_sol = cs_ilp_str.solutions.at(0) #reduction-rule("ClosestString", "ILP", example: true, - example-target-variant: (variable: "i32"), + example-target-variant: (coefficient: "i64", variable: "i64"), example-caption: [Binary alphabet, 4 length-3 strings], extra: [ #pred-commands( "pred create --example " + problem-spec(cs_ilp_str.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(cs_ilp_str) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + cs_ilp_str_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(cs_ilp_str_sol.source_config), ) - Source center witness $(#cs_ilp_str_sol.source_config.map(str).join(", "))$, target ILP witness $(#cs_ilp_str_sol.target_config.map(str).join(", "))$. + Source center witness $(#fmt-values(cs_ilp_str_sol.source_config))$, target ILP witness $(#fmt-values(cs_ilp_str_sol.target_config))$. ], )[ Binary variables select one alphabet symbol at each center position. An auxiliary radius variable upper-bounds the Hamming distance from the chosen center to every input string and is minimized. @@ -14164,21 +14106,21 @@ The following reductions to Integer Linear Programming are straightforward formu #let css_ilp = load-example( "ClosestSubstring", "ILP", - target-variant: (variable: "i32"), + target-variant: (coefficient: "i64", variable: "i64"), ) #let css_ilp_sol = css_ilp.solutions.at(0) #reduction-rule("ClosestSubstring", "ILP", example: true, - example-target-variant: (variable: "i32"), + example-target-variant: (coefficient: "i64", variable: "i64"), example-caption: [Binary alphabet, 3 length-5 strings, length-3 windows], extra: [ #pred-commands( "pred create --example " + problem-spec(css_ilp.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(css_ilp) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + css_ilp_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(css_ilp_sol.source_config), ) - Source center+windows witness $(#css_ilp_sol.source_config.map(str).join(", "))$, target ILP witness $(#css_ilp_sol.target_config.map(str).join(", "))$. + Source center+windows witness $(#fmt-values(css_ilp_sol.source_config))$, target ILP witness $(#fmt-values(css_ilp_sol.target_config))$. ], )[ Integer variables select one alphabet symbol at each center position and one window start per input string. A conditional radius constraint is activated by the window-choice indicator and upper-bounds the Hamming distance between the center and the selected window of each string. @@ -14261,82 +14203,45 @@ The following reductions to Integer Linear Programming are straightforward formu #let st_ilp = load-example("SteinerTree", "ILP") #let st_ilp_sol = st_ilp.solutions.at(0) -#let st_edges = st_ilp.source.instance.graph.edges -#let st_weights = st_ilp.source.instance.edge_weights -#let st_terminals = st_ilp.source.instance.terminals -#let st_root = st_terminals.at(0) -#let st_non_root_terminals = range(1, st_terminals.len()).map(i => st_terminals.at(i)) -#let st_selected_edge_indices = st_ilp_sol.source_config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) -#let st_selected_edges = st_selected_edge_indices.map(i => st_edges.at(i)) -#let st_cost = st_selected_edge_indices.map(i => st_weights.at(i)).sum() - #reduction-rule("SteinerTree", "ILP", example: true, - example-caption: [Canonical Steiner tree instance ($n = #st_ilp.source.instance.graph.num_vertices$, $m = #st_edges.len()$, $|T| = #st_terminals.len()$)], + example-caption: [Signed-weight exact tree formulation], extra: [ #pred-commands( "pred create --example SteinerTree -o steinertree.json", - "pred reduce steinertree.json --to " + target-spec(st_ilp) + " -o bundle.json", + "pred reduce steinertree.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate steinertree.json --config " + st_ilp_sol.source_config.map(str).join(","), + "pred evaluate steinertree.json --config " + cli-config(st_ilp_sol.source_config), ) - *Step 1 -- Choose a root and one commodity per remaining terminal.* The canonical source instance has terminals $T = {#st_terminals.map(t => $v_#t$).join(", ")}$. The reduction fixes the first terminal as root $r = v_#st_root$ and creates one flow commodity for each remaining terminal: $v_#st_non_root_terminals.at(0)$ and $v_#st_non_root_terminals.at(1)$. - - *Step 2 -- Count the variables from the source edge order.* The first #st_edges.len() target variables are the edge selectors $bold(y) = (#st_ilp_sol.target_config.slice(0, st_edges.len()).map(str).join(", "))$, one per source edge in the order #st_edges.enumerate().map(((i, e)) => [$e_#i = (#(e.at(0)), #(e.at(1)))$]).join(", "). The remaining #(st_ilp.target.instance.num_vars - st_edges.len()) variables are directed flow indicators: $2 m (|T| - 1) = 2 times #st_edges.len() times #st_non_root_terminals.len() = #(st_ilp.target.instance.num_vars - st_edges.len())$. - - *Step 3 -- Count the constraints commodity-by-commodity.* Each non-root terminal contributes one flow-conservation equality per vertex and two capacity inequalities per source edge. For this fixture that is $#st_ilp.source.instance.graph.num_vertices times #st_non_root_terminals.len() = #(st_ilp.source.instance.graph.num_vertices * st_non_root_terminals.len())$ equalities plus $#(2 * st_edges.len()) times #st_non_root_terminals.len() = #(2 * st_edges.len() * st_non_root_terminals.len())$ inequalities, totaling #st_ilp.target.instance.constraints.len() constraints. - - *Step 4 -- Read the canonical witness pair.* The source witness selects edges ${#st_selected_edges.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")}$, so $bold(y)$ already encodes the Steiner tree. In the target witness, the commodity for $v_2$ routes along $v_0 arrow v_1 arrow v_2$, while the commodity for $v_4$ routes along $v_0 arrow v_1 arrow v_3 arrow v_4$. Every flow 1-entry therefore sits under a selected edge variable #sym.checkmark - - *Step 5 -- Verify the objective end-to-end.* The selected-edge prefix is $bold(y) = (#st_ilp_sol.target_config.slice(0, st_edges.len()).map(str).join(", "))$, matching the source witness $(#st_ilp_sol.source_config.map(str).join(", "))$. The ILP objective is #st_selected_edge_indices.map(i => $#(st_weights.at(i))$).join($+$) $= #st_cost$, exactly the Steiner tree optimum stored in the fixture. - - *Multiplicity:* The fixture stores one canonical witness. Other optimal Steiner trees could yield different feasible ILP witnesses, but every valid witness still exposes the source solution in the first $m$ variables. + *Source.* The canonical instance has five vertices, seven edges, and terminals $T={0,2,4}$. + *Target.* Seven edge selectors, five vertex selectors, and four commodities with fourteen directed flow variables each give 68 binary variables and 94 constraints. Every selected vertex receives its own root flow. The edge prefix $(#fmt-values(st_ilp_sol.source_config))$ selects the tree of cost 6; the vertex selectors and flows certify connectivity, while the cardinality equality excludes cycles. ], )[ - The rooted multi-commodity flow formulation @wong1984steiner @kochmartin1998steiner introduces one binary selector $y_e$ for each source edge and, for every non-root terminal $t$, one binary flow variable on each directed source edge. Flow conservation sends one unit from the root to each terminal, while the linking inequalities $f^t_(u,v) <= y_e$ ensure that every used flow arc is backed by a selected source edge. The resulting binary ILP has $m + 2 m (k - 1)$ variables and $n (k - 1) + 2 m (k - 1)$ constraints. + Terminal-only flow formulations rely on nonnegative costs @goemansmyung1993catalog. To support signed weights and direct extraction of every feasible witness, use a commodity for every selected vertex and explicitly impose the tree cardinality identity. ][ - _Construction._ Given an undirected weighted graph $G = (V, E, w)$ with strictly positive edge weights, terminals $T = {t_0, dots, t_(k-1)}$, and root $r = t_0$, introduce binary edge selectors $y_e in {0,1}$ for every $e in E$. For each non-root terminal $t in T backslash {r}$ and each directed copy of an undirected edge $(u, v) in E$, introduce a binary flow variable $f^t_(u,v) in {0,1}$. The target objective is - $ min sum_(e in E) w_e y_e. $ - For every commodity $t$ and vertex $v$, enforce flow conservation: - $ sum_(u : (u, v) in A) f^t_(u,v) - sum_(u : (v, u) in A) f^t_(v,u) = b_(t,v), $ - where $A$ contains both orientations of every undirected edge, $b_(t,v) = -1$ at the root $v = r$, $b_(t,v) = 1$ at the sink $v = t$, and $b_(t,v) = 0$ otherwise. For every commodity $t$ and undirected edge $e = {u, v}$, add the capacity-linking inequalities - $ f^t_(u,v) <= y_e quad "and" quad f^t_(v,u) <= y_e. $ - Binary flow variables suffice because any Steiner tree yields a unique simple root-to-terminal path for each commodity, so every commodity can be realized as a 0/1 path indicator. - - The ILP is: - $ - min quad & sum_(e in E) w_e y_e \ - "subject to" quad & sum_(u : (u, v) in A) f^t_(u,v) - sum_(u : (v, u) in A) f^t_(v,u) = b_(t,v) quad forall t in T backslash {r}, v in V \ - & f^t_(u,v) <= y_e quad forall t in T backslash {r}, e = {u, v} in E \ - & f^t_(v,u) <= y_e quad forall t in T backslash {r}, e = {u, v} in E \ - & y_e, f^t_(u,v) in {0, 1}. - $ - - _Correctness._ ($arrow.r.double$) If $S subset.eq E$ is a Steiner tree, set $y_e = 1$ exactly for $e in S$. For each non-root terminal $t$, the unique path from $r$ to $t$ inside the tree defines a binary flow assignment satisfying the conservation equations, and every used arc lies on a selected edge, so all linking inequalities hold. The ILP objective equals $sum_(e in S) w_e$. ($arrow.l.double$) Any feasible ILP solution with edge selector set $Y = {e in E : y_e = 1}$ supports one unit of flow from $r$ to every non-root terminal, so the selected edges contain a connected subgraph spanning all terminals. Because all edge weights are strictly positive, any cycle in the selected subgraph has positive total cost; the optimizer therefore never includes redundant edges, so the selected subgraph is already a Steiner tree. Therefore an optimal ILP solution induces a minimum-cost Steiner tree. + _Construction._ Let $n=|V|$, $m=|E|$, $k=|T|$, and let $r$ be the first terminal. Binary $y_e$ selects edges, binary $z_v$ selects vertices, and binary $f^s_a$ carries a commodity for every $s in V backslash {r}$ on the bidirected source graph. Require $y_e<=z_u,z_v$ for each $e={u,v}$, $z_t=1$ for all terminals, and $sum_e y_e-sum_v z_v=-1$. For each sink $s$, inflow minus outflow equals $z_s$ at $s$, $-z_s$ at $r$, and zero elsewhere, with $f^s_a<=y_e$ for both orientations of edge $e$. Minimize $sum_e w_e y_e$ without changing weights. There are $m+n+2m(n-1)$ variables and $n(n-1)+2m n+k+1$ constraints. - _Variable mapping._ The first $m$ ILP variables are the source-edge indicators $y_0, dots, y_(m-1)$ in source edge order. For terminal $t_p$ with $p in {1, dots, k-1}$, the next block of $2 m$ variables stores the directed arc indicators $f^(t_p)_(u,v)$ and $f^(t_p)_(v,u)$ for each source edge $(u, v)$. + _Correctness._ Any source tree lifts by selecting its incident vertices and routing one unit along the unique root path to each selected sink; unselected sinks use zero flows. Conversely, a selected vertex outside the root component contradicts flow conservation summed over its component: all crossing flows vanish, but net demand is one. Thus all selected vertices are connected. Endpoint linking and the equality $|E'|=|V'|-1$ imply that the selected graph is a tree spanning all terminals. Both directions preserve the exact edge cost for positive, zero and negative weights; no optimality-based pruning is used. - _Solution extraction._ Read the first $m$ target variables as the source edge-selection vector. Since those coordinates are exactly the $y_e$ variables, the extracted source configuration is valid whenever the selected subgraph is pruned to its Steiner tree witness. - - _Remark._ Zero-weight edges are excluded because they allow degenerate optimal ILP solutions containing redundant cycles at no cost; following the convention of practical solvers (e.g., SCIP-Jack @kochmartin1998steiner), such edges should be contracted before applying the reduction. + _Extraction and numeric domain._ Validate the full target assignment and reject infeasibility, then return its first $m$ edge-selector coordinates. Source `Min` and target `Extremum` with minimization sense use identical objective sums. Checked counts precede allocation; structural coefficients are only $-1,0,1$. Existing typed objective-overflow checks remain in force. The endpoints and source tree definition are unchanged. ] #let mvc_hs = load-example("MinimumVertexCover", "MinimumHittingSet") #let mvc_hs_sol = mvc_hs.solutions.at(0) -#let mvc_hs_cover = mvc_hs_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) -#let mvc_hs_hit = mvc_hs_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let mvc_hs_cover = mvc_hs_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) +#let mvc_hs_hit = mvc_hs_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #reduction-rule("MinimumVertexCover", "MinimumHittingSet", example: true, example-caption: [Unit-weight VC to Hitting Set ($n = #graph-num-vertices(mvc_hs.source.instance)$, $|E| = #graph-num-edges(mvc_hs.source.instance)$)], extra: [ #pred-commands( "pred create --example 'MVC {weight: One}' -o mvc.json", - "pred reduce mvc.json --to " + target-spec(mvc_hs) + " -o bundle.json", + "pred reduce mvc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mvc.json --config " + mvc_hs_sol.source_config.map(str).join(","), + "pred evaluate mvc.json --config " + cli-config(mvc_hs_sol.source_config), ) - Source VC: $C = {#mvc_hs_cover.map(str).join(", ")}$ (size #mvc_hs_cover.len()) #h(1em) - Target HS: $H = {#mvc_hs_hit.map(str).join(", ")}$ (size #mvc_hs_hit.len()) \ + Source VC: $C = {#fmt-values(mvc_hs_cover)}$ (size #mvc_hs_cover.len()) #h(1em) + Target HS: $H = {#fmt-values(mvc_hs_hit)}$ (size #mvc_hs_hit.len()) \ The hitting set $H$ is identical to the vertex cover $C$ because the universe elements are the vertices and the subsets are the edges. ], )[ @@ -14426,21 +14331,21 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(mono_ilp.source) + " -o monochromatic-triangle.json", - "pred reduce monochromatic-triangle.json --to " + target-spec(mono_ilp) + " -o bundle.json", + "pred reduce monochromatic-triangle.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate monochromatic-triangle.json --config " + mono_ilp_sol.source_config.map(str).join(","), + "pred evaluate monochromatic-triangle.json --config " + cli-config(mono_ilp_sol.source_config), ) *Step 1 -- Source instance.* The canonical Monochromatic Triangle fixture is $K_4$ on vertices $0, 1, 2, 3$ with edges #{mono_ilp.source.instance.graph.edges.map(((u, v)) => [${#u, #v}$]).join(", ")}. It has $#mono_ilp.source.instance.triangles.len()$ triangles, so the reduction creates one pair of inequalities for each of those four triangles. - *Step 2 -- Build the ILP.* Introduce one binary variable per edge, so the target has $m = #mono_ilp.target.instance.num_vars$ variables. For each triangle, add the lower bound $x_a + x_b + x_c >= 1$ and the upper bound $x_a + x_b + x_c <= 2$, giving $#mono_ilp.target.instance.constraints.len()$ total constraints. + *Step 2 -- Build the ILP.* Introduce one binary variable per edge, so the target has $m = #mono_ilp.target.instance.variables.len()$ variables. For each triangle, add the lower bound $x_a + x_b + x_c >= 1$ and the upper bound $x_a + x_b + x_c <= 2$, giving $#mono_ilp.target.instance.constraints.len()$ total constraints. - *Step 3 -- Verify a witness.* The stored ILP witness is $(#mono_ilp_sol.target_config.map(str).join(", "))$. Because extraction is identity, it immediately yields the edge coloring $(#mono_ilp_sol.source_config.map(str).join(", "))$, and evaluating that coloring on the source returns `true` #sym.checkmark. Every triangle therefore uses both colors. + *Step 3 -- Verify a witness.* The stored ILP witness is $(#fmt-values(mono_ilp_sol.target_config))$. Because extraction is identity, it immediately yields the edge coloring $(#fmt-values(mono_ilp_sol.source_config))$, and evaluating that coloring on the source returns `true` #sym.checkmark. Every triangle therefore uses both colors. *Multiplicity:* The fixture stores one canonical edge coloring. Any binary ILP solution satisfying all triangle pairs is a valid Monochromatic Triangle witness. ], )[ - This $O(m + t)$ reduction uses one binary variable per edge and two linear inequalities per triangle, where $m = |E|$ and $t$ is the number of triangles in the source graph. The target ILP is a pure feasibility problem with $m$ variables and $2t$ constraints. + Use one binary variable per edge, two inequalities per triangle, and valid degree equalities in every five-vertex clique. The latter strengthen the linear relaxation without changing the set of feasible edge colourings. If $m=|E|$, $t$ is the triangle count and $h$ is the number of five-vertex cliques, the target has $m$ variables and at most $2t+15h$ constraints. Since $15h <= |V|^5/8$, enumeration and target size remain polynomial. ][ _Construction._ Let the source graph edges be indexed as $e_0, dots, e_(m-1)$. Introduce binary variables $x_0, dots, x_(m-1)$, where $x_i = 0$ or $1$ is the color assigned to edge $e_i$. For every triangle $T = {e_a, e_b, e_c}$ in the source graph, add the pair of inequalities $ @@ -14448,7 +14353,9 @@ The following reductions to Integer Linear Programming are straightforward formu quad "and" quad x_a + x_b + x_c <= 2. $ - The objective is empty, so the ILP asks only for feasibility. + For each five-vertex clique $C$ and every $v in C$, also impose $sum_(u in C backslash {v}) x_({u,v}) = 2$. For each triangle, enumerate edges between its common neighbours; each such edge completes a $K_5$. Any two edges opposite the same triangle must also have equal colours, so add equality constraints linking them to the first such edge. Add the degree equalities once per clique, when the base triangle consists of its three smallest vertices. The objective is empty, so the ILP asks only for feasibility. + + _Validity of the added equalities._ In any colouring of $K_5$ without monochromatic triangles, a vertex cannot have three neighbours joined to it by the same colour: all three edges between those neighbours would have to use the opposite colour, producing a monochromatic triangle. Every vertex has degree four inside the clique, so it has exactly two edges of each colour. Thus every valid source colouring satisfies the degree equalities. For a fixed colour, write $r$ for its edge count inside a triangle of the clique and $t'$ for the colour of the complementary edge. The same degree count gives $5=r+(6-2r)+t'$, so $t'=r-1$: every edge opposite the same triangle has the same colour. Hence all added equalities apply to any input graph, independently of its origin, and exclude no legal colouring. If $k_T$ cliques contain triangle $T$, at most $k_T$ linking equalities are added there; $sum_T k_T=10h$. Together with $5h$ degree equalities this gives the stated constraint bound. These explicit linking rows let ILP presolve eliminate colour copies. _Correctness._ ($arrow.r.double$) Any triangle-free 2-edge-coloring assigns each triangle at least one edge of color 0 and at least one edge of color 1, so the corresponding sum is either $1$ or $2$ and both inequalities hold. ($arrow.l.double$) Any feasible ILP assignment gives a 0/1 color to every edge, and the triangle bounds forbid sums $0$ and $3$, so no triangle is monochromatic. @@ -14463,9 +14370,9 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(ss_bt.source) + " -o set-splitting.json", - "pred reduce set-splitting.json --to " + target-spec(ss_bt) + " -o bundle.json", + "pred reduce set-splitting.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate set-splitting.json --config " + ss_bt_sol.source_config.map(str).join(","), + "pred evaluate set-splitting.json --config " + cli-config(ss_bt_sol.source_config), ) #{ @@ -14475,13 +14382,13 @@ The following reductions to Integer Linear Programming are straightforward formu let target_config = ss_bt_sol.target_config let pole = source.universe_size [ - *Step 1 -- Source instance.* The canonical Set Splitting fixture has universe $U = {0, 1, 2, 3, 4}$ and subsets $S_1 = {#source.subsets.at(0).map(str).join(", ")}$, $S_2 = {#source.subsets.at(1).map(str).join(", ")}$, $S_3 = {#source.subsets.at(2).map(str).join(", ")}$, and $S_4 = {#source.subsets.at(3).map(str).join(", ")}$. The stored splitting is $(#source_config.map(str).join(", "))$, so colors 0 and 1 both appear in every subset. + *Step 1 -- Source instance.* The canonical Set Splitting fixture has universe $U = {0, 1, 2, 3, 4}$ and subsets $S_1 = {#source.subsets.at(0).map(str).join(", ")}$, $S_2 = {#source.subsets.at(1).map(str).join(", ")}$, $S_3 = {#source.subsets.at(2).map(str).join(", ")}$, and $S_4 = {#source.subsets.at(3).map(str).join(", ")}$. The stored splitting is $(#fmt-values(source_config))$, so colors 0 and 1 both appear in every subset. *Step 2 -- Add the pole and clause auxiliaries.* Because every subset already has size 3, normalization adds no universe elements. The target therefore uses pole $p = a_#pole$ together with one auxiliary element for each subset, namely $d_1 = 6$, $d_2 = 7$, $d_3 = 8$, and $d_4 = 9$, for a total of $#target.num_elements$ elements. *Step 3 -- Form the betweenness triples.* The four subsets become the triple pairs $(#target.triples.at(0).map(str).join(", "))$, $(#target.triples.at(1).map(str).join(", "))$; $(#target.triples.at(2).map(str).join(", "))$, $(#target.triples.at(3).map(str).join(", "))$; $(#target.triples.at(4).map(str).join(", "))$, $(#target.triples.at(5).map(str).join(", "))$; and $(#target.triples.at(6).map(str).join(", "))$, $(#target.triples.at(7).map(str).join(", "))$. Each pair uses one auxiliary $d_j$ to force the corresponding 3-set to place at least one element on each side of the pole. - *Step 4 -- Verify the ordering and extraction.* The stored Betweenness witness is $f = (#target_config.map(str).join(", "))$, so the pole $p = 5$ sits at position $f(p) = #target_config.at(pole)$. Original elements $1, 3, 4$ lie to the left of the pole, while $0, 2$ lie to the right, so extraction returns $(#source_config.map(str).join(", "))$ exactly. For example, $(0, 6, 1)$ holds because $f(1) = #target_config.at(1) < f(6) = #target_config.at(6) < f(0) = #target_config.at(0)$, and $(6, 5, 2)$ holds because $f(6) = #target_config.at(6) < f(5) = #target_config.at(5) < f(2) = #target_config.at(2)$ #sym.checkmark. + *Step 4 -- Verify the ordering and extraction.* The stored Betweenness witness is $f = (#fmt-values(target_config))$, so the pole $p = 5$ sits at position $f(p) = #target_config.at(pole)$. Original elements $1, 3, 4$ lie to the left of the pole, while $0, 2$ lie to the right, so extraction returns $(#fmt-values(source_config))$ exactly. For example, $(0, 6, 1)$ holds because $f(1) = #target_config.at(1) < f(6) = #target_config.at(6) < f(0) = #target_config.at(0)$, and $(6, 5, 2)$ holds because $f(6) = #target_config.at(6) < f(5) = #target_config.at(5) < f(2) = #target_config.at(2)$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ] @@ -14538,9 +14445,9 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(kc_bcbs.source) + " -o kclique.json", - "pred reduce kclique.json --to " + target-spec(kc_bcbs) + " -o bundle.json", + "pred reduce kclique.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate kclique.json --config " + kc_bcbs_sol.source_config.map(str).join(","), + "pred evaluate kclique.json --config " + cli-config(kc_bcbs_sol.source_config), ) *Step 1 -- Pad the vertex set.* $C(#k, 2) = #ck2$ padding vertices are added, giving $n' = #n + #ck2 = #n_prime$ left vertices (Part $A$). @@ -14551,7 +14458,7 @@ The following reductions to Integer Linear Programming are straightforward formu *Step 4 -- Set target parameter.* $K' = n' - k = #n_prime - #k = #target_k$. - *Step 5 -- Verify a solution.* The #k\-clique is $S = {#kc_bcbs_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => str(i)).join(", ")}$. The #target_k left vertices NOT in $S$ plus the #ck2 padding vertices form the left side $A'$. The right side $B'$ contains the #ck2 intra-clique edge elements plus #{n - k} padding elements ($|B'| = #target_k$). All $#target_k times #target_k$ cross-edges are present because no $v in A'$ is an endpoint of any selected edge element. + *Step 5 -- Verify a solution.* The #k\-clique is $S = {#kc_bcbs_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$. The #target_k left vertices NOT in $S$ plus the #ck2 padding vertices form the left side $A'$. The right side $B'$ contains the #ck2 intra-clique edge elements plus #{n - k} padding elements ($|B'| = #target_k$). All $#target_k times #target_k$ cross-edges are present because no $v in A'$ is an endpoint of any selected edge element. *Multiplicity:* The fixture stores one canonical witness. ], @@ -14598,14 +14505,14 @@ The following reductions to Integer Linear Programming are straightforward formu let m-target = mmm_ach.target.instance.graph.edges.len() let color-of = mmm_ach_sol.target_config let matched = mmm_ach_sol.source_config.enumerate() - .filter(((i, x)) => x == 1) + .filter(((i, x)) => x) .map(((i, _)) => i) [ #pred-commands( "pred create --example " + problem-spec(mmm_ach.source) + " -o mmm.json", - "pred reduce mmm.json --to " + target-spec(mmm_ach) + " -o bundle.json", + "pred reduce mmm.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mmm.json --config " + mmm_ach_sol.source_config.map(str).join(","), + "pred evaluate mmm.json --config " + cli-config(mmm_ach_sol.source_config), ) *Step 1 -- Source instance.* The T-tree on $5$ vertices is the spider graph with centre $v_1$ and legs to $v_0$, $v_2$, $v_4$, plus the pendant edge $v_2 - v_3$. It is bipartite with $A = {v_0, v_2, v_4}$ and $B = {v_1, v_3}$. In unified indices the vertex set is ${0, 1, 2, 3, 4}$ (left vertices first, mapping $v_0 mapsto 0$, $v_2 mapsto 1$, $v_4 mapsto 2$, $v_1 mapsto 3$, $v_3 mapsto 4$), so $n = #n-source$ and the $m = #m-source$ edges are #source-edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). @@ -14614,7 +14521,7 @@ The following reductions to Integer Linear Programming are straightforward formu *Step 3 -- Source optimum.* The minimum maximal matching uses the central edge $(v_1, v_2)$, so $"mm"(G) = #matched.len() = 1$ (source index $#matched.at(0)$). The T-tree also admits two strictly larger maximal matchings $\{(v_0, v_1), (v_2, v_3)\}$ and $\{(v_1, v_4), (v_2, v_3)\}$, both of size $2$ -- this richness is the reason for choosing the T-tree over the path $P_4$ as the canonical example. - *Step 4 -- Target optimum.* The achromatic coloring stored in the fixture is $#color-of.map(str).join(", ")$. The size-$2$ color class corresponds to the source edge selected in Step 3, and the singletons contribute the remaining $n - 2$ classes, so the achromatic number is $psi(H) = n - "mm"(G) = #n-source - 1 = #(n-source - 1) #sym.checkmark$. + *Step 4 -- Target optimum.* The achromatic coloring stored in the fixture is $#fmt-values(color-of)$. The size-$2$ color class corresponds to the source edge selected in Step 3, and the singletons contribute the remaining $n - 2$ classes, so the achromatic number is $psi(H) = n - "mm"(G) = #n-source - 1 = #(n-source - 1) #sym.checkmark$. *Multiplicity:* The fixture stores one canonical witness; other valid achromatic $4$-colorings exist and would extract to the same minimum maximal matching after relabelling colors. ] @@ -14656,17 +14563,17 @@ The following reductions to Integer Linear Programming are straightforward formu let s-cfg = mmm_mmd_sol.source_config let t-cfg = mmm_mmd_sol.target_config let matched-source = s-cfg.enumerate() - .filter(((i, x)) => x == 1) + .filter(((i, x)) => x) .map(((i, _)) => i) let selected-target = t-cfg.enumerate() - .filter(((i, x)) => x == 1) + .filter(((i, x)) => x) .map(((i, _)) => i) [ #pred-commands( "pred create --example " + problem-spec(mmm_mmd.source) + " -o mmm.json", - "pred reduce mmm.json --to " + target-spec(mmm_mmd) + " -o bundle.json", + "pred reduce mmm.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mmm.json --config " + s-cfg.map(str).join(","), + "pred evaluate mmm.json --config " + cli-config(s-cfg), ) *Step 1 -- Source instance.* Bipartite graph $B$ with $|L| = #m-left$, $|R| = #n-right$ and $#local-edges.len()$ bipartite-local edges #local-edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The decision threshold is $K = 2$. @@ -14807,11 +14714,12 @@ The following reductions to Integer Linear Programming are straightforward formu $ "minimize" quad & sum_v s_v x_v + sum_(v,u) "usage"_v d(v, u) y_(v,u) \ "subject to" quad & sum_u y_(v,u) = 1 quad forall v \ - & y_(v,u) <= x_u quad forall v, u \ + & y_(v,u) <= x_u quad forall v, u " with " d(v,u) < infinity \ + & y_(v,u) = 0 quad forall v, u " with " d(v,u) = infinity \ & x_v, y_(v,u) in {0, 1}. $ - _Correctness._ Assignment constraints ensure each vertex is served by exactly one copy; capacity links prevent assignment to non-copy vertices; the objective linearizes the total cost. + _Correctness._ Assignment constraints ensure each vertex is served by exactly one reachable copy; unreachable pairs are explicitly forbidden, as required by the shortest-path distance in the problem definition @garey1979. Capacity links prevent assignment to non-copy vertices, and the objective linearizes the total cost. _Solution extraction._ Copy placement: ${v : x_v = 1}$. ] @@ -14946,12 +14854,12 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example PartitionIntoPathsOfLength2 -o ppl2.json", - "pred reduce ppl2.json --to " + target-spec(ppl2_bcsf) + " -o bundle.json", + "pred reduce ppl2.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ppl2.json --config " + ppl2_bcsf_sol.source_config.map(str).join(","), + "pred evaluate ppl2.json --config " + cli-config(ppl2_bcsf_sol.source_config), ) - Source PPL2: groups $= {#ppl2_bcsf_sol.source_config.map(str).join(", ")}$ on a graph with $n = #graph-num-vertices(ppl2_bcsf.source.instance)$ vertices and $|E| = #graph-num-edges(ppl2_bcsf.source.instance)$ edges \ - Target BCSF: components $= {#ppl2_bcsf_sol.target_config.map(str).join(", ")}$, $K = #ppl2_bcsf.target.instance.max_components$, $B = #ppl2_bcsf.target.instance.max_weight$ \ + Source PPL2: groups $= {#fmt-values(ppl2_bcsf_sol.source_config)}$ on a graph with $n = #graph-num-vertices(ppl2_bcsf.source.instance)$ vertices and $|E| = #graph-num-edges(ppl2_bcsf.source.instance)$ edges \ + Target BCSF: components $= {#fmt-values(ppl2_bcsf_sol.target_config)}$, $K = #ppl2_bcsf.target.instance.max_components$, $B = #ppl2_bcsf.target.instance.max_weight$ \ Identity mapping: source and target configs coincide #sym.checkmark ], )[ @@ -15015,7 +14923,7 @@ The following reductions to Integer Linear Programming are straightforward formu & x_(j,t) in {0, 1}. $ - _Correctness._ Per-task deadline is enforced by restricting the time domain of each task's indicator variables. + _Correctness._ Per-task deadline is enforced by restricting the time domain of each task's indicator variables. The implementation allocates a common horizon $max_j d_j$ and adds one zero-sum equality fixing all indicators outside individual deadlines to zero. _Solution extraction._ Task $j$ is scheduled at time $arg max_t x_(j,t)$. ] @@ -15049,7 +14957,7 @@ The following reductions to Integer Linear Programming are straightforward formu & x_(j,t) in {0, 1}. $ - _Correctness._ One-hot ensures each task starts once within its feasible window; non-overlap prevents simultaneous execution. + _Correctness._ One-hot ensures each task starts once within its feasible window; non-overlap prevents simultaneous execution. If a task has no feasible start time, its one-hot constraint is $0 = 1$, making the ILP infeasible. _Solution extraction._ Task $j$ starts at time $arg max_t x_(j,t)$; config$[j] = t - r_j$. ] @@ -15083,7 +14991,9 @@ The following reductions to Integer Linear Programming are straightforward formu & d^k_e in {0, 1}, f^k_(u,v) in ZZ_(>=0). $ - _Correctness._ Direction indicators linearize the capacity-sharing constraint; flow conservation and demand constraints ensure valid multi-commodity flow. + Here $b^k_v = 0$ at every vertex other than commodity $k$'s own source and sink; the sink receives at least its requirement and the source supplies that flow. + + _Correctness._ Direction indicators linearize the capacity-sharing constraint. Per-commodity conservation prevents flow from being created or destroyed at another commodity's terminals, as required by the standard multicommodity-flow formulation @garey1979. _Solution extraction._ Flow variables (first $4|E|$ variables). ] @@ -15179,45 +15089,48 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("DisjointConnectingPaths", "ILP")[ - Route one unit of flow for each terminal pair on an oriented copy of the graph, and forbid internal vertices from carrying more than one commodity. + Use a binary multicommodity flow with one commodity per terminal pair and unit capacity on every vertex. The construction has $2 k |E|$ variables and $k |V| + |V|$ constraints. ][ - _Construction._ For terminal pairs $(s_k, t_k)$, variables: binary $f^k_(u,v)$ on each orientation of each edge and integer order variables $h^k_v$. The ILP is: + _Construction._ Let $A$ contain both orientations of every edge, and let $S = {s_1, dots, s_k}$. For each pair $(s_i,t_i)$ and arc $(u,v) in A$, introduce a binary variable $f^i_(u,v)$. Write $delta^+(v)$ and $delta^-(v)$ for the arcs leaving and entering $v$. Add $ - "find" quad & bold(x) \ - "subject to" quad & sum_(w) f^k_(s_k,w) - sum_(u) f^k_(u,s_k) = 1 quad forall k \ - & sum_(u) f^k_(u,t_k) - sum_(w) f^k_(t_k,w) = 1 quad forall k \ - & sum_(w) f^k_(v,w) - sum_(u) f^k_(u,v) = 0 quad forall k, v in V backslash {s_k, t_k} \ - & f^k_(u,v) + f^k_(v,u) <= 1 quad forall {u, v} in E, k \ - & sum_k sum_(w in N(v)) f^k_(v,w) <= 1 quad forall "non-terminal" v \ - & h^k_v >= h^k_u + 1 - M (1 - f^k_(u,v)) quad forall k, u -> v \ - & f^k_(u,v) in {0, 1}, h^k_v in ZZ_(>=0). + sum_(a in delta^+(v)) f^i_a - sum_(a in delta^-(v)) f^i_a + = cases(1 & "if " v=s_i, -1 & "if " v=t_i, 0 & "otherwise") + quad forall i,v, + $ + and the vertex-capacity constraints + $ + sum_i sum_(a in delta^-(v)) f^i_a + bb(1)[v in S] <= 1 + quad forall v in V. $ + The source model requires all $2k$ terminal vertices to be pairwise distinct. - _Correctness._ ($arrow.r.double$) A family of pairwise internally vertex-disjoint connecting paths orients each path from its source to its sink and satisfies all constraints. ($arrow.l.double$) The conservation, disjointness, and ordering constraints force each commodity to trace one simple path, and different commodities can intersect only at terminals. + _Correctness._ ($arrow.r.double$) Given pairwise vertex-disjoint paths, orient path $i$ from $s_i$ to $t_i$ and set precisely its arc variables to one. Flow conservation holds. Every non-source path vertex has one incoming arc, each source contributes its indicator term, and disjointness therefore gives every vertex load at most one. - _Solution extraction._ Mark an edge selected in the source config iff some orientation of that edge carries flow for some commodity. + ($arrow.l.double$) Consider a feasible binary flow. At $s_i$, the capacity constraint permits no incoming arc, so conservation gives exactly one outgoing arc. At every reached nonterminal vertex, the single incoming arc and conservation give exactly one outgoing arc. The walk cannot revisit a vertex, because the first entry and the later entry would violate its unit incoming capacity; since the graph is finite, it reaches $t_i$. The same capacity constraints prevent paths of different commodities from sharing any vertex. Positive-flow components not reached from a source are circulations disjoint from these paths and do not affect the extracted witness. + + _Solution extraction._ For every commodity, search its positive-flow arcs from $s_i$ to $t_i$ and mark only the undirected edges on that path. Discard disconnected circulations. The union of the extracted paths is the source edge-selection vector. ] + #reduction-rule("LengthBoundedDisjointPaths", "ILP")[ - Use one unit-flow commodity for each requested path and add hop variables so every chosen path has at most the source bound $K$ edges. + Maximize the number of active unit-flow commodities, each using at most $K$ edges, with shared internal-vertex capacities. ][ - _Construction._ Variables: binary $f^k_(u,v)$ on each orientation of each edge for each path slot $k$, plus integer hop variables $h^k_v in {0, dots, K}$, where $K$ is the path-length bound and $M = K + 1$. The ILP is: + _Construction._ Let $A$ contain both orientations of every edge. Use binary $f^k_(u,v)$ for each arc and path slot $k$, together with binary activation variables $a_k$. The ILP is: $ - "find" quad & bold(x) \ - "subject to" quad & sum_(w) f^k_(s,w) - sum_(u) f^k_(u,s) = 1 quad forall k \ - & sum_(u) f^k_(u,t) - sum_(w) f^k_(t,w) = 1 quad forall k \ + max quad & sum_k a_k \ + "subject to" quad & sum_(w) f^k_(s,w) - sum_(u) f^k_(u,s) = a_k quad forall k \ + & sum_(u) f^k_(u,t) - sum_(w) f^k_(t,w) = a_k quad forall k \ & sum_(w) f^k_(v,w) - sum_(u) f^k_(u,v) = 0 quad forall k, v in V backslash {s, t} \ & f^k_(u,v) + f^k_(v,u) <= 1 quad forall {u, v} in E, k \ + & sum_k (f^k_(u,v) + f^k_(v,u)) <= 1 quad forall {u, v} in E \ & sum_k sum_(w in N(v)) f^k_(v,w) <= 1 quad forall v in V backslash {s, t} \ - & h^k_s = 0 quad forall k \ - & h^k_v >= h^k_u + 1 - M (1 - f^k_(u,v)) quad forall k, u -> v \ - & h^k_t <= K quad forall k \ - & f^k_(u,v) in {0, 1}, h^k_v in {0, dots, K}. + & sum_((u,v) in A) f^k_(u,v) <= K a_k quad forall k \ + & f^k_(u,v), a_k in {0, 1}. $ - _Correctness._ ($arrow.r.double$) A collection of $J$ internally disjoint $s$-$t$ paths of length at most $K$ yields feasible commodity flows and consistent hop labels. ($arrow.l.double$) The flow and hop constraints force each commodity to be a simple $s$-$t$ path, while the vertex-disjointness inequalities match the source requirement. + _Correctness._ ($arrow.r.double$) Orient each source path from $s$ to $t$ and activate its slot. The resulting flows satisfy all constraints and have objective equal to the number of paths. ($arrow.l.double$) Each active commodity has a unit net flow from $s$ to $t$, so its positive-flow arcs contain a simple $s$-$t$ path. Extracting one such path discards any extra circulation and cannot increase its length beyond $K$. Shared internal-vertex capacities prevent different extracted paths from sharing internal vertices, and edge capacities prevent repeated use of the direct $s$-$t$ edge. Inactive slots have zero flow. Thus extraction preserves the number of active paths and the objective value. - _Solution extraction._ For each path slot $k$, set the source vertex-indicator block to 1 exactly on the vertices incident to the commodity-$k$ path, including $s$ and $t$. + _Solution extraction._ For each active slot, run breadth-first search on its positive-flow arcs and mark the edges of the resulting simple $s$-$t$ path in the source graph's edge order. Return an all-zero edge-selection row for each inactive slot. ] #reduction-rule("MixedChinesePostman", "ILP")[ @@ -15226,7 +15139,7 @@ The following reductions to Integer Linear Programming are straightforward formu _Construction._ Let $n = |V|$, let the original directed arcs be $A = {a_0, dots, a_(m-1)}$ with $a_i = (alpha_i, beta_i)$, and let the undirected edges be $E = {e_0, dots, e_(q-1)}$ with $e_k = {u_k, v_k}$. Set $R = m + q$. If $R = 0$, return the empty feasible ILP: the empty walk already has length 0. Otherwise form the available directed-arc list $A^* = {b_0, dots, b_(L-1)}$ with $L = m + 2 q$, where $b_i = a_i$ for $0 <= i < m$, $b_(m + 2 k) = (u_k, v_k)$, and $b_(m + 2 k + 1) = (v_k, u_k)$. - Write $b_j = ("tail"_j, "head"_j)$ and let $ell_j$ be the corresponding length. Use `ILP` with binary variables encoded by bounds $0 <= x <= 1$. Order the variables as + Write $b_j = ("tail"_j, "head"_j)$ and let $ell_j$ be the corresponding length. Use `ILP` with binary variables encoded by bounds $0 <= x <= 1$. Order the variables as $(d_0, dots, d_(q-1), g_0, dots, g_(L-1), y_0, dots, y_(L-1), z_0, dots, z_(n-1), rho_0, dots, rho_(n-1), s, b_0, dots, b_(n-1), f_0, dots, f_(L-1), h_0, dots, h_(L-1))$, so $d_k$ has index $k$, $g_j$ has index $q + j$, $y_j$ has index $q + L + j$, $z_v$ has index $q + 2 L + v$, $rho_v$ has index $q + 2 L + n + v$, $s$ has index $q + 2 L + 2 n$, $b_v$ has index $q + 2 L + 2 n + 1 + v$, $f_j$ has index $q + 2 L + 3 n + 1 + j$, and $h_j$ has index $q + 3 L + 3 n + 1 + j$. There are $q + 4 L + 3 n + 1$ variables in total. @@ -15296,44 +15209,28 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("StackerCrane", "ILP")[ - Encode the required-arc order by a one-hot position assignment and charge the shortest connector distance between each consecutive pair of required arcs. + Encode the required-arc order by a one-hot position assignment and minimize the shortest connector distances between consecutive required arcs. ][ - _Construction._ Let the required arcs be $A = {a_0, dots, a_(m-1)}$ with $a_i = ("tail"_i, "head"_i)$. Build the mixed connector graph - $H = (V, A union {(u, v), (v, u) : {u, v} in E})$, - where the original required arcs keep their given lengths and each undirected edge contributes both orientations with the same length. Because all lengths are nonnegative, compute the all-pairs connector distances - $D[u, v] = "dist"_H(u, v)$ - either by running Dijkstra from every source vertex or by Floyd--Warshall on the $n$-vertex graph $H$; this is exactly the graph queried by `mixed_graph_adjacency()` and `shortest_path_length()` in the model. If $D[u, v] = oo$, the pair is impossible and will be forbidden explicitly. + _Construction._ Let $a_i = ("tail"_i, "head"_i)$ for $0 <= i < m$ be the required arcs, with lengths $ell_i >= 0$. In the mixed graph, retain every directed required arc and replace every undirected edge by both orientations of the same length. Compute shortest-path distances $D[u,v]$ in this graph; an unreachable pair has distance $oo$. - Use `ILP`. The binary position variables are $x_(i,p)$ for $i, p in {0, dots, m - 1}$, with index - $"idx"_x(i, p) = i m + p$. - The binary McCormick variables are $z_(i,j,p)$ for $i, j, p in {0, dots, m - 1}$, where position $p + 1$ is interpreted cyclically as $(p + 1) mod m$; their indices are - $"idx"_z(i, j, p) = m^2 + p m^2 + i m + j$. - There are $m^2 + m^3$ binary variables. + Introduce binary variables $x_(i,p)$ indicating that arc $i$ occupies position $p$, and $z_(i,j,p)$ indicating consecutive arcs $i,j$ at positions $p,p+1$. Positions are cyclic modulo $m$. Enforce + $ sum_p x_(i,p) = 1 quad forall i, quad sum_i x_(i,p) = 1 quad forall p, $ + and, for *every* triple $(i,j,p)$, impose all three product constraints + $ z_(i,j,p) <= x_(i,p), quad z_(i,j,p) <= x_(j,(p+1) mod m), $ + $ z_(i,j,p) >= x_(i,p) + x_(j,(p+1) mod m) - 1. $ + Additionally impose $z_(i,j,p) = 0$ whenever $D["head"_i,"tail"_j] = oo$. This additional constraint does not replace the product constraints: together they forbid the unreachable adjacency. - The constraints are: - $sum_(p = 0)^(m - 1) x_(i,p) = 1$ for each required arc $i$; - $sum_(i = 0)^(m - 1) x_(i,p) = 1$ for each position $p$; - $z_(i,j,p) <= x_(i,p)$, $z_(i,j,p) <= x_(j,(p + 1) mod m)$, and $z_(i,j,p) >= x_(i,p) + x_(j,(p + 1) mod m) - 1$ for all $i, j, p$; - if $D["head"_i, "tail"_j] = oo$, then either set $z_(i,j,p) = 0$ for all $p$ or, equivalently, impose $x_(i,p) + x_(j,(p + 1) mod m) <= 1$ for all $p$; - and finally - $sum_(i = 0)^(m - 1) ell_i + sum_(p = 0)^(m - 1) sum_(i = 0)^(m - 1) sum_(j = 0)^(m - 1) D["head"_i, "tail"_j] z_(i,j,p) <= B$. - The first term is the total length of the required traversals, and the second term charges exactly one connector distance for each consecutive pair in the cyclic order. + Minimize + $ sum_p sum_(i,j: D["head"_i,"tail"_j] < oo) D["head"_i,"tail"_j] z_(i,j,p). $ + The sum of required arc lengths $L = sum_i ell_i$ is constant and is omitted from the ILP objective. No decision threshold is introduced. - The ILP is: - $ - "find" quad & bold(x) \ - "subject to" quad & sum_(p = 0)^(m - 1) x_(i,p) = 1 quad forall i in {0, dots, m - 1} \ - & sum_(i = 0)^(m - 1) x_(i,p) = 1 quad forall p in {0, dots, m - 1} \ - & z_(i,j,p) <= x_(i,p), z_(i,j,p) <= x_(j,(p + 1) mod m) quad forall i, j, p \ - & z_(i,j,p) >= x_(i,p) + x_(j,(p + 1) mod m) - 1 quad forall i, j, p \ - & z_(i,j,p) = 0 quad "whenever" D["head"_i, "tail"_j] = oo \ - & sum_(i = 0)^(m - 1) ell_i + sum_(p = 0)^(m - 1) sum_(i = 0)^(m - 1) sum_(j = 0)^(m - 1) D["head"_i, "tail"_j] z_(i,j,p) <= B \ - & x_(i,p), z_(i,j,p) in {0, 1}. - $ + _Correctness._ ($arrow.r.double$) Every feasible source permutation defines a one-hot matrix $x$. Set each $z$ to its indicated product. All connectors used by the permutation are reachable, so the zero constraints also hold. The objective equals the source route length minus $L$. - _Correctness._ ($arrow.r.double$) Any feasible Stacker Crane permutation determines a one-hot assignment and consecutive-pair indicators whose connector costs equal the route length. ($arrow.l.double$) Any feasible ILP solution yields a permutation of the required arcs, and the linearized connector term is exactly the sum of shortest paths between consecutive arcs. + ($arrow.l.double$) The two assignment constraints make every feasible $x$ a permutation. The three product constraints force $z = x x$ for binary variables. If an adjacency were unreachable, its product would be one while its zero constraint required zero, a contradiction. Therefore the decoded permutation has a feasible closed walk, whose shortest connector lengths sum to the ILP objective. Its source value is exactly that objective plus $L$. - _Solution extraction._ Decode the permutation by taking, for each position $p$, the unique arc $a$ with $x_(a,p) = 1$. + Thus feasibility is equivalent in both directions and the constant offset preserves every optimum. Zero lengths and repeated or self-loop required arcs do not alter the argument. When $m=1$, the product is $x_(0,0)^2=x_(0,0)$ and still enforces a reachable return. When $m=0$, both models have the empty solution with value zero. + + _Solution extraction._ For each position $p$, return the unique $i$ with $x_(i,p)=1$, using the existing one-hot decoder. There are $m^2+m^3$ binary variables and $2m+3m^3+m r$ constraints, where $r$ is the number of unreachable ordered required-arc pairs; hence at most $2m+4m^3$ constraints. ] #reduction-rule("SteinerTreeInGraphs", "ILP")[ @@ -15392,7 +15289,7 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) Any feasible open-shop schedule with the given permutations $sigma_i$ induces valid ordering bits $x_{j k i}$ and $y_{j i i'}$ and start times satisfying all non-overlap constraints. ($arrow.l.double$) Any feasible ILP solution defines non-overlapping start times for all tasks, respecting both machine and job constraints. - _Solution extraction._ For each machine $i$, sort jobs by their ILP start times $s_{j,i}$ to recover the per-machine permutation; output the concatenation of these $m$ direct-index permutations. + _Solution extraction._ Return the $n m$ start-time variables $s_{j,i}$ directly in job-major order. ] #reduction-rule("MinimumTardinessSequencing", "ILP")[ @@ -15449,21 +15346,23 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("SequencingToMinimizeTardyTaskWeight", "ILP")[ - Place each task in exactly one schedule position with a binary tardy indicator forced on whenever the completion time at that position exceeds the task's deadline. + Use position assignments and exact tardiness indicators, with both implications of each deadline comparison. Prefix bounds supply valid constants for the bounded-disjunction construction @vielma2015, including the signed lengths, signed weights, and arbitrary deadlines accepted by the source model. ][ - _Construction._ Variables: binary $x_(j,p)$ with $x_(j,p) = 1$ iff task $j$ occupies position $p$, and binary tardy indicator $u_j$. Let $M = sum_j ell_j$. The ILP is: + _Construction._ For $n$ tasks introduce binary $x_(j,p)$ (task $j$ occupies position $p$) and binary $u_j$ (task $j$ is tardy), with the two families of assignment equations. Let $P_p = sum_(qd_j-ell_j$ iff $P_p>c_(j,p)$. Thus this bound transformation does not alter source semantics. Add $ - "minimize" quad & sum_j w_j u_j \ - "subject to" quad & sum_p x_(j,p) = 1 quad forall j \ - & sum_j x_(j,p) = 1 quad forall p \ - & M x_(j,p) + sum_(p' < p) sum_(j') ell_(j') x_(j',p') - M u_j <= d_j - ell_j + M quad forall j, p \ - & x_(j,p) in {0, 1}, u_j in {0, 1}. + P_p + A_(j,p)x_(j,p)-A_(j,p)u_j &<= U_p, \ + P_p - F_(j,p)x_(j,p)-F_(j,p)u_j &>= L_p-F_(j,p). $ - The third family of constraints enforces: if task $j$ is at position $p$ (so $x_(j,p) = 1$), then its completion time $ell_j + sum_(p' < p) sum_(j') ell_(j') x_(j',p')$ exceeds $d_j$ only when $u_j = 1$. + Minimize $sum_j w_j u_j$. There are exactly $n^2+n$ variables and $2n^2+2n$ constraints, with at most $n^4-n^3+6n^2$ nonzero coefficients. The formal ILP constructor normalizes the emitted constraints. - _Correctness._ ($arrow.r.double$) Any schedule induces completion times; for each tardy task the big-$M$ constraint forces $u_j = 1$, so the objective counts exactly the total tardy weight. ($arrow.l.double$) Any feasible ILP assignment is a valid permutation (by the assignment constraints) and the tardy indicators agree with the actual completion times. + _Correctness._ When $x_(j,p)=0$, both inequalities are redundant under the prefix bounds. At the occupied position, $u_j=0$ enforces $P_p<=c_(j,p)$, while $u_j=1$ enforces $P_p>=c_(j,p)+1$. Therefore $u_j$ equals the actual integer tardiness predicate, rather than merely being forced on for late tasks. + ($arrow.r.double$) Every source permutation and its true tardiness indicators satisfy all assignment and prefix constraints with exactly its source objective. ($arrow.l.double$) Every feasible target assignment determines a permutation and, by the occupied-position constraints, its unique correct tardiness indicators. Objective values agree for every feasible target witness, even with negative weights; optimum values consequently agree. Empty source arrays give the empty ILP and objective zero. - _Solution extraction._ Read the unique position $p$ with $x_(j,p) = 1$ for each task $j$ to recover the schedule permutation. + _Numeric domain and extraction._ Counts, coefficients, right-hand sides, and sparse-evaluation partial-sum bounds are checked before target construction. Signed subset-sum bounds ensure all source time and weight accumulations are representable on successful reduction. For each position, minima/maxima of the sorted prefix sums bound partial sums using at most $p$ lengths; adding the indicator coefficients gives bounds for every target evaluation after the assignment equations. Unrepresentable construction returns a typed reduction error, never an infeasibility claim. The target is `ILP` with fixed minimization sense. Validate it once, reject a nonfinite/infeasible evaluation, and use the existing one-hot decoder to recover the permutation. No source endpoint or model domain is changed. ] #reduction-rule("SequencingWithDeadlinesAndSetUpTimes", "ILP")[ @@ -15582,21 +15481,21 @@ The following reductions to Integer Linear Programming are straightforward formu #let hcd_ilp = load-example( "HighlyConnectedDeletion", "ILP", - target-variant: (variable: "bool"), + target-variant: (coefficient: "i64", variable: "bool"), ) #let hcd_ilp_sol = hcd_ilp.solutions.at(0) #reduction-rule("HighlyConnectedDeletion", "ILP", example: true, - example-target-variant: (variable: "bool"), + example-target-variant: (coefficient: "i64", variable: "bool"), example-caption: [Triangle plus pendant: $n = 4$ vertices, $m = 4$ edges], extra: [ #pred-commands( "pred create --example " + problem-spec(hcd_ilp.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(hcd_ilp) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + hcd_ilp_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(hcd_ilp_sol.source_config), ) - Source deletion witness $(#hcd_ilp_sol.source_config.map(str).join(", "))$, target ILP witness $(#hcd_ilp_sol.target_config.map(str).join(", "))$. + Source deletion witness $(#fmt-values(hcd_ilp_sol.source_config))$, target ILP witness $(#fmt-values(hcd_ilp_sol.target_config))$. ], )[ Enumerate the family of feasible clusters of $G$ and pick a partition of $V$ into feasible clusters maximizing the kept internal edge count; since $|E|$ is fixed, this is equivalent to minimizing deleted edges @HueffnerKomusiewiczLiebtrauNiedermeier2014. @@ -15616,21 +15515,21 @@ The following reductions to Integer Linear Programming are straightforward formu #let ep_ilp = load-example( "EulerianPath", "ILP", - target-variant: (variable: "i32"), + target-variant: (coefficient: "i64", variable: "i64"), ) #let ep_ilp_sol = ep_ilp.solutions.at(0) #reduction-rule("EulerianPath", "ILP", example: true, - example-target-variant: (variable: "i32"), + example-target-variant: (coefficient: "i64", variable: "i64"), example-caption: [3-vertex digraph with 4 arcs (parallel edges)], extra: [ #pred-commands( "pred create --example " + problem-spec(ep_ilp.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(ep_ilp) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + ep_ilp_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(ep_ilp_sol.source_config), ) - Source trail witness $(#ep_ilp_sol.source_config.map(str).join(", "))$, target ILP witness $(#ep_ilp_sol.target_config.map(str).join(", "))$. + Source trail witness $(#fmt-values(ep_ilp_sol.source_config))$, target ILP witness $(#fmt-values(ep_ilp_sol.target_config))$. ], )[ Encode the directed Eulerian-trail witness structure as an integer feasibility program: successor variables on compatible arc pairs, start / end indicators, and Miller--Tucker--Zemlin-style position variables eliminate spurious sub-cycles @Ebert1988ComputingEulerianTrails @BangJensenGutin2009Digraphs. @@ -15655,24 +15554,27 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("BottleneckTravelingSalesman", "ILP")[ - Use a cyclic position assignment for the tour and a bottleneck variable that dominates the weight of every chosen tour edge. + Use cyclic positions, explicit edge-use choices, and a selected maximum-weight edge. The objective preserves signed edge weights without a big-$M$ constant. ][ - _Construction._ Variables: binary $x_(v,p)$ for city-position assignment, binary $z_((u,v),p)$ for consecutive tour edges, and integer bottleneck variable $b$. The ILP is: + _Construction._ Let $n$ be the vertex count and $m$ the number of stored edges, counting parallel edges separately. Binary $x_(v,p)$ assigns vertex $v$ to position $p$. Binary $z_(e,p,d)$ chooses source edge $e$ at cyclic step $p$ in direction $d in {0,1}$. Binary $q_e$ selects one used maximum-weight edge. Define $y_e = sum_(p,d) z_(e,p,d)$ as an expression, not another variable. All variables retain the nonnegative integer ILP variant, with explicit upper bounds of one. + + Each vertex and position has exactly one assignment. For edge $e=(u,v)$, impose $z_(e,p,0) <= x_(u,p)$ and $z_(e,p,0) <= x_(v,(p+1) mod n)$, with reversed endpoints for direction one. Then impose $ - min quad & b \ - "subject to" quad & sum_p x_(v,p) = 1 quad forall v \ - & sum_v x_(v,p) = 1 quad forall p \ - & z_((u,v),p) <= x_(u,p) quad forall (u, v), p \ - & z_((u,v),p) <= x_(v,(p+1) mod n) quad forall (u, v), p \ - & z_((u,v),p) >= x_(u,p) + x_(v,(p+1) mod n) - 1 quad forall (u, v), p \ - & sum_((u,v) in E) z_((u,v),p) = 1 quad forall p \ - & b >= w_(u,v) z_((u,v),p) quad forall (u, v), p \ - & x_(v,p), z_((u,v),p) in {0, 1}, b in ZZ_(>=0). + sum_(e,d) z_(e,p,d) = 1 quad forall p, quad y_e <= 1 quad forall e, + $ + and the exact maximum selector $ + sum_e q_e = 1, quad q_e <= y_e, quad + y_e <= sum_(f: w_f >= w_e) q_f quad forall e. + $ + Minimize $sum_e w_e q_e$. The threshold principle selects among edge weights @larusic2010; the explicit maximum selector and stored-edge treatment here give a direct ILP construction. + + _Correctness._ ($arrow.r.double$) A connected degree-two spanning edge selection supplies a cyclic vertex order and distinct edge uses. This includes a one-vertex self-loop or a two-vertex cycle with two parallel edges when present in the native graph. Choose a used maximum-weight edge for $q$. All constraints hold and the objective is the source bottleneck. + ($arrow.l.double$) The position equalities give a permutation. Exactly one edge connects each consecutive pair, and no stored edge is used twice, so the decoded selection is connected, spans all vertices, and has degree two. The selected $q$ edge is used and dominates every used edge. Thus every feasible target witness has exactly the source value, not merely an upper bound. An empty graph has no selector and is infeasible. - _Correctness._ ($arrow.r.double$) Any Hamiltonian tour yields a feasible assignment and sets $b$ to the maximum selected edge weight. ($arrow.l.double$) Any feasible ILP solution encodes a Hamiltonian cycle, and the minimax constraints force $b$ to equal its bottleneck edge weight. + _Numeric contract._ Constraints use only coefficients of magnitude one; weights are compared without subtraction or negation. Exactly one objective coefficient contributes, so even the minimum signed integer is preserved without overflowing an intermediate sum. Checked dimensions bound binary constraint accumulation. The target has $n^2+2 m n+m$ variables and $n^2+6 m n+4m+3n+1$ constraints. The default floating solver's transport range is a separate limitation. - _Solution extraction._ Mark an edge selected in the source config iff it appears between two consecutive positions in the decoded cycle. + _Solution extraction._ Evaluate the target once, reject an infeasible assignment, and select precisely the stored edges whose edge-use block contains a one. Parallel edges keep their individual identities. ] #let hc_lc = load-example("HamiltonianCircuit", "LongestCircuit") @@ -15681,23 +15583,23 @@ The following reductions to Integer Linear Programming are straightforward formu #let hc_lc_source_edges = hc_lc.source.instance.graph.edges #let hc_lc_target_edges = hc_lc.target.instance.graph.edges #let hc_lc_target_weights = hc_lc.target.instance.edge_lengths -#let hc_lc_selected_edges = hc_lc_target_edges.enumerate().filter(((i, _)) => hc_lc_sol.target_config.at(i) == 1).map(((i, e)) => (e.at(0), e.at(1))) +#let hc_lc_selected_edges = hc_lc_target_edges.enumerate().filter(((i, _)) => hc_lc_sol.target_config.at(i)).map(((i, e)) => (e.at(0), e.at(1))) #reduction-rule("HamiltonianCircuit", "LongestCircuit", example: true, example-caption: [Cycle graph on $#hc_lc_n$ vertices with unit edge lengths], extra: [ #pred-commands( "pred create --example " + problem-spec(hc_lc.source) + " -o hc.json", - "pred reduce hc.json --to " + target-spec(hc_lc) + " -o bundle.json", + "pred reduce hc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hc.json --config " + hc_lc_sol.source_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(hc_lc_sol.source_config), ) - *Step 1 -- Start from the source graph.* The canonical source fixture is the cycle on vertices ${0, 1, dots, #(hc_lc_n - 1)}$ with $#hc_lc_source_edges.len()$ edges. The stored Hamiltonian-circuit witness is the permutation $[#hc_lc_sol.source_config.map(str).join(", ")]$.\ + *Step 1 -- Start from the source graph.* The canonical source fixture is the cycle on vertices ${0, 1, dots, #(hc_lc_n - 1)}$ with $#hc_lc_source_edges.len()$ edges. The stored Hamiltonian-circuit witness is the permutation $[#fmt-values(hc_lc_sol.source_config)]$.\ - *Step 2 -- Assign unit edge lengths.* The target keeps the same $#hc_lc_n$ vertices and $#hc_lc_target_edges.len()$ edges. Every edge receives length $1$, so the edge-length vector is $[#hc_lc_target_weights.map(str).join(", ")]$.\ + *Step 2 -- Assign unit edge lengths.* The target keeps the same $#hc_lc_n$ vertices and $#hc_lc_target_edges.len()$ edges. Every edge receives length $1$, so the edge-length vector is $[#fmt-values(hc_lc_target_weights)]$.\ - *Step 3 -- Verify the canonical witness.* The stored target configuration $[#hc_lc_sol.target_config.map(str).join(", ")]$ selects the edges #hc_lc_selected_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The total circuit length is $#hc_lc_selected_edges.len() times 1 = #hc_lc_n = n$, confirming a Hamiltonian circuit. Traversing the selected edges recovers the vertex permutation $[#hc_lc_sol.source_config.map(str).join(", ")]$.\ + *Step 3 -- Verify the canonical witness.* The stored target configuration $[#fmt-values(hc_lc_sol.target_config)]$ selects the edges #hc_lc_selected_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The total circuit length is $#hc_lc_selected_edges.len() times 1 = #hc_lc_n = n$, confirming a Hamiltonian circuit. Traversing the selected edges recovers the vertex permutation $[#fmt-values(hc_lc_sol.source_config)]$.\ *Multiplicity:* The fixture stores one canonical witness. For the $#hc_lc_n$-cycle there are $#hc_lc_n times 2 = #(hc_lc_n * 2)$ directed Hamiltonian circuits (choice of start vertex and direction), but they all select the same undirected edge set. ], @@ -15708,23 +15610,25 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) If $G$ has a Hamiltonian circuit $v_0, v_1, dots, v_(n-1), v_0$, then this circuit uses $n$ edges each of length 1, giving total length $n$. Since a simple circuit on $n$ vertices can use at most $n$ edges, this is optimal. ($arrow.l.double$) If the longest circuit in $G'$ has length $n$, it uses $n$ unit-weight edges and therefore visits $n$ distinct vertices, i.e., every vertex exactly once. This circuit is therefore a Hamiltonian circuit in $G$. - _Solution extraction._ Read the selected target edges, traverse the unique degree-2 cycle they form, and return the resulting vertex permutation as the source Hamiltonian-circuit witness. + _Solution extraction._ Evaluate the target selection once and require a feasible circuit of length $n$. Reject infeasible selections and shorter circuits before decoding. Then traverse the selected cycle and return its vertex permutation. This criterion applies to every target configuration, without requiring an optimality claim from the caller. If the target has no feasible circuit, or its proven optimum is less than $n$, the source answer is NO. In particular, simple graphs with fewer than three vertices have no circuit; an empty edge selection is not a witness, including on the empty graph. ] #reduction-rule("LongestCircuit", "ILP")[ - A direct cycle-selection ILP uses binary edge variables, degree constraints, and a connectivity witness to force exactly one simple circuit of length at least the bound. + A direct cycle-selection ILP maximizes total edge length, using degree constraints and binary multi-commodity flows from a root chosen on the circuit. For $n$ vertices and $m$ edges, it uses $m + 2n + 2m n$ binary variables. ][ - _Construction._ Variables: binary $y_e$ for edges, binary $s_v$ indicating whether vertex $v$ lies on the circuit, and root-flow variables on selected edges. The ILP is: + _Construction._ Let $G = (V, E)$ have $n = |V|$ vertices, $m = |E|$ edges, and positive edge lengths $l_e$. Introduce binary edge selections $y_e$, vertex selections $s_v$, root selections $r_v$, and a binary directed flow $f^t_(u v)$ in each direction of each edge for every destination $t in V$. Define the flow divergence $D^t_v = sum_(u : {v,u} in E) (f^t_(v u) - f^t_(u v))$. The ILP is: $ - "find" quad & bold(x) \ + max quad & sum_(e in E) l_e y_e \ "subject to" quad & sum_(e : v in e) y_e = 2 s_v quad forall v \ & sum_e y_e >= 3 \ - & sum_e l_e y_e >= K \ - & "root-flow connectivity constraints hold on the selected edges" \ - & y_e, s_v in {0, 1}. + & sum_v r_v = 1, quad r_v <= s_v quad forall v \ + & D^t_t = r_t - s_t quad forall t \ + & 0 <= D^t_v <= r_v quad forall t, v != t \ + & f^t_(u v) <= y_({u,v}) quad forall t, {u,v} in E " (both directions)" \ + & y_e, s_v, r_v, f^t_(u v) in {0, 1}. $ - _Correctness._ ($arrow.r.double$) A simple circuit has degree 2 at each used vertex, is connected, and meets the length bound $K$. ($arrow.l.double$) The degree and connectivity constraints force the selected edges to form exactly one simple circuit, and the final inequality enforces the required total length. + _Correctness._ ($arrow.r.double$) Given any simple circuit, select its edges and vertices and choose any of its vertices as root. For each selected non-root destination, send one unit along a simple path on the circuit from the root to that destination; set all other commodity flows to zero. Every constraint holds, and the objective equals the circuit length. ($arrow.l.double$) Degree constraints make the nonempty selected subgraph a disjoint union of simple circuits. If a selected destination $t$ were outside the root's component, summing its commodity's divergence over its component would give $-1$: the destination consumes one unit and every other vertex there has zero divergence. But no flow can cross that component's boundary, a contradiction. Thus all selected vertices lie in the root's component, giving exactly one simple circuit. The objective is preserved in both directions. _Solution extraction._ Output the binary edge-selection vector $(y_e)_(e in E)$. ] @@ -15805,19 +15709,17 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("AcyclicPartition", "ILP")[ - Assign every vertex to one partition class, bound the weight and crossing cost of those classes, and impose a topological order on the quotient digraph. + Assign every vertex to a topologically numbered partition class and directly require every arc to have nondecreasing class labels, following the upper-triangular formulation of @ozkayaCatalyurek2022. ][ - _Construction._ Let $n = |V|$ and let the directed arcs be $A = {a_0, dots, a_(m-1)}$ with $a_t = (u_t -> v_t)$. The source witness already allows every vertex to choose one label in ${0, dots, n - 1}$, so the ILP uses exactly the same label range. Use `ILP` with variable order - $(x_(v,c))_(v,c), (s_(t,c))_(t,c), (y_t)_t, (o_c)_c, (p_v)_v$. + _Construction._ Let $n = |V|$ and let the directed arcs be $A = {a_0, dots, a_(m-1)}$ with $a_t = (u_t -> v_t)$. The source witness already allows every vertex to choose one label in ${0, dots, n - 1}$, so the ILP uses exactly the same label range. Use `ILP` with variable order + $(x_(v,c))_(v,c), (s_(t,c))_(t,c), (y_t)_t$. The indices are $"idx"_x(v,c) = v n + c$, $"idx"_s(t,c) = n^2 + t n + c$, - $"idx"_y(t) = n^2 + m n + t$, - $"idx"_o(c) = n^2 + m n + m + c$, - and $ "idx"_p(v) = n^2 + m n + m + n + v$. - There are $n^2 + m n + m + 2 n$ variables. + and $"idx"_y(t) = n^2 + m n + t$. + There are $n^2 + m n + m$ variables. - Here $x_(v,c) in {0, 1}$ means vertex $v$ is assigned to class $c$, $s_(t,c) in {0, 1}$ means both endpoints of arc $a_t$ lie in class $c$, $y_t in {0, 1}$ marks that arc $a_t$ crosses between two different classes, $o_c in {0, dots, n - 1}$ is the order assigned to class $c$, and $p_v in {0, dots, n - 1}$ copies the order of the class chosen by vertex $v$. + Here $x_(v,c) in {0, 1}$ means vertex $v$ is assigned to topological class label $c$, $s_(t,c) in {0, 1}$ means both endpoints of arc $a_t$ lie in class $c$, and $y_t in {0, 1}$ marks that arc $a_t$ crosses between two different classes. The constraints are: $sum_(c = 0)^(n - 1) x_(v,c) = 1$ for every vertex $v$; @@ -15825,12 +15727,8 @@ The following reductions to Integer Linear Programming are straightforward formu $s_(t,c) <= x_(u_t,c)$, $s_(t,c) <= x_(v_t,c)$, and $s_(t,c) >= x_(u_t,c) + x_(v_t,c) - 1$ for every arc $a_t$ and class $c$; $y_t + sum_(c = 0)^(n - 1) s_(t,c) = 1$ for every arc $a_t$, so $y_t = 1$ exactly for crossing arcs; $sum_(t = 0)^(m - 1) "cost"(a_t) y_t <= K$; - $0 <= o_c <= n - 1$ and $0 <= p_v <= n - 1$ for all classes $c$ and vertices $v$; - $p_v - o_c <= (n - 1) (1 - x_(v,c))$ and $o_c - p_v <= (n - 1) (1 - x_(v,c))$ for all $v, c$, so $p_v = o_c$ whenever $x_(v,c) = 1$; - and for every arc $a_t = (u_t -> v_t)$, - $p_(v_t) - p_(u_t) >= 1 - n sum_(c = 0)^(n - 1) s_(t,c)$. - The exact big-$M$ here is $M = n$: if $u_t$ and $v_t$ lie in the same class, then $sum_c s_(t,c) = 1$ and the right-hand side is $1 - n = -(n - 1)$, which is precisely the smallest possible difference between two order variables in ${0, dots, n - 1}$. If the arc crosses between two distinct classes, then $sum_c s_(t,c) = 0$ and the inequality becomes $p_(v_t) - p_(u_t) >= 1$. For the realized classes $c$ and $d$ of the endpoints, this is exactly the requested form - $o_d - o_c >= 1 - M sum_h s_(t,h)$. + and $sum_(c = 0)^(n - 1) c x_(u_t,c) <= sum_(c = 0)^(n - 1) c x_(v_t,c)$ for every arc $a_t = (u_t -> v_t)$. + The last inequality directly requires every original arc to go from a lower or equal class label to a higher or equal label. Equality represents an internal arc; a crossing arc has distinct labels and therefore strictly increases along the corresponding quotient arc. The ILP is: $ @@ -15841,12 +15739,11 @@ The following reductions to Integer Linear Programming are straightforward formu & s_(t,c) >= x_(u_t,c) + x_(v_t,c) - 1 quad forall t, c \ & y_t + sum_(c = 0)^(n - 1) s_(t,c) = 1 quad forall t in {0, dots, m - 1} \ & sum_(t = 0)^(m - 1) "cost"(a_t) y_t <= K \ - & p_v - o_c <= (n - 1) (1 - x_(v,c)), o_c - p_v <= (n - 1) (1 - x_(v,c)) quad forall v, c \ - & p_(v_t) - p_(u_t) >= 1 - n sum_(c = 0)^(n - 1) s_(t,c) quad forall t in {0, dots, m - 1} \ - & x_(v,c), s_(t,c), y_t in {0, 1}; o_c, p_v in {0, dots, n - 1}. + & sum_(c = 0)^(n - 1) c x_(u_t,c) <= sum_(c = 0)^(n - 1) c x_(v_t,c) quad forall t in {0, dots, m - 1} \ + & x_(v,c), s_(t,c), y_t in {0, 1}. $ - _Correctness._ ($arrow.r.double$) Any valid acyclic partition gives a class assignment whose quotient arcs respect some topological ordering, with the same class weights and crossing cost. ($arrow.l.double$) Any feasible ILP solution partitions the vertices, keeps every class within the weight bound, charges exactly the inter-class arcs, and the order variables force the quotient digraph to be acyclic. + _Correctness._ ($arrow.r.double$) Given a valid acyclic partition, choose a topological ordering of its quotient digraph and relabel each used class by its position in that ordering. This relabeling preserves class membership, class weights, and crossing cost. Every internal arc has equal endpoint labels, while every quotient arc goes to a strictly larger label, so the direct ordering inequalities hold. ($arrow.l.double$) Any feasible ILP solution partitions the vertices, keeps every class within the weight bound, and charges exactly the inter-class arcs. Along every quotient arc the endpoint classes have distinct, nondecreasing labels and hence the label strictly increases. A directed quotient cycle would require a strict increase around the cycle back to its starting label, which is impossible; therefore the quotient digraph is acyclic. _Solution extraction._ For each vertex $v$, output the unique class label $c$ with $x_(v,c) = 1$. ] @@ -15869,51 +15766,28 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("BiconnectivityAugmentation", "ILP")[ - Select candidate edges under the budget and, for every deleted vertex, certify that the remaining augmented graph stays connected by a flow witness. + Select candidate edges under the total budget and certify connectivity of both the original augmented graph and every vertex-deleted graph using bounded integral flow witnesses. ][ - _Construction._ Let the base graph edges be $E = {e_0, dots, e_(m-1)}$ with $e_i = {u_i, v_i}$, and let the candidate edges be $F = {f_0, dots, f_(p-1)}$ with $f_j = {s_j, t_j}$. If $n = |V| <= 1$, return the empty feasible ILP, since every 0- or 1-vertex graph is already biconnected in the model. Otherwise fix, for each deleted vertex $q$, the surviving root - $r_q = 0$ if $q != 0$, and $r_0 = 1$. - This choice is explicit and valid because $n >= 2$. + _Construction._ Let $n$ be the vertex count, $m$ the number of base edges, and $p$ the number of candidate edges. Candidate $j$ has cost $w_j$, and the budget is $B$. Use `ILP` with an empty minimization objective and every variable bounded to $[0,1]$ through `IntegerVariable::binary()`. - Use `ILP`. The candidate-selection bits are $y_j in {0, 1}$ with index $j$. For the connectivity witnesses, allocate the full $(q, t)$ commodity grid with $q, t in {0, dots, n - 1}$, even though the commodities with $t = q$ or $t = r_q$ will be pinned to 0. For each base edge $e_i$ and orientation flag $eta in {0, 1}$, let $eta = 0$ mean $u_i -> v_i$ and $eta = 1$ mean $v_i -> u_i$; define binary flow variables $f^(q,t)_(i,eta)$ with index - $p + (((q n + t) m + i) 2 + eta)$. - For each candidate edge $f_j$ and orientation flag $eta in {0, 1}$, let $eta = 0$ mean $s_j -> t_j$ and $eta = 1$ mean $t_j -> s_j$; define binary flow variables $g^(q,t)_(j,eta)$ with index - $p + 2 m n^2 + (((q n + t) p + j) 2 + eta)$. - There are $p + 2 n^2 (m + p)$ variables in total. + Selection variable $y_j$ has index $j$. Connectivity scenarios are $q in {0, dots, n}$: $q` with variables ordered as + _Construction._ Let $n = |V|$, let the graph edges be $E = {e_0, dots, e_(m-1)}$ with $e_i = {u_i, v_i}$, and let the allowed component labels be $c in {0, dots, K - 1}$. Use `ILP` with variables ordered as $(x_(v,c))_(v,c), (u_c)_c, (r_(v,c))_(v,c), (s_c)_c, (b_(v,c))_(v,c), (f_(i,eta,c))_(i,eta,c)$. Their indices are $"idx"_x(v,c) = v K + c$, @@ -15984,7 +15858,7 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("StrongConnectivityAugmentation", "ILP")[ Select candidate arcs under the budget and certify strong connectivity by sending flow both from a root to every vertex and back again. ][ - _Construction._ Let the base arcs be $A = {a_0, dots, a_(m-1)}$ with $a_i = (u_i, v_i)$, let the candidate arcs be $C = {c_0, dots, c_(p-1)}$ with $c_j = (s_j, t_j)$, and, when $n = |V| >= 1$, fix the root to be vertex $r = 0$. If $n <= 1$, return the empty feasible ILP. Use `ILP` with variables ordered as + _Construction._ Let the base arcs be $A = {a_0, dots, a_(m-1)}$ with $a_i = (u_i, v_i)$, let the candidate arcs be $C = {c_0, dots, c_(p-1)}$ with $c_j = (s_j, t_j)$, and, when $n = |V| >= 1$, fix the root to be vertex $r = 0$. If $n <= 1$, return the empty feasible ILP. Use `ILP` with variables ordered as $(y_j)_j, (f^t_i)_(t,i), (bar(f)^t_j)_(t,j), (g^t_i)_(t,i), (bar(g)^t_j)_(t,j)$, where $f^t$ is the forward root-to-$t$ flow on base arcs, $bar(f)^t$ is the forward flow on candidate arcs, $g^t$ is the backward $t$-to-root flow on base arcs, and $bar(g)^t$ is the backward flow on candidate arcs. The indices are @@ -16283,9 +16157,9 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(ps_qubo.source) + " -o paintshop.json", - "pred reduce paintshop.json --to " + target-spec(ps_qubo) + " -o bundle.json", + "pred reduce paintshop.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate paintshop.json --config " + ps_qubo_sol.source_config.map(str).join(","), + "pred evaluate paintshop.json --config " + cli-config(ps_qubo_sol.source_config), ) #{ let n = ps_qubo.source.instance.num_cars @@ -16295,13 +16169,13 @@ The following reductions to Integer Linear Programming are straightforward formu let seq-labels = seq.map(i => labels.at(i)) let coloring = seq.enumerate().map(((pos, car)) => { let first-color = ps_qubo_sol.source_config.at(car) - if is-first.at(pos) { first-color } else { 1 - first-color } + if is-first.at(pos) { first-color } else { not first-color } }) [*Source:* $n = #n$ cars, sequence $(#seq-labels.join(", "))$. \ *Parity:* #seq.enumerate().map(((pos, _)) => if is-first.at(pos) { "1st" } else { "2nd" }).join(", ") \ *Step 1 -- One QUBO variable per car.* Binary variable $x_i in {0,1}$ for each car $i$: $x_i = 0$ means "first occurrence gets color 0, second gets color 1"; $x_i = 1$ reverses. \ *Step 2 -- Build the $Q$ matrix from adjacent pairs.* For each adjacent pair $(j, j+1)$ in the sequence with distinct cars $a, b$: if both positions have the _same_ parity (both first or both second occurrence), a color switch occurs when $x_a != x_b$, contributing $+1$ to $Q_(a a)$, $+1$ to $Q_(b b)$, and $-2$ to $Q_(a b)$. If they have _different_ parity, a switch occurs when $x_a = x_b$, contributing $-1$ to $Q_(a a)$, $-1$ to $Q_(b b)$, and $+2$ to $Q_(a b)$. \ - *Step 3 -- Verify.* The QUBO solution $bold(x) = (#ps_qubo_sol.target_config.map(str).join(", "))$ yields coloring $(#coloring.map(str).join(", "))$ with #{coloring.windows(2).filter(w => w.at(0) != w.at(1)).len()} color switches #sym.checkmark ] + *Step 3 -- Verify.* The QUBO solution $bold(x) = (#fmt-values(ps_qubo_sol.target_config))$ yields coloring $(#fmt-values(coloring))$ with #{coloring.windows(2).filter(w => w.at(0) != w.at(1)).len()} color switches #sym.checkmark ] } ], )[ @@ -16363,9 +16237,9 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(rta_rtsa.source) + " -o rta.json", - "pred reduce rta.json --to " + target-spec(rta_rtsa) + " -o bundle.json", + "pred reduce rta.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate rta.json --config " + rta_rtsa_sol.source_config.map(str).join(","), + "pred evaluate rta.json --config " + cli-config(rta_rtsa_sol.source_config), ) Source: path graph $P_4$ with vertices ${0, 1, 2, 3}$, edges $\{0,1\}, \{1,2\}, \{2,3\}$, and bound $K = 5$. \ Target: universe $X = {0, 1, 2, 3}$, subsets $\{0,1\}, \{1,2\}, \{2,3\}$, bound $K' = 5 - 3 = 2$. \ @@ -16388,7 +16262,7 @@ The following reductions to Integer Linear Programming are straightforward formu ][ _Construction._ Let $X = {0, dots, n - 1}$ and let the subset family be $cal(C) = {S_0, dots, S_(m-1)}$. For every subset of size 0 or 1 the model charges extension cost 0 automatically, so only the nontrivial subsets matter. Enumerate them as $I = {k_0 < dots < k_(r-1)} = {k : |S_k| >= 2}$. - Use `ILP`. The variable blocks are: + Use `ILP`. The variable blocks are: parent indicators $p_(v,u) in {0, 1}$ for all $v, u in X$; depths $d_v in {0, dots, n - 1}$; ancestor indicators $a_(u,v) in {0, 1}$, where $a_(u,v) = 1$ means $u$ is an ancestor of $v$ (allowing $u = v$); @@ -16486,11 +16360,11 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example " + problem-spec(mcmf_mcc.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(mcmf_mcc) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + mcmf_mcc_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(mcmf_mcc_sol.source_config), ) - Source flow $(#mcmf_mcc_sol.source_config.map(str).join(", "))$; target circulation $(#mcmf_mcc_sol.target_config.map(str).join(", "))$ appends the return arc. + Source flow $(#fmt-values(mcmf_mcc_sol.source_config))$; target circulation $(#fmt-values(mcmf_mcc_sol.target_config))$ appends the return arc. ], )[ Augment the flow network with a single return arc from the sink to the source. Give it capacity equal to a feasible-flow upper bound and a sufficiently negative cost so that the resulting min-cost circulation lex-orders the original (max value, min cost) objective. Recover the source flow by deleting the return arc. @@ -16514,7 +16388,7 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("MinimumEdgeCostFlow", "ILP")[ Introduce integer flow variables and binary arc-activation indicators, link them so that an indicator is forced to 1 whenever the corresponding arc carries positive flow, and minimize the total price of activated arcs. ][ - _Construction._ Let $m = |A|$ and $n = |V|$. Use `ILP` with $2m$ variables: integer flow variables $f_a in {0, dots, c(a)}$ for $a in {0, dots, m - 1}$ and binary activation indicators $y_a in {0, 1}$ for $a in {m, dots, 2m - 1}$. + _Construction._ Let $m = |A|$ and $n = |V|$. Use `ILP` with $2m$ variables: integer flow variables $f_a in {0, dots, c(a)}$ for $a in {0, dots, m - 1}$ and binary activation indicators $y_a in {0, 1}$ for $a in {m, dots, 2m - 1}$. Constraints: - _Linking:_ $f_a - c(a) dot y_a <= 0$ for each arc $a$ — forces $y_a = 1$ when $f_a > 0$ ($m$ constraints). @@ -16531,74 +16405,11 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ Output the first $m$ variables $(f_0, dots, f_(m-1))$ as the flow assignment. ] -#{ - let mfas_mlr = load-example("MinimumFeedbackArcSet", "MaximumLikelihoodRanking") - let mfas_mlr_sol = mfas_mlr.solutions.at(0) - let source-arcs = mfas_mlr.source.instance.graph.arcs - let target-matrix = mfas_mlr.target.instance.matrix - let ranking = mfas_mlr_sol.target_config - let removed-indices = mfas_mlr_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, _)) => i) - let removed-arcs = removed-indices.map(i => source-arcs.at(i)) - let target-cost = 0 - for a in range(target-matrix.len()) { - for b in range(target-matrix.len()) { - if a != b and ranking.at(a) > ranking.at(b) { - target-cost += target-matrix.at(a).at(b) - } - } - } - let fmt-mat(m) = m.map(row => row.map(v => str(v)).join(", ")).join("; ") - [ - #reduction-rule("MinimumFeedbackArcSet", "MaximumLikelihoodRanking", - example: true, - example-caption: [5-vertex digraph ($n = #mfas_mlr.source.instance.graph.num_vertices$, $|A| = #source-arcs.len()$, unit weights) mapped to a skew-symmetric ranking matrix], - extra: [ - #pred-commands( - "pred create --example " + problem-spec(mfas_mlr.source) + " -o mfas.json", - "pred reduce mfas.json --to " + target-spec(mfas_mlr) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate mfas.json --config " + mfas_mlr_sol.source_config.map(str).join(","), - ) - - *Step 1 -- Source instance.* The source digraph has vertices ${#range(mfas_mlr.source.instance.graph.num_vertices).map(str).join(", ")}$ and arcs #{source-arcs.map(a => $(#(a.at(0)) arrow #(a.at(1)))$).join(", ")}, all with unit weight. The extracted optimal feedback arc set removes #{removed-arcs.map(a => $(#(a.at(0)) arrow #(a.at(1)))$).join(" and ")}, so $|F| = #removed-arcs.len()$. - - *Step 2 -- Build the comparison matrix.* The reduction keeps the same item set and writes $M_(i j) = 1$ when only $i arrow j$ exists, $M_(i j) = -1$ when only $j arrow i$ exists, and $M_(i j) = 0$ otherwise. For this instance, - $ M = mat(#fmt-mat(target-matrix)). $ - Every off-diagonal pair sums to $0$, so the target is a valid Maximum Likelihood Ranking instance with $c = 0$. - - *Step 3 -- Verify a solution.* The stored ranking vector is $(#ranking.map(str).join(", "))$, interpreted as the map from items to ranks. The target disagreement cost is $#target-cost = 2 dot #removed-arcs.len() - #source-arcs.len()$, and the extracted source witness is exactly the backward-arc set #{removed-arcs.map(a => $(#(a.at(0)) arrow #(a.at(1)))$).join(" and ")} #sym.checkmark - - *Multiplicity:* The fixture stores one canonical optimum. Other optimal rankings exist because the DAG obtained after removing the two backward arcs has multiple valid topological orders. - ], - )[ - This $O(n^2)$ reduction @garey1979 applies to unit-weight feedback arc set instances. It keeps the same vertex set as ranking items and encodes each unordered pair by a skew-symmetric entry in $\{-1, 0, 1\}$ with comparison count $c = 0$. - ][ - _Construction._ Given a unit-weight Minimum Feedback Arc Set instance $(G = (V, A), bold(1))$ with $V = \{0, dots, n - 1\}$, construct the matrix $M in ZZ^(n times n)$ by setting $M_(i i) = 0$ and, for every distinct pair $i, j$, - $ - M_(i j) = cases( - 1 & "if" (i arrow j) in A and (j arrow i) not in A, - -1 & "if" (j arrow i) in A and (i arrow j) not in A, - 0 & "otherwise" - ). - $ - Then $M_(i j) + M_(j i) = 0$ for all $i != j$, so the target is a valid Maximum Likelihood Ranking instance with $n$ items. - - _Correctness._ ($arrow.r.double$) Let $pi$ be any ranking and let $B(pi) = \{(u arrow v) in A : pi(u) > pi(v)\}$ be its backward arcs. Removing $B(pi)$ leaves only forward arcs, hence a DAG, so $B(pi)$ is a feedback arc set. Partition unordered vertex pairs into one-directional pairs $A_1$ and bidirectional pairs $A_2$. Every one-directional backward arc contributes $+1$ to the MLR objective, every one-directional forward arc contributes $-1$, and bidirectional or absent pairs contribute $0$. Therefore - $ - "cost"(pi) = 2 |B(pi)| - (|A_1| + 2|A_2|) = 2 |B(pi)| - |A|. - $ - The target objective is thus the source objective shifted by the constant $-|A|$, so minimizing disagreement cost minimizes feedback arc set size. ($arrow.l.double$) Let $F subset.eq A$ be a minimum feedback arc set, and take a topological order $pi$ of the DAG $G - F$. Every arc in $A backslash F$ is forward in $pi$, hence every backward arc under $pi$ lies in $F$, so $B(pi) subset.eq F$. Since $B(pi)$ is itself a feedback arc set by the previous argument, minimality of $F$ forces $|B(pi)| = |F|$. Therefore an optimal source solution yields an optimal target ranking. - - _Solution extraction._ Given the target rank vector, output one source bit per source arc $(u arrow v)$ in source-arc order: set the bit to $1$ iff item $u$ is ranked after item $v$, and to $0$ otherwise. - ] - ] -} - #{ let mlr_ilp = load-example("MaximumLikelihoodRanking", "ILP") let mlr_ilp_sol = mlr_ilp.solutions.at(0) let mlr_n = mlr_ilp.source.instance.matrix.len() - let mlr_nv = mlr_ilp.target.instance.num_vars + let mlr_nv = mlr_ilp.target.instance.variables.len() let mlr_nc = mlr_ilp.target.instance.constraints.len() [ #reduction-rule("MaximumLikelihoodRanking", "ILP", @@ -16607,15 +16418,15 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example MaximumLikelihoodRanking -o mlr.json", - "pred reduce mlr.json --to " + target-spec(mlr_ilp) + " -o bundle.json", + "pred reduce mlr.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mlr.json --config " + mlr_ilp_sol.source_config.map(str).join(","), + "pred evaluate mlr.json --config " + cli-config(mlr_ilp_sol.source_config), ) *Step 1 -- Source instance.* A ranking instance with $n = #mlr_n$ items and comparison matrix $A$. *Step 2 -- Build the ILP.* Introduce $binom(#str(mlr_n), 2) = #mlr_nv$ binary variables $x_(i j)$ for each pair $i < j$. Add $#mlr_nc$ transitivity constraints from all $binom(#str(mlr_n), 3)$ triples. The ILP has #mlr_nv variables and #mlr_nc constraints. - *Step 3 -- Verify.* The ILP optimum extracts to ranking $(#mlr_ilp_sol.source_config.map(str).join(", "))$, which matches the source optimum #sym.checkmark. + *Step 3 -- Verify.* The ILP optimum extracts to ranking $(#fmt-values(mlr_ilp_sol.source_config))$, which matches the source optimum #sym.checkmark. ], )[ Each pair of items $(i, j)$ with $i < j$ gets a binary variable $x_(i j)$ indicating whether $i$ is ranked before $j$. Transitivity constraints enforce a valid linear order, and the objective minimizes the total disagreement cost. @@ -16642,7 +16453,7 @@ The following reductions to Integer Linear Programming are straightforward formu let ocst_ilp = load-example("OptimumCommunicationSpanningTree", "ILP") let ocst_ilp_sol = ocst_ilp.solutions.at(0) let ocst_n = ocst_ilp.source.instance.num_vertices - let ocst_nv = ocst_ilp.target.instance.num_vars + let ocst_nv = ocst_ilp.target.instance.variables.len() let ocst_nc = ocst_ilp.target.instance.constraints.len() [ #reduction-rule("OptimumCommunicationSpanningTree", "ILP", @@ -16651,27 +16462,27 @@ The following reductions to Integer Linear Programming are straightforward formu extra: [ #pred-commands( "pred create --example OptimumCommunicationSpanningTree -o ocst.json", - "pred reduce ocst.json --to " + target-spec(ocst_ilp) + " -o bundle.json", + "pred reduce ocst.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ocst.json --config " + ocst_ilp_sol.source_config.map(str).join(","), + "pred evaluate ocst.json --config " + cli-config(ocst_ilp_sol.source_config), ) *Step 1 -- Source instance.* $K_#ocst_n$ with edge weight and requirement matrices. *Step 2 -- Build the ILP.* Introduce edge selectors and multi-commodity flow variables. The ILP has #ocst_nv variables and #ocst_nc constraints. - *Step 3 -- Verify.* The ILP optimum extracts to edge selection $(#ocst_ilp_sol.source_config.map(str).join(", "))$, which matches the source optimum #sym.checkmark. + *Step 3 -- Verify.* The ILP optimum extracts to edge selection $(#fmt-values(ocst_ilp_sol.source_config))$, which matches the source optimum #sym.checkmark. ], )[ - Binary edge selectors determine the spanning tree. Multi-commodity flow variables route one unit of flow for each vertex pair with positive requirement, and the objective minimizes the total weighted communication cost. + Binary edge selectors determine the spanning tree. Multi-commodity flow variables route one unit for every vertex pair, including zero-requirement pairs, and the objective minimizes the total weighted communication cost @hu1974ocst. ][ - _Construction._ Given $K_n$ with weights $w(e)$ and requirements $r(u, v)$, let $m = n(n-1)/2$. Introduce binary edge variables $x_e$ for each edge. For each pair $(s, t)$ with $r(s, t) > 0$, add directed flow variables $f^(s t)_(e, "fwd")$ and $f^(s t)_(e, "bwd")$ for each edge $e$. + _Construction._ Given $K_n$ with weights $w(e)$ and requirements $r(u, v)$, let $m = n(n-1)/2$. Introduce binary edge variables $x_e$ for each edge. For every pair $(s, t)$ with $s < t$, add directed flow variables $f^(s t)_(e, "fwd")$ and $f^(s t)_(e, "bwd")$ for each edge $e$. Zero-requirement commodities have zero objective coefficient but are retained to certify that every vertex belongs to the spanning tree. Constraints: - Tree size: $sum_e x_e = n - 1$ - Flow conservation: for each commodity $(s, t)$ and each vertex $v$, net inflow equals $-1$ at source $s$, $+1$ at sink $t$, and $0$ elsewhere. - Capacity linking: $f^(s t)_(e, "dir") <= x_e$ for each commodity and direction. - Objective: $min sum_((s,t): r > 0) r(s,t) dot sum_e w(e) dot (f^(s t)_(e, "fwd") + f^(s t)_(e, "bwd")))$ + Objective: $min sum_(s (e.at(0), e.at(1))) let config = x.optimal_config - let resolving = range(nv).filter(v => config.at(v) == 1) + let resolving = range(nv).filter(v => config.at(v)) let size = metric-value(x.optimal_value) [ #problem-def("MinimumMetricDimension")[ @@ -16968,7 +16803,7 @@ The following table shows concrete variable overhead for example instances, take #pred-commands( "pred create --example MinimumMetricDimension -o mmd.json", "pred solve mmd.json", - "pred evaluate mmd.json --config " + config.map(str).join(","), + "pred evaluate mmd.json --config " + cli-config(config), ) ] ] @@ -17002,7 +16837,7 @@ The following table shows concrete variable overhead for example instances, take #pred-commands( "pred create --example MinimumGraphBandwidth -o mgb.json", "pred solve mgb.json", - "pred evaluate mgb.json --config " + config.map(str).join(","), + "pred evaluate mgb.json --config " + cli-config(config), ) ] ] @@ -17019,7 +16854,7 @@ The following table shows concrete variable overhead for example instances, take ] #{ - let x = load-model-example("MinimumCapacitatedSpanningTree", variant: (graph: "SimpleGraph", weight: "i32")) + let x = load-model-example("MinimumCapacitatedSpanningTree", variant: (graph: "SimpleGraph", weight: "i64")) let nv = x.instance.graph.num_vertices let edges-j = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) let ew = x.instance.weights @@ -17027,7 +16862,7 @@ The following table shows concrete variable overhead for example instances, take let reqs = x.instance.requirements let root = x.instance.root let config = x.optimal_config - let sel-idx = range(edges-j.len()).filter(i => config.at(i) == 1) + let sel-idx = range(edges-j.len()).filter(i => config.at(i)) let total-weight = metric-value(x.optimal_value) [ #problem-def("MinimumCapacitatedSpanningTree")[ @@ -17035,23 +16870,23 @@ The following table shows concrete variable overhead for example instances, take ][ Minimum Capacitated Spanning Tree (ND5) @garey1979. NP-hard in the strong sense, even with unit requirements and capacity 3. - *Example.* Consider $G$ with $n = #nv$ vertices, $|E| = #{edges-j.len()}$ edges, root $v_#root$, capacity $c = #cap$, vertex requirements $r = (#reqs.map(str).join(", "))$, and edge weights $w = (#ew.map(str).join(", "))$. The optimal tree selects edges #sel-idx.map(i => $(v_#(edges-j.at(i).at(0)), v_#(edges-j.at(i).at(1)))$).join(", ") with total weight $#sel-idx.map(i => str(ew.at(i))).join(" + ") = #total-weight$. Every subtree rooted away from $v_#root$ has total requirement at most $c = #cap$. + *Example.* Consider $G$ with $n = #nv$ vertices, $|E| = #{edges-j.len()}$ edges, root $v_#root$, capacity $c = #cap$, vertex requirements $r = (#fmt-values(reqs))$, and edge weights $w = (#fmt-values(ew))$. The optimal tree selects edges #sel-idx.map(i => $(v_#(edges-j.at(i).at(0)), v_#(edges-j.at(i).at(1)))$).join(", ") with total weight $#sel-idx.map(i => str(ew.at(i))).join(" + ") = #total-weight$. Every subtree rooted away from $v_#root$ has total requirement at most $c = #cap$. #pred-commands( "pred create --example MinimumCapacitatedSpanningTree -o mcst.json", "pred solve mcst.json", - "pred evaluate mcst.json --config " + config.map(str).join(","), + "pred evaluate mcst.json --config " + cli-config(config), ) ] ] } #reduction-rule("MinimumCapacitatedSpanningTree", "ILP")[ - Binary edge selectors $y_e$ plus directed requirement-flow variables. Flow conservation routes each vertex's requirement to the root; capacity constraints bound flow per edge. + Binary edge selectors $y_e$, directed requirement-flow variables $f$, and directed unit-demand connectivity variables $g$. The first flow enforces subtree capacities; the second connects even zero-requirement vertices. ][ - _Construction._ $3m$ variables: $m$ edge selectors + $2m$ directed flow. Tree cardinality, binary bounds, flow conservation (each non-root vertex generates $r(v)$ flow toward root), flow-edge linking, and per-edge capacity bounds ($<= c$). Minimize $sum_e w(e) dot y_e$. + _Construction._ $5m$ variables: $m$ edge selectors and two directed flows of $2m$ variables each. Requirement flow sends $r(v)$ units from each non-root vertex to the root and is bounded by capacity $c$. Connectivity flow sends one unit from every non-root vertex to the root and satisfies $g_(u v)+g_(v u) <= (n-1)y_e$. Also impose $sum_e y_e=n-1$ and minimize $sum_e w(e) dot y_e$. This combines the standard non-unit-demand flow model @gouveiaLopes2000 with the standard unit-demand spanning-tree flow. - _Correctness._ ($arrow.r.double$) A feasible capacitated spanning tree induces a valid flow. ($arrow.l.double$) A feasible ILP solution encodes a spanning tree satisfying capacity constraints. + _Correctness._ ($arrow.r.double$) A feasible capacitated spanning tree induces both flows along its unique root paths. ($arrow.l.double$) Unit-demand flow makes every vertex reachable from the root; together with $n-1$ selected edges this gives a spanning tree. Requirement flow on that tree equals each rooted subtree's total requirement, so its capacity bounds are exactly the source constraints. _Solution extraction._ First $m$ variables (edge selectors) give the source configuration. ] @@ -17070,16 +16905,13 @@ The following table shows concrete variable overhead for example instances, take // === Non-ILP reduction rules (issue #974) === #reduction-rule("ILP", "ILP")[ - Integer variables with finite upper bounds are replaced by groups of binary variables using truncated binary encoding. Feasibility-Based Bound Tightening (FBBT) first infers per-variable upper bounds $U_i$ from the constraint system; each integer variable $x_i in {0, dots, U_i}$ is then encoded as $x_i = sum_(j=0)^(K_i - 1) w_(i j) y_(i j)$ where $y_(i j) in {0, 1}$ and the weights are powers of two with a truncated final weight ensuring $sum w_(i j) = U_i$. + ILP variants convert between binary and bounded integer variable domains and between exact-integer and floating-point coefficients. Binary variables embed directly into integer variables. A finitely bounded integer variable is encoded by binary variables with truncated positional weights. Integer coefficients are embedded only when every stored coefficient and right-hand side has an exact `f64` representation. ][ - _Construction._ Given an ILP over $n$ non-negative integer variables with constraints $A x <= b$ (and/or $>=, =$) and objective $c^top x$: - (1) Run FBBT to infer upper bounds $U_1, dots, U_n$. If infeasible, return a trivially infeasible binary ILP. If unbounded, fall back to $U_i = 2^(31) - 1$. - (2) For each $x_i$, compute $K_i = ceil(log_2(U_i + 1))$ and weights $w_(i 0) = 1, w_(i 1) = 2, dots, w_(i, K_i - 2) = 2^(K_i - 2), w_(i, K_i - 1) = U_i - (2^(K_i - 1) - 1)$. - (3) Substitute $x_i = sum_j w_(i j) y_(i j)$ into every constraint and objective term, expanding each integer coefficient into a sum over binary coefficients. + _Construction._ For the binary-to-integer edge, copy the variables, constraints, objective, and optimization direction unchanged. For an integer variable $x_i in [L_i, U_i]$, let $D_i = U_i - L_i$ and choose positive truncated binary weights $w_(i j)$ whose subset sums represent every integer from $0$ through $D_i$; substitute $x_i = L_i + sum_j w_(i j)y_(i j)$ into every constraint and objective term. This edge rejects variables without two finite bounds. For the coefficient edge, copy the variable bounds and optimization direction and convert each entry of the constraint matrix, right-hand side, and objective independently; reject the instance if any integer lies outside the exactly representable `f64` integer range. - _Correctness._ ($arrow.r.double$) Any integer assignment $x_i in {0, dots, U_i}$ has a unique truncated binary representation $y_(i j)$ with $sum w_(i j) y_(i j) = x_i$, preserving all constraints and the objective value. ($arrow.l.double$) Any binary assignment $y_(i j) in {0,1}$ yields $x_i = sum w_(i j) y_(i j) in {0, dots, U_i}$, so the decoded integer assignment satisfies the original constraints. + _Correctness._ The binary-to-integer embedding changes no mathematical expression. For bounded integer variables, every $x_i in [L_i,U_i]$ has a truncated binary representation, and every binary assignment decodes inside that interval; substitution preserves all constraints and objective values. Exact conversion preserves every stored coefficient, so it constructs the same formal linear objective and constraints over the same integer variables. - _Solution extraction._ For each source variable $x_i$, compute $x_i = sum_(j=0)^(K_i - 1) w_(i j) y_(i j)$ from the binary solution. + _Solution extraction._ Binary-to-integer and coefficient conversions preserve the assignment; coefficient conversion additionally checks the assignment against the source integer ILP. Binary encoding returns $x_i = L_i + sum_j w_(i j)y_(i j)$. ] #let hc_hp = load-example("HamiltonianCircuit", "HamiltonianPath") @@ -17094,16 +16926,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hc_hp.source) + " -o hc.json", - "pred reduce hc.json --to " + target-spec(hc_hp) + " -o bundle.json", + "pred reduce hc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hc.json --config " + hc_hp_sol.source_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(hc_hp_sol.source_config), ) - *Step 1 -- Source instance.* The canonical source fixture is the cycle $C_#hc_hp_n$ on vertices ${0, dots, #(hc_hp_n - 1)}$ with #hc_hp_source_edges.len() edges: #hc_hp_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The stored Hamiltonian-circuit witness is the permutation $[#hc_hp_sol.source_config.map(str).join(", ")]$.\ + *Step 1 -- Source instance.* The canonical source fixture is the cycle $C_#hc_hp_n$ on vertices ${0, dots, #(hc_hp_n - 1)}$ with #hc_hp_source_edges.len() edges: #hc_hp_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The stored Hamiltonian-circuit witness is the permutation $[#fmt-values(hc_hp_sol.source_config)]$.\ *Step 2 -- Construction.* Fix $v_0 = 0$. Its neighbors in the source are ${1, 3}$ ($deg = 2$). Introduce $v' = #hc_hp_n$, $s = #(hc_hp_n + 1)$, $t = #(hc_hp_n + 2)$. The target graph $G'$ has $#hc_hp_target_n = #hc_hp_n + 3$ vertices and #hc_hp_target_edges.len() edges: #hc_hp_target_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The $#(hc_hp_target_edges.len() - hc_hp_source_edges.len())$ new edges are the duplicated adjacencies of $v'$ plus the two pendant edges.\ - *Step 3 -- Verify a solution.* The stored target Hamiltonian-path permutation is $[#hc_hp_sol.target_config.map(str).join(", ")]$, visiting every vertex of $G'$ exactly once. The path starts at pendant $s = #hc_hp_sol.target_config.at(0)$ and ends at pendant $t = #hc_hp_sol.target_config.at(hc_hp_target_n - 1)$. Dropping $s$ at the front and the last two vertices $v', t$ at the back gives $[#hc_hp_sol.target_config.slice(1, hc_hp_target_n - 2).map(str).join(", ")]$, which is the source Hamiltonian circuit $[#hc_hp_sol.source_config.map(str).join(", ")]$.\ + *Step 3 -- Verify a solution.* The stored target Hamiltonian-path permutation is $[#fmt-values(hc_hp_sol.target_config)]$, visiting every vertex of $G'$ exactly once. The path starts at pendant $s = #hc_hp_sol.target_config.at(0)$ and ends at pendant $t = #hc_hp_sol.target_config.at(hc_hp_target_n - 1)$. Dropping $s$ at the front and the last two vertices $v', t$ at the back gives $[#hc_hp_sol.target_config.slice(1, hc_hp_target_n - 2).map(str).join(", ")]$, which is the source Hamiltonian circuit $[#fmt-values(hc_hp_sol.source_config)]$.\ *Multiplicity:* The fixture stores one canonical witness. For $C_#hc_hp_n$ there are $#hc_hp_n times 2 = #(hc_hp_n * 2)$ directed Hamiltonian circuits (choice of start vertex and direction), each yielding a distinct target path. ], @@ -17125,16 +16957,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(kc_si.source) + " -o kclique.json", - "pred reduce kclique.json --to " + target-spec(kc_si) + " -o bundle.json", + "pred reduce kclique.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate kclique.json --config " + kc_si_sol.source_config.map(str).join(","), + "pred evaluate kclique.json --config " + cli-config(kc_si_sol.source_config), ) #{ let n = kc_si.source.instance.graph.num_vertices let k = kc_si.source.instance.k - let clique-verts = kc_si_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) - let fills = kc_si_sol.source_config.map(c => if c == 1 { graph-colors.at(0) } else { white }) + let clique-verts = kc_si_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) + let fills = kc_si_sol.source_config.map(selected => if selected { graph-colors.at(0) } else { white }) // Simple circular layout for the source graph let angle-step = 360deg / n let radius = 2.0 @@ -17158,7 +16990,7 @@ The following table shows concrete variable overhead for example instances, take *Step 3 -- Variable semantics.* The source uses $n = #kc_si.source.instance.graph.num_vertices$ binary indicator variables ($x_v = 1$ iff vertex $v$ is in the clique). The target uses $k = #kc_si.source.instance.k$ variables, each ranging over ${0, dots, n - 1}$, specifying which host vertex each pattern vertex maps to. - *Step 4 -- Verify a solution.* The canonical witness has source config $bold(x) = (#kc_si_sol.source_config.map(str).join(", "))$, selecting vertices ${#kc_si_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$. The target config is $(#kc_si_sol.target_config.map(str).join(", "))$, mapping pattern vertex $i$ to host vertex $c_i$. The image vertices ${#kc_si_sol.target_config.map(str).join(", ")}$ are pairwise adjacent in $G$ (they form a triangle), confirming the isomorphism #sym.checkmark + *Step 4 -- Verify a solution.* The canonical witness has source config $bold(x) = (#fmt-values(kc_si_sol.source_config))$, selecting vertices ${#kc_si_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ")}$. The target config is $(#fmt-values(kc_si_sol.target_config))$, mapping pattern vertex $i$ to host vertex $c_i$. The image vertices ${#fmt-values(kc_si_sol.target_config)}$ are pairwise adjacent in $G$ (they form a triangle), confirming the isomorphism #sym.checkmark *Multiplicity:* The fixture stores one canonical witness. The $k!$ permutations of the clique vertices each give a valid subgraph isomorphism, so the target side has $#kc_si.source.instance.k ! = #calc.fact(kc_si.source.instance.k)$ witnesses for this single source clique. ], @@ -17179,8 +17011,8 @@ The following table shows concrete variable overhead for example instances, take #let part_mps_total = part_mps_sizes.fold(0, (a, b) => a + b) #let part_mps_deadline = part_mps.target.instance.deadline #let part_mps_nproc = part_mps.target.instance.num_processors -#let part_mps_proc0 = part_mps_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => i) -#let part_mps_proc1 = part_mps_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let part_mps_proc0 = part_mps_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => i) +#let part_mps_proc1 = part_mps_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let part_mps_load0 = part_mps_proc0.map(i => part_mps_sizes.at(i)).fold(0, (a, b) => a + b) #let part_mps_load1 = part_mps_proc1.map(i => part_mps_sizes.at(i)).fold(0, (a, b) => a + b) #reduction-rule("Partition", "MultiprocessorScheduling", @@ -17189,16 +17021,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(part_mps.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_mps) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_mps_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_mps_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#part_mps_sizes.map(str).join(", "))$ with total sum $S = #part_mps_total$. A balanced partition requires each subset to sum to $S / 2 = #part_mps_deadline$. + *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#fmt-values(part_mps_sizes))$ with total sum $S = #part_mps_total$. A balanced partition requires each subset to sum to $S / 2 = #part_mps_deadline$. - *Step 2 -- Construction.* The reduction creates #part_mps_n tasks with lengths $(#part_mps.target.instance.lengths.map(str).join(", "))$, sets the number of processors to $m = #part_mps_nproc$, and the deadline to $D = floor(S / 2) = #part_mps_deadline$. No auxiliary variables are introduced: the target has the same #part_mps_n binary coordinates as the source. + *Step 2 -- Construction.* The reduction creates #part_mps_n tasks with lengths $(#fmt-values(part_mps.target.instance.lengths))$, sets the number of processors to $m = #part_mps_nproc$, and the deadline to $D = floor(S / 2) = #part_mps_deadline$. No auxiliary variables are introduced: the target has the same #part_mps_n binary coordinates as the source. - *Step 3 -- Verify a solution.* The canonical witness is $bold(x) = (#part_mps_sol.source_config.map(str).join(", "))$, which is the same binary vector on both sides. Processor 0 receives tasks at indices $\{#part_mps_proc0.map(str).join(", ")\}$ with sizes $(#part_mps_proc0.map(i => str(part_mps_sizes.at(i))).join(", "))$, giving load $#part_mps_load0 <= #part_mps_deadline = D$. Processor 1 receives tasks at indices $\{#part_mps_proc1.map(str).join(", ")\}$ with sizes $(#part_mps_proc1.map(i => str(part_mps_sizes.at(i))).join(", "))$, giving load $#part_mps_load1 <= #part_mps_deadline = D$. Total: $#part_mps_load0 + #part_mps_load1 = #part_mps_total = S$ #sym.checkmark + *Step 3 -- Verify a solution.* The canonical witness is $bold(x) = (#fmt-values(part_mps_sol.source_config))$, which is the same binary vector on both sides. Processor 0 receives tasks at indices $\{#fmt-values(part_mps_proc0)\}$ with sizes $(#part_mps_proc0.map(i => str(part_mps_sizes.at(i))).join(", "))$, giving load $#part_mps_load0 <= #part_mps_deadline = D$. Processor 1 receives tasks at indices $\{#fmt-values(part_mps_proc1)\}$ with sizes $(#part_mps_proc1.map(i => str(part_mps_sizes.at(i))).join(", "))$, giving load $#part_mps_load1 <= #part_mps_deadline = D$. Total: $#part_mps_load0 + #part_mps_load1 = #part_mps_total = S$ #sym.checkmark *Multiplicity:* The example DB stores one canonical witness. This instance admits other balanced partitions (e.g., swapping the two subsets), but one witness suffices to demonstrate the reduction. ], @@ -17219,8 +17051,8 @@ The following table shows concrete variable overhead for example instances, take #let part_sosp_total = part_sosp_sizes.fold(0, (a, b) => a + b) #let part_sosp_half = part_sosp_total / 2 #let part_sosp_opt = part_sosp_half * part_sosp_half * 2 -#let part_sosp_group0 = part_sosp_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => i) -#let part_sosp_group1 = part_sosp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let part_sosp_group0 = part_sosp_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => i) +#let part_sosp_group1 = part_sosp_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let part_sosp_load0 = part_sosp_group0.map(i => part_sosp_sizes.at(i)).fold(0, (a, b) => a + b) #let part_sosp_load1 = part_sosp_group1.map(i => part_sosp_sizes.at(i)).fold(0, (a, b) => a + b) #reduction-rule("Partition", "SumOfSquaresPartition", @@ -17229,16 +17061,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(part_sosp.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_sosp) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_sosp_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_sosp_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#part_sosp_sizes.map(str).join(", "))$ with total sum $S = #part_sosp_total$. A balanced 2-partition splits the elements into subsets summing to $S / 2 = #part_sosp_half$ each. + *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#fmt-values(part_sosp_sizes))$ with total sum $S = #part_sosp_total$. A balanced 2-partition splits the elements into subsets summing to $S / 2 = #part_sosp_half$ each. *Step 2 -- Construction.* The reduction copies the element sizes verbatim (cast to signed integers) into a SumOfSquaresPartition instance with $K = 2$ groups; no auxiliary variables are introduced. The same #part_sosp_n binary coordinates serve as both source subset assignments and target group assignments. - *Step 3 -- Verify a solution.* The canonical witness is $bold(x) = (#part_sosp_sol.source_config.map(str).join(", "))$. Group 0 contains indices $\{#part_sosp_group0.map(str).join(", ")\}$ with sizes $(#part_sosp_group0.map(i => str(part_sosp_sizes.at(i))).join(", "))$, summing to $#part_sosp_load0$. Group 1 contains indices $\{#part_sosp_group1.map(str).join(", ")\}$ with sizes $(#part_sosp_group1.map(i => str(part_sosp_sizes.at(i))).join(", "))$, summing to $#part_sosp_load1$. The sum of squared group sums is $#part_sosp_load0^2 + #part_sosp_load1^2 = #part_sosp_opt = S^2 / 2$ #sym.checkmark + *Step 3 -- Verify a solution.* The canonical witness is $bold(x) = (#fmt-values(part_sosp_sol.source_config))$. Group 0 contains indices $\{#fmt-values(part_sosp_group0)\}$ with sizes $(#part_sosp_group0.map(i => str(part_sosp_sizes.at(i))).join(", "))$, summing to $#part_sosp_load0$. Group 1 contains indices $\{#fmt-values(part_sosp_group1)\}$ with sizes $(#part_sosp_group1.map(i => str(part_sosp_sizes.at(i))).join(", "))$, summing to $#part_sosp_load1$. The sum of squared group sums is $#part_sosp_load0^2 + #part_sosp_load1^2 = #part_sosp_opt = S^2 / 2$ #sym.checkmark *Multiplicity:* The example DB stores one canonical balanced witness; other balanced splits of this instance also attain the optimum. ], @@ -17262,16 +17094,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hc_btsp.source) + " -o hc.json", - "pred reduce hc.json --to " + target-spec(hc_btsp) + " -o bundle.json", + "pred reduce hc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hc.json --config " + hc_btsp_sol.source_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(hc_btsp_sol.source_config), ) *Step 1 -- Source instance.* The canonical HC instance is a graph with $n = #graph-num-vertices(hc_btsp.source.instance)$ vertices and $|E| = #graph-num-edges(hc_btsp.source.instance)$ edges. - *Step 2 -- Construction.* Build the complete graph $K_#graph-num-vertices(hc_btsp.target.instance)$ with #graph-num-edges(hc_btsp.target.instance) edges. Each original edge gets weight 1 and each non-edge gets weight 2. The target edge weights are $(#hc_btsp.target.instance.edge_weights.map(str).join(", "))$, where the #hc_btsp.target.instance.edge_weights.filter(w => w == 1).len() entries equal to 1 correspond to the $|E| = #graph-num-edges(hc_btsp.source.instance)$ source edges and the #hc_btsp.target.instance.edge_weights.filter(w => w == 2).len() entries equal to 2 correspond to the non-edges. + *Step 2 -- Construction.* Build the complete graph $K_#graph-num-vertices(hc_btsp.target.instance)$ with #graph-num-edges(hc_btsp.target.instance) edges. Each original edge gets weight 1 and each non-edge gets weight 2. The target edge weights are $(#fmt-values(hc_btsp.target.instance.edge_weights))$, where the #hc_btsp.target.instance.edge_weights.filter(w => w == 1).len() entries equal to 1 correspond to the $|E| = #graph-num-edges(hc_btsp.source.instance)$ source edges and the #hc_btsp.target.instance.edge_weights.filter(w => w == 2).len() entries equal to 2 correspond to the non-edges. - *Step 3 -- Verify a solution.* The source tour visits vertices in order $(#hc_btsp_sol.source_config.map(str).join(", "))$. In the target, the selected edges are those with indicator 1 in $(#hc_btsp_sol.target_config.map(str).join(", "))$, all of which have weight 1, giving bottleneck cost $= 1$ #sym.checkmark. + *Step 3 -- Verify a solution.* The source tour visits vertices in order $(#fmt-values(hc_btsp_sol.source_config))$. In the target, the selected edges are those with indicator 1 in $(#fmt-values(hc_btsp_sol.target_config))$, all of which have weight 1, giving bottleneck cost $= 1$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. The $#graph-num-vertices(hc_btsp.source.instance)$-cycle has $#graph-num-vertices(hc_btsp.source.instance)$ rotations $times$ 2 reflections $= #(2 * graph-num-vertices(hc_btsp.source.instance))$ Hamiltonian circuits in total. ], @@ -17293,15 +17125,15 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(kc_cbq.source) + " -o kclique.json", - "pred reduce kclique.json --to " + target-spec(kc_cbq) + " -o bundle.json", + "pred reduce kclique.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate kclique.json --config " + kc_cbq_sol.source_config.map(str).join(","), + "pred evaluate kclique.json --config " + cli-config(kc_cbq_sol.source_config), ) #{ let verts = ((0, 1.5), (1.5, 2.5), (3, 1.5), (1.5, 0), (4.5, 0)) let edges = kc_cbq.source.instance.graph.edges - let clique-verts = kc_cbq_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) + let clique-verts = kc_cbq_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) align(center, canvas(length: 0.8cm, { for (u, v) in edges { let in-clique = clique-verts.contains(u) and clique-verts.contains(v) @@ -17320,7 +17152,7 @@ The following table shows concrete variable overhead for example instances, take *Step 3 -- Form the conjunctive query.* Introduce $k = #kc_cbq.target.instance.num_variables$ existential variables $y_0, y_1, y_2$ over $D$. The conjunction has $binom(k, 2) = #kc_cbq.target.instance.conjuncts.len()$ conjuncts: $R(y_0, y_1) and R(y_0, y_2) and R(y_1, y_2)$. - *Step 4 -- Verify a solution.* The satisfying assignment $(y_0, y_1, y_2) = (#kc_cbq_sol.target_config.map(str).join(", "))$ maps to vertices ${#kc_cbq_sol.target_config.map(str).join(", ")}$. Check each conjunct: $(#kc_cbq_sol.target_config.at(0), #kc_cbq_sol.target_config.at(1)) in R$ #sym.checkmark, $(#kc_cbq_sol.target_config.at(0), #kc_cbq_sol.target_config.at(2)) in R$ #sym.checkmark, $(#kc_cbq_sol.target_config.at(1), #kc_cbq_sol.target_config.at(2)) in R$ #sym.checkmark --- all pairs are adjacent, confirming the 3-clique. The source indicator is $(#kc_cbq_sol.source_config.map(str).join(", "))$, marking exactly vertices ${0, 1, 2}$. + *Step 4 -- Verify a solution.* The satisfying assignment $(y_0, y_1, y_2) = (#fmt-values(kc_cbq_sol.target_config))$ maps to vertices ${#fmt-values(kc_cbq_sol.target_config)}$. Check each conjunct: $(#kc_cbq_sol.target_config.at(0), #kc_cbq_sol.target_config.at(1)) in R$ #sym.checkmark, $(#kc_cbq_sol.target_config.at(0), #kc_cbq_sol.target_config.at(2)) in R$ #sym.checkmark, $(#kc_cbq_sol.target_config.at(1), #kc_cbq_sol.target_config.at(2)) in R$ #sym.checkmark --- all pairs are adjacent, confirming the 3-clique. The source indicator is $(#fmt-values(kc_cbq_sol.source_config))$, marking exactly vertices ${0, 1, 2}$. *Multiplicity:* The fixture stores one canonical witness. The triangle ${0, 1, 2}$ is the unique 3-clique in this graph, but the CBQ query has $3! = 6$ satisfying tuples (one per permutation of the three vertices); the canonical witness selects the sorted order $(0, 1, 2)$. ], @@ -17342,23 +17174,23 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(x3c_ss.source) + " -o x3c.json", - "pred reduce x3c.json --to " + target-spec(x3c_ss) + " -o bundle.json", + "pred reduce x3c.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate x3c.json --config " + x3c_ss_sol.source_config.map(str).join(","), + "pred evaluate x3c.json --config " + cli-config(x3c_ss_sol.source_config), ) - *Step 1 -- Source instance.* The X3C instance has universe $X = {0, dots, #(x3c_ss.source.instance.universe_size - 1)}$ ($|X| = #x3c_ss.source.instance.universe_size = 3q$, so $q = #(x3c_ss.source.instance.universe_size / 3)$) and $|cal(C)| = #x3c_ss.source.instance.subsets.len()$ subsets: #x3c_ss.source.instance.subsets.enumerate().map(((j, s)) => $S_#j = {#s.map(str).join(", ")}$).join(", "). + *Step 1 -- Source instance.* The X3C instance has universe $X = {0, dots, #(x3c_ss.source.instance.universe_size - 1)}$ ($|X| = #x3c_ss.source.instance.universe_size = 3q$, so $q = #(x3c_ss.source.instance.universe_size / 3)$) and $|cal(C)| = #x3c_ss.source.instance.subsets.len()$ subsets: #x3c_ss.source.instance.subsets.enumerate().map(((j, s)) => $S_#j = {#fmt-values(s)}$).join(", "). *Step 2 -- Construct StaffScheduling instance.* The reduction creates $#x3c_ss.target.instance.requirements.len()$ periods (one per universe element), each with requirement $r[i] = 1$. Each subset $S_j$ becomes a schedule $sigma_j$ with $sigma_j [i] = 1$ iff $i in S_j$, giving shifts_per_schedule $= #x3c_ss.target.instance.shifts_per_schedule$. The worker budget is $W = q = #x3c_ss.target.instance.num_workers$. - *Step 3 -- Verify a solution.* The canonical cover selects subsets via $(#x3c_ss_sol.source_config.map(str).join(", "))$: #{ - let selected = x3c_ss_sol.source_config.enumerate().filter(((j, x)) => x == 1).map(((j, x)) => { + *Step 3 -- Verify a solution.* The canonical cover selects subsets via $(#fmt-values(x3c_ss_sol.source_config))$: #{ + let selected = x3c_ss_sol.source_config.enumerate().filter(((j, x)) => x).map(((j, x)) => { let s = x3c_ss.source.instance.subsets.at(j) - $S_#j = {#s.map(str).join(", ")}$ + $S_#j = {#fmt-values(s)}$ }) selected.join(", ") - }. These $#x3c_ss_sol.source_config.filter(x => x == 1).len()$ subsets are pairwise disjoint and cover all $#x3c_ss.source.instance.universe_size$ elements #sym.checkmark \ - The target config $(#x3c_ss_sol.target_config.map(str).join(", "))$ assigns $w[j]$ workers to schedule $j$: total workers $= #x3c_ss_sol.target_config.sum() = q$ #sym.checkmark. Each period is covered by exactly one worker #sym.checkmark + }. These $#x3c_ss_sol.source_config.filter(x => x).len()$ subsets are pairwise disjoint and cover all $#x3c_ss.source.instance.universe_size$ elements #sym.checkmark \ + The target config $(#fmt-values(x3c_ss_sol.target_config))$ assigns $w[j]$ workers to schedule $j$: total workers $= #x3c_ss_sol.target_config.sum() = q$ #sym.checkmark. Each period is covered by exactly one worker #sym.checkmark *Multiplicity:* The fixture stores one canonical witness. The instance admits a second exact cover ${S_2, S_3}$, but the fixture records only the first found. ], @@ -17381,21 +17213,21 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_dmvc.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_dmvc) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_dmvc_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_dmvc_sol.source_config), ) *Step 1 -- Source instance.* The 3-SAT formula has $n = #ksat_dmvc.source.instance.num_vars$ variables and $m = #sat-num-clauses(ksat_dmvc.source.instance)$ clauses: #{ksat_dmvc.source.instance.clauses.enumerate().map(((j, c)) => { let lits = c.literals.map(l => if l > 0 { $x_#l$ } else { $overline(x)_#calc.abs(l)$ }) [$c_#j = (#lits.join($or$))$] - }).join(", ")}. A satisfying assignment is $(#ksat_dmvc_sol.source_config.map(str).join(", "))$. + }).join(", ")}. A satisfying assignment is $(#fmt-values(ksat_dmvc_sol.source_config))$. *Step 2 -- Build the vertex-cover graph.* Create one truth-setting edge $(u_i, overline(u)_i)$ per variable and one clause triangle per clause. Each triangle vertex is connected to the literal vertex of its clause position by a communication edge. The resulting graph has $|V| = 2n + 3m = #graph-num-vertices(ksat_dmvc.target.instance)$ vertices and $|E| = n + 6m = #graph-num-edges(ksat_dmvc.target.instance)$ edges. *Step 3 -- Add the decision threshold.* Wrap the constructed Minimum Vertex Cover instance in the decision predicate with bound $k = n + 2m = #ksat_dmvc.target.instance.bound$. For this example, $k = 3 + 2 dot 2 = 7$. - *Step 4 -- Verify a witness.* The target configuration $(#ksat_dmvc_sol.target_config.map(str).join(", "))$ selects #ksat_dmvc_sol.target_config.filter(x => x == 1).len() vertices, so it meets the bound exactly. Each truth-setting edge has one selected endpoint #sym.checkmark. Each clause triangle has two selected vertices #sym.checkmark. Each communication edge is covered either by its literal endpoint or by its triangle endpoint #sym.checkmark. Extracting the truth-setting choices returns $(#ksat_dmvc_sol.source_config.map(str).join(", "))$, which satisfies every clause #sym.checkmark + *Step 4 -- Verify a witness.* The target configuration $(#fmt-values(ksat_dmvc_sol.target_config))$ selects #ksat_dmvc_sol.target_config.filter(x => x).len() vertices, so it meets the bound exactly. Each truth-setting edge has one selected endpoint #sym.checkmark. Each clause triangle has two selected vertices #sym.checkmark. Each communication edge is covered either by its literal endpoint or by its triangle endpoint #sym.checkmark. Extracting the truth-setting choices returns $(#fmt-values(ksat_dmvc_sol.source_config))$, which satisfies every clause #sym.checkmark *Multiplicity:* The fixture stores one canonical decision witness. Different satisfying assignments may yield different size-$k$ covers of the same graph. ], @@ -17417,16 +17249,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(dmvc_hc.source) + " -o dmvc.json", - "pred reduce dmvc.json --to " + target-spec(dmvc_hc) + " -o bundle.json", + "pred reduce dmvc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate dmvc.json --config " + dmvc_hc_sol.source_config.map(str).join(","), + "pred evaluate dmvc.json --config " + cli-config(dmvc_hc_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Decision Minimum Vertex Cover fixture has inner graph $G$ on vertices ${0, 1, 2}$ with edges #{dmvc_hc.source.instance.inner.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")} and unit weights. The bound is $k = #dmvc_hc.source.instance.bound$, and the stored cover witness is $(#dmvc_hc_sol.source_config.map(str).join(", "))$, i.e.\ $C = {1}$. + *Step 1 -- Source instance.* The canonical Decision Minimum Vertex Cover fixture has inner graph $G$ on vertices ${0, 1, 2}$ with edges #{dmvc_hc.source.instance.inner.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")} and unit weights. The bound is $k = #dmvc_hc.source.instance.bound$, and the stored cover witness is $(#fmt-values(dmvc_hc_sol.source_config))$, i.e.\ $C = {1}$. *Step 2 -- Build the Hamiltonian graph.* There is one selector vertex $a_1$ and one 12-vertex gadget for each source edge, so the target graph has $1 + 2 dot 12 = #graph-num-vertices(dmvc_hc.target.instance)$ vertices. The path for source vertex $1$ chains the two gadgets through the connector from $(1, e_0, 6)$ to $(1, e_1, 1)$. The completed target has #graph-num-edges(dmvc_hc.target.instance) edges. - *Step 3 -- Verify a witness.* The stored Hamiltonian circuit is $(#dmvc_hc_sol.target_config.map(str).join(", "))$. Reading the cycle between selector contacts shows that the unique selector traverses first the gadget for edge $(0,1)$ in the "only vertex 1 chosen" mode and then the gadget for edge $(1,2)$ in the same mode, visiting every one of the 25 target vertices exactly once. Extracting the selector-adjacent path endpoints returns the source cover $(#dmvc_hc_sol.source_config.map(str).join(", ")) = {1}$, which indeed covers both source edges #sym.checkmark + *Step 3 -- Verify a witness.* The stored Hamiltonian circuit is $(#fmt-values(dmvc_hc_sol.target_config))$. Reading the cycle between selector contacts shows that the unique selector traverses first the gadget for edge $(0,1)$ in the "only vertex 1 chosen" mode and then the gadget for edge $(1,2)$ in the same mode, visiting every one of the 25 target vertices exactly once. Extracting the selector-adjacent path endpoints returns the source cover $(#fmt-values(dmvc_hc_sol.source_config)) = {1}$, which indeed covers both source edges #sym.checkmark *Multiplicity:* The fixture stores one canonical Hamiltonian circuit. Rotating or reversing that same cycle yields equivalent target witnesses with the same extracted cover. ], @@ -17448,17 +17280,17 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_mvc.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_mvc) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_mvc_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_mvc_sol.source_config), ) *Step 1 -- Source instance.* The 3-SAT formula has $n = #ksat_mvc.source.instance.num_vars$ variables and $m = #sat-num-clauses(ksat_mvc.source.instance)$ clauses: #{ksat_mvc.source.instance.clauses.enumerate().map(((j, c)) => { let lits = c.literals.map(l => if l > 0 { $x_#l$ } else { $overline(x)_#calc.abs(l)$ }) [$c_#j = (#lits.join($or$))$] - }).join(", ")}. A satisfying assignment is $(#ksat_mvc_sol.source_config.map(str).join(", "))$, i.e.\ #{range(ksat_mvc.source.instance.num_vars).map(i => { + }).join(", ")}. A satisfying assignment is $(#fmt-values(ksat_mvc_sol.source_config))$, i.e.\ #{range(ksat_mvc.source.instance.num_vars).map(i => { let v = ksat_mvc_sol.source_config.at(i) - if v == 1 { $x_#(i+1) = 1$ } else { $x_#(i+1) = 0$ } + if v { $x_#(i+1) = 1$ } else { $x_#(i+1) = 0$ } }).join(", ")}. *Step 2 -- Truth-setting edges.* For each variable $x_i$, create vertices $u_i$ (index $2(i-1)$) and $overline(u)_i$ (index $2(i-1)+1$) connected by a truth-setting edge. This gives $2n = #(2 * ksat_mvc.source.instance.num_vars)$ literal vertices and $n = #ksat_mvc.source.instance.num_vars$ edges. @@ -17467,7 +17299,7 @@ The following table shows concrete variable overhead for example instances, take *Step 4 -- Target graph dimensions.* The resulting graph has $|V| = 2n + 3m = #ksat_mvc.target.instance.graph.num_vertices$ vertices and $|E| = n + 6m = #ksat_mvc.target.instance.graph.edges.len()$ edges, with unit weights. - *Step 5 -- Verify a solution.* The satisfying assignment $(#ksat_mvc_sol.source_config.map(str).join(", "))$ maps to a vertex cover of size $n + 2m = #(ksat_mvc.source.instance.num_vars + 2 * sat-num-clauses(ksat_mvc.source.instance))$. The target configuration is $(#ksat_mvc_sol.target_config.map(str).join(", "))$: the cover selects #ksat_mvc_sol.target_config.filter(x => x == 1).len() vertices. For each truth-setting edge, exactly one endpoint is in the cover #sym.checkmark. For each clause triangle, exactly two of three vertices are covered #sym.checkmark. Each communication edge has at least one endpoint in the cover #sym.checkmark. + *Step 5 -- Verify a solution.* The satisfying assignment $(#fmt-values(ksat_mvc_sol.source_config))$ maps to a vertex cover of size $n + 2m = #(ksat_mvc.source.instance.num_vars + 2 * sat-num-clauses(ksat_mvc.source.instance))$. The target configuration is $(#fmt-values(ksat_mvc_sol.target_config))$: the cover selects #ksat_mvc_sol.target_config.filter(x => x).len() vertices. For each truth-setting edge, exactly one endpoint is in the cover #sym.checkmark. For each clause triangle, exactly two of three vertices are covered #sym.checkmark. Each communication edge has at least one endpoint in the cover #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. Other valid covers correspond to different satisfying assignments of the formula. ], @@ -17485,31 +17317,41 @@ The following table shows concrete variable overhead for example instances, take #let ksat_mono_sol = ksat_mono.solutions.at(0) #reduction-rule("KSatisfiability", "MonochromaticTriangle", example: true, - example-caption: [Single-clause 3-SAT instance ($n = #ksat_mono.source.instance.num_vars$, $m = #sat-num-clauses(ksat_mono.source.instance)$) reduced to a 4-triangle graph], + example-caption: [3-SAT encoded by NAE triangles and equality senders], extra: [ - #pred-commands( - "pred create --example " + problem-spec(ksat_mono.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_mono) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_mono_sol.source_config.map(str).join(","), - ) + The single-clause example has $n=3$, $m=1$, five logical variables including the sentinel and splitting variable, and seven NAE triples. Its graph has $#graph-num-vertices(ksat_mono.target.instance)$ vertices, $#graph-num-edges(ksat_mono.target.instance)$ edges and $#ksat_mono.target.instance.triangles.len()$ triangles. The stored target witness decodes to $(#fmt-values(ksat_mono_sol.source_config))$ by normalizing variable-signal colours against the sentinel colour. + ], +)[ + Use the signal-sender framework of @burrErdosLovasz1976Ramsey with the explicit seven-vertex equality sender proved below. The construction has linear graph size and preserves satisfiability in both directions. Its extraction works for every valid target colouring. +][ + _Equality sender._ Take two disjoint signal edges $(a,b)$ and $(c,d)$ and three private vertices $u,v,w$. Form the union of the two complete graphs on ${a,b,u,v,w}$ and ${c,d,u,v,w}$. The sender has seven vertices, seventeen edges and nineteen triangles. It contains no edge between the two signal edges. - *Step 1 -- Source instance.* The fixture uses the single clause $c_1 = (x_1 or x_2 or x_3)$. The extracted satisfying assignment is $(#ksat_mono_sol.source_config.map(str).join(", "))$. + In every colouring of $K_5$ without a monochromatic triangle, each vertex has exactly two incident edges of each colour: three neighbours of one colour would have all three mutual edges of the other colour. Each colour graph is consequently a five-cycle. For either colour, let $r$ be its edge count in the private triangle and $t$ its edge count in the complementary signal edge. The private vertices have total degree six in that colour, so the number of crossing edges is $6-2r$. Counting the five edges of that colour gives $5=r+(6-2r)+t$, hence $t=r-1$. Thus the signal edge has the majority colour of the private triangle. Applied to both cliques, this forces the signal edges to have equal colours. - *Step 2 -- Build the clause gadget.* Create literal vertices $p_1, p_2, p_3, n_1, n_2, n_3$ together with negation edges $(p_1, n_1)$, $(p_2, n_2)$, and $(p_3, n_3)$. For the clause, add intermediates $m_12, m_13, m_23$ and the six fan edges $(p_1, m_12)$, $(p_2, m_12)$, $(p_1, m_13)$, $(p_3, m_13)$, $(p_2, m_23)$, $(p_3, m_23)$, plus the clause triangle on $(m_12, m_13, m_23)$. The target therefore has $|V| = #graph-num-vertices(ksat_mono.target.instance)$ vertices, $|E| = #graph-num-edges(ksat_mono.target.instance)$ edges, and $#ksat_mono.target.instance.triangles.len()$ triangles. + Conversely, either equal signal colouring extends: make that colour the cycles $u,v,w,a,b,u$ and $u,v,w,c,d,u$ in the two cliques, and give all remaining edges the other colour. Both cycles agree on the private triangle, and each clique has no monochromatic triangle. No triangle crosses between the two signal edges. - *Step 3 -- Verify a witness.* The stored target coloring is $(#ksat_mono_sol.target_config.map(str).join(", "))$. Its first $n = #ksat_mono.source.instance.num_vars$ entries color the negation edges; reading those colors and applying the global color-swap symmetry fix yields the source assignment $(#ksat_mono_sol.source_config.map(str).join(", "))$, which satisfies $c_1$ #sym.checkmark. The same target coloring makes all four target triangles non-monochromatic #sym.checkmark. + _Construction._ Let the source have $n$ variables and $m$ clauses of length at most three; the typed API also admits shorter clauses. Let $q$ count its length-three clauses. - *Multiplicity:* The fixture stores one canonical target coloring. Swapping the two edge colors gives another valid witness and may flip the decoded assignment to its complement. - ], -)[ - This $O(n + m)$ reduction @garey1979[GT6] uses one positive/negative literal pair per variable and one three-vertex clause gadget per clause. The target graph has $2n + 3m$ vertices and $n + 9m$ edges. -][ - _Construction._ Let $phi = and_(j=1)^m (ell_1^j or ell_2^j or ell_3^j)$ be a 3-CNF formula on variables $x_1, dots, x_n$. For each variable $x_i$, create literal vertices $p_i$ and $n_i$ and add the negation edge $(p_i, n_i)$. For each clause $C_j$, map each literal $ell_r^j$ to its literal vertex $v_r^j$ ($x_i mapsto p_i$, $overline(x)_i mapsto n_i$). Introduce fresh intermediate vertices $m_12^j$, $m_13^j$, and $m_23^j$. Add fan edges $(v_1^j, m_12^j)$, $(v_2^j, m_12^j)$, $(v_1^j, m_13^j)$, $(v_3^j, m_13^j)$, $(v_2^j, m_23^j)$, and $(v_3^j, m_23^j)$, then connect the intermediates into the clause triangle $(m_12^j, m_13^j)$, $(m_12^j, m_23^j)$, $(m_13^j, m_23^j)$. Each clause gadget therefore contributes exactly four triangles: the clause triangle itself and the three fan triangles rooted at $v_1^j$, $v_2^j$, and $v_3^j$. + 1. Apply the existing typed embedding into Satisfiability and the existing SAT-to-NAE reduction. It adds a sentinel $s$, replacing each nonempty clause $C$ by $"NAE"(C,s)$ and an empty clause by $"NAE"(s,s)$. + 2. Replace $"NAE"(a,b)$ by $"NAE"(a,b,b)$; keep triples unchanged; replace $"NAE"(a,b,c,d)$ by $"NAE"(a,b,z)$ and $"NAE"(overline(z),c,d)$ with a fresh variable $z$. There are $N=n+1+q$ logical variables and $m+q$ resulting triples. + 3. Give every logical variable $i$ two disjoint signal edges $P_i,M_i$. Their $4N$ vertices are disjoint from all subsequent clause vertices. For each variable include the sign-consistency triple $"NAE"(P_i,P_i,M_i)$, followed by the logical triples, using $P_i$ for a positive literal and $M_i$ for a negative one. There are $T=N+m+q$ triples in total. + 4. Give every triple its own triangle. Connect its three edges to the three literal signals using three equality senders, each with fresh private vertices. The terminal edges already exist, so a sender adds three vertices and fifteen edges. Repeated literals use separate senders; no endpoint identification or case-specific repair is performed. + + _NAE identities._ The length-two identity holds because duplicating a value does not change whether all values agree. For the length-four identity, if $a=b$ the first triple forces $z=overline(a)$; if $c=d$ the second forces $z=c$. These requirements conflict exactly when all four values agree. If either pair differs, its corresponding triple imposes no condition on $z$. This proves the identity in both directions for all input patterns. + + _Triangle isolation._ Private vertices from different senders are nonadjacent. Any triangle containing a private sender vertex must lie in that sender: both other vertices belong to it, and the only edges among its four boundary vertices are its two terminal edges. This remains true after assembly because all variable signals and clause triangles have disjoint vertex sets, and senders add no boundary-to-boundary edges. A triangle containing no sender-private vertex is one of the clause triangles. Hence the target contains exactly the intended clause triangles and the nineteen triangles inside each sender, even with repeated or complementary literals and repeated variable occurrences. + + _Correctness._ ($arrow.r.double$) Extend a satisfying source assignment with sentinel false using the formal SAT-to-NAE construction, and select each auxiliary value by the NAE identity. Give $P_i$ the variable value and $M_i$ its complement. All sign-consistency and logical triples are not-all-equal. Colour each clause edge with its literal signal colour, and extend every equality sender as above. Triangle isolation proves that the complete graph has no monochromatic triangle. + + ($arrow.l.double$) In every valid target colouring, the sender forces each clause edge to equal its literal signal. Each sign-consistency triangle therefore enforces $"NAE"(P_i,P_i,M_i)$, so the negative signal is the complement of the positive one. Every logical triple satisfies its NAE relation. Eliminating the fresh splitting variables yields a valid assignment to the formal SAT-to-NAE target. Its extraction, which XORs the original variable values with the sentinel, satisfies the source formula. No trial assignment or source solve is needed. In particular, an empty source clause gives $"NAE"(s,s,s)$ and makes the target infeasible. - _Correctness._ ($arrow.r.double$) Fix a satisfying assignment of $phi$. Color each negation edge $(p_i, n_i)$ by the truth value of $x_i$, using color $0$ for true and color $1$ for false. For any clause, at least one of its literals is satisfied, and the four-triangle gadget has a 2-edge-coloring extending those literal choices with no monochromatic triangle; the implementation follows the verified local construction from issue #884 and colors each clause independently because the intermediates are clause-local. ($arrow.l.double$) Given a triangle-free coloring of the target graph, inspect the negation-edge colors. Either those colors, or their global complement after swapping the two colors, yields a satisfying assignment of the source formula under the same verified gadget analysis. + _Solution extraction._ Validate the target colouring and read the positive signal colours for the original variables and sentinel. Pass this assignment to the existing SAT-to-NAE extraction API. A global colour swap leaves the normalized source assignment unchanged. - _Solution extraction._ Read the negation-edge colors in variable order, setting $x_i = 1$ when $(p_i, n_i)$ has color $0$ and $x_i = 0$ otherwise. If that assignment does not satisfy $phi$, complement all bits; this accounts for the global color-swap symmetry of the target witness. + _Overhead._ The exact counts are $4N+12T$ vertices, $2N+48T$ edges and $58T$ triangles. Since $q<=m$, these are bounded by $16n+40m+16$, $50n+146m+50$ and $58n+174m+58$, respectively. All graph-size arithmetic is checked before allocation. With exactly three literals per clause the bounds are equalities. + + _YES example._ For the clause $(x_1 or x_2 or x_3)$, choose values $(1,0,0)$, sentinel $0$ and splitting variable $0$. Both logical triples evaluate to $"NAE"(1,0,0)$. Here $N=5$, $T=7$, giving 104 vertices, 346 edges and 406 triangles. The signal colours extend by the sender construction. + + _NO example._ On three variables, take all eight clauses obtained by choosing every combination of literal signs. Every assignment falsifies one clause. Here $N=12$, $T=28$, giving 384 vertices, 1368 edges and 1624 triangles. Any target colouring would decode to an assignment satisfying all eight clauses, a contradiction. For the empty formula on zero variables, the same uniform construction has $N=T=1$ and a valid colouring that decodes to the empty assignment. ] #let ksat_1in3 = load-example("KSatisfiability", "OneInThreeSatisfiability") @@ -17520,12 +17362,12 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_1in3.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_1in3) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_1in3_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_1in3_sol.source_config), ) - *Step 1 -- Source instance.* The source formula has one clause $c_1 = (x_1 or x_2 or x_3)$. The canonical satisfying assignment is $(#ksat_1in3_sol.source_config.map(str).join(", "))$, i.e.\ $x_1 = 0$, $x_2 = 0$, $x_3 = 1$. + *Step 1 -- Source instance.* The source formula has one clause $c_1 = (x_1 or x_2 or x_3)$. The canonical satisfying assignment is $(#fmt-values(ksat_1in3_sol.source_config))$, i.e.\ $x_1 = 0$, $x_2 = 0$, $x_3 = 1$. *Step 2 -- Add the global false literal.* Introduce $z_0$ (index 4) and $z_T$ (index 5), then add the forcing clause $R(z_0, z_0, z_T)$, where $R(u, v, w)$ means "exactly one of $u, v, w$ is true." In the stored witness, $(z_0, z_T) = (0, 1)$, which is the unique satisfying pattern of that clause. @@ -17540,28 +17382,28 @@ The following table shows concrete variable overhead for example instances, take In the exported target instance these become exactly the six clauses $(4, 4, 5)$, $(1, 6, 9)$, $(2, 7, 9)$, $(6, 7, 10)$, $(8, 9, 11)$, $(3, 8, 4)$. - *Step 4 -- Verify a witness.* The canonical target witness is $(#ksat_1in3_sol.target_config.map(str).join(", "))$. Reading off the auxiliaries gives $(a_1, b_1, c_1, d_1, e_1, f_1) = (0, 0, 0, 1, 1, 0)$. Every target clause has exactly one true literal #sym.checkmark, and restricting to the first three coordinates recovers $(0, 0, 1)$, which satisfies the original clause #sym.checkmark. + *Step 4 -- Verify a witness.* The canonical target witness is $(#fmt-values(ksat_1in3_sol.target_config))$. Reading off the auxiliaries gives $(a_1, b_1, c_1, d_1, e_1, f_1) = (0, 0, 0, 1, 1, 0)$. Every target clause has exactly one true literal #sym.checkmark, and restricting to the first three coordinates recovers $(0, 0, 1)$, which satisfies the original clause #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. This single-clause source formula has 7 satisfying assignments, and each satisfying literal pattern extends to at least one target witness by the gadget construction. ], )[ - This $O(n + m)$ reduction @schaefer1978 @garey1979[LO4] preserves the original variables, adds two global variables $z_0$ and $z_T$, and introduces six fresh auxiliaries per clause. Each 3-SAT clause $(ell_1 or ell_2 or ell_3)$ is replaced by a five-clause gadget of exact-one constraints. The target therefore has $n + 2 + 6m$ variables and $1 + 5m$ clauses. + Schaefer's Lemma 3.5 @schaefer1978 expresses a three-input disjunction by five exact-one constraints. The implementation compacts the $a$ appearing source variables, adds two global constant variables and six auxiliaries per clause. A source with $m$ clauses produces $a + 2 + 6m$ target variables and $1 + 5m$ target clauses, bounded by $n + 2 + 6m$ and $1 + 5m$ in the declared source parameters. ][ - _Construction._ Let $phi = and_(j=1)^m (ell_1^j or ell_2^j or ell_3^j)$ be a 3-CNF formula on variables $x_1, dots, x_n$. Introduce global variables $z_0$ and $z_T$, and add the clause $R(z_0, z_0, z_T)$, where $R(u, v, w)$ means that exactly one of the three literals is true. This forces $z_0 = 0$ and $z_T = 1$. For every source clause $C_j = (ell_1^j or ell_2^j or ell_3^j)$, introduce six fresh auxiliaries $a_j, b_j, c_j, d_j, e_j, f_j$ and append the five target clauses + _Construction._ Let $J$ be the sorted original indices of the appearing variables. Replace each actual source literal by the corresponding compact variable with its sign preserved. Use the existing SAT variable allocator to allocate $z_0$ and $z_T$, followed by six fresh variables $a_j, b_j, c_j, d_j, e_j, f_j$ for each source clause. The global exact-one constraint $R(z_0,z_0,z_T)$ forces $(z_0,z_T)=(0,1)$. Each native source clause has zero through three positions; fill missing positions with $z_0$. This preserves its disjunction, including the empty clause's false value. For the resulting three literals $ell_1^j,ell_2^j,ell_3^j$, append $ - R(ell_1^j, a_j, d_j), quad - R(ell_2^j, b_j, d_j), quad - R(a_j, b_j, e_j), quad - R(c_j, d_j, f_j), quad - R(ell_3^j, c_j, z_0). + R(ell_1^j,a_j,d_j), quad R(ell_2^j,b_j,d_j), quad + R(a_j,b_j,e_j), quad R(c_j,d_j,f_j), quad R(ell_3^j,c_j,z_0). $ - Negated source literals are copied directly; no complement variables are needed because 1-in-3 SAT clauses in this codebase may contain negated literals. + Here $R$ counts literal occurrences, so its repeated $z_0$ in the global constraint is intentional. Negative literals remain permitted by the target model. Every clause uses this same construction; no empty-clause target case is needed. + + _Correctness._ ($arrow.r.double$) Given a satisfying source assignment, assign its appearing coordinates to the compact target variables and set the constants as above. Let $(x,y,z)$ be one padded clause's literal truth values. Set $d=(1-x)(1-y)$, $a=1-x-d$, $b=1-y-d$, $c=1-z$, $e=1-a-b$ and $f=z-d$. If $x=y=0$, satisfaction forces $z=1$, giving $(a,b,c,d,e,f)=(0,0,0,1,1,0)$. Otherwise $d=0$, $a=1-x$, $b=1-y$, $e=x+y-1$, and $f=z$; these and $c=1-z$ are Boolean because $x+y>=1$. Substitution gives $x+a+d=y+b+d=a+b+e=c+d+f=z+c=1$, satisfying all five constraints. Auxiliary variables are disjoint across clauses, so the assignments combine. - _Correctness._ ($arrow.r.double$) Suppose $phi$ is satisfiable. Set the first $n$ target variables according to any satisfying source assignment, and set $(z_0, z_T) = (0, 1)$. Consider one clause gadget. Because $ell_1^j or ell_2^j or ell_3^j = 1$, the truth triple $(ell_1^j, ell_2^j, ell_3^j)$ is one of the seven nonzero 0/1 patterns. For each such pattern there is a choice of $(a_j, b_j, c_j, d_j, e_j, f_j)$ satisfying all five exact-one clauses; the implementation uses the gadget directly and the worked example shows one such extension. Doing this independently for every clause yields a satisfying 1-in-3 assignment. + ($arrow.l.double$) Every target solution forces $z_0=0$. If a padded clause had $x=y=z=0$, the final constraint would force $c=1$; the fourth would then force $d=f=0$. The first two would force $a=b=1$, contradicting the third. Thus every padded disjunction is true, which implies every original clause is true. Repetition, opposite signs and missing positions do not change this argument. - ($arrow.l.double$) Suppose the target instance is satisfiable. The global clause forces $z_0 = 0$. Fix any clause gadget, and assume for contradiction that $ell_1^j = ell_2^j = ell_3^j = 0$. Then $R(ell_3^j, c_j, z_0)$ forces $c_j = 1$. Next $R(c_j, d_j, f_j)$ forces $d_j = f_j = 0$. Then $R(ell_1^j, a_j, d_j)$ and $R(ell_2^j, b_j, d_j)$ force $a_j = b_j = 1$. But now $R(a_j, b_j, e_j)$ has two true literals, impossible. Therefore every source clause has at least one true literal, so the restriction to the original variables satisfies $phi$. + _Solution extraction._ Validate the target assignment once using the formal evaluator and reject $"Or(false)"$. Initialize a length-$n$ false vector, then restore the compact source coordinates to their original positions in $J$. Unused source variables stay false. The resolved value types are $"Or" arrow.r "Or"$. + + _Native bounds._ Since $a<=3m$, at most $9m+2$ target variables are allocated. The size of a successfully allocated native clause vector bounds $m$; with 24-byte clauses on 64-bit Rust (at least 12 bytes on 32-bit), this count stays within the signed literal domain and the native index domain. Sparse original indices never determine auxiliary allocation positions. Clause capacity is checked, allocator failures retain the formal construction error type, and the target is built through its fallible constructor. Resource exhaustion is not mathematical infeasibility. - _Solution extraction._ Return the first $n$ target coordinates unchanged, discarding $z_0$, $z_T$, and all clause auxiliaries. ] #let ksat_d2cif = load-example("KSatisfiability", "DirectedTwoCommodityIntegralFlow") @@ -17572,16 +17414,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_d2cif.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_d2cif) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_d2cif_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_d2cif_sol.source_config), ) - *Step 1 -- Source instance.* The source formula has clauses $c_1 = (x_1 or overline(x)_2 or x_3)$ and $c_2 = (overline(x)_1 or x_2 or overline(x)_3)$. The canonical satisfying assignment is $(#ksat_d2cif_sol.source_config.map(str).join(", "))$, i.e.\ $x_1 = 1$, $x_2 = 1$, $x_3 = 0$. + *Step 1 -- Source instance.* The source formula has clauses $c_1 = (x_1 or overline(x)_2 or x_3)$ and $c_2 = (overline(x)_1 or x_2 or overline(x)_3)$. The canonical satisfying assignment is $(#fmt-values(ksat_d2cif_sol.source_config))$, i.e.\ $x_1 = 1$, $x_2 = 1$, $x_3 = 0$. *Step 2 -- Build the lobes and clause sinks.* Each variable appears once positively and once negatively, so each lobe contains an entry vertex, an exit vertex, one dummy segment on each branch, and one literal-occurrence segment on each branch. That is $10$ vertices and $14$ arcs per variable. Adding the $4$ terminals and $2$ clause vertices gives $|V| = #ksat_d2cif.target.instance.graph.num_vertices = 36$; adding the $4$ commodity-1 chain arcs and $2$ clause-to-sink arcs gives $|A| = #ksat_d2cif.target.instance.graph.arcs.len() = 48$. - *Step 3 -- Verify a witness.* Commodity 1 uses the lower branch in the lobes of $x_1$ and $x_2$ and the upper branch in the lobe of $x_3$, exactly matching the assignment $(1, 1, 0)$. Clause $c_1$ is satisfied by $x_1$, so commodity 2 routes one unit through the positive occurrence segment of $x_1$ into $d_1$. Clause $c_2$ is satisfied by $x_2$, so a second unit routes through the positive occurrence segment of $x_2$ into $d_2$. Both clause-to-sink arcs carry one unit, so the target meets $R_2 = #ksat_d2cif.target.instance.requirement_2 = 2$ #sym.checkmark. Reading back which lower-branch entry arcs commodity 1 used recovers $(#ksat_d2cif_sol.source_config.map(str).join(", "))$ #sym.checkmark + *Step 3 -- Verify a witness.* Commodity 1 uses the lower branch in the lobes of $x_1$ and $x_2$ and the upper branch in the lobe of $x_3$, exactly matching the assignment $(1, 1, 0)$. Clause $c_1$ is satisfied by $x_1$, so commodity 2 routes one unit through the positive occurrence segment of $x_1$ into $d_1$. Clause $c_2$ is satisfied by $x_2$, so a second unit routes through the positive occurrence segment of $x_2$ into $d_2$. Both clause-to-sink arcs carry one unit, so the target meets $R_2 = #ksat_d2cif.target.instance.requirement_2 = 2$ #sym.checkmark. Reading back which lower-branch entry arcs commodity 1 used recovers $(#fmt-values(ksat_d2cif_sol.source_config))$ #sym.checkmark *Multiplicity:* The fixture stores one canonical witness. This source formula has multiple satisfying assignments, and each satisfying clause can choose any satisfied literal occurrence when routing commodity 2. ], @@ -17600,13 +17442,19 @@ The following table shows concrete variable overhead for example instances, take #reduction-rule("KSatisfiability", "FeasibleRegisterAssignment", example: false, )[ - Sethi's Reduction 3 @sethi1975 @garey1979[PO2] builds a DAG with shared-register variable leaf pairs and $p\/q\/r\/overline(r)$ clause gadgets with cyclic links, plus a preassigned register allocation. The target has $2n + 12m$ vertices, $15m$ arcs, and $K = n + 9m$ registers. + Sethi's Reduction 3 @sethi1973 builds a DAG with shared-register variable leaf pairs and $p\/q\/r\/overline(r)$ clause gadgets. Nonempty short clauses are padded by repeating a literal; occurring variables are compacted. For $s$ declared source variables and $c$ clauses, the target has at most $2s+12c$ vertices, $15c$ arcs, $s+9c$ registers, and $s+3c$ pairs of vertices sharing a register. ][ - _Construction._ For each variable $x_k$, create two leaf nodes $s_k^+, s_k^-$ sharing register $S_k$. For each literal occurrence $Y_(i,j)$ in clause $C_i$, create four nodes $p_(i,j), q_(i,j), r_(i,j), overline(r)_(i,j)$ with internal arcs $q_(i,j) -> p_(i,j) -> r_(i,j)$ and cyclic links $q_(i,1) -> overline(r)_(i,2)$, $q_(i,2) -> overline(r)_(i,3)$, $q_(i,3) -> overline(r)_(i,1)$. Nodes $r_(i,j)$ and $overline(r)_(i,j)$ share register $R_(i,j)$. If $Y_(i,j) = x_k$: $r_(i,j) -> s_k^+$ and $overline(r)_(i,j) -> s_k^-$; if $Y_(i,j) = overline(x)_k$: swap the attachments. + _Native normalization._ If any clause is empty, return the three-vertex DAG with dependencies $(2,0),(2,1)$ and register assignment $(0,0,1)$. Both predecessors must stay live until vertex 2, so neither can overwrite the other's register; this target is infeasible. Otherwise collect appearing source indices in increasing order and store their inverse map. Pad each nonempty clause to three literal occurrences by repeating its first literal. Repetition preserves a disjunction, and each occurrence has its own gadget and register pair, so no distinct-variable assumption is needed. An empty conjunction yields the empty DAG and empty realization. + + _Construction._ Let $a$ be the number of appearing variables. For each compact variable $x_k$, create leaves $s_k^+,s_k^-$ sharing register $S_k$. For every clause position $(i,j)$ create $p_(i,j),q_(i,j),r_(i,j),overline(r)_(i,j)$. The $p$ and $q$ registers are unique, while $r$ and $overline(r)$ share a fresh register $R_(i,j)$. Arcs point from a consumer to its dependency: $q_(i,j) -> p_(i,j) -> r_(i,j)$ and $q_(i,j) -> overline(r)_(i,j+1)$, with positions cyclic modulo three. For positive literal $x_k$, add $r_(i,j) -> s_k^+$ and $overline(r)_(i,j) -> s_k^-$; reverse these attachments for a negative literal. This DAG has four levels: leaves, $r/overline(r)$, $p$, and $q$. + + _Forward correctness._ Follow Sethi's Lemma 5.5 @sethi1973. For each variable, execute its true-literal leaf, all direct consumers of that leaf, and then its opposite leaf. Every old leaf value is consumed before the shared register is overwritten; exactly one of each $r/overline(r)$ pair has executed. For every occurrence whose $r$ has executed, execute its $p$ and then $overline(r)$, consuming $r$ before overwriting its register. Now all $overline(r)$ vertices are available. In each clause choose a true occurrence $j$. Its $p_j$ is available, so execute $q_j$. This consumes $overline(r)_(j+1)$ and releases that register; if $r_(j+1)$ is missing, execute it and its $p$, then execute $q_(j+1)$. Continue around all three positions. Every dependency is met and every register overwrite follows the last use of its former value. Occurrence registers are disjoint across clauses, so these schedules combine. - _Correctness._ ($arrow.r.double$) If $phi$ is satisfiable, place the truth-selected leaf first for each variable, then unlock each clause gadget starting from a satisfied literal. ($arrow.l.double$) The cyclic links force at least one position $j$ per clause where $r_(i,j)$ appears before $overline(r)_(i,j)$; shared-register order transfer forces the corresponding literal leaf to appear first, encoding a true literal. + _Backward correctness._ Lemma 5.7 transfers relative order between two shared-register pairs: if $u_1$ depends on $v_1$, $u_2$ on $v_2$, and each pair shares a register, then $u_1 < u_2$ iff $v_1 < v_2$ in every realization. A reversed order would overwrite a predecessor while its consumer is still pending. Apply this with $u_1=r$, $u_2=overline(r)$, and the two attached literal leaves. If every position in a clause had $overline(r)_j < r_j$, its register conflict would force $q_(j-1) < r_j$, whereas dependencies force $r_j < p_j < q_j$. Combining these inequalities cyclically is impossible. Thus at least one $r_j < overline(r)_j$, and order transfer makes its literal true. Every source clause is satisfied after undoing repetition and compaction. - _Solution extraction._ For each variable $x_k$, set $tau(x_k) = 1$ iff $s_k^+$ appears before $s_k^-$ in the realization. + _Extraction._ Validate the complete position vector and reject $"Or(false)"$. Set each appearing source variable true iff its compact positive leaf occurs before its negative leaf. Restore original indices through the inverse map and assign false to unused variables. The target model's permitted last-use overwrite does not weaken the order-transfer proof: neither member of a shared pair is the other member's consumer in this construction. + + _Bounds and native arithmetic._ For the ordinary construction $a <= min(s,3c)$, so the displayed upper bounds follow from the exact counts $2a+12c$, $15c$, $a+9c$, and $a+3c$. The fixed NO target has counts $(3,2,2,1)$ and also satisfies the bounds whenever an empty clause exists; the empty YES target has all counts zero. Let $I="isize::MAX"$. A native source clause stores a vector object of 24 bytes on 64-bit or 12 bytes on 32-bit, so successful allocation of its outer vector bounds $c$ by $I/24$ or $I/12$. Therefore $2a+12c <= 18c$, $a+9c <= 12c$, and $15c$ fit $"usize"$ before target allocation on both architectures. Once the target register-assignment vector is allocated, every indexed vertex is within that vector. All same-register groups have size two except the fixed target's singleton, so pair counting introduces no large product. Checked construction arithmetic is retained; allocation and solver limits are not infeasibility proofs. ] #let ksat_rs = load-example("KSatisfiability", "RegisterSufficiency") @@ -17617,29 +17465,38 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_rs.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_rs) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_rs_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_rs_sol.source_config), ) - *Step 1 -- Source instance.* The canonical source formula is $phi = (x_1 or overline(x)_2 or x_3) and (overline(x)_1 or x_2 or overline(x)_3)$. The stored satisfying assignment is $(#ksat_rs_sol.source_config.map(str).join(", "))$. + *Step 1 -- Source instance.* The canonical source formula is $phi = (x_1 or overline(x)_2 or x_3) and (overline(x)_1 or x_2 or overline(x)_3)$. The stored satisfying assignment is $(#fmt-values(ksat_rs_sol.source_config))$. *Step 2 -- Build Sethi's DAG.* Here $n = #ksat_rs.source.instance.num_vars$, $m = #sat-num-clauses(ksat_rs.source.instance)$, and $b = max(0, 2n - m) = 4$. The construction creates $|A| = 2n + 1 = 7$, $|B| = b = 4$, $|C| = m = 2$, $|F| = 3m = 6$, $|M| = 3$, $|R| = n(n+1) = 12$, $|S| = |T| = n^2 = 9$, $|U| = 2n = 6$, $|W| = |Z| = n = 3$, and $|X| = 2n = 6$, for a total of $|V| = #ksat_rs.target.instance.num_vertices = 70$ vertices. The target bound is $K = #ksat_rs.target.instance.bound = 23$, and the construction emits $|A| = #ksat_rs.target.instance.arcs.len() = 152$ arcs. For clause $C_1$, the six literal-lock arcs are $(x_1^+, f_(1,1))$, $(x_2^-, f_(1,2))$, $(x_3^+, f_(1,3))$, $(x_1^-, f_(1,2))$, $(x_1^-, f_(1,3))$, and $(x_2^+, f_(1,3))$. - *Step 3 -- Verify extraction at $w_n$.* The target witness is a computation ordering on $70$ vertices. Reading the prefix ending at $w_3$ marks exactly those variables whose $x_k^+$ node has already been computed, which reconstructs $(#ksat_rs_sol.source_config.map(str).join(", "))$ #sym.checkmark. Evaluating the original 3-SAT formula under that assignment returns true #sym.checkmark + *Step 3 -- Verify extraction at $w_n$.* The target witness is a computation ordering on $70$ vertices. Reading the prefix ending at $w_3$ marks exactly those variables whose $x_k^+$ node has already been computed, which reconstructs $(#fmt-values(ksat_rs_sol.source_config))$ #sym.checkmark. Evaluating the original 3-SAT formula under that assignment returns true #sym.checkmark *Multiplicity:* The fixture stores one canonical satisfying assignment. Different satisfying assignments can induce different valid computation orders in the target DAG. ], )[ - Sethi's Reduction I / Theorem 3.11 @sethi1975 @garey1979[PO1] builds a dependency DAG whose register pressure mirrors a literal-assignment phase followed by a clause-verification phase. For a 3-CNF formula with $n$ variables, $m$ clauses, and $b = max(0, 2n - m)$, the target has $3n^2 + 9n + 4m + b + 4$ vertices, $6n^2 + 19n + 16m + 2b + 1$ arcs, and register bound $K = 3m + 4n + 1 + b$. + Sethi's Reduction 1 and Theorem 3.11 @sethi1973 encode literal choices through register pressure. We extend the construction to the native zero-to-three-literal clause domain by truth-preserving normalization and explicit empty-formula boundaries. The original padding $b=max(2n-m,0)$ is retained. ][ - _Construction._ Let $phi = and_(i=1)^m C_i$ be a 3-CNF formula over variables $x_1, dots, x_n$, where $C_i = (Y_(i,1) or Y_(i,2) or Y_(i,3))$. Define $b = max(0, 2n - m)$. Create node families $A = {a_j : 1 <= j <= 2n+1}$, $B = {b_j : 1 <= j <= b}$, $C = {c_i : 1 <= i <= m}$, $F = {f_(i,j) : 1 <= i <= m, 1 <= j <= 3}$, $M = {"initial", d, "final"}$, $R = {r_(k,j) : 1 <= k <= n, 1 <= j <= 2n-2k+2}$, $S = {s_(k,j) : 1 <= k <= n, 1 <= j <= 2n-2k+1}$, $T = {t_(k,j) : 1 <= k <= n, 1 <= j <= 2n-2k+1}$, $U = {u_(k,1), u_(k,2) : 1 <= k <= n}$, $W = {w_k : 1 <= k <= n}$, $X = {x_k^+, x_k^- : 1 <= k <= n}$, and $Z = {z_k : 1 <= k <= n}$. Add the ten arc families from Sethi's theorem exactly as stated in the issue: $"initial"$ depends on every node in $A union B union F union U$, every node in $C union R union S union T union W$ depends on $"initial"$, $"final"$ depends on $W union X union Z union {"initial", d}$, each variable gadget links $x_k^+$ and $x_k^-$ to $z_k$, $u_(k,1)$, $u_(k,2)$, $s_(k,*)$, $t_(k,*)$, $r_(k,*)$, and each clause gadget links $c_i$ to $w_n$, $z_n$, its three $f_(i,j)$ nodes, and the literal-lock edges determined by whether $Y_(i,j)$ is positive or negative. + _Normalization._ A formula containing an empty clause is false; output a single output vertex with register bound zero. An empty conjunction is true; output the empty graph with bound zero. In every other case collect the appearing variables in increasing original-index order, map them to $1,dots,n$, and retain the inverse map. Keep the original clause order and literal occurrences. Repeat the first literal of each nonempty short clause until it has three positions. This preserves disjunction truth, gives $n,m>=1$, and never introduces a fictitious vertex for a missing literal. Let $Y_(i,j)$ denote the literal at position $j$ of clause $i$, allowing repeated literals. + + _DAG construction._ Put $b=max(2n-m,0)$ and $K=3m+4n+1+b$. Arcs $(v,u)$ mean that $v$ depends on $u$. Create the following disjoint families, with one-based subscripts: + $ |A|=2n+1, quad |B|=b, quad |C|=m, quad F={f_(i,j):1<=i<=m,1<=j<=3}, $ + $ M={"initial",d,"final"}, quad R_k={r_(k,j):1<=j<=2n-2k+2}, $ + $ S_k={s_(k,j):1<=j<=2n-2k+1}, quad T_k={t_(k,j):1<=j<=2n-2k+1}, $ + $ U_k={u_(k,1),u_(k,2)}, quad W={w_k}, quad X={x_k^+,x_k^-}, quad Z={z_k}, quad 1<=k<=n. $ + Add arcs from `initial` to $A union B union F union U$, from every $C,R,S,T,W$ vertex to `initial`, and from `final` to $W union X union Z union {"initial",d}$. For each $k$, $x_k^+$ depends on $z_k,u_(k,1),S_k$; $x_k^-$ depends on $z_k,u_(k,2),T_k$; $w_k$ depends on both $U_k$ vertices; and $z_k$ depends on $R_k$. For $k>=2$, $z_k$ also depends on $w_(k-1),z_(k-1)$. Each $c_i$ depends on $w_n,z_n$ and all three $f_(i,j)$; $d$ depends on $B union C$. Finally, $Y_(i,j)$ denotes its corresponding positive or negative $X$ vertex: add $(Y_(i,j),f_(i,j))$ for every position and $(overline(Y)_(i,j),f_(i,l))$ whenever $j=2n-1$ registers, enough to finish all remaining auxiliary groups. The last-use overwrite rule agrees with Sethi's Game 1. - _Correctness._ ($arrow.r.double$) Suppose $phi$ is satisfiable under assignment $tau$. Execute Sethi's eight-stage schedule. In the variable phase, the chain gadgets force a choice between the positive and negative side of each variable, and the schedule can be arranged so that by the moment $w_n$ is computed, $x_k^+$ has appeared iff $tau(x_k) = 1$. Because every clause has a satisfied literal, the corresponding $f_(i,j)$ node is unlocked during the clause phase, and the lock edges from the opposite literals prevent incompatible clause traversals. Sethi proves that this entire computation uses at most $K$ registers, so the target Register Sufficiency instance is feasible. + _Arbitrary-order variable invariant._ Auxiliary vertices in $R_k,S_k,T_k$ have the held `initial` value as their only predecessor and their designated parent as their only consumer. Delaying such a vertex until immediately before its parent preserves all parent orders and cannot increase the peak live set. Thus arbitrary bounded computations admit this grouped form for the counting argument. Before any clause executes, all $F,B$ values and `initial` remain held. Just after `initial`, $2n$ registers are free. The $R_1$ group needs all of them, preventing any premature $w$ vertex. Inductively, just after $z_k$, there are $2n-2k+1$ free registers. Computing one of $x_k^+,x_k^-$ consumes one permanently until its $U$ value can be released; the opposite auxiliary group then no longer fits. Computing $w_k$ first also consumes that one free register and permits neither literal. After $w_k$, exactly $2n-2k$ registers are free, all needed by $R_(k+1)$; older unchosen literal groups require more. Therefore, at the snapshot immediately after $w_n$, at most one literal per variable has executed and no register is free. This does not require every variable to have a selected literal. - ($arrow.l.double$) Suppose the target instance has a computation using at most $K$ registers. Stop immediately after $w_n$ is computed. At that snapshot, for each variable gadget, at most one of $x_k^+$ and $x_k^-$ has been computed. Define $tau(x_k) = 1$ iff $x_k^+$ has already been computed. Now assume some clause $C_i$ is false under $tau$. Then every literal $Y_(i,j)$ is false, so the corresponding literal node has not yet been computed by the $w_n$ snapshot. Consequently each $f_(i,j)$ still has an uncomputed literal predecessor. Sethi's clause-phase invariant leaves no free register between the computation of $w_n$ and the later computation of $d$, so such a clause node $c_i$ could not be discharged without exceeding $K$, contradiction. Therefore every clause contains a true literal under $tau$, and $phi$ is satisfiable. + _Clause locks and backward direction._ Before $d$, a completed $c_i$ retains its value. Its $F$ value at position $j$ can be released only if $Y_(i,j)$ and all earlier opposite literals have executed. Two released positions $j` source and vertex-to-position `Vec` target solutions. The ordinary target has $3n^2+9n+4m+b+4$ vertices, $6n^2+19n+16m+2b+1$ arcs, and one sink. For original counts $s,c$, upper bounds are $3s^2+11s+4c+4$, $6s^2+23s+16c+1$, $6s+3c+1$ registers, and one sink; both fixed targets also obey them. Layout squares, totals, register bound and arc capacity are checked before allocation; subsequent nonnegative offsets are bounded by those totals. Allocation and solver limits are reported as resource failures, not infeasibility. No endpoint or variant changes. ] #reduction-rule("FeasibleRegisterAssignment", "ILP", @@ -17670,20 +17527,20 @@ The following table shows concrete variable overhead for example instances, take #let mvc_mfas = load-example("MinimumVertexCover", "MinimumFeedbackArcSet") #let mvc_mfas_sol = mvc_mfas.solutions.at(0) -#let mvc_mfas_cover = mvc_mfas_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) -#let mvc_mfas_fas = mvc_mfas_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let mvc_mfas_cover = mvc_mfas_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) +#let mvc_mfas_fas = mvc_mfas_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #reduction-rule("MinimumVertexCover", "MinimumFeedbackArcSet", example: true, example-caption: [Triangle graph ($n = #graph-num-vertices(mvc_mfas.source.instance)$, $|E| = #graph-num-edges(mvc_mfas.source.instance)$): VC $arrow.r$ FAS via vertex splitting], extra: [ #pred-commands( "pred create --example " + problem-spec(mvc_mfas.source) + " -o mvc.json", - "pred reduce mvc.json --to " + target-spec(mvc_mfas) + " -o bundle.json", + "pred reduce mvc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mvc.json --config " + mvc_mfas_sol.source_config.map(str).join(","), + "pred evaluate mvc.json --config " + cli-config(mvc_mfas_sol.source_config), ) - *Step 1 -- Source instance.* The source graph $G$ has $n = #graph-num-vertices(mvc_mfas.source.instance)$ vertices and $|E| = #graph-num-edges(mvc_mfas.source.instance)$ edges: $E = {#mvc_mfas.source.instance.graph.edges.map(e => "(" + e.map(str).join(", ") + ")").join(", ")}$ with weights $bold(w) = (#mvc_mfas.source.instance.weights.map(str).join(", "))$. + *Step 1 -- Source instance.* The source graph $G$ has $n = #graph-num-vertices(mvc_mfas.source.instance)$ vertices and $|E| = #graph-num-edges(mvc_mfas.source.instance)$ edges: $E = {#mvc_mfas.source.instance.graph.edges.map(e => "(" + fmt-values(e) + ")").join(", ")}$ with weights $bold(w) = (#fmt-values(mvc_mfas.source.instance.weights))$. *Step 2 -- Construction.* Each vertex $v$ splits into $v^"in"$ and $v^"out"$, yielding $2n = #mvc_mfas.target.instance.graph.num_vertices$ nodes in the target digraph $H$. Internal arcs $(v^"in", v^"out")$ carry weight $w(v)$: #{ let arcs = mvc_mfas.target.instance.graph.arcs @@ -17697,7 +17554,7 @@ The following table shows concrete variable overhead for example instances, take arcs.enumerate().filter(((i, a)) => i >= n).map(((i, a)) => "(" + str(a.at(0)) + ", " + str(a.at(1)) + ") w=" + str(ws.at(i))).join(", ") }. Total: #mvc_mfas.target.instance.graph.arcs.len() arcs ($#mvc_mfas.source.instance.graph.num_vertices$ internal + $#(mvc_mfas.target.instance.graph.arcs.len() - mvc_mfas.source.instance.graph.num_vertices)$ crossing). - *Step 3 -- Verify a solution.* The source cover is $C = {#mvc_mfas_cover.map(str).join(", ")}$ (size #mvc_mfas_cover.len()). The target FAS selects arcs at indices ${#mvc_mfas_fas.map(str).join(", ")}$, which are the internal arcs #{ + *Step 3 -- Verify a solution.* The source cover is $C = {#fmt-values(mvc_mfas_cover)}$ (size #mvc_mfas_cover.len()). The target FAS selects arcs at indices ${#fmt-values(mvc_mfas_fas)}$, which are the internal arcs #{ let arcs = mvc_mfas.target.instance.graph.arcs mvc_mfas_fas.map(i => "(" + str(arcs.at(i).at(0)) + ", " + str(arcs.at(i).at(1)) + ")").join(", ") } -- exactly the internal arcs of cover vertices $v^"in" arrow.r v^"out"$ for $v in C$. No crossing arc (weight $M = #mvc_mfas.target.instance.weights.slice(mvc_mfas.source.instance.graph.num_vertices).at(0)$) is selected, confirming the optimal FAS uses only internal arcs #sym.checkmark @@ -17722,9 +17579,9 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_kc.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_kc) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_kc_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_kc_sol.source_config), ) *Step 1 -- Source instance.* The 3-CNF formula $phi$ has $m = #ksat_kc.source.instance.clauses.len()$ clauses over $n = #ksat_kc.source.instance.num_vars$ variables: @@ -17735,22 +17592,20 @@ The following table shows concrete variable overhead for example instances, take lits.join($or$) }).join($) and ($)) $ - *Step 2 -- Construct the conflict graph.* Create one vertex per literal position: vertex $3j + p$ represents position $p$ ($0$-indexed) in clause $j$, giving $|V| = 3 dot #ksat_kc.source.instance.clauses.len() = #ksat_kc.target.instance.graph.num_vertices$ vertices. Connect $(j_1, p_1)$ and $(j_2, p_2)$ whenever $j_1 != j_2$ and the two literals are not contradictory. The resulting graph has $|E| = #ksat_kc.target.instance.graph.edges.len()$ edges: $E = {#ksat_kc.target.instance.graph.edges.map(e => "(" + e.map(str).join(", ") + ")").join(", ")}$. Set $k = m = #ksat_kc.target.instance.k$. + *Step 2 -- Construct the compatibility graph.* Enumerate actual literal occurrences in clause order. For this fixture there are six occurrence vertices and anchor $6$. Connect compatible occurrences from different clauses, then connect the anchor to all six occurrences. The target has $#ksat_kc.target.instance.graph.num_vertices$ vertices, $#ksat_kc.target.instance.graph.edges.len()$ edges, and threshold $k=m+1=#ksat_kc.target.instance.k$. - *Step 3 -- Verify a solution.* The satisfying assignment $(x_1, x_2, x_3) = (#ksat_kc_sol.source_config.map(str).join(", "))$ makes literal $x_3$ true in clause 0 (position 2, vertex 2) and literal $overline(x_1)$ true in clause 1 (position 0, vertex 3). The target configuration $bold(x) = (#ksat_kc_sol.target_config.map(str).join(", "))$ selects vertices #{ - ksat_kc_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(" and ") - }, which form a clique (edge $(2, 3) in E$ #sym.checkmark) of size $k = #ksat_kc.target.instance.k$ spanning both clause groups #sym.checkmark + *Step 3 -- Verify a solution.* The assignment $(#fmt-values(ksat_kc_sol.source_config))$ makes occurrence $2$ ($x_3$) and occurrence $3$ ($overline(x_1)$) true. Together with anchor $6$, these form a triangle. The stored target vector is $(#fmt-values(ksat_kc_sol.target_config))$; decoding its literal vertices returns the stored source assignment #sym.checkmark *Multiplicity:* The fixture stores one canonical witness. The formula has multiple satisfying assignments; each induces at least one $k$-clique by choosing one true literal per clause. ], )[ - Assign one vertex per literal position $(j, p)$; connect vertices from different clauses whose literals are not contradictory. A $k$-clique selects one consistent true literal per clause. + Karp's literal-compatibility construction @karp1972 is extended with an anchor and, when needed, isolated padding. This keeps the same endpoint and handles both empty formulas and the shorter clauses accepted by the native `new_allow_less` API. ][ - _Construction._ Given 3-CNF $phi = C_1 and dots.c and C_m$ over $n$ variables, construct $G = (V, E)$ with $V = {(j, p) : 1 <= j <= m, 1 <= p <= 3}$, $|V| = 3m$. Edge between $(j_1, p_1)$ and $(j_2, p_2)$ iff $j_1 != j_2$ and $ell_(j_1, p_1) != not ell_(j_2, p_2)$. Set $k = m$. + _Construction._ Let $m$ be the clause count and $t=sum_j |C_j|$ the actual occurrence count. Number literal vertices in clause/position order, with stored variable indices and polarities. Set $N=max(t,m)+1$, $k=m+1$, and anchor $a=t$. Connect two literal vertices iff they come from different clauses and are not opposite polarities of the same variable; then connect $a$ to all literal vertices. Any remaining vertices are isolated. Thus $1<=k<=N$, $N<=3m+1$, and the edge count is at most $9m^2+3m$. - _Correctness._ ($arrow.r.double$) A satisfying assignment picks one true literal per clause; these vertices form a clique since they span all clauses without contradiction. ($arrow.l.double$) A $k$-clique has exactly one vertex per clause group; the selected literals are pairwise non-contradictory, defining a satisfying assignment. + _Correctness._ A satisfying assignment selects a true occurrence from each clause; these mutually compatible vertices and the anchor form a $k$-clique. Conversely, the literal subgraph contains at most one vertex per clause in a clique, hence has clique number at most $m$. For $m>0$, any clique of size at least $k>=2$ excludes padding isolates and must contain the anchor plus one occurrence from every clause. Their polarities consistently satisfy all clauses. An empty clause prevents such a clique. For $m=0$, the target is a single anchor with $k=1$, and every source assignment satisfies the empty conjunction. - _Solution extraction._ For selected vertex $v$: clause $j = floor(v / 3)$, position $p = v mod 3$. If literal $ell_(j,p) = x_i$ set $x_i = 1$; if $ell_(j,p) = not x_i$ set $x_i = 0$. + _Solution extraction._ Validate the complete target selection and its true clique predicate first. Decode the literal vertices via their stored source indices and signs, leaving unselected variables false; ignore the nonliteral anchor and padding by the defined mapping. Checked occurrence and output counts precede allocation. Formal `CNFClause::variables` supplies validated indices. Normal CLI/serde still requires exactly three literals per actual clause; native short-clause support does not change variants or serialization. ] #let ksat_co = load-example("KSatisfiability", "CyclicOrdering") @@ -17761,35 +17616,40 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_co.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_co) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_co_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_co_sol.source_config), ) - *Step 1 -- Source instance.* The canonical formula is $phi = (x_1 or x_2 or x_3)$ with $n = #ksat_co.source.instance.num_vars$ variables and $m = #ksat_co.source.instance.clauses.len()$ clause. The stored satisfying assignment is $(#ksat_co_sol.source_config.map(str).join(", "))$, so every literal in the clause is true. + *Step 1 -- Source instance.* The canonical formula is $phi = (x_1 or x_2 or x_3)$ with $n = #ksat_co.source.instance.num_vars$ variables and $m = #ksat_co.source.instance.clauses.len()$ clause. The stored satisfying assignment is $(#fmt-values(ksat_co_sol.source_config))$, so every literal in the clause is true. *Step 2 -- Create variable and clause elements.* The reduction introduces three variable elements per Boolean variable, giving variable triples $(alpha_1, beta_1, gamma_1) = (0, 1, 2)$, $(alpha_2, beta_2, gamma_2) = (3, 4, 5)$, and $(alpha_3, beta_3, gamma_3) = (6, 7, 8)$. The single clause adds five auxiliary elements $(j, k, l, m, n) = (9, 10, 11, 12, 13)$, so the target has $3n + 5m = #ksat_co.target.instance.num_elements$ elements total. - *Step 3 -- Emit the ten cyclic-ordering triples.* Because the clause literals are $(x_1, x_2, x_3)$, the literal-orientation triples are the forward variable triples. The target constraints are exactly #{ksat_co.target.instance.triples.map(t => "(" + t.map(str).join(", ") + ")").join(", ")}, so $|Delta| = #ksat_co.target.instance.triples.len() = 10m$. + *Step 3 -- Emit the ten cyclic-ordering triples.* Because the clause literals are $(x_1, x_2, x_3)$, the literal-orientation triples are the forward variable triples. The target constraints are exactly #{ksat_co.target.instance.triples.map(t => "(" + fmt-values(t) + ")").join(", ")}, so $|Delta| = #ksat_co.target.instance.triples.len() = 10m$. - *Step 4 -- Verify a solution.* The target witness permutation is $(#ksat_co_sol.target_config.map(str).join(", "))$. It satisfies all ten cyclic-order constraints #sym.checkmark. For each variable triple, the forward orientation $(alpha_i, beta_i, gamma_i)$ is _not_ derived by this permutation, so extraction returns $(#ksat_co_sol.source_config.map(str).join(", "))$, which satisfies $phi$ #sym.checkmark + *Step 4 -- Verify a solution.* The target witness permutation is $(#fmt-values(ksat_co_sol.target_config))$. It satisfies all ten cyclic-order constraints #sym.checkmark. For each variable triple, the forward orientation $(alpha_i, beta_i, gamma_i)$ is _not_ derived by this permutation, so extraction returns $(#fmt-values(ksat_co_sol.source_config))$, which satisfies $phi$ #sym.checkmark *Multiplicity:* The fixture stores one canonical witness. Any cyclic permutation of the target order is also valid, and other satisfying assignments of $phi$ induce additional valid cyclic orders. ], )[ - This $O(n + m)$ reduction @garey1979 @galilMegiddo1977 represents each variable by a three-element orientation gadget and each clause by five auxiliary elements linked through ten cyclic-ordering triples. For $n$ variables and $m$ clauses it produces $3n + 5m$ target elements and $10m$ triples. + This reduction @galilMegiddo1977 first normalizes clauses to three distinct, globally ordered variables. With $s$ declared source variables and $c$ source clauses it produces at most $3s + 26c + 3$ elements and $40c + 2$ cyclic-order constraints. The original ten-constraint gadget is applied to the normalized formula. ][ - _Construction._ Let $phi$ be a 3-CNF formula with variables $x_1, dots, x_n$ and clauses $C_1, dots, C_m$. For each variable $x_i$, create three target elements $alpha_i, beta_i, gamma_i$. For a positive literal $x_i$, define its associated cyclically ordered triple as $(alpha_i, beta_i, gamma_i)$; for a negative literal $not x_i$, define it as $(alpha_i, gamma_i, beta_i)$. For each clause $C_nu = (ell_1 or ell_2 or ell_3)$, write the three associated literal triples as $(a, b, c)$, $(d, e, f)$, and $(g, h, i)$. Add five fresh auxiliary elements $j_nu, k_nu, l_nu, m_nu, n_nu$ and the ten cyclic-ordering triples + _Normalization._ An empty clause maps to the fixed NO target on three elements with constraints $(0,1,2)$ and $(0,2,1)$. Otherwise remove repeated literals within each clause and discard clauses containing both signs of a variable. Compact the remaining appearing variables in increasing original-index order, retaining the inverse map. Replace a two-literal clause $(a or b)$ by $(a or b or u) and (a or b or not u)$ with fresh $u$. Replace a unit clause $a$ by the four clauses $(a or (plus.minus u) or (plus.minus v))$ over both independent signs, with fresh $u,v$. These conjunctions are true for every auxiliary assignment when the original clause is true and false for at least one constituent clause under every auxiliary assignment when the original clause is false. Every resulting clause has three distinct variables; order its literals by increasing variable index. If no clauses remain, use the one-element unconstrained YES target. + + _Construction._ Let the normalized formula have $N$ variables and $M$ clauses. For each variable $x_i$, create three target elements $alpha_i, beta_i, gamma_i$. A positive literal has associated triple $(alpha_i,beta_i,gamma_i)$; a negative literal has $(alpha_i,gamma_i,beta_i)$. For each normalized clause, let its three literal triples be $(a,b,c)$, $(d,e,f)$, $(g,h,i)$ in increasing variable order. Add five fresh elements $j,k,l,m,n$ and the ten constraints $ - (a, c, j_nu), (b, j_nu, k_nu), (c, k_nu, l_nu), (d, f, j_nu), (e, j_nu, l_nu), - (f, l_nu, m_nu), (g, i, k_nu), (h, k_nu, m_nu), (i, m_nu, n_nu), (n_nu, m_nu, l_nu). + (a,c,j), (b,j,k), (c,k,l), (d,f,j), (e,j,l), + (f,l,m), (g,i,k), (h,k,m), (i,m,n), (n,m,l). $ + There are $3N+5M$ elements and $10M$ constraints before the fixed boundary targets. + + _Correctness._ ($arrow.r.double$) Extend a satisfying source assignment to the fresh Boolean variables. Order all normalized variables by index, using the consecutive element block $(alpha_i,gamma_i,beta_i)$ for a true variable and $(alpha_i,beta_i,gamma_i)$ for a false one. For each clause, Lemma 1's seven explicit local orders @galilMegiddo1977 satisfy its gadget. Deleting their five auxiliary elements gives exactly the restriction of this common variable-block order; this is where distinct and consistently ordered variables are essential. Insert each local auxiliary sequence after its preceding variable element in the common order. When several clauses use the same gap, concatenate their sequences in clause-index order. Each local order remains the restriction of the resulting global order because no constraint connects auxiliary elements from different clauses. Thus every target constraint holds, as formalized by Corollary 2 of the paper. - _Correctness._ ($arrow.r.double$) Let $S$ be a satisfying assignment of $phi$. For each variable, exactly one of the two opposite orientations $(alpha_i, beta_i, gamma_i)$ and $(alpha_i, gamma_i, beta_i)$ is derived by any cyclic order; interpret the literal made true by $S$ as the one whose associated orientation is _not_ derived. In every clause at least one literal is true, and Galil--Megiddo's clause gadget lemma shows that the ten triples above are then consistent with the three literal orientations for that clause @galilMegiddo1977. Because different clauses use disjoint auxiliary element sets, the per-clause cyclic orders combine into one global cyclic order satisfying all target triples. ($arrow.l.double$) Conversely, let a cyclic ordering satisfy every target triple. For each variable $x_i$, put $x_i = 1$ iff $(alpha_i, beta_i, gamma_i)$ is _not_ derived. If some clause had all three literals false under this rule, then all three associated literal orientations would be derived. The same clause gadget lemma implies that the ten triples for that clause would then be inconsistent, contradicting feasibility. Hence every clause contains a true literal, so the extracted assignment satisfies $phi$. + ($arrow.l.double$) Any valid target permutation determines a unique orientation for each variable triple. Assign $x_i$ true precisely when its positive triple is not derived. If all literals of a normalized clause were false, the ten gadget constraints together with its three forward literal orientations would be inconsistent by Lemma 1. Hence every normalized clause is satisfied. The short-clause equivalences imply the original clauses are satisfied by restriction to original variables; removed tautologies hold under every assignment. - _Variable mapping._ Positive literal $x_i$ is read through the orientation $(alpha_i, beta_i, gamma_i)$, while negative literal $not x_i$ is read through the reversed orientation $(alpha_i, gamma_i, beta_i)$. + _Solution extraction._ Validate the complete target configuration through the formal API and reject $"Or(false)"$. Read the reverse orientation of each compact original variable triple, restore its original index through the saved inverse map, and set unused source variables false. Fresh Boolean variables do not occur in the extracted vector. Cyclic rotations preserve every extracted truth value. - _Solution extraction._ Given a target permutation $f$, set $x_i = 1$ iff $(f(alpha_i), f(beta_i), f(gamma_i))$ is _not_ in cyclic order; otherwise set $x_i = 0$. + _Overhead and numeric domain._ If $a$ original variables occur in retained clauses, then $a <= min(s,3c)$, $N <= a+2c$, and $M <= 4c$. This gives the registered upper bounds including the boundary targets. The native normalized clause representation is an array of three $"i64"$ values, occupying 24 bytes. A successfully allocated outer vector therefore has $24M <= I$, where $I="isize::MAX"$. Every normalized variable appears in at least one clause, so $N <= 3M$, $3N+5M <= 14M < I$, and $10M < I$. All element indices and dimension products fit $"usize"$. Before this allocation, the original clause vector bounds fresh-variable counts by $N <= 5c$; its 24-byte object layout on 64-bit and 12-byte layout on 32-bit ensure fresh signed literals fit $"i64"$. Original large sparse indices are compacted before arithmetic. Allocation or default-solver limits remain resource outcomes, not infeasibility certificates. ] #let ksat_ps = load-example("KSatisfiability", "PreemptiveScheduling") @@ -17800,9 +17660,9 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_ps.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_ps) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_ps_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_ps_sol.source_config), ) #{ @@ -17814,18 +17674,18 @@ The following table shows concrete variable overhead for example instances, take let original-jobs = 2 * n * (n + 1) + 2 * n + 7 * m let filler-jobs = num-jobs - original-jobs let sigma = range(num-jobs).map(job => - range(num-jobs).filter(slot => ksat_ps_sol.target_config.at(job * num-jobs + slot) == 1).at(0) + range(num-jobs).filter(slot => ksat_ps_sol.target_config.at(job).at(slot)).at(0) ) let slot-counts = range(t).map(slot => range(num-jobs).filter(job => sigma.at(job) == slot).len() ) let clause-slots = range(7).map(i => sigma.at(30 + i)) [ - *Step 1 -- Source instance.* The formula is $phi = (x_1 or x_2 or x_3)$ with satisfying assignment $(x_1, x_2, x_3) = (#ksat_ps_sol.source_config.map(str).join(", "))$. + *Step 1 -- Source instance.* The formula is $phi = (x_1 or x_2 or x_3)$ with satisfying assignment $(x_1, x_2, x_3) = (#fmt-values(ksat_ps_sol.source_config))$. *Step 2 -- Build Ullman's unit-task gadgets.* For $n = #n$, the reduction creates $2 n (n + 1) = #(2 * n * (n + 1))$ chain jobs $x_(i,j), overline(x)_(i,j)$, $2n = #(2 * n)$ forcing jobs $y_i, overline(y)_i$, and $7m = #(7 * m)$ clause jobs $D_(r,s)$. The slot capacities are $(#(n), #(2 * n + 1), #(2 * n + 2), #(2 * n + 2), #(m + n + 1), #(6 * m)) = (3, 7, 8, 8, 5, 6)$. We realize these capacities with $p = max(2n + 2, 6m) = #p$ processors and $F = #filler-jobs$ filler jobs, giving $#num-jobs$ total unit jobs. In this example the filler counts are $(5, 1, 0, 0, 3, 2)$. - *Step 3 -- Verify a schedule.* The witness schedule has exactly $p = #p$ jobs in each of the $T = #t$ slots: $(#slot-counts.map(str).join(", "))$. The positive chain starters $x_(1,0), x_(2,0), x_(3,0)$ are jobs $0, 8, 16$, placed at slots $(#sigma.at(0), #sigma.at(8), #sigma.at(16)) = (1, 1, 0)$, so extraction reads $(0, 0, 1)$ back from slot 0. The clause-pattern jobs are indices $30, dots, 36$; their slots are $(#clause-slots.map(str).join(", "))$, so exactly one clause job is promoted to slot $n + 1 = 4$ and the remaining six sit at slot $n + 2 = 5$. + *Step 3 -- Verify a schedule.* The witness schedule has exactly $p = #p$ jobs in each of the $T = #t$ slots: $(#fmt-values(slot-counts))$. The positive chain starters $x_(1,0), x_(2,0), x_(3,0)$ are jobs $0, 8, 16$, placed at slots $(#sigma.at(0), #sigma.at(8), #sigma.at(16)) = (1, 1, 0)$, so extraction reads $(0, 0, 1)$ back from slot 0. The clause-pattern jobs are indices $30, dots, 36$; their slots are $(#fmt-values(clause-slots))$, so exactly one clause job is promoted to slot $n + 1 = 4$ and the remaining six sit at slot $n + 2 = 5$. *Multiplicity:* The fixture stores one canonical witness. Other satisfying assignments induce different slot-0 choices for the variable chains and therefore different valid schedules meeting the same threshold. ] @@ -17853,22 +17713,22 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_td.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_td) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_td_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_td_sol.source_config), ) *Step 1 -- Source instance.* The canonical formula has $n = #ksat_td.source.instance.num_vars$ variables and $m = #ksat_td.source.instance.clauses.len()$ clauses: $ phi = (#ksat_td.source.instance.clauses.map(c => { c.literals.map(l => if l > 0 { $x_#l$ } else { $overline(x)_#calc.abs(l)$ }).join($or$) }).join($) and ($)) $ - The stored satisfying assignment is $(x_1, x_2, x_3) = (#ksat_td_sol.source_config.map(str).join(", "))$. + The stored satisfying assignment is $(x_1, x_2, x_3) = (#fmt-values(ksat_td_sol.source_config))$. *Step 2 -- Normalize and assign periods.* The source has $L = #ksat_td.source.instance.clauses.map(c => c.literals.len()).sum()$ literal occurrences. Each variable appears at most three times, so no bounded-occurrence cloning is needed. The normalized instance uses $#(ksat_td.target.instance.num_periods / 4)$ transformed variables and therefore $4q = #ksat_td.target.instance.num_periods$ timetable periods/colors. *Step 3 -- Compile the gadget graph.* The list-edge-coloring gadget becomes a timetable with $#ksat_td.target.instance.num_craftsmen$ craftsmen, $#ksat_td.target.instance.num_tasks$ tasks, and #ksat_td_req binary requirements. Each blocked color on a core-graph vertex is encoded directly by removing that period from the corresponding craftsman/task availability row, so no dummy "blocker" craftsmen or tasks are needed. - *Step 4 -- Verify a solution.* The target witness has #ksat_td_sol.target_config.filter(x => x == 1).len() scheduled pairs, exactly matching the #ksat_td_req nonzero requirements. Each required craftsman-task pair is scheduled in a period allowed by both availability rows, and `pred evaluate` accepts the timetable. Reading back the distinguished variable-gadget periods recovers $(#ksat_td_sol.source_config.map(str).join(", "))$, which satisfies both clauses #sym.checkmark + *Step 4 -- Verify a solution.* The target witness has #ksat_td_sol.target_config.flatten().filter(x => x).len() scheduled pairs, exactly matching the #ksat_td_req nonzero requirements. Each required craftsman-task pair is scheduled in a period allowed by both availability rows, and `pred evaluate` accepts the timetable. Reading back the distinguished variable-gadget periods recovers $(#fmt-values(ksat_td_sol.source_config))$, which satisfies both clauses #sym.checkmark *Multiplicity:* The fixture stores one canonical witness. Different satisfying assignments, or different satisfying choices for the clause-edge colors, can induce distinct feasible timetables for the same formula. ], @@ -17892,44 +17752,28 @@ The following table shows concrete variable overhead for example instances, take #let ksat_ap_sol = ksat_ap.solutions.at(0) #reduction-rule("KSatisfiability", "AcyclicPartition", example: true, - example-caption: [3-SAT with $n = #ksat_ap.source.instance.num_vars$ variables, $m = #ksat_ap.source.instance.clauses.len()$ clause $arrow.r$ acyclic partition on #ksat_ap.target.instance.graph.num_vertices vertices], + example-caption: [3-SAT to a partition with two heavy anchors and unit incidence items], extra: [ - #pred-commands( - "pred create --example " + problem-spec(ksat_ap.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_ap) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_ap_sol.source_config.map(str).join(","), - ) - - #{ - let n = ksat_ap.source.instance.num_vars - let m = ksat_ap.source.instance.clauses.len() - let tgt = ksat_ap.target.instance - [ - *Step 1 -- Source instance.* The canonical formula has $n = #n$ variable and $m = #m$ clause. The stored satisfying assignment is $(#ksat_ap_sol.source_config.map(str).join(", "))$. - - *Step 2 -- Compose the three-stage chain.* The reduction composes 3-SAT $arrow.r$ Subset Sum $arrow.r$ Partition $arrow.r$ Acyclic Partition. First, the Sipser digit-encoding produces a Subset Sum instance with $2n + 2m$ elements. Second, the Subset Sum $arrow.r$ Partition padding appends at most one element. Third, the Partition $arrow.r$ Acyclic Partition gadget builds a bipartite digraph: for each of the Partition elements, create one item vertex; add a source vertex and a sink vertex, with arcs from source to every item vertex and from every item vertex to sink. The resulting digraph has #tgt.graph.num_vertices vertices and #tgt.graph.arcs.len() arcs. Vertex weights are doubled element sizes for items and $(Sigma + 1)$ for the two endpoints; the weight bound is $Sigma + 1 + Sigma - (Sigma mod 2)$ where $Sigma$ is the Partition total, and the arc-cost bound equals the number of items. - - *Step 3 -- Verify a solution.* The target witness $(#ksat_ap_sol.target_config.map(str).join(", "))$ partitions the #tgt.graph.num_vertices vertices into two blocks. The source and sink land in different blocks, ensuring the quotient digraph is acyclic. The item vertices split so that the doubled sizes on each side, together with the endpoint weight, respect the weight cap. Extracting back through Partition and Subset Sum recovers $(#ksat_ap_sol.source_config.map(str).join(", "))$, which satisfies the formula #sym.checkmark - - *Multiplicity:* The fixture stores one canonical witness. Other satisfying assignments of the source formula induce different balanced partitions of the item vertices. - ] - } + The canonical formula $(x_1 or x_1 or x_1)$ gives a clique graph with four vertices, three edges, and threshold two. The incidence construction has seven items and two anchors, capacity $c=3$, magnitude $M=15$, weight bound $B=21$, and cost bound $K=101$. Anchor weights are 18 and 14. The stored partition $(#fmt-values(ksat_ap_sol.target_config))$ puts two clique vertices and their edge item with the source anchor. Its cut cost is $105-2-2=101$. Formal extraction gives $(#fmt-values(ksat_ap_sol.source_config))$. ], )[ - This composed reduction realizes 3-SAT $arrow.r$ Acyclic Partition via the witness-preserving chain 3-SAT $arrow.r$ Subset Sum $arrow.r$ Partition $arrow.r$ Acyclic Partition. The first two stages are the classical Sipser digit encoding @sipser2012 and Garey--Johnson padding @garey1979; the final stage embeds the balanced partition into a bipartite source--sink digraph whose two-block quotient is automatically acyclic. For $n$ variables and $m$ clauses, the target has $2n + 2m + 3$ vertices and $4n + 4m + 2$ arcs. + Compose the literal-compatibility clique construction @karp1972 with the incidence construction below. All weights and arc costs are positive integers of polynomial magnitude. This incidence lemma is proved here; it does not use the digit-encoded Subset Sum chain. ][ - _Construction._ Given a 3-CNF formula $phi$ with $n$ variables and $m$ clauses: + _Construction._ First obtain a clique instance $H=(V,E)$ with threshold $k$ from the formal 3-SAT-to-KClique rule, including its universal vertex and padding. Write $h=|V|$, $e=|E|$, and $L=h+e$. Here $1 <= k <= h$. + + 1. Create one unit-weight item per vertex and edge of $H$. Set $c=k(k+1)/2$, $M=2L+1$, and $B=2(L+c)+1$. + 2. Add anchors $s,t$ with weights $B-c$ and $B-L$. For every item $i$, add $(s,i)$ of cost $M$ and $(i,t)$ of cost $M-d_i$, where $d_i=deg_H(i)+1$ for vertex items and zero for edge items. + 3. For every edge $a={u,v}$ add $(u,a)$ and $(v,a)$, each of cost one. Set the weight bound to $B$ and cut-cost bound to $K=M L-k^2$. - (i) Apply the Sipser digit-encoding reduction (see KSatisfiability $arrow.r$ SubsetSum) to obtain a Subset Sum instance with $2n + 2m$ elements and target $T$. + _Forward direction._ Given a clique of at least $k$ vertices, choose exactly $k$ of them. Put these vertex items and their $k(k-1)/2$ edge items with $s$, and all remaining items with $t$. The source block contains exactly $c$ unit items; the sink block contains at most $L$. No dependency arc goes from the sink block to the source block, so the quotient is acyclic. Spoke costs are $M L-sum_(v " selected")(deg_H(v)+1)$, and dependency costs are $sum_(v " selected") deg_H(v)-k(k-1)$. Their sum is $M L-k^2=K$. - (ii) Apply the Subset Sum $arrow.r$ Partition reduction (see SubsetSum $arrow.r$ Partition) to obtain a Partition instance with at most $2n + 2m + 1$ elements and total $Sigma$. + _Backward direction._ Each anchor weighs more than $B/2$, so they occupy distinct blocks. If $r>=1$ items lie outside those blocks, spoke costs alone are at least $M(L+r)-(h+2e)>M L>K$, since $h+2e<=2L=1$. A dependency in the opposite direction would create a cycle, so an edge item in the source block has both endpoints there. - (iii) For each Partition element $a_i$, create one item vertex with weight $2 a_i$. Add a source vertex $s$ and a sink vertex $t$, each with weight $Sigma + 1$. Create arcs $(s, v_i)$ and $(v_i, t)$ for every item vertex $v_i$, all with unit cost. Set the weight bound $B = Sigma + 1 + Sigma - (Sigma mod 2)$ and the arc-cost bound $K =$ number of items. + Let $p$ and $q$ count vertex and edge items in the source block. Capacity gives $p+q<=c$ and closure gives $q<=p(p-1)/2$. The exact cut cost is $M L-p-2q$, hence $p+2q>=k^2$. If $pk$, then $p+2q<=2c-p "(" + e.map(str).join(", ") + ")").join(", ")}$. This is a 4-cycle, which admits a Hamiltonian circuit. + *Step 1 -- Source instance.* The source graph $G$ has $n = #graph-num-vertices(hc_bicon.source.instance)$ vertices and $|E| = #graph-num-edges(hc_bicon.source.instance)$ edges: $E = {#hc_bicon.source.instance.graph.edges.map(e => "(" + fmt-values(e) + ")").join(", ")}$. This is a 4-cycle, which admits a Hamiltonian circuit. *Step 2 -- Construction.* Start with the edgeless graph $H = (V, emptyset)$ on $n = #graph-num-vertices(hc_bicon.target.instance)$ vertices. For each pair ${u, v}$, create a potential edge with weight 1 if ${u,v} in E$ and weight 2 otherwise. This yields #hc_bicon.target.instance.potential_weights.len() potential edges: #hc_bicon.target.instance.potential_weights.map(pw => "{" + str(pw.at(0)) + ", " + str(pw.at(1)) + "} (w=" + str(pw.at(2)) + ")").join(", "). The budget is $B = #hc_bicon.target.instance.budget = n$. - *Step 3 -- Verify a solution.* The canonical Hamiltonian circuit visits vertices in order $(#hc_bicon_sol.source_config.map(str).join(", "))$. The target configuration $bold(x) = (#hc_bicon_sol.target_config.map(str).join(", "))$ selects potential edges #{ - let selected = hc_bicon_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => { + *Step 3 -- Verify a solution.* The canonical Hamiltonian circuit visits vertices in order $(#fmt-values(hc_bicon_sol.source_config))$. The target configuration $bold(x) = (#fmt-values(hc_bicon_sol.target_config))$ selects potential edges #{ + let selected = hc_bicon_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => { let pw = hc_bicon.target.instance.potential_weights.at(i) "{" + str(pw.at(0)) + ", " + str(pw.at(1)) + "}" }) selected.join(", ") - } — exactly the $n = #graph-num-vertices(hc_bicon.source.instance)$ cycle edges, each of weight 1, for a total cost of #hc_bicon_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => hc_bicon.target.instance.potential_weights.at(i).at(2)).sum() $= n = B$ #sym.checkmark + } — exactly the $n = #graph-num-vertices(hc_bicon.source.instance)$ cycle edges, each of weight 1, for a total cost of #hc_bicon_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => hc_bicon.target.instance.potential_weights.at(i).at(2)).sum() $= n = B$ #sym.checkmark - *Multiplicity:* The fixture stores one canonical witness. The 4-cycle has $#{ - let n = graph-num-vertices(hc_bicon.source.instance) - let fac = range(1, n).fold(1, (acc, x) => acc * x) - str(int(fac / 2)) - }$ distinct Hamiltonian circuits ($(n-1)! slash 2$ directed cycles up to reversal). + *Multiplicity:* The 4-cycle has one undirected Hamiltonian cycle up to rotation and reversal, represented by eight vertex permutations. All eight permutations select the same four undirected target edges. ], )[ - Start with the edgeless graph on $n$ vertices. Price original edges at cost 1 and non-edges at cost 2. A budget-$n$ augmentation is achievable iff $G$ has a Hamiltonian circuit (the only way to biconnect with $n$ weight-1 edges is a Hamiltonian cycle). + For $n < 3$, output three isolated vertices, no candidate edges, and budget zero. For $n >= 3$, start with the edgeless graph on $n$ vertices. Price original edges at cost 1 and non-edges at cost 2, and set budget $n$. The resulting augmentation is feasible iff $G$ has a Hamiltonian circuit. ][ - _Construction._ Given $G = (V, E)$ with $n = |V|$, let $H = (V, emptyset)$. For every pair ${u, v}$: potential edge with weight 1 if ${u,v} in E$, else weight 2. Budget $B = n$. + _Construction._ The weighted construction is due to Eswaran and Tarjan @eswarantarjan1976 (also Theorems 7–8 of their January 1974 technical report ERL-M441). The source requires at least three vertices. If $n < 3$, map this necessarily negative instance to the disconnected three-vertex graph with no candidates and budget zero. Otherwise, let $H = (V, emptyset)$ and give every unordered pair of distinct vertices cost 1 if it is a source edge and 2 otherwise, with $B = n$. Source loops are irrelevant to a Hamiltonian circuit on at least three vertices; repeated source edges do not change adjacency or candidate costs. - _Correctness._ ($arrow.r.double$) A Hamiltonian circuit selects $n$ weight-1 edges forming a 2-connected cycle, cost $= n$. ($arrow.l.double$) Budget $n$ with $>= n$ edges required (degree $>= 2$) forces exactly $n$ weight-1 edges (all from $E$), which must form a Hamiltonian cycle. + _Correctness._ For $n < 3$, both instances are infeasible. For $n >= 3$, a source Hamiltonian circuit selects $n$ weight-1 edges forming a biconnected cycle of cost $n$. Conversely, a feasible target is connected and every vertex has degree at least two: a degree-zero vertex contradicts connectivity, and a degree-one vertex would be separated from the other surviving vertices by deleting its neighbor. The degree sum therefore forces at least $n$ selected edges. Positive costs and budget $n$ force exactly $n$ edges, all of cost 1. Every degree is exactly two, so connectivity makes these edges a single spanning cycle of the source. All partial selected-weight sums are at most the final sum for feasible targets; evaluation therefore preserves the budget test with exact integer arithmetic. - _Solution extraction._ Walk the unique cycle from vertex 0 through selected edges to recover the circuit order. + _Solution extraction._ Validate the target certificate and require its evaluation to be `Or(true)` before decoding. Walk the selected cycle from vertex 0 to recover the circuit order. The negative sentinel has no feasible certificate. The target has at most $n+3$ vertices, no initial edges, and at most $n(n-1)/2$ candidates. ] #let hc_sca = load-example("HamiltonianCircuit", "StrongConnectivityAugmentation") #let hc_sca_sol = hc_sca.solutions.at(0) #let hc_sca_n = graph-num-vertices(hc_sca.source.instance) #let hc_sca_candidate_arcs = hc_sca.target.instance.candidate_arcs -#let hc_sca_selected = hc_sca_candidate_arcs.enumerate().filter(((i, _)) => hc_sca_sol.target_config.at(i) == 1).map(((i, a)) => a) +#let hc_sca_selected = hc_sca_candidate_arcs.enumerate().filter(((i, _)) => hc_sca_sol.target_config.at(i)).map(((i, a)) => a) #let hc_sca_w1 = hc_sca_candidate_arcs.filter(a => a.at(2) == 1) #let hc_sca_w2 = hc_sca_candidate_arcs.filter(a => a.at(2) == 2) #reduction-rule("HamiltonianCircuit", "StrongConnectivityAugmentation", @@ -17986,16 +17826,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hc_sca.source) + " -o hc.json", - "pred reduce hc.json --to " + target-spec(hc_sca) + " -o bundle.json", + "pred reduce hc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hc.json --config " + hc_sca_sol.source_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(hc_sca_sol.source_config), ) - *Step 1 -- Source instance.* The source graph is the cycle on $#hc_sca_n$ vertices with edges #hc_sca.source.instance.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The canonical Hamiltonian-circuit witness is the vertex permutation $[#hc_sca_sol.source_config.map(str).join(", ")]$. + *Step 1 -- Source instance.* The source graph is the cycle on $#hc_sca_n$ vertices with edges #hc_sca.source.instance.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The canonical Hamiltonian-circuit witness is the vertex permutation $[#fmt-values(hc_sca_sol.source_config)]$. *Step 2 -- Construction.* Start with the empty digraph $D = (V, emptyset)$ on $#hc_sca_n$ vertices. Generate all $#hc_sca_candidate_arcs.len()$ ordered pairs as candidate arcs: #hc_sca_w1.len() weight-1 arcs #hc_sca_w1.map(a => $(#a.at(0) arrow #a.at(1))$).join(", ") corresponding to source edges (both orientations), and #hc_sca_w2.len() weight-2 arcs #hc_sca_w2.map(a => $(#a.at(0) arrow #a.at(1))$).join(", ") for non-edges. Budget $B = #hc_sca.target.instance.bound$. - *Step 3 -- Verify a solution.* The target configuration $[#hc_sca_sol.target_config.map(str).join(", ")]$ selects arcs #hc_sca_selected.map(a => $(#a.at(0) arrow #a.at(1))$).join(", "), all weight 1. Total cost $= #hc_sca_selected.len() times 1 = #hc_sca_n = B$ #sym.checkmark. These $#hc_sca_n$ arcs form a single directed cycle visiting every vertex, so the augmented digraph is strongly connected. Extracting the circuit: follow successors from vertex 0 to recover $[#hc_sca_sol.source_config.map(str).join(", ")]$ #sym.checkmark + *Step 3 -- Verify a solution.* The target configuration $[#fmt-values(hc_sca_sol.target_config)]$ selects arcs #hc_sca_selected.map(a => $(#a.at(0) arrow #a.at(1))$).join(", "), all weight 1. Total cost $= #hc_sca_selected.len() times 1 = #hc_sca_n = B$ #sym.checkmark. These $#hc_sca_n$ arcs form a single directed cycle visiting every vertex, so the augmented digraph is strongly connected. Extracting the circuit: follow successors from vertex 0 to recover $[#fmt-values(hc_sca_sol.source_config)]$ #sym.checkmark *Multiplicity:* The fixture stores one canonical witness. For the 4-cycle there are $#hc_sca_n times 2 = #{hc_sca_n * 2}$ Hamiltonian-circuit permutations (choice of start vertex and direction), each yielding a distinct set of directed arcs. ], @@ -18021,27 +17861,31 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hc_sc.source) + " -o hc.json", - "pred reduce hc.json --to " + target-spec(hc_sc) + " -o bundle.json", + "pred reduce hc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hc.json --config " + hc_sc_sol.source_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(hc_sc_sol.source_config), ) - *Step 1 -- Source instance.* The canonical source fixture is the cycle $C_#hc_sc_n$ on vertices ${0, dots, #(hc_sc_n - 1)}$ with #hc_sc_source_edges.len() edges: #hc_sc_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The stored Hamiltonian-circuit witness is the permutation $[#hc_sc_sol.source_config.map(str).join(", ")]$.\ + *Step 1 -- Source instance.* The canonical source fixture is the cycle $C_#hc_sc_n$ on vertices ${0, dots, #(hc_sc_n - 1)}$ with #hc_sc_source_edges.len() edges: #hc_sc_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The stored Hamiltonian-circuit witness is the permutation $[#fmt-values(hc_sc_sol.source_config)]$.\ *Step 2 -- Construction.* Each vertex $v_i$ splits into $v_i^"in" = 2i$ and $v_i^"out" = 2i + 1$, giving $2 dot #hc_sc_n = #hc_sc.target.instance.num_vertices$ vertices. The reduction creates #hc_sc_target_arcs.len() mandatory arcs: #hc_sc_target_arcs.map(a => $(#a.at(0) arrow #a.at(1))$).join(", "), each of length 1. For each source edge, two undirected connector edges of length 1 are added, giving $2 dot #hc_sc_source_edges.len() = #hc_sc_target_edges.len()$ connector edges: #hc_sc_target_edges.map(e => ${#e.at(0), #e.at(1)}$).join(", ").\ - *Step 3 -- Verify a solution.* The stored target configuration $[#hc_sc_sol.target_config.map(str).join(", ")]$ is a permutation of arcs. Following this order: arc #hc_sc_sol.target_config.at(0) serves $(#hc_sc_target_arcs.at(hc_sc_sol.target_config.at(0)).at(0) arrow #hc_sc_target_arcs.at(hc_sc_sol.target_config.at(0)).at(1))$, then a connector edge leads to the next arc, and so on. The tour traverses $#hc_sc_target_arcs.len()$ arcs (cost $#hc_sc_target_arcs.len()$) and $#hc_sc_target_arcs.len()$ connector edges (cost $#hc_sc_target_arcs.len()$), for total cost $2 dot #hc_sc_n = #(hc_sc_n * 2)$. Recovering the source witness: arc $i$ corresponds to vertex $i$, so the permutation $[#hc_sc_sol.source_config.map(str).join(", ")]$ is the Hamiltonian circuit #sym.checkmark\ + *Step 3 -- Verify a solution.* The stored target configuration $[#fmt-values(hc_sc_sol.target_config)]$ is a permutation of arcs. Following this order: arc #hc_sc_sol.target_config.at(0) serves $(#hc_sc_target_arcs.at(hc_sc_sol.target_config.at(0)).at(0) arrow #hc_sc_target_arcs.at(hc_sc_sol.target_config.at(0)).at(1))$, then a connector edge leads to the next arc, and so on. The tour traverses $#hc_sc_target_arcs.len()$ arcs (cost $#hc_sc_target_arcs.len()$) and $#hc_sc_target_arcs.len()$ connector edges (cost $#hc_sc_target_arcs.len()$), for total cost $2 dot #hc_sc_n = #(hc_sc_n * 2)$. Recovering the source witness: arc $i$ corresponds to vertex $i$, so the permutation $[#fmt-values(hc_sc_sol.source_config)]$ is the Hamiltonian circuit #sym.checkmark\ *Multiplicity:* The fixture stores one canonical witness. For $C_#hc_sc_n$ there are $#hc_sc_n times 2 = #(hc_sc_n * 2)$ directed Hamiltonian circuits (choice of start vertex and direction), each yielding a distinct arc-service permutation. ], )[ - Each vertex $v_i$ splits into $(v_i^"in", v_i^"out")$ with a mandatory directed arc of length 1. Undirected connector edges of length 1 encode original graph edges. A tour of cost $2n$ exists iff a Hamiltonian circuit exists. + Each vertex $v_i$ splits into $(v_i^"in", v_i^"out")$ with a mandatory directed arc of length 1. Undirected connector edges of length 1 encode original graph edges. For $n>=3$, a tour of cost $2n$ exists iff a Hamiltonian circuit exists; smaller source graphs have answer NO. ][ - _Construction._ Given $G = (V, E)$ with $n = |V|$. Create $2n$ vertices: $v_i^"in" = 2i$, $v_i^"out" = 2i + 1$. Add $n$ mandatory arcs $(v_i^"in", v_i^"out")$ of length 1. For each ${v_i, v_j} in E$: connector edges $(v_i^"out", v_j^"in")$ and $(v_j^"out", v_i^"in")$ of length 1. + _Construction._ Let the source simple graph $G=(V,E)$ have $n=|V|$ vertices and $m=|E|$ edges. Replace vertex $i$ by $i^"in"=2i$ and $i^"out"=2i+1$, with a unit service arc from the former to the latter. For each source edge ${i,j}$, add unit undirected connectors ${i^"out",j^"in"}$ and ${j^"out",i^"in"}$. The target has exactly $2n$ vertices, $n$ service arcs, and $2m$ edges. The source answer is YES exactly when $n>=3$ and the target minimum exists and equals $2n$. + + _Forward direction._ A Hamiltonian ordering gives the same service ordering. Each of its $n$ services costs one and each consecutive pair, including the closing pair, has a direct unit connector. Its cost is $2n$. Every connector links distinct split endpoints and has cost at least one, so this cost is optimal. - _Correctness._ ($arrow.r.double$) A Hamiltonian circuit gives a tour serving arcs in circuit order, each inter-arc hop using one connector edge, total cost $2n$. ($arrow.l.double$) Cost $2n$ with $n$ arcs of cost 1 leaves exactly $n$ connector hops of cost 1 each; single-hop connections correspond to edges of $G$. + _Backward direction._ Suppose $n>=3$ and a service permutation has cost $2n$. Its $n$ services cost $n$, leaving exactly $n$ for the $n$ connectors. Each connector must therefore cost one. An outgoing split vertex has no outgoing service arc; a one-step connector to an incoming split vertex exists precisely for an original graph edge. Thus consecutive service indices, including the last and first, are adjacent in $G$. The permutation visits every vertex once and is a Hamiltonian circuit. Longer finite tours are not certificates. - _Solution extraction._ The arc service permutation directly encodes the Hamiltonian circuit vertex order. + _Boundaries and numeric domain._ Simple Hamiltonian circuits require at least three vertices. In particular, a two-vertex single-edge graph has target optimum four but no source circuit; the empty target has cost zero but the empty source has no circuit. The condition $n>=3$ handles this domain explicitly. A finite shortest connector can be chosen simple and uses at most $2n-1$ unit steps. Hence every finite service-order cost is at most $2n^2$. Split dimensions and this bound are checked for representability before allocation; a failure returns a typed reduction error. Target construction uses its fallible API. + + _Solution extraction._ Evaluate once, apply the same aggregate certificate predicate, and reject non-certifying tours with an extraction error. Otherwise the service permutation is the source vertex order. The target evaluator permits service arcs on connector paths; the proof remains valid because equality forces each connector to be a single undirected edge. No target-definition change is required. ] #let hc_rp = load-example("HamiltonianCircuit", "RuralPostman") @@ -18053,16 +17897,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hc_rp.source) + " -o hc.json", - "pred reduce hc.json --to " + target-spec(hc_rp) + " -o bundle.json", + "pred reduce hc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hc.json --config " + hc_rp_sol.source_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(hc_rp_sol.source_config), ) - *Step 1 -- Source instance.* The canonical HC instance is a cycle $C_#hc_rp_n$ with $n = #hc_rp_n$ vertices and $|E| = #graph-num-edges(hc_rp.source.instance)$ edges. The stored witness is the permutation $(#hc_rp_sol.source_config.map(str).join(", "))$. + *Step 1 -- Source instance.* The canonical HC instance is a cycle $C_#hc_rp_n$ with $n = #hc_rp_n$ vertices and $|E| = #graph-num-edges(hc_rp.source.instance)$ edges. The stored witness is the permutation $(#fmt-values(hc_rp_sol.source_config))$. *Step 2 -- Construction.* Each vertex splits into $(v_i^a, v_i^b)$, producing $2n = #graph-num-vertices(hc_rp.target.instance)$ vertices. The target graph has #graph-num-edges(hc_rp.target.instance) edges: #hc_rp.target.instance.required_edges.len() required edges (one per source vertex) and #(graph-num-edges(hc_rp.target.instance) - hc_rp.target.instance.required_edges.len()) connector edges (two per source edge). All edge lengths are 1. - *Step 3 -- Verify a solution.* The target solution assigns edge multiplicities $(#hc_rp_sol.target_config.map(str).join(", "))$. The tour traverses all #hc_rp.target.instance.required_edges.len() required edges plus #hc_rp_n connector edges, for total cost $= #(2 * hc_rp_n) = 2n$ #sym.checkmark. + *Step 3 -- Verify a solution.* The target solution assigns edge multiplicities $(#fmt-values(hc_rp_sol.target_config))$. The tour traverses all #hc_rp.target.instance.required_edges.len() required edges plus #hc_rp_n connector edges, for total cost $= #(2 * hc_rp_n) = 2n$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. The $#hc_rp_n$-cycle has $#hc_rp_n$ rotations $times$ 2 reflections $= #(2 * hc_rp_n)$ directed Hamiltonian circuits. ], @@ -18076,56 +17920,33 @@ The following table shows concrete variable overhead for example instances, take _Solution extraction._ Identify used connector edges and follow the successor map from vertex 0. ] -#let mis_ifb = load-example("MaximumIndependentSet", "IntegralFlowBundles") +#let mis_ifb = load-example("DecisionMaximumIndependentSet", "IntegralFlowBundles", source-variant: (graph: "SimpleGraph", weight: "One")) #let mis_ifb_sol = mis_ifb.solutions.at(0) -#reduction-rule("MaximumIndependentSet", "IntegralFlowBundles", +#reduction-rule("DecisionMaximumIndependentSet", "IntegralFlowBundles", example: true, - example-caption: [Path graph $P_#mis_ifb.source.instance.graph.num_vertices$ ($n = #mis_ifb.source.instance.graph.num_vertices$, $|E| = #mis_ifb.source.instance.graph.edges.len()$)], + example-source-variant: (graph: "SimpleGraph", weight: "One"), + example-caption: [The three-vertex path has an independent set of size at least two], extra: [ #pred-commands( - "pred create --example " + problem-spec(mis_ifb.source) + " -o mis.json", - "pred reduce mis.json --to " + target-spec(mis_ifb) + " -o bundle.json", + "pred create --example DecisionMaximumIndependentSet/One -o independent-set.json", + "pred reduce independent-set.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate mis.json --config " + mis_ifb_sol.source_config.map(str).join(","), + "pred extract bundle.json --config " + cli-config(mis_ifb_sol.target_config), ) - - #{ - let graph = mis_ifb.source.instance.graph - let n = graph.num_vertices - let verts = range(n).map(k => (k * 1.5, 0)) - let is-in-set = mis_ifb_sol.source_config.map(c => c > 0) - let blue = graph-colors.at(0) - align(center, canvas(length: 0.8cm, { - import draw: * - for (u, v) in graph.edges { - g-edge(verts.at(u), verts.at(v)) - } - for (k, pos) in verts.enumerate() { - g-node(pos, name: str(k), - fill: if is-in-set.at(k) { blue } else { white }, - label: [$v_#k$]) - } - })) - } - - *Step 1 -- Source instance.* The path graph $P_#mis_ifb.source.instance.graph.num_vertices$ has $n = #mis_ifb.source.instance.graph.num_vertices$ vertices and edges ${#mis_ifb.source.instance.graph.edges.map(e => "(" + str(e.at(0)) + "," + str(e.at(1)) + ")").join(", ")}$, with unit weights $(#mis_ifb.source.instance.weights.map(str).join(", "))$. - - *Step 2 -- Build the flow network.* The reduction creates a directed graph with $#mis_ifb.target.instance.graph.num_vertices$ nodes: source $s = #mis_ifb.target.instance.source$, intermediates $w_0, dots, w_(#(mis_ifb.source.instance.graph.num_vertices - 1))$, and sink $t = #mis_ifb.target.instance.sink$. There are $#mis_ifb.target.instance.graph.arcs.len()$ arcs ($2n = #(2 * mis_ifb.source.instance.graph.num_vertices)$): for each vertex $v_i$, arc $a_i^"in" = (s, w_i)$ at index $2i$ and $a_i^"out" = (w_i, t)$ at index $2i+1$. - - *Step 3 -- Create bundles.* There are $#mis_ifb.target.instance.bundles.len()$ bundles total. For each original edge ${v_i, v_j} in E$, an edge bundle ${a_i^"out", a_j^"out"}$ with capacity 1 enforces that at most one endpoint is selected (#mis_ifb.source.instance.graph.edges.len() edge bundles). For each vertex $v_i$, a vertex bundle ${a_i^"in", a_i^"out"}$ with capacity 2 links the in/out arcs (#mis_ifb.source.instance.graph.num_vertices vertex bundles). Bundle capacities: $(#mis_ifb.target.instance.bundle_capacities.map(str).join(", "))$. Flow requirement $R = #mis_ifb.target.instance.requirement$. - - *Step 4 -- Verify a solution.* The canonical IS selects vertices ${#mis_ifb_sol.source_config.enumerate().filter(((i, x)) => x > 0).map(((i, x)) => $v_#i$).join(", ")}$ (config $(#mis_ifb_sol.source_config.map(str).join(", "))$). Each selected vertex $v_i$ contributes flow 1 on arcs $a_i^"in"$ and $a_i^"out"$, giving target config $(#mis_ifb_sol.target_config.map(str).join(", "))$. The total flow equals the IS size (#mis_ifb_sol.source_config.fold(0, (a, b) => a + b)). Every edge bundle is satisfied because no two adjacent vertices are both selected, and vertex bundles are satisfied with capacity 2 $>=$ individual flow of 1. - - *Multiplicity:* The fixture stores one canonical witness. The path $P_#mis_ifb.source.instance.graph.num_vertices$ admits larger independent sets (e.g., ${v_0, v_2}$ or ${v_0, v_3}$), but the canonical witness suffices to demonstrate the reduction. + Source bound: #mis_ifb.source.instance.bound; selected vertices: #fmt-values(mis_ifb_sol.source_config) \ + Target: #mis_ifb.target.instance.graph.num_vertices vertices, #mis_ifb.target.instance.graph.arcs.len() arcs, #mis_ifb.target.instance.bundles.len() bundles; requirement #mis_ifb.target.instance.requirement \ + Feasible flow: #fmt-values(mis_ifb_sol.target_config) ], )[ - Each vertex $v_i$ maps to a flow unit through an intermediate node; edge-bundle constraints cap combined outflow of adjacent pairs at 1, so feasible flow of value $k$ exists iff an independent set of size $>= k$ exists. + The unit-weight decision independent-set problem reduces to integral flow with bundles. Sahni's original construction @sahni1974[Theorem 2.2(b)(iv), p. 270] represents cardinality and recovers a maximum by repeated flow-threshold queries. The direct rule here preserves one explicit decision threshold; it does not claim that an arbitrary feasible flow solves weighted maximum independent set. ][ - _Construction._ Directed graph on $n + 2$ nodes: source $s$, intermediates $w_0, dots, w_(n-1)$, sink $t$. Arcs $a_i^"in" = (s, w_i)$ (index $2i$) and $a_i^"out" = (w_i, t)$ (index $2i+1$). Edge bundles ${a_i^"out", a_j^"out"}$ with capacity 1 for each ${v_i, v_j} in E$. Vertex bundles ${a_i^"in", a_i^"out"}$ with capacity 2. Flow requirement $R = 1$. + _Construction._ Let $G$ have $n$ vertices, $m$ edge records, and integer bound $k$. Set $q = min(max(k,0),n+1)$ and $R=q+1$. Introduce a source $s$, a sink $t$, and intermediate vertices $w_0, dots, w_n$. For each $i$, add arcs $a_i=(s,w_i)$ and $b_i=(w_i,t)$, and the capacity-2 bundle ${a_i,b_i}$. Paths $0, dots, n-1$ represent source vertices; path $n$ is auxiliary. For each edge record $(u,v)$ add the capacity-1 bundle ${a_u,b_v}$. There are $n+3$ vertices, $2n+2$ arcs, and $m+n+1$ bundles. Every arc belongs to a bundle, and both arcs of each bundle are distinct even for a source self-loop. + + _Backward direction._ Flow conservation and the path bundles imply that both arcs on path $i$ carry the same binary flow $x_i$. The edge bundles impose $x_u+x_v <= 1$, so $S={i= R$, hence $|S| >= q$. For $0n$ force $|S|>=n+1$ and make the target infeasible. - _Correctness._ ($arrow.r.double$) An independent set $S$ gives flow $f_i = 1$ for $v_i in S$; edge bundles satisfied since $S$ is independent. ($arrow.l.double$) Feasible flow gives $f_i in {0,1}$; edge bundles force ${v_i : f_i = 1}$ to be independent. + _Forward direction._ Given any independent set $S$ meeting $k$, send one unit through each selected path and the auxiliary path, and zero through all others. Every capacity and conservation constraint holds, and the inflow $|S|+1$ meets $R$. Thus the source and target are equivalent for every integer bound. Even the empty graph produces one auxiliary path: it is feasible for $k<=0$ and infeasible for $k>0$. - _Solution extraction._ Vertex $v_i$ in independent set iff target solution at arc index $2i+1$ is positive. + _Solution extraction._ After validating target feasibility, select original vertex $i$ exactly when its outgoing arc has flow 1. The auxiliary path is omitted. Repeated source edges add repeated constraints and do not change the proof. Allocation counts and the shifted threshold are checked before construction; no source solver is invoked during construction or extraction. ] #let hc_qa = load-example("HamiltonianCircuit", "QuadraticAssignment") @@ -18136,27 +17957,33 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hc_qa.source) + " -o hc.json", - "pred reduce hc.json --to " + target-spec(hc_qa) + " -o bundle.json", + "pred reduce hc.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hc.json --config " + hc_qa_sol.source_config.map(str).join(","), + "pred evaluate hc.json --config " + cli-config(hc_qa_sol.source_config), ) - *Step 1 -- Source instance.* The graph $G$ has $n = #hc_qa.source.instance.graph.num_vertices$ vertices and edges ${#hc_qa.source.instance.graph.edges.map(e => "(" + str(e.at(0)) + "," + str(e.at(1)) + ")").join(", ")}$, forming a cycle $C_#hc_qa.source.instance.graph.num_vertices$. The penalty weight is $omega = n + 1 = #(hc_qa.source.instance.graph.num_vertices + 1)$. + *Step 1 -- Source instance.* The graph $G$ has $n = #hc_qa.source.instance.graph.num_vertices$ vertices and edges ${#hc_qa.source.instance.graph.edges.map(e => "(" + str(e.at(0)) + "," + str(e.at(1)) + ")").join(", ")}$, forming a cycle $C_#hc_qa.source.instance.graph.num_vertices$. - *Step 2 -- Construction.* The cost matrix $C$ encodes a directed cycle on positions: $c[i][(i+1) mod #hc_qa.source.instance.graph.num_vertices] = 1$, all other entries 0. The distance matrix $D$ encodes graph adjacency: $d[k][l] = 1$ if ${k,l} in E$, $d[k][l] = #(hc_qa.source.instance.graph.num_vertices + 1)$ for non-edges, $d[k][k] = 0$. Both matrices are $#hc_qa.source.instance.graph.num_vertices times #hc_qa.source.instance.graph.num_vertices$, so the QAP has $n = #hc_qa.target.instance.cost_matrix.len()$ facilities and $n = #hc_qa.target.instance.distance_matrix.len()$ locations. + *Step 2 -- Construction.* The cost matrix $C$ encodes a directed cycle on positions: $c[i][(i+1) mod #hc_qa.source.instance.graph.num_vertices] = 1$, all other entries 0. The distance matrix $D$ encodes graph adjacency: $d[k][l] = 0$ if ${k,l} in E$, $d[k][l] = 1$ for distinct non-edges, $d[k][k] = 0$. Both matrices are $#hc_qa.source.instance.graph.num_vertices times #hc_qa.source.instance.graph.num_vertices$, so the QAP has $n = #hc_qa.target.instance.cost_matrix.len()$ facilities and $n = #hc_qa.target.instance.distance_matrix.len()$ locations. - *Step 3 -- Verify a solution.* The canonical Hamiltonian circuit visits vertices in order $gamma = (#hc_qa_sol.source_config.map(str).join(", "))$. The QAP permutation is the same: $(#hc_qa_sol.target_config.map(str).join(", "))$. The QAP cost is $sum_(i=0)^(n-1) c[i][(i+1) mod n] dot d[gamma(i)][gamma((i+1) mod n)]$. Since $gamma$ maps each position $i$ to vertex $i$, each consecutive pair $(gamma(i), gamma(i+1 mod n))$ is an edge in $G$, contributing $1 dot 1 = 1$. Total cost $= #hc_qa.source.instance.graph.num_vertices = n$ #sym.checkmark + *Step 3 -- Verify a solution.* The canonical Hamiltonian circuit visits vertices in order $gamma = (#fmt-values(hc_qa_sol.source_config))$. The QAP permutation is the same: $(#fmt-values(hc_qa_sol.target_config))$. The QAP cost is $sum_(i=0)^(n-1) c[i][(i+1) mod n] dot d[gamma(i)][gamma((i+1) mod n)]$. Since $gamma$ maps each position $i$ to vertex $i$, each consecutive pair $(gamma(i), gamma(i+1 mod n))$ is an edge in $G$, contributing $1 dot 0 = 0$. Total cost $= 0$ #sym.checkmark - *Multiplicity:* The fixture stores one canonical witness. The cycle $C_#hc_qa.source.instance.graph.num_vertices$ has $#hc_qa.source.instance.graph.num_vertices$ rotations and 2 reflections, giving $2n = #(2 * hc_qa.source.instance.graph.num_vertices)$ distinct Hamiltonian circuits; the canonical one is the identity permutation. + *Multiplicity:* The fixture stores one canonical witness. The cycle $C_#hc_qa.source.instance.graph.num_vertices$ has $#hc_qa.source.instance.graph.num_vertices$ rotations and 2 reflections, giving $2n = #(2 * hc_qa.source.instance.graph.num_vertices)$ distinct visit-order permutations of the same undirected Hamiltonian circuit; the canonical one is the identity permutation. ], )[ - Position-adjacency encoded in cost matrix $C$ (directed cycle on positions), graph-adjacency in distance matrix $D$ (1 for edges, $omega = n+1$ for non-edges). QAP optimum equals $n$ iff a Hamiltonian circuit exists. + Encode a cycle of positions in $C$ and missing source edges in the binary distance matrix $D$. The QAP optimum is zero iff the source has a Hamiltonian circuit. Sources with fewer than three vertices map to a fixed positive-cost target. ][ - _Construction._ Let $G = (V, E)$ with $n = |V|$ and $omega = n + 1$. Cost matrix: $c[i][j] = 1$ if $j equiv i+1 space (mod n)$, else 0. Distance matrix: $d[k][l] = 0$ if $k = l$; 1 if ${k,l} in E$; $omega$ otherwise. + _Construction._ Let $n$ be the source vertex count and $N=max(n,3)$. Set $c[i][j]=1$ exactly when $j=(i+1) mod N$. Set diagonal entries of $D$ to zero. For $n>=3$, set $d[k][l]=1$ exactly when distinct vertices $k,l$ are not adjacent. For $n<3$, set every off-diagonal entry to 1. Both matrices have order $N$. + + _Relation to the literature._ The cycle-position construction is due to @sahni-gonzalez1976, Theorem 2.1(viii), p. 561. For $n>=3$, replacing their edge/non-edge costs 1 and $n+1$ by 0 and 1 changes the assignment cost from $n+n q$ to $q$, where $q$ counts missing cyclic edges. This preserves exact decision equivalence; no preservation of multiplicative approximation ratios is claimed. + + _Forward direction._ If a source Hamiltonian circuit has visit order $gamma$, then $n>=3$ and $gamma$ is a permutation. The only nonzero entries of $C$ connect consecutive cyclic positions, all of whose images are source edges. Each corresponding distance is zero, so the QAP cost is zero. + + _Backward direction._ A valid target assignment is a permutation because the two matrix orders agree. For $n>=3$, its cost is the sum of the $n$ nonnegative indicators of missing cyclic edges. Zero cost forces every indicator to vanish, yielding a Hamiltonian circuit. For $n<3$, the source is NO by definition; the target has three positions and every permutation costs 3, so it has no zero-cost certificate. - _Correctness._ ($arrow.r.double$) A Hamiltonian circuit $v_0, dots, v_(n-1)$ with $gamma(i) = v_i$ gives cost $n$ (each consecutive pair is an edge). ($arrow.l.double$) Cost $n$ forces $d[gamma(i), gamma((i+1) mod n)] = 1$ for all $i$, meaning all consecutive pairs are edges. + _Solution and value extraction._ Validate the target configuration once and require its value to be `Min(Some(0))` before returning the same permutation. Invalid assignments and positive-cost assignments are rejected. The aggregate value map is `Or(value == Min(Some(0)))`; apply it to a proved optimum to decide the source. A timeout or unproved incumbent cannot certify NO. - _Solution extraction._ The QAP permutation $gamma$ is the Hamiltonian circuit visit order directly. + _Domain and overhead._ Loops and repeated edges do not change adjacency between distinct vertices. Matrix entries are 0 or 1, with only $N$ nonzero cost entries, so every objective product and every partial sum lies between 0 and $N$. Native `SimpleGraph` vertex indices are bounded by the underlying `u32` graph representation, so these sums fit `i64`. Both target dimensions are $N<=n+3$; allocation failure is not a NO certificate. ] // Removed: HamiltonianPath → ConsecutiveOnesSubmatrix (unsound reduction, #1006) @@ -18167,8 +17994,8 @@ The following table shows concrete variable overhead for example instances, take #let part_bp_n = part_bp_sizes.len() #let part_bp_total = part_bp_sizes.fold(0, (a, b) => a + b) #let part_bp_capacity = part_bp.target.instance.capacity -#let part_bp_bin0 = part_bp_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => i) -#let part_bp_bin1 = part_bp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) +#let part_bp_bin0 = part_bp_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => i) +#let part_bp_bin1 = part_bp_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let part_bp_bin0_sizes = part_bp_bin0.map(i => part_bp_sizes.at(i)) #let part_bp_bin1_sizes = part_bp_bin1.map(i => part_bp_sizes.at(i)) #let part_bp_bin0_sum = part_bp_bin0_sizes.fold(0, (a, b) => a + b) @@ -18179,16 +18006,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(part_bp.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_bp) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_bp_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_bp_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#part_bp_sizes.map(str).join(", "))$ with total sum $S = #part_bp_total$, so a balanced partition requires each half to sum to $S / 2 = #part_bp_capacity$. + *Step 1 -- Source instance.* The canonical Partition instance has sizes $(#fmt-values(part_bp_sizes))$ with total sum $S = #part_bp_total$, so a balanced partition requires each half to sum to $S / 2 = #part_bp_capacity$. - *Step 2 -- Build the bin-packing instance.* The reduction copies each size into the item-size list and sets the bin capacity to $C = floor(S / 2) = #part_bp_capacity$ with $k = 2$ bins. The target instance has sizes $(#part_bp.target.instance.sizes.map(str).join(", "))$ and capacity $#part_bp_capacity$. No auxiliary variables are introduced, so the target has the same $#part_bp_n$ assignment coordinates as the source. + *Step 2 -- Build the bin-packing instance.* The reduction copies each size into the item-size list and sets the bin capacity to $C = floor(S / 2) = #part_bp_capacity$ with $k = 2$ bins. The target instance has sizes $(#fmt-values(part_bp.target.instance.sizes))$ and capacity $#part_bp_capacity$. No auxiliary variables are introduced, so the target has the same $#part_bp_n$ assignment coordinates as the source. - *Step 3 -- Verify the canonical witness.* The witness assigns each element to bin 0 or bin 1 via the binary vector $bold(b) = (#part_bp_sol.source_config.map(str).join(", "))$, which equals the target config $(#part_bp_sol.target_config.map(str).join(", "))$. Bin 0 receives elements $\{#part_bp_bin0.map(str).join(", ")\}$ with sizes $(#part_bp_bin0_sizes.map(str).join(", "))$ summing to $#part_bp_bin0_sum <= #part_bp_capacity$ #sym.checkmark. Bin 1 receives elements $\{#part_bp_bin1.map(str).join(", ")\}$ with sizes $(#part_bp_bin1_sizes.map(str).join(", "))$ summing to $#part_bp_bin1_sum <= #part_bp_capacity$ #sym.checkmark. Both bins fit within the capacity, and the total $#part_bp_bin0_sum + #part_bp_bin1_sum = #part_bp_total$ accounts for all items. + *Step 3 -- Verify the canonical witness.* The witness assigns each element to bin 0 or bin 1 via the binary vector $bold(b) = (#fmt-values(part_bp_sol.source_config))$, which equals the target config $(#fmt-values(part_bp_sol.target_config))$. Bin 0 receives elements $\{#fmt-values(part_bp_bin0)\}$ with sizes $(#fmt-values(part_bp_bin0_sizes))$ summing to $#part_bp_bin0_sum <= #part_bp_capacity$ #sym.checkmark. Bin 1 receives elements $\{#fmt-values(part_bp_bin1)\}$ with sizes $(#fmt-values(part_bp_bin1_sizes))$ summing to $#part_bp_bin1_sum <= #part_bp_capacity$ #sym.checkmark. Both bins fit within the capacity, and the total $#part_bp_bin0_sum + #part_bp_bin1_sum = #part_bp_total$ accounts for all items. *Multiplicity:* The fixture stores one canonical witness. This instance may admit other balanced partitions, but one witness suffices to demonstrate the reduction. ], @@ -18210,25 +18037,25 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(x3c_msp.source) + " -o x3c.json", - "pred reduce x3c.json --to " + target-spec(x3c_msp) + " -o bundle.json", + "pred reduce x3c.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate x3c.json --config " + x3c_msp_sol.source_config.map(str).join(","), + "pred evaluate x3c.json --config " + cli-config(x3c_msp_sol.source_config), ) *Step 1 -- Source instance.* The X3C instance has universe $X = {0, dots, #(x3c_msp.source.instance.universe_size - 1)}$ with $q = #(x3c_msp.source.instance.universe_size / 3)$ and $#x3c_msp.source.instance.subsets.len()$ candidate triples: #for (i, s) in x3c_msp.source.instance.subsets.enumerate() [ - $S_#i = {#s.map(str).join(", ")}$#if i < x3c_msp.source.instance.subsets.len() - 1 [, ] else [.] + $S_#i = {#fmt-values(s)}$#if i < x3c_msp.source.instance.subsets.len() - 1 [, ] else [.] ] - *Step 2 -- Construct the target.* The identity map copies each triple as a unit-weight set: $#x3c_msp.target.instance.sets.len()$ sets with weights $(#x3c_msp.target.instance.weights.map(str).join(", "))$. The target asks for a maximum packing of pairwise-disjoint sets. + *Step 2 -- Construct the target.* The identity map copies each triple as a unit-weight set: $#x3c_msp.target.instance.sets.len()$ sets with weights $(#fmt-values(x3c_msp.target.instance.weights))$. The target asks for a maximum packing of pairwise-disjoint sets. - *Step 3 -- Verify the canonical witness.* Source config $(#x3c_msp_sol.source_config.map(str).join(", "))$ selects subsets ${#x3c_msp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$: - #let selected = x3c_msp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) + *Step 3 -- Verify the canonical witness.* Source config $(#fmt-values(x3c_msp_sol.source_config))$ selects subsets ${#x3c_msp_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ")}$: + #let selected = x3c_msp_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #for idx in selected [ - $S_#idx = {#x3c_msp.source.instance.subsets.at(idx).map(str).join(", ")}$ ] These $#selected.len()$ triples are pairwise disjoint and cover all $#x3c_msp.source.instance.universe_size = 3 dot #(x3c_msp.source.instance.universe_size / 3)$ elements #sym.checkmark \\ - Target config is identical: $(#x3c_msp_sol.target_config.map(str).join(", "))$ — packing value $= #selected.len() = q$ #sym.checkmark + Target config is identical: $(#fmt-values(x3c_msp_sol.target_config))$ — packing value $= #selected.len() = q$ #sym.checkmark *Multiplicity:* The fixture stores one canonical witness. For this instance there are no other exact covers since every pair of triples that covers all 6 elements is unique. ], @@ -18250,20 +18077,20 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(x3c_mfdts.source) + " -o x3c.json", - "pred reduce x3c.json --to " + target-spec(x3c_mfdts) + " -o bundle.json", + "pred reduce x3c.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate x3c.json --config " + x3c_mfdts_sol.source_config.map(str).join(","), + "pred evaluate x3c.json --config " + cli-config(x3c_mfdts_sol.source_config), ) #let q = x3c_mfdts.source.instance.universe_size / 3 *Step 1 -- Source instance.* The X3C fixture has universe $U = {0, dots, #(x3c_mfdts.source.instance.universe_size - 1)}$ with $q = #q$ and triples #for (i, s) in x3c_mfdts.source.instance.subsets.enumerate() [ - $C_#i = {#s.map(str).join(", ")}$#if i < x3c_mfdts.source.instance.subsets.len() - 1 [, ] else [.] + $C_#i = {#fmt-values(s)}$#if i < x3c_mfdts.source.instance.subsets.len() - 1 [, ] else [.] ] - *Step 2 -- Build the fault-detection DAG.* Create one input vertex for each triple, one internal vertex for each universe element, and one shared output. The target therefore has $#x3c_mfdts.target.instance.num_vertices$ vertices, $#x3c_mfdts.target.instance.arcs.len()$ arcs, inputs ${#x3c_mfdts.target.instance.inputs.map(str).join(", ")}$, and output ${#x3c_mfdts.target.instance.outputs.map(str).join(", ")}$. Input $i_j$ connects to exactly the three internal vertices for elements in $C_j$, and every internal vertex connects to the shared output. + *Step 2 -- Build the fault-detection DAG.* Create one input vertex for each triple, one internal vertex for each universe element, and one shared output. The target therefore has $#x3c_mfdts.target.instance.num_vertices$ vertices, $#x3c_mfdts.target.instance.arcs.len()$ arcs, inputs ${#fmt-values(x3c_mfdts.target.instance.inputs)}$, and output ${#fmt-values(x3c_mfdts.target.instance.outputs)}$. Input $i_j$ connects to exactly the three internal vertices for elements in $C_j$, and every internal vertex connects to the shared output. - *Step 3 -- Verify the canonical witness.* The stored source configuration $(#x3c_mfdts_sol.source_config.map(str).join(", "))$ selects $C_0 = {#x3c_mfdts.source.instance.subsets.at(0).map(str).join(", ")}$ and $C_1 = {#x3c_mfdts.source.instance.subsets.at(1).map(str).join(", ")}$, which are disjoint and cover all six universe elements. The target configuration is identical: $(#x3c_mfdts_sol.target_config.map(str).join(", "))$. Pair $(0, #(x3c_mfdts.target.instance.outputs.at(0)))$ covers internal vertices ${0, 1, 2}$, pair $(1, #(x3c_mfdts.target.instance.outputs.at(0)))$ covers ${3, 4, 5}$, and together they cover every internal vertex with value $#q$ #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The stored source configuration $(#fmt-values(x3c_mfdts_sol.source_config))$ selects $C_0 = {#x3c_mfdts.source.instance.subsets.at(0).map(str).join(", ")}$ and $C_1 = {#x3c_mfdts.source.instance.subsets.at(1).map(str).join(", ")}$, which are disjoint and cover all six universe elements. The target configuration is identical: $(#fmt-values(x3c_mfdts_sol.target_config))$. Pair $(0, #(x3c_mfdts.target.instance.outputs.at(0)))$ covers internal vertices ${0, 1, 2}$, pair $(1, #(x3c_mfdts.target.instance.outputs.at(0)))$ covers ${3, 4, 5}$, and together they cover every internal vertex with value $#q$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. Any target witness of value $q$ selects exactly $q$ inputs, and since each selected pair covers only 3 internal vertices while there are $3q$ internal vertices overall, those $q$ neighborhoods must be pairwise disjoint and form an exact cover. ], @@ -18285,20 +18112,20 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(x3c_mas.source) + " -o x3c.json", - "pred reduce x3c.json --to " + target-spec(x3c_mas) + " -o bundle.json", + "pred reduce x3c.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate x3c.json --config " + x3c_mas_sol.source_config.map(str).join(","), + "pred evaluate x3c.json --config " + cli-config(x3c_mas_sol.source_config), ) #let x3c_mas_q = x3c_mas.source.instance.universe_size / 3 *Step 1 -- Source instance.* The X3C fixture has universe $U = {0, dots, #(x3c_mas.source.instance.universe_size - 1)}$ with $q = #x3c_mas_q$ and candidate triples #for (i, s) in x3c_mas.source.instance.subsets.enumerate() [ - $C_#i = {#s.map(str).join(", ")}$#if i < x3c_mas.source.instance.subsets.len() - 1 [, ] else [.] + $C_#i = {#fmt-values(s)}$#if i < x3c_mas.source.instance.subsets.len() - 1 [, ] else [.] ] *Step 2 -- Build the axiom system.* Create one element-sentence for each universe element and one set-sentence for each triple, so the target has $#x3c_mas.target.instance.num_sentences$ sentences and $#x3c_mas.target.instance.true_sentences.len()$ true sentences. Each triple contributes three forward implications and one backward implication, giving $#x3c_mas.target.instance.implications.len()$ implications total. The optimization instance itself stores only the axiom-system data; the X3C bound $q = #x3c_mas_q$ is checked externally against the optimum target value. - *Step 3 -- Verify the canonical witness.* The stored source config $(#x3c_mas_sol.source_config.map(str).join(", "))$ selects $C_3 = {#x3c_mas.source.instance.subsets.at(3).map(str).join(", ")}$ and $C_4 = {#x3c_mas.source.instance.subsets.at(4).map(str).join(", ")}$. These two triples are disjoint and cover all six universe elements #sym.checkmark. The target axiom vector $(#x3c_mas_sol.target_config.map(str).join(", "))$ selects exactly the set-sentence coordinates $#(x3c_mas.source.instance.universe_size + 3)$ and $#(x3c_mas.source.instance.universe_size + 4)$, namely $z_3$ and $z_4$. One closure round derives every element-sentence $e_0, dots, e_5$; then the backward rules derive the remaining set-sentences $z_0, z_1, z_2$, so the closure equals all $#x3c_mas.target.instance.true_sentences.len()$ true sentences. This witness therefore attains value $#x3c_mas_q$ #sym.checkmark, and extracting the chosen set-sentence coordinates recovers the exact cover. + *Step 3 -- Verify the canonical witness.* The stored source config $(#fmt-values(x3c_mas_sol.source_config))$ selects $C_3 = {#x3c_mas.source.instance.subsets.at(3).map(str).join(", ")}$ and $C_4 = {#x3c_mas.source.instance.subsets.at(4).map(str).join(", ")}$. These two triples are disjoint and cover all six universe elements #sym.checkmark. The target axiom vector $(#fmt-values(x3c_mas_sol.target_config))$ selects exactly the set-sentence coordinates $#(x3c_mas.source.instance.universe_size + 3)$ and $#(x3c_mas.source.instance.universe_size + 4)$, namely $z_3$ and $z_4$. One closure round derives every element-sentence $e_0, dots, e_5$; then the backward rules derive the remaining set-sentences $z_0, z_1, z_2$, so the closure equals all $#x3c_mas.target.instance.true_sentences.len()$ true sentences. This witness therefore attains value $#x3c_mas_q$ #sym.checkmark, and extracting the chosen set-sentence coordinates recovers the exact cover. *Multiplicity:* The fixture stores one canonical witness. Any target witness of value $q$ must select exactly $q$ set-sentences and no element-sentences, because each direct element axiom lowers the maximum possible element coverage by two. ], @@ -18334,9 +18161,9 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ss_part.source) + " -o subsetsum.json", - "pred reduce subsetsum.json --to " + target-spec(ss_part) + " -o bundle.json", + "pred reduce subsetsum.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate subsetsum.json --config " + ss_part_sol.source_config.map(str).join(","), + "pred evaluate subsetsum.json --config " + cli-config(ss_part_sol.source_config), ) #{ @@ -18345,11 +18172,11 @@ The following table shows concrete variable overhead for example instances, take let T = int(ss_part.source.instance.target) let d = calc.abs(sigma - 2 * T) [ - *Step 1 -- Source instance.* Subset Sum with sizes $(#sizes.map(str).join(", "))$ and target $T = #T$. Total $Sigma = #sigma$. + *Step 1 -- Source instance.* Subset Sum with sizes $(#fmt-values(sizes))$ and target $T = #T$. Total $Sigma = #sigma$. - *Step 2 -- Compute padding.* $Sigma = #sigma$, $2T = #(2 * T)$. Since $Sigma < 2T$, we have $d = 2T - Sigma = #d$. The Partition instance is $S union {d} = (#ss_part.target.instance.sizes.map(str).join(", "))$ with #ss_part.target.instance.sizes.len() elements. + *Step 2 -- Compute padding.* $Sigma = #sigma$, $2T = #(2 * T)$. Since $Sigma < 2T$, we have $d = 2T - Sigma = #d$. The Partition instance is $S union {d} = (#fmt-values(ss_part.target.instance.sizes))$ with #ss_part.target.instance.sizes.len() elements. - *Step 3 -- Verify a solution.* Source config $(#ss_part_sol.source_config.map(str).join(", "))$: selected elements $= {#ss_part_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(sizes.at(i))).join(", ")}$ sum to $#T = T$ #sym.checkmark. Target config $(#ss_part_sol.target_config.map(str).join(", "))$: side-0 sum $= #ss_part_sol.target_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => ss_part.target.instance.sizes.at(i)).sum()$, side-1 sum $= #ss_part_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => ss_part.target.instance.sizes.at(i)).sum()$ -- balanced #sym.checkmark. + *Step 3 -- Verify a solution.* Source config $(#fmt-values(ss_part_sol.source_config))$: selected elements $= {#ss_part_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(sizes.at(i))).join(", ")}$ sum to $#T = T$ #sym.checkmark. Target config $(#fmt-values(ss_part_sol.target_config))$: side-0 sum $= #ss_part_sol.target_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => ss_part.target.instance.sizes.at(i)).sum()$, side-1 sum $= #ss_part_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => ss_part.target.instance.sizes.at(i)).sum()$ -- balanced #sym.checkmark. ] } @@ -18382,7 +18209,7 @@ The following table shows concrete variable overhead for example instances, take #{ let sizes = ss_ik.source.instance.sizes.map(s => int(s)) let B = int(ss_ik.source.instance.target) - let chosen = ss_ik_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) + let chosen = ss_ik_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) let chosen_sum = chosen.map(i => sizes.at(i)).sum() [ #pred-commands( @@ -18392,11 +18219,11 @@ The following table shows concrete variable overhead for example instances, take "pred solve integer-knapsack.json", ) - *Step 1 -- Source instance.* The canonical Subset Sum instance has sizes $(#sizes.map(str).join(", "))$ and target $B = #B$. The stored witness $(#ss_ik_sol.source_config.map(str).join(", "))$ selects elements ${#chosen.map(str).join(", ")}$, whose values sum to $#chosen_sum = B$ #sym.checkmark. + *Step 1 -- Source instance.* The canonical Subset Sum instance has sizes $(#fmt-values(sizes))$ and target $B = #B$. The stored witness $(#fmt-values(ss_ik_sol.source_config))$ selects elements ${#fmt-values(chosen)}$, whose values sum to $#chosen_sum = B$ #sym.checkmark. - *Step 2 -- Build the target.* Copy each source size into both the size and value lists. The Integer Knapsack instance therefore has sizes $(#ss_ik.target.instance.sizes.map(str).join(", "))$, values $(#ss_ik.target.instance.values.map(str).join(", "))$, and the same capacity $B = #ss_ik.target.instance.capacity$. + *Step 2 -- Build the target.* Copy each source size into both the size and value lists. The Integer Knapsack instance therefore has sizes $(#fmt-values(ss_ik.target.instance.sizes))$, values $(#fmt-values(ss_ik.target.instance.values))$, and the same capacity $B = #ss_ik.target.instance.capacity$. - *Step 3 -- Verify the forward witness.* Reuse the same 0-1 vector as multiplicities: $(#ss_ik_sol.target_config.map(str).join(", "))$. Its total size is $#chosen_sum <= #ss_ik.target.instance.capacity$, and because size equals value coordinate-wise, its total value is also $#chosen_sum = B$ #sym.checkmark. + *Step 3 -- Verify the forward witness.* Reuse the same 0-1 vector as multiplicities: $(#fmt-values(ss_ik_sol.target_config))$. Its total size is $#chosen_sum <= #ss_ik.target.instance.capacity$, and because size equals value coordinate-wise, its total value is also $#chosen_sum = B$ #sym.checkmark. *Step 4 -- Backward gap.* For the source instance $A = {3}$ with target $B = 6$, Subset Sum is NO, but Integer Knapsack can set multiplicity $c_0 = 2$ and achieve total size/value $6$. This is why the catalog records the edge for proof topology only and disables all runtime reduction modes. ] @@ -18435,16 +18262,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(sat_nt.source) + " -o sat.json", - "pred reduce sat.json --to " + target-spec(sat_nt) + " -o bundle.json", + "pred reduce sat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate sat.json --config " + sat_nt_sol.source_config.map(str).join(","), + "pred evaluate sat.json --config " + cli-config(sat_nt_sol.source_config), ) *Step 1 -- Source instance.* CNF with $n = #sat_nt.source.instance.num_vars$ variables and $m = #sat-num-clauses(sat_nt.source.instance)$ clauses. *Step 2 -- Apply De Morgan.* Each clause $C_j = (l_1 or dots or l_k)$ becomes disjunct $D_j = (overline(l_1) and dots and overline(l_k))$. The Non-Tautology instance has #sat_nt.target.instance.disjuncts.len() disjuncts over #sat_nt.target.instance.num_vars variables. - *Step 3 -- Verify a solution.* Source config $(#sat_nt_sol.source_config.map(str).join(", "))$ satisfies the CNF. Target config $(#sat_nt_sol.target_config.map(str).join(", "))$ falsifies the DNF (same assignment). Variables are identical #sym.checkmark. + *Step 3 -- Verify a solution.* Source config $(#fmt-values(sat_nt_sol.source_config))$ satisfies the CNF. Target config $(#fmt-values(sat_nt_sol.target_config))$ falsifies the DNF (same assignment). Variables are identical #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ], @@ -18467,16 +18294,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(kc_pic.source) + " -o kcoloring.json", - "pred reduce kcoloring.json --to " + target-spec(kc_pic) + " -o bundle.json", + "pred reduce kcoloring.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate kcoloring.json --config " + kc_pic_sol.source_config.map(str).join(","), + "pred evaluate kcoloring.json --config " + cli-config(kc_pic_sol.source_config), ) *Step 1 -- Source instance.* Graph $G$ with $n = #graph-num-vertices(kc_pic.source.instance)$ vertices, $|E| = #graph-num-edges(kc_pic.source.instance)$ edges, $k = #kc_pic.source.instance.num_colors$ colors. *Step 2 -- Complement graph.* $overline(G)$ has the same $n = #graph-num-vertices(kc_pic.target.instance)$ vertices and $|overline(E)| = #graph-num-edges(kc_pic.target.instance)$ edges. Clique bound $K' = #kc_pic.target.instance.num_cliques$. - *Step 3 -- Verify a solution.* Source coloring $(#kc_pic_sol.source_config.map(str).join(", "))$. Target partition $(#kc_pic_sol.target_config.map(str).join(", "))$ -- each color class is a clique in $overline(G)$ #sym.checkmark. + *Step 3 -- Verify a solution.* Source coloring $(#fmt-values(kc_pic_sol.source_config))$. Target partition $(#fmt-values(kc_pic_sol.target_config))$ -- each color class is a clique in $overline(G)$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ], @@ -18503,29 +18330,28 @@ The following table shows concrete variable overhead for example instances, take // 5. KColoring → BicliqueCover (#1058) #reduction-rule("KColoring", "BicliqueCover")[ - Self-contained gadget @karp1972 @garey1979 @orlin1977 building a bipartite graph $H$ on $4 n$ vertices with rank $n + q$. Each source vertex $v$ contributes two left vertices $a_v, g_v$ and two right vertices $b_v, h_v$. Guard-anchor edges $(g_v, h_v)$ force $n$ bicliques to be spent on per-vertex guards, leaving at most $q$ remaining bicliques to cover the diagonal edges $(a_v, b_v)$, which behave as color classes under the classical sub-biclique semantics of BicliqueCover. + A guard construction embeds coloring feasibility into the finite-feasibility of BicliqueCover. The target uses the classical sub-biclique semantics described by @chandran_et_al:LIPIcs.IPEC.2016.11; the construction and proof below are self-contained. The source value is #raw("Or"), and the target value is #raw("Min"): any feasible target cover suffices, regardless of its total membership cost. ][ - _Construction._ Let $(G = (V, E), q)$ be the source instance with $n = |V|$. Build $H = (L union.sq R, F)$ with $L = {a_v, g_v : v in V}$ and $R = {b_v, h_v : v in V}$ (so $|L| = |R| = 2 n$). Set the BicliqueCover rank to $k = n + q$. The edge set $F$ consists of: - $ - "diagonal:" quad & (a_v, b_v) && quad forall v in V \ - "compatibility:" quad & (a_u, b_v) && quad forall u != v "with" {u, v} in.not E \ - "guard-anchor:" quad & (a_v, h_v), (g_v, h_v) && quad forall v in V \ - "guard-compat:" quad & (g_v, b_w) && quad forall v != w "with" {v, w} in.not E. - $ - The total edge count is $n + 2(n(n-1) - 2m) + 2n = 2 n (n - 1) - 4 m + 3 n$ where $m = |E|$. + _Construction._ Let the source be $(G=(V,E),q)$ with $n=|V|$, and set $q'=min(q,n)$. A coloring uses at most $n$ distinct labels, so relabeling used colors preserves feasibility with $q'$ colors, including $n=0$ and $q=0$. If $G$ has a self-loop, return a target with one vertex on each side, one edge, and rank zero. This is a fixed NO instance. + + Otherwise introduce left vertices $a_v,g_v$ and right vertices $b_v,h_v$ for every $v in V$. Set rank $k=n+q'$. Include the diagonal edges $(a_v,b_v)$, the guard edges $(a_v,h_v),(g_v,h_v)$, and, for every ordered pair of distinct nonadjacent vertices $u,v$, both $(a_u,b_v)$ and $(g_u,b_v)$. Repeated and reversed source edges express the same adjacency constraint. With $d$ distinct source edges, the target has $2n$ vertices per side and $2n^2+n-4d$ edges. The empty source produces an empty target of rank zero, a YES instance. Uniform upper bounds, including the loop sentinel, are $2n+1$ vertices per side, $4n+2$ total vertices, $2n^2+n+1$ edges, and rank $2n$. + + _Correctness._ ($arrow.r.double$) Relabel a proper source coloring to use at most $q'$ labels. For each vertex $v$, use the guard biclique with left side ${a_v,g_v}$ and right side ${h_v} union {b_w: w != v, {v,w} in.not E}$. For each used color class $C$, use the biclique with left side ${a_v:v in C}$ and right side ${b_v:v in C}$. Color classes are independent, so these are sub-bicliques. The guard bicliques cover both kinds of compatibility edges and all guard edges; the color bicliques cover every diagonal edge. Pad with empty rows to obtain exactly $k$ rows. + + ($arrow.l.double$) Distinct guard-anchor edges $(g_v,h_v)$ cannot share a biclique, since $(g_u,h_v)$ is absent for $u != v$. A biclique containing an anchor cannot cover any diagonal $(a_u,b_u)$: when $u=v$, $(g_v,b_v)$ is absent; when $u != v$, $(a_u,h_v)$ is absent. Thus at least $n$ rows are occupied by anchors, leaving at most $q'$ rows covering diagonals. For each vertex choose the first row covering its diagonal, and compact these row identifiers into colors. Distinct source vertices assigned the same row require both cross edges in the target, hence are nonadjacent. This yields a proper coloring using at most $q'<=q$ colors. A self-loop makes the source infeasible for every $q$, agreeing with the fixed NO target. - _Correctness._ ($arrow.r.double$) Given a proper $q$-coloring of $G$, emit $n$ guard bicliques $G_v = ({a_v, g_v}, {h_v} union {b_w : w != v, {v, w} in.not E})$ and one color biclique $C_(c) = ({a_v : v "has color" c}, {b_v : v "has color" c})$ per color $c$. Each color class is an independent set, so all required compatibility edges exist and the color bicliques are valid sub-bicliques. Guard bicliques cover all guard-anchor and guard-compat edges; color bicliques cover the diagonal edges. ($arrow.l.double$) In any biclique cover, each guard-anchor edge $(g_v, h_v)$ must lie in its own biclique because no cross edge $(g_u, h_v)$ exists for $u != v$. A biclique containing $(g_v, h_v)$ cannot cover any diagonal edge $(a_u, b_u)$: if $u = v$ then $(g_v, b_v) in.not F$, and if $u != v$ then $(a_u, h_v) in.not F$. Hence at least $n$ bicliques are spent on guards and at most $q$ remain to cover all $n$ diagonal edges $(a_v, b_v)$. Vertices $u, v$ sharing such a diagonal-covering biclique require both $(a_u, b_v)$ and $(a_v, b_u)$ in $F$, which forces $u != v$ and ${u, v} in.not E$. Compacting the at most $q$ diagonal bicliques into colors $0, dots, q - 1$ gives a proper $q$-coloring of $G$. + _Solution extraction._ Validate the complete target configuration once and reject #raw("Min(None)") before decoding. Apply the diagonal-row mapping above to every feasible cover, including nonoptimal covers. The empty source yields an empty coloring. - _Solution extraction._ For each source vertex $v$, locate any biclique $r$ that contains both $a_v$ and $b_v$. Compact the distinct diagonal-covering biclique indices into colors $0, dots, q - 1$ in first-seen order and assign each $v$ its compacted color. + _Native numeric domain._ Normalizing $q$ ensures $k<=2n$, even for the largest runtime color count. Native source node allocation bounds make $4n$ and all vertex indices fit #raw("usize"). On a 64-bit target let $I$ be #raw("isize::MAX"), $m$ the number of stored source edges, and $F$ the number of target edges. Each edge record occupies 16 bytes, so successful native allocations imply $F+4m<=5I/16$. Since $d<=m$ and $F=2n^2+n-4d$, this bounds $2n^2+n<=5I/16$. For $n>=2$, each valid target biclique has at most $2n$ members: a row with two $h$ vertices has no left vertices; one $h_v$ restricts the left side to ${a_v,g_v}$; and a row with no $h$ cannot contain both $g_v$ and $b_v$. Therefore every feasible cover has at most $4n^2=n>=2$, $ell<=m/2$ and $ceil(log_2 m)<=(m+1)/2$. Hence rank is at most $4m+9$ and each side has at most $17m/2+9$ vertices. The Boolean cell count is at most $68m^2+225m+162$. The target contains $9m(m-1)$ edges between distinct clause gadgets. Its edge-vector allocation therefore bounds this cell count by $I$ for $m>=6$ on 64-bit targets and $m>=75$ on 32-bit targets; the remaining small cases have bounds 2987 and 389180. All indices, dimensions, and membership counts consequently fit the native #raw("usize") and #raw("i64") types. Appearing-variable compaction also bounds new literal indices by actual clause storage. Allocation and solver failures remain execution or resource failures, not proofs of infeasibility. + ] #let clustering_ilp = load-example("Clustering", "ILP") #let clustering_ilp_sol = clustering_ilp.solutions.at(0) #reduction-rule("Clustering", "ILP", example: true, - example-caption: [4 elements, $K = 2$, $B = 1$ $arrow.r$ ILP with #clustering_ilp.target.instance.num_vars variables and #clustering_ilp.target.instance.constraints.len() constraints], + example-caption: [4 elements, $K = 2$, $B = 1$ $arrow.r$ ILP with #clustering_ilp.target.instance.variables.len() variables and #clustering_ilp.target.instance.constraints.len() constraints], extra: [ #pred-commands( "pred create --example " + problem-spec(clustering_ilp.source) + " -o clustering.json", - "pred reduce clustering.json --to " + target-spec(clustering_ilp) + " -o bundle.json", + "pred reduce clustering.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate clustering.json --config " + clustering_ilp_sol.source_config.map(str).join(","), + "pred evaluate clustering.json --config " + cli-config(clustering_ilp_sol.source_config), ) #{ @@ -18569,11 +18402,11 @@ The following table shows concrete variable overhead for example instances, take let distances = clustering_ilp.source.instance.distances let source-config = clustering_ilp_sol.source_config [ - *Step 1 -- Source instance.* The canonical source has $n = #n$ elements, cluster bound $K = #k$, and diameter bound $B = #b$. Its distance rows are #distances.map(row => "(" + row.map(str).join(", ") + ")").join("; "), so the only pairs above the bound are $(0, 2)$, $(0, 3)$, $(1, 2)$, and $(1, 3)$. + *Step 1 -- Source instance.* The canonical source has $n = #n$ elements, cluster bound $K = #k$, and diameter bound $B = #b$. Its distance rows are #distances.map(row => "(" + fmt-values(row) + ")").join("; "), so the only pairs above the bound are $(0, 2)$, $(0, 3)$, $(1, 2)$, and $(1, 3)$. - *Step 2 -- Build the ILP.* Introduce one binary variable $x_(i,c)$ for each element-cluster pair, giving $n K = #(n * k)$ variables. The $n = #n$ assignment equalities $sum_c x_(i,c) = 1$ force every element into exactly one cluster, and the four violating pairs contribute $4 dot K = #(4 * k)$ conflict inequalities. The stored target therefore has #clustering_ilp.target.instance.num_vars variables and #clustering_ilp.target.instance.constraints.len() constraints. + *Step 2 -- Build the ILP.* Introduce one binary variable $x_(i,c)$ for each element-cluster pair, giving $n K = #(n * k)$ variables. The $n = #n$ assignment equalities $sum_c x_(i,c) = 1$ force every element into exactly one cluster, and the four violating pairs contribute $4 dot K = #(4 * k)$ conflict inequalities. The stored target therefore has #clustering_ilp.target.instance.variables.len() variables and #clustering_ilp.target.instance.constraints.len() constraints. - *Step 3 -- Verify the canonical witness.* The stored ILP vector is $(#clustering_ilp_sol.target_config.map(str).join(", "))$. Reading each block of $K = #k$ variables yields the clustering $(#source-config.map(str).join(", "))$, so cluster 0 contains elements ${#source-config.enumerate().filter(((i, c)) => c == 0).map(((i, c)) => str(i)).join(", ")}$ and cluster 1 contains elements ${#source-config.enumerate().filter(((i, c)) => c == 1).map(((i, c)) => str(i)).join(", ")}$. The only within-cluster distances are $d(0,1) = #distances.at(0).at(1)$ and $d(2,3) = #distances.at(2).at(3)$, both at most $B$ #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The stored ILP vector is $(#fmt-values(clustering_ilp_sol.target_config))$. Reading each block of $K = #k$ variables yields the clustering $(#fmt-values(source-config))$, so cluster 0 contains elements ${#source-config.enumerate().filter(((i, c)) => c == 0).map(((i, c)) => str(i)).join(", ")}$ and cluster 1 contains elements ${#source-config.enumerate().filter(((i, c)) => c == 1).map(((i, c)) => str(i)).join(", ")}$. The only within-cluster distances are $d(0,1) = #distances.at(0).at(1)$ and $d(2,3) = #distances.at(2).at(3)$, both at most $B$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. Swapping the two cluster labels gives an equivalent second witness because the ILP distinguishes clusters only by index. ] @@ -18602,35 +18435,31 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(pic_mcbc.source) + " -o partition-into-cliques.json", - "pred reduce partition-into-cliques.json --to " + target-spec(pic_mcbc) + " -o bundle.json", + "pred reduce partition-into-cliques.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition-into-cliques.json --config " + pic_mcbc_sol.source_config.map(str).join(","), + "pred evaluate partition-into-cliques.json --config " + cli-config(pic_mcbc_sol.source_config), ) - *Step 1 -- Source instance.* Graph $G$ with $n = #graph-num-vertices(pic_mcbc.source.instance)$ vertices, $m = #graph-num-edges(pic_mcbc.source.instance)$ edge, and clique bound $K = #pic_mcbc.source.instance.num_cliques$. The stored partition witness is $(#pic_mcbc_sol.source_config.map(str).join(", "))$, namely the cliques ${0,1}$ and ${2}$. + *Step 1 -- Source instance.* Graph $G$ with $n = #graph-num-vertices(pic_mcbc.source.instance)$ vertices, $m = #graph-num-edges(pic_mcbc.source.instance)$ edge, and clique bound $K = #pic_mcbc.source.instance.num_cliques$. The stored partition witness is $(#fmt-values(pic_mcbc_sol.source_config))$, namely the cliques ${0,1}$ and ${2}$. *Step 2 -- Orlin construction.* The target graph has $#graph-num-vertices(pic_mcbc.target.instance)$ vertices and $#graph-num-edges(pic_mcbc.target.instance)$ edges. Because the source has two directed edge copies, the construction adds the gadgets $Q_(0,1)$ and $Q_(1,0)$, plus the side cliques $L^*$ and $R^*$. The threshold is $K' = K + 2m + 2 = #(pic_mcbc.source.instance.num_cliques + 2 * graph-num-edges(pic_mcbc.source.instance) + 2)$. - *Step 3 -- Verify the witness.* The target witness labels $#pic_mcbc_sol.target_config.len()$ target edges with 6 clique IDs, corresponding to $D_1 = {x_0, x_1, y_0, y_1}$, $D_2 = {x_2, y_2}$, $Q_(0,1)$, $Q_(1,0)$, $L^*$, and $R^*$. Reading only the labels on the matching edges $x_i y_i$ recovers the source partition $(#pic_mcbc_sol.source_config.map(str).join(", "))$ #sym.checkmark. + *Step 3 -- Verify the witness.* The target witness labels $#pic_mcbc_sol.target_config.len()$ target edges with 6 clique IDs, corresponding to $D_1 = {x_0, x_1, y_0, y_1}$, $D_2 = {x_2, y_2}$, $Q_(0,1)$, $Q_(1,0)$, $L^*$, and $R^*$. Reading only the labels on the matching edges $x_i y_i$ recovers the source partition $(#fmt-values(pic_mcbc_sol.source_config))$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. Any permutation of the six target clique labels is equivalent. ], )[ - This $O((n + 2m)^2)$ reduction @garey1979[GT17] @orlin1977 @kouStockmeyerWong1978 inlines Orlin's vertex-clique-cover to edge-clique-cover construction. Each source vertex $v_i$ becomes left/right copies $x_i$ and $y_i$, each directed source edge contributes a 4-vertex gadget, and two side cliques account for the additive $2m + 2$ slack. The target graph has an edge-clique cover of size at most $K + 2m + 2$ if and only if the source graph admits a partition into at most $K$ cliques. + This reduction @garey1979[GT17] @orlin1977 @kouStockmeyerWong1978 converts vertex clique partition to edge clique cover. It uses the actual distinct non-loop adjacency pairs, and includes a private vertex on each side so that the two forced side cliques also exist for an empty source. No source endpoint restriction is added. ][ - _Construction._ Let $(G = (V, E), K)$ be a Partition Into Cliques instance with $V = {v_1, dots, v_n}$ and $m = |E|$. Define the directed-edge index set $A = {(i,j) : i != j and {v_i,v_j} in E}$, so $|A| = 2m$. Create vertices $x_i, y_i$ for each source vertex $v_i$, gadget vertices $a_(i,j), b_(i,j)$ for each $(i,j) in A$, and two special vertices $z_L, z_R$. Let $L = {x_i : 1 <= i <= n} union {a_(i,j) : (i,j) in A}$ and $R = {y_i : 1 <= i <= n} union {b_(i,j) : (i,j) in A}$. Make $L$ a clique and $R$ a clique, join $z_L$ to every vertex of $L$ and $z_R$ to every vertex of $R$, add each matching edge $x_i y_i$, and for every $(i,j) in A$ add the four cross edges $x_i y_j$, $x_i b_(i,j)$, $a_(i,j) y_j$, and $a_(i,j) b_(i,j)$. Output the resulting Minimum Covering by Cliques instance. It has - $ - |V(H)| = 2n + 4m + 2, quad |E(H)| = (n + 2m)^2 + 2n + 10m, - $ - and threshold $K' = K + 2m + 2$. + _Construction._ Let $n$ be the source vertex count, $m$ its stored edge count, and $K$ its clique bound. Let $A = {(i,j) : i != j and {i,j} in E}$ and $q = |A|$. Source self-loops and repeated edges do not change $A$ or source clique feasibility; $q <= 2m$. Create copies $x_i,y_i$, gadget vertices $a_t,b_t$ for each pair $t=(i,j)$, anchors $z_L,z_R$, and private vertices $d_L,d_R$. Set $L={x_i} union {a_t} union {d_L}$ and $R={y_i} union {b_t} union {d_R}$. Make $L$ and $R$ cliques. Connect each anchor to its entire side. Add matching edges $x_i y_i$ and, for every $t=(i,j)$, the cross edges $x_i y_j$, $x_i b_t$, $a_t y_j$, and $a_t b_t$. The threshold is $B=min(K,n)+q+2$. - _Correctness._ ($arrow.r.double$) Suppose $G$ is partitioned into cliques $C_1, dots, C_t$ with $t <= K$. For each source clique $C_r$, define $D_r = {x_i, y_i : v_i in C_r}$. For each $(i,j) in A$, define $Q_(i,j) = {x_i, a_(i,j), b_(i,j), y_j}$. Also define $L^* = L union {z_L}$ and $R^* = R union {z_R}$. Every $D_r$ is a clique: if $v_i, v_j in C_r$ with $i != j$, then ${v_i,v_j} in E$, so the construction includes both cross edges $x_i y_j$ and $x_j y_i$. The family ${D_1, dots, D_t} union {Q_(i,j) : (i,j) in A} union {L^*, R^*}$ therefore covers every target edge, using at most $K + 2m + 2$ cliques. + _Correctness, forward._ Given a source partition into $c$ cliques $C$, cover the target with $D_C={x_i,y_i : i in C}$ for each block, $Q_t={x_i,a_t,b_t,y_j}$ for each pair $t=(i,j)$, and the two cliques $L union {z_L}$, $R union {z_R}$. Source adjacency supplies the cross edges in every $D_C$. This family covers every target edge using $c+q+2$ cliques, including when $n=0$. - ($arrow.l.double$) Conversely, suppose $H$ has an edge-clique cover with at most $K + 2m + 2$ cliques. Each gadget edge $a_(i,j) b_(i,j)$ belongs to the unique maximal clique $Q_(i,j)$, so covering all $2m$ such edges requires at least $2m$ distinct cliques. Likewise, some clique must contain $z_L$ and some clique must contain $z_R$, and neither of those cliques can contain a matching edge $x_i y_i$. Hence at most $K$ cliques remain available for the matching edges. If two matching edges $x_i y_i$ and $x_j y_j$ lie in the same target clique, that clique contains the four vertices $x_i, y_i, x_j, y_j$, so in particular $x_i y_j$ is an edge of $H$; by construction this implies ${v_i,v_j} in E$. Therefore the source vertices whose matching edges share one target-clique label form a clique of $G$. Grouping each $v_i$ by the label used on $x_i y_i$ yields a partition of $V$ into at most $K$ cliques. + _Correctness, backward._ The edge $a_t b_t$ belongs to the unique maximal clique $Q_t$, since $a_t$ has only $b_t,y_j$ as right neighbors and $b_t$ only $a_t,x_i$ as left neighbors. No two such forced edges share a cover label, and none can share a label with a matching edge because $i != j$. The edges $z_L d_L$ and $z_R d_R$ force two further labels unavailable to gadget or matching edges. Thus $q+2$ labels are reserved in every feasible cover. Matching edges sharing a remaining label imply pairwise source adjacency, giving a source clique partition. Consequently the exact target optimum is $q+2+c$, where $c$ is the minimum source clique partition size and $c=0$ for the empty graph. Source satisfaction is therefore equivalent to target optimum at most $B$. - _Variable mapping._ The source witness labels source vertices by clique. The target witness labels target edges by the covering clique that contains them. + _Extraction._ Evaluate the target once through the formal validator, and reject infeasibility or a value exceeding $B$. Read matching-edge labels in source vertex order and compress their equality classes. The backward proof guarantees at most $min(K,n)$ source cliques. Aggregate extraction applies the same finite-value threshold. This also handles serialized bounds $K=0$ or $K>n$ without reconstructing a more restrictive source constructor. - _Solution extraction._ Inspect the label assigned to each matching edge $x_i y_i$. Compress the distinct matching-edge labels to $0, dots, k-1$ and assign source vertex $v_i$ to the compressed label of its matching edge. The previous paragraph proves that these label classes are source cliques, and the forced gadget/side cliques guarantee $k <= K$ whenever the target cover has size at most $K + 2m + 2$. + _Size and arithmetic._ The exact counts are $|V(H)|=2n+2q+4$ and $|E(H)|=(n+q)^2+4n+7q+2$. Substituting $q<=2m$ gives the registered upper bounds. Checked layout counts and conversion of the edge count to `i64` ensure finite cover values are representable; the threshold is checked separately. Such representation failures are errors, not source NO answers. ] #let mcbc_migb = load-example("MinimumCoveringByCliques", "MinimumIntersectionGraphBasis") @@ -18641,11 +18470,11 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(mcbc_migb.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(mcbc_migb) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + mcbc_migb_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(mcbc_migb_sol.source_config), ) - Source clique labels $(#mcbc_migb_sol.source_config.map(str).join(", "))$, target intersection witness $(#mcbc_migb_sol.target_config.map(str).join(", "))$. + Source clique labels $(#fmt-values(mcbc_migb_sol.source_config))$, target intersection witness $(#fmt-values(mcbc_migb_sol.target_config))$. ], )[ This $O(n + m)$ identity reduction @garey1979[GT59] @erdosgoodmanposa1966 @kouStockmeyerWong1978 keeps the graph unchanged and reinterprets the objective. The minimum number of cliques covering all edges of $G$ equals the minimum universe size of an intersection representation of $G$, so the two optimization problems are equivalent reformulations. @@ -18668,40 +18497,43 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_ker.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_ker) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_ker_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_ker_sol.source_config), ) #{ let n = ksat_ker.source.instance.num_vars let m = sat-num-clauses(ksat_ker.source.instance) - let selected = ksat_ker_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) + let selected = ksat_ker_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) let literal-selected = selected.filter(v => v < 2 * n) let clause-selected = selected.filter(v => v >= 2 * n) let second-clause-base = 2 * n + 3 let last-literal-vertex = literal-selected.at(literal-selected.len() - 1) [ - *Step 1 -- Source instance.* 3-SAT with $n = #n$ variables and $m = #m$ clauses. The canonical satisfying assignment is $(#ksat_ker_sol.source_config.map(str).join(", "))$. + *Step 1 -- Source instance.* 3-SAT with $n = #n$ variables and $m = #m$ clauses. The canonical satisfying assignment is $(#fmt-values(ksat_ker_sol.source_config))$. *Step 2 -- Construct the digraph.* Variable gadgets contribute $2n = #(2 * n)$ literal vertices and $2n = #(2 * n)$ digon arcs. Clause gadgets contribute $3m = #(3 * m)$ clause vertices, $3m = #(3 * m)$ cycle arcs, and $3m = #(3 * m)$ literal arcs. Total: $#ksat_ker.target.instance.graph.num_vertices$ vertices and $#ksat_ker.target.instance.graph.arcs.len()$ arcs $= 2n + 6m$. - *Step 3 -- Verify the canonical witness.* The target kernel selects literal vertices ${#literal-selected.map(str).join(", ")}$ and clause vertices ${#clause-selected.map(str).join(", ")}$. Here ${#literal-selected.map(str).join(", ")}$ encode $(x_1, x_2, x_3) = (#ksat_ker_sol.source_config.map(str).join(", "))$, and the extra clause vertex $#(second-clause-base + 1)$ is needed in the second clause gadget: vertex $#second-clause-base$ is absorbed by arc $(#second-clause-base, #(second-clause-base + 1))$, while vertex $#(second-clause-base + 2)$ is absorbed by its literal arc to vertex $#last-literal-vertex$ #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The target kernel selects literal vertices ${#fmt-values(literal-selected)}$ and clause vertices ${#fmt-values(clause-selected)}$. Here ${#fmt-values(literal-selected)}$ encode $(x_1, x_2, x_3) = (#fmt-values(ksat_ker_sol.source_config))$, and the extra clause vertex $#(second-clause-base + 1)$ is needed in the second clause gadget: vertex $#second-clause-base$ is absorbed by arc $(#second-clause-base, #(second-clause-base + 1))$, while vertex $#(second-clause-base + 2)$ is absorbed by its literal arc to vertex $#last-literal-vertex$ #sym.checkmark. ] } *Multiplicity:* The fixture stores one canonical witness. ], )[ - This $O(n + m)$ reduction @garey1979 constructs a directed graph with $2n + 3m$ vertices and $2n + 6m$ arcs. Each variable contributes a digon on its positive and negative literal vertices, and each 3-clause contributes a directed 3-cycle whose three clause vertices point to the corresponding literal vertices. The implemented kernels may contain clause vertices as well as literal vertices. + This reduction uses variable digons and clause three-cycles, following the kernel hardness approach of @chvatal1973. With $a$ appearing variables, $m$ clauses and $L$ literal occurrences, the target has $2a + 3m$ vertices and $2a + 3m + L$ arcs. These are bounded by $2n + 3m$ and $2n + 6m$ on the native source domain. Each occurrence has its own clause vertex; kernels may include clause vertices as well as literals. ][ - _Construction._ Let $phi = C_1 and dots and C_m$ be a 3-SAT instance on variables $x_1, dots, x_n$. For each variable $x_i$, create two literal vertices $p_i$ and $n_i$ with arcs $(p_i, n_i)$ and $(n_i, p_i)$. For each clause $C_j = (ell_(j,0) or ell_(j,1) or ell_(j,2))$, create clause vertices $c_(j,0), c_(j,1), c_(j,2)$ with cycle arcs $(c_(j,0), c_(j,1))$, $(c_(j,1), c_(j,2))$, and $(c_(j,2), c_(j,0))$. Add one literal arc $(c_(j,t), v(ell_(j,t)))$ from each clause vertex to the literal vertex representing its own literal. Thus each clause contributes 3 cycle arcs and 3 literal arcs, for a total of $2n + 6m$ arcs. + _Construction._ Collect the sorted original indices $J$ of appearing variables, with $a = |J|$. For each compact variable $i$, create literal vertices $p_i, n_i$ and both digon arcs. Each native clause has zero through three literals. Create three vertices $c_(j,0), c_(j,1), c_(j,2)$ and the directed cycle in that order, then add $(c_(j,t), v(ell_(j,t)))$ only for its actual literal positions. Repeated literals keep separate occurrence vertices. An empty clause therefore produces a three-cycle with no outgoing literal arcs, which has no kernel; an empty formula produces an empty graph, whose empty selection is a kernel. No padding is needed. + + _Correctness._ ($arrow.r.double$) Given a satisfying source assignment, choose its true literal in each digon. Let $t_r$ indicate whether the literal at clause position $r$ is selected, with missing positions set to false. Select clause vertex $r$ exactly when $not t_r and t_((r+1) mod 3)$. A selected vertex points to no selected literal, and its successor cannot be selected, so independence holds. An unselected vertex with $t_r$ true is absorbed through its literal arc. Otherwise both $t_r$ and $t_((r+1) mod 3)$ are false. Since the clause is satisfied, $t_((r+2) mod 3)$ is true; its cycle successor is therefore selected and absorbs it. Unselected literal vertices are absorbed by their digon partners. + + ($arrow.l.double$) Each literal vertex has only its digon partner as an outgoing neighbor. Independence and absorption force exactly one selected endpoint per variable. If a clause has no selected literal, absorption must occur entirely within its directed three-cycle. Independence permits at most one selected cycle vertex, which cannot absorb both others. This contradiction proves that every clause is satisfied, including the native short-clause semantics. This proof does not require distinct variables or a particular literal order. - _Correctness._ ($arrow.r.double$) Let $alpha$ be a satisfying assignment. Put into $K$ exactly one literal vertex from each digon: $p_i$ if $alpha(x_i) = "true"$ and $n_i$ otherwise. For each clause $C_j$, additionally put $c_(j,t)$ into $K$ exactly when $ell_(j,t)$ is false and $ell_(j,(t+1) mod 3)$ is true. This never creates an arc inside $K$: each selected clause vertex points to a false literal vertex, and two adjacent clause vertices cannot both satisfy the selection rule. Every unselected literal vertex is absorbed by its digon partner. For an unselected clause vertex $c_(j,t)$, either $ell_(j,t)$ is true, so its literal arc hits the selected literal vertex, or $ell_(j,t)$ is false. In the latter case $c_(j,t)$ was not selected, so $ell_(j,(t+1) mod 3)$ is also false; because the clause is satisfied, $ell_(j,(t+2) mod 3)$ is true, hence $c_(j,(t+1) mod 3)$ was selected and absorbs $c_(j,t)$ along the cycle. + _Solution extraction._ Validate the target selection once using the formal target evaluator and reject $"Or(false)"$. Initialize a length-$n$ false vector and restore each original position $J_i$ from the selection of $p_i$. Unused variables remain false; selected clause vertices are ignored. The concrete value types are $"Or" arrow.r "Or"$. - ($arrow.l.double$) Let $K$ be a kernel of the constructed digraph. In each variable digon, at most one literal vertex can lie in $K$ by independence, and at least one must lie in $K$ to absorb the other endpoint; so each digon contributes exactly one selected literal vertex. Set $alpha(x_i) = "true"$ iff $p_i in K$. Now fix a clause gadget with cycle $(c_(j,0), c_(j,1), c_(j,2))$. If none of its three literal vertices were selected, then the only possible absorbers for $c_(j,0), c_(j,1), c_(j,2)$ would be the cycle successors. Independence allows at most one clause vertex in $K$, but one selected vertex on a directed 3-cycle cannot absorb the other two, contradiction. Therefore every clause has at least one selected literal vertex, so $alpha$ satisfies every clause. + _Bounds and native arithmetic._ Since $a <= L <= 3m$, the target has at most $9m$ vertices and $12m$ arcs. The registered relations use upper bounds in the declared source parameters, covering sparse variables and short clauses. Counts are checked before allocation, and every vertex index is bounded by the checked total. A successfully allocated native clause vector also bounds $m$ by its element layout and the address space; original sparse indices are never doubled. Allocation limits do not represent mathematical infeasibility. - _Solution extraction._ Read only the positive literal vertices: $alpha(x_i) = 1$ iff the even-indexed vertex for $x_i$ is in the kernel. Any selected clause vertices are ignored during extraction. ] // 7. HamiltonianPath → DegreeConstrainedSpanningTree (#911) @@ -18713,16 +18545,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hp_dcst.source) + " -o hampath.json", - "pred reduce hampath.json --to " + target-spec(hp_dcst) + " -o bundle.json", + "pred reduce hampath.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hampath.json --config " + hp_dcst_sol.source_config.map(str).join(","), + "pred evaluate hampath.json --config " + cli-config(hp_dcst_sol.source_config), ) *Step 1 -- Source instance.* Graph $G$ with $n = #graph-num-vertices(hp_dcst.source.instance)$ vertices and $|E| = #graph-num-edges(hp_dcst.source.instance)$ edges. *Step 2 -- Identity reduction.* Target graph is identical: $n = #graph-num-vertices(hp_dcst.target.instance)$ vertices, $|E| = #graph-num-edges(hp_dcst.target.instance)$ edges, degree bound $K = #hp_dcst.target.instance.max_degree$. - *Step 3 -- Verify a solution.* Hamiltonian path visits vertices in order $(#hp_dcst_sol.source_config.map(str).join(", "))$. The corresponding spanning tree selects #hp_dcst_sol.target_config.filter(x => x == 1).len() edges (all with max degree $<= 2$) #sym.checkmark. + *Step 3 -- Verify a solution.* Hamiltonian path visits vertices in order $(#fmt-values(hp_dcst_sol.source_config))$. The corresponding spanning tree selects #hp_dcst_sol.target_config.filter(x => x).len() edges (all with max degree $<= 2$) #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ], @@ -18745,21 +18577,21 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(nae_ss.source) + " -o naesat.json", - "pred reduce naesat.json --to " + target-spec(nae_ss) + " -o bundle.json", + "pred reduce naesat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate naesat.json --config " + nae_ss_sol.source_config.map(str).join(","), + "pred evaluate naesat.json --config " + cli-config(nae_ss_sol.source_config), ) #{ let colors = nae_ss_sol.target_config [ - *Step 1 -- Source instance.* The fixture has clauses $C_1 = (x_1 or overline(x_2) or x_3)$ and $C_2 = (overline(x_1) or x_2 or overline(x_3))$. The canonical NAE assignment is $(#nae_ss_sol.source_config.map(str).join(", "))$, so the two clauses evaluate to $(1, 0, 1)$ and $(0, 1, 0)$ respectively. + *Step 1 -- Source instance.* The fixture has clauses $C_1 = (x_1 or overline(x_2) or x_3)$ and $C_2 = (overline(x_1) or x_2 or overline(x_3))$. The canonical NAE assignment is $(#fmt-values(nae_ss_sol.source_config))$, so the two clauses evaluate to $(1, 0, 1)$ and $(0, 1, 0)$ respectively. *Step 2 -- Build the universe and complementarity subsets.* The reduction creates $U = {0, dots, #(nae_ss.target.instance.universe_size - 1)}$ with positive literals on $\{0, 1, 2\}$ and negative literals on $\{3, 4, 5\}$. The first three target subsets are $R_1 = {#nae_ss.target.instance.subsets.at(0).map(str).join(", ")}$, $R_2 = {#nae_ss.target.instance.subsets.at(1).map(str).join(", ")}$, and $R_3 = {#nae_ss.target.instance.subsets.at(2).map(str).join(", ")}$. - *Step 3 -- Encode the clauses as set-splitting constraints.* Clause $C_1$ becomes $T_1 = {#nae_ss.target.instance.subsets.at(3).map(str).join(", ")}$, and clause $C_2$ becomes $T_2 = {#nae_ss.target.instance.subsets.at(4).map(str).join(", ")}$. Under the target coloring $(#colors.map(str).join(", "))$, $T_1$ receives colors $(#colors.at(0), #colors.at(4), #colors.at(2))$ and $T_2$ receives $(#colors.at(3), #colors.at(1), #colors.at(5))$, so both subsets are non-monochromatic. + *Step 3 -- Encode the clauses as set-splitting constraints.* Clause $C_1$ becomes $T_1 = {#nae_ss.target.instance.subsets.at(3).map(str).join(", ")}$, and clause $C_2$ becomes $T_2 = {#nae_ss.target.instance.subsets.at(4).map(str).join(", ")}$. Under the target coloring $(#fmt-values(colors))$, $T_1$ receives colors $(#colors.at(0), #colors.at(4), #colors.at(2))$ and $T_2$ receives $(#colors.at(3), #colors.at(1), #colors.at(5))$, so both subsets are non-monochromatic. - *Step 4 -- Verify the witness pair.* Every complementarity pair has opposite colors: $(0, 3)$ gives $(#colors.at(0), #colors.at(3))$, $(1, 4)$ gives $(#colors.at(1), #colors.at(4))$, and $(2, 5)$ gives $(#colors.at(2), #colors.at(5))$. Reading the positive-literal colors $(#colors.at(0), #colors.at(1), #colors.at(2))$ recovers the source assignment $(#nae_ss_sol.source_config.map(str).join(", "))$ #sym.checkmark. + *Step 4 -- Verify the witness pair.* Every complementarity pair has opposite colors: $(0, 3)$ gives $(#colors.at(0), #colors.at(3))$, $(1, 4)$ gives $(#colors.at(1), #colors.at(4))$, and $(2, 5)$ gives $(#colors.at(2), #colors.at(5))$. Reading the positive-literal colors $(#colors.at(0), #colors.at(1), #colors.at(2))$ recovers the source assignment $(#fmt-values(nae_ss_sol.source_config))$ #sym.checkmark. ] } @@ -18784,22 +18616,22 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(nae_ppm.source) + " -o naesat.json", - "pred reduce naesat.json --to " + target-spec(nae_ppm) + " -o bundle.json", + "pred reduce naesat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate naesat.json --config " + nae_ppm_sol.source_config.map(str).join(","), + "pred evaluate naesat.json --config " + cli-config(nae_ppm_sol.source_config), ) #{ let target = nae_ppm.target.instance let config = nae_ppm_sol.target_config [ - *Step 1 -- Source instance.* The canonical NAE-SAT fixture has clauses $C_1 = (x_1 or x_2 or x_3)$ and $C_2 = (overline(x_1) or x_2 or overline(x_3))$. The stored source witness is $(#nae_ppm_sol.source_config.map(str).join(", "))$, so the clause truth patterns are $(1, 1, 0)$ and $(0, 1, 1)$, hence both clauses satisfy the NAE condition. + *Step 1 -- Source instance.* The canonical NAE-SAT fixture has clauses $C_1 = (x_1 or x_2 or x_3)$ and $C_2 = (overline(x_1) or x_2 or overline(x_3))$. The stored source witness is $(#fmt-values(nae_ppm_sol.source_config))$, so the clause truth patterns are $(1, 1, 0)$ and $(0, 1, 1)$, hence both clauses satisfy the NAE condition. *Step 2 -- Lay out the gadgets.* Each variable contributes 4 vertices, each clause contributes 6 signal vertices and 4 clause-gadget vertices, and each literal occurrence contributes one 2-vertex equality-chain pair. Concretely the target has $#target.graph.num_vertices$ vertices and $#target.graph.edges.len()$ edges: variable gadgets occupy vertices $0, dots, 11$, signal pairs occupy $12, dots, 23$, the two $K_4$ clause gadgets occupy $24, dots, 31$, and the equality-chain pairs occupy $32, dots, 43$. *Step 3 -- Propagate the literal values.* Because $x_1 = x_2 = 1$ and $x_3 = 0$, the three signal vertices for clause $C_1$ are in groups $(#config.at(12), #config.at(14), #config.at(16)) = (0, 0, 1)$, while the three signal vertices for clause $C_2$ are in groups $(#config.at(18), #config.at(20), #config.at(22)) = (1, 0, 0)$. The equality-chain pairs at $(32, 33), dots, (42, 43)$ carry the complementary groups needed to keep each copied signal synchronized with the appropriate $t_i$ or $f_i$. - *Step 4 -- Verify the clause gadgets and extraction.* The first $K_4$ gadget uses groups $(#config.at(24), #config.at(25), #config.at(26), #config.at(27)) = (1, 1, 0, 0)$, and the second uses $(#config.at(28), #config.at(29), #config.at(30), #config.at(31)) = (0, 1, 1, 0)$. Each gadget therefore splits $2 + 2$, so every clause gadget induces a perfect matching inside each group. Reading the truth assignment back from the variable vertices $(0, 4, 8)$ gives groups $(#config.at(0), #config.at(4), #config.at(8)) = (0, 0, 1)$, which extracts to $(#nae_ppm_sol.source_config.map(str).join(", "))$ #sym.checkmark. + *Step 4 -- Verify the clause gadgets and extraction.* The first $K_4$ gadget uses groups $(#config.at(24), #config.at(25), #config.at(26), #config.at(27)) = (1, 1, 0, 0)$, and the second uses $(#config.at(28), #config.at(29), #config.at(30), #config.at(31)) = (0, 1, 1, 0)$. Each gadget therefore splits $2 + 2$, so every clause gadget induces a perfect matching inside each group. Reading the truth assignment back from the variable vertices $(0, 4, 8)$ gives groups $(#config.at(0), #config.at(4), #config.at(8)) = (0, 0, 1)$, which extracts to $(#fmt-values(nae_ppm_sol.source_config))$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ] @@ -18828,21 +18660,21 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(x3c_sp.source) + " -o x3c.json", - "pred reduce x3c.json --to " + target-spec(x3c_sp) + " -o bundle.json", + "pred reduce x3c.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate x3c.json --config " + x3c_sp_sol.source_config.map(str).join(","), + "pred evaluate x3c.json --config " + cli-config(x3c_sp_sol.source_config), ) #{ let sizes = x3c_sp.target.instance.sizes [ - *Step 1 -- Source instance.* The fixture has $U = {0, dots, #(x3c_sp.source.instance.universe_size - 1)}$ and three 3-sets: $C_0 = {#x3c_sp.source.instance.subsets.at(0).map(str).join(", ")}$, $C_1 = {#x3c_sp.source.instance.subsets.at(1).map(str).join(", ")}$, and $C_2 = {#x3c_sp.source.instance.subsets.at(2).map(str).join(", ")}$. The witness $(#x3c_sp_sol.source_config.map(str).join(", "))$ selects $C_0$ and $C_1$. + *Step 1 -- Source instance.* The fixture has $U = {0, dots, #(x3c_sp.source.instance.universe_size - 1)}$ and three 3-sets: $C_0 = {#x3c_sp.source.instance.subsets.at(0).map(str).join(", ")}$, $C_1 = {#x3c_sp.source.instance.subsets.at(1).map(str).join(", ")}$, and $C_2 = {#x3c_sp.source.instance.subsets.at(2).map(str).join(", ")}$. The witness $(#fmt-values(x3c_sp_sol.source_config))$ selects $C_0$ and $C_1$. *Step 2 -- Recover the prime assignment from the concrete products.* The target numbers are $s_0 = #sizes.at(0) = 2 dot 3 dot 5$, $s_1 = #sizes.at(1) = 7 dot 11 dot 13$, and $s_2 = #sizes.at(2) = 2 dot 7 dot 11$. Thus the six universe elements are concretely labeled by the primes $(2, 3, 5, 7, 11, 13)$. *Step 3 -- Form the Subset Product instance.* The target product is $B = #x3c_sp.target.instance.target = 2 dot 3 dot 5 dot 7 dot 11 dot 13$. Selecting the first two source subsets therefore means selecting target numbers $(#sizes.at(0), #sizes.at(1))$. - *Step 4 -- Verify the witness pair.* The selected sets $C_0$ and $C_1$ are disjoint and cover all six elements exactly once, and on the target side $#sizes.at(0) dot #sizes.at(1) = #x3c_sp.target.instance.target$ while $#sizes.at(2)$ is omitted. Because the configuration is unchanged, the target witness $(#x3c_sp_sol.target_config.map(str).join(", "))$ extracts back to the same exact cover #sym.checkmark. + *Step 4 -- Verify the witness pair.* The selected sets $C_0$ and $C_1$ are disjoint and cover all six elements exactly once, and on the target side $#sizes.at(0) dot #sizes.at(1) = #x3c_sp.target.instance.target$ while $#sizes.at(2)$ is omitted. Because the configuration is unchanged, the target witness $(#fmt-values(x3c_sp_sol.target_config))$ extracts back to the same exact cover #sym.checkmark. ] } @@ -18869,28 +18701,28 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(x3c_bdst.source) + " -o x3c.json", - "pred reduce x3c.json --to " + target-spec(x3c_bdst) + " -o bundle.json", + "pred reduce x3c.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate x3c.json --config " + x3c_bdst_sol.source_config.map(str).join(","), + "pred evaluate x3c.json --config " + cli-config(x3c_bdst_sol.source_config), ) #let q = x3c_bdst.source.instance.universe_size / 3 #let m = x3c_bdst.source.instance.subsets.len() *Step 1 -- Source instance.* The X3C fixture has universe $U = {0, dots, #(x3c_bdst.source.instance.universe_size - 1)}$ with $q = #q$ and candidate triples #for (i, s) in x3c_bdst.source.instance.subsets.enumerate() [ - $C_#i = {#s.map(str).join(", ")}$#if i < m - 1 [, ] else [.] + $C_#i = {#fmt-values(s)}$#if i < m - 1 [, ] else [.] ] *Step 2 -- Build the spanning-tree gadget.* Create a root $r$, two forced-path vertices $v_1, v_2$, one set vertex $s_i$ per triple, and one element vertex $e_j$ per universe element. The target therefore has $#x3c_bdst_nv = 3 + #m + #(x3c_bdst.source.instance.universe_size)$ vertices and $#x3c_bdst_ne$ weighted edges: the forced path $(r, v_1), (v_1, v_2)$ at weight $1$, the root-to-set edges $(r, s_i)$ at weight $2$, the set-to-element edges $(s_i, e_j)$ for $j in C_i$ at weight $1$, and the set clique $(s_i, s_(i'))$ at weight $1$. The bounds are $D = #x3c_bdst.target.instance.diameter_bound$ and $B = 4q + m + 2 = #x3c_bdst.target.instance.weight_bound$. - *Step 3 -- Verify the canonical witness.* The stored source configuration $(#x3c_bdst_sol.source_config.map(str).join(", "))$ selects subsets ${#x3c_bdst_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => "C_" + str(i)).join(", ")}$. The corresponding tree keeps the forced path, every root-to-set edge for a selected $s_i$, every $(s_i, e_j)$ for $j in C_i$, and one clique edge to attach each remaining set vertex. With $q = #q$ selected sets it has total weight $2 + 2q + 3q + (m - q) = #(2 + 2 * q + 3 * q + m - q) = B$ and every vertex sits within distance $2$ of $r$, so the diameter is at most $4$ #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The stored source configuration $(#fmt-values(x3c_bdst_sol.source_config))$ selects subsets ${#x3c_bdst_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => "C_" + str(i)).join(", ")}$. The corresponding tree keeps the forced path, every root-to-set edge for a selected $s_i$, every $(s_i, e_j)$ for $j in C_i$, and one clique edge to attach each remaining set vertex. With $q = #q$ selected sets it has total weight $2 + 2q + 3q + (m - q) = #(2 + 2 * q + 3 * q + m - q) = B$ and every vertex sits within distance $2$ of $r$, so the diameter is at most $4$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. Any feasible spanning tree of weight $B$ and diameter $D = 4$ corresponds to an exact cover via the same extractor, so additional witnesses, when they exist, just enumerate other exact covers. ], )[ - This $O(m^2 + q)$ reduction @garey1979[ND4] embeds X3C into the spanning-tree gadget of Bounded Diameter Spanning Tree. The constructed graph has $3 + m + 3q$ vertices and $2 + 4m + binom(m, 2)$ edges with weights in ${1, 2}$. Setting $D = 4$ and $B = 4q + m + 2$, the BDST instance is feasible if and only if the X3C instance has an exact cover. + The X3C hardness route is catalogued in @garey1979[ND4]. The following $O(1 + m^2 + q)$ construction and proof embed X3C into the spanning-tree gadget of Bounded Diameter Spanning Tree. The constructed graph has $3 + m + 3q$ vertices and $2 + 4m + binom(m, 2)$ edges with weights in ${1, 2}$. Setting $D = 4$ and $B = 4q + m + 2$, the BDST instance is feasible if and only if the X3C instance has an exact cover. ][ - _Construction._ Let the X3C instance be $(U, cal(C))$ with $|U| = 3q$ and $cal(C) = {C_0, dots, C_(m-1)}$. Introduce a root vertex $r$, two forced-path vertices $v_1, v_2$, set vertices $s_0, dots, s_(m-1)$, and element vertices $e_0, dots, e_(3q-1)$. Add the edges + _Construction._ Let the X3C instance be $(U, cal(C))$ with $|U| = 3q$ and $cal(C) = {C_0, dots, C_(m-1)}$. First compute the union of all triples. If it omits any universe element, return two isolated vertices with weight bound $1$ and diameter bound $4$: both problems are NO. Otherwise introduce a root vertex $r$, two forced-path vertices $v_1, v_2$, set vertices $s_0, dots, s_(m-1)$, and element vertices $e_0, dots, e_(3q-1)$. Add the edges $ (r, v_1) " and " (v_1, v_2) " of weight " 1, quad (r, s_i) " of weight " 2 " for every " i, $ @@ -18899,11 +18731,14 @@ The following table shows concrete variable overhead for example instances, take $ Set the diameter bound $D = 4$ and the weight bound $B = 4q + m + 2$. - _Correctness._ ($arrow.r.double$) Let $cal(C)' = {C_(i_1), dots, C_(i_q)}$ be an exact cover. Pick the forced-path edges, the $q$ root-to-set edges for the chosen indices, the $3q$ set-to-element edges that match the cover, and for every unselected set $C_i$ a single clique edge to some chosen set. This is a spanning tree of weight $2 + 2q + 3q + (m - q) = 4q + m + 2 = B$, and every vertex lies within distance $2$ of $r$, so the diameter is at most $4 = D$. + _Correctness._ ($arrow.r.double$) Let $cal(C)' = {C_(i_1), dots, C_(i_q)}$ be an exact cover. The coverage test cannot reject it. If $q = 0$, the legal collection is empty and the forced two-edge path is already a feasible tree. Otherwise $q >= 1$. Pick the forced-path edges, the $q$ root-to-set edges for the chosen indices, the $3q$ set-to-element edges that match the cover, and for every unselected set $C_i$ a single clique edge to some chosen set. This is a spanning tree of weight $2 + 2q + 3q + (m - q) = 4q + m + 2 = B$, and every vertex lies within distance $2$ of $r$, so the diameter is at most $4 = D$. + + ($arrow.l.double$) Suppose $T$ is a spanning tree of weight at most $B$ and diameter at most $4$. Because $"dist"_T(r, v_2) = 2$ in any tree containing the forced edges, every other vertex must sit within distance $2$ of $r$; otherwise its distance to $v_2$ would exceed $4$. Element vertices $e_j$ have neighbors only among set vertices, so each $e_j$ is at depth $2$ and connects through some $s_i$ that is directly attached to $r$. Let $k$ be the number of root-to-set edges in $T$. Every spanning tree has exactly $m + 3q + 2$ edges, and exactly the $k$ root-to-set edges have weight $2$ rather than $1$. Its weight is therefore exactly $k + m + 3q + 2$. Feasibility forces $k <= q$. Each chosen set covers at most three element vertices, so covering all $3q$ elements requires $k >= q$, hence $k = q$. The $q$ chosen sets contribute exactly $3q$ element attachments, so they must be pairwise disjoint and form an exact cover. + + _Solution extraction._ First evaluate the target once and reject every configuration that is not a feasible bounded-diameter spanning tree. The fixed disconnected target has no accepted certificates. The target configuration has one coordinate per edge in the order produced by the construction. The $m$ coordinates indexing the root-to-set edges $(r, s_i)$ are the X3C selection vector: $x_i = 1$ iff $(r, s_i) in T$. - ($arrow.l.double$) Suppose $T$ is a spanning tree of weight at most $B$ and diameter at most $4$. Because $"dist"_T(r, v_2) = 2$ in any tree containing the forced edges, every other vertex must sit within distance $2$ of $r$; otherwise its distance to $v_2$ would exceed $4$. Element vertices $e_j$ have neighbors only among set vertices, so each $e_j$ is at depth $2$ and connects through some $s_i$ that is directly attached to $r$. Let $k$ be the number of root-to-set edges in $T$. The cheapest way to spawn the remaining $m - k$ set vertices uses clique edges of weight $1$, so the minimum tree weight is $k dot 2 + (m - k) dot 1 + 3q dot 1 + 2 dot 1 = k + m + 3q + 2$. Feasibility forces $k <= q$. Each chosen set covers at most three element vertices, so covering all $3q$ elements requires $k >= q$, hence $k = q$. The $q$ chosen sets contribute exactly $3q$ element attachments, so they must be pairwise disjoint and form an exact cover. + _Range and size._ Compute the union from the triples, without allocating an array indexed by the universe. Thus a large universe with no possible cover is handled before graph allocation. In the incidence branch the union test gives $3q <= 3m$. Vertex and edge counts and the weight bound are checked before allocation; all generated weights are positive. The target has at most $3 + m + 3q$ vertices and $2 + 4m + binom(m, 2)$ edges, including the disconnected NO target. Repeated triples remain separate set vertices; the exact-cover counting proof still applies. - _Solution extraction._ The target configuration has one coordinate per edge in the order produced by the construction. The $m$ coordinates indexing the root-to-set edges $(r, s_i)$ are the X3C selection vector: $x_i = 1$ iff $(r, s_i) in T$. ] // 8. SubsetSum → IntegerExpressionMembership (#569) @@ -18915,19 +18750,19 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ss_iem.source) + " -o subsetsum.json", - "pred reduce subsetsum.json --to " + target-spec(ss_iem) + " -o bundle.json", + "pred reduce subsetsum.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate subsetsum.json --config " + ss_iem_sol.source_config.map(str).join(","), + "pred evaluate subsetsum.json --config " + cli-config(ss_iem_sol.source_config), ) #{ let sizes = ss_iem.source.instance.sizes [ - *Step 1 -- Source instance.* The Subset Sum fixture has sizes $(#sizes.join(", "))$ and target $B = #ss_iem.source.instance.target$. The canonical source configuration $(#ss_iem_sol.source_config.map(str).join(", "))$ selects the second and third items, so the source sum is $5 + 6 = #ss_iem.source.instance.target$. + *Step 1 -- Source instance.* The Subset Sum fixture has sizes $(#sizes.join(", "))$ and target $B = #ss_iem.source.instance.target$. The canonical source configuration $(#fmt-values(ss_iem_sol.source_config))$ selects the second and third items, so the source sum is $5 + 6 = #ss_iem.source.instance.target$. *Step 2 -- Build the choice sets inside the expression.* Each source item contributes one union node $(1 union (s_i + 1))$, so the concrete choices are $(1 union 2)$, $(1 union 6)$, $(1 union 7)$, and $(1 union 9)$. With $n = #ss_iem_sol.target_config.len()$ union nodes, the target is shifted to $K = B + n = #ss_iem.target.instance.target$. - *Step 3 -- Follow the canonical branch choices.* The target configuration $(#ss_iem_sol.target_config.map(str).join(", "))$ means left, right, right, left, so the chosen branch values are $1$, $6$, $7$, and $1$. + *Step 3 -- Follow the canonical branch choices.* The target configuration $(#fmt-values(ss_iem_sol.target_config))$ means left, right, right, left, so the chosen branch values are $1$, $6$, $7$, and $1$. *Step 4 -- Verify the equality.* The target-side sum is $1 + 6 + 7 + 1 = #ss_iem.target.instance.target$, exactly matching $K$. The right branches occur in the same two positions as the chosen source elements, so extracting the target witness returns the original Subset Sum solution #sym.checkmark. ] @@ -18956,16 +18791,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(ksat_si.source) + " -o ksat.json", - "pred reduce ksat.json --to " + target-spec(ksat_si) + " -o bundle.json", + "pred reduce ksat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate ksat.json --config " + ksat_si_sol.source_config.map(str).join(","), + "pred evaluate ksat.json --config " + cli-config(ksat_si_sol.source_config), ) #{ let pairs = ksat_si.target.instance.pairs - let x = ksat_si_sol.target_config.at(0) + let x = ksat_si_sol.target_config [ - *Step 1 -- Source instance.* The two clauses are $C_1 = (x_1 or x_2 or x_2)$ and $C_2 = (overline(x_1) or x_2 or x_2)$. The canonical satisfying assignment is $(#ksat_si_sol.source_config.map(str).join(", "))$. + *Step 1 -- Source instance.* The two clauses are $C_1 = (x_1 or x_2 or x_2)$ and $C_2 = (overline(x_1) or x_2 or x_2)$. The canonical satisfying assignment is $(#fmt-values(ksat_si_sol.source_config))$. *Step 2 -- Assign primes and variable residue constraints.* With two variables, the reduction uses primes $3$ and $5$. The variable-generated forbidden pairs are $(#pairs.at(0).at(0), #pairs.at(0).at(1))$, $(#pairs.at(1).at(0), #pairs.at(1).at(1))$, $(#pairs.at(2).at(0), #pairs.at(2).at(1))$, and $(#pairs.at(3).at(0), #pairs.at(3).at(1))$, leaving only residues $1$ and $2$ modulo $3$ and modulo $5$. @@ -18995,11 +18830,11 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(n3dm_nmts.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(n3dm_nmts) + " -o bundle.json", + "pred reduce source.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate source.json --config " + n3dm_nmts_sol.source_config.map(str).join(","), + "pred evaluate source.json --config " + cli-config(n3dm_nmts_sol.source_config), ) - Source N3DM witness $(#n3dm_nmts_sol.source_config.map(str).join(", "))$, target NMTS pairing $(#n3dm_nmts_sol.target_config.map(str).join(", "))$. + Source N3DM witness $(#fmt-values(n3dm_nmts_sol.source_config))$, target NMTS pairing $(#fmt-values(n3dm_nmts_sol.target_config))$. ], )[ This linear-time reduction @garey1979 keeps the $X$ and $Y$ sets unchanged and replaces each $w_i in W$ by a target complement $B_i = B - s(w_i)$. For an instance with $m$ triples, the target has $m$ pairs. @@ -19020,19 +18855,19 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(part_stw.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_stw) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_stw_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_stw_sol.source_config), ) #{ let lengths = part_stw.target.instance.lengths let weights = part_stw.target.instance.weights let deadline = part_stw.target.instance.deadlines.at(0) - let on-time-sum = part_stw_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => part_stw.source.instance.sizes.at(i)).sum() - let tardy-sum = part_stw_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => part_stw.source.instance.sizes.at(i)).sum() + let on-time-sum = part_stw_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => part_stw.source.instance.sizes.at(i)).sum() + let tardy-sum = part_stw_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => part_stw.source.instance.sizes.at(i)).sum() [ - *Step 1 -- Source instance.* The Partition fixture has sizes $(#part_stw.source.instance.sizes.map(str).join(", "))$ with total $#part_stw.source.instance.sizes.sum()$, so the target deadline is $T = #deadline$. The canonical source vector $(#part_stw_sol.source_config.map(str).join(", "))$ splits the multiset into sums $#on-time-sum$ and $#tardy-sum$. + *Step 1 -- Source instance.* The Partition fixture has sizes $(#fmt-values(part_stw.source.instance.sizes))$ with total $#part_stw.source.instance.sizes.sum()$, so the target deadline is $T = #deadline$. The canonical source vector $(#fmt-values(part_stw_sol.source_config))$ splits the multiset into sums $#on-time-sum$ and $#tardy-sum$. *Step 2 -- Build the task table.* #table( columns: (auto, auto, auto, auto), @@ -19047,22 +18882,25 @@ The following table shows concrete variable overhead for example instances, take [$t_5$], [#lengths.at(5)], [#weights.at(5)], [#deadline], ) - *Step 3 -- Follow the canonical schedule.* The target permutation $(#part_stw_sol.target_config.map(str).join(", "))$ schedules tasks in the order $t_1, t_2, t_4, t_5, t_0, t_3$. The completion times are $1, 2, 4, 5, 8, 10$, so $t_1, t_2, t_4, t_5$ are on time and $t_0, t_3$ are tardy. + *Step 3 -- Follow the canonical schedule.* The target permutation $(#fmt-values(part_stw_sol.target_config))$ schedules tasks in the order $t_1, t_2, t_4, t_5, t_0, t_3$. The completion times are $1, 2, 4, 5, 8, 10$, so $t_1, t_2, t_4, t_5$ are on time and $t_0, t_3$ are tardy. - *Step 4 -- Compute tardy weight and recover the partition.* Because weights equal lengths here, the tardy weight is $w_0 + w_3 = 3 + 2 = #deadline$, and the on-time tasks have total size $#on-time-sum$ while the tardy tasks have total size #tardy-sum. Extracting the schedule therefore returns the balanced partition $(#part_stw_sol.source_config.map(str).join(", "))$ #sym.checkmark. + *Step 4 -- Compute tardy weight and recover the partition.* Because weights equal lengths here, the tardy weight is $w_0 + w_3 = 3 + 2 = #deadline$, and the on-time tasks have total size $#on-time-sum$ while the tardy tasks have total size #tardy-sum. Extracting the schedule therefore returns the balanced partition $(#fmt-values(part_stw_sol.source_config))$ #sym.checkmark. ] } *Multiplicity:* The fixture stores one canonical witness. ], )[ - This $O(n)$ reduction @karp1972 maps each element $a_i$ to a task with length $l(t_i) = a_i$, weight $w(t_i) = a_i$, and common deadline $T = B\/2$. The tardiness bound is $K = T$. Tasks scheduled before $T$ are on-time; those after are tardy. A balanced partition exists iff total tardy weight can be at most $K$. + This $O(n)$ specialization of Karp's common-deadline sequencing construction @karp1972 maps each element $a_i$ to a task with processing time and tardy weight $a_i$. For total $S$, use common deadline $B = floor(S / 2)$. A balanced partition exists exactly when the minimum tardy weight is $B$; other optimum values map to false through the formal aggregate reduction. ][ - _Construction._ Given Partition instance $A = {a_1, dots, a_n}$ with total $B$. If $B$ is odd, output a trivially infeasible instance (all deadlines 0, $K = 0$). If $B$ is even, set $T = B\/2$. For each $a_i$, create task $t_i$ with $l(t_i) = w(t_i) = a_i$ and deadline $d(t_i) = T$. Set bound $K = T$. + _Construction._ The source has $n >= 1$ positive sizes with checked total $S$. Create $n$ tasks in source order, each with length and weight $a_i$, and deadline $B = floor(S / 2)$. The construction is identical for odd and even totals. Karp's original paper gives Knapsack to Job Sequencing (p. 100), with equal processing times and penalties and a common deadline; the Partition specialization and its optimization certificate are proved here. - _Correctness._ ($arrow.r.double$) A balanced partition $A', A''$ with sums $T$ each: schedule $A'$ first (on-time, total time $T$), then $A''$ (tardy, weight $T = K$). ($arrow.l.double$) If tardy weight $<= K = T$, then on-time tasks fit before $T$ and sum to $<= T$, while tardy tasks have weight $B - sum_("on-time") <= T$, forcing on-time sum $= T$. This yields a balanced partition. + _Correctness._ Let $E$ be the total size of tasks completing by $B$ in any valid permutation. Positive processing times make these tasks a prefix, so $E <= B$. Because weights equal processing times, tardy weight is $W = S - E >= S - B >= B$. + ($arrow.r.double$) For a balanced partition, schedule one half first, then the other. Their sizes are $B$ each; the first half finishes by $B$, and every remaining task finishes after $B$. Thus $W = B$, attaining the lower bound. + ($arrow.l.double$) If $W = B$, then $E = S - B <= B$. Together with $S >= 2B$ this forces $S = 2B$ and $E = B$. Hence the on-time and tardy tasks form a balanced partition. For odd $S$, every schedule instead has $W >= B + 1$, so no separate odd-input construction is needed. + + _Solution extraction._ Validate the target with the formal API once and require finite tardy weight exactly $B$. Return false for tasks completing by $B$ and true for tardy tasks; reject all other configurations as noncertifying. The aggregate maps `Min(Some(B))` to `Or(true)` and all other values to `Or(false)`. Every permutation is a feasible target schedule, so source infeasibility is represented by an optimum above $B$, not target infeasibility. Every completion sum and tardy-weight sum is at most the source's checked $S$, establishing full `i64` arithmetic safety without changing either endpoint. - _Solution extraction._ On-time tasks (completing by $T$) form one partition half ($x_i = 0$), tardy tasks the other ($x_i = 1$). ] // 12. Partition → OpenShopScheduling (#481) @@ -19074,18 +18912,18 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(part_oss.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_oss) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_oss_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_oss_sol.source_config), ) #{ let q = part_oss.source.instance.sizes.sum() / 2 let p = part_oss.target.instance.processing_times - let left-sum = part_oss_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => part_oss.source.instance.sizes.at(i)).sum() - let right-sum = part_oss_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => part_oss.source.instance.sizes.at(i)).sum() + let left-sum = part_oss_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => part_oss.source.instance.sizes.at(i)).sum() + let right-sum = part_oss_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => part_oss.source.instance.sizes.at(i)).sum() [ - *Step 1 -- Source instance.* The Partition fixture has sizes $(#part_oss.source.instance.sizes.map(str).join(", "))$, total $#part_oss.source.instance.sizes.sum()$, and half-sum $Q = #q$. The canonical source vector $(#part_oss_sol.source_config.map(str).join(", "))$ gives subset sums $#left-sum$ and $#right-sum$. + *Step 1 -- Source instance.* The Partition fixture has sizes $(#fmt-values(part_oss.source.instance.sizes))$, total $#part_oss.source.instance.sizes.sum()$, and half-sum $Q = #q$. The canonical source vector $(#fmt-values(part_oss_sol.source_config))$ gives subset sums $#left-sum$ and $#right-sum$. *Step 2 -- Build the open-shop job table.* #table( columns: (auto, auto, auto, auto), @@ -19098,24 +18936,27 @@ The following table shows concrete variable overhead for example instances, take [$J_3$], [#p.at(3).at(0)], [#p.at(3).at(1)], [#p.at(3).at(2)], ) The first three jobs come from the partition elements, and the special job $J_3$ has processing time $Q = #q$ on every machine. - *Step 3 -- Decode the canonical machine orders.* The target configuration $(#part_oss_sol.target_config.map(str).join(", "))$ splits into $M_1 = (0, 1, 2, 3)$, $M_2 = (0, 1, 2, 3)$, and $M_3 = (2, 3, 0, 1)$. On machine $M_3$, job $J_2$ occupies $[0, 3)$ and the special job $J_3$ starts exactly at time $Q = 3$, so the prefix before the special job contains precisely job $J_2$. + *Step 3 -- Decode the canonical start times.* The target configuration $(#fmt-values(part_oss_sol.target_config))$ is job-major. On machine $M_1$, jobs $J_0$ and $J_1$ occupy $[0,1)$ and $[1,3)$, and the special job $J_3$ starts exactly at time $Q = 3$. - *Step 4 -- Verify extraction and makespan.* Because only $J_2$ finishes on $M_3$ by time $Q$, the extracted source vector is $(#part_oss_sol.source_config.map(str).join(", "))$, i.e.\ subset sum #right-sum versus #left-sum. Evaluating the stored machine orders gives a concrete makespan of $12$, so the `load-example()` fixture shows both the machine assignment and the middle-machine split used for extraction #sym.checkmark. + *Step 4 -- Verify extraction and makespan.* The element jobs completing on $M_1$ by time $Q$ are exactly $J_0,J_1$, so the extracted source vector is $(#fmt-values(part_oss_sol.source_config))$, i.e.\ subset sum #right-sum versus #left-sum. Evaluating the explicit schedule gives the optimal makespan $9 = 3Q$ #sym.checkmark. ] } *Multiplicity:* The fixture stores one canonical witness. ], )[ - This $O(k)$ reduction @gonzalez1976 creates $k + 1$ jobs on 3 machines: $k$ element jobs with $p_(j,i) = a_j$ on all machines, plus one special job with $p = Q = S\/2$. The target makespan is $3Q$. A balanced partition exists iff the open shop can achieve makespan $<= 3Q$. + This $O(k)$ construction follows the special-job scheduling argument of Gonzalez and Sahni @gonzalez1976[Lemma 4.1]. Their published construction uses $3k+1$ jobs with one nonzero operation per element copy; the implemented construction instead groups each element's three operations into one job and uses $k+1$ jobs. The independent schedule below proves this grouping preserves the reduction. A balanced partition exists exactly when the target achieves the makespan certificate $3Q$, where $Q=floor(S/2)$ and $S$ is the sum of the input sizes. ][ - _Construction._ Given Partition instance $A = {a_1, dots, a_k}$ with total $S$ and $Q = S\/2$. Set $m = 3$ machines. For each $a_j$, create element job $J_j$ with $p_(j,1) = p_(j,2) = p_(j,3) = a_j$. Create special job $J_(k+1)$ with $p_(k+1,i) = Q$ on all machines. Deadline $D = 3Q$. + _Construction._ The source is a nonempty list of positive integers $a_1,...,a_k$, with $S=sum_j a_j$. Set $Q=floor(S/2)$. Use three machines, one job $(a_j,a_j,a_j)$ per element, and one special job $(Q,Q,Q)$. Let $D=3Q$. - _Correctness._ ($arrow.r.double$) With a balanced partition $I_1, I_2$, schedule the special job consecutively on machines 1, 2, 3 during $[0,Q), [Q,2Q), [2Q,3Q)$. Use a rotated assignment for $I_1$ and $I_2$ jobs to fill the remaining idle blocks, each of length $Q$. ($arrow.l.double$) With makespan $<= 3Q$, the special job alone needs $3Q$ elapsed time, so it tiles $[0,3Q)$ exactly. On each machine, element jobs fill two idle blocks of length $Q$ each. The jobs in one block sum to $Q$, giving a balanced partition. + _Forward correctness._ Given a balanced partition into groups $A,B$, each group has total size $Q$ and $S=2Q$. Divide time into three phases $[r Q,(r+1)Q)$, $r=0,1,2$. In phase $r$, run the special job on machine $r$, all jobs of $A$ consecutively on machine $(r+1) mod 3$, and all jobs of $B$ consecutively on machine $(r+2) mod 3$. Each group exactly fills its phase, each element job has one operation per phase, and the machine rotation processes it once on every machine. This is a feasible nonpreemptive schedule of makespan $D$. - _Solution extraction._ Identify the special job's position on machine 1. Element jobs in one idle block form a subset summing to $Q$. -] + _Backward correctness._ Every schedule has makespan at least $3Q$ by the special job's total processing time, and at least $S+Q$ by each machine's load. If a feasible schedule attains $D$, these bounds give $S+Q<=3Q$; together with $S>=2Q$ this forces $S=2Q$. Since the source sizes are positive, $Q>0$. The special job must run without gaps and start its three operations at $0,Q,2Q$, in some machine order. Select the unique machine on which it starts at $Q$. Element operations on that machine fit before $Q$ or after $2Q$, each interval having capacity $Q$. Their total load is $2Q$, so the jobs completing by $Q$ have total size exactly $Q$ and define a balanced partition. It is the middle machine that supplies these two separate intervals, not an arbitrary fixed machine. + + _Odd sums and zero duration._ If $S$ is odd, each machine's load is $S+Q=3Q+1>D$, so the threshold cannot be attained. The only legal source with $Q=0$ is the singleton size one; the special job then has zero duration, but the positive element job prevents makespan zero. Thus no alternate endpoint or parity-specific construction is required. + _Aggregation and extraction._ Map a finite optimum equal to $D$ to true and all other values to false. Validate a target configuration once, apply this same certificate, then identify the middle machine and select its element jobs completing by $Q$. Reject invalid schedules and feasible schedules that do not attain the certificate. The existing checked target constructor validates its total horizon $3(S+Q)$ before computing $D$, so the smaller nonnegative certificate is representable. Target construction failures retain their formal error type. +] // 13. NAESatisfiability → MaxCut (#166) #let nae_mc = load-example("NAESatisfiability", "MaxCut") #let nae_mc_sol = nae_mc.solutions.at(0) @@ -19125,9 +18966,9 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(nae_mc.source) + " -o naesat.json", - "pred reduce naesat.json --to " + target-spec(nae_mc) + " -o bundle.json", + "pred reduce naesat.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate naesat.json --config " + nae_mc_sol.source_config.map(str).join(","), + "pred evaluate naesat.json --config " + cli-config(nae_mc_sol.source_config), ) #{ @@ -19141,57 +18982,35 @@ The following table shows concrete variable overhead for example instances, take *Step 2 -- Construct the weighted graph.* Variable gadgets contribute #n heavy edges of weight $M$. Because the canonical fixture has 3 literals per clause, each clause contributes one unit-weight triangle, so the target has #clause-edge-count unit-weight clause edges and $#graph-num-edges(nae_mc.target.instance)$ edges total on $#graph-num-vertices(nae_mc.target.instance)$ vertices. - *Step 3 -- Verify the canonical witness.* Source assignment $(#nae_mc_sol.source_config.map(str).join(", "))$ induces target cut $(#nae_mc_sol.target_config.map(str).join(", "))$. All #n heavy edges are cut, and each of the #m clause triangles has a 1-2 split contributing 2, so the total cut weight is $#cut-value$ #sym.checkmark. + *Step 3 -- Verify the canonical witness.* Source assignment $(#fmt-values(nae_mc_sol.source_config))$ induces target cut $(#fmt-values(nae_mc_sol.target_config))$. All #n heavy edges are cut, and each of the #m clause triangles has a 1-2 split contributing 2, so the total cut weight is $#cut-value$ #sym.checkmark. ] } *Multiplicity:* The fixture stores one canonical witness. ], )[ - This implemented reduction sets $M = m + 1$, creates two literal vertices per variable, and adds one unit-weight edge for every literal pair inside a clause. When every clause has 3 literals, each clause gadget is a triangle, so the construction is the usual NAE-SAT-to-Max-Cut graph on $2n$ vertices with $n + 3m$ edges. + Reduce arbitrary NAE clauses of length at least two to binary edges and ternary triangles. Long clauses use fresh auxiliary variables in the standard NAE clause chain @jackson2025. Every satisfying source assignment extends to a cut attaining an explicit componentwise upper bound; a target cut is decoded only when it attains that bound. ][ - _Construction._ Let $phi$ have $n$ variables and $m$ clauses, and set $M = m + 1$. For each variable $x_i$, create a positive literal vertex $p_i = 2i$ and a negative literal vertex $n_i = 2i + 1$, joined by one weight-$M$ edge. For each clause $C_j$, add a unit-weight edge between every pair of literal vertices appearing in $C_j$. In particular, if every clause has three literals, each clause becomes a unit-weight triangle. - - _Correctness._ Assume from here on that each clause has exactly three literals, matching the canonical fixture. Then every clause gadget is a triangle. + _Clause expansion._ Let the source have $n$ variables, $m$ clauses of lengths $ell_j >= 2$, and $L = sum_j ell_j$ literal occurrences. Keep binary and ternary clauses. For a clause $(l_1,...,l_r)$ of length $r >= 4$, introduce $r-3$ fresh variables $z_1,...,z_(r-3)$ and replace it by + $ "NAE"(l_1,l_2,z_1), quad "NAE"(not z_1,l_3,z_2), quad dots.c, quad "NAE"(not z_(r-3),l_(r-1),l_r). $ + For $r=4$ the chain consists of its first and last clauses. Auxiliary variables are private to each source clause. - ($arrow.r.double$) Let $alpha$ be a NAE-satisfying assignment. Put $p_i$ and $n_i$ on opposite sides of the cut according to $alpha$, so every variable edge is cut and contributes $M$. In each clause triangle, at least one literal is true and at least one is false, so the three vertices split $1$-$2$ across the cut and contribute exactly 2. Therefore the cut weight is $n M + 2m = n (m + 1) + 2m$. + _Chain equivalence._ For a list $R$ of at least two remaining literal values, + $ "NAE"(a,b,R) equiv exists z: "NAE"(a,b,z) and "NAE"(not z,R). $ + If $a=b=c$ and the original clause is NAE-satisfied, $R$ contains a value different from $c$; choosing $z=not c$ satisfies both new clauses. If $a != b$, the first clause is satisfied for either $z$; if $R$ is constant $c$, choose $z=c$, and if $R$ is mixed either choice works. Conversely, if every original value were $c$, the first clause would force $z=not c$, making the second clause constant $c$. Repeated application proves both extension and restriction for the complete chain, including repeated and complementary literals (@jackson2025, Section 4, describes this chain). - ($arrow.l.double$) Suppose a cut has weight at least $n (m + 1) + 2m$. The $m$ clause triangles contribute at most $2m$ in total, so the variable edges must contribute at least $n (m + 1)$. Since each variable edge contributes at most $M = m + 1$, all $n$ variable edges are cut. Thus $p_i$ and $n_i$ lie on opposite sides for every variable, and the cut defines a consistent Boolean assignment by reading the side of $p_i$. The remaining $2m$ weight must come from the clause triangles, so each triangle contributes exactly 2 and therefore has vertices on both sides of the cut. Hence every clause contains both a true and a false literal, and the extracted assignment NAE-satisfies $phi$. Because a satisfying instance attains $n (m + 1) + 2m$, every optimal cut of the target has this form. - - _Solution extraction._ Read the positive literal vertices: $x_i = 1$ iff vertex $2i$ lies on side 1 of the cut. -] - -#let tdm_tp = load-example("ThreeDimensionalMatching", "ThreePartition") -#let tdm_tp_sol = tdm_tp.solutions.at(0) -#let tdm_tmi = load-example("ThreeDimensionalMatching", "ThreeMatroidIntersection") -#let tdm_tmi_sol = tdm_tmi.solutions.at(0) -#reduction-rule("ThreeDimensionalMatching", "ThreeMatroidIntersection", - example: true, - example-caption: [$q = 3$, $t = 5$ triples], - extra: [ - #pred-commands( - "pred create --example " + problem-spec(tdm_tmi.source) + " -o source.json", - "pred reduce source.json --to " + target-spec(tdm_tmi) + " -o bundle.json", - "pred solve bundle.json", - "pred evaluate source.json --config " + tdm_tmi_sol.source_config.map(str).join(","), - ) - Source 3DM witness $(#tdm_tmi_sol.source_config.map(str).join(", "))$, target common-independent witness $(#tdm_tmi_sol.target_config.map(str).join(", "))$. - ], -)[ - This $O(t + q)$ direct embedding @garey1979[SP11] takes the triple set itself as the common ground set and builds three partition matroids, one per coordinate family. The target has $t = |T|$ ground-set elements, $3 q$ groups in total, and bound $K = q$. -][ - _Construction._ Let the 3DM instance have universe size $q$ and triples $T = {t_0, dots, t_(t - 1)} subset.eq W times X times Y$, where $t_l = (w_(a_l), x_(b_l), y_(c_l))$. Create a Three-Matroid Intersection instance whose ground set is $E = {0, dots, t - 1}$, with element $l$ representing triple $t_l$. + _Cut construction._ Write $a = sum_j max(ell_j-3,0)$, $q=n+a$, $b=|{j:ell_j=2}|$, and $t=sum_(j:ell_j>=3)(ell_j-2)$. There are $q$ variables, $b$ binary clauses and $t$ ternary clauses after expansion. Set $M=m+1$. For each original or auxiliary variable, create positive and negative literal vertices joined by a weight-$M$ edge. A binary clause contributes one unit edge between its literal vertices; a ternary clause contributes all three unit edges between its literal occurrences. Preserve repeated edges and self-loops as supported by the graph representation. Define the certificate $K=q M+b+2t$. - Build three partition matroids on $E$. For each $i in {0, dots, q - 1}$, let - $ G_i^W = {l in E : a_l = i}, quad G_i^X = {l in E : b_l = i}, quad G_i^Y = {l in E : c_l = i}. $ - A subset $S subset.eq E$ is independent in the first matroid iff $|S inter G_i^W| <= 1$ for every $i$; define the second and third matroids analogously using the $X$- and $Y$-coordinate groups. Set the target bound to $K = q$. + _Forward correctness._ A satisfying original assignment extends to satisfy all expanded clauses by chain equivalence. Put each literal vertex on its literal's truth side. All $q$ variable edges cross, each binary edge crosses, and every ternary triangle has exactly two crossing occurrence-pairs. The cut therefore attains $K$. - _Correctness._ ($arrow.r.double$) If $M subset.eq T$ is a perfect 3-dimensional matching, let $S subset.eq E$ contain exactly the indices of the triples in $M$. Because no two triples in $M$ share a $W$-, $X$-, or $Y$-coordinate, $S$ meets every group $G_i^W$, $G_i^X$, and $G_i^Y$ in at most one element, so $S$ is independent in all three matroids. Also $|S| = |M| = q$, hence $S$ is a feasible Three-Matroid Intersection solution. + _Backward correctness._ Every cut has weight at most $K$: each variable edge contributes at most $M$, each binary edge at most one, and each triangle at most two. This holds even with coincident literal vertices or repeated edges. Equality forces all individual bounds to be attained. Because $M>0$, each variable and its negation lie on opposite sides; the binary and ternary gadgets then certify all expanded NAE clauses. Restricting their satisfying assignment to the original variables satisfies the original formula by chain equivalence. Thus a feasible source has target optimum exactly $K$, while an infeasible source has optimum strictly below $K$. - ($arrow.l.double$) Let $S subset.eq E$ be a common independent set of size $q$. Independence in the first matroid implies that the selected triples use $q$ distinct $W$-coordinates; since only $q$ such coordinates exist, they use each element of $W$ exactly once. The same argument applies to $X$ and $Y$. Therefore the triples indexed by $S$ form a perfect 3-dimensional matching. + _Aggregation and extraction._ Return true precisely for a finite target optimum equal to $K$. Validate a target configuration once, apply this same certificate, and only then read the positive literal vertex of each original variable. Reject every configuration below the bound instead of returning an invalid source assignment. No assumption that an arbitrary optimum satisfies the formula is needed. - _Solution extraction._ Return the same binary indicator vector: target element $l$ is selected iff source triple $t_l$ is selected. + _Size and arithmetic._ The exact target has $2q$ vertices and $q+b+3t$ edges. Since $a=L-3m+b$, $t=L-2m$, and $b<=m$, the registered upper bounds are $2(n+L-2m)$ vertices and $n+4L-7m$ edges. All size totals, weights, the certificate, and the sum of all nonnegative target weights are checked before graph construction, including the graph's actual index domain. Consequently every successfully constructed cut sum is representable; arithmetic failures remain typed reduction errors. ] +#let tdm_tp = load-example("ThreeDimensionalMatching", "ThreePartition") +#let tdm_tp_sol = tdm_tp.solutions.at(0) #reduction-rule("ThreeDimensionalMatching", "ThreePartition", example: true, @@ -19199,9 +19018,9 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(tdm_tp.source) + " -o three-dimensional-matching.json", - "pred reduce three-dimensional-matching.json --to " + target-spec(tdm_tp) + " -o bundle.json", + "pred reduce three-dimensional-matching.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate three-dimensional-matching.json --config " + tdm_tp_sol.source_config.map(str).join(","), + "pred evaluate three-dimensional-matching.json --config " + cli-config(tdm_tp_sol.source_config), ) #{ @@ -19219,13 +19038,13 @@ The following table shows concrete variable overhead for example instances, take let target = tdm_tp.target.instance [ - *Step 1 -- Source instance.* The canonical source has $q = #q$ and a single triple $M = {(0, 0, 0)}$, so the witness $(#tdm_tp_sol.source_config.map(str).join(", "))$ selects the only available triple and is therefore a perfect 3-dimensional matching #sym.checkmark. + *Step 1 -- Source instance.* The canonical source has $q = #q$ and a single triple $M = {(0, 0, 0)}$, so the witness $(#fmt-values(tdm_tp_sol.source_config))$ selects the only available triple and is therefore a perfect 3-dimensional matching #sym.checkmark. *Step 2 -- Encode the ABCD and tagged 4-partition numbers.* Here $r = 32 q = #r$ and $T_1 = 40 r^4 = #T1$. Because every coordinate is 0 and occurs once, the ABCD numbers are all $10 r^4 = #(10 * r4)$. After the mod-16 tags, the 4-partition numbers become $(#a0, #b0, #c0, #d0)$ and sum to $T_2 = 16 T_1 + 15 = #T2$. - *Step 3 -- Build the 3-partition gadget.* From the 4 tagged numbers the construction creates #target.sizes.len() target numbers: 4 regular numbers, 12 pairing numbers, and 5 fillers. The target bound is $B = 64 T_2 + 4 = #B$, matching the exported instance's bound $#target.bound$. The canonical target witness is $(#tdm_tp_sol.target_config.map(str).join(", "))$: groups 0 and 1 are the non-filler triples, and the remaining 5 groups each contain one filler together with one unused pairing pair. + *Step 3 -- Build the 3-partition gadget.* From the 4 tagged numbers the construction creates #target.sizes.len() target numbers: 4 regular numbers, 12 pairing numbers, and 5 fillers. The target bound is $B = 64 T_2 + 4 = #B$, matching the exported instance's bound $#target.bound$. The canonical target witness is $(#fmt-values(tdm_tp_sol.target_config))$: groups 0 and 1 are the non-filler triples, and the remaining 5 groups each contain one filler together with one unused pairing pair. - *Step 4 -- Verify the witness.* The target configuration partitions all #target.sizes.len() numbers into $#(target.sizes.len() / 3)$ triples summing to $B = #B$ #sym.checkmark. Reversing the gadget recovers the unique tagged 4-set, whose $B$, $C$, and $D$ members are all first occurrences, so the extracted source witness is again $(#tdm_tp_sol.source_config.map(str).join(", "))$ #sym.checkmark. + *Step 4 -- Verify the witness.* The target configuration partitions all #target.sizes.len() numbers into $#(target.sizes.len() / 3)$ triples summing to $B = #B$ #sym.checkmark. Reversing the gadget recovers the unique tagged 4-set, whose $B$, $C$, and $D$ members are all first occurrences, so the extracted source witness is again $(#fmt-values(tdm_tp_sol.source_config))$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. In this $q = 1$ instance there is only one source matching, but the target still admits multiple equivalent 3-partition witnesses because any choice of one pairing gadget to split the unique 4-set yields a valid solution. ] @@ -19265,13 +19084,13 @@ The following table shows concrete variable overhead for example instances, take #let tdm_ilp_sol = tdm_ilp.solutions.at(0) #reduction-rule("ThreeDimensionalMatching", "ILP", example: true, - example-caption: [$q = #tdm_ilp.source.instance.universe_size$, $t = #tdm_ilp.source.instance.triples.len()$ triples $arrow.r$ ILP with #tdm_ilp.target.instance.num_vars variables and #tdm_ilp.target.instance.constraints.len() constraints], + example-caption: [$q = #tdm_ilp.source.instance.universe_size$, $t = #tdm_ilp.source.instance.triples.len()$ triples $arrow.r$ ILP with #tdm_ilp.target.instance.variables.len() variables and #tdm_ilp.target.instance.constraints.len() constraints], extra: [ #pred-commands( "pred create --example " + problem-spec(tdm_ilp.source) + " -o three-dimensional-matching.json", - "pred reduce three-dimensional-matching.json --to " + target-spec(tdm_ilp) + " -o bundle.json", + "pred reduce three-dimensional-matching.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate three-dimensional-matching.json --config " + tdm_ilp_sol.source_config.map(str).join(","), + "pred evaluate three-dimensional-matching.json --config " + cli-config(tdm_ilp_sol.source_config), ) #{ @@ -19279,11 +19098,11 @@ The following table shows concrete variable overhead for example instances, take let t = tdm_ilp.source.instance.triples.len() let triples = tdm_ilp.source.instance.triples [ - *Step 1 -- Source instance.* The canonical source has universe size $q = #q$ and $t = #t$ triples: #triples.map(tr => "(" + tr.map(str).join(", ") + ")").join(", "). The stored witness $(#tdm_ilp_sol.source_config.map(str).join(", "))$ selects triples #tdm_ilp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", "), which cover every element of $W$, $X$, and $Y$ exactly once #sym.checkmark. + *Step 1 -- Source instance.* The canonical source has universe size $q = #q$ and $t = #t$ triples: #triples.map(tr => "(" + fmt-values(tr) + ")").join(", "). The stored witness $(#fmt-values(tdm_ilp_sol.source_config))$ selects triples #tdm_ilp_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", "), which cover every element of $W$, $X$, and $Y$ exactly once #sym.checkmark. - *Step 2 -- Build the ILP.* One binary variable $x_j$ per triple ($j = 0, dots, #(t - 1)$). For each of the $3q = #(3 * q)$ elements across the three sets $W$, $X$, $Y$, add an equality constraint requiring that the sum of $x_j$ over all triples containing that element equals 1. This yields #tdm_ilp.target.instance.constraints.len() constraints and #tdm_ilp.target.instance.num_vars variables. + *Step 2 -- Build the ILP.* One binary variable $x_j$ per triple ($j = 0, dots, #(t - 1)$). For each of the $3q = #(3 * q)$ elements across the three sets $W$, $X$, $Y$, add an equality constraint requiring that the sum of $x_j$ over all triples containing that element equals 1. This yields #tdm_ilp.target.instance.constraints.len() constraints and #tdm_ilp.target.instance.variables.len() variables. - *Step 3 -- Verify a solution.* The target configuration $(#tdm_ilp_sol.target_config.map(str).join(", "))$ is identical to the source configuration because the mapping is one variable per triple with identity extraction. Each element-coverage constraint sums to exactly 1 #sym.checkmark. + *Step 3 -- Verify a solution.* The target configuration $(#fmt-values(tdm_ilp_sol.target_config))$ is identical to the source configuration because the mapping is one variable per triple with identity extraction. Each element-coverage constraint sums to exactly 1 #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ] @@ -19314,9 +19133,9 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(tdm_mwd.source) + " -o three-dimensional-matching.json", - "pred reduce three-dimensional-matching.json --to " + target-spec(tdm_mwd) + " -o bundle.json", + "pred reduce three-dimensional-matching.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate three-dimensional-matching.json --config " + tdm_mwd_sol.source_config.map(str).join(","), + "pred evaluate three-dimensional-matching.json --config " + cli-config(tdm_mwd_sol.source_config), ) #{ @@ -19325,11 +19144,11 @@ The following table shows concrete variable overhead for example instances, take let triples = tdm_mwd.source.instance.triples let target = tdm_mwd.target.instance [ - *Step 1 -- Source instance.* The canonical source has universe size $q = #q$ and $m = #m$ triples: #triples.map(tr => "(" + tr.map(str).join(", ") + ")").join(", "). The stored witness $(#tdm_mwd_sol.source_config.map(str).join(", "))$ selects triples #tdm_mwd_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", "), covering every element of $W$, $X$, $Y$ exactly once #sym.checkmark. + *Step 1 -- Source instance.* The canonical source has universe size $q = #q$ and $m = #m$ triples: #triples.map(tr => "(" + fmt-values(tr) + ")").join(", "). The stored witness $(#fmt-values(tdm_mwd_sol.source_config))$ selects triples #tdm_mwd_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", "), covering every element of $W$, $X$, $Y$ exactly once #sym.checkmark. *Step 2 -- Build the parity-check matrix.* Allocate a #target.matrix.len() $times$ #target.matrix.at(0).len() binary matrix $H$ with row blocks $W$ (rows $0, dots, #(q - 1)$), $X$ (rows $#q, dots, #(2 * q - 1)$), and $Y$ (rows $#(2 * q), dots, #(3 * q - 1)$). For each triple $t_j = (a_j, b_j, c_j)$, set $H[a_j, j] = H[q + b_j, j] = H[2q + c_j, j] = 1$. Every column has exactly three 1s. Set the syndrome to the all-ones vector $bold(s) = 1^(3q)$ of length #target.target.len(). - *Step 3 -- Verify a solution.* The target codeword $(#tdm_mwd_sol.target_config.map(str).join(", "))$ has Hamming weight #tdm_mwd_sol.target_config.filter(x => x == 1).len() $= q$. Multiplying $H$ by this vector over $bold(F)_2$ recovers the all-ones syndrome, so each element of $W union X union Y$ is covered an odd number of times -- exactly once #sym.checkmark. + *Step 3 -- Verify a solution.* The target codeword $(#fmt-values(tdm_mwd_sol.target_config))$ has Hamming weight #tdm_mwd_sol.target_config.filter(x => x).len() $= q$. Multiplying $H$ by this vector over $bold(F)_2$ recovers the all-ones syndrome, so each element of $W union X union Y$ is covered an odd number of times -- exactly once #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness; the instance admits a second perfect matching $\{t_2, t_3\}$ with codeword $(0, 0, 1, 1)$ of the same weight. ] @@ -19361,20 +19180,20 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(tp_rcs.source) + " -o threepartition.json", - "pred reduce threepartition.json --to " + target-spec(tp_rcs) + " -o bundle.json", + "pred reduce threepartition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate threepartition.json --config " + tp_rcs_sol.source_config.map(str).join(","), + "pred evaluate threepartition.json --config " + cli-config(tp_rcs_sol.source_config), ) - *Step 1 -- Source instance.* The canonical Three-Partition instance has $3m = #tp_rcs.source.instance.sizes.len()$ elements with sizes $(#tp_rcs.source.instance.sizes.map(str).join(", "))$ and bound $B = #tp_rcs.source.instance.bound$, so $m = #(tp_rcs.source.instance.sizes.len() / 3)$ groups are required. + *Step 1 -- Source instance.* The canonical Three-Partition instance has $3m = #tp_rcs.source.instance.sizes.len()$ elements with sizes $(#fmt-values(tp_rcs.source.instance.sizes))$ and bound $B = #tp_rcs.source.instance.bound$, so $m = #(tp_rcs.source.instance.sizes.len() / 3)$ groups are required. *Step 2 -- Build the Resource-Constrained Scheduling instance.* Each element $a_i$ becomes a unit-length task with resource requirement $r_i = a_i$. The reduction sets $p = #tp_rcs.target.instance.num_processors$ processors, a single resource with bound $#tp_rcs.target.instance.resource_bounds.at(0)$, and deadline $D = #tp_rcs.target.instance.deadline$. The target has #tp_rcs.target.instance.resource_requirements.len() tasks with resource requirements $(#tp_rcs.target.instance.resource_requirements.map(r => str(r.at(0))).join(", "))$. - *Step 3 -- Verify the canonical witness.* The source config $(#tp_rcs_sol.source_config.map(str).join(", "))$ assigns elements to groups: + *Step 3 -- Verify the canonical witness.* The source config $(#fmt-values(tp_rcs_sol.source_config))$ assigns elements to groups: #for g in range(tp_rcs.target.instance.deadline) [ - Slot #g: elements ${#tp_rcs_sol.source_config.enumerate().filter(((i, x)) => x == g).map(((i, x)) => str(i)).join(", ")}$ with sizes $#tp_rcs_sol.source_config.enumerate().filter(((i, x)) => x == g).map(((i, x)) => str(tp_rcs.source.instance.sizes.at(i))).join(" + ") = #tp_rcs.source.instance.bound = B$ #sym.checkmark ] - Each slot has exactly 3 tasks and each slot's resource usage sums to $B$. The target config is $(#tp_rcs_sol.target_config.map(str).join(", "))$, matching the source config since task $t_i$ is assigned to the same slot as element $a_i$. + Each slot has exactly 3 tasks and each slot's resource usage sums to $B$. The target config is $(#fmt-values(tp_rcs_sol.target_config))$, matching the source config since task $t_i$ is assigned to the same slot as element $a_i$. *Multiplicity:* The fixture stores one canonical witness. Other valid 3-partitions (if any) would yield equally valid schedules. ], @@ -19396,18 +19215,18 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(tp_srd.source) + " -o tp.json", - "pred reduce tp.json --to " + target-spec(tp_srd) + " -o bundle.json", + "pred reduce tp.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate tp.json --config " + tp_srd_sol.source_config.map(str).join(","), + "pred evaluate tp.json --config " + cli-config(tp_srd_sol.source_config), ) - *Step 1 -- Source instance.* The canonical 3-Partition instance has $3m = #tp_srd.source.instance.sizes.len()$ elements with sizes $(#tp_srd.source.instance.sizes.map(str).join(", "))$ and bound $B = #tp_srd.source.instance.bound$. Since $m = #(tp_srd.source.instance.sizes.len() / 3)$, we must partition the elements into $#(tp_srd.source.instance.sizes.len() / 3)$ groups, each summing to $B$. + *Step 1 -- Source instance.* The canonical 3-Partition instance has $3m = #tp_srd.source.instance.sizes.len()$ elements with sizes $(#fmt-values(tp_srd.source.instance.sizes))$ and bound $B = #tp_srd.source.instance.bound$. Since $m = #(tp_srd.source.instance.sizes.len() / 3)$, we must partition the elements into $#(tp_srd.source.instance.sizes.len() / 3)$ groups, each summing to $B$. *Step 2 -- Construct element tasks.* Each element $a_i$ becomes a task with processing time $p_i = a_i$, release time $r_i = 0$, and deadline $d_i = H$ where $H = m(B+1) - 1 = #(tp_srd.source.instance.sizes.len() / 3) dot (#tp_srd.source.instance.bound + 1) - 1 = #(tp_srd.source.instance.sizes.len() / 3 * (tp_srd.source.instance.bound + 1) - 1)$. This gives #tp_srd.source.instance.sizes.len() element tasks with lengths $(#tp_srd.target.instance.lengths.slice(0, tp_srd.source.instance.sizes.len()).map(str).join(", "))$. *Step 3 -- Construct filler tasks.* Add $m - 1 = #(tp_srd.source.instance.sizes.len() / 3 - 1)$ filler task(s). Filler $j$ has length $1$, release time $r_j = (j+1)B + j = #tp_srd.target.instance.release_times.at(tp_srd.source.instance.sizes.len())$, and deadline $d_j = r_j + 1 = #tp_srd.target.instance.deadlines.at(tp_srd.source.instance.sizes.len())$. This tight window pins each filler to a single time unit, splitting the timeline into $m$ slots of width $B = #tp_srd.source.instance.bound$. - *Step 4 -- Verify a solution.* The source witness assigns elements to groups: $[#tp_srd_sol.source_config.map(str).join(", ")]$. Group 0 contains elements with sizes $(#{ let s = tp_srd.source.instance.sizes; tp_srd_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => str(s.at(i))).join(", ")})$, summing to $#{ let s = tp_srd.source.instance.sizes; tp_srd_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => s.at(i)).sum() } = B$ #sym.checkmark. Group 1 contains sizes $(#{ let s = tp_srd.source.instance.sizes; tp_srd_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(s.at(i))).join(", ")})$, summing to $#{ let s = tp_srd.source.instance.sizes; tp_srd_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => s.at(i)).sum() } = B$ #sym.checkmark. The target Lehmer code is $[#tp_srd_sol.target_config.map(str).join(", ")]$: element tasks fill slot $[0, B)$, the filler occupies its tight window $[B, B+1)$, and remaining elements fill slot $[B+1, 2B+1)$. + *Step 4 -- Verify a solution.* The source witness assigns elements to groups: $[#fmt-values(tp_srd_sol.source_config)]$. Group 0 contains elements with sizes $(#{ let s = tp_srd.source.instance.sizes; tp_srd_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => str(s.at(i))).join(", ")})$, summing to $#{ let s = tp_srd.source.instance.sizes; tp_srd_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => s.at(i)).sum() } = B$ #sym.checkmark. Group 1 contains sizes $(#{ let s = tp_srd.source.instance.sizes; tp_srd_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(s.at(i))).join(", ")})$, summing to $#{ let s = tp_srd.source.instance.sizes; tp_srd_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => s.at(i)).sum() } = B$ #sym.checkmark. The target task order is $[#fmt-values(tp_srd_sol.target_config)]$: element tasks fill slot $[0, B)$, the filler occupies its tight window $[B, B+1)$, and remaining elements fill slot $[B+1, 2B+1)$. *Multiplicity:* The fixture stores one canonical witness. A second valid partition (swapping groups) exists, but both map to distinct Lehmer codes. ], @@ -19435,18 +19254,18 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(mc_mcbs.source) + " -o maxcut.json", - "pred reduce maxcut.json --to " + target-spec(mc_mcbs) + " -o bundle.json", + "pred reduce maxcut.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate maxcut.json --config " + mc_mcbs_sol.source_config.map(str).join(","), + "pred evaluate maxcut.json --config " + cli-config(mc_mcbs_sol.source_config), ) - *Step 1 -- Source instance.* The source MaxCut instance is a triangle $G = (V, E)$ with $n = #mc_mcbs.source.instance.graph.num_vertices$ vertices, $|E| = #mc_mcbs.source.instance.graph.edges.len()$ edges, and unit weights $w = (#mc_mcbs.source.instance.edge_weights.map(str).join(", "))$. A maximum cut partitions vertices into two sides to maximize crossing-edge weight; here the optimum is $2$ (any single vertex versus the other two). + *Step 1 -- Source instance.* The source MaxCut instance is a triangle $G = (V, E)$ with $n = #mc_mcbs.source.instance.graph.num_vertices$ vertices, $|E| = #mc_mcbs.source.instance.graph.edges.len()$ edges, and unit weights $w = (#fmt-values(mc_mcbs.source.instance.edge_weights))$. A maximum cut partitions vertices into two sides to maximize crossing-edge weight; here the optimum is $2$ (any single vertex versus the other two). *Step 2 -- Pad to even vertex count.* Since $n = 3$ is odd, set $n' = n + 1 = 4$. The target complete graph has $N = 2 n' = #mc_mcbs.target.instance.graph.num_vertices$ vertices, giving $#mc_mcbs.target.instance.graph.num_vertices dot (#mc_mcbs.target.instance.graph.num_vertices - 1) slash 2 = #mc_mcbs.target.instance.graph.edges.len()$ edges. - *Step 3 -- Invert weights on $K_#mc_mcbs.target.instance.graph.num_vertices$.* Compute $w_"max" = 1 + max w(e) = 2$. For each original edge $(i, j) in E$, the inverted weight is $tilde(w)(i,j) = w_"max" - w(i,j) = 2 - 1 = 1$. All other edges (including those to padding vertices) receive weight $w_"max" = 2$. Designate source $s = #mc_mcbs.target.instance.source$, sink $t = #mc_mcbs.target.instance.sink$, size bound $b = #mc_mcbs.target.instance.size_bound$. The target edge weights are $(#mc_mcbs.target.instance.edge_weights.map(str).join(", "))$. + *Step 3 -- Invert weights on $K_#mc_mcbs.target.instance.graph.num_vertices$.* Compute $w_"max" = 1 + max w(e) = 2$. For each original edge $(i, j) in E$, the inverted weight is $tilde(w)(i,j) = w_"max" - w(i,j) = 2 - 1 = 1$. All other edges (including those to padding vertices) receive weight $w_"max" = 2$. Designate source $s = #mc_mcbs.target.instance.source$, sink $t = #mc_mcbs.target.instance.sink$, size bound $b = #mc_mcbs.target.instance.size_bound$. The target edge weights are $(#fmt-values(mc_mcbs.target.instance.edge_weights))$. - *Step 4 -- Verify a solution.* The canonical source witness is $(#mc_mcbs_sol.source_config.map(str).join(", "))$: vertices $0, 1$ on side $0$ and vertex $2$ on side $1$, cutting $2$ of $3$ edges (max cut value $= 2$). The target witness is $(#mc_mcbs_sol.target_config.map(str).join(", "))$. Check: (1) the first $n = #mc_mcbs.source.instance.graph.num_vertices$ entries match the source partition #sym.checkmark; (2) source vertex $s = #mc_mcbs.target.instance.source$ and sink vertex $t = #mc_mcbs.target.instance.sink$ are on opposite sides #sym.checkmark; (3) each side has exactly $b = #mc_mcbs.target.instance.size_bound$ vertices (balanced bisection) #sym.checkmark. + *Step 4 -- Verify a solution.* The canonical source witness is $(#fmt-values(mc_mcbs_sol.source_config))$: vertices $0, 1$ on side $0$ and vertex $2$ on side $1$, cutting $2$ of $3$ edges (max cut value $= 2$). The target witness is $(#fmt-values(mc_mcbs_sol.target_config))$. Check: (1) the first $n = #mc_mcbs.source.instance.graph.num_vertices$ entries match the source partition #sym.checkmark; (2) source vertex $s = #mc_mcbs.target.instance.source$ and sink vertex $t = #mc_mcbs.target.instance.sink$ are on opposite sides #sym.checkmark; (3) each side has exactly $b = #mc_mcbs.target.instance.size_bound$ vertices (balanced bisection) #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. The triangle has $3$ maximum cuts of value $2$ (isolate any one vertex); padding vertices can be assigned to balance both sides, yielding multiple valid target configurations. ], @@ -19465,8 +19284,8 @@ The following table shows concrete variable overhead for example instances, take #let mc_mmc_sol = mc_mmc.solutions.at(0) #let mc_mmc_n = mc_mmc.source.instance.graph.num_vertices #let mc_mmc_W = mc_mmc.source.instance.edge_weights.fold(0, (a, b) => a + b) -#let mc_mmc_S = mc_mmc_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ") -#let mc_mmc_Sbar = mc_mmc_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => str(i)).join(", ") +#let mc_mmc_S = mc_mmc_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => str(i)).join(", ") +#let mc_mmc_Sbar = mc_mmc_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => str(i)).join(", ") #let mc_mmc_cut = mc_mmc.source.instance.graph.edges.filter(e => mc_mmc_sol.source_config.at(e.at(0)) != mc_mmc_sol.source_config.at(e.at(1))).len() #reduction-rule("MaxCut", "MinimumMatrixCover", example: true, @@ -19474,9 +19293,9 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(mc_mmc.source) + " -o maxcut.json", - "pred reduce maxcut.json --to " + target-spec(mc_mmc) + " -o bundle.json", + "pred reduce maxcut.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate maxcut.json --config " + mc_mmc_sol.source_config.map(str).join(","), + "pred evaluate maxcut.json --config " + cli-config(mc_mmc_sol.source_config), ) *Step 1 -- Source instance.* The source MaxCut instance is the 4-cycle $C_#mc_mmc_n$ with $n = #mc_mmc_n$ vertices, edges $E = {(0,1), (1,2), (2,3), (0,3)}$, and unit weights, so the total weight is $W = #mc_mmc_W$. @@ -19487,7 +19306,7 @@ The following table shows concrete variable overhead for example instances, take $ which is a valid MinimumMatrixCover instance (nonnegative integer entries). - *Step 3 -- Verify the witness.* The canonical witness is the sign assignment $f = (+1, -1, +1, -1)$, encoded as the binary config $(#mc_mmc_sol.source_config.map(str).join(", "))$. The partition is $S = {#mc_mmc_S}$ versus $overline(S) = {#mc_mmc_Sbar}$, cutting all #mc_mmc_cut edges. The quadratic form evaluates to $sum_(i, j) a_(i j) f(i) f(j) = 2 W - 4 dot #mc_mmc_cut = #(2 * mc_mmc_W) - #(4 * mc_mmc_cut) = #(2 * mc_mmc_W - 4 * mc_mmc_cut)$, which matches the MinimumMatrixCover optimum and is consistent with #raw("MaxCut") $= (2 W - "min" Q F) / 4 = #mc_mmc_cut$ #sym.checkmark. + *Step 3 -- Verify the witness.* The canonical witness is the sign assignment $f = (+1, -1, +1, -1)$, encoded as the binary config $(#fmt-values(mc_mmc_sol.source_config))$. The partition is $S = {#mc_mmc_S}$ versus $overline(S) = {#mc_mmc_Sbar}$, cutting all #mc_mmc_cut edges. The quadratic form evaluates to $sum_(i, j) a_(i j) f(i) f(j) = 2 W - 4 dot #mc_mmc_cut = #(2 * mc_mmc_W) - #(4 * mc_mmc_cut) = #(2 * mc_mmc_W - 4 * mc_mmc_cut)$, which matches the MinimumMatrixCover optimum and is consistent with #raw("MaxCut") $= (2 W - "min" Q F) / 4 = #mc_mmc_cut$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. The form is invariant under $f arrow.r -f$, so the complementary assignment $(-1, +1, -1, +1)$ (config $(0, 1, 0, 1)$) is equally optimal. ], @@ -19512,16 +19331,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hp_ist.source) + " -o hampath.json", - "pred reduce hampath.json --to " + target-spec(hp_ist) + " -o bundle.json", + "pred reduce hampath.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hampath.json --config " + hp_ist_sol.source_config.map(str).join(","), + "pred evaluate hampath.json --config " + cli-config(hp_ist_sol.source_config), ) *Step 1 -- Source instance.* Graph $G$ with $n = #graph-num-vertices(hp_ist.source.instance)$ vertices and $|E| = #graph-num-edges(hp_ist.source.instance)$ edges. *Step 2 -- Identity reduction.* Target host graph is identical. Target tree $T = P_#graph-num-vertices(hp_ist.target.instance)$ with #hp_ist.target.instance.tree.edges.len() edges. - *Step 3 -- Verify a solution.* Hamiltonian path visits vertices in order $(#hp_ist_sol.source_config.map(str).join(", "))$. The isomorphism maps $P_n$ to this path in $G$ #sym.checkmark. + *Step 3 -- Verify a solution.* Hamiltonian path visits vertices in order $(#fmt-values(hp_ist_sol.source_config))$. The isomorphism maps $P_n$ to this path in $G$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ], @@ -19544,9 +19363,9 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(x3c_gf2.source) + " -o x3c.json", - "pred reduce x3c.json --to " + target-spec(x3c_gf2) + " -o bundle.json", + "pred reduce x3c.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate x3c.json --config " + x3c_gf2_sol.source_config.map(str).join(","), + "pred evaluate x3c.json --config " + cli-config(x3c_gf2_sol.source_config), ) #{ @@ -19560,9 +19379,9 @@ The following table shows concrete variable overhead for example instances, take for elements 3 and 4, $x_1 + x_2 + 1 = 0$ and $x_1 x_2 = 0$; for element 5, $x_1 + 1 = 0$. - *Step 3 -- Evaluate the canonical target witness.* The target assignment is $x = (#x.map(str).join(", ")) = (1, 1, 0)$. Substituting gives $1 + 0 + 1 = 0$ mod 2, $1 dot 0 = 0$, and $1 + 1 = 0$ mod 2, which are exactly the three polynomial patterns appearing in the fixture. + *Step 3 -- Evaluate the canonical target witness.* The target assignment is $x = (#fmt-values(x)) = (1, 1, 0)$. Substituting gives $1 + 0 + 1 = 0$ mod 2, $1 dot 0 = 0$, and $1 + 1 = 0$ mod 2, which are exactly the three polynomial patterns appearing in the fixture. - *Step 4 -- Verify the witness pair.* The two 1-entries in $x$ select $C_0$ and $C_1$, while $x_2 = 0$ omits $C_2$. Thus the target witness encodes the same exact cover as the source configuration $(#x3c_gf2_sol.source_config.map(str).join(", "))$ #sym.checkmark. + *Step 4 -- Verify the witness pair.* The two 1-entries in $x$ select $C_0$ and $C_1$, while $x_2 = 0$ omits $C_2$. Thus the target witness encodes the same exact cover as the source configuration $(#fmt-values(x3c_gf2_sol.source_config))$ #sym.checkmark. ] } @@ -19587,17 +19406,17 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(part_pp.source) + " -o partition.json", - "pred reduce partition.json --to " + target-spec(part_pp) + " -o bundle.json", + "pred reduce partition.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate partition.json --config " + part_pp_sol.source_config.map(str).join(","), + "pred evaluate partition.json --config " + cli-config(part_pp_sol.source_config), ) #{ - let left-sum = part_pp_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => part_pp.source.instance.sizes.at(i)).sum() - let right-sum = part_pp_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => part_pp.source.instance.sizes.at(i)).sum() + let left-sum = part_pp_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => part_pp.source.instance.sizes.at(i)).sum() + let right-sum = part_pp_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => part_pp.source.instance.sizes.at(i)).sum() let prod = part_pp_sol.target_config [ - *Step 1 -- Source instance.* The Partition fixture has sizes $(#part_pp.source.instance.sizes.map(str).join(", "))$ with total $#part_pp.source.instance.sizes.sum()$, so $Q = #(part_pp.source.instance.sizes.sum() / 2)$. The canonical source vector $(#part_pp_sol.source_config.map(str).join(", "))$ splits the instance into sums $#left-sum$ and $#right-sum$. + *Step 1 -- Source instance.* The Partition fixture has sizes $(#fmt-values(part_pp.source.instance.sizes))$ with total $#part_pp.source.instance.sizes.sum()$, so $Q = #(part_pp.source.instance.sizes.sum() / 2)$. The canonical source vector $(#fmt-values(part_pp_sol.source_config))$ splits the instance into sums $#left-sum$ and $#right-sum$. *Step 2 -- Build the period table.* #table( columns: (auto, auto, auto, auto, auto), @@ -19612,9 +19431,9 @@ The following table shows concrete variable overhead for example instances, take [$P_5$], [#part_pp.target.instance.capacities.at(5)], [#part_pp.target.instance.setup_costs.at(5)], [#part_pp.target.instance.demands.at(5)], [#prod.at(5)], ) The first five periods encode the partition elements, and the last period carries the demand of $10$ units. - *Step 3 -- Track cumulative production and inventory.* The stored plan $(#prod.map(str).join(", "))$ gives cumulative production $0, 0, 0, 4, 10, 10$ against cumulative demand $0, 0, 0, 0, 0, 10$. Hence the inventory levels are $0, 0, 0, 4, 10, 0$, so every prefix remains feasible. + *Step 3 -- Track cumulative production and inventory.* The stored plan $(#fmt-values(prod))$ gives cumulative production $0, 0, 0, 4, 10, 10$ against cumulative demand $0, 0, 0, 0, 0, 10$. Hence the inventory levels are $0, 0, 0, 4, 10, 0$, so every prefix remains feasible. - *Step 4 -- Check the cost and recover the partition.* Only periods $P_3$ and $P_4$ are active, so the total cost is just the setup cost $4 + 6 = #part_pp.target.instance.cost_bound$; production and inventory costs are all zero in the fixture. The active periods therefore recover the source vector $(#part_pp_sol.source_config.map(str).join(", "))$, selecting the subset of size #right-sum #sym.checkmark. + *Step 4 -- Check the cost and recover the partition.* Only periods $P_3$ and $P_4$ are active, so the total cost is just the setup cost $4 + 6 = #part_pp.target.instance.cost_bound$; production and inventory costs are all zero in the fixture. The active periods therefore recover the source vector $(#fmt-values(part_pp_sol.source_config))$, selecting the subset of size #right-sum #sym.checkmark. ] } @@ -19639,16 +19458,16 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(hpbtv_lp.source) + " -o hampath2v.json", - "pred reduce hampath2v.json --to " + target-spec(hpbtv_lp) + " -o bundle.json", + "pred reduce hampath2v.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate hampath2v.json --config " + hpbtv_lp_sol.source_config.map(str).join(","), + "pred evaluate hampath2v.json --config " + cli-config(hpbtv_lp_sol.source_config), ) *Step 1 -- Source instance.* Graph $G$ with $n = #graph-num-vertices(hpbtv_lp.source.instance)$ vertices, $|E| = #graph-num-edges(hpbtv_lp.source.instance)$ edges, $s = #hpbtv_lp.source.instance.source_vertex$, $t = #hpbtv_lp.source.instance.target_vertex$. *Step 2 -- Identity reduction.* Target graph is identical with unit edge lengths. $K = n - 1 = #(graph-num-vertices(hpbtv_lp.source.instance) - 1)$, same $s$ and $t$. - *Step 3 -- Verify a solution.* Source Hamiltonian path visits vertices $(#hpbtv_lp_sol.source_config.map(str).join(", "))$ from $s = #hpbtv_lp.source.instance.source_vertex$ to $t = #hpbtv_lp.source.instance.target_vertex$. Target selects #hpbtv_lp_sol.target_config.filter(x => x == 1).len() edges, total length $= n - 1 = K$ #sym.checkmark. + *Step 3 -- Verify a solution.* Source Hamiltonian path visits vertices $(#fmt-values(hpbtv_lp_sol.source_config))$ from $s = #hpbtv_lp.source.instance.source_vertex$ to $t = #hpbtv_lp.source.instance.target_vertex$. Target selects #hpbtv_lp_sol.target_config.filter(x => x).len() edges, total length $= n - 1 = K$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. ], @@ -19659,7 +19478,7 @@ The following table shows concrete variable overhead for example instances, take _Correctness._ ($arrow.r.double$) A Hamiltonian $s$-$t$ path has $n - 1$ edges of length 1 each, giving total length $n - 1 = K$. ($arrow.l.double$) A simple $s'$-$t'$ path of length $>= K = n - 1$ has $>= n - 1$ edges. Since a simple path on $n$ vertices can have at most $n - 1$ edges, it has exactly $n - 1$ edges and visits all vertices -- it is a Hamiltonian $s$-$t$ path. - _Solution extraction._ From the edge-selection vector, trace the path from $s$ following selected edges to reconstruct the vertex permutation. + _Solution extraction._ Evaluate the target configuration once and apply the aggregate predicate: the value must be finite and equal to $n-1$. Reject infeasible selections and shorter paths before traversing any edges. The target feasibility check guarantees a single connected simple path from $s$ to $t$; its $n-1$ edges visit all $n$ vertices. Start at $s$ and repeatedly take the neighbor other than the preceding vertex. This terminates at $t$ and yields the source permutation without repetitions. The same argument applies to every supplied target configuration, independently of any caller claim of optimality. An absent target maximum or a proven maximum below $n-1$ certifies a NO source instance. The source requires distinct valid endpoints, so $n>=2$; no empty-graph or equal-endpoint convention is introduced. The target value resolves to `Max` because unit weight `One` has sum type `i64`. Checked evaluation errors are propagated, and the threshold comparison uses exact integer conversion. ] // 18. GraphPartitioning → MaxCut (from main codebase) @@ -19671,17 +19490,17 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example " + problem-spec(gp_mc.source) + " -o graphpart.json", - "pred reduce graphpart.json --to " + target-spec(gp_mc) + " -o bundle.json", + "pred reduce graphpart.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred evaluate graphpart.json --config " + gp_mc_sol.source_config.map(str).join(","), + "pred evaluate graphpart.json --config " + cli-config(gp_mc_sol.source_config), ) #{ let n = graph-num-vertices(gp_mc.source.instance) let m = graph-num-edges(gp_mc.source.instance) let penalty = m + 1 - let side-a = gp_mc_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => i) - let side-b = gp_mc_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => i) + let side-a = gp_mc_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => i) + let side-b = gp_mc_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) let source-cut = gp_mc.source.instance.graph.edges.filter(e => gp_mc_sol.source_config.at(e.at(0)) != gp_mc_sol.source_config.at(e.at(1))).len() let target-weight = gp_mc.target.instance.graph.edges.enumerate().filter(((i, e)) => gp_mc_sol.target_config.at(e.at(0)) != gp_mc_sol.target_config.at(e.at(1))).map(((i, e)) => gp_mc.target.instance.edge_weights.at(i)).sum() let unbalanced-pairs = (n / 2 - 1) * (n / 2 + 1) @@ -19691,7 +19510,7 @@ The following table shows concrete variable overhead for example instances, take *Step 2 -- Build the weighted complete graph.* The target has $#graph-num-vertices(gp_mc.target.instance)$ vertices and $#graph-num-edges(gp_mc.target.instance)$ edges. Original edges receive weight $P - 1 = #(penalty - 1)$, while non-edges receive weight $P = #penalty$. - *Step 3 -- Verify the canonical witness.* The balanced partition $(#gp_mc_sol.source_config.map(str).join(", "))$ gives sides $A = {#side-a.map(str).join(", ")}$ and $B = {#side-b.map(str).join(", ")}$ with $#(side-a.len() * side-b.len())$ crossing pairs. It cuts #source-cut source edges, so the identical Max-Cut partition has weight $#target-weight = #penalty dot #(side-a.len() * side-b.len()) - #source-cut$. Any unbalanced $2$-$4$ split has at most #unbalanced-pairs crossing pairs and therefore weight at most $#unbalanced-upper < #target-weight$, so the optimum is forced to be balanced #sym.checkmark. + *Step 3 -- Verify the canonical witness.* The balanced partition $(#fmt-values(gp_mc_sol.source_config))$ gives sides $A = {#fmt-values(side-a)}$ and $B = {#fmt-values(side-b)}$ with $#(side-a.len() * side-b.len())$ crossing pairs. It cuts #source-cut source edges, so the identical Max-Cut partition has weight $#target-weight = #penalty dot #(side-a.len() * side-b.len()) - #source-cut$. Any unbalanced $2$-$4$ split has at most #unbalanced-pairs crossing pairs and therefore weight at most $#unbalanced-upper < #target-weight$, so the optimum is forced to be balanced #sym.checkmark. ] } @@ -19729,10 +19548,10 @@ The following table shows concrete variable overhead for example instances, take extra: [ #pred-commands( "pred create --example PrizeCollectingSteinerForest -o pcsf.json", - "pred reduce pcsf.json --to " + target-spec(pcsf_st) + " -o bundle.json", + "pred reduce pcsf.json --via route.json -o bundle.json", "pred solve bundle.json", ) - The canonical PCSF source has $beta = #pcsf_st.source.instance.beta$, $omega = #pcsf_st.source.instance.omega$, and prizes $p = (#pcsf_st_prizes.at(0), #pcsf_st_prizes.at(1), #pcsf_st_prizes.at(2))$. The target SteinerTree has $|V_H| = n + k + 1 = #(pcsf_st_n + pcsf_st_k + 1)$ vertices, $|E_H| = m + n + 2 k = #(pcsf_st_m + pcsf_st_n + 2 * pcsf_st_k)$ edges, and $|T_H| = k + 1 = #(pcsf_st_k + 1)$ terminals, matching the registered overhead formulas. + The canonical PCSF source has $beta = #pcsf_st.source.instance.beta$, $omega = #pcsf_st.source.instance.omega$, and prizes $p = (#pcsf_st_prizes.at(0), #pcsf_st_prizes.at(1), #pcsf_st_prizes.at(2))$. The target SteinerTree has $|V_H| = n + k + 1 = #(pcsf_st_n + pcsf_st_k + 1)$ vertices, $|E_H| = m + n + 2 k = #(pcsf_st_m + pcsf_st_n + 2 * pcsf_st_k)$ edges, and $|T_H| = k + 1 = #(pcsf_st_k + 1)$ terminals, matching the registered exact size formulas. ], )[ Bienstock, Goemans, Simchi-Levi, Williamson @BienstockGoemansSimchiLeviWilliamson1993 introduced the prize/penalty framework for prize-collecting network design; Tuncbag and coauthors @TuncbagEtAl2013PCSF @TuncbagEtAl2012RECOMB used the same artificial-root idea to translate PCSF into a rooted prize-collecting Steiner tree on biological networks. The combined construction recorded here adds a per-vertex auxiliary-terminal gadget that compiles the remaining omitted-prize term `beta * p(v)` into ordinary Steiner-tree edge costs, so the target is a plain (unweighted-prize) Steiner Tree instance. diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 186f70de2..369d309f0 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -1,3 +1,13 @@ +@misc{ozkayaCatalyurek2022, + author = {M. Yusuf {\"O}zkaya and {\"U}mit V. {{\c C}}ataly{\"u}rek}, + title = {A Simple and Elegant Mathematical Formulation for the Acyclic {DAG} Partitioning Problem}, + year = {2022}, + eprint = {2207.13638}, + archiveprefix = {arXiv}, + primaryclass = {cs.DS}, + doi = {10.48550/arXiv.2207.13638} +} + @inproceedings{bouchez2006, author = {Florent Bouchez and Alain Darte and Christophe Guillon and Fabrice Rastello}, title = {Register Allocation: What Does the {NP}-Completeness Proof of {Chaitin} et al. Really Prove?}, @@ -31,6 +41,14 @@ @article{brunoSethi1976 doi = {10.1145/321958.321974} } +@inproceedings{sethi1973, + author = {Ravi Sethi}, + title = {Complete Register Allocation Problems}, + booktitle = {Proceedings of the Fifth Annual ACM Symposium on Theory of Computing}, + year = {1973}, + doi = {10.1145/800125.804049} +} + @article{sethi1975, author = {Ravi Sethi}, title = {Complete Register Allocation Problems}, @@ -216,6 +234,27 @@ @inproceedings{karp1972 pages = {85--103} } +@book{micciancio2002, + author = {Daniele Micciancio and Shafi Goldwasser}, + title = {Complexity of Lattice Problems: A Cryptographic Perspective}, + publisher = {Springer New York}, + series = {The Springer International Series in Engineering and Computer Science}, + volume = {671}, + year = {2002}, + doi = {10.1007/978-1-4615-0897-7} +} + +@article{fincke1985, + author = {Ulrich Fincke and Michael Pohst}, + title = {Improved Methods for Calculating Vectors of Short Length in a Lattice, Including a Complexity Analysis}, + journal = {Mathematics of Computation}, + volume = {44}, + number = {170}, + pages = {463--471}, + year = {1985}, + doi = {10.2307/2007966} +} + @article{lagarias1985, author = {Jeffrey C. Lagarias and Andrew M. Odlyzko}, title = {Solving Low-Density Subset Sum Problems}, @@ -283,7 +322,7 @@ @article{galilMegiddo1977 number = {2}, pages = {179--182}, year = {1977}, - doi = {10.1016/0304-3975(77)90005-1} + doi = {10.1016/0304-3975(77)90005-6} } @article{florianLenstraRinnooyKan1980, @@ -449,7 +488,8 @@ @article{sahni1974 volume = {3}, number = {4}, pages = {262--279}, - year = {1974} + year = {1974}, + doi = {10.1137/0203021} } @article{abdelWahabKameda1978, @@ -1454,6 +1494,17 @@ @article{hu1961 doi = {10.1287/opre.9.6.841} } +@article{hu1974ocst, + author = {Te Chiang Hu}, + title = {Optimum Communication Spanning Trees}, + journal = {SIAM Journal on Computing}, + volume = {3}, + number = {3}, + pages = {188--195}, + year = {1974}, + doi = {10.1137/0203015} +} + @article{adolphsonHu1973, author = {Donald Adolphson and Te Chiang Hu}, title = {Optimal Linear Ordering}, @@ -1695,12 +1746,13 @@ @article{horn1973 year = {1973} } -@techreport{plaisted1976, - author = {David A. Plaisted}, - title = {Some Polynomial and Integer Divisibility Problems Are {NP}-Hard}, - institution = {Stanford University, Department of Computer Science}, - number = {STAN-CS-76-583}, - year = {1976} +@inproceedings{plaisted1976, + author = {David A. Plaisted}, + title = {Some Polynomial and Integer Divisibility Problems Are {NP}-Hard}, + booktitle = {17th Annual Symposium on Foundations of Computer Science}, + pages = {264--267}, + year = {1976}, + doi = {10.1109/SFCS.1976.29} } @article{Streif2021, @@ -1775,33 +1827,6 @@ @article{yannakakis1980 doi = {10.1137/0138030} } -@inproceedings{edmonds1970, - author = {Jack Edmonds}, - title = {Submodular functions, matroids, and certain polyhedra}, - booktitle = {Combinatorial Structures and Their Applications}, - pages = {69--87}, - year = {1970}, - publisher = {Gordon and Breach} -} - -@article{doron2024, - author = {Ilan Doron-Arad and Ariel Kulik and Hadas Shachnai}, - title = {You (Almost) Can't Beat Brute Force for 3-Matroid Intersection}, - journal = {arXiv preprint arXiv:2412.02217}, - year = {2024} -} - -@article{fomin2019, - author = {Fedor V. Fomin and Daniel Lokshtanov and Fahad Panolan and Saket Saurabh}, - title = {Exact Algorithms via Monotone Local Search}, - journal = {Journal of the ACM}, - volume = {66}, - number = {2}, - pages = {1--23}, - year = {2019}, - doi = {10.1145/3277568} -} - @book{vonNeumannMorgenstern1944, author = {John von Neumann and Oskar Morgenstern}, title = {Theory of Games and Economic Behavior}, @@ -2143,3 +2168,156 @@ @article{berlekampMcElieceTilborg1978 doi = {10.1109/TIT.1978.1055873} } + +@article{gouveiaLopes2000, + author = {Luis Gouveia and Maria Joao Lopes}, + title = {Valid Inequalities for Non-Unit Demand Capacitated Spanning Tree Problems with Flow Costs}, + journal = {European Journal of Operational Research}, + volume = {121}, + number = {2}, + pages = {394--411}, + year = {2000}, + doi = {10.1016/S0377-2217(99)00043-0} +} + + +@inproceedings{jarvisaloEtAl2012, + author = {Matti J{"a}rvisalo and Petteri Kaski and Mikko Koivisto and Janne H. Korhonen}, + title = {Finding Efficient Circuits for Ensemble Computation}, + booktitle = {Theory and Applications of Satisfiability Testing -- SAT 2012}, + series = {Lecture Notes in Computer Science}, + volume = {7317}, + pages = {369--382}, + year = {2012}, + publisher = {Springer}, + doi = {10.1007/978-3-642-31612-8_28} +} + +@article{burrErdosLovasz1976Ramsey, + author = {Stefan A. Burr and Paul Erdős and László Lovász}, + title = {On graphs of {Ramsey} type}, + journal = {Ars Combinatoria}, + volume = {1}, + year = {1976}, + pages = {167--190}, + url = {https://users.renyi.hu/~p_erdos/1976-23.pdf} +} + +@article{goemansmyung1993catalog, + author = {Goemans, Michel X. and Myung, Young-soo}, + title = {A Catalog of Steiner Tree Formulations}, + journal = {Networks}, + year = {1993}, + volume = {23}, + number = {1}, + pages = {19--28}, + doi = {10.1002/net.3230230104}, +} + +@misc{borosgruber2014quadratization, + author = {Boros, Endre and Gruber, Aritanan}, + title = {On Quadratization of Pseudo-Boolean Functions}, + year = {2014}, + eprint = {1404.6538}, + archivePrefix = {arXiv}, + primaryClass = {math.OC}, + note = {Paper presented at ISAIM 2012; arXiv version 2014}, +} + +@techreport{bian2010ising, + author = {Bian, Zhengbing and Chudak, Fabian and Macready, William G. and Rose, Geordie}, + title = {The Ising Model: Teaching an Old Problem New Tricks}, + institution = {D-Wave Systems}, + year = {2010}, + url = {https://www.dwavequantum.com/media/vbklsvbh/weightedmaxsat_v2.pdf} +} + +@article{jackson2025, + author = {Marcel Jackson}, + title = {Flexible constraint satisfiability and a problem in semigroup theory}, + journal = {International Journal of Algebra and Computation}, + year = {2025}, + doi = {10.1142/S0218196725500225}, + url = {https://arxiv.org/abs/1512.03127} +} + +@article{vielma2015, + author = {Juan Pablo Vielma}, + title = {Mixed Integer Linear Programming Formulation Techniques}, + journal = {SIAM Review}, + volume = {57}, + number = {1}, + pages = {3--57}, + year = {2015}, + doi = {10.1137/130915303}, + url = {https://juan-pablo-vielma.github.io/publications/Mixed-Integer-Linear-Programming-Formulation-Techniques.pdf} +} + +@article{micciancio2001, + author = {Daniele Micciancio}, + title = {The Hardness of the Closest Vector Problem with Preprocessing}, + journal = {IEEE Transactions on Information Theory}, + volume = {47}, + number = {3}, + pages = {1212--1215}, + year = {2001}, + url = {https://cseweb.ucsd.edu/~daniele/papers/CVPP.pdf} +} + +@misc{larusic2010, + author = {Larusic, John and Punnen, Abraham P. and Aubanel, Eric}, + title = {Experimental Analysis of Heuristics for the Bottleneck Traveling Salesman Problem}, + year = {2010}, + howpublished = {Optimization Online preprint}, + url = {https://optimization-online.org/wp-content/uploads/2010/01/2516.pdf}, +} + +@misc{dally2006ee108, + author = {William J. Dally}, + title = {{EE108 Class Notes}}, + year = {2006}, + howpublished = {Stanford University, Section 10.4}, + url = {https://web.stanford.edu/class/archive/ee/ee108a/ee108a.1082/reader/ch1to12.pdf}, + note = {Copyright 2002--2006; Figures 10.15 and 10.16} +} + +@misc{karger2021flow, + author = {David Karger}, + title = {Flow: 6.854 Notes 6}, + year = {2021}, + howpublished = {MIT Advanced Algorithms lecture notes}, + url = {https://courses.csail.mit.edu/6.854/21/Notes/n06-flow.html}, + note = {Section 1.1: flow decomposition and cuts} +} + +@article{sahni-gonzalez1976, + author = {Sahni, Sartaj and Gonzalez, Teofilo}, + title = {P-Complete Approximation Problems}, + journal = {Journal of the ACM}, + volume = {23}, + number = {3}, + pages = {555--565}, + year = {1976}, + doi = {10.1145/321958.321975}, + url = {https://sites.cs.ucsb.edu/~teo/papers/JACM-PCom.pdf} +} + +@techreport{lipski1978consecutive, + author = {Witold Lipski Jr.}, + title = {Generalizations of the Consecutive Ones Property and Related {NP}-Complete Problems}, + institution = {Coordinated Science Laboratory, University of Illinois at Urbana-Champaign}, + number = {T-67}, + year = {1978}, + month = {October}, + url = {https://www.ideals.illinois.edu/items/99870/bitstreams/319087/data.pdf} +} + +@techreport{mandersAdleman1976, + author = {Kenneth L. Manders and Leonard Adleman}, + title = {{NP}-Complete Decision Problems for Binary Quadratics}, + institution = {Electronics Research Laboratory, University of California, Berkeley}, + number = {ERL-M615}, + year = {1976}, + month = {November}, + url = {https://digicoll.lib.berkeley.edu/record/134974/files/ERL-m-615.pdf} +} diff --git a/docs/src/cli-commands.md b/docs/src/cli-commands.md index 0232d9de3..97eb52217 100644 --- a/docs/src/cli-commands.md +++ b/docs/src/cli-commands.md @@ -10,7 +10,7 @@ pred show MIS pred list --rules ``` -`list` reports every problem with its aliases, variants, and reduction counts. `show` describes the resolved variant, its size fields, input schema, and incoming and outgoing reductions. Read that schema before constructing an instance. +`list` reports every problem with its aliases, variants, and reduction counts. `show` describes the resolved variant, its parameter fields, input schema, and incoming and outgoing reductions. Read that schema before constructing an instance.

Example: pred show MIS @@ -24,11 +24,11 @@ pred list --rules ## Names and variants ```bash -pred show MIS/SimpleGraph/i32 -pred path MIS/SimpleGraph/i32 ILP/bool +pred show MIS/SimpleGraph/i64 +pred path MIS/SimpleGraph/i64 ILP/bool ``` -Aliases such as `MIS` resolve to full names, and a bare name selects the declared default variant: `MIS` is `MaximumIndependentSet/SimpleGraph/One`. Slash-separated parameters select graph, weight, or other variant values. `One` means unit weights; passing non-unit `--weights` to `create` upgrades a default instance to `i32`. Name the exact variant when a reproducible endpoint matters. +Aliases such as `MIS` resolve to full names, and a bare name selects the declared default variant: `MIS` is `MaximumIndependentSet/SimpleGraph/One`. Slash-separated parameters select graph, weight, or other variant values. `One` means unit weights; passing non-unit `--weights` to `create` upgrades a default instance to `i64`. Name the exact variant when a reproducible endpoint matters. {{#include generated/pred-aliases.txt}} @@ -36,13 +36,13 @@ Aliases such as `MIS` resolve to full names, and a bare name selects the declare ```bash pred path MIS ILP -pred path MIS QUBO --all --max-paths 50 -pred path MIS QUBO --cost minimize:num_variables -o path.json +pred path MIS QUBO --limit 50 +pred path MIS QUBO --json -o paths.json pred from MIS --hops 2 pred to QUBO ``` -`path` finds the cheapest route between two endpoints; `--all` enumerates alternatives up to `--max-paths`. The default cost minimizes steps; `minimize:` uses the overhead metadata of a size field from `pred show`. `from` and `to` explore outgoing and incoming neighbors. Search defaults to reductions that can map a solution back. +`path` enumerates witness-capable simple routes between exact endpoints, without ranking. `--limit` accepts 1 through 999, or `all` for 999; the default is 20. JSON output contains `paths` and `truncated`. `from` and `to` explore outgoing and incoming neighbors.
Example: a multi-step path from Factoring to SpinGlass @@ -53,14 +53,14 @@ pred to QUBO
-Overhead formulas describe scaling bounds, not exact target sizes. A discovered route does not imply the target is cheap to solve; inspect the constructed target on representative instances. +Parameter transforms declare exact equalities, upper bounds, or unavailable relations. A discovered route does not imply the target is cheap to solve; inspect the constructed target on representative instances. ## Create ```bash pred create MIS --graph 0-1,1-2,2-3 -o problem.json -pred create MIS/SimpleGraph/i32 --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o weighted.json -pred create --example MVC/SimpleGraph/i32 --to MIS/SimpleGraph/i32 -o source.json +pred create MIS/SimpleGraph/i64 --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o weighted.json +pred create --example MVC/SimpleGraph/i64 --to MIS/SimpleGraph/i64 -o source.json pred create MIS --random --num-vertices 10 --edge-prob 0.3 --seed 42 -o random.json ``` @@ -79,21 +79,24 @@ pred create Factoring --target 6 --m 2 --n 2 -o factoring.json ```bash pred inspect problem.json -pred evaluate problem.json --config 1,0,1,0 -pred create MIS --graph 0-1,1-2,2-3 | pred evaluate - --config 1,0,1,0 +pred evaluate problem.json --config '[true,false,true,false]' +pred create MIS --graph 0-1,1-2,2-3 | pred evaluate - --config '[true,false,true,false]' ``` `inspect` reports the resolved variant and sizes of a problem file or reduction bundle. `evaluate` scores one configuration: selecting vertices 0 and 2 returns `Max(2)`, while selecting adjacent vertices returns `Max(None)`. Configurations follow each problem's variable domains and are not always binary. `-` reads from stdin. +For a problem file, JSON inspection includes `parameter_values`, the model's actual named instance parameters. These are separate from the `parameters` list of parameter names. + ## Reduce ```bash -pred reduce problem.json --to QUBO -o reduced.json +pred path MIS QUBO --json -o paths.json +python3 -c 'import json; print(json.dumps(json.load(open("paths.json"))["paths"][0]))' > path.json pred reduce problem.json --via path.json -o reduced.json -pred extract reduced.json --config 1,0,1,0 +pred extract reduced.json --config '[1,0,1,0]' ``` -The bundle contains the source instance, the target instance, and the variant-level path; keep it whole to preserve solution recovery. `--via` replays a path saved by `pred path -o`, whose source variant must match the input. `extract` maps a target-space configuration back to the source. +The bundle contains the source instance, the target instance, and the variant-level path; keep it whole to preserve solution recovery. `--via` replays one route extracted from the `paths` envelope, whose source variant must match the input. `extract` maps a target-space configuration back to the source. ## Solve @@ -105,16 +108,16 @@ pred solve reduced.json --timeout 30 --json | Solver | Behavior | |---|---| -| `ilp` (default) | Finds a route to ILP, solves the target, and recovers a source configuration | +| `ilp` | Executes the exact variant’s registered fixed ILP pipeline and recovers its source solution | | `brute-force` | Enumerates all configurations; for tiny instances and cross-checks | | `customized` | Exact structure-exploiting backends for selected models; see `pred solve --help` | -Solving a bundle solves the target and maps the result back; JSON output records the target result under `intermediate`. If the ILP solver reports no route, check `pred path ILP`. Aggregate-only problems return a value without a configuration. To verify a result, evaluate the returned configuration on the original instance, or compare the value with an exhaustive solve on a small example. +Default dispatch tries registered customized, ILP, then brute-force capabilities in that order. `pred inspect` lists the capabilities available for the exact variant. Solving a bundle solves its target and maps the result back; every successful solve returns a solution. A discovered path does not by itself provide a registered solver. Evaluate the returned solution on the original instance to verify its value. ## JSON and pipes ```bash -pred create MIS --graph 0-1,1-2,2-3 | pred reduce - --to QUBO | pred solve - --json +pred create MIS --graph 0-1,1-2,2-3 | pred reduce - --via path.json | pred solve - --json pred export-graph -o reduction_graph.json ``` diff --git a/docs/src/cli.md b/docs/src/cli.md index 22198cf3c..a5e337609 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -7,7 +7,7 @@ cargo install problemreductions-cli pred --version ``` -Rust and a native build toolchain are required. The default ILP backend is HiGHS; the `cplex` and `lp-solvers` features enable separately installed backends listed in the [CLI manifest](https://github.com/CodingThrust/problem-reductions/blob/main/problemreductions-cli/Cargo.toml). +Rust and a native build toolchain are required. ILP solving uses the bundled HiGHS backend. The published crate may lag behind the catalog on this site. To build the current checkout: @@ -22,10 +22,10 @@ cargo install --path problemreductions-cli ```bash pred create MIS --graph 0-1,1-2,2-3,3-4,4-0 -o cycle.json pred solve cycle.json -pred evaluate cycle.json --config 1,0,1,0,0 +pred evaluate cycle.json --config '[true,false,true,false,false]' ``` -`MIS` is Maximum Independent Set: select as many pairwise non-adjacent vertices as possible. The graph is a cycle on five vertices. `solve` discovers a route to ILP, solves the target, and maps the solution back, reporting `Max(2)` with a configuration such as `[1, 0, 1, 0, 0]`. `evaluate` scores a configuration of your own against the same instance. Several optimal configurations exist, so the solver's choice may differ from yours. +`MIS` is Maximum Independent Set: select as many pairwise non-adjacent vertices as possible. The graph is a cycle on five vertices. `solve` executes the registered ILP pipeline and maps the solution back, reporting `Max(2)` with a configuration such as `[true, false, true, false, false]`. `evaluate` scores a configuration of your own against the same instance. Several optimal configurations exist, so the solver's choice may differ from yours. ## Terminal session @@ -36,18 +36,13 @@ A recording of the real CLI: discover a route, transform the instance, solve, an [Open the player](static/cli-demo.html) · [Download the cast](static/cli-demo.cast) ```bash -pred path MIS ILP +pred path MIS ILP --json -o paths.json +python3 -c 'import json; print(json.dumps(json.load(open("paths.json"))["paths"][0]))' > path.json pred create MIS --graph 0-1,1-2,2-3,3-4,4-0 -o cycle.json -pred reduce cycle.json --to ILP -o reduced.json +pred reduce cycle.json --via path.json -o reduced.json pred solve reduced.json -pred evaluate cycle.json --config 1,0,1,0,0 +pred evaluate cycle.json --config '[true,false,true,false,false]' pred solve cycle.json ``` -The route passes through Maximum Set Packing and a weight cast before reaching binary ILP. `reduced.json` keeps the source instance and the path, so solving the bundle recovers a source solution. The final command solves the original file directly and discovers the same route on its own. - -| Check | Recorded result | -|---|---| -| Solve the reduction bundle | `Max(2)`, configuration `[1, 0, 1, 0, 0]` | -| Evaluate that configuration on the source | `Max(2)` | -| Solve the original instance directly | `Max(2)`, solver `ilp (via ILP)` | +`path.json` contains one explicitly selected route from the returned path set. `reduced.json` keeps the source instance and that route, so solving the bundle recovers a source solution. The final command solves the original file through its registered ILP pipeline. Both solves and the independent evaluation return `Max(2)`; optimal solutions may differ. diff --git a/docs/src/design.md b/docs/src/design.md index 32bdccb3c..54ed4f47f 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -1,8 +1,11 @@ # Design -The Rust library holds problem definitions, executable reductions, and their registry metadata. The CLI exposes that core to tools and agents. +This guide covers the library internals for contributors. -Each layer depends only on the layers above it. +See [Numeric types and arithmetic](#numeric-types-and-arithmetic) before +choosing numeric fields or implementing arithmetic in a model or reduction. + +## Module Architecture | Location | Responsibility | Depends on | |---|---|---| @@ -16,40 +19,137 @@ Each layer depends only on the layers above it. | `src/unit_tests/` | Tests mirroring the source tree | Everything | | `problemreductions-cli/` | The `pred` CLI | The library | -## Problem contract +## Problem Model -Every problem implements `Problem`. `evaluate()` returns the associated `Value` for one configuration; solvers fold those values across the configuration space defined by `dims()`. +Every problem implements `Problem`. The associated `Value` type is the per-configuration aggregate returned by `evaluate()`. Solvers fold these values across the configuration space, and witness-capable aggregates can also recover representative configurations. ```rust,ignore -trait Problem { +trait Problem: Clone { const NAME: &'static str; // e.g., "MaximumIndependentSet" - type Value: Clone; // e.g., Max, Or, Sum - fn dims(&self) -> Vec; // configuration space per variable - fn evaluate(&self, config: &[usize]) -> Self::Value; - fn variant() -> Vec<(&'static str, &'static str)>; // e.g., [("graph", "SimpleGraph"), ("weight", "i32")] - fn num_variables(&self) -> usize; // default: dims().len() + type Solution; // e.g., Vec, permutation, tuple + type Value: Clone; // e.g., Max, Or, Sum + fn parameter_names() -> &'static [&'static str]; + fn parameters(&self) -> ProblemParameters; + fn evaluate(&self, solution: &Self::Solution) -> Result; + fn variant() -> Vec<(&'static str, &'static str)>; // e.g., [("graph", "SimpleGraph"), ("weight", "i64")] fn problem_type() -> ProblemType; // default: registry lookup by NAME } ``` -A four-vertex independent set problem has `dims() = [2, 2, 2, 2]`; `evaluate(&[1, 0, 1, 0])` returns `Max(Some(2))` if vertices 0 and 2 are non-adjacent and `Max(None)` otherwise. Witness-capable objective problems use `Max`, `Min`, or `Extremum`; feasibility problems use `Or`; aggregate-only problems such as counting use `Sum` or `And` and solve to a value without a representative configuration. Each problem also provides inherent getters such as `num_vertices()` that reduction overhead expressions reference. - -## Variants - -One problem name can have several **variants**: weights on vertices, or a restricted topology such as a king's subgraph. Variants form a subtype hierarchy, and the reduction from a more specific variant to a less specific one is a **variant cast**, an identity mapping that preserves indices. +- **`Problem`** — the base trait. Every problem declares a mathematical `Solution` type, evaluates that type directly, and reports its canonical instance parameters. For example, a 4-vertex MIS uses `Vec`; `evaluate(&[true, false, true, false])` returns `Ok(Max(Some(2)))` if vertices 0 and 2 form an independent set, or `Ok(Max(None))` if they share an edge. Inherent getters such as `num_vertices()` and `num_edges()` supply the named parameters used by reduction expressions. +- **`BruteForceProblem`** — the reference-solver capability for registered variants with a finite Cartesian coordinate space. Its `dimensions()` method and the Cartesian iterator belong to the brute-force solver, not to the mathematical `Problem` contract. +- **Objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. +- **Feasibility problems** — typically use `Or`. +- **Solve contract** — a successful solve always returns the problem's `Solution`; a global count or statistic without a representative solution is not a `Problem` solve. +- **Common aggregate wrappers** — `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`, `ExtremumSense`. + +## Construction inputs + +`VariantEntry::inputs()` describes the values a concrete constructor accepts. +Models with a separate construction specification supply `CreateSpec::inputs()`; +direct constructors use their declared fields. CLI creation, MCP creation, and +`pred show` use this contract. Model-level +catalog fields describe the model family; they are not a concrete variant's input +schema. `show` exposes concrete `inputs` in JSON and labels them **Inputs** in text. + +Unit-valued data are implicit in `One` variants. For example, `MVC/One` accepts a +graph, while `MVC/i64` also accepts vertex weights. Constructors derive unit-vector +lengths from the graph, set family, or task deadlines. Internal `Vec` storage +and persisted instance JSON remain independent of construction inputs. Supplying +an undeclared weight or length input is an error, even when every value is one. + +`Decision

` composes the registered inputs of `P` with an objective `bound` and +calls `P`'s registered constructor before wrapping the result. It does not repeat +the inner input schema or deserialize construction inputs as persisted model JSON. + +## Numeric types and arithmetic + +Numeric formats are selected by semantic role: + +- `usize` represents in-memory indices, collection lengths, and brute-force + dimensions; +- `u64` represents public problem parameters and the input/output values + of reduction parameter expressions; +- `i64` represents signed mathematical integers; +- `bool` represents Boolean variables; and +- finite `f64` represents real or rational values when an approximate + representation is part of the model contract. + +`usize` is not a portable serialized parameter format, and `u64` is not an index or +general-purpose replacement for a model's mathematical integer domain. + +Another numeric format requires sufficient justification from the mathematical +problem or target schema. Required exceptions include `BigUint` in `Factoring`, +`SubsetSum`, `SubsetProduct`, `QuadraticCongruences`, and +`QuadraticDiophantineEquations`, where arbitrary precision is part of the +problem, and `One` in unweighted variants, where the type represents the +unit-weight domain. Implementation convenience is not sufficient justification. +There is no `i32` model or I/O numeric format. + +This contract applies only at model, result, reduction-target, and external I/O +boundaries; implementation-local values are outside its scope. For example, +SpinGlass couplings and its objective result use `i64`, while the temporary +`{−1, +1}` spin values used inside `evaluate()` need not. A reduction's +temporary calculations are also outside the contract, but numeric fields +written into its target model must follow the target model's numeric format. + +Weight variants are `One`, `i64`, and `f64`, with `One ⊂ i64 ⊂ f64`. +`i64 → f64` is a fallible reduction using a checked conversion in +`±(2^53-1)`, not `as f64`. + +### Arithmetic + +- Keep arithmetic in the declared type. Exact values use checked `i64` + operations; approximate values use finite `f64` operations. +- Constructors and reductions reject an arithmetic step that would overflow + `i64` when producing a stored field. They do not cap every magnitude at + `2^53-1`. `evaluate()` never widens, wraps, saturates, or silently + approximates. +- Do not promote an `i64` calculation to `i128`, `BigInt`, or `BigUint` to + accept a larger instance. + +### Boundaries + +- Use `From` only for value-preserving conversions and `TryFrom` when range, + sign, or domain can change. Do not use `as` for model-derived values. +- Converting a registered parameter getter from `usize` to `u64` is an internal + invariant of `Problem::parameters()`, not a recoverable construction error. A valid + instance's registered parameters must already fit `u64`; the + implementation checks this conversion to prevent silent truncation. +- Symbolic parameter evaluation may use arbitrary-precision integers for local + intermediate arithmetic, but a materialized `ProblemParameters` must fit `u64`. +- An `i64` to `f64` conversion is explicit and fallible: it succeeds only + for `|value| ≤ 2^53-1`. Use one shared helper at weight casts, solver + adapters, and other exact-to-float hubs. +- A lattice-to-`UnitDiskGraph` reduction converts coordinates fallibly and + rejects a stored `f64` geometry that would change source adjacency. +- Rust constructors keep `i64` fields as `i64`. CLI and MCP JSON encoding + of an `i64` with `|value| > 2^53-1` errors; there is no string encoding + and no clamping. + +## Variant System + +A single problem name like `MaximumIndependentSet` can have multiple +**variants**. Each variant is identified by dimension-value pairs such as +`{graph: "SimpleGraph", weight: "i64"}`. Concrete variants are registered +nodes in the reduction graph, and explicit reduction rules connect them.

-![Variant Hierarchy](static/variant-hierarchy.svg) +![Variant Dimensions](static/variant-hierarchy.svg)
-![Variant Hierarchy](static/variant-hierarchy-dark.svg) +![Variant Dimensions](static/variant-hierarchy-dark.svg)
-Variant parameters fall into three categories: graph type (`SimpleGraph` at the root, then `PlanarGraph`, `BipartiteGraph`, `UnitDiskGraph`, `KingsSubgraph`, `TriangularSubgraph`), weight type (`One`, `i32`, `f64`), and K value (`K3` for 3-SAT, `KN` for arbitrary K). +Variant types fall into three categories: + +- **Graph type** — `SimpleGraph`, `PlanarGraph`, `BipartiteGraph`, `UnitDiskGraph`, `KingsSubgraph`, `TriangularSubgraph`. +- **Weight type** — `One` (unweighted), `i64`, `f64`. +- **K value** — e.g., `K3` for 3-SAT, `KN` for arbitrary K.
@@ -62,58 +162,279 @@ Variant parameters fall into three categories: graph type (`SimpleGraph` at the
-Each parameter type implements `VariantParam`, declaring its category, value, and optional parent; types with a parent also implement `CastToParent` for the runtime conversion. `Problem::variant()` is composed from the type parameters with `variant_params![G, W]`. The macros `impl_variant_param!`, `impl_variant_reduction!`, and `declare_variants!` register parameter types, explicit variant casts, and concrete variants with their load, serialize, and solve metadata. Their current contract is documented in the [repository instructions](https://github.com/CodingThrust/problem-reductions/blob/main/.claude/CLAUDE.md) and visible in any model file. +
+Implementation details: VariantParam trait and macros -## Reductions +### VariantParam trait -A reduction connects exact source and target variants. Its capability determines how a result is recovered: +Each reusable variant parameter type implements `VariantParam`, which declares +its category and value: -| Capability | Contract | Example | -|---|---|---| -| Witness | `ReduceTo` and `ReductionResult::extract_solution` | Solve a target, recover a source configuration | -| Aggregate | `ReduceToAggregate` and `AggregateReductionResult::extract_value` | Solve a target value, recover a source value | -| Turing | Multiple target queries | Optimize by querying a decision problem at several bounds | +```rust,ignore +pub trait VariantParam: 'static { + const CATEGORY: &'static str; // e.g., "graph", "weight", "k" + const VALUE: &'static str; // e.g., "SimpleGraph", "i64" +} +``` + +### Registration with `impl_variant_param!` + +The `impl_variant_param!` macro implements `VariantParam` and optionally +`KValue` for a type: + +```rust,ignore +impl_variant_param!(SimpleGraph, "graph"); + +impl_variant_param!(KN, "k", k: None); + +impl_variant_param!(K3, "k", k: Some(3)); +``` + +### Explicit variant reductions + +`impl_variant_reduction!` registers a concrete same-model conversion with an +exact parameter transform and identity witness extraction: + +```rust,ignore +impl_variant_reduction!( + MaximumIndependentSet, + => , + fields: [num_vertices, num_edges], + |src| MaximumIndependentSet::new( + SimpleGraph::new( + src.num_vertices(), + Graph::edges(src.graph()), + ), + src.weights().to_vec()) +); +``` + +### Composing `Problem::variant()` -Graph search defaults to witness mode; `ReductionMode::Aggregate` and `ReductionMode::Turing` select the others. A witness reduction is registered with the `#[reduction]` attribute, whose `overhead` block is required: +The `variant_params!` macro composes the `Problem::variant()` body from type parameter names: ```rust,ignore -#[reduction(overhead = { +// MaximumIndependentSet +fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G, W] + // e.g., MaximumIndependentSet + // -> vec![("graph", "UnitDiskGraph"), ("weight", "One")] +} +``` + +### Querying one variant family + +`ReductionGraph::variants_for(name)` returns every registered concrete variant +of a problem. `ReductionGraph::outgoing_reductions(name)` returns their outgoing +edges. Filtering those edges by `target_name == name` produces the directed +relations within that variant family. + +
+ +## Reduction Rules + +A reduction requires two pieces: a **result struct** and a **`ReduceTo` impl**. + +The result struct holds the target problem and the logic to map solutions back: + +```rust,ignore +#[derive(Debug, Clone)] +pub struct ReductionISToVC { + target: MinimumVertexCover, +} + +impl ReductionResult for ReductionISToVC { + type Source = MaximumIndependentSet; + type Target = MinimumVertexCover; + + fn target_problem(&self) -> &Self::Target { &self.target } + fn extract_solution( + &self, + target_sol: &Vec, + ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_sol)?; + Ok(target_sol.iter().map(|&x| !x).collect()) + } +} +``` + +### Solution extraction contract + +`ReductionResult::extract_solution` accepts one complete target configuration +and returns the source configuration defined by the reduction. Extraction is a +fallible boundary, not a recovery mechanism: + +1. In every direct extractor, call `validate_target_solution()` once before + indexing or decoding. Composed extractors delegate this check. +2. Validate any structure required by the inverse mapping, such as exactly-one + blocks, permutations, paths, flows, or schedules. +3. Apply the reduction's mathematical inverse once and return a source + configuration with the required length and domains. +4. Return `ExtractionError` when a precondition is not satisfied. + +Do not truncate or pad input, substitute zero for missing data, select the +first of several invalid candidates, retry with another mapping, or panic on +caller-provided configuration data. Empty and singleton instances should flow +through the same mathematical mapping unless the reduction itself has a +genuine mathematical case distinction. + +Zero and sentinel values remain valid when the source model explicitly gives +them meaning. For example, `MaximumCommonEdgeSubgraph` includes an "unmapped" +sentinel in its source dimensions. Missing target data must never be +interpreted as that sentinel. + +Each conditional in an extractor should therefore either reject a named +invariant violation or implement a case in the reduction's mathematics. A +normal extractor has one validation phase followed by one decoding phase; it +does not accumulate compatibility or fallback branches. + +The `#[reduction]` attribute on the `ReduceTo` impl registers the reduction in the global registry (via `inventory`): + +```rust,ignore +#[reduction(transform = exact { num_vertices = "num_vertices", num_edges = "num_edges", })] -impl ReduceTo> - for MaximumIndependentSet +impl ReduceTo> + for MaximumIndependentSet { - // Provide Result and reduce_to(); the result owns the target - // and maps a vertex-cover witness to its independent-set complement. + type Result = ReductionISToVC; + fn reduce_to(&self) -> Result { /* ... */ } } ``` -See a [complete implementation](https://github.com/CodingThrust/problem-reductions/blob/main/src/rules/maximumindependentset_minimumvertexcover.rs) for the result type. Aggregate and Turing edges use manual `ReductionEntry` registration. Keep one primitive registration per exact endpoint pair. Correctness needs a proof that construction and extraction preserve the required result; a closed-loop test on a small example is evidence, not proof. +## Reduction Graph + +`ReductionGraph::new()` iterates all registered `ReductionEntry` items (via `inventory`) and builds a variant-level directed graph: + +- **Nodes** are unique `(problem_name, variant)` pairs — e.g., `("MaximumIndependentSet", {graph: "KingsSubgraph", weight: "i64"})`. +- **Edges** come from explicit `#[reduction]` registrations, including + cross-problem and same-problem variant reductions. + +Exported files: + +- [reduction_graph.json](reductions/reduction_graph.json) — all problem variants and reduction edges +- [problem_schemas.json](reductions/problem_schemas.json) — field definitions for each problem type -## Path costs and overhead +These JSON assets are generated during `make doc`, `make mdbook`, and `make paper`; they are build artifacts, not committed source files. +Generate them manually with `cargo run --example export_graph` and `cargo run --example export_schemas` when you need the raw exports locally. -`ReductionGraph` searches a directed graph of exact `(name, variant)` pairs. Registered reductions carry capabilities; natural variant connections follow the graph and weight subtype relations. +### Path finding -| Cost | Purpose | -|---|---| -| `MinimizeSteps` | Fewest reduction steps | -| `Minimize("field")` | Cost based on an output size field | -| `CustomCost(closure)` | User-defined edge cost from overhead and current size | +All path-finding operates on **exact variant nodes**. Use `ReductionGraph::variant_to_map(&T::variant())` to convert a `Problem::variant()` into the required `BTreeMap`. -`find_cheapest_path` takes source and target variant maps, an input `ProblemSize`, and a cost; `find_all_paths` enumerates simple paths with a bound. Overhead expressions refer to getters on the source type and are validated at compile time. They describe scaling bounds, not exact target counts: for a concrete instance inspect the constructed target, and for a chain use `path_overheads` and `compose_path_overhead` for an end-to-end bound. A shorter route can produce a harder target, so compare target sizes and solver measurements as well as hop counts. +| Method | Algorithm | Use case | +|--------|-----------|----------| +| `find_all_paths(src, src_var, dst, dst_var)` | All simple paths | Enumerate every route | +| `compose_path_parameter_transform(path)` | Symbolic composition | Compose each rule's exact or upper-bound parameter relation while preserving its promise | + +A rule has one relation for all of its formulas: either an exact equality or an upper +bound. Composition keeps exact formulas exact only when every step is exact; every other +combination is an upper bound. Concrete-instance measurement remains a separate execution +API. + +**Example:** Finding a path from `MIS{KingsSubgraph, i64}` to `VC{SimpleGraph, i64}`: + +``` +MIS{KingsSubgraph,i64} -> MIS{UnitDiskGraph,i64} -> MIS{SimpleGraph,i64} -> VC{SimpleGraph,i64} + variant reduction variant reduction reduction +``` -## JSON serialization +### Executable paths + +Execute an explicitly selected path with `ReductionGraph::reduce_along_path`: + +```rust,ignore +let reduction = graph.reduce_along_path(rpath, &factoring_instance)?.unwrap(); +let target: &SpinGlass = reduction.target_problem(); +let source_solution = reduction.extract_solution(&target_solution)?; +``` + +The returned `ReductionChain` stores each intermediate reduction and extracts the source solution by applying the inverse mappings in reverse order. Construction returns `ReductionError`; extraction returns `ExtractionError`. + +
+Parameter contracts + +Each reduction declares one relation for all represented target-parameter fields and may mark +other fields unavailable with a reason. The `#[reduction]` macro parses every formula into +the canonical `Expr` DAG at compile time: + +```rust,ignore +#[reduction( +transform = upper_bound { + num_vars = "num_vertices + num_edges", + num_clauses = "3 * num_edges", +}, +unavailable = { + encoding_bits = "coefficient magnitudes are not tracked", +}, +})] +impl ReduceTo for Source { ... } +``` + +`ParameterTransform` uses exact rational and arbitrary-precision integer arithmetic. Exact +relations must evaluate to non-negative integers, while upper-bound results round rational +values upward. Missing fields, negative or non-integral exact results, division by zero, +and explicit conversion outside `u64` are errors. + +Transforms can be evaluated with explicit source parameters: + +``` +Input: ProblemParameters { num_vertices: 10, num_edges: 15 } +Output: ProblemParameters { num_vars: 25 } +``` + +For multi-step paths, `compose_path_parameter_transform` substitutes each step into the next. +When only upper bounds are known for the intermediate fields, a downstream polynomial is +first fully expanded and like monomials are combined; terms with non-positive coefficients +are then removed before substitution. For example, `m <= n^2` followed by `k = 10 - m` +produces the sound bound `k <= 10`, while +`e' = v(v - 1)/2 - e` produces `e' <= v^2/2`. A non-polynomial downstream formula cannot +propagate symbolic upper bounds and reports an error. Projection to `Growth` is a separate descriptive terminal operation used for +Big-O display; it does not rank or filter paths. + +
+ +## Solvers + +The reference solver exposes a direct typed operation: + +```rust,ignore +BruteForce::solve(&problem) -> Result, SolveError> +``` + +`Some(solution)` is a successful exact solve, `None` means exhaustive search +proved infeasibility, and `Err` reports an operational failure. + +| Solver | Description | +|--------|-------------| +| **BruteForce** | Enumerates a registered finite search space and returns an optimal or satisfying solution. Used for testing and verification. | +| **ILPSolver** | Executes a problem's registered ILP pipeline. Each pipeline terminates at `ILP` or `ILP`, which is solved by HiGHS via `good_lp`. | + +ILP results are optimal or infeasible according to HiGHS numerical tolerances; +zero MIP gaps do not imply mathematical exactness. Integer extraction rounds +variable assignments, validates the original constraints, and recomputes the +source objective with checked integer arithmetic. Floating-point objective +comparisons in numerical regression tests use an explicit acceptance policy +in source units (absolute and relative tolerances of `1e-7` for the QUBO solver +regression), separate from the `1e-6` variable-rounding tolerance. This test +policy is not a universal bound on backend objective error. + +When an ILP target witness misses a source decision threshold, the solver +returns `ILPSolveError::UnresolvedDecision`, not infeasibility: the witness +alone cannot prove that no qualifying source solution exists. + +## JSON Serialization + +All problem types support JSON serialization via serde: ```rust,ignore use problemreductions::io::{to_json, from_json}; let json: String = to_json(&problem)?; -let restored: MaximumIndependentSet = from_json(&json)?; +let restored: MaximumIndependentSet = from_json(&json)?; ``` -These helpers serialize typed problem data. The CLI additionally wraps data with `type` and `variant` for dynamic loading; keep that wrapper when passing files between commands. - ## Contributing -Propose a model or reduction through the `propose` [skill](skills.md) or the [issue templates](https://github.com/CodingThrust/problem-reductions/issues/new/choose). A useful rule includes exact endpoint variants, a construction, solution or value extraction, a correctness argument, and overhead metadata. The `add-model` and `add-rule` skills list the required code, tests, examples, and paper changes; `.claude/CLAUDE.md` holds the conventions. +See [Call for Contributions](./open-problems.md) for the recommended issue-based workflow (no coding required). diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 90f238f6d..872b4e900 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -4,7 +4,7 @@ cargo add problemreductions ``` -HiGHS is enabled by default through the `ilp-highs` feature. Add `--no-default-features` to use the library without an ILP backend. +The library includes the HiGHS ILP backend. ## Solve a small instance @@ -12,16 +12,16 @@ HiGHS is enabled by default through the `ilp-highs` feature. Add `--no-default-f use problemreductions::prelude::*; fn main() { - let problem = MaximumSetPacking::::new(vec![ + let problem = MaximumSetPacking::::new(vec![ vec![0, 1], vec![1, 2], vec![2, 3], vec![4, 5], ]); let solver = BruteForce::new(); - let solution = solver.find_witness(&problem).unwrap(); - println!("{:?}: {}", solution, problem.evaluate(&solution)); + let solution = solver.solve(&problem).unwrap().unwrap(); + println!("{:?}: {}", solution, problem.evaluate(&solution).unwrap()); } ``` -The optimal packing selects sets 0, 2, and 3: the witness is `[1, 0, 1, 1]` and evaluates to `Max(3)`. `Problem::evaluate` scores a configuration; `BruteForce` enumerates the configuration space, so keep exhaustive examples small. +The optimal packing selects sets 0, 2, and 3: the witness is `[true, false, true, true]` and evaluates to `Max(3)`. `Problem::evaluate` scores a configuration; `BruteForce` enumerates the configuration space, so keep exhaustive examples small. ## Apply a reduction @@ -33,39 +33,35 @@ use problemreductions::models::algebraic::ILP; use problemreductions::solvers::ILPSolver; fn main() { - let problem = MaximumSetPacking::::new(vec![ + let problem = MaximumSetPacking::::new(vec![ vec![0, 1], vec![1, 2], vec![2, 3], vec![4, 5], ]); - let reduction = ReduceTo::>::reduce_to(&problem); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target = reduction.target_problem(); assert_eq!(target.num_vars(), 4); assert_eq!(target.num_constraints(), 2); let target_solution = ILPSolver::new().solve(target).unwrap(); - let solution = reduction.extract_solution(&target_solution); - assert_eq!(solution, vec![1, 0, 1, 1]); - println!("{}", problem.evaluate(&solution)); // Max(3) + let solution = reduction.extract_solution(&target_solution).unwrap(); + assert_eq!(solution, vec![true, false, true, true]); + println!("{}", problem.evaluate(&solution).unwrap()); // Max(3) } ``` -The target has one binary variable per set and one constraint per overlapping pair. `extract_solution` maps a target witness back to the source configuration space. For any type implementing `ReduceTo>`, `ILPSolver::new().solve_reduced(&problem)` combines these steps and returns an optional source configuration. +The target has one binary variable per set and a constraint for each element shared by multiple sets. `extract_solution` maps a target solution back to the source solution type. `ILPSolver::new().solve(&problem)` executes the exact variant’s registered ILP pipeline and returns its source solution. ## Discover and run a path Search uses exact variants. This discovers a route from `Factoring` to `SpinGlass` and executes it: ```rust,ignore -use problemreductions::prelude::*; -use problemreductions::rules::{MinimizeSteps, ReductionGraph}; -use problemreductions::topology::SimpleGraph; -use problemreductions::types::ProblemSize; +{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:imports}} {{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:step1}} +{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:step2}} -let factoring = Factoring::new(2, 2, 6); -let reduction = graph.reduce_along_path(&rpath, &factoring).unwrap(); +let reduction = graph.reduce_along_path(rpath, &factoring).unwrap().unwrap(); let target: &SpinGlass = reduction.target_problem(); -// Solve `target` with a compatible solver, then recover source bits: -// let source_config = reduction.extract_solution(&target_config); +// Solve `target`, then call reduction.extract_solution(&target_solution). ``` `extract_solution` walks the intermediate mappings in reverse. The full [example](https://github.com/CodingThrust/problem-reductions/blob/main/examples/chained_reduction_factoring_to_spinglass.rs) also solves factoring through a direct ILP reduction and checks that the recovered factors multiply to 6: @@ -78,10 +74,7 @@ cargo run --example chained_reduction_factoring_to_spinglass | API | Result | Scope | |---|---|---| -| `Solver::solve` with `BruteForce` | Aggregate value | Any problem whose value implements `Aggregate` | -| `BruteForce::find_witness` | Optional configuration | Values supporting representative witnesses | -| `ILPSolver::solve` | Optional ILP configuration | ILP instances; requires an ILP backend | -| `ILPSolver::solve_reduced` | Optional source configuration | Problems implementing `ReduceTo>` | -| `CustomizedSolver` | Model-specific exact solving | Selected models with specialized implementations | +| `BruteForce::solve` | `Result, SolveError>` | Registered finite search spaces; `None` proves infeasibility | +| `ILPSolver::solve` | `Result` | Exact variants with registered ILP pipelines | -Counting and other aggregate-only problems produce a value, not a representative configuration. For witness results, evaluate the recovered configuration against the source; for aggregate results, compare values through the reduction's extraction contract. Method signatures are in the [solver API](api/problemreductions/solvers/index.html). +Every successful solve returns the problem's `Solution`. Evaluate it against the source with `Problem::evaluate`, which returns `Result`. Path discovery enumerates routes; it does not rank them or register a solver capability. See the [solver API](api/problemreductions/solvers/index.html). diff --git a/docs/src/static/cli-demo.cast b/docs/src/static/cli-demo.cast index 0a10f372d..92ebaaf3b 100644 --- a/docs/src/static/cli-demo.cast +++ b/docs/src/static/cli-demo.cast @@ -20,232 +20,389 @@ [0.899218, "o", "I"] [0.943438, "o", "L"] [0.977069, "o", "P"] -[1.277069, "o", "\r\n"] -[1.427069, "o", "Path (3 steps): \u001b[1mMaximumIndependentSet/SimpleGraph/One\u001b[0m \u001b[32m\u2192\u001b[39m \u001b[1mMaximumSetPacking/One\u001b[0m \u001b[32m\u2192\u001b[39m \u001b[1mILP/bool\u001b[0m\r\n\r\n \u001b[36mStep 1\u001b[39m: \u001b[1mMaximumIndependentSet/SimpleGraph/One\u001b[0m \u001b[32m\u2192\u001b[39m \u001b[1mMaximumSetPacking/One\u001b[0m\r\n num_sets = O(num_vertices)\r\n universe_size = O(num_edges)\r\n\r\n \u001b[36mStep 2\u001b[39m: \u001b[1mMaximumSetPacking/One\u001b[0m \u001b[32m\u2192\u001b[39m \u001b[1mMaximumSetPacking/i32\u001b[0m\r\n num_sets = O(num_sets)\r\n universe_size = O(universe_size)\r\n\r\n \u001b[36mStep 3\u001b[39m: \u001b[1mMaximumSetPacking/i32\u001b[0m \u001b[32m\u2192\u001b[39m \u001b[1mILP/bool\u001b[0m\r\n num_vars = O(num_sets)\r\n num_constraints = O(universe_size)\r\n\r\n \u001b[36mOverall\u001b[39m:\r\n num_vars = O(num_vertices)\r\n num_constraints = O(num_edges)\r\n"] -[4.127069, "o", "\r\n"] -[4.127069, "o", "\u001b[90m# Create a five-vertex cycle\u001b[0m\r\n"] -[4.577069, "o", "$ "] -[4.605546, "o", "."] -[4.650582, "o", "/"] -[4.669056, "o", "p"] -[4.710726, "o", "r"] -[4.736143, "o", "e"] -[4.757408, "o", "d"] -[4.777916, "o", " "] -[4.803873, "o", "c"] -[4.844334, "o", "r"] -[4.86664, "o", "e"] -[4.9004, "o", "a"] -[4.935798, "o", "t"] -[4.96358, "o", "e"] -[4.996373, "o", " "] -[5.01531, "o", "M"] -[5.034156, "o", "I"] -[5.057183, "o", "S"] -[5.093766, "o", " "] -[5.123126, "o", "-"] -[5.149244, "o", "-"] -[5.183117, "o", "g"] -[5.213208, "o", "r"] -[5.238916, "o", "a"] -[5.278755, "o", "p"] +[1.005546, "o", " "] +[1.050582, "o", "-"] +[1.069056, "o", "-"] +[1.110726, "o", "j"] +[1.136143, "o", "s"] +[1.157408, "o", "o"] +[1.177916, "o", "n"] +[1.203873, "o", " "] +[1.244334, "o", "-"] +[1.26664, "o", "o"] +[1.3004, "o", " "] +[1.335798, "o", "p"] +[1.36358, "o", "a"] +[1.396373, "o", "t"] +[1.41531, "o", "h"] +[1.434156, "o", "s"] +[1.457183, "o", "."] +[1.493766, "o", "j"] +[1.523126, "o", "s"] +[1.549244, "o", "o"] +[1.583117, "o", "n"] +[1.883117, "o", "\r\n"] +[2.033117, "o", "Wrote paths.json\r\n"] +[4.733117, "o", "\r\n"] +[4.733117, "o", "\u001b[90m# Select the first returned route\u001b[0m\r\n"] +[5.183117, "o", "$ "] +[5.213208, "o", "p"] +[5.238916, "o", "y"] +[5.278755, "o", "t"] [5.31587, "o", "h"] -[5.339987, "o", " "] -[5.373542, "o", "0"] -[5.40569, "o", "-"] -[5.447837, "o", "1"] -[5.485821, "o", ","] -[5.511191, "o", "1"] -[5.556338, "o", "-"] -[5.576855, "o", "2"] -[5.605944, "o", ","] -[5.644719, "o", "2"] -[5.666205, "o", "-"] -[5.697318, "o", "3"] -[5.715581, "o", ","] -[5.751816, "o", "3"] -[5.790803, "o", "-"] -[5.824318, "o", "4"] -[5.866475, "o", ","] -[5.892582, "o", "4"] -[5.92959, "o", "-"] -[5.963715, "o", "0"] +[5.339987, "o", "o"] +[5.373542, "o", "n"] +[5.40569, "o", "3"] +[5.447837, "o", " "] +[5.485821, "o", "-"] +[5.511191, "o", "c"] +[5.556338, "o", " "] +[5.576855, "o", "'"] +[5.605944, "o", "i"] +[5.644719, "o", "m"] +[5.666205, "o", "p"] +[5.697318, "o", "o"] +[5.715581, "o", "r"] +[5.751816, "o", "t"] +[5.790803, "o", " "] +[5.824318, "o", "j"] +[5.866475, "o", "s"] +[5.892582, "o", "o"] +[5.92959, "o", "n"] +[5.963715, "o", ";"] [5.997427, "o", " "] -[6.027604, "o", "-"] -[6.068746, "o", "o"] -[6.11288, "o", " "] -[6.143568, "o", "c"] -[6.179687, "o", "y"] -[6.198563, "o", "c"] -[6.235748, "o", "l"] -[6.271381, "o", "e"] -[6.316898, "o", "."] -[6.357524, "o", "j"] -[6.382798, "o", "s"] -[6.410964, "o", "o"] -[6.447211, "o", "n"] -[6.747211, "o", "\r\n"] -[6.897211, "o", "Wrote cycle.json\r\n"] -[9.597211, "o", "\r\n"] -[9.597211, "o", "\u001b[90m# Transform the instance and preserve the way back\u001b[0m\r\n"] -[10.047211, "o", "$ "] -[10.064999, "o", "."] -[10.095333, "o", "/"] -[10.117277, "o", "p"] -[10.137765, "o", "r"] -[10.156593, "o", "e"] -[10.195685, "o", "d"] -[10.216523, "o", " "] -[10.240741, "o", "r"] -[10.269054, "o", "e"] -[10.311094, "o", "d"] -[10.33054, "o", "u"] -[10.360516, "o", "c"] -[10.393357, "o", "e"] -[10.43574, "o", " "] -[10.476291, "o", "c"] -[10.518119, "o", "y"] -[10.543217, "o", "c"] -[10.572225, "o", "l"] -[10.599618, "o", "e"] -[10.642024, "o", "."] -[10.686531, "o", "j"] -[10.707985, "o", "s"] -[10.730163, "o", "o"] -[10.753933, "o", "n"] -[10.777743, "o", " "] -[10.808742, "o", "-"] -[10.842717, "o", "-"] -[10.867367, "o", "t"] -[10.884627, "o", "o"] -[10.913739, "o", " "] -[10.941432, "o", "I"] -[10.974756, "o", "L"] -[11.019131, "o", "P"] -[11.056002, "o", " "] -[11.087873, "o", "-"] -[11.122661, "o", "o"] -[11.159124, "o", " "] -[11.17781, "o", "r"] -[11.220653, "o", "e"] -[11.260081, "o", "d"] -[11.30221, "o", "u"] -[11.342149, "o", "c"] -[11.370503, "o", "e"] -[11.399045, "o", "d"] -[11.419146, "o", "."] -[11.454412, "o", "j"] -[11.473333, "o", "s"] -[11.4924, "o", "o"] -[11.515508, "o", "n"] -[11.815508, "o", "\r\n"] -[11.965508, "o", "Wrote reduced.json\r\n"] -[14.665508, "o", "\r\n"] -[14.665508, "o", "\u001b[90m# Solve the target and recover a source solution\u001b[0m\r\n"] -[15.115508, "o", "$ "] -[15.137288, "o", "."] -[15.164147, "o", "/"] -[15.182792, "o", "p"] -[15.199941, "o", "r"] -[15.221406, "o", "e"] -[15.241448, "o", "d"] -[15.268979, "o", " "] -[15.286851, "o", "s"] -[15.328975, "o", "o"] -[15.363662, "o", "l"] -[15.385049, "o", "v"] -[15.4094, "o", "e"] -[15.436468, "o", " "] -[15.464015, "o", "r"] -[15.484668, "o", "e"] -[15.526066, "o", "d"] -[15.571583, "o", "u"] -[15.60204, "o", "c"] -[15.633007, "o", "e"] -[15.652604, "o", "d"] -[15.672666, "o", "."] -[15.699599, "o", "j"] -[15.724306, "o", "s"] -[15.76513, "o", "o"] -[15.786886, "o", "n"] -[16.086886, "o", "\r\n"] -[16.236886, "o", "Problem: MaximumIndependentSet\r\nSolver: ilp (via ILP)\r\nSolution: [1, 0, 1, 0, 0]\r\nEvaluation: Max(2)\r\n\r\nHint: use -o to save full solution details (including intermediate results) as JSON.\r\n"] -[18.936886, "o", "\r\n"] -[18.936886, "o", "\u001b[90m# Check an independent set of size two\u001b[0m\r\n"] -[19.386886, "o", "$ "] -[19.404689, "o", "."] -[19.449002, "o", "/"] -[19.481238, "o", "p"] -[19.50257, "o", "r"] -[19.535232, "o", "e"] -[19.553147, "o", "d"] -[19.585379, "o", " "] -[19.630479, "o", "e"] -[19.672288, "o", "v"] -[19.709323, "o", "a"] -[19.733926, "o", "l"] -[19.761546, "o", "u"] -[19.783461, "o", "a"] -[19.82266, "o", "t"] -[19.855019, "o", "e"] -[19.894421, "o", " "] -[19.920983, "o", "c"] -[19.944498, "o", "y"] -[19.984827, "o", "c"] -[20.030111, "o", "l"] -[20.071615, "o", "e"] -[20.111788, "o", "."] -[20.152312, "o", "j"] -[20.190594, "o", "s"] -[20.214215, "o", "o"] -[20.246148, "o", "n"] -[20.27345, "o", " "] -[20.29142, "o", "-"] -[20.309361, "o", "-"] -[20.334488, "o", "c"] -[20.359036, "o", "o"] -[20.395965, "o", "n"] -[20.440437, "o", "f"] -[20.470357, "o", "i"] -[20.514272, "o", "g"] -[20.559645, "o", " "] -[20.604073, "o", "1"] -[20.631634, "o", ","] -[20.655076, "o", "0"] -[20.6787, "o", ","] -[20.701463, "o", "1"] -[20.724445, "o", ","] -[20.759419, "o", "0"] -[20.802285, "o", ","] -[20.84344, "o", "0"] -[21.14344, "o", "\r\n"] -[21.29344, "o", "Max(2)\r\n"] -[23.99344, "o", "\r\n"] -[23.99344, "o", "\u001b[90m# Solve the original instance with the default solver\u001b[0m\r\n"] -[24.44344, "o", "$ "] -[24.474282, "o", "."] -[24.510082, "o", "/"] -[24.550071, "o", "p"] -[24.569636, "o", "r"] -[24.605653, "o", "e"] -[24.64879, "o", "d"] -[24.688284, "o", " "] -[24.726859, "o", "s"] -[24.75766, "o", "o"] -[24.779904, "o", "l"] -[24.819593, "o", "v"] -[24.846237, "o", "e"] -[24.88626, "o", " "] -[24.931165, "o", "c"] -[24.959617, "o", "y"] -[24.988228, "o", "c"] -[25.032423, "o", "l"] -[25.070274, "o", "e"] -[25.092274, "o", "."] -[25.113047, "o", "j"] -[25.134508, "o", "s"] -[25.177504, "o", "o"] -[25.21769, "o", "n"] -[25.51769, "o", "\r\n"] -[25.66769, "o", "Problem: MaximumIndependentSet\r\nSolver: ilp (via ILP)\r\nSolution: [1, 0, 1, 0, 0]\r\nEvaluation: Max(2)\r\n\r\nHint: use -o to save full solution details as JSON.\r\n"] -[28.36769, "o", "$ "] +[6.027604, "o", "j"] +[6.068746, "o", "s"] +[6.11288, "o", "o"] +[6.143568, "o", "n"] +[6.179687, "o", "."] +[6.198563, "o", "d"] +[6.235748, "o", "u"] +[6.271381, "o", "m"] +[6.316898, "o", "p"] +[6.357524, "o", "("] +[6.382798, "o", "j"] +[6.410964, "o", "s"] +[6.447211, "o", "o"] +[6.464999, "o", "n"] +[6.495333, "o", "."] +[6.517277, "o", "l"] +[6.537765, "o", "o"] +[6.556593, "o", "a"] +[6.595685, "o", "d"] +[6.616523, "o", "("] +[6.640741, "o", "o"] +[6.669054, "o", "p"] +[6.711094, "o", "e"] +[6.73054, "o", "n"] +[6.760516, "o", "("] +[6.793357, "o", "\""] +[6.83574, "o", "p"] +[6.876291, "o", "a"] +[6.918119, "o", "t"] +[6.943217, "o", "h"] +[6.972225, "o", "s"] +[6.999618, "o", "."] +[7.042024, "o", "j"] +[7.086531, "o", "s"] +[7.107985, "o", "o"] +[7.130163, "o", "n"] +[7.153933, "o", "\""] +[7.177743, "o", ")"] +[7.208742, "o", ")"] +[7.242717, "o", "["] +[7.267367, "o", "\""] +[7.284627, "o", "p"] +[7.313739, "o", "a"] +[7.341432, "o", "t"] +[7.374756, "o", "h"] +[7.419131, "o", "s"] +[7.456002, "o", "\""] +[7.487873, "o", "]"] +[7.522661, "o", "["] +[7.559124, "o", "0"] +[7.57781, "o", "]"] +[7.620653, "o", ","] +[7.660081, "o", " "] +[7.70221, "o", "o"] +[7.742149, "o", "p"] +[7.770503, "o", "e"] +[7.799045, "o", "n"] +[7.819146, "o", "("] +[7.854412, "o", "\""] +[7.873333, "o", "p"] +[7.8924, "o", "a"] +[7.915508, "o", "t"] +[7.937288, "o", "h"] +[7.964147, "o", "."] +[7.982792, "o", "j"] +[7.999941, "o", "s"] +[8.021406, "o", "o"] +[8.041448, "o", "n"] +[8.068979, "o", "\""] +[8.086851, "o", ","] +[8.128975, "o", " "] +[8.163662, "o", "\""] +[8.185049, "o", "w"] +[8.2094, "o", "\""] +[8.236468, "o", ")"] +[8.264015, "o", ")"] +[8.284668, "o", "'"] +[8.584668, "o", "\r\n"] +[8.734668, "o", "\r\n"] +[11.434668, "o", "\r\n"] +[11.434668, "o", "\u001b[90m# Create a five-vertex cycle\u001b[0m\r\n"] +[11.884668, "o", "$ "] +[11.926066, "o", "."] +[11.971583, "o", "/"] +[12.00204, "o", "p"] +[12.033007, "o", "r"] +[12.052604, "o", "e"] +[12.072666, "o", "d"] +[12.099599, "o", " "] +[12.124306, "o", "c"] +[12.16513, "o", "r"] +[12.186886, "o", "e"] +[12.204689, "o", "a"] +[12.249002, "o", "t"] +[12.281238, "o", "e"] +[12.30257, "o", " "] +[12.335232, "o", "M"] +[12.353147, "o", "I"] +[12.385379, "o", "S"] +[12.430479, "o", " "] +[12.472288, "o", "-"] +[12.509323, "o", "-"] +[12.533926, "o", "g"] +[12.561546, "o", "r"] +[12.583461, "o", "a"] +[12.62266, "o", "p"] +[12.655019, "o", "h"] +[12.694421, "o", " "] +[12.720983, "o", "0"] +[12.744498, "o", "-"] +[12.784827, "o", "1"] +[12.830111, "o", ","] +[12.871615, "o", "1"] +[12.911788, "o", "-"] +[12.952312, "o", "2"] +[12.990594, "o", ","] +[13.014215, "o", "2"] +[13.046148, "o", "-"] +[13.07345, "o", "3"] +[13.09142, "o", ","] +[13.109361, "o", "3"] +[13.134488, "o", "-"] +[13.159036, "o", "4"] +[13.195965, "o", ","] +[13.240437, "o", "4"] +[13.270357, "o", "-"] +[13.314272, "o", "0"] +[13.359645, "o", " "] +[13.404073, "o", "-"] +[13.431634, "o", "o"] +[13.455076, "o", " "] +[13.4787, "o", "c"] +[13.501463, "o", "y"] +[13.524445, "o", "c"] +[13.559419, "o", "l"] +[13.602285, "o", "e"] +[13.64344, "o", "."] +[13.674282, "o", "j"] +[13.710082, "o", "s"] +[13.750071, "o", "o"] +[13.769636, "o", "n"] +[14.069636, "o", "\r\n"] +[14.219636, "o", "Wrote cycle.json\r\n"] +[16.919636, "o", "\r\n"] +[16.919636, "o", "\u001b[90m# Transform the instance and preserve the way back\u001b[0m\r\n"] +[17.369636, "o", "$ "] +[17.405653, "o", "."] +[17.44879, "o", "/"] +[17.488284, "o", "p"] +[17.526859, "o", "r"] +[17.55766, "o", "e"] +[17.579904, "o", "d"] +[17.619593, "o", " "] +[17.646237, "o", "r"] +[17.68626, "o", "e"] +[17.731165, "o", "d"] +[17.759617, "o", "u"] +[17.788228, "o", "c"] +[17.832423, "o", "e"] +[17.870274, "o", " "] +[17.892274, "o", "c"] +[17.913047, "o", "y"] +[17.934508, "o", "c"] +[17.977504, "o", "l"] +[18.01769, "o", "e"] +[18.039009, "o", "."] +[18.079766, "o", "j"] +[18.124918, "o", "s"] +[18.16084, "o", "o"] +[18.187994, "o", "n"] +[18.220813, "o", " "] +[18.241699, "o", "-"] +[18.259248, "o", "-"] +[18.304131, "o", "v"] +[18.339836, "o", "i"] +[18.372024, "o", "a"] +[18.415842, "o", " "] +[18.445379, "o", "p"] +[18.487429, "o", "a"] +[18.528176, "o", "t"] +[18.551349, "o", "h"] +[18.575687, "o", "."] +[18.6012, "o", "j"] +[18.625216, "o", "s"] +[18.659114, "o", "o"] +[18.683667, "o", "n"] +[18.712782, "o", " "] +[18.73367, "o", "-"] +[18.776813, "o", "o"] +[18.804064, "o", " "] +[18.834297, "o", "r"] +[18.868107, "o", "e"] +[18.911087, "o", "d"] +[18.940248, "o", "u"] +[18.983611, "o", "c"] +[19.015087, "o", "e"] +[19.047425, "o", "d"] +[19.079525, "o", "."] +[19.097202, "o", "j"] +[19.12692, "o", "s"] +[19.149295, "o", "o"] +[19.16655, "o", "n"] +[19.46655, "o", "\r\n"] +[19.61655, "o", "Wrote reduced.json\r\n"] +[22.31655, "o", "\r\n"] +[22.31655, "o", "\u001b[90m# Solve the target and recover a source solution\u001b[0m\r\n"] +[22.76655, "o", "$ "] +[22.806526, "o", "."] +[22.828593, "o", "/"] +[22.859265, "o", "p"] +[22.897127, "o", "r"] +[22.930169, "o", "e"] +[22.956626, "o", "d"] +[22.988579, "o", " "] +[23.021591, "o", "s"] +[23.061142, "o", "o"] +[23.081317, "o", "l"] +[23.114468, "o", "v"] +[23.138711, "o", "e"] +[23.163765, "o", " "] +[23.202973, "o", "r"] +[23.234622, "o", "e"] +[23.267814, "o", "d"] +[23.306671, "o", "u"] +[23.349885, "o", "c"] +[23.379692, "o", "e"] +[23.414336, "o", "d"] +[23.445923, "o", "."] +[23.477699, "o", "j"] +[23.514634, "o", "s"] +[23.544701, "o", "o"] +[23.577081, "o", "n"] +[23.877081, "o", "\r\n"] +[24.027081, "o", "Problem: MaximumIndependentSet\r\nSolver: ilp (ILP -> ILP) (via ILP)\r\nStatus: optimal\r\nSolution: Array [Bool(true), Bool(false), Bool(false), Bool(true), Bool(false)]\r\nEvaluation: Max(2)\r\n\r\nHint: use -o to save full solution details (including intermediate results) as JSON.\r\n"] +[26.727081, "o", "\r\n"] +[26.727081, "o", "\u001b[90m# Check an independent set of size two\u001b[0m\r\n"] +[27.177081, "o", "$ "] +[27.207882, "o", "."] +[27.251925, "o", "/"] +[27.289045, "o", "p"] +[27.331232, "o", "r"] +[27.375294, "o", "e"] +[27.399854, "o", "d"] +[27.432983, "o", " "] +[27.477076, "o", "e"] +[27.518219, "o", "v"] +[27.53928, "o", "a"] +[27.559898, "o", "l"] +[27.589673, "o", "u"] +[27.608888, "o", "a"] +[27.632907, "o", "t"] +[27.652139, "o", "e"] +[27.688409, "o", " "] +[27.72795, "o", "c"] +[27.770722, "o", "y"] +[27.792278, "o", "c"] +[27.829882, "o", "l"] +[27.865889, "o", "e"] +[27.887117, "o", "."] +[27.929483, "o", "j"] +[27.97427, "o", "s"] +[27.997687, "o", "o"] +[28.042045, "o", "n"] +[28.070566, "o", " "] +[28.101631, "o", "-"] +[28.147056, "o", "-"] +[28.187983, "o", "c"] +[28.209739, "o", "o"] +[28.239211, "o", "n"] +[28.271085, "o", "f"] +[28.297917, "o", "i"] +[28.320653, "o", "g"] +[28.346896, "o", " "] +[28.384672, "o", "'"] +[28.402372, "o", "["] +[28.435344, "o", "t"] +[28.465072, "o", "r"] +[28.482731, "o", "u"] +[28.509346, "o", "e"] +[28.544315, "o", ","] +[28.576094, "o", "f"] +[28.595074, "o", "a"] +[28.640362, "o", "l"] +[28.680029, "o", "s"] +[28.724935, "o", "e"] +[28.745071, "o", ","] +[28.769802, "o", "t"] +[28.788076, "o", "r"] +[28.827476, "o", "u"] +[28.852345, "o", "e"] +[28.87319, "o", ","] +[28.902397, "o", "f"] +[28.94558, "o", "a"] +[28.986123, "o", "l"] +[29.010654, "o", "s"] +[29.032065, "o", "e"] +[29.07547, "o", ","] +[29.108915, "o", "f"] +[29.14607, "o", "a"] +[29.165769, "o", "l"] +[29.184556, "o", "s"] +[29.221361, "o", "e"] +[29.250656, "o", "]"] +[29.269868, "o", "'"] +[29.569868, "o", "\r\n"] +[29.719868, "o", "Max(2)\r\n"] +[32.419868, "o", "\r\n"] +[32.419868, "o", "\u001b[90m# Solve the original instance with the default solver\u001b[0m\r\n"] +[32.869868, "o", "$ "] +[32.913821, "o", "."] +[32.949091, "o", "/"] +[32.989137, "o", "p"] +[33.008673, "o", "r"] +[33.050279, "o", "e"] +[33.069325, "o", "d"] +[33.111119, "o", " "] +[33.141227, "o", "s"] +[33.16806, "o", "o"] +[33.201004, "o", "l"] +[33.244624, "o", "v"] +[33.26942, "o", "e"] +[33.290255, "o", " "] +[33.322452, "o", "c"] +[33.346407, "o", "y"] +[33.366678, "o", "c"] +[33.388433, "o", "l"] +[33.407015, "o", "e"] +[33.429923, "o", "."] +[33.45598, "o", "j"] +[33.481837, "o", "s"] +[33.52068, "o", "o"] +[33.546108, "o", "n"] +[33.846108, "o", "\r\n"] +[33.996108, "o", "Problem: MaximumIndependentSet\r\nSolver: ilp (MaximumIndependentSet -> MaximumIndependentSet -> MaximumSetPacking -> ILP -> ILP)\r\nStatus: optimal\r\nSolution: Array [Bool(true), Bool(false), Bool(false), Bool(true), Bool(false)]\r\nEvaluation: Max(2)\r\n\r\nHint: use -o to save full solution details as JSON.\r\n"] +[36.696108, "o", "$ "] diff --git a/docs/src/static/cli-demo.html b/docs/src/static/cli-demo.html index 185580188..129dc6d66 100644 --- a/docs/src/static/cli-demo.html +++ b/docs/src/static/cli-demo.html @@ -2396,7 +2396,7 @@

From a graph to a verified solution

diff --git a/docs/src/static/reduction-graph.js b/docs/src/static/reduction-graph.js index 3e2c0d0b9..8132f6c43 100644 --- a/docs/src/static/reduction-graph.js +++ b/docs/src/static/reduction-graph.js @@ -176,7 +176,7 @@ if (srcName === dstName) return; var key = srcName + '->' + dstName; if (!nameLevelEdges[key]) { - nameLevelEdges[key] = { count: 0, overhead: e.overhead, doc_path: e.doc_path }; + nameLevelEdges[key] = { count: 0, parameters: e.parameters, doc_path: e.doc_path }; } nameLevelEdges[key].count++; }); @@ -191,7 +191,7 @@ target: problemNodeIds[parts[1]], label: info.count > 1 ? '\u00d7' + info.count : '', edgeLevel: 'collapsed', - overhead: info.overhead, + parameters: info.parameters, doc_path: info.doc_path } }); @@ -208,7 +208,7 @@ edgeMap[key] = { source: srcId, target: dstId, - overhead: e.overhead || [], + parameters: e.parameters || [], doc_path: e.doc_path || '' }; } @@ -219,16 +219,18 @@ var srcName = e.source.split('/')[0]; var dstName = e.target.split('/')[0]; var isVariantCast = srcName === dstName && - e.overhead && - e.overhead.length > 0 && - e.overhead.every(function(o) { return o.field === o.formula; }); + e.parameters && + e.parameters.length > 0 && + e.parameters.every(function(o) { + return o.contract === 'exact' && o.field === o.formula; + }); return { data: { id: 'variant_' + key, source: e.source, target: e.target, edgeLevel: 'variant', - overhead: e.overhead, + parameters: e.parameters, doc_path: e.doc_path, isVariantCast: isVariantCast } @@ -531,8 +533,12 @@ cy.on('mouseover', 'edge', function(evt) { var d = evt.target.data(); var html = '' + evt.target.source().data('label') + ' \u2192 ' + evt.target.target().data('label') + ''; - if (d.overhead && d.overhead.length > 0) { - html += '
' + d.overhead.map(function(o) { return '' + o.field + ' = ' + o.formula + ''; }).join('
'); + if (d.parameters && d.parameters.length > 0) { + html += '
' + d.parameters.map(function(o) { + if (o.contract === 'exact') return '' + o.field + ' = ' + o.formula + ' (exact)'; + if (o.contract === 'upper_bound') return '' + o.field + '' + o.formula + ' (upper bound)'; + return '' + o.field + ' unavailable: ' + o.reason; + }).join('
'); } html += '
Click to highlight, double-click for source code'; tooltip.innerHTML = html; @@ -642,8 +648,12 @@ edge.source().addClass('highlighted'); edge.target().addClass('highlighted'); var text = edge.source().data('label') + ' \u2192 ' + edge.target().data('label'); - if (d.overhead && d.overhead.length > 0) { - text += ' | ' + d.overhead.map(function(o) { return o.field + ' = ' + o.formula; }).join(', '); + if (d.parameters && d.parameters.length > 0) { + text += ' | ' + d.parameters.map(function(o) { + if (o.contract === 'exact') return o.field + ' = ' + o.formula + ' (exact)'; + if (o.contract === 'upper_bound') return o.field + ' <= ' + o.formula + ' (upper bound)'; + return o.field + ' unavailable: ' + o.reason; + }).join(', '); } instructions.textContent = text; clearBtn.style.display = 'inline'; diff --git a/docs/src/static/trait-hierarchy.typ b/docs/src/static/trait-hierarchy.typ index c23dd50dc..b6c167ffd 100644 --- a/docs/src/static/trait-hierarchy.typ +++ b/docs/src/static/trait-hierarchy.typ @@ -28,9 +28,10 @@ #strong[trait Problem]\ #text(size: 8pt, fill: secondary)[ `const NAME: &str`\ + `type Solution`\ `type Value: Clone`\ - `fn dims() -> Vec`\ - `fn evaluate(&config) -> Value`\ + `fn size() -> ProblemParameters`\ + `fn evaluate(&solution) -> Value`\ `fn variant() -> Vec<(&str, &str)>` ] ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), @@ -41,23 +42,35 @@ #text(size: 8pt, fill: secondary)[ `fn identity() -> Self`\ `fn combine(self, other) -> Self`\ - `fn supports_witnesses() -> bool`\ - `fn contributes_to_witnesses(...)` + `fn is_absorbing(&self) -> bool`\ + #strong[trait SolutionAggregate: Aggregate]\ + `fn contributes_to_solution(...)` ] ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), + // Brute-force capability (bottom center) + node((0.7, 1), box(width: 48mm, align(left)[ + #strong[trait BruteForceProblem]\ + #text(size: 8pt, fill: secondary)[ + `extends Problem`\ + `fn dimensions() -> Vec`\ + #text(style: "italic")[reference solver only] + ] + ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), + // Common value types (bottom right) - node((1.25, 1), box(width: 48mm, align(left)[ + node((1.4, 1), box(width: 48mm, align(left)[ #strong[Common Value Types]\ #text(size: 8pt, fill: secondary)[ `Max | Min | Extremum`\ `Or | Sum | And`\ - #text(style: "italic")[used as `Problem::Value`] + #text(style: "italic")[only selecting values implement `SolutionAggregate`] ] ]), fill: type-fill, corner-radius: 6pt, inset: 10pt, name: ), // Conceptual relationships edge(, , "->", label: text(size: 8pt)[solver-bound on `Value`], label-side: left, label-fill: none), + edge(, , "->", label: text(size: 8pt)[extends], label-fill: none), edge(, , "->", label: text(size: 8pt)[implements], label-side: right, label-fill: none), ) } diff --git a/docs/src/static/variant-hierarchy-dark.svg b/docs/src/static/variant-hierarchy-dark.svg index fe8394aad..b6b7d8a8a 100644 --- a/docs/src/static/variant-hierarchy-dark.svg +++ b/docs/src/static/variant-hierarchy-dark.svg @@ -1,766 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/src/static/variant-hierarchy.svg b/docs/src/static/variant-hierarchy.svg index 9a466eec3..04252bf5b 100644 --- a/docs/src/static/variant-hierarchy.svg +++ b/docs/src/static/variant-hierarchy.svg @@ -1,766 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/src/static/variant-hierarchy.typ b/docs/src/static/variant-hierarchy.typ index e1df29872..9db5aadca 100644 --- a/docs/src/static/variant-hierarchy.typ +++ b/docs/src/static/variant-hierarchy.typ @@ -1,4 +1,4 @@ -#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge +#import "@preview/fletcher:0.5.8" as fletcher: diagram, node #set page(width: auto, height: auto, margin: (top: 5pt, bottom: 5pt, left: 5pt, right: 5pt), fill: none) #set text(font: "Helvetica Neue") @@ -28,47 +28,31 @@ node((3.2, -0.5), text(size: 10pt, weight: "bold")[Weights], stroke: none, fill: none), node((5, -0.5), text(size: 10pt, weight: "bold")[K Values], stroke: none, fill: none), - // Graph hierarchy (tree) - node((0, 0), [HyperGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((0, 1), [SimpleGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((-1, 2), [PlanarGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((0, 2), [BipartiteGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((1, 2), [UnitDiskGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((0.5, 3), [KingsSubgraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((1.5, 3), [TriangularSubgraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt, name: ), - - edge(, , "->"), - edge(, , "->"), - edge(, , "->"), - edge(, , "->"), - edge(, , "->"), - edge(, , "->"), - - // Weight hierarchy (chain: One → i32 → f64) - node((3.2, 0), [f64], fill: weight-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((3.2, 1), [i32], fill: weight-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((3.2, 2), [One], fill: weight-fill, corner-radius: 5pt, inset: 6pt, name: ), - - edge(, , "->"), - edge(, , "->"), - - // K value hierarchy (flat star) - node((5, 0), [KN], fill: k-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((4.2, 1), [K1], fill: k-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((4.6, 1), [K2], fill: k-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((5, 1), [K3], fill: k-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((5.4, 1), [K4], fill: k-fill, corner-radius: 5pt, inset: 6pt, name: ), - node((5.8, 1), [K5], fill: k-fill, corner-radius: 5pt, inset: 6pt, name: ), - - edge(, , "->"), - edge(, , "->"), - edge(, , "->"), - edge(, , "->"), - edge(, , "->"), + // Registered graph values + node((0, 0), [HyperGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt), + node((0, 1), [SimpleGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt), + node((-1, 2), [PlanarGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt), + node((0, 2), [BipartiteGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt), + node((1, 2), [UnitDiskGraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt), + node((0.5, 3), [KingsSubgraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt), + node((1.5, 3), [TriangularSubgraph], fill: graph-fill, corner-radius: 5pt, inset: 6pt), + + // Registered weight values + node((3.2, 0), [f64], fill: weight-fill, corner-radius: 5pt, inset: 6pt), + node((3.2, 1), [i64], fill: weight-fill, corner-radius: 5pt, inset: 6pt), + node((3.2, 2), [One], fill: weight-fill, corner-radius: 5pt, inset: 6pt), + + // Registered K values + node((5, 0), [KN], fill: k-fill, corner-radius: 5pt, inset: 6pt), + node((4.2, 1), [K1], fill: k-fill, corner-radius: 5pt, inset: 6pt), + node((4.6, 1), [K2], fill: k-fill, corner-radius: 5pt, inset: 6pt), + node((5, 1), [K3], fill: k-fill, corner-radius: 5pt, inset: 6pt), + node((5.4, 1), [K4], fill: k-fill, corner-radius: 5pt, inset: 6pt), + node((5.8, 1), [K5], fill: k-fill, corner-radius: 5pt, inset: 6pt), ) v(3mm) - text(size: 8pt, fill: secondary)[Arrows point from specific to general (subtype direction).] + text(size: 8pt, fill: secondary)[Concrete values grouped by variant dimension.] } #let standalone-dark = sys.inputs.at("dark", default: "false") == "true" diff --git a/docs/website/assets/site.css b/docs/website/assets/site.css index 383f04e97..d98daa52b 100644 --- a/docs/website/assets/site.css +++ b/docs/website/assets/site.css @@ -910,6 +910,27 @@ pre { color: var(--green); line-height: 1.5; margin-block: 23px; + overflow-x: auto; +} +.formula math { + font-family: math; +} +.formula math[display="block"] { + margin: 0; + text-align: left; +} +.formula-constraint { + margin-top: 14px; + font-size: 18px; +} +.math-expression { + overflow-x: auto; + max-width: 100%; + padding-block: 8px; +} +.math-expression math { + font-family: math; + font-size: 1.05em; } .formula small { display: block; diff --git a/docs/website/assets/site.js b/docs/website/assets/site.js index 743c32e1c..7d09f9def 100644 --- a/docs/website/assets/site.js +++ b/docs/website/assets/site.js @@ -353,7 +353,7 @@

${mis ? "A simple question. A rich search space." : "The problem"}

- ${mis ? '

Given a graph G = (V, E), choose a set of vertices such that no two chosen vertices are adjacent. Maximize the total weight of the chosen vertices. With unit weights, this is the largest independent set.

maximize ∑ wᵥ xᵥsubject to xᵤ + xᵥ ≤ 1 for every edge (u, v), with xᵥ ∈ {0, 1}.
' : `

${escape(description(name))}. The fields below define the instance accepted by the implementation. Consult the paper for its mathematical definition and the API for its full contract.

`}${mis && node.variant.graph === "SimpleGraph" ? demoPanel() : ""} + ${mis ? '

Given a graph G = (V, E), choose a set of vertices such that no two chosen vertices are adjacent. Maximize the total weight of the chosen vertices. With unit weights, this is the largest independent set.

maxvVwvxv
xu+xv1,(u,v)E
xv{0,1},vV
' : `

${escape(description(name))}. The fields below define the instance accepted by the implementation. Consult the paper for its mathematical definition and the API for its full contract.

`}${mis && node.variant.graph === "SimpleGraph" ? demoPanel() : ""}

Connections for this variant

@@ -400,7 +400,7 @@
Registered complexity
-
${escape(node.complexity || "See API")}
+
${node.complexity_mathml}
Direct connections
@@ -475,7 +475,7 @@

The construction

- ${complement ? `

The target uses the same graph and vertex weights. ${vcToMis ? "Solve Maximum Independent Set on that graph, then take the complement of its vertex selection to recover a minimum vertex cover." : "Solve Minimum Vertex Cover on that graph, then take the complement of its vertex selection to recover a maximum independent set."}

S ↔ V ∖ SA set is independent exactly when its complement is a vertex cover.

For weighted instances, the two objective values sum to the total vertex weight. Maximizing the independent-set weight therefore minimizes the complementary cover weight.

${demoPanel()}` : `

This rule maps ${escape(nameOf(sourceName))} to ${escape(nameOf(targetName))}. The implementation and paper describe the mapping, its preconditions, and solution extraction.

Read the construction `} + ${complement ? `

The target uses the same graph and vertex weights. ${vcToMis ? "Solve Maximum Independent Set on that graph, then take the complement of its vertex selection to recover a minimum vertex cover." : "Solve Minimum Vertex Cover on that graph, then take the complement of its vertex selection to recover a maximum independent set."}

SVSA set is independent exactly when its complement is a vertex cover.

For weighted instances, the two objective values sum to the total vertex weight. Maximizing the independent-set weight therefore minimizes the complementary cover weight.

${demoPanel()}` : `

This rule maps ${escape(nameOf(sourceName))} to ${escape(nameOf(targetName))}. The implementation and paper describe the mapping, its preconditions, and solution extraction.

Read the construction `}

What the rule guarantees

@@ -497,24 +497,23 @@ >
-

Target size bounds

+

Target parameter relations

- + + - ${(edge.overhead || []).map((o) => ``).join("")} + ${edge.parameters.map((p) => ``).join("")}
Target parameterBound in source parametersRelationFormula or reason
${escape(o.field)}${escape(o.formula)}
${escape(p.field)}${escape(p.contract)}${p.mathml ?? escape(p.reason)}

- Registry overheads are asymptotic upper bounds. Read the - constructor for exact instance - sizes.${complement ? " This construction preserves the number of vertices and edges exactly." : ""} + Each relation is an exact equality, an upper bound, or unavailable with a reason. Read the constructor for concrete instance parameters.${complement ? " This construction preserves the number of vertices and edges exactly." : ""}

diff --git a/examples/chained_reduction_factoring_to_spinglass.rs b/examples/chained_reduction_factoring_to_spinglass.rs index 8374906e7..48067cd3a 100644 --- a/examples/chained_reduction_factoring_to_spinglass.rs +++ b/examples/chained_reduction_factoring_to_spinglass.rs @@ -7,73 +7,57 @@ // ANCHOR: imports use problemreductions::models::algebraic::ILP; use problemreductions::prelude::*; -use problemreductions::rules::{MinimizeSteps, ReductionGraph}; +use problemreductions::rules::{ReductionGraph, ReductionMode}; use problemreductions::solvers::ILPSolver; use problemreductions::topology::SimpleGraph; -use problemreductions::types::ProblemSize; // ANCHOR_END: imports -pub fn run() { +pub fn run() -> std::result::Result<(), Box> { // ANCHOR: example // ANCHOR: step1 let graph = ReductionGraph::new(); // all registered reductions let src_var = ReductionGraph::variant_to_map(&Factoring::variant()); // {} (no variant params) let dst_var = ReductionGraph::variant_to_map(&SpinGlass::::variant()); // {graph: "SimpleGraph", weight: "f64"} - let rpath = graph - .find_cheapest_path( - "Factoring", // source problem name - &src_var, // source variant map - "SpinGlass", // target problem name - &dst_var, // target variant map - &ProblemSize::new(vec![]), // input size (empty = unknown) - &MinimizeSteps, // cost function: fewest hops - ) - .unwrap(); + let paths = graph.find_all_paths_mode( + "Factoring", + &src_var, + "SpinGlass", + &dst_var, + ReductionMode::Witness, + ); + let rpath = paths + .iter() + .find(|path| path.type_names() == ["Factoring", "CircuitSAT", "SpinGlass"]) + .expect("explicit Factoring -> CircuitSAT -> SpinGlass route"); println!(" {}", rpath); // ANCHOR_END: step1 // ANCHOR: step2 - let factoring = Factoring::new( + let factoring = Factoring::with_factor_bits( + 6, // target_product: find p × q = 6 2, // num_bits_first: p is a 2-bit factor 2, // num_bits_second: q is a 2-bit factor - 6, // target_product: find p × q = 6 ); // ANCHOR_END: step2 // ANCHOR: step3 - // Factoring reduces to ILP, so we manually reduce, solve, and extract + // Factoring reduces to ILP, so we manually reduce, solve, and extract let solver = ILPSolver::new(); - let reduction = ReduceTo::>::reduce_to(&factoring); + let reduction = ReduceTo::>::reduce_to(&factoring).expect("reduction should succeed"); let ilp_solution = solver.solve(reduction.target_problem()).unwrap(); - let solution = reduction.extract_solution(&ilp_solution); + let solution = reduction.extract_solution(&ilp_solution).unwrap(); // ANCHOR_END: step3 // ANCHOR: step4 - let (p, q) = factoring.read_factors(&solution); // decode bit assignments → integers + let (p, q) = solution; println!("{} = {} × {}", factoring.target(), p, q); - assert_eq!(p * q, 6, "Factors should multiply to 6"); + assert_eq!(p * q, 6u32.into(), "Factors should multiply to 6"); // ANCHOR_END: step4 - // ANCHOR: overhead - // Print per-edge overhead polynomials - let edge_overheads = graph.path_overheads(&rpath); - for (i, overhead) in edge_overheads.iter().enumerate() { - println!("{} → {}:", rpath.steps[i], rpath.steps[i + 1]); - for (field, poly) in &overhead.output_size { - println!(" {} = {}", field, poly); - } - } - - // Compose overheads symbolically along the full path - let composed = graph.compose_path_overhead(&rpath); - println!("Composed (source → target):"); - for (field, poly) in &composed.output_size { - println!(" {} = {}", field, poly); - } - // ANCHOR_END: overhead // ANCHOR_END: example + Ok(()) } -fn main() { +fn main() -> std::result::Result<(), Box> { run() } diff --git a/examples/export_graph.rs b/examples/export_graph.rs index 2b1b6e4dc..5a1ef241d 100644 --- a/examples/export_graph.rs +++ b/examples/export_graph.rs @@ -3,9 +3,9 @@ //! Run with: `cargo run --example export_graph [output_path]` use problemreductions::rules::ReductionGraph; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -fn main() { +pub fn run(output_path: &Path) { let graph = ReductionGraph::new(); // Print statistics @@ -13,19 +13,13 @@ fn main() { println!(" Problem types: {}", graph.num_types()); println!(" Reductions: {}", graph.num_reductions()); - // Export to JSON (single source for both mdBook and paper) - let output_path = std::env::args() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("docs/src/reductions/reduction_graph.json")); - // Create parent directories if needed if let Some(parent) = output_path.parent() { std::fs::create_dir_all(parent).expect("Failed to create output directory"); } graph - .to_json_file(&output_path) + .to_json_file(output_path) .expect("Failed to write JSON file"); println!("\nExported to: {}", output_path.display()); @@ -34,3 +28,11 @@ fn main() { println!("\nJSON content:"); println!("{}", graph.to_json_string().unwrap()); } + +fn main() { + let output_path = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("docs/src/reductions/reduction_graph.json")); + run(&output_path); +} diff --git a/examples/export_mapping_stages.rs b/examples/export_mapping_stages.rs index 3caf8cdec..884ac242c 100644 --- a/examples/export_mapping_stages.rs +++ b/examples/export_mapping_stages.rs @@ -20,9 +20,9 @@ use std::fs; #[derive(Serialize)] struct GridNodeExport { - row: i32, - col: i32, - weight: i32, + row: i64, + col: i64, + weight: i64, state: String, // "O" = Occupied, "D" = Doubled, "C" = Connected } @@ -39,8 +39,8 @@ struct CopyLineExport { #[derive(Serialize)] struct LocationExport { - row: i32, - col: i32, + row: i64, + col: i64, } #[derive(Serialize)] @@ -50,7 +50,7 @@ struct TapeEntryExport { gadget_idx: usize, row: usize, col: usize, - overhead: i32, + overhead: i64, } #[derive(Serialize)] @@ -75,10 +75,10 @@ struct MappingExport { stages: Vec, crossing_tape: Vec, simplifier_tape: Vec, - copyline_overhead: i32, - crossing_overhead: i32, - simplifier_overhead: i32, - total_overhead: i32, + copyline_overhead: i64, + crossing_overhead: i64, + simplifier_overhead: i64, + total_overhead: i64, } fn gadget_name(idx: usize) -> String { @@ -119,8 +119,8 @@ fn extract_grid_nodes(grid: &MappingGrid) -> Vec { CellState::Empty => ".", }; nodes.push(GridNodeExport { - row: r as i32, // 0-indexed - DO NOT change! - col: c as i32, // 0-indexed - DO NOT change! + row: r as i64, // 0-indexed - DO NOT change! + col: c as i64, // 0-indexed - DO NOT change! weight: cell.weight(), state: state.to_string(), }); @@ -158,7 +158,7 @@ fn crossat_triangular( } fn get_vertex_order_from_julia(graph_name: &str) -> Option> { - let path = format!("tests/julia/{}_triangular_trace.json", graph_name); + let path = format!("tests/data/{}_triangular_trace.json", graph_name); if let Ok(content) = fs::read_to_string(&path) { if let Ok(data) = serde_json::from_str::(&content) { if let Some(copy_lines) = data["copy_lines"].as_array() { @@ -229,7 +229,7 @@ fn export_triangular( let spacing = triangular::SPACING; let padding = triangular::PADDING; - let copylines = create_copylines(n, edges, vertex_order); + let copylines = create_copylines(n, edges, vertex_order).unwrap(); let max_hslot = copylines.iter().map(|l| l.hslot).max().unwrap_or(1); let max_vstop = copylines.iter().map(|l| l.vstop).max().unwrap_or(1); @@ -239,7 +239,7 @@ fn export_triangular( let mut grid = MappingGrid::with_padding(rows, cols, spacing, padding); for line in ©lines { for (row, col, weight) in line.copyline_locations_triangular(padding, spacing) { - grid.add_node(row, col, weight as i32); + grid.add_node(row, col, weight as i64); } } let stage1_nodes = extract_grid_nodes(&grid); @@ -276,17 +276,23 @@ fn export_triangular( let simplifier_tape = triangular::apply_simplifier_gadgets(&mut grid, 10); let stage4_nodes = extract_grid_nodes(&grid); - let copyline_overhead: i32 = copylines + let copyline_overhead: i64 = copylines .iter() - .map(|line| mis_overhead_copyline_triangular(line, spacing)) + .map(|line| mis_overhead_copyline_triangular(line, spacing).unwrap()) .sum(); - let crossing_overhead: i32 = crossing_tape + let crossing_overhead: i64 = crossing_tape .iter() - .map(triangular::tape_entry_mis_overhead) + .map(|entry| { + triangular::tape_entry_mis_overhead(entry) + .expect("generated triangular crossing tape must contain known gadgets") + }) .sum(); - let simplifier_overhead: i32 = simplifier_tape + let simplifier_overhead: i64 = simplifier_tape .iter() - .map(triangular::tape_entry_mis_overhead) + .map(|entry| { + triangular::tape_entry_mis_overhead(entry) + .expect("generated triangular simplifier tape must contain known gadgets") + }) .sum(); let copy_lines_export = export_copylines_triangular(©lines, padding, spacing); @@ -325,7 +331,7 @@ fn export_square( let spacing = ksg::SPACING; let padding = ksg::PADDING; - let copylines = create_copylines(n, edges, vertex_order); + let copylines = create_copylines(n, edges, vertex_order).unwrap(); let max_hslot = copylines.iter().map(|l| l.hslot).max().unwrap_or(1); let max_vstop = copylines.iter().map(|l| l.vstop).max().unwrap_or(1); @@ -375,14 +381,23 @@ fn export_square( let simplifier_tape = ksg::apply_simplifier_gadgets(&mut grid, 2); let stage4_nodes = extract_grid_nodes(&grid); - let copyline_overhead: i32 = copylines + let copyline_overhead: i64 = copylines + .iter() + .map(|line| mis_overhead_copyline(line, spacing, padding).unwrap()) + .sum(); + let crossing_overhead: i64 = crossing_tape .iter() - .map(|line| mis_overhead_copyline(line, spacing, padding) as i32) + .map(|entry| { + ksg::tape_entry_mis_overhead(entry) + .expect("generated KSG crossing tape must contain known gadgets") + }) .sum(); - let crossing_overhead: i32 = crossing_tape.iter().map(ksg::tape_entry_mis_overhead).sum(); - let simplifier_overhead: i32 = simplifier_tape + let simplifier_overhead: i64 = simplifier_tape .iter() - .map(ksg::tape_entry_mis_overhead) + .map(|entry| { + ksg::tape_entry_mis_overhead(entry) + .expect("generated KSG simplifier tape must contain known gadgets") + }) .sum(); let copy_lines_export = export_copylines_square(©lines, padding, spacing); @@ -421,7 +436,7 @@ fn export_weighted( let spacing = ksg::SPACING; let padding = ksg::PADDING; - let copylines = create_copylines(n, edges, vertex_order); + let copylines = create_copylines(n, edges, vertex_order).unwrap(); let max_hslot = copylines.iter().map(|l| l.hslot).max().unwrap_or(1); let max_vstop = copylines.iter().map(|l| l.vstop).max().unwrap_or(1); @@ -431,7 +446,7 @@ fn export_weighted( let mut grid = MappingGrid::with_padding(rows, cols, spacing, padding); for line in ©lines { for (row, col, weight) in line.copyline_locations(padding, spacing) { - grid.add_node(row, col, weight as i32); // Use actual weights from copyline (1 at endpoints, 2 elsewhere) + grid.add_node(row, col, weight as i64); // Use actual weights from copyline (1 at endpoints, 2 elsewhere) } } let stage1_nodes = extract_grid_nodes(&grid); @@ -469,17 +484,23 @@ fn export_weighted( let stage4_nodes = extract_grid_nodes(&grid); // Weighted mode: overhead = unweighted_overhead * 2 - let copyline_overhead: i32 = copylines + let copyline_overhead: i64 = copylines .iter() - .map(|line| mis_overhead_copyline(line, spacing, padding) as i32 * 2) + .map(|line| mis_overhead_copyline(line, spacing, padding).unwrap() * 2) .sum(); - let crossing_overhead: i32 = crossing_tape + let crossing_overhead: i64 = crossing_tape .iter() - .map(ksg::weighted_tape_entry_mis_overhead) + .map(|entry| { + ksg::weighted_tape_entry_mis_overhead(entry) + .expect("generated weighted KSG crossing tape must contain known gadgets") + }) .sum(); - let simplifier_overhead: i32 = simplifier_tape + let simplifier_overhead: i64 = simplifier_tape .iter() - .map(ksg::weighted_tape_entry_mis_overhead) + .map(|entry| { + ksg::weighted_tape_entry_mis_overhead(entry) + .expect("generated weighted KSG simplifier tape must contain known gadgets") + }) .sum(); let copy_lines_export = export_copylines_square(©lines, padding, spacing); @@ -555,8 +576,8 @@ fn export_copylines_triangular( locations: locs .iter() .map(|(r, c, _)| LocationExport { - row: *r as i32, // 0-indexed - DO NOT change! - col: *c as i32, // 0-indexed - DO NOT change! + row: *r as i64, // 0-indexed - DO NOT change! + col: *c as i64, // 0-indexed - DO NOT change! }) .collect(), } @@ -584,8 +605,8 @@ fn export_copylines_square( locations: locs .iter() .map(|(r, c, _)| LocationExport { - row: *r as i32, // 0-indexed - DO NOT change! - col: *c as i32, // 0-indexed - DO NOT change! + row: *r as i64, // 0-indexed - DO NOT change! + col: *c as i64, // 0-indexed - DO NOT change! }) .collect(), } @@ -606,7 +627,8 @@ fn export_triangular_tape( gadget_idx: e.gadget_idx, row: e.row, // 0-indexed - DO NOT change! col: e.col, // 0-indexed - DO NOT change! - overhead: triangular::tape_entry_mis_overhead(e), + overhead: triangular::tape_entry_mis_overhead(e) + .expect("generated triangular tape must contain known gadgets"), }) .collect() } @@ -621,7 +643,8 @@ fn export_square_tape(tape: &[ksg::KsgTapeEntry], offset: usize) -> Vec, crossing_tape: Vec, simplifier_tape: Vec, - copyline_overhead: i32, - crossing_overhead: i32, - simplifier_overhead: i32, + copyline_overhead: i64, + crossing_overhead: i64, + simplifier_overhead: i64, ) -> MappingExport { let mut export = MappingExport { graph_name: graph_name.to_string(), diff --git a/examples/export_petersen_mapping.rs b/examples/export_petersen_mapping.rs index d8a9ec56f..87753371e 100644 --- a/examples/export_petersen_mapping.rs +++ b/examples/export_petersen_mapping.rs @@ -47,7 +47,7 @@ struct SourceGraph { struct GridVisualization { nodes: Vec, edges: Vec<(usize, usize)>, - mis_overhead: i32, + mis_overhead: i64, padding: usize, spacing: usize, weighted: bool, @@ -55,9 +55,9 @@ struct GridVisualization { #[derive(Serialize)] struct NodeData { - row: i32, - col: i32, - weight: i32, + row: i64, + col: i64, + weight: i64, } impl GridVisualization { @@ -90,7 +90,7 @@ fn write_json(data: &T, path: &Path) { println!(" Wrote: {}", path.display()); } -fn main() { +pub fn run(output_dir: &Path) { println!("\n=== Independent Set to Grid Graph IS (Unit Disk Mapping) ===\n"); // Petersen graph: n=10, MIS=4 @@ -133,13 +133,13 @@ fn main() { edges: petersen_edges.clone(), mis: petersen_mis, }; - write_json(&source, Path::new("docs/paper/static/petersen_source.json")); + write_json(&source, &output_dir.join("petersen_source.json")); println!("\n=== Mapping to Grid Graphs ===\n"); // Map to weighted King's subgraph (square lattice) println!("1. King's Subgraph (Weighted)"); - let square_weighted_result = ksg::map_weighted(num_vertices, &petersen_edges); + let square_weighted_result = ksg::map_weighted(num_vertices, &petersen_edges).unwrap(); let square_weighted_viz = GridVisualization::from_result(&square_weighted_result, true); println!( " Vertices: {}, Edges: {}", @@ -151,16 +151,16 @@ fn main() { " MIS(grid) = MIS(source) + Δ = {} + {} = {}", petersen_mis, square_weighted_result.mis_overhead, - petersen_mis as i32 + square_weighted_result.mis_overhead + petersen_mis as i64 + square_weighted_result.mis_overhead ); write_json( &square_weighted_viz, - Path::new("docs/paper/static/petersen_square_weighted.json"), + &output_dir.join("petersen_square_weighted.json"), ); // Map to unweighted King's subgraph (square lattice) println!("\n2. King's Subgraph (Unweighted)"); - let square_unweighted_result = ksg::map_unweighted(num_vertices, &petersen_edges); + let square_unweighted_result = ksg::map_unweighted(num_vertices, &petersen_edges).unwrap(); let square_unweighted_viz = GridVisualization::from_result(&square_unweighted_result, false); println!( " Vertices: {}, Edges: {}", @@ -175,16 +175,16 @@ fn main() { " MIS(grid) = MIS(source) + Δ = {} + {} = {}", petersen_mis, square_unweighted_result.mis_overhead, - petersen_mis as i32 + square_unweighted_result.mis_overhead + petersen_mis as i64 + square_unweighted_result.mis_overhead ); write_json( &square_unweighted_viz, - Path::new("docs/paper/static/petersen_square_unweighted.json"), + &output_dir.join("petersen_square_unweighted.json"), ); // Map to weighted triangular lattice println!("\n3. Triangular Lattice (Weighted)"); - let triangular_result = triangular::map_weighted(num_vertices, &petersen_edges); + let triangular_result = triangular::map_weighted(num_vertices, &petersen_edges).unwrap(); let triangular_viz = GridVisualization::from_result(&triangular_result, true); println!( " Vertices: {}, Edges: {}", @@ -196,11 +196,11 @@ fn main() { " MIS(grid) = MIS(source) + Δ = {} + {} = {}", petersen_mis, triangular_result.mis_overhead, - petersen_mis as i32 + triangular_result.mis_overhead + petersen_mis as i64 + triangular_result.mis_overhead ); write_json( &triangular_viz, - Path::new("docs/paper/static/petersen_triangular.json"), + &output_dir.join("petersen_triangular.json"), ); println!("\n=== Summary ===\n"); @@ -209,22 +209,26 @@ fn main() { println!( "King's subgraph (weighted): {} vertices, MIS = {} (overhead Δ = {})", square_weighted_viz.nodes.len(), - petersen_mis as i32 + square_weighted_result.mis_overhead, + petersen_mis as i64 + square_weighted_result.mis_overhead, square_weighted_result.mis_overhead ); println!( "King's subgraph (unweighted): {} vertices, MIS = {} (overhead Δ = {})", square_unweighted_viz.nodes.len(), - petersen_mis as i32 + square_unweighted_result.mis_overhead, + petersen_mis as i64 + square_unweighted_result.mis_overhead, square_unweighted_result.mis_overhead ); println!( "Triangular lattice (weighted): {} vertices, MIS = {} (overhead Δ = {})", triangular_viz.nodes.len(), - petersen_mis as i32 + triangular_result.mis_overhead, + petersen_mis as i64 + triangular_result.mis_overhead, triangular_result.mis_overhead ); println!("\n✓ Unit disk mapping demonstrated successfully"); println!(" JSON files exported for paper visualization"); } + +fn main() { + run(Path::new("docs/paper/static")); +} diff --git a/examples/export_schemas.rs b/examples/export_schemas.rs index 19024dbb7..427386ca3 100644 --- a/examples/export_schemas.rs +++ b/examples/export_schemas.rs @@ -3,22 +3,25 @@ //! Run with: `cargo run --example export_schemas [output_path]` use problemreductions::registry::collect_schemas; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -fn main() { +pub fn run(output_path: &Path) { let schemas = collect_schemas(); println!("Collected {} problem schemas", schemas.len()); - // Single source for both mdBook and paper - let output_path = std::env::args() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("docs/src/reductions/problem_schemas.json")); if let Some(parent) = output_path.parent() { std::fs::create_dir_all(parent).expect("Failed to create output directory"); } let json = serde_json::to_string_pretty(&schemas).expect("Failed to serialize"); - std::fs::write(&output_path, &json).expect("Failed to write file"); + std::fs::write(output_path, &json).expect("Failed to write file"); println!("Exported to: {}", output_path.display()); } + +fn main() { + let output_path = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("docs/src/reductions/problem_schemas.json")); + run(&output_path); +} diff --git a/problemreductions-cli/Cargo.toml b/problemreductions-cli/Cargo.toml index 234f607fd..be37a9085 100644 --- a/problemreductions-cli/Cargo.toml +++ b/problemreductions-cli/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" description = "CLI tool for exploring NP-hard problem reductions" license = "MIT" repository = "https://github.com/CodingThrust/problem-reductions" +default-run = "pred" [[bin]] name = "pred" @@ -15,16 +16,12 @@ name = "pred-sym" path = "src/bin/pred_sym.rs" [features] -default = ["highs"] -all = ["highs", "mcp"] -highs = ["problemreductions/ilp-highs"] +all = ["mcp"] mcp = ["dep:rmcp", "dep:tokio", "dep:schemars", "dep:tracing", "dep:tracing-subscriber"] -cplex = ["problemreductions/ilp-cplex"] -lp-solvers = ["problemreductions/ilp-lp-solvers"] [dependencies] -problemreductions = { version = "0.6.0", path = "..", default-features = false, features = ["example-db"] } -clap = { version = "4", features = ["derive"] } +problemreductions = { version = "0.6.0", path = "..", features = ["example-db"] } +clap = { version = "4", features = ["derive", "string"] } anyhow = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/problemreductions-cli/src/bin/pred_sym.rs b/problemreductions-cli/src/bin/pred_sym.rs index daa28bf7a..28f7bdadb 100644 --- a/problemreductions-cli/src/bin/pred_sym.rs +++ b/problemreductions-cli/src/bin/pred_sym.rs @@ -1,5 +1,5 @@ use clap::{Parser, Subcommand}; -use problemreductions::{big_o_normal_form, canonical_form, Expr, ProblemSize}; +use problemreductions::{big_o_normal_form, evaluate_approximate, Expr, ProblemParameters}; #[derive(Parser)] #[command( @@ -19,11 +19,6 @@ enum Commands { /// Expression string expr: String, }, - /// Compute exact canonical form - Canon { - /// Expression string - expr: String, - }, /// Compute Big-O normal form BigO { /// Expression string @@ -33,7 +28,7 @@ enum Commands { #[arg(long)] raw: bool, }, - /// Compare two expressions (exits with code 1 if neither exact nor Big-O equal) + /// Compare two expressions for Big-O equivalence (exits 1 if not equal) Compare { /// First expression a: String, @@ -69,16 +64,6 @@ fn main() { let parsed = parse_expr_or_exit(&expr); println!("{parsed}"); } - Commands::Canon { expr } => { - let parsed = parse_expr_or_exit(&expr); - match canonical_form(&parsed) { - Ok(result) => println!("{result}"), - Err(e) => { - eprintln!("Error: {e}"); - std::process::exit(1); - } - } - } Commands::BigO { expr, raw } => { let parsed = parse_expr_or_exit(&expr); match big_o_normal_form(&parsed) { @@ -98,49 +83,49 @@ fn main() { Commands::Compare { a, b } => { let expr_a = parse_expr_or_exit(&a); let expr_b = parse_expr_or_exit(&b); - let canon_a = canonical_form(&expr_a); - let canon_b = canonical_form(&expr_b); let big_o_a = big_o_normal_form(&expr_a); let big_o_b = big_o_normal_form(&expr_b); println!("Expression A: {a}"); println!("Expression B: {b}"); - let mut exact_equal = false; - let mut big_o_equal = false; - if let (Ok(ca), Ok(cb)) = (&canon_a, &canon_b) { - exact_equal = ca == cb; - println!("Canonical A: {ca}"); - println!("Canonical B: {cb}"); - println!("Exact equal: {exact_equal}"); - } - if let (Ok(ba), Ok(bb)) = (&big_o_a, &big_o_b) { - big_o_equal = ba == bb; - println!("Big-O A: O({ba})"); - println!("Big-O B: O({bb})"); - println!("Big-O equal: {big_o_equal}"); - } - if !exact_equal && !big_o_equal { - std::process::exit(1); + match (&big_o_a, &big_o_b) { + (Ok(ba), Ok(bb)) => { + // Rendering is canonical, so equal growth ⇒ equal Big-O expr. + let big_o_equal = ba == bb; + println!("Big-O A: O({ba})"); + println!("Big-O B: O({bb})"); + println!("Big-O equal: {big_o_equal}"); + if !big_o_equal { + std::process::exit(1); + } + } + _ => { + if let Err(e) = &big_o_a { + println!("Big-O A: "); + } + if let Err(e) = &big_o_b { + println!("Big-O B: "); + } + std::process::exit(1); + } } } Commands::Eval { expr, vars } => { let parsed = parse_expr_or_exit(&expr); - let bindings: Vec<(&str, usize)> = vars + let bindings: Vec<(String, u64)> = vars .split(',') .filter_map(|pair| { let mut parts = pair.splitn(2, '='); let name = parts.next()?.trim(); - let value: usize = parts.next()?.trim().parse().ok()?; - // Leak the name for &'static str compatibility - let leaked: &'static str = Box::leak(name.to_string().into_boxed_str()); - Some((leaked, value)) + let value: u64 = parts.next()?.trim().parse().ok()?; + Some((name.to_string(), value)) }) .collect(); // Check for unbound variables let expr_vars = parsed.variables(); let bound_vars: std::collections::HashSet<&str> = - bindings.iter().map(|(k, _)| *k).collect(); + bindings.iter().map(|(name, _)| name.as_str()).collect(); let mut unbound: Vec<&str> = expr_vars .iter() .filter(|v| !bound_vars.contains(*v)) @@ -156,8 +141,11 @@ fn main() { std::process::exit(1); } - let size = ProblemSize::new(bindings); - let result = parsed.eval(&size); + let parameters = ProblemParameters::from_owned(bindings); + let result = evaluate_approximate(&parsed, ¶meters).unwrap_or_else(|error| { + eprintln!("Error: {error}"); + std::process::exit(1); + }); // Format as integer if it's a whole number if (result - result.round()).abs() < 1e-10 { diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 70fa1e5af..dd1518a1b 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -1,7 +1,10 @@ -use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; -use std::collections::HashMap; +use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum}; +use problemreductions::registry::ProblemCategory; +use std::ffi::OsString; use std::path::PathBuf; +pub use crate::create_args::CreateArgs; + #[derive(Parser)] #[command( name = "pred", @@ -11,13 +14,13 @@ use std::path::PathBuf; Typical workflow: pred create MIS --graph 0-1,1-2,2-3 -o problem.json pred solve problem.json - pred evaluate problem.json --config 1,0,1,0 + pred evaluate problem.json --config '[true,false,true,false]' Piping (use - to read from stdin): - pred create MIS --graph 0-1,1-2 | pred solve - # when an ILP reduction path exists + pred create MIS --graph 0-1,1-2 | pred solve - pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force - pred create MIS --graph 0-1,1-2 | pred evaluate - --config 1,0,1 - pred create MIS --graph 0-1,1-2 | pred reduce - --to QUBO + pred create MIS --graph 0-1,1-2 | pred evaluate - --config '[true,false,true]' + pred create MIS --graph 0-1,1-2 | pred reduce - --via route.json JSON output (any command): pred list --json # JSON to stdout @@ -46,18 +49,65 @@ pub struct Cli { pub command: Commands, } +impl Cli { + pub fn try_parse() -> Result { + Self::try_parse_from(std::env::args_os()) + } + + pub fn try_parse_from(args: I) -> Result + where + I: IntoIterator, + T: Into, + { + // The discovery command treats the problem spec as an external subcommand, + // so it can capture the selected model without registering the whole catalog. + let args = args.into_iter().map(Into::into).collect::>(); + let command = ::command(); + let discovery_matches = command.clone().try_get_matches_from(args.clone())?; + let selected = discovery_matches + .subcommand_matches("create") + .and_then(|matches| matches.subcommand_name()); + + let mut matches = if let Some(selected) = selected { + crate::create_args::command_for_selected_problem(command, selected)? + .try_get_matches_from(args)? + } else { + discovery_matches + }; + Self::from_arg_matches_mut(&mut matches) + } +} + #[derive(Subcommand)] pub enum Commands { - /// List all registered problem types (or reduction rules with --rules) + /// Browse registered problem types (or reduction rules with --rules) #[command(after_help = "\ Examples: - pred list # list problem types - pred list --rules # list all reduction rules + pred list # show catalog summary and categories + pred list matching # search names and aliases + pred list --category graph # list graph problems + pred list --all # list every problem compactly + pred list --rules --all # list every reduction rule pred list -o problems.json # save as JSON")] List { + /// Case-insensitive substring to search in names and aliases + query: Option, + /// List reduction rules instead of problem types #[arg(long)] rules: bool, + + /// Restrict problems to a model category such as graph, set, or misc + #[arg(long, conflicts_with = "rules")] + category: Option, + + /// List the complete catalog instead of the summary + #[arg(long)] + all: bool, + + /// Include per-variant complexity, rule counts, or rule parameter contracts + #[arg(long)] + verbose: bool, }, /// Show details for a problem type or variant (fields, reductions, complexity) @@ -65,7 +115,7 @@ Examples: Examples: pred show MIS # all variants for MIS pred show MIS/UnitDiskGraph # specific variant - pred show MIS/UnitDiskGraph/i32 # fully qualified variant + pred show MIS/UnitDiskGraph/i64 # fully qualified variant pred show KSAT/K3 # KSatisfiability with K=3 Use `pred list` to see all available problem types and variants.")] @@ -109,14 +159,14 @@ Use `pred to ` for incoming neighbors (what reduces to this).")] hops: usize, }, - /// Find the cheapest reduction path between two problems + /// Find reduction paths between two problems #[command(after_help = "\ Examples: - pred path MIS QUBO # cheapest path - pred path MIS QUBO --all # all paths - pred path MIS QUBO -o path.json # save for `pred reduce --via` - pred path MIS QUBO --all -o paths/ # save all paths to a folder - pred path MIS QUBO --cost minimize:num_variables + pred path MIS QUBO # inspect reduction paths + pred path MIS Clique mis.json # execute paths on an instance + pred path MIS QUBO --limit 50 # inspect the first 50 paths + pred path MIS QUBO --limit all # inspect up to 999 paths + pred path MIS QUBO -o paths.json # save the path set Use `pred list` to see available problems.")] Path { @@ -126,15 +176,15 @@ Use `pred list` to see available problems.")] /// Target problem (e.g., QUBO) #[arg(value_parser = crate::problem_name::ProblemNameParser)] target: String, - /// Cost function [default: minimize-steps] - #[arg(long, default_value = "minimize-steps")] - cost: String, - /// Show all paths instead of just the cheapest - #[arg(long)] - all: bool, - /// Maximum paths to return in --all mode - #[arg(long, default_value_t = 20)] - max_paths: usize, + /// Number of paths to inspect (1-999, or "all" as an alias for 999) + #[arg( + long, + default_value = "20", + value_parser = crate::commands::graph::parse_path_limit + )] + limit: usize, + /// Source problem instance JSON. When present, execute every returned path and measure each constructed problem. + instance: Option, }, /// Export the reduction graph to JSON @@ -162,9 +212,9 @@ Examples: /// Extract a source-space solution from a reduction bundle and a target-space config #[command(after_help = "\ Examples: - pred extract bundle.json --config 1,0,1,0 - pred extract bundle.json --config 1,0,1,0 -o source.json - cat bundle.json | pred extract - --config 1,0,1,0 + pred extract bundle.json --config '[1,0,1,0]' + pred extract bundle.json --config '[1,0,1,0]' -o source.json + cat bundle.json | pred extract - --config '[1,0,1,0]' Use this when an external solver has solved the bundle's target problem (e.g. a QUBO sampler, a neutral-atom platform, a QAOA runtime) and you want @@ -172,7 +222,7 @@ the corresponding solution in the original source problem space without having to shell back into `pred solve`. Input: a reduction bundle JSON (from `pred reduce`). Use - to read from stdin. ---config is the target-space configuration (comma-separated, e.g. 1,0,1,0).")] +--config is the target problem's solution encoded as JSON (e.g. '[1,0,1,0]').")] Extract(ExtractArgs), /// Start MCP (Model Context Protocol) server for AI assistant integration #[cfg(feature = "mcp")] @@ -227,1008 +277,17 @@ pub enum ExampleSide { #[derive(clap::Args)] #[command(after_help = "\ -TIP: Run `pred create ` (no other flags) to see problem-specific help. - Not every flag applies to every problem — the above list shows ALL flags. - -Flags by problem type: - MIS, MVC, MaxClique, MinDomSet --graph, --weights - MaxCut, MaxMatching, TSP, BottleneckTravelingSalesman --graph, --edge-weights - LongestPath --graph, --edge-lengths, --source-vertex, --target-vertex - HamiltonianPathBetweenTwoVertices --graph, --source-vertex, --target-vertex - ShortestWeightConstrainedPath --graph, --edge-lengths, --edge-weights, --source-vertex, --target-vertex, --weight-bound - GraphPartitioning --graph, --num-partitions - MaximalIS --graph, --weights - SAT, NAESAT --num-vars, --clauses - KSAT --num-vars, --clauses [--k] - NonTautology --num-vars, --disjuncts - QUBO --matrix - SpinGlass --graph, --couplings, --fields - KColoring --graph, --k - KClique --graph, --k - DecisionMinimumVertexCover --graph, --weights, --bound - MinimumMultiwayCut --graph, --terminals, --edge-weights - MonochromaticTriangle --graph - PartitionIntoTriangles --graph - GeneralizedHex --graph, --source, --sink - IntegralFlowWithMultipliers --arcs, --capacities, --source, --sink, --multipliers, --requirement - MinimumEdgeCostFlow --arcs, --edge-weights (prices), --capacities, --source, --sink, --requirement - MinimumCostMaximumFlow --arcs, --capacities, --costs, --source, --sink - MinimumCostCirculation, MCC --arcs, --capacities, --costs - MinimumCutIntoBoundedSets --graph, --edge-weights, --source, --sink, --size-bound - HamiltonianCircuit, HC --graph - MaximumLeafSpanningTree --graph - LongestCircuit --graph, --edge-weights - BoundedComponentSpanningForest --graph, --weights, --k, --max-weight - UndirectedFlowLowerBounds --graph, --capacities, --lower-bounds, --source, --sink, --requirement - IntegralFlowBundles --arcs, --bundles, --bundle-capacities, --source, --sink, --requirement [--num-vertices] - UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 - DisjointConnectingPaths --graph, --terminal-pairs - IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs - IsomorphicSpanningTree --graph, --tree - KthBestSpanningTree --graph, --edge-weights, --k, --bound - LengthBoundedDisjointPaths --graph, --source, --sink, --max-length - PathConstrainedNetworkFlow --arcs, --capacities, --source, --sink, --paths, --requirement - Factoring --target, --m, --n - BinPacking --sizes, --capacity - Clustering --distance-matrix, --k, --diameter-bound - CapacityAssignment --capacities, --cost-matrix, --delay-matrix, --cost-budget, --delay-budget - ProductionPlanning --num-periods, --demands, --capacities, --setup-costs, --production-costs, --inventory-costs, --cost-bound - SubsetProduct --sizes, --target - SubsetSum --sizes, --target - MinimumAxiomSet --n, --true-sentences, --implications - Numerical3DimensionalMatching --w-sizes, --x-sizes, --y-sizes, --bound - Betweenness --n, --sets (triples a,b,c) - CyclicOrdering --n, --sets (triples a,b,c) - ThreePartition --sizes, --bound - DynamicStorageAllocation --release-times, --deadlines, --sizes, --capacity - KthLargestMTuple --sets, --k, --bound - QuadraticCongruences --coeff-a, --coeff-b, --coeff-c - QuadraticDiophantineEquations --coeff-a, --coeff-b, --coeff-c - SimultaneousIncongruences --pairs (semicolon-separated a,b pairs) - SumOfSquaresPartition --sizes, --num-groups - ExpectedRetrievalCost --probabilities, --num-sectors - PaintShop --sequence - MaximumSetPacking --subsets [--weights] - MinimumHittingSet --universe-size, --subsets - MinimumSetCovering --universe-size, --subsets [--weights] - EnsembleComputation --universe-size, --subsets, --budget - ComparativeContainment --universe-size, --r-sets, --s-sets [--r-weights] [--s-weights] - X3C (ExactCoverBy3Sets) --universe-size, --subsets (3 elements each) - 3DM (ThreeDimensionalMatching) --universe-size, --subsets (triples w,x,y) - ThreeMatroidIntersection --universe-size, --partitions, --bound - SetBasis --universe-size, --subsets, --k - MinimumCardinalityKey --num-attributes, --dependencies - PrimeAttributeName --universe-size, --dependencies, --query-attribute - RootedTreeStorageAssignment --universe-size, --subsets, --bound - TwoDimensionalConsecutiveSets --alphabet-size, --subsets - BicliqueCover --left, --right, --biedges, --k - BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k - BiconnectivityAugmentation --graph, --potential-weights, --budget [--num-vertices] - PartialFeedbackEdgeSet --graph, --budget, --max-cycle-length [--num-vertices] - BMF --matrix (0/1), --rank - ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k - ConsecutiveOnesMatrixAugmentation --matrix (0/1), --bound - ConsecutiveOnesSubmatrix --matrix (0/1), --k - SparseMatrixCompression --matrix (0/1), --bound - MaximumLikelihoodRanking --matrix (i32 rows, semicolon-separated) - MinimumMatrixCover --matrix (i64 rows, semicolon-separated) - MinimumWeightDecoding --matrix (JSON 2D bool), --rhs (comma-separated booleans) - FeasibleBasisExtension --matrix (JSON 2D i64), --rhs, --required-columns - SteinerTree --graph, --edge-weights, --terminals - MultipleCopyFileAllocation --graph, --usage, --storage - AcyclicPartition --arcs [--weights] [--arc-weights] --weight-bound --cost-bound [--num-vertices] - CVP --basis, --target-vec [--bounds] - MultiprocessorScheduling --lengths, --num-processors, --deadline - SchedulingToMinimizeWeightedCompletionTime --lengths, --weights, --num-processors - SequencingWithinIntervals --release-times, --deadlines, --lengths - OptimalLinearArrangement --graph - RootedTreeArrangement --graph, --bound - MinMaxMulticenter (pCenter) --graph, --weights, --edge-weights, --k - MixedChinesePostman (MCPP) --graph, --arcs, --edge-weights, --arc-weights [--num-vertices] - RuralPostman (RPP) --graph, --edge-weights, --required-edges - StackerCrane --arcs, --graph, --arc-lengths, --edge-lengths [--num-vertices] - MultipleChoiceBranching --arcs [--weights] --partition --threshold [--num-vertices] - AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys] - ConsistencyOfDatabaseFrequencyTables --num-objects, --attribute-domains, --frequency-tables [--known-values] - SubgraphIsomorphism --graph (host), --pattern (pattern) - GroupingBySwapping --string, --bound [--alphabet-size] - LCS --strings [--alphabet-size] - ClosestString --alphabet-size, --strings - ClosestSubstring --alphabet-size, --strings, --substring-length - FAS --arcs [--weights] [--num-vertices] - FVS --arcs [--weights] [--num-vertices] - QBF --num-vars, --clauses, --quantifiers - SteinerTreeInGraphs --graph, --edge-weights, --terminals - PartitionIntoPathsOfLength2 --graph - ResourceConstrainedScheduling --num-processors, --resource-bounds, --resource-requirements, --deadline - IntegerKnapsack --sizes, --values, --capacity - PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences - QAP --matrix (cost), --distance-matrix - StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices] - JobShopScheduling --jobs [--num-processors] - FlowShopScheduling --task-lengths, --deadline [--num-processors] - StaffScheduling --schedules, --requirements, --num-workers, --k - TimetableDesign --num-periods, --num-craftsmen, --num-tasks, --craftsman-avail, --task-avail, --requirements - MinimumTardinessSequencing --num-tasks, --deadlines [--precedences] - RectilinearPictureCompression --matrix (0/1), --k - SchedulingWithIndividualDeadlines --num-tasks, --num-processors/--m, --deadlines [--precedences] - SequencingToMinimizeMaximumCumulativeCost --costs [--precedences] - SequencingToMinimizeTardyTaskWeight --lengths, --weights, --deadlines - SequencingToMinimizeWeightedCompletionTime --lengths, --weights [--precedences] - SequencingToMinimizeWeightedTardiness --lengths, --weights, --deadlines, --bound - SequencingWithDeadlinesAndSetUpTimes --lengths, --deadlines, --compilers, --setup-times - MinimumExternalMacroDataCompression --string, --pointer-cost [--alphabet-size] - MinimumInternalMacroDataCompression --string, --pointer-cost [--alphabet-size] - SCS --strings [--alphabet-size] - StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size] - D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 - MinimumDummyActivitiesPert --arcs [--num-vertices] - FeasibleRegisterAssignment --arcs, --assignment, --k [--num-vertices] - MinimumFaultDetectionTestSet --arcs, --inputs, --outputs [--num-vertices] - MinimumWeightAndOrGraph --arcs, --source, --gate-types, --weights [--num-vertices] - MinimumCodeGenerationOneRegister --arcs [--num-vertices] - MinimumCodeGenerationParallelAssignments --num-variables, --assignments - MinimumCodeGenerationUnlimitedRegisters --left-arcs, --right-arcs [--num-vertices] - MinimumRegisterSufficiencyForLoops --loop-length, --loop-variables - RegisterSufficiency --arcs, --bound [--num-vertices] - CBQ --domain-size, --relations, --conjuncts-spec - IntegerExpressionMembership --expression (JSON), --target - MinimumGeometricConnectedDominatingSet --positions (float x,y pairs), --radius - MinimumDecisionTree --test-matrix (JSON 2D bool), --num-objects, --num-tests - MinimumDisjunctiveNormalForm (MinDNF) --num-vars, --truth-table - SquareTiling (WangTiling) --num-colors, --tiles, --grid-size - ILP, CircuitSAT (via reduction only) - -Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): - KingsSubgraph, TriangularSubgraph --positions (integer x,y pairs) - UnitDiskGraph --positions (float x,y pairs) [--radius] - -Random generation: - --random --num-vertices N [--edge-prob 0.5] [--seed 42] - Examples: - pred create --example MIS/SimpleGraph/i32 - pred create --example MVC/SimpleGraph/i32 --to MIS/SimpleGraph/i32 - pred create --example MVC/SimpleGraph/i32 --to MIS/SimpleGraph/i32 --example-side target - pred create MIS --graph 0-1,1-2,2-3 --weights 1,1,1 - pred create SAT --num-vars 3 --clauses \"1,2;-1,3\" - pred create NonTautology --num-vars 3 --disjuncts \"1,2,3;-1,-2,-3\" - pred create QUBO --matrix \"1,0.5;0.5,2\" - pred create CapacityAssignment --capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12 - pred create ProductionPlanning --num-periods 6 --demands 5,3,7,2,8,5 --capacities 12,12,12,12,12,12 --setup-costs 10,10,10,10,10,10 --production-costs 1,1,1,1,1,1 --inventory-costs 1,1,1,1,1,1 --cost-bound 80 - pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5 - pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2 - pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10 - pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 | pred solve - --solver brute-force - pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force - pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\" - pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5 - pred create MIS --random --num-vertices 10 --edge-prob 0.3 - pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10 - pred create SchedulingToMinimizeWeightedCompletionTime --lengths 1,2,3,4,5 --weights 6,4,3,2,1 --num-processors 2 - pred create UndirectedFlowLowerBounds --graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3 - pred create ConsistencyOfDatabaseFrequencyTables --num-objects 6 --attribute-domains \"2,3,2\" --frequency-tables \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\" --known-values \"0,0,0;3,0,1;1,2,1\" - pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-weights 0-2:3,0-3:4,1-3:2 --budget 5 - pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1 - pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6 - pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 - pred create IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\" - pred create X3C --universe 9 --subsets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\" - pred create SetBasis --universe 4 --subsets \"0,1;1,2;0,2;0,1,2\" --k 3 - pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" - pred create PrimeAttributeName --universe 6 --dependencies \"0,1>2,3,4,5;2,3>0,1,4,5\" --query-attribute 3 - pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --subsets \"0,1,2;3,4,5;1,3;2,4;0,5\"")] -pub struct CreateArgs { - /// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example. - #[arg(value_parser = crate::problem_name::ProblemNameParser)] - pub problem: Option, - /// Build a problem from the canonical example database using a structural problem spec. - #[arg(long, value_parser = crate::problem_name::ProblemNameParser)] - pub example: Option, - /// Target problem spec for canonical rule example lookup. - #[arg(long = "to", value_parser = crate::problem_name::ProblemNameParser)] - pub example_target: Option, - /// Which side of a rule example to emit [default: source]. - #[arg(long, value_enum, default_value = "source")] - pub example_side: ExampleSide, - /// Graph edge list (e.g., 0-1,1-2,2-3) - #[arg(long)] - pub graph: Option, - /// Vertex weights (e.g., 1,1,1,1) [default: all 1s] - #[arg(long)] - pub weights: Option, - /// Edge weights (e.g., 2,3,1) [default: all 1s] - #[arg(long)] - pub edge_weights: Option, - /// Edge lengths (e.g., 2,3,1) [default: all 1s] - #[arg(long)] - pub edge_lengths: Option, - /// Capacities (edge capacities for flow problems, capacity levels for CapacityAssignment) - #[arg(long)] - pub capacities: Option, - /// Demands for ProductionPlanning (comma-separated, e.g., "5,3,7,2,8,5") - #[arg(long)] - pub demands: Option, - /// Setup costs for ProductionPlanning (comma-separated, e.g., "10,10,10,10,10,10") - #[arg(long)] - pub setup_costs: Option, - /// Per-unit production costs for ProductionPlanning (comma-separated, e.g., "1,1,1,1,1,1") - #[arg(long)] - pub production_costs: Option, - /// Per-unit inventory costs for ProductionPlanning (comma-separated, e.g., "1,1,1,1,1,1") - #[arg(long)] - pub inventory_costs: Option, - /// Bundle capacities for IntegralFlowBundles (e.g., 1,1,1) - #[arg(long)] - pub bundle_capacities: Option, - /// Cost matrix for CapacityAssignment (semicolon-separated rows, e.g., "1,3,6;2,4,7") - #[arg(long)] - pub cost_matrix: Option, - /// Delay matrix for CapacityAssignment (semicolon-separated rows, e.g., "8,4,1;7,3,1") - #[arg(long)] - pub delay_matrix: Option, - /// Edge lower bounds for lower-bounded flow problems (e.g., 1,1,0,0,1,0,1) - #[arg(long)] - pub lower_bounds: Option, - /// Vertex multipliers in vertex order (e.g., 1,2,3,1) - #[arg(long)] - pub multipliers: Option, - /// Source vertex for path-based graph problems and MinimumCutIntoBoundedSets - #[arg(long)] - pub source: Option, - /// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets - #[arg(long)] - pub sink: Option, - /// Required total flow R for IntegralFlowBundles, IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, PathConstrainedNetworkFlow, and UndirectedFlowLowerBounds - #[arg(long)] - pub requirement: Option, - /// Required number of paths for LengthBoundedDisjointPaths - #[arg(long)] - pub num_paths_required: Option, - /// Prescribed directed s-t paths as semicolon-separated arc-index sequences (e.g., "0,2,5;1,4,6") - #[arg(long)] - pub paths: Option, - /// Pairwise couplings J_ij for SpinGlass (e.g., 1,-1,1) [default: all 1s] - #[arg(long)] - pub couplings: Option, - /// On-site fields h_i for SpinGlass (e.g., 0,0,1) [default: all 0s] - #[arg(long)] - pub fields: Option, - /// Clauses for SAT problems (semicolon-separated, e.g., "1,2;-1,3") - #[arg(long)] - pub clauses: Option, - /// Disjuncts for NonTautology (semicolon-separated, e.g., "1,2;-1,3") - #[arg(long)] - pub disjuncts: Option, - /// Number of variables (for SAT/KSAT) - #[arg(long)] - pub num_vars: Option, - /// Matrix input. QUBO uses semicolon-separated numeric rows ("1,0.5;0.5,2"); - /// ConsecutiveBlockMinimization uses a JSON 2D bool array ('[[true,false],[false,true]]') - #[arg(long)] - pub matrix: Option, - /// Shared integer parameter (use `pred create ` for the problem-specific meaning) - #[arg(long)] - pub k: Option, - /// Number of partitions for GraphPartitioning (currently must be 2) - #[arg(long)] - pub num_partitions: Option, - /// Generate a random instance (graph-based problems only) - #[arg(long)] - pub random: bool, - /// Number of vertices for random graph generation - #[arg(long)] - pub num_vertices: Option, - /// Source vertex for path problems - #[arg(long)] - pub source_vertex: Option, - /// Target vertex for path problems - #[arg(long)] - pub target_vertex: Option, - /// Edge probability for random graph generation (0.0 to 1.0) [default: 0.5] - #[arg(long)] - pub edge_prob: Option, - /// Random seed for reproducibility - #[arg(long)] - pub seed: Option, - /// Target value (for Factoring, SubsetSum, and SubsetProduct) - #[arg(long)] - pub target: Option, - /// Bits for first factor (for Factoring); also accepted as a processor-count alias for scheduling create commands - #[arg(long)] - pub m: Option, - /// Bits for second factor (for Factoring) - #[arg(long)] - pub n: Option, - /// Vertex positions for geometry-based graphs (semicolon-separated x,y pairs, e.g., "0,0;1,0;1,1") - #[arg(long)] - pub positions: Option, - /// Radius for UnitDiskGraph [default: 1.0] - #[arg(long)] - pub radius: Option, - /// Source vertex s_1 for commodity 1 - #[arg(long)] - pub source_1: Option, - /// Sink vertex t_1 for commodity 1 - #[arg(long)] - pub sink_1: Option, - /// Source vertex s_2 for commodity 2 - #[arg(long)] - pub source_2: Option, - /// Sink vertex t_2 for commodity 2 - #[arg(long)] - pub sink_2: Option, - /// Required flow R_1 for commodity 1 - #[arg(long)] - pub requirement_1: Option, - /// Required flow R_2 for commodity 2 - #[arg(long)] - pub requirement_2: Option, - /// Item sizes for BinPacking (comma-separated, e.g., "3,3,2,2") - #[arg(long)] - pub sizes: Option, - /// Record access probabilities for ExpectedRetrievalCost (comma-separated, e.g., "0.2,0.15,0.15,0.2,0.1,0.2") - #[arg(long)] - pub probabilities: Option, - /// Link lengths for MinimumDiscretePlanarInverseKinematics (comma-separated positive reals, e.g., "2.0,1.0") - #[arg(long)] - pub link_lengths: Option, - /// Target point (x,y) for MinimumDiscretePlanarInverseKinematics (e.g., "2.0,1.0") - #[arg(long)] - pub target_point: Option, - /// Sampled absolute orientations per link for MinimumDiscretePlanarInverseKinematics (semicolon-separated angle lists, e.g., "0.0,1.5707963267948966;0.0,1.5707963267948966") - #[arg(long)] - pub orientation_samples: Option, - /// Admissible (a_{j-1}, a_j) pair sets per junction for MinimumDiscretePlanarInverseKinematics (pipe-separated junctions, each comma-separated "i-j" pairs, e.g., "0-0,0-1,1-1") - #[arg(long)] - pub allowed_pairs: Option, - /// Source labelled digraph G1 for MaximumCommonEdgeSubgraph. Format: ":,,..." with each arc "-