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

Ensure LRE compatibility with all supported frontends #2547

Merged
merged 5 commits into from
Nov 6, 2024
Merged
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
2 changes: 1 addition & 1 deletion mitiq/calibration/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def converted_circuit(
"""
circuit = self.circuit.copy()
circuit.append(cirq.measure(circuit.all_qubits()))
return convert_from_mitiq(circuit, circuit_type.value)
return convert_from_mitiq(circuit, circuit_type.name)

@property
def num_qubits(self) -> int:
Expand Down
42 changes: 33 additions & 9 deletions mitiq/interface/conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
"""Functions for converting to/from Mitiq's internal circuit representation."""

from functools import wraps
from typing import Any, Callable, Dict, Iterable, Optional, Tuple, cast
from typing import (
Any,
Callable,
Collection,
Concatenate,
Dict,
Optional,
ParamSpec,
Tuple,
TypeVar,
cast,
)

import cirq

Expand Down Expand Up @@ -150,6 +161,7 @@ def convert_from_mitiq(
circuit: Mitiq circuit to convert.
conversion_type: String specifier for the converted circuit type.
"""
conversion_type = conversion_type.lower()
conversion_function: Callable[[cirq.Circuit], QPROGRAM]
if conversion_type == "qiskit":
from mitiq.interface.mitiq_qiskit.conversions import to_qiskit
Expand Down Expand Up @@ -199,13 +211,21 @@ def conversion_function(circ: cirq.Circuit) -> cirq.Circuit:
return converted_circuit


P = ParamSpec("P")
R = TypeVar("R")


def accept_any_qprogram_as_input(
accept_cirq_circuit_function: Callable[[cirq.Circuit], Any],
) -> Callable[[QPROGRAM], Any]:
accept_cirq_circuit_function: Callable[Concatenate[cirq.Circuit, P], R],
) -> Callable[Concatenate[QPROGRAM, P], R]:
"""Converts functions which take as input cirq.Circuit object (and return
anything), to function which can accept any QPROGRAM.
"""

@wraps(accept_cirq_circuit_function)
def accept_any_qprogram_function(
circuit: QPROGRAM, *args: Any, **kwargs: Any
) -> Any:
circuit: QPROGRAM, *args: P.args, **kwargs: P.kwargs
) -> R:
cirq_circuit, _ = convert_to_mitiq(circuit)
return accept_cirq_circuit_function(cirq_circuit, *args, **kwargs)

Expand Down Expand Up @@ -245,15 +265,19 @@ def qprogram_modifier(


def atomic_one_to_many_converter(
cirq_circuit_modifier: Callable[..., Iterable[cirq.Circuit]],
) -> Callable[..., Iterable[QPROGRAM]]:
cirq_circuit_modifier: Callable[..., Collection[cirq.Circuit]],
) -> Callable[..., Collection[QPROGRAM]]:
"""Convert function which returns multiple cirq.Circuits into a function
which returns multiple QPROGRAM instances.
"""

@wraps(cirq_circuit_modifier)
def qprogram_modifier(
circuit: QPROGRAM, *args: Any, **kwargs: Any
) -> Iterable[QPROGRAM]:
) -> Collection[QPROGRAM]:
mitiq_circuit, input_circuit_type = convert_to_mitiq(circuit)

modified_circuits: Iterable[cirq.Circuit] = cirq_circuit_modifier(
modified_circuits = cirq_circuit_modifier(
mitiq_circuit, *args, **kwargs
)

Expand Down
2 changes: 2 additions & 0 deletions mitiq/lre/inference/multivariate_richardson.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from cirq import Circuit
from numpy.typing import NDArray

from mitiq.interface import accept_any_qprogram_as_input
from mitiq.lre.multivariate_scaling.layerwise_folding import (
_get_scale_factor_vectors,
)
Expand Down Expand Up @@ -120,6 +121,7 @@ def sample_matrix(
return sample_matrix


@accept_any_qprogram_as_input
def multivariate_richardson_coefficients(
input_circuit: Circuit,
degree: int,
Expand Down
21 changes: 11 additions & 10 deletions mitiq/lre/lre.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Any, Callable, Optional, Union

import numpy as np
from cirq import Circuit

from mitiq import QPROGRAM
from mitiq.lre.inference import (
Expand All @@ -22,8 +21,8 @@


def execute_with_lre(
input_circuit: Circuit,
executor: Callable[[Circuit], float],
input_circuit: QPROGRAM,
executor: Callable[[QPROGRAM], float],
degree: int,
fold_multiplier: int,
folding_method: Callable[
Expand Down Expand Up @@ -90,14 +89,14 @@ def execute_with_lre(


def mitigate_executor(
executor: Callable[[Circuit], float],
executor: Callable[[QPROGRAM], float],
degree: int,
fold_multiplier: int,
folding_method: Callable[
[Union[Any], float], Union[Any]
] = fold_gates_at_random,
num_chunks: Optional[int] = None,
) -> Callable[[Circuit], float]:
) -> Callable[[QPROGRAM], float]:
"""Returns a modified version of the input `executor` which is
error-mitigated with layerwise richardson extrapolation (LRE).

Expand All @@ -119,7 +118,7 @@ def mitigate_executor(
"""

@wraps(executor)
def new_executor(input_circuit: Circuit) -> float:
def new_executor(input_circuit: QPROGRAM) -> float:
return execute_with_lre(
input_circuit,
executor,
Expand All @@ -135,9 +134,11 @@ def new_executor(input_circuit: Circuit) -> float:
def lre_decorator(
degree: int,
fold_multiplier: int,
folding_method: Callable[[Circuit, float], Circuit] = fold_gates_at_random,
folding_method: Callable[
[QPROGRAM, float], QPROGRAM
] = fold_gates_at_random,
num_chunks: Optional[int] = None,
) -> Callable[[Callable[[Circuit], float]], Callable[[Circuit], float]]:
) -> Callable[[Callable[[QPROGRAM], float]], Callable[[QPROGRAM], float]]:
"""Decorator which adds an error-mitigation layer based on
layerwise richardson extrapolation (LRE).

Expand All @@ -159,8 +160,8 @@ def lre_decorator(
"""

def decorator(
executor: Callable[[Circuit], float],
) -> Callable[[Circuit], float]:
executor: Callable[[QPROGRAM], float],
) -> Callable[[QPROGRAM], float]:
return mitigate_executor(
executor,
degree,
Expand Down
8 changes: 7 additions & 1 deletion mitiq/lre/multivariate_scaling/layerwise_folding.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from cirq import Circuit

from mitiq import QPROGRAM
from mitiq.interface import accept_qprogram_and_validate
from mitiq.utils import _append_measurements, _pop_measurements
from mitiq.zne.scaling import fold_gates_at_random
from mitiq.zne.scaling.folding import _check_foldable
Expand Down Expand Up @@ -134,7 +135,7 @@ def _get_scale_factor_vectors(
]


def multivariate_layer_scaling(
def _multivariate_layer_scaling(
input_circuit: Circuit,
degree: int,
fold_multiplier: int,
Expand Down Expand Up @@ -208,3 +209,8 @@ def multivariate_layer_scaling(
multiple_folded_circuits.append(folded_circuit)

return multiple_folded_circuits


multivariate_layer_scaling = accept_qprogram_and_validate(
_multivariate_layer_scaling, one_to_many=True
)
43 changes: 33 additions & 10 deletions mitiq/lre/tests/test_layerwise_folding.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
"""Unit tests for scaling noise by unitary folding of layers in the input
circuit to allow for multivariate extrapolation."""

import math
from copy import deepcopy

import pytest
from cirq import Circuit, LineQubit, ops

from mitiq import SUPPORTED_PROGRAM_TYPES
from mitiq.interface import convert_from_mitiq
from mitiq.lre.multivariate_scaling.layerwise_folding import (
_get_chunks,
_get_num_layers_without_measurements,
Expand All @@ -30,14 +33,35 @@
test_circuit1_with_measurements.append(ops.measure_each(*qreg1))


def test_multivariate_layerwise_scaling():
"""Checks if multiple scaled circuits are returned to fit the required
folding pattern for multivariate extrapolation."""
multiple_scaled_circuits = multivariate_layer_scaling(
test_circuit1, 2, 2, 3
def test_multivariate_layerwise_scaling_num_circuits():
"""Ensure the correct number of circuits are generated."""
degree, fold_multiplier, num_chunks = 2, 2, 3
scaled_circuits = multivariate_layer_scaling(
test_circuit1, degree, fold_multiplier, num_chunks
)

assert len(multiple_scaled_circuits) == 10
depth = len(test_circuit1)
# number of circuit is `degree` + `depth` choose `degree`
assert len(scaled_circuits) == math.comb(degree + depth, degree)


@pytest.mark.parametrize("circuit_type", SUPPORTED_PROGRAM_TYPES)
def test_multivariate_layerwise_scaling_types(circuit_type):
"""Ensure layer scaling returns circuits of the correct type."""
circuit = convert_from_mitiq(test_circuit1, circuit_type.name)
degree, fold_multiplier, num_chunks = 2, 2, 3
scaled_circuits = multivariate_layer_scaling(
circuit, degree, fold_multiplier, num_chunks
)

assert all(
isinstance(circuit, circuit_type.value) for circuit in scaled_circuits
)


def test_multivariate_layerwise_scaling_cirq():
"""Ensure the folding pattern is correct for a nontrivial case."""
scaled_circuits = multivariate_layer_scaling(test_circuit1, 2, 2, 3)
folding_pattern = [
(1, 1, 1),
(5, 1, 1),
Expand All @@ -50,16 +74,15 @@ def test_multivariate_layerwise_scaling():
(1, 5, 5),
(1, 1, 9),
]

for i, scale_factor_vector in enumerate(folding_pattern):
scale_layer1, scale_layer2, scale_layer3 = scale_factor_vector
for scale_factors, circuit in zip(folding_pattern, scaled_circuits):
scale_layer1, scale_layer2, scale_layer3 = scale_factors
expected_circuit = Circuit(
[ops.H.on_each(*qreg1)] * scale_layer1,
[ops.CNOT.on(qreg1[0], qreg1[1]), ops.X.on(qreg1[2])]
* scale_layer2,
[ops.TOFFOLI.on(*qreg1)] * scale_layer3,
)
assert expected_circuit == multiple_scaled_circuits[i]
assert expected_circuit == circuit


@pytest.mark.parametrize(
Expand Down
27 changes: 25 additions & 2 deletions mitiq/lre/tests/test_lre.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Unit tests for the LRE extrapolation methods."""

import math
import random
import re
from unittest.mock import Mock

import pytest
from cirq import DensityMatrixSimulator, depolarize

from mitiq import benchmarks
from mitiq import SUPPORTED_PROGRAM_TYPES, benchmarks
from mitiq.lre import execute_with_lre, lre_decorator, mitigate_executor
from mitiq.zne.scaling import fold_all, fold_global

Expand Down Expand Up @@ -40,8 +43,28 @@ def test_lre_exp_value(degree, fold_multiplier):
assert abs(lre_exp_val - ideal_val) <= abs(noisy_val - ideal_val)


@pytest.mark.parametrize("circuit_type", SUPPORTED_PROGRAM_TYPES.keys())
def test_lre_all_qprogram(circuit_type):
"""Verify LRE works with all supported frontends."""
degree, fold_multiplier = 2, 3
circuit = benchmarks.generate_ghz_circuit(3, circuit_type)
depth = 3 # not all circuit types have a simple way to compute depth

mock_executor = Mock(side_effect=lambda _: random.random())

lre_exp_val = execute_with_lre(
circuit,
mock_executor,
degree=degree,
fold_multiplier=fold_multiplier,
)

assert isinstance(lre_exp_val, float)
assert mock_executor.call_count == math.comb(degree + depth, degree)


@pytest.mark.parametrize("degree, fold_multiplier", [(2, 2), (2, 3), (3, 4)])
def test_lre_exp_value_decorator(degree, fold_multiplier):
def test_lre_mitigate_executor(degree, fold_multiplier):
"""Verify LRE mitigated executor work as expected."""
mitigated_executor = mitigate_executor(
execute, degree=2, fold_multiplier=2
Expand Down
29 changes: 15 additions & 14 deletions mitiq/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from typing import (
Any,
Dict,
Iterable,
List,
Optional,
Sequence,
Expand All @@ -37,25 +36,17 @@

class EnhancedEnumMeta(EnumMeta):
def __str__(cls) -> str:
return ", ".join([member.value for member in cast(Type[Enum], cls)])
return ", ".join(
[member.name.lower() for member in cast(Type[Enum], cls)]
)


class EnhancedEnum(Enum, metaclass=EnhancedEnumMeta):
# This is for backwards compatibility with the old representation
# of SUPPORTED_PROGRAM_TYPES, which was a dictionary
@classmethod
def keys(cls) -> Iterable[str]:
return [member.value for member in cls]


# Supported quantum programs.
class SUPPORTED_PROGRAM_TYPES(EnhancedEnum):
BRAKET = "braket"
CIRQ = "cirq"
PENNYLANE = "pennylane"
PYQUIL = "pyquil"
QIBO = "qibo"
QISKIT = "qiskit"
def keys(cls) -> list[str]:
return [member.name.lower() for member in cls]


try:
Expand Down Expand Up @@ -90,6 +81,16 @@ class SUPPORTED_PROGRAM_TYPES(EnhancedEnum):
]


# Supported quantum programs.
class SUPPORTED_PROGRAM_TYPES(EnhancedEnum):
BRAKET = _BKCircuit
CIRQ = _Circuit
PENNYLANE = _QuantumTape
PYQUIL = _Program
QIBO = _QiboCircuit
QISKIT = _QuantumCircuit


# Define MeasurementResult, a result obtained by measuring qubits on a quantum
# computer.
Bitstring = Union[str, List[int]]
Expand Down