200 lines
8.1 KiB
Rust
200 lines
8.1 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.
|
||
// SplitMix64,因其只有几行且周期长而被选用。
|
||
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)`.
|
||
///
|
||
/// 返回 `[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.
|
||
///
|
||
/// 构建一个随机场景并返回两个答案,任务完全放不下时返回 `None`。
|
||
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.
|
||
// 枚举,通过请求一个开始并以原方式检验每个候选来达到。公开入口是扫描,故枚举是
|
||
// 经 `feasible_at` 在原所构建的同一候选集上被检验。
|
||
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);
|
||
}
|