diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 60f37fb54..341ffd5a5 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -117,6 +117,7 @@ "BoundedComponentSpanningForest": [Bounded Component Spanning Forest], "BinPacking": [Bin Packing], "BoyceCoddNormalFormViolation": [Boyce-Codd Normal Form Violation], + "CapacityAssignment": [Capacity Assignment], "ConsistencyOfDatabaseFrequencyTables": [Consistency of Database Frequency Tables], "ClosestVectorProblem": [Closest Vector Problem], "ConsecutiveSets": [Consecutive Sets], @@ -5052,6 +5053,39 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("CapacityAssignment") + [ + #problem-def("CapacityAssignment")[ + Given a finite set $C$ of communication links, an ordered set $M subset ZZ_(> 0)$ of capacities, cost and delay functions $g: C times M -> ZZ_(>= 0)$ and $d: C times M -> ZZ_(>= 0)$ such that for every $c in C$ and $i < j$ in the order of $M$ we have $g(c, i) <= g(c, j)$ and $d(c, i) >= d(c, j)$, and budgets $K, J in ZZ_(>= 0)$, determine whether there exists an assignment $sigma: C -> M$ such that $sum_(c in C) g(c, sigma(c)) <= K$ and $sum_(c in C) d(c, sigma(c)) <= J$. + ][ + Capacity Assignment is the bicriteria communication-network design problem SR7 in Garey & Johnson @garey1979. The original NP-completeness proof, via reduction from Subset Sum, is due to Van Sickle and Chandy @vansicklechandy1977. The model captures discrete provisioning of communication links, where upgrading a link increases installation cost but decreases delay. The direct witness encoding implemented in this repository yields an $O^*(|M|^(|C|))$ exact algorithm by brute-force enumeration#footnote[No algorithm improving on brute-force enumeration is known for the exact witness encoding used in this repository.]. Garey and Johnson also note a pseudo-polynomial dynamic-programming formulation when the budgets are small @garey1979. + + *Example.* Let $C = {c_1, c_2, c_3}$, $M = {1, 2, 3}$, $K = 10$, and $J = 12$. With cost rows $(1, 3, 6)$, $(2, 4, 7)$, $(1, 2, 5)$ and delay rows $(8, 4, 1)$, $(7, 3, 1)$, $(6, 3, 1)$, the assignment $sigma = (2, 2, 2)$ has total cost $3 + 4 + 2 = 9 <= 10$ and total delay $4 + 3 + 3 = 10 <= 12$, so the instance is satisfiable. Brute-force enumeration finds exactly 5 satisfying assignments; for contrast, $sigma = (1, 1, 1)$ violates the delay budget and $sigma = (3, 3, 3)$ violates the cost budget. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o capacity-assignment.json", + "pred solve capacity-assignment.json --solver brute-force", + "pred evaluate capacity-assignment.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure({ + table( + columns: (auto, auto, auto), + inset: 4pt, + align: left, + table.header([*Link*], [*Cost row*], [*Delay row*]), + [$c_1$], [$(1, 3, 6)$], [$(8, 4, 1)$], + [$c_2$], [$(2, 4, 7)$], [$(7, 3, 1)$], + [$c_3$], [$(1, 2, 5)$], [$(6, 3, 1)$], + ) + }, + caption: [Canonical Capacity Assignment instance with budgets $K = 10$ and $J = 12$. Each row lists the cost-delay trade-off for one communication link.], + ) + ] + ] +} + #{ let x = load-model-example("PrecedenceConstrainedScheduling") let n = x.instance.num_tasks diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 307837a77..d2b4b3c87 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -213,6 +213,14 @@ @article{robertsonSeymour1995 doi = {10.1006/jctb.1995.1006} } +@inproceedings{vansicklechandy1977, + author = {Lawrence Van Sickle and K. Mani Chandy}, + title = {Computational Complexity of Network Design Algorithms}, + booktitle = {IFIP Congress 77}, + pages = {235--239}, + year = {1977} +} + @article{bruckerGareyJohnson1977, author = {Peter Brucker and Michael R. Garey and David S. Johnson}, title = {Scheduling equal-length tasks under tree-like precedence constraints to minimize maximum lateness}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 6306791b9..efb9876ae 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -246,6 +246,7 @@ Flags by problem type: PathConstrainedNetworkFlow --arcs, --capacities, --source, --sink, --paths, --requirement Factoring --target, --m, --n BinPacking --sizes, --capacity + CapacityAssignment --capacities, --cost-matrix, --delay-matrix, --cost-budget, --delay-budget SubsetSum --sizes, --target SumOfSquaresPartition --sizes, --num-groups, --bound PaintShop --sequence @@ -321,6 +322,7 @@ Examples: pred create MIS --graph 0-1,1-2,2-3 --weights 1,1,1 pred create SAT --num-vars 3 --clauses \"1,2;-1,3\" pred create QUBO --matrix \"1,0.5;0.5,2\" + pred create CapacityAssignment --capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12 pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5 pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2 pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10 @@ -366,15 +368,21 @@ pub struct CreateArgs { /// Edge lengths (e.g., 2,3,1) [default: all 1s] #[arg(long)] pub edge_lengths: Option, - /// Edge capacities for multicommodity flow problems (e.g., 1,1,2) + /// Capacities (edge capacities for flow problems, capacity levels for CapacityAssignment) #[arg(long)] pub capacities: Option, - /// Edge lower bounds for lower-bounded flow problems (e.g., 1,1,0,0,1,0,1) - #[arg(long)] - pub lower_bounds: Option, /// Bundle capacities for IntegralFlowBundles (e.g., 1,1,1) #[arg(long)] pub bundle_capacities: Option, + /// Cost matrix for CapacityAssignment (semicolon-separated rows, e.g., "1,3,6;2,4,7") + #[arg(long)] + pub cost_matrix: Option, + /// Delay matrix for CapacityAssignment (semicolon-separated rows, e.g., "8,4,1;7,3,1") + #[arg(long)] + pub delay_matrix: Option, + /// Edge lower bounds for lower-bounded flow problems (e.g., 1,1,0,0,1,0,1) + #[arg(long)] + pub lower_bounds: Option, /// Vertex multipliers in vertex order (e.g., 1,2,3,1) #[arg(long)] pub multipliers: Option, @@ -547,6 +555,12 @@ pub struct CreateArgs { /// Upper bound on total inter-partition arc cost #[arg(long)] pub cost_bound: Option, + /// Budget on total cost for CapacityAssignment + #[arg(long)] + pub cost_budget: Option, + /// Budget on total delay penalty for CapacityAssignment + #[arg(long)] + pub delay_budget: Option, /// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0) #[arg(long)] pub pattern: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 1db378ef9..230b5cb28 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -19,15 +19,15 @@ use problemreductions::models::graph::{ SteinerTreeInGraphs, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ - AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, - ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, FlowShopScheduling, FrequencyTable, - KnownValue, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, - PaintShop, PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, - SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, - SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, TimetableDesign, + AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, + ConjunctiveBooleanQuery, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, + FlowShopScheduling, FrequencyTable, KnownValue, LongestCommonSubsequence, + MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, + QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, + SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, + SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, TimetableDesign, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; @@ -51,8 +51,10 @@ 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.cost_matrix.is_none() + && args.delay_matrix.is_none() + && args.lower_bounds.is_none() && args.multipliers.is_none() && args.source.is_none() && args.sink.is_none() @@ -111,6 +113,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.length_bound.is_none() && args.weight_bound.is_none() && args.cost_bound.is_none() + && args.cost_budget.is_none() + && args.delay_budget.is_none() && args.pattern.is_none() && args.strings.is_none() && args.costs.is_none() @@ -597,6 +601,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", + "CapacityAssignment" => { + "--capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12" + } "MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10", "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", "SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1", @@ -2874,6 +2881,90 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + "CapacityAssignment" => { + let usage = "Usage: pred create CapacityAssignment --capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12"; + let capacities_str = args.capacities.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "CapacityAssignment requires --capacities, --cost-matrix, --delay-matrix, --cost-budget, and --delay-budget\n\n{usage}" + ) + })?; + let cost_matrix_str = args.cost_matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --cost-matrix\n\n{usage}") + })?; + let delay_matrix_str = args.delay_matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --delay-matrix\n\n{usage}") + })?; + let cost_budget = args.cost_budget.ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --cost-budget\n\n{usage}") + })?; + let delay_budget = args.delay_budget.ok_or_else(|| { + anyhow::anyhow!("CapacityAssignment requires --delay-budget\n\n{usage}") + })?; + + let capacities: Vec = util::parse_comma_list(capacities_str)?; + anyhow::ensure!( + !capacities.is_empty(), + "CapacityAssignment requires at least one capacity value\n\n{usage}" + ); + anyhow::ensure!( + capacities.iter().all(|&capacity| capacity > 0), + "CapacityAssignment capacities must be positive\n\n{usage}" + ); + anyhow::ensure!( + capacities.windows(2).all(|w| w[0] < w[1]), + "CapacityAssignment capacities must be strictly increasing\n\n{usage}" + ); + + let cost = parse_u64_matrix_rows(cost_matrix_str, "cost")?; + let delay = parse_u64_matrix_rows(delay_matrix_str, "delay")?; + anyhow::ensure!( + cost.len() == delay.len(), + "cost matrix row count ({}) must match delay matrix row count ({})\n\n{usage}", + cost.len(), + delay.len() + ); + + for (index, row) in cost.iter().enumerate() { + anyhow::ensure!( + row.len() == capacities.len(), + "cost row {} length ({}) must match capacities length ({})\n\n{usage}", + index, + row.len(), + capacities.len() + ); + anyhow::ensure!( + row.windows(2).all(|w| w[0] <= w[1]), + "cost row {} must be non-decreasing\n\n{usage}", + index + ); + } + for (index, row) in delay.iter().enumerate() { + anyhow::ensure!( + row.len() == capacities.len(), + "delay row {} length ({}) must match capacities length ({})\n\n{usage}", + index, + row.len(), + capacities.len() + ); + anyhow::ensure!( + row.windows(2).all(|w| w[0] >= w[1]), + "delay row {} must be non-increasing\n\n{usage}", + index + ); + } + + ( + ser(CapacityAssignment::new( + capacities, + cost, + delay, + cost_budget, + delay_budget, + ))?, + resolved_variant.clone(), + ) + } + // MinimumMultiwayCut "MinimumMultiwayCut" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -5322,6 +5413,31 @@ fn parse_matrix(args: &CreateArgs) -> Result>> { .collect() } +fn parse_u64_matrix_rows(matrix_str: &str, matrix_name: &str) -> Result>> { + matrix_str + .split(';') + .enumerate() + .map(|(row_index, row)| { + let row = row.trim(); + anyhow::ensure!( + !row.is_empty(), + "{matrix_name} row {row_index} must not be empty" + ); + row.split(',') + .map(|value| { + value.trim().parse::().map_err(|error| { + anyhow::anyhow!( + "Invalid {matrix_name} row {row_index} value {:?}: {}", + value.trim(), + error + ) + }) + }) + .collect() + }) + .collect() +} + /// Parse `--quantifiers` as comma-separated quantifier labels (E/A or Exists/ForAll). /// E.g., "E,A,E" or "Exists,ForAll,Exists" fn parse_quantifiers(args: &CreateArgs, num_vars: usize) -> Result> { @@ -6581,6 +6697,49 @@ mod tests { assert!(err.to_string().contains("GeneralizedHex requires --sink")); } + #[test] + fn test_create_capacity_assignment_serializes_problem_json() { + let output = temp_output_path("capacity_assignment_create"); + let cli = Cli::try_parse_from([ + "pred", + "-o", + output.to_str().unwrap(), + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3,6;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--cost-budget", + "10", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + 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"], "CapacityAssignment"); + assert_eq!(json["data"]["capacities"], serde_json::json!([1, 2, 3])); + assert_eq!(json["data"]["cost_budget"], 10); + assert_eq!(json["data"]["delay_budget"], 12); + } + #[test] fn test_create_longest_path_serializes_problem_json() { let output = temp_output_path("longest_path_create"); @@ -6676,6 +6835,74 @@ mod tests { ); } + #[test] + fn test_create_capacity_assignment_rejects_non_monotone_cost_row() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3,2;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--cost-budget", + "10", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + 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().to_string(); + assert!(err.contains("cost row 0")); + assert!(err.contains("non-decreasing")); + } + + #[test] + fn test_create_capacity_assignment_rejects_matrix_width_mismatch() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "CapacityAssignment", + "--capacities", + "1,2,3", + "--cost-matrix", + "1,3;2,4,7;1,2,5", + "--delay-matrix", + "8,4,1;7,3,1;6,3,1", + "--cost-budget", + "10", + "--delay-budget", + "12", + ]) + .expect("parse create command"); + 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().to_string(); + assert!(err.contains("cost row 0")); + assert!(err.contains("capacities length")); + } + #[test] fn test_create_longest_path_requires_edge_lengths() { let cli = Cli::try_parse_from([ @@ -6788,8 +7015,10 @@ mod tests { edge_weights: None, edge_lengths: None, capacities: None, - lower_bounds: None, bundle_capacities: None, + cost_matrix: None, + delay_matrix: None, + lower_bounds: None, multipliers: None, source: None, sink: None, @@ -6847,6 +7076,8 @@ mod tests { length_bound: None, weight_bound: None, cost_bound: None, + cost_budget: None, + delay_budget: None, pattern: None, strings: None, arc_costs: None, diff --git a/src/lib.rs b/src/lib.rs index c26d77caa..29db0f87f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,7 +67,7 @@ pub mod prelude { UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ - AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, + AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, Partition, QueryArg, diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs new file mode 100644 index 000000000..46d889b4c --- /dev/null +++ b/src/models/misc/capacity_assignment.rs @@ -0,0 +1,200 @@ +//! Capacity Assignment problem implementation. +//! +//! Capacity Assignment asks whether each communication link can be assigned +//! one capacity level so that total cost and total delay both stay within +//! their respective budgets. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "CapacityAssignment", + display_name: "Capacity Assignment", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Assign capacities to links while respecting cost and delay budgets", + fields: &[ + FieldInfo { name: "capacities", type_name: "Vec", description: "Ordered capacity levels M" }, + FieldInfo { name: "cost", type_name: "Vec>", description: "Cost matrix g(c, m) for each link and capacity" }, + FieldInfo { name: "delay", type_name: "Vec>", description: "Delay matrix d(c, m) for each link and capacity" }, + FieldInfo { name: "cost_budget", type_name: "u64", description: "Budget K on total cost" }, + FieldInfo { name: "delay_budget", type_name: "u64", description: "Budget J on total delay penalty" }, + ], + } +} + +/// Capacity Assignment feasibility problem. +/// +/// Each variable chooses one capacity index for one communication link. +/// Costs are monotone non-decreasing and delays are monotone non-increasing +/// with respect to the ordered capacity list. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CapacityAssignment { + capacities: Vec, + cost: Vec>, + delay: Vec>, + cost_budget: u64, + delay_budget: u64, +} + +impl CapacityAssignment { + /// Create a new Capacity Assignment instance. + pub fn new( + capacities: Vec, + cost: Vec>, + delay: Vec>, + cost_budget: u64, + delay_budget: u64, + ) -> Self { + assert!(!capacities.is_empty(), "capacities must be non-empty"); + assert!( + capacities.iter().all(|&capacity| capacity > 0), + "capacities must be positive" + ); + assert!( + capacities.windows(2).all(|w| w[0] < w[1]), + "capacities must be strictly increasing" + ); + assert_eq!( + cost.len(), + delay.len(), + "cost and delay must have the same number of links" + ); + + let num_capacities = capacities.len(); + for (link, row) in cost.iter().enumerate() { + assert_eq!( + row.len(), + num_capacities, + "cost row {link} length must match capacities length" + ); + assert!( + row.windows(2).all(|w| w[0] <= w[1]), + "cost row {link} must be non-decreasing" + ); + } + for (link, row) in delay.iter().enumerate() { + assert_eq!( + row.len(), + num_capacities, + "delay row {link} length must match capacities length" + ); + assert!( + row.windows(2).all(|w| w[0] >= w[1]), + "delay row {link} must be non-increasing" + ); + } + + Self { + capacities, + cost, + delay, + cost_budget, + delay_budget, + } + } + + /// Number of communication links. + pub fn num_links(&self) -> usize { + self.cost.len() + } + + /// Number of discrete capacity choices per link. + pub fn num_capacities(&self) -> usize { + self.capacities.len() + } + + /// Ordered capacity levels. + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + /// Cost matrix indexed by link, then capacity. + pub fn cost(&self) -> &[Vec] { + &self.cost + } + + /// Delay matrix indexed by link, then capacity. + pub fn delay(&self) -> &[Vec] { + &self.delay + } + + /// Total cost budget. + pub fn cost_budget(&self) -> u64 { + self.cost_budget + } + + /// Total delay budget. + pub fn delay_budget(&self) -> u64 { + self.delay_budget + } + + fn total_cost_and_delay(&self, config: &[usize]) -> Option<(u128, u128)> { + if config.len() != self.num_links() { + return None; + } + + let num_capacities = self.num_capacities(); + let mut total_cost = 0u128; + let mut total_delay = 0u128; + + for (link, &choice) in config.iter().enumerate() { + if choice >= num_capacities { + return None; + } + total_cost += self.cost[link][choice] as u128; + total_delay += self.delay[link][choice] as u128; + } + + Some((total_cost, total_delay)) + } +} + +impl Problem for CapacityAssignment { + const NAME: &'static str = "CapacityAssignment"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![self.num_capacities(); self.num_links()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let Some((total_cost, total_delay)) = self.total_cost_and_delay(config) else { + return false; + }; + total_cost <= self.cost_budget as u128 && total_delay <= self.delay_budget as u128 + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for CapacityAssignment {} + +crate::declare_variants! { + default sat CapacityAssignment => "num_capacities ^ num_links", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "capacity_assignment", + instance: Box::new(CapacityAssignment::new( + vec![1, 2, 3], + vec![vec![1, 3, 6], vec![2, 4, 7], vec![1, 2, 5]], + vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], + 10, + 12, + )), + optimal_config: vec![1, 1, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/capacity_assignment.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index e3b86f58b..33b0aa36f 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -35,6 +35,7 @@ pub(crate) mod additional_key; mod bin_packing; mod boyce_codd_normal_form_violation; +mod capacity_assignment; pub(crate) mod conjunctive_boolean_query; pub(crate) mod conjunctive_query_foldability; mod consistency_of_database_frequency_tables; @@ -68,6 +69,7 @@ mod timetable_design; pub use additional_key::AdditionalKey; pub use bin_packing::BinPacking; pub use boyce_codd_normal_form_violation::BoyceCoddNormalFormViolation; +pub use capacity_assignment::CapacityAssignment; pub use conjunctive_boolean_query::{ConjunctiveBooleanQuery, QueryArg, Relation as CbqRelation}; pub use conjunctive_query_foldability::{ConjunctiveQueryFoldability, Term}; pub use consistency_of_database_frequency_tables::{ @@ -104,6 +106,7 @@ pub use timetable_design::TimetableDesign; pub(crate) fn canonical_model_example_specs() -> Vec { let mut specs = Vec::new(); specs.extend(boyce_codd_normal_form_violation::canonical_model_example_specs()); + specs.extend(capacity_assignment::canonical_model_example_specs()); specs.extend(consistency_of_database_frequency_tables::canonical_model_example_specs()); specs.extend(conjunctive_boolean_query::canonical_model_example_specs()); specs.extend(conjunctive_query_foldability::canonical_model_example_specs()); diff --git a/src/models/mod.rs b/src/models/mod.rs index 2a2f4cda1..49cb11443 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -35,15 +35,16 @@ pub use graph::{ }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ - AdditionalKey, BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, - ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, Factoring, FlowShopScheduling, - Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, - PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, - SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, - SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling, - StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, TimetableDesign, + AdditionalKey, BinPacking, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, + ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, + Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, + MultiprocessorScheduling, PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, + SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, + SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, + SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, + StackerCrane, StaffScheduling, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, + Term, TimetableDesign, }; pub use set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/unit_tests/models/misc/capacity_assignment.rs b/src/unit_tests/models/misc/capacity_assignment.rs new file mode 100644 index 000000000..46fcf78bc --- /dev/null +++ b/src/unit_tests/models/misc/capacity_assignment.rs @@ -0,0 +1,92 @@ +use crate::models::misc::CapacityAssignment; +use crate::solvers::BruteForce; +use crate::traits::Problem; + +fn example_problem() -> CapacityAssignment { + CapacityAssignment::new( + vec![1, 2, 3], + vec![vec![1, 3, 6], vec![2, 4, 7], vec![1, 2, 5]], + vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], + 10, + 12, + ) +} + +#[test] +fn test_capacity_assignment_basic_properties() { + let problem = example_problem(); + assert_eq!(problem.num_links(), 3); + assert_eq!(problem.num_capacities(), 3); + assert_eq!(problem.capacities(), &[1, 2, 3]); + assert_eq!(problem.cost_budget(), 10); + assert_eq!(problem.delay_budget(), 12); + assert_eq!(problem.dims(), vec![3, 3, 3]); + assert_eq!(::NAME, "CapacityAssignment"); + assert_eq!(::variant(), Vec::new()); +} + +#[test] +fn test_capacity_assignment_evaluate_yes_and_no_examples() { + let problem = example_problem(); + assert!(problem.evaluate(&[1, 1, 1])); + assert!(problem.evaluate(&[0, 1, 2])); + assert!(!problem.evaluate(&[0, 0, 0])); + assert!(!problem.evaluate(&[2, 2, 2])); +} + +#[test] +fn test_capacity_assignment_rejects_invalid_configs() { + let problem = example_problem(); + assert!(!problem.evaluate(&[1, 1])); + assert!(!problem.evaluate(&[1, 1, 3])); +} + +#[test] +fn test_capacity_assignment_bruteforce_solution_count() { + let problem = example_problem(); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert_eq!(solutions.len(), 5); + assert!(solutions.contains(&vec![1, 1, 1])); + assert!(solutions.contains(&vec![0, 1, 2])); +} + +#[test] +fn test_capacity_assignment_serialization_round_trip() { + let problem = example_problem(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: CapacityAssignment = serde_json::from_value(json).unwrap(); + assert_eq!(restored.capacities(), problem.capacities()); + assert_eq!(restored.cost(), problem.cost()); + assert_eq!(restored.delay(), problem.delay()); + assert_eq!(restored.cost_budget(), problem.cost_budget()); + assert_eq!(restored.delay_budget(), problem.delay_budget()); +} + +#[test] +fn test_capacity_assignment_paper_example() { + let problem = example_problem(); + let config = vec![1, 1, 1]; + assert!(problem.evaluate(&config)); + + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert_eq!(solutions.len(), 5); + assert!(solutions.contains(&config)); +} + +#[test] +fn test_capacity_assignment_rejects_non_increasing_capacities() { + let result = std::panic::catch_unwind(|| { + CapacityAssignment::new(vec![1, 1], vec![vec![1, 2]], vec![vec![2, 1]], 3, 3) + }); + assert!(result.is_err()); +} + +#[test] +fn test_capacity_assignment_rejects_non_monotone_delay_row() { + let result = std::panic::catch_unwind(|| { + CapacityAssignment::new(vec![1, 2], vec![vec![1, 2]], vec![vec![1, 2]], 3, 3) + }); + assert!(result.is_err()); +}