Chris773 commited on
Commit
3e5a70f
·
verified ·
1 Parent(s): aed4251

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +3 -268
app.py CHANGED
@@ -6,7 +6,6 @@
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 ~= 0 where measured) (segment-wise: BTO forced via speed-fit, BFO forced via heading root)
10
  #
11
  # Key behavior:
12
  # - Last radar spot FIXED: 18:22:00Z @ 6.578N, 96.341E
@@ -16,7 +15,6 @@
16
  #
17
  # THIS VERSION (changes requested):
18
  # - Mode D constant southern speed changed from 447 kt -> 443 kt
19
- # - NEW Mode E: variable heading & speed trying to force BOTH BTO and BFO errors to 0 (where measured)
20
  # - NEW Map feature: show lat/lon under mouse cursor (Leaflet MousePosition plugin)
21
 
22
  import os
@@ -152,16 +150,6 @@ SE_EAST_DIVERGE_START_SEG_INDEX = 4 # seg 4 starts after 22:40
152
  SE_VAR_EAST_DELTA_SET = [-8, -4, -2, 0, 2] # prefer small decreases (eastward) after 22:40
153
  SE_EAST_BIAS_PENALTY_US_PER_DEG = 0.8 # small; BTO still dominates
154
 
155
- # ===== NEW: "force BTO+BFO to 0" knobs =====
156
- ZERO_H_SCAN_HALF_WINDOW_DEG = 14.0 # scan around previous heading for sign change
157
- ZERO_H_SCAN_STEP_DEG = 0.5
158
- ZERO_H_BISECT_ITERS = 22
159
- ZERO_BTO_TOL_US = 0.2 # practical tolerance; speed-fit tries to hit near 0 already
160
- ZERO_BFO_TOL_HZ = 0.02 # practical tolerance
161
- ZERO_TURN_H_STEP_DEG = 2.0 # coarse grid for turn-end fit
162
- ZERO_TURN_V_STEP_KT = 10.0 # coarse grid for turn speed
163
- ZERO_SCORE_BFO_TO_BTO_US = 20.0 # convert 1 Hz ~ 20 us for scoring
164
-
165
  # Search area colors (optional)
166
  SEARCH_AREA_COLORS = {
167
  "Ocean Infinity": "#ff3b3b",
@@ -1284,19 +1272,6 @@ def _bto_calc(method: str, lat: float, lon: float, dt_ping: datetime, alt_ft: fl
1284
  return float(fp.calculateBTO_at_time(float(lat), float(lon), alt_m, dt_ping))
1285
 
1286
 
1287
- def _bfo_calc(method: str, lat: float, lon: float, dt_ping: datetime, hdg: float, spd_kt: float,
1288
- alt_ft: float, vs_fpm: float, start_dt: datetime, descent_start_dt: datetime | None):
1289
- alt_now_ft = altitude_ft_at_dt(dt_ping, start_dt, float(alt_ft), float(vs_fpm), descent_start_dt)
1290
- alt_m = alt_now_ft * 0.3048
1291
-
1292
- vs_mps = float(vs_fpm) * 0.00508
1293
- if descent_start_dt is not None and dt_ping < descent_start_dt:
1294
- vs_mps = 0.0
1295
-
1296
- spd_mps = float(spd_kt) * 1852.0 / 3600.0
1297
- return float(fp.calculateBFO_at_time(float(spd_mps), float(hdg), float(lat), float(lon), float(alt_m), float(vs_mps), dt_ping))
1298
-
1299
-
1300
  def _golden_min_abs(fn, lo: float, hi: float, iters: int = 24):
1301
  grc = (math.sqrt(5.0) - 1.0) / 2.0
1302
  a, b = float(lo), float(hi)
@@ -1351,7 +1326,7 @@ def _fit_segment_speed(
1351
  for _ in range(26):
1352
  m = 0.5 * (a + b)
1353
  fm = float(err(m))
1354
- if abs(fm) < float(ZERO_BTO_TOL_US):
1355
  a = b = m
1356
  break
1357
  if (fa < 0 and fm > 0) or (fa > 0 and fm < 0):
@@ -1951,246 +1926,6 @@ def solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice):
1951
  return best_global_legs, "", start_dt
1952
 
1953
 
1954
- # ================= NEW Guided Solver: Force BTO + BFO =================
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
- # ---- Step 1: choose (v_turn, h1) to best match BOTH BTO & BFO at 18:40 end-of-turn (best-effort)
2107
- dt_1840 = TURN_COMPLETE_TIME_UTC
2108
- bfo_meas_1840, bto_meas_1840 = _lookup_measured_at_dt(dt_1840)
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
- if best_turn is None:
2158
- return None, "Guided solve: Force BTO+BFO could not initialize turn.", None
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(GUIDED_SOUTH_SEG_MIN, GUIDED_SOUTH_SEG_LABELS_END):
2168
- dt_end = _dt_from_label(end_lbl)
2169
-
2170
- h_seg, v_seg, bto_err, bfo_err, (lat1, lon1) = _fit_heading_speed_force_bto_bfo(
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
- headings.append(float(h_seg))
2184
- speeds.append(float(v_seg))
2185
- lat_cur, lon_cur = float(lat1), float(lon1)
2186
- h_prev = float(h_seg)
2187
-
2188
- south_legs = _build_south_legs_variable(headings, speeds, GUIDED_SOUTH_SEG_MIN)
2189
- full_legs = clip_legs_to_end_dt(turn_legs_best + south_legs, start_dt, end_dt)
2190
-
2191
- return full_legs, "", start_dt
2192
-
2193
-
2194
  # ================= RUNNERS =================
2195
  def _run_common(start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice):
2196
  if start_dt == RADAR_FIX_TIME_UTC:
@@ -2239,7 +1974,8 @@ def run_guided(guided_mode, start_lat, start_lon, method, alt_ft, vs_fpm, descen
2239
  elif guided_mode.startswith("Variable heading (constant"):
2240
  legs, _report_md, start_dt = solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice)
2241
  else:
2242
- legs, _report_md, start_dt = solve_guided_force_bto_bfo(method, alt_ft, vs_fpm, descent_start_choice)
 
2243
 
2244
  if legs is None:
2245
  fig = go.Figure()
@@ -2286,7 +2022,6 @@ with gr.Blocks() as demo:
2286
  "Variable heading & speed",
2287
  "Variable heading & speed (SE after 22:40)",
2288
  f"Variable heading (constant {CONST_SOUTH_SPEED_KT:.0f} kt)",
2289
- "Variable heading & speed (force BTO+BFO = 0)",
2290
  ],
2291
  value="Straight heading",
2292
  label="Guided scenario",
 
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
  #
10
  # Key behavior:
11
  # - Last radar spot FIXED: 18:22:00Z @ 6.578N, 96.341E
 
15
  #
16
  # THIS VERSION (changes requested):
17
  # - Mode D constant southern speed changed from 447 kt -> 443 kt
 
18
  # - NEW Map feature: show lat/lon under mouse cursor (Leaflet MousePosition plugin)
19
 
20
  import os
 
150
  SE_VAR_EAST_DELTA_SET = [-8, -4, -2, 0, 2] # prefer small decreases (eastward) after 22:40
151
  SE_EAST_BIAS_PENALTY_US_PER_DEG = 0.8 # small; BTO still dominates
152
 
 
 
 
 
 
 
 
 
 
 
153
  # Search area colors (optional)
154
  SEARCH_AREA_COLORS = {
155
  "Ocean Infinity": "#ff3b3b",
 
1272
  return float(fp.calculateBTO_at_time(float(lat), float(lon), alt_m, dt_ping))
1273
 
1274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1275
  def _golden_min_abs(fn, lo: float, hi: float, iters: int = 24):
1276
  grc = (math.sqrt(5.0) - 1.0) / 2.0
1277
  a, b = float(lo), float(hi)
 
1326
  for _ in range(26):
1327
  m = 0.5 * (a + b)
1328
  fm = float(err(m))
1329
+ if abs(fm) < 0.2:
1330
  a = b = m
1331
  break
1332
  if (fa < 0 and fm > 0) or (fa > 0 and fm < 0):
 
1926
  return best_global_legs, "", start_dt
1927
 
1928
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1929
  # ================= RUNNERS =================
1930
  def _run_common(start_lat, start_lon, legs, method, start_dt, alt_ft, vs_fpm, descent_start_choice):
1931
  if start_dt == RADAR_FIX_TIME_UTC:
 
1974
  elif guided_mode.startswith("Variable heading (constant"):
1975
  legs, _report_md, start_dt = solve_guided_constant_speed(method, alt_ft, vs_fpm, descent_start_choice)
1976
  else:
1977
+ # Safety fallback: if an unknown label appears, just run Straight heading.
1978
+ legs, _report_md, start_dt = solve_guided_straight(method, alt_ft, vs_fpm, descent_start_choice)
1979
 
1980
  if legs is None:
1981
  fig = go.Figure()
 
2022
  "Variable heading & speed",
2023
  "Variable heading & speed (SE after 22:40)",
2024
  f"Variable heading (constant {CONST_SOUTH_SPEED_KT:.0f} kt)",
 
2025
  ],
2026
  value="Straight heading",
2027
  label="Guided scenario",