Relational Annotations#

Relational annotations define some kind of relationship between two or more annotations. Currently, PyBryt supports two types of relational annotations:

  • temporal annotations and

  • boolean annotations.

All relational annotations are subclasses of the abstract RelationalAnnotation class, which defines some helpful defaults for working with annotations that have child annotations.

Temporal Annotations#

Temporal annotations describe when variables should appear in student’s code relative to one another. For example, let us consider the problem of a dynamic programming algorithm to compute the Fibonacci sequence: the array containing \(n-1\) first Fibonacci numbers should appear in memory before the array with \(n\) first Fibonacci numbers. To enforce such a constraint, the Annotation class defines a before method that asserts that one annotation occurs before another:

def fib(n):
    """
    Compute and return an array of the first n Fibonacci numbers using dynamic programming.

    Args:
    n (``int``): the number of Fibonacci numbers to return

    Returns:
    ``np.ndarray``: the first ``n`` Fibonacci numbers

    """
    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)  # we expect curr_val to appear in memory before v
    curr_val = v
    if n == 2:
        return fibs

    for i in range(2, n):
        fibs[i] = fibs[i-1] + fibs[i-2]

        v = pybryt.Value(fibs) # array of first n Fibonacci numbers
        curr_val.before(v)     # check that first n-1 appear before first n Fibonacci numbers
        curr_val = v           # update curr_val for next iteration

    return fibs

In the example above, updating curr_val in the loop allows us to create a before condition to ensure the student followed the correct dynamic programming algorithm by checking each update to the fibs array.

Temporal annotations are satisfied when the student’s code satisfies all of the child Value annotations and when the first annotation (the one calling Annotation.before) has a timestamp greater than or equal to the timestamp of the second annotation.

Note that Annotation.before returns an instance of the BeforeAnnotation class, which is itself a subclass of Annotation and supports all of the same operations. Annotation also provides Annotation.after, which also returns an instance of the BeforeAnnotation class, but with the operands switched.

Boolean Annotations#

Boolean annotations define conditions on the presence of different values. For example, in solving an exercise, students may be able to take two different paths, and this logic can be enforced using a XorAnnotation to ensure that only one of the two possible values is present.

Relational annotations can be created either by instantiating the classes directly using the constructor or, as it is more recommended, by using Python’s bitwise logical operators, &, |, ^, and ~, on annotations. The special (dunder) methods for these operators have been overridden in Annotation class, and return the RelationalAnnotation subclass instance corresponding to the logical operator used.

To create the XOR example from two values v1 and v2, we write

v1 ^ v2

To assert that a student should not have a specific value v in their code, we use

~v