Data display#

One representation of a result, several ways of showing it.

A sweep produces the same thing every time: an abscissa, and one series of values per curve. Printing it as a table and plotting it are two renderings of that one object, and they should not require restating it:

data = {
    "x": lengths,
    "curves": {
        "Single carrier": runtime["single carrier"],
        "OFDM": runtime["OFDM"],
    },
}

print_data(data, xlabel="Frame length", ylabel="Runtime (ms)")
plot_data(data, xlabel="Frame length", ylabel="Runtime (ms)")

x is the abscissa, curves maps a name to a series, and every series has the length of x. That is the whole contract, and it is checked in one place (unpack()) so the two renderers cannot disagree about what a valid result looks like.

It is a plain dictionary of NumPy arrays on purpose. No class to construct, nothing to import before the data exists, no dependency on pandas – and it is already the shape a sweep returns, since monte_carlo() gives back one array per metric aligned with the values it swept:

curves = monte_carlo(chain, "awgn.snr_dB", snr_dB, {"ser": compute_ser},
                     N, reference="tx", seed=1)
data = {"x": snr_dB, "curves": curves}
unpack(data: Mapping[str, Any]) → Tuple[ndarray, Dict[str, ndarray]][source]#

Check the contract of a data dictionary and return its two parts.

Parameters:

data (mapping) – {"x": abscissa, "curves": {name: values, ...}}.

Returns:

tuple of (np.ndarray, dict of str to np.ndarray) – The abscissa, and the curves as float arrays.

Raises:
  • ComnumpyError – If a key is missing or curves is not a mapping.

  • ShapeError – If a series does not have the length of x.

Examples

>>> x, curves = unpack({"x": [1, 2], "curves": {"a": [3.0, 4.0]}})
>>> x.tolist(), curves["a"].tolist()
([1.0, 2.0], [3.0, 4.0])
format_data(data: Mapping[str, Any], *, xlabel: str = 'x', ylabel: str | None = None, transpose: bool = False) → str[source]#

Render a data dictionary as an aligned text table.

One column per curve, named after it, plus the abscissa on the left. Column widths follow the widest of the header and its cells, every column is written in one format chosen from its own dynamic range, and everything is right-aligned so the decimal points line up.

A nan prints as -: a sweep point with no measurement is a hole, and printing nan in a column of numbers reads as a value.

transpose=True turns the table a quarter: one row per curve, named on the left, one column per point of x. That is the right way round when the curves are many and their names are long – six receivers with names like "DBP, 50 steps/span" make a table 160 characters wide as columns, and a readable one as rows.

Parameters:
  • data (mapping) – {"x": abscissa, "curves": {name: values}}, see unpack().

  • xlabel (str, optional, keyword-only) – Header of the abscissa column. Default "x".

  • ylabel (str, optional, keyword-only) – What the curves measure, printed as a caption above the table – the unit belongs there rather than repeated in every cell.

  • transpose (bool, optional, keyword-only) – Curves as rows and x as columns. Default False.

Returns:

str – The table, without a trailing newline.

Examples

>>> data = {"x": [1000, 2000, 4000, 8000],
...         "curves": {"Single carrier": [12.43, 24.81, 49.07, 98.71],
...                    "OFDM": [3.21, 5.87, 11.42, 22.31]}}
>>> print(format_data(data, xlabel="Frame length", ylabel="Runtime (ms)"))
Runtime (ms)
Frame length  Single carrier   OFDM
-----------------------------------
        1000            12.4   3.21
        2000            24.8   5.87
        4000            49.1  11.42
        8000            98.7  22.31

Each column keeps three significant digits on its smallest entry, which is why the two columns above do not carry the same number of decimals: 12.4 loses nothing a reader wants, and 3.21 would.

An error rate spans decades, so its column goes to scientific notation on its own:

>>> print(format_data({"x": [0, 5], "curves": {"SER": [7.4e-1, 1.2e-6]}},
...                   xlabel="SNR [dB]"))
SNR [dB]        SER
-------------------
       0  7.400e-01
       5  1.200e-06

Turned a quarter, when the names are the long part:

>>> print(format_data({"x": [-6.0, -4.5, -3.0],
...                    "curves": {"dispersion compensation":
...                               [15.8, 17.1, 18.2],
...                               "DBP, 50 steps/span":
...                               [15.9, 17.4, 18.8]}},
...                   xlabel="launch power [dBm]", transpose=True))
launch power [dBm]       -6.0  -4.5  -3.0
-----------------------------------------
dispersion compensation  15.8  17.1  18.2
DBP, 50 steps/span       15.9  17.4  18.8
print_data(data: Mapping[str, Any], *, xlabel: str = 'x', ylabel: str | None = None, transpose: bool = False) → None[source]#

Print what format_data() renders.

Parameters:
  • data (mapping) – {"x": abscissa, "curves": {name: values}}, see unpack().

  • xlabel (str, optional, keyword-only) – As in format_data().

  • ylabel (str, optional, keyword-only) – As in format_data().

  • transpose (bool, optional, keyword-only) – Curves as rows. As in format_data().

Examples

>>> print_data({"x": [0, 1], "curves": {"gain": [1.5, 2.25]}})
x  gain
-------
0  1.50
1  2.25
plot_data(data: Mapping[str, Any], *, xlabel: str | None = None, ylabel: str | None = None, kind: str | None = None, xscale: str | None = None, yscale: str | None = None, ax: 'Axes' | None = None, **kwargs: Any) → Axes[source]#

Draw the same data dictionary as a figure.

One line per curve, labelled with its name – the table’s columns and the figure’s legend come from the same dictionary, so they cannot describe different runs.

Parameters:
  • data (mapping) – {"x": abscissa, "curves": {name: values}}, see unpack().

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

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

  • kind (str, optional, keyword-only) – Passed to comnumpy.style.apply() for the decoration; see its KINDS. Without it the axis still gets a grid and a legend, but no default labels.

  • xscale (str, optional, keyword-only) – "linear" or "log". Left alone when not given.

  • yscale (str, optional, keyword-only) – "linear" or "log". Left alone when not given.

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

  • **kwargs – Forwarded to ax.plot – a marker, a line width, whatever the page wants for every curve.

Returns:

matplotlib.axes.Axes – The axis, so the caller can keep working on it.

Examples

>>> import matplotlib
>>> matplotlib.use("Agg")
>>> data = {"x": [0, 1, 2], "curves": {"ZF": [0.1, 0.05, 0.01],
...                                    "ML": [0.08, 0.03, 0.004]}}
>>> ax = plot_data(data, xlabel="SNR [dB]", ylabel="SER", yscale="log")
>>> len(ax.lines), ax.get_yscale()
(2, 'log')