diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 44934dcf8..86e96ad3d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -179,7 +179,7 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `NumericSize` supertrait bundles common numeric bounds (`Clone + Default + PartialOrd + Num + Zero + Bounded + AddAssign + 'static`) ### Parameter Relations -Each reduction declares one rule-level parameter relation using the `Expr` AST in `src/expr.rs`. The `transform` declaration is required: +Each reduction declares explicit per-field parameter relations using the `Expr` AST in `src/expr.rs`. The `transform` declaration is required: ```rust #[reduction(transform = upper_bound { num_vertices = "num_vertices + num_clauses", @@ -189,11 +189,12 @@ 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 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 = exact { ... }` when every formula is an equality and `transform = upper_bound { ... }` when every formula is only an upper bound. For mixed accuracy, use `transform = { exact { ... }, upper_bound { ... }, unavailable { ... } }`. Every formula RHS uses only registered source parameters; derive target relationships and substitute source expressions before declaring them. - 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. +- `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` 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. - `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)"`) @@ -212,7 +213,7 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - 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]` 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 +- `#[reduction]` requires one uniform or mixed `transform` declaration and currently registers witness/config reductions; aggregate-only and Turing edges require manual `ReductionEntry` registration - `Decision

→ P` supports both mappings: compare the exact optimum to the bound, and recover a witness only if it meets the bound. `P → Decision

` is a Turing edge (binary search over decision bound). ### Extension Points @@ -351,3 +352,4 @@ Parameter expressions describe how target problem parameters relate to source pr 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 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 +6. Every new rule's `exact` and `upper_bound` fields must be covered by `src/unit_tests/parameter_formula_validation.rs`: evaluate each formula on a valid source instance and compare it with measured target parameters (equal for `exact`, predicted ≥ measured for `upper_bound`). Provide a canonical example or registered generator so the shared test can exercise the rule; add targeted boundary cases when needed. diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 3097be526..83f4cfecf 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -168,6 +168,8 @@ Add to `src/rules/mod.rs`: Create `src/unit_tests/rules/_.rs`: +Follow the [reduction parameter testing requirement](../../CLAUDE.md#reduction-parameter-relation): the shared test must cover every declared `exact` or `upper_bound` field using a valid source instance. + **Required: closed-loop test** (`test__to__closed_loop`): ```rust // 1. Create source problem instance diff --git a/docs/src/design.md b/docs/src/design.md index 4caa126b6..ebe73dac6 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -423,10 +423,10 @@ All path-finding operates on **exact variant nodes**. Use `ReductionGraph::varia | `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. +Each formula has its own relation: exact equality or upper bound. Composition preserves +exactness when the formula and its required inputs are exact. Bounded inputs require sound +upper-bound substitution. Unavailable fields affect only formulas that depend on them; +independent formulas survive. Concrete-instance measurement remains a separate execution API. **Example:** Finding a path from `MIS{KingsSubgraph, i64}` to `VC{SimpleGraph, i64}`: @@ -450,8 +450,10 @@ The returned `ReductionChain` stores each intermediate reduction and extracts th

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 +Each reduction classifies every target parameter exactly once as exact, upper bound, or +unavailable with a reason. Every formula uses only registered source parameters on its RHS. +Target structural relationships may justify a formula, but source expressions must be +substituted before registration; there is no automatic model-level inference. The `#[reduction]` macro parses every formula into the canonical `Expr` DAG at compile time: ```rust,ignore @@ -467,6 +469,28 @@ unavailable = { impl ReduceTo for Source { ... } ``` +Rules can mix accuracy explicitly while existing uniform declarations remain supported: + +```rust,ignore +#[reduction(transform = { + exact { num_vars = "num_vars" }, + upper_bound { num_quadratic_terms = "num_vars * (num_vars - 1) / 2" }, +})] +impl ReduceTo>> for KSatisfiability { ... } +``` + +Here both RHS expressions refer to the SAT source's `num_vars`. +Likewise, an ILP's canonical constraint matrix has at most variables times constraints +nonzeros. If a rule predicts those dimensions by source expressions `f` and `g`, it can +explicitly declare `num_nonzeros <= f * g`. Such structural bounds remain valid when +coefficients cancel; exact sparsity can still require additional source information. + +`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 +`ParameterTransform::new` constructor remains available; `from_fields` accepts mixed relations. +CLI contract JSON stores `relation` within each formula entry in `fields`. + `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, @@ -485,7 +509,7 @@ first fully expanded and like monomials are combined; terms with non-positive co 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 +propagate symbolic upper bounds and makes that field unavailable, preserving the other fields. Projection to `Growth` is a separate descriptive terminal operation used for Big-O display; it does not rank or filter paths.
diff --git a/problemreductions-cli/src/commands/graph.rs b/problemreductions-cli/src/commands/graph.rs index 465b740a4..53d94812c 100644 --- a/problemreductions-cli/src/commands/graph.rs +++ b/problemreductions-cli/src/commands/graph.rs @@ -2,6 +2,7 @@ use crate::dispatch::{load_problem, read_input, ProblemJson}; use crate::output::OutputConfig; use crate::problem_name::{aliases_for, parse_problem_spec, resolve_problem_ref}; use anyhow::Result; +use problemreductions::parameters::ParameterRelation; use problemreductions::registry::collect_schemas; use problemreductions::registry::ProblemCategory; use problemreductions::rules::{ExecutedPath, ReductionGraph, ReductionPath, TraversalFlow}; @@ -584,13 +585,9 @@ fn strongest_contract_fields( let mut fields = BTreeMap::new(); if let Some(transform) = contract.transform() { for (field, expression) in transform.expressions() { - let relation = match transform.relation() { - problemreductions::parameters::ParameterRelation::Exact => { - StrongestContractRelation::Exact(expression) - } - problemreductions::parameters::ParameterRelation::UpperBound => { - StrongestContractRelation::UpperBound(expression) - } + let relation = match transform.relation(field).expect("declared formula") { + ParameterRelation::Exact => StrongestContractRelation::Exact(expression), + ParameterRelation::UpperBound => StrongestContractRelation::UpperBound(expression), }; fields.insert(field, relation); } @@ -670,11 +667,11 @@ pub(crate) fn parameter_contract_to_json( ) -> serde_json::Value { match contract { Ok(contract) => serde_json::json!({ - "relation": contract.transform().map(|transform| transform.relation()), "fields": contract.transform().map(|transform| transform.expressions().map(|(field, expression)| { serde_json::json!({ "field": field, "formula": expression.to_string(), + "relation": transform.relation(field), "big_o": big_o_of(expression), }) }).collect::>()).unwrap_or_default(), @@ -731,88 +728,50 @@ struct PreparedParameterField { relation: PreparedParameterRelation, } -fn terminal_parameter_contract( - graph: &ReductionGraph, - path: &ReductionPath, -) -> Option { - path.steps - .windows(2) - .last() - .and_then(|pair| { - graph.find_entry( - &pair[0].name, - &pair[0].variant, - &pair[1].name, - &pair[1].variant, - ) - }) - .and_then(|entry| entry.parameter_contract.ok()) -} - fn prepare_overall_parameters( graph: &ReductionGraph, path: &ReductionPath, ) -> Vec { - let Some(target) = path.target() else { + let Some(target) = path.steps.last() else { return Vec::new(); }; let composed = graph.compose_path_parameter_transform(path); - let terminal_contract = terminal_parameter_contract(graph, path); - - graph - .parameter_names(target) - .into_iter() - .map(|field| { - let expression = composed - .as_ref() - .ok() - .and_then(|transform| transform.as_ref()) - .and_then(|transform| { - transform - .get(&field) - .map(|expression| (transform.relation(), expression)) - }); - let relation = if let Some((relation, expression)) = expression { - match relation { - problemreductions::parameters::ParameterRelation::Exact => { - PreparedParameterRelation::Exact(expression.to_string()) - } - problemreductions::parameters::ParameterRelation::UpperBound => { - PreparedParameterRelation::UpperBound(expression.to_string()) + let fields = problemreductions::registry::find_variant_entry(&target.name, &target.variant) + .map(|entry| entry.parameter_names()) + .unwrap_or_default(); + fields + .iter() + .map(|&field| { + let relation = match &composed { + Ok(Some(transform)) => { + if let Some(expression) = transform.get(field) { + match transform.relation(field).expect("declared formula") { + ParameterRelation::Exact => { + PreparedParameterRelation::Exact(expression.to_string()) + } + ParameterRelation::UpperBound => { + PreparedParameterRelation::UpperBound(expression.to_string()) + } + } + } else { + let reason = transform + .unavailable(field) + .map(ToString::to_string) + .unwrap_or_else(|| { + format!("no symbolic parameter relation is registered for target field {field}") + }); + PreparedParameterRelation::Unavailable(reason) } } - } else if let Some(unavailable) = terminal_contract.as_ref().and_then(|contract| { - contract - .unavailable() - .iter() - .find(|unavailable| unavailable.field == field) - }) { - PreparedParameterRelation::Unavailable(unavailable.reason.to_string()) - } else if terminal_contract - .as_ref() - .and_then(|contract| contract.transform()) - .is_some_and(|transform| transform.get(&field).is_some()) - { - PreparedParameterRelation::Unavailable(match &composed { - Err(error) => error.to_string(), - Ok(_) => { - format!( - "no composed parameter relation is available for target field {field}" - ) - } - }) - } else { - let reason = match &composed { - Err(error) => error.to_string(), - Ok(_) => { - format!( - "no symbolic parameter relation is registered for target field {field}" - ) - } - }; - PreparedParameterRelation::Unavailable(reason) + Err(error) => PreparedParameterRelation::Unavailable(error.to_string()), + Ok(None) => PreparedParameterRelation::Unavailable(format!( + "no composed parameter relation is available for target field {field}" + )), }; - PreparedParameterField { field, relation } + PreparedParameterField { + field: field.to_string(), + relation, + } }) .collect() } diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index 7592dc02c..aaaeaf946 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -399,7 +399,7 @@ problemreductions::inventory::submit! { source_variant_fn: AggregateValueSource::variant, target_variant_fn: AggregateValueTarget::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - relation: None, + fields: vec![], unavailable: vec![problemreductions::rules::registry::UnavailableParameterField { field: "num_values", @@ -430,7 +430,7 @@ problemreductions::inventory::submit! { source_variant_fn: AggregateValueSource::variant, target_variant_fn: ILP::::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - relation: None, + fields: vec![], unavailable: vec![ problemreductions::rules::registry::UnavailableParameterField { diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 4f9cd1949..e5641a0a5 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -5553,6 +5553,51 @@ fn test_path_overall_unavailable_is_reported_per_field_without_internal_modes() assert!(overall.get("bound_composition_error").is_none()); } +#[test] +fn test_path_preserves_exact_variables_and_bounded_quadratic_terms() { + for target in ["DecisionQUBO", "QUBO"] { + let output = pred() + .args([ + "path", + "KSatisfiability/K2", + target, + "--limit", + "1", + "--json", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let envelope: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let fields = envelope["paths"][0]["overall_parameters"]["fields"] + .as_array() + .unwrap(); + let relations = fields + .iter() + .map(|field| { + ( + field["field"].as_str().unwrap(), + field["relation"].as_str().unwrap(), + ) + }) + .collect::>(); + // The reduction preserves variables; distinct off-diagonal pairs bound + // quadratic terms even when contributions cancel. + assert_eq!( + relations, + std::collections::BTreeMap::from([ + ("num_vars", "exact"), + ("num_quadratic_terms", "upper_bound"), + ]), + "prediction relations for {target}" + ); + } +} + #[test] fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() { let output = pred() diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index 8d9154c9f..a0493ded6 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -202,7 +202,8 @@ fn option_inner_type(ty: &Type) -> Option<&Type> { /// # Attributes /// /// - `transform = exact { field = expression, ... }` — exact target-parameter equalities -/// - `transform = upper_bound { field = expression, ... }` — one rule-level upper bound +/// - `transform = upper_bound { field = expression, ... }` — uniform upper bounds +/// - `transform = { exact { ... }, upper_bound { ... }, unavailable { ... } }` — per-field relations /// - `transform = unavailable { field = "reason", ... }` — no symbolic parameter transform /// - `unavailable = { field = "reason", ... }` — fields that cannot be propagated /// @@ -350,17 +351,10 @@ fn generate_aggregate_entry(result: &Type) -> TokenStream2 { } } -#[derive(Clone)] -struct ParsedExpressionField { - name: String, - expression: problemreductions_expr::Expr, -} - /// Parsed attributes from #[reduction(...)] struct ReductionAttrs { transform_declared: bool, - relation: Option, - fields: Option>, + fields: Vec<(String, ParameterRelationAttr, String)>, unavailable: Option>, } @@ -374,8 +368,7 @@ impl syn::parse::Parse for ReductionAttrs { fn parse(input: syn::parse::ParseStream) -> syn::Result { let mut attrs = ReductionAttrs { transform_declared: false, - relation: None, - fields: None, + fields: Vec::new(), unavailable: None, }; @@ -392,27 +385,17 @@ impl syn::parse::Parse for ReductionAttrs { )); } attrs.transform_declared = true; - let relation: syn::Ident = input.parse()?; - let content; - syn::braced!(content in input); - match relation.to_string().as_str() { - "exact" => { - attrs.relation = Some(ParameterRelationAttr::Exact); - attrs.fields = Some(parse_expression_fields(&content)?); - } - "upper_bound" => { - attrs.relation = Some(ParameterRelationAttr::UpperBound); - attrs.fields = Some(parse_expression_fields(&content)?); - } - "unavailable" => { - attrs.unavailable = Some(parse_unavailable_fields(&content)?); - } - _ => { - return Err(syn::Error::new( - relation.span(), - "expected `exact`, `upper_bound`, or `unavailable`", - )); + if input.peek(syn::token::Brace) { + let groups; + syn::braced!(groups in input); + while !groups.is_empty() { + attrs.parse_group(&groups)?; + if groups.peek(syn::Token![,]) { + groups.parse::()?; + } } + } else { + attrs.parse_group(input)?; } } "unavailable" => { @@ -450,6 +433,40 @@ impl syn::parse::Parse for ReductionAttrs { } } +impl ReductionAttrs { + fn parse_group(&mut self, input: syn::parse::ParseStream) -> syn::Result<()> { + let relation: syn::Ident = input.parse()?; + let content; + syn::braced!(content in input); + let kind = match relation.to_string().as_str() { + "exact" => ParameterRelationAttr::Exact, + "upper_bound" => ParameterRelationAttr::UpperBound, + "unavailable" => { + if self.unavailable.is_some() { + return Err(syn::Error::new( + relation.span(), + "duplicate `unavailable` declaration", + )); + } + self.unavailable = Some(parse_unavailable_fields(&content)?); + return Ok(()); + } + _ => { + return Err(syn::Error::new( + relation.span(), + "expected `exact`, `upper_bound`, or `unavailable`", + )) + } + }; + self.fields.extend( + parse_expression_fields(&content)? + .into_iter() + .map(|(name, expression)| (name, kind, expression)), + ); + Ok(()) + } +} + fn parse_expression_fields(content: syn::parse::ParseStream) -> syn::Result> { let mut fields = Vec::new(); while !content.is_empty() { @@ -576,37 +593,6 @@ fn make_variant_fn_body(ty: &Type, type_generics: &HashSet) -> syn::Resu Ok(quote! { <#ty as crate::traits::Problem>::variant() }) } -/// Parse one explicit exact or bound field declaration into the canonical expression DAG. -fn parse_expression_fields_to_expr( - fields: &[(String, String)], -) -> syn::Result> { - fields - .iter() - .map(|(name, source)| { - let expression = problemreductions_expr::Expr::try_parse(source).map_err(|error| { - syn::Error::new( - proc_macro2::Span::call_site(), - format!("error parsing parameter expression \"{source}\": {error}"), - ) - })?; - Ok(ParsedExpressionField { - name: name.clone(), - expression, - }) - }) - .collect() -} - -fn generate_expression_fields(fields: &[ParsedExpressionField]) -> TokenStream2 { - let field_tokens = fields.iter().map(|field| { - let expression = expr_tokens(&field.expression); - let name = field.name.as_str(); - quote! { (#name, #expression) } - }); - - quote! { vec![#(#field_tokens),*] } -} - /// Generate the reduction entry code fn generate_reduction_entry( attrs: &ReductionAttrs, @@ -637,17 +623,28 @@ fn generate_reduction_entry( let source_variant_body = make_variant_fn_body(source_type, &type_generics)?; let target_variant_body = make_variant_fn_body(&target_type, &type_generics)?; - let fields = parse_expression_fields_to_expr(attrs.fields.as_deref().unwrap_or_default())?; - let field_tokens = generate_expression_fields(&fields); - let relation_tokens = match attrs.relation { - Some(ParameterRelationAttr::Exact) => { - quote! { Some(crate::parameters::ParameterRelation::Exact) } - } - Some(ParameterRelationAttr::UpperBound) => { - quote! { Some(crate::parameters::ParameterRelation::UpperBound) } - } - None => quote! { None }, - }; + let field_tokens = attrs + .fields + .iter() + .map(|(name, relation, source)| { + let parsed = problemreductions_expr::Expr::try_parse(source).map_err(|error| { + syn::Error::new( + proc_macro2::Span::call_site(), + format!("error parsing parameter expression {source:?}: {error}"), + ) + })?; + let expression = expr_tokens(&parsed); + let relation = match relation { + ParameterRelationAttr::Exact => { + quote! { crate::parameters::ParameterRelation::Exact } + } + ParameterRelationAttr::UpperBound => { + quote! { crate::parameters::ParameterRelation::UpperBound } + } + }; + Ok(quote! { (#name, #relation, #expression) }) + }) + .collect::>>()?; let unavailable_tokens = attrs .unavailable .as_deref() @@ -666,8 +663,7 @@ fn generate_reduction_entry( source_variant_fn: || { #source_variant_body }, target_variant_fn: || { #target_variant_body }, parameter_declarations_fn: || crate::rules::registry::ReductionParameterDeclarations { - relation: #relation_tokens, - fields: #field_tokens, + fields: vec![#(#field_tokens),*], unavailable: vec![#(#unavailable_tokens),*], }, module_path: module_path!(), @@ -1097,10 +1093,10 @@ mod tests { #[test] fn parameters_report_expression_domain_errors() { - let fields = vec![("num_vertices".to_string(), "0 / 0".to_string())]; - let Err(error) = parse_expression_fields_to_expr(&fields) else { - panic!("invalid parameter expression was accepted"); - }; + let attrs: ReductionAttrs = + syn::parse_quote! { transform = exact { num_vertices = "0 / 0" } }; + let implementation: ItemImpl = syn::parse_quote! { impl ReduceTo for Source {} }; + let error = generate_reduction_entry(&attrs, &implementation).unwrap_err(); assert!(error.to_string().contains("division by zero")); } @@ -1423,6 +1419,37 @@ mod tests { assert!(generate_aggregate_impl(&inherent).is_err()); } + #[test] + fn mixed_transform_accepts_independent_relations_and_unavailable_fields() { + let attrs: ReductionAttrs = syn::parse_quote! { + transform = { + exact { n = "n" }, + upper_bound { pairs = "n * (n - 1) / 2" }, + unavailable { bits = "input magnitudes are not registered" }, + } + }; + assert_eq!(attrs.fields[0].1, ParameterRelationAttr::Exact); + assert_eq!(attrs.fields[1].1, ParameterRelationAttr::UpperBound); + assert_eq!(attrs.unavailable.unwrap()[0].0, "bits"); + } + + #[test] + fn mixed_transform_rejects_invalid_relations_and_duplicate_unavailable_groups() { + for declaration in [ + quote! { transform = { approximate { n = "n" } } }, + quote! { transform = { + unavailable { n = "not represented" }, + unavailable { m = "not represented" }, + } }, + quote! { + unavailable = { n = "not represented" }, + transform = unavailable { m = "not represented" }, + }, + ] { + assert!(syn::parse2::(declaration).is_err()); + } + } + #[test] fn reduction_accepts_explicit_transform_attributes() { let attrs: ReductionAttrs = syn::parse_quote! { @@ -1431,12 +1458,19 @@ mod tests { }; assert_eq!( attrs.fields, - Some(vec![ - ("n".to_string(), "n".to_string()), - ("squared".to_string(), "n^2".to_string()), - ]) + vec![ + ( + "n".to_string(), + ParameterRelationAttr::UpperBound, + "n".to_string() + ), + ( + "squared".to_string(), + ParameterRelationAttr::UpperBound, + "n^2".to_string() + ), + ] ); - assert_eq!(attrs.relation, Some(ParameterRelationAttr::UpperBound)); assert_eq!( attrs.unavailable, Some(vec![( diff --git a/src/lib.rs b/src/lib.rs index 93dfbd4b7..afeac7aa1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,6 +135,9 @@ pub use problemreductions_macros::{ // Re-export inventory so `declare_variants!` can use `$crate::inventory::submit!` pub use inventory; +#[cfg(all(test, feature = "example-db"))] +#[path = "unit_tests/parameter_formula_validation.rs"] +mod parameter_formula_validation; #[cfg(all(test, feature = "example-db"))] #[path = "unit_tests/symbolic_parameter_contracts.rs"] mod symbolic_parameter_contracts; diff --git a/src/models/algebraic/qubo.rs b/src/models/algebraic/qubo.rs index 02b6a7153..91e23d51b 100644 --- a/src/models/algebraic/qubo.rs +++ b/src/models/algebraic/qubo.rs @@ -258,6 +258,11 @@ impl QUBO { self.num_vars } + /// Number of nonzero off-diagonal coefficients. + pub fn num_quadratic_terms(&self) -> usize { + self.entries.iter().filter(|(i, j, _)| i != j).count() + } + /// Nonzero upper-triangular coefficients `(i, j, Q[i][j])`, sorted by `(i, j)`. pub fn entries(&self) -> &[(usize, usize, W)] { &self.entries @@ -293,7 +298,10 @@ where type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_vars", num_vars),]; + crate::problem_parameters![ + ("num_vars", num_vars), + ("num_quadratic_terms", num_quadratic_terms), + ]; fn evaluate( &self, diff --git a/src/models/decision.rs b/src/models/decision.rs index ce1f9849d..9bd4ded89 100644 --- a/src/models/decision.rs +++ b/src/models/decision.rs @@ -79,10 +79,10 @@ macro_rules! register_decision_variant { source_variant_fn: <$crate::models::decision::Decision<$inner> as $crate::traits::Problem>::variant, target_variant_fn: <$inner as $crate::traits::Problem>::variant, parameter_declarations_fn: || $crate::rules::registry::ReductionParameterDeclarations { - relation: Some($crate::parameters::ParameterRelation::Exact), + fields: <$inner as $crate::traits::Problem>::parameter_names() .iter() - .map(|&name| (name, $crate::expr::Expr::variable(name))) + .map(|&name| (name, $crate::parameters::ParameterRelation::Exact, $crate::expr::Expr::variable(name))) .collect(), unavailable: vec![], }, @@ -124,10 +124,10 @@ macro_rules! register_decision_variant { source_variant_fn: <$inner as $crate::traits::Problem>::variant, target_variant_fn: <$crate::models::decision::Decision<$inner> as $crate::traits::Problem>::variant, parameter_declarations_fn: || $crate::rules::registry::ReductionParameterDeclarations { - relation: Some($crate::parameters::ParameterRelation::Exact), + fields: <$inner as $crate::traits::Problem>::parameter_names() .iter() - .map(|&name| (name, $crate::expr::Expr::variable(name))) + .map(|&name| (name, $crate::parameters::ParameterRelation::Exact, $crate::expr::Expr::variable(name))) .collect(), unavailable: vec![], }, diff --git a/src/models/graph/hamiltonian_path.rs b/src/models/graph/hamiltonian_path.rs index b31d6d2ff..bf64a90b4 100644 --- a/src/models/graph/hamiltonian_path.rs +++ b/src/models/graph/hamiltonian_path.rs @@ -89,6 +89,11 @@ impl HamiltonianPath { self.graph.num_edges() } + /// Number of consecutive position pairs in a Hamiltonian path. + pub fn num_consecutive_positions(&self) -> usize { + self.num_vertices().saturating_sub(1) + } + /// Check if a configuration is a valid Hamiltonian path. pub fn is_valid_solution(&self, config: &[usize]) -> bool { is_valid_hamiltonian_path(&self.graph, config) @@ -103,7 +108,11 @@ where type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("num_edges", num_edges), + ("num_vertices", num_vertices), + ("num_consecutive_positions", num_consecutive_positions), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 25d4fc572..8e76d423c 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -252,7 +252,7 @@ impl IntegralFlowBundles { Ok(self.evaluate_solution(config)?.0) } - fn arc_upper_bounds(&self) -> Vec { + pub(crate) fn arc_upper_bounds(&self) -> Vec { let mut upper_bounds = vec![i64::MAX; self.num_arcs()]; for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) { for &arc_index in bundle { diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index fa94bc4cf..79a3ab483 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -224,7 +224,7 @@ impl PathConstrainedNetworkFlow { Ok(()) } - fn path_bottleneck(&self, path: &[usize]) -> i64 { + pub(crate) fn path_bottleneck(&self, path: &[usize]) -> i64 { path.iter() .map(|&arc_idx| self.capacities[arc_idx]) .min() diff --git a/src/parameters.rs b/src/parameters.rs index 2cf4ec93e..0ab7a513d 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -8,7 +8,7 @@ use num_traits::{One, Signed, Zero}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; -/// What one reduction rule promises about all of its declared parameter formulas. +/// What a reduction promises about one parameter formula. #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum ParameterRelation { @@ -16,28 +16,19 @@ pub enum ParameterRelation { UpperBound, } -impl ParameterRelation { - fn compose(self, next: Self) -> Self { - if self == Self::Exact && next == Self::Exact { - Self::Exact - } else { - Self::UpperBound - } - } -} - -/// One rule-level symbolic transformation. Its relation applies to every formula. +/// Symbolic predictions with independent accuracy and availability per field. #[derive(Clone, Debug)] pub struct ParameterTransform { edge: Box, - relation: ParameterRelation, fields: Vec, + unavailable: BTreeMap, ParameterTransformError>, } #[derive(Clone, Debug)] struct ParameterField { name: Box, expression: Expr, + relation: ParameterRelation, plan: Plan, } @@ -68,11 +59,28 @@ impl ParameterTransform { where I: IntoIterator, N: Into>, + { + Self::from_fields( + edge, + fields + .into_iter() + .map(|(name, expression)| (name, relation, expression)), + ) + } + + /// Construct predictions whose relations may differ by target field. + pub fn from_fields( + edge: impl Into>, + fields: I, + ) -> Result + where + I: IntoIterator, + N: Into>, { let edge = edge.into(); let mut names = HashSet::new(); let mut raw_fields = Vec::new(); - for (name, expression) in fields { + for (name, relation, expression) in fields { let name = name.into(); if let Err(error) = Symbol::new(name.clone()) { return Err(ParameterTransformError::InvalidTargetField { @@ -84,24 +92,25 @@ impl ParameterTransform { if !names.insert(name.clone()) { return Err(ParameterTransformError::DuplicateTargetField { edge, field: name }); } - raw_fields.push((name, expression)); + raw_fields.push((name, relation, expression)); } let expressions = raw_fields .iter() - .map(|(_, expression)| expression) + .map(|(_, _, expression)| expression) .collect::>(); let analysis = AlgebraicAnalysis::new(&expressions); let mut plans = HashMap::new(); let fields = raw_fields .into_iter() - .map(|(name, expression)| { + .map(|(name, relation, expression)| { let plan = compile(&expression, &analysis, &mut plans).map_err(|failure| { validation_error(edge.clone(), name.clone(), expression.to_string(), failure) })?; Ok(ParameterField { name, expression, + relation, plan, }) }) @@ -109,8 +118,8 @@ impl ParameterTransform { Ok(Self { edge, - relation, fields, + unavailable: BTreeMap::new(), }) } @@ -118,8 +127,27 @@ impl ParameterTransform { &self.edge } - pub fn relation(&self) -> ParameterRelation { - self.relation + pub fn relation(&self, target_field: &str) -> Option { + self.fields + .iter() + .find(|field| field.name.as_ref() == target_field) + .map(|field| field.relation) + } + + /// Why this target field could not be predicted, including upstream causes. + pub fn unavailable(&self, target_field: &str) -> Option<&ParameterTransformError> { + self.unavailable.get(target_field) + } + + pub(crate) fn declare_unavailable(&mut self, field: &str, reason: &str) { + self.unavailable.insert( + field.into(), + ParameterTransformError::Unavailable { + edge: self.edge.clone(), + field: field.into(), + reason: reason.into(), + }, + ); } pub fn expressions(&self) -> impl Iterator { @@ -152,7 +180,7 @@ impl ParameterTransform { value, }); } - let value = if self.relation == ParameterRelation::Exact { + let value = if field.relation == ParameterRelation::Exact { if !value.is_integer() { return Err(ParameterTransformError::NonIntegralResult { edge: self.edge.clone(), @@ -181,11 +209,23 @@ impl ParameterTransform { ) -> Result { let edge = edge.into(); let replacements: HashMap<&str, &Expr> = self.expressions().collect(); - let fields = next - .fields - .iter() - .map(|field| { - let expression = if self.relation == ParameterRelation::UpperBound { + let mut fields = Vec::new(); + let mut unavailable = next.unavailable.clone(); + for field in &next.fields { + let result = (|| { + let mut bounded_input = false; + for input in field.expression.variables() { + if let Some(cause) = self.unavailable(input) { + return Err(ParameterTransformError::UnavailableInput { + edge: next.edge.clone(), + field: field.name.clone(), + input_field: input.into(), + cause: Box::new(cause.clone()), + }); + } + bounded_input |= self.relation(input) == Some(ParameterRelation::UpperBound); + } + let expression = if bounded_input { positive_polynomial_hull(&field.expression).ok_or_else(|| { ParameterTransformError::CannotPropagateUpperBound { edge: next.edge.clone(), @@ -200,14 +240,27 @@ impl ParameterTransform { expression .substitute_complete(&replacements) .map_err(|error| ParameterTransformError::MissingCompositionInput { - edge: edge.clone(), + edge: next.edge.clone(), field: field.name.clone(), input_fields: error.missing_variables().map(Box::::from).collect(), })?; - Ok((field.name.clone(), expression)) - }) - .collect::, ParameterTransformError>>()?; - Self::new(edge, self.relation.compose(next.relation), fields) + let relation = if bounded_input { + ParameterRelation::UpperBound + } else { + field.relation + }; + Ok((field.name.clone(), relation, expression)) + })(); + match result { + Ok(prediction) => fields.push(prediction), + Err(error) => { + unavailable.insert(field.name.clone(), error); + } + } + } + let mut composed = Self::from_fields(edge, fields)?; + composed.unavailable = unavailable; + Ok(composed) } } @@ -477,6 +530,22 @@ fn evaluation_error( /// Validation, composition, or evaluation failure for a [`ParameterTransform`]. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum ParameterTransformError { + #[error("reduction `{edge}` target field `{field}` is unavailable: {reason}")] + Unavailable { + edge: Box, + field: Box, + reason: Box, + }, + #[error( + "reduction `{edge}` target field `{field}` depends on unavailable `{input_field}`: {cause}" + )] + UnavailableInput { + edge: Box, + field: Box, + input_field: Box, + #[source] + cause: Box, + }, #[error("reduction `{edge}` has invalid target parameter field `{field}`: {reason}")] InvalidTargetField { edge: Box, diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index 9baca7955..f042f7b37 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -4,7 +4,7 @@ //! crossing flags y_t, and partition labels used directly as a topological order. //! See the paper entry for the full formulation. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::AcyclicPartition; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; @@ -43,15 +43,15 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionAcyclicPartitionToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices * num_vertices + num_arcs * num_vertices + num_arcs + num_vertices", num_constraints = "num_vertices^2 + 4 * num_vertices + 3 * num_arcs * num_vertices + 2 * num_arcs + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices * num_vertices + num_arcs * num_vertices + num_arcs + num_vertices) * (num_vertices^2 + 4 * num_vertices + 3 * num_arcs * num_vertices + 2 * num_arcs + 1)", + }, +})] impl ReduceTo> for AcyclicPartition { type Result = ReductionAcyclicPartitionToILP; @@ -146,7 +146,9 @@ impl ReduceTo> for AcyclicPartition { constraints.push(LinearConstraint::le(terms, 0)); } - let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + let variables = vec![IntegerVariable::binary(); num_vars]; + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionAcyclicPartitionToILP { target, n }) diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index 5857dfc7e..a14d79e83 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -45,15 +45,11 @@ impl ReductionResult for ReductionBCBSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBCBSToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices", - num_constraints = "num_vertices^2 + 2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices", + num_constraints = "num_vertices^2 + 2", + num_nonzeros = "num_vertices * (num_vertices^2 + 2)", +})] impl ReduceTo> for BalancedCompleteBipartiteSubgraph { type Result = ReductionBCBSToILP; diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 37e43cae5..af0513832 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -75,15 +75,11 @@ impl ReductionResult for ReductionBiconnAugToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)", - num_constraints = "1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices)", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)", + num_constraints = "1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices)", + num_nonzeros = "(num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)) * (1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices))", +})] impl ReduceTo> for BiconnectivityAugmentation { type Result = ReductionBiconnAugToILP; diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index 24d3cc5a8..bef93355a 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -47,15 +47,15 @@ impl ReductionResult for ReductionBPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_items * num_items + num_items", num_constraints = "2 * num_items", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_items * num_items + num_items) * (2 * num_items)", + }, +})] impl ReduceTo> for BinPacking { type Result = ReductionBPToILP; diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs index 8f9790636..867926684 100644 --- a/src/rules/bmf_bicliquecover.rs +++ b/src/rules/bmf_bicliquecover.rs @@ -95,12 +95,12 @@ impl ReductionResult for ReductionBMFToBicliqueCover { #[reduction( transform = exact { num_vertices = "rows + cols", - num_edges = "rows * cols", + left_size = "rows", + right_size = "cols", rank = "rank", }, unavailable = { - left_size = "the exact target parameter is not represented by this reduction's symbolic transform", - right_size = "the exact target parameter is not represented by this reduction's symbolic transform", + num_edges = "the number of true matrix entries is not a registered BMF parameter", } )] impl ReduceTo for BMF { diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 8944c45e5..c69887bb6 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -54,9 +54,7 @@ impl ReductionResult for ReductionBMFToILP { transform = exact { num_vars = "rows * rank + rank * cols + rows * rank * cols + rows * cols", num_constraints = "3 * rows * rank * cols + rank * rows * cols + rows * cols + rows * cols", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "10 * rows * rank * cols + 2 * rows * cols", } )] impl ReduceTo> for BMF { diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 50b2e2146..5c36ad7d8 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -1,6 +1,6 @@ //! Bottleneck TSP to ILP using cyclic positions and a selected maximum edge. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BottleneckTravelingSalesman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -79,15 +79,15 @@ impl ReductionBTSPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2 + 2 * num_edges * num_vertices + num_edges", num_constraints = "num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1", }, - unavailable = { - num_nonzeros = "threshold comparisons depend on the ordering of edge weights", - } -)] + upper_bound { + num_nonzeros = "(num_vertices^2 + 2 * num_edges * num_vertices + num_edges) * (num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1)", + }, +})] impl ReduceTo> for BottleneckTravelingSalesman { type Result = ReductionBTSPToILP; @@ -174,8 +174,11 @@ impl ReduceTo> for BottleneckTravelingSalesman { .enumerate() .map(|(edge, weight)| (q(edge), weight)) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let variables = vec![IntegerVariable::binary(); num_vars]; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionBTSPToILP { target, num_vertices: n, diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index 3a8242872..093f35112 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -4,7 +4,7 @@ //! connectivity inside each used component via flow. //! See the paper entry for the full formulation. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BoundedComponentSpanningForest; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; @@ -45,15 +45,15 @@ impl ReductionResult for ReductionBCSFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBCSFToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "3 * num_vertices * max_components + 2 * max_components + 2 * num_edges * max_components", num_constraints = "num_vertices + 5 * max_components + 6 * num_vertices * max_components + 6 * num_edges * max_components", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(3 * num_vertices * max_components + 2 * max_components + 2 * num_edges * max_components) * (num_vertices + 5 * max_components + 6 * num_vertices * max_components + 6 * num_edges * max_components)", + }, +})] impl ReduceTo> for BoundedComponentSpanningForest { type Result = ReductionBCSFToILP; @@ -188,7 +188,14 @@ impl ReduceTo> for BoundedComponentSpanningForest { } } - let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[2 * n * k + k..3 * n * k + 2 * k] + .fill(IntegerVariable::new(Some(0), Some(n_i64)).map_err(Self::target_construction)?); + variables[3 * n * k + 2 * k..].fill( + IntegerVariable::new(Some(0), Some(cap.max(0))).map_err(Self::target_construction)?, + ); + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionBCSFToILP { target, n, k }) } diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index 1b348b59a..f38073a82 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -49,15 +49,15 @@ impl ReductionResult for ReductionCAToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_links * num_capacities", num_constraints = "num_links + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_links * num_capacities) * (num_links + 1)", + }, +})] impl ReduceTo> for CapacityAssignment { type Result = ReductionCAToILP; diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index bcde39553..aa7169b5a 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -194,15 +194,11 @@ impl ILPBuilder { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCircuitToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_variables + 2 * num_expression_nodes", - num_constraints = "5 * num_expression_nodes + num_assignment_outputs", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_variables + 2 * num_expression_nodes", + num_constraints = "5 * num_expression_nodes + num_assignment_outputs", + num_nonzeros = "(num_variables + 2 * num_expression_nodes) * (5 * num_expression_nodes + num_assignment_outputs)", +})] impl ReduceTo> for CircuitSAT { type Result = ReductionCircuitToILP; diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 1f7d2a922..852f45288 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -21,7 +21,7 @@ //! substring problems," Journal of the ACM 49(2):157-171, 2002. //! -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -82,9 +82,7 @@ impl ReductionResult for ReductionClosestStringToILP { transform = exact { num_vars = "alphabet_size * string_length + 1", num_constraints = "string_length + num_strings", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "alphabet_size * string_length + num_strings * (string_length + 1)", } )] impl ReduceTo> for ClosestString { @@ -127,8 +125,17 @@ impl ReduceTo> for ClosestString { // Objective: minimize R. let objective = vec![(r_idx, 1)]; - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + // Every center has Hamming distance at most the string length, so an optimum has R <= m. + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[r_idx] = IntegerVariable::new( + Some(0), + Some(Self::exact_i64(m, "bounding the Hamming radius")?), + ) + .map_err(Self::target_construction)?; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionClosestStringToILP { target, diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 65416c54c..8bb7589b9 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -29,7 +29,7 @@ //! substring problems," Journal of the ACM 49(2):157-171, 2002. //! -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -118,15 +118,15 @@ fn decode_one_hot( Ok(index) } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "alphabet_size * substring_length + total_num_windows + 1", num_constraints = "substring_length + num_strings + total_num_windows + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(alphabet_size * substring_length + total_num_windows + 1) * (substring_length + num_strings + total_num_windows + 1)", + }, +})] impl ReduceTo> for ClosestSubstring { type Result = ReductionClosestSubstringToILP; @@ -200,8 +200,13 @@ impl ReduceTo> for ClosestSubstring { // Objective: minimize R. let objective = vec![(r_idx, 1)]; - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[r_idx] = + IntegerVariable::new(Some(0), Some(ell_i64)).map_err(Self::target_construction)?; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionClosestSubstringToILP { target, diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index b5654e166..6d8986da3 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -233,6 +233,7 @@ fn dot(left: &[i64], right: &[i64], operation: &str) -> Result> for ClosestVectorProblem { type Result = ReductionCVPToQUBO; diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index 00b2e91ee..95a457064 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -49,15 +49,11 @@ impl ReductionResult for ReductionClusteringToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionClusteringToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_elements * num_clusters", - num_constraints = "num_elements + num_elements * (num_elements - 1) / 2 * num_clusters", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_elements * num_clusters", + num_constraints = "num_elements + num_elements * (num_elements - 1) / 2 * num_clusters", + num_nonzeros = "(num_elements * num_clusters) * (num_elements + num_elements * (num_elements - 1) / 2 * num_clusters)", +})] impl ReduceTo> for Clustering { type Result = ReductionClusteringToILP; diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index dfb7d2a32..f064b7fc2 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -182,6 +182,7 @@ fn reduce_kcoloring_to_qubo( #[reduction( transform = exact { num_vars = "num_vertices * num_colors", + num_quadratic_terms = "num_vertices * num_colors * (num_colors - 1) / 2 + num_edges * num_colors", } )] impl ReduceTo>> for KColoring { diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index f61d3e4bb..08eb9974e 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -42,15 +42,11 @@ impl ReductionResult for ReductionCBMToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCBMToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_cols * num_cols + num_rows * num_cols + num_rows * num_cols", - num_constraints = "num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_cols * num_cols + num_rows * num_cols + num_rows * num_cols", + num_constraints = "num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1", + num_nonzeros = "(num_cols * num_cols + num_rows * num_cols + num_rows * num_cols) * (num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1)", +})] impl ReduceTo> for ConsecutiveBlockMinimization { type Result = ReductionCBMToILP; diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index 999a91899..18dbd6535 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionCOMAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCOMAToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_cols * num_cols + 5 * num_rows * num_cols", num_constraints = "num_cols + num_cols + num_rows * num_cols + 2 * num_rows + num_rows + 3 * num_rows * num_cols + 4 * num_rows * num_cols + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_cols * num_cols + 5 * num_rows * num_cols) * (num_cols + num_cols + num_rows * num_cols + 2 * num_rows + num_rows + 3 * num_rows * num_cols + 4 * num_rows * num_cols + 1)", + }, +})] impl ReduceTo> for ConsecutiveOnesMatrixAugmentation { type Result = ReductionCOMAToILP; diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index a1affc72d..04b40ce82 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -46,15 +46,11 @@ impl ReductionResult for ReductionCOSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCOSToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_cols + num_cols * bound + 5 * num_rows * bound", - num_constraints = "2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_cols + num_cols * bound + 5 * num_rows * bound", + num_constraints = "2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound", + num_nonzeros = "(num_cols + num_cols * bound + 5 * num_rows * bound) * (2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound)", +})] impl ReduceTo> for ConsecutiveOnesSubmatrix { type Result = ReductionCOSToILP; diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 7d4d095a9..0e1853f84 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -139,9 +139,7 @@ impl crate::rules::AggregateReductionResult for ReductionCDFTToILP {} transform = exact { num_vars = "num_objects * total_domain_size + num_objects * num_frequency_cells", num_constraints = "num_objects * num_attributes + num_known_values + num_frequency_cells + 3 * num_objects * num_frequency_cells", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "num_objects * total_domain_size + num_known_values + 8 * num_objects * num_frequency_cells", } )] impl ReduceTo> for ConsistencyOfDatabaseFrequencyTables { diff --git a/src/rules/directedhamiltonianpath_ilp.rs b/src/rules/directedhamiltonianpath_ilp.rs index d201e5bb2..fb4415956 100644 --- a/src/rules/directedhamiltonianpath_ilp.rs +++ b/src/rules/directedhamiltonianpath_ilp.rs @@ -53,15 +53,11 @@ impl ReductionResult for ReductionDirectedHamiltonianPathToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDirectedHamiltonianPathToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices^2", - num_constraints = "3 * num_vertices + num_vertices^3", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2", + num_constraints = "3 * num_vertices + num_vertices^3", + num_nonzeros = "(num_vertices^2) * (3 * num_vertices + num_vertices^3)", +})] impl ReduceTo> for DirectedHamiltonianPath { type Result = ReductionDirectedHamiltonianPathToILP; diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index ff9089ec1..de6b9e0ef 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -12,7 +12,7 @@ //! Objective: Minimize 0 (feasibility). //! Extraction: Direct 2*|A| variables. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -55,15 +55,11 @@ impl ReductionResult for ReductionD2CIFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionD2CIFToILP {} -#[reduction( - transform = upper_bound { - num_vars = "2 * num_arcs", - num_constraints = "num_arcs + 2 * num_vertices + 2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "2 * num_arcs", + num_constraints = "num_arcs + 2 * num_vertices + 2", + num_nonzeros = "(2 * num_arcs) * (num_arcs + 2 * num_vertices + 2)", +})] impl ReduceTo> for DirectedTwoCommodityIntegralFlow { type Result = ReductionD2CIFToILP; @@ -155,8 +151,18 @@ impl ReduceTo> for DirectedTwoCommodityIntegralFlow { } constraints.push(LinearConstraint::ge(sink2_terms, self.requirement_2())); + let variables = self + .capacities() + .iter() + .copied() + .cycle() + .take(num_vars) + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionD2CIFToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_arcs: m, }) diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index 7dfdbed51..674db108b 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -93,15 +93,15 @@ impl ReductionResult for ReductionDCPToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDCPToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_pairs * 2 * num_edges", num_constraints = "num_pairs * num_vertices + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_pairs * 2 * num_edges) * (num_pairs * num_vertices + num_vertices)", + }, +})] impl ReduceTo> for DisjointConnectingPaths { type Result = ReductionDCPToILP; diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index 2ed359613..edd086456 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -1,6 +1,6 @@ //! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP`. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::EnsembleComputation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -80,15 +80,15 @@ impl ReductionResult for ReductionEnsembleComputationToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "3 * budget * universe_size + budget * (budget - 1) * (universe_size + 1) + num_subsets * budget + budget", num_constraints = "5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets", }, - unavailable = { - num_nonzeros = "depends on the cardinalities and duplicate structure of the required subsets", - } -)] + upper_bound { + num_nonzeros = "(3 * budget * universe_size + budget * (budget - 1) * (universe_size + 1) + num_subsets * budget + budget) * (5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets)", + }, +})] impl ReduceTo> for EnsembleComputation { type Result = ReductionEnsembleComputationToILP; @@ -279,8 +279,11 @@ impl ReduceTo> for EnsembleComputation { } let objective = (0..budget).map(|step| (activity_base + step, 1)).collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let variables = vec![IntegerVariable::binary(); num_vars]; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionEnsembleComputationToILP { target, universe_size: u, diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index 5543806d8..64741308a 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -23,7 +23,7 @@ //! Bang-Jensen and Gutin, *Digraphs: Theory, Algorithms and Applications*, //! 2nd ed., Springer (2009). -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -147,15 +147,11 @@ fn compatible_pairs(arcs: &[(usize, usize)]) -> Vec<(usize, usize)> { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionEulerianPathToILP {} -#[reduction( - transform = upper_bound { - num_vars = "3 * num_arcs + num_arcs * num_arcs", - num_constraints = "5 * num_arcs + 2 * num_arcs * num_arcs + 2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "3 * num_arcs + num_arcs * num_arcs", + num_constraints = "5 * num_arcs + 2 * num_arcs * num_arcs + 2", + num_nonzeros = "(3 * num_arcs + num_arcs * num_arcs) * (5 * num_arcs + 2 * num_arcs * num_arcs + 2)", +})] impl ReduceTo> for EulerianPath { type Result = ReductionEulerianPathToILP; @@ -239,8 +235,14 @@ impl ReduceTo> for EulerianPath { constraints.push(LinearConstraint::eq(start_sum, 1)); constraints.push(LinearConstraint::eq(end_sum, 1)); - let target = ILP::new(num_vars, constraints, Vec::new(), ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[p + 2 * m..].fill( + IntegerVariable::new(Some(0), Some(m_i64 - 1)).map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, Vec::new(), ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionEulerianPathToILP { target, diff --git a/src/rules/exactcoverby3sets_ilp.rs b/src/rules/exactcoverby3sets_ilp.rs index 2a7ea358d..446243f8e 100644 --- a/src/rules/exactcoverby3sets_ilp.rs +++ b/src/rules/exactcoverby3sets_ilp.rs @@ -43,9 +43,7 @@ impl crate::rules::AggregateReductionResult for ReductionX3CToILP {} transform = exact { num_vars = "num_subsets", num_constraints = "universe_size + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "4 * num_subsets", } )] impl ReduceTo> for ExactCoverBy3Sets { diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index a2d76cefb..0d321f552 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -80,9 +80,7 @@ impl ReductionResult for ReductionERCToILP { transform = exact { num_vars = "num_records * num_sectors + num_records^2 * num_sectors^2", num_constraints = "num_records + 3 * num_records^2 * num_sectors^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "7 * num_records^2 * num_sectors^2", } )] impl ReduceTo> for ExpectedRetrievalCost { diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index f7e6fe5fb..a9ee5b30f 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -19,7 +19,7 @@ //! 4. Binary bounds: p_i ≤ 1, q_j ≤ 1 //! 5. Carry bounds: 0 ≤ c_k ≤ min(m, n) -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Factoring; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; @@ -113,15 +113,11 @@ impl ReductionResult for ReductionFactoringToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFactoringToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits", num_constraints = "3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits) * (3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1)", +})] impl ReduceTo> for Factoring { type Result = ReductionFactoringToILP; @@ -223,8 +219,15 @@ impl ReduceTo> for Factoring { // Objective: feasibility problem (minimize 0) let objective: Vec<(usize, i64)> = vec![]; - let ilp = ILP::::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_p + num_q + num_z..].fill( + IntegerVariable::new(Some(0), Some(carry_upper)) + .map_err(>>::target_construction)?, + ); + + let ilp = + ILP::::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(>>::target_construction)?; Ok(ReductionFactoringToILP { target: ilp, m, n }) } diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index 20ea224a6..d56068386 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -10,7 +10,7 @@ //! interval non-overlap: if `u` is before `v`, then `v` must be scheduled no //! earlier than the latest dependent of `u`. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -51,9 +51,7 @@ impl crate::rules::AggregateReductionResult for ReductionFeasibleRegisterAssignm transform = exact { num_vars = "2 * num_vertices + num_vertices * (num_vertices - 1) / 2", num_constraints = "3 * num_vertices * (num_vertices - 1) / 2 + 3 * num_vertices + 2 * num_arcs + 2 * num_same_register_pairs", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "4 * num_vertices + 4 * num_arcs + 7 * num_vertices * (num_vertices - 1) / 2 + 6 * num_same_register_pairs", } )] impl ReduceTo> for FeasibleRegisterAssignment { @@ -137,8 +135,14 @@ impl ReduceTo> for FeasibleRegisterAssignment { )); } + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[..2 * n].fill( + IntegerVariable::new(Some(0), Some(last_position)) + .map_err(Self::target_construction)?, + ); + Ok(ReductionFeasibleRegisterAssignmentToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_vertices: n, }) diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index 8bbce62e5..fd91dcb2d 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -5,7 +5,7 @@ //! Machine-chain and big-M disjunctive constraints enforce a valid flow-shop //! schedule; the deadline becomes a makespan bound. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FlowShopScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -71,15 +71,11 @@ impl ReductionResult for ReductionFSSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFSSToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors", num_constraints = "num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors) * (num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs)", +})] impl ReduceTo> for FlowShopScheduling { type Result = ReductionFSSToILP; @@ -191,8 +187,13 @@ impl ReduceTo> for FlowShopScheduling { } } + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_order_vars..].fill( + IntegerVariable::new(Some(0), Some(deadline)).map_err(Self::target_construction)?, + ); + Ok(ReductionFSSToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_jobs: n, num_machines: m, diff --git a/src/rules/graph.rs b/src/rules/graph.rs index a0f50f77e..5f922aad6 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -954,14 +954,22 @@ impl ReductionGraph { target_problem: path.steps[index + 1].name.clone(), error: Box::new(error.clone()), })?; - contract - .transform() - .cloned() - .ok_or_else(|| PathParameterError::Unavailable { - step: index + 1, - source_problem: path.steps[index].name.clone(), - target_problem: path.steps[index + 1].name.clone(), - }) + let mut transform = contract.transform().cloned().unwrap_or_else(|| { + crate::parameters::ParameterTransform::new( + format!( + "{} -> {}", + path.steps[index].name, + path.steps[index + 1].name + ), + crate::parameters::ParameterRelation::Exact, + Vec::<(&str, crate::expr::Expr)>::new(), + ) + .expect("empty transform is valid") + }); + for field in contract.unavailable() { + transform.declare_unavailable(field.field, field.reason); + } + Ok(transform) }) .collect() } @@ -1370,14 +1378,13 @@ impl ReductionGraph { let mut parameters = Vec::new(); if let Ok(contract) = contract { if let Some(transform) = contract.transform() { - let relation = match transform.relation() { - crate::parameters::ParameterRelation::Exact => "exact", - crate::parameters::ParameterRelation::UpperBound => "upper_bound", - }; parameters.extend(transform.expressions().map(|(field, expression)| { ParameterFieldJson { field: field.to_string(), - contract: relation, + contract: match transform.relation(field).expect("declared formula") { + crate::parameters::ParameterRelation::Exact => "exact", + crate::parameters::ParameterRelation::UpperBound => "upper_bound", + }, formula: Some(expression.to_string()), reason: None, } diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index 3186696a2..d6cceea7a 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionGraphPartitioningToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_edges", num_constraints = "2 * num_edges + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_edges) * (2 * num_edges + 1)", + }, +})] impl ReduceTo> for GraphPartitioning { type Result = ReductionGraphPartitioningToILP; diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs index 9840bc491..9033e1503 100644 --- a/src/rules/graphpartitioning_qubo.rs +++ b/src/rules/graphpartitioning_qubo.rs @@ -36,6 +36,7 @@ impl ReductionResult for ReductionGraphPartitioningToQUBO { #[reduction(transform = exact { num_vars = "num_vertices", + num_quadratic_terms = "num_vertices * (num_vertices - 1) / 2", })] impl ReduceTo> for GraphPartitioning { type Result = ReductionGraphPartitioningToQUBO; diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 06bd1b027..2a0ec4d69 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -90,6 +90,7 @@ impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToHam transform = upper_bound { num_vertices = "num_vertices + 3", num_edges = "num_edges + num_vertices + 1", + num_consecutive_positions = "num_vertices + 2", } )] impl ReduceTo> for HamiltonianCircuit { diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index 2cd1e8c0d..c5087553c 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -54,12 +54,10 @@ impl ReductionResult for ReductionHamiltonianPathToILP { impl crate::rules::AggregateReductionResult for ReductionHamiltonianPathToILP {} #[reduction( - transform = upper_bound { - num_vars = "num_vertices^2 + 2 * num_edges * num_vertices", - num_constraints = "2 * num_vertices + 6 * num_edges * num_vertices + num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + transform = exact { + num_vars = "num_vertices^2 + 2 * num_edges * num_consecutive_positions", + num_constraints = "2 * num_vertices + 6 * num_edges * num_consecutive_positions + num_consecutive_positions", + num_nonzeros = "2 * num_vertices^2 + 16 * num_edges * num_consecutive_positions", } )] impl ReduceTo> for HamiltonianPath { @@ -70,7 +68,7 @@ impl ReduceTo> for HamiltonianPath { let graph = self.graph(); let edges = graph.edges(); let m = edges.len(); - let n_pos = if n == 0 { 0 } else { n - 1 }; // number of consecutive-position pairs + let n_pos = self.num_consecutive_positions(); let num_x = n * n; let num_z = 2 * m * n_pos; diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index ea783e12f..4a6944ea2 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -111,9 +111,11 @@ impl ReductionResult for ReductionIntILPToBinaryILP { } #[reduction( - transform = unavailable { + transform = exact { + num_constraints = "num_constraints", + }, + unavailable = { num_vars = "the binary width depends on concrete variable bounds, not registered problem parameters", - num_constraints = "the exact row count is preserved but the target parameters model is unavailable until all ILP overhead declarations are migrated", num_nonzeros = "binary expansion depends on concrete variable bounds and row sparsity", }, )] diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index ca826cec4..32408abe6 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -78,11 +78,13 @@ impl crate::rules::AggregateReductionResult for ReductionILPToQUBO { } } -#[reduction( - transform = unavailable { - num_vars = "the slack-bit count depends on coefficient magnitudes and right-hand sides absent from the registered source parameters vector", - } -)] +// Each successfully computed positive i64 slack range needs at most 63 bits. +// Distinct off-diagonal pairs bound the resulting quadratic terms, including +// when penalty contributions cancel. Both bounds use only source parameters. +#[reduction(transform = upper_bound { + num_vars = "num_vars + 63 * num_constraints", + num_quadratic_terms = "(num_vars + 63 * num_constraints) * (num_vars + 63 * num_constraints - 1) / 2", +})] impl ReduceTo> for ILP { type Result = ReductionILPToQUBO; diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index 8eb5b2aca..42f56afa1 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -4,7 +4,7 @@ //! capacity inequality is kept directly, and explicit upper bounds //! `c_i <= floor(B / s_i)` preserve the exact witness domain of the source. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::IntegerKnapsack; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -36,9 +36,7 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { transform = exact { num_vars = "num_items", num_constraints = "num_items + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "2 * num_items", } )] impl ReduceTo> for IntegerKnapsack { @@ -66,9 +64,21 @@ impl ReduceTo> for IntegerKnapsack { let objective = values.iter().copied().enumerate().collect(); + let variables = self + .sizes() + .iter() + .map(|&size| IntegerVariable::new(Some(0), Some(self.capacity() / size))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionIntegerKnapsackToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Maximize, + ) + .map_err(Self::target_construction)?, }) } } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index e9d4ee032..98038f27c 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -4,7 +4,7 @@ //! the bundle-capacity inequalities, flow-conservation equalities at //! nonterminals, and the sink inflow lower bound from the source problem. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowBundles; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -41,15 +41,15 @@ impl ReductionResult for ReductionIFBToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionIFBToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_arcs", num_constraints = "num_bundles + num_vertices - 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_arcs * (num_bundles + num_vertices - 1)", + }, +})] impl ReduceTo> for IntegralFlowBundles { type Result = ReductionIFBToILP; @@ -90,14 +90,16 @@ impl ReduceTo> for IntegralFlowBundles { } constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let variables = self + .arc_upper_bounds() + .into_iter() + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionIFBToILP { - target: ILP::new( - self.num_arcs(), - constraints, - vec![], - ObjectiveSense::Minimize, - ) - .map_err(Self::target_construction)?, + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?, }) } } diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 502e80405..2ec627726 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -3,7 +3,7 @@ //! One integer flow variable per arc. Capacity bounds, conservation at //! non-terminals, homologous-pair equality, and sink inflow requirement. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowHomologousArcs; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,21 +40,16 @@ impl ReductionResult for ReductionIFHAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionIFHAToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_arcs", - num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_arcs", + num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", + num_nonzeros = "num_arcs * (num_arcs^2 + num_arcs + num_vertices + 1)", +})] impl ReduceTo> for IntegralFlowHomologousArcs { type Result = ReductionIFHAToILP; fn reduce_to(&self) -> Result { let arcs = self.graph().arcs(); - let num_arcs = self.num_arcs(); let num_vertices = self.num_vertices(); let mut constraints = Vec::new(); @@ -98,8 +93,16 @@ impl ReduceTo> for IntegralFlowHomologousArcs { } constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let variables = self + .capacities() + .iter() + .copied() + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionIFHAToILP { - target: ILP::new(num_arcs, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, }) } diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index 780e28dfb..dc41b9fcf 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -3,7 +3,7 @@ //! One integer flow variable per arc. Capacity bounds, multiplier-scaled //! conservation at non-terminals, and sink inflow requirement. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowWithMultipliers; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,21 +40,20 @@ impl ReductionResult for ReductionIFWMToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionIFWMToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_arcs", num_constraints = "num_arcs + num_vertices - 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_arcs * (num_arcs + num_vertices - 1)", + }, +})] impl ReduceTo> for IntegralFlowWithMultipliers { type Result = ReductionIFWMToILP; fn reduce_to(&self) -> Result { let arcs = self.graph().arcs(); - let num_arcs = self.num_arcs(); let num_vertices = self.num_vertices(); let mut constraints = Vec::new(); @@ -96,8 +95,16 @@ impl ReduceTo> for IntegralFlowWithMultipliers { } constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let variables = self + .capacities() + .iter() + .copied() + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionIFWMToILP { - target: ILP::new(num_arcs, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, }) } diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index b86657b1f..fadee1715 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -42,15 +42,11 @@ impl ReductionResult for ReductionISTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionISTToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices * num_vertices", - num_constraints = "2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices * num_vertices", + num_constraints = "2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices", + num_nonzeros = "(num_vertices * num_vertices) * (2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices)", +})] impl ReduceTo> for IsomorphicSpanningTree { type Result = ReductionISTToILP; diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 4ce5c4cf7..0ed5ae82e 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -57,15 +57,11 @@ impl ReductionResult for ReductionKCliqueToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionKCliqueToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices", - num_constraints = "num_vertices^2 + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices", + num_constraints = "num_vertices^2 + 1", + num_nonzeros = "num_vertices * (num_vertices^2 + 1)", +})] impl ReduceTo> for KClique { type Result = ReductionKCliqueToILP; diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index 2601ce5f9..1505e7a76 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -34,15 +34,15 @@ impl ReductionResult for ReductionKnapsackToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_items", num_constraints = "1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_items * 1", + }, +})] impl ReduceTo> for Knapsack { type Result = ReductionKnapsackToILP; diff --git a/src/rules/knapsack_qubo.rs b/src/rules/knapsack_qubo.rs index 63a0e208d..39d844644 100644 --- a/src/rules/knapsack_qubo.rs +++ b/src/rules/knapsack_qubo.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionKnapsackToQUBO { #[reduction(transform = unavailable { num_vars = "the exact piecewise slack-bit count is not representable in the parameter-expression language", + num_quadratic_terms = "the nonzero products depend on item sizes and values", })] impl ReduceTo> for Knapsack { type Result = ReductionKnapsackToQUBO; diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index 6ebb91c9f..f6d4d2337 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -330,8 +330,9 @@ impl crate::rules::AggregateReductionResult for ReductionKSatToQUBO {} impl crate::rules::AggregateReductionResult for Reduction3SATToQUBO {} #[reduction( - transform = exact { - num_vars = "num_vars", + transform = { + exact { num_vars = "num_vars" }, + upper_bound { num_quadratic_terms = "num_vars * (num_vars - 1) / 2" }, } )] impl ReduceTo>> for KSatisfiability { @@ -362,11 +363,14 @@ impl ReduceTo>> for KSatisfiability { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vars + num_clauses", - } -)] + }, + upper_bound { + num_quadratic_terms = "(num_vars + num_clauses) * ((num_vars + num_clauses) - 1) / 2", + }, +})] impl ReduceTo>> for KSatisfiability { type Result = Reduction3SATToQUBO; diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 5745f463e..2c330682c 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -100,15 +100,11 @@ impl ReductionResult for ReductionLBDPToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "max_paths * 2 * num_edges + max_paths", - num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "max_paths * 2 * num_edges + max_paths", + num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", + num_nonzeros = "(max_paths * 2 * num_edges + max_paths) * (max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths)", +})] impl ReduceTo> for LengthBoundedDisjointPaths { type Result = ReductionLBDPToILP; diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index b122df15a..cbac8605f 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -56,15 +56,15 @@ impl ReductionResult for ReductionLongestCircuitToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + 2 * num_vertices + 2 * num_edges * num_vertices", num_constraints = "2 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_edges + 2 * num_vertices + 2 * num_edges * num_vertices) * (2 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices)", + }, +})] impl ReduceTo> for LongestCircuit { type Result = ReductionLongestCircuitToILP; @@ -209,15 +209,15 @@ impl ReductionResult for ReductionDecisionLongestCircuitToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDecisionLongestCircuitToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + 2 * num_vertices + 2 * num_edges * num_vertices", num_constraints = "3 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices", }, - unavailable = { - num_nonzeros = "depends on the graph and nonzero edge lengths", - } -)] + upper_bound { + num_nonzeros = "(num_edges + 2 * num_vertices + 2 * num_edges * num_vertices) * (3 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices)", + }, +})] impl ReduceTo> for Decision> { type Result = ReductionDecisionLongestCircuitToILP; diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index 11225f470..d156ed261 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -53,9 +53,7 @@ impl ReductionResult for ReductionLCSToILP { transform = exact { num_vars = "max_length * (alphabet_size + 1) + max_length * total_length", num_constraints = "max_length + num_transitions + max_length * num_strings + max_length * total_length + num_transitions * sum_triangular_lengths", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "max_length * (alphabet_size + 1 + num_strings + 3 * total_length) + 2 * num_transitions * (1 + sum_triangular_lengths)", } )] impl ReduceTo> for LongestCommonSubsequence { diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index ef0246564..e2e99c0da 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -5,7 +5,7 @@ //! path positions. Flow-balance constraints force a single directed `s-t` path, //! while MTZ-style ordering constraints eliminate detached cycles. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LongestPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -48,15 +48,15 @@ impl ReductionResult for ReductionLongestPathToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_edges + num_vertices", num_constraints = "5 * num_edges + 4 * num_vertices + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 1)", + }, +})] impl ReduceTo> for LongestPath { type Result = ReductionLongestPathToILP; @@ -172,9 +172,19 @@ impl ReduceTo> for LongestPath { objective.push((ReductionLongestPathToILP::arc_var(edge_idx, 1), coeff)); } + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[2 * num_edges..].fill( + IntegerVariable::new(Some(0), Some(max_order)).map_err(Self::target_construction)?, + ); + Ok(ReductionLongestPathToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Maximize, + ) + .map_err(Self::target_construction)?, num_edges, }) } diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 1d2e3f93c..0830cbd6e 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -32,15 +32,15 @@ impl ReductionResult for ReductionMxISToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_edges + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * (num_edges + num_vertices)", + }, +})] impl ReduceTo> for MaximalIS { type Result = ReductionMxISToILP; diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index cfa12df13..a9445762b 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -40,15 +40,15 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vars + num_clauses", num_constraints = "num_clauses", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vars + num_clauses) * num_clauses", + }, +})] impl ReduceTo> for Maximum2Satisfiability { type Result = ReductionMaximum2SatisfiabilityToILP; diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index 7f62e15b1..f4922efd5 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -45,15 +45,11 @@ impl ReductionResult for ReductionCliqueToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_vertices", - num_constraints = "num_vertices^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices", + num_constraints = "num_vertices^2", + num_nonzeros = "num_vertices * (num_vertices^2)", +})] impl ReduceTo> for MaximumClique { type Result = ReductionCliqueToILP; diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index 8021bb316..d9e6067e1 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -80,15 +80,15 @@ where }) } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * num_vertices", + }, +})] impl ReduceTo> for MaximumCoKPlex { type Result = ReductionCoKPlexToILP; @@ -109,15 +109,15 @@ impl ReduceTo> for MaximumCoKPlex { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * num_vertices", + }, +})] impl ReduceTo> for MaximumCoKPlex { type Result = ReductionCoKPlexToILP; diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index 4ab7d8858..64001e434 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -66,15 +66,11 @@ impl ReductionResult for ReductionMCESToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2", - num_constraints = "num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2", + num_constraints = "num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2", + num_nonzeros = "(num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2) * (num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2)", +})] impl ReduceTo> for MaximumCommonEdgeSubgraph { type Result = ReductionMCESToILP; diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index 709e0053f..5970556b6 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -73,9 +73,7 @@ impl ReductionResult for ReductionCMOToILP { transform = exact { num_vars = "num_vertices_1 * num_vertices_2 + num_contacts_1 * num_contacts_2", num_constraints = "num_vertices_1 + num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) / 2 * num_vertices_2 * (num_vertices_2 + 1) / 2 + 2 * num_contacts_1 * num_contacts_2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "2 * num_vertices_1 * num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) * num_vertices_2 * (num_vertices_2 + 1) / 2 + 4 * num_contacts_1 * num_contacts_2", } )] impl ReduceTo> for MaximumContactMapOverlap { diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index 3a0221a6e..acba50a38 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -58,15 +58,15 @@ impl ReductionResult for ReductionDomaticNumberToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices * num_vertices + num_vertices", num_constraints = "num_vertices + num_vertices * num_vertices + num_vertices * num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices * num_vertices + num_vertices) * (num_vertices + num_vertices * num_vertices + num_vertices * num_vertices)", + }, +})] impl ReduceTo> for MaximumDomaticNumber { type Result = ReductionDomaticNumberToILP; diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index c7909601b..58f4cf93d 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -138,15 +138,15 @@ where }) } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_edges", num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", + }, +})] impl ReduceTo> for MaximumEdgeWeightedKClique { type Result = ReductionMaximumEdgeWeightedKCliqueToILP; @@ -155,15 +155,15 @@ impl ReduceTo> for MaximumEdgeWeightedKClique { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_edges", num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", + }, +})] impl ReduceTo> for MaximumEdgeWeightedKClique { type Result = ReductionMaximumEdgeWeightedKCliqueToILP; diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 2623018b7..1fac86ae3 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -18,7 +18,7 @@ //! //! Objective: maximize sum(z_v) -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -55,15 +55,15 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "3 * num_edges + num_vertices", num_constraints = "3 * num_vertices + 2 * num_edges + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(3 * num_edges + num_vertices) * (3 * num_vertices + 2 * num_edges + 1)", + }, +})] impl ReduceTo> for MaximumLeafSpanningTree { type Result = ReductionMaximumLeafSpanningTreeToILP; @@ -156,8 +156,15 @@ impl ReduceTo> for MaximumLeafSpanningTree { // Objective: maximize sum(z_v) let objective: Vec<(usize, i64)> = (0..n).map(|v| (leaf_var(v), 1)).collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m + n..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Maximize) + .map_err(Self::target_construction)?; Ok(ReductionMaximumLeafSpanningTreeToILP { target, diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index 954b09627..0afd9524a 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -76,10 +76,8 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { transform = exact { num_vars = "num_items * (num_items - 1) / 2", num_constraints = "num_items * (num_items - 1) * (num_items - 2) / 3", + num_nonzeros = "num_items * (num_items - 1) * (num_items - 2)", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for MaximumLikelihoodRanking { type Result = ReductionMaximumLikelihoodRankingToILP; diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index a4e27b0d9..84f01cdb4 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -45,15 +45,15 @@ impl ReductionResult for ReductionMatchingToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges", + }, + upper_bound { num_constraints = "num_vertices", + num_nonzeros = "2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +})] impl ReduceTo> for MaximumMatching { type Result = ReductionMatchingToILP; diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index b2195358a..be215a8f8 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -39,15 +39,11 @@ impl ReductionResult for ReductionSPToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_sets", - num_constraints = "universe_size", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_sets", + num_constraints = "universe_size", + num_nonzeros = "num_sets * universe_size", +})] impl ReduceTo> for MaximumSetPacking { type Result = ReductionSPToILP; diff --git a/src/rules/maximumsetpacking_qubo.rs b/src/rules/maximumsetpacking_qubo.rs index 2a7559884..fe5ffd594 100644 --- a/src/rules/maximumsetpacking_qubo.rs +++ b/src/rules/maximumsetpacking_qubo.rs @@ -40,11 +40,14 @@ impl ReductionResult for ReductionSPToQUBO { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_sets", - } -)] + }, + upper_bound { + num_quadratic_terms = "num_sets * (num_sets - 1) / 2", + }, +})] impl ReduceTo> for MaximumSetPacking { type Result = ReductionSPToQUBO; diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 0886ad247..42f2263e1 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -24,7 +24,7 @@ //! //! Objective: minimize sum(w_e * y_e) -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -62,15 +62,11 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "5 * num_edges", - num_constraints = "5 * num_edges + 2 * num_vertices + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "5 * num_edges", + num_constraints = "5 * num_edges + 2 * num_vertices + 1", + num_nonzeros = "(5 * num_edges) * (5 * num_edges + 2 * num_vertices + 1)", +})] impl ReduceTo> for MinimumCapacitatedSpanningTree { type Result = ReductionMinimumCapacitatedSpanningTreeToILP; @@ -208,8 +204,19 @@ impl ReduceTo> for MinimumCapacitatedSpanningTree { .map(|(edge_idx, weight)| (edge_var(edge_idx), weight.to_sum())) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m..3 * m].fill( + IntegerVariable::new(Some(0), Some(cap.min(total_req).max(0))) + .map_err(Self::target_construction)?, + ); + variables[3 * m..].fill( + IntegerVariable::new(Some(0), Some(connectivity_total)) + .map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionMinimumCapacitatedSpanningTreeToILP { target, diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index fc139660b..63278b39d 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -61,15 +61,15 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices * num_edges + num_edges + num_edges * num_edges", num_constraints = "num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices * num_edges + num_edges + num_edges * num_edges) * (num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges)", + }, +})] impl ReduceTo> for MinimumCoveringByCliques { type Result = ReductionMinimumCoveringByCliquesToILP; diff --git a/src/rules/minimumcutintoboundedsets_ilp.rs b/src/rules/minimumcutintoboundedsets_ilp.rs index bb0331be8..0386d9f6a 100644 --- a/src/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/rules/minimumcutintoboundedsets_ilp.rs @@ -39,15 +39,15 @@ impl ReductionResult for ReductionMinCutBSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_edges", num_constraints = "2 + 2 + 2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_edges) * (2 + 2 + 2 * num_edges)", + }, +})] impl ReduceTo> for MinimumCutIntoBoundedSets { type Result = ReductionMinCutBSToILP; diff --git a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs index 6753e3d39..5597a550a 100644 --- a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -78,8 +78,13 @@ impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { } } -#[reduction(transform = exact { - num_vars = "num_orientation_samples", +#[reduction(transform = { + exact { + num_vars = "num_orientation_samples", + }, + upper_bound { + num_quadratic_terms = "num_orientation_samples * (num_orientation_samples - 1) / 2", + }, })] impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { type Result = ReductionMinimumDiscretePlanarInverseKinematicsToQUBO; diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 5b1d3f945..83ad5fb96 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -46,15 +46,15 @@ impl ReductionResult for ReductionDSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * num_vertices", + }, +})] impl ReduceTo> for MinimumDominatingSet { type Result = ReductionDSToILP; diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index dd30eb04a..9588f942b 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -18,7 +18,7 @@ //! Objective: minimize Σ p(a) · y_a. //! Extraction: first m variables are the flow values. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumEdgeCostFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -53,15 +53,15 @@ impl ReductionResult for ReductionMECFToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_edges", num_constraints = "2 * num_edges + num_vertices - 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(2 * num_edges) * (2 * num_edges + num_vertices - 1)", + }, +})] impl ReduceTo> for MinimumEdgeCostFlow { type Result = ReductionMECFToILP; @@ -124,9 +124,22 @@ impl ReduceTo> for MinimumEdgeCostFlow { // Objective: minimize Σ p(a) · y_a let objective: Vec<(usize, i64)> = (0..m).map(|a| (y(a), self.prices()[a])).collect(); + let mut variables = self + .capacities() + .iter() + .map(|&capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + variables.resize(num_vars, IntegerVariable::binary()); + Ok(ReductionMECFToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_edges: m, }) } diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index ba34bba53..5f6acb6c8 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -213,15 +213,11 @@ fn encode_pointer(n: usize, start: usize, len: usize) -> usize { idx + len - 1 } -#[reduction( - transform = upper_bound { - num_vars = "string_length * alphabet_size + 2 * string_length + string_length ^ 3", - num_constraints = "string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "string_length * alphabet_size + 2 * string_length + string_length ^ 3", + num_constraints = "string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length", + num_nonzeros = "(string_length * alphabet_size + 2 * string_length + string_length ^ 3) * (string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length)", +})] impl ReduceTo> for MinimumExternalMacroDataCompression { type Result = ReductionEMDCToILP; diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index bf0893c70..c98bc4051 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionMFDTSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_inputs * num_outputs", num_constraints = "num_vertices - num_inputs - num_outputs", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_inputs * num_outputs) * (num_vertices - num_inputs - num_outputs)", + }, +})] impl ReduceTo> for MinimumFaultDetectionTestSet { type Result = ReductionMFDTSToILP; diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index 2de455d85..b2f236d5b 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -9,7 +9,7 @@ //! - Objective: Minimize Σ w_a * y_a //! - Variable layout: first |A| are y_a, next |V| are o_v -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackArcSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -54,15 +54,15 @@ impl ReductionResult for ReductionFASToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_arcs + num_vertices", num_constraints = "num_arcs + num_arcs + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_arcs + num_vertices) * (num_arcs + num_arcs + num_vertices)", + }, +})] impl ReduceTo> for MinimumFeedbackArcSet { type Result = ReductionFASToILP; @@ -109,8 +109,15 @@ impl ReduceTo> for MinimumFeedbackArcSet { .map(|(arc, &weight)| (arc, weight)) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionFASToILP { target, diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index 8b712b1a1..b6e0b88e7 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -6,7 +6,7 @@ //! Plus binary bounds (x_i <= 1) and order bounds (o_i <= n-1) //! - Objective: Minimize the weighted sum of removed vertices -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackVertexSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -51,15 +51,15 @@ impl ReductionResult for ReductionMFVSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_vertices", num_constraints = "num_arcs + 2 * num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(2 * num_vertices) * (num_arcs + 2 * num_vertices)", + }, +})] impl ReduceTo> for MinimumFeedbackVertexSet { type Result = ReductionMFVSToILP; @@ -106,8 +106,15 @@ impl ReduceTo> for MinimumFeedbackVertexSet { .map(|(vertex, &weight)| (vertex, weight)) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[n..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(>>::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(>>::target_construction)?; Ok(ReductionMFVSToILP { target, diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index 76e1b5692..9dc8f2914 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -7,7 +7,7 @@ //! - For each edge (u,v): pos_u - pos_v <= B, pos_v - pos_u <= B //! - Objective: minimize B -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumGraphBandwidth; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -49,15 +49,15 @@ impl ReductionResult for ReductionMGBToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2 + num_vertices + 1", num_constraints = "2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices^2 + num_vertices + 1) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges)", + }, +})] impl ReduceTo> for MinimumGraphBandwidth { type Result = ReductionMGBToILP; @@ -131,8 +131,15 @@ impl ReduceTo> for MinimumGraphBandwidth { // Objective: minimize B let objective = vec![(b_idx, 1)]; - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_x..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionMGBToILP { target, diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index fe8f498d9..e9771539d 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -31,15 +31,15 @@ impl ReductionResult for ReductionHSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "universe_size", num_constraints = "num_sets", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "universe_size * num_sets", + }, +})] impl ReduceTo> for MinimumHittingSet { type Result = ReductionHSToILP; diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 0ff986409..1c58d96b1 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -159,15 +159,11 @@ impl ReductionResult for ReductionIMDCToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "string_len + string_len ^ 3", - num_constraints = "string_len + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "string_len + string_len ^ 3", + num_constraints = "string_len + 1", + num_nonzeros = "(string_len + string_len ^ 3) * (string_len + 1)", +})] impl ReduceTo> for MinimumInternalMacroDataCompression { type Result = ReductionIMDCToILP; diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index d279e1b67..8de7cad1d 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -56,10 +56,8 @@ fn y_index(n: usize, i: usize, j: usize) -> usize { transform = exact { num_vars = "num_rows + num_rows * (num_rows - 1) / 2", num_constraints = "3 * num_rows * (num_rows - 1) / 2", + num_nonzeros = "7 * num_rows * (num_rows - 1) / 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for MinimumMatrixCover { type Result = ReductionMinimumMatrixCoverToILP; diff --git a/src/rules/minimummaximalmatching_ilp.rs b/src/rules/minimummaximalmatching_ilp.rs index 33e1f2afe..93dcd0cf3 100644 --- a/src/rules/minimummaximalmatching_ilp.rs +++ b/src/rules/minimummaximalmatching_ilp.rs @@ -48,15 +48,15 @@ impl ReductionResult for ReductionMMMToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges", num_constraints = "num_vertices + num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_edges * (num_vertices + num_edges)", + }, +})] impl ReduceTo> for MinimumMaximalMatching { type Result = ReductionMMMToILP; diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index a10a1e9a5..12fef2119 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -48,15 +48,15 @@ impl ReductionResult for ReductionMDToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_vertices * (num_vertices - 1) / 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * (num_vertices * (num_vertices - 1) / 2)", + }, +})] impl ReduceTo> for MinimumMetricDimension { type Result = ReductionMDToILP; diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index 51c157007..4600bf0ef 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -57,15 +57,15 @@ impl ReductionResult for ReductionMMCToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_terminals * num_vertices + num_edges", num_constraints = "num_vertices + 2 * num_terminals * num_edges + num_terminals * num_terminals", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_terminals * num_vertices + num_edges) * (num_vertices + 2 * num_terminals * num_edges + num_terminals * num_terminals)", + }, +})] impl ReduceTo> for MinimumMultiwayCut { type Result = ReductionMMCToILP; diff --git a/src/rules/minimummultiwaycut_qubo.rs b/src/rules/minimummultiwaycut_qubo.rs index d6b94f090..bdfb81d6e 100644 --- a/src/rules/minimummultiwaycut_qubo.rs +++ b/src/rules/minimummultiwaycut_qubo.rs @@ -84,8 +84,13 @@ impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { } } -#[reduction(transform = exact { - num_vars = "num_terminals * num_vertices", +#[reduction(transform = { + exact { + num_vars = "num_terminals * num_vertices", + }, + upper_bound { + num_quadratic_terms = "(num_terminals * num_vertices) * ((num_terminals * num_vertices) - 1) / 2", + }, })] impl ReduceTo> for MinimumMultiwayCut { type Result = ReductionMinimumMultiwayCutToQUBO; diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index 6f208be75..7b830ac62 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionSCToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_sets", num_constraints = "universe_size", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_sets * universe_size", + }, +})] impl ReduceTo> for MinimumSetCovering { type Result = ReductionSCToILP; diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index bb6f82fd6..edfa101cd 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -117,15 +117,11 @@ fn weighted_distances_msmc( dist } -#[reduction( - transform = upper_bound { - num_vars = "num_vertices + num_vertices^2", - num_constraints = "num_vertices^2 + 2 * num_vertices + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices + num_vertices^2", + num_constraints = "num_vertices^2 + 2 * num_vertices + 1", + num_nonzeros = "(num_vertices + num_vertices^2) * (num_vertices^2 + 2 * num_vertices + 1)", +})] impl ReduceTo> for MinimumSumMulticenter { type Result = ReductionMSMCToILP; diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index e8533153d..bbf61f745 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -104,15 +104,15 @@ fn build_common_constraints( } // Unit-length variant -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + num_precedences + num_tasks", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + num_precedences + num_tasks)", + }, +})] impl ReduceTo> for MinimumTardinessSequencing { type Result = ReductionMTSToILP; @@ -150,15 +150,15 @@ impl ReduceTo> for MinimumTardinessSequencing { } // Arbitrary-length variant -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + num_precedences + num_tasks * num_tasks", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + num_precedences + num_tasks * num_tasks)", + }, +})] impl ReduceTo> for MinimumTardinessSequencing { type Result = ReductionMTSWeightedToILP; diff --git a/src/rules/minimumvertexcover_longestcommonsubsequence.rs b/src/rules/minimumvertexcover_longestcommonsubsequence.rs index e2f0a23a8..2cdc94188 100644 --- a/src/rules/minimumvertexcover_longestcommonsubsequence.rs +++ b/src/rules/minimumvertexcover_longestcommonsubsequence.rs @@ -44,11 +44,11 @@ impl ReductionResult for ReductionVCToLCS { num_strings = "num_edges + 1", max_length = "num_vertices", total_length = "num_vertices + 2 * num_edges * num_vertices - 2 * num_edges", + sum_triangular_lengths = "num_vertices * (num_vertices + 1) / 2 + num_edges * (2 * num_vertices - 2) * (2 * num_vertices - 1) / 2", }, unavailable = { cross_frequency_product = "the exact target parameter is not represented by this reduction's symbolic transform", num_transitions = "the exact target parameter is not represented by this reduction's symbolic transform", - sum_triangular_lengths = "the exact target parameter is not represented by this reduction's symbolic transform", } )] impl ReduceTo for MinimumVertexCover { diff --git a/src/rules/minimumvertexcover_minimummaximalmatching.rs b/src/rules/minimumvertexcover_minimummaximalmatching.rs index 9338eae91..4fccecbdd 100644 --- a/src/rules/minimumvertexcover_minimummaximalmatching.rs +++ b/src/rules/minimumvertexcover_minimummaximalmatching.rs @@ -21,10 +21,10 @@ inventory::submit! { source_variant_fn: as Problem>::variant, target_variant_fn: as Problem>::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), + fields: vec![ - ("num_vertices", crate::expr::Expr::variable("num_vertices")), - ("num_edges", crate::expr::Expr::variable("num_edges")), + ("num_vertices", crate::parameters::ParameterRelation::Exact, crate::expr::Expr::variable("num_vertices")), + ("num_edges", crate::parameters::ParameterRelation::Exact, crate::expr::Expr::variable("num_edges")), ], unavailable: vec![], }, diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index 970470c71..606fe757d 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -16,7 +16,7 @@ //! Objective: minimize Σ x_j (Hamming weight). use crate::models::algebraic::MinimumWeightDecoding; -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -53,15 +53,15 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_cols + num_rows", num_constraints = "num_rows + num_cols", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_cols + num_rows) * (num_rows + num_cols)", + }, +})] impl ReduceTo> for MinimumWeightDecoding { type Result = ReductionMinimumWeightDecodingToILP; @@ -96,9 +96,23 @@ impl ReduceTo> for MinimumWeightDecoding { // Objective: minimize Σ x_j let objective: Vec<(usize, i64)> = (0..m).map(|j| (x(j), 1)).collect(); + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m..].fill( + IntegerVariable::new( + Some(0), + Some(Self::exact_i64(m / 2, "bounding parity quotients")?), + ) + .map_err(Self::target_construction)?, + ); + Ok(ReductionMinimumWeightDecodingToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_cols: m, }) } diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index f4360ac63..82b521f56 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -24,7 +24,7 @@ //! Note: All-pairs shortest-path distances are computed using weighted shortest //! paths over `edge_lengths`. Unreachable assignment variables are forced to 0. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -121,15 +121,15 @@ fn weighted_distances_mmc( dist } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_vertices^2 + 1", num_constraints = "2 * num_vertices^2 + 3 * num_vertices + 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_vertices^2 + 1) * (2 * num_vertices^2 + 3 * num_vertices + 2)", + }, +})] impl ReduceTo> for MinMaxMulticenter { type Result = ReductionMMCToILP; @@ -232,8 +232,13 @@ impl ReduceTo> for MinMaxMulticenter { // Objective: minimize z let objective = vec![(z_var, 1)]; - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[z_var] = + IntegerVariable::new(Some(0), Some(z_upper)).map_err(Self::target_construction)?; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionMMCToILP { target, num_vertices: n, diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index 88da8843a..a12819f2d 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -5,7 +5,7 @@ //! within the length bound. Uses connectivity flow constraints on both //! forward and reverse directions. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MixedChinesePostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -42,15 +42,11 @@ impl ReductionResult for ReductionMCPToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1", - num_constraints = "num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1", + num_constraints = "num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2", + num_nonzeros = "(num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1) * (num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2)", +})] impl ReduceTo> for MixedChinesePostman { type Result = ReductionMCPToILP; @@ -371,8 +367,18 @@ impl ReduceTo> for MixedChinesePostman { } } - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[q..q + l] + .fill(IntegerVariable::new(Some(0), Some(big_g)).map_err(Self::target_construction)?); + variables[s_idx..q + 2 * l + 3 * n + 1] + .fill(IntegerVariable::new(Some(0), Some(n_i64)).map_err(Self::target_construction)?); + variables[q + 2 * l + 3 * n + 1..].fill( + IntegerVariable::new(Some(0), Some(flow_big_m)).map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionMCPToILP { target, diff --git a/src/rules/monochromatictriangle_ilp.rs b/src/rules/monochromatictriangle_ilp.rs index 137f5d4b3..5bdcc3449 100644 --- a/src/rules/monochromatictriangle_ilp.rs +++ b/src/rules/monochromatictriangle_ilp.rs @@ -43,15 +43,11 @@ impl ReductionResult for ReductionMonochromaticTriangleToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionMonochromaticTriangleToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_edges", - num_constraints = "2 * num_triangles + num_vertices^5 / 8", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_edges", + num_constraints = "2 * num_triangles + num_vertices^5 / 8", + num_nonzeros = "num_edges * (2 * num_triangles + num_vertices^5 / 8)", +})] impl ReduceTo> for MonochromaticTriangle { type Result = ReductionMonochromaticTriangleToILP; diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index e341e994c..63e8b1121 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -1,6 +1,6 @@ //! Reduction from MultipleChoiceBranching with integer weights to integer ILP. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MultipleChoiceBranching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -39,15 +39,15 @@ impl ReductionResult for ReductionMultipleChoiceBranchingToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionMultipleChoiceBranchingToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_arcs + num_vertices", num_constraints = "2 * num_arcs + 2 * num_vertices + num_partition_groups + 1", }, - unavailable = { - num_nonzeros = "zero weights and loop normalization determine the exact nonzero count", - } -)] + upper_bound { + num_nonzeros = "(num_arcs + num_vertices) * (2 * num_arcs + 2 * num_vertices + num_partition_groups + 1)", + }, +})] impl ReduceTo> for MultipleChoiceBranching { type Result = ReductionMultipleChoiceBranchingToILP; @@ -101,13 +101,20 @@ impl ReduceTo> for MultipleChoiceBranching { *self.threshold(), )); - let target = ILP::new( - num_arcs + num_vertices, - constraints, - vec![], - ObjectiveSense::Minimize, - ) - .map_err(>>::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_arcs + num_vertices]; + variables[num_arcs..].fill( + IntegerVariable::new( + Some(0), + Some(Self::exact_i64( + num_vertices.saturating_sub(1), + "bounding topological labels", + )?), + ) + .map_err(Self::target_construction)?, + ); + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(>>::target_construction)?; Ok(ReductionMultipleChoiceBranchingToILP { target, num_arcs }) } } diff --git a/src/rules/multiplecopyfileallocation_ilp.rs b/src/rules/multiplecopyfileallocation_ilp.rs index 335aa7a91..9b294b615 100644 --- a/src/rules/multiplecopyfileallocation_ilp.rs +++ b/src/rules/multiplecopyfileallocation_ilp.rs @@ -68,15 +68,15 @@ fn bfs_distances(graph: &SimpleGraph, source: usize, n: usize) -> Vec { dist } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_vertices^2", num_constraints = "num_vertices^2 + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_vertices^2) * (num_vertices^2 + num_vertices)", + }, +})] impl ReduceTo> for MultipleCopyFileAllocation { type Result = ReductionMCFAToILP; diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index 520ee4674..be832401b 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -56,15 +56,15 @@ impl ReductionResult for ReductionMSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionMSToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_processors", num_constraints = "num_tasks + num_processors", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_processors) * (num_tasks + num_processors)", + }, +})] impl ReduceTo> for MultiprocessorScheduling { type Result = ReductionMSToILP; diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index 8affee9e1..03d6a3b72 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -44,15 +44,15 @@ impl ReductionResult for ReductionNAESATToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionNAESATToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vars", num_constraints = "2 * num_clauses", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vars * (2 * num_clauses)", + }, +})] impl ReduceTo> for NAESatisfiability { type Result = ReductionNAESATToILP; diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index 617233292..670905866 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -70,15 +70,11 @@ impl ReductionResult for ReductionNMTSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionNMTSToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_pairs * num_pairs * num_pairs", - num_constraints = "3 * num_pairs", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_pairs * num_pairs * num_pairs", + num_constraints = "3 * num_pairs", + num_nonzeros = "(num_pairs * num_pairs * num_pairs) * (3 * num_pairs)", +})] impl ReduceTo> for NumericalMatchingWithTargetSums { type Result = ReductionNMTSToILP; diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 5a7d01091..514ff218a 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -26,7 +26,7 @@ //! //! **Objective:** Minimize C. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::OpenShopScheduling; use crate::models::Decision; use crate::reduction; @@ -103,15 +103,15 @@ impl ReductionResult for ReductionOSSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1", num_constraints = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines)", + }, +})] impl ReduceTo> for OpenShopScheduling { type Result = ReductionOSSToILP; @@ -267,9 +267,20 @@ impl ReduceTo> for OpenShopScheduling { // Objective: minimize C let objective = vec![(c_var, 1)]; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + let time_domain = + IntegerVariable::new(Some(0), Some(total_p)).map_err(Self::target_construction)?; + variables[num_order_vars..num_order_vars + num_start_vars].fill(time_domain); + variables[c_var] = time_domain; + Ok(ReductionOSSToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_jobs: n, num_machines: m, num_order_vars, @@ -305,15 +316,15 @@ impl ReductionResult for ReductionDecisionOpenShopSchedulingToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDecisionOpenShopSchedulingToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1", num_constraints = "3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2", }, - unavailable = { - num_nonzeros = "depends on the generated scheduling constraints", - } -)] + upper_bound { + num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2)", + }, +})] impl ReduceTo> for Decision { type Result = ReductionDecisionOpenShopSchedulingToILP; diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index 934dd35b1..e37b18c83 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -7,7 +7,7 @@ //! - abs_diff_le constraints: z_{u,v} >= p_u - p_v, z_{u,v} >= p_v - p_u //! - Minimize: sum z_{u,v} -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::OptimalLinearArrangement; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -49,15 +49,15 @@ impl ReductionResult for ReductionOLAToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2 + num_vertices + num_edges", num_constraints = "2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices^2 + num_vertices + num_edges) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges)", + }, +})] impl ReduceTo> for OptimalLinearArrangement { type Result = ReductionOLAToILP; @@ -132,8 +132,15 @@ impl ReduceTo> for OptimalLinearArrangement { // Objective: minimize sum z_e let objective: Vec<(usize, i64)> = (0..m).map(|e| (z_idx(e), 1)).collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_x..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(>>::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(>>::target_construction)?; Ok(ReductionOLAToILP { target, diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index e7a14a680..a83378b83 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -57,9 +57,7 @@ impl ReductionResult for ReductionOLAToSequencingToMinimizeWeightedCompletionTim #[reduction( transform = exact { num_tasks = "num_vertices + num_edges", - }, - unavailable = { - num_precedences = "the exact target parameter is not represented by this reduction's symbolic transform", + num_precedences = "2 * num_edges", } )] impl ReduceTo diff --git a/src/rules/optimumcommunicationspanningtree_ilp.rs b/src/rules/optimumcommunicationspanningtree_ilp.rs index ee0c48bb7..b586d3211 100644 --- a/src/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/rules/optimumcommunicationspanningtree_ilp.rs @@ -46,15 +46,15 @@ impl ReductionResult for ReductionOptimumCommunicationSpanningTreeToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + 2 * num_edges * num_vertices * (num_vertices - 1) / 2", num_constraints = "1 + num_vertices * num_vertices * (num_vertices - 1) / 2 + 2 * num_edges * num_vertices * (num_vertices - 1) / 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_edges + 2 * num_edges * num_vertices * (num_vertices - 1) / 2) * (1 + num_vertices * num_vertices * (num_vertices - 1) / 2 + 2 * num_edges * num_vertices * (num_vertices - 1) / 2)", + }, +})] impl ReduceTo> for OptimumCommunicationSpanningTree { type Result = ReductionOptimumCommunicationSpanningTreeToILP; diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index b5f455827..6e51ae1a3 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -37,15 +37,11 @@ impl ReductionResult for ReductionPaintShopToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_cars + 2 * num_sequence", - num_constraints = "num_sequence + 2 * num_sequence", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_cars + 2 * num_sequence", + num_constraints = "num_sequence + 2 * num_sequence", + num_nonzeros = "(num_cars + 2 * num_sequence) * (num_sequence + 2 * num_sequence)", +})] impl ReduceTo> for PaintShop { type Result = ReductionPaintShopToILP; diff --git a/src/rules/paintshop_qubo.rs b/src/rules/paintshop_qubo.rs index b8a05456c..fd6bbeced 100644 --- a/src/rules/paintshop_qubo.rs +++ b/src/rules/paintshop_qubo.rs @@ -38,8 +38,13 @@ impl ReductionResult for ReductionPaintShopToQUBO { } } -#[reduction(transform = exact { - num_vars = "num_cars", +#[reduction(transform = { + exact { + num_vars = "num_cars", + }, + upper_bound { + num_quadratic_terms = "num_cars * (num_cars - 1) / 2", + }, })] impl ReduceTo> for PaintShop { type Result = ReductionPaintShopToQUBO; diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index ecfef47b6..546d20027 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -31,15 +31,15 @@ impl ReductionResult for ReductionPOKToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_items", num_constraints = "num_precedences + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_items * (num_precedences + 1)", + }, +})] impl ReduceTo> for PartiallyOrderedKnapsack { type Result = ReductionPOKToILP; diff --git a/src/rules/partition_multiprocessorscheduling.rs b/src/rules/partition_multiprocessorscheduling.rs index 4aa12595b..6ecdc103c 100644 --- a/src/rules/partition_multiprocessorscheduling.rs +++ b/src/rules/partition_multiprocessorscheduling.rs @@ -56,9 +56,7 @@ impl crate::rules::AggregateReductionResult for ReductionPartitionToMPS {} #[reduction( transform = exact { num_tasks = "num_elements", - }, - unavailable = { - num_processors = "the exact target parameter is not represented by this reduction's symbolic transform", + num_processors = "2", } )] impl ReduceTo for Partition { diff --git a/src/rules/partitionintocliques_ilp.rs b/src/rules/partitionintocliques_ilp.rs index f15013c49..7ef0c820d 100644 --- a/src/rules/partitionintocliques_ilp.rs +++ b/src/rules/partitionintocliques_ilp.rs @@ -49,15 +49,11 @@ impl ReductionResult for ReductionPartitionIntoCliquesToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPartitionIntoCliquesToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices^2", - num_constraints = "num_vertices + num_vertices^3", - }, - unavailable = { - num_nonzeros = "the exact target parameter depends on the source clique bound and non-edges", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2", + num_constraints = "num_vertices + num_vertices^3", + num_nonzeros = "(num_vertices^2) * (num_vertices + num_vertices^3)", +})] impl ReduceTo> for PartitionIntoCliques { type Result = ReductionPartitionIntoCliquesToILP; diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index d08b94113..30b32901d 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -67,15 +67,11 @@ impl ReductionResult for ReductionPIPL2ToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPIPL2ToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices^2 + num_edges * num_vertices", - num_constraints = "num_vertices^2 + num_edges * num_vertices + num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2 + num_edges * num_vertices", + num_constraints = "num_vertices^2 + num_edges * num_vertices + num_vertices", + num_nonzeros = "(num_vertices^2 + num_edges * num_vertices) * (num_vertices^2 + num_edges * num_vertices + num_vertices)", +})] impl ReduceTo> for PartitionIntoPathsOfLength2 { type Result = ReductionPIPL2ToILP; diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index 644eb9458..3ec37775c 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -60,15 +60,11 @@ impl ReductionResult for ReductionPITToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPITToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices^2", - num_constraints = "num_vertices^2 * num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2", + num_constraints = "num_vertices^2 * num_vertices", + num_nonzeros = "(num_vertices^2) * (num_vertices^2 * num_vertices)", +})] impl ReduceTo> for PartitionIntoTriangles { type Result = ReductionPITToILP; diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index bf9b50ebd..635030112 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -3,7 +3,7 @@ //! One integer variable per prescribed path. Arc capacity aggregation //! across paths and total flow requirement. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PathConstrainedNetworkFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,15 +40,11 @@ impl ReductionResult for ReductionPCNFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPCNFToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_paths", - num_constraints = "num_arcs + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_paths", + num_constraints = "num_arcs + 1", + num_nonzeros = "num_paths * (num_arcs + 1)", +})] impl ReduceTo> for PathConstrainedNetworkFlow { type Result = ReductionPCNFToILP; @@ -75,8 +71,15 @@ impl ReduceTo> for PathConstrainedNetworkFlow { let total_terms: Vec<(usize, i64)> = (0..num_paths).map(|i| (i, 1)).collect(); constraints.push(LinearConstraint::ge(total_terms, self.requirement())); + let variables = self + .paths() + .iter() + .map(|path| IntegerVariable::new(Some(0), Some(self.path_bottleneck(path)))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionPCNFToILP { - target: ILP::new(num_paths, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, }) } diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index 4de993d6f..5416b8e6d 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -61,15 +61,15 @@ impl ReductionResult for ReductionPCSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPCSToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * deadline", num_constraints = "num_tasks + deadline + num_precedences", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * deadline) * (num_tasks + deadline + num_precedences)", + }, +})] impl ReduceTo> for PrecedenceConstrainedScheduling { type Result = ReductionPCSToILP; diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index f6d65c971..badad6f82 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -21,7 +21,7 @@ //! Note: `ILP` treats all variables as non-negative integers. Binary constraints //! on x_{t,u} are enforced by x_{t,u} ≤ 1. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PreemptiveScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -67,15 +67,15 @@ impl ReductionResult for ReductionPSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * d_max + 1", num_constraints = "num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * d_max + 1) * (num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max)", + }, +})] impl ReduceTo> for PreemptiveScheduling { type Result = ReductionPSToILP; @@ -149,9 +149,22 @@ impl ReduceTo> for PreemptiveScheduling { // Objective: minimize M let objective = vec![(m_var, 1)]; + // All task slots end by d; lowering the makespan to d preserves every feasible schedule. + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m_var] = IntegerVariable::new( + Some(0), + Some(Self::exact_i64(d, "bounding the schedule makespan")?), + ) + .map_err(Self::target_construction)?; + Ok(ReductionPSToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_tasks: n, d_max: d, }) diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index aacef5d4e..f1585eb7d 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -50,12 +50,10 @@ impl ReductionResult for ReductionQAPToILP { } #[reduction( - transform = upper_bound { - num_vars = "num_facilities * num_locations + num_facilities^2 * num_locations^2", - num_constraints = "num_facilities + num_locations + 3 * num_facilities^2 * num_locations^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + transform = exact { + num_vars = "num_facilities * num_locations + num_facilities * (num_facilities - 1) * num_locations^2", + num_constraints = "num_facilities + num_locations + 3 * num_facilities * (num_facilities - 1) * num_locations^2", + num_nonzeros = "2 * num_facilities * num_locations + 7 * num_facilities * (num_facilities - 1) * num_locations^2", } )] impl ReduceTo> for QuadraticAssignment { diff --git a/src/rules/qubo_casts.rs b/src/rules/qubo_casts.rs index bef444980..0593cc879 100644 --- a/src/rules/qubo_casts.rs +++ b/src/rules/qubo_casts.rs @@ -8,7 +8,7 @@ use crate::types::i64_to_exact_f64; impl_variant_reduction!( QUBO, => , - fields: [num_vars], + fields: [num_vars, num_quadratic_terms], |src| { let entries = src .entries() diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index e08a10368..9ed5218dc 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -90,15 +90,14 @@ where }) } +#[rustfmt::skip] macro_rules! impl_qubo_to_ilp { ($coefficient:ty) => { #[reduction( - transform = upper_bound { - num_vars = "num_vars^2 + num_vars", - num_constraints = "3 * num_vars^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + transform = exact { + num_vars = "num_vars + num_quadratic_terms", + num_constraints = "3 * num_quadratic_terms", + num_nonzeros = "7 * num_quadratic_terms", } )] impl ReduceTo> for QUBO<$coefficient> { @@ -108,7 +107,7 @@ macro_rules! impl_qubo_to_ilp { reduce_qubo(self) } } - } + }; } impl_qubo_to_ilp!(i64); diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index 78334ddf4..751aee09e 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -39,15 +39,11 @@ impl ReductionResult for ReductionRPCToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRPCToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_rows^2 * num_cols^2", - num_constraints = "num_rows * num_cols + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_rows^2 * num_cols^2", + num_constraints = "num_rows * num_cols + 1", + num_nonzeros = "(num_rows^2 * num_cols^2) * (num_rows * num_cols + 1)", +})] impl ReduceTo> for RectilinearPictureCompression { type Result = ReductionRPCToILP; diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index 447c376c8..c5595f662 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -7,7 +7,7 @@ //! - binary threshold/live indicators to count how many values are live after //! each evaluation step -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::RegisterSufficiency; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -48,10 +48,8 @@ impl crate::rules::AggregateReductionResult for ReductionRegisterSufficiencyToIL transform = exact { num_vars = "3 * num_vertices^2 + num_vertices * (num_vertices - 1) / 2 + 2 * num_vertices", num_constraints = "9 * num_vertices^2 + 3 * num_vertices * (num_vertices - 1) / 2 + 3 * num_vertices + 2 * num_arcs + num_sinks", + num_nonzeros = "18 * num_vertices^2 + 2 * num_vertices + 7 * num_vertices * (num_vertices - 1) / 2 + 4 * num_arcs + num_sinks", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for RegisterSufficiency { type Result = ReductionRegisterSufficiencyToILP; @@ -186,8 +184,16 @@ impl ReduceTo> for RegisterSufficiency { )); } + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[time_offset..latest_offset].fill( + IntegerVariable::new(Some(0), Some(maximum_time)).map_err(Self::target_construction)?, + ); + variables[latest_offset..order_offset].fill( + IntegerVariable::new(Some(0), Some(latest_time)).map_err(Self::target_construction)?, + ); + Ok(ReductionRegisterSufficiencyToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_vertices: n, }) diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 7247687d4..4c46f27d5 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -16,8 +16,7 @@ pub struct UnavailableParameterField { /// Raw symbolic declaration emitted by the reduction proc macro. #[derive(Clone, Debug, Default)] pub struct ReductionParameterDeclarations { - pub relation: Option, - pub fields: Vec<(&'static str, Expr)>, + pub fields: Vec<(&'static str, ParameterRelation, Expr)>, pub unavailable: Vec, } @@ -37,7 +36,7 @@ impl ReductionParameterContract { let formula_names: HashSet<_> = declarations .fields .iter() - .map(|(field, _)| *field) + .map(|(field, _, _)| *field) .collect(); let mut unavailable_names = HashSet::new(); for unavailable in &declarations.unavailable { @@ -56,16 +55,13 @@ impl ReductionParameterContract { }); } } - let transform = match (declarations.relation, declarations.fields.is_empty()) { - (Some(relation), false) => Some(ParameterTransform::new( - edge, - relation, - declarations.fields, - )?), - (None, true) if !declarations.unavailable.is_empty() => None, - (None, true) => return Err(ParameterContractError::EmptyContract { edge }), - (Some(_), true) => return Err(ParameterContractError::EmptyTransform { edge }), - (None, false) => return Err(ParameterContractError::MissingRelation { edge }), + let transform = if declarations.fields.is_empty() { + if declarations.unavailable.is_empty() { + return Err(ParameterContractError::EmptyContract { edge }); + } + None + } else { + Some(ParameterTransform::from_fields(edge, declarations.fields)?) }; Ok(Self { transform, @@ -86,8 +82,6 @@ impl ReductionParameterContract { pub enum ParameterContractError { Transform(ParameterTransformError), EmptyContract { edge: Box }, - EmptyTransform { edge: Box }, - MissingRelation { edge: Box }, DuplicateClassification { edge: Box, field: Box }, EmptyUnavailableReason { edge: Box, field: Box }, } @@ -100,16 +94,6 @@ impl std::fmt::Display for ParameterContractError { formatter, "reduction `{edge}` has no parameter formulas or unavailable fields" ), - Self::EmptyTransform { edge } => { - write!( - formatter, - "reduction `{edge}` declares an empty parameter transform" - ) - } - Self::MissingRelation { edge } => write!( - formatter, - "reduction `{edge}` declares parameter formulas without a relation" - ), Self::DuplicateClassification { edge, field } => { write!( formatter, @@ -353,7 +337,7 @@ pub fn validate_reduction_parameter_schemas() -> Result<(), Vec> { for field in declarations .fields .iter() - .flat_map(|(_, expression)| expression.variables()) + .flat_map(|(_, _, expression)| expression.variables()) { if !source_fields.contains(field) { errors.push(format!( @@ -366,7 +350,7 @@ pub fn validate_reduction_parameter_schemas() -> Result<(), Vec> { let declared_target_fields = declarations .fields .iter() - .map(|(field, _)| *field) + .map(|(field, _, _)| *field) .chain(declarations.unavailable.iter().map(|field| field.field)) .collect::>(); for field in &declared_target_fields { diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index 99b8510ad..37ef1f54f 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -52,15 +52,15 @@ impl ReductionResult for ReductionRCSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRCSToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * deadline", num_constraints = "num_tasks + deadline + num_resources * deadline", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * deadline) * (num_tasks + deadline + num_resources * deadline)", + }, +})] impl ReduceTo> for ResourceConstrainedScheduling { type Result = ReductionRCSToILP; diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index 0ca97f3a7..dce49eff9 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -4,7 +4,7 @@ //! a_{u,v}, transitive-closure helpers h_{u,v,w}, and per-subset gadgets //! (top/bottom selectors, pair selectors, endpoint depths, extension costs). -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; @@ -90,15 +90,11 @@ impl ReductionResult for ReductionRTSAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRTSAToILP {} -#[reduction( - transform = upper_bound { - num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)", - num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)", + num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", + num_nonzeros = "(universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)) * (4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8))", +})] impl ReduceTo> for RootedTreeStorageAssignment { type Result = ReductionRTSAToILP; @@ -391,7 +387,13 @@ impl ReduceTo> for RootedTreeStorageAssignment { let cost_terms: Vec<(usize, i64)> = (0..r).map(|s| (idx_c(n, r, s), 1)).collect(); constraints.push(LinearConstraint::le(cost_terms, bound)); - let target = ILP::new(nv, constraints, vec![], ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); nv]; + let depth_domain = + IntegerVariable::new(Some(0), Some(big_m_depth)).map_err(Self::target_construction)?; + variables[n * n..n * n + n].fill(depth_domain); + variables[idx_big_t(n, r, 0)..].fill(depth_domain); + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionRTSAToILP { target, n }) } diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index ad8e63a1d..f056d2f7a 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -4,7 +4,7 @@ //! connectivity flow constraints to encode an Eulerian connected subgraph //! covering all required edges within the length bound. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::RuralPostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,15 +40,15 @@ impl ReductionResult for ReductionRPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + num_vertices + num_edges + num_vertices + 2 * num_edges", num_constraints = "2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_edges + num_vertices + num_edges + num_vertices + 2 * num_edges) * (2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices)", + }, +})] impl ReduceTo> for RuralPostman { type Result = ReductionRPToILP; @@ -204,8 +204,22 @@ impl ReduceTo> for RuralPostman { let objective: Vec<(usize, i64)> = (0..m) .map(|e| (t_idx(e), edge_lengths[e].to_sum())) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[..m] + .fill(IntegerVariable::new(Some(0), Some(2)).map_err(Self::target_construction)?); + variables[m..m + n].fill( + IntegerVariable::new( + Some(0), + Some(Self::exact_i64(m, "bounding half the traversal degree")?), + ) + .map_err(Self::target_construction)?, + ); + variables[2 * m + 2 * n..] + .fill(IntegerVariable::new(Some(0), Some(big_m)).map_err(Self::target_construction)?); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionRPToILP { target, diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index caefabe83..9b5aaf957 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -5,7 +5,7 @@ //! ordering variables `y_{i,j}` for each task pair. Big-M constraints //! enforce that tasks sharing a processor do not overlap. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; @@ -62,15 +62,15 @@ impl ReductionResult for ReductionSMWCTToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_processors + num_tasks + num_tasks * (num_tasks - 1) / 2", num_constraints = "num_tasks + num_tasks * num_processors + 2 * num_tasks + 2 * num_tasks * (num_tasks - 1) / 2 * num_processors + num_tasks * (num_tasks - 1) / 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_processors + num_tasks + num_tasks * (num_tasks - 1) / 2) * (num_tasks + num_tasks * num_processors + 2 * num_tasks + 2 * num_tasks * (num_tasks - 1) / 2 * num_processors + num_tasks * (num_tasks - 1) / 2)", + }, +})] impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { type Result = ReductionSMWCTToILP; @@ -190,9 +190,20 @@ impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { .map(|(task, weight)| (result.c_var(task), weight)) .collect(); - Ok(ReductionSMWCTToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[n * m..n * m + n].fill( + IntegerVariable::new(Some(0), Some(total_processing_time)) .map_err(Self::target_construction)?, + ); + + Ok(ReductionSMWCTToILP { + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_tasks: n, num_processors: m, }) diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index c073ba6c1..034bc4c89 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -57,15 +57,15 @@ impl ReductionResult for ReductionSWIDToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWIDToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * max_deadline", num_constraints = "num_tasks + max_deadline + num_precedences + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * max_deadline) * (num_tasks + max_deadline + num_precedences + 1)", + }, +})] impl ReduceTo> for SchedulingWithIndividualDeadlines { type Result = ReductionSWIDToILP; diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 1c87717e3..2e5bc5947 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -4,7 +4,7 @@ //! Permutation constraints, precedence constraints, and prefix cumulative-cost //! bounds at every position. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeMaximumCumulativeCost; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; @@ -45,14 +45,15 @@ impl ReductionResult for ReductionSTMMCCToILP { } } -#[reduction(transform = exact { - num_vars = "num_tasks^2 + 1", - num_constraints = "num_tasks^2 + 3 * num_tasks + num_precedences + 1", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = { + exact { + num_vars = "num_tasks^2 + 1", + num_constraints = "num_tasks^2 + 3 * num_tasks + num_precedences + 1", + }, + upper_bound { + num_nonzeros = "(num_tasks^2 + 1) * (num_tasks^2 + 3 * num_tasks + num_precedences + 1)", + }, +})] impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { type Result = ReductionSTMMCCToILP; @@ -130,9 +131,18 @@ impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { // Objective: minimize z (the maximum cumulative cost) let objective = vec![(z_var, 1)]; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[z_var] = + IntegerVariable::new(Some(0), Some(z_upper)).map_err(Self::target_construction)?; + Ok(ReductionSTMMCCToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_tasks: n, }) } diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index 4bf8c68f0..b2f3c8a0a 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -46,15 +46,15 @@ impl ReductionResult for ReductionSTMTTWToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + 2 * num_tasks * num_tasks", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + 2 * num_tasks * num_tasks)", + }, +})] impl ReduceTo> for SequencingToMinimizeTardyTaskWeight { type Result = ReductionSTMTTWToILP; diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 1171c160a..d0e1f4a4e 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -5,7 +5,7 @@ //! For each unordered pair `{i, j}`, a pair of big-M constraints forces one //! task to finish before the other starts. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -51,15 +51,15 @@ impl ReductionResult for ReductionSTMWCTToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks + num_tasks * (num_tasks - 1) / 2", num_constraints = "2 * num_tasks + 3 * num_tasks * (num_tasks - 1) / 2 + num_precedences", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks + num_tasks * (num_tasks - 1) / 2) * (2 * num_tasks + 3 * num_tasks * (num_tasks - 1) / 2 + num_precedences)", + }, +})] impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { type Result = ReductionSTMWCTToILP; @@ -132,9 +132,20 @@ impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { let objective = weights.iter().copied().enumerate().collect(); - Ok(Self::Result { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[..num_tasks].fill( + IntegerVariable::new(Some(0), Some(total_processing_time)) .map_err(Self::target_construction)?, + ); + + Ok(Self::Result { + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_tasks, }) } diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index feb90dd53..de1d41b38 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -4,7 +4,7 @@ //! and nonnegative tardiness variables T_j. Big-M disjunctive constraints //! force a single-machine order; the weighted tardiness sum is bounded by K. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedTardiness; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -57,15 +57,11 @@ impl ReductionResult for ReductionSTMWTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSTMWTToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "num_tasks^2 + 2 * num_tasks", num_constraints = "2 * num_tasks^2 + 3 * num_tasks + 1", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(num_tasks^2 + 2 * num_tasks) * (2 * num_tasks^2 + 3 * num_tasks + 1)", +})] impl ReduceTo> for SequencingToMinimizeWeightedTardiness { type Result = ReductionSTMWTToILP; @@ -156,8 +152,13 @@ impl ReduceTo> for SequencingToMinimizeWeightedTardiness { let terms: Vec<(usize, i64)> = (0..n).map(|j| (t_var(j), weights[j])).collect(); constraints.push(LinearConstraint::le(terms, bound)); + // Left-justify the job order: completion and tardiness are <= horizon, with no increase in cost. + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_order_vars..] + .fill(IntegerVariable::new(Some(0), Some(horizon)).map_err(Self::target_construction)?); + Ok(ReductionSTMWTToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_tasks: n, num_order_vars, diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 85dc2be3e..2829801bf 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -58,15 +58,11 @@ impl ReductionResult for ReductionSWDSTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWDSTToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "2 * num_tasks^2 + num_tasks", num_constraints = "2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(2 * num_tasks^2 + num_tasks) * (2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks)", +})] impl ReduceTo> for SequencingWithDeadlinesAndSetUpTimes { type Result = ReductionSWDSTToILP; diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index 663628d5f..4182c20a7 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -76,15 +76,11 @@ impl ReductionResult for ReductionSWIToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWIToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_start_slots", - num_constraints = "num_start_slots^2 + num_tasks", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_start_slots", + num_constraints = "num_start_slots^2 + num_tasks", + num_nonzeros = "num_start_slots * (num_start_slots^2 + num_tasks)", +})] impl ReduceTo> for SequencingWithinIntervals { type Result = ReductionSWIToILP; diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 91c5d8109..6d001eca3 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -58,15 +58,11 @@ impl ReductionResult for ReductionSWRTDToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWRTDToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "num_tasks * time_horizon", num_constraints = "num_tasks * time_horizon + num_tasks + time_horizon", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(num_tasks * time_horizon) * (num_tasks * time_horizon + num_tasks + time_horizon)", +})] impl ReduceTo> for SequencingWithReleaseTimesAndDeadlines { type Result = ReductionSWRTDToILP; diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index ecccd257b..ef0667f47 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -46,15 +46,15 @@ impl ReductionResult for ReductionSetSplittingToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSetSplittingToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "universe_size", num_constraints = "2 * num_subsets", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "universe_size * (2 * num_subsets)", + }, +})] impl ReduceTo> for SetSplitting { type Result = ReductionSetSplittingToILP; diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index d1c713717..784371a04 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -45,15 +45,11 @@ impl ReductionResult for ReductionSCSToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "max_length * (alphabet_size + 1) + total_length * max_length", - num_constraints = "max_length + total_length + total_length * max_length + total_length + max_length", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "max_length * (alphabet_size + 1) + total_length * max_length", + num_constraints = "max_length + total_length + total_length * max_length + total_length + max_length", + num_nonzeros = "(max_length * (alphabet_size + 1) + total_length * max_length) * (max_length + total_length + total_length * max_length + total_length + max_length)", +})] impl ReduceTo> for ShortestCommonSupersequence { type Result = ReductionSCSToILP; diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index fe8ff92be..c94aab980 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -6,7 +6,7 @@ //! bound constraint enforces the weight limit, and the objective minimizes //! total path length. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::ShortestWeightConstrainedPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -57,15 +57,15 @@ impl ReductionResult for ReductionSWCPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_edges + num_vertices", num_constraints = "5 * num_edges + 4 * num_vertices + 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 2)", + }, +})] impl ReduceTo> for ShortestWeightConstrainedPath { type Result = ReductionSWCPToILP; @@ -213,8 +213,14 @@ impl ReduceTo> for ShortestWeightConstrainedPath { ] }) .collect(); - let target_ilp = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[2 * num_edges..].fill( + IntegerVariable::new(Some(0), Some(max_order)).map_err(Self::target_construction)?, + ); + + let target_ilp = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionSWCPToILP { target: target_ilp, diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index b03552ee3..6b493c63b 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -45,15 +45,11 @@ impl ReductionResult for ReductionSMCToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSMCToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_rows * bound_k", - num_constraints = "num_rows + num_rows^2 * num_cols^2 * bound_k", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_rows * bound_k", + num_constraints = "num_rows + num_rows^2 * num_cols^2 * bound_k", + num_nonzeros = "(num_rows * bound_k) * (num_rows + num_rows^2 * num_cols^2 * bound_k)", +})] impl ReduceTo> for SparseMatrixCompression { type Result = ReductionSMCToILP; diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 31d1c46d3..9fbaf246a 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -39,7 +39,7 @@ impl ReductionResult for ReductionQUBOToSG { #[reduction( transform = exact { num_spins = "num_vars", - num_interactions = "num_vars^2", + num_interactions = "num_quadratic_terms", }, )] impl ReduceTo> for QUBO { @@ -63,10 +63,6 @@ impl ReduceTo> for QUBO { let mut onsite = vec![0.0; n]; for &(i, j, q) in self.entries() { - if q.abs() < 1e-10 { - continue; - } - if i == j { // Diagonal: Q_ii * x_i = Q_ii/2 * s_i + Q_ii/2 (constant) onsite[i] += q / 2.0; @@ -74,9 +70,7 @@ impl ReduceTo> for QUBO { // Off-diagonal: Q_ij * x_i * x_j // J_ij contribution let j_ij = q / 4.0; - if j_ij.abs() > 1e-10 { - interactions.push(((i, j), j_ij)); - } + interactions.push(((i, j), j_ij)); // h_i and h_j contributions onsite[i] += q / 4.0; onsite[j] += q / 4.0; @@ -126,11 +120,14 @@ where } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_spins", - } -)] + }, + upper_bound { + num_quadratic_terms = "num_spins * (num_spins - 1) / 2", + }, +})] impl ReduceTo> for SpinGlass { type Result = ReductionSGToQUBO; @@ -170,11 +167,14 @@ impl ReduceTo> for SpinGlass { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_spins", - } -)] + }, + upper_bound { + num_quadratic_terms = "num_spins * (num_spins - 1) / 2", + }, +})] impl ReduceTo> for SpinGlass { type Result = ReductionSGToQUBO; diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index 5e9a0d7d9..d4e15609a 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -44,15 +44,11 @@ impl ReductionResult for ReductionSCToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_arcs * num_arcs + num_arcs * num_arcs * num_arcs", - num_constraints = "num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_arcs * num_arcs + num_arcs * num_arcs * num_arcs", + num_constraints = "num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs", + num_nonzeros = "(num_arcs * num_arcs + num_arcs * num_arcs * num_arcs) * (num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs)", +})] impl ReduceTo> for StackerCrane { type Result = ReductionSCToILP; diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index 0bfa3f98d..2dbc30918 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionSteinerTreeToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + num_vertices + 2 * num_edges * (num_vertices - 1)", num_constraints = "num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_edges + num_vertices + 2 * num_edges * (num_vertices - 1)) * (num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1)", + }, +})] impl ReduceTo> for SteinerTree { type Result = ReductionSteinerTreeToILP; diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index aa4e22978..1f99ce66c 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -115,15 +115,11 @@ impl ReductionResult for ReductionSTSCToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSTSCToILP {} -#[reduction( - transform = upper_bound { - num_vars = "(bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound", - num_constraints = "4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "(bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound", + num_constraints = "4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1", + num_nonzeros = "((bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound) * (4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1)", +})] impl ReduceTo> for StringToStringCorrection { type Result = ReductionSTSCToILP; diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 00fbacf36..5fddf756f 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -4,7 +4,7 @@ //! sending flow both from a root to every vertex and back again. //! See the paper entry for the full formulation. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::StrongConnectivityAugmentation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -44,15 +44,11 @@ impl ReductionResult for ReductionSCAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSCAToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)", - num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)", + num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", + num_nonzeros = "(num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)) * (1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices)", +})] impl ReduceTo> for StrongConnectivityAugmentation { type Result = ReductionSCAToILP; @@ -181,7 +177,10 @@ impl ReduceTo> for StrongConnectivityAugmentation { } } - let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + // Each connectivity certificate can be a simple unit-flow path; cycles are unnecessary. + let variables = vec![IntegerVariable::binary(); num_vars]; + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionSCAToILP { target, diff --git a/src/rules/subgraphisomorphism_ilp.rs b/src/rules/subgraphisomorphism_ilp.rs index 7237dab2b..55a07a90b 100644 --- a/src/rules/subgraphisomorphism_ilp.rs +++ b/src/rules/subgraphisomorphism_ilp.rs @@ -57,15 +57,11 @@ impl ReductionResult for ReductionSubIsoToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSubIsoToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_pattern_vertices * num_host_vertices", - num_constraints = "num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_pattern_vertices * num_host_vertices", + num_constraints = "num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2", + num_nonzeros = "(num_pattern_vertices * num_host_vertices) * (num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2)", +})] impl ReduceTo> for SubgraphIsomorphism { type Result = ReductionSubIsoToILP; diff --git a/src/rules/subsetsum_integerknapsack.rs b/src/rules/subsetsum_integerknapsack.rs index b715ca836..3d4adea58 100644 --- a/src/rules/subsetsum_integerknapsack.rs +++ b/src/rules/subsetsum_integerknapsack.rs @@ -32,8 +32,8 @@ inventory::submit! { source_variant_fn: ::variant, target_variant_fn: ::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), - fields: vec![("num_items", Expr::variable("num_elements"))], + + fields: vec![("num_items", crate::parameters::ParameterRelation::Exact, Expr::variable("num_elements"))], unavailable: vec![crate::rules::registry::UnavailableParameterField { field: "capacity", reason: "the target capacity equals the SubsetSum target, which is not a registered source parameter", diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index 1d82db321..57ad1b258 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -76,10 +76,8 @@ impl ReductionResult for ReductionSSPToILP { transform = exact { num_vars = "num_elements * num_groups + num_elements^2 * num_groups", num_constraints = "num_elements + 3 * num_elements^2 * num_groups", + num_nonzeros = "7 * num_elements^2 * num_groups", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for SumOfSquaresPartition { type Result = ReductionSSPToILP; diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index b9a430bde..a7a325b9e 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -40,10 +40,8 @@ impl crate::rules::AggregateReductionResult for ReductionThreeDimensionalMatchin transform = exact { num_vars = "num_triples", num_constraints = "3 * universe_size", + num_nonzeros = "3 * num_triples", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for ThreeDimensionalMatching { type Result = ReductionThreeDimensionalMatchingToILP; diff --git a/src/rules/threepartition_resourceconstrainedscheduling.rs b/src/rules/threepartition_resourceconstrainedscheduling.rs index 48536d66c..7b4111b58 100644 --- a/src/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/rules/threepartition_resourceconstrainedscheduling.rs @@ -59,10 +59,8 @@ impl crate::rules::AggregateReductionResult for ReductionThreePartitionToRCS {} #[reduction( transform = exact { num_tasks = "num_elements", - }, - unavailable = { - deadline = "the exact target parameter is not represented by this reduction's symbolic transform", - num_resources = "the exact target parameter is not represented by this reduction's symbolic transform", + deadline = "num_groups", + num_resources = "1", } )] impl ReduceTo for ThreePartition { diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 4ddfe908f..0964aeae9 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -64,15 +64,11 @@ impl ReductionResult for ReductionTDToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionTDToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_craftsmen * num_tasks * num_periods", - num_constraints = "num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_craftsmen * num_tasks * num_periods", + num_constraints = "num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods", + num_nonzeros = "(num_craftsmen * num_tasks * num_periods) * (num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods)", +})] impl ReduceTo> for TimetableDesign { type Result = ReductionTDToILP; diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index ee89c2ec7..7f219ae13 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -65,15 +65,15 @@ impl ReductionResult for ReductionTSPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2 + 2 * num_vertices * num_edges", num_constraints = "num_vertices^3 + -1 * num_vertices^2 + 2 * num_vertices + 4 * num_vertices * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices^2 + 2 * num_vertices * num_edges) * (num_vertices^3 + -1 * num_vertices^2 + 2 * num_vertices + 4 * num_vertices * num_edges)", + }, +})] impl ReduceTo> for TravelingSalesman { type Result = ReductionTSPToILP; diff --git a/src/rules/travelingsalesman_qubo.rs b/src/rules/travelingsalesman_qubo.rs index 6a02a8e2b..2e3c66af6 100644 --- a/src/rules/travelingsalesman_qubo.rs +++ b/src/rules/travelingsalesman_qubo.rs @@ -123,11 +123,14 @@ impl crate::rules::AggregateReductionResult for ReductionTravelingSalesmanToQUBO } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2", - } -)] + }, + upper_bound { + num_quadratic_terms = "(num_vertices^2) * ((num_vertices^2) - 1) / 2", + }, +})] impl ReduceTo> for TravelingSalesman { type Result = ReductionTravelingSalesmanToQUBO; diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index d3310c430..b8cf95beb 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -23,7 +23,7 @@ //! //! Size upper bound: 3*|E| variables, 5*|E| + |V| + 1 constraints. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedFlowLowerBounds; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -78,15 +78,11 @@ impl ReductionResult for ReductionUFLBToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionUFLBToILP {} -#[reduction( - transform = upper_bound { - num_vars = "3 * num_edges", - num_constraints = "5 * num_edges + num_vertices + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "3 * num_edges", + num_constraints = "5 * num_edges + num_vertices + 1", + num_nonzeros = "(3 * num_edges) * (5 * num_edges + num_vertices + 1)", +})] impl ReduceTo> for UndirectedFlowLowerBounds { type Result = ReductionUFLBToILP; @@ -177,8 +173,17 @@ impl ReduceTo> for UndirectedFlowLowerBounds { } constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let mut variables = self + .capacities() + .iter() + .flat_map(|&capacity| std::iter::repeat_n(capacity, 2)) + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + variables.resize(num_vars, IntegerVariable::binary()); + Ok(ReductionUFLBToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_edges: e, }) diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 7cba64e21..61a69fa4d 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -24,7 +24,7 @@ //! //! Constraints per edge (7 per edge) + flow conservation (2 per non-terminal vertex) + net flow (2) -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -69,15 +69,15 @@ impl ReductionResult for ReductionU2CIFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionU2CIFToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "6 * num_edges", num_constraints = "7 * num_edges + num_conservation_constraints + 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(6 * num_edges) * (7 * num_edges + num_conservation_constraints + 2)", + }, +})] impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { type Result = ReductionU2CIFToILP; @@ -200,8 +200,17 @@ impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { } constraints.push(LinearConstraint::ge(sink2_terms, self.requirement_2())); + let mut variables = self + .capacities() + .iter() + .flat_map(|&capacity| std::iter::repeat_n(capacity, 4)) + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + variables.resize(num_vars, IntegerVariable::binary()); + Ok(ReductionU2CIFToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_edges: e, }) diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index 2c9539116..f76f651ff 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -95,6 +95,14 @@ fn test_num_variables() { assert_eq!(problem.num_variables(), 5); } +#[test] +fn quadratic_term_count_excludes_diagonal_coefficients() { + let problem = + QUBO::::from_entries(4, vec![(0, 0, 2.0), (0, 1, 3.0), (1, 3, -1.0), (3, 3, 4.0)]) + .unwrap(); + assert_eq!(problem.parameters().get("num_quadratic_terms"), Some(2)); +} + #[test] fn test_matrix_access() { let problem = QUBO::from_matrix(vec![ diff --git a/src/unit_tests/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs new file mode 100644 index 000000000..183c2111f --- /dev/null +++ b/src/unit_tests/parameter_formula_validation.rs @@ -0,0 +1,249 @@ +use crate::parameters::ParameterRelation; +use crate::registry::DynProblem; +use crate::rules::{registry::ReductionEntry, ReductionGraph}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; + +type SourceKey = (String, BTreeMap); + +#[test] +fn matrix_targets_with_dimension_predictions_also_bound_entries() { + for entry in crate::rules::registry::reduction_entries() { + let (dimensions, count): (&[&str], &str) = match entry.target_name { + "ILP" => (&["num_vars", "num_constraints"], "num_nonzeros"), + "QUBO" | "DecisionQUBO" => (&["num_vars"], "num_quadratic_terms"), + _ => continue, + }; + let contract = entry.parameter_contract().unwrap(); + let Some(transform) = contract.transform() else { + continue; + }; + if dimensions + .iter() + .all(|field| transform.get(field).is_some()) + { + assert!( + transform.get(count).is_some(), + "{} -> {} has dimension predictions but no {count} bound", + entry.source_name, + entry.target_name + ); + } + } +} + +fn canonical_sources() -> BTreeMap> { + let mut sources = BTreeMap::>::new(); + let db = crate::example_db::build_example_db().unwrap(); + for model in db.models { + sources + .entry((model.problem, model.variant)) + .or_default() + .push(model.instance); + } + for rule in db.rules { + let source = rule.source; + if let (Some(name), Some(inner)) = ( + source.problem.strip_prefix("Decision"), + source.instance.get("inner"), + ) { + sources + .entry((name.to_string(), source.variant.clone())) + .or_default() + .push(inner.clone()); + } + sources + .entry((source.problem, source.variant)) + .or_default() + .push(source.instance); + } + sources +} + +fn target_parameters( + entry: &ReductionEntry, + source: &dyn DynProblem, +) -> Result { + let variant = ReductionGraph::variant_to_map(&entry.target_variant()); + if let Some(reduce) = entry.reduce_fn { + let reduced = reduce(source.as_any()).map_err(|error| error.to_string())?; + return Ok(ReductionGraph::compute_problem_parameters( + entry.target_name, + &variant, + reduced.target_problem_any(), + )); + } + let source_json = source.serialize_json(); + let target_json = if entry.turing { + json!({"inner": source_json, "bound": 2}) + } else if entry.source_name == "MinimumVertexCover" + && entry.target_name == "MinimumMaximalMatching" + { + json!({"graph": source_json["graph"]}) + } else if entry.source_name == "SubsetSum" && entry.target_name == "IntegerKnapsack" { + let sizes: Vec = source_json["sizes"] + .as_array() + .ok_or("SubsetSum sizes are not an array")? + .iter() + .map(|item| { + item.as_str() + .ok_or("size is not a string")? + .parse() + .map_err(|error| format!("{error}")) + }) + .collect::>()?; + let capacity: i64 = source_json["target"] + .as_str() + .ok_or("target is not a string")? + .parse() + .map_err(|error| format!("{error}"))?; + json!({"sizes": sizes, "values": sizes, "capacity": capacity}) + } else { + return Err("no executable reduction or test construction".into()); + }; + let target = crate::registry::load_dyn(entry.target_name, &variant, target_json) + .map_err(|error| error.to_string())?; + Ok(target.parameters_dyn()) +} + +fn source_for( + entry: &ReductionEntry, + sources: &BTreeMap>, +) -> Result, String> { + let variant = ReductionGraph::variant_to_map(&entry.source_variant()); + let registered = crate::registry::find_variant_entry(entry.source_name, &variant).unwrap(); + let key = (entry.source_name.to_string(), variant.clone()); + if let Some(examples) = sources.get(&key) { + for example in examples { + if let Ok(source) = (registered.factory)(example.clone()) { + if target_parameters(entry, source.as_ref()).is_ok() { + return Ok(source); + } + } + } + } + if entry.source_name == "ILP" && variant.get("variable").is_some_and(|v| v == "i64") { + use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; + return Ok(Box::new( + ILP::::with_variables( + vec![IntegerVariable::new(Some(0), Some(3)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 3)], + vec![(0, 1)], + ObjectiveSense::Minimize, + ) + .unwrap(), + )); + } + let random = registered + .random + .ok_or_else(|| format!("no usable canonical source for {key:?}"))?; + let mut args = serde_json::Map::new(); + for input in (random.inputs)() { + let value = match input.name { + "num_vertices" => json!(5), + "seed" => json!(42), + "k" => json!(if variant.get("k").is_some_and(|v| v == "K3") { + 3 + } else { + 2 + }), + "bound" => json!(2), + _ if !input.required => continue, + name => return Err(format!("unsupported random input {name}")), + }; + args.insert(input.name.to_string(), value); + } + (random.generate)(Value::Object(args)).map_err(|error| error.to_string()) +} + +#[test] +fn integer_ilp_reductions_support_binary_encoding() { + use crate::models::algebraic::ILP; + use crate::rules::ReduceTo; + use crate::traits::Problem; + + let sources = canonical_sources(); + let mut failures = Vec::new(); + let mut checked = 0; + for entry in crate::rules::registry::reduction_entries() { + if entry.target_name != "ILP" || entry.target_variant() != ILP::::variant() { + continue; + } + let result = (|| { + let source = source_for(entry, &sources)?; + let reduced = + entry.reduce_fn.unwrap()(source.as_any()).map_err(|error| error.to_string())?; + let integer = reduced + .target_problem_any() + .downcast_ref::>() + .unwrap(); + ReduceTo::>::reduce_to(integer).map_err(|error| error.to_string())?; + Ok::<_, String>(()) + })(); + if let Err(error) = result { + failures.push(format!( + "{} {:?}: {error}", + entry.source_name, + entry.source_variant() + )); + } + checked += 1; + } + assert!(checked > 0); + assert!(failures.is_empty(), "{}", failures.join("\n")); +} + +#[test] +fn every_parameter_formula_matches_a_constructed_target() { + let sources = canonical_sources(); + let mut checked = 0; + let mut expected = 0; + let mut failures = Vec::new(); + for entry in crate::rules::registry::reduction_entries() { + let contract = entry.parameter_contract().unwrap(); + let Some(transform) = contract.transform() else { + continue; + }; + expected += transform.expressions().count(); + let label = format!( + "{} {:?} -> {} {:?}", + entry.source_name, + entry.source_variant(), + entry.target_name, + entry.target_variant() + ); + let result = (|| { + let source = source_for(entry, &sources)?; + let actual = target_parameters(entry, source.as_ref())?; + let predicted = transform + .evaluate(&source.parameters_dyn()) + .map_err(|error| error.to_string())?; + for (field, _) in transform.expressions() { + let valid = match (predicted.get(field), actual.get(field)) { + (Some(predicted), Some(actual)) => match transform.relation(field).unwrap() { + ParameterRelation::Exact => predicted == actual, + ParameterRelation::UpperBound => predicted >= actual, + }, + _ => false, + }; + if !valid { + return Err(format!( + "{field} ({:?}): predicted {:?}, measured {:?}; source {}", + transform.relation(field).unwrap(), + predicted.get(field), + actual.get(field), + source.serialize_json() + )); + } + checked += 1; + } + Ok::<_, String>(()) + })(); + if let Err(error) = result { + failures.push(format!("{label}: {error}")); + } + } + assert!(expected > 0); + assert!(failures.is_empty(), "{}", failures.join("\n")); + assert_eq!(checked, expected, "some formula fields were not checked"); +} diff --git a/src/unit_tests/parameters.rs b/src/unit_tests/parameters.rs index a98eb5dfc..908a9946c 100644 --- a/src/unit_tests/parameters.rs +++ b/src/unit_tests/parameters.rs @@ -2,6 +2,101 @@ use super::{ParameterRelation, ParameterTransform, ParameterTransformError}; use crate::expr::Expr; use crate::types::ProblemParameters; +#[test] +fn composition_preserves_fields_with_available_dependencies() { + let first = ParameterTransform::new( + "A -> B", + ParameterRelation::Exact, + [("variables", Expr::parse("n"))], + ) + .unwrap(); + let next = ParameterTransform::new( + "B -> C", + ParameterRelation::Exact, + [ + ("variables", Expr::parse("variables")), + ("terms", Expr::parse("terms")), + ], + ) + .unwrap(); + let composed = first.compose(&next, "A -> C").unwrap(); + assert_eq!( + composed + .evaluate(&ProblemParameters::new(vec![("n", 4)])) + .unwrap() + .get("variables"), + Some(4) + ); + assert!(composed.get("terms").is_none()); +} + +#[test] +fn composition_tracks_only_each_fields_dependencies() { + let first = ParameterTransform::from_fields( + "A -> B", + [ + ("vertices", ParameterRelation::Exact, Expr::parse("n")), + ("edges", ParameterRelation::UpperBound, Expr::parse("n^2")), + ], + ) + .unwrap(); + let next = ParameterTransform::new( + "B -> C", + ParameterRelation::Exact, + [ + ("vertices", Expr::parse("vertices")), + ("edges", Expr::parse("edges")), + ("reciprocal", Expr::parse("1 / edges")), + ], + ) + .unwrap(); + let composed = first.compose(&next, "A -> C").unwrap(); + assert_eq!( + composed.relation("vertices"), + Some(ParameterRelation::Exact) + ); + assert_eq!( + composed.relation("edges"), + Some(ParameterRelation::UpperBound) + ); + assert!(matches!( + composed.unavailable("reciprocal"), + Some(ParameterTransformError::CannotPropagateUpperBound { .. }) + )); + let values = composed + .evaluate(&ProblemParameters::new(vec![("n", 3)])) + .unwrap(); + assert_eq!(values.get("vertices"), Some(3)); + assert_eq!(values.get("edges"), Some(9)); +} + +#[test] +fn constants_survive_unavailable_inputs_and_dependent_fields_keep_the_cause() { + let mut first = + ParameterTransform::from_fields("A -> B", Vec::<(&str, ParameterRelation, Expr)>::new()) + .unwrap(); + first.declare_unavailable("size", "input magnitudes are not registered"); + let next = ParameterTransform::new( + "B -> C", + ParameterRelation::Exact, + [("fixed", Expr::parse("3")), ("size", Expr::parse("size"))], + ) + .unwrap(); + let composed = first.compose(&next, "A -> C").unwrap(); + assert_eq!( + composed + .evaluate(&ProblemParameters::default()) + .unwrap() + .get("fixed"), + Some(3) + ); + assert!( + matches!(composed.unavailable("size"), Some(ParameterTransformError::UnavailableInput { cause, .. }) + if matches!(cause.as_ref(), ParameterTransformError::Unavailable { edge, field, .. } + if edge.as_ref() == "A -> B" && field.as_ref() == "size")) + ); +} + #[test] fn exact_transform_evaluates_exactly() { let transform = ParameterTransform::new( @@ -31,7 +126,10 @@ fn upper_bound_relation_survives_evaluation_and_composition() { ) .unwrap(); let composed = first.compose(&second, "A -> C").unwrap(); - assert_eq!(composed.relation(), ParameterRelation::UpperBound); + assert_eq!( + composed.relation("k").unwrap(), + ParameterRelation::UpperBound + ); let result = composed .evaluate(&ProblemParameters::new(vec![("n", 4)])) .unwrap(); @@ -109,10 +207,12 @@ fn symbolic_upper_bound_composition_rejects_non_polynomial_formulas() { ) .unwrap(); + let composed = bounded_transform.compose(&reciprocal, "A -> C").unwrap(); assert!(matches!( - bounded_transform.compose(&reciprocal, "A -> C"), - Err(ParameterTransformError::CannotPropagateUpperBound { .. }) + composed.unavailable("k"), + Some(ParameterTransformError::CannotPropagateUpperBound { .. }) )); + assert!(composed.get("k").is_none()); } #[test] diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index ce165c6db..0370dbb4b 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -99,7 +99,7 @@ fn exact_rule_exposes_one_transform() { .unwrap() .unwrap(); assert_eq!( - transform.relation(), + transform.relation("num_vertices").unwrap(), crate::parameters::ParameterRelation::Exact ); } @@ -1080,8 +1080,11 @@ fn test_find_paths_bounded_returns_shortest_when_truncated() { parameter_contract: ReductionParameterContract::new( "synthetic edge", ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), - fields: vec![("n", Expr::variable("n"))], + fields: vec![( + "n", + crate::parameters::ParameterRelation::Exact, + Expr::variable("n"), + )], unavailable: vec![], }, ), diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 013856e87..a2a8912d6 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -26,7 +26,6 @@ fn empty_parameter_contract() -> Result Reductio parameter_contract: ReductionParameterContract::new( "synthetic edge", ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), fields: fields .iter() - .map(|(field, expression)| (*field, Expr::try_parse(expression).unwrap())) + .map(|(field, expression)| { + ( + *field, + crate::parameters::ParameterRelation::Exact, + Expr::try_parse(expression).unwrap(), + ) + }) .collect(), unavailable: vec![], }, @@ -653,10 +657,10 @@ fn path_parameter_contract_errors_are_typed_and_isolated() { turing: false, }, ); - assert!(matches!( - unavailable.path_parameter_transforms(&disconnected), - Err(PathParameterError::Unavailable { .. }) - )); + let transforms = unavailable + .path_parameter_transforms(&disconnected) + .unwrap(); + assert!(transforms[0].unavailable("size").is_some()); let invalid_contract = Err(ParameterContractError::EmptyUnavailableReason { edge: "A -> B".into(), @@ -720,9 +724,13 @@ fn path_size_composition_and_contract_evaluation_report_errors() { ], ); let chained = named_path(&["A", "B", "C"]); + let composed = invalid_composition + .compose_path_parameter_transform(&chained) + .unwrap() + .unwrap(); assert!(matches!( - invalid_composition.compose_path_parameter_transform(&chained), - Err(PathParameterError::Step { .. }) + composed.unavailable("z"), + Some(crate::parameters::ParameterTransformError::MissingCompositionInput { .. }) )); let valid = ReductionGraph::from_test_edges( @@ -1932,7 +1940,7 @@ fn parameter_contract_variables_are_registered_source_fields() { let input_vars: std::collections::HashSet<_> = declarations .fields .iter() - .flat_map(|(_, expression)| expression.variables()) + .flat_map(|(_, _, expression)| expression.variables()) .collect(); if input_vars.is_empty() { continue; diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index 08300e2c8..3f89c648d 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -3,6 +3,62 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense}; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +#[test] +fn parameter_bounds_cover_slack_boundaries_and_cancellation() { + use crate::parameters::ParameterRelation; + use crate::traits::Problem; + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == "ILP" + && entry.target_name == "QUBO" + && entry.source_variant() == ILP::::variant() + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract + .transform() + .expect("ILP must predict QUBO size bounds"); + let mut cases = vec![ + ILP::::new(0, vec![], vec![], ObjectiveSense::Minimize).unwrap(), + ILP::::new( + 2, + vec![ + LinearConstraint::eq(vec![(0, 1), (1, 1)], 0), + LinearConstraint::eq(vec![(0, 1), (1, -1)], 0), + ], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(), + ]; + for range in [0, 1, 2, 3, 4, 7, 8] { + for constraint in [ + LinearConstraint::le(vec![(0, -1)], range - 1), + LinearConstraint::ge(vec![(0, 1)], 1 - range), + ] { + cases.push( + ILP::::new(1, vec![constraint], vec![], ObjectiveSense::Minimize).unwrap(), + ); + } + } + for source in cases { + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let actual = reduction.target_problem().parameters(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for field in ["num_vars", "num_quadratic_terms"] { + assert_eq!( + transform.relation(field), + Some(ParameterRelation::UpperBound) + ); + assert!( + predicted.get(field).unwrap() >= actual.get(field).unwrap(), + "{field}" + ); + } + } +} + #[test] fn test_ilp_to_qubo_closed_loop() { // Binary ILP: maximize x0 + 2*x1 + 3*x2 diff --git a/src/unit_tests/rules/ksatisfiability_qubo.rs b/src/unit_tests/rules/ksatisfiability_qubo.rs index 3bb229434..2d9aae200 100644 --- a/src/unit_tests/rules/ksatisfiability_qubo.rs +++ b/src/unit_tests/rules/ksatisfiability_qubo.rs @@ -6,6 +6,51 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::variant::{K2, K3}; +#[test] +fn mixed_parameter_predictions_match_constructed_qubo_paths() { + use crate::parameters::ParameterRelation; + use crate::rules::ReductionGraph; + let graph = ReductionGraph::new(); + let path = graph + .find_all_paths( + KSatisfiability::::NAME, + &ReductionGraph::variant_to_map(&KSatisfiability::::variant()), + QUBO::::NAME, + &ReductionGraph::variant_to_map(&QUBO::::variant()), + ) + .into_iter() + .find(|path| path.len() == 2) + .unwrap(); + let transform = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + assert_eq!( + transform.relation("num_vars"), + Some(ParameterRelation::Exact) + ); + assert_eq!( + transform.relation("num_quadratic_terms"), + Some(ParameterRelation::UpperBound) + ); + for (n, clauses, expected_terms) in [ + (0, vec![], 0), + (1, vec![], 0), + (3, vec![vec![1, 2], vec![1, 3], vec![2, 3]], 3), + (2, vec![vec![1, 2], vec![-1, 2]], 0), + ] { + let source = + KSatisfiability::::new(n, clauses.into_iter().map(CNFClause::new).collect()); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let final_reduction = ReduceTo::>::reduce_to(reduction.target_problem()).unwrap(); + let actual = final_reduction.target_problem().parameters(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + assert_eq!(actual.get("num_quadratic_terms"), Some(expected_terms)); + assert_eq!(predicted.get("num_vars"), actual.get("num_vars")); + assert!(predicted.get("num_quadratic_terms").unwrap() >= expected_terms); + } +} + #[test] fn test_ksatisfiability_to_qubo_closed_loop() { // 3 vars, 4 clauses (matches ground truth): diff --git a/src/unit_tests/rules/maximummatching_ilp.rs b/src/unit_tests/rules/maximummatching_ilp.rs index 3dda34bfe..913253576 100644 --- a/src/unit_tests/rules/maximummatching_ilp.rs +++ b/src/unit_tests/rules/maximummatching_ilp.rs @@ -4,6 +4,35 @@ use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; +#[test] +fn parameter_predictions_account_for_isolated_vertices_and_loops() { + use crate::parameters::ParameterRelation; + for (vertices, edges, expected_constraints) in [(3, vec![(0, 1)], 2), (1, vec![(0, 0)], 1)] { + let source = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(vertices, edges)); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let actual = reduction.target_problem().parameters(); + assert_eq!(actual.get("num_constraints"), Some(expected_constraints)); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| entry.source_name == "MaximumMatching" && entry.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for (field, _) in transform.expressions() { + match transform.relation(field).unwrap() { + ParameterRelation::Exact => { + assert_eq!(predicted.get(field), actual.get(field), "{field}") + } + ParameterRelation::UpperBound => assert!( + predicted.get(field).unwrap() >= actual.get(field).unwrap(), + "{field}" + ), + } + } + } +} + #[test] fn test_reduction_creates_valid_ilp() { // Triangle graph: 3 vertices, 3 edges diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index e2d428dbf..d8a91d468 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -12,7 +12,8 @@ fn constraint_count_is_only_an_upper_bound() { .unwrap() .transform() .unwrap() - .relation(), + .relation("num_constraints") + .unwrap(), crate::parameters::ParameterRelation::UpperBound ); let problem = MaximumSetPacking::new(vec![vec![0], vec![1]]); diff --git a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs index 09413a71b..eb9e8aa51 100644 --- a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs @@ -4,6 +4,29 @@ use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; +#[test] +fn feedback_arc_set_solves_through_integer_binary_ilp_and_qubo() { + use crate::models::algebraic::QUBO; + + let source = + MinimumFeedbackArcSet::new(DirectedGraph::new(2, vec![(0, 1), (1, 0)]), vec![2_i64, 5]); + let integer = ReduceTo::>::reduce_to(&source).unwrap(); + let binary = ReduceTo::>::reduce_to(integer.target_problem()).unwrap(); + let qubo = ReduceTo::>::reduce_to(binary.target_problem()).unwrap(); + let optimum = BruteForce::new() + .solve(qubo.target_problem()) + .unwrap() + .unwrap(); + let binary_solution = qubo.extract_solution(&optimum).unwrap(); + let integer_solution = binary.extract_solution(&binary_solution).unwrap(); + assert!(integer + .target_problem() + .is_feasible(&integer_solution) + .unwrap()); + let solution = integer.extract_solution(&integer_solution).unwrap(); + assert_eq!(source.evaluate(&solution).unwrap(), Min(Some(2))); +} + #[test] fn test_reduction_creates_valid_ilp() { // Simple 3-cycle: 0 -> 1 -> 2 -> 0 diff --git a/src/unit_tests/rules/registry.rs b/src/unit_tests/rules/registry.rs index 72de79349..9a3eeec91 100644 --- a/src/unit_tests/rules/registry.rs +++ b/src/unit_tests/rules/registry.rs @@ -114,16 +114,19 @@ fn entry_with(declarations: fn() -> ReductionParameterDeclarations) -> Reduction } #[test] -fn one_relation_applies_to_the_whole_transform() { +fn declared_field_exposes_its_relation() { let entry = entry_with(|| ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), - fields: vec![("n", Expr::variable("n"))], + fields: vec![( + "n", + crate::parameters::ParameterRelation::Exact, + Expr::variable("n"), + )], unavailable: vec![], }); let contract = entry.parameter_contract().unwrap(); let transform = contract.transform().unwrap(); assert_eq!( - transform.relation(), + transform.relation("n").unwrap(), crate::parameters::ParameterRelation::Exact ); assert!(transform.get("n").is_some()); @@ -132,8 +135,11 @@ fn one_relation_applies_to_the_whole_transform() { #[test] fn unavailable_field_cannot_overlap_a_formula() { let entry = entry_with(|| ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), - fields: vec![("n", Expr::variable("n"))], + fields: vec![( + "n", + crate::parameters::ParameterRelation::Exact, + Expr::variable("n"), + )], unavailable: vec![UnavailableParameterField { field: "n", reason: "the construction does not expose this statistic", @@ -148,7 +154,6 @@ fn unavailable_field_cannot_overlap_a_formula() { #[test] fn unavailable_field_requires_a_reason() { let entry = entry_with(|| ReductionParameterDeclarations { - relation: None, fields: vec![], unavailable: vec![UnavailableParameterField { field: "n", diff --git a/src/unit_tests/rules/spinglass_qubo.rs b/src/unit_tests/rules/spinglass_qubo.rs index 7894a553b..97de04644 100644 --- a/src/unit_tests/rules/spinglass_qubo.rs +++ b/src/unit_tests/rules/spinglass_qubo.rs @@ -71,6 +71,18 @@ fn test_reduction_structure() { assert_eq!(qubo2.num_variables(), 3); } +#[test] +fn sparse_qubo_interactions_match_nonzero_quadratic_terms() { + let qubo = QUBO::::from_entries( + 4, + vec![(0, 0, 2.0), (0, 1, 1e-11), (1, 3, -2.0), (3, 3, 4.0)], + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&qubo).unwrap(); + assert_eq!(qubo.num_quadratic_terms(), 2); + assert_eq!(reduction.target_problem().num_interactions(), 2); +} + #[test] fn test_jl_parity_spinglass_to_qubo() { let data: serde_json::Value = serde_json::from_str(include_str!( diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 1ba9b0d78..c0411e1e0 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -123,9 +123,6 @@ fn canonical_examples_satisfy_upper_bound_parameter_contracts() { let Some(transform) = contract.transform() else { continue; }; - if transform.relation() != ParameterRelation::UpperBound { - continue; - } let source_size = ReductionGraph::compute_problem_parameters( &example.source.problem, &example.source.variant, @@ -152,3 +149,294 @@ fn canonical_examples_satisfy_upper_bound_parameter_contracts() { } } } + +fn check_reduced_parameters(source: S, fields: &[&str], relation: ParameterRelation) +where + S: Problem + ReduceTo, + T: Problem, +{ + let reduction = source.reduce_to().expect("reduction should succeed"); + let actual = reduction.target_problem().parameters(); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == S::NAME + && entry.target_name == T::NAME + && entry.source_variant() == S::variant() + && entry.target_variant() == T::variant() + }) + .expect("direct reduction is registered"); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().expect("symbolic transform exists"); + for (field, _) in transform.expressions() { + assert_eq!( + transform.relation(field), + Some(relation), + "{} -> {}: {field}", + S::NAME, + T::NAME + ); + } + let predicted = transform.evaluate(&source.parameters()).unwrap(); + if relation == ParameterRelation::Exact { + for (field, _) in transform.expressions() { + if fields.contains(&field) { + continue; + } + assert_eq!( + predicted.get(field), + actual.get(field), + "{} -> {}: {field}", + S::NAME, + T::NAME + ); + } + } + for &field in fields { + assert_eq!( + predicted.get(field), + actual.get(field), + "{} -> {}: {field}", + S::NAME, + T::NAME + ); + assert!( + !contract + .unavailable() + .iter() + .any(|item| item.field == field), + "{} -> {}: {field} is still unavailable", + S::NAME, + T::NAME + ); + } +} + +#[test] +fn newly_exact_parameters_match_reduced_instances() { + use crate::models::algebraic::MinimumMatrixCover; + use crate::models::algebraic::{ + IntegerVariable, LinearConstraint, ObjectiveSense, QuadraticAssignment, BMF, ILP, + }; + use crate::models::graph::BicliqueCover; + use crate::models::graph::{ + HamiltonianPath, MaximumContactMapOverlap, MinimumVertexCover, OptimalLinearArrangement, + }; + use crate::models::misc::{ + ClosestString, ConsistencyOfDatabaseFrequencyTables, ExpectedRetrievalCost, + FeasibleRegisterAssignment, LongestCommonSubsequence, MaximumLikelihoodRanking, + MultiprocessorScheduling, Partition, RegisterSufficiency, ResourceConstrainedScheduling, + SequencingToMinimizeWeightedCompletionTime, SumOfSquaresPartition, ThreePartition, + }; + use crate::models::set::{IntegerKnapsack, ThreeDimensionalMatching}; + use crate::types::One; + + let exact = ParameterRelation::Exact; + check_reduced_parameters::<_, ILP>( + BMF::new(vec![vec![true, false], vec![false, true]], 2), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ClosestString::new(2, vec![vec![0, 1], vec![1, 0]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ConsistencyOfDatabaseFrequencyTables::new(1, vec![2, 2], vec![], vec![]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ExactCoverBy3Sets::new(3, vec![[0, 1, 2]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ExpectedRetrievalCost::new(vec![0.5, 0.5], 2).unwrap(), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + IntegerKnapsack::new(vec![3, 4], vec![5, 6], 7).unwrap(), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + LongestCommonSubsequence::new(2, vec![vec![0, 1], vec![1, 0, 1]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + MaximumContactMapOverlap::new(3, vec![(0, 2)], 3, vec![(0, 1)]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + MaximumLikelihoodRanking::new(vec![vec![0, 1, 2], vec![2, 0, 1], vec![1, 2, 0]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + MinimumMatrixCover::new(vec![vec![0, 2], vec![3, 0]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + SumOfSquaresPartition::new(vec![1, 2, 3], 2), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ThreeDimensionalMatching::new(2, vec![(0, 1, 1), (1, 0, 0)]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + QuadraticAssignment::new(vec![vec![0, 1], vec![2, 0]], vec![vec![0, 3], vec![4, 0]]), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, BicliqueCover>( + BMF::new(vec![vec![true, false], vec![false, true]], 1), + &["num_vertices", "left_size", "right_size", "rank"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ILP::::with_variables( + vec![IntegerVariable::new(Some(0), Some(3)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 2)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(), + &["num_constraints"], + exact, + ); + check_reduced_parameters::<_, LongestCommonSubsequence>( + MinimumVertexCover::new(SimpleGraph::path(4), vec![One; 4]), + &["sum_triangular_lengths"], + exact, + ); + check_reduced_parameters::<_, SequencingToMinimizeWeightedCompletionTime>( + OptimalLinearArrangement::new(SimpleGraph::path(4)), + &["num_precedences"], + exact, + ); + check_reduced_parameters::<_, MultiprocessorScheduling>( + Partition::new(vec![1, 2, 3]).unwrap(), + &["num_processors"], + exact, + ); + check_reduced_parameters::<_, ResourceConstrainedScheduling>( + ThreePartition::new(vec![4, 5, 6, 4, 6, 5], 15), + &["deadline", "num_resources"], + exact, + ); +} + +#[test] +fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { + use crate::models::algebraic::{QuadraticAssignment, BMF, ILP}; + use crate::models::graph::{ + BicliqueCover, HamiltonianCircuit, HamiltonianPath, MinimumVertexCover, + }; + use crate::models::misc::{ + ConsistencyOfDatabaseFrequencyTables, FrequencyTable, KnownValue, LongestCommonSubsequence, + MaximumLikelihoodRanking, RegisterSufficiency, + }; + + let exact = ParameterRelation::Exact; + check_reduced_parameters::<_, ILP>( + ConsistencyOfDatabaseFrequencyTables::new( + 2, + vec![2, 2], + vec![FrequencyTable::new(0, 1, vec![vec![1, 0], vec![0, 1]])], + vec![KnownValue::new(0, 0, 0)], + ), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + LongestCommonSubsequence::new(2, vec![vec![], vec![0, 1]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + MaximumLikelihoodRanking::new(vec![]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + RegisterSufficiency::new(0, vec![], 0), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + QuadraticAssignment::new(vec![], vec![vec![0]]), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + QuadraticAssignment::new(vec![vec![0]], vec![vec![0, 1], vec![1, 0]]), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + HamiltonianPath::new(SimpleGraph::new(0, vec![])), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + HamiltonianPath::new(SimpleGraph::new(1, vec![])), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, HamiltonianPath>( + HamiltonianCircuit::new(SimpleGraph::new(0, vec![])), + &["num_consecutive_positions"], + ParameterRelation::UpperBound, + ); + check_reduced_parameters::<_, HamiltonianPath>( + HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])), + &["num_consecutive_positions"], + ParameterRelation::UpperBound, + ); + check_reduced_parameters::<_, LongestCommonSubsequence>( + MinimumVertexCover::new(SimpleGraph::new(0, vec![]), vec![]), + &["sum_triangular_lengths"], + exact, + ); + + let source = BMF::new(vec![vec![true, false], vec![false, true]], 1); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().parameters().get("num_edges"), + Some(2) + ); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| entry.source_name == "BMF" && entry.target_name == "BicliqueCover") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + assert!(contract.transform().unwrap().get("num_edges").is_none()); + assert!(contract + .unavailable() + .iter() + .any(|field| field.field == "num_edges")); +}