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.2 kB · 95 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596// OpenUnitPenalty — Scoring block (soft companion to ConcurrentUnitsLimit).//// Prices work-in-progress on whole units. Where the hard `ConcurrentUnitsLimit`// forbids more than N units having *active* work on a day, this scorer prices// the number of *open* units each day — open meaning between a unit's first// start and last finish, idle gaps included. A half-finished unit sitting idle// still ties up handover/supervision attention, so it still counts as WIP.// That open-span view is the truer "prioritize completion" pressure and the// reason this is the soft general-unit limit (a hard cap can stall when every// open unit is momentarily blocked).//// day_penalty(d) = weight × max(0, openUnits(d) − softMax)//// summed over working days. `discipline` (a resourceUniqueId) scopes the open// span to that discipline's tasks — "how many units are mid-commissioning."// Omit it for the whole-unit limit. `softMax` is the free allowance; with// softMax = 0 every open-unit-day is priced.//// MiniZinc compile contract: harness supplies `unit_open_span[u,d]` (true// between unit u's first start and last finish, discipline-filtered) and// `DAYS`. Distinct from the hard constraint's active-day `unit_open[u,d]`.
import { z } from "zod";
import { openUnitCount } from "../level-core/profiles.ts";import type { Schedule, Scorer } from "../level-core/types.ts";
import type { MiniZincFragment, ScoringBlock } from "./types.ts";
export const OpenUnitPenaltyInputSchema = z.object({ unitIds: z.array(z.number().int()), discipline: z.number().int().optional(), softMax: z.number().int().nonnegative().default(0), weight: z.number().positive().default(1),});
export type OpenUnitPenaltyInput = z.infer<typeof OpenUnitPenaltyInputSchema>;
const ScorerOutputSchema: z.ZodType<Scorer> = z.custom<Scorer>( (val): val is Scorer => typeof val === "object" && val !== null && typeof (val as Scorer).name === "string" && ((val as Scorer).direction === "min" || (val as Scorer).direction === "max") && typeof (val as Scorer).score === "function",);
export const OpenUnitPenaltyBlock: ScoringBlock<OpenUnitPenaltyInput> = { id: "OpenUnitPenalty", schema: { input: OpenUnitPenaltyInputSchema, output: ScorerOutputSchema }, apply: (input): Scorer => { const { unitIds, discipline, softMax, weight } = OpenUnitPenaltyInputSchema.parse(input); // MiniZinc treats the id list as a set, so iterate distinct ids here too — // a duplicate reference must not double-count a unit's open span. const distinctUnitIds = [...new Set(unitIds)]; return { name: `OpenUnitPenalty[${discipline === undefined ? "all" : `r=${String(discipline)}`}]`, direction: "min", reads: { unitIds: distinctUnitIds, resourceUniqueIds: discipline === undefined ? undefined : [discipline], }, score(schedule: Schedule): number { // Per working day, how many units are open (first start ≤ d < last // finish) — read from the shared unit-span slot, computed once per // schedule per discipline across all scorers. const openCount = openUnitCount(schedule, distinctUnitIds, discipline);
let total = 0; for (const open of openCount) { const over = open - softMax; if (over > 0) total += weight * over; } return total; }, }; }, toMiniZinc: (input): MiniZincFragment => { const { unitIds, softMax, weight } = OpenUnitPenaltyInputSchema.parse(input); const unitIdList = unitIds.map(String).join(","); return { text: `var float: open_unit_penalty = sum(d in DAYS) ` + `( ${String(weight)} * max(0, ` + `sum(u in {${unitIdList}}) (bool2int(unit_open_span[u,d])) - ${String(softMax)}) );`, }; }, doc: { nl: "Soft WIP penalty: prices each open-unit-day beyond a free allowance (softMax), where a unit is open across its whole span including idle gaps. Pressures the schedule to finish units before opening more.", pseudocode: "sum_d weight * max(0, count(u open on d) - softMax)", },};