text: ## Code of projection was taken from:
  # https://github.com/PacktPublishing/Deep-Reinforcement-Learning-Hands-On/blob/master/Chapter14/06_train_d4pg.py
  
  import numpy as np
  
  
  def _l2_project(next_distr_v, rewards_v, dones_mask_t, gamma, delta_z, n_atoms, v_min, v_max):
      next_distr = next_distr_v.data.cpu().numpy()
      rewards = rewards_v.data.cpu().numpy()
      dones_mask = dones_mask_t.cpu().numpy().astype(np.bool)
      batch_size = len(rewards)
      proj_distr = np.zeros((batch_size, n_atoms), dtype=np.float32)
      
      dones_mask=np.squeeze(dones_mask)
      rewards = rewards.reshape(-1)
  
      for atom in range(n_atoms):
          tz_j = np.minimum(v_max, np.maximum(v_min, rewards + (v_min + atom * delta_z) * gamma))
          b_j = (tz_j - v_min) / delta_z
          l = np.floor(b_j).astype(np.int64)
          u = np.ceil(b_j).astype(np.int64)
          eq_mask = u == l
          proj_distr[eq_mask, l[eq_mask]] += next_distr[eq_mask, atom]
          ne_mask = u != l
          proj_distr[ne_mask, l[ne_mask]] += next_distr[ne_mask, atom] * (u - b_j)[ne_mask]
          proj_distr[ne_mask, u[ne_mask]] += next_distr[ne_mask, atom] * (b_j - l)[ne_mask]
  
      if dones_mask.any():
          proj_distr[dones_mask] = 0.0
          tz_j = np.minimum(v_max, np.maximum(v_min, rewards[dones_mask]))
          b_j = (tz_j - v_min) / delta_z
          l = np.floor(b_j).astype(np.int64)
          u = np.ceil(b_j).astype(np.int64)
          eq_mask = u == l
          eq_dones = dones_mask.copy()
          eq_dones[dones_mask] = eq_mask
          if eq_dones.any():
              proj_distr[eq_dones, l[eq_mask]] = 1.0
          ne_mask = u != l
          ne_dones = dones_mask.copy()
          ne_dones[dones_mask] = ne_mask
          if ne_dones.any():
              proj_distr[ne_dones, l[ne_mask]] = (u - b_j)[ne_mask]
              proj_distr[ne_dones, u[ne_mask]] = (b_j - l)[ne_mask]
  
      return proj_distr