88It simulates a Sycamore-like random quantum circuit using both exact state vector simulation
99and an MPS-based simulator (DMRG-like algorithm) with varying bond dimensions.
1010The 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
1320import time
2431K = 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 )
0 commit comments