Skip to content

Commit fd74a6d

Browse files
authored
Handle undetectable errors (#167)
* Allow undetectable error when decompose_errors=True, provided it is not a component of a decomposed error * allow more than eight components * Add enable_correlations option to pymatching.Matching.from_detector_error_model_file and pymatching.Matching.from_stim_circuit_file * fix typo in README
1 parent 7111530 commit fd74a6d

5 files changed

Lines changed: 112 additions & 30 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ print(num_errors) # prints 8
147147

148148
To decode instead with correlated matching, set `enable_correlations=True` both when configuiing the `pymatching.Matching` object:
149149
```python
150-
matching_corr = pymatching.Matching.from_detector_error_model(dem, enable_correlations=True)
150+
matching_corr = pymatching.Matching.from_detector_error_model(model, enable_correlations=True)
151151
```
152152

153153
as well as when decoding:

src/pymatching/matching.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,14 +1296,23 @@ def from_detector_error_model(
12961296
return m
12971297

12981298
@staticmethod
1299-
def from_detector_error_model_file(dem_path: Union[str, Path]) -> 'pymatching.Matching':
1299+
def from_detector_error_model_file(
1300+
dem_path: Union[str, Path],
1301+
*,
1302+
enable_correlations: bool = False
1303+
) -> 'pymatching.Matching':
13001304
"""
13011305
Construct a `pymatching.Matching` by loading from a stim DetectorErrorModel file path.
13021306
13031307
Parameters
13041308
----------
13051309
dem_path : str
13061310
The path of the detector error model file
1311+
enable_correlations : bool, optional
1312+
If `enable_correlations==True`, the detector error model is converted into an internal
1313+
representation that allows correlated matching to be used. Note that you must set
1314+
`enable_correlations=True` here in order to use `enable_correlations=True` when decoding.
1315+
By default, False.
13071316
13081317
Returns
13091318
-------
@@ -1314,7 +1323,10 @@ def from_detector_error_model_file(dem_path: Union[str, Path]) -> 'pymatching.Ma
13141323
if isinstance(dem_path, Path):
13151324
dem_path = str(dem_path)
13161325
m = Matching()
1317-
m._matching_graph = _cpp_pm.detector_error_model_file_to_matching_graph(dem_path)
1326+
m._matching_graph = _cpp_pm.detector_error_model_file_to_matching_graph(
1327+
dem_path,
1328+
enable_correlations=enable_correlations
1329+
)
13181330
return m
13191331

13201332
@staticmethod
@@ -1371,7 +1383,11 @@ def from_stim_circuit(circuit: 'stim.Circuit', *, enable_correlations=False) ->
13711383
return m
13721384

13731385
@staticmethod
1374-
def from_stim_circuit_file(stim_circuit_path: Union[str, Path]) -> 'pymatching.Matching':
1386+
def from_stim_circuit_file(
1387+
stim_circuit_path: Union[str, Path],
1388+
*,
1389+
enable_correlations: bool = False
1390+
) -> 'pymatching.Matching':
13751391
"""
13761392
Construct a `pymatching.Matching` by loading from a stim circuit file path.
13771393
@@ -1386,11 +1402,19 @@ def from_stim_circuit_file(stim_circuit_path: Union[str, Path]) -> 'pymatching.M
13861402
A `pymatching.Matching` object representing the graphlike error mechanisms in the stim circuit
13871403
in the file `stim_circuit_path`, with any hyperedge error mechanisms decomposed into graphlike error
13881404
mechanisms. Parallel edges are merged using `merge_strategy="independent"`.
1405+
enable_correlations : bool, optional
1406+
If `enable_correlations==True`, the stim circuit's detector error model is converted into an internal
1407+
representation that allows correlated matching to be used. Note that you must set
1408+
`enable_correlations=True` here in order to use `enable_correlations=True` when decoding.
1409+
By default, False.
13891410
"""
13901411
if isinstance(stim_circuit_path, Path):
13911412
stim_circuit_path = str(stim_circuit_path)
13921413
m = Matching()
1393-
m._matching_graph = _cpp_pm.stim_circuit_file_to_matching_graph(stim_circuit_path)
1414+
m._matching_graph = _cpp_pm.stim_circuit_file_to_matching_graph(
1415+
stim_circuit_path,
1416+
enable_correlations=enable_correlations
1417+
)
13941418
return m
13951419

13961420
def _load_from_detector_error_model(self, model: 'stim.DetectorErrorModel', *, enable_correlations: bool = False) -> None:

src/pymatching/sparse_blossom/driver/user_graph.h

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ struct DecomposedDemError {
249249
/// The probability of this error occurring.
250250
double probability;
251251
/// Effects of the error.
252-
stim::FixedCapVector<UserEdge, 8> components;
252+
std::vector<UserEdge> components;
253253

254254
bool operator==(const DecomposedDemError& other) const;
255255
bool operator!=(const DecomposedDemError& other) const;
@@ -283,6 +283,7 @@ void iter_dem_instructions_include_correlations(
283283
component->node1 = SIZE_MAX;
284284
component->node2 = SIZE_MAX;
285285
size_t num_component_detectors = 0;
286+
bool instruction_contains_separator = false;
286287
for (auto& target : instruction.target_data) {
287288
// Decompose error
288289
if (target.is_relative_detector_id()) {
@@ -309,30 +310,46 @@ void iter_dem_instructions_include_correlations(
309310
} else if (target.is_observable_id()) {
310311
component->observable_indices.push_back(target.val());
311312
} else if (target.is_separator()) {
312-
// If the previous error in the decomposition had 3 or more components, we ignore it.
313-
if (component->node1 == SIZE_MAX) {
313+
instruction_contains_separator = true;
314+
// If the previous error in the decomposition had 3 or more detectors, we throw an exception.
315+
if (num_component_detectors > 2) {
314316
throw std::invalid_argument(
315317
"Encountered a decomposed error instruction with a hyperedge component (3 or more detectors). "
316318
"This is not supported.");
317-
} else if (p > 0) {
319+
} else if (num_component_detectors == 0) {
320+
throw std::invalid_argument(
321+
"Encountered a decomposed error instruction with an undetectable component (0 detectors). "
322+
"This is not supported.");
323+
} else if (num_component_detectors > 0) {
324+
// If the previous error in the decomposition had 1 or 2 detectors, we handle it
318325
handle_dem_error(p, {component->node1, component->node2}, component->observable_indices);
326+
decomposed_err.components.push_back({});
327+
component = &decomposed_err.components.back();
328+
component->node1 = SIZE_MAX;
329+
component->node2 = SIZE_MAX;
330+
num_component_detectors = 0;
319331
}
320-
decomposed_err.components.push_back({});
321-
component = &decomposed_err.components.back();
322-
component->node1 = SIZE_MAX;
323-
component->node2 = SIZE_MAX;
324-
num_component_detectors = 0;
325332
}
326333
}
327-
// If the final error in the decomposition had 3 or more components, we ignore it.
328-
if (component->node1 == SIZE_MAX) {
334+
335+
if (num_component_detectors > 2) {
329336
// Undecomposed hyperedges are not supported
330337
throw std::invalid_argument(
331338
"Encountered an undecomposed error instruction with 3 or mode detectors. "
332339
"This is not supported when using `enable_correlations=True`. "
333340
"Did you forget to set `decompose_errors=True` when "
334341
"converting the stim circuit to a detector error model?");
335-
} else if (p > 0) {
342+
} else if (num_component_detectors == 0) {
343+
if (instruction_contains_separator) {
344+
throw std::invalid_argument(
345+
"Encountered a decomposed error instruction with an undetectable component (0 detectors). "
346+
"This is not supported.");
347+
} else {
348+
// Ignore errors that are undetectable, provided they are not a component of a decomposed error
349+
return;
350+
}
351+
352+
} else if (num_component_detectors > 0) {
336353
if (component->node2 == SIZE_MAX) {
337354
handle_dem_error(p, {component->node1}, component->observable_indices);
338355
} else {

src/pymatching/sparse_blossom/driver/user_graph.test.cc

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,8 @@ TEST(IterDemInstructionsTest, ThreeDetectorErrorThrowsInvalidArgument) {
308308
stim::DetectorErrorModel dem("error(0.1) D0 D1 D2");
309309
TestHandler handler;
310310
std::map<std::pair<size_t, size_t>, std::map<std::pair<size_t, size_t>, double>> joint_probabilities;
311-
ASSERT_THROW(pm::iter_dem_instructions_include_correlations(dem, handler, joint_probabilities), std::invalid_argument);
311+
ASSERT_THROW(
312+
pm::iter_dem_instructions_include_correlations(dem, handler, joint_probabilities), std::invalid_argument);
312313
}
313314

314315
// Test a decomposed error instruction. The handler should be called for each component.
@@ -353,13 +354,30 @@ TEST(IterDemInstructionsTest, DecomposedErrorWithHyperedgeThrows) {
353354
pm::iter_dem_instructions_include_correlations(dem, handler, joint_probabilities), std::invalid_argument);
354355
}
355356

357+
// Test that a decomposed error with an undetectable component throws an exception.
358+
TEST(IterDemInstructionsTest, DecomposedErrorWithUndetectableErrorThrows) {
359+
stim::DetectorErrorModel dem("error(0.15) L0 ^ D2 D4 ^ D5 D6 L2");
360+
TestHandler handler;
361+
std::map<std::pair<size_t, size_t>, std::map<std::pair<size_t, size_t>, double>> joint_probabilities;
362+
363+
// Assert that the function throws std::invalid_argument when processing the DEM.
364+
ASSERT_THROW(
365+
pm::iter_dem_instructions_include_correlations(dem, handler, joint_probabilities), std::invalid_argument);
366+
367+
stim::DetectorErrorModel dem2("error(0.15) D2 D4 ^ D5 D6 L2 ^ L1");
368+
// Assert that the function throws std::invalid_argument when processing the DEM.
369+
ASSERT_THROW(
370+
pm::iter_dem_instructions_include_correlations(dem2, handler, joint_probabilities), std::invalid_argument);
371+
}
372+
356373
// Test a complex DEM with multiple instruction types and edge cases combined.
357374
TEST(IterDemInstructionsTest, CombinedComplexDem) {
358375
stim::DetectorErrorModel dem(R"DEM(
359376
error(0.1) D0 # Instruction 1: Simple
377+
error(0.3) L0 # Instruction 2: Undetectable error, ignored
360378
error(0.2) D1 D2 L0 # Instruction 2: Two detectors, one observable
361-
error(0.0) D7 # Instruction 4: Zero probability, ignored
362-
error(0.4) D8 ^ D9 L1 # Instruction 5: Decomposed
379+
error(0.0) D7 # Instruction 3: Zero probability, ignored
380+
error(0.4) D8 ^ D9 L1 # Instruction 4: Decomposed
363381
)DEM");
364382
TestHandler handler;
365383
std::map<std::pair<size_t, size_t>, std::map<std::pair<size_t, size_t>, double>> joint_probabilities;
@@ -395,6 +413,13 @@ double bernoulli_xor(double p1, double p2) {
395413
return p1 * (1 - p2) + p2 * (1 - p1);
396414
}
397415

416+
TEST(IterDemInstructionsTest, MoreThanEightComponents) {
417+
stim::DetectorErrorModel dem("error(0.1) D0 ^ D1 ^ D2 ^ D3 ^ D4 ^ D5 ^ D6 ^ D7 ^ D8");
418+
TestHandler handler;
419+
std::map<std::pair<size_t, size_t>, std::map<std::pair<size_t, size_t>, double>> joint_probabilities;
420+
pm::iter_dem_instructions_include_correlations(dem, handler, joint_probabilities);
421+
}
422+
398423
// Tests that multiple error instructions on the same edge correctly combine their probabilities.
399424
TEST(IterDemInstructionsTest, MultipleErrorsOnSameEdgeCombine) {
400425
stim::DetectorErrorModel dem("error(0.1) D0 D1\n error(0.2) D0 D1");

tests/matching/decode_test.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -387,22 +387,32 @@ def test_load_from_circuit_with_correlations():
387387
predictions, weights = m.decode_batch(shots=shots, return_weights=True, enable_correlations=True)
388388

389389

390-
def test_use_correlations_with_uncorrelated_dem_load_raises_value_error():
390+
def test_use_correlations_with_uncorrelated_dem_load_raises_value_error(tmp_path):
391391
stim = pytest.importorskip("stim")
392+
d = 3
393+
p = 0.001
392394
circuit = stim.Circuit.generated(
393395
code_task="surface_code:rotated_memory_x",
394-
distance=3,
395-
rounds=3,
396-
after_clifford_depolarization=0.001
396+
distance=d,
397+
rounds=d,
398+
after_clifford_depolarization=p
397399
)
400+
dem = circuit.detector_error_model(decompose_errors=True)
398401
shots = circuit.compile_detector_sampler().sample(shots=10)
399402
matching_1 = pymatching.Matching(circuit, enable_correlations=False)
400403
matching_2 = pymatching.Matching.from_stim_circuit(circuit=circuit, enable_correlations=False)
401404
matching_3 = pymatching.Matching.from_detector_error_model(
402-
model=circuit.detector_error_model(decompose_errors=True),
405+
model=dem,
403406
enable_correlations=False
404407
)
405-
for m in (matching_1, matching_2, matching_3):
408+
fn = f"surface_code_x_d{d}_r{d}_p{p}"
409+
stim_file = tmp_path / f"{fn}.stim"
410+
circuit.to_file(stim_file)
411+
matching_4 = pymatching.Matching.from_stim_circuit_file(stim_file, enable_correlations=False)
412+
dem_file = tmp_path / f"{fn}.dem"
413+
dem.to_file(dem_file)
414+
matching_5 = pymatching.Matching.from_detector_error_model_file(dem_file, enable_correlations=False)
415+
for m in (matching_1, matching_2, matching_3, matching_4, matching_5):
406416
with pytest.raises(ValueError):
407417
m.decode_batch(shots=shots, return_weights=True, enable_correlations=True)
408418
with pytest.raises(ValueError):
@@ -413,16 +423,22 @@ def test_use_correlations_with_uncorrelated_dem_load_raises_value_error():
413423
m.decode(shots[0], enable_correlations=True)
414424

415425

416-
def test_use_correlations_without_decompose_errors_raises_value_error():
426+
def test_use_correlations_without_decompose_errors_raises_value_error(tmp_path):
417427
stim = pytest.importorskip("stim")
428+
d = 3
429+
p = 0.001
418430
circuit = stim.Circuit.generated(
419431
code_task="surface_code:rotated_memory_x",
420-
distance=3,
421-
rounds=3,
422-
after_clifford_depolarization=0.001
432+
distance=d,
433+
rounds=d,
434+
after_clifford_depolarization=p
423435
)
424436
dem = circuit.detector_error_model(decompose_errors=False)
437+
dem_file = tmp_path / "surface_code.dem"
438+
dem.to_file(dem_file)
425439
with pytest.raises(ValueError):
426440
pymatching.Matching.from_detector_error_model(dem, enable_correlations=True)
427441
with pytest.raises(ValueError):
428442
pymatching.Matching(dem, enable_correlations=True)
443+
with pytest.raises(ValueError):
444+
pymatching.Matching.from_detector_error_model_file(dem_file, enable_correlations=True)

0 commit comments

Comments
 (0)