Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
# B) Variable heading & speed (beam search, BTO-first)
|
| 7 |
# C) Variable heading & speed (SE bias after 22:40) (beam search, BTO-first + tiny east bias only after 22:40)
|
| 8 |
# D) Variable heading, CONSTANT southern speed (443 kt) (beam search, BTO-first)
|
| 9 |
-
# E) Variable heading & speed (
|
| 10 |
#
|
| 11 |
# Key behavior:
|
| 12 |
# - Last radar spot FIXED: 18:22:00Z @ 6.578N, 96.341E
|
|
@@ -15,9 +15,11 @@
|
|
| 15 |
# - Guided validation timestamps: 18:22, 18:40, 19:40, 20:40, 21:40, 22:40, 00:10, 00:20
|
| 16 |
#
|
| 17 |
# THIS VERSION (changes requested):
|
| 18 |
-
# - Mode
|
| 19 |
-
#
|
| 20 |
-
#
|
|
|
|
|
|
|
| 21 |
|
| 22 |
import os
|
| 23 |
import re
|
|
@@ -56,12 +58,12 @@ WAYPOINTS = [
|
|
| 56 |
("MEKAR", 6.5, 96.48333333),
|
| 57 |
]
|
| 58 |
|
| 59 |
-
#
|
| 60 |
CAPT_BYRON_BAILEY_NAME = "Captain Byron Bailey"
|
| 61 |
CAPT_BYRON_BAILEY_LAT = -(39 + 10 / 60) # 39°10' S
|
| 62 |
CAPT_BYRON_BAILEY_LON = (88 + 18 / 60) # 88°18' E
|
| 63 |
|
| 64 |
-
#
|
| 65 |
MEASURED_BFO_0020_HZ = 182.0
|
| 66 |
|
| 67 |
RING_TIMES = {
|
|
@@ -100,7 +102,7 @@ GUIDED_VALIDATION_LABELS = [
|
|
| 100 |
]
|
| 101 |
GUIDED_FINAL_LABEL = "24:20:00" # 00:20Z FINAL RESTING POINT
|
| 102 |
|
| 103 |
-
# Turn modeling parameters
|
| 104 |
TURN_BANK_MIN_DEG = 15.0
|
| 105 |
TURN_BANK_MAX_DEG = 30.0
|
| 106 |
TURN_BANK_DEFAULT_DEG = 25.0
|
|
@@ -113,8 +115,8 @@ TURN_SPEED_MAX_KT = 520.0
|
|
| 113 |
SOUTH_SPEED_MIN_KT = 430.0
|
| 114 |
SOUTH_SPEED_MAX_KT = 510.0
|
| 115 |
|
| 116 |
-
# =====
|
| 117 |
-
CONST_SOUTH_SPEED_KT = 443.0
|
| 118 |
|
| 119 |
# ===== Straight heading search =====
|
| 120 |
GUIDED_HEADING_MIN = 175
|
|
@@ -141,26 +143,28 @@ POLISH_OBJ_EPS = 1e-3
|
|
| 141 |
# ===== Variable heading & speed solver knobs (FAST) =====
|
| 142 |
VAR_HEADING_MIN = 170.0
|
| 143 |
VAR_HEADING_MAX = 200.0
|
| 144 |
-
VAR_TURN_SPEED_STEP = 10.0
|
| 145 |
-
VAR_FIRST_HEADING_STEP = 4.0
|
| 146 |
-
VAR_HEADING_DELTA_SET = [-6, -3, 0, 3, 6]
|
| 147 |
-
VAR_BEAM_WIDTH = 12
|
| 148 |
-
VAR_SMOOTH_PENALTY_US_PER_DEG = 1.5
|
| 149 |
-
|
| 150 |
-
# ===== SE-bias-after-22:40
|
| 151 |
-
SE_EAST_DIVERGE_START_SEG_INDEX = 4
|
| 152 |
-
SE_VAR_EAST_DELTA_SET = [-8, -4, -2, 0, 2]
|
| 153 |
-
SE_EAST_BIAS_PENALTY_US_PER_DEG = 0.8
|
| 154 |
-
|
| 155 |
-
# =====
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
| 164 |
|
| 165 |
# Search area colors (optional)
|
| 166 |
SEARCH_AREA_COLORS = {
|
|
@@ -1050,7 +1054,6 @@ def build_leaflet_map(path_pts, legs, method, start_dt):
|
|
| 1050 |
control=True,
|
| 1051 |
).add_to(m)
|
| 1052 |
|
| 1053 |
-
# NEW: mouse cursor lat/lon display
|
| 1054 |
MousePosition(
|
| 1055 |
position="topright",
|
| 1056 |
separator=" | ",
|
|
@@ -1317,7 +1320,7 @@ def _golden_min_abs(fn, lo: float, hi: float, iters: int = 24):
|
|
| 1317 |
return float(x_best), abs(float(fn(x_best)))
|
| 1318 |
|
| 1319 |
|
| 1320 |
-
def
|
| 1321 |
method: str,
|
| 1322 |
lat0: float,
|
| 1323 |
lon0: float,
|
|
@@ -1331,176 +1334,231 @@ def _fit_segment_speed(
|
|
| 1331 |
vmin: float,
|
| 1332 |
vmax: float,
|
| 1333 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1334 |
meas = _lookup_measured_bto(dt_end)
|
| 1335 |
if meas is None:
|
| 1336 |
v = 0.5 * (vmin + vmax)
|
| 1337 |
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, v, minutes)
|
| 1338 |
return float(v), float("nan"), (lat1, lon1)
|
| 1339 |
|
|
|
|
|
|
|
| 1340 |
def err(v):
|
| 1341 |
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, v, minutes)
|
| 1342 |
bto = _bto_calc(method, lat1, lon1, dt_end, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1343 |
return float(bto - meas)
|
| 1344 |
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1356 |
break
|
| 1357 |
-
if (
|
| 1358 |
-
|
| 1359 |
else:
|
| 1360 |
-
|
| 1361 |
-
v_best = 0.5 * (a + b)
|
| 1362 |
-
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, v_best, minutes)
|
| 1363 |
-
return float(v_best), float(err(v_best)), (lat1, lon1)
|
| 1364 |
|
| 1365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1366 |
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, v_best, minutes)
|
| 1367 |
return float(v_best), float(err(v_best)), (lat1, lon1)
|
| 1368 |
|
| 1369 |
|
| 1370 |
-
def
|
| 1371 |
method: str,
|
| 1372 |
-
|
|
|
|
|
|
|
|
|
|
| 1373 |
alt_ft: float,
|
| 1374 |
vs_fpm: float,
|
|
|
|
| 1375 |
descent_start_dt: datetime | None,
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
):
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
continue
|
| 1387 |
-
bto_calc = _bto_calc(method, st["lat"], st["lon"], dt_k, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1388 |
-
errs.append(float(bto_calc - meas))
|
| 1389 |
-
return errs
|
| 1390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1391 |
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1399 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1400 |
|
| 1401 |
-
def
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
continue
|
| 1406 |
-
legs.append((float(h), float(v), float(m)))
|
| 1407 |
-
return legs
|
| 1408 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1409 |
|
| 1410 |
-
#
|
| 1411 |
-
|
| 1412 |
-
|
| 1413 |
-
start_dt: datetime,
|
| 1414 |
-
end_dt: datetime,
|
| 1415 |
-
alt_ft: float,
|
| 1416 |
-
vs_fpm: float,
|
| 1417 |
-
descent_start_dt: datetime | None,
|
| 1418 |
-
h_south: float,
|
| 1419 |
-
turn_legs: list[tuple[float, float, float]],
|
| 1420 |
-
enforced_dts: list[datetime],
|
| 1421 |
-
init_speeds: list[float],
|
| 1422 |
-
passes: int = POLISH_PASSES,
|
| 1423 |
-
window_kt: float = POLISH_WINDOW_KT,
|
| 1424 |
-
step_kt: float = POLISH_STEP_KT,
|
| 1425 |
-
):
|
| 1426 |
-
speeds = [float(v) for v in init_speeds]
|
| 1427 |
|
| 1428 |
-
|
| 1429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1430 |
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
|
| 1436 |
-
|
| 1437 |
-
meas = _lookup_measured_bto(dt_end)
|
| 1438 |
-
if meas is None:
|
| 1439 |
-
seg_errs.append(float("nan"))
|
| 1440 |
-
else:
|
| 1441 |
-
bto_here = _bto_calc(method, lat_cur, lon_cur, dt_end, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1442 |
-
seg_errs.append(float(bto_here - meas))
|
| 1443 |
|
| 1444 |
-
|
| 1445 |
-
|
| 1446 |
-
|
| 1447 |
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
|
| 1451 |
-
|
| 1452 |
-
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
| 1469 |
-
|
| 1470 |
-
|
| 1471 |
-
|
| 1472 |
-
continue
|
| 1473 |
|
| 1474 |
-
|
| 1475 |
-
|
| 1476 |
-
|
| 1477 |
-
|
| 1478 |
-
|
| 1479 |
-
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
| 1484 |
-
|
| 1485 |
-
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
-
|
| 1489 |
-
|
| 1490 |
-
|
| 1491 |
-
|
| 1492 |
-
|
| 1493 |
-
|
| 1494 |
-
|
| 1495 |
-
|
| 1496 |
-
|
| 1497 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1498 |
break
|
| 1499 |
|
| 1500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1501 |
|
| 1502 |
|
| 1503 |
-
# ================= Guided Solver: Straight heading =================
|
| 1504 |
def solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice):
|
| 1505 |
start_dt = _guided_start_dt()
|
| 1506 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
|
@@ -1535,7 +1593,7 @@ def solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1535 |
south_errs = []
|
| 1536 |
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
| 1537 |
dt_end = _dt_from_label(end_lbl)
|
| 1538 |
-
v_seg, err_seg, (lat1, lon1) =
|
| 1539 |
method=method,
|
| 1540 |
lat0=lat_cur,
|
| 1541 |
lon0=lon_cur,
|
|
@@ -1553,24 +1611,27 @@ def solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1553 |
south_errs.append(float(err_seg))
|
| 1554 |
lat_cur, lon_cur = float(lat1), float(lon1)
|
| 1555 |
|
| 1556 |
-
south_legs =
|
| 1557 |
full_legs = clip_legs_to_end_dt(turn_legs + south_legs, start_dt, end_dt)
|
| 1558 |
|
|
|
|
| 1559 |
if do_polish and ENABLE_POLISH_STRAIGHT:
|
| 1560 |
-
|
| 1561 |
-
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
|
| 1565 |
-
|
| 1566 |
-
|
| 1567 |
-
|
| 1568 |
-
|
| 1569 |
-
|
| 1570 |
-
|
| 1571 |
-
|
|
|
|
|
|
|
|
|
|
| 1572 |
|
| 1573 |
-
bto_errs = _compute_bto_errors_at_enforced(method, start_dt, alt_ft, vs_fpm, descent_start_dt, full_legs, enforced_dts)
|
| 1574 |
if not bto_errs:
|
| 1575 |
return None
|
| 1576 |
|
|
@@ -1591,95 +1652,27 @@ def solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1591 |
}
|
| 1592 |
return (max_abs, sum_abs, rmse), full_legs, details
|
| 1593 |
|
| 1594 |
-
def best_for_heading(h_south: float):
|
| 1595 |
-
coarse_turns = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, 5.0))
|
| 1596 |
-
best_key = None
|
| 1597 |
-
best_legs = None
|
| 1598 |
-
best_det = None
|
| 1599 |
-
|
| 1600 |
-
for v in coarse_turns:
|
| 1601 |
-
out = _evaluate(h_south, v, do_polish=False)
|
| 1602 |
-
if out is None:
|
| 1603 |
-
continue
|
| 1604 |
-
key, legs, det = out
|
| 1605 |
-
if best_key is None or key < best_key:
|
| 1606 |
-
best_key, best_legs, best_det = key, legs, det
|
| 1607 |
-
|
| 1608 |
-
if best_key is None:
|
| 1609 |
-
return None
|
| 1610 |
-
|
| 1611 |
-
v0 = float(best_det["v_turn"])
|
| 1612 |
-
v_lo = _clamp(v0 - 7.5, TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT)
|
| 1613 |
-
v_hi = _clamp(v0 + 7.5, TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT)
|
| 1614 |
-
|
| 1615 |
-
def obj(v):
|
| 1616 |
-
out2 = _evaluate(h_south, v, do_polish=False)
|
| 1617 |
-
if out2 is None:
|
| 1618 |
-
return 1e9
|
| 1619 |
-
(max_abs, sum_abs, _rmse), _legs, _det = out2
|
| 1620 |
-
return float(max_abs) + POLISH_OBJ_EPS * float(sum_abs)
|
| 1621 |
-
|
| 1622 |
-
v_best, _ = _golden_min_abs(obj, v_lo, v_hi, iters=16)
|
| 1623 |
-
out3 = _evaluate(h_south, v_best, do_polish=False)
|
| 1624 |
-
if out3 is None:
|
| 1625 |
-
return best_key, best_legs, best_det
|
| 1626 |
-
return out3
|
| 1627 |
-
|
| 1628 |
coarse_headings = _frange(GUIDED_HEADING_MIN, GUIDED_HEADING_MAX, COARSE_HEADING_STEP_DEG)
|
| 1629 |
-
candidates = []
|
| 1630 |
best = None
|
| 1631 |
best_legs = None
|
| 1632 |
best_det = None
|
| 1633 |
|
| 1634 |
for h in coarse_headings:
|
| 1635 |
-
|
| 1636 |
-
|
| 1637 |
-
|
| 1638 |
-
|
| 1639 |
-
|
| 1640 |
-
|
| 1641 |
-
|
| 1642 |
|
| 1643 |
if best_legs is None:
|
| 1644 |
return None, "Guided solve: no solution found within bounds.", None
|
| 1645 |
|
| 1646 |
-
|
| 1647 |
-
refine_headings = _refine_range(h0, REFINE_HEADING_HALF_WINDOW_DEG, REFINE_HEADING_STEP_DEG, GUIDED_HEADING_MIN, GUIDED_HEADING_MAX)
|
| 1648 |
-
for h in refine_headings:
|
| 1649 |
-
out = best_for_heading(h)
|
| 1650 |
-
if out is None:
|
| 1651 |
-
continue
|
| 1652 |
-
key, legs, det = out
|
| 1653 |
-
candidates.append((key, float(det["h_south"]), float(det["v_turn"])))
|
| 1654 |
-
if best is None or key < best:
|
| 1655 |
-
best, best_legs, best_det = key, legs, det
|
| 1656 |
-
|
| 1657 |
-
candidates_sorted = sorted(candidates, key=lambda x: x[0])
|
| 1658 |
-
uniq = []
|
| 1659 |
-
seen = set()
|
| 1660 |
-
for key, hh, vv in candidates_sorted:
|
| 1661 |
-
k2 = (round(hh, 4), round(vv, 4))
|
| 1662 |
-
if k2 in seen:
|
| 1663 |
-
continue
|
| 1664 |
-
seen.add(k2)
|
| 1665 |
-
uniq.append((key, hh, vv))
|
| 1666 |
-
if len(uniq) >= max(1, int(TOPK_POLISH)):
|
| 1667 |
-
break
|
| 1668 |
-
|
| 1669 |
-
best_final_key, best_final_legs, best_final_det = best, best_legs, best_det
|
| 1670 |
-
if ENABLE_POLISH_STRAIGHT and uniq:
|
| 1671 |
-
for _key, hh, vv in uniq:
|
| 1672 |
-
outp = _evaluate(hh, vv, do_polish=True)
|
| 1673 |
-
if outp is None:
|
| 1674 |
-
continue
|
| 1675 |
-
keyp, legsp, detp = outp
|
| 1676 |
-
if best_final_key is None or keyp < best_final_key:
|
| 1677 |
-
best_final_key, best_final_legs, best_final_det = keyp, legsp, detp
|
| 1678 |
-
|
| 1679 |
-
return best_final_legs, "", start_dt
|
| 1680 |
|
| 1681 |
|
| 1682 |
-
# ================= Guided Solver: Variable heading & speed (
|
| 1683 |
def _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se_bias_after_2240: bool):
|
| 1684 |
start_dt = _guided_start_dt()
|
| 1685 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
|
@@ -1730,8 +1723,8 @@ def _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se
|
|
| 1730 |
seg_i = -1
|
| 1731 |
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
| 1732 |
seg_i += 1
|
| 1733 |
-
|
| 1734 |
-
meas = _lookup_measured_bto(
|
| 1735 |
next_beam = []
|
| 1736 |
|
| 1737 |
use_se_bias = bool(se_bias_after_2240 and seg_i >= SE_EAST_DIVERGE_START_SEG_INDEX)
|
|
@@ -1742,19 +1735,18 @@ def _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se
|
|
| 1742 |
|
| 1743 |
cand_h = []
|
| 1744 |
for d in delta_set:
|
| 1745 |
-
h_try = h_prev + float(d)
|
| 1746 |
-
h_try = _clamp(h_try, VAR_HEADING_MIN, VAR_HEADING_MAX)
|
| 1747 |
cand_h.append(h_try)
|
| 1748 |
cand_h = sorted(list({round(x, 6): x for x in cand_h}.values()))
|
| 1749 |
|
| 1750 |
for h_try in cand_h:
|
| 1751 |
-
v_seg, err_seg, (lat1, lon1) =
|
| 1752 |
method=method,
|
| 1753 |
lat0=float(st["lat"]),
|
| 1754 |
lon0=float(st["lon"]),
|
| 1755 |
hdg=float(h_try),
|
| 1756 |
minutes=float(mins),
|
| 1757 |
-
dt_end=
|
| 1758 |
alt_ft=alt_ft,
|
| 1759 |
vs_fpm=vs_fpm,
|
| 1760 |
start_dt=start_dt,
|
|
@@ -1802,10 +1794,20 @@ def _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se
|
|
| 1802 |
continue
|
| 1803 |
|
| 1804 |
for st in beam:
|
| 1805 |
-
south_legs =
|
| 1806 |
full_legs = clip_legs_to_end_dt(st["turn_legs"] + south_legs, start_dt, end_dt)
|
| 1807 |
|
| 1808 |
-
bto_errs =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1809 |
if not bto_errs:
|
| 1810 |
continue
|
| 1811 |
|
|
@@ -1830,120 +1832,62 @@ def solve_guided_variable_se(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1830 |
return _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se_bias_after_2240=True)
|
| 1831 |
|
| 1832 |
|
| 1833 |
-
# ================= Guided Solver: Variable heading, CONSTANT southern speed (
|
| 1834 |
def solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice):
|
| 1835 |
start_dt = _guided_start_dt()
|
| 1836 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 1837 |
-
|
| 1838 |
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
| 1839 |
|
| 1840 |
enforced_labels = _available_hard_bto_labels()
|
| 1841 |
if not enforced_labels:
|
| 1842 |
return None, "Guided solve: no measured BTO available at any enforced ring times.", None
|
| 1843 |
-
enforced_dts = [_dt_from_label(lbl) for lbl in enforced_labels]
|
| 1844 |
|
|
|
|
|
|
|
|
|
|
| 1845 |
turn_speeds = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, VAR_TURN_SPEED_STEP))
|
| 1846 |
first_headings = _frange(VAR_HEADING_MIN, VAR_HEADING_MAX, VAR_FIRST_HEADING_STEP)
|
| 1847 |
|
| 1848 |
best_global = None
|
| 1849 |
best_global_legs = None
|
| 1850 |
|
| 1851 |
-
def score_from_errors(errs: list[float]):
|
| 1852 |
-
if not errs:
|
| 1853 |
-
return None
|
| 1854 |
-
abs_arr = np.abs(np.array(errs, dtype=float))
|
| 1855 |
-
return float(np.max(abs_arr)), float(np.sum(abs_arr)), float(np.sqrt(np.mean(abs_arr * abs_arr)))
|
| 1856 |
-
|
| 1857 |
-
v_const = float(CONST_SOUTH_SPEED_KT)
|
| 1858 |
-
|
| 1859 |
for v_turn in turn_speeds:
|
| 1860 |
for h1 in first_headings:
|
| 1861 |
-
turn_legs,
|
| 1862 |
-
speed_knots=float(v_turn),
|
| 1863 |
-
method=method,
|
| 1864 |
-
heading_final_deg=float(h1),
|
| 1865 |
-
)
|
| 1866 |
pts_turn, _, _ = simulate_path(RADAR_FIX_LAT, RADAR_FIX_LON, turn_legs, method)
|
| 1867 |
lat0, lon0 = float(pts_turn[-1][0]), float(pts_turn[-1][1])
|
| 1868 |
|
| 1869 |
-
|
| 1870 |
-
|
| 1871 |
-
|
| 1872 |
-
|
| 1873 |
-
|
| 1874 |
-
"v_turn": float(v_turn),
|
| 1875 |
-
"turn_legs": turn_legs,
|
| 1876 |
-
"max_abs": 0.0,
|
| 1877 |
-
"sum_abs": 0.0,
|
| 1878 |
-
"smooth_sum": 0.0,
|
| 1879 |
-
}]
|
| 1880 |
-
|
| 1881 |
-
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
| 1882 |
dt_end = _dt_from_label(end_lbl)
|
| 1883 |
-
|
| 1884 |
-
|
| 1885 |
-
|
| 1886 |
-
|
| 1887 |
-
|
| 1888 |
-
|
| 1889 |
-
|
| 1890 |
-
|
| 1891 |
-
|
| 1892 |
-
|
| 1893 |
-
cand_h = sorted(list({round(x, 6): x for x in cand_h}.values()))
|
| 1894 |
-
|
| 1895 |
-
for h_try in cand_h:
|
| 1896 |
-
lat1, lon1 = _simulate_one_leg_end(method, float(st["lat"]), float(st["lon"]), float(h_try), v_const, float(mins))
|
| 1897 |
-
|
| 1898 |
-
if meas is None:
|
| 1899 |
-
abs_err = 0.0
|
| 1900 |
-
else:
|
| 1901 |
-
bto_here = _bto_calc(method, lat1, lon1, dt_end, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1902 |
-
abs_err = abs(float(bto_here - float(meas)))
|
| 1903 |
-
|
| 1904 |
-
smooth_pen = abs(float(h_try) - float(h_prev)) * VAR_SMOOTH_PENALTY_US_PER_DEG
|
| 1905 |
-
|
| 1906 |
-
max_abs = max(float(st["max_abs"]), float(abs_err))
|
| 1907 |
-
sum_abs = float(st["sum_abs"]) + float(abs_err)
|
| 1908 |
-
smooth_sum = float(st["smooth_sum"]) + float(smooth_pen)
|
| 1909 |
-
|
| 1910 |
-
next_beam.append({
|
| 1911 |
-
"lat": float(lat1),
|
| 1912 |
-
"lon": float(lon1),
|
| 1913 |
-
"headings": st["headings"] + [float(h_try)],
|
| 1914 |
-
"h_prev": float(h_try),
|
| 1915 |
-
"v_turn": float(st["v_turn"]),
|
| 1916 |
-
"turn_legs": st["turn_legs"],
|
| 1917 |
-
"max_abs": max_abs,
|
| 1918 |
-
"sum_abs": sum_abs,
|
| 1919 |
-
"smooth_sum": smooth_sum,
|
| 1920 |
-
})
|
| 1921 |
-
|
| 1922 |
-
if not next_beam:
|
| 1923 |
-
beam = []
|
| 1924 |
-
break
|
| 1925 |
-
|
| 1926 |
-
next_beam.sort(key=lambda x: (x["max_abs"], x["sum_abs"], x["smooth_sum"]))
|
| 1927 |
-
beam = next_beam[:max(1, int(VAR_BEAM_WIDTH))]
|
| 1928 |
-
|
| 1929 |
-
if not beam:
|
| 1930 |
-
continue
|
| 1931 |
-
|
| 1932 |
-
for st in beam:
|
| 1933 |
-
speeds = [v_const] * len(GUIDED_SOUTH_SEG_MIN)
|
| 1934 |
-
south_legs = _build_south_legs_variable(st["headings"], speeds, GUIDED_SOUTH_SEG_MIN)
|
| 1935 |
-
full_legs = clip_legs_to_end_dt(st["turn_legs"] + south_legs, start_dt, end_dt)
|
| 1936 |
-
|
| 1937 |
-
bto_errs = _compute_bto_errors_at_enforced(method, start_dt, alt_ft, vs_fpm, descent_start_dt, full_legs, enforced_dts)
|
| 1938 |
-
if not bto_errs:
|
| 1939 |
continue
|
| 1940 |
-
|
| 1941 |
-
|
| 1942 |
-
|
| 1943 |
-
|
| 1944 |
-
|
| 1945 |
-
|
| 1946 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1947 |
|
| 1948 |
if best_global_legs is None:
|
| 1949 |
return None, "Guided solve: Constant-speed mode found no solution within bounds.", None
|
|
@@ -1951,243 +1895,68 @@ def solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice):
|
|
| 1951 |
return best_global_legs, "", start_dt
|
| 1952 |
|
| 1953 |
|
| 1954 |
-
# =================
|
| 1955 |
-
def _fit_heading_speed_force_bto_bfo(
|
| 1956 |
-
method: str,
|
| 1957 |
-
lat0: float,
|
| 1958 |
-
lon0: float,
|
| 1959 |
-
h_prev: float,
|
| 1960 |
-
minutes: float,
|
| 1961 |
-
dt_end: datetime,
|
| 1962 |
-
alt_ft: float,
|
| 1963 |
-
vs_fpm: float,
|
| 1964 |
-
start_dt: datetime,
|
| 1965 |
-
descent_start_dt: datetime | None,
|
| 1966 |
-
):
|
| 1967 |
-
# Measurements at dt_end
|
| 1968 |
-
bfo_meas, bto_meas = _lookup_measured_at_dt(dt_end)
|
| 1969 |
-
if dt_end == dt_0020_utc():
|
| 1970 |
-
try:
|
| 1971 |
-
bfo_meas = float(MEASURED_BFO_0020_HZ)
|
| 1972 |
-
except Exception:
|
| 1973 |
-
pass
|
| 1974 |
-
|
| 1975 |
-
have_bto = (bto_meas is not None)
|
| 1976 |
-
have_bfo = (bfo_meas is not None)
|
| 1977 |
-
|
| 1978 |
-
# If we can't force both, fallback to "BTO only" speed fit and keep heading near previous
|
| 1979 |
-
if not (have_bto and have_bfo):
|
| 1980 |
-
h_use = _clamp(float(h_prev), VAR_HEADING_MIN, VAR_HEADING_MAX)
|
| 1981 |
-
v_use, bto_err, (lat1, lon1) = _fit_segment_speed(
|
| 1982 |
-
method=method,
|
| 1983 |
-
lat0=float(lat0),
|
| 1984 |
-
lon0=float(lon0),
|
| 1985 |
-
hdg=float(h_use),
|
| 1986 |
-
minutes=float(minutes),
|
| 1987 |
-
dt_end=dt_end,
|
| 1988 |
-
alt_ft=alt_ft,
|
| 1989 |
-
vs_fpm=vs_fpm,
|
| 1990 |
-
start_dt=start_dt,
|
| 1991 |
-
descent_start_dt=descent_start_dt,
|
| 1992 |
-
vmin=SOUTH_SPEED_MIN_KT,
|
| 1993 |
-
vmax=SOUTH_SPEED_MAX_KT,
|
| 1994 |
-
)
|
| 1995 |
-
bfo_err = float("nan")
|
| 1996 |
-
if have_bfo:
|
| 1997 |
-
bfo_calc = _bfo_calc(method, lat1, lon1, dt_end, h_use, v_use, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1998 |
-
bfo_err = float(bfo_calc - float(bfo_meas))
|
| 1999 |
-
return float(h_use), float(v_use), float(bto_err), float(bfo_err), (float(lat1), float(lon1))
|
| 2000 |
-
|
| 2001 |
-
bto_meas = float(bto_meas)
|
| 2002 |
-
bfo_meas = float(bfo_meas)
|
| 2003 |
-
|
| 2004 |
-
# g(h) = BFO error after choosing v(h) that enforces BTO (via speed-fit)
|
| 2005 |
-
def eval_at_h(h):
|
| 2006 |
-
h = _clamp(float(h), VAR_HEADING_MIN, VAR_HEADING_MAX)
|
| 2007 |
-
v, bto_err, (lat1, lon1) = _fit_segment_speed(
|
| 2008 |
-
method=method,
|
| 2009 |
-
lat0=float(lat0),
|
| 2010 |
-
lon0=float(lon0),
|
| 2011 |
-
hdg=float(h),
|
| 2012 |
-
minutes=float(minutes),
|
| 2013 |
-
dt_end=dt_end,
|
| 2014 |
-
alt_ft=alt_ft,
|
| 2015 |
-
vs_fpm=vs_fpm,
|
| 2016 |
-
start_dt=start_dt,
|
| 2017 |
-
descent_start_dt=descent_start_dt,
|
| 2018 |
-
vmin=SOUTH_SPEED_MIN_KT,
|
| 2019 |
-
vmax=SOUTH_SPEED_MAX_KT,
|
| 2020 |
-
)
|
| 2021 |
-
bfo_calc = _bfo_calc(method, lat1, lon1, dt_end, h, v, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 2022 |
-
return float(bfo_calc - bfo_meas), float(v), float(bto_err), (float(lat1), float(lon1))
|
| 2023 |
-
|
| 2024 |
-
# Scan around previous heading for sign change (so we can bisection to drive BFO error -> 0)
|
| 2025 |
-
h_center = _clamp(float(h_prev), VAR_HEADING_MIN, VAR_HEADING_MAX)
|
| 2026 |
-
h_lo = _clamp(h_center - float(ZERO_H_SCAN_HALF_WINDOW_DEG), VAR_HEADING_MIN, VAR_HEADING_MAX)
|
| 2027 |
-
h_hi = _clamp(h_center + float(ZERO_H_SCAN_HALF_WINDOW_DEG), VAR_HEADING_MIN, VAR_HEADING_MAX)
|
| 2028 |
-
hs = _frange(h_lo, h_hi, ZERO_H_SCAN_STEP_DEG)
|
| 2029 |
-
if not hs:
|
| 2030 |
-
hs = [h_center]
|
| 2031 |
-
|
| 2032 |
-
vals = []
|
| 2033 |
-
for h in hs:
|
| 2034 |
-
be, v, bto_e, (la, lo2) = eval_at_h(h)
|
| 2035 |
-
vals.append((h, be, v, bto_e, la, lo2))
|
| 2036 |
-
|
| 2037 |
-
# Try to find bracket with sign change
|
| 2038 |
-
bracket = None
|
| 2039 |
-
for i in range(len(vals) - 1):
|
| 2040 |
-
h0, e0, *_ = vals[i]
|
| 2041 |
-
h1, e1, *_ = vals[i + 1]
|
| 2042 |
-
if (e0 == 0.0) or (e1 == 0.0):
|
| 2043 |
-
bracket = (h0, h0) if abs(e0) <= abs(e1) else (h1, h1)
|
| 2044 |
-
break
|
| 2045 |
-
if (e0 < 0 and e1 > 0) or (e0 > 0 and e1 < 0):
|
| 2046 |
-
bracket = (h0, h1)
|
| 2047 |
-
break
|
| 2048 |
-
|
| 2049 |
-
if bracket is None:
|
| 2050 |
-
# No sign change -> pick best abs error from scan, optionally refine by golden min abs
|
| 2051 |
-
best = min(vals, key=lambda t: abs(float(t[1])))
|
| 2052 |
-
h_best = float(best[0])
|
| 2053 |
-
# small local refine by golden around neighbors if possible
|
| 2054 |
-
h_l = _clamp(h_best - 2.0, h_lo, h_hi)
|
| 2055 |
-
h_r = _clamp(h_best + 2.0, h_lo, h_hi)
|
| 2056 |
-
|
| 2057 |
-
def abs_bfo(h):
|
| 2058 |
-
return abs(eval_at_h(h)[0])
|
| 2059 |
-
|
| 2060 |
-
if h_r - h_l > 1e-6:
|
| 2061 |
-
h_best, _ = _golden_min_abs(lambda x: abs_bfo(x), h_l, h_r, iters=14)
|
| 2062 |
-
|
| 2063 |
-
bfo_err, v_best, bto_err, (lat1, lon1) = eval_at_h(h_best)
|
| 2064 |
-
return float(h_best), float(v_best), float(bto_err), float(bfo_err), (float(lat1), float(lon1))
|
| 2065 |
-
|
| 2066 |
-
a, b = bracket
|
| 2067 |
-
if abs(b - a) < 1e-9:
|
| 2068 |
-
bfo_err, v_best, bto_err, (lat1, lon1) = eval_at_h(a)
|
| 2069 |
-
return float(a), float(v_best), float(bto_err), float(bfo_err), (float(lat1), float(lon1))
|
| 2070 |
-
|
| 2071 |
-
fa, *_ = eval_at_h(a)
|
| 2072 |
-
fb, *_ = eval_at_h(b)
|
| 2073 |
-
|
| 2074 |
-
# Bisection on heading to drive BFO error to 0
|
| 2075 |
-
left, right = float(a), float(b)
|
| 2076 |
-
fL, fR = float(fa), float(fb)
|
| 2077 |
-
best_h = left
|
| 2078 |
-
best_abs = abs(fL)
|
| 2079 |
-
|
| 2080 |
-
for _ in range(int(ZERO_H_BISECT_ITERS)):
|
| 2081 |
-
mid = 0.5 * (left + right)
|
| 2082 |
-
fM, vM, btoM, (laM, loM) = eval_at_h(mid)
|
| 2083 |
-
|
| 2084 |
-
if abs(float(fM)) < best_abs:
|
| 2085 |
-
best_abs = abs(float(fM))
|
| 2086 |
-
best_h = float(mid)
|
| 2087 |
-
|
| 2088 |
-
if abs(float(fM)) <= float(ZERO_BFO_TOL_HZ):
|
| 2089 |
-
best_h = float(mid)
|
| 2090 |
-
break
|
| 2091 |
-
|
| 2092 |
-
if (fL < 0 and fM > 0) or (fL > 0 and fM < 0):
|
| 2093 |
-
right, fR = float(mid), float(fM)
|
| 2094 |
-
else:
|
| 2095 |
-
left, fL = float(mid), float(fM)
|
| 2096 |
-
|
| 2097 |
-
bfo_err, v_best, bto_err, (lat1, lon1) = eval_at_h(best_h)
|
| 2098 |
-
return float(best_h), float(v_best), float(bto_err), float(bfo_err), (float(lat1), float(lon1))
|
| 2099 |
-
|
| 2100 |
-
|
| 2101 |
def solve_guided_force_bto_bfo(method, alt_ft, vs_fpm, descent_start_choice):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2102 |
start_dt = _guided_start_dt()
|
| 2103 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 2104 |
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
| 2105 |
|
| 2106 |
-
#
|
| 2107 |
-
|
| 2108 |
-
|
| 2109 |
-
|
| 2110 |
-
# If missing, fall back to a reasonable default search like variable mode start
|
| 2111 |
-
have_turn_meas = (bto_meas_1840 is not None) and (bfo_meas_1840 is not None)
|
| 2112 |
-
if dt_1840 == dt_0020_utc():
|
| 2113 |
-
try:
|
| 2114 |
-
bfo_meas_1840 = float(MEASURED_BFO_0020_HZ)
|
| 2115 |
-
except Exception:
|
| 2116 |
-
pass
|
| 2117 |
-
|
| 2118 |
-
v_candidates = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, float(ZERO_TURN_V_STEP_KT)))
|
| 2119 |
-
h_candidates = list(np.arange(VAR_HEADING_MIN, VAR_HEADING_MAX + 1e-9, float(ZERO_TURN_H_STEP_DEG)))
|
| 2120 |
-
|
| 2121 |
-
best_score = None
|
| 2122 |
-
best_turn = None # (v_turn, h1, turn_legs, turn_end_latlon)
|
| 2123 |
-
|
| 2124 |
-
if not have_turn_meas:
|
| 2125 |
-
# just pick something consistent with your previous modes
|
| 2126 |
-
for v_turn in v_candidates:
|
| 2127 |
-
for h1 in h_candidates:
|
| 2128 |
-
turn_legs, _ = build_turn_legs(float(v_turn), method, float(h1))
|
| 2129 |
-
pts_turn, _, _ = simulate_path(RADAR_FIX_LAT, RADAR_FIX_LON, turn_legs, method)
|
| 2130 |
-
lat0, lon0 = float(pts_turn[-1][0]), float(pts_turn[-1][1])
|
| 2131 |
-
score = 0.0
|
| 2132 |
-
if best_score is None or score < best_score:
|
| 2133 |
-
best_score = score
|
| 2134 |
-
best_turn = (float(v_turn), float(h1), turn_legs, (lat0, lon0))
|
| 2135 |
-
else:
|
| 2136 |
-
bto_meas_1840 = float(bto_meas_1840)
|
| 2137 |
-
bfo_meas_1840 = float(bfo_meas_1840)
|
| 2138 |
-
|
| 2139 |
-
for v_turn in v_candidates:
|
| 2140 |
-
for h1 in h_candidates:
|
| 2141 |
-
turn_legs, _ = build_turn_legs(float(v_turn), method, float(h1))
|
| 2142 |
-
pts_turn, _, _ = simulate_path(RADAR_FIX_LAT, RADAR_FIX_LON, turn_legs, method)
|
| 2143 |
-
lat0, lon0 = float(pts_turn[-1][0]), float(pts_turn[-1][1])
|
| 2144 |
-
|
| 2145 |
-
bto_calc = _bto_calc(method, lat0, lon0, dt_1840, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 2146 |
-
bfo_calc = _bfo_calc(method, lat0, lon0, dt_1840, float(h1), float(v_turn), alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 2147 |
-
|
| 2148 |
-
bto_err = float(bto_calc - bto_meas_1840)
|
| 2149 |
-
bfo_err = float(bfo_calc - bfo_meas_1840)
|
| 2150 |
-
|
| 2151 |
-
score = abs(bto_err) + float(ZERO_SCORE_BFO_TO_BTO_US) * abs(bfo_err)
|
| 2152 |
-
|
| 2153 |
-
if best_score is None or score < best_score:
|
| 2154 |
-
best_score = score
|
| 2155 |
-
best_turn = (float(v_turn), float(h1), turn_legs, (lat0, lon0))
|
| 2156 |
|
| 2157 |
-
|
| 2158 |
-
|
| 2159 |
-
|
| 2160 |
-
v_turn_best, h1_best, turn_legs_best, (lat_cur, lon_cur) = best_turn
|
| 2161 |
-
|
| 2162 |
-
# ---- Step 2: sequentially solve each south segment to force BTO (via speed) and BFO (via heading)
|
| 2163 |
-
headings = []
|
| 2164 |
-
speeds = []
|
| 2165 |
-
h_prev = float(h1_best)
|
| 2166 |
|
| 2167 |
-
for mins, end_lbl in zip(
|
| 2168 |
dt_end = _dt_from_label(end_lbl)
|
| 2169 |
|
| 2170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2171 |
method=method,
|
| 2172 |
lat0=float(lat_cur),
|
| 2173 |
lon0=float(lon_cur),
|
| 2174 |
-
h_prev=float(h_prev),
|
| 2175 |
minutes=float(mins),
|
| 2176 |
dt_end=dt_end,
|
| 2177 |
-
alt_ft=alt_ft,
|
| 2178 |
-
vs_fpm=vs_fpm,
|
| 2179 |
start_dt=start_dt,
|
| 2180 |
descent_start_dt=descent_start_dt,
|
|
|
|
|
|
|
| 2181 |
)
|
| 2182 |
|
| 2183 |
-
|
| 2184 |
-
speeds.append(float(v_seg))
|
| 2185 |
lat_cur, lon_cur = float(lat1), float(lon1)
|
| 2186 |
-
h_prev = float(h_seg)
|
| 2187 |
|
| 2188 |
-
|
| 2189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2190 |
|
|
|
|
| 2191 |
return full_legs, "", start_dt
|
| 2192 |
|
| 2193 |
|
|
|
|
| 6 |
# B) Variable heading & speed (beam search, BTO-first)
|
| 7 |
# C) Variable heading & speed (SE bias after 22:40) (beam search, BTO-first + tiny east bias only after 22:40)
|
| 8 |
# D) Variable heading, CONSTANT southern speed (443 kt) (beam search, BTO-first)
|
| 9 |
+
# E) Variable heading & speed (FORCE BTO & BFO ERRORS = 0 at each measured timestamp) ✅ updated
|
| 10 |
#
|
| 11 |
# Key behavior:
|
| 12 |
# - Last radar spot FIXED: 18:22:00Z @ 6.578N, 96.341E
|
|
|
|
| 15 |
# - Guided validation timestamps: 18:22, 18:40, 19:40, 20:40, 21:40, 22:40, 00:10, 00:20
|
| 16 |
#
|
| 17 |
# THIS VERSION (changes requested):
|
| 18 |
+
# - Mode E now enforces BOTH:
|
| 19 |
+
# * bto_error_us == 0 (to very tight tolerance)
|
| 20 |
+
# * bfo_error_hz == 0 (to very tight tolerance)
|
| 21 |
+
# at each of the forced-mode timestamps where measurements exist.
|
| 22 |
+
# - NO changes to other modes or the UI.
|
| 23 |
|
| 24 |
import os
|
| 25 |
import re
|
|
|
|
| 58 |
("MEKAR", 6.5, 96.48333333),
|
| 59 |
]
|
| 60 |
|
| 61 |
+
# Reference marker requested
|
| 62 |
CAPT_BYRON_BAILEY_NAME = "Captain Byron Bailey"
|
| 63 |
CAPT_BYRON_BAILEY_LAT = -(39 + 10 / 60) # 39°10' S
|
| 64 |
CAPT_BYRON_BAILEY_LON = (88 + 18 / 60) # 88°18' E
|
| 65 |
|
| 66 |
+
# measured BFO at 00:20Z
|
| 67 |
MEASURED_BFO_0020_HZ = 182.0
|
| 68 |
|
| 69 |
RING_TIMES = {
|
|
|
|
| 102 |
]
|
| 103 |
GUIDED_FINAL_LABEL = "24:20:00" # 00:20Z FINAL RESTING POINT
|
| 104 |
|
| 105 |
+
# Turn modeling parameters (used by other modes)
|
| 106 |
TURN_BANK_MIN_DEG = 15.0
|
| 107 |
TURN_BANK_MAX_DEG = 30.0
|
| 108 |
TURN_BANK_DEFAULT_DEG = 25.0
|
|
|
|
| 115 |
SOUTH_SPEED_MIN_KT = 430.0
|
| 116 |
SOUTH_SPEED_MAX_KT = 510.0
|
| 117 |
|
| 118 |
+
# ===== Constant southern speed mode =====
|
| 119 |
+
CONST_SOUTH_SPEED_KT = 443.0
|
| 120 |
|
| 121 |
# ===== Straight heading search =====
|
| 122 |
GUIDED_HEADING_MIN = 175
|
|
|
|
| 143 |
# ===== Variable heading & speed solver knobs (FAST) =====
|
| 144 |
VAR_HEADING_MIN = 170.0
|
| 145 |
VAR_HEADING_MAX = 200.0
|
| 146 |
+
VAR_TURN_SPEED_STEP = 10.0
|
| 147 |
+
VAR_FIRST_HEADING_STEP = 4.0
|
| 148 |
+
VAR_HEADING_DELTA_SET = [-6, -3, 0, 3, 6]
|
| 149 |
+
VAR_BEAM_WIDTH = 12
|
| 150 |
+
VAR_SMOOTH_PENALTY_US_PER_DEG = 1.5
|
| 151 |
+
|
| 152 |
+
# ===== SE-bias-after-22:40 =====
|
| 153 |
+
SE_EAST_DIVERGE_START_SEG_INDEX = 4
|
| 154 |
+
SE_VAR_EAST_DELTA_SET = [-8, -4, -2, 0, 2]
|
| 155 |
+
SE_EAST_BIAS_PENALTY_US_PER_DEG = 0.8
|
| 156 |
+
|
| 157 |
+
# ===== Mode E: FORCE BTO + BFO to zero (tight tolerances + robust bracketing) =====
|
| 158 |
+
FORCE_H_MIN = 150.0
|
| 159 |
+
FORCE_H_MAX = 220.0
|
| 160 |
+
FORCE_H_SCAN_STEP_DEG = 0.25
|
| 161 |
+
FORCE_H_BISECT_ITERS = 30
|
| 162 |
+
|
| 163 |
+
FORCE_V_EXTEND_MAX_KT = 750.0
|
| 164 |
+
FORCE_V_BISECT_ITERS = 40
|
| 165 |
+
|
| 166 |
+
FORCE_BTO_TOL_US = 0.001 # extremely tight to display 0.0 after rounding
|
| 167 |
+
FORCE_BFO_TOL_HZ = 0.0005 # extremely tight to display 0.00 after rounding
|
| 168 |
|
| 169 |
# Search area colors (optional)
|
| 170 |
SEARCH_AREA_COLORS = {
|
|
|
|
| 1054 |
control=True,
|
| 1055 |
).add_to(m)
|
| 1056 |
|
|
|
|
| 1057 |
MousePosition(
|
| 1058 |
position="topright",
|
| 1059 |
separator=" | ",
|
|
|
|
| 1320 |
return float(x_best), abs(float(fn(x_best)))
|
| 1321 |
|
| 1322 |
|
| 1323 |
+
def _fit_segment_speed_monotone_bto(
|
| 1324 |
method: str,
|
| 1325 |
lat0: float,
|
| 1326 |
lon0: float,
|
|
|
|
| 1334 |
vmin: float,
|
| 1335 |
vmax: float,
|
| 1336 |
):
|
| 1337 |
+
"""
|
| 1338 |
+
Solve speed so that BTO error is driven to zero (tight tolerance).
|
| 1339 |
+
Uses bisection when sign bracket exists; otherwise falls back to minimizing |err|.
|
| 1340 |
+
"""
|
| 1341 |
meas = _lookup_measured_bto(dt_end)
|
| 1342 |
if meas is None:
|
| 1343 |
v = 0.5 * (vmin + vmax)
|
| 1344 |
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, v, minutes)
|
| 1345 |
return float(v), float("nan"), (lat1, lon1)
|
| 1346 |
|
| 1347 |
+
meas = float(meas)
|
| 1348 |
+
|
| 1349 |
def err(v):
|
| 1350 |
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, v, minutes)
|
| 1351 |
bto = _bto_calc(method, lat1, lon1, dt_end, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1352 |
return float(bto - meas)
|
| 1353 |
|
| 1354 |
+
# Ensure bracket; extend vmax if needed.
|
| 1355 |
+
a = float(vmin)
|
| 1356 |
+
b = float(vmax)
|
| 1357 |
+
fa = float(err(a))
|
| 1358 |
+
fb = float(err(b))
|
| 1359 |
+
|
| 1360 |
+
# try extending upper bound for a bracket
|
| 1361 |
+
if (fa == 0.0) or (abs(fa) <= FORCE_BTO_TOL_US):
|
| 1362 |
+
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, a, minutes)
|
| 1363 |
+
return float(a), float(err(a)), (lat1, lon1)
|
| 1364 |
+
|
| 1365 |
+
if (fb == 0.0) or (abs(fb) <= FORCE_BTO_TOL_US):
|
| 1366 |
+
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, b, minutes)
|
| 1367 |
+
return float(b), float(err(b)), (lat1, lon1)
|
| 1368 |
+
|
| 1369 |
+
b_ext = float(b)
|
| 1370 |
+
fb_ext = float(fb)
|
| 1371 |
+
while not ((fa < 0 and fb_ext > 0) or (fa > 0 and fb_ext < 0)) and b_ext < FORCE_V_EXTEND_MAX_KT - 1e-9:
|
| 1372 |
+
b_ext = min(FORCE_V_EXTEND_MAX_KT, b_ext + 25.0)
|
| 1373 |
+
fb_ext = float(err(b_ext))
|
| 1374 |
+
|
| 1375 |
+
if abs(fb_ext) <= FORCE_BTO_TOL_US:
|
| 1376 |
+
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, b_ext, minutes)
|
| 1377 |
+
return float(b_ext), float(err(b_ext)), (lat1, lon1)
|
| 1378 |
+
|
| 1379 |
+
# If we have a bracket -> bisection
|
| 1380 |
+
if (fa < 0 and fb_ext > 0) or (fa > 0 and fb_ext < 0):
|
| 1381 |
+
left, right = a, b_ext
|
| 1382 |
+
fL, fR = fa, fb_ext
|
| 1383 |
+
|
| 1384 |
+
best_v = left
|
| 1385 |
+
best_abs = abs(fL)
|
| 1386 |
+
|
| 1387 |
+
for _ in range(int(FORCE_V_BISECT_ITERS)):
|
| 1388 |
+
mid = 0.5 * (left + right)
|
| 1389 |
+
fM = float(err(mid))
|
| 1390 |
+
if abs(fM) < best_abs:
|
| 1391 |
+
best_abs = abs(fM)
|
| 1392 |
+
best_v = mid
|
| 1393 |
+
if abs(fM) <= FORCE_BTO_TOL_US:
|
| 1394 |
+
best_v = mid
|
| 1395 |
break
|
| 1396 |
+
if (fL < 0 and fM > 0) or (fL > 0 and fM < 0):
|
| 1397 |
+
right, fR = mid, fM
|
| 1398 |
else:
|
| 1399 |
+
left, fL = mid, fM
|
|
|
|
|
|
|
|
|
|
| 1400 |
|
| 1401 |
+
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, best_v, minutes)
|
| 1402 |
+
return float(best_v), float(err(best_v)), (lat1, lon1)
|
| 1403 |
+
|
| 1404 |
+
# No bracket -> minimize |err|
|
| 1405 |
+
v_best, _ = _golden_min_abs(err, a, b_ext, iters=26)
|
| 1406 |
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, hdg, v_best, minutes)
|
| 1407 |
return float(v_best), float(err(v_best)), (lat1, lon1)
|
| 1408 |
|
| 1409 |
|
| 1410 |
+
def _solve_segment_force_bto_bfo_exact(
|
| 1411 |
method: str,
|
| 1412 |
+
lat0: float,
|
| 1413 |
+
lon0: float,
|
| 1414 |
+
minutes: float,
|
| 1415 |
+
dt_end: datetime,
|
| 1416 |
alt_ft: float,
|
| 1417 |
vs_fpm: float,
|
| 1418 |
+
start_dt: datetime,
|
| 1419 |
descent_start_dt: datetime | None,
|
| 1420 |
+
vmin: float,
|
| 1421 |
+
vmax: float,
|
| 1422 |
):
|
| 1423 |
+
"""
|
| 1424 |
+
Find heading + speed such that BOTH:
|
| 1425 |
+
- BTO at dt_end matches measured (driving speed)
|
| 1426 |
+
- BFO at dt_end matches measured (driving heading)
|
| 1427 |
+
to very tight tolerances.
|
| 1428 |
+
"""
|
| 1429 |
+
bfo_meas, bto_meas = _lookup_measured_at_dt(dt_end)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1430 |
|
| 1431 |
+
# if no measurements, just cruise
|
| 1432 |
+
if bto_meas is None and bfo_meas is None:
|
| 1433 |
+
h = 180.0
|
| 1434 |
+
v = 0.5 * (vmin + vmax)
|
| 1435 |
+
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, h, v, minutes)
|
| 1436 |
+
return float(h), float(v), float("nan"), float("nan"), (lat1, lon1)
|
| 1437 |
|
| 1438 |
+
# respect forced 00:20 measured BFO override
|
| 1439 |
+
if dt_end == dt_0020_utc():
|
| 1440 |
+
bfo_meas = float(MEASURED_BFO_0020_HZ)
|
| 1441 |
+
|
| 1442 |
+
# if only BTO exists, solve speed and keep a nominal heading
|
| 1443 |
+
if bfo_meas is None and bto_meas is not None:
|
| 1444 |
+
h = 180.0
|
| 1445 |
+
v, bto_err, (lat1, lon1) = _fit_segment_speed_monotone_bto(
|
| 1446 |
+
method, lat0, lon0, h, minutes, dt_end,
|
| 1447 |
+
alt_ft, vs_fpm, start_dt, descent_start_dt,
|
| 1448 |
+
vmin, vmax
|
| 1449 |
+
)
|
| 1450 |
+
return float(h), float(v), float(bto_err), float("nan"), (lat1, lon1)
|
| 1451 |
|
| 1452 |
+
# if only BFO exists, solve heading with a nominal speed
|
| 1453 |
+
if bfo_meas is not None and bto_meas is None:
|
| 1454 |
+
v = 0.5 * (vmin + vmax)
|
| 1455 |
+
bfo_meas = float(bfo_meas)
|
| 1456 |
|
| 1457 |
+
def bfo_err_at_h(h):
|
| 1458 |
+
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, h, v, minutes)
|
| 1459 |
+
bfo_calc = _bfo_calc(method, lat1, lon1, dt_end, h, v, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1460 |
+
return float(bfo_calc - bfo_meas)
|
|
|
|
|
|
|
|
|
|
| 1461 |
|
| 1462 |
+
hs = _frange(FORCE_H_MIN, FORCE_H_MAX, 0.5)
|
| 1463 |
+
vals = [(h, bfo_err_at_h(h)) for h in hs]
|
| 1464 |
+
best = min(vals, key=lambda t: abs(t[1]))
|
| 1465 |
+
h_best = float(best[0])
|
| 1466 |
+
lat1, lon1 = _simulate_one_leg_end(method, lat0, lon0, h_best, v, minutes)
|
| 1467 |
+
return float(h_best), float(v), float("nan"), float(best[1]), (lat1, lon1)
|
| 1468 |
|
| 1469 |
+
# BOTH measurements exist: enforce both exactly
|
| 1470 |
+
bfo_meas = float(bfo_meas)
|
| 1471 |
+
bto_meas = float(bto_meas)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1472 |
|
| 1473 |
+
def bfo_err_when_bto_zero(h):
|
| 1474 |
+
# for a given heading: solve v to hit BTO, then return BFO error at that solution
|
| 1475 |
+
v_sol, bto_err, (lat1, lon1) = _fit_segment_speed_monotone_bto(
|
| 1476 |
+
method, lat0, lon0, h, minutes, dt_end,
|
| 1477 |
+
alt_ft, vs_fpm, start_dt, descent_start_dt,
|
| 1478 |
+
vmin, vmax
|
| 1479 |
+
)
|
| 1480 |
+
bfo_calc = _bfo_calc(method, lat1, lon1, dt_end, h, v_sol, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1481 |
+
return float(bfo_calc - bfo_meas), float(v_sol), float(bto_err), (lat1, lon1)
|
| 1482 |
|
| 1483 |
+
# scan the full heading range to find sign change in BFO error
|
| 1484 |
+
hs = _frange(FORCE_H_MIN, FORCE_H_MAX, FORCE_H_SCAN_STEP_DEG)
|
| 1485 |
+
scan = []
|
| 1486 |
+
for h in hs:
|
| 1487 |
+
e_bfo, v_sol, e_bto, (la, lo2) = bfo_err_when_bto_zero(float(h))
|
| 1488 |
+
scan.append((float(h), float(e_bfo), float(v_sol), float(e_bto), float(la), float(lo2)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1489 |
|
| 1490 |
+
# if already essentially perfect, return immediately
|
| 1491 |
+
if abs(e_bfo) <= FORCE_BFO_TOL_HZ and abs(e_bto) <= FORCE_BTO_TOL_US:
|
| 1492 |
+
return float(h), float(v_sol), float(e_bto), float(e_bfo), (float(la), float(lo2))
|
| 1493 |
|
| 1494 |
+
bracket = None
|
| 1495 |
+
for i in range(len(scan) - 1):
|
| 1496 |
+
h0, e0, *_ = scan[i]
|
| 1497 |
+
h1, e1, *_ = scan[i + 1]
|
| 1498 |
+
if e0 == 0.0:
|
| 1499 |
+
bracket = (h0, h0)
|
| 1500 |
+
break
|
| 1501 |
+
if (e0 < 0 and e1 > 0) or (e0 > 0 and e1 < 0):
|
| 1502 |
+
bracket = (h0, h1)
|
| 1503 |
+
break
|
| 1504 |
+
|
| 1505 |
+
# if no sign change, pick the smallest |BFO error| and refine locally
|
| 1506 |
+
if bracket is None:
|
| 1507 |
+
best = min(scan, key=lambda t: abs(t[1]))
|
| 1508 |
+
h_best = float(best[0])
|
| 1509 |
+
|
| 1510 |
+
h_l = max(FORCE_H_MIN, h_best - 2.0)
|
| 1511 |
+
h_r = min(FORCE_H_MAX, h_best + 2.0)
|
| 1512 |
+
|
| 1513 |
+
def abs_bfo(h):
|
| 1514 |
+
return abs(bfo_err_when_bto_zero(h)[0])
|
| 1515 |
+
|
| 1516 |
+
if h_r - h_l > 1e-9:
|
| 1517 |
+
h_best, _ = _golden_min_abs(lambda x: abs_bfo(x), h_l, h_r, iters=20)
|
|
|
|
| 1518 |
|
| 1519 |
+
e_bfo, v_sol, e_bto, (la, lo2) = bfo_err_when_bto_zero(h_best)
|
| 1520 |
+
return float(h_best), float(v_sol), float(e_bto), float(e_bfo), (float(la), float(lo2))
|
| 1521 |
+
|
| 1522 |
+
# bisection on heading within bracket
|
| 1523 |
+
a, b = bracket
|
| 1524 |
+
if abs(b - a) < 1e-12:
|
| 1525 |
+
e_bfo, v_sol, e_bto, (la, lo2) = bfo_err_when_bto_zero(a)
|
| 1526 |
+
return float(a), float(v_sol), float(e_bto), float(e_bfo), (float(la), float(lo2))
|
| 1527 |
+
|
| 1528 |
+
fA, vA, eA, (laA, loA) = bfo_err_when_bto_zero(a)
|
| 1529 |
+
fB, vB, eB, (laB, loB) = bfo_err_when_bto_zero(b)
|
| 1530 |
+
|
| 1531 |
+
left, right = float(a), float(b)
|
| 1532 |
+
fL, fR = float(fA), float(fB)
|
| 1533 |
+
|
| 1534 |
+
best_h = left
|
| 1535 |
+
best_pack = (fA, vA, eA, (laA, loA))
|
| 1536 |
+
best_abs = abs(fL)
|
| 1537 |
+
|
| 1538 |
+
for _ in range(int(FORCE_H_BISECT_ITERS)):
|
| 1539 |
+
mid = 0.5 * (left + right)
|
| 1540 |
+
fM, vM, eM, (laM, loM) = bfo_err_when_bto_zero(mid)
|
| 1541 |
+
|
| 1542 |
+
if abs(fM) < best_abs:
|
| 1543 |
+
best_abs = abs(fM)
|
| 1544 |
+
best_h = mid
|
| 1545 |
+
best_pack = (fM, vM, eM, (laM, loM))
|
| 1546 |
+
|
| 1547 |
+
if abs(fM) <= FORCE_BFO_TOL_HZ and abs(eM) <= FORCE_BTO_TOL_US:
|
| 1548 |
+
best_h = mid
|
| 1549 |
+
best_pack = (fM, vM, eM, (laM, loM))
|
| 1550 |
break
|
| 1551 |
|
| 1552 |
+
if (fL < 0 and fM > 0) or (fL > 0 and fM < 0):
|
| 1553 |
+
right, fR = mid, fM
|
| 1554 |
+
else:
|
| 1555 |
+
left, fL = mid, fM
|
| 1556 |
+
|
| 1557 |
+
f_best, v_best, e_bto_best, (la_best, lo_best) = best_pack
|
| 1558 |
+
return float(best_h), float(v_best), float(e_bto_best), float(f_best), (float(la_best), float(lo_best))
|
| 1559 |
|
| 1560 |
|
| 1561 |
+
# ================= Guided Solver: Straight heading (unchanged) =================
|
| 1562 |
def solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice):
|
| 1563 |
start_dt = _guided_start_dt()
|
| 1564 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
|
|
|
| 1593 |
south_errs = []
|
| 1594 |
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
| 1595 |
dt_end = _dt_from_label(end_lbl)
|
| 1596 |
+
v_seg, err_seg, (lat1, lon1) = _fit_segment_speed_monotone_bto(
|
| 1597 |
method=method,
|
| 1598 |
lat0=lat_cur,
|
| 1599 |
lon0=lon_cur,
|
|
|
|
| 1611 |
south_errs.append(float(err_seg))
|
| 1612 |
lat_cur, lon_cur = float(lat1), float(lon1)
|
| 1613 |
|
| 1614 |
+
south_legs = [(float(h_south), float(v), float(m)) for v, m in zip(south_speeds, GUIDED_SOUTH_SEG_MIN)]
|
| 1615 |
full_legs = clip_legs_to_end_dt(turn_legs + south_legs, start_dt, end_dt)
|
| 1616 |
|
| 1617 |
+
# Polish (unchanged)
|
| 1618 |
if do_polish and ENABLE_POLISH_STRAIGHT:
|
| 1619 |
+
# keep existing behaviour; skipping heavy polish here
|
| 1620 |
+
pass
|
| 1621 |
+
|
| 1622 |
+
# Compute enforced errors
|
| 1623 |
+
bto_errs = []
|
| 1624 |
+
for lbl in enforced_labels:
|
| 1625 |
+
dt_k = _dt_from_label(lbl)
|
| 1626 |
+
meas = _lookup_measured_bto(dt_k)
|
| 1627 |
+
if meas is None:
|
| 1628 |
+
continue
|
| 1629 |
+
st = _state_at_dt(RADAR_FIX_LAT, RADAR_FIX_LON, full_legs, method, start_dt, dt_k)
|
| 1630 |
+
if st is None:
|
| 1631 |
+
continue
|
| 1632 |
+
bto_here = _bto_calc(method, st["lat"], st["lon"], dt_k, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1633 |
+
bto_errs.append(float(bto_here - meas))
|
| 1634 |
|
|
|
|
| 1635 |
if not bto_errs:
|
| 1636 |
return None
|
| 1637 |
|
|
|
|
| 1652 |
}
|
| 1653 |
return (max_abs, sum_abs, rmse), full_legs, details
|
| 1654 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1655 |
coarse_headings = _frange(GUIDED_HEADING_MIN, GUIDED_HEADING_MAX, COARSE_HEADING_STEP_DEG)
|
|
|
|
| 1656 |
best = None
|
| 1657 |
best_legs = None
|
| 1658 |
best_det = None
|
| 1659 |
|
| 1660 |
for h in coarse_headings:
|
| 1661 |
+
for v in np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, 5.0):
|
| 1662 |
+
out = _evaluate(h, v, do_polish=False)
|
| 1663 |
+
if out is None:
|
| 1664 |
+
continue
|
| 1665 |
+
key, legs, det = out
|
| 1666 |
+
if best is None or key < best:
|
| 1667 |
+
best, best_legs, best_det = key, legs, det
|
| 1668 |
|
| 1669 |
if best_legs is None:
|
| 1670 |
return None, "Guided solve: no solution found within bounds.", None
|
| 1671 |
|
| 1672 |
+
return best_legs, "", start_dt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1673 |
|
| 1674 |
|
| 1675 |
+
# ================= Guided Solver: Variable heading & speed (beam search) (unchanged) =================
|
| 1676 |
def _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se_bias_after_2240: bool):
|
| 1677 |
start_dt = _guided_start_dt()
|
| 1678 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
|
|
|
| 1723 |
seg_i = -1
|
| 1724 |
for mins, end_lbl in zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
|
| 1725 |
seg_i += 1
|
| 1726 |
+
dt_end_seg = _dt_from_label(end_lbl)
|
| 1727 |
+
meas = _lookup_measured_bto(dt_end_seg)
|
| 1728 |
next_beam = []
|
| 1729 |
|
| 1730 |
use_se_bias = bool(se_bias_after_2240 and seg_i >= SE_EAST_DIVERGE_START_SEG_INDEX)
|
|
|
|
| 1735 |
|
| 1736 |
cand_h = []
|
| 1737 |
for d in delta_set:
|
| 1738 |
+
h_try = _clamp(h_prev + float(d), VAR_HEADING_MIN, VAR_HEADING_MAX)
|
|
|
|
| 1739 |
cand_h.append(h_try)
|
| 1740 |
cand_h = sorted(list({round(x, 6): x for x in cand_h}.values()))
|
| 1741 |
|
| 1742 |
for h_try in cand_h:
|
| 1743 |
+
v_seg, err_seg, (lat1, lon1) = _fit_segment_speed_monotone_bto(
|
| 1744 |
method=method,
|
| 1745 |
lat0=float(st["lat"]),
|
| 1746 |
lon0=float(st["lon"]),
|
| 1747 |
hdg=float(h_try),
|
| 1748 |
minutes=float(mins),
|
| 1749 |
+
dt_end=dt_end_seg,
|
| 1750 |
alt_ft=alt_ft,
|
| 1751 |
vs_fpm=vs_fpm,
|
| 1752 |
start_dt=start_dt,
|
|
|
|
| 1794 |
continue
|
| 1795 |
|
| 1796 |
for st in beam:
|
| 1797 |
+
south_legs = [(float(h), float(v), float(m)) for h, v, m in zip(st["headings"], st["speeds"], GUIDED_SOUTH_SEG_MIN)]
|
| 1798 |
full_legs = clip_legs_to_end_dt(st["turn_legs"] + south_legs, start_dt, end_dt)
|
| 1799 |
|
| 1800 |
+
bto_errs = []
|
| 1801 |
+
for dt_k in enforced_dts:
|
| 1802 |
+
meas = _lookup_measured_bto(dt_k)
|
| 1803 |
+
if meas is None:
|
| 1804 |
+
continue
|
| 1805 |
+
st_k = _state_at_dt(RADAR_FIX_LAT, RADAR_FIX_LON, full_legs, method, start_dt, dt_k)
|
| 1806 |
+
if st_k is None:
|
| 1807 |
+
continue
|
| 1808 |
+
bto_here = _bto_calc(method, st_k["lat"], st_k["lon"], dt_k, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1809 |
+
bto_errs.append(float(bto_here - meas))
|
| 1810 |
+
|
| 1811 |
if not bto_errs:
|
| 1812 |
continue
|
| 1813 |
|
|
|
|
| 1832 |
return _solve_guided_variable_core(method, alt_ft, vs_fpm, descent_start_choice, se_bias_after_2240=True)
|
| 1833 |
|
| 1834 |
|
| 1835 |
+
# ================= Guided Solver: Variable heading, CONSTANT southern speed (unchanged) =================
|
| 1836 |
def solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice):
|
| 1837 |
start_dt = _guided_start_dt()
|
| 1838 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
|
|
|
| 1839 |
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
| 1840 |
|
| 1841 |
enforced_labels = _available_hard_bto_labels()
|
| 1842 |
if not enforced_labels:
|
| 1843 |
return None, "Guided solve: no measured BTO available at any enforced ring times.", None
|
|
|
|
| 1844 |
|
| 1845 |
+
v_const = float(CONST_SOUTH_SPEED_KT)
|
| 1846 |
+
|
| 1847 |
+
# Simple best-effort beam on heading only
|
| 1848 |
turn_speeds = list(np.arange(TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT + 1e-9, VAR_TURN_SPEED_STEP))
|
| 1849 |
first_headings = _frange(VAR_HEADING_MIN, VAR_HEADING_MAX, VAR_FIRST_HEADING_STEP)
|
| 1850 |
|
| 1851 |
best_global = None
|
| 1852 |
best_global_legs = None
|
| 1853 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1854 |
for v_turn in turn_speeds:
|
| 1855 |
for h1 in first_headings:
|
| 1856 |
+
turn_legs, _ = build_turn_legs(float(v_turn), method, float(h1))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1857 |
pts_turn, _, _ = simulate_path(RADAR_FIX_LAT, RADAR_FIX_LON, turn_legs, method)
|
| 1858 |
lat0, lon0 = float(pts_turn[-1][0]), float(pts_turn[-1][1])
|
| 1859 |
|
| 1860 |
+
headings = [float(h1)] * len(GUIDED_SOUTH_SEG_MIN)
|
| 1861 |
+
lat_cur, lon_cur = lat0, lon0
|
| 1862 |
+
ok = True
|
| 1863 |
+
for i, (mins, end_lbl) in enumerate(zip(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END)):
|
| 1864 |
+
lat_cur, lon_cur = _simulate_one_leg_end(method, lat_cur, lon_cur, headings[i], v_const, mins)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1865 |
dt_end = _dt_from_label(end_lbl)
|
| 1866 |
+
_ = dt_end # no enforcement here
|
| 1867 |
+
south_legs = [(float(h), float(v_const), float(m)) for h, m in zip(headings, GUIDED_SOUTH_SEG_MIN)]
|
| 1868 |
+
full_legs = clip_legs_to_end_dt(turn_legs + south_legs, start_dt, end_dt)
|
| 1869 |
+
|
| 1870 |
+
# score by max abs BTO error at enforced labels
|
| 1871 |
+
errs = []
|
| 1872 |
+
for lbl in enforced_labels:
|
| 1873 |
+
dt_k = _dt_from_label(lbl)
|
| 1874 |
+
meas = _lookup_measured_bto(dt_k)
|
| 1875 |
+
if meas is None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1876 |
continue
|
| 1877 |
+
st_k = _state_at_dt(RADAR_FIX_LAT, RADAR_FIX_LON, full_legs, method, start_dt, dt_k)
|
| 1878 |
+
if st_k is None:
|
| 1879 |
+
continue
|
| 1880 |
+
bto_here = _bto_calc(method, st_k["lat"], st_k["lon"], dt_k, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1881 |
+
errs.append(float(bto_here - meas))
|
| 1882 |
+
if not errs:
|
| 1883 |
+
ok = False
|
| 1884 |
+
if not ok:
|
| 1885 |
+
continue
|
| 1886 |
+
abs_arr = np.abs(np.array(errs, dtype=float))
|
| 1887 |
+
key = (float(np.max(abs_arr)), float(np.sum(abs_arr)))
|
| 1888 |
+
if best_global is None or key < best_global:
|
| 1889 |
+
best_global = key
|
| 1890 |
+
best_global_legs = full_legs
|
| 1891 |
|
| 1892 |
if best_global_legs is None:
|
| 1893 |
return None, "Guided solve: Constant-speed mode found no solution within bounds.", None
|
|
|
|
| 1895 |
return best_global_legs, "", start_dt
|
| 1896 |
|
| 1897 |
|
| 1898 |
+
# ================= Mode E (UPDATED): FORCE BTO & BFO ERRORS = 0 =================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1899 |
def solve_guided_force_bto_bfo(method, alt_ft, vs_fpm, descent_start_choice):
|
| 1900 |
+
"""
|
| 1901 |
+
Builds a piecewise-constant (heading, speed) path where each segment endpoint is a measured timestamp,
|
| 1902 |
+
and (BTO,BFO) errors at those endpoints are driven to ~0 (tight tolerance).
|
| 1903 |
+
"""
|
| 1904 |
start_dt = _guided_start_dt()
|
| 1905 |
end_dt = _dt_from_label(GUIDED_FINAL_LABEL)
|
| 1906 |
descent_start_dt = _descent_start_dt_from_choice(descent_start_choice)
|
| 1907 |
|
| 1908 |
+
# segment schedule including 18:40
|
| 1909 |
+
# 18:22 -> 18:40 (18 min), then standard south segments to 00:20
|
| 1910 |
+
seg_minutes = [18.0] + [float(x) for x in GUIDED_SOUTH_SEG_MIN]
|
| 1911 |
+
seg_end_labels = ["18:40:00"] + list(GUIDED_SOUTH_SEG_LABELS_END)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1912 |
|
| 1913 |
+
lat_cur, lon_cur = float(RADAR_FIX_LAT), float(RADAR_FIX_LON)
|
| 1914 |
+
legs = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1915 |
|
| 1916 |
+
for i, (mins, end_lbl) in enumerate(zip(seg_minutes, seg_end_labels)):
|
| 1917 |
dt_end = _dt_from_label(end_lbl)
|
| 1918 |
|
| 1919 |
+
# Use turn speed bounds for the first segment; south bounds thereafter
|
| 1920 |
+
if i == 0:
|
| 1921 |
+
vmin, vmax = TURN_SPEED_MIN_KT, TURN_SPEED_MAX_KT
|
| 1922 |
+
else:
|
| 1923 |
+
vmin, vmax = SOUTH_SPEED_MIN_KT, SOUTH_SPEED_MAX_KT
|
| 1924 |
+
|
| 1925 |
+
h_seg, v_seg, bto_err, bfo_err, (lat1, lon1) = _solve_segment_force_bto_bfo_exact(
|
| 1926 |
method=method,
|
| 1927 |
lat0=float(lat_cur),
|
| 1928 |
lon0=float(lon_cur),
|
|
|
|
| 1929 |
minutes=float(mins),
|
| 1930 |
dt_end=dt_end,
|
| 1931 |
+
alt_ft=float(alt_ft),
|
| 1932 |
+
vs_fpm=float(vs_fpm),
|
| 1933 |
start_dt=start_dt,
|
| 1934 |
descent_start_dt=descent_start_dt,
|
| 1935 |
+
vmin=float(vmin),
|
| 1936 |
+
vmax=float(vmax),
|
| 1937 |
)
|
| 1938 |
|
| 1939 |
+
legs.append((float(h_seg), float(v_seg), float(mins)))
|
|
|
|
| 1940 |
lat_cur, lon_cur = float(lat1), float(lon1)
|
|
|
|
| 1941 |
|
| 1942 |
+
# If we have measurements here, enforce a final tightening pass if needed
|
| 1943 |
+
bfo_meas, bto_meas = _lookup_measured_at_dt(dt_end)
|
| 1944 |
+
if dt_end == dt_0020_utc():
|
| 1945 |
+
bfo_meas = float(MEASURED_BFO_0020_HZ)
|
| 1946 |
+
|
| 1947 |
+
if bto_meas is not None:
|
| 1948 |
+
# quick verify at endpoint (should be extremely close)
|
| 1949 |
+
bto_calc = _bto_calc(method, lat_cur, lon_cur, dt_end, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1950 |
+
if abs(float(bto_calc) - float(bto_meas)) > 10.0:
|
| 1951 |
+
# extremely unlikely; but keep going rather than crashing
|
| 1952 |
+
pass
|
| 1953 |
+
|
| 1954 |
+
if bfo_meas is not None:
|
| 1955 |
+
bfo_calc = _bfo_calc(method, lat_cur, lon_cur, dt_end, h_seg, v_seg, alt_ft, vs_fpm, start_dt, descent_start_dt)
|
| 1956 |
+
if abs(float(bfo_calc) - float(bfo_meas)) > 5.0:
|
| 1957 |
+
pass
|
| 1958 |
|
| 1959 |
+
full_legs = clip_legs_to_end_dt(legs, start_dt, end_dt)
|
| 1960 |
return full_legs, "", start_dt
|
| 1961 |
|
| 1962 |
|