Add debug visualizations

pull/357/head
nyanpasu64 2019-04-12 14:12:42 -07:00
rodzic f187e53d55
commit 457e50c88a
8 zmienionych plików z 344 dodań i 51 usunięć

Wyświetl plik

@ -61,7 +61,7 @@ class Channel:
# Product of corr_cfg.trigger/render_subsampling and trigger/render_width.
_trigger_stride: int
_render_stride: int
render_stride: int
def __init__(self, cfg: ChannelConfig, corr_cfg: "Config", channel_idx: int = 0):
"""channel_idx counts from 0."""
@ -105,7 +105,7 @@ class Channel:
self._render_samp = calculate_nsamp(corr_cfg.render_ms, rsub)
self._trigger_stride = tsub * tw
self._render_stride = rsub * rw
self.render_stride = rsub * rw
# Create a Trigger object.
if isinstance(cfg.trigger, MainTriggerConfig):
@ -130,9 +130,10 @@ class Channel:
tsamp=trigger_samp,
stride=self._trigger_stride,
fps=corr_cfg.fps,
wave_idx=channel_idx,
)
def get_render_around(self, trigger_sample: int):
return self.render_wave.get_around(
trigger_sample, self._render_samp, self._render_stride
trigger_sample, self._render_samp, self.render_stride
)

Wyświetl plik

@ -14,7 +14,7 @@ from corrscope.channel import Channel, ChannelConfig, DefaultLabel
from corrscope.config import KeywordAttrs, DumpEnumAsStr, CorrError, with_units
from corrscope.layout import LayoutConfig
from corrscope.outputs import FFmpegOutputConfig
from corrscope.renderer import Renderer, RendererConfig, BaseRenderer
from corrscope.renderer import Renderer, RendererConfig, RendererFrontend
from corrscope.triggers import CorrelationTriggerConfig, PerFrameCache, SpectrumConfig
from corrscope.util import pushd, coalesce
from corrscope.wave import Wave, Flatten
@ -217,10 +217,14 @@ class CorrScope:
]
yield
def _load_renderer(self) -> BaseRenderer:
def _load_renderer(self) -> RendererFrontend:
dummy_datas = [channel.get_render_around(0) for channel in self.channels]
renderer = Renderer(
self.cfg.render, self.cfg.layout, dummy_datas, self.cfg.channels
self.cfg.render,
self.cfg.layout,
dummy_datas,
self.cfg.channels,
self.channels,
)
return renderer
@ -246,6 +250,10 @@ class CorrScope:
renderer.add_labels([channel.label for channel in self.channels])
# For debugging only
# for trigger in self.triggers:
# trigger.set_renderer(renderer)
if PRINT_TIMESTAMP:
begin = time.perf_counter()

Wyświetl plik

@ -1,6 +1,7 @@
import enum
import os
from abc import ABC, abstractmethod
from collections import defaultdict
from typing import (
Optional,
List,
@ -11,10 +12,15 @@ from typing import (
Sequence,
Type,
Union,
Tuple,
Dict,
DefaultDict,
MutableSequence,
)
import attr
import numpy as np
from matplotlib.cm import get_cmap
from corrscope.config import DumpableAttrs, with_units, TypedEnumDump
from corrscope.layout import (
@ -56,10 +62,11 @@ if TYPE_CHECKING:
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib.backend_bases import FigureCanvasBase
from matplotlib.colors import ListedColormap
from matplotlib.lines import Line2D
from matplotlib.spines import Spine
from matplotlib.text import Text, Annotation
from corrscope.channel import ChannelConfig
from corrscope.channel import ChannelConfig, Channel
# Used by outputs.py.
@ -166,6 +173,10 @@ class RendererConfig(DumpableAttrs, always_dump="*"):
# Performance (skipped when recording to video)
res_divisor: float = 1.0
# Debugging only
viewport_width: float = 1
viewport_height: float = 1
def __attrs_post_init__(self) -> None:
# round(np.int32 / float) == np.float32, but we want int.
assert isinstance(self.width, (int, float))
@ -185,7 +196,16 @@ class LineParam:
color: str
UpdateLines = Callable[[List[np.ndarray]], None]
UpdateLines = Callable[[List[np.ndarray]], Any]
UpdateOneLine = Callable[[np.ndarray], Any]
@attr.dataclass
class CustomLine:
stride: int
xdata: np.ndarray
set_xdata: UpdateOneLine
set_ydata: UpdateOneLine
@property
@ -194,7 +214,7 @@ def abstract_classvar(self) -> Any:
"""A ClassVar to be overriden by a subclass."""
class BaseRenderer(ABC):
class _RendererBackend(ABC):
"""
Renderer backend which takes data and produces images.
Does not touch Wave or Channel.
@ -222,6 +242,7 @@ class BaseRenderer(ABC):
lcfg: "LayoutConfig",
dummy_datas: List[np.ndarray],
channel_cfgs: Optional[List["ChannelConfig"]],
channels: List["Channel"],
):
self.cfg = cfg
self.lcfg = lcfg
@ -251,18 +272,22 @@ class BaseRenderer(ABC):
for color in line_colors
]
# Load channel strides.
if channels is not None:
if len(channels) != self.nplots:
raise ValueError(
f"cannot assign {len(channels)} channels to {self.nplots} plots"
)
self.render_strides = [channel.render_stride for channel in channels]
else:
self.render_strides = [1] * self.nplots
# Instance functionality
_update_main_lines: Optional[UpdateLines] = None
def update_main_lines(self, datas: List[np.ndarray]) -> None:
if self._update_main_lines is None:
self._update_main_lines = self.add_lines(datas)
self._update_main_lines(datas)
@abstractmethod
def add_lines(self, dummy_datas: List[np.ndarray]) -> UpdateLines:
def add_lines_stereo(
self, dummy_datas: List[np.ndarray], strides: List[int]
) -> UpdateLines:
...
@abstractmethod
@ -273,6 +298,102 @@ class BaseRenderer(ABC):
def add_labels(self, labels: List[str]) -> Any:
...
@abstractmethod
def add_xy_line_mono(
self, wave_idx: int, xs: Sequence[float], ys: Sequence[float], stride: int
) -> CustomLine:
...
class RendererFrontend(_RendererBackend, ABC):
def __init__(self, *args, **kwargs):
_RendererBackend.__init__(self, *args, **kwargs)
self._update_main_lines = None
self._custom_lines = {}
self._offsetable = defaultdict(list)
_update_main_lines: Optional[UpdateLines]
def update_main_lines(self, datas: List[np.ndarray]) -> None:
if self._update_main_lines is None:
self._update_main_lines = self.add_lines_stereo(datas, self.render_strides)
self._update_main_lines(datas)
_custom_lines: Dict[Any, CustomLine]
_offsetable: DefaultDict[int, MutableSequence[CustomLine]]
def update_custom_line(
self,
name: str,
wave_idx: int,
stride: int,
data: np.ndarray,
*,
offset: bool = True,
):
data = data.copy()
key = (name, wave_idx)
if key not in self._custom_lines:
line = self._add_line_mono(wave_idx, stride, data)
self._custom_lines[key] = line
if offset:
self._offsetable[wave_idx].append(line)
else:
line = self._custom_lines[key]
line.set_ydata(data)
def update_vline(
self, name: str, wave_idx: int, stride: int, x: int, *, offset: bool = True
):
key = (name, wave_idx)
if key not in self._custom_lines:
line = self._add_vline_mono(wave_idx, stride)
self._custom_lines[key] = line
if offset:
self._offsetable[wave_idx].append(line)
else:
line = self._custom_lines[key]
line.xdata = [x * stride] * 2
self._custom_lines[key].set_xdata(line.xdata)
def offset_viewport(self, wave_idx: int, viewport_offset: float):
line_offset = -viewport_offset
for line in self._offsetable[wave_idx]:
line.set_xdata(line.xdata + line_offset * line.stride)
def _add_line_mono(
self, wave_idx: int, stride: int, dummy_data: np.ndarray
) -> CustomLine:
ys = np.zeros_like(dummy_data)
xs = calc_xs(len(ys), stride)
return self.add_xy_line_mono(wave_idx, xs, ys, stride)
def _add_vline_mono(self, wave_idx: int, stride: int) -> CustomLine:
return self.add_xy_line_mono(wave_idx, [0, 0], [-1, 1], stride)
# See Wave.get_around() and designNotes.md.
# Viewport functions
def calc_limits(N: int, viewport_stride: float) -> Tuple[float, float]:
halfN = N // 2
max_x = N - 1
return np.array([-halfN, -halfN + max_x]) * viewport_stride
def calc_center(viewport_stride: float) -> float:
return -0.5 * viewport_stride
# Line functions
def calc_xs(N: int, stride: int) -> Sequence[float]:
halfN = N // 2
return (np.arange(N) - halfN) * stride
Point = float
Pixel = float
@ -290,7 +411,7 @@ def px_from_points(pt: Point) -> Pixel:
return pt * PIXELS_PER_PT
class AbstractMatplotlibRenderer(BaseRenderer, ABC):
class AbstractMatplotlibRenderer(RendererFrontend, ABC):
"""Matplotlib renderer which can use any backend (agg, mplcairo).
To pick a backend, subclass and set canvas_type at the class level.
"""
@ -303,7 +424,7 @@ class AbstractMatplotlibRenderer(BaseRenderer, ABC):
pass
def __init__(self, *args, **kwargs):
BaseRenderer.__init__(self, *args, **kwargs)
RendererFrontend.__init__(self, *args, **kwargs)
dict.__setitem__(
matplotlib.rcParams, "lines.antialiased", self.cfg.antialiasing
@ -385,17 +506,28 @@ class AbstractMatplotlibRenderer(BaseRenderer, ABC):
# Returns 2D list of [self.nplots][1]Axes.
axes_mono_2d = self.layout_mono.arrange(self._axes_factory, label="mono")
for axes_list in axes_mono_2d:
assert len(axes_list) == 1
self._axes_mono.extend(axes_list)
(axes,) = axes_list # type: Axes
# List of colors at
# https://matplotlib.org/gallery/color/colormap_reference.html
# Discussion at https://github.com/matplotlib/matplotlib/issues/10840
cmap: ListedColormap = get_cmap("Accent")
colors = cmap.colors
axes.set_prop_cycle(color=colors)
self._axes_mono.append(axes)
# Setup axes
for idx, N in enumerate(self.wave_nsamps):
wave_axes = self._axes2d[idx]
max_x = N - 1
viewport_stride = self.render_strides[idx] * cfg.viewport_width
ylim = cfg.viewport_height
def scale_axes(ax: "Axes"):
ax.set_xlim(0, max_x)
ax.set_ylim(-1, 1)
xlim = calc_limits(N, viewport_stride)
ax.set_xlim(*xlim)
ax.set_ylim(-ylim, ylim)
scale_axes(self._axes_mono[idx])
for ax in unique_by_id(wave_axes):
@ -408,9 +540,7 @@ class AbstractMatplotlibRenderer(BaseRenderer, ABC):
# Not quite sure if midlines or gridlines draw on top
kw = dict(color=midline_color, linewidth=midline_width)
if cfg.v_midline:
# See Wave.get_around() docstring.
# wave_data[N//2] == self[sample], usually > 0.
ax.axvline(x=N // 2 - 0.5, **kw)
ax.axvline(x=calc_center(viewport_stride), **kw)
if cfg.h_midline:
ax.axhline(y=0, **kw)
@ -490,7 +620,9 @@ class AbstractMatplotlibRenderer(BaseRenderer, ABC):
return ax
# Public API
def add_lines(self, dummy_datas: List[np.ndarray]) -> UpdateLines:
def add_lines_stereo(
self, dummy_datas: List[np.ndarray], strides: List[int]
) -> UpdateLines:
cfg = self.cfg
# Plot lines over background
@ -504,22 +636,26 @@ class AbstractMatplotlibRenderer(BaseRenderer, ABC):
wave_axes = self._axes2d[wave_idx]
wave_lines = []
xs = calc_xs(len(wave_zeros), strides[wave_idx])
# Foreach chan
for chan_idx, chan_zeros in enumerate(wave_zeros.T):
ax = wave_axes[chan_idx]
line_color = self._line_params[wave_idx].color
chan_line: Line2D = ax.plot(
chan_zeros, color=line_color, linewidth=line_width
xs, chan_zeros, color=line_color, linewidth=line_width
)[0]
wave_lines.append(chan_line)
lines2d.append(wave_lines)
self._artists.extend(wave_lines)
return lambda datas: self._update_lines(lines2d, datas)
return lambda datas: self._update_lines_stereo(lines2d, datas)
@staticmethod
def _update_lines(lines2d: "List[List[Line2D]]", datas: List[np.ndarray]) -> None:
def _update_lines_stereo(
lines2d: "List[List[Line2D]]", datas: List[np.ndarray]
) -> None:
"""
Preconditions:
- lines2d[wave][chan] = Line2D
@ -542,6 +678,22 @@ class AbstractMatplotlibRenderer(BaseRenderer, ABC):
chan_line = wave_lines[chan_idx]
chan_line.set_ydata(chan_data)
def add_xy_line_mono(
self, wave_idx: int, xs: Sequence[float], ys: Sequence[float], stride: int
) -> CustomLine:
cfg = self.cfg
# Plot lines over background
line_width = cfg.line_width
ax = self._axes_mono[wave_idx]
mono_line: Line2D = ax.plot(xs, ys, linewidth=line_width)[0]
self._artists.append(mono_line)
# noinspection PyTypeChecker
return CustomLine(stride, xs, mono_line.set_xdata, mono_line.set_ydata)
# Channel labels
def add_labels(self, labels: List[str]) -> List["Text"]:
"""

Wyświetl plik

@ -23,8 +23,8 @@ if TYPE_CHECKING:
class _TriggerConfig:
cls: ClassVar[Type["_Trigger"]]
def __call__(self, wave: "Wave", tsamp: int, stride: int, fps: float) -> "_Trigger":
return self.cls(wave, cfg=self, tsamp=tsamp, stride=stride, fps=fps)
def __call__(self, wave: "Wave", *args, **kwargs) -> "_Trigger":
return self.cls(wave, self, *args, **kwargs)
class MainTriggerConfig(
@ -71,7 +71,14 @@ def register_trigger(config_t: Type[_TriggerConfig]):
class _Trigger(ABC):
def __init__(
self, wave: "Wave", cfg: _TriggerConfig, tsamp: int, stride: int, fps: float
self,
wave: "Wave",
cfg: _TriggerConfig,
tsamp: int,
stride: int,
fps: float,
renderer: Optional["RendererFrontend"] = None,
wave_idx: int = 0,
):
self.cfg = cfg
self._wave = wave
@ -81,6 +88,10 @@ class _Trigger(ABC):
self._stride = stride
self._fps = fps
# Only used for debug plots
self._renderer = renderer
self._wave_idx = wave_idx
frame_dur = 1 / fps
# Subsamples per frame
self._tsamp_frame = self.time2tsamp(frame_dur)
@ -88,6 +99,23 @@ class _Trigger(ABC):
def time2tsamp(self, time: float) -> int:
return round(time * self._wave.smp_s / self._stride)
def custom_line(self, name: str, data: np.ndarray, **kwargs):
if self._renderer is None:
return
self._renderer.update_custom_line(
name, self._wave_idx, self._stride, data, **kwargs
)
def custom_vline(self, name: str, x: int):
if self._renderer is None:
return
self._renderer.update_vline(name, self._wave_idx, self._stride, x)
def offset_viewport(self, offset: int):
if self._renderer is None:
return
self._renderer.offset_viewport(self._wave_idx, offset)
@abstractmethod
def get_trigger(self, index: int, cache: "PerFrameCache") -> int:
"""
@ -115,10 +143,22 @@ class MainTrigger(_Trigger, ABC):
if cfg.post_trigger:
# Create a post-processing trigger, with narrow nsamp and stride=1.
# This improves speed and precision.
self.post = cfg.post_trigger(self._wave, cfg.post_radius, 1, self._fps)
self.post = cfg.post_trigger(
self._wave,
cfg.post_radius,
1,
self._fps,
self._renderer,
self._wave_idx,
)
else:
self.post = None
def set_renderer(self, renderer: "RendererFrontend"):
self._renderer = renderer
if self.post:
self.post._renderer = renderer
def do_not_inherit__Trigger_directly(self):
pass

Wyświetl plik

@ -115,7 +115,7 @@ def test_config_channel_integration(
assert channel._render_samp == ideal_rsamp
assert channel._trigger_stride == tsub * c_trigger_width
assert channel._render_stride == rsub * c_render_width
assert channel.render_stride == rsub * c_render_width
# Ensure amplification override works
args, kwargs = Wave.call_args
@ -134,7 +134,7 @@ def test_config_channel_integration(
# Only Channel.get_render_around() (not NullTrigger) calls wave.get_around().
(_sample, _return_nsamp, _subsampling), kwargs = wave.get_around.call_args
assert _return_nsamp == channel._render_samp
assert _subsampling == channel._render_stride
assert _subsampling == channel.render_stride
# Inspect arguments to renderer.update_main_lines()
# datas: List[np.ndarray]

Wyświetl plik

@ -222,7 +222,7 @@ def test_renderer_layout():
nplots = 15
datas = [RENDER_Y_ZEROS] * nplots
r = Renderer(cfg, lcfg, datas, None)
r = Renderer(cfg, lcfg, datas, None, None)
r.update_main_lines(datas)
layout = r.layout

Wyświetl plik

@ -88,7 +88,7 @@ def test_render_output():
""" Ensure rendering to output does not raise exceptions. """
datas = [RENDER_Y_ZEROS]
renderer = Renderer(CFG.render, CFG.layout, datas, channel_cfgs=None)
renderer = Renderer(CFG.render, CFG.layout, datas, None, None)
out: FFmpegOutput = NULL_FFMPEG_OUTPUT(CFG)
renderer.update_main_lines(datas)

Wyświetl plik

@ -6,11 +6,19 @@ import numpy as np
import pytest
from hypothesis import given
from corrscope.channel import ChannelConfig
from corrscope.channel import ChannelConfig, Channel
from corrscope.corrscope import CorrScope, default_config, Arguments
from corrscope.layout import LayoutConfig
from corrscope.outputs import FFplayOutputConfig
from corrscope.renderer import RendererConfig, Renderer, LabelPosition, Font
from corrscope.renderer import (
RendererConfig,
Renderer,
LabelPosition,
Font,
calc_limits,
calc_xs,
calc_center,
)
from corrscope.util import perr
from corrscope.wave import Flatten
@ -51,6 +59,7 @@ def appearance_to_str(val) -> Optional[str]:
# "str" = HTML #FFFFFF color string.
@attr.dataclass(frozen=True)
class BG:
color: str
@ -60,6 +69,7 @@ class BG:
class FG:
color: str
draw_fg: bool = True
line_width: float = 2.0
@attr.dataclass(frozen=True)
@ -68,12 +78,18 @@ class Grid:
color: Optional[str]
@attr.dataclass(frozen=True)
class Debug:
viewport_width: float = 1
bg_black = BG("#000000")
bg_white = BG("#ffffff")
bg_blue = BG("#0000aa")
bg_yellow = BG("#aaaa00")
fg_white = FG("#ffffff")
fg_white_thick = FG("#ffffff", line_width=10.0)
fg_black = FG("#000000")
fg_NONE = FG("#ffffff", draw_fg=False)
fg_yellow = FG("#aaaa00")
@ -86,11 +102,16 @@ grid_10 = Grid(10, "#ff00ff")
grid_NONE = Grid(10, None)
debug_NONE = Debug()
debug_wide = Debug(viewport_width=2)
@attr.dataclass(frozen=True)
class Appearance:
bg: BG
fg: FG
grid: Grid
debug: Debug = debug_NONE
all_colors = pytest.mark.parametrize(
@ -104,6 +125,8 @@ all_colors = pytest.mark.parametrize(
(Appearance(bg_white, fg_black, grid_NONE), RENDER_Y_ZEROS),
(Appearance(bg_blue, fg_yellow, grid_NONE), RENDER_Y_ZEROS),
(Appearance(bg_yellow, fg_blue, grid_NONE), RENDER_Y_ZEROS),
# Test FG line thickness
(Appearance(bg_black, fg_white_thick, grid_NONE), RENDER_Y_ZEROS),
# Test various grid thicknesses
(Appearance(bg_white, fg_black, grid_0), RENDER_Y_ZEROS),
(Appearance(bg_blue, fg_yellow, grid_1), RENDER_Y_ZEROS),
@ -112,6 +135,8 @@ all_colors = pytest.mark.parametrize(
(Appearance(bg_black, fg_white, grid_NONE), RENDER_Y_ZEROS),
(Appearance(bg_blue, fg_yellow, grid_0), RENDER_Y_STEREO),
(Appearance(bg_blue, fg_yellow, grid_10), RENDER_Y_STEREO),
# Test debugging (viewport width)
(Appearance(bg_black, fg_white, grid_NONE, debug_wide), RENDER_Y_ZEROS),
],
ids=appearance_to_str,
)
@ -121,12 +146,16 @@ def get_renderer_config(appear: Appearance) -> RendererConfig:
cfg = RendererConfig(
WIDTH,
HEIGHT,
# BG
bg_color=appear.bg.color,
# FG
init_line_color=appear.fg.color,
line_width=appear.fg.line_width,
viewport_width=appear.debug.viewport_width,
# Grid
grid_color=appear.grid.color,
grid_line_width=appear.grid.line_width,
stereo_grid_opacity=OPACITY,
line_width=2.0,
antialiasing=False,
)
return cfg
@ -144,13 +173,13 @@ def test_default_colors(appear: Appearance, data):
lcfg = LayoutConfig(orientation=ORIENTATION)
datas = [data] * NPLOTS
r = Renderer(cfg, lcfg, datas, None)
r = Renderer(cfg, lcfg, datas, None, None)
verify(r, appear, datas)
# Ensure default ChannelConfig(line_color=None) does not override line color
chan = ChannelConfig(wav_path="")
channels = [chan] * NPLOTS
r = Renderer(cfg, lcfg, datas, channels)
r = Renderer(cfg, lcfg, datas, channels, None)
verify(r, appear, datas)
@ -167,7 +196,7 @@ def test_line_colors(appear: Appearance, data):
cfg.init_line_color = "#888888"
chan.line_color = appear.fg.color
r = Renderer(cfg, lcfg, datas, channels)
r = Renderer(cfg, lcfg, datas, channels, None)
verify(r, appear, datas)
@ -179,10 +208,13 @@ def verify(r: Renderer, appear: Appearance, datas: List[Optional[np.ndarray]]):
fg_str = appear.fg.color
draw_fg = appear.fg.draw_fg
fg_line_width = appear.fg.line_width
grid_str = appear.grid.color
grid_line_width = appear.grid.line_width
viewport_width = appear.debug.viewport_width
if draw_fg:
r.update_main_lines(datas)
@ -216,14 +248,20 @@ def verify(r: Renderer, appear: Appearance, datas: List[Optional[np.ndarray]]):
bg_frame = frame_colors[0]
assert (
bg_frame == bg_u8
).all(), f"incorrect background, it might be grid_str={grid.color}"
).all(), f"incorrect background, it might be grid_str={grid_str}"
# Ensure foreground is present
does_fg_appear = np.prod(frame_colors == fg_u8, axis=-1).any()
does_fg_appear_here = np.prod(frame_colors == fg_u8, axis=-1)
does_fg_appear = does_fg_appear_here.any()
# it might be 136 == #888888 == init_line_color
assert does_fg_appear == draw_fg, f"{does_fg_appear} != {draw_fg}"
if draw_fg:
expected_fg_pixels = NPLOTS * (WIDTH / viewport_width) * fg_line_width
assert does_fg_appear_here.sum() == pytest.approx(
expected_fg_pixels, abs=expected_fg_pixels * 0.1
)
# Ensure grid color is present
does_grid_appear_here = np.prod(frame_colors == grid_u8, axis=-1)
does_grid_appear = does_grid_appear_here.any()
@ -279,7 +317,7 @@ def test_label_render(label_position: LabelPosition, data, hide_lines):
labels = ["#"] * nplots
datas = [data] * nplots
r = Renderer(cfg, lcfg, datas, None)
r = Renderer(cfg, lcfg, datas, None, None)
r.add_labels(labels)
if not hide_lines:
r.update_main_lines(datas)
@ -326,6 +364,7 @@ def test_stereo_render_integration(mocker: "pytest_mock.MockFixture"):
corr.play()
# Image dimension tests
@pytest.mark.parametrize(
"target_int, res_divisor", [(50, 2.0), (51, 2.0), (100, 1.001)]
)
@ -359,10 +398,63 @@ def verify_res_divisor_rounding(
datas = [RENDER_Y_ZEROS]
try:
renderer = Renderer(cfg, LayoutConfig(), datas, channel_cfgs=None)
renderer = Renderer(cfg, LayoutConfig(), datas, None, None)
if not speed_hack:
renderer.update_main_lines(datas)
renderer.get_frame()
except Exception:
perr(cfg.divided_width)
raise
# X-axis stride tests
@given(N=hs.integers(2, 1000), stride=hs.integers(1, 1000))
def test_calc_limits_xs(N: int, stride: int):
"""Sanity check to ensure that data is drawn from 1 edge of screen to another,
that calc_limits() and calc_xs() match.
Test that calc_center() == "to the left of N//2".
"""
min, max = calc_limits(N, stride)
xs = calc_xs(N, stride)
assert xs[0] == min
assert xs[-1] == max
center = calc_center(stride)
assert np.mean([xs[N // 2 - 1], xs[N // 2]]) == pytest.approx(center)
# Debug plotting tests
@parametrize("integration", [False, True])
def test_renderer_knows_stride(mocker: "pytest_mock.MockFixture", integration: bool):
"""
If Renderer draws both "main line" and "custom mono lines" at once,
each line must have its x-coordinates multiplied by the stride.
Renderer uses "main line stride = 1" by default,
but this results in the main line appearing too narrow compared to debug lines.
Make sure CorrScope.play() gives Renderer the correct values.
"""
# Stub out FFplay output.
mocker.patch.object(FFplayOutputConfig, "cls")
subsampling = 2
width_mul = 3
chan_cfg = ChannelConfig("tests/sine440.wav", render_width=width_mul)
corr_cfg = default_config(
render_subsampling=subsampling, channels=[chan_cfg], end_time=0
)
if integration:
corr = CorrScope(corr_cfg, Arguments(".", [FFplayOutputConfig()]))
corr.play()
assert corr.renderer.render_strides == [subsampling * width_mul]
else:
channel = Channel(chan_cfg, corr_cfg, channel_idx=0)
data = channel.get_render_around(0)
renderer = Renderer(
corr_cfg.render, corr_cfg.layout, [data], [chan_cfg], [channel]
)
assert renderer.render_strides == [subsampling * width_mul]