//  [python]
def propagate(self):
        self.reset_tensors()
        self.all_traj[:, 0, :] = self.q_cur
        P = self.Policy
        for i in range(1, self.dt_H+1):
            with record_function("TAG: Nominal vector field"):
                q_prev = self.all_traj[:, i - 1, :]
                # calculate nominal vector field
                #nominal_velocity = (q_prev - self.qf) @ self.A
                nominal_velocity = self.DS.get_velocity(q_prev)
                nominal_velocity_norm = torch.norm(nominal_velocity, dim=1).reshape(-1, 1)
                nominal_velocity_normalized = nominal_velocity / nominal_velocity_norm

            #distance calculation (NN)
            with record_function("TAG: evaluate NN"):
                # evaluate NN. Calculate kernel bases on first iteration
                distance, self.nn_grad = self.distance_repulsion_nn(q_prev, aot=True)
                self.nn_grad = self.nn_grad[0:self.N_traj, :]               # fixes issue with aot_function cache
                #distance, self.nn_grad = self.distance_repulsion_fk(q_prev) #not implemented for Franka

                distance -= self.dst_thr
                self.closest_dist_all[:, i - 1] = distance[0:self.N_traj]
                distance = distance[0:self.N_traj]
            with record_function("TAG: QR decomposition"):
                # calculate modulations
                self.basis_eye_temp = self.basis_eye_temp*0 + self.basis_eye
                self.basis_eye_temp[:, :, 0] = self.nn_grad.cpu().to(torch.float32)
                E, R = torch.linalg.qr(self.basis_eye_temp)
                E = E.to(**self.tensor_args)
                E[:, :, 0] = self.nn_grad / self.nn_grad.norm(2, 1).unsqueeze(1)
                self.norm_basis[:, i-1] = E

                dotproduct = (E[:, :, 0] * nominal_velocity_normalized).sum(dim=-1)
                self.dot_products[:, i-1] = dotproduct
                #l_vel = generalized_sigmoid(dotproduct, 0, 1, -0.2, 0.0, 100)
                l_vel = generalized_sigmoid(dotproduct, 0, 1, -1, 0, 10)

            with record_function("TAG: Modulation-propagation"):
                # calculate standard modulation coefficients
                # gamma = distance + 1
                # gamma[gamma < 0] = 1e-8
                # l_n = 1 - 1 / gamma
                # l_tau = 1 + 1 / gamma
                # l_n[l_n < 0] = 0
                # l_tau[l_tau < 1] = 1
                # calculate own modulation coefficients
                if 0:
                    # for planar robot (units)
                    dist_low, dist_high = 0.0, 2
                    k_sigmoid = 3
                else:
                    # for franka robot (meters)
                    dist_low, dist_high = 0.0, 0.1
                    k_sigmoid = 100
                ln_min, ln_max = 0, 1
                ltau_min, ltau_max = 1, 5
                l_n = generalized_sigmoid(distance, ln_min, ln_max, dist_low, dist_high, k_sigmoid)
                l_n_vel = l_vel * 1 + (1 - l_vel) * l_n
                l_tau = generalized_sigmoid(distance, ltau_max, ltau_min, dist_low, dist_high, k_sigmoid)
                # self.D = self.D * 0 + torch.eye(self.n_dof).to(**self.tensor_args)
                # self.D = l_tau[:, None, None] * self.D
                self.D = l_tau.repeat_interleave(self.n_dof).reshape((self.N_traj, self.n_dof)).diag_embed(0, 1, 2)
                self.D[:, 0, 0] = l_n_vel
                # build modulation matrix
                M = E @ self.D @ E.transpose(1, 2)

            # apply policies
            with record_function("TAG: Apply policies"):
                kernel_value = eval_rbf(q_prev, P.mu_tmp[:, 0:P.n_kernels], P.sigma_tmp[:, 0:P.n_kernels], P.p)
                self.ker_w = kernel_value
                #### important to nullify first component (to ensure tangential motion)
                # P.alpha_tmp[:, 0:P.n_kernels, 0] = 0
                #### that's for kernel gamma(q_k) policy
                #policy_all_flows = (P.kernel_obstacle_bases[0:P.n_kernels] @ P.alpha_tmp[:, 0:P.n_kernels].unsqueeze(3)).squeeze(-1)
                #### that's for local gamma(q) policy
                # policy_all_flows = torch.matmul(E[:, None], P.alpha_tmp[:, 0:P.n_kernels].unsqueeze(3)).squeeze(-1)
                #### disable tangential stuff, optimize just 7d vector field
                policy_all_flows = P.alpha_tmp[:, 0:P.n_kernels]

                # sum weighted inputs from kernels #### BUG HERE [FIXED FINALLY]!!!!
                policy_value = torch.sum(policy_all_flows * self.ker_w, 1)
                ## project it to tangent space (if needed)
                M_tau = E @ self.D_tau @ E.transpose(1, 2)
                #policy_value = (M_tau @ policy_value.unsqueeze(-1)).squeeze(-1)
                # for one kernel policy_all_flows [100, 7], ker_w [100, 1, 1]
                # valid multiplication
                if P.n_kernels > 0:
                    self.kernel_val_all[:, i - 1, 0:P.n_kernels] = kernel_value.reshape((self.N_traj, P.n_kernels))
                if P.n_kernels == 0:
                    policy_value = nominal_velocity * 0
            with record_function("TAG: Apply policy"):
                # policy control
                ### policy depends on kernel Gamma(q_k) - assuming matrix multiplication done above
                #policy_velocity = policy_value
                collision_activation = (1-l_n[:, None])
                velocity_activation = (1-l_vel[:, None])
                goal_activation = (q_prev-self.qf).norm(p=0.5, dim=1).clamp(0, 1).unsqueeze(1)
                goal_activation[goal_activation < 0.5] = 0
                total_activation = collision_activation * velocity_activation * goal_activation
                self.kernel_activations[:, i-1] = total_activation.squeeze(1)
                policy_velocity = total_activation * policy_value
                # policy_velocity = velocity_activation * policy_value
                # if self.N_traj == 1:
                #     print(collision_activation.item(), velocity_activation.item())
                #policy_velocity += ((1-l_n)/100).unsqueeze(1) * E[:, :, 0] #(some repuslion tweaking)
                # calculate modulated vector field (and normalize)

                # TODO CHECK POLICY VELOCITY NORMALIZATION (it's way less than 1)
                policy_velocity = policy_velocity * nominal_velocity_norm             # magnitude of nominal velocity
                total_velocity = nominal_velocity + policy_velocity                   # magnitude of 2x nominal velocity
                # total_velocity_norm = torch.norm(total_velocity, dim=1).unsqueeze(1)
                # total_velocity_scaled = nominal_velocity_norm * total_velocity / total_velocity_norm
                mod_velocity = (M @ (total_velocity).unsqueeze(2)).squeeze()
                # # normalization
                mod_velocity_norm = torch.norm(mod_velocity, dim=-1).reshape(-1, 1)
                mod_velocity_norm[mod_velocity_norm <= 0.5] = 1
                mod_velocity = torch.nan_to_num(mod_velocity / mod_velocity_norm)
                # slow down and repulsion for collision case
                mod_velocity[distance < 0] *= 0.1
                repulsion_velocity = E[:, :, 0] * nominal_velocity_norm*0.1
                mod_velocity[distance < 0] += repulsion_velocity[distance < 0]
            with record_function("TAG: Propagate"):
                # propagate
                if i < self.dt_H:
                    self.all_traj[:, i, :] = self.all_traj[:, i - 1, :] + self.dt * mod_velocity
                if i == 1:
                    self.qdot = mod_velocity
        return self.all_traj, self.closest_dist_all, self.kernel_val_all[:, :, 0:P.n_kernels], self.dot_products, self.kernel_activations