Cirq Intermediate Free 2/13 in series 13 min read By ·

Advanced Gates and Custom Unitaries in Cirq

Define custom quantum gates in Cirq from unitary matrices, use the fermionic simulation gate (FSimGate), and build parameterized custom operations.

Prerequisites

  • Python proficiency
  • Beginner quantum computing concepts (superposition, entanglement)
  • Linear algebra basics
In this guide10 sections
  1. 01Installation
  2. 02Gate Abstractions in Cirq
  3. 03MatrixGate: Any Unitary from a Matrix
  4. 04Parameterized Gates with Sympy
  5. 05FSimGate: Google’s Native Two-Qubit Gate
  6. 06Custom Gate Class
  7. 07Gate Decomposition
  8. 08Simulating Custom Gates
  9. 09Exporting to OpenQASM 3
  10. 10What to Try Next

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) = iSWAP
  • FSimGate(-pi/4, 0) = sqrt(iSWAP) (and FSimGate(pi/4, 0) is its inverse)
  • FSimGate(0, pi) = CZ
  • FSimGate(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 FSimGate sweep 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 show cirq.google.optimized_for_sycamore(), which has been removed)
  • Read the Cirq custom gates documentation for the full protocol interface

Was this tutorial helpful?