// 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; const ModeSelectionOutputSchema: z.ZodType = z.object({ kind: z.literal("ModeSelection"), taskUniqueId: z.number().int(), modes: z.array(TaskModeSchema), }); export const ModeSelectionBlock: ConstraintBlock = { 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", }, };