1 // Copyright 2020, 2021 Francesco Biscani (bluescarni@gmail.com), Dario Izzo (dario.izzo@gmail.com)
2 //
3 // This file is part of the heyoka library.
4 //
5 // This Source Code Form is subject to the terms of the Mozilla
6 // Public License v. 2.0. If a copy of the MPL was not distributed
7 // with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 
9 #include <heyoka/config.hpp>
10 
11 #include <algorithm>
12 #include <array>
13 #include <cmath>
14 #include <initializer_list>
15 #include <tuple>
16 #include <utility>
17 #include <vector>
18 
19 #if defined(HEYOKA_HAVE_REAL128)
20 
21 #include <mp++/real128.hpp>
22 
23 #endif
24 
25 #include <heyoka/expression.hpp>
26 #include <heyoka/math.hpp>
27 #include <heyoka/number.hpp>
28 #include <heyoka/taylor.hpp>
29 
30 #include "catch.hpp"
31 #include "test_utils.hpp"
32 
33 using namespace heyoka;
34 using namespace heyoka_test;
35 
36 const auto fp_types = std::tuple<double
37 #if !defined(HEYOKA_ARCH_PPC)
38                                  ,
39                                  long double
40 #endif
41 #if defined(HEYOKA_HAVE_REAL128)
42                                  ,
43                                  mppp::real128
44 #endif
45                                  >{};
46 
47 // Small helper to compute the angular momentum
48 // wrt the origin given a state vector.
49 template <typename T>
compute_am(const std::vector<T> & st)50 T compute_am(const std::vector<T> &st)
51 {
52     using std::sqrt;
53 
54     auto vx0 = st[0];
55     auto vx1 = st[1];
56     auto vy0 = st[2];
57     auto vy1 = st[3];
58     auto vz0 = st[4];
59     auto vz1 = st[5];
60 
61     auto x0 = st[6];
62     auto x1 = st[7];
63     auto y0 = st[8];
64     auto y1 = st[9];
65     auto z0 = st[10];
66     auto z1 = st[11];
67 
68     T am0[] = {y0 * vz0 - z0 * vy0, z0 * vx0 - x0 * vz0, x0 * vy0 - y0 * vx0};
69     T am1[] = {y1 * vz1 - z1 * vy1, z1 * vx1 - x1 * vz1, x1 * vy1 - y1 * vx1};
70     T am[] = {am0[0] + am1[0], am0[1] + am1[1], am0[2] + am1[2]};
71 
72     return sqrt(am[0] * am[0] + am[1] * am[1] + am[2] * am[2]);
73 }
74 
75 // Two-body problem energy from the state vector.
76 template <typename T>
tbp_energy(const std::vector<T> & st)77 T tbp_energy(const std::vector<T> &st)
78 {
79     using std::sqrt;
80 
81     auto Dx = st[6] - st[7];
82     auto Dy = st[8] - st[9];
83     auto Dz = st[10] - st[11];
84     auto dist = sqrt(Dx * Dx + Dy * Dy + Dz * Dz);
85     auto U = -1 / dist;
86 
87     auto v2_0 = st[0] * st[0] + st[2] * st[2] + st[4] * st[4];
88     auto v2_1 = st[1] * st[1] + st[3] * st[3] + st[5] * st[5];
89 
90     return T(1) / T(2) * (v2_0 + v2_1) + U;
91 }
92 
93 TEST_CASE("two body")
94 {
__anon463006ba0102(auto fp_x, unsigned opt_level, bool high_accuracy, bool compact_mode) 95     auto tester = [](auto fp_x, unsigned opt_level, bool high_accuracy, bool compact_mode) {
96         using std::abs;
97         using std::cos;
98 
99         using fp_t = decltype(fp_x);
100 
101         auto [vx0, vx1, vy0, vy1, vz0, vz1, x0, x1, y0, y1, z0, z1]
102             = make_vars("vx0", "vx1", "vy0", "vy1", "vz0", "vz1", "x0", "x1", "y0", "y1", "z0", "z1");
103 
104         auto x01 = x1 - x0;
105         auto y01 = y1 - y0;
106         auto z01 = z1 - z0;
107         auto r01_m3
108             = pow(x01 * x01 + y01 * y01 + z01 * z01, expression{number{fp_t{-3}}} / expression{number{fp_t{2}}});
109 
110         const auto kep = std::array<fp_t, 6>{fp_t{1.5}, fp_t{.2}, fp_t{.3}, fp_t{.4}, fp_t{.5}, fp_t{.6}};
111         const auto [c_x, c_v] = kep_to_cart(kep, fp_t{1} / 4);
112 
113         std::vector<fp_t> init_state{c_v[0], -c_v[0], c_v[1], -c_v[1], c_v[2], -c_v[2],
114                                      c_x[0], -c_x[0], c_x[1], -c_x[1], c_x[2], -c_x[2]};
115 
116         taylor_adaptive<fp_t> tad{{x01 * r01_m3, -x01 * r01_m3, y01 * r01_m3, -y01 * r01_m3, z01 * r01_m3,
117                                    -z01 * r01_m3, vx0, vx1, vy0, vy1, vz0, vz1},
118                                   std::move(init_state),
119                                   kw::opt_level = opt_level,
120                                   kw::high_accuracy = high_accuracy,
121                                   kw::compact_mode = compact_mode};
122 
123         const auto &st = tad.get_state();
124 
125         const auto en = tbp_energy(st);
126         const auto am = compute_am(st);
127 
128         for (auto i = 0; i < 200; ++i) {
129             const auto [oc, h] = tad.step();
130             REQUIRE(oc == taylor_outcome::success);
131             REQUIRE(tbp_energy(st) == approximately(en, fp_t{1E2}));
132             REQUIRE(compute_am(st) == approximately(am, fp_t{1E2}));
133 
134             const auto kep1 = cart_to_kep(std::array<fp_t, 3>{st[6], st[8], st[10]},
135                                           std::array<fp_t, 3>{st[0], st[2], st[4]}, fp_t{1} / 4);
136             const auto kep2 = cart_to_kep(std::array<fp_t, 3>{st[7], st[9], st[11]},
137                                           std::array<fp_t, 3>{st[1], st[3], st[5]}, fp_t{1} / 4);
138 
139             REQUIRE(kep1[0] == approximately(fp_t{1.5}, fp_t{1E2}));
140             REQUIRE(kep2[0] == approximately(fp_t{1.5}, fp_t{1E2}));
141             REQUIRE(kep1[1] == approximately(fp_t{.2}, fp_t{1E3}));
142             REQUIRE(kep2[1] == approximately(fp_t{.2}, fp_t{1E3}));
143             REQUIRE(kep1[2] == approximately(fp_t{.3}, fp_t{1E2}));
144             REQUIRE(kep2[2] == approximately(fp_t{.3}, fp_t{1E2}));
145             REQUIRE(abs(cos(kep1[3])) == approximately(abs(cos(fp_t{.4})), fp_t{1E3}));
146             REQUIRE(abs(cos(kep2[3])) == approximately(abs(cos(fp_t{.4})), fp_t{1E3}));
147             REQUIRE(kep1[4] == approximately(fp_t{.5}, fp_t{1E2}));
148             REQUIRE(kep2[4] == approximately(fp_t{.5}, fp_t{1E2}));
149         }
150     };
151 
152     for (auto cm : {true, false}) {
153         for (auto ha : {true, false}) {
__anon463006ba0202(auto x) 154             tuple_for_each(fp_types, [&tester, ha, cm](auto x) { tester(x, 0, ha, cm); });
__anon463006ba0302(auto x) 155             tuple_for_each(fp_types, [&tester, ha, cm](auto x) { tester(x, 1, ha, cm); });
__anon463006ba0402(auto x) 156             tuple_for_each(fp_types, [&tester, ha, cm](auto x) { tester(x, 2, ha, cm); });
__anon463006ba0502(auto x) 157             tuple_for_each(fp_types, [&tester, ha, cm](auto x) { tester(x, 3, ha, cm); });
158         }
159     }
160 }
161 
162 // Energy of two uniform overlapping spheres.
163 template <typename T>
tus_energy(T rs,const std::vector<T> & st)164 T tus_energy(T rs, const std::vector<T> &st)
165 {
166     auto Dx = st[6] - st[7];
167     auto Dy = st[8] - st[9];
168     auto Dz = st[10] - st[11];
169     auto dist = std::sqrt(Dx * Dx + Dy * Dy + Dz * Dz);
170     auto U = 1 / (160 * std::pow(rs, 6))
171              * (std::pow(dist, 5) - 30 * rs * rs * std::pow(dist, 3) + 80 * std::pow(rs, 3) * dist * dist);
172 
173     auto v2_0 = st[0] * st[0] + st[2] * st[2] + st[4] * st[4];
174     auto v2_1 = st[1] * st[1] + st[3] * st[3] + st[5] * st[5];
175 
176     // NOTE: -6/(5*rs) is a corrective term to ensure
177     // the potential matches the 2bp potential at the
178     // transition threshold.
179     return 1 / 2. * (v2_0 + v2_1) + U - 6. / (5 * rs);
180 }
181 
182 TEST_CASE("two uniform spheres")
183 {
184     auto [vx0, vx1, vy0, vy1, vz0, vz1, x0, x1, y0, y1, z0, z1]
185         = make_vars("vx0", "vx1", "vy0", "vy1", "vz0", "vz1", "x0", "x1", "y0", "y1", "z0", "z1");
186 
187     auto x01 = x1 - x0;
188     auto y01 = y1 - y0;
189     auto z01 = z1 - z0;
190     auto r01 = pow(x01 * x01 + y01 * y01 + z01 * z01, 1_dbl / 2_dbl);
191     const auto rs_val = std::sqrt(2);
192     auto rs = expression{number{rs_val}};
193     auto rs2 = rs * rs;
194     auto rs3 = rs2 * rs;
195     auto rs6 = rs3 * rs3;
196     auto fac = (r01 * r01 * r01 - 18_dbl * rs2 * r01 + 32_dbl * rs3) / (32_dbl * rs6);
197 
198     std::vector<double> init_state{0.593, -0.593, 0, 0, 0, 0, -1.000001, 1.000001, -1, 1, 0, 0};
199 
200     taylor_adaptive<double> tad{
201         {x01 * fac, -x01 * fac, y01 * fac, -y01 * fac, z01 * fac, -z01 * fac, vx0, vx1, vy0, vy1, vz0, vz1},
202         std::move(init_state)};
203 
204     const auto &st = tad.get_state();
205 
206     const auto en = tbp_energy(st);
207     const auto am = compute_am(st);
208 
209     for (auto i = 0; i < 200; ++i) {
210         const auto [oc, h] = tad.step();
211         REQUIRE(oc == taylor_outcome::success);
212         REQUIRE(std::abs((en - tus_energy(rs_val, st)) / en) <= 1E-11);
213         REQUIRE(std::abs((am - compute_am(st)) / am) <= 1E-11);
214     }
215 }
216 
217 TEST_CASE("mixed tb/spheres")
218 {
219     auto [vx0, vx1, vy0, vy1, vz0, vz1, x0, x1, y0, y1, z0, z1]
220         = make_vars("vx0", "vx1", "vy0", "vy1", "vz0", "vz1", "x0", "x1", "y0", "y1", "z0", "z1");
221 
222     auto x01 = x1 - x0;
223     auto y01 = y1 - y0;
224     auto z01 = z1 - z0;
225     auto r01 = pow(x01 * x01 + y01 * y01 + z01 * z01, 1_dbl / 2_dbl);
226     auto r01_m3 = pow(x01 * x01 + y01 * y01 + z01 * z01, -3_dbl / 2_dbl);
227     const auto rs_val = std::sqrt(2);
228     auto rs = expression{number{rs_val}};
229     auto rs2 = rs * rs;
230     auto rs3 = rs2 * rs;
231     auto rs6 = rs3 * rs3;
232     auto fac = (r01 * r01 * r01 - 18_dbl * rs2 * r01 + 32_dbl * rs3) / (32_dbl * rs6);
233 
234     std::vector<double> init_state{0.593, -0.593, 0, 0, 0, 0, -1.000001, 1.000001, -1, 1, 0, 0};
235 
236     // 2BP integrator.
237     taylor_adaptive<double> t_2bp{{x01 * r01_m3, -x01 * r01_m3, y01 * r01_m3, -y01 * r01_m3, z01 * r01_m3,
238                                    -z01 * r01_m3, vx0, vx1, vy0, vy1, vz0, vz1},
239                                   init_state};
240 
241     // 2US integrator.
242     taylor_adaptive<double> t_2us{
243         {x01 * fac, -x01 * fac, y01 * fac, -y01 * fac, z01 * fac, -z01 * fac, vx0, vx1, vy0, vy1, vz0, vz1},
244         std::move(init_state)};
245 
246     // Helper to compute the distance**2 between
247     // the sphere's centres given a state vector.
__anon463006ba0602(const auto &st) 248     auto compute_dist2 = [](const auto &st) {
249         auto Dx = st[6] - st[7];
250         auto Dy = st[8] - st[9];
251         auto Dz = st[10] - st[11];
252         return Dx * Dx + Dy * Dy + Dz * Dz;
253     };
254 
255     // Helper to get the dynamical regime given an input state vector:
256     // this returns true for the Keplerian regime (non-overlapping spheres),
257     // false for the 2US regime (overlapping spheres).
__anon463006ba0702(const auto &st) 258     auto get_regime = [rs_val, compute_dist2](const auto &st) { return compute_dist2(st) > 4 * rs_val * rs_val; };
259 
260     // The simulation is set up to start in the Keplerian regime.
261     auto cur_regime = get_regime(t_2bp.get_state());
262     REQUIRE(cur_regime);
263 
264     // Pointers to the current and other integrators.
265     taylor_adaptive<double> *cur_t = &t_2bp, *other_t = &t_2us;
266 
267     const auto en = tbp_energy(cur_t->get_state());
268     const auto am = compute_am(cur_t->get_state());
269 
270     for (auto i = 0; i < 200; ++i) {
271         // Compute the max velocity in the system.
272         const auto &st = cur_t->get_state();
273         auto v2_0 = st[0] * st[0] + st[2] * st[2] + st[4] * st[4];
274         auto v2_1 = st[1] * st[1] + st[3] * st[3] + st[5] * st[5];
275         auto max_v = std::max(std::sqrt(v2_0), std::sqrt(v2_1));
276 
277         // Do a timestep imposing that that max_v * delta_t < 1/2*rs.
278         auto [oc, h] = cur_t->step(rs_val / (2 * max_v));
279         REQUIRE((oc == taylor_outcome::success || oc == taylor_outcome::time_limit));
280 
281         if (get_regime(cur_t->get_state()) != cur_regime) {
282             auto cur_dist = std::sqrt(compute_dist2(cur_t->get_state()));
283 
284             while (std::abs(cur_dist - 2 * rs_val) > 1E-12 && h > 1E-12) {
285                 if (cur_regime) {
286                     // Keplerian regime -> sphere regime.
287                     if (cur_dist < 2 * rs_val) {
288                         // Spheres are overlapping, integrate
289                         // backward.
290                         cur_t->propagate_for(-h / 2);
291                     } else {
292                         // Spheres are apart, integrate forward.
293                         cur_t->propagate_for(h / 2);
294                     }
295                 } else {
296                     // Sphere regime -> Kepler regime.
297                     if (cur_dist < 2 * rs_val) {
298                         // Spheres are overlapping, integrate
299                         // forward.
300                         cur_t->propagate_for(h / 2);
301                     } else {
302                         // Spheres are apart, integrate backward.
303                         cur_t->propagate_for(-h / 2);
304                     }
305                 }
306                 // Update h and cur_dist.
307                 h /= 2;
308                 cur_dist = std::sqrt(compute_dist2(cur_t->get_state()));
309             }
310 
311             // Copy the current state to the other integrator.
312             other_t->set_time(cur_t->get_time());
313             std::copy(cur_t->get_state().begin(), cur_t->get_state().end(), other_t->get_state_data());
314 
315             // Swap around, update regime.
316             std::swap(other_t, cur_t);
317             cur_regime = !cur_regime;
318         } else {
319             const auto cur_energy
320                 = cur_regime ? tbp_energy(cur_t->get_state()) : tus_energy(rs_val, cur_t->get_state());
321             REQUIRE(std::abs((en - cur_energy) / en) <= 1E-8);
322             REQUIRE(std::abs((am - compute_am(cur_t->get_state())) / am) <= 1E-8);
323         }
324     }
325 }
326