QkObs
typedef struct QkObs QkObs
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:
Operator | QkBitTerm | Numeric value |
---|---|---|
(identity) | Not stored. | Not stored. |
(Pauli X) | QkBitTerm_X | 0b0010 (2) |
(Pauli Y) | QkBitTerm_Y | 0b0011 (3) |
(Pauli Z) | QkBitTerm_Z | 0b0001 (1) |
(projector to positive eigenstate of X) | QkBitTerm_Plus | 0b1010 (10) |
(projector to negative eigenstate of X) | QkBitTerm_Minus | 0b0110 (6) |
(projector to positive eigenstate of Y) | QkBitTerm_Right | 0b1011 (11) |
(projector to negative eigenstate of Y) | QkBitTerm_Left | 0b0111 (7) |
(projector to positive eigenstate of Z) | QkBitTerm_Zero | 0b1001 (9) |
(projector to negative eigenstate of Z) | QkBitTerm_One | 0b0101 (5) |
Due to allowing both the Paulis and their projectors, 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. As a consequence, comparison requires additional care and using qk_obs_canonicalize
on two mathematically equivalent observables might not result in the same representation.
QkObs
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 in terms of Paulis as , which requires stored terms. QkObs
requires only a single term to store this. The downside to this is that it is impractical to take an arbitrary matrix and find the best QkObs
representation. You typically will want to construct a QkObs
directly, rather than trying to decompose into one.
Representation
The internal representation of a QkObs
stores only the non-identity qubit operators. This makes it significantly more efficient to represent observables such as ; QkObs
requires an amount of memory linear in the total number of qubits. 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 accessible by | Length | Description |
---|---|---|
qk_obs_coeffs | The complex scalar multiplier for each term. | |
qk_obs_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. | |
qk_obs_indices | The corresponding qubit () for each of the bit terms. QkObs requires that this list is term-wise sorted, and algorithms can rely on this invariant being upheld. | |
qk_obs_boundaries | The indices that partition the bit terms and indices into complete terms. For term number , its complex coefficient is stored at index i , and its non-identity single-qubit operators and their corresponding qubits are in the range [boundaries[i], boundaries[i+1]) in the bit terms and indices, respectively. The boundaries always have an explicit 0 as their first element. |
The length parameter is the number of terms in the sum and can be queried using qk_obs_num_terms
. The parameter is the total number of non-identity single-qubit terms and can be queried using qk_obs_len
.
As illustrative examples:
- in the case of a zero operator, the boundaries are length 1 (a single 0) and all other vectors are empty.
- in the case of a fully simplified identity operator, the boundaries are
{0, 0}
, the coefficients have a single entry, and both the bit terms and indices are empty. - for the operator , the boundaries are
{0, 2, 4}
, the coeffs are{1.0, -1.0}
, the bit terms are{QkBitTerm_Z, QkBitTerm_Z, QkBitTerm_Y, QkBitTerm_X}
and the indices are{0, 2, 1, 3}
. The operator might act on more than four qubits, depending on the the number of qubits (seeqk_obs_num_qubits
). Note that the single-bit terms and indices are sorted into termwise sorted order.
These cases are not special, they’re fully consistent with the rules and should not need special handling.
Canonical ordering
For any given mathematical observable, there are several ways of representing it with QkObs
. 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 QkObs
can be in any order while representing the same observable, since addition is commutative (although while floating-point addition is not associative, QkObs
makes no guarantees about the summation order).
These two categories of representation degeneracy can cause the operator equality, qk_obs_equal
, 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 QkObs
in canonical form by using the qk_obs_canonicalize
function. 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:
bool equivalent(QkObs *left, QkObs *right, double tol) {
// compare a canonicalized version of left - right to the zero observable
QkObs *neg_right = qk_obs_mul(right, -1);
QkObs *diff = qk_obs_add(left, neg_right);
QkObs *canonical = qk_obs_canonicalize(diff, tol);
QkObs *zero = qk_obs_zero(qk_obs_num_qubits(left));
bool equiv = qk_obs_equal(diff, zero);
// free all temporary variables
qk_obs_free(neg_right);
qk_obs_free(diff);
qk_obs_free(canonical);
qk_obs_free(zero);
return equiv;
}
The canonical form produced by qk_obs_canonicalize
alone will not universally detect all observables that are equivalent due to the over-complete basis alphabet.
Indexing
Individual observable sum terms in QkObs
can be accessed via qk_obs_term
and return objects of type QkObsTerm
. These terms then contain fields with the coefficient of the term, its bit terms, indices and the number of qubits it is defined on. Together with the information of the number of terms, you can iterate over all observable terms as
size_t num_terms = qk_obs_num_terms(obs); // obs is QkObs*
for (size_t i = 0; i < num_terms; i++) {
QkObsTerm term; // allocate term on stack
int exit = qk_obs_term(obs, i, &term); // get the term (exit > 0 upon index errors)
// do something with the term...
}
Populating a QkObsTerm
via qk_obs_term
will reference data of the original QkObs
. Modifying the bit terms or indices will change the observable and can leave it in an incoherent state.
Construction
QkObs
can be constructed by initializing an empty observable (with qk_obs_zero
) and iteratively adding terms (with qk_obs_add_term
). Alternatively, an observable can be constructed from “raw” data (with qk_obs_new
) if all internal data is specified. This requires care to ensure the data is coherent and results in a valid observable.
Function | Summary |
---|---|
qk_obs_zero | Construct an empty observable on a given number of qubits. |
qk_obs_identity | Construct the identity observable on a given number of qubits. |
qk_obs_new | Construct an observable from the raw data arrays. |
Mathematical manipulation
QkObs
supports fundamental arithmetic operations in between observables or with scalars. You can:
- add two observables using
qk_obs_add
- multiply by a complex number with
qk_obs_multiply
- compose (multiply) two observables via
qk_obs_compose
andqk_obs_compose_map
Functions
qk_obs_zero
QkObs *qk_obs_zero(uint32_t num_qubits)
Construct the zero observable (without any terms).
Example
QkObs *zero = qk_obs_zero(100);
Parameters
num_qubits – The number of qubits the observable is defined on.
Returns
A pointer to the created observable.
qk_obs_identity
QkObs *qk_obs_identity(uint32_t num_qubits)
Construct the identity observable.
Example
QkObs *identity = qk_obs_identity(100);
Parameters
num_qubits – The number of qubits the observable is defined on.
Returns
A pointer to the created observable.
qk_obs_new
QkObs *qk_obs_new(uint32_t num_qubits, uint64_t num_terms, uint64_t num_bits, QkComplex64 *coeffs, QkBitTerm *bit_terms, uint32_t *indices, uintptr_t *boundaries)
Construct a new observable from raw data.
Example
// define the raw data for the 100-qubit observable |01><01|_{0, 1} - |+-><+-|_{98, 99}
uint32_t num_qubits = 100;
uint64_t num_terms = 2; // we have 2 terms: |01><01|, -1 * |+-><+-|
uint64_t num_bits = 4; // we have 4 non-identity bits: 0, 1, +, -
complex double coeffs[2] = {1, -1};
QkBitTerm bits[4] = {QkBitTerm_Zero, QkBitTerm_One, QkBitTerm_Plus, QkBitTerm_Minus};
uint32_t indices[4] = {0, 1, 98, 99}; // <-- e.g. {1, 0, 99, 98} would be invalid
size_t boundaries[3] = {0, 2, 4};
QkObs *obs = qk_obs_new(
num_qubits, num_terms, num_bits, coeffs, bits, indices, boundaries
);
Safety
Behavior is undefined if any of the following conditions are violated:
coeffs
is a pointer to acomplex double
array of lengthnum_terms
bit_terms
is a pointer to an array of validQkBitTerm
elements of lengthnum_bits
indices
is a pointer to auint32_t
array of lengthnum_bits
, which is term-wise sorted in strict ascending order, and every element is smaller thannum_qubits
boundaries
is a pointer to asize_t
array of lengthnum_terms + 1
, which is sorted in ascending order, the first element is 0 and the last element is smaller thannum_terms
Parameters
- num_qubits – The number of qubits the observable is defined on.
- num_terms – The number of terms.
- num_bits – The total number of non-identity bit terms.
- coeffs – A pointer to the first element of the coefficients array, which has length
num_terms
. - bit_terms – A pointer to the first element of the bit terms array, which has length
num_bits
. - indices – A pointer to the first element of the indices array, which has length
num_bits
. Note that, per term, these must be sorted incrementally. - boundaries – A pointer to the first element of the boundaries array, which has length
num_terms + 1
.
Returns
If the input data was coherent and the construction successful, the result is a pointer to the observable. Otherwise a null pointer is returned.
qk_obs_free
void qk_obs_free(QkObs *obs)
Free the observable.
Example
QkObs *obs = qk_obs_zero(100);
qk_obs_free(obs);
Safety
Behavior is undefined if obs
is not either null or a valid pointer to a QkObs
.
Parameters
obs – A pointer to the observable to free.
qk_obs_add_term
QkExitCode qk_obs_add_term(QkObs *obs, const QkObsTerm *cterm)
Add a term to the observable.
Example
uint32_t num_qubits = 100;
QkObs *obs = qk_obs_zero(num_qubits);
complex double coeff = 1;
QkBitTerm bit_terms[3] = {QkBitTerm_X, QkBitTerm_Y, QkBitTerm_Z};
uint32_t indices[3] = {0, 1, 2};
QkObsTerm term = {&coeff, 3, bit_terms, indices, num_qubits};
int exit_code = qk_obs_add_term(obs, &term);
Safety
Behavior is undefined if any of the following is violated:
obs
is a valid, non-null pointer to aQkObs
cterm
is a valid, non-null pointer to aQkObsTerm
Parameters
- obs – A pointer to the observable.
- cterm – A pointer to the term to add.
Returns
An exit code. This is >0
if the term is incoherent or adding the term fails.
qk_obs_term
QkExitCode qk_obs_term(QkObs *obs, uint64_t index, QkObsTerm *out)
Get an observable term by reference.
A QkObsTerm
contains pointers to the indices and bit terms in the term, which can be used to modify the internal data of the observable. This can leave the observable in an incoherent state and should be avoided, unless great care is taken. It is generally safer to construct a new observable instead of attempting in-place modifications.
Example
QkObs *obs = qk_obs_identity(100);
QkObsTerm term;
int exit_code = qk_obs_term(obs, 0, &term);
// out-of-bounds indices return an error code
// int error = qk_obs_term(obs, 12, &term);
Safety
Behavior is undefined if any of the following is violated
obs
is a valid, non-null pointer to aQkObs
out
is a valid, non-null pointer to aQkObsTerm
Parameters
- obs – A pointer to the observable.
- index – The index of the term to get.
- out – A pointer to a
QkObsTerm
used to return the observable term.
Returns
An exit code.
qk_obs_num_terms
uintptr_t qk_obs_num_terms(const QkObs *obs)
Get the number of terms in the observable.
Example
QkObs *obs = qk_obs_identity(100);
size_t num_terms = qk_obs_num_terms(obs); // num_terms==1
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
Parameters
obs – A pointer to the observable.
Returns
The number of terms in the observable.
qk_obs_num_qubits
uint32_t qk_obs_num_qubits(const QkObs *obs)
Get the number of qubits the observable is defined on.
Example
QkObs *obs = qk_obs_identity(100);
uint32_t num_qubits = qk_obs_num_qubits(obs); // num_qubits==100
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
Parameters
obs – A pointer to the observable.
Returns
The number of qubits the observable is defined on.
qk_obs_len
uintptr_t qk_obs_len(const QkObs *obs)
Get the number of bit terms/indices in the observable.
Example
QkObs *obs = qk_obs_identity(100);
size_t len = qk_obs_len(obs); // len==0, as there are no non-trivial bit terms
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
Parameters
obs – A pointer to the observable.
Returns
The number of terms in the observable.
qk_obs_coeffs
QkComplex64 *qk_obs_coeffs(QkObs *obs)
Get a pointer to the coefficients.
This can be used to read and modify the observable’s coefficients. The resulting pointer is valid to read for qk_obs_num_terms(obs)
elements of complex double
.
Example
QkObs *obs = qk_obs_identity(100);
size_t num_terms = qk_obs_num_terms(obs);
complex double *coeffs = qk_obs_coeffs(obs);
for (size_t i = 0; i < num_terms; i++) {
printf("%f + i%f\n", creal(coeffs[i]), cimag(coeffs[i]));
}
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
Parameters
obs – A pointer to the observable.
Returns
A pointer to the coefficients.
qk_obs_indices
uint32_t *qk_obs_indices(QkObs *obs)
Get a pointer to the indices.
This can be used to read and modify the observable’s indices. The resulting pointer is valid to read for qk_obs_len(obs)
elements of size uint32_t
.
Example
uint32_t num_qubits = 100;
QkObs *obs = qk_obs_zero(num_qubits);
complex double coeff = 1;
QkBitTerm bit_terms[3] = {QkBitTerm_X, QkBitTerm_Y, QkBitTerm_Z};
uint32_t indices[3] = {0, 1, 2};
QkObsTerm term = {&coeff, 3, bit_terms, indices, num_qubits};
qk_obs_add_term(obs, &term);
size_T len = qk_obs_len(obs);
uint32_t *indices = qk_obs_indices(obs);
for (size_t i = 0; i < len; i++) {
printf("index %i: %i\n", i, indices[i]);
}
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
Parameters
obs – A pointer to the observable.
Returns
A pointer to the indices.
qk_obs_boundaries
uintptr_t *qk_obs_boundaries(QkObs *obs)
Get a pointer to the term boundaries.
This can be used to read and modify the observable’s term boundaries. The resulting pointer is valid to read for qk_obs_num_terms(obs) + 1
elements of size size_t
.
Example
uint32_t num_qubits = 100;
QkObs *obs = qk_obs_zero(num_qubits);
complex double coeff = 1;
QkBitTerm bit_terms[3] = {QkBitTerm_X, QkBitTerm_Y, QkBitTerm_Z};
uint32_t indices[3] = {0, 1, 2};
QkObsTerm term = {&coeff, 3, bit_terms, indices, num_qubits};
qk_obs_add_term(obs, &term);
size_t num_terms = qk_obs_num_terms(obs);
uint32_t *boundaries = qk_obs_boundaries(obs);
for (size_t i = 0; i < num_terms + 1; i++) {
printf("boundary %i: %i\n", i, boundaries[i]);
}
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
Parameters
obs – A pointer to the observable.
Returns
A pointer to the boundaries.
qk_obs_bit_terms
QkBitTerm *qk_obs_bit_terms(QkObs *obs)
Get a pointer to the bit terms.
This can be used to read and modify the observable’s bit terms. The resulting pointer is valid to read for qk_obs_len(obs)
elements of size uint8_t
.
Example
uint32_t num_qubits = 100;
QkObs *obs = qk_obs_zero(num_qubits);
complex double coeff = 1;
QkBitTerm bit_terms[3] = {QkBitTerm_X, QkBitTerm_Y, QkBitTerm_Z};
uint32_t indices[3] = {0, 1, 2};
QkObsTerm term = {&coeff, 3, bit_terms, indices, num_qubits};
qk_obs_add_term(obs, &term);
size_t len = qk_obs_len(obs);
QkBitTerm *bits = qk_obs_bit_terms(obs);
for (size_t i = 0; i < len; i++) {
printf("bit term %i: %i\n", i, bits[i]);
}
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
, or if invalid valus are written into the resulting QkBitTerm
pointer.
Parameters
obs – A pointer to the observable.
Returns
A pointer to the bit terms.
qk_obs_multiply
QkObs *qk_obs_multiply(const QkObs *obs, const QkComplex64 *coeff)
Multiply the observable by a complex coefficient.
Example
QkObs *obs = qk_obs_identity(100);
complex double coeff = 2;
QkObs *result = qk_obs_multiply(obs, &coeff);
Safety
Behavior is undefined if any of the following is violated
obs
is a valid, non-null pointer to aQkObs
coeff
is a valid, non-null pointer to acomplex double
Parameters
- obs – A pointer to the observable.
- coeff – The coefficient to multiply the observable with.
qk_obs_add
QkObs *qk_obs_add(const QkObs *left, const QkObs *right)
Add two observables.
Example
QkObs *left = qk_obs_identity(100);
QkObs *right = qk_obs_zero(100);
QkObs *result = qk_obs_add(left, right);
Safety
Behavior is undefined if left
or right
are not valid, non-null pointers to QkObs
\ s.
Parameters
- left – A pointer to the left observable.
- right – A pointer to the right observable.
Returns
A pointer to the result left + right
.
qk_obs_compose
QkObs *qk_obs_compose(const QkObs *first, const QkObs *second)
Compose (multiply) two observables.
Example
QkObs *first = qk_obs_zero(100);
QkObs *second = qk_obs_identity(100);
QkObs *result = qk_obs_compose(first, second);
Safety
Behavior is undefined if first
or second
are not valid, non-null pointers to QkObs
\ s.
Parameters
- first – One observable.
- second – The other observable.
Returns
first.compose(second)
which equals the observable result = second @ first
, in terms of the matrix multiplication @
.
qk_obs_compose_map
QkObs *qk_obs_compose_map(const QkObs *first, const QkObs *second, const uint32_t *qargs)
Compose (multiply) two observables according to a custom qubit order.
Notably, this allows composing two observables of different size.
Example
QkObs *first = qk_obs_zero(100);
QkObs *second = qk_obs_identity(100);
QkObs *result = qk_obs_compose(first, second);
Safety
To call this function safely
first
andsecond
must be valid, non-null pointers toQkObs
\ sqargs
must point to an array ofuint32_t
, readable forqk_obs_num_qubits(second)
elements (meaning the number of qubits insecond
)
Parameters
- first – One observable.
- second – The other observable. The number of qubits must match the length of
qargs
. - qargs – The qubit arguments specified which indices in
first
to associate with the ones insecond
.
Returns
first.compose(second)
which equals the observable result = second @ first
, in terms of the matrix multiplication @
.
qk_obs_canonicalize
QkObs *qk_obs_canonicalize(const QkObs *obs, double tol)
Calculate the canonical representation of the observable.
Example
QkObs *iden = qk_obs_identity(100);
QkObs *two = qk_obs_add(iden, iden);
double tol = 1e-6;
QkObs *canonical = qk_obs_canonicalize(two);
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
Parameters
- obs – A pointer to the observable.
- tol – The tolerance below which coefficients are considered to be zero.
Returns
The canonical representation of the observable.
qk_obs_copy
QkObs *qk_obs_copy(const QkObs *obs)
Copy the observable.
Example
QkObs *original = qk_obs_identity(100);
QkObs *copied = qk_obs_copy(original);
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
Parameters
obs – A pointer to the observable.
Returns
A pointer to a copy of the observable.
qk_obs_equal
bool qk_obs_equal(const QkObs *obs, const QkObs *other)
Compare two observables for equality.
Note that this does not compare mathematical equality, but data equality. This means that two observables might represent the same observable but not compare as equal.
Example
QkObs *observable = qk_obs_identity(100);
QkObs *other = qk_obs_identity(100);
bool are_equal = qk_obs_equal(observable, other);
Safety
Behavior is undefined if obs
or other
are not valid, non-null pointers to QkObs
\ s.
Parameters
- obs – A pointer to one observable.
- other – A pointer to another observable.
Returns
true
if the observables are equal, false
otherwise.
qk_obs_str
char *qk_obs_str(const QkObs *obs)
Return a string representation of a QkObs
.
Example
QkObs *obs = qk_obs_identity(100);
char *string = qk_obs_str(obs);
qk_str_free(string);
Safety
Behavior is undefined obs
is not a valid, non-null pointer to a QkObs
.
The string must not be freed with the normal C free, you must use qk_str_free
to free the memory consumed by the String. Not calling qk_str_free
will lead to a memory leak.
Do not change the length of the string after it’s returned (by writing a nul byte somewhere inside the string or removing the final one), although values can be mutated.
Parameters
obs – A pointer to the QkObs
to get the string for.
Returns
A pointer to a nul-terminated char array of the string representation for obs
qk_str_free
void qk_str_free(char *string)
Free a string representation.
Safety
Behavior is undefined if str
is not a pointer returned by qk_obs_str
or qk_obsterm_str
.
Parameters
string – A pointer to the returned string representation from qk_obs_str
or qk_obsterm_str
.
qk_obs_to_python
PyObject *qk_obs_to_python(const QkObs *obs)
Convert to a Python-space SparseObservable
.
Safety
Behavior is undefined if obs
is not a valid, non-null pointer to a QkObs
.
It is assumed that the thread currently executing this function holds the Python GIL this is required to create the Python object returned by this function.
Parameters
obs – The C-space QkObs
pointer.
Returns
A Python object representing the SparseObservable
.