### 6.02 Lab #2: Real-world channels

Due date: Wednesday, 2/18, at 11:59p

Goal

In this lab we'll measure what happens when we transmit information over real-world (non-ideal) channels, build a mathematical model of the channel, and then use it to improve receiver performance. We'll introduce eye diagrams as a useful tool for visualing what happens to signals as they pass through a channel.

Instructions

See Lab #1 for a longer narrative about lab mechanics. The 1-sentence version:

As always, help is available in 32-083 (see the Lab Hours web page).

Task 1: Generating useful test sample sequences (1 point)

In order to run experiments on transmitting data through various channels, we'll need a handy way to construct test sample sequences. Please write a Python function transmit that generates the sequence of voltage samples output by the transmitter given a sequence of message bits to be sent:

```voltage_samples = transmit(bits,npreamble=0,
npostamble=0,
samples_per_bit=8,
v0 = 0.0,
v1 = 1.0,
repeat = 1)```
bits is a sequence (i.e., a tuple, list or numpy array) of binary values (i.e., the integers 0 or 1). The generation of voltage samples is controlled by the keyword arguments, shown above with their default values. Here's what the keyword arguments do:

npreamble -- number of "0" samples to output before any of the message bits are transmitted. See v0 keyword arg for value to be used.

npostamble -- number of "0" samples to output after all the message bits have been transmitted. See v0 keyword arg for value to be used.

samples_per_bit -- how many voltage samples to output for each message bit. So total number of samples output for one repitition is npreamble + samples_per_bit*len(bits) + npostamble.

v0 -- floating point voltage to use when generating samples for "0" bits (repeated samples_per_bit times).

v1 -- floating point voltage to use when generating samples for "1" bits (repeated samples_per_bit times).

repeat -- an integer specifying how many times to repeat the sequence of preamble samples, message samples, and postamble samples.

The default value for each keyword argument (i.e., the value used if the caller doesn't explicitly specify a value) is chosen so that no keyword arguments need be supplied unless the user is trying to do something out of the ordinary.

```# template for Lab #2, Task #1
import lab2

def transmit(bits,npreamble=0,
npostamble=0,
samples_per_bit=8,
v0 = 0.0,
v1 = 1.0,
repeat = 1):
""" generate sequence of voltage samples:
bits: binary message sequence
npreamble: number of leading v0 samples
npostamble: number of trailing v0 samples
samples_per_bit: number of samples for each message bit
v0: voltage to output for 0 bits
v1: voltage to output for 1 bits
repeat: how many times to repeat whole shebang
"""

if __name__ == '__main__':
lab2.test_transmit(transmit)
```

The call to lab2.test_transmit will try calling your function and verify that the sample sequence it returns is correct. If the test detects an error, a diagnostic message describing the failure is printed; if no errors are detected, no message is printed and the test routine simply returns.

Task 2: Sending data streams through a channel (1 point)

In lab2.py we've provided two functions that compute the effect of transmitting a sequence of voltage samples through each of two channels:

```receive_samples = lab2.channel1(transmit_samples,noise=0.0)

Just ignore the noise keyword argument for now.

Write a Python function test_channel that plots the result of sending some test sample sequences through the channel function passed as an argument:

test_channel(channel)
test_channel sends the message 10101010 through the channel using a 10-sample preamble and a 100-sample postamble. It then produces a new figure containing two appropriately labeled subplots, one showing the input samples, and one showing the output samples. See the matplotlib.pyplot.subplot() function for how to include multiple plots in a single figure.

Please use the same ranges for the vertical axes in both plots to make it easy to compare the two sample sequences (the horizontal axes will be the same since there are the same since the output sequence will have the same number of samples as the input sequence). This means you'll need to set the ranges programmatically (using matplotlib.pyplot.axis) to override the auto-ranging built into the plot routines -- call axis after you've plotted the values. Choose the appropriate plot ranges intelligently by determining the minimum and maximum values to be plotted -- just don't use some hard-wired constants that happen to work for these particular channels.

As always, the plots and axes should have meaningful labels. Note that you can get the name of the channel function using channel.__name__. You might also find matplotlib.pyplot.subplots_adjust a useful function to get the subplot spacing the way you want it.

lab2_2.py is a template file for this task:

```# template for Lab #2, Task #2
import matplotlib.pyplot as p
import lab2

# turn on interactive mode, useful if we're using ipython
p.ion()

def test_channel(channel):
"""
create a test waveform and plot both it and the output of
passing the waveform through the channel.
"""

if __name__ == '__main__':
# try out our two channels
test_channel(lab2.channel1)
test_channel(lab2.channel2)

# interact with plots before exiting.  p.show() returns
# after all plot windows have been closed by the user.
# Not needed if using ipython
p.show()

```

Interview questions:

• Looking at the plots identify which channel responds most quickly to changes in its inputs. Can you detect any evidence of the 10101010 pattern in the channel that responds more slowly to its inputs?

• Suppose the input bit pattern were 0111111... (a zero followed by all ones). Which system will settle to its final value first? Estimate how long will the slowest system take to be within, say, 5% of its final value?

• Which channel is likely to support the fastest data transfer rate?

Task 3: Determining the unit-sample response of a channel (1 point)

In lecture we saw that the unit-sample response completely characterizes the effect of a channel on sequences of samples that pass through the channel. So determining the response of a channel is a handy way to model a channel and (as we'll see in Task #6) is also useful in figuring out how to engineer the receiver to compensate for a channel's less desirable effects.

The unit-sample response is always measured by testing the channel with a single sample that has the value 1.0, followed by zero samples -- the digital signaling specification we're using doesn't play a role in this step. The Python expression [1.0]+[0.0]*k will build a (K+1)-sample sequence for testing the channel. If you call one of the channel functions with this test sequence as the argument, the sequence that is returned is the unit-sample response of the channel.

There's a small complication: officially the unit-sample response extends for an infinite number of samples. For all practical purposes it'll be very close to zero after a modest number of samples and our unit-sample response channel model will still be very accurate even if we truncate the response after it has effectively died out. Note that you'll need a unit-sample input sequence that's long enough to guarantee that you'll have sufficient response samples to find the right place to do the truncation.

Write a Python function unit_sample_response that returns a truncated sequence of samples that corresponds to the unit-sample response of the specified channel:

response = unit_sample_response(channel)
The channel argument will be one of the channel functions described in Task #2. The response sequence is truncated at the point where the magnitude of 10 consecutive samples is less than 1% of the largest magnitude sample.

lab2_3.py is a template file for this task:

```# template for Lab #2, Task #3
import numpy
import matplotlib.pyplot as p
import lab2

# turn on interactive mode, useful if we're using ipython
p.ion()

def unit_sample_response(channel):
"""
Return sequence of samples that corresponds to the unit-sample
response of the channel.  The sequence is truncated at the
point where the absolute value of 10 consecutive samples is
less than 1% of the value is the largest magnitude.
"""
pass

if __name__ == '__main__':
# plot the unit-sample response of our two channels
lab2.plot_unit_sample_response(unit_sample_response,lab2.channel1)
lab2.plot_unit_sample_response(unit_sample_response,lab2.channel2)

# interact with plots before exiting.  p.show() returns
# after all plot windows have been closed by the user.
# Not needed if using ipython
p.show()
```

The test code uses your function to compute the unit-sample response of our two channels and plots the result. If your function is working correctly you should see something like  Interview questions:

• The unit-sample response shows the response of the channel sample-by-sample. It's also useful to think about the response in terms of message bit times. Say there are 32 samples/message bit. How long is each response in terms of bit times? For either channel, will the sample values of a particular message bit be influenced by the sample values from the previous message bit? From two the previous two message bits? This influence is called inter-symbol interference (ISI).

• For each channel, how many samples/bit would there have to be in order for there to be no ISI?

• The first two values of the unit-sample response for one of the two channels is zero. What does that imply about the response of that system?

Task 4: Predicting channel behavior using the convolution sum (2 points)

We also saw in lecture that given the unit-sample response we can compute the effect of the channel on an arbitrary input by performing the appropriate convolution sum.

Write a Python script that does the following for each of the two channels mentioned in Task #2:

• As in Task #2, compute the output of the channel when transmitting the message 10101010 with a 10-sample preamble and a 100-sample postamble (use the transmit function from Task #2 to prepare the input waveform).

• Predict the output of the channel using a convolution sum involving the channel's unit-sample response (use your unit_sample_response function from Task #3) and the same input as above. You should write a helper function that computes the convolution sum given an input sample sequences and the unit-sample response. numpy.convolve will perform the convolution sum given two numpy arrays as arguments, dealing with all the indexing and zero-padding issues behind the scenes. You can convert a sequence to a numpy array with a call of the form array = numpy.array(seq).

• Produce a new figure with three subplots: a plot of the computed output, a plot of the predicted output and a plot showing the the difference between computed and predicted for each sample. You will find it useful to create a re-usable "make_plot()" function that takes as arguments the title of the plot, the labels of the the X and Y axes, and the data to be plotted.

There is no template file for this task -- just organize the Python script as you think best and submit the result.

Interview questions:

• Suppose the input samples to the system corresponded to the unit step. How would you change the truncation criteria you used in task three to make sure that the response to the unit step was within one percent of its final value?

• If the unit-sample response is always positive, will the response to a unit step always be monotonically increasing?

• You'll notice that starting at some particular sample the error plot goes from essentially zero to oscillating through a set of non-zero values. Explain why error appears starting at that particular sample.

Task 5: Generating eye diagrams (2 points)

Eye diagrams were introduced in lecture as a very effective way to visualize how a channel affects the sample sequences that pass through it. Recall that an eye diagram is just a plot of the channel output, but instead of stretching time out along the x axis of the plot, we make many overlayed plots of the output waveform, each overlay corresponding to the next short segment of time. The length of a plot segment is some small multiple of the time necessary to transmit one bit of the message.

Write a Python function plot_eye_diagram that generates a new figure containing an eye diagram for the channel function passed as an argument:

plot_eye_diagram(channel,samples_per_bit=8,plot_samples=16)
The channel argument will be one of the channel functions described in Task #2. samples_per_bit allows one to experiment with how many samples to send for each message bit. plot_samples determines how many samples should plotted in each overlay. Note that simply calling plot repeatedly with the same figure selected will cause the new plot to overlay any earlier plots to the figure.

For this task, use the transmission of a random message sequence to provide data for your eye diagram. You can build the message by importing the random module and calling random.randint(0,1) repeatedly and collecting the results in a list. 200 message bits should do the trick.

In order to ensure one complete eye (typically covering the second half of one bit cell and the first half of the next) in the diagram, include enough plot samples to cover two bit cells (i.e., set plot_samples to 2*samples_per_bit.

Once your function is working, experiment with the value of samples_per_bit for both channels. Try to find the minimum value that produces an eye that's open enough to permit successful reception of the transmitted bits. You should find that the minimum value is different for the two channels.

lab2_5.py is a template file for this task:

```# template for Lab #2, Task #5
import random
import matplotlib.pyplot as p
import lab2

# turn on interactive mode, useful if we're using ipython
p.ion()

def plot_eye_diagram(channel,samples_per_bit=8,plot_samples=16):
"""
Plot eye diagram for given channel using a 200-bit random
message.  samples_per_bit determines how many samples
are sent for each message bit. plot_samples determines
how many samples should be included in each plot overlay.
plot_samples should be an integer multiple of samples_per_bit.
"""

if __name__ == '__main__':
# plot the eye diagram for both our channels

# Experiment with different values of samples_per_bit for both
# channels until you feel that the eye is open enough to permit
# successful reception of the transmitted bits.  The result
# will be different for the two channels.
plot_eye_diagram(lab2.channel1,samples_per_bit=40)
plot_eye_diagram(lab2.channel2,samples_per_bit=40)

# interact with plots before exiting.  p.show() returns
# after all plot windows have been closed by the user.
# Not needed if using ipython
p.show()
```

Interview questions:

• For each of the two channels discuss the relationship between your choice for samples_per_bit, the length of the impulse response (see Task #3) and how "open" the eye was.

• Consider what will happen to the eye if the incoming signal had, say, 0.1 volts of noise. How would that change your choice for samples_per_bit, i.e., how much more "open" would the eye have to be in order to reliably determine that value of the transmitted message bit?

In lecture we talked about deconvolution, an approach to reconstructing the signal at the input to the transmission system by looking at the transmission channel's output (Y) and using the channel's unit-sample response (H).

In particular, we showed that the sequence W would be a reconstruction of the input sample sequence X if W satisfied the difference equation

y[n] = hw[n] + hw[n-1] + ... + h[K]w[n-K].

where Y is the sequence of channel output samples and the unit-sample response H is zero after some number of samples, i.e., h[n] = 0 when n > K.

We can rearrange this equation to solve for w[n]:

w[n] = (1/h)(y[n] - hw[n-1] - ... - h[K]w[n-K]).

Since you are given Y, and since you know w[n] = 0 for n < 0, you can solve the above equation for w given y, then for w given w and y, and so on:

```w = (1/h)(y)
w = (1/h)(y - hw)
...```

You will have to think carefully about how to modify this simple "plug and chug" strategy when the leading values of H (h, h,..) are zero.

```# template for Lab #2, Task #6
import numpy,random
import matplotlib.pyplot as p
import lab2

def deconvolver(y,h):
"""
Take the samples that are the output from a channel (y), and the
channel's unit-sample response (h), deconvolve the samples, and
return the reconstructed input.
"""

if __name__ == '__main__':
"""
Develop test patterns and try using deconvolution to reconstruct
the input from the output samples for each of the two channels.
Use 8 samples per bit.  Generate plots comparing your
reconstruction to the original input and output of the channel.
Finally, try performing reconstruction with nonzero noise.
"""
```
There are three parts to this deconvolution task:

1. Please fill in the function deconvolver. The function should take a set of channel output samples and a unit-sample response (from Task #3) and return the reconstructed input.

2. Write the test program that generates an 8-bit random message (using the technique described in Task #5), generates samples from the bit sequence assuming eight samples per bit and a 100-sample postamble, passes the samples through channel1, uses the deconvolver to reconstruct the input, and then generates plots comparing the input samples, output samples and reconstructed input samples.

3. Deconvolution is very effective if there is no noise in the transmission system. To demonstrate the impact of noise, you can add noise to the channel by calling, for example, channel1(input_samples,noise=1.0e-5). This will add noise with an amplitude of about 1.0e-5 to the output of channel1. Experiment with the noise amplitude (try 1.0e-4, 1.0e-3, etc), and determine the impact of noise on the effectiveness of deconvolution. Strange things will happen, some will be made clearer next week, some will have to wait until 6.003.

Note the unit-sample response of channel1 has leading zeros. Your code should handle this case correctly.

Interview questions:

• How does the truncation threshold you used for computing the unit-sample response affect the accuracy of the noise-free deconvolution?

• Is the noise threshold at which deconvolution stops working different for channel1 and channel2? Try it and see... Can you make a guess as to why?