Spaces:
Sleeping
Sleeping
Download flight_plotter_v32.py from WhereIsMH370/MH370Sim: direct link, hf CLI and curl.
- Browser
- Download file 27.4 kB
-
https://huggingface.co/spaces/WhereIsMH370/MH370Sim/resolve/main/flight_plotter_v32.py
- Command line
-
hf download hf://spaces/WhereIsMH370/MH370Sim/flight_plotter_v32.py
-
curl -L -o flight_plotter_v32.py https://huggingface.co/spaces/WhereIsMH370/MH370Sim/resolve/main/flight_plotter_v32.py
27.4 kB
| import math | |
| import numpy as np | |
| from datetime import datetime, timezone, timedelta | |
| speedOfLight_c = 299792458.0 | |
| uplinkFreq = 1646652500 | |
| downlinkFreq = 3615152500 | |
| btoBias = -495679 | |
| BFO_bias = 150 | |
| elip_a = 6378137.0 | |
| elip_f = 1.0 / 298.257223563 | |
| elip_b = elip_a * (1.0 - elip_f) | |
| elip_e2 = (2 * elip_f) - (elip_f * elip_f) | |
| def degToRad(d): | |
| return d * math.pi / 180.0 | |
| def radToDeg(r): | |
| return r * 180.0 / math.pi | |
| def feetToM(ft): | |
| return ft * 0.3048 | |
| satLoc = ( | |
| (("18:25:00"), (18136.7, 38071.8, 1148.5), (0.00188, -0.00117, 0.02690), 11, 142, 12520), | |
| (("18:40:00"), (18138.21, 38070.79, 1170.29), (0.001892134, -0.001142665, 0.02137832), 8, 88, None), | |
| (("19:40:00"), (18145.1, 38067.0, 1206.3), (0.00189, -0.00092, -0.00148), -1, 111, 11500), | |
| (("20:40:00"), (18152.1, 38064.0, 1159.7), (0.00200, -0.00077, -0.02422), -1, 141, 11740), | |
| (("21:40:00"), (18159.5, 38061.3, 1033.8), (0.00212, -0.00076, -0.04531), -18, 168, 12780), | |
| (("22:40:00"), (18167.2, 38058.3, 837.2), (0.00211, -0.00096, -0.06331), -29, 204, 14540), | |
| (("24:10:00"), (18177.5, 38051.7, 440.0), (0.00160, -0.00151, -0.08188), -38, 252, 18040), | |
| (("24:20:00"), (18178.4, 38050.8, 390.5), (0.00150, -0.00158, -0.08321), -38, 182, 18400) | |
| ) | |
| gsPerth = (-2368.8, 4881.1, -3342.0) | |
| nominalSatLoc = (0, 64.5, 36000000) | |
| def N_phi(lat_rad): | |
| sinp = math.sin(lat_rad) | |
| return elip_a / math.sqrt(1.0 - elip_e2 * sinp * sinp) | |
| def latLonToECEF(lat_deg, lon_deg, h_m): | |
| lat = degToRad(lat_deg) | |
| lon = degToRad(lon_deg) | |
| Np = N_phi(lat) | |
| x = (Np + h_m) * math.cos(lat) * math.cos(lon) | |
| y = (Np + h_m) * math.cos(lat) * math.sin(lon) | |
| z = (Np * (1 - elip_e2) + h_m) * math.sin(lat) | |
| return np.array([x, y, z], dtype=float) | |
| def distBetECEF(p1, p2): | |
| d = p1 - p2 | |
| return float(np.sqrt(np.dot(d, d))) | |
| def ENU2ECEFvelo(vE, vN, vU, lat_rad, lon_rad): | |
| sL, cL = (math.sin(lat_rad), math.cos(lat_rad)) | |
| sO, cO = (math.sin(lon_rad), math.cos(lon_rad)) | |
| R = np.array([[-sO, -sL * cO, cL * cO], [cO, -sL * sO, cL * sO], [0.0, cL, sL]], dtype=float) | |
| return R @ np.array([vE, vN, vU], dtype=float) | |
| def toECEFunitVector(p_from, p_to): | |
| v = p_to - p_from | |
| n = np.linalg.norm(v) | |
| return v / n if n != 0 else v | |
| def rhumb_direct(start_latlon, bearing_deg, distance_m): | |
| lat1 = degToRad(start_latlon[0]) | |
| lon1 = degToRad(start_latlon[1]) | |
| brg = degToRad(bearing_deg) | |
| R = elip_a | |
| d = distance_m / R | |
| dphi = d * math.cos(brg) | |
| lat2 = lat1 + dphi | |
| if abs(lat2) > math.pi / 2 - 1e-12: | |
| lat2 = math.copysign(math.pi / 2 - 1e-12, lat2) | |
| dpsi = math.log(math.tan(lat2 / 2 + math.pi / 4) / math.tan(lat1 / 2 + math.pi / 4)) | |
| q = dphi / dpsi if abs(dpsi) > 1e-12 else math.cos(lat1) | |
| dlon = d * math.sin(brg) / q | |
| lon2 = lon1 + dlon | |
| lon2 = (lon2 + math.pi) % (2 * math.pi) - math.pi | |
| return (radToDeg(lat2), radToDeg(lon2)) | |
| def vincenty_direct(latLon, alpha1_deg, s): | |
| lat1 = degToRad(latLon[0]) | |
| lon1 = degToRad(latLon[1]) | |
| alpha1 = degToRad(alpha1_deg) | |
| U1 = math.atan((1 - elip_f) * math.tan(lat1)) | |
| sigma1 = math.atan2(math.tan(U1), math.cos(alpha1)) | |
| sin_alpha = math.cos(U1) * math.sin(alpha1) | |
| cos2_alpha = 1 - sin_alpha ** 2 | |
| elip_b = elip_a * (1.0 - elip_f) | |
| u2 = cos2_alpha * (elip_a ** 2 - elip_b ** 2) / elip_b ** 2 | |
| A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2))) | |
| B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2))) | |
| sigma = s / (elip_b * A) | |
| tol = 1e-12 | |
| for _ in range(200): | |
| cos2sigma_m = math.cos(2 * sigma1 + sigma) | |
| sin_sigma = math.sin(sigma) | |
| cos_sigma = math.cos(sigma) | |
| delta_sigma = B * sin_sigma * (cos2sigma_m + B / 4 * (cos_sigma * (-1 + 2 * cos2sigma_m ** 2) - B / 6 * cos2sigma_m * (-3 + 4 * sin_sigma ** 2) * (-3 + 4 * cos2sigma_m ** 2))) | |
| sigma_new = s / (elip_b * A) + delta_sigma | |
| if abs(sigma_new - sigma) < tol: | |
| sigma = sigma_new | |
| break | |
| sigma = sigma_new | |
| lat2 = math.atan2(math.sin(U1) * cos_sigma + math.cos(U1) * sin_sigma * math.cos(alpha1), (1 - elip_f) * math.sqrt(sin_alpha ** 2 + (math.sin(U1) * sin_sigma - math.cos(U1) * cos_sigma * math.cos(alpha1)) ** 2)) | |
| lam = math.atan2(sin_sigma * math.sin(alpha1), math.cos(U1) * cos_sigma - math.sin(U1) * sin_sigma * math.cos(alpha1)) | |
| C = elip_f / 16 * cos2_alpha * (4 + elip_f * (4 - 3 * cos2_alpha)) | |
| L = lam - (1 - C) * elip_f * sin_alpha * (sigma + C * sin_sigma * (cos2sigma_m + C * cos_sigma * (-1 + 2 * cos2sigma_m ** 2))) | |
| lon2 = lon1 + L | |
| alpha2 = math.atan2(sin_alpha, -math.sin(U1) * sin_sigma + math.cos(U1) * cos_sigma * math.cos(alpha1)) | |
| return ([radToDeg(lat2), radToDeg(lon2)], (radToDeg(alpha2) + 360) % 360) | |
| SIDEREAL_DAY_S = 86164.0 | |
| OMEGA = 2.0 * math.pi / SIDEREAL_DAY_S | |
| SAT_EPOCH_UTC = datetime(2014, 3, 7, 16, 30, 0, tzinfo=timezone.utc) | |
| from datetime import datetime, timedelta, timezone | |
| def _safe_float(x, default=0.0): | |
| try: | |
| return float(x) | |
| except (TypeError, ValueError): | |
| return default | |
| def _extract_time_string(label_field): | |
| if isinstance(label_field, (tuple, list)): | |
| label = label_field[0] | |
| if isinstance(label, (tuple, list)): | |
| return str(label[0]) | |
| return str(label) | |
| return str(label_field) | |
| def _time_label_to_dt(label: str, epoch: datetime) -> datetime: | |
| hh, mm, ss = map(int, label.split(":")) | |
| day_offset = hh // 24 | |
| hh = hh % 24 | |
| dt = datetime(epoch.year, epoch.month, epoch.day, hh, mm, ss, tzinfo=epoch.tzinfo) | |
| if day_offset: | |
| dt += timedelta(days=day_offset) | |
| return dt | |
| def _extract_time_string(tfield): | |
| if isinstance(tfield, (tuple, list)): | |
| return str(tfield[0]) | |
| return str(tfield) | |
| def _build_sat_time_seconds(): | |
| times_sec = [] | |
| for entry in satLoc: | |
| tstr = _extract_time_string(entry[0]) | |
| try: | |
| h, m, s = map(int, tstr.split(':')) | |
| except Exception: | |
| continue | |
| add_day = 0 | |
| if h == 24: | |
| h = 0 | |
| add_day = 1 | |
| try: | |
| dt = datetime(SAT_EPOCH_UTC.year, SAT_EPOCH_UTC.month, SAT_EPOCH_UTC.day, h, m, s, tzinfo=timezone.utc) | |
| except ValueError: | |
| continue | |
| sec = (dt - SAT_EPOCH_UTC).total_seconds() + add_day * 86400.0 | |
| times_sec.append(sec) | |
| return np.array(times_sec, dtype=float) | |
| _sat_time_seconds = _build_sat_time_seconds() | |
| def seconds_since_sat_epoch(t_utc): | |
| return (t_utc - SAT_EPOCH_UTC).total_seconds() | |
| def calculateBTO_at_time(acLat, acLon, acAlt, time_utc): | |
| """ | |
| Compute BTO (microseconds) using the *same* satellite geometry as BFO: | |
| nearest satLoc entry to time_utc, with positions in km converted to meters. | |
| """ | |
| # Aircraft ECEF (meters) | |
| acECEF = latLonToECEF(acLat, acLon, acAlt) | |
| # Nearest satLoc satellite ECEF (meters) | |
| idx_nearest = nearest_measured_for_time(time_utc)[0] | |
| satECEF = 1000.0 * np.array(satLoc[idx_nearest][1], float) | |
| # Perth ground station ECEF (meters) | |
| gsECEF = 1000.0 * np.array(gsPerth, float) | |
| # One-way distances (meters) | |
| d1 = distBetECEF(acECEF, satECEF) # AC -> SAT | |
| d2 = distBetECEF(satECEF, gsECEF) # SAT -> GS | |
| # Round-trip signal time (microseconds), plus constant system bias | |
| bto_us = (2.0 * (d1 + d2) / speedOfLight_c) * 1e6 + btoBias | |
| return bto_us | |
| def calculateBFO_at_time(flightV_mps, track_deg, acLat, acLon, acAlt, acVS_mps, time_utc): | |
| """ | |
| Predict BFO using the satellite state from satLoc that is NEAREST to time_utc. | |
| Includes δF_sat + δF_AFC from satLoc[idx][3] and BFO_bias. | |
| """ | |
| # --- aircraft velocity in ENU and ECEF --- | |
| vE = flightV_mps * math.sin(degToRad(track_deg)) | |
| vN = flightV_mps * math.cos(degToRad(track_deg)) | |
| vU = acVS_mps | |
| acECEF = latLonToECEF(acLat, acLon, acAlt) # meters | |
| vECEF = ENU2ECEFvelo(vE, vN, vU, degToRad(acLat), degToRad(acLon)) # m/s | |
| # --- find nearest satLoc entry for this UTC --- | |
| idx_nearest = nearest_measured_for_time(time_utc)[0] | |
| # --- satellite state from satLoc (convert km → m, km/s → m/s) --- | |
| sat_pos_km = np.array(satLoc[idx_nearest][1], float) | |
| sat_vel_km_s = np.array(satLoc[idx_nearest][2], float) | |
| satECEF = 1000.0 * sat_pos_km # m | |
| satV_ECEF = 1000.0 * sat_vel_km_s # m/s | |
| # --- ground station (Perth) ECEF in meters --- | |
| gsECEF = 1000.0 * np.array(gsPerth, float) | |
| # --- line-of-sight unit vectors --- | |
| u_ac_sat = toECEFunitVector(acECEF, satECEF) | |
| u_sat_gs = toECEFunitVector(satECEF, gsECEF) | |
| # --- relative LOS velocities --- | |
| v_rel_ac_sat = float(np.dot(vECEF - satV_ECEF, u_ac_sat)) # m/s | |
| v_rel_sat_gs = float(np.dot(satV_ECEF, u_sat_gs)) # m/s (GS static in ECEF) | |
| # --- Doppler terms --- | |
| Fup = uplinkFreq * (v_rel_ac_sat / speedOfLight_c) | |
| Fdown = downlinkFreq * (v_rel_sat_gs / speedOfLight_c) | |
| # --- aircraft frequency compensation toward nominal sat location --- | |
| nomECEF = latLonToECEF(nominalSatLoc[0], nominalSatLoc[1], nominalSatLoc[2]) | |
| u_ac_nom = toECEFunitVector(acECEF, nomECEF) | |
| v_rel_ac_nom = float(np.dot(vECEF, u_ac_nom)) | |
| FcompAC = uplinkFreq * (v_rel_ac_nom / speedOfLight_c) | |
| # --- δF_sat + δF_AFC from satLoc (index 3), safe-cast in case of None --- | |
| fsat_afc = float(satLoc[idx_nearest][3]) if satLoc[idx_nearest][3] is not None else 0.0 | |
| # --- final BFO --- | |
| return Fup + Fdown - FcompAC + fsat_afc + BFO_bias | |
| def nearest_measured_for_time(t_utc): | |
| """ | |
| Return (idx, label_str, meas_bfo_hz, meas_bto_us) for the satLoc entry whose time label | |
| is nearest to t_utc. Guard against None in measured fields. | |
| """ | |
| times = [] | |
| for i, row in enumerate(satLoc): | |
| label_str = _extract_time_string(row[0]) | |
| try: | |
| dt = _time_label_to_dt(label_str, SAT_EPOCH_UTC) | |
| except Exception: | |
| continue | |
| times.append((i, dt, label_str)) | |
| if not times: | |
| raise RuntimeError("No valid satLoc times to compare.") | |
| idx, dt_near, label = min(times, key=lambda it: abs((t_utc - it[1]).total_seconds())) | |
| meas_bfo = _safe_float(satLoc[idx][4], float("nan")) | |
| meas_bto = _safe_float(satLoc[idx][5], float("nan")) | |
| return (idx, label, meas_bfo, meas_bto) | |
| def nearest_measured_with_bto_for_time(t_utc): | |
| """Like nearest_measured_for_time, but guarantees meas_bto is not None if possible.""" | |
| best = None | |
| best_dt = None | |
| for i, row in enumerate(satLoc): | |
| # row[0] is a time label like "19:40:00" | |
| row_dt = SAT_EPOCH_UTC + timedelta(seconds=_sat_time_seconds[i]) | |
| dt = abs((t_utc - row_dt).total_seconds()) | |
| meas_bto = row[5] | |
| if meas_bto is None: | |
| continue | |
| if best is None or dt < best_dt: | |
| best = (i, row[0], row[4], meas_bto) | |
| best_dt = dt | |
| if best is not None: | |
| return best | |
| # Fall back: return nearest even if BTO is None | |
| idx, meas_time_str, meas_bfo, meas_bto = nearest_measured_for_time(t_utc) | |
| return (idx, meas_time_str, meas_bfo, meas_bto) | |
| def auto_tune_heading_to_bto(current_lat, current_lon, alt_m, current_time, base_heading_deg, | |
| speed_knots, leg_minutes, model_sel, search_half_width_deg=5.0, | |
| heading_step_deg=0.1): | |
| """Search around base_heading_deg to minimize |measured_bto - calculated_bto| at leg end. | |
| Returns dict with best_heading, best_lat, best_lon, best_time, best_bto, best_delta_bto, meas_time_str. | |
| """ | |
| speed_mps = speed_knots * 0.514444 | |
| distance_m = speed_mps * (leg_minutes * 60.0) | |
| target_time = current_time + timedelta(minutes=leg_minutes) | |
| # get measurement reference (prefer one with non-None BTO) | |
| meas_idx, meas_time_str, meas_bfo, meas_bto = nearest_measured_with_bto_for_time(target_time) | |
| # If measurement BTO is still None (should be rare), we cannot tune. | |
| if meas_bto is None: | |
| return { | |
| "ok": False, | |
| "reason": "No measured BTO available near target time", | |
| "best_heading": base_heading_deg, | |
| "meas_time_str": meas_time_str, | |
| } | |
| def wrap(h): | |
| h = h % 360.0 | |
| return h + 360.0 if h < 0 else h | |
| best = None | |
| # number of steps on each side | |
| n = int(round(search_half_width_deg / heading_step_deg)) | |
| for k in range(-n, n + 1): | |
| hdg = wrap(base_heading_deg + k * heading_step_deg) | |
| if model_sel == '1': | |
| (lat2, lon2), _ = vincenty_direct((current_lat, current_lon), hdg, distance_m) | |
| else: | |
| lat2, lon2 = rhumb_direct((current_lat, current_lon), hdg, distance_m) | |
| bto_val = calculateBTO_at_time(lat2, lon2, alt_m, target_time) | |
| delta_bto = meas_bto - bto_val | |
| score = abs(delta_bto) | |
| if best is None or score < best["score"]: | |
| best = { | |
| "ok": True, | |
| "score": score, | |
| "best_heading": hdg, | |
| "best_lat": lat2, | |
| "best_lon": lon2, | |
| "best_time": target_time, | |
| "best_bto": bto_val, | |
| "best_delta_bto": delta_bto, | |
| "meas_time_str": meas_time_str, | |
| "meas_bto": meas_bto, | |
| } | |
| return best | |
| def auto_tune_heading_speed_to_bto(current_lat, current_lon, alt_m, current_time, | |
| base_heading_deg, base_speed_knots, leg_minutes, model_sel, | |
| heading_half_width_deg=8.0, heading_step_deg=0.2, | |
| speed_half_width_knots=80.0, speed_step_knots=2.0, | |
| refine_heading_half_width_deg=1.0, refine_heading_step_deg=0.05, | |
| refine_speed_half_width_knots=10.0, refine_speed_step_knots=0.5): | |
| """Search around (base_heading_deg, base_speed_knots) to minimize |ΔBTO| at leg end. | |
| Two-stage search: | |
| 1) coarse grid over heading±heading_half_width_deg and speed±speed_half_width_knots | |
| 2) refine around best using smaller steps | |
| Returns dict with best_heading, best_speed_knots, best_lat, best_lon, best_time, best_bto, | |
| best_delta_bto, meas_time_str, meas_bto. | |
| """ | |
| target_time = current_time + timedelta(minutes=leg_minutes) | |
| meas_idx, meas_time_str, meas_bfo, meas_bto = nearest_measured_with_bto_for_time(target_time) | |
| if meas_bto is None: | |
| return { | |
| "ok": False, | |
| "reason": "No measured BTO available near target time.", | |
| "best_heading": base_heading_deg, | |
| "best_speed_knots": base_speed_knots, | |
| "best_lat": None, | |
| "best_lon": None, | |
| "best_time": target_time, | |
| "best_bto": None, | |
| "best_delta_bto": None, | |
| "meas_time_str": meas_time_str, | |
| "meas_bto": None, | |
| } | |
| def eval_candidate(hdg_deg: float, spd_knots: float): | |
| speed_mps = spd_knots * 0.514444 | |
| distance_m = speed_mps * (leg_minutes * 60.0) | |
| if model_sel == '1': | |
| (latlon2, _az2) = vincenty_direct((current_lat, current_lon), hdg_deg, distance_m) | |
| lat2, lon2 = latlon2 | |
| else: | |
| lat2, lon2 = rhumb_direct((current_lat, current_lon), hdg_deg, distance_m) | |
| bto_val = calculateBTO_at_time(lat2, lon2, alt_m, target_time) | |
| delta_bto = meas_bto - bto_val | |
| return lat2, lon2, bto_val, delta_bto | |
| def wrap_heading(h): | |
| h = h % 360.0 | |
| return h + 360.0 if h < 0 else h | |
| best = None | |
| # ---- stage 1: coarse grid ---- | |
| h0 = base_heading_deg | |
| s0 = base_speed_knots | |
| h_min = h0 - heading_half_width_deg | |
| h_max = h0 + heading_half_width_deg | |
| s_min = max(0.0, s0 - speed_half_width_knots) | |
| s_max = s0 + speed_half_width_knots | |
| # Build grids (inclusive ends) | |
| headings = np.arange(h_min, h_max + 1e-9, heading_step_deg) | |
| speeds = np.arange(s_min, s_max + 1e-9, speed_step_knots) | |
| for spd in speeds: | |
| for hdg in headings: | |
| hdg_w = wrap_heading(float(hdg)) | |
| lat2, lon2, bto_val, delta_bto = eval_candidate(hdg_w, float(spd)) | |
| score = abs(delta_bto) | |
| if best is None or score < best["score"]: | |
| best = { | |
| "ok": True, | |
| "score": score, | |
| "best_heading": hdg_w, | |
| "best_speed_knots": float(spd), | |
| "best_lat": lat2, | |
| "best_lon": lon2, | |
| "best_time": target_time, | |
| "best_bto": bto_val, | |
| "best_delta_bto": delta_bto, | |
| "meas_time_str": meas_time_str, | |
| "meas_bto": meas_bto, | |
| } | |
| # ---- stage 2: refine around the best ---- | |
| if best is None: | |
| return { | |
| "ok": False, | |
| "reason": "Search failed.", | |
| "best_heading": base_heading_deg, | |
| "best_speed_knots": base_speed_knots, | |
| "best_lat": None, | |
| "best_lon": None, | |
| "best_time": target_time, | |
| "best_bto": None, | |
| "best_delta_bto": None, | |
| "meas_time_str": meas_time_str, | |
| "meas_bto": meas_bto, | |
| } | |
| h1 = best["best_heading"] | |
| s1 = best["best_speed_knots"] | |
| headings2 = np.arange(h1 - refine_heading_half_width_deg, h1 + refine_heading_half_width_deg + 1e-9, refine_heading_step_deg) | |
| speeds2 = np.arange(max(0.0, s1 - refine_speed_half_width_knots), s1 + refine_speed_half_width_knots + 1e-9, refine_speed_step_knots) | |
| for spd in speeds2: | |
| for hdg in headings2: | |
| hdg_w = wrap_heading(float(hdg)) | |
| lat2, lon2, bto_val, delta_bto = eval_candidate(hdg_w, float(spd)) | |
| score = abs(delta_bto) | |
| if score < best["score"]: | |
| best.update({ | |
| "score": score, | |
| "best_heading": hdg_w, | |
| "best_speed_knots": float(spd), | |
| "best_lat": lat2, | |
| "best_lon": lon2, | |
| "best_bto": bto_val, | |
| "best_delta_bto": delta_bto, | |
| }) | |
| return best | |
| def repl_fly_from_radar_fix(): | |
| print('=== MH370 REPL (radar fix) ===') | |
| print('Start: 2014-03-07 18:22:12Z @ 6.578°N, 96.341°E, FL350') | |
| current_lat = 6.578 | |
| current_lon = 96.341 | |
| current_alt_m = feetToM(35000) | |
| current_time = datetime(2014, 3, 7, 18, 22, 12, tzinfo=timezone.utc) | |
| path_points = [(current_lat, current_lon, current_time.isoformat())] | |
| while True: | |
| try: | |
| heading = float(input('HEADING (deg): ').strip()) | |
| speed_knots = float(input('SPEED (knots): ').strip()) | |
| model_sel = input('Enter 1 for VINCENTY or 2 for RHUMB: ').strip() | |
| model_sel = '1' if model_sel not in ('1', '2') else model_sel | |
| leg_str = input('Leg duration in minutes (default 10): ').strip() | |
| leg_minutes = float(leg_str) if leg_str else 10.0 | |
| except Exception as e: | |
| print('Invalid input, try again.', e) | |
| continue | |
| auto_sel = input('Auto-tune: (h) heading, (b) heading+speed, Enter = none: ').strip().lower() | |
| speed_mps = speed_knots * 0.514444 | |
| distance_m = speed_mps * (leg_minutes * 60.0) | |
| if auto_sel in ('b', 'both'): | |
| tuned = auto_tune_heading_speed_to_bto( | |
| current_lat=current_lat, | |
| current_lon=current_lon, | |
| alt_m=current_alt_m, | |
| current_time=current_time, | |
| base_heading_deg=heading, | |
| base_speed_knots=speed_knots, | |
| leg_minutes=leg_minutes, | |
| model_sel=model_sel, | |
| ) | |
| if tuned.get('ok'): | |
| heading = tuned['best_heading'] | |
| speed_knots = tuned['best_speed_knots'] | |
| # update distance for subsequent printing/metrics | |
| speed_mps = speed_knots * 0.514444 | |
| distance_m = speed_mps * (leg_minutes * 60.0) | |
| lat2 = tuned['best_lat'] | |
| lon2 = tuned['best_lon'] | |
| new_time = tuned['best_time'] | |
| print(f"[Auto-tune] Best heading: {heading:.2f}° | Best speed: {speed_knots:.2f} kt | Expected ΔBTO vs {tuned['meas_time_str']}: {tuned['best_delta_bto']:+.3f} µs") | |
| else: | |
| if model_sel == '1': | |
| (lat2, lon2), _ = vincenty_direct((current_lat, current_lon), heading, distance_m) | |
| else: | |
| lat2, lon2 = rhumb_direct((current_lat, current_lon), heading, distance_m) | |
| new_time = current_time + timedelta(minutes=leg_minutes) | |
| elif auto_sel in ('h', 'heading', 'y', 'yes'): | |
| tuned = auto_tune_heading_to_bto( | |
| current_lat=current_lat, | |
| current_lon=current_lon, | |
| alt_m=current_alt_m, | |
| current_time=current_time, | |
| base_heading_deg=heading, | |
| speed_knots=speed_knots, | |
| leg_minutes=leg_minutes, | |
| model_sel=model_sel, | |
| search_half_width_deg=5.0, | |
| heading_step_deg=0.1, | |
| ) | |
| if tuned.get('ok'): | |
| heading = tuned['best_heading'] | |
| lat2 = tuned['best_lat'] | |
| lon2 = tuned['best_lon'] | |
| new_time = tuned['best_time'] | |
| print(f"[Auto-tune] Best heading: {heading:.2f}° | Expected ΔBTO vs {tuned['meas_time_str']}: {tuned['best_delta_bto']:+.3f} µs") | |
| else: | |
| if model_sel == '1': | |
| (lat2, lon2), _ = vincenty_direct((current_lat, current_lon), heading, distance_m) | |
| else: | |
| lat2, lon2 = rhumb_direct((current_lat, current_lon), heading, distance_m) | |
| new_time = current_time + timedelta(minutes=leg_minutes) | |
| else: | |
| if model_sel == '1': | |
| (lat2, lon2), _ = vincenty_direct((current_lat, current_lon), heading, distance_m) | |
| else: | |
| lat2, lon2 = rhumb_direct((current_lat, current_lon), heading, distance_m) | |
| new_time = current_time + timedelta(minutes=leg_minutes) | |
| bto_val = calculateBTO_at_time(lat2, lon2, current_alt_m, new_time) | |
| tsec = seconds_since_sat_epoch(new_time) | |
| bfo_val = calculateBFO_at_time(speed_mps, heading, lat2, lon2, current_alt_m, 0.0, new_time) | |
| idx, meas_time_str, meas_bfo, meas_bto = nearest_measured_for_time(new_time) | |
| delta_bto = meas_bto - bto_val | |
| delta_bfo = meas_bfo - bfo_val | |
| print() | |
| print(f'New coordinates: {lat2:.6f}, {lon2:.6f} | Calculated BTO: {bto_val:.3f} (Δ vs nearest@{meas_time_str}: {delta_bto:.3f}) | Calculated BFO: {bfo_val:.1f} (Δ vs nearest@{meas_time_str}: {delta_bfo:.1f}) | Distance travelled: {distance_m / 1000.0:.2f} km | Timing (UTC): {new_time.isoformat()}') | |
| print() | |
| path_points.append((lat2, lon2, new_time.isoformat())) | |
| print('Would you like to:') | |
| print('A) Output the flight path generated so far') | |
| print('B) Input a new heading and ground speed for calculating the flight path to handshake no. 3') | |
| print('C) Restart calculations from an earlier handshake (Handshake 1 to 2)') | |
| print('D) Proceed to calculate a flight path to handshake no. 4') | |
| print('E) Manually input coordinates and speed for BTO & BFO calculations at handshake 3') | |
| print('F) Exit the program') | |
| selection = input('Selection: ').strip().upper() | |
| # --- Menu actions (dispatch table) --- | |
| advance_after_menu = True # if False, do not overwrite current_* with the just-computed leg | |
| def action_A(): | |
| print('\nFlight path so far (index: lat, lon, utc):') | |
| for i, (la, lo, ts) in enumerate(path_points): | |
| print(f'{i:02d}: {la:.6f}, {lo:.6f}, {ts}') | |
| def action_B(): | |
| # Placeholder: keep behavior as a no-op (you can wire this to a heading/speed editor later). | |
| pass | |
| def action_C(): | |
| idx_str = input(f'Restart from index (0..{len(path_points) - 1}): ').strip() | |
| try: | |
| i_idx = int(idx_str) | |
| if 0 <= i_idx < len(path_points): | |
| la, lo, ts = path_points[i_idx] | |
| return ('restart', i_idx, la, lo, ts) | |
| else: | |
| print('Index out of range.') | |
| except Exception as e: | |
| print('Invalid index.', e) | |
| return None | |
| def action_D(): | |
| return 'continue' | |
| def action_E(): | |
| try: | |
| la = float(input('Latitude (deg): ')) | |
| lo = float(input('Longitude (deg): ')) | |
| spd_kn = float(input('Speed (knots): ')) | |
| hdg2 = float(input('Heading (deg): ')) | |
| t_str = input('UTC time (YYYY-MM-DDTHH:MM:SSZ, blank = nearest handshake to current time): ').strip() | |
| if t_str: | |
| if t_str.endswith('Z'): | |
| t_str = t_str[:-1] + '+00:00' | |
| t_utc = datetime.fromisoformat(t_str) | |
| else: | |
| nearest_idx, meas_ts, *_ = nearest_measured_for_time(current_time) | |
| t_utc = SAT_EPOCH_UTC + timedelta(seconds=_sat_time_seconds[nearest_idx]) | |
| spd_mps = spd_kn * 0.514444 | |
| bto_v = calculateBTO_at_time(la, lo, current_alt_m, t_utc) | |
| bfo_v = calculateBFO_at_time(spd_mps, hdg2, la, lo, current_alt_m, 0.0, t_utc) | |
| idxn, m_ts, m_bfo, m_bto = nearest_measured_for_time(t_utc) | |
| print(f'Manual point BTO: {bto_v:.3f} (meas@{m_ts}: {m_bto}, Δ: {m_bto - bto_v:.3f})') | |
| print(f'Manual point BFO: {bfo_v:.1f} (meas@{m_ts}: {m_bfo}, Δ: {m_bfo - bfo_v:.1f})') | |
| except Exception as e: | |
| print('Bad manual input.', e) | |
| def action_F(): | |
| return 'exit' | |
| actions = { | |
| 'A': action_A, | |
| 'B': action_B, | |
| 'C': action_C, | |
| 'D': action_D, | |
| 'E': action_E, | |
| 'F': action_F, | |
| } | |
| action = actions.get(selection) | |
| if action is None: | |
| # Unknown / blank input: do nothing. | |
| pass | |
| else: | |
| result = action() | |
| if result == 'exit': | |
| print('Exiting.') | |
| break | |
| if result == 'continue': | |
| current_lat, current_lon, current_time = (lat2, lon2, new_time) | |
| continue | |
| if isinstance(result, tuple) and result and result[0] == 'restart': | |
| _, i_idx, la, lo, ts = result | |
| current_lat, current_lon = (la, lo) | |
| current_time = datetime.fromisoformat(ts.replace('Z', '+00:00')) | |
| path_points = path_points[:i_idx + 1] | |
| print(f'Restarted from index {i_idx}.') | |
| advance_after_menu = False | |
| if advance_after_menu: | |
| current_lat, current_lon, current_time = (lat2, lon2, new_time) | |
| if __name__ == '__main__': | |
| repl_fly_from_radar_fix() | |
| def satloc_labels(): | |
| return [_extract_time_string(r[0]) for r in satLoc] | |