///|
/// Campaign scheduling, resource loading, and maintenance windows.
pub enum ScheduleKind {
Production
Cleaning
Maintenance
Inspection
Changeover
}
///|
pub fn production_schedule() -> ScheduleKind {
Production
}
///|
pub fn cleaning_schedule() -> ScheduleKind {
Cleaning
}
///|
pub fn maintenance_schedule() -> ScheduleKind {
Maintenance
}
///|
pub fn inspection_schedule() -> ScheduleKind {
Inspection
}
///|
pub fn changeover_schedule() -> ScheduleKind {
Changeover
}
///|
pub struct ScheduleBlock {
id : String
asset : String
kind : ScheduleKind
start_hour : Float
duration_hours : Float
priority : Int
operator : String
}
///|
pub fn schedule_block(
id : String,
asset : String,
kind : ScheduleKind,
start_hour : Float,
duration_hours : Float,
priority : Int,
operator : String,
) -> ScheduleBlock {
{ id, asset, kind, start_hour, duration_hours, priority, operator }
}
///|
pub fn ScheduleBlock::end_hour(self : ScheduleBlock) -> Float {
self.start_hour + self.duration_hours
}
///|
pub fn ScheduleBlock::valid(self : ScheduleBlock) -> Bool {
self.start_hour >= 0.0 && self.duration_hours > 0.0 && self.priority >= 0
}
///|
pub fn ScheduleBlock::overlaps(
self : ScheduleBlock,
other : ScheduleBlock,
) -> Bool {
self.asset == other.asset &&
self.start_hour < other.end_hour() &&
other.start_hour < self.end_hour()
}
///|
pub struct ResourceCalendar {
name : String
capacity_hours : Float
blocks : Array[ScheduleBlock]
}
///|
pub fn resource_calendar(
name : String,
capacity_hours : Float,
blocks : Array[ScheduleBlock],
) -> ResourceCalendar {
{ name, capacity_hours, blocks }
}
///|
pub fn ResourceCalendar::loaded_hours(self : ResourceCalendar) -> Float {
self.blocks.fold(init=0.0, fn(total, block) { total + block.duration_hours })
}
///|
pub fn ResourceCalendar::utilization(self : ResourceCalendar) -> Float {
if self.capacity_hours == 0.0 {
0.0
} else {
self.loaded_hours() / self.capacity_hours
}
}
///|
pub fn ResourceCalendar::available(self : ResourceCalendar) -> Float {
self.capacity_hours - self.loaded_hours()
}
///|
pub fn ResourceCalendar::valid(self : ResourceCalendar) -> Bool {
self.capacity_hours >= 0.0 && self.blocks.all(fn(b) { b.valid() })
}
///|
pub fn ResourceCalendar::conflicts(self : ResourceCalendar) -> Array[String] {
let result : Array[String] = []
for i = 0; i < self.blocks.length(); i = i + 1 {
for j = i + 1; j < self.blocks.length(); j = j + 1 {
if self.blocks[i].overlaps(self.blocks[j]) {
result.push(self.blocks[i].id + ":" + self.blocks[j].id)
}
}
}
result
}
///|
pub struct Shift {
name : String
start_hour : Float
end_hour : Float
crew : Int
}
///|
pub fn shift(
name : String,
start_hour : Float,
end_hour : Float,
crew : Int,
) -> Shift {
{ name, start_hour, end_hour, crew }
}
///|
pub fn Shift::hours(self : Shift) -> Float {
if self.end_hour < self.start_hour {
self.end_hour + 24.0 - self.start_hour
} else {
self.end_hour - self.start_hour
}
}
///|
pub fn Shift::valid(self : Shift) -> Bool {
self.crew > 0 && self.hours() > 0.0
}
///|
pub fn total_crew_hours(shifts : Array[Shift]) -> Float {
shifts.fold(init=0.0, fn(v, s) { v + s.hours() * Float::from_int(s.crew) })
}
///|
pub struct MaintenanceWindow {
asset : String
due_hour : Float
duration_hours : Float
critical : Bool
completed : Bool
}
///|
pub fn maintenance_window(
asset : String,
due_hour : Float,
duration_hours : Float,
critical : Bool,
completed : Bool,
) -> MaintenanceWindow {
{ asset, due_hour, duration_hours, critical, completed }
}
///|
pub fn MaintenanceWindow::overdue(
self : MaintenanceWindow,
current_hour : Float,
) -> Bool {
!self.completed && current_hour > self.due_hour
}
///|
pub fn MaintenanceWindow::risk(
self : MaintenanceWindow,
current_hour : Float,
) -> String {
if self.overdue(current_hour) && self.critical {
"critical"
} else if self.overdue(current_hour) {
"overdue"
} else if self.completed {
"complete"
} else {
"planned"
}
}
///|
pub fn maintenance_windows_table(
items : Array[MaintenanceWindow],
current_hour : Float,
) -> ReportTable {
let rows : Array[Array[String]] = []
for item in items {
rows.push([
item.asset,
"\{item.due_hour}",
"\{item.duration_hours}",
"\{item.critical}",
item.risk(current_hour),
])
}
table(["asset", "due hour", "duration", "critical", "risk"], rows)
}
///|
pub fn earliest_available(
calendar : ResourceCalendar,
duration_hours : Float,
from_hour : Float,
) -> Float {
let mut cursor = from_hour
let ordered = calendar.blocks.copy()
ordered.sort_by(fn(a, b) {
if a.start_hour < b.start_hour {
-1
} else if a.start_hour > b.start_hour {
1
} else {
0
}
})
for block in ordered {
if cursor + duration_hours <= block.start_hour {
return cursor
}
if cursor < block.end_hour() {
cursor = block.end_hour()
}
}
cursor
}
///|
pub fn schedule_load_gate(
calendar : ResourceCalendar,
maximum_utilization : Float,
) -> Bool {
calendar.valid() &&
calendar.conflicts().length() == 0 &&
calendar.utilization() <= maximum_utilization
}
///|
pub fn schedule_table(blocks : Array[ScheduleBlock]) -> ReportTable {
let rows : Array[Array[String]] = []
for b in blocks {
rows.push([
b.id,
b.asset,
"\{b.start_hour}",
"\{b.end_hour()}",
"\{b.duration_hours}",
"\{b.priority}",
b.operator,
])
}
table(
["id", "asset", "start", "end", "duration", "priority", "operator"],
rows,
)
}