|
| 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 |
0 commit comments