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

zeFresk / ProPauli / 17306384951

28 Aug 2025 07:51PM UTC coverage: 89.106% (-3.9%) from 92.994%
17306384951

Pull #8

github

web-flow
Merge 40d2e22c4 into a58930335
Pull Request #8: Optimize the library

284 of 384 branches covered (73.96%)

Branch coverage included in aggregate %.

559 of 613 new or added lines in 11 files covered. (91.19%)

6 existing lines in 2 files now uncovered.

1041 of 1103 relevant lines covered (94.38%)

33917.1 hits per line

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

79.72
/include/observable.hpp
1
#ifndef PP_OBSERVABLE_HPP
2
#define PP_OBSERVABLE_HPP
3

4
/**
5
 * @file observable.hpp
6
 * @brief Defines the main Observable class used in the simulation.
7
 *
8
 * This file contains the `Observable` class, which represents a quantum observable
9
 * as a linear combination of Pauli strings. It serves as the primary data structure
10
 * that is evolved backward in time under the action of quantum gates in the
11
 * Heisenberg picture.
12
 */
13

14
#include "pauli.hpp"
15
#include "pauli_term.hpp"
16
#include "pauli_term_container.hpp"
17
#include "merge.hpp"
18

19
#include <cstring>
20
#include <initializer_list>
21
#include <iostream>
22
#include <ostream>
23
#include <stdexcept>
24
#include <string_view>
25
#include <type_traits>
26

27
/**
28
 * @brief Represents a quantum observable as a linear combination of Pauli strings.
29
 * @tparam T The numeric type for the coefficients (e.g., float, double).
30
 *
31
 * An `Observable` is a sum of Pauli terms. This class is the central data structure
32
 * in the Pauli back-propagation simulation. It is implemented using a memory-efficient
33
 * `PauliTermContainer` to store its terms. Quantum gates are applied to this object,
34
 * evolving it backward in the Heisenberg picture.
35
 */
36
template <typename T = coeff_t>
37
class Observable {
38
    public:
39
        /** @name Constructors
40
         * Ways to construct an Observable.
41
         * @{
42
         */
43

44
        /**
45
         * @brief Constructs an observable from a single Pauli string.
46
         * @param pauli_string A string representing the Pauli operators (e.g., "IXYZ").
47
         * @param coeff The coefficient of this Pauli term.
48
         * @snippet tests/snippets/observable.cpp observable_from_string
49
         */
50
        Observable(std::string_view pauli_string,
51
                   typename std::enable_if_t<std::is_convertible_v<T, coeff_t>, T> coeff = T{ 1 })
52
                : paulis_{ PauliTerm<T>(pauli_string, coeff) } {
13✔
53
                check_invariant();
13✔
54
        }
13✔
55

56
        /**
57
         * @brief Constructs an observable from a list of Pauli strings.
58
         * @param pauli_string_list An initializer list of Pauli strings. Each will have a coefficient of 1.
59
         * @snippet tests/snippets/observable.cpp observable_from_string_list
60
         */
61
        Observable(std::initializer_list<std::string_view> pauli_string_list) : paulis_{ pauli_string_list } {
45✔
62
                check_invariant();
45✔
63
        }
45✔
64

65
        /**
66
         * @brief Constructs an observable from a list of PauliTerm objects.
67
         * @param paulis_list An initializer list of `PauliTerm` objects.
68
         * @snippet tests/snippets/observable.cpp observable_from_pauli_terms
69
         */
70
        Observable(std::initializer_list<PauliTerm<T>> paulis_list) : paulis_{ paulis_list } { check_invariant(); }
11✔
71

72
        /**
73
         * @brief Constructs an observable from a range of PauliTerm-like objects.
74
         * @tparam Iter An iterator type.
75
         * @param begin An iterator to the beginning of the range.
76
         * @param end An iterator to the end of the range.
77
         * @snippet tests/snippets/observable.cpp observable_from_iterators
78
         */
79
        template <PauliTermIterator Iter>
80
        Observable(Iter&& begin, Iter&& end) : paulis_{ begin, end } {
3✔
81
                check_invariant();
3✔
82
        }
3✔
83
        /** @} */
84

85
        /** @name Gate Application
86
         * Methods for applying quantum gates to the observable.
87
         * @{
88
         */
89

90
        /**
91
         * @brief Applies a single-qubit Pauli gate to the observable.
92
         * @param g The Pauli gate to apply.
93
         * @param qubit The index of the qubit to apply the gate to.
94
         * @pre `qubit` must be a valid index less than `nb_qubits()`.
95
         */
96
        void apply_pauli(Pauli_gates g, unsigned qubit) {
38✔
97
                check_qubit(qubit);
38✔
98
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
95✔
99
                        paulis_[i].apply_pauli(g, qubit);
57✔
100
                }
57✔
101
        }
38✔
102

103
        /**
104
         * @brief Applies a single-qubit Clifford gate to the observable.
105
         * @param g The Clifford gate to apply (e.g., Hadamard).
106
         * @param qubit The index of the qubit to apply the gate to.
107
         * @pre `qubit` must be a valid index less than `nb_qubits()`.
108
         */
109
        void apply_clifford(Clifford_Gates_1Q g, unsigned qubit) {
98✔
110
                check_qubit(qubit);
98✔
111
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
320✔
112
                        paulis_[i].apply_clifford(g, qubit);
222✔
113
                }
222✔
114
        }
98✔
115

116
        /**
117
         * @brief Applies a single-qubit unital noise channel to the observable.
118
         * @param n The type of unital noise (e.g., Depolarizing, Dephasing).
119
         * @param qubit The index of the qubit to apply the noise to.
120
         * @param p The noise probability parameter.
121
         * @pre `qubit` must be a valid index less than `nb_qubits()`.
122
         */
123
        void apply_unital_noise(UnitalNoise n, unsigned qubit, T p) {
47✔
124
                check_qubit(qubit);
47✔
125
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
137✔
126
                        paulis_[i].apply_unital_noise(n, qubit, p);
90✔
127
                }
90✔
128
        }
47✔
129

130
        /**
131
         * @brief Applies a CNOT (CX) gate to the observable.
132
         * @param qubit_control The index of the control qubit.
133
         * @param qubit_target The index of the target qubit.
134
         * @pre `qubit_control` and `qubit_target` must be valid and distinct qubit indices.
135
         */
136
        void apply_cx(unsigned qubit_control, unsigned qubit_target) {
118✔
137
                check_qubit(qubit_control);
118✔
138
                check_qubit(qubit_target);
118✔
139
                if (qubit_control == qubit_target) {
118✔
140
                        throw std::invalid_argument("cx gate target must be != from control.");
1✔
141
                }
1✔
142

143
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
267✔
144
                        paulis_[i].apply_cx(qubit_control, qubit_target);
150✔
145
                }
150✔
146
        }
117✔
147

148
        /**
149
         * @brief Applies a single-qubit Rz rotation gate to the observable.
150
         * @param qubit The index of the qubit to apply the rotation to.
151
         * @param theta The rotation angle in radians.
152
         * @note This is a **splitting** operation. Applying an Rz gate to a Pauli term
153
         * containing an X or Y operator on the target qubit will result in two
154
         * output Pauli terms. This can increase the size of the observable, often
155
         * necessitating a subsequent `merge()` or `truncate()` call.
156
         */
157
        void apply_rz(unsigned qubit, T theta) {
103✔
158
                check_qubit(qubit);
103✔
159
                const auto nb_terms = paulis_.nb_terms();
103✔
160
                for (std::size_t i = 0; i < nb_terms; ++i) {
549✔
161
                        auto p = paulis_[i];
446✔
162
                        if (!p.get_pauli(qubit).commutes_with(p_z)) {
446✔
163
                                auto new_path = paulis_.duplicate_pauliterm(i);
340✔
164
                                paulis_[i].apply_rz(qubit, theta, new_path);
340✔
165
                        }
340✔
166
                }
446✔
167
        }
103✔
168

169
        /**
170
         * @brief Applies an amplitude damping noise channel.
171
         * @param qubit The index of the qubit to apply the channel to.
172
         * @param pn The noise probability parameter.
173
         * @note This can be a **splitting** operation. If a term has a Z operator on the
174
         * target qubit, it will be split into two. If it has X or Y, its coefficient is
175
         * simply scaled. If it has I, there is no effect.
176
         */
177
        void apply_amplitude_damping(unsigned qubit, T pn) {
45✔
178
                check_qubit(qubit);
45✔
179
                const auto nb_terms = paulis_.nb_terms();
45✔
180
                for (std::size_t i = 0; i < nb_terms; ++i) {
123✔
181
                        auto p = paulis_[i];
78✔
182
                        if (p.get_pauli(qubit) == p_z) {
78✔
183
                                auto new_path = paulis_.duplicate_pauliterm(i); // invalidates p
58✔
184
                                paulis_[i].apply_amplitude_damping_z(qubit, pn, new_path);
58✔
185
                        } else if (p.get_pauli(qubit) == p_x || p.get_pauli(qubit) == p_y) {
58✔
186
                                p.apply_amplitude_damping_xy(qubit, pn);
7✔
187
                        }
7✔
188
                }
78✔
189
        }
45✔
190
        /** @} */
191

192
        /**
193
         * @brief Calculates the expectation value of the observable.
194
         * @return The expectation value.
195
         *
196
         * The expectation value is the sum of coefficients of all Pauli terms composed
197
         * entirely of I and Z operators. These are the terms that are diagonal in the
198
         * computational basis.
199
         */
200
        T expectation_value() const {
76✔
201
                T ret = 0;
76✔
202
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
276✔
203
                        ret += paulis_[i].expectation_value();
200✔
204
                }
200✔
205
                return ret;
76✔
206
        }
76✔
207

208
        /** @name Container Interface
209
         * Methods to interact with the Observable like a container.
210
         * @{
211
         */
212

213
        /**
214
         * @brief Accesses a specific Pauli term via a non-owning view.
215
         * @return A lightweight view of the term. Modifying it modifies the observable directly.
216
         */
217
        auto operator[](std::size_t idx) { return paulis_[idx]; }
141✔
218
        /** @brief Accesses a specific Pauli term via a read-only non-owning view. */
219
        auto operator[](std::size_t idx) const { return paulis_[idx]; }
14✔
220

221
        /** @brief Returns an iterator to the beginning of the terms. */
222
        decltype(auto) begin() { return paulis_.begin(); }
14✔
223
        /** @brief Returns a const iterator to the beginning of the terms. */
UNCOV
224
        decltype(auto) begin() const { return paulis_.begin(); }
×
225
        /** @brief Returns a const iterator to the beginning of the terms. */
226
        decltype(auto) cbegin() const { return paulis_.cbegin(); }
11✔
227
        /** @brief Returns an iterator to the end of the terms. */
228
        decltype(auto) end() { return paulis_.end(); }
28✔
229
        /** @brief Returns a const iterator to the end of the terms. */
UNCOV
230
        decltype(auto) end() const { return paulis_.end(); }
×
231
        /** @brief Returns a const iterator to the end of the terms. */
232
        decltype(auto) cend() const { return paulis_.cend(); }
12✔
233

234
        /** @brief Gets the number of Pauli terms in the observable. */
235
        std::size_t size() const { return paulis_.nb_terms(); }
900✔
236
        /** @brief Gets the number of qubits in the observable. */
237
        std::size_t nb_qubits() const { return paulis_.nb_qubits(); }
668✔
238

239
        /**
240
         * @brief Creates an owning copy of a term at a specific index.
241
         * @param idx The index of the term to copy.
242
         * @return An owning `PauliTerm<T>` object.
243
         * @note This is a potentially expensive operation as it involves constructing a new object.
244
         */
245
        PauliTerm<T> copy_term(std::size_t idx) const {
13✔
246
                if (idx >= size()) {
13!
NEW
247
                        throw std::invalid_argument("Index out of range");
×
UNCOV
248
                }
×
249
                auto nopt = (*this)[idx];
13✔
250
                return static_cast<PauliTerm<T>>(nopt);
13✔
251
        }
13✔
252
        /** @} */
253

254
        /**
255
         * @brief Merges terms with identical Pauli strings by summing their coefficients.
256
         * @return The number of terms remaining after the merge.
257
         *
258
         * This is a crucial optimization for reducing the complexity of the simulation.
259
         * It calls a high-performance, in-place merging algorithm.
260
         */
261
        std::size_t merge() {
97✔
262
                merge_inplace_noalloc(paulis_);
97✔
263
                return paulis_.nb_terms();
97✔
264
        }
97✔
265

266
        /**
267
         * @brief Truncates the observable based on a given truncation strategy.
268
         * @tparam TruncatorImpl The type of the truncator object, which must satisfy the Truncator interface.
269
         * @param truncator The truncator object that defines the truncation logic.
270
         * @return The number of Pauli terms removed.
271
         * @see Truncator
272
         */
273
        template <typename TruncatorImpl>
274
        std::size_t truncate(TruncatorImpl&& truncator) {
77✔
275
                auto ret = truncator.truncate(paulis_);
77✔
276
                if (paulis_.nb_terms() == 0) {
77!
277
                        const auto warn_env = std::getenv("WARN_EMPTY_TREE");
1✔
278
                        if (warn_env == nullptr || strcmp(warn_env, "0") != 0) {
1!
279
                                std::cerr
1✔
280
                                        << "[ProPauli] Warning: truncation lead to empty tree. Disable this warning by setting `WARN_EMPTY_TREE=0`, if this is intended."
1✔
281
                                        << std::endl;
1✔
282
                        }
1✔
283
                }
1✔
284
                return ret;
77✔
285
        }
77✔
286

287
        friend bool operator==(Observable const& lhs, Observable const& rhs) {
11✔
288
                return lhs.size() == rhs.size() && lhs.paulis_ == rhs.paulis_;
11!
289
        }
11✔
290

291
    private:
292
        PauliTermContainer<T> paulis_;
293

294
        void check_invariant() const {
68✔
295
                if (paulis_.nb_terms() == 0) {
68!
UNCOV
296
                        throw std::invalid_argument("Can't have empty observable (no terms)");
×
UNCOV
297
                }
×
298
                const auto nb_qubits = this->nb_qubits();
68✔
299
                if (nb_qubits == 0) {
68!
300
                        throw std::invalid_argument("0 qubit observable not allowed.");
1✔
301
                }
1✔
302
        }
68✔
303

304
        void check_qubit(unsigned qubit) const {
566✔
305
                if (qubit >= nb_qubits()) {
566✔
306
                        throw std::invalid_argument("Qubit index out of range.");
7✔
307
                }
7✔
308
        }
566✔
309
};
310

311
/**
312
 * @brief Prints the observable to an output stream.
313
 * @relates Observable
314
 * @note This can be an expensive operation for large observables.
315
 */
316
template <typename T>
317
std::ostream& operator<<(std::ostream& os, Observable<T> const& obs) {
3✔
318
        bool first = true;
3✔
319
        for (std::size_t i = 0; i < obs.size(); ++i) {
8!
320
                if (!first) {
5!
321
                        os << " ";
2✔
322
                }
2✔
323
                // copy_term is needed because the stream operator is defined for owning PauliTerm
324
                os << obs.copy_term(i);
5✔
325
                first = false;
5✔
326
        }
5✔
327
        return os;
3✔
328
}
3✔
329

330
#endif
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

© 2026 Coveralls, Inc