View on QuantumAI | Run in Google Colab | View source on GitHub | Download notebook |
The Transformers page introduced what a transformer is, what transformers are available in Cirq, and how to create a simple one as a composite of others. This page covers the details necessary for creating more nuanced custom transformers, including cirq.TransformerContext
, primitives and decompositions.
Setup
try:
import cirq
except ImportError:
print("installing cirq...")
!pip install --quiet cirq
import cirq
print("installed cirq.")
cirq.TRANSFORMER
API and @cirq.transformer
decorator
Any callable that satisfies the cirq.TRANSFORMER
contract, i.e. takes a cirq.AbstractCircuit
and cirq.TransformerContext
and returns a transformed cirq.AbstractCircuit
, is a valid transformer in Cirq.
You can create a custom transformer by simply decorating a class/method, that satisfies the above contract, with @cirq.transformer
decorator.
@cirq.transformer
def reverse_circuit(circuit, *, context=None):
"""Transformer to reverse the input circuit."""
return circuit[::-1]
@cirq.transformer
class SubstituteGate:
"""Transformer to substitute `source` gates with `target` in the input circuit."""
def __init__(self, source, target):
self._source = source
self._target = target
def __call__(self, circuit, *, context=None):
batch_replace = []
for i, op in circuit.findall_operations(lambda op: op.gate == self._source):
batch_replace.append((i, op, self._target.on(*op.qubits)))
transformed_circuit = circuit.unfreeze(copy=True)
transformed_circuit.batch_replace(batch_replace)
return transformed_circuit
# Build your circuit
q = cirq.NamedQubit("q")
circuit = cirq.Circuit(
cirq.X(q), cirq.CircuitOperation(cirq.FrozenCircuit(cirq.X(q), cirq.Y(q))), cirq.Z(q)
)
# Transform and compare the circuits.
substitute_gate = SubstituteGate(cirq.X, cirq.S)
print("Original Circuit:", circuit, "\n", sep="\n")
print("Reversed Circuit:", reverse_circuit(circuit), "\n", sep="\n")
print("Substituted Circuit:", substitute_gate(circuit), sep="\n")
Original Circuit: q: ───X───[ q: ───X───Y─── ]───Z─── Reversed Circuit: q: ───Z───[ q: ───X───Y─── ]───X─── Substituted Circuit: q: ───S───[ q: ───X───Y─── ]───Z───
cirq.TransformerContext
to store common configurable options
cirq.TransformerContext
is a dataclass that stores common configurable options for all transformers. All cirq transformers should accept the transformer context as an optional keyword argument.
The @cirq.transformer
decorator can inspect the cirq.TransformerContext
argument and automatically append useful functionality, like support for automated logging and recursively running the transformer on nested sub-circuits.
cirq.TransformerLogger
and support for automated logging
The cirq.TransformerLogger
class is used to log the actions of a transformer on an input circuit. @cirq.transformer
decorator automatically adds support for logging the initial and final circuits for each transfomer step.
# Note that you want to log the steps.
context = cirq.TransformerContext(logger=cirq.TransformerLogger())
# Transform the circuit.
transformed_circuit = reverse_circuit(circuit, context=context)
transformed_circuit = substitute_gate(transformed_circuit, context=context)
# Show the steps.
context.logger.show()
Transformer-1: reverse_circuit Initial Circuit: q: ───X───[ q: ───X───Y─── ]───Z─── Final Circuit: q: ───Z───[ q: ───X───Y─── ]───X─── ---------------------------------------- Transformer-2: SubstituteGate Initial Circuit: q: ───Z───[ q: ───X───Y─── ]───X─── Final Circuit: q: ───Z───[ q: ───X───Y─── ]───S─── ----------------------------------------
Neither of the custom transformers, reverse_circuit
or substitute_gate
, had any explicit support for a logger present in the context
argument, but the decorator was able to use it anyways.
If your custom transformer calls another transformer as part of it, then that transformer should log its behavior as long as you pass the context
object to it. All Cirq-provided transformers do this.
@cirq.transformer
def reverse_and_substitute(circuit, context=None):
reversed_circuit = reverse_circuit(circuit, context=context)
reversed_and_substituted_circuit = substitute_gate(reversed_circuit, context=context)
return reversed_and_substituted_circuit
# Note that you want to log the steps.
context = cirq.TransformerContext(logger=cirq.TransformerLogger())
# Transform the circuit.
transformed_circuit = reverse_and_substitute(circuit, context=context)
# Show the steps.
context.logger.show()
Transformer-1: reverse_and_substitute Initial Circuit: q: ───X───[ q: ───X───Y─── ]───Z─── Final Circuit: q: ───Z───[ q: ───X───Y─── ]───S─── ---------------------------------------- Transformer-2: reverse_circuit Initial Circuit: q: ───X───[ q: ───X───Y─── ]───Z─── Final Circuit: q: ───Z───[ q: ───X───Y─── ]───X─── ---------------------------------------- Transformer-3: SubstituteGate Initial Circuit: q: ───Z───[ q: ───X───Y─── ]───X─── Final Circuit: q: ───Z───[ q: ───X───Y─── ]───S─── ----------------------------------------
Support for deep=True
You can call @cirq.transformer(add_deep_support=True)
to automatically add the functionality of recursively running the custom transformer on circuits wrapped inside cirq.CircuitOperation
. The recursive execution behavior of the transformer can then be controlled by setting deep=True
in the transformer context.
@cirq.transformer(add_deep_support=True)
def reverse_circuit_deep(circuit, *, context=None):
"""Transformer to reverse the input circuit."""
return circuit[::-1]
@cirq.transformer(add_deep_support=True)
class SubstituteGateDeep(SubstituteGate):
"""Transformer to substitute `source` gates with `target` in the input circuit."""
pass
# Note that you want to transform the CircuitOperations.
context = cirq.TransformerContext(deep=True)
# Transform and compare the circuits.
substitute_gate_deep = SubstituteGateDeep(cirq.X, cirq.S)
print("Original Circuit:", circuit, "\n", sep="\n")
print(
"Reversed Circuit with deep=True:",
reverse_circuit_deep(circuit, context=context),
"\n",
sep="\n",
)
print(
"Substituted Circuit with deep=True:", substitute_gate_deep(circuit, context=context), sep="\n"
)
Original Circuit: q: ───X───[ q: ───X───Y─── ]───Z─── Reversed Circuit with deep=True: q: ───Z───[ q: ───Y───X─── ]───X─── Substituted Circuit with deep=True: q: ───S───[ q: ───S───Y─── ]───Z───
Transformer Primitives and Decompositions
If you need to perform more fundamental changes than just running other transformers in sequence (like SubstituteGate
did with cirq.Circuit.batch_replace
), Cirq provides circuit compilation primitives and gate decomposition utilities for doing so.
Moment preserving transformer primitives
Cirq's transformer primitives are useful abstractions to implement common transformer patterns, while preserving the moment structure of input circuit. Some of the notable transformer primitives are:
cirq.map_operations
: Applies local transformations on operations, by callingmap_func(op)
for eachop
.cirq.map_moments
: Applies local transformation on moments, by callingmap_func(m)
for each momentm
.cirq.merge_operations
: Merges connected component of operations by iteratively callingmerge_func(op1, op2)
for every pair of mergeable operationsop1
andop2
.cirq.merge_moments
: Merges adjacent moments, from left to right, by iteratively callingmerge_func(m1, m2)
for adjacent momentsm1
andm2
.
An important property of these primitives is that they have support for common configurable options present in cirq.TransformerContext
, such as tags_to_ignore
and deep
, as demonstrated in the example below.
@cirq.transformer
def substitute_gate_using_primitives(circuit, *, context=None, source=cirq.X, target=cirq.S):
"""Transformer to substitute `source` gates with `target` in the input circuit.
The transformer is implemented using `cirq.map_operations` primitive and hence
has built-in support for
1. Recursively running the transformer on sub-circuits if `context.deep is True`.
2. Ignoring operations tagged with any of `context.tags_to_ignore`.
"""
return cirq.map_operations(
circuit,
map_func=lambda op, _: target.on(*op.qubits) if op.gate == source else op,
deep=context.deep if context else False,
tags_to_ignore=context.tags_to_ignore if context else (),
)
# Build your circuit from x_y_x components.
x_y_x = [cirq.X(q), cirq.Y(q), cirq.X(q).with_tags("ignore")]
circuit = cirq.Circuit(x_y_x, cirq.CircuitOperation(cirq.FrozenCircuit(x_y_x)), x_y_x)
# Note that you want to transform the CircuitOperations and ignore tagged operations.
context = cirq.TransformerContext(deep=True, tags_to_ignore=("ignore",))
# Compare the before and after circuits.
print("Original Circuit:", circuit, "\n", sep="\n")
print(
"Substituted Circuit:",
substitute_gate_using_primitives(circuit, context=context),
"\n",
sep="\n",
)
Original Circuit: q: ───X───Y───X[ignore]───[ q: ───X───Y───X[ignore]─── ]───X───Y───X[ignore]─── Substituted Circuit: q: ───S───Y───X[ignore]───[ q: ───S───Y───X[ignore]─── ]───S───Y───X[ignore]───
Analytical Gate Decompositions
Gate decomposition is the process of implementing / decomposing a given unitary U
using only gates that belong to a specific target gateset.
Cirq provides analytical decomposition methods, often based on KAK Decomposition, to decompose one-, two-, and three-qubit unitary matrices into specific target gatesets. Some notable decompositions are:
cirq.single_qubit_matrix_to_pauli_rotations
: Decomposes a single qubit matrix to ZPow/XPow/YPow rotations.cirq.single_qubit_matrix_to_phased_x_z
: Decomposes a single-qubit matrix to a PhasedX and Z gate.cirq.two_qubit_matrix_to_sqrt_iswap_operations
: Decomposes any two-qubit unitary matrix into ZPow/XPow/YPow/sqrt-iSWAP gates.cirq.two_qubit_matrix_to_cz_operations
: Decomposes any two-qubit unitary matrix into ZPow/XPow/YPow/CZ gates.cirq.three_qubit_matrix_to_operations
: Decomposes any three-qubit unitary matrix into CZ/CNOT and single qubit rotations.
You can use these analytical decomposition methods to build transformers which can rewrite a given circuit using only gates from the target gateset. This example again uses the transformer primitives to support recursive execution and ignore
tagging.
@cirq.transformer
def convert_to_cz_target(circuit, *, context=None, atol=1e-8, allow_partial_czs=True):
"""Transformer to rewrite the given circuit using CZs + 1-qubit rotations.
Note that the transformer decomposes only operations on <= 2-qubits and is
presented as an illustration of using transformer primitives + analytical
decomposition methods.
"""
def map_func(op: cirq.Operation, _) -> cirq.OP_TREE:
if not (cirq.has_unitary(op) and cirq.num_qubits(op) <= 2):
return op
matrix = cirq.unitary(op)
qubits = op.qubits
if cirq.num_qubits(op) == 1:
g = cirq.single_qubit_matrix_to_phxz(matrix)
return g.on(*qubits) if g else []
return cirq.two_qubit_matrix_to_cz_operations(
*qubits, matrix, allow_partial_czs=allow_partial_czs, atol=atol
)
return cirq.map_operations_and_unroll(
circuit,
map_func,
deep=context.deep if context else False,
tags_to_ignore=context.tags_to_ignore if context else (),
)
# Build the circuit from three versions of the same random component
component = cirq.testing.random_circuit(qubits=3, n_moments=2, op_density=0.8, random_state=1234)
component_operation = cirq.CircuitOperation(cirq.FrozenCircuit(component))
# A normal component, a CircuitOperation version, and a ignore-tagged CircuitOperation version
circuit = cirq.Circuit(component, component_operation, component_operation.with_tags('ignore'))
# Note that you want to transform the CircuitOperations, ignore tagged operations, and log the steps.
context = cirq.TransformerContext(
deep=True, tags_to_ignore=("ignore",), logger=cirq.TransformerLogger()
)
# Run your transformer.
converted_circuit = convert_to_cz_target(circuit, context=context)
# Ensure that the resulting circuit is equivalent.
cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(circuit, converted_circuit)
# Show the steps executed.
context.logger.show()
Transformer-1: convert_to_cz_target Initial Circuit: [ 0: ───iSwap─────── ] [ 0: ───iSwap─────── ] [ │ ] [ │ ] 0: ───iSwap───────[ 1: ───iSwap─────── ]───[ 1: ───iSwap─────── ]─────────── │ [ ] [ ] │ [ 2: ───────────Z─── ] [ 2: ───────────Z─── ][ignore] │ │ │ 1: ───iSwap───────#2───────────────────────#2─────────────────────────────── │ │ 2: ───────────Z───#3───────────────────────#3─────────────────────────────── Final Circuit: [ 0: ───PhX(-0.75)^0.5───@───PhX(0.25)^0.5────@───PhX(-0.75)^0.5───S─────────────────────────── ] [ 0: ───iSwap─────── ] [ │ │ ] [ │ ] 0: ───PhX(-0.75)^0.5───@───PhX(0.25)^0.5────@───PhX(-0.75)^0.5───S───────────────────────────[ 1: ───PhX(0.25)^0.5────@───PhX(-0.75)^0.5───@───PhX(0.25)^0.5────S─────────────────────────── ]───[ 1: ───iSwap─────── ]─────────── │ │ [ ] [ ] │ │ [ 2: ──────────────────────────────────────────────────────────────────PhXZ(a=-0.5,x=0,z=-1)─── ] [ 2: ───────────Z─── ][ignore] │ │ │ │ 1: ───PhX(0.25)^0.5────@───PhX(-0.75)^0.5───@───PhX(0.25)^0.5────S───────────────────────────#2──────────────────────────────────────────────────────────────────────────────────────────────────#2─────────────────────────────── │ │ 2: ──────────────────────────────────────────────────────────────────PhXZ(a=-0.5,x=0,z=-1)───#3──────────────────────────────────────────────────────────────────────────────────────────────────#3─────────────────────────────── ----------------------------------------
Heuristic Gate Decompositions
Cirq also provides heuristic methods for decomposing any two qubit unitary matrix in terms of any specified two qubit target unitary + single qubit rotations. These methods are useful when accurate analytical decompositions for the target unitary are not known or when gate decomposition fidelity (i.e. accuracy of decomposition) can be traded off against decomposition depth (i.e. number of 2q gates in resulting decomposition) to achieve a higher overall gate fidelity.
See the following resources for more details on heuristic gate decomposition:
Summary
Cirq provides a flexible and powerful framework to
- Use built-in transformer primitives and analytical tools to create powerful custom transformers both from scratch and by composing existing transformers.
- Easily integrate custom transformers with built-in infrastructure to augment functionality like automated logging, recursive execution on sub-circuits, support for no-compile tags etc.