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

LRE Executors #2499

Merged
merged 16 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mitiq.egg-info/
dist/
build/
jupyter_execute/

.mypy_cache/
# Coverage reports
coverage.xml
.coverage
Expand Down
5 changes: 5 additions & 0 deletions docs/source/apidoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ See Ref. {cite}`Czarnik_2021_Quantum` for more details on these methods.

### Layerwise Richardson Extrapolation

```{eval-rst}
.. automodule:: mitiq.lre.lre
:members:
```

```{eval-rst}
.. automodule:: mitiq.lre.multivariate_scaling.layerwise_folding
:members:
Expand Down
4 changes: 3 additions & 1 deletion mitiq/lre/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
from mitiq.lre.inference.multivariate_richardson import (
multivariate_richardson_coefficients,
sample_matrix,
)
)

from mitiq.lre.lre import execute_with_lre, mitigate_executor, lre_decorator
161 changes: 161 additions & 0 deletions mitiq/lre/lre.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Copyright (C) Unitary Fund
#
# This source code is licensed under the GPL license (v3) found in the
# LICENSE file in the root directory of this source tree.

"""Extrapolation methods for Layerwise Richardson Extrapolation (LRE)"""

from functools import wraps
from typing import Any, Callable, Optional, Union

import numpy as np
from cirq import Circuit

from mitiq import QuantumResult
from mitiq.lre import (
multivariate_layer_scaling,
multivariate_richardson_coefficients,
)
from mitiq.zne.scaling import fold_gates_at_random


def execute_with_lre(
input_circuit: Circuit,
executor: Callable[[Circuit], QuantumResult],
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
degree: int,
fold_multiplier: int,
folding_method: Callable[
[Union[Any], float], Union[Any]
] = fold_gates_at_random,
Copy link
Member

Choose a reason for hiding this comment

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

Let's follow what happens in ZNE here for consistency.

scale_noise: Callable[[QPROGRAM, float], QPROGRAM] = fold_gates_at_random, # type: ignore [has-type]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't use the num_to_average param in LRE.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I'm missing how num_to_average is related here. I'm just suggesting we typehint the folding method differently.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's weird. I was viewing this suggestion locally in VS code. I interpreted 'follow what happens in ZNE for consistency' as use the typehint as well as the additional param.

image

Copy link
Member

Choose a reason for hiding this comment

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

Have you modified code above these lines? If so then it will probably change the location of the suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hadn't modified anything in ZNE which is why it's weird that when you linked L70 in zne.py, L71 also showed up.

The location of the suggestion was not changed.

num_chunks: Optional[int] = None,
) -> float:
r"""
Defines the executor required for Layerwise Richardson
Extrapolation as defined in :cite:`Russo_2024_LRE`.

Note that this method only works for the multivariate extrapolation
methods. It does not allows a user to choose which layers in the input
circuit will be scaled.

.. seealso::

If you would prefer to choose the layers for unitary
folding, use :func:`mitiq.zne.scaling.layer_scaling.get_layer_folding`
instead.

Args:
input_circuit: Circuit to be scaled.
executor: Executes a circuit and returns a `QuantumResult`
degree: Degree of the multivariate polynomial.
fold_multiplier: Scaling gap required by unitary folding.
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
folding_method: Unitary folding method. Default is
:func:`fold_gates_at_random`.
num_chunks: Number of desired approximately equal chunks. When the
number of chunks is the same as the layers in the input circuit,
the input circuit is unchanged.


Returns:
Error-mitigated expectation value

"""
noise_scaled_circuits = multivariate_layer_scaling(
input_circuit, degree, fold_multiplier, num_chunks, folding_method
)
linear_combination_coeffs = multivariate_richardson_coefficients(
input_circuit, degree, fold_multiplier, num_chunks
)

lre_exp_values = []
for scaled_circuit in noise_scaled_circuits:
circ_exp_val = executor(scaled_circuit)
lre_exp_values.append(circ_exp_val)

# verify the linear combination coefficients and the calculated expectation
# values have the same length
assert len(lre_exp_values) == len(linear_combination_coeffs)
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved

return np.dot(lre_exp_values, linear_combination_coeffs)


def mitigate_executor(
executor: Callable[[Circuit], QuantumResult],
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]:
"""Returns a modified version of the input `executor` which is
error-mitigated with layerwise richardson extrapolation (LRE).

Args:
input_circuit: Circuit to be scaled.
executor: Executes a circuit and returns a `QuantumResult`
degree: Degree of the multivariate polynomial.
fold_multiplier: Scaling gap required by unitary folding.
folding_method: Unitary folding method. Default is
:func:`fold_gates_at_random`.
num_chunks: Number of desired approximately equal chunks. When the
number of chunks is the same as the layers in the input circuit,
the input circuit is unchanged.


Returns:
Error-mitigated version of the circuit executor.
"""

@wraps(executor)
def new_executor(input_circuit: Circuit) -> float:
return execute_with_lre(
input_circuit,
executor,
degree,
fold_multiplier,
folding_method,
num_chunks,
)

return new_executor


def lre_decorator(
degree: int,
fold_multiplier: int,
folding_method: Callable[[Circuit, float], Circuit] = fold_gates_at_random,
num_chunks: Optional[int] = None,
) -> Callable[
[Callable[[Circuit], QuantumResult]], Callable[[Circuit], float]
]:
"""Decorator which adds an error-mitigation layer based on
layerwise richardson extrapolation (LRE).

Args:
input_circuit: Circuit to be scaled.
executor: Executes a circuit and returns a `QuantumResult`
degree: Degree of the multivariate polynomial.
fold_multiplier: Scaling gap required by unitary folding.
folding_method: Unitary folding method. Default is
:func:`fold_gates_at_random`.
num_chunks: Number of desired approximately equal chunks. When the
number of chunks is the same as the layers in the input circuit,
the input circuit is unchanged.


Returns:
Error-mitigated decorator.
"""

def decorator(
executor: Callable[[Circuit], QuantumResult],
) -> Callable[[Circuit], float]:
return mitigate_executor(
executor,
degree,
fold_multiplier,
folding_method,
num_chunks,
)

return decorator
107 changes: 107 additions & 0 deletions mitiq/lre/tests/test_lre.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Unit tests for the LRE extrapolation methods."""

import re

import pytest
from cirq import DensityMatrixSimulator, depolarize

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

# default circuit for all unit tests
test_cirq = benchmarks.generate_rb_circuits(
n_qubits=1,
num_cliffords=2,
)[0]


def execute(circuit, noise_level=0.025):
"""Default executor for all unit tests."""
# Replace with code based on your frontend and backend.
noisy_circuit = circuit.with_noise(depolarize(p=noise_level))
rho = DensityMatrixSimulator().simulate(noisy_circuit).final_density_matrix
return rho[0, 0].real


noisy_val = execute(test_cirq)


def test_lre_exp_value():
"""Verify LRE executors work as expected."""
ideal_val = execute(test_cirq, noise_level=0)
assert abs(ideal_val - noisy_val) > 0
lre_exp_val = execute_with_lre(
test_cirq, execute, degree=2, fold_multiplier=2
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
)
assert lre_exp_val > noisy_val
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved

# verify the mitigated decorator work as expected
mitigated_executor = mitigate_executor(
execute, degree=2, fold_multiplier=2
)
exp_val_from_mitigate_executor = mitigated_executor(test_cirq)
assert exp_val_from_mitigate_executor > noisy_val


def test_lre_decorator():
"""Verify LRE decorators work as expected."""

@lre_decorator(degree=2, fold_multiplier=2)
def execute(circuit, noise_level=0.025):
Copy link
Contributor

Choose a reason for hiding this comment

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

Something more in the spirit of unit tests (and more future-proof) would be a test that only checks whether the correct functions are called, and doesn't actually run the circuit through the simulator. Here you are testing lots of things at the same time (including a Google's simulator), all of which could go wrong without giving much insights on the unit you are testing here, which is the behaviour of the new decorator.

Copy link
Contributor Author

@purva-thakre purva-thakre Sep 27, 2024

Choose a reason for hiding this comment

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

@cosenal @natestemen Are either of you available for a quick call on Friday (excluding the community call)? I have been unsuccessful in trying to write a unit test that utilizes mock objects.

It's unclear to me what needs to be a mock object and what must have a pre-defined value.

mitiq_circuit = circuit
noisy_circuit = mitiq_circuit.with_noise(depolarize(p=noise_level))
rho = (
DensityMatrixSimulator()
.simulate(noisy_circuit)
.final_density_matrix
)
return rho[0, 0].real

assert noisy_val < execute(test_cirq)


def test_lre_decorator_raised_error():
"""Verify error is raised when the defualt parameters for the decorator are
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
not specified."""
with pytest.raises(TypeError, match=re.escape("lre_decorator() missing")):

@lre_decorator()
def execute(circuit, noise_level=0.025):
mitiq_circuit = circuit
noisy_circuit = mitiq_circuit.with_noise(depolarize(p=noise_level))
rho = (
DensityMatrixSimulator()
.simulate(noisy_circuit)
.final_density_matrix
)
return rho[0, 0].real

assert noisy_val < execute(test_cirq)


def test_lre_executor_with_chunking():
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
"""Verify the executor works as expected for chunking a large circuit into
a smaller circuit."""
ideal_val = execute(test_cirq * 20, noise_level=0)
assert abs(ideal_val - noisy_val) > 0
lre_exp_val = execute_with_lre(
test_cirq, execute, degree=2, fold_multiplier=2, num_chunks=5
)
assert lre_exp_val > noisy_val


@pytest.mark.parametrize("input_method", [(fold_global), (fold_all)])
def test_lre_executor_with_different_folding_methods(input_method):
"""Verify the executor works as expected for using non-default unitary
folding methods."""
ideal_val = execute(test_cirq, noise_level=0)
assert abs(ideal_val - noisy_val) > 0
lre_exp_val = execute_with_lre(
test_cirq,
execute,
degree=2,
fold_multiplier=2,
folding_method=input_method,
)
assert lre_exp_val > noisy_val
Loading