|
| 1 | +# Converting cirq to squin |
| 2 | + |
| 3 | +If you want to obtain a squin kernel from a circuit, you can use the `load_circuit` method in the `squin.cirq` submodule. |
| 4 | +What you're effectively doing is lowering a circuit to a squin IR. |
| 5 | +This IR can then be further lowered to eventually run on hardware. |
| 6 | + |
| 7 | +## Basic examples |
| 8 | + |
| 9 | +Here are some basic usage examples to help you get started. |
| 10 | + |
| 11 | +```python |
| 12 | +from bloqade import squin |
| 13 | +import cirq |
| 14 | + |
| 15 | +qubits = cirq.LineQubit.range(2) |
| 16 | +circuit = cirq.Circuit( |
| 17 | + cirq.H(qubits[0]), |
| 18 | + cirq.CX(qubits[0], qubits[1]), |
| 19 | + cirq.measure(qubits) |
| 20 | +) |
| 21 | + |
| 22 | +# let's have a look |
| 23 | +print(circuit) |
| 24 | + |
| 25 | +main_loaded = squin.cirq.load_circuit(circuit, kernel_name="main_loaded") |
| 26 | +``` |
| 27 | + |
| 28 | +The above is equivalent to writing the following kernel function yourself: |
| 29 | + |
| 30 | +```python |
| 31 | +@squin.kernel |
| 32 | +def main(): |
| 33 | + q = squin.qubit.new(2) |
| 34 | + H = squin.op.h() |
| 35 | + CX = squin.op.cx() |
| 36 | + squin.qubit.apply(H, q[0]) |
| 37 | + squin.qubit.apply(CX, q) |
| 38 | + squin.qubit.measure(q) |
| 39 | +``` |
| 40 | + |
| 41 | +You can further inspect the lowered kernel as usual, e.g. by printing the IR. |
| 42 | +Let's compare the manually written version and the loaded version: |
| 43 | + |
| 44 | +```python |
| 45 | +main.print() |
| 46 | +main_loaded.print() |
| 47 | +``` |
| 48 | + |
| 49 | +The resulting IR is equivalent, yet the loaded is a bit longer since the automated loading can make fewer assumptions about the code. |
| 50 | +Still, you can use the kernel as any other, e.g. by calling it from another kernel or running it via a simulator. |
| 51 | + |
| 52 | +## Noise |
| 53 | + |
| 54 | +Lowering a noisy circuit to squin is also supported. |
| 55 | +All common channels in cirq will be lowered to an equivalent noise statement in squin. |
| 56 | + |
| 57 | +```python |
| 58 | +from bloqade import squin |
| 59 | +import cirq |
| 60 | + |
| 61 | +qubits = cirq.LineQubit.range(2) |
| 62 | +noisy_circuit = cirq.Circuit( |
| 63 | + cirq.H(qubits[0]), |
| 64 | + cirq.CX(qubits[0], qubits[1]), |
| 65 | + cirq.depolarize(p=0.01).on_each(qubits), |
| 66 | +) |
| 67 | + |
| 68 | +# let's have a look |
| 69 | +print(noisy_circuit) |
| 70 | + |
| 71 | +noisy_kernel = squin.cirq.load_circuit(noisy_circuit) |
| 72 | +noisy_kernel.print() |
| 73 | +``` |
| 74 | + |
| 75 | +This becomes especially useful when used together with a `cirq.NoiseModel` that automatically adds noise to a circuit via `circuit.with_noise(model)`. |
| 76 | + |
| 77 | +## Composability of kernels |
| 78 | + |
| 79 | +You may also run into a situation, where you define a circuit that is used as part of a larger one, maybe even multiple times. |
| 80 | +In order to allow you to do something similar here, you can pass in and / or return the qubit register in a loaded kernel. |
| 81 | +Both these options are controlled by simple keyword arguments. |
| 82 | + |
| 83 | +### Qubits as argument to the kernel function |
| 84 | + |
| 85 | +Setting `register_as_argument=True` when loading a kernel, will result in a squin kernel function that accepts (and requires) a single argument of type `IList[Qubit]`. |
| 86 | +This means you can use a loaded circuit as part of another kernel function. |
| 87 | +Check it out: |
| 88 | + |
| 89 | +```python |
| 90 | +from bloqade import squin |
| 91 | +import cirq |
| 92 | + |
| 93 | +qubits = cirq.LineQubit.range(2) |
| 94 | +circuit = cirq.Circuit( |
| 95 | + cirq.H(qubits[0]), |
| 96 | + cirq.CX(qubits[0], qubits[1]), |
| 97 | +) |
| 98 | + |
| 99 | +sub_kernel = squin.cirq.load_circuit(circuit, register_as_argument=True, kernel_name="sub_kernel") |
| 100 | + |
| 101 | + |
| 102 | +@squin.kernel |
| 103 | +def main(): |
| 104 | + q = squin.qubit.new(4) |
| 105 | + |
| 106 | + # entangle qubits 1 and 2 |
| 107 | + sub_kernel([q[0], q[1]]) |
| 108 | + |
| 109 | + # entangle qubits 3 and 4 |
| 110 | + sub_kernel([q[2], q[3]]) |
| 111 | + |
| 112 | + |
| 113 | +main.print() |
| 114 | +``` |
| 115 | + |
| 116 | +Looking at the IR of the resulting kernel, you can see that there is are `invoke sub_kernel` statements present, which call the lowered circuit with the given arguments. |
| 117 | + |
| 118 | +### Qubits as return value from the kernel |
| 119 | + |
| 120 | +Similarly to above, you may also want to return a list of qubits from a loaded kernel. |
| 121 | +Let's adapt the above to instantiate and return a pair of entangled qubits using the same circuit: |
| 122 | + |
| 123 | +```python |
| 124 | + |
| 125 | +sub_kernel = squin.cirq.load_circuit(circuit, return_register=True, kernel_name="sub_kernel") |
| 126 | + |
| 127 | +@squin.kernel |
| 128 | +def main(): |
| 129 | + # instantiate and entangle a list of two qubits |
| 130 | + q1 = sub_kernel() |
| 131 | + |
| 132 | + # do it again, to get another set |
| 133 | + q2 = sub_kernel() |
| 134 | + |
| 135 | + # now we have 4 qubits to work with |
| 136 | + ... |
| 137 | + |
| 138 | +main.print() |
| 139 | +``` |
| 140 | + |
| 141 | + |
| 142 | +!!! note |
| 143 | + You can also mix both options by setting `register_as_argument = True` and `return_register = True` in order to obtain a kernel function that both accepts and returns a list of qubits. |
| 144 | + |
| 145 | + |
| 146 | +## Limitations |
| 147 | + |
| 148 | +There are some limitations when loading circuits. |
| 149 | +One, for example, is that custom gates are not supported as you can't generally know how to lower them to a squin statement. |
| 150 | + |
| 151 | +If you find a missing feature, please feel free to [open a GitHub issue](https://github.com/QuEraComputing/bloqade-circuit/issues). |
0 commit comments