Visualizers#

Plotting functions for time-domain signals.

Visualization is deliberately not part of a chain: chains describe the communication system only, signals are extracted with Sequential(observations=...) and handed to the plain functions below. Every function takes ax=None, draws on the given axis (creating one only when needed) and returns it (decision D25); none of them calls plt.show().

For a 2D input, each row is treated as one stream and overlaid on the same axis with a legend; slice beforehand for anything fancier.

plot_time(x: ndarray, *, fs: float = 1.0, plot_type: Literal['real', 'abs', 'pow'] = 'real', marker: str = '-', title: str = 'Time domain', ax: Axes | None = None) → Axes[source]#

Plot a signal against time.

Parameters:
  • x (np.ndarray) – Signal, 1D or (n_streams, N) (streams are overlaid).

  • fs (float, keyword-only) – Sampling frequency used for the time axis. Default 1.0.

  • plot_type ({"real", "abs", "pow"}, keyword-only) – Quantity to plot. Default "real".

  • marker (str, keyword-only) – Matplotlib format string. Default "-".

  • title (str, keyword-only) – Axis title.

  • ax (matplotlib.axes.Axes, optional) – Axis to draw on; created when None (decision D25).

Returns:

matplotlib.axes.Axes

plot_spectrum(x: ndarray, *, fs: float = 1.0, norm: bool = True, dB: bool = True, shift: bool = False, xlim: Tuple[float, float] | None = None, ylim: Tuple[float, float] | None = None, title: str = 'Spectrum', ax: Axes | None = None) → Axes[source]#

Plot the periodogram (squared FFT modulus) of a signal.

Parameters:
  • x (np.ndarray) – Signal, 1D or (n_streams, N) (streams are overlaid).

  • fs (float, keyword-only) – Sampling frequency. Default 1.0.

  • norm (bool, keyword-only) – Normalize to the maximum. Default True.

  • dB (bool, keyword-only) – Decibel scale. Default True.

  • shift (bool, keyword-only) – Put the zero frequency at the center. Default False.

  • xlim (tuple, optional, keyword-only) – Axis limits.

  • ylim (tuple, optional, keyword-only) – Axis limits.

  • title (str, keyword-only) – Axis title.

  • ax (matplotlib.axes.Axes, optional) – Axis to draw on; created when None (decision D25).

Returns:

matplotlib.axes.Axes

plot_welch(x: ndarray, *, fs: float = 1.0, nperseg: int | None = None, norm: bool = True, dB: bool = True, xlim: Tuple[float, float] | None = None, ylim: Tuple[float, float] | None = None, title: str = 'Welch PSD', ax: Axes | None = None) → Axes[source]#

Plot the power spectral density using Welch’s method.

Parameters:
  • x (np.ndarray) – Signal, 1D.

  • fs (float, keyword-only) – Sampling frequency. Default 1.0.

  • nperseg (int, optional, keyword-only) – Samples per Welch segment (scipy.signal.welch).

  • norm – Same conventions as plot_spectrum().

  • dB – Same conventions as plot_spectrum().

  • xlim – Same conventions as plot_spectrum().

  • ylim – Same conventions as plot_spectrum().

  • title – Same conventions as plot_spectrum().

  • ax – Same conventions as plot_spectrum().

Returns:

matplotlib.axes.Axes

plot_iq(x: ndarray, *, marker: str = '.', title: str = 'Constellation', reference: ndarray | None = None, label: str | None = None, ax: Axes | None = None) → Axes[source]#

Scatter the in-phase and quadrature components of a signal.

For a single stream this is two matplotlib calls, and the examples of this repository make them rather than call this function:

ax.plot(np.real(x), np.imag(x), ".")
style.apply(ax, "iq")

which is shorter, shows the reader what an IQ plane is, and leaves the axis limits and the markers where they can be seen. What is left here is the multi-stream overlay – one labelled scatter per row of a 2D input – and the reference overlay described below.

Parameters:
  • x (np.ndarray) – Complex signal, 1D or (n_streams, N) (streams are overlaid).

  • marker (str, keyword-only) – Matplotlib format string. Default ".".

  • title (str, keyword-only) – Axis title.

  • reference (np.ndarray, optional, keyword-only) – Ideal constellation to overlay as black crosses – normally the alphabet the signal was drawn from. A received cloud means little without the points it is supposed to be near: the overlay is what turns the picture into a measurement of how far it drifted, which is why it is here rather than left to each caller.

  • label (str, optional, keyword-only) – Legend entry for the scatter. Without it a multi-stream signal labels its streams stream 0, stream 1, … and a single-stream one is not labelled at all; pass a label to overlay two signals on one axis (before and after a compensator, say) and tell them apart.

  • ax (matplotlib.axes.Axes, optional) – Axis to draw on; created when None (decision D25).

Returns:

matplotlib.axes.Axes

Examples

>>> from comnumpy.core.utils import get_alphabet
>>> alphabet = get_alphabet("QAM", 16)
>>> rng = np.random.default_rng(0)
>>> received = alphabet[rng.integers(0, 16, 200)] + 0.1 * rng.standard_normal(200)
>>> ax = plot_iq(received, reference=alphabet, title="16-QAM")
>>> len(ax.lines)             # the cloud, and the ideal points
2
plot_kde(x: ndarray, *, bw_adjust: float = 1.0, thresh: float = 0.05, levels: int = 10, title: str = 'Density', ax: Axes | None = None) → Axes[source]#

Contour plot of the I/Q kernel density estimate of a signal.

Parameters:
  • x (np.ndarray) – Complex signal (flattened before estimation).

  • bw_adjust (float, keyword-only) – Bandwidth multiplier of scipy.stats.gaussian_kde. Default 1.0.

  • thresh (float, keyword-only) – Fraction of the peak density below which nothing is drawn. Default 0.05.

  • levels (int, keyword-only) – Number of contour levels. Default 10.

  • title (str, keyword-only) – Axis title.

  • ax (matplotlib.axes.Axes, optional) – Axis to draw on; created when None (decision D25).

Returns:

matplotlib.axes.Axes

plot_error_rate(x: ndarray, measured: Mapping[str, ndarray] | ndarray, *, theory: Mapping[str, ndarray] | None = None, x_theory: ndarray | None = None, xlabel: str = 'SNR [dB]', ylabel: str = 'error rate', xscale: Literal['linear', 'log'] = 'linear', yscale: Literal['linear', 'log'] = 'log', title: str = '', ax: Axes | None = None) → Axes[source]#

Plot Monte-Carlo error rates, and the curves they are read against.

The figure every sweep ends with: measured points as hollow markers, the closed form they are supposed to reach as a line in the same colour, a logarithmic ordinate and a grid on both decades. Fourteen scripts of this repository drew it by hand before this existed.

Zeros are masked rather than plotted. A sweep point where no error was seen means the estimate ran out of samples, not that the error rate is zero, and a logarithmic axis has no place to put it. This is the ordinate’s nonpositive="mask", and it is worth knowing when drawing such a figure by hand: matplotlib’s default is "clip", which sends the point far below the axis instead of dropping it, so a joined curve dives off the bottom of the figure and comes back. On a linear ordinate a zero is an ordinary value and is kept.

The decoration – grid, labels, legend – is comnumpy.style.apply(), so a figure drawn by hand and one drawn here cannot end up looking different.

Each measured curve gets its own marker shape, and a slightly larger one than the curve before it, so that two detectors which are supposed to agree – and therefore plot on top of each other – can still be told apart: the later one rings the earlier one.

Parameters:
  • x (np.ndarray) – Abscissa of the measurements, usually an SNR in dB.

  • measured (mapping of str to np.ndarray, or np.ndarray) – Simulated error rates. A bare array is one unnamed curve.

  • theory (mapping of str to np.ndarray, optional, keyword-only) – Reference curves. A key also present in measured is drawn in that curve’s colour, so a pair reads as one statement; any other key gets its own colour.

  • x_theory (np.ndarray, optional, keyword-only) – Abscissa of the reference curves, when they are evaluated on a finer grid than the measurements. Defaults to x.

  • xlabel (str, optional, keyword-only) – Axis labels and title.

  • ylabel (str, optional, keyword-only) – Axis labels and title.

  • title (str, optional, keyword-only) – Axis labels and title.

  • xscale ({"linear", "log"}, optional, keyword-only) – Axis scales. The defaults are the ones an error-rate sweep wants, a linear SNR in dB against a logarithmic rate; yscale="linear" is for the quantities that are read on a linear axis – a throughput, a rate in bit/symbol, a probability close to one – and xscale="log" for a sweep over a blocklength or a number of iterations.

  • yscale ({"linear", "log"}, optional, keyword-only) – Axis scales. The defaults are the ones an error-rate sweep wants, a linear SNR in dB against a logarithmic rate; yscale="linear" is for the quantities that are read on a linear axis – a throughput, a rate in bit/symbol, a probability close to one – and xscale="log" for a sweep over a blocklength or a number of iterations.

  • ax (matplotlib.axes.Axes, optional, keyword-only) – Axis to draw on; created when None (decision D25).

Returns:

matplotlib.axes.Axes

Examples

>>> snr = np.arange(0, 12, 2)
>>> ax = plot_error_rate(snr, {"QPSK": 10.0 ** (-snr / 8)},
...                      theory={"QPSK": 10.0 ** (-snr / 8 - 0.05)})
>>> len(ax.lines)                      # one marker set, one curve
2
>>> ax.get_yscale()
'log'

A linear ordinate keeps the zeros, because there a zero is a point and not a missing measurement:

>>> ax = plot_error_rate(snr, {"rate": np.array([0., 1., 2., 2., 3., 3.])},
...                      ylabel="bit/symbol", yscale="linear")
>>> len(ax.lines[0].get_xdata())
6
plot_chain_profiling(chain: object, input: object, *, ax: Axes | None = None, title: str = 'Processor timings', N_test: int = 100, orientation: Literal['horizontal', 'vertical'] = 'horizontal') → Axes[source]#

Plot the profiling results of a chain of processors using a box plot to visualize the distribution of execution times.

This function runs a specified chain of processors multiple times, collects the profiling results, and visualizes them using a box plot to show the distribution of execution times for each method in the chain.

Parameters:
  • chain (object) – An object representing the chain of processors to be profiled. It must have a method profile_execution_time that takes input as an argument and returns a dictionary of execution times for each method in the chain.

  • input (any) – The input data to be passed to the profile_execution_time method of the chain. The type and structure of this input depend on the specific implementation of the chain.

  • ax (matplotlib.axes.Axes or None, optional) – Axis to draw on. If None, a new figure and axis are created.

  • title (str, optional) – The title of the box plot. Default is ‘Box Plot of Method Timings’.

  • N_test (int, optional) – The number of times to run the chain and collect profiling results. Default is 100.

  • orientation ({"horizontal", "vertical"}, optional) – Orientation of the box plot. Default is “horizontal”.

  • note:: (..) – Increasing the number N_test provides a more accurate representation of the execution time distribution but takes longer to compute.

Returns:

matplotlib.axes.Axes – The axis containing the box plot of the profiling results (decision D25).