# Are these two circuits equivalent in performing controlled time-evolution?

I want to perform the controlled time-evolution of some 2 or 3-qubit Hamiltonian. Say we have this example: $$H= Z_0\otimes Z_1 + Z_1\otimes Z_2$$ The circuit performing the time-evolution controlled by qubit 3 could be generated as (the arguments of R_Z gate are randomly chosen to be pi):

qrz = QuantumRegister(4,'q')
crz = ClassicalRegister(1,'c')
qcz = QuantumCircuit(qrz,crz)
qcz.cx(0,1)
qcz.crz(np.pi,3,1)
qcz.cx(0,1)
qcz.cx(1,2)
qcz.crz(np.pi,3,2)
qcz.cx(1,2)
qcz.draw(output = 'mpl') However, I wonder is it possible that we can turn the circuit responsible for time-evolution directly into a quantum gate, and then the gate is controlled by qubit 3? Here's what I tried:

my_circ = QuantumCircuit(3)
my_circ.cx(0,1)
my_circ.rz(np.pi,1)
my_circ.cx(0,1)
my_circ.cx(1,2)
my_circ.rz(np.pi,2)
my_circ.cx(1,2)
my_circ.draw(output = 'mpl') Then I have

new_circ = QuantumCircuit(4)
new_circ.append(custom,[3,2,1,0])
new_circ.draw(output = 'mpl') Are the two circuits qcz and new_circ equivalent? Generally, if I know how to construct the quantum circuit for the time evolution of some Hamiltonian but don't know exactly how to perform controlled time-evolution, can I just transform the time-evolution circuit into a gate, and then do something similar to this example? Thanks for the help!

Yes, they should be.

You can check their statevector to confirm that they indeed generate the same state. First, you can generate some arbitrary initial state and use the following two methods to show that they generate the same statevector. For instance:

init_circ = QuantumCircuit(3)
init_circ.ry(2.5,0)
init_circ.ry(1.2, 1)
init_circ.ry(0.5, 2)
print(init_circ)
┌─────────┐
q_0: ┤ RY(2.5) ├
├─────────┤
q_1: ┤ RY(1.2) ├
├─────────┤
q_2: ┤ RY(0.5) ├
└─────────┘

my_circ = QuantumCircuit(3)
my_circ.cx(0,1)
my_circ.rz(np.pi,1)
my_circ.cx(0,1)
my_circ.cx(1,2)
my_circ.rz(np.pi,2)
my_circ.cx(1,2)
print(my_circ)

q_0: ──■─────────────■─────────────────────
┌─┴─┐┌───────┐┌─┴─┐
q_1: ┤ X ├┤ RZ(π) ├┤ X ├──■─────────────■──
└───┘└───────┘└───┘┌─┴─┐┌───────┐┌─┴─┐
q_2: ───────────────────┤ X ├┤ RZ(π) ├┤ X ├
└───┘└───────┘└───┘

xs_gate = my_circ.to_gate()
cxs_gate = xs_gate.control()
circuit = QuantumCircuit(4)
circuit.append(init_circ, [1,2,3])
circuit.append(cxs_gate, [0,1,2,3])
print(circuit)

q_0: ───────────────────────■────────
┌──────────────┐┌──────┴───────┐
q_1: ┤0             ├┤0             ├
│              ││              │
q_2: ┤1 circuit-155 ├┤1 circuit-156 ├
│              ││              │
q_3: ┤2             ├┤2             ├
└──────────────┘└──────────────┘

from qiskit.quantum_info import Statevector
circuit_statevector = Statevector(circuit)
print(circuit_statevector )
Statevector([0.25215633+0.j, 0.        +0.j, 0.75888206+0.j,
0.        +0.j, 0.17250943+0.j, 0.        +0.j,
0.51917915+0.j, 0.        +0.j, 0.06438608+0.j,
0.        +0.j, 0.1937744 +0.j, 0.        +0.j,
0.04404889+0.j, 0.        +0.j, 0.1325682 +0.j,
0.        +0.j],
dims=(2, 2, 2, 2))


Note: I added a layer of some arbitrary $$RY$$ rotations at the beginning just so that we don't start at the $$|0\rangle$$ state.

And for the other circuit you have:

qcz = QuantumCircuit(4)
qcz.ry(2.5,1)
qcz.ry(1.2, 2)
qcz.ry(0.5, 3)
qcz.cx(1,2)
qcz.crz(np.pi,0,2)
qcz.cx(1,2)
qcz.cx(2,3)
qcz.crz(np.pi,0,3)
qcz.cx(2,3)
print(qcz)

q_0: ────────────────────■──────────────────■─────────
┌─────────┐         │                  │
q_1: ┤ RY(2.5) ├──■──────┼──────■───────────┼─────────
├─────────┤┌─┴─┐┌───┴───┐┌─┴─┐         │
q_2: ┤ RY(1.2) ├┤ X ├┤ RZ(π) ├┤ X ├──■──────┼──────■──
├─────────┤└───┘└───────┘└───┘┌─┴─┐┌───┴───┐┌─┴─┐
q_3: ┤ RY(0.5) ├───────────────────┤ X ├┤ RZ(π) ├┤ X ├
└─────────┘                   └───┘└───────┘└───┘

qcz_statevector = Statevector(qcz)
print(qcz_statevector )
Statevector([0.25215633+0.j, 0.        +0.j, 0.75888206+0.j,
0.        +0.j, 0.17250943+0.j, 0.        +0.j,
0.51917915+0.j, 0.        +0.j, 0.06438608+0.j,
0.        +0.j, 0.1937744 +0.j, 0.        +0.j,
0.04404889+0.j, 0.        +0.j, 0.1325682 +0.j,
0.        +0.j],
dims=(2, 2, 2, 2))


Notice how the two statevectors are the same. This is because the two circuits are equivalent.

### Update:

I didn't think much when I answered this, and knew that the two circuits were equivalent to start so I just tested it out using the method above. Which is kinda convoluted. The other answer used Operator from quantum_info to show that the two operators are indeed equivalent is much better as it show they are two equivalent unitary matrix. There is a another method to do this, and I will provide it below:

qcz = QuantumCircuit(4)
qcz.cx(1,2)
qcz.crz(np.pi,0,2)
qcz.cx(1,2)
qcz.cx(2,3)
qcz.crz(np.pi,0,3)
qcz.cx(2,3)
backend = Aer.get_backend('unitary_simulator')
job = execute(qcz, backend)
result = job.result()
qcz_unitary  = result.get_unitary(qcz, decimals=3)

my_circ = QuantumCircuit(3)
my_circ.cx(0,1)
my_circ.rz(np.pi,1)
my_circ.cx(0,1)
my_circ.cx(1,2)
my_circ.rz(np.pi,2)
my_circ.cx(1,2)
xs_gate = my_circ.to_gate()
cxs_gate = xs_gate.control()
circuit = QuantumCircuit(4)
circuit.append(cxs_gate, [0,1,2,3])
backend = Aer.get_backend('unitary_simulator')
job = execute(circuit, backend)
result = job.result()
circuit_unitary  = result.get_unitary(circuit, decimals=3)


Now you can check that the two operators, qcz_unitary and circuit_unitary, represent the two circuits are equivalent.

Also note that these two circuits are equivalent fundamentally as pointed out by DaftWullie in this answer. Here, we are just checking to make sure what being implemented in Qiskit is correct.

• Thanks so much, that really helps!
– ZR-
Aug 24, 2021 at 19:00
• @ZR- No problem! Glad to see you still around the community. Aug 24, 2021 at 19:02
• Haha thanks!! In principle, can I apply this method to any quantum circuit controlled by an additional qubit?
– ZR-
Aug 24, 2021 at 19:05
• @ZR- yes. I don't see why not. Aug 24, 2021 at 19:10
• You need to check all basis vectors as inputs to verify unitary equivalence using this method. Aug 25, 2021 at 11:24

You can also check their equivalence using Operator, a class from the Qiskit's quantum_info module as follows.

from qiskit import *
from qiskit.quantum_info import Operator
import numpy as np


Build and convert each circuit to an operator.( used your name convention for the circuits )

The code below builds and converts the first circuit qrz to op1

qrz = QuantumCircuit(4)
qrz.cx(0, 1)
qrz.crz(np.pi, 3, 1)
qrz.cx(0, 1)
qrz.cx(1, 2)
qrz.crz(np.pi, 3, 2)
qrz.cx(1, 2)

op1 = Operator(qrz)

print(qrz)

q_0: ──■─────────────■─────────────────────
┌─┴─┐┌───────┐┌─┴─┐
q_1: ┤ X ├┤ RZ(π) ├┤ X ├──■─────────────■──
└───┘└───┬───┘└───┘┌─┴─┐┌───────┐┌─┴─┐
q_2: ─────────┼─────────┤ X ├┤ RZ(π) ├┤ X ├
│         └───┘└───┬───┘└───┘
q_3: ─────────■──────────────────■─────────



Following code transform your my_circ to op2.

my_circ = QuantumCircuit(3, name='custom')
my_circ.cx(0,1)
my_circ.rz(np.pi,1)
my_circ.cx(0,1)
my_circ.cx(1,2)
my_circ.rz(np.pi,2)
my_circ.cx(1,2)
print(my_circ)

custom = my_circ.to_gate().control()

new_circ = QuantumCircuit(4)
new_circ.append(custom,[3,2,1,0])
print(new_circ)

op2 = Operator(new_circ)

q_0: ──■─────────────■─────────────────────
┌─┴─┐┌───────┐┌─┴─┐
q_1: ┤ X ├┤ RZ(π) ├┤ X ├──■─────────────■──
└───┘└───────┘└───┘┌─┴─┐┌───────┐┌─┴─┐
q_2: ───────────────────┤ X ├┤ RZ(π) ├┤ X ├
└───┘└───────┘└───┘
┌─────────┐
q_0: ┤2        ├
│         │
q_1: ┤1 custom ├
│         │
q_2: ┤0        ├
└────┬────┘
q_3: ─────■─────



Now compare the two operator to check the equivalence of the circuits.

op1 == op2
True

• That's really neat. But I suppose it relies on a numerical matrix representation? Is that robust WRT floating-point inaccuracies? Aug 25, 2021 at 15:37
• @leftaroundabout Yes, it relies on numeric as you can see here github.com/Qiskit/qiskit-terra/blob/… and you can find tolerance values here github.com/Qiskit/qiskit-terra/blob/… Aug 25, 2021 at 16:50

Yes. In general, a trivial construction of controlled-$$U$$, given a circuit for $$U$$ is to make every circuit element controlled off the control qubit.

However, this introduces more overhead than necessary. If you have a part of your circuit that looks like $$VWV^\dagger$$, it is only necessary to make the $$W$$ controlled because the $$VV^\dagger$$ cancel in the absence of the $$W$$. This is what you've effectively done in your circuit where $$V=V^\dagger$$ is the controlled-not and $$W$$ is the $$R_z$$ gate.