perf(planning): update plan observations incrementally
Cache an immutable occupancy per placements map and derive a candidate's time-off period groups, holiday-week completeness and placement tie key from its parent when one date is added or removed. The from-scratch computation stays as the test oracle and an opt-in shadow mode. A fast-check property test compares both after every operation. The loaded year drops from 66.5 s to 48.5 s and the protocol from 262.0 s to 193.7 s. Outcomes, explanations and final states stay identical. Refs #283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87d72064be
commit
7890a03a74
6 files changed
+717
-89
No files matched your search
@@ -13,6 +13,9 @@ import {
|
||||
optimizationBalanceObservations,
|
||||
optimizationObservations,
|
||||
periodData,
|
||||
derivePlacements,
|
||||
notePlacementAdded,
|
||||
notePlacementRemoved,
|
||||
placementTieKey,
|
||||
utcDate,
|
||||
workSequenceDates,
|
||||
@@ -698,19 +701,21 @@ function reason(code: string, message: string, ruleIds?: string[]): PlanningReas
|
||||
}
|
||||
|
||||
function cloneState(state: State, placements?: State['placements']): State {
|
||||
const clonedPlacements =
|
||||
placements ??
|
||||
new Map(
|
||||
[...state.placements.entries()].map(([date, placement]) => [
|
||||
date,
|
||||
{
|
||||
...placement,
|
||||
ruleIds: new Set(placement.ruleIds),
|
||||
},
|
||||
]),
|
||||
);
|
||||
derivePlacements(state.placements, clonedPlacements);
|
||||
return {
|
||||
balances: state.balances.map((balance) => ({ ...balance })),
|
||||
placements:
|
||||
placements ??
|
||||
new Map(
|
||||
[...state.placements.entries()].map(([date, placement]) => [
|
||||
date,
|
||||
{
|
||||
...placement,
|
||||
ruleIds: new Set(placement.ruleIds),
|
||||
},
|
||||
]),
|
||||
),
|
||||
placements: clonedPlacements,
|
||||
earnedActivations: state.earnedActivations.map((activation) => ({
|
||||
...activation,
|
||||
dates: [...activation.dates],
|
||||
@@ -1460,6 +1465,7 @@ function assignDateVariants(
|
||||
const result = cloneState(state);
|
||||
const target = result.balances.find((item) => item.id === balance.id)!;
|
||||
target.remaining--;
|
||||
notePlacementAdded(state.placements, result.placements, date);
|
||||
result.placements.set(date, {
|
||||
date,
|
||||
balanceId: target.id,
|
||||
@@ -1498,6 +1504,7 @@ function assignDateToBalance(
|
||||
const result = cloneState(state, new Map(state.placements));
|
||||
const target = result.balances.find((item) => item.id === balanceId)!;
|
||||
target.remaining--;
|
||||
notePlacementAdded(state.placements, result.placements, date);
|
||||
result.placements.set(date, {
|
||||
date,
|
||||
balanceId,
|
||||
@@ -1838,6 +1845,7 @@ function releasePlacement(state: State, date: string): State {
|
||||
})),
|
||||
};
|
||||
const placement = result.placements.get(date)!;
|
||||
notePlacementRemoved(state.placements, result.placements, date);
|
||||
result.placements.delete(date);
|
||||
result.balances.find((balance) => balance.id === placement.balanceId)!.remaining++;
|
||||
return result;
|
||||
@@ -2811,7 +2819,7 @@ function* optimizeRulePlacements(
|
||||
moved &&
|
||||
dateInvariantObjectives &&
|
||||
compareObjectiveLists(
|
||||
tieBreakObjectives(moved),
|
||||
tieBreakObjectives(moved, context),
|
||||
beforeVector.objectives.slice(-2),
|
||||
) >= 0
|
||||
) {
|
||||
@@ -3346,7 +3354,7 @@ function* activeObjectives(
|
||||
}
|
||||
}
|
||||
|
||||
function tieBreakObjectives(state: State): PlanningObjective[] {
|
||||
function tieBreakObjectives(state: State, context: SearchContext): PlanningObjective[] {
|
||||
return [
|
||||
{
|
||||
id: 'balance-expiry',
|
||||
@@ -3357,7 +3365,7 @@ function tieBreakObjectives(state: State): PlanningObjective[] {
|
||||
{
|
||||
id: 'placement-date',
|
||||
direction: 'minimize',
|
||||
value: placementTieKey(state),
|
||||
value: placementTieKey(state, context),
|
||||
label: 'Placement date tie-break',
|
||||
},
|
||||
];
|
||||
@@ -3367,7 +3375,10 @@ function obligationPreviewObjectives(
|
||||
preview: { state: State },
|
||||
context: SearchContext,
|
||||
): PlanningObjective[] {
|
||||
return [...activeObjectiveVector(preview.state, context), ...tieBreakObjectives(preview.state)];
|
||||
return [
|
||||
...activeObjectiveVector(preview.state, context),
|
||||
...tieBreakObjectives(preview.state, context),
|
||||
];
|
||||
}
|
||||
|
||||
/** Compare complete previews so obligation DFS does not commit to its first feasible branch. */
|
||||
@@ -3553,7 +3564,7 @@ function objectiveVector(
|
||||
label: 'Unsatisfied obligations',
|
||||
},
|
||||
...active,
|
||||
...tieBreakObjectives(state),
|
||||
...tieBreakObjectives(state, context),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -3692,6 +3703,7 @@ function planExplanation(
|
||||
compareDate(normalizedBalance.expiryDate, context.horizon.end) <= 0
|
||||
: false;
|
||||
const withoutPlacement = cloneState(state);
|
||||
notePlacementRemoved(state.placements, withoutPlacement.placements, placement.date);
|
||||
withoutPlacement.placements.delete(placement.date);
|
||||
const released = withoutPlacement.balances.find(
|
||||
(candidate) => candidate.id === placement.balanceId,
|
||||
@@ -4975,7 +4987,10 @@ export function analyzeCounterfactualInternal(
|
||||
const baseline = cloneState(finalState);
|
||||
const removeDate = selected.date;
|
||||
const removed = removeDate ? baseline.placements.get(removeDate) : undefined;
|
||||
if (removeDate) baseline.placements.delete(removeDate);
|
||||
if (removeDate) {
|
||||
notePlacementRemoved(finalState.placements, baseline.placements, removeDate);
|
||||
baseline.placements.delete(removeDate);
|
||||
}
|
||||
const released = baseline.balances.find((balance) => balance.id === selected.balanceId);
|
||||
if (released && removed?.balanceId === selected.balanceId) released.remaining++;
|
||||
const beforeState = baseline;
|
||||
|
||||
Reference in new issue
Block a user