Skip to content

Commit 813371a

Browse files
authored
Fix measurement count for MPP (#48)
In the current implementation, the number of measurements in one measurement instruction is counted as the number of targets in this instruction. But it is wrong if the measurement instruction is MPP. For example ``` MPP X0 * X1 ``` will have three targets but only one measurement. This PR fixed this issue and was used in STAR simulation.
1 parent f756a32 commit 813371a

2 files changed

Lines changed: 116 additions & 6 deletions

File tree

src/tsim/noise/dem.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,15 @@ def get_detector_error_model(
122122
"MY",
123123
"MZ",
124124
]:
125-
num_meas = len(instruction.targets_copy())
125+
targets = instruction.targets_copy()
126+
if instruction.name == "MPP":
127+
# MPP produces one measurement per Pauli product.
128+
# Products are separated by spaces; within a product, Paulis are joined by '*' (combiners).
129+
# num_measurements = num_non_combiner_targets - num_combiners
130+
num_combiners = sum(1 for t in targets if t.is_combiner)
131+
num_meas = len(targets) - 2 * num_combiners
132+
else:
133+
num_meas = len(targets)
126134
for idx in obs:
127135
# update measurement rec indices for the OBSERVABLE_INCLUDE instructions
128136
obs[idx] = [t - num_meas for t in obs[idx]]
@@ -162,6 +170,7 @@ def get_detector_error_model(
162170
)
163171

164172
new_dem = stim.DetectorErrorModel()
173+
165174
for instruction in dem:
166175
assert not isinstance(instruction, stim.DemRepeatBlock)
167176

@@ -170,7 +179,7 @@ def get_detector_error_model(
170179
for t in instruction.targets_copy():
171180
if (
172181
isinstance(t, stim.DemTarget)
173-
and t.is_relative_detector_id
182+
and t.is_relative_detector_id()
174183
and t.val in mapping
175184
):
176185
new_targets.append(stim.target_logical_observable_id(mapping[t.val]))
@@ -185,9 +194,16 @@ def get_detector_error_model(
185194
new_targets,
186195
)
187196

197+
# Remove gauge statements that only affect logical observables (e.g., "error(0.5) L0").
198+
# These arise from non-deterministic observables and should not appear in the final DEM.
188199
if instruction.args_copy() == [0.5]:
189-
# remove gauge statements "error(0.5) L<idx>"
190-
continue
200+
# Check if all targets are logical observables (no detectors)
201+
all_logical = all(
202+
isinstance(t, stim.DemTarget) and t.is_logical_observable_id()
203+
for t in new_targets
204+
)
205+
if all_logical:
206+
continue
191207

192208
new_dem.append(new_instruction)
193209

test/unit/noise/test_dem.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ def test_get_detector_error_model_with_gauge_detectors():
7272
DETECTOR rec[-2]
7373
"""
7474
)
75+
# Gauge errors that only trigger observables (error(0.5) L0) are removed,
76+
# but gauge errors that trigger detectors (error(0.5) D0) are kept.
7577
assert get_detector_error_model(c).approx_equals(
76-
stim.DetectorErrorModel("error(0.01) D0 L0"), atol=1e-12
78+
stim.DetectorErrorModel("error(0.5) D0\n error(0.01) D0 L0"),
79+
atol=1e-12,
7780
)
7881

7982

@@ -111,7 +114,6 @@ def test_get_detector_error_model_no_errors():
111114

112115

113116
def test_get_detector_error_model_with_logical_observables():
114-
115117
with pytest.raises(
116118
ValueError, match="The number of observables changed after conversion."
117119
):
@@ -125,3 +127,95 @@ def test_get_detector_error_model_with_logical_observables():
125127
"""
126128
)
127129
get_detector_error_model(c)
130+
131+
132+
def test_get_detector_error_model_with_mpp_single_product():
133+
"""Test that a single MPP product is correctly counted as 1 measurement."""
134+
# MPP X0*X1*X2 measures a single Pauli product and produces exactly 1 measurement
135+
c = stim.Circuit(
136+
"""
137+
R 0 1 2
138+
H 0 1 2
139+
X_ERROR(0.01) 0
140+
MPP X0*X1*X2
141+
OBSERVABLE_INCLUDE(0) rec[-1]
142+
M 0
143+
DETECTOR rec[-1] rec[-2]
144+
"""
145+
)
146+
dem = get_detector_error_model(c)
147+
assert dem.num_detectors == 1
148+
assert dem.num_observables == 1
149+
assert "D0" in str(dem)
150+
assert "L0" in str(dem)
151+
152+
153+
def test_get_detector_error_model_with_mpp_multiple_products():
154+
"""Test that MPP with multiple products produces one measurement per product.
155+
156+
MPP X0*X1 Z2*Z3 Y4 produces 3 measurements (one per space-separated product).
157+
"""
158+
c = stim.Circuit(
159+
"""
160+
R 0 1 2 3 4
161+
H 0 1 2 3 4
162+
X_ERROR(0.01) 0
163+
MPP X0*X1 Z2*Z3 Y4
164+
OBSERVABLE_INCLUDE(0) rec[-3]
165+
M 0
166+
DETECTOR rec[-1] rec[-4]
167+
"""
168+
)
169+
dem = get_detector_error_model(c)
170+
# rec[-3] refers to the first MPP product (X0*X1) since MPP produces 3 measurements
171+
# and then M produces 1, so rec[-4] is X0*X1, rec[-3] is Z2*Z3, rec[-2] is Y4, rec[-1] is M
172+
assert dem.num_detectors == 1
173+
assert dem.num_observables == 1
174+
175+
176+
def test_get_detector_error_model_with_multiple_mpp_instructions():
177+
"""Test multiple separate MPP instructions with OBSERVABLE_INCLUDE between them."""
178+
c = stim.Circuit(
179+
"""
180+
R 0 1 2 3
181+
H 0 1 2 3
182+
X_ERROR(0.01) 0
183+
MPP X0*X1
184+
OBSERVABLE_INCLUDE(0) rec[-1]
185+
MPP X2*X3
186+
DETECTOR rec[-1] rec[-2]
187+
"""
188+
)
189+
dem = get_detector_error_model(c)
190+
assert dem.num_detectors == 1
191+
assert dem.num_observables == 1
192+
193+
194+
def test_get_detector_error_model_mpp_measurement_counting():
195+
"""Test correct measurement counting for MPP vs regular M measurements.
196+
197+
This test verifies that rec indices are correctly adjusted when MPP instructions
198+
produce multiple measurements. The circuit has a non-deterministic observable
199+
(Z2*Z3 measured on |++⟩ state), which should raise a ValueError because stim
200+
interprets it as a gauge and eliminates it.
201+
"""
202+
# Circuit with MPP producing 2 measurements + M producing 2 measurements
203+
c = stim.Circuit(
204+
"""
205+
R 0 1 2 3
206+
H 0 1 2 3
207+
Z_ERROR(0.01) 0
208+
MPP Z0*Z1 Z2*Z3
209+
M 0 1
210+
OBSERVABLE_INCLUDE(0) rec[-3]
211+
DETECTOR rec[-4] rec[-2] rec[-1]
212+
"""
213+
)
214+
# MPP Z0*Z1 Z2*Z3 produces 2 measurements: rec[-4] and rec[-3] (before M)
215+
# M 0 1 produces 2 measurements: rec[-2] and rec[-1]
216+
# OBSERVABLE_INCLUDE(0) rec[-3] refers to the Z2*Z3 measurement
217+
# Since the observable is non-deterministic (gauge), it gets eliminated
218+
with pytest.raises(
219+
ValueError, match="The number of observables changed after conversion."
220+
):
221+
get_detector_error_model(c)

0 commit comments

Comments
 (0)