def reconstruct_matrix(self, signal, calibration_matrix, eigval1_reciprocal, eigval2_reciprocal): """ Recovers the random matrix. Parameters ---------- signal: ComplexTensor, Tensor with the signal values calibration_matrix: torch.Tensor, calibration matrix (the partial one) eigenvalues_reciprocal1: ComplexTensor, eigenvalues of the first circulant matrix block of the partial calibration matrix. eigenvalues_reciprocal2: ComplexTensor, eigenvalues of the second circulant matrix block of the partial calibration matrix. Returns ------- reconstructed_A: ComplexTensor, recostructed transmission matrix. If batch size<rows, it is a batch of rows. """ start = time() if self.solver == "least-square": inv_calibration_matrix = torch.pinverse(calibration_matrix) reconstructed_A = ComplexTensor( real=torch.matmul(signal.real, inv_calibration_matrix), imag=torch.matmul(signal.imag, inv_calibration_matrix)) elif self.solver == "fft": signal = signal.conj().stack() signal1_star = signal[:, :self.n_signals // 2] signal2_star = signal[:, self.n_signals // 2:] fft_buffer = torch.fft(signal1_star, signal_ndim=1) block1 = ComplexTensor(real=fft_buffer[:, :, 0], imag=fft_buffer[:, :, 1]) block1 = block1.batch_elementwise(eigval1_reciprocal) fft_buffer = torch.fft(signal2_star, signal_ndim=1) block2 = ComplexTensor(real=fft_buffer[:, :, 0], imag=fft_buffer[:, :, 1]) block2 = block2.batch_elementwise(eigval2_reciprocal) reconstructed_A = torch.ifft((block1 + block2).stack(), signal_ndim=1) reconstructed_A = ComplexTensor(real=reconstructed_A[:, :, 0], imag=reconstructed_A[:, :, 1]).conj() self.time_logger["solver"] += time() - start return reconstructed_A
def test_single_pair(self): size = (40, 30) real = .01 * torch.rand(size) im = .01 * torch.rand(size) mem = HRR(data=ComplexTensor(real, im)) k = ComplexTensor(2 * torch.rand(size), 3 * torch.rand(size)) v = ComplexTensor(5 * torch.ones(size), 2 * torch.ones(size)) mem.write(k, v) restored = mem.read(k) torch.testing.assert_allclose(restored.real, v.real, rtol=0, atol=.1) torch.testing.assert_allclose(restored.im, v.im, rtol=0, atol=.1) restored_phase = mem.get_phase(k) torch.testing.assert_allclose(restored_phase, v.phase, rtol=0, atol=.1)
def get_eigenvalues(self, column): """ Computes the eigenvalues of a circulant matrix through an FFT on a single column. the code returns 0.5 * the reciprocal of the eiganvalues conjugated, to avoid making this computation for every row later Parameters ---------- column: torch Tensor, column of a circulant matrix Returns ------- eigenvalues_reciprocal: ComplexTensor, eigenvalues of the circulant matrix. """ fft_buffer = torch.rfft(column, signal_ndim=1, onesided=False) eigenvalues = ComplexTensor(real=fft_buffer[:, 0], imag=fft_buffer[:, 1]) eigenvalues_reciprocal = 0.5 * eigenvalues.reciprocal().conj() return eigenvalues_reciprocal
def get_calibration_phase(self, anchors_RP, difference_RP, anchors_diff_RP): """ Computes the real and imaginary part of the signal in the complex space (Y in the paper). Parameters ---------- anchors_RP: torch.Tensor, Random projection of the anchors. difference_RP: torch.Tensor, Random projection of the difference between anchors and calibration matrices. anchors_diff_RP: torch.Tensor, Random projection of the pairwise difference between anchors. Returns ------- signal: ComplexTensor, Tensor with the signal values """ LHS = difference_RP - anchors_RP.unsqueeze(-1) anchor_no_square = self.mds(anchors_diff_RP) # The matrix of ones can be cached to avoid generation at every step. However, do be midful # the number of rows is not divisible by the batch size RHS = torch.stack( (-2 * anchor_no_square.real, -2 * anchor_no_square.imag, torch.ones(anchor_no_square.shape()[0], self.n_anchors + 1)), dim=2) start = time() lateration_solution = torch.pinverse(RHS) @ LHS signal = ComplexTensor(real=lateration_solution[:, 0, :], imag=lateration_solution[:, 1, :]) self.time_logger["lateration"] += time() - start return signal
def test_multiple_pairs(self): size = (40, 30) real = .01 * torch.rand(size) im = .01 * torch.rand(size) mem = HRR(ComplexTensor(real, im)) k = ComplexTensor(torch.rand(size), torch.rand(size)) v = ComplexTensor(5 * torch.ones(size), 2 * torch.ones(size)) mem.write(k, v) k = ComplexTensor(torch.rand(size), torch.rand(size)) v = ComplexTensor(5 * torch.ones(size), 2 * torch.ones(size)) mem.write(k, v) k = ComplexTensor(torch.rand(size), torch.rand(size)) v = ComplexTensor(5 * torch.ones(size), 2 * torch.ones(size)) mem.write(k, v) restored_phase = mem.get_phase(k) torch.testing.assert_allclose(restored_phase, v.phase, rtol=0, atol=.1)
def fit(self, A): """ Partially recovers the transmission matrix, then combines the result to get the fully recovered matrix. todo: the combined part can be rewritten in torch to speed things up a bit. Parameters ---------- A: ComplexTensor or OPUMap object, either a random matrix or the OPUMap object. Returns ------- rec_A: ComplexTensor, Fully reconstructed transmission matrix """ full_calibration_matrix, first_ind, second_ind, ind_common, col1, col2, col3, col4 = self.build_X( ) anchors1_ind = np.arange(self.n_signals) anchors2_ind = np.arange(self.n_signals, 2 * self.n_signals) with torch.no_grad(): rec_A1 = self.recover_matrix(A, full_calibration_matrix, anchors1_ind, col1, col2) rec_A2 = self.recover_matrix(A, full_calibration_matrix, anchors2_ind, col3, col4) start = time() A1_ind = np.arange( self.n_col)[~np.isin(np.arange(self.n_col), first_ind)] A2_ind = np.arange( self.n_col)[~np.isin(np.arange(self.n_col), second_ind)] A1 = ComplexTensor(real=torch.zeros(self.n_row, self.n_col), imag=torch.zeros(self.n_row, self.n_col)) A2 = ComplexTensor(real=torch.zeros(self.n_row, self.n_col), imag=torch.zeros(self.n_row, self.n_col)) A1[:, A1_ind] = rec_A1 A2[:, A2_ind] = rec_A2 A1 = A1.numpy() A2 = A2.numpy() P1 = np.angle(A1[:, ind_common] / A2[:, ind_common]).astype('float32') P2 = np.angle(A1[:, ind_common] / np.conj(A2[:, ind_common])).astype('float32') mean_P1 = np.mean(P1, axis=1) mean_P2 = np.mean(P2, axis=1) P1 = np.std(P1, axis=1) P2 = np.std(P2, axis=1) mask1 = P1 < P2 mask2 = np.invert(mask1) A2[mask2] = np.conj(A2[mask2]) phases = (mean_P1 * mask1 + mean_P2 * mask2).reshape( [self.n_row, 1]) phases = np.exp(1j * phases) A2 = phases * A2 A1_ind = first_ind # indices which are all zero A1[:, A1_ind] = A2[:, A1_ind] rec_A = ComplexTensor(real=torch.FloatTensor(np.real(A1)), imag=torch.FloatTensor(np.imag(A1))) self.time_logger["post_processing"] = time() - start return rec_A
def recover_matrix(self, A, full_calibration_matrix, anchors_ind, col1, col2): """ partially recovers the transmission matrix (TM) of the OPU. This requires launching the algorithm twice, with two sets of calibration and anchor matrices. Parameters ---------- A: ComplexTensor or OPUMap object, either a random matrix or the OPUMap object. full_calibration_matrix: numpy array, full calibration matrix. The necessary slice will be obtained here. anchors_ind: numpy array, contains the indeces of the slice of the TM to use in the recovery. col1: numpy array, column of the first circulant matrix col2: numpy array, column of the second circulant matrix Returns ------- reconstructed_A: ComplexTensor, partially reconstructed transmission matrix. """ reconstructed_A = ComplexTensor( real=torch.zeros(self.n_row, self.circ_N), imag=torch.zeros(self.n_row, self.circ_N)) calibration_matrix, opu_input = self.get_input_matrices( full_calibration_matrix, anchors_ind) eigval1_reciprocal = self.get_eigenvalues(torch.FloatTensor(col1)) eigval2_reciprocal = self.get_eigenvalues(torch.FloatTensor(col2)) anchors_RP, difference_RP, anchors_diff_RP = self.project(A, opu_input) difference_RP = difference_RP.reshape(self.n_row, -1, self.n_anchors + 1).transpose( 1, 2) if self.verbose: print("Recovering rows...") n_batches = torch.ceil(torch.tensor( self.n_row / self.batch_size)).type(torch.int).item() for batch_idx in tqdm(range(n_batches)): batch_start, batch_end = self.batch_size * batch_idx, self.batch_size * ( batch_idx + 1) signal = self.get_calibration_phase( anchors_RP[batch_start:batch_end], difference_RP[batch_start:batch_end], anchors_diff_RP[batch_start:batch_end]) rec_batch = self.reconstruct_matrix(signal, calibration_matrix, eigval1_reciprocal, eigval2_reciprocal) reconstructed_A[batch_start:batch_end, :] = rec_batch return reconstructed_A
def mds(self, anchors_diff_RP): """ Creates a difference matrix for the anchors, then performs classical multidimensional scaling to recover the anchors' real and imaginary part. Parameters ---------- anchors_diff_RP: torch tensor, Random projection with modulus square of the pairwise difference of the anchors. Shape = (n_rows * anchors*(anchors - 1)/2). Returns ------- anchor: ComplexTensor, reconstructed anchors. NOTE: In the diagonalization, 2 eigenvalues should be dominant, while the others should be close to zero. For some reason, numpy catches this much better than torch: the "0" of numpy is 1e*-14, while for torch is 1e-6 NOTE 2: in pytorch, symeig returns the eigenvalues in crescent order. That is why the indeces are reversed when assigning the real and imaginary part at the end. NOTE 3: maybe move the computation of the difference matrix outside the class. THE BIGGEST NOTE: The distance matrix at this point has size (self.n_anchors + 2, self.n_anchors + 2), even though we have self.n_anchors. That is because we added the distances between all anchors and the origin at the end, and the distance between all anchors and a row of the calibration matrix at the beginning. When grabbing the recovered anchors, the first will be discarded, because that will be the row of the calibration matrix in the complex space, which we do not need. """ start = time() upper_idx = torch.triu_indices(self.n_anchors + 2, self.n_anchors + 2, offset=1) distances = torch.zeros(anchors_diff_RP.shape[0], self.n_anchors + 2, self.n_anchors + 2) for row in range(anchors_diff_RP.shape[0]): distances[row, upper_idx[0, :], upper_idx[1, :]] = anchors_diff_RP[row, :] distances[row] = distances[row] + distances[row].T # The centering matrix can be generated just once centering_matrix = torch.eye( self.n_anchors + 2) - 1. / (self.n_anchors + 2) * torch.ones( (self.n_anchors + 2, self.n_anchors + 2)) normalized_distances = -0.5 * centering_matrix @ distances @ centering_matrix diagonalization = torch.symeig(normalized_distances, eigenvectors=True) eigval = diagonalization.eigenvalues[:, -2:] eigvec = diagonalization.eigenvectors[:, :, -2:] eigval = eigval.reshape(-1) eigvec = eigvec.permute(1, 0, 2).reshape(self.n_anchors + 2, -1) rec_anchors = torch.sqrt(eigval) * eigvec rec_anchors = rec_anchors - rec_anchors[-1] # distances.shape[0] is the batch size. Since the last batch can have less elements, I use that instead. rec_anchors = rec_anchors.reshape(self.n_anchors + 2, distances.shape[0], 2).permute( (1, 0, 2)) anchor = ComplexTensor(real=rec_anchors[:, 1:, 1], imag=rec_anchors[:, 1:, 0]) self.time_logger["mds"] += time() - start return anchor