Constructing circuits in the SDK
This section provides examples of defining a circuit, viewing available gates, extending a circuit, and viewing gates that each device supports. It also contains instructions on how to manually allocate qubits, instruct the compiler to run your circuits exactly as defined, and build noisy circuits with a noise simulator.
You can also work at the pulse level in Braket for various gates with certain QPUs. For more information, see Pulse Control on Amazon Braket.
In this section:
Gates and circuits
Quantum gates and circuits are defined in the braket.circuits
Circuit()
.
Example: Define a circuit
The example starts by defining a sample circuit of four qubits
(labelled q0
, q1
, q2
, and q3
)
consisting of standard, single-qubit Hadamard gates and two-qubit CNOT gates. You can
visualize this circuit by calling the print
function as the following
example shows.
# import the circuit module from braket.circuits import Circuit # define circuit with 4 qubits my_circuit = Circuit().h(range(4)).cnot(control=0, target=2).cnot(control=1, target=3) print(my_circuit)
T : |0| 1 | q0 : -H-C--- | q1 : -H-|-C- | | q2 : -H-X-|- | q3 : -H---X- T : |0| 1 |
Example: Define a parameterized circuit
In this example, we define a circuit with gates that depend on free parameters. We can specify the values of these parameters to create a new circuit, or, when submitting the circuit, to run as a quantum task on certain devices.
from braket.circuits import Circuit, FreeParameter #define a FreeParameter to represent the angle of a gate alpha = FreeParameter("alpha") #define a circuit with three qubits my_circuit = Circuit().h(range(3)).cnot(control=0, target=2).rx(0, alpha).rx(1, alpha) print(my_circuit)
You can create a new, non-parametrized circuit from a parametrized one by supplying
either a single float
(which is the value all free parameters will take) or
keyword arguments specifying each parameter’s value to the circuit as follows.
my_fixed_circuit = my_circuit(1.2) my_fixed_circuit = my_circuit(alpha=1.2)
Note that my_circuit
is unmodified, so you can use it to instantiate
many new circuits with fixed parameter values.
Example: Modify gates in a circuit
The following example defines a circuit with gates that use control and power
modifiers. You can use these modifications to create new gates, such as the controlled
Ry
gate.
from braket.circuits import Circuit # Create a bell circuit with a controlled x gate my_circuit = Circuit().h(0).x(control=0, target=1) # Add a multi-controlled Ry gate of angle .13 my_circuit.ry(angle=.13, target=2, control=(0, 1)) # Add a 1/5 root of X gate my_circuit.x(0, power=1/5) print(my_circuit)
Gate modifiers are supported only on the local simulator.
Example: See all available gates
The following example shows how to look at all the available gates in Amazon Braket.
from braket.circuits import Gate # print all available gates in Amazon Braket gate_set = [attr for attr in dir(Gate) if attr[0].isupper()] print(gate_set)
The output from this code lists all of the gates.
['CCNot', 'CNot', 'CPhaseShift', 'CPhaseShift00', 'CPhaseShift01', 'CPhaseShift10', 'CSwap', 'CV', 'CY', 'CZ', 'ECR', 'GPi', 'GPi2', 'H', 'I', 'ISwap', 'MS', 'PSwap', 'PhaseShift', 'PulseGate', 'Rx', 'Ry', 'Rz', 'S', 'Si', 'Swap', 'T', 'Ti', 'Unitary', 'V', 'Vi', 'X', 'XX', 'XY', 'Y', 'YY', 'Z', 'ZZ']
Any of these gates can be appended to a circuit by calling the method for that type
of circuit. For example, you’d call circ.h(0)
, to add a Hadamard gate to
the first qubit.
Note
Gates are appended in place, and the example that follows adds all of the gates listed in the previous example to the same circuit.
circ = Circuit() # toffoli gate with q0, q1 the control qubits and q2 the target. circ.ccnot(0, 1, 2) # cnot gate circ.cnot(0, 1) # controlled-phase gate that phases the |11> state, cphaseshift(phi) = diag((1,1,1,exp(1j*phi))), where phi=0.15 in the examples below circ.cphaseshift(0, 1, 0.15) # controlled-phase gate that phases the |00> state, cphaseshift00(phi) = diag([exp(1j*phi),1,1,1]) circ.cphaseshift00(0, 1, 0.15) # controlled-phase gate that phases the |01> state, cphaseshift01(phi) = diag([1,exp(1j*phi),1,1]) circ.cphaseshift01(0, 1, 0.15) # controlled-phase gate that phases the |10> state, cphaseshift10(phi) = diag([1,1,exp(1j*phi),1]) circ.cphaseshift10(0, 1, 0.15) # controlled swap gate circ.cswap(0, 1, 2) # swap gate circ.swap(0,1) # phaseshift(phi)= diag([1,exp(1j*phi)]) circ.phaseshift(0,0.15) # controlled Y gate circ.cy(0, 1) # controlled phase gate circ.cz(0, 1) # Echoed cross-resonance gate applied to q0, q1 circ = Circuit().ecr(0,1) # X rotation with angle 0.15 circ.rx(0, 0.15) # Y rotation with angle 0.15 circ.ry(0, 0.15) # Z rotation with angle 0.15 circ.rz(0, 0.15) # Hadamard gates applied to q0, q1, q2 circ.h(range(3)) # identity gates applied to q0, q1, q2 circ.i([0, 1, 2]) # iswap gate, iswap = [[1,0,0,0],[0,0,1j,0],[0,1j,0,0],[0,0,0,1]] circ.iswap(0, 1) # pswap gate, PSWAP(phi) = [[1,0,0,0],[0,0,exp(1j*phi),0],[0,exp(1j*phi),0,0],[0,0,0,1]] circ.pswap(0, 1, 0.15) # X gate applied to q1, q2 circ.x([1, 2]) # Y gate applied to q1, q2 circ.y([1, 2]) # Z gate applied to q1, q2 circ.z([1, 2]) # S gate applied to q0, q1, q2 circ.s([0, 1, 2]) # conjugate transpose of S gate applied to q0, q1 circ.si([0, 1]) # T gate applied to q0, q1 circ.t([0, 1]) # conjugate transpose of T gate applied to q0, q1 circ.ti([0, 1]) # square root of not gate applied to q0, q1, q2 circ.v([0, 1, 2]) # conjugate transpose of square root of not gate applied to q0, q1, q2 circ.vi([0, 1, 2]) # exp(-iXX theta/2) circ.xx(0, 1, 0.15) # exp(i(XX+YY) theta/4), where theta=0.15 in the examples below circ.xy(0, 1, 0.15) # exp(-iYY theta/2) circ.yy(0, 1, 0.15) # exp(-iZZ theta/2) circ.zz(0, 1, 0.15) # IonQ native gate GPi with angle 0.15 applied to q0 circ.gpi(0, 0.15) # IonQ native gate GPi2 with angle 0.15 applied to q0 circ.gpi2(0, 0.15) # IonQ native gate MS with angles 0.15, 0.15, 0.15 applied to q0, q1 circ.ms(0, 1, 0.15, 0.15, 0.15)
Apart from the pre-defined gate set, you also can apply self-defined unitary gates to
the circuit. These can be single-qubit gates (as shown in the following source code) or
multi-qubit gates applied to the qubits defined by the
targets
parameter.
import numpy as np # apply a general unitary my_unitary = np.array([[0, 1],[1, 0]]) circ.unitary(matrix=my_unitary, targets=[0])
Example: Extend existing circuits
You can extend existing circuits by adding instructions. An Instruction
is a quantum directive that describes the quantum task to perform on a quantum device.
Instruction
operators include objects of type Gate
only.
# import the Gate and Instruction modules from braket.circuits import Gate, Instruction # add instructions directly. circ = Circuit([Instruction(Gate.H(), 4), Instruction(Gate.CNot(), [4, 5])]) # or with add_instruction/add functions instr = Instruction(Gate.CNot(), [0, 1]) circ.add_instruction(instr) circ.add(instr) # specify where the circuit is appended circ.add_instruction(instr, target=[3, 4]) circ.add_instruction(instr, target_mapping={0: 3, 1: 4}) # print the instructions print(circ.instructions) # if there are multiple instructions, you can print them in a for loop for instr in circ.instructions: print(instr) # instructions can be copied new_instr = instr.copy() # appoint the instruction to target new_instr = instr.copy(target=[5]) new_instr = instr.copy(target_mapping={0: 5})
Example: View the gates that each device supports
Simulators support all gates in the Braket SDK, but QPU devices support a smaller subset. You can find the supported gates of a device in the device properties. The following shows an example with an IonQ device:
# import the device module from braket.aws import AwsDevice device = AwsDevice("arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1") # get device name device_name = device.name # show supportedQuantumOperations (supported gates for a device) device_operations = device.properties.dict()['action']['braket.ir.openqasm.program']['supportedOperations'] print('Quantum Gates supported by {}:\n {}'.format(device_name, device_operations))
Quantum Gates supported by the Aria-1 device: ['x', 'y', 'z', 'rx', 'ry', 'rz', 'h', 'cnot', 's', 'si', 't', 'ti', 'v', 'vi', 'xx', 'yy', 'zz', 'swap']
Supported gates may need to be compiled into native gates before they can run on quantum hardware. When you submit a circuit, Amazon Braket performs this compilation automatically.
Example: Programmatically retrieve the fidelity of native gates supported by a device
You can view the fidelity information on the Devices page of the Braket console. Sometimes it is helpful to access the same information programmatically. The following code shows how to extract the two qubit gate fidelity between two gates of a QPU.
# import the device module from braket.aws import AwsDevice device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2") #specify the qubits a=10 b=11 print(f"Fidelity of the ISWAP gate between qubits {a} and {b}: ", device.properties.provider.specs["2Q"][f"{a}-{b}"]["fISWAP"])
Partial measurement
Following the previous examples, we have measured all the qubits in the quantum circuit. However, it is possible to measure individual qubits or a subset of qubits.
Example: Measure a subset of qubits
In this example, we demonstrate a partial measurement by adding a measure
instruction
with the target qubits to the end of the circuit.
# Use the local state vector simulator device = LocalSimulator() # Define an example bell circuit and measure qubit 0 circuit = Circuit().h(0).cnot(0, 1).measure(0) # Run the circuit task = device.run(circuit, shots=10) # Get the results result = task.result() # Print the circuit and measured qubits print(circuit) print() print("Measured qubits: ", result.measured_qubits)
Manual qubit allocation
When you run a quantum circuit on quantum computers from Rigetti, you
can optionally use manual qubit allocation to control which
qubits are used for your algorithm. The Amazon Braket Console
Manual qubit allocation enables you to run circuits with greater accuracy and to investigate individual qubit properties. Researchers and advanced users optimize their circuit design based on the latest device calibration data and can obtain more accurate results.
The following example demonstrates how to allocate qubits explicitly.
circ = Circuit().h(0).cnot(0, 7) # Indices of actual qubits in the QPU my_task = device.run(circ, s3_location, shots=100, disable_qubit_rewiring=True)
For more information, see the Amazon Braket examples on GitHub
Verbatim compilation
When you run a quantum circuit on gate-based quantum computers you can direct the compiler to run your circuits exactly as defined without any modifications. Using verbatim compilation, you can specify either that an entire circuit be preserved precisely as specified or that only specific parts of it be preserved (supported by Rigetti only). When developing algorithms for hardware benchmarking or error mitigation protocols, you need have the option to exactly specify the gates and circuit layouts that you’re running on the hardware. Verbatim compilation gives you direct control over the compilation process by turning off certain optimization steps, thereby ensuring that your circuits run exactly as designed.
Verbatim compilation is currently supported on the Rigetti, IonQ, and IQM devices and requires the use of native gates. When using verbatim compilation, it is advisable to check the topology of the device to ensure that gates are called on connected qubits and that the circuit uses the native gates supported on the hardware. The following example shows how to programmatically access the list of native gates supported by a device.
device.properties.paradigm.nativeGateSet
For Rigetti, qubit rewiring must be turned off by
setting disableQubitRewiring=True
for use with verbatim compilation. If
disableQubitRewiring=False
is set when using verbatim boxes in a
compilation, the quantum circuit fails validation and does not run.
If verbatim compilation is enabled for a circuit and run on a QPU that does not support it, an error is generated indicating that an unsupported operation has caused the task to fail. As more quantum hardware natively support compiler functions, this feature will be expanded to include these devices. Devices that support verbatim compilation include it as a supported operation when queried with the following code.
from braket.aws import AwsDevice from braket.device_schema.device_action_properties import DeviceActionType device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2") device.properties.action[DeviceActionType.OPENQASM].supportedPragmas
There is no additional cost associated with using verbatim compilation. You continue
to be charged for quantum tasks executed on Braket QPU devices, notebook instances, and
on-demand simulators based on current rates as specified on the Amazon Braket Pricing
Note
If you are using OpenQASM to write your circuits for the
IonQ device, and you wish to map your circuit directly to the
physical qubits, you need to use the #pragma braket verbatim
as the
disableQubitRewiring
flag is completely ignored by OpenQASM.
Noise simulation
To instantiate the local noise simulator you can change the backend as follows.
device = LocalSimulator(backend="braket_dm")
You can build noisy circuits in two ways:
-
Build the noisy circuit from the bottom up.
-
Take an existing, noise-free circuit and inject noise throughout.
The following example shows the approaches using a simple circuit with depolarizing noise and a custom Kraus channel.
# Bottom up approach # apply depolarizing noise to qubit 0 with probability of 0.1 circ = Circuit().x(0).x(1).depolarizing(0, probability=0.1) # create an arbitrary 2-qubit Kraus channel E0 = scipy.stats.unitary_group.rvs(4) * np.sqrt(0.8) E1 = scipy.stats.unitary_group.rvs(4) * np.sqrt(0.2) K = [E0, E1] # apply a two-qubit Kraus channel to qubits 0 and 2 circ = circ.kraus([0,2], K)
# Inject noise approach # define phase damping noise noise = Noise.PhaseDamping(gamma=0.1) # the noise channel is applied to all the X gates in the circuit circ = Circuit().x(0).y(1).cnot(0,2).x(1).z(2) circ_noise = circ.copy() circ_noise.apply_gate_noise(noise, target_gates = Gate.X)
Running a circuit is the same user experience as before, as shown in the following two examples.
Example 1
task = device.run(circ, s3_location)
Or
Example 2
task = device.run(circ_noise, s3_location)
For more examples, see the Braket introductory noise simulator example