moonsatkit

Pure MoonBit toolkit for satellite orbit data, pass prediction, and visualization

satellite
orbit
tle
sgp4
geodesy
visualization
moon add oilleelssq-wq/moonsatkit@0.1.0
Download zip
Version
0.1.0
License
Apache-2.0
Last updated
6 hours ago
Downloads
2
README

#MoonSatKit

MoonSatKit is a pure MoonBit toolkit for satellite orbit data, TLE parsing, time handling, near-earth SGP4 propagation, ground-station observation, and pass prediction.

Project status: near-earth propagation phase. The package now includes a pure MoonBit near-earth SGP4 implementation validated against the Vallado 00005 reference state, plus SGP4-based ground-station pass prediction. Deep-space SDP4, full official vector coverage, and export formats remain planned work.

#Current scope

  • TLE parsing with raw field preservation
  • TLE checksum validation
  • SGP4 input metadata decoding: mean-motion derivatives and BSTAR
  • Pure MoonBit near-earth SGP4 propagation with WGS-72 constants and drag terms
  • Explicit Sgp4Elements initialization boundary with deep-space rejection
  • UTC civil timestamp parsing, formatting, Julian day conversion, and elapsed seconds
  • Leap-year and month-length helpers
  • TLE orbital-element decoding with structured numeric errors
  • Two-body Kepler preview propagation with ECI-style position and velocity output
  • GMST, ECI/ECEF position and state transforms with Earth rotation correction
  • WGS-84 geodetic and ground-station coordinate conversion
  • Ground-station ENU observation with azimuth, elevation, range, range rate, and horizon status
  • Two-body preview pass prediction with AOS, TCA, LOS, and maximum elevation
  • SGP4 pass prediction with one-second event refinement and ordered visibility windows
  • JSON serialization for orbit states and pass windows
  • Low-precision geocentric Sun position and right-ascension/declination output
  • Geometric sunlit, penumbra, and umbra classification for propagated states
  • CSV pass-window export and GeoJSON SGP4 ground-track export
  • Named two-line/three-line TLE catalog parsing and multi-satellite pass prediction
  • Pass duration reporting and browser catalog-pass JSON API
  • Vector, station, observation, and pass data types
  • CLI entry points for TLE validation and timestamp parsing

#Quick start

Parse a timestamp:

moon run cmd/main -- time parse 2026-08-11T12:00:00

Validate a TLE:

moon run cmd/main -- tle validate "1 25544U 98067A 26224.50000000 .00001234 00000-0 12345-4 0 9991" "2 25544 51.6416 21.0000 0001234 123.4567 234.5678 15.50000000"

Propagate a TLE at a UTC instant:

moon run cmd/main -- propagate "<line1>" "<line2>" --at 2026-08-23T12:00:00

Predict SGP4 passes for a ground station:

moon run cmd/main -- passes "<line1>" "<line2>" --station 31.2304,121.4737,10 --from 2026-08-23T12:00:00 --hours 24 --min-elevation 10 --format json

Export a GeoJSON ground track:

moon run cmd/main -- track "<line1>" "<line2>" --from 2026-08-23T12:00:00 --seconds 86400 --step 60

Classify satellite illumination and print a machine-readable report:

moon run cmd/main -- eclipse "<line1>" "<line2>" --at 2026-08-23T12:00:00

Predict pass windows from a validated TLE:

let tle = @moonsatkit.validate_tle(line1, line2).unwrap() let station : @moonsatkit.GroundStation = { latitude_deg: 31.2304, longitude_deg: 121.4737, altitude_m: 10.0, } let passes = @moonsatkit.predict_passes_sgp4( tle, station, from=start, duration_hours=24, minimum_elevation_deg=10.0, ).unwrap()

predict_passes remains the transparent two-body compatibility API.

Parse a named TLE catalog and predict all satellite passes:

let catalog = @moonsatkit.parse_tle_catalog(catalog_text).unwrap() let catalog_passes = @moonsatkit.predict_catalog_passes_sgp4( catalog, station, from=start, duration_hours=24, minimum_elevation_deg=10.0, ).unwrap() let json = @moonsatkit.catalog_pass_windows_to_json(catalog_passes)
predict_passes_sgp4 uses the near-earth SGP4 propagator and returns integer-second AOS, TCA, and LOS timestamps plus the maximum elevation. Deep-space TLEs currently return UnsupportedOrbit until SDP4 is added.

Export results:

let json = @moonsatkit.pass_windows_to_json(passes) let csv = @moonsatkit.pass_windows_to_csv(passes) let track = @moonsatkit.track_geojson_sgp4(tle, start, 86400, 60).unwrap()

#Acceptance scope

The current acceptance baseline covers TLE checksum validation, near-earth SGP4 propagation, WGS-84 coordinate conversion, ground-station observation, one-second-refined pass windows, JSON/CSV/GeoJSON export, and geometric illumination classification. The reproducible checklist is in docs/ACCEPTANCE.md.

#Roadmap

  • Deep-space SDP4 propagation and broader official SGP4 vector coverage
  • Higher-fidelity TEME, Earth orientation, and time-scale refinements
  • Multi-satellite catalogs, Doppler estimates, and constellation coverage

#Development

moon info moon fmt moon test -p oilleelssq-wq/moonsatkit --target all --deny-warn

#License

MoonSatKit is licensed under the Apache License 2.0.

#Browser preview

The repository includes a lightweight browser console at web/index.html. It renders a real MoonSatKit-generated GeoJSON track and SGP4 pass-window sample for Vallado catalog object 00005, with a timeline scrubber, TLE checksum validation, station marker, pass-elevation filter, GeoJSON import, and track download.

Start a local preview from the repository root:

cd web python -m http.server 4173

Then open http://127.0.0.1:4173/. The page loads the self-contained MoonBit JS adapter at web/moonsatkit.js, which exposes TLE validation, SGP4 GeoJSON track generation, single-satellite pass-window prediction, and named catalog pass prediction through moon_sat_catalog_passes_json. Edit the TLE, station, start time, duration, or sample step and press 计算轨迹 to run the real near-earth SGP4 implementation in the browser. The bundled Vallado 00005 data remains available as the initial view and fallback demonstration when the adapter cannot be loaded. Rebuild the adapter after MoonBit changes with ./web/build.ps1 (or run moon build web/wasm --target js --release and copy _build/js/release/build/web/wasm/wasm.js to web/moonsatkit.js).

#
CatalogPassWindow

pub(all) struct CatalogPassWindow {
name : String
catalog_number : Int
window : PassWindow
} derive(Eq,
Debug
)

A pass window associated with one entry in a TLE catalog.

#
EcefState

pub(all) struct EcefState {
epoch : UtcDateTime
position_km : Vector3
velocity_km_s : Vector3
}

A state expressed in the Earth-fixed frame, in kilometres and kilometres per second.

#
EclipseState

pub(all) enum EclipseState {
Sunlit
Penumbra
Umbra
} derive(Eq,
Debug
)

A geometric illumination classification for a satellite state.

Penumbra means the satellite is inside the Earth's partial shadow. The model is intentionally geometric and is suitable for visibility analysis, not for spacecraft power or mission-safety decisions.

#
EclipseState::to_string

fn EclipseState::to_string(self : EclipseState) -> String

#
GeodeticPosition

pub(all) struct GeodeticPosition {
latitude_deg : Double
longitude_deg : Double
altitude_km : Double
}

A WGS-84 geodetic position. Longitude is positive east and altitude is in kilometres.

#
GroundStation

pub(all) struct GroundStation {
latitude_deg : Double
longitude_deg : Double
altitude_m : Double
} derive(Eq,
Debug
)

#
Observation

pub(all) struct Observation {
azimuth_deg : Double
elevation_deg : Double
range_km : Double
range_rate_km_s : Double
above_horizon : Bool
} derive(Eq,
Debug
)

#
OrbitElements

pub(all) struct OrbitElements {
catalog_number : Int
epoch : UtcDateTime
epoch_julian_day : Double
inclination_deg : Double
right_ascension_deg : Double
eccentricity : Double
argument_of_perigee_deg : Double
mean_anomaly_deg : Double
mean_motion_rev_per_day : Double
mean_motion_rad_per_min : Double
}

Numerical orbital elements decoded from a TLE.

The elements retain the TLE epoch and angular fields in conventional units. They are suitable for the two-body preview propagator and form the input boundary for the future SGP4 implementation.

#
OrbitState

pub(all) struct OrbitState {
epoch : UtcDateTime
position_km : Vector3
velocity_km_s : Vector3
}

An inertial state expressed in kilometres and kilometres per second.

#
PassWindow

pub(all) struct PassWindow {
aos : UtcDateTime
tca : UtcDateTime
los : UtcDateTime
maximum_elevation_deg : Double
} derive(Eq,
Debug
)

#
PassWindow::duration_seconds

fn PassWindow::duration_seconds(self : PassWindow) -> Int

Return the visible duration of a pass in integral seconds.

#
SatError

pub(all) struct SatError {
kind : SatErrorKind
line : Int
fragment : String
message : String
} derive(Eq,
Debug
)

#
SatError::new

fn SatError::new(kind : SatErrorKind, message : String, fragment? : String, line? : Int) -> SatError

#
SatError::to_string

fn SatError::to_string(self : SatError) -> String

#
SatErrorKind

pub(all) enum SatErrorKind {
EmptyInput
InvalidFormat
InvalidChecksum
InvalidCatalogNumber
MismatchedCatalogNumber
InvalidTleNumber
UnsupportedOrbit
PropagationError
InvalidDateTime
OutOfRange
} derive(Eq,
Debug
)

#
Sgp4Elements

pub(all) struct Sgp4Elements {
catalog_number : Int
epoch : UtcDateTime
epoch_julian_day : Double
inclination_rad : Double
right_ascension_rad : Double
eccentricity : Double
argument_of_perigee_rad : Double
mean_anomaly_rad : Double
mean_motion_rad_per_min : Double
mean_motion_first_derivative_rev_per_day2 : Double
mean_motion_second_derivative_div2_rev_per_day3 : Double
bstar : Double
}

Numerical TLE fields required to initialize an SGP4 propagator.

Angles are stored in radians. The derivative fields retain the TLE conventions: revolutions per day squared and the second derivative divided by two in revolutions per day cubed. BSTAR is dimensionless.

#
SunPosition

pub(all) struct SunPosition {
position_km : Vector3
distance_km : Double
distance_au : Double
right_ascension_deg : Double
declination_deg : Double
} derive(Eq,
Debug
)

Low-precision geocentric Sun position for visibility and eclipse analysis.

The position is expressed in the same J2000-style inertial frame used by the library's ECI helpers. Distance is in kilometres, right ascension and declination are in degrees, and distance_au is retained for diagnostics.

#
Tle

pub(all) struct Tle {
line1 : String
line2 : String
catalog_number : Int
epoch : String
inclination : String
right_ascension : String
eccentricity : String
argument_of_perigee : String
mean_anomaly : String
mean_motion : String
mean_motion_first_derivative : String
mean_motion_second_derivative : String
bstar : String
checksum_line1_ok : Bool
checksum_line2_ok : Bool
} derive(Eq,
Debug
)

#
TleCatalogEntry

pub(all) struct TleCatalogEntry {
name : String
tle : Tle
} derive(Eq,
Debug
)

A named TLE entry loaded from a two-line or three-line catalog.

#
UtcDateTime

pub(all) struct UtcDateTime {
year : Int
month : Int
day : Int
hour : Int
minute : Int
second : Int
} derive(Compare, Eq,
Debug
)

#
UtcDateTime::add_seconds

fn UtcDateTime::add_seconds(self : UtcDateTime, seconds : Int) -> UtcDateTime

Add an integral number of seconds to a UTC civil timestamp.

This helper is intentionally calendar-based, so pass-search event times remain integer-second UTC values without relying on a platform date API.

#
UtcDateTime::next_second

fn UtcDateTime::next_second(self : UtcDateTime) -> UtcDateTime

#
UtcDateTime::to_string

fn UtcDateTime::to_string(self : UtcDateTime) -> String

#
Vector3

pub(all) struct Vector3 {
x : Double
y : Double
z : Double
} derive(Eq,
Debug
)

#
Vector3::add

fn Vector3::add(self : Vector3, other : Vector3) -> Vector3

#
Vector3::dot

fn Vector3::dot(self : Vector3, other : Vector3) -> Double

#
Vector3::scale

fn Vector3::scale(self : Vector3, factor : Double) -> Vector3

#
Vector3::sub

fn Vector3::sub(self : Vector3, other : Vector3) -> Vector3

#
Vector3::to_string

fn Vector3::to_string(self : Vector3) -> String

#
Vector3::zero

fn Vector3::zero() -> Vector3

#
catalog_pass_windows_to_csv

fn catalog_pass_windows_to_csv(passes : Array[CatalogPassWindow]) -> String

Serialize catalog pass windows as CSV.

#
catalog_pass_windows_to_json

fn catalog_pass_windows_to_json(passes : Array[CatalogPassWindow]) -> String

Serialize catalog pass windows with the satellite name and NORAD number.

#
days_in_month

fn days_in_month(year : Int, month : Int) -> Int

#
ecef_to_eci

fn ecef_to_eci(position : Vector3, instant : UtcDateTime) -> Vector3

Rotate an Earth-fixed position into the inertial frame at the given instant.

#
ecef_to_eci_state

fn ecef_to_eci_state(state : EcefState) -> OrbitState

Convert an Earth-fixed state to an inertial state, including Earth rotation.

#
ecef_to_geodetic

fn ecef_to_geodetic(position : Vector3) -> GeodeticPosition

Convert Earth-fixed Cartesian coordinates to WGS-84 geodetic coordinates.

#
eci_to_ecef

fn eci_to_ecef(position : Vector3, instant : UtcDateTime) -> Vector3

Rotate an ECI position into the Earth-fixed frame at the given instant.

#
eci_to_ecef_state

fn eci_to_ecef_state(state : OrbitState) -> EcefState

Convert an inertial state to an Earth-fixed state, including Earth rotation.

#
eclipse_report_to_json

fn eclipse_report_to_json(tle : Tle, instant : UtcDateTime) -> Result[String, SatError]

Serialize a propagated state together with its illumination status.

#
eclipse_state

fn eclipse_state(state : OrbitState) -> EclipseState

Classify whether a satellite is sunlit, in the penumbra, or in the umbra.

The Earth is modelled as a sphere with WGS-84's equatorial radius. The solar umbra and penumbra are treated as cones whose apexes are determined by the apparent Sun and Earth radii.

#
eclipse_state_sgp4

fn eclipse_state_sgp4(tle : Tle, instant : UtcDateTime) -> Result[EclipseState, SatError]

Propagate a TLE and classify its illumination at the requested instant.

#
geodetic_to_ecef

fn geodetic_to_ecef(value : GeodeticPosition) -> Vector3

Convert a WGS-84 geodetic position to Earth-fixed Cartesian coordinates.

#
gmst_angle

fn gmst_angle(julian_day_value : Double) -> Double

Compute Greenwich mean sidereal time from an astronomical Julian Day.

The result is a normalized angle in radians in the range [0, 2*pi).

#
greenwich_mean_sidereal_time

fn greenwich_mean_sidereal_time(value : UtcDateTime) -> Double

Compute Greenwich mean sidereal time for a UTC timestamp.

#
ground_station_to_ecef

fn ground_station_to_ecef(station : GroundStation) -> Vector3

Convert a ground station height in metres to Earth-fixed Cartesian coordinates.

#
is_leap_year

fn is_leap_year(year : Int) -> Bool

#
julian_day

fn julian_day(value : UtcDateTime) -> Double

Convert a UTC civil timestamp to the astronomical Julian Day.

The result follows the standard convention where 2000-01-01 12:00:00 UTC is 2451545.0. This helper uses the proleptic Gregorian calendar.

#
observe

fn observe(state : OrbitState, station : GroundStation) -> Observation

Compute an observation from an inertial state by applying the ECI/ECEF transformation at the state's epoch first.

#
observe_ecef

fn observe_ecef(state : EcefState, station : GroundStation) -> Observation

Compute the local ENU observation of an Earth-fixed satellite state.

Azimuth is measured clockwise from north in degrees. Elevation is measured above the local horizon. Range and range rate use kilometres and kilometres per second, respectively.

#
orbit_elements

fn orbit_elements(tle : Tle) -> Result[OrbitElements, SatError]

Decode the numerical orbital elements represented by a parsed TLE.

#
orbit_state_geodetic

fn orbit_state_geodetic(state : OrbitState) -> GeodeticPosition

Convert an inertial state position to WGS-84 geodetic coordinates.

#
orbit_state_to_json

fn orbit_state_to_json(state : OrbitState) -> String

Serialize an inertial orbit state as a small JSON object.

#
parse_tle

fn parse_tle(line1 : String, line2 : String) -> Result[Tle, SatError]

Parse a two-line element set and preserve the raw orbital fields.

#
parse_tle_catalog

fn parse_tle_catalog(text : String) -> Result[Array[TleCatalogEntry], SatError]

Parse a catalog containing either repeated two-line TLE records or the common three-line format with a satellite name before each pair.

Blank lines are ignored. Named records keep their display name; unnamed records receive a NORAD name.

#
parse_utc_datetime

fn parse_utc_datetime(text : String) -> Result[UtcDateTime, SatError]

#
pass_windows_to_csv

fn pass_windows_to_csv(passes : Array[PassWindow]) -> String

Serialize pass windows as a UTF-8 CSV document with a header row.

#
pass_windows_to_json

fn pass_windows_to_json(passes : Array[PassWindow]) -> String

Serialize pass windows as a JSON array.

#
predict_catalog_passes_sgp4

fn predict_catalog_passes_sgp4(entries : Array[TleCatalogEntry], station : GroundStation, from : UtcDateTime, duration_hours : Int, minimum_elevation_deg : Double) -> Result[Array[CatalogPassWindow], SatError]

Predict SGP4 pass windows for every entry in a named TLE catalog.

#
predict_passes

fn predict_passes(tle : Tle, station : GroundStation, from : UtcDateTime, duration_hours : Int, minimum_elevation_deg : Double) -> Result[Array[PassWindow], SatError]

Predict visibility windows using the current two-body preview propagator.

The returned event times have one-second resolution. AOS and LOS are refined by elevation-threshold bisection, while TCA is selected by a one-second search around the sampled elevation maximum.

#
predict_passes_from_elements

fn predict_passes_from_elements(elements : OrbitElements, station : GroundStation, from : UtcDateTime, duration_hours : Int, minimum_elevation_deg : Double) -> Result[Array[PassWindow], SatError]

Predict visibility windows from already-decoded orbital elements.

#
predict_passes_sgp4

fn predict_passes_sgp4(tle : Tle, station : GroundStation, from : UtcDateTime, duration_hours : Int, minimum_elevation_deg : Double) -> Result[Array[PassWindow], SatError]

Predict visibility windows with the near-earth SGP4 propagator.

This API is the high-fidelity counterpart to predict_passes, which keeps its transparent two-body preview behavior for compatibility. A deep-space TLE returns UnsupportedOrbit because SDP4 is not implemented yet.

#
propagate_sgp4

fn propagate_sgp4(tle : Tle, instant : UtcDateTime) -> Result[OrbitState, SatError]

Propagate a near-earth TLE with the Vallado SGP4 model.

The returned state uses kilometres and kilometres per second. Deep-space orbits return UnsupportedOrbit until SDP4 is added.

#
propagate_two_body

fn propagate_two_body(elements : OrbitElements, instant : UtcDateTime) -> Result[OrbitState, SatError]

Propagate an orbit with a two-body Kepler approximation.

This is an intentionally transparent preview model. It does not include the perturbations and Earth orientation terms required for SGP4 accuracy.

#
seconds_between

fn seconds_between(start : UtcDateTime, end : UtcDateTime) -> Double

Return the elapsed seconds from start to end.

#
sgp4_elements

fn sgp4_elements(tle : Tle) -> Result[Sgp4Elements, SatError]

Decode the complete numerical TLE input boundary for a future SGP4 propagator. This function does not propagate a state.

#
sun_position_eci

fn sun_position_eci(instant : UtcDateTime) -> SunPosition

Compute a low-precision geocentric Sun position.

This follows the compact NOAA-style solar position equations. It is accurate enough for eclipse classification and educational visualisation; high-precision ephemerides remain outside the current scope.

#
track_geojson_sgp4

fn track_geojson_sgp4(tle : Tle, from : UtcDateTime, duration_seconds : Int, step_seconds : Int) -> Result[String, SatError]

Export a near-earth SGP4 ground track as a GeoJSON LineString.

Coordinates use the GeoJSON order [longitude, latitude, altitude_km]. The interval is sampled inclusively, so a zero-duration track contains one coordinate. Deep-space TLEs return UnsupportedOrbit.

#
validate_tle

fn validate_tle(line1 : String, line2 : String) -> Result[Tle, SatError]

Parse and require both TLE checksum digits to be valid.

#
vallado_00005_line1

let vallado_00005_line1 : String

Public SGP4 verification sample distributed with Vallado's CelesTrak test material. MoonSatKit currently uses it for TLE parsing and checksum tests; propagation checks can reuse the same fixture once SGP4 state vectors land.

#
vallado_00005_line2

let vallado_00005_line2 : String