Chris773 commited on
Commit
c4ecb8d
·
verified ·
1 Parent(s): 62da916

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -164
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", with fast speed optimization
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
- # NEW (per your request):
22
- # - Adds a fast "global polish" step over the 6 southern segment speeds ONLY
23
- # (coordinate descent), to reduce max |BTO error| at enforced endpoints.
24
- # This helps when greedy segment-fit hits the speed floor (430 kt) but still leaves residual error.
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
- # ===== Global polish (southern speeds only) =====
136
- POLISH_PASSES = 5 # small number keeps it fast
137
- POLISH_WINDOW_KT = 6.0 # explore +/- window around current speed
138
- POLISH_STEP_KT = 1.0 # 1 kt grid within window (fast + robust)
 
 
 
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 _simulate_south_endpoints(
1237
  method: str,
1238
- lat0: float,
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
- labels_used = []
1262
- for (lat, lon), end_lbl in zip(endpoints, GUIDED_SOUTH_SEG_LABELS_END):
1263
- dt_end = _dt_from_label(end_lbl)
1264
- meas = _lookup_measured_bto(dt_end)
1265
  if meas is None:
1266
  continue
1267
- bto = _bto_calc(method, lat, lon, dt_end, alt_ft, vs_fpm, start_dt, descent_start_dt)
1268
- errs.append(float(bto - meas))
1269
- labels_used.append(end_lbl)
1270
- return labels_used, errs
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 _polish_south_speeds(
1286
  method: str,
1287
- lat0: float,
1288
- lon0: float,
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
- speeds = [float(v) for v in speeds_init]
1297
- best_endpoints = _simulate_south_endpoints(method, lat0, lon0, hdg, speeds, GUIDED_SOUTH_SEG_MIN)
1298
- _lbls, errs = _bto_errors_at_south_endpoints(method, best_endpoints, alt_ft, vs_fpm, start_dt, descent_start_dt)
1299
- obj0 = _bto_obj_from_errs(errs)
1300
- if obj0 is None:
1301
- return speeds, errs, {"polish_passes": 0, "improved": False}
 
 
 
 
 
1302
 
1303
- best_score = obj0[0]
1304
- improved_any = False
 
 
 
 
 
 
 
 
 
 
1305
 
1306
- for _pass in range(int(POLISH_PASSES)):
1307
- improved_this_pass = False
 
1308
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1309
  for i in range(len(speeds)):
1310
- v0 = float(speeds[i])
1311
- v_lo = _clamp(v0 - POLISH_WINDOW_KT, SOUTH_SPEED_MIN_KT, SOUTH_SPEED_MAX_KT)
1312
- v_hi = _clamp(v0 + POLISH_WINDOW_KT, SOUTH_SPEED_MIN_KT, SOUTH_SPEED_MAX_KT)
1313
 
1314
- cand_vs = _frange(v_lo, v_hi, POLISH_STEP_KT)
1315
- if not cand_vs:
 
1316
  continue
1317
 
1318
- local_best_v = v0
1319
- local_best_score = best_score
1320
-
1321
- # try each candidate on this coordinate
1322
- for v_try in cand_vs:
1323
- tmp = speeds[:]
1324
- tmp[i] = float(v_try)
1325
- endpoints = _simulate_south_endpoints(method, lat0, lon0, hdg, tmp, GUIDED_SOUTH_SEG_MIN)
1326
- _lbls2, errs2 = _bto_errors_at_south_endpoints(method, endpoints, alt_ft, vs_fpm, start_dt, descent_start_dt)
1327
- obj2 = _bto_obj_from_errs(errs2)
1328
- if obj2 is None:
1329
- continue
1330
- score2 = obj2[0]
1331
- if score2 + 1e-12 < local_best_score:
1332
- local_best_score = score2
1333
- local_best_v = float(v_try)
1334
-
1335
- if local_best_v != v0:
1336
- speeds[i] = local_best_v
1337
- best_score = local_best_score
1338
- improved_this_pass = True
1339
  improved_any = True
1340
 
1341
- if not improved_this_pass:
1342
  break
1343
 
1344
- # final errors for reporting
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 evaluate_heading_and_turnspeed(h_south: float, v_turn: float):
 
 
 
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
- lat_turn_end, lon_turn_end = pts_turn[-1]
1384
 
1385
- # Fit south segments sequentially (greedy per segment)
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
- # ---- NEW: global polish on southern segment speeds only ----
1410
- south_speeds_pol, enforced_errs_pol, polish_info = _polish_south_speeds(
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
- # Compute BTO errors at enforced timestamps (from full legs for safety)
1427
- bto_errs = []
1428
- for dt_k in enforced_dts:
1429
- meas = _lookup_measured_bto(dt_k)
1430
- if meas is None:
1431
- continue
1432
- st = _state_at_dt(RADAR_FIX_LAT, RADAR_FIX_LON, full_legs, method, start_dt, dt_k)
1433
- if st is None:
1434
- continue
1435
- bto_calc = _bto_calc(method, st["lat"], st["lon"], dt_k, alt_ft, vs_fpm, start_dt, descent_start_dt)
1436
- bto_errs.append(float(bto_calc - meas))
 
 
 
 
 
 
 
 
 
1437
 
 
 
1438
  if not bto_errs:
1439
  return None
1440
 
1441
- obj = _bto_obj_from_errs(bto_errs)
1442
- if obj is None:
1443
- return None
1444
- _score, max_abs, sum_abs, rmse = obj
1445
 
1446
  details = {
1447
  "h_south": float(h_south),
1448
  "v_turn": float(v_turn),
1449
- "south_speeds": [float(v) for v in south_speeds_pol],
1450
- "south_errs_us": south_errs, # greedy segment-end err (may include nan if missing meas)
1451
  "turn_info": turn_info,
1452
  "enforced_labels": enforced_labels,
1453
- "max_abs_err_us": float(max_abs),
1454
- "sum_abs_err_us": float(sum_abs),
1455
- "rmse_bto_us": float(rmse),
1456
- "polish": polish_info,
1457
  }
1458
- return (max_abs, sum_abs, rmse), full_legs, details
 
 
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 = evaluate_heading_and_turnspeed(h_south, v)
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 via golden minimize max_abs_err
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 = evaluate_heading_and_turnspeed(h_south, v)
1487
  if out2 is None:
1488
  return 1e9
1489
  (max_abs, sum_abs, _rmse), _legs, _det = out2
1490
- return float(max_abs) + 1e-3 * float(sum_abs)
1491
 
1492
- v_best, _ = _golden_min_abs(obj, v_lo, v_hi, iters=22)
1493
- out3 = evaluate_heading_and_turnspeed(h_south, v_best)
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 = global_det["south_speeds"]
1530
- greedy_seg_end_errs = global_det["south_errs_us"]
1531
- polish_info = global_det.get("polish", {}) or {}
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 (fast) + global polish (southern speeds)**",
1536
- f"- Best southern heading: **{global_det['h_south']:.2f}°**",
1537
- f"- Turn speed range: {TURN_SPEED_MIN_KT:.0f}–{TURN_SPEED_MAX_KT:.0f} kt | Best: **{global_det['v_turn']:.2f} kt**",
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** (greedy end err {greedy_seg_end_errs[0]:+.2f} µs)",
1543
- f"- 19:40→20:40: **{south_speeds[1]:.2f} kt** (greedy end err {greedy_seg_end_errs[1]:+.2f} µs)",
1544
- f"- 20:40→21:40: **{south_speeds[2]:.2f} kt** (greedy end err {greedy_seg_end_errs[2]:+.2f} µs)",
1545
- f"- 21:40→22:40: **{south_speeds[3]:.2f} kt** (greedy end err {greedy_seg_end_errs[3]:+.2f} µs)",
1546
- f"- 22:40→00:10: **{south_speeds[4]:.2f} kt** (greedy end err {greedy_seg_end_errs[4]:+.2f} µs)",
1547
- f"- 00:10→00:20: **{south_speeds[5]:.2f} kt** (greedy end err {greedy_seg_end_errs[5]:+.2f} µs)",
1548
  "",
1549
- f"- Enforced BTO timestamps (measured available): **{', '.join(global_det['enforced_labels'])}**",
1550
- f"- Max |BTO error| at enforced times: **{global_det['max_abs_err_us']:.2f} µs**",
1551
- f"- RMSE_BTO at enforced times: **{global_det['rmse_bto_us']:.2f} µs**",
1552
  "",
1553
  "*If 18:40 has no measured BTO in your satLoc, it is automatically not enforced.*",
 
1554
  ]
1555
 
1556
- return global_legs, "\n".join(report_lines), start_dt
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"**Global polish:** {POLISH_PASSES} passes, ±{POLISH_WINDOW_KT:.0f} kt window, {POLISH_STEP_KT:.0f} kt step (southern speeds only)."
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
- "- Then runs a **global polish** step to reduce max |BTO error| across enforced endpoints.\n"
1678
- "- If 18:40 has no measured BTO in your satLoc, it is **automatically skipped** for enforcement."
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)")