class MuChannel:
class MuMimoChannel(MuChannel):
class MultiUserChannelMatrix:  # pylint: disable=R0902
class MultiUserChannelMatrixExtInt(  # pylint: disable=R0904
        MultiUserChannelMatrix):
    def __init__(self,
                 N: Union[int, Tuple[int, int]],
                 fading_generator: Optional[FadingGenerator] = None,
                 channel_profile: Optional[TdlChannelProfile] = None,
                 tap_powers_dB: Optional[np.ndarray] = None,
                 tap_delays: Optional[np.ndarray] = None,
                 Ts: Optional[float] = None) -> None:
    def __repr__(self) -> str:
    def switched_direction(self) -> bool:
    def switched_direction(self, value: bool) -> None:
    def num_tx_antennas(self) -> int:
    def num_rx_antennas(self) -> int:
    def channel_profile(self) -> TdlChannelProfile:
    def num_taps(self) -> int:
    def num_taps_with_padding(self) -> int:
    def pathloss_matrix(self) -> np.ndarray:
    def set_pathloss(self, pathloss_matrix: np.ndarray) -> None:
    def corrupt_data(self, signal: np.ndarray) -> np.ndarray:
    def corrupt_data_in_freq_domain(
            self,
            signal: np.ndarray,
            fft_size: int,
            carrier_indexes: Indexes = None) -> np.ndarray:
    def get_last_impulse_response(self, rx_idx: int,
                                  tx_idx: int) -> TdlImpulseResponse:
    def __init__(self,
                 N: Union[int, Tuple[int, int]],
                 num_rx_antennas: int,
                 num_tx_antennas: int,
                 fading_generator: Optional[FadingGenerator] = None,
                 channel_profile: Optional[TdlChannelProfile] = None,
                 tap_powers_dB: Optional[np.ndarray] = None,
                 tap_delays: Optional[np.ndarray] = None,
                 Ts: Optional[float] = None) -> None:
    def __init__(self) -> None:
    def set_channel_seed(self,
                         seed: Optional[Seed] = None
                         ) -> None:  # pragma: no cover
    def set_noise_seed(self,
                       seed: Optional[Seed] = None
                       ) -> None:  # pragma: no cover
    def re_seed(self) -> None:  # pragma: no cover
    def Nr(self) -> np.ndarray:
    def Nt(self) -> np.ndarray:
    def K(self) -> int:
    def H(self) -> np.ndarray:
    def big_H(self) -> np.ndarray:
    def pathloss(self) -> Optional[np.ndarray]:
    def last_noise(self) -> Optional[np.ndarray]:
    def noise_var(self) -> Optional[float]:
    def noise_var(self, value: Optional[float]) -> None:
    def _from_small_matrix_to_big_matrix(
            small_matrix: np.ndarray,
            Nr: np.ndarray,
            Nt: np.ndarray,
            Kr: int,
            Kt: Optional[int] = None) -> np.ndarray:
    def init_from_channel_matrix(self, channel_matrix: np.ndarray,
                                 Nr: IntOrIntArrayUnion,
                                 Nt: IntOrIntArrayUnion, K: int) -> None:
    def randomize(self, Nr: IntOrIntArrayUnion, Nt: IntOrIntArrayUnion,
                  K: int) -> None:
    def get_Hkl(self, k: int, l: int) -> np.ndarray:
    def get_Hk(self, k: int) -> np.ndarray:
    def set_post_filter(self, filters: np.ndarray) -> None:
    def W(self) -> Optional[List[np.ndarray]]:
    def big_W(self) -> np.ndarray:
    def corrupt_concatenated_data(self, data: np.ndarray) -> np.ndarray:
    def corrupt_data(self, data: np.ndarray) -> np.ndarray:
    def set_pathloss(self,
                     pathloss_matrix: Optional[np.ndarray] = None) -> None:
    def _calc_Q_impl(self, k: int, F_all_users: np.ndarray) -> np.ndarray:
    def calc_Q(self, k: int, F_all_users: np.ndarray) -> np.ndarray:
    def _calc_JP_Q_impl(self, k: int, F_all_users: np.ndarray) -> np.ndarray:
    def calc_JP_Q(self, k: int, F_all_users: np.ndarray) -> np.ndarray:
    def _calc_Bkl_cov_matrix_first_part(
            self,
            F_all_users: np.ndarray,
            k: int,
            N0_or_Rek: NumberOrArray = 0.0) -> np.ndarray:
    def _calc_Bkl_cov_matrix_second_part(self, Fk: np.ndarray, k: int,
                                         l: int) -> np.ndarray:
    def _calc_Bkl_cov_matrix_all_l(
            self,
            F_all_users: np.ndarray,
            k: int,
            N0_or_Rek: NumberOrArray = 0.0) -> np.ndarray:
    def _calc_JP_Bkl_cov_matrix_first_part_impl(
            self, Hk: np.ndarray, F_all_users: np.ndarray,
            Rek: NumberOrArray) -> np.ndarray:
    def _calc_JP_Bkl_cov_matrix_first_part(
            self,
            F_all_users: np.ndarray,
            k: int,
            noise_power: float = 0.0) -> np.ndarray:
    def _calc_JP_Bkl_cov_matrix_second_part_impl(Hk: np.ndarray,
                                                 Fk: np.ndarray,
                                                 l: int) -> np.ndarray:
    def _calc_JP_Bkl_cov_matrix_second_part(self, Fk: np.ndarray, k: int,
                                            l: int) -> np.ndarray:
    def _calc_JP_Bkl_cov_matrix_all_l(
            self,
            F_all_users: np.ndarray,
            k: int,
            N0_or_Rek: NumberOrArray = 0.0) -> np.ndarray:
    def _calc_SINR_k(self, k: int, Fk: np.ndarray, Uk: np.ndarray,
                     Bkl_all_l: np.ndarray) -> np.ndarray:
    def calc_SINR(self, F: np.ndarray, U: np.ndarray) -> np.ndarray:
    def _calc_JP_SINR_k_impl(Hk: np.ndarray, Fk: np.ndarray, Uk: np.ndarray,
                             Bkl_all_l: np.ndarray) -> np.ndarray:
    def _calc_JP_SINR_k(self, k: int, Fk: np.ndarray, Uk: np.ndarray,
                        Bkl_all_l: np.ndarray) -> np.ndarray:
    def calc_JP_SINR(self, F: np.ndarray, U: np.ndarray) -> np.ndarray:
    def __init__(self) -> None:
    def extIntK(self) -> int:
    def extIntNt(self) -> int:
    def Nr(self) -> np.ndarray:
    def Nt(self) -> np.ndarray:
    def K(self) -> int:
    def big_H_no_ext_int(self) -> np.ndarray:
    def H(self) -> np.ndarray:
    def H_no_ext_int(self) -> np.ndarray:
    def corrupt_data(  # type: ignore
            self, data: np.ndarray, ext_int_data: np.ndarray) -> np.ndarray:
    def corrupt_concatenated_data(self, data: np.ndarray) -> np.ndarray:
    def get_Hk_without_ext_int(self, k: int) -> np.ndarray:
    def get_Hk_with_ext_int(self, k: int) -> np.ndarray:
    def _prepare_input_parans(
        Nr: np.ndarray, Nt: np.ndarray, K: int, NtE: Iterable[int]
    ) -> Tuple[np.ndarray, np.ndarray, int, int, np.ndarray]:
    def init_from_channel_matrix(  # type: ignore
            self, channel_matrix: np.ndarray, Nr: np.ndarray, Nt: np.ndarray,
            K: int, NtE: Iterable[int]) -> None:
    def randomize(  # type: ignore
            self, Nr: IntOrIntArrayUnion, Nt: IntOrIntArrayUnion, K: int,
            NtE: Iterable[int]) -> None:
    def set_pathloss(self,
                     pathloss_matrix: Optional[np.ndarray] = None,
                     ext_int_pathloss: Optional[np.ndarray] = None) -> None:
    def calc_cov_matrix_extint_without_noise(self,
                                             pe: float = 1.0) -> np.ndarray:
    def calc_cov_matrix_extint_plus_noise(self, pe: float = 1.0) -> np.ndarray:
    def calc_Q(self,
               k: int,
               F_all_users: np.ndarray,
               pe: float = 1.0) -> np.ndarray:
    def _calc_JP_Q(self, k: int, F_all_users: np.ndarray) -> np.ndarray:
    def calc_JP_Q(self,
                  k: int,
                  F_all_users: np.ndarray,
                  pe: float = 1.0) -> np.ndarray:
    def calc_SINR(self,
                  F: np.ndarray,
                  U: np.ndarray,
                  pe: float = 1.0) -> np.ndarray:
    def _calc_JP_Bkl_cov_matrix_first_part(  # type: ignore
            self, F_all_users: np.ndarray, k: int,
            Rek: NumberOrArray) -> np.ndarray:
    def _calc_JP_Bkl_cov_matrix_second_part(self, Fk: np.ndarray, k: int,
                                            l: int) -> np.ndarray:
    def _calc_JP_SINR_k(self, k: int, Fk: np.ndarray, Uk: np.ndarray,
                        Bkl_all_l: np.ndarray) -> np.ndarray:
    def calc_JP_SINR(self,
                     F: np.ndarray,
                     U: np.ndarray,
                     pe: float = 1.0) -> np.ndarray:
        HK_H = Hk.conjugate().transpose()
        K = self.K
        K = self.K
        H = self._H_no_pathloss[0:self.K]
        H = MultiUserChannelMatrix.H.fget(self)  # type: ignore
        K = self.K
        K = self.K def block_diagonalize(mtChannel: np.ndarray, num_users: int, iPu: float,
                      noise_var: float) -> BdSolution:
def calc_receive_filter(newH: np.ndarray) -> np.ndarray:
def _calc_stream_reduction_matrix(Re_k: np.ndarray,
                                  kept_streams: int) -> np.ndarray:
def _calc_effective_throughput(sinrs: NumberOrArrayUnion, modulator: Modulator,
                               packet_length: int) -> float:
    def __init__(self, num_users: int, iPu: float, noise_var: float) -> None:
    def _calc_BD_matrix_no_power_scaling(self,
                                         mtChannel: np.ndarray) -> BdSolution:
    def _perform_global_waterfilling_power_scaling(
            self, Ms_bad: np.ndarray, Sigma: np.ndarray) -> np.ndarray:
    def _perform_normalized_waterfilling_power_scaling(
            self, Ms_bad: np.ndarray, Sigma: np.ndarray) -> np.ndarray:
    def block_diagonalize(self, mtChannel: np.ndarray) -> BdSolution:
    def block_diagonalize_no_waterfilling(self,
                                          mtChannel: np.ndarray) -> BdSolution:
    def calc_receive_filter(newH: np.ndarray) -> np.ndarray:
    def _get_tilde_channel(self, mtChannel: np.ndarray,
                           user: int) -> np.ndarray:
    def _get_sub_channel(
            self, mt_channel: np.ndarray,
            desired_users: Union[int, Iterable[int]]) -> np.ndarray:
    def __init__(self, num_users: int, iPu: float, noise_var: float,
                 pe: float) -> None:
    def calc_whitening_matrices(
            self,
            mu_channel: MultiUserChannelMatrixExtInt) -> List[np.ndarray]:
    def __init__(self, num_users: int, iPu: float, noise_var: float,
                 pe: float) -> None:
    def _calc_receive_filter_with_whitening(newH: np.ndarray,
                                            whitening_filter: np.ndarray,
                                            Nr: np.ndarray,
                                            Nt: np.ndarray) -> np.ndarray:
    def block_diagonalize_no_waterfilling(  # type: ignore
            self,
            mu_channel: MultiUserChannelMatrixExtInt) -> BdWithExtIntSolution:
    def __init__(self, num_users: int, iPu: float, noise_var: float,
                 pe: float) -> None:
    def set_ext_int_handling_metric(
            self,
            metric: Optional[str],
            metric_func_extra_args_dict: Optional[MetricExtraArgs] = None
    ) -> None:
    def metric_name(self) -> str:
    def calc_receive_filter_user_k(Heq_k_P: np.ndarray,
                                   P: Optional[np.ndarray] = None
                                   ) -> np.ndarray:
    def _calc_linear_SINRs(Heq_k_red: np.ndarray, Wk: np.ndarray,
                           Re_k: np.ndarray) -> np.ndarray:
    def _perform_BD_no_waterfilling_no_stream_reduction(
            self,
            mu_channel: MultiUserChannelMatrixExtInt) -> BdWithExtIntSolution:
    def _perform_BD_no_waterfilling_fixed_or_naive_reduction(
            self,
            mu_channel: MultiUserChannelMatrixExtInt) -> BdWithExtIntSolution:
    def _perform_BD_no_waterfilling_decide_number_streams(
            self,
            mu_channel: MultiUserChannelMatrixExtInt) -> BdWithExtIntSolution:
    def block_diagonalize_no_waterfilling(  # type: ignore
            self,
            mu_channel: MultiUserChannelMatrixExtInt) -> BdWithExtIntSolution:
    BD = BlockDiagonalizer(num_users, iPu, noise_var)
            (_, V1, S) = least_right_singular_vectors(
                np.dot(H_cur_user, tilde_V0),
                # Number of receive antennas minus number of desired
                # streams
                iNrU - nStreams)
        K = mu_channel.K
        K = Nr.size
            W = np.linalg.pinv(Heq_k_P)
            W = np.dot(np.linalg.pinv(np.dot(overbar_P, Heq_k_P)), overbar_P)
        K = mu_channel.K
        K = mu_channel.K
        K = mu_channel.K
class BlockDiagonalizer:
class BDWithExtIntBase(BlockDiagonalizer):
class WhiteningBD(BDWithExtIntBase):
class EnhancedBD(BDWithExtIntBase): def doWF(vtChannels: np.ndarray,
         dPt: float,
         noiseVar: float = 1.0,
         Es: float = 1.0) -> Tuple[np.ndarray, float]:     _MATPLOTLIB_AVAILABLE = True
    _MATPLOTLIB_AVAILABLE = False
PI = np.pi
        M = symbols.size
        BER = self.calcTheoreticalBER(SNR)
        PER = 1 - ((1 - BER)**packet_length)
        L = int(round(math.sqrt(M)))
        L = int(round(math.sqrt(M)))
class Modulator:
class PSK(Modulator):
class QPSK(PSK):  # pragma: no cover
class BPSK(Modulator):
class QAM(Modulator):
    def __init__(self) -> None:
    def name(self) -> str:
    def M(self) -> int:
    def K(self) -> int:
    def __repr__(self) -> str:  # pragma: no cover
    def setConstellation(self, symbols: np.ndarray) -> None:
    def plotConstellation(self) -> None:  # pragma: no cover
    def modulate(self, inputData: Union[int, np.ndarray]) -> np.ndarray:
    def demodulate(self, receivedData: np.ndarray) -> np.ndarray:
    def calcTheoreticalSER(
            self, SNR: NumberOrArray) -> NumberOrArray:  # pragma: no cover
    def calcTheoreticalBER(
            self, SNR: NumberOrArray) -> NumberOrArray:  # pragma: no cover
    def calcTheoreticalPER(self, SNR: NumberOrArray,
                           packet_length: int) -> NumberOrArray:
    def calcTheoreticalSpectralEfficiency(
            self,
            SNR: NumberOrArray,
            packet_length: Optional[int] = None) -> NumberOrArray:
    def __init__(self, M: int, phaseOffset: float = 0) -> None:
    def _createConstellation(M: int, phaseOffset: float) -> np.ndarray:
    def setPhaseOffset(self, phaseOffset: float) -> None:
    def calcTheoreticalSER(self, SNR: NumberOrArray) -> NumberOrArray:
    def calcTheoreticalBER(self, SNR: NumberOrArray) -> NumberOrArray:
    def __init__(self) -> None:
    def __repr__(self) -> str:  # pragma: no cover
    def __init__(self) -> None:
    def name(self) -> str:
    def __repr__(self) -> str:  # pragma: no cover
    def calcTheoreticalSER(self, SNR: NumberOrArray) -> NumberOrArray:
    def calcTheoreticalBER(self, SNR: NumberOrArray) -> NumberOrArray:
    def modulate(self, inputData: np.ndarray) -> np.ndarray:
    def demodulate(self, receivedData: np.ndarray) -> np.ndarray:
    def __init__(self, M: int) -> None:
    def _createConstellation(M: int) -> np.ndarray:
    def _calculateGrayMappingIndexQAM(L: int) -> np.ndarray:
    def _calcTheoreticalSingleCarrierErrorRate(
            self, SNR: NumberOrArray) -> NumberOrArray:
    def calcTheoreticalSER(self, SNR: NumberOrArray) -> NumberOrArray:
    def calcTheoreticalBER(self, SNR: NumberOrArray) -> NumberOrArray: @staticmethod
def calcProjectionMatrix(A: np.ndarray) -> np.ndarray:
    """
    Calculates the projection matrix that projects a vector (or a
    matrix) into the signal space spanned by the columns of `A`.

    Parameters
    ----------
    A : np.ndarray
        A matrix whose columns form a basis for the desired subspace.

    Returns
    -------
    np.ndarray
        The projection matrix that can be used to project a vector or a
        matrix into the subspace spanned by the columns of `A`

    See also
    --------
    calcOrthogonalProjectionMatrix

    Examples
    --------
    >>> A = np.array([[1 + 1j, 2 - 2j], [3 - 2j, 0], \
                      [-1 - 1j, 2 - 3j]])
    >>> # Matrix that projects into the subspace spanned by the columns
    >>> # of A
    >>> Q = calcProjectionMatrix(A)
    >>> np.allclose(Q.round(4), np.array( \
      [[ 0.5239+0.j, 0.0366+0.3296j, 0.3662+0.0732j], \
       [ 0.0366-0.3296j, 0.7690+0.j, -0.0789+0.2479j], \
       [ 0.3662-0.0732j, -0.0789-0.2479j, 0.7070-0.j]]))
    True
    """
    # MATLAB version: A/(A'*A)*A';
    A_H = A.conjugate().transpose()
    return (A.dot(np.linalg.inv(A_H.dot(A)))).dot(A_H) def dB2Linear(valueIndB: NumberOrArray) -> NumberOrArray:
    """
    Convert input from dB to linear scale.

    Parameters
    ----------
    valueIndB : int | float | np.ndarray
        Value in dB

    Returns
    -------
    valueInLinear : int | float | np.ndarray
        Value in Linear scale.

    Examples
    --------
    >>> dB2Linear(30)
    1000.0
    """
    return pow(10, valueIndB / 10.0) def linear2dB(valueInLinear: NumberOrArray) -> NumberOrArray:
    """
    Convert input from linear to dB scale.

    Parameters
    ----------
    valueInLinear : int | float | np.ndarray
        Value in Linear scale.

    Returns
    -------
    valueIndB : int | float | np.ndarray
        Value in dB scale.

    Examples
    --------
    >>> linear2dB(1000)
    30.0
    """
    return 10.0 * np.log10(valueInLinear)  # type: ignore def calc_shannon_sum_capacity(sinrs: NumberOrArrayUnion) -> float:
    """
    Calculate the sum of the Shannon capacity of the values in `sinrs`

    Parameters
    ----------
    sinrs : float | np.ndarray
        SINR values (in linear scale).

    Returns
    -------
    sum_capacity : float
        Sum capacity.

    Examples
    --------
    >>> calc_shannon_sum_capacity(11.4)
    3.6322682154995127
    >>> calc_shannon_sum_capacity(20.3)
    4.412781525338476
    >>> sinrs_linear = np.array([11.4, 20.3])
    >>> print(calc_shannon_sum_capacity(sinrs_linear))
    8.045049740837989
    """
    sum_capacity = np.sum(np.log2(1 + sinrs))

    return cast(float, sum_capacity) def calc_whitening_matrix(cov_matrix: np.ndarray) -> np.ndarray:
    """
    Calculates the whitening matrix that can be applied to a data vector
    whose covariance matrix is ``cov_matrix`` so that the new vector
    covariance matrix is an identity matrix

    Parameters
    ----------
    cov_matrix : np.ndarray
        The covariance matrix of the original data that will be
        decorrelated. This must be a symmetric and positive semi-definite
        matrix

    Returns
    -------
    whitening_matrix : np.ndarray
        The whitening matrix :math:`\\mtW_W`. If the original data is a
        vector :math:`$\\vtX$` it can be whitened with
        :math:`\\mtW_W^H \\vtX`.

    Notes
    -----
    The returned `whitening_matrix` matrix will make the covariance of the
    filtered data an identity matrix. If you only need the the covariance
    matrix of the filtered data to be a diagonal matrix (not necessarily an
    identity matrix) what you want to calculate is the "decorrelation
    matrix". See the :func:`calc_decorrelation_matrix` function for that.

    See also
    --------
    calc_decorrelation_matrix
    """
    L, V = np.linalg.eig(cov_matrix)
    W = np.dot(V, np.diag(1. / (L**0.5)))
    return W def randn_c(*args: int) -> np.ndarray:
    """
    Generates a random circularly complex gaussian matrix.

    Parameters
    ----------
    *args : any
        Variable number of arguments (int values) specifying the
        dimensions of the returned array. This is directly passed to the
        numpy.random.randn function.

    Returns
    -------
    result : np.ndarray
        A random N-dimensional numpy array (complex dtype) where the `N` is
        equal to the number of parameters passed to `randn_c`.

    Examples
    --------
    >>> a = randn_c(4,3)
    >>> a.shape
    (4, 3)
    >>> a.dtype
    dtype('complex128')

    """
    # noinspection PyArgumentList
    return (1.0 / math.sqrt(2.0)) * (np.random.randn(*args) +
                                     (1j * np.random.randn(*args)))
import doctest
import unittest
import numpy as np
from scipy.linalg import block_diag
from pyphysim.channels import multiuser
from pyphysim.comm import blockdiagonalization, waterfilling
from pyphysim.modulators import fundamental
from pyphysim.subspace.projections import calcProjectionMatrix
from pyphysim.util.conversion import dB2Linear, linear2dB
from pyphysim.util.misc import (calc_shannon_sum_capacity,
                                calc_whitening_matrix, randn_c)
        enhancedBD_obj = blockdiagonalization.EnhancedBD(K, iPu, noise_var, pe)

        # xxxxx Test if an assert is raised for invalid arguments xxxxxxxxx
        with self.assertRaises(AttributeError):
            enhancedBD_obj.set_ext_int_handling_metric('lala')

        with self.assertRaises(AttributeError):
            # If we set the metric to effective_throughput but not provide
            # the modulator and packet_length attributes.
            enhancedBD_obj.set_ext_int_handling_metric('effective_throughput')

        with self.assertRaises(AttributeError):
            enhancedBD_obj.set_ext_int_handling_metric('naive')
        # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

        # xxxxx Test setting the metric to effective_throughput xxxxxxxxxxx
        psk_obj = fundamental.PSK(4)
        enhancedBD_obj.set_ext_int_handling_metric('effective_throughput', {
            'modulator': psk_obj,
            'packet_length': 120
        })
        self.assertEqual(enhancedBD_obj._metric_func,
                         blockdiagonalization._calc_effective_throughput)
        self.assertEqual(enhancedBD_obj.metric_name, "effective_throughput")

        metric_func_extra_args = enhancedBD_obj._metric_func_extra_args
        self.assertEqual(metric_func_extra_args['modulator'], psk_obj)
        self.assertEqual(metric_func_extra_args['packet_length'], 120)
        # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

        # xxxxx Test setting the metric to capacity xxxxxxxxxxxxxxxxxxxxxxx
        enhancedBD_obj.set_ext_int_handling_metric('capacity')
        self.assertEqual(enhancedBD_obj._metric_func,
                         calc_shannon_sum_capacity)
        self.assertEqual(enhancedBD_obj.metric_name, "capacity")
        # metric_func_extra_args is an empty dictionary for the capacity
        # metric
        self.assertEqual(enhancedBD_obj._metric_func_extra_args, {})
        # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

        # xxxxx Test setting the metric to None xxxxxxxxxxxxxxxxxxxxxxxxxxx
        enhancedBD_obj.set_ext_int_handling_metric(None)
        self.assertIsNone(enhancedBD_obj._metric_func)
        self.assertEqual(enhancedBD_obj.metric_name, "None")

        # metric_func_extra_args is an empty dictionary for the None metric
        self.assertEqual(enhancedBD_obj._metric_func_extra_args, {})

        # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

        # xxxxx Test setting the metric to naive xxxxxxxxxxxxxxxxxxxxxxxxxx
        enhancedBD_obj.set_ext_int_handling_metric('naive', {'num_streams': 2})
        self.assertIsNone(enhancedBD_obj._metric_func)
        self.assertEqual(enhancedBD_obj.metric_name, "naive")

        metric_func_extra_args = enhancedBD_obj._metric_func_extra_args
        self.assertEqual(metric_func_extra_args['num_streams'], 2)
        # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    def test_calc_receive_filter(self) -> None:
        # Equivalent channel without including stream reduction
        Heq_k = randn_c(3, 3)
        Re_k = randn_c(3, 2)
        Re_k = np.dot(Re_k, Re_k.transpose().conjugate())

        P1 = blockdiagonalization._calc_stream_reduction_matrix(Re_k, 1)
        P2 = blockdiagonalization._calc_stream_reduction_matrix(Re_k, 2)
        P3 = blockdiagonalization._calc_stream_reduction_matrix(Re_k, 3)

        # Equivalent channels with the stream reduction
        Heq_k_P1 = np.dot(Heq_k, P1)
        Heq_k_P2 = np.dot(Heq_k, P2)
        Heq_k_P3 = np.dot(Heq_k, P3)

        W1 = blockdiagonalization.EnhancedBD.calc_receive_filter_user_k(
            Heq_k_P1, P1)
        W2 = blockdiagonalization.EnhancedBD.calc_receive_filter_user_k(
            Heq_k_P2, P2)
        W3 = blockdiagonalization.EnhancedBD.calc_receive_filter_user_k(
            Heq_k_P3, P3)
        # Note that since P3 is actually including all streams, then the
        # performance is the same as if we don't reduce streams. However W3
        # and W_full are different matrices, since W3 has to compensate the
        # right multiplication of the equivalent channel by P3 and W_full
        # does not. The performance is the same because no energy is lost
        # due to stream reduction and the Frobenius norms of W3 and W_full
        # are equal.
        W_full = blockdiagonalization.EnhancedBD.calc_receive_filter_user_k(
            Heq_k)

        np.testing.assert_array_almost_equal(np.dot(W1, np.dot(Heq_k, P1)),
                                             np.eye(1))
        np.testing.assert_array_almost_equal(np.dot(W2, np.dot(Heq_k, P2)),
                                             np.eye(2))
        np.testing.assert_array_almost_equal(np.dot(W3, np.dot(Heq_k, P3)),
                                             np.eye(3))
        np.testing.assert_array_almost_equal(np.dot(W_full, Heq_k), np.eye(3))

        overbar_P2 = calcProjectionMatrix(P2)
        expected_W2 = np.dot(
            np.linalg.pinv(np.dot(overbar_P2, np.dot(Heq_k, P2))), overbar_P2)
        np.testing.assert_array_almost_equal(expected_W2, W2)

    # TODO: Implement-me
    def test_calc_linear_SINRs(self) -> None:
        # Heq_k_red = np.array([[2, 2], [1, 2]])
        # # Usually this will be the inverse of Heq_k_red, but for testing
        # # purposes we can specify a different Wk
        # Wk = np.array([[1, 1], [1.5, 0.5]])
        # Rk = np.array([[0.5, 0.2], [0.25, 0.1]])
        # SINRs = blockdiagonalization.EnhancedBD._calc_linear_SINRs(
        #     Heq_k_red, Wk, Rk)
        # print SINRs
        pass

    def test_calc_effective_throughput(self) -> None:
        psk_obj = fundamental.PSK(8)
        packet_length = 60

        SINRs_dB = np.array([11.4, 20.3])

        sinrs_linear = dB2Linear(SINRs_dB)