Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,11 +13,15 @@
|
|
| 13 |
# - Guided mode computes a realistic turn between 18:22→18:40 and then southern path.
|
| 14 |
# - Guided validation timestamps: 18:22, 18:40, 19:40, 20:40, 21:40, 22:40, 00:10, 00:20
|
| 15 |
#
|
| 16 |
-
# THIS VERSION (UI
|
| 17 |
-
# - Removes the big
|
| 18 |
-
# - Adds
|
| 19 |
-
# - Descent
|
| 20 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
import os
|
| 23 |
import re
|
|
@@ -55,10 +59,13 @@ WAYPOINTS = [
|
|
| 55 |
("MEKAR", 6.5, 96.48333333),
|
| 56 |
]
|
| 57 |
|
| 58 |
-
#
|
| 59 |
-
|
| 60 |
-
CAPT_BYRON_BAILEY_LAT = -(39
|
| 61 |
-
CAPT_BYRON_BAILEY_LON = (88
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
RING_TIMES = {
|
| 64 |
"Ping Ring 1": "18:25:00Z",
|
|
@@ -144,10 +151,7 @@ VAR_BEAM_WIDTH = 12 # keep top-N partial paths each segment
|
|
| 144 |
VAR_SMOOTH_PENALTY_US_PER_DEG = 1.5 # small penalty to avoid wiggles (still BTO-first)
|
| 145 |
|
| 146 |
# ===== SE-bias-after-22:40 (new mode) =====
|
| 147 |
-
|
| 148 |
-
# Penalty is applied ONLY when heading increases (more west-of-south),
|
| 149 |
-
# nudging solutions to be slightly more east while keeping BTO primary.
|
| 150 |
-
SE_EAST_DIVERGE_START_SEG_INDEX = 4 # segment indices: 0..5 ; seg 3 ends at 22:40, seg 4 starts after 22:40
|
| 151 |
SE_VAR_EAST_DELTA_SET = [-8, -4, -2, 0, 2] # prefer small decreases (eastward) after 22:40
|
| 152 |
SE_EAST_BIAS_PENALTY_US_PER_DEG = 0.8 # small; BTO still dominates
|
| 153 |
|
|
@@ -505,6 +509,42 @@ def _dt_from_label(label_hms: str) -> datetime:
|
|
| 505 |
return fp._time_label_to_dt(s, fp.SAT_EPOCH_UTC)
|
| 506 |
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
# ================= ALTITUDE HELPERS =================
|
| 509 |
def altitude_ft_at_dt(dt_ping: datetime, start_dt: datetime, alt0_ft: float,
|
| 510 |
vs_fpm: float, descent_start_dt: datetime | None):
|
|
@@ -543,26 +583,35 @@ def build_altitude_plot(start_dt: datetime, total_time_min: float, alt0_ft: floa
|
|
| 543 |
return fig
|
| 544 |
|
| 545 |
|
|
|
|
| 546 |
def build_speed_profile_plot(start_dt: datetime, legs: list[tuple[float, float, float]]):
|
| 547 |
-
#
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
for (
|
| 552 |
-
spd = float(spd)
|
| 553 |
mins = float(mins)
|
| 554 |
-
|
| 555 |
-
if
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
|
| 564 |
fig = go.Figure()
|
| 565 |
-
fig.add_trace(go.Scatter(x=
|
| 566 |
fig.update_layout(
|
| 567 |
title="Speed Profile",
|
| 568 |
xaxis_title="Minutes since start",
|
|
@@ -733,12 +782,19 @@ def _ping_points_after(start_dt: datetime):
|
|
| 733 |
return pts
|
| 734 |
|
| 735 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
def bfo_analysis_with_legs(start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice):
|
| 737 |
pts = _ping_points_after(start_dt)
|
| 738 |
|
| 739 |
-
descent_start_dt =
|
| 740 |
-
if (descent_start_choice or "").startswith("At 00:20Z"):
|
| 741 |
-
descent_start_dt = _dt_from_label("24:20:00")
|
| 742 |
|
| 743 |
leg_secs = [float(m) * 60.0 for _, _, m in legs]
|
| 744 |
boundaries = np.cumsum([0.0] + leg_secs)
|
|
@@ -748,6 +804,14 @@ def bfo_analysis_with_legs(start_lat, start_lon, legs, method, start_dt, alt_ft,
|
|
| 748 |
|
| 749 |
x_labels, measured, calculated = [], [], []
|
| 750 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
for label, dt_ping, meas_bfo, _meas_bto in pts:
|
| 752 |
t_sec = (dt_ping - start_dt).total_seconds()
|
| 753 |
if t_sec < 0 or t_sec > total_end:
|
|
@@ -772,7 +836,7 @@ def bfo_analysis_with_legs(start_lat, start_lon, legs, method, start_dt, alt_ft,
|
|
| 772 |
spd_mps = float(spd) * 1852.0 / 3600.0
|
| 773 |
pred_bfo = float(fp.calculateBFO_at_time(spd_mps, float(hdg), float(lat), float(lon), alt_m, vs_mps, dt_ping))
|
| 774 |
|
| 775 |
-
x_labels.append(label)
|
| 776 |
measured.append(float(meas_bfo))
|
| 777 |
calculated.append(float(pred_bfo))
|
| 778 |
|
|
@@ -802,6 +866,8 @@ def bfo_analysis_with_legs(start_lat, start_lon, legs, method, start_dt, alt_ft,
|
|
| 802 |
margin=dict(l=60, r=160, t=80, b=60),
|
| 803 |
height=520,
|
| 804 |
)
|
|
|
|
|
|
|
| 805 |
return fig, ""
|
| 806 |
|
| 807 |
|
|
@@ -818,9 +884,7 @@ def build_validation_table(
|
|
| 818 |
start_lat, start_lon, method, start_dt, legs,
|
| 819 |
alt_ft, vs_fpm, descent_start_choice
|
| 820 |
):
|
| 821 |
-
descent_start_dt =
|
| 822 |
-
if (descent_start_choice or "").startswith("At 00:20Z"):
|
| 823 |
-
descent_start_dt = _dt_from_label("24:20:00")
|
| 824 |
|
| 825 |
is_guided = (start_dt == RADAR_FIX_TIME_UTC)
|
| 826 |
|
|
@@ -859,6 +923,13 @@ def build_validation_table(
|
|
| 859 |
|
| 860 |
bfo_meas, bto_meas = _lookup_measured_at_dt(dt_k)
|
| 861 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
bfo_err = None if bfo_meas is None else (float(bfo_calc) - float(bfo_meas))
|
| 863 |
bto_err = None if bto_meas is None else (float(bto_calc) - float(bto_meas))
|
| 864 |
|
|
@@ -899,6 +970,13 @@ def _all_bounds(points_list):
|
|
| 899 |
|
| 900 |
|
| 901 |
def add_permanent_label(m, name, lat, lon):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
folium.CircleMarker(
|
| 903 |
location=(lat, lon),
|
| 904 |
radius=4,
|
|
@@ -907,7 +985,8 @@ def add_permanent_label(m, name, lat, lon):
|
|
| 907 |
fill=True,
|
| 908 |
fill_color="white",
|
| 909 |
fill_opacity=1.0,
|
| 910 |
-
tooltip=
|
|
|
|
| 911 |
).add_to(m)
|
| 912 |
|
| 913 |
label_html = f"""
|
|
@@ -918,25 +997,28 @@ def add_permanent_label(m, name, lat, lon):
|
|
| 918 |
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
|
| 919 |
white-space: nowrap;
|
| 920 |
transform: translate(8px, -12px);
|
|
|
|
| 921 |
">{html.escape(name)}</div>
|
| 922 |
"""
|
| 923 |
folium.Marker(location=(lat, lon), icon=folium.DivIcon(html=label_html)).add_to(m)
|
| 924 |
|
| 925 |
|
| 926 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
folium.CircleMarker(
|
| 928 |
location=(float(lat), float(lon)),
|
| 929 |
-
radius=
|
| 930 |
-
color="
|
| 931 |
-
weight=
|
| 932 |
fill=True,
|
| 933 |
fill_color="cyan",
|
| 934 |
-
fill_opacity=0.
|
| 935 |
-
tooltip=
|
| 936 |
-
popup=
|
| 937 |
-
f"<b>{html.escape(name)}</b><br>Lat: {float(lat):.5f}<br>Lon: {float(lon):.5f}",
|
| 938 |
-
max_width=320,
|
| 939 |
-
),
|
| 940 |
).add_to(m)
|
| 941 |
|
| 942 |
|
|
@@ -1064,9 +1146,24 @@ def build_leaflet_map(path_pts, legs, method, start_dt):
|
|
| 1064 |
marker_pts_for_bounds.append((lat, lon))
|
| 1065 |
|
| 1066 |
tlabel = _to_label_utc(dt_k)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
|
| 1068 |
is_final = (start_dt == RADAR_FIX_TIME_UTC) and (dt_k == _dt_from_label(GUIDED_FINAL_LABEL))
|
| 1069 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
if is_final:
|
| 1071 |
folium.CircleMarker(
|
| 1072 |
location=(lat, lon),
|
|
@@ -1076,6 +1173,7 @@ def build_leaflet_map(path_pts, legs, method, start_dt):
|
|
| 1076 |
fill_color="magenta",
|
| 1077 |
fill_opacity=0.95,
|
| 1078 |
tooltip=f"{tlabel} (FINAL)",
|
|
|
|
| 1079 |
).add_to(m)
|
| 1080 |
else:
|
| 1081 |
folium.CircleMarker(
|
|
@@ -1086,26 +1184,25 @@ def build_leaflet_map(path_pts, legs, method, start_dt):
|
|
| 1086 |
fill_color="white",
|
| 1087 |
fill_opacity=1.0,
|
| 1088 |
tooltip=tlabel,
|
|
|
|
| 1089 |
).add_to(m)
|
| 1090 |
|
|
|
|
| 1091 |
for name, lat, lon in WAYPOINTS:
|
| 1092 |
add_permanent_label(m, name, float(lat), float(lon))
|
| 1093 |
|
| 1094 |
-
# Requested
|
| 1095 |
-
|
| 1096 |
|
| 1097 |
-
waypoint_pts = [(float(lat), float(lon)) for _, lat, lon in WAYPOINTS]
|
| 1098 |
all_ring_pts = []
|
| 1099 |
for k in PING_RINGS:
|
| 1100 |
all_ring_pts.extend(PING_RINGS.get(k, []))
|
| 1101 |
|
| 1102 |
-
ref_pts = [(CAPT_BYRON_BAILEY_LAT, CAPT_BYRON_BAILEY_LON)]
|
| 1103 |
-
|
| 1104 |
bounds = _all_bounds(
|
| 1105 |
[path_pts]
|
| 1106 |
+ [waypoint_pts]
|
| 1107 |
+ [all_ring_pts]
|
| 1108 |
-
+ [ref_pts]
|
| 1109 |
+ ([search_area_pts_for_bounds] if search_area_pts_for_bounds else [])
|
| 1110 |
+ ([marker_pts_for_bounds] if marker_pts_for_bounds else [])
|
| 1111 |
)
|
|
@@ -1385,18 +1482,16 @@ def solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1385 |
start_dt = _guided_start_dt()
|
| 1386 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 1387 |
|
| 1388 |
-
descent_start_dt =
|
| 1389 |
-
if (descent_start_choice or "").startswith("At 00:20Z"):
|
| 1390 |
-
descent_start_dt = _dt_from_label("24:20:00")
|
| 1391 |
|
| 1392 |
pts_all = _ping_points_after(start_dt)
|
| 1393 |
pts_window = [(lbl, dt, bfo, bto) for (lbl, dt, bfo, bto) in pts_all if start_dt <= dt <= end_dt]
|
| 1394 |
if len(pts_window) < 2:
|
| 1395 |
-
return None, "", None
|
| 1396 |
|
| 1397 |
enforced_labels = _available_hard_bto_labels()
|
| 1398 |
if not enforced_labels:
|
| 1399 |
-
return None, "", None
|
| 1400 |
|
| 1401 |
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1402 |
|
|
@@ -1461,43 +1556,105 @@ def solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1461 |
sum_abs = float(np.sum(abs_arr))
|
| 1462 |
rmse = float(np.sqrt(np.mean(np.array(bto_errs, dtype=float) ** 2)))
|
| 1463 |
|
| 1464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1465 |
|
| 1466 |
def best_for_heading(h_south: float):
|
| 1467 |
coarse_turns = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, 5.0))
|
| 1468 |
best_key = None
|
| 1469 |
best_legs = None
|
|
|
|
| 1470 |
|
| 1471 |
for v in coarse_turns:
|
| 1472 |
out = _evaluate(h_south, v, do_polish=False)
|
| 1473 |
if out is None:
|
| 1474 |
continue
|
| 1475 |
-
key, legs = out
|
| 1476 |
if best_key is None or key < best_key:
|
| 1477 |
-
best_key, best_legs = key, legs
|
| 1478 |
|
| 1479 |
if best_key is None:
|
| 1480 |
return None
|
| 1481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1482 |
|
| 1483 |
coarse_headings = _frange(GUIDED_HEADING_MIN, GUIDED_HEADING_MAX, COARSE_HEADING_STEP_DEG)
|
|
|
|
| 1484 |
best = None
|
| 1485 |
best_legs = None
|
|
|
|
| 1486 |
|
| 1487 |
for h in coarse_headings:
|
| 1488 |
out = best_for_heading(h)
|
| 1489 |
if out is None:
|
| 1490 |
continue
|
| 1491 |
-
key, legs = out
|
|
|
|
| 1492 |
if best is None or key < best:
|
| 1493 |
-
best, best_legs = key, legs
|
| 1494 |
|
| 1495 |
if best_legs is None:
|
| 1496 |
-
return None, "", None
|
| 1497 |
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1501 |
|
| 1502 |
|
| 1503 |
# ================= Guided Solver: Variable heading & speed (FAST beam search) =================
|
|
@@ -1505,13 +1662,11 @@ def _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se
|
|
| 1505 |
start_dt = _guided_start_dt()
|
| 1506 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 1507 |
|
| 1508 |
-
descent_start_dt =
|
| 1509 |
-
if (descent_start_choice or "").startswith("At 00:20Z"):
|
| 1510 |
-
descent_start_dt = _dt_from_label("24:20:00")
|
| 1511 |
|
| 1512 |
enforced_labels = _available_hard_bto_labels()
|
| 1513 |
if not enforced_labels:
|
| 1514 |
-
return None, "", None
|
| 1515 |
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1516 |
|
| 1517 |
turn_speeds = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, VAR_TURN_SPEED_STEP))
|
|
@@ -1562,9 +1717,11 @@ def _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se
|
|
| 1562 |
|
| 1563 |
for st in beam:
|
| 1564 |
h_prev = float(st["h_prev"])
|
|
|
|
| 1565 |
cand_h = []
|
| 1566 |
for d in delta_set:
|
| 1567 |
-
h_try =
|
|
|
|
| 1568 |
cand_h.append(h_try)
|
| 1569 |
cand_h = sorted(list({round(x, 6): x for x in cand_h}.values()))
|
| 1570 |
|
|
@@ -1638,8 +1795,9 @@ def _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se
|
|
| 1638 |
best_global_legs = full_legs
|
| 1639 |
|
| 1640 |
if best_global_legs is None:
|
| 1641 |
-
return None, "", None
|
| 1642 |
|
|
|
|
| 1643 |
return best_global_legs, "", start_dt
|
| 1644 |
|
| 1645 |
|
|
@@ -1656,13 +1814,11 @@ def solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1656 |
start_dt = _guided_start_dt()
|
| 1657 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 1658 |
|
| 1659 |
-
descent_start_dt =
|
| 1660 |
-
if (descent_start_choice or "").startswith("At 00:20Z"):
|
| 1661 |
-
descent_start_dt = _dt_from_label("24:20:00")
|
| 1662 |
|
| 1663 |
enforced_labels = _available_hard_bto_labels()
|
| 1664 |
if not enforced_labels:
|
| 1665 |
-
return None, "", None
|
| 1666 |
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1667 |
|
| 1668 |
turn_speeds = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, VAR_TURN_SPEED_STEP))
|
|
@@ -1701,13 +1857,16 @@ def solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1701 |
"smooth_sum": 0.0,
|
| 1702 |
}]
|
| 1703 |
|
|
|
|
| 1704 |
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
|
|
|
| 1705 |
dt_end = _dt_from_label(end_lbl)
|
| 1706 |
meas = _lookup_measured_bto(dt_end)
|
| 1707 |
next_beam = []
|
| 1708 |
|
| 1709 |
for st in beam:
|
| 1710 |
h_prev = float(st["h_prev"])
|
|
|
|
| 1711 |
cand_h = []
|
| 1712 |
for d in VAR_HEADING_DELTA_SET:
|
| 1713 |
h_try = _clamp(h_prev + float(d), VAR_HEADING_MIN, VAR_HEADING_MAX)
|
|
@@ -1768,8 +1927,9 @@ def solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1768 |
best_global_legs = full_legs
|
| 1769 |
|
| 1770 |
if best_global_legs is None:
|
| 1771 |
-
return None, "", None
|
| 1772 |
|
|
|
|
| 1773 |
return best_global_legs, "", start_dt
|
| 1774 |
|
| 1775 |
|
|
@@ -1784,15 +1944,13 @@ def _run_common(start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, de
|
|
| 1784 |
html_map = build_leaflet_map(path_pts, legs, method, start_dt)
|
| 1785 |
stats_md = compute_stats(path_pts, seg_dists_m, total_time_min, legs)
|
| 1786 |
|
| 1787 |
-
bfo_fig,
|
| 1788 |
start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice
|
| 1789 |
)
|
| 1790 |
|
| 1791 |
-
descent_start_dt =
|
| 1792 |
-
if (descent_start_choice or "").startswith("At 00:20Z"):
|
| 1793 |
-
descent_start_dt = _dt_from_label("24:20:00")
|
| 1794 |
-
|
| 1795 |
alt_fig = build_altitude_plot(start_dt, total_time_min, alt_ft, vs_fpm, descent_start_dt)
|
|
|
|
| 1796 |
speed_fig = build_speed_profile_plot(start_dt, legs)
|
| 1797 |
|
| 1798 |
headers, rows = build_validation_table(
|
|
@@ -1809,19 +1967,20 @@ def run_sim_manual(start_lat, start_lon, legs_df, method, start_time_str, alt_ft
|
|
| 1809 |
html_map, stats_md, bfo_fig, alt_fig, speed_fig, headers, rows = _run_common(
|
| 1810 |
start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice
|
| 1811 |
)
|
| 1812 |
-
|
|
|
|
| 1813 |
return gr.update(), "", html_map, stats_md, bfo_fig, alt_fig, speed_fig, gr.update(headers=headers, value=rows)
|
| 1814 |
|
| 1815 |
|
| 1816 |
def run_guided(guided_mode, start_lat, start_lon, method, alt_ft, vs_fpm, descent_start_choice):
|
| 1817 |
if guided_mode == "Straight heading":
|
| 1818 |
-
legs,
|
| 1819 |
elif guided_mode == "Variable heading & speed":
|
| 1820 |
-
legs,
|
| 1821 |
elif guided_mode == "Variable heading & speed (SE after 22:40)":
|
| 1822 |
-
legs,
|
| 1823 |
else:
|
| 1824 |
-
legs,
|
| 1825 |
|
| 1826 |
if legs is None:
|
| 1827 |
fig = go.Figure()
|
|
@@ -1841,6 +2000,7 @@ def run_guided(guided_mode, start_lat, start_lon, method, alt_ft, vs_fpm, descen
|
|
| 1841 |
RADAR_FIX_LAT, RADAR_FIX_LON, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice
|
| 1842 |
)
|
| 1843 |
|
|
|
|
| 1844 |
return gr.update(value=legs_to_rows(legs)), "", html_map, stats_md, bfo_fig, alt_fig, speed_fig, gr.update(headers=headers, value=rows)
|
| 1845 |
|
| 1846 |
|
|
@@ -1884,10 +2044,7 @@ with gr.Blocks() as demo:
|
|
| 1884 |
alt_ft = gr.Number(value=35000, label="Altitude at start (ft)")
|
| 1885 |
vs_fpm = gr.Number(value=0, label="Vertical speed (ft/min) (negative = descent)")
|
| 1886 |
descent_start_choice = gr.Dropdown(
|
| 1887 |
-
[
|
| 1888 |
-
"None (constant VS from start)",
|
| 1889 |
-
"At 00:20Z only",
|
| 1890 |
-
],
|
| 1891 |
value="None (constant VS from start)",
|
| 1892 |
label="When does descent start? (VS applies after this time)",
|
| 1893 |
)
|
|
@@ -1896,7 +2053,7 @@ with gr.Blocks() as demo:
|
|
| 1896 |
guided_btn = gr.Button("Generate: Guided scenario")
|
| 1897 |
manual_btn = gr.Button("Simulate: Manual legs")
|
| 1898 |
|
| 1899 |
-
#
|
| 1900 |
status_md = gr.Markdown(visible=False)
|
| 1901 |
|
| 1902 |
out_map = gr.HTML()
|
|
|
|
| 13 |
# - Guided mode computes a realistic turn between 18:22→18:40 and then southern path.
|
| 14 |
# - Guided validation timestamps: 18:22, 18:40, 19:40, 20:40, 21:40, 22:40, 00:10, 00:20
|
| 15 |
#
|
| 16 |
+
# THIS VERSION (UI cleanup + fixes):
|
| 17 |
+
# - Removes the big solver/bounds markdown from the UI (status panels hidden/blank)
|
| 18 |
+
# - Adds Speed Profile plot (kt vs time)
|
| 19 |
+
# - Descent option changed:
|
| 20 |
+
# * "None (constant VS from start)"
|
| 21 |
+
# * "At 00:20Z only"
|
| 22 |
+
# - Ensures BFO at 00:20Z is measured at 182 Hz (patch + guaranteed inclusion in BFO plot)
|
| 23 |
+
# - Restores waypoint hover info (tooltip now includes name + coords; popup added)
|
| 24 |
+
# - Adds location marker at 39°10'S, 88°18'E with hover label "Captain Byron Bailey"
|
| 25 |
|
| 26 |
import os
|
| 27 |
import re
|
|
|
|
| 59 |
("MEKAR", 6.5, 96.48333333),
|
| 60 |
]
|
| 61 |
|
| 62 |
+
# NEW: Reference marker requested
|
| 63 |
+
CAPT_BYRON_BAILEY_NAME = "Captain Byron Bailey"
|
| 64 |
+
CAPT_BYRON_BAILEY_LAT = -(39 + 10 / 60) # 39°10' S
|
| 65 |
+
CAPT_BYRON_BAILEY_LON = (88 + 18 / 60) # 88°18' E
|
| 66 |
+
|
| 67 |
+
# NEW: measured BFO at 00:20Z
|
| 68 |
+
MEASURED_BFO_0020_HZ = 182.0
|
| 69 |
|
| 70 |
RING_TIMES = {
|
| 71 |
"Ping Ring 1": "18:25:00Z",
|
|
|
|
| 151 |
VAR_SMOOTH_PENALTY_US_PER_DEG = 1.5 # small penalty to avoid wiggles (still BTO-first)
|
| 152 |
|
| 153 |
# ===== SE-bias-after-22:40 (new mode) =====
|
| 154 |
+
SE_EAST_DIVERGE_START_SEG_INDEX = 4 # seg 4 starts after 22:40
|
|
|
|
|
|
|
|
|
|
| 155 |
SE_VAR_EAST_DELTA_SET = [-8, -4, -2, 0, 2] # prefer small decreases (eastward) after 22:40
|
| 156 |
SE_EAST_BIAS_PENALTY_US_PER_DEG = 0.8 # small; BTO still dominates
|
| 157 |
|
|
|
|
| 509 |
return fp._time_label_to_dt(s, fp.SAT_EPOCH_UTC)
|
| 510 |
|
| 511 |
|
| 512 |
+
def dt_ping6_utc() -> datetime:
|
| 513 |
+
return fp._time_label_to_dt("22:40:00", fp.SAT_EPOCH_UTC)
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def dt_ping7_utc() -> datetime:
|
| 517 |
+
return fp._time_label_to_dt("24:10:00", fp.SAT_EPOCH_UTC)
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def dt_0020_utc() -> datetime:
|
| 521 |
+
return fp._time_label_to_dt("24:20:00", fp.SAT_EPOCH_UTC)
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
# ================= PATCH satLoc: ensure measured BFO at 00:20 is 182 Hz =================
|
| 525 |
+
def _patch_measured_bfo_0020():
|
| 526 |
+
target_labels = {"24:20:00", "00:20:00"}
|
| 527 |
+
patched = False
|
| 528 |
+
try:
|
| 529 |
+
for row in fp.satLoc:
|
| 530 |
+
try:
|
| 531 |
+
lbl = fp._extract_time_string(row[0])
|
| 532 |
+
except Exception:
|
| 533 |
+
continue
|
| 534 |
+
if lbl in target_labels:
|
| 535 |
+
try:
|
| 536 |
+
row[SATLOC_IDX_MEAS_BFO] = float(MEASURED_BFO_0020_HZ)
|
| 537 |
+
patched = True
|
| 538 |
+
except Exception:
|
| 539 |
+
pass
|
| 540 |
+
except Exception:
|
| 541 |
+
return False
|
| 542 |
+
return patched
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
_ = _patch_measured_bfo_0020()
|
| 546 |
+
|
| 547 |
+
|
| 548 |
# ================= ALTITUDE HELPERS =================
|
| 549 |
def altitude_ft_at_dt(dt_ping: datetime, start_dt: datetime, alt0_ft: float,
|
| 550 |
vs_fpm: float, descent_start_dt: datetime | None):
|
|
|
|
| 583 |
return fig
|
| 584 |
|
| 585 |
|
| 586 |
+
# ================= SPEED PROFILE PLOT =================
|
| 587 |
def build_speed_profile_plot(start_dt: datetime, legs: list[tuple[float, float, float]]):
|
| 588 |
+
# Step-like plot: speed held constant across each leg
|
| 589 |
+
xs = [0.0]
|
| 590 |
+
ys = []
|
| 591 |
+
cum_min = 0.0
|
| 592 |
+
for (_hdg, spd, mins) in legs:
|
|
|
|
| 593 |
mins = float(mins)
|
| 594 |
+
spd = float(spd)
|
| 595 |
+
if mins <= 0:
|
| 596 |
+
continue
|
| 597 |
+
if not ys:
|
| 598 |
+
ys.append(spd)
|
| 599 |
+
else:
|
| 600 |
+
ys.append(ys[-1])
|
| 601 |
+
# extend step
|
| 602 |
+
xs.append(cum_min)
|
| 603 |
+
ys.append(spd)
|
| 604 |
+
cum_min += mins
|
| 605 |
+
xs.append(cum_min)
|
| 606 |
+
ys.append(spd)
|
| 607 |
+
|
| 608 |
+
if not ys:
|
| 609 |
+
fig = go.Figure()
|
| 610 |
+
fig.update_layout(title="Speed Profile", xaxis_title="Minutes since start", yaxis_title="Speed (kt)", height=320)
|
| 611 |
+
return fig
|
| 612 |
|
| 613 |
fig = go.Figure()
|
| 614 |
+
fig.add_trace(go.Scatter(x=xs[1:], y=ys, mode="lines", name="Speed (kt)"))
|
| 615 |
fig.update_layout(
|
| 616 |
title="Speed Profile",
|
| 617 |
xaxis_title="Minutes since start",
|
|
|
|
| 782 |
return pts
|
| 783 |
|
| 784 |
|
| 785 |
+
def _descent_start_dt_from_choice(descent_start_choice: str) -> datetime | None:
|
| 786 |
+
ch = (descent_start_choice or "").strip()
|
| 787 |
+
if ch.startswith("At 00:20Z only"):
|
| 788 |
+
return dt_0020_utc()
|
| 789 |
+
if ch.startswith("None"):
|
| 790 |
+
return None
|
| 791 |
+
return None
|
| 792 |
+
|
| 793 |
+
|
| 794 |
def bfo_analysis_with_legs(start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice):
|
| 795 |
pts = _ping_points_after(start_dt)
|
| 796 |
|
| 797 |
+
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
|
|
|
|
|
|
| 798 |
|
| 799 |
leg_secs = [float(m) * 60.0 for _, _, m in legs]
|
| 800 |
boundaries = np.cumsum([0.0] + leg_secs)
|
|
|
|
| 804 |
|
| 805 |
x_labels, measured, calculated = [], [], []
|
| 806 |
|
| 807 |
+
# Ensure 00:20 is included as a measured point in the plot if within window
|
| 808 |
+
dt_0020 = dt_0020_utc()
|
| 809 |
+
have_0020 = any((lbl.replace("Z", "") in {"24:20:00", "00:20:00"}) for (lbl, *_rest) in pts)
|
| 810 |
+
if (start_dt <= dt_0020 <= (start_dt + timedelta(seconds=total_end))) and not have_0020:
|
| 811 |
+
pts.append(("24:20:00", dt_0020, float(MEASURED_BFO_0020_HZ), None))
|
| 812 |
+
|
| 813 |
+
pts = sorted(pts, key=lambda x: x[1])
|
| 814 |
+
|
| 815 |
for label, dt_ping, meas_bfo, _meas_bto in pts:
|
| 816 |
t_sec = (dt_ping - start_dt).total_seconds()
|
| 817 |
if t_sec < 0 or t_sec > total_end:
|
|
|
|
| 836 |
spd_mps = float(spd) * 1852.0 / 3600.0
|
| 837 |
pred_bfo = float(fp.calculateBFO_at_time(spd_mps, float(hdg), float(lat), float(lon), alt_m, vs_mps, dt_ping))
|
| 838 |
|
| 839 |
+
x_labels.append(label if label.endswith("Z") else (label + "Z"))
|
| 840 |
measured.append(float(meas_bfo))
|
| 841 |
calculated.append(float(pred_bfo))
|
| 842 |
|
|
|
|
| 866 |
margin=dict(l=60, r=160, t=80, b=60),
|
| 867 |
height=520,
|
| 868 |
)
|
| 869 |
+
|
| 870 |
+
# UI request: remove "BFO scoring" markdown from UI
|
| 871 |
return fig, ""
|
| 872 |
|
| 873 |
|
|
|
|
| 884 |
start_lat, start_lon, method, start_dt, legs,
|
| 885 |
alt_ft, vs_fpm, descent_start_choice
|
| 886 |
):
|
| 887 |
+
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
|
|
|
|
|
|
| 888 |
|
| 889 |
is_guided = (start_dt == RADAR_FIX_TIME_UTC)
|
| 890 |
|
|
|
|
| 923 |
|
| 924 |
bfo_meas, bto_meas = _lookup_measured_at_dt(dt_k)
|
| 925 |
|
| 926 |
+
# Force measured BFO at 00:20 to 182 Hz
|
| 927 |
+
try:
|
| 928 |
+
if dt_k == dt_0020_utc():
|
| 929 |
+
bfo_meas = float(MEASURED_BFO_0020_HZ)
|
| 930 |
+
except Exception:
|
| 931 |
+
pass
|
| 932 |
+
|
| 933 |
bfo_err = None if bfo_meas is None else (float(bfo_calc) - float(bfo_meas))
|
| 934 |
bto_err = None if bto_meas is None else (float(bto_calc) - float(bto_meas))
|
| 935 |
|
|
|
|
| 970 |
|
| 971 |
|
| 972 |
def add_permanent_label(m, name, lat, lon):
|
| 973 |
+
# Tooltip should contain waypoint info again (name + coords)
|
| 974 |
+
tip = f"{html.escape(name)}<br>Lat: {float(lat):.5f}<br>Lon: {float(lon):.5f}"
|
| 975 |
+
pop = folium.Popup(
|
| 976 |
+
f"<b>{html.escape(name)}</b><br>Lat: {float(lat):.5f}<br>Lon: {float(lon):.5f}",
|
| 977 |
+
max_width=260,
|
| 978 |
+
)
|
| 979 |
+
|
| 980 |
folium.CircleMarker(
|
| 981 |
location=(lat, lon),
|
| 982 |
radius=4,
|
|
|
|
| 985 |
fill=True,
|
| 986 |
fill_color="white",
|
| 987 |
fill_opacity=1.0,
|
| 988 |
+
tooltip=folium.Tooltip(tip, sticky=True),
|
| 989 |
+
popup=pop,
|
| 990 |
).add_to(m)
|
| 991 |
|
| 992 |
label_html = f"""
|
|
|
|
| 997 |
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
|
| 998 |
white-space: nowrap;
|
| 999 |
transform: translate(8px, -12px);
|
| 1000 |
+
pointer-events: none;
|
| 1001 |
">{html.escape(name)}</div>
|
| 1002 |
"""
|
| 1003 |
folium.Marker(location=(lat, lon), icon=folium.DivIcon(html=label_html)).add_to(m)
|
| 1004 |
|
| 1005 |
|
| 1006 |
+
def add_named_marker(m, name: str, lat: float, lon: float):
|
| 1007 |
+
tip = f"{html.escape(name)}<br>Lat: {float(lat):.5f}<br>Lon: {float(lon):.5f}"
|
| 1008 |
+
pop = folium.Popup(
|
| 1009 |
+
f"<b>{html.escape(name)}</b><br>Lat: {float(lat):.5f}<br>Lon: {float(lon):.5f}",
|
| 1010 |
+
max_width=260,
|
| 1011 |
+
)
|
| 1012 |
folium.CircleMarker(
|
| 1013 |
location=(float(lat), float(lon)),
|
| 1014 |
+
radius=6,
|
| 1015 |
+
color="white",
|
| 1016 |
+
weight=2,
|
| 1017 |
fill=True,
|
| 1018 |
fill_color="cyan",
|
| 1019 |
+
fill_opacity=0.9,
|
| 1020 |
+
tooltip=folium.Tooltip(tip, sticky=True),
|
| 1021 |
+
popup=pop,
|
|
|
|
|
|
|
|
|
|
| 1022 |
).add_to(m)
|
| 1023 |
|
| 1024 |
|
|
|
|
| 1146 |
marker_pts_for_bounds.append((lat, lon))
|
| 1147 |
|
| 1148 |
tlabel = _to_label_utc(dt_k)
|
| 1149 |
+
elapsed_min = st["elapsed_min"]
|
| 1150 |
+
hdg = st["heading_deg"]
|
| 1151 |
+
spd = st["speed_kt"]
|
| 1152 |
+
dist_km = st["dist_km"]
|
| 1153 |
|
| 1154 |
is_final = (start_dt == RADAR_FIX_TIME_UTC) and (dt_k == _dt_from_label(GUIDED_FINAL_LABEL))
|
| 1155 |
|
| 1156 |
+
popup_lines = [
|
| 1157 |
+
f"<b>{'FINAL RESTING POINT' if is_final else 'Validation point'}</b>",
|
| 1158 |
+
f"UTC time: {tlabel}",
|
| 1159 |
+
f"Lat: {lat:.5f}",
|
| 1160 |
+
f"Lon: {lon:.5f}",
|
| 1161 |
+
f"Time elapsed: {elapsed_min:.1f} min",
|
| 1162 |
+
f"Heading: {hdg:.1f}°",
|
| 1163 |
+
f"Speed: {spd:.1f} kt",
|
| 1164 |
+
f"Distance flown: {dist_km:.2f} km",
|
| 1165 |
+
]
|
| 1166 |
+
|
| 1167 |
if is_final:
|
| 1168 |
folium.CircleMarker(
|
| 1169 |
location=(lat, lon),
|
|
|
|
| 1173 |
fill_color="magenta",
|
| 1174 |
fill_opacity=0.95,
|
| 1175 |
tooltip=f"{tlabel} (FINAL)",
|
| 1176 |
+
popup=folium.Popup("<br>".join(popup_lines), max_width=360),
|
| 1177 |
).add_to(m)
|
| 1178 |
else:
|
| 1179 |
folium.CircleMarker(
|
|
|
|
| 1184 |
fill_color="white",
|
| 1185 |
fill_opacity=1.0,
|
| 1186 |
tooltip=tlabel,
|
| 1187 |
+
popup=folium.Popup("<br>".join(popup_lines), max_width=360),
|
| 1188 |
).add_to(m)
|
| 1189 |
|
| 1190 |
+
# Waypoints
|
| 1191 |
for name, lat, lon in WAYPOINTS:
|
| 1192 |
add_permanent_label(m, name, float(lat), float(lon))
|
| 1193 |
|
| 1194 |
+
# Requested marker: Captain Byron Bailey
|
| 1195 |
+
add_named_marker(m, CAPT_BYRON_BAILEY_NAME, CAPT_BYRON_BAILEY_LAT, CAPT_BYRON_BAILEY_LON)
|
| 1196 |
|
| 1197 |
+
waypoint_pts = [(float(lat), float(lon)) for _, lat, lon in WAYPOINTS] + [(CAPT_BYRON_BAILEY_LAT, CAPT_BYRON_BAILEY_LON)]
|
| 1198 |
all_ring_pts = []
|
| 1199 |
for k in PING_RINGS:
|
| 1200 |
all_ring_pts.extend(PING_RINGS.get(k, []))
|
| 1201 |
|
|
|
|
|
|
|
| 1202 |
bounds = _all_bounds(
|
| 1203 |
[path_pts]
|
| 1204 |
+ [waypoint_pts]
|
| 1205 |
+ [all_ring_pts]
|
|
|
|
| 1206 |
+ ([search_area_pts_for_bounds] if search_area_pts_for_bounds else [])
|
| 1207 |
+ ([marker_pts_for_bounds] if marker_pts_for_bounds else [])
|
| 1208 |
)
|
|
|
|
| 1482 |
start_dt = _guided_start_dt()
|
| 1483 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 1484 |
|
| 1485 |
+
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
|
|
|
|
|
|
| 1486 |
|
| 1487 |
pts_all = _ping_points_after(start_dt)
|
| 1488 |
pts_window = [(lbl, dt, bfo, bto) for (lbl, dt, bfo, bto) in pts_all if start_dt <= dt <= end_dt]
|
| 1489 |
if len(pts_window) < 2:
|
| 1490 |
+
return None, "Guided solve: not enough ping points to score.", None
|
| 1491 |
|
| 1492 |
enforced_labels = _available_hard_bto_labels()
|
| 1493 |
if not enforced_labels:
|
| 1494 |
+
return None, "Guided solve: no measured BTO available at any enforced ring times.", None
|
| 1495 |
|
| 1496 |
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1497 |
|
|
|
|
| 1556 |
sum_abs = float(np.sum(abs_arr))
|
| 1557 |
rmse = float(np.sqrt(np.mean(np.array(bto_errs, dtype=float) ** 2)))
|
| 1558 |
|
| 1559 |
+
details = {
|
| 1560 |
+
"h_south": float(h_south),
|
| 1561 |
+
"v_turn": float(v_turn),
|
| 1562 |
+
"south_speeds": [float(v) for v in south_speeds],
|
| 1563 |
+
"south_errs_us": [float(e) for e in south_errs],
|
| 1564 |
+
"enforced_labels": enforced_labels,
|
| 1565 |
+
"max_abs_err_us": max_abs,
|
| 1566 |
+
"sum_abs_err_us": sum_abs,
|
| 1567 |
+
"rmse_bto_us": rmse,
|
| 1568 |
+
}
|
| 1569 |
+
return (max_abs, sum_abs, rmse), full_legs, details
|
| 1570 |
|
| 1571 |
def best_for_heading(h_south: float):
|
| 1572 |
coarse_turns = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, 5.0))
|
| 1573 |
best_key = None
|
| 1574 |
best_legs = None
|
| 1575 |
+
best_det = None
|
| 1576 |
|
| 1577 |
for v in coarse_turns:
|
| 1578 |
out = _evaluate(h_south, v, do_polish=False)
|
| 1579 |
if out is None:
|
| 1580 |
continue
|
| 1581 |
+
key, legs, det = out
|
| 1582 |
if best_key is None or key < best_key:
|
| 1583 |
+
best_key, best_legs, best_det = key, legs, det
|
| 1584 |
|
| 1585 |
if best_key is None:
|
| 1586 |
return None
|
| 1587 |
+
|
| 1588 |
+
v0 = float(best_det["v_turn"])
|
| 1589 |
+
v_lo = _clamp(v0 - 7.5, TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT)
|
| 1590 |
+
v_hi = _clamp(v0 + 7.5, TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT)
|
| 1591 |
+
|
| 1592 |
+
def obj(v):
|
| 1593 |
+
out2 = _evaluate(h_south, v, do_polish=False)
|
| 1594 |
+
if out2 is None:
|
| 1595 |
+
return 1e9
|
| 1596 |
+
(max_abs, sum_abs, _rmse), _legs, _det = out2
|
| 1597 |
+
return float(max_abs) + POLISH_OBJ_EPS * float(sum_abs)
|
| 1598 |
+
|
| 1599 |
+
v_best, _ = _golden_min_abs(obj, v_lo, v_hi, iters=16)
|
| 1600 |
+
out3 = _evaluate(h_south, v_best, do_polish=False)
|
| 1601 |
+
if out3 is None:
|
| 1602 |
+
return best_key, best_legs, best_det
|
| 1603 |
+
return out3
|
| 1604 |
|
| 1605 |
coarse_headings = _frange(GUIDED_HEADING_MIN, GUIDED_HEADING_MAX, COARSE_HEADING_STEP_DEG)
|
| 1606 |
+
candidates = []
|
| 1607 |
best = None
|
| 1608 |
best_legs = None
|
| 1609 |
+
best_det = None
|
| 1610 |
|
| 1611 |
for h in coarse_headings:
|
| 1612 |
out = best_for_heading(h)
|
| 1613 |
if out is None:
|
| 1614 |
continue
|
| 1615 |
+
key, legs, det = out
|
| 1616 |
+
candidates.append((key, float(det["h_south"]), float(det["v_turn"])))
|
| 1617 |
if best is None or key < best:
|
| 1618 |
+
best, best_legs, best_det = key, legs, det
|
| 1619 |
|
| 1620 |
if best_legs is None:
|
| 1621 |
+
return None, "Guided solve: no solution found within bounds.", None
|
| 1622 |
|
| 1623 |
+
h0 = float(best_det["h_south"])
|
| 1624 |
+
refine_headings = _refine_range(h0, REFINE_HEADING_HALF_WINDOW_DEG, REFINE_HEADING_STEP_DEG, GUIDED_HEADING_MIN, GUIDED_HEADING_MAX)
|
| 1625 |
+
for h in refine_headings:
|
| 1626 |
+
out = best_for_heading(h)
|
| 1627 |
+
if out is None:
|
| 1628 |
+
continue
|
| 1629 |
+
key, legs, det = out
|
| 1630 |
+
candidates.append((key, float(det["h_south"]), float(det["v_turn"])))
|
| 1631 |
+
if best is None or key < best:
|
| 1632 |
+
best, best_legs, best_det = key, legs, det
|
| 1633 |
+
|
| 1634 |
+
candidates_sorted = sorted(candidates, key=lambda x: x[0])
|
| 1635 |
+
uniq = []
|
| 1636 |
+
seen = set()
|
| 1637 |
+
for key, hh, vv in candidates_sorted:
|
| 1638 |
+
k2 = (round(hh, 4), round(vv, 4))
|
| 1639 |
+
if k2 in seen:
|
| 1640 |
+
continue
|
| 1641 |
+
seen.add(k2)
|
| 1642 |
+
uniq.append((key, hh, vv))
|
| 1643 |
+
if len(uniq) >= max(1, int(TOPK_POLISH)):
|
| 1644 |
+
break
|
| 1645 |
+
|
| 1646 |
+
best_final_key, best_final_legs, best_final_det = best, best_legs, best_det
|
| 1647 |
+
if ENABLE_POLISH_STRAIGHT and uniq:
|
| 1648 |
+
for _key, hh, vv in uniq:
|
| 1649 |
+
outp = _evaluate(hh, vv, do_polish=True)
|
| 1650 |
+
if outp is None:
|
| 1651 |
+
continue
|
| 1652 |
+
keyp, legsp, detp = outp
|
| 1653 |
+
if best_final_key is None or keyp < best_final_key:
|
| 1654 |
+
best_final_key, best_final_legs, best_final_det = keyp, legsp, detp
|
| 1655 |
+
|
| 1656 |
+
# UI request: remove solver report text from UI -> return empty report
|
| 1657 |
+
return best_final_legs, "", start_dt
|
| 1658 |
|
| 1659 |
|
| 1660 |
# ================= Guided Solver: Variable heading & speed (FAST beam search) =================
|
|
|
|
| 1662 |
start_dt = _guided_start_dt()
|
| 1663 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 1664 |
|
| 1665 |
+
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
|
|
|
|
|
|
| 1666 |
|
| 1667 |
enforced_labels = _available_hard_bto_labels()
|
| 1668 |
if not enforced_labels:
|
| 1669 |
+
return None, "Guided solve: no measured BTO available at any enforced ring times.", None
|
| 1670 |
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1671 |
|
| 1672 |
turn_speeds = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, VAR_TURN_SPEED_STEP))
|
|
|
|
| 1717 |
|
| 1718 |
for st in beam:
|
| 1719 |
h_prev = float(st["h_prev"])
|
| 1720 |
+
|
| 1721 |
cand_h = []
|
| 1722 |
for d in delta_set:
|
| 1723 |
+
h_try = h_prev + float(d)
|
| 1724 |
+
h_try = _clamp(h_try, VAR_HEADING_MIN, VAR_HEADING_MAX)
|
| 1725 |
cand_h.append(h_try)
|
| 1726 |
cand_h = sorted(list({round(x, 6): x for x in cand_h}.values()))
|
| 1727 |
|
|
|
|
| 1795 |
best_global_legs = full_legs
|
| 1796 |
|
| 1797 |
if best_global_legs is None:
|
| 1798 |
+
return None, "Guided solve: Variable mode found no solution within bounds.", None
|
| 1799 |
|
| 1800 |
+
# UI request: remove solver report text from UI -> return empty report
|
| 1801 |
return best_global_legs, "", start_dt
|
| 1802 |
|
| 1803 |
|
|
|
|
| 1814 |
start_dt = _guided_start_dt()
|
| 1815 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 1816 |
|
| 1817 |
+
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
|
|
|
|
|
|
| 1818 |
|
| 1819 |
enforced_labels = _available_hard_bto_labels()
|
| 1820 |
if not enforced_labels:
|
| 1821 |
+
return None, "Guided solve: no measured BTO available at any enforced ring times.", None
|
| 1822 |
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1823 |
|
| 1824 |
turn_speeds = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, VAR_TURN_SPEED_STEP))
|
|
|
|
| 1857 |
"smooth_sum": 0.0,
|
| 1858 |
}]
|
| 1859 |
|
| 1860 |
+
seg_i = -1
|
| 1861 |
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
| 1862 |
+
seg_i += 1
|
| 1863 |
dt_end = _dt_from_label(end_lbl)
|
| 1864 |
meas = _lookup_measured_bto(dt_end)
|
| 1865 |
next_beam = []
|
| 1866 |
|
| 1867 |
for st in beam:
|
| 1868 |
h_prev = float(st["h_prev"])
|
| 1869 |
+
|
| 1870 |
cand_h = []
|
| 1871 |
for d in VAR_HEADING_DELTA_SET:
|
| 1872 |
h_try = _clamp(h_prev + float(d), VAR_HEADING_MIN, VAR_HEADING_MAX)
|
|
|
|
| 1927 |
best_global_legs = full_legs
|
| 1928 |
|
| 1929 |
if best_global_legs is None:
|
| 1930 |
+
return None, "Guided solve: Constant-speed mode found no solution within bounds.", None
|
| 1931 |
|
| 1932 |
+
# UI request: remove solver report text from UI -> return empty report
|
| 1933 |
return best_global_legs, "", start_dt
|
| 1934 |
|
| 1935 |
|
|
|
|
| 1944 |
html_map = build_leaflet_map(path_pts, legs, method, start_dt)
|
| 1945 |
stats_md = compute_stats(path_pts, seg_dists_m, total_time_min, legs)
|
| 1946 |
|
| 1947 |
+
bfo_fig, _bfo_md = bfo_analysis_with_legs(
|
| 1948 |
start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice
|
| 1949 |
)
|
| 1950 |
|
| 1951 |
+
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
|
|
|
|
|
|
|
|
|
| 1952 |
alt_fig = build_altitude_plot(start_dt, total_time_min, alt_ft, vs_fpm, descent_start_dt)
|
| 1953 |
+
|
| 1954 |
speed_fig = build_speed_profile_plot(start_dt, legs)
|
| 1955 |
|
| 1956 |
headers, rows = build_validation_table(
|
|
|
|
| 1967 |
html_map, stats_md, bfo_fig, alt_fig, speed_fig, headers, rows = _run_common(
|
| 1968 |
start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice
|
| 1969 |
)
|
| 1970 |
+
|
| 1971 |
+
# UI cleanup: keep status blank; no BFO scoring markdown
|
| 1972 |
return gr.update(), "", html_map, stats_md, bfo_fig, alt_fig, speed_fig, gr.update(headers=headers, value=rows)
|
| 1973 |
|
| 1974 |
|
| 1975 |
def run_guided(guided_mode, start_lat, start_lon, method, alt_ft, vs_fpm, descent_start_choice):
|
| 1976 |
if guided_mode == "Straight heading":
|
| 1977 |
+
legs, _report_md, start_dt = solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice)
|
| 1978 |
elif guided_mode == "Variable heading & speed":
|
| 1979 |
+
legs, _report_md, start_dt = solve_guided_variable(method, alt_ft, vs_fpm, descent_start_choice)
|
| 1980 |
elif guided_mode == "Variable heading & speed (SE after 22:40)":
|
| 1981 |
+
legs, _report_md, start_dt = solve_guided_variable_se(method, alt_ft, vs_fpm, descent_start_choice)
|
| 1982 |
else:
|
| 1983 |
+
legs, _report_md, start_dt = solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice)
|
| 1984 |
|
| 1985 |
if legs is None:
|
| 1986 |
fig = go.Figure()
|
|
|
|
| 2000 |
RADAR_FIX_LAT, RADAR_FIX_LON, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice
|
| 2001 |
)
|
| 2002 |
|
| 2003 |
+
# UI cleanup: keep status blank
|
| 2004 |
return gr.update(value=legs_to_rows(legs)), "", html_map, stats_md, bfo_fig, alt_fig, speed_fig, gr.update(headers=headers, value=rows)
|
| 2005 |
|
| 2006 |
|
|
|
|
| 2044 |
alt_ft = gr.Number(value=35000, label="Altitude at start (ft)")
|
| 2045 |
vs_fpm = gr.Number(value=0, label="Vertical speed (ft/min) (negative = descent)")
|
| 2046 |
descent_start_choice = gr.Dropdown(
|
| 2047 |
+
["None (constant VS from start)", "At 00:20Z only"],
|
|
|
|
|
|
|
|
|
|
| 2048 |
value="None (constant VS from start)",
|
| 2049 |
label="When does descent start? (VS applies after this time)",
|
| 2050 |
)
|
|
|
|
| 2053 |
guided_btn = gr.Button("Generate: Guided scenario")
|
| 2054 |
manual_btn = gr.Button("Simulate: Manual legs")
|
| 2055 |
|
| 2056 |
+
# UI cleanup: keep status hidden
|
| 2057 |
status_md = gr.Markdown(visible=False)
|
| 2058 |
|
| 2059 |
out_map = gr.HTML()
|