Quickstart

The following examples serve to get a glimpse on how exercises are modelled in PyRope. For an extensive introduction with detailed explanations, please refer to the sections on Exercises and Running exercises.

We recommend to develop exercises interactively within a Jupyter Notebook. This is by far the simplest and fastest way, as it allows to seamlessly alternate between writing code and testing it. To follow the examples below, you must import the PyRope module beforehand.

import pyrope

Minimal Example

For simplicity, let us start with a static exercise that has single input field with a fixed answer. Copy the following code into a Notebook cell and execute it by pressing Shift+Enter.

class FortyTwo(pyrope.Exercise):

    def problem(self):
        return pyrope.Problem('''
            What is the answer to the Ultimate Question of Life, The Universe,
            and Everything?

            <<answer>>
            ''',
            answer=pyrope.Natural()
        )

    def the_solution(self):    # prefix 'the_' indicates uniqueness
        return 42

This defines the exercise. To see if it runs as expected, execute the following cell.

FortyTwo().run()
Exercise as shown to the learner

To rerun the exercise, simply execute the cell again. If you are not satisfied, you can go back, edit the code in the exercise definition and then run the exercise again to see the effect of your changes.

Scoring

Observe that we do not need to implement a method to score the answer in the example above. This is because PyRope has an auto-scoring mechanism, which by default awards one point if the answer is correct and zero if not. The correctness of the answer is deduced from comparing it to the given sample solution. If you are not satisfied with the auto-scoring, you can implement your own as follows.

class FortyTwo(pyrope.Exercise):

    def problem(self):
        return pyrope.Problem('''
            What is the answer to the Ultimate Question of Life, The Universe,
            and Everything?

            <<answer>>
            ''',
            answer=pyrope.Natural()
        )

    def scores(self, answer):
        if answer == 42:
            return 100
        else:
            return 0

    def the_solution(self):
        return 42

In the example above we still need to provide a sample solution. This is because the auto-scoring will deduce the maximal score from it. Omitting the sample solution will result in an error when submitting the answer, since PyRope cannot determine the maximal score.

---------------------------------------------------------------------------
IllPosedError                             Traceback (most recent call last)

[...]

IllPosedError: Unable to determine maximal score for input field 'answer'.

Alternatively, the maximal score can be given explicitly by returning a pair instead of a single number from the scores() method.

class FortyTwo(pyrope.Exercise):

    def problem(self):
        return pyrope.Problem('''
            What is the answer to the Ultimate Question of Life, The Universe,
            and Everything?

            <<answer>>
            ''',
            answer=pyrope.Natural()
        )

    def scores(self, answer):
        if answer == 42:
            return (100, 100)  # read "100 of 100"
        else:
            return (0, 100)  # read "0 of 100"

Sample Solution

Notice that in the above example the learner does not get the correct solution as feedback for a wrong answer. This is why you should always implement a sample solution. After all, if you can not provide a solution, why should your students?

A unique sample solution is provided via the method the_solution() as above. If the solution is not unique, you must use a_solution() instead.

class Factor(pyrope.Exercise):

    def problem(self):
        return pyrope.Problem(
            'Give a non-trivial divisor of 42: <<answer>>',
            answer=pyrope.Integer(minimum=2, maximum=41)
        )

    def scores(self, answer):
        return 42 % answer == 0

    def a_solution(self):  # prefix 'a_' indicates non-uniqueness
        return 7

In this case we still need to implement the scores() method. Otherwise the auto-scoring cannot determine the correctness of the answer and raises an error when submitting the exercise.

---------------------------------------------------------------------------
IllPosedError                             Traceback (most recent call last)

[...]

IllPosedError: Automatic scoring for input field 'answer' needs a unique sample solution.

Randomisation

Randomised parameters can be generated in the parameters() method and then used in all other methods.

import random

class Product(pyrope.Exercise):

    def parameters(self):
        return dict(
            a=random.randint(2, 9),
            b=random.randint(2, 9),
        )

    def problem(self, a, b):
        return pyrope.Problem(
            'The product of <<a>> and <<b>> is <<answer>>.',
            answer=pyrope.Natural()
        )

    def the_solution(self, a, b):
        return a * b

Implicit solution

Often the sample solution is one of the parameters. In this case, there is no need to implement a sample solution or a scoring method. Instead, you can indicate that an input field has a particular parameter as correct answer by appending an underscore to the parameter name and let PyRope do the rest.

import random

class Product(pyrope.Exercise):

    def parameters(self):
        a = random.randint(2, 9)
        b = random.randint(2, 9)
        return dict(a=a, b=b, product=a*b)

    def problem(self, a, b):
        return pyrope.Problem(
            'The product of <<a>> and <<b>> is <<product_>>.',
            product_=pyrope.Natural()
        )

For input fields using this naming convention, the solution is assumed to be unique. This is why PyRope here automatically inserts the sample solution into the feedback.

Feedback

Apart from the sample solution, which is given automatically, you can give individual feedback based on the parameters and the learner’s answers via the feedback() method.

class Apples(pyrope.Exercise):

    def problem(self):
        return pyrope.Problem(
            '''
            If there are five apples and you take away three,
            how many do you have?

            <<number>>
            ''',
            number=pyrope.Natural()
        )

    def the_solution(self):
        return 3

    def feedback(self, number):
        if number == 3:
            return "Be honest: You knew the quiz, didn't you?"
        return 'You took three apples, so you have three!'

Multiple input fields

If the exercise has more than one input field, then the scores for each input field can be returned in a dictionary. The same holds for the/a sample solution.

import random

class SumAndProduct(pyrope.Exercise):

    def parameters(self):
        a = random.randint(1, 9)
        b = random.randint(1, 9)
        return dict(a=a, b=b)

    def problem(self):
        return pyrope.Problem('''
            * The sum of <<a>> and <<b>> is <<the_sum>>.
            * The product of <<a>> and <<b>> is <<product>>.
            ''',
            the_sum=pyrope.Natural(),
            product=pyrope.Natural(),
        )

    def scores(self, a, b, the_sum, product):
        scores = dict(the_sum=0, product=0)
        if the_sum == a + b:
            scores['the_sum'] = 1
        if product == a * b:
            scores['product'] = 2
        return scores

    def the_solution(self, a, b):
        return dict(the_sum=a+b, product=a*b)

In cases where it is not possible to score input fields individually, you can return an overall score from the scores() method.

import random

class Factorisation(pyrope.Exercise):

    def parameters(self):
        return dict(
            p=random.randint(2, 9),
            q=random.randint(2, 9),
        )

    def problem(self, p, q):
        return pyrope.Problem(
            fr'{p * q} = <<p_>> $\times$ <<q_>>',
            p_=pyrope.Integer(minimum=2),
            q_=pyrope.Integer(minimum=2),
        )

    def scores(self, p, q, p_, q_):
        return p_ * q_ == p * q

Hints

Depending on the configuration, the learner can request one or more hints when stuck with the solution of an exercise. These are provided by the hints() method.

class HelpMe(pyrope.Exercise):

    def problem(self):
        return pyrope.Problem('''
            What is the answer to the Ultimate Question of Life, The Universe,
            and Everything?

            <<answer>>
            ''',
            answer=pyrope.Natural()
        )

    def the_solution(self):
        return 42

    def hints(self):
        yield 'It is a natural number.'
        yield 'You can find it in the "Hitchhiker\'s Guide to the Galaxy".'

Unit testing

Examples may contain inconsistencies, for instance when providing both, a maximal score and a sample solution, as in the following example.

import random

class SmallestPrime(pyrope.Exercise):

    def problem(self):
        return pyrope.Problem(
            r'What is the smallest prime number? <<answer>>',
            answer=pyrope.Natural()
        )

    def scores(self, answer):
        if answer == 2:
            return (1, 1)
        return (0, 1)

    def the_solution(self):
        return 1  # should be two

To avoid this and other common mistakes, you can let PyRope run a couple of automated tests on an exercise.

SmallestPrime().test()
.........F...............
======================================================================
FAIL: test_maximal_total_score_for_sample_solution (pyrope.tests.TestParametrizedExercise.test_maximal_total_score_for_sample_solution) (exercise=<class '__main__.SpotTheError'>)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/pyrope/venv/lib/python3.12/site-packages/pyrope/tests.py", line 101, in wrapped_test
    test(self, pexercise)
  File "~/pyrope/venv/lib/python3.12/site-packages/pyrope/tests.py", line 399, in test_maximal_total_score_for_sample_solution
    self.assertEqual(
AssertionError: 0.0 != 1.0 : The sample solution does not get maximal total score.

----------------------------------------------------------------------
Ran 25 tests in 0.045s

FAILED (failures=1)

This helps to avoid exceptions during exercise runs and to gain confidence in third party exercises obtained from foreign sources.