diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index a9108235b..3ee98fbb2 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2437,7 +2437,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let witness = (2, 1, 1, 1, 1, 2, 1) [ #problem-def("UndirectedFlowLowerBounds")[ - Given an undirected graph $G = (V, E)$, specified vertices $s, t in V$, lower bounds $l: E -> ZZ_(>= 0)$, upper capacities $c: E -> ZZ^+$ with $l(e) <= c(e)$ for every edge, and a requirement $R in ZZ^+$, determine whether there exists a flow function $f: {(u, v), (v, u): {u, v} in E} -> ZZ_(>= 0)$ such that each edge carries flow in at most one direction, every edge value lies between its lower and upper bound, flow is conserved at every vertex in $V backslash {s, t}$, and the net flow into $t$ is at least $R$. + Given an undirected graph $G = (V, E)$, specified vertices $s, t in V$, lower bounds $l: E -> ZZ_(>= 0)$, upper capacities $c: E -> ZZ_(>= 0)$ with $l(e) <= c(e)$ for every edge, and a requirement $R in ZZ^+$, determine whether there exists a flow function $f: {(u, v), (v, u): {u, v} in E} -> ZZ_(>= 0)$ such that each edge carries flow in at most one direction, every edge value lies between its lower and upper bound, flow is conserved at every vertex in $V backslash {s, t}$, and the net flow into $t$ is at least $R$. ][ Undirected Flow with Lower Bounds appears as ND37 in Garey and Johnson's catalog @garey1979. Itai proved that even this single-commodity undirected feasibility problem is NP-complete, contrasting sharply with the directed lower-bounded case, which reduces to ordinary max-flow machinery @itai1978. @@ -13795,6 +13795,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) Any satisfying bundled flow assigns a non-negative integer to each arc, satisfies every bundle inequality by definition, satisfies every nonterminal conservation equality, and yields sink inflow at least $R$, so it is a feasible ILP solution. ($arrow.l.double$) Any feasible ILP solution gives non-negative integral arc values obeying the same bundle, conservation, and sink-inflow constraints, hence it is a satisfying solution to the original Integral Flow with Bundles instance. _Solution extraction._ Identity: read the ILP vector $(x_0, dots, x_(m-1))$ directly as the arc-flow vector of the source problem. + + _Numeric bounds._ Give each arc the explicit domain $0 <= x_i <= u_i$, where $u_i$ is the minimum capacity of a bundle containing it. With $S = sum_i u_i$, replace $R$ by $min(R, S+1)$: a requirement above $S$ remains infeasible. If $h$ is the maximum bundle-capacity bit length (at least one), all target constraint and domain magnitudes have at most $h + |A| + 1$ bits. No separate requirement parameter is needed. ] #let ola_seqmwct = load-example("OptimalLinearArrangement", "SequencingToMinimizeWeightedCompletionTime") @@ -15026,6 +15028,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ Direction indicators linearize the capacity-sharing constraint. Per-commodity conservation prevents flow from being created or destroyed at another commodity's terminals, as required by the standard multicommodity-flow formulation @garey1979. _Solution extraction._ Flow variables (first $4|E|$ variables). + + _Numeric bounds._ Bound each flow variable explicitly by its edge capacity. With $S = sum_e "cap"_e$, each sink's net inflow lies in $[-S, S]$, so replace each requirement $R_k$ by $max(-S, min(R_k, S+1))$. This preserves feasibility, including impossible demands. If $h$ is the maximum capacity bit length (at least one), $h + |E| + 1$ bounds the target constraint and domain magnitude bits. ] #reduction-rule("DirectedTwoCommodityIntegralFlow", "ILP")[ @@ -15063,6 +15067,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ Direction indicators force flow in one direction per edge; bounds enforce both upper and lower capacity limits. _Solution extraction._ Edge orientations: $z_e$ values. + + _Numeric bounds._ Require $0 <= "lower"_e <= "cap"_e$ and give each directional flow the explicit domain $[0, "cap"_e]$. With $S = sum_e "cap"_e$, replace the positive requirement $R$ by $min(R, S+1)$; demands above $S$ remain infeasible. The capacity bit length $h >= 1$ therefore gives the bound $h + |E| + 1$ on target constraint and domain magnitude bits, without a separate lower-bound or requirement parameter. ] // Flow-based @@ -15100,6 +15106,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) A valid multiplier flow satisfies these linear equalities and inequalities by definition. ($arrow.l.double$) Any feasible ILP solution gives an integral arc flow whose non-terminal outflow equals the prescribed multiple of its inflow and whose sink inflow meets the requirement. _Solution extraction._ Output the arc-flow vector $(f_a)_(a in A)$. + + _Numeric bounds._ Give each arc the explicit domain $[0, c_a]$ and let $S = sum_a c_a$. Replace $h(v)$ by $min(h(v), S+1)$. If $h(v)>S$, any positive integral inflow would require outflow above $S$, so both the original and replacement equation force zero inflow and outflow. Replace $R$ by $max(-S, min(R, S+1))$. If $b >= 1$ is the maximum capacity bit length, all target constraint and domain magnitudes have at most $b + |A| + 1$ bits. Thus the prediction needs no multiplier or requirement parameter. ] #reduction-rule("PathConstrainedNetworkFlow", "ILP")[ diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index d60932ca0..698b9e1fa 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -5603,8 +5603,8 @@ fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() { let output = pred() .args([ "path", - "IntegralFlowWithMultipliers", - "ILP/i64/i64/bounded", + "DecisionLongestCircuit", + "ILP/bool", "--limit", "1", "--json", @@ -5632,10 +5632,7 @@ fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() { .iter() .find(|field| field["relation"] == "unavailable") .unwrap(); - assert!(unavailable["reason"] - .as_str() - .unwrap() - .contains("multipliers")); + assert!(unavailable["reason"].as_str().unwrap().contains("length")); } #[test] @@ -7021,7 +7018,8 @@ fn test_inspect_integral_flow_with_multipliers_reports_parameters() { assert!(parameters.contains(&"num_vertices")); assert!(parameters.contains(&"num_arcs")); assert!(parameters.contains(&"max_capacity")); - assert!(parameters.contains(&"requirement")); + assert!(parameters.contains(&"max_capacity_bits")); + assert_eq!(json["parameter_values"]["max_capacity_bits"], 3); std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&result_file).ok(); diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 8e76d423c..61fe71d5c 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -224,6 +224,11 @@ impl IntegralFlowBundles { &self.bundle_capacities } + /// Maximum bit length of the bundle capacities, with a minimum of one. + pub fn max_capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.bundle_capacities.iter().copied()) + } + /// Get the required net inflow at the sink. pub fn requirement(&self) -> i64 { self.requirement @@ -350,6 +355,7 @@ impl Problem for IntegralFlowBundles { type Value = crate::types::Or; crate::problem_parameters![ + ("max_capacity_bits", max_capacity_bits), ("num_arcs", num_arcs), ("num_bundles", num_bundles), ("num_vertices", num_vertices), diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index bcb3bc0bd..42fc7169d 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -206,6 +206,11 @@ impl IntegralFlowWithMultipliers { self.requirement } + /// Maximum bit length of the arc capacities, with a minimum of one. + pub fn max_capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.capacities.iter().copied()) + } + pub fn num_vertices(&self) -> usize { self.graph.num_vertices() } @@ -296,9 +301,9 @@ impl Problem for IntegralFlowWithMultipliers { crate::problem_parameters![ ("max_capacity", max_capacity), + ("max_capacity_bits", max_capacity_bits), ("num_arcs", num_arcs), ("num_vertices", num_vertices), - ("requirement", requirement), ]; fn evaluate( diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index 8cd64d366..1c7472ef8 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -126,9 +126,9 @@ impl UndirectedFlowLowerBounds { } for (edge_index, (&lower, &upper)) in lower_bounds.iter().zip(&capacities).enumerate() { - if lower > upper { + if lower < 0 || lower > upper { return Err(format!( - "lower bound at edge {edge_index} must be at most its capacity" + "lower bound at edge {edge_index} must be nonnegative and at most its capacity" ) .into()); } @@ -168,6 +168,11 @@ impl UndirectedFlowLowerBounds { self.requirement } + /// Maximum bit length of the edge capacities, with a minimum of one. + pub fn max_capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.capacities.iter().copied()) + } + pub fn num_vertices(&self) -> usize { self.graph.num_vertices() } @@ -278,7 +283,11 @@ impl Problem for UndirectedFlowLowerBounds { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("max_capacity_bits", max_capacity_bits), + ("num_edges", num_edges), + ("num_vertices", num_vertices), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index 72161a497..0f50526c6 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -212,6 +212,11 @@ impl UndirectedTwoCommodityIntegralFlow { &self.capacities } + /// Maximum bit length of the edge capacities, with a minimum of one. + pub fn max_capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.capacities.iter().copied()) + } + pub fn source_1(&self) -> usize { self.source_1 } @@ -423,6 +428,7 @@ impl Problem for UndirectedTwoCommodityIntegralFlow { type Value = crate::types::Or; crate::problem_parameters![ + ("max_capacity_bits", max_capacity_bits), ("num_edges", num_edges), ("num_conservation_constraints", num_conservation_constraints), ("num_vertices", num_vertices), diff --git a/src/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/rules/decisionmaximumindependentset_integralflowbundles.rs index 3f367ec5d..fe3b6d246 100644 --- a/src/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -80,6 +80,7 @@ impl crate::rules::AggregateReductionResult for ReductionDecisionMISToIFB {} #[reduction( transform = exact { + max_capacity_bits = "2", num_vertices = "num_vertices + 3", num_arcs = "2 * num_vertices + 2", num_bundles = "num_edges + num_vertices + 1", diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index 224233008..8edf06a77 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -7,6 +7,7 @@ use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowBundles; use crate::reduction; +use crate::rules::ilp_helpers::bounded_flow_requirement; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing IntegralFlowBundles to ILP. @@ -42,14 +43,12 @@ impl ReductionResult for ReductionIFBToILP { impl crate::rules::AggregateReductionResult for ReductionIFBToILP {} #[reduction(transform = { - unavailable { - max_constraint_magnitude_bits = "bundle capacities and the flow requirement are not registered source parameters", - }, exact { num_vars = "num_arcs", num_constraints = "num_bundles + num_vertices - 1", }, upper_bound { + max_constraint_magnitude_bits = "max_capacity_bits + num_arcs + 1", num_nonzeros = "num_arcs * (num_bundles + num_vertices - 1)", }, })] @@ -91,10 +90,12 @@ impl ReduceTo> for IntegralFlowBundles { sink_terms.push((arc_index, 1)); } } - constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let upper_bounds = self.arc_upper_bounds(); + let requirement = + bounded_flow_requirement(self.requirement(), upper_bounds.iter().copied()); + constraints.push(LinearConstraint::ge(sink_terms, requirement)); - let variables = self - .arc_upper_bounds() + let variables = upper_bounds .into_iter() .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) .collect::, _>>() diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index 4da1eb82f..94619829a 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -6,6 +6,7 @@ use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowWithMultipliers; use crate::reduction; +use crate::rules::ilp_helpers::bounded_flow_requirement; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing IntegralFlowWithMultipliers to ILP. @@ -41,14 +42,12 @@ impl ReductionResult for ReductionIFWMToILP { impl crate::rules::AggregateReductionResult for ReductionIFWMToILP {} #[reduction(transform = { - unavailable { - max_constraint_magnitude_bits = "vertex multipliers are not bounded by registered source parameters", - }, exact { num_vars = "num_arcs", num_constraints = "num_arcs + num_vertices - 1", }, upper_bound { + max_constraint_magnitude_bits = "max_capacity_bits + num_arcs + 1", num_nonzeros = "num_arcs * (num_arcs + num_vertices - 1)", }, })] @@ -59,6 +58,11 @@ impl ReduceTo> for IntegralFlowWithMultipliers { let arcs = self.graph().arcs(); let num_vertices = self.num_vertices(); let mut constraints = Vec::new(); + let total_capacity = self + .capacities() + .iter() + .copied() + .fold(0_i64, i64::saturating_add); // Capacity: f_a <= c_a for each arc for (arc_idx, &capacity) in self.capacities().iter().enumerate() { @@ -73,7 +77,9 @@ impl ReduceTo> for IntegralFlowWithMultipliers { if vertex == self.source() || vertex == self.sink() { continue; } - let multiplier = self.multipliers()[vertex]; + // Outflow cannot exceed the total capacity S. A multiplier above + // S forces both integral inflow and outflow to zero, as does S+1. + let multiplier = self.multipliers()[vertex].min(total_capacity.saturating_add(1)); let mut terms = Vec::new(); for (arc_idx, &(u, v)) in arcs.iter().enumerate() { if u == vertex { @@ -96,7 +102,8 @@ impl ReduceTo> for IntegralFlowWithMultipliers { sink_terms.push((arc_idx, -1)); // outgoing } } - constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let requirement = bounded_flow_requirement(self.requirement(), [total_capacity]); + constraints.push(LinearConstraint::ge(sink_terms, requirement)); let variables = self .capacities() diff --git a/src/rules/partition_integralflowwithmultipliers.rs b/src/rules/partition_integralflowwithmultipliers.rs index f72063df2..2cabe4659 100644 --- a/src/rules/partition_integralflowwithmultipliers.rs +++ b/src/rules/partition_integralflowwithmultipliers.rs @@ -56,12 +56,12 @@ impl crate::rules::AggregateReductionResult for ReductionPartitionToIntegralFlow #[reduction( transform = upper_bound { + max_capacity_bits = "max_numeric_magnitude_bits + num_elements", num_vertices = "num_elements + 3", num_arcs = "2 * num_elements + 1", }, unavailable = { - max_capacity = "the target capacity depends on source numeric values not represented by Partition parameters", - requirement = "the target requirement depends on source numeric values not represented by Partition parameters", + max_capacity = "bounding raw capacities from source magnitude bits requires a variable exponent; downstream ILP predictions use max_capacity_bits", } )] impl ReduceTo for Partition { diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index acb9f5442..3edc5e228 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -5,13 +5,13 @@ //! f_{vu} = 2*e + 1 (flow in v→u direction, ≥ 0) //! z_e = 2*|E| + e (binary orientation: 1 if u→v, 0 if v→u) //! -//! Constraints per edge (4 constraints): +//! Constraints per edge (up to 5 constraints): //! z_e ≤ 1 (force binary) //! f_{uv} ≤ cap[e] * z_e (only if oriented u→v) //! f_{vu} ≤ cap[e] * (1 - z_e) (only if oriented v→u) //! f_{uv} ≥ lower[e] * z_e (must carry at least lower bound if oriented u→v) //! f_{vu} ≥ lower[e] * (1 - z_e)(must carry at least lower bound if oriented v→u) -//! Since we need all 4: linearized as: +//! Linearized as: //! z_e ≤ 1 //! f_{uv} - cap[e]*z_e ≤ 0 //! f_{vu} + cap[e]*z_e ≤ cap[e] @@ -26,6 +26,7 @@ use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedFlowLowerBounds; use crate::reduction; +use crate::rules::ilp_helpers::bounded_flow_requirement; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::Graph; @@ -80,13 +81,11 @@ impl crate::rules::AggregateReductionResult for ReductionUFLBToILP {} #[reduction( transform = upper_bound { + max_constraint_magnitude_bits = "max_capacity_bits + num_edges + 1", num_vars = "3 * num_edges", num_constraints = "5 * num_edges + num_vertices + 1", num_nonzeros = "(3 * num_edges) * (5 * num_edges + num_vertices + 1)", }, - unavailable = { - max_constraint_magnitude_bits = "flow capacities, lower bounds and the requirement are not registered source parameters", - }, )] impl ReduceTo> for UndirectedFlowLowerBounds { type Result = ReductionUFLBToILP; @@ -176,7 +175,9 @@ impl ReduceTo> for UndirectedFlowLowerBounds { sink_terms.push((f_vu(edge_idx), 1)); } } - constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let requirement = + bounded_flow_requirement(self.requirement(), self.capacities().iter().copied()); + constraints.push(LinearConstraint::ge(sink_terms, requirement)); let mut variables = self .capacities() diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 52e9affbd..be316f533 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -27,6 +27,7 @@ use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedTwoCommodityIntegralFlow; use crate::reduction; +use crate::rules::ilp_helpers::bounded_flow_requirement; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::Graph; @@ -70,14 +71,12 @@ impl ReductionResult for ReductionU2CIFToILP { impl crate::rules::AggregateReductionResult for ReductionU2CIFToILP {} #[reduction(transform = { - unavailable { - max_constraint_magnitude_bits = "flow capacities and requirements are not registered source parameters", - }, exact { num_vars = "6 * num_edges", num_constraints = "7 * num_edges + num_conservation_constraints + 2", }, upper_bound { + max_constraint_magnitude_bits = "max_capacity_bits + num_edges + 1", num_nonzeros = "(6 * num_edges) * (7 * num_edges + num_conservation_constraints + 2)", }, })] @@ -172,36 +171,28 @@ impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { } } - // Net flow into sinks ≥ requirements - // Commodity 1: net inflow at sink_1 ≥ requirement_1 - let sink_1 = self.sink_1(); - let mut sink1_terms: Vec<(usize, i64)> = Vec::new(); - for (edge_idx, &(u, v)) in edges.iter().enumerate() { - if sink_1 == v { - sink1_terms.push((f1_uv(edge_idx), 1)); - sink1_terms.push((f1_vu(edge_idx), -1)); - } - if sink_1 == u { - sink1_terms.push((f1_uv(edge_idx), -1)); - sink1_terms.push((f1_vu(edge_idx), 1)); - } - } - constraints.push(LinearConstraint::ge(sink1_terms, self.requirement_1())); - - // Commodity 2: net inflow at sink_2 ≥ requirement_2 - let sink_2 = self.sink_2(); - let mut sink2_terms: Vec<(usize, i64)> = Vec::new(); - for (edge_idx, &(u, v)) in edges.iter().enumerate() { - if sink_2 == v { - sink2_terms.push((f2_uv(edge_idx), 1)); - sink2_terms.push((f2_vu(edge_idx), -1)); - } - if sink_2 == u { - sink2_terms.push((f2_uv(edge_idx), -1)); - sink2_terms.push((f2_vu(edge_idx), 1)); + // Net flow into each sink must meet its normalized requirement. + for (sink, requirement, flow_offset) in [ + (self.sink_1(), self.requirement_1(), 0), + (self.sink_2(), self.requirement_2(), 2), + ] { + let mut terms = Vec::new(); + for (edge_idx, &(u, v)) in edges.iter().enumerate() { + let uv = 4 * edge_idx + flow_offset; + let vu = uv + 1; + if sink == v { + terms.push((uv, 1)); + terms.push((vu, -1)); + } + if sink == u { + terms.push((uv, -1)); + terms.push((vu, 1)); + } } + let requirement = + bounded_flow_requirement(requirement, self.capacities().iter().copied()); + constraints.push(LinearConstraint::ge(terms, requirement)); } - constraints.push(LinearConstraint::ge(sink2_terms, self.requirement_2())); let mut variables = self .capacities() diff --git a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs index f3caf8687..b1af8705e 100644 --- a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs +++ b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs @@ -146,10 +146,32 @@ fn test_integral_flow_with_multipliers_problem_name_and_parameters() { .collect(); assert_eq!( fields, - HashSet::from(["max_capacity", "num_arcs", "num_vertices", "requirement"]) + HashSet::from([ + "max_capacity", + "max_capacity_bits", + "num_arcs", + "num_vertices" + ]) ); } +#[test] +fn negative_requirement_has_nonnegative_prediction_parameters() { + let source = IntegralFlowWithMultipliers::new( + DirectedGraph::new(2, vec![(0, 1)]), + 0, + 1, + vec![1, 1], + vec![1], + i64::MIN, + ); + let restored: IntegralFlowWithMultipliers = + serde_json::from_value(serde_json::to_value(&source).unwrap()).unwrap(); + assert_eq!(restored.requirement(), i64::MIN); + assert!(restored.evaluate(&vec![0]).unwrap().0); + assert_eq!(restored.parameters().get("max_capacity_bits"), Some(1)); +} + #[cfg(feature = "example-db")] #[test] fn test_integral_flow_with_multipliers_canonical_example_spec() { diff --git a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs index b230e7b65..3c5ef89f9 100644 --- a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs +++ b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs @@ -19,6 +19,8 @@ fn test_undirected_flow_lower_bounds_invalid_inputs() { ("requirement", serde_json::json!(0)), ("requirement", serde_json::json!(-1)), ("lower_bounds", serde_json::json!([3, 1, 0, 0, 1, 0, 1])), + ("lower_bounds", serde_json::json!([-1, 1, 0, 0, 1, 0, 1])), + ("capacities", serde_json::json!([-1, 2, 2, 2, 1, 3, 2])), ] { let mut invalid = valid.clone(); invalid[field] = value; diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index 555385530..298c2c5e9 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -1424,3 +1424,168 @@ fn numeric_magnitude_bits_cover_padding_and_empty_targets() { check::<_, ILP>(BinPacking::new(Vec::::new(), i64::MAX).unwrap()); check::<_, ILP>(BinPacking::new(vec![8_i64], 1).unwrap()); } + +#[test] +fn flow_capacity_bits_bound_ilp_parameters_at_numeric_boundaries() { + use crate::models::algebraic::Bounded; + use crate::models::graph::{ + IntegralFlowBundles, IntegralFlowWithMultipliers, UndirectedFlowLowerBounds, + UndirectedTwoCommodityIntegralFlow, + }; + use crate::topology::DirectedGraph; + + fn check>>(source: S, bits: u64) { + assert_eq!( + source.parameters().get("max_capacity_bits"), + Some(bits), + "{}", + S::NAME + ); + let reduction = source.reduce_to().unwrap(); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| entry.source_name == S::NAME && entry.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let predicted = contract + .transform() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + for (field, actual) in reduction.target_problem().parameters().iter() { + assert!( + predicted.get(field).expect("complete flow bound") >= actual, + "{}: {field}", + S::NAME + ); + } + } + for (capacity, bits) in [(0, 1), (1, 1), (7, 3), (8, 4), (i64::MAX, 63)] { + if capacity > 0 { + check( + IntegralFlowBundles::new( + DirectedGraph::new(2, vec![(0, 1)]), + 0, + 1, + vec![vec![0]], + vec![capacity], + i64::MAX, + ), + bits, + ); + } + check( + IntegralFlowWithMultipliers::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + 0, + 2, + vec![1, i64::MAX, 1], + vec![capacity; 2], + i64::MAX, + ), + bits, + ); + check( + UndirectedFlowLowerBounds::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![capacity], + vec![capacity], + 0, + 1, + i64::MAX, + ), + bits, + ); + check( + UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![capacity], + 0, + 1, + 0, + 1, + i64::MIN, + i64::MAX, + ), + bits, + ); + } + check( + IntegralFlowBundles::new( + DirectedGraph::new(2, vec![]), + 0, + 1, + vec![], + vec![], + i64::MAX, + ), + 1, + ); +} + +#[test] +fn incoming_flow_predictions_compose_to_qubo_and_recover_witnesses() { + use crate::models::algebraic::Bounded; + use crate::models::graph::{IntegralFlowBundles, IntegralFlowWithMultipliers}; + use crate::models::misc::Partition; + + fn check(source: S, expected: bool) + where + S: Problem + 'static, + S::Solution: 'static, + F: Problem, + { + let path = ReductionPath { + steps: [ + (S::NAME, S::variant()), + (F::NAME, F::variant()), + ( + ILP::::NAME, + ILP::::variant(), + ), + (ILP::::NAME, ILP::::variant()), + (QUBO::::NAME, QUBO::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let graph = ReductionGraph::new(); + let predicted = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let target = chain.target_problem::>(); + for (field, actual) in target.parameters().iter() { + assert!(predicted.get(field).expect("composed flow bound") >= actual); + } + let solution = crate::solvers::ILPSolver::new().solve(target).unwrap(); + let recovered = chain.extract_solution::(&solution); + if expected { + assert!(source.evaluate(&recovered.unwrap()).unwrap().0); + } else { + assert!( + recovered.is_err(), + "infeasible flow must not decode a witness" + ); + } + } + for (sizes, feasible) in [(vec![1, 1], true), (vec![1, 2], false), (vec![2], false)] { + check::<_, IntegralFlowWithMultipliers>(Partition::new(sizes).unwrap(), feasible); + } + for bound in [0, 1, 2] { + check::<_, IntegralFlowBundles>( + Decision::new( + MaximumIndependentSet::new(SimpleGraph::new(1, vec![]), vec![One]), + bound, + ), + bound <= 1, + ); + } +} diff --git a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs index c6cec2209..f4c223796 100644 --- a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs +++ b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs @@ -47,3 +47,45 @@ fn test_integralflowwithmultipliers_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn normalized_multipliers_preserve_all_small_flows() { + // Unit capacities bound the flow despite arbitrarily large multipliers. + // Include a loop to exercise merged coefficients. + for multiplier in [1, 2, 3, 4, i64::MAX] { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = IntegralFlowWithMultipliers::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2), (1, 1)]), + 0, + 2, + vec![1, multiplier, 1], + vec![1; 3], + requirement, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + // Capacity sum is three, so four bounds every normalized coefficient + // and demand regardless of the original multiplier or requirement. + assert!( + target + .parameters() + .get("max_constraint_magnitude_bits") + .unwrap() + <= 3 + ); + for mask in 0..8 { + let assignment: Vec = (0..3).map(|i| (mask >> i) & 1).collect(); + let incoming = assignment[0] + assignment[2]; + let outgoing = assignment[1] + assignment[2]; + let expected = i128::from(outgoing) + == i128::from(multiplier) * i128::from(incoming) + && assignment[1] >= requirement; + assert_eq!(target.is_feasible(&assignment).unwrap(), expected); + if expected { + let recovered = reduction.extract_solution(&assignment).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } + } + } + } +}