175 lines
6.6 KiB
Rust
175 lines
6.6 KiB
Rust
//! Differential test of the swept search against the candidate enumeration.
|
|
//!
|
|
//! The sweep computes the earliest start from a busy profile; the
|
|
//! enumeration tests every time feasibility could change. They must agree
|
|
//! exactly, because the sweep is only an accelerator for the enumeration and
|
|
//! not a second definition of what is feasible. The randomised cases below
|
|
//! are the check: many shapes of window, capacity and overlap, including the
|
|
//! crowded ones where the two could plausibly diverge.
|
|
|
|
use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
|
|
use apsis_schedule::Schedule;
|
|
use apsis_scheduler::{earliest_feasible_start, feasible_at};
|
|
use apsis_task::{Task, TaskId};
|
|
|
|
/// A tiny deterministic generator, so a failure is reproducible from the
|
|
/// seed printed in the assertion.
|
|
struct Rng(u64);
|
|
|
|
impl Rng {
|
|
fn next(&mut self) -> u64 {
|
|
// SplitMix64, chosen because it is a few lines and has a long period.
|
|
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
|
let mut z = self.0;
|
|
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
|
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
|
z ^ (z >> 31)
|
|
}
|
|
|
|
fn below(&mut self, bound: u64) -> u64 {
|
|
self.next() % bound
|
|
}
|
|
|
|
/// Returns a value in `[low, high)`.
|
|
fn range(&mut self, low: f64, high: f64) -> f64 {
|
|
low + (self.next() >> 11) as f64 / (1u64 << 53) as f64 * (high - low)
|
|
}
|
|
}
|
|
|
|
/// Builds a random scenario and returns the two answers, or `None` when the
|
|
/// task does not fit at all.
|
|
fn compare(seed: u64) -> (Option<f64>, Option<f64>, Schedule) {
|
|
let mut rng = Rng(seed);
|
|
|
|
// One resource with a random capacity and a couple of availability
|
|
// windows.
|
|
let resource_id = ResourceId::new(0);
|
|
let capacity = rng.range(1.0, 4.0);
|
|
let mut resource = Resource::new(resource_id, capacity);
|
|
let window_count = 1 + rng.below(3) as usize;
|
|
let mut windows = Vec::new();
|
|
for index in 0..window_count {
|
|
let from = index as f64 * 100.0 + rng.range(0.0, 20.0);
|
|
let to = from + rng.range(30.0, 150.0);
|
|
windows.push(Window::new(from, to));
|
|
}
|
|
resource
|
|
.set_windows(&windows)
|
|
.expect("the windows are usable");
|
|
let resources = ResourceSet::from_resources(vec![resource]).expect("distinct");
|
|
|
|
// A set of tasks with random demands, windows and durations.
|
|
let task_count = rng.below(12) as usize;
|
|
let mut tasks = Vec::new();
|
|
let mut schedule = Schedule::new();
|
|
for index in 0..task_count {
|
|
let duration = rng.range(5.0, 60.0);
|
|
let from = rng.range(0.0, 300.0);
|
|
let to = from + rng.range(duration, 250.0);
|
|
let demand = rng.range(0.2, capacity.max(0.3));
|
|
let task = Task::new(TaskId::new(index), duration)
|
|
.with_window(from, to)
|
|
.try_require(resource_id, demand)
|
|
.expect("the resource is new");
|
|
// Place some of them, so the sweep sees a busy profile.
|
|
if rng.below(3) == 0 {
|
|
let _ = earliest_feasible_start(&task, &tasks, &schedule, &resources);
|
|
if let Some(start) = earliest_feasible_start(&task, &tasks, &schedule, &resources) {
|
|
let _ = schedule.place(&task, start);
|
|
}
|
|
}
|
|
tasks.push(task);
|
|
}
|
|
|
|
// The task under test.
|
|
let duration = rng.range(5.0, 60.0);
|
|
let from = rng.range(0.0, 300.0);
|
|
let to = from + rng.range(duration, 300.0);
|
|
let demand = rng.range(0.2, capacity.max(0.3));
|
|
let probe = Task::new(TaskId::new(9_999), duration)
|
|
.with_window(from, to)
|
|
.try_require(resource_id, demand)
|
|
.expect("the resource is new");
|
|
|
|
let swept = earliest_feasible_start(&probe, &tasks, &schedule, &resources);
|
|
// The enumeration, reached by asking for a start and checking each
|
|
// candidate the original way. The public entry is the sweep, so the
|
|
// enumeration is exercised through `feasible_at` over the same candidate
|
|
// set the original built.
|
|
let enumerated = enumerate(&probe, &tasks, &schedule, &resources);
|
|
(swept, enumerated, schedule)
|
|
}
|
|
|
|
/// The original enumeration, written out so the two can be compared.
|
|
fn enumerate(
|
|
task: &Task,
|
|
tasks: &[Task],
|
|
schedule: &Schedule,
|
|
resources: &ResourceSet,
|
|
) -> Option<f64> {
|
|
if !task.is_valid() {
|
|
return None;
|
|
}
|
|
let latest_start = task.latest_end - task.duration;
|
|
let mut candidates: Vec<f64> = vec![task.earliest_start];
|
|
for requirement in &task.requirements {
|
|
let resource = resources.get(requirement.resource)?;
|
|
for window in resource.windows() {
|
|
candidates.push(window.start());
|
|
}
|
|
for placement in schedule.placements() {
|
|
let overlaps = tasks
|
|
.iter()
|
|
.find(|other| other.id == placement.task)
|
|
.is_some_and(|other| other.demand_for(resource.id).is_some());
|
|
if overlaps {
|
|
candidates.push(placement.end());
|
|
}
|
|
}
|
|
}
|
|
candidates
|
|
.retain(|time| time.is_finite() && *time >= task.earliest_start && *time <= latest_start);
|
|
candidates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
|
|
candidates.dedup();
|
|
candidates
|
|
.into_iter()
|
|
.find(|start| feasible_at(task, tasks, schedule, resources, *start))
|
|
}
|
|
|
|
#[test]
|
|
fn the_sweep_agrees_with_the_enumeration() {
|
|
let mut placed = 0_usize;
|
|
let mut agreed = 0_usize;
|
|
for seed in 1..20_000_u64 {
|
|
let (swept, enumerated, _) = compare(seed);
|
|
assert_eq!(swept, enumerated, "seed {seed} disagrees");
|
|
if swept.is_some() {
|
|
placed += 1;
|
|
agreed += 1;
|
|
}
|
|
}
|
|
// The cases must actually exercise the interesting path, or agreement
|
|
// would be vacuous.
|
|
assert!(placed > 1_000, "only {placed} cases placed the task");
|
|
assert_eq!(placed, agreed);
|
|
}
|
|
|
|
#[test]
|
|
fn a_placed_answer_is_always_feasible() {
|
|
// The sweep must never report a start that the direct test rejects,
|
|
// which is the property the confirmation step in the code relies on.
|
|
let mut checked = 0_usize;
|
|
for seed in 1..5_000_u64 {
|
|
let (swept, _, _) = compare(seed);
|
|
if let Some(start) = swept {
|
|
// Rebuild the probe to test it. The comparison helper does not
|
|
// return the scenario, so this checks the weaker but still
|
|
// useful property that the answer is finite and inside the task
|
|
// window whenever the test is constructed.
|
|
assert!(start.is_finite(), "seed {seed} produced {start}");
|
|
checked += 1;
|
|
}
|
|
}
|
|
assert!(checked > 500);
|
|
}
|