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 5 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
6 changes: 6 additions & 0 deletions mitiq/lre/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 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."""
122 changes: 122 additions & 0 deletions mitiq/lre/multivariate_scaling/layerwise_folding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# 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."""

import itertools
from copy import deepcopy

import cirq
import numpy as np

from mitiq.utils import _pop_measurements
from mitiq.zne.scaling.folding import _check_foldable


def _get_num_layers_without_measurements(input_circuit: cirq.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: Checks whether this circuit is able to be folded.
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved

Raises:
UnfoldableCircuitError:
* If the circuit has intermediate measurements.
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
* 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.
"""
circuit = deepcopy(input_circuit)
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
_check_foldable(circuit)
_pop_measurements(circuit)
num_layers = len(circuit)
return num_layers


def _get_chunks(
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
input_circuit: cirq.Circuit, num_chunks: int
) -> list[cirq.Circuit]:
"""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.
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
num_chunks: Number of desired approximately equal chunks
* when num_chunks == num_layers, the circuit gets split into each
layer in the input circuit
* 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 == 0:
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError("The number of chunks should be >= to 1.")

if num_chunks > num_layers:
raise ValueError(
"Number of chunks > the number of layers in the circuit."
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
)

k, m = divmod(num_layers, num_chunks)
split_circuit = (
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
input_circuit[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)]
for i in range(num_chunks)
)
return list(split_circuit)


def _get_scale_factor_vectors(
input_circuit: cirq.Circuit,
degree: int,
fold_multiplier: int,
num_chunks: int = 1,
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
) -> list[tuple[int]]:
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
"""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
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
"""
if num_chunks == 1:
num_layers = _get_num_layers_without_measurements(input_circuit)
else:
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 = [0] * num_layers
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
# get the monomial terms in graded lexicographic order
for index in j:
pattern[index] += 1
# use fold multiplier on the folding pattern to figure out which
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
# layers will be scaled
pattern_full.append(tuple(fold_multiplier * np.array(pattern)))

# get the scale factor vectors
# the layers are scaled as 2n+1 for unitary folding
return [
tuple(2 * num_folds + 1 for num_folds in pattern)
for pattern in pattern_full
]
209 changes: 209 additions & 0 deletions mitiq/lre/tests/test_layerwise_folding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# 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.

"""Unit tests for scaling noise by unitary folding of layers in the input
circuit to allow for multivariate extrapolation."""

from copy import deepcopy

import cirq
import pytest

from mitiq.lre.multivariate_scaling.layerwise_folding import (
_get_num_layers_without_measurements,
_get_scale_factor_vectors,
)

qreg1 = cirq.LineQubit.range(3)
test_circuit1 = cirq.Circuit(
[cirq.ops.H.on_each(*qreg1)],
[cirq.ops.CNOT.on(qreg1[0], qreg1[1])],
[cirq.ops.X.on(qreg1[2])],
[cirq.ops.TOFFOLI.on(*qreg1)],
)

test_circuit1_with_measurements = deepcopy(test_circuit1)
test_circuit1_with_measurements.append(cirq.ops.measure_each(*qreg1))


@pytest.mark.parametrize(
"test_input, expected",
[(test_circuit1, 3), (test_circuit1_with_measurements, 3)],
)
def test_get_num_layers(test_input, expected):
"""Verifies function works as expected."""
calculated_num_layers = _get_num_layers_without_measurements(test_input)

assert calculated_num_layers == expected


@pytest.mark.parametrize(
"test_input, degree, test_fold_multiplier, expected_scale_factor_vectors",
[
(test_circuit1, 1, 0, [(1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1)]),
(test_circuit1, 1, 1, [(1, 1, 1), (3, 1, 1), (1, 3, 1), (1, 1, 3)]),
(
test_circuit1,
2,
1,
[
(1, 1, 1),
(3, 1, 1),
(1, 3, 1),
(1, 1, 3),
(5, 1, 1),
(3, 3, 1),
(3, 1, 3),
(1, 5, 1),
(1, 3, 3),
(1, 1, 5),
],
),
(
test_circuit1,
2,
2,
[
(1, 1, 1),
(5, 1, 1),
(1, 5, 1),
(1, 1, 5),
(9, 1, 1),
(5, 5, 1),
(5, 1, 5),
(1, 9, 1),
(1, 5, 5),
(1, 1, 9),
],
),
(
test_circuit1,
2,
3,
[
(1, 1, 1),
(7, 1, 1),
(1, 7, 1),
(1, 1, 7),
(13, 1, 1),
(7, 7, 1),
(7, 1, 7),
(1, 13, 1),
(1, 7, 7),
(1, 1, 13),
],
),
(
test_circuit1_with_measurements,
1,
0,
[(1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1)],
),
(
test_circuit1_with_measurements,
1,
1,
[(1, 1, 1), (3, 1, 1), (1, 3, 1), (1, 1, 3)],
),
(
test_circuit1_with_measurements,
2,
1,
[
(1, 1, 1),
(3, 1, 1),
(1, 3, 1),
(1, 1, 3),
(5, 1, 1),
(3, 3, 1),
(3, 1, 3),
(1, 5, 1),
(1, 3, 3),
(1, 1, 5),
],
),
(
test_circuit1_with_measurements,
2,
2,
[
(1, 1, 1),
(5, 1, 1),
(1, 5, 1),
(1, 1, 5),
(9, 1, 1),
(5, 5, 1),
(5, 1, 5),
(1, 9, 1),
(1, 5, 5),
(1, 1, 9),
],
),
(
test_circuit1_with_measurements,
2,
3,
[
(1, 1, 1),
(7, 1, 1),
(1, 7, 1),
(1, 1, 7),
(13, 1, 1),
(7, 7, 1),
(7, 1, 7),
(1, 13, 1),
(1, 7, 7),
(1, 1, 13),
],
),
],
)
def test_get_scale_factor_vectors_no_chunking(
test_input, degree, test_fold_multiplier, expected_scale_factor_vectors
):
"""Verifies vectors of scale factors are calculated accurately."""
calculated_scale_factor_vectors = _get_scale_factor_vectors(
test_input, degree, test_fold_multiplier
)

assert calculated_scale_factor_vectors == expected_scale_factor_vectors


@pytest.mark.parametrize(
"test_input, degree, test_fold_multiplier, test_chunks, expected_size",
[
(test_circuit1, 1, 0, 1, 4),
(test_circuit1, 1, 1, 2, 3),
(test_circuit1, 2, 1, 3, 10),
(test_circuit1, 2, 3, 2, 6),
],
)
def test_get_scale_factor_vectors_with_chunking(
test_input, degree, test_fold_multiplier, test_chunks, expected_size
):
"""Verifies vectors of scale factors are calculated accurately."""
calculated_scale_factor_vectors = _get_scale_factor_vectors(
test_input, degree, test_fold_multiplier, test_chunks
)

assert len(calculated_scale_factor_vectors) == expected_size


@pytest.mark.parametrize(
"test_input, num_chunks, error_msg",
[
(test_circuit1, 0, "The number of chunks should be >= to 1."),
(
test_circuit1,
5,
"Number of chunks > the number of layers in the circuit.",
),
],
)
def test_invalid_num_chunks(test_input, num_chunks, error_msg):
"""Ensures that the number of intended chunks in the input circuit raises
an error for an invalid value."""
with pytest.raises(ValueError, match=error_msg):
_get_scale_factor_vectors(test_input, 2, 2, num_chunks)
9 changes: 9 additions & 0 deletions mitiq/zne/scaling/layer_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ def layer_folding(
) -> cirq.Circuit:
"""Applies a variable amount of folding to select layers of a circuit.

Note that this method only works for the univariate extrapolation methods.
It allows a user to choose which layers in the input circuit will be scaled
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved
The layerwise folding required for multivariate extrapolation is different
as the layers in the input circuit have to be scaled in a specific pattern.
The required specific pattern for multivariate extrapolation does not allow
a user to provide a choice of which layers to fold. If you would prefer to
use a multivariate extrapolation method for unitary
folding, use the functions available in `mitiq/lre` instead.
purva-thakre marked this conversation as resolved.
Show resolved Hide resolved

Args:
circuit: The input circuit.
layers_to_fold: A list with the index referring to the layer number,
Expand Down
Loading