1use crate::{C, G, GravityParticle, GravitySolver};
35use phyz_math::Vec3;
36
37#[derive(Debug, Clone)]
39pub struct PostNewtonianSolver {
40 pub max_order: f64,
42 pub softening: f64,
44 pub include_radiation: bool,
46}
47
48impl PostNewtonianSolver {
49 pub fn new(max_order: f64) -> Self {
51 Self {
52 max_order,
53 softening: 1e-3,
54 include_radiation: max_order >= 2.5,
55 }
56 }
57
58 fn newtonian_acceleration(
60 &self,
61 xi: Vec3,
62 _vi: Vec3,
63 _mi: f64,
64 xj: Vec3,
65 _vj: Vec3,
66 mj: f64,
67 ) -> Vec3 {
68 let r = xj - xi;
69 let r2 = r.norm_squared() + self.softening * self.softening;
70 let r_mag = r2.sqrt();
71
72 G * mj / r2 * (r / r_mag)
74 }
75
76 fn pn1_acceleration(&self, xi: Vec3, vi: Vec3, mi: f64, xj: Vec3, vj: Vec3, mj: f64) -> Vec3 {
90 let r = xj - xi;
91 let r2 = r.norm_squared() + self.softening * self.softening;
92 let r_mag = r2.sqrt();
93 let n = r / r_mag;
94
95 let v2_i = vi.norm_squared();
96 let v2_j = vj.norm_squared();
97 let vi_dot_vj = vi.dot(&vj);
98 let vi_dot_n = vi.dot(&n);
99 let vj_dot_n = vj.dot(&n);
100
101 let c2 = C * C;
102
103 let schwarzschild = 4.0 * G * (mi + mj) / r_mag;
105
106 let coeff = G * mj / r2 / c2;
108
109 let term1 =
110 (schwarzschild - v2_i - 2.0 * v2_j + 4.0 * vi_dot_vj + 1.5 * vj_dot_n.powi(2)) * n;
111 let term2 = (4.0 * vi_dot_n - 3.0 * vj_dot_n) * vj;
112
113 coeff * (term1 + term2)
114 }
115
116 fn pn2_5_acceleration(&self, xi: Vec3, vi: Vec3, mi: f64, xj: Vec3, vj: Vec3, mj: f64) -> Vec3 {
128 if !self.include_radiation {
129 return Vec3::zeros();
130 }
131
132 let r = xj - xi;
133 let r2 = r.norm_squared() + self.softening * self.softening;
134 let r_mag = r2.sqrt();
135 let r3 = r_mag * r2;
136 let n = r / r_mag;
137
138 let v_rel = vi - vj;
139 let v_rel_dot_n = v_rel.dot(&n);
140
141 let c5 = C.powi(5);
142 let coeff = -8.0 / 5.0 * G * G * mi * mj * (mi + mj) / (c5 * r3);
143
144 coeff * (v_rel - 3.0 * v_rel_dot_n * n)
145 }
146
147 fn pn_acceleration(&self, xi: Vec3, vi: Vec3, mi: f64, xj: Vec3, vj: Vec3, mj: f64) -> Vec3 {
149 let mut a = self.newtonian_acceleration(xi, vi, mi, xj, vj, mj);
150
151 if self.max_order >= 1.0 {
152 a += self.pn1_acceleration(xi, vi, mi, xj, vj, mj);
153 }
154
155 if self.max_order >= 2.5 {
156 a += self.pn2_5_acceleration(xi, vi, mi, xj, vj, mj);
157 }
158
159 a
160 }
161}
162
163impl GravitySolver for PostNewtonianSolver {
164 fn compute_forces(&mut self, particles: &mut [GravityParticle]) {
165 let n = particles.len();
166
167 for p in particles.iter_mut() {
169 p.reset_force();
170 }
171
172 for i in 0..n {
176 for j in 0..n {
177 if i == j {
178 continue;
179 }
180
181 let xi = particles[i].x;
182 let vi = particles[i].v;
183 let mi = particles[i].m;
184
185 let xj = particles[j].x;
186 let vj = particles[j].v;
187 let mj = particles[j].m;
188
189 let a = self.pn_acceleration(xi, vi, mi, xj, vj, mj);
190 particles[i].add_force(a * mi);
191 }
192 }
193 }
194
195 fn potential_energy(&self, particles: &[GravityParticle]) -> f64 {
196 let n = particles.len();
197 let mut u = 0.0;
198
199 for i in 0..n {
201 for j in i + 1..n {
202 let r = (particles[j].x - particles[i].x).norm();
203 let r_soft = (r * r + self.softening * self.softening).sqrt();
204 u -= G * particles[i].m * particles[j].m / r_soft;
205 }
206 }
207
208 u
210 }
211}
212
213pub fn orbital_elements(x: Vec3, v: Vec3, m_central: f64) -> (f64, f64, f64, f64, f64, f64) {
215 let mu = G * m_central;
216
217 let r = x.norm();
218 let v2 = v.norm_squared();
219
220 let energy = v2 / 2.0 - mu / r;
222
223 let h = x.cross(&v);
225 let h_mag = h.norm();
226
227 let a = -mu / (2.0 * energy);
229
230 let e_vec = v.cross(&h) / mu - x / r;
232 let e = e_vec.norm();
233
234 let i = (h.z / h_mag).acos();
236
237 let n = Vec3::new(-h.y, h.x, 0.0);
239 let n_mag = n.norm();
240 let omega = if n_mag > 1e-10 {
241 let omega_raw = (n.x / n_mag).acos();
242 if n.y < 0.0 {
243 2.0 * std::f64::consts::PI - omega_raw
244 } else {
245 omega_raw
246 }
247 } else {
248 0.0
249 };
250
251 let omega_bar = if n_mag > 1e-10 && e > 1e-10 {
253 let omega_bar_raw = (n.dot(&e_vec) / (n_mag * e)).acos();
254 if e_vec.z < 0.0 {
255 2.0 * std::f64::consts::PI - omega_bar_raw
256 } else {
257 omega_bar_raw
258 }
259 } else {
260 0.0
261 };
262
263 let nu = if e > 1e-10 {
265 let nu_raw = (e_vec.dot(&x) / (e * r)).acos();
266 if x.dot(&v) < 0.0 {
267 2.0 * std::f64::consts::PI - nu_raw
268 } else {
269 nu_raw
270 }
271 } else {
272 0.0
273 };
274
275 (a, e, i, omega, omega_bar, nu)
276}
277
278pub fn perihelion_precession_rate(a: f64, e: f64, m_central: f64, period: f64) -> f64 {
285 let delta_omega_per_orbit =
286 6.0 * std::f64::consts::PI * G * m_central / (C * C * a * (1.0 - e * e));
287
288 let orbits_per_century = 3.15576e9 / period; let delta_omega_century = delta_omega_per_orbit * orbits_per_century;
291
292 delta_omega_century * 206265.0
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_newtonian_vs_pn() {
302 let mut solver_newton = PostNewtonianSolver::new(0.0); let mut solver_pn = PostNewtonianSolver::new(1.0); let mut particles_newton = vec![
306 GravityParticle::new(Vec3::zeros(), Vec3::zeros(), 1e10),
307 GravityParticle::new(Vec3::new(1.0, 0.0, 0.0), Vec3::zeros(), 1e10),
308 ];
309
310 let mut particles_pn = particles_newton.clone();
311
312 solver_newton.compute_forces(&mut particles_newton);
313 solver_pn.compute_forces(&mut particles_pn);
314
315 let diff = (particles_newton[0].f - particles_pn[0].f).norm();
317 assert!(diff < 1e-6);
319 }
320
321 #[test]
322 fn test_mercury_precession() {
323 let a = 57.9e9; let e = 0.2056; let m_sun = 1.989e30; let period = 87.969 * 24.0 * 3600.0; let precession = perihelion_precession_rate(a, e, m_sun, period);
330
331 assert!(precession > 38.0 && precession < 48.0);
334 }
335
336 #[test]
337 fn test_orbital_elements() {
338 let m_sun = 1.989e30;
340 let r = 1.5e11; let v_circular = (G * m_sun / r).sqrt();
342
343 let x = Vec3::new(r, 0.0, 0.0);
344 let v = Vec3::new(0.0, v_circular, 0.0);
345
346 let (a, e, _i, _omega, _omega_bar, _nu) = orbital_elements(x, v, m_sun);
347
348 assert!((a - r).abs() / r < 1e-6);
350 assert!(e < 1e-6);
351 }
352
353 #[test]
354 fn test_2_5pn_radiation() {
355 let mut solver = PostNewtonianSolver::new(2.5);
356
357 let mut particles = vec![
359 GravityParticle::new(Vec3::new(-0.5e9, 0.0, 0.0), Vec3::new(0.0, 1e4, 0.0), 1e30),
360 GravityParticle::new(Vec3::new(0.5e9, 0.0, 0.0), Vec3::new(0.0, -1e4, 0.0), 1e30),
361 ];
362
363 solver.compute_forces(&mut particles);
364
365 assert!(particles[0].f.norm() > 0.0);
367 }
368}