From b095710bcd0d1deb305287eaf2d10e7b81ee76f0 Mon Sep 17 00:00:00 2001 From: Cody Wang Date: Tue, 20 Aug 2024 13:36:03 -0700 Subject: [PATCH] feat: Allow early qubit binding of observables (#1022) --- examples/bell_result_types.py | 10 +- examples/hybrid_job.py | 4 +- examples/hybrid_job_script.py | 4 +- examples/local_noise_simulation.py | 4 +- src/braket/circuits/observable.py | 56 +++-- src/braket/circuits/observables.py | 204 ++++++++++++------ src/braket/circuits/result_types.py | 71 +++--- .../braket/circuits/test_observables.py | 91 ++++++-- 8 files changed, 311 insertions(+), 133 deletions(-) diff --git a/examples/bell_result_types.py b/examples/bell_result_types.py index 2bcb87b79..9271d09fc 100644 --- a/examples/bell_result_types.py +++ b/examples/bell_result_types.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from braket.circuits import Circuit, Observable +from braket.circuits import Circuit, observables from braket.devices import LocalSimulator device = LocalSimulator() @@ -24,7 +24,7 @@ .h(0) .cnot(0, 1) .probability(target=[0]) - .expectation(observable=Observable.Z(), target=[1]) + .expectation(observable=observables.Z(1)) .amplitude(state=["00"]) .state_vector() ) @@ -45,9 +45,9 @@ Circuit() .h(0) .cnot(0, 1) - .expectation(observable=Observable.Y() @ Observable.X(), target=[0, 1]) - .variance(observable=Observable.Y() @ Observable.X(), target=[0, 1]) - .sample(observable=Observable.Y() @ Observable.X(), target=[0, 1]) + .expectation(observable=observables.Y(0) @ observables.X(1)) + .variance(observable=observables.Y(0) @ observables.X(1)) + .sample(observable=observables.Y(0) @ observables.X(1)) ) # When shots>0 for a simulator, probability, expectation, variance are calculated from measurements diff --git a/examples/hybrid_job.py b/examples/hybrid_job.py index 7f54c9955..545c0505c 100644 --- a/examples/hybrid_job.py +++ b/examples/hybrid_job.py @@ -12,7 +12,7 @@ # language governing permissions and limitations under the License. from braket.aws import AwsDevice -from braket.circuits import Circuit, FreeParameter, Observable +from braket.circuits import Circuit, FreeParameter, observables from braket.devices import Devices from braket.jobs import get_job_device_arn, hybrid_job from braket.jobs.metrics import log_metric @@ -34,7 +34,7 @@ def run_hybrid_job(num_tasks=1): circ = Circuit() circ.rx(0, FreeParameter("theta")) circ.cnot(0, 1) - circ.expectation(observable=Observable.X(), target=0) + circ.expectation(observable=observables.X(0)) # initial parameter theta = 0.0 diff --git a/examples/hybrid_job_script.py b/examples/hybrid_job_script.py index b544ff9df..f7db6df94 100644 --- a/examples/hybrid_job_script.py +++ b/examples/hybrid_job_script.py @@ -13,7 +13,7 @@ from braket.aws import AwsDevice, AwsQuantumJob -from braket.circuits import Circuit, FreeParameter, Observable +from braket.circuits import Circuit, FreeParameter, observables from braket.devices import Devices from braket.jobs import get_job_device_arn, save_job_result from braket.jobs.metrics import log_metric @@ -27,7 +27,7 @@ def run_hybrid_job(num_tasks: int): circ = Circuit() circ.rx(0, FreeParameter("theta")) circ.cnot(0, 1) - circ.expectation(observable=Observable.X(), target=0) + circ.expectation(observable=observables.X(0)) # initial parameter theta = 0.0 diff --git a/examples/local_noise_simulation.py b/examples/local_noise_simulation.py index 0857bfd0c..8c173dd51 100644 --- a/examples/local_noise_simulation.py +++ b/examples/local_noise_simulation.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from braket.circuits import Circuit, Noise +from braket.circuits import Circuit, noises from braket.devices import LocalSimulator device = LocalSimulator("braket_dm") @@ -23,7 +23,7 @@ circuit = Circuit().x(0).x(1) -noise = Noise.BitFlip(probability=0.1) +noise = noises.BitFlip(probability=0.1) circuit.apply_gate_noise(noise) print("Second example: ") print(circuit) diff --git a/src/braket/circuits/observable.py b/src/braket/circuits/observable.py index d3f3fc862..11ed02eb6 100644 --- a/src/braket/circuits/observable.py +++ b/src/braket/circuits/observable.py @@ -16,7 +16,6 @@ import numbers from collections.abc import Sequence from copy import deepcopy -from typing import Union import numpy as np @@ -27,7 +26,7 @@ OpenQASMSerializationProperties, SerializationProperties, ) -from braket.registers.qubit_set import QubitSet +from braket.registers import QubitInput, QubitSet, QubitSetInput class Observable(QuantumOperator): @@ -37,23 +36,36 @@ class Observable(QuantumOperator): `ResultType.Expectation` to specify the measurement basis. """ - def __init__(self, qubit_count: int, ascii_symbols: Sequence[str]): + def __init__( + self, qubit_count: int, ascii_symbols: Sequence[str], targets: QubitSetInput | None = None + ): super().__init__(qubit_count=qubit_count, ascii_symbols=ascii_symbols) + if targets is not None: + targets = QubitSet(targets) + if (num_targets := len(targets)) != qubit_count: + raise ValueError( + f"Length of target {num_targets} does not match qubit count {qubit_count}" + ) + self._targets = targets + else: + self._targets = None self._coef = 1 def _unscaled(self) -> Observable: - return Observable(qubit_count=self.qubit_count, ascii_symbols=self.ascii_symbols) + return Observable( + qubit_count=self.qubit_count, ascii_symbols=self.ascii_symbols, targets=self.targets + ) def to_ir( self, - target: QubitSet | None = None, + target: QubitSetInput | None = None, ir_type: IRType = IRType.JAQCD, serialization_properties: SerializationProperties | None = None, - ) -> Union[str, list[Union[str, list[list[list[float]]]]]]: + ) -> str | list[str | list[list[list[float]]]]: """Returns the IR representation for the observable Args: - target (QubitSet | None): target qubit(s). Defaults to None. + target (QubitSetInput | None): target qubit(s). Defaults to None. ir_type(IRType) : The IRType to use for converting the result type object to its IR representation. Defaults to IRType.JAQCD. serialization_properties (SerializationProperties | None): The serialization properties @@ -61,7 +73,7 @@ def to_ir( properties supplied must correspond to the supplied `ir_type`. Defaults to None. Returns: - Union[str, list[Union[str, list[list[list[float]]]]]]: The IR representation for + str | list[str | list[list[list[float]]]]: The IR representation for the observable. Raises: @@ -84,27 +96,35 @@ def to_ir( else: raise ValueError(f"Supplied ir_type {ir_type} is not supported.") - def _to_jaqcd(self) -> list[Union[str, list[list[list[float]]]]]: + def _to_jaqcd(self) -> list[str | list[list[list[float]]]]: """Returns the JAQCD representation of the observable.""" raise NotImplementedError("to_jaqcd has not been implemented yet.") def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, - target: QubitSet | None = None, + targets: QubitSetInput | None = None, ) -> str: """Returns the openqasm string representation of the result type. Args: serialization_properties (OpenQASMSerializationProperties): The serialization properties to use while serializing the object to the IR representation. - target (QubitSet | None): target qubit(s). Defaults to None. + targets (QubitSetInput | None): target qubit(s). Defaults to None. Returns: str: Representing the openqasm representation of the result type. """ raise NotImplementedError("to_openqasm has not been implemented yet.") + @property + def targets(self) -> QubitSet | None: + """QubitSet | None: The target qubits of this observable + + If `None`, this is provided by the enclosing result type. + """ + return self._targets + @property def coefficient(self) -> int: """The coefficient of the observable. @@ -185,7 +205,11 @@ def __sub__(self, other: Observable): return self + (-1 * other) def __repr__(self) -> str: - return f"{self.name}('qubit_count': {self.qubit_count})" + return ( + f"{self.name}('qubit_count': {self._qubit_count})" + if self._targets is None + else f"{self.name}('qubit_count': {self._qubit_count}, 'target': {self._targets})" + ) def __eq__(self, other: Observable) -> bool: if isinstance(other, Observable): @@ -198,8 +222,12 @@ class StandardObservable(Observable): eigenvalues of (+1, -1). """ - def __init__(self, ascii_symbols: Sequence[str]): - super().__init__(qubit_count=1, ascii_symbols=ascii_symbols) + def __init__(self, ascii_symbols: Sequence[str], target: QubitInput | None = None): + super().__init__( + qubit_count=1, + ascii_symbols=ascii_symbols, + targets=[target] if target is not None else None, + ) self._eigenvalues = (1.0, -1.0) # immutable def _unscaled(self) -> StandardObservable: diff --git a/src/braket/circuits/observables.py b/src/braket/circuits/observables.py index ae4f36a09..9346435a2 100644 --- a/src/braket/circuits/observables.py +++ b/src/braket/circuits/observables.py @@ -30,20 +30,28 @@ verify_quantum_operator_matrix_dimensions, ) from braket.circuits.serialization import IRType, OpenQASMSerializationProperties -from braket.registers.qubit_set import QubitSet +from braket.registers import QubitInput, QubitSet, QubitSetInput class H(StandardObservable): """Hadamard operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.H() + def __init__(self, target: QubitInput | None = None): + """Initializes Hadamard observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.H(0) + >>> observables.H() """ - super().__init__(ascii_symbols=["H"]) + super().__init__(ascii_symbols=["H"], target=target) def _unscaled(self) -> StandardObservable: - return H() + return H(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -54,11 +62,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}h({qubit_target})" - else: - return f"{coef_prefix}h all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}h({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}h all" def to_matrix(self) -> np.ndarray: return self.coefficient * ( @@ -76,14 +84,22 @@ def basis_rotation_gates(self) -> tuple[Gate, ...]: class I(Observable): # noqa: E742 """Identity operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.I() + def __init__(self, target: QubitInput | None = None): + """Initializes Identity observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.I(0) + >>> observables.I() """ - super().__init__(qubit_count=1, ascii_symbols=["I"]) + super().__init__(qubit_count=1, ascii_symbols=["I"], targets=target) def _unscaled(self) -> Observable: - return I() + return I(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -94,11 +110,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}i({qubit_target})" - else: - return f"{coef_prefix}i all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}i({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}i all" def to_matrix(self) -> np.ndarray: return self.coefficient * np.eye(2, dtype=complex) @@ -126,14 +142,22 @@ def eigenvalue(self, index: int) -> float: class X(StandardObservable): """Pauli-X operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.X() + def __init__(self, target: QubitInput | None = None): + """Initializes Pauli-X observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.X(0) + >>> observables.X() """ - super().__init__(ascii_symbols=["X"]) + super().__init__(ascii_symbols=["X"], target=target) def _unscaled(self) -> StandardObservable: - return X() + return X(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -144,11 +168,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}x({qubit_target})" - else: - return f"{coef_prefix}x all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}x({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}x all" def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) @@ -164,14 +188,22 @@ def basis_rotation_gates(self) -> tuple[Gate, ...]: class Y(StandardObservable): """Pauli-Y operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.Y() + def __init__(self, target: QubitInput | None = None): + """Initializes Pauli-Y observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.Y(0) + >>> observables.Y() """ - super().__init__(ascii_symbols=["Y"]) + super().__init__(ascii_symbols=["Y"], target=target) def _unscaled(self) -> StandardObservable: - return Y() + return Y(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -182,11 +214,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}y({qubit_target})" - else: - return f"{coef_prefix}y all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}y({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}y all" def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) @@ -202,14 +234,22 @@ def basis_rotation_gates(self) -> tuple[Gate, ...]: class Z(StandardObservable): """Pauli-Z operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.Z() + def __init__(self, target: QubitInput | None = None): + """Initializes Pauli-Z observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.Z(0) + >>> observables.Z() """ - super().__init__(ascii_symbols=["Z"]) + super().__init__(ascii_symbols=["Z"], target=target) def _unscaled(self) -> StandardObservable: - return Z() + return Z(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -220,11 +260,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}z({qubit_target})" - else: - return f"{coef_prefix}z all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}z({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}z all" def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex) @@ -247,13 +287,13 @@ def __init__(self, observables: list[Observable]): observables (list[Observable]): List of observables for tensor product Examples: - >>> t1 = Observable.Y() @ Observable.X() + >>> t1 = Observable.Y(0) @ Observable.X(1) >>> t1.to_matrix() array([[0.+0.j, 0.+0.j, 0.-0.j, 0.-1.j], [0.+0.j, 0.+0.j, 0.-1.j, 0.-0.j], [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.+1.j, 0.+0.j, 0.+0.j, 0.+0.j]]) - >>> t2 = Observable.Z() @ t1 + >>> t2 = Observable.Z(3) @ t1 >>> t2.factors (Z('qubit_count': 1), Y('qubit_count': 1), X('qubit_count': 1)) @@ -281,7 +321,22 @@ def __init__(self, observables: list[Observable]): f"{coefficient if coefficient != 1 else ''}" f"{'@'.join([obs.ascii_symbols[0] for obs in unscaled_factors])}" ) - super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count) + all_targets = [factor.targets for factor in unscaled_factors] + if all(targets is None for targets in all_targets): + merged_targets = None + elif all(targets is not None for targets in all_targets): + flat_targets = [qubit for target in all_targets for qubit in target] + merged_targets = QubitSet(flat_targets) + if len(merged_targets) != len(flat_targets): + raise ValueError("Cannot have repeated target qubits") + else: + raise ValueError("Cannot mix factors with and without targets") + + super().__init__( + qubit_count=qubit_count, + ascii_symbols=[display_name] * qubit_count, + targets=merged_targets, + ) self._coef = coefficient self._factors = unscaled_factors self._factor_dimensions = tuple( @@ -316,7 +371,7 @@ def _to_openqasm( ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" factors = [] - use_qubits = iter(target) + use_qubits = iter(target or self._targets) for obs in self._factors: obs_target = QubitSet() num_qubits = int(np.log2(obs.to_matrix().shape[0])) @@ -438,7 +493,7 @@ def __init__(self, observables: list[Observable], display_name: str = "Hamiltoni observable for circuit diagrams. Defaults to `Hamiltonian`. Examples: - >>> t1 = -3 * Observable.Y() + 2 * Observable.X() + >>> t1 = -3 * Observable.Y(0) + 2 * Observable.X(0) Sum(X('qubit_count': 1), Y('qubit_count': 1)) >>> t1.summands (X('qubit_count': 1), Y('qubit_count': 1)) @@ -452,7 +507,15 @@ def __init__(self, observables: list[Observable], display_name: str = "Hamiltoni self._summands = tuple(flattened_observables) qubit_count = max(flattened_observables, key=lambda obs: obs.qubit_count).qubit_count + all_targets = [observable.targets for observable in flattened_observables] + if all(targets is None for targets in all_targets): + targets = None + elif all(targets is not None for targets in all_targets): + targets = all_targets + else: + raise ValueError("Cannot mix terms with and without targets") super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count) + self._targets = targets def __mul__(self, other: numbers.Number) -> Observable: """Scalar multiplication""" @@ -469,8 +532,9 @@ def _to_jaqcd(self) -> list[str]: def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, - target: list[QubitSet] = None, + target: list[QubitSetInput] = None, ) -> str: + target = target or self._targets if len(self.summands) != len(target): raise ValueError( f"Invalid target of length {len(target)} for Sum with {len(self.summands)} terms" @@ -529,21 +593,30 @@ class Hermitian(Observable): # Cache of eigenpairs _eigenpairs: ClassVar = {} - def __init__(self, matrix: np.ndarray, display_name: str = "Hermitian"): + def __init__( + self, + matrix: np.ndarray, + display_name: str = "Hermitian", + targets: QubitSetInput | None = None, + ): """Inits a `Hermitian`. Args: matrix (np.ndarray): Hermitian matrix that defines the observable. display_name (str): Name to use for an instance of this Hermitian matrix observable for circuit diagrams. Defaults to `Hermitian`. + targets (QubitSetInput | None): The target qubits to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. Raises: ValueError: If `matrix` is not a two-dimensional square matrix, - or has a dimension length that is not a positive power of 2, - or is not Hermitian. + is not Hermitian, or has a dimension length that is either not a positive power of 2 + or, if targets is supplied, doesn't match the size of targets. Examples: - >>> Observable.Hermitian(matrix=np.array([[0, 1],[1, 0]])) + >>> observables.Hermitian(matrix=np.array([[0, 1],[1, 0]]), targets=[0]) + >>> observables.Hermitian(matrix=np.array([[0, 1],[1, 0]])) """ verify_quantum_operator_matrix_dimensions(matrix) self._matrix = np.array(matrix, dtype=complex) @@ -557,10 +630,14 @@ def __init__(self, matrix: np.ndarray, display_name: str = "Hermitian"): Gate.Unitary(matrix=eigendecomposition["eigenvectors"].conj().T), ) - super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count) + super().__init__( + qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count, targets=targets + ) def _unscaled(self) -> Observable: - return Hermitian(matrix=self._matrix, display_name=self.ascii_symbols[0]) + return Hermitian( + matrix=self._matrix, display_name=self.ascii_symbols[0], targets=self._targets + ) def _to_jaqcd(self) -> list[list[list[list[float]]]]: if self.coefficient != 1: @@ -573,6 +650,7 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" + target = target or self._targets if target: qubit_target = ", ".join( [serialization_properties.format_target(int(t)) for t in target] diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index 325fa8f46..f682b9ba1 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -96,7 +96,7 @@ def __init__(self, target: QubitSetInput | None = None): full density matrix is returned. Examples: - >>> ResultType.DensityMatrix(target=[0, 1]) + >>> result_types.DensityMatrix(target=[0, 1]) """ self._target = QubitSet(target) ascii_symbols = ["DensityMatrix"] * len(self._target) if self._target else ["DensityMatrix"] @@ -198,14 +198,15 @@ def __init__( Examples: - >>> ResultType.AdjointGradient(observable=Observable.Z(), + >>> result_types.AdjointGradient(observable=observables.Z(0), + parameters=["alpha", "beta"]) + >>> result_types.AdjointGradient(observable=observables.Z(), target=0, parameters=["alpha", "beta"]) - >>> tensor_product = Observable.Y() @ Observable.Z() - >>> hamiltonian = Observable.Y() @ Observable.Z() + Observable.H() - >>> ResultType.AdjointGradient( + >>> tensor_product = observables.Y(0) @ observables.Z(1) + >>> hamiltonian = observables.Y(0) @ observables.Z(1) + observables.H(0) + >>> result_types.AdjointGradient( >>> observable=tensor_product, - >>> target=[[0, 1], [2]], >>> parameters=["alpha", "beta"], >>> ) """ @@ -261,7 +262,7 @@ def adjoint_gradient( Examples: >>> alpha, beta = FreeParameter('alpha'), FreeParameter('beta') >>> circ = Circuit().h(0).h(1).rx(0, alpha).yy(0, 1, beta).adjoint_gradient( - >>> observable=Observable.Z(), target=[0], parameters=[alpha, beta] + >>> observable=observables.Z(0), parameters=[alpha, beta] >>> ) """ return ResultType.AdjointGradient( @@ -288,7 +289,7 @@ def __init__(self, state: list[str]): state is not a list of strings of '0' and '1' Examples: - >>> ResultType.Amplitude(state=['01', '10']) + >>> result_types.Amplitude(state=['01', '10']) """ if ( not state @@ -367,7 +368,7 @@ def __init__(self, target: QubitSetInput | None = None): circuit. Examples: - >>> ResultType.Probability(target=[0, 1]) + >>> result_types.Probability(target=[0, 1]) """ self._target = QubitSet(target) ascii_symbols = ["Probability"] * len(self._target) if self._target else ["Probability"] @@ -453,16 +454,18 @@ def __init__(self, observable: Observable, target: QubitSetInput | None = None): Args: observable (Observable): the observable for the result type - target (QubitSetInput | None): Target qubits that the - result type is requested for. Default is `None`, which means the observable must - operate only on 1 qubit and it is applied to all qubits in parallel. - + target (QubitSetInput | None): Target qubits that the result type is requested for. + If not provided, the observable's target will be used instead. If neither exist, + then it is applied to all qubits in parallel; in this case the observable must + operate only on 1 qubit. + Default: `None`. Examples: - >>> ResultType.Expectation(observable=Observable.Z(), target=0) + >>> result_types.Expectation(observable=observables.Z(0)) + >>> result_types.Expectation(observable=observables.Z(), target=0) - >>> tensor_product = Observable.Y() @ Observable.Z() - >>> ResultType.Expectation(observable=tensor_product, target=[0, 1]) + >>> tensor_product = observables.Y(0) @ observables.Z(1) + >>> result_types.Expectation(observable=tensor_product) """ super().__init__( ascii_symbols=[f"Expectation({obs_ascii})" for obs_ascii in observable.ascii_symbols], @@ -501,7 +504,7 @@ def expectation(observable: Observable, target: QubitSetInput | None = None) -> ResultType: expectation as a requested result type Examples: - >>> circ = Circuit().expectation(observable=Observable.Z(), target=0) + >>> circ = Circuit().expectation(observable=observables.Z(0)) """ return ResultType.Expectation(observable=observable, target=target) @@ -526,15 +529,18 @@ def __init__(self, observable: Observable, target: QubitSetInput | None = None): Args: observable (Observable): the observable for the result type - target (QubitSetInput | None): Target qubits that the - result type is requested for. Default is `None`, which means the observable must - operate only on 1 qubit and it is applied to all qubits in parallel. + target (QubitSetInput | None): Target qubits that the result type is requested for. + If not provided, the observable's target will be used instead. If neither exist, + then it is applied to all qubits in parallel; in this case the observable must + operate only on 1 qubit. + Default: `None`. Examples: - >>> ResultType.Sample(observable=Observable.Z(), target=0) + >>> result_types.Sample(observable=observables.Z(0)) + >>> result_types.Sample(observable=observables.Z(), target=0) - >>> tensor_product = Observable.Y() @ Observable.Z() - >>> ResultType.Sample(observable=tensor_product, target=[0, 1]) + >>> tensor_product = observables.Y(0) @ observables.Z(1) + >>> result_types.Sample(observable=tensor_product) """ super().__init__( ascii_symbols=[f"Sample({obs_ascii})" for obs_ascii in observable.ascii_symbols], @@ -573,7 +579,7 @@ def sample(observable: Observable, target: QubitSetInput | None = None) -> Resul ResultType: sample as a requested result type Examples: - >>> circ = Circuit().sample(observable=Observable.Z(), target=0) + >>> circ = Circuit().sample(observable=observables.Z(0)) """ return ResultType.Sample(observable=observable, target=target) @@ -599,19 +605,22 @@ def __init__(self, observable: Observable, target: QubitSetInput | None = None): Args: observable (Observable): the observable for the result type - target (QubitSetInput | None): Target qubits that the - result type is requested for. Default is `None`, which means the observable must - operate only on 1 qubit and it is applied to all qubits in parallel. + target (QubitSetInput | None): Target qubits that the result type is requested for. + If not provided, the observable's target will be used instead. If neither exist, + then it is applied to all qubits in parallel; in this case the observable must + operate only on 1 qubit. + Default: `None`. Raises: ValueError: If the observable's qubit count does not equal the number of target qubits, or if `target=None` and the observable's qubit count is not 1. Examples: - >>> ResultType.Variance(observable=Observable.Z(), target=0) + >>> result_types.Variance(observable=observables.Z(0)) + >>> result_types.Variance(observable=observables.Z(), target=0) - >>> tensor_product = Observable.Y() @ Observable.Z() - >>> ResultType.Variance(observable=tensor_product, target=[0, 1]) + >>> tensor_product = observables.Y(0) @ observables.Z(1) + >>> result_types.Variance(observable=tensor_product) """ super().__init__( ascii_symbols=[f"Variance({obs_ascii})" for obs_ascii in observable.ascii_symbols], @@ -650,7 +659,7 @@ def variance(observable: Observable, target: QubitSetInput | None = None) -> Res ResultType: variance as a requested result type Examples: - >>> circ = Circuit().variance(observable=Observable.Z(), target=0) + >>> circ = Circuit().variance(observable=observables.Z(0)) """ return ResultType.Variance(observable=observable, target=target) diff --git a/test/unit_tests/braket/circuits/test_observables.py b/test/unit_tests/braket/circuits/test_observables.py index b6430d4d8..213a29705 100644 --- a/test/unit_tests/braket/circuits/test_observables.py +++ b/test/unit_tests/braket/circuits/test_observables.py @@ -61,100 +61,116 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv @pytest.mark.parametrize( - "observable, serialization_properties, target, expected_ir", + "observable, observable_with_targets, serialization_properties, target, expected_ir", [ ( Observable.I(), + Observable.I(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "i(q[3])", ), ( Observable.I(), + Observable.I(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "i($3)", ), ( Observable.I(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "i all", ), ( Observable.X(), + Observable.X(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "x(q[3])", ), ( Observable.X(), + Observable.X(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "x($3)", ), ( Observable.X(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "x all", ), ( Observable.Y(), + Observable.Y(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "y(q[3])", ), ( Observable.Y(), + Observable.Y(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "y($3)", ), ( Observable.Y(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "y all", ), ( Observable.Z(), + Observable.Z(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "z(q[3])", ), ( Observable.Z(), + Observable.Z(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "z($3)", ), ( Observable.Z(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "z all", ), ( Observable.H(), + Observable.H(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "h(q[3])", ), ( Observable.H(), + Observable.H(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "h($3)", ), ( Observable.H(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "h all", ), ( Observable.Hermitian(np.eye(4)), + Observable.Hermitian(np.eye(4), targets=[1, 2]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [1, 2], "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " @@ -162,6 +178,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv ), ( Observable.Hermitian(np.eye(4)), + Observable.Hermitian(np.eye(4), targets=[1, 2]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [1, 2], "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " @@ -169,36 +186,42 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv ), ( Observable.Hermitian(np.eye(2)), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "hermitian([[1+0im, 0im], [0im, 1+0im]]) all", ), ( Observable.H() @ Observable.Z(), + Observable.H(3) @ Observable.Z(0), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0], "h(q[3]) @ z(q[0])", ), ( Observable.H() @ Observable.Z(), + Observable.H(3) @ Observable.Z(0), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3, 0], "h($3) @ z($0)", ), ( Observable.H() @ Observable.Z() @ Observable.I(), + Observable.H(3) @ Observable.Z(0) @ Observable.I(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0, 1], "h(q[3]) @ z(q[0]) @ i(q[1])", ), ( Observable.H() @ Observable.Z() @ Observable.I(), + Observable.H(3) @ Observable.Z(0) @ Observable.I(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3, 0, 1], "h($3) @ z($0) @ i($1)", ), ( Observable.Hermitian(np.eye(4)) @ Observable.I(), + Observable.Hermitian(np.eye(4), targets=[3, 0]) @ Observable.I(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0, 1], "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " @@ -207,32 +230,23 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv ), ( Observable.I() @ Observable.Hermitian(np.eye(4)), + Observable.I(3) @ Observable.Hermitian(np.eye(4), targets=[0, 1]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3, 0, 1], "i($3) @ " "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " "[0im, 0im, 1+0im, 0im], [0im, 0im, 0im, 1+0im]]) $0, $1", ), - ( - (2 * Observable.Z()) @ (3 * Observable.H()), - OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), - [3, 3], - "6 * z($3) @ h($3)", - ), - ( - (2 * Observable.Z()) @ (3 * Observable.H()) @ (2 * Observable.Y()), - OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), - [3, 3, 1], - "12 * z($3) @ h($3) @ y($1)", - ), ( 3 * (2 * Observable.Z()), + 3 * (2 * Observable.Z(3)), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "6 * z($3)", ), ( (2 * Observable.I()) @ (2 * Observable.Hermitian(np.eye(4))), + (2 * Observable.I(3)) @ (2 * Observable.Hermitian(np.eye(4), targets=[0, 1])), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3, 0, 1], "4 * i($3) @ " @@ -241,55 +255,84 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv ), ( Observable.Z() + 2 * Observable.H(), + Observable.Z(3) + 2 * Observable.H(4), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[3], [4]], "z($3) + 2 * h($4)", ), ( 3 * (Observable.H() + 2 * Observable.X()), + 3 * (Observable.H(3) + 2 * Observable.X(0)), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[3], [0]], "3 * h($3) + 6 * x($0)", ), ( 3 * (Observable.H() + 2 * Observable.H()), + 3 * (Observable.H(3) + 2 * Observable.H(3)), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[3], [3]], "3 * h($3) + 6 * h($3)", ), ( 3 * (Observable.H() + 2 * Observable.H()), + 3 * (Observable.H(3) + 2 * Observable.H(5)), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[3], [5]], "3 * h($3) + 6 * h($5)", ), ( (2 * Observable.Y()) @ (3 * Observable.I()) + 0.75 * Observable.Y() @ Observable.Z(), + (2 * Observable.Y(0)) @ (3 * Observable.I(1)) + + 0.75 * Observable.Y(0) @ Observable.Z(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[0, 1], [0, 1]], "6 * y($0) @ i($1) + 0.75 * y($0) @ z($1)", ), ( (-2 * Observable.Y()) @ (3 * Observable.I()) + -0.75 * Observable.Y() @ Observable.Z(), + (-2 * Observable.Y(0)) @ (3 * Observable.I(1)) + + -0.75 * Observable.Y(0) @ Observable.Z(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[0, 1], [0, 1]], "-6 * y($0) @ i($1) - 0.75 * y($0) @ z($1)", ), ( 4 * (2 * Observable.Z() + 2 * (3 * Observable.X() @ (2 * Observable.Y()))), + 4 * (2 * Observable.Z(0) + 2 * (3 * Observable.X(1) @ (2 * Observable.Y(2)))), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[0], [1, 2]], "8 * z($0) + 48 * x($1) @ y($2)", ), + ( + 4 * (2 * Observable.Z(0) + 2 * (3 * Observable.X(1) @ (2 * Observable.Y(2)))), + None, + OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), + [[5], [4, 3]], + "8 * z($5) + 48 * x($4) @ y($3)", + ), ], ) -def test_observables_to_ir_openqasm(observable, serialization_properties, target, expected_ir): +def test_observables_to_ir_openqasm( + observable, + observable_with_targets, + serialization_properties, + target, + expected_ir, +): assert ( observable.to_ir( target, ir_type=IRType.OPENQASM, serialization_properties=serialization_properties ) == expected_ir ) + if observable_with_targets: + assert ( + observable_with_targets.to_ir( + None, ir_type=IRType.OPENQASM, serialization_properties=serialization_properties + ) + == expected_ir + ) @pytest.mark.parametrize( @@ -458,6 +501,11 @@ def test_hermitian_eigenvalues(matrix, eigenvalues): compare_eigenvalues(Observable.Hermitian(matrix=matrix), eigenvalues) +def test_hermitian_matrix_target_mismatch(): + with pytest.raises(ValueError): + Observable.Hermitian(np.eye(4), targets=[0, 1, 2]) + + def test_flattened_tensor_product(): observable_one = Observable.Z() @ Observable.Y() observable_two = Observable.X() @ Observable.H() @@ -612,6 +660,16 @@ def test_tensor_product_basis_rotation_gates(observable, basis_rotation_gates): assert observable.basis_rotation_gates == basis_rotation_gates +def test_tensor_product_repeated_qubits(): + with pytest.raises(ValueError): + (2 * Observable.Z(3)) @ (3 * Observable.H(3)) + + +def test_tensor_product_with_and_without_targets(): + with pytest.raises(ValueError): + (2 * Observable.Z(3)) @ (3 * Observable.H()) + + def test_observable_from_ir_tensor_product(): expected_observable = Observable.TensorProduct([Observable.Z(), Observable.I(), Observable.X()]) actual_observable = observable_from_ir(["z", "i", "x"]) @@ -714,3 +772,8 @@ def test_unscaled_tensor_product(): observable = 3 * ((2 * Observable.X()) @ (5 * Observable.Y())) assert observable == 30 * (Observable.X() @ Observable.Y()) assert observable._unscaled() == Observable.X() @ Observable.Y() + + +def test_sum_with_and_without_targets(): + with pytest.raises(ValueError): + Observable.X() + 3 * Observable.Y(4)