SparseObservable
class qiskit.quantum_info.SparseObservable
Bases: object
An observable over Pauli bases that stores its data in a qubit-sparse format.
Mathematics
This observable represents a sum over strings of the Pauli operators and Pauli-eigenstate projectors, with each term weighted by some complex number. That is, the full observable is
for complex numbers and single-qubit operators acting on qubit from a restricted alphabet . The sum over is the sum of the individual terms, and the tensor product produces the operator strings.
The alphabet of allowed single-qubit operators that the are drawn from is the Pauli operators and the Pauli-eigenstate projection operators. Explicitly, these are:
Label | Operator | Numeric value | BitTerm attribute |
---|---|---|---|
"I" | (identity) | Not stored. | Not stored. |
"X" | (Pauli X) | 0b0010 (2) | X |
"Y" | (Pauli Y) | 0b0011 (3) | Y |
"Z" | (Pauli Z) | 0b0001 (1) | Z |
"+" | (projector to positive eigenstate of X) | 0b1010 (10) | PLUS |
"-" | (projector to negative eigenstate of X) | 0b0110 (6) | MINUS |
"r" | (projector to positive eigenstate of Y) | 0b1011 (11) | RIGHT |
"l" | (projector to negative eigenstate of Y) | 0b0111 (7) | LEFT |
"0" | (projector to positive eigenstate of Z) | 0b1001 (9) | ZERO |
"1" | (projector to negative eigenstate of Z) | 0b0101 (5) | ONE |
The allowed alphabet forms an overcomplete basis of the operator space. This means that there is not a unique summation to represent a given observable. By comparison, SparsePauliOp
uses a precise basis of the operator space, so (after combining terms of the same Pauli string, removing zeros, and sorting the terms to some canonical order) there is only one representation of any operator.
SparseObservable
uses its particular overcomplete basis with the aim of making “efficiency of measurement” equivalent to “efficiency of representation”. For example, the observable can be efficiently measured on hardware with simple measurements, but can only be represented by SparsePauliOp
as , which requires stored terms. SparseObservable
requires only a single term to store this.
The downside to this is that it is impractical to take an arbitrary matrix or SparsePauliOp
and find the best SparseObservable
representation. You typically will want to construct a SparseObservable
directly, rather than trying to decompose into one.
Representation
The internal representation of a SparseObservable
stores only the non-identity qubit operators. This makes it significantly more efficient to represent observables such as ; SparseObservable
requires an amount of memory linear in the total number of qubits, while SparsePauliOp
scales quadratically.
The terms are stored compressed, similar in spirit to the compressed sparse row format of sparse matrices. In this analogy, the terms of the sum are the “rows”, and the qubit terms are the “columns”, where an absent entry represents the identity rather than a zero. More explicitly, the representation is made up of four contiguous arrays:
Attribute | Length | Description |
---|---|---|
coeffs | The complex scalar multiplier for each term. | |
bit_terms | Each of the non-identity single-qubit terms for all of the operators, in order. These correspond to the non-identity in the sum description, where the entries are stored in order of increasing first, and in order of increasing within each term. | |
indices | The corresponding qubit () for each of the operators in bit_terms . SparseObservable requires that this list is term-wise sorted, and algorithms can rely on this invariant being upheld. | |
boundaries | The indices that partition bit_terms and indices into complete terms. For term number , its complex coefficient is coeffs[i] , and its non-identity single-qubit operators and their corresponding qubits are the slice boundaries[i] : boundaries[i+1] into bit_terms and indices respectively. boundaries always has an explicit 0 as its first element. |
The length parameter is the number of terms in the sum, and the parameter is the total number of non-identity single-qubit terms.
As illustrative examples:
- in the case of a zero operator,
boundaries
is length 1 (a single 0) and all other vectors are empty. - in the case of a fully simplified identity operator,
boundaries
is[0, 0]
,coeffs
has a single entry, andbit_terms
andindices
are empty. - for the operator ,
boundaries
is[0, 2, 4]
,coeffs
is[1.0, -1.0]
,bit_terms
is[BitTerm.Z, BitTerm.Z, BitTerm.Y, BitTerm.X]
andindices
is[0, 2, 1, 3]
. The operator might act on more than four qubits, depending on thenum_qubits
parameter. Thebit_terms
are integer values, whose magic numbers can be accessed via theBitTerm
attribute class. Note that the single-bit terms and indices are sorted into termwise sorted order. This is a requirement of the class.
These cases are not special, they’re fully consistent with the rules and should not need special handling.
The scalar item of the bit_terms
array is stored as a numeric byte. The numeric values are related to the symplectic Pauli representation that SparsePauliOp
uses, and are accessible with named access by an enumeration:
BitTerm
class BitTerm
An IntEnum
that provides named access to the numerical values used to represent each of the single-qubit alphabet terms enumerated in Alphabet of single-qubit terms used in SparseObservable.
This class is attached to SparseObservable
. Access it as SparseObservable.BitTerm
. If this is too much typing, and you are solely dealing with :class:¬SparseObservable` objects and the BitTerm
name is not ambiguous, you might want to shorten it as:
>>> ops = SparseObservable.BitTerm
>>> assert ops.X is SparseObservable.BitTerm.X
You can access all the values of the enumeration by either their full all-capitals name, or by their single-letter label. The single-letter labels are not generally valid Python identifiers, so you must use indexing notation to access them:
>>> assert SparseObservable.BitTerm.ZERO is SparseObservable.BitTerm["0"]
The numeric structure of these is that they are all four-bit values of which the low two bits are the (phase-less) symplectic representation of the Pauli operator related to the object, where the low bit denotes a contribution by and the second lowest a contribution by , while the upper two bits are 00
for a Pauli operator, 01
for the negative-eigenstate projector, and 10
for the positive-eigenstate projector.
X
Default value: 2
The Pauli operator. Uses the single-letter label "X"
.
PLUS
Default value: 10
The projector to the positive eigenstate of the operator: . Uses the single-letter label "+"
.
MINUS
Default value: 6
The projector to the negative eigenstate of the operator: . Uses the single-letter label "-"
.
Y
Default value: 3
The Pauli operator. Uses the single-letter label "Y"
.
RIGHT
Default value: 11
The projector to the positive eigenstate of the operator: . Uses the single-letter label "r"
.
LEFT
Default value: 7
The projector to the negative eigenstate of the operator: . Uses the single-letter label "l"
.
Z
Default value: 1
The Pauli operator. Uses the single-letter label "Z"
.
ZERO
Default value: 9
The projector to the positive eigenstate of the operator: . Uses the single-letter label "0"
.
ONE
Default value: 5
The projector to the negative eigenstate of the operator: . Uses the single-letter label "1"
.
Each of the array-like attributes behaves like a Python sequence. You can index and slice these with standard list
-like semantics. Slicing an attribute returns a Numpy ndarray
containing a copy of the relevant data with the natural dtype
of the field; this lets you easily do mathematics on the results, like bitwise operations on bit_terms
. You can assign to indices or slices of each of the attributes, but beware that you must uphold the data coherence rules while doing this. For example:
>>> obs = SparseObservable.from_list([("XZY", 1.5j), ("+1r", -0.5)])
>>> assert isinstance(obs.coeffs[:], np.ndarray)
>>> # Reduce all single-qubit terms to the relevant Pauli operator, if they are a projector.
>>> obs.bit_terms[:] = obs.bit_terms[:] & 0b00_11
>>> assert obs == SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)])
The above reduction to the Pauli bases can also be achieved with pauli_bases()
.
Canonical ordering
For any given mathematical observable, there are several ways of representing it with SparseObservable
. For example, the same set of single-bit terms and their corresponding indices might appear multiple times in the observable. Mathematically, this is equivalent to having only a single term with all the coefficients summed. Similarly, the terms of the sum in a SparseObservable
can be in any order while representing the same observable, since addition is commutative (although while floating-point addition is not associative, SparseObservable
makes no guarantees about the summation order).
These two categories of representation degeneracy can cause the ==
operator to claim that two observables are not equal, despite representating the same object. In these cases, it can be convenient to define some canonical form, which allows observables to be compared structurally.
You can put a SparseObservable
in canonical form by using the simplify()
method. The precise ordering of terms in canonical ordering is not specified, and may change between versions of Qiskit. Within the same version of Qiskit, however, you can compare two observables structurally by comparing their simplified forms.
If you wish to account for floating-point tolerance in the comparison, it is safest to use a recipe such as:
def equivalent(left, right, tol):
return (left - right).simplify(tol) == SparseObservable.zero(left.num_qubits)
The canonical form produced by simplify()
will still not universally detect all observables that are equivalent due to the over-complete basis alphabet; it is not computationally feasible to do this at scale. For example, on observable built from +
and -
components will not canonicalize to a single X
term.
Indexing
SparseObservable
behaves as a Python sequence (the standard form, not the expanded collections.abc.Sequence
). The observable can be indexed by integers, and iterated through to yield individual terms.
Each term appears as an instance a self-contained class. The individual terms are copied out of the base observable; mutations to them will not affect the observable.
Term
class Term
Bases: object
A single term from a complete SparseObservable
.
These are typically created by indexing into or iterating through a SparseObservable
.
bit_terms
Read-only view onto the individual single-qubit terms.
The only valid values in the array are those with a corresponding BitTerm
.
coeff
The complex coefficient of the term.
copy
copy()
Get a copy of this term.
indices
Read-only view onto the indices of each non-identity single-qubit term.
The indices will always be in sorted order.
num_qubits
Number of qubits the entire term applies to.
pauli_base
pauli_base()
Get a Pauli
object that represents the measurement basis needed for this term.
For example, the projector 0l+
will return a Pauli ZXY
. The resulting Pauli
is dense, in the sense that explicit identities are stored. An identity in the Pauli output does not require a concrete measurement.
Returns
the Pauli operator representing the necessary measurement basis.
Return type
SparseObservable.pauli_bases()
A similar method for an entire observable at once.
to_observable
to_observable()
Convert this term to a complete SparseObservable
.
Construction
SparseObservable
defines several constructors. The default constructor will attempt to delegate to one of the more specific constructors, based on the type of the input. You can always use the specific constructors to have more control over the construction.
Method | Summary |
---|---|
from_label() | Convert a dense string label into a single-term SparseObservable . |
from_list() | Sum a list of tuples of dense string labels and the associated coefficients into an observable. |
from_sparse_list() | Sum a list of tuples of sparse string labels, the qubits they apply to, and their coefficients into an observable. |
from_pauli() | Raise a single Pauli into a single-term SparseObservable . |
from_sparse_pauli_op() | Raise a SparsePauliOp into a SparseObservable . |
from_terms() | Sum explicit single Term instances. |
from_raw_parts() | Build the observable from the raw data arrays. |
__new__
__new__(data, /, num_qubits=None)
The default constructor of SparseObservable
.
This delegates to one of the explicit conversion-constructor methods, based on the type of the data
argument. If num_qubits
is supplied and constructor implied by the type of data
does not accept a number, the given integer must match the input.
Parameters
- data – The data type of the input. This can be another
SparseObservable
, in which case the input is copied, aPauli
orSparsePauliOp
, in which casefrom_pauli()
orfrom_sparse_pauli_op()
are called as appropriate, or it can be a list in a valid format for eitherfrom_list()
orfrom_sparse_list()
. - num_qubits (int|None) – Optional number of qubits for the operator. For most data inputs, this can be inferred and need not be passed. It is only necessary for empty lists or the sparse-list format. If given unnecessarily, it must match the data input.
In addition to the conversion-based constructors, there are also helper methods that construct special forms of observables.
Method | Summary |
---|---|
zero() | The zero operator on a given number of qubits. |
identity() | The identity operator on a given number of qubits. |
Mathematical manipulation
SparseObservable
supports the standard set of Python mathematical operators like other quantum_info
operators.
In basic arithmetic, you can:
- add two observables using
+
- subtract two observables using
-
- multiply or divide by an
int
,float
orcomplex
using*
and/
- negate all the coefficients in an observable with unary
-
Each of the basic binary arithmetic operators has a corresponding specialized in-place method, which mutates the left-hand side in-place. Using these is typically more efficient than the infix operators, especially for building an observable in a loop.
The tensor product is calculated with tensor()
(for standard, juxtaposition ordering of Pauli labels) or expand()
(for the reverse order). The ^
operator is overloaded to be equivalent to tensor()
.
When using the binary operators ^
(tensor()
) and &
(compose()
), beware that Python’s operator-precedence rules may cause the evaluation order to be different to your expectation. In particular, the operator +
binds more tightly than ^
or &
, just like *
binds more tightly than +
.
When using the operators in mixed expressions, it is safest to use parentheses to group the operands of tensor products.
A SparseObservable
has a well-defined adjoint()
. The notions of scalar complex conjugation (conjugate()
) and real-value transposition (transpose()
) are defined analogously to the matrix representation of other Pauli operators in Qiskit.
Efficiency notes
Internally, SparseObservable
is in-place mutable, including using over-allocating growable vectors for extending the number of terms. This means that the cost of appending to an observable using +=
is amortised linear in the total number of terms added, rather than the quadratic complexity that the binary +
would require.
Additions and subtractions are implemented by a term-stacking operation; there is no automatic “simplification” (summing of like terms), because the majority of additions to build up an observable generate only a small number of duplications, and like-term detection has additional costs. If this does not fit your use cases, you can either periodically call simplify()
, or discuss further APIs with us for better building of observables.
Attributes
bit_terms
A flat list of single-qubit terms. This is more naturally a list of lists, but is stored flat for memory usage and locality reasons, with the sublists denoted by boundaries.
boundaries
Indices that partition bit_terms
and indices
into sublists for each individual term in the sum. boundaries[0] : boundaries[1]
is the range of indices into bit_terms
and indices
that correspond to the first term of the sum. All unspecified qubit indices are implicitly the identity. This is one item longer than coeffs
, since boundaries[0]
is always an explicit zero (for algorithmic ease).
coeffs
The coefficients of each abstract term in in the sum. This has as many elements as terms in the sum.
indices
A flat list of the qubit indices that the corresponding entries in bit_terms
act on. This list must always be term-wise sorted, where a term is a sublist as denoted by boundaries
.
If writing to this attribute from Python space, you must ensure that you only write in indices that are term-wise sorted.
num_qubits
The number of qubits the operator acts on.
This is not inferable from any other shape or values, since identities are not stored explicitly.
num_terms
The number of terms in the sum this operator is tracking.
Methods
adjoint
adjoint()
Calculate the adjoint of this observable.
This is well defined in the abstract mathematical sense. All the terms of the single-qubit alphabet are self-adjoint, so the result of this operation is the same observable, except its coefficients are all their complex conjugates.
Examples
>>> left = SparseObservable.from_list([("XY+-", 1j)])
>>> right = SparseObservable.from_list([("XY+-", -1j)])
>>> assert left.adjoint() == right
apply_layout
apply_layout(layout, num_qubits=None)
Apply a transpiler layout to this SparseObservable
.
Typically you will have defined your observable in terms of the virtual qubits of the circuits you will use to prepare states. After transpilation, the virtual qubits are mapped to particular physical qubits on a device, which may be wider than your circuit. That mapping can also change over the course of the circuit. This method transforms the input observable on virtual qubits to an observable that is suitable to apply immediately after the fully transpiled physical circuit.
Parameters
- layout (TranspileLayout |list[int] | None) – The layout to apply. Most uses of this function should pass the
QuantumCircuit.layout
field from a circuit that was transpiled for hardware. In addition, you can pass a list of new qubit indices. If given as explicitlyNone
, no remapping is applied (but you can still usenum_qubits
to expand the observable). - num_qubits (int | None) – The number of qubits to expand the observable to. If not supplied, the output will be as wide as the given
TranspileLayout
, or the same width as the input if thelayout
is given in another form.
Returns
A new SparseObservable
with the provided layout applied.
clear
clear()
Clear all the terms from this operator, making it equal to the zero operator again.
This does not change the capacity of the internal allocations, so subsequent addition or substraction operations may not need to reallocate.
Examples
>>> obs = SparseObservable.from_list([("IX+-rl", 2.0), ("01YZII", -1j)])
>>> obs.clear()
>>> assert obs == SparseObservable.zero(obs.num_qubits)
conjugate
conjugate()
Calculate the complex conjugation of this observable.
This operation is defined in terms of the standard matrix conventions of Qiskit, in that the matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related alphabet terms are unaffected by the complex conjugation, but $Y$-related terms modify their alphabet terms. Precisely:
- conjguates to
- conjugates to
- conjugates to
Additionally, all coefficients are conjugated.
Examples
>>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)])
>>> assert obs.conjugate() == SparseObservable([("III", -1j), ("Ylr", -0.5)])
copy
copy()
Get a copy of this observable.
Examples
>>> obs = SparseObservable.from_list([("IXZ+lr01", 2.5), ("ZXI-rl10", 0.5j)])
>>> assert obs == obs.copy()
>>> assert obs is not obs.copy()
expand
expand(other, /)
Reverse-order tensor product.
This is equivalent to other.tensor(self)
, except that other
will first be type-cast to SparseObservable
if it isn’t already one (by calling the default constructor).
Parameters
other – the observable to put on the left-hand side of the tensor product.
Examples
This is equivalent to tensor()
with the order of the arguments flipped:
>>> left = SparseObservable.from_label("XYZ")
>>> right = SparseObservable.from_label("+-IIrl")
>>> assert left.tensor(right) == right.expand(left)
from_label
static from_label(label, /)
Construct a single-term observable from a dense string label.
The resulting operator will have a coefficient of 1. The label must be a sequence of the alphabet 'IXYZ+-rl01'
. The label is interpreted analogously to a bitstring. In other words, the right-most letter is associated with qubit 0, and so on. This is the same as the labels for Pauli
and SparsePauliOp
.
Parameters
label (str) – the dense label.
Examples
>>> SparseObservable.from_label("IIII+ZI")
<SparseObservable with 1 term on 7 qubits: (1+0j)(+_2 Z_1)>
>>> label = "IYXZI"
>>> pauli = Pauli(label)
>>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli)
A generalization of this method that constructs a sum operator from multiple labels and their corresponding coefficients.
from_list
static from_list(iter, /, *, num_qubits=None)
Construct an observable from a list of dense labels and coefficients.
This is analogous to SparsePauliOp.from_list()
, except it uses the extended alphabet of SparseObservable
. In this dense form, you must supply all identities explicitly in each label.
The label must be a sequence of the alphabet 'IXYZ+-rl01'
. The label is interpreted analogously to a bitstring. In other words, the right-most letter is associated with qubit 0, and so on. This is the same as the labels for Pauli
and SparsePauliOp
.
Parameters
- iter (list[tuple[str, complex]]) – Pairs of labels and their associated coefficients to sum. The labels are interpreted the same way as in
from_label()
. - num_qubits (int | None) – It is not necessary to specify this if you are sure that
iter
is not an empty sequence, since it can be inferred from the label lengths. Ifiter
may be empty, you must specify this argument to disambiguate how many qubits the observable is for. If this is given anditer
is not empty, the value must match the label lengths.
Examples
Construct an observable from a list of labels of the same length:
>>> SparseObservable.from_list([
... ("III++", 1.0),
... ("II--I", 1.0j),
... ("I++II", -0.5),
... ("--III", -0.25j),
... ])
<SparseObservable with 4 terms on 5 qubits:
(1+0j)(+_1 +_0) + (0+1j)(-_2 -_1) + (-0.5+0j)(+_3 +_2) + (-0-0.25j)(-_4 -_3)>
Use num_qubits
to disambiguate potentially empty inputs:
>>> SparseObservable.from_list([], num_qubits=10)
<SparseObservable with 0 terms on 10 qubits: 0.0>
This method is equivalent to calls to from_sparse_list()
with the explicit qubit-arguments field set to decreasing integers:
>>> labels = ["XY+Z", "rl01", "-lXZ"]
>>> coeffs = [1.5j, 2.0, -0.5]
>>> from_list = SparseObservable.from_list(list(zip(labels, coeffs)))
>>> from_sparse_list = SparseObservable.from_sparse_list([
... (label, (3, 2, 1, 0), coeff)
... for label, coeff in zip(labels, coeffs)
... ])
>>> assert from_list == from_sparse_list
A similar constructor, but takes only a single label and always has its coefficient set to 1.0
.
Construct the observable from a list of labels without explicit identities, but with the qubits each single-qubit term applies to listed explicitly.
from_pauli
static from_pauli(pauli, /)
Construct a SparseObservable
from a single Pauli
instance.
The output observable will have a single term, with a unitary coefficient dependent on the phase.
Parameters
pauli (Pauli
) – the single Pauli to convert.
Examples
>>> label = "IYXZI"
>>> pauli = Pauli(label)
>>> SparseObservable.from_pauli(pauli)
<SparseObservable with 1 term on 5 qubits: (1+0j)(Y_3 X_2 Z_1)>
>>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli)
from_raw_parts
static from_raw_parts(num_qubits, coeffs, bit_terms, indices, boundaries, check=True)
Construct a SparseObservable
from raw Numpy arrays that match the required data representation described in the class-level documentation.
The data from each array is copied into fresh, growable Rust-space allocations.
Parameters
-
num_qubits – number of qubits in the observable.
-
coeffs – complex coefficients of each term of the observable. This should be a Numpy array with dtype
complex128
. -
bit_terms – flattened list of the single-qubit terms comprising all complete terms. This should be a Numpy array with dtype
uint8
(which is compatible withBitTerm
). -
indices – flattened term-wise sorted list of the qubits each single-qubit term corresponds to. This should be a Numpy array with dtype
uint32
. -
boundaries – the indices that partition
bit_terms
andindices
into terms. This should be a Numpy array with dtypeuintp
. -
check –
if
True
(the default), validate that the data satisfies all coherence guarantees. IfFalse
, no checks are done.WarningIf
check=False
, thebit_terms
absolutely must be all be valid values ofSparseObservable.BitTerm
. If they are not, Rust-space undefined behavior may occur, entirely invalidating the program execution.
Examples
Construct a sum of on each individual qubit:
>>> num_qubits = 100
>>> terms = np.full((num_qubits,), SparseObservable.BitTerm.Z, dtype=np.uint8)
>>> indices = np.arange(num_qubits, dtype=np.uint32)
>>> coeffs = np.ones((num_qubits,), dtype=complex)
>>> boundaries = np.arange(num_qubits + 1, dtype=np.uintp)
>>> SparseObservable.from_raw_parts(num_qubits, coeffs, terms, indices, boundaries)
<SparseObservable with 100 terms on 100 qubits: (1+0j)(Z_0) + ... + (1+0j)(Z_99)>
from_sparse_list
static from_sparse_list(iter, /, num_qubits)
Construct an observable from a list of labels, the qubits each item applies to, and the coefficient of the whole term.
This is analogous to SparsePauliOp.from_sparse_list()
, except it uses the extended alphabet of SparseObservable
.
The “labels” and “indices” fields of the triples are associated by zipping them together. For example, this means that a call to from_list()
can be converted to the form used by this method by setting the “indices” field of each triple to (num_qubits-1, ..., 1, 0)
.
Parameters
- iter (list[tuple[str, Sequence[int], complex]]) – triples of labels, the qubits each single-qubit term applies to, and the coefficient of the entire term.
- num_qubits (int) – the number of qubits in the operator.
Examples
Construct a simple operator:
>>> SparseObservable.from_sparse_list(
... [("ZX", (1, 4), 1.0), ("YY", (0, 3), 2j)],
... num_qubits=5,
... )
<SparseObservable with 2 terms on 5 qubits: (1+0j)(X_4 Z_1) + (0+2j)(Y_3 Y_0)>
Construct the identity observable (though really, just use identity()
):
>>> SparseObservable.from_sparse_list([("", (), 1.0)], num_qubits=100)
<SparseObservable with 1 term on 100 qubits: (1+0j)()>
This method can replicate the behavior of from_list()
, if the qubit-arguments field of the triple is set to decreasing integers:
>>> labels = ["XY+Z", "rl01", "-lXZ"]
>>> coeffs = [1.5j, 2.0, -0.5]
>>> from_list = SparseObservable.from_list(list(zip(labels, coeffs)))
>>> from_sparse_list = SparseObservable.from_sparse_list([
... (label, (3, 2, 1, 0), coeff)
... for label, coeff in zip(labels, coeffs)
... ])
>>> assert from_list == from_sparse_list
from_sparse_pauli_op
static from_sparse_pauli_op(op, /)
Construct a SparseObservable
from a SparsePauliOp
instance.
This will be a largely direct translation of the SparsePauliOp
; in particular, there is no on-the-fly summing of like terms, nor any attempt to refactorize sums of Pauli terms into equivalent projection operators.
Parameters
op (SparsePauliOp
) – the operator to convert.
Examples
>>> spo = SparsePauliOp.from_list([("III", 1.0), ("IIZ", 0.5), ("IZI", 0.5)])
>>> SparseObservable.from_sparse_pauli_op(spo)
<SparseObservable with 3 terms on 3 qubits: (1+0j)() + (0.5+0j)(Z_0) + (0.5+0j)(Z_1)>
from_terms
static from_terms(obj, /, num_qubits=None)
Construct a SparseObservable
out of individual terms.
All the terms must have the same number of qubits. If supplied, the num_qubits
argument must match the terms.
No simplification is done as part of the observable creation.
Parameters
- obj (Iterable[Term]) – Iterable of individual terms to build the observable from.
- num_qubits (int | None) – The number of qubits the observable should act on. This is usually inferred from the input, but can be explicitly given to handle the case of an empty iterable.
Returns
The corresponding observable.
identity
static identity(num_qubits)
Get the identity operator over the given number of qubits.
Examples
Get the identity operator for 100 qubits:
>>> SparseObservable.identity(100)
<SparseObservable with 1 term on 100 qubits: (1+0j)()>
pauli_bases
pauli_bases()
Get a PauliList
object that represents the measurement basis needed for each term (in order) in this observable.
For example, the projector 0l+
will return a Pauli ZXY
. The resulting Pauli
is dense, in the sense that explicit identities are stored. An identity in the Pauli output does not require a concrete measurement.
This will return an entry in the Pauli list for every term in the sum.
Returns
the Pauli operator list representing the necessary measurement bases.
Return type
simplify
simplify(tol=1e-08)
Sum any like terms in this operator, removing them if the resulting complex coefficient has an absolute value within tolerance of zero.
As a side effect, this sorts the operator into canonical order.
When using this for equality comparisons, note that floating-point rounding and the non-associativity fo floating-point addition may cause non-zero coefficients of summed terms to compare unequal. To compare two observables up to a tolerance, it is safest to compare the canonicalized difference of the two observables to zero.
Parameters
tol (float) – after summing like terms, any coefficients whose absolute value is less than the given absolute tolerance will be suppressed from the output.
Examples
Using simplify()
to compare two operators that represent the same observable, but would compare unequal due to the structural tests by default:
>>> base = SparseObservable.from_sparse_list([
... ("XZ", (2, 1), 1e-10), # value too small
... ("+-", (3, 1), 2j),
... ("+-", (3, 1), 2j), # can be combined with the above
... ("01", (3, 1), 0.5), # out of order compared to `expected`
... ], num_qubits=5)
>>> expected = SparseObservable.from_list([("I0I1I", 0.5), ("I+I-I", 4j)])
>>> assert base != expected # non-canonical comparison
>>> assert base.simplify() == expected.simplify()
Note that in the above example, the coefficients are chosen such that all floating-point calculations are exact, and there are no intermediate rounding or associativity concerns. If this cannot be guaranteed to be the case, the safer form is:
>>> left = SparseObservable.from_list([("XYZ", 1.0/3.0)] * 3) # sums to 1.0
>>> right = SparseObservable.from_list([("XYZ", 1.0/7.0)] * 7) # doesn't sum to 1.0
>>> assert left.simplify() != right.simplify()
>>> assert (left - right).simplify() == SparseObservable.zero(left.num_qubits)
tensor
tensor(other, /)
Tensor product of two observables.
The bit ordering is defined such that the qubit indices of the argument will remain the same, and the indices of self
will be offset by the number of qubits in other
. This is the same convention as used by the rest of Qiskit’s quantum_info
operators.
This function is used for the infix ^
operator. If using this operator, beware that Python’s operator-precedence rules may cause the evaluation order to be different to your expectation. In particular, the operator +
binds more tightly than ^
, just like *
binds more tightly than +
. Use parentheses to fix the evaluation order, if needed.
The argument will be cast to SparseObservable
using its default constructor, if it is not already in the correct form.
Parameters
other – the observable to put on the right-hand side of the tensor product.
Examples
The bit ordering of this is such that the tensor product of two observables made from a single label “looks like” an observable made by concatenating the two strings:
>>> left = SparseObservable.from_label("XYZ")
>>> right = SparseObservable.from_label("+-IIrl")
>>> assert left.tensor(right) == SparseObservable.from_label("XYZ+-IIrl")
You can also use the infix ^
operator for tensor products, which will similarly cast the right-hand side of the operation if it is not already a SparseObservable
:
>>> assert SparseObservable("rl") ^ Pauli("XYZ") == SparseObservable("rlXYZ")
The same function, but with the order of arguments flipped. This can be useful if you like using the casting behavior for the argument, but you want your existing
SparseObservable
to be on the right-hand side of the tensor ordering.
transpose
transpose()
Calculate the matrix transposition of this observable.
This operation is defined in terms of the standard matrix conventions of Qiskit, in that the matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related alphabet terms are unaffected by the transposition, but $Y$-related terms modify their alphabet terms. Precisely:
- transposes to
- transposes to
- transposes to
Examples
>>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)])
>>> assert obs.transpose() == SparseObservable([("III", 1j), ("Ylr", -0.5)])
zero
static zero(num_qubits)
Get the zero operator over the given number of qubits.
The zero operator is the operator whose expectation value is zero for all quantum states. It has no terms. It is the identity element for addition of two SparseObservable
instances; anything added to the zero operator is equal to itself.
If you want the projector onto the all zeros state, use:
>>> num_qubits = 10
>>> all_zeros = SparseObservable.from_label("0" * num_qubits)
Examples
Get the zero operator for 100 qubits:
>>> SparseObservable.zero(100)
<SparseObservable with 0 terms on 100 qubits: 0.0>