Today we will learn about how to compare different ansatz structure by measuring their expressibility and entanglement capabilities. Things covered in this blog is inspired from [1].
Ansatze are simply a parameterized quantum circuits (PQC), which play an essential role in the performance of many variational hybrid quantum-classical (HQC) algorithms. Major challenge while designing an asatz is to choose an effective template circuit that well represents the solution space while maintaining a low circuit depth and number of parameters. Here, we make a choice of two ansatze, one randomly and another inspired from the given hint.
xxxxxxxxxx
def ansatz1(params, num_qubits):
"""
Generate an templated ansatz with given parameters
Args:
params (array[float]): Parameters to initialize the parameterized unitary.
num_qubits (int): Number of qubits in the circuit.
Returns:
ansatz (QuantumCircuit): Generated ansatz circuit
"""
ansatz = QuantumCircuit(num_qubits, num_qubits)
params = params.reshape(2,2)
for idx in range(num_qubits):
ansatz.rx(params[0][idx], idx)
ansatz.cnot(0, 1)
for idx in range(num_qubits):
ansatz.rz(params[1][idx], idx)
return ansatz
ansatz1(np.random.uniform(-np.pi, np.pi, (2,2)), 2).draw()
┌─────────────┐ ┌──────────────┐ q_0: ┤ RX(0.74906) ├──■──┤ RZ(-0.10279) ├ ├─────────────┤┌─┴─┐├─────────────┬┘ q_1: ┤ RX(0.40493) ├┤ X ├┤ RZ(-2.3803) ├─ └─────────────┘└───┘└─────────────┘ c: 2/════════════════════════════════════
xxxxxxxxxx
def ansatz2(params, num_qubits):
"""
Generate an templated ansatz with given parameters
Args:
params (array[float]): Parameters to initialize the parameterized unitary.
num_qubits (int): Number of qubits in the circuit.
Returns:
ansatz (QuantumCircuit): Generated ansatz circuit
"""
params = np.array(params).reshape(1)
ansatz = QuantumCircuit(num_qubits, num_qubits)
ansatz.h(0)
ansatz.cx(0, 1)
ansatz.rx(params[0], 0)
return ansatz
ansatz2([np.pi], 2).draw()
┌───┐ ┌────────┐ q_0: ┤ H ├──■──┤ RX(pi) ├ └───┘┌─┴─┐└────────┘ q_1: ─────┤ X ├────────── └───┘ c: 2/════════════════════
We quantify expressibility of ansatze using the Hilbert-Schmidt norm of
This quantity needs to be taken with a pinch of salt as it is an oversimplification of the
Here, the first term, i.e. a Haar integral, is the integral over a group of unitaries distributed randomly according to the Haar measure. Whereas, the second term, is taken over all states over the measure induced by uniformly sampling the parameters
xxxxxxxxxx
def random_unitary(N):
"""
Return a Haar distributed random unitary from U(N)
"""
Z = np.random.randn(N, N) + 1.0j * np.random.randn(N, N)
[Q, R] = sp.linalg.qr(Z)
D = np.diag(np.diagonal(R) / np.abs(np.diagonal(R)))
return np.dot(Q, D)
def haar_integral(num_qubits, samples):
"""
Return calculation of Haar Integral for a specified number of samples.
"""
N = 2**num_qubits
randunit_density = np.zeros((N, N), dtype=complex)
zero_state = np.zeros(N, dtype=complex)
zero_state[0] = 1
for _ in range(samples):
A = np.matmul(zero_state, random_unitary(N)).reshape(-1,1)
randunit_density += np.kron(A, A.conj().T)
randunit_density/=samples
return randunit_density
def pqc_integral(num_qubits, ansatze, size, samples):
"""
Return calculation of Integral for a PQC over the uniformly sampled
the parameters θ for the specified number of samples.
"""
N = num_qubits
randunit_density = np.zeros((2**N, 2**N), dtype=complex)
for _ in range(samples):
params = np.random.uniform(-np.pi, np.pi, size)
ansatz = ansatze(params, N)
result = execute(ansatz,
backend=Aer.get_backend('statevector_simulator')).result()
U = result.get_statevector(ansatz, decimals=5).reshape(-1,1)
randunit_density += np.kron(U, U.conj().T)
return randunit_density/samples
xxxxxxxxxx
np.linalg.norm(haar_integral(2, 2048) - haar_integral(2, 2048))
xxxxxxxxxx
0.025708942385801254
xxxxxxxxxx
np.linalg.norm(haar_integral(2, 2048) - pqc_integral(2, ansatz1, (2,2), 2048))
xxxxxxxxxx
0.02432573321735785
xxxxxxxxxx
np.linalg.norm(haar_integral(2, 2048) - pqc_integral(2, ansatz2, 1, 2048))
xxxxxxxxxx
0.49896675806724666
xxxxxxxxxx
def ansatz3(params, num_qubits):
"""
Generate an templated ansatz with no parameters
Args:
params (array[float]): Parameters to initialize the parameterized unitary.
num_qubits (int): Number of qubits in the circuit.
Returns:
ansatz (QuantumCircuit): Generated ansatz circuit
"""
ansatz = QuantumCircuit(num_qubits, num_qubits)
return ansatz
np.linalg.norm(haar_integral(2, 2048) - pqc_integral(2, ansatz3, 0, 2048))
xxxxxxxxxx
0.8589639169688795
Clearly, expressibility are in the order: Ansatz 3 < Ansatz 2 < Ansatz 1, i.e. the power to probe Hilbert space is much more for our randomly chosen ansatz, which is guessable.
We quantify entanlging capability [1] of ansatze by calculating the average Meyer-Wallach entanglement,
Here, partial_trace
.
xxxxxxxxxx
def meyer_wallach(circuit, num_qubits, size, sample=1024):
"""
Returns the meyer-wallach entanglement measure for the given circuit.
"""
res = np.zeros(sample, dtype=complex)
N = num_qubits
for i in range(sample):
params = np.random.uniform(-np.pi, np.pi, size)
ansatz = circuit(params, N)
result = execute(ansatz,
backend=Aer.get_backend('statevector_simulator')).result()
U = result.get_statevector(ansatz, decimals=5)
entropy = 0
qb = list(range(N))
for j in range(N):
dens = quantum_info.partial_trace(U, qb[:j]+qb[j+1:]).data
trace = np.trace(dens**2)
entropy += trace
entropy /= N
res[i] = 1 - entropy
return 2*np.sum(res).real/sample
xxxxxxxxxx
meyer_wallach(ansatz3, 2, 0)
xxxxxxxxxx
0.0
xxxxxxxxxx
meyer_wallach(ansatz1, 2, (2,2))
xxxxxxxxxx
0.6190388917964508
xxxxxxxxxx
meyer_wallach(ansatz2, 2, 1)
xxxxxxxxxx
1.0
Clearly, the entangling capability are in the order: Ansatz 3 < Ansatz 1 < Ansatz 2. Therefore, we can guess limited expressibility of Ansatz 2 is compensated by its higher entangling capability.