Skip to content
Merged
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
63 changes: 63 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"LongestCircuit": [Longest Circuit],
"LongestPath": [Longest Path],
"ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path],
"UndirectedFlowLowerBounds": [Undirected Flow with Lower Bounds],
"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],
"PathConstrainedNetworkFlow": [Path-Constrained Network Flow],
"LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths],
Expand Down Expand Up @@ -1134,6 +1135,68 @@ is feasible: each set induces a connected subgraph, the component weights are $2
]
]
}
#{
let x = load-model-example("UndirectedFlowLowerBounds")
let s = x.instance.source
let t = x.instance.sink
let R = x.instance.requirement
let orientation = x.optimal_config
let edges = x.instance.graph.edges
let lower = x.instance.lower_bounds
let caps = x.instance.capacities
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$.
][
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.

The implementation exposes one binary decision per edge rather than raw flow magnitudes. The configuration $(#orientation.map(str).join(", "))$ means "orient every edge exactly as listed in the stored edge order"; once an orientation is fixed, `evaluate()` checks the remaining lower-bounded directed circulation conditions internally. This keeps the explicit search space at $2^m$ for $m = |E|$, matching the registry complexity bound.

*Example.* The canonical fixture uses source $s = v_#s$, sink $t = v_#t$, requirement $R = #R$, edges ${#edges.map(((u, v)) => $(v_#u, v_#v)$).join(", ")}$, and lower/upper pairs ${#range(edges.len()).map(i => $(#lower.at(i), #caps.at(i))$).join(", ")}$ in that order. Under the all-zero orientation config, a feasible witness sends flows $(#witness.map(str).join(", "))$ along those edges respectively: $2$ on $(v_0, v_1)$, $1$ on $(v_0, v_2)$, $1$ on $(v_1, v_3)$, $1$ on $(v_2, v_3)$, $1$ on $(v_1, v_4)$, $2$ on $(v_3, v_5)$, and $1$ on $(v_4, v_5)$. Every lower bound is satisfied, each nonterminal vertex has equal inflow and outflow, and the sink receives $2 + 1 = 3 >= R$, so the instance evaluates to true. A separate rule issue tracks the natural reduction to ILP; this model PR only documents the standalone verifier.

#pred-commands(
"pred create --example UndirectedFlowLowerBounds -o undirected-flow-lower-bounds.json",
"pred solve undirected-flow-lower-bounds.json",
"pred evaluate undirected-flow-lower-bounds.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
canvas(length: 0.9cm, {
import draw: *
let blue = graph-colors.at(0)
let red = rgb("#e15759")
let gray = luma(190)
let verts = ((0, 0), (1.6, 1.2), (1.6, -1.2), (3.4, 0.5), (3.4, -1.5), (5.2, -0.3))
let labels = (
[$s = v_0$],
[$v_1$],
[$v_2$],
[$v_3$],
[$v_4$],
[$t = v_5$],
)
for (u, v) in edges {
g-edge(verts.at(u), verts.at(v), stroke: 1.8pt + blue)
}
for (i, pos) in verts.enumerate() {
let fill = if i == s { blue } else if i == t { red } else { white }
let label = if i == s or i == t { text(fill: white)[#labels.at(i)] } else { labels.at(i) }
g-node(pos, name: "uflb-" + str(i), fill: fill, label: label)
}
content((0.75, 0.7), text(7pt, fill: gray)[$f = 2$])
content((0.75, -0.7), text(7pt, fill: gray)[$f = 1$])
content((2.45, 1.05), text(7pt, fill: gray)[$f = 1$])
content((2.45, -0.25), text(7pt, fill: gray)[$f = 1$])
content((2.45, -1.45), text(7pt, fill: gray)[$f = 1$])
content((4.35, 0.35), text(7pt, fill: gray)[$f = 2$])
content((4.35, -1.1), text(7pt, fill: gray)[$f = 1$])
}),
caption: [Canonical YES instance for Undirected Flow with Lower Bounds. Blue edges follow the all-zero orientation config, and edge labels show one feasible witness flow.],
) <fig:undirected-flow-lower-bounds>
]
]
}
#{
let x = load-model-example("UndirectedTwoCommodityIntegralFlow")
let satisfying_count = 1
Expand Down
24 changes: 17 additions & 7 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -266,14 +266,14 @@ @article{evenItaiShamir1976
doi = {10.1137/0205048}
}

@article{sahni1974,
author = {Sartaj Sahni},
title = {Computationally Related Problems},
journal = {SIAM Journal on Computing},
volume = {3},
@article{itai1978,
author = {Alon Itai},
title = {Two-Commodity Flow},
journal = {Journal of the ACM},
volume = {25},
number = {4},
pages = {262--279},
year = {1974},
pages = {596--611},
year = {1978},
doi = {10.1137/0203021}
}

Expand All @@ -288,6 +288,16 @@ @article{jewell1962
doi = {10.1287/opre.10.4.476}
}

@article{sahni1974,
author = {Sartaj Sahni},
title = {Computationally Related Problems},
journal = {SIAM Journal on Computing},
volume = {3},
number = {4},
pages = {262--279},
year = {1974}
}

@article{abdelWahabKameda1978,
author = {H. M. Abdel-Wahab and T. Kameda},
title = {Scheduling to Minimize Maximum Cumulative Cost Subject to Series-Parallel Precedence Constraints},
Expand Down
7 changes: 6 additions & 1 deletion problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ Flags by problem type:
HamiltonianCircuit, HC --graph
LongestCircuit --graph, --edge-weights, --bound
BoundedComponentSpanningForest --graph, --weights, --k, --bound
UndirectedFlowLowerBounds --graph, --capacities, --lower-bounds, --source, --sink, --requirement
IntegralFlowBundles --arcs, --bundles, --bundle-capacities, --source, --sink, --requirement [--num-vertices]
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs
Expand Down Expand Up @@ -325,6 +326,7 @@ Examples:
pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5
pred create MIS --random --num-vertices 10 --edge-prob 0.3
pred create MultiprocessorScheduling --lengths 4,5,3,2,6 --num-processors 2 --deadline 10
pred create UndirectedFlowLowerBounds --graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3
pred create ConsistencyOfDatabaseFrequencyTables --num-objects 6 --attribute-domains \"2,3,2\" --frequency-tables \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\" --known-values \"0,0,0;3,0,1;1,2,1\"
pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
Expand Down Expand Up @@ -363,6 +365,9 @@ pub struct CreateArgs {
/// Edge capacities for multicommodity flow problems (e.g., 1,1,2)
#[arg(long)]
pub capacities: Option<String>,
/// Edge lower bounds for lower-bounded flow problems (e.g., 1,1,0,0,1,0,1)
#[arg(long)]
pub lower_bounds: Option<String>,
/// Bundle capacities for IntegralFlowBundles (e.g., 1,1,1)
#[arg(long)]
pub bundle_capacities: Option<String>,
Expand All @@ -375,7 +380,7 @@ pub struct CreateArgs {
/// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets
#[arg(long)]
pub sink: Option<usize>,
/// Required total flow R for IntegralFlowBundles, IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, and PathConstrainedNetworkFlow
/// Required total flow R for IntegralFlowBundles, IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, PathConstrainedNetworkFlow, and UndirectedFlowLowerBounds
#[arg(long)]
pub requirement: Option<u64>,
/// Required number of paths for LengthBoundedDisjointPaths
Expand Down
180 changes: 163 additions & 17 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.edge_weights.is_none()
&& args.edge_lengths.is_none()
&& args.capacities.is_none()
&& args.lower_bounds.is_none()
&& args.bundle_capacities.is_none()
&& args.multipliers.is_none()
&& args.source.is_none()
Expand Down Expand Up @@ -538,6 +539,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"LongestPath" => {
"--graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6 --edge-lengths 3,2,4,1,5,2,3,2,4,1 --source-vertex 0 --target-vertex 6"
}
"UndirectedFlowLowerBounds" => {
"--graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3"
}
"UndirectedTwoCommodityIntegralFlow" => {
"--graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1"
},
Expand Down Expand Up @@ -1478,11 +1482,56 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// UndirectedFlowLowerBounds (graph + capacities + lower bounds + terminals + requirement)
"UndirectedFlowLowerBounds" => {
let usage = "Usage: pred create UndirectedFlowLowerBounds --graph 0-1,0-2,1-3,2-3,1-4,3-5,4-5 --capacities 2,2,2,2,1,3,2 --lower-bounds 1,1,0,0,1,0,1 --source 0 --sink 5 --requirement 3";
let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
let capacities = parse_capacities(args, graph.num_edges(), usage)?;
let lower_bounds = parse_lower_bounds(args, graph.num_edges(), usage)?;
let num_vertices = graph.num_vertices();
let source = args.source.ok_or_else(|| {
anyhow::anyhow!("UndirectedFlowLowerBounds requires --source\n\n{usage}")
})?;
let sink = args.sink.ok_or_else(|| {
anyhow::anyhow!("UndirectedFlowLowerBounds requires --sink\n\n{usage}")
})?;
let requirement = args.requirement.ok_or_else(|| {
anyhow::anyhow!("UndirectedFlowLowerBounds requires --requirement\n\n{usage}")
})?;
validate_vertex_index("source", source, num_vertices, usage)?;
validate_vertex_index("sink", sink, num_vertices, usage)?;
(
ser(UndirectedFlowLowerBounds::new(
graph,
capacities,
lower_bounds,
source,
sink,
requirement,
))?,
resolved_variant.clone(),
)
}

// UndirectedTwoCommodityIntegralFlow (graph + capacities + terminals + requirements)
"UndirectedTwoCommodityIntegralFlow" => {
let usage = "Usage: pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1";
let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
let capacities = parse_capacities(args, graph.num_edges(), usage)?;
for (edge_index, &capacity) in capacities.iter().enumerate() {
let fits = usize::try_from(capacity)
.ok()
.and_then(|value| value.checked_add(1))
.is_some();
if !fits {
bail!(
"capacity {} at edge index {} is too large for this platform\n\n{}",
capacity,
edge_index,
usage
);
}
}
let num_vertices = graph.num_vertices();
let source_1 = args.source_1.ok_or_else(|| {
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-1\n\n{usage}")
Expand Down Expand Up @@ -4479,9 +4528,10 @@ fn validate_vertex_index(

/// Parse `--capacities` as edge capacities (u64).
fn parse_capacities(args: &CreateArgs, num_edges: usize, usage: &str) -> Result<Vec<u64>> {
let capacities = args.capacities.as_deref().ok_or_else(|| {
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --capacities\n\n{usage}")
})?;
let capacities = args
.capacities
.as_deref()
.ok_or_else(|| anyhow::anyhow!("This problem requires --capacities\n\n{usage}"))?;
let capacities: Vec<u64> = capacities
.split(',')
.map(|s| {
Expand All @@ -4499,23 +4549,34 @@ fn parse_capacities(args: &CreateArgs, num_edges: usize, usage: &str) -> Result<
usage
);
}
for (edge_index, &capacity) in capacities.iter().enumerate() {
let fits = usize::try_from(capacity)
.ok()
.and_then(|value| value.checked_add(1))
.is_some();
if !fits {
bail!(
"capacity {} at edge index {} is too large for this platform\n\n{}",
capacity,
edge_index,
usage
);
}
}
Ok(capacities)
}

/// Parse `--lower-bounds` as edge lower bounds (u64).
fn parse_lower_bounds(args: &CreateArgs, num_edges: usize, usage: &str) -> Result<Vec<u64>> {
let lower_bounds = args.lower_bounds.as_deref().ok_or_else(|| {
anyhow::anyhow!("UndirectedFlowLowerBounds requires --lower-bounds\n\n{usage}")
})?;
let lower_bounds: Vec<u64> = lower_bounds
.split(',')
.map(|s| {
let trimmed = s.trim();
trimmed
.parse::<u64>()
.with_context(|| format!("Invalid lower bound `{trimmed}`\n\n{usage}"))
})
.collect::<Result<Vec<_>>>()?;
if lower_bounds.len() != num_edges {
bail!(
"Expected {} lower bounds but got {}\n\n{}",
num_edges,
lower_bounds.len(),
usage
);
}
Ok(lower_bounds)
}

fn parse_bundle_capacities(args: &CreateArgs, num_bundles: usize, usage: &str) -> Result<Vec<u64>> {
let capacities = args.bundle_capacities.as_deref().ok_or_else(|| {
anyhow::anyhow!("IntegralFlowBundles requires --bundle-capacities\n\n{usage}")
Expand Down Expand Up @@ -6462,6 +6523,55 @@ mod tests {
);
}

#[test]
fn test_create_undirected_flow_lower_bounds_serializes_problem_json() {
let output = temp_output_path("undirected_flow_lower_bounds_create");
let cli = Cli::try_parse_from([
"pred",
"-o",
output.to_str().unwrap(),
"create",
"UndirectedFlowLowerBounds",
"--graph",
"0-1,0-2,1-3,2-3,1-4,3-5,4-5",
"--capacities",
"2,2,2,2,1,3,2",
"--lower-bounds",
"1,1,0,0,1,0,1",
"--source",
"0",
"--sink",
"5",
"--requirement",
"3",
])
.unwrap();
let out = OutputConfig {
output: cli.output.clone(),
quiet: true,
json: false,
auto_json: false,
};
let args = match cli.command {
Commands::Create(args) => args,
_ => unreachable!(),
};

create(&args, &out).unwrap();

let json: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap();
fs::remove_file(&output).unwrap();
assert_eq!(json["type"], "UndirectedFlowLowerBounds");
assert_eq!(json["data"]["source"], 0);
assert_eq!(json["data"]["sink"], 5);
assert_eq!(json["data"]["requirement"], 3);
assert_eq!(
json["data"]["lower_bounds"],
serde_json::json!([1, 1, 0, 0, 1, 0, 1])
);
}

#[test]
fn test_create_longest_path_requires_edge_lengths() {
let cli = Cli::try_parse_from([
Expand Down Expand Up @@ -6528,6 +6638,41 @@ mod tests {
.contains("LongestPath uses --edge-lengths, not --weights"));
}

#[test]
fn test_create_undirected_flow_lower_bounds_requires_lower_bounds() {
let cli = Cli::try_parse_from([
"pred",
"create",
"UndirectedFlowLowerBounds",
"--graph",
"0-1,0-2,1-3,2-3,1-4,3-5,4-5",
"--capacities",
"2,2,2,2,1,3,2",
"--source",
"0",
"--sink",
"5",
"--requirement",
"3",
])
.unwrap();
let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};
let args = match cli.command {
Commands::Create(args) => args,
_ => unreachable!(),
};

let err = create(&args, &out).unwrap_err();
assert!(err
.to_string()
.contains("UndirectedFlowLowerBounds requires --lower-bounds"));
}

fn empty_args() -> CreateArgs {
CreateArgs {
problem: Some("BiconnectivityAugmentation".to_string()),
Expand All @@ -6539,6 +6684,7 @@ mod tests {
edge_weights: None,
edge_lengths: None,
capacities: None,
lower_bounds: None,
bundle_capacities: None,
multipliers: None,
source: None,
Expand Down
Loading
Loading