Reference Implementations#
The functional unit of PyBryt is a reference implementation. A reference implenetation is a set
of conditions expected of students’ code that determine whether a student has correctly implemented
some program. They are constructed by creating a series of annotations that are
tracked in a ReferenceImplementation
object.
Creating Reference Implementations#
Reference implemenations can be created by compiling Jupyter Notebooks that have been marked-up
with annotations. To compile a reference implementation, use
ReferenceImplementation.compile
, which takes in
the path to a notebook file:
pybryt.ReferenceImplementation.compile("reference.ipynb")
There are two ways to mark-up a notebook: by creating annotations and having PyBryt track them automatically (for a single reference), or by tracking annotations in a list (for creating multiple reference implementations from a single notebook).
When compiling a notebook, PyBryt executes all of the code in the notebook and searches the
resulting global environment for instances of the
ReferenceImplementation
class. If it finds them, it
collects them into a list and returns the list of reference implementations (or a single reference
implementation if only one is found). If it doesn’t find any reference implementations, it takes all
of the annotations tracked by PyBryt and turns those into a single
reference implementation.
Automatic Reference Creation#
To create a single reference implementation from a notebook, create
Annotation
instances (assigning them to variables is not necessary).
After annotating the notebook, when PyBryt compiles the notebook, it will find all of the
annotations.
PyBryt finds the annotations because the __init__
method automatically adds the instances
created to a singleton list that PyBryt maintains, so assigning them to variables or tracking them
further is unnecessary unless more advanced reference implementations are being built. This means
that when marking up code, as below, creating new variables is unnecessary unless further conditions
are to be made later down the line.
fibs = np.zeros(n, dtype=int)
fibs[0] = 0
curr_val = pybryt.Value(fibs)
if n == 1:
return fibs
fibs[1] = 1
v = pybryt.Value(fibs)
curr_val.before(v) # not assigned to a variable, but still tracked
curr_val = v
if n == 2:
return fibs
for i in range(2, n-1):
fibs[i] = fibs[i-1] + fibs[i-2]
v = pybryt.Value(fibs)
curr_val.before(v) # not assigned to a variable, but still tracked
curr_val = v
Manual Reference Creation#
To create multiple reference implementations from a single notebook, begin by creating
Annotation
instances and grouping them into lists. These lists will
be passed to the ReferenceImplementation
constructor
to create the reference implementations. These objects must be assigned to global variables, or
PyBryt will not find them. Note that the constructor takes an additional positional argument which
corresponds to the name of the reference implementation.
As an example, consider the code below, which creates two reference implementations for a Fibonacci sequence generator:
n_fibs = 50
first_ref = []
second_ref = []
# first implementation: dynamic programming
fibs = np.zeros(n_fibs, dtype=int)
fibs[0] = 0
first_ref.append(pybryt.Value(fibs))
if n_fibs == 1:
return fibs
fibs[1] = 1
v = pybryt.Value(fibs)
first_ref.append(curr_val.before(v))
curr_val = v
if n_fibs == 2:
return fibs
for i in range(2, n_fibs-1):
fibs[i] = fibs[i-1] + fibs[i-2]
v = pybryt.Value(fibs)
first_ref.append(curr_val.before(v))
curr_val = v
final_answer = fibs[-1]
# second implementation: hash map
fib_map = {}
def fib(n):
if n == 0:
return 0
if n == 1:
return 1
if n in fib_map:
return fib_map[n]
ans = fib(n-1) + fib(n-2)
fib_map[n] = ans
second_ref.append(pybryt.Value(fib_map))
return ans
final_answer = fib(n_fibs)
# create references
ref1 = pybryt.ReferenceImplementation("ref1", first_ref)
ref2 = pybryt.ReferenceImplementation("ref2", second_ref)
For creating more readable reports, you can also set a display name for reference implementations
that will be used when PyBryt generates a textual report of results. To do this, pass the
display_name
argument to the constructor:
pybryt.ReferenceImplementation("ref1", first_ref, display_name="Reference 1")
This argument is also accepted by
ReferenceImplementation.compile
.
Interacting with Reference Implementations#
The ReferenceImplementation
class defines an API for
working with reference implementations. The core method for reconciling a student implementation,
encoded as a list of 2-tuples, is
ReferenceImplementation.run
. This method is
abstracted away by the StudentImplementation.check
method, which calls it for that student implementation.
Storing Reference Implementations#
Reference implementation objects can be saved to a file by calling
ReferenceImplementation.dump
, which takes in the
path to the file and uses the dill
library to serialize the object. To load a reference
implementation, or a list of reference implementations, from a file, use the static method
ReferenceImplementation.load
.
ref = pybryt.ReferenceImplementation("foo", [...])
ref.dump() # defaults to filename '{ref.name}.pkl'
ref = pybryt.ReferenceImplementation.load('foo.pkl')