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
//! Newton–Raphson solver for the universal Kepler equation.
//!
//! This module provides a safeguarded Newton–Raphson solver as the primary
//! solver for the universal Kepler equation. For a more robust but slower
//! alternative, see [`brent_dekker`](crate::kepler::brent_dekker).
//!
//! # Algorithm overview
//!
//! 1. **Initial guess** ([`prelim_kepuni`](crate::kepler::UniversalKeplerParams::prelim_kepuni)) — heuristic estimate of $ \psi_0 $ ,
//! or a caller-supplied warm start.
//! 2. **Newton–Raphson iteration** — repeated application of
//! $ \psi_{n+1} = \psi_n - \frac{f(\psi_n)}{f'(\psi_n)} $
//! where
//! $ f(\psi) = r_0 \cdot s_1(\psi,\alpha) + \sigma_0 \cdot s_2(\psi,\alpha)
//! + \mu \cdot s_3(\psi,\alpha) - \Delta t $
//! $ f'(\psi) = r_0 \cdot s_0(\psi,\alpha) + \sigma_0 \cdot s_1(\psi,\alpha)
//! + \mu \cdot s_2(\psi,\alpha) $
//! 3. **Safeguards** — derivative guard, step limiter, sign-change damping.
//!
//! # Supported regimes
//!
//! * **Elliptic** ( $ \alpha < 0 $ ) and **Hyperbolic** ( $ \alpha > 0 $ ) motions.
//! * **Parabolic** ( $ \alpha = 0 $ ) is not handled and returns `None`.
use crateUniversalKeplerSolution;
use UniversalKeplerParams;
use s_funct;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/// Maximum number of Newton–Raphson iterations before declaring non-convergence.
const MAX_NEWTON_ITERATIONS: usize = 50;
/// Maximum allowed step magnitude relative to the current $ |\psi| $ :
/// $ |\Delta\psi| \leq k \cdot (1 + |\psi|) $ .
///
/// Prevents runaway updates on hyperbolic trajectories where $ \psi $ can grow
/// rapidly.
const MAX_RELATIVE_STEP_FACTOR: f64 = 2.0;
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// Solve the **universal Kepler equation** using a safeguarded Newton–Raphson
/// iteration, with an optional warm-start guess.
///
/// # Goal
///
/// Find the universal anomaly $ \psi^* $ such that the residual
///
/// $ f(\psi) = r_0 \cdot s_1(\psi, \alpha) + \sigma_0 \cdot s_2(\psi, \alpha)
/// + \mu \cdot s_3(\psi, \alpha) - \Delta t = 0 $
///
/// vanishes. The universal anomaly $ \psi $ parametrises the along-track
/// progress of a body regardless of orbit type, unifying the classical
/// anomalies (eccentric, hyperbolic, parabolic) into a single variable.
///
/// # Scientific background
///
/// The **universal-variable formulation** unifies elliptic and hyperbolic
/// motion in a single equation. The Stumpff-like functions $ (s_0, s_1, s_2,
/// s_3) $ appear in the **Lagrange f–g series**, enabling position and velocity
/// propagation across conic regimes by combining geometry ( $ r_0 $ , $ \sigma_0 $ )
/// with dynamics ( $ \mu $ , $ \alpha $ ) and the time of flight $ \Delta t $ .
///
/// The energy parameter $ \alpha = -\mu / (2a) $ encodes the orbit type:
///
/// | Regime | Condition | Shape |
/// |---|---|---|
/// | Elliptic | $ \alpha < 0 $ | Closed orbit |
/// | Parabolic | $ \alpha = 0 $ | Escape at exact escape velocity |
/// | Hyperbolic | $ \alpha > 0 $ | Open, unbound trajectory |
///
/// # Numerical strategy
///
/// The solver pipeline proceeds in four steps:
///
/// 1. **Tolerance setup** — two tolerances are derived:
/// - Step tolerance: $ \varepsilon_\psi $ (default $ 100\varepsilon $ ), controls
/// convergence on $ |\Delta\psi| $ .
/// - Residual tolerance: $ \varepsilon_f = 10\varepsilon \cdot (1 + |\Delta t|) $ ,
/// scale-aware to remain meaningful for both small and large flight times.
/// 2. **Orbit-type guard** — parabolic orbits ( $ \alpha = 0 $ ) are rejected
/// immediately.
/// 3. **Initial guess** — uses the caller-supplied `psi_guess` if provided
/// (warm start), otherwise falls back to [`prelim_kepuni`](crate::kepler::UniversalKeplerParams::prelim_kepuni).
/// 4. **Newton–Raphson loop** with three safeguards applied at each step:
/// - **Derivative guard** — if $ |f'(\psi)| $ is too small or non-finite,
/// $ \psi $ is halved to escape a flat region.
/// - **Step limiter** — $ |\Delta\psi| $ is capped at
/// $ k \cdot (1 + |\psi|) $ with $ k = 2 $ , preventing overshooting on
/// hyperbolic trajectories.
/// - **Sign-change damping** — if the Newton step would cross zero, $ \psi $
/// is halved instead to avoid oscillations around $ \psi = 0 $ .
///
/// # Convergence criteria
///
/// The iteration terminates as soon as **any** of the following holds:
///
/// - **Residual**: $ |f(\psi)| \leq \varepsilon_f $
/// - **Absolute step**: $ |\Delta\psi| \leq \varepsilon_\psi $
/// - **Relative step**: $ |\Delta\psi| \leq \varepsilon_\psi \cdot (1 + |\psi|) $
///
/// # Comparison with Brent–Dekker
///
/// | Property | Newton–Raphson | Brent–Dekker |
/// |---|---|---|
/// | Convergence guarantee | No (can diverge) | Yes (within bracket) |
/// | Convergence rate | Quadratic (near root) | Superlinear |
/// | Requires derivative | Yes | No |
/// | Requires bracket | No | Yes |
///
/// Newton–Raphson is preferred when a good initial guess is available and
/// quadratic convergence is desirable. Use the Brent-Dekker method as a fallback when this solver fails to converge.
///
/// # Arguments
///
/// * `params` – Packed orbital parameters for the universal-variable
/// formulation:
/// - `r0` — initial radius $ r_0 $ ,
/// - `sig0` — radial velocity proxy $ \sigma_0 = \mathbf{r}_0 \cdot \dot{\mathbf{r}}_0 / \sqrt{\mu} $ ,
/// - `mu` — gravitational parameter $ \mu $ ,
/// - `alpha` — energy parameter $ \alpha $ ,
/// - `dt` — time of flight $ \Delta t $ .
/// * `convergency` – Optional absolute tolerance on $ |\Delta\psi| $
/// (default: $ 100\varepsilon \approx 2.2 \times 10^{-14} $ ).
/// * `psi_guess` – Optional warm-start value for $ \psi_0 $ . If `None`,
/// [`prelim_kepuni`](crate::kepler::UniversalKeplerParams::prelim_kepuni) is used to construct the initial guess.
///
/// # Return
///
/// * `Some(UniversalKeplerSolution)` — the converged universal anomaly
/// $ \psi^* $ together with the evaluated Stumpff functions
/// $ (s_0, s_1, s_2, s_3) $ at $ \psi^* $ .
/// * `None` if any of the following conditions occur:
/// - the orbit is parabolic ( $ \alpha = 0 $ ),
/// - [`prelim_kepuni`](crate::kepler::UniversalKeplerParams::prelim_kepuni) fails to produce a finite initial guess,
/// - the Newton loop exhausts maximum iteration without meeting any
/// convergence criterion.
///
/// # See also
///
/// * [`solve_kepuni`] – Thin wrapper without warm-start argument.
/// * [`s_funct`](crate::kepler::s_funct) – Stumpff auxiliary functions.
/// * [`prelim_kepuni`](crate::kepler::UniversalKeplerParams::prelim_kepuni) – Heuristic initial guess.
/// Solve the universal Kepler equation without a warm-start guess.
///
/// This is a thin wrapper around [`solve_kepuni_with_guess`] that omits the
/// optional warm-start argument. It exists for call sites that only have the
/// orbital parameters and a convergence tolerance available.
///
/// # Behavior
///
/// Delegates directly to [`solve_kepuni_with_guess`] with `psi_guess = None`.
/// The initial guess is then provided by [`prelim_kepuni`](crate::kepler::UniversalKeplerParams::prelim_kepuni).
///
/// # Arguments
///
/// * `params` – Packed orbital parameters for the universal-variable
/// formulation.
/// * `convergency` – Optional absolute tolerance on $ |\Delta\psi| $
/// (default: $ 100\varepsilon $ ).
///
/// # Return
///
/// * `Some(UniversalKeplerSolution)` on convergence.
/// * `None` if convergence fails or the orbit is parabolic ( $ \alpha = 0 $ ).
///
/// # See also
///
/// * [`solve_kepuni_with_guess`] – Extended variant with warm-start support.
// ---------------------------------------------------------------------------
// Internal pipeline steps
// ---------------------------------------------------------------------------
/// Compute the scale-aware residual tolerance.
///
/// $ \varepsilon_f = 10\varepsilon \cdot (1 + |\sqrt{\mu}\Delta t|) $
///
/// Mixing an absolute floor $ 10\varepsilon $ with a relative component
/// $ 10\varepsilon \cdot |\sqrt{\mu}\Delta t| $ keeps the criterion
/// meaningful across the full range of flight times. The residual is
/// compared against $ \sqrt{\mu}\Delta t $ (not raw $ \Delta t $ ), so the
/// tolerance is scaled accordingly.
/// Evaluate the universal Kepler residual $ f(\psi) $ and its derivative
/// $ f'(\psi) $ at the given universal anomaly.
///
/// $ f(\psi) = r_0 \cdot s_1 + \sigma_0 \cdot s_2 + s_3 - \sqrt{\mu} \cdot \Delta t $
/// $ f'(\psi) = r_0 \cdot s_0 + \sigma_0 \cdot s_1 + s_2 $
///
/// The derivative chain of the Stumpff functions gives
/// $ \mathrm{d}s_k / \mathrm{d}\psi = s_{k-1} $ . `alpha` here is the
/// reciprocal semi-major-axis convention ( $ \alpha = -1/a $ ), so no extra
/// `mu` factor is needed on the `s2`/`s3` terms — see
/// [`UniversalKeplerParams`](crate::kepler::UniversalKeplerParams).
/// Run the core Newton–Raphson iteration loop.
///
/// Returns `Some(UniversalKeplerSolution)` on convergence, or `None` if
/// maximum iteration is exhausted without meeting any convergence
/// criterion.
// ---------------------------------------------------------------------------
// Newton step helpers
// ---------------------------------------------------------------------------
/// Return `true` if the derivative is too small or non-finite to produce a
/// reliable Newton step.
/// Compute the Newton step $ \Delta\psi = -f(\psi) / f'(\psi) $ , capped to
/// prevent overshooting.
///
/// The step magnitude is limited to
/// $ |\Delta\psi| \leq k \cdot (1 + |\psi|) $ with $ k = $ [`MAX_RELATIVE_STEP_FACTOR`].
/// Apply sign-change damping to the Newton candidate.
///
/// If applying `step` to `psi` would flip the sign of $\psi$ (i.e., cross
/// zero), halve $\psi$ instead. This prevents oscillations around $\psi = 0$
/// that are a common instability pattern in the Newton iteration.
/// Check convergence on the step size after updating $\psi$.
///
/// Two criteria are tested in order:
///
/// - **Absolute**: $|\Delta\psi| \leq \varepsilon_\psi$
/// — reuses the Stumpff values already computed at the previous $\psi$
/// to avoid one extra call.
/// - **Relative**: $|\Delta\psi| \leq \varepsilon_\psi \cdot (1 + |\psi|)$
/// — recomputes Stumpff functions at the updated $\psi$ to ensure strict
/// consistency of the returned solution.
///
/// Returns `Some(UniversalKeplerSolution)` if either criterion is met,
/// `None` otherwise.