-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Add Observable
container type for Estimator
#11594
base: main
Are you sure you want to change the base?
Changes from all commits
942d997
f8bf6c4
ae5ee53
4315907
0fae636
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,169 @@ | ||||||
# This code is part of Qiskit. | ||||||
# | ||||||
# (C) Copyright IBM 2024. | ||||||
# | ||||||
# This code is licensed under the Apache License, Version 2.0. You may | ||||||
# obtain a copy of this license in the LICENSE.txt file in the root directory | ||||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||||||
# | ||||||
# Any modifications or derivative works of this code must retain this | ||||||
# copyright notice, and modified files need to carry a notice indicating | ||||||
# that they have been altered from the originals. | ||||||
|
||||||
|
||||||
""" | ||||||
Sparse container class for an Estimator observable. | ||||||
""" | ||||||
from __future__ import annotations | ||||||
|
||||||
from typing import Union, Mapping as MappingType | ||||||
from collections.abc import Mapping | ||||||
from collections import defaultdict | ||||||
from numbers import Real, Complex | ||||||
|
||||||
from qiskit.quantum_info import Pauli, SparsePauliOp | ||||||
|
||||||
|
||||||
ObservableKey = "tuple[tuple[int, ...], str]" # pylint: disable = invalid-name | ||||||
ObservableKeyLike = Union[Pauli, str, ObservableKey] | ||||||
ObservableLike = Union[ | ||||||
SparsePauliOp, | ||||||
ObservableKeyLike, | ||||||
MappingType[ObservableKeyLike, Real], | ||||||
] | ||||||
"""Types that can be natively used to construct an observable.""" | ||||||
|
||||||
|
||||||
class Observable(Mapping[ObservableKey, float]): | ||||||
"""A sparse container for a observable for an estimator primitive.""" | ||||||
|
||||||
__slots__ = ("_data", "_num_qubits") | ||||||
|
||||||
def __init__( | ||||||
self, | ||||||
data: Mapping[ObservableKey, float], | ||||||
num_qubits: int = None, | ||||||
validate: bool = True, | ||||||
): | ||||||
"""Initialize an observables array. | ||||||
|
||||||
Args: | ||||||
data: The observable data. | ||||||
num_qubits: The number of qubits in the data. | ||||||
validate: If ``True``, the input data is validated during initialization. | ||||||
|
||||||
Raises: | ||||||
ValueError: If ``validate=True`` and the input observable-like is not valid. | ||||||
""" | ||||||
self._data = data | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we care about not sharing a reference to the internal thing:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its a trade off with that vs performance since this requires an iteration over the data at init |
||||||
self._num_qubits = num_qubits | ||||||
if validate: | ||||||
self.validate() | ||||||
|
||||||
def __repr__(self): | ||||||
return f"{type(self).__name__}({self._data})" | ||||||
|
||||||
def __getitem__(self, key: ObservableKey) -> float: | ||||||
return self._data[key] | ||||||
|
||||||
def __iter__(self): | ||||||
return iter(self._data) | ||||||
|
||||||
def __len__(self): | ||||||
return len(self._data) | ||||||
|
||||||
@property | ||||||
def num_qubits(self) -> int: | ||||||
"""The number of qubits in the observable""" | ||||||
if self._num_qubits is None: | ||||||
num_qubits = 0 | ||||||
for key in self._data: | ||||||
num_qubits = max(num_qubits, 1 + max(key[0])) | ||||||
self._num_qubits = num_qubits | ||||||
return self._num_qubits | ||||||
|
||||||
def validate(self): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This validation allows no terms, which I guess corresponds to the identity observable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should fail if emtpy so identity has to be represented as |
||||||
"""Validate the consistency in observables array.""" | ||||||
if not isinstance(self._data, Mapping): | ||||||
raise TypeError(f"Observable data type {type(self._data)} is not a Mapping.") | ||||||
|
||||||
# If not already set, we compute num_qubits while iterating over data | ||||||
# to avoid having to do another iteration later | ||||||
data_num_qubits = 0 | ||||||
for key, value in self._data.items(): | ||||||
if not isinstance(key, tuple): | ||||||
raise TypeError("Invalid Observable key type") | ||||||
if len(key) != 2: | ||||||
raise ValueError(f"Invalid Observable key value {key}") | ||||||
# Check tuple pos types after checking length | ||||||
if not isinstance(key[0], tuple) or not isinstance(key[1], str): | ||||||
raise TypeError("Invalid Observable key type") | ||||||
data_num_qubits = max(data_num_qubits, 1 + max(key[0])) | ||||||
if not isinstance(value, Real): | ||||||
raise TypeError(f"Value {value} is not a real number") | ||||||
if self._num_qubits is None: | ||||||
self._num_qubits = data_num_qubits | ||||||
elif self._num_qubits < data_num_qubits: | ||||||
raise ValueError("Num qubits is less than the maximum qubit in observable keys") | ||||||
|
||||||
@classmethod | ||||||
def coerce(cls, observable: ObservableLike) -> Observable: | ||||||
"""Coerce an observable-like object into an :class:`.Observable`. | ||||||
|
||||||
Args: | ||||||
observable: The observable-like input. | ||||||
|
||||||
Returns: | ||||||
A coerced observables array. | ||||||
|
||||||
Raises: | ||||||
TypeError: If the input cannot be formatted because its type is not valid. | ||||||
ValueError: If the input observable is invalid. | ||||||
""" | ||||||
return cls._coerce(observable, num_qubits=None) | ||||||
|
||||||
@classmethod | ||||||
def _coerce(cls, observable, num_qubits=None): | ||||||
# Pauli-type conversions | ||||||
if isinstance(observable, SparsePauliOp): | ||||||
# TODO: Make sparse by removing identity qubits in keys | ||||||
data = dict(observable.simplify(atol=0).to_list()) | ||||||
return cls._coerce(data, num_qubits=observable.num_qubits) | ||||||
|
||||||
if isinstance(observable, (Pauli, str, tuple)): | ||||||
return cls._coerce({observable: 1}, num_qubits=num_qubits) | ||||||
|
||||||
# Mapping conversion (with possible Pauli keys) | ||||||
if isinstance(observable, Mapping): | ||||||
key_qubits = set() | ||||||
unique = defaultdict(float) | ||||||
for key, coeff in observable.items(): | ||||||
if isinstance(key, Pauli): | ||||||
# TODO: Make sparse by removing identity qubits in keys | ||||||
label, phase = key[:].to_label(), key.phase | ||||||
if phase != 0: | ||||||
coeff = coeff * (-1j) ** phase | ||||||
chriseclectic marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
qubits = tuple(range(key.num_qubits)) | ||||||
key = (qubits, label) | ||||||
elif isinstance(key, str): | ||||||
qubits = tuple(range(len(key))) | ||||||
key = (qubits, key) | ||||||
Comment on lines
+149
to
+150
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should have the same TODO as Pauli. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assumes we always mean "I" in a key to mean identity, which if we do we should be more explicit about (and in general with strings being single characters same length as qubits etc) |
||||||
if not isinstance(key, tuple): | ||||||
raise TypeError(f"Invalid key type {type(key)}") | ||||||
if len(key) != 2: | ||||||
raise ValueError(f"Invalid key {key}") | ||||||
if not isinstance(key[0], tuple) or not isinstance(key[1], str): | ||||||
raise TypeError("Invalid key type") | ||||||
# Truncate complex numbers to real | ||||||
if isinstance(coeff, Complex): | ||||||
if abs(coeff.imag) > 1e-7: | ||||||
raise TypeError(f"Invalid coeff for key {key}, coeff must be real.") | ||||||
coeff = coeff.real | ||||||
unique[key] += coeff | ||||||
key_qubits.update(key[0]) | ||||||
if num_qubits is None: | ||||||
num_qubits = 1 + max(key_qubits) | ||||||
obs = cls(dict(unique), num_qubits=num_qubits) | ||||||
return obs | ||||||
|
||||||
raise TypeError(f"Invalid observable type: {type(observable)}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be good to make a dataclass of
ObservableKey
and add a validation method for #11594 (comment).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer the name
ObservableTerm
here, but at the end of the day, it's just a name. I wonder if you can comment on the feasibility of leaving this astuple[tuple[int, ...], str]
for now and changing it to a class (eg as suggested by @t-imamichi) in the future? In terms of breaking changes and possible subclassing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im mostly worried about this container being slow and memory intensive with all the nested classes. I did have a prototype using a class that function like a tuple (had getitem), but im a bit skeptical of validation other than via type since it might be slow. We can't validate the strings, so validation would be that qubits is a tuple with no duplicates and (if we want to enforce this) that the string is same lenght as qubits.
In the current version the design doesn't actually require the string to represent a bunch of operators for each qubit, its up to an estimator to decide what it does with it, but we are using it to represent a Pauli label
(side note: our reference estimator is going to be rather inefficient since it unpacks SparsePauliOps into this potentially much more memory heavy format, and then has to convert back to SparsePauliOp for its simulation)