///|
/// A calendar template for interval tasks with explicit end variables.
/// It provides a compact bridge between domain constraints and resource
/// planning applications.
pub struct CalendarTask {
name : String
start : Int
end : Int
duration : Int
resource : Int
}
///|
/// A mutable interval calendar model.
pub struct CalendarModel {
solver : Solver
tasks : Array[CalendarTask]
horizon : Int
resources : Int
}
///|
/// Create an empty calendar.
pub fn calendar_model(horizon : Int, resources : Int) -> CalendarModel? {
if horizon < 1 || resources < 1 {
return None
}
Some({ solver: new_solver(), tasks: [], horizon, resources })
}
///|
/// Return the planning horizon.
pub fn CalendarModel::horizon(self : CalendarModel) -> Int {
self.horizon
}
///|
/// Return the resource count.
pub fn CalendarModel::resource_count(self : CalendarModel) -> Int {
self.resources
}
///|
/// Return the number of tasks.
pub fn CalendarModel::task_count(self : CalendarModel) -> Int {
self.tasks.length()
}
///|
/// Add an interval task and return its task id.
pub fn CalendarModel::add_task(
self : CalendarModel,
name : String,
duration : Int,
resource : Int,
) -> Int? {
if duration < 1 ||
duration > self.horizon ||
resource < 0 ||
resource >= self.resources {
return None
}
let task_id = self.tasks.length()
let start = self.solver.add_variable(
variable("calendar_{task_id}_start", 0, self.horizon - duration),
)
let end = self.solver.add_variable(
variable("calendar_{task_id}_end", duration, self.horizon),
)
self.solver.add_constraint(linear([(start, 1), (end, -1)], -duration))
self.tasks.push({ name, start, end, duration, resource })
Some(task_id)
}
///|
/// Return a task or abort on an invalid id.
pub fn CalendarModel::task(self : CalendarModel, task_id : Int) -> CalendarTask {
if task_id < 0 || task_id >= self.tasks.length() {
abort("calendar task is outside the model")
}
self.tasks[task_id]
}
///|
/// Return all task metadata.
pub fn CalendarModel::tasks(self : CalendarModel) -> Array[CalendarTask] {
self.tasks.copy()
}
///|
/// Return a task's start variable.
pub fn CalendarTask::start(self : CalendarTask) -> Int {
self.start
}
///|
/// Return a task's end variable.
pub fn CalendarTask::end(self : CalendarTask) -> Int {
self.end
}
///|
/// Return a task's duration.
pub fn CalendarTask::duration(self : CalendarTask) -> Int {
self.duration
}
///|
/// Return a task's resource.
pub fn CalendarTask::resource(self : CalendarTask) -> Int {
self.resource
}
///|
/// Return a task's name.
pub fn CalendarTask::name(self : CalendarTask) -> String {
self.name
}
///|
/// Return whether a task can fit in a horizon.
pub fn CalendarTask::fits(self : CalendarTask, horizon : Int) -> Bool {
self.duration > 0 && self.duration <= horizon
}
///|
/// Render task metadata without a solution.
pub fn CalendarTask::describe(self : CalendarTask) -> String {
"\{self.name}: start=\{self.start}, end=\{self.end}, duration=\{self.duration}, resource=\{self.resource}"
}
///|
/// Fix a task at one start time.
pub fn CalendarModel::fix_start(
self : CalendarModel,
task_id : Int,
start : Int,
) -> Bool {
let item = self.task(task_id)
if start < 0 || start + item.duration > self.horizon {
return false
}
self.solver.assign(item.start, start)
}
///|
/// Restrict a task to a start-time window.
pub fn CalendarModel::post_start_window(
self : CalendarModel,
task_id : Int,
lower : Int,
upper : Int,
) -> Unit {
let item = self.task(task_id)
if lower > upper {
abort("calendar start window is inverted")
}
self.solver.add_constraint(between(item.start, lower, upper))
}
///|
/// Require a task to finish no later than a deadline.
pub fn CalendarModel::post_deadline(
self : CalendarModel,
task_id : Int,
deadline : Int,
) -> Unit {
let item = self.task(task_id)
self.solver.add_constraint(between(item.end, item.duration, deadline))
}
///|
/// Require the second task to begin after the first task ends.
pub fn CalendarModel::post_precedence(
self : CalendarModel,
first : Int,
second : Int,
gap : Int,
) -> Unit {
if gap < 0 {
abort("calendar precedence gap cannot be negative")
}
let left = self.task(first)
let right = self.task(second)
self.solver.add_constraint(
linear_greater_equal([(right.start, 1), (left.end, -1)], gap),
)
}
///|
/// Require tasks on one resource not to overlap.
pub fn CalendarModel::post_resource_non_overlap(
self : CalendarModel,
resource : Int,
) -> Unit {
if resource < 0 || resource >= self.resources {
abort("calendar resource is outside the model")
}
let intervals : Array[(Int, Int)] = []
for item in self.tasks {
if item.resource == resource {
intervals.push((item.start, item.duration))
}
}
self.solver.add_constraint(no_overlap(intervals))
}
///|
/// Require every resource to execute non-overlapping tasks.
pub fn CalendarModel::post_all_resources_non_overlap(
self : CalendarModel,
) -> Unit {
for resource in 0.. Unit {
if resource < 0 || resource >= self.resources || capacity < 1 {
abort("calendar capacity is invalid")
}
let intervals : Array[(Int, Int, Int)] = []
for item in self.tasks {
if item.resource == resource {
intervals.push((item.start, item.duration, 1))
}
}
self.solver.add_constraint(cumulative(intervals, capacity))
}
///|
/// Restrict all task starts to one of a finite set of slots.
pub fn CalendarModel::post_allowed_starts(
self : CalendarModel,
task_id : Int,
starts : Array[Int],
) -> Unit {
let item = self.task(task_id)
self.solver.add_constraint(allowed_values(item.start, starts))
}
///|
/// Configure search.
pub fn CalendarModel::configure(
self : CalendarModel,
config : SearchConfig,
) -> Unit {
self.solver.configure(config)
}
///|
/// Solve the calendar.
pub fn CalendarModel::solve(self : CalendarModel) -> Solution? {
self.solver.solve()
}
///|
/// Enumerate calendar solutions.
pub fn CalendarModel::solve_all(
self : CalendarModel,
limit : Int,
) -> Array[Solution] {
self.solver.limit(limit)
self.solver.solve_all()
}
///|
/// Return the latest statistics.
pub fn CalendarModel::stats(self : CalendarModel) -> SearchStats {
self.solver.stats()
}
///|
/// Validate a complete calendar solution.
pub fn CalendarModel::is_valid(
self : CalendarModel,
solution : Solution,
) -> Bool {
self.solver.is_valid_solution(solution)
}
///|
/// Read a task's start in a solution.
pub fn CalendarModel::start_of(
self : CalendarModel,
solution : Solution,
task_id : Int,
) -> Int {
solution.get(self.task(task_id).start)
}
///|
/// Read a task's end in a solution.
pub fn CalendarModel::end_of(
self : CalendarModel,
solution : Solution,
task_id : Int,
) -> Int {
solution.get(self.task(task_id).end)
}
///|
/// Return the latest end time in a solution.
pub fn CalendarModel::makespan(
self : CalendarModel,
solution : Solution,
) -> Int {
let mut result = 0
for task_id in 0.. result {
result = end
}
}
result
}
///|
/// Return task intervals for a solution.
pub fn CalendarModel::intervals(
self : CalendarModel,
solution : Solution,
) -> Array[(String, Int, Int, Int)] {
self.tasks.map(task => {
(task.name, solution.get(task.start), solution.get(task.end), task.resource)
})
}
///|
/// Return task names in insertion order.
pub fn CalendarModel::task_names(self : CalendarModel) -> Array[String] {
self.tasks.map(task => task.name)
}
///|
/// Return ids assigned to one resource.
pub fn CalendarModel::task_ids_for_resource(
self : CalendarModel,
resource : Int,
) -> Array[Int] {
let result : Array[Int] = []
for id, task in self.tasks {
if task.resource == resource {
result.push(id)
}
}
result
}
///|
/// Sum task durations assigned to one resource.
pub fn CalendarModel::resource_load(
self : CalendarModel,
resource : Int,
) -> Int {
let mut total = 0
for task in self.tasks {
if task.resource == resource {
total += task.duration
}
}
total
}
///|
/// Return total duration across all tasks.
pub fn CalendarModel::total_duration(self : CalendarModel) -> Int {
self.tasks.fold(init=0, (total, task) => total + task.duration)
}
///|
/// Return the number of tasks assigned to a resource.
pub fn CalendarModel::resource_task_count(
self : CalendarModel,
resource : Int,
) -> Int {
self.task_ids_for_resource(resource).length()
}
///|
/// Return the earliest start time in a solution.
pub fn CalendarModel::earliest_start(
self : CalendarModel,
solution : Solution,
) -> Int {
let mut result = self.horizon
for task in self.tasks {
let start = solution.get(task.start)
if start < result {
result = start
}
}
result
}
///|
/// Return the latest end time in a solution.
pub fn CalendarModel::latest_end(
self : CalendarModel,
solution : Solution,
) -> Int {
let mut result = 0
for task in self.tasks {
let end = solution.get(task.end)
if end > result {
result = end
}
}
result
}
///|
/// Return idle horizon time after accounting for total task duration.
pub fn CalendarModel::idle_time(
self : CalendarModel,
solution : Solution,
) -> Int {
self.latest_end(solution) -
self.earliest_start(solution) -
self.tasks.fold(init=0, (total, task) => total + task.duration)
}
///|
/// Return the current start domains for every task.
pub fn CalendarModel::start_domains(self : CalendarModel) -> Array[Domain] {
self.tasks.map(task => self.solver.domain_of(task.start))
}
///|
/// Return the current end domains for every task.
pub fn CalendarModel::end_domains(self : CalendarModel) -> Array[Domain] {
self.tasks.map(task => self.solver.domain_of(task.end))
}
///|
/// Check that every task metadata record fits the configured horizon.
pub fn CalendarModel::all_tasks_fit(self : CalendarModel) -> Bool {
for task in self.tasks {
if !task.fits(self.horizon) {
return false
}
}
true
}
///|
/// Return a stable schedule fingerprint.
pub fn CalendarModel::fingerprint(
self : CalendarModel,
solution : Solution,
) -> String {
let builder = StringBuilder()
for id, task in self.tasks {
if id > 0 {
builder.write_char(';')
}
builder.write_string(
"\{task.name}:\{solution.get(task.start)}-\{solution.get(task.end)}@\{task.resource}",
)
}
builder.to_string()
}
///|
/// Render tasks by start time and resource id.
pub fn CalendarModel::render(
self : CalendarModel,
solution : Solution,
) -> String {
let builder = StringBuilder()
for index, item in self.intervals(solution) {
if index > 0 {
builder.write_char('\n')
}
let (name, start, end, resource) = item
builder.write_string("\{name}: resource=\{resource}, \{start}..\{end}")
}
builder.to_string()
}
///|
/// A summary of calendar utilization.
pub struct CalendarSummary {
tasks : Int
makespan : Int
occupied : Int
resource_load : Array[Int]
}
///|
/// Compute utilization counters from a solution.
pub fn CalendarModel::summary(
self : CalendarModel,
solution : Solution,
) -> CalendarSummary {
let loads : Array[Int] = []
for _ in 0.. Int {
self.tasks
}
///|
/// Return the schedule makespan.
pub fn CalendarSummary::makespan(self : CalendarSummary) -> Int {
self.makespan
}
///|
/// Return total occupied time.
pub fn CalendarSummary::occupied(self : CalendarSummary) -> Int {
self.occupied
}
///|
/// Return per-resource occupied time.
pub fn CalendarSummary::resource_load(self : CalendarSummary) -> Array[Int] {
self.resource_load.copy()
}
///|
/// Render utilization counters.
pub fn CalendarSummary::describe(self : CalendarSummary) -> String {
"tasks=\{self.tasks}, makespan=\{self.makespan}, occupied=\{self.occupied}, loads=\{Repr(self.resource_load)}"
}
///|
/// Build a simple precedence chain.
pub fn calendar_chain(durations : Array[Int], horizon : Int) -> CalendarModel? {
if durations.length() == 0 {
return None
}
match calendar_model(horizon, 1) {
Some(model) => {
let ids : Array[Int] = []
for index, duration in durations {
match model.add_task("task_\{index}", duration, 0) {
Some(id) => ids.push(id)
None => return None
}
}
for index in 0..<(ids.length() - 1) {
model.post_precedence(ids[index], ids[index + 1], 0)
}
model.post_resource_non_overlap(0)
Some(model)
}
None => None
}
}