Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/phosphor/trace_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,15 @@ def show_individual(self) -> bool:
def show_mean(self) -> bool:
return self._show_mean

@property
def show_error(self) -> bool:
"""Whether the standard-deviation band is drawn.

Reads back the resolved value, not what was asked for: requesting a band
without statistics resolves to off, because there is no spread to draw.
"""
return self._show_error

@property
def autoscale(self) -> bool:
return self._autoscale
Expand Down Expand Up @@ -581,13 +590,20 @@ def _summary_positions(self) -> tuple[np.ndarray | None, np.ndarray | None]:
return mean_pos, self._curve_positions(minmax_decimate(band, self._dec_plan))

def _curve_positions(self, curve: np.ndarray) -> np.ndarray:
"""One line per row of *curve*, laid into the cells."""
"""One line per row of *curve*, laid into the cells.

*curve* holds a whole number of per-channel blocks -- one for a mean,
two for the lower and upper edges of a band. Each block is mapped on its
own, because the cell mapping is per channel and would otherwise be
asked to broadcast a block of channels against twice as many rows.
"""
n_lines, m = curve.shape[0], curve.shape[-1]
pos = np.empty((n_lines, m, 3), dtype=np.float32)
# The band is two curves per channel, so x tiles rather than broadcasts.
reps = n_lines // self._n_ch
pos = np.empty((n_lines, m, 3), dtype=np.float32)
pos[..., 0] = np.tile(self._x_line_dec, (reps, 1))
pos[..., 1] = self._map_y(curve)
for i in range(reps):
block = slice(i * self._n_ch, (i + 1) * self._n_ch)
pos[block, :, 1] = self._map_y(curve[block])
pos[..., 2] = 0.0
return pos

Expand Down
25 changes: 21 additions & 4 deletions tests/test_trace_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ def make_widget(n_ch=2, n_samples=4, history=3, **config_kwargs) -> TraceGridWid
w._x_line_dec = np.tile(np.arange(n_samples, dtype=np.float32), (n_ch, 1))
w._indiv_ml = w._mean_ml = w._error_ml = None
w._graphics_version = -1
# _map_y is affine per channel; identity keeps these tests about layout.
w._map_y = lambda a: np.asarray(a, dtype=np.float32)
# The real _map_y, not a stub. It is per channel, and stubbing it is what
# let a band of 2 x n_ch rows reach it and raise on every frame while the
# tests stayed green.
w._rects = np.column_stack([np.zeros(n_ch), np.arange(n_ch, dtype=float), np.ones(n_ch)])
w._y_min, w._y_max = -1000.0, 1000.0
return w


Expand Down Expand Up @@ -196,7 +199,9 @@ def test_the_mean_spans_more_waveforms_than_are_drawn():
w._buffer.push(wave(v, n_ch=1))

mean_pos, _ = w._summary_positions()
np.testing.assert_allclose(mean_pos[0, :, 1], 3.0) # mean of 1..5, not of 4..5
# Compared through the cell mapping, since that is what the drawn y is.
expected = w._map_y(np.full((1, w._n_samples), 3.0, dtype=np.float32)) # 1..5, not 4..5
np.testing.assert_allclose(mean_pos[0, :, 1], expected[0], rtol=1e-6)


# ---- graphics actually get created ------------------------------------------
Expand Down Expand Up @@ -286,7 +291,8 @@ def test_the_newest_waveform_reaches_the_graphic():
w._refresh_lines()

ys = w._indiv_ml.data[..., 1]
assert np.isclose(ys, 7.0).any(), "the value just pushed should be in the graphic"
expected = w._map_y(np.full((w._n_ch, w._n_samples), 7.0, dtype=np.float32))[0, 0]
assert np.isclose(ys, expected).any(), "the value just pushed should be in the graphic"


def test_the_error_band_appears_once_there_is_a_spread():
Expand Down Expand Up @@ -316,3 +322,14 @@ def test_clearing_and_refilling_brings_the_graphics_back():
w._buffer.push(wave(2.0))
w._refresh_lines()
assert w._indiv_ml is not None


def test_show_error_reads_back_what_was_resolved_not_what_was_asked():
"""A band without statistics is not a band. Callers persist this value, so
reading back the request rather than the resolution would restore a setting
that never took effect."""
w = make_widget(show_error=True, track_statistics=False)
assert w.show_error is False

w = make_widget(show_error=True)
assert w.show_error is True
Loading