Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ impl ReduceTo<Target> for Source { ... }
- 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. Composition preserves independent fields and their accuracy. An unavailable dependency or unsafe upper-bound substitution makes only the affected field 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.
- Rules producing `ILP<i64>` must declare known finite variable domains with `ILP::with_variables`; constraint rows alone do not supply bounds to binary encoding. Bounds on auxiliary variables must preserve feasibility and the optimum. Document genuinely unbounded variables rather than inventing a cutoff.
- Rules producing ILP must target the most specific applicable registered variant: `ILP<bool>` for binary variables, `ILP<i64, i64, Bounded>` for explicit finite integer domains, and general `ILP<i64>` otherwise. Supply finite domains with `ILP::with_variables`; constraint rows alone do not supply bounds to binary encoding. Bounds on auxiliary variables must preserve feasibility and the optimum. The independent `bounds` dimension defaults to `general`; register additional concrete variants only when a reduction needs them.
- `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)"`)
Expand Down
70 changes: 38 additions & 32 deletions docs/paper/reductions.typ

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion docs/src/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,12 @@ nonzeros. If a rule predicts those dimensions by source expressions `f` and `g`,
explicitly declare `num_nonzeros <= f * g`. Such structural bounds remain valid when
coefficients cancel; exact sparsity can still require additional source information.

Prefer coarse, sound bounds that use existing source parameters. Before adding a
parameter, check whether an equivalent normalization can remove irrelevant input
magnitudes. New parameters must describe intrinsic source data independently of
any reduction, and their propagation must be audited on incoming rules. Keep
model-specific definitions and rule-specific formulas beside their implementations.

`ReductionParameterDeclarations::fields` stores `(name, relation, expression)` triples.
Use `ParameterTransform::relation(field)` to inspect a formula's accuracy and
`unavailable(field)` for a composition failure and its upstream cause. The uniform
Expand Down Expand Up @@ -528,7 +534,7 @@ 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, terminating at the native `ILP<V, C>` with `bool`/`i64` variables and `i64`/`f64` coefficients. `HighsAdapter` owns numerical conversion, backend settings, termination status, and returned-assignment validation. Integer terminals go directly to the adapter; the explicit integer-to-float reduction remains available but is not part of solver pipelines. Optimality and infeasibility follow HiGHS numerical tolerances; the adapter does not provide exact proofs. |
| **ILPSolver** | Executes a problem's registered ILP pipeline, terminating at the native `ILP<V, C, B>` with `bool`/`i64` variables and `i64`/`f64` coefficients. `HighsAdapter` owns numerical conversion, backend settings, termination status, and returned-assignment validation. Integer terminals go directly to the adapter; the explicit integer-to-float reduction remains available but is not part of solver pipelines. Optimality and infeasibility follow HiGHS numerical tolerances; the adapter does not provide exact proofs. |

ILP results are optimal or infeasible according to HiGHS numerical tolerances;
zero MIP gaps do not imply mathematical exactness. Integer extraction rounds
Expand Down
6 changes: 4 additions & 2 deletions examples/chained_reduction_factoring_to_spinglass.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use problemreductions::models::algebraic::Bounded;
// # Chained Reduction: Factoring -> SpinGlass
//
// Mirrors Julia's examples/Ising.jl — reduces a Factoring problem
Expand Down Expand Up @@ -43,9 +44,10 @@ pub fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
// ANCHOR_END: step2

// ANCHOR: step3
// Factoring reduces to ILP<i64>, so we manually reduce, solve, and extract
// Factoring reduces to ILP<i64, i64, Bounded>, so we manually reduce, solve, and extract
let solver = ILPSolver::new();
let reduction = ReduceTo::<ILP<i64>>::reduce_to(&factoring).expect("reduction should succeed");
let reduction = ReduceTo::<ILP<i64, i64, Bounded>>::reduce_to(&factoring)
.expect("reduction should succeed");
let ilp_solution = solver.solve(reduction.target_problem()).unwrap();
let solution = reduction.extract_solution(&ilp_solution).unwrap();
// ANCHOR_END: step3
Expand Down
29 changes: 28 additions & 1 deletion problemreductions-cli/src/create_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,28 @@ fn invalid_problem_spec(command: &Command, message: String) -> Error {
}

fn canonical_problem_spec(problem: &ProblemType, variant: &BTreeMap<String, String>) -> String {
let values = problem
let mut values = problem
.dimensions
.iter()
.filter_map(|dimension| {
let value = dimension_value(variant, dimension.key, dimension.default_value);
(value != dimension.default_value).then_some(value)
})
.collect::<Vec<_>>();
if values.iter().any(|value| {
problem
.dimensions
.iter()
.filter(|dimension| dimension.allowed_values.contains(value))
.count()
> 1
}) {
values = problem
.dimensions
.iter()
.map(|dimension| dimension_value(variant, dimension.key, dimension.default_value))
.collect();
}
join_spec(problem.canonical_name, &values)
}

Expand Down Expand Up @@ -284,6 +298,19 @@ fn add_value_parser(arg: Arg, kind: crate::commands::create::InputValueKind) ->

#[cfg(test)]
mod tests {
#[test]
fn canonical_create_specs_resolve_to_the_original_variant() {
let graph = problemreductions::rules::ReductionGraph::new();
for entry in problemreductions::registry::variant_entries() {
let problem = problemreductions::registry::find_problem_type(entry.name).unwrap();
let variant = entry.variant_map();
let spec = super::canonical_problem_spec(&problem, &variant);
let resolved = crate::problem_name::resolve_problem_ref(&spec, &graph)
.unwrap_or_else(|error| panic!("{spec}: {error}"));
assert_eq!(resolved.variant, variant, "{spec}");
}
}

#[test]
fn decision_create_help_includes_field_descriptions_and_bound_direction() {
for (spec, direction) in [("DecisionMaxCut", ">="), ("DecisionQUBO", "<=")] {
Expand Down
2 changes: 1 addition & 1 deletion problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ mod tests {
let route = crate::commands::reduce::parse_path_json(
r#"{"path":[{
"from":{"name":"ExpectedRetrievalCost","variant":{}},
"to":{"name":"ILP","variant":{"coefficient":"f64","variable":"bool"}}
"to":{"name":"ILP","variant":{"coefficient":"f64","variable":"bool","bounds":"general"}}
}]}"#,
)
.unwrap();
Expand Down
37 changes: 29 additions & 8 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,19 +156,21 @@ fn resolve_variant_updates(

let problem = problemreductions::registry::find_problem_type(&spec.name)
.expect("registered problem has a schema");
if spec.variant_values.len() == problem.dimensions.len()
if spec.variant_values.len() <= problem.dimensions.len()
&& problem
.dimensions
.iter()
.zip(&spec.variant_values)
.all(|(dimension, value)| dimension.allowed_values.contains(&value.as_str()))
{
let resolved = problem
.dimensions
.iter()
.zip(&spec.variant_values)
.map(|(dimension, value)| (dimension.key.to_string(), value.clone()))
.collect();
let mut resolved = default_variant.clone();
resolved.extend(
problem
.dimensions
.iter()
.zip(&spec.variant_values)
.map(|(dimension, value)| (dimension.key.to_string(), value.clone())),
);
anyhow::ensure!(
known_variants.contains(&resolved),
"Resolved variant {} is not declared for {}",
Expand Down Expand Up @@ -406,10 +408,29 @@ mod tests {
let resolved = resolve_problem_ref("ILP/bool/i64", &graph).unwrap();
assert_eq!(resolved.variant["variable"], "bool");
assert_eq!(resolved.variant["coefficient"], "i64");
assert_eq!(resolved.variant["bounds"], "general");
assert_eq!(
crate::commands::graph::variant_to_full_slash("ILP", &resolved.variant),
"/bool/i64"
"/bool/i64/general"
);
}

#[test]
fn ilp_bounds_default_to_general_without_registering_unused_combinations() {
let graph = problemreductions::rules::ReductionGraph::new();
for spec in ["ILP/i64", "ILP/i64/i64", "ILP/variable=i64"] {
let resolved = resolve_problem_ref(spec, &graph).unwrap();
assert_eq!(resolved.variant["variable"], "i64");
assert_eq!(resolved.variant["bounds"], "general");
}
assert_eq!(
resolve_problem_ref("ILP/i64/i64/bounded", &graph)
.unwrap()
.variant["bounds"],
"bounded"
);
assert!(resolve_problem_ref("ILP/i64/f64/bounded", &graph).is_err());
assert!(resolve_problem_ref("ILP/bool/i64/bounded", &graph).is_err());
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions problemreductions-cli/src/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,10 @@ problemreductions::inventory::submit! {

fields: vec![],
unavailable: vec![
problemreductions::rules::registry::UnavailableParameterField {
field: "max_constraint_magnitude_bits",
reason: "the synthetic aggregate-to-ILP reduction has no parameter model",
},
problemreductions::rules::registry::UnavailableParameterField {
field: "num_vars",
reason: "the synthetic aggregate-to-ILP reduction has no parameter model",
Expand Down
43 changes: 30 additions & 13 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3588,7 +3588,7 @@ fn test_solve_bundle_ilp() {
}

#[test]
fn test_solve_direct_ilp_i64_problem() {
fn test_solve_direct_bounded_integer_ilp_problem() {
let problem_file = std::env::temp_dir().join("pred_test_solve_ilp_i64_problem.json");

let create_out = pred()
Expand All @@ -3599,7 +3599,7 @@ fn test_solve_direct_ilp_i64_problem() {
"--example",
"SequencingToMinimizeWeightedCompletionTime",
"--to",
"ILP/variable=i64",
"ILP/variable=i64/bounds=bounded",
"--example-side",
"target",
])
Expand Down Expand Up @@ -5603,8 +5603,8 @@ fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() {
let output = pred()
.args([
"path",
"HighlyConnectedDeletion",
"ILP/bool",
"IntegralFlowWithMultipliers",
"ILP/i64/i64/bounded",
"--limit",
"1",
"--json",
Expand All @@ -5626,11 +5626,20 @@ fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() {
})
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(relations["num_constraints"], "exact");
assert_eq!(relations["num_vars"], "unavailable");
assert_eq!(relations["num_vars"], "exact");
assert_eq!(relations["max_constraint_magnitude_bits"], "unavailable");
let unavailable = fields
.iter()
.find(|field| field["relation"] == "unavailable")
.unwrap();
assert!(unavailable["reason"]
.as_str()
.unwrap()
.contains("multipliers"));
}

#[test]
fn test_path_overall_unavailable_reason_explains_unsupported_bound() {
fn test_path_highly_connected_deletion_has_complete_polynomial_predictions() {
let output = pred()
.args(["path", "HighlyConnectedDeletion", "ILP/bool", "--json"])
.output()
Expand All @@ -5644,11 +5653,15 @@ fn test_path_overall_unavailable_reason_explains_unsupported_bound() {
.map(|field| (field["field"].as_str().unwrap(), field))
.collect::<std::collections::BTreeMap<_, _>>();

assert_eq!(fields["num_vars"]["relation"], "unavailable");
assert!(fields["num_vars"]["reason"]
.as_str()
.unwrap()
.contains("variable exponent unsupported"));
assert_eq!(fields.len(), 4);
assert_eq!(fields["num_vars"]["relation"], "exact");
for field in [
"num_constraints",
"num_nonzeros",
"max_constraint_magnitude_bits",
] {
assert_eq!(fields[field]["relation"], "upper_bound");
}
}

#[test]
Expand Down Expand Up @@ -9750,13 +9763,17 @@ fn test_extract_rejects_infeasible_target_even_when_decoded_source_is_feasible()
use serde_json::json;

let source = OpenShopScheduling::new(1, vec![vec![1]]);
let reduction = ReduceTo::<ILP<i64>>::reduce_to(&source).unwrap();
let reduction =
ReduceTo::<ILP<i64, i64, problemreductions::models::algebraic::Bounded>>::reduce_to(
&source,
)
.unwrap();
let bundle = std::env::temp_dir().join(format!(
"pred-extract-target-feasibility-{}.json",
std::process::id()
));
let source_key = json!({"name":"OpenShopScheduling","variant":{}});
let target_variant = json!({"variable":"i64","coefficient":"i64"});
let target_variant = json!({"variable":"i64","coefficient":"i64","bounds":"bounded"});
std::fs::write(
&bundle,
json!({
Expand Down
44 changes: 28 additions & 16 deletions src/example_db/specs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
//! that can be validated against the catalog and reduction registry.

use crate::export::{ProblemSide, RuleExample, SolutionPair};
use crate::models::algebraic::{
Bounded, BoundsPolicy, General, ILPCoefficient, VariableDomain, ILP,
};
use crate::prelude::{Problem, ReduceTo, ReductionResult};
use crate::registry::DynProblem;
use serde::Serialize;
Expand Down Expand Up @@ -70,34 +73,43 @@ where
/// the double `reduce_to()` that would occur with `rule_example_with_witness`.
pub fn rule_example_via_ilp<S, V>(source: S) -> RuleExample
where
S: Problem + Serialize + ReduceTo<crate::models::algebraic::ILP<V>>,
V: crate::models::algebraic::VariableDomain,
<S as ReduceTo<crate::models::algebraic::ILP<V>>>::Result:
ReductionResult<Source = S, Target = crate::models::algebraic::ILP<V>>,
S: Problem + Serialize + ReduceTo<ILP<V>>,
V: VariableDomain,
<S as ReduceTo<ILP<V>>>::Result: ReductionResult<Source = S, Target = ILP<V>>,
S::Solution: Serialize,
{
rule_example_via_typed_ilp::<S, V, i64>(source)
rule_example_via_typed_ilp::<S, V, i64, General>(source)
}

/// Integer ILP example with explicit finite variable domains.
pub fn rule_example_via_bounded_ilp<S>(source: S) -> RuleExample
where
S: Problem + Serialize + ReduceTo<ILP<i64, i64, Bounded>>,
<S as ReduceTo<ILP<i64, i64, Bounded>>>::Result:
ReductionResult<Source = S, Target = ILP<i64, i64, Bounded>>,
S::Solution: Serialize,
{
rule_example_via_typed_ilp::<S, i64, i64, Bounded>(source)
}

/// Float-coefficient counterpart of [`rule_example_via_ilp`].
pub fn rule_example_via_float_ilp<S, V>(source: S) -> RuleExample
where
S: Problem + Serialize + ReduceTo<crate::models::algebraic::ILP<V, f64>>,
V: crate::models::algebraic::VariableDomain,
<S as ReduceTo<crate::models::algebraic::ILP<V, f64>>>::Result:
ReductionResult<Source = S, Target = crate::models::algebraic::ILP<V, f64>>,
S: Problem + Serialize + ReduceTo<ILP<V, f64>>,
V: VariableDomain,
<S as ReduceTo<ILP<V, f64>>>::Result: ReductionResult<Source = S, Target = ILP<V, f64>>,
S::Solution: Serialize,
{
rule_example_via_typed_ilp::<S, V, f64>(source)
rule_example_via_typed_ilp::<S, V, f64, General>(source)
}

fn rule_example_via_typed_ilp<S, V, C>(source: S) -> RuleExample
fn rule_example_via_typed_ilp<S, V, C, B>(source: S) -> RuleExample
where
S: Problem + Serialize + ReduceTo<crate::models::algebraic::ILP<V, C>>,
V: crate::models::algebraic::VariableDomain,
C: crate::models::algebraic::ILPCoefficient + Serialize,
<S as ReduceTo<crate::models::algebraic::ILP<V, C>>>::Result:
ReductionResult<Source = S, Target = crate::models::algebraic::ILP<V, C>>,
S: Problem + Serialize + ReduceTo<ILP<V, C, B>>,
V: VariableDomain,
C: ILPCoefficient + Serialize,
B: BoundsPolicy,
<S as ReduceTo<ILP<V, C, B>>>::Result: ReductionResult<Source = S, Target = ILP<V, C, B>>,
S::Solution: Serialize,
{
use crate::export::SolutionPair;
Expand Down
Loading
Loading