Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update parallel XEB methods to support circuits instead of single gates #6940

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 83 additions & 17 deletions cirq-core/cirq/experiments/random_quantum_circuit_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,36 @@ def random_rotations_between_two_qubit_circuit(
return circuit


def _generate_library_of_2q_circuits(
n_library_circuits: int,
two_qubit_op_factory: Callable[
['cirq.Qid', 'cirq.Qid', 'np.random.RandomState'], 'cirq.OP_TREE'
] = lambda a, b, _: ops.CZPowGate()(a, b),
*,
max_cycle_depth: int = 100,
q0: 'cirq.Qid' = devices.LineQubit(0),
q1: 'cirq.Qid' = devices.LineQubit(1),
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
):
rs = value.parse_random_state(random_state)
exponents = np.linspace(0, 7 / 4, 8)
single_qubit_gates = [
ops.PhasedXZGate(x_exponent=0.5, z_exponent=z, axis_phase_exponent=a)
for a, z in itertools.product(exponents, repeat=2)
]
return [
random_rotations_between_two_qubit_circuit(
q0,
q1,
depth=max_cycle_depth,
two_qubit_op_factory=two_qubit_op_factory,
single_qubit_gates=single_qubit_gates,
seed=rs,
)
for _ in range(n_library_circuits)
]


def generate_library_of_2q_circuits(
n_library_circuits: int,
two_qubit_gate: 'cirq.Gate',
Expand Down Expand Up @@ -269,23 +299,59 @@ def generate_library_of_2q_circuits(
random_state: A random state or seed used to deterministically sample the random circuits.
tags: Tags to add to the two qubit operations.
"""
rs = value.parse_random_state(random_state)
exponents = np.linspace(0, 7 / 4, 8)
single_qubit_gates = [
ops.PhasedXZGate(x_exponent=0.5, z_exponent=z, axis_phase_exponent=a)
for a, z in itertools.product(exponents, repeat=2)
]
return [
random_rotations_between_two_qubit_circuit(
q0,
q1,
depth=max_cycle_depth,
two_qubit_op_factory=lambda a, b, _: two_qubit_gate(a, b).with_tags(*tags),
single_qubit_gates=single_qubit_gates,
seed=rs,
)
for _ in range(n_library_circuits)
]
two_qubit_op_factory = lambda a, b, _: two_qubit_gate(a, b).with_tags(*tags)
return _generate_library_of_2q_circuits(
n_library_circuits=n_library_circuits,
two_qubit_op_factory=two_qubit_op_factory,
max_cycle_depth=max_cycle_depth,
q0=q0,
q1=q1,
random_state=random_state,
)


def generate_library_of_2q_circuits_for_circuit_op(
n_library_circuits: int,
circuit_or_op: Union[circuits.AbstractCircuit, ops.Operation],
*,
max_cycle_depth: int = 100,
q0: 'cirq.Qid' = devices.LineQubit(0),
q1: 'cirq.Qid' = devices.LineQubit(1),
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
tags: Sequence[Any] = (),
):
"""Generate a library of two-qubit Circuits.

For single-qubit gates, this uses PhasedXZGates where the axis-in-XY-plane is one
of eight eighth turns and the Z rotation angle is one of eight eighth turns. This
provides 8*8=64 total choices, each implementable with one PhasedXZGate. This is
appropriate for architectures with microwave single-qubit control.

Args:
n_library_circuits: The number of circuits to generate.
circuit_or_op: A circuit on two qubits or a two qubit operation.
max_cycle_depth: The maximum cycle_depth in the circuits to generate. If you are using XEB,
this must be greater than or equal to the maximum value in `cycle_depths`.
q0: The first qubit to use when constructing the circuits.
q1: The second qubit to use when constructing the circuits
random_state: A random state or seed used to deterministically sample the random circuits.
tags: Tags to add to the two qubit operations.
"""
if isinstance(circuit_or_op, ops.Operation):
op = circuit_or_op.with_tags(*tags)
two_qubit_op_factory = lambda a, b, _: op.with_qubits(a, b)
else:
cop = circuits.CircuitOperation(circuit_or_op.freeze().with_tags(*tags))
two_qubit_op_factory = lambda a, b, _: cop.with_qubits(a, b).mapped_op()

return _generate_library_of_2q_circuits(
n_library_circuits=n_library_circuits,
two_qubit_op_factory=two_qubit_op_factory,
max_cycle_depth=max_cycle_depth,
q0=q0,
q1=q1,
random_state=random_state,
)


def _get_active_pairs(graph: nx.Graph, grid_layer: GridInteractionLayer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
get_random_combinations_for_pairs,
get_random_combinations_for_layer_circuit,
get_grid_interaction_layer_circuit,
generate_library_of_2q_circuits_for_circuit_op,
)

SINGLE_QUBIT_LAYER = Dict[cirq.GridQubit, Optional[cirq.Gate]]
Expand Down Expand Up @@ -86,6 +87,40 @@ def test_generate_library_of_2q_circuits():
assert m2.operations[0].gate == cirq.CNOT


def test_generate_library_of_2q_circuits_for_circuit_op_with_operation():
circuits = generate_library_of_2q_circuits_for_circuit_op(
n_library_circuits=5,
circuit_or_op=cirq.CNOT(cirq.q(0), cirq.q(1)),
max_cycle_depth=13,
random_state=9,
)
assert len(circuits) == 5
for circuit in circuits:
assert len(circuit.all_qubits()) == 2
assert sorted(circuit.all_qubits()) == cirq.LineQubit.range(2)
for m1, m2 in zip(circuit.moments[::2], circuit.moments[1::2]):
assert len(m1.operations) == 2 # single qubit layer
assert len(m2.operations) == 1
assert m2.operations[0].gate == cirq.CNOT


def test_generate_library_of_2q_circuits_for_circuit_op_with_circuit():
circuits = generate_library_of_2q_circuits_for_circuit_op(
n_library_circuits=5,
circuit_or_op=cirq.Circuit(cirq.CNOT(cirq.q(0), cirq.q(1))),
max_cycle_depth=13,
random_state=9,
)
assert len(circuits) == 5
for circuit in circuits:
assert len(circuit.all_qubits()) == 2
assert sorted(circuit.all_qubits()) == cirq.LineQubit.range(2)
for m1, m2 in zip(circuit.moments[::2], circuit.moments[1::2]):
assert len(m1.operations) == 2 # single qubit layer
assert len(m2.operations) == 1
assert isinstance(m2.operations[0], cirq.CircuitOperation)


def test_generate_library_of_2q_circuits_custom_qubits():
circuits = generate_library_of_2q_circuits(
n_library_circuits=5,
Expand Down
36 changes: 28 additions & 8 deletions cirq-core/cirq/experiments/two_qubit_xeb.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

"""Provides functions for running and analyzing two-qubit XEB experiments."""
from typing import Sequence, TYPE_CHECKING, Optional, Tuple, Dict, cast, Mapping, Any
from typing import Sequence, TYPE_CHECKING, Optional, Tuple, Dict, cast, Mapping, Any, Union

from dataclasses import dataclass
from types import MappingProxyType
Expand Down Expand Up @@ -403,6 +403,7 @@ def parallel_xeb_workflow(
pool: Optional['multiprocessing.pool.Pool'] = None,
batch_size: int = 9,
tags: Sequence[Any] = (),
entangling_circuit_or_op: Optional[Union['cirq.AbstractCircuit', 'cirq.Operation']] = None,
Copy link
Collaborator

@eliottrosenberg eliottrosenberg Jan 14, 2025

Choose a reason for hiding this comment

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

Actually, in order to do parallel XEB, we will want to be able to input something like a set of circuits (that each act on two qubits) or a dictionary from pairs to circuits because, for example, we may have different z-phase corrections on different pairs of qubits.

**plot_kwargs,
) -> Tuple[pd.DataFrame, Sequence['cirq.Circuit'], pd.DataFrame]:
"""A utility method that runs the full XEB workflow.
Expand All @@ -424,6 +425,8 @@ def parallel_xeb_workflow(
environments. The number of (circuit, cycle_depth) tasks to be run in each batch
is given by this number.
tags: Tags to add to two qubit operations.
entangling_circuit_or_op: Optional operation or circuit for XEB.
When provided it overrides `entangling_gate`.
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.

Returns:
Expand All @@ -447,13 +450,22 @@ def parallel_xeb_workflow(
ax.set_title('device layout')
ax.plot(**plot_kwargs)

circuit_library = rqcg.generate_library_of_2q_circuits(
n_library_circuits=n_circuits,
two_qubit_gate=entangling_gate,
random_state=rs,
max_cycle_depth=max(cycle_depths),
tags=tags,
)
if entangling_circuit_or_op is not None:
circuit_library = rqcg.generate_library_of_2q_circuits_for_circuit_op(
n_library_circuits=n_circuits,
circuit_or_op=entangling_circuit_or_op,
random_state=rs,
max_cycle_depth=max(cycle_depths),
tags=tags,
)
else:
circuit_library = rqcg.generate_library_of_2q_circuits(
n_library_circuits=n_circuits,
two_qubit_gate=entangling_gate,
random_state=rs,
max_cycle_depth=max(cycle_depths),
tags=tags,
)

combs_by_layer = rqcg.get_random_combinations_for_device(
n_library_circuits=len(circuit_library),
Expand Down Expand Up @@ -492,6 +504,7 @@ def parallel_two_qubit_xeb(
pairs: Optional[Sequence[tuple['cirq.GridQubit', 'cirq.GridQubit']]] = None,
batch_size: int = 9,
tags: Sequence[Any] = (),
entangling_circuit_or_op: Optional[Union['cirq.AbstractCircuit', 'cirq.Operation']] = None,
**plot_kwargs,
) -> TwoQubitXEBResult:
"""A convenience method that runs the full XEB workflow.
Expand All @@ -512,6 +525,8 @@ def parallel_two_qubit_xeb(
environments. The number of (circuit, cycle_depth) tasks to be run in each batch
is given by this number.
tags: Tags to add to two qubit operations.
entangling_circuit_or_op: Optional operation or circuit for XEB.
When provided it overrides `entangling_gate`.
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
Returns:
A TwoQubitXEBResult object representing the results of the experiment.
Expand All @@ -531,6 +546,7 @@ def parallel_two_qubit_xeb(
ax=ax,
batch_size=batch_size,
tags=tags,
entangling_circuit_or_op=entangling_circuit_or_op,
**plot_kwargs,
)
return TwoQubitXEBResult(fit_exponential_decays(fids))
Expand All @@ -551,6 +567,7 @@ def run_rb_and_xeb(
pairs: Optional[Sequence[tuple['cirq.GridQubit', 'cirq.GridQubit']]] = None,
batch_size: int = 9,
tags: Sequence[Any] = (),
entangling_circuit_or_op: Optional[Union['cirq.AbstractCircuit', 'cirq.Operation']] = None,
) -> InferredXEBResult:
"""A convenience method that runs both RB and XEB workflows.

Expand All @@ -569,6 +586,8 @@ def run_rb_and_xeb(
environments. The number of (circuit, cycle_depth) tasks to be run in each batch
is given by this number.
tags: Tags to add to two qubit operations.
entangling_circuit_or_op: Optional operation or circuit for XEB.
When provided it overrides `entangling_gate`.

Returns:
An InferredXEBResult object representing the results of the experiment.
Expand Down Expand Up @@ -599,6 +618,7 @@ def run_rb_and_xeb(
random_state=random_state,
batch_size=batch_size,
tags=tags,
entangling_circuit_or_op=entangling_circuit_or_op,
)

return InferredXEBResult(rb, xeb)
51 changes: 51 additions & 0 deletions cirq-core/cirq/experiments/two_qubit_xeb_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,57 @@ def test_run_rb_and_xeb(
)


@pytest.mark.parametrize(
'sampler,qubits,pairs',
[
(
cirq.DensityMatrixSimulator(
seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1))
),
cirq.GridQubit.rect(3, 2, 4, 3),
None,
),
(
cirq.DensityMatrixSimulator(
seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1))
),
None,
[
(cirq.GridQubit(0, 0), cirq.GridQubit(0, 1)),
(cirq.GridQubit(0, 0), cirq.GridQubit(1, 0)),
],
),
(
DensityMatrixSimulatorWithProcessor(
seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1))
),
None,
None,
),
],
)
def test_run_rb_and_xeb_with_circuit(
sampler: cirq.Sampler,
qubits: Optional[Sequence[cirq.GridQubit]],
pairs: Optional[Sequence[tuple[cirq.GridQubit, cirq.GridQubit]]],
):
res = cirq.experiments.run_rb_and_xeb(
sampler=sampler,
qubits=qubits,
pairs=pairs,
repetitions=100,
num_clifford_range=tuple(np.arange(3, 10, 1)),
xeb_combinations=1,
num_circuits=1,
depths_xeb=(3, 4, 5),
random_state=0,
entangling_circuit_or_op=cirq.Circuit(cirq.CZ(cirq.q(0), cirq.q(1))),
)
np.testing.assert_allclose(
[res.xeb_result.xeb_error(*pair) for pair in res.all_qubit_pairs], 0.1, atol=1e-1
)


def test_run_rb_and_xeb_without_processor_fails():
sampler = (
cirq.DensityMatrixSimulator(
Expand Down
16 changes: 13 additions & 3 deletions cirq-core/cirq/experiments/xeb_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import tqdm

from cirq import ops, devices, value, protocols
from cirq.circuits import Circuit, Moment
from cirq.circuits import Circuit, Moment, CircuitOperation
from cirq.experiments.random_quantum_circuit_generation import CircuitLibraryCombination

if TYPE_CHECKING:
Expand Down Expand Up @@ -172,6 +172,12 @@ class _ZippedCircuit:
combination_i: int


def _map_circuit(circuit):
"""Calls .mapped_op on all CircuitOperations within the circuit."""
op_fn = lambda op: op.mapped_op() if isinstance(op, CircuitOperation) else op
return Circuit.from_moments(*[[op_fn(op) for op in m] for m in circuit])


def _get_combinations_by_layer_for_isolated_xeb(
circuits: Sequence['cirq.Circuit'],
) -> Tuple[List[CircuitLibraryCombination], List['cirq.Circuit']]:
Expand All @@ -184,7 +190,11 @@ def _get_combinations_by_layer_for_isolated_xeb(
"""
q0, q1 = _verify_and_get_two_qubits_from_circuits(circuits)
circuits = [
circuit.transform_qubits(lambda q: {q0: devices.LineQubit(0), q1: devices.LineQubit(1)}[q])
_map_circuit(
circuit.transform_qubits(
lambda q: {q0: devices.LineQubit(0), q1: devices.LineQubit(1)}[q]
)
)
for circuit in circuits
]
return [
Expand Down Expand Up @@ -215,7 +225,7 @@ def _zip_circuits(
for combination_i, combination in enumerate(layer_combinations.combinations):
wide_circuit = Circuit.zip(
*(
circuits[i].transform_qubits(lambda q: pair[q.x])
_map_circuit(circuits[i].transform_qubits(lambda q: pair[q.x]))
for i, pair in zip(combination, layer_combinations.pairs)
)
)
Expand Down