Update scheduling algorithm
This commit is contained in:
+72
-121
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user