"""
This a Random Multitone Signal Generator module. |br|

It is able to generate *N* multitone random signals according to settings
given by a user. |br|

*Examples*:
    Please go to the *examples/signals* directory for examples on how to use 
    the generator. |br|

*Settings*:
    Parameters of the generator described below.

    Take a look on '__parametersDefine' function for more info on the 
    parameters.

    Parameters of the generator are attributes of the class which must/can
    be set before the generator run.

    Required parameters:

    - a. **tS** (*float*): time of a signals

    - b. **fR** (*float*): signals' representation sampling frequency

    - c. **fRes** (*float*): tones frequency resolution


    Optional parameters:

    - d. **fMax** (*float*): maximum frequency present in signals [default = 0.4 * fR]

    - e. **fMin** (*float*): minimum allowed frequency present in the spectrum  [default = fRes]

    - f. **nSigs** (*int*): the number of signals to be generated  [default = 1]

    - g. **iSNR** (*float*): level of noise in signals [dB] (SNR)  [default = +int]

    - h. **iP** (*float*): requested power of signals  [default = do not regulate the power]


    - i. **vFrqs** (*Numpy array 1D*): vector with requested frequencies of tones  [default = empty]

    - j. **vAmps** (*Numpy array 1D*): vector with requested amplitudes of tones   [default = empty]

    - k. **vPhs** (*Numpy array 1D*): vector with requested phases of tones        [default = empty]

    
    - l. **nTones** (*int*): the number of additional tones    [default = 0]

    - m. **iMinAmp** (*float*): min amplitude of a tone present in a signal      [default = 0.1]

    - n. **iGraAmp** (*float*): gradation of a random amplitude of a tone        [default = 0.1]

    - o. **iMaxAmp** (*float*): max amplitude of a tone present in a signal      [default = 1.0]

    - p. **iMinPhs** (*float*): min allowed phase of a tone present in a signal  [default = -179 deg]

    - q. **iGraPhs** (*float*): gradation of a random phase of a tone            [default = 1 deg]

    - r. **iMaxPhs** (*float*): max allowed phase of a tone present in a signal  [default = +180 deg]


    - s. **bMute** (*int*): mute the console output from the generator [default = 0]


*Output*:
    Description of the generator output is below. 
    This is the list of attributes of the generator class which are available 
    after calling the 'run' method:

    - a. **mSig** (*Numpy array 2D*): Matrix with output signals

    - b. **mSigNN** (*Numpy array 2D*): Matrix with nonnoisy output signals

    - c. **nSmp** (*int*): The number of samples in the signals

    - d. **vTSig** (*Numpy array 1D*): The time vector for the generated signals (name alias vT is available)

    - e. **vP** (*Numpy array 1D*): Power of the signals

    - f. **vPNN** (*Numpy array 1D*): Power of the non noisy signals

    - g. **vPCoef** (*Numpy array 1D*): Power adjustment coefficients

    - h. **mFrqs** (*Numpy array 2D*): Frequencies of tones in the signals

    - i. **mAmps** (*Numpy array 2D*): Amplitudes of tones in the signals

    - j. **mPhs** (*Numpy array 2D*): Phases of tones in the signals

    - k. **mAmPh** (*Numpy array 2D*): Complex matrix with amplitudes/phases of tones

    - l. **fFFTR** (*float*): Signal FFT frequency resolution

*Author*:
    <NAME>, Aalborg University, Denmark. <<EMAIL>>

*Version*:
    0.1    | 15-MAY-2014 : * Initial version. |br|
    0.2    | 16-MAY-2014 : * Docstrings added. |br|
    0.3    | 19-MAY-2014 : * The main func. divided into smaller functions. |br|
    0.4    | 20-MAY-2014 : * Errors are served by 'raise'. |br|
    0.5    | 20-MAY-2014 : * Docstrings are added to the internal functions. |br|
    0.5r1  | 20-MAY-2014 : * Order of the internal functions was changed. |br|
    1.0    | 20-MAY-2014 : * Version 1.0 released. |br|
    1.0r1  | 21-MAY-2014 : * Error in progress printing is fixed. |br|
    1.0r2  | 21-MAY-2014 : * Error in default random amplitude is fixed. |br|
    1.0r3  | 21-MAY-2014 : * Error in progress printing (if muted) is fixed. |br|
    1.0r4  | 27-MAY-2014 : * Error in the number of given frqs is fixed. |br|
    1.0r5  | 27-MAY-2014 : * Error in the vector with signal time is fixed. |br|
    2.0    | 21-JUL-2015 : * Objectified version (2.0) |br|
    2.0r1  | 18-AUG-2015 : * Adjusted to RxCSobject v1.0 |br|
    2.1    | 02-SEP-2015 : * Max frequency in the spectrum is an optional parameter
                             printing improvements. |br|

*License*:
    BSD 2-Clause
"""

from __future__ import division
import numpy as np
import rxcs


class randMult(rxcs._RxCSobject):

    def __init__(self, *args):
        rxcs._RxCSobject.__init__(self)    # Make it a RxCS object 
        
        self.strRxCSgroup = 'Signal generator'  # Name of group of RxCS modules
        self.strModuleName = 'Random multitone'     # Module name        

        self.__parametersDefine()      # Define the parameters

    # Define parameters
    def __parametersDefine(self):

        # Time of the signal [s]
        self.paramAddMan('tS', 'Time of the signal', unit='s')
        self.paramType('tS', (int, float))
        self.paramH('tS', 0)
        self.paramL('tS', np.inf)       

        # The signal representation sampling frequency [Hz]
        self.paramAddMan('fR', 'The signal representation sampling frequency', unit='Hz')
        self.paramType('fR', (int, float))
        self.paramH('fR', 0)
        self.paramL('fR', np.inf)
 
        # The highest possible frequency in the signal [Hz]
        self.paramAddOpt('fMax', 'The highest possible frequency in the signal', unit='Hz', default='$$fR', mul=0.4)
        self.paramType('fMax', (int, float))
        self.paramH('fMax', 0)
        self.paramL('fMax', 'fR', mul=0.5)  # Nyquist principle

        # Signal spectrum resolution [Hz]
        self.paramAddMan('fRes', 'Signal spectrum resolution', unit='Hz')
        self.paramType('fRes', (int, float))
        self.paramH('fRes', 0)
        self.paramL('fRes', np.inf)


        # The minimum frequency of additional tones
        self.paramAddOpt('fMin', 'Minimum frequency of additional tones', default='$$fRes', unit='Hz')
        self.paramType('fMin', (int, float))
        self.paramH('fMin', 0)
        self.paramLE('fMin', 'fMax')

        # The number of signals to be generated
        self.paramAddOpt('nSigs', 'The number of signals to be generated', default=1)
        self.paramType('nSigs', (int))
        self.paramH('nSigs', 0)            # The number of signals must be higher than zero
        self.paramL('nSigs', np.inf)       # ...and lower than infinity

        # Signal noise [dB]
        self.paramAddOpt('iSNR', 'Signal to noise ratio', default=np.inf, unit='dB')
        self.paramType('iSNR', (int, float))

        # Power of signals [W]
        self.paramAddOpt('iP', 'Power of signals', default=np.nan, unit='W')
        self.paramType('iP', (int, float))
        self.paramH('iP', 0)
        self.paramL('iP', np.inf)

        # Vector with given frequencies of signal cosine tones
        self.paramAddOpt('vFrqs', 'Vector with given frequencies of signal cosine tones', default=np.zeros(0), noprint=1)
        self.paramType('vFrqs', np.ndarray)        
        strError = 'Frequencies in \'vFrqs\' vector must not be higher than the highest possible in the signal spectrum!'
        self.paramLE('vFrqs', 'fMax', errnote=strError)           
        strError = 'Frequencies in the \'vFrqs\' must be higher than 0!'
        self.paramH('vFrqs', 0, errnote=strError)   
        strError = 'Frequencies in \'vFrqs\' vector must not be lower than the lowest possible in the signal spectrum!'
        self.paramHE('vFrqs', 'fMin', errnote=strError)
        strError = 'Size of the vector with given frequencies \'vFrqs\' must be equal to size of the vectors \'vAmps\' and \'vPhs\''
        self.paramSizEq('vFrqs', 'vAmps', errnote=strError)
        self.paramSizEq('vFrqs', 'vPhs', errnote=strError)
        self.paramNDimEq('vFrqs', 1)
        strError = 'There are frequencies repeated in the \'vFrqs\' vector!'
        self.paramUnique('vFrqs', errnote=strError)
        self.NaNAllowedEl('vFrqs')


        # Vector with given amplitudes of signal cosine tones
        self.paramAddOpt('vAmps', 'Vector with given amplitudes of signal cosine tones', default=np.zeros(0), noprint=1)
        self.paramType('vAmps', np.ndarray)
        self.paramH('vAmps', 0, errnote='Amplituides of tones must be higher than 0')
        self.paramNDimEq('vAmps', 1)
        self.NaNAllowedEl('vAmps')

        # Vector with given phases of signal cosine tones content
        self.paramAddOpt('vPhs', 'Vector with given phases of signal cosine tones', default=np.zeros(0), noprint=1)
        self.paramType('vPhs', np.ndarray)
        self.paramH('vPhs', -180, errnote='Phases of tones must be higher than -180 [deg]')
        self.paramLE('vPhs', 180, errnote='Phases of tones must be lower or equal to 180 [deg]')
        self.paramNDimEq('vPhs', 1)
        self.NaNAllowedEl('vPhs')

        # The number of additional tones
        self.paramAddOpt('nTones', 'The number of additional tones', default=0)
        self.paramType('nTones', (int))
        self.paramHE('nTones', 0)

        # The boundaries for amplitudes: amplitude min value
        self.paramAddOpt('iMinAmp', 'Minimum value of random amplitudes', default=0.1, unitprefix=' ')
        self.paramType('iMinAmp', (int, float))
        self.paramH('iMinAmp', 0)
        self.paramLE('iMinAmp','iMaxAmp')
 
        # The boundaries for amplitudes: amplitude gradation
        self.paramAddOpt('iGraAmp', 'Gradation of value of random amplitudes', default=0.1, unitprefix=' ')
        self.paramType('iGraAmp', (int, float))
        self.paramH('iGraAmp', 0)
        
        # The boundaries for amplitudes: amplitude max value
        self.paramAddOpt('iMaxAmp', 'Maximum value of random amplitudes', default=1.0, unitprefix=' ')
        self.paramType('iMaxAmp', (int, float))
        self.paramH('iMaxAmp', 0)

        # The boundaries for amplitudes: phase min value
        self.paramAddOpt('iMinPhs', 'Minimum value of random phase', default=-179, unitprefix=' ')
        self.paramType('iMinPhs', (int, float))
        self.paramH('iMinPhs',-180)
        self.paramLE('iMinPhs',180)
        self.paramLE('iMinPhs','iMaxPhs')
        
        # The boundaries for amplitudes: phase gradation
        self.paramAddOpt('iGraPhs', 'Gradation of value of random phase', default=1, unitprefix=' ')
        self.paramType('iGraPhs', (int, float))
        self.paramH('iGraPhs', 0)

        # The boundaries for amplitudes: phase max value
        self.paramAddOpt('iMaxPhs', 'Maximum value of random phase', default=180, unitprefix=' ')
        self.paramType('iMaxPhs', (int, float))
        self.paramH('iMaxPhs',-180)
        self.paramLE('iMaxPhs',180)

        # 'Mute the output' flag
        self.paramAddOpt('bMute', 'Mute the output', noprint=1, default=0)
        self.paramType('bMute', int)           # Must be of int type
        self.paramAllowed('bMute',[0, 1])      # It can be either 1 or 0


    # Run
    def run(self):

        self.parametersCheck()         # Check if all the needed partameters are in place and are correct
        self.parametersPrint()         # Print the values of parameters
        
        self.engineStartsInfo()  # Info that the engine starts
        self.__engine()          # Run the engine
        self.engineStopsInfo()   # Info that the engine ends
        return self.__dict__     # Return dictionary with the parameters


    # Engine of the function
    def __engine(self):
        self._checkConf()

        # - - - - - - - - - - - - - - - - - - -        
        # Signal generation starts here:
        
        self.mFrqsInx = self._drawFreq(self.vFrqs, self.nTones, self.fMin, self.fMax, self.nSigs, self.fRes)         # Draw frequencies of the signals
        self.mAmps = self._drawAmps(self.vAmps, self.nTones, self.nSigs, self.iMinAmp, self.iGraAmp, self.iMaxAmp)   # Draw amplitudes of the signals
        self.mPhs = self._drawPhases(self.vPhs, self.nTones, self.nSigs, self.iMinPhs, self.iGraPhs, self.iMaxPhs)   # Draw phases of the signals
        
        # Generate the signals by IFFT
        (mSig, self.mAmPh, self.mFrqs, self.fFFTR) = \
            self._genSigs(self.mFrqsInx, self.mAmps, self.mPhs, self.nSigs, self.tS, self.fR, self.fRes)
    
        # - - - - - - - - - - - - - - - - - - -
    
        # Adjust the signal power
        (mSig, vP, self.vPCoef, self.mAmps, self.mAmPh) = self._adjPower(mSig, self.iP, self.mAmps, self.mAmPh)
    
        # Add the AWGN noise to the signals
        (self.mSigNN, self.vPNN, self.mSig, self.vP) = self._addNoise(mSig, vP, self.iSNR)
        
        # Generate the time vector for the signal
        self.vTSig = np.arange(self.nSmp) / self.fR 
        self.vT = self.vTSig   # Name alias for vTSig is vT

        return


    def _checkConf(self):
        """
        This function checks the configuration of the generator.    
        """
        #----------------------------------------------------------------------
        # If the minimum frequency was not given, it is equal to the frequency resolution
        if np.isnan(self.fMin):
            self.fMin = self.fRes
        
        #----------------------------------------------------------------------
        # Check the lowest possible frequency in the signal vs spectrum
        # resolution
        strErr = 'The lowest possible frequency in the signal is not a multiple '
        strErr = strErr + 'of the signal spectrum resolution'
        if (round(self.fMin/self.fRes) - self.fMin/self.fRes) > 1e-15:
            raise ValueError(strErr)
                  
        #----------------------------------------------------------------------
        # Check the highest possible frequency in the signal vs spectrum
        # resolution
        strErr = 'The highest possible frequency in the signal is not a multiple '
        strErr = strErr + 'of the signal spectrum resolution'
        if (round(self.fMax/self.fRes) - self.fMax/self.fRes) > 1e-15:
            raise ValueError(strErr)
    
        #----------------------------------------------------------------------
        # Check if there is a space for all the frequencies requested in
        # the signal, both given in the vFrqs vector and requested to be 
        # chosen randomly 
    
        # Compute the total number of tones in the max possible spectrum
        nSpectTones = int(self.fMax/self.fRes) - int(self.fMin/self.fRes) + 1 
                                                                               
        nFG = self.vFrqs.size          # The number of frequencies given in the vector with frequencies
        nSigTones = nFG + self.nTones  # The total number of tones which will be present in the signal

        strErr = 'The signal spectrum consists of %d tones. ' % (nSpectTones)
        strErr = strErr + 'I can not put there %d [vFrqs] + %d [nTones] tones' \
            % (nFG, self.nTones)
        if nSpectTones < nSigTones:
            raise ValueError(strErr)

        #----------------------------------------------------------------------
        # Check if there is a frequency leackage
        self.nSmp = int(round(self.tS*self.fR))  # Calculate the number of samples in the signals
        fFFTR = self.fR/self.nSmp    # Calculate the FFT frequency resolution
    
        if abs(round(self.fRes/fFFTR) - self.fRes/fFFTR) > 0:
            strErr = ('Frequency leackage! Signal spectrum resolution can not be ')
            strErr = strErr + ('represented with the current signal parameters!')
            raise ValueError(strErr)
    
        #----------------------------------------------------------------------
        # Check the vector with given frequencies
    
        # Check the vector with given frequencies, if it is longer then 0
        if nFG > 0:
    
            # Create the vector with given and specified frequencies (not np.nan)
            vFrqs_ = self.vFrqs[np.isnan(self.vFrqs) == 0]

            # 1. check resolution of given frequencies:
            if np.abs(np.sum(np.round(vFrqs_/self.fRes) - (vFrqs_/self.fRes))) > 1e-15:
                strErr = ('A frequency given in the vFrqs vector is ')
                strErr = strErr + ('incoherent with the resolution of signal ')
                strErr = strErr + ('spectrum!\n')
                raise ValueError(strErr)
    
            # Do correction of possible representation errors in the vector with frequencies
            self.vFrqs = np.round(self.vFrqs/self.fRes)*self.fRes
                        
        #----------------------------------------------------------------------
        return


    # =================================================================
    # Draw frequencies of the signals
    # =================================================================
    def _drawFreq(self, vFrqs, nTones, fMin, fMax, nSigs, fRes):
        """
        This function draws frequencies of tones for all the signals
        according to the rules specified by users,
    
        Args:
            vFrqs (vector):  vector with specified frequencies
            nTones (int):    the number of additional tones
            fMin (int):      the min allowed frequency in the signal spectrum
            fMax (int):      the max allowed frequency in the signal spectrum
            nSigs (int):     the number of signals to be generated
            fRes (int):      signal spectrum resolution
                             (distance between the tones in the spectrum)
    
        Returns:
            mFrqsInx (matrix):  matrix with frequencies of tones for all
                                the signals (one row - one signal)
                                The frequencies are represented as indices of
                                frequencies from the allowed signal spectrum.
    
        """
       
        #----------------------------------------------------------------------
        # Recalculate frequencies to indices of tones in the spectrum
    
        # Recalculate the vector with frequencies from frequency to index
        # of a frequency in the spectrum
        vFrqsInx = (vFrqs / fRes)
    
        # Create a vector with GIVEN (not nan) indices of frequencies in the
        # vFrqs vector
        vFrqsInx_ = (vFrqsInx[np.isnan(vFrqsInx) == 0]).astype(int)
    
        #----------------------------------------------------------------------
        # Create the vector with indices of avaialble frequencies in the spectrum
    
        # Create a vector with indices of all the available tones in the spectrum
        vSpecInx = np.arange(1, int(fMax/fRes) + 1)
    
        # Boolean vector which indicates if the frequency is free
        vFreqIsFree = np.ones(int(fMax/fRes)).astype(bool)
        
        # Mark all the frequencies below min frequency as unavailable 
        for inxF in np.arange(int(fMin/fRes)-1):
            vFreqIsFree[inxF] = 0
    
        # Mark the frequencies taken by vFreq vector as unavailable
        vFreqIsFree[vFrqsInx_ - 1] = 0
    
        # Create the vector with indices of available frequencies
        vAvailFreqsInx = vSpecInx[vFreqIsFree]
    
        #----------------------------------------------------------------------
        # Construct a vector with indices of frequencies for all the needed signals
    
        # Add unknown frequencies of the additional tones to the vFrqsInx vector
        vFrqsInx = np.concatenate((vFrqsInx, np.nan*np.zeros(nTones)))
    
        # Calculate the number of missing frequencies in the vector with
        # frequencies
        iMissF = len(vFrqsInx) - len(vFrqsInx_)
    
        # Construct a matrix with indices of frequencies for all the needed signals
        mFrqsInx = np.tile(vFrqsInx, (nSigs, 1))
    
        #----------------------------------------------------------------------
        # Draw the frequencies
        for inxSig in np.arange(nSigs):
    
            # Permute all the indices of frequencies in the spectrum
            vPermutedFreqsInx = ((np.random.permutation(vAvailFreqsInx)).T)
    
            # From the permuted indices of frequencices take as many
            # as it is missing
            vTakenFreqsInx = vPermutedFreqsInx[np.arange(iMissF)]
    
            # Put the taken indices of frequencies to the matrix with frequency
            # indices for all the signals
            mFrqsInx[inxSig, np.isnan(mFrqsInx[inxSig, :])] = vTakenFreqsInx
    
        return mFrqsInx


    # =================================================================
    # Draw amplitudes of the signals
    # =================================================================
    def _drawAmps(self, vAmps, nTones, nSigs, iMinAmp, iGraAmp, iMaxAmp):
        """
        This function draws amplitudes of tones for all the signals
        according to the rules specified by users,
    
        Args:
            vAmps (vector):  vector with specified amplitudes of tones in signals
            nTones (int):    the number of additional tones
            nSigs (int):     the number of signals to be generated
            iMinAmp (int):   min amplitude of a random tone present in a signal
            iGraAmp (int):   gradation of a amplitude of a random tone
            iMaxAmp (int):   max amplitude of a random tone present in a signal
    
        Returns:
            mAmps (matrix):  matrix with amplitudes of tones for all
                             the signals (one row - one signal)
    
        """
    
        # Add unknown amplitudes of the additional tones to the vAmps vector
        vAmps = np.concatenate((vAmps, np.nan*np.zeros(nTones)))
    
        # Compute the number of missing amplitudes for every signal
        iMissA = (vAmps[np.isnan(vAmps)]).size
    
        #----------------------------------------------------------------------
    
        # Compute the number of possible amplitude values
        nAmpVal = np.floor((iMaxAmp - iMinAmp) / iGraAmp) + 1
    
        #----------------------------------------------------------------------
    
        # Draw the missing amplitudes for all the signals
        vDrawAmps = \
            iMinAmp + iGraAmp*(np.random.randint(0, nAmpVal, (nSigs*iMissA)))
    
        # Construct a matrix with amplitudes of tones for all the needed signals
        mAmps = np.tile(vAmps, (nSigs, 1))
    
        # Put the draw amplitudes to the matrix with amplitudes of tones for
        # all the needed signals
        mAmps[np.isnan(mAmps)] = vDrawAmps
    
        return mAmps
    
    
    # =================================================================
    # Draw phases of the signals
    # =================================================================
    def _drawPhases(self, vPhs, nTones, nSigs, iMinPhs, iGraPhs, iMaxPhs):
        """
        This function draws phases of tones for all the signals
        according to the rules specified by users,
    
        Args:
            vPhs (vector):   vector with specified phases of tones in signals
            nTones (int):    the number of additional tones
            nSigs (int):     the number of signals to be generated
            iMinPhs (int):   min phase of a random tone present in a signal
            iGraPhs (int):   gradation of a phase of a random tone
            iMaxPhs (int):   max phase of a random tone present in a signal
    
        Returns:
            mPhs (matrix):   matrix with phases of tones for all
                             the signals (one row - one signal)
    
        """
    
        # Add unknown phases of the additional tones to the vAmps vector
        vPhs = np.concatenate((vPhs, np.nan*np.zeros(nTones)))
    
        # Compute the number of missing phases for every signal
        iMissP = (vPhs[np.isnan(vPhs)]).size
    
        #----------------------------------------------------------------------
    
        # Compute the number of possible phase values
        nPhsVal = np.floor((iMaxPhs - iMinPhs)/iGraPhs) + 1
    
        #----------------------------------------------------------------------
    
        # Draw the missing phases for all the signals
        vDrawPhs = \
            iMinPhs + iGraPhs*(np.random.randint(0, nPhsVal, (nSigs*iMissP)))
    
        # Construct a matrix with phases of tones for all the needed signals
        mPhs = np.tile(vPhs, (nSigs, 1))
    
        # Put the draw phases to the matrix with phases of tones for
        # all the needed signals
        mPhs[np.isnan(mPhs)] = vDrawPhs
    
        return mPhs
    
    
    # =================================================================
    # Generate the signals by IFFT
    # =================================================================
    def _genSigs(self, mFrqsInx, mAmps, mPhs, nSigs, tS, fR, fRes):
        """
        This function generate the multitone signals using the IFFT algorithm.
    
        Args:
            mFrqsInx (matrix):  matrix with freqs of tones for all the signals
                                (as indices of tones in the allowed spectrum)
            mAmps (matrix):     matrix with amplitudes of tones for all the signals
            mPhs (matrix):      matrix with phases of tones for all the signals
            nSigs (float):      the number of signals
            tS (float):         time of the signals
            fR (float):         signal representation sampling frequency
            fRes (float):       signal spectrum resolution
                                (distance between the tones in the spectrum)
    
        Returns:
            mSig (matrix):   matrix with signals (one row - one signal)
            mAmPh (float):   complex matrix with amplitudes/phases of tones
            mFrqs (matrix):  matrix with freqs of tones for all the signals
            fFFTR (float):   signal FFT frequency resolution
        """
    
        # Calculate the number of samples in the signals
        nSmp = int(round(tS*fR))
    
        # Calculate the FFT frequency resolution
        fFFTR = fR/nSmp
    
        #----------------------------------------------------------------------
    
        # Adjust the amplitudes value to the number of points
        mAmpsAdj = mAmps * nSmp/2
    
        # Change phases into radians
        mPhsRad = mPhs*np.pi/180
    
        # Generate a one complex matrix for all the signals and its conjugated copy
        mAmPh = mAmpsAdj*np.cos(mPhsRad) + 1j*mAmpsAdj*np.sin(mPhsRad)
        mAmPh_conj = np.conjugate(mAmPh)
    
        #----------------------------------------------------------------------
        # Put the complex matrix with amplitudes and phases of tones into
        # one matrix dedicated for IFFT
    
        # Recalculate the matrix with indices of frequencies in the spectrum
        # to real frequencies
        mFrqs = mFrqsInx*fRes
    
        # Recalculate the matrix with indices of frequencies in the spectrum
        # to indices of frequencies in the IFFT transform
        mIFFTFrqsInx = np.around(mFrqs/fFFTR).astype(int)
    
        # Allocate the vector for the ifft coefficients for all the signals
        # (one signal in one row)
        mIFFT = np.zeros((nSigs, nSmp)) + 1j*np.zeros((nSigs, nSmp))
    
        # Put the complex vector with tones values into the IFFT matrix
        for inxSig in np.arange(nSigs):
    
            # IFFT indices of tones for the current signal
            vInx = mIFFTFrqsInx[inxSig, :]
    
            # Put the tones for the current signal
            mIFFT[inxSig, vInx] = mAmPh[inxSig, :]
    
            # IFFT indices of conjugate tones for the current signal
            vInxConj = (nSmp - mIFFTFrqsInx[inxSig, :]).astype(int)
    
            # Put the conjugate tones for the current signal
            mIFFT[inxSig, vInxConj] = mAmPh_conj[inxSig, :]
    
        #----------------------------------------------------------------------
        # Generate the signals (perform the IFFT)
        mSig = np.fft.ifftn(mIFFT, axes=[1]).real
    
        return (mSig, mAmPh, mFrqs, fFFTR)
    
    
    # =================================================================
    # Adjust the signal power
    # =================================================================
    def _adjPower(self, mSig, iP, mAmps, mAmPh):
        """
        This function adjustes powers of the generated signals.
        If the requested power of the signals is equal to NaN or inf, then
        the signals are not adjusted.
    
        Args:
            mSig (matrix):   matrix with signals (one row - one signal)
            iP (float):      requested power of the signals
            mAmps (matrix):  matrix with amplitudes of tones in the signals
            mAmPh (matrix):  complex matrix with amplitudes/phases of tones
    
        Returns:
            mSig (matrix):   matrix with noisy signals
            vP (vector):     vector with powers of noisy signals
            vPCoef (vector): vector with coefficients which adjsuted the signals
            mAmps (matrix):  matrix with adjusted amplitudes of tones
            mAmPh (matrix):  complex matrix with adjusted amplitudes/phases
        """
    
        # Get the number of signals and the size of signals (the number of samples)
        (nSigs, nSmp) = mSig.shape
    
        # Measure the power of the signals
        vP = (np.sum(mSig * mSig, axis=1) / nSmp).reshape(nSigs, 1)
    
        # Adjust the signal power, if needed
        if not np.isnan(iP) or np.isinf(iP):
    
            # Compute power adjustments coefficients for the noise signals
            vPCoef = np.sqrt(iP / vP)
    
            # Adjust the signal power
            mPCoef = np.tile(vPCoef, (1, nSmp))
            mSig = mSig * mPCoef
    
            # Adjust the reported amplitudes of tones
            (_, nAmps) = mAmps.shape
            mPCoef = np.tile(vPCoef, (1, nAmps))
            mAmps = mAmps * mPCoef
            mAmPh = mAmPh * mPCoef
    
            # Measure the power of the adjusted signals
            vP = np.sum(mSig*mSig, axis=1) / nSmp
    
        else:
            # Power adjustment coefficients are equal to 1 (no adjustment)
            vPCoef = np.ones((nSigs, 1))
    
        return (mSig, vP, vPCoef, mAmps, mAmPh)
    
    
    # =================================================================
    # Add the AWGN noise to the signals
    # =================================================================
    def _addNoise(self, mSig, vP, iSNR):
        """
        This function adds noise to the generated signals.
        If the requested level of noise is equal to NaN or inf,
        then no noise is added.
    
    
        Args:
            mSig (matrix):   matrix with signals (one row - one signal)
            vP (vector):     vector with powers of signals
            iSNR (float):    wanted level of noise in the signals
    
        Returns:
            mSigNN (matrix): matrix with non noisy signals
            vPNN (vector):   vector with powers of non noisy signals
            mSig (matrix):   matrix with noisy signals
            vP (vector):     vector with powers of noisy signals
        """
    
        # Backup the non noisy signals
        mSigNN = mSig.copy()      # Matrix with signals
        vPNN = vP.copy()          # Power of non noisy signals
    
        # Add the noise, if needed
        if not (np.isnan(iSNR) or np.isinf(iSNR)):
    
            # Get the number of signals and the size of signals
            # (the number of samples)
            (nSigs, nSmp) = mSig.shape
    
            # Generate the noise
            mNoise = np.random.randn(nSigs, nSmp)
    
            # Measure the current powers of the noise signals
            vNoisePReal = (np.sum(mNoise*mNoise, axis=1) / nSmp).reshape(nSigs, 1)
    
            # Compute the requested noise power for every signal
            vNoiseP = (vP / (10**(iSNR/10))).reshape(nSigs, 1)
    
            # Compute power adjustments coefficients for the noise signals
            vPNoiseCoef = np.sqrt(vNoiseP / vNoisePReal)
    
            # Adjust the noise power
            mPNoiseCoef = np.tile(vPNoiseCoef, (1, nSmp))
            mNoise = mPNoiseCoef * mNoise
    
            # Add the noise to the signals
            mSig = mSig + mNoise
    
            # Measure the power of the signals
            vP = np.sum(mSig * mSig, axis=1) / nSmp
    
        return (mSigNN, vPNN, mSig, vP)
  
    
"""
This a Random Multitone Signal Generator module. |br|

It is able to generate *N* multitone random signals according to settings
given by a user. |br|

*Examples*:
    Please go to the *examples/signals* directory for examples on how to use 
    the generator. |br|

*Settings*:
    Parameters of the generator described below.

    Take a look on '__parametersDefine' function for more info on the 
    parameters.

    Parameters of the generator are attributes of the class which must/can
    be set before the generator run.

    Required parameters:

    - a. **tS** (*float*): time of a signals

    - b. **fR** (*float*): signals' representation sampling frequency

    - c. **fRes** (*float*): tones frequency resolution


    Optional parameters:

    - d. **fMax** (*float*): maximum frequency present in signals [default = 0.4 * fR]

    - e. **fMin** (*float*): minimum allowed frequency present in the spectrum  [default = fRes]

    - f. **nSigs** (*int*): the number of signals to be generated  [default = 1]

    - g. **iSNR** (*float*): level of noise in signals [dB] (SNR)  [default = +int]

    - h. **iP** (*float*): requested power of signals  [default = do not regulate the power]


    - i. **vFrqs** (*Numpy array 1D*): vector with requested frequencies of tones  [default = empty]

    - j. **vAmps** (*Numpy array 1D*): vector with requested amplitudes of tones   [default = empty]

    - k. **vPhs** (*Numpy array 1D*): vector with requested phases of tones        [default = empty]

    
    - l. **nTones** (*int*): the number of additional tones    [default = 0]

    - m. **iMinAmp** (*float*): min amplitude of a tone present in a signal      [default = 0.1]

    - n. **iGraAmp** (*float*): gradation of a random amplitude of a tone        [default = 0.1]

    - o. **iMaxAmp** (*float*): max amplitude of a tone present in a signal      [default = 1.0]

    - p. **iMinPhs** (*float*): min allowed phase of a tone present in a signal  [default = -179 deg]

    - q. **iGraPhs** (*float*): gradation of a random phase of a tone            [default = 1 deg]

    - r. **iMaxPhs** (*float*): max allowed phase of a tone present in a signal  [default = +180 deg]


    - s. **bMute** (*int*): mute the console output from the generator [default = 0]


*Output*:
    Description of the generator output is below. 
    This is the list of attributes of the generator class which are available 
    after calling the 'run' method:

    - a. **mSig** (*Numpy array 2D*): Matrix with output signals

    - b. **mSigNN** (*Numpy array 2D*): Matrix with nonnoisy output signals

    - c. **nSmp** (*int*): The number of samples in the signals

    - d. **vTSig** (*Numpy array 1D*): The time vector for the generated signals (name alias vT is available)

    - e. **vP** (*Numpy array 1D*): Power of the signals

    - f. **vPNN** (*Numpy array 1D*): Power of the non noisy signals

    - g. **vPCoef** (*Numpy array 1D*): Power adjustment coefficients

    - h. **mFrqs** (*Numpy array 2D*): Frequencies of tones in the signals

    - i. **mAmps** (*Numpy array 2D*): Amplitudes of tones in the signals

    - j. **mPhs** (*Numpy array 2D*): Phases of tones in the signals

    - k. **mAmPh** (*Numpy array 2D*): Complex matrix with amplitudes/phases of tones

    - l. **fFFTR** (*float*): Signal FFT frequency resolution

*Author*:
    <NAME>, Aalborg University, Denmark. <<EMAIL>>

*Version*:
    0.1    | 15-MAY-2014 : * Initial version. |br|
    0.2    | 16-MAY-2014 : * Docstrings added. |br|
    0.3    | 19-MAY-2014 : * The main func. divided into smaller functions. |br|
    0.4    | 20-MAY-2014 : * Errors are served by 'raise'. |br|
    0.5    | 20-MAY-2014 : * Docstrings are added to the internal functions. |br|
    0.5r1  | 20-MAY-2014 : * Order of the internal functions was changed. |br|
    1.0    | 20-MAY-2014 : * Version 1.0 released. |br|
    1.0r1  | 21-MAY-2014 : * Error in progress printing is fixed. |br|
    1.0r2  | 21-MAY-2014 : * Error in default random amplitude is fixed. |br|
    1.0r3  | 21-MAY-2014 : * Error in progress printing (if muted) is fixed. |br|
    1.0r4  | 27-MAY-2014 : * Error in the number of given frqs is fixed. |br|
    1.0r5  | 27-MAY-2014 : * Error in the vector with signal time is fixed. |br|
    2.0    | 21-JUL-2015 : * Objectified version (2.0) |br|
    2.0r1  | 18-AUG-2015 : * Adjusted to RxCSobject v1.0 |br|
    2.1    | 02-SEP-2015 : * Max frequency in the spectrum is an optional parameter
                             printing improvements. |br|

*License*:
    BSD 2-Clause
"""

from __future__ import division
import numpy as np
import rxcs


class randMult(rxcs._RxCSobject):

    def __init__(self, *args):
        rxcs._RxCSobject.__init__(self)    # Make it a RxCS object 
        
        self.strRxCSgroup = 'Signal generator'  # Name of group of RxCS modules
        self.strModuleName = 'Random multitone'     # Module name        

        self.__parametersDefine()      # Define the parameters

    # Define parameters
    def __parametersDefine(self):

        # Time of the signal [s]
        self.paramAddMan('tS', 'Time of the signal', unit='s')
        self.paramType('tS', (int, float))
        self.paramH('tS', 0)
        self.paramL('tS', np.inf)       

        # The signal representation sampling frequency [Hz]
        self.paramAddMan('fR', 'The signal representation sampling frequency', unit='Hz')
        self.paramType('fR', (int, float))
        self.paramH('fR', 0)
        self.paramL('fR', np.inf)
 
        # The highest possible frequency in the signal [Hz]
        self.paramAddOpt('fMax', 'The highest possible frequency in the signal', unit='Hz', default='$$fR', mul=0.4)
        self.paramType('fMax', (int, float))
        self.paramH('fMax', 0)
        self.paramL('fMax', 'fR', mul=0.5)  # Nyquist principle

        # Signal spectrum resolution [Hz]
        self.paramAddMan('fRes', 'Signal spectrum resolution', unit='Hz')
        self.paramType('fRes', (int, float))
        self.paramH('fRes', 0)
        self.paramL('fRes', np.inf)


        # The minimum frequency of additional tones
        self.paramAddOpt('fMin', 'Minimum frequency of additional tones', default='$$fRes', unit='Hz')
        self.paramType('fMin', (int, float))
        self.paramH('fMin', 0)
        self.paramLE('fMin', 'fMax')

        # The number of signals to be generated
        self.paramAddOpt('nSigs', 'The number of signals to be generated', default=1)
        self.paramType('nSigs', (int))
        self.paramH('nSigs', 0)            # The number of signals must be higher than zero
        self.paramL('nSigs', np.inf)       # ...and lower than infinity

        # Signal noise [dB]
        self.paramAddOpt('iSNR', 'Signal to noise ratio', default=np.inf, unit='dB')
        self.paramType('iSNR', (int, float))

        # Power of signals [W]
        self.paramAddOpt('iP', 'Power of signals', default=np.nan, unit='W')
        self.paramType('iP', (int, float))
        self.paramH('iP', 0)
        self.paramL('iP', np.inf)

        # Vector with given frequencies of signal cosine tones
        self.paramAddOpt('vFrqs', 'Vector with given frequencies of signal cosine tones', default=np.zeros(0), noprint=1)
        self.paramType('vFrqs', np.ndarray)        
        strError = 'Frequencies in \'vFrqs\' vector must not be higher than the highest possible in the signal spectrum!'
        self.paramLE('vFrqs', 'fMax', errnote=strError)           
        strError = 'Frequencies in the \'vFrqs\' must be higher than 0!'
        self.paramH('vFrqs', 0, errnote=strError)   
        strError = 'Frequencies in \'vFrqs\' vector must not be lower than the lowest possible in the signal spectrum!'
        self.paramHE('vFrqs', 'fMin', errnote=strError)
        strError = 'Size of the vector with given frequencies \'vFrqs\' must be equal to size of the vectors \'vAmps\' and \'vPhs\''
        self.paramSizEq('vFrqs', 'vAmps', errnote=strError)
        self.paramSizEq('vFrqs', 'vPhs', errnote=strError)
        self.paramNDimEq('vFrqs', 1)
        strError = 'There are frequencies repeated in the \'vFrqs\' vector!'
        self.paramUnique('vFrqs', errnote=strError)
        self.NaNAllowedEl('vFrqs')


        # Vector with given amplitudes of signal cosine tones
        self.paramAddOpt('vAmps', 'Vector with given amplitudes of signal cosine tones', default=np.zeros(0), noprint=1)
        self.paramType('vAmps', np.ndarray)
        self.paramH('vAmps', 0, errnote='Amplituides of tones must be higher than 0')
        self.paramNDimEq('vAmps', 1)
        self.NaNAllowedEl('vAmps')

        # Vector with given phases of signal cosine tones content
        self.paramAddOpt('vPhs', 'Vector with given phases of signal cosine tones', default=np.zeros(0), noprint=1)
        self.paramType('vPhs', np.ndarray)
        self.paramH('vPhs', -180, errnote='Phases of tones must be higher than -180 [deg]')
        self.paramLE('vPhs', 180, errnote='Phases of tones must be lower or equal to 180 [deg]')
        self.paramNDimEq('vPhs', 1)
        self.NaNAllowedEl('vPhs')

        # The number of additional tones
        self.paramAddOpt('nTones', 'The number of additional tones', default=0)
        self.paramType('nTones', (int))
        self.paramHE('nTones', 0)

        # The boundaries for amplitudes: amplitude min value
        self.paramAddOpt('iMinAmp', 'Minimum value of random amplitudes', default=0.1, unitprefix=' ')
        self.paramType('iMinAmp', (int, float))
        self.paramH('iMinAmp', 0)
        self.paramLE('iMinAmp','iMaxAmp')
 
        # The boundaries for amplitudes: amplitude gradation
        self.paramAddOpt('iGraAmp', 'Gradation of value of random amplitudes', default=0.1, unitprefix=' ')
        self.paramType('iGraAmp', (int, float))
        self.paramH('iGraAmp', 0)
        
        # The boundaries for amplitudes: amplitude max value
        self.paramAddOpt('iMaxAmp', 'Maximum value of random amplitudes', default=1.0, unitprefix=' ')
        self.paramType('iMaxAmp', (int, float))
        self.paramH('iMaxAmp', 0)

        # The boundaries for amplitudes: phase min value
        self.paramAddOpt('iMinPhs', 'Minimum value of random phase', default=-179, unitprefix=' ')
        self.paramType('iMinPhs', (int, float))
        self.paramH('iMinPhs',-180)
        self.paramLE('iMinPhs',180)
        self.paramLE('iMinPhs','iMaxPhs')
        
        # The boundaries for amplitudes: phase gradation
        self.paramAddOpt('iGraPhs', 'Gradation of value of random phase', default=1, unitprefix=' ')
        self.paramType('iGraPhs', (int, float))
        self.paramH('iGraPhs', 0)

        # The boundaries for amplitudes: phase max value
        self.paramAddOpt('iMaxPhs', 'Maximum value of random phase', default=180, unitprefix=' ')
        self.paramType('iMaxPhs', (int, float))
        self.paramH('iMaxPhs',-180)
        self.paramLE('iMaxPhs',180)

        # 'Mute the output' flag
        self.paramAddOpt('bMute', 'Mute the output', noprint=1, default=0)
        self.paramType('bMute', int)           # Must be of int type
        self.paramAllowed('bMute',[0, 1])      # It can be either 1 or 0


    # Run
    def run(self):

        self.parametersCheck()         # Check if all the needed partameters are in place and are correct
        self.parametersPrint()         # Print the values of parameters
        
        self.engineStartsInfo()  # Info that the engine starts
        self.__engine()          # Run the engine
        self.engineStopsInfo()   # Info that the engine ends
        return self.__dict__     # Return dictionary with the parameters


    # Engine of the function
    def __engine(self):
        self._checkConf()

        # - - - - - - - - - - - - - - - - - - -        
        # Signal generation starts here:
        
        self.mFrqsInx = self._drawFreq(self.vFrqs, self.nTones, self.fMin, self.fMax, self.nSigs, self.fRes)         # Draw frequencies of the signals
        self.mAmps = self._drawAmps(self.vAmps, self.nTones, self.nSigs, self.iMinAmp, self.iGraAmp, self.iMaxAmp)   # Draw amplitudes of the signals
        self.mPhs = self._drawPhases(self.vPhs, self.nTones, self.nSigs, self.iMinPhs, self.iGraPhs, self.iMaxPhs)   # Draw phases of the signals
        
        # Generate the signals by IFFT
        (mSig, self.mAmPh, self.mFrqs, self.fFFTR) = \
            self._genSigs(self.mFrqsInx, self.mAmps, self.mPhs, self.nSigs, self.tS, self.fR, self.fRes)
    
        # - - - - - - - - - - - - - - - - - - -
    
        # Adjust the signal power
        (mSig, vP, self.vPCoef, self.mAmps, self.mAmPh) = self._adjPower(mSig, self.iP, self.mAmps, self.mAmPh)
    
        # Add the AWGN noise to the signals
        (self.mSigNN, self.vPNN, self.mSig, self.vP) = self._addNoise(mSig, vP, self.iSNR)
        
        # Generate the time vector for the signal
        self.vTSig = np.arange(self.nSmp) / self.fR 
        self.vT = self.vTSig   # Name alias for vTSig is vT

        return


    def _checkConf(self):
        """
        This function checks the configuration of the generator.    
        """
        #----------------------------------------------------------------------
        # If the minimum frequency was not given, it is equal to the frequency resolution
        if np.isnan(self.fMin):
            self.fMin = self.fRes
        
        #----------------------------------------------------------------------
        # Check the lowest possible frequency in the signal vs spectrum
        # resolution
        strErr = 'The lowest possible frequency in the signal is not a multiple '
        strErr = strErr + 'of the signal spectrum resolution'
        if (round(self.fMin/self.fRes) - self.fMin/self.fRes) > 1e-15:
            raise ValueError(strErr)
                  
        #----------------------------------------------------------------------
        # Check the highest possible frequency in the signal vs spectrum
        # resolution
        strErr = 'The highest possible frequency in the signal is not a multiple '
        strErr = strErr + 'of the signal spectrum resolution'
        if (round(self.fMax/self.fRes) - self.fMax/self.fRes) > 1e-15:
            raise ValueError(strErr)
    
        #----------------------------------------------------------------------
        # Check if there is a space for all the frequencies requested in
        # the signal, both given in the vFrqs vector and requested to be 
        # chosen randomly 
    
        # Compute the total number of tones in the max possible spectrum
        nSpectTones = int(self.fMax/self.fRes) - int(self.fMin/self.fRes) + 1 
                                                                               
        nFG = self.vFrqs.size          # The number of frequencies given in the vector with frequencies
        nSigTones = nFG + self.nTones  # The total number of tones which will be present in the signal

        strErr = 'The signal spectrum consists of %d tones. ' % (nSpectTones)
        strErr = strErr + 'I can not put there %d [vFrqs] + %d [nTones] tones' \
            % (nFG, self.nTones)
        if nSpectTones < nSigTones:
            raise ValueError(strErr)

        #----------------------------------------------------------------------
        # Check if there is a frequency leackage
        self.nSmp = int(round(self.tS*self.fR))  # Calculate the number of samples in the signals
        fFFTR = self.fR/self.nSmp    # Calculate the FFT frequency resolution
    
        if abs(round(self.fRes/fFFTR) - self.fRes/fFFTR) > 0:
            strErr = ('Frequency leackage! Signal spectrum resolution can not be ')
            strErr = strErr + ('represented with the current signal parameters!')
            raise ValueError(strErr)
    
        #----------------------------------------------------------------------
        # Check the vector with given frequencies
    
        # Check the vector with given frequencies, if it is longer then 0
        if nFG > 0:
    
            # Create the vector with given and specified frequencies (not np.nan)
            vFrqs_ = self.vFrqs[np.isnan(self.vFrqs) == 0]

            # 1. check resolution of given frequencies:
            if np.abs(np.sum(np.round(vFrqs_/self.fRes) - (vFrqs_/self.fRes))) > 1e-15:
                strErr = ('A frequency given in the vFrqs vector is ')
                strErr = strErr + ('incoherent with the resolution of signal ')
                strErr = strErr + ('spectrum!\n')
                raise ValueError(strErr)
    
            # Do correction of possible representation errors in the vector with frequencies
            self.vFrqs = np.round(self.vFrqs/self.fRes)*self.fRes
                        
        #----------------------------------------------------------------------
        return


    # =================================================================
    # Draw frequencies of the signals
    # =================================================================
    def _drawFreq(self, vFrqs, nTones, fMin, fMax, nSigs, fRes):
        """
        This function draws frequencies of tones for all the signals
        according to the rules specified by users,
    
        Args:
            vFrqs (vector):  vector with specified frequencies
            nTones (int):    the number of additional tones
            fMin (int):      the min allowed frequency in the signal spectrum
            fMax (int):      the max allowed frequency in the signal spectrum
            nSigs (int):     the number of signals to be generated
            fRes (int):      signal spectrum resolution
                             (distance between the tones in the spectrum)
    
        Returns:
            mFrqsInx (matrix):  matrix with frequencies of tones for all
                                the signals (one row - one signal)
                                The frequencies are represented as indices of
                                frequencies from the allowed signal spectrum.
    
        """
       
        #----------------------------------------------------------------------
        # Recalculate frequencies to indices of tones in the spectrum
    
        # Recalculate the vector with frequencies from frequency to index
        # of a frequency in the spectrum
        vFrqsInx = (vFrqs / fRes)
    
        # Create a vector with GIVEN (not nan) indices of frequencies in the
        # vFrqs vector
        vFrqsInx_ = (vFrqsInx[np.isnan(vFrqsInx) == 0]).astype(int)
    
        #----------------------------------------------------------------------
        # Create the vector with indices of avaialble frequencies in the spectrum
    
        # Create a vector with indices of all the available tones in the spectrum
        vSpecInx = np.arange(1, int(fMax/fRes) + 1)
    
        # Boolean vector which indicates if the frequency is free
        vFreqIsFree = np.ones(int(fMax/fRes)).astype(bool)
        
        # Mark all the frequencies below min frequency as unavailable 
        for inxF in np.arange(int(fMin/fRes)-1):
            vFreqIsFree[inxF] = 0
    
        # Mark the frequencies taken by vFreq vector as unavailable
        vFreqIsFree[vFrqsInx_ - 1] = 0
    
        # Create the vector with indices of available frequencies
        vAvailFreqsInx = vSpecInx[vFreqIsFree]
    
        #----------------------------------------------------------------------
        # Construct a vector with indices of frequencies for all the needed signals
    
        # Add unknown frequencies of the additional tones to the vFrqsInx vector
        vFrqsInx = np.concatenate((vFrqsInx, np.nan*np.zeros(nTones)))
    
        # Calculate the number of missing frequencies in the vector with
        # frequencies
        iMissF = len(vFrqsInx) - len(vFrqsInx_)
    
        # Construct a matrix with indices of frequencies for all the needed signals
        mFrqsInx = np.tile(vFrqsInx, (nSigs, 1))
    
        #----------------------------------------------------------------------
        # Draw the frequencies
        for inxSig in np.arange(nSigs):
    
            # Permute all the indices of frequencies in the spectrum
            vPermutedFreqsInx = ((np.random.permutation(vAvailFreqsInx)).T)
    
            # From the permuted indices of frequencices take as many
            # as it is missing
            vTakenFreqsInx = vPermutedFreqsInx[np.arange(iMissF)]
    
            # Put the taken indices of frequencies to the matrix with frequency
            # indices for all the signals
            mFrqsInx[inxSig, np.isnan(mFrqsInx[inxSig, :])] = vTakenFreqsInx
    
        return mFrqsInx


    # =================================================================
    # Draw amplitudes of the signals
    # =================================================================
    def _drawAmps(self, vAmps, nTones, nSigs, iMinAmp, iGraAmp, iMaxAmp):
        """
        This function draws amplitudes of tones for all the signals
        according to the rules specified by users,
    
        Args:
            vAmps (vector):  vector with specified amplitudes of tones in signals
            nTones (int):    the number of additional tones
            nSigs (int):     the number of signals to be generated
            iMinAmp (int):   min amplitude of a random tone present in a signal
            iGraAmp (int):   gradation of a amplitude of a random tone
            iMaxAmp (int):   max amplitude of a random tone present in a signal
    
        Returns:
            mAmps (matrix):  matrix with amplitudes of tones for all
                             the signals (one row - one signal)
    
        """
    
        # Add unknown amplitudes of the additional tones to the vAmps vector
        vAmps = np.concatenate((vAmps, np.nan*np.zeros(nTones)))
    
        # Compute the number of missing amplitudes for every signal
        iMissA = (vAmps[np.isnan(vAmps)]).size
    
        #----------------------------------------------------------------------
    
        # Compute the number of possible amplitude values
        nAmpVal = np.floor((iMaxAmp - iMinAmp) / iGraAmp) + 1
    
        #----------------------------------------------------------------------
    
        # Draw the missing amplitudes for all the signals
        vDrawAmps = \
            iMinAmp + iGraAmp*(np.random.randint(0, nAmpVal, (nSigs*iMissA)))
    
        # Construct a matrix with amplitudes of tones for all the needed signals
        mAmps = np.tile(vAmps, (nSigs, 1))
    
        # Put the draw amplitudes to the matrix with amplitudes of tones for
        # all the needed signals
        mAmps[np.isnan(mAmps)] = vDrawAmps
    
        return mAmps
    
    
    # =================================================================
    # Draw phases of the signals
    # =================================================================
    def _drawPhases(self, vPhs, nTones, nSigs, iMinPhs, iGraPhs, iMaxPhs):
        """
        This function draws phases of tones for all the signals
        according to the rules specified by users,
    
        Args:
            vPhs (vector):   vector with specified phases of tones in signals
            nTones (int):    the number of additional tones
            nSigs (int):     the number of signals to be generated
            iMinPhs (int):   min phase of a random tone present in a signal
            iGraPhs (int):   gradation of a phase of a random tone
            iMaxPhs (int):   max phase of a random tone present in a signal
    
        Returns:
            mPhs (matrix):   matrix with phases of tones for all
                             the signals (one row - one signal)
    
        """
    
        # Add unknown phases of the additional tones to the vAmps vector
        vPhs = np.concatenate((vPhs, np.nan*np.zeros(nTones)))
    
        # Compute the number of missing phases for every signal
        iMissP = (vPhs[np.isnan(vPhs)]).size
    
        #----------------------------------------------------------------------
    
        # Compute the number of possible phase values
        nPhsVal = np.floor((iMaxPhs - iMinPhs)/iGraPhs) + 1
    
        #----------------------------------------------------------------------
    
        # Draw the missing phases for all the signals
        vDrawPhs = \
            iMinPhs + iGraPhs*(np.random.randint(0, nPhsVal, (nSigs*iMissP)))
    
        # Construct a matrix with phases of tones for all the needed signals
        mPhs = np.tile(vPhs, (nSigs, 1))
    
        # Put the draw phases to the matrix with phases of tones for
        # all the needed signals
        mPhs[np.isnan(mPhs)] = vDrawPhs
    
        return mPhs
    
    
    # =================================================================
    # Generate the signals by IFFT
    # =================================================================
    def _genSigs(self, mFrqsInx, mAmps, mPhs, nSigs, tS, fR, fRes):
        """
        This function generate the multitone signals using the IFFT algorithm.
    
        Args:
            mFrqsInx (matrix):  matrix with freqs of tones for all the signals
                                (as indices of tones in the allowed spectrum)
            mAmps (matrix):     matrix with amplitudes of tones for all the signals
            mPhs (matrix):      matrix with phases of tones for all the signals
            nSigs (float):      the number of signals
            tS (float):         time of the signals
            fR (float):         signal representation sampling frequency
            fRes (float):       signal spectrum resolution
                                (distance between the tones in the spectrum)
    
        Returns:
            mSig (matrix):   matrix with signals (one row - one signal)
            mAmPh (float):   complex matrix with amplitudes/phases of tones
            mFrqs (matrix):  matrix with freqs of tones for all the signals
            fFFTR (float):   signal FFT frequency resolution
        """
    
        # Calculate the number of samples in the signals
        nSmp = int(round(tS*fR))
    
        # Calculate the FFT frequency resolution
        fFFTR = fR/nSmp
    
        #----------------------------------------------------------------------
    
        # Adjust the amplitudes value to the number of points
        mAmpsAdj = mAmps * nSmp/2
    
        # Change phases into radians
        mPhsRad = mPhs*np.pi/180
    
        # Generate a one complex matrix for all the signals and its conjugated copy
        mAmPh = mAmpsAdj*np.cos(mPhsRad) + 1j*mAmpsAdj*np.sin(mPhsRad)
        mAmPh_conj = np.conjugate(mAmPh)
    
        #----------------------------------------------------------------------
        # Put the complex matrix with amplitudes and phases of tones into
        # one matrix dedicated for IFFT
    
        # Recalculate the matrix with indices of frequencies in the spectrum
        # to real frequencies
        mFrqs = mFrqsInx*fRes
    
        # Recalculate the matrix with indices of frequencies in the spectrum
        # to indices of frequencies in the IFFT transform
        mIFFTFrqsInx = np.around(mFrqs/fFFTR).astype(int)
    
        # Allocate the vector for the ifft coefficients for all the signals
        # (one signal in one row)
        mIFFT = np.zeros((nSigs, nSmp)) + 1j*np.zeros((nSigs, nSmp))
    
        # Put the complex vector with tones values into the IFFT matrix
        for inxSig in np.arange(nSigs):
    
            # IFFT indices of tones for the current signal
            vInx = mIFFTFrqsInx[inxSig, :]
    
            # Put the tones for the current signal
            mIFFT[inxSig, vInx] = mAmPh[inxSig, :]
    
            # IFFT indices of conjugate tones for the current signal
            vInxConj = (nSmp - mIFFTFrqsInx[inxSig, :]).astype(int)
    
            # Put the conjugate tones for the current signal
            mIFFT[inxSig, vInxConj] = mAmPh_conj[inxSig, :]
    
        #----------------------------------------------------------------------
        # Generate the signals (perform the IFFT)
        mSig = np.fft.ifftn(mIFFT, axes=[1]).real
    
        return (mSig, mAmPh, mFrqs, fFFTR)
    
    
    # =================================================================
    # Adjust the signal power
    # =================================================================
    def _adjPower(self, mSig, iP, mAmps, mAmPh):
        """
        This function adjustes powers of the generated signals.
        If the requested power of the signals is equal to NaN or inf, then
        the signals are not adjusted.
    
        Args:
            mSig (matrix):   matrix with signals (one row - one signal)
            iP (float):      requested power of the signals
            mAmps (matrix):  matrix with amplitudes of tones in the signals
            mAmPh (matrix):  complex matrix with amplitudes/phases of tones
    
        Returns:
            mSig (matrix):   matrix with noisy signals
            vP (vector):     vector with powers of noisy signals
            vPCoef (vector): vector with coefficients which adjsuted the signals
            mAmps (matrix):  matrix with adjusted amplitudes of tones
            mAmPh (matrix):  complex matrix with adjusted amplitudes/phases
        """
    
        # Get the number of signals and the size of signals (the number of samples)
        (nSigs, nSmp) = mSig.shape
    
        # Measure the power of the signals
        vP = (np.sum(mSig * mSig, axis=1) / nSmp).reshape(nSigs, 1)
    
        # Adjust the signal power, if needed
        if not np.isnan(iP) or np.isinf(iP):
    
            # Compute power adjustments coefficients for the noise signals
            vPCoef = np.sqrt(iP / vP)
    
            # Adjust the signal power
            mPCoef = np.tile(vPCoef, (1, nSmp))
            mSig = mSig * mPCoef
    
            # Adjust the reported amplitudes of tones
            (_, nAmps) = mAmps.shape
            mPCoef = np.tile(vPCoef, (1, nAmps))
            mAmps = mAmps * mPCoef
            mAmPh = mAmPh * mPCoef
    
            # Measure the power of the adjusted signals
            vP = np.sum(mSig*mSig, axis=1) / nSmp
    
        else:
            # Power adjustment coefficients are equal to 1 (no adjustment)
            vPCoef = np.ones((nSigs, 1))
    
        return (mSig, vP, vPCoef, mAmps, mAmPh)
    
    
    # =================================================================
    # Add the AWGN noise to the signals
    # =================================================================
    def _addNoise(self, mSig, vP, iSNR):
        """
        This function adds noise to the generated signals.
        If the requested level of noise is equal to NaN or inf,
        then no noise is added.
    
    
        Args:
            mSig (matrix):   matrix with signals (one row - one signal)
            vP (vector):     vector with powers of signals
            iSNR (float):    wanted level of noise in the signals
    
        Returns:
            mSigNN (matrix): matrix with non noisy signals
            vPNN (vector):   vector with powers of non noisy signals
            mSig (matrix):   matrix with noisy signals
            vP (vector):     vector with powers of noisy signals
        """
    
        # Backup the non noisy signals
        mSigNN = mSig.copy()      # Matrix with signals
        vPNN = vP.copy()          # Power of non noisy signals
    
        # Add the noise, if needed
        if not (np.isnan(iSNR) or np.isinf(iSNR)):
    
            # Get the number of signals and the size of signals
            # (the number of samples)
            (nSigs, nSmp) = mSig.shape
    
            # Generate the noise
            mNoise = np.random.randn(nSigs, nSmp)
    
            # Measure the current powers of the noise signals
            vNoisePReal = (np.sum(mNoise*mNoise, axis=1) / nSmp).reshape(nSigs, 1)
    
            # Compute the requested noise power for every signal
            vNoiseP = (vP / (10**(iSNR/10))).reshape(nSigs, 1)
    
            # Compute power adjustments coefficients for the noise signals
            vPNoiseCoef = np.sqrt(vNoiseP / vNoisePReal)
    
            # Adjust the noise power
            mPNoiseCoef = np.tile(vPNoiseCoef, (1, nSmp))
            mNoise = mPNoiseCoef * mNoise
    
            # Add the noise to the signals
            mSig = mSig + mNoise
    
            # Measure the power of the signals
            vP = np.sum(mSig * mSig, axis=1) / nSmp
    
        return (mSigNN, vPNN, mSig, vP)
  
    
This a Random Multitone Signal Generator module. |br|  It is able to generate *N* multitone random signals according to settings given by a user. |br|  *Examples*:     Please go to the *examples/signals* directory for examples on how to use      the generator. |br|  *Settings*:     Parameters of the generator described below.      Take a look on '__parametersDefine' function for more info on the      parameters.      Parameters of the generator are attributes of the class which must/can     be set before the generator run.      Required parameters:      - a. **tS** (*float*): time of a signals      - b. **fR** (*float*): signals' representation sampling frequency      - c. **fRes** (*float*): tones frequency resolution       Optional parameters:      - d. **fMax** (*float*): maximum frequency present in signals [default = 0.4 * fR]      - e. **fMin** (*float*): minimum allowed frequency present in the spectrum  [default = fRes]      - f. **nSigs** (*int*): the number of signals to be generated  [default = 1]      - g. **iSNR** (*float*): level of noise in signals [dB] (SNR)  [default = +int]      - h. **iP** (*float*): requested power of signals  [default = do not regulate the power]       - i. **vFrqs** (*Numpy array 1D*): vector with requested frequencies of tones  [default = empty]      - j. **vAmps** (*Numpy array 1D*): vector with requested amplitudes of tones   [default = empty]      - k. **vPhs** (*Numpy array 1D*): vector with requested phases of tones        [default = empty]           - l. **nTones** (*int*): the number of additional tones    [default = 0]      - m. **iMinAmp** (*float*): min amplitude of a tone present in a signal      [default = 0.1]      - n. **iGraAmp** (*float*): gradation of a random amplitude of a tone        [default = 0.1]      - o. **iMaxAmp** (*float*): max amplitude of a tone present in a signal      [default = 1.0]      - p. **iMinPhs** (*float*): min allowed phase of a tone present in a signal  [default = -179 deg]      - q. **iGraPhs** (*float*): gradation of a random phase of a tone            [default = 1 deg]      - r. **iMaxPhs** (*float*): max allowed phase of a tone present in a signal  [default = +180 deg]       - s. **bMute** (*int*): mute the console output from the generator [default = 0]   *Output*:     Description of the generator output is below.      This is the list of attributes of the generator class which are available      after calling the 'run' method:      - a. **mSig** (*Numpy array 2D*): Matrix with output signals      - b. **mSigNN** (*Numpy array 2D*): Matrix with nonnoisy output signals      - c. **nSmp** (*int*): The number of samples in the signals      - d. **vTSig** (*Numpy array 1D*): The time vector for the generated signals (name alias vT is available)      - e. **vP** (*Numpy array 1D*): Power of the signals      - f. **vPNN** (*Numpy array 1D*): Power of the non noisy signals      - g. **vPCoef** (*Numpy array 1D*): Power adjustment coefficients      - h. **mFrqs** (*Numpy array 2D*): Frequencies of tones in the signals      - i. **mAmps** (*Numpy array 2D*): Amplitudes of tones in the signals      - j. **mPhs** (*Numpy array 2D*): Phases of tones in the signals      - k. **mAmPh** (*Numpy array 2D*): Complex matrix with amplitudes/phases of tones      - l. **fFFTR** (*float*): Signal FFT frequency resolution  *Author*:     <NAME>, Aalborg University, Denmark. <<EMAIL>>  *Version*:     0.1    | 15-MAY-2014 : * Initial version. |br|     0.2    | 16-MAY-2014 : * Docstrings added. |br|     0.3    | 19-MAY-2014 : * The main func. divided into smaller functions. |br|     0.4    | 20-MAY-2014 : * Errors are served by 'raise'. |br|     0.5    | 20-MAY-2014 : * Docstrings are added to the internal functions. |br|     0.5r1  | 20-MAY-2014 : * Order of the internal functions was changed. |br|     1.0    | 20-MAY-2014 : * Version 1.0 released. |br|     1.0r1  | 21-MAY-2014 : * Error in progress printing is fixed. |br|     1.0r2  | 21-MAY-2014 : * Error in default random amplitude is fixed. |br|     1.0r3  | 21-MAY-2014 : * Error in progress printing (if muted) is fixed. |br|     1.0r4  | 27-MAY-2014 : * Error in the number of given frqs is fixed. |br|     1.0r5  | 27-MAY-2014 : * Error in the vector with signal time is fixed. |br|     2.0    | 21-JUL-2015 : * Objectified version (2.0) |br|     2.0r1  | 18-AUG-2015 : * Adjusted to RxCSobject v1.0 |br|     2.1    | 02-SEP-2015 : * Max frequency in the spectrum is an optional parameter                              printing improvements. |br|  *License*:     BSD 2-Clause # Make it a RxCS object # Name of group of RxCS modules # Module name # Define the parameters # Define parameters # Time of the signal [s] # The signal representation sampling frequency [Hz] # The highest possible frequency in the signal [Hz] # Nyquist principle # Signal spectrum resolution [Hz] # The minimum frequency of additional tones # The number of signals to be generated # The number of signals must be higher than zero # ...and lower than infinity # Signal noise [dB] # Power of signals [W] # Vector with given frequencies of signal cosine tones # Vector with given amplitudes of signal cosine tones # Vector with given phases of signal cosine tones content # The number of additional tones # The boundaries for amplitudes: amplitude min value # The boundaries for amplitudes: amplitude gradation # The boundaries for amplitudes: amplitude max value # The boundaries for amplitudes: phase min value # The boundaries for amplitudes: phase gradation # The boundaries for amplitudes: phase max value # 'Mute the output' flag # Must be of int type # It can be either 1 or 0 # Run # Check if all the needed partameters are in place and are correct # Print the values of parameters # Info that the engine starts # Run the engine # Info that the engine ends # Return dictionary with the parameters # Engine of the function # - - - - - - - - - - - - - - - - - - - # Signal generation starts here: # Draw frequencies of the signals # Draw amplitudes of the signals # Draw phases of the signals # Generate the signals by IFFT # - - - - - - - - - - - - - - - - - - - # Adjust the signal power # Add the AWGN noise to the signals # Generate the time vector for the signal # Name alias for vTSig is vT This function checks the configuration of the generator. #---------------------------------------------------------------------- # If the minimum frequency was not given, it is equal to the frequency resolution #---------------------------------------------------------------------- # Check the lowest possible frequency in the signal vs spectrum # resolution #---------------------------------------------------------------------- # Check the highest possible frequency in the signal vs spectrum # resolution #---------------------------------------------------------------------- # Check if there is a space for all the frequencies requested in # the signal, both given in the vFrqs vector and requested to be # chosen randomly # Compute the total number of tones in the max possible spectrum # The number of frequencies given in the vector with frequencies # The total number of tones which will be present in the signal #---------------------------------------------------------------------- # Check if there is a frequency leackage # Calculate the number of samples in the signals # Calculate the FFT frequency resolution #---------------------------------------------------------------------- # Check the vector with given frequencies # Check the vector with given frequencies, if it is longer then 0 # Create the vector with given and specified frequencies (not np.nan) # 1. check resolution of given frequencies: # Do correction of possible representation errors in the vector with frequencies #---------------------------------------------------------------------- # ================================================================= # Draw frequencies of the signals # ================================================================= This function draws frequencies of tones for all the signals         according to the rules specified by users,              Args:             vFrqs (vector):  vector with specified frequencies             nTones (int):    the number of additional tones             fMin (int):      the min allowed frequency in the signal spectrum             fMax (int):      the max allowed frequency in the signal spectrum             nSigs (int):     the number of signals to be generated             fRes (int):      signal spectrum resolution                              (distance between the tones in the spectrum)              Returns:             mFrqsInx (matrix):  matrix with frequencies of tones for all                                 the signals (one row - one signal)                                 The frequencies are represented as indices of                                 frequencies from the allowed signal spectrum. #---------------------------------------------------------------------- # Recalculate frequencies to indices of tones in the spectrum # Recalculate the vector with frequencies from frequency to index # of a frequency in the spectrum # Create a vector with GIVEN (not nan) indices of frequencies in the # vFrqs vector #---------------------------------------------------------------------- # Create the vector with indices of avaialble frequencies in the spectrum # Create a vector with indices of all the available tones in the spectrum # Boolean vector which indicates if the frequency is free # Mark all the frequencies below min frequency as unavailable # Mark the frequencies taken by vFreq vector as unavailable # Create the vector with indices of available frequencies #---------------------------------------------------------------------- # Construct a vector with indices of frequencies for all the needed signals # Add unknown frequencies of the additional tones to the vFrqsInx vector # Calculate the number of missing frequencies in the vector with # frequencies # Construct a matrix with indices of frequencies for all the needed signals #---------------------------------------------------------------------- # Draw the frequencies # Permute all the indices of frequencies in the spectrum # From the permuted indices of frequencices take as many # as it is missing # Put the taken indices of frequencies to the matrix with frequency # indices for all the signals # ================================================================= # Draw amplitudes of the signals # ================================================================= This function draws amplitudes of tones for all the signals         according to the rules specified by users,              Args:             vAmps (vector):  vector with specified amplitudes of tones in signals             nTones (int):    the number of additional tones             nSigs (int):     the number of signals to be generated             iMinAmp (int):   min amplitude of a random tone present in a signal             iGraAmp (int):   gradation of a amplitude of a random tone             iMaxAmp (int):   max amplitude of a random tone present in a signal              Returns:             mAmps (matrix):  matrix with amplitudes of tones for all                              the signals (one row - one signal) # Add unknown amplitudes of the additional tones to the vAmps vector # Compute the number of missing amplitudes for every signal #---------------------------------------------------------------------- # Compute the number of possible amplitude values #---------------------------------------------------------------------- # Draw the missing amplitudes for all the signals # Construct a matrix with amplitudes of tones for all the needed signals # Put the draw amplitudes to the matrix with amplitudes of tones for # all the needed signals # ================================================================= # Draw phases of the signals # ================================================================= This function draws phases of tones for all the signals         according to the rules specified by users,              Args:             vPhs (vector):   vector with specified phases of tones in signals             nTones (int):    the number of additional tones             nSigs (int):     the number of signals to be generated             iMinPhs (int):   min phase of a random tone present in a signal             iGraPhs (int):   gradation of a phase of a random tone             iMaxPhs (int):   max phase of a random tone present in a signal              Returns:             mPhs (matrix):   matrix with phases of tones for all                              the signals (one row - one signal) # Add unknown phases of the additional tones to the vAmps vector # Compute the number of missing phases for every signal #---------------------------------------------------------------------- # Compute the number of possible phase values #---------------------------------------------------------------------- # Draw the missing phases for all the signals # Construct a matrix with phases of tones for all the needed signals # Put the draw phases to the matrix with phases of tones for # all the needed signals # ================================================================= # Generate the signals by IFFT # ================================================================= This function generate the multitone signals using the IFFT algorithm.              Args:             mFrqsInx (matrix):  matrix with freqs of tones for all the signals                                 (as indices of tones in the allowed spectrum)             mAmps (matrix):     matrix with amplitudes of tones for all the signals             mPhs (matrix):      matrix with phases of tones for all the signals             nSigs (float):      the number of signals             tS (float):         time of the signals             fR (float):         signal representation sampling frequency             fRes (float):       signal spectrum resolution                                 (distance between the tones in the spectrum)              Returns:             mSig (matrix):   matrix with signals (one row - one signal)             mAmPh (float):   complex matrix with amplitudes/phases of tones             mFrqs (matrix):  matrix with freqs of tones for all the signals             fFFTR (float):   signal FFT frequency resolution # Calculate the number of samples in the signals # Calculate the FFT frequency resolution #---------------------------------------------------------------------- # Adjust the amplitudes value to the number of points # Change phases into radians # Generate a one complex matrix for all the signals and its conjugated copy #---------------------------------------------------------------------- # Put the complex matrix with amplitudes and phases of tones into # one matrix dedicated for IFFT # Recalculate the matrix with indices of frequencies in the spectrum # to real frequencies # Recalculate the matrix with indices of frequencies in the spectrum # to indices of frequencies in the IFFT transform # Allocate the vector for the ifft coefficients for all the signals # (one signal in one row) # Put the complex vector with tones values into the IFFT matrix # IFFT indices of tones for the current signal # Put the tones for the current signal # IFFT indices of conjugate tones for the current signal # Put the conjugate tones for the current signal #---------------------------------------------------------------------- # Generate the signals (perform the IFFT) # ================================================================= # Adjust the signal power # ================================================================= This function adjustes powers of the generated signals.         If the requested power of the signals is equal to NaN or inf, then         the signals are not adjusted.              Args:             mSig (matrix):   matrix with signals (one row - one signal)             iP (float):      requested power of the signals             mAmps (matrix):  matrix with amplitudes of tones in the signals             mAmPh (matrix):  complex matrix with amplitudes/phases of tones              Returns:             mSig (matrix):   matrix with noisy signals             vP (vector):     vector with powers of noisy signals             vPCoef (vector): vector with coefficients which adjsuted the signals             mAmps (matrix):  matrix with adjusted amplitudes of tones             mAmPh (matrix):  complex matrix with adjusted amplitudes/phases # Get the number of signals and the size of signals (the number of samples) # Measure the power of the signals # Adjust the signal power, if needed # Compute power adjustments coefficients for the noise signals # Adjust the signal power # Adjust the reported amplitudes of tones # Measure the power of the adjusted signals # Power adjustment coefficients are equal to 1 (no adjustment) # ================================================================= # Add the AWGN noise to the signals # ================================================================= This function adds noise to the generated signals.         If the requested level of noise is equal to NaN or inf,         then no noise is added.                   Args:             mSig (matrix):   matrix with signals (one row - one signal)             vP (vector):     vector with powers of signals             iSNR (float):    wanted level of noise in the signals              Returns:             mSigNN (matrix): matrix with non noisy signals             vPNN (vector):   vector with powers of non noisy signals             mSig (matrix):   matrix with noisy signals             vP (vector):     vector with powers of noisy signals # Backup the non noisy signals # Matrix with signals # Power of non noisy signals # Add the noise, if needed # Get the number of signals and the size of signals # (the number of samples) # Generate the noise # Measure the current powers of the noise signals # Compute the requested noise power for every signal # Compute power adjustments coefficients for the noise signals # Adjust the noise power # Add the noise to the signals # Measure the power of the signals