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
Esempio n. 2
0
    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
Esempio n. 5
0
    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