diff --git a/.gitignore b/.gitignore
index af0c7adfce227bf40f275c9da81c268c4a35ddd1..678a0a16bd1411abd35dc010156c56e2fbf649b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 /__pycache__
 *.pyc
-/playground
\ No newline at end of file
+/playground
+*.DS_Store
\ No newline at end of file
diff --git a/tasks/src/bht_algorithm_3D.py b/tasks/src/bht_algorithm_3D.py
new file mode 100644
index 0000000000000000000000000000000000000000..a1bad0fa677fd4e55711f18043de83d36938589d
--- /dev/null
+++ b/tasks/src/bht_algorithm_3D.py
@@ -0,0 +1,394 @@
+"""
+This module implements the Barnes Hut Tree (BHT) Algorithm for 3D data.
+"""
+
+import numpy as np
+
+
+class MainApp:
+
+    def __init__(self):
+        """Initialize the MainApp with a root node."""
+        self.rootNode = TreeNode(x=0, y=0, z=0, width=20, height=20, depth=20)
+
+    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)
+        for particle in particles:
+            self.rootNode.insert(particle)
+
+    def ResetTree(self, particles):
+        """Reset the Octree by reinitializing the root node."""
+
+        if all(self.rootNode.contains(particle) for particle in particles):
+            # If all particles are contained in the current tree, keep same dimensions
+            root_x = self.rootNode.x
+            root_y = self.rootNode.y
+            root_z = self.rootNode.z
+            root_width = self.rootNode.width
+            root_height = self.rootNode.height
+            root_depth = self.rootNode.depth
+
+        else:
+            # If a particle leaves the root node boundaries, resize the tree
+            print('New tree dimensions')
+            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])
+
+            x_m = (min_x + max_x) / 2
+            y_m = (min_y + max_y) / 2
+            z_m = (min_z + max_z) / 2
+
+            delta_x = max_x - min_x
+            delta_y = max_y - min_y
+            delta_z = max_z - min_z
+
+            # Recalculate dimensions
+            root_x = x_m - delta_x
+            root_y = y_m - delta_y
+            root_z = z_m - delta_z
+            root_dim = max(delta_x, delta_y, delta_z)
+            root_width = 2 * root_dim
+            root_height = 2 * root_dim
+            root_depth = 2 * root_dim
+
+        self.rootNode = TreeNode(x=root_x, y=root_y, z=root_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/src/ff_simulation.py b/tasks/src/ff_simulation.py
index 26fba3ba2b4cc8fa54ae7b151417138e7a3ac4c7..868c140183be3629408563dea83071500451537d 100644
--- a/tasks/src/ff_simulation.py
+++ b/tasks/src/ff_simulation.py
@@ -6,7 +6,7 @@ of gravitational n-body system
 import matplotlib.pyplot as plt
 import numpy as np
 
-from jobs.src.bht_algorithm_3D import MainApp, Particle
+from bht_algorithm_3D import MainApp, Particle
 
 
 # Simulation class for galaxy collision using Barnes-Hut and Euler method
@@ -16,7 +16,7 @@ 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(self.particles)  # Initialize Barnes-Hut algorithm instance
+        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 = {
@@ -31,15 +31,17 @@ class GalaxyCollisionSimulation:
             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],
-                self.barnes_hut.rootNode.center_of_mass[2])
-        }
+        self.system_center_of_mass = [
+            (self.barnes_hut.rootNode.center_of_mass[0], self.barnes_hut.rootNode.center_of_mass[1],
+             self.barnes_hut.rootNode.center_of_mass[2])
+        ]
+        self.treeDims = [(self.barnes_hut.rootNode.x, self.barnes_hut.rootNode.width, self.barnes_hut.rootNode.y,
+                          self.barnes_hut.rootNode.height, self.barnes_hut.rootNode.z, self.barnes_hut.rootNode.depth)]
 
     def initialize_particles(self, initial='random'):
 
         if initial == 'two':
-            particles = [Particle(60, 50, 40, 1000), Particle(40, 50, 60, 1000)]
+            particles = [Particle(60, 49, 40, 1000), Particle(40, 51, 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]
@@ -48,19 +50,19 @@ class GalaxyCollisionSimulation:
         elif initial == 'random':
             # Generate random particles within a specified range
             particles = []
+            initial_v = True
             for _ in range(self.num_particles):
                 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 = 1000
                 particle = Particle(x, y, z, mass)
-                particle.vx = vx
-                particle.vy = vy
-                particle.vz = vz
+                if initial_v:
+                    particle.vx = np.random.uniform(-5, 5)
+                    particle.vy = np.random.uniform(-5, 5)
+                    particle.vz = np.random.uniform(-5, 5)
                 particles.append(particle)
 
             return particles
@@ -91,14 +93,17 @@ class GalaxyCollisionSimulation:
             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],
-                                                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, particle.z))
                 self.particle_velocities[i].append((particle.vx, particle.vy, particle.vz))
                 self.particle_forces[i].append((particle.fx, particle.fy, particle.fz))
+                self.system_center_of_mass.append(
+                    (self.barnes_hut.rootNode.center_of_mass[0], self.barnes_hut.rootNode.center_of_mass[1],
+                     self.barnes_hut.rootNode.center_of_mass[2]))
+                self.treeDims.append(
+                    (self.barnes_hut.rootNode.x, self.barnes_hut.rootNode.width, self.barnes_hut.rootNode.y,
+                     self.barnes_hut.rootNode.height, self.barnes_hut.rootNode.z, self.barnes_hut.rootNode.depth))
 
     def evaluate_forces(self):
         # Evaluate forces acting on each particle using Barnes-Hut algorithm
@@ -145,7 +150,9 @@ class GalaxyCollisionSimulation:
                 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
+        """
+        Method to display particle positions as snapshots
+        """
         num_time_steps = len(next(iter(self.particle_positions.values())))
         print('===Quadtree at the end===')
         self.barnes_hut.rootNode.print_tree()  # print the tree
@@ -156,9 +163,12 @@ class GalaxyCollisionSimulation:
             fig = plt.figure()
             ax = fig.add_subplot(111, projection='3d')
             if fix_axes:
-                ax.set_xlim(0, 200)
-                ax.set_ylim(0, 200)
-                ax.set_zlim(0, 200)
+                x_min, width = self.treeDims[step][0], self.treeDims[step][1]
+                y_min, height = self.treeDims[step][2], self.treeDims[step][3]
+                z_min, depth = self.treeDims[step][4], self.treeDims[step][5]
+                ax.set_xlim(x_min, x_min + width)
+                ax.set_ylim(y_min, y_min + height)
+                ax.set_zlim(z_min, z_min + depth)
             ax.set_xlabel('X-axis')
             ax.set_ylabel('Y-axis')
             ax.set_zlabel('Z-axis')
@@ -181,8 +191,44 @@ class GalaxyCollisionSimulation:
 
             plt.show()
 
+    def plot_trajectory(self, update_interval=100):
+        """
+        Method to visualize particles trajectories as a slideshow
+        """
+
+        n_time_step = len(self.particle_positions[0])
+        n_bodies = self.num_particles
+
+        fig = plt.figure()
+        ax = fig.add_subplot(projection='3d')
+
+        for i in range(0, n_time_step, update_interval):
+            ax.clear()
+            for j in range(n_bodies):
+
+                body_traj = self.particle_positions[j][i]
+                ax.scatter(body_traj[0], body_traj[1], body_traj[2], c='blue', alpha=0.5)
+                c_x, c_y, c_z = self.system_center_of_mass[i]
+                ax.scatter(c_x, c_y, c_z, c='orange', s=40)
+
+            ax.set_xlim(0, 200)
+            ax.set_ylim(0, 200)
+            ax.set_zlim(0, 200)
+
+            ax.set_xlabel("X")
+            ax.set_ylabel("Y")
+            ax.set_zlabel("Z")
+            ax.set_title(f"Time step: {i}")
+            ax.set_xticks([])
+            ax.set_yticks([])
+            ax.set_zticks([])
+            ax.grid()
+
+            plt.pause(0.01)
+
 
 if __name__ == "__main__":
-    sim = GalaxyCollisionSimulation(num_particles=10)
+    sim = GalaxyCollisionSimulation(num_particles=5)
     sim.simulate(num_steps=10000, time_step=0.001)
-    sim.display_snapshots(200, fix_axes=True)
+    sim.plot_trajectory()
+    #sim.display_snapshots(20)
diff --git a/tasks/src/time_measurement.py b/tasks/src/time_measurement.py
new file mode 100644
index 0000000000000000000000000000000000000000..218401f85b3828e32499fe1497910d611f81207e
--- /dev/null
+++ b/tasks/src/time_measurement.py
@@ -0,0 +1,85 @@
+import numpy as np
+import time
+import matplotlib.pyplot as plt
+
+if __name__ == "__main__":
+
+    N_particles = [2, 3, 10, 15, 25, 50, 100]
+    no_time_steps = [5, 10, 100, 500, 1000, 5000, 10000]
+    results = np.zeros((7, 7))
+    for i in range(len(N_particles)):
+        num_particles = N_particles[i]
+        for j in range(len(no_time_steps)):
+            num_steps = no_time_steps[j]
+            start_time = time.time()
+            #sim = GalaxyCollisionSimulation(num_particles=num_particles)
+            #sim.simulate(num_steps=num_steps, time_step=0.001)
+            execution_time = time.time() - start_time
+            print(f'N_PARTICLES={num_particles}, NUMSTEPS={num_steps}')
+            #results[i, j] = execution_time
+
+    results = [[
+        3.20506096e-03, 4.54711914e-03, 2.99420357e-02, 1.30662918e-01, 5.54497004e-01, 1.61694264e+00, 2.64192891e+00
+    ], [1.98988914e-02, 5.33080101e-03, 4.81491089e-02, 2.01280832e-01, 3.57017279e-01, 1.68726897e+00, 4.41522312e+00],
+               [
+                   4.58290577e-02, 4.86457348e-02, 4.17565823e-01, 1.45790005e+00, 3.72849512e+00, 1.69347689e+01,
+                   3.80113401e+01
+               ],
+               [
+                   1.59001112e-01, 7.18071461e-02, 7.43870020e-01, 3.17859292e+00, 6.89351201e+00, 4.38741171e+01,
+                   7.23900406e+01
+               ],
+               [
+                   2.58275032e-01, 1.59539938e-01, 1.69004893e+00, 8.19918489e+00, 1.74204440e+01, 9.41446784e+01,
+                   1.98575519e+02
+               ],
+               [
+                   5.22159815e-01, 4.45141077e-01, 4.30359578e+00, 2.12155340e+01, 4.82974751e+01, 2.71531863e+02,
+                   5.68442169e+02
+               ],
+               [
+                   1.30096102e+00, 1.02588534e+00, 9.99782109e+00, 4.98463769e+01, 1.01500050e+02, 7.60922490e+02,
+                   1.57951990e+03
+               ]]
+
+    fig, ax = plt.subplots()
+    im = plt.imshow(results)
+    ax.set_xticks(np.arange(len(no_time_steps)), labels=no_time_steps)
+    ax.set_yticks(np.arange(len(N_particles)), labels=N_particles)
+    ax.set_title("Time scaling for various particle numbers and time steps")
+    ax.set_xlabel('Num steps')
+    ax.set_ylabel('N particles')
+    plt.colorbar()
+    plt.show()
+
+    fig, ax = plt.subplots()
+    im = plt.imshow(np.log(results))
+    ax.set_xticks(np.arange(len(no_time_steps)), labels=no_time_steps)
+    ax.set_yticks(np.arange(len(N_particles)), labels=N_particles)
+    ax.set_title("Time scaling, logarithmic scale")
+    ax.set_xlabel('Num steps')
+    ax.set_ylabel('N particles')
+    plt.colorbar()
+    plt.show()
+
+    model_function = np.array([n * np.emath.logn(8, n) for n in N_particles])
+    n_squared = np.array([n**2 for n in N_particles])
+
+    for k in range(3, len(no_time_steps)):
+        data = np.transpose(results)[k]
+        coeff = data[-1] / model_function[-1]
+        rescaled_model = coeff * model_function
+        plt.plot(N_particles, rescaled_model, '--', label='a*NlogN')
+        plt.scatter(N_particles, data, alpha=0.3, label=f'{no_time_steps[k]} time steps')
+        print(model_function[-1], data[-1])
+
+    plt.plot(N_particles, n_squared, '--', label='N**2')
+    plt.ylim(0, 1600)
+    plt.xlabel('N particles')
+    plt.ylabel('Time complexity')
+    plt.title('Comparative plot for various simulation lengths')
+    plt.legend()
+    plt.show()
+
+print('RESULTS')
+#print(results)