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:
- Quantum programs are (decorated) Python functions, no separate circuit objects.
- Quantum operations are methods on the
Qubitclass.
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"]
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.
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
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.