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

zeFresk / ProPauli / 18403163920

10 Oct 2025 10:00AM UTC coverage: 75.354% (-0.05%) from 75.403%
18403163920

Pull #19

github

web-flow
Merge f6fdc1eae into 29fb7d4f3
Pull Request #19: Parallel version

1009 of 1884 branches covered (53.56%)

Branch coverage included in aggregate %.

222 of 227 new or added lines in 8 files covered. (97.8%)

22 existing lines in 2 files now uncovered.

2241 of 2429 relevant lines covered (92.26%)

15962.84 hits per line

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

63.98
/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 "symbolic/coefficient.hpp"
18
#include "merge.hpp"
19
#include "policy.hpp"
20

21
#include <cstring>
22
#include <initializer_list>
23
#include <iostream>
24
#include <ostream>
25
#include <stdexcept>
26
#include <string_view>
27
#include <type_traits>
28
#include <unordered_map>
29

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

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

58
        /**
59
         * @brief Constructs an observable from a list of Pauli strings.
60
         * @param pauli_string_list An initializer list of Pauli strings. Each will have a coefficient of 1.
61
         * @snippet tests/snippets/observable.cpp observable_from_string_list
62
         */
63
        Observable(std::initializer_list<std::string_view> pauli_string_list) : paulis_{ pauli_string_list } { check_invariant(); }
80✔
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(); }
18✔
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 } {
7✔
81
                check_invariant();
7✔
82
        }
7✔
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
        template <typename ExecutionPolicy = DefaultExecutionPolicy>
97
        void apply_pauli(Pauli_gates g, unsigned qubit, ExecutionPolicy&& policy = ExecutionPolicy{}) {
60✔
98
                check_qubit(qubit);
60✔
99
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
60✔
100
                Policy_t::apply_pauli(paulis_, g, qubit);
60✔
101
        }
60✔
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
        template <typename ExecutionPolicy = DefaultExecutionPolicy>
110
        void apply_clifford(Clifford_Gates_1Q g, unsigned qubit, ExecutionPolicy&& policy = ExecutionPolicy{}) {
122✔
111
                check_qubit(qubit);
122✔
112
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
122✔
113
                Policy_t::apply_clifford(paulis_, g, qubit);
122✔
114
        }
122✔
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
        template <typename ExecutionPolicy = DefaultExecutionPolicy>
124
        void apply_unital_noise(UnitalNoise n, unsigned qubit, T p, ExecutionPolicy&& policy = ExecutionPolicy{}) {
72✔
125
                check_qubit(qubit);
72✔
126
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
72✔
127
                Policy_t::apply_unital_noise(paulis_, n, qubit, p);
72✔
128
        }
72✔
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
        template <typename ExecutionPolicy = DefaultExecutionPolicy>
137
        void apply_cx(unsigned qubit_control, unsigned qubit_target, ExecutionPolicy&& policy = ExecutionPolicy{}) {
133✔
138
                check_qubit(qubit_control);
133✔
139
                check_qubit(qubit_target);
133✔
140
                if (qubit_control == qubit_target) {
133!
141
                        throw std::invalid_argument("cx gate target must be != from control.");
1✔
142
                }
1✔
143

144
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
132✔
145
                Policy_t::apply_cx(paulis_, qubit_control, qubit_target);
132✔
146
        }
132✔
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
        template <typename ExecutionPolicy = DefaultExecutionPolicy>
158
        void apply_rz(unsigned qubit, T theta, ExecutionPolicy&& policy = ExecutionPolicy{}) {
161✔
159
                check_qubit(qubit);
161✔
160
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
161✔
161
                Policy_t::apply_rz(paulis_, qubit, theta);
161✔
162
        }
161✔
163

164
        /**
165
         * @brief Applies an amplitude damping noise channel.
166
         * @param qubit The index of the qubit to apply the channel to.
167
         * @param pn The noise probability parameter.
168
         * @note This can be a **splitting** operation. If a term has a Z operator on the
169
         * target qubit, it will be split into two. If it has X or Y, its coefficient is
170
         * simply scaled. If it has I, there is no effect.
171
         */
172
        template <typename ExecutionPolicy = DefaultExecutionPolicy>
173
        void apply_amplitude_damping(unsigned qubit, T pn, ExecutionPolicy&& policy = ExecutionPolicy{}) {
69✔
174
                check_qubit(qubit);
69✔
175
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
69✔
176
                Policy_t::apply_amplitude_damping(paulis_, qubit, pn);
69✔
177
        }
69✔
178
        /** @} */
179

180
        /**
181
         * @brief Calculates the expectation value of the observable.
182
         * @return The expectation value.
183
         *
184
         * The expectation value is the sum of coefficients of all Pauli terms composed
185
         * entirely of I and Z operators. These are the terms that are diagonal in the
186
         * computational basis.
187
         */
188
        template <typename ExecutionPolicy = DefaultExecutionPolicy>
189
        T expectation_value(ExecutionPolicy&& policy = ExecutionPolicy{}) const {
167✔
190
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
167✔
191
                return Policy_t::expectation_value(paulis_);
167✔
192
        }
167✔
193

194
        /** @name Container Interface
195
         * Methods to interact with the Observable like a container.
196
         * @{
197
         */
198

199
        /**
200
         * @brief Accesses a specific Pauli term via a non-owning view.
201
         * @return A lightweight view of the term. Modifying it modifies the observable directly.
202
         */
203
        auto operator[](std::size_t idx) { return paulis_[idx]; }
274✔
204
        /** @brief Accesses a specific Pauli term via a read-only non-owning view. */
205
        auto operator[](std::size_t idx) const { return paulis_[idx]; }
135✔
206

207
        /** @brief Returns an iterator to the beginning of the terms. */
208
        decltype(auto) begin() { return paulis_.begin(); }
42✔
209
        /** @brief Returns a const iterator to the beginning of the terms. */
UNCOV
210
        decltype(auto) begin() const { return paulis_.begin(); }
×
211
        /** @brief Returns a const iterator to the beginning of the terms. */
212
        decltype(auto) cbegin() const { return paulis_.cbegin(); }
23✔
213
        /** @brief Returns an iterator to the end of the terms. */
214
        decltype(auto) end() { return paulis_.end(); }
84✔
215
        /** @brief Returns a const iterator to the end of the terms. */
UNCOV
216
        decltype(auto) end() const { return paulis_.end(); }
×
217
        /** @brief Returns a const iterator to the end of the terms. */
218
        decltype(auto) cend() const { return paulis_.cend(); }
25✔
219

220
        /** @brief Gets the number of Pauli terms in the observable. */
221
        std::size_t size() const { return paulis_.nb_terms(); }
1,225✔
222
        /** @brief Gets the number of qubits in the observable. */
223
        std::size_t nb_qubits() const { return paulis_.nb_qubits(); }
910✔
224

225
        /**
226
         * @brief Creates an owning copy of a term at a specific index.
227
         * @param idx The index of the term to copy.
228
         * @return An owning `PauliTerm<T>` object.
229
         * @note This is a potentially expensive operation as it involves constructing a new object.
230
         */
231
        PauliTerm<T> copy_term(std::size_t idx) const {
134✔
232
                if (idx >= size()) {
134!
UNCOV
233
                        throw std::invalid_argument("Index out of range");
×
UNCOV
234
                }
×
235
                auto nopt = (*this)[idx];
134✔
236
                return static_cast<PauliTerm<T>>(nopt);
134✔
237
        }
134✔
238
        /** @} */
239

240
        /**
241
         * @brief Merges terms with identical Pauli strings by summing their coefficients.
242
         * @return The number of terms remaining after the merge.
243
         *
244
         * This is a crucial optimization for reducing the complexity of the simulation.
245
         * It calls a high-performance, in-place merging algorithm.
246
         */
247
        template <typename ExecutionPolicy = DefaultExecutionPolicy>
248
        std::size_t merge(ExecutionPolicy&& policy = ExecutionPolicy{}) {
115✔
249
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
115✔
250
                using Merger_t = Policy_t:: template Merger<T>;
115✔
251
                std::get<Merger_t>(merger_)(paulis_);
115✔
252
                return paulis_.nb_terms();
115✔
253
        }
115✔
254

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

276
        friend bool operator==(Observable const& lhs, Observable const& rhs) {
11✔
277
                return lhs.size() == rhs.size() && lhs.paulis_ == rhs.paulis_;
11!
278
        }
11✔
279

280
        bool constexpr is_symbolic() const { return Symbolic<T>; }
281

282
        template <typename U = T, std::enable_if_t<std::is_same_v<U, T> && Symbolic<U>, bool> = true>
283
        void simplify(std::unordered_map<std::string, typename U::Underlying_t> const& variables = {}) {
3✔
284
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
15✔
285
                        paulis_[i].simplify(variables);
12✔
286
                }
12✔
287
        }
3✔
288

289
    private:
290
        PauliTermContainer<T> paulis_;
291
        RuntimeMerger<T> merger_;
292

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

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

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

328
                os << obs.copy_term(i);
52✔
329
                first = false;
52✔
330
        }
52✔
331
        return os;
19✔
332
}
19✔
333

334
#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