Skip to content

Commit 9156ab6

Browse files
authored
Add MBQC decompositions to the gates documentation (#942)
- Fix `stim.Flow.__init__` not xor-sorting measurements/observables - Add `stim.Flow.__mul__`
1 parent eca777a commit 9156ab6

16 files changed

+2069
-17
lines changed

doc/gates.md

Lines changed: 923 additions & 0 deletions
Large diffs are not rendered by default.

doc/python_api_reference_vDev.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi
220220
- [`stim.Flow`](#stim.Flow)
221221
- [`stim.Flow.__eq__`](#stim.Flow.__eq__)
222222
- [`stim.Flow.__init__`](#stim.Flow.__init__)
223+
- [`stim.Flow.__mul__`](#stim.Flow.__mul__)
223224
- [`stim.Flow.__ne__`](#stim.Flow.__ne__)
224225
- [`stim.Flow.__repr__`](#stim.Flow.__repr__)
225226
- [`stim.Flow.__str__`](#stim.Flow.__str__)
@@ -9165,6 +9166,40 @@ def __init__(
91659166
"""
91669167
```
91679168

9169+
<a name="stim.Flow.__mul__"></a>
9170+
```python
9171+
# stim.Flow.__mul__
9172+
9173+
# (in class stim.Flow)
9174+
def __mul__(
9175+
self,
9176+
rhs: stim.Flow,
9177+
) -> stim.Flow:
9178+
"""Computes the product of two flows.
9179+
9180+
Args:
9181+
rhs: The right hand side of the multiplication.
9182+
9183+
Returns:
9184+
The product of the two flows.
9185+
9186+
Raises:
9187+
ValueError: The inputs anti-commute (their product would be anti-Hermitian).
9188+
For example, 1 -> X times 1 -> Y fails because it would give 1 -> iZ.
9189+
9190+
Examples:
9191+
>>> import stim
9192+
>>> stim.Flow("X -> X") * stim.Flow("Z -> Z")
9193+
stim.Flow("Y -> Y")
9194+
9195+
>>> stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ")
9196+
stim.Flow("1 -> -YY")
9197+
9198+
>>> stim.Flow("X -> rec[-1]") * stim.Flow("X -> rec[-2]")
9199+
stim.Flow("_ -> rec[-2] xor rec[-1]")
9200+
"""
9201+
```
9202+
91689203
<a name="stim.Flow.__ne__"></a>
91699204
```python
91709205
# stim.Flow.__ne__

doc/stim.pyi

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7309,6 +7309,33 @@ class Flow:
73097309
>>> stim.Flow("X -> Y xor obs[3] xor obs[3] xor obs[3]")
73107310
stim.Flow("X -> Y xor obs[3]")
73117311
"""
7312+
def __mul__(
7313+
self,
7314+
rhs: stim.Flow,
7315+
) -> stim.Flow:
7316+
"""Computes the product of two flows.
7317+
7318+
Args:
7319+
rhs: The right hand side of the multiplication.
7320+
7321+
Returns:
7322+
The product of the two flows.
7323+
7324+
Raises:
7325+
ValueError: The inputs anti-commute (their product would be anti-Hermitian).
7326+
For example, 1 -> X times 1 -> Y fails because it would give 1 -> iZ.
7327+
7328+
Examples:
7329+
>>> import stim
7330+
>>> stim.Flow("X -> X") * stim.Flow("Z -> Z")
7331+
stim.Flow("Y -> Y")
7332+
7333+
>>> stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ")
7334+
stim.Flow("1 -> -YY")
7335+
7336+
>>> stim.Flow("X -> rec[-1]") * stim.Flow("X -> rec[-2]")
7337+
stim.Flow("_ -> rec[-2] xor rec[-1]")
7338+
"""
73127339
def __ne__(
73137340
self,
73147341
arg0: stim.Flow,

file_lists/source_files_no_main

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ src/stim/util_top/export_crumble_url.cc
9595
src/stim/util_top/export_qasm.cc
9696
src/stim/util_top/export_quirk_url.cc
9797
src/stim/util_top/has_flow.cc
98+
src/stim/util_top/mbqc_decomposition.cc
9899
src/stim/util_top/reference_sample_tree.cc
99100
src/stim/util_top/simplified_circuit.cc
100101
src/stim/util_top/transform_without_feedback.cc

file_lists/test_files

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ src/stim/util_top/export_crumble_url.test.cc
9292
src/stim/util_top/export_qasm.test.cc
9393
src/stim/util_top/export_quirk_url.test.cc
9494
src/stim/util_top/has_flow.test.cc
95+
src/stim/util_top/mbqc_decomposition.test.cc
9596
src/stim/util_top/reference_sample_tree.test.cc
9697
src/stim/util_top/simplified_circuit.test.cc
9798
src/stim/util_top/stabilizers_to_tableau.test.cc

glue/python/src/stim/__init__.pyi

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7309,6 +7309,33 @@ class Flow:
73097309
>>> stim.Flow("X -> Y xor obs[3] xor obs[3] xor obs[3]")
73107310
stim.Flow("X -> Y xor obs[3]")
73117311
"""
7312+
def __mul__(
7313+
self,
7314+
rhs: stim.Flow,
7315+
) -> stim.Flow:
7316+
"""Computes the product of two flows.
7317+
7318+
Args:
7319+
rhs: The right hand side of the multiplication.
7320+
7321+
Returns:
7322+
The product of the two flows.
7323+
7324+
Raises:
7325+
ValueError: The inputs anti-commute (their product would be anti-Hermitian).
7326+
For example, 1 -> X times 1 -> Y fails because it would give 1 -> iZ.
7327+
7328+
Examples:
7329+
>>> import stim
7330+
>>> stim.Flow("X -> X") * stim.Flow("Z -> Z")
7331+
stim.Flow("Y -> Y")
7332+
7333+
>>> stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ")
7334+
stim.Flow("1 -> -YY")
7335+
7336+
>>> stim.Flow("X -> rec[-1]") * stim.Flow("X -> rec[-2]")
7337+
stim.Flow("_ -> rec[-2] xor rec[-1]")
7338+
"""
73127339
def __ne__(
73137340
self,
73147341
arg0: stim.Flow,

src/stim.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
#include "stim/util_top/export_qasm.h"
118118
#include "stim/util_top/export_quirk_url.h"
119119
#include "stim/util_top/has_flow.h"
120+
#include "stim/util_top/mbqc_decomposition.h"
120121
#include "stim/util_top/reference_sample_tree.h"
121122
#include "stim/util_top/simplified_circuit.h"
122123
#include "stim/util_top/stabilizers_to_tableau.h"

src/stim/cmd/command_help.cc

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,24 @@
1919
#include <iostream>
2020
#include <map>
2121
#include <set>
22-
#include <stim/circuit/circuit.h>
23-
24-
#include "command_convert.h"
25-
#include "command_detect.h"
26-
#include "command_diagram.h"
27-
#include "command_explain_errors.h"
28-
#include "command_gen.h"
29-
#include "command_m2d.h"
30-
#include "command_repl.h"
31-
#include "command_sample.h"
32-
#include "command_sample_dem.h"
22+
23+
#include "stim/circuit/circuit.h"
24+
#include "stim/cmd/command_convert.h"
25+
#include "stim/cmd/command_detect.h"
26+
#include "stim/cmd/command_diagram.h"
27+
#include "stim/cmd/command_explain_errors.h"
28+
#include "stim/cmd/command_gen.h"
29+
#include "stim/cmd/command_m2d.h"
30+
#include "stim/cmd/command_repl.h"
31+
#include "stim/cmd/command_sample.h"
32+
#include "stim/cmd/command_sample_dem.h"
3333
#include "stim/cmd/command_analyze_errors.h"
3434
#include "stim/gates/gates.h"
3535
#include "stim/io/stim_data_formats.h"
3636
#include "stim/stabilizers/flow.h"
3737
#include "stim/stabilizers/tableau.h"
3838
#include "stim/util_bot/arg_parse.h"
39+
#include "stim/util_top/mbqc_decomposition.h"
3940

4041
using namespace stim;
4142

@@ -255,13 +256,39 @@ void print_decomposition(Acc &out, const Gate &gate) {
255256
out << "# The following circuit is equivalent (up to global phase) to `";
256257
out << undecomposed.str() << "`";
257258
out << decomposition;
258-
if (Circuit(decomposition) == Circuit(undecomposed.str())) {
259+
Circuit c(decomposition);
260+
if (c == Circuit(undecomposed.str())) {
259261
out << "\n# (The decomposition is trivial because this gate is in the target gate set.)\n";
262+
} else if (c.operations.empty()) {
263+
out << "\n# (The decomposition is empty because this gate has no effect.)\n";
260264
}
261265
out.change_indent(-4);
262266
}
263267
}
264268

269+
void print_mbqc_decomposition(Acc &out, const Gate &gate) {
270+
const char *decomposition = mbqc_decomposition(gate.id);
271+
if (decomposition != nullptr) {
272+
std::stringstream undecomposed;
273+
auto decomp_targets = gate_decomposition_help_targets_for_gate_type(gate.id);
274+
undecomposed << CircuitInstruction{gate.id, {}, decomp_targets, ""};
275+
276+
out << "MBQC Decomposition (into MX, MY, MZ, MXX, MZZ, and Pauli feedback):\n";
277+
out.change_indent(+4);
278+
out << "# The following circuit performs `";
279+
out << undecomposed.str() << "` (but affects the measurement record and an ancilla qubit)";
280+
out << decomposition;
281+
Circuit c(decomposition);
282+
if (c == Circuit(undecomposed.str())) {
283+
out << "\n# (The decomposition is trivial because this gate is in the target gate set.)\n";
284+
} else if (c.operations.empty()) {
285+
out << "\n# (The decomposition is empty because this gate has no effect.)\n";
286+
}
287+
288+
out.change_indent(-4);
289+
}
290+
}
291+
265292
void print_stabilizer_generators(Acc &out, const Gate &gate) {
266293
auto flows = gate.flows<MAX_BITWORD_WIDTH>();
267294
if (flows.empty()) {
@@ -425,6 +452,7 @@ std::string generate_per_gate_help_markdown(const Gate &alt_gate, int indent, bo
425452
print_bloch_vector(out, gate);
426453
print_unitary_matrix(out, gate);
427454
print_decomposition(out, gate);
455+
print_mbqc_decomposition(out, gate);
428456
out.flush();
429457
return out.settled;
430458
}

src/stim/stabilizers/flow.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ struct Flow {
3636
/// ENTRIES MUST BE SORTED AND UNIQUE.
3737
std::vector<uint32_t> observables;
3838

39+
/// Fixes non-unique non-sorted measurements and observables.
40+
void canonicalize();
41+
3942
static Flow<W> from_str(std::string_view text);
43+
Flow<W> operator*(const Flow<W> &rhs) const;
4044
bool operator<(const Flow<W> &other) const;
4145
bool operator==(const Flow<W> &other) const;
4246
bool operator!=(const Flow<W> &other) const;

src/stim/stabilizers/flow.inl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,34 @@ std::ostream &operator<<(std::ostream &out, const Flow<W> &flow) {
258258
return out;
259259
}
260260

261+
template <size_t W>
262+
void Flow<W>::canonicalize() {
263+
size_t measurements_kept = inplace_xor_sort(SpanRef<int32_t>(measurements)).size();
264+
size_t observables_kept = inplace_xor_sort(SpanRef<uint32_t>(observables)).size();
265+
measurements.resize(measurements_kept);
266+
observables.resize(observables_kept);
267+
}
268+
269+
template <size_t W>
270+
Flow<W> Flow<W>::operator*(const Flow<W> &rhs) const {
271+
Flow<W> result = *this;
272+
273+
result.input.ensure_num_qubits(rhs.input.num_qubits, 1.1);
274+
result.output.ensure_num_qubits(rhs.output.num_qubits, 1.1);
275+
uint8_t log_i = 0;
276+
log_i -= result.input.ref().inplace_right_mul_returning_log_i_scalar(rhs.input.ref());
277+
log_i += result.output.ref().inplace_right_mul_returning_log_i_scalar(rhs.output.ref());
278+
if (log_i & 1) {
279+
throw std::invalid_argument(str() + " anticommutes with " + rhs.str());
280+
}
281+
if (log_i & 2) {
282+
result.output.sign ^= true;
283+
}
284+
285+
result.measurements.insert(result.measurements.end(), rhs.measurements.begin(), rhs.measurements.end());
286+
result.observables.insert(result.observables.end(), rhs.observables.begin(), rhs.observables.end());
287+
result.canonicalize();
288+
return result;
289+
}
290+
261291
} // namespace stim

src/stim/stabilizers/flow.pybind.cc

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,13 @@ static Flow<MAX_BITWORD_WIDTH> py_init_flow(
101101
result.measurements.push_back(pybind11::cast<int32_t>(h));
102102
}
103103
}
104-
inplace_xor_sort(SpanRef<int32_t>(result.measurements));
105104
}
106105
if (!included_observables.is_none()) {
107106
for (const auto &h : included_observables) {
108107
result.observables.push_back(pybind11::cast<uint32_t>(h));
109108
}
110-
inplace_xor_sort(SpanRef<uint32_t>(result.observables));
111109
}
110+
result.canonicalize();
112111
return result;
113112
}
114113

@@ -287,6 +286,36 @@ void stim_pybind::pybind_flow_methods(pybind11::module &m, pybind11::class_<Flow
287286
)DOC")
288287
.data());
289288

289+
c.def(
290+
"__mul__",
291+
&Flow<MAX_BITWORD_WIDTH>::operator*,
292+
pybind11::arg("rhs"),
293+
clean_doc_string(R"DOC(
294+
Computes the product of two flows.
295+
296+
Args:
297+
rhs: The right hand side of the multiplication.
298+
299+
Returns:
300+
The product of the two flows.
301+
302+
Raises:
303+
ValueError: The inputs anti-commute (their product would be anti-Hermitian).
304+
For example, 1 -> X times 1 -> Y fails because it would give 1 -> iZ.
305+
306+
Examples:
307+
>>> import stim
308+
>>> stim.Flow("X -> X") * stim.Flow("Z -> Z")
309+
stim.Flow("Y -> Y")
310+
311+
>>> stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ")
312+
stim.Flow("1 -> -YY")
313+
314+
>>> stim.Flow("X -> rec[-1]") * stim.Flow("X -> rec[-2]")
315+
stim.Flow("_ -> rec[-2] xor rec[-1]")
316+
)DOC")
317+
.data());
318+
290319
c.def(pybind11::self == pybind11::self, "Determines if two flows have identical contents.");
291320
c.def(pybind11::self != pybind11::self, "Determines if two flows have non-identical contents.");
292321
c.def("__str__", &Flow<MAX_BITWORD_WIDTH>::str, "Returns a shorthand description of the flow.");

src/stim/stabilizers/flow_pybind_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,24 @@ def test_obs_include_pauli_terms_sensitivity():
176176
assert zs == 0
177177
assert 256 <= ys <= 768
178178
assert xs == ys
179+
180+
181+
def test_flow_canonicalization():
182+
assert stim.Flow(measurements=[4, 0, 4]) == stim.Flow(measurements=[0])
183+
assert stim.Flow(included_observables=[4, 0, 4]) == stim.Flow(included_observables=[0])
184+
185+
186+
def test_flow_multiplication():
187+
assert stim.Flow("XYZ -> 1") * stim.Flow("1 -> XYZ") == stim.Flow("XYZ -> XYZ")
188+
assert stim.Flow("XX_ -> 1") * stim.Flow("_XX -> 1") == stim.Flow("X_X -> 1")
189+
assert stim.Flow("1 -> XX_") * stim.Flow("1 -> _XX") == stim.Flow("1 -> X_X")
190+
assert stim.Flow("1 -> rec[-1] xor rec[-3]") * stim.Flow("1 -> rec[-1] xor rec[-2]") == stim.Flow("1 -> rec[-2] xor rec[-3]")
191+
assert stim.Flow("1 -> obs[1] xor obs[3]") * stim.Flow("1 -> obs[1] xor obs[2]") == stim.Flow("1 -> obs[2] xor obs[3]")
192+
assert stim.Flow("X -> X") * stim.Flow("Z -> Z") == stim.Flow("Y -> Y")
193+
assert stim.Flow("1 -> XX") * stim.Flow("1 -> ZZ") == stim.Flow("1 -> -YY")
194+
assert stim.Flow("1 -> obs[1]") * stim.Flow("1 -> obs[1]") == stim.Flow("1 -> 1")
195+
assert stim.Flow("1 -> rec[1]") * stim.Flow("1 -> rec[1]") == stim.Flow("1 -> 1")
196+
with pytest.raises(ValueError, match="anticommute"):
197+
_ = stim.Flow("1 -> X") * stim.Flow("1 -> Y")
198+
with pytest.raises(ValueError, match="anticommute"):
199+
_ = stim.Flow("1 -> Y") * stim.Flow("1 -> X")

src/stim/stabilizers/pauli_string_ref.inl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,14 @@ PauliStringRef<W> &PauliStringRef<W>::operator*=(const PauliStringRef<W> &rhs) {
114114

115115
template <size_t W>
116116
uint8_t PauliStringRef<W>::inplace_right_mul_returning_log_i_scalar(const PauliStringRef<W> &rhs) noexcept {
117-
assert(num_qubits == rhs.num_qubits);
117+
assert(num_qubits >= rhs.num_qubits);
118118

119119
// Accumulator registers for counting mod 4 in parallel across each bit position.
120120
simd_word<W> cnt1{};
121121
simd_word<W> cnt2{};
122122

123-
xs.for_each_word(
124-
zs, rhs.xs, rhs.zs, [&cnt1, &cnt2](simd_word<W> &x1, simd_word<W> &z1, simd_word<W> &x2, simd_word<W> &z2) {
123+
rhs.xs.for_each_word(
124+
rhs.zs, xs, zs, [&cnt1, &cnt2](simd_word<W> &x2, simd_word<W> &z2, simd_word<W> &x1, simd_word<W> &z1) {
125125
// Update the left hand side Paulis.
126126
auto old_x1 = x1;
127127
auto old_z1 = z1;

0 commit comments

Comments
 (0)