Skip to content

Commit

Permalink
Fix a minor Simulation bug, where SimulationTrace was not actually op…
Browse files Browse the repository at this point in the history
…tional.

Test simulations without SimulationTrace.

Minor documentation improvements in simulation.py.
  • Loading branch information
fdxmw committed May 16, 2024
1 parent 602a4cc commit 13687a0
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 56 deletions.
131 changes: 75 additions & 56 deletions pyrtl/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
import typing

from .pyrtlexceptions import PyrtlError, PyrtlInternalError
from .core import working_block, PostSynthBlock, _PythonSanitizer
from .core import working_block, PostSynthBlock, _PythonSanitizer, Block
from .wire import Input, Register, Const, Output, WireVector
from .memory import RomBlock
from .memory import RomBlock, MemBlock
from .helperfuncs import check_rtl_assertions, _currently_in_jupyter_notebook
from .helperfuncs import val_to_signed_integer
from .importexport import _VerilogSanitizer
Expand All @@ -36,13 +36,11 @@ class Simulation(object):
A Simulation step works as follows:
1. Registers are updated:
1. (If this is the first step) With the default values passed in
to the Simulation during instantiation and/or any reset values
specified in the individual registers.
2. (Otherwise) With their next values calculated in the previous step
(``r`` logic nets).
2. The new values of these registers as well as the values of block inputs
are propagated through the combinational logic.
3. Memory writes are performed (``@`` logic nets).
Expand Down Expand Up @@ -81,28 +79,29 @@ class Simulation(object):
}

def __init__(
self, tracer=True, register_value_map={}, memory_value_map={},
default_value=0, block=None):
self, tracer: SimulationTrace = True,
register_value_map: dict[Register, int] = {},
memory_value_map: dict[MemBlock, dict[int, int]] = {},
default_value: int = 0, block: Block = None):
"""Creates a new circuit simulator.
:param SimulationTrace tracer: Stores execution results. Defaults to a
new :class:`.SimulationTrace` with no params passed to it. If None
is passed, no tracer is instantiated (which is good for long
running simulations). If the default (true) is passed, Simulation
will create a new tracer automatically which can be referenced by
the member variable ``.tracer``
:param dict[Register, int] register_value_map: Defines the initial
value for the registers specified; overrides the registers's
:param tracer: Stores execution results. If ``None`` is passed, no
tracer is instantiated, which improves performance for long running
simulations). If the default (``True``) is passed, Simulation will
create a new :class:`.SimulationTrace` automatically which can be
referenced by the member variable ``.tracer``
:param register_value_map: Defines the initial
value for the registers specified; overrides the register's
``reset_value``.
:param memory_value_map: Defines initial values for many
addresses in a single or multiple memory. Format: {Memory: {address: Value}}.
Memory is a memory block, address is the address of a value
:param int default_value: The value that all unspecified registers and
:param memory_value_map: Defines initial values for many addresses in a
single or multiple memory. Format: ``{Memory: {address: Value}}``.
``Memory`` is a memory block, address is the address of a value
:param default_value: The value that all unspecified registers and
memories will initialize to (default 0). For registers, this is the
value that will be used if the particular register doesn't have a
specified ``reset_value``, and isn't found in the
`register_value_map`.
:param Block block: the hardware block to be traced (which might be of
:param block: the hardware block to be traced (which might be of
type :class:`.PostSynthBlock`). Defaults to the working block
Warning: Simulation initializes some things when called with
Expand Down Expand Up @@ -181,10 +180,12 @@ def _initialize(self, register_value_map={}, memory_value_map={}):
self.reg_update_nets = tuple((self.block.logic_subset('r')))
self.mem_update_nets = tuple((self.block.logic_subset('@')))

self.tracer._set_initial_values(self.default_value, self.regvalue.copy(),
copy.deepcopy(self.memvalue))
if self.tracer is not None:
self.tracer._set_initial_values(
self.default_value, self.regvalue.copy(),
copy.deepcopy(self.memvalue))

def step(self, provided_inputs):
def step(self, provided_inputs: dict[str, int]):
"""Take the simulation forward one cycle.
:param provided_inputs: a dictionary mapping WireVectors to their
Expand Down Expand Up @@ -266,8 +267,10 @@ def step(self, provided_inputs):
# raise the appropriate exceptions
check_rtl_assertions(self)

def step_multiple(self, provided_inputs={}, expected_outputs={}, nsteps=None,
file=sys.stdout, stop_after_first_error=False):
def step_multiple(self, provided_inputs: dict[str, int] = {},
expected_outputs: dict[str, int] = {},
nsteps: int = None,
file=sys.stdout, stop_after_first_error: bool = False):
"""Take the simulation forward N cycles, based on the number of values
for each input
Expand Down Expand Up @@ -394,10 +397,10 @@ def _sort_tuple(t):
file.write("{0:>5} {1:>10} {2:>8} {3:>8}\n".format(step, name, expected, actual))
file.flush()

def inspect(self, w):
def inspect(self, w: str) -> int:
""" Get the value of a WireVector in the last simulation cycle.
:param str w: the name of the WireVector to inspect
:param w: the name of the WireVector to inspect
(passing in a WireVector instead of a name is deprecated)
:return: value of w in the current step of simulation
Expand All @@ -410,7 +413,7 @@ def inspect(self, w):
wire = self.block.wirevector_by_name.get(w, w)
return self.value[wire]

def inspect_mem(self, mem):
def inspect_mem(self, mem: MemBlock) -> dict[int, int]:
""" Get the values in a map during the current simulation cycle.
:param mem: the memory to inspect
Expand Down Expand Up @@ -497,13 +500,11 @@ class FastSimulation(object):
A Simulation step works as follows:
1. Registers are updated:
1. (If this is the first step) With the default values passed in
to the Simulation during instantiation and/or any reset values
specified in the individual registers.
2. (Otherwise) With their next values calculated in the previous step
(``r`` logic nets).
2. The new values of these registers as well as the values of block inputs
are propagated through the combinational logic.
3. Memory writes are performed (``@`` logic nets).
Expand All @@ -527,21 +528,26 @@ class FastSimulation(object):
# when put into the generated code

def __init__(
self, register_value_map={}, memory_value_map={},
default_value=0, tracer=True, block=None, code_file=None):
""" Instantiates a Fast Simulation instance.
self, register_value_map: dict[Register, int] = {},
memory_value_map: dict = {},
default_value: int = 0, tracer: SimulationTrace = True,
block: Block = None, code_file=None):
"""Instantiates a Fast Simulation instance.
The interface for FastSimulation and Simulation should be almost identical.
In addition to the Simulation arguments, FastSimulation additionally takes:
The interface for FastSimulation and :py:class:`Simulation` should be
almost identical. In addition to the :py:class:`Simulation` arguments,
FastSimulation additionally takes:
:param code_file: The file in which to store a copy of the generated
Python code. Defaults to no code being stored.
Look at :meth:`.Simulation.__init__` for descriptions for the other parameters.
Look at :meth:`.Simulation.__init__` for descriptions for the other
parameters.
This builds the FastSimulation compiled Python code, so all changes to
the circuit after calling this function will not be reflected in the
simulation.
This builds the Fast Simulation compiled Python code, so all changes
to the circuit after calling this function will not be reflected in
the simulation.
"""

block = working_block(block)
Expand Down Expand Up @@ -578,8 +584,9 @@ def _initialize(self, register_value_map={}, memory_value_map={}):
with open(self.code_file, 'w') as file:
file.write(s)

self.tracer._set_initial_values(self.default_value, self.regs.copy(),
copy.deepcopy(self.mems))
if self.tracer is not None:
self.tracer._set_initial_values(
self.default_value, self.regs.copy(), copy.deepcopy(self.mems))

context = {}
logic_creator = compile(s, '<string>', 'exec')
Expand All @@ -601,7 +608,7 @@ def _initialize_mems(self, memory_value_map):
else:
self.mems[self._mem_varname(mem)] = {}

def step(self, provided_inputs):
def step(self, provided_inputs: dict[str, int]):
""" Run the simulation for a cycle.
:param provided_inputs: a dictionary mapping WireVectors (or their
Expand Down Expand Up @@ -645,8 +652,10 @@ def step(self, provided_inputs):
# check the rtl assertions
check_rtl_assertions(self)

def step_multiple(self, provided_inputs={}, expected_outputs={}, nsteps=None,
file=sys.stdout, stop_after_first_error=False):
def step_multiple(self, provided_inputs: dict[str, int] = {},
expected_outputs: dict[str, int] = {},
nsteps: int = None, file=sys.stdout,
stop_after_first_error: bool = False):
"""Take the simulation forward N cycles, where N is the number of
values for each provided input.
Expand Down Expand Up @@ -782,10 +791,10 @@ def _sort_tuple(t):
file.write("{0:>5} {1:>10} {2:>8} {3:>8}\n".format(step, name, expected, actual))
file.flush()

def inspect(self, w):
def inspect(self, w: str) -> int:
""" Get the value of a WireVector in the last simulation cycle.
:param str w: the name of the WireVector to inspect
:param w: the name of the WireVector to inspect
(passing in a WireVector instead of a name is deprecated)
:return: value of `w` in the current step of simulation
Expand All @@ -801,7 +810,7 @@ def inspect(self, w):
# "and measure the probe value to measure this wire's value"
# .format(w))

def inspect_mem(self, mem):
def inspect_mem(self, mem: MemBlock) -> dict[int, int]:
""" Get the values in a map during the current simulation cycle.
:param mem: the memory to inspect
Expand Down Expand Up @@ -996,7 +1005,7 @@ class WaveRenderer(object):
of :py:class:`RendererConstants`.
"""
def __init__(self, constants):
def __init__(self, constants: RendererConstants):
"""Instantiate a WaveRenderer.
:param constants: Subclass of :py:class:`RendererConstants` that
Expand All @@ -1006,7 +1015,8 @@ def __init__(self, constants):
"""
self.constants = constants

def render_ruler_segment(self, n, cycle_len, segment_size, maxtracelen):
def render_ruler_segment(self, n: int, cycle_len: int, segment_size: int,
maxtracelen: int):
"""Render a major tick padded to segment_size.
:param n: Cycle number for the major tick mark.
Expand All @@ -1029,8 +1039,10 @@ def render_ruler_segment(self, n, cycle_len, segment_size, maxtracelen):
ticks = major_tick.ljust(cycle_len * segment_size)
return ticks

def val_to_str(self, value: int, wire: WireVector,
repr_func: typing.Callable, repr_per_name: dict) -> str:
def val_to_str(
self, value: int, wire: WireVector,
repr_func: typing.Callable[[int], str],
repr_per_name: dict[str, typing.Callable[[int], str]]) -> str:
"""Return a string representing 'value'.
:param value: The value to convert to string.
Expand Down Expand Up @@ -1058,12 +1070,15 @@ def invoke_f(f, value):
else:
return invoke_f(repr_func, value)

def render_val(self, w, prior_val, current_val, symbol_len, cycle_len,
repr_func, repr_per_name, prev_line, is_last):
def render_val(
self, w: WireVector, prior_val: int, current_val: int,
symbol_len: int, cycle_len: int,
repr_func: typing.Callable[[int], str],
repr_per_name: dict[str, typing.Callable[[int], str]],
prev_line: bool, is_last: bool) -> str:
"""Return a string encoding the given value in a waveform.
:param w: The WireVector we are rendering to a waveform
:param n: An integer from 0 to segment_len-1
:param prior_val: Last value rendered. None if there was no last value.
:param current_val: the value to be rendered
:param symbol_len: Width of each value, in characters.
Expand All @@ -1077,6 +1092,7 @@ def render_val(self, w, prior_val, current_val, symbol_len, cycle_len,
:param prev_line: If True, render the gap between signals. If False,
render the main signal. This is useful for rendering signals across
two lines, see the _prev_line* fields in RendererConstants.
:param is_last: If True, current_val is in the last cycle.
Returns a string of printed length symbol_len that will draw the
representation of current_val. The input prior_val is used to
Expand Down Expand Up @@ -1449,7 +1465,8 @@ def __getitem__(self, key):
class SimulationTrace(object):
""" Storage and presentation of simulation waveforms. """

def __init__(self, wires_to_track=None, block=None):
def __init__(self, wires_to_track: list[WireVector] = None,
block: Block = None):
"""
Creates a new Simulation Trace
Expand Down Expand Up @@ -1604,8 +1621,10 @@ def print_trace_strs(time):

def render_trace(
self, trace_list: list[str] = None, file=sys.stdout,
renderer: WaveRenderer = default_renderer(), symbol_len: int = None,
repr_func: typing.Callable = hex, repr_per_name: dict = {},
renderer: WaveRenderer = default_renderer(),
symbol_len: int = None,
repr_func: typing.Callable[[int], str] = hex,
repr_per_name: dict[str, typing.Callable[[int], str]] = {},
segment_size: int = 1):

"""Render the trace to a file using unicode and ASCII escape sequences.
Expand Down
19 changes: 19 additions & 0 deletions tests/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,25 @@ def test_invalid_base(self):
self.sim_trace.print_trace(base=4)


class NoTracerBase(unittest.TestCase):
"""Test simulation without a SimulationTrace."""
def setUp(self):
pyrtl.reset_working_block()

def test_no_tracer(self):
a_val = 1
b_val = 2
expected_c = a_val + b_val
a = pyrtl.Const(a_val, bitwidth=2)
b = pyrtl.Const(b_val, bitwidth=2)
c = pyrtl.Output(name='c', bitwidth=3)
c <<= a + b
sim = self.sim(tracer=None)
sim.step({})
actual_c = sim.inspect('c')
self.assertEqual(expected_c, actual_c)


def make_unittests():
"""
Generates separate unittests for each of the simulators
Expand Down

0 comments on commit 13687a0

Please sign in to comment.