1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
//! N-body propagator with state transition matrix (DOP853).
//!
//! # Overview
//!
//! This module integrates the heliocentric equations of motion for a small
//! solar-system body together with the 6×6 **state transition matrix** (STM) Φ —
//! the matrix of partial derivatives ∂(r,v)(t1)/∂(r,v)(t0) that maps small
//! perturbations of the initial state to perturbations of the final state,
//! using the explicit 8th-order Dormand–Prince (DOP853) integrator provided by
//! the [`differential-equations`](differential_equations) crate.
//!
//! ## State layout
//! The augmented state vector has 42 components stored as `[f64; 42]`:
//!
//! ```text
//! y[0..3] – heliocentric position r [AU]
//! y[3..6] – heliocentric velocity v [AU/day]
//! y[6..42] – STM Φ, stored column-major (nalgebra convention) as a flat 36-element slice
//! ```
//!
//! ## Acceleration model
//! The Newtonian heliocentric acceleration for perturber `i` is
//!
//! ```text
//! a += −GMᵢ/|d|³ · d + GMᵢ/|rᵢ|³ · rᵢ (indirect term)
//! ```
//!
//! where `d = r − rᵢ` is the asteroid–perturber vector, and the second term
//! removes the Sun's perturbation on the integration centre (heliocentric
//! formulation). When the Sun itself is a perturber the indirect term cancels
//! the direct term exactly, yielding the standard two-body acceleration.
//!
//! ## Variational equations
//! ```text
//! dΦ/dt = A(t) · Φ
//! ```
//! with `A = [[0, I], [∂a/∂r, 0]]` and
//!
//! ```text
//! ∂a/∂r = Σᵢ GMᵢ · (3 d dᵀ/|d|⁵ − I/|d|³)
//! ```
//!
//! ## Element Jacobian
//! The result of this module is `dpos_delem_ecl` (Matrix6x3, rows = elements,
//! cols = ecliptic Cartesian components) that is directly substitutable for the
//! matrix returned by `solve_two_body_problem`. It is computed as
//!
//! ```text
//! J(t1) = [Φ_pp | Φ_pv] · J0 (top 3 rows of Φ(t1) · J0_full)
//! ```
//!
//! where `J0_full` is the 6×6 matrix formed by stacking `dpos_delem` and
//! `dvel_delem` (each 6×3) from a zero-propagation call to
//! `solve_two_body_problem`.
use ODE;
use *;
use ;
use crate::;
use ;
// ---------------------------------------------------------------------------
// ODE right-hand side
// ---------------------------------------------------------------------------
// The augmented state is [f64; 42]:
// y[0..3] = position (AU)
// y[3..6] = velocity (AU/day)
// y[6..42] = STM Φ stored col-major
/// Snapshot of a perturbing body at a fixed epoch.
///
/// The snapshot is taken at t0 and held constant over the integration arc.
/// This is accurate for short arcs (≲ 30 days) where planetary motion is slow.
pub
/// ODE right-hand side for the augmented state (position, velocity, STM).
///
/// Implements [`ODE<f64, [f64; 42]>`] so that it can be driven by the DOP853
/// integrator. The dynamics are frozen at t0: perturber positions are sampled
/// once and held constant throughout the integration arc.
pub
// ---------------------------------------------------------------------------
// Acceleration sub-functions
// ---------------------------------------------------------------------------
/// Computes the direct gravitational acceleration exerted by a single perturber
/// on the small body:
///
/// ```text
/// a_direct = −GM / |asteroid_to_perturber|³ · asteroid_to_perturber
/// ```
///
/// # Arguments
///
/// * `asteroid_to_perturber` – Vector from the heliocentric position of the
/// small body to the heliocentric position of the perturber, i.e.
/// `r_perturber − r_asteroid`. Units: AU.
/// * `gravitational_parameter` – Gravitational parameter GM of the perturber.
/// Units: AU³/day².
///
/// # Returns
///
/// The direct acceleration vector (AU/day²) pointing from the small body
/// toward the perturber, scaled by GM / |d|³.
/// Computes the indirect (heliocentric frame correction) acceleration from a
/// single perturber:
///
/// ```text
/// a_indirect = +GM / |perturber_heliocentric_pos|³ · perturber_heliocentric_pos
/// ```
///
/// Returns zero if the perturber is at (or very near) the origin (i.e. it is
/// the Sun itself, which cancels with its direct term).
///
/// # Arguments
///
/// * `perturber_heliocentric_position` – Heliocentric position of the perturber
/// in the ecliptic J2000 frame. Units: AU.
/// * `gravitational_parameter` – Gravitational parameter GM of the perturber.
/// Units: AU³/day².
///
/// # Returns
///
/// The indirect acceleration correction vector (AU/day²) that removes the
/// apparent acceleration of the heliocentric origin due to the perturber.
/// Returns [`Vector3::zeros`] when the perturber distance is ≤ 1 × 10⁻¹⁰ AU
/// (i.e. the perturber coincides with the Sun).
/// Computes the gravity-gradient matrix contribution (∂a/∂r) from a single
/// perturber:
///
/// ```text
/// ∂a/∂r += −GM · (I/|d|³ − 3 d dᵀ/|d|⁵)
/// ```
///
/// # Arguments
///
/// * `asteroid_to_perturber` – Vector from the heliocentric position of the
/// small body to that of the perturber (`r_perturber − r_asteroid`).
/// Units: AU.
/// * `gravitational_parameter` – Gravitational parameter GM of the perturber.
/// Units: AU³/day².
///
/// # Returns
///
/// A 3×3 matrix (units: day⁻²) representing this perturber's contribution to
/// ∂a/∂r, the partial derivative of the acceleration with respect to the
/// small-body position. Summing this matrix over all perturbers yields the
/// lower-left block of the linearised dynamics matrix A used in the variational
/// equations `dΦ/dt = A · Φ`.
/// Accumulates total heliocentric acceleration and gravity-gradient matrix
/// over all perturbers.
///
/// Iterates over every [`PerturberSnapshot`] and sums the direct acceleration,
/// indirect acceleration correction, and gravity-gradient contribution from
/// each body.
///
/// # Arguments
///
/// * `asteroid_heliocentric_position` – Heliocentric position of the small body
/// in the ecliptic J2000 frame. Units: AU.
/// * `perturbers` – Slice of perturber snapshots evaluated at t0. Each entry
/// provides the perturber's heliocentric position (AU) and GM (AU³/day²).
///
/// # Returns
///
/// A tuple `(total_acceleration, da_dr)` where:
/// - `total_acceleration` – Combined heliocentric acceleration vector (AU/day²)
/// from all perturbers, including indirect terms.
/// - `da_dr` – 3×3 gravity-gradient matrix (day⁻²), i.e. the sum of each
/// perturber's ∂a/∂r contribution, used to build the linearised dynamics
/// matrix for the STM variational equations.
/// Builds the 6×6 linearised dynamics matrix A from the gravity-gradient block:
///
/// ```text
/// A = | 0 I |
/// | ∂a/∂r 0 |
/// ```
///
/// # Arguments
///
/// * `gravity_gradient` – 3×3 gravity-gradient matrix ∂a/∂r (day⁻²), as
/// returned by [`accumulate_perturber_effects`]. It occupies the lower-left
/// 3×3 block of A.
///
/// # Returns
///
/// The 6×6 linearised dynamics matrix A (units: mixed — upper-right block is
/// dimensionless identity, lower-left block has units day⁻²). This matrix
/// satisfies the variational equation `dΦ/dt = A · Φ`.
/// Writes position and velocity derivatives into the first 6 components of
/// `state_derivative` from the current velocity and computed acceleration.
///
/// # Arguments
///
/// * `current_velocity` – Full 42-element augmented state vector. Indices 3–5
/// are used as the current velocity components (AU/day), which become the
/// time derivative of position.
/// * `acceleration` – Total heliocentric acceleration vector (AU/day²)
/// previously computed by [`accumulate_perturber_effects`].
/// * `state_derivative` – Mutable reference to the 42-element output derivative
/// array. On return, indices 0–2 contain `ṙ = v` (AU/day) and indices 3–5
/// contain `v̇ = a` (AU/day²).
/// Computes dΦ/dt = A · Φ and writes the result into `state_derivative[6..42]`.
///
/// # Arguments
///
/// * `dynamics_matrix` – 6×6 linearised dynamics matrix A built by
/// [`build_dynamics_matrix`]. Units: mixed (see that function's documentation).
/// * `augmented_state` – Full 42-element augmented state vector. Indices 6–41
/// hold the current STM Φ stored in column-major order.
/// * `state_derivative` – Mutable reference to the 42-element output derivative
/// array. On return, indices 6–41 contain the flattened (column-major) entries
/// of dΦ/dt = A · Φ.
// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------
/// Result of a single N-body propagation step.
// ---------------------------------------------------------------------------
// propagate_nbody sub-functions
// ---------------------------------------------------------------------------
/// Builds the augmented initial state vector `[pos, vel, vec(Φ=I₆)]`.
///
/// Packs position and velocity into indices 0–5 and initialises the STM block
/// (indices 6–41) to the identity matrix I₆ stored in column-major order, so
/// that the integration starts with Φ(t0) = I.
///
/// # Arguments
///
/// * `initial_position` – Heliocentric position of the small body at t0.
/// Units: AU.
/// * `initial_velocity` – Heliocentric velocity of the small body at t0.
/// Units: AU/day.
///
/// # Returns
///
/// A 42-element array `[f64; 42]` with layout:
/// - indices 0–2: position components (AU),
/// - indices 3–5: velocity components (AU/day),
/// - indices 6–41: column-major entries of Φ₀ = I₆ (dimensionless).
pub
/// Queries the ephemeris and builds a perturber snapshot vector at the given
/// epoch.
///
/// For each body listed in [`NBodyConfig::perturbing_bodies`], this function
/// looks up the gravitational parameter from the static table in
/// [`planet_gm`](super::planet_gm) and queries the heliocentric position from
/// the supplied JPL ephemeris file.
///
/// # Arguments
///
/// * `config` – N-body configuration specifying which perturbing bodies to
/// include and the integrator tolerances.
/// * `jpl` – Opened JPL ephemeris file used to query the heliocentric position
/// of each perturbing body.
/// * `epoch` – Reference epoch at which perturber positions are sampled.
/// Passed directly to [`JPLEphem::body_ephemeris`].
///
/// # Returns
///
/// A [`Vec<PerturberSnapshot>`] with one entry per body listed in
/// `config.perturbing_bodies`, ordered identically. Each entry contains the
/// body's heliocentric position (AU) and GM (AU³/day²) at the given epoch.
///
/// # Errors
///
/// - Returns [`OutfitError::EphemerisBodyNotSupported`] if a perturbing body
/// has no GM entry in the static table or cannot be resolved by the JPL
/// ephemeris file.
pub
/// Runs the DOP853 integrator from t=0 to t=`time_span_days` and returns the
/// final augmented state vector.
///
/// Constructs a DOP853 initial-value problem from the provided ODE, integrates
/// from time 0 to `time_span_days` using the absolute and relative tolerances
/// specified in `config`, and returns the last computed augmented state.
///
/// # Arguments
///
/// * `ode` – Reference to the [`NBodyOde`] instance holding the frozen perturber
/// snapshots. Implements the ODE right-hand side.
/// * `augmented_initial_state` – 42-element initial augmented state vector at
/// t=0, as produced by [`build_augmented_initial_state`].
/// * `time_span_days` – Integration duration. Positive for forward propagation,
/// negative for backward propagation. Units: days.
/// * `config` – N-body configuration supplying `abs_tol` (AU) and `rel_tol`
/// (dimensionless) for the adaptive step-size control of DOP853.
///
/// # Returns
///
/// The 42-element augmented state at t=`time_span_days`:
/// - indices 0–2: propagated position (AU),
/// - indices 3–5: propagated velocity (AU/day),
/// - indices 6–41: column-major entries of Φ(t1) (dimensionless).
///
/// # Errors
///
/// - Returns [`OutfitError::NBodyPropagationFailed`] if the DOP853 solver
/// returns an error or if the solution contains no steps.
pub
/// Converts the t0 element Jacobians `(dpos_delem0, dvel_delem0)` (each 6×3)
/// into a single 6×6 matrix whose rows are state components and whose columns
/// are element indices:
///
/// ```text
/// J0 = | (dpos_delem0)ᵀ |
/// | (dvel_delem0)ᵀ |
/// ```
///
/// # Arguments
///
/// * `dpos_delem_at_t0` – 6×3 Jacobian of heliocentric position with respect to
/// the six equinoctial elements at t0 (rows = elements, cols = Cartesian
/// components). Units: AU / (element unit).
/// * `dvel_delem_at_t0` – 6×3 Jacobian of heliocentric velocity with respect to
/// the six equinoctial elements at t0 (rows = elements, cols = Cartesian
/// velocity components). Units: (AU/day) / (element unit).
///
/// # Returns
///
/// A 6×6 matrix J0 where:
/// - rows 0–2 correspond to the three position Cartesian components,
/// - rows 3–5 correspond to the three velocity Cartesian components,
/// - column `j` corresponds to equinoctial element index `j`.
///
/// This layout is compatible with the STM Φ so that the product `Φ(t1) · J0`
/// propagates the element Jacobian to t1.
pub
/// Splits the propagated 6×6 Jacobian `Φ(t1) · J0` back into the
/// `(dpos_delem, dvel_delem)` pair at t1 (each 6×3).
///
/// Performs the inverse of the layout established by [`build_initial_state_jacobian`]:
/// extracts the top 3 rows into `dpos_delem_at_t1` and the bottom 3 rows into
/// `dvel_delem_at_t1`, transposing the index ordering back to the 6×3 convention
/// (rows = elements, cols = Cartesian components).
///
/// # Arguments
///
/// * `propagated_state_jacobian` – 6×6 matrix `Φ(t1) · J0` resulting from
/// multiplying the STM at t1 by the initial-state Jacobian J0.
/// - rows 0–2: propagated position partials,
/// - rows 3–5: propagated velocity partials,
/// - column `j`: partial with respect to equinoctial element `j`.
///
/// # Returns
///
/// A tuple `(dpos_delem_at_t1, dvel_delem_at_t1)` where each matrix has shape
/// 6×3 (rows = equinoctial elements, cols = ecliptic Cartesian components):
/// - `dpos_delem_at_t1`: ∂pos(t1)/∂elem. Units: AU / (element unit).
/// - `dvel_delem_at_t1`: ∂vel(t1)/∂elem. Units: (AU/day) / (element unit).
pub