Skip to content
270 changes: 270 additions & 0 deletions recirq/dfl/dfl_entropy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
# Copyright 2025 Google
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Run experiments for measuring second Renyi entropy via randomized measurements
in 1D DFL circuits.
"""


from collections.abc import Sequence
from pathlib import Path
import pickle

import cirq
import numpy as np
import numpy.typing as npt

from recirq.dfl.dfl_enums import InitialState


def _layer_interaction(
grid: Sequence[cirq.GridQubit],
dt: float,
) -> cirq.Circuit:
"""Implements the ZZZ term of the DFL Hamiltonian
Each ZZZ term acts on matter-gauge-matter qubits.
The resulting circuit for each term looks like:
0: ───────@────────────────────────@───────
│ │
1: ───H───@───@───Rx(2 * dt)───@───@───H───
│ │
2: ───────────@────────────────@───────────

Args:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe explain what this circuit is supposed to look like?
Also put a blank line before "Args:"

grid: The 1D sequence of qubits used in the experiment.
dt: The time step size for the Trotterization.

Returns:
cirq.Circuit for the Trotter evolution of the ZZZ term.
"""

moment_1 = []
moment_2 = []
moment_3 = []
moment_h = []
for i in range(0, len(grid) // 2):
q1 = grid[(2 * i)]
q2 = grid[2 * i + 1]
q3 = grid[(2 * i + 2) % len(grid)]
moment_1.append(cirq.CZ(q1, q2))
moment_2.append(cirq.CZ(q3, q2))
moment_h.append(cirq.H(q2))
moment_3.append(cirq.rx(2 * dt).on(q2))

return cirq.Circuit.from_moments(
cirq.Moment(moment_h),
cirq.Moment(moment_1),
cirq.Moment(moment_2),
cirq.Moment(moment_3),
cirq.Moment(moment_2),
cirq.Moment(moment_1),
cirq.Moment(moment_h),
)


def _layer_matter_gauge_x(
grid: Sequence[cirq.GridQubit], dt: float, h: float, mu: float
) -> cirq.Circuit:
"""Implements the X rotation for the matter and gauge qubits.
The resulting circuit should look like:
0: ──Rx(2*mu*dt)──

1: ──Rx(2*h*dt)────

2: ──Rx(2*mu*dt)───

Args:
grid: The 1D sequence of qubits used in the experiment.
dt: The time step size for the Trotterization.
h: The gauge field strength coefficient.
mu: The matter field strength coefficient.

Returns:
cirq.Circuit for the Trotter evolution of the matter and gauge fields.
"""

moment = []
for i in range(len(grid)):
if i % 2 == 0:
moment.append(cirq.rx(2 * mu * dt).on(grid[i]))
else:
moment.append(cirq.rx(2 * h * dt).on(grid[i]))
return cirq.Circuit.from_moments(cirq.Moment(moment))


def layer_floquet(
grid: Sequence[cirq.GridQubit], dt: float, h: float, mu: float
) -> cirq.Circuit:
"""Constructs a Trotter circuit for 1D Disorder-Free Localization (DFL) simulation.

Args:
grid: The 1D sequence of qubits used in the experiment.
dt: Trotter step size.
h: The coefficient on the gauge X term.
mu: The coefficient on the matter sigma_x term.

Returns:
The complete cirq.Circuit for the Trotter evolution.

"""

return _layer_interaction(grid, dt) + _layer_matter_gauge_x(grid, dt, h, mu)


def initial_state_for_entropy(
grid: Sequence[cirq.GridQubit], matter_config: InitialState
) -> cirq.Circuit:
"""Circuit for three types of initial states:
single_sector: |-> |+> |-> |+>...
superposition: |0> |+> |0> |+>...
disordered: |+/-> |-> |+/->... with randomly chosen |+> or |-> on matter sites
"""

moment = []
for i in range(len(grid)):
if i % 2 == 0:
if matter_config == InitialState.SINGLE_SECTOR:
moment.append(cirq.X(grid[i]))
moment.append(cirq.H(grid[i]))

elif matter_config == InitialState.DISORDERED:
r = np.random.choice([0, 1])
if r:
moment.append(cirq.X(grid[i]))
moment.append(cirq.H(grid[i]))

else:
moment.append(cirq.I(grid[i]))
else:
if matter_config == InitialState.DISORDERED:
moment.append(cirq.X(grid[i]))
moment.append(cirq.H(grid[i]))

return cirq.Circuit(moment)


def get_1d_dfl_entropy_experiment_circuits(
grid: Sequence[cirq.GridQubit],
initial_state: InitialState,
ncycles: int,
dt: float,
h: float,
mu: float,
n_basis: int = 100,
) -> Sequence[cirq.Circuit]:
"""Generate the circuit instances for the entropy experiment

Args:
grid: The qubits to use for the experiment.
initial_state: Which initial state, see `InitialState` enum.
ncycles: The number of Trotter steps (can be 0).
dt: Trotter step size.
h: The coefficient on the gauge X term.
mu: The coefficient on the matter sigma_x term.
n_basis: The number of random measurement bases to use.

Returns:
A list of the circuit instances.
"""

initial_circuit = initial_state_for_entropy(grid, initial_state)
circuits = []
circ = initial_circuit + layer_floquet(grid, dt, h, mu) * ncycles

for _ in range(n_basis):
circ_randomized = cirq.transformers.RandomizedMeasurements()(
circ, unitary_ensemble="Clifford"
)

circuits.append(circ_randomized)

return circuits


def run_1d_dfl_entropy_experiment(
grid: Sequence[cirq.GridQubit],
initial_states: Sequence[InitialState],
save_dir: Path,
n_cycles: Sequence[int] | npt.NDArray,
dt: float,
h: float,
mu: float,
n_basis: int = 100,
n_shots: int = 1000,
sampler: cirq.Sampler = cirq.Simulator(),
gauge_compile: bool = True,
dynamical_decouple: bool = True,
) -> None:
"""Run the 1D DFL experiment (Fig 4 of the paper).
The paper is available at: https://arxiv.org/abs/2410.06557
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a description about where this data is being saved, since it is writing to local disk.

Saves the measurement bitstrings to save_dir.

Data is saved in the following directory structure:
save_dir/dt{dt}/h{h}_mu{mu}/{initial_state}/cycle{n_cycle}.pickle

Attrs:
grid: The qubits to use for the experiment.
initial_states: The list of InitialState to use.
save_dir: The directory in which to save the results.
n_cycles: The list of number of Trotter steps to use.
dt: The Trotter step size.
h: The coefficient on the gauge X term.
mu: The coefficient on the matter sigma_x term.
n_basis: The number of random measurement bases to use.
n_shots: The number of measurement shots to use.
sampler: The cirq sampler to use.
gauge_compile: Whether to apply gauge compiling.
dynamical_decouple: Whether to apply dynamical decoupling.

Returns:
None
"""

for initial_state in initial_states:
dir_path = (
save_dir / f"dt{dt:.2f}" / f"h{h:.2f}_mu{mu:.2f}" / initial_state.value
)
dir_path.mkdir(parents=True, exist_ok=True)

for n_cycle in n_cycles:
print("Initial state:", initial_state.value, "Cycle:", n_cycle)
fname = dir_path / "cycle{}.pickle".format(n_cycle)
circuits = get_1d_dfl_entropy_experiment_circuits(
grid,
initial_state=initial_state,
ncycles=n_cycle,
dt=dt,
h=h,
mu=mu,
n_basis=n_basis,
)

circuits_modified = []
for i in range(len(circuits)):
circ_i = circuits[i]

if gauge_compile:
circ_i = cirq.transformers.gauge_compiling.CZGaugeTransformer(circ_i)
if dynamical_decouple:
circ_i = cirq.add_dynamical_decoupling(circ_i)
circuits_modified.append(circ_i)

results = sampler.run_batch(circuits, repetitions=n_shots)
bitstrings = []
for j in range(n_basis):
bitstrings.append(results[j][0].measurements["m"])

with open(fname, "wb") as myfile:
pickle.dump(bitstrings, myfile)
return None
66 changes: 66 additions & 0 deletions recirq/dfl/dfl_entropy_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright 2025 Google
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cirq

from recirq.dfl.dfl_entropy import layer_floquet


def test_layer_floquet_basic():
# Create a simple grid of 4 qubits
grid = [cirq.GridQubit(0, i) for i in range(4)]
dt = 0.1
h = 0.5
mu = 0.3

circuit = layer_floquet(grid, dt, h, mu)
# Check that the output is a cirq.Circuit
assert isinstance(circuit, cirq.Circuit)
# Check that the circuit has operations on all qubits
qubits_in_circuit = set(q for op in circuit.all_operations() for q in op.qubits)
assert set(grid) == qubits_in_circuit


def test_layer_floquet_three_qubits_structure():
# Test for 3 qubits, manually construct the expected circuit and compare
q1, q2, q3 = cirq.LineQubit.range(3)
grid = [q1, q2, q3]
dt = 0.2
h = 0.4
mu = 0.6

# Manually construct the expected circuit based on layer_interaction and RX layer
expected_circuit = cirq.Circuit(
cirq.H(q2),
cirq.CZ(q1, q2),
cirq.CZ(q3, q2),
cirq.rx(2 * dt).on(q2),
cirq.CZ(q3, q2),
cirq.CZ(q1, q2),
cirq.H(q2),
)

expected_circuit += cirq.Circuit.from_moments(
cirq.Moment(
[
cirq.rx(2 * mu * dt).on(q1),
cirq.rx(2 * h * dt).on(q2),
cirq.rx(2 * mu * dt).on(q3),
]
)
)

actual_circuit = layer_floquet(grid, dt, h, mu)

assert actual_circuit == expected_circuit
1 change: 1 addition & 0 deletions recirq/dfl/dfl_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class InitialState(enum.Enum):
"""
SINGLE_SECTOR = 'single_sector'
SUPERPOSITION = 'superposition'
DISORDERED = 'disordered'


class Basis(enum.Enum):
Expand Down