From 0407e488897bf5b14d2860bc3a4b291b9b606c29 Mon Sep 17 00:00:00 2001 From: Jeremy Lau <30300826+fdxmw@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:09:16 -0700 Subject: [PATCH 1/2] Minor WaveDrom rendering fixes and improvements: * Rename `trace_to_html` to `trace_to_json`, and generalize the function so it just returns WaveJSON, which can be used by many different tools. Previously, `trace_to_html` returned HTML-embeddable WaveJSON, which is only useful for building a webpage. * Improve documentation for `trace_to_json` and add a doctest example. * Update `render_trace` to pass `repr_func` and `repr_per_name` to `trace_to_json`, to support custom data formatting in Jupyter notebooks. Many examples rely on these features. * Display cycle numbers above traces for consistency with the console renderer. * Fix `example8`'s deprecated call to `render_trace` (pass wire names instead of `WireVector`s). * Use `json.dumps` in `trace_to_json` instead of doing JSON string formatting ourselves. * Move `val_to_str` to module-level, so it can be called from `trace_to_json`. Simplify `val_to_str`'s implementation. * Remove `renderer-demo` from `ipynb-examples`. The different rendering options only work for the console renderer. --- .gitignore | 5 +- docs/export.rst | 2 +- examples/Makefile | 2 +- examples/example8-verilog.py | 4 +- ipynb-examples/example8-verilog.ipynb | 4 +- ipynb-examples/renderer-demo.ipynb | 173 -------------------------- pyrtl/__init__.py | 4 +- pyrtl/simulation.py | 86 +++++++------ pyrtl/visualization.py | 127 ++++++++++--------- tests/test_simulation.py | 3 +- tests/test_visualization.py | 98 +++++++-------- 11 files changed, 166 insertions(+), 342 deletions(-) delete mode 100644 ipynb-examples/renderer-demo.ipynb diff --git a/.gitignore b/.gitignore index 97033368..9652cf77 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ __pycache__ # Unit test / coverage reports .coverage -.tox # Translations *.mo @@ -46,9 +45,6 @@ docs/_build # There must be a very good reason for someone to add a verilog file to the repo .v -# ipynb -ipynb-examples/.ipynb_checkpoints - # VS Code .vscode @@ -56,5 +52,6 @@ ipynb-examples/.ipynb_checkpoints pyvenv.cfg # Jupyter +.ipynb_checkpoints .jupyterlite.doit.db _output diff --git a/docs/export.rst b/docs/export.rst index 99ffb22e..35fa67d7 100644 --- a/docs/export.rst +++ b/docs/export.rst @@ -28,5 +28,5 @@ Outputting for Visualization .. autofunction:: pyrtl.output_to_svg .. autofunction:: pyrtl.block_to_graphviz_string .. autofunction:: pyrtl.block_to_svg -.. autofunction:: pyrtl.trace_to_html +.. autofunction:: pyrtl.trace_to_json .. autofunction:: pyrtl.net_graph diff --git a/examples/Makefile b/examples/Makefile index 71b0c703..ae614623 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,5 +1,5 @@ PYTHON=uv run -PY_FILES=$(wildcard *.py) +PY_FILES=$(wildcard example*.py) introduction-to-hardware.py IPYNB_FILES=$(addprefix ../ipynb-examples/, $(PY_FILES:.py=.ipynb)) all: $(IPYNB_FILES) diff --git a/examples/example8-verilog.py b/examples/example8-verilog.py index 1aca987d..d5838a78 100644 --- a/examples/example8-verilog.py +++ b/examples/example8-verilog.py @@ -67,9 +67,7 @@ {"x": random.randrange(2), "y": random.randrange(2), "cin": random.randrange(2)} ) # Only display the `Input` and `Output` `WireVectors` for clarity. -input_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Input) -output_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Output) -sim.tracer.render_trace(trace_list=[*input_vectors, *output_vectors], symbol_len=2) +sim.tracer.render_trace(trace_list=["x", "y", "cin", "sum", "cout"], symbol_len=2) # ## Exporting to Verilog # diff --git a/ipynb-examples/example8-verilog.ipynb b/ipynb-examples/example8-verilog.ipynb index 5745a90e..3e566613 100644 --- a/ipynb-examples/example8-verilog.ipynb +++ b/ipynb-examples/example8-verilog.ipynb @@ -149,9 +149,7 @@ }, "outputs": [], "source": [ - "input_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Input)\n", - "output_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Output)\n", - "sim.tracer.render_trace(trace_list=[*input_vectors, *output_vectors], symbol_len=2)\n" + "sim.tracer.render_trace(trace_list=[\"x\", \"y\", \"cin\", \"sum\", \"cout\"], symbol_len=2)\n" ] }, { diff --git a/ipynb-examples/renderer-demo.ipynb b/ipynb-examples/renderer-demo.ipynb deleted file mode 100644 index aeae46b6..00000000 --- a/ipynb-examples/renderer-demo.ipynb +++ /dev/null @@ -1,173 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " # Render traces with various `WaveRenderer` options.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " Run this demo to see which options work well in your terminal.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "%pip install pyrtl\n", - "\n", - "import pyrtl\n", - "\n", - "\n", - "pyrtl.reset_working_block()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " Make some clock dividers and counters.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def make_clock(period: int):\n", - " \"\"\"Make a clock signal that inverts every `period` cycles.\"\"\"\n", - " assert period > 0\n", - "\n", - " # Build a chain of registers.\n", - " first_reg = pyrtl.Register(bitwidth=1, name=f\"clock_0_{period}\", reset_value=1)\n", - " last_reg = first_reg\n", - " for offset in range(1, period):\n", - " reg = pyrtl.Register(bitwidth=1, name=f\"clock_{offset}_{period}\")\n", - " reg.next <<= last_reg\n", - " last_reg = reg\n", - "\n", - " # The first register's input is the inverse of the last register's output.\n", - " first_reg.next <<= ~last_reg\n", - " return last_reg\n", - "\n", - "\n", - "def make_counter(period: int, bitwidth: int = 2):\n", - " \"\"\"Make a counter that increments every `period` cycles.\"\"\"\n", - " assert period > 0\n", - "\n", - " # Build a chain of registers.\n", - " first_reg = pyrtl.Register(bitwidth=bitwidth, name=f\"counter_0_{period}\")\n", - " last_reg = first_reg\n", - " for offset in range(1, period):\n", - " reg = pyrtl.Register(bitwidth=bitwidth, name=f\"counter_{offset}_{period}\")\n", - " reg.next <<= last_reg\n", - " last_reg = reg\n", - "\n", - " # The first register's input is the last register's output plus 1.\n", - " first_reg.next <<= last_reg + pyrtl.Const(1)\n", - " return last_reg\n", - "\n", - "\n", - "make_clock(period=1)\n", - "make_clock(period=2)\n", - "make_counter(period=1)\n", - "make_counter(period=2)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " Simulate 20 cycles.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "sim = pyrtl.Simulation()\n", - "sim.step_multiple(nsteps=20)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " Render the trace with a variety of rendering options.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "renderers = {\n", - " \"powerline\": (\n", - " pyrtl.simulation.PowerlineRendererConstants(),\n", - " \"Requires a font with powerline glyphs\",\n", - " ),\n", - " \"utf-8\": (\n", - " pyrtl.simulation.Utf8RendererConstants(),\n", - " \"Unicode, default non-Windows renderer\",\n", - " ),\n", - " \"utf-8-alt\": (\n", - " pyrtl.simulation.Utf8AltRendererConstants(),\n", - " \"Unicode, alternate display option\",\n", - " ),\n", - " \"cp437\": (\n", - " pyrtl.simulation.Cp437RendererConstants(),\n", - " \"Code page 437 (8-bit ASCII), default Windows renderer\",\n", - " ),\n", - " \"ascii\": (pyrtl.simulation.AsciiRendererConstants(), \"Basic 7-bit ASCII renderer\"),\n", - "}\n", - "\n", - "for name, (constants, notes) in renderers.items():\n", - " print(f\"# {notes}\")\n", - " print(f\"export PYRTL_RENDERER={name}\\n\")\n", - " sim.tracer.render_trace(\n", - " renderer=pyrtl.simulation.WaveRenderer(constants), repr_func=int\n", - " )\n", - " print()\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} \ No newline at end of file diff --git a/pyrtl/__init__.py b/pyrtl/__init__.py index ecb6a2b2..923d95ad 100644 --- a/pyrtl/__init__.py +++ b/pyrtl/__init__.py @@ -95,7 +95,7 @@ output_to_svg, block_to_graphviz_string, block_to_svg, - trace_to_html, + trace_to_json, net_graph, ) @@ -236,7 +236,7 @@ "output_to_svg", "block_to_graphviz_string", "block_to_svg", - "trace_to_html", + "trace_to_json", "net_graph", # importexport "input_from_verilog", diff --git a/pyrtl/simulation.py b/pyrtl/simulation.py index 36687c38..65749d80 100644 --- a/pyrtl/simulation.py +++ b/pyrtl/simulation.py @@ -1030,6 +1030,37 @@ def make_split(source, split_length, split_start_bit, split_res_start_bit): # +def val_to_str( + value: int, + wire: WireVector, + repr_func: Callable[[int], str], + repr_per_name: dict[str, Callable[[int], str]], +) -> str: + """Return a string representing 'value'. + + :param value: The value to convert to string. + :param wire: Wire that produced this value. + :param repr_func: function to use for representing the current_val; examples are + 'hex', 'oct', 'bin', 'str' (for decimal), or the function returned by + :func:`enum_name`. Defaults to 'hex'. + :param repr_per_name: Map from signal name to a function that takes in the signal's + value and returns a user-defined representation. If a signal name is not found + in the map, the argument `repr_func` will be used instead. + + :return: a string representing 'value'. + """ + func = repr_per_name.get(wire.name) + if func is None: + if isinstance(wire, Register) and wire.State is not None: + func = enum_name(wire.State) + else: + func = repr_func + + if func is val_to_signed_integer: + return str(val_to_signed_integer(value=value, bitwidth=wire.bitwidth)) + return str(func(value)) + + class WaveRenderer: """Render a SimulationTrace to the terminal. @@ -1077,39 +1108,6 @@ def render_ruler_segment( # Pad major_tick out to segment_size. return major_tick.ljust(cycle_len * segment_size) - def val_to_str( - self, - value: int, - wire: WireVector, - repr_func: Callable[[int], str], - repr_per_name: dict[str, Callable[[int], str]], - ) -> str: - """Return a string representing 'value'. - - :param value: The value to convert to string. - :param wire: Wire that produced this value. - :param repr_func: function to use for representing the current_val; examples are - 'hex', 'oct', 'bin', 'str' (for decimal), or the function returned by - :func:`enum_name`. Defaults to 'hex'. - :param repr_per_name: Map from signal name to a function that takes in the - signal's value and returns a user-defined representation. If a signal name - is not found in the map, the argument `repr_func` will be used instead. - - :return: a string representing 'value'. - """ - f = repr_per_name.get(wire.name) - - def invoke_f(f, value): - if f is val_to_signed_integer: - return str(val_to_signed_integer(value=value, bitwidth=wire.bitwidth)) - return str(f(value)) - - if f is not None: - return invoke_f(f, value) - if isinstance(wire, Register) and wire.State is not None: - return invoke_f(enum_name(wire.State), value) - return invoke_f(repr_func, value) - def render_val( self, w: WireVector, @@ -1181,7 +1179,7 @@ def render_val( out += self.constants._bus_start # Display the current non-zero value. out += ( - self.val_to_str(current_val, w, repr_func, repr_per_name) + val_to_str(current_val, w, repr_func, repr_per_name) .rstrip("L") .ljust(symbol_len)[:symbol_len] ) @@ -1768,15 +1766,23 @@ def render_trace( display, ) - from pyrtl.visualization import trace_to_html + from pyrtl.visualization import trace_to_json display( HTML( - trace_to_html(self, trace_list=trace_list, sortkey=_trace_sort_key) + '\n" ), HTML(""" - - + + """), # Wait for WaveDrom to load, polling every 100ms. Javascript(""" @@ -1881,9 +1887,7 @@ def formatted_trace_line(wire, trace): trace = self.trace[trace_name] current_symbol_len = max( len( - renderer.val_to_str( - v, self._wires[trace_name], repr_func, repr_per_name - ) + val_to_str(v, self._wires[trace_name], repr_func, repr_per_name) ) for v in trace ) diff --git a/pyrtl/visualization.py b/pyrtl/visualization.py index 4286cac9..ff60279c 100644 --- a/pyrtl/visualization.py +++ b/pyrtl/visualization.py @@ -8,6 +8,7 @@ from __future__ import annotations import collections +import json from collections.abc import Callable from typing import TYPE_CHECKING @@ -560,17 +561,42 @@ def block_to_svg( # | | | | | |___ -def trace_to_html( +def trace_to_json( simtrace: SimulationTrace, trace_list: list[str] | None = None, sortkey=None, repr_func: Callable[[int], str] = hex, repr_per_name: dict[str, Callable[[int], str]] | None = None, ) -> str: - """Return a HTML block showing the trace. + """Return a `WaveJSON `_ + representation of a :class:`.SimulationTrace`. - :param simtrace: A trace to render in HTML. - :param trace_list: (optional) A list of wires to display. + The returned WaveJSON is compatible with `WaveDrom + `_; see the `WaveDrom README + `_ for various + ways to visualize WaveJSON. + + .. doctest only:: + + >>> import pyrtl + >>> pyrtl.reset_working_block() + + Example:: + + >>> counter = pyrtl.Register(name="counter", bitwidth=2) + >>> counter.next <<= counter + 1 + + >>> sim = pyrtl.Simulation() + >>> sim.step_multiple(nsteps=4) + + >>> print(pyrtl.trace_to_json(sim.tracer, repr_func=str)) + {"signal": [{"name": "counter", "wave": "====", "data": ["0", "1", "2", "3"]}], "config": {"hscale": 1}, "head": {"tick": 0}} + + Try pasting the WaveJSON output from the example above in the `WaveDrom editor + `_. + + :param simtrace: A trace to convert to WaveJSON. + :param trace_list: (optional) A list of wires to include in the WaveJSON. :param sortkey: (optional) The key with which to sort the ``trace_list``. :param repr_func: Function to use for representing each value in the trace. Examples include :func:`hex`, :func:`oct`, :func:`bin`, and :class:`str` (for decimal), @@ -580,16 +606,12 @@ def trace_to_html( value and returns a user-defined representation. If a signal name is not found in the map, the argument ``repr_func`` will be used instead. - :return: An HTML block showing the trace. - """ - - from pyrtl.simulation import SimulationTrace, _trace_sort_key + :return: WaveJSON representing ``simtrace``. + """ # noqa: E501 + from pyrtl.simulation import _trace_sort_key, val_to_str if repr_per_name is None: repr_per_name = {} - if not isinstance(simtrace, SimulationTrace): - msg = "first arguement must be of type SimulationTrace" - raise PyrtlError(msg) trace = simtrace.trace if sortkey is None: @@ -598,54 +620,41 @@ def trace_to_html( if trace_list is None: trace_list = sorted(trace, key=sortkey) - wave_template = """\ - -""" + signals = [] + wave_json = {"signal": signals, "config": {"hscale": 1}, "head": {"tick": 0}} + # Length of the longest value string, in characters. + max_value_length = 1 + for signal_name in trace_list: + # Make a WaveDrom WaveLane for ``signal_name``. + wave_list = [] + data_list = [] + last_value = None + + wire = simtrace._wires[signal_name] + for value in trace[signal_name]: + if last_value == value: + wave_list.append(".") + continue + + if len(wire) == 1: + # int() to convert True/False to 0/1. + wave_list.append(str(int(value))) + else: + wave_list.append("=") + data = val_to_str(value, wire, repr_func, repr_per_name) + data_list.append(data) + max_value_length = max(max_value_length, len(data)) - vallens = [] # For determining longest value length + last_value = value - def extract(w): - wavelist = [] - datalist = [] - last = None + signal = {"name": signal_name, "wave": "".join(wave_list)} + if len(data_list) > 0: + signal["data"] = data_list + signals.append(signal) - for value in trace[w]: - if last == value: - wavelist.append(".") - else: - f = repr_per_name.get(w) - if f is not None: - wavelist.append("=") - datalist.append(str(f(value))) - elif len(simtrace._wires[w]) == 1: - # int() to convert True/False to 0/1 - wavelist.append(str(int(value))) - else: - wavelist.append("=") - datalist.append(str(repr_func(value))) - - last = value - - wavestring = "".join(wavelist) - datastring = ", ".join([f'"{data}"' for data in datalist]) - if repr_per_name.get(w) is None and len(simtrace._wires[w]) == 1: - vallens.append(1) # all are the same length - return bool_signal_template % (w, wavestring) - vallens.extend([len(data) for data in datalist]) - return int_signal_template % (w, wavestring, datastring) - - bool_signal_template = ' { name: "%s", wave: "%s" },' - int_signal_template = ' { name: "%s", wave: "%s", data: [%s] },' - signals = [extract(w) for w in trace_list] - all_signals = "\n".join(signals) - maxvallen = max(vallens) - scale = (maxvallen // 5) + 1 - return wave_template % (all_signals, scale) - # print(wave) + # WaveDrom doesn't automatically scale cycle width to fit `data`, so we use `hscale` + # to manually adjust cycle width. `hscale: 1` fits about three characters of `data` + # in a cycle. `hscale` must be an integer. + wave_json["config"]["hscale"] = max_value_length // 4 + 1 + + return json.dumps(wave_json) diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 681f11ff..2205bc79 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -330,7 +330,7 @@ class Foo(enum.IntEnum): D = 3 i = pyrtl.Input(4, "i") - state = pyrtl.Register(max(Foo).bit_length(), name="state") + state = pyrtl.Register(name="state", State=Foo) o = pyrtl.Output(name="o") o <<= state @@ -350,7 +350,6 @@ class Foo(enum.IntEnum): sim.tracer.render_trace( file=buff, renderer=self.renderer, - repr_per_name={"state": pyrtl.enum_name(Foo)}, ) expected = ( " |0 |1 |2 |3 |4 \n" diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 2c3a9782..989aafb0 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -1,3 +1,4 @@ +import doctest import enum import io import random @@ -5,6 +6,16 @@ import pyrtl + +class TestDocTests(unittest.TestCase): + """Test documentation examples.""" + + def test_doctests(self): + failures, tests = doctest.testmod(m=pyrtl.visualization) + self.assertGreater(tests, 0) + self.assertEqual(failures, 0) + + graphviz_string_detailed = """\ digraph g { graph [splines="spline", outputorder="edgesfirst"]; @@ -348,51 +359,43 @@ def test_one_bit_adder_matches_expected(self): } ) - pyrtl.trace_to_html(sim.tracer) # tests if it compiles or not + pyrtl.trace_to_json(sim.tracer) # tests if it compiles or not - def test_trace_to_html(self): + def test_trace_to_json(self): i = pyrtl.Input(1, "i") o = pyrtl.Output(2, "o") o <<= i + 1 sim = pyrtl.Simulation() sim.step_multiple({"i": "0100110"}) - htmlstring = pyrtl.trace_to_html(sim.tracer) + htmlstring = pyrtl.trace_to_json(sim.tracer) expected = ( - '\n" + '{"signal": [' + '{"name": "i", "wave": "010.1.0"}, ' + '{"name": "o", "wave": "===.=.=", "data": ' + '["0x1", "0x2", "0x1", "0x2", "0x1"]}], ' + '"config": {"hscale": 1}, "head": {"tick": 0}}' ) self.assertEqual(htmlstring, expected) - def test_trace_to_html_repr_func(self): + def test_trace_to_json_repr_func(self): i = pyrtl.Input(1, "i") o = pyrtl.Output(2, "o") o <<= i + 1 sim = pyrtl.Simulation() sim.step_multiple({"i": "0100110"}) - htmlstring = pyrtl.trace_to_html(sim.tracer, repr_func=bin) + htmlstring = pyrtl.trace_to_json(sim.tracer, repr_func=bin) expected = ( - '\n" + '{"signal": [' + '{"name": "i", "wave": "010.1.0"}, ' + '{"name": "o", "wave": "===.=.=", "data": ' + '["0b1", "0b10", "0b1", "0b10", "0b1"]}], ' + '"config": {"hscale": 2}, "head": {"tick": 0}}' ) self.assertEqual(htmlstring, expected) - def test_trace_to_html_repr_per_name(self): + def test_trace_to_json_repr_per_name(self): class Foo(enum.IntEnum): A = 0 B = 1 @@ -400,7 +403,7 @@ class Foo(enum.IntEnum): D = 3 i = pyrtl.Input(4, "i") - state = pyrtl.Register(max(Foo).bit_length(), name="state") + state = pyrtl.Register(name="state", State=Foo) o = pyrtl.Output(name="o") o <<= state @@ -417,30 +420,26 @@ class Foo(enum.IntEnum): sim = pyrtl.Simulation() sim.step_multiple({"i": [1, 2, 4, 8, 0]}) - htmlstring = pyrtl.trace_to_html( - sim.tracer, repr_per_name={"state": pyrtl.enum_name(Foo)} - ) + htmlstring = pyrtl.trace_to_json(sim.tracer) expected = ( - '\n" + '{"signal": [' + '{"name": "i", "wave": "=====", "data": ' + '["0x1", "0x2", "0x4", "0x8", "0x0"]}, ' + '{"name": "o", "wave": "=.===", "data": ' + '["0x0", "0x1", "0x2", "0x3"]}, ' + '{"name": "state", "wave": "=.===", "data": ' + '["A", "B", "C", "D"]}], ' + '"config": {"hscale": 1}, "head": {"tick": 0}}' ) self.assertEqual(htmlstring, expected) - def test_trace_to_html_repr_per_name_enum_is_bool(self): + def test_trace_to_json_repr_per_name_enum_is_bool(self): class Foo(enum.IntEnum): A = 0 B = 1 i = pyrtl.Input(2, "i") - state = pyrtl.Register(max(Foo).bit_length(), name="state") + state = pyrtl.Register(name="state", State=Foo) o = pyrtl.Output(name="o") o <<= state @@ -453,20 +452,13 @@ class Foo(enum.IntEnum): sim = pyrtl.Simulation() sim.step_multiple({"i": [1, 2, 1, 2, 2]}) - htmlstring = pyrtl.trace_to_html( - sim.tracer, repr_per_name={"state": pyrtl.enum_name(Foo)} - ) + htmlstring = pyrtl.trace_to_json(sim.tracer) expected = ( - '\n" + '{"signal": [' + '{"name": "i", "wave": "====.", "data": ["0x1", "0x2", "0x1", "0x2"]}, ' + '{"name": "o", "wave": "0.101"}, ' + '{"name": "state", "wave": "0.101"}], ' + '"config": {"hscale": 1}, "head": {"tick": 0}}' ) self.assertEqual(htmlstring, expected) From 99a702a5b7ff71bc265c211759a5c4ab3f82b9fa Mon Sep 17 00:00:00 2001 From: Jeremy Lau <30300826+fdxmw@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:00:37 -0700 Subject: [PATCH 2/2] Minor test reformatting, move a variable closer to its first use. --- pyrtl/visualization.py | 4 ++-- tests/test_visualization.py | 31 +++++++++++++------------------ 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/pyrtl/visualization.py b/pyrtl/visualization.py index ff60279c..991edfe9 100644 --- a/pyrtl/visualization.py +++ b/pyrtl/visualization.py @@ -621,7 +621,6 @@ def trace_to_json( trace_list = sorted(trace, key=sortkey) signals = [] - wave_json = {"signal": signals, "config": {"hscale": 1}, "head": {"tick": 0}} # Length of the longest value string, in characters. max_value_length = 1 for signal_name in trace_list: @@ -655,6 +654,7 @@ def trace_to_json( # WaveDrom doesn't automatically scale cycle width to fit `data`, so we use `hscale` # to manually adjust cycle width. `hscale: 1` fits about three characters of `data` # in a cycle. `hscale` must be an integer. - wave_json["config"]["hscale"] = max_value_length // 4 + 1 + hscale = max_value_length // 4 + 1 + wave_json = {"signal": signals, "config": {"hscale": hscale}, "head": {"tick": 0}} return json.dumps(wave_json) diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 989aafb0..c67a18c3 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -368,15 +368,14 @@ def test_trace_to_json(self): sim = pyrtl.Simulation() sim.step_multiple({"i": "0100110"}) - htmlstring = pyrtl.trace_to_json(sim.tracer) + json_string = pyrtl.trace_to_json(sim.tracer) expected = ( '{"signal": [' '{"name": "i", "wave": "010.1.0"}, ' - '{"name": "o", "wave": "===.=.=", "data": ' - '["0x1", "0x2", "0x1", "0x2", "0x1"]}], ' + '{"name": "o", "wave": "===.=.=", "data": ["0x1", "0x2", "0x1", "0x2", "0x1"]}], ' # noqa: E501 '"config": {"hscale": 1}, "head": {"tick": 0}}' ) - self.assertEqual(htmlstring, expected) + self.assertEqual(json_string, expected) def test_trace_to_json_repr_func(self): i = pyrtl.Input(1, "i") @@ -385,15 +384,14 @@ def test_trace_to_json_repr_func(self): sim = pyrtl.Simulation() sim.step_multiple({"i": "0100110"}) - htmlstring = pyrtl.trace_to_json(sim.tracer, repr_func=bin) + json_string = pyrtl.trace_to_json(sim.tracer, repr_func=bin) expected = ( '{"signal": [' '{"name": "i", "wave": "010.1.0"}, ' - '{"name": "o", "wave": "===.=.=", "data": ' - '["0b1", "0b10", "0b1", "0b10", "0b1"]}], ' + '{"name": "o", "wave": "===.=.=", "data": ["0b1", "0b10", "0b1", "0b10", "0b1"]}], ' # noqa: E501 '"config": {"hscale": 2}, "head": {"tick": 0}}' ) - self.assertEqual(htmlstring, expected) + self.assertEqual(json_string, expected) def test_trace_to_json_repr_per_name(self): class Foo(enum.IntEnum): @@ -420,18 +418,15 @@ class Foo(enum.IntEnum): sim = pyrtl.Simulation() sim.step_multiple({"i": [1, 2, 4, 8, 0]}) - htmlstring = pyrtl.trace_to_json(sim.tracer) + json_string = pyrtl.trace_to_json(sim.tracer) expected = ( '{"signal": [' - '{"name": "i", "wave": "=====", "data": ' - '["0x1", "0x2", "0x4", "0x8", "0x0"]}, ' - '{"name": "o", "wave": "=.===", "data": ' - '["0x0", "0x1", "0x2", "0x3"]}, ' - '{"name": "state", "wave": "=.===", "data": ' - '["A", "B", "C", "D"]}], ' + '{"name": "i", "wave": "=====", "data": ["0x1", "0x2", "0x4", "0x8", "0x0"]}, ' # noqa: E501 + '{"name": "o", "wave": "=.===", "data": ["0x0", "0x1", "0x2", "0x3"]}, ' + '{"name": "state", "wave": "=.===", "data": ["A", "B", "C", "D"]}], ' '"config": {"hscale": 1}, "head": {"tick": 0}}' ) - self.assertEqual(htmlstring, expected) + self.assertEqual(json_string, expected) def test_trace_to_json_repr_per_name_enum_is_bool(self): class Foo(enum.IntEnum): @@ -452,7 +447,7 @@ class Foo(enum.IntEnum): sim = pyrtl.Simulation() sim.step_multiple({"i": [1, 2, 1, 2, 2]}) - htmlstring = pyrtl.trace_to_json(sim.tracer) + json_string = pyrtl.trace_to_json(sim.tracer) expected = ( '{"signal": [' '{"name": "i", "wave": "====.", "data": ["0x1", "0x2", "0x1", "0x2"]}, ' @@ -460,7 +455,7 @@ class Foo(enum.IntEnum): '{"name": "state", "wave": "0.101"}], ' '"config": {"hscale": 1}, "head": {"tick": 0}}' ) - self.assertEqual(htmlstring, expected) + self.assertEqual(json_string, expected) if __name__ == "__main__":