├── .coveragerc ├── .github └── workflows │ ├── ci.yaml │ └── integration-test.yaml ├── .gitignore ├── .pylintrc ├── LICENSE ├── MJxPrep.js ├── README.md ├── basic_demo.py ├── codecov.yml ├── conftest.py ├── course ├── README.md ├── assets │ └── assets.xml ├── config.example.py ├── course.xml ├── course │ └── course.xml ├── html │ ├── formulainfo.xml │ ├── integralinfo.xml │ ├── intervalinfo.xml │ ├── introduction.xml │ ├── listinfo.xml │ ├── matrixinfo.xml │ ├── singlelistinfo.xml │ ├── stringinfo.xml │ ├── suminfo.xml │ └── usage.xml ├── policies │ ├── assets.json │ └── course │ │ ├── grading_policy.json │ │ └── policy.json ├── problem │ ├── formula1.xml │ ├── formula10.xml │ ├── formula11.xml │ ├── formula12.xml │ ├── formula13.xml │ ├── formula14.xml │ ├── formula2.xml │ ├── formula3.xml │ ├── formula4.xml │ ├── formula5.xml │ ├── formula6.xml │ ├── formula7.xml │ ├── formula8.xml │ ├── formula9.xml │ ├── integral1.xml │ ├── integral2.xml │ ├── integral3.xml │ ├── integral4.xml │ ├── interval1.xml │ ├── interval2.xml │ ├── interval3.xml │ ├── list1.xml │ ├── list2.xml │ ├── list3.xml │ ├── list4.xml │ ├── list5.xml │ ├── list6.xml │ ├── matrix1.xml │ ├── matrix10.xml │ ├── matrix2.xml │ ├── matrix3.xml │ ├── matrix4.xml │ ├── matrix5.xml │ ├── matrix6.xml │ ├── matrix7.xml │ ├── matrix8.xml │ ├── matrix9.xml │ ├── singlelist1.xml │ ├── singlelist2.xml │ ├── singlelist3.xml │ ├── singlelist4.xml │ ├── singlelist5.xml │ ├── string1.xml │ ├── string2.xml │ ├── string3.xml │ ├── string4.xml │ ├── string5.xml │ ├── string6.xml │ ├── sum1.xml │ ├── sum2.xml │ ├── sum3.xml │ ├── sum4.xml │ └── sum5.xml ├── static │ ├── MJxPrep.js │ ├── integral.svg │ ├── mitx.jpg │ └── python_lib.zip ├── upload.sh └── uploader.py ├── docs ├── changelog.md ├── css │ ├── cinder_tweaks.css │ ├── extra.css │ └── readthedocs.css ├── edx.md ├── faq.md ├── graders.md ├── grading_lists │ ├── list_grader.md │ └── single_list_grader.md ├── grading_math │ ├── comparer_functions.md │ ├── formula_grader.md │ ├── functions_and_constants.md │ ├── integral_grader.md │ ├── interval_grader.md │ ├── matrix_grader │ │ ├── input_by_entries.png │ │ ├── input_by_symbols.png │ │ └── matrix_grader.md │ ├── numerical_grader.md │ ├── renderer.md │ ├── sampling.md │ ├── sum_grader.md │ └── user_functions.md ├── index.md ├── item_grader.md ├── plugins.md └── string_grader.md ├── integration_tests └── integration_test.py ├── makezip.sh ├── mitxgraders-js ├── MJxPrep.js ├── MJxPrep.test.js └── package.json ├── mitxgraders ├── __init__.py ├── attemptcredit.py ├── baseclasses.py ├── comparers │ ├── __init__.py │ ├── baseclasses.py │ ├── comparers.py │ └── linear_comparer.py ├── exceptions.py ├── formulagrader │ ├── __init__.py │ ├── formulagrader.py │ ├── integralgrader.py │ ├── intervalgrader.py │ └── matrixgrader.py ├── helpers │ ├── __init__.py │ ├── calc │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── expressions.py │ │ ├── formatters.py │ │ ├── math_array.py │ │ ├── mathfuncs.py │ │ ├── robust_pow.py │ │ └── specify_domain.py │ ├── compatibility.py │ ├── get_number_of_args.py │ ├── math_helpers.py │ ├── munkres.py │ └── validatorfuncs.py ├── listgrader.py ├── matrixsampling.py ├── plugins │ ├── __init__.py │ ├── defaults_sample.py │ └── template.py ├── sampling.py ├── stringgrader.py └── version.py ├── mkdocs.yml ├── pytest.ini ├── python_lib.zip ├── requirements-python311.txt ├── requirements-python38.txt ├── tests ├── __init__.py ├── comparers │ └── test_linear_comparer.py ├── conftest.py ├── formulagrader │ ├── test_formulagrader.py │ ├── test_integralgrader.py │ ├── test_intervalgrader.py │ ├── test_matrixgrader.py │ └── test_sumgrader.py ├── helpers.py ├── helpers │ ├── calc │ │ ├── test_expressions.py │ │ ├── test_expressions_arrays.py │ │ ├── test_math_array.py │ │ ├── test_mathfuncs.py │ │ └── test_specify_domain.py │ └── test_validatorfuncs.py ├── test_base.py ├── test_listgrader.py ├── test_matrixsampling.py ├── test_plugins.py ├── test_sampling.py ├── test_singlelistgrader.py ├── test_stringgrader.py └── test_zip.py ├── tidy.sh └── voluptuous ├── LICENSE ├── __init__.py ├── error.py ├── humanize.py ├── schema_builder.py ├── util.py └── validators.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = mitxgraders/helpers/munkres.py 3 | branch = false 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: [push, pull_request] 3 | jobs: 4 | Python-3-Tests: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: [3.8, 3.11] 9 | steps: 10 | - name: Check out repository code 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | if [ "${{ matrix.python-version }}" == "3.11" ]; then 22 | pip install -r requirements-python311.txt 23 | elif [ "${{ matrix.python-version }}" == "3.8" ]; then 24 | pip install -r requirements-python38.txt 25 | fi 26 | 27 | - name: Run test cases 28 | run: pytest --cov=mitxgraders --cov-report=term-missing 29 | 30 | - name: Run coverage 31 | run: codecov 32 | 33 | NodeJS-Tests: 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | node-version: [16] 38 | steps: 39 | - name: Check out repository code 40 | uses: actions/checkout@v4 41 | 42 | - name: Set up Node.js ${{ matrix.node-version }} 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: ${{ matrix.node-version }} 46 | 47 | - name: Install dependencies 48 | run: | 49 | cd mitxgraders-js 50 | npm install 51 | 52 | - name: Run test cases 53 | run: | 54 | cd mitxgraders-js 55 | npm test 56 | -------------------------------------------------------------------------------- /.github/workflows/integration-test.yaml: -------------------------------------------------------------------------------- 1 | name: Integration Test 2 | 3 | on: 4 | schedule: 5 | # Montlhy at 00:00 6 | - cron: '0 0 1 * *' 7 | 8 | jobs: 9 | edx-platform-integration-test: 10 | name: Integration with Tutor 11 | strategy: 12 | matrix: 13 | # Open edX Version: Sumac 14 | tutor_version: ["<20.0.0"] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | path: mitx-grading-library 21 | 22 | - name: Adjust permissions to execute Tutor commands 23 | run: | 24 | chmod 777 . -R 25 | shell: bash 26 | 27 | - name: Set Tutor environment variables 28 | run: | 29 | cat <> "$GITHUB_ENV" 30 | LMS_HOST=local.edly.io 31 | CMS_HOST=studio.local.edly.io 32 | TUTOR_ROOT=$(pwd) 33 | COURSE_KEY=course-v1:MITx+grading-library+course 34 | EOF 35 | shell: bash 36 | 37 | - name: Install Tutor 38 | run: pip install "tutor${{ matrix.tutor_version }}" 39 | shell: bash 40 | 41 | - name: Install, enable and initialize Tutor Codejail Plugin 42 | run: | 43 | pip install git+https://github.com/edunext/tutor-contrib-codejail 44 | tutor plugins enable codejail 45 | tutor local do init --limit codejail 46 | shell: bash 47 | 48 | - name: Mount Integration Test 49 | run: tutor mounts add cms:mitx-grading-library/integration_tests/integration_test.py:/openedx/edx-platform/integration_test.py 50 | shell: bash 51 | 52 | - name: Launch Tutor 53 | run: tutor local launch -I 54 | shell: bash 55 | 56 | - name: Import MITx Demo Course 57 | run: | 58 | tutor local do importdemocourse -r ${{ github.event.pull_request.head.repo.clone_url }} -d course -v ${{ github.event.pull_request.head.ref }} 59 | shell: bash 60 | 61 | - name: Run integration tests 62 | run: | 63 | tutor local run cms python integration_test.py "$COURSE_KEY" 64 | shell: bash 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | */__pycache__ 3 | *.pytest_cache 4 | *.coverage 5 | *.sublime-project 6 | *.sublime-workspace 7 | *.DS_Store 8 | course/course.tar.gz 9 | course/config.py 10 | site/ 11 | mitxgraders-js/node_modules 12 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | ignored-modules=numpy,numpy.random 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 2 | 3 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 4 | 5 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 6 | 7 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MITx Grading Library 2 | 3 | [![Build Status](https://github.com/mitodl/mitx-grading-library/actions/workflows/ci.yaml/badge.svg)](https://github.com/mitodl/mitx-grading-library/actions) [![Coverage Status](https://codecov.io/gh/mitodl/mitx-grading-library/branch/master/graphs/badge.svg)](https://codecov.io/gh/mitodl/mitx-grading-library) 4 | 5 | A library of graders for edX Custom Response problems. 6 | 7 | Version 3.0.0 ([changelog](docs/changelog.md)) 8 | 9 | Copyright 2017-2024 Jolyon Bloomfield and Chris Chudzicki 10 | 11 | Licensed under the [BSD-3 License](LICENSE). 12 | 13 | We thank the MIT Office of Open Learning for their support. 14 | 15 | **Table of Contents** 16 | 17 | - [Demo Course](#demo-course) 18 | - [Grader Documentation](#documentation-for-edx-course-authors) 19 | - [Local Installation](#local-installation) 20 | - [FAQ](#faq) 21 | 22 | 23 | ## Demo Course 24 | 25 | A demonstration course for the MITx Grading Library can be viewed [here](https://edge.edx.org/courses/course-v1:MITx+grading-library+examples/). The source code for this course is contained in this repository [here](course/). 26 | 27 | 28 | ## Documentation for edX Course Authors 29 | [Extensive documentation](https://mitodl.github.io/mitx-grading-library/) has been compiled for the configuration of the different graders in the library. 30 | 31 | 32 | ## Local Installation 33 | 34 | This is not required but can be useful for testing configurations in Python, rather than in edX. 35 | 36 | To install: 37 | 38 | **Requirements:** An installation of Python 3.8 or 3.11 (current edX versions). 39 | 40 | 0. (Optional) Create and activate a new Python Virtual Environment. 41 | 1. Clone this repository and `cd` into it. 42 | 2. Run `pip install -r requirements-python38.txt` or `pip install -r requirements-python311.txt` to install the requirements based on the Python version you want to test. 43 | 3. Run `pytest` to check that tests are passing. (To invoke tests of just the documentation, you can run the following command: `python -m pytest --no-cov --disable-warnings docs/*`) 44 | 45 | 46 | ## FAQ 47 | 48 | * What's this `voluptuous` thing? 49 | 50 | [Voluptuous](https://github.com/alecthomas/voluptuous) is a library that handles configuration validation, while giving (hopefully) meaningful error messages. We use it to automate the checking of the configurations passed into the `mitxgraders` library. They need to be packaged together in the `python_lib.zip` file. 51 | -------------------------------------------------------------------------------- /basic_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | basic_demo.py 3 | 4 | A simple demo of using the grading library 5 | """ 6 | import pprint 7 | from mitxgraders import * 8 | 9 | pp = pprint.PrettyPrinter(indent=4) 10 | 11 | list_grader = SingleListGrader( 12 | answers=['cat', 'dog', 'unicorn'], 13 | subgrader=StringGrader() 14 | ) 15 | 16 | answers = "unicorn, cat, dog, fish" 17 | demo = list_grader(None, answers) 18 | pp.pprint(demo) 19 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff" 3 | behavior: new 4 | require_changes: false 5 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | try: 4 | # Prior to version 1.13, numpy added an extra space before floats when printing arrays 5 | # We use 1.6 for Python 2 and 1.16 for Python 3, so the printing difference 6 | # causes problems for doctests. 7 | # 8 | # Setting the printer to legacy 1.13 combined with the doctest directive 9 | # NORMALIZE_WHITESPACE fixes the issue. 10 | np.set_printoptions(legacy='1.13') 11 | body = "# Setting numpy to print in legacy mode" 12 | msg = "{header}\n{body}\n{footer}".format(header='#'*40, footer='#'*40, body=body) 13 | print(msg) 14 | except TypeError: 15 | pass 16 | -------------------------------------------------------------------------------- /course/README.md: -------------------------------------------------------------------------------- 1 | Example edX Course 2 | ================== 3 | 4 | This is the source code for an example edX course demonstrating the various capabilities of the MITx Grading Library. You can view this course to see live examples in action [here](https://edge.edx.org/courses/course-v1:MITx+grading-library+examples/) (hosted on edX edge). 5 | -------------------------------------------------------------------------------- /course/assets/assets.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /course/config.example.py: -------------------------------------------------------------------------------- 1 | """ 2 | config.py 3 | 4 | Sets username and password for edX login 5 | 6 | Note that config.py should be in the .gitignore file 7 | """ 8 | username = "email@address.com" 9 | password = "edx password" 10 | -------------------------------------------------------------------------------- /course/course.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /course/html/formulainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Any time you want to grade numbers or formulas, these are your graders. The basic grader is FormulaGrader. NumericalGrader is a specialized version of FormulaGrader that cannot have any unknown variables, but otherwise, just sets some defaults a bit differently to FormulaGrader. If you want to handle vectors or matrices, you want MatrixGrader, which is another specialized version of FormulaGrader.

4 | 5 |

These graders have significant extensions over the edX capabilities, as we demonstrate here. Furthermore, we use our own parser for these graders, and have incorporated substantial improvements over the edX formularesponse and numericalresponse problem types.

6 | 7 | 8 | -------------------------------------------------------------------------------- /course/html/integralinfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

It is possible to construct specialized-purpose graders that leverage the framework of the library. An example of this is IntegralGrader, which grades the construction of an integral. We include are a few examples to demonstrate how this works.

4 | 5 | 6 | -------------------------------------------------------------------------------- /course/html/intervalinfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Here is another example of a special-purpose grader that leverages multiple aspects of the library to perform its task. In this section, we demonstrate the IntervalGrader, which allows students to enter mathematical intervals, and be graded not only on the endpoints of the intervals, but also whether the intervals are open or closed.

4 | 5 | 6 | -------------------------------------------------------------------------------- /course/html/introduction.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

This course exists as a demonstration course to show off the features and capabilities of the MITx Grading Library. Please feel free to explore the course.

4 | 5 |

The MITx Grading Library is an open source python grading library for edX. The full source code (as well as the source code for this demonstration course) can be found on github, while comprehensive documentation can be found here.

6 | 7 | 8 |

About

9 | 10 |

edX provides a variety of default problem types. However, it's very easy to want to do something beyond the basics. Using customresponse problems and python graders, it's possible to create much richer problems, but each one has to be individually coded, and this is very time consuming. Until now.

11 | 12 |

Having tired of recreating the wheel multiple times, we decided to make a python grading library for edX that could handle almost anything you could want to throw at it. The library is designed to be simple to use, give useful error messages, and be incredibly flexible. If what we provide can't handle what you want to do, then you can always write a plugin that leverages the infrastructure we've already created. While we were at it, we implemented a number of long-overdue features and usability improvements over the edX implementation.

13 | 14 |

We encourage you to play around with the examples that this demonstration course contains, so that you can get an idea of what is possible using this library. Further examples are available in the full documentation included with the library.

15 | 16 | 17 |

How does it work?

18 | 19 |

The library is broken up into individual "graders" to grade different types of problems. Some simple graders just grade a single input, such as StringGrader and FormulaGrader. More complicated graders combine simpler graders to grade multiple entries, such as ListGrader. Examples of how these all work are given in this course.

20 | 21 | 22 |

Where can I get it?

23 | 24 |

This library is made freely available on github, where you can see the full source code, including the source for this demonstration course. Comprehensive documentation is available here. We have endeavoured to make this library as simple as possible to set up and use. Just download the python_lib.zip file and start using it!

25 | 26 | 27 |

Legal details and Credit

28 | 29 |

This library is released for use under the BSD-3 license. See the github repository for full license details.

30 | 31 |

The MITx Grading Library was created by Jolyon Bloomfield and Christopher Chudzicki. Copyright 2017-2020.

32 | 33 | 34 | -------------------------------------------------------------------------------- /course/html/listinfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Whenever there is more than one input to a logical problem unit, you will need to use a ListGrader to combine the inputs. ListGraders allow you to have items that are ordered/unordered and in groups. You can even nest ListGraders to construct problems that are very simple on pen and paper, but until now have been basically impossible in edX.

4 | 5 | 6 | -------------------------------------------------------------------------------- /course/html/matrixinfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | MatrixGrader is an extended version of FormulaGrader used to grade mathematical expressions containing scalars, vectors, and matrices. Authors and students may enter matrix (or vector) expressions by using variables sampled from matrices, or by entering a matrix entry-by-entry. 5 |

6 | 7 | 8 | -------------------------------------------------------------------------------- /course/html/singlelistinfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

A SingleListGrader grades a list of responses in a single textbox. The responses can be separated by whatever character you like, and graded using StringGrader/FormulaGrader/etc. These are particularly useful when you want a list of multiple integers.

4 | 5 | 6 | -------------------------------------------------------------------------------- /course/html/stringinfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

The StringGrader grader grades raw text (also known as "strings"). It is our most basic grader, which makes it a good place to start.

4 | 5 | 6 | -------------------------------------------------------------------------------- /course/html/suminfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Another example of a specialized-purpose graders that leverage the framework of the library is the SumGrader, which grades the construction of an summation. We include are a few examples to demonstrate how this works.

4 | 5 | 6 | -------------------------------------------------------------------------------- /course/html/usage.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Studio

4 | 5 |

The library is straightforward to use. To set up, you need to do two things:

6 | 7 |
    8 |
  • Download python_lib.zip
  • 9 |
  • Upload python_lib.zip as a static asset (Warning: if you are already using a custom python grading library, then you will need to combine the two!)
  • 10 |
11 | 12 |

To use in a problem, create an advanced problem, and enter the following to see if things are working:

13 | 14 |
<problem>
15 | <script type="text/python">
16 | from mitxgraders import *
17 | grader = StringGrader()
18 | </script>
19 | 
20 | <customresponse cfn="grader">
21 |     <textline correct_answer="cat"/>
22 | </customresponse>
23 | </problem>
24 | 25 | 26 |

XML

27 | 28 |

Very similar to the Studio usage above. To set up:

29 | 30 |
    31 |
  • Download python_lib.zip
  • 32 |
  • Put python_lib.zip in your static folder (Warning: if you are already using a custom python grading library, then you will need to combine the two!)
  • 33 |
34 | 35 |

To use, create a problem, and use the above code to test it out.

36 | 37 | 38 |

LaTeX2edX

39 | 40 |

To use this in LaTeX2edX, put the python_lib.zip file in your static folder, as for the XML usage. For a problem script, it's best to put the python in an edXscript environment:

41 | 42 |
\begin{edXscript}
43 | from mitxgraders import *
44 | grader = StringGrader()
45 | \end{edXscript}
46 | 47 |

Next, construct an edXabox command as follows:

48 | 49 |
\edXabox{type="custom" expect="something" cfn="grader"}
50 | 51 |

Alternatively, you can put the customresponse tags in an edXxml tag:

52 | 53 |
\edXxml{<customresponse cfn="grader">
54 |     <textline correct_answer="cat"/>
55 | </customresponse>}
56 | 57 | 58 | -------------------------------------------------------------------------------- /course/policies/assets.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /course/policies/course/grading_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "GRADER": [ 3 | { 4 | "drop_count": 0, 5 | "min_count": 1, 6 | "short_label": "HW", 7 | "type": "Homework", 8 | "weight": 1 9 | } 10 | ], 11 | "GRADE_CUTOFFS": { 12 | "Pass": 0.5 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /course/policies/course/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "course/course": { 3 | "cert_html_view_enabled": false, 4 | "display_name": "MITx Grading Library", 5 | "course_image": "mitx.jpg", 6 | "display_organization": "MIT", 7 | "display_coursenumber": "Demonstration Course", 8 | "language": "en", 9 | "start": "2017-01-01T10:00:00Z", 10 | "end": "2050-01-01T10:00:00Z", 11 | "showanswer": "always", 12 | "tabs": [ 13 | { 14 | "course_staff_only": false, 15 | "name": "Home", 16 | "type": "course_info" 17 | }, 18 | { 19 | "course_staff_only": false, 20 | "name": "Course", 21 | "type": "courseware" 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /course/problem/formula1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

The following is a simple formularesponse problem, except that we're using our library instead of edX. There is a variable m, and the answer is m*(1-sin(2*m)).

4 | 5 |

The interesting part is what happens when you have a mistake in your input. Try the following:

6 | 7 |
    8 |
  • m*(1-sin(2*m) (missing closing parentheses)
  • 9 |
  • m*(1-sin(2*m))) (extra closing parentheses)
  • 10 |
  • m*(1-Sin(2*m)) (wrong capitalization for function name)
  • 11 |
  • m(1-sin(2*m)) (forgot multiplication before brackets)
  • 12 |
  • m*(1-sin(2m)) (forgot multiplication in 2*m)
  • 13 |
  • x*(1-sin(2*m)) (unknown variable x)
  • 14 |
  • f(1-sin(2*m)) (unknown function f)
  • 15 |
  • 1/0 (division by zero)
  • 16 |
  • 10^400 (overflow)
  • 17 |
18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

For comparison, the following box is an edX formularesponse grader with exactly the same configuration.

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |

We recommend looking at the code here to see just how straightforward setting this up is!

37 | 38 |

Note: We have now managed to merge a number of these enhancements into edX!

39 | 40 |

View source

41 | 42 |
43 | -------------------------------------------------------------------------------- /course/problem/formula10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

When a student submits several mathematical inputs as part of one problem, it is sometimes useful to grade these inputs in comparison to each other. This problem uses sibling variables to score subsequent answers based off of the previous one. Sibling variables are available to FormulaGraders, NumericalGraders, and MatrixGraders when used inside an ordered ListGrader.

4 | 5 |

Use Newton's Method on \(f(x)=x^2 - 9\) to calculate the square root of 9. Use anything in the interval \( (4, 6) \) as your starting value, \(x_0\). Round answers to no fewer than 6 decimal places.

6 | 7 |

Suggested inputs:

8 | 9 |
    10 |
  • [5, 3.4, 3.0235294] is a correct input
  • 11 |
  • [4.5, 3.25, 3.0096154] is a correct input
  • 12 |
  • Try changing one or two values in the lists above.
  • 13 |
14 | 15 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
Value
\(x_0\)
\(x_1\)
\(x_2\)
66 |
67 | 68 |

We envisage that this functionality will be particularly useful in multi-part exam problems, where if students make a mistake in an early part, partial credit can be awarded for carrying that mistake forwards to subsequent parts.

69 | 70 |

Resources

71 | 79 | 80 |
81 | -------------------------------------------------------------------------------- /course/problem/formula11.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

As a further example of the extended function listing in the library, here we demonstrate a function that takes multiple inputs: the Kronecker delta.

4 | 5 |

The Kronecker delta often appears in math expressions. To invoke it, use kronecker(x, y). Note that when using the AsciiMath preprocessor, this will render correctly. We decided against calling it delta(x, y), as [mathjaxinline]\delta[/mathjaxinline] is a common user function name, and we do not support name overloading of functions.

6 | 7 |

When using a Kronecker delta in a solution, we strongly recommend using integer sampling in a small range (e.g., 1 to 4) and using many samples (e.g., 30) so that the various possible permutations are triggered in the grading.

8 | 9 |

What is the derivative [mathjaxinline]\frac{\partial x_i}{\partial x_j}[/mathjaxinline]?

10 | 11 |

Suggested inputs:

12 |
    13 |
  • kronecker(i, j) is the correct input
  • 14 |
  • kronecker(i+1, j+1) is also correct
  • 15 |
  • kronecker(i+1, j) is incorrect
  • 16 |
  • 1 and 0 are incorrect, but may be graded as correct in very rare circumstances, depending on the ranges and number of samples used
  • 17 |
18 | 19 | 31 | 32 |

[mathjaxinline]\displaystyle \frac{\partial x_i}{\partial x_j} =[/mathjaxinline]

33 | 34 | 35 | 36 | 37 |

View source

38 | 39 |
40 | -------------------------------------------------------------------------------- /course/problem/formula12.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

This powerful feature allows you to assign automatic partial credit in formula problems when the student's input is off by a numerical factor (e.g., 2, pi, -1, sqrt(3), etc). It works by performing sampling as usual, and then performing checking to see if the student's results are described by a linear transformation of the answer. Partial credit can then be awarded based on which linear relation worked, and a message can also be included.

4 | 5 |
    6 |
  • Equality: The answer and the student input were the same
  • 7 |
  • Proportionality: The answer and the student input were related by a constant multiplicative factor
  • 8 |
  • Offset: The answer and the student input differed only by a constant offset (useful for arbitrary offsets and integration constants)
  • 9 |
  • Linear: The answer and the student input were related by a linear transformation \(y = mx + c\)
  • 10 |
11 | 12 |

Below, the correct answer is x. Different messages and credit have been assigned to the different possible relations. It is also possible to turn off credit for whichever relations are desired. Note that all valid relations are checked, and the most highly-scoring one determines the students' grade. Try the following inputs:

13 | 14 |
    15 |
  • x
  • 16 |
  • x+1
  • 17 |
  • 2*x
  • 18 |
  • 2*x+1
  • 19 |
  • x^2
  • 20 |
  • 0
  • 21 |
22 | 23 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |

If the instructor answer is 0, then only equality and offsets can be awarded any credit.

49 | 50 |

The default comparer, used when no other comparer is specified, is called equality_comparer. This example also demonstrates changing that default.

51 | 52 |

While the default comparer can be assigned course-wide through the use of plugins, we recommend against doing so, as you will not always want to award such partial credit (for example, when dimensional analysis trivially provides the algebraic dependence on variables). We recommend constructing the desired partial credit comparer in a plugin, and invoking it using FormulaGrader.set_default_comparer(comparer) in problems as necessary.

53 | 54 |

View source

55 | 56 |
57 | -------------------------------------------------------------------------------- /course/problem/formula13.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Sometimes, you may wish to apply a transforming function on both the student input and instructor answer before performing the comparison. This example demonstrates such a use case.

4 | 5 |

Here, the problem explicitly requests an equality after the imaginary part of the input is taken. The expected answer is x, but adding a real component to the answer should still be graded as correct. Try entering x+1 (correct) and x+i (incorrect). (Note that x is sampled as a complex variable.)

6 | 7 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 36 | 39 | 40 |
31 | [mathjaxinline]\displaystyle f = \mathrm{Im} \bigg([/mathjaxinline] 32 | 34 | 35 | 37 | [mathjaxinline]\bigg)[/mathjaxinline] 38 |
41 |
42 | 43 |

The default comparer, used when no other comparer is specified, is called equality_comparer. This example also demonstrates changing that default.

44 | 45 | 46 |

View source

47 | 48 |
49 | -------------------------------------------------------------------------------- /course/problem/formula14.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

If you want to have multiple numerical inputs and grade them together using a custom function, you can use custom comparers to do the job straightforwardly.

4 | 5 |

Enter two numbers, \(a\) and \(b\), subject to the condition that \(a > 2 b\).

6 | 7 | 35 | 36 | 37 |

\(a = \)

38 |
39 |

\(b = \)

40 |
41 | 42 |

To accomplish this, we use a ListGrader with partial_credit set to False, so that either the pair is correct, or the pair is incorrect. The first entry is graded using a between_comparer with a range of \(-\infty\) to \(\infty\), while the second is graded using our own custom comparer. To get access to the value of the first entry in the custom comparer, we use sibling variables.

43 | 44 |

View source

45 | 46 |
47 | -------------------------------------------------------------------------------- /course/problem/formula2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

We have significantly expanded the list of mathematical functions that the grader recognizes. Functions like min, max, floor, ceil, arctan2, and a variety of trigonometric functions have been included, as well as functions specific to vector/matrix manipulation.

4 | 5 |

In this example, we demonstrate the use of the factorial function.

6 | 7 |

What is the Taylor series expansion of \(\exp(x)\) about \(x = 0\)?

8 | 9 |

Suggested answers:

10 | 11 |
    12 |
  • x^n/fact(n)
  • 13 |
  • x^n/fact(n-1)
  • 14 |
  • x^n/fact(n+1/2)
  • 15 |
16 | 17 | 27 | 28 |

[mathjaxinline]e^x = \sum_{n = 0}^\infty[/mathjaxinline]

29 | 30 | 31 | 32 | 33 |

Note that we use the gamma function to compute factorials, so expressions like fact(1.5) are accepted!

34 | 35 |

When using textline input (as in the above example), the math preview doesn't natively recognize fact() as a function. This affects a number of other functions too, including a number of standard edX functions. We've developed a series of renderer definitions as a javascript library to teach the renderer how to make it look pretty. These definitions are loaded through the MathJax preprocessor in the textline tag. See the documentation for details.

36 | 37 |

View source

38 | 39 |
40 | -------------------------------------------------------------------------------- /course/problem/formula3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

We have expanded the function evaluations to return complex numbers where appropriate. This means that you no longer need to be very careful about ensuring you set your ranges so that that square roots never act on a negative number! Here's an example where the answer is sqrt(1-x), and x is sampled from 0 to 10. edX will basically always grade the student wrong in this situation. Here's our take.

4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 |

You can also specify that a variable to be sampled is complex. We have added functions re, im, abs and conj to allow for manipulating complex expressions. Here is an example that asks for the modulus of z squared. You can enter abs(z)^2, z*conj(z), re(z)^2+im(z)^2, or whatever else does the job. Note that z^2 will be graded incorrect.

20 | 21 | 30 | 31 | 32 | 33 | 34 | 35 |

Note that re and im display correctly as functions, and conj displays with a nice overline. To accomplish this, we use AsciiMath renderer definitions loaded as a javascript file. There is also an option to display conj with a star instead of a bar. See the documentation for details.

36 | 37 | 38 | 39 |

View source

40 | 41 |
42 | -------------------------------------------------------------------------------- /course/problem/formula4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Variable names can now include primes at the end! Here, the answer is x'+v*t (a Galilean change of coordinate).

4 | 5 | 10 | 11 |

[mathjaxinline]x =[/mathjaxinline]

12 | 13 | 14 | 15 | 16 |

You can also use tensor notation for variable names. Here, the answer is T_{x}^{13} + 2*T_{y}^{13}. Notice that the preview box styles these correctly. The caret to indicate raised indices is not confused with the caret to indicate powers.

17 | 18 | 24 | 25 |

[mathjaxinline]x =[/mathjaxinline]

26 | 27 | 28 | 29 | 30 |

Update: These enhancements have now been added to edX!

31 | 32 |

View source

33 | 34 |
35 | -------------------------------------------------------------------------------- /course/problem/formula5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

It is possible to specify user-defined constants and functions. Here, we add a constant tau (= 2*pi), and also define the Heaviside step function H(x). You can also specify an unknown function, for which we sample a well-behaved random function. In the following example, the answer is H(x)*f''(tau), where f'' is a random function.

4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 |

Try typing 1/H(x) in the box above. Do you see a problem? Try the box below - it's exactly the same, except that we've loaded our AsciiMath preprocessor to ensure that the rendering is picture perfect. We strongly recommend using our AsciiMath renderer to ensure that students see a math preview that matches with what they should be writing on paper.

31 | 32 | 33 | 34 | 35 | 36 |

View source

37 | 38 |
39 | -------------------------------------------------------------------------------- /course/problem/formula6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Sometimes you want students to answer a question like "What is \(\sin(2x)\)?". The answer you want is "2*sin(x)*cos(x)". However, students can type in "sin(2*x)" and will still be graded correct.

4 | 5 |

We introduce a number of tools to address this. The first is the concept of whitelists and blacklists. It's possible to set a blacklist of functions, which disallows the use of those mathematical functions in the answer. It's also possible to instead have a whitelist, where you specify the only functions that you want students to have access to.

6 | 7 |

You can also require the use of a certain function in the answer. This may sound good, but remember that "0*cos(1)" technically makes use of the cos function.

8 | 9 |

Finally, you can also specify forbidden strings. These are strings that are not allowed to be used in the answer. In the above example, disallowing "+x", "x+", "x*", "*x", "-x" "x-" and "x/" should stop students from entering anything like "sin(2*x)".

10 | 11 |

The answer to the below question is 2*sin(x)*cos(x). You can try entering sin(2*x) in any clever form, but most expressions will be stymied (it is possible to get around it though - can you figure out how?).

12 | 13 | 22 | 23 | 24 | 25 | 26 | 27 |

View source

28 | 29 |
30 | -------------------------------------------------------------------------------- /course/problem/formula7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Often, there are multiple ways of writing a result, depending on the variables you use. Sometimes, you want students to have flexibility in the use of variables. This is particularly common in the case of polar coordinates, where you may want \(x\), \(y\) and \(r\) to appear.

4 | 5 |

The answer to the below question is x/r. You can also enter x/sqrt(x^2+y^2).

6 | 7 | 16 | 17 | 18 | 19 | 20 | 21 |

View source

22 | 23 |
24 | -------------------------------------------------------------------------------- /course/problem/formula8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

By default, FormulaGrader numerically samples the author formula and student formula then compares the numerical samples for equality (within bounds specified by tolerance). Occasionally, it can be useful to compare author and student formulas in some other way. For example, if grading angles in degrees, it may be useful to compare formulas modulo 360.

4 | 5 |

The answer to the below question is b^2/a. You can enter b^2/a + 2*360, or plus any integer multiple of 360.

6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 |

We have a number of built-in comparison functions for various purposes, and it is also straightforward to write your own (see this example).

25 | 26 |

View source

27 | 28 |
29 | -------------------------------------------------------------------------------- /course/problem/formula9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

If you have a system that contains a large or infinite number of numbered coefficients such as \(a_1\), \(a_2\), etc, it can be a pain to initialize all of these variables as independent variables that the grader should accept. Numbered variables allows you to specify that "a" is a numbered variable, and the system will then accept any entry of the form a_{##} where ## is an integer.

4 | 5 |

The answer to the problem below is a_{0} + a_{1} + a_{-1}. Try including a_{42} in your expression. The grader will be happy to parse your expression and grade you appropriately.

6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

View source

17 | 18 |
19 | -------------------------------------------------------------------------------- /course/problem/integral1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

This style of problem is used in an introductory calculus course, to assess students on setting up an integral. It works by comparing the value of the integral that the student inputs to the value of the integral in the answer. Thus, different variables of integration can be used, and equivalent integrals are graded correct. (Credit: Jennifer French, MIT)

4 | 5 |

The below plot is a graph of the function \(y = e^x\).

6 |
7 | 8 |
9 |

Write the area of the shaded region as a definite integral.

10 | 11 | 12 | 23 | 24 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 55 | 59 | 60 | 61 | 64 | 65 | 66 |
48 | 49 |
53 |

\( \displaystyle \huge{ \int }\)

54 |
56 |
57 | 58 |
62 | 63 |
67 |
68 |
69 | 70 |

While the expected answer is 1, e^x and 0 (top to bottom), by making a substitution \(x = 2u\), an equivalent integral is 0.5, 2*e^(2*x), 0 (top to bottom). As these integrals integrate to the same value, they will both be graded correct.

71 | 72 |

View source

73 | 74 |
75 | -------------------------------------------------------------------------------- /course/problem/integral2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

This is the same problem as previously, except now, the student can choose their variable of integration.

4 | 5 | 6 | 17 | 18 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 51 | 55 | 58 | 62 | 63 | 64 | 67 | 68 | 69 |
44 | 45 |
49 |

\( \displaystyle \huge{ \int }\)

50 |
52 |
53 | 54 |
56 |

\(d\)

57 |
59 |
60 | 61 |
65 | 66 |
70 |
71 |
72 | 73 |

Try the following inputs:

74 | 75 |
    76 |
  • 1, e^x, x, 0 (top to bottom, left to right)
  • 77 |
  • 1, e^u, u, 0 (top to bottom, left to right)
  • 78 |
  • 0.5, 2*e^(2*u), u, 0 (top to bottom, left to right)
  • 79 |
80 | 81 |

View source

82 | 83 |
84 | -------------------------------------------------------------------------------- /course/problem/integral3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

In this example, there is only one box for students to fill in: the integrand. IntegralGrader is still useful in this case, as there still substitutions that can be made that keep the limits invariant. Note that the limits of integration are infinite in this case, showing off the power of out integrator.

4 | 5 | 6 | 17 | 18 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 44 | 45 | 46 |
38 |

\( \displaystyle \huge{ \int }_0^\infty\)

39 |
41 |
42 | 43 |
47 |
48 |
49 | 50 |

Try the following inputs:

51 | 52 |
    53 |
  • e^(-x^2/2)
  • 54 |
  • e^(-x/2)/2/sqrt(x)
  • 55 |
56 | 57 |

View source

58 | 59 |
60 | -------------------------------------------------------------------------------- /course/problem/integral4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

In this problem, we show that the grader can even handle complex integration (when the option is turned on). Note that it will struggle if the integrand is highly oscillatory or the integral is slowly convergent, however.

4 | 5 | 6 | 17 | 18 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 45 | 46 | 47 |
39 |

\( \displaystyle \huge{ \int }_0^\pi\)

40 |
42 |
43 | 44 |
48 |
49 |
50 | 51 |

Try the following inputs:

52 | 53 |
    54 |
  • sin(x)
  • 55 |
  • -i*exp(i*x)
  • 56 |
57 | 58 |

View source

59 | 60 |
61 | -------------------------------------------------------------------------------- /course/problem/interval1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

IntervalGrader is a specialized grader to grade two mathematical expressions, as well as an opening and a closing character. Below is a typical example.

4 | 5 |

In what interval is \(x - x^2\) positive semidefinite? Use () to denote open intervals and [] to denote closed intervals, and separate the limits with a comma.

6 | 7 |

Suggested answers:

8 | 9 |
    10 |
  • [0, 1] (correct answer)
  • 11 |
  • [0, 2]
  • 12 |
  • (0, 1)
  • 13 |
14 | 15 | 18 | 19 |

\(x \in\)

20 | 21 | 22 | 23 | 24 | 25 |

Each limit is a mathematical expression that can be parsed. Also, infty is understood to mean infinity, so you can have an interval end at infinity.

26 | 27 |

Where is \(1/(x - \sqrt{2})\) positive?

28 | 29 |

\(x \in\)

30 | 31 | 32 | 33 | 34 | 35 |

Note: We have turned off the AsciiMath Preprocessor for these inputs, as otherwise, square brackets will render as column vectors!

36 | 37 |

View source

38 | 39 |
40 | -------------------------------------------------------------------------------- /course/problem/interval2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

The partial credit options on IntervalGrader are completely customizable. The following example has the correct answer of [0, 1). Almost every possible type of option is included in the below example, so try all sorts of different things to see what different messages you get, as well as the different grades you receive.

4 | 5 |

Suggested answers:

6 | 7 |
    8 |
  • [0, 1) (correct answer)
  • 9 |
  • [0, 2)
  • 10 |
  • (1, 0]
  • 11 |
  • [0, 1]
  • 12 |
  • (0, 1)
  • 13 |
  • [0, 2)
  • 14 |
  • [-1, 2)
  • 15 |
16 | 17 | 18 | 39 | 40 |

\(x \in\)

41 | 42 | 43 | 44 | 45 | 46 |

View source

47 | 48 |
49 | -------------------------------------------------------------------------------- /course/problem/interval3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

As IntervalGrader is an ItemGrader, it can be nested in lists. Here is an example of such a usage.

4 | 5 |

In what intervals is \(x(x-1)(x+1)\) positive? Enter your answer as a semicolon-separated list of intervals, using infty for \(\infty\). Your intervals may be in any order.

6 | 7 |
    8 |
  • (-1, 0); (1, infty) (correct answer)
  • 9 |
  • (-1, 0)
  • 10 |
  • (-1, 0); (1, 2); (3, infty)
  • 11 |
12 | 13 | 20 | 21 |

\(x \in\)

22 | 23 | 24 | 25 | 26 |

View source

27 | 28 |
29 | -------------------------------------------------------------------------------- /course/problem/list1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

A common problem is to have two answers, and want them to be able to be entered in any order, but not the same answer twice. Here is how to do that with a ListGrader, where you specify subgraders to grade the individual entries.

4 | 5 |

What are the two roots of unity? (Answer: +1 and -1)

6 | 7 | 14 | 15 | 16 |

First answer:

17 |
18 |

Second answer:

19 |
20 | 21 |

Try testing different orderings to your answer, entering the same answer twice, etc.

22 | 23 |

View source

24 | 25 |
26 | -------------------------------------------------------------------------------- /course/problem/list2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

It's possible to have a SingleListGrader inside a ListGrader. Separate the numbers between 1 and 10 inclusive into even and odd numbers. Put the odd numbers in one box, and the even numbers in the other. Order is unimportant.

4 | 5 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 |

Note that partial credit is awarded if applicable.

25 | 26 |

View source

27 | 28 |
29 | -------------------------------------------------------------------------------- /course/problem/list3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Often you will want to group input. There are three lions, two dogs and one bird. In the following boxes, write the singular of each animal, and the corresponding number of each animal. Order is unimportant, but the pairing must be right.

4 | 5 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
AnimalNumber
40 |
41 | 42 |

Note that such questions are often most cleanly presented using an HTML table, as shown here.

43 | 44 |

View source

45 | 46 |
47 | -------------------------------------------------------------------------------- /course/problem/list4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Sometimes you will want partially-ordered input. In the following example, we want "Halloween" in the first box, and "ghost", "bat" and "pumpkin" in any order in the other boxes.

4 | 5 | 22 | 23 | 24 |

Event:

25 |
26 |

Object:

27 |
28 |

Object:

29 |
30 |

Object:

31 |
32 | 33 |

Note that repeating an object will be graded as incorrect.

34 | 35 |

View source

36 | 37 |
38 | -------------------------------------------------------------------------------- /course/problem/list5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Here is the jewel in the crown of ListGrader: the ability to nest ListGraders.

4 | 5 |

Find the eigenvalues and eigenvectors of the matrix

6 | 7 | \[M = 8 | \begin{bmatrix} 9 | 1 & x \\ 10 | x & -1 11 | \end{bmatrix} 12 | \] 13 | 14 |

Answer: (pairs may be in any order, but correct vector must go with correct value)

15 |
    16 |
  • 1st pair: +sqrt(1+x^2), [1+sqrt(1+x^2), x]
  • 17 |
  • 2nd pair: -sqrt(1+x^2), [1-sqrt(1+x^2), x]
  • 18 |
19 | 20 |

The eigenvectors can be multiplied by any constant. Try using 5*[1-sqrt(1+x^2), x] for the second eigenvector, for example.

21 | 22 |

In this problem, partial credit is only awarded if you get both the eigenvalue and eigenvector in the pairing correct. Getting one correct but the other incorrect will result in no credit for that pair.

23 | 24 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
EigenvalueEigenvector
1st Eigenpair
2nd Eigenpair
84 |
85 | 86 |

View source

87 | 88 |
89 | -------------------------------------------------------------------------------- /course/problem/list6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

You can use CSS and HTML tables combined with ListGrader to style your problems however you want them to look. Below is an example of a double integral. While it doesn't have the fancy invariance under transformations that IntegralGrader offers for single integrals, if your variables have physical significance, then the invariance is likely unnecessary.

4 | 5 |

Enter the following integral:

6 | 7 | [mathjax]\Phi = \int_0^a dx \int_0^b dy \frac{1}{\sqrt{x^2 + y^2 + a^2}}[/mathjax] 8 | 9 | 21 | 22 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 50 | 51 | 52 | 53 | 54 | 57 | 60 | 64 | 65 | 66 | 67 | 70 | 73 | 74 | 75 | 76 |
45 | 46 | 48 | 49 |
\(\displaystyle \Phi =\) 55 |

\( \displaystyle \huge\int\)

56 |
58 |

\( \displaystyle \huge\int\)

59 |
61 |
62 | 63 |
68 | 69 | 71 | 72 |
77 | 78 |
79 | 80 |

View source

81 | 82 |
83 | -------------------------------------------------------------------------------- /course/problem/matrix1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 |

This is a demo problem with 3 variables: A and B are 2 by 2 matrices, while v is a vector of length 2.

19 | 20 |

Try inputting different forms of the correct answer, as well as different incorrect answers:

21 |
    22 |
  • 4*A*B^2*v is correct
  • 23 |
  • A*(2*B^2 + 2*B*I*B)*v is also correct; here I is the 2 by 2 identity matrix.
  • 24 |
  • 4*B*A*B*v is incorrect (non-commutative)
  • 25 |
  • The following answers will raise interesting errors: 26 |
      27 |
    • A + v
    • 28 |
    • v^2
    • 29 |
    • A^2.5
    • 30 |
    • 4*A*B^2
    • 31 |
    32 |
  • 33 |
34 | 35 | 36 | 37 | 38 | 39 |

View source

40 | 41 |
42 | -------------------------------------------------------------------------------- /course/problem/matrix10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

If you need to define an author-specified function in a MatrixGrader problem, you can use the specify_domain decorator to provide informative error messages to students when things go wrong. (For example, if a student inputs a scalar to a function whose domain is vectors).

4 | 5 |

This problem is intended only to show off the syntax (see source code below) and error messages provided by specify_domain.

6 | 7 |

Available variables and functions:

8 |
    9 |
  • func(arg1, arg2, arg3) expects a scalar arg1, a 3 by 2 matrix arg2, and a 3-component vector arg3.
  • 10 |
  • a is a scalar
  • 11 |
  • b is a 3 by 2 matrix
  • 12 |
  • c is a 3-component vector
  • 13 |
14 | 15 |

Suggested inputs:

16 |
    17 |
  • f(a, b, c) is the correct answer
  • 18 |
  • f(a, [1, 2], b), or anything where arguments have the wrong shape, will produce interesting error messages
  • 19 |
20 | 21 | 42 | 43 | 44 | 45 | 46 | 47 |

48 |

Resources

49 | 57 |

58 | 59 |
60 | -------------------------------------------------------------------------------- /course/problem/matrix2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 |

A linear transformation \(\mathcal{T}: \mathbb{R}^2 \rightarrow \mathbb{R}^2\) rotates vectors counter-clockwise by angle \(\theta\). Enter the matrix representation of \(\mathcal{T}\) acting on a vector \(\vec{v} = (x, y)\).

15 | 16 |

This problem requires entry-by-entry input of a matrix. Suggested inputs:

17 | 18 |
    19 |
  • [[cos(theta), -sin(theta)], [sin(theta), cos(theta)]], correct answer
  • 20 |
  • -[[-cos(theta), sin(theta)], [-sin(theta), -cos(theta)]], also correct
  • 21 |
  • Partial credit with feedback has been turned on for this problem. Try changing some entries to be incorrect!
  • 22 |
  • Try inputting ill-formed matrices such as [[1, 2], [3]]
  • 23 |
24 | 25 | 26 | 27 | 28 | 29 |

View source

30 | 31 |
32 | -------------------------------------------------------------------------------- /course/problem/matrix3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 |

A line goes through points \( \vec{a}, \vec{b} \in \mathbb{R}^3 \). Give an expression for the minimum distance from this line to a third point \( \vec{c} \).

18 | 19 |
    20 |
  • You may enter \(\vec{a}\), \(\vec{b}\), and \(\vec{c}\) as veca, vecb, and vecc.
  • 21 |
  • Use cross(a, b) for \( \vec{a} \times \vec{b}\)
  • 22 |
23 | 24 |

This demo problem is primarily intended to show off the cross product. Some suggested inputs:

25 | 26 |
    27 |
  • 28 | abs(cross(vecb-veca, vecc-veca))/abs(vecb-veca) is correct, 29 |
  • 30 |
  • 31 | abs(cross(vecb-veca, vecc-vecb))/abs(vecb-veca) is also correct 32 |
  • 33 |
  • 34 | cross(1, [1, 2, 3]) to see hepful error messages. All standard functions (norm, trans, det, ...) provided by the MatrixGrader class give similar error messages when something goes wrong. 35 |
  • 36 |
37 | 38 | 39 | 40 | 41 | 42 |

View source

43 | 44 |
45 | -------------------------------------------------------------------------------- /course/problem/matrix4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 |

This pair of graders is intended to display how MatrixGrader treats vectors, single-row matrices, and single-column matrices as different objects. The problems are identical, except that the second problem allows entry-by-entry matrix input.

18 | 19 |

Suggested inputs:

20 | 21 |
    22 |
  • 23 | Correct answers: 24 |
      25 |
    • [1, 2]
    • 26 |
    • trans([1, 2]), transpose does nothing to vectors
    • 27 |
    • [[1, 0], [0, 1]] * [1, 2]
    • 28 |
    • [1,2] * [[1, 0], [0, 1]]
    • 29 |
    30 |
  • 31 |
  • [[1, 2]], single-row matrix, incorrect
  • 32 |
  • [[1], [2]], single-column matrix, incorrect
  • 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 51 | 52 |
Only vector input is allowedVector and matrix input allowed
42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 |
53 | 54 |

Here, we use a MathJax preprocessor to display vectors as columns rather than rows (the default display from AsciiMath). This is an option that can be turned on or off inside the preprocessor.

55 | 56 |

Avoiding Confusion

57 |

In "real" problems, we strongly recommend disallowing entry-by-entry matrix input (by default, this is disallowed) unless it is needed, to avoid potential confusion between [1, 2] and [[1], [2]] (or [1, 2] and [[1, 2]] with the column vector option turned off).

58 | 59 |

View source

60 | 61 |
62 | -------------------------------------------------------------------------------- /course/problem/matrix5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 |

Enter an eigenvector of \( 21 | \begin{bmatrix} 22 | 1 & x \\ 23 | x & -1 24 | \end{bmatrix} 25 | \) with positive eigenvalue.

26 | 27 |

This problem uses the eigenvector_comparer comparer function shipped with mitxgraders to check that the student's input is an eigenvector of author-specified matrix. Suggested inputs:

28 | 29 |
    30 |
  • Correct answers: any nonzero scalar multiple of [1+sqrt(1+x^2), x]
  • 31 |
  • The zero vector, [0, 0]
  • 32 |
33 | 34 | 35 | 36 | 37 | 38 |

39 |

Resources

40 | 48 |

49 | 50 |
51 | -------------------------------------------------------------------------------- /course/problem/matrix6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

The matrix grader has some pre-defined constants that you can use in your problems. It is common in physics for unit vectors to be present in problems. Rather than define scalar variables hatx, haty and hatz, you can load vector quantities for these constants. Then, if students make vector errors in their entries, they will be informed so.

4 | 5 |

In this problem, a rock is thrown from the origin with initial velocity \(v_x \hat{x} + v_y \hat{y}\). The acceleration due to gravity is \(-g \hat{y}\). What is the position \(\vec{r}\) of the rock as a function of time?

6 | 7 |

Suggested inputs:

8 | 9 |
    10 |
  • v_x*t*hatx + (v_y*t-g*t^2/2)*haty (the correct answer)
  • 11 |
  • v_x*t + (v_y*t-g*t^2/2)
  • 12 |
  • hatx/hatz
  • 13 |
  • hatx*haty
  • 14 |
15 | 16 | 25 | 26 |

\(\vec{r} = \)

27 | 28 | 29 | 30 | 31 |

Note that in this example, we set max_array_dim to 0, meaning that students cannot enter vector or matrix quantities manually. If we hadn't, the grader would accept the answer [v_x*t,v_y*t-g*t^2/2,0].

32 | 33 |

In addition to cartesian_xyz, we also define cartesian_ijk for those that prefer the \(\hat{i}\), \(\hat{j}\) and \(\hat{k}\) unit vectors, as well as the Pauli matrices sigma_x, sigma_y and sigma_z as pauli.

34 | 35 |

View source

36 | 37 |
38 | -------------------------------------------------------------------------------- /course/problem/matrix7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

One of the perennial issues in grading vectors is dealing with normalization and phase conventions. We have a couple of custom comparers to help address these issues.

4 | 5 |

Our first example allows a vector to be compared against a reference vector, and graded correct if the two vectors are related by a phase. The vectors must have the same normalization, however. In the below case, the suggested answer is [1, i], but this can be multiplied by any phase and still be graded as correct.

6 | 7 |

Suggested inputs:

8 | 9 |
    10 |
  • [1, i]
  • 11 |
  • [i, -1]
  • 12 |
  • [-1, -i]
  • 13 |
  • [-i, 1]
  • 14 |
  • exp(i*1.2345)*[1, i]
  • 15 |
  • [1, i+0.1]
  • 16 |
  • exp(i*1.2345)*[1, i]*1.01
  • 17 |
18 | 19 | 28 | 29 |

\(\vec{v} = \)

30 | 31 | 32 | 33 | 34 | 35 |

While this deals with phases, sometimes we also want to allow for arbitrary normalization too. In this case, we can request that the student enter a vector in the span of our reference vector. Here, we again set that reference vector to be [1, i].

36 | 37 |

Suggested inputs:

38 | 39 |
    40 |
  • [1, i]
  • 41 |
  • 17.8*exp(i*1.2345)*[1, i]
  • 42 |
  • [1, i+0.1]
  • 43 |
44 | 45 | 53 | 54 |

55 |

\(\vec{v} = \)

56 | 57 | 58 | 59 |

60 | 61 |

62 |

Resources

63 | 71 |

72 | 73 |
74 | -------------------------------------------------------------------------------- /course/problem/matrix8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Sometimes, you want to work with non-commuting variables or operations. These can be straightforwardly implemented as matrices, as in the following example. Note that although we secretly have matrix variables, students do not need to know this, and hence we never give error messages that have anything to do with matrices.

4 | 5 |

[mathjaxinline]A[/mathjaxinline] is a scalar function. Write down its gradient.

6 | 7 |

Suggested inputs:

8 | 9 |
    10 |
  • nabla * A (correct)
  • 11 |
  • A * nabla (incorrect)
  • 12 |
  • nabla * A + 1 (incorrect, but looks like a matrix + a scalar)
  • 13 |
14 | 15 | 27 | 28 |

Gradient of \(A\) =

29 | 30 | 31 | 32 | 33 |

View source

34 | 35 |
36 | -------------------------------------------------------------------------------- /course/problem/matrix9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

A large number of options are available for sampling vectors/matrices/tensors. Here are the options available for the different array types:

4 | 5 |

Vectors

6 | 7 |
    8 |
  • Dimension
  • 9 |
  • Range for norm
  • 10 |
  • Complex
  • 11 |
12 | 13 |

Tensors

14 | 15 |
    16 |
  • Dimensions
  • 17 |
  • Range for norm (Frobenius norm)
  • 18 |
  • Complex
  • 19 |
20 | 21 |

General Matrices

22 | 23 |
    24 |
  • Dimensions
  • 25 |
  • Range for norm (Frobenius norm)
  • 26 |
  • Complex
  • 27 |
  • Upper/lower triangular
  • 28 |
29 | 30 |

Square Matrices

31 | 32 |
    33 |
  • Square Dimension
  • 34 |
  • Range for norm (Frobenius norm)
  • 35 |
  • Complex
  • 36 |
  • Multiple of identity
  • 37 |
  • Orthogonal*
  • 38 |
  • Unitary*
  • 39 |
  • Diagonal/Symmetric/Antisymmetric/Hermitian/Antihermitian
  • 40 |
  • Unit/zero determinant
  • 41 |
  • Traceless
  • 42 |
43 | 44 |

* Code exists to sample orthogonal and unitary matrices, but relies on the upcoming python 3 upgrade for edX. When this upgrade occurs, we'll demonstrate it here!

45 | 46 |

Here is an example of using a Hermitian matrix. Let \(H\) be Hermitian, and take vectors \(\vec{u}\) and \(\vec{v}\). Compute \(\vec{u} \cdot (H \vec{v})\).

47 | 48 |

Suggested inputs:

49 | 50 |
    51 |
  • u*H*v
  • 52 |
  • u*adj(H)*v (correct)
  • 53 |
  • u*trans(H)*v (incorrect)
  • 54 |
55 | 56 | 68 | 69 |

Quantity =

70 | 71 | 72 | 73 | 74 |

75 |

Resources

76 | 84 |

85 | 86 |
87 | -------------------------------------------------------------------------------- /course/problem/singlelist1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Here is a basic example of SingleListGrader. The answer is "cat" and "dog", in any order, separated by a comma.

4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

View source

15 | 16 |
17 | -------------------------------------------------------------------------------- /course/problem/singlelist2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Here is an example where multiple answers are accepted. You can either enter ("cat" or "feline") and "dog", or "goat" and "vole". The entries should be comma separated, and can appear in any order. Note that mixing one set of answers with the other set will only give you partial credit though.

4 | 5 | 15 | 16 | 17 | 18 | 19 | 20 |

View source

21 | 22 |
23 | -------------------------------------------------------------------------------- /course/problem/singlelist3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

By default, the grader accepts any number of inputs in the list (try it in some of the other examples!). If you want to raise an error if the number of inputs is wrong, then you can do that too.

4 | 5 |

In 3D, what are the components of the \(\hat{y}\) unit vector? (Answer: 0, 1, 0). Try putting in two or four components instead of three. Also note that this input is ordered. (Note that you should probably use the MatrixGrader for this question instead of a SingleListGrader!)

6 | 7 |

This example also demonstrates how you can provide a string for the answers key, rather than a list.

8 | 9 | 18 | 19 | 20 | 21 | 22 | 23 |

View source

24 | 25 |
26 | -------------------------------------------------------------------------------- /course/problem/singlelist4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

It's possible to nest SingleListGraders by using different delimiters. In the following example, the answer is "a, b; c, d". You can also use "c, d; a, b". However, the sublists are ordered, so "b, a; c, d" is only partly correct (and receives partial credit).

4 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

View source

22 | 23 |
24 | -------------------------------------------------------------------------------- /course/problem/singlelist5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

You can construct powerful grading and feedback structures for SingleListGraders. Here is an example.

4 | 5 |

What is the greatest cartoon pairing of all time? (Admittedly, a little subjective!) Try the following answers (note that ordering is unimportant):

6 | 7 |
    8 |
  • Mickey, Minnie
  • 9 |
  • Bugs Bunny, Daffy Duck
  • 10 |
  • Bugs Bunny, Elmer Fudd
  • 11 |
  • Tom, Jerry
  • 12 |
13 | 14 |

Also try mixing the answers from different pairings together, noting the different messages and grades you can receive.

15 | 16 | 27 | 28 | 29 | 30 | 31 | 32 |

View source

33 | 34 |
35 | -------------------------------------------------------------------------------- /course/problem/string1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Here is a basic example. The answer to this problem is "wolf", as is shown when you click on "Show Answer". However, an alternative correct answer is "canis lupus". A partially correct answer is "dog". A special message is displayed for "unicorn", and another message for all other input. This demonstrates the ability to specify various triplets of (answer, score, message). Note that this ability is not limited to StringGraders, but can be used by all graders.

4 | 5 | 21 | 22 | 23 | 24 | 25 | 26 |

View source

27 | 28 |
29 | -------------------------------------------------------------------------------- /course/problem/string2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

By default, StringGrader strips all leading/trailing whitespace from both the answer and input. It is also case sensitive by default. You can control both of these behaviors. For example, the answer below is "Hat ", with a single space at the end. This is case sensitive, and requires the space at the end.

4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |

Here is another example, where the answer is two spaces, complete with two spaces between the words. By default, multiple consecutive spaces are turned into single spaces, but again, you can control this behavior.

18 | 19 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |

View source

33 | 34 |
35 | -------------------------------------------------------------------------------- /course/problem/string3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Sometimes, you may want to accept anything that a student types in (possibly with some caveats, which we'll get to in a bit). To do this, we can use the accept_any option. Here is an example.

4 | 5 |

Please enter your favorite color in the textbox below.

6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 |

You can also require the student to enter a minimum number of characters and/or words. The following problem needs at least 10 characters and 3 words. See what happens when you don't meet these requirements too!

21 | 22 |

What do you think about this problem?

23 | 24 | 34 | 35 | 36 | 37 | 38 | 39 |

View source

40 | 41 |
42 | -------------------------------------------------------------------------------- /course/problem/string4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

StringGrader can enforce that the student input matches a given regex pattern as part of its grading. This is useful in giving students error messages if their input doesn't match the required format, and helps prevent them from making input errors that are not meaningfully part of the assessment.

4 | 5 |

As an example, type the chemical formula for ethane in the following box. Try the following inputs:

6 | 7 |
    8 |
  • CH_3 CH_3
  • 9 |
  • C_2H_6
  • 10 |
  • CH3CH3
  • 11 |
  • NO_2
  • 12 |
13 | 14 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Pattern matching can also be used in conjuction with accept_any to grade students based on whether their input matches the given regex pattern. Here's an example.

31 | 32 |

Use the min function in python to take the minimum of some numbers. Try the following inputs:

33 | 34 |
    35 |
  • min(1.5, 2)
  • 36 |
  • min(1, -2, 3, 4.6000001, 5, 6, 7, 8, 9, 10)
  • 37 |
  • max(1, 2)
  • 38 |
  • min(1+2j, 3)
  • 39 |
40 | 41 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |

View source

57 | 58 |
59 | -------------------------------------------------------------------------------- /course/problem/string5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Any grader in the library can be "inlined" as in the following example. In the following example, the grader is included in the customresponse tag, with the answer inferred from the customresponse tag. This makes setting up simple graders really quick and easy!

4 | 5 |

The answer below is "cat".

6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

View source

18 | 19 |
20 | -------------------------------------------------------------------------------- /course/problem/string6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

The library is capable of awarding reduced credit based on the attempt number for the student. A python function converts the attempt number into a maximum amount of credit. We provide three built-in functions, but you're welcome to write your own.

4 | 5 |
    6 |
  • LinearCredit: Linearly decreasing credit to a minimum
  • 7 |
  • GeometricCredit: Maximum credit decreases by a factor for each attempt
  • 8 |
  • ReciprocalCredit: Maximum credit is 1 / attempt number
  • 9 |
10 | 11 |

In the below example, we use LinearCredit (but we modify it so that it "resets" every 10 attempts, so attempts 11, 21, 31, ... are treated like attempt 1 for the purpose of this demonstration, as you have no way to reset your attempts!).

12 | 13 |

For a student's first attempt, they can be awarded maximum credit. A student's second attempt will be awarded at most 80%. Each attempt will decrease the maximum by 20%, to a minimum of 20%. When credit is decreased because of attempt-based partial credit, students receive a message informing them of this by default.

14 | 15 |

The answer is "cat".

16 | 17 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

Note that attempt-based partial credit applies to all graders; this is just an example of using it with a StringGrader. It's also possible to set course-wide defaults to apply attempt-based partial credit to all your questions through the use of a plugin (which we provide a sample for).

36 | 37 | 38 |

View source

39 | 40 |
41 | -------------------------------------------------------------------------------- /course/problem/sum1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Construct a sum that sums the integers from 1 to 10, using n as the dummy variable of summation.

4 | 5 | 16 | 17 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 49 | 53 | 54 | 55 | 58 | 59 | 60 |
42 | 43 |
47 |

\( \displaystyle \huge{ \sum }\)

48 |
50 |
51 | 52 |
56 |

[mathjaxinline]n = [/mathjaxinline]

57 |
61 |
62 |
63 | 64 |

While the expected answer is 10, n and 1 (top to bottom), the student could also use 0 as the bottom limit, which is equally correct. The student could also enter 9, n+1 and 0 and also receive full credit. Unlike integrals, the order of the limits is unimportant in summations, so you can also switch the limits and still be graded correctly.

65 | 66 |

View source

67 | 68 |
69 | -------------------------------------------------------------------------------- /course/problem/sum2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

SumGrader is capable of handling infinite sums, such as the following.

4 | 5 | 16 | 17 | 33 | 34 |

What is the Taylor series expansion of [mathjaxinline]e^x[/mathjaxinline]?

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 50 | 51 | 52 |
44 |

\( \displaystyle e^x = \large{\sum}_{n=0}^{\infty} \)

45 |
47 |
48 | 49 |
53 |
54 |
55 | 56 |

Here, the expected answer is x^n/fact(n). Note that we don't allow the student to input the limits in this example. In the underlying code, we sample \(x\) from \([0, 0.5]\), which ensures that the numerical evaluation of the sum rapidly converges.

57 | 58 |

View source

59 | 60 |
61 | -------------------------------------------------------------------------------- /course/problem/sum3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Students can even specify their own dummy variable, as in the following example.

4 | 5 |

Construct a summation that computes the \(n^{\rm th}\) triangular number.

6 | 7 | 18 | 19 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 54 | 58 | 59 | 60 | 65 | 66 | 67 |
47 | 48 |
52 |

\( \displaystyle \huge{ \sum }\)

53 |
55 |
56 | 57 |
61 | 62 |

[mathjaxinline]=[/mathjaxinline]

63 | 64 |
68 |
69 |
70 | 71 |

Here, the anticipated answer is n, t, t and 1, though students can also swap t for other possible variables. Note that i and j are not available, as these are used for the imaginary unit.

72 | 73 |

View source

74 | 75 |
76 | -------------------------------------------------------------------------------- /course/problem/sum4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

You can specify that a sum is over even or odd integers only.

4 | 5 |

Construct a Taylor series for \(\sin(x)\), summing over odd integers only.

6 | 7 | 18 | 19 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 54 | 58 | 59 | 60 | 63 | 64 | 65 |
47 | 48 |
52 |

\( \displaystyle \huge{ \sum }\)

53 |
55 |
56 | 57 |
61 |

[mathjaxinline]n \text{ odd,} \ n = [/mathjaxinline]

62 |
66 |
67 |
68 | 69 |

Here, the correct answer is infty, i^(n-1)*x^n/fact(n) and 1 (top to bottom), although there are other ways of writing this also.

70 | 71 |

Unfortunately, students are unable to specify that the sum should be for even/odd integers only; this must be specified by the problem author upon creation.

72 | 73 |

View source

74 | 75 |
76 | -------------------------------------------------------------------------------- /course/problem/sum5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

SumGrader can even sum vectors and matrices.

4 | 5 |

Here, we compute the exponential of a matrix. If you have no idea what that means, that's perfectly ok - just follow the answers below!

6 | 7 | 18 | 19 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 50 | 51 | 52 |
44 |

\( \displaystyle \exp \left( i \theta \begin{bmatrix}0 & 1 \\ 1 & 0\end{bmatrix} \right) = \large{\sum}_{n=0}^{\infty}\)

45 |
47 |
48 | 49 |
53 |
54 |
55 | 56 |

The expected answer here is (i*theta)^n/fact(n)*[[0,1],[1,0]]^n. It turns out that you can evaluate this analytically. To test that the grader correctly evaluates the sum, try entering (cos(theta)*[[1,0],[0,1]]+i*sin(theta)*[[0,1],[1,0]])*kronecker(n,0), which is the analytic solution (the Kronecker delta is zero unless n=0, so essentially collapses to the sum to a single term).

57 | 58 |

View source

59 | 60 |
61 | -------------------------------------------------------------------------------- /course/static/MJxPrep.js: -------------------------------------------------------------------------------- 1 | ../../MJxPrep.js -------------------------------------------------------------------------------- /course/static/mitx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitodl/mitx-grading-library/7cdcfc4651771ee3f6e6db779d42b8f4524ed14d/course/static/mitx.jpg -------------------------------------------------------------------------------- /course/static/python_lib.zip: -------------------------------------------------------------------------------- 1 | ../../python_lib.zip -------------------------------------------------------------------------------- /course/upload.sh: -------------------------------------------------------------------------------- 1 | find . -name '*.DS_Store' -type f -delete 2 | if [ -f 'course.tar.gz' ] 3 | then 4 | rm course.tar.gz 5 | fi 6 | tar -cLzhf course.tar.gz * 7 | echo "Tar file created. Beginning upload." 8 | python uploader.py https://studio.edge.edx.org course-v1:MITx+grading-library+examples course.tar.gz 9 | -------------------------------------------------------------------------------- /course/uploader.py: -------------------------------------------------------------------------------- 1 | """ 2 | uploader.py 3 | 4 | Script to upload a tarball to an edX course. 5 | 6 | Based on 7 | https://github.com/mitodl/git2edx/blob/master/edxStudio.py 8 | by Ike Chuang 9 | """ 10 | import os 11 | import sys 12 | import time 13 | import requests 14 | import argparse 15 | import json 16 | 17 | # Import username and password 18 | from config import username, password 19 | 20 | class edxStudio(object): 21 | 22 | def __init__(self, site): 23 | self.session = requests.session() 24 | self.site = site 25 | 26 | def login(self, username, password): 27 | url = '{}/login'.format(self.site) 28 | self.session.get(url) 29 | csrf = self.session.cookies['csrftoken'] 30 | url = '{}/login_post'.format(self.site) 31 | headers = {'X-CSRFToken': csrf, 'Referer': '{}/signin'.format(self.site)} 32 | r2 = self.session.post(url, 33 | data={'email': username, 'password': password}, 34 | headers=headers) 35 | 36 | if not r2.status_code == 200: 37 | print("Login failed!") 38 | print(r2.content) 39 | sys.stdout.flush() 40 | sys.exit(-1) 41 | 42 | def do_upload(self, course_id, tar_file, nwait=20): 43 | 44 | filename = os.path.basename(tar_file) 45 | url = '{}/import/{}'.format(self.site, course_id) 46 | 47 | files = {'course-data': (filename, open(tar_file, 'rb'), 'application/x-gzip')} 48 | 49 | csrf = self.session.cookies['csrftoken'] 50 | 51 | headers = {'X-CSRFToken': csrf, 52 | 'Referer': url, 53 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 54 | } 55 | 56 | print("Uploading to {}".format(url)) 57 | 58 | try: 59 | r3 = self.session.post(url, files=files, headers=headers) 60 | except Exception as err: 61 | print("Error uploading file: {}".format(err)) 62 | print("url={}, files={}, headers={}".format(url, files, headers)) 63 | sys.stdout.flush() 64 | sys.exit(-1) 65 | 66 | print("File uploaded. Response: {}".format(r3.status_code)) 67 | 68 | status = json.loads(r3.content.decode("utf-8"))["ImportStatus"] 69 | if status == 0: 70 | print("Status: 0: No status info found (import done or upload still in progress)") 71 | elif status == 1: 72 | print("Status: 1: Unpacking") 73 | elif status == 2: 74 | print("Status: 2: Verifying") 75 | elif status == 3: 76 | print("Status: 3: Updating") 77 | elif status == 4: 78 | print("Status: 4: Import successful") 79 | return 80 | else: 81 | print("Error in upload") 82 | print("Status: {}".format(status)) 83 | 84 | url = '{}/import_status/{}/{}'.format(self.site, course_id, filename) 85 | print("File is being processed...") 86 | print("Status URL: {}".format(url)) 87 | 88 | for k in range(nwait): 89 | r4 = self.session.get(url) 90 | # -X : Import unsuccessful due to some error with X as stage [0-3] 91 | # 0 : No status info found (import done or upload still in progress) 92 | # 1 : Unpacking 93 | # 2 : Verifying 94 | # 3 : Updating 95 | # 4 : Import successful 96 | status = json.loads(r4.content.decode("utf-8"))["ImportStatus"] 97 | if status == 0: 98 | print("Status: 0: No status info found (import done or upload still in progress)") 99 | elif status == 1: 100 | print("Status: 1: Unpacking") 101 | elif status == 2: 102 | print("Status: 2: Verifying") 103 | elif status == 3: 104 | print("Status: 3: Updating") 105 | elif status == 4: 106 | print("Status: 4: Import successful") 107 | return 108 | else: 109 | print("Error in upload") 110 | print("Status: {}".format(status)) 111 | 112 | sys.stdout.flush() 113 | time.sleep(2) 114 | 115 | # Deal with command line arguments 116 | parser = argparse.ArgumentParser(description="Uploads tar.gz files to an edX instance using credentials in config.py") 117 | parser.add_argument("site", help="URL of edX site (eg: https://studio.edge.edx.org)") 118 | parser.add_argument("course_id", help="Course ID (eg: course-v1:MITx+8.04.1x+3T2018)") 119 | parser.add_argument("tar_file", help="Name of .tar.gz file to upload") 120 | args = parser.parse_args() 121 | 122 | # Get things started 123 | print("Uploading {}".format(args.tar_file)) 124 | print("to {}".format(args.site)) 125 | print("with course id {}".format(args.course_id)) 126 | es = edxStudio(args.site) 127 | 128 | # Connect to studio 129 | print("Connecting...") 130 | sys.stdout.flush() 131 | es.login(username, password) 132 | print("Login successful") 133 | 134 | # Do the upload 135 | print("Uploading...") 136 | es.do_upload(args.course_id, args.tar_file) 137 | -------------------------------------------------------------------------------- /docs/css/cinder_tweaks.css: -------------------------------------------------------------------------------- 1 | code { 2 | background-color: rgba(0, 0, 0, 0.05); 3 | border-radius: 2pt; 4 | } 5 | 6 | .admonition code { 7 | background-color: rgba(255, 255, 255, 0.75) 8 | } 9 | 10 | /* make the sidebar nav scrollable when window too small */ 11 | .bs-sidebar.affix { 12 | overflow-y:scroll; 13 | max-height: 95%; 14 | } 15 | 16 | /* cinder has weird extra space before headers. Get rid of it */ 17 | h1[id]:before, h2[id]:before, 18 | h3[id]:before, h4[id]:before, 19 | h5[id]:before, h6[id]:before { 20 | margin-top: 0pt; 21 | height: 0pt; 22 | } 23 | 24 | /* Next two rules makes header links only visible when hovering. 25 | NOTE: cinder specific; default theme does this automatically 26 | */ 27 | .headerlink { 28 | visibility:hidden; 29 | } 30 | 31 | h1:hover .headerlink, 32 | h2:hover .headerlink, 33 | h3:hover .headerlink, 34 | h4:hover .headerlink, 35 | h5:hover .headerlink, 36 | h6:hover .headerlink, 37 | /* active is for touchscreens */ 38 | h1:active .headerlink, 39 | h2:active .headerlink, 40 | h3:active .headerlink, 41 | h4:active .headerlink, 42 | h5:active .headerlink, 43 | h6:active .headerlink { 44 | visibility:visible; 45 | } 46 | 47 | /* Cinder theme does not include css for admonitions 48 | This css is copied from the base theme: 49 | 50 | https://github.com/mkdocs/mkdocs/blob/master/mkdocs/themes/mkdocs/css/base.css 51 | */ 52 | 53 | .admonition { 54 | padding: 15px; 55 | margin-bottom: 20px; 56 | border: 1px solid transparent; 57 | border-radius: 4px; 58 | text-align: left; 59 | } 60 | 61 | .admonition.note { /* csslint allow: adjoining-classes */ 62 | color: #3a87ad; 63 | background-color: #d9edf7; 64 | border-color: #bce8f1; 65 | } 66 | 67 | .admonition.warning { /* csslint allow: adjoining-classes */ 68 | color: #c09853; 69 | background-color: #fcf8e3; 70 | border-color: #fbeed5; 71 | } 72 | 73 | .admonition.danger { /* csslint allow: adjoining-classes */ 74 | color: #b94a48; 75 | background-color: #f2dede; 76 | border-color: #eed3d7; 77 | } 78 | 79 | .admonition-title { 80 | font-weight: bold; 81 | text-align: left; 82 | } 83 | -------------------------------------------------------------------------------- /docs/css/extra.css: -------------------------------------------------------------------------------- 1 | /* make prompt impossible to select for hljs */ 2 | .hljs-prompt { 3 | user-select: none; 4 | } 5 | 6 | /* make prompt impossible to select for pygments */ 7 | .gp { 8 | user-select: none; 9 | } 10 | 11 | pre, pre code { 12 | white-space: pre-wrap 13 | } 14 | -------------------------------------------------------------------------------- /docs/css/readthedocs.css: -------------------------------------------------------------------------------- 1 | .codehilite {background-color: #ffffff; color:#333333;} 2 | .codehilite .hll {background-color: #ffffcc;} 3 | .codehilite .c {color: #999988; font-style: italic;} 4 | .codehilite .err {color: #a61717; background-color: #e3d2d2;} 5 | .codehilite .k {font-weight: bold;} 6 | .codehilite .o {font-weight: bold;} 7 | .codehilite .cm {color: #999988; font-style: italic;} 8 | .codehilite .cp {color: #999999; font-weight: bold;} 9 | .codehilite .c1 {color: #999988; font-style: italic;} 10 | .codehilite .cs {color: #999999; font-weight: bold; font-style: italic;} 11 | .codehilite .gd {color: black; background-color: #ffdddd;} 12 | .codehilite .gd .x {color: black; background-color: #ffaaaa;} 13 | .codehilite .ge {font-style: italic;} 14 | .codehilite .gr {color: #aa0000;} 15 | .codehilite .gh {color: #999999;} 16 | .codehilite .gi {color: black; background-color: #ddffdd;} 17 | .codehilite .gi .x {color: black; background-color: #aaffaa;} 18 | .codehilite .go {color: #888888;} 19 | .codehilite .gp {color: #555555;} 20 | .codehilite .gs {font-weight: bold;} 21 | .codehilite .gu {color: purple; font-weight: bold;} 22 | .codehilite .gt {color: #aa0000;} 23 | .codehilite .kc {font-weight: bold;} 24 | .codehilite .kd {font-weight: bold;} 25 | .codehilite .kn {font-weight: bold;} 26 | .codehilite .kp {font-weight: bold;} 27 | .codehilite .kr {font-weight: bold;} 28 | .codehilite .kt {color: #445588; font-weight: bold;} 29 | .codehilite .m {color: #009999;} 30 | .codehilite .s {color: #dd1144;} 31 | .codehilite .n {color: #333333;} 32 | .codehilite .na {color: teal;} 33 | .codehilite .nb {color: #0086b3;} 34 | .codehilite .nc {color: #445588; font-weight: bold;} 35 | .codehilite .no {color: teal;} 36 | .codehilite .ni {color: purple;} 37 | .codehilite .ne {color: #990000; font-weight: bold;} 38 | .codehilite .nf {color: #990000; font-weight: bold;} 39 | .codehilite .nn {color: #555555;} 40 | .codehilite .nt {color: navy;} 41 | .codehilite .nv {color: teal;} 42 | .codehilite .ow {font-weight: bold;} 43 | .codehilite .w {color: #bbbbbb;} 44 | .codehilite .mf {color: #009999;} 45 | .codehilite .mh {color: #009999;} 46 | .codehilite .mi {color: #009999;} 47 | .codehilite .mo {color: #009999;} 48 | .codehilite .sb {color: #dd1144;} 49 | .codehilite .sc {color: #dd1144;} 50 | .codehilite .sd {color: #dd1144;} 51 | .codehilite .s2 {color: #dd1144;} 52 | .codehilite .se {color: #dd1144;} 53 | .codehilite .sh {color: #dd1144;} 54 | .codehilite .si {color: #dd1144;} 55 | .codehilite .sx {color: #dd1144;} 56 | .codehilite .sr {color: #009926;} 57 | .codehilite .s1 {color: #dd1144;} 58 | .codehilite .ss {color: #990073;} 59 | .codehilite .bp {color: #D2691E;} 60 | .codehilite .vc {color: teal;} 61 | .codehilite .vg {color: teal;} 62 | .codehilite .vi {color: teal;} 63 | .codehilite .il {color: #009999;} 64 | .codehilite .gc {color: #999999; background-color: #eaf2f5;} 65 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions (FAQs) 2 | 3 | - Does the grading library work with multiple choice/checkbox/dropdown lists? 4 | 5 | Unfortunately, no. Those problem types cannot be used in a `customresponse` problem, so we can't grade them with a python grader. 6 | 7 | - Does the grader work with python 2 or python 3? 8 | 9 | It works with both! Older versions of edX ran python graders in python 2.7; newer versions use python 3.8. The library works seamlessly with both. No changes to any code are required to switch between versions. Some functionality requires python 3.5 however. 10 | -------------------------------------------------------------------------------- /docs/grading_math/functions_and_constants.md: -------------------------------------------------------------------------------- 1 | # Mathematical Functions and Constants 2 | 3 | ## FormulaGrader and NumericalGrader Default Functions 4 | 5 | !!! note 6 | Below, expressions marked with a * may require our [AsciiMath renderer definitions](renderer.md) to display properly in edX. 7 | 8 | By default, all of the following functions are made available to students in `FormulaGrader` problems. 9 | 10 | - `sin(x)` Sine 11 | - `cos(x)` Cosine 12 | - `tan(x)` Tangent 13 | - `sec(x)` Secant 14 | - `csc(x)` Cosecant 15 | - `cot(x)` Cotangent 16 | - `sqrt(x)` Square Root 17 | - `log10(x)` Log (base 10)* 18 | - `log2(x)` Log (base 2)* 19 | - `ln(x)` Natural logarithm 20 | - `exp(x)` Exponential 21 | - `arccos(x)` Inverse Cosine 22 | - `arcsin(x)` Inverse Sine 23 | - `arctan(x)` Inverse Tangent 24 | - `arctan2(x, y)` Four-quadrant Inverse Tangent* 25 | - `arcsec(x)` Inverse Secant* 26 | - `arccsc(x)` Inverse Cosecant* 27 | - `arccot(x)` Inverse Cotangent* 28 | - `abs(x)` Absolute value (real) or modulus (complex) 29 | - `factorial(x)` and `fact(x)` Factorial* 30 | - domain: all complex numbers except negative integers. Large outputs may raise `OverflowError`s. 31 | - `sinh(x)` Hyperbolic Sine 32 | - `cosh(x)` Hyperbolic Cosine 33 | - `tanh(x)` Hyperbolic Tangent 34 | - `sech(x)` Hyperbolic Secant 35 | - `csch(x)` Hyperbolic Cosecant 36 | - `coth(x)` Hyperbolic Cotangent 37 | - `arcsinh(x)` Inverse Hyperbolic Sine* 38 | - `arccosh(x)` Inverse Hyperbolic Cosine* 39 | - `arctanh(x)` Inverse Hyperbolic Tangent* 40 | - `arcsech(x)` Inverse Hyperbolic Secant* 41 | - `arccsch(x)` Inverse Hyperbolic Cosecant* 42 | - `arccoth(x)` Inverse Hyperbolic Cotangent* 43 | - `floor(x)` Floor function (applies only to real numbers) 44 | - `ceil(x)` Ceiling function (applies only to real numbers) 45 | - `min(x, y, z, ...)` Minimum of the arguments (applies only to real numbers, 2 or more arguments) 46 | - `max(x, y, z, ...)` Maximum of the arguments (applies only to real numbers, 2 or more arguments) 47 | - `re(x)` Real part of a complex expression* 48 | - `im(x)` Imaginary part of a complex expression* 49 | - `conj(x)` Complex conjugate of a complex expression* 50 | - `kronecker(x, y)` Kronecker delta* (Note that we highly recommend integer sampling over a short range (eg, 1 to 4) when Kronecker deltas appear in an answer, and using many samples (eg, 30) so that most permutations appear in the sampling.) 51 | 52 | 53 | ## MatrixGrader Default Functions 54 | 55 | In `MatrixGrader` problems, all `FormulaGrader` functions are available by default, as are the following extra functions: 56 | 57 | - `abs(x)`: absolute value of a scalar or magnitude of a vector 58 | - `adj(x)`: Hermitian adjoint, same as `ctrans(x)`* 59 | - `cross(x, y)`: cross product, inputs must be 3-component vectors* 60 | - `ctrans(x)`: conjugate transpose, same as `adj(x)`* 61 | - `det(x)`: determinant, input must be square matrix 62 | - `norm(x)`: Frobenius norm, works for scalars, vectors, and matrices 63 | - `trans(x)`: transpose* 64 | - `trace(x)`: trace 65 | 66 | 67 | ## Default Constants 68 | 69 | Available in `FormulaGrader`, `NumericalGrader`, and `MatrixGrader` by default: 70 | 71 | - `i`: imaginary unit (same as `j`) 72 | - `j`: imaginary unit (same as `i`) 73 | - `e`: approximately 2.718281828 74 | - `pi`: approximately 3.141592654 75 | 76 | 77 | ## Optional Constant Collections 78 | 79 | We provide a few collections of constants that can be imported for convenience and reuse. For example, `pauli` is a dictionary with keys `sigma_x`, `sigma_y`, and `sigma_z` that are [`MathArray`](matrix_grader/matrix_grader/#matrix-operations-and-matharrays) representations of the 2x2 Pauli matrices. 80 | 81 | The collections of available mathematical constants are: 82 | 83 | - `pauli`: MathArray representations of the 2x2 Pauli matrices, `sigma_x`, `sigma_y`, and `sigma_z` 84 | - `cartesian_xyz`: MathArray representations of the three-dimensional Cartesian unit vectors, named `hatx`, `haty`, `hatz` 85 | - `cartesian_ijk`: MathArray representations of the three-dimensional Cartesian unit vectors, named `hati`, `hatj`, `hatk` 86 | 87 | Each collection is a dictionary that can be provided as a value of `user_constants`: 88 | 89 | ```pycon 90 | >>> from mitxgraders import * 91 | >>> grader = MatrixGrader( 92 | ... answers='sigma_x + sigma_z', 93 | ... user_constants=pauli 94 | ... ) 95 | 96 | ``` 97 | -------------------------------------------------------------------------------- /docs/grading_math/matrix_grader/input_by_entries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitodl/mitx-grading-library/7cdcfc4651771ee3f6e6db779d42b8f4524ed14d/docs/grading_math/matrix_grader/input_by_entries.png -------------------------------------------------------------------------------- /docs/grading_math/matrix_grader/input_by_symbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitodl/mitx-grading-library/7cdcfc4651771ee3f6e6db779d42b8f4524ed14d/docs/grading_math/matrix_grader/input_by_symbols.png -------------------------------------------------------------------------------- /docs/grading_math/numerical_grader.md: -------------------------------------------------------------------------------- 1 | # NumericalGrader 2 | 3 | When grading math expressions without functions or variables, you can use `NumericalGrader` instead of `FormulaGrader`. `NumericalGrader` is a specialized version of `FormulaGrader` whose behavior resembles the edX `` tag. 4 | 5 | ## Configuration 6 | 7 | `NumericalGrader` has all of the same options as `FormulaGrader` except: 8 | 9 | * `tolerance`: has a higher default value of `'5%'` 10 | * `failable_evals` is always set to 0 11 | * `samples` is always set to 1 12 | * `variables` is always set to `[]` (no variables allowed) 13 | * `sample_from` is always set to `{}` (no variables allowed) 14 | * `user_functions` can only define specific functions, with no random functions 15 | 16 | Note that `NumericalGrader` will still evaluate formulas. If you are grading simple integers (such as 0, 1, 2, -1, etc), you may want to consider using `StringGrader` instead of `NumericalGrader`. 17 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Plugins are an advanced feature of the library that allows users to construct their own custom-built graders, built on top of the infrastructure of the library. They can also be used to allow for straightforward code re-use, and to override library defaults on a course-wide basis. 4 | 5 | Any `.py` file stored in the `mitxgraders/plugins` folder will be automatically loaded. All variables in the `__all__` list will be made available when doing `from mitxgraders import *`. See `template.py` for an example. 6 | 7 | You can define custom grading classes in your plugin. To learn how this works, we recommend copying the code from `stringgrader.py`, renaming the class, and building a simple plugin based on `StringGrader`. 8 | 9 | We are happy to include user-contributed plugins in the repository for this library. If you have built a plugin that you would like to see combined into this library, please contact the authors through [github](https://github.com/mitodl/mitx-grading-library). We are also willing to consider incorporating good plugins into the library itself. 10 | 11 | 12 | ## Overriding Library Defaults 13 | 14 | Library defaults for any grading class can be specified by constructing the desired dictionary of defaults, and calling `register_defaults(dict)` on the class. For example, to specify that all `StringGrader`s should be case-insensitive by default, you can do the following. 15 | 16 | ```pycon 17 | StringGrader.register_defaults({ 18 | 'case_sensitive': False 19 | }) 20 | ``` 21 | 22 | When this code is included in a file in the plugins folder, it automatically runs every time the library is loaded, leading to course-wide defaults. If for some reason you need to reset to the library defaults for a specific problem, you can call `clear_registered_defaults()` on the class in that problem. 23 | 24 | An example plugin has been provided for you in `defaults_sample.py`. The code in this plugin is commented out so that it doesn't change anything by default. If you are interested in overriding library defaults on a course-wide basis, we recommend copying this file to `defaults.py` and setting the desired defaults using the code templates provided. This is particularly useful if you wish to use attempt-based partial credit throughout your course. 25 | 26 | 27 | ## Inserting Plugins into the Library 28 | 29 | To use a plugin, you will need to download the `python_lib.zip` file, unzip it, put the plugin in the plugins directory, and rezip everything. Your new zip file should unzip to have the `mitxgraders` and `voluptuous` directories. 30 | -------------------------------------------------------------------------------- /integration_tests/integration_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script is used to test the integration of the mitx-graders library with the Open edX platform. 3 | Running a code that uses the functions provided by the library using the safe_exec function, and in the MIT course context, 4 | to be able to use the python_lib.zip that contains the library. 5 | """ 6 | 7 | import logging 8 | import os 9 | import sys 10 | 11 | import django 12 | from opaque_keys.edx.keys import CourseKey 13 | from xmodule.capa.safe_exec import safe_exec 14 | from xmodule.contentstore.django import contentstore 15 | from xmodule.util.sandboxing import SandboxService 16 | 17 | # Set up Django environment 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.envs.test") 19 | django.setup() 20 | 21 | log = logging.getLogger(__name__) 22 | 23 | # Define the code to be executed 24 | GRADING_CLASSES_CODE = """ 25 | # To test, we can use the custom graders 26 | from mitxgraders import ( 27 | StringGrader, 28 | FormulaGrader, 29 | NumericalGrader, 30 | MatrixGrader, 31 | SingleListGrader, 32 | ListGrader, 33 | IntegralGrader, 34 | IntervalGrader, 35 | SumGrader, 36 | RealMatrices, 37 | RealVectors, 38 | ComplexRectangle, 39 | ) 40 | 41 | # Test the MatrixGrader, the class that uses the most external dependencies 42 | MatrixGrader( 43 | answers='x*A*B*u + z*C^3*v/(u*C*v)', 44 | variables=['A', 'B', 'C', 'u', 'v', 'z', 'x'], 45 | sample_from={ 46 | 'A': RealMatrices(shape=[2, 3]), 47 | 'B': RealMatrices(shape=[3, 2]), 48 | 'C': RealMatrices(shape=[2, 2]), 49 | 'u': RealVectors(shape=[2]), 50 | 'v': RealVectors(shape=[2]), 51 | 'z': ComplexRectangle() 52 | }, 53 | identity_dim=2 54 | ) 55 | 56 | """ 57 | 58 | 59 | def execute_code(course_key_str): 60 | """ 61 | Executes the provided code in a sandboxed environment with the specified course context. 62 | 63 | Args: 64 | course_key_str (str): The string representation of the course key. 65 | 66 | Returns: 67 | None 68 | """ 69 | course_key = CourseKey.from_string(course_key_str) 70 | sandbox_service = SandboxService( 71 | course_id=course_key, 72 | contentstore=contentstore 73 | ) 74 | zip_lib = sandbox_service.get_python_lib_zip() 75 | 76 | extra_files = [] 77 | python_path = [] 78 | 79 | if zip_lib is not None: 80 | extra_files.append(("python_lib.zip", zip_lib)) 81 | python_path.append("python_lib.zip") 82 | 83 | safe_exec( 84 | code=GRADING_CLASSES_CODE, 85 | globals_dict={}, 86 | python_path=python_path, 87 | extra_files=extra_files, 88 | slug="integration-test", 89 | limit_overrides_context=course_key_str, 90 | unsafely=False, 91 | ) 92 | 93 | 94 | if __name__ == "__main__": 95 | if len(sys.argv) != 2: 96 | print("Usage: python integration_test.py ") 97 | sys.exit(1) 98 | 99 | course_key_str = sys.argv[1] 100 | execute_code(course_key_str) 101 | -------------------------------------------------------------------------------- /makezip.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Creates python_lib.zip for the library 3 | 4 | # Kill all .DS_Store and .pyc files and __pycache__ folders 5 | echo Removing all unwanted files... 6 | find . | grep -E "(__pycache__|\.DS_Store|\.pyc)" | xargs rm -rf 7 | 8 | # Remove the old python_lib.zip 9 | file="python_lib.zip" 10 | if [ -f $file ] ; then 11 | echo Removing old python_lib.zip... 12 | rm $file 13 | fi 14 | 15 | # Copy the license into the grading folder 16 | cp LICENSE ./mitxgraders/LICENSE 17 | 18 | # Create the zip file 19 | echo Building python_lib.zip... 20 | zip -r $file mitxgraders voluptuous 21 | 22 | # Remove the license from the grading folder 23 | rm ./mitxgraders/LICENSE 24 | 25 | echo Done! 26 | -------------------------------------------------------------------------------- /mitxgraders-js/MJxPrep.js: -------------------------------------------------------------------------------- 1 | ../MJxPrep.js -------------------------------------------------------------------------------- /mitxgraders-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mitxgraders", 3 | "version": "1.0.0", 4 | "description": "Javascript files to accompany mitxgraders Python library", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mitodl/mitx-grading-library.git" 12 | }, 13 | "author": "Jolyon Bloomfield and Chris Chudzicki", 14 | "bugs": { 15 | "url": "https://github.com/mitodl/mitx-grading-library/issues" 16 | }, 17 | "homepage": "https://github.com/mitodl/mitx-grading-library#readme", 18 | "devDependencies": { 19 | "jest": "^24.3.0" 20 | }, 21 | "jest": { 22 | "verbose": true, 23 | "testURL": "http://localhost/" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mitxgraders/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MITx Grading Library 3 | https://github.com/mitodl/mitx-grading-library 4 | Copyright (c) 2017-2020 Jolyon Bloomfield and Chris Chudzicki 5 | All Rights Reserved 6 | 7 | See version.py for version number 8 | 9 | An edX python grading library 10 | """ 11 | 12 | # Add the current version 13 | 14 | from mitxgraders.version import __version__ 15 | 16 | # Add voluptuous 17 | try: 18 | import voluptuous 19 | except ImportError: 20 | raise ImportError("External dependency 'voluptuous' not found;" 21 | " see https://github.com/mitodl/mitx-grading-library#faq") 22 | 23 | # All modules defining grading classes must set __all__ to specify 24 | # which classes are imported with the package 25 | from mitxgraders.stringgrader import * 26 | from mitxgraders.listgrader import * 27 | from mitxgraders.formulagrader import * 28 | from mitxgraders.sampling import * 29 | from mitxgraders.matrixsampling import * 30 | from mitxgraders.exceptions import ConfigError, StudentFacingError, InvalidInput, MissingInput 31 | from mitxgraders.helpers.calc import * 32 | from mitxgraders.comparers import * 33 | from mitxgraders.attemptcredit import * 34 | 35 | def import_plugins(): 36 | """Imports all plugins into the global namespace""" 37 | import os 38 | import importlib 39 | 40 | # Get the list of all plugin files 41 | my_dir = os.path.dirname(os.path.realpath(__file__)) 42 | plugin_dir = os.path.join(my_dir, "plugins") 43 | plugin_files = [ 44 | x[:-3] 45 | for x in os.listdir(plugin_dir) 46 | if x.endswith(".py") and not x.startswith("_") 47 | ] 48 | 49 | # Import all plugins 50 | for plugin in plugin_files: 51 | # Import the module into the namespace 52 | mod = importlib.import_module(__name__ + ".plugins." + plugin) 53 | # Add everything listed in plugin.__all__ to the global namespace 54 | # for this package 55 | if hasattr(mod, "__all__"): 56 | globals().update({name: mod.__dict__[name] for name in mod.__all__}) 57 | 58 | globals().update({'loaded_from': "mitxgraders directory"}) 59 | 60 | 61 | def import_zip_plugins(): # pragma: no cover 62 | """Imports all plugins from python_lib.zip into the global namespace""" 63 | import os 64 | import importlib 65 | import zipfile 66 | 67 | # Find all of the plugins in python_lib.zip 68 | with zipfile.ZipFile('python_lib.zip', 'r') as myzip: 69 | file_list = myzip.namelist() 70 | plugins = [ 71 | file[:-3] for file in file_list 72 | if file.startswith("mitxgraders/plugins/") 73 | and file.endswith(".py") 74 | and not file.endswith("__init__.py") 75 | ] 76 | 77 | # Turn the plugin names into module names 78 | # Eg, "graders/plugins/template" becomes "graders.plugins.template" 79 | plugin_names = [plugin.replace("/", ".") for plugin in plugins] 80 | 81 | # Import all plugins 82 | for plugin in plugin_names: 83 | # Import the module into the namespace 84 | mod = importlib.import_module(plugin) 85 | # Add everything listed in plugin.__all__ to the global namespace 86 | # for this package 87 | if hasattr(mod, "__all__"): 88 | globals().update({name: mod.__dict__[name] for name in mod.__all__}) 89 | 90 | globals().update({'loaded_from': "python_lib.zip"}) 91 | 92 | 93 | # Import all the plugins 94 | if "python_lib.zip" in __file__: 95 | # If this package is inside python_lib.zip, we need to work a little differently 96 | import_zip_plugins() # pragma: no cover 97 | else: 98 | import_plugins() 99 | 100 | # Clean up the namespace 101 | del import_plugins, import_zip_plugins 102 | -------------------------------------------------------------------------------- /mitxgraders/attemptcredit.py: -------------------------------------------------------------------------------- 1 | """ 2 | attemptcredit.py 3 | 4 | This file contains various routines for assigning attempt-based partial credit. 5 | """ 6 | from voluptuous import Schema, Required, Any, Range, All 7 | from mitxgraders.baseclasses import ObjectWithSchema 8 | from mitxgraders.helpers.validatorfuncs import Positive 9 | 10 | __all__ = ['LinearCredit', 'GeometricCredit', 'ReciprocalCredit'] 11 | 12 | class LinearCredit(ObjectWithSchema): 13 | """ 14 | This class assigns credit based on a piecewise linear progression: 15 | Constant - Linear Decrease - Constant 16 | 17 | Credit 18 | ^ 19 | |-- 20 | | \ 21 | | \ 22 | | ----- 23 | | 24 | +---------> Attempt number 25 | 26 | Configuration: 27 | ============== 28 | decrease_credit_after (positive int): The last attempt number to award maximum 29 | credit to (default 1) 30 | 31 | minimum_credit (float between 0 and 1): The minimum amount of credit to be awarded 32 | after using too many attempts (default 0.2) 33 | 34 | decrease_credit_steps (positive int): How many attempts it takes to get to minimum 35 | credit. So, if set to 1, after decrease_credit_after attempts, the next attempt 36 | will receive minimum_credit. If set to 2, the next attempt will be halfway 37 | between 1 and minimum_credit, and the attempt after that will be awarded 38 | minimum_credit. (default 4) 39 | 40 | Usage: 41 | ====== 42 | 43 | >>> creditor = LinearCredit(decrease_credit_after=1, minimum_credit=0.2, decrease_credit_steps=4) 44 | >>> creditor(1) 45 | 1 46 | >>> creditor(2) 47 | 0.8 48 | >>> creditor(3) 49 | 0.6 50 | >>> creditor(4) 51 | 0.4 52 | >>> creditor(5) 53 | 0.2 54 | >>> creditor(6) 55 | 0.2 56 | 57 | """ 58 | @property 59 | def schema_config(self): 60 | return Schema({ 61 | Required('decrease_credit_after', default=1): Positive(int), 62 | Required('decrease_credit_steps', default=4): Positive(int), 63 | Required('minimum_credit', default=0.2): Any(All(float, Range(0, 1)), 0, 1) 64 | }) 65 | 66 | def __call__(self, attempt): 67 | """ 68 | Return the credit associated with a given attempt number 69 | """ 70 | if attempt == 1: 71 | return 1 72 | 73 | # How far past the point of decreasing credit are we? 74 | steps = attempt - self.config['decrease_credit_after'] 75 | if steps <= 0: 76 | return 1 77 | 78 | # Compute the credit to be awarded 79 | min_cred = self.config['minimum_credit'] 80 | decrease_steps = self.config['decrease_credit_steps'] 81 | if steps >= decrease_steps: 82 | credit = min_cred 83 | else: 84 | # Linear interpolation 85 | credit = 1 + (min_cred - 1) * steps / decrease_steps 86 | 87 | return round(credit, 4) 88 | 89 | class GeometricCredit(ObjectWithSchema): 90 | """ 91 | This class assigns credit based on a geometric progression: 92 | 1, x, x^2, etc. 93 | x = 3/4 by default. 94 | 95 | Configuration: 96 | ============== 97 | factor (float): Number between 0 and 1 inclusive that is the decreasing 98 | factor for each attempt (default 0.75). 99 | 100 | Usage: 101 | ====== 102 | 103 | >>> creditor = GeometricCredit(factor=0.5) 104 | >>> creditor(1) 105 | 1 106 | >>> creditor(2) 107 | 0.5 108 | >>> creditor(3) 109 | 0.25 110 | 111 | """ 112 | @property 113 | def schema_config(self): 114 | return Schema({ 115 | Required('factor', default=0.75): Any(All(float, Range(0, 1)), 0, 1) 116 | }) 117 | 118 | def __call__(self, attempt): 119 | """ 120 | Return the credit associated with a given attempt number 121 | """ 122 | if attempt == 1: 123 | return 1 124 | credit = self.config['factor'] ** (attempt - 1) 125 | return round(credit, 4) 126 | 127 | class ReciprocalCredit(ObjectWithSchema): 128 | """ 129 | This class assigns credit based on the reciprocal of attempt number. 130 | 1, 1/2, 1/3, etc. 131 | 132 | Configuration: 133 | ============== 134 | None 135 | 136 | Usage: 137 | ====== 138 | 139 | >>> creditor = ReciprocalCredit() 140 | >>> creditor(1) 141 | 1 142 | >>> creditor(2) 143 | 0.5 144 | >>> creditor(3) # doctest: +ELLIPSIS 145 | 0.333... 146 | >>> creditor(4) 147 | 0.25 148 | 149 | """ 150 | @property 151 | def schema_config(self): 152 | # No configuration for this one! 153 | return Schema({}) 154 | 155 | def __call__(self, attempt): 156 | """ 157 | Return the credit associated with a given attempt number 158 | """ 159 | if attempt == 1: 160 | return 1 161 | credit = 1.0 / attempt 162 | return round(credit, 4) 163 | -------------------------------------------------------------------------------- /mitxgraders/comparers/__init__.py: -------------------------------------------------------------------------------- 1 | from mitxgraders.comparers.comparers import ( 2 | EqualityComparer, 3 | MatrixEntryComparer, 4 | equality_comparer, 5 | congruence_comparer, 6 | eigenvector_comparer, 7 | between_comparer, 8 | vector_span_comparer, 9 | vector_phase_comparer 10 | ) 11 | from mitxgraders.comparers.baseclasses import Comparer, CorrelatedComparer 12 | 13 | from mitxgraders.comparers.linear_comparer import LinearComparer 14 | 15 | __all__ = [ 16 | 'EqualityComparer', 17 | 'MatrixEntryComparer', 18 | 'equality_comparer', 19 | 'congruence_comparer', 20 | 'eigenvector_comparer', 21 | 'between_comparer', 22 | 'vector_span_comparer', 23 | 'vector_phase_comparer', 24 | 'Comparer', 25 | 'CorrelatedComparer', 26 | 'LinearComparer' 27 | ] 28 | -------------------------------------------------------------------------------- /mitxgraders/comparers/baseclasses.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines baseclasses for comparers. 3 | 4 | Note: Any callable object with the correct signature can be used as a comparer 5 | function. We use classes for comparer functions that have configuration options. 6 | """ 7 | import abc 8 | from mitxgraders.baseclasses import ObjectWithSchema 9 | 10 | class Comparer(ObjectWithSchema): 11 | """ 12 | Comparers are callable objects used as comparer functions in FormulaGrader problems. 13 | Unlike standard comparer functions, Comparers can be passed options at instantiation. 14 | 15 | This class is abstract. Comparers should inherit from it. 16 | """ 17 | 18 | @abc.abstractmethod 19 | def __call__(self, comparer_params_eval, student_eval, utils): 20 | """ 21 | Compares student input evalution with expected evaluation. 22 | 23 | Arguments: 24 | `comparer_params_eval`: The `comparer_params` list, numerically evaluated 25 | according to variable and function sampling. 26 | `student_eval`: The student's input, numerically evaluated according to 27 | variable and function sampling. 28 | `utils`: a convenience object, same as for simple comparers. 29 | Returns: 30 | bool or results dict 31 | """ 32 | 33 | class CorrelatedComparer(Comparer): 34 | """ 35 | CorrelatedComparer are callable objects used as comparer functions 36 | in FormulaGrader problems. Unlike standard comparer functions, CorrelatedComparer 37 | are given access to all parameter evaluations at once. 38 | 39 | For example, a comparer function that decides whether the student input is a 40 | nonzero constant multiple of the expected input would need to be a correlated 41 | comparer so that it can determine if there is a linear relationship between 42 | the student and expected samples. 43 | 44 | This class is abstract. Correlated Comparers should inherit from it. 45 | """ 46 | 47 | @abc.abstractmethod 48 | def __call__(self, comparer_params_evals, student_evals, utils): 49 | """ 50 | Compares student input evalutions with expected evaluations. 51 | 52 | Below, N is the number of samples used by a FormulaGrader problem. 53 | 54 | Arguments: 55 | `comparer_params_evals`: a nested list of evaluated comparer params, 56 | [params_evals_0, params_evals_1,..., params_evals_N] 57 | `student_evals`: a list of N student input numerical evaluations 58 | `utils`: a convenience object, same as for simple comparers. 59 | Returns: 60 | bool or results dict 61 | """ 62 | -------------------------------------------------------------------------------- /mitxgraders/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | exceptions.py 3 | Contains generic grader-related exceptions 4 | """ 5 | 6 | 7 | class MITxError(Exception): 8 | """ 9 | Base class for all exceptions in mitxgraders, has a privileged role 10 | in grading. 11 | 12 | if an MITxError is raised when a grader tries to check a student's input, 13 | its message will be displayed directly to the student. If any other error 14 | occurs, it will be caught and a a generic error message is shown. 15 | 16 | (Except when debug=True, in which case all errors are simply 17 | re-raised.) 18 | """ 19 | 20 | class ConfigError(MITxError): 21 | """ 22 | Raised whenever a configuration error occurs. This is intended for 23 | author-facing messages. 24 | """ 25 | 26 | class StudentFacingError(MITxError): 27 | """ 28 | Base class for errors whose messages are intended for students to view. 29 | """ 30 | 31 | class InvalidInput(StudentFacingError): 32 | """ 33 | Raised when an input has failed some validation. 34 | 35 | Usually we use this when the input can be graded, but is invalid in some 36 | other sense. For example, an input contains a forbidden string or function. 37 | """ 38 | 39 | class InputTypeError(InvalidInput): 40 | """ 41 | Indicates that student's input has evaluated to an object of the wrong 42 | type (or shape). 43 | """ 44 | 45 | class MissingInput(StudentFacingError): 46 | """ 47 | Raised when a required input has been left blank. 48 | """ 49 | -------------------------------------------------------------------------------- /mitxgraders/formulagrader/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains classes for numerical and formula graders 3 | * NumericalGrader 4 | * FormulaGrader 5 | * MatrixGrader 6 | * IntegralGrader 7 | * IntervalGrader 8 | """ 9 | from mitxgraders.formulagrader.formulagrader import ( 10 | FormulaGrader, 11 | NumericalGrader, 12 | ) 13 | 14 | from mitxgraders.formulagrader.matrixgrader import ( 15 | MatrixGrader, 16 | ) 17 | 18 | from mitxgraders.formulagrader.integralgrader import ( 19 | IntegralGrader, 20 | SumGrader, 21 | ) 22 | 23 | from mitxgraders.formulagrader.intervalgrader import ( 24 | IntervalGrader, 25 | ) 26 | 27 | # Set the objects to be *-imported from package 28 | __all__ = [ 29 | "NumericalGrader", 30 | "FormulaGrader", 31 | "MatrixGrader", 32 | "IntegralGrader", 33 | "SumGrader", 34 | "IntervalGrader" 35 | ] 36 | -------------------------------------------------------------------------------- /mitxgraders/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitodl/mitx-grading-library/7cdcfc4651771ee3f6e6db779d42b8f4524ed14d/mitxgraders/helpers/__init__.py -------------------------------------------------------------------------------- /mitxgraders/helpers/calc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | calc module 3 | 4 | Exports frequently used objects for convenience 5 | """ 6 | 7 | 8 | from mitxgraders.helpers.calc.expressions import evaluator, parse 9 | from mitxgraders.helpers.calc.mathfuncs import ( 10 | DEFAULT_VARIABLES, 11 | DEFAULT_FUNCTIONS, 12 | DEFAULT_SUFFIXES, 13 | METRIC_SUFFIXES, 14 | pauli, 15 | cartesian_xyz, 16 | cartesian_ijk, 17 | within_tolerance 18 | ) 19 | from mitxgraders.helpers.calc.math_array import MathArray, identity 20 | from mitxgraders.helpers.calc.specify_domain import specify_domain 21 | from mitxgraders.helpers.calc.exceptions import CalcError 22 | 23 | __all__ = [ 24 | "parse", 25 | "evaluator", 26 | "DEFAULT_VARIABLES", 27 | "DEFAULT_FUNCTIONS", 28 | "DEFAULT_SUFFIXES", 29 | "METRIC_SUFFIXES", 30 | "pauli", 31 | "cartesian_xyz", 32 | "cartesian_ijk", 33 | "within_tolerance", 34 | "MathArray", 35 | "identity", 36 | "specify_domain", 37 | "CalcError" 38 | ] 39 | -------------------------------------------------------------------------------- /mitxgraders/helpers/calc/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from mitxgraders.exceptions import StudentFacingError 4 | 5 | class CalcError(StudentFacingError): 6 | """Base class for errors originating in calc module""" 7 | 8 | class UndefinedVariable(CalcError): 9 | """ 10 | Indicate when a student inputs a variable which was not expected. 11 | """ 12 | 13 | class UndefinedFunction(CalcError): 14 | """ 15 | Indicate when a student inputs a function which was not expected. 16 | """ 17 | 18 | class UnbalancedBrackets(CalcError): 19 | """ 20 | Indicate when a student's input has unbalanced brackets. 21 | """ 22 | 23 | class CalcZeroDivisionError(CalcError): 24 | """ 25 | Indicates division by zero 26 | """ 27 | 28 | class CalcOverflowError(CalcError): 29 | """ 30 | Indicates numerical overflow 31 | """ 32 | 33 | class FunctionEvalError(CalcError): 34 | """ 35 | Indicates that something has gone wrong during function evaluation. 36 | """ 37 | 38 | class UnableToParse(CalcError): 39 | """ 40 | Indicate when an expression cannot be parsed 41 | """ 42 | 43 | class DomainError(CalcError): 44 | """ 45 | Raised when a function has domain error. 46 | """ 47 | 48 | class ArgumentError(DomainError): 49 | """ 50 | Raised when the wrong number of arguments is passed to a function 51 | """ 52 | 53 | class ArgumentShapeError(DomainError): 54 | """ 55 | Raised when the wrong type of argument is passed to a function 56 | """ 57 | 58 | class MathArrayError(CalcError): 59 | """ 60 | Thrown by MathArray when anticipated errors are made. 61 | """ 62 | 63 | class MathArrayShapeError(MathArrayError): 64 | """Raised when a MathArray operation cannot be performed because of shape 65 | mismatch.""" 66 | -------------------------------------------------------------------------------- /mitxgraders/helpers/calc/formatters.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from numbers import Number 4 | from mitxgraders.helpers.calc.math_array import MathArray 5 | 6 | def get_description(obj, detailed=True): 7 | """ 8 | Gets a student-facing description of obj. 9 | 10 | Arguments: 11 | obj: the object to describe 12 | detailed (bool): for MathArray instances, whether to provide the shape 13 | or only the shape name (scalar, vector, matrix, tensor) 14 | 15 | Numbers return scalar: 16 | >>> get_description(5) 17 | 'scalar' 18 | 19 | MathArrays return their own description: 20 | >>> get_description(MathArray([1, 2, 3])) 21 | 'vector of length 3' 22 | 23 | Unless detailed=False, then only type is provided: 24 | >>> get_description(MathArray([1, 2, 3]), detailed=False) 25 | 'vector' 26 | 27 | Other objects return their class name: 28 | >>> get_description({}) 29 | 'dict' 30 | """ 31 | if isinstance(obj, Number): 32 | return 'scalar' 33 | elif isinstance(obj, MathArray): 34 | return obj.description if detailed else obj.shape_name 35 | else: 36 | return obj.__class__.__name__ 37 | -------------------------------------------------------------------------------- /mitxgraders/helpers/calc/robust_pow.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import numpy as np 4 | """ 5 | Contains the power function used by MathArray and parsing.py 6 | """ 7 | 8 | # This is used in expressions.py's eval_power function also. 9 | def robust_pow(base, exponent): 10 | """ 11 | Calculates __pow__, and tries other approachs if that doesn't work. 12 | 13 | Usage: 14 | ====== 15 | 16 | >>> robust_pow(5, 2) 17 | 25 18 | >>> robust_pow(0.5, -1) 19 | 2.0 20 | 21 | If base is negative and power is fractional, complex results are returned: 22 | >>> almost_j = robust_pow(-1, 0.5) 23 | >>> np.allclose(almost_j, 1j) 24 | True 25 | """ 26 | return base ** exponent 27 | -------------------------------------------------------------------------------- /mitxgraders/helpers/compatibility.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper functions to facilitate numpy ufuncs compatibility 3 | """ 4 | 5 | 6 | import functools 7 | 8 | def wraps(wrapped, 9 | assigned=functools.WRAPPER_ASSIGNMENTS, 10 | updated=functools.WRAPPER_UPDATES): 11 | """ 12 | A light wrapper around functools.wraps to facilitate compatibility with 13 | Python 3, and numpy ufuncs. 14 | 15 | Uses __name__ as __qualname__ if __qualname__ doesn't exist 16 | (this helps with numpy ufuncs, which do not have a __qualname__) 17 | 18 | References: 19 | functools source: 20 | https://github.com/python/cpython/blob/master/Lib/functools.py 21 | """ 22 | pruned_assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr)) 23 | wrapper = functools.wraps(wrapped, pruned_assigned, updated) 24 | def _wrapper(f): 25 | _f = wrapper(f) 26 | if '__qualname__' not in pruned_assigned and '__name__' in pruned_assigned: 27 | _f.__qualname__ = _f.__name__ 28 | return _f 29 | return _wrapper 30 | -------------------------------------------------------------------------------- /mitxgraders/helpers/get_number_of_args.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | def get_number_of_args(callable_obj): 5 | """ 6 | Get number of positional arguments of function or callable object. 7 | Based on inspect.signature 8 | 9 | Examples 10 | ======== 11 | 12 | Works for simple functions: 13 | >>> def f(x, y): 14 | ... return x + y 15 | >>> get_number_of_args(f) 16 | 2 17 | 18 | Positional arguments only: 19 | >>> def f(x, y, z=5): 20 | ... return x + y 21 | >>> get_number_of_args(f) 22 | 2 23 | 24 | Works with bound and unbound object methods 25 | >>> class Foo: 26 | ... def do_stuff(self, x, y, z): 27 | ... return x*y*z 28 | >>> get_number_of_args(Foo.do_stuff) # unbound, is NOT automatically passed self as argument 29 | 4 30 | >>> foo = Foo() 31 | >>> get_number_of_args(foo.do_stuff) # bound, is automatically passed self as argument 32 | 3 33 | 34 | Works for callable objects instances: 35 | >>> class Bar: 36 | ... def __call__(self, x, y): 37 | ... return x + y 38 | >>> bar = Bar() 39 | >>> get_number_of_args(bar) # bound instance, is automatically passed self as argument 40 | 2 41 | 42 | Works on built-in functions (assuming their docstring is correct) 43 | >>> get_number_of_args(pow) 44 | 2 45 | 46 | Works on numpy ufuncs 47 | >>> import numpy as np 48 | >>> get_number_of_args(np.sin) 49 | 1 50 | 51 | Works on RandomFunctions (tested in unit tests due to circular imports) 52 | """ 53 | if hasattr(callable_obj, "nin"): 54 | # Matches RandomFunction or numpy ufunc 55 | # Sadly, even Py3's inspect.signature can't handle numpy ufunc... 56 | return callable_obj.nin 57 | 58 | params = inspect.signature(callable_obj).parameters 59 | empty = inspect.Parameter.empty 60 | return sum([params[key].default == empty for key in params]) 61 | -------------------------------------------------------------------------------- /mitxgraders/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitodl/mitx-grading-library/7cdcfc4651771ee3f6e6db779d42b8f4524ed14d/mitxgraders/plugins/__init__.py -------------------------------------------------------------------------------- /mitxgraders/plugins/defaults_sample.py: -------------------------------------------------------------------------------- 1 | """ 2 | defaults_sample.py 3 | 4 | This plug-in can be used to set course-wide defaults for a given grading class. 5 | 6 | We recommend copying this file (so that upgrading the library won't overwrite it), 7 | renaming it to defaults.py, and modifying it according to your needs. 8 | 9 | This plug-in works by registering a dictionary with the appropriate grading class (note, 10 | not class instance). 11 | """ 12 | 13 | # Here are all of the graders that you can specify defaults for 14 | from mitxgraders.stringgrader import StringGrader 15 | from mitxgraders.baseclasses import AbstractGrader, ItemGrader 16 | from mitxgraders.listgrader import ListGrader, SingleListGrader 17 | from mitxgraders.formulagrader.integralgrader import IntegralGrader 18 | from mitxgraders.formulagrader.formulagrader import FormulaGrader, NumericalGrader 19 | from mitxgraders.formulagrader.matrixgrader import MatrixGrader 20 | 21 | # These will be needed to set attempt-based credit course-wide 22 | from mitxgraders.attemptcredit import LinearCredit, GeometricCredit, ReciprocalCredit 23 | 24 | # These modifications are commented out so that they don't override the normal defaults. 25 | # To use them, you need to uncomment them. 26 | 27 | # Change case_sensitive to False by default 28 | # StringGrader.register_defaults({ 29 | # 'case_sensitive': False 30 | # }) 31 | 32 | # Turn on attempt-based partial credit by default and modify related settings 33 | # This will turn on attempt-based partial credit for all graders 34 | # AbstractGrader.register_defaults({ 35 | # 'attempt_based_credit': ReciprocalCredit(), 36 | # 'attempt_based_credit_msg': True 37 | # }) 38 | # Note that if you do this, you will need to either pass cfn_extra_args="attempt" in every 39 | # customresponse tag or explicitly disable attempt-based credit. 40 | 41 | # Note that registered defaults can be applied to a higher level class than where those 42 | # options normally live. For example, to turn on debug by default in all StringGrader 43 | # instances (debug is defined in AbstractGrader), you can do the following: 44 | # StringGrader.register_defaults({ 45 | # 'debug': True 46 | # }) 47 | 48 | # Precedence is given to the registered defaults of higher level classes. If 49 | # register_defaults is called twice on the same class, the options stack on top of 50 | # each other, overwriting earlier options as necessary. 51 | 52 | # In this example, we make all MatrixGrader problems award partial credit by default. 53 | # MatrixGrader.register_defaults({ 54 | # 'entry_partial_credit': 'partial' 55 | # }) 56 | 57 | # You can also use this plug-in to make pre-built graders and functions available to 58 | # all your problems. You just need to include them in the __all__ list. For example: 59 | # my_grader = FormulaGrader(variables=['x', 'y']) 60 | # __all__ = ['my_grader'] 61 | # Now, "from mitxgraders import *"" will make my_grader available to you in a problem 62 | # You can similarly define custom functions etc, in the same way. 63 | -------------------------------------------------------------------------------- /mitxgraders/plugins/template.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a template file that also is used to test that the plugin loading 3 | mechanism is working. 4 | """ 5 | 6 | 7 | # Make sure that imports are working 8 | 9 | def plugin_test(): 10 | """Function is designed to be called as part of a test suite""" 11 | return True 12 | 13 | # Allow the function to be imported with * 14 | __all__ = ["plugin_test"] 15 | -------------------------------------------------------------------------------- /mitxgraders/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | MITx Grading Library 3 | Version number 4 | """ 5 | 6 | __version__ = "3.0.0" 7 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: MITxGraders 2 | repo_url: 'https://github.com/mitodl/mitx-grading-library' 3 | edit_uri: '' 4 | theme: 5 | name: 'cinder' 6 | colorscheme: vs-min 7 | nav: 8 | - Getting Started: 9 | - Overview: 'index.md' 10 | - edX Syntax: 'edx.md' 11 | - Introduction to Graders: 'graders.md' 12 | - ItemGrader: 'item_grader.md' 13 | - StringGrader: 'string_grader.md' 14 | - Grading Math: 15 | - FormulaGrader: 'grading_math/formula_grader.md' 16 | - NumericalGrader: 'grading_math/numerical_grader.md' 17 | - MatrixGrader: 'grading_math/matrix_grader/matrix_grader.md' 18 | - Sampling Sets: 'grading_math/sampling.md' 19 | - Functions and Constants List: 'grading_math/functions_and_constants.md' 20 | - User-Defined Functions: 'grading_math/user_functions.md' 21 | - Comparer Functions: 'grading_math/comparer_functions.md' 22 | - AsciiMath Renderer Definitions: 'grading_math/renderer.md' 23 | - IntegralGrader: 'grading_math/integral_grader.md' 24 | - SumGrader: 'grading_math/sum_grader.md' 25 | - IntervalGrader: 'grading_math/interval_grader.md' 26 | - Grading Lists: 27 | - SingleListGrader: 'grading_lists/single_list_grader.md' 28 | - ListGrader: 'grading_lists/list_grader.md' 29 | - Plugins: 'plugins.md' 30 | - FAQs: 'faq.md' 31 | - Changelog: 'changelog.md' 32 | markdown_extensions: 33 | - toc: 34 | permalink: ' # ' 35 | - admonition 36 | - codehilite 37 | extra_css: 38 | - css/extra.css 39 | - css/cinder_tweaks.css 40 | - css/readthedocs.css 41 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --doctest-modules --cov=mitxgraders --cov-report=term-missing --doctest-glob='*.md' 3 | testpaths = tests mitxgraders docs 4 | doctest_optionflags = ALLOW_UNICODE 5 | -------------------------------------------------------------------------------- /python_lib.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitodl/mitx-grading-library/7cdcfc4651771ee3f6e6db779d42b8f4524ed14d/python_lib.zip -------------------------------------------------------------------------------- /requirements-python311.txt: -------------------------------------------------------------------------------- 1 | # Used by the library 2 | # (taken from Sumac Open edX version) 3 | numpy==1.26.4 4 | scipy==1.14.1 5 | pyparsing==3.2.0 6 | 7 | # For testing 8 | pytest 9 | pytest-cov 10 | codecov 11 | 12 | # For documentation 13 | mkdocs 14 | mkdocs-cinder 15 | pygments 16 | 17 | # For uploading to edge 18 | requests 19 | -------------------------------------------------------------------------------- /requirements-python38.txt: -------------------------------------------------------------------------------- 1 | # Used by the library 2 | # (taken from Redwood Open edX version) 3 | numpy==1.24.4 4 | scipy==1.10.1 5 | pyparsing==3.1.2 6 | 7 | # For testing 8 | pytest 9 | pytest-cov 10 | codecov 11 | 12 | # For documentation 13 | mkdocs 14 | mkdocs-cinder 15 | pygments 16 | 17 | # For uploading to edge 18 | requests 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitodl/mitx-grading-library/7cdcfc4651771ee3f6e6db779d42b8f4524ed14d/tests/__init__.py -------------------------------------------------------------------------------- /tests/comparers/test_linear_comparer.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from mitxgraders import FormulaGrader, MatrixGrader, RealMatrices, NumericalGrader 3 | from mitxgraders.comparers import LinearComparer 4 | from mitxgraders.exceptions import ConfigError 5 | 6 | def test_linear_comparer_default_modes(): 7 | grader = FormulaGrader( 8 | answers={ 9 | 'comparer_params': ['m*c^2'], 10 | 'comparer': LinearComparer() 11 | }, 12 | variables=['m', 'c'] 13 | ) 14 | 15 | equals_result = {'msg': '', 'grade_decimal': 1.0, 'ok': True} 16 | proportional_result = { 17 | 'msg': 'The submitted answer differs from an expected answer by a constant factor.', 18 | 'grade_decimal': 0.5, 19 | 'ok': 'partial' 20 | } 21 | wrong_result = {'msg': '', 'grade_decimal': 0, 'ok': False} 22 | 23 | assert grader(None, 'm*c^2') == equals_result 24 | assert grader(None, '3*m*c^2') == proportional_result 25 | assert grader(None, 'm*c^2 + 10') == wrong_result 26 | assert grader(None, '-3*m*c^2 + 10') == wrong_result 27 | assert grader(None, 'm*c^3') == wrong_result 28 | assert grader(None, '0') == wrong_result 29 | 30 | def test_linear_comparer_with_zero_as_correct_answer(): 31 | grader = FormulaGrader( 32 | answers={ 33 | 'comparer_params': ['0'], 34 | 'comparer': LinearComparer(proportional=0.5, offset=0.4, linear=0.3) 35 | }, 36 | variables=['m', 'c'] 37 | ) 38 | assert grader(None, '0')['grade_decimal'] == 1 39 | assert grader(None, 'm')['grade_decimal'] == 0 # proportional & linear test fails 40 | assert grader(None, '1')['grade_decimal'] == 0.4 # not 0.5, proportional disabled 41 | 42 | def test_linear_comparer_custom_credit_modes(): 43 | grader = FormulaGrader( 44 | answers={ 45 | 'comparer_params': ['m*c^2'], 46 | 'comparer': LinearComparer(equals=0.8, proportional=0.6, offset=0.4, linear=0.2) 47 | }, 48 | variables=['m', 'c'] 49 | ) 50 | 51 | equals_result = {'msg': '', 'grade_decimal': 0.8, 'ok': 'partial'} 52 | proportional_result = { 53 | 'msg': 'The submitted answer differs from an expected answer by a constant factor.', 54 | 'grade_decimal': 0.6, 55 | 'ok': 'partial' 56 | } 57 | offset_result = {'msg': '', 'grade_decimal': 0.4, 'ok': 'partial'} 58 | linear_result = {'msg': '', 'grade_decimal': 0.2, 'ok': 'partial'} 59 | wrong_result = {'msg': '', 'grade_decimal': 0, 'ok': False} 60 | 61 | assert grader(None, 'm*c^2') == equals_result 62 | assert grader(None, '3*m*c^2') == proportional_result 63 | assert grader(None, 'm*c^2 + 10') == offset_result 64 | assert grader(None, '-3*m*c^2 + 10') == linear_result 65 | assert grader(None, 'm*c^3') == wrong_result 66 | assert grader(None, '0') == wrong_result 67 | 68 | def test_scaling_partial_credit(): 69 | FormulaGrader.set_default_comparer(LinearComparer()) 70 | grader = FormulaGrader( 71 | answers=( 72 | 'm*c^2', 73 | { 'expect': 'm*c^3', 'grade_decimal': 0.1 } 74 | ), 75 | variables=['m', 'c'] 76 | ) 77 | FormulaGrader.reset_default_comparer() 78 | 79 | expected = { 80 | 'ok': 'partial', 81 | 'grade_decimal': 0.1 * 0.5, 82 | # This message is a bit awkward ... in this situation, probably better to set up 83 | # a different LinearComparer for the common wrong answers, if you want to do that. 84 | # Or only use an linear comparer for the correct answer, and use equality_compaer 85 | # for the common wrong answers. 86 | # Anyway, I'm just testing the partial credit scaling 87 | 'msg': 'The submitted answer differs from an expected answer by a constant factor.', 88 | } 89 | 90 | assert grader(None, '4*m*c^3') == expected 91 | 92 | def test_works_with_matrixgrader(): 93 | grader = MatrixGrader( 94 | answers={ 95 | 'comparer_params': ['x*A*B^2'], 96 | 'comparer': LinearComparer(proportional=0.6, offset=0.4, linear=0.2) 97 | }, 98 | variables=['x', 'A', 'B'], 99 | sample_from={ 100 | 'A': RealMatrices(), 101 | 'B': RealMatrices() 102 | }, 103 | max_array_dim=2 104 | ) 105 | 106 | assert grader(None, 'x*A*B^2')['grade_decimal'] == 1.0 107 | assert grader(None, '2*x*A*B^2')['grade_decimal'] == 0.6 108 | assert grader(None, 'x*A*B^2 + 5*[[1, 1], [1, 1]]')['grade_decimal'] == 0.4 109 | assert grader(None, '3*x*A*B^2 + 5*[[1, 1], [1, 1]]')['grade_decimal'] == 0.2 110 | assert grader(None, 'x*A*B^2 + x*[[1, 1], [1, 1]]')['grade_decimal'] == 0 111 | assert grader(None, '0*A')['grade_decimal'] == 0 112 | 113 | def test_linear_too_few_comparisons(): 114 | FormulaGrader.set_default_comparer(LinearComparer()) 115 | grader = FormulaGrader(samples=2) 116 | with raises(ConfigError, match='Cannot perform linear comparison with less than 3 samples'): 117 | grader('1.5', '1.5') 118 | 119 | # Ensure that NumericalGrader does not use the same default comparer as FormulaGrader 120 | grader = NumericalGrader() 121 | assert grader('1.5', '1.5')['ok'] 122 | 123 | FormulaGrader.reset_default_comparer() 124 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from mitxgraders.sampling import set_seed 4 | 5 | def pytest_runtest_setup(item): 6 | """called before ``pytest_runtest_call(item).""" 7 | print("Resetting random.seed and numpy.random.seed for test {item}".format(item=item)) 8 | set_seed() 9 | -------------------------------------------------------------------------------- /tests/formulagrader/test_intervalgrader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for FormulaGrader and NumericalGrader 3 | """ 4 | 5 | 6 | from pytest import raises 7 | from voluptuous import Error 8 | 9 | from mitxgraders import IntervalGrader, NumericalGrader, FormulaGrader, StringGrader 10 | from mitxgraders.exceptions import ConfigError, InvalidInput 11 | 12 | def test_usage(): 13 | grader = IntervalGrader() 14 | assert grader("[1, 2)", "[1, 2)")['ok'] 15 | assert grader("[1, 2)", "[1, 2]")['grade_decimal'] == 0.5 16 | assert grader("(1, 2)", "[1, 2]")['grade_decimal'] == 0 17 | assert grader("(1, 2)", "(2, 3)")['grade_decimal'] == 0 18 | 19 | grader = IntervalGrader(partial_credit=False) 20 | assert grader("[1, 2)", "[1, 2]")['grade_decimal'] == 0 21 | 22 | def test_errors(): 23 | msg = r"Invalid opening bracket. The opening_brackets configuration allows for '\[', '\(' as opening brackets." 24 | with raises(ConfigError, match=msg): 25 | grader = IntervalGrader(answers=['{','1','2',']']) 26 | 27 | msg = r"Invalid closing bracket. The closing_brackets configuration allows for '\]', '\)' as closing brackets." 28 | with raises(ConfigError, match=msg): 29 | grader = IntervalGrader(answers=['(','1','2','}']) 30 | 31 | msg = r"Answer list must have 4 entries: opening bracket, lower bound, upper bound, closing bracket." 32 | with raises(ConfigError, match=msg): 33 | grader = IntervalGrader(answers=['(','2','}']) 34 | with raises(ConfigError, match=msg): 35 | grader = IntervalGrader(answers=['(','2','3','5','}']) 36 | 37 | msg = r"Opening bracket must be a single character." 38 | with raises(ConfigError, match=msg): 39 | grader = IntervalGrader(answers=['(a','2','3','}']) 40 | 41 | msg = r"Closing bracket must be a single character." 42 | with raises(ConfigError, match=msg): 43 | grader = IntervalGrader(answers=['(','2','3','}a']) 44 | 45 | msg = r'Unable to read interval from answer: "abcd"' 46 | with raises(ConfigError, match=msg): 47 | grader = IntervalGrader() 48 | grader('abcd', '1, 2, 3') 49 | 50 | msg = r'Unable to read interval from answer: "2, 3"' 51 | with raises(ConfigError, match=msg): 52 | grader = IntervalGrader() 53 | grader('[1,2]', '2, 3') 54 | 55 | msg = r"Invalid opening bracket: '{'. Valid options are: '\[', '\('." 56 | with raises(InvalidInput, match=msg): 57 | grader = IntervalGrader() 58 | grader('[1,2]', '{1, 2}') 59 | 60 | msg = r"Invalid closing bracket: '}'. Valid options are: '\]', '\)'." 61 | with raises(InvalidInput, match=msg): 62 | grader = IntervalGrader() 63 | grader('[1,2]', '[1, 2}') 64 | 65 | def test_messages(): 66 | grader = IntervalGrader(answers=[('[', {'expect': '(', 'msg': 'testing'}), '1', '2', ')']) 67 | assert grader("[1, 2)", "(1, 2)")['msg'] == 'testing' 68 | 69 | grader = IntervalGrader(answers=[('[', {'expect': '(', 'msg': 'testing'}), {'expect': '1', 'msg': 'yay!'}, '2', ')']) 70 | assert grader("[1, 2)", "(1, 2)")['msg'] == 'yay!
\ntesting' 71 | 72 | def test_subgraders(): 73 | assert IntervalGrader() == IntervalGrader(subgrader=NumericalGrader(tolerance=1e-13, allow_inf=True)) 74 | 75 | # Make sure this instantiates 76 | IntervalGrader(subgrader=FormulaGrader(tolerance=1e-13)) 77 | 78 | with raises(Error): 79 | IntervalGrader(subgrader=StringGrader()) 80 | 81 | def test_inferring_answers(): 82 | grader = IntervalGrader() 83 | grader1 = IntervalGrader(answers="(1,2]") 84 | grader2 = IntervalGrader(answers=["(", "1", "2", "]"]) 85 | assert grader1 == grader2 86 | 87 | assert grader("(1,2]", "(1,2]") == grader1("(1,2]", "(1,2]") 88 | assert grader("(1,2]", "(1,2)") == grader1("(1,2]", "(1,2)") 89 | 90 | def test_multiple_answers(): 91 | grader = IntervalGrader(answers={'expect': ("(1,2]", '(3,4]')}) 92 | 93 | expected_result = {'ok': True, 'grade_decimal': 1, 'msg': ''} 94 | assert grader(None, "(1,2]") == expected_result 95 | assert grader(None, "(3,4]") == expected_result 96 | 97 | def test_formulas(): 98 | grader = IntervalGrader( 99 | answers="(x, y^2 + 1)", 100 | subgrader=FormulaGrader(variables=['x', 'y']) 101 | ) 102 | assert grader(None, '(x, y^2+1]')['grade_decimal'] == 0.5 103 | 104 | def test_infinite_interval(): 105 | grader = IntervalGrader( 106 | answers="(0, infty)" 107 | ) 108 | assert grader(None, '(0, infty)')['ok'] 109 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import re 4 | from functools import wraps 5 | 6 | def log_results(results): 7 | """ 8 | Generate a decorator that logs function calls to results. 9 | 10 | Arguments: 11 | results: list into which results are logged 12 | Returns: 13 | Decorator that logs result of each call to function to results 14 | 15 | Usage: 16 | >>> results = [] 17 | >>> @log_results(results) 18 | ... def f(x, y): 19 | ... return x + y 20 | >>> f(2, 3); f(20, 30); 21 | 5 22 | 50 23 | >>> results 24 | [5, 50] 25 | """ 26 | 27 | def make_decorator(results): 28 | def decorator(func): 29 | @wraps(func) 30 | def _func(*args, **kwargs): 31 | result = func(*args,**kwargs) 32 | results.append(result) 33 | return result 34 | 35 | return _func 36 | return decorator 37 | 38 | return make_decorator(results) 39 | 40 | def round_decimals_in_string(string, round_to=6): 41 | """ 42 | Round all decimals in a string to a specified number of places. 43 | 44 | Usage 45 | ===== 46 | >>> s = "pi is 3.141592653589793 and e is 2.71828182845904523536028747 and one is 1.000" 47 | >>> round_decimals_in_string(s) 48 | 'pi is 3.141593 and e is 2.718282 and one is 1.000' 49 | 50 | Note that the final occurrence of 1.000 was not rounded. 51 | """ 52 | pattern = r"([0-9]\.[0-9]{{{round_to}}}[0-9]+)".format(round_to=round_to) 53 | def replacer(match): 54 | number = float(match.group(1)) 55 | formatter = "{{0:.{round_to}f}}".format(round_to=round_to) 56 | return formatter.format(number) 57 | 58 | return re.sub(pattern, replacer, string) 59 | -------------------------------------------------------------------------------- /tests/helpers/calc/test_expressions_arrays.py: -------------------------------------------------------------------------------- 1 | """ 2 | tests of expression.py with arrays 3 | """ 4 | 5 | 6 | from pytest import raises 7 | from mitxgraders import evaluator, MathArray 8 | from mitxgraders.helpers.calc.math_array import equal_as_arrays 9 | from mitxgraders.helpers.calc.exceptions import UnableToParse, CalcError 10 | 11 | def test_array_input(): 12 | """Test that vectors/matrices can be inputted into calc's evaluator""" 13 | result = evaluator("[1, 2, 3]", {}, {}, {}, max_array_dim=1)[0] 14 | assert equal_as_arrays(result, MathArray([1, 2, 3])) 15 | 16 | result = evaluator("[[1, 2], [3, 4]]", {}, {}, {}, max_array_dim=2)[0] 17 | assert equal_as_arrays(result, MathArray([[1, 2], [3, 4]])) 18 | 19 | expr = '[v, [4, 5, 6]]' 20 | result = evaluator(expr, {'v': MathArray([1, 2, 3])}, max_array_dim=2)[0] 21 | assert equal_as_arrays(result, MathArray([[1, 2, 3], [4, 5, 6]])) 22 | 23 | msg = "Vector and matrix expressions have been forbidden in this entry." 24 | with raises(UnableToParse, match=msg): 25 | evaluator("[[1, 2], [3, 4]]", {}, {}, {}, max_array_dim=0) 26 | msg = "Matrix expressions have been forbidden in this entry." 27 | with raises(UnableToParse, match=msg): 28 | evaluator("[[1, 2], [3, 4]]", {}, {}, {}, max_array_dim=1) 29 | msg = "Tensor expressions have been forbidden in this entry." 30 | with raises(UnableToParse, match=msg): 31 | evaluator("[[[1, 2], [3, 4]]]", {}, {}, {}, max_array_dim=2) 32 | 33 | # By default, this is fine 34 | evaluator("[[[1, 2], [3, 4]]]") 35 | 36 | # NOTE: These two examples are very brief. The vast majority of MathArray testing 37 | # is done in its own file. Essentially, all I want to do here is verify that 38 | # FormulaParser.evaluate (through evaluator) is calling the correct methods 39 | 40 | def test_math_arrays(): 41 | A = MathArray([ 42 | [1, 5], 43 | [4, -2] 44 | ]) 45 | v = MathArray([3, -2]) 46 | n = 3 47 | x = 4.2 48 | z = 2 + 3j 49 | variables = { 50 | 'A': A, 51 | 'v': v, 52 | 'n': n, 53 | 'x': x, 54 | 'z': z 55 | } 56 | 57 | expr = '(z*[[1, 5], [4, -2]]^n + 10*A/x)*v' 58 | result = evaluator(expr, variables, max_array_dim=2)[0] 59 | assert equal_as_arrays(result, (z*A**n + 10*A/x)*v) 60 | 61 | def test_triple_vector_product_raises_error(): 62 | # Since vector products are interpretted as dot products, they are 63 | # ambiguous, and we disallow them. 64 | 65 | variables = { 66 | 'i': MathArray([1, 0]), 67 | 'j': MathArray([0, 1]), 68 | } 69 | 70 | assert equal_as_arrays( 71 | evaluator("(i*i)*j", variables)[0], 72 | variables['j'] 73 | ) 74 | 75 | match = ("Multiplying three or more vectors is ambiguous. " 76 | "Please place parentheses around vector multiplications.") 77 | with raises(CalcError, match=match): 78 | evaluator("i*i*j", variables)[0] 79 | 80 | with raises(CalcError, match=match): 81 | evaluator("i*2*i*3*j", variables)[0] 82 | 83 | # Next example should raise an operator shape error, not a triple vec error 84 | match='Cannot divide by a vector' 85 | with raises(CalcError, match=match): 86 | evaluator("i*j/i*i*j", variables)[0] 87 | 88 | def test_matharray_errors_make_it_through(): 89 | """ 90 | There is some overlap between this test and the tests in test_math_array. 91 | 92 | Main goal here is to make sure numpy numerics are not introduced during 93 | evaluator(...) calls, because 94 | 95 | np.float64(1.0) + MathArray([1, 2, 3]) 96 | 97 | does not throw an error. 98 | """ 99 | 100 | v = MathArray([1, 2, 3]) 101 | variables = { 102 | 'v': v 103 | } 104 | with raises(CalcError, match="Cannot add/subtract"): 105 | evaluator('v*v + v', variables) 106 | 107 | with raises(CalcError, match="Cannot add/subtract"): 108 | evaluator('v*v - v', variables) 109 | 110 | with raises(CalcError, match="Cannot divide"): 111 | evaluator('v*v/v', variables) 112 | -------------------------------------------------------------------------------- /tests/helpers/calc/test_specify_domain.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from pytest import raises 4 | import numpy as np 5 | from voluptuous import Error 6 | from mitxgraders.helpers.calc import specify_domain 7 | from mitxgraders.helpers.calc.exceptions import DomainError, ArgumentError 8 | from mitxgraders.helpers.calc.math_array import equal_as_arrays, random_math_array 9 | from mitxgraders.exceptions import ConfigError 10 | 11 | def get_somefunc(display_name=None): 12 | 13 | if display_name: 14 | # Takes: scalar, [2, 3] matrix, 3-component vector, 2-component vector 15 | @specify_domain(input_shapes=[1, [2, 3], 3, 2], display_name=display_name) 16 | def somefunc(w, x, y, z): 17 | return w*x*y + z 18 | else: 19 | # Takes: scalar, [2, 3] matrix, 3-component vector, 2-component vector 20 | @specify_domain(input_shapes=[1, [2, 3], 3, 2]) 21 | def somefunc(w, x, y, z): 22 | return w*x*y + z 23 | 24 | return somefunc 25 | 26 | def get_somefunc_from_static_method(display_name=None): 27 | """ 28 | Uses specify_domain.make_decorator to decorate the function. 29 | Unlike author-facing specify_domain, make_decorator's shapes must ALL be tuples. 30 | """ 31 | shapes = [ 32 | (1,), # scalar 33 | (2, 3), # 2 by 3 matrix 34 | (3,), # vector of length 3 35 | (2,) # vector of length 2 36 | ] 37 | if display_name: 38 | # Takes: scalar, [2, 3] matrix, 3-component vector, 2-component vector 39 | 40 | @specify_domain.make_decorator(*shapes, display_name=display_name) 41 | def somefunc(w, x, y, z): 42 | return w*x*y + z 43 | else: 44 | # Takes: scalar, [2, 3] matrix, 3-component vector, 2-component vector 45 | @specify_domain.make_decorator(*shapes) 46 | def somefunc(w, x, y, z): 47 | return w*x*y + z 48 | 49 | return somefunc 50 | 51 | def test_correct_arguments_get_passed_to_function(): 52 | f = get_somefunc() 53 | 54 | w = np.random.uniform(-10, 10) 55 | x = random_math_array([2, 3]) 56 | y = random_math_array([3]) 57 | z = random_math_array([2]) 58 | 59 | assert equal_as_arrays(f(w, x, y, z), w*x*y + z) 60 | 61 | def test_incorrect_arguments_raise_errors(): 62 | f = get_somefunc() 63 | F = get_somefunc_from_static_method() 64 | 65 | w = np.random.uniform(-10, 10) 66 | x = random_math_array([2, 2]) 67 | y = np.random.uniform(-10, 10) 68 | z = random_math_array([2]) 69 | 70 | match = (r"There was an error evaluating function {0}\(...\)" 71 | r"\n1st input is ok: received a scalar as expected" 72 | r"\n2nd input has an error: received a matrix of shape \(rows: 2, cols: 2\), " 73 | r"expected a matrix of shape \(rows: 2, cols: 3\)" 74 | r"\n3rd input has an error: received a scalar, expected a vector of length 3" 75 | r"\n4th input is ok: received a vector of length 2 as expected") 76 | with raises(DomainError, match=match.format('somefunc')): 77 | f(w, x, y, z) 78 | with raises(DomainError, match=match.format('somefunc')): 79 | F(w, x, y, z) 80 | 81 | # Test display name 82 | g = get_somefunc('puppy') 83 | G = get_somefunc_from_static_method('puppy') 84 | with raises(DomainError, match=match.format('puppy')): 85 | g(w, x, y, z) 86 | with raises(DomainError, match=match.format('puppy')): 87 | G(w, x, y, z) 88 | 89 | def test_incorrect_number_of_inputs_raises_useful_error(): 90 | f = get_somefunc() 91 | match = r'Wrong number of arguments passed to somefunc\(...\): Expected 4 inputs, but received 2.' 92 | with raises(DomainError, match=match): 93 | f(1, 2) 94 | 95 | # Test min_length length argument error 96 | @specify_domain(input_shapes=[1], min_length=2) 97 | def testfunc(*args): 98 | return min(args) 99 | 100 | with raises(ArgumentError, match=r"Wrong number of arguments passed to testfunc\(...\): " 101 | r"Expected at least 2 inputs, but received 0."): 102 | testfunc() 103 | 104 | def test_author_facing_decorator_raises_errors_with_invalid_config(): 105 | 106 | match = r"required key not provided @ data\[u?'input_shapes'\]. Got None" 107 | with raises(Error, match=match): 108 | @specify_domain() 109 | def f(): 110 | pass 111 | 112 | match = (r"expected shape specification to be a positive integer, or a " 113 | r"list/tuple of positive integers \(min length 1, max length None\) @ " 114 | r"data\[u?'input_shapes'\]\[1\]. Got 0") 115 | with raises(Error, match=match): 116 | @specify_domain(input_shapes=[5, 0, [1, 2]]) 117 | def g(): 118 | pass 119 | 120 | match = ("SpecifyDomain was called with a specified min_length, which " 121 | "requires input_shapes to specify only a single shape. " 122 | "However, 2 shapes were provided.") 123 | with raises(ConfigError, match=match): 124 | @specify_domain(input_shapes=[5, 1], min_length=2) 125 | def h(*args): 126 | pass 127 | -------------------------------------------------------------------------------- /tests/helpers/test_validatorfuncs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests of validatorfuncs.py 3 | """ 4 | 5 | 6 | from pytest import raises 7 | from voluptuous import Invalid, truth 8 | from mitxgraders import RandomFunction 9 | from mitxgraders.helpers import validatorfuncs 10 | 11 | def test_validators(): 12 | """Tests of validatorfuncs.py that aren't covered elsewhere""" 13 | # PercentageString 14 | with raises(Invalid, match="Not a valid percentage string"): 15 | validatorfuncs.PercentageString("mess%") 16 | 17 | # ListOfType 18 | testfunc = validatorfuncs.ListOfType(int) 19 | assert testfunc([1, 2, 3]) == [1, 2, 3] 20 | 21 | # TupleOfType 22 | @truth 23 | def testvalidator(obj): 24 | """Returns true""" 25 | return True 26 | testfunc = validatorfuncs.TupleOfType(int, testvalidator) 27 | assert testfunc((-1,)) == (-1,) 28 | 29 | def test_argument_number_of_RandomFunction(): 30 | """Tests to make sure we can extract the number of inputs expected for a random function""" 31 | func = RandomFunction(input_dim=3).gen_sample() 32 | assert validatorfuncs.get_number_of_args(func) == 3 33 | -------------------------------------------------------------------------------- /tests/test_plugins.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that the plugin implementation is functioning 3 | """ 4 | 5 | 6 | from mitxgraders import * 7 | 8 | def test_plugins(): 9 | """ 10 | Tests that the functions loaded with the template can be accessed 11 | in all the appropriate ways 12 | """ 13 | assert plugins.template.plugin_test() == True 14 | assert plugin_test() == True 15 | -------------------------------------------------------------------------------- /tests/test_zip.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that python_lib.zip loads correctly 3 | """ 4 | 5 | 6 | 7 | import os 8 | from importlib import reload 9 | import pytest 10 | import mitxgraders 11 | 12 | 13 | @pytest.fixture() 14 | def loadzip(): 15 | """pytest fixture to dynamically load the library from python_lib.zip""" 16 | # Add python_lib.zip to the path for searching for modules 17 | import sys 18 | parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | zip_path = os.path.join(parent_dir, 'python_lib.zip') 20 | sys.path.insert(0, zip_path) 21 | # The 0 tells it to search this zip file before even the module directory 22 | # Hence, 23 | reload(mitxgraders) 24 | # loads mitxgraders from the zip file 25 | # Now, provide the zipfile library to our test functions 26 | yield mitxgraders 27 | # Before resuming, fix the system path 28 | del sys.path[0] 29 | # And restore the old version of the library 30 | reload(mitxgraders) 31 | 32 | def test_zipfile(loadzip): 33 | """Test that the plugins have loaded properly from the zip file""" 34 | # Make sure that we have the zip file 35 | assert loadzip.loaded_from == "python_lib.zip" 36 | grader = loadzip.StringGrader(answers="hello") 37 | expect = {'grade_decimal': 1, 'msg': '', 'ok': True} 38 | assert grader(None, "hello") == expect 39 | assert loadzip.plugins.template.plugin_test() 40 | assert loadzip.plugin_test() 41 | 42 | def test_notzipfile(): 43 | """Test that the mitxgraders library loads normally""" 44 | assert mitxgraders.loaded_from == "mitxgraders directory" 45 | -------------------------------------------------------------------------------- /tidy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Ensure that script is run in the correct directory 3 | SCRIPT_PATH=${0%/*} 4 | if [ "$0" != "$SCRIPT_PATH" ] && [ "$SCRIPT_PATH" != "" ]; then 5 | cd $SCRIPT_PATH 6 | fi 7 | 8 | # Make docs 9 | echo Making docs... 10 | mkdocs gh-deploy 11 | 12 | # Make zip file 13 | echo Making zip file... 14 | ./makezip.sh 15 | 16 | # Update course - no longer works, because edX changed their login process 17 | #echo Updating course... 18 | #cd course 19 | #./upload.sh 20 | #cd .. 21 | 22 | # Done! 23 | echo Done! 24 | -------------------------------------------------------------------------------- /voluptuous/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Alec Thomas 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | - Neither the name of SwapOff.org nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /voluptuous/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from voluptuous.schema_builder import * 4 | from voluptuous.validators import * 5 | from voluptuous.util import * 6 | from voluptuous.error import * 7 | 8 | __version__ = '0.11.5' 9 | __author__ = 'alecthomas' 10 | -------------------------------------------------------------------------------- /voluptuous/error.py: -------------------------------------------------------------------------------- 1 | 2 | class Error(Exception): 3 | """Base validation exception.""" 4 | 5 | 6 | class SchemaError(Error): 7 | """An error was encountered in the schema.""" 8 | 9 | 10 | class Invalid(Error): 11 | """The data was invalid. 12 | 13 | :attr msg: The error message. 14 | :attr path: The path to the error, as a list of keys in the source data. 15 | :attr error_message: The actual error message that was raised, as a 16 | string. 17 | 18 | """ 19 | 20 | def __init__(self, message, path=None, error_message=None, error_type=None): 21 | Error.__init__(self, message) 22 | self.path = path or [] 23 | self.error_message = error_message or message 24 | self.error_type = error_type 25 | 26 | @property 27 | def msg(self): 28 | return self.args[0] 29 | 30 | def __str__(self): 31 | path = ' @ data[%s]' % ']['.join(map(repr, self.path)) \ 32 | if self.path else '' 33 | output = Exception.__str__(self) 34 | if self.error_type: 35 | output += ' for ' + self.error_type 36 | return output + path 37 | 38 | def prepend(self, path): 39 | self.path = path + self.path 40 | 41 | 42 | class MultipleInvalid(Invalid): 43 | def __init__(self, errors=None): 44 | self.errors = errors[:] if errors else [] 45 | 46 | def __repr__(self): 47 | return 'MultipleInvalid(%r)' % self.errors 48 | 49 | @property 50 | def msg(self): 51 | return self.errors[0].msg 52 | 53 | @property 54 | def path(self): 55 | return self.errors[0].path 56 | 57 | @property 58 | def error_message(self): 59 | return self.errors[0].error_message 60 | 61 | def add(self, error): 62 | self.errors.append(error) 63 | 64 | def __str__(self): 65 | return str(self.errors[0]) 66 | 67 | def prepend(self, path): 68 | for error in self.errors: 69 | error.prepend(path) 70 | 71 | 72 | class RequiredFieldInvalid(Invalid): 73 | """Required field was missing.""" 74 | 75 | 76 | class ObjectInvalid(Invalid): 77 | """The value we found was not an object.""" 78 | 79 | 80 | class DictInvalid(Invalid): 81 | """The value found was not a dict.""" 82 | 83 | 84 | class ExclusiveInvalid(Invalid): 85 | """More than one value found in exclusion group.""" 86 | 87 | 88 | class InclusiveInvalid(Invalid): 89 | """Not all values found in inclusion group.""" 90 | 91 | 92 | class SequenceTypeInvalid(Invalid): 93 | """The type found is not a sequence type.""" 94 | 95 | 96 | class TypeInvalid(Invalid): 97 | """The value was not of required type.""" 98 | 99 | 100 | class ValueInvalid(Invalid): 101 | """The value was found invalid by evaluation function.""" 102 | 103 | 104 | class ContainsInvalid(Invalid): 105 | """List does not contain item""" 106 | 107 | 108 | class ScalarInvalid(Invalid): 109 | """Scalars did not match.""" 110 | 111 | 112 | class CoerceInvalid(Invalid): 113 | """Impossible to coerce value to type.""" 114 | 115 | 116 | class AnyInvalid(Invalid): 117 | """The value did not pass any validator.""" 118 | 119 | 120 | class AllInvalid(Invalid): 121 | """The value did not pass all validators.""" 122 | 123 | 124 | class MatchInvalid(Invalid): 125 | """The value does not match the given regular expression.""" 126 | 127 | 128 | class RangeInvalid(Invalid): 129 | """The value is not in given range.""" 130 | 131 | 132 | class TrueInvalid(Invalid): 133 | """The value is not True.""" 134 | 135 | 136 | class FalseInvalid(Invalid): 137 | """The value is not False.""" 138 | 139 | 140 | class BooleanInvalid(Invalid): 141 | """The value is not a boolean.""" 142 | 143 | 144 | class UrlInvalid(Invalid): 145 | """The value is not a url.""" 146 | 147 | 148 | class EmailInvalid(Invalid): 149 | """The value is not a email.""" 150 | 151 | 152 | class FileInvalid(Invalid): 153 | """The value is not a file.""" 154 | 155 | 156 | class DirInvalid(Invalid): 157 | """The value is not a directory.""" 158 | 159 | 160 | class PathInvalid(Invalid): 161 | """The value is not a path.""" 162 | 163 | 164 | class LiteralInvalid(Invalid): 165 | """The literal values do not match.""" 166 | 167 | 168 | class LengthInvalid(Invalid): 169 | pass 170 | 171 | 172 | class DatetimeInvalid(Invalid): 173 | """The value is not a formatted datetime string.""" 174 | 175 | 176 | class DateInvalid(Invalid): 177 | """The value is not a formatted date string.""" 178 | 179 | 180 | class InInvalid(Invalid): 181 | pass 182 | 183 | 184 | class NotInInvalid(Invalid): 185 | pass 186 | 187 | 188 | class ExactSequenceInvalid(Invalid): 189 | pass 190 | 191 | 192 | class NotEnoughValid(Invalid): 193 | """The value did not pass enough validations.""" 194 | 195 | 196 | class TooManyValid(Invalid): 197 | """The value passed more than expected validations.""" 198 | -------------------------------------------------------------------------------- /voluptuous/humanize.py: -------------------------------------------------------------------------------- 1 | from voluptuous import Invalid, MultipleInvalid 2 | from voluptuous.error import Error 3 | 4 | 5 | MAX_VALIDATION_ERROR_ITEM_LENGTH = 500 6 | 7 | 8 | def _nested_getitem(data, path): 9 | for item_index in path: 10 | try: 11 | data = data[item_index] 12 | except (KeyError, IndexError, TypeError): 13 | # The index is not present in the dictionary, list or other 14 | # indexable or data is not subscriptable 15 | return None 16 | return data 17 | 18 | 19 | def humanize_error(data, validation_error, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH): 20 | """ Provide a more helpful + complete validation error message than that provided automatically 21 | Invalid and MultipleInvalid do not include the offending value in error messages, 22 | and MultipleInvalid.__str__ only provides the first error. 23 | """ 24 | if isinstance(validation_error, MultipleInvalid): 25 | return '\n'.join(sorted( 26 | humanize_error(data, sub_error, max_sub_error_length) 27 | for sub_error in validation_error.errors 28 | )) 29 | else: 30 | offending_item_summary = repr(_nested_getitem(data, validation_error.path)) 31 | if len(offending_item_summary) > max_sub_error_length: 32 | offending_item_summary = offending_item_summary[:max_sub_error_length - 3] + '...' 33 | return '%s. Got %s' % (validation_error, offending_item_summary) 34 | 35 | 36 | def validate_with_humanized_errors(data, schema, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH): 37 | try: 38 | return schema(data) 39 | except (Invalid, MultipleInvalid) as e: 40 | raise Error(humanize_error(data, e, max_sub_error_length)) 41 | -------------------------------------------------------------------------------- /voluptuous/util.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from voluptuous.error import LiteralInvalid, TypeInvalid 4 | from voluptuous.schema_builder import default_factory 5 | 6 | __author__ = 'tusharmakkar08' 7 | 8 | 9 | def _fix_str(v): 10 | if sys.version_info[0] == 2 and isinstance(v, unicode): 11 | s = v 12 | else: 13 | s = str(v) 14 | return s 15 | 16 | 17 | def Lower(v): 18 | """Transform a string to lower case. 19 | 20 | >>> s = Schema(Lower) 21 | >>> s('HI') 22 | 'hi' 23 | """ 24 | return _fix_str(v).lower() 25 | 26 | 27 | def Upper(v): 28 | """Transform a string to upper case. 29 | 30 | >>> s = Schema(Upper) 31 | >>> s('hi') 32 | 'HI' 33 | """ 34 | return _fix_str(v).upper() 35 | 36 | 37 | def Capitalize(v): 38 | """Capitalise a string. 39 | 40 | >>> s = Schema(Capitalize) 41 | >>> s('hello world') 42 | 'Hello world' 43 | """ 44 | return _fix_str(v).capitalize() 45 | 46 | 47 | def Title(v): 48 | """Title case a string. 49 | 50 | >>> s = Schema(Title) 51 | >>> s('hello world') 52 | 'Hello World' 53 | """ 54 | return _fix_str(v).title() 55 | 56 | 57 | def Strip(v): 58 | """Strip whitespace from a string. 59 | 60 | >>> s = Schema(Strip) 61 | >>> s(' hello world ') 62 | 'hello world' 63 | """ 64 | return _fix_str(v).strip() 65 | 66 | 67 | class DefaultTo(object): 68 | """Sets a value to default_value if none provided. 69 | 70 | >>> s = Schema(DefaultTo(42)) 71 | >>> s(None) 72 | 42 73 | >>> s = Schema(DefaultTo(list)) 74 | >>> s(None) 75 | [] 76 | """ 77 | 78 | def __init__(self, default_value, msg=None): 79 | self.default_value = default_factory(default_value) 80 | self.msg = msg 81 | 82 | def __call__(self, v): 83 | if v is None: 84 | v = self.default_value() 85 | return v 86 | 87 | def __repr__(self): 88 | return 'DefaultTo(%s)' % (self.default_value(),) 89 | 90 | 91 | class SetTo(object): 92 | """Set a value, ignoring any previous value. 93 | 94 | >>> s = Schema(validators.Any(int, SetTo(42))) 95 | >>> s(2) 96 | 2 97 | >>> s("foo") 98 | 42 99 | """ 100 | 101 | def __init__(self, value): 102 | self.value = default_factory(value) 103 | 104 | def __call__(self, v): 105 | return self.value() 106 | 107 | def __repr__(self): 108 | return 'SetTo(%s)' % (self.value(),) 109 | 110 | 111 | class Set(object): 112 | """Convert a list into a set. 113 | 114 | >>> s = Schema(Set()) 115 | >>> s([]) == set([]) 116 | True 117 | >>> s([1, 2]) == set([1, 2]) 118 | True 119 | >>> with raises(Invalid, regex="^cannot be presented as set: "): 120 | ... s([set([1, 2]), set([3, 4])]) 121 | """ 122 | 123 | def __init__(self, msg=None): 124 | self.msg = msg 125 | 126 | def __call__(self, v): 127 | try: 128 | set_v = set(v) 129 | except Exception as e: 130 | raise TypeInvalid( 131 | self.msg or 'cannot be presented as set: {0}'.format(e)) 132 | return set_v 133 | 134 | def __repr__(self): 135 | return 'Set()' 136 | 137 | 138 | class Literal(object): 139 | def __init__(self, lit): 140 | self.lit = lit 141 | 142 | def __call__(self, value, msg=None): 143 | if self.lit != value: 144 | raise LiteralInvalid( 145 | msg or '%s not match for %s' % (value, self.lit) 146 | ) 147 | else: 148 | return self.lit 149 | 150 | def __str__(self): 151 | return str(self.lit) 152 | 153 | def __repr__(self): 154 | return repr(self.lit) 155 | 156 | 157 | def u(x): 158 | if sys.version_info < (3,): 159 | return unicode(x) 160 | else: 161 | return x 162 | --------------------------------------------------------------------------------