#!/usr/bin/env python3
"""
The Balloon and the Planet -- companion script v1
solvetheuniverse.com / Pressure-Based Theory, 2026-09-23

WHAT THIS IS. The numbers behind the article, re-runnable by anyone. Four
parts, each stating its inputs and where they came from:

  A. Perihelion precession. General relativity's per-orbit advance
       delta_phi = 6 pi G M / (c^2 a (1 - e^2))
     converted to arcseconds per Julian century for Mercury, Venus, Earth
     and Mars from published orbital elements, set against the measured
     values, and set against the table in a 2025 X post that claimed to
     match Venus, Earth and Mars "within one, two and three arcseconds."
  B. Light bending in a fluid medium. Calls the SHIPPED function from
     barotropic-medium-gravity-v1.py (this site's twenty-ninth calculation,
     2026-09-14) rather than re-implementing it: a barotropic medium in
     hydrostatic equilibrium, fitted once to the clock benchmark, and the
     solar-limb deflection that same medium then predicts.
  C. Bernoulli between two balloons. The pressure drop 1/2 rho v^2 for a
     stated air speed, and the same expression at v = 0.
  D. The 2025 write-up's own mechanism, Bernoulli on a line vortex, and the
     force law it actually gives (1/r^3, against the claimed 1/r^2).

Item 11 rule: a fixture calls the shipped function, never a pasted copy.
Part B loads barotropic-medium-gravity-v1.py and how-it-could-work-v1.py
from this directory and calls them.

INPUTS (every number is an input unless marked computed):
  Orbital elements: NASA Planetary Fact Sheet (nssdc.gsfc.nasa.gov),
    semi-major axis, eccentricity, sidereal period.
  GM_sun, c: IAU 2015 nominal / exact.
  Measured precessions: Mercury -- Will, Living Rev. Relativ. 17, 4 (2014),
    eq. 65 gives 42.98 x (2+2gamma-beta)/3 and reports the Messenger-era fits
    as beta-1 = (-4.1 +/- 7.8)e-5 (Cassini gamma adopted), i.e. agreement to
    better than 1e-4, not as a precession with an error bar; Venus
    8.6247 +/- 0.0005 and Earth 3.8387 +/- 0.0004 from Table 1 of Biswas and
    Mani, Cent. Eur. J. Phys. 6, 754 (2008), arXiv:0802.0176, read from the
    paper's own PDF (observational column, Venus credited to Pitjeva 2007);
    Mars -- Will 2014 reports beta-1 = (0.4 +/- 2.4)e-4 from Mars
    Reconnaissance Orbiter data, agreement with GR to ~2e-4, again not as a
    precession figure.
  The post's table: predicted 43 / 3.8 / 1.4 and observed 42 / 5.0 / 4.0
    for Venus / Earth / Mars, transcribed from
    x.com/slave_2_liberty/status/1959675672391291224 (2025-08-24).

Run:
  python3 the-balloon-and-the-planet-v1.py
  python3 the-balloon-and-the-planet-v1.py --selftest
"""
import argparse, importlib.util, math, os, sys

HERE = os.path.dirname(os.path.abspath(__file__))

# ---- constants ----------------------------------------------------------------
c      = 299_792_458.0          # m/s, exact
GM_sun = 1.327_124_400_18e20    # m^3/s^2, IAU 2015 nominal
R_sun  = 6.957e8                # m, IAU 2015 nominal
AU     = 1.495_978_707e11       # m, exact
ARCSEC = math.pi / (180 * 3600)
JULIAN_CENTURY_DAYS = 36525.0

# NASA Planetary Fact Sheet: a in 10^6 km, e, sidereal period in days
PLANETS = {
    "Mercury": dict(a=57.909e9,  e=0.2056, T=87.969),
    "Venus":   dict(a=108.210e9, e=0.0068, T=224.701),
    "Earth":   dict(a=149.598e9, e=0.0167, T=365.256),
    "Mars":    dict(a=227.956e9, e=0.0935, T=686.980),
}

MEASURED = {  # arcsec / Julian century, (value, one sigma, source)
    "Mercury": (42.98, None, "GR value; Messenger fits beta-1 = (-4.1 +/- 7.8)e-5, Will 2014 eq. 65"),
    "Venus":   (8.6247, 0.0005, "Biswas & Mani 2008 Table 1 (Pitjeva 2007)"),
    "Earth":   (3.8387, 0.0004, "Biswas & Mani 2008 Table 1"),
    "Mars":    (None, None, "MRO fits beta-1 = (0.4 +/- 2.4)e-4 with Cassini gamma, Will 2014"),
}

POST_TABLE = {  # the 2025 X post's own numbers
    "Venus": dict(predicted=43.0, observed=42.0),
    "Earth": dict(predicted=3.8,  observed=5.0),
    "Mars":  dict(predicted=1.4,  observed=4.0),
}

# ---- Part A: precession -------------------------------------------------------
def gr_precession_arcsec_per_century(a, e, T, GM=GM_sun):
    """General relativity's per-orbit perihelion advance, 6 pi GM / (c^2 a (1-e^2)),
    scaled to a Julian century by the number of orbits in that time."""
    per_orbit = 6 * math.pi * GM / (c**2 * a * (1 - e**2))       # radians
    orbits_per_century = JULIAN_CENTURY_DAYS / T
    return per_orbit * orbits_per_century / ARCSEC

# ---- Part B: shipped fluid-medium functions -----------------------------------
def load(name, modname):
    path = os.path.join(HERE, name)
    if not os.path.exists(path):
        sys.exit(f"missing {name} in {HERE}; download it from /models/ alongside this script")
    spec = importlib.util.spec_from_file_location(modname, path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod

def fluid_medium_rows():
    bm = load("barotropic-medium-gravity-v1.py", "bmg_v1")
    Gamma = bm.gamma_for_clock(c)                     # fitted once, to the clock benchmark
    defl = bm.deflection_arcsec(R_sun, GM_sun, Gamma, c)
    gr_defl = (4 * GM_sun / (R_sun * c**2)) / ARCSEC
    closed = (2 * GM_sun / (R_sun * c**2)) / ARCSEC
    shift_22m = bm.clock_shift(6.371e6, 6.371e6 + 22.5, GM_earth(), Gamma, c)
    return dict(Gamma=Gamma, deflection=defl, gr=gr_defl, closed=closed,
                ratio=defl / gr_defl, shift_22m=shift_22m,
                sobral=(bm.SOBRAL_DEFL, bm.SOBRAL_ERR),
                cassini=(bm.CASSINI_GM1, bm.CASSINI_SIG))

def GM_earth():
    return 3.986_004_418e14   # m^3/s^2, IERS 2010

# ---- Part C: Bernoulli --------------------------------------------------------
def bernoulli_drop_pa(v, rho=1.204):
    """Pressure drop 1/2 rho v^2 between two bodies for air moving at v between
    them (rho: dry air at 20 C, 1 atm). Zero flow, zero drop."""
    return 0.5 * rho * v**2

# ---- Part D: the write-up's own vortex mechanism ------------------------------
def vortex_bernoulli_pressure(r, Gamma_circ, rho=1.0, p0=0.0):
    """The 2025 write-up's stated mechanism: a line vortex v(r) = Gamma/(2 pi r)
    and Bernoulli p = p0 - rho v^2 / 2, claimed to yield a 'gravitational-like
    force scaling as F ~ 1/r^2'. This returns that pressure field as written."""
    v = Gamma_circ / (2 * math.pi * r)
    return p0 - 0.5 * rho * v**2

def vortex_force_exponent(Gamma_circ=1.0, r=1.0, h=1e-4):
    """Local power-law exponent of the pressure-gradient force, d(ln|dp/dr|)/d(ln r),
    from the field above by finite differences. Bernoulli on a 1/r velocity gives
    p ~ -1/r^2 and hence a force ~ 1/r^3, not 1/r^2."""
    def dpdr(rr):
        return (vortex_bernoulli_pressure(rr + h, Gamma_circ) - vortex_bernoulli_pressure(rr - h, Gamma_circ)) / (2 * h)
    r2 = 2.0 * r
    return math.log(abs(dpdr(r2)) / abs(dpdr(r))) / math.log(r2 / r)

# ---- report -------------------------------------------------------------------
def run():
    print("=" * 78)
    print("The Balloon and the Planet -- companion script v1 (2026-09-23)")
    print("=" * 78)
    print("\n[A] Perihelion precession, arcsec per Julian century")
    print(f"{'planet':8s} {'GR (computed)':>14s} {'measured':>18s}   {'post predicted':>14s} {'post observed':>13s}")
    gr = {}
    for name, el in PLANETS.items():
        gr[name] = gr_precession_arcsec_per_century(el["a"], el["e"], el["T"])
        m, s, src = MEASURED[name]
        meas = f"{m:.4f} +/- {s:.4f}" if (m is not None and s is not None) else ("agrees w/ GR; see note" if m else "agrees w/ GR; see note")
        pt = POST_TABLE.get(name)
        pp = f"{pt['predicted']:.1f}" if pt else "-"
        po = f"{pt['observed']:.1f}" if pt else "-"
        print(f"{name:8s} {gr[name]:14.3f} {meas:>18s}   {pp:>14s} {po:>13s}")
    print("\n    The post's 'predicted' column against GR under a different label:")
    for post_planet, gr_planet in (("Venus", "Mercury"), ("Earth", "Earth"), ("Mars", "Mars")):
        print(f"    post '{post_planet}' predicted {POST_TABLE[post_planet]['predicted']:.1f}"
              f"  ~  GR {gr_planet} {gr[gr_planet]:.2f}")
    print(f"    post 'Venus' observed 42.0 vs measured Venus {MEASURED['Venus'][0]:.4f}: "
          f"off by {42.0 - MEASURED['Venus'][0]:.1f} arcsec/century")
    print(f"    post 'Earth' observed 5.0 vs measured Earth {MEASURED['Earth'][0]:.4f}: "
          f"off by {5.0 - MEASURED['Earth'][0]:.1f} arcsec/century")

    print("\n[B] A fluid medium, fitted to the clock, then asked about light (shipped functions)")
    f = fluid_medium_rows()
    print(f"    fitted polytropic index Gamma = {f['Gamma']:+.3f}  (from the clock benchmark alone)")
    print(f"    22.5 m height shift, model: {f['shift_22m']:.4e}  (Pound-Rebka / Pound-Snider tower)")
    print(f"    solar-limb deflection, model: {f['deflection']:.4f} arcsec  (closed form 2GM/(bc^2): {f['closed']:.4f})")
    print(f"    general relativity 4GM/(bc^2):  {f['gr']:.4f} arcsec;  Sobral 1920: {f['sobral'][0]} +/- {f['sobral'][1]} (1 s.d.)")
    print(f"    model / GR = {f['ratio']:.4f};  implied PPN gamma = {2*f['ratio']-1:+.4f};  "
          f"Cassini gamma-1 = {f['cassini'][0]:.1e} +/- {f['cassini'][1]:.1e}")
    sig = abs((2*f['ratio']-1) - 1 - f['cassini'][0]) / f['cassini'][1]
    print(f"    distance from Cassini: {sig:,.0f} sigma")

    print("\n[C] Bernoulli between two balloons")
    for v in (0.0, 5.0, 10.0):
        dp = bernoulli_drop_pa(v)
        print(f"    air at {v:4.1f} m/s between them: pressure drop {dp:6.1f} Pa;"
              f" on a 0.03 m^2 face, {dp*0.03:.2f} N")
    print("\n[D] The write-up's own mechanism: Bernoulli on a line vortex v = Gamma/(2 pi r)")
    print(f"    pressure-gradient force falls as r^({vortex_force_exponent():.3f}); the write-up claims F ~ 1/r^2")
    print("    (and a line vortex is axial, so its field has an axis, where gravity has none)")
    print("\nNotes: [A] inputs are orbital elements; the GR column is computed, the measured"
          "\ncolumn is quoted (sources in the docstring). [B] every number comes from the"
          "\nshipped twenty-ninth-calculation functions. [C] is the textbook expression."
          "\n[D] is the write-up's own velocity field under Bernoulli, differentiated numerically.")

# ---- self-test ----------------------------------------------------------------
def selftest():
    n = 0
    # 1. GR formula reproduces Mercury's 42.98 from the fact-sheet elements
    m = gr_precession_arcsec_per_century(**PLANETS["Mercury"])
    assert abs(m - 42.98) < 0.1, m; n += 1
    # 2. and Earth's measured 3.8387 within the elements' own rounding
    e = gr_precession_arcsec_per_century(**PLANETS["Earth"])
    assert abs(e - 3.8387) < 0.02, e; n += 1
    # 3. the post's 'Venus 43' is Mercury's GR value, not Venus's (the mislabel is mechanical)
    v = gr_precession_arcsec_per_century(**PLANETS["Venus"])
    assert abs(43.0 - m) < 0.5 and abs(43.0 - v) > 30, (m, v); n += 1
    # 4. shipped deflection at the fitted Gamma matches its own closed form (calls the shipped code)
    f = fluid_medium_rows()
    assert abs(f["deflection"] - f["closed"]) / f["closed"] < 1e-4, f; n += 1
    # 5. and is half of GR, the article's central number
    assert abs(f["ratio"] - 0.5) < 1e-3, f["ratio"]; n += 1
    # 6. failure path: a Gamma that is NOT fitted to the clock does not give the clock shift
    bm = load("barotropic-medium-gravity-v1.py", "bmg_v1b")
    wrong = bm.clock_shift(6.371e6, 6.371e6 + 22.5, GM_earth(), 1.0, c)   # Gamma = 1: Step-0 null
    assert abs(wrong) < 1e-20 and abs(f["shift_22m"]) > 1e-15, (wrong, f["shift_22m"]); n += 1
    # 7. Bernoulli: zero flow, zero drop; 10 m/s gives 60.2 Pa
    assert bernoulli_drop_pa(0.0) == 0.0 and abs(bernoulli_drop_pa(10.0) - 60.2) < 0.1; n += 1
    # 8. the write-up's vortex: Bernoulli on v ~ 1/r gives a force ~ 1/r^3, not the claimed 1/r^2
    ex = vortex_force_exponent()
    assert abs(ex + 3.0) < 1e-3, ex; n += 1
    print(f"selftest: {n}/8 passed (Mercury 42.98 from elements; Earth 3.84; the post's 'Venus 43' "
          f"is GR Mercury; shipped deflection vs closed form; half of GR; Gamma=1 null as failure path; Bernoulli; vortex force ~ 1/r^3)")

if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--selftest", action="store_true")
    a = ap.parse_args()
    selftest() if a.selftest else run()
