def filter_sample_modulated_pulse(self,
                                   receive_filter: np.ndarray,
                                   symbol_period: int,
                                   quantization_level=None):
     """
     The complement to modulate_sampled_pulse or provide_transmitted_matrix, this function will provide a sampling
     of a modulated, discrete signal. Note that the next step after this will typically be to perform some sort of
     equalization of the detected symbols.
     :param receive_filter: The filter with which to correlate the channel output
     :param symbol_period: The frequency with which samples of the correlation will be detected.
     :param quantization_level: Add a quantization to the detected symbols
     :return:
     """
     if self.transmit_signal_matrix is not None:
         sampled_received_streams = []
         for stream in self.transmit_signal_matrix:
             number_symbols = self.symbol_stream_matrix.shape[1]
             detected_symbols = []
             for symbol_ind in range(number_symbols):
                 incoming_samples = stream[symbol_ind *
                                           symbol_period:symbol_ind *
                                           symbol_period +
                                           receive_filter.size]
                 incoming_samples = normalize_vector2(incoming_samples)
                 sample = receive_filter[:incoming_samples.
                                         size] @ incoming_samples
                 detected_symbols.append(sample)
             sampled_received_streams.append(np.asarray(detected_symbols))
         self.channel_output = np.reshape(np.asarray(detected_symbols),
                                          self.symbol_stream_matrix.shape)
         if quantization_level is not None:
             #   TODO allow for selecting the quantizer of the system.
             self.channel_output = quantizer(self.channel_output,
                                             quantization_level)
def test_quantizer():
    input_array = np.linspace(-1, 1, num=200)
    quantized = quantizer(input_array, 1, -.5, .5)
    plt.figure(2)
    plt.plot(input_array, quantized, label='Test Error')
    plt.xlabel("Quantizer Input")
    plt.ylabel("Quantizer Ouput")
    plt.show()
    # path = f"Output/Quantizer.png"
    # plt.savefig(path, format="png")
    def send_through_channel(self,
                             quantization_level=None,
                             plot=False,
                             noise_levels=None):
        """
        TODO generalize channel as a class through which a signal is sent.
        This function simulates passing the transmitted signal through an LTI channel.
        IMPORTANT: The output of this function is in the sample order as input. This is key for proper detection.
        Note that given the numpy convolution default, the impulse response should be provided with the longest delay
        tap on the left most index.
        :param quantization_level: If the channel includes quantization apply this level of the quantizer
        :param plot: Allow for plotting for generating figures to visualize channel output
        :return:
        """
        self.channel_output = []
        #   Apply LTI Channel
        for bit_streams in range(self.symbol_stream_matrix.shape[0]):
            # Note that Numpy's Convolve automatically flips second argument to the function.
            self.channel_output.append(
                np.convolve(self.symbol_stream_matrix[bit_streams, :],
                            np.flip(self.CIR_matrix[bit_streams, :]),
                            mode="full"))
        self.channel_output = np.asarray(self.channel_output)

        if plot == True:
            plt.figure()
            fig_main = plt.figure()
            # no_noise = fig_main.add_subplot(1, 1, 1)
            # no_noise.set_title("Channel Output")
            # no_noise.scatter(self.channel_output, self.channel_output)

        #   Add AWGN with pre-selected noise power
        self.noise_parameter[1] = np.sqrt(
            np.var(self.alphabet) * (1 / (self.SNR * 2)))
        self.channel_output += self.noise_parameter[0] + self.noise_parameter[
            1] * np.random.standard_normal(self.channel_output.shape)
        if plot == True:
            noised = fig_main.add_subplot(1, 1, 1)
            # noised.set_title("Noise Added")
            noised.set_title("Channel Output")
            plt.xlabel("Real")
            plt.ylabel("Real")
            noised.scatter(self.channel_output, self.channel_output)
            plt.show()

        if noise_levels is not None:
            #   Temporary way of setting noise average
            self.noise_parameter[0] = 10
            #   Generate a noise floor that is added randomly to certain received symbols
            self.channel_output += self.noise_parameter[
                0] * np.random.random_integers(0, noise_levels,
                                               self.channel_output.shape)

        #   Quantize before adding noise to ensure noise profile is not changed
        if quantization_level is not None:
            self.channel_output = quantizer(self.channel_output,
                                            quantization_level)
            quantized_input = np.linspace(-4, 4, num=200)
            # plt.figure()
            # plt.plot(quantized_input, quantizer(quantized_input, quantization_level), 'r', label='Quantization Function')
            # plt.scatter(self.channel_output, self.channel_output, label="Quantized Output")
            # plt.axvline(c='grey', lw=1)
            # plt.axhline(c='grey', lw=1)
            # plt.legend(loc='upper left')
            # plt.show()

        if plot == True:
            quantized = fig_main.add_subplot(1, 3, 2)
            quantized.set_title("Quantized")
            quantized.scatter(self.channel_output, self.channel_output)