Skip to content

Commit 4867eb4

Browse files
committed
minor version bump
1 parent 2724f75 commit 4867eb4

File tree

5 files changed

+310
-3
lines changed

5 files changed

+310
-3
lines changed
Loading

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = qrisp
3-
version = 0.5.1
3+
version = 0.5.2
44
author = The Qrisp authors
55
author_email = raphael.seidel@fokus.fraunhofer.de
66
description = A high-level quantum programming language

src/qrisp/circuit/quantum_circuit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ def to_latex(self, **kwargs):
10391039
"""
10401040
from qrisp.interface import convert_to_qiskit
10411041

1042-
qiskit_qc = convert_to_qiskit(self, "qiskit", transpile=False)
1042+
qiskit_qc = convert_to_qiskit(self, transpile = False)
10431043

10441044
from qiskit.visualization import circuit_drawer
10451045

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
"""
2+
\********************************************************************************
3+
* Copyright (c) 2023 the Qrisp authors
4+
*
5+
* This program and the accompanying materials are made available under the
6+
* terms of the Eclipse Public License 2.0 which is available at
7+
* http://www.eclipse.org/legal/epl-2.0.
8+
*
9+
* This Source Code may also be made available under the following Secondary
10+
* Licenses when the conditions for such availability set forth in the Eclipse
11+
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
12+
* with the GNU Classpath Exception which is
13+
* available at https://www.gnu.org/software/classpath/license.html.
14+
*
15+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
16+
********************************************************************************/
17+
"""
18+
19+
# This file implements a performance benchmark of simulating molecules using PySCF data
20+
# We compare the performance of the algorithm implemented in OpenFermion
21+
# (described here: https://quantumai.google/openfermion/tutorials/intro_workshop_exercises#hamiltonian_simulation_with_trotter_formulas)
22+
# vs. the Qrisp implementation https://www.qrisp.eu/general/tutorial/H2.html
23+
24+
# The amount of RZ gates is proportional to the required computation effort
25+
# because Hamiltonian Simulation is an algorithm, which requires fault tolerant devices.
26+
# In this device model, arbitrary angle gates (like RZ, P or RX) need to be
27+
# synthesized by a sequence of Clifford+T gates. More details here: https://arxiv.org/abs/1403.2975
28+
29+
import numpy as np
30+
import openfermion as of
31+
import openfermionpyscf as ofpyscf
32+
import cirq
33+
from qrisp import QuantumCircuit, QuantumVariable
34+
from qrisp.operators import FermionicOperator
35+
from scipy.sparse import linalg
36+
from numpy.linalg import norm
37+
38+
# Function to indicate whether an Operation object contributes
39+
# to the RZ-depth
40+
# For more details check "Gate-speed aware compilation" here:
41+
# https://www.qrisp.eu/reference/Core/generated/qrisp.QuantumSession.compile.html
42+
def rz_depth_indicator(op):
43+
if op.name in ["rz", "p", "u1", "rx", "ry"]:
44+
return 1
45+
# Some u3 operations in the OpenFermion circuit have u3 gates where the parameters
46+
# are still Clifford. We filter out the ones where all three are Clifford
47+
# using some function that is defined below. For the other ones, we assume
48+
# that they can be implemented with two arbitrary angle rotations
49+
# (which is very conservative - normally you need three!)
50+
if op.name == "u3":
51+
return 2
52+
else:
53+
return 0
54+
55+
# Function to benchmark simulation of molecules
56+
def benchmark_hamiltonian_simulation(molecule_data):
57+
geometry, basis, multiplicity, charge = molecule_data
58+
59+
# Generate Hamiltonian
60+
hamiltonian = ofpyscf.generate_molecular_hamiltonian(geometry, basis, multiplicity, charge)
61+
hamiltonian_ferm_op = of.get_fermion_operator(hamiltonian)
62+
n_qubits = of.count_qubits(hamiltonian)
63+
64+
# OpenFermion implementation taken from here:
65+
# https://quantumai.google/openfermion/tutorials/intro_workshop_exercises#hamiltonian_simulation_with_trotter_formulas
66+
def openfermion_circuit():
67+
qubits = cirq.LineQubit.range(n_qubits)
68+
circuit = cirq.Circuit(
69+
of.simulate_trotter(qubits, hamiltonian, time=1.0, n_steps=1, order=0, algorithm=of.LOW_RANK)
70+
)
71+
# Convert to Qrisp circuit via OpenQASM
72+
return QuantumCircuit.from_qasm_str(circuit.to_qasm())
73+
74+
# Qrisp implementation taken from here:
75+
# https://www.qrisp.eu/general/tutorial/H2.html
76+
def qrisp_circuit():
77+
H = FermionicOperator.from_openfermion(hamiltonian_ferm_op)
78+
qv = QuantumVariable(n_qubits)
79+
U = H.trotterization()
80+
U(qv, steps=1, t=1)
81+
qc = qv.qs.compile(workspace = len(qv)//4, gate_speed = rz_depth_indicator)
82+
# Move the workspace qubits up to facilitate computation of the unitary
83+
i = qc.num_qubits() - 1
84+
while i >= len(qv):
85+
qc.qubits.insert(0, qc.qubits.pop(-1))
86+
i -= 1
87+
return qc.transpile()
88+
89+
# Function to determine the RZ count
90+
def count_rz(qc):
91+
ops_count_dic = qc.transpile().count_ops()
92+
return ops_count_dic.get("rz", 0) + ops_count_dic.get("u1", 0) + 2*ops_count_dic.get("u3", 0) + ops_count_dic.get("p", 0)
93+
94+
# Function to count the CNOT gates
95+
# We don't count the SWAP gates within the OpenFermion circuit since these SWAP
96+
# gates ensure linear connectivity.
97+
# The Qrisp algorithm can also be run on linear connectivity with a minimal number of
98+
# SWAPS, which is however not yet implemented.
99+
def count_cnot(qc):
100+
ops_count_dic = qc.count_ops()
101+
return ops_count_dic.get("cx", 0) + ops_count_dic.get("cz", 0) + 3*ops_count_dic.get("compiled_gidney_mcx", 0) + 0.5*ops_count_dic.get("compiled_gidney_mcx_inv", 0)
102+
103+
# Function to compute the RZ depth
104+
def rz_depth(qc):
105+
return qc.depth(depth_indicator = rz_depth_indicator)
106+
107+
# Function to compute the precision of the quantum circuit to the exact unitary
108+
def unitary_distance(qc, exact_unitary):
109+
qc_unitary = qc.get_unitary()
110+
qc_unitary = qc_unitary[:exact_unitary.shape[0],:exact_unitary.shape[0]]
111+
112+
# Since OpenFermion doesn't properly implement the global phase
113+
# (this is a bug!) we manually correct.
114+
argmax = np.argmax(np.abs(qc_unitary).ravel())
115+
qc_phase = np.angle(qc_unitary.ravel()[argmax])
116+
exact_phase = np.angle(exact_unitary.ravel()[0, argmax])
117+
118+
qc_unitary = np.exp(1j*(exact_phase-qc_phase))*qc_unitary
119+
120+
# Compute the distance
121+
return norm(qc_unitary - exact_unitary)
122+
123+
124+
# Compute circuits
125+
qc_of = openfermion_circuit()
126+
qc_qrisp = qrisp_circuit()
127+
128+
# Compute metrics
129+
130+
# Some gates in the OpenFermion implementation are arbitrary angle but have a
131+
# parameter value, which indicates that they are clifford (for instance (RZ(pi) = Z)).
132+
# We filter out these gates since we only want to count arbitrary angle
133+
# gates that need to be synthesized since these are the costly resource.
134+
clifford_filtered_qc = filter_clifford(qc_of)
135+
136+
of_rz_count = count_rz(clifford_filtered_qc)
137+
of_cx_count = count_cnot(clifford_filtered_qc)
138+
of_rz_depth = rz_depth(clifford_filtered_qc)
139+
140+
qrisp_rz_count = count_rz(qc_qrisp)
141+
qrisp_cx_count = count_cnot(qc_qrisp)
142+
qrisp_rz_depth = rz_depth(qc_qrisp)
143+
144+
# If the circuit has more than 10 qubits, computing the unitary is not
145+
# feasible.
146+
# For the cases below 10 qubits, both Qrisp and OpenFermion implement almost the exact same
147+
# precision.
148+
149+
if qc_qrisp.num_qubits() < 10:
150+
# Compute exact unitary
151+
hamiltonian_jw_sparse = of.get_sparse_operator(of.jordan_wigner(hamiltonian_ferm_op))
152+
exact_unitary = linalg.expm(-1j * hamiltonian_jw_sparse).todense()
153+
of_precision = unitary_distance(qc_of, exact_unitary)
154+
qrisp_precision = unitary_distance(qc_qrisp, exact_unitary)
155+
else:
156+
of_precision = 0
157+
qrisp_precision = 0
158+
159+
return {
160+
"OpenFermion": {
161+
"RZ count": of_rz_count,
162+
"CX count": of_cx_count,
163+
"RZ depth": of_rz_depth,
164+
"Precision": of_precision
165+
},
166+
"Qrisp": {
167+
"RZ count": qrisp_rz_count,
168+
"CX count": qrisp_cx_count,
169+
"RZ depth": qrisp_rz_depth,
170+
"Precision": qrisp_precision
171+
}
172+
}
173+
174+
# Some gates in the OpenFermion implementation are arbitrary angle but have a
175+
# parameter value, which indicates that they are clifford. We filter out these
176+
# gates since we only want to count arbitrary angle gates that actually need to
177+
# be synthesized (Clifford gates don't).
178+
def filter_clifford(qc):
179+
180+
qc_new = qc.clearcopy()
181+
182+
# Filter out non-clifford operations
183+
for instr in qc.data:
184+
185+
if len(instr.op.params) > 0:
186+
par = instr.op.params[0]
187+
188+
if instr.op.name == "ry":
189+
if abs(par-np.pi/2) < 1E-4:
190+
qc_new.sx_dg(instr.qubits)
191+
qc_new.s(instr.qubits)
192+
qc_new.sx(instr.qubits)
193+
elif instr.op.name == "rx":
194+
if abs(par-np.pi/2) < 1E-4:
195+
qc_new.h(instr.qubits)
196+
qc_new.s(instr.qubits)
197+
qc_new.h(instr.qubits)
198+
elif instr.op.name in ["rz", "u1"]:
199+
if abs(par) < 1E-4:
200+
continue
201+
elif abs(abs(par) - np.pi) < 1E-4:
202+
qc_new.z(instr.qubits)
203+
elif abs(abs(par) - np.pi/2) < 1E-4:
204+
if par < 0:
205+
qc_new.s_dg(instr.qubits)
206+
else:
207+
qc_new.s(instr.qubits)
208+
elif abs(abs(par) - np.pi/4) < 1E-4:
209+
if par < 0:
210+
qc_new.t_dg(instr.qubits)
211+
else:
212+
qc_new.t(instr.qubits)
213+
else:
214+
qc_new.append(instr)
215+
elif instr.op.name == "u3":
216+
217+
for par in instr.op.params:
218+
if par not in [i*np.pi/4 for i in range(8)]:
219+
break
220+
else:
221+
# For the case that all parameters are clifford,
222+
# we don't append any gate, so it is not counted.
223+
continue
224+
225+
qc_new.append(instr)
226+
227+
else:
228+
qc_new.append(instr)
229+
230+
return qc_new
231+
232+
# Compute the benchmark results
233+
234+
molecule_list = [
235+
([("H", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 0.8))], "sto-3g", 1, 0),
236+
([("Li", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 1.6))], "sto-3g", 1, 0),
237+
([("Be", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 1.3))], "sto-3g", 1, 1),
238+
([("B", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 1.2))], "sto-3g", 1, 0),
239+
([("O", (0.0, 0.0, 0.0)), ("H", (0.757, 0.586, 0.0)), ("H", (-0.757, 0.586, 0.0))], "sto-3g", 1, 0),
240+
([("N", (0.0, 0.0, 0.0)),
241+
("H", (0.0, -0.934, -0.365)),
242+
("H", (0.809, 0.467, -0.365)),
243+
("H", (-0.809, 0.467, -0.365))], "sto-3g", 1, 0),
244+
]
245+
246+
# Lists to store results
247+
molecules = []
248+
of_rz_counts = []
249+
of_rz_depths = []
250+
of_precisions = []
251+
qrisp_rz_counts = []
252+
qrisp_rz_depths = []
253+
qrisp_cx_count = []
254+
of_cx_count = []
255+
qrisp_precisions = []
256+
257+
# Run benchmarks for each molecule
258+
for idx, molecule_data in enumerate(molecule_list):
259+
print(molecule_data)
260+
results = benchmark_hamiltonian_simulation(molecule_data)
261+
262+
molecules.append(f"Molecule {idx+1}")
263+
of_rz_counts.append(results["OpenFermion"]["RZ count"])
264+
of_rz_depths.append(results["OpenFermion"]["RZ depth"])
265+
of_precisions.append(results["OpenFermion"]["Precision"])
266+
qrisp_rz_counts.append(results["Qrisp"]["RZ count"])
267+
qrisp_rz_depths.append(results["Qrisp"]["RZ depth"])
268+
qrisp_precisions.append(results["Qrisp"]["Precision"])
269+
qrisp_cx_count.append(results["Qrisp"]["CX count"])
270+
of_cx_count.append(results["OpenFermion"]["CX count"])
271+
272+
273+
# Plot the results
274+
275+
import matplotlib.pyplot as plt
276+
277+
# Set up the plots
278+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(7, 7))
279+
x = np.arange(len(molecules))
280+
width = 0.35
281+
molecules = ["$H_2$", "LiH", "BeH", "BH", "$H_2O$", "$NH_3$"]
282+
ax1.grid()
283+
ax2.grid()
284+
285+
# Plot RZ counts
286+
ax1.bar(x + width/2, qrisp_rz_counts, width, label="Qrisp 0.5", color='#20306f', alpha=1, zorder = 100)
287+
ax1.bar(x - width/2, of_rz_counts, width, label="Google: OpenFermion", color='#FFBA00', alpha=1, zorder = 100)
288+
ax1.set_ylabel('RZ Count')
289+
ax1.set_title('RZ Count Comparison')
290+
ax1.set_xticks(x)
291+
ax1.set_xticklabels(molecules)
292+
ax1.legend()
293+
294+
295+
# Plot RZ depths
296+
ax2.bar(x + width/2, qrisp_rz_depths, width, label="Qrisp 0.5", color='#20306f', alpha=1, zorder = 100)
297+
ax2.bar(x - width/2, of_rz_depths, width, label="Google: OpenFermion", color='#FFBA00', alpha=1, zorder = 100)
298+
ax2.set_ylabel('RZ Depth')
299+
ax2.set_title('RZ Depth Comparison')
300+
ax2.set_xticks(x)
301+
ax2.set_xticklabels(molecules)
302+
ax2.legend()
303+
304+
305+
plt.tight_layout()
306+
plt.show()
307+
plt.savefig("performance_comparison.png", dpi = 300)

src/qrisp/operators/qubit/qubit_term.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ def __str__(self):
413413
return str(expr)
414414

415415
def __repr__(self):
416-
return str(s4elf)
416+
return str(self)
417417

418418
def non_trivial_indices(self):
419419
res = set()

0 commit comments

Comments
 (0)