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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -23
app.py CHANGED
@@ -17,6 +17,11 @@
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
  import os
22
  import re
@@ -127,6 +132,11 @@ BTO_TARGET_TIGHT_US = 1.0 # purely informational
127
  # HARD BTO labels list (will be AUTO-FILTERED to those with measured BTO)
128
  HARD_BTO_LABELS = ["18:40:00", "19:40:00", "20:40:00", "21:40:00", "22:40:00", "24:10:00", "24:20:00"]
129
 
 
 
 
 
 
130
  # Search area colors (optional)
131
  SEARCH_AREA_COLORS = {
132
  "Ocean Infinity": "#ff3b3b",
@@ -1223,6 +1233,120 @@ def _build_south_legs_piecewise(h_south: float, speeds: list[float], minutes: li
1223
  return legs
1224
 
1225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1226
  # ================= Guided Solver =================
1227
  def solve_guided_straight(
1228
  method, alt_ft, vs_fpm,
@@ -1256,9 +1380,10 @@ def solve_guided_straight(
1256
  heading_final_deg=float(h_south),
1257
  )
1258
  pts_turn, _, _ = simulate_path(RADAR_FIX_LAT, RADAR_FIX_LON, turn_legs, method)
1259
- lat_cur, lon_cur = pts_turn[-1]
1260
 
1261
- # Fit south segments sequentially (per segment speed)
 
1262
  south_speeds = []
1263
  south_errs = []
1264
  for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
@@ -1281,10 +1406,24 @@ def solve_guided_straight(
1281
  south_errs.append(float(err_seg))
1282
  lat_cur, lon_cur = float(lat1), float(lon1)
1283
 
1284
- south_legs = _build_south_legs_piecewise(float(h_south), south_speeds, GUIDED_SOUTH_SEG_MIN)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1285
  full_legs = clip_legs_to_end_dt(turn_legs + south_legs, start_dt, end_dt)
1286
 
1287
- # Compute BTO errors at enforced timestamps
1288
  bto_errs = []
1289
  for dt_k in enforced_dts:
1290
  meas = _lookup_measured_bto(dt_k)
@@ -1299,27 +1438,29 @@ def solve_guided_straight(
1299
  if not bto_errs:
1300
  return None
1301
 
1302
- max_abs = float(np.max(np.abs(bto_errs)))
1303
- sum_abs = float(np.sum(np.abs(bto_errs)))
1304
- rmse = float(np.sqrt(np.mean(np.array(bto_errs) ** 2)))
 
1305
 
1306
  details = {
1307
  "h_south": float(h_south),
1308
  "v_turn": float(v_turn),
1309
- "south_speeds": south_speeds,
1310
- "south_errs_us": south_errs,
1311
  "turn_info": turn_info,
1312
  "enforced_labels": enforced_labels,
1313
- "max_abs_err_us": max_abs,
1314
- "sum_abs_err_us": sum_abs,
1315
- "rmse_bto_us": rmse,
 
1316
  }
1317
  return (max_abs, sum_abs, rmse), full_legs, details
1318
 
1319
  def best_for_heading(h_south: float):
1320
  h_south = float(h_south)
1321
 
1322
- # Coarse scan turn speed (fast, avoids expensive nested golden everywhere)
1323
  coarse_turns = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, 5.0))
1324
  best_key = None
1325
  best_legs = None
@@ -1346,7 +1487,6 @@ def solve_guided_straight(
1346
  if out2 is None:
1347
  return 1e9
1348
  (max_abs, sum_abs, _rmse), _legs, _det = out2
1349
- # primary max abs, secondary sum abs
1350
  return float(max_abs) + 1e-3 * float(sum_abs)
1351
 
1352
  v_best, _ = _golden_min_abs(obj, v_lo, v_hi, iters=22)
@@ -1387,21 +1527,24 @@ def solve_guided_straight(
1387
 
1388
  # Reporting
1389
  south_speeds = global_det["south_speeds"]
1390
- south_errs = global_det["south_errs_us"]
 
1391
 
1392
  report_lines = [
1393
  "**Guided mode: Straight + computed turn (18:22→18:40), clipped to 00:20**",
1394
- "- Solver mode: **BTO-first ring-fit (fast)**",
1395
  f"- Best southern heading: **{global_det['h_south']:.2f}°**",
1396
  f"- Turn speed range: {TURN_SPEED_MIN_KT:.0f}–{TURN_SPEED_MAX_KT:.0f} kt | Best: **{global_det['v_turn']:.2f} kt**",
1397
  "",
 
 
1398
  "**Southern per-segment speeds (each constrained to 430–510 kt):**",
1399
- f"- 18:40→19:40: **{south_speeds[0]:.2f} kt** (segment end err {south_errs[0]:+.2f} µs)",
1400
- f"- 19:40→20:40: **{south_speeds[1]:.2f} kt** (segment end err {south_errs[1]:+.2f} µs)",
1401
- f"- 20:40→21:40: **{south_speeds[2]:.2f} kt** (segment end err {south_errs[2]:+.2f} µs)",
1402
- f"- 21:40→22:40: **{south_speeds[3]:.2f} kt** (segment end err {south_errs[3]:+.2f} µs)",
1403
- f"- 22:40→00:10: **{south_speeds[4]:.2f} kt** (segment end err {south_errs[4]:+.2f} µs)",
1404
- f"- 00:10→00:20: **{south_speeds[5]:.2f} kt** (segment end err {south_errs[5]:+.2f} µs)",
1405
  "",
1406
  f"- Enforced BTO timestamps (measured available): **{', '.join(global_det['enforced_labels'])}**",
1407
  f"- Max |BTO error| at enforced times: **{global_det['max_abs_err_us']:.2f} µs**",
@@ -1507,7 +1650,8 @@ with gr.Blocks() as demo:
1507
  f"- **Final resting point:** **00:20:00Z** (trajectory clipped exactly at this time)\n\n"
1508
  f"**Turn speed bounds:** {TURN_SPEED_MIN_KT:.0f}–{TURN_SPEED_MAX_KT:.0f} kt\n"
1509
  f"**Southern speed bounds:** {SOUTH_SPEED_MIN_KT:.0f}–{SOUTH_SPEED_MAX_KT:.0f} kt (per segment)\n"
1510
- f"**Ring-fit goal:** match BTO at ping times where measured BTO exists (auto-detected from satLoc)."
 
1511
  )
1512
 
1513
  with gr.Row():
@@ -1530,6 +1674,7 @@ with gr.Blocks() as demo:
1530
  gr.Markdown(
1531
  "- Guided mode has **no speed inputs**.\n"
1532
  "- Solver uses **fast BTO-first ring-fit** (turn speed + per-segment southern speeds).\n"
 
1533
  "- If 18:40 has no measured BTO in your satLoc, it is **automatically skipped** for enforcement."
1534
  )
1535
 
 
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
  # 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 = {
142
  "Ocean Infinity": "#ff3b3b",
 
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 =================
1351
  def solve_guided_straight(
1352
  method, alt_ft, vs_fpm,
 
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
  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)
 
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
 
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)
 
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**",
 
1650
  f"- **Final resting point:** **00:20:00Z** (trajectory clipped exactly at this time)\n\n"
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
  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