def mlsExciationSignal(self):
        """ function to create graphs to illustrate generating mls signals """
        self.logger.debug("entering mlsExciationSignal")

        # get the mls signal
        mls_db = MlsDb()

        mls = mls_db.getMls(5)

        # convert into -1's and 1s
        mls = -2 * mls + 1

        # hold values to produce plots
        mls_plot = reduce(lambda x, y: x + y, zip(mls, mls))
        bins_plot = reduce(lambda x, y: x + y, zip(arange(len(mls)), arange(1, len(mls) + 1)))

        plot(bins_plot, mls_plot)
        ylim(-1.1, 1.1)
        yticks([-1, 0, 1])
        xlabel("Bins")
        ylabel("Amplitude")
        axhline(y=-1, ls="--", color="gray")
        axhline(y=0, ls="--", color="gray")
        axhline(y=1, ls="--", color="gray")
        subplots_adjust(left=0.10, right=0.97, top=0.97, bottom=0.10)
        xlim(0, 31)
        
        savefig("Analysis/Images/5_tap_mls_signal.eps")
    def mlsExciationSignal(self):
        """ function to create graphs to illustrate generating mls signals """
        self.logger.debug("entering mlsExciationSignal")

        # get the mls signal
        mls_db = MlsDb()

        mls = mls_db.getMls(5)

        # convert into -1's and 1s
        mls = -2 * mls + 1

        # hold values to produce plots
        mls_plot = reduce(lambda x, y: x + y, zip(mls, mls))
        bins_plot = reduce(lambda x, y: x + y,
                           zip(arange(len(mls)), arange(1,
                                                        len(mls) + 1)))

        plot(bins_plot, mls_plot)
        ylim(-1.1, 1.1)
        yticks([-1, 0, 1])
        xlabel("Bins")
        ylabel("Amplitude")
        axhline(y=-1, ls="--", color="gray")
        axhline(y=0, ls="--", color="gray")
        axhline(y=1, ls="--", color="gray")
        subplots_adjust(left=0.10, right=0.97, top=0.97, bottom=0.10)
        xlim(0, 31)

        savefig("Analysis/Images/5_tap_mls_signal.eps")
Example #3
0
    def __init__(self, microphone_signals, generator_signals,
                 measurement_settings, analysis_settings):
        """ Constructor for AbsorptionCoefficient object.

        :param microphone_signals:
            An array of signals recorded from the microphone.
        :type microphone_signals:
            array of float
        :param generator_signals:
            An array of signals recorded directly from the generator.
        :type generator_signals:
            array of float
        :param measurement_settings:
            A dictionary containing the settings used to measure the signals.
            As well as the location of the impulse in the microphone signal and
            the generator signal.
        :type measurement_settings:
            dict
        :param analysis_settings:
            A dictionary containing the settings used to analyze the signals.
        :type analysis_settings:
            dict
        """
        self.logger = logging.getLogger("Alpha")
        self.logger.debug("Creating AbsorptionCoefficient Object")

        self.microphone_signals = microphone_signals
        self.generator_signals = generator_signals
        self.measurement_settings = measurement_settings
        self.analysis_settings = analysis_settings

        self.mls_db = MlsDb()

        self.determineAlpha()
    def __init__(self, microphone_signals, generator_signals,
        measurement_settings):
        """ Default constructor to create the FrequencyResponse object.

            :param microphone_signals:
                An array of signals recorded from the microphone.
            :type microphone_signals:
                array of float
            :param generator_signals:
                An array of signals recorded directly from the generator.
            :type generator_signals:
                array of float
            :param measurement_settings:
                A dictionary containing the settings used to measure the
                signals. As well as the location of the impulse in the
                microphone signal and the generator signal.
            :type measurement_settings:
                dict
        """
        self.logger = logging.getLogger("Alpha")
        self.logger.debug("Creating Frequency Response Object")

        # Set up object variables
        self.microphone_signals = microphone_signals
        self.generator_signals = generator_signals
        self.measurement_settings = measurement_settings

        # Create new object to access MLS database
        self.mls_db = MlsDb()

        self.determineFreqResp()
    def __init__(self, microphone_signals, generator_signals,
        measurement_settings, analysis_settings):

        """ Constructor for AbsorptionCoefficient object.

        :param microphone_signals:
            An array of signals recorded from the microphone.
        :type microphone_signals:
            array of float
        :param generator_signals:
            An array of signals recorded directly from the generator.
        :type generator_signals:
            array of float
        :param measurement_settings:
            A dictionary containing the settings used to measure the signals.
            As well as the location of the impulse in the microphone signal and
            the generator signal.
        :type measurement_settings:
            dict
        :param analysis_settings:
            A dictionary containing the settings used to analyze the signals.
        :type analysis_settings:
            dict
        """
        self.logger = logging.getLogger("Alpha")
        self.logger.debug("Creating AbsorptionCoefficient Object")

        self.microphone_signals = microphone_signals
        self.generator_signals = generator_signals
        self.measurement_settings = measurement_settings
        self.analysis_settings = analysis_settings

        self.mls_db = MlsDb()

        self.determineAlpha()
    def __init__(self, parameters):
        """Constructor to create signal generator

        :param parameters:
            The parameters to use to generate the signal.  It is a dictionary
            containing information, such as sample rate, signal type and other
            depending on the signal.
        :type parameters:
            dict
        """
        self.logger = logging.getLogger("Alpha")

        self.parameters = parameters

        self.mls_db = MlsDb()

        self.generateSignal()
class AbsorptionCoefficient(object):

    def __init__(self, microphone_signals, generator_signals,
        measurement_settings, analysis_settings):

        """ Constructor for AbsorptionCoefficient object.

        :param microphone_signals:
            An array of signals recorded from the microphone.
        :type microphone_signals:
            array of float
        :param generator_signals:
            An array of signals recorded directly from the generator.
        :type generator_signals:
            array of float
        :param measurement_settings:
            A dictionary containing the settings used to measure the signals.
            As well as the location of the impulse in the microphone signal and
            the generator signal.
        :type measurement_settings:
            dict
        :param analysis_settings:
            A dictionary containing the settings used to analyze the signals.
        :type analysis_settings:
            dict
        """
        self.logger = logging.getLogger("Alpha")
        self.logger.debug("Creating AbsorptionCoefficient Object")

        self.microphone_signals = microphone_signals
        self.generator_signals = generator_signals
        self.measurement_settings = measurement_settings
        self.analysis_settings = analysis_settings

        self.mls_db = MlsDb()

        self.determineAlpha()

    def determineAlpha(self):
        """ Determine the absorption coefficient of the material under test.

        The absorption coefficient is determined directly by lifting the
        impulse response from the power cepstrum.  The power cepstrum is the
        generator's power cepstrum subtracted from microphone's power cepstrum.
        The microphone and generator power cepstra are determined from the
        averaged recorded microphone and generator response.  These averaged
        signals are filtered with an anti-aliasing filter, and decimated.
        """
        self.logger.debug("Entering determineAlpha")

        # Extract the signals from the recorded responses, and average
        self._extractSignals()

        # Determine the system response of the signals
        self._determineResponse()

        # LPF the signals and decimate
        self._downsampleSignals()

        # Determine the Cepstrum
        self._determineCepstrum()

        # Lift the impulse response
        self._liftImpulseResponse()

        # Determine the absorption coefficient
        self._determineAbsorptionCoefficient()

    def _determineAbsorptionCoefficient(self):
        """ By taking the Fourier transform of the impulse response, the absorption coefficient can be determined.

        The absorption coefficient is simply,
         a = 1 - |F{h}|^2
         with:
            a - the absorption coefficient
            h - the impulse response
            F{} - The Fourier transform operator
        """
        self.logger.debug("Entering _determineAbsorptionCoefficient")

        fft_size = int(self.measurement_settings["fft size"])
        self.alpha = 1 - abs(fft(self.impulse_response, fft_size)) ** 2

    def _determineResponse(self):
        """ For Psuedorandom signal, determine the system response using the autocorrellation property of the signal.

        """
        self.logger.debug("Entering _determineResponse")

        # If MLS signal, then utilize the circular convolution property
        signal_type = self.measurement_settings["signal type"]
        sample_rate = float(self.measurement_settings["sample rate"])

        if signal_type.lower() == "maximum length sequence":
            number_taps = int(self.measurement_settings["mls taps"])

            self.microphone_response = self.mls_db.getSystemResponse(self.average_microphone_response, number_taps)

            self.generator_response = self.mls_db.getSystemResponse(self.average_generator_response, number_taps)

            self.system_response = ifft(fft(self.microphone_response) / fft(self.generator_response))

        elif signal_type.lower() == "inverse repeat sequence":
            number_taps = int(self.measurement_settings["mls taps"])
            # TODO: Re-factor this out of absorption coefficient
            mls = self.mls_db.getMls(number_taps)
            mls = -2 * mls + 1
            irs = array([])
            for index, sample in enumerate(r_[mls, mls]):
                if index % 2 == 0:
                    irs = r_[irs, sample]
                else:
                    irs = r_[irs, -1 * sample]


            self.microphone_response = irfft(rfft(self.average_microphone_response) * rfft(irs[-1::-1]))
            self.generator_response = irfft(rfft(self.average_generator_response) * rfft(irs[-1::-1]))

            # The output of the auto-correlation of an irs signal is a + impulse at 0 and a
            # - impulse at N / 2.  One is only interested in the positive impulse, so extract
            # up to N / 2 samples, or the MLS length of 2 ^ (number of taps) - 1.  BUT, since
            # the DFT is circular, samples at the end wrap to be connected to sample's at the
            # start - if the start of the IRS signal is missed, then part of the - impulse
            # response will corrupt the positive impulse response.

            impulse_length = len(mls)
            window = hanning(0.1 * impulse_length)

            self.microphone_response = self.microphone_response[:0.9 * impulse_length]
            self.microphone_response[-len(window) / 2 :] *= window[-len(window) / 2 :]
            tmp_response = self.microphone_response[-0.1 * impulse_length:]
            tmp_response[:len(window) / 2] *= window[:len(window) / 2]
            append(self.microphone_response, tmp_response)

            self.generator_response = self.generator_response[:0.9 * impulse_length]
            self.generator_response[-len(window) / 2 :] *= window[-len(window) / 2 :]
            tmp_response = self.generator_response[-0.1 * impulse_length:]
            tmp_response[:len(window) / 2] *= window[:len(window) / 2]
            append(self.generator_response, tmp_response)

        else:
            self.microphone_response = self.average_microphone_response
            self.generator_response = self.average_generator_response
    def _extractSignals(self):
        """ Extract the microphone and generator signals from the raw signals.

        The microphone and generator signal are preceded by an impulse.  The
        location of the impulse is given in the signal settings.  The delay
        from the impulse to the start of the signal is also specified in the
        signal settings.  The microphone and generator can therefore be extracted
        from the start of the signal.
        """
        self.logger.debug("Entering _extractSignals")

        sample_rate = float(self.measurement_settings["sample rate"])
        signal_type = str(self.measurement_settings["signal type"])
        impulse_location = int(self.measurement_settings["microphone impulse location"])
        impulse_signal_delay = float(self.measurement_settings["impulse delay"])

        impulse_signal_samples = impulse_signal_delay * sample_rate

        signal_start = impulse_location + impulse_signal_samples

        self.microphone_responses = []
        for signal_index, signal in enumerate(self.microphone_signals):
            # Ignore first signal, unless there is only one signal
            if signal_index > 0 or len(self.microphone_signals) == 1:
                self.microphone_responses.append(signal[signal_start:])
        self.average_microphone_response = average(self.microphone_responses, axis=0)

        impulse_location = int(self.measurement_settings["generator impulse location"])
        signal_start = impulse_location + impulse_signal_samples

        self.generator_responses = []
        for signal_index, signal in enumerate(self.generator_signals):
            # Ignore first signal, unless there is only one signal
            if signal_index > 0 or len(self.generator_signals) == 1:
                self.generator_responses.append(array(signal[signal_start:]))

        self.average_generator_response = average(self.generator_responses,axis=0)

        if signal_type.lower() == "maximum length sequence":
            mls_reps = int(self.measurement_settings["mls reps"])
            mls_taps = int(self.measurement_settings["mls taps"])
            assert(mls_reps > 0)

            mls_length = 2 ** mls_taps - 1
            mls_sig = self.average_microphone_response[mls_length:(mls_length * (mls_reps))]
            mls_array = reshape(mls_sig, (mls_reps - 1, -1))
            self.average_microphone_response = average(mls_array, axis=0)

            mls_sig = self.average_generator_response[mls_length:(mls_length * (mls_reps))]
            mls_array = reshape(mls_sig, (mls_reps - 1, -1))
            self.average_generator_response = average(mls_array, axis=0)

        elif signal_type.lower() == "inverse repeat sequence":
            mls_reps = int(self.measurement_settings["mls reps"])
            mls_taps = int(self.measurement_settings["mls taps"])
            assert(mls_reps > 1)

            mls_length = 2 ** mls_taps - 1
            irs_length = 2 * mls_length

            irs_sig = self.average_microphone_response[irs_length:(irs_length * (mls_reps))]
            irs_array = reshape(irs_sig, (mls_reps - 1, -1))
            self.average_microphone_response = average(irs_array, axis=0)

            irs_sig = self.average_generator_response[irs_length:(irs_length * (mls_reps))]
            irs_array = reshape(irs_sig, (mls_reps - 1, -1))
            self.average_generator_response = average(irs_array, axis=0)

    def _downsampleSignals(self):
        """ Low pass filter microphone response and generator response, then down
            sample by an Integer factor.

            The response signals is either the raw signal, or if the MLS signal
            was used to excite the system, the system response determined by
            the convolution property of MLS signals.
        """
        self.logger.debug("Entering _downsampleSignals")

        # Get required variables
        sample_rate = float(self.measurement_settings["sample rate"])
        decimation_factor = int(self.analysis_settings["decimation factor"])
        filter_order = int(self.analysis_settings["antialiasing filter order"])

        # Down sample the responses
        if decimation_factor > 1:
            self.microphone_response_ds = decimate(self.microphone_response, decimation_factor, ftype="fir")
            self.generator_response_ds = decimate(self.generator_response, decimation_factor, ftype="fir")
        else:
            self.microphone_response_ds = self.microphone_response
            self.generator_response_ds = self.generator_response
        #self.system_response_ds = decimate(self.system_response, decimation_factor)
        #self.microphone_response_ds = lfilter(b, a, self.microphone_response[::decimation_factor])
        #self.generator_response_ds = lfilter(b, a, self.generator_response[::decimation_factor])

    def _liftImpulseResponse(self):
        """ Lift the impulse response off the power cepstrum.

        Using the specified window settings, create a window to lift the
        impulse response off the power cepstrum.
        """
        self.logger.debug("Entering _liftImpulseResponse")

        # Get required variables
        window_type = str(self.analysis_settings["window type"])
        window_start = float(self.analysis_settings["window start"])
        window_end = float(self.analysis_settings["window end"])
        taper_length = float(self.analysis_settings["taper length"])
        sample_rate = float(self.measurement_settings["sample rate"])
        decimation_factor = float(self.analysis_settings["decimation factor"])

        effective_sample_rate = sample_rate / decimation_factor

        window_length = window_end - window_start

        window_samples = window_length * effective_sample_rate
        taper_samples = taper_length * effective_sample_rate

        # Create the window
        tapers = hanning(2 * taper_samples)
        if window_type == "one sided":
            self.window = r_[ones(window_samples - taper_samples), tapers[taper_samples:]]
        elif window_type == "two sided":
            self.window = r_[tapers[:taper_samples],
                        ones(window_samples - (2 * taper_samples)),
                        tapers[taper_samples:]]
        self.window[-1] = 0
        # Lift impulse response
        start = window_start * effective_sample_rate
        end = start + len(self.window)

        self.impulse_response = self.power_cepstrum[start:end].copy()
        self.impulse_response *= self.window

    def _determineCepstrum(self):
        """ Determines the power cepstra of the averaged microphone and
            generator signals.

            The power cepstra is defined as:
                c(t) = ifft(log(abs(fft(x(t)) ** 2))

            "The inverse Fourier Transform of the logarithmic squared magnitude
            of the Fourier Transform of a signal"

            The scaled generator cepstrum is subtracted from the microphone
            cepstrum to determine the overall system cepstrum.

            The algorithm to determine the scaling factor, beta, is as follows:
            1. Determine where the squared modulus of the generator cepstrum reaches
                1% of it's peak.
            2. Window out the generator cepstrum from that sample to the sample before
                the start of the impulse response lifter.
            3. Window out the same section from the microphone cepstrum.
            4. Beta is then equal to the ratio of the windowed generator cepstrum to
                that of the windowed microphone cepstrum.

        """
        self.logger.debug("Entering _determineCepstrum")

        # required variables
        fft_size = int(self.measurement_settings["fft size"])
        window_start = float(self.analysis_settings["window start"])
        sample_rate = float(self.measurement_settings["sample rate"])
        decimation_factor = float(self.analysis_settings["decimation factor"])

        effective_sample_rate = sample_rate / decimation_factor

        cepstrum = lambda x: irfft(log(abs(rfft(x, fft_size)) ** 2))

        # Determine Cepstra
        self.microphone_cepstrum = cepstrum(self.microphone_response_ds)
        self.generator_cepstrum = cepstrum(self.generator_response_ds)
        #self.system_cepstrum = cepstrum(self.system_response_ds)

        #self.power_cepstrum = ifft(log(abs(fft(self.microphone_response_ds, fft_size) /  fft(self.generator_response_ds, fft_size)) ** 2))

        self.power_cepstrum = self.microphone_cepstrum - self.generator_cepstrum
Example #8
0
class AbsorptionCoefficient(object):
    def __init__(self, microphone_signals, generator_signals,
                 measurement_settings, analysis_settings):
        """ Constructor for AbsorptionCoefficient object.

        :param microphone_signals:
            An array of signals recorded from the microphone.
        :type microphone_signals:
            array of float
        :param generator_signals:
            An array of signals recorded directly from the generator.
        :type generator_signals:
            array of float
        :param measurement_settings:
            A dictionary containing the settings used to measure the signals.
            As well as the location of the impulse in the microphone signal and
            the generator signal.
        :type measurement_settings:
            dict
        :param analysis_settings:
            A dictionary containing the settings used to analyze the signals.
        :type analysis_settings:
            dict
        """
        self.logger = logging.getLogger("Alpha")
        self.logger.debug("Creating AbsorptionCoefficient Object")

        self.microphone_signals = microphone_signals
        self.generator_signals = generator_signals
        self.measurement_settings = measurement_settings
        self.analysis_settings = analysis_settings

        self.mls_db = MlsDb()

        self.determineAlpha()

    def determineAlpha(self):
        """ Determine the absorption coefficient of the material under test.

        The absorption coefficient is determined directly by lifting the
        impulse response from the power cepstrum.  The power cepstrum is the
        generator's power cepstrum subtracted from microphone's power cepstrum.
        The microphone and generator power cepstra are determined from the
        averaged recorded microphone and generator response.  These averaged
        signals are filtered with an anti-aliasing filter, and decimated.
        """
        self.logger.debug("Entering determineAlpha")

        # Extract the signals from the recorded responses, and average
        self._extractSignals()

        # Determine the system response of the signals
        self._determineResponse()

        # LPF the signals and decimate
        self._downsampleSignals()

        # Determine the Cepstrum
        self._determineCepstrum()

        # Lift the impulse response
        self._liftImpulseResponse()

        # Determine the absorption coefficient
        self._determineAbsorptionCoefficient()

    def _determineAbsorptionCoefficient(self):
        """ By taking the Fourier transform of the impulse response, the absorption coefficient can be determined.

        The absorption coefficient is simply,
         a = 1 - |F{h}|^2
         with:
            a - the absorption coefficient
            h - the impulse response
            F{} - The Fourier transform operator
        """
        self.logger.debug("Entering _determineAbsorptionCoefficient")

        fft_size = int(self.measurement_settings["fft size"])
        self.alpha = 1 - abs(fft(self.impulse_response, fft_size))**2

    def _determineResponse(self):
        """ For Psuedorandom signal, determine the system response using the autocorrellation property of the signal.

        """
        self.logger.debug("Entering _determineResponse")

        # If MLS signal, then utilize the circular convolution property
        signal_type = self.measurement_settings["signal type"]
        sample_rate = float(self.measurement_settings["sample rate"])

        if signal_type.lower() == "maximum length sequence":
            number_taps = int(self.measurement_settings["mls taps"])

            self.microphone_response = self.mls_db.getSystemResponse(
                self.average_microphone_response, number_taps)

            self.generator_response = self.mls_db.getSystemResponse(
                self.average_generator_response, number_taps)

            self.system_response = ifft(
                fft(self.microphone_response) / fft(self.generator_response))

        elif signal_type.lower() == "inverse repeat sequence":
            number_taps = int(self.measurement_settings["mls taps"])
            # TODO: Re-factor this out of absorption coefficient
            mls = self.mls_db.getMls(number_taps)
            mls = -2 * mls + 1
            irs = array([])
            for index, sample in enumerate(r_[mls, mls]):
                if index % 2 == 0:
                    irs = r_[irs, sample]
                else:
                    irs = r_[irs, -1 * sample]

            self.microphone_response = irfft(
                rfft(self.average_microphone_response) * rfft(irs[-1::-1]))
            self.generator_response = irfft(
                rfft(self.average_generator_response) * rfft(irs[-1::-1]))

            # The output of the auto-correlation of an irs signal is a + impulse at 0 and a
            # - impulse at N / 2.  One is only interested in the positive impulse, so extract
            # up to N / 2 samples, or the MLS length of 2 ^ (number of taps) - 1.  BUT, since
            # the DFT is circular, samples at the end wrap to be connected to sample's at the
            # start - if the start of the IRS signal is missed, then part of the - impulse
            # response will corrupt the positive impulse response.

            impulse_length = len(mls)
            window = hanning(0.1 * impulse_length)

            self.microphone_response = self.microphone_response[:0.9 *
                                                                impulse_length]
            self.microphone_response[-len(window) /
                                     2:] *= window[-len(window) / 2:]
            tmp_response = self.microphone_response[-0.1 * impulse_length:]
            tmp_response[:len(window) / 2] *= window[:len(window) / 2]
            append(self.microphone_response, tmp_response)

            self.generator_response = self.generator_response[:0.9 *
                                                              impulse_length]
            self.generator_response[-len(window) / 2:] *= window[-len(window) /
                                                                 2:]
            tmp_response = self.generator_response[-0.1 * impulse_length:]
            tmp_response[:len(window) / 2] *= window[:len(window) / 2]
            append(self.generator_response, tmp_response)

        else:
            self.microphone_response = self.average_microphone_response
            self.generator_response = self.average_generator_response

    def _extractSignals(self):
        """ Extract the microphone and generator signals from the raw signals.

        The microphone and generator signal are preceded by an impulse.  The
        location of the impulse is given in the signal settings.  The delay
        from the impulse to the start of the signal is also specified in the
        signal settings.  The microphone and generator can therefore be extracted
        from the start of the signal.
        """
        self.logger.debug("Entering _extractSignals")

        sample_rate = float(self.measurement_settings["sample rate"])
        signal_type = str(self.measurement_settings["signal type"])
        impulse_location = int(
            self.measurement_settings["microphone impulse location"])
        impulse_signal_delay = float(
            self.measurement_settings["impulse delay"])

        impulse_signal_samples = impulse_signal_delay * sample_rate

        signal_start = impulse_location + impulse_signal_samples

        self.microphone_responses = []
        for signal_index, signal in enumerate(self.microphone_signals):
            # Ignore first signal, unless there is only one signal
            if signal_index > 0 or len(self.microphone_signals) == 1:
                self.microphone_responses.append(signal[signal_start:])
        self.average_microphone_response = average(self.microphone_responses,
                                                   axis=0)

        impulse_location = int(
            self.measurement_settings["generator impulse location"])
        signal_start = impulse_location + impulse_signal_samples

        self.generator_responses = []
        for signal_index, signal in enumerate(self.generator_signals):
            # Ignore first signal, unless there is only one signal
            if signal_index > 0 or len(self.generator_signals) == 1:
                self.generator_responses.append(array(signal[signal_start:]))

        self.average_generator_response = average(self.generator_responses,
                                                  axis=0)

        if signal_type.lower() == "maximum length sequence":
            mls_reps = int(self.measurement_settings["mls reps"])
            mls_taps = int(self.measurement_settings["mls taps"])
            assert (mls_reps > 0)

            mls_length = 2**mls_taps - 1
            mls_sig = self.average_microphone_response[mls_length:(mls_length *
                                                                   (mls_reps))]
            mls_array = reshape(mls_sig, (mls_reps - 1, -1))
            self.average_microphone_response = average(mls_array, axis=0)

            mls_sig = self.average_generator_response[mls_length:(mls_length *
                                                                  (mls_reps))]
            mls_array = reshape(mls_sig, (mls_reps - 1, -1))
            self.average_generator_response = average(mls_array, axis=0)

        elif signal_type.lower() == "inverse repeat sequence":
            mls_reps = int(self.measurement_settings["mls reps"])
            mls_taps = int(self.measurement_settings["mls taps"])
            assert (mls_reps > 1)

            mls_length = 2**mls_taps - 1
            irs_length = 2 * mls_length

            irs_sig = self.average_microphone_response[irs_length:(irs_length *
                                                                   (mls_reps))]
            irs_array = reshape(irs_sig, (mls_reps - 1, -1))
            self.average_microphone_response = average(irs_array, axis=0)

            irs_sig = self.average_generator_response[irs_length:(irs_length *
                                                                  (mls_reps))]
            irs_array = reshape(irs_sig, (mls_reps - 1, -1))
            self.average_generator_response = average(irs_array, axis=0)

    def _downsampleSignals(self):
        """ Low pass filter microphone response and generator response, then down
            sample by an Integer factor.

            The response signals is either the raw signal, or if the MLS signal
            was used to excite the system, the system response determined by
            the convolution property of MLS signals.
        """
        self.logger.debug("Entering _downsampleSignals")

        # Get required variables
        sample_rate = float(self.measurement_settings["sample rate"])
        decimation_factor = int(self.analysis_settings["decimation factor"])
        filter_order = int(self.analysis_settings["antialiasing filter order"])

        # Down sample the responses
        if decimation_factor > 1:
            self.microphone_response_ds = decimate(self.microphone_response,
                                                   decimation_factor,
                                                   ftype="fir")
            self.generator_response_ds = decimate(self.generator_response,
                                                  decimation_factor,
                                                  ftype="fir")
        else:
            self.microphone_response_ds = self.microphone_response
            self.generator_response_ds = self.generator_response
        #self.system_response_ds = decimate(self.system_response, decimation_factor)
        #self.microphone_response_ds = lfilter(b, a, self.microphone_response[::decimation_factor])
        #self.generator_response_ds = lfilter(b, a, self.generator_response[::decimation_factor])

    def _liftImpulseResponse(self):
        """ Lift the impulse response off the power cepstrum.

        Using the specified window settings, create a window to lift the
        impulse response off the power cepstrum.
        """
        self.logger.debug("Entering _liftImpulseResponse")

        # Get required variables
        window_type = str(self.analysis_settings["window type"])
        window_start = float(self.analysis_settings["window start"])
        window_end = float(self.analysis_settings["window end"])
        taper_length = float(self.analysis_settings["taper length"])
        sample_rate = float(self.measurement_settings["sample rate"])
        decimation_factor = float(self.analysis_settings["decimation factor"])

        effective_sample_rate = sample_rate / decimation_factor

        window_length = window_end - window_start

        window_samples = window_length * effective_sample_rate
        taper_samples = taper_length * effective_sample_rate

        # Create the window
        tapers = hanning(2 * taper_samples)
        if window_type == "one sided":
            self.window = r_[ones(window_samples - taper_samples),
                             tapers[taper_samples:]]
        elif window_type == "two sided":
            self.window = r_[tapers[:taper_samples],
                             ones(window_samples - (2 * taper_samples)),
                             tapers[taper_samples:]]
        self.window[-1] = 0
        # Lift impulse response
        start = window_start * effective_sample_rate
        end = start + len(self.window)

        self.impulse_response = self.power_cepstrum[start:end].copy()
        self.impulse_response *= self.window

    def _determineCepstrum(self):
        """ Determines the power cepstra of the averaged microphone and
            generator signals.

            The power cepstra is defined as:
                c(t) = ifft(log(abs(fft(x(t)) ** 2))

            "The inverse Fourier Transform of the logarithmic squared magnitude
            of the Fourier Transform of a signal"

            The scaled generator cepstrum is subtracted from the microphone
            cepstrum to determine the overall system cepstrum.

            The algorithm to determine the scaling factor, beta, is as follows:
            1. Determine where the squared modulus of the generator cepstrum reaches
                1% of it's peak.
            2. Window out the generator cepstrum from that sample to the sample before
                the start of the impulse response lifter.
            3. Window out the same section from the microphone cepstrum.
            4. Beta is then equal to the ratio of the windowed generator cepstrum to
                that of the windowed microphone cepstrum.

        """
        self.logger.debug("Entering _determineCepstrum")

        # required variables
        fft_size = int(self.measurement_settings["fft size"])
        window_start = float(self.analysis_settings["window start"])
        sample_rate = float(self.measurement_settings["sample rate"])
        decimation_factor = float(self.analysis_settings["decimation factor"])

        effective_sample_rate = sample_rate / decimation_factor

        cepstrum = lambda x: irfft(log(abs(rfft(x, fft_size))**2))

        # Determine Cepstra
        self.microphone_cepstrum = cepstrum(self.microphone_response_ds)
        self.generator_cepstrum = cepstrum(self.generator_response_ds)
        #self.system_cepstrum = cepstrum(self.system_response_ds)

        #self.power_cepstrum = ifft(log(abs(fft(self.microphone_response_ds, fft_size) /  fft(self.generator_response_ds, fft_size)) ** 2))

        self.power_cepstrum = self.microphone_cepstrum - self.generator_cepstrum
class SignalGenerator(object):

    def __init__(self, parameters):
        """Constructor to create signal generator

        :param parameters:
            The parameters to use to generate the signal.  It is a dictionary
            containing information, such as sample rate, signal type and other
            depending on the signal.
        :type parameters:
            dict
        """
        self.logger = logging.getLogger("Alpha")

        self.parameters = parameters

        self.mls_db = MlsDb()

        self.generateSignal()

    def setParameters(self, parameters):
        """" Update the signal parameters, and update the signal.

        :param parameters:
            The signal parameters to use to generate the signal.
        :type parameters:
            dict
        """
        self.logger.debug("Entering setParameters")

        self.parameters = parameters
        self.generateSignal()

    def generateSignal(self):
        """Generate the signal specified by the parameters.

        Using the parameters specified, a new signal will be generated.  The
        signal will then be accessible by the signal property of the
        SignalGenerator object.
        """
        self.logger.debug("Entering generateSignal")

        # Get parameters
        signal_type = str(self.parameters["signal type"])

        lpf_enabled = int(self.parameters["lpf enabled"])
        lpf_cutoff = int(self.parameters["lpf cutoff"])
        lpf_order = int(self.parameters["lpf order"])
        hpf_enabled = int(self.parameters["hpf enabled"])
        hpf_cutoff = int(self.parameters["hpf cutoff"])
        hpf_order = int(self.parameters["hpf order"])

        pad_signal = int(self.parameters["pad signal"])
        signal_reps = int(self.parameters["signal reps"])

        if "gain" in self.parameters:
            gain = float(self.parameters["gain"])
            if gain <= 0:
                # Gain is in dB; convert to decimal
                gain = 10 ** (gain / 20.0)
        else:
            gain = 0.562341325190349 # -6 dB
        # Generate the signal
        if signal_type.lower() == "swept sine":
            self.generateSweptSine()
        if signal_type.lower() == "low pass swept sine":
            self.generateLowPassSweptSine()
        if signal_type.lower() == "maximum length sequence":
            self.generateMls()
        if signal_type.lower() == "inverse repeat sequence":
            self.generateIRS()

        # Filter the signal
        if lpf_enabled == 1:
            self.filterSignal(lpf_cutoff, lpf_order, "low")
        if hpf_enabled == 1:
            self.filterSignal(hpf_cutoff, hpf_order, "high")


        #self.inverseFilter()
        # Adjust gain
        # TODO: Get gain from database
        self.signal /= max(abs(self.signal))
        self.signal *= gain

        # Pad the filter with impulse, and delay at the end
        if pad_signal == 1:
            self.padSignal()

        # Repeat the signal to improve SNR
        tmp_signal = self.signal
        for i in range(signal_reps):
            self.signal = r_[self.signal, tmp_signal]

    def inverseFilter(self):
        """
            Loads the frequency response of the loud speaker,
            fits a cosine series to the logarithm of squared magnitude of the frequency response.
            determines the magnitude of the spectrum by
                |Sf| = |St| / |Sl|
            with:
                Sf the frequency repsonse of the consentation filter
                St is the target spectrum
                Sl is the loudspeaker response

            determines the minimum phase of the compensation filter:
        """
        self.logger.debug("Entering inverseFilter")
        import BaseDelegate
        # Create new base delegate
        bd = BaseDelegate.BaseDelegate()

        # Load the frequency response
        measurement_file = "../testdata/120802_frequency_response_20.fdb"

        freq_response = bd.loadFrequencyResponse(measurement_file)
        sample_rate = float(freq_response.measurement_settings["sample rate"])

        N = len(freq_response.frequency_response)
        # find the bin of 4000 Hz
        bin = float(floor(4410* N / sample_rate))
        freq = freq_response.frequency_response

        # We are solving Ax = 2 * log10(abs(y))
        # Determine A
        M = 20
        k = arange(bin)

        a = array([])
        for m in range(M):
            a = r_[a, cos(2 * pi * k * m / bin)]
        A = matrix(reshape(a, (M, bin)))

        # Determine the weights
        W = pinv(A).transpose()*asmatrix(2 * log10(abs(freq[:bin]))).transpose()

        # Create 2 * log10(abs(y))
        s = zeros(bin)
        for m, w in enumerate(W):
            s += w[0,0] * cos(2 * pi * k * m / bin)

        # target spectrum is now
        mix_samples = ceil(bin * 0.1)
        # create first half of s
        transistion = linspace(1, 0, mix_samples) * s[-mix_samples:] + linspace(0, 1, mix_samples) * 2 * log10(freq_response.frequency_response[bin - mix_samples: bin])
        s = r_[s[:bin - mix_samples], transistion, 2 * log10(freq_response.frequency_response[bin:N / 2])]

        # mirror it
        s = r_[s, s[::-1]]

        plot(s)
        plot(2*log10(freq_response.frequency_response))
        show()

        S = 10 ** (s / 2.0)
        #plot(S,  "--")
        #plot(freq_response.frequency_response)
        #show()
        # compensation filter
        X = fft(self.signal, N)
        Sc = abs(freq_response.frequency_response) / abs(X)

        #Sc = abs(S) / abs(freq_response.frequency_response)

        # To ensure that the filter is causal, and the impulse response is as short as possible in the time domain
        # determine the minimum phase to use with the filter
        c = ifft(log(abs(Sc) ** -1), N)
        m = r_[c[0], 2 * c[1:N / 2.0 - 1], c[N/2] ]
        m = r_[m, zeros(N - len(m))]

        Scmp = exp(fft(m, N))

        Y = Scmp * X
        x = ifft(Y)

        x = x[:len(self.signal)]

        self.signal = x / max(abs(x))



    def generateSweptSine(self):
        """Generate a linear swept sine wave from lower frequency to an upper
           frequency in a specific length of time.

           A swept sine signal sweeps in frequency from a lower frequency, f_0,
           to a upper frequency, f_1, in a given time, T.

           a = pi * ( f_1 - f_0 ) / T
           b = 2 * pi * f_0

           s = sin( (at + b) * t)

           The parameters for the swept sine are stored in the parameters
           dictionary.
        """
        self.logger.debug("Entering generateSweptSine")

        # Get signal parameters
        if "lower frequency" in self.parameters:
            f_0 = int(self.parameters["lower frequency"])
        else:
            f_0 = 0
        f_1 = int(self.parameters["upper frequency"])
        T = float(self.parameters["signal length"])
        sample_rate = float(self.parameters["sample rate"])
        signal_length = float(self.parameters["signal length"])

        # Generate time
        t = arange(0, signal_length, 1 / sample_rate)

        # Generate the signal
        a = pi * (f_1 - f_0) / T
        b = 2 * pi * f_0

        self.signal = sin((a * t + b) * t)

    def generateLowPassSweptSine(self):
        """Generates a swept sine signal, the inverse filters the signal to
            reduce the ripples in the spectrum.

        Creates a minimum phases inverse filter to create a flat spectrum for
        the swept sine.  It should be noted that this algorithm creates a flat
        spectrum up to half of the Nyquist ( Fs / 2 ); where as for cepstral
        techniques a spectrum up to a specified frequency (in this case, the
        upper frequency of the swept sine) is ideal.

        This function therefore assumes a sampling rate of 2 * upper frequency
        of the swept sine, inverse filters the signal, and re-samples the signal
        up to the real sampling rate.

        The algorithm is given as:
          First a swept sine is generated using
              s = sin((a * t + b) * t)
          where:
              a = pi * ((sample_rate / 2) - f_0) / T
              b = 2 * pi * freq1
              T is the signal length
              f_1, f_0 are the upper and lower frequencies

          Then zero pad to 4096 points, and get
              S = FFT(s)

          Take the inverse of S.

          The minimum phase is generated by the following algorithm
              cp = IFFT(log|S| ^ -1)

          This function is then windowed as follows:
                       / cp[n]         n = 0, N/2
              m[n] =  | 2 * cp[n]      1 <= n < N/2
                       \ 0             N/2 < n <= N-1
          Then
              Re[FFT(m)] is equal to log|S| ^ -1
              Im[FFT(m)] is the minimum phase function ie <S_mp

          The minimum phase inverse spectrum is then given by
              S_mp ^ -1 = e ^ { IFFT(m) }
        """
        self.logger.debug("Entering generateModifiedSweptSine")

        # Get signal parameters
        f_0 = int(self.parameters["lower frequency"])
        f_1 = int(self.parameters["upper frequency"])
        T = float(self.parameters["signal length"])
        sample_rate = float(self.parameters["sample rate"])
        fft_size = int(self.parameters["fft size"])
        signal_length = float(self.parameters["signal length"])

        # Generate time vector
        t = arange(0, signal_length, 1 / sample_rate)

        # Generate the signal from 0 to Nyquist frequency
        s = sin(2 * pi * (((sample_rate / 2)   - 0) / (2 * T) * t + 0) * t)


        # Determine the spectrum
        S = fft(s, fft_size)

        # Inverse of the magnitude spectrum
        iaS = abs(S) ** -1

        # c, similar to the cepstrum, is the inverse of the logarithmic inverse
        # magnitude spectrum
        c = ifft(log(iaS))

        # Window c to produce m
        m = r_[c[0], 2 * c[1:len(S) / 2 - 1], c[len(S) / 2], zeros(len(S) / 2)]

        # Determine the spectrum of the windowed 'cepstrum'
        M = fft(m, fft_size)

        # Determine the minimum phase inverse filter
        iSmp = exp(M)

        # Determine the minimum phase spectrum
        Smp = S * iSmp

        # Determin the minimum phase signal
        smp = ifft(Smp)

        # smp will have fft_size samples, which could be very long
        # reduce to length of the signal specified
        smp = smp[:len(t)]

         # Low pass filter the signal to the upper frequency
        [b, a] = butter(8, 0.8 * f_1 / (sample_rate / 2), btype="low")
        #smp = lfilter(b, a, smp)

        # Normalize so that the maximum value is 1
        smp /= max(abs(smp))

        self.signal = smp

    def generateMls(self):
        """Fetches the MLS signal from the database with the desired number of
            taps, and maps the values from {0,1} -> {+1, -1}

        """
        self.logger.debug("Entering generateMls")

        # Get signal parameters
        taps = int(self.parameters["mls taps"])
        reps = int(self.parameters["mls reps"])

        mls = self.mls_db.getMls(taps)

        mls = -2 * mls + 1

        repeated_mls = mls

        for i in range(reps + 1):
            repeated_mls = r_[repeated_mls, mls]

        self.signal = repeated_mls

    def generateIRS(self):
        """ Creates Inverse Repeat Sequence by fetching the required number of
            taps from the MLS db, then forming the IRS signal as follows:
                x[n] = s[n]     n is even
                x[n] = -s[n]    n is odd
        """
        self.logger.debug("Entering generateIRS")

        # Get signal parameters
        taps = int(self.parameters["mls taps"])
        reps = int(self.parameters["mls reps"])

        mls = self.mls_db.getMls(taps)

        mls = -2 * mls + 1

        irs = array([])
        for index, sample in enumerate(r_[mls, mls]):
            if index % 2 == 0:
                irs = r_[irs, sample]
            else:
                irs = r_[irs, -1 * sample]

        repeated_irs = irs
        for i in range(reps + 1):
            repeated_irs = r_[repeated_irs, irs]

        self.signal = repeated_irs

    def filterSignal(self, cutoff, order, type):
        """ Filters the current signal with a specified filter.

        :param cutoff:
            The cut off frequency of the filter.
        :type cutoff:
            float
        :param order:
            The order of the filter to use.
        :type order:
            int
        :param type:
            The type of filter to use, either "low" or "high", for low pass
            filter, or high pass filter.
        :type type:
            str
        """
        self.logger.debug("Entering filterSignal (%s, %s, %s)" % (cutoff, order, type))

        # Get signal parameters
        sample_rate = float(self.parameters["sample rate"])

        if type == "high":
            [b, a] = butter(order, cutoff / (sample_rate / 2), btype=type)
            self.signal = lfilter(b, a, self.signal)
        #    b = firwin(251, cutoff=[50, 200], nyq=(sample_rate / 2))
        #    a = 1
        #    self.signal = lfilter(b, a, self.signal)

        #    [b, a] = iirdesign(wp=cutoff / (sample_rate / 2), ws = 50 / (sample_rate / 2), gpass=1, gstop=12, ftype="butter")
        else:

            [b, a] = butter(order, cutoff / (sample_rate / 2), btype=type)

            self.signal = lfilter(b, a, self.signal)

    def padSignal(self):
        """ Pad signal with an impulse at the front of the signal, followed by
            a delay, also adds a delay to the end of the signal.
        """
        self.logger.debug("Entering padSignal")

        # Get signal parameters
        impulse_delay = float(self.parameters["impulse delay"])
        signal_padding = float(self.parameters["signal padding"])
        sample_rate = float(self.parameters["sample rate"])

        impulse_delay_samples = zeros(int(impulse_delay * sample_rate))
        signal_padding_samples = zeros(int(signal_padding * sample_rate))

        self.signal = r_[0, 1, impulse_delay_samples, self.signal,
                         signal_padding_samples]
class FrequencyResponse(object):

    def __init__(self, microphone_signals, generator_signals,
        measurement_settings):
        """ Default constructor to create the FrequencyResponse object.

            :param microphone_signals:
                An array of signals recorded from the microphone.
            :type microphone_signals:
                array of float
            :param generator_signals:
                An array of signals recorded directly from the generator.
            :type generator_signals:
                array of float
            :param measurement_settings:
                A dictionary containing the settings used to measure the
                signals. As well as the location of the impulse in the
                microphone signal and the generator signal.
            :type measurement_settings:
                dict
        """
        self.logger = logging.getLogger("Alpha")
        self.logger.debug("Creating Frequency Response Object")

        # Set up object variables
        self.microphone_signals = microphone_signals
        self.generator_signals = generator_signals
        self.measurement_settings = measurement_settings

        # Create new object to access MLS database
        self.mls_db = MlsDb()

        self.determineFreqResp()

    def determineFreqResp(self):
        """ Determine the frequency response of device under test, usually a
        loudspeaker.

        First, the system response of the microphone response and the generator
        response are determined.  From this, the generator response is
        deconvolved from the microphone response.  The resulting response is
        then transformed into the frequency domain, giving the frequency
        response of the device.
        """
        self.logger.debug("Entering determineFreqResp")

        # First the signals need be extracted and averaged together
        self._extractSignals()

        # Then determine the system response
        self._determineSystemResponse()

        # deconvolve the generator response from the microphone response
        self._deconvoleSignals()

        # Extract the impulse response
        self._extractImpulseResponse()

    def _extractImpulseResponse(self):
        """ Extract the impulse response of the device under investigation, by
        windowing it out directly from the system response.
        """
        self.logger.debug("Entering _extractImpulseResponse")

        window_length = int(self.measurement_settings["window length"])
        taper_length = int(self.measurement_settings["taper length"])
        fft_size = int(self.measurement_settings["fft size"])

        taper = hanning(2 * taper_length)

        self.impulse_response = self.microphone_response[:window_length]
        self.impulse_response[-taper_length:] *= taper[-taper_length:]

        self.frequency_response = fft(self.impulse_response, fft_size)

    def _deconvoleSignals(self):
        """ Deconvolves the generator signal from the microphone signal,
            removing the effects of the generator on the signal.
        """
        self.logger.debug("Entering _deconvoleSignals")

        self.system_response = deconvolve(self.microphone_response,self.generator_response)
        # deconvolve function returns a tuple (remainder, deconvolved signal)
        self.system_response = ifft(fft(self.microphone_response) / fft(self.generator_response))


    def _determineSystemResponse(self):
        """ Determines the system response of both the microphone and generator
            recorded signals.
        """
        self.logger.debug("Entering _determineSystemResponse")

        # If MLS / IRS signal is used, use the circular convolution property
        # to determine the system response.
        signal_type = self.measurement_settings["signal type"]

        if signal_type.lower() == "maximum length sequence":
            number_taps = int(self.measurement_settings["mls taps"])

            self.microphone_response = self.mls_db.getSystemResponse(self.average_microphone_response, number_taps)
            self.generator_response = self.mls_db.getSystemResponse(self.average_generator_response, number_taps)
        elif signal_type.lower() == "inverse repeat sequence":
            number_taps = int(self.measurement_settings["mls taps"])

            # Preform the circular convolution manually!
            mls = self.mls_db.getMls(number_taps)
            mls = -2 * mls + 1
            irs = array([])
            for index, sample in enumerate(r_[mls, mls]):
                if index % 2 == 0:
                    irs = r_[irs, sample]
                else:
                    irs = r_[irs, -1 * sample]

            assert (len(self.average_microphone_response) == len(irs))
            assert (len(self.average_generator_response) == len(irs))

            self.microphone_response = ifft(fft(self.average_microphone_response) * fft(irs[-1::-1]))
            self.microphone_response = self.microphone_response[:2 ** number_taps - 1]
            self.generator_response = ifft(fft(self.average_generator_response) * fft(irs[-1::-1]))
            self.generator_response = self.generator_response[:2 ** number_taps - 1]
        else:
            self.microphone_response = self.average_microphone_response
            self.generator_response = self.average_microphone_response

    def _extractSignals(self):
        """ Extract the microphone and generator signals from the raw signals.

        The microphone and generator signal are preceded by an impulse.  The
        location of the impulse is given in the signal settings.  The delay
        from the impulse to the start of the signal is also specified in the
        signal settings.  The microphone and generator can therefore be
        extracted from the start of the signal.
        """
        self.logger.debug("Entering _extractSignals")

        sample_rate = float(self.measurement_settings["sample rate"])
        signal_type = str(self.measurement_settings["signal type"])
        impulse_location = int(self.measurement_settings["microphone impulse location"])
        impulse_signal_delay = float(self.measurement_settings["impulse delay"])

        impulse_signal_samples = impulse_signal_delay * sample_rate
        # Since the impulse is also a sample, we need to add one before the actual
        # start of the signal.

        signal_start = impulse_location + impulse_signal_samples

        self.microphone_responses = []
        for signal in self.microphone_signals:
            self.microphone_responses.append(signal[signal_start:])
        self.average_microphone_response = average(self.microphone_responses[1:], axis=0)

        impulse_location = int(self.measurement_settings["generator impulse location"])
        signal_start = impulse_location + impulse_signal_samples

        self.generator_responses = []
        for signal in self.generator_signals:
            self.generator_responses.append(array(signal[signal_start:]))
        self.average_generator_response = average(self.generator_responses[1:], axis=0)

        if signal_type.lower() == "maximum length sequence":
            mls_reps = int(self.measurement_settings["mls reps"])
            mls_taps = int(self.measurement_settings["mls taps"])
            assert(mls_reps > 0)

            mls_length = 2 ** mls_taps - 1
            mls_sig = self.average_microphone_response[mls_length:(mls_length * (mls_reps + 1))]
            mls_array = reshape(mls_sig, (mls_reps, -1))
            self.average_microphone_response = average(mls_array, axis=0)
            mls_sig = self.average_generator_response[mls_length:(mls_length * (mls_reps + 1))]
            mls_array = reshape(mls_sig, (mls_reps, -1))
            self.average_generator_response = average(mls_array, axis=0)
        elif signal_type.lower() == "inverse repeat sequence":
            mls_reps = int(self.measurement_settings["mls reps"])
            mls_taps = int(self.measurement_settings["mls taps"])
            assert(mls_reps > 0)

            mls_length = 2 ** mls_taps - 1
            irs_length = 2 * mls_length

            irs_sig = self.average_microphone_response[irs_length:(irs_length * (mls_reps + 1))]
            irs_array = reshape(irs_sig, (mls_reps, -1))
            self.average_microphone_response = average(irs_array, axis=0)
            irs_sig = self.average_generator_response[irs_length:(irs_length * (mls_reps + 1))]
            irs_array = reshape(irs_sig, (mls_reps, -1))
            self.average_generator_response = average(irs_array, axis=0)