···1414 trail_length : int;
1515 pos : Kepler.Vec3.t;
1616 vel : Kepler.Vec3.t;
1717- elements : Kepler.Analytic.elements option;
1817}
19182019(* ── Helpers ───────────────────────────────────────────────────────── *)
···2423 Gl_coord.of_kepler p
2524 else fallback
26252727-(* ── Propagator builders ───────────────────────────────────────────── *)
2626+let make_ghost propagate ~period ~fallback =
2727+ let n = 120 in
2828+ let dur = if Float.is_finite period && period > 0. then period else 5400. 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)))
28322929-let kepler ~pos ~vel =
3030- let el = Kepler.Analytic.precompute ~pos ~vel in
3131- fun ~dt -> Kepler.Analytic.at_precomputed el ~dt
3333+let make_trail propagate ~period ~trail_length ~fallback ~dt =
3434+ let n = trail_length in
3535+ let step =
3636+ if Float.is_finite period && period > 0. then period /. Float.of_int (n * 3)
3737+ else 30.
3838+ in
3939+ Array.init n (fun i ->
4040+ let t_offset = dt -. (Float.of_int (n - 1 - i) *. step) in
4141+ Some (safe_gl fallback (propagate ~dt:t_offset)))
32423333-(* ── Constructor ───────────────────────────────────────────────────── *)
4343+let make ~propagate ~color ~epoch_unix ~period ~trail_length ~pos ~vel =
4444+ let fallback = Gl_coord.of_kepler pos in
4545+ let ghost_points = make_ghost propagate ~period ~fallback in
4646+ { propagate; color; period; epoch_unix; ghost_points; trail_length; pos; vel }
34473535-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
4646- in
4848+(* ── Constructors ──────────────────────────────────────────────────── *)
4949+5050+let of_state_vector ~pos ~vel ~color ?(epoch_unix = 0.) ?(trail_length = 50) ()
5151+ =
5252+ let el = Kepler.Analytic.precompute ~pos ~vel in
5353+ let period = Kepler.Analytic.period el in
4754 let period =
4848- if period > 0. then period
4949- else if Float.is_finite auto_period && auto_period > 0. then auto_period
5050- else 5400.
5555+ if Float.is_finite period && period > 0. then period else 5400.
5156 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)))
5757+ let propagate ~dt = Kepler.Analytic.at_precomputed el ~dt in
5858+ make ~propagate ~color ~epoch_unix ~period ~trail_length ~pos ~vel
5959+6060+let of_tle ~tle ~state ~color ?(trail_length = 50) () =
6161+ let epoch_unix = Sgp4.epoch_unix tle in
6262+ let period = 2. *. Float.pi /. tle.no *. 60. in
6363+ (* no is rad/min *)
6464+ (* Extract pos/vel at epoch *)
6565+ let pos, vel =
6666+ match Sgp4.propagate state tle 0. with
6767+ | Ok (p, v) -> (Kepler.Vec3.v p.x p.y p.z, Kepler.Vec3.v v.vx v.vy v.vz)
6868+ | Error _ -> (Kepler.Vec3.zero, Kepler.Vec3.zero)
5869 in
5959- {
6060- propagate;
6161- color;
6262- period;
6363- epoch_unix;
6464- ghost_points;
6565- trail_length;
6666- pos;
6767- vel;
6868- elements;
6969- }
7070+ let propagate ~dt =
7171+ let tsince = dt /. 60. in
7272+ match Sgp4.propagate state tle tsince with
7373+ | Ok (p, _v) -> Kepler.Vec3.v p.x p.y p.z
7474+ | Error _ -> pos (* fallback to epoch position *)
7575+ in
7676+ make ~propagate ~color ~epoch_unix ~period ~trail_length ~pos ~vel
7777+7878+let of_ephemeris ~points ~color ?(trail_length = 50) () =
7979+ let n = Array.length points in
8080+ if n = 0 then
8181+ make
8282+ ~propagate:(fun ~dt:_ -> Kepler.Vec3.zero)
8383+ ~color ~epoch_unix:0. ~period:5400. ~trail_length ~pos:Kepler.Vec3.zero
8484+ ~vel:Kepler.Vec3.zero
8585+ else
8686+ let mid = n / 2 in
8787+ let epoch_unix, pos = points.(mid) in
8888+ (* Estimate velocity from neighboring points *)
8989+ let vel =
9090+ if n >= 2 then
9191+ let i0 = max 0 (mid - 1) and i1 = min (n - 1) (mid + 1) in
9292+ let t0, (p0 : Kepler.Vec3.t) = points.(i0)
9393+ and t1, (p1 : Kepler.Vec3.t) = points.(i1) in
9494+ let dt = t1 -. t0 in
9595+ if dt > 0. then
9696+ Kepler.Vec3.v
9797+ ((p1.x -. p0.x) /. dt)
9898+ ((p1.y -. p0.y) /. dt)
9999+ ((p1.z -. p0.z) /. dt)
100100+ else Kepler.Vec3.zero
101101+ else Kepler.Vec3.zero
102102+ in
103103+ (* Estimate period from data span *)
104104+ let t_start = fst points.(0) and t_end = fst points.(n - 1) in
105105+ let span = t_end -. t_start in
106106+ let period = if span > 0. then span else 5400. in
107107+ (* Linear interpolation *)
108108+ let propagate ~dt =
109109+ let target = epoch_unix +. dt in
110110+ (* Binary search for bracket *)
111111+ let rec find lo hi =
112112+ if lo >= hi - 1 then lo
113113+ else
114114+ let mid = (lo + hi) / 2 in
115115+ if fst points.(mid) <= target then find mid hi else find lo mid
116116+ in
117117+ let i = find 0 (n - 1) in
118118+ let t0, p0 = points.(i) in
119119+ if i >= n - 1 then p0
120120+ else
121121+ let t1, p1 = points.(i + 1) in
122122+ let dt = t1 -. t0 in
123123+ if dt < 1e-10 then p0
124124+ else
125125+ let frac = (target -. t0) /. dt in
126126+ let frac = Float.max 0. (Float.min 1. frac) in
127127+ Kepler.Vec3.v
128128+ (p0.x +. (frac *. (p1.x -. p0.x)))
129129+ (p0.y +. (frac *. (p1.y -. p0.y)))
130130+ (p0.z +. (frac *. (p1.z -. p0.z)))
131131+ in
132132+ make ~propagate ~color ~epoch_unix ~period ~trail_length ~pos ~vel
7013371134(* ── Accessors ─────────────────────────────────────────────────────── *)
72135···82145let position_at t ~dt = safe_gl (Gl_coord.of_kepler t.pos) (t.propagate ~dt)
8314684147let trail_positions t ~dt =
8585- let n = t.trail_length in
8686- let step =
8787- if Float.is_finite t.period && t.period > 0. then
8888- t.period /. Float.of_int (n * 3)
8989- else 30.
9090- in
9191- let fallback = Gl_coord.of_kepler t.pos in
9292- Array.init n (fun i ->
9393- let t_offset = dt -. (Float.of_int (n - 1 - i) *. step) in
9494- Some (safe_gl fallback (t.propagate ~dt:t_offset)))
148148+ make_trail t.propagate ~period:t.period ~trail_length:t.trail_length
149149+ ~fallback:(Gl_coord.of_kepler t.pos) ~dt
9515096151let dot t ~dt = (position_at t ~dt, t.color)
9715298153(* ── Orbital elements ──────────────────────────────────────────────── *)
99154100155let eccentricity t =
101101- match t.elements with
102102- | Some el -> Kepler.Analytic.eccentricity el
103103- | None ->
104104- let el = Kepler.Analytic.precompute ~pos:t.pos ~vel:t.vel in
105105- Kepler.Analytic.eccentricity el
156156+ let el = Kepler.Analytic.precompute ~pos:t.pos ~vel:t.vel in
157157+ Kepler.Analytic.eccentricity el
106158107159let inclination t =
108160 let p = t.pos and v = t.vel in
+31-22
lib/satellite.mli
···11(** Satellite state management for globe rendering.
2233+ Three constructors for different data sources:
44+35 {[
44- (* Kepler propagation (default): *)
55- Satellite.v ~pos ~vel ~color ~epoch_unix ()
66- (* SGP4 from TLE: *)
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 ()
66+ (* From CDM state vector (Kepler propagation): *)
77+ Satellite.of_state_vector ~pos ~vel ~color ~epoch_unix ()
88+ (* From TLE (SGP4 propagation): *)
99+ Satellite.of_tle ~tle ~state ~color ()
1010+ (* From ephemeris points (interpolation): *)
1111+ Satellite.of_ephemeris ~points ~color ()
1012 ]} *)
11131214type t
1315(** Satellite state with cached ghost orbit. *)
14161515-type propagator = dt:float -> Kepler.Vec3.t
1616-(** Position at [dt] seconds from epoch, in J2000 km. *)
1717+(** {1 Constructors} *)
17181818-(** {1 Propagator builders} *)
1919-2020-val kepler : pos:Kepler.Vec3.t -> vel:Kepler.Vec3.t -> propagator
2121-(** Two-body Kepler. ~20 FLOPs/call. Accurate ±1 orbit. *)
2222-2323-(** {1 Constructor} *)
2424-2525-val v :
1919+val of_state_vector :
2620 pos:Kepler.Vec3.t ->
2721 vel:Kepler.Vec3.t ->
2822 color:Color.t ->
2929- ?propagate:propagator ->
3023 ?epoch_unix:float ->
3131- ?period:float ->
3224 ?trail_length:int ->
3325 unit ->
3426 t
3535-(** [v ~pos ~vel ~color ()] creates a satellite.
2727+(** From a J2000 state vector. Uses Kepler two-body propagation. Accurate within
2828+ ±1 orbit of epoch. *)
36293737- [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. *)
3030+val of_tle :
3131+ tle:Sgp4.tle ->
3232+ state:Sgp4.state ->
3333+ color:Color.t ->
3434+ ?trail_length:int ->
3535+ unit ->
3636+ t
3737+(** From a TLE. Uses SGP4/SDP4 propagation with J2 + drag. Accurate for days.
3838+ Epoch and period extracted from TLE. *)
3939+4040+val of_ephemeris :
4141+ points:(float * Kepler.Vec3.t) array ->
4242+ color:Color.t ->
4343+ ?trail_length:int ->
4444+ unit ->
4545+ t
4646+(** From time-tagged positions [(unix_time, j2000_pos_km)]. Uses Hermite
4747+ interpolation between points. Points must be sorted by time. Epoch is the
4848+ midpoint. *)
40494150(** {1 Accessors} *)
4251