|
18 | 18 | `dwave-gate` lets you easily construct and simulate quantum circuits. |
19 | 19 |
|
20 | 20 | This tutorial guides you through using the `dwave-gate` library to inspect, |
21 | | -construct circuits from, and simulate quantum gates using a performant |
22 | | -state-vector simulator. |
| 21 | +construct, and simulate quantum circuits using a performant state-vector |
| 22 | +simulator. |
23 | 23 |
|
24 | | -## Circuits and Operations |
25 | | -Begin by importing the necessary modules. |
26 | | -
|
27 | | -* `Circuit` objects contain the full quantum circuit, with all the operations, |
28 | | - measurements, qubits and bits. |
29 | | -* Operations, or gates, are objects that contain information related to a |
30 | | - particular operation, including its matrix representation, potential |
31 | | - decompositions, and how to apply it to a circuit. |
| 24 | +## Circuits and operations |
| 25 | +Begin with two necessary imports: The `Circuit` class, which will contain the |
| 26 | +full quantum circuit with all the operations, measurements, qubits and bits, and |
| 27 | +the operations module. The operations, or gates, contain information related to |
| 28 | +a particular operation, including its matrix representation, potential |
| 29 | +decompositions, and how to apply it to a circuit. |
32 | 30 | """ |
33 | 31 |
|
34 | 32 | from dwave.gate.circuit import Circuit |
35 | 33 | import dwave.gate.operations as ops |
36 | 34 |
|
| 35 | +############################################################################### |
| 36 | +# The `Circuit` keeps track of the operations and the logical flow of their |
| 37 | +# excecutions. It also stores the qubits, bits and measurments. |
| 38 | +# |
| 39 | +# When initializing a circuit, the number of qubits and (optionally) the number of |
| 40 | +# bits, i.e., classical measurement results containers, need to be declared. More |
| 41 | +# qubits and bits can be added using the `Circuit.add_qubit` and `Circuit.add_bit` |
| 42 | +# methods. |
| 43 | + |
| 44 | +circuit = Circuit(num_qubits=2, num_bits=2) |
| 45 | + |
37 | 46 | ############################################################################### |
38 | 47 | # You can use the `operations` module (shortened above to `ops`) to access a |
39 | 48 | # variety of quantum gates; for example, a Pauli X operator. |
40 | 49 |
|
41 | 50 | ops.X() |
42 | 51 |
|
43 | 52 | ############################################################################### |
44 | | -# Notice above that an operation can be instantiated without declaring which qubits |
45 | | -# it should be applied to. |
| 53 | +# Notice above that an operation can be instantiated without declaring which |
| 54 | +# qubits it should be applied to. |
46 | 55 | # |
47 | | -# The matrix property can be accessed either via the gate class itself or an |
48 | | -# instance (i.e., an instantiated operation, which can contain additional |
49 | | -# parameters and qubit information). |
| 56 | +# The matrix property can be accessed either via the operation class itself or an |
| 57 | +# instance of the operation, which additionally can contain parameters and qubit |
| 58 | +# information. |
50 | 59 |
|
51 | 60 | ops.X.matrix |
52 | 61 |
|
53 | 62 | ############################################################################### |
54 | | -# If the matrix representation is dependent on parameters (e.g., the X-rotation operation) it can |
55 | | -# only be retrieved from an instance. |
56 | | - |
| 63 | +# If the matrix representation is dependent on parameters (e.g., the rotation operation `ops.RX`) |
| 64 | +# it can only be retrieved from an instance. |
57 | 65 |
|
58 | 66 | ops.RZ(4.2).matrix |
59 | 67 |
|
60 | 68 | ############################################################################### |
61 | | -# ## Circuit Context |
62 | | -# Operations are applied by calling, within the context of a circuit, either a |
63 | | -# class or an instance of a class. |
| 69 | +# ## The circuit context |
64 | 70 | # |
65 | | -# You can apply operations to a circuit in several different ways, as demonstrated |
66 | | -# below. You can pass both qubits and parameters as either single values (when |
67 | | -# supported by the gate) or sequences. Note that different types of gates accept |
68 | | -# slightly different arguments, although you can _always_ pass the qubits as |
69 | | -# sequences via the keyword argument `qubits`. |
| 71 | +# Operations are applied by calling either a class or an instance of a class |
| 72 | +# within the context of the circuit. |
| 73 | +# |
| 74 | +# ```python |
| 75 | +# with circuit.context: |
| 76 | +# # apply operations here |
| 77 | +# ``` |
70 | 78 | # |
71 | | -# Always apply any operations you instantiate within a circuit context to |
72 | | -# specific qubits in the circuit's qubit register. You can access the qubit |
73 | | -# register via the named tuple returned by the context manager as `q`, indexing |
74 | | -# into it to retrieve the corresponding qubit. |
75 | 79 |
|
76 | 80 | ############################################################################### |
77 | | -# ## Registers |
78 | | -# This example starts by creating a circuit object with two qubits in its |
79 | | -# register and applying a single X gate to the first qubit. |
| 81 | +# When activating the context, a named tuple containing reference registers to the |
| 82 | +# circuit's qubits and classical bits is returned. You can also access the qubit |
| 83 | +# registers directly via the `Circuit.qregisters` property, or the reference |
| 84 | +# registers containing all the qubits via `Circuit.qubits`. |
| 85 | +# |
| 86 | +# In the example below, the circuit contains a single qubit register with two |
| 87 | +# qubits; it could contain any number of qubit registers. You can |
| 88 | +# |
| 89 | +# * add another register with the `Circuit.add_qregister` method, where an argument `n` |
| 90 | +# is the number of qubits in the new register |
| 91 | +# * add a qubit with `Circuit.add_qubit`, optionally passing a qubit object |
| 92 | +# and/or a register to which to add it. |
80 | 93 |
|
81 | 94 | circuit = Circuit(2) |
82 | 95 |
|
83 | 96 | with circuit.context as reg: |
84 | 97 | ops.X(reg.q[0]) |
85 | 98 |
|
86 | 99 | ############################################################################### |
87 | | -# You can access the qubit register via the `Circuit.qregisters` attribute. |
88 | | -# |
89 | | -# In the current example, this attribute contains a single qubit register with |
90 | | -# two qubits; it could contain any number of qubit registers. You can |
91 | | -# |
92 | | -# * add another register with the `Circuit.add_qregister(n)` method, where `n` |
93 | | -# is the number of qubits in the new register |
94 | | -# * add a qubit with `Circuit.add_qubit()`, optionally passing a qubit object |
95 | | -# and/or a register to which to add it. |
96 | | -# |
97 | | -# The registers tuple can also be unwrapped directly into a qubit register `q` (and a |
98 | | -# classical register `c`). |
| 100 | +# This example created a circuit object with two qubits in its register, applying |
| 101 | +# a single X gate to the first qubit. Print the circuit to see general information |
| 102 | +# about it: type of circuit, number of qubits/bits, and number of operations. |
99 | 103 |
|
| 104 | +print(circuit) |
100 | 105 |
|
101 | 106 | ############################################################################### |
102 | | -# ## Example: Applying Various gates to a Circuit |
103 | | - |
| 107 | +# ## Applying gates to circuits |
| 108 | +# |
| 109 | +# You can apply operations to a circuit in several different ways, as demonstrated |
| 110 | +# in the example below. You can pass both qubits and parameters as either single |
| 111 | +# values (when supported by the gate) or sequences. Note that different types of |
| 112 | +# gates accept slightly different arguments, although you can _always_ pass the |
| 113 | +# qubits as sequences via the keyword argument `qubits`. |
| 114 | +# |
| 115 | +# :::note |
| 116 | +# Always apply any operations you instantiate within a circuit context to |
| 117 | +# specific qubits in the circuit's qubit register. You can access the qubit |
| 118 | +# register via the named tuple returned by the context manager as `q`, indexing |
| 119 | +# into it to retrieve the corresponding qubit. |
| 120 | +# ::: |
104 | 121 |
|
105 | 122 | circuit = Circuit(3) |
106 | 123 |
|
|
129 | 146 | ops.Toffoli(q) |
130 | 147 |
|
131 | 148 | ############################################################################### |
132 | | -# Print the circuit above to see general information about it: type of circuit, |
133 | | -# number of qubits/bits, and number of operations. |
| 149 | +# You can access all the operations in a circuit using the `Circuit.circuit` |
| 150 | +# property. The code below iterates over the returned list of all operations that |
| 151 | +# have been applied to the circuit. |
| 152 | + |
| 153 | +for op in circuit.circuit: |
| 154 | + print(op) |
| 155 | + |
| 156 | +############################################################################### |
| 157 | +# :::note |
| 158 | +# The CNOT alias for the controlled NOT gate is labelled CX in the circuit. You |
| 159 | +# can find all operation aliases in the source code and documentation for the |
| 160 | +# operations module. |
| 161 | +# ::: |
| 162 | + |
| 163 | +############################################################################### |
| 164 | +# ## Simulating a circuit |
| 165 | +# |
| 166 | +# `dwave-gate` comes with a performant state-vector simulator. It can be called by passing a circuit to the `simulate` method, which will update the quatum state stored in the circuit, accessible via `Circuit.state`. |
| 167 | + |
| 168 | +from dwave.gate.simulator import simulate |
| 169 | + |
| 170 | +############################################################################### |
| 171 | +# We create a circuit object with 2 qubits and 1 bit in a quantum and classical registers respectively --- the bit is required to store a single qubit measurement --- and then apply a Hadamard gate and a CNOT gate to the circuit. |
| 172 | + |
| 173 | +circuit = Circuit(2, 1) |
| 174 | + |
| 175 | +with circuit.context as (q, c): |
| 176 | + ops.Hadamard(q[0]) |
| 177 | + ops.CNOT(q[0], q[1]) |
| 178 | + |
| 179 | +############################################################################### |
| 180 | +# We can now simulate the circuit, which will update its stored quantum state. |
| 181 | + |
| 182 | +simulate(circuit) |
| 183 | + |
| 184 | +############################################################################### |
| 185 | +# Printing the state reveals the expected state-vector $\frac{1}{\sqrt{2}}\left[1, |
| 186 | +# 0, 0, 1\right]$ corresponding to the state: |
| 187 | +# |
| 188 | +# $$\vert\psi\rangle = \frac{1}{\sqrt{2}}\left(\vert00\rangle + \vert11\rangle\right)$$ |
| 189 | + |
| 190 | +circuit.state |
| 191 | + |
| 192 | +############################################################################### |
| 193 | +# ## Measurements |
| 194 | +# |
| 195 | +# Measurements work like any other operation in dwave-gate. The main difference is that the operation generates a measurement value when simulated which can be stored in the classical register by piping it into a classical bit. |
| 196 | +# |
| 197 | +# We can reuse the circuit from above by simply unlocking it and appending a `Measurement` to it. |
| 198 | + |
| 199 | +circuit.unlock() |
| 200 | +with circuit.context as (q, c): |
| 201 | + m = ops.Measurement(q[1]) | c[0] |
| 202 | + |
| 203 | +############################################################################### |
| 204 | +# :::note |
| 205 | +# We stored the measurement instance as `m`, which we can use for post-processing. It's also possible to do this with all other operations in the same way, allowing for multiple identical operation applications. |
| 206 | +# ```python |
| 207 | +# with circuit.context as q, _: |
| 208 | +# single_x_op = ops.X(q[0]) |
| 209 | +# # apply the X-gate again to the second qubit |
| 210 | +# # using the previously stored operation |
| 211 | +# single_x_op(q[1]) |
| 212 | +# ``` |
| 213 | +# This procedure can also be shortened into a single line for further convience. |
| 214 | +# ```python |
| 215 | +# ops.CNOT(q[0], q[1])(q[1], q[2])(q[2], q[3]) |
| 216 | +# ``` |
| 217 | +# ::: |
| 218 | + |
| 219 | +############################################################################### |
| 220 | +# The circuit should now contain 3 operations: a Hadamard, a CNOT and a measurment. |
134 | 221 |
|
135 | 222 | print(circuit) |
136 | 223 |
|
137 | 224 | ############################################################################### |
138 | | -# You can also access operations in a circuit using the `Circuit.circuit` |
139 | | -# attribute. The code below iterates over the returned list of all operations. |
| 225 | +# When simulating this circuit, the measurement will be applied and the measured value will be stored in the classical register. Since a measurement will affect the quantum state, the resulting state will have collapsed into the expected result dependent on the value which has been measured. |
140 | 226 |
|
| 227 | +simulate(circuit) |
141 | 228 |
|
142 | | -for op in circuit.circuit: |
143 | | - print(op) |
| 229 | +############################################################################### |
| 230 | +# If the measurement result is 0 the state should collapse into $\vert00\rangle$, and if the measurement result is 1 the state should collapse into $\vert11\rangle$. Outputting the measurement value and the state reveals that this is indeed the case. |
| 231 | + |
| 232 | +print(circuit.bits[0].value) |
| 233 | +print(circuit.state) |
144 | 234 |
|
145 | 235 | ############################################################################### |
146 | | -# Note that the CNOT alias for the controlled NOT gate is labelled CX in the |
147 | | -# circuit. You can find all operation aliases in the source code and documentation |
148 | | -# for the operations module. |
| 236 | +# ## Measurement post-access |
| 237 | +# Since we stored the measurement operation in `m`, we can use it to access the state as it was before the measurement. |
| 238 | +# |
| 239 | +# :::note |
| 240 | +# Accessing the state of the circuit along with any measurement post-sampling and state-access is only available for simulators. |
| 241 | +# ::: |
| 242 | + |
| 243 | +m.state |
149 | 244 |
|
150 | 245 | ############################################################################### |
151 | | -# ## Simulating a circuit |
| 246 | +# We can also sample that same state again using the `Measurement.sample` method, which by default only samples the state once. Here, we request 10 samples. |
| 247 | + |
| 248 | +m.sample(num_samples=10) |
| 249 | + |
| 250 | +############################################################################### |
| 251 | +# Finally, we can calculate the expected value of the measurment based on a specific number of samples. |
| 252 | + |
| 253 | +m.expval(num_samples=10000) |
| 254 | + |
| 255 | +"" |
| 256 | + |
0 commit comments