From 76f6245986c7e6cc78386adfc4f39b66bf76f248 Mon Sep 17 00:00:00 2001
From: jakut77 <jakut77@zedat.fu-berlin.de>
Date: Fri, 23 Feb 2024 00:16:10 +0100
Subject: [PATCH] BHT unit test

---
 jobs/src/bht_algorithm.py                     |   3 -
 .../bht.py => jobs/src/bht_algorithm_2d.py    |   9 +-
 jobs/{unittests => tests}/__init__.py         |   0
 jobs/{unittests => tests}/test_a_job.py       |   0
 jobs/tests/test_bht_algorithm.py              | 150 ++++++++++++++++++
 jobs/tests/test_integrator.py                 | 139 ++++++++++++++++
 .../test_jpl_data_query.py                    |   0
 jobs/unittests/test_bht_algorithm.py          |   0
 jobs/unittests/test_integrator.py             |   5 -
 tasks/bht/2D/{bht.py => bht_quad.py}          |   0
 tasks/bht/{ => 3D}/bht_oct.py                 |   0
 tasks/bht/{ => 3D}/galaxysimulation.py        |   0
 tasks/bht/{ => 3D}/simulate.py                |   0
 13 files changed, 294 insertions(+), 12 deletions(-)
 delete mode 100644 jobs/src/bht_algorithm.py
 rename tasks/bht/bht.py => jobs/src/bht_algorithm_2d.py (98%)
 rename jobs/{unittests => tests}/__init__.py (100%)
 rename jobs/{unittests => tests}/test_a_job.py (100%)
 create mode 100644 jobs/tests/test_bht_algorithm.py
 create mode 100644 jobs/tests/test_integrator.py
 rename jobs/{unittests => tests}/test_jpl_data_query.py (100%)
 delete mode 100644 jobs/unittests/test_bht_algorithm.py
 delete mode 100644 jobs/unittests/test_integrator.py
 rename tasks/bht/2D/{bht.py => bht_quad.py} (100%)
 rename tasks/bht/{ => 3D}/bht_oct.py (100%)
 rename tasks/bht/{ => 3D}/galaxysimulation.py (100%)
 rename tasks/bht/{ => 3D}/simulate.py (100%)

diff --git a/jobs/src/bht_algorithm.py b/jobs/src/bht_algorithm.py
deleted file mode 100644
index c6630ca..0000000
--- a/jobs/src/bht_algorithm.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""
-This module implements the Barnes Hut Tree Algorithm
-"""
\ No newline at end of file
diff --git a/tasks/bht/bht.py b/jobs/src/bht_algorithm_2d.py
similarity index 98%
rename from tasks/bht/bht.py
rename to jobs/src/bht_algorithm_2d.py
index 30f345a..071da04 100644
--- a/tasks/bht/bht.py
+++ b/jobs/src/bht_algorithm_2d.py
@@ -5,7 +5,7 @@ class MainApp:
 
     def __init__(self):
         """Initialize the MainApp with a root node."""
-        self.rootNode = TreeNode(x=0, y=0, width=100, height=100)
+        self.rootNode = TreeNode(x=-1, y=-1, width=2, height=2)
 
     def BuildTree(self, particles):
         """Build the Quadtree by inserting particles.
@@ -19,7 +19,7 @@ class MainApp:
 
     def ResetTree(self):
         """Reset the Quadtree by reinitializing the root node."""
-        self.rootNode = TreeNode(x=0, y=0, width=100, height=100)
+        self.rootNode = TreeNode(x=-1, y=-1, width=2, height=2)
 
 
 class Particle:
@@ -263,11 +263,12 @@ class TreeNode:
         np.ndarray: The gravitational force vector between particle1 and particle2.
         """
         #G = 6.674 * (10 ** -11)  # Gravitational constant
-        G = 1
+        #G = 1
+        G = 4 * np.pi**2  # AU^3 / m / yr^2
 
         dx = particle2.x - particle1.x
         dy = particle2.y - particle1.y
-        cutoff_radius = 5
+        cutoff_radius = 0
         r = max(np.sqrt(dx**2 + dy**2), cutoff_radius)
 
         force_magnitude = G * particle1.mass * particle2.mass / (r**2)
diff --git a/jobs/unittests/__init__.py b/jobs/tests/__init__.py
similarity index 100%
rename from jobs/unittests/__init__.py
rename to jobs/tests/__init__.py
diff --git a/jobs/unittests/test_a_job.py b/jobs/tests/test_a_job.py
similarity index 100%
rename from jobs/unittests/test_a_job.py
rename to jobs/tests/test_a_job.py
diff --git a/jobs/tests/test_bht_algorithm.py b/jobs/tests/test_bht_algorithm.py
new file mode 100644
index 0000000..c25c8ad
--- /dev/null
+++ b/jobs/tests/test_bht_algorithm.py
@@ -0,0 +1,150 @@
+"""
+This unittest tests implementation of the BHT algorithm
+for the restricted three-body (sun-earth-moon) problem.
+The sun is fixed at the origin (center of mass). For 
+simplicity, the moon is assumed to be in the eliptic plane,
+so that vectors can be treated as two-dimensional. 
+"""
+
+import logging
+import unittest
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+from jobs.src.integrators import verlet
+from jobs.src.system import GravitationalSystem
+from jobs.src.bht_algorithm_2d import MainApp, Particle
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+# system settings
+R_SE = 1  # distance between Earth and the Sun, 1 astronomical unit (AU)
+R_L = 2.57e-3 * R_SE  # distance between Earth and the Moon
+
+M_S = 1  # solar mass
+M_E = 3.00e-6 * M_S
+M_L = 3.69e-8 * M_S
+
+T_E = 1  # earth yr
+T_L = 27.3 / 365.3 * T_E
+
+G = 4 * np.pi**2  # AU^3 / m / yr^2
+
+# simulation settings
+T = 0.3  # yr
+n_order = 6
+dt = 4**(-n_order)  # yr
+
+
+# force computation
+def force(q: np.ndarray) -> np.ndarray:
+    r1 = q[0:2]  # Earth coordinates
+    r2 = q[2:4]  # Moon coordinates
+
+    sun = Particle(0, 0, M_S)
+    earth = Particle(r1[0], r1[1], M_E)
+    moon = Particle(r2[0], r2[1], M_L)
+
+    particles = [sun, earth, moon]
+
+    barnes_hut = MainApp()  # Initialize Barnes-Hut algorithm instance
+    barnes_hut.BuildTree(particles)  # Build the Barnes-Hut tree with particles
+    barnes_hut.rootNode.ComputeMassDistribution()  #Compute the center of mass of the tree nodes
+
+    f_earth = barnes_hut.rootNode.CalculateForceFromTree(earth)
+    f_moon = barnes_hut.rootNode.CalculateForceFromTree(moon)
+
+    return np.concatenate((f_earth, f_moon), axis=0)
+
+
+class IntegratorTest(unittest.TestCase):
+
+    def test_verlet(self):
+        """
+        Test functionalities of velocity-Verlet algorithm
+        """
+        # vector of r0 and v0
+        x0 = np.array([
+            R_SE,
+            0,
+            R_SE + R_L,
+            0,
+            0,
+            R_SE * 2 * np.pi / T_E,
+            0,
+            1 / M_E * M_E * R_SE * 2 * np.pi / T_E + 1 * R_L * 2 * np.pi / T_L,
+        ])
+
+        system = GravitationalSystem(r0=x0[:4],
+                                     v0=x0[4:],
+                                     m=np.array([M_E, M_E, M_L, M_L]),
+                                     t=np.linspace(0, T, int(T // dt)),
+                                     force=force,
+                                     solver=verlet)
+
+        t, p, q = system.direct_simulation()
+
+        ## checking total energy conservation
+        H = np.linalg.norm(p[:,:2], axis=1)**2 / (2 * M_E) + np.linalg.norm(p[:,2:], axis=1)**2 / (2 * M_L) + \
+            -G * M_S * M_E / np.linalg.norm(q[:,:2], axis=1) - G * M_S * M_L / np.linalg.norm(q[:,2:], axis=1) + \
+            -G * M_E * M_L / np.linalg.norm(q[:,2:] - q[:,:2], axis=1)
+
+        self.assertTrue(np.greater(1e-10 + np.zeros(H.shape[0]), H - H[0]).all())
+
+        ## checking total linear momentum conservation
+        P = p[:, :2] + p[:, 2:]
+        self.assertTrue(np.greater(1e-10 + np.zeros(P[0].shape), P - P[0]).all())
+
+        ## checking total angular momentum conservation
+        L = np.cross(q[:, :2], p[:, :2]) + np.cross(q[:, 2:], p[:, 2:])
+        self.assertTrue(np.greater(1e-10 + np.zeros(L.shape[0]), L - L[0]).all())
+
+        ## checking error
+        dts = [dt, 2 * dt, 4 * dt]
+        errors = []
+        ts = []
+        for i in dts:
+            system = GravitationalSystem(r0=x0[:4],
+                                         v0=x0[4:],
+                                         m=np.array([M_E, M_E, M_L, M_L]),
+                                         t=np.linspace(0, T, int(T // i)),
+                                         force=force,
+                                         solver=verlet)
+
+            t, p_t, q_t = system.direct_simulation()
+
+
+            H = np.linalg.norm(p_t[:,:2], axis=1)**2 / (2 * M_E) + np.linalg.norm(p_t[:,2:], axis=1)**2 / (2 * M_L) + \
+            -G * M_S * M_E / np.linalg.norm(q_t[:,:2], axis=1) - G * M_S * M_L / np.linalg.norm(q_t[:,2:], axis=1) + \
+            -G * M_E * M_L / np.linalg.norm(q_t[:,2:] - q_t[:,:2], axis=1)
+
+            errors.append((H - H[0]) / i**2)
+            ts.append(t)
+
+        plt.figure()
+        plt.plot(ts[0], errors[0], label="dt")
+        plt.plot(ts[1], errors[1], linestyle='--', label="2*dt")
+        plt.plot(ts[2], errors[2], linestyle=':', label="4*dt")
+        plt.xlabel("$t$")
+        plt.ylabel("$\delta E(t)/(\Delta t)^2$")
+        plt.legend()
+        plt.show()
+
+        ## checking time reversal: p -> -p
+        x0 = np.concatenate((q[-1, :], -1 * p[-1, :] / np.array([M_E, M_E, M_L, M_L])), axis=0)
+        system = GravitationalSystem(r0=x0[:4],
+                                     v0=x0[4:],
+                                     m=np.array([M_E, M_E, M_L, M_L]),
+                                     t=np.linspace(0, T, int(T // dt)),
+                                     force=force,
+                                     solver=verlet)
+
+        t, p_reverse, q_reverse = system.direct_simulation()
+        self.assertTrue(np.greater(1e-10 + np.zeros(4), q_reverse[-1] - q[0]).all())
+        self.assertTrue(np.greater(1e-10 + np.zeros(4), p_reverse[-1] - p[0]).all())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/jobs/tests/test_integrator.py b/jobs/tests/test_integrator.py
new file mode 100644
index 0000000..f772a13
--- /dev/null
+++ b/jobs/tests/test_integrator.py
@@ -0,0 +1,139 @@
+"""
+This unittest tests implementation of integrators
+for the restricted three-body (sun-earth-moon) problem.
+The sun is fixed at the origin (center of mass). For 
+simplicity, the moon is assumed to be in the eliptic plane,
+so that vectors can be treated as two-dimensional. 
+"""
+
+import logging
+import unittest
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+from jobs.src.integrators import verlet
+from jobs.src.system import GravitationalSystem
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+# system settings
+R_SE = 1  # distance between Earth and the Sun, 1 astronomical unit (AU)
+R_L = 2.57e-3 * R_SE  # distance between Earth and the Moon
+
+M_S = 1  # solar mass
+M_E = 3.00e-6 * M_S
+M_L = 3.69e-8 * M_S
+
+T_E = 1  # earth yr
+T_L = 27.3 / 365.3 * T_E
+
+G = 4 * np.pi**2  # AU^3 / m / yr^2
+
+# simulation settings
+T = 0.3  # yr
+n_order = 6
+dt = 4**(-n_order)  # yr
+
+
+# force computation
+def force(q: np.ndarray) -> np.ndarray:
+    r1 = q[0:2]
+    r2 = q[2:4]
+
+    return np.array([
+        -G * M_E * (M_S * r1 / np.linalg.norm(r1)**3 + M_L * (r1 - r2) / np.linalg.norm(r1 - r2)**3),
+        -G * M_L * (M_S * r2 / np.linalg.norm(r2)**3 + M_E * (r2 - r1) / np.linalg.norm(r2 - r1)**3),
+    ]).flatten()
+
+
+class IntegratorTest(unittest.TestCase):
+
+    def test_verlet(self):
+        """
+        Test functionalities of velocity-Verlet algorithm
+        """
+        # vector of r0 and v0
+        x0 = np.array([
+            R_SE,
+            0,
+            R_SE + R_L,
+            0,
+            0,
+            R_SE * 2 * np.pi / T_E,
+            0,
+            1 / M_E * M_E * R_SE * 2 * np.pi / T_E + 1 * R_L * 2 * np.pi / T_L,
+        ])
+
+        system = GravitationalSystem(r0=x0[:4],
+                                     v0=x0[4:],
+                                     m=np.array([M_E, M_E, M_L, M_L]),
+                                     t=np.linspace(0, T, int(T // dt)),
+                                     force=force,
+                                     solver=verlet)
+
+        t, p, q = system.direct_simulation()
+
+        ## checking total energy conservation
+        H = np.linalg.norm(p[:,:2], axis=1)**2 / (2 * M_E) + np.linalg.norm(p[:,2:], axis=1)**2 / (2 * M_L) + \
+            -G * M_S * M_E / np.linalg.norm(q[:,:2], axis=1) - G * M_S * M_L / np.linalg.norm(q[:,2:], axis=1) + \
+            -G * M_E * M_L / np.linalg.norm(q[:,2:] - q[:,:2], axis=1)
+
+        self.assertTrue(np.greater(1e-10 + np.zeros(H.shape[0]), H - H[0]).all())
+
+        ## checking total linear momentum conservation
+        P = p[:, :2] + p[:, 2:]
+        self.assertTrue(np.greater(1e-10 + np.zeros(P[0].shape), P - P[0]).all())
+
+        ## checking total angular momentum conservation
+        L = np.cross(q[:, :2], p[:, :2]) + np.cross(q[:, 2:], p[:, 2:])
+        self.assertTrue(np.greater(1e-10 + np.zeros(L.shape[0]), L - L[0]).all())
+
+        ## checking error
+        dts = [dt, 2 * dt, 4 * dt]
+        errors = []
+        ts = []
+        for i in dts:
+            system = GravitationalSystem(r0=x0[:4],
+                                         v0=x0[4:],
+                                         m=np.array([M_E, M_E, M_L, M_L]),
+                                         t=np.linspace(0, T, int(T // i)),
+                                         force=force,
+                                         solver=verlet)
+
+            t, p_t, q_t = system.direct_simulation()
+
+
+            H = np.linalg.norm(p_t[:,:2], axis=1)**2 / (2 * M_E) + np.linalg.norm(p_t[:,2:], axis=1)**2 / (2 * M_L) + \
+            -G * M_S * M_E / np.linalg.norm(q_t[:,:2], axis=1) - G * M_S * M_L / np.linalg.norm(q_t[:,2:], axis=1) + \
+            -G * M_E * M_L / np.linalg.norm(q_t[:,2:] - q_t[:,:2], axis=1)
+
+            errors.append((H - H[0]) / i**2)
+            ts.append(t)
+
+        plt.figure()
+        plt.plot(ts[0], errors[0], label="dt")
+        plt.plot(ts[1], errors[1], linestyle='--', label="2*dt")
+        plt.plot(ts[2], errors[2], linestyle=':', label="4*dt")
+        plt.xlabel("$t$")
+        plt.ylabel("$\delta E(t)/(\Delta t)^2$")
+        plt.legend()
+        plt.show()
+
+        ## checking time reversal: p -> -p
+        x0 = np.concatenate((q[-1, :], -1 * p[-1, :] / np.array([M_E, M_E, M_L, M_L])), axis=0)
+        system = GravitationalSystem(r0=x0[:4],
+                                     v0=x0[4:],
+                                     m=np.array([M_E, M_E, M_L, M_L]),
+                                     t=np.linspace(0, T, int(T // dt)),
+                                     force=force,
+                                     solver=verlet)
+
+        t, p_reverse, q_reverse = system.direct_simulation()
+        self.assertTrue(np.greater(1e-10 + np.zeros(4), q_reverse[-1] - q[0]).all())
+        self.assertTrue(np.greater(1e-10 + np.zeros(4), p_reverse[-1] - p[0]).all())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/jobs/unittests/test_jpl_data_query.py b/jobs/tests/test_jpl_data_query.py
similarity index 100%
rename from jobs/unittests/test_jpl_data_query.py
rename to jobs/tests/test_jpl_data_query.py
diff --git a/jobs/unittests/test_bht_algorithm.py b/jobs/unittests/test_bht_algorithm.py
deleted file mode 100644
index e69de29..0000000
diff --git a/jobs/unittests/test_integrator.py b/jobs/unittests/test_integrator.py
deleted file mode 100644
index 6d1ad12..0000000
--- a/jobs/unittests/test_integrator.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""
-This unittest tests implementation of integrators
-for analytically well-understood configurations of
-gravitional n-body systems where n ∈ {2, 3}
-"""
\ No newline at end of file
diff --git a/tasks/bht/2D/bht.py b/tasks/bht/2D/bht_quad.py
similarity index 100%
rename from tasks/bht/2D/bht.py
rename to tasks/bht/2D/bht_quad.py
diff --git a/tasks/bht/bht_oct.py b/tasks/bht/3D/bht_oct.py
similarity index 100%
rename from tasks/bht/bht_oct.py
rename to tasks/bht/3D/bht_oct.py
diff --git a/tasks/bht/galaxysimulation.py b/tasks/bht/3D/galaxysimulation.py
similarity index 100%
rename from tasks/bht/galaxysimulation.py
rename to tasks/bht/3D/galaxysimulation.py
diff --git a/tasks/bht/simulate.py b/tasks/bht/3D/simulate.py
similarity index 100%
rename from tasks/bht/simulate.py
rename to tasks/bht/3D/simulate.py
-- 
GitLab