diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md index ee5b43b49..9905cd76c 100644 --- a/doc/python_api_reference_vDev.md +++ b/doc/python_api_reference_vDev.md @@ -173,6 +173,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`stim.FlipSimulator`](#stim.FlipSimulator) - [`stim.FlipSimulator.__init__`](#stim.FlipSimulator.__init__) - [`stim.FlipSimulator.batch_size`](#stim.FlipSimulator.batch_size) + - [`stim.FlipSimulator.broadcast_pauli_errors`](#stim.FlipSimulator.broadcast_pauli_errors) - [`stim.FlipSimulator.do`](#stim.FlipSimulator.do) - [`stim.FlipSimulator.get_detector_flips`](#stim.FlipSimulator.get_detector_flips) - [`stim.FlipSimulator.get_measurement_flips`](#stim.FlipSimulator.get_measurement_flips) @@ -6153,6 +6154,61 @@ def batch_size( """ ``` + +```python +# stim.FlipSimulator.broadcast_pauli_errors + +# (in class stim.FlipSimulator) +def broadcast_pauli_errors( + self, + *, + pauli: Union[str, int], + mask: np.ndarray, +) -> None: + """Applies a pauli error to all qubits in all instances, filtered by a mask. + + Args: + pauli: The pauli, specified as an integer or string. + Uses the convention 0=I, 1=X, 2=Y, 3=Z. + Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. + mask: A 2d numpy array specifying where to apply errors. The first axis + is qubits, the second axis is simulation instances. The first axis + can have a length less than the current number of qubits (or more, + which adds qubits to the simulation). The length of the second axis + must match the simulator's `batch_size`. The array must satisfy + + mask.dtype == np.bool_ + len(mask.shape) == 2 + mask.shape[1] == flip_sim.batch_size + + The error is only applied to qubit q in instance k when + + mask[q, k] == True. + + Examples: + >>> import stim + >>> import numpy as np + >>> sim = stim.FlipSimulator( + ... batch_size=2, + ... num_qubits=3, + ... disable_stabilizer_randomization=True, + ... ) + >>> sim.broadcast_pauli_errors( + ... pauli='X', + ... mask=np.asarray([[True, False],[False, False],[True, True]]), + ... ) + >>> sim.peek_pauli_flips() + [stim.PauliString("+X_X"), stim.PauliString("+__X")] + + >>> sim.broadcast_pauli_errors( + ... pauli='Z', + ... mask=np.asarray([[False, True],[False, False],[True, True]]), + ... ) + >>> sim.peek_pauli_flips() + [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] + """ +``` + ```python # stim.FlipSimulator.do diff --git a/doc/stim.pyi b/doc/stim.pyi index 697608bdc..6a7ff42f8 100644 --- a/doc/stim.pyi +++ b/doc/stim.pyi @@ -4670,6 +4670,54 @@ class FlipSimulator: >>> sim.batch_size 42 """ + def broadcast_pauli_errors( + self, + *, + pauli: Union[str, int], + mask: np.ndarray, + ) -> None: + """Applies a pauli error to all qubits in all instances, filtered by a mask. + + Args: + pauli: The pauli, specified as an integer or string. + Uses the convention 0=I, 1=X, 2=Y, 3=Z. + Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. + mask: A 2d numpy array specifying where to apply errors. The first axis + is qubits, the second axis is simulation instances. The first axis + can have a length less than the current number of qubits (or more, + which adds qubits to the simulation). The length of the second axis + must match the simulator's `batch_size`. The array must satisfy + + mask.dtype == np.bool_ + len(mask.shape) == 2 + mask.shape[1] == flip_sim.batch_size + + The error is only applied to qubit q in instance k when + + mask[q, k] == True. + + Examples: + >>> import stim + >>> import numpy as np + >>> sim = stim.FlipSimulator( + ... batch_size=2, + ... num_qubits=3, + ... disable_stabilizer_randomization=True, + ... ) + >>> sim.broadcast_pauli_errors( + ... pauli='X', + ... mask=np.asarray([[True, False],[False, False],[True, True]]), + ... ) + >>> sim.peek_pauli_flips() + [stim.PauliString("+X_X"), stim.PauliString("+__X")] + + >>> sim.broadcast_pauli_errors( + ... pauli='Z', + ... mask=np.asarray([[False, True],[False, False],[True, True]]), + ... ) + >>> sim.peek_pauli_flips() + [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] + """ def do( self, obj: Union[stim.Circuit, stim.CircuitInstruction, stim.CircuitRepeatBlock], diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi index 697608bdc..6a7ff42f8 100644 --- a/glue/python/src/stim/__init__.pyi +++ b/glue/python/src/stim/__init__.pyi @@ -4670,6 +4670,54 @@ class FlipSimulator: >>> sim.batch_size 42 """ + def broadcast_pauli_errors( + self, + *, + pauli: Union[str, int], + mask: np.ndarray, + ) -> None: + """Applies a pauli error to all qubits in all instances, filtered by a mask. + + Args: + pauli: The pauli, specified as an integer or string. + Uses the convention 0=I, 1=X, 2=Y, 3=Z. + Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. + mask: A 2d numpy array specifying where to apply errors. The first axis + is qubits, the second axis is simulation instances. The first axis + can have a length less than the current number of qubits (or more, + which adds qubits to the simulation). The length of the second axis + must match the simulator's `batch_size`. The array must satisfy + + mask.dtype == np.bool_ + len(mask.shape) == 2 + mask.shape[1] == flip_sim.batch_size + + The error is only applied to qubit q in instance k when + + mask[q, k] == True. + + Examples: + >>> import stim + >>> import numpy as np + >>> sim = stim.FlipSimulator( + ... batch_size=2, + ... num_qubits=3, + ... disable_stabilizer_randomization=True, + ... ) + >>> sim.broadcast_pauli_errors( + ... pauli='X', + ... mask=np.asarray([[True, False],[False, False],[True, True]]), + ... ) + >>> sim.peek_pauli_flips() + [stim.PauliString("+X_X"), stim.PauliString("+__X")] + + >>> sim.broadcast_pauli_errors( + ... pauli='Z', + ... mask=np.asarray([[False, True],[False, False],[True, True]]), + ... ) + >>> sim.peek_pauli_flips() + [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] + """ def do( self, obj: Union[stim.Circuit, stim.CircuitInstruction, stim.CircuitRepeatBlock], diff --git a/src/stim/simulators/frame_simulator.pybind.cc b/src/stim/simulators/frame_simulator.pybind.cc index 56e6795a8..6d503756f 100644 --- a/src/stim/simulators/frame_simulator.pybind.cc +++ b/src/stim/simulators/frame_simulator.pybind.cc @@ -33,6 +33,32 @@ std::optional py_index_to_optional_size_t( return (size_t)i; } +uint8_t pybind11_object_to_pauli_ixyz(const pybind11::object &obj) { + if (pybind11::isinstance(obj)) { + std::string s = pybind11::cast(obj); + if (s == "X") { + return 1; + } else if (s == "Y") { + return 2; + } else if (s == "Z") { + return 3; + } else if (s == "I" || s == "_") { + return 0; + } + } else if (pybind11::isinstance(obj)) { + uint8_t v = 255; + try { + v = pybind11::cast(obj); + } catch (const pybind11::cast_error &) { + } + if (v < 4) { + return (uint8_t)v; + } + } + + throw std::invalid_argument("Need pauli in ['I', 'X', 'Y', 'Z', 0, 1, 2, 3, '_']."); +} + pybind11::class_> stim_pybind::pybind_frame_simulator(pybind11::module &m) { return pybind11::class_>( m, @@ -374,27 +400,7 @@ void stim_pybind::pybind_frame_simulator_methods( const pybind11::object &pauli, int64_t qubit_index, int64_t instance_index) { - uint8_t p = 255; - try { - p = pybind11::cast(pauli); - } catch (const pybind11::cast_error &) { - try { - std::string s = pybind11::cast(pauli); - if (s == "X") { - p = 1; - } else if (s == "Y") { - p = 2; - } else if (s == "Z") { - p = 3; - } else if (s == "I" || s == "_") { - p = 0; - } - } catch (const pybind11::cast_error &) { - } - } - if (p > 3) { - throw std::invalid_argument("Expected pauli in [0, 1, 2, 3, '_', 'I', 'X', 'Y', 'Z']"); - } + uint8_t p = pybind11_object_to_pauli_ixyz(pauli); if (instance_index < 0) { instance_index += self.batch_size; } @@ -409,6 +415,7 @@ void stim_pybind::pybind_frame_simulator_methods( stats.num_qubits = qubit_index + 1; self.ensure_safe_to_do_circuit_with_stats(stats); } + p ^= p >> 1; self.x_table[qubit_index][instance_index] = (p & 1) != 0; self.z_table[qubit_index][instance_index] = (p & 2) != 0; @@ -770,4 +777,93 @@ void stim_pybind::pybind_frame_simulator_methods( [stim.PauliString("+YX__")] )DOC") .data()); + + c.def( + "broadcast_pauli_errors", + [](FrameSimulator &self, const pybind11::object &pauli, const pybind11::object &mask) { + uint8_t p = pybind11_object_to_pauli_ixyz(pauli); + + if (!pybind11::isinstance>(mask)) { + throw std::invalid_argument("Need isinstance(mask, np.ndarray) and mask.dtype == np.bool_"); + } + const pybind11::array_t &arr = pybind11::cast>(mask); + + if (arr.ndim() != 2) { + throw std::invalid_argument( + "Need a 2d mask (first axis is qubits, second axis is simulation instances). Need len(mask.shape) " + "== 2."); + } + + pybind11::ssize_t s_mask_num_qubits = arr.shape(0); + pybind11::ssize_t s_mask_batch_size = arr.shape(1); + if ((uint64_t)s_mask_batch_size != self.batch_size) { + throw std::invalid_argument("Need mask.shape[1] == flip_sim.batch_size"); + } + if (s_mask_num_qubits > UINT32_MAX) { + throw std::invalid_argument("Mask exceeds maximum number of simulated qubits."); + } + uint32_t mask_num_qubits = (uint32_t)s_mask_num_qubits; + uint32_t mask_batch_size = (uint32_t)s_mask_batch_size; + + self.ensure_safe_to_do_circuit_with_stats(CircuitStats{.num_qubits = mask_num_qubits}); + auto u = arr.unchecked<2>(); + bool p_x = (0b0110 >> p) & 1; // parity of 2 bit number + bool p_z = p & 2; + for (size_t i = 0; i < mask_num_qubits; i++) { + for (size_t j = 0; j < mask_batch_size; j++) { + bool b = *u.data(i, j); + self.x_table[i][j] ^= b & p_x; + self.z_table[i][j] ^= b & p_z; + } + } + }, + pybind11::kw_only(), + pybind11::arg("pauli"), + pybind11::arg("mask"), + clean_doc_string(R"DOC( + @signature def broadcast_pauli_errors(self, *, pauli: Union[str, int], mask: np.ndarray) -> None: + Applies a pauli error to all qubits in all instances, filtered by a mask. + + Args: + pauli: The pauli, specified as an integer or string. + Uses the convention 0=I, 1=X, 2=Y, 3=Z. + Any value from [0, 1, 2, 3, 'X', 'Y', 'Z', 'I', '_'] is allowed. + mask: A 2d numpy array specifying where to apply errors. The first axis + is qubits, the second axis is simulation instances. The first axis + can have a length less than the current number of qubits (or more, + which adds qubits to the simulation). The length of the second axis + must match the simulator's `batch_size`. The array must satisfy + + mask.dtype == np.bool_ + len(mask.shape) == 2 + mask.shape[1] == flip_sim.batch_size + + The error is only applied to qubit q in instance k when + + mask[q, k] == True. + + Examples: + >>> import stim + >>> import numpy as np + >>> sim = stim.FlipSimulator( + ... batch_size=2, + ... num_qubits=3, + ... disable_stabilizer_randomization=True, + ... ) + >>> sim.broadcast_pauli_errors( + ... pauli='X', + ... mask=np.asarray([[True, False],[False, False],[True, True]]), + ... ) + >>> sim.peek_pauli_flips() + [stim.PauliString("+X_X"), stim.PauliString("+__X")] + + >>> sim.broadcast_pauli_errors( + ... pauli='Z', + ... mask=np.asarray([[False, True],[False, False],[True, True]]), + ... ) + >>> sim.peek_pauli_flips() + [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] + + )DOC") + .data()); } diff --git a/src/stim/simulators/frame_simulator_pybind_test.py b/src/stim/simulators/frame_simulator_pybind_test.py index 945d7acfc..0078a8d8e 100644 --- a/src/stim/simulators/frame_simulator_pybind_test.py +++ b/src/stim/simulators/frame_simulator_pybind_test.py @@ -191,15 +191,15 @@ def test_set_pauli_flip(): stim.PauliString('XZ_'), ] - with pytest.raises(ValueError, match='Expected pauli'): + with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip(-1, qubit_index=0, instance_index=0) - with pytest.raises(ValueError, match='Expected pauli'): + with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip(4, qubit_index=0, instance_index=0) - with pytest.raises(ValueError, match='Expected pauli'): + with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip('R', qubit_index=0, instance_index=0) - with pytest.raises(ValueError, match='Expected pauli'): + with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip('XY', qubit_index=0, instance_index=0) - with pytest.raises(ValueError, match='Expected pauli'): + with pytest.raises(ValueError, match='pauli'): sim.set_pauli_flip(object(), qubit_index=0, instance_index=0) with pytest.raises(IndexError, match='instance_index'): @@ -216,6 +216,193 @@ def test_set_pauli_flip(): stim.PauliString('XZ___'), ] +def test_broadcast_pauli_errors(): + sim = stim.FlipSimulator( + batch_size=2, + num_qubits=3, + disable_stabilizer_randomization=True, + ) + sim.broadcast_pauli_errors( + pauli='X', + mask=np.asarray([ + [True, False], + [False, False], + [True, True]] + ), + ) + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+X_X"), + stim.PauliString("+__X") + ] + sim.broadcast_pauli_errors( + pauli='Z', + mask=np.asarray([ + [True, True], + [True, False], + [False, False]] + ), + ) + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+YZX"), + stim.PauliString("+Z_X") + ] + sim.broadcast_pauli_errors( + pauli='Y', + mask=np.asarray([ + [True, False], + [False, True], + [False, True]] + ), + ) + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+_ZX"), + stim.PauliString("+ZYZ") + ] + sim.broadcast_pauli_errors( + pauli='I', + mask=np.asarray([ + [True, True], + [False, True], + [True, True]] + ), + ) + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+_ZX"), + stim.PauliString("+ZYZ") + ] + + # do it again with ints + sim = stim.FlipSimulator( + batch_size=2, + num_qubits=3, + disable_stabilizer_randomization=True, + ) + sim.broadcast_pauli_errors( + pauli=1, + mask=np.asarray([ + [True, False], + [False, False], + [True, True]] + ), + ) + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+X_X"), + stim.PauliString("+__X") + ] + sim.broadcast_pauli_errors( + pauli=3, + mask=np.asarray([ + [True, True], + [True, False], + [False, False]] + ), + ) + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+YZX"), + stim.PauliString("+Z_X") + ] + sim.broadcast_pauli_errors( + pauli=2, + mask=np.asarray([ + [True, False], + [False, True], + [False, True]] + ), + ) + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+_ZX"), + stim.PauliString("+ZYZ") + ] + sim.broadcast_pauli_errors( + pauli=0, + mask=np.asarray([ + [True, True], + [False, True], + [True, True]] + ), + ) + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+_ZX"), + stim.PauliString("+ZYZ") + ] + + with pytest.raises(ValueError, match='pauli'): + sim.broadcast_pauli_errors( + pauli='whoops', + mask=np.asarray([ + [True, True], + [False, True], + [True, True]] + ), + ) + with pytest.raises(ValueError, match='pauli'): + sim.broadcast_pauli_errors( + pauli=4, + mask=np.asarray([ + [True, True], + [False, True], + [True, True]] + ), + ) + with pytest.raises(ValueError, match='batch_size'): + sim.broadcast_pauli_errors( + pauli='X', + mask=np.asarray([ + [True, True,True], + [False, True, True], + [True, True, True]] + ), + ) + with pytest.raises(ValueError, match='batch_size'): + sim.broadcast_pauli_errors( + pauli='X', + mask=np.asarray([ + [True], + [False], + [True]] + ), + ) + sim = stim.FlipSimulator( + batch_size=2, + num_qubits=3, + disable_stabilizer_randomization=True, + ) + sim.broadcast_pauli_errors( + pauli='X', + mask=np.asarray([ + [True, False], + [False, False], + [True, True], + [True, True]] + ), + ) # automatically expands the qubit basis + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+X_XX"), + stim.PauliString("+__XX") + ] + sim.broadcast_pauli_errors( + pauli='X', + mask=np.asarray([ + [True, False], + [False, False], + ] + ), + ) # tolerates fewer qubits in mask than in simulator + peek = sim.peek_pauli_flips() + assert peek == [ + stim.PauliString("+__XX"), + stim.PauliString("+__XX") + ] + def test_repro_heralded_pauli_channel_1_bug(): circuit = stim.Circuit("""