///|
/// A finite-horizon interval scheduling model.
pub struct IntervalTask {
name : String
start : Int
duration : Int
demand : Int
latest_start : Int
}
///|
/// A resource-constrained schedule with optional capacity checks.
pub struct ResourceSchedule {
solver : Solver
tasks : Array[IntervalTask]
horizon : Int
capacity : Int
}
///|
/// Create an empty resource schedule.
pub fn resource_schedule(horizon : Int, capacity : Int) -> ResourceSchedule? {
if horizon < 1 || capacity < 1 {
return None
}
Some({ solver: new_solver(), tasks: [], horizon, capacity })
}
///|
/// Add an interval task and return its index.
pub fn ResourceSchedule::add_task(
self : ResourceSchedule,
name : String,
duration : Int,
demand : Int,
latest_start : Int,
) -> Int? {
if duration < 1 ||
demand < 1 ||
demand > self.capacity ||
latest_start < 0 ||
latest_start + duration > self.horizon {
return None
}
let start = self.solver.add_variable(
variable("start_\{name}_\{self.tasks.length()}", 0, latest_start),
)
let task = { name, start, duration, demand, latest_start }
self.tasks.push(task)
self.refresh_constraints()
Some(self.tasks.length() - 1)
}
///|
/// Add an interval with a full-horizon start window.
pub fn ResourceSchedule::add_flexible_task(
self : ResourceSchedule,
name : String,
duration : Int,
demand : Int,
) -> Int? {
self.add_task(name, duration, demand, self.horizon - duration)
}
///|
/// Set an exact task start.
pub fn ResourceSchedule::fix_start(
self : ResourceSchedule,
task : Int,
start : Int,
) -> Bool {
match self.tasks.get(task) {
Some(value) => self.solver.assign(value.start, start)
None => false
}
}
///|
/// Return the start variable for a task.
pub fn ResourceSchedule::start_variable(
self : ResourceSchedule,
task : Int,
) -> Int {
match self.tasks.get(task) {
Some(value) => value.start
None => abort("interval task index is outside the schedule")
}
}
///|
/// Return task duration.
pub fn ResourceSchedule::duration(self : ResourceSchedule, task : Int) -> Int {
match self.tasks.get(task) {
Some(value) => value.duration
None => abort("interval task index is outside the schedule")
}
}
///|
/// Return task name.
pub fn ResourceSchedule::task_name(
self : ResourceSchedule,
task : Int,
) -> String {
match self.tasks.get(task) {
Some(value) => value.name
None => abort("interval task index is outside the schedule")
}
}
///|
/// Add a precedence relation by tightening the successor start domain.
pub fn ResourceSchedule::precede(
self : ResourceSchedule,
first : Int,
second : Int,
) -> Bool {
let left = match self.tasks.get(first) {
Some(value) => value
None => return false
}
let right = match self.tasks.get(second) {
Some(value) => value
None => return false
}
if first == second {
return false
}
self.solver.add_constraint(greater_equal(right.start, left.start))
self.solver.add_constraint(
no_overlap([(left.start, left.duration), (right.start, right.duration)]),
)
true
}
///|
/// Re-post all global resource constraints after a task is added.
fn ResourceSchedule::refresh_constraints(self : ResourceSchedule) -> Unit {
let intervals : Array[(Int, Int)] = []
let demands : Array[(Int, Int, Int)] = []
for task in self.tasks {
intervals.push((task.start, task.duration))
demands.push((task.start, task.duration, task.demand))
}
if intervals.length() > 1 {
self.solver.add_constraint(no_overlap(intervals))
}
if demands.length() > 0 {
self.solver.add_constraint(cumulative(demands, self.capacity))
}
}
///|
/// Solve the resource schedule once.
pub fn ResourceSchedule::solve(self : ResourceSchedule) -> Solution? {
self.solver.solve()
}
///|
/// Enumerate up to `limit` schedules.
pub fn ResourceSchedule::solve_all(
self : ResourceSchedule,
limit : Int,
) -> Array[Solution] {
self.solver.limit(limit)
self.solver.solve_all()
}
///|
/// Return statistics for the most recent solve.
pub fn ResourceSchedule::stats(self : ResourceSchedule) -> SearchStats {
self.solver.stats()
}
///|
/// Return the number of tasks.
pub fn ResourceSchedule::task_count(self : ResourceSchedule) -> Int {
self.tasks.length()
}
///|
/// Return all task starts from a solution.
pub fn ResourceSchedule::starts(
self : ResourceSchedule,
solution : Solution,
) -> Array[Int] {
self.tasks.map(task => solution.get(task.start))
}
///|
/// Return task end times from a solution.
pub fn ResourceSchedule::ends(
self : ResourceSchedule,
solution : Solution,
) -> Array[Int] {
self.tasks.map(task => solution.get(task.start) + task.duration)
}
///|
/// Check every task and capacity slot in a complete schedule.
pub fn ResourceSchedule::is_valid(
self : ResourceSchedule,
solution : Solution,
) -> Bool {
if !self.solver.is_valid_solution(solution) {
return false
}
for time in 0..= start && time < start + task.duration {
load += task.demand
}
}
if load > self.capacity {
return false
}
}
true
}
///|
/// Render a Gantt-like one-line-per-task schedule.
pub fn ResourceSchedule::render(
self : ResourceSchedule,
solution : Solution,
) -> String {
let builder = StringBuilder()
for index, task in self.tasks {
if index > 0 {
builder.write_char('\n')
}
let start = solution.get(task.start)
builder.write_string(task.name)
builder.write_string(
" [\{start}, \{start + task.duration}) demand=\{task.demand}",
)
}
builder.to_string()
}
///|
/// Return a resource load profile.
pub fn ResourceSchedule::load_profile(
self : ResourceSchedule,
solution : Solution,
) -> Array[Int] {
let loads = Array::make(self.horizon, 0)
for task in self.tasks {
let start = solution.get(task.start)
for time in start..<(start + task.duration) {
if time >= 0 && time < self.horizon {
loads[time] += task.demand
}
}
}
loads
}
///|
/// Return a stable task list summary.
pub fn ResourceSchedule::summary(self : ResourceSchedule) -> String {
let builder = StringBuilder()
builder.write_string(
"horizon=\{self.horizon}, capacity=\{self.capacity}, tasks=\{self.tasks.length()}",
)
for index, task in self.tasks {
builder.write_string(
"\n [\{index}] \{task.name}: duration=\{task.duration}, demand=\{task.demand}, latest=\{task.latest_start}",
)
}
builder.to_string()
}
///|
/// Add an exclusive maintenance window to every task by fixing an allowed
/// start range that lies entirely before or after the window.
pub fn ResourceSchedule::avoid_window(
self : ResourceSchedule,
window_start : Int,
window_end : Int,
) -> Bool {
if window_start < 0 || window_end > self.horizon || window_start >= window_end {
return false
}
for task in self.tasks {
let latest_before = window_start - task.duration
let earliest_after = window_end
let allowed : Array[Int] = []
for start in 0..<=task.latest_start {
if start <= latest_before || start >= earliest_after {
allowed.push(start)
}
}
self.solver.add_constraint(allowed_values(task.start, allowed))
}
true
}
///|
/// Return a two-machine flow-shop example.
pub fn flow_shop(
jobs : Array[(String, Int, Int)],
horizon : Int,
) -> ResourceSchedule? {
let schedule = match resource_schedule(horizon, 1) {
Some(value) => value
None => return None
}
for job in jobs {
let (name, first_duration, second_duration) = job
let first = match
schedule.add_flexible_task("\{name}-m1", first_duration, 1) {
Some(value) => value
None => return None
}
let second = match
schedule.add_flexible_task("\{name}-m2", second_duration, 1) {
Some(value) => value
None => return None
}
ignore(schedule.precede(first, second))
}
Some(schedule)
}