Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ Only recent releases are listed. Older entries are in this file's git history (`

## Unreleased

### Added

- Kepler: checked constructor `Kepler::try_new` / `Kepler::validate` (Python: the constructor and the `a`/`eccen`/`inclination`/`mu` setters raise `ValueError` for a non-finite value, `a <= 0`, `eccen` outside [0, 1), `incl` outside [0, π] or `mu <= 0` instead of producing NaN); per-instance gravitational parameter `mu` (`Kepler::with_mu`, `from_pv_with_mu`; Python `kepler(..., mu=)`, `kepler.mu`, `from_pv(..., mu=)`) so lunar and heliocentric elements have a correct period, `propagate` and `to_pv`; derived quantities `periapsis`, `apoapsis`, `specific_energy`, `angular_momentum`, `flight_path_angle`, `argument_of_latitude`, `true_longitude`; `SatState::from_kepler` / `satstate.from_kepler(time, kepler)`; a one-line `repr` for Python `kepler`; `Kepler` derives `PartialEq` and serde (`mu` defaults to Earth's when absent), `Anomaly` derives `Copy`/`PartialEq` ([#168](https://github.com/ssmichael1/satkit/pull/168))

### Fixed

- A corrupt or truncated `tab5.2*.txt` in a data directory no longer panics the first frame transform: satkit warns and uses the compiled-in copy of the same IERS table (exact, not an approximation), and the parser now rejects text with no table header or fewer rows than declared — an HTML notice page saved under the table's name previously loaded as six empty series and silently dropped the nutation terms ([#166](https://github.com/ssmichael1/satkit/pull/166))

### Changed

- **Breaking (Rust):** `Kepler.w` is renamed `Kepler.argp`, the struct gains a `mu` field (struct-literal construction must supply it — prefer `Kepler::new(...).with_mu(...)`), and `kepler::Error` is `#[non_exhaustive]` (new `InvalidElement` variant). Python: `argp` is the constructor parameter and property name; `w` still works as a constructor keyword and as a property (kept indefinitely) but emits `DeprecationWarning`. **Breaking (Python):** `kepler.from_pv` raises `ValueError` instead of `RuntimeError` for a hyperbolic/parabolic or rectilinear state, and every element setter (`a`, `eccen`, `inclination`, `raan`, `argp`, `nu`, `mu`) raises `ValueError` for an out-of-domain or non-finite value instead of accepting it ([#168](https://github.com/ssmichael1/satkit/pull/168))
- `update_datafiles()` no longer downloads files that are compiled into the library: the IERS tables and gravity models are `default: false` in the manifest (still pinned and fetchable by name), and the unused `leap-seconds.list` (nothing ever read it — the runtime leap-second table is a compiled-in constant) is removed from the manifest entirely. The only static download left is the JPL ephemeris, alongside the daily EOP / space-weather / solar-cycle refreshes ([#163](https://github.com/ssmichael1/satkit/pull/163))

### Docs
Expand Down
83 changes: 63 additions & 20 deletions docs/guide/kepler.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ the [Keplerian Elements tutorial](../tutorials/Keplerian%20Elements.ipynb).
## The Element Set

A bound two-body orbit is described by six numbers. `satkit` stores them under
these names, in SI units and radians:
these names, in SI units and radians, together with the gravitational
parameter of the body they orbit:

| Field | Symbol | Meaning |
|---------|------------|------------------------------------------------------|
| `a` | $a$ | semi-major axis, **meters** |
| `a` | $a$ | semi-major axis, **meters**, $a > 0$ |
| `eccen` | $e$ | eccentricity, $0 \le e < 1$ |
| `incl` | $i$ | inclination, radians, $0 \le i \le \pi$ |
| `raan` | $\Omega$ | right ascension of the ascending node, radians |
| `w` | $\omega$ | argument of perigee, radians |
| `argp` | $\omega$ | argument of periapsis, radians |
| `nu` | $\nu$ | true anomaly, radians |
| `mu` | $\mu$ | gravitational parameter of the central body, m³ s⁻²; Earth's unless given |

The size and shape of the ellipse are $a$ and $e$; the orientation of the
orbital plane and of the ellipse within it are $i$, $\Omega$ and $\omega$; and
Expand All @@ -26,9 +28,45 @@ The semiparameter (semi-latus rectum) $p = a(1 - e^2)$ is available as a
derived property, but the class is constructed from $a$, not $p$.

In Python the inclination property is spelled `inclination`; the constructor
argument and the Rust field are `incl`. Angles returned by `from_pv` are
argument and the Rust field are `incl`. The argument of periapsis was called
`w` before 0.22; in Python `w` still works as a constructor keyword and as a
property (kept indefinitely), with a `DeprecationWarning`. Angles returned by `from_pv` are
reduced to $[0, 2\pi)$; angles you set are stored as given.

### Validation

The Python constructor, `from_pv`, and every element setter reject an
element outside its domain with `ValueError`: a non-finite value,
$a \le 0$, $e \notin [0, 1)$, $i \notin [0, \pi]$ or $\mu \le 0$. The bounds
are strict — $e = 1$ is not a closed orbit and is refused rather than
producing NaN anomalies. The `mean_anomaly` / `eccentric_anomaly` setters
likewise refuse a non-finite value, so an element set can never hold NaN
through any setter. In Rust, `Kepler::try_new`
performs the same checks (returning `kepler::Error::InvalidElement`, which
names the offending element) and `Kepler::validate` re-checks an element set
whose public fields were assigned directly; `Kepler::new` remains unchecked.

### Derived quantities

All are read-only properties in Python and methods in Rust:

| Property | Symbol / formula | Units |
|-------------------------|----------------------------------------------------|-------|
| `semiparameter` | $p = a(1 - e^2)$ | m |
| `periapsis` | $r_p = a(1 - e)$ | m |
| `apoapsis` | $r_a = a(1 + e)$ | m |
| `mean_motion` | $n = \sqrt{\mu / a^3}$ | rad/s |
| `period` | $T = 2\pi / n$ | s |
| `specific_energy` | $\xi = -\mu / 2a$ | J/kg |
| `angular_momentum` | $h = \sqrt{\mu p}$ | m²/s |
| `flight_path_angle` | $\gamma = \operatorname{atan2}(e\sin\nu,\ 1 + e\cos\nu)$ — zero at periapsis and apoapsis, positive while climbing | rad |
| `argument_of_latitude` | $u = \omega + \nu$, reduced to $[0, 2\pi)$ — defined for circular orbits | rad |
| `true_longitude` | $\lambda = \Omega + \omega + \nu$, reduced to $[0, 2\pi)$ — defined for circular equatorial orbits | rad |

`satstate.from_kepler(time, k)` (Rust `SatState::from_kepler`) builds a
propagatable state from the two-body position and velocity of an element
set, taken as GCRF at `time`.

### Anomalies

Three angles can locate the satellite in its orbit
Expand Down Expand Up @@ -75,26 +113,31 @@ the rest of `satkit` assumes **GCRF**, so convert ITRF or TEME states with
state produces elements that are numerically valid but physically
meaningless.

**Central body.** The Earth's gravitational parameter
[`consts.MU_EARTH`](../api/consts.md) ($3.986004418 \times 10^{14}$
m³ s⁻²) is used everywhere: in the `from_pv` energy equation, in `to_pv`, and
in the mean motion, period and `propagate`. There is no way to use a
different $\mu$; for heliocentric or lunar orbits compute the elements
yourself.

**Closed orbits only.** `from_pv` returns an error (Python: `RuntimeError`)
**Central body.** Each element set carries its own $\mu$. By default it is
the Earth's, [`consts.MU_EARTH`](../api/consts.md) ($3.986004418 \times
10^{14}$ m³ s⁻²), and it is used everywhere: in the `from_pv` energy
equation, in `to_pv`, and in the mean motion, period and `propagate`. For a
lunar or heliocentric orbit pass `mu=` to the constructor or to `from_pv`
(Rust: `Kepler::with_mu`, `Kepler::from_pv_with_mu`); the six geometric
elements are unchanged, only the dynamics re-target the other body. Note
that `from_pv` interprets a state with whatever $\mu$ it is given — a lunar
state read with Earth's $\mu$ yields a valid-looking but wrong ellipse.

**Closed orbits only.** `from_pv` returns an error (Python: `ValueError`)
for parabolic or hyperbolic states ($e \ge 1$) and for rectilinear states
(zero angular momentum, where the orbital plane is undefined). The
constructor itself does not validate $e$; supplying $e \ge 1$ produces
meaningless anomaly conversions.
(zero angular momentum, where the orbital plane is undefined). The Python
constructor and `Kepler::try_new` reject $e \ge 1$ up front (see
[Validation](#validation)).

**Singular cases.** $\Omega$ is undefined for an equatorial orbit and $\omega$
for a circular one. `from_pv` follows the conventions of
[Vallado (2013)](references.md#vallado2013), Algorithm 9: for a circular
inclined orbit `w` is 0 and `nu` holds the argument of latitude; for an
elliptical equatorial orbit `raan` is 0 and `w` holds the true longitude of
inclined orbit `argp` is 0 and `nu` holds the argument of latitude; for an
elliptical equatorial orbit `raan` is 0 and `argp` holds the true longitude of
perigee; for a circular equatorial orbit both are 0 and `nu` holds the true
longitude. In each case `to_pv` reproduces the input state.
longitude. In each case `to_pv` reproduces the input state. The
`argument_of_latitude` and `true_longitude` properties give the well-defined
combinations directly in every case.

## Conversions

Expand Down Expand Up @@ -125,7 +168,7 @@ longitude. In each case `to_pv` reproduces the input state.
eccen=0.001,
incl=math.radians(98.0),
raan=math.radians(45.0),
w=0.0,
argp=0.0,
mean_anomaly=math.radians(30.0),
)
print(f"period = {k.period / 60:.2f} min, nu = {math.degrees(k.nu):.3f} deg")
Expand Down Expand Up @@ -158,7 +201,7 @@ longitude. In each case `to_pv` reproduces the input state.
0.001, // eccen
98.0_f64.to_radians(), // incl
45.0_f64.to_radians(), // raan
0.0, // w
0.0, // argp
Anomaly::Mean(30.0_f64.to_radians()),
);
println!("period = {:.2} min, nu = {:.3} deg",
Expand Down
Loading