From 95c359a47ef580a201724c559b8e042506337cd1 Mon Sep 17 00:00:00 2001
From: jakut77 <jakut77@zedat.fu-berlin.de>
Date: Tue, 6 Feb 2024 12:54:11 +0100
Subject: [PATCH] updated to Octree

---
 tasks/bht/2D/bht.py                           | 316 +++++++++++++++
 tasks/bht/2D/galaxysimulation.py              | 147 +++++++
 tasks/bht/2D/simulate.py                      |   5 +
 tasks/bht/__pycache__/bht_oct.cpython-311.pyc | Bin 0 -> 19590 bytes
 .../galaxysimulation.cpython-311.pyc          | Bin 10722 -> 12599 bytes
 tasks/bht/bht_oct.py                          | 362 ++++++++++++++++++
 tasks/bht/galaxysimulation.py                 | 103 +++--
 tasks/bht/print_tree.py                       |   2 +-
 tasks/bht/simulate.py                         |   6 +-
 tasks/bht/system.py                           |  57 +++
 10 files changed, 957 insertions(+), 41 deletions(-)
 create mode 100644 tasks/bht/2D/bht.py
 create mode 100644 tasks/bht/2D/galaxysimulation.py
 create mode 100644 tasks/bht/2D/simulate.py
 create mode 100644 tasks/bht/__pycache__/bht_oct.cpython-311.pyc
 create mode 100644 tasks/bht/bht_oct.py
 create mode 100644 tasks/bht/system.py

diff --git a/tasks/bht/2D/bht.py b/tasks/bht/2D/bht.py
new file mode 100644
index 0000000..30f345a
--- /dev/null
+++ b/tasks/bht/2D/bht.py
@@ -0,0 +1,316 @@
+import numpy as np
+
+
+class MainApp:
+
+    def __init__(self):
+        """Initialize the MainApp with a root node."""
+        self.rootNode = TreeNode(x=0, y=0, width=100, height=100)
+
+    def BuildTree(self, particles):
+        """Build the Quadtree by inserting particles.
+        
+        Args:
+        particles (list): A list of Particle objects to be inserted into the Quadtree.
+        """
+        self.ResetTree()  # Empty the tree
+        for particle in particles:
+            self.rootNode.insert(particle)
+
+    def ResetTree(self):
+        """Reset the Quadtree by reinitializing the root node."""
+        self.rootNode = TreeNode(x=0, y=0, width=100, height=100)
+
+
+class Particle:
+
+    def __init__(self, x, y, mass):
+        """Initialize a Particle with x and y coordinates and mass."""
+        self.x = x
+        self.y = y
+        self.mass = mass
+        self.vx = 0.0  # Velocity component in x direction
+        self.vy = 0.0  # Velocity component in y direction
+        self.fx = 0.0  # Force component in x direction
+        self.fy = 0.0  # Force component in y direction
+
+
+class TreeNode:
+
+    def __init__(self, x, y, width, height):
+        """Initialize a TreeNode representing a quadrant in the Quadtree.
+
+        Args:
+        x (float): x-coordinate of the node.
+        y (float): y-coordinate of the node.
+        width (float): Width of the node.
+        height (float): Height of the node.
+        """
+        self.x = x
+        self.y = y
+        self.width = width
+        self.height = height
+        self.particle = None  # Particle contained in this node
+        self.center_of_mass = np.array([(x + width) / 2, (y + width) / 2])
+        self.total_mass = 0
+        self.children = np.empty(4, dtype=object)  # Children nodes (SW, SE, NW, NE)
+
+    def contains(self, particle):
+        """Check if the particle is within the bounds of this node.
+        
+        Args:
+        particle (Particle): The particle to be checked.
+
+        Returns:
+        bool: True if the particle is within the node's bounds, False otherwise.
+        """
+        return (self.x <= particle.x < self.x + self.width and self.y <= particle.y < self.y + self.height)
+
+    def insert(self, particle):
+        """Insert a particle into the Quadtree.
+        
+        Args:
+        particle (Particle): The particle to be inserted.
+
+        Returns:
+        bool: True if the particle is inserted, False otherwise.
+        """
+        if not self.contains(particle):
+            return False  # Particle doesn't belong in this node
+
+        if self.particle is None and all(child is None for child in self.children):
+            # If the node is empty and has no children, insert particle here.
+            self.particle = particle
+            #print(f'particle inserted: x={round(self.particle.x,2)}, y={round(self.particle.y,2)}')
+            return True  # Particle inserted in an empty node
+
+        if all(child is None for child in self.children):
+            # If no children exist, create and insert both particles
+            self.subdivide()
+            self.insert(self.particle)  # Reinsert existing particle
+            self.insert(particle)  # Insert new particle
+            self.particle = None  # Clear particle from this node
+        else:
+            # If the node has children, insert particle in the child node.
+            quad_index = self.get_quadrant(particle)
+            if self.children[quad_index] is None:
+                # Create a child node if it doesn't exist
+                self.children[quad_index] = TreeNode(self.x + (quad_index % 2) * (self.width / 2),
+                                                     self.y + (quad_index // 2) * (self.height / 2), self.width / 2,
+                                                     self.height / 2)
+            self.children[quad_index].insert(particle)
+
+    def subdivide(self):
+        """Subdivide the node into four quadrants."""
+        sub_width = self.width / 2
+        sub_height = self.height / 2
+        self.children[0] = TreeNode(self.x, self.y, sub_width, sub_height)  # SW
+        self.children[1] = TreeNode(self.x + sub_width, self.y, sub_width, sub_height)  # SE
+        self.children[2] = TreeNode(self.x, self.y + sub_height, sub_width, sub_height)  # NW
+        self.children[3] = TreeNode(self.x + sub_width, self.y + sub_height, sub_width, sub_height)  # NE
+
+    def get_quadrant(self, particle):
+        """Determine the quadrant index for a particle based on its position.
+        
+        Args:
+        particle (Particle): The particle to determine the quadrant index for.
+
+        Returns:
+        int: Quadrant index (0 for SW, 1 for SE, 2 for NW, 3 for NE).
+        """
+        mid_x = self.x + self.width / 2
+        mid_y = self.y + self.height / 2
+        quad_index = (particle.x >= mid_x) + 2 * (particle.y >= mid_y)
+        return quad_index
+
+    def print_tree(self, depth=0):
+        """Print the structure of the Quadtree.
+        
+        Args:
+        depth (int): Current depth in the tree (for indentation in print).
+        """
+        if self.particle:
+            print(
+                f"{' ' * depth}Particle at ({round(self.particle.x,2)}, {round(self.particle.y,2)}) in Node ({self.x}, {self.y}), size={self.width}"
+            )
+        else:
+            print(f"{' ' * depth}Node ({self.x}, {self.y}) - Width: {self.width}, Height: {self.height}")
+            for child in self.children:
+                if child:
+                    child.print_tree(depth + 2)
+
+    def ComputeMassDistribution(self):
+        """Compute the mass distribution for the tree nodes.
+
+        This function calculates the total mass and the center of mass
+        for each node in the Quadtree. It's a recursive function that
+        computes the mass distribution starting from the current node.
+
+        Note:
+        This method modifies the 'mass' and 'center_of_mass' attributes
+        for each node in the Quadtree.
+
+        Returns:
+        None
+        """
+        if self.particle is not None:
+            # Node contains only one particle
+            self.center_of_mass = np.array([self.particle.x, self.particle.y])
+            self.total_mass = self.particle.mass
+        else:
+            # Multiple particles in node
+            total_mass = 0
+            center_of_mass_accumulator = np.array([0.0, 0.0])
+
+            for child in self.children:
+                if child is not None:
+                    # Recursively compute mass distribution for child nodes
+                    child.ComputeMassDistribution()
+                    total_mass += child.total_mass
+                    center_of_mass_accumulator += child.total_mass * child.center_of_mass
+
+            if total_mass > 0:
+                self.center_of_mass = center_of_mass_accumulator / total_mass
+                self.total_mass = total_mass
+            else:
+                # If total mass is 0 or no child nodes have mass, leave values as default
+                pass
+                #self.center_of_mass = np.array([(x+width)/2, (y+width)/2])
+                #self.total_mass = 0
+
+    def CalculateForceFromTree(self, target_particle, theta=1.0):
+        """Calculate the force on a target particle using the Barnes-Hut algorithm.
+
+        Args:
+        target_particle (Particle): The particle for which the force is calculated.
+        theta (float): The Barnes-Hut criterion for force approximation.
+
+        Returns:
+        np.ndarray: The total force acting on the target particle.
+        """
+
+        total_force = np.array([0.0, 0.0])
+
+        if self.particle is not None:
+            # Node contains only one particle
+            if self.particle != target_particle:
+                # Calculate gravitational force between target_particle and node's particle
+                force = self.GravitationalForce(target_particle, self.particle)
+                total_force += force
+        else:
+            if self.total_mass == 0:
+                return total_force
+
+            r = np.linalg.norm(np.array([target_particle.x, target_particle.y]) - self.center_of_mass)
+            d = max(self.width, self.height)
+
+            if d / r < theta:
+                # Calculate gravitational force between target_particle and "node particle" representing cluster
+                node_particle = Particle(self.center_of_mass[0], self.center_of_mass[1], self.total_mass)
+                force = self.GravitationalForce(target_particle, node_particle)
+                total_force += force
+            else:
+                for child in self.children:
+                    if child is not None:
+                        # Recursively calculate force from child nodes
+                        if target_particle is not None:  # Check if the target_particle is not None
+                            force = child.CalculateForceFromTree(target_particle)
+                            total_force += force
+
+        return total_force
+
+    def CalculateForce(self, target_particle, particle, theta=1.0):
+        """Calculate the gravitational force between two particles.
+
+        Args:
+        target_particle (Particle): The particle for which the force is calculated.
+        particle (Particle): The particle exerting the force.
+
+        Returns:
+        np.ndarray: The force vector acting on the target particle due to 'particle'.
+        """
+        force = np.array([0.0, 0.0])
+        print('function CalculateForce is called')
+        if self.particle is not None:
+            # Node contains only one particle
+            if self.particle != target_particle:
+                # Calculate gravitational force between target_particle and node's particle
+                force = self.GravitationalForce(target_particle, self.particle)
+        else:
+            if target_particle is not None and particle is not None:  # Check if both particles are not None
+                r = np.linalg.norm(
+                    np.array([target_particle.x, target_particle.y]) - np.array([particle.x, particle.y]))
+                d = max(self.width, self.height)
+
+                if d / r < theta:
+                    # Calculate gravitational force between target_particle and particle
+                    force = self.GravitationalForce(target_particle, particle)
+                else:
+                    for child in self.children:
+                        if child is not None:
+                            # Recursively calculate force from child nodes
+                            force += child.CalculateForce(target_particle, particle)
+        return force
+
+    def GravitationalForce(self, particle1, particle2):
+        """Calculate the gravitational force between two particles.
+
+        Args:
+        particle1 (Particle): First particle.
+        particle2 (Particle): Second particle.
+
+        Returns:
+        np.ndarray: The gravitational force vector between particle1 and particle2.
+        """
+        #G = 6.674 * (10 ** -11)  # Gravitational constant
+        G = 1
+
+        dx = particle2.x - particle1.x
+        dy = particle2.y - particle1.y
+        cutoff_radius = 5
+        r = max(np.sqrt(dx**2 + dy**2), cutoff_radius)
+
+        force_magnitude = G * particle1.mass * particle2.mass / (r**2)
+        force_x = force_magnitude * (dx / r)
+        force_y = force_magnitude * (dy / r)
+
+        return np.array([force_x, force_y])
+
+    # Helper method to retrieve all particles in the subtree
+    def particles_in_subtree(self):
+        """Retrieve all particles in the subtree rooted at this node.
+
+        Returns:
+        list: A list of particles in the subtree rooted at this node.
+        """
+        particles = []
+        if self.particle is not None:
+            particles.append(self.particle)
+        else:
+            for child in self.children:
+                if child is not None:
+                    particles.extend(child.particles_in_subtree())
+        return particles
+
+    def compute_center_of_mass(self):
+        """Compute the center of mass for the node."""
+        print('Function compute_center_of_mass is called')
+        if self.particle is not None:
+            self.center_of_mass = np.array([self.particle.x, self.particle.y])
+            self.mass = self.particle.mass
+        else:
+            total_mass = 0
+            center_of_mass_accumulator = np.array([0.0, 0.0])
+
+            for child in self.children:
+                if child is not None:
+                    child.compute_center_of_mass()
+                    total_mass += child.mass
+                    center_of_mass_accumulator += child.mass * child.center_of_mass
+
+            if total_mass > 0:
+                self.center_of_mass = center_of_mass_accumulator / total_mass
+                self.mass = total_mass
+            else:
+                self.center_of_mass = np.array([0.0, 0.0])
+                self.mass = 0
diff --git a/tasks/bht/2D/galaxysimulation.py b/tasks/bht/2D/galaxysimulation.py
new file mode 100644
index 0000000..f8aae98
--- /dev/null
+++ b/tasks/bht/2D/galaxysimulation.py
@@ -0,0 +1,147 @@
+import matplotlib.pyplot as plt
+import numpy as np
+from bht import MainApp, Particle
+
+
+# Simulation class for galaxy collision using Barnes-Hut and Euler method
+class GalaxyCollisionSimulation:
+
+    def __init__(self, num_particles):
+        # Initialize the simulation with a given number of particles
+        self.num_particles = num_particles
+        self.particles = self.initialize_particles(initial='random')  # Generate particles
+        self.barnes_hut = MainApp()  # Initialize Barnes-Hut algorithm instance
+        self.barnes_hut.BuildTree(self.particles)  # Build the Barnes-Hut tree with particles
+        self.barnes_hut.rootNode.ComputeMassDistribution()  #Compute the center of mass of the tree nodes
+        self.particle_positions = {
+            i: [(particle.x, particle.y)]
+            for i, particle in enumerate(self.particles)
+        }  # Store particle positions for each time step
+        self.particle_velocities = {
+            i: [(particle.vx, particle.vy)]
+            for i, particle in enumerate(self.particles)
+        }  # Store particle velocities for each time step
+        self.particle_forces = {
+            i: [(particle.fx, particle.fy)]
+            for i, particle in enumerate(self.particles)
+        }  # Store particle velocities for each time step
+        self.system_center_of_mass = {
+            0: (self.barnes_hut.rootNode.center_of_mass[0], self.barnes_hut.rootNode.center_of_mass[1])
+        }
+
+    def initialize_particles(self, initial='random'):
+
+        if initial == 'two':
+            particles = [Particle(80, 50, 1000), Particle(20, 50, 1000)]
+            #Particle(0.5, 0, 10000),
+            #Particle(-0.5, 0, 10000)]
+            particles[0].vx, particles[0].vy = 0, 3
+            particles[1].vx, particles[1].vy = 0, -3
+            #particles=particles[:2]
+            return particles
+
+        elif initial == 'random':
+            # Generate random particles within a specified range
+            particles = []
+            for _ in range(self.num_particles):
+                x = np.random.uniform(0, 100)
+                y = np.random.uniform(0, 100)
+                #vx = 0
+                #vy = 0
+                #mass = np.random.uniform(10, 100)
+                mass = 10000
+                particles.append(Particle(x, y, mass))
+            return particles
+
+    def simulate(self, num_steps, time_step):
+        # Simulate the galaxy collision for a certain number of steps
+        for step in range(num_steps):
+            # For each step, build the tree
+            self.barnes_hut.BuildTree(self.particles)
+            self.barnes_hut.rootNode.ComputeMassDistribution()
+            if step in np.arange(0, num_steps, 1000):
+                print(f'===Quadtree at step {step}===')
+                self.barnes_hut.rootNode.print_tree()
+            self.evaluate_forces()  # Evaluate forces acting on each particle
+            self.integrate(time_step, 'velocity_verlet')  # Integrate to update particle positions
+
+            self.system_center_of_mass[step] = (self.barnes_hut.rootNode.center_of_mass[0],
+                                                self.barnes_hut.rootNode.center_of_mass[1])
+            # Store particle positions, velocities and forces for each time step
+            for i, particle in enumerate(self.particles):
+                self.particle_positions[i].append((particle.x, particle.y))
+                self.particle_velocities[i].append((particle.vx, particle.vy))
+                self.particle_forces[i].append((particle.fx, particle.fy))
+
+    def evaluate_forces(self):
+        # Evaluate forces acting on each particle using Barnes-Hut algorithm
+        for particle in self.particles:
+            force = self.barnes_hut.rootNode.CalculateForceFromTree(particle)
+            particle.fx, particle.fy = force
+
+    def calculate_force(self, particle):
+        # Calculate force acting on a single particle
+        force = self.barnes_hut.rootNode.CalculateForceFromTree(particle)
+        particle.fx, particle.fy = force
+
+    def integrate(self, time_step, method):
+        # Integrate to update particle positions based on calculated forces
+        if method == 'explicit_euler':
+            for particle in self.particles:
+                particle.vx += particle.fx * time_step / particle.mass  # Update velocity components
+                particle.vy += particle.fy * time_step / particle.mass
+                particle.x += particle.vx * time_step  # Update particle positions using velocity
+                particle.y += particle.vy * time_step
+
+        elif method == 'velocity_verlet':
+            for particle in self.particles:
+                particle.vx += particle.fx * time_step / (2 * particle.mass)  # First half-step
+                particle.vy += particle.fy * time_step / (2 * particle.mass)
+                particle.x += particle.vx * time_step  # Full step for position
+                particle.y += particle.vy * time_step
+
+                self.calculate_force(particle)  # Recalculate force for the particle
+
+                particle.vx += particle.fx * time_step / (2 * particle.mass)  # Second half-step
+                particle.vy += particle.fy * time_step / (2 * particle.mass)
+
+    def print_particle_positions(self):
+        # Print the positions of all particles for each time step
+        for i, positions in self.particle_positions.items():
+            print(f"Particle {i + 1} Positions:")
+            for step, pos in enumerate(positions):
+                print(f"  Time Step {step}: Position ({pos[0]}, {pos[1]})")
+
+    def display_snapshots(self, num_shots, fix_axes=True):
+        # Display pictures for each time step
+        num_time_steps = len(next(iter(self.particle_positions.values())))
+        print('===Quadtree at the end===')
+        self.barnes_hut.rootNode.print_tree()  # print the tree
+        axes_type = "fixed" if fix_axes else "unfixed"
+        #Fixed axis limits
+        print(f"For {axes_type} axis limits, num_particles={self.num_particles}")
+        for step in np.arange(0, num_time_steps, int(num_time_steps / num_shots)):
+            fig, ax = plt.subplots()
+            if fix_axes:
+                ax.set_xlim(0, 100)
+                ax.set_ylim(0, 100)
+            ax.set_xlabel('X-axis')
+            ax.set_ylabel('Y-axis')
+            ax.set_title(f'Time Step {step}')
+            ax.grid()
+
+            print(f'Step {step}')
+            for particle_index, positions in self.particle_positions.items():
+                x, y = positions[step]
+                vx, vy = self.particle_velocities[particle_index][step]
+                fx, fy = self.particle_forces[particle_index][step]
+                #mass = self.particles[particle_index].mass
+                ax.scatter(x, y, c='blue', s=20, alpha=0.5)  # Plot particle position for the current time step
+                c_x, c_y = self.system_center_of_mass[step]
+                ax.scatter(c_x, c_y, c='orange', s=20)
+                print(
+                    f'i={particle_index}: x={round(x,2)}, y={round(y,2)}, vx={round(vx,2)}, vy={round(vy,2)}, fx={round(fx,2)}, fy={round(fy,2)}'
+                )
+                #print(y)
+
+            plt.show()
diff --git a/tasks/bht/2D/simulate.py b/tasks/bht/2D/simulate.py
new file mode 100644
index 0000000..c703e42
--- /dev/null
+++ b/tasks/bht/2D/simulate.py
@@ -0,0 +1,5 @@
+from galaxysimulation import GalaxyCollisionSimulation
+
+sim = GalaxyCollisionSimulation(num_particles=6)
+sim.simulate(num_steps=7000, time_step=0.001)
+sim.display_snapshots(200, fix_axes=True)
diff --git a/tasks/bht/__pycache__/bht_oct.cpython-311.pyc b/tasks/bht/__pycache__/bht_oct.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..040eedbf568386874364ac28f4d803d8393a67f5
GIT binary patch
literal 19590
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

literal 0
HcmV?d00001

diff --git a/tasks/bht/__pycache__/galaxysimulation.cpython-311.pyc b/tasks/bht/__pycache__/galaxysimulation.cpython-311.pyc
index 217cfe8eeb559192f49de5628658625e9ff730ff..1a9755185c2906f2b2ed6f525202579068b0e3dc 100644
GIT binary patch
delta 5767
zcmaD9ygi9;IWI340|NuYDd|J06O=acF*9<pGB7YOGcYiGj+-pa7&<wMNosQ)<0i(*
zEX<0E9Fic#V4TLB$ymb>&kN=-FfgPr1T$!IPIhBf6lG&zU=RkG;K0DZFr6Wjp@tz=
zdh!7lCFL5XEGC$SH0E^1$xJ;m!3--IG?{NPR@`E&yv0~Gc@y(WM!U%|EE-%ZB-Th>
z5HPvGZ#ub)1&^_|*o`gXW?%rB%v4r!i>a*g7E@VO5kCV1LovvA3JMBEAU76?Oy*~G
zV6>YYz^dV}!eWi}1p)I5{1zXWnN_$xFu;f+Q3eKvWJZ|N85kIF`jL=h(kgB-rB&Wy
zO2g$CYc>gxBkd-;uxV(axJUuyA}9fNQ4uIGUxJ+Vl4)`;+e=;rkWP>w$aj;Q+07Vr
zChua`;98)vMDL=M;T0*v$<Nqt)ZY*gnZkcXK;w#l`3}Y_0#+ZG83k-UFf*%hb+BCF
zP`Us{5F0o^9tW8S^E)Rjc?hwBBDaPyg)ti}UsO^8atTyj3R4Pm3QG#>G8P7g)htj!
z28aq32sec#g*k-@T}e7qEmI0x4D)1fPNT`Gocz3?*aB-v;fP_L+{kHSlfsEehVgLe
z8m3@|8paeZh-)S=7D=Zw)H0QT+zZyuz)-`G#X5PTtQe0A!vw}y##-hQ9tMWVKV>zT
zQkW+zatSj-xe_VNDcsAL7#LQAJqFTU%L-GAO_B{E$&<p1rjs3?BnM0~D+N<0X9{aA
z7bM6)ZeU=jVX0xQVXNUn4b)oh8ul9QczEE~aMW<e!(Cp(S;LJSNDDY8KV%fHXQ<&`
z#>l|18m^0xVFF_hGc*wSAc0u)1le|cQ3`P%_83Jm0~F)@DXbQ#UO-dAk|KbSELdup
zQUtLk3n56dP{5unQiNle85nAqQbZ;Pa*8vGOrFmvrjR0nEd`~BLY0bvm5PCs)~ASJ
zQwj<vaET}GcZ<2CJpZK)0|UcLBM_khB3>|qik_FCvh*b=g%+7HFff3#t0r@iDM-cN
z|Ns9(MCCwIpnUdn8c5<dNCKqf7E4-wX?@X4H;^buktQ?P1c)w>fiMAckO@!$FdG#6
zMPQaZxD147L5PCYautE{Wsx{YGpNWZ0(tZnYf)ledTJ4<B)P?uS1{R}TRf7dNEf6C
zl$DB<7#J9CF~%41fJ8v0e-R&ug=Pv%Zenq9kr1dj0x48L0+3=?k%57sN^i0Pi`3-B
z+#>ZLZgDUZ149GD18$)Ssh7B=FK|oWl~i1yv><7P(u$OelEzmgjkiYZNIIZ&AnAn4
zMN|JPrv4Ws0y;czIJjTrvF-4jkl5qX<3A&ELE)U3Iq@qpcQCHWS(AUm!2^%%17Vp9
zJTedJc|~S$T;f%@$gA)`LaM{_uBg-ulPl7?7e)21i0XY{;N{i+2qGph-W3s>;(1*}
z^^%C{f}|D77e(~0i0F|lEGj-F^17(nB~i5nMJo(1iq`915!Ic*bQfJ5ukZtL=?N@%
zrIi;rE+|}~xFY1Dw9yr5qX`^0<P;`wOyNeO1SGEuC|nXySRipxK<kQt)&&8rivn6m
z{%5<yD|dlc?x}#p451l83xpPgKos63UEl*Jt0Z5C=La4JM@A1uJi!l(oO)bw=ShTS
z?0O+S`yk1{Cx3xg4qTZfgK{FIq+(!Tht*?k%nS^ZFYqX{tgmIRnOq~HG`U8Wd-5$l
zzR5>;B{zHXCNoZM;#1^h2jvck<}~KX8~CglXHR~^=ciP|ngwb{fMwE{(aY2thAffE
z0sOk=pauk3O$}?77?{Dpz)-`Q1<E&IE>^t~AieHzn`>CJq`-<HdZl4(OuapGCNTDF
z1eZ`8evnebWU_#Ofi|cdDFPP?GDWrw3=BoeAOch)gL8I~Du}BFBJ3uo30MbfgP1xX
zLLEea3b!IqvkpD~Yl37!o+*MCB}G~wF_0sRWI!xgP*KEIq&xYkfP$DHNCaFyfLc<J
z;-N@*vb3O~q7SIlSkDM53!aKd&d|IlqH;w<<pYBtX9VL%5IH$l@Tm0-1CuTMR}4I^
z7=&Mlh`eGD^?{YiAcnES<qD75f|83oI$t>$6f{?etdYAYZ*fK5;sXOSZv-QloE#&h
zV0psgjMW9Bu!{oWR|LW@2!!7d5V<0tu|n{QfcA9(qe}ut8$7mnACNdAeInyb_66gp
zivrPC1fnkpL{DBTBpG%><BawNeT41{0^uLo!4}AZoC$F(B+<cZSOx|LP<;SOgrA$h
zE#n%7ELapWxG=<;O_mULW@MfmBP^v{!x#?=7_fAYIVd7mGWuz<Lm~+rXR4EXg=I`p
zgLfq}q@)G~>@C)`{G#Mk)Yh&CsMys66}y_LWr;bZi6yD=5M_y=Q1@g2h57>tnK^tP
z7??P%z{F%35ot>k%ZogQ9UeX2GbH9nFUVMuy&(GoD@+B%qoC>q>|O>222jlc_GsSZ
z91%BA8?J^C)(Qdn&jRG5B7aDVfOrR#AOb)Hs+WoaK@J0%1#YrW61A^S2Z<O!oC<Bp
zUKg;xBw&Al<D!7)6#>r<)*A+<7ddo0IC{7zL`;cZps_@If%XDzh&w@M!Q2T7V-Wu{
z6AQRC%LH%DZb6HjRkh4D%oU7Tj0}@^iFPrvPd+NDIyp*=kC9`trI;c=sQv~U3Rc89
zIa5qA9#mf<#K8S55VwXo3uG;b$xy>s#mc}?#mm5u%oGn-(X$#9JT*)yOptci1jZt3
zROd`!j5V3OQB-O2Tk)L9#$tku?34M$weh>hmTdRbFf8B(wHhby5}%K-jTi2V35-1^
zlcObM>Rmwb2QFQBASo4GB7h^*2_ywB8^DDDs7xyYl?dnsNeC!Sut1WrH>j<_mYZ6V
zk)Hx-On}lAsIY)EB0$NtO4&UzC$XZ^IX@?7vKEVgZf0IdYI;#(Noo!#ndm^<6oMB-
zRWEU?Uf@=}A)_$I?*juHuh|C%w#h1zE|a|^JtoUYiEB*Y`oIej<~92uz`!SRL0sb!
zuf|1Q4X7R-USGzK3_O!}N%~LDlaimzDy27BMyhafmXxy5MD7pL41Agwcr`vSfz*9q
z0FjUshg<}JnoQsn_igfiDGSE9$v>om8I33VNZZ@8*RZ4@^{{6%q;SsVM6XeLOnTx$
zsY8>?uZlAuv8W_7IVV-2NN(~rY11HZ$^MH`Q<J3#R8rq!%`8dHEiMAJAd5idw<aqj
zU_qttEzW}c;>?oF{Ji2KP?39!xgfu|7*s-m0vOUez9lg^M@C|DfQ$)S8z?llOzx7A
zW)A}uqnu%r*T|UEgV>C`VT=<wC$LONzAGSlK`dlJ$OSPF+EBd1a7!74eL(m?;1Mwh
z`-Jg{!ZT(N_64z!ivpon1VS%>(FbObVINqTIY9=4NN@;-pj6$Ud;$)_9+u6)vbl_m
zeUlH$sj=0v)vzt#2c=VKh7|S`jy0U<J+T_5BB#mn^3sf4lda`7IoVRUASxzj%PS-C
zrphZba!=kbuOXJggC6`f><dIeR){mC@M4h@1IfuUr0~_S&Ssd)ghhooNX29YIibli
z3OtMwP#!;v3qx${WPb&5P+3{aRm)w=Q^SxYIk`|l5iG)6%U8=^!;mEf7ZIoxtQD$Z
zUmy)K!j++htA@LVCxt&nAVqME&@wg#hSl)=$q4HDg0dZ?%wYv(J}{F3)ZwWWt`(^j
zg_)$wP$OI;QX`rooFcGBWEm?1!)j2P04rx?=xHmF1~b7#mh9w<a$-E-uHyu7q7<E+
zs}$<-G9Q$2nT%638E<hF6y;~7CWF%1%M+m5hb=-ku_Cj$iY*dKM?q+=kj&gvh2WCZ
z0)<;FNjasdFGVJQQc?q#_C*<>QbFz(b53gBEtb60ijrF_nI)-3kW|hFs>xEJ$sAFc
zxPwX@P(P-~4#aW=xrsS5ujCeUK~BjnwzSOj(xTK`+=(eE@x`S{1v&X8w>XMZOX4eX
zGIJrU%ACwxb_la-@-Af=HZG8S)#MAx;%ZSKGr_GMP&4NidvS7N3CKmDw$d$@;*9)q
zO|c@~$s#Hyj6Rb+Rb*qMK^j1vp(0QbfUVb51PWS2Tj>^aa(o4ds00yJ#h_**IBQ}M
zMZ%L;t2ndug0iUX<S#1AjifonKQc&jig&o&6%w7IHBo1ZPKWaiL6Pb5ljIi&ZxERv
ze^JovilAAC(+z%cP@_m?@&#4V$rn{cCSO#wp8QMIto{QVgPze9U6<>+UYB&eF6#PR
z(e?Sjz|Ly}CN2tSPT*U>c|%Hhfy*5IiR=>?Cm2phydkT2Mb>mf-W6Hb11VQzy(e-_
zV4C1Gg9$8dxIyoVtm6TdE3)n|@dpAz)43*b%`lvixIl10+6JZ_$`iOQ3fNr{unV{#
zV0S}7<%)vU4wEYi4ikANFi!}cp?m|X!*~Jng5VV%JD5P~t*;1JUjU=8JPf=7JszE2
zJzgDNon9Zzr8&hvfC8dObn-QI(|SEnB!D`bMZO>ws5w*ws&R@uLEKmn(GDU&Lx<p!
z2U5|1a#9h90QDM*7l4}{4Ga+Yfklj!>jMLvFwkU`{lI`qgfR-Uf*POo5DH9cv9MZw
zV1N@m%&cY~7~lk#1gkIO2L>e4gq0OkCL)o1%B<oa7;q2}*Mr=w$$3jKH?ahiz$bHP
z3QH7#G6HK}X>LI!IA`2qPs%8X&rhDLuPG)9>ZgGH{ENdTH$SB`C)KW~e{z(jVYuoA
b1}x|UM<x@a_y-2;<VUdh7chyb2JA!tr))4b

delta 4599
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
SfdM=D5iI@%Ok%15TLu6W3my~z

diff --git a/tasks/bht/bht_oct.py b/tasks/bht/bht_oct.py
new file mode 100644
index 0000000..c55b43f
--- /dev/null
+++ b/tasks/bht/bht_oct.py
@@ -0,0 +1,362 @@
+import numpy as np
+
+
+class MainApp:
+
+    def __init__(self, particles):
+        """Initialize the MainApp with a root node."""
+        self.rootNode = TreeNode(x=0, y=0, z=0, width=200, height=200, depth=200)
+
+    def BuildTree(self, particles):
+        """Build the Octree by inserting particles.
+        
+        Args:
+        particles (list): A list of Particle objects to be inserted into the Octree.
+        """
+        self.ResetTree(particles)  # Empty the tree
+        for particle in particles:
+            self.rootNode.insert(particle)
+
+    def ResetTree(self, particles):
+        """Reset the Quadtree by reinitializing the root node."""
+        # Define the size of the rootNode based on the positions of the particles
+        #min_x = min([particle.x for particle in particles])
+        #min_y = min([particle.y for particle in particles])
+        #min_z = min([particle.z for particle in particles])
+        #max_x = max([particle.x for particle in particles])
+        #max_y = max([particle.y for particle in particles])
+        #max_z = max([particle.z for particle in particles])
+        #root_height = max_y - min_y
+        #root_width = max_x - min_x
+        #root_depth = max_z - min_z
+        #self.rootNode = TreeNode(x=min_x, y=min_y, z=min_z, width=root_width, height=root_height, depth=root_depth)
+        self.rootNode = TreeNode(x=0, y=0, z=0, width=200, height=200, depth=200)
+
+
+class Particle:
+
+    def __init__(self, x, y, z, mass):
+        """Initialize a Particle with x, y, z coordinates and mass."""
+        self.x = x
+        self.y = y
+        self.z = z
+        self.mass = mass
+
+        self.vx = 0.0  # Velocity component in x direction
+        self.vy = 0.0  # Velocity component in y direction
+        self.vz = 0.0  # Velocity component in z direction
+
+        self.fx = 0.0  # Force component in x direction
+        self.fy = 0.0  # Force component in y direction
+        self.fz = 0.0  # Force component in z direction
+
+
+class TreeNode:
+
+    def __init__(self, x, y, z, width, height, depth):
+        """Initialize a TreeNode representing a octant in the Octree.
+
+        Args:
+        x (float): x-coordinate of the node.
+        y (float): y-coordinate of the node.
+        z (float): z-coordinate of the node.
+        width (float): Width of the node.
+        height (float): Height of the node.
+        depth (float): Depth of the node.
+        """
+        self.x = x
+        self.y = y
+        self.z = z
+        self.width = width
+        self.height = height
+        self.depth = depth
+        self.particle = None  # Particle contained in this node
+        self.center_of_mass = np.array([(x + width) / 2, (y + height) / 2, (z + depth) / 2])
+        self.total_mass = 0  # Total mass contained in the node
+        self.children = np.empty(8, dtype=object)  # Children nodes (F-SW, F-SE, F-NW, F-NE, B-SW, B-SE, B-NW, B-NE,)
+
+    def contains(self, particle):
+        """Check if the particle is within the bounds of this node.
+        
+        Args:
+        particle (Particle): The particle to be checked.
+
+        Returns:
+        bool: True if the particle is within the node's bounds, False otherwise.
+        """
+        return (self.x <= particle.x <= self.x + self.width and self.y <= particle.y <= self.y + self.height
+                and self.z <= particle.z <= self.z + self.depth)
+
+    def insert(self, particle):
+        """Insert a particle into the Octree.
+        
+        Args:
+        particle (Particle): The particle to be inserted.
+
+        Returns:
+        bool: True if the particle is inserted, False otherwise.
+        """
+        if not self.contains(particle):
+            return False  # Particle doesn't belong in this node
+
+        if self.particle is None and all(child is None for child in self.children):
+            # If the node is empty and has no children, insert particle here.
+            self.particle = particle
+            #print(f'particle inserted: x={round(self.particle.x,2)}, y={round(self.particle.y,2)}, , z={round(self.particle.z,2)}')
+            return True  # Particle inserted in an empty node
+
+        if all(child is None for child in self.children):
+            # If no children exist, create and insert both particles
+            self.subdivide()
+            self.insert(self.particle)  # Reinsert existing particle into subnode
+            self.insert(particle)  # Insert new particle
+            self.particle = None  # Clear particle from this node
+        else:
+            # If the node has children, insert particle in the child node.
+            oct_index = self.get_octant(particle)
+            if self.children[oct_index] is None:
+                print('Missing node:', self.children)
+                # Create a child node if it doesn't exist
+                self.children[oct_index] = TreeNode(self.x + (oct_index % 2) * (self.width / 2),
+                                                    self.y + (oct_index // 2) * (self.height / 2),
+                                                    self.z + (oct_index // 2) * (self.depth / 2), self.width / 2,
+                                                    self.height / 2, self.depth / 2)
+            self.children[oct_index].insert(particle)
+
+    def subdivide(self):
+        """Create the children of the node."""
+        sub_width = self.width / 2
+        sub_height = self.height / 2
+        sub_depth = self.depth / 2
+        self.children[0] = TreeNode(self.x, self.y, self.z, sub_width, sub_height, sub_depth)  # B-SW
+        self.children[1] = TreeNode(self.x + sub_width, self.y, self.z, sub_width, sub_height, sub_depth)  # B-SE
+        self.children[2] = TreeNode(self.x, self.y + sub_height, self.z, sub_width, sub_height, sub_depth)  # B-NW
+        self.children[3] = TreeNode(self.x + sub_width, self.y + sub_height, self.z, sub_width, sub_height,
+                                    sub_depth)  # B-NE
+        self.children[4] = TreeNode(self.x, self.y, self.z + sub_depth, sub_width, sub_height, sub_depth)  # T-SW
+        self.children[5] = TreeNode(self.x + sub_width, self.y, self.z + sub_depth, sub_width, sub_height,
+                                    sub_depth)  # T-SE
+        self.children[6] = TreeNode(self.x, self.y + sub_height, self.z + sub_depth, sub_width, sub_height,
+                                    sub_depth)  # T-NW
+        self.children[7] = TreeNode(self.x + sub_width, self.y + sub_height, self.z + sub_depth, sub_width, sub_height,
+                                    sub_depth)  # T-NE
+
+    def get_octant(self, particle):
+        """Determine the octant index for a particle based on its position.
+        
+        Args:
+        particle (Particle): The particle to determine the octant index for.
+
+        Returns:
+        int: Octant index 
+        B - Bottom, T - Top
+        (0 for B-SW, 1 for B-SE, 2 for B-NW, 3 for B-NE,
+        4 for T-SW, 5 for T-SE, 6 for T-NW, 7 for T-NE).
+        """
+        # Determine separating planes
+        mid_x = self.x + self.width / 2
+        mid_y = self.y + self.height / 2
+        mid_z = self.z + self.depth / 2
+        quad_index = (particle.x >= mid_x) + 2 * (particle.y >= mid_y) + 4 * (particle.z >= mid_z)
+        return quad_index
+
+    def print_tree(self, depth_=0):
+        """Print the structure of the Octree.
+        
+        Args:
+        depth_ (int): Current depth in the tree (for indentation in print).
+        """
+        if self.particle:
+            print(
+                f"{' ' * depth_}Particle at ({round(self.particle.x,2)}, {round(self.particle.y,2)}, {round(self.particle.z,2)}) in Node ({round(self.x,2)}, {round(self.y,2)}, {round(self.z,2)}), width={round(self.width,2)}, height={round(self.height,2)}, depth={round(self.depth,2)}"
+            )
+        else:
+            print(
+                f"{' ' * depth_}Node ({round(self.x,2)}, {round(self.y,2)}, {round(self.z,2)}) - Width: {round(self.width,2)}, Height: {round(self.height,2)}, Depth: {round(self.depth,2)}"
+            )
+            for child in self.children:
+                if child:
+                    child.print_tree(depth_ + 2)
+
+    def ComputeMassDistribution(self):
+        """Compute the mass distribution for the tree nodes.
+
+        This function calculates the total mass and the center of mass
+        for each node in the Octree. It's a recursive function that
+        computes the mass distribution starting from the current node.
+
+        Note:
+        This method modifies the 'total_mass' and 'center_of_mass' attributes
+        for each node in the Octree.
+
+        Returns:
+        None
+        """
+        if self.particle is not None:
+            # Node contains only one particle
+            self.center_of_mass = np.array([self.particle.x, self.particle.y, self.particle.z])
+            self.total_mass = self.particle.mass
+        else:
+            # Multiple particles in node
+            total_mass = 0
+            center_of_mass_accumulator = np.array([0.0, 0.0, 0.0])
+
+            for child in self.children:
+                if child is not None:
+                    # Recursively compute mass distribution for child nodes
+                    child.ComputeMassDistribution()
+                    total_mass += child.total_mass
+                    center_of_mass_accumulator += child.total_mass * child.center_of_mass
+
+            if total_mass > 0:
+                self.center_of_mass = center_of_mass_accumulator / total_mass
+                self.total_mass = total_mass
+            else:
+                # If total mass is 0 or no child nodes have mass, leave values as default
+                pass
+                #self.center_of_mass = np.array([(x+width)/2, (y+height)/2, (z+depth)/2])
+                #self.total_mass = 0
+
+    def CalculateForceFromTree(self, target_particle, theta=1.0):
+        """Calculate the force on a target particle using the Barnes-Hut algorithm.
+
+        Args:
+        target_particle (Particle): The particle for which the force is calculated.
+        theta (float): The Barnes-Hut criterion for force approximation.
+
+        Returns:
+        np.ndarray: The total force acting on the target particle.
+        """
+
+        total_force = np.array([0.0, 0.0, 0.0])
+
+        if self.particle is not None:
+            # Node contains only one particle
+            if self.particle != target_particle:
+                # Calculate gravitational force between target_particle and node's particle
+                force = self.GravitationalForce(target_particle, self.particle)
+                total_force += force
+        else:
+            if self.total_mass == 0:
+                return total_force
+
+            r = np.linalg.norm(
+                np.array([target_particle.x, target_particle.y, target_particle.z]) - self.center_of_mass)
+            d = max(self.width, self.height, self.depth)
+
+            if d / r < theta:
+                # Calculate gravitational force between target_particle and "node particle" representing cluster
+
+                force = self.GravitationalForce(
+                    target_particle,
+                    Particle(self.center_of_mass[0], self.center_of_mass[1], self.center_of_mass[2], self.total_mass))
+                total_force += force
+            else:
+                for child in self.children:
+                    if child is not None:
+                        # Recursively calculate force from child nodes
+                        if target_particle is not None:  # Check if the target_particle is not None
+                            force = child.CalculateForceFromTree(target_particle)
+                            total_force += force
+
+        return total_force
+
+    def CalculateForce(self, target_particle, particle, theta=1.0):
+        """Calculate the gravitational force between two particles.
+
+        Args:
+        target_particle (Particle): The particle for which the force is calculated.
+        particle (Particle): The particle exerting the force.
+
+        Returns:
+        np.ndarray: The force vector acting on the target particle due to 'particle'.
+        """
+        force = np.array([0.0, 0.0, 0.0])
+        print('function CalculateForce is called')
+        if self.particle is not None:
+            # Node contains only one particle
+            if self.particle != target_particle:
+                # Calculate gravitational force between target_particle and node's particle
+                force = self.GravitationalForce(target_particle, self.particle)
+        else:
+            if target_particle is not None and particle is not None:  # Check if both particles are not None
+                r = np.linalg.norm(
+                    np.array([target_particle.x, target_particle.y, target_particle.z]) -
+                    np.array([particle.x, particle.y, particle.z]))
+                d = max(self.width, self.height, self.depth)
+
+                if d / r < theta:
+                    # Calculate gravitational force between target_particle and particle
+                    force = self.GravitationalForce(target_particle, particle)
+                else:
+                    for child in self.children:
+                        if child is not None:
+                            # Recursively calculate force from child nodes
+                            force += child.CalculateForce(target_particle, particle)
+        return force
+
+    def GravitationalForce(self, particle1, particle2):
+        """Calculate the gravitational force between two particles.
+
+        Args:
+        particle1 (Particle): First particle.
+        particle2 (Particle): Second particle.
+
+        Returns:
+        np.ndarray: The gravitational force vector between particle1 and particle2.
+        """
+        #G = 6.674 * (10 ** -11)  # Gravitational constant
+        G = 1
+
+        dx = particle2.x - particle1.x
+        dy = particle2.y - particle1.y
+        dz = particle2.z - particle1.z
+        cutoff_radius = 5
+        r = max(np.sqrt(dx**2 + dy**2 + dz**2), cutoff_radius)
+
+        force_magnitude = G * particle1.mass * particle2.mass / (r**2)
+        force_x = force_magnitude * (dx / r)
+        force_y = force_magnitude * (dy / r)
+        force_z = force_magnitude * (dz / r)
+
+        return np.array([force_x, force_y, force_z])
+
+    # Helper method to retrieve all particles in the subtree
+    def particles_in_subtree(self):
+        """Retrieve all particles in the subtree rooted at this node.
+
+        Returns:
+        list: A list of particles in the subtree rooted at this node.
+        """
+        particles = []
+
+        if self.particle is not None:
+            particles.append(self.particle)
+        else:
+            for child in self.children:
+                if child is not None:
+                    particles.extend(child.particles_in_subtree())
+        return len(particles), particles
+
+    def compute_center_of_mass(self):
+        """Compute the center of mass for the node."""
+        print('Function compute_center_of_mass is called')
+        if self.particle is not None:
+            self.center_of_mass = np.array([self.particle.x, self.particle.y, self.particle.z])
+            self.mass = self.particle.mass
+        else:
+            total_mass = 0
+            center_of_mass_accumulator = np.array([0.0, 0.0, 0.0])
+
+            for child in self.children:
+                if child is not None:
+                    child.compute_center_of_mass()
+                    total_mass += child.mass
+                    center_of_mass_accumulator += child.mass * child.center_of_mass
+
+            if total_mass > 0:
+                self.center_of_mass = center_of_mass_accumulator / total_mass
+                self.mass = total_mass
+            else:
+                self.center_of_mass = np.array([0.0, 0.0, 0.0])
+                self.mass = 0
diff --git a/tasks/bht/galaxysimulation.py b/tasks/bht/galaxysimulation.py
index f8aae98..03d532f 100644
--- a/tasks/bht/galaxysimulation.py
+++ b/tasks/bht/galaxysimulation.py
@@ -1,6 +1,6 @@
 import matplotlib.pyplot as plt
 import numpy as np
-from bht import MainApp, Particle
+from bht_oct import MainApp, Particle
 
 
 # Simulation class for galaxy collision using Barnes-Hut and Euler method
@@ -10,33 +10,32 @@ class GalaxyCollisionSimulation:
         # Initialize the simulation with a given number of particles
         self.num_particles = num_particles
         self.particles = self.initialize_particles(initial='random')  # Generate particles
-        self.barnes_hut = MainApp()  # Initialize Barnes-Hut algorithm instance
+        self.barnes_hut = MainApp(self.particles)  # Initialize Barnes-Hut algorithm instance
         self.barnes_hut.BuildTree(self.particles)  # Build the Barnes-Hut tree with particles
         self.barnes_hut.rootNode.ComputeMassDistribution()  #Compute the center of mass of the tree nodes
         self.particle_positions = {
-            i: [(particle.x, particle.y)]
+            i: [(particle.x, particle.y, particle.z)]
             for i, particle in enumerate(self.particles)
         }  # Store particle positions for each time step
         self.particle_velocities = {
-            i: [(particle.vx, particle.vy)]
+            i: [(particle.vx, particle.vy, particle.vz)]
             for i, particle in enumerate(self.particles)
         }  # Store particle velocities for each time step
         self.particle_forces = {
-            i: [(particle.fx, particle.fy)]
+            i: [(particle.fx, particle.fy, particle.fz)]
             for i, particle in enumerate(self.particles)
         }  # Store particle velocities for each time step
         self.system_center_of_mass = {
-            0: (self.barnes_hut.rootNode.center_of_mass[0], self.barnes_hut.rootNode.center_of_mass[1])
+            0: (self.barnes_hut.rootNode.center_of_mass[0], self.barnes_hut.rootNode.center_of_mass[1],
+                self.barnes_hut.rootNode.center_of_mass[2])
         }
 
     def initialize_particles(self, initial='random'):
 
         if initial == 'two':
-            particles = [Particle(80, 50, 1000), Particle(20, 50, 1000)]
-            #Particle(0.5, 0, 10000),
-            #Particle(-0.5, 0, 10000)]
-            particles[0].vx, particles[0].vy = 0, 3
-            particles[1].vx, particles[1].vy = 0, -3
+            particles = [Particle(60, 50, 40, 1000), Particle(40, 50, 60, 1000)]
+            particles[0].vx, particles[0].vy, particles[0].vz = 5, 3, 3
+            particles[1].vx, particles[1].vy, particles[1].vz = 5, -3, 3
             #particles=particles[:2]
             return particles
 
@@ -44,13 +43,34 @@ class GalaxyCollisionSimulation:
             # Generate random particles within a specified range
             particles = []
             for _ in range(self.num_particles):
-                x = np.random.uniform(0, 100)
-                y = np.random.uniform(0, 100)
-                #vx = 0
-                #vy = 0
+                x = np.random.uniform(50, 150)
+                y = np.random.uniform(50, 150)
+                z = np.random.uniform(50, 150)
+                vx = np.random.uniform(-5, 5)
+                vy = np.random.uniform(-5, 5)
+                vz = np.random.uniform(-5, 5)
                 #mass = np.random.uniform(10, 100)
-                mass = 10000
-                particles.append(Particle(x, y, mass))
+                mass = 1000
+                particle = Particle(x, y, z, mass)
+                particle.vx = vx
+                particle.vy = vy
+                particle.vz = vz
+                particles.append(particle)
+
+            return particles
+
+        elif initial == 'four':
+            particles = [
+                Particle(60, 50, 40, 1000),
+                Particle(40, 50, 60, 1000),
+                Particle(50, 60, 70, 1000),
+                Particle(50, 40, 30, 1000)
+            ]
+            particles[0].vx, particles[0].vy, particles[0].vz = 0, 3, 3
+            particles[1].vx, particles[1].vy, particles[1].vz = 0, -3, 3
+            particles[2].vx, particles[2].vy, particles[2].vz = -3, 0, -3
+            particles[3].vx, particles[3].vy, particles[3].vz = 3, 0, -3
+            #particles=particles[:2]
             return particles
 
     def simulate(self, num_steps, time_step):
@@ -66,23 +86,24 @@ class GalaxyCollisionSimulation:
             self.integrate(time_step, 'velocity_verlet')  # Integrate to update particle positions
 
             self.system_center_of_mass[step] = (self.barnes_hut.rootNode.center_of_mass[0],
-                                                self.barnes_hut.rootNode.center_of_mass[1])
+                                                self.barnes_hut.rootNode.center_of_mass[1],
+                                                self.barnes_hut.rootNode.center_of_mass[2])
             # Store particle positions, velocities and forces for each time step
             for i, particle in enumerate(self.particles):
-                self.particle_positions[i].append((particle.x, particle.y))
-                self.particle_velocities[i].append((particle.vx, particle.vy))
-                self.particle_forces[i].append((particle.fx, particle.fy))
+                self.particle_positions[i].append((particle.x, particle.y, particle.z))
+                self.particle_velocities[i].append((particle.vx, particle.vy, particle.vz))
+                self.particle_forces[i].append((particle.fx, particle.fy, particle.fz))
 
     def evaluate_forces(self):
         # Evaluate forces acting on each particle using Barnes-Hut algorithm
         for particle in self.particles:
             force = self.barnes_hut.rootNode.CalculateForceFromTree(particle)
-            particle.fx, particle.fy = force
+            particle.fx, particle.fy, particle.fz = force
 
     def calculate_force(self, particle):
         # Calculate force acting on a single particle
         force = self.barnes_hut.rootNode.CalculateForceFromTree(particle)
-        particle.fx, particle.fy = force
+        particle.fx, particle.fy, particle.fz = force
 
     def integrate(self, time_step, method):
         # Integrate to update particle positions based on calculated forces
@@ -90,27 +111,32 @@ class GalaxyCollisionSimulation:
             for particle in self.particles:
                 particle.vx += particle.fx * time_step / particle.mass  # Update velocity components
                 particle.vy += particle.fy * time_step / particle.mass
+                particle.vz += particle.fz * time_step / particle.mass
                 particle.x += particle.vx * time_step  # Update particle positions using velocity
                 particle.y += particle.vy * time_step
+                particle.z += particle.vz * time_step
 
         elif method == 'velocity_verlet':
             for particle in self.particles:
                 particle.vx += particle.fx * time_step / (2 * particle.mass)  # First half-step
                 particle.vy += particle.fy * time_step / (2 * particle.mass)
+                particle.vz += particle.fz * time_step / (2 * particle.mass)
                 particle.x += particle.vx * time_step  # Full step for position
                 particle.y += particle.vy * time_step
+                particle.z += particle.vz * time_step
 
                 self.calculate_force(particle)  # Recalculate force for the particle
 
                 particle.vx += particle.fx * time_step / (2 * particle.mass)  # Second half-step
                 particle.vy += particle.fy * time_step / (2 * particle.mass)
+                particle.vz += particle.fz * time_step / (2 * particle.mass)
 
     def print_particle_positions(self):
         # Print the positions of all particles for each time step
         for i, positions in self.particle_positions.items():
             print(f"Particle {i + 1} Positions:")
             for step, pos in enumerate(positions):
-                print(f"  Time Step {step}: Position ({pos[0]}, {pos[1]})")
+                print(f"  Time Step {step}: Position ({pos[0]}, {pos[1]}, {pos[2]})")
 
     def display_snapshots(self, num_shots, fix_axes=True):
         # Display pictures for each time step
@@ -121,27 +147,30 @@ class GalaxyCollisionSimulation:
         #Fixed axis limits
         print(f"For {axes_type} axis limits, num_particles={self.num_particles}")
         for step in np.arange(0, num_time_steps, int(num_time_steps / num_shots)):
-            fig, ax = plt.subplots()
+            fig = plt.figure()
+            ax = fig.add_subplot(111, projection='3d')
             if fix_axes:
-                ax.set_xlim(0, 100)
-                ax.set_ylim(0, 100)
+                ax.set_xlim(0, 200)
+                ax.set_ylim(0, 200)
+                ax.set_zlim(0, 200)
             ax.set_xlabel('X-axis')
             ax.set_ylabel('Y-axis')
+            ax.set_zlabel('Z-axis')
             ax.set_title(f'Time Step {step}')
             ax.grid()
 
-            print(f'Step {step}')
+            #print(f'Step {step}')
             for particle_index, positions in self.particle_positions.items():
-                x, y = positions[step]
-                vx, vy = self.particle_velocities[particle_index][step]
-                fx, fy = self.particle_forces[particle_index][step]
+                x, y, z = positions[step]
+                vx, vy, vz = self.particle_velocities[particle_index][step]
+                fx, fy, fz = self.particle_forces[particle_index][step]
                 #mass = self.particles[particle_index].mass
-                ax.scatter(x, y, c='blue', s=20, alpha=0.5)  # Plot particle position for the current time step
-                c_x, c_y = self.system_center_of_mass[step]
-                ax.scatter(c_x, c_y, c='orange', s=20)
-                print(
-                    f'i={particle_index}: x={round(x,2)}, y={round(y,2)}, vx={round(vx,2)}, vy={round(vy,2)}, fx={round(fx,2)}, fy={round(fy,2)}'
-                )
+                ax.scatter(x, y, z, c='blue', s=20, alpha=0.5)  # Plot particle position for the current time step
+                c_x, c_y, c_z = self.system_center_of_mass[step]
+                ax.scatter(c_x, c_y, c_z, c='orange', s=20)
+                #print(
+                #f'i={particle_index}: x={round(x,2)}, y={round(y,2)}, z={round(z,2)}, vx={round(vx,2)}, vy={round(vy,2)}, vz={round(vz,2)}, fx={round(fx,2)}, fy={round(fy,2)}, fz={round(fz,2)}'
+                #)
                 #print(y)
 
             plt.show()
diff --git a/tasks/bht/print_tree.py b/tasks/bht/print_tree.py
index 9390f83..5ed8745 100644
--- a/tasks/bht/print_tree.py
+++ b/tasks/bht/print_tree.py
@@ -1,5 +1,5 @@
 import numpy as np
-from bht import MainApp, Particle
+from tasks.bht.bht_oct import MainApp, Particle
 
 
 # Function to generate random particles
diff --git a/tasks/bht/simulate.py b/tasks/bht/simulate.py
index c703e42..1de867a 100644
--- a/tasks/bht/simulate.py
+++ b/tasks/bht/simulate.py
@@ -1,5 +1,5 @@
 from galaxysimulation import GalaxyCollisionSimulation
 
-sim = GalaxyCollisionSimulation(num_particles=6)
-sim.simulate(num_steps=7000, time_step=0.001)
-sim.display_snapshots(200, fix_axes=True)
+sim = GalaxyCollisionSimulation(num_particles=4)
+sim.simulate(num_steps=10000, time_step=0.001)
+sim.display_snapshots(200, fix_axes=True)
\ No newline at end of file
diff --git a/tasks/bht/system.py b/tasks/bht/system.py
new file mode 100644
index 0000000..264dcfd
--- /dev/null
+++ b/tasks/bht/system.py
@@ -0,0 +1,57 @@
+"""
+This module contains base class for a n-body gravitational system
+with 2 methods of simulations: direct and approximated force field.
+"""
+
+from collections.abc import Callable
+from dataclasses import dataclass
+
+import numpy as np
+
+
+@dataclass
+class GravitationalSystem:
+    r0: np.ndarray[float]  # shape = (N,d)
+    v0: np.ndarray[float]  # shape = (N,d)
+    m: np.ndarray[float]  # shape = N
+    t: np.ndarray[float]  # shape = n_time_steps
+    force: Callable
+    solver: Callable
+
+    def __post_init__(self):
+        "Checking dimensions of inputs"
+        assert self.r0.shape == self.v0.shape
+        assert self.m.shape[0] == self.r0.shape[0]
+
+        solvers = ['verlet']
+        assert self.solver.__name__ in solvers
+
+    def direct_simulation(self):
+        """
+        Using integrator to compute trajector in phase space
+        """
+        p = np.zeros((len(self.t), *self.v0.shape))
+        q = np.zeros((len(self.t), *self.r0.shape))
+        q[0] = self.r0
+        p[0] = self.m * self.v0
+
+        for i in range(1, len(self.t)):
+            dt = self.t[i] - self.t[i - 1]
+            p[i], q[i] = self.solver(self.force, q[i - 1], p[i - 1], self.m, dt)
+
+        return self.t, p, q
+
+    def force_field(self):
+        """
+        Using force field method to compute trajector in phase space
+        """
+        p = np.zeros((len(self.t), *self.v0.shape))
+        q = np.zeros((len(self.t), *self.r0.shape))
+        q[0] = self.r0
+        p[0] = self.m * self.v0
+        for i in range(1, len(self.t)):
+            # something with dt = self.t[i] - self.t[i-1]
+            # p = m*v
+            pass
+
+        return self.t, p, q
-- 
GitLab