diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md
index 8d31a8fac..e263f9d15 100644
--- a/doc/python_api_reference_vDev.md
+++ b/doc/python_api_reference_vDev.md
@@ -35,6 +35,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi
- [`stim.Circuit.generated`](#stim.Circuit.generated)
- [`stim.Circuit.get_detector_coordinates`](#stim.Circuit.get_detector_coordinates)
- [`stim.Circuit.get_final_qubit_coordinates`](#stim.Circuit.get_final_qubit_coordinates)
+ - [`stim.Circuit.has_flow`](#stim.Circuit.has_flow)
- [`stim.Circuit.inverse`](#stim.Circuit.inverse)
- [`stim.Circuit.num_detectors`](#stim.Circuit.num_detectors)
- [`stim.Circuit.num_measurements`](#stim.Circuit.num_measurements)
@@ -1881,6 +1882,113 @@ def get_final_qubit_coordinates(
"""
```
+
+```python
+# stim.Circuit.has_flow
+
+# (in class stim.Circuit)
+def has_flow(
+ self,
+ *,
+ start: Optional[stim.PauliString] = None,
+ end: Optional[stim.PauliString] = None,
+ measurements: Optional[Iterable[Union[int, stim.GateTarget]]] = None,
+ unsigned: bool = False,
+) -> bool:
+ """Determines if the circuit has a stabilizer flow or not.
+
+ A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer
+ P at the start of the circuit to the instantaneous stabilizer Q at the end of
+ the circuit. The flow may be mediated by certain measurements. For example,
+ a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and
+ the CNOT flows implemented by the circuit involve these measurements.
+
+ A flow like P -> Q means that the circuit transforms P into Q.
+ A flow like IDENTITY -> P means that the circuit prepares P.
+ A flow like P -> IDENTITY means that the circuit measures P.
+ A flow like IDENTITY -> IDENTITY means that the circuit contains a detector.
+
+ Args:
+ start: The input into the flow at the start of the circuit. Defaults to None
+ (the identity Pauli string).
+ end: The output from the flow at the end of the circuit. Defaults to None
+ (the identity Pauli string).
+ measurements: Defaults to None (empty). The indices of measurements to
+ include in the flow. This should be a collection of integers and/or
+ stim.GateTarget instances. Indexing uses the python convention where
+ non-negative indices index from the start and negative indices index
+ from the end.
+ unsigned: Defaults to False. When False, the flows must be correct including
+ the sign of the Pauli strings. When True, only the Pauli terms need to
+ be correct; the signs are permitted to be inverted. In effect, this
+ requires the circuit to be correct up to Pauli gates.
+
+ Returns:
+ True if the circuit has the given flow; False otherwise.
+
+ References:
+ Stim's gate documentation includes the stabilizer flows of each gate.
+
+ Appendix A of https://arxiv.org/abs/2302.02192 describes how flows are
+ defined and provides a circuit construction for experimentally verifying
+ their presence.
+
+ Examples:
+ >>> import stim
+
+ >>> stim.Circuit('''
+ ... RY 0
+ ... ''').has_flow(
+ ... end=stim.PauliString("Y"),
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... RY 0
+ ... ''').has_flow(
+ ... end=stim.PauliString("X"),
+ ... )
+ False
+
+ >>> stim.Circuit('''
+ ... CX 0 1
+ ... ''').has_flow(
+ ... start=stim.PauliString("+X_"),
+ ... end=stim.PauliString("+XX"),
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... # Lattice surgery CNOT
+ ... R 1
+ ... MXX 0 1
+ ... MZZ 1 2
+ ... MX 1
+ ... ''').has_flow(
+ ... start=stim.PauliString("+X_X"),
+ ... end=stim.PauliString("+__X"),
+ ... measurements=[0, 2],
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... H 0
+ ... ''').has_flow(
+ ... start=stim.PauliString("Y"),
+ ... end=stim.PauliString("Y"),
+ ... unsigned=True,
+ ... )
+ True
+
+ Caveats:
+ Currently, the unsigned=False version of this method is implemented by
+ performing 256 randomized tests. Each test has a 50% chance of a false
+ positive, and a 0% chance of a false negative. So, when the method returns
+ True, there is technically still a 2^-256 chance the circuit doesn't have
+ the flow. This is lower than the chance of a cosmic ray flipping the result.
+ """
+```
+
```python
# stim.Circuit.inverse
@@ -10092,7 +10200,7 @@ def then(
# (in class stim.Tableau)
def to_circuit(
self,
- method: str = 'elimination',
+ method: 'Literal["elimination", "graph_state"]' = 'elimination',
) -> stim.Circuit:
"""Synthesizes a circuit that implements the tableau's Clifford operation.
diff --git a/doc/stim.pyi b/doc/stim.pyi
index b10cf74e6..291954718 100644
--- a/doc/stim.pyi
+++ b/doc/stim.pyi
@@ -1311,6 +1311,106 @@ class Circuit:
>>> circuit.get_final_qubit_coordinates()
{1: [1.0, 2.0, 3.0]}
"""
+ def has_flow(
+ self,
+ *,
+ start: Optional[stim.PauliString] = None,
+ end: Optional[stim.PauliString] = None,
+ measurements: Optional[Iterable[Union[int, stim.GateTarget]]] = None,
+ unsigned: bool = False,
+ ) -> bool:
+ """Determines if the circuit has a stabilizer flow or not.
+
+ A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer
+ P at the start of the circuit to the instantaneous stabilizer Q at the end of
+ the circuit. The flow may be mediated by certain measurements. For example,
+ a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and
+ the CNOT flows implemented by the circuit involve these measurements.
+
+ A flow like P -> Q means that the circuit transforms P into Q.
+ A flow like IDENTITY -> P means that the circuit prepares P.
+ A flow like P -> IDENTITY means that the circuit measures P.
+ A flow like IDENTITY -> IDENTITY means that the circuit contains a detector.
+
+ Args:
+ start: The input into the flow at the start of the circuit. Defaults to None
+ (the identity Pauli string).
+ end: The output from the flow at the end of the circuit. Defaults to None
+ (the identity Pauli string).
+ measurements: Defaults to None (empty). The indices of measurements to
+ include in the flow. This should be a collection of integers and/or
+ stim.GateTarget instances. Indexing uses the python convention where
+ non-negative indices index from the start and negative indices index
+ from the end.
+ unsigned: Defaults to False. When False, the flows must be correct including
+ the sign of the Pauli strings. When True, only the Pauli terms need to
+ be correct; the signs are permitted to be inverted. In effect, this
+ requires the circuit to be correct up to Pauli gates.
+
+ Returns:
+ True if the circuit has the given flow; False otherwise.
+
+ References:
+ Stim's gate documentation includes the stabilizer flows of each gate.
+
+ Appendix A of https://arxiv.org/abs/2302.02192 describes how flows are
+ defined and provides a circuit construction for experimentally verifying
+ their presence.
+
+ Examples:
+ >>> import stim
+
+ >>> stim.Circuit('''
+ ... RY 0
+ ... ''').has_flow(
+ ... end=stim.PauliString("Y"),
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... RY 0
+ ... ''').has_flow(
+ ... end=stim.PauliString("X"),
+ ... )
+ False
+
+ >>> stim.Circuit('''
+ ... CX 0 1
+ ... ''').has_flow(
+ ... start=stim.PauliString("+X_"),
+ ... end=stim.PauliString("+XX"),
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... # Lattice surgery CNOT
+ ... R 1
+ ... MXX 0 1
+ ... MZZ 1 2
+ ... MX 1
+ ... ''').has_flow(
+ ... start=stim.PauliString("+X_X"),
+ ... end=stim.PauliString("+__X"),
+ ... measurements=[0, 2],
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... H 0
+ ... ''').has_flow(
+ ... start=stim.PauliString("Y"),
+ ... end=stim.PauliString("Y"),
+ ... unsigned=True,
+ ... )
+ True
+
+ Caveats:
+ Currently, the unsigned=False version of this method is implemented by
+ performing 256 randomized tests. Each test has a 50% chance of a false
+ positive, and a 0% chance of a false negative. So, when the method returns
+ True, there is technically still a 2^-256 chance the circuit doesn't have
+ the flow. This is lower than the chance of a cosmic ray flipping the result.
+ """
def inverse(
self,
) -> stim.Circuit:
@@ -7828,7 +7928,7 @@ class Tableau:
"""
def to_circuit(
self,
- method: str = 'elimination',
+ method: 'Literal["elimination", "graph_state"]' = 'elimination',
) -> stim.Circuit:
"""Synthesizes a circuit that implements the tableau's Clifford operation.
diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi
index b10cf74e6..291954718 100644
--- a/glue/python/src/stim/__init__.pyi
+++ b/glue/python/src/stim/__init__.pyi
@@ -1311,6 +1311,106 @@ class Circuit:
>>> circuit.get_final_qubit_coordinates()
{1: [1.0, 2.0, 3.0]}
"""
+ def has_flow(
+ self,
+ *,
+ start: Optional[stim.PauliString] = None,
+ end: Optional[stim.PauliString] = None,
+ measurements: Optional[Iterable[Union[int, stim.GateTarget]]] = None,
+ unsigned: bool = False,
+ ) -> bool:
+ """Determines if the circuit has a stabilizer flow or not.
+
+ A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer
+ P at the start of the circuit to the instantaneous stabilizer Q at the end of
+ the circuit. The flow may be mediated by certain measurements. For example,
+ a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and
+ the CNOT flows implemented by the circuit involve these measurements.
+
+ A flow like P -> Q means that the circuit transforms P into Q.
+ A flow like IDENTITY -> P means that the circuit prepares P.
+ A flow like P -> IDENTITY means that the circuit measures P.
+ A flow like IDENTITY -> IDENTITY means that the circuit contains a detector.
+
+ Args:
+ start: The input into the flow at the start of the circuit. Defaults to None
+ (the identity Pauli string).
+ end: The output from the flow at the end of the circuit. Defaults to None
+ (the identity Pauli string).
+ measurements: Defaults to None (empty). The indices of measurements to
+ include in the flow. This should be a collection of integers and/or
+ stim.GateTarget instances. Indexing uses the python convention where
+ non-negative indices index from the start and negative indices index
+ from the end.
+ unsigned: Defaults to False. When False, the flows must be correct including
+ the sign of the Pauli strings. When True, only the Pauli terms need to
+ be correct; the signs are permitted to be inverted. In effect, this
+ requires the circuit to be correct up to Pauli gates.
+
+ Returns:
+ True if the circuit has the given flow; False otherwise.
+
+ References:
+ Stim's gate documentation includes the stabilizer flows of each gate.
+
+ Appendix A of https://arxiv.org/abs/2302.02192 describes how flows are
+ defined and provides a circuit construction for experimentally verifying
+ their presence.
+
+ Examples:
+ >>> import stim
+
+ >>> stim.Circuit('''
+ ... RY 0
+ ... ''').has_flow(
+ ... end=stim.PauliString("Y"),
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... RY 0
+ ... ''').has_flow(
+ ... end=stim.PauliString("X"),
+ ... )
+ False
+
+ >>> stim.Circuit('''
+ ... CX 0 1
+ ... ''').has_flow(
+ ... start=stim.PauliString("+X_"),
+ ... end=stim.PauliString("+XX"),
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... # Lattice surgery CNOT
+ ... R 1
+ ... MXX 0 1
+ ... MZZ 1 2
+ ... MX 1
+ ... ''').has_flow(
+ ... start=stim.PauliString("+X_X"),
+ ... end=stim.PauliString("+__X"),
+ ... measurements=[0, 2],
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... H 0
+ ... ''').has_flow(
+ ... start=stim.PauliString("Y"),
+ ... end=stim.PauliString("Y"),
+ ... unsigned=True,
+ ... )
+ True
+
+ Caveats:
+ Currently, the unsigned=False version of this method is implemented by
+ performing 256 randomized tests. Each test has a 50% chance of a false
+ positive, and a 0% chance of a false negative. So, when the method returns
+ True, there is technically still a 2^-256 chance the circuit doesn't have
+ the flow. This is lower than the chance of a cosmic ray flipping the result.
+ """
def inverse(
self,
) -> stim.Circuit:
@@ -7828,7 +7928,7 @@ class Tableau:
"""
def to_circuit(
self,
- method: str = 'elimination',
+ method: 'Literal["elimination", "graph_state"]' = 'elimination',
) -> stim.Circuit:
"""Synthesizes a circuit that implements the tableau's Clifford operation.
diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc
index ebf7b15bf..4382ed526 100644
--- a/src/stim/circuit/circuit.pybind.cc
+++ b/src/stim/circuit/circuit.pybind.cc
@@ -20,6 +20,7 @@
#include "stim/circuit/circuit_repeat_block.pybind.h"
#include "stim/circuit/export_qasm.h"
#include "stim/circuit/gate_target.pybind.h"
+#include "stim/circuit/stabilizer_flow.h"
#include "stim/cmd/command_diagram.pybind.h"
#include "stim/dem/detector_error_model_target.pybind.h"
#include "stim/diagram/detector_slice/detector_slice_set.h"
@@ -40,6 +41,7 @@
#include "stim/simulators/tableau_simulator.h"
#include "stim/simulators/transform_without_feedback.h"
#include "stim/stabilizers/conversions.h"
+#include "stim/stabilizers/pauli_string.pybind.h"
using namespace stim;
using namespace stim_pybind;
@@ -2216,6 +2218,159 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ bool {
+ auto num_measurements = self.count_measurements();
+ PauliString raw_start(0);
+ PauliString raw_end(0);
+ std::vector raw_measurements;
+ if (!start.is_none()) {
+ raw_start = pybind11::cast(start).value;
+ }
+ if (!end.is_none()) {
+ raw_end = pybind11::cast(end).value;
+ }
+ if (!measurements.is_none()) {
+ for (const pybind11::handle &e : measurements) {
+ if (pybind11::isinstance(e)) {
+ auto d = pybind11::cast(e);
+ if (d.is_measurement_record_target()) {
+ raw_measurements.push_back(d);
+ continue;
+ }
+ } else {
+ try {
+ int64_t s = pybind11::cast(e);
+ if (s >= 0 && s < (int64_t)num_measurements) {
+ s -= num_measurements;
+ }
+ if (s < 0 && -s <= (int64_t)num_measurements) {
+ raw_measurements.push_back(GateTarget::rec(s));
+ continue;
+ }
+ } catch (const pybind11::cast_error &) {
+ }
+ }
+ throw std::invalid_argument(
+ "Each measurement must be an integer in `range(-circuit.num_measurements, "
+ "circuit.num_measurements)`, or a `stim.GateTarget`.");
+ }
+ }
+ StabilizerFlow flow{
+ .input = raw_start, .output = raw_end, .measurement_outputs = raw_measurements};
+ if (unsigned_only) {
+ return check_if_circuit_has_unsigned_stabilizer_flows(self, &flow)[0];
+ } else {
+ auto rng = externally_seeded_rng();
+ return sample_if_circuit_has_stabilizer_flows(256, rng, self, &flow)[0];
+ }
+ },
+ pybind11::kw_only(),
+ pybind11::arg("start") = pybind11::none(),
+ pybind11::arg("end") = pybind11::none(),
+ pybind11::arg("measurements") = pybind11::none(),
+ pybind11::arg("unsigned") = false,
+ clean_doc_string(R"DOC(
+ @signature def has_flow(self, *, start: Optional[stim.PauliString] = None, end: Optional[stim.PauliString] = None, measurements: Optional[Iterable[Union[int, stim.GateTarget]]] = None, unsigned: bool = False) -> bool:
+ Determines if the circuit has a stabilizer flow or not.
+
+ A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer
+ P at the start of the circuit to the instantaneous stabilizer Q at the end of
+ the circuit. The flow may be mediated by certain measurements. For example,
+ a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and
+ the CNOT flows implemented by the circuit involve these measurements.
+
+ A flow like P -> Q means that the circuit transforms P into Q.
+ A flow like IDENTITY -> P means that the circuit prepares P.
+ A flow like P -> IDENTITY means that the circuit measures P.
+ A flow like IDENTITY -> IDENTITY means that the circuit contains a detector.
+
+ Args:
+ start: The input into the flow at the start of the circuit. Defaults to None
+ (the identity Pauli string).
+ end: The output from the flow at the end of the circuit. Defaults to None
+ (the identity Pauli string).
+ measurements: Defaults to None (empty). The indices of measurements to
+ include in the flow. This should be a collection of integers and/or
+ stim.GateTarget instances. Indexing uses the python convention where
+ non-negative indices index from the start and negative indices index
+ from the end.
+ unsigned: Defaults to False. When False, the flows must be correct including
+ the sign of the Pauli strings. When True, only the Pauli terms need to
+ be correct; the signs are permitted to be inverted. In effect, this
+ requires the circuit to be correct up to Pauli gates.
+
+ Returns:
+ True if the circuit has the given flow; False otherwise.
+
+ References:
+ Stim's gate documentation includes the stabilizer flows of each gate.
+
+ Appendix A of https://arxiv.org/abs/2302.02192 describes how flows are
+ defined and provides a circuit construction for experimentally verifying
+ their presence.
+
+ Examples:
+ >>> import stim
+
+ >>> stim.Circuit('''
+ ... RY 0
+ ... ''').has_flow(
+ ... end=stim.PauliString("Y"),
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... RY 0
+ ... ''').has_flow(
+ ... end=stim.PauliString("X"),
+ ... )
+ False
+
+ >>> stim.Circuit('''
+ ... CX 0 1
+ ... ''').has_flow(
+ ... start=stim.PauliString("+X_"),
+ ... end=stim.PauliString("+XX"),
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... # Lattice surgery CNOT
+ ... R 1
+ ... MXX 0 1
+ ... MZZ 1 2
+ ... MX 1
+ ... ''').has_flow(
+ ... start=stim.PauliString("+X_X"),
+ ... end=stim.PauliString("+__X"),
+ ... measurements=[0, 2],
+ ... )
+ True
+
+ >>> stim.Circuit('''
+ ... H 0
+ ... ''').has_flow(
+ ... start=stim.PauliString("Y"),
+ ... end=stim.PauliString("Y"),
+ ... unsigned=True,
+ ... )
+ True
+
+ Caveats:
+ Currently, the unsigned=False version of this method is implemented by
+ performing 256 randomized tests. Each test has a 50% chance of a false
+ positive, and a 0% chance of a false negative. So, when the method returns
+ True, there is technically still a 2^-256 chance the circuit doesn't have
+ the flow. This is lower than the chance of a cosmic ray flipping the result.
+ )DOC")
+ .data());
+
c.def(
"diagram",
&circuit_diagram,
diff --git a/src/stim/circuit/circuit_pybind_test.py b/src/stim/circuit/circuit_pybind_test.py
index 70772b4cc..a4cc62ed1 100644
--- a/src/stim/circuit/circuit_pybind_test.py
+++ b/src/stim/circuit/circuit_pybind_test.py
@@ -1573,3 +1573,93 @@ def test_detslice_filter_coords_flexibility():
assert str(d1) == str(d3)
assert str(d1) == str(d4)
assert str(d1) == str(d5)
+
+
+def test_has_flow_ry():
+ c = stim.Circuit("""
+ RY 0
+ """)
+ assert c.has_flow(end=stim.PauliString("Y"))
+ assert not c.has_flow(end=stim.PauliString("-Y"))
+ assert not c.has_flow(end=stim.PauliString("X"))
+ assert c.has_flow(end=stim.PauliString("Y"), unsigned=True)
+ assert not c.has_flow(end=stim.PauliString("X"), unsigned=True)
+ assert c.has_flow(end=stim.PauliString("-Y"), unsigned=True)
+
+
+def test_has_flow_cxs():
+ c = stim.Circuit("""
+ CX 0 1
+ S 0
+ """)
+
+ assert c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("YX"))
+ assert c.has_flow(start=stim.PauliString("Y_"), end=stim.PauliString("-XX"))
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("XX"))
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("-XX"))
+
+ assert c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("YX"), unsigned=True)
+ assert c.has_flow(start=stim.PauliString("Y_"), end=stim.PauliString("-XX"), unsigned=True)
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("XX"), unsigned=True)
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("-XX"), unsigned=True)
+
+
+def test_has_flow_cxm():
+ c = stim.Circuit("""
+ CX 0 1
+ M 1
+ """)
+ assert c.has_flow(end=stim.PauliString("_Z"), measurements=[0])
+ assert c.has_flow(start=stim.PauliString("ZZ"), measurements=[0])
+ assert c.has_flow(start=stim.PauliString("ZZ"), end=stim.PauliString("_Z"))
+ assert c.has_flow(start=stim.PauliString("XX"), end=stim.PauliString("X_"))
+ assert c.has_flow(end=stim.PauliString("_Z"), measurements=[0], unsigned=True)
+ assert c.has_flow(start=stim.PauliString("ZZ"), measurements=[0], unsigned=True)
+ assert c.has_flow(start=stim.PauliString("ZZ"), end=stim.PauliString("_Z"), unsigned=True)
+ assert c.has_flow(start=stim.PauliString("XX"), end=stim.PauliString("X_"), unsigned=True)
+
+
+def test_has_flow_lattice_surgery():
+ c = stim.Circuit("""
+ # Lattice surgery CNOT with feedback.
+ RX 2
+ MZZ 2 0
+ MXX 2 1
+ MZ 2
+ CX rec[-1] 1 rec[-3] 1
+ CZ rec[-2] 0
+
+ S 0
+ """)
+ assert c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("YX"))
+ assert c.has_flow(start=stim.PauliString("Z_"), end=stim.PauliString("Z_"))
+ assert c.has_flow(start=stim.PauliString("_X"), end=stim.PauliString("_X"))
+ assert c.has_flow(start=stim.PauliString("_Z"), end=stim.PauliString("ZZ"))
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("XX"))
+
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("XX"))
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("-YX"))
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("XX"), unsigned=True)
+ assert c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("-YX"), unsigned=True)
+
+
+def test_has_flow_lattice_surgery_without_feedback():
+ c = stim.Circuit("""
+ # Lattice surgery CNOT without feedback.
+ RX 2
+ MZZ 2 0
+ MXX 2 1
+ MZ 2
+
+ S 0
+ """)
+ assert c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("YX"), measurements=[1])
+ assert c.has_flow(start=stim.PauliString("Z_"), end=stim.PauliString("Z_"))
+ assert c.has_flow(start=stim.PauliString("_X"), end=stim.PauliString("_X"))
+ assert c.has_flow(start=stim.PauliString("_Z"), end=stim.PauliString("ZZ"), measurements=[0, 2])
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("XX"))
+
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("XX"))
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("-YX"))
+ assert not c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("XX"), unsigned=True)
+ assert c.has_flow(start=stim.PauliString("X_"), end=stim.PauliString("-YX"), unsigned=True, measurements=[1])
diff --git a/src/stim/circuit/gate_data.test.cc b/src/stim/circuit/gate_data.test.cc
index 1e0fa2bc0..93afe26b0 100644
--- a/src/stim/circuit/gate_data.test.cc
+++ b/src/stim/circuit/gate_data.test.cc
@@ -176,7 +176,7 @@ TEST_EACH_WORD_SIZE_W(gate_data, stabilizer_flows_are_correct, {
Circuit c;
c.safe_append(g.id, targets, {});
auto rng = INDEPENDENT_TEST_RNG();
- auto r = check_if_circuit_has_stabilizer_flows(256, rng, c, flows);
+ auto r = sample_if_circuit_has_stabilizer_flows(256, rng, c, flows);
for (uint32_t fk = 0; fk < (uint32_t)flows.size(); fk++) {
EXPECT_TRUE(r[fk]) << "gate " << g.name << " has an unsatisfied flow: " << flows[fk];
}
@@ -208,7 +208,7 @@ TEST_EACH_WORD_SIZE_W(gate_data, stabilizer_flows_are_also_correct_for_decompose
}
Circuit c(g.h_s_cx_m_r_decomposition);
- auto r = check_if_circuit_has_stabilizer_flows(256, rng, c, flows);
+ auto r = sample_if_circuit_has_stabilizer_flows(256, rng, c, flows);
for (uint32_t fk = 0; fk < (uint32_t)flows.size(); fk++) {
EXPECT_TRUE(r[fk]) << "gate " << g.name << " has a decomposition with an unsatisfied flow: " << flows[fk];
}
diff --git a/src/stim/circuit/stabilizer_flow.h b/src/stim/circuit/stabilizer_flow.h
index ada575f86..66560da61 100644
--- a/src/stim/circuit/stabilizer_flow.h
+++ b/src/stim/circuit/stabilizer_flow.h
@@ -18,6 +18,7 @@
#define _STIM_CIRCUIT_STABILIZER_FLOW_H
#include
+#include
#include "stim/circuit/gate_target.h"
#include "stim/stabilizers/pauli_string.h"
@@ -49,8 +50,12 @@ struct StabilizerFlow {
/// A vector containing one boolean for each flow. The k'th boolean is true if the
/// k'th flow passed all checks.
template
-std::vector check_if_circuit_has_stabilizer_flows(
- size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, const std::vector> flows);
+std::vector sample_if_circuit_has_stabilizer_flows(
+ size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, SpanRef> flows);
+
+template
+std::vector check_if_circuit_has_unsigned_stabilizer_flows(
+ const Circuit &circuit, SpanRef> flows);
template
std::ostream &operator<<(std::ostream &out, const StabilizerFlow &flow);
diff --git a/src/stim/circuit/stabilizer_flow.inl b/src/stim/circuit/stabilizer_flow.inl
index 1766b6308..8e7c627c3 100644
--- a/src/stim/circuit/stabilizer_flow.inl
+++ b/src/stim/circuit/stabilizer_flow.inl
@@ -3,6 +3,7 @@
#include "stim/circuit/stabilizer_flow.h"
#include "stim/simulators/frame_simulator_util.h"
#include "stim/simulators/tableau_simulator.h"
+#include "stim/simulators/sparse_rev_frame_tracker.h"
namespace stim {
@@ -24,7 +25,7 @@ void _pauli_string_controlled_not(PauliStringRef control, uint32_t target, Ci
}
template
-bool _check_if_circuit_has_stabilizer_flow(
+bool _sample_if_circuit_has_stabilizer_flow(
size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, const StabilizerFlow &flow) {
uint32_t n = (uint32_t)circuit.count_qubits();
n = std::max(n, (uint32_t)flow.input.num_qubits);
@@ -58,11 +59,11 @@ bool _check_if_circuit_has_stabilizer_flow(
}
template
-std::vector check_if_circuit_has_stabilizer_flows(
- size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, const std::vector> flows) {
+std::vector sample_if_circuit_has_stabilizer_flows(
+ size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, SpanRef> flows) {
std::vector result;
for (const auto &flow : flows) {
- result.push_back(_check_if_circuit_has_stabilizer_flow(num_samples, rng, circuit, flow));
+ result.push_back(_sample_if_circuit_has_stabilizer_flow(num_samples, rng, circuit, flow));
}
return result;
}
@@ -166,4 +167,76 @@ std::ostream &operator<<(std::ostream &out, const StabilizerFlow &flow) {
return out;
}
+template
+std::vector check_if_circuit_has_unsigned_stabilizer_flows(const Circuit &circuit, SpanRef> flows) {
+ auto stats = circuit.compute_stats();
+ size_t num_qubits = stats.num_qubits;
+ for (const auto &flow : flows) {
+ num_qubits = std::max(num_qubits, flow.input.num_qubits);
+ num_qubits = std::max(num_qubits, flow.output.num_qubits);
+ }
+ SparseUnsignedRevFrameTracker rev(num_qubits, stats.num_measurements, flows.size(), false);
+
+ // Add end of flows into frames.
+ for (size_t f = 0; f < flows.size(); f++) {
+ const auto &flow = flows[f];
+ for (size_t q = 0; q < flow.output.num_qubits; q++) {
+ if (flow.output.xs[q]) {
+ rev.xs[q].xor_item(DemTarget::relative_detector_id(f));
+ }
+ if (flow.output.zs[q]) {
+ rev.zs[q].xor_item(DemTarget::relative_detector_id(f));
+ }
+ }
+ }
+
+ // Mark measurements for inclusion.
+ for (size_t f = flows.size(); f--;) {
+ const auto &flow = flows[f];
+ rev.undo_DETECTOR(CircuitInstruction{GateType::DETECTOR, {}, flow.measurement_outputs});
+ }
+
+ // Undo the circuit.
+ circuit.for_each_operation_reverse([&](const CircuitInstruction &inst) {
+ if (inst.gate_type == GateType::DETECTOR) {
+ // Substituted.
+ } else if (inst.gate_type == GateType::OBSERVABLE_INCLUDE) {
+ // Skip.
+ } else {
+ rev.undo_gate(inst);
+ }
+ });
+
+ // Remove start of flows from frames.
+ for (size_t f = 0; f < flows.size(); f++) {
+ const auto &flow = flows[f];
+ for (size_t q = 0; q < flow.input.num_qubits; q++) {
+ if (flow.input.xs[q]) {
+ rev.xs[q].xor_item(DemTarget::relative_detector_id(f));
+ }
+ if (flow.input.zs[q]) {
+ rev.zs[q].xor_item(DemTarget::relative_detector_id(f));
+ }
+ }
+ }
+
+ // Determine which flows survived.
+ std::vector result(flows.size(), true);
+ for (const auto &xs : rev.xs) {
+ for (const auto &t : xs) {
+ result[t.val()] = false;
+ }
+ }
+ for (const auto &zs : rev.zs) {
+ for (const auto &t : zs) {
+ result[t.val()] = false;
+ }
+ }
+ for (const auto &anti : rev.anticommutations) {
+ result[anti.val()] = false;
+ }
+
+ return result;
+}
+
} // namespace stim
diff --git a/src/stim/circuit/stabilizer_flow.test.cc b/src/stim/circuit/stabilizer_flow.test.cc
index f48a5b1ca..887249abb 100644
--- a/src/stim/circuit/stabilizer_flow.test.cc
+++ b/src/stim/circuit/stabilizer_flow.test.cc
@@ -22,9 +22,9 @@
using namespace stim;
-TEST_EACH_WORD_SIZE_W(stabilizer_flow, check_if_circuit_has_stabilizer_flows, {
+TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows, {
auto rng = INDEPENDENT_TEST_RNG();
- auto results = check_if_circuit_has_stabilizer_flows(
+ auto results = sample_if_circuit_has_stabilizer_flows(
256,
rng,
Circuit(R"CIRCUIT(
@@ -32,7 +32,7 @@ TEST_EACH_WORD_SIZE_W(stabilizer_flow, check_if_circuit_has_stabilizer_flows, {
CX 0 4 1 4 2 4 3 4
M 4
)CIRCUIT"),
- {
+ std::vector>{
StabilizerFlow::from_str("Z___ -> Z____"),
StabilizerFlow::from_str("_Z__ -> _Z__"),
StabilizerFlow::from_str("__Z_ -> __Z_"),
@@ -70,3 +70,41 @@ TEST_EACH_WORD_SIZE_W(stabilizer_flow, str_and_from_str, {
PauliString::from_str("-X"),
{GateTarget::rec(-1), GateTarget::rec(-3)}}));
})
+
+TEST_EACH_WORD_SIZE_W(stabilizer_flow, check_if_circuit_has_unsigned_stabilizer_flows, {
+ auto results = check_if_circuit_has_unsigned_stabilizer_flows(
+ Circuit(R"CIRCUIT(
+ R 4
+ CX 0 4 1 4 2 4 3 4
+ M 4
+ )CIRCUIT"),
+ std::vector>{
+ StabilizerFlow::from_str("Z___ -> Z____"),
+ StabilizerFlow::from_str("_Z__ -> _Z__"),
+ StabilizerFlow::from_str("__Z_ -> __Z_"),
+ StabilizerFlow::from_str("___Z -> ___Z"),
+ StabilizerFlow::from_str("XX__ -> XX__"),
+ StabilizerFlow::from_str("XXXX -> XXXX"),
+ StabilizerFlow::from_str("XYZ_ -> XYZ_"),
+ StabilizerFlow::from_str("XXX_ -> XXX_"),
+ StabilizerFlow::from_str("ZZZZ -> ____ xor rec[-1]"),
+ StabilizerFlow::from_str("+___Z -> -___Z"),
+ StabilizerFlow::from_str("-___Z -> -___Z"),
+ StabilizerFlow::from_str("-___Z -> +___Z"),
+ });
+ ASSERT_EQ(results, (std::vector{1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1}));
+});
+
+TEST_EACH_WORD_SIZE_W(stabilizer_flow, check_if_circuit_has_unsigned_stabilizer_flows_historical_failure, {
+ auto results = check_if_circuit_has_unsigned_stabilizer_flows(
+ Circuit(R"CIRCUIT(
+ CX 0 1
+ S 0
+ )CIRCUIT"),
+ std::vector>{
+ StabilizerFlow::from_str("X_ -> YX"),
+ StabilizerFlow::from_str("Y_ -> XX"),
+ StabilizerFlow::from_str("X_ -> XX"),
+ });
+ ASSERT_EQ(results, (std::vector{1, 1, 0}));
+})
diff --git a/src/stim/simulators/sparse_rev_frame_tracker.cc b/src/stim/simulators/sparse_rev_frame_tracker.cc
index ef4eb275f..62d1de9d6 100644
--- a/src/stim/simulators/sparse_rev_frame_tracker.cc
+++ b/src/stim/simulators/sparse_rev_frame_tracker.cc
@@ -180,12 +180,14 @@ void SparseUnsignedRevFrameTracker::undo_gate(const CircuitInstruction &inst) {
}
SparseUnsignedRevFrameTracker::SparseUnsignedRevFrameTracker(
- uint64_t num_qubits, uint64_t num_measurements_in_past, uint64_t num_detectors_in_past)
+ uint64_t num_qubits, uint64_t num_measurements_in_past, uint64_t num_detectors_in_past, bool fail_on_anticommute)
: xs(num_qubits),
zs(num_qubits),
rec_bits(),
num_measurements_in_past(num_measurements_in_past),
- num_detectors_in_past(num_detectors_in_past) {
+ num_detectors_in_past(num_detectors_in_past),
+ fail_on_anticommute(fail_on_anticommute),
+ anticommutations() {
}
void SparseUnsignedRevFrameTracker::handle_xor_gauge(
@@ -193,14 +195,27 @@ void SparseUnsignedRevFrameTracker::handle_xor_gauge(
if (sorted1 == sorted2) {
return;
}
- throw std::invalid_argument("A detector or observable anticommuted with a dissipative operation.");
+ if (fail_on_anticommute) {
+ throw std::invalid_argument("A detector or observable anticommuted with a dissipative operation.");
+ }
+ SparseXorVec dif;
+ dif.xor_sorted_items(sorted1);
+ dif.xor_sorted_items(sorted2);
+ for (const auto &d : dif) {
+ anticommutations.insert(d);
+ }
}
void SparseUnsignedRevFrameTracker::handle_gauge(SpanRef sorted) {
if (sorted.empty()) {
return;
}
- throw std::invalid_argument("A detector or observable anticommuted with a dissipative operation.");
+ if (fail_on_anticommute) {
+ throw std::invalid_argument("A detector or observable anticommuted with a dissipative operation.");
+ }
+ for (const auto &d : sorted) {
+ anticommutations.insert(d);
+ }
}
void SparseUnsignedRevFrameTracker::undo_classical_pauli(GateTarget classical_control, GateTarget target) {
diff --git a/src/stim/simulators/sparse_rev_frame_tracker.h b/src/stim/simulators/sparse_rev_frame_tracker.h
index dce4fed79..0de3958a3 100644
--- a/src/stim/simulators/sparse_rev_frame_tracker.h
+++ b/src/stim/simulators/sparse_rev_frame_tracker.h
@@ -38,9 +38,17 @@ struct SparseUnsignedRevFrameTracker {
uint64_t num_measurements_in_past;
/// Number of detectors that have not yet been processed.
uint64_t num_detectors_in_past;
+ /// If false, anticommuting dets and obs are stored .
+ /// If true, an exception is raised if anticommutation is detected.
+ bool fail_on_anticommute;
+ /// Where anticommuting dets and obs are stored.
+ std::set anticommutations;
SparseUnsignedRevFrameTracker(
- uint64_t num_qubits, uint64_t num_measurements_in_past, uint64_t num_detectors_in_past);
+ uint64_t num_qubits,
+ uint64_t num_measurements_in_past,
+ uint64_t num_detectors_in_past,
+ bool fail_on_anticommute = true);
template
PauliString current_error_sensitivity_for(DemTarget target) const {
@@ -52,7 +60,7 @@ struct SparseUnsignedRevFrameTracker {
return result;
}
- void undo_gate(const CircuitInstruction &data);
+ void undo_gate(const CircuitInstruction &inst);
void undo_gate(const CircuitInstruction &op, const Circuit &parent);
void handle_xor_gauge(SpanRef sorted1, SpanRef sorted2);
@@ -64,10 +72,10 @@ struct SparseUnsignedRevFrameTracker {
void undo_circuit(const Circuit &circuit);
void undo_loop(const Circuit &loop, uint64_t repetitions);
void undo_loop_by_unrolling(const Circuit &loop, uint64_t repetitions);
- void clear_qubits(const CircuitInstruction &dat);
- void handle_x_gauges(const CircuitInstruction &dat);
- void handle_y_gauges(const CircuitInstruction &dat);
- void handle_z_gauges(const CircuitInstruction &dat);
+ void clear_qubits(const CircuitInstruction &inst);
+ void handle_x_gauges(const CircuitInstruction &inst);
+ void handle_y_gauges(const CircuitInstruction &inst);
+ void handle_z_gauges(const CircuitInstruction &inst);
void undo_DETECTOR(const CircuitInstruction &inst);
void undo_OBSERVABLE_INCLUDE(const CircuitInstruction &inst);
diff --git a/src/stim/simulators/sparse_rev_frame_tracker.test.cc b/src/stim/simulators/sparse_rev_frame_tracker.test.cc
index c638a7d9a..96bd70b9f 100644
--- a/src/stim/simulators/sparse_rev_frame_tracker.test.cc
+++ b/src/stim/simulators/sparse_rev_frame_tracker.test.cc
@@ -687,3 +687,28 @@ TEST(SparseUnsignedRevFrameTracker, runs_on_general_circuit) {
ASSERT_EQ(s.num_measurements_in_past, 0);
ASSERT_EQ(s.num_detectors_in_past, 0);
}
+
+TEST(SparseUnsignedRevFrameTracker, tracks_anticommutation) {
+ Circuit circuit(R"CIRCUIT(
+ R 0 1 2
+ H 0
+ CX 0 1 0 2
+ MX 0 1 2
+ DETECTOR rec[-1]
+ DETECTOR rec[-1] rec[-2] rec[-3]
+ DETECTOR rec[-2] rec[-3]
+ OBSERVABLE_INCLUDE(2) rec[-3]
+ OBSERVABLE_INCLUDE(1) rec[-1] rec[-2] rec[-3]
+ )CIRCUIT");
+
+ SparseUnsignedRevFrameTracker rev(
+ circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors(), false);
+ rev.undo_circuit(circuit);
+ ASSERT_EQ(
+ rev.anticommutations,
+ (std::set{
+ DemTarget::relative_detector_id(0), DemTarget::relative_detector_id(2), DemTarget::observable_id(2)}));
+
+ SparseUnsignedRevFrameTracker rev2(circuit.count_qubits(), circuit.count_measurements(), circuit.count_detectors());
+ ASSERT_THROW({ rev.undo_circuit(circuit); }, std::invalid_argument);
+}
diff --git a/src/stim/stabilizers/tableau.pybind.cc b/src/stim/stabilizers/tableau.pybind.cc
index 30a3ef6d5..ff622fb0a 100644
--- a/src/stim/stabilizers/tableau.pybind.cc
+++ b/src/stim/stabilizers/tableau.pybind.cc
@@ -670,7 +670,7 @@ void stim_pybind::pybind_tableau_methods(pybind11::module &m, pybind11::class_ stim.Circuit:
+ @signature def to_circuit(self, method: 'Literal["elimination", "graph_state"]' = 'elimination') -> stim.Circuit:
Synthesizes a circuit that implements the tableau's Clifford operation.
The circuits returned by this method are not guaranteed to be stable