Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable efficient contraction order simulation #166

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e9b2ad6
🎨 Apply isort/black
Jun 3, 2024
fdde361
🎨 Fix type checks & text length
Jun 10, 2024
f9d34cc
🎨 Add psi argument in Statevec.__init__ method
Jun 10, 2024
bc18da5
🐛 Fix type check
Jun 10, 2024
7fb6085
✨Improve to_statevector by introducing efficient contraction order ca…
Jun 10, 2024
eb7f77d
🎨 Change max_qubit_num as optional
Jun 17, 2024
abc5c87
🎨 Move random_circuit.py to the graphix dir
Jun 17, 2024
a41ac08
🎨 Move StatevectorBackend test location
Jun 17, 2024
f833447
🐛 Fix StatevectorBackend __init__
Jun 17, 2024
fa4965f
✅ Add tests for _initial_state
Jun 17, 2024
d086fbf
🎨 Mod _initial_state func
Jun 17, 2024
9ad4674
📝 Add benchmarking code for eco-sim
Jun 17, 2024
84cf08e
Merge branch 'master' into 92-eco-sim-new
Jun 17, 2024
cb3f2b0
🎨 Apply black & isort
Jun 17, 2024
5c5c1e5
Merge branch 'master' into 92-eco-sim-new
Jul 14, 2024
2814ae0
🔥 Delete tests for old Statevec.init
Jul 14, 2024
a837920
🎨 Apply isort
Jul 14, 2024
264e2bb
🙈 Ignore generated coverage files
Jul 14, 2024
bafcbad
🚚 Move random_circuits.py content into random_objects.py
Jul 14, 2024
0a7f601
🔥 Delete random_circuit.py
Jul 14, 2024
c6e2bb7
🎨 Fix format
Jul 22, 2024
020fc03
🐛 Fix bug in tensornet
Jul 22, 2024
7d4d62f
🎨 Add detailed description on to_statevector method
Jul 22, 2024
fae1f4a
🎨 Add benchmark for contraction skipped results
Jul 22, 2024
2a028ce
🎨 Fix format
Jul 22, 2024
40471f9
🔥 Delete skip option
Jul 25, 2024
06512cc
🔥 Remove not essential simulations
Jul 25, 2024
cf14228
🎨 Reformat
Jul 25, 2024
a1b0e8d
🎨 Apply black
Jul 25, 2024
405194c
🎨 Ignore E402 in benchmarking file
Aug 19, 2024
f572796
Merge branch 'master' into 92-eco-sim-new
Aug 19, 2024
b6f20ca
🎨 Apply ruff
Aug 19, 2024
ea52373
♻️ Refactor benchmark code
Aug 19, 2024
c9f0358
✨ Show error bars in benchmarking results
Aug 19, 2024
175e026
🎨 Fix import setting
Aug 19, 2024
d124700
🎨 Fix TYPE_CHECKING import
Aug 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions benchmarks/efficient_contraction_order_statevec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Efficient contraction order statevector simulation of MBQC patterns
===================================================================

Here we benchmark our efficient contraction order statevector simulator for MBQC,
which uses the TensorNetwork backend.

The methods and modules we use are the followings:
1. :meth:`graphix.pattern.Pattern.simulate_pattern`
Pattern simulation with Statevector backend.
:meth:`graphix.pattern.Pattern.minimize_space` locally minimizes the space of the pattern.
2. :mod:`graphix.sim.tensornet`
Pattern simulation with TensorNetwork backend.
This enables the efficient contraction order simulation of the pattern.
Here we use the `cotengra` optimizer for the contraction order optimization.
"""

# %%
# Firstly, let us import relevant modules:

from copy import deepcopy
from time import perf_counter

import cotengra as ctg
import quimb as qu
from numpy.random import PCG64, Generator

from graphix.random_circuit import get_rand_circuit

# %%
# Next, set global seed and number of thread workers
GLOBAL_SEED = 2
qu.core._NUM_THREAD_WORKERS = 1

# %%
# We then run simulations.
# Let's benchmark the simulation time of the statevector simulator and the efficient contraction order simulator.

n_qubit_list = list(range(2, 31))

sv_sim = []
eco_sim = []
eco_sim_wo_ctg = []
max_space_ls = []

for n_qubit in n_qubit_list:
rng = Generator(PCG64(GLOBAL_SEED))
circuit = get_rand_circuit(n_qubit, n_qubit, rng)
pattern = circuit.transpile().pattern
pattern.standardize()
pat_original = deepcopy(pattern)
start = perf_counter()
pat_original.minimize_space()
max_sp = pat_original.max_space()
max_space_ls.append(max_sp)
sv = pat_original.simulate_pattern(max_qubit_num=max_sp)
end = perf_counter()
sv_sim.append(end - start)
del sv

# efficient contraction order simulation (eco-sim): tn
tn = pattern.simulate_pattern("tensornetwork")
output_inds = [tn._dangling[str(index)] for index in tn.default_output_nodes]

start = perf_counter()
tn_sv = tn.to_statevector(
backend="numpy",
skip=False,
optimize=ctg.HyperOptimizer(minimize="combo", max_time=600, progbar=True),
)
end = perf_counter()
eco_sim.append(end - start)
del tn_sv

# eco-sim: tn without cotengra optimizer
start = perf_counter()
tn_sv = tn.to_statevector(
backend="numpy",
skip=False,
optimize=None,
)
end = perf_counter()
eco_sim_wo_ctg.append(end - start)
del tn_sv, tn

# %%
# Finally, we save the results to a text file.
with open("sqrqcresults.txt", "w") as f:
f.write("n_qubit, sv_sim, eco_sim, eco_sim_wo_ctg, max_space_ls\n")
for i in range(len(n_qubit_list)):
f.write(f"{n_qubit_list[i]}, {sv_sim[i]}, {eco_sim[i]}, {eco_sim_wo_ctg[i]}, {max_space_ls[i]}\n")

# %%
# We can now plot the simulation time results.
import matplotlib.pyplot as plt
import numpy as np

data = np.loadtxt("sqrqcresults.txt", delimiter=",", skiprows=1)
n_qubits = data[:, 0].astype(int)
sv_sim = data[:, 1].astype(float)
eco_sim = data[:, 2].astype(float)
eco_sim_wo_ctg = data[:, 3].astype(float)
max_sp = data[:, 4].astype(int)

fig, ax1 = plt.subplots(figsize=(8, 5))
color = "tab:red"
ax1.set_xlabel("Original Circuit Size [qubit]")
ax1.set_ylabel("Simulation time [sec]")
ax1.set_yscale("log")
ax1.scatter(n_qubits, sv_sim, marker="x", label="MBQC Statevector (minimizing sp)")
ax1.scatter(n_qubits, eco_sim, marker="x", label="MBQC TN base (with cotengra)")
ax1.scatter(n_qubits, eco_sim_wo_ctg, marker="x", label="MBQC TN base (without cotengra)")
ax1.tick_params(axis="y")
ax1.legend(loc="upper left")
ax1.set_title("Simulation time (Square RQC)")
plt.grid(True, which="Major")

ax2 = ax1.twinx()
color = "tab:blue"
ax2.set_ylabel("Max Space [qubit]")
ax2.plot(n_qubits, max_sp, color=color, linestyle="--", label="Max Space")
ax2.tick_params(axis="y")
ax2.legend(loc="lower right")

plt.rcParams["svg.fonttype"] = "none"
plt.savefig("simulation_time_wo_p.png")
plt.close()
8 changes: 7 additions & 1 deletion examples/MBQCvqe.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
import networkx as nx
import numpy as np
from scipy.optimize import minimize

from graphix import Circuit
from graphix.simulator import PatternSimulator


# %%
# Define the Hamiltonian for the VQE problem (Example: H = Z0Z1 + X0 + X1)
def create_hamiltonian():
Expand All @@ -33,6 +35,7 @@ def create_hamiltonian():
H = np.kron(Z, Z) + np.kron(X, np.eye(2)) + np.kron(np.eye(2), X)
return H


# %%
# Function to build the VQE circuit
def build_vqe_circuit(n_qubits, params):
Expand All @@ -45,6 +48,7 @@ def build_vqe_circuit(n_qubits, params):
circuit.cnot(i, i + 1)
return circuit


# %%
class MBQCVQE:
def __init__(self, n_qubits, hamiltonian):
Expand Down Expand Up @@ -85,6 +89,7 @@ def compute_energy(self, params):
energy = tn.expectation_value(self.hamiltonian, qubit_indices=range(self.n_qubits))
return energy


# %%
# Set parameters for VQE
n_qubits = 2
Expand All @@ -99,13 +104,14 @@ def compute_energy(self, params):
def cost_function(params):
return mbqc_vqe.compute_energy(params)


# %%
# Random initial parameters
initial_params = np.random.rand(n_qubits * 3)

# %%
# Perform the optimization using COBYLA
result = minimize(cost_function, initial_params, method='COBYLA', options={'maxiter': 100})
result = minimize(cost_function, initial_params, method="COBYLA", options={"maxiter": 100})

print(f"Optimized parameters: {result.x}")
print(f"Optimized energy: {result.fun}")
Expand Down
15 changes: 15 additions & 0 deletions tests/random_circuit.py → graphix/random_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ def get_rand_circuit(
use_rzz: bool = False,
use_ccx: bool = False,
) -> Circuit:
"""Generate a random circuit.

Parameters
----------
nqubits : int
Number of qubits in the circuit. Must be at least 2.
depth : int
Number of layers in the circuit.
rng : Generator
Random number generator.
use_rzz : bool, optional
Whether to include RZZ gates, by default False.
use_ccx : bool, optional
Whether to include CCX gates, by default False.
"""
nabe98 marked this conversation as resolved.
Show resolved Hide resolved
circuit = Circuit(nqubits)
gate_choice = (
functools.partial(circuit.ry, angle=np.pi / 4),
Expand Down
78 changes: 61 additions & 17 deletions graphix/sim/statevec.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
from copy import deepcopy

import numpy as np
from numpy.typing import NDArray

import graphix.sim.base_backend
from graphix.clifford import CLIFFORD, CLIFFORD_CONJ, CLIFFORD_MUL
from graphix.clifford import CLIFFORD, CLIFFORD_CONJ
from graphix.ops import Ops


class StatevectorBackend(graphix.sim.base_backend.Backend):
"""MBQC simulator with statevector method."""

def __init__(self, pattern, max_qubit_num=20, pr_calc=True):
def __init__(self, pattern, pr_calc=True, max_qubit_num=None):
"""
Parameters
-----------
pattern : :class:`graphix.pattern.Pattern` object
MBQC pattern to be simulated.
backend : str, 'statevector'
optional argument for simulation.
max_qubit_num : int
optional argument specifying the maximum number of qubits
to be stored in the statevector at a time.
pr_calc : bool
whether or not to compute the probability distribution before choosing the measurement result.
if False, measurements yield results 0/1 with 50% probabilities each.
max_qubit_num : int, optional
optional argument specifying the maximum number of qubits
to be stored in the statevector at a time.
"""
# check that pattern has output nodes configured
# assert len(pattern.output_nodes) > 0
Expand All @@ -34,9 +35,7 @@
self.Nqubit = 0
self.to_trace = []
self.to_trace_loc = []
self.max_qubit_num = max_qubit_num
if pattern.max_space() > max_qubit_num:
raise ValueError("Pattern.max_space is larger than max_qubit_num. Increase max_qubit_num and try again")
self.max_qubit_num = _validate_max_qubit_num(max_qubit_num, pattern.max_space())
super().__init__(pr_calc)

def qubit_dim(self):
Expand Down Expand Up @@ -182,21 +181,19 @@
class Statevec:
"""Simple statevector simulator"""

def __init__(self, nqubit=1, plus_states=True):
def __init__(self, nqubit=1, psi=None, plus_states=True):
"""Initialize statevector

Parameters
----------
nqubit : int, optional:
number of qubits. Defaults to 1.
psi : numpy.ndarray, optional
statevector. Defaults to None.
plus_states : bool, optional
whether or not to start all qubits in + state or 0 state. Defaults to +
"""
if plus_states:
self.psi = np.ones((2,) * nqubit) / 2 ** (nqubit / 2)
else:
self.psi = np.zeros((2,) * nqubit)
self.psi[(0,) * nqubit] = 1
self.psi = _initial_state(nqubit, psi, plus_states)

def __repr__(self):
return f"Statevec, data={self.psi}, shape={self.dims()}"
Expand Down Expand Up @@ -225,8 +222,13 @@
target qubits' indices
"""
op_dim = int(np.log2(len(op)))
# TODO shape = (2,)* 2 * op_dim
shape = [2 for _ in range(2 * op_dim)]
shape = (
[
2,
]
* 2
* op_dim
)
op_tensor = op.reshape(shape)
self.psi = np.tensordot(
op_tensor,
Expand Down Expand Up @@ -409,6 +411,48 @@
return np.dot(st2.psi.flatten().conjugate(), st1.psi.flatten())


def _get_statevec_norm(psi):
def _get_statevec_norm(psi) -> float:
"""returns norm of the state"""
return np.sqrt(np.sum(psi.flatten().conj() * psi.flatten()))


def _initial_state(nqubit: int = 1, psi: NDArray = None, plus_states: bool = True) -> NDArray:
"""Create initial state

Parameters
----------
nqubit : int, optional:
number of qubits. Defaults to 1.
psi : numpy.ndarray, optional
statevector. Defaults to None.
plus_states : bool, optional
whether or not to start all qubits in + state or 0 state. Defaults to +

Returns
-------
numpy.ndarray
statevector
"""
if isinstance(psi, np.ndarray):
return psi
if not isinstance(nqubit, int):
raise TypeError("nqubit must be an integer")
if nqubit < 0:
raise ValueError("nqubit must be a non-negative integer")
if plus_states:
return np.ones((2,) * nqubit) / 2 ** (nqubit / 2)
psi = np.zeros((2,) * nqubit)
psi[(0,) * nqubit] = 1
return psi

Check warning on line 446 in graphix/sim/statevec.py

View check run for this annotation

Codecov / codecov/patch

graphix/sim/statevec.py#L444-L446

Added lines #L444 - L446 were not covered by tests


def _validate_max_qubit_num(max_qubit_num: int | None, max_space: int) -> int | None:
if max_qubit_num is None:
return
if not isinstance(max_qubit_num, int):
raise ValueError("max_qubit_num must be an integer")
if max_qubit_num < 1:
raise ValueError("max_qubit_num must be a positive integer")
if max_qubit_num < max_space:
raise ValueError("Pattern.max_space is larger than max_qubit_num. Increase max_qubit_num and try again")
return max_qubit_num
Loading
Loading