Value Annotations#

Value annotations are the most basic type of annotation. They expect a specific value to appear while executing the student’s code. To create a value annotation, create an instance of Value and pass to its constructor the value expected in the student’s code:

arr = np.linspace(0, 10, 11)
pybryt.Value(arr)

Note that when an instance of Value is created, copy.copy() is called on the argument passed to it, so values cannot be affected by mutability.

Attribute Annotations#

PyBryt supports checking for the presence of an object with a specific attribute value using the Attribute annotation. This annotation takes in an object and one or more strings representing an instance variable, and asserts that the student’s memory footprint should contain an object with that attribute such that the value of the attribute equals the value of the attribute in the annotation.

For example, this can be useful in checking that students have correctly fitted in sklearn regression model by checking that the coefficients are correct:

import sklearn.linear_model as lm

model = lm.LinearRegression()
model.fit(X, y)

pybryt.Attribute(model, "coef_",
    failure_message="Your model doesn't have the correct coefficients.")

Note that, by default, PyBryt doesn’t check that the object satisfying the attribute annotation has the same type as the object the annotation was created for. If a student knew the coefficient values in the above example, the following student code would satisfy that annotation:

class Foo:

    coef_ = ...  # the array of coefficients

f = Foo()  # f will satisfy the annotation

To ensure that the annotation is only satisfied when the object is of the same type, set enforce_type=True in the constructor:

pybryt.Attribute(model, "coef_", enforce_type=True,
    failure_message="Your model doesn't have the correct coefficients.")

Return Value Annotations#

In addition to checking for the presence of a value, PyBryt also has an annotation that asserts that a value was returned by a student’s function. The annotation does not specify anything about the function that returns it except that the function is part of the submission code (i.e. not part of an imported library); it merely checks that the value was seen at least once in a return event passed to the trace function.

You can create a return value annotation with the ReturnValue constructor; it accepts all the same arguments as the Value constructor:

pybryt.ReturnValue(df, success_message="...", failure_message="...")

Numerical Tolerances#

For numerical values, or iterables of numerical values that support vectorized math, it is also possible to define an absolute tolerance (atol) and/or a relative tolerance (rtol) to allow the student’s solution to deviate from the reference. Numerical tolerances are computed as with numpy.allcose, where the value is “equal enough” if it is within \(v \pm (\texttt{atol} + \texttt{rtol} \cdot |v|)\), where \(v\) is the value of the annotation. Both atol and rtol default to zero and have to be specified when value annotation is defined:

pybryt.Value(arr, atol=1e-3, rtol=1e-5)

Invariants#

Similar to numerical tolerances, which allow the student’s solution to deviate from the reference value, PyBryt allows specifying conditions when other data types should be considered equal. PyBryt supports defining these conditions using invariants. To use invariants on a value, we need to pass the invariant objects as a list to the invariants argument of the Value constructor. For instance, let’s say we want to allow the student’s solution (a string) to be case-insensitive.

correct_answer = "a CasE-inSensiTiVe stRING"
pybryt.Value(correct_answer, invariants=[pybryt.invariants.string_capitalization])

More information about invariants can be found here.

Custom Equivalence Functions#

In some cases, the algorithm that value annotations use for checking if two objects are equivalent may not be suitable to the problem at hand. For cases like this, you can provide a custom equivalence function that the value annotation will use instead to determine if two objects are equal. The equivalence function should return True if the objects are equal and False otherwise. If the equivalence function raises an error, this will be interpeted as False (unless debug mode is enabled).

For example, we could implement the string_capitalization invariant using a custom equivalence function:

def string_lower_eq(s1, s2):
    return s1.lower() == s2.lower()

pybryt.Value(correct_answer, equivalence_fn=string_lower_eq)