Skip to content

Commit

Permalink
Add broadcast_pauli_errors to FlipSimulator interface (#678)
Browse files Browse the repository at this point in the history
Addresses part of #653

Gives a general method for applying errors with 2D masks, key for speeding up interactive use of the flip simulator
  • Loading branch information
Matthew McEwen authored Dec 11, 2023
1 parent 6f365e0 commit 65677df
Show file tree
Hide file tree
Showing 5 changed files with 461 additions and 26 deletions.
56 changes: 56 additions & 0 deletions doc/python_api_reference_vDev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -6153,6 +6154,61 @@ def batch_size(
"""
```

<a name="stim.FlipSimulator.broadcast_pauli_errors"></a>
```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")]
"""
```

<a name="stim.FlipSimulator.do"></a>
```python
# stim.FlipSimulator.do
Expand Down
48 changes: 48 additions & 0 deletions doc/stim.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
48 changes: 48 additions & 0 deletions glue/python/src/stim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
138 changes: 117 additions & 21 deletions src/stim/simulators/frame_simulator.pybind.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ std::optional<size_t> py_index_to_optional_size_t(
return (size_t)i;
}

uint8_t pybind11_object_to_pauli_ixyz(const pybind11::object &obj) {
if (pybind11::isinstance<pybind11::str>(obj)) {
std::string s = pybind11::cast<std::string>(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<pybind11::int_>(obj)) {
uint8_t v = 255;
try {
v = pybind11::cast<uint8_t>(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_<FrameSimulator<MAX_BITWORD_WIDTH>> stim_pybind::pybind_frame_simulator(pybind11::module &m) {
return pybind11::class_<FrameSimulator<MAX_BITWORD_WIDTH>>(
m,
Expand Down Expand Up @@ -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<uint8_t>(pauli);
} catch (const pybind11::cast_error &) {
try {
std::string s = pybind11::cast<std::string>(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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -770,4 +777,93 @@ void stim_pybind::pybind_frame_simulator_methods(
[stim.PauliString("+YX__")]
)DOC")
.data());

c.def(
"broadcast_pauli_errors",
[](FrameSimulator<MAX_BITWORD_WIDTH> &self, const pybind11::object &pauli, const pybind11::object &mask) {
uint8_t p = pybind11_object_to_pauli_ixyz(pauli);

if (!pybind11::isinstance<pybind11::array_t<bool>>(mask)) {
throw std::invalid_argument("Need isinstance(mask, np.ndarray) and mask.dtype == np.bool_");
}
const pybind11::array_t<bool> &arr = pybind11::cast<pybind11::array_t<bool>>(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());
}
Loading

0 comments on commit 65677df

Please sign in to comment.