From 1b09d945120cf3fd22d779970609a0736e408048 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Fri, 16 Feb 2024 19:06:32 -0800 Subject: [PATCH] Add `stim.target_pauli` and `stim.target_combined_paulis` (#696) --- doc/python_api_reference_vDev.md | 82 +++++++++++++++ doc/stim.pyi | 66 ++++++++++++ glue/python/src/stim/__init__.pyi | 66 ++++++++++++ src/stim/py/stim.pybind.cc | 166 ++++++++++++++++++++++++++++++ src/stim/py/stim_pybind_test.py | 89 ++++++++++++++++ 5 files changed, 469 insertions(+) diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md index 517845184..38c116751 100644 --- a/doc/python_api_reference_vDev.md +++ b/doc/python_api_reference_vDev.md @@ -385,9 +385,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) @@ -13446,6 +13448,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 +13566,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..3a8214ccc 100644 --- a/doc/stim.pyi +++ b/doc/stim.pyi @@ -10525,6 +10525,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 +10615,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/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi index f6a6d5105..3a8214ccc 100644 --- a/glue/python/src/stim/__init__.pyi +++ b/glue/python/src/stim/__init__.pyi @@ -10525,6 +10525,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 +10615,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/py/stim.pybind.cc b/src/stim/py/stim.pybind.cc index 0c34f7ff1..df3c8f067 100644 --- a/src/stim/py/stim.pybind.cc +++ b/src/stim/py/stim.pybind.cc @@ -92,6 +92,96 @@ 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, 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"))