Skip to content

Commit 7906394

Browse files
GiggleLiuisPANNclaude
authored
Fix #410: [Model] MultipleCopyFileAllocation (#665)
* Add plan for #410: [Model] MultipleCopyFileAllocation * feat: add MultipleCopyFileAllocation model * feat: add CLI support for MultipleCopyFileAllocation * test: strengthen MultipleCopyFileAllocation CLI coverage * docs: add MultipleCopyFileAllocation paper entry * chore: remove plan file after implementation * fix: address PR #665 review feedback * fix: improve MCFA review follow-ups * Fix canonical example spec and paper for merged-main API changes - Update canonical_model_example_specs to use new ModelExampleSpec fields (instance, optimal_config, optimal_value) instead of removed `build` field - Fix paper Typst: remove `.inner` from graph access, use optimal_config instead of removed .optimal/.samples accessors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix formatting after merge Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Revert solver auto-selection: keep main's default ILP behavior Remove supports_ilp(), available_solvers(), default_solver() from DynProblem. Revert solve/inspect/MCP to main's hardcoded ILP default. Update MCFA CLI test to use explicit --solver brute-force. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing usage and storage fields to empty_args() test helper Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix CLI tests: update graph serialization format, correct solver assertion - graph uses edges/num_vertices (no inner/nodes wrapper) - inspect solver list is dynamic on main (brute-force only for MCFA) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1bafa02 commit 7906394

10 files changed

Lines changed: 735 additions & 24 deletions

File tree

docs/paper/reductions.typ

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"SequencingWithReleaseTimesAndDeadlines": [Sequencing with Release Times and Deadlines],
125125
"ShortestCommonSupersequence": [Shortest Common Supersequence],
126126
"MinimumSumMulticenter": [Minimum Sum Multicenter],
127+
"MultipleCopyFileAllocation": [Multiple Copy File Allocation],
127128
"SteinerTree": [Steiner Tree],
128129
"StrongConnectivityAugmentation": [Strong Connectivity Augmentation],
129130
"SubgraphIsomorphism": [Subgraph Isomorphism],
@@ -1557,6 +1558,45 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
15571558
]
15581559
}
15591560

1561+
#{
1562+
let x = load-model-example("MultipleCopyFileAllocation")
1563+
let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1)))
1564+
let K = x.instance.bound
1565+
let sol = (config: x.optimal_config, metric: x.optimal_value)
1566+
let copies = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
1567+
[
1568+
#problem-def("MultipleCopyFileAllocation")[
1569+
Given a graph $G = (V, E)$, usage values $u: V -> ZZ_(> 0)$, storage costs $s: V -> ZZ_(> 0)$, and a positive integer $K$, determine whether there exists a subset $V' subset.eq V$ such that
1570+
$sum_(v in V') s(v) + sum_(v in V) u(v) dot d(v, V') <= K,$
1571+
where $d(v, V') = min_(w in V') d_G(v, w)$ is the shortest-path distance from $v$ to the nearest copy vertex.
1572+
][
1573+
Multiple Copy File Allocation appears in the storage-and-retrieval section of Garey and Johnson (SR6) @garey1979. The model combines two competing costs: each chosen copy vertex incurs a storage charge, while every vertex pays an access cost weighted by its demand and graph distance to the nearest copy. Garey and Johnson record the problem as NP-complete in the strong sense, even when usage and storage costs are uniform @garey1979.
1574+
1575+
*Example.* Consider the 6-cycle $C_6$ with uniform usage $u(v) = 10$, uniform storage $s(v) = 1$, and bound $K = #K$. Placing copies at $V' = {#copies.map(i => $v_#i$).join(", ")}$ gives storage cost $1 + 1 + 1 = 3$. The remaining vertices $v_0, v_2, v_4$ are each at distance 1 from the nearest copy, so the access cost is $10 + 10 + 10 = 30$. Thus the total cost is $3 + 30 = 33 <= #K$, so this placement is satisfying. The alternating placement shown below is one symmetric witness.
1576+
1577+
#figure({
1578+
let blue = graph-colors.at(0)
1579+
let gray = luma(200)
1580+
canvas(length: 1cm, {
1581+
import draw: *
1582+
let verts = ((0, 1.6), (1.35, 0.8), (1.35, -0.8), (0, -1.6), (-1.35, -0.8), (-1.35, 0.8))
1583+
for (u, v) in edges {
1584+
g-edge(verts.at(u), verts.at(v), stroke: 1pt + gray)
1585+
}
1586+
for (k, pos) in verts.enumerate() {
1587+
let has-copy = copies.any(c => c == k)
1588+
g-node(pos, name: "v" + str(k),
1589+
fill: if has-copy { blue } else { white },
1590+
label: if has-copy { text(fill: white)[$v_#k$] } else { [$v_#k$] })
1591+
}
1592+
})
1593+
},
1594+
caption: [Multiple Copy File Allocation on a 6-cycle. Copy vertices $v_1$, $v_3$, and $v_5$ are shown in blue; every white vertex is one hop from the nearest copy, so the total cost is $33$.],
1595+
) <fig:multiple-copy-file-allocation>
1596+
]
1597+
]
1598+
}
1599+
15601600
== Set Problems
15611601

15621602
#{

problemreductions-cli/src/cli.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ Flags by problem type:
251251
BMF --matrix (0/1), --rank
252252
ConsecutiveOnesSubmatrix --matrix (0/1), --k
253253
SteinerTree --graph, --edge-weights, --terminals
254+
MultipleCopyFileAllocation --graph, --usage, --storage, --bound
254255
CVP --basis, --target-vec [--bounds]
255256
MultiprocessorScheduling --lengths, --num-processors, --deadline
256257
SequencingWithinIntervals --release-times, --deadlines, --lengths
@@ -470,7 +471,7 @@ pub struct CreateArgs {
470471
/// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4")
471472
#[arg(long)]
472473
pub required_edges: Option<String>,
473-
/// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection)
474+
/// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection)
474475
#[arg(long, allow_hyphen_values = true)]
475476
pub bound: Option<i64>,
476477
/// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0)
@@ -509,6 +510,12 @@ pub struct CreateArgs {
509510
/// Candidate weighted arcs for StrongConnectivityAugmentation (e.g., 2>0:1,2>1:3)
510511
#[arg(long)]
511512
pub candidate_arcs: Option<String>,
513+
/// Usage frequencies for MultipleCopyFileAllocation (comma-separated, e.g., "5,4,3,2")
514+
#[arg(long)]
515+
pub usage: Option<String>,
516+
/// Storage costs for MultipleCopyFileAllocation (comma-separated, e.g., "1,1,1,1")
517+
#[arg(long)]
518+
pub storage: Option<String>,
512519
/// Deadlines for scheduling problems (comma-separated, e.g., "5,5,5,3,3")
513520
#[arg(long)]
514521
pub deadlines: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ use problemreductions::topology::{
3232
use serde::Serialize;
3333
use std::collections::{BTreeMap, BTreeSet};
3434

35+
const MULTIPLE_COPY_FILE_ALLOCATION_EXAMPLE_ARGS: &str =
36+
"--graph 0-1,1-2,2-3 --usage 5,4,3,2 --storage 1,1,1,1 --bound 8";
37+
const MULTIPLE_COPY_FILE_ALLOCATION_USAGE: &str =
38+
"Usage: pred create MultipleCopyFileAllocation --graph 0-1,1-2,2-3 --usage 5,4,3,2 --storage 1,1,1,1 --bound 8";
39+
3540
/// Check if all data flags are None (no problem-specific input provided).
3641
fn all_data_flags_empty(args: &CreateArgs) -> bool {
3742
args.graph.is_none()
@@ -89,6 +94,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
8994
&& args.strings.is_none()
9095
&& args.costs.is_none()
9196
&& args.arcs.is_none()
97+
&& args.usage.is_none()
98+
&& args.storage.is_none()
9299
&& args.source.is_none()
93100
&& args.sink.is_none()
94101
&& args.size_bound.is_none()
@@ -99,6 +106,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
99106
&& args.candidate_arcs.is_none()
100107
&& args.potential_edges.is_none()
101108
&& args.budget.is_none()
109+
&& args.deadlines.is_none()
102110
&& args.precedence_pairs.is_none()
103111
&& args.resource_bounds.is_none()
104112
&& args.resource_requirements.is_none()
@@ -402,6 +410,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
402410
"--schedules \"1,1,1,1,1,0,0;0,1,1,1,1,1,0;0,0,1,1,1,1,1;1,0,0,1,1,1,1;1,1,0,0,1,1,1\" --requirements 2,2,2,3,3,2,1 --num-workers 4 --k 5"
403411
}
404412
"SteinerTree" => "--graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4",
413+
"MultipleCopyFileAllocation" => {
414+
MULTIPLE_COPY_FILE_ALLOCATION_EXAMPLE_ARGS
415+
}
405416
"OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5",
406417
"DirectedTwoCommodityIntegralFlow" => {
407418
"--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1"
@@ -957,6 +968,37 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
957968
(ser(HamiltonianPath::new(graph))?, resolved_variant.clone())
958969
}
959970

971+
// MultipleCopyFileAllocation (graph + usage + storage + bound)
972+
"MultipleCopyFileAllocation" => {
973+
let (graph, num_vertices) = parse_graph(args)
974+
.map_err(|e| anyhow::anyhow!("{e}\n\n{MULTIPLE_COPY_FILE_ALLOCATION_USAGE}"))?;
975+
let usage = parse_vertex_i64_values(
976+
args.usage.as_deref(),
977+
"usage",
978+
num_vertices,
979+
"MultipleCopyFileAllocation",
980+
MULTIPLE_COPY_FILE_ALLOCATION_USAGE,
981+
)?;
982+
let storage = parse_vertex_i64_values(
983+
args.storage.as_deref(),
984+
"storage",
985+
num_vertices,
986+
"MultipleCopyFileAllocation",
987+
MULTIPLE_COPY_FILE_ALLOCATION_USAGE,
988+
)?;
989+
let bound = args.bound.ok_or_else(|| {
990+
anyhow::anyhow!(
991+
"MultipleCopyFileAllocation requires --bound\n\n{MULTIPLE_COPY_FILE_ALLOCATION_USAGE}"
992+
)
993+
})?;
994+
(
995+
ser(MultipleCopyFileAllocation::new(
996+
graph, usage, storage, bound,
997+
))?,
998+
resolved_variant.clone(),
999+
)
1000+
}
1001+
9601002
// UndirectedTwoCommodityIntegralFlow (graph + capacities + terminals + requirements)
9611003
"UndirectedTwoCommodityIntegralFlow" => {
9621004
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";
@@ -3105,6 +3147,29 @@ fn parse_vertex_weights(args: &CreateArgs, num_vertices: usize) -> Result<Vec<i3
31053147
}
31063148
}
31073149

3150+
fn parse_vertex_i64_values(
3151+
raw: Option<&str>,
3152+
field_name: &str,
3153+
num_vertices: usize,
3154+
problem_name: &str,
3155+
usage: &str,
3156+
) -> Result<Vec<i64>> {
3157+
let raw =
3158+
raw.ok_or_else(|| anyhow::anyhow!("{problem_name} requires --{field_name}\n\n{usage}"))?;
3159+
let values: Vec<i64> = util::parse_comma_list(raw)
3160+
.map_err(|e| anyhow::anyhow!("invalid {field_name} list: {e}\n\n{usage}"))?;
3161+
if values.len() != num_vertices {
3162+
bail!(
3163+
"Expected {} {} values but got {}\n\n{}",
3164+
num_vertices,
3165+
field_name,
3166+
values.len(),
3167+
usage
3168+
);
3169+
}
3170+
Ok(values)
3171+
}
3172+
31083173
/// Parse `--terminals` as comma-separated vertex indices.
31093174
fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result<Vec<usize>> {
31103175
let s = args
@@ -4554,6 +4619,8 @@ mod tests {
45544619
costs: None,
45554620
cut_bound: None,
45564621
size_bound: None,
4622+
usage: None,
4623+
storage: None,
45574624
}
45584625
}
45594626

0 commit comments

Comments
 (0)