Cirq gives you fine-grained control over quantum gates that higher-level frameworks abstract away. You can define gates from raw unitary matrices, express parameterized families of gates with symbolic expressions, and use Google’s native two-qubit gate (FSimGate) directly. This tutorial covers the mechanics of custom gate definition, decomposition, and simulation.
Installation
pip install cirq cirq-core sympy
Gate Abstractions in Cirq
Cirq separates three concepts that other frameworks conflate:
- Gate: a reusable quantum operation defined by its mathematical action (a unitary, a channel, a measurement). Gates are stateless; they carry no qubit information.
- Operation: a Gate applied to specific qubits.
cirq.H(q)is an Operation. - Moment: a set of Operations that can be applied simultaneously (no shared qubits). A list of Moments forms a Circuit.
This distinction makes it straightforward to define a gate once and apply it to arbitrary qubits, or to inspect a circuit’s time structure explicitly.
MatrixGate: Any Unitary from a Matrix
cirq.MatrixGate wraps any unitary matrix as a gate. This is the fastest path to using an arbitrary single- or two-qubit operation:
import cirq
import numpy as np
q = cirq.LineQubit.range(2)
# Custom single-qubit phase gate: |0> -> |0>, |1> -> e^(i*phi)|1>
phi = np.pi / 4
phase_matrix = np.array([
[1, 0],
[0, np.exp(1j * phi)]
])
phase_gate = cirq.MatrixGate(phase_matrix)
print("Phase gate unitary:\n", cirq.unitary(phase_gate))
# Verify it is unitary
U = cirq.unitary(phase_gate)
assert np.allclose(U @ U.conj().T, np.eye(2)), "Not unitary"
# Apply it in a circuit
circuit = cirq.Circuit([
cirq.H(q[0]),
phase_gate(q[0]),
cirq.CNOT(q[0], q[1]),
])
print(circuit)
# Custom two-qubit SWAP-like gate: parametric SWAP at angle pi/4
theta = np.pi / 4
swap_matrix = np.array([
[1, 0, 0, 0],
[0, np.cos(theta), 1j * np.sin(theta), 0],
[0, 1j * np.sin(theta), np.cos(theta), 0],
[0, 0, 0, 1],
])
partial_swap = cirq.MatrixGate(swap_matrix)
circuit2 = cirq.Circuit([partial_swap(q[0], q[1])])
print(circuit2)
print("Partial SWAP unitary:\n", np.round(cirq.unitary(partial_swap), 3))
MatrixGate automatically generates circuit diagram symbols and handles simulation. The downside: it cannot be decomposed into native gates automatically; use cirq.decompose for that.
Parameterized Gates with Sympy
Sympy symbols let you define gate families where parameters are resolved later, useful for variational algorithms and hardware calibration:
import cirq
import sympy
import numpy as np
# Define a symbolic rotation angle
theta = sympy.Symbol("theta")
phi = sympy.Symbol("phi")
# Cirq's built-in parameterized Rz gate
q = cirq.LineQubit(0)
rz = cirq.rz(theta)(q)
circuit = cirq.Circuit([
cirq.H(q),
cirq.rz(theta)(q),
cirq.ry(phi)(q),
])
print("Parameterized circuit:")
print(circuit)
# Resolve symbols to concrete values
resolver = cirq.ParamResolver({"theta": np.pi / 3, "phi": np.pi / 6})
resolved = cirq.resolve_parameters(circuit, resolver)
print("\nResolved circuit:")
print(resolved)
# Simulate the resolved circuit
sim = cirq.Simulator()
result = sim.simulate(resolved)
print("\nFinal state vector:", result.final_state_vector)
Parameterized circuits in Cirq are compatible with cirq.sweep: running the same circuit over a grid of parameter values in one call, which is efficient for variational sweeps.
FSimGate: Google’s Native Two-Qubit Gate
The FSimGate(theta, phi) is Google’s hardware-native two-qubit gate on Sycamore processors. It combines a partial SWAP (fermionic SWAP) with a conditional phase:
FSimGate(theta, phi) matrix (in the |00>, |01>, |10>, |11> basis):
[1, 0, 0, 0 ]
[0, cos(theta), -i*sin(theta), 0 ]
[0, -i*sin(theta), cos(theta), 0 ]
[0, 0, 0, e^(-i*phi) ]
Special cases:
FSimGate(-pi/2, 0)= iSWAPFSimGate(-pi/4, 0)= sqrt(iSWAP) (andFSimGate(pi/4, 0)is its inverse)FSimGate(0, pi)= CZFSimGate(pi/2, pi/6)= Sycamore gate
import cirq
import numpy as np
q0, q1 = cirq.LineQubit.range(2)
# iSWAP-equivalent
iswap_fsim = cirq.FSimGate(theta=-np.pi/2, phi=0)
print("FSimGate(-pi/2, 0) unitary:")
print(np.round(cirq.unitary(iswap_fsim), 3))
# Compare to cirq.ISWAP directly
print("\ncirq.ISWAP unitary:")
print(np.round(cirq.unitary(cirq.ISWAP), 3))
# CZ-equivalent
cz_fsim = cirq.FSimGate(theta=0, phi=np.pi)
print("\nFSimGate(0, pi) unitary (should match CZ):")
print(np.round(cirq.unitary(cz_fsim), 3))
# Sycamore gate: the actual Google hardware gate
sycamore_gate = cirq.FSimGate(theta=np.pi/2, phi=np.pi/6)
circuit = cirq.Circuit([
cirq.H(q0),
sycamore_gate(q0, q1),
cirq.H(q1),
])
print("\nSycamore gate circuit:")
print(circuit)
When targeting Google hardware, building circuits from FSimGate operations eliminates the translation pass entirely and reduces gate count.
Custom Gate Class
For gates you intend to reuse widely, define a proper cirq.Gate subclass. This gives you control over diagram labels, decomposition, and serialization:
import cirq
import numpy as np
from typing import Sequence, Union
class ControlledPhaseGate(cirq.Gate):
"""
A controlled-phase gate: applies phase e^(i*phi) to |11>.
This is equivalent to cirq.CZPowGate but written from scratch.
"""
def __init__(self, phi: float):
self.phi = phi
def _num_qubits_(self) -> int:
return 2
def _unitary_(self) -> np.ndarray:
return np.array([
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, np.exp(1j * self.phi)],
])
def _circuit_diagram_info_(
self, args: cirq.CircuitDiagramInfoArgs
) -> cirq.CircuitDiagramInfo:
angle_str = f"{self.phi / np.pi:.2f}π"
return cirq.CircuitDiagramInfo(wire_symbols=["@", f"P({angle_str})"])
def __repr__(self) -> str:
return f"ControlledPhaseGate(phi={self.phi})"
# Use it in a circuit
q0, q1 = cirq.LineQubit.range(2)
cp_gate = ControlledPhaseGate(phi=np.pi / 3)
circuit = cirq.Circuit([
cirq.H(q0),
cirq.H(q1),
cp_gate(q0, q1),
])
print(circuit)
print("Unitary:\n", np.round(cirq.unitary(cp_gate), 3))
Gate Decomposition
Add a _decompose_ method to express your gate in terms of standard gates. Cirq uses this for compilation and for hardware targets that don’t natively support your gate:
import cirq
import numpy as np
class DecomposableControlledPhase(cirq.Gate):
def __init__(self, phi: float):
self.phi = phi
def _num_qubits_(self) -> int:
return 2
def _unitary_(self) -> np.ndarray:
return np.diag([1, 1, 1, np.exp(1j * self.phi)])
def _decompose_(self, qubits):
q0, q1 = qubits
# Controlled-phase decomposes as:
# Rz(phi/2) on q0, Rz(phi/2) on q1, CX, Rz(-phi/2) on q1, CX
yield cirq.rz(self.phi / 2)(q0)
yield cirq.rz(self.phi / 2)(q1)
yield cirq.CNOT(q0, q1)
yield cirq.rz(-self.phi / 2)(q1)
yield cirq.CNOT(q0, q1)
def _circuit_diagram_info_(self, args):
return cirq.CircuitDiagramInfo(wire_symbols=["@", f"P({self.phi:.2f})"])
q0, q1 = cirq.LineQubit.range(2)
gate = DecomposableControlledPhase(phi=np.pi / 4)
# Original circuit
circuit = cirq.Circuit([gate(q0, q1)])
print("Original circuit:")
print(circuit)
# Decomposed form
decomposed = cirq.Circuit(cirq.decompose(gate(q0, q1)))
print("\nDecomposed circuit:")
print(decomposed)
# Verify unitaries match up to global phase (global phase is physically unobservable)
U1 = cirq.unitary(circuit)
U2 = cirq.unitary(decomposed)
idx = np.argmax(np.abs(U2.flat))
phase = U1.flat[idx] / U2.flat[idx]
assert np.allclose(U1, phase * U2), "Unitaries do not match even up to global phase"
print("Unitaries match (up to global phase): True")
Simulating Custom Gates
Custom gates work with all Cirq simulators without any extra configuration:
import cirq
import numpy as np
class DecomposableControlledPhase(cirq.Gate):
def __init__(self, phi: float):
self.phi = phi
def _num_qubits_(self) -> int:
return 2
def _unitary_(self) -> np.ndarray:
return np.diag([1, 1, 1, np.exp(1j * self.phi)])
def _decompose_(self, qubits):
q0, q1 = qubits
yield cirq.rz(self.phi / 2)(q0)
yield cirq.rz(self.phi / 2)(q1)
yield cirq.CNOT(q0, q1)
yield cirq.rz(-self.phi / 2)(q1)
yield cirq.CNOT(q0, q1)
def _circuit_diagram_info_(self, args):
return cirq.CircuitDiagramInfo(wire_symbols=["@", f"P({self.phi:.2f})"])
q0, q1 = cirq.LineQubit.range(2)
gate = DecomposableControlledPhase(phi=np.pi) # phi=pi is a CZ gate
circuit = cirq.Circuit([
cirq.H(q0),
cirq.H(q1),
gate(q0, q1),
cirq.H(q0),
cirq.H(q1),
])
sim = cirq.Simulator()
result = sim.simulate(circuit)
print("Final state vector:")
print(np.round(result.final_state_vector, 3))
print("\nCircuit:")
print(circuit)
# Measure
circuit_with_measurement = circuit + cirq.measure(q0, q1, key="result")
sample_result = sim.run(circuit_with_measurement, repetitions=1000)
print("\nMeasurement results:")
print(sample_result.histogram(key="result"))
Exporting to OpenQASM 3
Cirq can serialize circuits with custom gates to OpenQASM 3 for interoperability with other frameworks, provided the gate has a decomposition into standard gates:
import cirq
import numpy as np
class DecomposableControlledPhase(cirq.Gate):
def __init__(self, phi: float):
self.phi = phi
def _num_qubits_(self) -> int:
return 2
def _unitary_(self) -> np.ndarray:
return np.diag([1, 1, 1, np.exp(1j * self.phi)])
def _decompose_(self, qubits):
q0, q1 = qubits
yield cirq.rz(self.phi / 2)(q0)
yield cirq.rz(self.phi / 2)(q1)
yield cirq.CNOT(q0, q1)
yield cirq.rz(-self.phi / 2)(q1)
yield cirq.CNOT(q0, q1)
def _circuit_diagram_info_(self, args):
return cirq.CircuitDiagramInfo(wire_symbols=["@", f"P({self.phi:.2f})"])
q0, q1 = cirq.LineQubit.range(2)
gate = DecomposableControlledPhase(phi=np.pi / 2)
circuit = cirq.Circuit([
cirq.H(q0),
gate(q0, q1),
cirq.measure(q0, q1, key="out"),
])
# Decompose custom gates before export
decomposed_circuit = cirq.Circuit(cirq.decompose(circuit))
try:
qasm_output = cirq.qasm(decomposed_circuit)
print("OpenQASM 3 output:")
print(qasm_output)
except Exception as e:
print(f"QASM export note: {e}")
print("Full decomposed circuit:")
print(decomposed_circuit)
If export fails for gates without built-in QASM support, decompose first then export the decomposed circuit, which consists only of standard gates.
What to Try Next
- Use
cirq.testing.assert_implements_consistent_protocols(gate)to validate that your custom gate satisfies all Cirq gate contracts - Implement a parameterized
FSimGatesweep to characterize how entanglement varies with theta from 0 to pi/2 - Use
cirq.optimize_for_target_gateset(circuit, gateset=cirq_google.SycamoreTargetGateset())to compile arbitrary circuits to Sycamore-native FSimGate operations (older docs showcirq.google.optimized_for_sycamore(), which has been removed) - Read the Cirq custom gates documentation for the full protocol interface