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

Noise Scaling for LRE #2347

Merged
merged 22 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions docs/source/apidoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ See Ref. {cite}`Czarnik_2021_Quantum` for more details on these methods.
:members:
```

### Layerwise Richardson Extrapolation

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

### Pauli Twirling

```{eval-rst}
Expand Down Expand Up @@ -116,6 +123,7 @@ See Ref. {cite}`Czarnik_2021_Quantum` for more details on these methods.
:members:
```


#### Learning-based PEC

```{eval-rst}
Expand Down
10 changes: 10 additions & 0 deletions docs/source/refs.bib
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,16 @@ @misc{Russo_2022_Testing
year = {2022},
copyright = {arXiv.org perpetual, non-exclusive license},
}

@misc{Russo_2024_LRE,
title={Quantum error mitigation by layerwise Richardson extrapolation},
author={Vincent Russo and Andrea Mari},
year={2024},
eprint={2402.04000},
archivePrefix={arXiv},
primaryClass={quant-ph}
}

# Letter S

@article{Sagastizabal_2019_PRA,
Expand Down
17 changes: 14 additions & 3 deletions mitiq/interface/conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""Functions for converting to/from Mitiq's internal circuit representation."""

from functools import wraps
from itertools import chain
from typing import Any, Callable, Dict, Iterable, Optional, Tuple, cast

import cirq
Expand Down Expand Up @@ -317,6 +318,12 @@ def new_function(circuit: QPROGRAM, *args: Any, **kwargs: Any) -> QPROGRAM:
)
out_circuits = [out_circuit]

# for the case when the circuit modifier returns a list of circuits
# flatten the list of lists into a list
if isinstance(out_circuits[0], list):
new_out_circuits = list(chain(*out_circuits))
out_circuits = new_out_circuits

purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
circuits_to_return = []
for out_circuit in out_circuits:
# Post atomic conversion
Expand Down Expand Up @@ -379,9 +386,13 @@ def new_function(circuit: QPROGRAM, *args: Any, **kwargs: Any) -> QPROGRAM:
out_circuit.measure(q, c)
circuits_to_return.append(out_circuit)

if not one_to_many:
assert len(circuits_to_return) == 1
return circuits_to_return[0]
try:
if not one_to_many:
assert len(circuits_to_return) == 1
return circuits_to_return[0]
except AssertionError:
# for the case when the circuit modifier returns a list of circuits
assert len(out_circuits) == len(circuits_to_return)

return circuits_to_return

Expand Down
8 changes: 8 additions & 0 deletions mitiq/lre/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 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.

"""Methods for scaling noise in circuits by layers and using multivariate extrapolation."""

from mitiq.lre.multivariate_scaling.layerwise_folding import multivariate_layer_scaling
214 changes: 214 additions & 0 deletions mitiq/lre/multivariate_scaling/layerwise_folding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# 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.

"""Functions for layerwise folding of input circuits to allow for multivariate
extrapolation as defined in :cite:`Russo_2024_LRE`.
"""

import itertools
from copy import deepcopy
from typing import Any, Callable, List, Optional, Tuple

import numpy as np
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


def _get_num_layers_without_measurements(input_circuit: Circuit) -> int:
"""Checks if the circuit has non-terminal measurements and returns the
number of layers in the input circuit without the terminal measurements.

Args:
input_circuit: Circuit of interest

Raises:
UnfoldableCircuitError:
* If the circuit has intermediate measurements
* If the circuit has non-unitary channels which are not
terminal measurements

Returns:
num_layers: the number of layers in the input circuit without the
terminal measurements
"""

_check_foldable(input_circuit)
Copy link
Contributor

Choose a reason for hiding this comment

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

This can raise an UnfoldableCircuitError, it should be documented in the docstring of this caller function as well.

Copy link
Collaborator Author

@purva-thakre purva-thakre Jun 22, 2024

Choose a reason for hiding this comment

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

I had included it previously. #2347 (comment)

We should agree on style guidelines for this project and update the docs style guidelines section on docstrings.

Couple of things that confused me:

  • Since I am relying on private functions, a user won't be able to read the source of the error in the API. Which is why I felt it's better to document the raised errors in the public function.
  • Referring to Vincent's comment on raising errors from a function in a different module, the API-doc page only mentions UnfoldableCircuitError once as a defined exception. Since _check_foldable is a private function, the error raised by the function does not show up publicly.
  • Most of the raised errors in the public-facing function are specified in a paragraph.
    image
    As we rely on google-style docstrings, there is no convention to allow lists in the description of the different sections of a docstring. Portions of our API-doc have run-on sentences because someone used a list in the section description which appear as badly formatted sentences publicly.
Intended in API-doc
image image

circuit = deepcopy(input_circuit)
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
_pop_measurements(circuit)
return len(circuit)


def _get_chunks(
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
input_circuit: Circuit, num_chunks: Optional[int] = None
) -> List[Circuit]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Side note: Are we still supporting versions of Python that wouldn't allow -> list[Circuit] vs. List[Circuit]?

Copy link
Collaborator Author

@purva-thakre purva-thakre Jun 10, 2024

Choose a reason for hiding this comment

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

This is a good question. Which versions of python did you have in mind? I know there's an open issue to drop 3.9 once we add support for 3.12.

The validate workflow only runs for python 3.11 which is used for type checking by mypy.

python-version: "3.11"
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
make install requirements
- name: Check types with mypy
run: make check-types

From what I could find, 3.9 uses list[Circuit] as a type alias and List[Circuit] for type annotations. Same with 3.10.

Copy link
Contributor

Choose a reason for hiding this comment

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

TIL that type aliases List and Dict are deprecated starting from Python 3.9 https://docs.python.org/3/library/typing.html#deprecated-aliases

"""Splits a circuit into approximately equal chunks.

Adapted from:
https://stackoverflow.com/questions/2130016/splitting-a-list-into-n-parts-of-approximately-equal-length

Args:
input_circuit: Circuit of interest
num_chunks: Number of desired approximately equal chunks
* when num_chunks == num_layers, the original circuit is
returned
* when num_chunks == 1, the entire circuit is chunked into 1
layer


Returns:
cosenal marked this conversation as resolved.
Show resolved Hide resolved
split_circuit: Circuit of interest split into approximately equal
chunks
"""
num_layers = _get_num_layers_without_measurements(input_circuit)
if num_chunks is None:
num_chunks = num_layers
cosenal marked this conversation as resolved.
Show resolved Hide resolved

if num_chunks == 0:
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"Number of chunks should be greater than or equal to 1."
)

if num_chunks > num_layers:
raise ValueError(
f"Number of chunks {num_chunks} cannot be greater than the number"
f" of layers {num_layers}."
)

k, m = divmod(num_layers, num_chunks)
return [
input_circuit[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)]
for i in range(num_chunks)
]


def _get_scale_factor_vectors(
input_circuit: Circuit,
degree: int,
fold_multiplier: int,
num_chunks: Optional[int] = None,
) -> List[Tuple[Any, ...]]:
"""Returns the patterned scale factor vectors required for multivariate
extrapolation.

Args:
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
input_circuit: Circuit to be scaled
degree: Degree of the multivariate polynomial
num_chunks: Number of desired approximately equal chunks
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
fold_multiplier: Scaling gap required by unitary folding

Returns:
scale_factor_vectors: Multiple variations of scale factors for each
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
layer in the input circuit
"""

circuit_chunks = _get_chunks(input_circuit, num_chunks)
num_layers = len(circuit_chunks)

# Find the exponents of all the monomial terms required for the folding
# pattern
pattern_full = []
for i in range(degree + 1):
for j in itertools.combinations_with_replacement(range(num_layers), i):
pattern = np.zeros(num_layers, dtype=int)
# Get the monomial terms in graded lexicographic order.
for index in j:
pattern[index] += 1
# Use the fold multiplier on the folding pattern to determine which
# layers will be scaled.
pattern_full.append(tuple(fold_multiplier * pattern))

# Get the scale factor vectors.
# The layers are scaled as 2n+1 due to unitary folding.
return [
tuple(2 * num_folds + 1 for num_folds in pattern)
for pattern in pattern_full
]


@accept_qprogram_and_validate
def multivariate_layer_scaling(
input_circuit: Circuit,
degree: int,
fold_multiplier: int,
num_chunks: Optional[int] = None,
folding_method: Callable[
[QPROGRAM, float], QPROGRAM
] = fold_gates_at_random,
) -> List[Circuit]:
r"""
Defines the noise scaling function 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
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
degree: Degree of the multivariate polynomial
fold_multiplier: Scaling gap required by unitary folding
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.
folding_method: Unitary folding method. Default is
:func:`fold_gates_at_random`

Returns:
Multiple folded variations of the input circuit
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved

Raises:
ValueError:
When the degree for the multinomial is not greater than or
equal to 1.
ValueError:
When the fold multiplier to scale the circuit is greater than or
equal to 1.
ValueError:
When the number of chunks for a large circuit is 0.
ValueError:
When the number of chunks in a circuit is greater than the number
of layers in the input circuit.

"""
if degree < 1:
raise ValueError(
"Multinomial degree must be greater than or equal to 1."
)
if fold_multiplier < 1:
raise ValueError("Fold multiplier must be greater than or equal to 1.")
circuit_copy = deepcopy(input_circuit)
terminal_measurements = _pop_measurements(circuit_copy)

scaling_pattern = _get_scale_factor_vectors(
circuit_copy, degree, fold_multiplier, num_chunks
)

chunks = _get_chunks(circuit_copy, num_chunks)

multiple_folded_circuits = []
for scale_factor_vector in scaling_pattern:
folded_circuit = Circuit()
for chunk, scale_factor in zip(chunks, scale_factor_vector):
if scale_factor == 1:
folded_circuit += chunk
else:
chunks_circ = Circuit(chunk)
folded_chunk_circ = folding_method(chunks_circ, scale_factor)
folded_circuit += folded_chunk_circ
_append_measurements(folded_circuit, terminal_measurements)
multiple_folded_circuits.append((folded_circuit))
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved

return multiple_folded_circuits
Loading
Loading