quantum-exercises 0.8.0__py3-none-any.whl

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.
Files changed (162) hide show
  1. quantum_exercises/__init__.py +44 -0
  2. quantum_exercises/_course/exercises/01_environment/README.md +34 -0
  3. quantum_exercises/_course/exercises/01_environment/check.py +63 -0
  4. quantum_exercises/_course/exercises/01_environment/exercise.py +11 -0
  5. quantum_exercises/_course/exercises/01_environment/hints.md +30 -0
  6. quantum_exercises/_course/exercises/01_environment/meta.toml +4 -0
  7. quantum_exercises/_course/exercises/01_environment/solution.py +5 -0
  8. quantum_exercises/_course/exercises/01_environment/template.py +11 -0
  9. quantum_exercises/_course/exercises/02_dictionaries/README.md +41 -0
  10. quantum_exercises/_course/exercises/02_dictionaries/check.py +91 -0
  11. quantum_exercises/_course/exercises/02_dictionaries/exercise.py +40 -0
  12. quantum_exercises/_course/exercises/02_dictionaries/hints.md +40 -0
  13. quantum_exercises/_course/exercises/02_dictionaries/meta.toml +4 -0
  14. quantum_exercises/_course/exercises/02_dictionaries/solution.py +15 -0
  15. quantum_exercises/_course/exercises/02_dictionaries/template.py +40 -0
  16. quantum_exercises/_course/exercises/03_first_circuit/README.md +39 -0
  17. quantum_exercises/_course/exercises/03_first_circuit/check.py +49 -0
  18. quantum_exercises/_course/exercises/03_first_circuit/exercise.py +13 -0
  19. quantum_exercises/_course/exercises/03_first_circuit/hints.md +28 -0
  20. quantum_exercises/_course/exercises/03_first_circuit/meta.toml +4 -0
  21. quantum_exercises/_course/exercises/03_first_circuit/solution.py +6 -0
  22. quantum_exercises/_course/exercises/03_first_circuit/template.py +13 -0
  23. quantum_exercises/_course/exercises/04_measurement/README.md +48 -0
  24. quantum_exercises/_course/exercises/04_measurement/check.py +88 -0
  25. quantum_exercises/_course/exercises/04_measurement/exercise.py +25 -0
  26. quantum_exercises/_course/exercises/04_measurement/hints.md +31 -0
  27. quantum_exercises/_course/exercises/04_measurement/meta.toml +4 -0
  28. quantum_exercises/_course/exercises/04_measurement/solution.py +13 -0
  29. quantum_exercises/_course/exercises/04_measurement/template.py +25 -0
  30. quantum_exercises/_course/exercises/05_reading_counts/README.md +64 -0
  31. quantum_exercises/_course/exercises/05_reading_counts/check.py +74 -0
  32. quantum_exercises/_course/exercises/05_reading_counts/exercise.py +28 -0
  33. quantum_exercises/_course/exercises/05_reading_counts/hints.md +38 -0
  34. quantum_exercises/_course/exercises/05_reading_counts/meta.toml +4 -0
  35. quantum_exercises/_course/exercises/05_reading_counts/solution.py +9 -0
  36. quantum_exercises/_course/exercises/05_reading_counts/template.py +28 -0
  37. quantum_exercises/_course/exercises/06_born_rule/README.md +68 -0
  38. quantum_exercises/_course/exercises/06_born_rule/check.py +93 -0
  39. quantum_exercises/_course/exercises/06_born_rule/exercise.py +29 -0
  40. quantum_exercises/_course/exercises/06_born_rule/hints.md +33 -0
  41. quantum_exercises/_course/exercises/06_born_rule/meta.toml +4 -0
  42. quantum_exercises/_course/exercises/06_born_rule/solution.py +17 -0
  43. quantum_exercises/_course/exercises/06_born_rule/template.py +29 -0
  44. quantum_exercises/_course/exercises/07_statevector/README.md +51 -0
  45. quantum_exercises/_course/exercises/07_statevector/check.py +73 -0
  46. quantum_exercises/_course/exercises/07_statevector/exercise.py +15 -0
  47. quantum_exercises/_course/exercises/07_statevector/hints.md +45 -0
  48. quantum_exercises/_course/exercises/07_statevector/meta.toml +4 -0
  49. quantum_exercises/_course/exercises/07_statevector/solution.py +13 -0
  50. quantum_exercises/_course/exercises/07_statevector/template.py +15 -0
  51. quantum_exercises/_course/exercises/08_gates_as_matrices/README.md +51 -0
  52. quantum_exercises/_course/exercises/08_gates_as_matrices/check.py +58 -0
  53. quantum_exercises/_course/exercises/08_gates_as_matrices/exercise.py +15 -0
  54. quantum_exercises/_course/exercises/08_gates_as_matrices/hints.md +43 -0
  55. quantum_exercises/_course/exercises/08_gates_as_matrices/meta.toml +4 -0
  56. quantum_exercises/_course/exercises/08_gates_as_matrices/solution.py +9 -0
  57. quantum_exercises/_course/exercises/08_gates_as_matrices/template.py +15 -0
  58. quantum_exercises/_course/exercises/09_interference/README.md +69 -0
  59. quantum_exercises/_course/exercises/09_interference/check.py +157 -0
  60. quantum_exercises/_course/exercises/09_interference/exercise.py +24 -0
  61. quantum_exercises/_course/exercises/09_interference/hints.md +43 -0
  62. quantum_exercises/_course/exercises/09_interference/meta.toml +4 -0
  63. quantum_exercises/_course/exercises/09_interference/solution.py +17 -0
  64. quantum_exercises/_course/exercises/09_interference/template.py +24 -0
  65. quantum_exercises/_course/exercises/10_deutsch/README.md +91 -0
  66. quantum_exercises/_course/exercises/10_deutsch/check.py +156 -0
  67. quantum_exercises/_course/exercises/10_deutsch/exercise.py +62 -0
  68. quantum_exercises/_course/exercises/10_deutsch/hints.md +55 -0
  69. quantum_exercises/_course/exercises/10_deutsch/meta.toml +4 -0
  70. quantum_exercises/_course/exercises/10_deutsch/solution.py +43 -0
  71. quantum_exercises/_course/exercises/10_deutsch/template.py +62 -0
  72. quantum_exercises/_course/exercises/11_bell_entanglement/README.md +62 -0
  73. quantum_exercises/_course/exercises/11_bell_entanglement/check.py +121 -0
  74. quantum_exercises/_course/exercises/11_bell_entanglement/exercise.py +23 -0
  75. quantum_exercises/_course/exercises/11_bell_entanglement/hints.md +45 -0
  76. quantum_exercises/_course/exercises/11_bell_entanglement/meta.toml +4 -0
  77. quantum_exercises/_course/exercises/11_bell_entanglement/solution.py +13 -0
  78. quantum_exercises/_course/exercises/11_bell_entanglement/template.py +23 -0
  79. quantum_exercises/_course/exercises/12_exponential_wall/README.md +114 -0
  80. quantum_exercises/_course/exercises/12_exponential_wall/check.py +276 -0
  81. quantum_exercises/_course/exercises/12_exponential_wall/exercise.py +47 -0
  82. quantum_exercises/_course/exercises/12_exponential_wall/hints.md +52 -0
  83. quantum_exercises/_course/exercises/12_exponential_wall/meta.toml +4 -0
  84. quantum_exercises/_course/exercises/12_exponential_wall/solution.py +22 -0
  85. quantum_exercises/_course/exercises/12_exponential_wall/template.py +47 -0
  86. quantum_exercises/_course/exercises/13_migration/README.md +62 -0
  87. quantum_exercises/_course/exercises/13_migration/check.py +102 -0
  88. quantum_exercises/_course/exercises/13_migration/exercise.py +30 -0
  89. quantum_exercises/_course/exercises/13_migration/hints.md +54 -0
  90. quantum_exercises/_course/exercises/13_migration/meta.toml +4 -0
  91. quantum_exercises/_course/exercises/13_migration/solution.py +18 -0
  92. quantum_exercises/_course/exercises/13_migration/template.py +30 -0
  93. quantum_exercises/_course/exercises/14_real_hardware/README.md +77 -0
  94. quantum_exercises/_course/exercises/14_real_hardware/check.py +195 -0
  95. quantum_exercises/_course/exercises/14_real_hardware/exercise.py +31 -0
  96. quantum_exercises/_course/exercises/14_real_hardware/hints.md +43 -0
  97. quantum_exercises/_course/exercises/14_real_hardware/meta.toml +5 -0
  98. quantum_exercises/_course/exercises/14_real_hardware/solution.py +17 -0
  99. quantum_exercises/_course/exercises/14_real_hardware/template.py +31 -0
  100. quantum_exercises/_course/exercises/15_honest_reading/README.md +71 -0
  101. quantum_exercises/_course/exercises/15_honest_reading/check.py +134 -0
  102. quantum_exercises/_course/exercises/15_honest_reading/exercise.py +43 -0
  103. quantum_exercises/_course/exercises/15_honest_reading/hints.md +45 -0
  104. quantum_exercises/_course/exercises/15_honest_reading/meta.toml +4 -0
  105. quantum_exercises/_course/exercises/15_honest_reading/solution.py +20 -0
  106. quantum_exercises/_course/exercises/15_honest_reading/template.py +43 -0
  107. quantum_exercises/_course/exercises/16_readout_repair/README.md +113 -0
  108. quantum_exercises/_course/exercises/16_readout_repair/check.py +304 -0
  109. quantum_exercises/_course/exercises/16_readout_repair/exercise.py +55 -0
  110. quantum_exercises/_course/exercises/16_readout_repair/hints.md +54 -0
  111. quantum_exercises/_course/exercises/16_readout_repair/meta.toml +4 -0
  112. quantum_exercises/_course/exercises/16_readout_repair/solution.py +39 -0
  113. quantum_exercises/_course/exercises/16_readout_repair/template.py +55 -0
  114. quantum_exercises/_course/exercises/17_estimator/README.md +79 -0
  115. quantum_exercises/_course/exercises/17_estimator/check.py +162 -0
  116. quantum_exercises/_course/exercises/17_estimator/exercise.py +31 -0
  117. quantum_exercises/_course/exercises/17_estimator/hints.md +47 -0
  118. quantum_exercises/_course/exercises/17_estimator/meta.toml +4 -0
  119. quantum_exercises/_course/exercises/17_estimator/solution.py +17 -0
  120. quantum_exercises/_course/exercises/17_estimator/template.py +31 -0
  121. quantum_exercises/_course/exercises/18_measurement_bases/README.md +74 -0
  122. quantum_exercises/_course/exercises/18_measurement_bases/check.py +180 -0
  123. quantum_exercises/_course/exercises/18_measurement_bases/exercise.py +33 -0
  124. quantum_exercises/_course/exercises/18_measurement_bases/hints.md +55 -0
  125. quantum_exercises/_course/exercises/18_measurement_bases/meta.toml +4 -0
  126. quantum_exercises/_course/exercises/18_measurement_bases/solution.py +19 -0
  127. quantum_exercises/_course/exercises/18_measurement_bases/template.py +33 -0
  128. quantum_exercises/_course/exercises/19_bloch_sphere/README.md +106 -0
  129. quantum_exercises/_course/exercises/19_bloch_sphere/check.py +446 -0
  130. quantum_exercises/_course/exercises/19_bloch_sphere/exercise.py +53 -0
  131. quantum_exercises/_course/exercises/19_bloch_sphere/hints.md +63 -0
  132. quantum_exercises/_course/exercises/19_bloch_sphere/meta.toml +4 -0
  133. quantum_exercises/_course/exercises/19_bloch_sphere/solution.py +36 -0
  134. quantum_exercises/_course/exercises/19_bloch_sphere/template.py +53 -0
  135. quantum_exercises/_course/exercises/20_chsh/README.md +124 -0
  136. quantum_exercises/_course/exercises/20_chsh/check.py +216 -0
  137. quantum_exercises/_course/exercises/20_chsh/exercise.py +48 -0
  138. quantum_exercises/_course/exercises/20_chsh/hints.md +50 -0
  139. quantum_exercises/_course/exercises/20_chsh/meta.toml +4 -0
  140. quantum_exercises/_course/exercises/20_chsh/solution.py +33 -0
  141. quantum_exercises/_course/exercises/20_chsh/template.py +48 -0
  142. quantum_exercises/_course/notebooks/lab-1-qiskit-patterns.ipynb +344 -0
  143. quantum_exercises/_course/notebooks/lab-2-noise.ipynb +327 -0
  144. quantum_exercises/_course/notebooks/lab-3-dynamic-circuits.ipynb +350 -0
  145. quantum_exercises/_course/notebooks/playground.ipynb +235 -0
  146. quantum_exercises/backends.py +270 -0
  147. quantum_exercises/checks.py +451 -0
  148. quantum_exercises/cli.py +831 -0
  149. quantum_exercises/doctor.py +342 -0
  150. quantum_exercises/errors.py +395 -0
  151. quantum_exercises/registry.py +303 -0
  152. quantum_exercises/runner.py +341 -0
  153. quantum_exercises/state.py +310 -0
  154. quantum_exercises/theme.py +189 -0
  155. quantum_exercises/ui.py +497 -0
  156. quantum_exercises/watch.py +115 -0
  157. quantum_exercises/worker.py +365 -0
  158. quantum_exercises-0.8.0.dist-info/METADATA +542 -0
  159. quantum_exercises-0.8.0.dist-info/RECORD +162 -0
  160. quantum_exercises-0.8.0.dist-info/WHEEL +4 -0
  161. quantum_exercises-0.8.0.dist-info/entry_points.txt +2 -0
  162. quantum_exercises-0.8.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,44 @@
1
+ """qx: a command line course in quantum computing with Qiskit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import sys
8
+ from functools import lru_cache
9
+
10
+ __version__ = "0.8.0"
11
+
12
+
13
+ @lru_cache(maxsize=1)
14
+ def invocation() -> str:
15
+ """How to spell the command in guidance the tool prints.
16
+
17
+ `qx` once it is installed globally, `uv run qx` otherwise. The quickstart
18
+ installs it, so every written file says plainly `qx`; this exists for the
19
+ reader who skipped that step, so the guidance still matches what they can
20
+ type. Printing one form unconditionally would contradict one of them.
21
+
22
+ Lives here rather than in ui so that registry and runner can use it too,
23
+ since ui imports both of them.
24
+ """
25
+ # A qx inside this interpreter's own environment is the one `uv run` or an
26
+ # activated venv puts on PATH for the moment. It is not there in the reader's
27
+ # next shell, so answering "qx" on the strength of it would be exactly the
28
+ # mistake this helper exists to prevent.
29
+ #
30
+ # So look past it: search every PATH entry and keep the first qx that is not
31
+ # inside this environment. Finding one means a global install really is there
32
+ # and `qx` is what the reader should type, whether or not a venv happens to be
33
+ # active right now. Compared without resolving symlinks on purpose, because
34
+ # `uv tool install` leaves a link in ~/.local/bin pointing back into the
35
+ # tool's own environment, and that link is a real, permanent entry on PATH.
36
+ prefix = os.path.normcase(os.path.abspath(sys.prefix)) + os.sep
37
+ for directory in os.get_exec_path():
38
+ candidate = shutil.which("qx", path=directory)
39
+ if candidate and not os.path.normcase(os.path.abspath(candidate)).startswith(prefix):
40
+ return "qx"
41
+ return "uv run qx"
42
+
43
+
44
+ __all__ = ["__version__", "invocation"]
@@ -0,0 +1,34 @@
1
+ # 01 - Your environment works
2
+
3
+ Before any quantum mechanics, one boring question has to be settled: does code you
4
+ write in this repository actually run, against the Qiskit that is actually installed?
5
+
6
+ Plenty of quantum tutorials fail at this step and never say so. You get an error
7
+ about `execute` or `Aer`, you assume you misunderstood the physics, and you stop.
8
+ You did not misunderstand anything. You were reading instructions for a version of
9
+ the library that no longer exists.
10
+
11
+ So the first exercise checks the ground you are standing on.
12
+
13
+ ## Your task
14
+
15
+ Open `exercise.py` and do two things:
16
+
17
+ 1. Import the `qiskit` package.
18
+ 2. Set `qiskit_version` to the version string Qiskit reports about itself.
19
+
20
+ The runner compares your answer against the version genuinely installed in this
21
+ project's virtual environment. Copying a number from a blog post will not pass.
22
+
23
+ ## Run it
24
+
25
+ ```bash
26
+ qx run 1
27
+ ```
28
+
29
+ ## What you should take away
30
+
31
+ A Python package usually exposes its own version as `package.__version__`. That
32
+ attribute is the single most useful thing to print when something behaves
33
+ differently from a tutorial, because almost every "this doesn't work" in the
34
+ Qiskit world is a version mismatch.
@@ -0,0 +1,63 @@
1
+ """Verification for exercise 01."""
2
+
3
+ from importlib.metadata import version
4
+ from pathlib import Path
5
+
6
+ import qiskit
7
+
8
+ from quantum_exercises.checks import CheckFailed, require, text_artifact
9
+
10
+
11
+ def check(mod):
12
+ reported = require(mod, "qiskit_version", str)
13
+ actual = qiskit.__version__
14
+
15
+ _require_asking_the_package(mod)
16
+
17
+ if reported != actual:
18
+ raise CheckFailed(
19
+ f"You reported Qiskit {reported!r}, but this environment has {actual!r}.",
20
+ detail=(
21
+ "Read the value from the package rather than typing it in, so the answer "
22
+ "stays true after the next `uv sync`."
23
+ ),
24
+ )
25
+
26
+ return text_artifact(
27
+ f"Qiskit SDK {actual}\n"
28
+ f"qiskit-aer {_safe_version('qiskit-aer')}\n"
29
+ f"IBM Runtime {_safe_version('qiskit-ibm-runtime')}",
30
+ caption="Your quantum stack",
31
+ )
32
+
33
+
34
+ def _require_asking_the_package(mod) -> None:
35
+ """Typing the right number in also matches, and stops being right next sync.
36
+
37
+ The exercise is about asking the package, so read the file and insist on it.
38
+ """
39
+ source_file = getattr(mod, "__file__", None)
40
+ if not source_file:
41
+ return
42
+ try:
43
+ source = Path(source_file).read_text(encoding="utf-8")
44
+ except OSError:
45
+ return
46
+
47
+ if "__version__" in source or "importlib" in source:
48
+ return
49
+
50
+ raise CheckFailed(
51
+ "The version is written in by hand rather than read from the package.",
52
+ detail=(
53
+ "A literal is right until the next `uv sync` and then quietly wrong, which is the "
54
+ "exact failure this exercise is about. Ask qiskit what version it is."
55
+ ),
56
+ )
57
+
58
+
59
+ def _safe_version(package: str) -> str:
60
+ try:
61
+ return version(package)
62
+ except Exception: # noqa: BLE001 - a missing optional package is not a failure here
63
+ return "not installed"
@@ -0,0 +1,11 @@
1
+ """Exercise 01 - your environment works.
2
+
3
+ Fill in the TODOs, then run: qx run 1
4
+ """
5
+
6
+ # TODO: import the qiskit package.
7
+
8
+ # TODO: replace None with the version string that qiskit reports about itself.
9
+ # A package normally exposes this as a module-level attribute whose name
10
+ # starts and ends with two underscores.
11
+ qiskit_version = None
@@ -0,0 +1,30 @@
1
+ ## Hint 1
2
+
3
+ Two lines are enough. The first is an `import`. The second assigns to
4
+ `qiskit_version`.
5
+
6
+ Do not type the version number by hand, even if you know it. Ask the package.
7
+
8
+ ## Hint 2
9
+
10
+ Almost every Python package stores its own version in an attribute named with two
11
+ leading and two trailing underscores. You have seen the pattern before in
12
+ `__init__` and `__main__`.
13
+
14
+ Try this in a terminal to see it:
15
+
16
+ ```bash
17
+ uv run python -c "import qiskit; print(dir(qiskit))"
18
+ ```
19
+
20
+ Look through the names that start with `__`.
21
+
22
+ ## Hint 3
23
+
24
+ The attribute is `__version__`:
25
+
26
+ ```python
27
+ import qiskit
28
+
29
+ qiskit_version = qiskit.__version__
30
+ ```
@@ -0,0 +1,4 @@
1
+ title = "Your environment works"
2
+ act = "Act I - Reaching a first result"
3
+ summary = "Prove the toolchain is real: import Qiskit and read the version it reports about itself."
4
+ hardware = false
@@ -0,0 +1,5 @@
1
+ """Exercise 01 - reference solution."""
2
+
3
+ import qiskit
4
+
5
+ qiskit_version = qiskit.__version__
@@ -0,0 +1,11 @@
1
+ """Exercise 01 - your environment works.
2
+
3
+ Fill in the TODOs, then run: qx run 1
4
+ """
5
+
6
+ # TODO: import the qiskit package.
7
+
8
+ # TODO: replace None with the version string that qiskit reports about itself.
9
+ # A package normally exposes this as a module-level attribute whose name
10
+ # starts and ends with two underscores.
11
+ qiskit_version = None
@@ -0,0 +1,41 @@
1
+ # 02 - Counts is just a dictionary
2
+
3
+ Every measurement result you will see in this course arrives in the same shape:
4
+
5
+ ```python
6
+ {"00": 508, "11": 516}
7
+ ```
8
+
9
+ Keys are the outcomes, as strings of bits. Values are how many of your shots
10
+ produced that outcome. That is the whole data structure. No quantum magic.
11
+
12
+ This exercise exists because the single most common stumbling block in the first
13
+ week is not superposition, it is reaching for `counts[0]` and getting a
14
+ `KeyError`. `"00"` is a string key, not a position. A dict is not a list.
15
+
16
+ ## Your task
17
+
18
+ Implement three functions in `exercise.py`. You will use all three repeatedly for
19
+ the rest of the course.
20
+
21
+ | Function | Returns |
22
+ |---|---|
23
+ | `total_shots(counts)` | how many shots produced this result |
24
+ | `most_common(counts)` | the outcome that occurred most often |
25
+ | `outcome_probabilities(counts)` | `{outcome: estimated probability}` |
26
+
27
+ The runner calls your functions with several different dictionaries, so hardcoding
28
+ one answer will not pass.
29
+
30
+ ## Run it
31
+
32
+ ```bash
33
+ qx run 2
34
+ ```
35
+
36
+ ## What you should take away
37
+
38
+ `outcome_probabilities` is doing real physics, even though it is three lines of
39
+ Python. Dividing a count by the number of shots is how you estimate a probability
40
+ from a finite sample, and it is exactly why your numbers never come out at a clean
41
+ 0.5. You are sampling, not reading a value off the state.
@@ -0,0 +1,91 @@
1
+ """Verification for exercise 02."""
2
+
3
+ import math
4
+
5
+ from quantum_exercises.checks import CheckFailed, require, text_artifact
6
+
7
+ # Several inputs, so a hardcoded answer to one of them fails on the others.
8
+ # No ties in any case, so most_common has a single correct answer.
9
+ CASES = [
10
+ {"00": 500, "11": 524},
11
+ {"0": 750, "1": 250},
12
+ {"000": 10, "001": 30, "111": 60},
13
+ {"1010": 4096},
14
+ ]
15
+
16
+
17
+ def check(mod):
18
+ total_shots = _callable(mod, "total_shots")
19
+ most_common = _callable(mod, "most_common")
20
+ outcome_probabilities = _callable(mod, "outcome_probabilities")
21
+
22
+ if total_shots({}) != 0:
23
+ raise CheckFailed(
24
+ "total_shots({}) should be 0.",
25
+ detail="Summing an empty collection gives 0. Python's sum() already does this.",
26
+ )
27
+
28
+ for counts in CASES:
29
+ expected_total = sum(counts.values())
30
+ actual_total = total_shots(counts)
31
+ if actual_total != expected_total:
32
+ raise CheckFailed(
33
+ f"total_shots({counts}) returned {actual_total!r}, expected {expected_total}.",
34
+ detail="Add up the values, not the keys. counts.values() gives you the numbers.",
35
+ )
36
+
37
+ expected_common = max(counts, key=lambda outcome: counts[outcome])
38
+ actual_common = most_common(counts)
39
+ if actual_common != expected_common:
40
+ raise CheckFailed(
41
+ f"most_common({counts}) returned {actual_common!r}, expected {expected_common!r}.",
42
+ detail=(
43
+ "Return the outcome itself, the key, not how many times it happened. "
44
+ f"Here {expected_common!r} occurred {counts[expected_common]} times."
45
+ ),
46
+ )
47
+
48
+ expected_probs = {k: v / expected_total for k, v in counts.items()}
49
+ actual_probs = outcome_probabilities(counts)
50
+ _compare_probs(counts, actual_probs, expected_probs)
51
+
52
+ lines = []
53
+ for counts in CASES[:2]:
54
+ probs = outcome_probabilities(counts)
55
+ rendered = ", ".join(f"{k}: {v:.3f}" for k, v in sorted(probs.items()))
56
+ lines.append(
57
+ f"{counts}\n -> {total_shots(counts)} shots, most common {most_common(counts)!r}"
58
+ )
59
+ lines.append(f" -> probabilities {{{rendered}}}")
60
+
61
+ return text_artifact("\n".join(lines), caption="Your functions on real data")
62
+
63
+
64
+ def _callable(mod, name):
65
+ value = require(mod, name)
66
+ if not callable(value):
67
+ raise CheckFailed(f"`{name}` should be a function, but it is a {type(value).__name__}.")
68
+ return value
69
+
70
+
71
+ def _compare_probs(counts, actual, expected):
72
+ if not isinstance(actual, dict):
73
+ raise CheckFailed(
74
+ f"outcome_probabilities({counts}) returned a {type(actual).__name__}, expected a dict.",
75
+ )
76
+ if set(actual) != set(expected):
77
+ raise CheckFailed(
78
+ f"outcome_probabilities({counts}) returned keys {sorted(actual)}, "
79
+ f"expected {sorted(expected)}.",
80
+ detail="Keep the same outcomes as the input; only the values change.",
81
+ )
82
+ for outcome, want in expected.items():
83
+ got = actual[outcome]
84
+ if not isinstance(got, (int, float)) or not math.isclose(float(got), want, abs_tol=1e-9):
85
+ raise CheckFailed(
86
+ f"outcome_probabilities({counts})[{outcome!r}] is {got!r}, expected {want}.",
87
+ detail=(
88
+ "Divide each count by the total number of shots. If you got a whole number, "
89
+ "check that you are dividing with / and not //."
90
+ ),
91
+ )
@@ -0,0 +1,40 @@
1
+ """Exercise 02 - counts is just a dictionary.
2
+
3
+ Fill in the TODOs, then run: qx run 2
4
+ """
5
+
6
+
7
+ def total_shots(counts: dict[str, int]) -> int:
8
+ """Return how many shots produced this result.
9
+
10
+ That is the sum of all the values. An empty dict must return 0.
11
+
12
+ >>> total_shots({"00": 500, "11": 524})
13
+ 1024
14
+ """
15
+ # TODO: replace this with the sum of the dictionary's values.
16
+ return 0
17
+
18
+
19
+ def most_common(counts: dict[str, int]) -> str:
20
+ """Return the outcome that occurred most often.
21
+
22
+ `counts` always has at least one entry, and there is never a tie.
23
+
24
+ >>> most_common({"00": 500, "11": 524})
25
+ '11'
26
+ """
27
+ # TODO: return the key whose value is the largest.
28
+ return ""
29
+
30
+
31
+ def outcome_probabilities(counts: dict[str, int]) -> dict[str, float]:
32
+ """Return each outcome's share of the shots.
33
+
34
+ `counts` always has at least one entry and at least one shot.
35
+
36
+ >>> outcome_probabilities({"0": 750, "1": 250})
37
+ {'0': 0.75, '1': 0.25}
38
+ """
39
+ # TODO: build a new dict mapping each outcome to count / total.
40
+ return {}
@@ -0,0 +1,40 @@
1
+ ## Hint 1
2
+
3
+ Three built-ins do almost all the work here: `sum`, `max`, and a dict
4
+ comprehension.
5
+
6
+ For `total_shots`, remember that a dict has three views: `.keys()`, `.values()`
7
+ and `.items()`. You want the one holding the numbers.
8
+
9
+ ## Hint 2
10
+
11
+ `max(counts)` alone gives the largest **key** in alphabetical order, which is not
12
+ what you want. `max` takes a `key=` argument telling it what to rank by:
13
+
14
+ ```python
15
+ max(counts, key=lambda outcome: counts[outcome])
16
+ ```
17
+
18
+ Read that as: go through the outcomes, and rank each one by the number it maps to.
19
+
20
+ For the probabilities, a dict comprehension has the shape
21
+ `{k: f(v) for k, v in counts.items()}`.
22
+
23
+ ## Hint 3
24
+
25
+ ```python
26
+ def total_shots(counts):
27
+ return sum(counts.values())
28
+
29
+
30
+ def most_common(counts):
31
+ return max(counts, key=lambda outcome: counts[outcome])
32
+
33
+
34
+ def outcome_probabilities(counts):
35
+ shots = total_shots(counts)
36
+ return {outcome: count / shots for outcome, count in counts.items()}
37
+ ```
38
+
39
+ Note `/` and not `//`. Integer division floors, so every probability short of a
40
+ whole 1 comes back as 0.
@@ -0,0 +1,4 @@
1
+ title = "Counts is just a dictionary"
2
+ act = "Act I - Reaching a first result"
3
+ summary = "Quantum results arrive as a plain Python dict. Write the three functions you will keep reusing."
4
+ hardware = false
@@ -0,0 +1,15 @@
1
+ """Exercise 02 - reference solution."""
2
+
3
+
4
+ def total_shots(counts: dict[str, int]) -> int:
5
+ return sum(counts.values())
6
+
7
+
8
+ def most_common(counts: dict[str, int]) -> str:
9
+ # max() over the keys, ranked by the value each key maps to.
10
+ return max(counts, key=lambda outcome: counts[outcome])
11
+
12
+
13
+ def outcome_probabilities(counts: dict[str, int]) -> dict[str, float]:
14
+ shots = total_shots(counts)
15
+ return {outcome: count / shots for outcome, count in counts.items()}
@@ -0,0 +1,40 @@
1
+ """Exercise 02 - counts is just a dictionary.
2
+
3
+ Fill in the TODOs, then run: qx run 2
4
+ """
5
+
6
+
7
+ def total_shots(counts: dict[str, int]) -> int:
8
+ """Return how many shots produced this result.
9
+
10
+ That is the sum of all the values. An empty dict must return 0.
11
+
12
+ >>> total_shots({"00": 500, "11": 524})
13
+ 1024
14
+ """
15
+ # TODO: replace this with the sum of the dictionary's values.
16
+ return 0
17
+
18
+
19
+ def most_common(counts: dict[str, int]) -> str:
20
+ """Return the outcome that occurred most often.
21
+
22
+ `counts` always has at least one entry, and there is never a tie.
23
+
24
+ >>> most_common({"00": 500, "11": 524})
25
+ '11'
26
+ """
27
+ # TODO: return the key whose value is the largest.
28
+ return ""
29
+
30
+
31
+ def outcome_probabilities(counts: dict[str, int]) -> dict[str, float]:
32
+ """Return each outcome's share of the shots.
33
+
34
+ `counts` always has at least one entry and at least one shot.
35
+
36
+ >>> outcome_probabilities({"0": 750, "1": 250})
37
+ {'0': 0.75, '1': 0.25}
38
+ """
39
+ # TODO: build a new dict mapping each outcome to count / total.
40
+ return {}
@@ -0,0 +1,39 @@
1
+ # 03 - Your first circuit
2
+
3
+ A quantum circuit in Qiskit is an object you build up line by line. You create it
4
+ with a number of wires, then you call methods on it to add gates.
5
+
6
+ ```python
7
+ qc = QuantumCircuit(2) # two qubits, no classical bits
8
+ qc.h(0) # Hadamard on qubit 0
9
+ qc.cx(0, 1) # controlled-NOT, control 0, target 1
10
+ ```
11
+
12
+ The Hadamard gate is where every course starts, because it is the cheapest way to
13
+ create superposition. Applied to a qubit sitting in state 0, it produces a state
14
+ that is equally likely to be measured as 0 or as 1.
15
+
16
+ ## Your task
17
+
18
+ In `exercise.py`, build a circuit with exactly **one qubit and no classical bits**,
19
+ and apply a Hadamard gate to qubit 0. Leave the result in `qc`.
20
+
21
+ Do not add a measurement. This exercise looks at the circuit as a matrix, and a
22
+ measurement is not a matrix. Exercise 04 adds the measurement.
23
+
24
+ ## Run it
25
+
26
+ ```bash
27
+ qx run 3
28
+ ```
29
+
30
+ ## What you should take away
31
+
32
+ The runner does not compare your code to the solution as text. It builds the
33
+ matrix your circuit represents and compares it to the matrix a Hadamard gate
34
+ represents, using `Operator.equiv`.
35
+
36
+ `equiv` deliberately ignores global phase. If your circuit produces a matrix that
37
+ differs from Hadamard's by an overall factor like -1 or `i`, it still passes,
38
+ because no measurement can ever distinguish those two circuits. That is physics,
39
+ not leniency. You will meet this again in exercise 07.
@@ -0,0 +1,49 @@
1
+ """Verification for exercise 03."""
2
+
3
+ from qiskit.circuit.library import HGate
4
+ from qiskit.quantum_info import Operator
5
+
6
+ from quantum_exercises.checks import (
7
+ CheckFailed,
8
+ assert_operator_equiv,
9
+ matrix_artifact,
10
+ require_circuit,
11
+ text_artifact,
12
+ )
13
+
14
+
15
+ def check(mod):
16
+ qc = require_circuit(mod, "qc")
17
+
18
+ if qc.num_qubits != 1:
19
+ raise CheckFailed(
20
+ f"Your circuit has {qc.num_qubits} qubits, but this exercise asks for exactly 1.",
21
+ detail="QuantumCircuit(1) creates a single-qubit circuit.",
22
+ )
23
+
24
+ if qc.num_clbits != 0:
25
+ raise CheckFailed(
26
+ f"Your circuit has {qc.num_clbits} classical bits, but this exercise asks for none.",
27
+ detail=(
28
+ "A classical register is only needed to store measurement results, and this "
29
+ "exercise has no measurement. QuantumCircuit(1) with a single argument gives "
30
+ "you zero classical bits."
31
+ ),
32
+ )
33
+
34
+ if len(qc.data) == 0:
35
+ raise CheckFailed(
36
+ "Your circuit is empty: no gate was added.",
37
+ detail="Creating the circuit is only half the task. Now apply the Hadamard gate.",
38
+ )
39
+
40
+ operator = assert_operator_equiv(
41
+ qc,
42
+ Operator(HGate()),
43
+ message="Your circuit does not implement a Hadamard gate.",
44
+ )
45
+
46
+ return [
47
+ text_artifact(str(qc.draw(output="text")), caption="Your circuit"),
48
+ matrix_artifact(operator, caption="The matrix it represents"),
49
+ ]
@@ -0,0 +1,13 @@
1
+ """Exercise 03 - your first circuit.
2
+
3
+ Fill in the TODOs, then run: qx run 3
4
+ """
5
+
6
+ from qiskit import QuantumCircuit
7
+
8
+ # TODO: create a circuit with exactly one qubit and no classical bits.
9
+ # QuantumCircuit takes the number of qubits as its first argument.
10
+ qc = None
11
+
12
+ # TODO: apply a Hadamard gate to qubit 0.
13
+ # Gate methods are named after the gate and take the qubit index.
@@ -0,0 +1,28 @@
1
+ ## Hint 1
2
+
3
+ Two lines, replacing the two TODOs.
4
+
5
+ `QuantumCircuit(1)` and `QuantumCircuit(1, 1)` are both valid Python, but they
6
+ build different things. The second argument is the number of classical bits, and
7
+ this exercise wants none of those.
8
+
9
+ ## Hint 2
10
+
11
+ Gate methods on a circuit are short and lowercase, named after the gate itself:
12
+
13
+ - `qc.h(0)` - Hadamard
14
+ - `qc.x(0)` - NOT
15
+ - `qc.z(0)` - phase flip
16
+ - `qc.cx(0, 1)` - controlled-NOT
17
+
18
+ They mutate the circuit in place, so you call `qc.h(0)` on its own line. Writing
19
+ `qc = qc.h(0)` would throw away your circuit.
20
+
21
+ ## Hint 3
22
+
23
+ ```python
24
+ from qiskit import QuantumCircuit
25
+
26
+ qc = QuantumCircuit(1)
27
+ qc.h(0)
28
+ ```
@@ -0,0 +1,4 @@
1
+ title = "Your first circuit"
2
+ act = "Act I - Reaching a first result"
3
+ summary = "Build a one-qubit circuit with a Hadamard gate and see the matrix it represents."
4
+ hardware = false
@@ -0,0 +1,6 @@
1
+ """Exercise 03 - reference solution."""
2
+
3
+ from qiskit import QuantumCircuit
4
+
5
+ qc = QuantumCircuit(1)
6
+ qc.h(0)
@@ -0,0 +1,13 @@
1
+ """Exercise 03 - your first circuit.
2
+
3
+ Fill in the TODOs, then run: qx run 3
4
+ """
5
+
6
+ from qiskit import QuantumCircuit
7
+
8
+ # TODO: create a circuit with exactly one qubit and no classical bits.
9
+ # QuantumCircuit takes the number of qubits as its first argument.
10
+ qc = None
11
+
12
+ # TODO: apply a Hadamard gate to qubit 0.
13
+ # Gate methods are named after the gate and take the qubit index.