• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Qiskit / qiskit / 13659671616

04 Mar 2025 05:42PM CUT coverage: 87.12% (+0.06%) from 87.064%
13659671616

Pull #13836

github

web-flow
Merge 4073279af into 091228cf1
Pull Request #13836: `SparseObservable` evolution

246 of 250 new or added lines in 6 files covered. (98.4%)

187 existing lines in 13 files now uncovered.

76554 of 87872 relevant lines covered (87.12%)

326790.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.14
/crates/accelerate/src/sparse_observable/mod.rs
1
// This code is part of Qiskit.
2
//
3
// (C) Copyright IBM 2024
4
//
5
// This code is licensed under the Apache License, Version 2.0. You may
6
// obtain a copy of this license in the LICENSE.txt file in the root directory
7
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8
//
9
// Any modifications or derivative works of this code must retain this
10
// copyright notice, and modified files need to carry a notice indicating
11
// that they have been altered from the originals.
12

13
mod lookup;
14

15
use hashbrown::HashSet;
16
use indexmap::IndexSet;
17
use itertools::Itertools;
18
use ndarray::Array2;
19
use num_complex::Complex64;
20
use num_traits::Zero;
21
use numpy::{
22
    PyArray1, PyArray2, PyArrayDescr, PyArrayDescrMethods, PyArrayLike1, PyArrayMethods,
23
    PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods,
24
};
25
use pyo3::{
26
    exceptions::{PyRuntimeError, PyTypeError, PyValueError, PyZeroDivisionError},
27
    intern,
28
    prelude::*,
29
    sync::GILOnceCell,
30
    types::{IntoPyDict, PyList, PyString, PyTuple, PyType},
31
    IntoPyObjectExt, PyErr,
32
};
33
use std::{
34
    cmp::Ordering,
35
    collections::btree_map,
36
    ops::{AddAssign, DivAssign, MulAssign, SubAssign},
37
    sync::{Arc, RwLock},
38
};
39
use thiserror::Error;
40

41
use qiskit_circuit::{
42
    imports::{ImportOnceCell, NUMPY_COPY_ONLY_IF_NEEDED},
43
    slice::{PySequenceIndex, SequenceIndex},
44
};
45

46
static PAULI_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Pauli");
47
static PAULI_LIST_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "PauliList");
48
static SPARSE_PAULI_OP_TYPE: ImportOnceCell =
49
    ImportOnceCell::new("qiskit.quantum_info", "SparsePauliOp");
50
static BIT_TERM_PY_ENUM: GILOnceCell<Py<PyType>> = GILOnceCell::new();
51
static BIT_TERM_INTO_PY: GILOnceCell<[Option<Py<PyAny>>; 16]> = GILOnceCell::new();
52

53
/// Named handle to the alphabet of single-qubit terms.
54
///
55
/// This is just the Rust-space representation.  We make a separate Python-space `enum.IntEnum` to
56
/// represent the same information, since we enforce strongly typed interactions in Rust, including
57
/// not allowing the stored values to be outside the valid `BitTerm`s, but doing so in Python would
58
/// make it very difficult to use the class efficiently with Numpy array views.  We attach this
59
/// sister class of `BitTerm` to `SparseObservable` as a scoped class.
60
///
61
/// # Representation
62
///
63
/// The `u8` representation and the exact numerical values of these are part of the public API.  The
64
/// low two bits are the symplectic Pauli representation of the required measurement basis with Z in
65
/// the Lsb0 and X in the Lsb1 (e.g. X and its eigenstate projectors all have their two low bits as
66
/// `0b10`).  The high two bits are `00` for the operator, `10` for the projector to the positive
67
/// eigenstate, and `01` for the projector to the negative eigenstate.
68
///
69
/// The `0b00_00` representation thus ends up being the natural representation of the `I` operator,
70
/// but this is never stored, and is not named in the enumeration.
71
///
72
/// This operator does not store phase terms of $-i$.  `BitTerm::Y` has `(1, 1)` as its `(z, x)`
73
/// representation, and represents exactly the Pauli Y operator; any additional phase is stored only
74
/// in a corresponding coefficient.
75
///
76
/// # Dev notes
77
///
78
/// This type is required to be `u8`, but it's a subtype of `u8` because not all `u8` are valid
79
/// `BitTerm`s.  For interop with Python space, we accept Numpy arrays of `u8` to represent this,
80
/// which we transmute into slices of `BitTerm`, after checking that all the values are correct (or
81
/// skipping the check if Python space promises that it upheld the checks).
82
///
83
/// We deliberately _don't_ impl `numpy::Element` for `BitTerm` (which would let us accept and
84
/// return `PyArray1<BitTerm>` at Python-space boundaries) so that it's clear when we're doing
85
/// the transmute, and we have to be explicit about the safety of that.
86
#[repr(u8)]
87
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
88
pub enum BitTerm {
89
    /// Pauli X operator.
90
    X = 0b00_10,
91
    /// Projector to the positive eigenstate of Pauli X.
92
    Plus = 0b10_10,
93
    /// Projector to the negative eigenstate of Pauli X.
94
    Minus = 0b01_10,
95
    /// Pauli Y operator.
96
    Y = 0b00_11,
97
    /// Projector to the positive eigenstate of Pauli Y.
98
    Right = 0b10_11,
99
    /// Projector to the negative eigenstate of Pauli Y.
100
    Left = 0b01_11,
101
    /// Pauli Z operator.
102
    Z = 0b00_01,
103
    /// Projector to the positive eigenstate of Pauli Z.
104
    Zero = 0b10_01,
105
    /// Projector to the negative eigenstate of Pauli Z.
106
    One = 0b01_01,
107
}
108
impl From<BitTerm> for u8 {
109
    fn from(value: BitTerm) -> u8 {
2,264✔
110
        value as u8
2,264✔
111
    }
2,264✔
112
}
113
unsafe impl ::bytemuck::CheckedBitPattern for BitTerm {
114
    type Bits = u8;
115

116
    #[inline(always)]
117
    fn is_valid_bit_pattern(bits: &Self::Bits) -> bool {
4,320✔
118
        *bits <= 0b11_11 && (*bits & 0b11_00) < 0b11_00 && (*bits & 0b00_11) != 0
4,320✔
119
    }
4,320✔
120
}
121
unsafe impl ::bytemuck::NoUninit for BitTerm {}
122

123
impl BitTerm {
124
    /// Get the label of this `BitTerm` used in Python-space applications.  This is a single-letter
125
    /// string.
126
    #[inline]
127
    pub fn py_label(&self) -> &'static str {
11,290✔
128
        // Note: these labels are part of the stable Python API and should not be changed.
11,290✔
129
        match self {
11,290✔
130
            Self::X => "X",
3,702✔
131
            Self::Plus => "+",
160✔
132
            Self::Minus => "-",
68✔
133
            Self::Y => "Y",
3,828✔
134
            Self::Right => "r",
68✔
135
            Self::Left => "l",
68✔
136
            Self::Z => "Z",
3,194✔
137
            Self::Zero => "0",
118✔
138
            Self::One => "1",
84✔
139
        }
140
    }
11,290✔
141

142
    /// Get the name of this `BitTerm`, which is how Python space refers to the integer constant.
143
    #[inline]
144
    pub fn py_name(&self) -> &'static str {
288✔
145
        // Note: these names are part of the stable Python API and should not be changed.
288✔
146
        match self {
288✔
147
            Self::X => "X",
32✔
148
            Self::Plus => "PLUS",
32✔
149
            Self::Minus => "MINUS",
32✔
150
            Self::Y => "Y",
32✔
151
            Self::Right => "RIGHT",
32✔
152
            Self::Left => "LEFT",
32✔
153
            Self::Z => "Z",
32✔
154
            Self::Zero => "ZERO",
32✔
155
            Self::One => "ONE",
32✔
156
        }
157
    }
288✔
158

159
    /// Attempt to convert a `u8` into `BitTerm`.
160
    ///
161
    /// Unlike the implementation of `TryFrom<u8>`, this allows `b'I'` as an alphabet letter,
162
    /// returning `Ok(None)` for it.  All other letters outside the alphabet return the complete
163
    /// error condition.
164
    #[inline]
165
    fn try_from_u8(value: u8) -> Result<Option<Self>, BitTermFromU8Error> {
15,744✔
166
        match value {
15,744✔
167
            b'+' => Ok(Some(BitTerm::Plus)),
664✔
168
            b'-' => Ok(Some(BitTerm::Minus)),
560✔
169
            b'0' => Ok(Some(BitTerm::Zero)),
750✔
170
            b'1' => Ok(Some(BitTerm::One)),
704✔
171
            b'I' => Ok(None),
3,506✔
172
            b'X' => Ok(Some(BitTerm::X)),
3,252✔
173
            b'Y' => Ok(Some(BitTerm::Y)),
2,532✔
174
            b'Z' => Ok(Some(BitTerm::Z)),
2,526✔
175
            b'l' => Ok(Some(BitTerm::Left)),
618✔
176
            b'r' => Ok(Some(BitTerm::Right)),
608✔
177
            _ => Err(BitTermFromU8Error(value)),
24✔
178
        }
179
    }
15,744✔
180

181
    /// Does this term include an X component in its ZX representation?
182
    ///
183
    /// This is true for the operators and eigenspace projectors associated with X and Y.
184
    pub fn has_x_component(&self) -> bool {
76✔
185
        ((*self as u8) & (Self::X as u8)) != 0
76✔
186
    }
76✔
187

188
    /// Does this term include a Z component in its ZX representation?
189
    ///
190
    /// This is true for the operators and eigenspace projectors associated with Y and Z.
191
    pub fn has_z_component(&self) -> bool {
76✔
192
        ((*self as u8) & (Self::Z as u8)) != 0
76✔
193
    }
76✔
194

195
    pub fn is_projector(&self) -> bool {
6,564✔
196
        !matches!(self, BitTerm::X | BitTerm::Y | BitTerm::Z)
6,564✔
197
    }
6,564✔
198
}
199

200
fn bit_term_as_pauli(bit: &BitTerm) -> &'static [(bool, Option<BitTerm>)] {
6,564✔
201
    match bit {
6,564✔
202
        BitTerm::X => &[(true, Some(BitTerm::X))],
2,238✔
203
        BitTerm::Y => &[(true, Some(BitTerm::Y))],
2,232✔
204
        BitTerm::Z => &[(true, Some(BitTerm::Z))],
1,498✔
205
        BitTerm::Plus => &[(true, None), (true, Some(BitTerm::X))],
96✔
206
        BitTerm::Minus => &[(true, None), (false, Some(BitTerm::X))],
62✔
207
        BitTerm::Right => &[(true, None), (true, Some(BitTerm::Y))],
90✔
208
        BitTerm::Left => &[(true, None), (false, Some(BitTerm::Y))],
112✔
209
        BitTerm::Zero => &[(true, None), (true, Some(BitTerm::Z))],
130✔
210
        BitTerm::One => &[(true, None), (false, Some(BitTerm::Z))],
106✔
211
    }
212
}
6,564✔
213

214
/// The error type for a failed conversion into `BitTerm`.
215
#[derive(Error, Debug)]
216
#[error("{0} is not a valid letter of the single-qubit alphabet")]
217
pub struct BitTermFromU8Error(u8);
218

219
// `BitTerm` allows safe `as` casting into `u8`.  This is the reverse, which is fallible, because
220
// `BitTerm` is a value-wise subtype of `u8`.
221
impl ::std::convert::TryFrom<u8> for BitTerm {
222
    type Error = BitTermFromU8Error;
223

224
    fn try_from(value: u8) -> Result<Self, Self::Error> {
3,450✔
225
        ::bytemuck::checked::try_cast(value).map_err(|_| BitTermFromU8Error(value))
3,450✔
226
    }
3,450✔
227
}
228

229
/// Error cases stemming from data coherence at the point of entry into `SparseObservable` from
230
/// user-provided arrays.
231
///
232
/// These most typically appear during [from_raw_parts], but can also be introduced by various
233
/// remapping arithmetic functions.
234
///
235
/// These are generally associated with the Python-space `ValueError` because all of the
236
/// `TypeError`-related ones are statically forbidden (within Rust) by the language, and conversion
237
/// failures on entry to Rust from Python space will automatically raise `TypeError`.
238
#[derive(Error, Debug)]
239
pub enum CoherenceError {
240
    #[error("`boundaries` ({boundaries}) must be one element longer than `coeffs` ({coeffs})")]
241
    MismatchedTermCount { coeffs: usize, boundaries: usize },
242
    #[error("`bit_terms` ({bit_terms}) and `indices` ({indices}) must be the same length")]
243
    MismatchedItemCount { bit_terms: usize, indices: usize },
244
    #[error("the first item of `boundaries` ({0}) must be 0")]
245
    BadInitialBoundary(usize),
246
    #[error("the last item of `boundaries` ({last}) must match the length of `bit_terms` and `indices` ({items})")]
247
    BadFinalBoundary { last: usize, items: usize },
248
    #[error("all qubit indices must be less than the number of qubits")]
249
    BitIndexTooHigh,
250
    #[error("the values in `boundaries` include backwards slices")]
251
    DecreasingBoundaries,
252
    #[error("the values in `indices` are not term-wise increasing")]
253
    UnsortedIndices,
254
    #[error("the input contains duplicate qubits")]
255
    DuplicateIndices,
256
    #[error("the provided qubit mapping does not account for all contained qubits")]
257
    IndexMapTooSmall,
258
    #[error("cannot shrink the qubit count in an observable from {current} to {target}")]
259
    NotEnoughQubits { current: usize, target: usize },
260
}
261

262
/// An error related to processing of a string label (both dense and sparse).
263
#[derive(Error, Debug)]
264
pub enum LabelError {
265
    #[error("label with length {label} cannot be added to a {num_qubits}-qubit operator")]
266
    WrongLengthDense { num_qubits: u32, label: usize },
267
    #[error("label with length {label} does not match indices of length {indices}")]
268
    WrongLengthIndices { label: usize, indices: usize },
269
    #[error("index {index} is out of range for a {num_qubits}-qubit operator")]
270
    BadIndex { index: u32, num_qubits: u32 },
271
    #[error("index {index} is duplicated in a single specifier")]
272
    DuplicateIndex { index: u32 },
273
    #[error("labels must only contain letters from the alphabet 'IXYZ+-rl01'")]
274
    OutsideAlphabet,
275
}
276

277
#[derive(Error, Debug)]
278
pub enum ArithmeticError {
279
    #[error("mismatched numbers of qubits: {left}, {right}")]
280
    MismatchedQubits { left: u32, right: u32 },
281
}
282

283
/// One part of the type of the iteration value from [PairwiseOrdered].
284
///
285
/// The struct iterates over two sorted lists, and returns values from the left iterator, the right
286
/// iterator, or both simultaneously, depending on some "ordering" key attached to each.  This
287
/// `enum` is to pass on the information on which iterator is being returned from.
288
enum Paired<T> {
289
    Left(T),
290
    Right(T),
291
    Both(T, T),
292
}
293

294
/// An iterator combinator that zip-merges two sorted iterators.
295
///
296
/// This is created by [pairwise_ordered]; see that method for the description.
297
struct PairwiseOrdered<C, T, I1, I2>
298
where
299
    C: Ord,
300
    I1: Iterator<Item = (C, T)>,
301
    I2: Iterator<Item = (C, T)>,
302
{
303
    left: ::std::iter::Peekable<I1>,
304
    right: ::std::iter::Peekable<I2>,
305
}
306
impl<C, T, I1, I2> Iterator for PairwiseOrdered<C, T, I1, I2>
307
where
308
    C: Ord,
309
    I1: Iterator<Item = (C, T)>,
310
    I2: Iterator<Item = (C, T)>,
311
{
312
    type Item = (C, Paired<T>);
313

314
    fn next(&mut self) -> Option<Self::Item> {
1,244✔
315
        let order = match (self.left.peek(), self.right.peek()) {
1,244✔
316
            (None, None) => return None,
458✔
317
            (Some(_), None) => Ordering::Less,
150✔
318
            (None, Some(_)) => Ordering::Greater,
166✔
319
            (Some((left, _)), Some((right, _))) => left.cmp(right),
470✔
320
        };
321
        match order {
786✔
322
            Ordering::Less => self.left.next().map(|(i, value)| (i, Paired::Left(value))),
230✔
323
            Ordering::Greater => self
262✔
324
                .right
262✔
325
                .next()
262✔
326
                .map(|(i, value)| (i, Paired::Right(value))),
262✔
327
            Ordering::Equal => {
328
                let (index, left) = self.left.next().unwrap();
294✔
329
                let (_, right) = self.right.next().unwrap();
294✔
330
                Some((index, Paired::Both(left, right)))
294✔
331
            }
332
        }
333
    }
1,244✔
334
    fn size_hint(&self) -> (usize, Option<usize>) {
×
335
        let left = self.left.size_hint();
×
336
        let right = self.right.size_hint();
×
337
        (
×
338
            left.0.max(right.0),
×
339
            left.1.and_then(|left| right.1.map(|right| left.max(right))),
×
340
        )
×
341
    }
×
342
}
343
/// An iterator combinator that zip-merges two sorted iterators.
344
///
345
/// The two iterators must yield the same items, where each item comprises some totally ordered
346
/// index, and an associated value.  Both input iterators must be sorted in terms of the index.  The
347
/// output iteration is over 2-tuples, also in sorted order, of the seen ordered index values, and a
348
/// [Paired] object indicating which iterator (or both) the values were drawn from.
349
fn pairwise_ordered<C, T, I1, I2>(
470✔
350
    left: I1,
470✔
351
    right: I2,
470✔
352
) -> PairwiseOrdered<C, T, <I1 as IntoIterator>::IntoIter, <I2 as IntoIterator>::IntoIter>
470✔
353
where
470✔
354
    C: Ord,
470✔
355
    I1: IntoIterator<Item = (C, T)>,
470✔
356
    I2: IntoIterator<Item = (C, T)>,
470✔
357
{
470✔
358
    PairwiseOrdered {
470✔
359
        left: left.into_iter().peekable(),
470✔
360
        right: right.into_iter().peekable(),
470✔
361
    }
470✔
362
}
470✔
363

364
/// An observable over Pauli bases that stores its data in a qubit-sparse format.
365
///
366
/// See [PySparseObservable] for detailed docs.
367
#[derive(Clone, Debug, PartialEq)]
368
pub struct SparseObservable {
369
    /// The number of qubits the operator acts on.  This is not inferable from any other shape or
370
    /// values, since identities are not stored explicitly.
371
    num_qubits: u32,
372
    /// The coefficients of each abstract term in in the sum.  This has as many elements as terms in
373
    /// the sum.
374
    coeffs: Vec<Complex64>,
375
    /// A flat list of single-qubit terms.  This is more naturally a list of lists, but is stored
376
    /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.`
377
    bit_terms: Vec<BitTerm>,
378
    /// A flat list of the qubit indices that the corresponding entries in `bit_terms` act on.  This
379
    /// list must always be term-wise sorted, where a term is a sublist as denoted by `boundaries`.
380
    indices: Vec<u32>,
381
    /// Indices that partition `bit_terms` and `indices` into sublists for each individual term in
382
    /// the sum.  `boundaries[0]..boundaries[1]` is the range of indices into `bit_terms` and
383
    /// `indices` that correspond to the first term of the sum.  All unspecified qubit indices are
384
    /// implicitly the identity.  This is one item longer than `coeffs`, since `boundaries[0]` is
385
    /// always an explicit zero (for algorithmic ease).
386
    boundaries: Vec<usize>,
387
}
388

389
impl SparseObservable {
390
    /// Create a new observable from the raw components that make it up.
391
    ///
392
    /// This checks the input values for data coherence on entry.  If you are certain you have the
393
    /// correct values, you can call `new_unchecked` instead.
394
    pub fn new(
2,320✔
395
        num_qubits: u32,
2,320✔
396
        coeffs: Vec<Complex64>,
2,320✔
397
        bit_terms: Vec<BitTerm>,
2,320✔
398
        indices: Vec<u32>,
2,320✔
399
        boundaries: Vec<usize>,
2,320✔
400
    ) -> Result<Self, CoherenceError> {
2,320✔
401
        if coeffs.len() + 1 != boundaries.len() {
2,320✔
402
            return Err(CoherenceError::MismatchedTermCount {
2✔
403
                coeffs: coeffs.len(),
2✔
404
                boundaries: boundaries.len(),
2✔
405
            });
2✔
406
        }
2,318✔
407
        if bit_terms.len() != indices.len() {
2,318✔
408
            return Err(CoherenceError::MismatchedItemCount {
4✔
409
                bit_terms: bit_terms.len(),
4✔
410
                indices: indices.len(),
4✔
411
            });
4✔
412
        }
2,314✔
413
        // We already checked that `boundaries` is at least length 1.
2,314✔
414
        if *boundaries.first().unwrap() != 0 {
2,314✔
415
            return Err(CoherenceError::BadInitialBoundary(boundaries[0]));
2✔
416
        }
2,312✔
417
        if *boundaries.last().unwrap() != indices.len() {
2,312✔
418
            return Err(CoherenceError::BadFinalBoundary {
4✔
419
                last: *boundaries.last().unwrap(),
4✔
420
                items: indices.len(),
4✔
421
            });
4✔
422
        }
2,308✔
423
        for (&left, &right) in boundaries[..].iter().zip(&boundaries[1..]) {
3,206✔
424
            if right < left {
3,206✔
425
                return Err(CoherenceError::DecreasingBoundaries);
2✔
426
            }
3,204✔
427
            let indices = &indices[left..right];
3,204✔
428
            if !indices.is_empty() {
3,204✔
429
                for (index_left, index_right) in indices[..].iter().zip(&indices[1..]) {
6,180✔
430
                    if index_left == index_right {
6,180✔
431
                        return Err(CoherenceError::DuplicateIndices);
×
432
                    } else if index_left > index_right {
6,180✔
433
                        return Err(CoherenceError::UnsortedIndices);
2✔
434
                    }
6,178✔
435
                }
436
            }
138✔
437
            if indices.last().map(|&ix| ix >= num_qubits).unwrap_or(false) {
3,202✔
438
                return Err(CoherenceError::BitIndexTooHigh);
4✔
439
            }
3,198✔
440
        }
441
        // SAFETY: we've just done the coherence checks.
442
        Ok(unsafe { Self::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) })
2,300✔
443
    }
2,320✔
444

445
    /// Create a new observable from the raw components without checking data coherence.
446
    ///
447
    /// # Safety
448
    ///
449
    /// It is up to the caller to ensure that the data-coherence requirements, as enumerated in the
450
    /// struct-level documentation, have been upheld.
451
    #[inline(always)]
452
    pub unsafe fn new_unchecked(
2,358✔
453
        num_qubits: u32,
2,358✔
454
        coeffs: Vec<Complex64>,
2,358✔
455
        bit_terms: Vec<BitTerm>,
2,358✔
456
        indices: Vec<u32>,
2,358✔
457
        boundaries: Vec<usize>,
2,358✔
458
    ) -> Self {
2,358✔
459
        Self {
2,358✔
460
            num_qubits,
2,358✔
461
            coeffs,
2,358✔
462
            bit_terms,
2,358✔
463
            indices,
2,358✔
464
            boundaries,
2,358✔
465
        }
2,358✔
466
    }
2,358✔
467

468
    /// Create a zero operator with pre-allocated space for the given number of summands and
469
    /// single-qubit bit terms.
470
    #[inline]
471
    pub fn with_capacity(num_qubits: u32, num_terms: usize, num_bit_terms: usize) -> Self {
4,080✔
472
        Self {
4,080✔
473
            num_qubits,
4,080✔
474
            coeffs: Vec::with_capacity(num_terms),
4,080✔
475
            bit_terms: Vec::with_capacity(num_bit_terms),
4,080✔
476
            indices: Vec::with_capacity(num_bit_terms),
4,080✔
477
            boundaries: {
4,080✔
478
                let mut boundaries = Vec::with_capacity(num_terms + 1);
4,080✔
479
                boundaries.push(0);
4,080✔
480
                boundaries
4,080✔
481
            },
4,080✔
482
        }
4,080✔
483
    }
4,080✔
484

485
    /// Get an iterator over the individual terms of the operator.
486
    ///
487
    /// Recall that two [SparseObservable]s that have different term orders can still represent the
488
    /// same object.  Use [canonicalize] to apply a canonical ordering to the terms.
489
    pub fn iter(&'_ self) -> impl ExactSizeIterator<Item = SparseTermView<'_>> + '_ {
7,620✔
490
        self.coeffs.iter().enumerate().map(|(i, coeff)| {
10,118✔
491
            let start = self.boundaries[i];
10,118✔
492
            let end = self.boundaries[i + 1];
10,118✔
493
            SparseTermView {
10,118✔
494
                num_qubits: self.num_qubits,
10,118✔
495
                coeff: *coeff,
10,118✔
496
                bit_terms: &self.bit_terms[start..end],
10,118✔
497
                indices: &self.indices[start..end],
10,118✔
498
            }
10,118✔
499
        })
10,118✔
500
    }
7,620✔
501

502
    /// Get an iterator over the individual terms of the operator that allows in-place mutation.
503
    ///
504
    /// The length and indices of these views cannot be mutated, since both would allow breaking
505
    /// data coherence.
506
    pub fn iter_mut(&mut self) -> IterMut<'_> {
264✔
507
        self.into()
264✔
508
    }
264✔
509

510
    /// Get the number of qubits the observable is defined on.
511
    #[inline]
512
    pub fn num_qubits(&self) -> u32 {
7,780✔
513
        self.num_qubits
7,780✔
514
    }
7,780✔
515

516
    /// Get the number of terms in the observable.
517
    #[inline]
518
    pub fn num_terms(&self) -> usize {
1,542✔
519
        self.coeffs.len()
1,542✔
520
    }
1,542✔
521

522
    /// Get the coefficients of the terms.
523
    #[inline]
524
    pub fn coeffs(&self) -> &[Complex64] {
1,670✔
525
        &self.coeffs
1,670✔
526
    }
1,670✔
527

528
    /// Get a mutable slice of the coefficients.
529
    #[inline]
530
    pub fn coeffs_mut(&mut self) -> &mut [Complex64] {
196✔
531
        &mut self.coeffs
196✔
532
    }
196✔
533

534
    /// Get the indices of each [BitTerm].
535
    #[inline]
536
    pub fn indices(&self) -> &[u32] {
1,564✔
537
        &self.indices
1,564✔
538
    }
1,564✔
539

540
    /// Get a mutable slice of the indices.
541
    ///
542
    /// # Safety
543
    ///
544
    /// Modifying the indices can cause an incoherent state of the [SparseObservable].
545
    /// It should be ensured that the indices are consistent with the coeffs, bit_terms, and
546
    /// boundaries.
547
    #[inline]
548
    pub unsafe fn indices_mut(&mut self) -> &mut [u32] {
10✔
549
        &mut self.indices
10✔
550
    }
10✔
551

552
    /// Get the boundaries of each term.
553
    #[inline]
554
    pub fn boundaries(&self) -> &[usize] {
2,308✔
555
        &self.boundaries
2,308✔
556
    }
2,308✔
557

558
    /// Get a mutable slice of the boundaries.
559
    ///
560
    /// # Safety
561
    ///
562
    /// Modifying the boundaries can cause an incoherent state of the [SparseObservable].
563
    /// It should be ensured that the boundaries are sorted and the length/elements are consistent
564
    /// with the coeffs, bit_terms, and indices.
565
    #[inline]
566
    pub unsafe fn boundaries_mut(&mut self) -> &mut [usize] {
8✔
567
        &mut self.boundaries
8✔
568
    }
8✔
569

570
    /// Get the [BitTerm]s in the observable.
571
    #[inline]
572
    pub fn bit_terms(&self) -> &[BitTerm] {
1,428✔
573
        &self.bit_terms
1,428✔
574
    }
1,428✔
575

576
    /// Get a muitable slice of the bit terms.
577
    #[inline]
578
    pub fn bit_terms_mut(&mut self) -> &mut [BitTerm] {
90✔
579
        &mut self.bit_terms
90✔
580
    }
90✔
581

582
    /// Create a zero operator on ``num_qubits`` qubits.
583
    pub fn zero(num_qubits: u32) -> Self {
2,140✔
584
        Self::with_capacity(num_qubits, 0, 0)
2,140✔
585
    }
2,140✔
586

587
    /// Create an identity operator on ``num_qubits`` qubits.
588
    pub fn identity(num_qubits: u32) -> Self {
678✔
589
        Self {
678✔
590
            num_qubits,
678✔
591
            coeffs: vec![Complex64::new(1.0, 0.0)],
678✔
592
            bit_terms: vec![],
678✔
593
            indices: vec![],
678✔
594
            boundaries: vec![0, 0],
678✔
595
        }
678✔
596
    }
678✔
597

598
    /// Clear all the terms from this operator, making it equal to the zero operator again.
599
    ///
600
    /// This does not change the capacity of the internal allocations, so subsequent addition or
601
    /// substraction operations may not need to reallocate.
602
    pub fn clear(&mut self) {
28✔
603
        self.coeffs.clear();
28✔
604
        self.bit_terms.clear();
28✔
605
        self.indices.clear();
28✔
606
        self.boundaries.truncate(1);
28✔
607
    }
28✔
608

609
    /// Reduce the observable to its canonical form.
610
    ///
611
    /// This sums like terms, removing them if the final complex coefficient's absolute value is
612
    /// less than or equal to the tolerance.  The terms are reordered to some canonical ordering.
613
    ///
614
    /// This function is idempotent.
615
    pub fn canonicalize(&self, tol: f64) -> SparseObservable {
148✔
616
        let mut terms = btree_map::BTreeMap::new();
148✔
617
        for term in self.iter() {
396✔
618
            terms
396✔
619
                .entry((term.indices, term.bit_terms))
396✔
620
                .and_modify(|c| *c += term.coeff)
396✔
621
                .or_insert(term.coeff);
396✔
622
        }
396✔
623
        let mut out = SparseObservable::zero(self.num_qubits);
148✔
624
        for ((indices, bit_terms), coeff) in terms {
516✔
625
            if coeff.norm_sqr() <= tol * tol {
368✔
626
                continue;
22✔
627
            }
346✔
628
            out.coeffs.push(coeff);
346✔
629
            out.bit_terms.extend_from_slice(bit_terms);
346✔
630
            out.indices.extend_from_slice(indices);
346✔
631
            out.boundaries.push(out.indices.len());
346✔
632
        }
633
        out
148✔
634
    }
148✔
635

636
    /// Tensor product of `self` with `other`.
637
    ///
638
    /// The bit ordering is defined such that the qubit indices of `other` will remain the same, and
639
    /// the indices of `self` will be offset by the number of qubits in `other`.  This is the same
640
    /// convention as used by the rest of Qiskit's `quantum_info` operators.
641
    ///
642
    /// Put another way, in the simplest case of two observables formed of dense labels:
643
    ///
644
    /// ```
645
    /// let mut left = SparseObservable::zero(5);
646
    /// left.add_dense_label("IXY+Z", Complex64::new(1.0, 0.0));
647
    /// let mut right = SparseObservable::zero(6);
648
    /// right.add_dense_label("IIrl01", Complex64::new(1.0, 0.0));
649
    ///
650
    /// // The result is the concatenation of the two labels.
651
    /// let mut out = SparseObservable::zero(11);
652
    /// out.add_dense_label("IXY+ZIIrl01", Complex64::new(1.0, 0.0));
653
    ///
654
    /// assert_eq!(left.tensor(right), out);
655
    /// ```
656
    pub fn tensor(&self, other: &SparseObservable) -> SparseObservable {
1,204✔
657
        let mut out = SparseObservable::with_capacity(
1,204✔
658
            self.num_qubits + other.num_qubits,
1,204✔
659
            self.coeffs.len() * other.coeffs.len(),
1,204✔
660
            other.coeffs.len() * self.bit_terms.len() + self.coeffs.len() * other.bit_terms.len(),
1,204✔
661
        );
1,204✔
662
        let mut self_indices = Vec::new();
1,204✔
663
        for self_term in self.iter() {
1,264✔
664
            self_indices.clear();
1,264✔
665
            self_indices.extend(self_term.indices.iter().map(|i| i + other.num_qubits));
2,508✔
666
            for other_term in other.iter() {
1,276✔
667
                out.coeffs.push(self_term.coeff * other_term.coeff);
1,276✔
668
                out.indices.extend_from_slice(other_term.indices);
1,276✔
669
                out.indices.extend_from_slice(&self_indices);
1,276✔
670
                out.bit_terms.extend_from_slice(other_term.bit_terms);
1,276✔
671
                out.bit_terms.extend_from_slice(self_term.bit_terms);
1,276✔
672
                out.boundaries.push(out.bit_terms.len());
1,276✔
673
            }
1,276✔
674
        }
675
        out
1,204✔
676
    }
1,204✔
677

678
    /// Calculate the adjoint of this observable.
679
    ///
680
    /// This is well defined in the abstract mathematical sense.  All the terms of the single-qubit
681
    /// alphabet are self-adjoint, so the result of this operation is the same observable, except
682
    /// its coefficients are all their complex conjugates.
683
    pub fn adjoint(&self) -> SparseObservable {
126✔
684
        SparseObservable {
126✔
685
            num_qubits: self.num_qubits,
126✔
686
            coeffs: self.coeffs.iter().map(|c| c.conj()).collect(),
144✔
687
            bit_terms: self.bit_terms.clone(),
126✔
688
            indices: self.indices.clone(),
126✔
689
            boundaries: self.boundaries.clone(),
126✔
690
        }
126✔
691
    }
126✔
692

693
    /// Calculate the transpose.
694
    ///
695
    /// This operation transposes the individual bit terms but does directly act
696
    /// on the coefficients.
697
    pub fn transpose(&self) -> SparseObservable {
264✔
698
        let mut out = self.clone();
264✔
699
        for term in out.iter_mut() {
396✔
700
            for bit_term in term.bit_terms {
1,272✔
701
                match bit_term {
876✔
702
                    BitTerm::Y => {
132✔
703
                        *term.coeff = -*term.coeff;
132✔
704
                    }
132✔
705
                    BitTerm::Right => {
84✔
706
                        *bit_term = BitTerm::Left;
84✔
707
                    }
84✔
708
                    BitTerm::Left => {
84✔
709
                        *bit_term = BitTerm::Right;
84✔
710
                    }
84✔
711
                    _ => (),
576✔
712
                }
713
            }
714
        }
715
        out
264✔
716
    }
264✔
717

718
    /// Calculate the complex conjugate.
719
    ///
720
    /// This operation equals transposing the observable and complex conjugating the coefficients.
721
    pub fn conjugate(&self) -> SparseObservable {
132✔
722
        let mut out = self.transpose();
132✔
723
        for coeff in out.coeffs.iter_mut() {
198✔
724
            *coeff = coeff.conj();
198✔
725
        }
198✔
726
        out
132✔
727
    }
132✔
728

729
    /// Compose another [SparseObservable] onto this one.
730
    ///
731
    /// In terms of operator algebras, composition corresponds to left-multiplication:
732
    /// ``let c = a.compose(b);`` corresponds to $C = B A$.
733
    ///
734
    /// Beware that this function can cause exponential explosion of the memory usage of the
735
    /// observable, as the alphabet of [SparseObservable] is not closed under composition; the
736
    /// composition of two single-bit terms can be a sum, which multiplies the total number of
737
    /// terms.  This memory usage is not _necessarily_ inherent to the resultant observable, but
738
    /// finding an efficient re-factorization of the sum is generally equally computationally hard.
739
    /// It's better to use domain knowledge of your observables to minimize the number of terms that
740
    /// ever exist, rather than trying to simplify them after the fact.
741
    ///
742
    /// # Panics
743
    ///
744
    /// If `self` and `other` have different numbers of qubits.
745
    pub fn compose(&self, other: &SparseObservable) -> SparseObservable {
422✔
746
        if other.num_qubits != self.num_qubits {
422✔
747
            panic!(
×
748
                "operand ({}) has a different number of qubits to the base ({})",
×
749
                other.num_qubits, self.num_qubits
×
750
            );
×
751
        }
422✔
752
        let mut out = SparseObservable::zero(self.num_qubits);
422✔
753
        let mut term_state = compose::Iter::new(self.num_qubits);
422✔
754

755
        for left in other.iter() {
432✔
756
            for right in self.iter() {
456✔
757
                term_state.load_from(
456✔
758
                    left.coeff * right.coeff,
456✔
759
                    left.indices
456✔
760
                        .iter()
456✔
761
                        .copied()
456✔
762
                        .zip(left.bit_terms.iter().copied()),
456✔
763
                    right
456✔
764
                        .indices
456✔
765
                        .iter()
456✔
766
                        .copied()
456✔
767
                        .zip(right.bit_terms.iter().copied()),
456✔
768
                );
456✔
769
                out.boundaries.reserve(term_state.num_terms());
456✔
770
                out.coeffs.reserve(term_state.num_terms());
456✔
771
                out.indices
456✔
772
                    .reserve(term_state.num_terms() * term_state.term_len());
456✔
773
                out.bit_terms
456✔
774
                    .reserve(term_state.num_terms() * term_state.term_len());
456✔
775
                while let Some(term) = term_state.next() {
1,352✔
776
                    out.add_term(term)
896✔
777
                        .expect("qubit counts were checked during initialisation");
896✔
778
                }
896✔
779
            }
780
        }
781
        out
422✔
782
    }
422✔
783

784
    /// Compose another [SparseObservable] onto this one, remapping the qubits.
785
    ///
786
    /// The `qubit_fn` is called for each qubit in each term in `other` to determine which qubit in
787
    /// `self` it corresponds to; this should typically be a very cheap function to call, like a
788
    /// getter from a slice.
789
    ///
790
    /// See [compose] for further information.
791
    ///
792
    /// # Panics
793
    ///
794
    /// If `other` has more qubits than `self`, if the `qubit_fn` returns a qubit index greater
795
    /// or equal to the number of qubits in `self`, or if `qubit_fn` returns a duplicate index (this
796
    /// is only guaranteed to be detected if an individual term contains duplicates).
797
    pub fn compose_map(
6✔
798
        &self,
6✔
799
        other: &SparseObservable,
6✔
800
        mut qubit_fn: impl FnMut(u32) -> u32,
6✔
801
    ) -> SparseObservable {
6✔
802
        if other.num_qubits > self.num_qubits {
6✔
803
            panic!(
×
804
                "operand has more qubits ({}) than the base ({})",
×
805
                other.num_qubits, self.num_qubits
×
806
            );
×
807
        }
6✔
808
        let mut out = SparseObservable::zero(self.num_qubits);
6✔
809
        let mut mapped_term = btree_map::BTreeMap::<u32, BitTerm>::new();
6✔
810
        let mut term_state = compose::Iter::new(self.num_qubits);
6✔
811

812
        // This choice of loop ordering is because we already know that `self`'s `indices` are
813
        // sorted, but there's no reason that that the output of `qubit_fn` should maintain order,
814
        // and this way round, we sort as few times as possible.
815
        for left in other.iter() {
8✔
816
            mapped_term.clear();
8✔
817
            for (index, term) in left.indices.iter().zip(left.bit_terms) {
12✔
818
                let qubit = qubit_fn(*index);
12✔
819
                if qubit >= self.num_qubits {
12✔
820
                    panic!("remapped {index} to {qubit}, which is out of bounds");
×
821
                }
12✔
822
                if mapped_term.insert(qubit, *term).is_some() {
12✔
823
                    panic!("duplicate qubit {qubit} in remapped term");
×
824
                };
12✔
825
            }
826
            for right in self.iter() {
14✔
827
                term_state.load_from(
14✔
828
                    left.coeff * right.coeff,
14✔
829
                    mapped_term.iter().map(|(k, v)| (*k, *v)),
24✔
830
                    right
14✔
831
                        .indices
14✔
832
                        .iter()
14✔
833
                        .copied()
14✔
834
                        .zip(right.bit_terms.iter().copied()),
14✔
835
                );
14✔
836
                out.boundaries.reserve(term_state.num_terms());
14✔
837
                out.coeffs.reserve(term_state.num_terms());
14✔
838
                out.indices
14✔
839
                    .reserve(term_state.num_terms() * term_state.term_len());
14✔
840
                out.bit_terms
14✔
841
                    .reserve(term_state.num_terms() * term_state.term_len());
14✔
842
                while let Some(term) = term_state.next() {
38✔
843
                    out.add_term(term)
24✔
844
                        .expect("qubit counts were checked during initialisation");
24✔
845
                }
24✔
846
            }
847
        }
848
        out
6✔
849
    }
6✔
850

851
    /// Get a view onto a representation of a single sparse term.
852
    ///
853
    /// This is effectively an indexing operation into the [SparseObservable].  Recall that two
854
    /// [SparseObservable]s that have different term orders can still represent the same object.
855
    /// Use [canonicalize] to apply a canonical ordering to the terms.
856
    ///
857
    /// # Panics
858
    ///
859
    /// If the index is out of bounds.
860
    pub fn term(&self, index: usize) -> SparseTermView {
248✔
861
        debug_assert!(index < self.num_terms(), "index {index} out of bounds");
248✔
862
        let start = self.boundaries[index];
248✔
863
        let end = self.boundaries[index + 1];
248✔
864
        SparseTermView {
248✔
865
            num_qubits: self.num_qubits,
248✔
866
            coeff: self.coeffs[index],
248✔
867
            bit_terms: &self.bit_terms[start..end],
248✔
868
            indices: &self.indices[start..end],
248✔
869
        }
248✔
870
    }
248✔
871

872
    /// Expand all projectors into Pauli representation.
873
    ///
874
    /// # Warning
875
    ///
876
    /// This representation is highly inefficient for projectors. For example, a term with
877
    /// :math:`n` projectors :math:`|+\rangle\langle +|` will use :math:`2^n` Pauli terms.
878
    pub fn as_paulis(&self) -> Self {
2,030✔
879
        let mut paulis: Vec<BitTerm> = Vec::new(); // maybe get capacity here
2,030✔
880
        let mut indices: Vec<u32> = Vec::new();
2,030✔
881
        let mut coeffs: Vec<Complex64> = Vec::new();
2,030✔
882
        let mut boundaries: Vec<usize> = vec![0];
2,030✔
883

884
        for view in self.iter() {
2,364✔
885
            let num_projectors = view
2,364✔
886
                .bit_terms
2,364✔
887
                .iter()
2,364✔
888
                .filter(|&bit| bit.is_projector())
6,564✔
889
                .count();
2,364✔
890
            let div = 2_f64.powi(num_projectors as i32);
2,364✔
891

2,364✔
892
            let combinations = view
2,364✔
893
                .bit_terms
2,364✔
894
                .iter()
2,364✔
895
                .map(bit_term_as_pauli)
2,364✔
896
                .multi_cartesian_product();
2,364✔
897

898
            for combination in combinations {
6,208✔
899
                let mut positive = true; // keep track of the global sign
3,844✔
900

901
                for (index, (sign, bit)) in combination.iter().enumerate() {
13,806✔
902
                    positive ^= !sign; // accumulate the sign; global_sign *= local_sign
13,806✔
903
                    if let Some(bit) = bit {
13,806✔
904
                        paulis.push(*bit);
10,588✔
905
                        indices.push(view.indices[index]);
10,588✔
906
                    }
10,588✔
907
                }
908
                boundaries.push(paulis.len());
3,844✔
909

910
                let coeff = if positive { view.coeff } else { -view.coeff };
3,844✔
911
                coeffs.push(coeff / div)
3,844✔
912
            }
913
        }
914

915
        Self {
2,030✔
916
            num_qubits: self.num_qubits,
2,030✔
917
            coeffs,
2,030✔
918
            bit_terms: paulis,
2,030✔
919
            indices,
2,030✔
920
            boundaries,
2,030✔
921
        }
2,030✔
922
    }
2,030✔
923

924
    /// Add the term implied by a dense string label onto this observable.
925
    pub fn add_dense_label<L: AsRef<[u8]>>(
2,290✔
926
        &mut self,
2,290✔
927
        label: L,
2,290✔
928
        coeff: Complex64,
2,290✔
929
    ) -> Result<(), LabelError> {
2,290✔
930
        let label = label.as_ref();
2,290✔
931
        if label.len() != self.num_qubits() as usize {
2,290✔
932
            return Err(LabelError::WrongLengthDense {
12✔
933
                num_qubits: self.num_qubits(),
12✔
934
                label: label.len(),
12✔
935
            });
12✔
936
        }
2,278✔
937
        // The only valid characters in the alphabet are ASCII, so if we see something other than
938
        // ASCII, we're already in the failure path.
939
        for (i, letter) in label.iter().rev().enumerate() {
9,702✔
940
            match BitTerm::try_from_u8(*letter) {
9,702✔
941
                Ok(Some(term)) => {
6,184✔
942
                    self.bit_terms.push(term);
6,184✔
943
                    self.indices.push(i as u32);
6,184✔
944
                }
6,184✔
945
                Ok(None) => (),
3,498✔
946
                Err(_) => {
947
                    // Undo any modifications to ourselves so we stay in a consistent state.
948
                    let num_single_terms = self.boundaries[self.boundaries.len() - 1];
20✔
949
                    self.bit_terms.truncate(num_single_terms);
20✔
950
                    self.indices.truncate(num_single_terms);
20✔
951
                    return Err(LabelError::OutsideAlphabet);
20✔
952
                }
953
            }
954
        }
955
        self.coeffs.push(coeff);
2,258✔
956
        self.boundaries.push(self.bit_terms.len());
2,258✔
957
        Ok(())
2,258✔
958
    }
2,290✔
959

960
    /// Relabel the `indices` in the operator to new values.
961
    ///
962
    /// This fails if any of the new indices are too large, or if any mapping would cause a term to
963
    /// contain duplicates of the same index.  It may not detect if multiple qubits are mapped to
964
    /// the same index, if those qubits never appear together in the same term.  Such a mapping
965
    /// would not cause data-coherence problems (the output observable will be valid), but is
966
    /// unlikely to be what you intended.
967
    ///
968
    /// *Panics* if `new_qubits` is not long enough to map every index used in the operator.
969
    pub fn relabel_qubits_from_slice(&mut self, new_qubits: &[u32]) -> Result<(), CoherenceError> {
44✔
970
        for qubit in new_qubits {
286✔
971
            if *qubit >= self.num_qubits {
242✔
972
                return Err(CoherenceError::BitIndexTooHigh);
×
973
            }
242✔
974
        }
975
        let mut order = btree_map::BTreeMap::new();
44✔
976
        for i in 0..self.num_terms() {
98✔
977
            let start = self.boundaries[i];
98✔
978
            let end = self.boundaries[i + 1];
98✔
979
            for j in start..end {
222✔
980
                order.insert(new_qubits[self.indices[j] as usize], self.bit_terms[j]);
222✔
981
            }
222✔
982
            if order.len() != end - start {
98✔
983
                return Err(CoherenceError::DuplicateIndices);
×
984
            }
98✔
985
            for (index, dest) in order.keys().zip(&mut self.indices[start..end]) {
222✔
986
                *dest = *index;
222✔
987
            }
222✔
988
            for (bit_term, dest) in order.values().zip(&mut self.bit_terms[start..end]) {
222✔
989
                *dest = *bit_term;
222✔
990
            }
222✔
991
            order.clear();
98✔
992
        }
993
        Ok(())
44✔
994
    }
44✔
995

996
    /// Apply a transpiler layout.
997
    pub fn apply_layout(
72✔
998
        &self,
72✔
999
        layout: Option<&[u32]>,
72✔
1000
        num_qubits: u32,
72✔
1001
    ) -> Result<Self, CoherenceError> {
72✔
1002
        match layout {
72✔
1003
            None => {
1004
                let mut out = self.clone();
22✔
1005
                if num_qubits < self.num_qubits {
22✔
1006
                    // return Err(CoherenceError::BitIndexTooHigh);
1007
                    return Err(CoherenceError::NotEnoughQubits {
2✔
1008
                        current: self.num_qubits as usize,
2✔
1009
                        target: num_qubits as usize,
2✔
1010
                    });
2✔
1011
                }
20✔
1012
                out.num_qubits = num_qubits;
20✔
1013
                Ok(out)
20✔
1014
            }
1015
            Some(layout) => {
50✔
1016
                if layout.len() < self.num_qubits as usize {
50✔
1017
                    return Err(CoherenceError::IndexMapTooSmall);
2✔
1018
                }
48✔
1019
                if layout.iter().any(|qubit| *qubit >= num_qubits) {
256✔
1020
                    return Err(CoherenceError::BitIndexTooHigh);
2✔
1021
                }
46✔
1022
                if layout.iter().collect::<HashSet<_>>().len() != layout.len() {
46✔
1023
                    return Err(CoherenceError::DuplicateIndices);
2✔
1024
                }
44✔
1025
                let mut out = self.clone();
44✔
1026
                out.num_qubits = num_qubits;
44✔
1027
                out.relabel_qubits_from_slice(layout)?;
44✔
1028
                Ok(out)
44✔
1029
            }
1030
        }
1031
    }
72✔
1032

1033
    /// Add a single term to this operator.
1034
    pub fn add_term(&mut self, term: SparseTermView) -> Result<(), ArithmeticError> {
988✔
1035
        if self.num_qubits != term.num_qubits {
988✔
1036
            return Err(ArithmeticError::MismatchedQubits {
4✔
1037
                left: self.num_qubits,
4✔
1038
                right: term.num_qubits,
4✔
1039
            });
4✔
1040
        }
984✔
1041
        self.coeffs.push(term.coeff);
984✔
1042
        self.bit_terms.extend_from_slice(term.bit_terms);
984✔
1043
        self.indices.extend_from_slice(term.indices);
984✔
1044
        self.boundaries.push(self.bit_terms.len());
984✔
1045
        Ok(())
984✔
1046
    }
988✔
1047

1048
    /// Return a suitable Python error if two observables do not have equal numbers of qubits.
1049
    pub fn check_equal_qubits(&self, other: &SparseObservable) -> Result<(), ArithmeticError> {
192✔
1050
        if self.num_qubits != other.num_qubits {
192✔
1051
            Err(ArithmeticError::MismatchedQubits {
8✔
1052
                left: self.num_qubits,
8✔
1053
                right: other.num_qubits,
8✔
1054
            })
8✔
1055
        } else {
1056
            Ok(())
184✔
1057
        }
1058
    }
192✔
1059
}
1060

1061
impl ::std::ops::Add<&SparseObservable> for SparseObservable {
1062
    type Output = SparseObservable;
1063

1064
    fn add(mut self, rhs: &SparseObservable) -> SparseObservable {
×
1065
        self += rhs;
×
1066
        self
×
1067
    }
×
1068
}
1069
impl ::std::ops::Add for &SparseObservable {
1070
    type Output = SparseObservable;
1071

1072
    fn add(self, rhs: &SparseObservable) -> SparseObservable {
58✔
1073
        let mut out = SparseObservable::with_capacity(
58✔
1074
            self.num_qubits,
58✔
1075
            self.coeffs.len() + rhs.coeffs.len(),
58✔
1076
            self.bit_terms.len() + rhs.bit_terms.len(),
58✔
1077
        );
58✔
1078
        out += self;
58✔
1079
        out += rhs;
58✔
1080
        out
58✔
1081
    }
58✔
1082
}
1083
impl ::std::ops::AddAssign<&SparseObservable> for SparseObservable {
1084
    fn add_assign(&mut self, rhs: &SparseObservable) {
208✔
1085
        if self.num_qubits != rhs.num_qubits {
208✔
1086
            panic!("attempt to add two operators with incompatible qubit counts");
×
1087
        }
208✔
1088
        self.coeffs.extend_from_slice(&rhs.coeffs);
208✔
1089
        self.bit_terms.extend_from_slice(&rhs.bit_terms);
208✔
1090
        self.indices.extend_from_slice(&rhs.indices);
208✔
1091
        // We only need to write out the new endpoints, not the initial zero.
208✔
1092
        let offset = self.boundaries[self.boundaries.len() - 1];
208✔
1093
        self.boundaries
208✔
1094
            .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary));
208✔
1095
    }
208✔
1096
}
1097

1098
impl ::std::ops::Sub<&SparseObservable> for SparseObservable {
1099
    type Output = SparseObservable;
1100

1101
    fn sub(mut self, rhs: &SparseObservable) -> SparseObservable {
×
1102
        self -= rhs;
×
1103
        self
×
1104
    }
×
1105
}
1106
impl ::std::ops::Sub for &SparseObservable {
1107
    type Output = SparseObservable;
1108

1109
    fn sub(self, rhs: &SparseObservable) -> SparseObservable {
58✔
1110
        let mut out = SparseObservable::with_capacity(
58✔
1111
            self.num_qubits,
58✔
1112
            self.coeffs.len() + rhs.coeffs.len(),
58✔
1113
            self.bit_terms.len() + rhs.bit_terms.len(),
58✔
1114
        );
58✔
1115
        out += self;
58✔
1116
        out -= rhs;
58✔
1117
        out
58✔
1118
    }
58✔
1119
}
1120
impl ::std::ops::SubAssign<&SparseObservable> for SparseObservable {
1121
    fn sub_assign(&mut self, rhs: &SparseObservable) {
92✔
1122
        if self.num_qubits != rhs.num_qubits {
92✔
1123
            panic!("attempt to subtract two operators with incompatible qubit counts");
×
1124
        }
92✔
1125
        self.coeffs.extend(rhs.coeffs.iter().map(|coeff| -coeff));
92✔
1126
        self.bit_terms.extend_from_slice(&rhs.bit_terms);
92✔
1127
        self.indices.extend_from_slice(&rhs.indices);
92✔
1128
        // We only need to write out the new endpoints, not the initial zero.
92✔
1129
        let offset = self.boundaries[self.boundaries.len() - 1];
92✔
1130
        self.boundaries
92✔
1131
            .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary));
92✔
1132
    }
92✔
1133
}
1134

1135
impl ::std::ops::Mul<Complex64> for SparseObservable {
1136
    type Output = SparseObservable;
1137

1138
    fn mul(mut self, rhs: Complex64) -> SparseObservable {
×
1139
        self *= rhs;
×
1140
        self
×
1141
    }
×
1142
}
1143
impl ::std::ops::Mul<Complex64> for &SparseObservable {
1144
    type Output = SparseObservable;
1145

1146
    fn mul(self, rhs: Complex64) -> SparseObservable {
244✔
1147
        if rhs == Complex64::new(0.0, 0.0) {
244✔
1148
            SparseObservable::zero(self.num_qubits)
28✔
1149
        } else {
1150
            SparseObservable {
216✔
1151
                num_qubits: self.num_qubits,
216✔
1152
                coeffs: self.coeffs.iter().map(|c| c * rhs).collect(),
240✔
1153
                bit_terms: self.bit_terms.clone(),
216✔
1154
                indices: self.indices.clone(),
216✔
1155
                boundaries: self.boundaries.clone(),
216✔
1156
            }
216✔
1157
        }
1158
    }
244✔
1159
}
1160
impl ::std::ops::Mul<SparseObservable> for Complex64 {
1161
    type Output = SparseObservable;
1162

1163
    fn mul(self, mut rhs: SparseObservable) -> SparseObservable {
×
1164
        rhs *= self;
×
1165
        rhs
×
1166
    }
×
1167
}
1168
impl ::std::ops::Mul<&SparseObservable> for Complex64 {
1169
    type Output = SparseObservable;
1170

1171
    fn mul(self, rhs: &SparseObservable) -> SparseObservable {
×
1172
        rhs * self
×
1173
    }
×
1174
}
1175
impl ::std::ops::MulAssign<Complex64> for SparseObservable {
1176
    fn mul_assign(&mut self, rhs: Complex64) {
84✔
1177
        if rhs == Complex64::new(0.0, 0.0) {
84✔
1178
            self.coeffs.clear();
14✔
1179
            self.bit_terms.clear();
14✔
1180
            self.indices.clear();
14✔
1181
            self.boundaries.clear();
14✔
1182
            self.boundaries.push(0);
14✔
1183
        } else {
14✔
1184
            self.coeffs.iter_mut().for_each(|c| *c *= rhs)
80✔
1185
        }
1186
    }
84✔
1187
}
1188

1189
impl ::std::ops::Div<Complex64> for SparseObservable {
1190
    type Output = SparseObservable;
1191

1192
    fn div(mut self, rhs: Complex64) -> SparseObservable {
×
1193
        self /= rhs;
×
1194
        self
×
1195
    }
×
1196
}
1197
impl ::std::ops::Div<Complex64> for &SparseObservable {
1198
    type Output = SparseObservable;
1199

1200
    fn div(self, rhs: Complex64) -> SparseObservable {
56✔
1201
        SparseObservable {
56✔
1202
            num_qubits: self.num_qubits,
56✔
1203
            coeffs: self.coeffs.iter().map(|c| c / rhs).collect(),
64✔
1204
            bit_terms: self.bit_terms.clone(),
56✔
1205
            indices: self.indices.clone(),
56✔
1206
            boundaries: self.boundaries.clone(),
56✔
1207
        }
56✔
1208
    }
56✔
1209
}
1210
impl ::std::ops::DivAssign<Complex64> for SparseObservable {
1211
    fn div_assign(&mut self, rhs: Complex64) {
56✔
1212
        self.coeffs.iter_mut().for_each(|c| *c /= rhs)
64✔
1213
    }
56✔
1214
}
1215

1216
impl ::std::ops::Neg for &SparseObservable {
1217
    type Output = SparseObservable;
1218

1219
    fn neg(self) -> SparseObservable {
58✔
1220
        SparseObservable {
58✔
1221
            num_qubits: self.num_qubits,
58✔
1222
            coeffs: self.coeffs.iter().map(|c| -c).collect(),
88✔
1223
            bit_terms: self.bit_terms.clone(),
58✔
1224
            indices: self.indices.clone(),
58✔
1225
            boundaries: self.boundaries.clone(),
58✔
1226
        }
58✔
1227
    }
58✔
1228
}
1229
impl ::std::ops::Neg for SparseObservable {
1230
    type Output = SparseObservable;
1231

1232
    fn neg(mut self) -> SparseObservable {
×
1233
        self.coeffs.iter_mut().for_each(|c| *c = -*c);
×
1234
        self
×
1235
    }
×
1236
}
1237

1238
/// Worker objects for the [SparseObservable::compose]-alike functions.
1239
mod compose {
1240
    use super::*;
1241

1242
    /// An non-scalar entry in the multi-Cartesian product iteration.
1243
    ///
1244
    /// Each [MultipleItem] corresponds to a bit index that becomes a summation as part of the
1245
    /// composition, so the multi-Cartesian product has to keep iterating through it.
1246
    struct MultipleItem {
1247
        /// The index into the full `bit_terms` [Vec] that this item refers to.
1248
        loc: usize,
1249
        /// The next item in the slice that should be written out.  If this equal to the length
1250
        /// of the slice, the implication is that it needs to be reset to 0 by a lower-index
1251
        /// item getting incremented and flowing forwards.
1252
        cur: usize,
1253
        /// The underlying slice of the multiple iteration, from the compose lookup table.
1254
        slice: &'static [(Complex64, BitTerm)],
1255
    }
1256

1257
    /// An implementation of the multiple Cartesian-product iterator that produces the sum terms
1258
    /// that make up the composition of two individual [SparseObservable] terms.
1259
    ///
1260
    /// This mutates itself in-place to keep track of the state, which allows us to re-use
1261
    /// partial results and to avoid allocations per item (since we have to copy out of the
1262
    /// buffers to the output [SparseObservable] each time anyway).
1263
    pub struct Iter {
1264
        num_qubits: u32,
1265
        /// Tracking data for the places in the output that have multiple branches to take.
1266
        multiples: Vec<MultipleItem>,
1267
        /// Stack of the coefficients to this point.  We could recalculate by a full
1268
        /// multiplication on each go, but most steps will be in the low indices, where we can
1269
        /// re-use all the multiplications that came before.  This is one longer than the length
1270
        /// of `multliples`, because it starts off populated with the product of the
1271
        /// non-multiple coefficients (or 1).
1272
        coeffs: Vec<Complex64>,
1273
        /// The full set of indices (including ones that don't correspond to multiples).  Within
1274
        /// a given iteration, this never changes; it's just stored to make it easier to memcpy
1275
        /// out of.
1276
        indices: Vec<u32>,
1277
        /// The full set of [BitTerm]s (including ones that don't correspond to multiples).  The
1278
        /// multiple ones get updated inplace during iteration.
1279
        bit_terms: Vec<BitTerm>,
1280
        /// Whether iteration has started.
1281
        started: bool,
1282
        /// Whether iteration will yield any further items.
1283
        exhausted: bool,
1284
    }
1285
    impl Iter {
1286
        pub fn new(num_qubits: u32) -> Self {
428✔
1287
            Self {
428✔
1288
                num_qubits,
428✔
1289
                multiples: Vec::new(),
428✔
1290
                coeffs: vec![Complex64::new(1., 0.)],
428✔
1291
                indices: Vec::new(),
428✔
1292
                bit_terms: Vec::new(),
428✔
1293
                started: false,
428✔
1294
                exhausted: false,
428✔
1295
            }
428✔
1296
        }
428✔
1297
        /// Load up the next terms an iterator over the `(index, term)` pairs from the left-hand
1298
        /// side and the right-hand side.  The iterators must return indices in strictly increasing
1299
        /// order.
1300
        pub fn load_from(
470✔
1301
            &mut self,
470✔
1302
            coeff: Complex64,
470✔
1303
            left: impl IntoIterator<Item = (u32, BitTerm)>,
470✔
1304
            right: impl IntoIterator<Item = (u32, BitTerm)>,
470✔
1305
        ) {
470✔
1306
            self.multiples.clear();
470✔
1307
            self.coeffs.clear();
470✔
1308
            self.coeffs.push(coeff);
470✔
1309
            self.indices.clear();
470✔
1310
            self.bit_terms.clear();
470✔
1311
            self.started = false;
470✔
1312
            self.exhausted = false;
470✔
1313

1314
            for (index, values) in pairwise_ordered(left, right) {
786✔
1315
                match values {
786✔
1316
                    Paired::Left(term) | Paired::Right(term) => {
492✔
1317
                        self.indices.push(index);
492✔
1318
                        self.bit_terms.push(term);
492✔
1319
                    }
492✔
1320
                    Paired::Both(left, right) => {
294✔
1321
                        let Some(slice) = lookup::matmul(left, right) else {
294✔
1322
                            self.exhausted = true;
12✔
1323
                            return;
12✔
1324
                        };
1325
                        match slice {
282✔
1326
                            &[] => (),
282✔
1327
                            &[(coeff, term)] => {
64✔
1328
                                self.coeffs[0] *= coeff;
64✔
1329
                                self.indices.push(index);
64✔
1330
                                self.bit_terms.push(term);
64✔
1331
                            }
64✔
1332
                            slice => {
212✔
1333
                                self.multiples.push(MultipleItem {
212✔
1334
                                    loc: self.bit_terms.len(),
212✔
1335
                                    cur: 0,
212✔
1336
                                    slice,
212✔
1337
                                });
212✔
1338
                                self.indices.push(index);
212✔
1339
                                self.coeffs.push(Default::default());
212✔
1340
                                self.bit_terms.push(slice[0].1);
212✔
1341
                            }
212✔
1342
                        }
1343
                    }
1344
                }
1345
            }
1346
        }
470✔
1347
        /// Expose the current iteration item, assuming the state has been updated.
1348
        fn iter_item(&self) -> SparseTermView {
920✔
1349
            SparseTermView {
920✔
1350
                num_qubits: self.num_qubits,
920✔
1351
                coeff: *self.coeffs.last().expect("coeffs is never empty"),
920✔
1352
                indices: &self.indices,
920✔
1353
                bit_terms: &self.bit_terms,
920✔
1354
            }
920✔
1355
        }
920✔
1356
        // Not actually the iterator method, because we're borrowing from `self`.
1357
        /// Get the next term in the iteration.
1358
        pub fn next(&mut self) -> Option<SparseTermView> {
1,390✔
1359
            if self.exhausted {
1,390✔
1360
                return None;
12✔
1361
            }
1,378✔
1362
            if !self.started {
1,378✔
1363
                self.started = true;
458✔
1364
                // Initialising the struct has to put us in a place where the indices and bit terms
1365
                // are ready, but we still need to initialise the coefficient stack.
1366
                for (i, item) in self.multiples.iter().enumerate() {
458✔
1367
                    // `item.cur` should always be 0 at this point.
212✔
1368
                    self.coeffs[i + 1] = self.coeffs[i] * item.slice[item.cur].0;
212✔
1369
                }
212✔
1370
                return Some(self.iter_item());
458✔
1371
            }
920✔
1372
            // The lowest index into `multiples` that didn't overflow as we were updating the
1373
            // bit-terms state.
1374
            let non_overflow_index = 'inc_index: {
462✔
1375
                for (i, item) in self.multiples.iter_mut().enumerate().rev() {
920✔
1376
                    // The slices are always non-empty.
1377
                    if item.cur == item.slice.len() - 1 {
852✔
1378
                        item.cur = 0;
390✔
1379
                    } else {
462✔
1380
                        item.cur += 1;
462✔
1381
                    }
462✔
1382
                    self.bit_terms[item.loc] = item.slice[item.cur].1;
852✔
1383
                    if item.cur > 0 {
852✔
1384
                        break 'inc_index i;
462✔
1385
                    }
390✔
1386
                }
1387
                self.exhausted = true;
458✔
1388
                return None;
458✔
1389
            };
1390
            // Now run forwards updating the coefficient tree from the point it changes.
1391
            for (offset, item) in self.multiples[non_overflow_index..].iter_mut().enumerate() {
640✔
1392
                let base = non_overflow_index + offset;
640✔
1393
                self.coeffs[base + 1] = self.coeffs[base] * item.slice[item.cur].0;
640✔
1394
            }
640✔
1395
            Some(self.iter_item())
462✔
1396
        }
1,390✔
1397
        /// The total number of items in the iteration (this ignores the iteration state; only
1398
        /// [load_from] changes it).  If a 0 multiplier is encountered during the load, the
1399
        /// iterator is empty.
1400
        pub fn num_terms(&self) -> usize {
1,880✔
1401
            self.exhausted
1,880✔
1402
                .then_some(0)
1,880✔
1403
                .unwrap_or_else(|| self.multiples.iter().map(|item| item.slice.len()).product())
1,880✔
1404
        }
1,880✔
1405
        /// The length of each individual term in the iteration.
1406
        pub fn term_len(&self) -> usize {
940✔
1407
            self.bit_terms.len()
940✔
1408
        }
940✔
1409
    }
1410
}
1411

1412
/// A view object onto a single term of a `SparseObservable`.
1413
///
1414
/// The lengths of `bit_terms` and `indices` are guaranteed to be created equal, but might be zero
1415
/// (in the case that the term is proportional to the identity).
1416
#[derive(Clone, Copy, PartialEq, Debug)]
1417
pub struct SparseTermView<'a> {
1418
    pub num_qubits: u32,
1419
    pub coeff: Complex64,
1420
    pub bit_terms: &'a [BitTerm],
1421
    pub indices: &'a [u32],
1422
}
1423
impl SparseTermView<'_> {
1424
    /// Convert this `SparseTermView` into an owning [SparseTerm] of the same data.
1425
    pub fn to_term(&self) -> SparseTerm {
224✔
1426
        SparseTerm {
224✔
1427
            num_qubits: self.num_qubits,
224✔
1428
            coeff: self.coeff,
224✔
1429
            bit_terms: self.bit_terms.into(),
224✔
1430
            indices: self.indices.into(),
224✔
1431
        }
224✔
1432
    }
224✔
1433

1434
    pub fn to_sparse_str(self) -> String {
52✔
1435
        let coeff = format!("{}", self.coeff).replace('i', "j");
52✔
1436
        let paulis = self
52✔
1437
            .indices
52✔
1438
            .iter()
52✔
1439
            .zip(self.bit_terms)
52✔
1440
            .rev()
52✔
1441
            .map(|(i, op)| format!("{}_{}", op.py_label(), i))
96✔
1442
            .collect::<Vec<String>>()
52✔
1443
            .join(" ");
52✔
1444
        format!("({})({})", coeff, paulis)
52✔
1445
    }
52✔
1446
}
1447

1448
/// A mutable view object onto a single term of a [SparseObservable].
1449
///
1450
/// The lengths of [bit_terms] and [indices] are guaranteed to be created equal, but might be zero
1451
/// (in the case that the term is proportional to the identity).  [indices] is not mutable because
1452
/// this would allow data coherence to be broken.
1453
#[derive(Debug)]
1454
pub struct SparseTermViewMut<'a> {
1455
    pub num_qubits: u32,
1456
    pub coeff: &'a mut Complex64,
1457
    pub bit_terms: &'a mut [BitTerm],
1458
    pub indices: &'a [u32],
1459
}
1460

1461
/// Iterator type allowing in-place mutation of the [SparseObservable].
1462
///
1463
/// Created by [SparseObservable::iter_mut].
1464
#[derive(Debug)]
1465
pub struct IterMut<'a> {
1466
    num_qubits: u32,
1467
    coeffs: &'a mut [Complex64],
1468
    bit_terms: &'a mut [BitTerm],
1469
    indices: &'a [u32],
1470
    boundaries: &'a [usize],
1471
    i: usize,
1472
}
1473
impl<'a> From<&'a mut SparseObservable> for IterMut<'a> {
1474
    fn from(value: &mut SparseObservable) -> IterMut {
264✔
1475
        IterMut {
264✔
1476
            num_qubits: value.num_qubits,
264✔
1477
            coeffs: &mut value.coeffs,
264✔
1478
            bit_terms: &mut value.bit_terms,
264✔
1479
            indices: &value.indices,
264✔
1480
            boundaries: &value.boundaries,
264✔
1481
            i: 0,
264✔
1482
        }
264✔
1483
    }
264✔
1484
}
1485
impl<'a> Iterator for IterMut<'a> {
1486
    type Item = SparseTermViewMut<'a>;
1487

1488
    fn next(&mut self) -> Option<Self::Item> {
660✔
1489
        // The trick here is that the lifetime of the 'self' borrow is shorter than the lifetime of
660✔
1490
        // the inner borrows.  We can't give out mutable references to our inner borrow, because
660✔
1491
        // after the lifetime on 'self' expired, there'd be nothing preventing somebody using the
660✔
1492
        // 'self' borrow to see _another_ mutable borrow of the inner data, which would be an
660✔
1493
        // aliasing violation.  Instead, we keep splitting the inner views we took out so the
660✔
1494
        // mutable references we return don't overlap with the ones we continue to hold.
660✔
1495
        let coeffs = ::std::mem::take(&mut self.coeffs);
660✔
1496
        let (coeff, other_coeffs) = coeffs.split_first_mut()?;
660✔
1497
        self.coeffs = other_coeffs;
396✔
1498

396✔
1499
        let len = self.boundaries[self.i + 1] - self.boundaries[self.i];
396✔
1500
        self.i += 1;
396✔
1501

396✔
1502
        let all_bit_terms = ::std::mem::take(&mut self.bit_terms);
396✔
1503
        let all_indices = ::std::mem::take(&mut self.indices);
396✔
1504
        let (bit_terms, rest_bit_terms) = all_bit_terms.split_at_mut(len);
396✔
1505
        let (indices, rest_indices) = all_indices.split_at(len);
396✔
1506
        self.bit_terms = rest_bit_terms;
396✔
1507
        self.indices = rest_indices;
396✔
1508

396✔
1509
        Some(SparseTermViewMut {
396✔
1510
            num_qubits: self.num_qubits,
396✔
1511
            coeff,
396✔
1512
            bit_terms,
396✔
1513
            indices,
396✔
1514
        })
396✔
1515
    }
660✔
1516

1517
    fn size_hint(&self) -> (usize, Option<usize>) {
×
1518
        (self.coeffs.len(), Some(self.coeffs.len()))
×
1519
    }
×
1520
}
1521
impl ExactSizeIterator for IterMut<'_> {}
1522
impl ::std::iter::FusedIterator for IterMut<'_> {}
1523

1524
/// A single term from a complete :class:`SparseObservable`.
1525
///
1526
/// These are typically created by indexing into or iterating through a :class:`SparseObservable`.
1527
#[derive(Clone, Debug, PartialEq)]
1528
pub struct SparseTerm {
1529
    /// Number of qubits the entire term applies to.
1530
    num_qubits: u32,
1531
    /// The complex coefficient of the term.
1532
    coeff: Complex64,
1533
    bit_terms: Box<[BitTerm]>,
1534
    indices: Box<[u32]>,
1535
}
1536
impl SparseTerm {
1537
    pub fn new(
64✔
1538
        num_qubits: u32,
64✔
1539
        coeff: Complex64,
64✔
1540
        bit_terms: Box<[BitTerm]>,
64✔
1541
        indices: Box<[u32]>,
64✔
1542
    ) -> Result<Self, CoherenceError> {
64✔
1543
        if bit_terms.len() != indices.len() {
64✔
1544
            return Err(CoherenceError::MismatchedItemCount {
×
1545
                bit_terms: bit_terms.len(),
×
1546
                indices: indices.len(),
×
1547
            });
×
1548
        }
64✔
1549

64✔
1550
        if indices.iter().any(|index| *index >= num_qubits) {
102✔
1551
            return Err(CoherenceError::BitIndexTooHigh);
×
1552
        }
64✔
1553

64✔
1554
        Ok(Self {
64✔
1555
            num_qubits,
64✔
1556
            coeff,
64✔
1557
            bit_terms,
64✔
1558
            indices,
64✔
1559
        })
64✔
1560
    }
64✔
1561

1562
    pub fn num_qubits(&self) -> u32 {
128✔
1563
        self.num_qubits
128✔
1564
    }
128✔
1565

1566
    pub fn coeff(&self) -> Complex64 {
56✔
1567
        self.coeff
56✔
1568
    }
56✔
1569

1570
    pub fn indices(&self) -> &[u32] {
72✔
1571
        &self.indices
72✔
1572
    }
72✔
1573

1574
    pub fn bit_terms(&self) -> &[BitTerm] {
170✔
1575
        &self.bit_terms
170✔
1576
    }
170✔
1577

1578
    pub fn view(&self) -> SparseTermView {
68✔
1579
        SparseTermView {
68✔
1580
            num_qubits: self.num_qubits,
68✔
1581
            coeff: self.coeff,
68✔
1582
            bit_terms: &self.bit_terms,
68✔
1583
            indices: &self.indices,
68✔
1584
        }
68✔
1585
    }
68✔
1586

1587
    /// Convert this term to a complete :class:`SparseObservable`.
1588
    pub fn to_observable(&self) -> SparseObservable {
22✔
1589
        SparseObservable {
22✔
1590
            num_qubits: self.num_qubits,
22✔
1591
            coeffs: vec![self.coeff],
22✔
1592
            bit_terms: self.bit_terms.to_vec(),
22✔
1593
            indices: self.indices.to_vec(),
22✔
1594
            boundaries: vec![0, self.bit_terms.len()],
22✔
1595
        }
22✔
1596
    }
22✔
1597
}
1598

1599
#[derive(Error, Debug)]
1600
struct InnerReadError;
1601

1602
#[derive(Error, Debug)]
1603
struct InnerWriteError;
1604

1605
impl ::std::fmt::Display for InnerReadError {
1606
    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
×
1607
        write!(f, "Failed acquiring lock for reading.")
×
1608
    }
×
1609
}
1610

1611
impl ::std::fmt::Display for InnerWriteError {
1612
    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
×
1613
        write!(f, "Failed acquiring lock for writing.")
×
1614
    }
×
1615
}
1616

1617
impl From<InnerReadError> for PyErr {
1618
    fn from(value: InnerReadError) -> PyErr {
×
1619
        PyRuntimeError::new_err(value.to_string())
×
1620
    }
×
1621
}
1622
impl From<InnerWriteError> for PyErr {
1623
    fn from(value: InnerWriteError) -> PyErr {
×
1624
        PyRuntimeError::new_err(value.to_string())
×
1625
    }
×
1626
}
1627

1628
impl From<BitTermFromU8Error> for PyErr {
1629
    fn from(value: BitTermFromU8Error) -> PyErr {
6✔
1630
        PyValueError::new_err(value.to_string())
6✔
1631
    }
6✔
1632
}
1633
impl From<CoherenceError> for PyErr {
1634
    fn from(value: CoherenceError) -> PyErr {
34✔
1635
        PyValueError::new_err(value.to_string())
34✔
1636
    }
34✔
1637
}
1638
impl From<LabelError> for PyErr {
1639
    fn from(value: LabelError) -> PyErr {
52✔
1640
        PyValueError::new_err(value.to_string())
52✔
1641
    }
52✔
1642
}
1643
impl From<ArithmeticError> for PyErr {
1644
    fn from(value: ArithmeticError) -> PyErr {
12✔
1645
        PyValueError::new_err(value.to_string())
12✔
1646
    }
12✔
1647
}
1648

1649
/// The single-character string label used to represent this term in the :class:`SparseObservable`
1650
/// alphabet.
1651
#[pyfunction]
450✔
1652
#[pyo3(name = "label")]
1653
fn bit_term_label(py: Python, slf: BitTerm) -> &Bound<PyString> {
450✔
1654
    // This doesn't use `py_label` so we can use `intern!`.
450✔
1655
    match slf {
450✔
1656
        BitTerm::X => intern!(py, "X"),
50✔
1657
        BitTerm::Plus => intern!(py, "+"),
50✔
1658
        BitTerm::Minus => intern!(py, "-"),
50✔
1659
        BitTerm::Y => intern!(py, "Y"),
50✔
1660
        BitTerm::Right => intern!(py, "r"),
50✔
1661
        BitTerm::Left => intern!(py, "l"),
50✔
1662
        BitTerm::Z => intern!(py, "Z"),
50✔
1663
        BitTerm::Zero => intern!(py, "0"),
50✔
1664
        BitTerm::One => intern!(py, "1"),
50✔
1665
    }
1666
}
450✔
1667
/// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `BitTerm`.
1668
///
1669
/// We don't make `BitTerm` a direct `pyclass` because we want the behaviour of `IntEnum`, which
1670
/// specifically also makes its variants subclasses of the Python `int` type; we use a type-safe
1671
/// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in
1672
/// Numpy arrays for efficiency.
1673
///
1674
/// The resulting class is attached to `SparseObservable` as a class attribute, and its
1675
/// `__qualname__` is set to reflect this.
1676
fn make_py_bit_term(py: Python) -> PyResult<Py<PyType>> {
12✔
1677
    let terms = [
12✔
1678
        BitTerm::X,
12✔
1679
        BitTerm::Plus,
12✔
1680
        BitTerm::Minus,
12✔
1681
        BitTerm::Y,
12✔
1682
        BitTerm::Right,
12✔
1683
        BitTerm::Left,
12✔
1684
        BitTerm::Z,
12✔
1685
        BitTerm::Zero,
12✔
1686
        BitTerm::One,
12✔
1687
    ]
12✔
1688
    .into_iter()
12✔
1689
    .flat_map(|term| {
108✔
1690
        let mut out = vec![(term.py_name(), term as u8)];
108✔
1691
        if term.py_name() != term.py_label() {
108✔
1692
            // Also ensure that the labels are created as aliases.  These can't be (easily) accessed
72✔
1693
            // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket)
72✔
1694
            // syntax, or programmatically with `getattr`.
72✔
1695
            out.push((term.py_label(), term as u8));
72✔
1696
        }
72✔
1697
        out
108✔
1698
    })
108✔
1699
    .collect::<Vec<_>>();
12✔
1700
    let obj = py.import("enum")?.getattr("IntEnum")?.call(
12✔
1701
        ("BitTerm", terms),
12✔
1702
        Some(
12✔
1703
            &[
12✔
1704
                ("module", "qiskit.quantum_info"),
12✔
1705
                ("qualname", "SparseObservable.BitTerm"),
12✔
1706
            ]
12✔
1707
            .into_py_dict(py)?,
12✔
1708
        ),
1709
    )?;
×
1710
    let label_property = py
12✔
1711
        .import("builtins")?
12✔
1712
        .getattr("property")?
12✔
1713
        .call1((wrap_pyfunction!(bit_term_label, py)?,))?;
12✔
1714
    obj.setattr("label", label_property)?;
12✔
1715
    Ok(obj.downcast_into::<PyType>()?.unbind())
12✔
1716
}
12✔
1717

1718
// Return the relevant value from the Python-space sister enumeration.  These are Python-space
1719
// singletons and subclasses of Python `int`.  We only use this for interaction with "high level"
1720
// Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it
1721
// efficiently.
1722
impl<'py> IntoPyObject<'py> for BitTerm {
1723
    type Target = PyAny;
1724
    type Output = Bound<'py, PyAny>;
1725
    type Error = PyErr;
1726

1727
    fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
176✔
1728
        let terms = BIT_TERM_INTO_PY.get_or_init(py, || {
176✔
1729
            let py_enum = BIT_TERM_PY_ENUM
8✔
1730
                .get_or_try_init(py, || make_py_bit_term(py))
8✔
1731
                .expect("creating a simple Python enum class should be infallible")
8✔
1732
                .bind(py);
8✔
1733
            ::std::array::from_fn(|val| {
128✔
1734
                ::bytemuck::checked::try_cast(val as u8)
128✔
1735
                    .ok()
128✔
1736
                    .map(|term: BitTerm| {
128✔
1737
                        py_enum
72✔
1738
                            .getattr(term.py_name())
72✔
1739
                            .expect("the created `BitTerm` enum should have matching attribute names to the terms")
72✔
1740
                            .unbind()
72✔
1741
                    })
128✔
1742
            })
128✔
1743
        });
176✔
1744
        Ok(terms[self as usize]
176✔
1745
            .as_ref()
176✔
1746
            .expect("the lookup table initializer populated a 'Some' in all valid locations")
176✔
1747
            .bind(py)
176✔
1748
            .clone())
176✔
1749
    }
176✔
1750
}
1751

1752
impl<'py> FromPyObject<'py> for BitTerm {
1753
    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
556✔
1754
        let value = ob
556✔
1755
            .extract::<isize>()
556✔
1756
            .map_err(|_| match ob.get_type().repr() {
556✔
1757
                Ok(repr) => PyTypeError::new_err(format!("bad type for 'BitTerm': {}", repr)),
×
1758
                Err(err) => err,
×
1759
            })?;
556✔
1760
        let value_error = || {
556✔
1761
            PyValueError::new_err(format!(
×
1762
                "value {} is not a valid letter of the single-qubit alphabet for 'BitTerm'",
×
1763
                value
×
1764
            ))
×
1765
        };
×
1766
        let value: u8 = value.try_into().map_err(|_| value_error())?;
556✔
1767
        value.try_into().map_err(|_| value_error())
556✔
1768
    }
556✔
1769
}
1770

1771
/// A single term from a complete :class:`SparseObservable`.
1772
///
1773
/// These are typically created by indexing into or iterating through a :class:`SparseObservable`.
1774
#[pyclass(name = "Term", frozen, module = "qiskit.quantum_info")]
236✔
1775
#[derive(Clone, Debug)]
1776
struct PySparseTerm {
1777
    inner: SparseTerm,
1778
}
1779
#[pymethods]
244✔
1780
impl PySparseTerm {
1781
    // Mark the Python class as being defined "within" the `SparseObservable` class namespace.
1782
    #[classattr]
1783
    #[pyo3(name = "__qualname__")]
1784
    fn type_qualname() -> &'static str {
36✔
1785
        "SparseObservable.Term"
36✔
1786
    }
36✔
1787

1788
    #[new]
1789
    #[pyo3(signature = (/, num_qubits, coeff, bit_terms, indices))]
1790
    fn py_new(
66✔
1791
        num_qubits: u32,
66✔
1792
        coeff: Complex64,
66✔
1793
        bit_terms: Vec<BitTerm>,
66✔
1794
        indices: Vec<u32>,
66✔
1795
    ) -> PyResult<Self> {
66✔
1796
        if bit_terms.len() != indices.len() {
66✔
1797
            return Err(CoherenceError::MismatchedItemCount {
×
1798
                bit_terms: bit_terms.len(),
×
1799
                indices: indices.len(),
×
1800
            }
×
1801
            .into());
×
1802
        }
66✔
1803
        let mut order = (0..bit_terms.len()).collect::<Vec<_>>();
66✔
1804
        order.sort_unstable_by_key(|a| indices[*a]);
160✔
1805
        let bit_terms = order.iter().map(|i| bit_terms[*i]).collect();
106✔
1806
        let mut sorted_indices = Vec::<u32>::with_capacity(order.len());
66✔
1807
        for i in order {
170✔
1808
            let index = indices[i];
106✔
1809
            if sorted_indices
106✔
1810
                .last()
106✔
1811
                .map(|prev| *prev >= index)
106✔
1812
                .unwrap_or(false)
106✔
1813
            {
1814
                return Err(CoherenceError::UnsortedIndices.into());
2✔
1815
            }
104✔
1816
            sorted_indices.push(index)
104✔
1817
        }
1818
        let inner = SparseTerm::new(
64✔
1819
            num_qubits,
64✔
1820
            coeff,
64✔
1821
            bit_terms,
64✔
1822
            sorted_indices.into_boxed_slice(),
64✔
1823
        )?;
64✔
1824
        Ok(PySparseTerm { inner })
64✔
1825
    }
66✔
1826

1827
    /// Convert this term to a complete :class:`SparseObservable`.
1828
    fn to_observable(&self) -> PyResult<PySparseObservable> {
26✔
1829
        let obs = SparseObservable::new(
26✔
1830
            self.inner.num_qubits(),
26✔
1831
            vec![self.inner.coeff()],
26✔
1832
            self.inner.bit_terms().to_vec(),
26✔
1833
            self.inner.indices().to_vec(),
26✔
1834
            vec![0, self.inner.bit_terms().len()],
26✔
1835
        )?;
26✔
1836
        Ok(obs.into())
26✔
1837
    }
26✔
1838

NEW
1839
    fn to_label(&self) -> PyResult<String> {
×
NEW
1840
        Ok(self.inner.view().to_sparse_str())
×
NEW
1841
    }
×
1842

1843
    fn __eq__(slf: Bound<Self>, other: Bound<PyAny>) -> PyResult<bool> {
68✔
1844
        if slf.is(&other) {
68✔
1845
            return Ok(true);
×
1846
        }
68✔
1847
        let Ok(other) = other.downcast_into::<Self>() else {
68✔
1848
            return Ok(false);
×
1849
        };
1850
        let slf = slf.borrow();
68✔
1851
        let other = other.borrow();
68✔
1852
        Ok(slf.inner.eq(&other.inner))
68✔
1853
    }
68✔
1854

1855
    fn __repr__(&self) -> PyResult<String> {
24✔
1856
        Ok(format!(
24✔
1857
            "<{} on {} qubit{}: {}>",
24✔
1858
            Self::type_qualname(),
24✔
1859
            self.inner.num_qubits(),
24✔
1860
            if self.inner.num_qubits() == 1 {
24✔
1861
                ""
4✔
1862
            } else {
1863
                "s"
20✔
1864
            },
1865
            self.inner.view().to_sparse_str(),
24✔
1866
        ))
24✔
1867
    }
24✔
1868

1869
    fn __getnewargs__(slf_: Bound<Self>) -> PyResult<Bound<PyTuple>> {
24✔
1870
        let py = slf_.py();
24✔
1871
        let borrowed = slf_.borrow();
24✔
1872
        (
24✔
1873
            borrowed.inner.num_qubits(),
24✔
1874
            borrowed.inner.coeff(),
24✔
1875
            Self::get_bit_terms(slf_.clone()),
24✔
1876
            Self::get_indices(slf_),
24✔
1877
        )
24✔
1878
            .into_pyobject(py)
24✔
1879
    }
24✔
1880

1881
    /// Get a copy of this term.
1882
    fn copy(&self) -> Self {
×
1883
        self.clone()
×
1884
    }
×
1885

1886
    /// Read-only view onto the individual single-qubit terms.
1887
    ///
1888
    /// The only valid values in the array are those with a corresponding
1889
    /// :class:`~SparseObservable.BitTerm`.
1890
    #[getter]
1891
    fn get_bit_terms(slf_: Bound<Self>) -> Bound<PyArray1<u8>> {
30✔
1892
        let borrowed = slf_.borrow();
30✔
1893
        let bit_terms = borrowed.inner.bit_terms();
30✔
1894
        let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(bit_terms));
30✔
1895
        // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python.
30✔
1896
        // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the
30✔
1897
        // `Box<[BitTerm]>` allocation (including dropping or reallocating it) other than the entire
30✔
1898
        // object getting dropped, which Python will keep safe.
30✔
1899
        let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) };
30✔
1900
        out.readwrite().make_nonwriteable();
30✔
1901
        out
30✔
1902
    }
30✔
1903

1904
    /// The number of qubits the term is defined on.
1905
    #[getter]
1906
    fn get_num_qubits(&self) -> u32 {
6✔
1907
        self.inner.num_qubits()
6✔
1908
    }
6✔
1909

1910
    /// The term's coefficient.
1911
    #[getter]
1912
    fn get_coeff(&self) -> Complex64 {
6✔
1913
        self.inner.coeff()
6✔
1914
    }
6✔
1915

1916
    /// Read-only view onto the indices of each non-identity single-qubit term.
1917
    ///
1918
    /// The indices will always be in sorted order.
1919
    #[getter]
1920
    fn get_indices(slf_: Bound<Self>) -> Bound<PyArray1<u32>> {
34✔
1921
        let borrowed = slf_.borrow();
34✔
1922
        let indices = borrowed.inner.indices();
34✔
1923
        let arr = ::ndarray::aview1(indices);
34✔
1924
        // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python.
34✔
1925
        // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the
34✔
1926
        // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire
34✔
1927
        // object getting dropped, which Python will keep safe.
34✔
1928
        let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) };
34✔
1929
        out.readwrite().make_nonwriteable();
34✔
1930
        out
34✔
1931
    }
34✔
1932

1933
    /// Get a :class:`.Pauli` object that represents the measurement basis needed for this term.
1934
    ///
1935
    /// For example, the projector ``0l+`` will return a Pauli ``ZYX``.  The resulting
1936
    /// :class:`.Pauli` is dense, in the sense that explicit identities are stored.  An identity in
1937
    /// the Pauli output does not require a concrete measurement.
1938
    ///
1939
    /// Returns:
1940
    ///     :class:`.Pauli`: the Pauli operator representing the necessary measurement basis.
1941
    ///
1942
    /// See also:
1943
    ///     :meth:`SparseObservable.pauli_bases`
1944
    ///         A similar method for an entire observable at once.
1945
    fn pauli_base<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
12✔
1946
        let mut x = vec![false; self.inner.num_qubits() as usize];
12✔
1947
        let mut z = vec![false; self.inner.num_qubits() as usize];
12✔
1948
        for (bit_term, index) in self
38✔
1949
            .inner
12✔
1950
            .bit_terms()
12✔
1951
            .iter()
12✔
1952
            .zip(self.inner.indices().iter())
12✔
1953
        {
38✔
1954
            x[*index as usize] = bit_term.has_x_component();
38✔
1955
            z[*index as usize] = bit_term.has_z_component();
38✔
1956
        }
38✔
1957
        PAULI_TYPE
12✔
1958
            .get_bound(py)
12✔
1959
            .call1(((PyArray1::from_vec(py, z), PyArray1::from_vec(py, x)),))
12✔
1960
    }
12✔
1961

1962
    /// Return the bit labels of the term as string.
1963
    ///
1964
    /// The bit labels will match the order of :attr:`.SparseTerm.indices`, such that the
1965
    /// i-th character in the string is applied to the qubit index at ``term.indices[i]``.
1966
    ///
1967
    /// Returns:
1968
    ///     The non-identity bit terms as concatenated string.
1969
    fn bit_labels<'py>(&self, py: Python<'py>) -> Bound<'py, PyString> {
76✔
1970
        let string: String = self
76✔
1971
            .inner
76✔
1972
            .bit_terms()
76✔
1973
            .iter()
76✔
1974
            .map(|bit| bit.py_label())
248✔
1975
            .collect();
76✔
1976
        PyString::new(py, string.as_str())
76✔
1977
    }
76✔
1978
}
1979

1980
/// An observable over Pauli bases that stores its data in a qubit-sparse format.
1981
///
1982
/// Mathematics
1983
/// ===========
1984
///
1985
/// This observable represents a sum over strings of the Pauli operators and Pauli-eigenstate
1986
/// projectors, with each term weighted by some complex number.  That is, the full observable is
1987
///
1988
/// .. math::
1989
///
1990
///     \text{\texttt{SparseObservable}} = \sum_i c_i \bigotimes_n A^{(n)}_i
1991
///
1992
/// for complex numbers :math:`c_i` and single-qubit operators acting on qubit :math:`n` from a
1993
/// restricted alphabet :math:`A^{(n)}_i`.  The sum over :math:`i` is the sum of the individual
1994
/// terms, and the tensor product produces the operator strings.
1995
///
1996
/// The alphabet of allowed single-qubit operators that the :math:`A^{(n)}_i` are drawn from is the
1997
/// Pauli operators and the Pauli-eigenstate projection operators.  Explicitly, these are:
1998
///
1999
/// .. _sparse-observable-alphabet:
2000
/// .. table:: Alphabet of single-qubit terms used in :class:`SparseObservable`
2001
///
2002
///   =======  =======================================  ===============  ===========================
2003
///   Label    Operator                                 Numeric value    :class:`.BitTerm` attribute
2004
///   =======  =======================================  ===============  ===========================
2005
///   ``"I"``  :math:`I` (identity)                     Not stored.      Not stored.
2006
///
2007
///   ``"X"``  :math:`X` (Pauli X)                      ``0b0010`` (2)   :attr:`~.BitTerm.X`
2008
///
2009
///   ``"Y"``  :math:`Y` (Pauli Y)                      ``0b0011`` (3)   :attr:`~.BitTerm.Y`
2010
///
2011
///   ``"Z"``  :math:`Z` (Pauli Z)                      ``0b0001`` (1)   :attr:`~.BitTerm.Z`
2012
///
2013
///   ``"+"``  :math:`\lvert+\rangle\langle+\rvert`     ``0b1010`` (10)  :attr:`~.BitTerm.PLUS`
2014
///            (projector to positive eigenstate of X)
2015
///
2016
///   ``"-"``  :math:`\lvert-\rangle\langle-\rvert`     ``0b0110`` (6)   :attr:`~.BitTerm.MINUS`
2017
///            (projector to negative eigenstate of X)
2018
///
2019
///   ``"r"``  :math:`\lvert r\rangle\langle r\rvert`   ``0b1011`` (11)  :attr:`~.BitTerm.RIGHT`
2020
///            (projector to positive eigenstate of Y)
2021
///
2022
///   ``"l"``  :math:`\lvert l\rangle\langle l\rvert`   ``0b0111`` (7)   :attr:`~.BitTerm.LEFT`
2023
///            (projector to negative eigenstate of Y)
2024
///
2025
///   ``"0"``  :math:`\lvert0\rangle\langle0\rvert`     ``0b1001`` (9)   :attr:`~.BitTerm.ZERO`
2026
///            (projector to positive eigenstate of Z)
2027
///
2028
///   ``"1"``  :math:`\lvert1\rangle\langle1\rvert`     ``0b0101`` (5)   :attr:`~.BitTerm.ONE`
2029
///            (projector to negative eigenstate of Z)
2030
///   =======  =======================================  ===============  ===========================
2031
///
2032
/// The allowed alphabet forms an overcomplete basis of the operator space.  This means that there
2033
/// is not a unique summation to represent a given observable.  By comparison,
2034
/// :class:`.SparsePauliOp` uses a precise basis of the operator space, so (after combining terms of
2035
/// the same Pauli string, removing zeros, and sorting the terms to :ref:`some canonical order
2036
/// <sparse-observable-canonical-order>`) there is only one representation of any operator.
2037
///
2038
/// :class:`SparseObservable` uses its particular overcomplete basis with the aim of making
2039
/// "efficiency of measurement" equivalent to "efficiency of representation".  For example, the
2040
/// observable :math:`{\lvert0\rangle\langle0\rvert}^{\otimes n}` can be efficiently measured on
2041
/// hardware with simple :math:`Z` measurements, but can only be represented by
2042
/// :class:`.SparsePauliOp` as :math:`{(I + Z)}^{\otimes n}/2^n`, which requires :math:`2^n` stored
2043
/// terms.  :class:`SparseObservable` requires only a single term to store this.
2044
///
2045
/// The downside to this is that it is impractical to take an arbitrary matrix or
2046
/// :class:`.SparsePauliOp` and find the *best* :class:`SparseObservable` representation.  You
2047
/// typically will want to construct a :class:`SparseObservable` directly, rather than trying to
2048
/// decompose into one.
2049
///
2050
///
2051
/// Representation
2052
/// ==============
2053
///
2054
/// The internal representation of a :class:`SparseObservable` stores only the non-identity qubit
2055
/// operators.  This makes it significantly more efficient to represent observables such as
2056
/// :math:`\sum_{n\in \text{qubits}} Z^{(n)}`; :class:`SparseObservable` requires an amount of
2057
/// memory linear in the total number of qubits, while :class:`.SparsePauliOp` scales quadratically.
2058
///
2059
/// The terms are stored compressed, similar in spirit to the compressed sparse row format of sparse
2060
/// matrices.  In this analogy, the terms of the sum are the "rows", and the qubit terms are the
2061
/// "columns", where an absent entry represents the identity rather than a zero.  More explicitly,
2062
/// the representation is made up of four contiguous arrays:
2063
///
2064
/// .. _sparse-observable-arrays:
2065
/// .. table:: Data arrays used to represent :class:`.SparseObservable`
2066
///
2067
///   ==================  ===========  =============================================================
2068
///   Attribute           Length       Description
2069
///   ==================  ===========  =============================================================
2070
///   :attr:`coeffs`      :math:`t`    The complex scalar multiplier for each term.
2071
///
2072
///   :attr:`bit_terms`   :math:`s`    Each of the non-identity single-qubit terms for all of the
2073
///                                    operators, in order.  These correspond to the non-identity
2074
///                                    :math:`A^{(n)}_i` in the sum description, where the entries
2075
///                                    are stored in order of increasing :math:`i` first, and in
2076
///                                    order of increasing :math:`n` within each term.
2077
///
2078
///   :attr:`indices`     :math:`s`    The corresponding qubit (:math:`n`) for each of the operators
2079
///                                    in :attr:`bit_terms`.  :class:`SparseObservable` requires
2080
///                                    that this list is term-wise sorted, and algorithms can rely
2081
///                                    on this invariant being upheld.
2082
///
2083
///   :attr:`boundaries`  :math:`t+1`  The indices that partition :attr:`bit_terms` and
2084
///                                    :attr:`indices` into complete terms.  For term number
2085
///                                    :math:`i`, its complex coefficient is ``coeffs[i]``, and its
2086
///                                    non-identity single-qubit operators and their corresponding
2087
///                                    qubits are the slice ``boundaries[i] : boundaries[i+1]`` into
2088
///                                    :attr:`bit_terms` and :attr:`indices` respectively.
2089
///                                    :attr:`boundaries` always has an explicit 0 as its first
2090
///                                    element.
2091
///   ==================  ===========  =============================================================
2092
///
2093
/// The length parameter :math:`t` is the number of terms in the sum, and the parameter :math:`s` is
2094
/// the total number of non-identity single-qubit terms.
2095
///
2096
/// As illustrative examples:
2097
///
2098
/// * in the case of a zero operator, :attr:`boundaries` is length 1 (a single 0) and all other
2099
///   vectors are empty.
2100
/// * in the case of a fully simplified identity operator, :attr:`boundaries` is ``[0, 0]``,
2101
///   :attr:`coeffs` has a single entry, and :attr:`bit_terms` and :attr:`indices` are empty.
2102
/// * for the operator :math:`Z_2 Z_0 - X_3 Y_1`, :attr:`boundaries` is ``[0, 2, 4]``,
2103
///   :attr:`coeffs` is ``[1.0, -1.0]``, :attr:`bit_terms` is ``[BitTerm.Z, BitTerm.Z, BitTerm.Y,
2104
///   BitTerm.X]`` and :attr:`indices` is ``[0, 2, 1, 3]``.  The operator might act on more than
2105
///   four qubits, depending on the :attr:`num_qubits` parameter.  The :attr:`bit_terms` are integer
2106
///   values, whose magic numbers can be accessed via the :class:`BitTerm` attribute class.  Note
2107
///   that the single-bit terms and indices are sorted into termwise sorted order.  This is a
2108
///   requirement of the class.
2109
///
2110
/// These cases are not special, they're fully consistent with the rules and should not need special
2111
/// handling.
2112
///
2113
/// The scalar item of the :attr:`bit_terms` array is stored as a numeric byte.  The numeric values
2114
/// are related to the symplectic Pauli representation that :class:`.SparsePauliOp` uses, and are
2115
/// accessible with named access by an enumeration:
2116
///
2117
/// ..
2118
///     This is documented manually here because the Python-space `Enum` is generated
2119
///     programmatically from Rust - it'd be _more_ confusing to try and write a docstring somewhere
2120
///     else in this source file. The use of `autoattribute` is because it pulls in the numeric
2121
///     value.
2122
///
2123
/// .. py:class:: SparseObservable.BitTerm
2124
///
2125
///     An :class:`~enum.IntEnum` that provides named access to the numerical values used to
2126
///     represent each of the single-qubit alphabet terms enumerated in
2127
///     :ref:`sparse-observable-alphabet`.
2128
///
2129
///     This class is attached to :class:`.SparseObservable`.  Access it as
2130
///     :class:`.SparseObservable.BitTerm`.  If this is too much typing, and you are solely dealing
2131
///     with :class:¬SparseObservable` objects and the :class:`BitTerm` name is not ambiguous, you
2132
///     might want to shorten it as::
2133
///
2134
///         >>> ops = SparseObservable.BitTerm
2135
///         >>> assert ops.X is SparseObservable.BitTerm.X
2136
///
2137
///     You can access all the values of the enumeration by either their full all-capitals name, or
2138
///     by their single-letter label.  The single-letter labels are not generally valid Python
2139
///     identifiers, so you must use indexing notation to access them::
2140
///
2141
///         >>> assert SparseObservable.BitTerm.ZERO is SparseObservable.BitTerm["0"]
2142
///
2143
///     The numeric structure of these is that they are all four-bit values of which the low two
2144
///     bits are the (phase-less) symplectic representation of the Pauli operator related to the
2145
///     object, where the low bit denotes a contribution by :math:`Z` and the second lowest a
2146
///     contribution by :math:`X`, while the upper two bits are ``00`` for a Pauli operator, ``01``
2147
///     for the negative-eigenstate projector, and ``10`` for the positive-eigenstate projector.
2148
///
2149
///     Values
2150
///     ------
2151
///
2152
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.X
2153
///
2154
///         The Pauli :math:`X` operator.  Uses the single-letter label ``"X"``.
2155
///
2156
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.PLUS
2157
///
2158
///         The projector to the positive eigenstate of the :math:`X` operator:
2159
///         :math:`\lvert+\rangle\langle+\rvert`.  Uses the single-letter label ``"+"``.
2160
///
2161
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.MINUS
2162
///
2163
///         The projector to the negative eigenstate of the :math:`X` operator:
2164
///         :math:`\lvert-\rangle\langle-\rvert`.  Uses the single-letter label ``"-"``.
2165
///
2166
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Y
2167
///
2168
///         The Pauli :math:`Y` operator.  Uses the single-letter label ``"Y"``.
2169
///
2170
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.RIGHT
2171
///
2172
///         The projector to the positive eigenstate of the :math:`Y` operator:
2173
///         :math:`\lvert r\rangle\langle r\rvert`.  Uses the single-letter label ``"r"``.
2174
///
2175
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.LEFT
2176
///
2177
///         The projector to the negative eigenstate of the :math:`Y` operator:
2178
///         :math:`\lvert l\rangle\langle l\rvert`.  Uses the single-letter label ``"l"``.
2179
///
2180
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Z
2181
///
2182
///         The Pauli :math:`Z` operator.  Uses the single-letter label ``"Z"``.
2183
///
2184
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ZERO
2185
///
2186
///         The projector to the positive eigenstate of the :math:`Z` operator:
2187
///         :math:`\lvert0\rangle\langle0\rvert`.  Uses the single-letter label ``"0"``.
2188
///
2189
///     .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ONE
2190
///
2191
///         The projector to the negative eigenstate of the :math:`Z` operator:
2192
///         :math:`\lvert1\rangle\langle1\rvert`.  Uses the single-letter label ``"1"``.
2193
///
2194
///     Attributes
2195
///     ----------
2196
///
2197
///     .. autoproperty:: qiskit.quantum_info::SparseObservable.BitTerm.label
2198
///
2199
/// Each of the array-like attributes behaves like a Python sequence.  You can index and slice these
2200
/// with standard :class:`list`-like semantics.  Slicing an attribute returns a Numpy
2201
/// :class:`~numpy.ndarray` containing a copy of the relevant data with the natural ``dtype`` of the
2202
/// field; this lets you easily do mathematics on the results, like bitwise operations on
2203
/// :attr:`bit_terms`.  You can assign to indices or slices of each of the attributes, but beware
2204
/// that you must uphold :ref:`the data coherence rules <sparse-observable-arrays>` while doing
2205
/// this.  For example::
2206
///
2207
///     >>> obs = SparseObservable.from_list([("XZY", 1.5j), ("+1r", -0.5)])
2208
///     >>> assert isinstance(obs.coeffs[:], np.ndarray)
2209
///     >>> # Reduce all single-qubit terms to the relevant Pauli operator, if they are a projector.
2210
///     >>> obs.bit_terms[:] = obs.bit_terms[:] & 0b00_11
2211
///     >>> assert obs == SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)])
2212
///
2213
/// .. note::
2214
///
2215
///     The above reduction to the Pauli bases can also be achieved with :meth:`pauli_bases`.
2216
///
2217
/// .. _sparse-observable-canonical-order:
2218
///
2219
/// Canonical ordering
2220
/// ------------------
2221
///
2222
/// For any given mathematical observable, there are several ways of representing it with
2223
/// :class:`SparseObservable`.  For example, the same set of single-bit terms and their
2224
/// corresponding indices might appear multiple times in the observable.  Mathematically, this is
2225
/// equivalent to having only a single term with all the coefficients summed.  Similarly, the terms
2226
/// of the sum in a :class:`SparseObservable` can be in any order while representing the same
2227
/// observable, since addition is commutative (although while floating-point addition is not
2228
/// associative, :class:`SparseObservable` makes no guarantees about the summation order).
2229
///
2230
/// These two categories of representation degeneracy can cause the ``==`` operator to claim that
2231
/// two observables are not equal, despite representating the same object.  In these cases, it can
2232
/// be convenient to define some *canonical form*, which allows observables to be compared
2233
/// structurally.
2234
///
2235
/// You can put a :class:`SparseObservable` in canonical form by using the :meth:`simplify` method.
2236
/// The precise ordering of terms in canonical ordering is not specified, and may change between
2237
/// versions of Qiskit.  Within the same version of Qiskit, however, you can compare two observables
2238
/// structurally by comparing their simplified forms.
2239
///
2240
/// .. note::
2241
///
2242
///     If you wish to account for floating-point tolerance in the comparison, it is safest to use
2243
///     a recipe such as::
2244
///
2245
///         def equivalent(left, right, tol):
2246
///             return (left - right).simplify(tol) == SparseObservable.zero(left.num_qubits)
2247
///
2248
/// .. note::
2249
///
2250
///     The canonical form produced by :meth:`simplify` alone will not universally detect all
2251
///     observables that are equivalent due to the over-complete basis alphabet. To obtain a
2252
///     unique expression, you can first represent the observable using Pauli terms only by
2253
///     calling :meth:`as_paulis`, followed by :meth:`simplify`. Note that the projector
2254
///     expansion (e.g. ``+`` into ``I`` and ``X``) is not computationally feasible at scale.
2255
///
2256
/// Indexing
2257
/// --------
2258
///
2259
/// :class:`SparseObservable` behaves as `a Python sequence
2260
/// <https://docs.python.org/3/glossary.html#term-sequence>`__ (the standard form, not the expanded
2261
/// :class:`collections.abc.Sequence`).  The observable can be indexed by integers, and iterated
2262
/// through to yield individual terms.
2263
///
2264
/// Each term appears as an instance a self-contained class.  The individual terms are copied out of
2265
/// the base observable; mutations to them will not affect the observable.
2266
///
2267
/// .. autoclass:: qiskit.quantum_info::SparseObservable.Term
2268
///     :members:
2269
///
2270
/// Construction
2271
/// ============
2272
///
2273
/// :class:`SparseObservable` defines several constructors.  The default constructor will attempt to
2274
/// delegate to one of the more specific constructors, based on the type of the input.  You can
2275
/// always use the specific constructors to have more control over the construction.
2276
///
2277
/// .. _sparse-observable-convert-constructors:
2278
/// .. table:: Construction from other objects
2279
///
2280
///   ============================  ================================================================
2281
///   Method                        Summary
2282
///   ============================  ================================================================
2283
///   :meth:`from_label`            Convert a dense string label into a single-term
2284
///                                 :class:`.SparseObservable`.
2285
///
2286
///   :meth:`from_list`             Sum a list of tuples of dense string labels and the associated
2287
///                                 coefficients into an observable.
2288
///
2289
///   :meth:`from_sparse_list`      Sum a list of tuples of sparse string labels, the qubits they
2290
///                                 apply to, and their coefficients into an observable.
2291
///
2292
///   :meth:`from_pauli`            Raise a single :class:`.Pauli` into a single-term
2293
///                                 :class:`.SparseObservable`.
2294
///
2295
///   :meth:`from_sparse_pauli_op`  Raise a :class:`.SparsePauliOp` into a :class:`SparseObservable`.
2296
///
2297
///   :meth:`from_terms`            Sum explicit single :class:`Term` instances.
2298
///
2299
///   :meth:`from_raw_parts`        Build the observable from :ref:`the raw data arrays
2300
///                                 <sparse-observable-arrays>`.
2301
///   ============================  ================================================================
2302
///
2303
/// .. py:function:: SparseObservable.__new__(data, /, num_qubits=None)
2304
///
2305
///     The default constructor of :class:`SparseObservable`.
2306
///
2307
///     This delegates to one of :ref:`the explicit conversion-constructor methods
2308
///     <sparse-observable-convert-constructors>`, based on the type of the ``data`` argument.  If
2309
///     ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not accept a
2310
///     number, the given integer must match the input.
2311
///
2312
///     :param data: The data type of the input.  This can be another :class:`SparseObservable`, in
2313
///         which case the input is copied, a :class:`.Pauli` or :class:`.SparsePauliOp`, in which
2314
///         case :meth:`from_pauli` or :meth:`from_sparse_pauli_op` are called as appropriate, or it
2315
///         can be a list in a valid format for either :meth:`from_list` or
2316
///         :meth:`from_sparse_list`.
2317
///     :param int|None num_qubits: Optional number of qubits for the operator.  For most data
2318
///         inputs, this can be inferred and need not be passed.  It is only necessary for empty
2319
///         lists or the sparse-list format.  If given unnecessarily, it must match the data input.
2320
///
2321
/// In addition to the conversion-based constructors, there are also helper methods that construct
2322
/// special forms of observables.
2323
///
2324
/// .. table:: Construction of special observables
2325
///
2326
///   ============================  ================================================================
2327
///   Method                        Summary
2328
///   ============================  ================================================================
2329
///   :meth:`zero`                  The zero operator on a given number of qubits.
2330
///
2331
///   :meth:`identity`              The identity operator on a given number of qubits.
2332
///   ============================  ================================================================
2333
///
2334
/// Conversions
2335
/// ===========
2336
///
2337
/// An existing :class:`SparseObservable` can be converted into other :mod:`~qiskit.quantum_info`
2338
/// operators or generic formats.  Beware that other objects may not be able to represent the same
2339
/// observable as efficiently as :class:`SparseObservable`, including potentially needed
2340
/// exponentially more memory.
2341
///
2342
/// .. table:: Conversion methods to other observable forms.
2343
///
2344
///   ===========================  =================================================================
2345
///   Method                       Summary
2346
///   ===========================  =================================================================
2347
///   :meth:`as_paulis`            Create a new :class:`SparseObservable`, expanding in terms
2348
///                                of Pauli operators only.
2349
///
2350
///   :meth:`to_sparse_list`       Express the observable in a sparse list format with elements
2351
///                                ``(bit_terms, indices, coeff)``.
2352
///   ===========================  =================================================================
2353
///
2354
/// In addition, :meth:`.SparsePauliOp.from_sparse_observable` is available for conversion from this
2355
/// class to :class:`.SparsePauliOp`.  Beware that this method suffers from the same
2356
/// exponential-memory usage concerns as :meth:`as_paulis`.
2357
///
2358
/// Mathematical manipulation
2359
/// =========================
2360
///
2361
/// :class:`SparseObservable` supports the standard set of Python mathematical operators like other
2362
/// :mod:`~qiskit.quantum_info` operators.
2363
///
2364
/// In basic arithmetic, you can:
2365
///
2366
/// * add two observables using ``+``
2367
/// * subtract two observables using ``-``
2368
/// * multiply or divide by an :class:`int`, :class:`float` or :class:`complex` using ``*`` and ``/``
2369
/// * negate all the coefficients in an observable with unary ``-``
2370
///
2371
/// Each of the basic binary arithmetic operators has a corresponding specialized in-place method,
2372
/// which mutates the left-hand side in-place.  Using these is typically more efficient than the
2373
/// infix operators, especially for building an observable in a loop.
2374
///
2375
/// The tensor product is calculated with :meth:`tensor` (for standard, juxtaposition ordering of
2376
/// Pauli labels) or :meth:`expand` (for the reverse order).  The ``^`` operator is overloaded to be
2377
/// equivalent to :meth:`tensor`.
2378
///
2379
/// .. note::
2380
///
2381
///     When using the binary operators ``^`` (:meth:`tensor`) and ``&`` (:meth:`compose`), beware
2382
///     that `Python's operator-precedence rules
2383
///     <https://docs.python.org/3/reference/expressions.html#operator-precedence>`__ may cause the
2384
///     evaluation order to be different to your expectation.  In particular, the operator ``+``
2385
///     binds more tightly than ``^`` or ``&``, just like ``*`` binds more tightly than ``+``.
2386
///
2387
///     When using the operators in mixed expressions, it is safest to use parentheses to group the
2388
///     operands of tensor products.
2389
///
2390
/// A :class:`SparseObservable` has a well-defined :meth:`adjoint`.  The notions of scalar complex
2391
/// conjugation (:meth:`conjugate`) and real-value transposition (:meth:`transpose`) are defined
2392
/// analogously to the matrix representation of other Pauli operators in Qiskit.
2393
///
2394
///
2395
/// Efficiency notes
2396
/// ----------------
2397
///
2398
/// Internally, :class:`SparseObservable` is in-place mutable, including using over-allocating
2399
/// growable vectors for extending the number of terms.  This means that the cost of appending to an
2400
/// observable using ``+=`` is amortised linear in the total number of terms added, rather than
2401
/// the quadratic complexity that the binary ``+`` would require.
2402
///
2403
/// Additions and subtractions are implemented by a term-stacking operation; there is no automatic
2404
/// "simplification" (summing of like terms), because the majority of additions to build up an
2405
/// observable generate only a small number of duplications, and like-term detection has additional
2406
/// costs.  If this does not fit your use cases, you can either periodically call :meth:`simplify`,
2407
/// or discuss further APIs with us for better building of observables.
2408
#[pyclass(name = "SparseObservable", module = "qiskit.quantum_info", sequence)]
10,546✔
2409
#[derive(Debug)]
2410
pub struct PySparseObservable {
2411
    // This class keeps a pointer to a pure Rust-SparseTerm and serves as interface from Python.
2412
    inner: Arc<RwLock<SparseObservable>>,
2413
}
2414
#[pymethods]
16,914✔
2415
impl PySparseObservable {
2416
    #[pyo3(signature = (data, /, num_qubits=None))]
2417
    #[new]
2418
    fn py_new(data: &Bound<PyAny>, num_qubits: Option<u32>) -> PyResult<Self> {
304✔
2419
        let py = data.py();
304✔
2420
        let check_num_qubits = |data: &Bound<PyAny>| -> PyResult<()> {
304✔
2421
            let Some(num_qubits) = num_qubits else {
48✔
2422
                return Ok(());
36✔
2423
            };
2424
            let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::<u32>()?;
12✔
2425
            if num_qubits == other_qubits {
12✔
2426
                return Ok(());
6✔
2427
            }
6✔
2428
            Err(PyValueError::new_err(format!(
6✔
2429
                "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})"
6✔
2430
            )))
6✔
2431
        };
48✔
2432

2433
        if data.is_instance(PAULI_TYPE.get_bound(py))? {
304✔
2434
            check_num_qubits(data)?;
26✔
2435
            return Self::from_pauli(data);
24✔
2436
        }
278✔
2437
        if data.is_instance(SPARSE_PAULI_OP_TYPE.get_bound(py))? {
278✔
2438
            check_num_qubits(data)?;
18✔
2439
            return Self::from_sparse_pauli_op(data);
16✔
2440
        }
260✔
2441
        if let Ok(label) = data.extract::<String>() {
260✔
2442
            let num_qubits = num_qubits.unwrap_or(label.len() as u32);
186✔
2443
            if num_qubits as usize != label.len() {
186✔
2444
                return Err(PyValueError::new_err(format!(
2✔
2445
                    "explicitly given 'num_qubits' ({}) does not match label ({})",
2✔
2446
                    num_qubits,
2✔
2447
                    label.len(),
2✔
2448
                )));
2✔
2449
            }
184✔
2450
            return Self::from_label(&label).map_err(PyErr::from);
184✔
2451
        }
74✔
2452
        if let Ok(observable) = data.downcast_exact::<Self>() {
74✔
2453
            check_num_qubits(data)?;
4✔
2454
            let borrowed = observable.borrow();
2✔
2455
            let inner = borrowed.inner.read().map_err(|_| InnerReadError)?;
2✔
2456
            return Ok(inner.clone().into());
2✔
2457
        }
70✔
2458
        // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or
2459
        // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the
2460
        // `extract`.  The empty list will pass either, but it means the same to both functions.
2461
        if let Ok(vec) = data.extract() {
70✔
2462
            return Self::from_list(vec, num_qubits);
12✔
2463
        }
58✔
2464
        if let Ok(vec) = data.extract() {
58✔
2465
            let Some(num_qubits) = num_qubits else {
6✔
2466
                return Err(PyValueError::new_err(
2✔
2467
                    "if using the sparse-list form, 'num_qubits' must be provided",
2✔
2468
                ));
2✔
2469
            };
2470
            return Self::from_sparse_list(vec, num_qubits).map_err(PyErr::from);
4✔
2471
        }
52✔
2472
        if let Ok(term) = data.downcast_exact::<PySparseTerm>() {
52✔
2473
            return term.borrow().to_observable();
10✔
2474
        };
42✔
2475
        if let Ok(observable) = Self::from_terms(data, num_qubits) {
42✔
2476
            return Ok(observable);
14✔
2477
        }
28✔
2478
        Err(PyTypeError::new_err(format!(
28✔
2479
            "unknown input format for 'SparseObservable': {}",
28✔
2480
            data.get_type().repr()?,
28✔
2481
        )))
2482
    }
304✔
2483

2484
    /// Get a copy of this observable.
2485
    ///
2486
    /// Examples:
2487
    ///
2488
    ///     .. code-block:: python
2489
    ///
2490
    ///         >>> obs = SparseObservable.from_list([("IXZ+lr01", 2.5), ("ZXI-rl10", 0.5j)])
2491
    ///         >>> assert obs == obs.copy()
2492
    ///         >>> assert obs is not obs.copy()
2493
    fn copy(&self) -> PyResult<Self> {
844✔
2494
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
844✔
2495
        Ok(inner.clone().into())
844✔
2496
    }
844✔
2497

2498
    /// The number of qubits the operator acts on.
2499
    ///
2500
    /// This is not inferable from any other shape or values, since identities are not stored
2501
    /// explicitly.
2502
    #[getter]
2503
    #[inline]
2504
    pub fn num_qubits(&self) -> PyResult<u32> {
4,360✔
2505
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
4,360✔
2506
        Ok(inner.num_qubits())
4,360✔
2507
    }
4,360✔
2508

2509
    /// The number of terms in the sum this operator is tracking.
2510
    #[getter]
2511
    #[inline]
2512
    pub fn num_terms(&self) -> PyResult<usize> {
992✔
2513
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
992✔
2514
        Ok(inner.num_terms())
992✔
2515
    }
992✔
2516

2517
    /// The coefficients of each abstract term in in the sum.  This has as many elements as terms in
2518
    /// the sum.
2519
    #[getter]
2520
    fn get_coeffs(slf_: &Bound<Self>) -> ArrayView {
1,604✔
2521
        let borrowed = slf_.borrow();
1,604✔
2522
        ArrayView {
1,604✔
2523
            base: borrowed.inner.clone(),
1,604✔
2524
            slot: ArraySlot::Coeffs,
1,604✔
2525
        }
1,604✔
2526
    }
1,604✔
2527

2528
    /// A flat list of single-qubit terms.  This is more naturally a list of lists, but is stored
2529
    /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.`
2530
    #[getter]
2531
    fn get_bit_terms(slf_: &Bound<Self>) -> ArrayView {
1,234✔
2532
        let borrowed = slf_.borrow();
1,234✔
2533
        ArrayView {
1,234✔
2534
            base: borrowed.inner.clone(),
1,234✔
2535
            slot: ArraySlot::BitTerms,
1,234✔
2536
        }
1,234✔
2537
    }
1,234✔
2538

2539
    /// A flat list of the qubit indices that the corresponding entries in :attr:`bit_terms` act on.
2540
    /// This list must always be term-wise sorted, where a term is a sublist as denoted by
2541
    /// :attr:`boundaries`.
2542
    ///
2543
    /// .. warning::
2544
    ///
2545
    ///     If writing to this attribute from Python space, you *must* ensure that you only write in
2546
    ///     indices that are term-wise sorted.
2547
    #[getter]
2548
    fn get_indices(slf_: &Bound<Self>) -> ArrayView {
1,126✔
2549
        let borrowed = slf_.borrow();
1,126✔
2550
        ArrayView {
1,126✔
2551
            base: borrowed.inner.clone(),
1,126✔
2552
            slot: ArraySlot::Indices,
1,126✔
2553
        }
1,126✔
2554
    }
1,126✔
2555

2556
    /// Indices that partition :attr:`bit_terms` and :attr:`indices` into sublists for each
2557
    /// individual term in the sum.  ``boundaries[0] : boundaries[1]`` is the range of indices into
2558
    /// :attr:`bit_terms` and :attr:`indices` that correspond to the first term of the sum.  All
2559
    /// unspecified qubit indices are implicitly the identity.  This is one item longer than
2560
    /// :attr:`coeffs`, since ``boundaries[0]`` is always an explicit zero (for algorithmic ease).
2561
    #[getter]
2562
    fn get_boundaries(slf_: &Bound<Self>) -> ArrayView {
2,082✔
2563
        let borrowed = slf_.borrow();
2,082✔
2564
        ArrayView {
2,082✔
2565
            base: borrowed.inner.clone(),
2,082✔
2566
            slot: ArraySlot::Boundaries,
2,082✔
2567
        }
2,082✔
2568
    }
2,082✔
2569

2570
    /// Get the zero operator over the given number of qubits.
2571
    ///
2572
    /// The zero operator is the operator whose expectation value is zero for all quantum states.
2573
    /// It has no terms.  It is the identity element for addition of two :class:`SparseObservable`
2574
    /// instances; anything added to the zero operator is equal to itself.
2575
    ///
2576
    /// If you want the projector onto the all zeros state, use::
2577
    ///
2578
    ///     >>> num_qubits = 10
2579
    ///     >>> all_zeros = SparseObservable.from_label("0" * num_qubits)
2580
    ///
2581
    /// Examples:
2582
    ///
2583
    ///     Get the zero operator for 100 qubits::
2584
    ///
2585
    ///         >>> SparseObservable.zero(100)
2586
    ///         <SparseObservable with 0 terms on 100 qubits: 0.0>
2587
    #[pyo3(signature = (/, num_qubits))]
2588
    #[staticmethod]
2589
    pub fn zero(num_qubits: u32) -> Self {
698✔
2590
        SparseObservable::zero(num_qubits).into()
698✔
2591
    }
698✔
2592

2593
    /// Get the identity operator over the given number of qubits.
2594
    ///
2595
    /// Examples:
2596
    ///
2597
    ///     Get the identity operator for 100 qubits::
2598
    ///
2599
    ///         >>> SparseObservable.identity(100)
2600
    ///         <SparseObservable with 1 term on 100 qubits: (1+0j)()>
2601
    #[pyo3(signature = (/, num_qubits))]
2602
    #[staticmethod]
2603
    pub fn identity(num_qubits: u32) -> Self {
678✔
2604
        SparseObservable::identity(num_qubits).into()
678✔
2605
    }
678✔
2606

2607
    /// Construct a :class:`.SparseObservable` from a single :class:`.Pauli` instance.
2608
    ///
2609
    /// The output observable will have a single term, with a unitary coefficient dependent on the
2610
    /// phase.
2611
    ///
2612
    /// Args:
2613
    ///     pauli (:class:`.Pauli`): the single Pauli to convert.
2614
    ///
2615
    /// Examples:
2616
    ///
2617
    ///     .. code-block:: python
2618
    ///
2619
    ///         >>> label = "IYXZI"
2620
    ///         >>> pauli = Pauli(label)
2621
    ///         >>> SparseObservable.from_pauli(pauli)
2622
    ///         <SparseObservable with 1 term on 5 qubits: (1+0j)(Y_3 X_2 Z_1)>
2623
    ///         >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli)
2624
    #[staticmethod]
2625
    #[pyo3(signature = (pauli, /))]
2626
    fn from_pauli(pauli: &Bound<PyAny>) -> PyResult<Self> {
62✔
2627
        let py = pauli.py();
62✔
2628
        let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::<u32>()?;
62✔
2629
        let z = pauli
62✔
2630
            .getattr(intern!(py, "z"))?
62✔
2631
            .extract::<PyReadonlyArray1<bool>>()?;
62✔
2632
        let x = pauli
62✔
2633
            .getattr(intern!(py, "x"))?
62✔
2634
            .extract::<PyReadonlyArray1<bool>>()?;
62✔
2635
        let mut bit_terms = Vec::new();
62✔
2636
        let mut indices = Vec::new();
62✔
2637
        let mut num_ys = 0;
62✔
2638
        for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() {
400✔
2639
            // The only failure case possible here is the identity, because of how we're
2640
            // constructing the value to convert.
2641
            let Ok(term) = ::bytemuck::checked::try_cast((*x as u8) << 1 | (*z as u8)) else {
400✔
2642
                continue;
160✔
2643
            };
2644
            num_ys += (term == BitTerm::Y) as isize;
240✔
2645
            indices.push(i as u32);
240✔
2646
            bit_terms.push(term);
240✔
2647
        }
2648
        let boundaries = vec![0, indices.len()];
62✔
2649
        // The "empty" state of a `Pauli` represents the identity, which isn't our empty state
2650
        // (that's zero), so we're always going to have a coefficient.
2651
        let group_phase = pauli
62✔
2652
            // `Pauli`'s `_phase` is a Numpy array ...
62✔
2653
            .getattr(intern!(py, "_phase"))?
62✔
2654
            // ... that should have exactly 1 element ...
2655
            .call_method0(intern!(py, "item"))?
62✔
2656
            // ... which is some integral type.
2657
            .extract::<isize>()?;
62✔
2658
        let phase = match (group_phase - num_ys).rem_euclid(4) {
62✔
2659
            0 => Complex64::new(1.0, 0.0),
40✔
2660
            1 => Complex64::new(0.0, -1.0),
10✔
2661
            2 => Complex64::new(-1.0, 0.0),
2✔
2662
            3 => Complex64::new(0.0, 1.0),
10✔
2663
            _ => unreachable!("`x % 4` has only four values"),
×
2664
        };
2665
        let coeffs = vec![phase];
62✔
2666
        let inner = SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)?;
62✔
2667
        Ok(inner.into())
62✔
2668
    }
62✔
2669

2670
    /// Construct a single-term observable from a dense string label.
2671
    ///
2672
    /// The resulting operator will have a coefficient of 1.  The label must be a sequence of the
2673
    /// alphabet ``'IXYZ+-rl01'``.  The label is interpreted analogously to a bitstring.  In other
2674
    /// words, the right-most letter is associated with qubit 0, and so on.  This is the same as the
2675
    /// labels for :class:`.Pauli` and :class:`.SparsePauliOp`.
2676
    ///
2677
    /// Args:
2678
    ///     label (str): the dense label.
2679
    ///
2680
    /// Examples:
2681
    ///
2682
    ///     .. code-block:: python
2683
    ///
2684
    ///         >>> SparseObservable.from_label("IIII+ZI")
2685
    ///         <SparseObservable with 1 term on 7 qubits: (1+0j)(+_2 Z_1)>
2686
    ///         >>> label = "IYXZI"
2687
    ///         >>> pauli = Pauli(label)
2688
    ///         >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli)
2689
    ///
2690
    /// See also:
2691
    ///     :meth:`from_list`
2692
    ///         A generalization of this method that constructs a sum operator from multiple labels
2693
    ///         and their corresponding coefficients.
2694
    #[staticmethod]
2695
    #[pyo3(signature = (label, /))]
2696
    fn from_label(label: &str) -> Result<Self, LabelError> {
818✔
2697
        let mut inner = SparseObservable::zero(label.len() as u32);
818✔
2698
        inner.add_dense_label(label, Complex64::new(1.0, 0.0))?;
818✔
2699
        Ok(inner.into())
802✔
2700
    }
818✔
2701

2702
    /// Construct an observable from a list of dense labels and coefficients.
2703
    ///
2704
    /// This is analogous to :meth:`.SparsePauliOp.from_list`, except it uses
2705
    /// :ref:`the extended alphabet <sparse-observable-alphabet>` of :class:`.SparseObservable`.  In
2706
    /// this dense form, you must supply all identities explicitly in each label.
2707
    ///
2708
    /// The label must be a sequence of the alphabet ``'IXYZ+-rl01'``.  The label is interpreted
2709
    /// analogously to a bitstring.  In other words, the right-most letter is associated with qubit
2710
    /// 0, and so on.  This is the same as the labels for :class:`.Pauli` and
2711
    /// :class:`.SparsePauliOp`.
2712
    ///
2713
    /// Args:
2714
    ///     iter (list[tuple[str, complex]]): Pairs of labels and their associated coefficients to
2715
    ///         sum. The labels are interpreted the same way as in :meth:`from_label`.
2716
    ///     num_qubits (int | None): It is not necessary to specify this if you are sure that
2717
    ///         ``iter`` is not an empty sequence, since it can be inferred from the label lengths.
2718
    ///         If ``iter`` may be empty, you must specify this argument to disambiguate how many
2719
    ///         qubits the observable is for.  If this is given and ``iter`` is not empty, the value
2720
    ///         must match the label lengths.
2721
    ///
2722
    /// Examples:
2723
    ///
2724
    ///     Construct an observable from a list of labels of the same length::
2725
    ///
2726
    ///         >>> SparseObservable.from_list([
2727
    ///         ...     ("III++", 1.0),
2728
    ///         ...     ("II--I", 1.0j),
2729
    ///         ...     ("I++II", -0.5),
2730
    ///         ...     ("--III", -0.25j),
2731
    ///         ... ])
2732
    ///         <SparseObservable with 4 terms on 5 qubits:
2733
    ///             (1+0j)(+_1 +_0) + (0+1j)(-_2 -_1) + (-0.5+0j)(+_3 +_2) + (-0-0.25j)(-_4 -_3)>
2734
    ///
2735
    ///     Use ``num_qubits`` to disambiguate potentially empty inputs::
2736
    ///
2737
    ///         >>> SparseObservable.from_list([], num_qubits=10)
2738
    ///         <SparseObservable with 0 terms on 10 qubits: 0.0>
2739
    ///
2740
    ///     This method is equivalent to calls to :meth:`from_sparse_list` with the explicit
2741
    ///     qubit-arguments field set to decreasing integers::
2742
    ///
2743
    ///         >>> labels = ["XY+Z", "rl01", "-lXZ"]
2744
    ///         >>> coeffs = [1.5j, 2.0, -0.5]
2745
    ///         >>> from_list = SparseObservable.from_list(list(zip(labels, coeffs)))
2746
    ///         >>> from_sparse_list = SparseObservable.from_sparse_list([
2747
    ///         ...     (label, (3, 2, 1, 0), coeff)
2748
    ///         ...     for label, coeff in zip(labels, coeffs)
2749
    ///         ... ])
2750
    ///         >>> assert from_list == from_sparse_list
2751
    ///
2752
    /// See also:
2753
    ///     :meth:`from_label`
2754
    ///         A similar constructor, but takes only a single label and always has its coefficient
2755
    ///         set to ``1.0``.
2756
    ///
2757
    ///     :meth:`from_sparse_list`
2758
    ///         Construct the observable from a list of labels without explicit identities, but with
2759
    ///         the qubits each single-qubit term applies to listed explicitly.
2760
    #[staticmethod]
2761
    #[pyo3(signature = (iter, /, *, num_qubits=None))]
2762
    fn from_list(iter: Vec<(String, Complex64)>, num_qubits: Option<u32>) -> PyResult<Self> {
622✔
2763
        if iter.is_empty() && num_qubits.is_none() {
622✔
2764
            return Err(PyValueError::new_err(
2✔
2765
                "cannot construct an observable from an empty list without knowing `num_qubits`",
2✔
2766
            ));
2✔
2767
        }
620✔
2768
        let num_qubits = match num_qubits {
620✔
2769
            Some(num_qubits) => num_qubits,
22✔
2770
            None => iter[0].0.len() as u32,
598✔
2771
        };
2772
        let mut inner = SparseObservable::with_capacity(num_qubits, iter.len(), 0);
620✔
2773
        for (label, coeff) in iter {
2,076✔
2774
            inner.add_dense_label(&label, coeff)?;
1,472✔
2775
        }
2776
        Ok(inner.into())
604✔
2777
    }
622✔
2778

2779
    /// Construct an observable from a list of labels, the qubits each item applies to, and the
2780
    /// coefficient of the whole term.
2781
    ///
2782
    /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`, except it uses
2783
    /// :ref:`the extended alphabet <sparse-observable-alphabet>` of :class:`.SparseObservable`.
2784
    ///
2785
    /// The "labels" and "indices" fields of the triples are associated by zipping them together.
2786
    /// For example, this means that a call to :meth:`from_list` can be converted to the form used
2787
    /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1,
2788
    /// 0)``.
2789
    ///
2790
    /// Args:
2791
    ///     iter (list[tuple[str, Sequence[int], complex]]): triples of labels, the qubits
2792
    ///         each single-qubit term applies to, and the coefficient of the entire term.
2793
    ///
2794
    ///     num_qubits (int): the number of qubits in the operator.
2795
    ///
2796
    /// Examples:
2797
    ///
2798
    ///     Construct a simple operator::
2799
    ///
2800
    ///         >>> SparseObservable.from_sparse_list(
2801
    ///         ...     [("ZX", (1, 4), 1.0), ("YY", (0, 3), 2j)],
2802
    ///         ...     num_qubits=5,
2803
    ///         ... )
2804
    ///         <SparseObservable with 2 terms on 5 qubits: (1+0j)(X_4 Z_1) + (0+2j)(Y_3 Y_0)>
2805
    ///
2806
    ///     Construct the identity observable (though really, just use :meth:`identity`)::
2807
    ///
2808
    ///         >>> SparseObservable.from_sparse_list([("", (), 1.0)], num_qubits=100)
2809
    ///         <SparseObservable with 1 term on 100 qubits: (1+0j)()>
2810
    ///
2811
    ///     This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments
2812
    ///     field of the triple is set to decreasing integers::
2813
    ///
2814
    ///         >>> labels = ["XY+Z", "rl01", "-lXZ"]
2815
    ///         >>> coeffs = [1.5j, 2.0, -0.5]
2816
    ///         >>> from_list = SparseObservable.from_list(list(zip(labels, coeffs)))
2817
    ///         >>> from_sparse_list = SparseObservable.from_sparse_list([
2818
    ///         ...     (label, (3, 2, 1, 0), coeff)
2819
    ///         ...     for label, coeff in zip(labels, coeffs)
2820
    ///         ... ])
2821
    ///         >>> assert from_list == from_sparse_list
2822
    ///
2823
    /// See also:
2824
    ///     :meth:`to_sparse_list`
2825
    ///         The reverse of this method.
2826
    #[staticmethod]
2827
    #[pyo3(signature = (iter, /, num_qubits))]
2828
    fn from_sparse_list(
1,738✔
2829
        iter: Vec<(String, Vec<u32>, Complex64)>,
1,738✔
2830
        num_qubits: u32,
1,738✔
2831
    ) -> PyResult<Self> {
1,738✔
2832
        let coeffs = iter.iter().map(|(_, _, coeff)| *coeff).collect();
2,074✔
2833
        let mut boundaries = Vec::with_capacity(iter.len() + 1);
1,738✔
2834
        boundaries.push(0);
1,738✔
2835
        let mut indices = Vec::new();
1,738✔
2836
        let mut bit_terms = Vec::new();
1,738✔
2837
        // Insertions to the `BTreeMap` keep it sorted by keys, so we use this to do the termwise
1,738✔
2838
        // sorting on-the-fly.
1,738✔
2839
        let mut sorted = btree_map::BTreeMap::new();
1,738✔
2840
        for (label, qubits, _) in iter {
3,792✔
2841
            sorted.clear();
2,074✔
2842
            let label: &[u8] = label.as_ref();
2,074✔
2843
            if label.len() != qubits.len() {
2,074✔
2844
                return Err(LabelError::WrongLengthIndices {
6✔
2845
                    label: label.len(),
6✔
2846
                    indices: indices.len(),
6✔
2847
                }
6✔
2848
                .into());
6✔
2849
            }
2,068✔
2850
            for (letter, index) in label.iter().zip(qubits) {
6,052✔
2851
                if index >= num_qubits {
6,052✔
2852
                    return Err(LabelError::BadIndex { index, num_qubits }.into());
6✔
2853
                }
6,046✔
2854
                let btree_map::Entry::Vacant(entry) = sorted.entry(index) else {
6,046✔
2855
                    return Err(LabelError::DuplicateIndex { index }.into());
4✔
2856
                };
2857
                entry.insert(
6,042✔
2858
                    BitTerm::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?,
6,042✔
2859
                );
2860
            }
2861
            for (index, term) in sorted.iter() {
6,014✔
2862
                let Some(term) = term else {
6,014✔
2863
                    continue;
8✔
2864
                };
2865
                indices.push(*index);
6,006✔
2866
                bit_terms.push(*term);
6,006✔
2867
            }
2868
            boundaries.push(bit_terms.len());
2,054✔
2869
        }
2870
        let inner = SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)?;
1,718✔
2871
        Ok(inner.into())
1,718✔
2872
    }
1,738✔
2873

2874
    /// Express the observable in Pauli terms only, by writing each projector as sum of Pauli terms.
2875
    ///
2876
    /// Note that there is no guarantee of the order the resulting Pauli terms. Use
2877
    /// :meth:`SparseObservable.simplify` in addition to obtain a canonical representation.
2878
    ///
2879
    /// .. warning::
2880
    ///
2881
    ///     Beware that this will use at least :math:`2^n` terms if there are :math:`n`
2882
    ///     single-qubit projectors present, which can lead to an exponential number of terms.
2883
    ///
2884
    /// Returns:
2885
    ///     The same observable, but expressed in Pauli terms only.
2886
    ///
2887
    /// Examples:
2888
    ///
2889
    ///     Rewrite an observable in terms of projectors into Pauli operators::
2890
    ///
2891
    ///         >>> obs = SparseObservable("+")
2892
    ///         >>> obs.as_paulis()
2893
    ///         <SparseObservable with 2 terms on 1 qubit: (0.5+0j)() + (0.5+0j)(X_0)>
2894
    ///         >>> direct = SparseObservable.from_list([("I", 0.5), ("Z", 0.5)])
2895
    ///         >>> assert direct.simplify() == obs.as_paulis().simplify()
2896
    ///
2897
    ///     For small operators, this can be used with :meth:`simplify` as a unique canonical form::
2898
    ///
2899
    ///         >>> left = SparseObservable.from_list([("+", 0.5), ("-", 0.5)])
2900
    ///         >>> right = SparseObservable.from_list([("r", 0.5), ("l", 0.5)])
2901
    ///         >>> assert left.as_paulis().simplify() == right.as_paulis().simplify()
2902
    ///
2903
    /// See also:
2904
    ///     :meth:`.SparsePauliOp.from_sparse_observable`
2905
    ///         A constructor of :class:`.SparsePauliOp` that can convert a
2906
    ///         :class:`SparseObservable` in the :class:`.SparsePauliOp` dense Pauli representation.
2907
    fn as_paulis(&self) -> PyResult<Self> {
2,030✔
2908
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
2,030✔
2909
        Ok(inner.as_paulis().into())
2,030✔
2910
    }
2,030✔
2911

2912
    /// Express the observable in terms of a sparse list format.
2913
    ///
2914
    /// This can be seen as counter-operation of :meth:`.SparseObservable.from_sparse_list`, however
2915
    /// the order of terms is not guaranteed to be the same at after a roundtrip to a sparse
2916
    /// list and back.
2917
    ///
2918
    /// Examples:
2919
    ///
2920
    ///     >>> obs = SparseObservable.from_list([("IIXIZ", 2j), ("IIZIX", 2j)])
2921
    ///     >>> reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits)
2922
    ///
2923
    /// See also:
2924
    ///     :meth:`from_sparse_list`
2925
    ///         The constructor that can interpret these lists.
2926
    #[pyo3(signature = ())]
2927
    fn to_sparse_list(&self, py: Python) -> PyResult<Py<PyList>> {
2,080✔
2928
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
2,080✔
2929

2930
        // turn a SparseView into a Python tuple of (bit terms, indices, coeff)
2931
        let to_py_tuple = |view: SparseTermView| {
3,868✔
2932
            let mut pauli_string = String::with_capacity(view.bit_terms.len());
3,868✔
2933

2934
            // we reverse the order of bits and indices so the Pauli string comes out in
2935
            // "reading order", consistent with how one would write the label in
2936
            // SparseObservable.from_list or .from_label
2937
            for bit in view.bit_terms.iter().rev() {
10,754✔
2938
                pauli_string.push_str(bit.py_label());
10,754✔
2939
            }
10,754✔
2940
            let py_string = PyString::new(py, &pauli_string).unbind();
3,868✔
2941
            let py_indices = PyList::new(py, view.indices.iter().rev())?.unbind();
3,868✔
2942
            let py_coeff = view.coeff.into_py_any(py)?;
3,868✔
2943

2944
            PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any(), &py_coeff])
3,868✔
2945
        };
3,868✔
2946

2947
        let out = PyList::empty(py);
2,080✔
2948
        for view in inner.iter() {
3,868✔
2949
            out.append(to_py_tuple(view)?)?;
3,868✔
2950
        }
2951
        Ok(out.unbind())
2,080✔
2952
    }
2,080✔
2953

2954
    /// Construct a :class:`.SparseObservable` from a :class:`.SparsePauliOp` instance.
2955
    ///
2956
    /// This will be a largely direct translation of the :class:`.SparsePauliOp`; in particular,
2957
    /// there is no on-the-fly summing of like terms, nor any attempt to refactorize sums of Pauli
2958
    /// terms into equivalent projection operators.
2959
    ///
2960
    /// Args:
2961
    ///     op (:class:`.SparsePauliOp`): the operator to convert.
2962
    ///
2963
    /// Examples:
2964
    ///
2965
    ///     .. code-block:: python
2966
    ///
2967
    ///         >>> spo = SparsePauliOp.from_list([("III", 1.0), ("IIZ", 0.5), ("IZI", 0.5)])
2968
    ///         >>> SparseObservable.from_sparse_pauli_op(spo)
2969
    ///         <SparseObservable with 3 terms on 3 qubits: (1+0j)() + (0.5+0j)(Z_0) + (0.5+0j)(Z_1)>
2970
    #[staticmethod]
2971
    #[pyo3(signature = (op, /))]
2972
    fn from_sparse_pauli_op(op: &Bound<PyAny>) -> PyResult<Self> {
38✔
2973
        let py = op.py();
38✔
2974
        let pauli_list_ob = op.getattr(intern!(py, "paulis"))?;
38✔
2975
        let coeffs = op
38✔
2976
            .getattr(intern!(py, "coeffs"))?
38✔
2977
            .extract::<PyReadonlyArray1<Complex64>>()
38✔
2978
            .map_err(|_| PyTypeError::new_err("only 'SparsePauliOp' with complex-typed coefficients can be converted to 'SparseObservable'"))?
38✔
2979
            .as_array()
34✔
2980
            .to_vec();
34✔
2981
        let op_z = pauli_list_ob
34✔
2982
            .getattr(intern!(py, "z"))?
34✔
2983
            .extract::<PyReadonlyArray2<bool>>()?;
34✔
2984
        let op_x = pauli_list_ob
34✔
2985
            .getattr(intern!(py, "x"))?
34✔
2986
            .extract::<PyReadonlyArray2<bool>>()?;
34✔
2987
        // We don't extract the `phase`, because that's supposed to be 0 for all `SparsePauliOp`
2988
        // instances - they use the symplectic convention in the representation with any phase term
2989
        // absorbed into the coefficients (like us).
2990
        let [num_terms, num_qubits] = *op_z.shape() else {
34✔
2991
            unreachable!("shape is statically known to be 2D")
×
2992
        };
2993
        if op_x.shape() != [num_terms, num_qubits] {
34✔
2994
            return Err(PyValueError::new_err(format!(
×
2995
                "'x' and 'z' have different shapes ({:?} and {:?})",
×
2996
                op_x.shape(),
×
2997
                op_z.shape()
×
2998
            )));
×
2999
        }
34✔
3000
        if num_terms != coeffs.len() {
34✔
3001
            return Err(PyValueError::new_err(format!(
×
3002
                "'x' and 'z' have a different number of operators to 'coeffs' ({} and {})",
×
3003
                num_terms,
×
3004
                coeffs.len(),
×
3005
            )));
×
3006
        }
34✔
3007

34✔
3008
        let mut bit_terms = Vec::new();
34✔
3009
        let mut indices = Vec::new();
34✔
3010
        let mut boundaries = Vec::with_capacity(num_terms + 1);
34✔
3011
        boundaries.push(0);
34✔
3012
        for (term_x, term_z) in op_x
66✔
3013
            .as_array()
34✔
3014
            .rows()
34✔
3015
            .into_iter()
34✔
3016
            .zip(op_z.as_array().rows())
34✔
3017
        {
3018
            for (i, (x, z)) in term_x.iter().zip(term_z.iter()).enumerate() {
342✔
3019
                // The only failure case possible here is the identity, because of how we're
3020
                // constructing the value to convert.
3021
                let Ok(term) = ::bytemuck::checked::try_cast((*x as u8) << 1 | (*z as u8)) else {
342✔
3022
                    continue;
178✔
3023
                };
3024
                indices.push(i as u32);
164✔
3025
                bit_terms.push(term);
164✔
3026
            }
3027
            boundaries.push(indices.len());
66✔
3028
        }
3029

3030
        let inner =
34✔
3031
            SparseObservable::new(num_qubits as u32, coeffs, bit_terms, indices, boundaries)?;
34✔
3032
        Ok(inner.into())
34✔
3033
    }
38✔
3034

3035
    /// Construct a :class:`SparseObservable` out of individual terms.
3036
    ///
3037
    /// All the terms must have the same number of qubits.  If supplied, the ``num_qubits`` argument
3038
    /// must match the terms.
3039
    ///
3040
    /// No simplification is done as part of the observable creation.
3041
    ///
3042
    /// Args:
3043
    ///     obj (Iterable[Term]): Iterable of individual terms to build the observable from.
3044
    ///     num_qubits (int | None): The number of qubits the observable should act on.  This is
3045
    ///         usually inferred from the input, but can be explicitly given to handle the case
3046
    ///         of an empty iterable.
3047
    ///
3048
    /// Returns:
3049
    ///     The corresponding observable.
3050
    #[staticmethod]
3051
    #[pyo3(signature = (obj, /, num_qubits=None))]
3052
    fn from_terms(obj: &Bound<PyAny>, num_qubits: Option<u32>) -> PyResult<Self> {
62✔
3053
        let mut iter = obj.try_iter()?;
62✔
3054
        let mut inner = match num_qubits {
48✔
3055
            Some(num_qubits) => SparseObservable::zero(num_qubits),
12✔
3056
            None => {
3057
                let Some(first) = iter.next() else {
36✔
3058
                    return Err(PyValueError::new_err(
14✔
3059
                        "cannot construct an observable from an empty list without knowing `num_qubits`",
14✔
3060
                    ));
14✔
3061
                };
3062
                let py_term = first?.downcast::<PySparseTerm>()?.borrow();
22✔
3063
                py_term.inner.to_observable()
22✔
3064
            }
3065
        };
3066
        for bound_py_term in iter {
74✔
3067
            let py_term = bound_py_term?.downcast::<PySparseTerm>()?.borrow();
46✔
3068
            inner.add_term(py_term.inner.view())?;
44✔
3069
        }
3070
        Ok(inner.into())
28✔
3071
    }
62✔
3072

3073
    // SAFETY: this cannot invoke undefined behaviour if `check = true`, but if `check = false` then
3074
    // the `bit_terms` must all be valid `BitTerm` representations.
3075
    /// Construct a :class:`.SparseObservable` from raw Numpy arrays that match :ref:`the required
3076
    /// data representation described in the class-level documentation <sparse-observable-arrays>`.
3077
    ///
3078
    /// The data from each array is copied into fresh, growable Rust-space allocations.
3079
    ///
3080
    /// Args:
3081
    ///     num_qubits: number of qubits in the observable.
3082
    ///     coeffs: complex coefficients of each term of the observable.  This should be a Numpy
3083
    ///         array with dtype :attr:`~numpy.complex128`.
3084
    ///     bit_terms: flattened list of the single-qubit terms comprising all complete terms.  This
3085
    ///         should be a Numpy array with dtype :attr:`~numpy.uint8` (which is compatible with
3086
    ///         :class:`.BitTerm`).
3087
    ///     indices: flattened term-wise sorted list of the qubits each single-qubit term corresponds
3088
    ///         to.  This should be a Numpy array with dtype :attr:`~numpy.uint32`.
3089
    ///     boundaries: the indices that partition ``bit_terms`` and ``indices`` into terms.  This
3090
    ///         should be a Numpy array with dtype :attr:`~numpy.uintp`.
3091
    ///     check: if ``True`` (the default), validate that the data satisfies all coherence
3092
    ///         guarantees.  If ``False``, no checks are done.
3093
    ///
3094
    ///         .. warning::
3095
    ///
3096
    ///             If ``check=False``, the ``bit_terms`` absolutely *must* be all be valid values
3097
    ///             of :class:`.SparseObservable.BitTerm`.  If they are not, Rust-space undefined
3098
    ///             behavior may occur, entirely invalidating the program execution.
3099
    ///
3100
    /// Examples:
3101
    ///
3102
    ///     Construct a sum of :math:`Z` on each individual qubit::
3103
    ///
3104
    ///         >>> num_qubits = 100
3105
    ///         >>> terms = np.full((num_qubits,), SparseObservable.BitTerm.Z, dtype=np.uint8)
3106
    ///         >>> indices = np.arange(num_qubits, dtype=np.uint32)
3107
    ///         >>> coeffs = np.ones((num_qubits,), dtype=complex)
3108
    ///         >>> boundaries = np.arange(num_qubits + 1, dtype=np.uintp)
3109
    ///         >>> SparseObservable.from_raw_parts(num_qubits, coeffs, terms, indices, boundaries)
3110
    ///         <SparseObservable with 100 terms on 100 qubits: (1+0j)(Z_0) + ... + (1+0j)(Z_99)>
3111
    #[staticmethod]
3112
    #[pyo3(
3113
        signature = (/, num_qubits, coeffs, bit_terms, indices, boundaries, check=true),
3114
    )]
3115
    unsafe fn from_raw_parts<'py>(
540✔
3116
        num_qubits: u32,
540✔
3117
        coeffs: PyArrayLike1<'py, Complex64>,
540✔
3118
        bit_terms: PyArrayLike1<'py, u8>,
540✔
3119
        indices: PyArrayLike1<'py, u32>,
540✔
3120
        boundaries: PyArrayLike1<'py, usize>,
540✔
3121
        check: bool,
540✔
3122
    ) -> PyResult<Self> {
540✔
3123
        let coeffs = coeffs.as_array().to_vec();
540✔
3124
        let bit_terms = if check {
540✔
3125
            bit_terms
482✔
3126
                .as_array()
482✔
3127
                .into_iter()
482✔
3128
                .copied()
482✔
3129
                .map(BitTerm::try_from)
482✔
3130
                .collect::<Result<_, _>>()?
482✔
3131
        } else {
3132
            let bit_terms_as_u8 = bit_terms.as_array().to_vec();
58✔
3133
            // SAFETY: the caller enforced that each `u8` is a valid `BitTerm`, and `BitTerm` is be
58✔
3134
            // represented by a `u8`.  We can't use `bytemuck` because we're casting a `Vec`.
58✔
3135
            unsafe { ::std::mem::transmute::<Vec<u8>, Vec<BitTerm>>(bit_terms_as_u8) }
58✔
3136
        };
3137
        let indices = indices.as_array().to_vec();
538✔
3138
        let boundaries = boundaries.as_array().to_vec();
538✔
3139

3140
        let inner = if check {
538✔
3141
            SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)
480✔
3142
                .map_err(PyErr::from)
480✔
3143
        } else {
3144
            // SAFETY: the caller promised they have upheld the coherence guarantees.
3145
            Ok(unsafe {
58✔
3146
                SparseObservable::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries)
58✔
3147
            })
58✔
3148
        }?;
20✔
3149
        Ok(inner.into())
518✔
3150
    }
540✔
3151

3152
    /// Clear all the terms from this operator, making it equal to the zero operator again.
3153
    ///
3154
    /// This does not change the capacity of the internal allocations, so subsequent addition or
3155
    /// substraction operations may not need to reallocate.
3156
    ///
3157
    /// Examples:
3158
    ///
3159
    ///     .. code-block:: python
3160
    ///
3161
    ///         >>> obs = SparseObservable.from_list([("IX+-rl", 2.0), ("01YZII", -1j)])
3162
    ///         >>> obs.clear()
3163
    ///         >>> assert obs == SparseObservable.zero(obs.py_num_qubits())
3164
    pub fn clear(&mut self) -> PyResult<()> {
14✔
3165
        let mut inner = self.inner.write().map_err(|_| InnerWriteError)?;
14✔
3166
        inner.clear();
14✔
3167
        Ok(())
14✔
3168
    }
14✔
3169

3170
    /// Sum any like terms in this operator, removing them if the resulting complex coefficient has
3171
    /// an absolute value within tolerance of zero.
3172
    ///
3173
    /// As a side effect, this sorts the operator into :ref:`canonical order
3174
    /// <sparse-observable-canonical-order>`.
3175
    ///
3176
    /// .. note::
3177
    ///
3178
    ///     When using this for equality comparisons, note that floating-point rounding and the
3179
    ///     non-associativity fo floating-point addition may cause non-zero coefficients of summed
3180
    ///     terms to compare unequal.  To compare two observables up to a tolerance, it is safest to
3181
    ///     compare the canonicalized difference of the two observables to zero.
3182
    ///
3183
    /// Args:
3184
    ///     tol (float): after summing like terms, any coefficients whose absolute value is less
3185
    ///         than the given absolute tolerance will be suppressed from the output.
3186
    ///
3187
    /// Examples:
3188
    ///
3189
    ///     Using :meth:`simplify` to compare two operators that represent the same observable, but
3190
    ///     would compare unequal due to the structural tests by default::
3191
    ///
3192
    ///         >>> base = SparseObservable.from_sparse_list([
3193
    ///         ...     ("XZ", (2, 1), 1e-10),  # value too small
3194
    ///         ...     ("+-", (3, 1), 2j),
3195
    ///         ...     ("+-", (3, 1), 2j),     # can be combined with the above
3196
    ///         ...     ("01", (3, 1), 0.5),    # out of order compared to `expected`
3197
    ///         ... ], num_qubits=5)
3198
    ///         >>> expected = SparseObservable.from_list([("I0I1I", 0.5), ("I+I-I", 4j)])
3199
    ///         >>> assert base != expected  # non-canonical comparison
3200
    ///         >>> assert base.simplify() == expected.simplify()
3201
    ///
3202
    ///     Note that in the above example, the coefficients are chosen such that all floating-point
3203
    ///     calculations are exact, and there are no intermediate rounding or associativity
3204
    ///     concerns.  If this cannot be guaranteed to be the case, the safer form is::
3205
    ///
3206
    ///         >>> left = SparseObservable.from_list([("XYZ", 1.0/3.0)] * 3)   # sums to 1.0
3207
    ///         >>> right = SparseObservable.from_list([("XYZ", 1.0/7.0)] * 7)  # doesn't sum to 1.0
3208
    ///         >>> assert left.simplify() != right.simplify()
3209
    ///         >>> assert (left - right).simplify() == SparseObservable.zero(left.num_qubits)
3210
    #[pyo3(
3211
        signature = (/, tol=1e-8),
3212
    )]
3213
    fn simplify(&self, tol: f64) -> PyResult<Self> {
148✔
3214
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
148✔
3215
        let simplified = inner.canonicalize(tol);
148✔
3216
        Ok(simplified.into())
148✔
3217
    }
148✔
3218

3219
    /// Calculate the adjoint of this observable.
3220
    ///
3221
    ///
3222
    /// This is well defined in the abstract mathematical sense.  All the terms of the single-qubit
3223
    /// alphabet are self-adjoint, so the result of this operation is the same observable, except
3224
    /// its coefficients are all their complex conjugates.
3225
    ///
3226
    /// Examples:
3227
    ///
3228
    ///     .. code-block::
3229
    ///
3230
    ///         >>> left = SparseObservable.from_list([("XY+-", 1j)])
3231
    ///         >>> right = SparseObservable.from_list([("XY+-", -1j)])
3232
    ///         >>> assert left.adjoint() == right
3233
    fn adjoint(&self) -> PyResult<Self> {
126✔
3234
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
126✔
3235
        Ok(inner.adjoint().into())
126✔
3236
    }
126✔
3237

3238
    /// Calculate the matrix transposition of this observable.
3239
    ///
3240
    /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the
3241
    /// matrix form is taken to be in the $Z$ computational basis.  The $X$- and $Z$-related
3242
    /// alphabet terms are unaffected by the transposition, but $Y$-related terms modify their
3243
    /// alphabet terms.  Precisely:
3244
    ///
3245
    /// * :math:`Y` transposes to :math:`-Y`
3246
    /// * :math:`\lvert r\rangle\langle r\rvert` transposes to :math:`\lvert l\rangle\langle l\rvert`
3247
    /// * :math:`\lvert l\rangle\langle l\rvert` transposes to :math:`\lvert r\rangle\langle r\rvert`
3248
    ///
3249
    /// Examples:
3250
    ///
3251
    ///     .. code-block::
3252
    ///
3253
    ///         >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)])
3254
    ///         >>> assert obs.transpose() == SparseObservable([("III", 1j), ("Ylr", -0.5)])
3255
    fn transpose(&self) -> PyResult<Self> {
132✔
3256
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
132✔
3257
        Ok(inner.transpose().into())
132✔
3258
    }
132✔
3259

3260
    /// Calculate the complex conjugation of this observable.
3261
    ///
3262
    /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the
3263
    /// matrix form is taken to be in the $Z$ computational basis.  The $X$- and $Z$-related
3264
    /// alphabet terms are unaffected by the complex conjugation, but $Y$-related terms modify their
3265
    /// alphabet terms.  Precisely:
3266
    ///
3267
    /// * :math:`Y` conjguates to :math:`-Y`
3268
    /// * :math:`\lvert r\rangle\langle r\rvert` conjugates to :math:`\lvert l\rangle\langle l\rvert`
3269
    /// * :math:`\lvert l\rangle\langle l\rvert` conjugates to :math:`\lvert r\rangle\langle r\rvert`
3270
    ///
3271
    /// Additionally, all coefficients are conjugated.
3272
    ///
3273
    /// Examples:
3274
    ///
3275
    ///     .. code-block::
3276
    ///
3277
    ///         >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)])
3278
    ///         >>> assert obs.conjugate() == SparseObservable([("III", -1j), ("Ylr", -0.5)])
3279
    fn conjugate(&self) -> PyResult<Self> {
132✔
3280
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
132✔
3281
        Ok(inner.conjugate().into())
132✔
3282
    }
132✔
3283

3284
    /// Tensor product of two observables.
3285
    ///
3286
    /// The bit ordering is defined such that the qubit indices of the argument will remain the
3287
    /// same, and the indices of ``self`` will be offset by the number of qubits in ``other``.  This
3288
    /// is the same convention as used by the rest of Qiskit's :mod:`~qiskit.quantum_info`
3289
    /// operators.
3290
    ///
3291
    /// This function is used for the infix ``^`` operator.  If using this operator, beware that
3292
    /// `Python's operator-precedence rules
3293
    /// <https://docs.python.org/3/reference/expressions.html#operator-precedence>`__ may cause the
3294
    /// evaluation order to be different to your expectation.  In particular, the operator ``+``
3295
    /// binds more tightly than ``^``, just like ``*`` binds more tightly than ``+``.  Use
3296
    /// parentheses to fix the evaluation order, if needed.
3297
    ///
3298
    /// The argument will be cast to :class:`SparseObservable` using its default constructor, if it
3299
    /// is not already in the correct form.
3300
    ///
3301
    /// Args:
3302
    ///
3303
    ///     other: the observable to put on the right-hand side of the tensor product.
3304
    ///
3305
    /// Examples:
3306
    ///
3307
    ///     The bit ordering of this is such that the tensor product of two observables made from a
3308
    ///     single label "looks like" an observable made by concatenating the two strings::
3309
    ///
3310
    ///         >>> left = SparseObservable.from_label("XYZ")
3311
    ///         >>> right = SparseObservable.from_label("+-IIrl")
3312
    ///         >>> assert left.tensor(right) == SparseObservable.from_label("XYZ+-IIrl")
3313
    ///
3314
    ///     You can also use the infix ``^`` operator for tensor products, which will similarly cast
3315
    ///     the right-hand side of the operation if it is not already a :class:`SparseObservable`::
3316
    ///
3317
    ///         >>> assert SparseObservable("rl") ^ Pauli("XYZ") == SparseObservable("rlXYZ")
3318
    ///
3319
    /// See also:
3320
    ///     :meth:`expand`
3321
    ///
3322
    ///         The same function, but with the order of arguments flipped.  This can be useful if
3323
    ///         you like using the casting behavior for the argument, but you want your existing
3324
    ///         :class:`SparseObservable` to be on the right-hand side of the tensor ordering.
3325
    #[pyo3(signature = (other, /))]
3326
    fn tensor(&self, other: &Bound<PyAny>) -> PyResult<Py<PyAny>> {
688✔
3327
        let py = other.py();
688✔
3328
        let Some(other) = coerce_to_observable(other)? else {
688✔
3329
            return Err(PyTypeError::new_err(format!(
×
3330
                "unknown type for tensor: {}",
×
3331
                other.get_type().repr()?
×
3332
            )));
3333
        };
3334

3335
        let other = other.borrow();
688✔
3336
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
688✔
3337
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
688✔
3338
        inner.tensor(&other_inner).into_py_any(py)
688✔
3339
    }
688✔
3340

3341
    /// Reverse-order tensor product.
3342
    ///
3343
    /// This is equivalent to ``other.tensor(self)``, except that ``other`` will first be type-cast
3344
    /// to :class:`SparseObservable` if it isn't already one (by calling the default constructor).
3345
    ///
3346
    /// Args:
3347
    ///
3348
    ///     other: the observable to put on the left-hand side of the tensor product.
3349
    ///
3350
    /// Examples:
3351
    ///
3352
    ///     This is equivalent to :meth:`tensor` with the order of the arguments flipped::
3353
    ///
3354
    ///         >>> left = SparseObservable.from_label("XYZ")
3355
    ///         >>> right = SparseObservable.from_label("+-IIrl")
3356
    ///         >>> assert left.tensor(right) == right.expand(left)
3357
    ///
3358
    /// See also:
3359
    ///     :meth:`tensor`
3360
    ///
3361
    ///         The same function with the order of arguments flipped.  :meth:`tensor` is the more
3362
    ///         standard argument ordering, and matches Qiskit's other conventions.
3363
    #[pyo3(signature = (other, /))]
3364
    fn expand<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PySparseObservable>> {
516✔
3365
        let py = other.py();
516✔
3366
        let Some(other) = coerce_to_observable(other)? else {
516✔
3367
            return Err(PyTypeError::new_err(format!(
×
3368
                "unknown type for expand: {}",
×
3369
                other.get_type().repr()?
×
3370
            )));
3371
        };
3372

3373
        let other = other.borrow();
516✔
3374
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
516✔
3375
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
516✔
3376
        other_inner.tensor(&inner).into_pyobject(py)
516✔
3377
    }
516✔
3378

3379
    /// Compose another :class:`SparseObservable` onto this one.
3380
    ///
3381
    /// In terms of operator algebras, composition corresponds to left-multiplication:
3382
    /// ``c = a.compose(b)`` corresponds to $C = B A$.  In other words, ``a.compose(b)`` returns an
3383
    /// operator that "performs ``a``, and then performs ``b`` on the result".  The argument
3384
    /// ``front=True`` instead makes this a right multiplication.
3385
    ///
3386
    /// ``self`` and ``other`` must be the same size, unless ``qargs`` is given, in which case
3387
    /// ``other`` can be smaller than ``self``, provided the number of qubits in ``other`` and the
3388
    /// length of ``qargs`` match.  ``qargs`` can never contain duplicates or indices of qubits that
3389
    /// do not exist in ``self``.
3390
    ///
3391
    /// Beware that this function can cause exponential explosion of the memory usage of the
3392
    /// observable, as the alphabet of :class:`SparseObservable` is not closed under composition;
3393
    /// the composition of two single-bit terms can be a sum, which multiplies the total number of
3394
    /// terms.  This memory usage is not _necessarily_ inherent to the resultant observable, but
3395
    /// finding an efficient re-factorization of the sum is generally equally computationally hard.
3396
    /// It's better to use domain knowledge of your observables to minimize the number of terms that
3397
    /// ever exist, rather than trying to simplify them after the fact.
3398
    ///
3399
    /// Args:
3400
    ///     other: the observable used to left-multiply ``self``.
3401
    ///     qargs: if given, the qubits in ``self`` to be associated with the qubits in ``other``.
3402
    ///         Put another way: if this is given, it is similar to a more efficient implementation
3403
    ///         of::
3404
    ///
3405
    ///             self.compose(other.apply_layout(qargs, self.num_qubits))
3406
    ///
3407
    ///         as no temporary observable is created to store the applied-layout form of ``other``.
3408
    ///     front: if ``True``, then right-multiply by ``other`` instead of left-multiplying
3409
    ///         (default ``False``).  The ``qargs`` are still applied to ``other``.  This is most
3410
    ///         useful when ``qargs`` is set, or ``other`` might be an object that must be coerced
3411
    ///         to :class:`SparseObservable`.
3412
    #[pyo3(signature = (other, /, qargs=None, *, front=false))]
3413
    fn compose<'py>(
450✔
3414
        &self,
450✔
3415
        other: &Bound<'py, PyAny>,
450✔
3416
        qargs: Option<&Bound<'py, PyAny>>,
450✔
3417
        front: bool,
450✔
3418
    ) -> PyResult<Bound<'py, PySparseObservable>> {
450✔
3419
        let py = other.py();
450✔
3420
        let Some(other) = coerce_to_observable(other)? else {
450✔
3421
            return Err(PyTypeError::new_err(format!(
4✔
3422
                "unknown type for compose: {}",
4✔
3423
                other.get_type().repr()?
4✔
3424
            )));
3425
        };
3426
        let other = other.borrow();
446✔
3427
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
446✔
3428
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
446✔
3429
        if let Some(order) = qargs {
446✔
3430
            if other_inner.num_qubits() > inner.num_qubits() {
26✔
3431
                return Err(PyValueError::new_err(format!(
×
3432
                    "argument has more qubits ({}) than the base ({})",
×
3433
                    other_inner.num_qubits(),
×
3434
                    inner.num_qubits()
×
3435
                )));
×
3436
            }
26✔
3437
            let in_length = order.len()?;
26✔
3438
            if other_inner.num_qubits() as usize != in_length {
26✔
3439
                return Err(PyValueError::new_err(format!(
4✔
3440
                    "input qargs has length {}, but the observable is on {} qubit(s)",
4✔
3441
                    in_length,
4✔
3442
                    other_inner.num_qubits()
4✔
3443
                )));
4✔
3444
            }
22✔
3445
            let order = order
22✔
3446
                .try_iter()?
22✔
3447
                .map(|obj| obj.and_then(|obj| obj.extract::<u32>()))
44✔
3448
                .collect::<PyResult<IndexSet<u32>>>()?;
22✔
3449
            if order.len() != in_length {
22✔
3450
                return Err(PyValueError::new_err("duplicate indices in qargs"));
6✔
3451
            }
16✔
3452
            if order
16✔
3453
                .iter()
16✔
3454
                .max()
16✔
3455
                .is_some_and(|max| *max >= inner.num_qubits())
16✔
3456
            {
3457
                return Err(PyValueError::new_err("qargs contains out-of-range qubits"));
4✔
3458
            }
12✔
3459
            if front {
12✔
3460
                // This implementation can be improved if it turns out to be needed a lot and the
3461
                // extra copy is a bottleneck.
3462
                let other_to_self = order.iter().copied().collect::<Vec<_>>();
6✔
3463
                other_inner
6✔
3464
                    .apply_layout(Some(other_to_self.as_slice()), inner.num_qubits)?
6✔
3465
                    .compose(&inner)
6✔
3466
                    .into_pyobject(py)
6✔
3467
            } else {
3468
                inner
6✔
3469
                    .compose_map(&other_inner, |bit| {
12✔
3470
                        *order
12✔
3471
                            .get_index(bit as usize)
12✔
3472
                            .expect("order has the same length and no duplicates")
12✔
3473
                    })
12✔
3474
                    .into_pyobject(py)
6✔
3475
            }
3476
        } else {
3477
            if other_inner.num_qubits() != inner.num_qubits() {
420✔
3478
                return Err(PyValueError::new_err(format!(
4✔
3479
                    "mismatched numbers of qubits: {} (base) and {} (argument)",
4✔
3480
                    inner.num_qubits(),
4✔
3481
                    other_inner.num_qubits()
4✔
3482
                )));
4✔
3483
            }
416✔
3484
            if front {
416✔
3485
                other_inner.compose(&inner).into_pyobject(py)
6✔
3486
            } else {
3487
                inner.compose(&other_inner).into_pyobject(py)
410✔
3488
            }
3489
        }
3490
    }
450✔
3491

3492
    /// Apply a transpiler layout to this :class:`SparseObservable`.
3493
    ///
3494
    /// Typically you will have defined your observable in terms of the virtual qubits of the
3495
    /// circuits you will use to prepare states.  After transpilation, the virtual qubits are mapped
3496
    /// to particular physical qubits on a device, which may be wider than your circuit.  That
3497
    /// mapping can also change over the course of the circuit.  This method transforms the input
3498
    /// observable on virtual qubits to an observable that is suitable to apply immediately after
3499
    /// the fully transpiled *physical* circuit.
3500
    ///
3501
    /// Args:
3502
    ///     layout (TranspileLayout | list[int] | None): The layout to apply.  Most uses of this
3503
    ///         function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that
3504
    ///         was transpiled for hardware.  In addition, you can pass a list of new qubit indices.
3505
    ///         If given as explicitly ``None``, no remapping is applied (but you can still use
3506
    ///         ``num_qubits`` to expand the observable).
3507
    ///     num_qubits (int | None): The number of qubits to expand the observable to.  If not
3508
    ///         supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the
3509
    ///         same width as the input if the ``layout`` is given in another form.
3510
    ///
3511
    /// Returns:
3512
    ///     A new :class:`SparseObservable` with the provided layout applied.
3513
    #[pyo3(signature = (/, layout, num_qubits=None))]
3514
    fn apply_layout(&self, layout: Bound<PyAny>, num_qubits: Option<u32>) -> PyResult<Self> {
70✔
3515
        let py = layout.py();
70✔
3516
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
70✔
3517

3518
        // A utility to check the number of qubits is compatible with the observable.
3519
        let check_inferred_qubits = |inferred: u32| -> PyResult<u32> {
70✔
3520
            if inferred < inner.num_qubits() {
48✔
3521
                return Err(CoherenceError::NotEnoughQubits {
4✔
3522
                    current: inner.num_qubits() as usize,
4✔
3523
                    target: inferred as usize,
4✔
3524
                }
4✔
3525
                .into());
4✔
3526
            }
44✔
3527
            Ok(inferred)
44✔
3528
        };
48✔
3529

3530
        // Normalize the number of qubits in the layout and the layout itself, depending on the
3531
        // input types, before calling SparseObservable.apply_layout to do the actual work.
3532
        let (num_qubits, layout): (u32, Option<Vec<u32>>) = if layout.is_none() {
70✔
3533
            (num_qubits.unwrap_or(inner.num_qubits()), None)
22✔
3534
        } else if layout.is_instance(
48✔
3535
            &py.import(intern!(py, "qiskit.transpiler"))?
48✔
3536
                .getattr(intern!(py, "TranspileLayout"))?,
48✔
3537
        )? {
×
3538
            (
3539
                check_inferred_qubits(
10✔
3540
                    layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32
10✔
3541
                )?,
2✔
3542
                Some(
3543
                    layout
8✔
3544
                        .call_method0(intern!(py, "final_index_layout"))?
8✔
3545
                        .extract::<Vec<u32>>()?,
8✔
3546
                ),
3547
            )
3548
        } else {
3549
            (
3550
                check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?,
38✔
3551
                Some(layout.extract()?),
36✔
3552
            )
3553
        };
3554

3555
        let out = inner.apply_layout(layout.as_deref(), num_qubits)?;
66✔
3556
        Ok(out.into())
58✔
3557
    }
70✔
3558

3559
    /// Get a :class:`.PauliList` object that represents the measurement basis needed for each term
3560
    /// (in order) in this observable.
3561
    ///
3562
    /// For example, the projector ``0l+`` will return a Pauli ``ZXY``.  The resulting
3563
    /// :class:`.Pauli` is dense, in the sense that explicit identities are stored.  An identity in
3564
    /// the Pauli output does not require a concrete measurement.
3565
    ///
3566
    /// This will return an entry in the Pauli list for every term in the sum.
3567
    ///
3568
    /// Returns:
3569
    ///     :class:`.PauliList`: the Pauli operator list representing the necessary measurement
3570
    ///     bases.
3571
    fn pauli_bases<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
2✔
3572
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
2✔
3573
        let mut x = Array2::from_elem([inner.num_terms(), inner.num_qubits() as usize], false);
2✔
3574
        let mut z = Array2::from_elem([inner.num_terms(), inner.num_qubits() as usize], false);
2✔
3575
        for (loc, term) in inner.iter().enumerate() {
12✔
3576
            let mut x_row = x.row_mut(loc);
12✔
3577
            let mut z_row = z.row_mut(loc);
12✔
3578
            for (bit_term, index) in term.bit_terms.iter().zip(term.indices) {
38✔
3579
                x_row[*index as usize] = bit_term.has_x_component();
38✔
3580
                z_row[*index as usize] = bit_term.has_z_component();
38✔
3581
            }
38✔
3582
        }
3583
        PAULI_LIST_TYPE
2✔
3584
            .get_bound(py)
2✔
3585
            .getattr(intern!(py, "from_symplectic"))?
2✔
3586
            .call1((
2✔
3587
                PyArray2::from_owned_array(py, z),
2✔
3588
                PyArray2::from_owned_array(py, x),
2✔
3589
            ))
2✔
3590
    }
2✔
3591

3592
    fn __len__(&self) -> PyResult<usize> {
76✔
3593
        self.num_terms()
76✔
3594
    }
76✔
3595

3596
    fn __getitem__<'py>(
254✔
3597
        &self,
254✔
3598
        py: Python<'py>,
254✔
3599
        index: PySequenceIndex<'py>,
254✔
3600
    ) -> PyResult<Bound<'py, PyAny>> {
254✔
3601
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
254✔
3602
        let indices = match index.with_len(inner.num_terms())? {
254✔
3603
            SequenceIndex::Int(index) => {
224✔
3604
                return PySparseTerm {
224✔
3605
                    inner: inner.term(index).to_term(),
224✔
3606
                }
224✔
3607
                .into_bound_py_any(py)
224✔
3608
            }
3609
            indices => indices,
8✔
3610
        };
8✔
3611
        let mut out = SparseObservable::zero(inner.num_qubits());
8✔
3612
        for index in indices.iter() {
24✔
3613
            out.add_term(inner.term(index))?;
24✔
3614
        }
3615
        out.into_bound_py_any(py)
8✔
3616
    }
254✔
3617

3618
    fn __eq__(slf: Bound<Self>, other: Bound<PyAny>) -> PyResult<bool> {
3,084✔
3619
        // this is also important to check before trying to read both slf and other
3,084✔
3620
        if slf.is(&other) {
3,084✔
3621
            return Ok(true);
2✔
3622
        }
3,082✔
3623
        let Ok(other) = other.downcast_into::<Self>() else {
3,082✔
3624
            return Ok(false);
6✔
3625
        };
3626
        let slf_borrowed = slf.borrow();
3,076✔
3627
        let other_borrowed = other.borrow();
3,076✔
3628
        let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?;
3,076✔
3629
        let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?;
3,076✔
3630
        Ok(slf_inner.eq(&other_inner))
3,076✔
3631
    }
3,084✔
3632

3633
    fn __repr__(&self) -> PyResult<String> {
36✔
3634
        let num_terms = self.num_terms()?;
36✔
3635
        let num_qubits = self.num_qubits()?;
36✔
3636

3637
        let str_num_terms = format!(
36✔
3638
            "{} term{}",
36✔
3639
            num_terms,
36✔
3640
            if num_terms == 1 { "" } else { "s" }
36✔
3641
        );
3642
        let str_num_qubits = format!(
36✔
3643
            "{} qubit{}",
36✔
3644
            num_qubits,
36✔
3645
            if num_qubits == 1 { "" } else { "s" }
36✔
3646
        );
3647

3648
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
36✔
3649
        let str_terms = if num_terms == 0 {
36✔
3650
            "0.0".to_owned()
12✔
3651
        } else {
3652
            inner
24✔
3653
                .iter()
24✔
3654
                .map(SparseTermView::to_sparse_str)
24✔
3655
                .collect::<Vec<_>>()
24✔
3656
                .join(" + ")
24✔
3657
        };
3658
        Ok(format!(
36✔
3659
            "<SparseObservable with {} on {}: {}>",
36✔
3660
            str_num_terms, str_num_qubits, str_terms
36✔
3661
        ))
36✔
3662
    }
36✔
3663

3664
    fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
56✔
3665
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
56✔
3666
        let bit_terms: &[u8] = ::bytemuck::cast_slice(inner.bit_terms());
56✔
3667
        (
56✔
3668
            py.get_type::<Self>().getattr("from_raw_parts")?,
56✔
3669
            (
56✔
3670
                inner.num_qubits(),
56✔
3671
                PyArray1::from_slice(py, inner.coeffs()),
56✔
3672
                PyArray1::from_slice(py, bit_terms),
56✔
3673
                PyArray1::from_slice(py, inner.indices()),
56✔
3674
                PyArray1::from_slice(py, inner.boundaries()),
56✔
3675
                false,
56✔
3676
            ),
56✔
3677
        )
56✔
3678
            .into_pyobject(py)
56✔
3679
    }
56✔
3680

3681
    fn __add__<'py>(
76✔
3682
        slf_: &Bound<'py, Self>,
76✔
3683
        other: &Bound<'py, PyAny>,
76✔
3684
    ) -> PyResult<Bound<'py, PyAny>> {
76✔
3685
        let py = slf_.py();
76✔
3686
        let Some(other) = coerce_to_observable(other)? else {
76✔
3687
            return Ok(py.NotImplemented().into_bound(py));
6✔
3688
        };
3689

3690
        let other = other.borrow();
68✔
3691
        let slf_ = slf_.borrow();
68✔
3692
        if Arc::ptr_eq(&slf_.inner, &other.inner) {
68✔
3693
            // This fast path is for consistency with the in-place `__iadd__`, which would otherwise
3694
            // struggle to do the addition to itself.
3695
            let inner = slf_.inner.read().map_err(|_| InnerReadError)?;
14✔
3696
            return <&SparseObservable as ::std::ops::Mul<_>>::mul(
14✔
3697
                &inner,
14✔
3698
                Complex64::new(2.0, 0.0),
14✔
3699
            )
14✔
3700
            .into_bound_py_any(py);
14✔
3701
        }
54✔
3702
        let slf_inner = slf_.inner.read().map_err(|_| InnerReadError)?;
54✔
3703
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
54✔
3704
        slf_inner.check_equal_qubits(&other_inner)?;
54✔
3705
        <&SparseObservable as ::std::ops::Add>::add(&slf_inner, &other_inner).into_bound_py_any(py)
50✔
3706
    }
76✔
3707

3708
    fn __radd__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
12✔
3709
        // No need to handle the `self is other` case here, because `__add__` will get it.
12✔
3710
        let py = other.py();
12✔
3711
        let Some(other) = coerce_to_observable(other)? else {
12✔
3712
            return Ok(py.NotImplemented().into_bound(py));
2✔
3713
        };
3714

3715
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
8✔
3716
        let other = other.borrow();
8✔
3717
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
8✔
3718
        inner.check_equal_qubits(&other_inner)?;
8✔
3719
        <&SparseObservable as ::std::ops::Add>::add(&other_inner, &inner).into_bound_py_any(py)
8✔
3720
    }
12✔
3721

3722
    fn __iadd__(slf_: Bound<PySparseObservable>, other: &Bound<PyAny>) -> PyResult<()> {
50✔
3723
        let Some(other) = coerce_to_observable(other)? else {
50✔
3724
            // This is not well behaved - we _should_ return `NotImplemented` to Python space
3725
            // without an exception, but limitations in PyO3 prevent this at the moment.  See
3726
            // https://github.com/PyO3/pyo3/issues/4605.
3727
            return Err(PyTypeError::new_err(format!(
2✔
3728
                "invalid object for in-place addition of 'SparseObservable': {}",
2✔
3729
                other.repr()?
2✔
3730
            )));
3731
        };
3732

3733
        let other = other.borrow();
48✔
3734
        let slf_ = slf_.borrow();
48✔
3735
        let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?;
48✔
3736

3737
        // Check if slf_ and other point to the same SparseObservable object, in which case
3738
        // we just multiply it by 2
3739
        if Arc::ptr_eq(&slf_.inner, &other.inner) {
48✔
3740
            *slf_inner *= Complex64::new(2.0, 0.0);
14✔
3741
            return Ok(());
14✔
3742
        }
34✔
3743

3744
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
34✔
3745
        slf_inner.check_equal_qubits(&other_inner)?;
34✔
3746
        slf_inner.add_assign(&other_inner);
34✔
3747
        Ok(())
34✔
3748
    }
50✔
3749

3750
    fn __sub__<'py>(
72✔
3751
        slf_: &Bound<'py, Self>,
72✔
3752
        other: &Bound<'py, PyAny>,
72✔
3753
    ) -> PyResult<Bound<'py, PyAny>> {
72✔
3754
        let py = slf_.py();
72✔
3755
        let Some(other) = coerce_to_observable(other)? else {
72✔
3756
            return Ok(py.NotImplemented().into_bound(py));
2✔
3757
        };
3758

3759
        let other = other.borrow();
68✔
3760
        let slf_ = slf_.borrow();
68✔
3761
        if Arc::ptr_eq(&slf_.inner, &other.inner) {
68✔
3762
            return PySparseObservable::zero(slf_.num_qubits()?).into_bound_py_any(py);
14✔
3763
        }
54✔
3764

3765
        let slf_inner = slf_.inner.read().map_err(|_| InnerReadError)?;
54✔
3766
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
54✔
3767
        slf_inner.check_equal_qubits(&other_inner)?;
54✔
3768
        <&SparseObservable as ::std::ops::Sub>::sub(&slf_inner, &other_inner).into_bound_py_any(py)
50✔
3769
    }
72✔
3770

3771
    fn __rsub__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
12✔
3772
        let py = other.py();
12✔
3773
        let Some(other) = coerce_to_observable(other)? else {
12✔
3774
            return Ok(py.NotImplemented().into_bound(py));
2✔
3775
        };
3776
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
8✔
3777
        let other = other.borrow();
8✔
3778
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
8✔
3779
        inner.check_equal_qubits(&other_inner)?;
8✔
3780
        <&SparseObservable as ::std::ops::Sub>::sub(&other_inner, &inner).into_bound_py_any(py)
8✔
3781
    }
12✔
3782

3783
    fn __isub__(slf_: Bound<PySparseObservable>, other: &Bound<PyAny>) -> PyResult<()> {
50✔
3784
        let Some(other) = coerce_to_observable(other)? else {
50✔
3785
            // This is not well behaved - we _should_ return `NotImplemented` to Python space
3786
            // without an exception, but limitations in PyO3 prevent this at the moment.  See
3787
            // https://github.com/PyO3/pyo3/issues/4605.
3788
            return Err(PyTypeError::new_err(format!(
2✔
3789
                "invalid object for in-place subtraction of 'SparseObservable': {}",
2✔
3790
                other.repr()?
2✔
3791
            )));
3792
        };
3793
        let other = other.borrow();
48✔
3794
        let slf_ = slf_.borrow();
48✔
3795
        let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?;
48✔
3796

3797
        if Arc::ptr_eq(&slf_.inner, &other.inner) {
48✔
3798
            // This is not strictly the same thing as `a - a` if `a` contains non-finite
3799
            // floating-point values (`inf - inf` is `NaN`, for example); we don't really have a
3800
            // clear view on what floating-point guarantees we're going to make right now.
3801
            slf_inner.clear();
14✔
3802
            return Ok(());
14✔
3803
        }
34✔
3804

3805
        let other_inner = other.inner.read().map_err(|_| InnerReadError)?;
34✔
3806
        slf_inner.check_equal_qubits(&other_inner)?;
34✔
3807
        slf_inner.sub_assign(&other_inner);
34✔
3808
        Ok(())
34✔
3809
    }
50✔
3810

3811
    fn __pos__(&self) -> PyResult<Self> {
28✔
3812
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
28✔
3813
        Ok(inner.clone().into())
28✔
3814
    }
28✔
3815

3816
    fn __neg__(&self) -> PyResult<Self> {
58✔
3817
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
58✔
3818
        let neg = <&SparseObservable as ::std::ops::Neg>::neg(&inner);
58✔
3819
        Ok(neg.into())
58✔
3820
    }
58✔
3821

3822
    fn __mul__(&self, other: Complex64) -> PyResult<Self> {
230✔
3823
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
230✔
3824
        let mult = <&SparseObservable as ::std::ops::Mul<_>>::mul(&inner, other);
230✔
3825
        Ok(mult.into())
230✔
3826
    }
230✔
3827
    fn __rmul__(&self, other: Complex64) -> PyResult<Self> {
160✔
3828
        self.__mul__(other)
160✔
3829
    }
160✔
3830

3831
    fn __imul__(&mut self, other: Complex64) -> PyResult<()> {
70✔
3832
        let mut inner = self.inner.write().map_err(|_| InnerWriteError)?;
70✔
3833
        inner.mul_assign(other);
70✔
3834
        Ok(())
70✔
3835
    }
70✔
3836

3837
    fn __truediv__(&self, other: Complex64) -> PyResult<Self> {
70✔
3838
        if other.is_zero() {
70✔
3839
            return Err(PyZeroDivisionError::new_err("complex division by zero"));
14✔
3840
        }
56✔
3841
        let inner = self.inner.read().map_err(|_| InnerReadError)?;
56✔
3842
        let div = <&SparseObservable as ::std::ops::Div<_>>::div(&inner, other);
56✔
3843
        Ok(div.into())
56✔
3844
    }
70✔
3845
    fn __itruediv__(&mut self, other: Complex64) -> PyResult<()> {
70✔
3846
        if other.is_zero() {
70✔
3847
            return Err(PyZeroDivisionError::new_err("complex division by zero"));
14✔
3848
        }
56✔
3849
        let mut inner = self.inner.write().map_err(|_| InnerWriteError)?;
56✔
3850
        inner.div_assign(other);
56✔
3851
        Ok(())
56✔
3852
    }
70✔
3853

3854
    fn __xor__(&self, other: &Bound<PyAny>) -> PyResult<Py<PyAny>> {
182✔
3855
        // we cannot just delegate this to ``tensor`` since ``other`` might allow
182✔
3856
        // right-hand-side arithmetic and we have to try deferring to that object,
182✔
3857
        // which is done by returning ``NotImplemented``
182✔
3858
        let py = other.py();
182✔
3859
        let Some(other) = coerce_to_observable(other)? else {
182✔
3860
            return Ok(py.NotImplemented());
4✔
3861
        };
3862

3863
        self.tensor(&other)
176✔
3864
    }
182✔
3865

3866
    fn __rxor__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
8✔
3867
        let py = other.py();
8✔
3868
        let Some(other) = coerce_to_observable(other)? else {
8✔
3869
            return Ok(py.NotImplemented().into_bound(py));
2✔
3870
        };
3871
        self.expand(&other).map(|obj| obj.into_any())
4✔
3872
    }
8✔
3873

3874
    // The documentation for this is inlined into the class-level documentation of
3875
    // `SparseObservable`.
3876
    #[allow(non_snake_case)]
3877
    #[classattr]
3878
    fn BitTerm(py: Python) -> PyResult<Py<PyType>> {
12✔
3879
        BIT_TERM_PY_ENUM
12✔
3880
            .get_or_try_init(py, || make_py_bit_term(py))
12✔
3881
            .map(|obj| obj.clone_ref(py))
12✔
3882
    }
12✔
3883

3884
    // The documentation for this is inlined into the class-level documentation of
3885
    // `SparseObservable`.
3886
    #[allow(non_snake_case)]
3887
    #[classattr]
3888
    fn Term(py: Python) -> Bound<PyType> {
12✔
3889
        py.get_type::<PySparseTerm>()
12✔
3890
    }
12✔
3891
}
3892
impl From<SparseObservable> for PySparseObservable {
3893
    fn from(val: SparseObservable) -> PySparseObservable {
10,782✔
3894
        PySparseObservable {
10,782✔
3895
            inner: Arc::new(RwLock::new(val)),
10,782✔
3896
        }
10,782✔
3897
    }
10,782✔
3898
}
3899
impl<'py> IntoPyObject<'py> for SparseObservable {
3900
    type Target = PySparseObservable;
3901
    type Output = Bound<'py, Self::Target>;
3902
    type Error = PyErr;
3903

3904
    fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
1,770✔
3905
        PySparseObservable::from(self).into_pyobject(py)
1,770✔
3906
    }
1,770✔
3907
}
3908

3909
/// Helper class of `ArrayView` that denotes the slot of the `SparseObservable` we're looking at.
3910
#[derive(Clone, Copy, PartialEq, Eq)]
3911
enum ArraySlot {
3912
    Coeffs,
3913
    BitTerms,
3914
    Indices,
3915
    Boundaries,
3916
}
3917

3918
/// Custom wrapper sequence class to get safe views onto the Rust-space data.  We can't directly
3919
/// expose Python-managed wrapped pointers without introducing some form of runtime exclusion on the
3920
/// ability of `SparseObservable` to re-allocate in place; we can't leave dangling pointers for
3921
/// Python space.
3922
#[pyclass(frozen, sequence)]
6,054✔
3923
struct ArrayView {
3924
    base: Arc<RwLock<SparseObservable>>,
3925
    slot: ArraySlot,
3926
}
3927
#[pymethods]
7,050✔
3928
impl ArrayView {
3929
    fn __repr__(&self, py: Python) -> PyResult<String> {
8✔
3930
        let obs = self.base.read().map_err(|_| InnerReadError)?;
8✔
3931
        let data = match self.slot {
8✔
3932
            // Simple integers look the same in Rust-space debug as Python.
3933
            ArraySlot::Indices => format!("{:?}", obs.indices()),
2✔
3934
            ArraySlot::Boundaries => format!("{:?}", obs.boundaries()),
2✔
3935
            // Complexes don't have a nice repr in Rust, so just delegate the whole load to Python
3936
            // and convert back.
3937
            ArraySlot::Coeffs => PyList::new(py, obs.coeffs())?.repr()?.to_string(),
2✔
3938
            ArraySlot::BitTerms => format!(
2✔
3939
                "[{}]",
2✔
3940
                obs.bit_terms()
2✔
3941
                    .iter()
2✔
3942
                    .map(BitTerm::py_label)
2✔
3943
                    .collect::<Vec<_>>()
2✔
3944
                    .join(", ")
2✔
3945
            ),
2✔
3946
        };
3947
        Ok(format!(
8✔
3948
            "<observable {} view: {}>",
8✔
3949
            match self.slot {
8✔
3950
                ArraySlot::Coeffs => "coeffs",
2✔
3951
                ArraySlot::BitTerms => "bit_terms",
2✔
3952
                ArraySlot::Indices => "indices",
2✔
3953
                ArraySlot::Boundaries => "boundaries",
2✔
3954
            },
3955
            data,
3956
        ))
3957
    }
8✔
3958

3959
    fn __getitem__<'py>(
6,252✔
3960
        &self,
6,252✔
3961
        py: Python<'py>,
6,252✔
3962
        index: PySequenceIndex,
6,252✔
3963
    ) -> PyResult<Bound<'py, PyAny>> {
6,252✔
3964
        // The slightly verbose generic setup here is to allow the type of a scalar return to be
3965
        // different to the type that gets put into the Numpy array, since the `BitTerm` enum can be
3966
        // a direct scalar, but for Numpy, we need it to be a raw `u8`.
3967
        fn get_from_slice<'py, T, S>(
6,252✔
3968
            py: Python<'py>,
6,252✔
3969
            slice: &[T],
6,252✔
3970
            index: PySequenceIndex,
6,252✔
3971
        ) -> PyResult<Bound<'py, PyAny>>
6,252✔
3972
        where
6,252✔
3973
            T: IntoPyObject<'py> + Copy + Into<S>,
6,252✔
3974
            S: ::numpy::Element,
6,252✔
3975
        {
6,252✔
3976
            match index.with_len(slice.len())? {
6,252✔
3977
                SequenceIndex::Int(index) => slice[index].into_bound_py_any(py),
3,856✔
3978
                indices => PyArray1::from_iter(py, indices.iter().map(|index| slice[index].into()))
4,482✔
3979
                    .into_bound_py_any(py),
2,100✔
3980
            }
3981
        }
6,252✔
3982

3983
        let obs = self.base.read().map_err(|_| InnerReadError)?;
6,252✔
3984
        match self.slot {
6,252✔
3985
            ArraySlot::Coeffs => get_from_slice::<_, Complex64>(py, obs.coeffs(), index),
1,326✔
3986
            ArraySlot::BitTerms => get_from_slice::<_, u8>(py, obs.bit_terms(), index),
1,298✔
3987
            ArraySlot::Indices => get_from_slice::<_, u32>(py, obs.indices(), index),
1,452✔
3988
            ArraySlot::Boundaries => get_from_slice::<_, usize>(py, obs.boundaries(), index),
2,176✔
3989
        }
3990
    }
6,252✔
3991

3992
    fn __setitem__(&self, index: PySequenceIndex, values: &Bound<PyAny>) -> PyResult<()> {
304✔
3993
        /// Set values of a slice according to the indexer, using `extract` to retrieve the
3994
        /// Rust-space object from the collection of Python-space values.
3995
        ///
3996
        /// This indirects the Python extraction through an intermediate type to marginally improve
3997
        /// the error messages for things like `BitTerm`, where Python-space extraction might fail
3998
        /// because the user supplied an invalid alphabet letter.
3999
        ///
4000
        /// This allows broadcasting a single item into many locations in a slice (like Numpy), but
4001
        /// otherwise requires that the index and values are the same length (unlike Python's
4002
        /// `list`) because that would change the length.
4003
        fn set_in_slice<'py, T, S>(
304✔
4004
            slice: &mut [T],
304✔
4005
            index: PySequenceIndex<'py>,
304✔
4006
            values: &Bound<'py, PyAny>,
304✔
4007
        ) -> PyResult<()>
304✔
4008
        where
304✔
4009
            T: Copy + TryFrom<S>,
304✔
4010
            S: FromPyObject<'py>,
304✔
4011
            PyErr: From<<T as TryFrom<S>>::Error>,
304✔
4012
        {
304✔
4013
            match index.with_len(slice.len())? {
304✔
4014
                SequenceIndex::Int(index) => {
126✔
4015
                    slice[index] = values.extract::<S>()?.try_into()?;
126✔
4016
                    Ok(())
116✔
4017
                }
4018
                indices => {
178✔
4019
                    if let Ok(value) = values.extract::<S>() {
178✔
4020
                        let value = value.try_into()?;
22✔
4021
                        for index in indices {
68✔
4022
                            slice[index] = value;
48✔
4023
                        }
48✔
4024
                    } else {
4025
                        let values = values
156✔
4026
                            .try_iter()?
156✔
4027
                            .map(|value| value?.extract::<S>()?.try_into().map_err(PyErr::from))
220✔
4028
                            .collect::<PyResult<Vec<_>>>()?;
156✔
4029
                        if indices.len() != values.len() {
156✔
4030
                            return Err(PyValueError::new_err(format!(
4✔
4031
                                "tried to set a slice of length {} with a sequence of length {}",
4✔
4032
                                indices.len(),
4✔
4033
                                values.len(),
4✔
4034
                            )));
4✔
4035
                        }
152✔
4036
                        for (index, value) in indices.into_iter().zip(values) {
202✔
4037
                            slice[index] = value;
202✔
4038
                        }
202✔
4039
                    }
4040
                    Ok(())
172✔
4041
                }
4042
            }
4043
        }
304✔
4044

4045
        let mut obs = self.base.write().map_err(|_| InnerWriteError)?;
304✔
4046
        match self.slot {
304✔
4047
            ArraySlot::Coeffs => set_in_slice::<_, Complex64>(obs.coeffs_mut(), index, values),
196✔
4048
            ArraySlot::BitTerms => set_in_slice::<BitTerm, u8>(obs.bit_terms_mut(), index, values),
90✔
4049
            ArraySlot::Indices => unsafe {
4050
                set_in_slice::<_, u32>(obs.indices_mut(), index, values)
10✔
4051
            },
4052
            ArraySlot::Boundaries => unsafe {
4053
                set_in_slice::<_, usize>(obs.boundaries_mut(), index, values)
8✔
4054
            },
4055
        }
4056
    }
304✔
4057

4058
    fn __len__(&self, _py: Python) -> PyResult<usize> {
214✔
4059
        let obs = self.base.read().map_err(|_| InnerReadError)?;
214✔
4060
        let len = match self.slot {
214✔
4061
            ArraySlot::Coeffs => obs.coeffs().len(),
64✔
4062
            ArraySlot::BitTerms => obs.bit_terms().len(),
58✔
4063
            ArraySlot::Indices => obs.indices().len(),
34✔
4064
            ArraySlot::Boundaries => obs.boundaries().len(),
58✔
4065
        };
4066
        Ok(len)
214✔
4067
    }
214✔
4068

4069
    #[pyo3(signature = (/, dtype=None, copy=None))]
4070
    fn __array__<'py>(
272✔
4071
        &self,
272✔
4072
        py: Python<'py>,
272✔
4073
        dtype: Option<&Bound<'py, PyAny>>,
272✔
4074
        copy: Option<bool>,
272✔
4075
    ) -> PyResult<Bound<'py, PyAny>> {
272✔
4076
        // This method always copies, so we don't leave dangling pointers lying around in Numpy
272✔
4077
        // arrays; it's not enough just to set the `base` of the Numpy array to the
272✔
4078
        // `SparseObservable`, since the `Vec` we're referring to might re-allocate and invalidate
272✔
4079
        // the pointer the Numpy array is wrapping.
272✔
4080
        if !copy.unwrap_or(true) {
272✔
4081
            return Err(PyValueError::new_err(
×
4082
                "cannot produce a safe view onto movable memory",
×
4083
            ));
×
4084
        }
272✔
4085
        let obs = self.base.read().map_err(|_| InnerReadError)?;
272✔
4086
        match self.slot {
272✔
4087
            ArraySlot::Coeffs => cast_array_type(py, PyArray1::from_slice(py, obs.coeffs()), dtype),
222✔
4088
            ArraySlot::Indices => {
4089
                cast_array_type(py, PyArray1::from_slice(py, obs.indices()), dtype)
20✔
4090
            }
4091
            ArraySlot::Boundaries => {
4092
                cast_array_type(py, PyArray1::from_slice(py, obs.boundaries()), dtype)
16✔
4093
            }
4094
            ArraySlot::BitTerms => {
4095
                let bit_terms: &[u8] = ::bytemuck::cast_slice(obs.bit_terms());
14✔
4096
                cast_array_type(py, PyArray1::from_slice(py, bit_terms), dtype)
14✔
4097
            }
4098
        }
4099
    }
272✔
4100
}
4101

4102
/// Use the Numpy Python API to convert a `PyArray` into a dynamically chosen `dtype`, copying only
4103
/// if required.
4104
fn cast_array_type<'py, T>(
272✔
4105
    py: Python<'py>,
272✔
4106
    array: Bound<'py, PyArray1<T>>,
272✔
4107
    dtype: Option<&Bound<'py, PyAny>>,
272✔
4108
) -> PyResult<Bound<'py, PyAny>> {
272✔
4109
    let base_dtype = array.dtype();
272✔
4110
    let dtype = dtype
272✔
4111
        .map(|dtype| PyArrayDescr::new(py, dtype))
272✔
4112
        .unwrap_or_else(|| Ok(base_dtype.clone()))?;
272✔
4113
    if dtype.is_equiv_to(&base_dtype) {
272✔
4114
        return Ok(array.into_any());
268✔
4115
    }
4✔
4116
    PyModule::import(py, intern!(py, "numpy"))?
4✔
4117
        .getattr(intern!(py, "array"))?
4✔
4118
        .call(
4119
            (array,),
4✔
4120
            Some(
4✔
4121
                &[
4✔
4122
                    (intern!(py, "copy"), NUMPY_COPY_ONLY_IF_NEEDED.get_bound(py)),
4✔
4123
                    (intern!(py, "dtype"), dtype.as_any()),
4✔
4124
                ]
4✔
4125
                .into_py_dict(py)?,
4✔
4126
            ),
4127
        )
4128
}
272✔
4129

4130
/// Attempt to coerce an arbitrary Python object to a [PySparseObservable].
4131
///
4132
/// This returns:
4133
///
4134
/// * `Ok(Some(obs))` if the coercion was completely successful.
4135
/// * `Ok(None)` if the input value was just completely the wrong type and no coercion could be
4136
///    attempted.
4137
/// * `Err` if the input was a valid type for coercion, but the coercion failed with a Python
4138
///   exception.
4139
///
4140
/// The purpose of this is for conversion the arithmetic operations, which should return
4141
/// [PyNotImplemented] if the type is not valid for coercion.
4142
fn coerce_to_observable<'py>(
2,116✔
4143
    value: &Bound<'py, PyAny>,
2,116✔
4144
) -> PyResult<Option<Bound<'py, PySparseObservable>>> {
2,116✔
4145
    let py = value.py();
2,116✔
4146
    if let Ok(obs) = value.downcast_exact::<PySparseObservable>() {
2,116✔
4147
        return Ok(Some(obs.clone()));
2,000✔
4148
    }
116✔
4149
    match PySparseObservable::py_new(value, None) {
116✔
4150
        Ok(obs) => Ok(Some(Bound::new(py, obs)?)),
78✔
4151
        Err(e) => {
38✔
4152
            if e.is_instance_of::<PyTypeError>(py) {
38✔
4153
                Ok(None)
26✔
4154
            } else {
4155
                Err(e)
12✔
4156
            }
4157
        }
4158
    }
4159
}
2,116✔
4160
pub fn sparse_observable(m: &Bound<PyModule>) -> PyResult<()> {
12✔
4161
    m.add_class::<PySparseObservable>()?;
12✔
4162
    Ok(())
12✔
4163
}
12✔
4164

4165
#[cfg(test)]
4166
mod test {
4167
    use super::*;
4168

4169
    #[test]
4170
    fn test_error_safe_add_dense_label() {
4171
        let base = SparseObservable::identity(5);
4172
        let mut modified = base.clone();
4173
        assert!(matches!(
4174
            modified.add_dense_label("IIZ$X", Complex64::new(1.0, 0.0)),
4175
            Err(LabelError::OutsideAlphabet)
4176
        ));
4177
        // `modified` should have been left in a valid state.
4178
        assert_eq!(base, modified);
4179
    }
4180
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc