"""Phasing: drop onto a smaller ellipse, come back early, and the cost of haste.

Panel A (geometry).  The chaser trails the target by an angle dth on a common
circular orbit of radius r.  It burns *retrograde* at its own position, which
becomes the **apoapsis** of a smaller ellipse with the same focus, tangent to
the circle at that one point.  Because a_ph < r the phasing period is shorter,
so after k revolutions on the ellipse the chaser is back at the burn point --
and the target, which had to cover 2*pi*k - dth in the same time, arrives
there at the same instant.  A second, equal and opposite impulse re-circularises.

The drawing is exaggerated for legibility: dth = 60 deg in one revolution is a
far larger correction than any real rendezvous asks for, and the Earth is drawn
well out of scale so that the phasing perigee -- the quantity that actually
limits the manoeuvre -- is visible as a gap.

Panel B (cost).  Total dv against the number of phasing revolutions k.

Run from the sfm-slides directory:

    python3 codes/fig-man-phasing.py
"""
import numpy as np
import matplotlib.pyplot as plt
from _manstyle import CREAM, INK, MAROON, BLUE, GREEN, AMBER, PURPLE, despine

mu, RE = 398600.4418, 6378.137

# ---- panel A : geometry ---------------------------------------------------
figL, axL = plt.subplots(figsize=(7.8, 6.2))

r = 1.0
dth = np.radians(60.0)                      # exaggerated for legibility
k = 1
aph = r*(1 - dth/(2*np.pi*k))**(2/3)        # Kepler III on the shorter period
rp = 2*aph - r                              # apoapsis is the burn point
e = (r - rp)/(r + rp)

th = np.linspace(0, 2*np.pi, 720)
R_EARTH = 0.52                              # out of scale, and said so
axL.fill(R_EARTH*np.cos(th), R_EARTH*np.sin(th), color=BLUE, alpha=0.10,
         zorder=0)
axL.plot(R_EARTH*np.cos(th), R_EARTH*np.sin(th), color=BLUE, lw=1.0,
         alpha=0.45, zorder=1)

# the common circular orbit, and the phasing ellipse (apoapsis on +x)
axL.plot(r*np.cos(th), r*np.sin(th), color=BLUE, lw=2.0, zorder=3)
f = np.linspace(0, 2*np.pi, 720)
rr = aph*(1 - e**2)/(1 + e*np.cos(f))
axL.plot(-rr*np.cos(f), -rr*np.sin(f), color=MAROON, lw=2.4, zorder=4)

axL.text(0.0, r + 0.04, r"circular orbit, radius $r$, period $T$", color=BLUE,
         fontsize=11, ha="center", va="bottom")
axL.text(0.0, -aph - 0.30, "phasing ellipse:  " r"$a_{ph}<r$",
         color=MAROON, fontsize=11, ha="center", va="center")

# the phasing perigee -- the constraint that really bites
axL.plot(-rp, 0.0, "o", color=MAROON, ms=6, mfc=CREAM, mew=1.8, zorder=6)
axL.annotate("perigee " r"$r_{p}=2a_{ph}-r$:" "\nthe binding constraint",
             xy=(-rp, 0.0), xytext=(-1.66, -0.60), color=MAROON, fontsize=10,
             ha="left", va="top",
             arrowprops=dict(arrowstyle="-", color=MAROON, lw=0.9, alpha=0.7))

# the two spacecraft, and the angle between them
burn = np.array([r, 0.0])
tgt = np.array([r*np.cos(dth), r*np.sin(dth)])
for p, col in ((burn, MAROON), (tgt, GREEN)):
    axL.plot([0.0, p[0]], [0.0, p[1]], color=INK, lw=0.9, ls=":", alpha=0.55,
             zorder=2)
axL.plot(*burn, "o", color=MAROON, ms=11, zorder=7)
axL.plot(*tgt, "o", color=GREEN, ms=11, zorder=7)
axL.annotate("target\n(leads by $\\Delta\\theta$)", xy=tgt, xytext=(1.02, 1.12),
             color=GREEN, fontsize=11, ha="left", va="center",
             arrowprops=dict(arrowstyle="-", color=GREEN, lw=0.9, alpha=0.7))
axL.text(burn[0] + 0.09, burn[1] + 0.02, "chaser\n(burn point)", color=MAROON,
         fontsize=11, ha="left", va="center")

arc = np.linspace(0.0, dth, 120)
R_ARC = 0.63                       # in the clear annulus: Earth < R_ARC < ellipse
axL.plot(R_ARC*np.cos(arc), R_ARC*np.sin(arc), color=GREEN, lw=1.8, zorder=5)
axL.text(0.73*np.cos(0.5*dth), 0.73*np.sin(0.5*dth), r"$\Delta\theta$",
         color=GREEN, fontsize=14, ha="center", va="center")

# sense of motion, drawn on the circle itself
sweep = np.linspace(np.radians(112), np.radians(150), 40)
axL.annotate("", xy=(1.14*np.cos(sweep[-1]), 1.14*np.sin(sweep[-1])),
             xytext=(1.14*np.cos(sweep[0]), 1.14*np.sin(sweep[0])),
             arrowprops=dict(arrowstyle="-|>", color=INK, lw=1.6, alpha=0.75,
                             connectionstyle="arc3,rad=-0.18"))
axL.text(1.18*np.cos(np.radians(136)), 1.18*np.sin(np.radians(136)),
         "direction\nof motion", color=INK, fontsize=9.5, alpha=0.8,
         ha="right", va="bottom")

# the two impulses: equal and opposite, one revolution apart, same point
L = 0.34
axL.annotate("", xy=(burn[0], burn[1] - L), xytext=(burn[0], burn[1] - 0.02),
             arrowprops=dict(arrowstyle="-|>", color=AMBER, lw=2.6))
axL.text(burn[0] + 0.09, burn[1] - 0.30,
         r"$\Delta v_1$ retrograde" "\ndrop onto the ellipse", color=AMBER,
         fontsize=10.5, ha="left", va="center")
xo = burn[0] + 0.30
axL.annotate("", xy=(xo, burn[1] + L), xytext=(xo, burn[1] + 0.02),
             arrowprops=dict(arrowstyle="-|>", color=AMBER, lw=2.6))
axL.text(xo + 0.07, burn[1] + 0.30,
         r"$\Delta v_2$ prograde" "\n" r"$k$ revs later, re-circularise",
         color=AMBER, fontsize=10.5, ha="left", va="center")

axL.plot(0, 0, "o", color=INK, ms=8, zorder=8)
axL.text(0.0, -1.30,
         "smaller $a$  $\\Rightarrow$  shorter period: the chaser returns to the burn point\n"
         "after $k$ revolutions, just as the target --- which had to cover "
         "$2\\pi k-\\Delta\\theta$ --- arrives",
         color=MAROON, fontsize=10.5, ha="center", va="top")
axL.text(-1.66, 1.30, "Earth, $\\Delta\\theta$ and the perigee drop\nexaggerated for legibility",
         color=INK, fontsize=8.5, alpha=0.55, ha="left", va="top")

axL.set_aspect("equal"); axL.axis("off")
axL.set_xlim(-1.66, 1.66)
axL.set_ylim(-1.62, 1.34)
axL.set_title("Phasing orbit: same burn point, smaller ellipse", fontsize=12.5)

figL.savefig("img/fig-man-phasing-geom.png", dpi=160, bbox_inches="tight")
plt.close(figL)
print("wrote img/fig-man-phasing-geom.png")

# ---- panel B : cost vs waiting time --------------------------------------
figR, axR = plt.subplots(figsize=(9.0, 5.8))

r0 = 6778.0
vc = np.sqrt(mu/r0)
T0 = 2*np.pi*np.sqrt(r0**3/mu)
dth_deg = np.array([5.0, 10.0, 20.0, 40.0])
ks = np.arange(1, 11)
for d, col in zip(dth_deg, (BLUE, MAROON, GREEN, PURPLE)):
    dt = np.radians(d)
    aph = r0*(1 - dt/(2*np.pi*ks))**(2/3)
    dv = 2*np.abs(np.sqrt(mu*(2/r0 - 1/aph)) - vc)*1000     # m/s
    axR.plot(ks, dv, "o-", color=col, lw=2.0, ms=5, label=fr"$\Delta\theta={d:.0f}^\circ$")
    if d == 40.0:
        rpk = 2*aph - r0
        bad = rpk < RE + 100
        axR.plot(ks[bad], dv[bad], "x", color=INK, ms=11, mew=2.2)
        axR.text(1.15, dv[0] - 60, "perigee below 100 km:\ninfeasible", color=INK, fontsize=9.5)

axR.set_xlabel(r"number of phasing revolutions  $k$")
axR.set_ylabel(r"total $\Delta v = 2|v_{ph}-v_c|$   [m/s]")
axR.set_title("Patience is the cheapest propellant  ($\\Delta v \\propto 1/k$)", fontsize=12)
axR.set_xticks(ks)
axR.legend(frameon=False, fontsize=10)
axR.set_ylim(0, 640)
despine(axR)

figR.tight_layout()
figR.savefig("img/fig-man-phasing-cost.png", dpi=160, bbox_inches="tight")
plt.close(figR)
print("wrote img/fig-man-phasing-cost.png")

# ---- verification ---------------------------------------------------------
if __name__ == "__main__":
    Tph_over_T = (aph_g := r*(1 - dth/(2*np.pi*k))**(2/3))**1.5 / r**1.5
    print(f"\ndth = {np.degrees(dth):.1f} deg, k = {k}")
    print(f"  a_ph/r        = {aph_g:.6f}")
    print(f"  T_ph/T        = {Tph_over_T:.6f}  (target {1 - dth/(2*np.pi*k):.6f})")
    print(f"  r_p/r         = {2*aph_g - r:.6f}   (drawn Earth radius {R_EARTH})")
    print(f"  chaser return = {k*Tph_over_T:.6f} T")
    print(f"  target angle  = {np.degrees(2*np.pi*k*Tph_over_T + dth) % 360:.3f} deg"
          "  (0 deg = at the burn point)")
