knapsack scheduling algorithm
This commit is contained in:
+210
-43
@@ -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")
|
||||
|
||||
@@ -80,10 +87,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 +317,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 +339,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 +360,36 @@ 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,
|
||||
)
|
||||
|
||||
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 +430,14 @@ 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)
|
||||
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],
|
||||
"windows": _build_schedule_windows(week_start_date, all_rows, holiday_dates),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
|
+79
-78
@@ -6,7 +6,7 @@ from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
from graph import build_station_testpoint_map
|
||||
from test_config import serialize_station_testpoint_map
|
||||
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parent
|
||||
@@ -123,13 +123,6 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
|
||||
minutes INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS graph_cache (
|
||||
name TEXT PRIMARY KEY,
|
||||
payload_json TEXT NOT NULL,
|
||||
test_count INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rerun_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
detected_date TEXT NOT NULL,
|
||||
@@ -166,14 +159,38 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
|
||||
|
||||
def _serialize_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> str:
|
||||
"""Compute and serialize the station-to-testpoint map from a test config."""
|
||||
station_map = build_station_testpoint_map(config)
|
||||
return json.dumps(station_map)
|
||||
return serialize_station_testpoint_map(config)
|
||||
|
||||
|
||||
def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> int:
|
||||
if not records:
|
||||
return 0
|
||||
|
||||
values: list[tuple[Any, ...]] = []
|
||||
for r in records:
|
||||
station_testpoint_map = _serialize_station_testpoint_map(r.config)
|
||||
|
||||
values.append(
|
||||
(
|
||||
r.test_id,
|
||||
r.device,
|
||||
r.test_type,
|
||||
r.rotation,
|
||||
r.rx_tx,
|
||||
int(r.has_coe_pair),
|
||||
json.dumps(r.coe_pairing or []),
|
||||
json.dumps(r.config),
|
||||
r.priority,
|
||||
r.victim_band,
|
||||
int(r.throttled),
|
||||
r.estimated_minutes,
|
||||
r.status,
|
||||
int(r.excluded),
|
||||
json.dumps(r.raw_payload or {}),
|
||||
station_testpoint_map,
|
||||
)
|
||||
)
|
||||
|
||||
with get_connection(db_path) as conn:
|
||||
conn.executemany(
|
||||
"""
|
||||
@@ -200,27 +217,7 @@ def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> in
|
||||
station_testpoint_map = excluded.station_testpoint_map,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
[
|
||||
(
|
||||
r.test_id,
|
||||
r.device,
|
||||
r.test_type,
|
||||
r.rotation,
|
||||
r.rx_tx,
|
||||
int(r.has_coe_pair),
|
||||
json.dumps(r.coe_pairing or []),
|
||||
json.dumps(r.config),
|
||||
r.priority,
|
||||
r.victim_band,
|
||||
int(r.throttled),
|
||||
r.estimated_minutes,
|
||||
r.status,
|
||||
int(r.excluded),
|
||||
json.dumps(r.raw_payload or {}),
|
||||
_serialize_station_testpoint_map(r.config),
|
||||
)
|
||||
for r in records
|
||||
],
|
||||
values,
|
||||
)
|
||||
return len(records)
|
||||
|
||||
@@ -253,52 +250,6 @@ def read_settings(db_path: str | Path = DB_PATH) -> dict[str, Any]:
|
||||
return {row["key"]: json.loads(row["value_json"]) for row in rows}
|
||||
|
||||
|
||||
def save_graph_cache(
|
||||
name: str,
|
||||
payload: dict[str, list[str]],
|
||||
test_count: int,
|
||||
db_path: str | Path = DB_PATH,
|
||||
) -> None:
|
||||
with get_connection(db_path) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO graph_cache(name, payload_json, test_count, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
payload_json = excluded.payload_json,
|
||||
test_count = excluded.test_count,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
(name, json.dumps(payload), int(test_count)),
|
||||
)
|
||||
|
||||
|
||||
def load_graph_cache(name: str, db_path: str | Path = DB_PATH) -> dict[str, Any] | None:
|
||||
with get_connection(db_path) as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT payload_json, test_count, updated_at
|
||||
FROM graph_cache
|
||||
WHERE name = ?
|
||||
""",
|
||||
(name,),
|
||||
).fetchone()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"payload": json.loads(row["payload_json"] or "{}"),
|
||||
"test_count": int(row["test_count"]),
|
||||
"updated_at": row["updated_at"],
|
||||
}
|
||||
|
||||
|
||||
def delete_graph_cache(name: str, db_path: str | Path = DB_PATH) -> None:
|
||||
with get_connection(db_path) as conn:
|
||||
conn.execute("DELETE FROM graph_cache WHERE name = ?", (name,))
|
||||
|
||||
|
||||
def _parse_rule_tokens(rule: str | None) -> list[str]:
|
||||
if not rule:
|
||||
return []
|
||||
@@ -345,7 +296,7 @@ def _match_any_band_value(record: TestRecord, key: str, token_suffix: str) -> bo
|
||||
|
||||
target_num = _extract_signed_int(token_suffix)
|
||||
target_compact = _normalize_compact(token_suffix)
|
||||
keys = ("Station 1", "Station 2", "Station 3") if record.test_type == "P3P" else ("5G", "6G", "2G")
|
||||
keys = ("STATION 1", "STATION 2", "STATION 3") if record.test_type == "P3P" else ("5G", "6G", "2G")
|
||||
|
||||
for band in keys:
|
||||
entry = _band_entry(record.config, band)
|
||||
@@ -579,6 +530,56 @@ def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[Sc
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_schedule_rows_for_latest_version(db_path: str | Path = DB_PATH) -> list[ScheduleRow]:
|
||||
with get_connection(db_path) as conn:
|
||||
version_row = conn.execute("SELECT MAX(schedule_version) AS latest FROM schedules").fetchone()
|
||||
latest = version_row["latest"]
|
||||
if latest is None:
|
||||
return []
|
||||
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT
|
||||
s.test_id,
|
||||
s.device,
|
||||
s.scheduled_date,
|
||||
s.shift_index,
|
||||
s.sequence_in_shift,
|
||||
t.test_type,
|
||||
t.rotation,
|
||||
t.config_json,
|
||||
t.status,
|
||||
t.priority,
|
||||
t.estimated_minutes
|
||||
FROM schedules s
|
||||
JOIN tests t ON t.test_id = s.test_id AND t.device = s.device
|
||||
WHERE s.schedule_version = ?
|
||||
ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift
|
||||
""",
|
||||
(latest,),
|
||||
).fetchall()
|
||||
|
||||
result: list[ScheduleRow] = []
|
||||
for row in rows:
|
||||
result.append(
|
||||
ScheduleRow(
|
||||
test_id=row["test_id"],
|
||||
device=row["device"],
|
||||
scheduled_date=row["scheduled_date"],
|
||||
shift_index=int(row["shift_index"]),
|
||||
sequence_in_shift=int(row["sequence_in_shift"]),
|
||||
test_type=row["test_type"],
|
||||
rotation=row["rotation"],
|
||||
config=json.loads(row["config_json"] or "{}"),
|
||||
status=row["status"],
|
||||
priority=int(row["priority"]),
|
||||
estimated_minutes=int(row["estimated_minutes"]),
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def mark_tests_completed(test_ids_with_device: list[tuple[str, str]], db_path: str | Path = DB_PATH) -> None:
|
||||
if not test_ids_with_device:
|
||||
return
|
||||
|
||||
@@ -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())
|
||||
+95
-87
@@ -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,7 +230,6 @@ 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,
|
||||
@@ -476,11 +475,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 +523,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")),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+283
-607
@@ -2,616 +2,292 @@ 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
|
||||
from test_window import get_shift_sequence_with_capacity, 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
|
||||
date_device_lock: dict[str, str] = {}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
forced_device: str | None = None
|
||||
for date_obj, _shift_idx in shift_sequence:
|
||||
locked_device = date_device_lock.get(date_obj.isoformat())
|
||||
if locked_device is None:
|
||||
continue
|
||||
if forced_device is None:
|
||||
forced_device = locked_device
|
||||
elif forced_device != locked_device:
|
||||
raise ValueError(f"Conflicting device locks in shift sequence for {date_obj.isoformat()}")
|
||||
|
||||
placed_entries, placed_test_ids, placed_last_date, window_device = _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,
|
||||
forced_device=forced_device,
|
||||
)
|
||||
entries.extend(placed_entries)
|
||||
if window_device is not None:
|
||||
for date_obj, _shift_idx in shift_sequence:
|
||||
date_device_lock.setdefault(date_obj.isoformat(), window_device)
|
||||
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],
|
||||
forced_device: str | None = None,
|
||||
) -> tuple[list[ScheduleEntry], set[TestKey], str | None, 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
|
||||
- Device used for this window, if any bundle was placed
|
||||
|
||||
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
|
||||
window_device: 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()
|
||||
window_placed_devices: 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
|
||||
|
||||
bundle_devices = {test_key[1] for test_key in bundle.test_ids}
|
||||
if len(bundle_devices) > 1:
|
||||
# A bundle cannot span devices because each window is single-device only.
|
||||
continue
|
||||
if forced_device is not None and forced_device not in bundle_devices:
|
||||
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
|
||||
|
||||
if window_placed_devices and not bundle_devices.issubset(window_placed_devices):
|
||||
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)
|
||||
window_placed_devices.update(bundle_devices)
|
||||
if window_device is None:
|
||||
window_device = next(iter(bundle_devices))
|
||||
|
||||
return entries, placed_test_ids, last_date, window_device
|
||||
|
||||
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:
|
||||
for device in (DUT, REF):
|
||||
bundle_test_ids: list[TestKey] = []
|
||||
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:
|
||||
for device in (DUT, REF):
|
||||
bundle_test_ids: list[TestKey] = []
|
||||
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)
|
||||
|
||||
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":
|
||||
for device in (DUT, REF):
|
||||
key = _active_key(test_id, device)
|
||||
if key is None or key in processed:
|
||||
continue
|
||||
bundle_test_ids = [key]
|
||||
# Check if any test in bundle is top priority
|
||||
if key[0] in 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[item].estimated_minutes for item 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 _is_off_day(day: date, holiday_dates: set[str]) -> bool:
|
||||
"""Return True for weekend days and configured holidays."""
|
||||
return day.weekday() >= 5 or day.isoformat() in holiday_dates
|
||||
|
||||
|
||||
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
|
||||
|
||||
# Any off-day (weekend/holiday) uses full daytime+night shifts for that date.
|
||||
if _is_off_day(start_date, holiday_dates):
|
||||
shifts.append((start_date, 1))
|
||||
shifts.append((start_date, 2))
|
||||
shifts.append((start_date, 3))
|
||||
return shifts
|
||||
|
||||
# Working-day windows always start at shift 3.
|
||||
shifts.append((start_date, 3))
|
||||
next_day = start_date + timedelta(days=1)
|
||||
|
||||
# If tomorrow starts an off-day chain (holiday/weekend), extend the window
|
||||
# through all off-days and end at shift 1 of the next working day.
|
||||
if _is_off_day(next_day, holiday_dates):
|
||||
cursor = next_day
|
||||
while _is_off_day(cursor, holiday_dates):
|
||||
shifts.append((cursor, 1))
|
||||
shifts.append((cursor, 2))
|
||||
shifts.append((cursor, 3))
|
||||
cursor += timedelta(days=1)
|
||||
shifts.append((cursor, 1))
|
||||
return shifts
|
||||
|
||||
# Default working-day pair: tonight shift 3 + next day shift 1.
|
||||
shifts.append((next_day, 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,
|
||||
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.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)
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# 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}
|
||||
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:
|
||||
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=window_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 _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:
|
||||
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 idx, test in enumerate(bundle.tests):
|
||||
schedule_key = f"{test}:{bundle.device}"
|
||||
if schedule_key in self.scheduled_test_ids:
|
||||
continue
|
||||
self.scheduled_test_ids.add(schedule_key)
|
||||
|
||||
self.schedule.append(
|
||||
ScheduleEntry(
|
||||
test_id=test,
|
||||
device=bundle.device,
|
||||
scheduled_date=str(window.shifts[0][0]),
|
||||
shift_index=window.shifts[0][1],
|
||||
sequence_in_shift=idx + 1,
|
||||
)
|
||||
)
|
||||
window.remaining_minutes -= self._get_test_minutes(test, bundle.device)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 []
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user