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.
4.8 kB · 139 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140// Shared derived data over a Schedule — spaCy's listener pattern.//// Several scoring blocks derive from the same expensive intermediates:// per-day resource histograms and per-unit open spans. Computed// independently by each scorer that's O(scorers × days × tasks) per// candidate schedule; computed once here it's O(days × tasks) plus cheap// reads. The provider (this module) owns the derivation; scorer blocks are// listeners that read the slot. Ownership sits with the runner, not the// blocks, because blocks can't see each other.//// Memoization is per-Schedule via WeakMap, which is safe because Schedule// is immutable by contract: a search emits a new Schedule per candidate// rather than mutating one in place.
import { resolveWorkingCalendar } from "./calendarDays.ts";import type { DayIndex, Schedule, ScheduledTask } from "./types.ts";import { buildResourceLoad, effectiveAssignments, type ResourceLoad,} from "./weeklyProfile.ts";
export interface UnitSpan { readonly startDay: DayIndex; readonly finishDay: DayIndex;}
export interface ScheduleProfiles { /** Per-resource per-working-day demand (realized-mode aware). */ readonly resourceLoad: ResourceLoad; /** Scheduled task lookup by uniqueId. */ readonly taskById: ReadonlyMap<number, ScheduledTask>; /** Open span (first start → last finish) of every work unit, optionally * counting only tasks assigned to `discipline` (a resourceUniqueId). * Units with no scheduled (matching) tasks, or an empty span, are absent. */ unitSpans(discipline?: number): ReadonlyMap<number, UnitSpan>;}
function computeUnitSpans( schedule: Schedule, discipline: number | null,): ReadonlyMap<number, UnitSpan> { const taskById = new Map(schedule.tasks.map((t) => [t.uniqueId, t])); const disciplineTasks = discipline === null ? null : new Set( effectiveAssignments(schedule) .filter((a) => a.resourceUniqueId === discipline) .map((a) => a.taskUniqueId), );
const spans = new Map<number, UnitSpan>(); for (const [unitId, unit] of schedule.resolved.workUnits) { let start = Infinity; let finish = -Infinity; for (const taskId of unit.taskUniqueIds) { if (disciplineTasks && !disciplineTasks.has(taskId)) continue; const t = taskById.get(taskId); if (!t) continue; if (t.startDay < start) start = t.startDay; if (t.finishDay > finish) finish = t.finishDay; } if (start === Infinity || finish <= start) continue; spans.set(unitId, { startDay: start, finishDay: finish }); } return spans;}
class ProfilesImpl implements ScheduleProfiles { private resourceLoadMemo: ResourceLoad | null = null; private taskByIdMemo: ReadonlyMap<number, ScheduledTask> | null = null; private readonly unitSpansMemo = new Map< number | null, ReadonlyMap<number, UnitSpan> >();
private readonly schedule: Schedule;
constructor(schedule: Schedule) { this.schedule = schedule; }
get resourceLoad(): ResourceLoad { this.resourceLoadMemo ??= buildResourceLoad(this.schedule); return this.resourceLoadMemo; }
get taskById(): ReadonlyMap<number, ScheduledTask> { this.taskByIdMemo ??= new Map( this.schedule.tasks.map((t) => [t.uniqueId, t]), ); return this.taskByIdMemo; }
unitSpans(discipline?: number): ReadonlyMap<number, UnitSpan> { const key = discipline ?? null; let spans = this.unitSpansMemo.get(key); if (!spans) { spans = computeUnitSpans(this.schedule, key); this.unitSpansMemo.set(key, spans); } return spans; }}
const cache = new WeakMap<Schedule, ScheduleProfiles>();
/** The shared read slot for a schedule's derived data. Same Schedule object * → same profiles instance; each derivation runs at most once. */export function scheduleProfiles(schedule: Schedule): ScheduleProfiles { let profiles = cache.get(schedule); if (!profiles) { profiles = new ProfilesImpl(schedule); cache.set(schedule, profiles); } return profiles;}
/** Count of open units per working day across the given unit ids, using the * shared span slot. Shared by OpenUnitPenalty and unit-WIP reporting. */export function openUnitCount( schedule: Schedule, unitIds: readonly number[], discipline?: number,): Int32Array { const cal = resolveWorkingCalendar(schedule.resolved, null); const spans = scheduleProfiles(schedule).unitSpans(discipline); const counts = new Int32Array(cal.horizonDays); for (const unitId of new Set(unitIds)) { const span = spans.get(unitId); if (!span) continue; const end = Math.min(span.finishDay, cal.horizonDays); for (let d = span.startDay; d < end; d++) { if (cal.bits[d] === 1) counts[d]! += 1; } } return counts;}