Quantum Operations¶
CUDA-Q provides a default set of quantum operations on qubits.
These operations can be used to define custom kernels and libraries.
Since the set of quantum intrinsic operations natively supported on a specific target
depends on the backends architecture, the nvq++
compiler automatically
decomposes the default operations into the appropriate set of intrinsic operations
for that target.
The sections Unitary Operations on Qubits and Measurements on Qubits list the default set of quantum operations on qubits.
Operations that implement unitary transformations of the quantum state are templated. The template argument allows to invoke the adjoint and controlled version of the quantum transformation, see the section on Adjoint and Controlled Operations.
CUDA-Q additionally provides overloads to support broadcasting of
single-qubit operations across a vector of qubits. For example,
x(cudaq::qvector<>&)
flips the state of each qubit in the provided
cudaq::qvector
.
Unitary Operations on Qubits¶
x
¶
This operation implements the transformation defined by the Pauli-X matrix. It is also known as the quantum version of a NOT
-gate.
qubit = cudaq.qubit()
# Apply the unitary transformation
# X = | 0 1 |
# | 1 0 |
x(qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// X = | 0 1 |
// | 1 0 |
x(qubit);
y
¶
This operation implements the transformation defined by the Pauli-Y matrix.
qubit = cudaq.qubit()
# Apply the unitary transformation
# Y = | 0 -i |
# | i 0 |
y(qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// Y = | 0 -i |
// | i 0 |
y(qubit);
z
¶
This operation implements the transformation defined by the Pauli-Z matrix.
qubit = cudaq.qubit()
# Apply the unitary transformation
# Z = | 1 0 |
# | 0 -1 |
z(qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// Z = | 1 0 |
// | 0 -1 |
z(qubit);
h
¶
This operation is a rotation by π about the X+Z axis, and enables one to create a superposition of computational basis states.
qubit = cudaq.qubit()
# Apply the unitary transformation
# H = (1 / sqrt(2)) * | 1 1 |
# | 1 -1 |
h(qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// H = (1 / sqrt(2)) * | 1 1 |
// | 1 -1 |
h(qubit);
r1
¶
This operation is an arbitrary rotation about the |1>
state.
qubit = cudaq.qubit()
# Apply the unitary transformation
# R1(λ) = | 1 0 |
# | 0 exp(iλ) |
r1(math.pi, qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// R1(λ) = | 1 0 |
// | 0 exp(iλ) |
r1(std::numbers::pi, qubit);
rx
¶
This operation is an arbitrary rotation about the X axis.
qubit = cudaq.qubit()
# Apply the unitary transformation
# Rx(θ) = | cos(θ/2) -isin(θ/2) |
# | -isin(θ/2) cos(θ/2) |
rx(math.pi, qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// Rx(θ) = | cos(θ/2) -isin(θ/2) |
// | -isin(θ/2) cos(θ/2) |
rx(std::numbers::pi, qubit);
ry
¶
This operation is an arbitrary rotation about the Y axis.
qubit = cudaq.qubit()
# Apply the unitary transformation
# Ry(θ) = | cos(θ/2) -sin(θ/2) |
# | sin(θ/2) cos(θ/2) |
ry(math.pi, qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// Ry(θ) = | cos(θ/2) -sin(θ/2) |
// | sin(θ/2) cos(θ/2) |
ry(std::numbers::pi, qubit);
rz
¶
This operation is an arbitrary rotation about the Z axis.
qubit = cudaq.qubit()
# Apply the unitary transformation
# Rz(λ) = | exp(-iλ/2) 0 |
# | 0 exp(iλ/2) |
rz(math.pi, qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// Rz(λ) = | exp(-iλ/2) 0 |
// | 0 exp(iλ/2) |
rz(std::numbers::pi, qubit);
s
¶
This operation applies to its target a rotation by π/2 about the Z axis.
qubit = cudaq.qubit()
# Apply the unitary transformation
# S = | 1 0 |
# | 0 i |
s(qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// S = | 1 0 |
// | 0 i |
s(qubit);
t
¶
This operation applies to its target a π/4 rotation about the Z axis.
qubit = cudaq.qubit()
# Apply the unitary transformation
# T = | 1 0 |
# | 0 exp(iπ/4) |
t(qubit)
cudaq::qubit qubit;
// Apply the unitary transformation
// T = | 1 0 |
// | 0 exp(iπ/4) |
t(qubit);
swap
¶
This operation swaps the states of two qubits.
qubit_1, qubit_2 = cudaq.qubit(), cudaq.qubit()
# Apply the unitary transformation
# Swap = | 1 0 0 0 |
# | 0 0 1 0 |
# | 0 1 0 0 |
# | 0 0 0 1 |
swap(qubit_1, qubit_2)
cudaq::qubit qubit_1, qubit_2;
// Apply the unitary transformation
// Swap = | 1 0 0 0 |
// | 0 0 1 0 |
// | 0 1 0 0 |
// | 0 0 0 1 |
swap(qubit_1, qubit_2);
u3
¶
This operation applies the universal three-parameters operator to target qubit. The three parameters are Euler angles - theta (θ), phi (φ), and lambda (λ).
qubit = cudaq.qubit()
# Apply the unitary transformation
# U3(θ,φ,λ) = | cos(θ/2) -exp(iλ) * sin(θ/2) |
# | exp(iφ) * sin(θ/2) exp(i(λ + φ)) * cos(θ/2) |
u3(np.pi, np.pi, np.pi / 2, q)
cudaq::qubit qubit;
// Apply the unitary transformation
// U3(θ,φ,λ) = | cos(θ/2) -exp(iλ) * sin(θ/2) |
// | exp(iφ) * sin(θ/2) exp(i(λ + φ)) * cos(θ/2) |
u3(M_PI, M_PI, M_PI_2, q);
Adjoint and Controlled Operations¶
The adj
method of any gate can be used to invoke the
adjoint transformation:
# Create a kernel and allocate a qubit in a |0> state.
qubit = cudaq.qubit()
# Apply the unitary transformation defined by the matrix
# T = | 1 0 |
# | 0 exp(iπ/4) |
# to the state of the qubit `q`:
t(qubit)
# Apply its adjoint transformation defined by the matrix
# T† = | 1 0 |
# | 0 exp(-iπ/4) |
t.adj(qubit)
# `qubit` is now again in the initial state |0>.
The ctrl
method of any gate can be used to apply the transformation
conditional on the state of one or more control qubits, see also this
Wikipedia entry.
# Create a kernel and allocate qubits in a |0> state.
ctrl_1, ctrl_2, target = cudaq.qubit(), cudaq.qubit(), cudaq.qubit()
# Create a superposition.
h(ctrl_1)
# `ctrl_1` is now in a state (|0> + |1>) / √2.
# Apply the unitary transformation
# | 1 0 0 0 |
# | 0 1 0 0 |
# | 0 0 0 1 |
# | 0 0 1 0 |
x.ctrl(ctrl_1, ctrl_2)
# `ctrl_1` and `ctrl_2` are in a state (|00> + |11>) / √2.
# Set the state of `target` to |1>:
x(target)
# Apply the transformation T only if both
# control qubits are in a |1> state:
t.ctrl([ctrl_1, ctrl_2], target)
# The qubits ctrl_1, ctrl_2, and target are now in a state
# (|000> + exp(iπ/4)|111>) / √2.
The template argument cudaq::adj
can be used to invoke the
adjoint transformation:
// Allocate a qubit in a |0> state.
cudaq::qubit qubit;
// Apply the unitary transformation defined by the matrix
// T = | 1 0 |
// | 0 exp(iπ/4) |
// to the state of the qubit `q`:
t(qubit);
// Apply its adjoint transformation defined by the matrix
// T† = | 1 0 |
// | 0 exp(-iπ/4) |
t<cudaq::adj>(qubit);
// Qubit `q` is now again in the initial state |0>.
The template argument cudaq::ctrl
can be used to apply the transformation
conditional on the state of one or more control qubits, see also this
Wikipedia entry.
// Allocate qubits in a |0> state.
cudaq::qubit ctrl_1, ctrl_2, target;
// Create a superposition.
h(ctrl_1);
// Qubit ctrl_1 is now in a state (|0> + |1>) / √2.
// Apply the unitary transformation
// | 1 0 0 0 |
// | 0 1 0 0 |
// | 0 0 0 1 |
// | 0 0 1 0 |
x<cudaq::ctrl>(ctrl_1, ctrl_2);
// The qubits ctrl_1 and ctrl_2 are in a state (|00> + |11>) / √2.
// Set the state of `target` to |1>:
x(target);
// Apply the transformation T only if both
// control qubits are in a |1> state:
t<cudaq::ctrl>(ctrl_1, ctrl_2, target);
// The qubits ctrl_1, ctrl_2, and target are now in a state
// (|000> + exp(iπ/4)|111>) / √2.
Following common convention, by default the transformation is applied to the target qubit(s)
if all control qubits are in a |1>
state.
However, that behavior can be changed to instead apply the transformation when a control qubit is in
a |0>
state by negating the polarity of the control qubit.
The syntax for negating the polarity is the not-operator preceding the
control qubit:
cudaq::qubit c, q;
h(c);
x<cudaq::ctrl>(!c, q);
// The qubits c and q are in a state (|01> + |10>) / √2.
This notation is only supported in the context of applying a controlled operation and is only valid for control qubits. For example, negating either of the target qubits in the
swap
operation is not allowed.
Negating the polarity of control qubits is similarly supported when using cudaq::control
to conditionally apply a custom quantum kernel.
Measurements on Qubits¶
mz
¶
This operation measures a qubit with respect to the computational basis, i.e., it projects the state of that qubit onto the eigenvectors of the Pauli-Z matrix. This is a non-linear transformation, and no template overloads are available.
qubit = cudaq.qubit()
mz(qubit)
cudaq::qubit qubit;
mz(qubit);
mx
¶
This operation measures a qubit with respect to the Pauli-X basis, i.e., it projects the state of that qubit onto the eigenvectors of the Pauli-X matrix. This is a non-linear transformation, and no template overloads are available.
qubit = cudaq.qubit()
mx(qubit)
cudaq::qubit qubit;
mx(qubit);
my
¶
This operation measures a qubit with respect to the Pauli-Y basis, i.e., it projects the state of that qubit onto the eigenvectors of the Pauli-Y matrix. This is a non-linear transformation, and no template overloads are available.
qubit = cudaq.qubit()
kernel.my(qubit)
cudaq::qubit qubit;
my(qubit);
User-Defined Custom Operations¶
Users can define a custom quantum operation by its unitary matrix. First use
the API to register a custom operation, outside of a CUDA-Q kernel. Then the
operation can be used within a CUDA-Q kernel like any of the built-in operations
defined above.
Custom operations are supported on qubits only (qudit
with level = 2
).
The cudaq.register_operation
API accepts an identifier string for
the custom operation and its unitary matrix. The matrix can be a list
or
numpy
array of complex numbers. A 1D matrix is interpreted as row-major.
import cudaq
import numpy as np
cudaq.register_operation("custom_h", 1. / np.sqrt(2.) * np.array([1, 1, 1, -1]))
cudaq.register_operation("custom_x", np.array([0, 1, 1, 0]))
@cudaq.kernel
def bell():
qubits = cudaq.qvector(2)
custom_h(qubits[0])
custom_x.ctrl(qubits[0], qubits[1])
cudaq.sample(bell).dump()
The macro CUDAQ_REGISTER_OPERATION
accepts a unique name for the
operation, the number of target qubits, the number of rotation parameters
(can be 0), and the unitary matrix as a 1D row-major std::vector<complex>
representation.
#include <cudaq.h>
CUDAQ_REGISTER_OPERATION(custom_h, 1, 0,
{M_SQRT1_2, M_SQRT1_2, M_SQRT1_2, -M_SQRT1_2})
CUDAQ_REGISTER_OPERATION(custom_x, 1, 0, {0, 1, 1, 0})
__qpu__ void bell_pair() {
cudaq::qubit q, r;
custom_h(q);
custom_x<cudaq::ctrl>(q, r);
}
int main() {
auto counts = cudaq::sample(bell_pair);
for (auto &[bits, count] : counts) {
printf("%s\n", bits.data());
}
}
For multi-qubit operations, the matrix is interpreted with MSB qubit ordering, i.e. big-endian convention. The following example shows two different custom operations, each operating on 2 qubits.
import cudaq
import numpy as np
# Create and test a custom CNOT operation.
cudaq.register_operation("my_cnot", np.array([1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 0, 1,
0, 0, 1, 0]))
@cudaq.kernel
def bell_pair():
qubits = cudaq.qvector(2)
h(qubits[0])
my_cnot(qubits[0], qubits[1]) # `my_cnot(control, target)`
cudaq.sample(bell_pair).dump() # prints { 11:500 00:500 } (exact numbers will be random)
# Construct a custom unitary matrix for X on the first qubit and Y
# on the second qubit.
X = np.array([[0, 1 ], [1 , 0]])
Y = np.array([[0, -1j], [1j, 0]])
XY = np.kron(X, Y)
# Register the custom operation
cudaq.register_operation("my_XY", XY)
@cudaq.kernel
def custom_xy_test():
qubits = cudaq.qvector(2)
my_XY(qubits[0], qubits[1])
y(qubits[1]) # undo the prior Y gate on qubit 1
cudaq.sample(custom_xy_test).dump() # prints { 10:1000 }
#include <cudaq.h>
CUDAQ_REGISTER_OPERATION(MyCNOT, 2, 0,
{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0});
CUDAQ_REGISTER_OPERATION(
MyXY, 2, 0,
{0, 0, 0, {0, -1}, 0, 0, {0, 1}, 0, 0, {0, -1}, 0, 0, {0, 1}, 0, 0, 0});
__qpu__ void bell_pair() {
cudaq::qubit q, r;
h(q);
MyCNOT(q, r); // MyCNOT(control, target)
}
__qpu__ void custom_xy_test() {
cudaq::qubit q, r;
MyXY(q, r);
y(r); // undo the prior Y gate on qubit 1
}
int main() {
auto counts = cudaq::sample(bell_pair);
counts.dump(); // prints { 11:500 00:500 } (exact numbers will be random)
counts = cudaq::sample(custom_xy_test);
counts.dump(); // prints { 10:1000 }
}
Note
Custom operations are currently supported only on CUDA-Q Simulation Backends. Attempt to use with a hardware backend will result in runtime error.
Photonic Operations on Qudits¶
These operations are valid only on the photonics
target which does not support the quantum operations above.
plus
¶
This is a place-holder, to be updated later.
q = qudit(3)
plus(q)
cudaq::qvector<3> q(1);
plus(q[0]);
phase_shift
¶
This is a place-holder, to be updated later.
q = qudit(4)
phase_shift(q, 0.17)
cudaq::qvector<4> q(1);
phase_shift(q[0], 0.17);
beam_splitter
¶
This is a place-holder, to be updated later.
q = [qudit(3) for _ in range(2)]
beam_splitter(q[0], q[1], 0.34)
cudaq::qvector<3> q(2);
beam_splitter(q[0], q[1], 0.34);
mz
¶
This operation returns the measurement results of the input qudit(s).
qutrits = [qudit(3) for _ in range(2)]
mz(qutrits)
cudaq::qvector<3> qutrits(2);
mz(qutrits);