diff --git a/tasks/bht/2D/bht.py b/tasks/bht/2D/bht.py
new file mode 100644
index 0000000000000000000000000000000000000000..30f345a2744dec06dd289082f5cb4f0e21e3c692
--- /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 0000000000000000000000000000000000000000..f8aae983d9ba230a7e8addf3c65fe797b492af95
--- /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 0000000000000000000000000000000000000000..c703e4231fcaa037e6bbe49f5f9195b7020e1509
--- /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
Binary files /dev/null and b/tasks/bht/__pycache__/bht_oct.cpython-311.pyc differ
diff --git a/tasks/bht/__pycache__/galaxysimulation.cpython-311.pyc b/tasks/bht/__pycache__/galaxysimulation.cpython-311.pyc
index 217cfe8eeb559192f49de5628658625e9ff730ff..1a9755185c2906f2b2ed6f525202579068b0e3dc 100644
Binary files a/tasks/bht/__pycache__/galaxysimulation.cpython-311.pyc and b/tasks/bht/__pycache__/galaxysimulation.cpython-311.pyc differ
diff --git a/tasks/bht/bht_oct.py b/tasks/bht/bht_oct.py
new file mode 100644
index 0000000000000000000000000000000000000000..c55b43fc09d0607b4635256a619434c10a16625d
--- /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 f8aae983d9ba230a7e8addf3c65fe797b492af95..03d532fed339454cf6209117f15564d350e99f92 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 9390f837d3d1473bf229d06c8d28a11970442e87..5ed8745915b53ad8063ea64ea90c57ef9651519f 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 c703e4231fcaa037e6bbe49f5f9195b7020e1509..1de867ad187aee396b3b7ffb9f9a6251ff8b5bfb 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 0000000000000000000000000000000000000000..264dcfdf22572b1208902fe034f99542928a374c
--- /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