Skip to content

Commit 05c55a4

Browse files
authored
Fix MR-family measurement noise double counting (#124)
## Summary The MR, MRX, MRY channels with finite bit flip probability incorrectly applied two bitflip channels instead of one. - Remove the extra pre-measurement error channel from `MR`, `MRX`, and `MRY` - Add parser-level coverage for probability-bearing instructions and MR-family noise semantics - Document the fix in the changelog
1 parent 164029c commit 05c55a4

3 files changed

Lines changed: 156 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- `MR`, `MRX`, and `MRY` no longer double-count their measurement flip probability as both a pre-measurement Pauli error and a measurement-result flip.
12+
1013
## [0.1.3] - 2026-04-13
1114

1215
### Added

src/tsim/core/instructions.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -971,8 +971,6 @@ def mr(b: GraphRepresentation, qubit: int, p: float = 0, invert: bool = False) -
971971
Projects each target qubit into |0> or |1>, reports its value (false=|0>, true=|1>),
972972
then resets to |0>.
973973
"""
974-
if p > 0:
975-
x_error(b, qubit, p)
976974
m(b, qubit, p=p, invert=invert)
977975
_r(b, qubit, perform_trace=False)
978976

@@ -984,8 +982,6 @@ def mrx(b: GraphRepresentation, qubit: int, p: float = 0, invert: bool = False)
984982
then resets to |+>.
985983
"""
986984
h(b, qubit)
987-
if p > 0:
988-
x_error(b, qubit, p)
989985
m(b, qubit, p=p, invert=invert)
990986
_r(b, qubit, perform_trace=False)
991987
h(b, qubit)
@@ -998,8 +994,6 @@ def mry(b: GraphRepresentation, qubit: int, p: float = 0, invert: bool = False)
998994
then resets to |i>.
999995
"""
1000996
h_yz(b, qubit)
1001-
if p > 0:
1002-
x_error(b, qubit, p)
1003997
m(b, qubit, p=p, invert=invert)
1004998
_r(b, qubit, perform_trace=False)
1005999
h_yz(b, qubit)

test/unit/core/test_parse.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
import stim
23
from numpy.testing import assert_allclose
34

@@ -150,6 +151,158 @@ def test_heralded_erase_multiple_targets(self):
150151
assert len(b.channel_probs) == 3
151152

152153

154+
class TestProbabilityBearingInstructions:
155+
"""Tests for instructions that create probabilistic error channels."""
156+
157+
@pytest.mark.parametrize(
158+
"program",
159+
[
160+
"X_ERROR(0.07) 0",
161+
"Y_ERROR(0.07) 0",
162+
"Z_ERROR(0.07) 0",
163+
"M(0.07) 0",
164+
"MX(0.07) 0",
165+
"MY(0.07) 0",
166+
"MZ(0.07) 0",
167+
"MR(0.07) 0",
168+
"MRX(0.07) 0",
169+
"MRY(0.07) 0",
170+
"MRZ(0.07) 0",
171+
"MXX(0.07) 0 1",
172+
"MYY(0.07) 0 1",
173+
"MZZ(0.07) 0 1",
174+
"MPP(0.07) X0*Z1",
175+
"MPAD(0.07) 0",
176+
"CORRELATED_ERROR(0.07) X0",
177+
],
178+
)
179+
def test_single_bit_probability_channels(self, program):
180+
"""Single-bit probability-bearing instructions should create one channel."""
181+
b = parse_stim_circuit(stim.Circuit(program))
182+
183+
assert b.num_error_bits == 1
184+
assert len(b.channel_probs) == 1
185+
assert_allclose(b.channel_probs[0], [0.93, 0.07])
186+
187+
@pytest.mark.parametrize("program", ["MR(0.07) 0", "MRX(0.07) 0", "MRY(0.07) 0"])
188+
def test_mr_family_does_not_double_count_measurement_noise(self, program):
189+
"""MR-family measurement noise is a result flip, not an extra Pauli error."""
190+
b = parse_stim_circuit(stim.Circuit(program))
191+
192+
assert len(b.rec) == 1
193+
assert b.num_error_bits == 1
194+
assert len(b.channel_probs) == 1
195+
assert_allclose(b.channel_probs[0], [0.93, 0.07])
196+
197+
@pytest.mark.parametrize(
198+
("program", "expected"),
199+
[
200+
("DEPOLARIZE1(0.12) 0", [0.88, 0.04, 0.04, 0.04]),
201+
("PAULI_CHANNEL_1(0.01, 0.02, 0.03) 0", [0.94, 0.03, 0.01, 0.02]),
202+
(
203+
"HERALDED_ERASE(0.2) 0",
204+
[0.8, 0.05, 0.0, 0.05, 0.0, 0.05, 0.0, 0.05],
205+
),
206+
(
207+
"HERALDED_PAULI_CHANNEL_1(0.01, 0.02, 0.03, 0.04) 0",
208+
[0.9, 0.01, 0.0, 0.04, 0.0, 0.02, 0.0, 0.03],
209+
),
210+
],
211+
)
212+
def test_multi_bit_probability_channels(self, program, expected):
213+
"""Multi-bit probability channels should use the documented outcome ordering."""
214+
b = parse_stim_circuit(stim.Circuit(program))
215+
216+
assert len(b.channel_probs) == 1
217+
assert_allclose(b.channel_probs[0], expected)
218+
219+
def test_depolarize2_probability_channel(self):
220+
"""DEPOLARIZE2(p) should distribute p uniformly over non-identity outcomes."""
221+
b = parse_stim_circuit(stim.Circuit("DEPOLARIZE2(0.15) 0 1"))
222+
223+
assert b.num_error_bits == 4
224+
assert len(b.channel_probs) == 1
225+
assert_allclose(b.channel_probs[0], [0.85] + [0.01] * 15)
226+
227+
def test_pauli_channel_2_probability_channel(self):
228+
"""PAULI_CHANNEL_2 should preserve the expected packed Pauli outcome order."""
229+
args = [
230+
0.001,
231+
0.002,
232+
0.003,
233+
0.004,
234+
0.005,
235+
0.006,
236+
0.007,
237+
0.008,
238+
0.009,
239+
0.010,
240+
0.011,
241+
0.012,
242+
0.013,
243+
0.014,
244+
0.015,
245+
]
246+
program = f"PAULI_CHANNEL_2({', '.join(map(str, args))}) 0 1"
247+
b = parse_stim_circuit(stim.Circuit(program))
248+
249+
assert b.num_error_bits == 4
250+
assert len(b.channel_probs) == 1
251+
assert_allclose(
252+
b.channel_probs[0],
253+
[
254+
0.88,
255+
0.012,
256+
0.004,
257+
0.008,
258+
0.003,
259+
0.015,
260+
0.007,
261+
0.011,
262+
0.001,
263+
0.013,
264+
0.005,
265+
0.009,
266+
0.002,
267+
0.014,
268+
0.006,
269+
0.010,
270+
],
271+
)
272+
273+
@pytest.mark.parametrize(
274+
"program",
275+
[
276+
"X_ERROR(0.07) 0 1",
277+
"M(0.07) 0 1",
278+
"MR(0.07) 0 1",
279+
"MRX(0.07) 0 1",
280+
"MRY(0.07) 0 1",
281+
"MXX(0.07) 0 1 2 3",
282+
"MPP(0.07) X0 X1",
283+
"MPAD(0.07) 0 1",
284+
],
285+
)
286+
def test_repeated_probability_instructions_create_independent_channels(
287+
self, program
288+
):
289+
"""Repeated targets/products should each get their own probability channel."""
290+
b = parse_stim_circuit(stim.Circuit(program))
291+
292+
assert b.num_error_bits == 2
293+
assert len(b.channel_probs) == 2
294+
for probs in b.channel_probs:
295+
assert_allclose(probs, [0.93, 0.07])
296+
297+
@pytest.mark.parametrize("program", ["I_ERROR(0.07) 0", "II_ERROR(0.07) 0 1"])
298+
def test_identity_error_instructions_do_not_create_channels(self, program):
299+
"""Identity error instructions allocate lanes but do not affect noise state."""
300+
b = parse_stim_circuit(stim.Circuit(program))
301+
302+
assert b.num_error_bits == 0
303+
assert len(b.channel_probs) == 0
304+
305+
153306
class TestParseIIError:
154307
"""Tests for parsing II_ERROR instructions with parenthesized arguments."""
155308

0 commit comments

Comments
 (0)