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.
3.4 kB · 93 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394// ModeSelection — Constraint block.//// Multi-mode RCPSP: the task runs in exactly one of the listed modes, each an// alternative duration + resource demand. Listed order is preference — the// greedy serial-SGS tries modes in order at each candidate day, so a later// mode (e.g. a borrowed crew) only wins when it lets the task start strictly// earlier than every mode listed before it. The chosen mode lands on// `ScheduledTask.modeId` and its demand in `Schedule.realizedAssignments`.//// MiniZinc compile contract: harness supplies `mode[t]` (the per-task mode// decision variable) plus per-mode duration/demand tables derived from this// constraint's `modes`; the fragment pins the task's domain to its mode ids.//// Prefer one instance per task: the two targets disagree when a task carries// multiple ModeSelection instances. Serial-SGS unions their mode lists (the// preprocess step appends), while the MiniZinc fragments intersect the domain// (`mode[t] in {..}` per instance), typically UNSAT. A compile-time merge or an// analyzeRun warning to reconcile this divergence is future work.
import { z } from "zod";
import type { ModeSelectionConstraint } from "../level-core/types.ts";
import type { ConstraintBlock, MiniZincFragment } from "./types.ts";
const TaskModeSchema = z .object({ modeId: z.number().int(), durationDays: z.number().int().positive(), resourceDemand: z.array( z.object({ resourceUniqueId: z.number().int(), units: z.number().positive(), }), ), }) .refine( (mode) => new Set(mode.resourceDemand.map((d) => d.resourceUniqueId)).size === mode.resourceDemand.length, { message: "resourceUniqueId must be unique within a mode — a repeat double-counts load and materializes duplicate assignments", path: ["resourceDemand"], }, );
export const ModeSelectionInputSchema = z .object({ taskUniqueId: z.number().int(), modes: z.array(TaskModeSchema).min(1), }) .refine( (input) => new Set(input.modes.map((m) => m.modeId)).size === input.modes.length, { message: "mode ids must be unique — the chosen modeId attributes the placement", path: ["modes"], }, );
export type ModeSelectionInput = z.infer<typeof ModeSelectionInputSchema>;
const ModeSelectionOutputSchema: z.ZodType<ModeSelectionConstraint> = z.object({ kind: z.literal("ModeSelection"), taskUniqueId: z.number().int(), modes: z.array(TaskModeSchema),});
export const ModeSelectionBlock: ConstraintBlock<ModeSelectionInput> = { id: "ModeSelection", schema: { input: ModeSelectionInputSchema, output: ModeSelectionOutputSchema, }, apply: ({ taskUniqueId, modes }): ModeSelectionConstraint => ({ kind: "ModeSelection", taskUniqueId, modes, }), toMiniZinc: ({ taskUniqueId, modes }): MiniZincFragment => { const modeIdList = modes.map((m) => String(m.modeId)).join(","); return { text: `constraint mode[${String(taskUniqueId)}] in {${modeIdList}};`, }; }, doc: { nl: "The task runs in exactly one of the listed modes — alternative duration and resource-demand pairs. Order is preference: a later mode is chosen only when it starts the task strictly earlier.", pseudocode: "(duration(task), demand(task)) = modes[chosen], chosen in modeIds", },};