Something went wrong. Try again.
A functional toolkit for authoring resource-leveling algorithms — compose constraint and scoring blocks, enumerate feasible schedules
Something went wrong. Try again.
5.2 kB · 162 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163// Weekly-binned demand profiles derived from a Schedule.//// Several scoring blocks (`UnimodalDeviation`, `IdlePercent`, `Smoothness`,// `HiringLagPenalty`, `LayoffHysteresis`) need the same weekly-peak view of// per-resource demand. Lift it to level-core so every consumer agrees on// binning rules (Sunday-snapped, working-day-only, max-per-week).//// Weeks bin from the Sunday on-or-before each day so adjacent runs share// boundaries even if their range starts differ by a few days.
import { dayToDate, resolveWorkingCalendar } from "./calendarDays.ts";import type { DayIndex, ResolvedProject, Schedule } from "./types.ts";
export interface WeeklyBucket { readonly weekStart: DayIndex; readonly value: number;}
/** Per-resource per-working-day load. O(|assignments| × avg-span). */export type ResourceLoad = Map<number, Int16Array>;
// Effective demand = static assignments, except mode-governed tasks whose// realized mode demand REPLACES their static assignment (single rule: a task// in realizedAssignments is loaded from there and never from resolved).export function effectiveAssignments( schedule: Schedule,): readonly Schedule["resolved"]["assignments"][number][] { const realized = schedule.realizedAssignments; if (!realized || realized.length === 0) return schedule.resolved.assignments; const overridden = new Set(realized.map((a) => a.taskUniqueId)); return [ ...schedule.resolved.assignments.filter( (a) => !overridden.has(a.taskUniqueId), ), ...realized, ];}
export function buildResourceLoad(schedule: Schedule): ResourceLoad { const cal = resolveWorkingCalendar(schedule.resolved, null); const out: ResourceLoad = new Map();
const effective = effectiveAssignments(schedule);
for (const a of effective) { if (!out.has(a.resourceUniqueId)) { out.set(a.resourceUniqueId, new Int16Array(cal.horizonDays)); } }
const taskById = new Map(schedule.tasks.map((t) => [t.uniqueId, t])); for (const a of effective) { const t = taskById.get(a.taskUniqueId); if (!t) continue; const arr = out.get(a.resourceUniqueId)!; for (let d = t.startDay; d < t.finishDay; d++) { if (cal.bits[d] === 1) arr[d]! += a.units; } } return out;}
export function peakLoad(load: ResourceLoad, resourceId: number): number { const arr = load.get(resourceId); if (!arr) return 0; let m = 0; for (const v of arr) if (v > m) m = v; return m;}
export function combinedPeakLoad( load: ResourceLoad, resourceIds: readonly number[],): number { const arrs = resourceIds .map((id) => load.get(id)) .filter((a): a is Int16Array => !!a); if (arrs.length === 0) return 0; let m = 0; const n = arrs[0]!.length; for (let i = 0; i < n; i++) { let s = 0; for (const a of arrs) s += a[i]!; if (s > m) m = s; } return m;}
/** Snap `day` back to the Sunday on-or-before in the resolved calendar's epoch frame. */export function snapToSunday( resolved: ResolvedProject, day: DayIndex,): DayIndex { const cal = resolveWorkingCalendar(resolved, null); const d = dayToDate(cal, day); return day - d.getDay();}
/** Per-resource weekly peak — max working-day units in each 7-day window. */export function weeklyProfile( resolved: ResolvedProject, arr: Int16Array | undefined, rangeStart: DayIndex, rangeEnd: DayIndex,): WeeklyBucket[] { if (!arr) return []; const cal = resolveWorkingCalendar(resolved, null); const result: WeeklyBucket[] = []; let ws = snapToSunday(resolved, rangeStart); while (ws <= rangeEnd) { let max = 0; const dStart = Math.max(0, ws); const dEnd = Math.min(ws + 6, rangeEnd, cal.horizonDays - 1); for (let d = dStart; d <= dEnd; d++) { if (cal.bits[d] === 1 && arr[d]! > max) max = arr[d]!; } result.push({ weekStart: ws, value: max }); ws += 7; } return result;}
/** Combined weekly peak across multiple resources — for "install pool" views. */export function weeklyProfileCombined( resolved: ResolvedProject, arrs: readonly Int16Array[], rangeStart: DayIndex, rangeEnd: DayIndex,): WeeklyBucket[] { const cal = resolveWorkingCalendar(resolved, null); const result: WeeklyBucket[] = []; let ws = snapToSunday(resolved, rangeStart); while (ws <= rangeEnd) { let max = 0; const dStart = Math.max(0, ws); const dEnd = Math.min(ws + 6, rangeEnd, cal.horizonDays - 1); for (let d = dStart; d <= dEnd; d++) { if (cal.bits[d] !== 1) continue; let s = 0; for (const a of arrs) s += a[d]!; if (s > max) max = s; } result.push({ weekStart: ws, value: max }); ws += 7; } return result;}
/** Derive a [start, end] day-index range from a Schedule's actual placements. */export function scheduleSpan(schedule: Schedule): { startDay: DayIndex; endDay: DayIndex;} { if (schedule.tasks.length === 0) return { startDay: 0, endDay: 0 }; let s = Infinity; let e = 0; for (const t of schedule.tasks) { if (t.startDay < s) s = t.startDay; if (t.finishDay > e) e = t.finishDay; } return { startDay: s === Infinity ? 0 : s, endDay: e };}