docs: def pso(self, n_particles, n_iterations, lower_start=None, upper_start=None, threadCount=1, init_pos=None,
              mpi=False, print_key='PSO'):
          """
          returns the best fit for the lense model on catalogue basis with particle swarm optimizer
          """
          if lower_start is None or upper_start is None:
              lower_start, upper_start = np.array(self.lower_limit), np.array(self.upper_limit)
              print("PSO initialises its particles with default values")
          else:
              lower_start = np.maximum(lower_start, self.lower_limit)
              upper_start = np.minimum(upper_start, self.upper_limit)
          if mpi is True:
              pso = MpiParticleSwarmOptimizer(self.chain, lower_start, upper_start, n_particles, threads=1)
              if pso.isMaster():
                  print('MPI option chosen')
          else:
              pso = ParticleSwarmOptimizer(self.chain, lower_start, upper_start, n_particles, threads=threadCount)
          if init_pos is None:
              init_pos = (upper_start - lower_start) / 2 + lower_start
          if not init_pos is None:
              pso.gbest.position = init_pos
              pso.gbest.velocity = [0]*len(init_pos)
              pso.gbest.fitness, _ = self.chain.likelihood(init_pos)
          X2_list = []
          vel_list = []
          pos_list = []
          time_start = time.time()
          if pso.isMaster():
              print('Computing the %s ...' % print_key)
          num_iter = 0
          for swarm in pso.sample(n_iterations):
              X2_list.append(pso.gbest.fitness*2)
              vel_list.append(pso.gbest.velocity)
              pos_list.append(pso.gbest.position)
              num_iter += 1
              if pso.isMaster():
                  if num_iter % 10 == 0:
                      print(num_iter)
          if not mpi:
              result = pso.gbest.position
          else:
              result = MpiUtil.mpiBCast(pso.gbest.position)
  
          #if (pso.isMaster() and mpi is True) or self.chain.sampling_option == 'X2_catalogue':
          if mpi is True and not pso.isMaster():
              pass
          else:
              lens_dict, source_dict, lens_light_dict, ps_dict, kwargs_cosmo = self.chain.param.args2kwargs(result)
              print(pso.gbest.fitness * 2 / (self.chain.effectiv_num_data_points(lens_dict, source_dict, lens_light_dict, ps_dict)), 'reduced X^2 of best position')
              print(pso.gbest.fitness, 'logL')
              print(self.chain.effectiv_num_data_points(lens_dict, source_dict, lens_light_dict, ps_dict), 'effective number of data points')
              print(lens_dict, 'lens result')
              print(source_dict, 'source result')
              print(lens_light_dict, 'lens light result')
              print(ps_dict, 'point source result')
              print(kwargs_cosmo, 'cosmo result')
              time_end = time.time()
              print(time_end - time_start, 'time used for PSO', print_key)
              print('===================')
          return result, [X2_list, pos_list, vel_list, []]