qupython

Quantum programs that read like Python

Stop writing code that looks like assembly! quPython makes quantum programs as easy as regular Python code. To illustrate, here's "Hello, world!" in quPython (make sure to pip install qupython first):

from qupython import Qubit, quantum

@quantum
def random_bit():
    qubit = Qubit()         # Allocate new qubit
    qubit.h()               # Mutate qubit
    return qubit.measure()  # Measure qubit to bool

print(random_bit())         # Prints "True" or "False"

The @quantum decorator converts the function into a quantum function that can be executed on a quantum computer. When you run random_bit, quPython compiles your function to a quantum program, executes it, and returns the results. Read on to see why this is a big deal.

Use Python to organise your quantum programs

quPython just a wrapper for Qiskit, but it makes two different design decisions:

  1. Quantum programs are (decorated) Python functions, no separate circuit objects.
  2. Quantum operations are methods on the Qubit class.

These small changes make a big difference in how you write your programs. Qubits feel like standard Python objects, which makes it natural to organise quantum programs using classes and other Python features. The following example creates a simple logical qubit class. This shows some nice consequences of quPython's design decisions:

  • The lower level qubits and operation are handled by the class
  • Qubits and classical bits can be initialized in methods and scoped to those methods
  • Methods return classical bit objects to be used in the program or returned to the user; no need to keep track of bit indices or registers.
from qupython import Qubit, quantum
from qupython.typing import BitPromise

class LogicalQubit:
    """
    Simple logical qubit using the five-qubit code.
    See https://en.wikipedia.org/wiki/Five-qubit_error_correcting_code
    """
    def __init__(self):
        """
        Create new logical qubit and initialize to logical |0>.
        Uses initialization procedure from https://quantumcomputing.stackexchange.com/a/14449
        """
        self.qubits = [Qubit() for _ in range(5)]
        self.qubits[4].z()
        for q in self.qubits[:4]:
            q.h()
            with q.as_control():
                self.qubits[4].x()
        for a, b in [(0,4),(0,1),(2,3),(1,2),(3,4)]:
            with self.qubits[b].as_control():
                self.qubits[a].z()

    def measure(self) -> BitPromise:
        """
        Measure logical qubit to single classical bit
        """
        # Note the `out` qubit is scoped to this function
        out = Qubit().h()
        for q in self.qubits:
            with out.as_control():
                q.z()
        return out.h().measure()

Here's how you'd use this class.

@quantum
def logical_qubit_demo() -> BitPromise:
    q = LogicalQubit()
    return q.measure()
>>> logical_qubit_demo()
False

See the Logical qubit example for a more complete class.

Generate Qiskit circuits

If you want, you can just use quPython to create Qiskit circuits with Pythonic syntax (rather than the assembly-like syntax of qc.cx(0, 1) in native Qiskit).

# Compile using quPython
logical_qubit_demo.compile()

# Draw compiled Qiskit circuit
logical_qubit_demo.circuit.draw()
     ┌───┐                                                       
q_0: ┤ H ├────────────■────────■───────────■─────■───────────────
     ├───┤            │        │           │     │               
q_1: ┤ H ├──■─────────┼────────┼──■──■──■──┼─────┼───────────────
     ├───┤  │         │        │  │  │  │  │     │               
q_2: ┤ H ├──┼────■────┼────────┼──┼──■──┼──■──■──┼───────────────
     ├───┤┌─┴─┐┌─┴─┐┌─┴─┐┌───┐ │  │     │     │  │               
q_3: ┤ Z ├┤ X ├┤ X ├┤ X ├┤ X ├─┼──■──■──┼─────┼──┼─────■─────────
     ├───┤└───┘└───┘└───┘└─┬─┘ │     │  │     │  │     │ ┌───┐┌─┐
q_4: ┤ H ├─────────────────┼───┼─────┼──■─────■──■──■──■─┤ H ├┤M├
     ├───┤                 │   │     │              │    └───┘└╥┘
q_5: ┤ H ├─────────────────■───■─────■──────────────■──────────╫─
     └───┘                                                     ║ 
c: 1/══════════════════════════════════════════════════════════╩═
                                                               0 

You can compile the function without executing it, optimize the circuit, execute it however you like, then use quPython to interpret the results.

from qiskit_aer.primitives import Sampler
qiskit_result = Sampler().run(logical_qubit_demo.circuit).result()
logical_qubit_demo.interpret_result(qiskit_result)  # returns `False`



API documentation

The rest of the page is the API documentation for the two main quPython imports: Qubit and quantum. You can also import BitPromise objects for type annotations from qupython.typing.

 1"""
 2.. include:: ../README.md
 3   :start-line: 2
 4
 5<br>
 6<br>
 7
 8## API documentation
 9
10The rest of the page is the API documentation for the two main quPython
11imports: `Qubit` and `quantum`. You can also import `BitPromise` objects for
12type annotations from `qupython.typing`.
13
14"""
15__docformat__ = "restructuredtext"
16from .qubit import Qubit
17from .decorator import quantum
18
19__all__ = ["Qubit", "quantum", "qubit", "typing", "function"]
class Qubit(qupython.qubit.Bit):
 50class Qubit(Bit):
 51    """
 52    This is the main quPython object you'll interact with and the only object
 53    you should instantiate directly. Qubits start in state |0〉.
 54
 55    `Qubit` objects support the following single-qubit gate operations as
 56    methods.
 57
 58    | Name  | Qiskit object                                                                           |
 59    |-------|-----------------------------------------------------------------------------------------|
 60    | `x`   | [`XGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.XGate)         |
 61    | `y`   | [`YGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.YGate)         |
 62    | `z`   | [`ZGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.ZGate)         |
 63    | `h`   | [`HGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.HGate)         |
 64    | `s`   | [`SGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.SGate)         |
 65    | `sdg` | [`SdgGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.SdgGate)     |
 66    | `t`   | [`TGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.TGate)         |
 67    | `tdg` | [`TdgGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.TdgGate)     |
 68    | `p`   | [`PhaseGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.PhaseGate) |
 69    | `rx`  | [`RXGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.RXGate)       |
 70    | `ry`  | [`RYGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.RYGate)       |
 71    | `rz`  | [`RZGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.RZGate)       |
 72    | `u`   | [`UGate`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.library.UGate)         |
 73
 74    These gate methods mutate the `Qubit` object and return it so you can stack them.
 75
 76    ```python
 77    qubit = Qubit().x().h()  # Qubit in state |->
 78    qubit.h()  # mutate to state |1>
 79    ```
 80
 81    The `p`, `rx`, `ry`, `rz`, and `u` gates require angles. See their
 82    corresponding Qiskit objects for a description of the gates and their
 83    angles.
 84
 85    ```python
 86    Qubit().rx(0.2)
 87    ```
 88
 89    For a controlled gate, use the `as_control` method and the `with`
 90    statement. This will control all gates inside the context by that qubit.
 91
 92    ```python
 93    qubit, another_qubit = Qubit(), Qubit()
 94    with qubit.as_control():
 95        another_qubit.x()
 96    ```
 97
 98    You can also control gates by passing a list of `Qubit`, `BitPromise`, and
 99    `bool` objects to the `conditions` argument. All conditions must be true to
100    for the gate to apply.
101
102    ```python
103    qubit.x(conditions=[another_qubit, measurement_result, True])
104    ```
105
106    Use the `measure` method to return a `BitPromise`. These promises can
107    control quantum gates too using the `as_control` method.
108    """
109    def __init__(self):
110        self.operations = []
111        self._create_1q_gate_methods()
112
113    def __bool__(self):
114        raise ValueError(
115            "Can't cast Qubit to bool; use `.measure()` to measure"
116            " the qubit instead."
117        )
118
119    def _separate_conditions(self, conditions):
120        qubits = [c for c in conditions if isinstance(c, Qubit)]
121        promises = [c for c in conditions if isinstance(c, BitPromise)]
122        build_time_conditions = [c for c in conditions if not isinstance(c, (Qubit, BitPromise))]
123        return qubits, promises, build_time_conditions
124
125    def _create_1q_gate_methods(self):
126        """
127        Generate methods such as self.h, self.p, etc.
128        This method runs on object initialization.
129        """
130
131        # TODO: unit test
132        # TODO: neaten up
133        def _create_method(gate):
134            def add_gate(*args, **kwargs):
135                conditions = kwargs.pop("conditions", [])
136                conditions += __active_controls__.get()
137                qubits, promises, build_time_conditions = self._separate_conditions(conditions)
138                if not all(build_time_conditions):
139                    return
140                qiskit_inst = gate(*args, **kwargs)
141                if qubits:
142                    qiskit_inst = qiskit_inst.control(len(qubits))
143                inst = _quPythonInstruction(
144                    qiskit_instruction=qiskit_inst,
145                    qubits=qubits + [self],
146                    promises=promises
147                )
148                for qubit in qubits + [self]:
149                    qubit.operations.append(inst)
150                for promise in promises:
151                    promise.operations.append(inst)
152                return self
153
154            return add_gate
155
156        for gate, name in [
157            (clib.XGate, "x"),
158            (clib.YGate, "y"),
159            (clib.ZGate, "z"),
160            (clib.HGate, "h"),
161            (clib.SGate, "s"),
162            (clib.SdgGate, "sdg"),
163            (clib.TGate, "t"),
164            (clib.TdgGate, "tdg"),
165            (clib.PhaseGate, "p"),
166            (clib.RXGate, "rx"),
167            (clib.RYGate, "ry"),
168            (clib.RZGate, "rz"),
169            (clib.UGate, "u"),
170        ]:
171            setattr(self, name, _create_method(gate))
172            attr = getattr(self, name)
173            attr.__doc__ = f"Apply '{name}' gate to qubit"
174            attr.__annotations__["return"] = Self
175
176    def measure(self, conditions=None) -> BitPromise:
177        """
178        Add measure instruction to Qubit and return BitPromise
179        """
180        conditions = conditions or []
181        conditions += __active_controls__.get()
182        qubits, promises, build_time_conditions = self._separate_conditions(conditions)
183        if qubits or promises:
184            raise ConditionMeasurementAtRuntimeError(ERR_MSG["ConditionMeasurementAtRuntimeError"])
185        if not all(build_time_conditions):
186            return
187        inst = _quPythonMeasurement(self)
188        self.operations.append(inst)
189        return inst.promises[0]

This is the main quPython object you'll interact with and the only object you should instantiate directly. Qubits start in state |0〉.

Qubit objects support the following single-qubit gate operations as methods.

Name Qiskit object
x XGate
y YGate
z ZGate
h HGate
s SGate
sdg SdgGate
t TGate
tdg TdgGate
p PhaseGate
rx RXGate
ry RYGate
rz RZGate
u UGate

These gate methods mutate the Qubit object and return it so you can stack them.

qubit = Qubit().x().h()  # Qubit in state |->
qubit.h()  # mutate to state |1>

The p, rx, ry, rz, and u gates require angles. See their corresponding Qiskit objects for a description of the gates and their angles.

Qubit().rx(0.2)

For a controlled gate, use the as_control method and the with statement. This will control all gates inside the context by that qubit.

qubit, another_qubit = Qubit(), Qubit()
with qubit.as_control():
    another_qubit.x()

You can also control gates by passing a list of Qubit, BitPromise, and bool objects to the conditions argument. All conditions must be true to for the gate to apply.

qubit.x(conditions=[another_qubit, measurement_result, True])

Use the measure method to return a BitPromise. These promises can control quantum gates too using the as_control method.

operations

History of operations on the bit

def measure(self, conditions=None) -> qupython.qubit.BitPromise:
176    def measure(self, conditions=None) -> BitPromise:
177        """
178        Add measure instruction to Qubit and return BitPromise
179        """
180        conditions = conditions or []
181        conditions += __active_controls__.get()
182        qubits, promises, build_time_conditions = self._separate_conditions(conditions)
183        if qubits or promises:
184            raise ConditionMeasurementAtRuntimeError(ERR_MSG["ConditionMeasurementAtRuntimeError"])
185        if not all(build_time_conditions):
186            return
187        inst = _quPythonMeasurement(self)
188        self.operations.append(inst)
189        return inst.promises[0]

Add measure instruction to Qubit and return BitPromise

Inherited Members
qupython.qubit.Bit
as_control
def quantum(func) -> qupython.function.quPythonFunction:
 7def quantum(func) -> quPythonFunction:
 8    """
 9    Decorator for quantum functions. Calling a `@quantum` function will compile
10    and execute a quantum circuit.
11
12    ```python
13    from qupython import Qubit, quantum
14
15    @quantum
16    def bell_example():
17        left, right Qubit(), Qubit()
18        with left.as_control():
19            right.x()
20        return left.measure(), right.measure()
21
22    my_function()  # Returns either (True, True) or (False, False)
23    ```
24
25    See qupython.function.quPythonFunction for more information.
26    """
27    return quPythonFunction(func)

Decorator for quantum functions. Calling a @quantum function will compile and execute a quantum circuit.

from qupython import Qubit, quantum

@quantum
def bell_example():
    left, right Qubit(), Qubit()
    with left.as_control():
        right.x()
    return left.measure(), right.measure()

my_function()  # Returns either (True, True) or (False, False)

See qupython.function.quPythonFunction for more information.