Update scheduling algorithm

This commit is contained in:
2026-07-23 14:02:14 -04:00
parent 5eebee70d8
commit 23ae3a2f3a
12 changed files with 555 additions and 231 deletions
+72 -121
View File
@@ -4,7 +4,7 @@ import os
from dataclasses import dataclass
from datetime import date, timedelta
from test_window import get_shift_sequence_with_capacity, get_shift_capacity_for_date, is_off_day, next_window_start_date
from test_bundle import BUNDLE_PRIORITY_TOP, Test, TestBundle, build_test_bundles, bundle_pair_lookup
from test_bundle import Test, TestBundle, build_test_bundles, update_mirrored_bundle_priorities
DUT = (os.getenv("DUT") or "DUT").strip()
REF = (os.getenv("REF") or "REF").strip()
@@ -38,7 +38,7 @@ class Scheduler:
daytime_testing_today: bool = False,
dual_device_weekend_start_enabled: bool = False,
dual_device_window_start_dates: set[str] | None = None,
priority_weight: int = 100,
priority_weight: int = 1000,
):
self.top_priority_tests = top_priority_tests
self.tests = tests
@@ -50,11 +50,10 @@ class Scheduler:
self.dual_device_weekend_start_enabled = dual_device_weekend_start_enabled
self.dual_device_window_start_dates = dual_device_window_start_dates or set()
self.priority_weight = priority_weight
self.pending_dut_mirror: list[TestBundle] = []
self.pending_ref_mirror: list[TestBundle] = []
self.schedule: list[ScheduleEntry] = []
self.scheduled_bundle_keys: set[tuple[int, str]] = set()
self.scheduled_bundle_keys: set[tuple[int, str]] = set()
self.scheduled_test_ids: set[str] = set()
def compile_schedule(self) -> str | None:
bundles = build_test_bundles(self.active_dut, self.active_ref, self.top_priority_tests)
@@ -65,80 +64,42 @@ class Scheduler:
print("Compiled Test Bundles:")
for bundle in bundles:
print(f"Bundle Index: {bundle.index}, Device: {bundle.device}, Priority: {bundle.priority}, Config: {bundle.config}, Total Minutes: {bundle.total_minutes}, Tests: {bundle.tests}")
active_dut_bundles = [b for b in bundles if b.device == DUT]
active_ref_bundles = [b for b in bundles if b.device == REF]
# Group by individual TC values (from config tuples)
all_tcs = set()
for bundle in bundles:
all_tcs.update(bundle.config if bundle.config else [None])
all_bundles_by_tc = self.create_tc_dict(bundles, all_tcs)
# Create dict: TC -> bundles supporting that TC
bundles_by_tc: dict = {}
# Sort with None last
sorted_tcs = sorted([tc for tc in all_tcs if tc is not None]) + ([None] if None in all_tcs else [])
for tc in sorted_tcs:
if tc is None:
bundles_by_tc[tc] = [b for b in bundles if not b.config]
else:
bundles_by_tc[tc] = [b for b in bundles if tc in b.config]
# Sort by priority, then index
bundles_by_tc[tc].sort(key=lambda b: (b.priority, b.index))
window_index = 0
cursor_date = self.start_date
# Process TCs that include top-priority bundles first.
# REF top-priority TCs come before DUT top-priority TCs so that
# entering tests in the REF top-priority field always claims tonight's
# first window for REF.
def tc_sort_key(item: tuple[str | None, list[TestBundle]]) -> tuple[int, int, int, str]:
tc, tc_bundles = item
has_ref_top = any(bundle.priority == BUNDLE_PRIORITY_TOP and bundle.device == REF for bundle in tc_bundles)
has_dut_top = any(bundle.priority == BUNDLE_PRIORITY_TOP and bundle.device == DUT for bundle in tc_bundles)
min_priority = min((bundle.priority for bundle in tc_bundles), default=BUNDLE_PRIORITY_TOP + 99)
tc_name = tc if tc is not None else "~"
if has_dut_top:
tier = 0 # DUT top-priority TCs first (wins when both exist)
elif has_ref_top:
tier = 1 # REF-only top-priority TCs second
else:
tier = 2 # All other TCs
return (tier, min_priority, -len(tc_bundles), tc_name)
tc_order = self.get_tc_order(self.tests)
sorted_tc_items = sorted(
[(tc, bundles_by_tc[tc]) for tc in bundles_by_tc],
key=tc_sort_key,
)
for tc in tc_order:
tc_bundles = all_bundles_by_tc[tc]
if tc_bundles is None or len(tc_bundles) == 0:
continue
window_device = DUT
pending_dut: list[TestBundle] = []
pending_ref: list[TestBundle] = []
for bundle in tc_bundles:
if bundle.device == DUT:
pending_dut.append(bundle)
for tc, tc_bundles in sorted_tc_items:
tc_has_ref_top_priority = any(
bundle.device == REF and bundle.priority == BUNDLE_PRIORITY_TOP
for bundle in tc_bundles
)
tc_has_dut_top_priority = any(
bundle.device == DUT and bundle.priority == BUNDLE_PRIORITY_TOP
for bundle in tc_bundles
)
elif bundle.device == REF:
pending_ref.append(bundle)
if tc_has_ref_top_priority:
window_device = REF
elif tc_has_dut_top_priority:
window_device = DUT
else:
window_device = DUT
dut_unscheduled = [b for b in tc_bundles if b.device == DUT]
ref_unscheduled = [b for b in tc_bundles if b.device == REF]
while dut_unscheduled or ref_unscheduled:
active_unscheduled = dut_unscheduled if window_device == DUT else ref_unscheduled
if len(active_unscheduled) == 0:
while pending_dut or pending_ref:
active_pending = pending_dut if window_device == DUT else pending_ref
if len(active_pending) == 0:
# If no unscheduled bundles for the current device, switch to the other device
window_device = REF if window_device == DUT else DUT
active_unscheduled = dut_unscheduled if window_device == DUT else ref_unscheduled
active_pending = pending_dut if window_device == DUT else pending_ref
shifts, capacity = get_shift_sequence_with_capacity(cursor_date, self.holiday_dates, self.daytime_testing_today)
cursor_date_key = cursor_date.isoformat()
@@ -146,60 +107,20 @@ class Scheduler:
if not dual_device_window and self.dual_device_weekend_start_enabled:
dual_device_window = self._is_weekend_start_day(cursor_date)
# Evaluate both devices without mutating queue state, then commit once.
mirrored_bundles, knapsack_bundles, remaining_time = self._select_bundles(
window_device,
capacity,
active_unscheduled,
tc,
mutate=True,
)
selected_bundles = mirrored_bundles + knapsack_bundles
if len(selected_bundles) == 0:
window_device = REF if window_device == DUT else DUT
active_unscheduled = dut_unscheduled if window_device == DUT else ref_unscheduled
mirrored_bundles, knapsack_bundles, remaining_time = self._select_bundles(
window_device,
capacity,
active_unscheduled,
tc,
mutate=True,
)
selected_bundles = mirrored_bundles + knapsack_bundles
if len(selected_bundles) == 0:
break
selected_bundles = []
selected_bundles = self._knapsack_select(capacity, active_pending)
remaining_time = capacity - sum(bundle.total_minutes for bundle in selected_bundles)
primary_device = window_device
if dual_device_window and remaining_time > 0:
secondary_device = REF if primary_device == DUT else DUT
secondary_unscheduled = dut_unscheduled if secondary_device == DUT else ref_unscheduled
secondary_mirrored, secondary_knapsack, remaining_time = self._select_bundles(
secondary_device,
remaining_time,
secondary_unscheduled,
tc,
mutate=True,
)
selected_bundles.extend(secondary_mirrored + secondary_knapsack)
# Create a mirror of the other device for the next window
bundle_pairs = bundle_pair_lookup(knapsack_bundles, dut_unscheduled, ref_unscheduled)
if window_device == DUT:
pending_target = self.pending_ref_mirror
else:
pending_target = self.pending_dut_mirror
existing_pending = {(b.index, b.device) for b in pending_target}
selected_bundle_keys = {(b.index, b.device) for b in selected_bundles}
for pair_bundle in bundle_pairs:
pair_key = (pair_bundle.index, pair_bundle.device)
if pair_key in self.scheduled_bundle_keys or pair_key in existing_pending or pair_key in selected_bundle_keys:
continue
pending_target.append(pair_bundle)
existing_pending.add(pair_key)
secondary_pending = pending_dut if secondary_device == DUT else pending_ref
secondary_selected_bundles = self._knapsack_select(remaining_time, secondary_pending)
selected_bundles.extend(secondary_selected_bundles)
# Create a mirror of the other device for the next window by updating the priorities
pending_dut, pending_ref = update_mirrored_bundle_priorities(selected_bundles, pending_dut, pending_ref)
window = ScheduleWindow(
index=window_index,
@@ -216,12 +137,12 @@ class Scheduler:
selected_keys = {(b.index, b.device) for b in selected_bundles}
# Remove scheduled bundles from ALL TC buckets globally
for all_tc in bundles_by_tc:
bundles_by_tc[all_tc] = [b for b in bundles_by_tc[all_tc] if (b.index, b.device) not in selected_keys]
for all_tc in all_bundles_by_tc:
all_bundles_by_tc[all_tc] = [b for b in all_bundles_by_tc[all_tc] if (b.index, b.device) not in selected_keys]
# Rebuild current TC unscheduled lists
dut_unscheduled = [b for b in bundles_by_tc[tc] if b.device == DUT]
ref_unscheduled = [b for b in bundles_by_tc[tc] if b.device == REF]
# Remove from pending queues
pending_dut = [b for b in pending_dut if (b.index, b.device) not in selected_keys]
pending_ref = [b for b in pending_ref if (b.index, b.device) not in selected_keys]
window_index += 1
window_device = REF if window_device == DUT else DUT
@@ -243,7 +164,7 @@ class Scheduler:
mutate: bool,
) -> tuple[list[TestBundle], list[TestBundle], int]:
# Consume pending mirrored bundles only while there is room.
pending = self.pending_dut_mirror if device == DUT else self.pending_ref_mirror
pending = self.pending_dut if device == DUT else self.pending_ref
mirrored_bundles: list[TestBundle] = []
still_pending: list[TestBundle] = [] # Bundles that couldn't fit in the remaining capacity
remaining_capacity = capacity
@@ -288,11 +209,13 @@ class Scheduler:
) -> list[TestBundle]:
if capacity <= 0 or not candidates:
return []
# Weights are total minutes of each bundle
weights = [bundle.total_minutes for bundle in candidates]
# Values are based on priority, lower priority number means higher value
values = [self.priority_weight - bundle.priority for bundle in candidates]
# Values strongly favor higher priority tiers without hard-forcing them.
max_priority = max(bundle.priority for bundle in candidates)
priority_bias = 2.5
values = [int(self.priority_weight * (priority_bias ** (max_priority - bundle.priority))) for bundle in candidates]
# Implement dynamic programming knapsack algorithm to select bundles
n = len(candidates)
@@ -387,6 +310,34 @@ class Scheduler:
if device == REF and test_id in self.active_ref:
return self.active_ref[test_id].estimated_minutes
return 0
def create_tc_dict(self, bundles: list[TestBundle], all_tcs: set[str | None]) -> dict[str | None, list[TestBundle]]:
# Create dict: TC -> bundles supporting that TC
all_bundles_by_tc: dict[str | None, list[TestBundle]] = {}
# Sort with None last
sorted_tcs = sorted([tc for tc in all_tcs if tc is not None]) + ([None] if None in all_tcs else [])
for tc in sorted_tcs:
if tc is None:
all_bundles_by_tc[tc] = [b for b in bundles if not b.config]
else:
all_bundles_by_tc[tc] = [b for b in bundles if tc in b.config]
# Sort by priority, then index
all_bundles_by_tc[tc].sort(key=lambda b: (b.priority, b.index))
return all_bundles_by_tc
def get_tc_order(self, all_tests: list[Test]) -> list[str | None]:
# Implement the logic for getting the test case order
all_dut = {test.test_id: test for test in all_tests if test.device == DUT}
all_ref = {test.test_id: test for test in all_tests if test.device == REF}
bundles = build_test_bundles(all_dut, all_ref, [])
all_tcs = set()
for bundle in bundles:
all_tcs.update(bundle.config if bundle.config else [None])
all_bundles_by_tc = self.create_tc_dict(bundles, all_tcs)
# Sort TCs by the number of bundles available for each TC (most to least)
sorted_tcs = sorted(all_bundles_by_tc.keys(), key=lambda tc: len(all_bundles_by_tc[tc]), reverse=True)
return sorted_tcs