apsis-rs/crates/ops/apsis-scheduler/tests/precedence.rs

321 lines
12 KiB
Rust

//! Tests for precedence honoured by the planner, and for the improvement
//! pass.
//!
//! 规划器所遵守的先后约束,以及改进遍的测试。
use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
use apsis_scheduler::{ImprovementSettings, Planner};
use apsis_task::{PrecedenceGraph, Task, TaskId};
/// A resource available from 0 to 1 000 seconds.
///
/// 一个自 0 到 1 000 秒可用的资源。
fn resource(id: usize, capacity: f64) -> Resource {
let mut resource = Resource::new(ResourceId::new(id), capacity);
resource
.set_windows(&[Window::new(0.0, 1_000.0)])
.expect("the window is usable");
resource
}
/// A task requiring one unit of each named resource.
///
/// 一个需要每个指名资源一单位的任务。
fn task(id: usize, duration: f64, resources: &[usize]) -> Task {
let mut task = Task::new(TaskId::new(id), duration);
for resource in resources {
task = task
.try_require(ResourceId::new(*resource), 1.0)
.expect("the resource is new");
}
task
}
#[test]
fn a_chain_of_tasks_is_placed_in_order() {
// Three tasks that must run one after another on the same resource.
// 三个必须在同一资源上依次运行的任务。
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = [
task(0, 60.0, &[0]),
task(1, 30.0, &[0]),
task(2, 90.0, &[0]),
];
let mut graph = PrecedenceGraph::new();
graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle");
graph.add(TaskId::new(1), TaskId::new(2)).expect("no cycle");
let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph);
assert!(result.is_complete(), "{:?}", result.outcomes);
assert!(result.honours_precedence(&graph));
let first = result
.schedule
.placement_of(TaskId::new(0))
.expect("placed");
let second = result
.schedule
.placement_of(TaskId::new(1))
.expect("placed");
let third = result
.schedule
.placement_of(TaskId::new(2))
.expect("placed");
assert_eq!(first.start, 0.0);
assert_eq!(second.start, 60.0);
assert_eq!(third.start, 90.0);
}
#[test]
fn a_precedence_floor_delays_a_task_that_could_start_earlier() {
// The second task could start at zero on its own, but must wait for the
// first, which runs on a different resource.
// 第二个任务本身可以从零开始,但必须等待在另一资源上运行的第一个。
let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct");
let tasks = [task(0, 100.0, &[0]), task(1, 60.0, &[1])];
let mut graph = PrecedenceGraph::new();
graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle");
let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph);
assert!(result.is_complete());
let second = result
.schedule
.placement_of(TaskId::new(1))
.expect("placed");
// The second resource was free from the start, but the constraint delays
// the task to the end of the first.
// 第二个资源从一开始就空闲,但该约束把任务推迟到第一个结束。
assert_eq!(second.start, 100.0);
assert!(result.honours_precedence(&graph));
}
#[test]
fn a_diamond_precedence_is_honoured() {
let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct");
// The first task feeds two parallel tasks, which both feed a last one.
// 第一个任务供给两个并行任务,二者又都供给最后一个。
let tasks = [
task(0, 100.0, &[0]),
task(1, 60.0, &[1]),
task(2, 60.0, &[1]),
task(3, 30.0, &[0]),
];
let mut graph = PrecedenceGraph::new();
graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle");
graph.add(TaskId::new(0), TaskId::new(2)).expect("no cycle");
graph.add(TaskId::new(1), TaskId::new(3)).expect("no cycle");
graph.add(TaskId::new(2), TaskId::new(3)).expect("no cycle");
let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph);
assert!(result.is_complete(), "{:?}", result.outcomes);
assert!(result.honours_precedence(&graph));
// The last task must follow both of the parallel ones.
// 最后一个任务必须跟随两个并行任务。
let last = result
.schedule
.placement_of(TaskId::new(3))
.expect("placed");
for parallel in [1_usize, 2] {
let placement = result
.schedule
.placement_of(TaskId::new(parallel))
.expect("placed");
assert!(
placement.end() <= last.start,
"{placement:?} against {last:?}"
);
}
}
#[test]
fn a_sequential_observation_and_downlink_is_expressed_as_two_tasks() {
// An observation uses the payload and a downlink uses the transmitter,
// and the downlink must follow the observation. This is the pattern for
// a task that needs two resources in sequence.
// 一次观测使用载荷、一次下行使用发射机,且下行必须跟随观测。这是按序需要两种
// 资源的任务模式。
let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)]).expect("distinct");
let observation = task(0, 120.0, &[0]).with_window(50.0, 500.0);
let downlink = task(1, 90.0, &[1]).with_window(0.0, 1_000.0);
let tasks = [observation, downlink];
let mut graph = PrecedenceGraph::new();
graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle");
let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph);
assert!(result.is_complete());
assert!(result.honours_precedence(&graph));
let collection = result
.schedule
.placement_of(TaskId::new(0))
.expect("placed");
let transmission = result
.schedule
.placement_of(TaskId::new(1))
.expect("placed");
assert_eq!(collection.start, 50.0);
// The downlink starts when the collection ends.
// 下行在采集结束时开始。
assert_eq!(transmission.start, 170.0);
// The two use different resources, so they may be placed back to back
// without a conflict.
// 二者使用不同资源,故它们可前后相接地放置而无冲突。
assert_eq!(collection.end(), transmission.start);
}
#[test]
fn a_successor_of_an_unplaced_task_is_deferred() {
// The first task cannot be placed, so the constraint cannot be met and
// the second is deferred rather than placed early.
// 第一个任务无法放置,故约束无法满足,第二个被推迟而非提前放置。
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let impossible = task(0, 90.0, &[0]).with_window(0.0, 60.0);
let successor = task(1, 30.0, &[0]);
let tasks = [impossible, successor];
let mut graph = PrecedenceGraph::new();
graph.add(TaskId::new(0), TaskId::new(1)).expect("no cycle");
let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph);
assert!(!result.schedule.is_placed(TaskId::new(1)));
assert!(result.deferred().contains(&TaskId::new(1)));
// The constraints are trivially satisfied, because nothing was promised.
// 各约束空洞地满足,因为什么都没承诺。
assert!(result.honours_precedence(&graph));
}
#[test]
fn the_precedence_aware_order_is_topological() {
let tasks = [
Task::new(TaskId::new(0), 60.0),
Task::new(TaskId::new(1), 60.0),
Task::new(TaskId::new(2), 60.0),
];
let mut graph = PrecedenceGraph::new();
graph.add(TaskId::new(2), TaskId::new(0)).expect("no cycle");
let order = Planner::new().order_with_precedence(&tasks, Some(&graph));
let position = |id: TaskId| {
order
.iter()
.position(|candidate| *candidate == id)
.expect("present")
};
assert!(position(TaskId::new(2)) < position(TaskId::new(0)));
}
#[test]
fn a_cycle_in_the_graph_falls_back_to_the_value_order() {
// The graph refuses a cycle, so a cycle can only exist if the caller
// builds the graph from one task set and orders another. The planner
// then falls back to the value order rather than failing.
// 该图拒绝环,故只有调用方从一套任务集构建图而排序另一套时环才可能存在。规划器
// 随后回退到价值次序而非失败。
let tasks = [
Task::new(TaskId::new(0), 60.0),
Task::new(TaskId::new(1), 60.0),
];
let graph = PrecedenceGraph::new();
let order = Planner::new().order_with_precedence(&tasks, Some(&graph));
// An empty graph gives the value order, which for equal values is by
// identifier.
// 空图给出价值次序,价值相等时按标识符。
assert_eq!(order, vec![TaskId::new(0), TaskId::new(1)]);
}
#[test]
fn the_improvement_pass_is_skipped_when_precedence_is_used() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = [task(0, 60.0, &[0]), task(1, 60.0, &[0])];
let graph = PrecedenceGraph::new();
let result = Planner::new().schedule_with_precedence(&tasks, &resources, &graph);
// The pass does not run, so nothing is reported as improved.
// 该遍不运行,故没有报告改进。
assert!(result.improved.is_empty());
}
#[test]
fn the_improvement_pass_can_be_disabled() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let optional = task(0, 60.0, &[0])
.with_priority(100.0)
.with_window(0.0, 1_000.0);
let mandatory = task(1, 60.0, &[0])
.with_priority(1.0)
.with_window(0.0, 100.0)
.with_mandatory(true);
let tasks = [optional, mandatory];
let planner = Planner::new()
.with_mandatory_first(false)
.with_improvement(ImprovementSettings::disabled());
let result = planner.schedule(&tasks, &resources);
assert!(result.improved.is_empty());
assert_eq!(result.schedule.len(), 1);
}
#[test]
fn the_improvement_pass_preserves_feasibility() {
// Whatever the pass decides, the result must remain conflict free.
// 无论该遍如何决定,结果必须保持无冲突。
let resources =
ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 2.0)]).expect("distinct");
let tasks: Vec<Task> = (0..10)
.map(|index| {
task(index, 40.0 + f64::from(index as u32) * 5.0, &[index % 2])
.with_priority(1.0 + f64::from(index as u32))
})
.collect();
let result = Planner::new().schedule(&tasks, &resources);
// The schedule must not place two tasks beyond the capacity of a
// resource at the same time.
// 调度不得在同一时刻把两个任务放置到超出某资源的容量。
let report = apsis_conflict::detect(&result.schedule, &tasks, &resources);
assert!(
report.is_clear(),
"the improvement pass broke feasibility: {:?}",
report.conflicts()
);
}
#[test]
fn the_improvement_pass_never_loses_a_placement() {
// Every task that the greedy pass placed must still be placed
// afterwards, because the pass only adds placements or moves them.
// 贪心遍所放置的每个任务其后必须仍被放置,因为该遍只添加或移动放置。
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks: Vec<Task> = (0..6)
.map(|index| task(index, 60.0, &[0]).with_priority(1.0 + f64::from(index as u32)))
.collect();
let with_pass = Planner::new().schedule(&tasks, &resources);
let without_pass = Planner::new()
.with_improvement(ImprovementSettings::disabled())
.schedule(&tasks, &resources);
assert!(
with_pass.schedule.len() >= without_pass.schedule.len(),
"{} against {}",
with_pass.schedule.len(),
without_pass.schedule.len()
);
}
#[test]
fn the_improvement_settings_are_validated_by_use() {
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
let tasks = [task(0, 60.0, &[0])];
// A bound of zero disables the move search but leaves the pass enabled,
// which behaves as if the pass were off.
// 界为零禁用移动搜索但保留该遍启用,其行为如同该遍关闭。
let planner = Planner::new().with_improvement(ImprovementSettings::new(0));
let result = planner.schedule(&tasks, &resources);
assert!(result.is_complete());
assert!(result.improved.is_empty());
}