Skip to content

Commit 18e0639

Browse files
committed
Add a to_ipynb.py script that converts a PyRTL example script to a Jupyter notebook. This change also updates all Jupyter notebooks in ipynb-examples.
This change also deletes the incomplete `example9-transforms-draft.py`. The updated Jupyter notebooks were tested in Jupyter Lite.
1 parent 6636fa8 commit 18e0639

31 files changed

+3270
-1819
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,8 @@ ipynb-examples/.ipynb_checkpoints
5353
.vscode
5454

5555
# Python venv
56-
pyvenv.cfg
56+
pyvenv.cfg
57+
58+
# Jupyter
59+
.jupyterlite.doit.db
60+
_output

examples/Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
PYTHON=python
2+
PY_FILES=$(wildcard *.py)
3+
IPYNB_FILES=$(addprefix ../ipynb-examples/, $(PY_FILES:.py=.ipynb))
4+
5+
all: $(IPYNB_FILES)
6+
7+
# Convert a PyRTL example Python script to a Jupyter notebook.
8+
../ipynb-examples/%.ipynb: %.py tools/to_ipynb.py
9+
$(PYTHON) tools/to_ipynb.py $< $@

examples/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# PyRTL's Examples
2+
3+
PyRTL's examples are Python scripts that demonstrate various PyRTL features.
4+
These scripts can be run with `python $SCRIPT_FILE_NAME`.
5+
6+
Each script is converted to an equivalent Jupyter notebook in the
7+
`ipynb-examples` directory. These conversions are done by the `to_ipynb.py`
8+
script in the `examples/tools` directory.
9+
10+
If you update an example script, be sure to update its corresponding Jupyter
11+
notebook. These updates are handled by the `Makefile` in this directory, so all
12+
Jupyter notebooks can be updated by running `make`.
Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
# # A simple combinational logic example.
2+
#
3+
# Create an 8-bit adder that adds `a` and `b`. Check if the sum is greater than `5`.
14
import pyrtl
25

3-
# "pin" input/outputs
4-
a = pyrtl.Input(8, "a")
5-
b = pyrtl.Input(8, "b")
6-
q = pyrtl.Output(8, "q")
7-
gt5 = pyrtl.Output(1, "gt5")
6+
# Define `Inputs` and `Outputs`.
7+
a = pyrtl.Input(bitwidth=8, name="a")
8+
b = pyrtl.Input(bitwidth=8, name="b")
89

9-
sum = a + b # makes an 8-bit adder
10-
q <<= sum # assigns output of adder to out pin
11-
gt5 <<= sum > 5 # does a comparison, assigns that to different pin
10+
q = pyrtl.Output(bitwidth=8, name="q")
11+
gt5 = pyrtl.Output(bitwidth=1, name="gt5")
1212

13-
# the simulation and waveform output
13+
# Define the logic that connects the `Inputs` to the `Outputs`.
14+
sum = a + b # Makes an 8-bit adder.
15+
q <<= sum # Connects the adder's output to the `q` output pin.
16+
gt5 <<= sum > 5 # Does a comparison and connects the result to the `gt5` output pin.
17+
18+
# Simulate various values for `a` and `b` over 5 cycles.
1419
sim = pyrtl.Simulation()
1520
sim.step_multiple({"a": [0, 1, 2, 3, 4], "b": [2, 2, 3, 3, 4]})
21+
22+
# Display simulation traces as waveforms.
1623
sim.tracer.render_trace()

examples/example1-combologic.py

Lines changed: 59 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,110 @@
1-
"""
2-
Example 1: A simple combination logic block example.
3-
4-
This example declares a block of hardware with three one-bit inputs, (a,b,c) and two
5-
one-bit outputs (sum, cout). The logic declared is a simple one-bit adder and the
6-
definition uses some of the most common parts of PyRTL. The adder is then simulated on
7-
random data, the wave form is printed to the screen, and the resulting trace is compared
8-
to a "correct" addition. If the result is correct then a 0 is returned, else 1.
9-
"""
10-
1+
# # Example 1: A simple combination logic block example.
2+
#
3+
# This example declares a block of hardware with three one-bit inputs, (`a`,`b`,`c`) and
4+
# two one-bit outputs (`sum`, `cout`). The logic declared is a simple one-bit adder and
5+
# the definition uses some of the most common parts of PyRTL. The adder is then
6+
# simulated on random data, the wave form is printed to the screen, and the resulting
7+
# trace is compared to a "correct" addition.
118
import random
129

1310
import pyrtl
1411

15-
# The basic idea of PyRTL is to specify the component of a some hardware block through
16-
# the declaration of wires and operations on those wires. The current working block, an
17-
# instance of a class devilishly named "Block", is implicit in all of the below code --
18-
# it is easiest to start with the way wires work.
19-
20-
# --- Step 1: Define Logic -------------------------------------------------
21-
22-
# One of the most fundamental types in PyRTL is the "WireVector" which acts very much
12+
# The basic idea of PyRTL is to specify a hardware block by defining wires and the
13+
# operations performed on those wires. The current working block, an instance of a class
14+
# devilishly named `Block`, is implicit in all of the code below -- it is easiest to
15+
# start with the way wires work.
16+
#
17+
# ## Step 1: Define Logic
18+
#
19+
# One of the most fundamental types in PyRTL is the `WireVector` which acts very much
2320
# like a Python list of 1-bit wires. Unlike a normal list, though, the number of bits is
2421
# explicitly declared.
2522
temp1 = pyrtl.WireVector(bitwidth=1, name="temp1")
2623

27-
# Both arguments are in fact optional and default to a bitwidth of 1 and a unique name
28-
# generated by PyRTL starting with 'tmp'
24+
# Both arguments are optional and default to a `bitwidth` of 1 and a unique `name`
25+
# generated by PyRTL that starts with `tmp`.
2926
temp2 = pyrtl.WireVector()
3027

31-
# Two special types of WireVectors are Input and Output, which are used to specify an
28+
# `Input` and `Output` are two special types of `WireVectors`, which specify the
3229
# interface to the hardware block.
33-
a, b, c = pyrtl.Input(1, "a"), pyrtl.Input(1, "b"), pyrtl.Input(1, "c")
34-
sum, carry_out = pyrtl.Output(1, "sum"), pyrtl.Output(1, "carry_out")
30+
a = pyrtl.Input(1, "a")
31+
b = pyrtl.Input(1, "b")
32+
c = pyrtl.Input(1, "c")
33+
34+
sum = pyrtl.Output(1, "sum")
35+
carry_out = pyrtl.Output(1, "carry_out")
3536

3637
# Okay, let's build a one-bit adder. To do this we need to use the assignment operator,
37-
# which is '<<='. This takes an already declared wire and "connects" it to some other
38-
# already declared wire. Let's start with the sum bit, which is of course just the xor
39-
# of the three inputs
38+
# which is `<<=`. This takes an already declared wire and "connects" it to another
39+
# already declared wire. Let's start with the `sum` bit, which is of course just the xor
40+
# of the three inputs:
4041
sum <<= a ^ b ^ c
4142

42-
# The carry_out bit would just be "carry_out <<= a & b | a & c | b & c" but let's break
43-
# than down a bit to see what is really happening. What if we want to give names to the
44-
# partial signals in the middle of that computation? When you take "a & b" in PyRTL,
45-
# what that really means is "make an AND gate, connect one input to 'a' and the other to
46-
# 'b' and return the result of the gate". The result of that AND gate can then be
47-
# assigned to temp1 or it can be used like any other Python variable.
48-
49-
temp1 <<= a & b # connect the result of a & b to the pre-allocated wirevector
43+
# The `carry_out` bit would just be `carry_out <<= a & b | a & c | b & c` but let's
44+
# break that down a bit to see what is really happening. What if we want to give names
45+
# to the intermediate signals in the middle of that computation? When you take `a & b`
46+
# in PyRTL, what that really means is "make an AND gate, connect one input to `a` and
47+
# the other to `b` and return the result of the gate". The result of that AND gate can
48+
# then be assigned to `temp1` or it can be used like any other Python variable.
49+
temp1 <<= a & b # connect the result of a & b to the pre-allocated WireVector
5050
temp2 <<= a & c
5151
temp3 = b & c # temp3 IS the result of b & c (this is the first mention of temp3)
5252
carry_out <<= temp1 | temp2 | temp3
5353

54-
# You can access the working block through pyrtl.working_block(), and for most things
55-
# one block is all you will need. Example 2 discusses this in more detail, but for now
56-
# we can just print the block to see that in fact it looks like the hardware we
57-
# described. The format is a bit weird, but roughly translates to a list of gates (the
58-
# 'w' gates are just wires). The ins and outs of the gates are printed
59-
# 'name'/'bitwidth''WireVectorType'
60-
54+
# You can access the working block through `working_block()`, and for most things one
55+
# block is all you will need. Example 2 discusses this in more detail, but for now we
56+
# can just print the block to see that in fact it looks like the hardware we described.
57+
# The format is a bit weird, but roughly translates to a list of gates (the `w` gates
58+
# are just wires). The ins and outs of the gates are formatted as
59+
# `{name}/{bitwidth}{WireVectorType}`.
6160
print("--- One Bit Adder Implementation ---")
6261
print(pyrtl.working_block())
63-
print()
64-
65-
# --- Step 2: Simulate Design -----------------------------------------------
6662

63+
# ## Step 2: Simulate Design
64+
#
6765
# Okay, let's simulate our one-bit adder.
68-
6966
sim = pyrtl.Simulation()
7067

71-
# Now all we need to do is call "sim.step" to simulate each clock cycle of our design.
72-
# We just need to pass in some input each cycle, which is a dictionary mapping inputs
73-
# (the *names* of the inputs, not the actual Input instances) to their value for that
74-
# signal each cycle. In this simple example, we can just specify a random value of 0 or
75-
# 1 with Python's random module. We call step 15 times to simulate 15 cycles.
76-
68+
# Now all we need to do is call `step()` to simulate each clock cycle of our design. We
69+
# just need to pass in some input each cycle, which is a dictionary mapping inputs (the
70+
# *names* of the inputs, not the actual instances of `Input`) to their value for that
71+
# signal in the current cycle. In this simple example, we can just specify a random
72+
# value of 0 or 1 with Python's random module. We call step 15 times to simulate 15
73+
# cycles.
7774
for _cycle in range(15):
7875
sim.step(
79-
{
80-
"a": random.choice([0, 1]),
81-
"b": random.choice([0, 1]),
82-
"c": random.choice([0, 1]),
83-
}
76+
{"a": random.randrange(2), "b": random.randrange(2), "c": random.randrange(2)}
8477
)
8578

8679
# Now all we need to do is print the trace results to the screen. Here we use
87-
# "render_trace" with some size information.
88-
print("--- One Bit Adder Simulation ---")
80+
# `render_trace()` with some size information.
81+
print("\n--- One Bit Adder Simulation ---")
8982
sim.tracer.render_trace(symbol_len=2)
9083

9184
a_value = sim.inspect(a)
9285
print("The latest value of 'a' was: ", a_value)
9386

94-
# --- Step 3: Verification of Simulated Design ---------------------------------------
95-
96-
# Now finally, let's check the trace to make sure that sum and carry_out are actually
87+
# ## Step 3: Verification of Simulated Design
88+
#
89+
# Finally, let's check the trace to make sure that `sum` and `carry_out` are actually
9790
# the right values when compared to Python's addition operation. Note that all the
9891
# simulation is done at this point and we are just checking the waveform, but there is
9992
# no reason you could not do this at simulation time if you had a really long-running
10093
# design.
101-
10294
for cycle in range(15):
103-
# Note that we are doing all arithmetic on values, NOT wirevectors, here. We can add
104-
# the inputs together to get a value for the result
95+
# Note that we are doing all arithmetic on values, NOT `WireVectors` here. We can
96+
# add the inputs together to get a value for the result:
10597
add_result = (
10698
sim.tracer.trace["a"][cycle]
10799
+ sim.tracer.trace["b"][cycle]
108100
+ sim.tracer.trace["c"][cycle]
109101
)
110-
# We can select off the bits and compare
102+
# We can select off the bits and compare:
111103
python_sum = add_result & 0x1
112104
python_cout = (add_result >> 1) & 0x1
113105
if (
114106
python_sum != sim.tracer.trace["sum"][cycle]
115107
or python_cout != sim.tracer.trace["carry_out"][cycle]
116108
):
117-
print("This Example is Broken!!!")
118-
exit(1)
109+
msg = "This Example is Broken!!!"
110+
raise Exception(msg)
Lines changed: 49 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
"""Example 1.1: Working with signed integers.
2-
3-
This example demonstrates:
4-
• Correct addition of signed integers with `signed_add`.
5-
• Displaying signed integers in traces with `val_to_signed_integer`.
6-
7-
Signed integers are represented in two's complement.
8-
https://en.wikipedia.org/wiki/Two%27s_complement
9-
10-
"""
11-
1+
# # Example 1.1: Working with signed integers.
2+
#
3+
# This example demonstrates:
4+
# * Correct addition of signed integers with `signed_add()`.
5+
# * Displaying signed integers in traces with `val_to_signed_integer()`.
6+
#
7+
# Signed integers are represented in two's complement.
8+
# https://en.wikipedia.org/wiki/Two%27s_complement
129
import pyrtl
1310

14-
# Let's start with unsigned addition.
15-
# ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
11+
# ## Let's start with unsigned addition.
12+
#
1613
# Add all combinations of two unsigned 2-bit inputs.
1714
a = pyrtl.Input(bitwidth=2, name="a")
1815
b = pyrtl.Input(bitwidth=2, name="b")
@@ -28,43 +25,44 @@
2825

2926
sim = pyrtl.Simulation()
3027
sim.step_multiple(provided_inputs=unsigned_inputs)
28+
3129
# In this trace, `unsigned_sum` is the sum of all combinations of
3230
# {0, 1, 2, 3} + {0, 1, 2, 3}. For example:
33-
# cycle 0 shows 0 + 0 = 0
34-
# cycle 1 shows 0 + 1 = 1
35-
# cycle 15 shows 3 + 3 = 6
31+
# * cycle 0 shows 0 + 0 = 0
32+
# * cycle 1 shows 0 + 1 = 1
33+
# * cycle 15 shows 3 + 3 = 6
3634
print(
37-
"Unsigned addition. Each cycle adds a different combination of "
38-
"numbers.\nunsigned_sum == a + b"
35+
"Unsigned addition. Each cycle adds a different combination of numbers.\n"
36+
"unsigned_sum == a + b"
3937
)
4038
sim.tracer.render_trace(repr_func=int)
4139

42-
# Re-interpreting `a`, `b`, and `unsigned_sum` as signed is incorrect.
43-
# ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
44-
# Use `val_to_signed_integer` to re-interpret the previous simulation results
45-
# as signed integers. But the results are INCORRECT, because `unsigned_sum`
40+
# ## Re-interpreting `a`, `b`, and `unsigned_sum` as signed is **incorrect**.
41+
#
42+
# Use `val_to_signed_integer()` to re-interpret the previous simulation results
43+
# as signed integers. But the results are **INCORRECT**, because `unsigned_sum`
4644
# performed unsigned addition with `+`. For example:
4745
#
48-
# cycle 2 shows 0 + -2 = 2
49-
# cycle 13 shows -1 + 1 = -4
46+
# * cycle 2 shows 0 + -2 = 2
47+
# * cycle 13 shows -1 + 1 = -4
5048
#
5149
# `unsigned_sum` is incorrect because PyRTL must extend `a` and `b` to the
5250
# sum's bitwidth before adding, but PyRTL zero-extends by default, instead of
5351
# sign-extending. Zero-extending is correct when `a` and `b` are unsigned, but
54-
# sign-extending is correct when `a` and `b` are signed. Use `signed_add` to
52+
# sign-extending is correct when `a` and `b` are signed. Use `signed_add()` to
5553
# sign-extend `a` and `b`, as demonstrated next.
5654
print(
57-
"\nUse `val_to_signed_integer` to re-interpret the previous simulation "
58-
"results as\nsigned integers. But re-interpreting the previous trace as "
59-
"signed integers \nproduces INCORRECT RESULTS!"
55+
"\nUse `val_to_signed_integer()` to re-interpret the previous simulation results as"
56+
"\nsigned integers. But re-interpreting the previous trace as signed integers \n"
57+
"produces INCORRECT RESULTS!"
6058
)
6159
sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer)
6260

63-
# Use `signed_add` to correctly add signed integers.
64-
# ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
65-
# `signed_add` works by sign-extending its inputs to the sum's bitwidth before
61+
# ## Use `signed_add()` to correctly add signed integers.
62+
#
63+
# `signed_add()` works by sign-extending its inputs to the sum's bitwidth before
6664
# adding. There are many `signed_*` functions for signed operations, like
67-
# `signed_sub`, `signed_mul`, `signed_lt`, and so on.
65+
# `signed_sub()`, `signed_mult()`, `signed_lt()`, and so on.
6866
pyrtl.reset_working_block()
6967

7068
a = pyrtl.Input(bitwidth=2, name="a")
@@ -83,30 +81,32 @@
8381

8482
# In this trace, `signed_sum` is the sum of all combinations of
8583
# {-2, -1, 0, 1} + {-2, -1, 0, 1}. For example:
86-
# cycle 0 shows -2 + -2 = -4
87-
# cycle 1 shows -2 + -1 = -3
88-
# cycle 15 shows 1 + 1 = 2
84+
# * cycle 0 shows -2 + -2 = -4
85+
# * cycle 1 shows -2 + -1 = -3
86+
# * cycle 15 shows 1 + 1 = 2
8987
print(
90-
"\nReset the simulation and use `signed_add` to correctly add signed "
91-
"integers.\nsigned_sum == signed_add(a, b)"
88+
"\nReset the simulation and use `signed_add()` to correctly add signed integers.\n"
89+
"signed_sum == signed_add(a, b)"
9290
)
9391
sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer)
9492

95-
# Manually sign-extend inputs to correctly add signed integers with `+`.
96-
# ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
97-
# Instead of using `signed_add`, we can manually sign-extend the inputs and
98-
# truncate the output to correctly add signed integers with `+`. Use this trick
99-
# to implement other signed arithmetic operations.
93+
# ## Manually sign-extend inputs to correctly add signed integers with `+`.
94+
#
95+
# Instead of using `signed_add()`, we can manually sign-extend the inputs with
96+
# `sign_extended()` and `truncate()` the output to correctly add signed integers with
97+
# `+`. Use this trick to implement other signed arithmetic operations.
10098
pyrtl.reset_working_block()
10199

102100
a = pyrtl.Input(bitwidth=2, name="a")
103101
b = pyrtl.Input(bitwidth=2, name="b")
104102

105103
# Using `+` produces the correct result for signed addition here because we
106-
# manually extend `a` and `b` to 3 bits. The result of `a.sign_extended(3) +
107-
# b.sign_extended(3)` is now 4 bits, but we truncate it to 3 bits. This
108-
# truncation is subtle, but important! The addition's full 4 bit result would
109-
# not be correct.
104+
# manually extend `a` and `b` to 3 bits. The result of
105+
#
106+
# a.sign_extended(3) + b.sign_extended(3)
107+
#
108+
# is now 4 bits, but we truncate it to 3 bits. This truncation is subtle, but important!
109+
# The addition's full 4 bit result would not be correct.
110110
sign_extended_sum = pyrtl.Output(bitwidth=3, name="sign_extended_sum")
111111
extended_a = a.sign_extended(bitwidth=3)
112112
extended_b = b.sign_extended(bitwidth=3)
@@ -121,9 +121,8 @@
121121
sim.step_multiple(provided_inputs=signed_inputs)
122122

123123
print(
124-
"\nInstead of using `signed_add`, we can also manually sign extend the "
125-
"inputs to\ncorrectly add signed integers with `+`.\n"
126-
"sign_extended_sum == (a.sign_extended(3) + b.sign_extended(3))"
127-
".truncate(3)"
124+
"\nInstead of using `signed_add()`, we can also manually sign extend the inputs to"
125+
"\ncorrectly add signed integers with `+`.\nsign_extended_sum == "
126+
"(a.sign_extended(3) + b.sign_extended(3)).truncate(3)"
128127
)
129128
sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer)

0 commit comments

Comments
 (0)