diff --git a/.gitignore b/.gitignore index ded0387..b479b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /backend/__pycache__ /backend/_run_sched_test.py /backend/output.txt +/backend/.env IMPLEMENTATIONPLAN.md scheduler.db scheduler.db* \ No newline at end of file diff --git a/DESIGNPLAN.md b/DESIGNPLAN.md index b69627e..3067ab5 100644 --- a/DESIGNPLAN.md +++ b/DESIGNPLAN.md @@ -7,7 +7,7 @@ A test scheduler for the NJTH which compiles a best schedule according to the ru - DUT test result directory path, REF test result directory path - Estimated runtime for each type of test for calculations -- Rotation definition +- Test config definition - Test exclusions - Top priority tests - Allow user to manually override and enter tests that needs to be run tonight @@ -15,23 +15,30 @@ A test scheduler for the NJTH which compiles a best schedule according to the ru ## Output ### Backend -- Compile a schedule that would require the lowest amount of time in total according to the following rules and priority: +- Compile a schedule that would require the lowest amount of time and least amount of wasted time in total according to the following rules and priority: - **Rules** - - Can be run in the same night - - Same rotation and same test point P2P/COE tests - - COERXBE002: T1F -> STA63, T2A -> STA56, T3E -> STA4, R3 - - COERXBE003: T1F -> STA63, T2A -> STA56, T3E -> STA4, R3 - - - Different rotations but all stations are at same testpoints - - R2 P2PRXAC003 5G T1C -> STA4 - - R1 P2PRXAC012 2G T1C -> STA4 - - - Same rotation and same test point P3P tests - - Needs to be run back to back + - Test Windows + - Weekday night time test window: Monday - Thursday + - Starts 5PM of that day, ends 9AM the next day + - Capcity: 17 hrs (16hrs + 1hr margin) + - Ex. Monday 6/29 5PM - Tuesday 6/30 9AM + - Weekday day time test window: Monday-Thursday + - Starts 9AM - 5PM of that day + - Capacity: 8 hrs + - Weekend test window: Friday - Sunday + - Starts Friday 5PM, ends Monday 9AM + - Capcity: 65 hrs (64hrs + 1hr margin) + - Holiday test window + - All tests in the same test window needs to have the same test config + - Tests are performed on one device in each test window, unless overrides by user + - Some tests needs to be run back to back + - P2P COE pairings + - Run P2P first then its COE pairings - P2P up link (RX) and down link (TX) - Ex. P2PRXAX001 and P2PTXAX001 - - COE P2P bundle - - DUT and REF tests + - P3P throttled(TH) and unthrottled(UT) + - Same tests, different devices(DUT, REF) needs to be run in back to back test windows + - Ex. Monday - **Priority** - Default priority: COE P2P pair -> P2P only -> P3P - 1: highest priority (must run tonight) @@ -40,8 +47,6 @@ A test scheduler for the NJTH which compiles a best schedule according to the ru - 4: P3P - 5: lowest priority - Priority 1 and 5 needs to be entered manually - - Same test points and same rotation will be run first - - Same test points but different will run if there is still remaining testing time for the day - **Calculation** - Use the estimate run time for each type of test to fit as many tests as possible in a shift of testing. - Weekdays are 16hrs, weekends are 24 hours with 1hr margin @@ -137,7 +142,7 @@ build_test_bundles(): - Create bundles of tests that needs to be run back to back - Types of test bundles and their order 1. Failed tests rerun if exist - 2. The base is P2P and has COE pairs: P2P on DUT -> COE pairings on DUT -> P2P on REF -> COE pairings on REF + 2. The P2P and COE pairs: P2P -> COE pairings 3. P2P tests without COE pairs: P2PRX on DUT -> P2PTX on DUT -> P2PRX on REF -> P2PTX on REF 4. P3P tests (ALL ROT's) ``` diff --git a/backend/.env b/backend/.env deleted file mode 100644 index bc8f05d..0000000 --- a/backend/.env +++ /dev/null @@ -1,2 +0,0 @@ -DUT="CGW453" -REF="CGW452" \ No newline at end of file diff --git a/backend/app.py b/backend/app.py index 5e36462..54b258c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,7 +1,7 @@ from contextlib import asynccontextmanager from pathlib import Path from typing import Any -from datetime import date, datetime +from datetime import date, datetime, timedelta import os from fastapi import FastAPI, HTTPException @@ -9,9 +9,15 @@ from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field import db -import graph from parser import CsvValidationError, parse_target_csv -from scheduler import SchedulerTest, compile_schedule, remove_from_active, reset_scheduler_state + +# Ensure new_scheduler resolves the same DUT/REF labels as db records. +os.environ.setdefault("DUT", db.DEVICE_DUT) +os.environ.setdefault("REF", db.DEVICE_REF) + +from scheduler import Scheduler, Test as SchedulerTest +from test_config import build_bundle_test_configs, build_config_rows +from test_window import get_shift_capacity_for_date, get_shift_sequence, is_off_day from watcher import configure_result_watcher, stop_result_watcher @@ -21,6 +27,7 @@ DUT = os.getenv("DUT", "CGW453").strip() REF = os.getenv("REF", "CGW452").strip() + class LoadTestsRequest(BaseModel): csv_path: str = Field(..., description="Absolute or backend-relative path to target CSV") @@ -37,7 +44,7 @@ def _smb_credentials_from_settings(settings: dict[str, Any]) -> dict[str, str]: } -def _runtime_overrides_from_settings(settings: dict[str, Any]) -> dict[str, int]: +def _runtime_overrides_from_settings(settings: dict[str, Any]) -> dict[str, dict[str, int]]: def _parse_positive_int(value: Any) -> int | None: if value is None: return None @@ -51,15 +58,23 @@ def _runtime_overrides_from_settings(settings: dict[str, Any]) -> dict[str, int] return None return parsed if parsed > 0 else None - overrides: dict[str, int] = {} - for test_type, key in ( - ("P2P", "p2pRuntimeMinutes"), - ("COE", "coeRuntimeMinutes"), - ("P3P", "p3pRuntimeMinutes"), - ): - minutes = _parse_positive_int(settings.get(key)) - if minutes is not None: - overrides[test_type] = minutes + legacy_overrides = { + "P2P": _parse_positive_int(settings.get("p2pRuntimeMinutes")), + "COE": _parse_positive_int(settings.get("coeRuntimeMinutes")), + "P3P": _parse_positive_int(settings.get("p3pRuntimeMinutes")), + } + + overrides: dict[str, dict[str, int]] = {} + for device_key, field_prefix in ((db.DEVICE_DUT, "dut"), (db.DEVICE_REF, "ref")): + device_overrides: dict[str, int] = {} + for test_type, suffix in (("P2P", "P2p"), ("COE", "Coe"), ("P3P", "P3p")): + minutes = _parse_positive_int(settings.get(f"{field_prefix}{suffix}RuntimeMinutes")) + if minutes is None: + minutes = legacy_overrides[test_type] + if minutes is not None: + device_overrides[test_type] = minutes + if device_overrides: + overrides[device_key] = device_overrides return overrides @@ -68,6 +83,8 @@ class CompileScheduleRequest(BaseModel): start_date: str | None = Field(default=None, description="YYYY-MM-DD") rule: str = "" daytime_testing_today: bool = False + dual_device_weekend_start_enabled: bool = False + dual_device_weekend_start_dates: list[str] = Field(default_factory=list) top_priority_tests: list[str] = Field(default_factory=list) lowest_priority_tests: list[str] = Field(default_factory=list) @@ -80,10 +97,179 @@ class SaveHolidaysRequest(BaseModel): dates: list[str] = Field(default_factory=list) +SHIFT_LABELS = { + 1: "12AM–9AM", + 2: "9AM–5PM", + 3: "5PM–12AM", +} + +SHIFT_BOUNDARIES = { + 1: ((0, 0), (9, 0)), + 2: ((9, 0), (17, 0)), + 3: ((17, 0), (24, 0)), +} + + +def _serialize_schedule_row(row: db.ScheduleRow) -> dict[str, Any]: + return { + "test_id": row.test_id, + "device": row.device, + "scheduled_date": row.scheduled_date, + "shift_index": row.shift_index, + "sequence_in_shift": row.sequence_in_shift, + "test_type": row.test_type, + "rotation": row.rotation, + "config": row.config, + "status": row.status, + "priority": row.priority, + "estimated_minutes": row.estimated_minutes, + } + + +def _window_segment_key(day: date, shift_index: int) -> str: + return f"{day.isoformat()}::shift{shift_index}" + + +def _combine_date_time(day: date, hour: int, minute: int) -> str: + if hour == 24: + return datetime.combine(day + timedelta(days=1), datetime.min.time()).isoformat(timespec="minutes") + return datetime.combine(day, datetime.min.time()).replace(hour=hour, minute=minute).isoformat(timespec="minutes") + + +def _window_intersects_week(segments: list[tuple[date, int]], week_start: date, week_end: date) -> bool: + for day, _shift_index in segments: + if week_start <= day < week_end: + return True + return False + + +def _build_schedule_windows(week_start: date, all_rows: list[db.ScheduleRow], holiday_dates: set[str]) -> list[dict[str, Any]]: + if not all_rows: + return [] + + all_dates = [datetime.strptime(row.scheduled_date, "%Y-%m-%d").date() for row in all_rows] + first_scheduled_date = min(all_dates) + last_scheduled_date = max(all_dates) + week_end = week_start + timedelta(days=7) + + rows_by_segment: dict[tuple[str, int], list[dict[str, Any]]] = {} + weekday_shift2_dates: set[str] = set() + for row in all_rows: + serialized = _serialize_schedule_row(row) + rows_by_segment.setdefault((row.scheduled_date, row.shift_index), []).append(serialized) + row_date = datetime.strptime(row.scheduled_date, "%Y-%m-%d").date() + if row.shift_index == 2 and not is_off_day(row_date, holiday_dates): + weekday_shift2_dates.add(row.scheduled_date) + + simulation_start = min(first_scheduled_date, week_start) - timedelta(days=7) + while is_off_day(simulation_start, holiday_dates): + simulation_start -= timedelta(days=1) + + simulation_end = max(last_scheduled_date, week_end) + timedelta(days=7) + current_date = simulation_start + windows: list[dict[str, Any]] = [] + seen_window_ids: set[str] = set() + + while current_date <= simulation_end: + current_iso = current_date.isoformat() + + if current_iso in weekday_shift2_dates: + segments = get_shift_sequence(current_date, holiday_dates, daytime_shift2_only=True) + window_id = f"{current_iso}-shift2" + if window_id not in seen_window_ids and _window_intersects_week(segments, week_start, week_end): + seen_window_ids.add(window_id) + shift_tests = [ + test + for day, shift_index in segments + for test in rows_by_segment.get((day.isoformat(), shift_index), []) + ] + available_runtime_minutes = sum( + get_shift_capacity_for_date(day, holiday_dates, daytime_shift2_only=True).get(shift_index, 0) + for day, shift_index in segments + ) + start_hour, start_minute = SHIFT_BOUNDARIES[segments[0][1]][0] + end_hour, end_minute = SHIFT_BOUNDARIES[segments[-1][1]][1] + windows.append({ + "window_id": window_id, + "window_type": "daytime", + "start_date": segments[0][0].isoformat(), + "start_shift_index": segments[0][1], + "end_date": segments[-1][0].isoformat(), + "end_shift_index": segments[-1][1], + "start_at": _combine_date_time(segments[0][0], start_hour, start_minute), + "end_at": _combine_date_time(segments[-1][0], end_hour, end_minute), + "available_runtime_minutes": available_runtime_minutes, + "estimated_runtime_minutes": sum(test["estimated_minutes"] for test in shift_tests), + "bundle_test_configs": build_bundle_test_configs(shift_tests), + "segments": [ + { + "date": day.isoformat(), + "shift_index": shift_index, + "label": SHIFT_LABELS[shift_index], + "segment_key": _window_segment_key(day, shift_index), + } + for day, shift_index in segments + ], + "configRows": build_config_rows(shift_tests), + "tests": shift_tests, + }) + + segments = get_shift_sequence(current_date, holiday_dates, daytime_shift2_only=False) + last_sequence_date = segments[-1][0] + last_shift_index = segments[-1][1] + window_kind = "offday" if is_off_day(current_date, holiday_dates) else "overnight" + window_id = f"{current_iso}-shift{segments[0][1]}" + + if window_id not in seen_window_ids and _window_intersects_week(segments, week_start, week_end): + seen_window_ids.add(window_id) + window_tests = [ + test + for day, shift_index in segments + for test in rows_by_segment.get((day.isoformat(), shift_index), []) + ] + available_runtime_minutes = sum( + get_shift_capacity_for_date(day, holiday_dates, daytime_shift2_only=False).get(shift_index, 0) + for day, shift_index in segments + ) + start_hour, start_minute = SHIFT_BOUNDARIES[segments[0][1]][0] + end_hour, end_minute = SHIFT_BOUNDARIES[segments[-1][1]][1] + windows.append({ + "window_id": window_id, + "window_type": window_kind, + "start_date": segments[0][0].isoformat(), + "start_shift_index": segments[0][1], + "end_date": segments[-1][0].isoformat(), + "end_shift_index": segments[-1][1], + "start_at": _combine_date_time(segments[0][0], start_hour, start_minute), + "end_at": _combine_date_time(segments[-1][0], end_hour, end_minute), + "available_runtime_minutes": available_runtime_minutes, + "estimated_runtime_minutes": sum(test["estimated_minutes"] for test in window_tests), + "bundle_test_configs": build_bundle_test_configs(window_tests), + "segments": [ + { + "date": day.isoformat(), + "shift_index": shift_index, + "label": SHIFT_LABELS[shift_index], + "segment_key": _window_segment_key(day, shift_index), + } + for day, shift_index in segments + ], + "configRows": build_config_rows(window_tests), + "tests": window_tests, + }) + + if last_shift_index in {1, 2}: + current_date = last_sequence_date + else: + current_date = last_sequence_date + timedelta(days=1) + + windows.sort(key=lambda window: (window["start_at"], window["window_id"])) + return windows + + @asynccontextmanager async def lifespan(application: FastAPI): db.init_db(DB_PATH) - graph.reset_graph_state() settings = db.read_settings(DB_PATH) configure_result_watcher(settings) try: @@ -141,21 +327,17 @@ def load_tests(request: LoadTestsRequest) -> dict[str, Any]: raise HTTPException(status_code=400, detail=str(exc)) from exc count = db.upsert_tests(parsed.tests, DB_PATH) - reset_scheduler_state() - graph.reset_graph_state() - all_dut_tests = db.list_tests_for_device(DUT, DB_PATH) - graph.build_and_persist_graph(all_dut_tests, DB_PATH) + print(f"Loaded {count} tests from {csv_path}, with {len(parsed.warnings)} warnings.") return { "loaded_tests": count, "warnings": parsed.warnings, - "dut_graph_nodes": len(graph.get_graph()), } - @app.post("/api/schedule/active/remove") def remove_active_tests(request: RemoveActiveTestsRequest) -> dict[str, Any]: - remove_from_active({item.strip() for item in request.test_ids if item.strip()}) + # new_scheduler does not keep global mutable active state in app lifecycle. + # Keep endpoint for compatibility with frontend calls. return {"status": "ok", "removed": len(request.test_ids)} @@ -167,7 +349,6 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any] except ValueError as exc: raise HTTPException(status_code=400, detail="start_date must be YYYY-MM-DD") from exc - reset_scheduler_state() stored_tests = db.list_schedulable_tests(DB_PATH, rule=request.rule) if not stored_tests: version = db.create_schedule_version([], DB_PATH) @@ -189,29 +370,38 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any] config=t.config, throttled=t.throttled, estimated_minutes=t.estimated_minutes, - priority=t.priority, - raw_payload=t.raw_payload or {}, ) for t in stored_tests ] - # DB-backed graph retrieval ensures compile works after restart without manual save/load. - all_dut_tests = db.list_tests_for_device(DUT, DB_PATH) - graph.get_graph(DB_PATH, all_dut_tests) - holiday_dates = db.list_holidays(DB_PATH) + top_priority_pairs: set[tuple[str, str]] = set() + for test_id in {item.strip() for item in request.top_priority_tests if item.strip()}: + if any(t.test_id == test_id and t.device == DUT for t in stored_tests): + top_priority_pairs.add((test_id, DUT)) + if any(t.test_id == test_id and t.device == REF for t in stored_tests): + top_priority_pairs.add((test_id, REF)) + + scheduler = Scheduler( + tests=scheduler_tests, + top_priority_tests=top_priority_pairs, + start_date=request.start_date, + holiday_dates=holiday_dates, + daytime_testing_today=request.daytime_testing_today, + dual_device_weekend_start_enabled=request.dual_device_weekend_start_enabled, + dual_device_window_start_dates=set(item.strip() for item in request.dual_device_weekend_start_dates if item.strip()), + ) + try: - entries, completion_date = compile_schedule( - tests=scheduler_tests, - start_date=request.start_date, - holiday_dates=holiday_dates, - top_priority_tests={item.strip() for item in request.top_priority_tests if item.strip()}, - lowest_priority_tests={item.strip() for item in request.lowest_priority_tests if item.strip()}, - daytime_testing_today=request.daytime_testing_today, - ) - except RuntimeError as exc: + schedule_error = scheduler.compile_schedule() + entries = scheduler.get_schedule() + except (RuntimeError, ValueError) as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc + completion_date = max((e.scheduled_date for e in entries), default=None) + if schedule_error: + raise HTTPException(status_code=409, detail=schedule_error) + version = db.create_schedule_version( [(e.test_id, e.device, e.scheduled_date, e.shift_index, e.sequence_in_shift) for e in entries], DB_PATH, @@ -252,25 +442,16 @@ def get_schedule_week(start: str | None = None) -> dict[str, Any]: except ValueError as exc: raise HTTPException(status_code=400, detail="start must be YYYY-MM-DD") from exc + week_start_date = datetime.strptime(week_start, "%Y-%m-%d").date() rows = db.get_schedule_week(week_start, DB_PATH) + all_rows = db.get_schedule_rows_for_latest_version(DB_PATH) + completion_date = max((row.scheduled_date for row in all_rows), default=None) + holiday_dates = db.list_holidays(DB_PATH) return { "start_date": week_start, - "items": [ - { - "test_id": row.test_id, - "device": row.device, - "scheduled_date": row.scheduled_date, - "shift_index": row.shift_index, - "sequence_in_shift": row.sequence_in_shift, - "test_type": row.test_type, - "rotation": row.rotation, - "config": row.config, - "status": row.status, - "priority": row.priority, - "estimated_minutes": row.estimated_minutes, - } - for row in rows - ], + "items": [_serialize_schedule_row(row) for row in rows], + "completion_date": completion_date, + "windows": _build_schedule_windows(week_start_date, all_rows, holiday_dates), } if __name__ == "__main__": diff --git a/backend/data/p2p_coe_tests.csv b/backend/data/CGW453_P2P_COE_Tests.csv similarity index 83% rename from backend/data/p2p_coe_tests.csv rename to backend/data/CGW453_P2P_COE_Tests.csv index 392590b..908f389 100644 --- a/backend/data/p2p_coe_tests.csv +++ b/backend/data/CGW453_P2P_COE_Tests.csv @@ -1,251 +1,251 @@ -Priority,Index,Interferer,COE Pair ,Rotation,TC ID,Victim Band,6GHz Power Mode,5G Test Point,5G Channel,5G Bandwidth,5G RSSI,5G Direction,5G STA,6G Test Point,6G Channel,6G Bandwidth,6G RSSI,6G Direction,6G STA,2G Test Point,2G Channel,2G Bandwidth,2G RSSI,2G Direction,2G STA -P2,3,Yes,Yes,R2,COERXAC001,5G,LPI,T1F,100,80,-76,UL,STA4,T2A,5,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56 -P2,6,Yes,Yes,R2,COERXAC002,5G,LPI,T1F,161,80,-76,UL,STA4,T2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56 -P2,15,Yes,Yes,R1,COERXAC003,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,5,160,OFF,DL,STA63,T3E,1,20,-70,UL,STA4 -P2,17,Yes,Yes,R1,COERXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,133,160,OFF,DL,STA63,T3E,6,20,-70,UL,STA4 -P2,19,Yes,Yes,R1,COERXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,197,160,OFF,DL,STA63,T3E,11,20,-70,UL,STA4 -P2,3,Yes,Yes,R1,COERXAX001,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,4,Yes,Yes,R1,COERXAX002,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,6,Yes,Yes,R1,COERXAX003,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,7,Yes,Yes,R1,COERXAX004,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,8,Yes,Yes,R1,COERXAX005,5G,SP,T1F,161,80,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,9,Yes,Yes,R1,COERXAX006,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,11,Yes,Yes,R1,COERXAX007,5G,SP,T1F,100,160,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,12,Yes,Yes,R1,COERXAX008,5G,LPI,T1F,100,160,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,14,Yes,Yes,R2,COERXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4 -P2,16,Yes,Yes,R2,COERXAX010,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4 -P2,18,Yes,Yes,R2,COERXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4 -P2,20,Yes,Yes,R2,COERXAX012,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4 -P2,22,Yes,Yes,R2,COERXAX013,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4 -P2,24,Yes,Yes,R3,COERXAX014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,UL,STA56 -P2,26,Yes,Yes,R3,COERXAX015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,UL,STA56 -P2,28,Yes,Yes,R3,COERXAX016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,UL,STA56 +Priority,Index,Interferer,COE Pair ,Rotation,TC ID,Victim Band,6GHz Power Mode,5G_Test Point,5G_Channel,5G_Bandwidth,5G_RSSI,5G_Direction,5G_STA,6G_Test Point,6G_Channel,6G_Bandwidth,6G_RSSI,6G_Direction,6G_STA,2G_Test Point,2G_Channel,2G_Bandwidth,2G_RSSI,2G_Direction,2G_STA +P1,1,No,No,R3,P2PRXBE001,5G,LPI,T1F,36,80,-76,UL,STA63,,,,,,,,,,,, +P1,1,No,No,R3,P2PRXBE002,5G,LPI,T1B,36,80,-67,UL,STA63,,,,,,,,,,,, +P1,1,No,No,R3,P2PRXBE003,5G,LPI,T1C,36,80,-45,UL,STA63,,,,,,,,,,,, +P1,2,No,Yes,R3,P2PRXBE004,5G,LPI,T1F,100,80,-76,UL,STA63,,,,,,,,,,,, +P1,2,No,No,R3,P2PRXBE005,5G,LPI,T1B,100,80,-67,UL,STA63,,,,,,,,,,,, +P1,2,No,No,R3,P2PRXBE006,5G,LPI,T1C,100,80,-45,UL,STA63,,,,,,,,,,,, P2,3,Yes,Yes,R3,COERXBE001,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 P2,4,Yes,Yes,R3,COERXBE002,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 +P1,5,No,Yes,R3,P2PRXBE007,5G,LPI,T1F,161,80,-76,UL,STA63,,,,,,,,,,,, +P1,5,No,No,R3,P2PRXBE008,5G,LPI,T1B,161,80,-67,UL,STA63,,,,,,,,,,,, +P1,5,No,No,R3,P2PRXBE009,5G,LPI,T1C,161,80,-45,UL,STA63,,,,,,,,,,,, P2,6,Yes,Yes,R3,COERXBE003,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 P2,7,Yes,Yes,R3,COERXBE004,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 P2,8,Yes,Yes,R3,COERXBE005,5G,SP,T1F,161,80,-76,UL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4 P2,9,Yes,Yes,R3,COERXBE006,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4 +P1,10,No,Yes,R3,P2PRXBE010,5G,LPI,T1F,100,160,-76,UL,STA63,,,,,,,,,,,, +P1,10,No,No,R3,P2PRXBE011,5G,LPI,T1B,100,160,-67,UL,STA63,,,,,,,,,,,, +P1,10,No,No,R3,P2PRXBE012,5G,LPI,T1C,100,160,-45,UL,STA63,,,,,,,,,,,, P2,11,Yes,Yes,R3,COERXBE007,5G,SP,T1F,100,160,-76,UL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4 P2,12,Yes,Yes,R3,COERXBE008,5G,LPI,T1F,100,160,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4 -P2,14,Yes,Yes,R1,COERXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4 -P2,16,Yes,Yes,R1,COERXBE010,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4 -P2,18,Yes,Yes,R1,COERXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4 -P2,20,Yes,Yes,R1,COERXBE012,6G,SP,T1F,161,80,-76,DL,STA56,T2A,5,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4 -P2,22,Yes,Yes,R1,COERXBE013,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4 -P2,24,Yes,Yes,R2,COERXBE014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA56,T3E,1,20,-70,UL,STA63 -P2,26,Yes,Yes,R2,COERXBE015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,UL,STA63 -P2,28,Yes,Yes,R2,COERXBE016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA56,T3E,11,20,-70,UL,STA63 -P2,3,Yes,Yes,R2,COETXAC001,5G,LPI,T1F,100,80,-76,DL,STA4,T1K2A,5,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56 -P2,6,Yes,Yes,R2,COETXAC002,5G,LPI,T1F,161,80,-76,DL,STA4,T1K2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56 -P2,15,Yes,Yes,R1,COETXAC003,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,5,160,OFF,DL,STA63,T3E,1,20,-70,DL,STA4 -P2,17,Yes,Yes,R1,COETXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,19,Yes,Yes,R1,COETXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,5,320,OFF,DL,STA63,T3E,11,20,-70,DL,STA4 -P2,3,Yes,Yes,R1,COETXAX001,5G,LPI,T1F,100,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,4,Yes,Yes,R1,COETXAX002,5G,LPI,T1F,100,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,6,Yes,Yes,R1,COETXAX003,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,7,Yes,Yes,R1,COETXAX004,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,9,Yes,Yes,R1,COETXAX005,5G,SP,T1F,100,160,-76,DL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,10,Yes,Yes,R1,COETXAX006,5G,LPI,T1F,100,160,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,12,Yes,Yes,R2,COETXAX007,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,14,Yes,Yes,R2,COETXAX008,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,16,Yes,Yes,R2,COETXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,18,Yes,Yes,R2,COETXAX010,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,20,Yes,Yes,R2,COETXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,22,Yes,Yes,R3,COETXAX012,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,DL,STA56 -P2,24,Yes,Yes,R3,COETXAX013,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA56 -P2,26,Yes,Yes,R3,COETXAX014,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,DL,STA56 -P2,3,Yes,Yes,R3,COETXBE001,5G,LPI,T1F,100,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,4,Yes,Yes,R3,COETXBE002,5G,LPI,T1F,100,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,6,Yes,Yes,R3,COETXBE003,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,7,Yes,Yes,R3,COETXBE004,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 -P2,9,Yes,Yes,R3,COETXBE005,5G,SP,T1F,100,160,-76,DL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4 -P2,10,Yes,Yes,R3,COETXBE006,5G,LPI,T1F,100,160,-76,DL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4 -P2,12,Yes,Yes,R1,COETXBE007,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,14,Yes,Yes,R1,COETXBE008,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,16,Yes,Yes,R1,COETXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,18,Yes,Yes,R1,COETXBE010,6G,SP,T1F,161,80,-76,DL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,20,Yes,Yes,R1,COETXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 -P2,22,Yes,Yes,R2,COETXBE012,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA56,T3E,1,20,-70,DL,STA63 -P2,24,Yes,Yes,R2,COETXBE013,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA63 -P2,26,Yes,Yes,R2,COETXBE014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA56,T3E,11,20,-70,DL,STA63 -P1,1,No,No,R2,P2PRXAC001,5G,LPI,T1F,36,80,-76,UL,STA4,,,,,,,,,,,, -P1,1,No,No,R2,P2PRXAC002,5G,LPI,T2B,36,80,-67,UL,STA4,,,,,,,,,,,, -P1,1,No,No,R2,P2PRXAC003,5G,LPI,T1C,36,80,-45,UL,STA4,,,,,,,,,,,, -P1,2,No,Yes,R2,P2PRXAC004,5G,LPI,T1F,100,80,-76,UL,STA4,,,,,,,,,,,, -P1,2,No,No,R2,P2PRXAC005,5G,LPI,T2B,100,80,-67,UL,STA4,,,,,,,,,,,, -P1,2,No,No,R2,P2PRXAC006,5G,LPI,T1C,100,80,-45,UL,STA4,,,,,,,,,,,, -P1,5,No,Yes,R2,P2PRXAC007,5G,LPI,T1F,161,80,-76,UL,STA4,,,,,,,,,,,, -P1,5,No,No,R2,P2PRXAC008,5G,LPI,T2B,161,80,-67,UL,STA4,,,,,,,,,,,, -P1,5,No,No,R2,P2PRXAC009,5G,LPI,T1C,161,80,-45,UL,STA4,,,,,,,,,,,, -P1,14,No,Yes,R1,P2PRXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA4 -P1,14,No,No,R1,P2PRXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA4 -P1,14,No,No,R1,P2PRXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA4 -P1,16,No,Yes,R1,P2PRXAC013,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA4 -P1,16,No,No,R1,P2PRXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA4 -P1,16,No,No,R1,P2PRXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA4 -P1,18,No,Yes,R1,P2PRXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA4 -P1,18,No,No,R1,P2PRXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA4 -P1,18,No,No,R1,P2PRXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA4 -P1,1,No,No,R1,P2PRXAX001,5G,LPI,T1F,36,80,-76,UL,STA56,,,,,,,,,,,, -P1,1,No,No,R1,P2PRXAX002,5G,LPI,T2B,36,80,-67,UL,STA56,,,,,,,,,,,, -P1,1,No,No,R1,P2PRXAX003,5G,LPI,T1C,36,80,-45,UL,STA56,,,,,,,,,,,, -P1,2,No,Yes,R1,P2PRXAX004,5G,LPI,T1F,100,80,-76,UL,STA56,,,,,,,,,,,, -P1,2,No,No,R1,P2PRXAX005,5G,LPI,T2B,100,80,-67,UL,STA56,,,,,,,,,,,, -P1,2,No,No,R1,P2PRXAX006,5G,LPI,T1C,100,80,-45,UL,STA56,,,,,,,,,,,, -P1,5,No,Yes,R1,P2PRXAX007,5G,LPI,T1F,161,80,-76,UL,STA56,,,,,,,,,,,, -P1,5,No,No,R1,P2PRXAX008,5G,LPI,T2B,161,80,-67,UL,STA56,,,,,,,,,,,, -P1,5,No,No,R1,P2PRXAX009,5G,LPI,T1C,161,80,-45,UL,STA56,,,,,,,,,,,, -P1,10,No,Yes,R1,P2PRXAX010,5G,LPI,T1F,100,160,-76,UL,STA56,,,,,,,,,,,, -P1,10,No,No,R1,P2PRXAX011,5G,LPI,T2B,100,160,-67,UL,STA56,,,,,,,,,,,, -P1,10,No,No,R1,P2PRXAX012,5G,LPI,T1C,100,160,-45,UL,STA56,,,,,,,,,,,, -P1,13,No,Yes,R2,P2PRXAX013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA56,,,,,, -P1,13,No,No,R2,P2PRXAX014,6G,LPI,,,,,,,T2B,5,160,-70,UL,STA56,,,,,, -P1,13,No,No,R2,P2PRXAX015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA56,,,,,, -P1,15,No,Yes,R2,P2PRXAX016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA56,,,,,, -P1,15,No,No,R2,P2PRXAX017,6G,LPI,,,,,,,T2B,133,160,-70,UL,STA56,,,,,, -P1,15,No,No,R2,P2PRXAX018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,, -P1,17,No,Yes,R2,P2PRXAX019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA56,,,,,, -P1,17,No,No,R2,P2PRXAX020,6G,LPI,,,,,,,T2B,197,160,-70,UL,STA56,,,,,, -P1,17,No,No,R2,P2PRXAX021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA56,,,,,, -P1,19,No,Yes,R2,P2PRXAX022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA56,,,,,, -P1,19,No,No,R2,P2PRXAX023,6G,SP,,,,,,,T2B,5,320,-70,UL,STA56,,,,,, -P1,19,No,No,R2,P2PRXAX024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA56,,,,,, -P1,21,No,Yes,R2,P2PRXAX025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA56,,,,,, -P1,21,No,No,R2,P2PRXAX026,6G,LPI,,,,,,,T2B,133,320,-70,UL,STA56,,,,,, -P1,21,No,No,R2,P2PRXAX027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA56,,,,,, -P1,23,No,Yes,R3,P2PRXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56 -P1,23,No,No,R3,P2PRXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56 -P1,23,No,No,R3,P2PRXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56 -P1,25,No,Yes,R3,P2PRXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56 -P1,25,No,No,R3,P2PRXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56 -P1,25,No,No,R3,P2PRXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56 -P1,27,No,Yes,R3,P2PRXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56 -P1,27,No,No,R3,P2PRXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56 -P1,27,No,No,R3,P2PRXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56 -P1,1,No,No,R3,P2PRXBE001,5G,LPI,T1F,36,80,-76,UL,STA63,,,,,,,,,,,, -P1,1,No,No,R3,P2PRXBE002,5G,LPI,T2B,36,80,-67,UL,STA63,,,,,,,,,,,, -P1,1,No,No,R3,P2PRXBE003,5G,LPI,T1C,36,80,-45,UL,STA63,,,,,,,,,,,, -P1,2,No,Yes,R3,P2PRXBE004,5G,LPI,T1F,100,80,-76,UL,STA63,,,,,,,,,,,, -P1,2,No,No,R3,P2PRXBE005,5G,LPI,T2B,100,80,-67,UL,STA63,,,,,,,,,,,, -P1,2,No,No,R3,P2PRXBE006,5G,LPI,T1C,100,80,-45,UL,STA63,,,,,,,,,,,, -P1,5,No,Yes,R3,P2PRXBE007,5G,LPI,T1F,161,80,-76,UL,STA63,,,,,,,,,,,, -P1,5,No,No,R3,P2PRXBE008,5G,LPI,T2B,161,80,-67,UL,STA63,,,,,,,,,,,, -P1,5,No,No,R3,P2PRXBE009,5G,LPI,T1C,161,80,-45,UL,STA63,,,,,,,,,,,, -P1,10,No,Yes,R3,P2PRXBE010,5G,LPI,T1F,100,160,-76,UL,STA63,,,,,,,,,,,, -P1,10,No,No,R3,P2PRXBE011,5G,LPI,T2B,100,160,-67,UL,STA63,,,,,,,,,,,, -P1,10,No,No,R3,P2PRXBE012,5G,LPI,T1C,100,160,-45,UL,STA63,,,,,,,,,,,, P1,13,No,Yes,R1,P2PRXBE013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA63,,,,,, -P1,13,No,No,R1,P2PRXBE014,6G,LPI,,,,,,,T2B,5,160,-70,UL,STA63,,,,,, +P1,13,No,No,R1,P2PRXBE014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA63,,,,,, P1,13,No,No,R1,P2PRXBE015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA63,,,,,, +P2,14,Yes,Yes,R1,COERXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4 P1,15,No,Yes,R1,P2PRXBE016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA63,,,,,, -P1,15,No,No,R1,P2PRXBE017,6G,LPI,,,,,,,T2B,133,160,-70,UL,STA63,,,,,, +P1,15,No,No,R1,P2PRXBE017,6G,LPI,,,,,,,T1B,133,160,-70,UL,STA63,,,,,, P1,15,No,No,R1,P2PRXBE018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,, +P2,16,Yes,Yes,R1,COERXBE010,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4 P1,17,No,Yes,R1,P2PRXBE019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA63,,,,,, -P1,17,No,No,R1,P2PRXBE020,6G,LPI,,,,,,,T2B,197,160,-70,UL,STA63,,,,,, +P1,17,No,No,R1,P2PRXBE020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA63,,,,,, P1,17,No,No,R1,P2PRXBE021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA63,,,,,, +P2,18,Yes,Yes,R1,COERXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4 P1,19,No,Yes,R1,P2PRXBE022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA63,,,,,, -P1,19,No,No,R1,P2PRXBE023,6G,SP,,,,,,,T2B,5,320,-70,UL,STA63,,,,,, +P1,19,No,No,R1,P2PRXBE023,6G,SP,,,,,,,T1B,5,320,-70,UL,STA63,,,,,, P1,19,No,No,R1,P2PRXBE024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA63,,,,,, +P2,20,Yes,Yes,R1,COERXBE012,6G,SP,T1F,161,80,-76,DL,STA56,T2A,5,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4 P1,21,No,Yes,R1,P2PRXBE025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA63,,,,,, -P1,21,No,No,R1,P2PRXBE026,6G,LPI,,,,,,,T2B,133,320,-70,UL,STA63,,,,,, +P1,21,No,No,R1,P2PRXBE026,6G,LPI,,,,,,,T1B,133,320,-70,UL,STA63,,,,,, P1,21,No,No,R1,P2PRXBE027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA63,,,,,, +P2,22,Yes,Yes,R1,COERXBE013,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4 P1,23,No,Yes,R2,P2PRXBE028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA63 P1,23,No,No,R2,P2PRXBE029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA63 P1,23,No,No,R2,P2PRXBE030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA63 +P2,24,Yes,Yes,R2,COERXBE014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA56,T3E,1,20,-70,UL,STA63 P1,25,No,Yes,R2,P2PRXBE031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA63 P1,25,No,No,R2,P2PRXBE032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA63 P1,25,No,No,R2,P2PRXBE033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA63 +P2,26,Yes,Yes,R2,COERXBE015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,UL,STA63 P1,27,No,Yes,R2,P2PRXBE034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA63 P1,27,No,No,R2,P2PRXBE035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA63 P1,27,No,No,R2,P2PRXBE036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA63 -P1,1,No,No,R2,P2PTXAC001,5G,LPI,T1F,36,80,-76,DL,STA4,,,,,,,,,,,, -P1,1,No,No,R2,P2PTXAC002,5G,LPI,T2B,36,80,-67,DL,STA4,,,,,,,,,,,, -P1,1,No,No,R2,P2PTXAC003,5G,LPI,T1C,36,80,-45,DL,STA4,,,,,,,,,,,, -P1,2,No,Yes,R2,P2PTXAC004,5G,LPI,T1F,100,80,-76,DL,STA4,,,,,,,,,,,, -P1,2,No,No,R2,P2PTXAC005,5G,LPI,T2B,100,80,-67,DL,STA4,,,,,,,,,,,, -P1,2,No,No,R2,P2PTXAC006,5G,LPI,T1C,100,80,-45,DL,STA4,,,,,,,,,,,, -P1,5,No,Yes,R2,P2PTXAC007,5G,LPI,T1F,161,80,-76,DL,STA4,,,,,,,,,,,, -P1,5,No,No,R2,P2PTXAC008,5G,LPI,T2B,161,80,-67,DL,STA4,,,,,,,,,,,, -P1,5,No,No,R2,P2PTXAC009,5G,LPI,T1C,161,80,-45,DL,STA4,,,,,,,,,,,, -P1,14,No,Yes,R1,P2PTXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,DL,STA4 -P1,14,No,No,R1,P2PTXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,DL,STA4 -P1,14,No,No,R1,P2PTXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,DL,STA4 -P1,16,No,Yes,R1,P2PTXAC013,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,DL,STA4 -P1,16,No,No,R1,P2PTXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,DL,STA4 -P1,16,No,No,R1,P2PTXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,DL,STA4 -P1,18,No,Yes,R1,P2PTXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,DL,STA4 -P1,18,No,No,R1,P2PTXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,DL,STA4 -P1,18,No,No,R1,P2PTXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,DL,STA4 -P1,1,No,No,R1,P2PTXAX001,5G,LPI,T1F,36,80,-76,DL,STA56,,,,,,,,,,,, -P1,1,No,No,R1,P2PTXAX002,5G,LPI,T2B,36,80,-67,DL,STA56,,,,,,,,,,,, -P1,1,No,No,R1,P2PTXAX003,5G,LPI,T1C,36,80,-45,DL,STA56,,,,,,,,,,,, -P1,2,No,Yes,R1,P2PTXAX004,5G,LPI,T1F,100,80,-76,DL,STA56,,,,,,,,,,,, -P1,2,No,No,R1,P2PTXAX005,5G,LPI,T2B,100,80,-67,DL,STA56,,,,,,,,,,,, -P1,2,No,No,R1,P2PTXAX006,5G,LPI,T1C,100,80,-45,DL,STA56,,,,,,,,,,,, -P1,5,No,Yes,R1,P2PTXAX007,5G,LPI,T1F,161,80,-76,DL,STA56,,,,,,,,,,,, -P1,5,No,No,R1,P2PTXAX008,5G,LPI,T2B,161,80,-67,DL,STA56,,,,,,,,,,,, -P1,5,No,No,R1,P2PTXAX009,5G,LPI,T1C,161,80,-45,DL,STA56,,,,,,,,,,,, -P1,8,No,Yes,R1,P2PTXAX010,5G,LPI,T1F,100,160,-76,DL,STA56,,,,,,,,,,,, -P1,8,No,No,R1,P2PTXAX011,5G,LPI,T2B,100,160,-67,DL,STA56,,,,,,,,,,,, -P1,8,No,No,R1,P2PTXAX012,5G,LPI,T1C,100,160,-45,DL,STA56,,,,,,,,,,,, -P1,11,No,Yes,R2,P2PTXAX013,6G,LPI,,,,,,,T2A,5,160,-79,DL,STA56,,,,,, -P1,11,No,No,R2,P2PTXAX014,6G,LPI,,,,,,,T2B,5,160,-70,DL,STA56,,,,,, -P1,11,No,No,R2,P2PTXAX015,6G,LPI,,,,,,,T1C,5,160,-45,DL,STA56,,,,,, -P1,13,No,Yes,R2,P2PTXAX016,6G,LPI,,,,,,,T2A,133,160,-79,DL,STA56,,,,,, -P1,13,No,No,R2,P2PTXAX017,6G,LPI,,,,,,,T2B,133,160,-70,DL,STA56,,,,,, -P1,13,No,No,R2,P2PTXAX018,6G,LPI,,,,,,,T1C,133,160,-45,DL,STA56,,,,,, -P1,15,No,Yes,R2,P2PTXAX019,6G,LPI,,,,,,,T2A,197,160,-79,DL,STA56,,,,,, -P1,15,No,No,R2,P2PTXAX020,6G,LPI,,,,,,,T2B,197,160,-70,DL,STA56,,,,,, -P1,15,No,No,R2,P2PTXAX021,6G,LPI,,,,,,,T1C,197,160,-45,DL,STA56,,,,,, -P1,17,No,Yes,R2,P2PTXAX022,6G,SP,,,,,,,T2A,5,320,-79,DL,STA56,,,,,, -P1,17,No,No,R2,P2PTXAX023,6G,SP,,,,,,,T2B,5,320,-70,DL,STA56,,,,,, -P1,17,No,No,R2,P2PTXAX024,6G,SP,,,,,,,T1C,5,320,-45,DL,STA56,,,,,, -P1,19,No,Yes,R2,P2PTXAX025,6G,LPI,,,,,,,T2A,133,320,-79,DL,STA56,,,,,, -P1,19,No,No,R2,P2PTXAX026,6G,LPI,,,,,,,T2B,133,320,-70,DL,STA56,,,,,, -P1,19,No,No,R2,P2PTXAX027,6G,LPI,,,,,,,T1C,133,320,-45,DL,STA56,,,,,, -P1,21,No,Yes,R3,P2PTXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56 -P1,21,No,No,R3,P2PTXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56 -P1,21,No,No,R3,P2PTXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56 -P1,23,No,Yes,R3,P2PTXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56 -P1,23,No,No,R3,P2PTXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56 -P1,23,No,No,R3,P2PTXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56 -P1,25,No,Yes,R3,P2PTXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56 -P1,25,No,No,R3,P2PTXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56 -P1,25,No,No,R3,P2PTXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56 +P2,28,Yes,Yes,R2,COERXBE016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA56,T3E,11,20,-70,UL,STA63 +P1,1,No,No,R1,P2PRXAX001,5G,LPI,T1F,36,80,-76,UL,STA56,,,,,,,,,,,, +P1,1,No,No,R1,P2PRXAX002,5G,LPI,T1B,36,80,-67,UL,STA56,,,,,,,,,,,, +P1,1,No,No,R1,P2PRXAX003,5G,LPI,T1C,36,80,-45,UL,STA56,,,,,,,,,,,, +P1,2,No,Yes,R1,P2PRXAX004,5G,LPI,T1F,100,80,-76,UL,STA56,,,,,,,,,,,, +P1,2,No,No,R1,P2PRXAX005,5G,LPI,T1B,100,80,-67,UL,STA56,,,,,,,,,,,, +P1,2,No,No,R1,P2PRXAX006,5G,LPI,T1C,100,80,-45,UL,STA56,,,,,,,,,,,, +P2,3,Yes,Yes,R1,COERXAX001,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P2,4,Yes,Yes,R1,COERXAX002,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P1,5,No,Yes,R1,P2PRXAX007,5G,LPI,T1F,161,80,-76,UL,STA56,,,,,,,,,,,, +P1,5,No,No,R1,P2PRXAX008,5G,LPI,T1B,161,80,-67,UL,STA56,,,,,,,,,,,, +P1,5,No,No,R1,P2PRXAX009,5G,LPI,T1C,161,80,-45,UL,STA56,,,,,,,,,,,, +P2,6,Yes,Yes,R1,COERXAX003,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P2,7,Yes,Yes,R1,COERXAX004,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P2,8,Yes,Yes,R1,COERXAX005,5G,SP,T1F,161,80,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P2,9,Yes,Yes,R1,COERXAX006,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P1,10,No,Yes,R1,P2PRXAX010,5G,LPI,T1F,100,160,-76,UL,STA56,,,,,,,,,,,, +P1,10,No,No,R1,P2PRXAX011,5G,LPI,T1B,100,160,-67,UL,STA56,,,,,,,,,,,, +P1,10,No,No,R1,P2PRXAX012,5G,LPI,T1C,100,160,-45,UL,STA56,,,,,,,,,,,, +P2,11,Yes,Yes,R1,COERXAX007,5G,SP,T1F,100,160,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P2,12,Yes,Yes,R1,COERXAX008,5G,LPI,T1F,100,160,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P1,13,No,Yes,R2,P2PRXAX013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA56,,,,,, +P1,13,No,No,R2,P2PRXAX014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA56,,,,,, +P1,13,No,No,R2,P2PRXAX015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA56,,,,,, +P2,14,Yes,Yes,R2,COERXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4 +P1,15,No,Yes,R2,P2PRXAX016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA56,,,,,, +P1,15,No,No,R2,P2PRXAX017,6G,LPI,,,,,,,T1B,133,160,-70,UL,STA56,,,,,, +P1,15,No,No,R2,P2PRXAX018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,, +P2,16,Yes,Yes,R2,COERXAX010,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4 +P1,17,No,Yes,R2,P2PRXAX019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA56,,,,,, +P1,17,No,No,R2,P2PRXAX020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA56,,,,,, +P1,17,No,No,R2,P2PRXAX021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA56,,,,,, +P2,18,Yes,Yes,R2,COERXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4 +P1,19,No,Yes,R2,P2PRXAX022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA56,,,,,, +P1,19,No,No,R2,P2PRXAX023,6G,SP,,,,,,,T1B,5,320,-70,UL,STA56,,,,,, +P1,19,No,No,R2,P2PRXAX024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA56,,,,,, +P2,20,Yes,Yes,R2,COERXAX012,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4 +P1,21,No,Yes,R2,P2PRXAX025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA56,,,,,, +P1,21,No,No,R2,P2PRXAX026,6G,LPI,,,,,,,T1B,133,320,-70,UL,STA56,,,,,, +P1,21,No,No,R2,P2PRXAX027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA56,,,,,, +P2,22,Yes,Yes,R2,COERXAX013,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4 +P1,23,No,Yes,R3,P2PRXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56 +P1,23,No,No,R3,P2PRXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56 +P1,23,No,No,R3,P2PRXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56 +P2,24,Yes,Yes,R3,COERXAX014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,UL,STA56 +P1,25,No,Yes,R3,P2PRXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56 +P1,25,No,No,R3,P2PRXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56 +P1,25,No,No,R3,P2PRXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56 +P2,26,Yes,Yes,R3,COERXAX015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,UL,STA56 +P1,27,No,Yes,R3,P2PRXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56 +P1,27,No,No,R3,P2PRXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56 +P1,27,No,No,R3,P2PRXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56 +P2,28,Yes,Yes,R3,COERXAX016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,UL,STA56 +P1,1,No,No,R2,P2PRXAC001,5G,LPI,T1F,36,80,-76,UL,STA4,,,,,,,,,,,, +P1,1,No,No,R2,P2PRXAC002,5G,LPI,T1B,36,80,-67,UL,STA4,,,,,,,,,,,, +P1,1,No,No,R2,P2PRXAC003,5G,LPI,T1C,36,80,-45,UL,STA4,,,,,,,,,,,, +P1,2,No,Yes,R2,P2PRXAC004,5G,LPI,T1F,100,80,-76,UL,STA4,,,,,,,,,,,, +P1,2,No,No,R2,P2PRXAC005,5G,LPI,T1B,100,80,-67,UL,STA4,,,,,,,,,,,, +P1,2,No,No,R2,P2PRXAC006,5G,LPI,T1C,100,80,-45,UL,STA4,,,,,,,,,,,, +P2,3,Yes,Yes,R2,COERXAC001,5G,LPI,T1F,100,80,-76,UL,STA4,T2A,5,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56 +P1,5,No,Yes,R2,P2PRXAC007,5G,LPI,T1F,161,80,-76,UL,STA4,,,,,,,,,,,, +P1,5,No,No,R2,P2PRXAC008,5G,LPI,T1B,161,80,-67,UL,STA4,,,,,,,,,,,, +P1,5,No,No,R2,P2PRXAC009,5G,LPI,T1C,161,80,-45,UL,STA4,,,,,,,,,,,, +P2,6,Yes,Yes,R2,COERXAC002,5G,LPI,T1F,161,80,-76,UL,STA4,T2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56 +P1,14,No,Yes,R1,P2PRXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA4 +P1,14,No,No,R1,P2PRXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA4 +P1,14,No,No,R1,P2PRXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA4 +P2,15,Yes,Yes,R1,COERXAC003,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,5,160,OFF,DL,STA63,T3E,1,20,-70,UL,STA4 +P1,16,No,Yes,R1,P2PRXAC013,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA4 +P1,16,No,No,R1,P2PRXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA4 +P1,16,No,No,R1,P2PRXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA4 +P2,17,Yes,Yes,R1,COERXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,133,160,OFF,DL,STA63,T3E,6,20,-70,UL,STA4 +P1,18,No,Yes,R1,P2PRXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA4 +P1,18,No,No,R1,P2PRXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA4 +P1,18,No,No,R1,P2PRXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA4 +P2,19,Yes,Yes,R1,COERXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,197,160,OFF,DL,STA63,T3E,11,20,-70,UL,STA4 P1,1,No,No,R3,P2PTXBE001,5G,LPI,T1F,36,80,-76,DL,STA63,,,,,,,,,,,, -P1,1,No,No,R3,P2PTXBE002,5G,LPI,T2B,36,80,-67,DL,STA63,,,,,,,,,,,, +P1,1,No,No,R3,P2PTXBE002,5G,LPI,T1B,36,80,-67,DL,STA63,,,,,,,,,,,, P1,1,No,No,R3,P2PTXBE003,5G,LPI,T1C,36,80,-45,DL,STA63,,,,,,,,,,,, P1,2,No,Yes,R3,P2PTXBE004,5G,LPI,T1F,100,80,-76,DL,STA63,,,,,,,,,,,, -P1,2,No,No,R3,P2PTXBE005,5G,LPI,T2B,100,80,-67,DL,STA63,,,,,,,,,,,, +P1,2,No,No,R3,P2PTXBE005,5G,LPI,T1B,100,80,-67,DL,STA63,,,,,,,,,,,, P1,2,No,No,R3,P2PTXBE006,5G,LPI,T1C,100,80,-45,DL,STA63,,,,,,,,,,,, +P2,3,Yes,Yes,R3,COETXBE001,5G,LPI,T1F,100,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 +P2,4,Yes,Yes,R3,COETXBE002,5G,LPI,T1F,100,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 P1,5,No,Yes,R3,P2PTXBE007,5G,LPI,T1F,161,80,-76,DL,STA63,,,,,,,,,,,, -P1,5,No,No,R3,P2PTXBE008,5G,LPI,T2B,161,80,-67,DL,STA63,,,,,,,,,,,, +P1,5,No,No,R3,P2PTXBE008,5G,LPI,T1B,161,80,-67,DL,STA63,,,,,,,,,,,, P1,5,No,No,R3,P2PTXBE009,5G,LPI,T1C,161,80,-45,DL,STA63,,,,,,,,,,,, +P2,6,Yes,Yes,R3,COETXBE003,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 +P2,7,Yes,Yes,R3,COETXBE004,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 P1,8,No,Yes,R3,P2PTXBE010,5G,LPI,T1F,100,160,-76,DL,STA63,,,,,,,,,,,, -P1,8,No,No,R3,P2PTXBE011,5G,LPI,T2B,100,160,-67,DL,STA63,,,,,,,,,,,, +P1,8,No,No,R3,P2PTXBE011,5G,LPI,T1B,100,160,-67,DL,STA63,,,,,,,,,,,, P1,8,No,No,R3,P2PTXBE012,5G,LPI,T1C,100,160,-45,DL,STA63,,,,,,,,,,,, -P1,11,No,Yes,R3,P2PTXBE013,6G,LPI,T1F,5,160,-79,DL,STA63,,,,,,,,,,,, -P1,11,No,No,R1,P2PTXBE014,6G,LPI,T2B,5,160,-70,DL,STA63,,,,,,,,,,,, +P2,9,Yes,Yes,R3,COETXBE005,5G,SP,T1F,100,160,-76,DL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4 +P2,10,Yes,Yes,R3,COETXBE006,5G,LPI,T1F,100,160,-76,DL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4 +P1,11,No,Yes,R1,P2PTXBE013,6G,LPI,T1F,5,160,-79,DL,STA63,,,,,,,,,,,, +P1,11,No,No,R1,P2PTXBE014,6G,LPI,T1B,5,160,-70,DL,STA63,,,,,,,,,,,, P1,11,No,No,R1,P2PTXBE015,6G,LPI,T1C,5,160,-45,DL,STA63,,,,,,,,,,,, +P2,12,Yes,Yes,R1,COETXBE007,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 P1,13,No,Yes,R1,P2PTXBE016,6G,LPI,T1F,133,160,-79,DL,STA63,,,,,,,,,,,, -P1,13,No,No,R1,P2PTXBE017,6G,LPI,T2B,133,160,-70,DL,STA63,,,,,,,,,,,, +P1,13,No,No,R1,P2PTXBE017,6G,LPI,T1B,133,160,-70,DL,STA63,,,,,,,,,,,, P1,13,No,No,R1,P2PTXBE018,6G,LPI,T1C,133,160,-45,DL,STA63,,,,,,,,,,,, +P2,14,Yes,Yes,R1,COETXBE008,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 P1,15,No,Yes,R1,P2PTXBE019,6G,LPI,T1F,197,160,-79,DL,STA63,,,,,,,,,,,, -P1,15,No,No,R1,P2PTXBE020,6G,LPI,T2B,197,160,-70,DL,STA63,,,,,,,,,,,, +P1,15,No,No,R1,P2PTXBE020,6G,LPI,T1B,197,160,-70,DL,STA63,,,,,,,,,,,, P1,15,No,No,R1,P2PTXBE021,6G,LPI,T1C,197,160,-45,DL,STA63,,,,,,,,,,,, +P2,16,Yes,Yes,R1,COETXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 P1,17,No,Yes,R1,P2PTXBE022,6G,SP,T1F,5,320,-79,DL,STA63,,,,,,,,,,,, -P1,17,No,No,R1,P2PTXBE023,6G,SP,T2B,5,320,-70,DL,STA63,,,,,,,,,,,, +P1,17,No,No,R1,P2PTXBE023,6G,SP,T1B,5,320,-70,DL,STA63,,,,,,,,,,,, P1,17,No,No,R1,P2PTXBE024,6G,SP,T1C,5,320,-45,DL,STA63,,,,,,,,,,,, +P2,18,Yes,Yes,R1,COETXBE010,6G,SP,T1F,161,80,-76,DL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 P1,19,No,Yes,R1,P2PTXBE025,6G,LPI,T1F,133,320,-79,DL,STA63,,,,,,,,,,,, -P1,19,No,No,R1,P2PTXBE026,6G,LPI,T2B,133,320,-70,DL,STA63,,,,,,,,,,,, +P1,19,No,No,R1,P2PTXBE026,6G,LPI,T1B,133,320,-70,DL,STA63,,,,,,,,,,,, P1,19,No,No,R1,P2PTXBE027,6G,LPI,T1C,133,320,-45,DL,STA63,,,,,,,,,,,, +P2,20,Yes,Yes,R1,COETXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 P1,21,No,Yes,R2,P2PTXBE028,2G,LPI,T3E,1,20,-70,UL,STA63,,,,,,,,,,,, P1,21,No,No,R2,P2PTXBE029,2G,LPI,T1D,1,20,-60,UL,STA63,,,,,,,,,,,, P1,21,No,No,R2,P2PTXBE030,2G,LPI,T1C,1,20,-45,UL,STA63,,,,,,,,,,,, +P2,22,Yes,Yes,R2,COETXBE012,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA56,T3E,1,20,-70,DL,STA63 P1,23,No,Yes,R2,P2PTXBE031,2G,LPI,T3E,6,20,-70,UL,STA63,,,,,,,,,,,, P1,23,No,No,R2,P2PTXBE032,2G,LPI,T1D,6,20,-60,UL,STA63,,,,,,,,,,,, P1,23,No,No,R2,P2PTXBE033,2G,LPI,T1C,6,20,-45,UL,STA63,,,,,,,,,,,, +P2,24,Yes,Yes,R2,COETXBE013,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA63 P1,25,No,Yes,R2,P2PTXBE034,2G,LPI,T3E,11,20,-70,UL,STA63,,,,,,,,,,,, P1,25,No,No,R2,P2PTXBE035,2G,LPI,T1D,11,20,-60,UL,STA63,,,,,,,,,,,, P1,25,No,No,R2,P2PTXBE036,2G,LPI,T1C,11,20,-45,UL,STA63,,,,,,,,,,,, +P2,26,Yes,Yes,R2,COETXBE014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA56,T3E,11,20,-70,DL,STA63 +P1,1,No,No,R1,P2PTXAX001,5G,LPI,T1F,36,80,-76,DL,STA56,,,,,,,,,,,, +P1,1,No,No,R1,P2PTXAX002,5G,LPI,T1B,36,80,-67,DL,STA56,,,,,,,,,,,, +P1,1,No,No,R1,P2PTXAX003,5G,LPI,T1C,36,80,-45,DL,STA56,,,,,,,,,,,, +P1,2,No,Yes,R1,P2PTXAX004,5G,LPI,T1F,100,80,-76,DL,STA56,,,,,,,,,,,, +P1,2,No,No,R1,P2PTXAX005,5G,LPI,T1B,100,80,-67,DL,STA56,,,,,,,,,,,, +P1,2,No,No,R1,P2PTXAX006,5G,LPI,T1C,100,80,-45,DL,STA56,,,,,,,,,,,, +P2,3,Yes,Yes,R1,COETXAX001,5G,LPI,T1F,100,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P2,4,Yes,Yes,R1,COETXAX002,5G,LPI,T1F,100,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P1,5,No,Yes,R1,P2PTXAX007,5G,LPI,T1F,161,80,-76,DL,STA56,,,,,,,,,,,, +P1,5,No,No,R1,P2PTXAX008,5G,LPI,T1B,161,80,-67,DL,STA56,,,,,,,,,,,, +P1,5,No,No,R1,P2PTXAX009,5G,LPI,T1C,161,80,-45,DL,STA56,,,,,,,,,,,, +P2,6,Yes,Yes,R1,COETXAX003,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P2,7,Yes,Yes,R1,COETXAX004,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P1,8,No,Yes,R1,P2PTXAX010,5G,LPI,T1F,100,160,-76,DL,STA56,,,,,,,,,,,, +P1,8,No,No,R1,P2PTXAX011,5G,LPI,T1B,100,160,-67,DL,STA56,,,,,,,,,,,, +P1,8,No,No,R1,P2PTXAX012,5G,LPI,T1C,100,160,-45,DL,STA56,,,,,,,,,,,, +P2,9,Yes,Yes,R1,COETXAX005,5G,SP,T1F,100,160,-76,DL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P2,10,Yes,Yes,R1,COETXAX006,5G,LPI,T1F,100,160,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4 +P1,11,No,Yes,R2,P2PTXAX013,6G,LPI,,,,,,,T2A,5,160,-79,DL,STA56,,,,,, +P1,11,No,No,R2,P2PTXAX014,6G,LPI,,,,,,,T1B,5,160,-70,DL,STA56,,,,,, +P1,11,No,No,R2,P2PTXAX015,6G,LPI,,,,,,,T1C,5,160,-45,DL,STA56,,,,,, +P2,12,Yes,Yes,R2,COETXAX007,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 +P1,13,No,Yes,R2,P2PTXAX016,6G,LPI,,,,,,,T2A,133,160,-79,DL,STA56,,,,,, +P1,13,No,No,R2,P2PTXAX017,6G,LPI,,,,,,,T1B,133,160,-70,DL,STA56,,,,,, +P1,13,No,No,R2,P2PTXAX018,6G,LPI,,,,,,,T1C,133,160,-45,DL,STA56,,,,,, +P2,14,Yes,Yes,R2,COETXAX008,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 +P1,15,No,Yes,R2,P2PTXAX019,6G,LPI,,,,,,,T2A,197,160,-79,DL,STA56,,,,,, +P1,15,No,No,R2,P2PTXAX020,6G,LPI,,,,,,,T1B,197,160,-70,DL,STA56,,,,,, +P1,15,No,No,R2,P2PTXAX021,6G,LPI,,,,,,,T1C,197,160,-45,DL,STA56,,,,,, +P2,16,Yes,Yes,R2,COETXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4 +P1,17,No,Yes,R2,P2PTXAX022,6G,SP,,,,,,,T2A,5,320,-79,DL,STA56,,,,,, +P1,17,No,No,R2,P2PTXAX023,6G,SP,,,,,,,T1B,5,320,-70,DL,STA56,,,,,, +P1,17,No,No,R2,P2PTXAX024,6G,SP,,,,,,,T1C,5,320,-45,DL,STA56,,,,,, +P2,18,Yes,Yes,R2,COETXAX010,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4 +P1,19,No,Yes,R2,P2PTXAX025,6G,LPI,,,,,,,T2A,133,320,-79,DL,STA56,,,,,, +P1,19,No,No,R2,P2PTXAX026,6G,LPI,,,,,,,T1B,133,320,-70,DL,STA56,,,,,, +P1,19,No,No,R2,P2PTXAX027,6G,LPI,,,,,,,T1C,133,320,-45,DL,STA56,,,,,, +P2,20,Yes,Yes,R2,COETXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4 +P1,21,No,Yes,R3,P2PTXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56 +P1,21,No,No,R3,P2PTXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56 +P1,21,No,No,R3,P2PTXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56 +P2,22,Yes,Yes,R3,COETXAX012,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,DL,STA56 +P1,23,No,Yes,R3,P2PTXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56 +P1,23,No,No,R3,P2PTXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56 +P1,23,No,No,R3,P2PTXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56 +P2,24,Yes,Yes,R3,COETXAX013,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA56 +P1,25,No,Yes,R3,P2PTXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56 +P1,25,No,No,R3,P2PTXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56 +P1,25,No,No,R3,P2PTXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56 +P2,26,Yes,Yes,R3,COETXAX014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,DL,STA56 +P1,1,No,No,R2,P2PTXAC001,5G,LPI,T1F,36,80,-76,DL,STA4,,,,,,,,,,,, +P1,1,No,No,R2,P2PTXAC002,5G,LPI,T1B,36,80,-67,DL,STA4,,,,,,,,,,,, +P1,1,No,No,R2,P2PTXAC003,5G,LPI,T1C,36,80,-45,DL,STA4,,,,,,,,,,,, +P1,2,No,Yes,R2,P2PTXAC004,5G,LPI,T1F,100,80,-76,DL,STA4,,,,,,,,,,,, +P1,2,No,No,R2,P2PTXAC005,5G,LPI,T1B,100,80,-67,DL,STA4,,,,,,,,,,,, +P1,2,No,No,R2,P2PTXAC006,5G,LPI,T1C,100,80,-45,DL,STA4,,,,,,,,,,,, +P2,3,Yes,Yes,R2,COETXAC001,5G,LPI,T1F,100,80,-76,DL,STA4,T1K2A,5,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56 +P1,5,No,Yes,R2,P2PTXAC007,5G,LPI,T1F,161,80,-76,DL,STA4,,,,,,,,,,,, +P1,5,No,No,R2,P2PTXAC008,5G,LPI,T1B,161,80,-67,DL,STA4,,,,,,,,,,,, +P1,5,No,No,R2,P2PTXAC009,5G,LPI,T1C,161,80,-45,DL,STA4,,,,,,,,,,,, +P2,6,Yes,Yes,R2,COETXAC002,5G,LPI,T1F,161,80,-76,DL,STA4,T1K2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56 +P1,14,No,Yes,R1,P2PTXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,DL,STA4 +P1,14,No,No,R1,P2PTXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,DL,STA4 +P1,14,No,No,R1,P2PTXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,DL,STA4 +P2,15,Yes,Yes,R1,COETXAC003,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,5,160,OFF,DL,STA63,T3E,1,20,-70,DL,STA4 +P1,16,No,Yes,R1,P2PTXAC013,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,DL,STA4 +P1,16,No,No,R1,P2PTXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,DL,STA4 +P1,16,No,No,R1,P2PTXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,DL,STA4 +P2,17,Yes,Yes,R1,COETXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA4 +P1,18,No,Yes,R1,P2PTXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,DL,STA4 +P1,18,No,No,R1,P2PTXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,DL,STA4 +P1,18,No,No,R1,P2PTXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,DL,STA4 +P2,19,Yes,Yes,R1,COETXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,5,320,OFF,DL,STA63,T3E,11,20,-70,DL,STA4 diff --git a/backend/data/p3p_tests.csv b/backend/data/CGW453_P3P_Tests.csv similarity index 86% rename from backend/data/p3p_tests.csv rename to backend/data/CGW453_P3P_Tests.csv index 614790a..8232be8 100644 --- a/backend/data/p3p_tests.csv +++ b/backend/data/CGW453_P3P_Tests.csv @@ -1,81 +1,89 @@ -Priority,Index,Throttled,Rotation,TC ID,Band,6GHz Power Mode,Station 1 Test Point,Station 1 Channel,Station 1 Bandwidth,Station 1 RSSI,Station 1 Direction,Station 1 Rate,Station 1 STA,Station 2 Test Point,Station 2 Channel,Station 2 Bandwidth,Station 2 RSSI,Station 2 Direction,Station 2 Rate,Station 2 STA,Station 3 Test Point,Station 3 Channel,Station 3 Bandwidth,Station 3 RSSI,Station 3 Direction,Station 3 Rate,Station 3 STA +Priority,Index,Throttled,Rotation,TC ID,Band,6GHz Power Mode,STATION 1_Test Point,STATION 1_Channel,STATION 1_Bandwidth,STATION 1_RSSI,STATION 1_Direction,STATION 1_Rate,STATION 1_STA,STATION 2_Test Point,STATION 2_Channel,STATION 2_Bandwidth,STATION 2_RSSI,STATION 2_Direction,STATION 2_Rate,STATION 2_STA,STATION 3_Test Point,STATION 3_Channel,STATION 3_Bandwidth,STATION 3_RSSI,STATION 3_Direction,STATION 3_Rate,STATION 3_STA +P2,1,Yes,R3,P3PRXBETH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA63,T1I,36,80,-76,UL,22Mbps,STA64,T2J,36,80,-76,UL,11Mbps,STA65 +P2,2,Yes,R3,P3PRXBETH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA63,T1I,100,80,-76,UL,22Mbps,STA64,T2J,100,80,-76,UL,11Mbps,STA65 +P2,3,Yes,R3,P3PRXBETH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA63,T1I,161,80,-76,UL,22Mbps,STA64,T2J,161,80,-76,UL,11Mbps,STA65 P2,4,Yes,R1,P3PRXBETH004,6G,LPI,T2A,5,160,-79,UL,22Mbps,STA63,T1L,5,160,-79,UL,22Mbps,STA64,T2O,5,160,-79,UL,11Mbps,STA65 P2,5,Yes,R1,P3PRXBETH005,6G,LPI,T2A,133,160,-79,UL,22Mbps,STA63,T1L,133,160,-79,UL,22Mbps,STA64,T2O,133,160,-79,UL,11Mbps,STA65 P2,6,Yes,R1,P3PRXBETH006,6G,LPI,T2A,197,160,-79,UL,22Mbps,STA63,T1L,197,160,-79,UL,22Mbps,STA64,T2O,197,160,-79,UL,11Mbps,STA65 +P2,7,Yes,R2,P3PRXBETH007,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA63,T1P,6,20,-70,UL,22Mbps,STA64,T2Q,6,20,-70,UL,11Mbps,STA65 +P2,8,Yes,R1,P3PRXBETH008,6G,SP,T2A,5,320,-79,UL,22Mbps,STA63,T1L,5,320,-79,UL,22Mbps,STA64,T2O,5,320,-79,UL,11Mbps,STA65 P2,9,Yes,R1,P3PRXBETH009,6G,LPI,T2A,133,320,-79,UL,22Mbps,STA63,T1L,133,320,-79,UL,22Mbps,STA64,T2O,133,320,-79,UL,11Mbps,STA65 +P2,1,No,R3,P3PRXBEUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA63,T1I,36,80,-76,UL,Unlimited,STA64,T2J,36,80,-76,UL,Unlimited,STA65 +P2,2,No,R3,P3PRXBEUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA63,T1I,100,80,-76,UL,Unlimited,STA64,T2J,100,80,-76,UL,Unlimited,STA65 +P2,3,No,R3,P3PRXBEUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA63,T1I,161,80,-76,UL,Unlimited,STA64,T2J,161,80,-76,UL,Unlimited,STA65 P2,4,No,R1,P3PRXBEUT004,6G,LPI,T2A,5,160,-79,UL,Unlimited,STA63,T1L,5,160,-79,UL,Unlimited,STA64,T2O,5,160,-79,UL,Unlimited,STA65 P2,5,No,R1,P3PRXBEUT005,6G,LPI,T2A,133,160,-79,UL,Unlimited,STA63,T1L,133,160,-79,UL,Unlimited,STA64,T2O,133,160,-79,UL,Unlimited,STA65 P2,6,No,R1,P3PRXBEUT006,6G,LPI,T2A,197,160,-79,UL,Unlimited,STA63,T1L,197,160,-79,UL,Unlimited,STA64,T2O,197,160,-79,UL,Unlimited,STA65 +P2,7,No,R2,P3PRXBEUT007,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA63,T1P,6,20,-70,UL,Unlimited,STA64,T2Q,6,20,-70,UL,Unlimited,STA65 +P2,8,No,R1,P3PRXBEUT008,6G,SP,T2A,5,320,-79,UL,Unlimited,STA63,T1L,5,320,-79,UL,Unlimited,STA64,T2O,5,320,-79,UL,Unlimited,STA65 P2,9,No,R1,P3PRXBEUT009,6G,LPI,T2A,133,320,-79,UL,Unlimited,STA63,T1L,133,320,-79,UL,Unlimited,STA64,T2O,133,320,-79,UL,Unlimited,STA65 +P2,1,Yes,R3,P3PTXBETH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA63,T1I,36,80,-76,DL,22Mbps,STA64,T2J,36,80,-76,DL,11Mbps,STA65 +P2,2,Yes,R3,P3PTXBETH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA63,T1I,100,80,-76,DL,22Mbps,STA64,T2J,100,80,-76,DL,11Mbps,STA65 +P2,3,Yes,R3,P3PTXBETH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA63,T1I,161,80,-76,DL,22Mbps,STA64,T2J,161,80,-76,DL,11Mbps,STA65 P2,4,Yes,R1,P3PTXBETH004,6G,LPI,T2A,5,160,-79,DL,22Mbps,STA63,T1L,5,160,-79,DL,22Mbps,STA64,T2O,5,160,-79,DL,11Mbps,STA65 P2,5,Yes,R1,P3PTXBETH005,6G,LPI,T2A,133,160,-79,DL,22Mbps,STA63,T1L,133,160,-79,DL,22Mbps,STA64,T2O,133,160,-79,DL,11Mbps,STA65 P2,6,Yes,R1,P3PTXBETH006,6G,LPI,T2A,197,160,-79,DL,22Mbps,STA63,T1L,197,160,-79,DL,22Mbps,STA64,T2O,197,160,-79,DL,11Mbps,STA65 +P2,7,Yes,R2,P3PTXBETH007,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA63,T1P,6,20,-70,DL,22Mbps,STA64,T2Q,6,20,-70,DL,11Mbps,STA65 +P2,8,Yes,R1,P3PTXBETH008,6G,SP,T2A,5,320,-79,DL,22Mbps,STA63,T1L,5,320,-79,DL,22Mbps,STA64,T2O,5,320,-79,DL,11Mbps,STA65 P2,9,Yes,R1,P3PTXBETH009,6G,LPI,T2A,133,320,-79,DL,22Mbps,STA63,T1L,133,320,-79,DL,22Mbps,STA64,T2O,133,320,-79,DL,11Mbps,STA65 +P2,1,No,R3,P3PTXBEUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA63,T1I,36,80,-76,DL,Unlimited,STA64,T2J,36,80,-76,DL,Unlimited,STA65 +P2,2,No,R3,P3PTXBEUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA63,T1I,100,80,-76,DL,Unlimited,STA64,T2J,100,80,-76,DL,Unlimited,STA65 +P2,3,No,R3,P3PTXBEUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA63,T1I,161,80,-76,DL,Unlimited,STA64,T2J,161,80,-76,DL,Unlimited,STA65 P2,4,No,R1,P3PTXBEUT004,6G,LPI,T2A,5,160,-79,DL,Unlimited,STA63,T1L,5,160,-79,DL,Unlimited,STA64,T2O,5,160,-79,DL,Unlimited,STA65 P2,5,No,R1,P3PTXBEUT005,6G,LPI,T2A,133,160,-79,DL,Unlimited,STA63,T1L,133,160,-79,DL,Unlimited,STA64,T2O,133,160,-79,DL,Unlimited,STA65 P2,6,No,R1,P3PTXBEUT006,6G,LPI,T2A,197,160,-79,DL,Unlimited,STA63,T1L,197,160,-79,DL,Unlimited,STA64,T2O,197,160,-79,DL,Unlimited,STA65 +P2,7,No,R2,P3PTXBEUT007,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA63,T1P,6,20,-70,DL,Unlimited,STA64,T2Q,6,20,-70,DL,Unlimited,STA65 +P2,8,No,R1,P3PTXBEUT008,6G,SP,T2A,5,320,-79,DL,Unlimited,STA63,T1L,5,320,-79,DL,Unlimited,STA64,T2O,5,320,-79,DL,Unlimited,STA65 P2,9,No,R1,P3PTXBEUT009,6G,LPI,T2A,133,320,-79,DL,Unlimited,STA63,T1L,133,320,-79,DL,Unlimited,STA64,T2O,133,320,-79,DL,Unlimited,STA65 P2,1,Yes,R1,P3PRXAXTH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA56,T1I,36,80,-76,UL,22Mbps,STA58,T2J,36,80,-76,UL,11Mbps,STA59 P2,2,Yes,R1,P3PRXAXTH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA56,T1I,100,80,-76,UL,22Mbps,STA58,T2J,100,80,-76,UL,11Mbps,STA59 P2,3,Yes,R1,P3PRXAXTH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA56,T1I,161,80,-76,UL,22Mbps,STA58,T2J,161,80,-76,UL,11Mbps,STA59 -P2,1,No,R1,P3PRXAXUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA56,T1I,36,80,-76,UL,Unlimited,STA58,T2J,36,80,-76,UL,Unlimited,STA59 -P2,2,No,R1,P3PRXAXUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA56,T1I,100,80,-76,UL,Unlimited,STA58,T2J,100,80,-76,UL,Unlimited,STA59 -P2,3,No,R1,P3PRXAXUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA56,T1I,161,80,-76,UL,Unlimited,STA58,T2J,161,80,-76,UL,Unlimited,STA59 -P2,1,Yes,R1,P3PTXAXTH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA56,T1I,36,80,-76,DL,22Mbps,STA58,T2J,36,80,-76,DL,11Mbps,STA59 -P2,2,Yes,R1,P3PTXAXTH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA56,T1I,100,80,-76,DL,22Mbps,STA58,T2J,100,80,-76,DL,11Mbps,STA59 -P2,3,Yes,R1,P3PTXAXTH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA56,T1I,161,80,-76,DL,22Mbps,STA58,T2J,161,80,-76,DL,11Mbps,STA59 -P2,1,No,R1,P3PTXAXUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA56,T1I,36,80,-76,DL,Unlimited,STA58,T2J,36,80,-76,DL,Unlimited,STA59 -P2,2,No,R1,P3PTXAXUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA56,T1I,100,80,-76,DL,Unlimited,STA58,T2J,100,80,-76,DL,Unlimited,STA59 -P2,3,No,R1,P3PTXAXUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA56,T1I,161,80,-76,DL,Unlimited,STA58,T2J,161,80,-76,DL,Unlimited,STA59 -P2,4,Yes,R1,P3PRXACTH004,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA4,T1P,6,20,-70,UL,22Mbps,STA5,T2Q,6,20,-70,UL,11Mbps,STA6 -P2,4,No,R1,P3PRXACUT004,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA4,T1P,6,20,-70,UL,Unlimited,STA5,T2Q,6,20,-70,UL,Unlimited,STA6 -P2,4,Yes,R1,P3PTXACTH004,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA4,T1P,6,20,-70,DL,22Mbps,STA5,T2Q,6,20,-70,DL,11Mbps,STA6 -P2,4,No,R1,P3PTXACUT004,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA4,T1P,6,20,-70,DL,Unlimited,STA5,T2Q,6,20,-70,DL,Unlimited,STA6 -P2,7,Yes,R2,P3PRXBETH007,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA63,T1P,6,20,-70,UL,22Mbps,STA64,T2Q,6,20,-70,UL,11Mbps,STA65 -P2,7,No,R2,P3PRXBEUT007,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA63,T1P,6,20,-70,UL,Unlimited,STA64,T2Q,6,20,-70,UL,Unlimited,STA65 -P2,7,Yes,R2,P3PTXBETH007,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA63,T1P,6,20,-70,DL,22Mbps,STA64,T2Q,6,20,-70,DL,11Mbps,STA65 -P2,7,No,R2,P3PTXBEUT007,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA63,T1P,6,20,-70,DL,Unlimited,STA64,T2Q,6,20,-70,DL,Unlimited,STA65 P2,4,Yes,R2,P3PRXAXTH004,6G,LPI,T2A,5,160,-79,UL,22Mbps,STA56,T1L,5,160,-79,UL,22Mbps,STA58,T2O,5,160,-79,UL,11Mbps,STA59 P2,5,Yes,R2,P3PRXAXTH005,6G,LPI,T2A,133,160,-79,UL,22Mbps,STA56,T1L,133,160,-79,UL,22Mbps,STA58,T2O,133,160,-79,UL,11Mbps,STA59 P2,6,Yes,R2,P3PRXAXTH006,6G,LPI,T2A,197,160,-79,UL,22Mbps,STA56,T1L,197,160,-79,UL,22Mbps,STA58,T2O,197,160,-79,UL,11Mbps,STA59 +P2,7,Yes,R3,P3PRXAXTH007,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA56,T1P,6,20,-70,UL,22Mbps,STA58,T2Q,6,20,-70,UL,11Mbps,STA59 +P2,8,Yes,R2,P3PRXAXTH008,6G,SP,T2A,5,320,-79,UL,22Mbps,STA56,T1L,5,320,-79,UL,22Mbps,STA58,T2O,5,320,-79,UL,11Mbps,STA59 P2,9,Yes,R2,P3PRXAXTH009,6G,LPI,T2A,133,320,-79,UL,22Mbps,STA56,T1L,133,320,-79,UL,22Mbps,STA58,T2O,133,320,-79,UL,11Mbps,STA59 +P2,1,No,R1,P3PRXAXUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA56,T1I,36,80,-76,UL,Unlimited,STA58,T2J,36,80,-76,UL,Unlimited,STA59 +P2,2,No,R1,P3PRXAXUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA56,T1I,100,80,-76,UL,Unlimited,STA58,T2J,100,80,-76,UL,Unlimited,STA59 +P2,3,No,R1,P3PRXAXUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA56,T1I,161,80,-76,UL,Unlimited,STA58,T2J,161,80,-76,UL,Unlimited,STA59 P2,4,No,R2,P3PRXAXUT004,6G,LPI,T2A,5,160,-79,UL,Unlimited,STA56,T1L,5,160,-79,UL,Unlimited,STA58,T2O,5,160,-79,UL,Unlimited,STA59 P2,5,No,R2,P3PRXAXUT005,6G,LPI,T2A,133,160,-79,UL,Unlimited,STA56,T1L,133,160,-79,UL,Unlimited,STA58,T2O,133,160,-79,UL,Unlimited,STA59 P2,6,No,R2,P3PRXAXUT006,6G,LPI,T2A,197,160,-79,UL,Unlimited,STA56,T1L,197,160,-79,UL,Unlimited,STA58,T2O,197,160,-79,UL,Unlimited,STA59 +P2,7,No,R3,P3PRXAXUT007,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA56,T1P,6,20,-70,UL,Unlimited,STA58,T2Q,6,20,-70,UL,Unlimited,STA59 +P2,8,No,R2,P3PRXAXUT008,6G,SP,T2A,5,320,-79,UL,Unlimited,STA56,T1L,5,320,-79,UL,Unlimited,STA58,T2O,5,320,-79,UL,Unlimited,STA59 P2,9,No,R2,P3PRXAXUT009,6G,LPI,T2A,133,320,-79,UL,Unlimited,STA56,T1L,133,320,-79,UL,Unlimited,STA58,T2O,133,320,-79,UL,Unlimited,STA59 +P2,1,Yes,R1,P3PTXAXTH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA56,T1I,36,80,-76,DL,22Mbps,STA58,T2J,36,80,-76,DL,11Mbps,STA59 +P2,2,Yes,R1,P3PTXAXTH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA56,T1I,100,80,-76,DL,22Mbps,STA58,T2J,100,80,-76,DL,11Mbps,STA59 +P2,3,Yes,R1,P3PTXAXTH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA56,T1I,161,80,-76,DL,22Mbps,STA58,T2J,161,80,-76,DL,11Mbps,STA59 P2,4,Yes,R2,P3PTXAXTH004,6G,LPI,T2A,5,160,-79,DL,22Mbps,STA56,T1L,5,160,-79,DL,22Mbps,STA58,T2O,5,160,-79,DL,11Mbps,STA59 P2,5,Yes,R2,P3PTXAXTH005,6G,LPI,T2A,133,160,-79,DL,22Mbps,STA56,T1L,133,160,-79,DL,22Mbps,STA58,T2O,133,160,-79,DL,11Mbps,STA59 P2,6,Yes,R2,P3PTXAXTH006,6G,LPI,T2A,197,160,-79,DL,22Mbps,STA56,T1L,197,160,-79,DL,22Mbps,STA58,T2O,197,160,-79,DL,11Mbps,STA59 +P2,7,Yes,R3,P3PTXAXTH007,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA56,T1P,6,20,-70,DL,22Mbps,STA58,T2Q,6,20,-70,DL,11Mbps,STA59 +P2,8,Yes,R2,P3PTXAXTH008,6G,SP,T2A,5,320,-79,DL,22Mbps,STA56,T1L,5,320,-79,DL,22Mbps,STA58,T2O,5,320,-79,DL,11Mbps,STA59 P2,9,Yes,R2,P3PTXAXTH009,6G,LPI,T2A,133,320,-79,DL,22Mbps,STA56,T1L,133,320,-79,DL,22Mbps,STA58,T2O,133,320,-79,DL,11Mbps,STA59 +P2,1,No,R1,P3PTXAXUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA56,T1I,36,80,-76,DL,Unlimited,STA58,T2J,36,80,-76,DL,Unlimited,STA59 +P2,2,No,R1,P3PTXAXUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA56,T1I,100,80,-76,DL,Unlimited,STA58,T2J,100,80,-76,DL,Unlimited,STA59 +P2,3,No,R1,P3PTXAXUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA56,T1I,161,80,-76,DL,Unlimited,STA58,T2J,161,80,-76,DL,Unlimited,STA59 P2,4,No,R2,P3PTXAXUT004,6G,LPI,T2A,5,160,-79,DL,Unlimited,STA56,T1L,5,160,-79,DL,Unlimited,STA58,T2O,5,160,-79,DL,Unlimited,STA59 P2,5,No,R2,P3PTXAXUT005,6G,LPI,T2A,133,160,-79,DL,Unlimited,STA56,T1L,133,160,-79,DL,Unlimited,STA58,T2O,133,160,-79,DL,Unlimited,STA59 P2,6,No,R2,P3PTXAXUT006,6G,LPI,T2A,197,160,-79,DL,Unlimited,STA56,T1L,197,160,-79,DL,Unlimited,STA58,T2O,197,160,-79,DL,Unlimited,STA59 +P2,7,No,R3,P3PTXAXUT007,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA56,T1P,6,20,-70,DL,Unlimited,STA58,T2Q,6,20,-70,DL,Unlimited,STA59 +P2,8,No,R2,P3PTXAXUT008,6G,SP,T2A,5,320,-79,DL,Unlimited,STA56,T1L,5,320,-79,DL,Unlimited,STA58,T2O,5,320,-79,DL,Unlimited,STA59 P2,9,No,R2,P3PTXAXUT009,6G,LPI,T2A,133,320,-79,DL,Unlimited,STA56,T1L,133,320,-79,DL,Unlimited,STA58,T2O,133,320,-79,DL,Unlimited,STA59 P2,1,Yes,R2,P3PRXACTH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA4,T1I,36,80,-76,UL,22Mbps,STA5,T2J,36,80,-76,UL,11Mbps,STA6 P2,2,Yes,R2,P3PRXACTH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA4,T1I,100,80,-76,UL,22Mbps,STA5,T2J,100,80,-76,UL,11Mbps,STA6 P2,3,Yes,R2,P3PRXACTH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA4,T1I,161,80,-76,UL,22Mbps,STA5,T2J,161,80,-76,UL,11Mbps,STA6 +P2,4,Yes,R1,P3PRXACTH004,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA4,T1P,6,20,-70,UL,22Mbps,STA5,T2Q,6,20,-70,UL,11Mbps,STA6 P2,1,No,R2,P3PRXACUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA4,T1I,36,80,-76,UL,Unlimited,STA5,T2J,36,80,-76,UL,Unlimited,STA6 P2,2,No,R2,P3PRXACUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA4,T1I,100,80,-76,UL,Unlimited,STA5,T2J,100,80,-76,UL,Unlimited,STA6 P2,3,No,R2,P3PRXACUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA4,T1I,161,80,-76,UL,Unlimited,STA5,T2J,161,80,-76,UL,Unlimited,STA6 +P2,4,No,R1,P3PRXACUT004,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA4,T1P,6,20,-70,UL,Unlimited,STA5,T2Q,6,20,-70,UL,Unlimited,STA6 P2,1,Yes,R2,P3PTXACTH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA4,T1I,36,80,-76,DL,22Mbps,STA5,T2J,36,80,-76,DL,11Mbps,STA6 P2,2,Yes,R2,P3PTXACTH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA4,T1I,100,80,-76,DL,22Mbps,STA5,T2J,100,80,-76,DL,11Mbps,STA6 P2,3,Yes,R2,P3PTXACTH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA4,T1I,161,80,-76,DL,22Mbps,STA5,T2J,161,80,-76,DL,11Mbps,STA6 +P2,4,Yes,R1,P3PTXACTH004,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA4,T1P,6,20,-70,DL,22Mbps,STA5,T2Q,6,20,-70,DL,11Mbps,STA6 P2,1,No,R2,P3PTXACUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA4,T1I,36,80,-76,DL,Unlimited,STA5,T2J,36,80,-76,DL,Unlimited,STA6 P2,2,No,R2,P3PTXACUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA4,T1I,100,80,-76,DL,Unlimited,STA5,T2J,100,80,-76,DL,Unlimited,STA6 P2,3,No,R2,P3PTXACUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA4,T1I,161,80,-76,DL,Unlimited,STA5,T2J,161,80,-76,DL,Unlimited,STA6 -P2,1,Yes,R3,P3PRXBETH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA63,T1I,36,80,-76,UL,22Mbps,STA64,T2J,36,80,-76,UL,11Mbps,STA65 -P2,2,Yes,R3,P3PRXBETH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA63,T1I,100,80,-76,UL,22Mbps,STA64,T2J,100,80,-76,UL,11Mbps,STA65 -P2,3,Yes,R3,P3PRXBETH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA63,T1I,161,80,-76,UL,22Mbps,STA64,T2J,161,80,-76,UL,11Mbps,STA65 -P2,1,No,R3,P3PRXBEUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA63,T1I,36,80,-76,UL,Unlimited,STA64,T2J,36,80,-76,UL,Unlimited,STA65 -P2,2,No,R3,P3PRXBEUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA63,T1I,100,80,-76,UL,Unlimited,STA64,T2J,100,80,-76,UL,Unlimited,STA65 -P2,3,No,R3,P3PRXBEUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA63,T1I,161,80,-76,UL,Unlimited,STA64,T2J,161,80,-76,UL,Unlimited,STA65 -P2,1,Yes,R3,P3PTXBETH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA63,T1I,36,80,-76,DL,22Mbps,STA64,T2J,36,80,-76,DL,11Mbps,STA65 -P2,2,Yes,R3,P3PTXBETH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA63,T1I,100,80,-76,DL,22Mbps,STA64,T2J,100,80,-76,DL,11Mbps,STA65 -P2,3,Yes,R3,P3PTXBETH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA63,T1I,161,80,-76,DL,22Mbps,STA64,T2J,161,80,-76,DL,11Mbps,STA65 -P2,1,No,R3,P3PTXBEUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA63,T1I,36,80,-76,DL,Unlimited,STA64,T2J,36,80,-76,DL,Unlimited,STA65 -P2,2,No,R3,P3PTXBEUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA63,T1I,100,80,-76,DL,Unlimited,STA64,T2J,100,80,-76,DL,Unlimited,STA65 -P2,3,No,R3,P3PTXBEUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA63,T1I,161,80,-76,DL,Unlimited,STA64,T2J,161,80,-76,DL,Unlimited,STA65 -P2,7,Yes,R3,P3PRXAXTH007,2G,LPI,T1D,6,20,-70,UL,22Mbps,STA56,T1P,6,20,-70,UL,22Mbps,STA58,T2Q,6,20,-70,UL,11Mbps,STA59 -P2,7,No,R3,P3PRXAXUT007,2G,LPI,T1D,6,20,-70,UL,Unlimited,STA56,T1P,6,20,-70,UL,Unlimited,STA58,T2Q,6,20,-70,UL,Unlimited,STA59 -P2,7,Yes,R3,P3PTXAXTH007,2G,LPI,T1D,6,20,-70,DL,22Mbps,STA56,T1P,6,20,-70,DL,22Mbps,STA58,T2Q,6,20,-70,DL,11Mbps,STA59 -P2,7,No,R3,P3PTXAXUT007,2G,LPI,T1D,6,20,-70,DL,Unlimited,STA56,T1P,6,20,-70,DL,Unlimited,STA58,T2Q,6,20,-70,DL,Unlimited,STA59 +P2,4,No,R1,P3PTXACUT004,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA4,T1P,6,20,-70,DL,Unlimited,STA5,T2Q,6,20,-70,DL,Unlimited,STA6 diff --git a/backend/db.py b/backend/db.py index 9b86daf..225ecf3 100644 --- a/backend/db.py +++ b/backend/db.py @@ -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,21 +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, - failed_test_ids_json TEXT NOT NULL, - estimated_rerun_minutes INTEGER NOT NULL, - rerun_during_day INTEGER - ); - CREATE INDEX IF NOT EXISTS idx_tests_status ON tests(status); CREATE INDEX IF NOT EXISTS idx_tests_priority ON tests(priority); CREATE INDEX IF NOT EXISTS idx_schedules_date_shift ON schedules(scheduled_date, shift_index); @@ -166,14 +151,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 +209,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 +242,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 +288,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) @@ -552,11 +495,82 @@ def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[Sc FROM schedules s JOIN tests t ON t.test_id = s.test_id AND t.device = s.device WHERE s.schedule_version = ? - AND s.scheduled_date >= ? - AND s.scheduled_date < date(?, '+7 day') + AND ( + ( + s.scheduled_date >= ? + AND s.scheduled_date < date(?, '+7 day') + AND s.shift_index IN (2, 3) + ) + OR ( + s.scheduled_date > ? + AND s.scheduled_date < date(?, '+7 day') + AND s.shift_index = 1 + ) + OR ( + s.scheduled_date = date(?, '+7 day') + AND s.shift_index = 1 + ) + ) ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift """, - (latest, start_date, start_date), + ( + latest, + start_date, + start_date, + start_date, + start_date, + start_date, + ), + ).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 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] = [] diff --git a/backend/graph.py b/backend/graph.py deleted file mode 100644 index 364429a..0000000 --- a/backend/graph.py +++ /dev/null @@ -1,179 +0,0 @@ -from __future__ import annotations - -import json -import os -from pathlib import Path -from typing import Any - -# Immutable-in-practice graph state for the loaded DUT test set. -_TESTS_BY_ID: dict[str, Any] = {} -_GRAPH: dict[str, set[str]] = {} -DUT = os.getenv("DUT", "CGW453").strip() -APP_ROOT = Path(__file__).resolve().parent -DB_PATH = APP_ROOT / "scheduler.db" -GRAPH_CACHE_NAME = "dut_compatibility" - - -def reset_graph_state() -> None: - global _TESTS_BY_ID, _GRAPH - _TESTS_BY_ID = {} - _GRAPH = {} - - -def _get_station_map(test: Any) -> dict[str, str]: - """Get station testpoint map from test record or compute it from config.""" - # Try to get from serialized map first (cached from DB) - if hasattr(test, 'station_testpoint_map') and test.station_testpoint_map: - try: - return json.loads(test.station_testpoint_map) - except (json.JSONDecodeError, TypeError): - pass - - # Safeguard: return empty map - return {} - - -def is_graph_built() -> bool: - return bool(_GRAPH) - - -def build_graph_once(tests: list[Any]) -> dict[str, set[str]]: - """Build DUT-only compatibility adjacency graph once per load lifecycle.""" - global _TESTS_BY_ID, _GRAPH - if _GRAPH: - return _GRAPH - - # Keep only one representative per DUT test_id. - _TESTS_BY_ID = {str(t.test_id): t for t in tests if getattr(t, "test_id", None)} - _GRAPH = _build_graph(_TESTS_BY_ID) - return _GRAPH - - -def serialize_graph(graph: dict[str, set[str]]) -> dict[str, list[str]]: - return {str(test_id): sorted(str(neighbor) for neighbor in neighbors) for test_id, neighbors in graph.items()} - - -def deserialize_graph(data: dict[str, list[str]]) -> dict[str, set[str]]: - deserialized: dict[str, set[str]] = {} - for test_id, neighbors in (data or {}).items(): - deserialized[str(test_id)] = {str(neighbor) for neighbor in (neighbors or [])} - return deserialized - - -def _persist_graph(db_path: str | Path = DB_PATH) -> None: - import db as db_module - db_module.save_graph_cache( - name=GRAPH_CACHE_NAME, - payload=serialize_graph(_GRAPH), - test_count=len(_GRAPH), - db_path=db_path, - ) - - -def _load_graph_from_db(db_path: str | Path = DB_PATH) -> bool: - global _GRAPH - import db as db_module - cached = db_module.load_graph_cache(GRAPH_CACHE_NAME, db_path) - if cached is None: - return False - _GRAPH = deserialize_graph(cached.get("payload") or {}) - return True - - -def build_and_persist_graph(tests: list[Any], db_path: str | Path = DB_PATH) -> dict[str, set[str]]: - graph = build_graph_once(tests) - _persist_graph(db_path) - return graph - - -def get_graph( - db_path: str | Path = DB_PATH, - dut_tests: list[Any] | None = None, -) -> dict[str, set[str]]: - if _GRAPH: - return _GRAPH - - if _load_graph_from_db(db_path): - return _GRAPH - - if dut_tests is None: - import db as db_module - dut_tests = db_module.list_tests_for_device(DUT, db_path) - - build_and_persist_graph(dut_tests or [], db_path) - return _GRAPH - - -def _build_graph(tests: dict[str, Any]) -> dict[str, set[str]]: - test_ids = list(tests.keys()) - graph = {test_id: set() for test_id in test_ids} - for i in range(len(test_ids)): - for j in range(i + 1, len(test_ids)): - a_id = test_ids[i] - b_id = test_ids[j] - a = tests[a_id] - b = tests[b_id] - if _compatible(a, b): - graph[a_id].add(b_id) - graph[b_id].add(a_id) - return graph - - -def _compatible(a: Any, b: Any) -> bool: - station_map_a = _get_station_map(a) - station_map_b = _get_station_map(b) - if _same_station_to_testpoint(station_map_a, station_map_b, a, b) and _same_testpoint_to_station(station_map_a, station_map_b, a, b): - return True - return False - -def _same_station_to_testpoint( - station_map_a: dict[str, str], - station_map_b: dict[str, str], test_a: Any, test_b: Any -) -> bool: - overlap_stations = set(station_map_a.keys()) & set(station_map_b.keys()) - for station in overlap_stations: - if station_map_a[station] != station_map_b[station]: - return False - return True - -def _same_testpoint_to_station( - station_map_a: dict[str, str], - station_map_b: dict[str, str], test_a: Any, test_b: Any -) -> bool: - overlap_testpoints = set(station_map_a.values()) & set(station_map_b.values()) - for testpoint in overlap_testpoints: - stations_a = {s for s, t in station_map_a.items() if t == testpoint} - stations_b = {s for s, t in station_map_b.items() if t == testpoint} - if stations_a != stations_b: - return False - return True - -def build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> dict[str, str]: - station_to_testpoint: dict[str, str] = {} - - def _add_entry(entry: dict[str, str | None]) -> None: - testpoint = _norm(entry.get("test_point")) - sta_raw = _norm(entry.get("sta")) - if not testpoint or not sta_raw: - return - for sta in sta_raw.split(","): - sta_clean = _norm(sta) - if sta_clean: - station_to_testpoint[sta_clean] = testpoint - - for band in ("5G", "6G", "2G"): - entry = config.get(band) or {} - _add_entry(entry) - - for station_key in ("Station 1", "Station 2", "Station 3"): - entry = config.get(station_key) or {} - _add_entry(entry) - - return station_to_testpoint - - - -def _norm(value: Any) -> str: - if value is None: - return "" - return " ".join(str(value).strip().upper().split()) diff --git a/backend/parser.py b/backend/parser.py index 26e75a6..0c3475b 100644 --- a/backend/parser.py +++ b/backend/parser.py @@ -21,24 +21,24 @@ P2P_COE_REQUIRED_COLUMNS = [ "TC ID", "Victim Band", "6GHz Power Mode", - "5G Test Point", - "5G Channel", - "5G Bandwidth", - "5G RSSI", - "5G Direction", - "5G STA", - "6G Test Point", - "6G Channel", - "6G Bandwidth", - "6G RSSI", - "6G Direction", - "6G STA", - "2G Test Point", - "2G Channel", - "2G Bandwidth", - "2G RSSI", - "2G Direction", - "2G STA", + "5G_Test Point", + "5G_Channel", + "5G_Bandwidth", + "5G_RSSI", + "5G_Direction", + "5G_STA", + "6G_Test Point", + "6G_Channel", + "6G_Bandwidth", + "6G_RSSI", + "6G_Direction", + "6G_STA", + "2G_Test Point", + "2G_Channel", + "2G_Bandwidth", + "2G_RSSI", + "2G_Direction", + "2G_STA", ] P3P_REQUIRED_COLUMNS = [ @@ -49,27 +49,27 @@ P3P_REQUIRED_COLUMNS = [ "TC ID", "Band", "6GHz Power Mode", - "Station 1 Test Point", - "Station 1 Channel", - "Station 1 Bandwidth", - "Station 1 RSSI", - "Station 1 Direction", - "Station 1 Rate", - "Station 1 STA", - "Station 2 Test Point", - "Station 2 Channel", - "Station 2 Bandwidth", - "Station 2 RSSI", - "Station 2 Direction", - "Station 2 Rate", - "Station 2 STA", - "Station 3 Test Point", - "Station 3 Channel", - "Station 3 Bandwidth", - "Station 3 RSSI", - "Station 3 Direction", - "Station 3 Rate", - "Station 3 STA", + "STATION 1_Test Point", + "STATION 1_Channel", + "STATION 1_Bandwidth", + "STATION 1_RSSI", + "STATION 1_Direction", + "STATION 1_Rate", + "STATION 1_STA", + "STATION 2_Test Point", + "STATION 2_Channel", + "STATION 2_Bandwidth", + "STATION 2_RSSI", + "STATION 2_Direction", + "STATION 2_Rate", + "STATION 2_STA", + "STATION 3_Test Point", + "STATION 3_Channel", + "STATION 3_Bandwidth", + "STATION 3_RSSI", + "STATION 3_Direction", + "STATION 3_Rate", + "STATION 3_STA", ] RUNTIME_DEFAULTS = { @@ -230,20 +230,23 @@ def _detect_csv_format(normalized_fieldnames: set[str]) -> str: f"{', '.join(p2p_coe_missing)}; missing columns for P3P format: {', '.join(p3p_missing)}" ) - def parse_target_csv( csv_path: str | Path | list[str | Path] | tuple[str | Path, ...], smb_credentials: dict[str, Any] | None = None, runtime_overrides: dict[str, Any] | None = None, ) -> ParseResult: paths = _resolve_csv_paths(csv_path, smb_credentials=smb_credentials) - runtime_defaults = _resolve_runtime_defaults(runtime_overrides) + runtime_defaults_by_device = _resolve_runtime_defaults(runtime_overrides) all_tests: list[TestRecord] = [] all_warnings: list[str] = [] seen_test_keys: set[tuple[str, str]] = set() for path in paths: - parsed = _parse_single_csv(path, smb_credentials=smb_credentials, runtime_defaults=runtime_defaults) + parsed = _parse_single_csv( + path, + smb_credentials=smb_credentials, + runtime_defaults_by_device=runtime_defaults_by_device, + ) all_warnings.extend(parsed.warnings) for record in parsed.tests: key = (record.test_id, record.device) @@ -258,13 +261,13 @@ def parse_target_csv( return ParseResult(tests=all_tests, warnings=all_warnings) -def _resolve_runtime_defaults(runtime_overrides: dict[str, Any] | None) -> dict[str, int]: +def _coerce_runtime_defaults(raw_overrides: dict[str, Any] | None) -> dict[str, int]: defaults = dict(RUNTIME_DEFAULTS) - if not runtime_overrides: + if not raw_overrides: return defaults for test_type in ("P2P", "COE", "P3P"): - raw_value = runtime_overrides.get(test_type) + raw_value = raw_overrides.get(test_type) if raw_value is None: continue if isinstance(raw_value, str): @@ -281,6 +284,25 @@ def _resolve_runtime_defaults(runtime_overrides: dict[str, Any] | None) -> dict[ return defaults +def _resolve_runtime_defaults(runtime_overrides: dict[str, Any] | None) -> dict[str, dict[str, int]]: + legacy_defaults = _coerce_runtime_defaults(runtime_overrides) + if not runtime_overrides: + return { + DEVICE_DUT: dict(legacy_defaults), + DEVICE_REF: dict(legacy_defaults), + } + + resolved: dict[str, dict[str, int]] = {} + for device in (DEVICE_DUT, DEVICE_REF): + device_defaults = dict(legacy_defaults) + raw_device_overrides = runtime_overrides.get(device) + if isinstance(raw_device_overrides, dict): + device_defaults.update(_coerce_runtime_defaults(raw_device_overrides)) + resolved[device] = device_defaults + + return resolved + + def _resolve_csv_paths( csv_path: str | Path | list[str | Path] | tuple[str | Path, ...], @@ -319,7 +341,7 @@ def _open_csv_handle(path: str, smb_credentials: dict[str, Any] | None): def _parse_single_csv( path: str, smb_credentials: dict[str, Any] | None, - runtime_defaults: dict[str, int], + runtime_defaults_by_device: dict[str, dict[str, int]], ) -> ParseResult: with _open_csv_handle(path, smb_credentials=smb_credentials) as handle: reader = csv.DictReader(handle) @@ -353,7 +375,6 @@ def _parse_single_csv( has_coe_pair = _normalize_yes_no(_row_get(row, "COE Pair")) if csv_format == "p2p_coe" else False config = _build_config(row, csv_format) signature = _victim_band_signature(row) if csv_format == "p2p_coe" else None - estimated_minutes = runtime_defaults.get(test_type, RUNTIME_DEFAULTS[test_type]) victim_band_source = "Victim Band" if csv_format == "p2p_coe" else "Band" victim_band = _normalize_victim_band(_row_get(row, victim_band_source)) @@ -377,7 +398,7 @@ def _parse_single_csv( victim_band=victim_band, config=config, throttled=throttled, - estimated_minutes=estimated_minutes, + estimated_minutes=RUNTIME_DEFAULTS[test_type], status="pending", excluded=False, raw_payload=row, @@ -402,6 +423,7 @@ def _parse_single_csv( pairs = sorted(coe_by_signature_and_suffix.get(key, [])) if key else [] for device in (DEVICE_DUT, DEVICE_REF): + device_runtime_defaults = runtime_defaults_by_device.get(device, RUNTIME_DEFAULTS) tests.append( TestRecord( test_id=record.test_id, @@ -415,7 +437,7 @@ def _parse_single_csv( victim_band=record.victim_band, config=record.config, throttled=record.throttled, - estimated_minutes=record.estimated_minutes, + estimated_minutes=device_runtime_defaults.get(record.test_type, RUNTIME_DEFAULTS[record.test_type]), status=record.status, excluded=record.excluded, raw_payload=record.raw_payload, @@ -476,11 +498,20 @@ def _victim_band_signature(row: dict[str, str]) -> tuple[str, ...] | None: if band is None: return None - prefix = f"{band} " - test_point = _normalize_value(_row_get(row, f"{prefix}Test Point")) - channel = _normalize_value(_row_get(row, f"{prefix}Channel")) - rssi = _normalize_value(_row_get(row, f"{prefix}RSSI")) - bandwidth = _normalize_value(_row_get(row, f"{prefix}Bandwidth")) + # Source CSVs may use either "5G_Test Point" or "5G Test Point" style headers. + prefixes = (f"{band}_", f"{band} ") + + def _pick_value(suffix: str) -> str: + for prefix in prefixes: + value = _row_get(row, f"{prefix}{suffix}") + if _normalize_value(value): + return _normalize_value(value) + return "" + + test_point = _pick_value("Test Point") + channel = _pick_value("Channel") + rssi = _pick_value("RSSI") + bandwidth = _pick_value("Bandwidth") # Direction is intentionally ignored for COE pairing matching. if not all([test_point, channel, rssi, bandwidth]): @@ -515,61 +546,61 @@ def _normalize_yes_no(value: str | None) -> bool: def _build_legacy_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]: return { "5G": { - "test_point": _empty_to_none(_row_get(row, "5G Test Point")), - "channel": _empty_to_none(_row_get(row, "5G Channel")), - "bandwidth": _empty_to_none(_row_get(row, "5G Bandwidth")), - "rssi": _empty_to_none(_row_get(row, "5G RSSI")), - "direction": _empty_to_none(_row_get(row, "5G Direction")), - "sta": _empty_to_none(_row_get(row, "5G STA")), + "test_point": _empty_to_none(_row_get(row, "5G_Test Point")), + "channel": _empty_to_none(_row_get(row, "5G_Channel")), + "bandwidth": _empty_to_none(_row_get(row, "5G_Bandwidth")), + "rssi": _empty_to_none(_row_get(row, "5G_RSSI")), + "direction": _empty_to_none(_row_get(row, "5G_Direction")), + "sta": _empty_to_none(_row_get(row, "5G_STA")), }, "6G": { "power_mode": _empty_to_none(_row_get(row, "6GHz Power Mode")), - "test_point": _empty_to_none(_row_get(row, "6G Test Point")), - "channel": _empty_to_none(_row_get(row, "6G Channel")), - "bandwidth": _empty_to_none(_row_get(row, "6G Bandwidth")), - "rssi": _empty_to_none(_row_get(row, "6G RSSI")), - "direction": _empty_to_none(_row_get(row, "6G Direction")), - "sta": _empty_to_none(_row_get(row, "6G STA")), + "test_point": _empty_to_none(_row_get(row, "6G_Test Point")), + "channel": _empty_to_none(_row_get(row, "6G_Channel")), + "bandwidth": _empty_to_none(_row_get(row, "6G_Bandwidth")), + "rssi": _empty_to_none(_row_get(row, "6G_RSSI")), + "direction": _empty_to_none(_row_get(row, "6G_Direction")), + "sta": _empty_to_none(_row_get(row, "6G_STA")), }, "2G": { - "test_point": _empty_to_none(_row_get(row, "2G Test Point")), - "channel": _empty_to_none(_row_get(row, "2G Channel")), - "bandwidth": _empty_to_none(_row_get(row, "2G Bandwidth")), - "rssi": _empty_to_none(_row_get(row, "2G RSSI")), - "direction": _empty_to_none(_row_get(row, "2G Direction")), - "sta": _empty_to_none(_row_get(row, "2G STA")), + "test_point": _empty_to_none(_row_get(row, "2G_Test Point")), + "channel": _empty_to_none(_row_get(row, "2G_Channel")), + "bandwidth": _empty_to_none(_row_get(row, "2G_Bandwidth")), + "rssi": _empty_to_none(_row_get(row, "2G_RSSI")), + "direction": _empty_to_none(_row_get(row, "2G_Direction")), + "sta": _empty_to_none(_row_get(row, "2G_STA")), }, } def _build_p3p_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]: return { - "Station 1": { - "test_point": _empty_to_none(_row_get(row, "Station 1 Test Point")), - "channel": _empty_to_none(_row_get(row, "Station 1 Channel")), - "bandwidth": _empty_to_none(_row_get(row, "Station 1 Bandwidth")), - "rssi": _empty_to_none(_row_get(row, "Station 1 RSSI")), - "direction": _empty_to_none(_row_get(row, "Station 1 Direction")), - "rate": _empty_to_none(_row_get(row, "Station 1 Rate")), - "sta": _empty_to_none(_row_get(row, "Station 1 STA")), + "STATION1": { + "test_point": _empty_to_none(_row_get(row, "STATION 1_Test Point")), + "channel": _empty_to_none(_row_get(row, "STATION 1_Channel")), + "bandwidth": _empty_to_none(_row_get(row, "STATION 1_Bandwidth")), + "rssi": _empty_to_none(_row_get(row, "STATION 1_RSSI")), + "direction": _empty_to_none(_row_get(row, "STATION 1_Direction")), + "rate": _empty_to_none(_row_get(row, "STATION 1_Rate")), + "sta": _empty_to_none(_row_get(row, "STATION 1_STA")), }, - "Station 2": { - "test_point": _empty_to_none(_row_get(row, "Station 2 Test Point")), - "channel": _empty_to_none(_row_get(row, "Station 2 Channel")), - "bandwidth": _empty_to_none(_row_get(row, "Station 2 Bandwidth")), - "rssi": _empty_to_none(_row_get(row, "Station 2 RSSI")), - "direction": _empty_to_none(_row_get(row, "Station 2 Direction")), - "rate": _empty_to_none(_row_get(row, "Station 2 Rate")), - "sta": _empty_to_none(_row_get(row, "Station 2 STA")), + "STATION2": { + "test_point": _empty_to_none(_row_get(row, "STATION 2_Test Point")), + "channel": _empty_to_none(_row_get(row, "STATION 2_Channel")), + "bandwidth": _empty_to_none(_row_get(row, "STATION 2_Bandwidth")), + "rssi": _empty_to_none(_row_get(row, "STATION 2_RSSI")), + "direction": _empty_to_none(_row_get(row, "STATION 2_Direction")), + "rate": _empty_to_none(_row_get(row, "STATION 2_Rate")), + "sta": _empty_to_none(_row_get(row, "STATION 2_STA")), }, - "Station 3": { - "test_point": _empty_to_none(_row_get(row, "Station 3 Test Point")), - "channel": _empty_to_none(_row_get(row, "Station 3 Channel")), - "bandwidth": _empty_to_none(_row_get(row, "Station 3 Bandwidth")), - "rssi": _empty_to_none(_row_get(row, "Station 3 RSSI")), - "direction": _empty_to_none(_row_get(row, "Station 3 Direction")), - "rate": _empty_to_none(_row_get(row, "Station 3 Rate")), - "sta": _empty_to_none(_row_get(row, "Station 3 STA")), + "STATION3": { + "test_point": _empty_to_none(_row_get(row, "STATION 3_Test Point")), + "channel": _empty_to_none(_row_get(row, "STATION 3_Channel")), + "bandwidth": _empty_to_none(_row_get(row, "STATION 3_Bandwidth")), + "rssi": _empty_to_none(_row_get(row, "STATION 3_RSSI")), + "direction": _empty_to_none(_row_get(row, "STATION 3_Direction")), + "rate": _empty_to_none(_row_get(row, "STATION 3_Rate")), + "sta": _empty_to_none(_row_get(row, "STATION 3_STA")), }, } diff --git a/backend/scheduler.py b/backend/scheduler.py index 46603d6..6832feb 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -2,592 +2,356 @@ from __future__ import annotations import os from dataclasses import dataclass -from datetime import date, datetime, timedelta -from typing import Any - -import graph - -# Bundle priority tiers for scheduling order (lower number = higher priority) -BUNDLE_PRIORITY_FAILED = 0 # Failed tests requiring rerun -BUNDLE_PRIORITY_P2P_WITH_COE = 1 # P2P tests with COE pairs -BUNDLE_PRIORITY_P2P_ONLY = 2 # P2P tests without COE pairs (RX/TX bundled) -BUNDLE_PRIORITY_COE_ONLY = 3 # COE tests without P2P pairing (should be rare/unschedulable) -BUNDLE_PRIORITY_P3P = 4 # P3P tests - -DUT = os.getenv("DUT", "CGW453").strip() -REF = os.getenv("REF", "CGW452").strip() -TestKey = tuple[str, str] - -@dataclass(frozen=True) -class SchedulerTest: - test_id: str - device: str - test_type: str - rotation: str | None - rx_tx: str | None - has_coe_pair: bool - coe_pairing: list[str] - config: dict[str, dict[str, str | None]] - throttled: bool - estimated_minutes: int - priority: int - raw_payload: dict[str, Any] +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 Test, TestBundle, build_test_bundles, bundle_pair_lookup +DUT = (os.getenv("DUT") or "DUT").strip() +REF = (os.getenv("REF") or "REF").strip() @dataclass(frozen=True) class ScheduleEntry: - test_id: str - device: str - scheduled_date: str - shift_index: int - sequence_in_shift: int - - -@dataclass(frozen=True) -class TestBundle: - """A bundle of tests that must be run together in sequence.""" - test_ids: list[TestKey] - priority_tier: int - total_minutes: int - - -# Global scheduler state. Graph is built once per loaded test set. -_TESTS_BY_ID: dict[TestKey, SchedulerTest] = {} -_ACTIVE_DUT: dict[TestKey, int] = {} -_ACTIVE_REF: dict[TestKey, int] = {} - - -def reset_scheduler_state() -> None: - global _TESTS_BY_ID, _ACTIVE_DUT, _ACTIVE_REF - _TESTS_BY_ID = {} - _ACTIVE_DUT = {} - _ACTIVE_REF = {} - - -def initialize_scheduler_state(tests: list[SchedulerTest]) -> None: - """Initialize active scheduler state once for the current loaded schedulable dataset.""" - global _TESTS_BY_ID, _ACTIVE_DUT, _ACTIVE_REF - if _TESTS_BY_ID: - return - - _TESTS_BY_ID = {(t.test_id, t.device): t for t in tests} - _ACTIVE_DUT = { - test_id: _derive_priority(test, set(), set(), tests) - for test_id, test in _TESTS_BY_ID.items() if test.device == DUT - } - _ACTIVE_REF = { - test_id: _derive_priority(test, set(), set(), tests) - for test_id, test in _TESTS_BY_ID.items() if test.device == REF - } - - -def set_user_priorities(top_priority_tests: set[str], lowest_priority_tests: set[str]) -> None: - """Update active priority map in-place using user overrides.""" - if not _TESTS_BY_ID: - return - - all_tests = list(_TESTS_BY_ID.values()) - for key in list(_ACTIVE_DUT.keys()): - test = _TESTS_BY_ID.get(key) - if test is None: - continue - _ACTIVE_DUT[key] = _derive_priority(test, top_priority_tests, lowest_priority_tests, all_tests) - - for key in list(_ACTIVE_REF.keys()): - test = _TESTS_BY_ID.get(key) - if test is None: - continue - _ACTIVE_REF[key] = _derive_priority(test, top_priority_tests, lowest_priority_tests, all_tests) - - -def remove_from_active(completed_test_ids: set[str]) -> None: - """Remove completed/invalid tests from active list without rebuilding graph.""" - for test_id in completed_test_ids: - _ACTIVE_DUT.pop((test_id, DUT), None) - _ACTIVE_REF.pop((test_id, REF), None) - - - -def compile_schedule( - tests: list[SchedulerTest], - start_date: str | None, - holiday_dates: set[str], - top_priority_tests: set[str], - lowest_priority_tests: set[str], - daytime_testing_today: bool, -) -> tuple[list[ScheduleEntry], str | None]: - """Compile an optimized schedule for the given tests. - - Uses the greedy algorithm respecting shift sequences per design: - - Mon-Thu: shift 3 (5pm-1am) then shift 1 next day (1am-10am) - - Friday: shift 3 through Monday shift 1 (4-day window) - Returns a list of ScheduleEntry objects and the completion date. - """ - if not tests: - return [], None - - initialize_scheduler_state(tests) - set_user_priorities(top_priority_tests, lowest_priority_tests) - - # Use a local working copy for this compile run. Global active remains until result processing removes IDs. - dut_active_priority: dict[TestKey, int] = dict(_ACTIVE_DUT) - ref_active_priority: dict[TestKey, int] = dict(_ACTIVE_REF) - entries: list[ScheduleEntry] = [] - - window_start_date = _parse_date(start_date) - current_date = window_start_date - last_date: str | None = None - daytime_shift2_window_pending = daytime_testing_today - - while dut_active_priority or ref_active_priority: - is_special_daytime_shift2_window = ( - daytime_shift2_window_pending - and current_date == window_start_date - and current_date.weekday() < 5 - and current_date.isoformat() not in holiday_dates - ) - - # Get the shift sequence for current date (respects Mon/Fri/weekend rules) - shift_sequence = _get_shift_sequence( - current_date, - holiday_dates, - daytime_shift2_only=is_special_daytime_shift2_window, - ) - - if not shift_sequence: - break # No valid shift sequence - - dut_active_test_ids: set[TestKey] = set(dut_active_priority.keys()) - ref_active_test_ids: set[TestKey] = set(ref_active_priority.keys()) - bundles = _create_bundles(dut_active_test_ids, ref_active_test_ids, _TESTS_BY_ID, top_priority_tests) - shift_capacities = { - (date_obj, shift_idx): _shift_capacity_for_date( - current_date=date_obj, - holiday_dates=holiday_dates, - daytime_shift2_only=is_special_daytime_shift2_window and date_obj == current_date, - ).get(shift_idx, 0) - for date_obj, shift_idx in shift_sequence - } - - placed_entries, placed_test_ids, placed_last_date = _fit_bundles_to_shifts( - bundles=bundles, - shift_sequence=shift_sequence, - tests=_TESTS_BY_ID, - graph_by_test_id=graph.get_graph(), - shift_capacities=shift_capacities, - ) - entries.extend(placed_entries) - for key in placed_test_ids: - dut_active_priority.pop(key, None) - ref_active_priority.pop(key, None) - if placed_last_date is not None: - last_date = placed_last_date - - # Daytime testing special handling applies only to the first scheduling window. - daytime_shift2_window_pending = False - - # Move to next scheduling window start date - # After shift 1 (1am-9am), there's shift 2 if daytime testing, then shift 3 (5pm) - # After shift 3 (5pm), shift 1 is next day (1am) - if shift_sequence: - last_sequence_date = shift_sequence[-1][0] - last_shift_index = shift_sequence[-1][1] - - if last_shift_index == 1: - # Last shift was 1 (1am-9am); next shift 3 is same day (5pm) - current_date = last_sequence_date - elif last_shift_index == 2: - # Last shift was 2 (9am-5pm); next shift 3 is same day (5pm) - current_date = last_sequence_date - else: # last_shift_index == 3 - # Last shift was 3 (5pm-1am); next shift 1 is next day (1am) - current_date = last_sequence_date + timedelta(days=1) - else: - current_date = current_date + timedelta(days=1) - - return entries, last_date - - -def format_schedule_for_frontend( - entries: list[ScheduleEntry], -) -> dict[str, dict[int, list[SchedulerTest]]]: - """Convert flat ScheduleEntry list to nested format for frontend. - - Returns: {date_string: {shift_index: [SchedulerTest, ...]}, ...} - """ - schedule: dict[str, dict[int, list[SchedulerTest]]] = {} - - for entry in entries: - if entry.scheduled_date not in schedule: - schedule[entry.scheduled_date] = {1: [], 2: [], 3: []} - - test = _TESTS_BY_ID.get((entry.test_id, entry.device)) - if test: - schedule[entry.scheduled_date][entry.shift_index].append(test) - - return schedule - - -def _derive_priority( - test: SchedulerTest, - top_priority_tests: set[str], - lowest_priority_tests: set[str], - all_tests: list[SchedulerTest], -) -> int: - if test.test_id in top_priority_tests: - return 1 - if test.test_id in lowest_priority_tests: - return 5 - return test.priority - - -def _fit_bundles_to_shifts( - bundles: list[TestBundle], - shift_sequence: list[tuple[date, int]], - tests: dict[TestKey, SchedulerTest], - graph_by_test_id: dict[str, set[str]], - shift_capacities: dict[tuple[date, int], int], -) -> tuple[list[ScheduleEntry], set[TestKey], str | None]: - """Fit bundles into a shift sequence window (e.g., one day or one weekend). - - Returns: - - List of ScheduleEntry for placed tests - - Set of test_ids that were placed - - Last scheduled date - - Bundles are placed in sequence order and may span multiple shifts inside the same window. - """ - entries: list[ScheduleEntry] = [] - placed_test_ids: set[TestKey] = set() - last_date: str | None = None - - # Track state per shift - shift_state: dict[tuple[date, int], dict] = {} - for date_shift in shift_sequence: - shift_state[date_shift] = { - 'remaining_minutes': shift_capacities.get(date_shift, 0), - 'sequence_counter': 1, - } - - shift_positions = {date_shift: idx for idx, date_shift in enumerate(shift_sequence)} - current_shift_pos = 0 - window_placed_test_ids: set[str] = set() - - for bundle in bundles: - # Skip bundles that are already fully placed. - if any(test_id in placed_test_ids for test_id in bundle.test_ids): - continue - - if not bundle.test_ids: - continue - - # Keep all tests in the same window pairwise-compatible. - if window_placed_test_ids: - is_compatible_with_window = True - bundle_test_ids_only = {test_key[0] for test_key in bundle.test_ids} - for placed_test_id in window_placed_test_ids: - placed_neighbors = graph_by_test_id.get(placed_test_id, set()) - if not all(current_test_id in placed_neighbors for current_test_id in bundle_test_ids_only): - is_compatible_with_window = False - break - if not is_compatible_with_window: - continue - - # Ensure the whole bundle can still fit somewhere in the remaining window. - remaining_window = sum( - shift_state[date_shift]['remaining_minutes'] for date_shift in shift_sequence[current_shift_pos:] - ) - if remaining_window < bundle.total_minutes: - continue - - bundle_start_shift_pos = current_shift_pos - placed_bundle_tests: list[tuple[TestKey, tuple[date, int]]] = [] - failed = False - - for key in bundle.test_ids: - test = tests[key] - prev_remaining = 0 - while current_shift_pos < len(shift_sequence): - date_shift = shift_sequence[current_shift_pos] - state = shift_state[date_shift] - state["remaining_minutes"] += prev_remaining # Add back any leftover from previous shift - if state['remaining_minutes'] >= test.estimated_minutes: - #print(f"remaining minutes for {date_shift}: {state['remaining_minutes']} - placing {key} ({test.estimated_minutes}m)") - entries.append( - ScheduleEntry( - test_id=test.test_id, - device=test.device, - scheduled_date=date_shift[0].isoformat(), - shift_index=date_shift[1], - sequence_in_shift=state['sequence_counter'], - ) - ) - state['sequence_counter'] += 1 - state['remaining_minutes'] -= test.estimated_minutes - placed_test_ids.add(key) - placed_bundle_tests.append((key, date_shift)) - last_date = date_shift[0].isoformat() - break - - # Move to the next shift in the sequence and keep the bundle contiguous. - current_shift_pos += 1 - prev_remaining = state['remaining_minutes'] - if current_shift_pos >= len(shift_sequence): - failed = True - break - - if failed: - break - - if failed: - # Roll back any partially placed tests from this bundle. - for key, date_shift in reversed(placed_bundle_tests): - state = shift_state[date_shift] - state['remaining_minutes'] += tests[key].estimated_minutes - state['sequence_counter'] -= 1 - entries.pop() - placed_test_ids.discard(key) - last_date = None - current_shift_pos = bundle_start_shift_pos - continue - - if placed_bundle_tests: - window_placed_test_ids.update(test_key[0] for test_key in bundle.test_ids) - - return entries, placed_test_ids, last_date - -def _create_bundles( - dut_active_test_ids: set[TestKey], - ref_active_test_ids: set[TestKey], - tests: dict[TestKey, SchedulerTest], - top_priority_tests: set[str] | None = None, -) -> list[TestBundle]: - """Create bundles of tests that must run together, sorted by priority tier and efficiency. - - Bundling rules: - - P2P with COE pairs: [P2P, COE1, COE2, ...] - - P2P without COE: [P2P_RX, P2P_TX] if both active - - COE without a linked active P2P: [COE] (standalone bundle) - - P3P: individual test (not bundled) - - Returned list is sorted by (priority_tier, total_minutes) to place small/high-priority bundles first. - """ - if top_priority_tests is None: - top_priority_tests = set() - - bundles: list[TestBundle] = [] - processed: set[TestKey] = set() - - dut_by_id: dict[str, TestKey] = {test_id: key for test_id, _device in dut_active_test_ids for key in [(test_id, DUT)] if key in dut_active_test_ids} - ref_by_id: dict[str, TestKey] = {test_id: key for test_id, _device in ref_active_test_ids for key in [(test_id, REF)] if key in ref_active_test_ids} - all_ids = sorted(set(dut_by_id.keys()) | set(ref_by_id.keys())) - - def _active_key(test_id: str, device: str) -> TestKey | None: - key = (test_id, device) - if key in dut_active_test_ids or key in ref_active_test_ids: - return key - return None - - for test_id in all_ids: - seed_keys = [key for key in (_active_key(test_id, DUT), _active_key(test_id, REF)) if key is not None and key not in processed] - if not seed_keys: - continue - - representative_key = seed_keys[0] - test = tests[representative_key] - if test.test_type == "P2P": - if test.has_coe_pair: - bundle_test_ids: list[TestKey] = [] - for device in (DUT, REF): - base_key = _active_key(test_id, device) - if base_key is not None and base_key not in processed: - bundle_test_ids.append(base_key) - for coe_id in sorted(test.coe_pairing): - coe_key = _active_key(coe_id, device) - if coe_key is not None and coe_key not in processed: - bundle_test_ids.append(coe_key) - - if not bundle_test_ids: - continue - processed.update(bundle_test_ids) - # Check if any test in bundle is top priority - bundle_test_ids_only = {key[0] for key in bundle_test_ids} - if bundle_test_ids_only & top_priority_tests: - priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0) - else: - priority_tier = BUNDLE_PRIORITY_P2P_WITH_COE - bundles.append(TestBundle( - test_ids=bundle_test_ids, - priority_tier=priority_tier, - total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids) - )) - else: - bundle_test_ids: list[TestKey] = [] - for device in (DUT, REF): - base_key = _active_key(test_id, device) - if base_key is None or base_key in processed: - continue - bundle_test_ids.append(base_key) - pair_id = _rx_tx_pair_id(test_id, set(k[0] for k in (dut_active_test_ids if device == DUT else ref_active_test_ids))) - if pair_id: - pair_key = _active_key(pair_id, device) - if pair_key is not None and pair_key not in processed and pair_key not in bundle_test_ids: - bundle_test_ids.append(pair_key) - - if not bundle_test_ids: - continue - processed.update(bundle_test_ids) - # Check if any test in bundle is top priority - bundle_test_ids_only = {key[0] for key in bundle_test_ids} - if bundle_test_ids_only & top_priority_tests: - priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0) - else: - priority_tier = BUNDLE_PRIORITY_P2P_ONLY - bundles.append(TestBundle( - test_ids=bundle_test_ids, - priority_tier=priority_tier, - total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids) - )) - elif test.test_type == "P3P": - bundle_test_ids = [key for key in (_active_key(test_id, DUT), _active_key(test_id, REF)) if key is not None and key not in processed] - if not bundle_test_ids: - continue - # Check if any test in bundle is top priority - bundle_test_ids_only = {key[0] for key in bundle_test_ids} - if bundle_test_ids_only & top_priority_tests: - priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0) - else: - priority_tier = BUNDLE_PRIORITY_P3P - bundles.append(TestBundle( - test_ids=bundle_test_ids, - priority_tier=priority_tier, - total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids) - )) - processed.update(bundle_test_ids) - - # Second pass to catch any active tests left uncovered by first-pass grouping. - all_active_keys = sorted(dut_active_test_ids | ref_active_test_ids) - for key in all_active_keys: - if key in processed: - continue - test = tests[key] - bundle_test_ids = [key] - # Check if this test is top priority - if key[0] in top_priority_tests: - priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0) - else: - priority_tier = BUNDLE_PRIORITY_COE_ONLY - bundles.append(TestBundle( - test_ids=bundle_test_ids, - priority_tier=priority_tier, - total_minutes=sum(tests[item].estimated_minutes for item in bundle_test_ids) - )) - processed.update(bundle_test_ids) - - # Sort by priority tier (lower first), then by total minutes (smaller first for efficiency) - bundles.sort(key=lambda b: (b.priority_tier, b.total_minutes)) - return bundles - - -def _rx_tx_pair_id(test_id: str, active: set[str]) -> str | None: - if "RX" in test_id: - candidate = test_id.replace("RX", "TX", 1) - return candidate if candidate in active else None - if "TX" in test_id: - candidate = test_id.replace("TX", "RX", 1) - return candidate if candidate in active else None - return None - - -def _shift_capacity_for_date( - current_date: date, - holiday_dates: set[str], - daytime_shift2_only: bool, -) -> dict[int, int]: - iso = current_date.isoformat() - if iso in holiday_dates: - return {1: 480, 2: 480, 3: 480} - - is_weekend = current_date.weekday() >= 5 - if is_weekend: - return {1: 480, 2: 480, 3: 480} - - if daytime_shift2_only: - return {1: 0, 2: 480, 3: 0} - - return {1: 600, 2: 0, 3: 420} - - -def _parse_date(value: str | None) -> date: - if not value: - return date.today() - return datetime.strptime(value, "%Y-%m-%d").date() - - -def _get_shift_sequence( - start_date: date, - holiday_dates: set[str], - daytime_shift2_only: bool = False, -) -> list[tuple[date, int]]: - """Generate the sequence of (date, shift_index) tuples for a scheduling window. - - Rules per design: - - Daytime testing first window (weekday only): [2] (shift 2 today only) - - Monday-Thursday: [3, 1] (shift 3 today, shift 1 next day) = 16 hours - - Friday: [3, 1, 2, 3, 1, 2, 3, 1] (Fri-Mon) = 24 hours continuous - - Saturday/Sunday/Holiday weekday: all 3 shifts - - Weekday default starts on shift 3 unless daytime testing shift-2-only is enabled. - Returns ordered list of (date, shift_index) pairs. - """ - iso = start_date.isoformat() - is_holiday = iso in holiday_dates - weekday = start_date.weekday() # 0=Mon, 4=Fri, 5=Sat, 6=Sun - - shifts: list[tuple[date, int]] = [] - - if daytime_shift2_only and weekday < 5 and not is_holiday: - shifts.append((start_date, 2)) - return shifts - - # If holiday on a weekday, treat as weekend (all 3 shifts) - if is_holiday and weekday < 5: - shifts.append((start_date, 1)) - shifts.append((start_date, 2)) - shifts.append((start_date, 3)) - return shifts - - # Standard weekend day (Sat/Sun) - if weekday >= 5: - shifts.append((start_date, 1)) - shifts.append((start_date, 2)) - shifts.append((start_date, 3)) - return shifts - - # Friday: 4-day window (Fri-Mon) - if weekday == 4: - shifts.append((start_date, 3)) # Fri shift 3 - - sat = start_date + timedelta(days=1) - shifts.append((sat, 1)) # Sat shift 1 - shifts.append((sat, 2)) # Sat shift 2 - shifts.append((sat, 3)) # Sat shift 3 - - sun = sat + timedelta(days=1) - shifts.append((sun, 1)) # Sun shift 1 - shifts.append((sun, 2)) # Sun shift 2 - shifts.append((sun, 3)) # Sun shift 3 - - mon = sun + timedelta(days=1) - shifts.append((mon, 1)) # Mon shift 1 - - return shifts - - # Monday-Thursday: 2-shift window - shifts.append((start_date, 3)) # Today shift 3 - next_day = start_date + timedelta(days=1) - shifts.append((next_day, 1)) # Tomorrow shift 1 - - return shifts + test_id: str + device: str + scheduled_date: str + shift_index: int + sequence_in_shift: int + + +@dataclass +class ScheduleWindow: + index: int + shifts: list[tuple[date, int]] + capacity_minutes: int + remaining_minutes: int + assigned_device: str | None = None + assigned_config: tuple[str, ...] | None = None + + +class Scheduler: + def __init__( + self, + tests: list[Test], + top_priority_tests: set[tuple[str, str]], + start_date: str | None = None, + holiday_dates: set[str] = set(), + 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, + ): + self.top_priority_tests = top_priority_tests + self.tests = tests + self.active_dut: dict[str, Test] = {test.test_id: test for test in tests if test.device == DUT} + self.active_ref: dict[str, Test] = {test.test_id: test for test in tests if test.device == REF} + self.start_date = date.fromisoformat(start_date) if start_date else date.today() + self.holiday_dates = holiday_dates + self.daytime_testing_today = daytime_testing_today + 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_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) + if not bundles: + return f"Error: No test bundles could be created from the provided 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]) + + # 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 + + # Sort TC keys by bundle count (largest first); tie-break by TC name. + sorted_tc_items = sorted( + [(tc, bundles_by_tc[tc]) for tc in bundles_by_tc if tc is not None], + key=lambda x: (-len(x[1]), x[0]), + ) + if None in bundles_by_tc: + sorted_tc_items.append((None, bundles_by_tc[None])) + + + for tc, tc_bundles in sorted_tc_items: + 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: + # 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 + + shifts, capacity = get_shift_sequence_with_capacity(cursor_date, self.holiday_dates, self.daytime_testing_today) + cursor_date_key = cursor_date.isoformat() + dual_device_window = cursor_date_key in self.dual_device_window_start_dates + 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 + + 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) + + + window = ScheduleWindow( + index=window_index, + shifts=shifts, + capacity_minutes=capacity, + remaining_minutes=remaining_time, + assigned_config=tc, + assigned_device=primary_device, + ) + + self._place_bundles_in_window(window, selected_bundles) + + # Mark bundles as scheduled and remove from ALL TC buckets + 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] + + # 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] + + window_index += 1 + window_device = REF if window_device == DUT else DUT + cursor_date = next_window_start_date(shifts) + + return None + + def _is_weekend_start_day(self, current_date: date) -> bool: + if is_off_day(current_date, self.holiday_dates): + return False + return is_off_day(current_date + timedelta(days=1), self.holiday_dates) + + def _select_bundles( + self, + device: str, + capacity: int, + unscheduled: list[TestBundle], + tc, + 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 + mirrored_bundles: list[TestBundle] = [] + still_pending: list[TestBundle] = [] # Bundles that couldn't fit in the remaining capacity + remaining_capacity = capacity + + for bundle in pending: + bundle_key = (bundle.index, bundle.device) + if bundle_key in self.scheduled_bundle_keys: + continue + if bundle.device != device or (tc is not None and tc not in bundle.config) or (tc is None and bundle.config): + still_pending.append(bundle) + continue + if bundle.total_minutes <= remaining_capacity: + mirrored_bundles.append(bundle) + remaining_capacity -= bundle.total_minutes + else: + still_pending.append(bundle) + + if mutate: + pending[:] = still_pending + + # Run knapsack selection for remaining capacity + candidates = [ + b + for b in unscheduled + if b not in mirrored_bundles + and b.device == device + and (b.index, b.device) not in self.scheduled_bundle_keys + ] + knapsack_bundles = self._knapsack_select(remaining_capacity, candidates) + remaining_capacity -= sum(bundle.total_minutes for bundle in knapsack_bundles) + + print(f"Selected {len(mirrored_bundles)} mirrored bundles and {len(knapsack_bundles)} knapsack bundles for device {device} with remaining capacity {remaining_capacity} minutes.") + return mirrored_bundles, knapsack_bundles, remaining_capacity + + def get_schedule(self) -> list[ScheduleEntry]: + return self.schedule + + def _knapsack_select( + self, + capacity: int, + candidates: list[TestBundle], + ) -> 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] + + # Implement dynamic programming knapsack algorithm to select bundles + n = len(candidates) + dp = [[0] * (capacity + 1) for _ in range(n + 1)] + for i in range(1, n + 1): + for w in range(capacity + 1): + if weights[i - 1] <= w: + dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]) + else: + dp[i][w] = dp[i - 1][w] + + # Traceback to find selected bundles + w = capacity + selected_indices = [] + for i in range(n, 0, -1): + if w <= 0: + break + if dp[i][w] != dp[i - 1][w]: + selected_indices.append(i - 1) + w -= weights[i - 1] + + # Return the selected bundles in the order they were added + return [candidates[i] for i in reversed(selected_indices)] + + def _place_bundles_in_window( + self, + window: ScheduleWindow, + bundles: list[TestBundle], + ) -> None: + # Build per-shift remaining capacity + shift_remaining = [ + get_shift_capacity_for_date(day, self.holiday_dates, self.daytime_testing_today).get(shift_idx, 0) + for day, shift_idx in window.shifts + ] + current_shift_pos = 0 + sequence_in_shift = [0] * len(window.shifts) + + for bundle in bundles: + bundle_key = (bundle.index, bundle.device) + if bundle_key in self.scheduled_bundle_keys: + continue + self.scheduled_bundle_keys.add(bundle_key) + + for test in bundle.tests: + schedule_key = f"{test}:{bundle.device}" + if schedule_key in self.scheduled_test_ids: + continue + self.scheduled_test_ids.add(schedule_key) + + test_minutes = self._get_test_minutes(test, bundle.device) + + while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] <= 0: + current_shift_pos += 1 + + if current_shift_pos >= len(window.shifts): + break + + if sum(shift_remaining[current_shift_pos:]) < test_minutes: + break + + start_shift_pos = current_shift_pos + shift_day, shift_idx = window.shifts[start_shift_pos] + sequence_in_shift[start_shift_pos] += 1 + + remaining_test_minutes = test_minutes + consume_shift_pos = start_shift_pos + while remaining_test_minutes > 0 and consume_shift_pos < len(window.shifts): + consumed_minutes = min(shift_remaining[consume_shift_pos], remaining_test_minutes) + shift_remaining[consume_shift_pos] -= consumed_minutes + remaining_test_minutes -= consumed_minutes + consume_shift_pos += 1 + + window.remaining_minutes -= test_minutes + + while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] <= 0: + current_shift_pos += 1 + + self.schedule.append( + ScheduleEntry( + test_id=test, + device=bundle.device, + scheduled_date=str(shift_day), + shift_index=shift_idx, + sequence_in_shift=sequence_in_shift[start_shift_pos], + ) + ) + + + def _get_test_minutes(self, test_id: str, device: str) -> int: + if device == DUT and test_id in self.active_dut: + return self.active_dut[test_id].estimated_minutes + if device == REF and test_id in self.active_ref: + return self.active_ref[test_id].estimated_minutes + return 0 + diff --git a/backend/test_bundle.py b/backend/test_bundle.py new file mode 100644 index 0000000..1b887f7 --- /dev/null +++ b/backend/test_bundle.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +from dataclasses import dataclass +import os +from test_config import build_bundle_test_configs + + +@dataclass(frozen=True) +class Test: + test_id: str + device: str + test_type: str + rotation: str | None + rx_tx: str | None + has_coe_pair: bool + coe_pairing: list[str] + config: dict[str, dict[str, str | None]] + throttled: bool + estimated_minutes: int + +# Bundle priority tiers for scheduling order (lower number = higher priority) +BUNDLE_PRIORITY_TOP = 0 # Failed tests requiring rerun +BUNDLE_PRIORITY_P2P_WITH_COE = 1 # P2P tests with COE pairs +BUNDLE_PRIORITY_P2P_ONLY = 2 # P2P tests without COE pairs (RX/TX bundled) +BUNDLE_PRIORITY_COE_ONLY = 3 # COE tests without P2P pairing (should be rare/unschedulable) +BUNDLE_PRIORITY_P3P = 4 # P3P tests + +DUT = (os.getenv("DUT") or "DUT").strip() +REF = (os.getenv("REF") or "REF").strip() + +@dataclass(frozen=True) +class TestBundle: + index: int + tests: list[str] + total_minutes: int + device: str + config: tuple[str, ...] + priority: int + +def build_test_bundles(active_dut: dict[str, Test], active_ref: dict[str, Test], top_priority_tests: set[tuple[str, str]]) -> list[TestBundle]: + """Build deterministic bundles for DP scheduling.""" + + processed_dut: set[str] = set() + processed_ref: set[str] = set() + test_bundles: list[TestBundle] = [] + bundle_index = 0 + + def dedupe_preserve_order(test_ids: list[str]) -> list[str]: + seen: set[str] = set() + deduped: list[str] = [] + for test_id in test_ids: + if test_id in seen: + continue + seen.add(test_id) + deduped.append(test_id) + return deduped + + dut_top_priority: list[str] = [] + ref_top_priority: list[str] = [] + + for test_id, device in top_priority_tests: + if device == DUT and test_id in active_dut: + dut_top_priority.append(test_id) + elif device == REF and test_id in active_ref: + ref_top_priority.append(test_id) + + if dut_top_priority: + test_bundles.append(create_bundle(dut_top_priority, DUT, BUNDLE_PRIORITY_TOP, bundle_index, active_dut, active_ref)) + processed_dut.update(dut_top_priority) + bundle_index += 1 + + if ref_top_priority: + test_bundles.append(create_bundle(ref_top_priority, REF, BUNDLE_PRIORITY_TOP, bundle_index, active_dut, active_ref)) + processed_ref.update(ref_top_priority) + bundle_index += 1 + + # Phase 1: DUT P2P bundles + for dut_test_id, dut_test in active_dut.items(): + if dut_test_id in processed_dut or dut_test.test_type != "P2P": + continue + + dut_bundled_tests = [dut_test_id] + processed_dut.add(dut_test_id) + + active_coe_pairings = [ + test_id + for test_id in dut_test.coe_pairing + if test_id in active_dut and test_id not in processed_dut + ] + + if active_coe_pairings: + dut_bundled_tests.extend(active_coe_pairings) + processed_dut.update(active_coe_pairings) + priority = BUNDLE_PRIORITY_P2P_WITH_COE + else: + rx_tx_pair_id = get_rx_tx_pair_id(dut_test, DUT, active_dut, active_ref) + if rx_tx_pair_id and rx_tx_pair_id not in processed_dut: + dut_bundled_tests.append(rx_tx_pair_id) + processed_dut.add(rx_tx_pair_id) + priority = BUNDLE_PRIORITY_P2P_ONLY + + dut_bundled_tests = dedupe_preserve_order(dut_bundled_tests) + test_bundles.append( + create_bundle(dut_bundled_tests, DUT, priority, bundle_index, active_dut, active_ref) + ) + + ref_bundled_tests = [ + test_id + for test_id in dut_bundled_tests + if test_id in active_ref and test_id not in processed_ref + ] + if ref_bundled_tests: + ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests) + processed_ref.update(ref_bundled_tests) + test_bundles.append( + create_bundle(ref_bundled_tests, REF, priority, bundle_index, active_dut, active_ref) + ) + + bundle_index += 1 + + # Phase 2: DUT P3P bundles + for dut_test_id, dut_test in active_dut.items(): + if dut_test_id in processed_dut or dut_test.test_type != "P3P": + continue + + dut_bundled_tests = [dut_test_id] + processed_dut.add(dut_test_id) + + th_ut_pair_id = get_th_ut_pair_id(dut_test, DUT, active_dut, active_ref) + if th_ut_pair_id and th_ut_pair_id not in processed_dut: + dut_bundled_tests.append(th_ut_pair_id) + processed_dut.add(th_ut_pair_id) + + dut_bundled_tests = dedupe_preserve_order(dut_bundled_tests) + test_bundles.append( + create_bundle(dut_bundled_tests, DUT, BUNDLE_PRIORITY_P3P, bundle_index, active_dut, active_ref) + ) + + ref_bundled_tests = [ + test_id + for test_id in dut_bundled_tests + if test_id in active_ref and test_id not in processed_ref + ] + if ref_bundled_tests: + ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests) + processed_ref.update(ref_bundled_tests) + test_bundles.append( + create_bundle(ref_bundled_tests, REF, BUNDLE_PRIORITY_P3P, bundle_index, active_dut, active_ref) + ) + + bundle_index += 1 + + # Phase 3: DUT leftovers (including COE-only) + for dut_test_id in active_dut: + if dut_test_id in processed_dut: + continue + test_bundles.append( + create_bundle([dut_test_id], DUT, BUNDLE_PRIORITY_COE_ONLY, bundle_index, active_dut, active_ref) + ) + bundle_index += 1 + processed_dut.add(dut_test_id) + + # Phase 4: unmatched REF P2P bundles + for ref_test_id, ref_test in active_ref.items(): + if ref_test_id in processed_ref or ref_test.test_type != "P2P": + continue + + ref_bundled_tests = [ref_test_id] + processed_ref.add(ref_test_id) + + active_coe_pairings = [ + test_id + for test_id in ref_test.coe_pairing + if test_id in active_ref and test_id not in processed_ref + ] + + if active_coe_pairings: + ref_bundled_tests.extend(active_coe_pairings) + processed_ref.update(active_coe_pairings) + priority = BUNDLE_PRIORITY_P2P_WITH_COE + else: + rx_tx_pair_id = get_rx_tx_pair_id(ref_test, REF, active_dut, active_ref) + if rx_tx_pair_id and rx_tx_pair_id not in processed_ref: + ref_bundled_tests.append(rx_tx_pair_id) + processed_ref.add(rx_tx_pair_id) + priority = BUNDLE_PRIORITY_P2P_ONLY + + ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests) + test_bundles.append( + create_bundle(ref_bundled_tests, REF, priority, bundle_index, active_dut, active_ref) + ) + bundle_index += 1 + + # Phase 5: unmatched REF P3P bundles + for ref_test_id, ref_test in active_ref.items(): + if ref_test_id in processed_ref or ref_test.test_type != "P3P": + continue + + ref_bundled_tests = [ref_test_id] + processed_ref.add(ref_test_id) + + th_ut_pair_id = get_th_ut_pair_id(ref_test, REF, active_dut, active_ref) + if th_ut_pair_id and th_ut_pair_id not in processed_ref: + ref_bundled_tests.append(th_ut_pair_id) + processed_ref.add(th_ut_pair_id) + + ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests) + test_bundles.append( + create_bundle(ref_bundled_tests, REF, BUNDLE_PRIORITY_P3P, bundle_index, active_dut, active_ref) + ) + bundle_index += 1 + + # Phase 6: REF leftovers (including COE-only) + for ref_test_id in active_ref: + if ref_test_id in processed_ref: + continue + test_bundles.append( + create_bundle([ref_test_id], REF, BUNDLE_PRIORITY_COE_ONLY, bundle_index, active_dut, active_ref) + ) + bundle_index += 1 + processed_ref.add(ref_test_id) + + test_bundles.sort(key=lambda b: (b.priority, b.index, b.device, b.config)) + return test_bundles + +def get_rx_tx_pair_id(test: Test, device: str, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> str | None: + active_tests = active_dut if device == DUT else active_ref + test_id = test.test_id + pair_id = test_id.replace("RX", "TX", 1) if "RX" in test_id else test_id.replace("TX", "RX", 1) + return pair_id if pair_id in active_tests else None + +def get_th_ut_pair_id(test: Test, device: str, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> str | None: + active_tests = active_dut if device == DUT else active_ref + test_id = test.test_id + pair_id = test_id.replace("TH", "UT", 1) if "TH" in test_id else test_id.replace("UT", "TH", 1) + return pair_id if pair_id in active_tests else None + +def create_bundle(tests: list[str], device: str, priority: int, index: int, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> TestBundle: + if not tests: + raise ValueError("Cannot create bundle with no tests") + + if device == DUT: + active_tests = active_dut + else: + active_tests = active_ref + + total_minutes = sum(active_tests[test_id].estimated_minutes for test_id in tests) + + bundle_tests = [active_tests[test_id] for test_id in tests] + config = tuple(build_bundle_test_configs(bundle_tests)) + + return TestBundle( + index=index, + tests=tests, + total_minutes=total_minutes, + device=device, + config=config, + priority=priority, + ) + +def bundle_pair_lookup(bundles: list[TestBundle], dut_bundles: list[TestBundle], ref_bundles: list[TestBundle]) -> list[TestBundle]: + device = bundles[0].device if bundles else None + active_bundle_ids = {b.index for b in dut_bundles} if device == REF else {b.index for b in ref_bundles} + + bundle_indexes = [b.index for b in bundles if b.index in active_bundle_ids] + if device == DUT: + return [b for b in ref_bundles if b.index in bundle_indexes] + elif device == REF: + return [b for b in dut_bundles if b.index in bundle_indexes] + return [] \ No newline at end of file diff --git a/backend/test_config.py b/backend/test_config.py new file mode 100644 index 0000000..178bb65 --- /dev/null +++ b/backend/test_config.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import json +from typing import Any + +TEST_CONFIG_DEFINITION = { + "TC1": {"T1D": "STA5", "T1F": "STA56", "T1I": "STA58", "T1L": "STA64", "T2A":"STA63", "T2O":"STA65", "T2J": "STA59", "T2E": "STA6", "T3E": "STA4"}, + "TC2": {"T1D": "STA64", "T1F": "STA4", "T1I": "STA5", "T1L": "STA58", "T2A":"STA56", "T2O":"STA59", "T2J": "STA6", "T2E": "STA65", "T3E": "STA63"}, + "TC3": {"T1D": "STA58", "T1F": "STA63", "T1I": "STA64", "T2J": "STA65", "T2E": "STA59", "T3E": "STA56"}, + "TC4": {"T1B": "STA56", "T1C": "STA4", "T1D": "STA63"}, + "TC5": {"T1B": "STA4", "T1C": "STA63", "T1D": "STA56"}, + "TC6": {"T1B": "STA63", "T1C": "STA56", "T1D": "STA4"}, + "TC7": {"T1F": "STA4", "T2A": "STA63", "T3E": "STA56"}, + "TC8": {"T1F": "STA63", "T2A": "STA56", "T3E": "STA4"}, + "TC9": {"T1F": "STA63", "T2A": "STA64", "T3E": "STA4"}, + "TC10": {"T1F": "STA4", "T3E": "STA56", "T1K2A": "STA63"}, + "TC11": {"T1F": "STA56", "T3E": "STA63", "T1K2A": "STA4"}, + "TC12": {"T1F": "STA56", "T3E": "STA4", "T1K2A": "STA63"}, +} + + + +def _norm(value: Any) -> str: + if value is None: + return "" + return " ".join(str(value).strip().upper().split()) + + +def _get_test_config(test: Any) -> dict[str, dict[str, str | None]]: + if isinstance(test, dict): + config = test.get("config") or {} + else: + config = getattr(test, "config", None) or {} + return config if isinstance(config, dict) else {} + + +def _testpoint_to_station_sets(config: dict[str, dict[str, str | None]]) -> dict[str, set[str]]: + result: dict[str, set[str]] = {} + + def _add_entry(entry: dict[str, str | None]) -> None: + testpoint = _norm(entry.get("test_point") or entry.get("Test Point")) + sta_raw = _norm(entry.get("sta") or entry.get("STA")) + if not testpoint or not sta_raw: + return + + stations = {_norm(sta) for sta in sta_raw.split(",") if _norm(sta)} + if not stations: + return + + result.setdefault(testpoint, set()).update(stations) + + for entry in config.values(): + if isinstance(entry, dict): + _add_entry(entry) + + return result + + +def build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> dict[str, str]: + """Build station->testpoint map used by legacy graph serialization.""" + station_to_testpoint: dict[str, str] = {} + for testpoint, stations in _testpoint_to_station_sets(config).items(): + for station in stations: + station_to_testpoint[station] = testpoint + return station_to_testpoint + + +def resolve_test_config_keys(config: dict[str, dict[str, str | None]]) -> list[str]: + """Return all TC keys whose definitions contain all testpoint->station mappings in config.""" + testpoint_stations = _testpoint_to_station_sets(config) + if not testpoint_stations: + return [] + + matches: list[str] = [] + for tc_key, tc_mapping in TEST_CONFIG_DEFINITION.items(): + is_match = True + for testpoint, stations in testpoint_stations.items(): + # TC definitions map one testpoint to exactly one station. + if len(stations) != 1: + is_match = False + break + expected_station = tc_mapping.get(testpoint) + if expected_station is None or expected_station not in stations: + is_match = False + break + if is_match: + matches.append(tc_key) + + return matches + + +def build_config_rows(tests: list[dict[str, Any]]) -> list[dict[str, Any]]: + by_sta: dict[str, set[str]] = {} + + for test in tests: + config = test.get("config") or {} + if not isinstance(config, dict): + continue + for entry in config.values(): + if not isinstance(entry, dict): + continue + sta_raw = entry.get("sta") or entry.get("STA") + test_point = entry.get("test_point") or entry.get("Test Point") + if not sta_raw or not test_point: + continue + + for sta_part in str(sta_raw).split(","): + sta = sta_part.strip().upper() + if not sta: + continue + by_sta.setdefault(sta, set()).add(str(test_point)) + + return [ + { + "sta": sta, + "testPoints": sorted(points), + } + for sta, points in sorted(by_sta.items()) + ] + + +def build_bundle_test_configs(tests: list[Any]) -> list[str]: + merged_by_testpoint: dict[str, set[str]] = {} + for test in tests: + config = _get_test_config(test) + for entry in config.values(): + if not isinstance(entry, dict): + continue + testpoint_raw = entry.get("test_point") or entry.get("Test Point") + sta_raw = entry.get("sta") or entry.get("STA") + if not testpoint_raw or not sta_raw: + continue + + testpoint = " ".join(str(testpoint_raw).strip().upper().split()) + if not testpoint: + continue + + stations = { + " ".join(str(sta).strip().upper().split()) + for sta in str(sta_raw).split(",") + if " ".join(str(sta).strip().upper().split()) + } + if not stations: + continue + + merged_by_testpoint.setdefault(testpoint, set()).update(stations) + + merged_config: dict[str, dict[str, str | None]] = {} + for idx, (testpoint, stations) in enumerate(sorted(merged_by_testpoint.items()), start=1): + merged_config[f"Station {idx}"] = { + "test_point": testpoint, + "sta": ",".join(sorted(stations)), + } + + return resolve_test_config_keys(merged_config) + + +def serialize_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> str: + return json.dumps(build_station_testpoint_map(config)) diff --git a/backend/test_window.py b/backend/test_window.py new file mode 100644 index 0000000..e21a890 --- /dev/null +++ b/backend/test_window.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from datetime import date, timedelta + + +def is_off_day(current_date: date, holiday_dates: set[str]) -> bool: + return current_date.weekday() >= 5 or current_date.isoformat() in holiday_dates + +def get_shift_sequence( + current_date: date, + holiday_dates: set[str], + daytime_shift2_only: bool = False, +) -> list[tuple[date, int]]: + is_holiday = current_date.isoformat() in holiday_dates + weekday = current_date.weekday() + shifts: list[tuple[date, int]] = [] + + if daytime_shift2_only and weekday < 5 and not is_holiday: + return [(current_date, 2)] + + if is_off_day(current_date, holiday_dates): + return [(current_date, 1), (current_date, 2), (current_date, 3)] + + shifts.append((current_date, 3)) + next_day = current_date + timedelta(days=1) + if is_off_day(next_day, holiday_dates): + cursor = next_day + while is_off_day(cursor, holiday_dates): + shifts.extend([(cursor, 1), (cursor, 2), (cursor, 3)]) + cursor += timedelta(days=1) + shifts.append((cursor, 1)) + return shifts + + shifts.append((next_day, 1)) + return shifts + + +def get_shift_capacity_for_date( + current_date: date, + holiday_dates: set[str], + daytime_shift2_only: bool = False, +) -> dict[int, int]: + if daytime_shift2_only: + return {1: 0, 2: 480, 3: 0} + if is_off_day(current_date, holiday_dates): + return {1: 480, 2: 480, 3: 480} + return {1: 480, 2: 0, 3: 480} + + +def get_shift_sequence_with_capacity( + current_date: date, + holiday_dates: set[str], + daytime_shift2_only: bool = False, +) -> tuple[list[tuple[date, int]], int]: + shifts = get_shift_sequence(current_date, holiday_dates, daytime_shift2_only) + capacity = sum( + get_shift_capacity_for_date(day, holiday_dates, daytime_shift2_only).get(shift_index, 0) + for day, shift_index in shifts + ) + return shifts, capacity + + +def next_window_start_date(shifts: list[tuple[date, int]]) -> date: + last_date, last_shift = shifts[-1] + if last_shift in (1, 2): + return last_date + return last_date + timedelta(days=1) diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..6cc8aa7 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +/data diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 46a3a61..9877bbc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,9 +1,10 @@ import { useState, useEffect, useCallback, useMemo } from 'react' -import Header from './components/Header' -import FailedBanner from './components/FailedBanner' -import Calendar from './components/Calendar' -import RightPanel from './components/RightPanel' -import SettingsModal from './components/SettingsModal' +import Header from './components/Header' +import FailedBanner from './components/FailedBanner' +import Calendar from './components/Calendar' +import RightPanel from './components/RightPanel' +import SettingsModal from './components/SettingsModal' +import TestWindowDetailsPanel from './components/TestWindowDetailsPanel' import { api, groupScheduleItems } from './api' // --------------------------------------------------------------------------- @@ -36,6 +37,33 @@ function addDays(date, n) { return d } +function parseHolidayDates(raw = '') { + return new Set( + String(raw) + .split(',') + .map((token) => token.trim()) + .filter(Boolean), + ) +} + +function isOffDay(date, holidaySet) { + const weekday = date.getDay() + if (weekday === 0 || weekday === 6) return true + return holidaySet.has(toKey(date)) +} + +function getWeekendStartDatesForWeek(weekStart, holidaySet) { + const dates = [] + for (let offset = 0; offset < 7; offset += 1) { + const current = addDays(weekStart, offset) + const nextDay = addDays(current, 1) + if (!isOffDay(current, holidaySet) && isOffDay(nextDay, holidaySet)) { + dates.push(toKey(current)) + } + } + return dates +} + function getTonightWindowKeys() { const now = new Date() const hour = now.getHours() @@ -99,17 +127,34 @@ const DEFAULT_SETTINGS = { smbUsername: '', smbPassword: '', smbDomain: '', - p2pRuntimeMinutes: '', - coeRuntimeMinutes: '', - p3pRuntimeMinutes: '', + dutP2pRuntimeMinutes: '', + dutCoeRuntimeMinutes: '', + dutP3pRuntimeMinutes: '', + refP2pRuntimeMinutes: '', + refCoeRuntimeMinutes: '', + refP3pRuntimeMinutes: '', testExclusion: '', holidays: '', } function sanitizeSettings(saved = {}) { - const { startDateOverride: _ignored, ...rest } = saved + const { + p2pRuntimeMinutes, + coeRuntimeMinutes, + p3pRuntimeMinutes, + ...rest + } = saved + + delete rest.startDateOverride + return { ...DEFAULT_SETTINGS, + dutP2pRuntimeMinutes: rest.dutP2pRuntimeMinutes ?? p2pRuntimeMinutes ?? '', + dutCoeRuntimeMinutes: rest.dutCoeRuntimeMinutes ?? coeRuntimeMinutes ?? '', + dutP3pRuntimeMinutes: rest.dutP3pRuntimeMinutes ?? p3pRuntimeMinutes ?? '', + refP2pRuntimeMinutes: rest.refP2pRuntimeMinutes ?? p2pRuntimeMinutes ?? '', + refCoeRuntimeMinutes: rest.refCoeRuntimeMinutes ?? coeRuntimeMinutes ?? '', + refP3pRuntimeMinutes: rest.refP3pRuntimeMinutes ?? p3pRuntimeMinutes ?? '', ...rest, } } @@ -119,23 +164,49 @@ function sanitizeSettings(saved = {}) { export default function App() { const [settingsOpen, setSettingsOpen] = useState(false) const [settings, setSettings] = useState(DEFAULT_SETTINGS) - const [daytimeEnabled, setDaytimeEnabled] = useState(false) + const [dualDeviceWeekendWeekSelections, setDualDeviceWeekendWeekSelections] = useState({}) const [topPriority, setTopPriority] = useState('') const [lowestPriority, setLowestPriority] = useState('') const [startDateOverride, setStartDateOverride] = useState('') - const [failedTests, setFailedTests] = useState([]) - const [scheduleData, setScheduleData] = useState({}) - const [completionDate, setCompletionDate] = useState(null) - const [weekStart, setWeekStart] = useState(() => getMondayOfWeek(new Date())) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) + const [failedTests, setFailedTests] = useState([]) + const [scheduleData, setScheduleData] = useState({}) + const [scheduleWindows, setScheduleWindows] = useState([]) + const [selectedWindowId, setSelectedWindowId] = useState(null) + const [completionDate, setCompletionDate] = useState(null) + const [weekStart, setWeekStart] = useState(() => getMondayOfWeek(new Date())) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) const tonightConfigRows = useMemo(() => toTonightConfigRows(scheduleData), [scheduleData]) + const windowLookup = useMemo(() => { + const lookup = new Map() + + for (const windowDetails of scheduleWindows) { + for (const segment of windowDetails.segments ?? []) { + lookup.set(`${segment.date}::shift${segment.shift_index}`, windowDetails) + } + } + + return lookup + }, [scheduleWindows]) + const selectedWindow = useMemo( + () => scheduleWindows.find((windowDetails) => windowDetails.window_id === selectedWindowId) ?? null, + [scheduleWindows, selectedWindowId], + ) + const holidaySet = useMemo(() => parseHolidayDates(settings.holidays), [settings.holidays]) + const activeWeekKey = useMemo(() => toKey(weekStart), [weekStart]) + const dualDeviceWeekendWeekEnabled = Boolean(dualDeviceWeekendWeekSelections[activeWeekKey]) + const dualDeviceWindowStartDates = useMemo(() => { + if (!dualDeviceWeekendWeekEnabled) return [] + return getWeekendStartDatesForWeek(weekStart, holidaySet) + }, [dualDeviceWeekendWeekEnabled, weekStart, holidaySet]) // Fetch schedule for the given weekStart (Monday) const fetchSchedule = useCallback(async (start) => { try { const data = await api.getScheduleWeek(toKey(start)) setScheduleData(groupScheduleItems(data.items)) + setScheduleWindows(data.windows ?? []) + setCompletionDate(data.completion_date ?? null) } catch (e) { console.error('Failed to fetch schedule:', e) } @@ -226,7 +297,7 @@ export default function App() { const effectiveStartDate = startDateOverride.trim() const result = await api.compileSchedule({ start_date: effectiveStartDate || null, - daytime_testing_today: daytimeEnabled, + dual_device_weekend_start_dates: dualDeviceWindowStartDates, top_priority_tests: topPriority.split(',').map(s => s.trim()).filter(Boolean), lowest_priority_tests: lowestPriority.split(',').map(s => s.trim()).filter(Boolean), rule: settings.testExclusion ?? '', @@ -254,20 +325,30 @@ export default function App() { } } - function handleRerunDecision(rerunDuringDay) { - const rerunIds = failedTests.map(t => t.test_id).join(', ') + function handleRerunDecision(_rerunDuringDay) { setTopPriority(prev => { const existing = prev.split(',').map(s => s.trim()).filter(Boolean) const incoming = failedTests.map(t => t.test_id) const merged = [...new Set([...existing, ...incoming])] return merged.join(', ') }) - if (rerunDuringDay) { - setDaytimeEnabled(true) - } setFailedTests([]) } + useEffect(() => { + if (selectedWindowId && !selectedWindow) { + setSelectedWindowId(null) + } + }, [selectedWindow, selectedWindowId]) + + function handleWindowSelect(windowDetails) { + setSelectedWindowId(windowDetails?.window_id ?? null) + } + + function handleCloseWindowDetails() { + setSelectedWindowId(null) + } + // ------------------------------------------------------------------------- return ( @@ -286,13 +367,22 @@ export default function App() { onDecision={handleRerunDecision} /> -
+
{ + setDualDeviceWeekendWeekSelections((prev) => ({ + ...prev, + [activeWeekKey]: enabled, + })) + }} + windowLookup={windowLookup} + onWindowSelect={handleWindowSelect} />
@@ -300,8 +390,6 @@ export default function App() { completionDate={completionDate} startDateOverride={startDateOverride} onStartDateOverrideChange={setStartDateOverride} - daytimeEnabled={daytimeEnabled} - onDaytimeEnabledChange={setDaytimeEnabled} topPriority={topPriority} onTopPriorityChange={setTopPriority} lowestPriority={lowestPriority} @@ -310,6 +398,12 @@ export default function App() { loading={loading} tonightConfigRows={tonightConfigRows} /> + +