module

Kozai::Illumination

Whether a pass can actually be watched.

A radio pass needs only the horizon. A visual one needs two more things at the same time: the satellite has to be in sunlight, and the sky over the station has to be dark. Both are geometry, and neither is visible in an elevation plot — which is the point. A pass list that does not know about them presents a satellite crossing the Earth's shadow over a station in broad daylight with exactly the same confidence as one blazing overhead at the end of twilight.

illumination = Kozai::Illumination.for(satellite, observer, pass, -6.0)
illumination.category # => Kozai::Illumination::Category::Visual
illumination.visible  # => [{start, stop}] — when to actually go outside

Constants

ITERATIONS = Passes::REFINEMENT_ITERATIONS

Iteration cap for the bisection, shared with Passes.

SUBDIVISIONS = Passes::THRESHOLD_SUBDIVISIONS

How many samples a pass is divided into when looking for shadow and twilight boundaries.

The same figure Passes uses to narrow a pass to a threshold, and for the same reason: it is a fraction of the pass being searched rather than of the orbit, so the resolution follows what is actually being asked about.

SUN_RADIUS_KM = Sun::RADIUS_KM

Nominal photosphere radius, kilometres. See Sun::RADIUS_KM.

TOLERANCE_MINUTES = Passes::REFINEMENT_TOLERANCE_MINUTES

How precisely a boundary is located, in minutes. Shared with Passes.

Class methods

for(satellite : SGP4::Satellite, observer : Observer, pass : Passes::Pass, twilight : Float64) : PassIllumination

Everything about the illumination of one pass.

twilight is the Sun elevation at the station below which the sky counts as dark: −6 for civil twilight, which is the default and the convention visual pass predictions are usually quoted at.

Source
phase_angle(satellite_teme : Vector3, sun_teme : Vector3, observer : Observer, gmst : Float64) : Float64

The Sun–satellite–observer angle in degrees.

Zero would be the observer looking straight down the sunbeam at a fully lit face; 180 is a satellite between observer and Sun, showing its dark side. Computed in the earth-fixed frame because that is where the station already lives — Observer#ecef is precomputed once at construction.

Source
shadow(satellite_teme : Vector3, sun_teme : Vector3) : Shadow

Which part of the Earth's shadow a satellite is in.

satellite_teme and sun_teme are both geocentric, both in TEME, and both in kilometres.

NOTE: the frame is TEME and no rotation is needed. The shadow is a cone with its axis through the centre of the Earth pointing away from the Sun, so the test is a comparison of two vectors and one radius, and every term of it is invariant under the rotation about the polar axis that separates TEME from the earth-fixed frame. TEME's z is that polar axis, which is also what the flattening correction below needs, so this runs directly on the propagator's output. The test is evaluated a few hundred times per pass; a rotation avoided is worth having.

NOTE: the Earth is an ellipsoid here, and a cone, not a cylinder.

Two corrections, both small, both worth naming because their absence is what a comparison against another tool measures:

  • The cone. The Sun is not a point, so the umbra narrows behind the Earth by tan(0.264°) per kilometre — about 32 km at the distance a low satellite crosses it. Treating the shadow as a cylinder of the Earth's radius, which is what Skyfield's is_sunlit and PyEphem's eclipsed both do, puts eclipse entry about four seconds early and exit about four seconds late for a satellite in a low orbit.
  • The flattening. The Earth is 21.385 km smaller pole to pole than it is across, so a spherical shadow is too large over the poles by nearly all of that — around three seconds of flight. Scaling z by a/b maps the ellipsoid onto a sphere of radius a and removes the term exactly. The scaling is not conformal, so it distorts the cone angle itself by one part in 298 of 0.264 degrees — a tenth of a kilometre on the boundary, three orders of magnitude below the term it removes.

And between the umbra and full sunlight there is a penumbra some 65 km wide at low altitudes: about eight or nine seconds of flight. Eclipse entry is not an instant, and neither this function nor anything reading it should pretend otherwise.

Source
shadow_at(satellite : SGP4::Satellite, minutes : Float64) : Shadow | Nil

:ditto:, at minutes after the element set epoch.

Source
shadow_of(satellite : SGP4::Satellite, time : Time) : Shadow | Nil

Which part of the shadow satellite is in at time.

Source
umbra_margin(satellite_teme : Vector3, sun_teme : Vector3) : Float64

Signed distance from the umbra boundary in kilometres, positive outside.

This is what the boundary search bisects. A continuous quantity rather than the Shadow value itself, for the same reason Passes bisects an elevation rather than an "is it up" flag: a root of a smooth function is located to the tolerance asked for, while a sign change in a boolean is only ever located to the width of the last step. It is also the quantity a cross-check needs, since a disagreement about the shadow model is a disagreement in kilometres before it is one in seconds.

Source

Nested types