knapsack scheduling algorithm

This commit is contained in:
2026-07-12 14:19:58 -04:00
parent 031da48ddd
commit 9e100d60d0
56 changed files with 1936 additions and 1286 deletions
+79 -78
View File
@@ -6,7 +6,7 @@ from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterator
from graph import build_station_testpoint_map
from test_config import serialize_station_testpoint_map
APP_ROOT = Path(__file__).resolve().parent
@@ -123,13 +123,6 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
minutes INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS graph_cache (
name TEXT PRIMARY KEY,
payload_json TEXT NOT NULL,
test_count INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS rerun_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
detected_date TEXT NOT NULL,
@@ -166,14 +159,38 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
def _serialize_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> str:
"""Compute and serialize the station-to-testpoint map from a test config."""
station_map = build_station_testpoint_map(config)
return json.dumps(station_map)
return serialize_station_testpoint_map(config)
def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> int:
if not records:
return 0
values: list[tuple[Any, ...]] = []
for r in records:
station_testpoint_map = _serialize_station_testpoint_map(r.config)
values.append(
(
r.test_id,
r.device,
r.test_type,
r.rotation,
r.rx_tx,
int(r.has_coe_pair),
json.dumps(r.coe_pairing or []),
json.dumps(r.config),
r.priority,
r.victim_band,
int(r.throttled),
r.estimated_minutes,
r.status,
int(r.excluded),
json.dumps(r.raw_payload or {}),
station_testpoint_map,
)
)
with get_connection(db_path) as conn:
conn.executemany(
"""
@@ -200,27 +217,7 @@ def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> in
station_testpoint_map = excluded.station_testpoint_map,
updated_at = CURRENT_TIMESTAMP
""",
[
(
r.test_id,
r.device,
r.test_type,
r.rotation,
r.rx_tx,
int(r.has_coe_pair),
json.dumps(r.coe_pairing or []),
json.dumps(r.config),
r.priority,
r.victim_band,
int(r.throttled),
r.estimated_minutes,
r.status,
int(r.excluded),
json.dumps(r.raw_payload or {}),
_serialize_station_testpoint_map(r.config),
)
for r in records
],
values,
)
return len(records)
@@ -253,52 +250,6 @@ def read_settings(db_path: str | Path = DB_PATH) -> dict[str, Any]:
return {row["key"]: json.loads(row["value_json"]) for row in rows}
def save_graph_cache(
name: str,
payload: dict[str, list[str]],
test_count: int,
db_path: str | Path = DB_PATH,
) -> None:
with get_connection(db_path) as conn:
conn.execute(
"""
INSERT INTO graph_cache(name, payload_json, test_count, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(name) DO UPDATE SET
payload_json = excluded.payload_json,
test_count = excluded.test_count,
updated_at = CURRENT_TIMESTAMP
""",
(name, json.dumps(payload), int(test_count)),
)
def load_graph_cache(name: str, db_path: str | Path = DB_PATH) -> dict[str, Any] | None:
with get_connection(db_path) as conn:
row = conn.execute(
"""
SELECT payload_json, test_count, updated_at
FROM graph_cache
WHERE name = ?
""",
(name,),
).fetchone()
if row is None:
return None
return {
"payload": json.loads(row["payload_json"] or "{}"),
"test_count": int(row["test_count"]),
"updated_at": row["updated_at"],
}
def delete_graph_cache(name: str, db_path: str | Path = DB_PATH) -> None:
with get_connection(db_path) as conn:
conn.execute("DELETE FROM graph_cache WHERE name = ?", (name,))
def _parse_rule_tokens(rule: str | None) -> list[str]:
if not rule:
return []
@@ -345,7 +296,7 @@ def _match_any_band_value(record: TestRecord, key: str, token_suffix: str) -> bo
target_num = _extract_signed_int(token_suffix)
target_compact = _normalize_compact(token_suffix)
keys = ("Station 1", "Station 2", "Station 3") if record.test_type == "P3P" else ("5G", "6G", "2G")
keys = ("STATION 1", "STATION 2", "STATION 3") if record.test_type == "P3P" else ("5G", "6G", "2G")
for band in keys:
entry = _band_entry(record.config, band)
@@ -579,6 +530,56 @@ def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[Sc
return result
def get_schedule_rows_for_latest_version(db_path: str | Path = DB_PATH) -> list[ScheduleRow]:
with get_connection(db_path) as conn:
version_row = conn.execute("SELECT MAX(schedule_version) AS latest FROM schedules").fetchone()
latest = version_row["latest"]
if latest is None:
return []
rows = conn.execute(
"""
SELECT
s.test_id,
s.device,
s.scheduled_date,
s.shift_index,
s.sequence_in_shift,
t.test_type,
t.rotation,
t.config_json,
t.status,
t.priority,
t.estimated_minutes
FROM schedules s
JOIN tests t ON t.test_id = s.test_id AND t.device = s.device
WHERE s.schedule_version = ?
ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift
""",
(latest,),
).fetchall()
result: list[ScheduleRow] = []
for row in rows:
result.append(
ScheduleRow(
test_id=row["test_id"],
device=row["device"],
scheduled_date=row["scheduled_date"],
shift_index=int(row["shift_index"]),
sequence_in_shift=int(row["sequence_in_shift"]),
test_type=row["test_type"],
rotation=row["rotation"],
config=json.loads(row["config_json"] or "{}"),
status=row["status"],
priority=int(row["priority"]),
estimated_minutes=int(row["estimated_minutes"]),
)
)
return result
def mark_tests_completed(test_ids_with_device: list[tuple[str, str]], db_path: str | Path = DB_PATH) -> None:
if not test_ids_with_device:
return