···5566(** Satellite state management for globe rendering.
7788- Caches precomputed Kepler elements, generates ghost orbits and per-frame
99- trail positions. Pure OCaml, no WebGL dependency. *)
88+ Propagation-agnostic: takes a [propagator] function that maps dt → position.
99+ The default constructor uses Kepler; [of_propagator] accepts any backend. *)
1010+1111+type propagator = dt:float -> Kepler.Vec3.t
10121113type t = {
1212- elements : Kepler.Analytic.elements;
1313- pos : Kepler.Vec3.t;
1414- vel : Kepler.Vec3.t;
1414+ propagate : propagator;
1515 color : Color.t;
1616 period : float;
1717 epoch_unix : float;
1818 ghost_points : Math.Vec3.t option array;
1919 trail_length : int;
2020+ (* Kepler-specific, optional *)
2121+ elements : Kepler.Analytic.elements option;
2222+ pos0 : Kepler.Vec3.t;
2323+ vel0 : Kepler.Vec3.t;
2024}
21252626+(* ── Safe GL coordinate conversion ─────────────────────────────────── *)
2727+2828+let safe_gl (fallback : Math.Vec3.t) (p : Kepler.Vec3.t) =
2929+ if Float.is_finite p.x && Float.is_finite p.y && Float.is_finite p.z then
3030+ Gl_coord.of_kepler p
3131+ else fallback
3232+3333+let make_ghost propagate ~period ~ghost_duration ~fallback =
3434+ let n = 120 in
3535+ let dur =
3636+ if Float.is_finite ghost_duration && ghost_duration > 0. then ghost_duration
3737+ else if Float.is_finite period && period > 0. then period
3838+ else 5400.
3939+ in
4040+ Array.init n (fun i ->
4141+ let t = Float.of_int i *. dur /. Float.of_int n in
4242+ Some (safe_gl fallback (propagate ~dt:t)))
4343+4444+(* ── Constructors ──────────────────────────────────────────────────── *)
4545+2246let v ~pos ~vel ~color ?(epoch_unix = 0.) ?(trail_length = 50) () =
2347 let elements = Kepler.Analytic.precompute ~pos ~vel in
2448 let period = Kepler.Analytic.period elements in
4949+ let propagate ~dt = Kepler.Analytic.at_precomputed elements ~dt in
5050+ let fallback = Gl_coord.of_kepler pos in
2551 let ghost_points =
2626- let n = 120 in
2727- (* Use period for bound orbits, 5400s arc for unbound *)
2828- let dur = if Float.is_finite period && period > 0. then period else 5400. in
2929- let fallback = Gl_coord.of_kepler pos in
3030- Array.init n (fun i ->
3131- let t = Float.of_int i *. dur /. Float.of_int n in
3232- let p = Kepler.Analytic.at_precomputed elements ~dt:t in
3333- if Float.is_finite p.x && Float.is_finite p.y && Float.is_finite p.z
3434- then Some (Gl_coord.of_kepler p)
3535- else Some fallback)
5252+ make_ghost propagate ~period ~ghost_duration:period ~fallback
3653 in
3737- { elements; pos; vel; color; period; epoch_unix; ghost_points; trail_length }
5454+ {
5555+ propagate;
5656+ color;
5757+ period;
5858+ epoch_unix;
5959+ ghost_points;
6060+ trail_length;
6161+ elements = Some elements;
6262+ pos0 = pos;
6363+ vel0 = vel;
6464+ }
6565+6666+let of_propagator ~propagate ~color ?(epoch_unix = 0.) ?(period = 5400.)
6767+ ?(ghost_duration = 0.) ?(trail_length = 50) () =
6868+ let ghost_dur = if ghost_duration > 0. then ghost_duration else period in
6969+ let fallback = safe_gl Math.Vec3.zero (propagate ~dt:0.) in
7070+ let ghost_points =
7171+ make_ghost propagate ~period ~ghost_duration:ghost_dur ~fallback
7272+ in
7373+ {
7474+ propagate;
7575+ color;
7676+ period;
7777+ epoch_unix;
7878+ ghost_points;
7979+ trail_length;
8080+ elements = None;
8181+ pos0 = Kepler.Vec3.zero;
8282+ vel0 = Kepler.Vec3.zero;
8383+ }
8484+8585+(* ── Accessors ─────────────────────────────────────────────────────── *)
38863987let color t = t.color
4088let period t = t.period
4189let epoch_unix t = t.epoch_unix
4290let ghost_points t = t.ghost_points
4343-let epoch_position t = Gl_coord.of_astro ~x:t.pos.x ~y:t.pos.y ~z:t.pos.z
9191+9292+(* ── Rendering ─────────────────────────────────────────────────────── *)
44934594let position_at t ~dt =
4646- let p = Kepler.Analytic.at_precomputed t.elements ~dt in
4747- if Float.is_finite p.x && Float.is_finite p.y && Float.is_finite p.z then
4848- Gl_coord.of_kepler p
4949- else epoch_position t
9595+ let fallback = Gl_coord.of_kepler t.pos0 in
9696+ safe_gl fallback (t.propagate ~dt)
50975198let trail_positions t ~dt =
5299 let n = t.trail_length in
5353- (* For unbound orbits, use 30s steps; for bound, 1/150 of period *)
54100 let step =
55101 if Float.is_finite t.period && t.period > 0. then
56102 t.period /. Float.of_int (n * 3)
57103 else 30.
58104 in
5959- let fallback = epoch_position t in
105105+ let fallback = Gl_coord.of_kepler t.pos0 in
60106 Array.init n (fun i ->
61107 let t_offset = dt -. (Float.of_int (n - 1 - i) *. step) in
6262- let p = Kepler.Analytic.at_precomputed t.elements ~dt:t_offset in
6363- if Float.is_finite p.x && Float.is_finite p.y && Float.is_finite p.z then
6464- Some (Gl_coord.of_kepler p)
6565- else Some fallback)
108108+ Some (safe_gl fallback (t.propagate ~dt:t_offset)))
109109+110110+let dot t ~dt = (position_at t ~dt, t.color)
661116767-let dot t ~dt =
6868- let pos = position_at t ~dt in
6969- (pos, t.color)
112112+(* ── Orbital elements (Kepler only) ───────────────────────────────── *)
701137171-let eccentricity t = Kepler.Analytic.eccentricity t.elements
114114+let eccentricity t =
115115+ match t.elements with
116116+ | Some el -> Kepler.Analytic.eccentricity el
117117+ | None -> 0.
7211873119let inclination t =
7474- (* i = acos(h_z / |h|) where h = pos x vel *)
7575- let hx = (t.pos.y *. t.vel.z) -. (t.pos.z *. t.vel.y) in
7676- let hy = (t.pos.z *. t.vel.x) -. (t.pos.x *. t.vel.z) in
7777- let hz = (t.pos.x *. t.vel.y) -. (t.pos.y *. t.vel.x) in
120120+ let hx = (t.pos0.y *. t.vel0.z) -. (t.pos0.z *. t.vel0.y) in
121121+ let hy = (t.pos0.z *. t.vel0.x) -. (t.pos0.x *. t.vel0.z) in
122122+ let hz = (t.pos0.x *. t.vel0.y) -. (t.pos0.y *. t.vel0.x) in
78123 let h_mag = sqrt ((hx *. hx) +. (hy *. hy) +. (hz *. hz)) in
79124 if h_mag < 1e-12 then 0.
80125 else acos (Float.max (-1.) (Float.min 1. (hz /. h_mag)))
8112682127let pp ppf t =
8383- Fmt.pf ppf "sat(period=%.0fs e=%.4f color=%a)" t.period
8484- (Kepler.Analytic.eccentricity t.elements)
128128+ Fmt.pf ppf "sat(period=%.0fs e=%.4f color=%a)" t.period (eccentricity t)
85129 Color.pp t.color
+69-22
lib/satellite.mli
···11(** Satellite state management for globe rendering.
2233- Caches precomputed Kepler elements, generates ghost orbits and per-frame
44- trail positions. Pure OCaml, no WebGL dependency. *)
33+ Supports multiple propagation backends: Kepler (two-body), SGP4 (TLE), or
44+ ephemeris interpolation (OEM). The propagation is abstracted behind a
55+ [propagator] function — the rendering code doesn't know which method is
66+ used.
77+88+ {2 Propagation backends}
99+1010+ {b Kepler} (default): Fast two-body analytical propagation from a J2000
1111+ state vector. Accurate within ±1 orbit of epoch. Use for CDM visualization.
1212+1313+ {b SGP4}: Standard TLE propagation with J2/drag. Accurate for days. Pass an
1414+ SGP4 propagator via {!of_propagator}.
1515+1616+ {b OEM}: Interpolate time-tagged ephemeris. Exact at provider precision.
1717+ Pass an interpolation function via {!of_propagator}.
1818+1919+ {2 Quick start}
2020+2121+ {[
2222+ (* From CDM state vector (Kepler): *)
2323+ let sat = Satellite.v ~pos ~vel ~color ~epoch_unix ()
2424+2525+ (* From custom propagator (SGP4, OEM, etc.): *)
2626+ let sat =
2727+ Satellite.of_propagator ~propagate ~color ~epoch_unix ~period
2828+ ~ghost_duration ()
2929+ ]} *)
530631type t
77-(** Satellite state with cached orbital elements and ghost orbit. *)
3232+(** Satellite state with cached ghost orbit. *)
3333+3434+(** {1 Propagator type} *)
3535+3636+type propagator = dt:float -> Kepler.Vec3.t
3737+(** A propagation function: given [dt] seconds from epoch, returns the J2000
3838+ position in km. This is the abstraction that allows swapping Kepler, SGP4,
3939+ or OEM interpolation. *)
4040+4141+(** {1 Constructors} *)
842943val v :
1044 pos:Kepler.Vec3.t ->
···1448 ?trail_length:int ->
1549 unit ->
1650 t
1717-(** [v ~pos ~vel ~color ?epoch_unix ?trail_length ()] creates a satellite from a
1818- J2000 state vector. [epoch_unix] is the Unix timestamp of the state vector
1919- (default 0). Precomputes orbital elements and ghost orbit. *)
5151+(** [v ~pos ~vel ~color ?epoch_unix ?trail_length ()] creates a satellite using
5252+ Kepler (two-body) propagation from a J2000 state vector. *)
5353+5454+val of_propagator :
5555+ propagate:propagator ->
5656+ color:Color.t ->
5757+ ?epoch_unix:float ->
5858+ ?period:float ->
5959+ ?ghost_duration:float ->
6060+ ?trail_length:int ->
6161+ unit ->
6262+ t
6363+(** [of_propagator ~propagate ~color ?epoch_unix ?period ?ghost_duration
6464+ ?trail_length ()] creates a satellite from any propagation backend.
6565+6666+ - [propagate]: position at dt seconds from epoch (J2000 km)
6767+ - [period]: orbital period in seconds (for ghost orbit and trail spacing;
6868+ default 5400s)
6969+ - [ghost_duration]: duration of ghost orbit arc in seconds (default: period)
7070+ - [trail_length]: number of trail points (default 50) *)
7171+7272+(** {1 Accessors} *)
20732174val color : t -> Color.t
2222-(** [color t] is the satellite's display color. *)
2323-2475val epoch_unix : t -> float
2525-(** [epoch_unix t] is the Unix timestamp of the state vector epoch. *)
7676+val period : t -> float
26772727-val period : t -> float
2828-(** [period t] is the orbital period in seconds. *)
7878+(** {1 Rendering} *)
29793080val ghost_points : t -> Math.Vec3.t option array
3131-(** [ghost_points t] is the precomputed full-orbit ghost (GL coords). Computed
3232- once at creation, reusable across frames. *)
8181+(** Precomputed ghost orbit (GL coords). Computed once at creation. *)
33823483val position_at : t -> dt:float -> Math.Vec3.t
3535-(** [position_at t ~dt] is the satellite's GL position at [dt] seconds from
3636- epoch. Very fast (~20 FLOPs). *)
8484+(** GL position at [dt] seconds from epoch. *)
37853886val trail_positions : t -> dt:float -> Math.Vec3.t option array
3939-(** [trail_positions t ~dt] generates trail points up to [dt], fading into the
4040- past. For use with {!Globe_webgl.Orbit.add_trail}. *)
8787+(** Trail points for {!Globe_webgl.Orbit.add_trail}. *)
41884289val dot : t -> dt:float -> Math.Vec3.t * Color.t
4343-(** [dot t ~dt] is the satellite's current position and color, for building
4444- {!Globe_webgl.Orbit.dot} values. *)
9090+(** Current position + color for dot rendering. *)
9191+9292+(** {1 Orbital elements (Kepler only)} *)
45934694val eccentricity : t -> float
4747-(** [eccentricity t] is the orbital eccentricity. *)
9595+(** Eccentricity. Returns [0.] for non-Kepler propagators. *)
48964997val inclination : t -> float
5050-(** [inclination t] is the orbital inclination in radians. *)
9898+(** Inclination in radians. Returns [0.] for non-Kepler propagators. *)
519952100val pp : t Fmt.t
5353-(** [pp] pretty-prints a satellite (period, eccentricity, color). *)