Skip to content

Commit b84842c

Browse files
authored
Merge pull request #1223 from CodingThrust/fix/qubo-scheduling-bounds
Tighten StaffScheduling and SWRTD QUBO size bounds
2 parents b4a4e06 + 0801132 commit b84842c

10 files changed

Lines changed: 82 additions & 13 deletions

‎docs/paper/reductions.typ‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12318,7 +12318,7 @@ _Solution extraction and bounds._ Copy the subset bits; aggregate recovery is id
1231812318

1231912319
_Correctness._ $arrow.r.double$: a feasible staffing vector has unused budget $z=W-sum_j c_j$ and nonnegative coverage surplus at most $W-r_t$, so all squares vanish. $arrow.l.double$: a target witness meeting the bound has $H<=0$. Every square is nonnegative, so the budget and coverage equations hold exactly. All decoded counts are nonnegative, their sum is at most $W$, and every positive requirement is met. Nonpositive requirements hold automatically. Unit penalties suffice because this endpoint is feasibility, with no competing objective.
1232012320

12321-
_Solution extraction and bounds._ Validate target feasibility, then sum each schedule's weighted bits. Aggregate `Or` recovery is identity, including NO results and the existing ExactCoverBy3Sets incoming route. With $b(B)=ceil(log_2(B+1))$, the exact variable count outside the early NO case is $(s+1)b(W)+sum_(t:r_t>0)b(W-r_t)$. For `i64` budgets this is at most $N=63(s+p+1)$, with at most $binom(63(s+1),2)+p(3969s+1953)$ registered couplings. The budget row already contains every count-count pair and every count-budget pair. Each period adds only count-surplus pairs (at most $63^2 s$) and within-surplus pairs (at most $binom(63,2)$); different periods' surplus blocks never interact. The coupling bound is therefore $O(s^2+s p+p)$, linear in periods for fixed schedule count. Construction performs $O((p+1)(s+1)^2+s+p)$ coefficient updates and row bookkeeping under the fixed 63-bit bound, since each coverage square may revisit count-count coefficients; ordered-map updates add a logarithmic factor. Bit counts grow logarithmically with the budget. Before expansion, check $s W$ to protect the source's count and coverage folds. Dimension arithmetic, expanded coefficients, omitted constant, and separate positive and negative coefficient sums are checked independently; an unrepresentable construction returns a typed overflow error. The conservative guard can reject representable special cases.
12321+
_Solution extraction and bounds._ Validate target feasibility, then sum each schedule's weighted bits. Aggregate `Or` recovery is identity, including NO results and the existing ExactCoverBy3Sets incoming route. With $b(B)=ceil(log_2(B+1))$, the exact variable count outside the early NO case is $(s+1)b(W)+sum_(t:r_t>0)b(W-r_t)$. With $beta$ the magnitude bits of $W$ (so $b(W-r_t)<=b(W)<=beta$), this is at most $N=beta(s+p+1)$, with at most $binom(beta(s+1),2)+p(beta^2 s+binom(beta,2))$ registered couplings. The budget row already contains every count-count pair and every count-budget pair. Each period adds only count-surplus pairs (at most $beta^2 s$) and within-surplus pairs (at most $binom(beta,2)$); different periods' surplus blocks never interact. The coupling bound is therefore $O(s^2+s p+p)$, linear in periods for fixed schedule count. Construction performs $O((p+1)(s+1)^2+s+p)$ coefficient updates and row bookkeeping for fixed $beta$, since each coverage square may revisit count-count coefficients; ordered-map updates add a logarithmic factor. Bit counts grow logarithmically with the budget. Before expansion, check $s W$ to protect the source's count and coverage folds. Dimension arithmetic, expanded coefficients, omitted constant, and separate positive and negative coefficient sums are checked independently; an unrepresentable construction returns a typed overflow error. The conservative guard can reject representable special cases.
1232212322

1232312323
*Example.* #let ex = load-example("StaffScheduling", "DecisionQUBO")
1232412324
The canonical source assigns #repr(ex.solutions.at(0).source_config) workers across its #ex.source.instance.schedules.len() patterns, with budget #ex.source.instance.num_workers. The required coverages are #repr(ex.source.instance.requirements). The count blocks, unused-budget block and coverage-surplus blocks produce the displayed target; its canonical bits make every residual zero.
@@ -13141,7 +13141,7 @@ These constructions use the checked sparse penalty and extraction contracts abov
1314113141
][
1314213142
_Construction._ First check that $max_i r_i+sum_i p_i$ fits the source integer type, bounding finish additions for every source order. Task $i$ chooses $t=r_i,dots,d_i-p_i$. An impossible window gives an empty row. Add squared one-hot rows and a unit pair penalty whenever neither selected task finishes before the other starts. Store the nonconstant energy with bound equal to the negative omitted constant.
1314313143
_Correctness._ ($arrow.r.double$) Earliest execution of a feasible source order gives valid choices without conflicts. ($arrow.l.double$) Zero-penalty choices are sorted by start, duration and index, placing zero-duration tasks first at a shared start. Inductively, earliest execution of that order starts no later than each selected start and finishes by its deadline. Source finish additions on recovered witnesses are bounded by these deadlines. Empty rows enforce NO and touching intervals are allowed.
13144-
_Extraction and size._ Decode and sort after target validation; completed feasibility maps identically. For $T=max_i d_i$, $S=sum_i max(0,d_i-p_i-r_i+1)<=n(T+1)$, with at most $S(S-1)/2$ couplings and $O(S^2)$ candidate pairs. This grid is pseudopolynomial in $T$, and can be much larger than the existing bounded-start route.
13144+
_Extraction and size._ Decode and sort after target validation; completed feasibility maps identically. For $T=max_i d_i$, $S=sum_i max(0,d_i-p_i-r_i+1)<=n w$, where $w<=T+1$ is the largest single start window, with at most $S(S-1)/2$ couplings and $O(S^2)$ candidate pairs. This grid is pseudopolynomial in $T$, and can be much larger than the existing bounded-start route.
1314513145

1314613146
*Example.* #let ex = load-example("SequencingWithReleaseTimesAndDeadlines", "DecisionQUBO")
1314713147
The canonical source witness #repr(ex.solutions.at(0).source_config) maps to a target with #ex.target.instance.inner.num_vars variables; the exact source and target configurations are shown below.
@@ -17893,6 +17893,8 @@ The following table shows concrete target-variable counts for example instances,
1789317893

1789417894
_Correctness._ ($arrow.r.double$) An exact cover $I$ with $|I| = q$ assigns one worker per selected schedule, covering each period exactly once. ($arrow.l.double$) A feasible assignment with $sum_j w_j = q$ and each period covered exactly once gives pairwise-disjoint schedules covering all of $X$.
1789517895

17896+
_Size._ The budget $q$ has at most $q+1$ magnitude bits; transforms carry no logarithm, so the declared bound is linear in $|X|/3$.
17897+
1789617898
_Solution extraction._ Map $w in NN^m$ to $c in {0,1}^m$ by $c[j] = 1$ if $w[j] > 0$.
1789717899
]
1789817900

@@ -19902,6 +19904,8 @@ The following table shows concrete target-variable counts for example instances,
1990219904

1990319905
_Correctness._ ($arrow.r.double$) A valid 3-partition fills each slot of width $B$ with exactly 3 elements; fillers are placed in their tight windows. ($arrow.l.double$) Fillers pinned to unit windows create $m$ slots of width $B$; total element work $m B$ fills all slots exactly; size constraints force 3 elements per slot.
1990419906

19907+
_Size._ Every release time and deadline lies in $[0, m(B+1)-1]$, so no task has more than $m(B+1)$ start times.
19908+
1990519909
_Solution extraction._ Decode the Lehmer code, simulate the schedule tracking start times; assign elements to groups by $floor("start" / (B+1))$.
1990619910
]
1990719911

‎src/models/misc/sequencing_with_release_times_and_deadlines.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,25 @@ impl SequencingWithReleaseTimesAndDeadlines {
137137
self.lengths.len()
138138
}
139139

140+
/// Returns the largest number of in-window start times of any task.
141+
pub fn max_start_slots(&self) -> u64 {
142+
self.lengths
143+
.iter()
144+
.zip(&self.release_times)
145+
.zip(&self.deadlines)
146+
.map(|((&length, &release), &deadline)| {
147+
// Nonnegative inputs keep `deadline - release` and the slack in range.
148+
let slack = deadline - release;
149+
if slack < length {
150+
0
151+
} else {
152+
(slack - length).unsigned_abs() + 1
153+
}
154+
})
155+
.max()
156+
.unwrap_or(0)
157+
}
158+
140159
/// Returns the time horizon (maximum deadline).
141160
pub fn time_horizon(&self) -> i64 {
142161
self.deadlines.iter().copied().max().unwrap_or(0)
@@ -154,6 +173,7 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines {
154173
type Value = crate::types::Or;
155174

156175
crate::problem_parameters![
176+
("max_start_slots", max_start_slots),
157177
("num_tasks", num_tasks),
158178
("time_horizon", time_horizon),
159179
("time_horizon_bits", time_horizon_bits),

‎src/models/misc/staff_scheduling.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ impl StaffScheduling {
163163
self.num_workers
164164
}
165165

166+
/// Magnitude bits of the worker budget, with at least one bit.
167+
pub fn num_workers_bits(&self) -> u64 {
168+
crate::types::max_numeric_magnitude_bits([self.num_workers])
169+
}
170+
166171
/// Get the number of schedule patterns.
167172
pub fn num_schedules(&self) -> usize {
168173
self.schedules.len()
@@ -232,6 +237,7 @@ impl Problem for StaffScheduling {
232237
("num_periods", num_periods),
233238
("num_schedules", num_schedules),
234239
("num_workers", num_workers),
240+
("num_workers_bits", num_workers_bits),
235241
];
236242

237243
fn evaluate(

‎src/rules/exactcoverby3sets_staffscheduling.rs‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,14 @@ impl ReductionResult for ReductionXC3SToStaffScheduling {
5252
impl crate::rules::AggregateReductionResult for ReductionXC3SToStaffScheduling {}
5353

5454
#[reduction(
55-
transform = exact {
56-
num_periods = "universe_size",
57-
num_schedules = "num_subsets",
58-
num_workers = "universe_size / 3",
55+
transform = {
56+
exact {
57+
num_periods = "universe_size",
58+
num_schedules = "num_subsets",
59+
num_workers = "universe_size / 3",
60+
},
61+
// Transforms have no logarithm; q = universe_size / 3 has at most q + 1 bits.
62+
upper_bound { num_workers_bits = "universe_size / 3 + 1", },
5963
}
6064
)]
6165
impl ReduceTo<StaffScheduling> for ExactCoverBy3Sets {

‎src/rules/sequencingwithreleasetimesanddeadlines_qubo.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ impl crate::rules::AggregateReductionResult
3535
for ReductionSequencingWithReleaseTimesAndDeadlinesToQUBO
3636
{
3737
}
38-
#[crate::reduction(transform = upper_bound { num_vars = "num_tasks * (time_horizon + 1)", num_quadratic_terms = "(num_tasks * (time_horizon + 1))^2" })]
38+
#[crate::reduction(transform = upper_bound { num_vars = "num_tasks * max_start_slots", num_quadratic_terms = "num_tasks * max_start_slots * (num_tasks * max_start_slots - 1) / 2" })]
3939
impl ReduceTo<Decision<QUBO<i64>>> for SequencingWithReleaseTimesAndDeadlines {
4040
type Result = ReductionSequencingWithReleaseTimesAndDeadlinesToQUBO;
4141
fn reduce_to(&self) -> Result<Self::Result, ReductionError> {

‎src/rules/staffscheduling_qubo.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ impl ReductionResult for ReductionStaffSchedulingToQUBO {
4848
impl crate::rules::AggregateReductionResult for ReductionStaffSchedulingToQUBO {}
4949

5050
#[crate::reduction(transform = upper_bound {
51-
num_vars = "63 * (num_schedules + num_periods + 1)",
52-
num_quadratic_terms = "63 * (num_schedules + 1) * (63 * (num_schedules + 1) - 1) / 2 + num_periods * (3969 * num_schedules + 1953)",
51+
num_vars = "num_workers_bits * (num_schedules + num_periods + 1)",
52+
num_quadratic_terms = "num_workers_bits * (num_schedules + 1) * (num_workers_bits * (num_schedules + 1) - 1) / 2 + num_periods * num_workers_bits * (num_workers_bits * num_schedules + (num_workers_bits - 1) / 2)",
5353
})]
5454
impl ReduceTo<Decision<QUBO<i64>>> for StaffScheduling {
5555
type Result = ReductionStaffSchedulingToQUBO;

‎src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ impl crate::rules::AggregateReductionResult for ReductionThreePartitionToSRTD {}
101101
num_tasks = "num_elements + num_groups - 1",
102102
time_horizon = "num_groups * (bound + 1) - 1",
103103
},
104-
upper_bound { time_horizon_bits = "max_numeric_magnitude_bits + num_groups + 1", },
104+
// Each task's start window lies within [0, time_horizon].
105+
upper_bound {
106+
time_horizon_bits = "max_numeric_magnitude_bits + num_groups + 1",
107+
max_start_slots = "num_groups * (bound + 1)",
108+
},
105109
},
106110
)]
107111
impl ReduceTo<SequencingWithReleaseTimesAndDeadlines> for ThreePartition {

‎src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ fn test_sequencing_rtd_basic() {
3939
assert_eq!(problem.release_times(), &[0, 1, 5, 0, 8]);
4040
assert_eq!(problem.deadlines(), &[5, 6, 10, 3, 12]);
4141
assert_eq!(problem.time_horizon(), 12);
42+
// Start windows hold [3, 4, 2, 3, 3] start times.
43+
assert_eq!(problem.max_start_slots(), 4);
4244
// Lehmer code dims: [5, 4, 3, 2, 1]
4345
assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]);
4446
assert_eq!(
@@ -97,6 +99,7 @@ fn test_sequencing_rtd_empty_instance() {
9799
let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![], vec![], vec![]);
98100
assert_eq!(problem.num_tasks(), 0);
99101
assert_eq!(problem.time_horizon(), 0);
102+
assert_eq!(problem.max_start_slots(), 0);
100103
assert_eq!(problem.dimensions(), Vec::<usize>::new());
101104
assert!(problem.evaluate(&vec![]).unwrap());
102105
}
@@ -172,3 +175,17 @@ fn test_sequencing_rtd_invalid_task_index() {
172175
Err(crate::traits::EvaluationError::InvalidConfiguration(_))
173176
));
174177
}
178+
179+
#[test]
180+
fn test_sequencing_rtd_max_start_slots_extremes() {
181+
// A release after the deadline or a task longer than its window has no start.
182+
let closed = SequencingWithReleaseTimesAndDeadlines::new(vec![0, 3], vec![5, 0], vec![4, 2]);
183+
assert_eq!(closed.max_start_slots(), 0);
184+
// The widest window, [0, i64::MAX] with length 0, has 2^63 starts.
185+
let widest = SequencingWithReleaseTimesAndDeadlines::new(
186+
vec![0, i64::MAX],
187+
vec![0, i64::MAX],
188+
vec![i64::MAX, 0],
189+
);
190+
assert_eq!(widest.max_start_slots(), 1 << 63);
191+
}

‎src/unit_tests/models/misc/staff_scheduling.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ fn test_staff_scheduling_creation() {
6262
assert_eq!(problem.num_schedules(), 5);
6363
assert_eq!(problem.requirements(), &[2, 2, 2, 3, 3, 2, 1]);
6464
assert_eq!(problem.num_workers(), 4);
65+
assert_eq!(problem.num_workers_bits(), 3);
6566
assert_eq!(problem.dimensions(), vec![5; 5]);
6667
}
6768

@@ -149,3 +150,9 @@ fn test_staff_scheduling_problem_name_and_variant() {
149150
assert_eq!(<StaffScheduling as Problem>::NAME, "StaffScheduling");
150151
assert!(<StaffScheduling as Problem>::variant().is_empty());
151152
}
153+
154+
#[test]
155+
fn test_staff_scheduling_zero_workers_use_one_bit() {
156+
let problem = StaffScheduling::new(1, vec![vec![true]], vec![0], 0);
157+
assert_eq!(problem.num_workers_bits(), 1);
158+
}

‎src/unit_tests/parameter_formula_validation.rs‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,8 @@ fn direct_qubo_sparse_support_bounds_avoid_dense_cliques() {
417417
use crate::models::algebraic::QUBO;
418418
use crate::models::graph::OptimalLinearArrangement;
419419
use crate::models::misc::{
420-
CyclicOrdering, ShortestCommonSuperstring, StackerCrane, StaffScheduling, ThreePartition,
420+
CyclicOrdering, SequencingWithReleaseTimesAndDeadlines, ShortestCommonSuperstring,
421+
StackerCrane, StaffScheduling, ThreePartition,
421422
};
422423
use crate::models::Decision;
423424
use crate::rules::{ReduceTo, ReductionResult};
@@ -452,7 +453,8 @@ fn direct_qubo_sparse_support_bounds_avoid_dense_cliques() {
452453
S::NAME
453454
);
454455
}
455-
// Budget clique plus separate per-period surplus blocks, at most 63 bits per integer.
456+
// Budget clique plus separate per-period surplus blocks, two bits for W = 3:
457+
// binom(8, 2) + 3 * (2^2 * 3 + binom(2, 2)).
456458
check::<_, Decision<QUBO<i64>>>(
457459
StaffScheduling::new(
458460
2,
@@ -464,7 +466,12 @@ fn direct_qubo_sparse_support_bounds_avoid_dense_cliques() {
464466
vec![2, 2, 2],
465467
3,
466468
),
467-
73_206,
469+
67,
470+
);
471+
// Three tasks with at most three start times each: binom(9, 2) couplings.
472+
check::<_, Decision<QUBO<i64>>>(
473+
SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]),
474+
36,
468475
);
469476
// Triangle terms, path/cycle transitions, and eligible value-triple multiplicities.
470477
// Vanishing factors must remain nonnegative and valid on empty/singleton inputs.

0 commit comments

Comments
 (0)