Skip to content

Commit 8bbbbdc

Browse files
Added DFL entropy code (#444)
* Added DFL entropy code
1 parent 755949b commit 8bbbbdc

File tree

3 files changed

+337
-0
lines changed

3 files changed

+337
-0
lines changed

recirq/dfl/dfl_entropy.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# Copyright 2025 Google
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Run experiments for measuring second Renyi entropy via randomized measurements
16+
in 1D DFL circuits.
17+
"""
18+
19+
20+
from collections.abc import Sequence
21+
from pathlib import Path
22+
import pickle
23+
24+
import cirq
25+
import numpy as np
26+
import numpy.typing as npt
27+
28+
from recirq.dfl.dfl_enums import InitialState
29+
30+
31+
def _layer_interaction(
32+
grid: Sequence[cirq.GridQubit],
33+
dt: float,
34+
) -> cirq.Circuit:
35+
"""Implements the ZZZ term of the DFL Hamiltonian
36+
Each ZZZ term acts on matter-gauge-matter qubits.
37+
The resulting circuit for each term looks like:
38+
0: ───────@────────────────────────@───────
39+
│ │
40+
1: ───H───@───@───Rx(2 * dt)───@───@───H───
41+
│ │
42+
2: ───────────@────────────────@───────────
43+
44+
Args:
45+
grid: The 1D sequence of qubits used in the experiment.
46+
dt: The time step size for the Trotterization.
47+
48+
Returns:
49+
cirq.Circuit for the Trotter evolution of the ZZZ term.
50+
"""
51+
52+
moment_1 = []
53+
moment_2 = []
54+
moment_3 = []
55+
moment_h = []
56+
for i in range(0, len(grid) // 2):
57+
q1 = grid[(2 * i)]
58+
q2 = grid[2 * i + 1]
59+
q3 = grid[(2 * i + 2) % len(grid)]
60+
moment_1.append(cirq.CZ(q1, q2))
61+
moment_2.append(cirq.CZ(q3, q2))
62+
moment_h.append(cirq.H(q2))
63+
moment_3.append(cirq.rx(2 * dt).on(q2))
64+
65+
return cirq.Circuit.from_moments(
66+
cirq.Moment(moment_h),
67+
cirq.Moment(moment_1),
68+
cirq.Moment(moment_2),
69+
cirq.Moment(moment_3),
70+
cirq.Moment(moment_2),
71+
cirq.Moment(moment_1),
72+
cirq.Moment(moment_h),
73+
)
74+
75+
76+
def _layer_matter_gauge_x(
77+
grid: Sequence[cirq.GridQubit], dt: float, h: float, mu: float
78+
) -> cirq.Circuit:
79+
"""Implements the X rotation for the matter and gauge qubits.
80+
The resulting circuit should look like:
81+
0: ──Rx(2*mu*dt)──
82+
83+
1: ──Rx(2*h*dt)────
84+
85+
2: ──Rx(2*mu*dt)───
86+
87+
Args:
88+
grid: The 1D sequence of qubits used in the experiment.
89+
dt: The time step size for the Trotterization.
90+
h: The gauge field strength coefficient.
91+
mu: The matter field strength coefficient.
92+
93+
Returns:
94+
cirq.Circuit for the Trotter evolution of the matter and gauge fields.
95+
"""
96+
97+
moment = []
98+
for i in range(len(grid)):
99+
if i % 2 == 0:
100+
moment.append(cirq.rx(2 * mu * dt).on(grid[i]))
101+
else:
102+
moment.append(cirq.rx(2 * h * dt).on(grid[i]))
103+
return cirq.Circuit.from_moments(cirq.Moment(moment))
104+
105+
106+
def layer_floquet(
107+
grid: Sequence[cirq.GridQubit], dt: float, h: float, mu: float
108+
) -> cirq.Circuit:
109+
"""Constructs a Trotter circuit for 1D Disorder-Free Localization (DFL) simulation.
110+
111+
Args:
112+
grid: The 1D sequence of qubits used in the experiment.
113+
dt: Trotter step size.
114+
h: The coefficient on the gauge X term.
115+
mu: The coefficient on the matter sigma_x term.
116+
117+
Returns:
118+
The complete cirq.Circuit for the Trotter evolution.
119+
120+
"""
121+
122+
return _layer_interaction(grid, dt) + _layer_matter_gauge_x(grid, dt, h, mu)
123+
124+
125+
def initial_state_for_entropy(
126+
grid: Sequence[cirq.GridQubit], matter_config: InitialState
127+
) -> cirq.Circuit:
128+
"""Circuit for three types of initial states:
129+
single_sector: |-> |+> |-> |+>...
130+
superposition: |0> |+> |0> |+>...
131+
disordered: |+/-> |-> |+/->... with randomly chosen |+> or |-> on matter sites
132+
"""
133+
134+
moment = []
135+
for i in range(len(grid)):
136+
if i % 2 == 0:
137+
if matter_config == InitialState.SINGLE_SECTOR:
138+
moment.append(cirq.X(grid[i]))
139+
moment.append(cirq.H(grid[i]))
140+
141+
elif matter_config == InitialState.DISORDERED:
142+
r = np.random.choice([0, 1])
143+
if r:
144+
moment.append(cirq.X(grid[i]))
145+
moment.append(cirq.H(grid[i]))
146+
147+
else:
148+
moment.append(cirq.I(grid[i]))
149+
else:
150+
if matter_config == InitialState.DISORDERED:
151+
moment.append(cirq.X(grid[i]))
152+
moment.append(cirq.H(grid[i]))
153+
154+
return cirq.Circuit(moment)
155+
156+
157+
def get_1d_dfl_entropy_experiment_circuits(
158+
grid: Sequence[cirq.GridQubit],
159+
initial_state: InitialState,
160+
ncycles: int,
161+
dt: float,
162+
h: float,
163+
mu: float,
164+
n_basis: int = 100,
165+
) -> Sequence[cirq.Circuit]:
166+
"""Generate the circuit instances for the entropy experiment
167+
168+
Args:
169+
grid: The qubits to use for the experiment.
170+
initial_state: Which initial state, see `InitialState` enum.
171+
ncycles: The number of Trotter steps (can be 0).
172+
dt: Trotter step size.
173+
h: The coefficient on the gauge X term.
174+
mu: The coefficient on the matter sigma_x term.
175+
n_basis: The number of random measurement bases to use.
176+
177+
Returns:
178+
A list of the circuit instances.
179+
"""
180+
181+
initial_circuit = initial_state_for_entropy(grid, initial_state)
182+
circuits = []
183+
circ = initial_circuit + layer_floquet(grid, dt, h, mu) * ncycles
184+
185+
for _ in range(n_basis):
186+
circ_randomized = cirq.transformers.RandomizedMeasurements()(
187+
circ, unitary_ensemble="Clifford"
188+
)
189+
190+
circuits.append(circ_randomized)
191+
192+
return circuits
193+
194+
195+
def run_1d_dfl_entropy_experiment(
196+
grid: Sequence[cirq.GridQubit],
197+
initial_states: Sequence[InitialState],
198+
save_dir: Path,
199+
n_cycles: Sequence[int] | npt.NDArray,
200+
dt: float,
201+
h: float,
202+
mu: float,
203+
n_basis: int = 100,
204+
n_shots: int = 1000,
205+
sampler: cirq.Sampler = cirq.Simulator(),
206+
gauge_compile: bool = True,
207+
dynamical_decouple: bool = True,
208+
) -> None:
209+
"""Run the 1D DFL experiment (Fig 4 of the paper).
210+
The paper is available at: https://arxiv.org/abs/2410.06557
211+
Saves the measurement bitstrings to save_dir.
212+
213+
Data is saved in the following directory structure:
214+
save_dir/dt{dt}/h{h}_mu{mu}/{initial_state}/cycle{n_cycle}.pickle
215+
216+
Attrs:
217+
grid: The qubits to use for the experiment.
218+
initial_states: The list of InitialState to use.
219+
save_dir: The directory in which to save the results.
220+
n_cycles: The list of number of Trotter steps to use.
221+
dt: The Trotter step size.
222+
h: The coefficient on the gauge X term.
223+
mu: The coefficient on the matter sigma_x term.
224+
n_basis: The number of random measurement bases to use.
225+
n_shots: The number of measurement shots to use.
226+
sampler: The cirq sampler to use.
227+
gauge_compile: Whether to apply gauge compiling.
228+
dynamical_decouple: Whether to apply dynamical decoupling.
229+
230+
Returns:
231+
None
232+
"""
233+
234+
for initial_state in initial_states:
235+
dir_path = (
236+
save_dir / f"dt{dt:.2f}" / f"h{h:.2f}_mu{mu:.2f}" / initial_state.value
237+
)
238+
dir_path.mkdir(parents=True, exist_ok=True)
239+
240+
for n_cycle in n_cycles:
241+
print("Initial state:", initial_state.value, "Cycle:", n_cycle)
242+
fname = dir_path / "cycle{}.pickle".format(n_cycle)
243+
circuits = get_1d_dfl_entropy_experiment_circuits(
244+
grid,
245+
initial_state=initial_state,
246+
ncycles=n_cycle,
247+
dt=dt,
248+
h=h,
249+
mu=mu,
250+
n_basis=n_basis,
251+
)
252+
253+
circuits_modified = []
254+
for i in range(len(circuits)):
255+
circ_i = circuits[i]
256+
257+
if gauge_compile:
258+
circ_i = cirq.transformers.gauge_compiling.CZGaugeTransformer(circ_i)
259+
if dynamical_decouple:
260+
circ_i = cirq.add_dynamical_decoupling(circ_i)
261+
circuits_modified.append(circ_i)
262+
263+
results = sampler.run_batch(circuits, repetitions=n_shots)
264+
bitstrings = []
265+
for j in range(n_basis):
266+
bitstrings.append(results[j][0].measurements["m"])
267+
268+
with open(fname, "wb") as myfile:
269+
pickle.dump(bitstrings, myfile)
270+
return None

recirq/dfl/dfl_entropy_test.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2025 Google
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import cirq
16+
17+
from recirq.dfl.dfl_entropy import layer_floquet
18+
19+
20+
def test_layer_floquet_basic():
21+
# Create a simple grid of 4 qubits
22+
grid = [cirq.GridQubit(0, i) for i in range(4)]
23+
dt = 0.1
24+
h = 0.5
25+
mu = 0.3
26+
27+
circuit = layer_floquet(grid, dt, h, mu)
28+
# Check that the output is a cirq.Circuit
29+
assert isinstance(circuit, cirq.Circuit)
30+
# Check that the circuit has operations on all qubits
31+
qubits_in_circuit = set(q for op in circuit.all_operations() for q in op.qubits)
32+
assert set(grid) == qubits_in_circuit
33+
34+
35+
def test_layer_floquet_three_qubits_structure():
36+
# Test for 3 qubits, manually construct the expected circuit and compare
37+
q1, q2, q3 = cirq.LineQubit.range(3)
38+
grid = [q1, q2, q3]
39+
dt = 0.2
40+
h = 0.4
41+
mu = 0.6
42+
43+
# Manually construct the expected circuit based on layer_interaction and RX layer
44+
expected_circuit = cirq.Circuit(
45+
cirq.H(q2),
46+
cirq.CZ(q1, q2),
47+
cirq.CZ(q3, q2),
48+
cirq.rx(2 * dt).on(q2),
49+
cirq.CZ(q3, q2),
50+
cirq.CZ(q1, q2),
51+
cirq.H(q2),
52+
)
53+
54+
expected_circuit += cirq.Circuit.from_moments(
55+
cirq.Moment(
56+
[
57+
cirq.rx(2 * mu * dt).on(q1),
58+
cirq.rx(2 * h * dt).on(q2),
59+
cirq.rx(2 * mu * dt).on(q3),
60+
]
61+
)
62+
)
63+
64+
actual_circuit = layer_floquet(grid, dt, h, mu)
65+
66+
assert actual_circuit == expected_circuit

recirq/dfl/dfl_enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class InitialState(enum.Enum):
3636
"""
3737
SINGLE_SECTOR = 'single_sector'
3838
SUPERPOSITION = 'superposition'
39+
DISORDERED = 'disordered'
3940

4041

4142
class Basis(enum.Enum):

0 commit comments

Comments
 (0)