perf(planning): prune candidates that cannot beat the protected prefix
Finalizing a candidate never moves or retypes a placement, so preference results and calendar optimizations already have their final values before it, and leave-balance optimizations do too when nothing can be earned. Reject a candidate before finalizing when the first differing such objective of the protected prefix is worse than the baseline. Leave-placement objectives and earning contexts stop the walk. The frozen-engine check now also compares cancelled asynchronous runs, including checkpoints and retained incumbents. The loaded year drops from 48.5 s to 44.3 s; outcomes, explanations and final states are unchanged. Refs #284 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7890a03a74
commit
68a9e33de6
4 files changed
+208
-4
No files matched your search
@@ -2350,6 +2350,71 @@ function* timeOffPeriodPlacementCandidates(
|
||||
}
|
||||
}
|
||||
|
||||
type FinalizeInvariance = 'exact' | 'without-earning' | 'unknown';
|
||||
const finalizeInvarianceCache = new WeakMap<SearchContext, FinalizeInvariance[]>();
|
||||
|
||||
/**
|
||||
* Finalizing never moves or retypes placements; it only edits ruleIds, supporting, and earned
|
||||
* activations/balances. Preference results and the calendar optimizations (public-holiday-weeks,
|
||||
* time-off-periods, work-sequences) read only placement dates and types, so the pre-finalize
|
||||
* value is already the final one. leave-balances is exact only when nothing can be earned (no
|
||||
* rewarding if_then rule and no earned activation on the candidate). leave-placements reads
|
||||
* `supporting`, which finalizing rewrites. Entries follow activeObjectives order.
|
||||
*/
|
||||
function finalizeInvariance(context: SearchContext): FinalizeInvariance[] {
|
||||
let entries = finalizeInvarianceCache.get(context);
|
||||
if (!entries) {
|
||||
const earning = context.rules.some(
|
||||
(rule) => rule.enabled && rule.type === 'if_then' && rule.rewardDays,
|
||||
);
|
||||
entries = context.rules
|
||||
.filter((rule) => rule.enabled && rule.force === 'preference')
|
||||
.map((rule) => {
|
||||
if (rule.type !== 'optimization') return 'exact';
|
||||
switch (rule.configuration.elements) {
|
||||
case 'public-holiday-weeks':
|
||||
case 'time-off-periods':
|
||||
case 'work-sequences':
|
||||
return 'exact';
|
||||
case 'leave-balances':
|
||||
return earning ? 'unknown' : 'without-earning';
|
||||
default:
|
||||
// New element kinds stay unpruned until their bound is argued.
|
||||
return 'unknown';
|
||||
}
|
||||
});
|
||||
finalizeInvarianceCache.set(context, entries);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** True when the first differing finalize-invariant protected objective is already worse than the baseline. */
|
||||
function provablyWorseThanProtectedPrefix(
|
||||
candidate: State,
|
||||
context: SearchContext,
|
||||
throughIndex: number,
|
||||
baseline: readonly PlanningObjective[],
|
||||
): boolean {
|
||||
const invariance = finalizeInvariance(context);
|
||||
let index = 0;
|
||||
for (const objective of activeObjectives(candidate, context, throughIndex)) {
|
||||
const kind = invariance[index];
|
||||
if (
|
||||
kind === 'unknown' ||
|
||||
(kind === 'without-earning' && candidate.earnedActivations.length)
|
||||
)
|
||||
return false;
|
||||
const comparison = compareObjectiveValues(
|
||||
objective.direction,
|
||||
objective.value,
|
||||
baseline[index].value,
|
||||
);
|
||||
if (comparison !== 0) return comparison > 0;
|
||||
index++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function evaluateRuleCandidate(
|
||||
before: State,
|
||||
candidate: State | null,
|
||||
@@ -2360,6 +2425,8 @@ function evaluateRuleCandidate(
|
||||
incumbentVector = objectiveVector(before, context),
|
||||
): { state: State; vector?: PlanningObjectiveVector } | undefined {
|
||||
if (!candidate) return undefined;
|
||||
if (provablyWorseThanProtectedPrefix(candidate, context, throughIndex, baseline))
|
||||
return undefined;
|
||||
const state = finalizeRuleCandidate(candidate, context, before);
|
||||
if (!state) return undefined;
|
||||
const active: PlanningObjective[] = [];
|
||||
|
||||
Reference in new issue
Block a user