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