apsis-rs/crates/ops/apsis-scheduler/src/planner.rs

581 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The greedy planner.
//!
//! 贪心规划器。
use crate::greedy::infeasible_tasks;
use crate::improve::{ImprovementSettings, improve};
use crate::sweep::earliest_feasible_start;
use apsis_resource::ResourceSet;
use apsis_schedule::Schedule;
use apsis_task::{PrecedenceGraph, Task, TaskId, TaskState, task_order};
/// What happened to one task during planning.
///
/// The outcome pairs a task with the state the planner assigned, which is the
/// information an operator needs to see. It is separate from the schedule
/// because a task that was not placed has no placement but still has an
/// outcome worth reporting.
///
/// 规划过程中某个任务的结果。
///
/// 它把任务与规划器所指定的状态配对,这是操作者需要看到的信息。它与
/// 计划分开,因为未放置的任务虽无放置,却仍有一份值得报告的结果。
#[derive(Debug, Clone, PartialEq)]
pub struct PlanOutcome {
/// The task.
///
/// 该任务。
pub task: TaskId,
/// The state the planner assigned.
///
/// 规划器所指定的状态。
pub state: TaskState,
}
impl PlanOutcome {
/// Returns `true` when the task was placed.
///
/// 当该任务已被放置时返回 `true`。
pub fn is_scheduled(&self) -> bool {
self.state.is_scheduled()
}
/// Returns `true` when the task could not be placed for a structural
/// reason, rather than being left out for want of time.
///
/// 当该任务因结构性原因无法放置、而非因时间不足被排除时返回 `true`。
pub fn is_infeasible(&self) -> bool {
matches!(self.state, TaskState::Infeasible { .. })
}
}
/// The result of planning.
///
/// 规划的结果。
#[derive(Debug, Clone, PartialEq)]
pub struct SchedulingResult {
/// The schedule that was produced.
///
/// 所产出的计划。
pub schedule: Schedule,
/// The outcome of every task, in the order the planner considered them.
///
/// The order is the order of the task list the planner was given, not the
/// order in which the planner placed them, so that a caller can index the
/// outcomes by the position of the task in its own list.
///
/// 每个任务的结果,按规划器考虑它们的顺序排列。
///
/// 该顺序是规划器所获任务列表的顺序,而非其放置的顺序,故调用方
/// 可按任务在自身列表中的位置索引结果。
pub outcomes: Vec<PlanOutcome>,
/// The identifiers of the tasks that the improvement pass placed.
///
/// These tasks were left out by the greedy pass and placed afterwards by
/// moving other placements. They are also recorded as scheduled in the
/// outcomes.
///
/// 改进轮所放置的任务标识符。
///
/// 这些任务被贪心轮排除,其后通过移动其他放置而被安排。它们在
/// 结果中同样被记为已调度。
pub improved: Vec<TaskId>,
}
impl SchedulingResult {
/// Returns the outcome of a task.
///
/// 返回某任务的结果。
pub fn outcome_of(&self, task: TaskId) -> Option<&PlanOutcome> {
self.outcomes.iter().find(|outcome| outcome.task == task)
}
/// Returns the identifiers of the tasks that were placed.
///
/// 返回已放置任务的标识符。
pub fn scheduled(&self) -> Vec<TaskId> {
self.outcomes
.iter()
.filter(|outcome| outcome.is_scheduled())
.map(|outcome| outcome.task)
.collect()
}
/// Returns the identifiers of the tasks that were left out.
///
/// 返回被排除任务的标识符。
pub fn deferred(&self) -> Vec<TaskId> {
self.outcomes
.iter()
.filter(|outcome| outcome.state == TaskState::Deferred)
.map(|outcome| outcome.task)
.collect()
}
/// Returns the identifiers of the tasks that cannot be placed at all.
///
/// 返回完全无法放置的任务的标识符。
pub fn infeasible(&self) -> Vec<TaskId> {
self.outcomes
.iter()
.filter(|outcome| outcome.is_infeasible())
.map(|outcome| outcome.task)
.collect()
}
/// Returns `true` when every offered task was placed.
///
/// 当所有提交任务均被放置时返回 `true`。
///
/// 当所有提交任务均被放置时返回 `true`。
pub fn is_complete(&self) -> bool {
self.outcomes.iter().all(PlanOutcome::is_scheduled)
}
/// Returns `true` when every mandatory task was placed.
///
/// A plan that leaves a mandatory task unplaced is not usable, however
/// much optional work it found room for.
///
/// 当所有强制任务均被放置时返回 `true`。
///
/// 留下强制任务未放置的计划不可用,无论它为多少可选工作腾出了
/// 空间。
pub fn honours_mandatory(&self, tasks: &[Task]) -> bool {
tasks
.iter()
.filter(|task| task.mandatory)
.all(|task| self.schedule.is_placed(task.id))
}
/// Returns `true` when every precedence constraint is satisfied.
///
/// A constraint is satisfied when the second task starts at or after the
/// end of the first. A constraint whose tasks are not both placed is
/// trivially satisfied, because nothing has been promised about them.
///
/// 当所有优先约束都得到满足时返回 `true`。
///
/// 约束被满足,指第二个任务的起点不早于第一个的终点。两个任务未
/// 同时放置的约束平凡地满足,因为对它们未作任何承诺。
pub fn honours_precedence(&self, graph: &PrecedenceGraph) -> bool {
self.outcomes
.iter()
.filter(|outcome| outcome.is_scheduled())
.all(|outcome| {
let Some(after) = self.schedule.placement_of(outcome.task) else {
return true;
};
graph.predecessors_of(outcome.task).iter().all(|before| {
self.schedule
.placement_of(*before)
.is_none_or(|placement| placement.end() <= after.start)
})
})
}
}
/// A greedy planner.
///
/// The planner holds the settings of the search. The settings are separated
/// from the schedule so that the same planner can be applied to many task
/// sets with the same policy.
///
/// 贪心规划器。
///
/// 该规划器持有搜索的设置。设置与计划分开,故同一个规划器可按同一策略
/// 应用于许多任务集。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Planner {
/// Whether to place mandatory tasks before the others regardless of
/// their value.
///
/// A mandatory task must run, so it is worth placing first even when its
/// weighted value is low: the optional tasks can use whatever time is
/// left, while the reverse order could consume the only window the
/// mandatory task had.
///
/// 是否不论价值高低都先放置强制任务。
///
/// 强制任务必须执行,故即便其加权价值很低也值得先放置:可选任务
/// 可以利用剩下的时间,而相反的顺序可能耗尽强制任务仅有的那个窗口。
pub mandatory_first: bool,
/// The settings of the improvement pass.
///
/// 改进轮的设置。
pub improvement: ImprovementSettings,
}
impl Default for Planner {
/// Returns a planner that places mandatory tasks first and runs the
/// improvement pass.
///
/// 返回一个先放置强制任务并运行改进轮的规划器。
fn default() -> Self {
Self {
mandatory_first: true,
improvement: ImprovementSettings::default(),
}
}
}
impl Planner {
/// Creates a planner with the default settings.
///
/// The defaults are the ones [`Planner::default`] uses: mandatory tasks
/// first and the improvement pass enabled with four attempts per task.
///
/// 以默认设置创建规划器。
///
/// 默认值即 [`Planner::default`] 所用者:先放置强制任务,改进轮
/// 启用且每任务尝试四次。
pub const fn new() -> Self {
Self {
mandatory_first: true,
improvement: ImprovementSettings::new(4),
}
}
/// Returns a copy of this planner with the mandatory placement rule set.
///
/// 返回设置了强制放置规则的规划器副本。
pub const fn with_mandatory_first(mut self, mandatory_first: bool) -> Self {
self.mandatory_first = mandatory_first;
self
}
/// Returns a copy of this planner with the improvement pass configured.
///
/// 返回配置了改进轮的规划器副本。
pub const fn with_improvement(mut self, improvement: ImprovementSettings) -> Self {
self.improvement = improvement;
self
}
/// Returns the order in which the planner considers the tasks.
///
/// Without a precedence graph the order is mandatory tasks first, when
/// the planner is configured that way, and then decreasing weighted
/// value with ties broken by deadline and identifier.
///
/// With a precedence graph the order is a topological order of the
/// graph, using the value order to break ties. That places a task as
/// early as its constraints allow and the most valuable one first among
/// the tasks that are free.
///
/// 返回规划器考虑任务的顺序。
///
/// 无优先关系图时,顺序是先强制任务(若规划器如此配置),再按加权
/// 价值递减,平局按截止时刻与标识符打破。
///
/// 有优先关系图时,顺序是该图的拓扑序,并以价值次序打破平局。这
/// 使任务在其约束允许的最早时刻被放置,而在自由的任务中优先放置
/// 价值最高者。
pub fn order(&self, tasks: &[Task]) -> Vec<TaskId> {
self.order_with_precedence(tasks, None)
}
/// Returns the order in which the planner considers the tasks, honouring
/// a precedence graph.
///
/// A graph whose constraints contain a cycle produces the value order
/// unchanged, because the constraints then have no satisfiable
/// interpretation. The caller should validate the graph before
/// planning; the order is a fallback so that planning still produces
/// something rather than failing outright.
///
/// 返回规划器考虑任务的顺序,并遵从给定的优先关系图。
///
/// 约束含环的图会原样产生价值次序,因为这些约束此时没有可满足的
/// 解释。调用方应在规划前校验该图;此处的次序是一个回退,使规划
/// 仍能产出结果而非直接失败。
pub fn order_with_precedence(
&self,
tasks: &[Task],
precedence: Option<&PrecedenceGraph>,
) -> Vec<TaskId> {
let ordered = task_order(tasks);
let valued = if !self.mandatory_first {
ordered
} else {
let mandatory: Vec<TaskId> = tasks
.iter()
.filter(|task| task.mandatory)
.map(|task| task.id)
.collect();
let mut result = Vec::with_capacity(ordered.len());
result.extend(ordered.iter().filter(|id| mandatory.contains(id)).copied());
result.extend(ordered.iter().filter(|id| !mandatory.contains(id)).copied());
result
};
let Some(graph) = precedence else {
return valued;
};
if graph.is_empty() {
return valued;
}
graph.topological_order(&valued).unwrap_or(valued)
}
/// Plans a set of tasks on a set of resources.
///
/// Every task is considered once, in the order the planner chooses. A
/// task is placed at the earliest time at which all of its resources are
/// free together. A task that cannot be placed is reported as deferred,
/// or as infeasible when no placement could ever exist.
///
/// The schedule that is returned is feasible by construction, because
/// each placement is checked against the capacity before it is made. The
/// caller may confirm that with `apsis-conflict`.
///
/// 在一组资源上规划一组任务。
///
/// 每个任务按规划器所选的顺序被考虑一次,被放置在其全部资源恰好同时
/// 空闲的最早时刻。无法放置的任务被报告为被推迟;不存在任何可行
/// 放置的则报告为不可行。
///
/// 返回的计划按构造即可行,因为每处放置在做下之前都对照容量校验
/// 过。调用方可用 `apsis-conflict` 复核。
pub fn schedule(&self, tasks: &[Task], resources: &ResourceSet) -> SchedulingResult {
self.plan(tasks, resources, None)
}
/// Plans a set of tasks, honouring a precedence graph.
///
/// A task is placed only after every task that must precede it has been
/// placed, so the search for its start begins no earlier than the latest
/// end among its predecessors. A predecessor that could not be placed
/// makes the successor deferred as well, because the constraint cannot be
/// met.
///
/// The improvement pass is not applied to a plan with precedence
/// constraints, because relocating a placement could break a constraint
/// that the pass does not consider. A caller that needs both should
/// improve the plan and then verify it with
/// [`SchedulingResult::honours_precedence`].
///
/// 遵从优先关系图规划一组任务。
///
/// 任务仅在其所有前驱均已放置之后才被放置,故其起点搜索不早于
/// 前驱中最晚的结束时刻。无法放置的前驱会使其后继同样被推迟,
/// 因为该约束无法满足。
///
/// 改进轮不作用于带优先约束的计划,因为移动某处放置可能破坏该轮
/// 并不考虑的约束。两者皆需的调用方应先改进计划,再用
/// [`SchedulingResult::honours_precedence`] 复核。
pub fn schedule_with_precedence(
&self,
tasks: &[Task],
resources: &ResourceSet,
precedence: &PrecedenceGraph,
) -> SchedulingResult {
self.plan(tasks, resources, Some(precedence))
}
/// The shared planning routine.
///
/// 共享的规划例程。
fn plan(
&self,
tasks: &[Task],
resources: &ResourceSet,
precedence: Option<&PrecedenceGraph>,
) -> SchedulingResult {
let mut schedule = Schedule::new();
// The state of every task, indexed by identifier, so that a
// predecessor's placement can be looked up by index.
// 各任务的状态,按标识符索引,使前驱的放置可按下标查得。
let mut states = vec![TaskState::Pending; tasks.len()];
let mut outcomes = Vec::with_capacity(tasks.len());
let mut order: Vec<TaskId> = self.order_with_precedence(tasks, precedence);
// A deferred task is one whose search failed, as opposed to one whose
// structural preconditions failed.
// 被推迟的任务指搜索失败者,区别于结构性前置条件失败者。
let mut deferred: Vec<TaskId> = Vec::new();
let infeasible = infeasible_tasks(tasks, resources);
for id in order.drain(..) {
let task = tasks
.iter()
.find(|task| task.id == id)
.expect("the order was built from the task list");
if infeasible.contains(&id) {
let reason = if !task.is_valid() {
"the task is not usable".to_string()
} else {
format!("the resources cannot hold the demand of {id}")
};
set_state(
&mut states,
id,
TaskState::Infeasible {
reason: reason.clone(),
},
);
outcomes.push(PlanOutcome {
task: id,
state: TaskState::Infeasible { reason },
});
continue;
}
// A task may not start before the latest end among the tasks that
// must precede it.
// 任务的起点不得早于其前驱中最晚的结束时刻。
let earliest = match self.precedence_floor(id, tasks, precedence, &schedule, &states) {
Ok(floor) => floor,
Err(reason) => {
set_state(&mut states, id, TaskState::Deferred);
outcomes.push(PlanOutcome {
task: id,
state: TaskState::Deferred,
});
let _ = reason;
deferred.push(id);
continue;
}
};
// The task with the floor applied, so that the search respects
// the constraints.
// 施加该下界后的任务,使搜索遵从各约束。
let constrained = match earliest {
Some(floor) if floor > task.earliest_start => {
let mut copy = task.clone();
copy.earliest_start = floor;
copy
}
_ => task.clone(),
};
match earliest_feasible_start(&constrained, tasks, &schedule, resources) {
Some(start) => match schedule.place(task, start) {
Ok(_) => {
set_state(&mut states, id, TaskState::Scheduled { start });
outcomes.push(PlanOutcome {
task: id,
state: TaskState::Scheduled { start },
});
}
Err(error) => {
let reason = error.to_string();
set_state(
&mut states,
id,
TaskState::Infeasible {
reason: reason.clone(),
},
);
outcomes.push(PlanOutcome {
task: id,
state: TaskState::Infeasible { reason },
});
}
},
None => {
set_state(&mut states, id, TaskState::Deferred);
outcomes.push(PlanOutcome {
task: id,
state: TaskState::Deferred,
});
deferred.push(id);
}
}
}
// The improvement pass runs only for a plan without precedence
// constraints, for the reason given on `schedule_with_precedence`.
// 改进轮只作用于无优先约束的计划,原因见
// `schedule_with_precedence`。
let improved = if precedence.is_some() {
Vec::new()
} else {
let outcome = improve(
tasks,
resources,
&deferred,
&mut schedule,
&mut states,
self.improvement,
);
// The outcomes that the pass changed must be updated, and the
// order of the outcomes follows the task list so that a caller
// can index them by position.
// 该轮改动的结果必须更新,且结果次序遵循任务列表,使调用
// 方可按位置索引。
for id in &outcome.placed {
if let Some(slot) = outcomes.iter_mut().find(|entry| entry.task == *id) {
let start = schedule
.placement_of(*id)
.map_or(0.0, |placement| placement.start);
slot.state = TaskState::Scheduled { start };
}
}
outcome.placed
};
// Reorder the outcomes to follow the task list, which is the order a
// caller expects when it indexes by its own position.
// 将结果重排为遵循任务列表,即调用方按自身位置索引时所期望的
// 次序。
outcomes.sort_by_key(|outcome| outcome.task.index());
SchedulingResult {
schedule,
outcomes,
improved,
}
}
/// Returns the earliest start a task may have, given its predecessors.
///
/// Returns `Ok(None)` when there is no constraint, `Ok(Some(t))` when the
/// task may start no earlier than `t`, and `Err` when a predecessor was
/// not placed, which makes the constraint unsatisfiable.
///
/// 给定其前驱,返回某任务可有的最早起点。
///
/// 无约束时返回 `Ok(None)`;任务起点不早于 `t` 时返回 `Ok(Some(t))`;
/// 某前驱未被放置致使约束不可满足时返回 `Err`。
fn precedence_floor(
&self,
id: TaskId,
_tasks: &[Task],
precedence: Option<&PrecedenceGraph>,
schedule: &Schedule,
states: &[TaskState],
) -> Result<Option<f64>, String> {
let Some(graph) = precedence else {
return Ok(None);
};
let mut floor: Option<f64> = None;
for predecessor in graph.predecessors_of(id) {
// A predecessor that was deferred or is infeasible cannot be
// waited for.
// 被推迟或不可行的前驱无从等待。
let placed = schedule.placement_of(predecessor);
let Some(placement) = placed else {
let reason = match states.get(predecessor.index()) {
Some(TaskState::Infeasible { reason }) => reason.clone(),
_ => format!("{predecessor} was not placed"),
};
return Err(reason);
};
floor =
Some(floor.map_or(placement.end(), |current: f64| current.max(placement.end())));
}
Ok(floor)
}
}
/// Records the state of a task, ignoring an identifier outside the list.
///
/// 记录某任务的状态;标识符不在列表之内时忽略。
fn set_state(states: &mut [TaskState], id: TaskId, state: TaskState) {
if let Some(slot) = states.get_mut(id.index()) {
*slot = state;
}
}