Particle simulator using Python - Why is the NumPy version slower than pure Python?
Community wiki - update as posted by the OP in the question based on review by Graipher and AlexV



As Graipher and AlexV mentioned the problem with NumPy version was that the inner loop which updates particle positions (x and y) should be performed independently (not repeated in each timestep). The updated version is as follows,

def evolve_numpy(self, dt):
    timestep = 0.00001
    nsteps = int(dt / timestep)
    r_i = np.array([[p.x, p.y] for p in self.particles])
    ang_vel_i = np.array([p.ang_v for p in self.particles])
    for i in range(nsteps):
        norm_i = np.sqrt((r_i ** 2).sum(axis=1))
        v_i = r_i[:, [1, 0]]
        v_i[:, 0] *= -1
        v_i /= norm_i[:, np.newaxis]
        d_i = timestep * ang_vel_i[:, np.newaxis] * v_i
        r_i += d_i
    for i, p in enumerate(self.particles):
        p.x, p.y = r_i[i]


Doing this improves the results considerably (see the figure below) which shows that the results stated in the book were accurate and this was my mistake (as expected:)),


(The runtime of numpy method is now less than 4 seconds.)