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

zeFresk / ProPauli / 18416695007

10 Oct 2025 07:39PM UTC coverage: 77.713% (+2.3%) from 75.403%
18416695007

push

github

web-flow
Update version

1095 of 1962 branches covered (55.81%)

Branch coverage included in aggregate %.

2371 of 2498 relevant lines covered (94.92%)

26793.86 hits per line

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

67.31
/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) } {
50✔
55
                check_invariant();
50✔
56
        }
50✔
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(); }
140✔
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(); }
21✔
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 <IsNotVariant ExecutionPolicy = DefaultExecutionPolicy>
97
        void apply_pauli(Pauli_gates g, unsigned qubit, ExecutionPolicy&& policy = ExecutionPolicy{}) {
145✔
98
                check_qubit(qubit);
145✔
99
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
145✔
100
                Policy_t::apply_pauli(paulis_, g, qubit);
145✔
101
        }
145✔
102

103
        template <IsVariant DynamicPolicy>
104
        void apply_pauli(Pauli_gates g, unsigned qubit, DynamicPolicy&& rpol) {
21✔
105
                return std::visit([&, this](auto const& pol) { return this->apply_pauli(g, qubit, pol); }, rpol);
21✔
106
        }
21✔
107

108
        /**
109
         * @brief Applies a single-qubit Clifford gate to the observable.
110
         * @param g The Clifford gate to apply (e.g., Hadamard).
111
         * @param qubit The index of the qubit to apply the gate to.
112
         * @pre `qubit` must be a valid index less than `nb_qubits()`.
113
         */
114
        template <IsNotVariant ExecutionPolicy = DefaultExecutionPolicy>
115
        void apply_clifford(Clifford_Gates_1Q g, unsigned qubit, ExecutionPolicy&& policy = ExecutionPolicy{}) {
395✔
116
                check_qubit(qubit);
395✔
117
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
395✔
118
                Policy_t::apply_clifford(paulis_, g, qubit);
395✔
119
        }
395✔
120

121
        template <IsVariant DynamicPolicy>
122
        void apply_clifford(Clifford_Gates_1Q g, unsigned qubit, DynamicPolicy&& rpol) {
5✔
123
                return std::visit([&, this](auto const& pol) { return this->apply_clifford(g, qubit, pol); }, rpol);
5✔
124
        }
5✔
125

126
        /**
127
         * @brief Applies a single-qubit unital noise channel to the observable.
128
         * @param n The type of unital noise (e.g., Depolarizing, Dephasing).
129
         * @param qubit The index of the qubit to apply the noise to.
130
         * @param p The noise probability parameter.
131
         * @pre `qubit` must be a valid index less than `nb_qubits()`.
132
         */
133
        template <IsNotVariant ExecutionPolicy = DefaultExecutionPolicy>
134
        void apply_unital_noise(UnitalNoise n, unsigned qubit, T p, ExecutionPolicy&& policy = ExecutionPolicy{}) {
132✔
135
                check_qubit(qubit);
132✔
136
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
132✔
137
                Policy_t::apply_unital_noise(paulis_, n, qubit, p);
132✔
138
        }
132✔
139

140
        template <IsVariant DynamicPolicy>
141
        void apply_unital_noise(UnitalNoise n, unsigned qubit, T p, DynamicPolicy&& rpol) {
16✔
142
                return std::visit([&, this](auto const& pol) { return this->apply_unital_noise(n, qubit, p, pol); }, rpol);
16✔
143
        }
16✔
144

145
        /**
146
         * @brief Applies a CNOT (CX) gate to the observable.
147
         * @param qubit_control The index of the control qubit.
148
         * @param qubit_target The index of the target qubit.
149
         * @pre `qubit_control` and `qubit_target` must be valid and distinct qubit indices.
150
         */
151
        template <IsNotVariant ExecutionPolicy = DefaultExecutionPolicy>
152
        void apply_cx(unsigned qubit_control, unsigned qubit_target, ExecutionPolicy&& policy = ExecutionPolicy{}) {
266✔
153
                check_qubit(qubit_control);
266✔
154
                check_qubit(qubit_target);
266✔
155
                if (qubit_control == qubit_target) {
266!
156
                        throw std::invalid_argument("cx gate target must be != from control.");
2✔
157
                }
2✔
158

159
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
264✔
160
                Policy_t::apply_cx(paulis_, qubit_control, qubit_target);
264✔
161
        }
264✔
162

163
        template <IsVariant DynamicPolicy>
164
        void apply_cx(unsigned qubit_control, unsigned qubit_target, DynamicPolicy&& rpol) {
15✔
165
                return std::visit([&, this](auto const& pol) { return this->apply_cx(qubit_control, qubit_target, pol); }, rpol);
15✔
166
        }
15✔
167

168
        /**
169
         * @brief Applies a single-qubit Rz rotation gate to the observable.
170
         * @param qubit The index of the qubit to apply the rotation to.
171
         * @param theta The rotation angle in radians.
172
         * @note This is a **splitting** operation. Applying an Rz gate to a Pauli term
173
         * containing an X or Y operator on the target qubit will result in two
174
         * output Pauli terms. This can increase the size of the observable, often
175
         * necessitating a subsequent `merge()` or `truncate()` call.
176
         */
177
        template <IsNotVariant ExecutionPolicy = DefaultExecutionPolicy>
178
        void apply_rz(unsigned qubit, T theta, ExecutionPolicy&& policy = ExecutionPolicy{}) {
373✔
179
                check_qubit(qubit);
373✔
180
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
373✔
181
                Policy_t::apply_rz(paulis_, qubit, theta);
373✔
182
        }
373✔
183

184
        template <IsVariant DynamicPolicy>
185
        void apply_rz(unsigned qubit, T theta, DynamicPolicy&& rpol) {
36✔
186
                return std::visit([&, this](auto const& pol) { return this->apply_rz(qubit, theta, pol); }, rpol);
36✔
187
        }
36✔
188

189
        /**
190
         * @brief Applies an amplitude damping noise channel.
191
         * @param qubit The index of the qubit to apply the channel to.
192
         * @param pn The noise probability parameter.
193
         * @note This can be a **splitting** operation. If a term has a Z operator on the
194
         * target qubit, it will be split into two. If it has X or Y, its coefficient is
195
         * simply scaled. If it has I, there is no effect.
196
         */
197
        template <IsNotVariant ExecutionPolicy = DefaultExecutionPolicy>
198
        void apply_amplitude_damping(unsigned qubit, T pn, ExecutionPolicy&& policy = ExecutionPolicy{}) {
120✔
199
                check_qubit(qubit);
120✔
200
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
120✔
201
                Policy_t::apply_amplitude_damping(paulis_, qubit, pn);
120✔
202
        }
120✔
203

204
        template <IsVariant DynamicPolicy>
205
        void apply_amplitude_damping(unsigned qubit, T pn, DynamicPolicy&& rpol) {
13✔
206
                return std::visit([&, this](auto const& pol) { return this->apply_amplitude_damping(qubit, pn, pol); }, rpol);
13✔
207
        }
13✔
208
        /** @} */
209

210
        /**
211
         * @brief Calculates the expectation value of the observable.
212
         * @return The expectation value.
213
         *
214
         * The expectation value is the sum of coefficients of all Pauli terms composed
215
         * entirely of I and Z operators. These are the terms that are diagonal in the
216
         * computational basis.
217
         */
218
        template <IsNotVariant ExecutionPolicy = DefaultExecutionPolicy>
219
        T expectation_value(ExecutionPolicy&& policy = ExecutionPolicy{}) const {
258✔
220
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
258✔
221
                return Policy_t::expectation_value(paulis_);
258✔
222
        }
258✔
223

224
        template <IsVariant DynamicPolicy>
225
        T expectation_value(DynamicPolicy&& rpol) const {
62✔
226
                return std::visit([this](auto const& pol) { return this->expectation_value(pol); }, rpol);
62✔
227
        }
62✔
228

229
        /** @name Container Interface
230
         * Methods to interact with the Observable like a container.
231
         * @{
232
         */
233

234
        /**
235
         * @brief Accesses a specific Pauli term via a non-owning view.
236
         * @return A lightweight view of the term. Modifying it modifies the observable directly.
237
         */
238
        auto operator[](std::size_t idx) { return paulis_[idx]; }
404✔
239
        /** @brief Accesses a specific Pauli term via a read-only non-owning view. */
240
        auto operator[](std::size_t idx) const { return paulis_[idx]; }
315✔
241

242
        /** @brief Returns an iterator to the beginning of the terms. */
243
        decltype(auto) begin() { return paulis_.begin(); }
56✔
244
        /** @brief Returns a const iterator to the beginning of the terms. */
245
        decltype(auto) begin() const { return paulis_.begin(); }
×
246
        /** @brief Returns a const iterator to the beginning of the terms. */
247
        decltype(auto) cbegin() const { return paulis_.cbegin(); }
34✔
248
        /** @brief Returns an iterator to the end of the terms. */
249
        decltype(auto) end() { return paulis_.end(); }
112✔
250
        /** @brief Returns a const iterator to the end of the terms. */
251
        decltype(auto) end() const { return paulis_.end(); }
×
252
        /** @brief Returns a const iterator to the end of the terms. */
253
        decltype(auto) cend() const { return paulis_.cend(); }
37✔
254

255
        /** @brief Gets the number of Pauli terms in the observable. */
256
        std::size_t size() const { return paulis_.nb_terms(); }
3,190✔
257
        /** @brief Gets the number of qubits in the observable. */
258
        std::size_t nb_qubits() const { return paulis_.nb_qubits(); }
2,002✔
259

260
        /**
261
         * @brief Creates an owning copy of a term at a specific index.
262
         * @param idx The index of the term to copy.
263
         * @return An owning `PauliTerm<T>` object.
264
         * @note This is a potentially expensive operation as it involves constructing a new object.
265
         */
266
        PauliTerm<T> copy_term(std::size_t idx) const {
314✔
267
                if (idx >= size()) {
314!
268
                        throw std::invalid_argument("Index out of range");
×
269
                }
×
270
                auto nopt = (*this)[idx];
314✔
271
                return static_cast<PauliTerm<T>>(nopt);
314✔
272
        }
314✔
273
        /** @} */
274

275
        /**
276
         * @brief Merges terms with identical Pauli strings by summing their coefficients.
277
         * @return The number of terms remaining after the merge.
278
         *
279
         * This is a crucial optimization for reducing the complexity of the simulation.
280
         * It calls a high-performance, in-place merging algorithm.
281
         */
282
        template <IsNotVariant ExecutionPolicy = DefaultExecutionPolicy>
283
        std::size_t merge(ExecutionPolicy&& policy = ExecutionPolicy{}) {
285✔
284
                using Policy_t = std::remove_cvref_t<decltype(policy)>;
285✔
285
                using Merger_t = Policy_t::template Merger<T>;
285✔
286
                std::get<Merger_t>(merger_)(paulis_);
285✔
287
                return paulis_.nb_terms();
285✔
288
        }
285✔
289

290
        template <IsVariant DynamicPolicy>
291
        auto merge(DynamicPolicy&& rpol) {
38✔
292
                return std::visit([this](auto const& pol) { return this->merge(pol); }, rpol);
38✔
293
        }
38✔
294

295
        /**
296
         * @brief Truncates the observable based on a given truncation strategy.
297
         * @tparam TruncatorImpl The type of the truncator object, which must satisfy the Truncator interface.
298
         * @param truncator The truncator object that defines the truncation logic.
299
         * @return The number of Pauli terms removed.
300
         * @see Truncator
301
         */
302
        template <typename TruncatorImpl>
303
        std::size_t truncate(TruncatorImpl&& truncator) {
240✔
304
                auto ret = truncator.truncate(paulis_);
240✔
305
                if (paulis_.nb_terms() == 0) {
240!
306
                        const auto warn_env = std::getenv("WARN_EMPTY_TREE");
1✔
307
                        if (warn_env == nullptr || strcmp(warn_env, "0") != 0) {
1!
308
                                std::cerr
1✔
309
                                        << "[ProPauli] Warning: truncation lead to empty tree. Disable this warning by setting `WARN_EMPTY_TREE=0`, if this is intended."
1✔
310
                                        << std::endl;
1✔
311
                        }
1✔
312
                }
1✔
313
                return ret;
240✔
314
        }
240✔
315

316
        friend bool operator==(Observable const& lhs, Observable const& rhs) {
22✔
317
                return lhs.size() == rhs.size() && lhs.paulis_ == rhs.paulis_;
22!
318
        }
22✔
319

320
        bool constexpr is_symbolic() const { return Symbolic<T>; }
321

322
        template <typename U = T, std::enable_if_t<std::is_same_v<U, T> && Symbolic<U>, bool> = true>
323
        void simplify(std::unordered_map<std::string, typename U::Underlying_t> const& variables = {}) {
3✔
324
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
15✔
325
                        paulis_[i].simplify(variables);
12✔
326
                }
12✔
327
        }
3✔
328

329
    private:
330
        PauliTermContainer<T> paulis_;
331
        RuntimeMerger<T> merger_;
332

333
        void check_invariant() const {
214✔
334
                if (paulis_.nb_terms() == 0) {
214!
335
                        throw std::invalid_argument("Can't have empty observable (no terms)");
×
336
                }
×
337
                const auto nb_qubits = this->nb_qubits();
214✔
338
                if (nb_qubits == 0) {
214!
339
                        throw std::invalid_argument("0 qubit observable not allowed.");
1✔
340
                }
1✔
341
        }
214✔
342

343
        void check_qubit(unsigned qubit) const {
1,695✔
344
                if (qubit >= nb_qubits()) {
1,695!
345
                        throw std::invalid_argument("Qubit index out of range.");
14✔
346
                }
14✔
347
        }
1,695✔
348
};
349

350
/**
351
 * @brief Prints the observable to an output stream.
352
 * @relates Observable
353
 * @note This can be an expensive operation for large observables.
354
 */
355
template <typename T>
356
std::ostream& operator<<(std::ostream& os, Observable<T> const& obs) {
179✔
357
        bool first = true;
179✔
358
        for (std::size_t i = 0; i < obs.size(); ++i) {
403!
359
                if (!first) {
224!
360
                        if constexpr (Symbolic<T>) {
45✔
361
                                os << " + ";
37✔
362
                        } else {
37✔
363
                                os << " ";
8✔
364
                        }
8✔
365
                }
45✔
366
                // copy_term is needed because the stream operator is defined for owning PauliTerm
367

368
                os << obs.copy_term(i);
224✔
369
                first = false;
224✔
370
        }
224✔
371
        return os;
179✔
372
}
179✔
373

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