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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -2429,7 +2429,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.

Expand Down Expand Up @@ -13787,6 +13787,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")
Expand Down Expand Up @@ -15018,6 +15020,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")[
Expand Down Expand Up @@ -15055,6 +15059,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
Expand Down Expand Up @@ -15092,6 +15098,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")[
Expand Down
12 changes: 5 additions & 7 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/models/graph/integral_flow_bundles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
7 changes: 6 additions & 1 deletion src/models/graph/integral_flow_with_multipliers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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(
Expand Down
15 changes: 12 additions & 3 deletions src/models/graph/undirected_flow_lower_bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,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());
}
Expand Down Expand Up @@ -165,6 +165,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()
}
Expand Down Expand Up @@ -275,7 +280,11 @@ impl Problem for UndirectedFlowLowerBounds {
type Solution = Vec<bool>;
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![]
Expand Down
6 changes: 6 additions & 0 deletions src/models/graph/undirected_two_commodity_integral_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 7 additions & 6 deletions src/rules/integralflowbundles_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)",
},
})]
Expand Down Expand Up @@ -91,10 +90,12 @@ impl ReduceTo<ILP<i64, i64, Bounded>> 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::<Result<Vec<_>, _>>()
Expand Down
17 changes: 12 additions & 5 deletions src/rules/integralflowwithmultipliers_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)",
},
})]
Expand All @@ -59,6 +58,11 @@ impl ReduceTo<ILP<i64, i64, Bounded>> 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() {
Expand All @@ -73,7 +77,9 @@ impl ReduceTo<ILP<i64, i64, Bounded>> 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 {
Expand All @@ -96,7 +102,8 @@ impl ReduceTo<ILP<i64, i64, Bounded>> 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()
Expand Down
4 changes: 2 additions & 2 deletions src/rules/partition_integralflowwithmultipliers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IntegralFlowWithMultipliers> for Partition {
Expand Down
13 changes: 7 additions & 6 deletions src/rules/undirectedflowlowerbounds_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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;

Expand Down Expand Up @@ -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<ILP<i64, i64, Bounded>> for UndirectedFlowLowerBounds {
type Result = ReductionUFLBToILP;
Expand Down Expand Up @@ -176,7 +175,9 @@ impl ReduceTo<ILP<i64, i64, Bounded>> 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()
Expand Down
Loading
Loading