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

feature: Add noise operations and methods to add noise to circuits #149

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
84dbccb
feature: Add Noise classes
yitinche Aug 17, 2020
95888bc
feature: Add is_CPTP()
yitinche Aug 17, 2020
e9a4543
feature: Add add_noise() to Circuit
yitinche Aug 17, 2020
31f5084
feature: Allow printing Noise in circuit printout
yitinche Aug 17, 2020
01c4734
feature: Add NoiseMomentsKey()
yitinche Aug 17, 2020
821a286
feature: Add DensityMatrix as a result type
yitinche Aug 17, 2020
2357f73
Add tests for features related to noise simulator
yitinche Aug 17, 2020
3898019
fix: make changes for flake8 and remove unused import
yitinche Aug 18, 2020
e3659a7
fix: Update check for 0<= probability <=1
yitinche Aug 18, 2020
b0eb845
fix: check probability >=0 and <=1
yitinche Aug 19, 2020
99c0e61
feature: Move type checking of add_noise() input var to noise_helpers.py
yitinche Aug 19, 2020
a9cf99c
Add tests to increase unit-tests coverage
yitinche Aug 19, 2020
9ebc651
fix: linters
yitinche Aug 19, 2020
08008f1
Add tests to increase unit-test coverage
yitinche Aug 19, 2020
840561e
fix: Update condition for adding noise
yitinche Aug 19, 2020
0daa312
fix: Fix docstring and type hints
yitinche Aug 24, 2020
fb0dd0d
fix: Fix docstring and variable names
yitinche Aug 25, 2020
0caa432
fix: Update IR name in test: "Probability"->"SingleProbability"
yitinche Aug 25, 2020
6388a9b
feature: Add PhaseDamping noise
yitinche Aug 25, 2020
5f3f1f0
feature: Include add_noise() to Circuit.add()
yitinche Aug 25, 2020
2e20ca5
fix: Remove NoiseInsertStrategy and default to "inclusive" behavior
yitinche Aug 26, 2020
e21dc1b
fix: Merge the target checking into _add_noise_to_gates()
yitinche Aug 26, 2020
085995b
fix: Add check for len(target_qubits)==gate.qubit_count
yitinche Aug 26, 2020
1ea8f8b
fix: docstring and format
yitinche Aug 26, 2020
86d1fff
fix: Add checks and tests
yitinche Aug 26, 2020
eb88866
fix: Remove redundant methods
yitinche Aug 26, 2020
7b169f8
fix: Change name "ProbabilityNoise"->"ProbabilisticNoise"
yitinche Aug 26, 2020
53ccdae
fix: add tests and descriptions for noises.py
yitinche Aug 26, 2020
c71a9b3
fix: Add display name of noise channels in docstring
yitinche Aug 26, 2020
7e289f3
fix: linting from black's update
yitinche Aug 27, 2020
3937d9c
fix: Add integ-tests for noise simulator
yitinche Aug 27, 2020
fc5d38c
fix: change the input type of target_gates to Gate class
yitinche Sep 1, 2020
b2f27ca
fix: docstring and downcase CPTP->cptp
yitinche Sep 1, 2020
dba7759
fix: Add check for matrices of different sizes in Noise.Kraus
yitinche Sep 1, 2020
3d190fc
fix: add NoiseMomentKey to the type hint of Moments
yitinche Sep 1, 2020
a6b12ce
fix: Avoid looping through moments when add noise
yitinche Sep 1, 2020
e955961
fix: Fix the issue mentioned in #discussion_r480512575
yitinche Sep 1, 2020
2fb5b7d
fix: change math expression to LaTeX format
yitinche Sep 2, 2020
d4d8ccd
fix: docstring
yitinche Sep 2, 2020
b04c5d5
fix: Make helper methods public
yitinche Sep 2, 2020
86aa111
Merge https://github.com/aws/amazon-braket-sdk-python into noise
yitinche Sep 9, 2020
53a5f9b
change: Add example for local noise simulation
yitinche Sep 9, 2020
4d702f3
change: Update README.md
yitinche Sep 9, 2020
e77a631
update development version to v1.1.2.dev0
Sep 9, 2020
5916077
Merge branch 'main' into noise
amazon-braket-ci-bot Sep 9, 2020
5731cdc
use device data to create device level parameter data when creating a…
ajberdy Mar 29, 2021
f3af07a
use device data to create device level parameter data when creating a…
ajberdy Mar 29, 2021
69f49ad
Merge branch 'dwave-update' into noise
speller26 May 15, 2021
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: 2 additions & 0 deletions src/braket/circuits/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

# Execute initialization code in gates module
import braket.circuits.gates as gates # noqa: F401
import braket.circuits.noises as noises # noqa: F401
import braket.circuits.observables as observables # noqa: F401
import braket.circuits.result_types as result_types # noqa: F401
from braket.circuits.angled_gate import AngledGate # noqa: F401
Expand All @@ -25,6 +26,7 @@
from braket.circuits.gate import Gate # noqa: F401
from braket.circuits.instruction import Instruction # noqa: F401
from braket.circuits.moments import Moments, MomentsKey # noqa: F401
from braket.circuits.noise import Noise # noqa: F401
from braket.circuits.observable import Observable, StandardObservable # noqa: F401
from braket.circuits.operator import Operator # noqa: F401
from braket.circuits.quantum_operator import QuantumOperator # noqa: F401
Expand Down
5 changes: 3 additions & 2 deletions src/braket/circuits/ascii_circuit_diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from braket.circuits.circuit_diagram import CircuitDiagram
from braket.circuits.gate import Gate
from braket.circuits.instruction import Instruction
from braket.circuits.noise import Noise
from braket.circuits.qubit_set import QubitSet
from braket.circuits.result_type import ResultType

Expand Down Expand Up @@ -101,8 +102,8 @@ def _ascii_group_items(
"""
groupings = []
for item in items:
# Can only print Gate operators for instructions at the moment
if isinstance(item, Instruction) and not isinstance(item.operator, Gate):
# Can only print Gate and Noise operators for instructions at the moment
if isinstance(item, Instruction) and not isinstance(item.operator, (Gate, Noise)):
yitinche marked this conversation as resolved.
Show resolved Hide resolved
continue

if isinstance(item, ResultType) and not item.target:
Expand Down
130 changes: 130 additions & 0 deletions src/braket/circuits/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@

from typing import Callable, Dict, Iterable, List, TypeVar, Union

from braket.circuits import noise_helpers
from braket.circuits.ascii_circuit_diagram import AsciiCircuitDiagram
from braket.circuits.instruction import Instruction
from braket.circuits.moments import Moments
from braket.circuits.noise import Noise
from braket.circuits.observable import Observable
from braket.circuits.observables import TensorProduct
from braket.circuits.qubit import QubitInput
Expand Down Expand Up @@ -438,6 +440,134 @@ def add_circuit(

return self

def add_noise(
self,
noise: Noise,
target_gates: Union[str, Iterable[str]] = None,
yitinche marked this conversation as resolved.
Show resolved Hide resolved
target_qubits: QubitSetInput = None,
target_times: Union[int, Iterable[int]] = None,
licedric marked this conversation as resolved.
Show resolved Hide resolved
insert_strategy: str = "inclusive",
yitinche marked this conversation as resolved.
Show resolved Hide resolved
) -> Circuit:
"""
For any parameter that is None, that specification is ignored (e.g. if 'target_gates'
yitinche marked this conversation as resolved.
Show resolved Hide resolved
and 'target_qubits' are None then the noise is added to all qubits at target_times).
If 'target_gates', 'target_qubits', and 'target_times' are all None, then 'noise' is
added to every qubit at every moment.

When `target_gates` is not None and `noise.qubit_count` > 1, `noise.qubit_count`
must be the same as `qubit_count` of gates specified by `target_gates`. When
`noise.qubit_count` == 1, ie. `noise` is single-qubit, `noise` is added
to all qubits in `target_qubits` that interact with the target gates.

Args:
noise (Noise): Noise to be added to the circuit. When `noise.qubit_count` > 1,
`noise.qubit_count` must be the same as `qubit_count` of gates specified by
`target_gates`.
target_gates (Union[str, Iterable[str], optional]): Name or List of name of gates
which `noise` is added to. If None, `noise` is added only according to
`target_qubits` and `target_times`. None should be used when users want
to add `noise` to a ciruit moment that has no gate.
target_qubits (Union[QubitSetInput, optional]): Index or indices of qubit(s). When
`target_gates` is not None, the usage of `target_qubits` is determined by
`insert_strategy`. Default=None.
target_times (Union[int, Iterable[int], optional]): Index of indices of time which
`noise` is added to. Default=None.
insert_strategy (Union[str, optional]): Rule of how `target_qubit` is used.
`insert_strategy` is used only when `target_gates` is not None.
Default="inclusive".
Options:
"strict": Insert noise to a gate when `gate.target` exactly matches
`target_qubits`. Sensitive to the order of qubits.
"inclusive": Insert noise to a gate when `gate.target` is a subset
of `target_qubits`.

Returns:
Circuit: self

Raises:
yitinche marked this conversation as resolved.
Show resolved Hide resolved
TypeError: If `noise` is not Noise type, `target_gates` is not str,
Iterable[str] or None, `target_times` is not int or Iterable[int].

Example:
yitinche marked this conversation as resolved.
Show resolved Hide resolved
>>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1)
>>> print(circ)
T : |0|1|2|
q0 : -X-Z-C-
|
q1 : -Y-X-X-
T : |0|1|2|
>>> noise = Noise.Bit_Flip(probability=0.1)
>>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1)
>>> print(circ.add_noise(noise, target_gates = 'X'))
T : | 0 | 1 |2|
q0 : -X-NB(0.1)-Z---------C-
|
q1 : -Y---------X-NB(0.1)-X-
T : | 0 | 1 |2|
>>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1)
>>> print(circ.add_noise(noise, target_qubits = 1))
T : | 0 | 1 | 2 |
q0 : -X---------Z---------C---------
|
q1 : -Y-NB(0.1)-X-NB(0.1)-X-NB(0.1)-
T : | 0 | 1 | 2 |
>>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1)
>>> print(circ.add_noise(noise, target_gates = 'X', target_qubits = 1))
T : |0| 1 |2|
q0 : -X-Z---------C-
|
q1 : -Y-X-NB(0.1)-X-
T : |0| 1 |2|
>>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1)
>>> print(circ.add_noise(noise, target_gates = ['X','Y'], target_qubits = [0,1]))
T : | 0 | 1 |2|
q0 : -X-NB(0.1)-Z---------C-
|
q1 : -Y-NB(0.1)-X-NB(0.1)-X-
T : | 0 | 1 |2|
>>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1)
>>> print(circ.add_noise(noise, target_times=[0,2]))
T : | 0 |1| 2 |
q0 : -X-NB(0.1)-Z-C-NB(0.1)-
|
q1 : -Y-NB(0.1)-X-X-NB(0.1)-
T : | 0 |1| 2 |
>>> circ = Circuit().x(0).y(1).z(0).x(1).cnot(0,1)
>>> print(circ.add_noise(noise,
... target_gates = ['X','Y'],
... target_qubits = [0,1],
... target_times=1)
... )
T : |0| 1 |2|
q0 : -X-Z---------C-
|
q1 : -Y-X-NB(0.1)-X-
T : |0| 1 |2|

"""

if isinstance(target_gates, str):
target_gates = [target_gates]
if target_qubits is None:
target_qubits = QubitSet(range(self.qubit_count))
else:
target_qubits = QubitSet(target_qubits)
if target_times is None:
target_times = range(self.depth)
if isinstance(target_times, int):
target_times = [target_times]

if not isinstance(noise, Noise):
raise TypeError("noise must be a Noise class")
yitinche marked this conversation as resolved.
Show resolved Hide resolved
if not noise_helpers.type_check_target_gates(target_gates):
raise TypeError(f"all elements in {target_gates} must be str")
if not noise_helpers.type_check_target_times(target_times):
raise TypeError("target_times must be int or Iterable[int]")

return noise_helpers._add_noise(
self, noise, target_gates, target_qubits, target_times, insert_strategy
)

def add(self, addable: AddableTypes, *args, **kwargs) -> Circuit:
yitinche marked this conversation as resolved.
Show resolved Hide resolved
"""
Generic add method for adding item(s) to self. Any arguments that
yitinche marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
48 changes: 38 additions & 10 deletions src/braket/circuits/moments.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
)

from braket.circuits.instruction import Instruction
from braket.circuits.noise import Noise
from braket.circuits.qubit import Qubit
from braket.circuits.qubit_set import QubitSet

Expand All @@ -35,6 +36,14 @@ class MomentsKey(NamedTuple):
qubits: QubitSet


class NoiseMomentsKey(NamedTuple):
"""Key of the Moments mapping for Noise."""

time: int
qubits: QubitSet
noise_index: int


class Moments(Mapping[MomentsKey, Instruction]):
"""
An ordered mapping of `MomentsKey` to `Instruction`. The core data structure that
Expand Down Expand Up @@ -138,16 +147,35 @@ def add(self, instructions: Iterable[Instruction]) -> None:
self._add(instruction)

def _add(self, instruction: Instruction) -> None:
qubit_range = instruction.target
time = max([self._max_time_for_qubit(qubit) for qubit in qubit_range]) + 1

# Mark all qubits in qubit_range with max_time
for qubit in qubit_range:
self._max_times[qubit] = max(time, self._max_time_for_qubit(qubit))

self._moments[MomentsKey(time, instruction.target)] = instruction
self._qubits.update(instruction.target)
self._depth = max(self._depth, time + 1)
if isinstance(instruction.operator, Noise):
qubit_range = instruction.target
time = max(0, *[self._max_time_for_qubit(qubit) for qubit in qubit_range])

# Find the maximum noise_index at the time. The new noise is added to
# noise_index = maximum noise_index + 1.
max_noise_index = max(
[0,]
+ [
k.noise_index
for k in self._moments
yitinche marked this conversation as resolved.
Show resolved Hide resolved
if (k.time == time and isinstance(k, NoiseMomentsKey))
]
)

noise_key = NoiseMomentsKey(time, instruction.target, max_noise_index + 1)
self._moments[noise_key] = instruction
yitinche marked this conversation as resolved.
Show resolved Hide resolved
self._qubits.update(instruction.target)
else:
qubit_range = instruction.target
time = max([self._max_time_for_qubit(qubit) for qubit in qubit_range]) + 1

# Mark all qubits in qubit_range with max_time
for qubit in qubit_range:
self._max_times[qubit] = max(time, self._max_time_for_qubit(qubit))

self._moments[MomentsKey(time, instruction.target)] = instruction
self._qubits.update(instruction.target)
self._depth = max(self._depth, time + 1)

def _max_time_for_qubit(self, qubit: Qubit) -> int:
return self._max_times.get(qubit, -1)
Expand Down
140 changes: 140 additions & 0 deletions src/braket/circuits/noise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from typing import Any, List, Sequence

from braket.circuits.quantum_operator import QuantumOperator
from braket.circuits.qubit_set import QubitSet


class Noise(QuantumOperator):
"""
Class `Noise` represents a noise channel that operates on one or multiple qubits. Noise
are considered as building blocks of quantum circuits that simulate noise. It can be
used as an operator in an Instruction() object. It appears in the diagram when user prints
yitinche marked this conversation as resolved.
Show resolved Hide resolved
a circuit with Noise. This class is considered the noise channel definition containing
the metadata that defines what a noise channel is and what it does.
"""

def __init__(
self, qubit_count: int, ascii_symbols: Sequence[str],
):
"""
Args:
qubit_count (int): Number of qubits this noise channel interacts with.
ascii_symbols (Sequence[str]): ASCII string symbols for this noise channel. These
are used when printing a diagram of circuits. Length must be the same as
`qubit_count`, and index ordering is expected to correlate with target ordering
on the instruction.

Raises:
ValueError: `qubit_count` is less than 1, `ascii_symbols` are None, or
`ascii_symbols` length != `qubit_count`
"""
if qubit_count < 1:
raise ValueError(f"qubit_count, {qubit_count}, must be greater than zero")
self._qubit_count = qubit_count

if ascii_symbols is None:
raise ValueError("ascii_symbols must not be None")

if len(ascii_symbols) != qubit_count:
msg = f"ascii_symbols, {ascii_symbols}, length must equal qubit_count, {qubit_count}"
raise ValueError(msg)
self._ascii_symbols = tuple(ascii_symbols)

@property
def qubit_count(self) -> int:
"""int: Returns number of qubits this quantum operator interacts with."""
return self._qubit_count

@property
def ascii_symbols(self) -> List[str]:
"""List[str]: Returns the ascii symbols for the quantum operator."""
return self._ascii_symbols
yitinche marked this conversation as resolved.
Show resolved Hide resolved

@property
def name(self) -> str:
"""
Returns the name of the quantum operator

Returns:
The name of the quantum operator as a string
"""
return self.__class__.__name__

def to_ir(self, target: QubitSet) -> Any:
"""Returns IR object of quantum operator and target

Args:
target (QubitSet): target qubit(s)
Returns:
IR object of the quantum operator and target
"""
raise NotImplementedError("to_ir has not been implemented yet.")

def to_matrix(self, *args, **kwargs) -> Any:
"""Returns a list of matrices defining the Kraus matrices of
the noise channel.

Returns:
Iterable[np.ndarray]: list of matrices defining the Kraus
matrices of the noise channel.
"""
raise NotImplementedError("to_matrix has not been implemented yet.")

def __eq__(self, other):
if isinstance(other, Noise):
return self.name == other.name
return NotImplemented

def __repr__(self):
return f"{self.name}('qubit_count': {self.qubit_count})"

@classmethod
def register_noise(cls, noise: "Noise"):
"""Register a noise implementation by adding it into the Noise class.

Args:
noise (Noise): Noise class to register.
"""
setattr(cls, noise.__name__, noise)


class ProbabilityNoise(Noise):
yitinche marked this conversation as resolved.
Show resolved Hide resolved
"""
Class `ProbabilityNoise` represents a noise channel that operates on N qubits
and is parameterized by a probability.
yitinche marked this conversation as resolved.
Show resolved Hide resolved
"""

def __init__(self, probability: float, qubit_count: int, ascii_symbols: Sequence[str]):
"""
Args:
probability (float): The probability of noise, a parameter that generates Kraus
matrices.
qubit_count (int): The number of qubits that this noise interacts with.
ascii_symbols (Sequence[str]): ASCII string symbols for the noise. These are used when
printing a diagram of a circuit. The length must be the same as `qubit_count`, and
index ordering is expected to correlate with the target ordering on the instruction.

Raises:
ValueError: If the `qubit_count` is less than 1, `ascii_symbols` are `None`, or
`ascii_symbols` length != `qubit_count`, `probability` is not `float`,
`probability`>1.0, or `probability`<0.0
"""
super().__init__(qubit_count=qubit_count, ascii_symbols=ascii_symbols)

if not isinstance(probability, float):
raise ValueError("probability must be float type")
if not (probability <= 1.0 and probability >= 0.0):
raise ValueError("probability must be a real number in the interval [0,1]")
self._probability = probability

@property
def probability(self) -> float:
""" Returns the probability parameter for the noise.

Returns:
probability (float): The probability that parameterizes the Kraus matrices.
"""
return self._probability

def __repr__(self):
return f"{self.name}('probability': {self.probability}, 'qubit_count': {self.qubit_count})"
Loading