<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Generating radio signals
I want to be able to generate modulated digital signals such as this one...
This is a binary signal coded with On-off keying (also known as Amplitude-shift keying).
I'm taking the long pulses to mean "1" and the short pulses to mean "0", so this signal is transmitting the digital code, "0110100010000000".
Step1:  Basic wave functions
Step2:  Amplitude modulation
First, we need to figure out how to modulate the amplitude of the carrier wave to "high amplitude" and "low amplitude" to correspond with the binary"s and 0s.
Step3:  So looking at the above example, let's see how we calculate the size of the multiplier array...
The carrier frequency is 20Hz.
The baud, in this case, is 2 bits per second, which is why we need 1 second to contain the 2 bits.
The size of the multiplier wave to modulate the carrier wave is 1000 in size because it must match the number of samples there are (which in this case, 1 second x 1000 samples per second = 1000).
Note the width of each bit (in terms of samples) is samples_per_second / bits_per_second = 1000 / 2 = 500
Write function to modulate amplitude according to binary data
To modulate the carrier wave signal, we will simply
Step4:  Sweet! So it looks like it roughly worked. But there is a problem
Step5:  Looks pretty close to the original!
Generate real wave and write to file
Step6:  Now let's try to replicate the full signal
First, we need a way to generate a HIGH or LOW pulse...
Step7:  Comparison of recorded signal vs generated signal
Complex number waves
It turns out that the HackRF only can transmit complex signals (essentially, a 2-dimensional sine wave, where time is the x axis, and the signal spins around the x axis in a helical shape that would appear like a sine wave when viewed perpendicularly).
So let's generate a complex signal...
There may be a better way, but the best way I know to generate a complex sine wave is with Euler's formula
Step8:  Now, let's rewrite the generation functions to use complex math
Step9:  Sadly, this signal did not trigger the outlet
So what's different? Well, for one, when I play the 2 signals audibly in Audacity, the original signal is pretty loud, whereas, the one I generated sounds really quiet - almost as if there is the waves I'm generating is self-canceling.
Here's a close-up of the wave I generated
<ASSISTANT_TASK:>
Python Code:
# Imports and boilerplate to make graphs look better
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import scipy
import wave
from IPython.display import Audio
def setup_graph(title='', x_label='', y_label='', fig_size=None):
    fig = plt.figure()
    if fig_size != None:
        fig.set_size_inches(fig_size[0], fig_size[1])
    ax = fig.add_subplot(111)
    ax.set_title(title)
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
Explanation: Generating radio signals
I want to be able to generate modulated digital signals such as this one...
This is a binary signal coded with On-off keying (also known as Amplitude-shift keying).
I'm taking the long pulses to mean "1" and the short pulses to mean "0", so this signal is transmitting the digital code, "0110100010000000".
End of explanation
SAMPLE_BITSIZE = 16
MAX_AMP_16BIT = int(2**SAMPLE_BITSIZE/2 - 1)
def generate_wave(freq, len_in_sec=1, samp_rate=44100, amplitude=MAX_AMP_16BIT):
    t = np.linspace(0, len_in_sec, samp_rate * len_in_sec)
    sig = amplitude * np.sin(freq * 2 * np.pi * t) 
    return sig
def write_wav_file(file_path, wav_data, sample_rate=44100, num_channels=1):
    f = wave.open(file_path, 'wb')
    f.setparams((num_channels, 2, sample_rate, len(wav_data), "NONE", "Uncompressed"))
    f.writeframes(np.array(wav_data, dtype=np.int16))
    f.close()
def write_pcm_file(signal_data, file_path, dtype='complex64'):
    np.array(signal_data).astype(dtype).tofile(file_path)
Explanation: Basic wave functions
End of explanation
samp_rate = 1000
len_in_sec = 1
carrier_freq = 20
low_amp = 0.1
high_amp = 1
t = np.linspace(0, 1, samp_rate * len_in_sec)
carrier = 1*np.sin(carrier_freq * 2 * np.pi * t)
# Modulate with the binary signal: ['0', '1']
amp_mult = np.array([low_amp]*500 + [high_amp]*500)
sig = amp_mult * carrier
setup_graph(title='sig', x_label='time', y_label='freq', fig_size=(12,6))
plt.plot(t, sig)
Explanation: Amplitude modulation
First, we need to figure out how to modulate the amplitude of the carrier wave to "high amplitude" and "low amplitude" to correspond with the binary"s and 0s.
End of explanation
SAMPLE_BITSIZE = 16
MAX_AMP_16BIT = int(2**SAMPLE_BITSIZE/2 - 1)
DEFAULT_RATIOS = {
    '_': 1,
    '0': 1,
    '1': 3
}
DEFAULT_AMP_MAP = {
    '0': MAX_AMP_16BIT * .02,
    '1': MAX_AMP_16BIT
}
def get_modulation_array(binary_data, sample_rate, baud, sig_ratios, amp_map, dtype=np.int16):
    data_points_in_bit = int(sample_rate * 1/baud)
    modulation_array = np.array([], dtype=dtype)
    for bit in binary_data:
        bit_amplitude = amp_map[bit]
        modulated_bit = np.full(data_points_in_bit, bit_amplitude, dtype=np.int16)
        modulation_array = np.append(modulation_array, modulated_bit)
    return modulation_array
def generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate,
                               baud, sig_ratios=DEFAULT_RATIOS, amp_map=DEFAULT_AMP_MAP, dtype=np.int16):
    signal_len_secs = len(binary_data) * (1/baud)
    t = np.linspace(0, signal_len_secs, sample_rate * signal_len_secs)
    
    carrier_wave = 1 * np.sin(carrier_wave_freq * 2 * np.pi * t)
    modulation_array = get_modulation_array(binary_data, sample_rate, baud, sig_ratios, amp_map, dtype)
    return t, carrier_wave * modulation_array
    
binary_data = '0110100010000000'
carrier_wave_freq = 20 #315e6
sample_rate = 100
baud = 1
t, sig = generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate, baud)
setup_graph(title='sig', x_label='time', y_label='freq', fig_size=(12,6))
plt.plot(t, sig)
Explanation: So looking at the above example, let's see how we calculate the size of the multiplier array...
The carrier frequency is 20Hz.
The baud, in this case, is 2 bits per second, which is why we need 1 second to contain the 2 bits.
The size of the multiplier wave to modulate the carrier wave is 1000 in size because it must match the number of samples there are (which in this case, 1 second x 1000 samples per second = 1000).
Note the width of each bit (in terms of samples) is samples_per_second / bits_per_second = 1000 / 2 = 500
Write function to modulate amplitude according to binary data
To modulate the carrier wave signal, we will simply:
* Generate the carrier wave
* Generate a "modulation array" - an array with the LOW or HIGH amplitudes for each data point in the carrier wave array
* Multiply the "carrier wave array" with the "modulation array", to result in the "modulated signal"
End of explanation
DEFAULT_AMP_MAP = {
    'LOW': MAX_AMP_16BIT * .02,
    'HIGH': MAX_AMP_16BIT
}
def get_modulation_array(binary_data, sample_rate, baud, sig_ratios, amp_map, dtype=np.int16):
    data_points_in_bit = int(sample_rate * 1/baud)
    modulation_array = np.array([], dtype=dtype)
    # To describe this general algorithms, I'll use the specific concrete pulse ratios:
    #    '_': 1,
    #    '0': 1,
    #    '1': 3
    # Meaning that a "1" should be 3x longer than a "0" or a "space" pulse. Now since we need a space
    # between "1"s (as well as "0"), we can calculate that the pulse for a "1" should be 3/4 of the bit
    # and the pulse for a "0" should be 1/4 of the bit (since for the 1, it's 3 parts "1" and 1 part "space")
    one_pulse_len = int((sig_ratios['1'] / (sig_ratios['1'] + sig_ratios['_'])) * data_points_in_bit)
    one_space_len = data_points_in_bit - one_pulse_len
    zero_pulse_len = int((sig_ratios['0'] / (sig_ratios['1'] + sig_ratios['_'])) * data_points_in_bit)
    zero_space_len = data_points_in_bit - zero_pulse_len
    
    modulated_one_bit = np.append(np.full(one_pulse_len, amp_map['HIGH'], dtype=dtype),
                                  np.full(one_space_len, amp_map['LOW'], dtype=dtype))
    modulated_zero_bit = np.append(np.full(zero_pulse_len, amp_map['HIGH'], dtype=dtype),
                                   np.full(zero_space_len, amp_map['LOW'], dtype=dtype))
    
    for bit in binary_data:
        modulated_bit = modulated_one_bit if bit == '1' else modulated_zero_bit
        modulation_array = np.append(modulation_array, modulated_bit)
    return modulation_array
def generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate,
                               baud, sig_ratios=DEFAULT_RATIOS, amp_map=DEFAULT_AMP_MAP, dtype=np.int16):
    signal_len_secs = len(binary_data) * (1/baud)
    t = np.linspace(0, signal_len_secs, sample_rate * signal_len_secs)
    
    carrier_wave = 1 * np.sin(carrier_wave_freq * 2 * np.pi * t)
    modulation_array = get_modulation_array(binary_data, sample_rate, baud, sig_ratios, amp_map, dtype)
    
    # Pad (or trim) the modulation array to match the length of the carrier wave
    if len(carrier_wave) > len(modulation_array):
        pad_len = len(carrier_wave) - len(modulation_array)
        modulation_array = np.append(modulation_array, np.full(pad_len, amp_map['LOW'], dtype=dtype))
    elif len(carrier_wave) < len(modulation_array):
        modulation_array = modulation_array[:len(carrier_wave)]
    
    return t, carrier_wave * modulation_array
binary_data = '0110100010000000'
carrier_wave_freq = 20 #315e6
sample_rate = 1000
baud = 1
t, sig = generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate, baud)
setup_graph(title='sig', x_label='time', y_label='freq', fig_size=(14,7))
plt.plot(t, sig)
Explanation: Sweet! So it looks like it roughly worked. But there is a problem: we have no breaks between bits (so a group of 1's looks like a single pulse). Let's fix that...
Add the zero pulse and bit spacing
There are 2 problems with the above approach:
* Zeros should actually have a short pulse rather than LOW amplitude
* Bits should be spaced apart from each other
To accomplish this, we'll just rewrite the get_modulation_array() function so that each "modulated bit" will contain both the "1" or "0" pulse AND a "space" (which will simply be the LOW value).
Also, for simplicity/efficienty, we can just calculate the "1" and "0" modulation arrays once and reuse them.
I'm also going to turn up the sample rate a bit to get a more accurate signal.
End of explanation
# Estimating baud
bits_transmitted = 16
viewing_sample_rate = 100000  # samples per second
real_sample_rate = 2000000  # samples per second
viewing_transmission_time = 8.219-6.664  # seconds
real_transmission_time = viewing_transmission_time * (viewing_sample_rate / real_sample_rate)
baud = bits_transmitted / real_transmission_time # bits per second
print('Real transmission time: {}\nBaud: {}'.format(real_transmission_time, baud))
binary_data = '0110100010000000'
carrier_wave_freq = 315e6
sample_rate = 2e6
baud = 205
t, sig = generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate, baud)
setup_graph(title='sig', x_label='time', y_label='freq', fig_size=(14,7))
plt.plot(t, sig)
write_pcm_file(sig, 'raw_data/generated_sig1.pcm', dtype='int16')
Explanation: Looks pretty close to the original!
Generate real wave and write to file
End of explanation
# Generate preamble and repeat pattern
binary_data = '0110100010000000'
carrier_wave_freq = 315e6
sample_rate = 2e6
baud = 205
def generate_pulse(bit_val, carrier_wave_freq, sample_rate, baud, multiple_of_bit_len,
                   amp_map=DEFAULT_AMP_MAP, dtype=np.int16):
    signal_len_secs = multiple_of_bit_len * (1/baud)
    t = np.linspace(0, signal_len_secs, sample_rate * signal_len_secs)
    
    high_or_low = 'HIGH' if bit_val == '1' else 'LOW'
    pulse = amp_map[high_or_low] * np.sin(carrier_wave_freq * 2 * np.pi * t)
    return t, pulse
t1, signal_header = generate_pulse('1', carrier_wave_freq, sample_rate, baud, 3.85)
t2, signal_spacer = generate_pulse('0', carrier_wave_freq, sample_rate, baud, 3.78)
setup_graph(title='sig', x_label='time', y_label='freq', fig_size=(14, 7))
plt.plot(np.append(signal_spacer, signal_header))
def join_all_arrays(array_list):
    joined = array_list[0]
    for a in array_list[1:]:
        joined = np.append(joined, a)
    return joined
full_signal = join_all_arrays([signal_header] + ([sig, signal_spacer] * 12))
setup_graph(title='sig', x_label='samples', y_label='freq', fig_size=(14, 7))
plt.plot(full_signal)
write_pcm_file(full_signal, 'raw_data/generated_sig2.pcm', dtype='int16')
Explanation: Now let's try to replicate the full signal
First, we need a way to generate a HIGH or LOW pulse...
End of explanation
t = np.linspace(0, 1, 1000)
amp = 3
freq = 10 # Hz
simple_sig = amp * np.cos(freq * 2 * np.pi * t)
plt.plot(t, simple_sig)
import cmath
complex_sig = 3 * np.e**(freq * 2 * np.pi * (0+1j) * t)
plt.plot(t, complex_sig)
simple_sig[5]
complex_sig[5]
abs(complex_sig[5])  # This is the amplitude of the wave
Explanation: Comparison of recorded signal vs generated signal
Complex number waves
It turns out that the HackRF only can transmit complex signals (essentially, a 2-dimensional sine wave, where time is the x axis, and the signal spins around the x axis in a helical shape that would appear like a sine wave when viewed perpendicularly).
So let's generate a complex signal...
There may be a better way, but the best way I know to generate a complex sine wave is with Euler's formula:
e^(i*t) = cos(t) + i sin(t)
For more information on rotation with e, see https://github.com/calebmadrigal/FourierTalkOSCON/blob/master/05_RotationWithE.ipynb
End of explanation
def generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate,
                               baud, sig_ratios=DEFAULT_RATIOS, amp_map=DEFAULT_AMP_MAP, dtype=np.int16):
    signal_len_secs = len(binary_data) * (1/baud)
    t = np.linspace(0, signal_len_secs, sample_rate * signal_len_secs)
    
    # Using Euler's formula to generate a complex sinusoidal wave
    carrier_wave = 1 * np.e**(carrier_wave_freq * 2 * np.pi * (0+1j) * t)
    modulation_array = get_modulation_array(binary_data, sample_rate, baud, sig_ratios, amp_map, dtype)
    
    # Pad (or trim) the modulation array to match the length of the carrier wave
    if len(carrier_wave) > len(modulation_array):
        pad_len = len(carrier_wave) - len(modulation_array)
        modulation_array = np.append(modulation_array, np.full(pad_len, amp_map['LOW'], dtype=dtype))
    elif len(carrier_wave) < len(modulation_array):
        modulation_array = modulation_array[:len(carrier_wave)]
    
    return t, carrier_wave * modulation_array
def generate_pulse(bit_val, carrier_wave_freq, sample_rate, baud, multiple_of_bit_len,
                   amp_map=DEFAULT_AMP_MAP, dtype=np.int16):
    signal_len_secs = multiple_of_bit_len * (1/baud)
    t = np.linspace(0, signal_len_secs, sample_rate * signal_len_secs)
    
    high_or_low = 'HIGH' if bit_val == '1' else 'LOW'
    pulse = amp_map[high_or_low] * np.e**(carrier_wave_freq * 2 * np.pi * (0+1j) * t)
    return t, pulse
binary_data = '0110100010000000'
carrier_wave_freq = 315e6
sample_rate = 2e6
baud = 205
complex64_amp_map = {
    'LOW': 1.4 * .02,
    'HIGH': 1.4
}
t, complex_signal = generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate, baud, amp_map=complex64_amp_map, dtype='complex64')
t2, signal_header = generate_pulse('1', carrier_wave_freq, sample_rate, baud, 3.85, amp_map=complex64_amp_map, dtype='complex64')
t3, signal_spacer = generate_pulse('0', carrier_wave_freq, sample_rate, baud, 3.78, amp_map=complex64_amp_map, dtype='complex64')
full_signal = join_all_arrays([signal_header] + ([complex_signal, signal_spacer] * 12))
write_pcm_file(full_signal, 'raw_data/generated_sig2.pcm', dtype='complex64')
Explanation: Now, let's rewrite the generation functions to use complex math
End of explanation
def generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate,
                               baud, sig_ratios=DEFAULT_RATIOS, amp_map=DEFAULT_AMP_MAP, dtype=np.int16):
    signal_len_secs = len(binary_data) * (1/baud)
    t = np.linspace(0, signal_len_secs, sample_rate * signal_len_secs)
    
    # Using Euler's formula to generate a complex sinusoidal wave
    carrier_wave = 1 * np.e**(carrier_wave_freq * 2 * np.pi * (0+1j) * t)
    modulation_array = get_modulation_array(binary_data, sample_rate, baud, sig_ratios, amp_map, dtype)
    
    # Pad (or trim) the modulation array to match the length of the carrier wave
    if len(carrier_wave) > len(modulation_array):
        pad_len = len(carrier_wave) - len(modulation_array)
        modulation_array = np.append(modulation_array, np.full(pad_len, amp_map['LOW'], dtype=dtype))
    elif len(carrier_wave) < len(modulation_array):
        modulation_array = modulation_array[:len(carrier_wave)]
    
    # Modulate by superwave
    super_wave_freq = carrier_wave_freq / (160*2)
    super_wave = 1 * np.e**(super_wave_freq * 2 * np.pi * (0+1j) * t)
    
    return t, carrier_wave * modulation_array * super_wave
def generate_pulse(bit_val, carrier_wave_freq, sample_rate, baud, multiple_of_bit_len,
                   amp_map=DEFAULT_AMP_MAP, dtype=np.int16):
    signal_len_secs = multiple_of_bit_len * (1/baud)
    t = np.linspace(0, signal_len_secs, sample_rate * signal_len_secs)
    
    high_or_low = 'HIGH' if bit_val == '1' else 'LOW'
    pulse = amp_map[high_or_low] * np.e**(carrier_wave_freq * 2 * np.pi * (0+1j) * t)
    return t, pulse
binary_data = '0110100010000000'
carrier_wave_freq = 315e6
sample_rate = 2e6
baud = 205 * 2
complex64_amp_map = {
    'LOW': 1.4 * .02,
    'HIGH': 1.4
}
t, complex_signal = generate_on_off_key_signal(binary_data, carrier_wave_freq, sample_rate, baud, amp_map=complex64_amp_map, dtype='complex64')
t2, signal_header = generate_pulse('1', carrier_wave_freq, sample_rate, baud, 3.85, amp_map=complex64_amp_map, dtype='complex64')
t3, signal_spacer = generate_pulse('0', carrier_wave_freq, sample_rate, baud, 3.78, amp_map=complex64_amp_map, dtype='complex64')
full_signal = join_all_arrays([signal_header] + ([complex_signal, signal_spacer] * 12))
write_pcm_file(full_signal, 'raw_data/generated_sig3.pcm', dtype='complex64')
Explanation: Sadly, this signal did not trigger the outlet
So what's different? Well, for one, when I play the 2 signals audibly in Audacity, the original signal is pretty loud, whereas, the one I generated sounds really quiet - almost as if there is the waves I'm generating is self-canceling.
Here's a close-up of the wave I generated:
It almost looks like ther eare equal and opposite waves that would be canceling each other out, but when you look closer, it doesn't seem so:
And here's a close-up of the original wave:
It looks like in the original, the carrier wave is modulated by multiplying by another wave, rather than by unchanging "HIGH" values.
So I'm going to try that now: to modulate the carrier wave by another wave, rather than by a static HIGH value...
End of explanation
<END_TASK>