{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "ndHX0W11xPkS"
   },
   "source": [
    "# Lab 4: Simulating a single qubit system\n",
    "In this assignment, you’ll build a small (but real) quantum state simulator in Python while practicing core software engineering skills. You’ll implement a reusable `QubitSystem` interface (using an abstract base class) that supports initializing and inspecting quantum states and applying a handful of standard quantum gates, then create a concrete `SingleQubitSystem` implementation that inherits from it. Along the way, you’ll strengthen your understanding of how quantum states can be represented (bra-ket vs. state vectors), how **object-oriented** design helps you structure and extend code cleanly, and how test-driven development (TDD) can guide you to implement correct behavior incrementally with confidence.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "gqB0SZ4AxWf_"
   },
   "source": [
    "# Object-oriented programming (OOP)\n",
    "\n",
    "Object-oriented programming is a way to organize code around “objects” that bundle **data** (attributes) with **behavior** (methods). A core OOP idea is to define a general blueprint for a family of related things, then create more specific versions by extending that blueprint. This helps you reuse code, keep responsibilities clear, and make it easier to add new features without rewriting existing logic.\n",
    "\n",
    "### Base classes and shared behavior\n",
    "\n",
    "A common pattern is to create a **base class** (sometimes called an interface) that defines what all related objects should be able to do. In the example below, `Polygon` represents “any shape with sides.” Because every polygon has a number of sides, we can implement `print_number_of_sides()` once in `Polygon` and automatically reuse it in every child class.\n",
    "\n",
    "### Abstract methods and specialization\n",
    "\n",
    "Some behaviors depend on the specific type of polygon. For instance, the formula for area is different for triangles and rectangles, so `calculate_area()` is declared as an abstract method in `Polygon`. That means `Polygon` is promising “every polygon can calculate an area,” but it requires each subclass to provide the actual formula.\n",
    "\n",
    "### Inheritance and `super()`\n",
    "\n",
    "When you write a child class, you often still want to run some of the “standard setup” code that the parent class already provides. Python’s `super()` lets you call that parent-class code from inside your child class, instead of copying it.\n",
    "\n",
    "For example, in `Triangle`, we call `super().__init__(num_sides=3)` to run the `Polygon` constructor so the triangle is initialized with the correct number of sides. Then we add triangle-specific fields like `base` and `height`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "IFGAuDCaxL7B"
   },
   "outputs": [],
   "source": [
    "from abc import ABC, abstractmethod\n",
    "\n",
    "class Polygon(ABC):\n",
    "    def __init__(self, num_sides: int):\n",
    "        self.num_sides = num_sides\n",
    "\n",
    "    def print_number_of_sides(self) -> str:\n",
    "        return f\"This polygon has {self.num_sides-1} sides.\"\n",
    "\n",
    "    @abstractmethod\n",
    "    def calculate_area(self) -> float:\n",
    "        \"\"\"Return the area of this polygon.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "\n",
    "class Triangle(Polygon):\n",
    "    def __init__(self, base: float, height: float):\n",
    "        super().__init__(num_sides=3)\n",
    "        self.base = base\n",
    "        self.height = height\n",
    "\n",
    "    def calculate_area(self) -> float:\n",
    "        return 0.5 * self.base * self.height\n",
    "\n",
    "\n",
    "class Rectangle(Polygon):\n",
    "    def __init__(self, width: float, height: float):\n",
    "        super().__init__(num_sides=4)\n",
    "        self.width = width\n",
    "        self.height = height\n",
    "\n",
    "    def calculate_area(self) -> float:\n",
    "        return self.width * self.height\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "Co6ZwWewyE8r"
   },
   "outputs": [],
   "source": [
    "# We can now instantiate (create an instance of) a triangle:\n",
    "t = Triangle(10, 4)\n",
    "\n",
    "# Compute its area\n",
    "print(t.calculate_area())\n",
    "\n",
    "# We can also instantiate a rectangle:\n",
    "r = Rectangle(10, 4)\n",
    "\n",
    "# It has a different area!\n",
    "print(r.calculate_area())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "6ettFXotymjN"
   },
   "source": [
    "In this assignment, you’ll use the same design approach: define a general (abstract) `QubitSystem` with shared behaviors where possible, then implement a specific `SingleQubitSystem` that inherits from it and fills in the details (or restricts operations that don’t make sense).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "aSogT-CTy9rW"
   },
   "source": [
    "# Test Driven Development\n",
    "\n",
    "Test-driven development (TDD) means you write a small test first, watch it fail (because the feature isn’t implemented yet), then write the minimum code to make it pass, and finally clean up your design without changing behavior. This workflow helps you lock in expected behavior early and catch regressions as you refactor your classes.\n",
    "\n",
    "The general approach for a test is:\n",
    "- **Arrange**: create the instances of the object you are testing\n",
    "- **Act**: perform actions or operations on the instance of the object\n",
    "- **Assert**: validate the result of the actions being tested\n",
    "\n",
    "### What we’re testing\n",
    "\n",
    "For the `Triangle` subclass, we’ll write two tests:\n",
    "\n",
    "- `test_calculate_area`: verifies `calculate_area()` returns the correct numeric result for known inputs.\n",
    "- `test_print_number_of_sides`: verifies `print_number_of_sides()` prints the expected text (since it’s implemented in the parent `Polygon` class but should work when called on a `Triangle`).\n",
    "\n",
    "Each test will return `True` if behavior is correct and `False` otherwise, so you can run them without any testing framework.\n",
    "\n",
    "### Test 1: `test_calculate_area`\n",
    "\n",
    "This test constructs a triangle with a known base and height, then checks whether the computed area matches what you expect.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "9QYUUtQZy-n8"
   },
   "outputs": [],
   "source": [
    "def test_calculate_area() -> bool:\n",
    "    # Arrange\n",
    "    t = Triangle(base=10, height=4)\n",
    "\n",
    "    # Act\n",
    "    area = t.calculate_area()\n",
    "\n",
    "    # Assert\n",
    "    expected = 20.0  # 0.5 * 10 * 4\n",
    "    return area == expected\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "DSDH69OWzsfo"
   },
   "source": [
    "Now let's write a test for `print_number_of_sides`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "eATK9Mh8zlJI"
   },
   "outputs": [],
   "source": [
    "def test_print_number_of_sides() -> bool:\n",
    "    # Arrange\n",
    "    t = Triangle(base=3, height=5)\n",
    "\n",
    "    # Act\n",
    "    printed = t.print_number_of_sides()\n",
    "\n",
    "    # Assert\n",
    "    return printed == \"This polygon has 3 sides.\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "4FNfVPEm0MDT"
   },
   "source": [
    "### Running your tests\n",
    "\n",
    "A simple runner can quickly call each test and print whether it passed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "fApwZbjx0SET"
   },
   "outputs": [],
   "source": [
    "def run_tests() -> None:\n",
    "    tests = [\n",
    "        (\"test_calculate_area\", test_calculate_area),\n",
    "        (\"test_print_number_of_sides\", test_print_number_of_sides),\n",
    "    ]\n",
    "\n",
    "    for name, fn in tests:\n",
    "        result = fn()\n",
    "        print(f\"{name}: {'PASS' if result else 'FAIL'}\")\n",
    "\n",
    "run_tests()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "0n-ZQGlXV8jy"
   },
   "source": [
    "Oh no! The test for number of sides failed! That means our code must have a bug in it.\n",
    "\n",
    "## Task 0: Ensure our tests pass\n",
    "\n",
    "Find and fix the bug in our code above, then run the test suite again to make sure it passes!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "F5fu-v6P0boi"
   },
   "source": [
    "# Assignment Specs:\n",
    "Now that you are familiar with OOP we can move on the `QubitSystem`. You will be implementing the following:\n",
    "\n",
    "\n",
    "### `QubitSystem` technical specs\n",
    "\n",
    "| Function | Behavior |\n",
    "| --- | --- |\n",
    "| `set_value(value)` | Sets the system’s quantum state. Accept a list of floats representing the state vector amplitudes in **big-endian** basis order. Implementations should validate that the input represents a valid state for the system size and raise a `ValueError` if not. |\n",
    "| `get_value_braket() -> str` | Returns a bra-ket style string representing the current state. This is primarily for readability/debugging, you won't be penalized for stylistic differences or formating. |\n",
    "| `get_value_vector() -> list[float]` | Returns the current state vector amplitudes in big-endian basis order. |\n",
    "| `apply_not(i: int) -> None` | Applies the NOT gate (Pauli-$X$) to qubit at index $i$, updating the system state. Raise an `IndexError` if $i$ is not a valid qubit index. |\n",
    "| `apply_h(i: int) -> None` | Applies the Hadamard gate ($H$) to qubit at index $i$, updating the system state. Raise an `IndexError` if $i$ is not a valid qubit index. |\n",
    "| `apply_z(i: int) -> None` | Applies the Pauli-$Z$ gate to qubit at index $i$, updating the system state. Raise an `IndexError` if $i$ is not a valid qubit index. |\n",
    "| `apply_cnot(control: int, target: int) -> None` | Applies a controlled-NOT gate with `control` as the control qubit and `target` as the target qubit, updating the system state. Raise an `IndexError` if either index is invalid, or if they are the same. |\n",
    "| `apply_swap(i: int, j: int) -> None` | Swaps qubits at indices $i$ and $j$, updating the system state. Raise an `IndexError` if either index is invalid, or if they are the same. |\n",
    "| `measure() -> str` | <p>Simulates a measurement of the state of the system and returns one of the possible values as a big-endian string of binary: e.g., if the state $\\lvert 00 \\rangle$ is being measured the result would always be `'00'`, If the state $\\frac{1}{\\sqrt{2}} \\lvert 00 \\rangle + \\frac{1}{\\sqrt{2}} \\lvert 01 \\rangle$ is measured half the time we would expect `'00'` the other half we would expect `'01'`.<ul><li>Note: If a system is in a state of superposition before `measure()`, the act of measurement should collapse the superposition.</li><li>Note 2: The output should always have the same number of bits as the system does. For `SingleQubitSystem` the outputs will be `'0'` or `'1'`.</li></ul></p> |\n",
    "\n",
    "### `SingleQubitSystem` technical specs\n",
    "\n",
    "`SingleQubitSystem`: A concrete `QubitSystem` implementation for exactly one qubit. It must support initialization, retrieval in both formats, and single-qubit gates (`apply_not`, `apply_h`, `apply_z`) with correct state updates.\n",
    "\n",
    "| Function | Behavior |\n",
    "| --- | --- |\n",
    "| `set_value(value)` | In addition to supporting a list of floats, the `SingleQubitSystem`'s `set_value` will also accept the following strings representing bra-ket states: `\"\\|0>\"`, `\"\\|1>\"`, `\"\\|+>\"` or `\"\\|->\"` |\n",
    "| `apply_cnot(control: int, target: int) -> None` | Must always raise an `IndexError` when called on a single-qubit system (since there is no valid pair of qubit indices). |\n",
    "| `apply_swap(i: int, j: int) -> None` | Must always raise an `IndexError` when called on a single-qubit system (since there is no valid pair of qubit indices). |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "W67YKYgV4PkZ"
   },
   "source": [
    "\n",
    "## Importing external libraries (don't): \n",
    "You are free to use [`numpy`](https://numpy.org/doc/stable/) to handle all matrix and mathematical operations or you may choose to implement everything yourself as helper functions. Regardless of your decision, we expect all students to know how to programmatically implement matrix multiplication (*hint: potential midterm question*). You may **not** use `qiskit` or **any** other library besides `numpy` as part of your solution.\n",
    "**Adding extra import statement _will_ crash the autograder**\n",
    "\n",
    "## Implementation Steps:\n",
    "For now *all* of `QubitSystem`'s functions are marked as abstract; it is up to you to decide which ones to implement and which ones to delegate (i.e., only implement in child classes).\n",
    "\n",
    "If you decide the functionality is broad enough that ALL qubit systems regardless of number of qubits would be able to use it you should:\n",
    "\n",
    "\n",
    "1.   Implement it in `QubitSystem`\n",
    "2.   Remove the `@abstractmethod` tag\n",
    "3.   Delete the function definition from `SingleQubitSystem` (unless you will be [overloading](https://en.wikipedia.org/wiki/Function_overloading) it. For an example of this see the `__init__` methods)\n",
    "\n",
    "If instead you think a particular set of functionality should be implemented by `SingleQubitSystem` you may leave everything as is and go straight to `SingleQubitSystem`.\n",
    "\n",
    "Next week's lab will involve implementing an `NQubitSystem` which *will* be able to handle all gates so it is worth the effort to plan out a versatile implementation **now** rather than having to deal with refractoring later.\n",
    "\n",
    "Note: if you decide to declare helper functions **make sure to include them within your class declarations**; otherwise, your submission might crash when being autograded.\n",
    "\n",
    "For your convenience, we have provided skeleton code to get you started. **Do not rename any of the provided functions.**\n",
    "\n",
    "## Task 1: Before you start implementing, scroll down and review the test suite"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "RpiSlAWO4OhI"
   },
   "outputs": [],
   "source": [
    "from abc import ABC, abstractmethod\n",
    "import random\n",
    "\n",
    "class QubitSystem(ABC):\n",
    "    \"\"\"\n",
    "    Abstract interface for a system of qubits.\n",
    "\n",
    "    Child classes should store and update the quantum state, and must raise\n",
    "    IndexError when gate indices are out of bounds.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, num_qubits: int):\n",
    "        self._num_qubits = num_qubits\n",
    "        self.state = [0]*2**num_qubits\n",
    "        self.state[0] = 1\n",
    "\n",
    "    @property\n",
    "    def num_qubits(self) -> int:\n",
    "        return self._num_qubits\n",
    "\n",
    "    def _check_index(self, i: int) -> None:\n",
    "        \"\"\"Helper for consistent index-out-of-bounds behavior.\"\"\"\n",
    "        if i < 0 or i >= self._num_qubits:\n",
    "            raise IndexError(f\"Qubit index out of bounds: {i}\")\n",
    "\n",
    "    def _check_pair(self, i: int, j: int) -> None:\n",
    "        \"\"\"Helper for two-qubit operations.\"\"\"\n",
    "        self._check_index(i)\n",
    "        self._check_index(j)\n",
    "\n",
    "    @abstractmethod\n",
    "    def set_value(self, value) -> None:\n",
    "        \"\"\"Set the current state using bra-ket string or state-vector list.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @abstractmethod\n",
    "    def get_value_braket(self) -> str:\n",
    "        \"\"\"Return a bra-ket formatted string for the current state.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @abstractmethod\n",
    "    def get_value_vector(self) -> list[float]:\n",
    "        \"\"\"Return the state vector (big-endian basis ordering).\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @abstractmethod\n",
    "    def apply_not(self, i: int) -> None:\n",
    "        \"\"\"Apply NOT (Pauli-X) to qubit i.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @abstractmethod\n",
    "    def apply_h(self, i: int) -> None:\n",
    "        \"\"\"Apply Hadamard to qubit i.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @abstractmethod\n",
    "    def apply_z(self, i: int) -> None:\n",
    "        \"\"\"Apply Pauli-Z to qubit i.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @abstractmethod\n",
    "    def apply_cnot(self, control: int, target: int) -> None:\n",
    "        \"\"\"Apply CNOT with given control and target qubits.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @abstractmethod\n",
    "    def apply_swap(self, i: int, j: int) -> None:\n",
    "        \"\"\"Swap qubits i and j.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @abstractmethod\n",
    "    def measure(self) -> str:\n",
    "        \"\"\"Simualte a measurement of the system.\"\"\"\n",
    "        raise NotImplementedError\n",
    "\n",
    "class SingleQubitSystem(QubitSystem):\n",
    "    def __init__(self):\n",
    "        super().__init__(num_qubits=1)\n",
    "\n",
    "    def set_value(self, value) -> None:\n",
    "        raise NotImplementedError\n",
    "\n",
    "    def get_value_braket(self) -> str:\n",
    "        raise NotImplementedError\n",
    "\n",
    "    def get_value_vector(self) -> list[float]:\n",
    "        raise NotImplementedError\n",
    "\n",
    "    def apply_not(self, i: int) -> None:\n",
    "        raise NotImplementedError\n",
    "\n",
    "    def apply_h(self, i: int) -> None:\n",
    "        raise NotImplementedError\n",
    "\n",
    "    def apply_z(self, i: int) -> None:\n",
    "        raise NotImplementedError\n",
    "\n",
    "    def apply_cnot(self, control: int, target: int) -> None:\n",
    "        raise NotImplementedError\n",
    "\n",
    "    def apply_swap(self, i: int, j: int) -> None:\n",
    "        raise NotImplementedError\n",
    "\n",
    "    def measure(self) -> str:\n",
    "        raise NotImplementedError"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "a7iOU8Q71xCF"
   },
   "source": [
    "# Test Suite\n",
    "We will begin by defining the bare-bones testing suite. **The autograder will expect for these tests to be present in your submission and will evaluate them for correctness; do not change their names.**\n",
    "\n",
    "As you progress through the lab be sure to revisit your tests and come up with meaningful ways to evaluate your program's functionality under known circumstances. Your tests should always exit gracefully, even when intentionally triggering errors (e.g., ensuring a `SingleQubitSystem` throws an `IndexError` when a multi-qubit gate is applied): your code should catch the errors rather than letting them propagate.\n",
    "\n",
    "You will be graded on your test-suite's coverage in addition to the correctness of your `QubitSystem` and `SingleQubitSystem` classes.\n",
    "\n",
    "## Task 2: Comparing `float`s\n",
    "Comparing floating point numbers is trickier than it might seem due to rounding errors. The following exmple shows how even mathematically identical numbers can be interpreted as different due to the way computers handle data:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "VwfFGMqdbqce"
   },
   "outputs": [],
   "source": [
    "a = 0.0\n",
    "b = 0.0\n",
    "\n",
    "# Same math, different grouping\n",
    "for _ in range(1_000_000):\n",
    "    a += 0.1\n",
    "\n",
    "b = 0.1 * 1_000_000\n",
    "\n",
    "print(a)\n",
    "print(b)\n",
    "print(a == b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "YRUX-6WZcnve"
   },
   "source": [
    "To help with this, we have provided with a `compare_lists` function. It compares two lists and, so long as all numbers are within a small margin of each other (conventionally labeled as epsilon $\\epsilon$), it will return `True`. We recommend you use it when testing your code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "kAMkCI6zdLTD"
   },
   "outputs": [],
   "source": [
    "def compare_lists(first_list, second_list, eps: float = 1e-3) -> bool:\n",
    "    import numpy as np\n",
    "    return bool(np.allclose(first_list, second_list, atol=eps, rtol=0.0))\n",
    "\n",
    "print([a]==[b])\n",
    "print(compare_lists([a], [b]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "zT1nnExycbsX"
   },
   "source": [
    "# Task 3: Implement `set_value`\n",
    "We've provided a minimal test case for `set_value`. Scroll up to the class definitions, implement the barebones functionality (storing a properly formatted list) required to pass the test, then run the cells containing your class definitions and testing suite again.\n",
    "\n",
    "The autograder will evaluate each of the test cases below for coverage so you must add multiple cycles of arrange-act-assert within each case. **Do not rename these test cases, any helper functions must be defined _within_ an existing test case**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "OrXJqqZi0mOK"
   },
   "outputs": [],
   "source": [
    "def test_set_value() -> bool:\n",
    "    # Arrange: Set up a new SingleQubit to evaluate\n",
    "    q = SingleQubitSystem()\n",
    "\n",
    "    # Act: Evaluate the code you are testing\n",
    "    q.set_value([1.0, 0.0])\n",
    "\n",
    "    # Assert: Compare the results with the expected outcome\n",
    "\n",
    "    return compare_lists(q.state, [1.0,0.0])\n",
    "\n",
    "\n",
    "def test_get_value_vector() -> bool:\n",
    "    # Arrange: Set up a new SingleQubit to evaluate\n",
    "\n",
    "    # Act: Evaluate the code you are testing\n",
    "\n",
    "    # Assert: Compare the results with the expected outcome\n",
    "\n",
    "    return False  # overwrite when ready\n",
    "\n",
    "\n",
    "def test_apply_not() -> bool:\n",
    "    # Arrange: Set up a new SingleQubit to evaluate\n",
    "\n",
    "    # Act: Evaluate the code you are testing\n",
    "\n",
    "    # Assert: Compare the results with the expected outcome\n",
    "\n",
    "    return False  # overwrite when ready\n",
    "\n",
    "\n",
    "def test_apply_h() -> bool:\n",
    "    # Arrange: Set up a new SingleQubit to evaluate\n",
    "\n",
    "    # Act: Evaluate the code you are testing\n",
    "\n",
    "    # Assert: Compare the results with the expected outcome\n",
    "\n",
    "    return False  # overwrite when ready\n",
    "\n",
    "\n",
    "def test_apply_z() -> bool:\n",
    "    # Arrange: Set up a new SingleQubit to evaluate\n",
    "\n",
    "    # Act: Evaluate the code you are testing\n",
    "\n",
    "    # Assert: Compare the results with the expected outcome\n",
    "\n",
    "    return False  # overwrite when ready\n",
    "\n",
    "\n",
    "def test_apply_cnot() -> bool:\n",
    "    # Arrange: Set up a new SingleQubit to evaluate\n",
    "\n",
    "    # Act: Evaluate the code you are testing\n",
    "\n",
    "    # Assert: Compare the results with the expected outcome\n",
    "\n",
    "    return False  # overwrite when ready\n",
    "\n",
    "\n",
    "def test_apply_swap() -> bool:\n",
    "    # Arrange: Set up a new SingleQubit to evaluate\n",
    "\n",
    "    # Act: Evaluate the code you are testing\n",
    "\n",
    "    # Assert: Compare the results with the expected outcome\n",
    "\n",
    "    return False  # overwrite when ready\n",
    "\n",
    "\n",
    "def test_measure() -> bool:\n",
    "    # Arrange: Set up a new SingleQubit to evaluate\n",
    "\n",
    "    # Act: Evaluate the code you are testing\n",
    "    # hint: How do you test non-deterministic behavior? Is once enough?\n",
    "\n",
    "    # Assert: Compare the results with the expected outcome\n",
    "\n",
    "    return False  # overwrite when ready\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "def run_tests() -> None:\n",
    "    tests = [\n",
    "        (\"test_set_value\", test_set_value),\n",
    "        (\"test_get_value_vector\", test_get_value_vector),\n",
    "        (\"test_apply_not\", test_apply_not),\n",
    "        (\"test_apply_h\", test_apply_h),\n",
    "        (\"test_apply_z\", test_apply_z),\n",
    "        (\"test_apply_cnot\", test_apply_cnot),\n",
    "        (\"test_apply_swap\", test_apply_swap),\n",
    "        (\"test_measure\", test_measure),\n",
    "    ]\n",
    "\n",
    "    for name, fn in tests:\n",
    "        try:\n",
    "            result = fn()\n",
    "        except Exception as e:\n",
    "            print(f'Exception on {name}:', e)\n",
    "            result = False\n",
    "\n",
    "        print(f\"{name}: {'PASS' if result else 'FAIL'}\")\n",
    "\n",
    "run_tests()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "OTkBFwWye0fw"
   },
   "source": [
    "## Task 4: Implement everything else!\n",
    "Remember to be thorough with your test cases! For example, even though our `set_value` test is now non-trivial, it is far from exhaustive; it does not test for:\n",
    "\n",
    "\n",
    "1.   String inputs: Per the specs SingleQubit should support the following strings representing bra-ket states: `\"|0>\"`, `\"|1>\"`, `\"|+>\"` and `\"|->\"`\n",
    "2.   States with an incorrect number of qubits\n",
    "3.   Invalid states\n",
    "\n",
    "Good luck!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "JH6l5Kns7VYO"
   },
   "source": [
    "# Submission\n",
    "Congratulations on completing the lab!\n",
    "Make sure you:\n",
    "\n",
    "\n",
    "1.   Test all of your functions by calling them at least once.\n",
    "2.   Download your lab as a **python** `.py` script (NOT an `.ipynb` file):\n",
    "      \n",
    "      ```File -> Download -> Download .py```\n",
    "\n",
    "3.   Rename the downloaded file to `Lab4Answers.py`.\n",
    "4.   Upload the `Lab4Answers.py` file to Gradescope.\n",
    "5.   Ensure the autograder runs successfully."
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
