···1212 epoch_unix : float;
1313 ghost_points : Math.Vec3.t option array;
1414 trail_length : int;
1515+ pos : Kepler.Vec3.t;
1616+ vel : Kepler.Vec3.t;
1517 elements : Kepler.Analytic.elements option;
1616- pos0 : Kepler.Vec3.t;
1717- vel0 : Kepler.Vec3.t;
1818}
19192020(* ── Helpers ───────────────────────────────────────────────────────── *)
21212222-let safe_gl (fallback : Math.Vec3.t) (p : Kepler.Vec3.t) =
2222+let safe_gl fallback (p : Kepler.Vec3.t) =
2323 if Float.is_finite p.x && Float.is_finite p.y && Float.is_finite p.z then
2424 Gl_coord.of_kepler p
2525 else fallback
26262727-let make_ghost propagate ~dur ~fallback =
2828- let n = 120 in
2929- Array.init n (fun i ->
3030- let t = Float.of_int i *. dur /. Float.of_int n in
3131- Some (safe_gl fallback (propagate ~dt:t)))
3232-3327(* ── Propagator builders ───────────────────────────────────────────── *)
34283529let kepler ~pos ~vel =
···38323933(* ── Constructor ───────────────────────────────────────────────────── *)
40344141-let v ?propagate ?pos ?vel ~color ?(epoch_unix = 0.) ?(period = 0.)
4242- ?(ghost_duration = 0.) ?(trail_length = 50) () =
4343- let propagate, elements, pos0, vel0, auto_period =
4444- match (propagate, pos, vel) with
4545- | Some p, _, _ ->
4646- let pos0 = match pos with Some p -> p | None -> Kepler.Vec3.zero in
4747- let vel0 = match vel with Some v -> v | None -> Kepler.Vec3.zero in
4848- (p, None, pos0, vel0, 5400.)
4949- | None, Some pos, Some vel ->
5050- let el = Kepler.Analytic.precompute ~pos ~vel in
5151- let p = Kepler.Analytic.period el in
5252- let prop ~dt = Kepler.Analytic.at_precomputed el ~dt in
5353- (prop, Some el, pos, vel, p)
5454- | None, _, _ ->
5555- let pos0 = match pos with Some p -> p | None -> Kepler.Vec3.zero in
5656- ((fun ~dt:_ -> pos0), None, pos0, Kepler.Vec3.zero, 5400.)
3535+let v ~pos ~vel ~color ?propagate ?(epoch_unix = 0.) ?(period = 0.)
3636+ ?(trail_length = 50) () =
3737+ let is_kepler = propagate = None in
3838+ let elements =
3939+ if is_kepler then Some (Kepler.Analytic.precompute ~pos ~vel) else None
4040+ in
4141+ let auto_period =
4242+ match elements with Some el -> Kepler.Analytic.period el | None -> 5400.
4343+ in
4444+ let propagate =
4545+ match propagate with Some p -> p | None -> kepler ~pos ~vel
5746 in
5847 let period =
5948 if period > 0. then period
6049 else if Float.is_finite auto_period && auto_period > 0. then auto_period
6150 else 5400.
6251 in
6363- let ghost_dur = if ghost_duration > 0. then ghost_duration else period in
6464- let fallback = Gl_coord.of_kepler pos0 in
6565- let ghost_points = make_ghost propagate ~dur:ghost_dur ~fallback in
5252+ let fallback = Gl_coord.of_kepler pos in
5353+ let ghost_points =
5454+ let n = 120 in
5555+ Array.init n (fun i ->
5656+ let t = Float.of_int i *. period /. Float.of_int n in
5757+ Some (safe_gl fallback (propagate ~dt:t)))
5858+ in
6659 {
6760 propagate;
6861 color;
···7063 epoch_unix;
7164 ghost_points;
7265 trail_length;
6666+ pos;
6767+ vel;
7368 elements;
7474- pos0;
7575- vel0;
7669 }
77707871(* ── Accessors ─────────────────────────────────────────────────────── *)
···8073let color t = t.color
8174let period t = t.period
8275let epoch_unix t = t.epoch_unix
7676+let pos t = t.pos
7777+let vel t = t.vel
8378let ghost_points t = t.ghost_points
84798580(* ── Rendering ─────────────────────────────────────────────────────── *)
86818787-let position_at t ~dt = safe_gl (Gl_coord.of_kepler t.pos0) (t.propagate ~dt)
8282+let position_at t ~dt = safe_gl (Gl_coord.of_kepler t.pos) (t.propagate ~dt)
88838984let trail_positions t ~dt =
9085 let n = t.trail_length in
···9388 t.period /. Float.of_int (n * 3)
9489 else 30.
9590 in
9696- let fallback = Gl_coord.of_kepler t.pos0 in
9191+ let fallback = Gl_coord.of_kepler t.pos in
9792 Array.init n (fun i ->
9893 let t_offset = dt -. (Float.of_int (n - 1 - i) *. step) in
9994 Some (safe_gl fallback (t.propagate ~dt:t_offset)))
···105100let eccentricity t =
106101 match t.elements with
107102 | Some el -> Kepler.Analytic.eccentricity el
108108- | None -> 0.
103103+ | None ->
104104+ let el = Kepler.Analytic.precompute ~pos:t.pos ~vel:t.vel in
105105+ Kepler.Analytic.eccentricity el
109106110107let inclination t =
111111- let p = t.pos0 and v = t.vel0 in
108108+ let p = t.pos and v = t.vel in
112109 let hx = (p.y *. v.z) -. (p.z *. v.y) in
113110 let hy = (p.z *. v.x) -. (p.x *. v.z) in
114111 let hz = (p.x *. v.y) -. (p.y *. v.x) in
+15-48
lib/satellite.mli
···11(** Satellite state management for globe rendering.
2233- Supports multiple propagation backends via the optional [~propagate]
44- parameter. Default: Kepler two-body from [~pos] and [~vel].
55-63 {[
77- (* Kepler (default — needs pos + vel): *)
44+ (* Kepler propagation (default): *)
85 Satellite.v ~pos ~vel ~color ~epoch_unix ()
96 (* SGP4 from TLE: *)
1010- Satellite.v
1111- ~propagate:(Satellite.sgp4 tle state)
1212- ~color ~epoch_unix ~period ()
1313- (* Custom propagator: *)
1414- Satellite.v
1515- ~propagate:(fun ~dt -> my_interp dt)
1616- ~color ~epoch_unix ~period ()
77+ Satellite.v ~pos ~vel ~propagate:my_sgp4 ~color ~epoch_unix ~period ()
88+ (* OEM interpolation: *)
99+ Satellite.v ~pos ~vel ~propagate:my_oem ~color ~epoch_unix ~period ()
1710 ]} *)
18111912type t
···2518(** {1 Propagator builders} *)
26192720val kepler : pos:Kepler.Vec3.t -> vel:Kepler.Vec3.t -> propagator
2828-(** [kepler ~pos ~vel] builds a two-body Kepler propagator. Uses
2929- {!Kepler.Analytic.precompute} internally — fast, ~20 FLOPs per call.
3030- Accurate within ±1 orbit of epoch. *)
2121+(** Two-body Kepler. ~20 FLOPs/call. Accurate ±1 orbit. *)
31223223(** {1 Constructor} *)
33243425val v :
3535- ?propagate:propagator ->
3636- ?pos:Kepler.Vec3.t ->
3737- ?vel:Kepler.Vec3.t ->
2626+ pos:Kepler.Vec3.t ->
2727+ vel:Kepler.Vec3.t ->
3828 color:Color.t ->
2929+ ?propagate:propagator ->
3930 ?epoch_unix:float ->
4031 ?period:float ->
4141- ?ghost_duration:float ->
4232 ?trail_length:int ->
4333 unit ->
4434 t
4545-(** [v ~color ()] creates a satellite.
3535+(** [v ~pos ~vel ~color ()] creates a satellite.
46364747- Propagation is chosen by:
4848- - [~propagate:p] — use propagator [p] (from {!kepler}, {!sgp4}, or custom)
4949- - [~pos] + [~vel] without [~propagate] — auto-builds Kepler propagator
5050- - [~propagate] without [~pos]/[~vel] — propagator-only (no orbital elements)
5151-5252- Optional parameters:
5353- - [period]: orbital period in seconds (auto-detected for Kepler/SGP4)
5454- - [ghost_duration]: ghost orbit arc in seconds (default: [period])
5555- - [trail_length]: number of trail points (default 50) *)
3737+ [pos] and [vel] define the J2000 state vector at epoch. [propagate]
3838+ overrides the propagation method (default: {!kepler}). [period] is
3939+ auto-detected for Kepler; required for custom propagators. *)
56405741(** {1 Accessors} *)
58425943val color : t -> Color.t
6060-(** Satellite color. *)
6161-6244val epoch_unix : t -> float
6363-(** Unix timestamp of the propagation epoch. *)
6464-6545val period : t -> float
6666-(** Orbital period in seconds. *)
4646+val pos : t -> Kepler.Vec3.t
4747+val vel : t -> Kepler.Vec3.t
67486849(** {1 Rendering} *)
69507051val ghost_points : t -> Math.Vec3.t option array
7171-(** Precomputed ghost orbit (GL coords). Computed once at creation. *)
7272-7352val position_at : t -> dt:float -> Math.Vec3.t
7474-(** [position_at t ~dt] returns the GL position at [dt] seconds from epoch. *)
7575-7653val trail_positions : t -> dt:float -> Math.Vec3.t option array
7777-(** Trail points for orbit rendering. *)
7878-7954val dot : t -> dt:float -> Math.Vec3.t * Color.t
8080-(** Current position and color for dot rendering. *)
81558282-(** {1 Orbital elements (Kepler only, 0. otherwise)} *)
5656+(** {1 Orbital elements} *)
83578458val eccentricity : t -> float
8585-(** [eccentricity t] returns the orbital eccentricity. Returns [0.] for
8686- non-Kepler propagators. *)
8787-8859val inclination : t -> float
8989-(** [inclination t] returns the orbital inclination in radians. Returns [0.] for
9090- non-Kepler propagators. *)
9191-9260val pp : t Fmt.t
9393-(** Pretty-print a satellite. *)