qc-grader 2026.6.18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qc_grader-2026.6.18/.github/workflows/ci.yml +18 -0
- qc_grader-2026.6.18/.github/workflows/release.yml +147 -0
- qc_grader-2026.6.18/.gitignore +74 -0
- qc_grader-2026.6.18/AGENTS.md +17 -0
- qc_grader-2026.6.18/CLAUDE.md +1 -0
- qc_grader-2026.6.18/CODE_OF_CONDUCT.md +9 -0
- qc_grader-2026.6.18/CONTRIBUTING.md +279 -0
- qc_grader-2026.6.18/Justfile +10 -0
- qc_grader-2026.6.18/LICENSE.txt +203 -0
- qc_grader-2026.6.18/PKG-INFO +22 -0
- qc_grader-2026.6.18/README.md +76 -0
- qc_grader-2026.6.18/pyproject.toml +53 -0
- qc_grader-2026.6.18/qc_grader/__init__.py +15 -0
- qc_grader-2026.6.18/qc_grader/challenges/__init__.py +9 -0
- qc_grader-2026.6.18/qc_grader/challenges/common/__init__.py +9 -0
- qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/__init__.py +9 -0
- qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/lab_hadron.py +220 -0
- qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/lab_qmoo.py +154 -0
- qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/lab_skqd.py +172 -0
- qc_grader-2026.6.18/qc_grader/challenges/common/r2p_2026/qmoo_files.py +65 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/__init__.py +127 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab0.py +105 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab1.py +76 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab2.py +200 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab3.py +272 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab4a.py +44 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab4b.py +408 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab4b_test.py +671 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/lab4c.py +174 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/utils/lab4c_test_occ.npy +0 -0
- qc_grader-2026.6.18/qc_grader/challenges/qgss_2026/utils/lab4c_test_samples.npy +0 -0
- qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_canada/__init__.py +78 -0
- qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_canada/lab_hadron.py +39 -0
- qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_canada/lab_qmoo.py +25 -0
- qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_canada/lab_skqd.py +29 -0
- qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_us/__init__.py +78 -0
- qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_us/lab_hadron.py +39 -0
- qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_us/lab_qmoo.py +25 -0
- qc_grader-2026.6.18/qc_grader/challenges/r2p_2026_us/lab_skqd.py +29 -0
- qc_grader-2026.6.18/qc_grader/challenges/test_challenges/__init__.py +9 -0
- qc_grader-2026.6.18/qc_grader/challenges/test_challenges/individual.py +51 -0
- qc_grader-2026.6.18/qc_grader/challenges/test_challenges/team.py +56 -0
- qc_grader-2026.6.18/qc_grader/custom_encoder/__init__.py +14 -0
- qc_grader-2026.6.18/qc_grader/custom_encoder/json_encoder.py +78 -0
- qc_grader-2026.6.18/qc_grader/custom_encoder/json_encoder_test.py +60 -0
- qc_grader-2026.6.18/qc_grader/custom_encoder/serializer.py +151 -0
- qc_grader-2026.6.18/qc_grader/grader/__init__.py +9 -0
- qc_grader-2026.6.18/qc_grader/grader/api.py +46 -0
- qc_grader-2026.6.18/qc_grader/grader/auth.py +77 -0
- qc_grader-2026.6.18/qc_grader/grader/env.py +24 -0
- qc_grader-2026.6.18/qc_grader/grader/grade.py +202 -0
- qc_grader-2026.6.18/qc_grader/grader/grade_test.py +185 -0
- qc_grader-2026.6.18/uv.lock +1510 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: astral-sh/setup-uv@v5
|
|
12
|
+
with:
|
|
13
|
+
python-version: "3.10"
|
|
14
|
+
- uses: extractions/setup-just@v3
|
|
15
|
+
- name: install
|
|
16
|
+
run: uv sync
|
|
17
|
+
- run: just lint
|
|
18
|
+
- run: just test
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- 'qc_grader/__init__.py'
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
validate:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
outputs:
|
|
15
|
+
version: ${{ steps.get-version.outputs.version }}
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Get version from qc_grader/__init__.py
|
|
21
|
+
id: get-version
|
|
22
|
+
run: |
|
|
23
|
+
VERSION=$(python -c "import re; print(re.search(r'__version__\s*=\s*\"([^\"]+)\"', open('qc_grader/__init__.py').read()).group(1))")
|
|
24
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
25
|
+
echo "Version to release: $VERSION"
|
|
26
|
+
|
|
27
|
+
- name: Validate version format (YEAR.MONTH.DAY, no leading zeros)
|
|
28
|
+
run: |
|
|
29
|
+
VERSION="${{ steps.get-version.outputs.version }}"
|
|
30
|
+
if ! [[ "$VERSION" =~ ^[0-9]{4}\.(1[0-2]|[1-9])\.(3[01]|[12][0-9]|[1-9])(\.[0-9]+)?$ ]]; then
|
|
31
|
+
echo "Error: Invalid version format: $VERSION"
|
|
32
|
+
echo "Expected a date-based version YEAR.MONTH.DAY with no leading zeros, e.g. 2026.6.18"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
- name: Check if tag exists
|
|
37
|
+
run: |
|
|
38
|
+
VERSION="${{ steps.get-version.outputs.version }}"
|
|
39
|
+
if git ls-remote --tags origin "v$VERSION" | grep -q .; then
|
|
40
|
+
echo "Error: Tag v$VERSION already exists"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
test:
|
|
45
|
+
needs: validate
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- uses: astral-sh/setup-uv@v5
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.10"
|
|
52
|
+
- uses: extractions/setup-just@v3
|
|
53
|
+
- name: install
|
|
54
|
+
run: uv sync
|
|
55
|
+
- run: just lint
|
|
56
|
+
- run: just test
|
|
57
|
+
|
|
58
|
+
build:
|
|
59
|
+
needs: [validate, test]
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- uses: astral-sh/setup-uv@v5
|
|
65
|
+
|
|
66
|
+
- name: Build distribution
|
|
67
|
+
run: uv build
|
|
68
|
+
|
|
69
|
+
- name: Store the distribution packages
|
|
70
|
+
uses: actions/upload-artifact@v4
|
|
71
|
+
with:
|
|
72
|
+
name: python-package-distributions
|
|
73
|
+
path: dist/
|
|
74
|
+
|
|
75
|
+
publish-to-pypi:
|
|
76
|
+
needs: build
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
environment:
|
|
79
|
+
name: pypi
|
|
80
|
+
url: https://pypi.org/project/qc_grader/
|
|
81
|
+
permissions:
|
|
82
|
+
id-token: write
|
|
83
|
+
|
|
84
|
+
steps:
|
|
85
|
+
- name: Download all the dists
|
|
86
|
+
uses: actions/download-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
name: python-package-distributions
|
|
89
|
+
path: dist/
|
|
90
|
+
|
|
91
|
+
- name: Publish distribution to PyPI
|
|
92
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
93
|
+
|
|
94
|
+
create-release:
|
|
95
|
+
needs: [validate, publish-to-pypi]
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
permissions:
|
|
98
|
+
contents: write
|
|
99
|
+
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v4
|
|
102
|
+
with:
|
|
103
|
+
fetch-depth: 0
|
|
104
|
+
|
|
105
|
+
- name: Create and push tag
|
|
106
|
+
run: |
|
|
107
|
+
VERSION="${{ needs.validate.outputs.version }}"
|
|
108
|
+
git config user.name "github-actions[bot]"
|
|
109
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
110
|
+
git tag -a "v$VERSION" -m "Release version $VERSION"
|
|
111
|
+
git push origin "v$VERSION"
|
|
112
|
+
|
|
113
|
+
- name: Write release notes
|
|
114
|
+
env:
|
|
115
|
+
VERSION: ${{ needs.validate.outputs.version }}
|
|
116
|
+
REPO: ${{ github.repository }}
|
|
117
|
+
run: |
|
|
118
|
+
# The previous release tag, if any. HEAD is already tagged v$VERSION,
|
|
119
|
+
# so look at HEAD^ to find the prior release.
|
|
120
|
+
PREV=$(git describe --tags --abbrev=0 --match 'v*' "v$VERSION^" 2>/dev/null || true)
|
|
121
|
+
if [ -n "$PREV" ]; then
|
|
122
|
+
URL="https://github.com/$REPO/compare/$PREV...v$VERSION"
|
|
123
|
+
else
|
|
124
|
+
URL="https://github.com/$REPO/commits/v$VERSION"
|
|
125
|
+
fi
|
|
126
|
+
{
|
|
127
|
+
echo "**What changed:** see the commits included in this release:"
|
|
128
|
+
echo ""
|
|
129
|
+
echo "$URL"
|
|
130
|
+
} > release-notes.md
|
|
131
|
+
cat release-notes.md
|
|
132
|
+
|
|
133
|
+
- name: Download distribution artifacts
|
|
134
|
+
uses: actions/download-artifact@v4
|
|
135
|
+
with:
|
|
136
|
+
name: python-package-distributions
|
|
137
|
+
path: dist/
|
|
138
|
+
|
|
139
|
+
- name: Create GitHub Release
|
|
140
|
+
env:
|
|
141
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
142
|
+
VERSION: ${{ needs.validate.outputs.version }}
|
|
143
|
+
run: |
|
|
144
|
+
gh release create "v$VERSION" \
|
|
145
|
+
--title "v$VERSION" \
|
|
146
|
+
--notes-file release-notes.md \
|
|
147
|
+
dist/*
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
|
|
5
|
+
.Python
|
|
6
|
+
build/
|
|
7
|
+
develop-eggs/
|
|
8
|
+
dist/
|
|
9
|
+
downloads/
|
|
10
|
+
eggs/
|
|
11
|
+
.eggs/
|
|
12
|
+
lib/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
pip-wheel-metadata/
|
|
19
|
+
share/python-wheels/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.installed.cfg
|
|
22
|
+
*.egg
|
|
23
|
+
MANIFEST
|
|
24
|
+
|
|
25
|
+
*.manifest
|
|
26
|
+
*.spec
|
|
27
|
+
|
|
28
|
+
pip-log.txt
|
|
29
|
+
pip-delete-this-directory.txt
|
|
30
|
+
|
|
31
|
+
htmlcov/
|
|
32
|
+
.tox/
|
|
33
|
+
.nox/
|
|
34
|
+
.coverage
|
|
35
|
+
.coverage.*
|
|
36
|
+
.cache
|
|
37
|
+
nosetests.xml
|
|
38
|
+
coverage.xml
|
|
39
|
+
*.cover
|
|
40
|
+
*.py,cover
|
|
41
|
+
.hypothesis/
|
|
42
|
+
.pytest_cache/
|
|
43
|
+
|
|
44
|
+
.ipynb_checkpoints
|
|
45
|
+
profile_default/
|
|
46
|
+
ipython_config.py
|
|
47
|
+
|
|
48
|
+
.python-version
|
|
49
|
+
|
|
50
|
+
__pypackages__/
|
|
51
|
+
|
|
52
|
+
.mypy_cache/
|
|
53
|
+
.dmypy.json
|
|
54
|
+
dmypy.json
|
|
55
|
+
|
|
56
|
+
.pyre/
|
|
57
|
+
|
|
58
|
+
.DS_Store
|
|
59
|
+
|
|
60
|
+
.vscode
|
|
61
|
+
.idea/
|
|
62
|
+
|
|
63
|
+
setup.cfg
|
|
64
|
+
qc_archives/
|
|
65
|
+
|
|
66
|
+
# Environments
|
|
67
|
+
.env
|
|
68
|
+
.venv
|
|
69
|
+
env/
|
|
70
|
+
venv/
|
|
71
|
+
ENV/
|
|
72
|
+
env.bak/
|
|
73
|
+
venv.bak/
|
|
74
|
+
.claude/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Agents.md
|
|
2
|
+
|
|
3
|
+
## About the project
|
|
4
|
+
|
|
5
|
+
This project is a Python library for students to submit their answers to quantum computing challenges to a grader server. The server will return back whether the answer was correct or not.
|
|
6
|
+
|
|
7
|
+
Users use this Python library through a REPL or Jupyter notebook, rather than a traditional Python program.
|
|
8
|
+
|
|
9
|
+
Users are primarily students and are sometimes beginners to either programming or quantum computing. So, where reasonable, we try to make the program user-friendly, such as useful error messages.
|
|
10
|
+
|
|
11
|
+
## Workflows
|
|
12
|
+
|
|
13
|
+
* Format: `just fmt`
|
|
14
|
+
* Linter and type checker: `just lint`
|
|
15
|
+
* Test: `just test`
|
|
16
|
+
* Add new labs and exercises: refer to `CONTRIBUTING.md`
|
|
17
|
+
* Releases: refer to `CONTRIBUTING.md`. All user-facing changes should have a new release.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- Copyright Contributors to the Qiskit project. -->
|
|
2
|
+
|
|
3
|
+
# Code of Conduct
|
|
4
|
+
All members of this project agree to adhere to the Qiskit Code of Conduct listed at [https://github.com/Qiskit/qiskit/blob/master/CODE_OF_CONDUCT.md](https://github.com/Qiskit/qiskit/blob/master/CODE_OF_CONDUCT.md)
|
|
5
|
+
|
|
6
|
+
----
|
|
7
|
+
|
|
8
|
+
License: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/),
|
|
9
|
+
Copyright Contributors to Qiskit.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
* [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
|
6
|
+
* [Just](https://just.systems/man/en/)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
uv sync
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
(uv automatically creates and manages a virtual environment.)
|
|
15
|
+
|
|
16
|
+
## Format code
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
just fmt
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Lint
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
just lint
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
To run the individual linters:
|
|
29
|
+
|
|
30
|
+
* Ruff: `uv run ruff check`
|
|
31
|
+
* Ty (type check): `uv run ty check`
|
|
32
|
+
|
|
33
|
+
## Tests
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
just test
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Update dependencies
|
|
40
|
+
|
|
41
|
+
([Original documentation](https://docs.astral.sh/uv/concepts/projects/dependencies/))
|
|
42
|
+
|
|
43
|
+
Add a dependency:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
uv add <dependency>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Add a dev dependency:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
uv add <dependency> --dev
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Release a new version
|
|
56
|
+
|
|
57
|
+
You should release a new version any time you make user-facing changes.
|
|
58
|
+
|
|
59
|
+
Releasing is automated. To cut a release, update `__version__` in
|
|
60
|
+
[`qc_grader/__init__.py`](qc_grader/__init__.py) to **today's date** and merge to `main`.
|
|
61
|
+
A GitHub Actions workflow then builds the package, publishes it to PyPI, and creates a
|
|
62
|
+
GitHub Release whose notes link to the commits included in that release.
|
|
63
|
+
|
|
64
|
+
### Version format
|
|
65
|
+
|
|
66
|
+
Versions are date-based, written as **`YEAR.MONTH.DAY`** with **no leading zeros**:
|
|
67
|
+
|
|
68
|
+
* The year comes first, then the month, then the day (`YEAR.MONTH.DAY`).
|
|
69
|
+
* Do not pad with zeros: use `6`, not `06`.
|
|
70
|
+
|
|
71
|
+
For example, a release made on **18 June 2026** is version `2026.6.18`.
|
|
72
|
+
|
|
73
|
+
| Date | Version |
|
|
74
|
+
| -------------- | ----------- |
|
|
75
|
+
| 18 June 2026 | `2026.6.18` |
|
|
76
|
+
| 1 December 2026 | `2026.12.1` |
|
|
77
|
+
| 5 January 2027 | `2027.1.5` |
|
|
78
|
+
|
|
79
|
+
If you need to release more than once on the same day, add a counter at the end, starting
|
|
80
|
+
at `.1`: the second release on 18 June 2026 is `2026.6.18.1`, the third is `2026.6.18.2`,
|
|
81
|
+
and so on.
|
|
82
|
+
|
|
83
|
+
Versions only ever move forward — we always release from the latest `main` and never
|
|
84
|
+
maintain older versions in parallel.
|
|
85
|
+
|
|
86
|
+
## Run the client
|
|
87
|
+
|
|
88
|
+
Use this workflow to test the Python client against the Grader server.
|
|
89
|
+
|
|
90
|
+
### Initial setup
|
|
91
|
+
|
|
92
|
+
You must create a Quantum API token for an account with at least one instance.
|
|
93
|
+
|
|
94
|
+
Prod server:
|
|
95
|
+
|
|
96
|
+
1. Use https://quantum.cloud.ibm.com to create the API key
|
|
97
|
+
2. Save the key by running `uv run python`, then this code:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from qiskit_ibm_runtime import QiskitRuntimeService
|
|
101
|
+
|
|
102
|
+
QiskitRuntimeService.save_account(
|
|
103
|
+
token="<your-api-key>",
|
|
104
|
+
instance="<CRN>",
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
3. Close the REPL.
|
|
108
|
+
|
|
109
|
+
Staging or local development server:
|
|
110
|
+
|
|
111
|
+
1. Use https://quantum.test.cloud.ibm.com to create the API key.
|
|
112
|
+
2. Save the key by running `uv run python`, then this code:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from qiskit_ibm_runtime import QiskitRuntimeService
|
|
116
|
+
|
|
117
|
+
QiskitRuntimeService.save_account(
|
|
118
|
+
token="<your-api-key>",
|
|
119
|
+
instance="<CRN>",
|
|
120
|
+
name="grader-staging",
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
3. Close the REPL.
|
|
124
|
+
|
|
125
|
+
### How to run
|
|
126
|
+
|
|
127
|
+
1. Launch a Python REPL:
|
|
128
|
+
- Prod server: `uv run python`
|
|
129
|
+
- Staging server: `STAGING=1 uv run python`
|
|
130
|
+
- Local development server: `DEV=1 uv run python`
|
|
131
|
+
2. In the REPL, import and run your exercises. For example:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
>>> from qc_grader.challenges.qgss_2026 import grade_lab0_ex1
|
|
135
|
+
>>> grade_lab0_ex1()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For developers testing how the server behaves, you can use the files from `qc_grader.challenges.test_challenges`, such as `grade_success` from `qc_grader.challenges.test_challenges.individual`.
|
|
139
|
+
|
|
140
|
+
## Adding a new challenge
|
|
141
|
+
|
|
142
|
+
Create a new folder under `qc_grader/challenges` with the name of the challenge. This folder should contain:
|
|
143
|
+
|
|
144
|
+
* A file for each lab (such as `lab0.py`, `lab2.py`)
|
|
145
|
+
* An `__init__.py`, which imports and re-exports the grading functions from your labs.
|
|
146
|
+
|
|
147
|
+
Every challenge must also export a `check_progress` function so users can see how far they've gotten:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from qc_grader.grader.grade import create_check_progress_function
|
|
151
|
+
|
|
152
|
+
# Replace the string with the name of your challenge
|
|
153
|
+
check_progress = create_check_progress_function("...")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Users call `check_progress()` (no arguments) to print a per-lab and per-exercise breakdown of their submissions plus a challenge-wide aggregate.
|
|
157
|
+
|
|
158
|
+
If your challenge is a team challenge, you should also export a `join_team` function so users can register with a team when they start.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from qc_grader.grader.grade import create_join_team_function
|
|
162
|
+
|
|
163
|
+
# Replace the string with the name of your challenge
|
|
164
|
+
join_team = create_join_team_function("...")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Users must call `join_team()` with their team name to participate in a team challenge. They can switch teams any time.
|
|
168
|
+
|
|
169
|
+
You may find it easier to copy an existing challenge and modify it.
|
|
170
|
+
|
|
171
|
+
## Adding new labs
|
|
172
|
+
|
|
173
|
+
A *lab* is a single Python file corresponding to a Jupyter notebook that users receive. Each *challenge* has one or more labs. When you add new exercises to the server, add a matching Python file here so that users can call grading functions from their Jupyter notebooks.
|
|
174
|
+
|
|
175
|
+
Create `qc_grader/challenges/{challenge}/{lab}.py`, e.g. `qc_grader/challenges/qgss_2027/lab1.py`.
|
|
176
|
+
|
|
177
|
+
The `_CHALLENGE` and `_LAB` constants, and each exercise string (e.g., `"ex1"`), must exactly match the identifiers configured on the server. These are permanent: once a challenge is live, changing them breaks existing notebook submissions.
|
|
178
|
+
|
|
179
|
+
A minimal lab file:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# qc_grader/challenges/qgss_2027/lab1.py
|
|
183
|
+
from typing import Any
|
|
184
|
+
|
|
185
|
+
from typeguard import typechecked
|
|
186
|
+
|
|
187
|
+
from qc_grader.grader.grade import grade_answer
|
|
188
|
+
|
|
189
|
+
_CHALLENGE = "qgss_2027"
|
|
190
|
+
_LAB = "lab1"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _grade(answer: Any, exercise: str) -> None:
|
|
194
|
+
grade_answer(answer, lab=_LAB, exercise=exercise, challenge=_CHALLENGE)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@typechecked
|
|
198
|
+
def grade_lab1_ex1(answer: str) -> None:
|
|
199
|
+
_grade(answer, "ex1")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@typechecked
|
|
203
|
+
def grade_lab1_ex2(answer: int) -> None:
|
|
204
|
+
_grade(answer, "ex2")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Then, export every grading function from the challenge package's `__init__.py`:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
# qc_grader/challenges/qgss_2027/__init__.py
|
|
211
|
+
from .lab1 import grade_lab1_ex1, grade_lab1_ex2
|
|
212
|
+
|
|
213
|
+
__all__ = ["grade_lab1_ex1", "grade_lab1_ex2"]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Users can then import your functions like this:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from qc_grader.challenges.qgss_2027 import grade_lab1_ex1
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Type validation
|
|
223
|
+
|
|
224
|
+
All grading functions must use the `@typechecked` decorator from `typeguard` and precise type hints on the answer parameter. This lets the client reject submissions with the wrong data type before they reach the server.
|
|
225
|
+
|
|
226
|
+
Use the most specific type that describes what the user should submit — `QuantumCircuit`, `Statevector`, `int`, `float`, etc. Avoid `Any` or bare `dict` and bare `list`.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from typeguard import typechecked
|
|
230
|
+
|
|
231
|
+
@typechecked
|
|
232
|
+
def grade_lab1_ex1(arg1: str, arg2: list[int], arg3: QuantumCircuit) -> None:
|
|
233
|
+
...
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### Dictionaries with required keys
|
|
237
|
+
|
|
238
|
+
If the user submits a dictionary with specific keys, use `typing.TypedDict` rather than a generic `dict`. `TypedDict` allows `typechecked` to validate each key's name and type:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from typing import TypedDict
|
|
242
|
+
|
|
243
|
+
from typeguard import typechecked
|
|
244
|
+
|
|
245
|
+
Ex1Input = TypedDict("Ex1Input", {"0": int, "1": int})
|
|
246
|
+
|
|
247
|
+
@typechecked
|
|
248
|
+
def grade_lab0_ex1(counts: Ex1Input) -> None:
|
|
249
|
+
...
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Multiple accepted types
|
|
253
|
+
|
|
254
|
+
Use a union (`|`) to accept more than one type:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
@typechecked
|
|
258
|
+
def grade_lab0_ex1(answer: int | float) -> None:
|
|
259
|
+
...
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### Flexible types with transformation
|
|
263
|
+
|
|
264
|
+
It is often helpful to accept a more flexible data type and transform it before sending to the server. When doing so, anticipate likely user mistakes and raise a `ValueError` if they violate your assumptions. For example, this accepts either a `float` or an `ndarray` (useful when users are working with NumPy) and validates that the array is a scalar:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from typeguard import typechecked
|
|
268
|
+
|
|
269
|
+
@typechecked
|
|
270
|
+
def grade_lab0_ex1(exp_val: np.ndarray | float) -> None:
|
|
271
|
+
arr = np.asarray(exp_val)
|
|
272
|
+
if arr.ndim != 0 and arr.size != 1:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
f"exp_val must be a scalar, got shape {arr.shape}. "
|
|
275
|
+
f"Use result[0].data.evs (not result.data.evs) for a single expectation value."
|
|
276
|
+
)
|
|
277
|
+
exp_val = float(arr.flat[0])
|
|
278
|
+
_grade(exp_val, "ex1")
|
|
279
|
+
```
|