|
| 1 | +//! Bounded worker multiplicities with exact budget and coverage residuals. |
| 2 | +use crate::models::{algebraic::QUBO, misc::StaffScheduling, Decision}; |
| 3 | +use crate::rules::ilp_i64_ilp_bool::binary_weights; |
| 4 | +use crate::rules::qubo_helpers::QuboBuilder; |
| 5 | +use crate::rules::traits::validate_target_witness; |
| 6 | +use crate::rules::{ExtractionResult, ReduceTo, ReductionError, ReductionResult}; |
| 7 | + |
| 8 | +#[derive(Debug, Clone)] |
| 9 | +pub struct ReductionStaffSchedulingToQUBO { |
| 10 | + target: Decision<QUBO<i64>>, |
| 11 | + weights: Vec<i64>, |
| 12 | + num_schedules: usize, |
| 13 | +} |
| 14 | + |
| 15 | +impl ReductionResult for ReductionStaffSchedulingToQUBO { |
| 16 | + type Source = StaffScheduling; |
| 17 | + type Target = Decision<QUBO<i64>>; |
| 18 | + |
| 19 | + fn target_problem(&self) -> &Self::Target { |
| 20 | + &self.target |
| 21 | + } |
| 22 | + |
| 23 | + fn extract_solution(&self, bits: &Vec<bool>) -> ExtractionResult<Vec<usize>> { |
| 24 | + validate_target_witness( |
| 25 | + &self.target, |
| 26 | + bits, |
| 27 | + |value| value.0, |
| 28 | + "QUBO does not certify a staffing assignment", |
| 29 | + )?; |
| 30 | + (0..self.num_schedules) |
| 31 | + .map(|schedule| { |
| 32 | + let count: i64 = self |
| 33 | + .weights |
| 34 | + .iter() |
| 35 | + .enumerate() |
| 36 | + .filter(|(bit, _)| bits[schedule * self.weights.len() + bit]) |
| 37 | + .map(|(_, &weight)| weight) |
| 38 | + .sum(); |
| 39 | + usize::try_from(count).map_err(|_| { |
| 40 | + crate::rules::ExtractionError::invalid("worker count does not fit usize") |
| 41 | + }) |
| 42 | + }) |
| 43 | + .collect() |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +#[crate::aggregate_reduction(identity)] |
| 48 | +impl crate::rules::AggregateReductionResult for ReductionStaffSchedulingToQUBO {} |
| 49 | + |
| 50 | +#[crate::reduction(transform = upper_bound { |
| 51 | + num_vars = "63 * (num_schedules + num_periods + 1)", |
| 52 | + num_quadratic_terms = "(63 * (num_schedules + num_periods + 1))^2", |
| 53 | +})] |
| 54 | +impl ReduceTo<Decision<QUBO<i64>>> for StaffScheduling { |
| 55 | + type Result = ReductionStaffSchedulingToQUBO; |
| 56 | + |
| 57 | + fn reduce_to(&self) -> Result<Self::Result, ReductionError> { |
| 58 | + type Builder = QuboBuilder<StaffScheduling, Decision<QUBO<i64>>>; |
| 59 | + let overflow = Builder::overflow; |
| 60 | + let workers = self.num_workers(); |
| 61 | + // Certify the source's count fold before any target coefficient cancellation. |
| 62 | + Self::exact_i64(self.num_schedules(), "counting schedule patterns")? |
| 63 | + .checked_mul(workers) |
| 64 | + .ok_or_else(overflow)?; |
| 65 | + if self |
| 66 | + .requirements() |
| 67 | + .iter() |
| 68 | + .any(|&required| required > workers) |
| 69 | + { |
| 70 | + let (target, _) = Builder::new(0).finish()?; |
| 71 | + return Ok(Self::Result { |
| 72 | + target: Decision::new(target, -1), |
| 73 | + weights: Vec::new(), |
| 74 | + num_schedules: self.num_schedules(), |
| 75 | + }); |
| 76 | + } |
| 77 | + |
| 78 | + let weights = binary_weights(workers); |
| 79 | + let count_bits = Builder::dimension(self.num_schedules(), weights.len())?; |
| 80 | + let mut num_vars = count_bits.checked_add(weights.len()).ok_or_else(overflow)?; |
| 81 | + let surpluses: Vec<_> = self |
| 82 | + .requirements() |
| 83 | + .iter() |
| 84 | + .enumerate() |
| 85 | + .filter(|(_, required)| **required > 0) |
| 86 | + .map(|(period, &required)| (period, required, binary_weights(workers - required))) |
| 87 | + .collect(); |
| 88 | + for (_, _, surplus) in &surpluses { |
| 89 | + num_vars = num_vars.checked_add(surplus.len()).ok_or_else(overflow)?; |
| 90 | + } |
| 91 | + isize::try_from(Builder::dimension( |
| 92 | + num_vars, |
| 93 | + std::mem::size_of::<(usize, i64)>(), |
| 94 | + )?) |
| 95 | + .map_err(|_| overflow())?; |
| 96 | + |
| 97 | + let mut q = Builder::new(num_vars); |
| 98 | + let mut budget = Vec::new(); |
| 99 | + for schedule in 0..self.num_schedules() { |
| 100 | + budget.extend( |
| 101 | + weights |
| 102 | + .iter() |
| 103 | + .enumerate() |
| 104 | + .map(|(bit, &weight)| (schedule * weights.len() + bit, weight)), |
| 105 | + ); |
| 106 | + } |
| 107 | + budget.extend( |
| 108 | + weights |
| 109 | + .iter() |
| 110 | + .enumerate() |
| 111 | + .map(|(bit, &weight)| (count_bits + bit, weight)), |
| 112 | + ); |
| 113 | + q.affine_square(&budget, workers)?; |
| 114 | + let mut offset = count_bits + weights.len(); |
| 115 | + for (period, required, surplus) in surpluses { |
| 116 | + let mut coverage = Vec::new(); |
| 117 | + for (schedule, pattern) in self.schedules().iter().enumerate() { |
| 118 | + if pattern[period] { |
| 119 | + coverage.extend( |
| 120 | + weights |
| 121 | + .iter() |
| 122 | + .enumerate() |
| 123 | + .map(|(bit, &weight)| (schedule * weights.len() + bit, weight)), |
| 124 | + ); |
| 125 | + } |
| 126 | + } |
| 127 | + coverage.extend( |
| 128 | + surplus |
| 129 | + .iter() |
| 130 | + .enumerate() |
| 131 | + .map(|(bit, &weight)| (offset + bit, -weight)), |
| 132 | + ); |
| 133 | + q.affine_square(&coverage, required)?; |
| 134 | + offset += surplus.len(); |
| 135 | + } |
| 136 | + let (target, constant) = q.finish()?; |
| 137 | + Ok(Self::Result { |
| 138 | + target: Decision::new(target, constant.checked_neg().ok_or_else(overflow)?), |
| 139 | + weights, |
| 140 | + num_schedules: self.num_schedules(), |
| 141 | + }) |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +#[cfg(feature = "example-db")] |
| 146 | +pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> { |
| 147 | + vec![crate::example_db::specs::RuleExampleSpec { |
| 148 | + id: "staffscheduling_qubo", |
| 149 | + build: || { |
| 150 | + crate::example_db::specs::rule_example_with_witness::<_, Decision<QUBO<i64>>>( |
| 151 | + StaffScheduling::new( |
| 152 | + 2, |
| 153 | + vec![ |
| 154 | + vec![true, true, false], |
| 155 | + vec![false, true, true], |
| 156 | + vec![true, false, true], |
| 157 | + ], |
| 158 | + vec![2, 2, 2], |
| 159 | + 3, |
| 160 | + ), |
| 161 | + crate::export::SolutionPair { |
| 162 | + source_config: serde_json::json!([1, 1, 1]), |
| 163 | + target_config: serde_json::json!([ |
| 164 | + true, false, true, false, true, false, false, false, false, false, false |
| 165 | + ]), |
| 166 | + }, |
| 167 | + ) |
| 168 | + }, |
| 169 | + }] |
| 170 | +} |
0 commit comments