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

zeFresk / ProPauli / 17216228354

25 Aug 2025 05:37PM UTC coverage: 91.333% (-1.7%) from 92.994%
17216228354

Pull #8

github

web-flow
Merge 0e291ecb9 into a58930335
Pull Request #8: Optimize the library

258 of 338 branches covered (76.33%)

Branch coverage included in aggregate %.

371 of 389 new or added lines in 8 files covered. (95.37%)

6 existing lines in 1 file now uncovered.

859 of 885 relevant lines covered (97.06%)

41987.21 hits per line

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

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

4
#include "pauli.hpp"
5
#include "pauli_term.hpp"
6
#include "pauli_container.hpp"
7
#include "non_owning_pauli_term.hpp"
8
#include "merge.hpp"
9

10
#include <algorithm>
11
#include <cstring>
12
#include <initializer_list>
13
#include <iostream>
14
#include <iterator>
15
#include <ostream>
16
#include <stdexcept>
17
#include <string_view>
18
#include <type_traits>
19
#include <unordered_map>
20
#include <vector>
21

22
/**
23
 * @brief Represents a quantum observable as a linear combination of Pauli strings.
24
 * @tparam T The numeric type for the coefficients (e.g., float, double).
25
 *
26
 * An `Observable` is a sum of `PauliTerm` objects, each consisting of a Pauli
27
 * string and a coefficient. This class is the central data structure in the
28
 * Pauli back-propagation simulation. Quantum gates are applied to this object,
29
 * evolving it backward in the Heisenberg picture.
30
 */
31
template <typename T = coeff_t>
32
class Observable {
33
    public:
34
        /**
35
         * @brief Constructs an observable from a single Pauli string.
36
         * @param pauli_string A string representing the Pauli operators (e.g., "IXYZ").
37
         * @param coeff The coefficient of this Pauli term.
38
         *
39
         * @snippet tests/snippets/observable.cpp observable_from_string
40
         */
41
        Observable(std::string_view pauli_string,
42
                   typename std::enable_if_t<std::is_convertible_v<T, coeff_t>, T> coeff = T{ 1 })
43
                : paulis_{ { PauliTerm<T>(pauli_string, coeff) } } {
14✔
44
                check_invariant();
14✔
45
        }
14✔
46

47
        /**
48
         * @brief Constructs an observable from a list of Pauli strings.
49
         * @param pauli_string_list An initializer list of Pauli strings. Each will have a coefficient of 1.
50
         *
51
         * @snippet tests/snippets/observable.cpp observable_from_string_list
52
         */
53
        Observable(std::initializer_list<std::string_view> pauli_string_list) : paulis_{ pauli_string_list } {
46✔
54
                check_invariant();
46✔
55
        }
46✔
56

57
        /**
58
         * @brief Constructs an observable from a list of PauliTerm objects.
59
         * @param paulis_list An initializer list of `PauliTerm` objects.
60
         *
61
         * @snippet tests/snippets/observable.cpp observable_from_pauli_terms
62
         */
63
        Observable(std::initializer_list<PauliTerm<T>> paulis_list) : paulis_{ paulis_list } { check_invariant(); }
16✔
64

65
        /**
66
         * @brief Constructs an observable from a range of PauliTerm objects.
67
         * @tparam Iter An iterator type.
68
         * @param begin An iterator to the beginning of the range.
69
         * @param end An iterator to the end of the range.
70
         *
71
         * @snippet tests/snippets/observable.cpp observable_from_iterators
72
         */
73
        template <PauliTermIterator Iter>
74
        Observable(Iter&& begin, Iter&& end) : paulis_{ begin, end } {
4✔
75
                check_invariant();
4✔
76
        }
4✔
77

78
        /**
79
         * @brief Applies a single-qubit Pauli gate to the observable.
80
         * @param g The Pauli gate to apply.
81
         * @param qubit The index of the qubit to apply the gate to.
82
         * @pre qubit index must be less than the number of qubits in the observable.
83
         */
84
        void apply_pauli(Pauli_gates g, unsigned qubit) {
38✔
85
                check_qubit(qubit);
38✔
86
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
95✔
87
                        paulis_[i].apply_pauli(g, qubit);
57✔
88
                }
57✔
89
        }
38✔
90

91
        /**
92
         * @brief Applies a single-qubit Clifford gate to the observable.
93
         * @param g The Clifford gate to apply (e.g., Hadamard).
94
         * @param qubit The index of the qubit to apply the gate to.
95
         * @pre qubit index must be less than the number of qubits in the observable.
96
         */
97
        void apply_clifford(Clifford_Gates_1Q g, unsigned qubit) {
98✔
98
                check_qubit(qubit);
98✔
99
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
320✔
100
                        paulis_[i].apply_clifford(g, qubit);
222✔
101
                }
222✔
102
        }
98✔
103

104
        /**
105
         * @brief Applies a single-qubit unital noise channel to the observable.
106
         * @param n The type of unital noise (e.g., Depolarizing, Dephasing).
107
         * @param qubit The index of the qubit to apply the noise to.
108
         * @param p The noise probability parameter.
109
         * @pre qubit index must be less than the number of qubits in the observable.
110
         */
111
        void apply_unital_noise(UnitalNoise n, unsigned qubit, T p) {
47✔
112
                check_qubit(qubit);
47✔
113
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
137✔
114
                        paulis_[i].apply_unital_noise(n, qubit, p);
90✔
115
                }
90✔
116
        }
47✔
117

118
        /**
119
         * @brief Applies a CNOT (CX) gate to the observable.
120
         * @param qubit_control The index of the control qubit.
121
         * @param qubit_target The index of the target qubit.
122
         * @pre qubits indexes must be less than the number of qubits in the observable.
123
         * @pre control qubit != target qubit
124
         */
125
        void apply_cx(unsigned qubit_control, unsigned qubit_target) {
118✔
126
                check_qubit(qubit_control);
118✔
127
                check_qubit(qubit_target);
118✔
128
                if (qubit_control == qubit_target) {
118✔
129
                        throw std::invalid_argument("cx gate target must be != from control.");
1✔
130
                }
1✔
131

132
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
267✔
133
                        paulis_[i].apply_cx(qubit_control, qubit_target);
150✔
134
                }
150✔
135
        }
117✔
136

137
        /**
138
         * @brief Applies a single-qubit Rz rotation gate to the observable.
139
         * @param qubit The index of the qubit to apply the rotation to.
140
         * @param theta The rotation angle in radians.
141
         *
142
         * @note This is a "splitting" operation. Applying an Rz gate to a Pauli term
143
         * containing an X or Y operator on the target qubit will result in two
144
         * output Pauli terms, potentially doubling the size of the observable.
145
         */
146
        void apply_rz(unsigned qubit, T theta) {
103✔
147
                check_qubit(qubit);
103✔
148

149
                const auto nb_terms = paulis_.nb_terms();
103✔
150
                for (std::size_t i = 0; i < nb_terms; ++i) {
549✔
151
                        auto p = paulis_[i];
446✔
152
                        if (!p[qubit].commutes_with(p_z)) {
446✔
153
                                auto new_path = paulis_.create_pauliterm();
340✔
154
                                paulis_[i].apply_rz(qubit, theta, new_path);
340✔
155
                        }
340✔
156
                }
446✔
157
        }
103✔
158

159
        /**
160
         * @brief Applies an amplitude damping noise channel.
161
         * @param qubit The index of the qubit to apply the channel to.
162
         * @param pn The noise probability parameter.
163
         *
164
         * @note This can be a "splitting" operation. If the Pauli term has a Z operator
165
         * on the target qubit, the term will be split into two. If it has X or Y, the
166
         * coefficient is simply scaled. If it has I, there is no effect.
167
         */
168
        void apply_amplitude_damping(unsigned qubit, T pn) {
45✔
169
                check_qubit(qubit);
45✔
170
                // paulis_.reserve(paulis_.size() * 2);
171
                const auto nb_terms = paulis_.nb_terms();
45✔
172
                for (std::size_t i = 0; i < nb_terms; ++i) {
123✔
173
                        auto p = paulis_[i];
78✔
174
                        if (p[qubit] == p_z) {
78✔
175
                                auto new_path = paulis_.create_pauliterm(); // invalidate p
58✔
176
                                paulis_[i].apply_amplitude_damping_z(qubit, pn, new_path);
58✔
177
                        } else if (p[qubit] == p_x || p[qubit] == p_y) {
58✔
178
                                p.apply_amplitude_damping_xy(qubit, pn);
7✔
179
                        }
7✔
180
                }
78✔
181
        }
45✔
182

183
        /**
184
         * @brief Calculates the expectation value of the observable.
185
         * @return The expectation value.
186
         *
187
         * The expectation value is calculated by summing the coefficients of all Pauli terms
188
         * that commute with Z on all qubits (i.e., contain only I and Z operators).
189
         */
190
        T expectation_value() const {
76✔
191
                T ret = 0;
76✔
192
                for (std::size_t i = 0; i < paulis_.nb_terms(); ++i) {
276✔
193
                        ret += paulis_[i].expectation_value();
200✔
194
                }
200✔
195
                return ret;
76✔
196
        }
76✔
197

198
        /** @brief Accesses a specific PauliTerm in the observable. */
199
        NonOwningPauliTerm<T> operator[](std::size_t idx) { return paulis_[idx]; }
141✔
200
        /** @brief Accesses a specific PauliTerm in the observable (const version). */
201
        ReadOnlyNonOwningPauliTerm<T> const operator[](std::size_t idx) const { return paulis_[idx]; }
14✔
202
        /** @brief Returns an iterator to the beginning of the PauliTerm vector. */
203
        decltype(auto) begin() { return paulis_.begin(); }
14✔
204
        /** @brief Returns a const iterator to the beginning of the PauliTerm vector. */
UNCOV
205
        decltype(auto) begin() const { return paulis_.begin(); }
×
206
        /** @brief Returns a const iterator to the beginning of the PauliTerm vector. */
207
        decltype(auto) cbegin() const { return paulis_.cbegin(); }
11✔
208
        /** @brief Returns an iterator to the end of the PauliTerm vector. */
209
        decltype(auto) end() { return paulis_.end(); }
14✔
210
        /** @brief Returns a const iterator to the end of the PauliTerm vector. */
UNCOV
211
        decltype(auto) end() const { return paulis_.end(); }
×
212
        /** @brief Returns a const iterator to the end of the PauliTerm vector. */
213
        decltype(auto) cend() const { return paulis_.cend(); }
12✔
214
        /** @brief Gets the number of Pauli terms in the observable. */
215
        std::size_t size() const { return paulis_.nb_terms(); }
900✔
216
        /* @brief Get the number of qubits of this Observable */
217
        std::size_t nb_qubits() const { return paulis_.nb_qubits(); }
675✔
218

219
        PauliTerm<T> copy_term(std::size_t idx) const {
13✔
220
                if (idx >= size()) {
13!
NEW
221
                        throw std::invalid_argument("Index out of range");
×
NEW
222
                }
×
223
                auto nopt = (*this)[idx];
13✔
224
                PauliTerm<T> ret{ nopt.begin(), nopt.end(), nopt.coefficient() };
13✔
225
                return ret;
13✔
226
        }
13✔
227

228
        /**
229
         * @brief Merges Pauli terms with identical Pauli strings.
230
         * @return The number of terms after the merge.
231
         *
232
         * This function iterates through all Pauli terms in the observable. If two or more
233
         * terms have the same Pauli string, their coefficients are added together, and they are
234
         * replaced by a single term. This is a crucial optimization for reducing the complexity of the simulation.
235
         */
236
        std::size_t merge() {
97✔
237
                merge_inplace_noalloc(paulis_);
97✔
238
                return paulis_.nb_terms();
97✔
239
        }
97✔
240

241
        /**
242
         * @brief Truncates the observable based on a given truncation strategy.
243
         * @tparam Truncator The type of the truncator object.
244
         * @param truncator The truncator object that defines the truncation logic.
245
         * @return The number of Pauli terms removed.
246
         * @see Truncator
247
         */
248
        template <typename Truncator>
249
        std::size_t truncate(Truncator&& truncator) {
81✔
250
                auto ret = truncator.truncate(paulis_);
81✔
251
                if (paulis_.nb_terms() == 0) {
81!
252
                        const auto warn_env = std::getenv("WARN_EMPTY_TREE");
2✔
253
                        if (warn_env == nullptr || strcmp(warn_env, "0") != 0) {
2!
254
                                std::cerr
2✔
255
                                        << "[ProPauli] Warning: truncation lead to empty tree. Disable this warning by setting `WARN_EMPTY_TREE=0`, if this is intended."
2✔
256
                                        << std::endl;
2✔
257
                        }
2✔
258
                }
2✔
259
                return ret;
81✔
260
        }
81✔
261

262
        friend bool operator==(Observable const& lhs, Observable const& rhs) {
11✔
263
                return lhs.size() == rhs.size() && lhs.paulis_ == rhs.paulis_;
11!
264
        }
11✔
265

266
    private:
267
        // std::vector<PauliTerm<T>> paulis_;
268
        PauliTermContainer<T> paulis_;
269

270
        void check_invariant() const {
75✔
271
                if (paulis_.nb_terms() == 0) {
75!
UNCOV
272
                        throw std::invalid_argument("Can't have empty observable (no terms)");
×
UNCOV
273
                }
×
274
                const auto nb_qubits = this->nb_qubits();
75✔
275
                if (nb_qubits == 0) {
75!
UNCOV
276
                        throw std::invalid_argument("0 qubit observable not allowed.");
×
UNCOV
277
                }
×
278
                /*if (!std::all_of(paulis_.cbegin(), paulis_.cend(),
279
                                 [=](auto const& pt) { return pt.size() == nb_qubits; })) {
280
                        throw std::invalid_argument("Can't have observable with mismatching number of qubits.");
281
                }*/
282
        }
75✔
283

284
        void check_qubit(unsigned qubit) const {
566✔
285
                if (qubit >= nb_qubits()) {
566✔
286
                        throw std::invalid_argument("Qubit index out of range.");
7✔
287
                }
7✔
288
        }
566✔
289
};
290

291
template <typename T>
292
std::ostream& operator<<(std::ostream& os, Observable<T> const& obs) {
3✔
293
        bool first = true;
3✔
294
        for (std::size_t i = 0; i < obs.size(); ++i) {
8✔
295
                if (!first) {
5✔
296
                        os << " ";
2✔
297
                }
2✔
298
                os << obs.copy_term(i); // TODO: optimize
5✔
299

300
                first = false;
5✔
301
        }
5✔
302
        return os;
3✔
303
}
3✔
304

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