Skip to content

Commit 0a5d3ba

Browse files
Update simulation to use layerwise DMRG-like compression
Revised the MPS simulation to apply gates layer-by-layer without immediate truncation, followed by a compression sweep (DMRG-style) to reduce the bond dimension. This addresses the feedback to use a layerwise DMRG logic instead of gate-wise TEBD. Co-authored-by: refraction-ray <35157286+refraction-ray@users.noreply.github.com>
1 parent 26fd626 commit 0a5d3ba

File tree

2 files changed

+103
-54
lines changed

2 files changed

+103
-54
lines changed

examples/reproduce_papers/2022_dmrg_circuit_simulation/main.py

Lines changed: 103 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
It simulates a Sycamore-like random quantum circuit using both exact state vector simulation
99
and an MPS-based simulator (DMRG-like algorithm) with varying bond dimensions.
1010
The script plots the infidelity (1 - Fidelity) as a function of the bond dimension.
11+
12+
Implementation Note:
13+
This script implements a "layerwise DMRG" logic. Instead of truncating the MPS
14+
after every 2-qubit gate (TEBD approach), we apply a full layer of gates to the
15+
MPS (allowing the bond dimension to grow) and then perform a compression sweep
16+
to truncate the MPS back to the target bond dimension. This variational-like
17+
compression is closer to the DMRG-style algorithm described in the paper.
1118
"""
1219

1320
import time
@@ -24,91 +31,141 @@
2431
K = tc.set_backend("numpy")
2532

2633

27-
def generate_sycamore_like_circuit(rows, cols, depth, seed=42):
34+
def generate_sycamore_like_circuit_structure(rows, cols, depth, seed=42):
2835
"""
2936
Generates a random quantum circuit with a structure similar to Sycamore circuits.
30-
Since real Sycamore gates are specific, we use a simplified model:
31-
- 2D Grid connectivity
32-
- Layers of random single-qubit gates
33-
- Layers of two-qubit gates (CZ or similar) in a specific pattern
37+
Returns the exact circuit (for validation) and a list of layers, where each layer
38+
is a list of gate dictionaries.
3439
"""
3540
np.random.seed(seed)
3641
n_qubits = rows * cols
3742
c = tc.Circuit(n_qubits)
43+
layers = []
3844

3945
def q(r, col):
4046
return r * cols + col
4147

4248
for d in range(depth):
49+
layer_gates = []
50+
4351
# Single qubit gates
4452
for i in range(n_qubits):
45-
# Random single qubit gate (e.g., Rx, Ry, Rz)
46-
# Simplification: Use a general random unitary or specific rotations
47-
# Sycamore uses sqrt(X), sqrt(Y), sqrt(W)
48-
# We'll use random rotations for generic RQC behavior
4953
theta = np.random.uniform(0, 2 * np.pi)
5054
phi = np.random.uniform(0, 2 * np.pi)
5155
lam = np.random.uniform(0, 2 * np.pi)
56+
5257
c.rz(i, theta=phi)
58+
layer_gates.append(
59+
{"gatef": tc.gates.rz, "index": (i,), "parameters": {"theta": phi}}
60+
)
61+
5362
c.ry(i, theta=theta)
63+
layer_gates.append(
64+
{"gatef": tc.gates.ry, "index": (i,), "parameters": {"theta": theta}}
65+
)
66+
5467
c.rz(i, theta=lam)
68+
layer_gates.append(
69+
{"gatef": tc.gates.rz, "index": (i,), "parameters": {"theta": lam}}
70+
)
5571

56-
# Two-qubit gates
57-
# Sycamore uses a specific pattern (ABCD...)
58-
# We will use a simple alternating pattern
59-
# Layer A: horizontal even
60-
# Layer B: horizontal odd
61-
# Layer C: vertical even
62-
# Layer D: vertical odd
63-
# We cycle through these patterns based on depth
72+
layers.append(layer_gates)
73+
layer_gates = []
6474

75+
# Two-qubit gates
6576
layer_type = d % 4
6677

6778
if layer_type == 0: # Horizontal (col, col+1) for even cols
6879
for r in range(rows):
6980
for col in range(0, cols - 1, 2):
7081
c.cz(q(r, col), q(r, col + 1))
82+
layer_gates.append(
83+
{
84+
"gatef": tc.gates.cz,
85+
"index": (q(r, col), q(r, col + 1)),
86+
"parameters": {},
87+
}
88+
)
7189
elif layer_type == 1: # Horizontal (col, col+1) for odd cols
7290
for r in range(rows):
7391
for col in range(1, cols - 1, 2):
7492
c.cz(q(r, col), q(r, col + 1))
93+
layer_gates.append(
94+
{
95+
"gatef": tc.gates.cz,
96+
"index": (q(r, col), q(r, col + 1)),
97+
"parameters": {},
98+
}
99+
)
75100
elif layer_type == 2: # Vertical (row, row+1) for even rows
76101
for col in range(cols):
77102
for r in range(0, rows - 1, 2):
78103
c.cz(q(r, col), q(r + 1, col))
104+
layer_gates.append(
105+
{
106+
"gatef": tc.gates.cz,
107+
"index": (q(r, col), q(r + 1, col)),
108+
"parameters": {},
109+
}
110+
)
79111
elif layer_type == 3: # Vertical (row, row+1) for odd rows
80112
for col in range(cols):
81113
for r in range(1, rows - 1, 2):
82114
c.cz(q(r, col), q(r + 1, col))
115+
layer_gates.append(
116+
{
117+
"gatef": tc.gates.cz,
118+
"index": (q(r, col), q(r + 1, col)),
119+
"parameters": {},
120+
}
121+
)
122+
123+
if layer_gates:
124+
layers.append(layer_gates)
83125

84-
return c
126+
return c, layers
85127

86128

87-
def run_mps_simulation(c, bond_dim):
129+
def run_mps_simulation_layerwise(n_qubits, layers, bond_dim):
88130
"""
89-
Runs the simulation using MPSCircuit with a maximum bond dimension.
131+
Runs the simulation using MPSCircuit with layerwise DMRG-like compression.
90132
"""
91-
# Create MPSCircuit from the circuit operations
92-
# We need to re-apply the gates to an MPSCircuit
93-
# Or we can just build the MPSCircuit directly in the generation function.
94-
# But to ensure exactly the same circuit, we can iterate over the qir of the tc.Circuit
95-
n = c._nqubits
96-
mps = tc.MPSCircuit(n)
97-
98-
# Set truncation rules
99-
# We use `max_singular_values` to control the bond dimension (chi)
100-
mps.set_split_rules({"max_singular_values": bond_dim})
101-
102-
for gate in c._qir:
103-
index = gate["index"]
104-
params = gate.get("parameters", {})
105-
106-
# Construct the gate on MPS
107-
# tc.Circuit stores 'gatef' which returns a Gate object when called with params
108-
g_obj = gate["gatef"](**params)
109-
110-
# Apply to MPS
111-
mps.apply(g_obj, *index)
133+
mps = tc.MPSCircuit(n_qubits)
134+
135+
# We want to manually control truncation
136+
# First, we set no truncation for gate application
137+
mps.set_split_rules({}) # Infinite bond dimension during application
138+
139+
for layer in layers:
140+
# 1. Apply all gates in the layer
141+
# This will increase the bond dimension significantly
142+
for gate in layer:
143+
index = gate["index"]
144+
params = gate.get("parameters", {})
145+
g_obj = gate["gatef"](**params)
146+
mps.apply(g_obj, *index)
147+
148+
# 2. Perform compression sweep (DMRG-style logic)
149+
# We sweep from left to right (and/or right to left) and truncate
150+
# the bonds to the target dimension `bond_dim`.
151+
152+
# We use standard SVD-based compression (sweeping) which is optimal for minimizing 2-norm error
153+
# This is effectively what DMRG does when optimizing overlap for a fixed bond dimension.
154+
155+
# Sweep Left -> Right
156+
# First ensure we are at the beginning
157+
mps.position(0)
158+
for i in range(n_qubits - 1):
159+
mps.reduce_dimension(
160+
i, center_left=False, split={"max_singular_values": bond_dim}
161+
)
162+
163+
# Sweep Right -> Left (to ensure canonicalization and further optimization)
164+
# We are at n_qubits - 1 now.
165+
for i in range(n_qubits - 2, -1, -1):
166+
mps.reduce_dimension(
167+
i, center_left=True, split={"max_singular_values": bond_dim}
168+
)
112169

113170
return mps
114171

@@ -118,17 +175,9 @@ def calculate_fidelity(exact_c, mps_c):
118175
Calculates the fidelity between the exact state and the MPS state.
119176
F = |<psi_exact | psi_mps>|^2
120177
"""
121-
# Get exact state vector
122178
psi_exact = exact_c.state()
123-
124-
# Get MPS state vector (converted to full tensor)
125-
# Note: For large N, this will OOM. We should keep N small (e.g. <= 20).
126179
psi_mps = mps_c.wavefunction()
127180

128-
# Compute overlap
129-
# exact state is (2^N,) or (1, 2^N)
130-
# mps state is (1, 2^N) usually
131-
132181
psi_exact = K.reshape(psi_exact, (-1,))
133182
psi_mps = K.reshape(psi_mps, (-1,))
134183

@@ -143,12 +192,12 @@ def main():
143192
COLS = 4 # 12 qubits
144193
DEPTH = 8
145194
# Bond dimensions to sweep
146-
# For 12 qubits, full bond dimension is 2^6=64.
147-
# So we should see perfect fidelity at 64, and error below it.
148195
BOND_DIMS = [2, 4, 8, 16, 32, 64]
149196

150197
logger.info(f"Generating random circuit: {ROWS}x{COLS} grid, Depth {DEPTH}")
151-
circuit = generate_sycamore_like_circuit(ROWS, COLS, DEPTH, seed=42)
198+
circuit, layers = generate_sycamore_like_circuit_structure(
199+
ROWS, COLS, DEPTH, seed=42
200+
)
152201

153202
# 1. Exact Simulation
154203
logger.info("Running Exact Simulation...")
@@ -160,10 +209,10 @@ def main():
160209
infidelities = []
161210

162211
# 2. MPS Simulation with varying bond dimension
163-
logger.info("Running MPS Simulations...")
212+
logger.info("Running MPS Simulations (Layerwise DMRG)...")
164213
for chi in BOND_DIMS:
165214
start_time = time.time()
166-
mps = run_mps_simulation(circuit, chi)
215+
mps = run_mps_simulation_layerwise(circuit._nqubits, layers, chi)
167216

168217
# Calculate Fidelity
169218
fid = calculate_fidelity(circuit, mps)
-1013 Bytes
Loading

0 commit comments

Comments
 (0)