316 lines
12 KiB
Rust
316 lines
12 KiB
Rust
//! The conflict detection itself.
|
|
|
|
use crate::conflict::{Conflict, ConflictKind, ConflictReport};
|
|
use apsis_resource::{ResourceId, ResourceSet};
|
|
use apsis_schedule::Schedule;
|
|
use apsis_task::{Task, TaskId};
|
|
|
|
/// A precedence constraint: one task must finish before another starts.
|
|
///
|
|
/// 一条优先约束:一个任务必须先于另一个开始而完成。
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct Precedence {
|
|
/// The task that must finish first.
|
|
///
|
|
/// 必须先完成的任务。
|
|
pub before: TaskId,
|
|
/// The task that must start after.
|
|
///
|
|
/// 必须后开始的任务。
|
|
pub after: TaskId,
|
|
}
|
|
|
|
impl Precedence {
|
|
/// Creates a precedence constraint.
|
|
///
|
|
/// 创建一条优先约束。
|
|
pub const fn new(before: TaskId, after: TaskId) -> Self {
|
|
Self { before, after }
|
|
}
|
|
}
|
|
|
|
/// Returns the placements of a task, which is at most one.
|
|
///
|
|
/// 返回某任务的放置,至多一处。
|
|
pub fn placements_of(schedule: &Schedule, task: TaskId) -> Option<&apsis_schedule::Placement> {
|
|
schedule.placement_of(task)
|
|
}
|
|
|
|
/// Returns every conflict in a schedule.
|
|
///
|
|
/// The checks are performed in full: the report contains every problem found,
|
|
/// not just the first. That is deliberate, because a planner that is handed a
|
|
/// plan usually wants to know how bad it is before deciding whether to repair
|
|
/// or rebuild.
|
|
///
|
|
/// The checks are:
|
|
///
|
|
/// - every placement's task exists in the list, and the durations agree;
|
|
/// - every placement falls inside its task's window;
|
|
/// - no resource exceeds its capacity at any instant;
|
|
/// - every placement falls inside the availability windows of each resource
|
|
/// it requires.
|
|
///
|
|
/// The resource sweep examines the boundaries of the placements, because the
|
|
/// total demand on a resource is constant between them, so the check is exact
|
|
/// and takes time proportional to the number of placements rather than to the
|
|
/// length of the plan.
|
|
///
|
|
/// 返回计划中的所有冲突。
|
|
///
|
|
/// 检查是完整执行的:报告包含所有发现的问题,而非只有首个。这是有意
|
|
/// 的,因为拿到一份计划的规划器通常希望先知道它有多糟,再决定修复还是
|
|
/// 重建。
|
|
///
|
|
/// 检查项为:
|
|
///
|
|
/// - 每处放置的任务都在列表中,且时长一致;
|
|
/// - 每处放置都落在其任务窗口内;
|
|
/// - 任何瞬时没有资源超出其容量;
|
|
/// - 每处放置都落在其所要求的每个资源的可用窗口内。
|
|
///
|
|
/// 资源扫掠检查各放置的边界,因为资源上的总需求在相邻边界之间恒定,
|
|
/// 故该检查是精确的,耗时正比于放置数而非计划长度。
|
|
pub fn detect(schedule: &Schedule, tasks: &[Task], resources: &ResourceSet) -> ConflictReport {
|
|
let mut conflicts = Vec::new();
|
|
|
|
// The requirement and window checks need only the placement and its
|
|
// task.
|
|
// 需求与窗口检查只需放置及其任务。
|
|
for placement in schedule.placements() {
|
|
let task = tasks.iter().find(|task| task.id == placement.task);
|
|
let Some(task) = task else {
|
|
conflicts.push(
|
|
Conflict::new(
|
|
ConflictKind::Requirement,
|
|
placement.task,
|
|
placement.start,
|
|
"the task is not in the task list",
|
|
)
|
|
.with_resource(ResourceId::new(0)),
|
|
);
|
|
continue;
|
|
};
|
|
|
|
if (placement.duration - task.duration).abs() > 1e-12 {
|
|
conflicts.push(Conflict::new(
|
|
ConflictKind::Requirement,
|
|
placement.task,
|
|
placement.start,
|
|
format!(
|
|
"placed for {} s but the task lasts {} s",
|
|
placement.duration, task.duration
|
|
),
|
|
));
|
|
}
|
|
|
|
if placement.start < task.earliest_start {
|
|
conflicts.push(Conflict::new(
|
|
ConflictKind::Window,
|
|
placement.task,
|
|
placement.start,
|
|
format!(
|
|
"starts at {}, before the window opens at {}",
|
|
placement.start, task.earliest_start
|
|
),
|
|
));
|
|
}
|
|
if placement.end() > task.latest_end {
|
|
conflicts.push(Conflict::new(
|
|
ConflictKind::Window,
|
|
placement.task,
|
|
placement.end(),
|
|
format!(
|
|
"ends at {}, after the window closes at {}",
|
|
placement.end(),
|
|
task.latest_end
|
|
),
|
|
));
|
|
}
|
|
}
|
|
|
|
// The availability check: a placement must fall inside a window of every
|
|
// resource it requires.
|
|
// 可用性检查:每处放置必须落在其所要求的每个资源的某个窗口内。
|
|
for placement in schedule.placements() {
|
|
let Some(task) = tasks.iter().find(|task| task.id == placement.task) else {
|
|
continue;
|
|
};
|
|
for requirement in &task.requirements {
|
|
let Some(resource) = resources.get(requirement.resource) else {
|
|
conflicts.push(
|
|
Conflict::new(
|
|
ConflictKind::Requirement,
|
|
placement.task,
|
|
placement.start,
|
|
format!("{} does not exist", requirement.resource),
|
|
)
|
|
.with_resource(requirement.resource),
|
|
);
|
|
continue;
|
|
};
|
|
// A demand greater than the capacity can never be met, which is a
|
|
// requirement problem rather than a placement problem.
|
|
// 超过容量的需求永远无法满足,故属需求问题而非放置问题。
|
|
if requirement.demand > resource.capacity() + 1e-9 {
|
|
conflicts.push(
|
|
Conflict::new(
|
|
ConflictKind::Requirement,
|
|
placement.task,
|
|
placement.start,
|
|
format!(
|
|
"demand {} exceeds the capacity {} of {}",
|
|
requirement.demand,
|
|
resource.capacity(),
|
|
resource.id
|
|
),
|
|
)
|
|
.with_resource(requirement.resource),
|
|
);
|
|
continue;
|
|
}
|
|
if !resource.is_available_over(placement.start, placement.duration) {
|
|
conflicts.push(
|
|
Conflict::new(
|
|
ConflictKind::Availability,
|
|
placement.task,
|
|
placement.start,
|
|
format!(
|
|
"{} is not available from {} for {} s",
|
|
resource.id, placement.start, placement.duration
|
|
),
|
|
)
|
|
.with_resource(requirement.resource),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The capacity check, one resource at a time.
|
|
// 容量检查,逐个资源进行。
|
|
for resource in resources.resources() {
|
|
// Each event is a time, a change in demand, and the task responsible,
|
|
// so that a conflict can name the pair.
|
|
// 每个事件含时刻、需求变化与责任任务,使冲突得以指明这一对。
|
|
let mut events: Vec<(f64, f64, TaskId)> = Vec::new();
|
|
for placement in schedule.placements() {
|
|
let Some(task) = tasks.iter().find(|task| task.id == placement.task) else {
|
|
continue;
|
|
};
|
|
if let Some(demand) = task.demand_for(resource.id) {
|
|
events.push((placement.start, demand, placement.task));
|
|
events.push((placement.end(), -demand, placement.task));
|
|
}
|
|
}
|
|
if events.is_empty() {
|
|
continue;
|
|
}
|
|
// A release sorts before an acquisition at the same instant, so two
|
|
// placements that touch are not counted as overlapping.
|
|
// 同一瞬先排释放后排获取,使两个相接的放置不计为重叠。
|
|
events.sort_by(|first, second| {
|
|
first
|
|
.0
|
|
.partial_cmp(&second.0)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
.then_with(|| {
|
|
first
|
|
.1
|
|
.partial_cmp(&second.1)
|
|
.unwrap_or(core::cmp::Ordering::Equal)
|
|
})
|
|
.then_with(|| first.2.cmp(&second.2))
|
|
});
|
|
|
|
let mut current = 0.0;
|
|
let mut holders: Vec<TaskId> = Vec::new();
|
|
for (time, change, task) in events {
|
|
if change > 0.0 {
|
|
holders.push(task);
|
|
} else {
|
|
holders.retain(|holder| *holder != task);
|
|
}
|
|
current += change;
|
|
if current > resource.capacity() + 1e-9 {
|
|
// Report the pair that first pushes the resource over, so
|
|
// that the conflict names two tasks rather than only the one
|
|
// that arrived last.
|
|
// 报告首次把该资源推过量的一对,使冲突指明两个任务而
|
|
// 不仅是最后到达的那个。
|
|
let mut involved = holders.clone();
|
|
involved.sort();
|
|
let first = involved.first().copied();
|
|
let second = involved.get(1).copied();
|
|
let detail = format!(
|
|
"demand {} exceeds the capacity {} of {}",
|
|
current,
|
|
resource.capacity(),
|
|
resource.id
|
|
);
|
|
let mut conflict =
|
|
Conflict::new(ConflictKind::Capacity, first.unwrap_or(task), time, detail)
|
|
.with_resource(resource.id);
|
|
if let Some(second) = second {
|
|
conflict = conflict.with_other(second);
|
|
}
|
|
conflicts.push(conflict);
|
|
// Report each resource once, at the first instant it is over
|
|
// committed. A second report at a later instant would add
|
|
// nothing a repair pass can use.
|
|
// 每个资源只报告一次,取其首次超订的时刻。较晚时刻
|
|
// 的第二次报告对一个修复轮毫无用处。
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ConflictReport::new(conflicts, tasks.len(), schedule.len())
|
|
}
|
|
|
|
/// Returns the precedence conflicts among a set of constraints.
|
|
///
|
|
/// A precedence conflict occurs when the task that must come second starts
|
|
/// before the one that must come first has finished. A constraint whose
|
|
/// tasks are not both placed is not a conflict, because nothing has been
|
|
/// promised about them.
|
|
///
|
|
/// The constraints are checked as given, so they must be acyclic. A cycle is
|
|
/// a contradiction in the caller's own model and is not detected here.
|
|
///
|
|
/// 返回一组约束中的优先冲突。
|
|
///
|
|
/// 当本应居次的任务在应居首者完成之前开始,即为优先冲突。若某约束的两
|
|
/// 个任务未被同时放置,则不构成冲突,因为对它们未作任何承诺。
|
|
///
|
|
/// 约束按给定形式检查,故必须无环。环是调用方自身模型中的矛盾,此处
|
|
/// 不予检测。
|
|
pub fn detect_precedence(schedule: &Schedule, constraints: &[Precedence]) -> Vec<Conflict> {
|
|
let mut conflicts = Vec::new();
|
|
for constraint in constraints {
|
|
let (Some(before), Some(after)) = (
|
|
schedule.placement_of(constraint.before),
|
|
schedule.placement_of(constraint.after),
|
|
) else {
|
|
continue;
|
|
};
|
|
if after.start < before.end() {
|
|
conflicts.push(
|
|
Conflict::new(
|
|
ConflictKind::Precedence,
|
|
constraint.after,
|
|
after.start,
|
|
format!(
|
|
"{} must follow {} but starts at {} before it ends at {}",
|
|
constraint.after,
|
|
constraint.before,
|
|
after.start,
|
|
before.end()
|
|
),
|
|
)
|
|
.with_other(constraint.before),
|
|
);
|
|
}
|
|
}
|
|
conflicts
|
|
}
|