Source code for torchsig.signals.builders.am

""" AM Signal
"""

# TorchSig
from torchsig.signals.builder import SignalBuilder
from torchsig.datasets.dataset_metadata import DatasetMetadata
from torchsig.utils.dsp import (
    low_pass_iterative_design,
    convolve,
    frequency_shift,
    polyphase_decimator,
    torchsig_complex_data_type
)
from torchsig.signals.signal_lists import TorchSigSignalLists

# Third Party
import numpy as np


[docs] def am_modulator ( class_name:str, bandwidth:float, sample_rate:float, num_samples:int, rng=np.random.default_rng() ) -> np.ndarray: """Amplitude Modulator (AM). Args: class_name (str): Name of the signal to modulate, ex: 'am-dsb'. bandwidth (float): The desired 3 dB bandwidth of the signal. Must be in the same units as `sample_rate` and within the bounds 0 < `bandwidth` < `sample_rate`. sample_rate (float): The sampling rate for the IQ signal. The sample rate can use a normalized value of 1, or it can use a practical sample rate such as 10 MHz. However, it must use the same units as the bandwidth parameter. num_samples (int): The number of IQ samples to produce. rng (optional): Seedable random number generator for reproducibility. Returns: np.ndarray: AM modulated signal at the appropriate bandwidth. """ if ("lsb" in class_name or "usb" in class_name): # must generate 2x the number of samples to account for the decimation by 2 # with the LSB and USB modulations num_samples_mod = 2*num_samples else: # no rate change, so no change to number of samples num_samples_mod = num_samples # generate the random message, and make complex data type message = rng.normal(0,1,num_samples_mod).astype(torchsig_complex_data_type) # scale to unit power message = message/np.sqrt(np.mean(np.abs(message)**2)) # calculate filter cutoff cutoff = bandwidth/2 # calculate maximum transition bandwidth max_transition_bandwidth = (sample_rate/2) - cutoff # derive actual transition bandwidth transition_bandwidth = max_transition_bandwidth/2 # generate bandwidth-limiting LPF lpf = low_pass_iterative_design(cutoff=cutoff, transition_bandwidth=transition_bandwidth, sample_rate=sample_rate) # apply bandwidth-limiting filter shaped_message = convolve(message, lpf) if class_name == "am-dsb-sc": baseband_signal = shaped_message elif class_name == "am-dsb": # randomly determine modulation index modulation_index = rng.uniform(0.8,4) # find the max of the message shaped_message_max = np.max(np.abs(shaped_message)) # build and scale the carrier carrier = (shaped_message_max/modulation_index)*np.ones(len(shaped_message)) # add in the carrier baseband_signal = (modulation_index*shaped_message) + carrier elif class_name == "am-lsb": # upconvert signal to center frequency = bandwidth/2 dsb_upconverted = frequency_shift(shaped_message,bandwidth/2,sample_rate) # the existing BW limiting filter can be be repurposed to discard upper band lsb_signal_at_if = convolve(dsb_upconverted,lpf) # mix LSB back down to baseband from center frequency = bandwidth/4 baseband_signal_oversampled = frequency_shift(lsb_signal_at_if,-bandwidth/4,sample_rate) # since threw away 1/2 the bandwidth to only retain LSB, then downsample by 2 in order to match # the requested self.bandwidth baseband_signal = polyphase_decimator ( baseband_signal_oversampled, 2 ) # scale by 2 baseband_signal *= 2 elif class_name == "am-usb": # downconvert signal to -bandwidth/2 dsb_downconverted = frequency_shift(shaped_message,-bandwidth/2,sample_rate) # the existing BW limiting filter can be be repurposed to discard upper band usb_signal_atif = convolve(dsb_downconverted,lpf) # mix USB back up to baseband baseband_signal_oversampled = frequency_shift(usb_signal_atif,bandwidth/4,sample_rate) # since threw away 1/2 the bandwidth to only retain USB, then downsample by 2 in order to match # the requested bandwidth baseband_signal = polyphase_decimator ( baseband_signal_oversampled, 2 ) # scale by 2 baseband_signal *= 2 # convert to appropriate type baseband_signal = baseband_signal.astype(torchsig_complex_data_type) return baseband_signal
# Builder
[docs] class AMSignalBuilder(SignalBuilder): """Implements SignalBuilder() for amplitude modulation (AM) waveform. Attributes: dataset_metadata (DatasetMetadata): Parameters describing the dataset required for signal generation. supported_classes (List[str]): List of supported signal classes. Set to `["am-dsb-sc"]`. """ supported_classes = TorchSigSignalLists.am_signals
[docs] def __init__(self, dataset_metadata: DatasetMetadata, class_name='am-dsb-sc', **kwargs): """Initializes AM Signal Builder. Sets `class_name= "am-dsb-sc"`. Args: dataset_metadata (DatasetMetadata): Dataset metadata. class_name (str, optional): Class name. """ super().__init__(dataset_metadata=dataset_metadata, class_name=class_name, **kwargs)
def _update_data(self) -> None: """Creates the IQ samples for the AM waveform based on the signal metadata fields. """ # wideband params sample_rate = self.dataset_metadata.sample_rate # signal params num_iq_samples_signal = self._signal.metadata.duration_in_samples bandwidth = self._signal.metadata.bandwidth class_name = self._signal.metadata.class_name # AM modulator at complex baseband self._signal.data = am_modulator( class_name, bandwidth, sample_rate, num_iq_samples_signal, self.random_generator ) def _update_metadata(self) -> None: """Performs a signals-specific update of signal metadata. This does nothing because the signal does not need any fields to be updated. This `_update_metadata()` must be implemented but is not required to create or modify any data or fields for this particular signal case. """