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
curvesis 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
nanprints as-: a sweep point with no measurement is a hole, and printingnanin a column of numbers reads as a value.transpose=Trueturns the table a quarter: one row per curve, named on the left, one column per point ofx. 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}}, seeunpack().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
xas 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}}, seeunpack().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}}, seeunpack().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 itsKINDS. 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')