Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,17 +11,17 @@
|
|
| 11 |
# - Guided validation timestamps: 18:22, 18:40, 19:40, 20:40, 21:40, 22:40, 00:10, 00:20
|
| 12 |
#
|
| 13 |
# THIS VERSION:
|
| 14 |
-
# - Guided solver is BTO-first "ring-fit"
|
| 15 |
# - IMPORTANT: If a timestamp has no measured BTO (e.g. 18:40 in your satLoc),
|
| 16 |
# the solver automatically does NOT enforce BTO there.
|
| 17 |
# - Turn speed range: 470–520 kt
|
| 18 |
# - Southern segment speed range: 430–510 kt (per segment)
|
| 19 |
# - Waypoints at ping times are driven to ping rings for those timestamps with measured BTO.
|
| 20 |
#
|
| 21 |
-
#
|
| 22 |
-
# -
|
| 23 |
-
#
|
| 24 |
-
#
|
| 25 |
|
| 26 |
import os
|
| 27 |
import re
|
|
@@ -132,10 +132,13 @@ BTO_TARGET_TIGHT_US = 1.0 # purely informational
|
|
| 132 |
# HARD BTO labels list (will be AUTO-FILTERED to those with measured BTO)
|
| 133 |
HARD_BTO_LABELS = ["18:40:00", "19:40:00", "20:40:00", "21:40:00", "22:40:00", "24:10:00", "24:20:00"]
|
| 134 |
|
| 135 |
-
# =====
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
# Search area colors (optional)
|
| 141 |
SEARCH_AREA_COLORS = {
|
|
@@ -1233,118 +1236,127 @@ def _build_south_legs_piecewise(h_south: float, speeds: list[float], minutes: li
|
|
| 1233 |
return legs
|
| 1234 |
|
| 1235 |
|
| 1236 |
-
def
|
| 1237 |
method: str,
|
| 1238 |
-
|
| 1239 |
-
lon0: float,
|
| 1240 |
-
hdg: float,
|
| 1241 |
-
speeds: list[float],
|
| 1242 |
-
minutes: list[float],
|
| 1243 |
-
):
|
| 1244 |
-
lat, lon = float(lat0), float(lon0)
|
| 1245 |
-
endpoints = []
|
| 1246 |
-
for v, m in zip(speeds, minutes):
|
| 1247 |
-
lat, lon = _simulate_one_leg_end(method, lat, lon, float(hdg), float(v), float(m))
|
| 1248 |
-
endpoints.append((float(lat), float(lon)))
|
| 1249 |
-
return endpoints
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
def _bto_errors_at_south_endpoints(
|
| 1253 |
-
method: str,
|
| 1254 |
-
endpoints: list[tuple[float, float]],
|
| 1255 |
alt_ft: float,
|
| 1256 |
vs_fpm: float,
|
| 1257 |
-
start_dt: datetime,
|
| 1258 |
descent_start_dt: datetime | None,
|
|
|
|
|
|
|
| 1259 |
):
|
| 1260 |
errs = []
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
dt_end = _dt_from_label(end_lbl)
|
| 1264 |
-
meas = _lookup_measured_bto(dt_end)
|
| 1265 |
if meas is None:
|
| 1266 |
continue
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
def _bto_obj_from_errs(errs: list[float]):
|
| 1274 |
-
if not errs:
|
| 1275 |
-
return None
|
| 1276 |
-
arr = np.array(errs, dtype=float)
|
| 1277 |
-
max_abs = float(np.max(np.abs(arr)))
|
| 1278 |
-
sum_abs = float(np.sum(np.abs(arr)))
|
| 1279 |
-
rmse = float(np.sqrt(np.mean(arr * arr)))
|
| 1280 |
-
# primary max abs, secondary sum abs
|
| 1281 |
-
score = float(max_abs) + 1e-3 * float(sum_abs)
|
| 1282 |
-
return score, max_abs, sum_abs, rmse
|
| 1283 |
|
| 1284 |
|
| 1285 |
-
def
|
| 1286 |
method: str,
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
hdg: float,
|
| 1290 |
-
speeds_init: list[float],
|
| 1291 |
alt_ft: float,
|
| 1292 |
vs_fpm: float,
|
| 1293 |
-
start_dt: datetime,
|
| 1294 |
descent_start_dt: datetime | None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1295 |
):
|
| 1296 |
-
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
| 1301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1302 |
|
| 1303 |
-
|
| 1304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
|
| 1306 |
-
|
| 1307 |
-
|
|
|
|
| 1308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1309 |
for i in range(len(speeds)):
|
| 1310 |
-
v0 = float(
|
| 1311 |
-
v_lo = _clamp(v0 -
|
| 1312 |
-
v_hi = _clamp(v0 +
|
| 1313 |
|
| 1314 |
-
|
| 1315 |
-
|
|
|
|
| 1316 |
continue
|
| 1317 |
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
if
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
improved_any = True
|
| 1340 |
|
| 1341 |
-
if not
|
| 1342 |
break
|
| 1343 |
|
| 1344 |
-
|
| 1345 |
-
final_endpoints = _simulate_south_endpoints(method, lat0, lon0, hdg, speeds, GUIDED_SOUTH_SEG_MIN)
|
| 1346 |
-
_lbls3, final_errs = _bto_errors_at_south_endpoints(method, final_endpoints, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1347 |
-
return speeds, final_errs, {"polish_passes": int(POLISH_PASSES), "improved": bool(improved_any)}
|
| 1348 |
|
| 1349 |
|
| 1350 |
# ================= Guided Solver =================
|
|
@@ -1372,18 +1384,22 @@ def solve_guided_straight(
|
|
| 1372 |
|
| 1373 |
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1374 |
|
| 1375 |
-
def
|
|
|
|
|
|
|
|
|
|
| 1376 |
# Build turn legs
|
| 1377 |
turn_legs, turn_info = build_turn_legs(
|
| 1378 |
speed_knots=float(v_turn),
|
| 1379 |
method=method,
|
| 1380 |
heading_final_deg=float(h_south),
|
| 1381 |
)
|
|
|
|
|
|
|
| 1382 |
pts_turn, _, _ = simulate_path(RADAR_FIX_LAT, RADAR_FIX_LON, turn_legs, method)
|
| 1383 |
-
|
| 1384 |
|
| 1385 |
-
# Fit south segments sequentially (
|
| 1386 |
-
lat_cur, lon_cur = float(lat_turn_end), float(lon_turn_end)
|
| 1387 |
south_speeds = []
|
| 1388 |
south_errs = []
|
| 1389 |
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
|
@@ -1406,68 +1422,68 @@ def solve_guided_straight(
|
|
| 1406 |
south_errs.append(float(err_seg))
|
| 1407 |
lat_cur, lon_cur = float(lat1), float(lon1)
|
| 1408 |
|
| 1409 |
-
#
|
| 1410 |
-
|
| 1411 |
-
method=method,
|
| 1412 |
-
lat0=float(lat_turn_end),
|
| 1413 |
-
lon0=float(lon_turn_end),
|
| 1414 |
-
hdg=float(h_south),
|
| 1415 |
-
speeds_init=south_speeds,
|
| 1416 |
-
alt_ft=alt_ft,
|
| 1417 |
-
vs_fpm=vs_fpm,
|
| 1418 |
-
start_dt=start_dt,
|
| 1419 |
-
descent_start_dt=descent_start_dt,
|
| 1420 |
-
)
|
| 1421 |
-
|
| 1422 |
-
# Build full legs with polished speeds
|
| 1423 |
-
south_legs = _build_south_legs_piecewise(float(h_south), south_speeds_pol, GUIDED_SOUTH_SEG_MIN)
|
| 1424 |
full_legs = clip_legs_to_end_dt(turn_legs + south_legs, start_dt, end_dt)
|
| 1425 |
|
| 1426 |
-
#
|
| 1427 |
-
|
| 1428 |
-
|
| 1429 |
-
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
|
| 1436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1437 |
|
|
|
|
|
|
|
| 1438 |
if not bto_errs:
|
| 1439 |
return None
|
| 1440 |
|
| 1441 |
-
|
| 1442 |
-
|
| 1443 |
-
|
| 1444 |
-
|
| 1445 |
|
| 1446 |
details = {
|
| 1447 |
"h_south": float(h_south),
|
| 1448 |
"v_turn": float(v_turn),
|
| 1449 |
-
"south_speeds": [float(v) for v in
|
| 1450 |
-
"south_errs_us":
|
| 1451 |
"turn_info": turn_info,
|
| 1452 |
"enforced_labels": enforced_labels,
|
| 1453 |
-
"max_abs_err_us":
|
| 1454 |
-
"sum_abs_err_us":
|
| 1455 |
-
"rmse_bto_us":
|
| 1456 |
-
"
|
| 1457 |
}
|
| 1458 |
-
|
|
|
|
|
|
|
| 1459 |
|
| 1460 |
def best_for_heading(h_south: float):
|
| 1461 |
h_south = float(h_south)
|
| 1462 |
|
| 1463 |
-
# Coarse scan turn speed
|
| 1464 |
coarse_turns = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, 5.0))
|
| 1465 |
best_key = None
|
| 1466 |
best_legs = None
|
| 1467 |
best_details = None
|
| 1468 |
|
| 1469 |
for v in coarse_turns:
|
| 1470 |
-
out =
|
| 1471 |
if out is None:
|
| 1472 |
continue
|
| 1473 |
key, legs, det = out
|
|
@@ -1477,20 +1493,20 @@ def solve_guided_straight(
|
|
| 1477 |
if best_key is None:
|
| 1478 |
return None
|
| 1479 |
|
| 1480 |
-
# Refine around best coarse speed
|
| 1481 |
v0 = float(best_details["v_turn"])
|
| 1482 |
v_lo = _clamp(v0 - 7.5, TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT)
|
| 1483 |
v_hi = _clamp(v0 + 7.5, TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT)
|
| 1484 |
|
| 1485 |
def obj(v):
|
| 1486 |
-
out2 =
|
| 1487 |
if out2 is None:
|
| 1488 |
return 1e9
|
| 1489 |
(max_abs, sum_abs, _rmse), _legs, _det = out2
|
| 1490 |
-
return float(max_abs) +
|
| 1491 |
|
| 1492 |
-
v_best, _ = _golden_min_abs(obj, v_lo, v_hi, iters=
|
| 1493 |
-
out3 =
|
| 1494 |
if out3 is None:
|
| 1495 |
return best_key, best_legs, best_details
|
| 1496 |
key3, legs3, det3 = out3
|
|
@@ -1502,11 +1518,15 @@ def solve_guided_straight(
|
|
| 1502 |
global_legs = None
|
| 1503 |
global_det = None
|
| 1504 |
|
|
|
|
|
|
|
|
|
|
| 1505 |
for h in coarse_headings:
|
| 1506 |
out = best_for_heading(h)
|
| 1507 |
if out is None:
|
| 1508 |
continue
|
| 1509 |
key, legs, det = out
|
|
|
|
| 1510 |
if global_best is None or key < global_best:
|
| 1511 |
global_best, global_legs, global_det = key, legs, det
|
| 1512 |
|
|
@@ -1522,38 +1542,69 @@ def solve_guided_straight(
|
|
| 1522 |
if out is None:
|
| 1523 |
continue
|
| 1524 |
key, legs, det = out
|
|
|
|
| 1525 |
if global_best is None or key < global_best:
|
| 1526 |
global_best, global_legs, global_det = key, legs, det
|
| 1527 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1528 |
# Reporting
|
| 1529 |
-
south_speeds =
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
|
| 1533 |
report_lines = [
|
| 1534 |
"**Guided mode: Straight + computed turn (18:22→18:40), clipped to 00:20**",
|
| 1535 |
-
"- Solver mode: **BTO-first ring-fit
|
| 1536 |
-
f"- Best southern heading: **{
|
| 1537 |
-
f"- Turn speed range: {TURN_SPEED_MIN_KT:.0f}–{TURN_SPEED_MAX_KT:.0f} kt | Best: **{
|
| 1538 |
-
"",
|
| 1539 |
-
f"**Polish:** passes={POLISH_PASSES}, window=±{POLISH_WINDOW_KT:.0f} kt, step={POLISH_STEP_KT:.0f} kt | improved={polish_info.get('improved', False)}",
|
| 1540 |
"",
|
| 1541 |
"**Southern per-segment speeds (each constrained to 430–510 kt):**",
|
| 1542 |
-
f"- 18:40→19:40: **{south_speeds[0]:.2f} kt** (
|
| 1543 |
-
f"- 19:40→20:40: **{south_speeds[1]:.2f} kt** (
|
| 1544 |
-
f"- 20:40→21:40: **{south_speeds[2]:.2f} kt** (
|
| 1545 |
-
f"- 21:40→22:40: **{south_speeds[3]:.2f} kt** (
|
| 1546 |
-
f"- 22:40→00:10: **{south_speeds[4]:.2f} kt** (
|
| 1547 |
-
f"- 00:10→00:20: **{south_speeds[5]:.2f} kt** (
|
| 1548 |
"",
|
| 1549 |
-
f"- Enforced BTO timestamps (measured available): **{', '.join(
|
| 1550 |
-
f"- Max |BTO error| at enforced times: **{
|
| 1551 |
-
f"- RMSE_BTO at enforced times: **{
|
| 1552 |
"",
|
| 1553 |
"*If 18:40 has no measured BTO in your satLoc, it is automatically not enforced.*",
|
|
|
|
| 1554 |
]
|
| 1555 |
|
| 1556 |
-
return
|
| 1557 |
|
| 1558 |
|
| 1559 |
# ================= RUNNERS =================
|
|
@@ -1651,7 +1702,7 @@ with gr.Blocks() as demo:
|
|
| 1651 |
f"**Turn speed bounds:** {TURN_SPEED_MIN_KT:.0f}–{TURN_SPEED_MAX_KT:.0f} kt\n"
|
| 1652 |
f"**Southern speed bounds:** {SOUTH_SPEED_MIN_KT:.0f}–{SOUTH_SPEED_MAX_KT:.0f} kt (per segment)\n"
|
| 1653 |
f"**Ring-fit goal:** match BTO at ping times where measured BTO exists (auto-detected from satLoc).\n"
|
| 1654 |
-
f"**
|
| 1655 |
)
|
| 1656 |
|
| 1657 |
with gr.Row():
|
|
@@ -1674,8 +1725,8 @@ with gr.Blocks() as demo:
|
|
| 1674 |
gr.Markdown(
|
| 1675 |
"- Guided mode has **no speed inputs**.\n"
|
| 1676 |
"- Solver uses **fast BTO-first ring-fit** (turn speed + per-segment southern speeds).\n"
|
| 1677 |
-
"-
|
| 1678 |
-
"-
|
| 1679 |
)
|
| 1680 |
|
| 1681 |
gr.Markdown("### Guided scoring weights (for reporting only)")
|
|
|
|
| 11 |
# - Guided validation timestamps: 18:22, 18:40, 19:40, 20:40, 21:40, 22:40, 00:10, 00:20
|
| 12 |
#
|
| 13 |
# THIS VERSION:
|
| 14 |
+
# - Guided solver is BTO-first "ring-fit"
|
| 15 |
# - IMPORTANT: If a timestamp has no measured BTO (e.g. 18:40 in your satLoc),
|
| 16 |
# the solver automatically does NOT enforce BTO there.
|
| 17 |
# - Turn speed range: 470–520 kt
|
| 18 |
# - Southern segment speed range: 430–510 kt (per segment)
|
| 19 |
# - Waypoints at ping times are driven to ping rings for those timestamps with measured BTO.
|
| 20 |
#
|
| 21 |
+
# PERFORMANCE UPDATE (Top-K polish):
|
| 22 |
+
# - The expensive “polish” step is NOT run for every candidate anymore.
|
| 23 |
+
# - We search fast first, keep top-K candidates, then polish ONLY those.
|
| 24 |
+
# - This cuts runtime a lot while keeping the good ≤50 µs fits.
|
| 25 |
|
| 26 |
import os
|
| 27 |
import re
|
|
|
|
| 132 |
# HARD BTO labels list (will be AUTO-FILTERED to those with measured BTO)
|
| 133 |
HARD_BTO_LABELS = ["18:40:00", "19:40:00", "20:40:00", "21:40:00", "22:40:00", "24:10:00", "24:20:00"]
|
| 134 |
|
| 135 |
+
# ===== Performance knobs: Top-K polish =====
|
| 136 |
+
TOPK_POLISH = 5 # polish only the best K candidates globally
|
| 137 |
+
ENABLE_POLISH = True # keep polish, but only for top-K
|
| 138 |
+
POLISH_PASSES = 3 # fewer passes = faster
|
| 139 |
+
POLISH_WINDOW_KT = 6.0 # +/- window around current segment speed
|
| 140 |
+
POLISH_STEP_KT = 2.0 # step for coordinate search
|
| 141 |
+
POLISH_OBJ_EPS = 1e-3 # secondary weight for sum(abs)
|
| 142 |
|
| 143 |
# Search area colors (optional)
|
| 144 |
SEARCH_AREA_COLORS = {
|
|
|
|
| 1236 |
return legs
|
| 1237 |
|
| 1238 |
|
| 1239 |
+
def _compute_bto_errors_at_enforced(
|
| 1240 |
method: str,
|
| 1241 |
+
start_dt: datetime,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1242 |
alt_ft: float,
|
| 1243 |
vs_fpm: float,
|
|
|
|
| 1244 |
descent_start_dt: datetime | None,
|
| 1245 |
+
full_legs: list[tuple[float, float, float]],
|
| 1246 |
+
enforced_dts: list[datetime],
|
| 1247 |
):
|
| 1248 |
errs = []
|
| 1249 |
+
for dt_k in enforced_dts:
|
| 1250 |
+
meas = _lookup_measured_bto(dt_k)
|
|
|
|
|
|
|
| 1251 |
if meas is None:
|
| 1252 |
continue
|
| 1253 |
+
st = _state_at_dt(RADAR_FIX_LAT, RADAR_FIX_LON, full_legs, method, start_dt, dt_k)
|
| 1254 |
+
if st is None:
|
| 1255 |
+
continue
|
| 1256 |
+
bto_calc = _bto_calc(method, st["lat"], st["lon"], dt_k, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1257 |
+
errs.append(float(bto_calc - meas))
|
| 1258 |
+
return errs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1259 |
|
| 1260 |
|
| 1261 |
+
def _polish_south_speeds_topk(
|
| 1262 |
method: str,
|
| 1263 |
+
start_dt: datetime,
|
| 1264 |
+
end_dt: datetime,
|
|
|
|
|
|
|
| 1265 |
alt_ft: float,
|
| 1266 |
vs_fpm: float,
|
|
|
|
| 1267 |
descent_start_dt: datetime | None,
|
| 1268 |
+
h_south: float,
|
| 1269 |
+
turn_legs: list[tuple[float, float, float]],
|
| 1270 |
+
enforced_dts: list[datetime],
|
| 1271 |
+
init_speeds: list[float],
|
| 1272 |
+
passes: int = POLISH_PASSES,
|
| 1273 |
+
window_kt: float = POLISH_WINDOW_KT,
|
| 1274 |
+
step_kt: float = POLISH_STEP_KT,
|
| 1275 |
):
|
| 1276 |
+
"""
|
| 1277 |
+
Coordinate-descent style "polish" of south speeds:
|
| 1278 |
+
- Adjust one segment at a time within +/- window to reduce objective:
|
| 1279 |
+
max_abs_error + eps * sum_abs_error
|
| 1280 |
+
This is expensive, so we run it ONLY for top-K candidates.
|
| 1281 |
+
"""
|
| 1282 |
+
speeds = [float(v) for v in init_speeds]
|
| 1283 |
+
|
| 1284 |
+
# Precompute turn end position (constant for this candidate)
|
| 1285 |
+
pts_turn, _, _ = simulate_path(RADAR_FIX_LAT, RADAR_FIX_LON, turn_legs, method)
|
| 1286 |
+
turn_end_lat, turn_end_lon = float(pts_turn[-1][0]), float(pts_turn[-1][1])
|
| 1287 |
|
| 1288 |
+
def build_full(speeds_now: list[float]):
|
| 1289 |
+
lat_cur, lon_cur = turn_end_lat, turn_end_lon
|
| 1290 |
+
south_errs_local = []
|
| 1291 |
+
for mins, end_lbl, vseg in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END, speeds_now):
|
| 1292 |
+
dt_end = _dt_from_label(end_lbl)
|
| 1293 |
+
lat_cur, lon_cur = _simulate_one_leg_end(method, lat_cur, lon_cur, h_south, vseg, mins)
|
| 1294 |
+
meas = _lookup_measured_bto(dt_end)
|
| 1295 |
+
if meas is None:
|
| 1296 |
+
south_errs_local.append(float("nan"))
|
| 1297 |
+
else:
|
| 1298 |
+
bto_here = _bto_calc(method, lat_cur, lon_cur, dt_end, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1299 |
+
south_errs_local.append(float(bto_here - meas))
|
| 1300 |
|
| 1301 |
+
south_legs = _build_south_legs_piecewise(h_south, speeds_now, GUIDED_SOUTH_SEG_MIN)
|
| 1302 |
+
full_legs = clip_legs_to_end_dt(turn_legs + south_legs, start_dt, end_dt)
|
| 1303 |
+
return full_legs, south_errs_local
|
| 1304 |
|
| 1305 |
+
def objective(full_legs):
|
| 1306 |
+
bto_errs = _compute_bto_errors_at_enforced(method, start_dt, alt_ft, vs_fpm, descent_start_dt, full_legs, enforced_dts)
|
| 1307 |
+
if not bto_errs:
|
| 1308 |
+
return 1e9, []
|
| 1309 |
+
abs_arr = np.abs(np.array(bto_errs, dtype=float))
|
| 1310 |
+
max_abs = float(np.max(abs_arr))
|
| 1311 |
+
sum_abs = float(np.sum(abs_arr))
|
| 1312 |
+
return float(max_abs + POLISH_OBJ_EPS * sum_abs), bto_errs
|
| 1313 |
+
|
| 1314 |
+
# initial
|
| 1315 |
+
full_legs, south_errs = build_full(speeds)
|
| 1316 |
+
best_obj, best_bto_errs = objective(full_legs)
|
| 1317 |
+
best_speeds = speeds[:]
|
| 1318 |
+
best_full = full_legs
|
| 1319 |
+
best_south_errs = south_errs
|
| 1320 |
+
|
| 1321 |
+
for _p in range(int(passes)):
|
| 1322 |
+
improved_any = False
|
| 1323 |
for i in range(len(speeds)):
|
| 1324 |
+
v0 = float(best_speeds[i])
|
| 1325 |
+
v_lo = _clamp(v0 - float(window_kt), SOUTH_SPEED_MIN_KT, SOUTH_SPEED_MAX_KT)
|
| 1326 |
+
v_hi = _clamp(v0 + float(window_kt), SOUTH_SPEED_MIN_KT, SOUTH_SPEED_MAX_KT)
|
| 1327 |
|
| 1328 |
+
# candidate grid (small)
|
| 1329 |
+
cand = list(np.arange(v_lo, v_hi + 1e-9, float(step_kt)))
|
| 1330 |
+
if not cand:
|
| 1331 |
continue
|
| 1332 |
|
| 1333 |
+
local_best_obj = best_obj
|
| 1334 |
+
local_best_speeds = best_speeds[:]
|
| 1335 |
+
local_best_full = best_full
|
| 1336 |
+
local_best_south_errs = best_south_errs
|
| 1337 |
+
|
| 1338 |
+
for v_try in cand:
|
| 1339 |
+
sp = best_speeds[:]
|
| 1340 |
+
sp[i] = float(v_try)
|
| 1341 |
+
full_try, south_errs_try = build_full(sp)
|
| 1342 |
+
obj_try, _ = objective(full_try)
|
| 1343 |
+
if obj_try < local_best_obj - 1e-9:
|
| 1344 |
+
local_best_obj = obj_try
|
| 1345 |
+
local_best_speeds = sp
|
| 1346 |
+
local_best_full = full_try
|
| 1347 |
+
local_best_south_errs = south_errs_try
|
| 1348 |
+
|
| 1349 |
+
if local_best_obj < best_obj - 1e-9:
|
| 1350 |
+
best_obj = local_best_obj
|
| 1351 |
+
best_speeds = local_best_speeds
|
| 1352 |
+
best_full = local_best_full
|
| 1353 |
+
best_south_errs = local_best_south_errs
|
| 1354 |
improved_any = True
|
| 1355 |
|
| 1356 |
+
if not improved_any:
|
| 1357 |
break
|
| 1358 |
|
| 1359 |
+
return best_speeds, best_full, best_south_errs
|
|
|
|
|
|
|
|
|
|
| 1360 |
|
| 1361 |
|
| 1362 |
# ================= Guided Solver =================
|
|
|
|
| 1384 |
|
| 1385 |
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1386 |
|
| 1387 |
+
def _evaluate(h_south: float, v_turn: float, do_polish: bool = False):
|
| 1388 |
+
h_south = float(h_south)
|
| 1389 |
+
v_turn = float(v_turn)
|
| 1390 |
+
|
| 1391 |
# Build turn legs
|
| 1392 |
turn_legs, turn_info = build_turn_legs(
|
| 1393 |
speed_knots=float(v_turn),
|
| 1394 |
method=method,
|
| 1395 |
heading_final_deg=float(h_south),
|
| 1396 |
)
|
| 1397 |
+
|
| 1398 |
+
# Find turn end position
|
| 1399 |
pts_turn, _, _ = simulate_path(RADAR_FIX_LAT, RADAR_FIX_LON, turn_legs, method)
|
| 1400 |
+
lat_cur, lon_cur = float(pts_turn[-1][0]), float(pts_turn[-1][1])
|
| 1401 |
|
| 1402 |
+
# Fit south segments sequentially (per segment speed)
|
|
|
|
| 1403 |
south_speeds = []
|
| 1404 |
south_errs = []
|
| 1405 |
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
|
|
|
| 1422 |
south_errs.append(float(err_seg))
|
| 1423 |
lat_cur, lon_cur = float(lat1), float(lon1)
|
| 1424 |
|
| 1425 |
+
# Assemble full legs
|
| 1426 |
+
south_legs = _build_south_legs_piecewise(float(h_south), south_speeds, GUIDED_SOUTH_SEG_MIN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1427 |
full_legs = clip_legs_to_end_dt(turn_legs + south_legs, start_dt, end_dt)
|
| 1428 |
|
| 1429 |
+
# (Optional) polish only for Top-K
|
| 1430 |
+
if do_polish and ENABLE_POLISH:
|
| 1431 |
+
polished_speeds, polished_full, polished_seg_errs = _polish_south_speeds_topk(
|
| 1432 |
+
method=method,
|
| 1433 |
+
start_dt=start_dt,
|
| 1434 |
+
end_dt=end_dt,
|
| 1435 |
+
alt_ft=alt_ft,
|
| 1436 |
+
vs_fpm=vs_fpm,
|
| 1437 |
+
descent_start_dt=descent_start_dt,
|
| 1438 |
+
h_south=float(h_south),
|
| 1439 |
+
turn_legs=turn_legs,
|
| 1440 |
+
enforced_dts=enforced_dts,
|
| 1441 |
+
init_speeds=south_speeds,
|
| 1442 |
+
passes=POLISH_PASSES,
|
| 1443 |
+
window_kt=POLISH_WINDOW_KT,
|
| 1444 |
+
step_kt=POLISH_STEP_KT,
|
| 1445 |
+
)
|
| 1446 |
+
south_speeds = polished_speeds
|
| 1447 |
+
full_legs = polished_full
|
| 1448 |
+
south_errs = polished_seg_errs
|
| 1449 |
|
| 1450 |
+
# Compute BTO errors at enforced timestamps
|
| 1451 |
+
bto_errs = _compute_bto_errors_at_enforced(method, start_dt, alt_ft, vs_fpm, descent_start_dt, full_legs, enforced_dts)
|
| 1452 |
if not bto_errs:
|
| 1453 |
return None
|
| 1454 |
|
| 1455 |
+
abs_arr = np.abs(np.array(bto_errs, dtype=float))
|
| 1456 |
+
max_abs = float(np.max(abs_arr))
|
| 1457 |
+
sum_abs = float(np.sum(abs_arr))
|
| 1458 |
+
rmse = float(np.sqrt(np.mean(np.array(bto_errs, dtype=float) ** 2)))
|
| 1459 |
|
| 1460 |
details = {
|
| 1461 |
"h_south": float(h_south),
|
| 1462 |
"v_turn": float(v_turn),
|
| 1463 |
+
"south_speeds": [float(v) for v in south_speeds],
|
| 1464 |
+
"south_errs_us": [float(e) for e in south_errs],
|
| 1465 |
"turn_info": turn_info,
|
| 1466 |
"enforced_labels": enforced_labels,
|
| 1467 |
+
"max_abs_err_us": max_abs,
|
| 1468 |
+
"sum_abs_err_us": sum_abs,
|
| 1469 |
+
"rmse_bto_us": rmse,
|
| 1470 |
+
"polished": bool(do_polish and ENABLE_POLISH),
|
| 1471 |
}
|
| 1472 |
+
# Primary sort: max_abs, then sum_abs, then rmse (stable-ish)
|
| 1473 |
+
key = (max_abs, sum_abs, rmse)
|
| 1474 |
+
return key, full_legs, details
|
| 1475 |
|
| 1476 |
def best_for_heading(h_south: float):
|
| 1477 |
h_south = float(h_south)
|
| 1478 |
|
| 1479 |
+
# Coarse scan turn speed (fast)
|
| 1480 |
coarse_turns = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, 5.0))
|
| 1481 |
best_key = None
|
| 1482 |
best_legs = None
|
| 1483 |
best_details = None
|
| 1484 |
|
| 1485 |
for v in coarse_turns:
|
| 1486 |
+
out = _evaluate(h_south, v, do_polish=False)
|
| 1487 |
if out is None:
|
| 1488 |
continue
|
| 1489 |
key, legs, det = out
|
|
|
|
| 1493 |
if best_key is None:
|
| 1494 |
return None
|
| 1495 |
|
| 1496 |
+
# Refine around best coarse speed (golden minimize max_abs + eps * sum_abs)
|
| 1497 |
v0 = float(best_details["v_turn"])
|
| 1498 |
v_lo = _clamp(v0 - 7.5, TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT)
|
| 1499 |
v_hi = _clamp(v0 + 7.5, TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT)
|
| 1500 |
|
| 1501 |
def obj(v):
|
| 1502 |
+
out2 = _evaluate(h_south, v, do_polish=False)
|
| 1503 |
if out2 is None:
|
| 1504 |
return 1e9
|
| 1505 |
(max_abs, sum_abs, _rmse), _legs, _det = out2
|
| 1506 |
+
return float(max_abs) + POLISH_OBJ_EPS * float(sum_abs)
|
| 1507 |
|
| 1508 |
+
v_best, _ = _golden_min_abs(obj, v_lo, v_hi, iters=18)
|
| 1509 |
+
out3 = _evaluate(h_south, v_best, do_polish=False)
|
| 1510 |
if out3 is None:
|
| 1511 |
return best_key, best_legs, best_details
|
| 1512 |
key3, legs3, det3 = out3
|
|
|
|
| 1518 |
global_legs = None
|
| 1519 |
global_det = None
|
| 1520 |
|
| 1521 |
+
# Collect candidates for Top-K polish
|
| 1522 |
+
candidates = [] # list of (key, h_south, v_turn)
|
| 1523 |
+
|
| 1524 |
for h in coarse_headings:
|
| 1525 |
out = best_for_heading(h)
|
| 1526 |
if out is None:
|
| 1527 |
continue
|
| 1528 |
key, legs, det = out
|
| 1529 |
+
candidates.append((key, float(det["h_south"]), float(det["v_turn"])))
|
| 1530 |
if global_best is None or key < global_best:
|
| 1531 |
global_best, global_legs, global_det = key, legs, det
|
| 1532 |
|
|
|
|
| 1542 |
if out is None:
|
| 1543 |
continue
|
| 1544 |
key, legs, det = out
|
| 1545 |
+
candidates.append((key, float(det["h_south"]), float(det["v_turn"])))
|
| 1546 |
if global_best is None or key < global_best:
|
| 1547 |
global_best, global_legs, global_det = key, legs, det
|
| 1548 |
|
| 1549 |
+
# ===== Top-K polish ONLY here =====
|
| 1550 |
+
# Sort and unique candidates
|
| 1551 |
+
candidates_sorted = sorted(candidates, key=lambda x: x[0])
|
| 1552 |
+
uniq = []
|
| 1553 |
+
seen = set()
|
| 1554 |
+
for key, hh, vv in candidates_sorted:
|
| 1555 |
+
k2 = (round(hh, 4), round(vv, 4))
|
| 1556 |
+
if k2 in seen:
|
| 1557 |
+
continue
|
| 1558 |
+
seen.add(k2)
|
| 1559 |
+
uniq.append((key, hh, vv))
|
| 1560 |
+
if len(uniq) >= max(1, int(TOPK_POLISH)):
|
| 1561 |
+
break
|
| 1562 |
+
|
| 1563 |
+
# Start from best unpolished
|
| 1564 |
+
best_final_key = global_best
|
| 1565 |
+
best_final_legs = global_legs
|
| 1566 |
+
best_final_det = global_det
|
| 1567 |
+
|
| 1568 |
+
if ENABLE_POLISH and uniq:
|
| 1569 |
+
for _key, hh, vv in uniq:
|
| 1570 |
+
outp = _evaluate(hh, vv, do_polish=True)
|
| 1571 |
+
if outp is None:
|
| 1572 |
+
continue
|
| 1573 |
+
keyp, legsp, detp = outp
|
| 1574 |
+
if best_final_key is None or keyp < best_final_key:
|
| 1575 |
+
best_final_key = keyp
|
| 1576 |
+
best_final_legs = legsp
|
| 1577 |
+
best_final_det = detp
|
| 1578 |
+
|
| 1579 |
# Reporting
|
| 1580 |
+
south_speeds = best_final_det["south_speeds"]
|
| 1581 |
+
south_errs = best_final_det["south_errs_us"]
|
| 1582 |
+
polished_flag = best_final_det.get("polished", False)
|
| 1583 |
|
| 1584 |
report_lines = [
|
| 1585 |
"**Guided mode: Straight + computed turn (18:22→18:40), clipped to 00:20**",
|
| 1586 |
+
"- Solver mode: **BTO-first ring-fit**",
|
| 1587 |
+
f"- Best southern heading: **{best_final_det['h_south']:.2f}°**",
|
| 1588 |
+
f"- Turn speed range: {TURN_SPEED_MIN_KT:.0f}–{TURN_SPEED_MAX_KT:.0f} kt | Best: **{best_final_det['v_turn']:.2f} kt**",
|
| 1589 |
+
f"- Polish: **{'ON (Top-K only)' if ENABLE_POLISH else 'OFF'}** | Applied to winner: **{'YES' if polished_flag else 'NO'}**",
|
|
|
|
| 1590 |
"",
|
| 1591 |
"**Southern per-segment speeds (each constrained to 430–510 kt):**",
|
| 1592 |
+
f"- 18:40→19:40: **{south_speeds[0]:.2f} kt** (segment end err {south_errs[0]:+.2f} µs)",
|
| 1593 |
+
f"- 19:40→20:40: **{south_speeds[1]:.2f} kt** (segment end err {south_errs[1]:+.2f} µs)",
|
| 1594 |
+
f"- 20:40→21:40: **{south_speeds[2]:.2f} kt** (segment end err {south_errs[2]:+.2f} µs)",
|
| 1595 |
+
f"- 21:40→22:40: **{south_speeds[3]:.2f} kt** (segment end err {south_errs[3]:+.2f} µs)",
|
| 1596 |
+
f"- 22:40→00:10: **{south_speeds[4]:.2f} kt** (segment end err {south_errs[4]:+.2f} µs)",
|
| 1597 |
+
f"- 00:10→00:20: **{south_speeds[5]:.2f} kt** (segment end err {south_errs[5]:+.2f} µs)",
|
| 1598 |
"",
|
| 1599 |
+
f"- Enforced BTO timestamps (measured available): **{', '.join(best_final_det['enforced_labels'])}**",
|
| 1600 |
+
f"- Max |BTO error| at enforced times: **{best_final_det['max_abs_err_us']:.2f} µs**",
|
| 1601 |
+
f"- RMSE_BTO at enforced times: **{best_final_det['rmse_bto_us']:.2f} µs**",
|
| 1602 |
"",
|
| 1603 |
"*If 18:40 has no measured BTO in your satLoc, it is automatically not enforced.*",
|
| 1604 |
+
f"*Performance: polish runs only for top-{TOPK_POLISH} candidates now.*",
|
| 1605 |
]
|
| 1606 |
|
| 1607 |
+
return best_final_legs, "\n".join(report_lines), start_dt
|
| 1608 |
|
| 1609 |
|
| 1610 |
# ================= RUNNERS =================
|
|
|
|
| 1702 |
f"**Turn speed bounds:** {TURN_SPEED_MIN_KT:.0f}–{TURN_SPEED_MAX_KT:.0f} kt\n"
|
| 1703 |
f"**Southern speed bounds:** {SOUTH_SPEED_MIN_KT:.0f}–{SOUTH_SPEED_MAX_KT:.0f} kt (per segment)\n"
|
| 1704 |
f"**Ring-fit goal:** match BTO at ping times where measured BTO exists (auto-detected from satLoc).\n"
|
| 1705 |
+
f"**Performance:** polish runs only for **top-{TOPK_POLISH}** candidates."
|
| 1706 |
)
|
| 1707 |
|
| 1708 |
with gr.Row():
|
|
|
|
| 1725 |
gr.Markdown(
|
| 1726 |
"- Guided mode has **no speed inputs**.\n"
|
| 1727 |
"- Solver uses **fast BTO-first ring-fit** (turn speed + per-segment southern speeds).\n"
|
| 1728 |
+
"- If 18:40 has no measured BTO in your satLoc, it is **automatically skipped** for enforcement.\n"
|
| 1729 |
+
f"- **Performance:** “polish” runs only on the best **top-{TOPK_POLISH}** candidates."
|
| 1730 |
)
|
| 1731 |
|
| 1732 |
gr.Markdown("### Guided scoring weights (for reporting only)")
|