> For the complete documentation index, see [llms.txt](https://threadpoolx.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://threadpoolx.gitbook.io/docs/htb-writeups/challenges/impulsive-thoughts.md).

# Impulsive Thoughts

### Challenge Information

| Field          | Details                             |
| -------------- | ----------------------------------- |
| **Name**       | Impulsive Thoughts                  |
| **Category**   | Science / Orbital Mechanics         |
| **Difficulty** | Easy                                |
| **Tags**       | Python, Astrodynamics, Hohmann, API |

***

### Challenge Description

> Control is re-established, but time is running out. Following standard procedure, the launch vehicle deployed our satellite into a highly elliptical Geostationary Transfer Orbit (GTO). Normally, the satellite would automatically perform its own burn (Apogee Kick) to circularize its orbit and park in its final Geostationary (GEO) slot. With Auto-Guidance offline, this will not happen. Calculate the exact instantaneous impulse (Δv) required at apogee (this GTO intercepts GEO at apogee) to circularize the orbit, and inject it into the system.

***

### Understanding the Challenge

Two files are provided:

* **`app.py`** — the Flask server running on the target
* **`client.py`** — a skeleton solve script with one stubbed function

#### The API

| Method | Endpoint          | Purpose                                                              |
| ------ | ----------------- | -------------------------------------------------------------------- |
| GET    | `/api/gto_orbit`  | Returns the satellite's current orbit as Keplerian elements          |
| POST   | `/api/gto_to_geo` | Validates your maneuver (orbit + dt + dv) against GEO constraints    |
| POST   | `/api/simulate`   | Runs a full simulation and returns the **flag** if maneuver is valid |

#### The Full Flow

```
1. Fetch GTO orbit  →  a, e, i, Ω, ω, ν  (Keplerian elements, angles in radians)
2. Compute          →  dt (seconds to apogee)  +  dv [x, y, z] (km/s burn vector)
3. Validate         →  server checks resulting orbit meets GEO constraints
4. Simulate         →  if correct, server returns FLAG
```

#### Server-Side Constraints

From `app.py`:

```python
MAX_REQUESTS      = 10       # seed resets after 10 requests
MAX_DV_ALLOWED    = 3        # km/s, sanity cap on burn magnitude
GEO_APOGEE_KM     = 42164.0  # target orbital radius
ORBIT_TOLERANCE   = 10.0     # km tolerance on semi-major axis

# GEO verification checks:
abs(a - 42164) <= 10   # semi-major axis within 10 km of GEO
e <= 0.001             # nearly circular
i < 1                  # inclination < 1 radian (~57°)
```

#### The Stub Function

Inside `client.py`, the only thing to implement is:

```python
def solve_gto_to_geo(gto_orbit):
    # TODO: Implement this.
    return None, None
```

It must return a tuple `(dt, dv_vector)` where:

* `dt` = seconds from simulation start to the burn
* `dv_vector` = numpy array `[dv_x, dv_y, dv_z]` in km/s

***

### Background: The Apogee Kick Maneuver (AKM)

A **GTO (Geostationary Transfer Orbit)** is an ellipse where:

* **Perigee** (lowest point) is at LEO altitude (\~6678 km from Earth's centre)
* **Apogee** (highest point) is at GEO altitude (\~42164 km)

The satellite rides this ellipse for half an orbit, coasting upward until it reaches apogee. At that exact moment, it fires its engine in the **prograde direction** (the direction it is already travelling) to increase velocity just enough to match the circular GEO speed. This single burn is called the **Apogee Kick Maneuver**.

```
          Apogee (42164 km) ← BURN HERE
              ╭───────╮
             ╱         ╲
GEO ────────●           ●──────── GEO
             ╲         ╱
              ╰────●───╯
              Perigee (6678 km) ← Start here
```

After the burn, the orbit is circular at GEO altitude — the satellite is parked.

***

### The Algorithm

The solution requires two calculations: **when to burn** (dt) and **how much to burn** (dv).

#### Part 1 — Time to Apogee (dt)

The satellite is currently at some true anomaly `ν` (its angular position along the orbit). Apogee is at `ν = π` radians (180°). We need to find how many seconds it takes to travel from the current position to apogee.

This uses **Kepler's Equation**, which relates position to time.

**Step 1: True anomaly → Eccentric anomaly**

```
E₀ = 2 · arctan2( √(1−e) · sin(ν/2),  √(1+e) · cos(ν/2) )
```

**Step 2: Eccentric anomaly → Mean anomaly**

```
M₀ = E₀ − e · sin(E₀)
```

**Step 3: Mean anomaly at apogee**

At `ν = π`: `E_apo = π`, and `M_apo = π − e·sin(π) = π`

**Step 4: Time difference**

```
n  = √(μ / a³)           ← mean motion (rad/s)
dt = (M_apo − M₀) / n    ← time in seconds

# If the satellite has already passed apogee this orbit, wait for next pass:
if dt < 0:
    dt += 2π / n          ← add one full orbital period
```

For the common case where the orbit starts at perigee (`ν = 0`), `M₀ = 0` and `dt = π / n = T / 2` — exactly half the orbital period.

#### Part 2 — Delta-V Vector (dv)

**Step 1: Compute the velocity change needed**

Using the **vis-viva equation** to find GTO speed at apogee:

```
r_apo    = a · (1 + e)                   ← apogee radius = 42164 km
v_gto    = √( μ · (2/r_apo − 1/a) )     ← GTO speed at apogee
v_geo    = √( μ / r_apo )               ← GEO circular speed at same altitude
Δv_mag   = v_geo − v_gto                 ← positive: we need to speed up
```

**Step 2: Find the burn direction**

At apogee (`ν = π`), the satellite's velocity in the **Perifocal (PQW) frame** is:

```
v_PQW = [ −√(μ/p)·sin(π),  √(μ/p)·(e + cos(π)),  0 ]
       = [ 0,               √(μ/p)·(e − 1),         0 ]
```

Since `e < 1`, the y-component is negative. The unit direction is `[0, −1, 0]` — pointing in the **−Q direction** of the perifocal frame.

**Step 3: Rotate the direction to ECI**

Using the same rotation matrix as in the Elementary challenge (`R = Rz(−Ω) · Rx(−i) · Rz(−ω)`):

```
cΩ, sΩ = cos(Ω), sin(Ω)
ci, si = cos(i),  sin(i)
cω, sω = cos(ω),  sin(ω)

R = | cΩ·cω − sΩ·sω·ci    −cΩ·sω − sΩ·cω·ci    sΩ·si |
    | sΩ·cω + cΩ·sω·ci    −sΩ·sω + cΩ·cω·ci   −cΩ·si |
    | si·sω                 si·cω                ci    |

v̂_ECI = R · [0, −1, 0]
```

**Step 4: Assemble the dv vector**

```
dv = Δv_mag · v̂_ECI
```

***

### Solution

#### Full Solve Script

```python
import requests
import numpy as np
from typing import Tuple

MU = 398600.4418          # km³/s²  Earth gravitational parameter
API_BASE_URL = "http://<TARGET_IP>:<PORT>/api"


def solve_gto_to_geo(gto_orbit: Tuple) -> Tuple[float, np.ndarray]:
    """
    Compute time-to-burn (dt, seconds) and delta-v vector (km/s)
    for an instantaneous Apogee Kick Maneuver: GTO → GEO.
    All angles (i, Omega, omega, nu) are in RADIANS.
    """
    a, e, i, Omega, omega, nu = gto_orbit

    # ── Part 1: Time to apogee ──────────────────────────────────────────
    # True anomaly → Eccentric anomaly
    E0 = 2.0 * np.arctan2(
        np.sqrt(1.0 - e) * np.sin(nu / 2.0),
        np.sqrt(1.0 + e) * np.cos(nu / 2.0)
    )
    # Eccentric anomaly → Mean anomaly
    M0 = E0 - e * np.sin(E0)

    # Mean anomaly at apogee = π
    M_apo = np.pi

    # Mean motion (rad/s)
    n = np.sqrt(MU / a**3)

    # Seconds from simulation start to apogee
    dt = (M_apo - M0) / n
    if dt < 0:
        dt += 2.0 * np.pi / n    # wrap to next apogee passage

    # ── Part 2: Delta-V vector at apogee ────────────────────────────────
    r_apo = a * (1.0 + e)        # apogee radius ≈ 42164 km

    v_gto_apo = np.sqrt(MU * (2.0 / r_apo - 1.0 / a))   # GTO speed at apogee
    v_geo     = np.sqrt(MU / r_apo)                        # GEO circular speed

    dv_mag = v_geo - v_gto_apo   # positive → prograde burn to speed up

    # At nu=π, velocity direction in perifocal frame is [0, −1, 0]
    v_hat_pqw = np.array([0.0, -1.0, 0.0])

    # Rotation matrix: Perifocal → ECI
    cO, sO = np.cos(Omega), np.sin(Omega)
    ci, si = np.cos(i),     np.sin(i)
    co, so = np.cos(omega), np.sin(omega)

    R = np.array([
        [ cO*co - sO*so*ci,  -cO*so - sO*co*ci,  sO*si ],
        [ sO*co + cO*so*ci,  -sO*so + cO*co*ci, -cO*si ],
        [ si*so,               si*co,              ci   ]
    ])

    # Δv vector in ECI frame
    dv_vec = dv_mag * (R @ v_hat_pqw)

    return dt, dv_vec


def main():
    s = requests.Session()
    base = API_BASE_URL

    # 1. Fetch GTO orbit
    print("[*] Fetching GTO orbit...")
    data = s.get(f"{base}/gto_orbit").json()
    orbit_data = data.get("orbit", {})
    gto_orbit = (
        orbit_data['a'], orbit_data['e'], orbit_data['i'],
        orbit_data['Omega'], orbit_data['omega'], orbit_data['nu']
    )
    a, e = gto_orbit[0], gto_orbit[1]
    print(f"    a={a:.3f} km,  e={e:.6f}")
    print(f"    Apogee={a*(1+e):.3f} km,  Perigee={a*(1-e):.3f} km")

    # 2. Compute apogee kick
    print("\n[*] Computing apogee kick maneuver...")
    dt, dv = solve_gto_to_geo(gto_orbit)
    print(f"    Time to apogee (dt): {dt:.2f} s")
    print(f"    Δv magnitude:        {np.linalg.norm(dv):.6f} km/s")
    print(f"    Δv vector (km/s):    [{dv[0]:.6f}, {dv[1]:.6f}, {dv[2]:.6f}]")

    payload_base = {
        **orbit_data,
        "dt": dt, "dv_x": dv[0], "dv_y": dv[1], "dv_z": dv[2]
    }

    # 3. Validate maneuver
    print("\n[*] Validating maneuver with server...")
    val = s.post(f"{base}/gto_to_geo", json=payload_base).json()
    print(f"    Status : {val.get('status')}")
    print(f"    Message: {val.get('message')}")

    if not val.get('status'):
        print("\n[-] Validation failed. Exiting.")
        return

    # 4. Simulate and retrieve flag
    print("\n[*] Running simulation...")
    sim = s.post(f"{base}/simulate", json={"dt": dt, "dv_x": dv[0], "dv_y": dv[1], "dv_z": dv[2]}).json()
    print(f"    Status : {sim.get('status')}")
    print(f"    Message: {sim.get('message')}")
    print(f"\n{'='*55}")
    print(f"  FLAG => {sim.get('flag')}")
    print(f"{'='*55}\n")


if __name__ == "__main__":
    main()
```

#### Running the Script

Replace `<TARGET_IP>:<PORT>` with the challenge instance address and run:

```bash
python3 solve.py
```

#### Expected Output

```
[*] Fetching GTO orbit...
    a=24421.069 km,  e=0.726542
    Apogee=42164.000 km,  Perigee=6678.137 km

[*] Computing apogee kick maneuver...
    Time to apogee (dt): 19027.xx s
    Δv magnitude:        1.478xxx km/s
    Δv vector (km/s):    [x.xxxxxx, x.xxxxxx, 0.000000]

[*] Validating maneuver with server...
    Status : True
    Message: Valid GEO

[*] Running simulation...
    Status : True
    Message: GTO->GEO maneuver was successful!

=======================================================
  FLAG => HTB{...}
=======================================================
```

***

### Important Difference from the Previous Challenge (Elementary)

In **Elementary**, the `/api/simulate` endpoint accepted the full orbit payload from the client. In **Impulsive Thoughts**, the simulate endpoint regenerates the GTO orbit **server-side** using its internal seed:

```python
# app.py — /api/simulate
gto_orbit = generate_ideal_gto_orbit(seed=seed)    # generated internally
is_success, message, calculated_orbit = gto_to_geo(gto_orbit, dt, dv)
```

This means the simulation payload only needs `dt`, `dv_x`, `dv_y`, `dv_z` — **not** the orbit elements. As long as you call GET and then POST within the same 10-request window, the seed stays consistent and your `dv` matches the orbit the server is using.

***

### Summary

| Step            | What happened                                                                                                                                   |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Recon**       | Read `app.py` to understand the 3-endpoint API, GEO validation constraints, and that the simulate endpoint uses a server-side seed              |
| **Part 1 (dt)** | Used Kepler's Equation to propagate from current `ν` to apogee (`ν = π`), converting ν → E → M to get time-to-apogee                            |
| **Part 2 (dv)** | Used vis-viva to find the speed deficit at apogee, then rotated the prograde direction from Perifocal to ECI using the standard rotation matrix |
| **Angle units** | Same as Elementary — all angles from the server are in **radians**, not degrees                                                                 |
| **Flag**        | Returned by `/api/simulate` after the resulting orbit passes: `a ≈ 42164 km`, `e < 0.001`, `i < 1 rad`                                          |

***

### Key Takeaways

* The **Apogee Kick Maneuver** is a single prograde burn at apogee that converts an elliptical transfer orbit into a circular parking orbit. It is one of the most fundamental maneuvers in satellite operations.
* The **vis-viva equation** `v = √(μ·(2/r − 1/a))` is the universal tool for computing orbital speed at any point. For a circular orbit it simplifies to `v = √(μ/r)`.
* **Kepler's Equation** (`M = E − e·sin(E)`) is the bridge between orbital geometry (true anomaly) and time. The conversion chain is: `ν → E → M → t`.
* The velocity direction at apogee in the perifocal frame is always `[0, −1, 0]`. The rotation to ECI is identical to the matrix derived in the Elementary challenge — reusing it here saves work.
* Always verify that `dt > 0` and wrap to the next apogee if needed (`dt += T` when negative). This handles orbits that have already passed apogee and are coasting toward perigee.
