<sub>2026-03-08 @2037</sub> #orbital-mechanics #python #simulator [[2026-03-13-pytest-fixtures]] # Orbits, Sine Waves, and Why Vertical Speed Is Just the Derivative I have been working through a mock simulator in this project of mine and kept running into math that was really giving me a hard time. I couldn't quite figure out how to simulate telemetry pertaining to orbits like altitude and vertical speed (or commonly called radial speed). I understood basic derivatives, sin/cos waves but when reading papers about radial speed being calculated from altitude signal I got lost. This is my attempt to write it out slowly enough that it actually makes sense to me. For context: this project uses a mock simulator that mimics the physics engine of a particular video game. The game has real orbital mechanics built in, but for testing purposes I needed a lightweight simulator that produces plausible-looking telemetry signals without running the actual game. That simulator generates signals like altitude, vertical speed, and orbital velocity as pure math functions of elapsed time. That has been my plan anyways and I'm slowing getting to something that is usable. Understanding why those functions are shaped the way they are is what this post is about. --- ## Part One: Why Orbital Altitude Goes Up and Down ### An Orbit Is Just Falling Sideways Before getting into the math, it helped me to stop thinking of an orbit as a spacecraft flying through space and start thinking of it as a spacecraft that is constantly falling but moving sideways fast enough that the ground keeps curving away beneath it. It never stops falling. It just keeps missing the planet. ### Circular vs. Eccentric A perfectly circular orbit keeps the spacecraft at the same altitude the whole time. The altitude reading would be a flat line on a graph. That is the $e = 0$ case. Most real orbits are not perfect circles. They are ellipses, which means one side of the orbit is closer to the planet and one side is farther away. The closest point is called periapsis. The farthest point is apoapsis. The spacecraft spends each orbit sweeping continuously between these two extremes. Eccentricity is a number between 0 and 1 that describes how stretched the ellipse is. Zero means a circle. Higher means more stretched. *The planet sits at one focus of the ellipse, not at the center. That offset is what causes the altitude to change continuously throughout the orbit.* --- ## Part Two: Why I Model That With a Sine Wave ### The Shape of the Variation Once I accepted that altitude rises and falls each orbit, the next question was what shape that variation takes. Two things are true about it. It is **periodic**. It repeats on the same schedule every orbit (from a simple simulation point of view). If an orbit takes 90 minutes, the altitude returns to the same value after exactly 90 minutes, every time. It is **smooth**. There are no sharp corners. The spacecraft eases up toward apoapsis and eases back down. Nothing sudden happens. Periodic and smooth, with a peak and a trough. That is exactly what a sine wave is. ### What a Sine Wave Actually Says ![[orbit_eccentricity.gif]] *One full cycle. $T$ = one orbital period. The wave starts at center, climbs to the peak, falls back through center, reaches the bottom, and returns.* The general formula for this shape is: $\text{altitude}(t) = \text{centre} + \text{amplitude} \times \sin\!\left(\frac{2\pi\, t}{T}\right)$ Let me break each piece down the way I had to do it for it to click. **centre** is where the wave sits vertically. In the picture above it is $1$. Here $a = 1$ means the distance is normalized to the average orbital radius. So $1$ means $1$ orbit width. >[!note] >***In my simulator I need to express this in meters which I set to $250{,}000$ meters. That is the mean altitude, the midpoint the wave oscillates around.*** **amplitude** is how far above and below the centre the wave swings and is denoted as $e$ being the eccentricity value.So here when $e=0{.}40$ the curve oscillates between $0{.}60$ (periapsis) and $1{.}40$ (apoapsis). > [!note] > ***In my simulator I need to express this in meters which I set to $50{,}000$ meters. This is my amplitude (how much the orbit swings on either side)*** **$T$** is the period, meaning how many seconds it takes to complete one full orbit and return to the same altitude. In my expression below we can put in $5{,}400$ seconds, which apparently is a roughly 90-minute real-world LEO period I'm told. **$\dfrac{2\pi}{T}$** is the angular frequency. This is the part that confused me for a while. The reason $2\pi$ appears is that sine completes one full cycle from $0$ to $2\pi$ in radians. Dividing by $T$ (i think) stretches that cycle to fit the desired period. I've read that I can think of it as how fast the angle is spinning. **$t$** is just elapsed time in seconds which I get from the in-game physics engine. So with the simulator's actual numbers plugged in: $\text{altitude}(t) = 250{,}000 + 50{,}000 \times \sin\!\left(\frac{2\pi\, t}{5{,}400}\right)$ Which says: start at 250 km. Every 5,400 seconds, swing 50 km up, then 50 km down, then back. Repeat forever basically. ```Python class Simulator: # tunable _ALT_CENTRE = 250_000.0 # m mean orbit altitude _ALT_AMP = 50_000.0 # m altitude oscillation amplitude _ALT_PERIOD = 5_400.0 # s compressed ~90-min orbital period _UT_EPOCH = 1_000_000.0 # s fake universal-time offset def __init__(self): self._t0 = time.monotonic() def _t(self) -> float: # elapsed seconds since simulator start return time.monotonic() - self._t0 def _sin(self, period: float, phase: float = 0.0) -> float: return math.sin(2 * math.pi * self._t() / period + phase) def _cos(self, period: float, phase: float = 0.0) -> float: return math.cos(2 * math.pi * self._t() / period + phase) @property def ut(self) -> float: # simulate in-game UT (seconds) since mission epoch return self._t() + self._UT_EPOCH @property def mean_altitude(self) -> float: return self._ALT_CENTRE + self._ALT_AMP * self._sin(self._ALT_PERIOD) ``` --- ## Part Three: Vertical Speed Is the Derivative of Altitude This is the part that took me the longest to feel comfortable with. ### What a Derivative Actually Means Here The derivative of altitude with respect to time is the rate at which altitude is changing. That is literally what vertical speed is: meters per second. How fast is the altitude number going up or down right now. So I do not need a separate equation for vertical speed I don't believ. I can compute it directly from the altitude equation by taking its derivative. ### Working It Out Step by Step Start with the full altitude expression: $\text{altitude}(t) = 250{,}000 + 50{,}000 \times \sin\!\left(\frac{2\pi\, t}{5{,}400}\right)$ --- **Step 1 — The constant term disappears.** The derivative of any constant is zero. The $250{,}000$ is the mean altitude and never changes with time (or at least in my simple simulator it won't), so it drops out entirely: $\frac{d}{dt}\left[250{,}000\right] = 0$ --- **Step 2 — What remains.** After removing the constant I am left with: $\frac{d}{dt}\left[50{,}000 \times \sin\!\left(\frac{2\pi\, t}{5{,}400}\right)\right]$ --- **Step 3 — Pull the constant multiplier out front.** $50{,}000$ does not change with time (or at least not in my simple simulator it won't), so it rides along outside the derivative: $50{,}000 \times \frac{d}{dt}\left[\sin\!\left(\frac{2\pi\, t}{5{,}400}\right)\right]$ --- **Step 4 — Apply the chain rule.** I need the derivative of $\sin(\text{something} \times t)$. The chain rule says: take the derivative of the outer function, keep the inside unchanged, then multiply by the derivative of the inside. - Let $u = \dfrac{2\pi}{5{,}400}\, t$, so lets just briefly say the inside is $u$. - Ok, so the outer function is now $\sin(u)$ and its derivative is $\cos(u)$. - Now inside we have $\dfrac{2\pi}{5{,}400}\, t$ and its derivative with respect to $t$ is just $\dfrac{2\pi}{5{,}400}$ Applying the chain rule: $\frac{d}{dt}\left[\sin\!\left(\frac{2\pi\, t}{5{,}400}\right)\right] = \cos\!\left(\frac{2\pi\, t}{5{,}400}\right) \times \frac{2\pi}{5{,}400}$ --- **Step 5 — Reassemble everything.** Multiplying it all back together gives the final expression for vertical speed: $\boxed{v_z(t) = 50{,}000 \times \frac{2\pi}{5{,}400} \times \cos\!\left(\frac{2\pi\, t}{5{,}400}\right)}$ I can drop this in to my simulator code now: ```Python @property def vertical_speed(self) -> float: return ( self._ALT_AMP * (2 * math.pi / self._ALT_PERIOD) * self._cos(self._ALT_PERIOD) ) ``` --- **Step 6 — Reflect** So we can say the altitude wave follows this equation: $\text{altitude}(t) = 250{,}000 + 50{,}000 \cdot \sin\!\left(\frac{2\pi \, t}{T}\right)$ The vertical speed is the derivative of that (how fast altitude is changing at any moment): $v_z(t) = 50{,}000 \cdot \frac{2\pi}{T} \cdot \cos\!\left(\frac{2\pi \, t}{T}\right)$ The cosine term on the right is the key. No matter what surrounds it, cosine alone always oscillates between $-1$ and $+1$. That raw oscillation gets scaled up by everything in front of it: $50{,}000 \times \frac{2\pi}{5{,}400} \approx 50{,}000 \times 0.001164 \approx 58.2 \ \text{m/s}$ So vertical speed swings between $-58.2$ and $+58.2$ m/s (that is the green wave in the visual). At the midpoint (250 km, between apoapsis and periapsis) is where the orbit is neither climbing toward apoapsis nor falling toward periapsis. The altitude wave crosses through the centre and is at its steepest. The red tangent line is nearly vertical, cosine equals exactly $\pm 1$, and vertical speed hits its maximum of $\pm 58.2$ m/s. The spacecraft is moving through altitude the fastest here. At apoapsis (300 km, the far point) the spacecraft has reached the top of its altitude wave and momentarily stops climbing. The red tangent line goes flat, cosine equals zero, and vertical speed is exactly $0$ m/s. All of the spacecraft's velocity is now horizontal meaning it is neither climbing nor descending, just sweeping around the top of the ellipse before gravity pulls it back down toward periapsis. At periapsis (200 km, the close point) the mirror image of apoapsis. The spacecraft has reached the bottom of its altitude wave and momentarily stops descending. Tangent is flat, cosine is zero, vertical speed is $0$ m/s again. It is about to be flung back up toward apoapsis. ![[alt_vrt.gif]] $\text{steepest altitude slope} \implies \text{maximum vertical speed}$ $\text{flat altitude slope (apoapsis / periapsis)} \implies \text{zero vertical speed}$ The two graphs are always a quarter orbit out of phase with each other. Where the altitude wave is steepest the speed wave peaks. Where the altitude wave is flat at apoapsis or periapsis the speed wave crosses zero. That quarter-orbit offset is not a coincidence it is exactly what the derivative relationship between sine and cosine produces mathematically. I just never really thought about orbits like this. --- # Why This Matters Beyond the Simulator Thinking out loud here, the reason I need to compute these two signals this way, rather than having two independent equations, is that real telemetry behaves this way. Altitude and vertical speed are physically coupled. Now I haven't quite gotten to the ML portion in my project so this is also me thinking out loud. A ML model trained on signals that are internally computed like this probably has better chance of learning real relationships?! If the altitude and vertical speed numbers were generated independently and happened to disagree with each other, the model would be learning from noise rather than from structure. It would also be a poor simulation period. I feel like I have to do it this way. --- ## Notes to Myself 1. I still do not fully have the chain rule in my fingers. I need to work more examples by hand before it feels automatic. The pattern is: outer derivative times inner derivative. I've kind of lost my derivative skills since my calculus days. 2. The $2\pi$ in the period formula keeps catching me. It might help to think of it as: one full revolution of the unit circle is $2\pi$ radians, and the period $T$ is how many seconds that revolution takes?! 3. Come back to why Kepler's actual equation is asymmetric. The sine wave approximation treats rising and falling as mirror images. Real orbits spend more time near apoapsis. Understanding why would improve my understanding I think. 4. Should take another look at our atmospheric pressure calculation in the sim. I'm not sure if a Gaussian bell centered on a specific altitude make sense. 5. At some point I need to update my plots of these signals in my Jupyter Notebook. Right now using other visualizations just to get an understanding.