From b38ce30b9da6bf4857fed018057ed91dbecbfcf3 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Fri, 16 Feb 2024 17:28:52 -0800 Subject: [PATCH] Add `stim.Flow` and `stim.Circuit.has_all_flows` - Add `stim.Flow` class for representing stabilizer flows - Refactor `stim.has_flow` to only take a `stim.Flow` [breaking change vs dev versions] - Add `stim.Circuit.has_all_flows` method for more efficiently checking flows In C++ land: - Refactor more methods to take `std::string_view` instead of `const char *` or `std::string` - Rename `stim::StabilizerFlow` -> `stim::Flow` - Refactor `stim::Flow` to allow non-negative measurement indices --- doc/python_api_reference_vDev.md | 455 +++++++++++++++--- doc/stim.pyi | 379 ++++++++++++--- file_lists/python_api_files | 1 + file_lists/test_files | 2 +- glue/python/src/stim/__init__.pyi | 379 ++++++++++++--- src/stim.h | 2 +- src/stim/arg_parse.cc | 78 ++- src/stim/arg_parse.h | 6 +- src/stim/arg_parse.test.cc | 19 +- src/stim/circuit/circuit.pybind.cc | 263 +++++----- src/stim/circuit/circuit_pybind_test.py | 119 ++--- src/stim/circuit/stabilizer_flow.test.cc | 205 -------- src/stim/cmd/command_diagram.cc | 2 +- src/stim/cmd/command_help.cc | 2 +- .../detector_slice/detector_slice_set.cc | 4 +- .../detector_slice/detector_slice_set.h | 2 +- src/stim/gates/gates.h | 20 +- src/stim/gates/gates.pybind.cc | 22 +- src/stim/gates/gates.test.cc | 2 +- src/stim/py/stim.pybind.cc | 168 +++++++ src/stim/py/stim_pybind_test.py | 89 ++++ src/stim/stabilizers/flex_pauli_string.cc | 9 +- .../stabilizers/flex_pauli_string.test.cc | 2 + .../stabilizer_flow.h => stabilizers/flow.h} | 23 +- .../flow.inl} | 177 ++++--- src/stim/stabilizers/flow.pybind.cc | 245 ++++++++++ src/stim/stabilizers/flow.pybind.h | 29 ++ src/stim/stabilizers/flow.test.cc | 218 +++++++++ src/stim/stabilizers/flow_pybind_test.py | 106 ++++ src/stim/stabilizers/tableau_iter.pybind.cc | 2 +- 30 files changed, 2289 insertions(+), 741 deletions(-) delete mode 100644 src/stim/circuit/stabilizer_flow.test.cc rename src/stim/{circuit/stabilizer_flow.h => stabilizers/flow.h} (73%) rename src/stim/{circuit/stabilizer_flow.inl => stabilizers/flow.inl} (61%) create mode 100644 src/stim/stabilizers/flow.pybind.cc create mode 100644 src/stim/stabilizers/flow.pybind.h create mode 100644 src/stim/stabilizers/flow.test.cc create mode 100644 src/stim/stabilizers/flow_pybind_test.py diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md index 517845184..4a197b909 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_all_flows`](#stim.Circuit.has_all_flows) - [`stim.Circuit.has_flow`](#stim.Circuit.has_flow) - [`stim.Circuit.inverse`](#stim.Circuit.inverse) - [`stim.Circuit.num_detectors`](#stim.Circuit.num_detectors) @@ -188,6 +189,15 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`stim.FlippedMeasurement.__init__`](#stim.FlippedMeasurement.__init__) - [`stim.FlippedMeasurement.observable`](#stim.FlippedMeasurement.observable) - [`stim.FlippedMeasurement.record_index`](#stim.FlippedMeasurement.record_index) +- [`stim.Flow`](#stim.Flow) + - [`stim.Flow.__eq__`](#stim.Flow.__eq__) + - [`stim.Flow.__init__`](#stim.Flow.__init__) + - [`stim.Flow.__ne__`](#stim.Flow.__ne__) + - [`stim.Flow.__repr__`](#stim.Flow.__repr__) + - [`stim.Flow.__str__`](#stim.Flow.__str__) + - [`stim.Flow.input_copy`](#stim.Flow.input_copy) + - [`stim.Flow.measurements_copy`](#stim.Flow.measurements_copy) + - [`stim.Flow.output_copy`](#stim.Flow.output_copy) - [`stim.GateData`](#stim.GateData) - [`stim.GateData.__eq__`](#stim.GateData.__eq__) - [`stim.GateData.__init__`](#stim.GateData.__init__) @@ -385,9 +395,11 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`stim.gate_data`](#stim.gate_data) - [`stim.main`](#stim.main) - [`stim.read_shot_data_file`](#stim.read_shot_data_file) +- [`stim.target_combined_paulis`](#stim.target_combined_paulis) - [`stim.target_combiner`](#stim.target_combiner) - [`stim.target_inv`](#stim.target_inv) - [`stim.target_logical_observable_id`](#stim.target_logical_observable_id) +- [`stim.target_pauli`](#stim.target_pauli) - [`stim.target_rec`](#stim.target_rec) - [`stim.target_relative_detector_id`](#stim.target_relative_detector_id) - [`stim.target_separator`](#stim.target_separator) @@ -1885,6 +1897,66 @@ def get_final_qubit_coordinates( """ ``` + +```python +# stim.Circuit.has_all_flows + +# (in class stim.Circuit) +def has_all_flows( + self, + flows: Iterable[stim.Flow], + *, + unsigned: bool = False, +) -> bool: + """Determines if the circuit has all the given stabilizer flow or not. + + This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster + because, behind the scenes, the circuit can be iterated once instead of once + per flow. + + Args: + flows: An iterable of `stim.Flow` instances representing the flows to check. + 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. + + Examples: + >>> import stim + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ]) + False + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> -Y'), + ... stim.Flow('Z -> X'), + ... ]) + True + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ], 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.has_flow @@ -1892,14 +1964,11 @@ def get_final_qubit_coordinates( # (in class stim.Circuit) def has_flow( self, - shorthand: Optional[str] = None, + flow: stim.Flow, *, - start: Union[None, str, stim.PauliString] = None, - end: Union[None, str, 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. + """Determines if the circuit has the given 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 @@ -1913,26 +1982,7 @@ def has_flow( A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). Args: - shorthand: Specifies the flow as a short string like "X1 -> -YZ xor rec[1]". - The text must contain "->" to separate the input pauli string from the - output pauli string. Measurements are included by appending - " xor rec[k]" for each measurement index k. Indexing uses the python - convention where non-negative indices index from the start and negative - indices index from the end. The pauli strings are parsed as if by - `stim.PauliString.__init__`. - start: The input into the flow at the start of the circuit. Defaults to None - (the identity Pauli string). When specified, this should be a - `stim.PauliString`, or a `str` (which will be parsed using - `stim.PauliString.__init__`). - end: The output from the flow at the end of the circuit. Defaults to None - (the identity Pauli string). When specified, this should be a - `stim.PauliString`, or a `str` (which will be parsed using - `stim.PauliString.__init__`). - 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. + flow: The flow to check for. 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 @@ -1941,56 +1991,49 @@ def has_flow( 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 >>> m = stim.Circuit('M 0') - >>> m.has_flow('Z -> Z') + >>> m.has_flow(stim.Flow('Z -> Z')) True - >>> m.has_flow('X -> X') + >>> m.has_flow(stim.Flow('X -> X')) False - >>> m.has_flow('Z -> I') + >>> m.has_flow(stim.Flow('Z -> I')) False - >>> m.has_flow('Z -> I xor rec[-1]') + >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) True - >>> m.has_flow('Z -> rec[-1]') + >>> m.has_flow(stim.Flow('Z -> rec[-1]')) True >>> cx58 = stim.Circuit('CX 5 8') - >>> cx58.has_flow('X5 -> X5*X8') + >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) True - >>> cx58.has_flow('X_ -> XX') + >>> cx58.has_flow(stim.Flow('X_ -> XX')) False - >>> cx58.has_flow('_____X___ -> _____X__X') + >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) True >>> stim.Circuit(''' ... RY 0 - ... ''').has_flow( - ... end=stim.PauliString("Y"), - ... ) + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("Y"), + ... )) True >>> stim.Circuit(''' ... RY 0 - ... ''').has_flow( - ... end=stim.PauliString("X"), - ... ) + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("X"), + ... )) False >>> stim.Circuit(''' ... CX 0 1 - ... ''').has_flow( - ... start=stim.PauliString("+X_"), - ... end=stim.PauliString("+XX"), - ... ) + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_"), + ... output=stim.PauliString("+XX"), + ... )) True >>> stim.Circuit(''' @@ -1999,22 +2042,29 @@ def has_flow( ... MXX 0 1 ... MZZ 1 2 ... MX 1 - ... ''').has_flow( - ... start=stim.PauliString("+X_X"), - ... end=stim.PauliString("+__X"), + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_X"), + ... output=stim.PauliString("+__X"), ... measurements=[0, 2], - ... ) + ... )) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( - ... start=stim.PauliString("Y"), - ... end=stim.PauliString("Y"), + ... stim.Flow("Y -> Y"), ... unsigned=True, ... ) True + >>> stim.Circuit(''' + ... H 0 + ... ''').has_flow( + ... stim.Flow("Y -> Y"), + ... unsigned=False, + ... ) + False + 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 @@ -6748,6 +6798,219 @@ def record_index( """ ``` + +```python +# stim.Flow + +# (at top-level in the stim module) +class Flow: + """A stabilizer flow (e.g. "XI -> XX xor rec[-1]"). + + Stabilizer circuits implement, and can be defined by, how they turn input + stabilizers into output stabilizers mediated by measurements. These + relationships are called stabilizer flows, and `stim.Flow` is a representation + of such a flow. For example, a `stim.Flow` can be given to + `stim.Circuit.has_flow` to verify that a circuit implements the flow. + + 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 the circuit transforms P into Q. + A flow like 1 -> P means the circuit prepares P. + A flow like P -> 1 means the circuit measures P. + A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). + + 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 + >>> c = stim.Circuit("CNOT 2 4") + + >>> c.has_flow(stim.Flow("__X__ -> __X_X")) + True + + >>> c.has_flow(stim.Flow("X2*X4 -> X2")) + True + + >>> c.has_flow(stim.Flow("Z4 -> Z4")) + False + """ +``` + + +```python +# stim.Flow.__eq__ + +# (in class stim.Flow) +def __eq__( + self, + arg0: stim.Flow, +) -> bool: + """Determines if two flows have identical contents. + """ +``` + + +```python +# stim.Flow.__init__ + +# (in class stim.Flow) +def __init__( + self, + arg: Union[None, str, stim.Flow] = None, + /, + *, + input: Optional[stim.PauliString] = None, + output: Optional[stim.PauliString] = None, + measurements: Optional[Iterable[Union[int, GateTarget]]] = None, +) -> None: + """Initializes a stim.Flow. + + When given a string, the string is parsed as flow shorthand. For example, + the string "X_ -> ZZ xor rec[-1]" will result in a flow with input pauli string + "X_", output pauli string "ZZ", and measurement indices [-1]. + + Arguments: + arg [position-only]: Defaults to None. Must be specified by itself if used. + str: Initializes a flow by parsing the given shorthand text. + stim.Flow: Initializes a copy of the given flow. + None (default): Initializes an empty flow. + input: Defaults to None. Can be set to a stim.PauliString to directly + specify the flow's input stabilizer. + output: Defaults to None. Can be set to a stim.PauliString to directly + specify the flow's output stabilizer. + measurements: Can be set to a list of integers or gate targets like + `stim.target_rec(-1)`, to specify the measurements that mediate the + flow. Negative and positive measurement indices are allowed. Indexes + follow the python convention where -1 is the last measurement in a + circuit and 0 is the first measurement in a circuit. + + Examples: + >>> import stim + + >>> stim.Flow("X2 -> -Y2*Z4 xor rec[-1]") + stim.Flow("__X -> -__Y_Z xor rec[-1]") + + >>> stim.Flow("Z -> 1 xor rec[-1]") + stim.Flow("Z -> rec[-1]") + + >>> stim.Flow( + ... input=stim.PauliString("XX"), + ... output=stim.PauliString("_X"), + ... measurements=[], + ... ) + stim.Flow("XX -> _X") + """ +``` + + +```python +# stim.Flow.__ne__ + +# (in class stim.Flow) +def __ne__( + self, + arg0: stim.Flow, +) -> bool: + """Determines if two flows have non-identical contents. + """ +``` + + +```python +# stim.Flow.__repr__ + +# (in class stim.Flow) +def __repr__( + self, +) -> str: + """Returns valid python code evaluating to an equivalent `stim.Flow`. + """ +``` + + +```python +# stim.Flow.__str__ + +# (in class stim.Flow) +def __str__( + self, +) -> str: + """Returns a shorthand description of the flow. + """ +``` + + +```python +# stim.Flow.input_copy + +# (in class stim.Flow) +def input_copy( + self, +) -> stim.PauliString: + """Returns a copy of the flow's input stabilizer. + + Examples: + >>> import stim + >>> f = stim.Flow(input=stim.PauliString('XX')) + >>> f.input_copy() + stim.PauliString("+XX") + + >>> f.input_copy() is f.input_copy() + False + """ +``` + + +```python +# stim.Flow.measurements_copy + +# (in class stim.Flow) +def measurements_copy( + self, +) -> List[int]: + """Returns a copy of the flow's measurement indices. + + Examples: + >>> import stim + >>> f = stim.Flow(measurements=[-1, 2]) + >>> f.measurements_copy() + [-1, 2] + + >>> f.measurements_copy() is f.measurements_copy() + False + """ +``` + + +```python +# stim.Flow.output_copy + +# (in class stim.Flow) +def output_copy( + self, +) -> stim.PauliString: + """Returns a copy of the flow's output stabilizer. + + Examples: + >>> import stim + >>> f = stim.Flow(output=stim.PauliString('XX')) + >>> f.output_copy() + stim.PauliString("+XX") + + >>> f.output_copy() is f.output_copy() + False + """ +``` + ```python # stim.GateData @@ -13446,6 +13709,40 @@ def read_shot_data_file( """ ``` + +```python +# stim.target_combined_paulis + +# (at top-level in the stim module) +def target_combined_paulis( + paulis: Union[stim.PauliString, List[stim.GateTarget]], + invert: bool = False, +) -> stim.GateTarget: + """Returns a list of targets encoding a pauli product for instructions like MPP. + + Args: + paulis: The paulis to encode into the targets. This can be a + `stim.PauliString` or a list of pauli targets from `stim.target_x`, + `stim.target_pauli`, etc. + invert: Defaults to False. If True, the product is inverted (like "!X2*Y3"). + Note that this is in addition to any inversions specified by the + `paulis` argument. + + Examples: + >>> import stim + >>> circuit = stim.Circuit() + >>> circuit.append("MPP", [ + ... *stim.target_combined_paulis(stim.PauliString("-XYZ")), + ... *stim.target_combined_paulis([stim.target_x(2), stim.target_y(5)]), + ... *stim.target_combined_paulis([stim.target_z(9)], invert=True), + ... ]) + >>> circuit + stim.Circuit(''' + MPP !X0*Y1*Z2 X2*Y5 !Z9 + ''') + """ +``` + ```python # stim.target_combiner @@ -13530,6 +13827,52 @@ def target_logical_observable_id( """ ``` + +```python +# stim.target_pauli + +# (at top-level in the stim module) +def target_pauli( + qubit_index: int, + pauli: Union[str, int], + invert: bool = False, +) -> stim.GateTarget: + """Returns a pauli target that can be passed into `stim.Circuit.append`. + + Args: + qubit_index: The qubit that the Pauli applies to. + pauli: The pauli gate to use. This can either be a string identifying the + pauli by name ("x", "X", "y", "Y", "z", or "Z") or an integer following + the convention (1=X, 2=Y, 3=Z). Setting this argument to "I" or to + 0 will return a qubit target instead of a pauli target. + invert: Defaults to False. If True, the target is inverted (like "!X10"), + indicating that, for example, measurement results should be inverted). + + Examples: + >>> import stim + >>> circuit = stim.Circuit() + >>> circuit.append("MPP", [ + ... stim.target_pauli(2, "X"), + ... stim.target_combiner(), + ... stim.target_pauli(3, "y", invert=True), + ... stim.target_pauli(5, 3), + ... ]) + >>> circuit + stim.Circuit(''' + MPP X2*!Y3 Z5 + ''') + + >>> circuit.append("M", [ + ... stim.target_pauli(7, "I"), + ... ]) + >>> circuit + stim.Circuit(''' + MPP X2*!Y3 Z5 + M 7 + ''') + """ +``` + ```python # stim.target_rec diff --git a/doc/stim.pyi b/doc/stim.pyi index f6a6d5105..7e476efcc 100644 --- a/doc/stim.pyi +++ b/doc/stim.pyi @@ -1311,16 +1311,66 @@ class Circuit: >>> circuit.get_final_qubit_coordinates() {1: [1.0, 2.0, 3.0]} """ + def has_all_flows( + self, + flows: Iterable[stim.Flow], + *, + unsigned: bool = False, + ) -> bool: + """Determines if the circuit has all the given stabilizer flow or not. + + This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster + because, behind the scenes, the circuit can be iterated once instead of once + per flow. + + Args: + flows: An iterable of `stim.Flow` instances representing the flows to check. + 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. + + Examples: + >>> import stim + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ]) + False + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> -Y'), + ... stim.Flow('Z -> X'), + ... ]) + True + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ], 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 has_flow( self, - shorthand: Optional[str] = None, + flow: stim.Flow, *, - start: Union[None, str, stim.PauliString] = None, - end: Union[None, str, 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. + """Determines if the circuit has the given 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 @@ -1334,26 +1384,7 @@ class Circuit: A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). Args: - shorthand: Specifies the flow as a short string like "X1 -> -YZ xor rec[1]". - The text must contain "->" to separate the input pauli string from the - output pauli string. Measurements are included by appending - " xor rec[k]" for each measurement index k. Indexing uses the python - convention where non-negative indices index from the start and negative - indices index from the end. The pauli strings are parsed as if by - `stim.PauliString.__init__`. - start: The input into the flow at the start of the circuit. Defaults to None - (the identity Pauli string). When specified, this should be a - `stim.PauliString`, or a `str` (which will be parsed using - `stim.PauliString.__init__`). - end: The output from the flow at the end of the circuit. Defaults to None - (the identity Pauli string). When specified, this should be a - `stim.PauliString`, or a `str` (which will be parsed using - `stim.PauliString.__init__`). - 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. + flow: The flow to check for. 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 @@ -1362,56 +1393,49 @@ class Circuit: 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 >>> m = stim.Circuit('M 0') - >>> m.has_flow('Z -> Z') + >>> m.has_flow(stim.Flow('Z -> Z')) True - >>> m.has_flow('X -> X') + >>> m.has_flow(stim.Flow('X -> X')) False - >>> m.has_flow('Z -> I') + >>> m.has_flow(stim.Flow('Z -> I')) False - >>> m.has_flow('Z -> I xor rec[-1]') + >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) True - >>> m.has_flow('Z -> rec[-1]') + >>> m.has_flow(stim.Flow('Z -> rec[-1]')) True >>> cx58 = stim.Circuit('CX 5 8') - >>> cx58.has_flow('X5 -> X5*X8') + >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) True - >>> cx58.has_flow('X_ -> XX') + >>> cx58.has_flow(stim.Flow('X_ -> XX')) False - >>> cx58.has_flow('_____X___ -> _____X__X') + >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) True >>> stim.Circuit(''' ... RY 0 - ... ''').has_flow( - ... end=stim.PauliString("Y"), - ... ) + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("Y"), + ... )) True >>> stim.Circuit(''' ... RY 0 - ... ''').has_flow( - ... end=stim.PauliString("X"), - ... ) + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("X"), + ... )) False >>> stim.Circuit(''' ... CX 0 1 - ... ''').has_flow( - ... start=stim.PauliString("+X_"), - ... end=stim.PauliString("+XX"), - ... ) + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_"), + ... output=stim.PauliString("+XX"), + ... )) True >>> stim.Circuit(''' @@ -1420,22 +1444,29 @@ class Circuit: ... MXX 0 1 ... MZZ 1 2 ... MX 1 - ... ''').has_flow( - ... start=stim.PauliString("+X_X"), - ... end=stim.PauliString("+__X"), + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_X"), + ... output=stim.PauliString("+__X"), ... measurements=[0, 2], - ... ) + ... )) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( - ... start=stim.PauliString("Y"), - ... end=stim.PauliString("Y"), + ... stim.Flow("Y -> Y"), ... unsigned=True, ... ) True + >>> stim.Circuit(''' + ... H 0 + ... ''').has_flow( + ... stim.Flow("Y -> Y"), + ... unsigned=False, + ... ) + False + 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 @@ -5159,6 +5190,156 @@ class FlippedMeasurement: For example, the fifth measurement in a circuit has a measurement record index of 4. """ +class Flow: + """A stabilizer flow (e.g. "XI -> XX xor rec[-1]"). + + Stabilizer circuits implement, and can be defined by, how they turn input + stabilizers into output stabilizers mediated by measurements. These + relationships are called stabilizer flows, and `stim.Flow` is a representation + of such a flow. For example, a `stim.Flow` can be given to + `stim.Circuit.has_flow` to verify that a circuit implements the flow. + + 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 the circuit transforms P into Q. + A flow like 1 -> P means the circuit prepares P. + A flow like P -> 1 means the circuit measures P. + A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). + + 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 + >>> c = stim.Circuit("CNOT 2 4") + + >>> c.has_flow(stim.Flow("__X__ -> __X_X")) + True + + >>> c.has_flow(stim.Flow("X2*X4 -> X2")) + True + + >>> c.has_flow(stim.Flow("Z4 -> Z4")) + False + """ + def __eq__( + self, + arg0: stim.Flow, + ) -> bool: + """Determines if two flows have identical contents. + """ + def __init__( + self, + arg: Union[None, str, stim.Flow] = None, + /, + *, + input: Optional[stim.PauliString] = None, + output: Optional[stim.PauliString] = None, + measurements: Optional[Iterable[Union[int, GateTarget]]] = None, + ) -> None: + """Initializes a stim.Flow. + + When given a string, the string is parsed as flow shorthand. For example, + the string "X_ -> ZZ xor rec[-1]" will result in a flow with input pauli string + "X_", output pauli string "ZZ", and measurement indices [-1]. + + Arguments: + arg [position-only]: Defaults to None. Must be specified by itself if used. + str: Initializes a flow by parsing the given shorthand text. + stim.Flow: Initializes a copy of the given flow. + None (default): Initializes an empty flow. + input: Defaults to None. Can be set to a stim.PauliString to directly + specify the flow's input stabilizer. + output: Defaults to None. Can be set to a stim.PauliString to directly + specify the flow's output stabilizer. + measurements: Can be set to a list of integers or gate targets like + `stim.target_rec(-1)`, to specify the measurements that mediate the + flow. Negative and positive measurement indices are allowed. Indexes + follow the python convention where -1 is the last measurement in a + circuit and 0 is the first measurement in a circuit. + + Examples: + >>> import stim + + >>> stim.Flow("X2 -> -Y2*Z4 xor rec[-1]") + stim.Flow("__X -> -__Y_Z xor rec[-1]") + + >>> stim.Flow("Z -> 1 xor rec[-1]") + stim.Flow("Z -> rec[-1]") + + >>> stim.Flow( + ... input=stim.PauliString("XX"), + ... output=stim.PauliString("_X"), + ... measurements=[], + ... ) + stim.Flow("XX -> _X") + """ + def __ne__( + self, + arg0: stim.Flow, + ) -> bool: + """Determines if two flows have non-identical contents. + """ + def __repr__( + self, + ) -> str: + """Returns valid python code evaluating to an equivalent `stim.Flow`. + """ + def __str__( + self, + ) -> str: + """Returns a shorthand description of the flow. + """ + def input_copy( + self, + ) -> stim.PauliString: + """Returns a copy of the flow's input stabilizer. + + Examples: + >>> import stim + >>> f = stim.Flow(input=stim.PauliString('XX')) + >>> f.input_copy() + stim.PauliString("+XX") + + >>> f.input_copy() is f.input_copy() + False + """ + def measurements_copy( + self, + ) -> List[int]: + """Returns a copy of the flow's measurement indices. + + Examples: + >>> import stim + >>> f = stim.Flow(measurements=[-1, 2]) + >>> f.measurements_copy() + [-1, 2] + + >>> f.measurements_copy() is f.measurements_copy() + False + """ + def output_copy( + self, + ) -> stim.PauliString: + """Returns a copy of the flow's output stabilizer. + + Examples: + >>> import stim + >>> f = stim.Flow(output=stim.PauliString('XX')) + >>> f.output_copy() + stim.PauliString("+XX") + + >>> f.output_copy() is f.output_copy() + False + """ class GateData: """Details about a gate supported by stim. @@ -5235,22 +5416,22 @@ class GateData: >>> for e in stim.gate_data('H').__unstable_flows: ... print(e) - +X -> +Z - +Z -> +X + X -> Z + Z -> X >>> for e in stim.gate_data('ISWAP').__unstable_flows: ... print(e) - +X_ -> +ZY - +Z_ -> +_Z - +_X -> +YZ - +_Z -> +Z_ + X_ -> ZY + Z_ -> _Z + _X -> YZ + _Z -> Z_ >>> for e in stim.gate_data('MXX').__unstable_flows: ... print(e) - +X_ -> +X_ - +_X -> +_X - +ZZ -> +ZZ - +XX -> rec[-1] + X_ -> X_ + _X -> _X + ZZ -> ZZ + XX -> rec[-1] """ @property def aliases( @@ -10525,6 +10706,33 @@ def read_shot_data_file( array([[False, False, False, False], [False, True, False, True]]) """ +def target_combined_paulis( + paulis: Union[stim.PauliString, List[stim.GateTarget]], + invert: bool = False, +) -> stim.GateTarget: + """Returns a list of targets encoding a pauli product for instructions like MPP. + + Args: + paulis: The paulis to encode into the targets. This can be a + `stim.PauliString` or a list of pauli targets from `stim.target_x`, + `stim.target_pauli`, etc. + invert: Defaults to False. If True, the product is inverted (like "!X2*Y3"). + Note that this is in addition to any inversions specified by the + `paulis` argument. + + Examples: + >>> import stim + >>> circuit = stim.Circuit() + >>> circuit.append("MPP", [ + ... *stim.target_combined_paulis(stim.PauliString("-XYZ")), + ... *stim.target_combined_paulis([stim.target_x(2), stim.target_y(5)]), + ... *stim.target_combined_paulis([stim.target_z(9)], invert=True), + ... ]) + >>> circuit + stim.Circuit(''' + MPP !X0*Y1*Z2 X2*Y5 !Z9 + ''') + """ def target_combiner( ) -> stim.GateTarget: """Returns a target combiner that can be used to build Pauli products. @@ -10588,6 +10796,45 @@ def target_logical_observable_id( error(0.25) L13 ''') """ +def target_pauli( + qubit_index: int, + pauli: Union[str, int], + invert: bool = False, +) -> stim.GateTarget: + """Returns a pauli target that can be passed into `stim.Circuit.append`. + + Args: + qubit_index: The qubit that the Pauli applies to. + pauli: The pauli gate to use. This can either be a string identifying the + pauli by name ("x", "X", "y", "Y", "z", or "Z") or an integer following + the convention (1=X, 2=Y, 3=Z). Setting this argument to "I" or to + 0 will return a qubit target instead of a pauli target. + invert: Defaults to False. If True, the target is inverted (like "!X10"), + indicating that, for example, measurement results should be inverted). + + Examples: + >>> import stim + >>> circuit = stim.Circuit() + >>> circuit.append("MPP", [ + ... stim.target_pauli(2, "X"), + ... stim.target_combiner(), + ... stim.target_pauli(3, "y", invert=True), + ... stim.target_pauli(5, 3), + ... ]) + >>> circuit + stim.Circuit(''' + MPP X2*!Y3 Z5 + ''') + + >>> circuit.append("M", [ + ... stim.target_pauli(7, "I"), + ... ]) + >>> circuit + stim.Circuit(''' + MPP X2*!Y3 Z5 + M 7 + ''') + """ def target_rec( lookback_index: int, ) -> stim.GateTarget: diff --git a/file_lists/python_api_files b/file_lists/python_api_files index 25a80a28d..068bbe0fe 100644 --- a/file_lists/python_api_files +++ b/file_lists/python_api_files @@ -20,6 +20,7 @@ src/stim/simulators/frame_simulator.pybind.cc src/stim/simulators/matched_error.pybind.cc src/stim/simulators/measurements_to_detection_events.pybind.cc src/stim/simulators/tableau_simulator.pybind.cc +src/stim/stabilizers/flow.pybind.cc src/stim/stabilizers/pauli_string.pybind.cc src/stim/stabilizers/pauli_string_iter.pybind.cc src/stim/stabilizers/tableau.pybind.cc diff --git a/file_lists/test_files b/file_lists/test_files index 6506e3f6a..b08819aff 100644 --- a/file_lists/test_files +++ b/file_lists/test_files @@ -4,7 +4,6 @@ src/stim/circuit/circuit.test.cc src/stim/circuit/export_qasm.test.cc src/stim/circuit/gate_decomposition.test.cc src/stim/circuit/gate_target.test.cc -src/stim/circuit/stabilizer_flow.test.cc src/stim/cmd/command_analyze_errors.test.cc src/stim/cmd/command_convert.test.cc src/stim/cmd/command_detect.test.cc @@ -72,6 +71,7 @@ src/stim/simulators/transform_without_feedback.test.cc src/stim/simulators/vector_simulator.test.cc src/stim/stabilizers/conversions.test.cc src/stim/stabilizers/flex_pauli_string.test.cc +src/stim/stabilizers/flow.test.cc src/stim/stabilizers/pauli_string.test.cc src/stim/stabilizers/pauli_string_iter.test.cc src/stim/stabilizers/pauli_string_ref.test.cc diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi index f6a6d5105..7e476efcc 100644 --- a/glue/python/src/stim/__init__.pyi +++ b/glue/python/src/stim/__init__.pyi @@ -1311,16 +1311,66 @@ class Circuit: >>> circuit.get_final_qubit_coordinates() {1: [1.0, 2.0, 3.0]} """ + def has_all_flows( + self, + flows: Iterable[stim.Flow], + *, + unsigned: bool = False, + ) -> bool: + """Determines if the circuit has all the given stabilizer flow or not. + + This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster + because, behind the scenes, the circuit can be iterated once instead of once + per flow. + + Args: + flows: An iterable of `stim.Flow` instances representing the flows to check. + 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. + + Examples: + >>> import stim + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ]) + False + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> -Y'), + ... stim.Flow('Z -> X'), + ... ]) + True + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ], 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 has_flow( self, - shorthand: Optional[str] = None, + flow: stim.Flow, *, - start: Union[None, str, stim.PauliString] = None, - end: Union[None, str, 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. + """Determines if the circuit has the given 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 @@ -1334,26 +1384,7 @@ class Circuit: A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). Args: - shorthand: Specifies the flow as a short string like "X1 -> -YZ xor rec[1]". - The text must contain "->" to separate the input pauli string from the - output pauli string. Measurements are included by appending - " xor rec[k]" for each measurement index k. Indexing uses the python - convention where non-negative indices index from the start and negative - indices index from the end. The pauli strings are parsed as if by - `stim.PauliString.__init__`. - start: The input into the flow at the start of the circuit. Defaults to None - (the identity Pauli string). When specified, this should be a - `stim.PauliString`, or a `str` (which will be parsed using - `stim.PauliString.__init__`). - end: The output from the flow at the end of the circuit. Defaults to None - (the identity Pauli string). When specified, this should be a - `stim.PauliString`, or a `str` (which will be parsed using - `stim.PauliString.__init__`). - 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. + flow: The flow to check for. 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 @@ -1362,56 +1393,49 @@ class Circuit: 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 >>> m = stim.Circuit('M 0') - >>> m.has_flow('Z -> Z') + >>> m.has_flow(stim.Flow('Z -> Z')) True - >>> m.has_flow('X -> X') + >>> m.has_flow(stim.Flow('X -> X')) False - >>> m.has_flow('Z -> I') + >>> m.has_flow(stim.Flow('Z -> I')) False - >>> m.has_flow('Z -> I xor rec[-1]') + >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) True - >>> m.has_flow('Z -> rec[-1]') + >>> m.has_flow(stim.Flow('Z -> rec[-1]')) True >>> cx58 = stim.Circuit('CX 5 8') - >>> cx58.has_flow('X5 -> X5*X8') + >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) True - >>> cx58.has_flow('X_ -> XX') + >>> cx58.has_flow(stim.Flow('X_ -> XX')) False - >>> cx58.has_flow('_____X___ -> _____X__X') + >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) True >>> stim.Circuit(''' ... RY 0 - ... ''').has_flow( - ... end=stim.PauliString("Y"), - ... ) + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("Y"), + ... )) True >>> stim.Circuit(''' ... RY 0 - ... ''').has_flow( - ... end=stim.PauliString("X"), - ... ) + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("X"), + ... )) False >>> stim.Circuit(''' ... CX 0 1 - ... ''').has_flow( - ... start=stim.PauliString("+X_"), - ... end=stim.PauliString("+XX"), - ... ) + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_"), + ... output=stim.PauliString("+XX"), + ... )) True >>> stim.Circuit(''' @@ -1420,22 +1444,29 @@ class Circuit: ... MXX 0 1 ... MZZ 1 2 ... MX 1 - ... ''').has_flow( - ... start=stim.PauliString("+X_X"), - ... end=stim.PauliString("+__X"), + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_X"), + ... output=stim.PauliString("+__X"), ... measurements=[0, 2], - ... ) + ... )) True >>> stim.Circuit(''' ... H 0 ... ''').has_flow( - ... start=stim.PauliString("Y"), - ... end=stim.PauliString("Y"), + ... stim.Flow("Y -> Y"), ... unsigned=True, ... ) True + >>> stim.Circuit(''' + ... H 0 + ... ''').has_flow( + ... stim.Flow("Y -> Y"), + ... unsigned=False, + ... ) + False + 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 @@ -5159,6 +5190,156 @@ class FlippedMeasurement: For example, the fifth measurement in a circuit has a measurement record index of 4. """ +class Flow: + """A stabilizer flow (e.g. "XI -> XX xor rec[-1]"). + + Stabilizer circuits implement, and can be defined by, how they turn input + stabilizers into output stabilizers mediated by measurements. These + relationships are called stabilizer flows, and `stim.Flow` is a representation + of such a flow. For example, a `stim.Flow` can be given to + `stim.Circuit.has_flow` to verify that a circuit implements the flow. + + 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 the circuit transforms P into Q. + A flow like 1 -> P means the circuit prepares P. + A flow like P -> 1 means the circuit measures P. + A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). + + 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 + >>> c = stim.Circuit("CNOT 2 4") + + >>> c.has_flow(stim.Flow("__X__ -> __X_X")) + True + + >>> c.has_flow(stim.Flow("X2*X4 -> X2")) + True + + >>> c.has_flow(stim.Flow("Z4 -> Z4")) + False + """ + def __eq__( + self, + arg0: stim.Flow, + ) -> bool: + """Determines if two flows have identical contents. + """ + def __init__( + self, + arg: Union[None, str, stim.Flow] = None, + /, + *, + input: Optional[stim.PauliString] = None, + output: Optional[stim.PauliString] = None, + measurements: Optional[Iterable[Union[int, GateTarget]]] = None, + ) -> None: + """Initializes a stim.Flow. + + When given a string, the string is parsed as flow shorthand. For example, + the string "X_ -> ZZ xor rec[-1]" will result in a flow with input pauli string + "X_", output pauli string "ZZ", and measurement indices [-1]. + + Arguments: + arg [position-only]: Defaults to None. Must be specified by itself if used. + str: Initializes a flow by parsing the given shorthand text. + stim.Flow: Initializes a copy of the given flow. + None (default): Initializes an empty flow. + input: Defaults to None. Can be set to a stim.PauliString to directly + specify the flow's input stabilizer. + output: Defaults to None. Can be set to a stim.PauliString to directly + specify the flow's output stabilizer. + measurements: Can be set to a list of integers or gate targets like + `stim.target_rec(-1)`, to specify the measurements that mediate the + flow. Negative and positive measurement indices are allowed. Indexes + follow the python convention where -1 is the last measurement in a + circuit and 0 is the first measurement in a circuit. + + Examples: + >>> import stim + + >>> stim.Flow("X2 -> -Y2*Z4 xor rec[-1]") + stim.Flow("__X -> -__Y_Z xor rec[-1]") + + >>> stim.Flow("Z -> 1 xor rec[-1]") + stim.Flow("Z -> rec[-1]") + + >>> stim.Flow( + ... input=stim.PauliString("XX"), + ... output=stim.PauliString("_X"), + ... measurements=[], + ... ) + stim.Flow("XX -> _X") + """ + def __ne__( + self, + arg0: stim.Flow, + ) -> bool: + """Determines if two flows have non-identical contents. + """ + def __repr__( + self, + ) -> str: + """Returns valid python code evaluating to an equivalent `stim.Flow`. + """ + def __str__( + self, + ) -> str: + """Returns a shorthand description of the flow. + """ + def input_copy( + self, + ) -> stim.PauliString: + """Returns a copy of the flow's input stabilizer. + + Examples: + >>> import stim + >>> f = stim.Flow(input=stim.PauliString('XX')) + >>> f.input_copy() + stim.PauliString("+XX") + + >>> f.input_copy() is f.input_copy() + False + """ + def measurements_copy( + self, + ) -> List[int]: + """Returns a copy of the flow's measurement indices. + + Examples: + >>> import stim + >>> f = stim.Flow(measurements=[-1, 2]) + >>> f.measurements_copy() + [-1, 2] + + >>> f.measurements_copy() is f.measurements_copy() + False + """ + def output_copy( + self, + ) -> stim.PauliString: + """Returns a copy of the flow's output stabilizer. + + Examples: + >>> import stim + >>> f = stim.Flow(output=stim.PauliString('XX')) + >>> f.output_copy() + stim.PauliString("+XX") + + >>> f.output_copy() is f.output_copy() + False + """ class GateData: """Details about a gate supported by stim. @@ -5235,22 +5416,22 @@ class GateData: >>> for e in stim.gate_data('H').__unstable_flows: ... print(e) - +X -> +Z - +Z -> +X + X -> Z + Z -> X >>> for e in stim.gate_data('ISWAP').__unstable_flows: ... print(e) - +X_ -> +ZY - +Z_ -> +_Z - +_X -> +YZ - +_Z -> +Z_ + X_ -> ZY + Z_ -> _Z + _X -> YZ + _Z -> Z_ >>> for e in stim.gate_data('MXX').__unstable_flows: ... print(e) - +X_ -> +X_ - +_X -> +_X - +ZZ -> +ZZ - +XX -> rec[-1] + X_ -> X_ + _X -> _X + ZZ -> ZZ + XX -> rec[-1] """ @property def aliases( @@ -10525,6 +10706,33 @@ def read_shot_data_file( array([[False, False, False, False], [False, True, False, True]]) """ +def target_combined_paulis( + paulis: Union[stim.PauliString, List[stim.GateTarget]], + invert: bool = False, +) -> stim.GateTarget: + """Returns a list of targets encoding a pauli product for instructions like MPP. + + Args: + paulis: The paulis to encode into the targets. This can be a + `stim.PauliString` or a list of pauli targets from `stim.target_x`, + `stim.target_pauli`, etc. + invert: Defaults to False. If True, the product is inverted (like "!X2*Y3"). + Note that this is in addition to any inversions specified by the + `paulis` argument. + + Examples: + >>> import stim + >>> circuit = stim.Circuit() + >>> circuit.append("MPP", [ + ... *stim.target_combined_paulis(stim.PauliString("-XYZ")), + ... *stim.target_combined_paulis([stim.target_x(2), stim.target_y(5)]), + ... *stim.target_combined_paulis([stim.target_z(9)], invert=True), + ... ]) + >>> circuit + stim.Circuit(''' + MPP !X0*Y1*Z2 X2*Y5 !Z9 + ''') + """ def target_combiner( ) -> stim.GateTarget: """Returns a target combiner that can be used to build Pauli products. @@ -10588,6 +10796,45 @@ def target_logical_observable_id( error(0.25) L13 ''') """ +def target_pauli( + qubit_index: int, + pauli: Union[str, int], + invert: bool = False, +) -> stim.GateTarget: + """Returns a pauli target that can be passed into `stim.Circuit.append`. + + Args: + qubit_index: The qubit that the Pauli applies to. + pauli: The pauli gate to use. This can either be a string identifying the + pauli by name ("x", "X", "y", "Y", "z", or "Z") or an integer following + the convention (1=X, 2=Y, 3=Z). Setting this argument to "I" or to + 0 will return a qubit target instead of a pauli target. + invert: Defaults to False. If True, the target is inverted (like "!X10"), + indicating that, for example, measurement results should be inverted). + + Examples: + >>> import stim + >>> circuit = stim.Circuit() + >>> circuit.append("MPP", [ + ... stim.target_pauli(2, "X"), + ... stim.target_combiner(), + ... stim.target_pauli(3, "y", invert=True), + ... stim.target_pauli(5, 3), + ... ]) + >>> circuit + stim.Circuit(''' + MPP X2*!Y3 Z5 + ''') + + >>> circuit.append("M", [ + ... stim.target_pauli(7, "I"), + ... ]) + >>> circuit + stim.Circuit(''' + MPP X2*!Y3 Z5 + M 7 + ''') + """ def target_rec( lookback_index: int, ) -> stim.GateTarget: diff --git a/src/stim.h b/src/stim.h index c2777add7..3e6930bf4 100644 --- a/src/stim.h +++ b/src/stim.h @@ -9,7 +9,6 @@ #include "stim/circuit/export_qasm.h" #include "stim/circuit/gate_decomposition.h" #include "stim/circuit/gate_target.h" -#include "stim/circuit/stabilizer_flow.h" #include "stim/cmd/command_analyze_errors.h" #include "stim/cmd/command_convert.h" #include "stim/cmd/command_detect.h" @@ -99,6 +98,7 @@ #include "stim/simulators/vector_simulator.h" #include "stim/stabilizers/conversions.h" #include "stim/stabilizers/flex_pauli_string.h" +#include "stim/stabilizers/flow.h" #include "stim/stabilizers/pauli_string.h" #include "stim/stabilizers/pauli_string_iter.h" #include "stim/stabilizers/pauli_string_ref.h" diff --git a/src/stim/arg_parse.cc b/src/stim/arg_parse.cc index 24e943da6..a976f8e4b 100644 --- a/src/stim/arg_parse.cc +++ b/src/stim/arg_parse.cc @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -389,8 +390,8 @@ ostream_else_cout stim::find_output_stream_argument( return {std::move(f)}; } -std::vector stim::split(char splitter, const std::string &text) { - std::vector result; +std::vector stim::split_view(char splitter, std::string_view text) { + std::vector result; size_t start = 0; for (size_t k = 0; k < text.size(); k++) { if (text[k] == splitter) { @@ -402,31 +403,60 @@ std::vector stim::split(char splitter, const std::string &text) { return result; } -double stim::parse_exact_double_from_string(const std::string &text) { - char *end = nullptr; - const char *c = text.c_str(); - double d = strtod(c, &end); - if (text.size() > 0 && !isspace(*c)) { - if (end == c + text.size() && !std::isinf(d) && !std::isnan(d)) { - return d; - } +double stim::parse_exact_double_from_string(std::string_view text) { + double out = 0; + if (text.starts_with("+")) { + text = text.substr(1); + } + const char *end = text.data() + text.size(); + std::from_chars_result result = std::from_chars(text.data(), end, out); + if (result.ptr == end && !text.empty() && !isspace(text.front()) && !isspace(text.back()) && !std::isinf(out) && + !std::isnan(out)) { + return out; } - throw std::invalid_argument("Not an exact double: '" + text + "'"); + std::stringstream ss; + ss << "Not an exact finite double: '" << text << "'"; + throw std::invalid_argument(ss.str()); } -uint64_t stim::parse_exact_uint64_t_from_string(const std::string &text) { - char *end = nullptr; - const char *c = text.c_str(); - auto v = strtoull(c, &end, 10); - if (end == c + text.size()) { - // strtoull silently accepts spaces and negative signs and overflowing - // values. The only guaranteed way I've found to ensure it actually - // worked is to recreate the string and check that it's the same. - std::stringstream ss; - ss << v; - if (ss.str() == text) { - return v; +static bool try_parse_exact_uint64_t_from_string(std::string_view text, uint64_t *out) { + if (text.empty()) { + return false; + } + if (text[0] == '-') { + return false; + } + size_t k = 0; + if (text[k] == '+') { + k += 1; + } + uint64_t acc = 0; + while (k < text.size()) { + char c = text[k]; + if (c < '0' || c > '9') { + return false; } + if (acc > UINT64_MAX / 10) { + return false; + } + acc *= 10; + uint8_t d = c - '0'; + if (acc > UINT64_MAX - d) { + return false; + } + acc += d; + k++; } - throw std::invalid_argument("Not an integer that can be stored in a uint64_t: '" + text + "'"); + *out = acc; + return true; +} + +uint64_t stim::parse_exact_uint64_t_from_string(std::string_view text) { + uint64_t result = 0; + if (try_parse_exact_uint64_t_from_string(text, &result)) { + return result; + } + std::stringstream ss; + ss << "Not an exact integer that can be stored in a uint64_t: '" << text << "'"; + throw std::invalid_argument(ss.str()); } diff --git a/src/stim/arg_parse.h b/src/stim/arg_parse.h index 68baf9d4d..b246340d6 100644 --- a/src/stim/arg_parse.h +++ b/src/stim/arg_parse.h @@ -260,10 +260,10 @@ struct ostream_else_cout { /// Command line argument isn't present and default_std_out is false. ostream_else_cout find_output_stream_argument(const char *name, bool default_std_out, int argc, const char **argv); -std::vector split(char splitter, const std::string &text); +std::vector split_view(char splitter, std::string_view text); -double parse_exact_double_from_string(const std::string &text); -uint64_t parse_exact_uint64_t_from_string(const std::string &text); +double parse_exact_double_from_string(std::string_view text); +uint64_t parse_exact_uint64_t_from_string(std::string_view text); bool parse_int64(std::string_view data, int64_t *out); } // namespace stim diff --git a/src/stim/arg_parse.test.cc b/src/stim/arg_parse.test.cc index 5d8a1b593..8e72b6682 100644 --- a/src/stim/arg_parse.test.cc +++ b/src/stim/arg_parse.test.cc @@ -295,14 +295,14 @@ TEST(arg_parse, find_open_file_argument) { fclose(tmp); } -TEST(arg_parse, split) { - ASSERT_EQ(split(',', ""), (std::vector{""})); - ASSERT_EQ(split(',', "abc"), (std::vector{"abc"})); - ASSERT_EQ(split(',', ","), (std::vector{"", ""})); - ASSERT_EQ(split(',', "abc,"), (std::vector{"abc", ""})); - ASSERT_EQ(split(',', ",abc"), (std::vector{"", "abc"})); - ASSERT_EQ(split(',', "abc,def,ghi"), (std::vector{"abc", "def", "ghi"})); - ASSERT_EQ(split(',', "abc,def,ghi,"), (std::vector{"abc", "def", "ghi", ""})); +TEST(arg_parse, split_view) { + ASSERT_EQ(split_view(',', ""), (std::vector{""})); + ASSERT_EQ(split_view(',', "abc"), (std::vector{"abc"})); + ASSERT_EQ(split_view(',', ","), (std::vector{"", ""})); + ASSERT_EQ(split_view(',', "abc,"), (std::vector{"abc", ""})); + ASSERT_EQ(split_view(',', ",abc"), (std::vector{"", "abc"})); + ASSERT_EQ(split_view(',', "abc,def,ghi"), (std::vector{"abc", "def", "ghi"})); + ASSERT_EQ(split_view(',', "abc,def,ghi,"), (std::vector{"abc", "def", "ghi", ""})); } TEST(arg_parse, parse_exact_double_from_string) { @@ -320,6 +320,7 @@ TEST(arg_parse, parse_exact_double_from_string) { ASSERT_EQ(parse_exact_double_from_string("0"), 0); ASSERT_EQ(parse_exact_double_from_string("1"), 1); ASSERT_EQ(parse_exact_double_from_string("+1"), 1); + ASSERT_EQ(parse_exact_double_from_string("-1"), -1); ASSERT_EQ(parse_exact_double_from_string("1e3"), 1000); ASSERT_EQ(parse_exact_double_from_string("1.5"), 1.5); ASSERT_EQ(parse_exact_double_from_string("-1.5"), -1.5); @@ -343,7 +344,9 @@ TEST(arg_parse, parse_exact_uint64_t_from_string) { ASSERT_EQ(parse_exact_uint64_t_from_string("0"), 0); ASSERT_EQ(parse_exact_uint64_t_from_string("1"), 1); + ASSERT_EQ(parse_exact_uint64_t_from_string("+1"), 1); ASSERT_EQ(parse_exact_uint64_t_from_string("2"), 2); + ASSERT_EQ(parse_exact_uint64_t_from_string("13"), 13); ASSERT_EQ(parse_exact_uint64_t_from_string("18446744073709551615"), UINT64_MAX); } diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc index 6ee717ed5..d0e0c6349 100644 --- a/src/stim/circuit/circuit.pybind.cc +++ b/src/stim/circuit/circuit.pybind.cc @@ -20,7 +20,6 @@ #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" @@ -41,6 +40,7 @@ #include "stim/simulators/tableau_simulator.h" #include "stim/simulators/transform_without_feedback.h" #include "stim/stabilizers/conversions.h" +#include "stim/stabilizers/flow.h" #include "stim/stabilizers/pauli_string.pybind.h" using namespace stim; @@ -219,90 +219,6 @@ uint64_t obj_to_abs_detector_id(const pybind11::handle &obj, bool fail) { throw std::invalid_argument(ss.str()); } -FlexPauliString arg_to_pauli_string(const pybind11::object &arg) { - if (arg.is_none()) { - return FlexPauliString(PauliString(0)); - } else if (pybind11::isinstance(arg)) { - return pybind11::cast(arg); - } else if (pybind11::isinstance(arg)) { - return FlexPauliString::from_text(pybind11::cast(arg).c_str()); - } else { - throw std::invalid_argument( - "Don't know how to get a stim.PauliString from " + pybind11::cast(pybind11::repr(arg))); - } -} - -void append_measurements_from_args( - uint64_t num_circuit_measurements, - const pybind11::object &arg_measurements, - std::vector &out_measurements) { - if (arg_measurements.is_none()) { - return; - } - for (const pybind11::handle &e : arg_measurements) { - if (pybind11::isinstance(e)) { - auto d = pybind11::cast(e); - if (d.is_measurement_record_target()) { - out_measurements.push_back(d); - continue; - } - } else { - try { - int64_t s = pybind11::cast(e); - if (s >= 0 && s < (int64_t)num_circuit_measurements) { - s -= num_circuit_measurements; - } - if (s < 0 && -s <= (int64_t)num_circuit_measurements) { - out_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 args_to_flow( - uint64_t num_circuit_measurements, - const pybind11::object &shorthand, - const pybind11::object &start, - const pybind11::object &end, - const pybind11::object &measurements) { - StabilizerFlow flow{ - .input = PauliString{0}, - .output = PauliString{0}, - .measurement_outputs = {}, - }; - if (!shorthand.is_none() && !start.is_none()) { - throw std::invalid_argument("Can't specify both `shorthand` and `start`."); - } - if (!shorthand.is_none() && !end.is_none()) { - throw std::invalid_argument("Can't specify both `shorthand` and `end`."); - } - - if (!shorthand.is_none()) { - flow = StabilizerFlow::from_str( - pybind11::cast(shorthand).c_str(), num_circuit_measurements); - } else { - FlexPauliString in = arg_to_pauli_string(start); - FlexPauliString out = arg_to_pauli_string(end); - if (in.imag != out.imag) { - throw std::invalid_argument( - "The requested flow '" + in.str() + " -> " + out.str() + - "' is anti-Hermitian (unbalanced imaginary signs). Stabilizer flows are always Hermitian."); - } - flow.input = std::move(in.value); - flow.output = std::move(out.value); - } - - append_measurements_from_args(num_circuit_measurements, measurements, flow.measurement_outputs); - - return flow; -} - std::set obj_to_abs_detector_id_set( const pybind11::object &obj, const std::function &get_num_detectors) { std::set filter; @@ -2304,30 +2220,21 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ bool { - StabilizerFlow flow = - args_to_flow(self.count_measurements(), shorthand, start, end, measurements); + [](const Circuit &self, const Flow &flow, bool unsigned_only) -> bool { + std::span> flows = {&flow, &flow + 1}; if (unsigned_only) { - return check_if_circuit_has_unsigned_stabilizer_flows(self, &flow)[0]; + return check_if_circuit_has_unsigned_stabilizer_flows(self, flows)[0]; } else { auto rng = externally_seeded_rng(); - return sample_if_circuit_has_stabilizer_flows(256, rng, self, &flow)[0]; + return sample_if_circuit_has_stabilizer_flows(256, rng, self, flows)[0]; } }, - pybind11::arg("shorthand") = pybind11::none(), + pybind11::arg("flow"), 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, shorthand: Optional[str] = None, *, start: Union[None, str, stim.PauliString] = None, end: Union[None, str, 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. + @signature def has_flow(self, flow: stim.Flow, *, unsigned: bool = False) -> bool: + Determines if the circuit has the given 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 @@ -2341,26 +2248,7 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ 1 means the circuit contains a check (could be a DETECTOR). Args: - shorthand: Specifies the flow as a short string like "X1 -> -YZ xor rec[1]". - The text must contain "->" to separate the input pauli string from the - output pauli string. Measurements are included by appending - " xor rec[k]" for each measurement index k. Indexing uses the python - convention where non-negative indices index from the start and negative - indices index from the end. The pauli strings are parsed as if by - `stim.PauliString.__init__`. - start: The input into the flow at the start of the circuit. Defaults to None - (the identity Pauli string). When specified, this should be a - `stim.PauliString`, or a `str` (which will be parsed using - `stim.PauliString.__init__`). - end: The output from the flow at the end of the circuit. Defaults to None - (the identity Pauli string). When specified, this should be a - `stim.PauliString`, or a `str` (which will be parsed using - `stim.PauliString.__init__`). - 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. + flow: The flow to check for. 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 @@ -2369,56 +2257,49 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_>> import stim >>> m = stim.Circuit('M 0') - >>> m.has_flow('Z -> Z') + >>> m.has_flow(stim.Flow('Z -> Z')) True - >>> m.has_flow('X -> X') + >>> m.has_flow(stim.Flow('X -> X')) False - >>> m.has_flow('Z -> I') + >>> m.has_flow(stim.Flow('Z -> I')) False - >>> m.has_flow('Z -> I xor rec[-1]') + >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) True - >>> m.has_flow('Z -> rec[-1]') + >>> m.has_flow(stim.Flow('Z -> rec[-1]')) True >>> cx58 = stim.Circuit('CX 5 8') - >>> cx58.has_flow('X5 -> X5*X8') + >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) True - >>> cx58.has_flow('X_ -> XX') + >>> cx58.has_flow(stim.Flow('X_ -> XX')) False - >>> cx58.has_flow('_____X___ -> _____X__X') + >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) True >>> stim.Circuit(''' ... RY 0 - ... ''').has_flow( - ... end=stim.PauliString("Y"), - ... ) + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("Y"), + ... )) True >>> stim.Circuit(''' ... RY 0 - ... ''').has_flow( - ... end=stim.PauliString("X"), - ... ) + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("X"), + ... )) False >>> stim.Circuit(''' ... CX 0 1 - ... ''').has_flow( - ... start=stim.PauliString("+X_"), - ... end=stim.PauliString("+XX"), - ... ) + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_"), + ... output=stim.PauliString("+XX"), + ... )) True >>> stim.Circuit(''' @@ -2427,22 +2308,100 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_>> stim.Circuit(''' ... H 0 ... ''').has_flow( - ... start=stim.PauliString("Y"), - ... end=stim.PauliString("Y"), + ... stim.Flow("Y -> Y"), ... unsigned=True, ... ) True + >>> stim.Circuit(''' + ... H 0 + ... ''').has_flow( + ... stim.Flow("Y -> Y"), + ... unsigned=False, + ... ) + False + + 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( + "has_all_flows", + [](const Circuit &self, const std::vector> &flows, bool unsigned_only) -> bool { + std::vector results; + if (unsigned_only) { + results = check_if_circuit_has_unsigned_stabilizer_flows(self, flows); + } else { + auto rng = externally_seeded_rng(); + results = sample_if_circuit_has_stabilizer_flows(256, rng, self, flows); + } + for (auto b : results) { + if (!b) { + return false; + } + } + return true; + }, + pybind11::arg("flows"), + pybind11::kw_only(), + pybind11::arg("unsigned") = false, + clean_doc_string(R"DOC( + @signature def has_all_flows(self, flows: Iterable[stim.Flow], *, unsigned: bool = False) -> bool: + Determines if the circuit has all the given stabilizer flow or not. + + This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster + because, behind the scenes, the circuit can be iterated once instead of once + per flow. + + Args: + flows: An iterable of `stim.Flow` instances representing the flows to check. + 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. + + Examples: + >>> import stim + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ]) + False + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> -Y'), + ... stim.Flow('Z -> X'), + ... ]) + True + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ], 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 diff --git a/src/stim/circuit/circuit_pybind_test.py b/src/stim/circuit/circuit_pybind_test.py index 31fe6510f..4dca08503 100644 --- a/src/stim/circuit/circuit_pybind_test.py +++ b/src/stim/circuit/circuit_pybind_test.py @@ -1579,12 +1579,12 @@ 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) + assert c.has_flow(stim.Flow("1 -> Y")) + assert not c.has_flow(stim.Flow("1 -> -Y")) + assert not c.has_flow(stim.Flow("1 -> X")) + assert c.has_flow(stim.Flow("1 -> Y"), unsigned=True) + assert not c.has_flow(stim.Flow("1 -> X"), unsigned=True) + assert c.has_flow(stim.Flow("1 -> -Y"), unsigned=True) def test_has_flow_cxs(): @@ -1593,15 +1593,15 @@ def test_has_flow_cxs(): 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(stim.Flow("X_ -> YX")) + assert c.has_flow(stim.Flow("Y_ -> -XX")) + assert not c.has_flow(stim.Flow("X_ -> XX")) + assert not c.has_flow(stim.Flow("X_ -> -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) + assert c.has_flow(stim.Flow("X_ -> YX"), unsigned=True) + assert c.has_flow(stim.Flow("Y_ -> -XX"), unsigned=True) + assert not c.has_flow(stim.Flow("X_ -> XX"), unsigned=True) + assert not c.has_flow(stim.Flow("X_ -> -XX"), unsigned=True) def test_has_flow_cxm(): @@ -1609,14 +1609,14 @@ def test_has_flow_cxm(): 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) + assert c.has_flow(stim.Flow("1 -> _Z xor rec[0]")) + assert c.has_flow(stim.Flow("ZZ -> rec[0]")) + assert c.has_flow(stim.Flow("ZZ -> _Z")) + assert c.has_flow(stim.Flow("XX -> X_")) + assert c.has_flow(stim.Flow("1 -> _Z xor rec[0]"), unsigned=True) + assert c.has_flow(stim.Flow("ZZ -> rec[0]"), unsigned=True) + assert c.has_flow(stim.Flow("ZZ -> _Z"), unsigned=True) + assert c.has_flow(stim.Flow("XX -> X_"), unsigned=True) def test_has_flow_lattice_surgery(): @@ -1631,16 +1631,16 @@ def test_has_flow_lattice_surgery(): 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 c.has_flow(stim.Flow("X_ -> YX")) + assert c.has_flow(stim.Flow("Z_ -> Z_")) + assert c.has_flow(stim.Flow("_X -> _X")) + assert c.has_flow(stim.Flow("_Z -> ZZ")) + assert not c.has_flow(stim.Flow("X_ -> 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) + assert not c.has_flow(stim.Flow("X_ -> XX")) + assert not c.has_flow(stim.Flow("X_ -> -YX")) + assert not c.has_flow(stim.Flow("X_ -> XX"), unsigned=True) + assert c.has_flow(stim.Flow("X_ -> -YX"), unsigned=True) def test_has_flow_lattice_surgery_without_feedback(): @@ -1653,16 +1653,26 @@ def test_has_flow_lattice_surgery_without_feedback(): 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]) + assert c.has_flow(stim.Flow("X_ -> YX xor rec[1]")) + assert c.has_flow(stim.Flow("Z_ -> Z_")) + assert c.has_flow(stim.Flow("_X -> _X")) + assert c.has_flow(stim.Flow("_Z -> ZZ xor rec[0] xor rec[2]")) + assert not c.has_flow(stim.Flow("X_ -> XX")) + assert c.has_all_flows([]) + assert c.has_all_flows([ + stim.Flow("X_ -> YX xor rec[1]"), + stim.Flow("Z_ -> Z_"), + ]) + assert not c.has_all_flows([ + stim.Flow("X_ -> YX xor rec[1]"), + stim.Flow("Z_ -> Z_"), + stim.Flow("X_ -> XX"), + ]) + + assert not c.has_flow(stim.Flow("X_ -> XX")) + assert not c.has_flow(stim.Flow("X_ -> -YX")) + assert not c.has_flow(stim.Flow("X_ -> XX"), unsigned=True) + assert c.has_flow(stim.Flow("X_ -> -YX xor rec[1]"), unsigned=True) def test_has_flow_shorthands(): @@ -1673,18 +1683,17 @@ def test_has_flow_shorthands(): MX 99 """) - assert c.has_flow("X_ -> XX xor rec[1] xor rec[3]") - assert c.has_flow("Z_ -> Z_") - assert c.has_flow("_X -> _X") - assert c.has_flow("_Z -> ZZ", measurements=[0, 2]) - assert c.has_flow("_Z -> ZZ xor rec[0]", measurements=[2]) - - assert c.has_flow(start="X_", end="XX", measurements=[1, 3]) - assert not c.has_flow("Z_ -> -Z_") - assert not c.has_flow("-Z_ -> Z_") - assert not c.has_flow("Z_ -> X_") - assert c.has_flow("iX_ -> iXX xor rec[1] xor rec[3]") - assert not c.has_flow("-iX_ -> iXX xor rec[1] xor rec[3]") - assert c.has_flow("-iX_ -> -iXX xor rec[1] xor rec[3]") + assert c.has_flow(stim.Flow("X_ -> XX xor rec[1] xor rec[3]")) + assert c.has_flow(stim.Flow("Z_ -> Z_")) + assert c.has_flow(stim.Flow("_X -> _X")) + assert c.has_flow(stim.Flow("_Z -> ZZ xor rec[0] xor rec[2]")) + + assert c.has_flow(stim.Flow("X_ -> XX xor rec[1] xor rec[3]")) + assert not c.has_flow(stim.Flow("Z_ -> -Z_")) + assert not c.has_flow(stim.Flow("-Z_ -> Z_")) + assert not c.has_flow(stim.Flow("Z_ -> X_")) + assert c.has_flow(stim.Flow("iX_ -> iXX xor rec[1] xor rec[3]")) + assert not c.has_flow(stim.Flow("-iX_ -> iXX xor rec[1] xor rec[3]")) + assert c.has_flow(stim.Flow("-iX_ -> -iXX xor rec[1] xor rec[3]")) with pytest.raises(ValueError): - c.has_flow("iX_ -> XX") + stim.Flow("iX_ -> XX") diff --git a/src/stim/circuit/stabilizer_flow.test.cc b/src/stim/circuit/stabilizer_flow.test.cc deleted file mode 100644 index e11511e84..000000000 --- a/src/stim/circuit/stabilizer_flow.test.cc +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "stim/circuit/stabilizer_flow.h" - -#include "gtest/gtest.h" - -#include "stim/circuit/circuit.h" -#include "stim/mem/simd_word.test.h" -#include "stim/test_util.test.h" - -using namespace stim; - -TEST_EACH_WORD_SIZE_W(stabilizer_flow, from_str, { - ASSERT_THROW({ StabilizerFlow::from_str(""); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X>X"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X-X"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X > X"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X - X"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("->X"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X->"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("rec[0] -> X"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X -> rec[ -1]"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X -> X rec[-1]"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X -> X xor"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X -> rec[-1] xor X"); }, std::invalid_argument); - ASSERT_THROW({ StabilizerFlow::from_str("X -> rec[55]"); }, std::invalid_argument); - - ASSERT_EQ( - StabilizerFlow::from_str("1 -> 1"), - (StabilizerFlow{ - .input = PauliString::from_str(""), - .output = PauliString::from_str(""), - .measurement_outputs = {}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("i -> -i"), - (StabilizerFlow{ - .input = PauliString::from_str(""), - .output = PauliString::from_str("-"), - .measurement_outputs = {}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("iX -> -iY"), - (StabilizerFlow{ - .input = PauliString::from_str("X"), - .output = PauliString::from_str("-Y"), - .measurement_outputs = {}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("X->-Y"), - (StabilizerFlow{ - .input = PauliString::from_str("X"), - .output = PauliString::from_str("-Y"), - .measurement_outputs = {}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("X -> -Y"), - (StabilizerFlow{ - .input = PauliString::from_str("X"), - .output = PauliString::from_str("-Y"), - .measurement_outputs = {}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("-X -> Y"), - (StabilizerFlow{ - .input = PauliString::from_str("-X"), - .output = PauliString::from_str("Y"), - .measurement_outputs = {}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("XYZ -> -Z_Z"), - (StabilizerFlow{ - .input = PauliString::from_str("XYZ"), - .output = PauliString::from_str("-Z_Z"), - .measurement_outputs = {}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("XYZ -> Z_Y xor rec[-1]"), - (StabilizerFlow{ - .input = PauliString::from_str("XYZ"), - .output = PauliString::from_str("Z_Y"), - .measurement_outputs = {GateTarget::rec(-1)}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("XYZ -> rec[-1]"), - (StabilizerFlow{ - .input = PauliString::from_str("XYZ"), - .output = PauliString::from_str(""), - .measurement_outputs = {GateTarget::rec(-1)}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("XYZ -> Z_Y xor rec[-1] xor rec[-3]"), - (StabilizerFlow{ - .input = PauliString::from_str("XYZ"), - .output = PauliString::from_str("Z_Y"), - .measurement_outputs = {GateTarget::rec(-1), GateTarget::rec(-3)}, - })); - ASSERT_EQ( - StabilizerFlow::from_str("XYZ -> ZIY xor rec[55] xor rec[-3]", 100), - (StabilizerFlow{ - .input = PauliString::from_str("XYZ"), - .output = PauliString::from_str("Z_Y"), - .measurement_outputs = {GateTarget::rec(-45), GateTarget::rec(-3)}, - })); -}); - -TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows, { - auto rng = INDEPENDENT_TEST_RNG(); - auto results = sample_if_circuit_has_stabilizer_flows( - 256, - rng, - 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, 0, 1, 0})); -}) - -TEST_EACH_WORD_SIZE_W(stabilizer_flow, str_and_from_str, { - auto flow = StabilizerFlow{ - PauliString::from_str("XY"), - PauliString::from_str("_Z"), - {GateTarget::rec(-3)}, - }; - auto s = "+XY -> +_Z xor rec[-3]"; - ASSERT_EQ(flow.str(), s); - ASSERT_EQ(StabilizerFlow::from_str(s), flow); - - ASSERT_EQ( - StabilizerFlow::from_str("1 -> rec[-1]"), - (StabilizerFlow{PauliString(0), PauliString(0), {GateTarget::rec(-1)}})); - - ASSERT_EQ( - StabilizerFlow::from_str("-1 -> -X xor rec[-1] xor rec[-3]"), - (StabilizerFlow{ - PauliString::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/cmd/command_diagram.cc b/src/stim/cmd/command_diagram.cc index bdfe80fcc..04eadf028 100644 --- a/src/stim/cmd/command_diagram.cc +++ b/src/stim/cmd/command_diagram.cc @@ -94,7 +94,7 @@ std::vector _read_coord_filter(int argc, const char **argv) { } std::vector result; - for (const auto &term : split(':', arg)) { + for (std::string_view term : split_view(':', arg)) { result.push_back(CoordFilter::parse_from(term)); } return result; diff --git a/src/stim/cmd/command_help.cc b/src/stim/cmd/command_help.cc index 909bb6965..0bbee7027 100644 --- a/src/stim/cmd/command_help.cc +++ b/src/stim/cmd/command_help.cc @@ -31,10 +31,10 @@ #include "command_sample.h" #include "command_sample_dem.h" #include "stim/arg_parse.h" -#include "stim/circuit/stabilizer_flow.h" #include "stim/cmd/command_analyze_errors.h" #include "stim/gates/gates.h" #include "stim/io/stim_data_formats.h" +#include "stim/stabilizers/flow.h" #include "stim/stabilizers/tableau.h" using namespace stim; diff --git a/src/stim/diagram/detector_slice/detector_slice_set.cc b/src/stim/diagram/detector_slice/detector_slice_set.cc index e600531b6..100b16cd8 100644 --- a/src/stim/diagram/detector_slice/detector_slice_set.cc +++ b/src/stim/diagram/detector_slice/detector_slice_set.cc @@ -192,7 +192,7 @@ bool CoordFilter::matches(stim::SpanRef coords, stim::DemTarget ta } return true; } -CoordFilter CoordFilter::parse_from(const std::string &data) { +CoordFilter CoordFilter::parse_from(std::string_view data) { CoordFilter filter; if (data.empty()) { // no filter @@ -203,7 +203,7 @@ CoordFilter CoordFilter::parse_from(const std::string &data) { filter.use_target = true; filter.exact_target = DemTarget::observable_id(parse_exact_double_from_string(data.substr(1))); } else { - for (const auto &v : split(',', data)) { + for (const auto &v : split_view(',', data)) { if (v == "*") { filter.coordinates.push_back(std::numeric_limits::quiet_NaN()); } else { diff --git a/src/stim/diagram/detector_slice/detector_slice_set.h b/src/stim/diagram/detector_slice/detector_slice_set.h index 41f3a85f0..82056a23f 100644 --- a/src/stim/diagram/detector_slice/detector_slice_set.h +++ b/src/stim/diagram/detector_slice/detector_slice_set.h @@ -32,7 +32,7 @@ struct CoordFilter { stim::DemTarget exact_target{}; bool matches(stim::SpanRef coords, stim::DemTarget target) const; - static CoordFilter parse_from(const std::string &data); + static CoordFilter parse_from(std::string_view data); }; struct DetectorSliceSet { diff --git a/src/stim/gates/gates.h b/src/stim/gates/gates.h index f6f9c577f..0edd527a9 100644 --- a/src/stim/gates/gates.h +++ b/src/stim/gates/gates.h @@ -32,7 +32,7 @@ template struct Tableau; template -struct StabilizerFlow; +struct Flow; template struct PauliString; @@ -255,25 +255,25 @@ struct Gate { } template - std::vector> flows() const { + std::vector> flows() const { if (flags & GateFlags::GATE_IS_UNITARY) { auto t = tableau(); if (flags & GateFlags::GATE_TARGETS_PAIRS) { return { - StabilizerFlow{stim::PauliString::from_str("X_"), t.xs[0], {}}, - StabilizerFlow{stim::PauliString::from_str("Z_"), t.zs[0], {}}, - StabilizerFlow{stim::PauliString::from_str("_X"), t.xs[1], {}}, - StabilizerFlow{stim::PauliString::from_str("_Z"), t.zs[1], {}}, + Flow{stim::PauliString::from_str("X_"), t.xs[0], {}}, + Flow{stim::PauliString::from_str("Z_"), t.zs[0], {}}, + Flow{stim::PauliString::from_str("_X"), t.xs[1], {}}, + Flow{stim::PauliString::from_str("_Z"), t.zs[1], {}}, }; } return { - StabilizerFlow{stim::PauliString::from_str("X"), t.xs[0], {}}, - StabilizerFlow{stim::PauliString::from_str("Z"), t.zs[0], {}}, + Flow{stim::PauliString::from_str("X"), t.xs[0], {}}, + Flow{stim::PauliString::from_str("Z"), t.zs[0], {}}, }; } - std::vector> out; + std::vector> out; for (const auto &c : flow_data) { - out.push_back(StabilizerFlow::from_str(c)); + out.push_back(Flow::from_str(c)); } return out; } diff --git a/src/stim/gates/gates.pybind.cc b/src/stim/gates/gates.pybind.cc index a70b60de7..583938461 100644 --- a/src/stim/gates/gates.pybind.cc +++ b/src/stim/gates/gates.pybind.cc @@ -14,9 +14,9 @@ #include "stim/gates/gates.h" -#include "stim/circuit/stabilizer_flow.h" #include "stim/gates/gates.pybind.h" #include "stim/py/base.pybind.h" +#include "stim/stabilizers/flow.h" #include "stim/str_util.h" using namespace stim; @@ -478,22 +478,22 @@ void stim_pybind::pybind_gate_data_methods(pybind11::module &m, pybind11::class_ >>> for e in stim.gate_data('H').__unstable_flows: ... print(e) - +X -> +Z - +Z -> +X + X -> Z + Z -> X >>> for e in stim.gate_data('ISWAP').__unstable_flows: ... print(e) - +X_ -> +ZY - +Z_ -> +_Z - +_X -> +YZ - +_Z -> +Z_ + X_ -> ZY + Z_ -> _Z + _X -> YZ + _Z -> Z_ >>> for e in stim.gate_data('MXX').__unstable_flows: ... print(e) - +X_ -> +X_ - +_X -> +_X - +ZZ -> +ZZ - +XX -> rec[-1] + X_ -> X_ + _X -> _X + ZZ -> ZZ + XX -> rec[-1] )DOC") .data()); diff --git a/src/stim/gates/gates.test.cc b/src/stim/gates/gates.test.cc index c2879d2f6..19adc114d 100644 --- a/src/stim/gates/gates.test.cc +++ b/src/stim/gates/gates.test.cc @@ -17,9 +17,9 @@ #include "gtest/gtest.h" #include "stim/circuit/circuit.h" -#include "stim/circuit/stabilizer_flow.h" #include "stim/mem/simd_word.test.h" #include "stim/simulators/tableau_simulator.h" +#include "stim/stabilizers/flow.h" #include "stim/test_util.test.h" using namespace stim; diff --git a/src/stim/py/stim.pybind.cc b/src/stim/py/stim.pybind.cc index 0c34f7ff1..9f1e05a2f 100644 --- a/src/stim/py/stim.pybind.cc +++ b/src/stim/py/stim.pybind.cc @@ -36,6 +36,7 @@ #include "stim/simulators/matched_error.pybind.h" #include "stim/simulators/measurements_to_detection_events.pybind.h" #include "stim/simulators/tableau_simulator.pybind.h" +#include "stim/stabilizers/flow.pybind.h" #include "stim/stabilizers/pauli_string.pybind.h" #include "stim/stabilizers/pauli_string_iter.pybind.h" #include "stim/stabilizers/tableau.h" @@ -92,6 +93,95 @@ GateTarget target_z(const pybind11::object &qubit, bool invert) { return GateTarget::z(pybind11::cast(qubit), invert); } +std::vector target_combined_paulis(const pybind11::object &paulis, bool invert) { + std::vector result; + if (pybind11::isinstance(paulis)) { + const FlexPauliString &ps = pybind11::cast(paulis); + if (ps.imag) { + std::stringstream ss; + ss << "Imaginary sign: paulis="; + ss << paulis; + throw std::invalid_argument(ss.str()); + } + invert ^= ps.value.sign; + for (size_t q = 0; q < ps.value.num_qubits; q++) { + bool x = ps.value.xs[q]; + bool z = ps.value.zs[q]; + if (x | z) { + result.push_back(GateTarget::pauli_xz(q, x, z)); + result.push_back(GateTarget::combiner()); + } + } + } else { + for (const auto &h : paulis) { + if (pybind11::isinstance(h)) { + GateTarget g = pybind11::cast(h); + if (g.pauli_type() != 'I') { + if (g.is_inverted_result_target()) { + invert ^= true; + g.data ^= TARGET_INVERTED_BIT; + } + result.push_back(g); + result.push_back(GateTarget::combiner()); + continue; + } + } + + std::stringstream ss; + ss << "Expected a pauli string or iterable of stim.GateTarget but got this when iterating: "; + ss << h; + throw std::invalid_argument(ss.str()); + } + } + + if (result.empty()) { + std::stringstream ss; + ss << "Identity pauli product: paulis="; + ss << paulis; + throw std::invalid_argument(ss.str()); + } + result.pop_back(); + if (invert) { + result[0].data ^= TARGET_INVERTED_BIT; + } + return result; +} + +GateTarget target_pauli(uint32_t qubit_index, const pybind11::object &pauli, bool invert) { + if ((qubit_index & TARGET_VALUE_MASK) != qubit_index) { + std::stringstream ss; + ss << "qubit_index=" << qubit_index << " is too large. Maximum qubit index is " << TARGET_VALUE_MASK << "."; + throw std::invalid_argument(ss.str()); + } + if (pybind11::isinstance(pauli)) { + std::string p = pybind11::cast(pauli); + if (p == "X" || p == "x") { + return GateTarget::x(qubit_index, invert); + } else if (p == "Y" || p == "y") { + return GateTarget::y(qubit_index, invert); + } else if (p == "Z" || p == "z") { + return GateTarget::z(qubit_index, invert); + } else if (p == "I") { + return GateTarget::qubit(qubit_index, invert); + } + } else if (pybind11::isinstance(pauli)) { + uint8_t p = pybind11::cast(pauli); + if (p == 1) { + return GateTarget::x(qubit_index, invert); + } else if (p == 2) { + return GateTarget::y(qubit_index, invert); + } else if (p == 3) { + return GateTarget::z(qubit_index, invert); + } else if (p == 0) { + return GateTarget::qubit(qubit_index, invert); + } + } + + std::stringstream ss; + ss << "Expected pauli in [0, 1, 2, 3, *'IXYZxyz'] but got pauli=" << pauli; + throw std::invalid_argument(ss.str()); +} + GateTarget target_sweep_bit(uint32_t qubit) { return GateTarget::sweep_bit(qubit); } @@ -299,6 +389,82 @@ void top_level(pybind11::module &m) { )DOC") .data()); + m.def( + "target_pauli", + &target_pauli, + pybind11::arg("qubit_index"), + pybind11::arg("pauli"), + pybind11::arg("invert") = false, + clean_doc_string(R"DOC( + @signature def target_pauli(qubit_index: int, pauli: Union[str, int], invert: bool = False) -> stim.GateTarget: + Returns a pauli target that can be passed into `stim.Circuit.append`. + + Args: + qubit_index: The qubit that the Pauli applies to. + pauli: The pauli gate to use. This can either be a string identifying the + pauli by name ("x", "X", "y", "Y", "z", or "Z") or an integer following + the convention (1=X, 2=Y, 3=Z). Setting this argument to "I" or to + 0 will return a qubit target instead of a pauli target. + invert: Defaults to False. If True, the target is inverted (like "!X10"), + indicating that, for example, measurement results should be inverted). + + Examples: + >>> import stim + >>> circuit = stim.Circuit() + >>> circuit.append("MPP", [ + ... stim.target_pauli(2, "X"), + ... stim.target_combiner(), + ... stim.target_pauli(3, "y", invert=True), + ... stim.target_pauli(5, 3), + ... ]) + >>> circuit + stim.Circuit(''' + MPP X2*!Y3 Z5 + ''') + + >>> circuit.append("M", [ + ... stim.target_pauli(7, "I"), + ... ]) + >>> circuit + stim.Circuit(''' + MPP X2*!Y3 Z5 + M 7 + ''') + )DOC") + .data()); + + m.def( + "target_combined_paulis", + &target_combined_paulis, + pybind11::arg("paulis"), + pybind11::arg("invert") = false, + clean_doc_string(R"DOC( + @signature def target_combined_paulis(paulis: Union[stim.PauliString, List[stim.GateTarget]], invert: bool = False) -> stim.GateTarget: + Returns a list of targets encoding a pauli product for instructions like MPP. + + Args: + paulis: The paulis to encode into the targets. This can be a + `stim.PauliString` or a list of pauli targets from `stim.target_x`, + `stim.target_pauli`, etc. + invert: Defaults to False. If True, the product is inverted (like "!X2*Y3"). + Note that this is in addition to any inversions specified by the + `paulis` argument. + + Examples: + >>> import stim + >>> circuit = stim.Circuit() + >>> circuit.append("MPP", [ + ... *stim.target_combined_paulis(stim.PauliString("-XYZ")), + ... *stim.target_combined_paulis([stim.target_x(2), stim.target_y(5)]), + ... *stim.target_combined_paulis([stim.target_z(9)], invert=True), + ... ]) + >>> circuit + stim.Circuit(''' + MPP !X0*Y1*Z2 X2*Y5 !Z9 + ''') + )DOC") + .data()); + m.def( "target_sweep_bit", &target_sweep_bit, @@ -444,6 +610,7 @@ PYBIND11_MODULE(STIM_PYBIND11_MODULE_NAME, m) { auto c_circuit_targets_inside_instruction = pybind_circuit_targets_inside_instruction(m); auto c_circuit_error_location = pybind_circuit_error_location(m); auto c_circuit_error_location_methods = pybind_explained_error(m); + auto c_flow = pybind_flow(m); auto c_diagram_helper = pybind_diagram(m); @@ -484,6 +651,7 @@ PYBIND11_MODULE(STIM_PYBIND11_MODULE_NAME, m) { pybind_circuit_targets_inside_instruction_methods(m, c_circuit_targets_inside_instruction); pybind_circuit_error_location_methods(m, c_circuit_error_location); pybind_explained_error_methods(m, c_circuit_error_location_methods); + pybind_flow_methods(m, c_flow); pybind_diagram_methods(m, c_diagram_helper); } diff --git a/src/stim/py/stim_pybind_test.py b/src/stim/py/stim_pybind_test.py index 34db65338..22b1a9eb3 100644 --- a/src/stim/py/stim_pybind_test.py +++ b/src/stim/py/stim_pybind_test.py @@ -197,3 +197,92 @@ def test_target_methods_accept_gate_targets(): with pytest.raises(ValueError): stim.target_z(stim.target_sweep_bit(4)) + + +def test_target_pauli(): + assert stim.target_pauli(2, "I") == stim.GateTarget(2) + assert stim.target_pauli(2, "X") == stim.target_x(2) + assert stim.target_pauli(2, "Y") == stim.target_y(2) + assert stim.target_pauli(2, "Z") == stim.target_z(2) + assert stim.target_pauli(5, "x") == stim.target_x(5) + assert stim.target_pauli(2, "y") == stim.target_y(2) + assert stim.target_pauli(2, "z") == stim.target_z(2) + assert stim.target_pauli(2, 0) == stim.GateTarget(2) + assert stim.target_pauli(2, 1) == stim.target_x(2) + assert stim.target_pauli(2, 2) == stim.target_y(2) + assert stim.target_pauli(2, 3) == stim.target_z(2) + assert stim.target_pauli(2, 3, True) == stim.target_z(2, True) + assert stim.target_pauli(qubit_index=2, pauli=3, invert=True) == stim.target_z(2, True) + + with pytest.raises(ValueError, match="too large"): + stim.target_pauli(2**31, 'X') + with pytest.raises(ValueError, match="Expected pauli"): + stim.target_pauli(5, 'F') + + +def test_target_combined_paulis(): + assert stim.target_combined_paulis(stim.PauliString("XYZ")) == [ + stim.target_x(0), + stim.target_combiner(), + stim.target_y(1), + stim.target_combiner(), + stim.target_z(2), + ] + + assert stim.target_combined_paulis(stim.PauliString("X"), True) == [ + stim.target_x(0, True), + ] + + assert stim.target_combined_paulis(stim.PauliString("-XYIZ")) == [ + stim.target_x(0, invert=True), + stim.target_combiner(), + stim.target_y(1), + stim.target_combiner(), + stim.target_z(3), + ] + + assert stim.target_combined_paulis(stim.PauliString("-XYIZ"), True) == [ + stim.target_x(0), + stim.target_combiner(), + stim.target_y(1), + stim.target_combiner(), + stim.target_z(3), + ] + + assert stim.target_combined_paulis([stim.target_x(5), stim.target_z(9)]) == [ + stim.target_x(5), + stim.target_combiner(), + stim.target_z(9), + ] + + assert stim.target_combined_paulis([stim.target_x(5, True), stim.target_z(9)]) == [ + stim.target_x(5, True), + stim.target_combiner(), + stim.target_z(9), + ] + assert stim.target_combined_paulis([stim.target_x(5), stim.target_z(9, True)]) == [ + stim.target_x(5, True), + stim.target_combiner(), + stim.target_z(9), + ] + assert stim.target_combined_paulis([stim.target_x(5), stim.target_z(9)], True) == [ + stim.target_x(5, True), + stim.target_combiner(), + stim.target_z(9), + ] + assert stim.target_combined_paulis([stim.target_y(4)]) == [ + stim.target_y(4), + ] + + with pytest.raises(ValueError, match="Expected a pauli string"): + stim.target_combined_paulis([stim.target_rec(-2)]) + with pytest.raises(ValueError, match="Expected a pauli string"): + stim.target_combined_paulis([object()]) + with pytest.raises(ValueError, match="Identity pauli product"): + stim.target_combined_paulis([]) + with pytest.raises(ValueError, match="Identity pauli product"): + stim.target_combined_paulis(stim.PauliString(0)) + with pytest.raises(ValueError, match="Identity pauli product"): + stim.target_combined_paulis(stim.PauliString(10)) + with pytest.raises(ValueError, match="Imaginary"): + stim.target_combined_paulis(stim.PauliString("iX")) diff --git a/src/stim/stabilizers/flex_pauli_string.cc b/src/stim/stabilizers/flex_pauli_string.cc index 82a758457..a98733c5a 100644 --- a/src/stim/stabilizers/flex_pauli_string.cc +++ b/src/stim/stabilizers/flex_pauli_string.cc @@ -227,9 +227,12 @@ static void parse_sparse_pauli_string(std::string_view text, FlexPauliString *ou if (cur_pauli == '\0' || !has_cur_index || cur_index > out->value.num_qubits) { throw std::invalid_argument(""); } - out->value.right_mul_pauli( - GateTarget::pauli_xz(cur_index, cur_pauli == 'X' || cur_pauli == 'Y', cur_pauli == 'Z' || cur_pauli == 'Y'), - &out->imag); + if (cur_pauli != 'I') { + out->value.right_mul_pauli( + GateTarget::pauli_xz( + cur_index, cur_pauli == 'X' || cur_pauli == 'Y', cur_pauli == 'Z' || cur_pauli == 'Y'), + &out->imag); + } has_cur_index = false; cur_pauli = '\0'; cur_index = 0; diff --git a/src/stim/stabilizers/flex_pauli_string.test.cc b/src/stim/stabilizers/flex_pauli_string.test.cc index 6e7b5f600..7a14ccced 100644 --- a/src/stim/stabilizers/flex_pauli_string.test.cc +++ b/src/stim/stabilizers/flex_pauli_string.test.cc @@ -75,4 +75,6 @@ TEST(flex_pauli_string, from_text) { ASSERT_EQ(f.value.zs.as_u64(), 0b100000100); ASSERT_EQ(FlexPauliString::from_text("X1"), FlexPauliString::from_text("_X")); + + ASSERT_EQ(FlexPauliString::from_text("X20*I21"), FlexPauliString::from_text("____________________X_")); } diff --git a/src/stim/circuit/stabilizer_flow.h b/src/stim/stabilizers/flow.h similarity index 73% rename from src/stim/circuit/stabilizer_flow.h rename to src/stim/stabilizers/flow.h index d3d53027b..b5046d203 100644 --- a/src/stim/circuit/stabilizer_flow.h +++ b/src/stim/stabilizers/flow.h @@ -14,8 +14,8 @@ * limitations under the License. */ -#ifndef _STIM_CIRCUIT_STABILIZER_FLOW_H -#define _STIM_CIRCUIT_STABILIZER_FLOW_H +#ifndef _STIM_STABILIZERS_FLOW_H +#define _STIM_STABILIZERS_FLOW_H #include #include @@ -26,14 +26,15 @@ namespace stim { template -struct StabilizerFlow { +struct Flow { stim::PauliString input; stim::PauliString output; - std::vector measurement_outputs; + /// Indexing follows python convention: -1 is the last element, 0 is the first element. + std::vector measurements; - static StabilizerFlow from_str(const char *text, uint64_t num_measurements_for_non_neg_recs = 0); - bool operator==(const StabilizerFlow &other) const; - bool operator!=(const StabilizerFlow &other) const; + static Flow from_str(std::string_view text); + bool operator==(const Flow &other) const; + bool operator!=(const Flow &other) const; std::string str() const; }; @@ -51,17 +52,17 @@ struct StabilizerFlow { /// k'th flow passed all checks. template std::vector sample_if_circuit_has_stabilizer_flows( - size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, SpanRef> flows); + size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, std::span> flows); template std::vector check_if_circuit_has_unsigned_stabilizer_flows( - const Circuit &circuit, SpanRef> flows); + const Circuit &circuit, std::span> flows); template -std::ostream &operator<<(std::ostream &out, const StabilizerFlow &flow); +std::ostream &operator<<(std::ostream &out, const Flow &flow); } // namespace stim -#include "stim/circuit/stabilizer_flow.inl" +#include "stim/stabilizers/flow.inl" #endif diff --git a/src/stim/circuit/stabilizer_flow.inl b/src/stim/stabilizers/flow.inl similarity index 61% rename from src/stim/circuit/stabilizer_flow.inl rename to src/stim/stabilizers/flow.inl index 74c81d93b..2ebfccfe1 100644 --- a/src/stim/circuit/stabilizer_flow.inl +++ b/src/stim/stabilizers/flow.inl @@ -1,10 +1,10 @@ #include "stim/arg_parse.h" #include "stim/circuit/circuit.h" -#include "stim/circuit/stabilizer_flow.h" #include "stim/simulators/frame_simulator_util.h" #include "stim/simulators/sparse_rev_frame_tracker.h" #include "stim/simulators/tableau_simulator.h" #include "stim/stabilizers/flex_pauli_string.h" +#include "stim/stabilizers/flow.h" namespace stim { @@ -25,32 +25,51 @@ void _pauli_string_controlled_not(PauliStringRef control, uint32_t target, Ci } } +template +static GateTarget measurement_index_to_target(int32_t m, uint64_t num_measurements, const Flow &flow) { + if ((m >= 0 && (uint64_t)m >= num_measurements) || (m < 0 && (uint64_t) - (int64_t)m > num_measurements)) { + std::stringstream ss; + ss << "The flow '" << flow; + ss << "' is malformed for the given circuit. "; + ss << "The flow mentions a measurement index '" << m; + ss << "', but this index out of range because the circuit only has "; + ss << num_measurements << " measurements."; + throw std::invalid_argument(ss.str()); + } + if (m >= 0) { + m -= num_measurements; + } + return GateTarget::rec(m); +} + template 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); - n = std::max(n, (uint32_t)flow.output.num_qubits); + size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, const Flow &flow) { + uint32_t num_qubits = (uint32_t)circuit.count_qubits(); + uint64_t num_measurements = circuit.count_measurements(); + + num_qubits = std::max(num_qubits, (uint32_t)flow.input.num_qubits); + num_qubits = std::max(num_qubits, (uint32_t)flow.output.num_qubits); Circuit augmented_circuit; - for (uint32_t k = 0; k < n; k++) { - augmented_circuit.safe_append_u("XCX", {k, k + n + 1}, {}); + for (uint32_t k = 0; k < num_qubits; k++) { + augmented_circuit.safe_append_u("XCX", {k, k + num_qubits + 1}, {}); } - for (uint32_t k = 0; k < n; k++) { + for (uint32_t k = 0; k < num_qubits; k++) { augmented_circuit.safe_append_u("DEPOLARIZE1", {k}, {0.75}); } augmented_circuit.append_from_text("TICK"); - _pauli_string_controlled_not(flow.input, n, augmented_circuit); + _pauli_string_controlled_not(flow.input, num_qubits, augmented_circuit); augmented_circuit.append_from_text("TICK"); augmented_circuit += circuit; augmented_circuit.append_from_text("TICK"); - _pauli_string_controlled_not(flow.output, n, augmented_circuit); - for (const auto &m : flow.measurement_outputs) { - assert(m.is_measurement_record_target()); - std::vector targets{m, GateTarget::qubit(n)}; + _pauli_string_controlled_not(flow.output, num_qubits, augmented_circuit); + for (int32_t m : flow.measurements) { + std::array targets{ + measurement_index_to_target(m, num_measurements, flow), GateTarget::qubit(num_qubits)}; augmented_circuit.safe_append(GateType::CX, targets, {}); } - augmented_circuit.safe_append_u("M", {n}, {}); + augmented_circuit.safe_append_u("M", {num_qubits}, {}); auto out = sample_batch_measurements( augmented_circuit, TableauSimulator::reference_sample_circuit(augmented_circuit), num_samples, rng, false); @@ -61,7 +80,7 @@ bool _sample_if_circuit_has_stabilizer_flow( template std::vector sample_if_circuit_has_stabilizer_flows( - size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, SpanRef> flows) { + size_t num_samples, std::mt19937_64 &rng, const Circuit &circuit, std::span> flows) { std::vector result; for (const auto &flow : flows) { result.push_back(_sample_if_circuit_has_stabilizer_flow(num_samples, rng, circuit, flow)); @@ -69,8 +88,7 @@ std::vector sample_if_circuit_has_stabilizer_flows( return result; } -inline bool parse_rec_allowing_non_negative( - std::string_view rec, size_t num_measurements_for_non_neg, GateTarget *out) { +inline bool parse_rec_allowing_non_negative(std::string_view rec, int32_t *out) { if (rec.size() < 6 || rec[0] != 'r' || rec[1] != 'e' || rec[2] != 'c' || rec[3] != '[' || rec.back() != ']') { throw std::invalid_argument(""); // Caught and given a message below. } @@ -79,12 +97,8 @@ inline bool parse_rec_allowing_non_negative( return false; } - if (i >= INT32_MIN && i < 0) { - *out = stim::GateTarget::rec((int32_t)i); - return true; - } - if (i >= 0 && (size_t)i < num_measurements_for_non_neg) { - *out = stim::GateTarget::rec((int32_t)i - (int32_t)num_measurements_for_non_neg); + if (i >= INT32_MIN && i <= INT32_MAX) { + *out = (int32_t)i; return true; } return false; @@ -117,21 +131,21 @@ PauliString parse_non_empty_pauli_string_allowing_i(std::string_view text, bo } template -StabilizerFlow StabilizerFlow::from_str(const char *text, uint64_t num_measurements_for_non_neg_recs) { +Flow Flow::from_str(std::string_view text) { try { - auto parts = split('>', text); + auto parts = split_view('>', text); if (parts.size() != 2 || parts[0].empty() || parts[0].back() != '-') { throw std::invalid_argument(""); // Caught and given a message below. } - parts[0].pop_back(); + parts[0] = parts[0].substr(0, parts[0].size() - 1); while (!parts[0].empty() && parts[0].back() == ' ') { - parts[0].pop_back(); + parts[0] = parts[0].substr(0, parts[0].size() - 1); } bool imag_inp = false; bool imag_out = false; PauliString inp = parse_non_empty_pauli_string_allowing_i(parts[0], &imag_inp); - parts = split(' ', parts[1]); + parts = split_view(' ', parts[1]); size_t k = 0; while (k < parts.size() && parts[k].empty()) { k += 1; @@ -140,89 +154,124 @@ StabilizerFlow StabilizerFlow::from_str(const char *text, uint64_t num_mea throw std::invalid_argument(""); // Caught and given a message below. } PauliString out(0); - std::vector measurements; + std::vector measurements; if (!parts[k].empty() && parts[k][0] != 'r') { out = parse_non_empty_pauli_string_allowing_i(parts[k], &imag_out); } else { - GateTarget t; - if (!parse_rec_allowing_non_negative(parts[k], num_measurements_for_non_neg_recs, &t)) { + int32_t rec; + if (!parse_rec_allowing_non_negative(parts[k], &rec)) { throw std::invalid_argument(""); // Caught and given a message below. } - measurements.push_back(t); + measurements.push_back(rec); } k++; while (k < parts.size()) { if (parts[k] != "xor" || k + 1 == parts.size()) { throw std::invalid_argument(""); // Caught and given a message below. } - GateTarget rec; - if (!parse_rec_allowing_non_negative(parts[k + 1], num_measurements_for_non_neg_recs, &rec)) { + int32_t rec; + if (!parse_rec_allowing_non_negative(parts[k + 1], &rec)) { throw std::invalid_argument(""); // Caught and given a message below. } measurements.push_back(rec); k += 2; } if (imag_inp != imag_out) { - throw std::invalid_argument("Anti-hermitian flows aren't allowed."); + throw std::invalid_argument("Anti-Hermitian flows aren't allowed."); } - return StabilizerFlow{inp, out, measurements}; + return Flow{inp, out, measurements}; } catch (const std::invalid_argument &ex) { + if (*ex.what() != '\0') { + throw; + } throw std::invalid_argument("Invalid stabilizer flow text: '" + std::string(text) + "'."); } } template -bool StabilizerFlow::operator==(const StabilizerFlow &other) const { - return input == other.input && output == other.output && measurement_outputs == other.measurement_outputs; +bool Flow::operator==(const Flow &other) const { + return input == other.input && output == other.output && measurements == other.measurements; } template -bool StabilizerFlow::operator!=(const StabilizerFlow &other) const { +bool Flow::operator!=(const Flow &other) const { return !(*this == other); } template -std::string StabilizerFlow::str() const { +std::string Flow::str() const { std::stringstream result; result << *this; return result.str(); } template -std::ostream &operator<<(std::ostream &out, const StabilizerFlow &flow) { - if (flow.input.num_qubits == 0) { - if (flow.input.sign) { +std::ostream &operator<<(std::ostream &out, const Flow &flow) { + bool use_sparse = false; + + // Sparse is only useful if most terms are identity. + if (flow.input.num_qubits > 8 && flow.input.ref().weight() * 8 <= flow.input.num_qubits) { + use_sparse = true; + } + if (flow.output.num_qubits > 8 && flow.output.ref().weight() * 8 <= flow.output.num_qubits) { + use_sparse = true; + } + + // Sparse would lose length data if the last pauli is an identity. + if (flow.input.num_qubits > 0 && !flow.input.xs[flow.input.num_qubits - 1] && + !flow.input.zs[flow.input.num_qubits - 1]) { + use_sparse = false; + } + if (flow.output.num_qubits > 0 && !flow.output.xs[flow.output.num_qubits - 1] && + !flow.output.zs[flow.output.num_qubits - 1]) { + use_sparse = false; + } + + auto write_sparse = [&](const PauliString &ps) -> bool { + if (ps.sign) { out << "-"; } + bool has_any = false; + for (size_t q = 0; q < ps.num_qubits; q++) { + uint8_t p = ps.xs[q] + 2 * ps.zs[q]; + if (use_sparse) { + if (p) { + if (has_any) { + out << "*"; + } + out << "_XZY"[p]; + out << q; + has_any = true; + } + } else { + out << "_XZY"[p]; + has_any = true; + } + } + return has_any; + }; + + if (!write_sparse(flow.input)) { out << "1"; - } else { - out << flow.input; } out << " -> "; - bool skip_xor = false; - if (flow.output.num_qubits == 0) { - if (flow.output.sign) { - out << "-1"; - } else if (flow.measurement_outputs.empty()) { - out << "+1"; - } - skip_xor = true; - } else { - out << flow.output; - } - for (const auto &t : flow.measurement_outputs) { - if (!skip_xor) { + bool has_out = write_sparse(flow.output); + for (const auto &t : flow.measurements) { + if (has_out) { out << " xor "; } - skip_xor = false; - t.write_succinct(out); + has_out = true; + out << "rec[" << t << "]"; + } + if (!has_out) { + out << "1"; } return out; } template std::vector check_if_circuit_has_unsigned_stabilizer_flows( - const Circuit &circuit, SpanRef> flows) { + const Circuit &circuit, std::span> flows) { auto stats = circuit.compute_stats(); size_t num_qubits = stats.num_qubits; for (const auto &flow : flows) { @@ -247,7 +296,11 @@ std::vector check_if_circuit_has_unsigned_stabilizer_flows( // 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}); + std::vector targets; + for (int32_t m : flow.measurements) { + targets.push_back(measurement_index_to_target(m, stats.num_measurements, flow)); + } + rev.undo_DETECTOR(CircuitInstruction{GateType::DETECTOR, {}, targets}); } // Undo the circuit. diff --git a/src/stim/stabilizers/flow.pybind.cc b/src/stim/stabilizers/flow.pybind.cc new file mode 100644 index 000000000..f438fc4df --- /dev/null +++ b/src/stim/stabilizers/flow.pybind.cc @@ -0,0 +1,245 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "stim/stabilizers/flow.h" + +#include "stim/py/base.pybind.h" +#include "stim/stabilizers/flow.pybind.h" + +using namespace stim; +using namespace stim_pybind; + +pybind11::class_> stim_pybind::pybind_flow(pybind11::module &m) { + return pybind11::class_>( + m, + "Flow", + clean_doc_string(R"DOC( + A stabilizer flow (e.g. "XI -> XX xor rec[-1]"). + + Stabilizer circuits implement, and can be defined by, how they turn input + stabilizers into output stabilizers mediated by measurements. These + relationships are called stabilizer flows, and `stim.Flow` is a representation + of such a flow. For example, a `stim.Flow` can be given to + `stim.Circuit.has_flow` to verify that a circuit implements the flow. + + 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 the circuit transforms P into Q. + A flow like 1 -> P means the circuit prepares P. + A flow like P -> 1 means the circuit measures P. + A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). + + 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 + >>> c = stim.Circuit("CNOT 2 4") + + >>> c.has_flow(stim.Flow("__X__ -> __X_X")) + True + + >>> c.has_flow(stim.Flow("X2*X4 -> X2")) + True + + >>> c.has_flow(stim.Flow("Z4 -> Z4")) + False + )DOC") + .data()); +} + +static Flow py_init_flow( + const pybind11::object &arg, + const pybind11::object &input, + const pybind11::object &output, + const pybind11::object &measurements) { + if (arg.is_none()) { + Flow result{PauliString(0), PauliString(0)}; + bool imag = false; + if (!input.is_none()) { + auto f = pybind11::cast(input); + imag ^= f.imag; + result.input = std::move(f.value); + } + if (!output.is_none()) { + auto f = pybind11::cast(output); + imag ^= f.imag; + result.output = std::move(f.value); + } + if (imag) { + throw std::invalid_argument("Anti-Hermitian flows aren't allowed."); + } + if (!measurements.is_none()) { + for (const auto &h : measurements) { + if (pybind11::isinstance(h)) { + GateTarget g = pybind11::cast(h); + if (!g.is_measurement_record_target()) { + throw std::invalid_argument("Not a measurement offset: " + g.str()); + } + result.measurements.push_back(g.rec_offset()); + } else { + result.measurements.push_back(pybind11::cast(h)); + } + } + } + return result; + } + + if (!input.is_none()) { + throw std::invalid_argument("Can't specify both a positional argument and `input=`."); + } + if (!output.is_none()) { + throw std::invalid_argument("Can't specify both a positional argument and `output=`."); + } + if (!measurements.is_none()) { + throw std::invalid_argument("Can't specify both a positional argument and `measurements=`."); + } + + if (pybind11::isinstance>(arg)) { + return pybind11::cast>(arg); + } else if (pybind11::isinstance(arg)) { + return Flow::from_str(pybind11::cast(arg)); + } + + std::stringstream ss; + ss << "Don't know how to turn '" << arg << " into a flow."; + throw std::invalid_argument(ss.str()); +} + +void stim_pybind::pybind_flow_methods(pybind11::module &m, pybind11::class_> &c) { + c.def( + pybind11::init(&py_init_flow), + pybind11::arg("arg") = pybind11::none(), + pybind11::pos_only(), + pybind11::kw_only(), + pybind11::arg("input") = pybind11::none(), + pybind11::arg("output") = pybind11::none(), + pybind11::arg("measurements") = pybind11::none(), + clean_doc_string(R"DOC( + @signature def __init__(self, arg: Union[None, str, stim.Flow] = None, /, *, input: Optional[stim.PauliString] = None, output: Optional[stim.PauliString] = None, measurements: Optional[Iterable[Union[int, GateTarget]]] = None) -> None: + Initializes a stim.Flow. + + When given a string, the string is parsed as flow shorthand. For example, + the string "X_ -> ZZ xor rec[-1]" will result in a flow with input pauli string + "X_", output pauli string "ZZ", and measurement indices [-1]. + + Arguments: + arg [position-only]: Defaults to None. Must be specified by itself if used. + str: Initializes a flow by parsing the given shorthand text. + stim.Flow: Initializes a copy of the given flow. + None (default): Initializes an empty flow. + input: Defaults to None. Can be set to a stim.PauliString to directly + specify the flow's input stabilizer. + output: Defaults to None. Can be set to a stim.PauliString to directly + specify the flow's output stabilizer. + measurements: Can be set to a list of integers or gate targets like + `stim.target_rec(-1)`, to specify the measurements that mediate the + flow. Negative and positive measurement indices are allowed. Indexes + follow the python convention where -1 is the last measurement in a + circuit and 0 is the first measurement in a circuit. + + Examples: + >>> import stim + + >>> stim.Flow("X2 -> -Y2*Z4 xor rec[-1]") + stim.Flow("__X -> -__Y_Z xor rec[-1]") + + >>> stim.Flow("Z -> 1 xor rec[-1]") + stim.Flow("Z -> rec[-1]") + + >>> stim.Flow( + ... input=stim.PauliString("XX"), + ... output=stim.PauliString("_X"), + ... measurements=[], + ... ) + stim.Flow("XX -> _X") + )DOC") + .data()); + + c.def( + "input_copy", + [](const Flow &self) -> FlexPauliString { + return FlexPauliString{self.input, false}; + }, + clean_doc_string(R"DOC( + Returns a copy of the flow's input stabilizer. + + Examples: + >>> import stim + >>> f = stim.Flow(input=stim.PauliString('XX')) + >>> f.input_copy() + stim.PauliString("+XX") + + >>> f.input_copy() is f.input_copy() + False + )DOC") + .data()); + + c.def( + "output_copy", + [](const Flow &self) -> FlexPauliString { + return FlexPauliString{self.output, false}; + }, + clean_doc_string(R"DOC( + Returns a copy of the flow's output stabilizer. + + Examples: + >>> import stim + >>> f = stim.Flow(output=stim.PauliString('XX')) + >>> f.output_copy() + stim.PauliString("+XX") + + >>> f.output_copy() is f.output_copy() + False + )DOC") + .data()); + + c.def( + "measurements_copy", + [](const Flow &self) -> std::vector { + return self.measurements; + }, + clean_doc_string(R"DOC( + Returns a copy of the flow's measurement indices. + + Examples: + >>> import stim + >>> f = stim.Flow(measurements=[-1, 2]) + >>> f.measurements_copy() + [-1, 2] + + >>> f.measurements_copy() is f.measurements_copy() + False + )DOC") + .data()); + + c.def(pybind11::self == pybind11::self, "Determines if two flows have identical contents."); + c.def(pybind11::self != pybind11::self, "Determines if two flows have non-identical contents."); + c.def("__str__", &Flow::str, "Returns a shorthand description of the flow."); + + c.def( + "__repr__", + [](const Flow &self) { + return "stim.Flow(\"" + self.str() + "\")"; + }, + "Returns valid python code evaluating to an equivalent `stim.Flow`."); +} diff --git a/src/stim/stabilizers/flow.pybind.h b/src/stim/stabilizers/flow.pybind.h new file mode 100644 index 000000000..3168e85f7 --- /dev/null +++ b/src/stim/stabilizers/flow.pybind.h @@ -0,0 +1,29 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef _STIM_STABILIZERS_FLOW_PYBIND_H +#define _STIM_STABILIZERS_FLOW_PYBIND_H + +#include + +#include "stim/stabilizers/flow.h" + +namespace stim_pybind { + +pybind11::class_> pybind_flow(pybind11::module &m); +void pybind_flow_methods(pybind11::module &m, pybind11::class_> &c); + +} // namespace stim_pybind + +#endif diff --git a/src/stim/stabilizers/flow.test.cc b/src/stim/stabilizers/flow.test.cc new file mode 100644 index 000000000..6525b1af3 --- /dev/null +++ b/src/stim/stabilizers/flow.test.cc @@ -0,0 +1,218 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "stim/stabilizers/flow.h" + +#include "gtest/gtest.h" + +#include "stim/circuit/circuit.h" +#include "stim/mem/simd_word.test.h" +#include "stim/test_util.test.h" + +using namespace stim; + +TEST_EACH_WORD_SIZE_W(stabilizer_flow, from_str, { + ASSERT_THROW({ Flow::from_str(""); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X>X"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X-X"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X > X"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X - X"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("->X"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X->"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("rec[0] -> X"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X -> rec[ -1]"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X -> X rec[-1]"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X -> X xor"); }, std::invalid_argument); + ASSERT_THROW({ Flow::from_str("X -> rec[-1] xor X"); }, std::invalid_argument); + + ASSERT_EQ( + Flow::from_str("1 -> 1"), + (Flow{ + .input = PauliString::from_str(""), + .output = PauliString::from_str(""), + .measurements = {}, + })); + ASSERT_EQ( + Flow::from_str("i -> -i"), + (Flow{ + .input = PauliString::from_str(""), + .output = PauliString::from_str("-"), + .measurements = {}, + })); + ASSERT_EQ( + Flow::from_str("iX -> -iY"), + (Flow{ + .input = PauliString::from_str("X"), + .output = PauliString::from_str("-Y"), + .measurements = {}, + })); + ASSERT_EQ( + Flow::from_str("X->-Y"), + (Flow{ + .input = PauliString::from_str("X"), + .output = PauliString::from_str("-Y"), + .measurements = {}, + })); + ASSERT_EQ( + Flow::from_str("X -> -Y"), + (Flow{ + .input = PauliString::from_str("X"), + .output = PauliString::from_str("-Y"), + .measurements = {}, + })); + ASSERT_EQ( + Flow::from_str("-X -> Y"), + (Flow{ + .input = PauliString::from_str("-X"), + .output = PauliString::from_str("Y"), + .measurements = {}, + })); + ASSERT_EQ( + Flow::from_str("XYZ -> -Z_Z"), + (Flow{ + .input = PauliString::from_str("XYZ"), + .output = PauliString::from_str("-Z_Z"), + .measurements = {}, + })); + ASSERT_EQ( + Flow::from_str("XYZ -> Z_Y xor rec[-1]"), + (Flow{ + .input = PauliString::from_str("XYZ"), + .output = PauliString::from_str("Z_Y"), + .measurements = {-1}, + })); + ASSERT_EQ( + Flow::from_str("XYZ -> Z_Y xor rec[5]"), + (Flow{ + .input = PauliString::from_str("XYZ"), + .output = PauliString::from_str("Z_Y"), + .measurements = {5}, + })); + ASSERT_EQ( + Flow::from_str("XYZ -> rec[-1]"), + (Flow{ + .input = PauliString::from_str("XYZ"), + .output = PauliString::from_str(""), + .measurements = {-1}, + })); + ASSERT_EQ( + Flow::from_str("XYZ -> Z_Y xor rec[-1] xor rec[-3]"), + (Flow{ + .input = PauliString::from_str("XYZ"), + .output = PauliString::from_str("Z_Y"), + .measurements = {-1, -3}, + })); + ASSERT_EQ( + Flow::from_str("XYZ -> ZIY xor rec[55] xor rec[-3]"), + (Flow{ + .input = PauliString::from_str("XYZ"), + .output = PauliString::from_str("Z_Y"), + .measurements = {55, -3}, + })); + ASSERT_EQ( + Flow::from_str("X9 -> -Z5*Y3 xor rec[55] xor rec[-3]"), + (Flow{ + .input = PauliString::from_str("_________X"), + .output = PauliString::from_str("-___Y_Z"), + .measurements = {55, -3}, + })); +}) + +TEST_EACH_WORD_SIZE_W(stabilizer_flow, sample_if_circuit_has_stabilizer_flows, { + auto rng = INDEPENDENT_TEST_RNG(); + auto results = sample_if_circuit_has_stabilizer_flows( + 256, + rng, + Circuit(R"CIRCUIT( + R 4 + CX 0 4 1 4 2 4 3 4 + M 4 + )CIRCUIT"), + std::vector>{ + Flow::from_str("Z___ -> Z____"), + Flow::from_str("_Z__ -> _Z__"), + Flow::from_str("__Z_ -> __Z_"), + Flow::from_str("___Z -> ___Z"), + Flow::from_str("XX__ -> XX__"), + Flow::from_str("XXXX -> XXXX"), + Flow::from_str("XYZ_ -> XYZ_"), + Flow::from_str("XXX_ -> XXX_"), + Flow::from_str("ZZZZ -> ____ xor rec[-1]"), + Flow::from_str("+___Z -> -___Z"), + Flow::from_str("-___Z -> -___Z"), + Flow::from_str("-___Z -> +___Z"), + }); + ASSERT_EQ(results, (std::vector{1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0})); +}) + +TEST_EACH_WORD_SIZE_W(stabilizer_flow, str_and_from_str, { + auto flow = Flow{ + PauliString::from_str("XY"), + PauliString::from_str("_Z"), + {-3}, + }; + auto s_dense = "XY -> _Z xor rec[-3]"; + auto s_sparse = "X0*Y1 -> Z1 xor rec[-3]"; + ASSERT_EQ(flow.str(), s_dense); + ASSERT_EQ(Flow::from_str(s_sparse), flow); + ASSERT_EQ(Flow::from_str(s_dense), flow); + + ASSERT_EQ(Flow::from_str("1 -> rec[-1]"), (Flow{PauliString(0), PauliString(0), {-1}})); + ASSERT_EQ(Flow::from_str("1 -> 1 xor rec[-1]"), (Flow{PauliString(0), PauliString(0), {-1}})); + ASSERT_EQ( + Flow::from_str("1 -> Z9 xor rec[55]"), (Flow{PauliString(0), PauliString("_________Z"), {55}})); + + ASSERT_EQ( + Flow::from_str("-1 -> -X xor rec[-1] xor rec[-3]"), + (Flow{PauliString::from_str("-"), PauliString::from_str("-X"), {-1, -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>{ + Flow::from_str("Z___ -> Z____"), + Flow::from_str("_Z__ -> _Z__"), + Flow::from_str("__Z_ -> __Z_"), + Flow::from_str("___Z -> ___Z"), + Flow::from_str("XX__ -> XX__"), + Flow::from_str("XXXX -> XXXX"), + Flow::from_str("XYZ_ -> XYZ_"), + Flow::from_str("XXX_ -> XXX_"), + Flow::from_str("ZZZZ -> ____ xor rec[-1]"), + Flow::from_str("+___Z -> -___Z"), + Flow::from_str("-___Z -> -___Z"), + Flow::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>{ + Flow::from_str("X_ -> YX"), + Flow::from_str("Y_ -> XX"), + Flow::from_str("X_ -> XX"), + }); + ASSERT_EQ(results, (std::vector{1, 1, 0})); +}) diff --git a/src/stim/stabilizers/flow_pybind_test.py b/src/stim/stabilizers/flow_pybind_test.py new file mode 100644 index 000000000..07de30762 --- /dev/null +++ b/src/stim/stabilizers/flow_pybind_test.py @@ -0,0 +1,106 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import stim +import pytest + + +def test_basics(): + p = stim.Flow() + assert p.input_copy() == stim.PauliString(0) + assert p.output_copy() == stim.PauliString(0) + assert p.measurements_copy() == [] + assert str(p) == "1 -> 1" + assert repr(p) == 'stim.Flow("1 -> 1")' + + p = stim.Flow( + input=stim.PauliString("XX"), + output=stim.PauliString("-YYZ"), + measurements=[-1, 2, 3], + ) + assert p.input_copy() == stim.PauliString("XX") + assert p.output_copy() == stim.PauliString("-YYZ") + assert p.measurements_copy() == [-1, 2, 3] + assert str(p) == "XX -> -YYZ xor rec[-1] xor rec[2] xor rec[3]" + assert repr(p) == 'stim.Flow("XX -> -YYZ xor rec[-1] xor rec[2] xor rec[3]")' + + p = stim.Flow("-X1*Z2 -> Y3 xor rec[-1]") + assert p.input_copy() == stim.PauliString("-_XZ") + assert p.output_copy() == stim.PauliString("___Y") + assert p.measurements_copy() == [-1] + assert str(p) == "-_XZ -> ___Y xor rec[-1]" + assert repr(p) == 'stim.Flow("-_XZ -> ___Y xor rec[-1]")' + + p = stim.Flow("X20 -> Y xor rec[-1]") + assert p.input_copy() == stim.PauliString("X20") + assert p.output_copy() == stim.PauliString("Y") + assert p.measurements_copy() == [-1] + assert str(p) == "X20 -> Y0 xor rec[-1]" + assert repr(p) == 'stim.Flow("X20 -> Y0 xor rec[-1]")' + + p = stim.Flow("X20*I21 -> Y xor rec[-1]") + assert p.input_copy() == stim.PauliString("____________________X_") + assert p.output_copy() == stim.PauliString("Y") + assert p.measurements_copy() == [-1] + assert str(p) == "____________________X_ -> Y xor rec[-1]" + assert repr(p) == 'stim.Flow("____________________X_ -> Y xor rec[-1]")' + + p = stim.Flow("iX -> iY") + assert p.input_copy() == stim.PauliString("X") + assert p.output_copy() == stim.PauliString("Y") + assert p.measurements_copy() == [] + + p = stim.Flow(input=stim.PauliString("iX"), output=stim.PauliString("iY")) + assert p.input_copy() == stim.PauliString("X") + assert p.output_copy() == stim.PauliString("Y") + assert p.measurements_copy() == [] + + with pytest.raises(ValueError, match="Anti-Hermitian"): + stim.Flow("iX -> Y") + with pytest.raises(ValueError, match="Anti-Hermitian"): + stim.Flow(input=stim.PauliString("iX"), output=stim.PauliString("Y")) + + +def test_equality(): + assert not (stim.Flow() == None) + assert not (stim.Flow() == "other object") + assert not (stim.Flow() == object()) + assert stim.Flow() != None + assert stim.Flow() != "other object" + assert stim.Flow() != object() + + assert stim.Flow('X -> Y') == stim.Flow('X -> Y') + assert stim.Flow('X -> X') != stim.Flow('X -> Y') + assert not (stim.Flow('X -> Y') != stim.Flow('X -> Y')) + assert not (stim.Flow('X -> Y') == stim.Flow('X -> X')) + + assert stim.Flow("X -> X xor rec[-1]") == stim.Flow("X -> X xor rec[-1]") + assert stim.Flow("X -> X xor rec[-1]") != stim.Flow("Y -> X xor rec[-1]") + assert stim.Flow("X -> X xor rec[-1]") != stim.Flow("X -> Y xor rec[-1]") + assert stim.Flow("X -> X xor rec[-1]") != stim.Flow("X -> X xor rec[-2]") + + +@pytest.mark.parametrize("value", [ + stim.Flow(), + stim.Flow("X -> Y xor rec[-1]"), + stim.Flow("X -> 1"), + stim.Flow("-X -> Y"), + stim.Flow("X -> -Y"), + stim.Flow("-X -> -Y"), + stim.Flow("1 -> X"), + stim.Flow("X__________________ -> ________Y"), +]) +def test_repr(value): + assert eval(repr(value), {'stim': stim}) == value + assert repr(eval(repr(value), {'stim': stim})) == repr(value) diff --git a/src/stim/stabilizers/tableau_iter.pybind.cc b/src/stim/stabilizers/tableau_iter.pybind.cc index 5d886b4c4..090e535da 100644 --- a/src/stim/stabilizers/tableau_iter.pybind.cc +++ b/src/stim/stabilizers/tableau_iter.pybind.cc @@ -44,7 +44,7 @@ void stim_pybind::pybind_tableau_iter_methods( c.def( "__iter__", [](TableauIterator &self) -> TableauIterator { - TableauIterator copy = self; + TableauIterator copy = self; return copy; }, clean_doc_string(R"DOC(