diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md
index 8439360b6..a9659701f 100644
--- a/doc/python_api_reference_vDev.md
+++ b/doc/python_api_reference_vDev.md
@@ -174,6 +174,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi
- [`stim.DetectorErrorModel.rounded`](#stim.DetectorErrorModel.rounded)
- [`stim.DetectorErrorModel.shortest_graphlike_error`](#stim.DetectorErrorModel.shortest_graphlike_error)
- [`stim.DetectorErrorModel.to_file`](#stim.DetectorErrorModel.to_file)
+ - [`stim.DetectorErrorModel.to_simple_error_lists`](#stim.DetectorErrorModel.to_simple_error_lists)
- [`stim.ExplainedError`](#stim.ExplainedError)
- [`stim.ExplainedError.__init__`](#stim.ExplainedError.__init__)
- [`stim.ExplainedError.circuit_error_locations`](#stim.ExplainedError.circuit_error_locations)
@@ -6581,6 +6582,68 @@ def to_file(
"""
```
+
+```python
+# stim.DetectorErrorModel.to_simple_error_lists
+
+# (in class stim.DetectorErrorModel)
+def to_simple_error_lists(
+ self,
+) -> Tuple['np.ndarray[float]', List[Set[int]], List[Set[int]]]:
+ """Simplifies the model into lists of chances, detection sets, and obs flips sets.
+
+ Note that this summary doesn't include information about loops, coordinates, or
+ suggested decompositions. Also note that this method will merge all errors with
+ identical symptoms, by Bernoulli summing their probabilities, and will sort the
+ errors. For example, the following two detector error models will give identical
+ results:
+
+ # dem 1
+ error(0.01) D1 D2 ^ D3 D4
+ error(0.125) D1
+ error(0.25) D1
+ detector(2, 3, 5) D1
+
+ vs
+
+ # dem 2
+ error(0.3125) D1
+ error(0.01) D1 D2 D3 D4
+
+ Returns:
+ A tuple of sequences (probabilities, dets, obs).
+
+ `probabilities` will be a numpy array with dtype=np.float64 and
+ shape=(num_errors,).
+
+ `dets` will be a list of detection event sets. Each detection
+ event set is a python set containing the indices of detectors
+ flipped by the error.
+
+ `obs` will be a list of observable flip sets. Each observable
+ flip set is a python set containing the indices of detectors
+ flipped by the error.
+
+ These sequences are paired up by index. They will have the same length, and
+ the combination of probabilities[k], dets[k], and obs[k] form a single
+ error.
+
+ Examples:
+ >>> import stim
+ >>> dem = stim.DetectorErrorModel('''
+ ... error(0.25) D0 D1
+ ... error(0.125) D0 D2 L1 L5
+ ... ''')
+ >>> probs, dets, obs = dem.to_simple_error_lists()
+ >>> probs
+ array([0.25 , 0.125])
+ >>> dets
+ [{0, 1}, {0, 2}]
+ >>> obs
+ [set(), {1, 5}]
+ """
+```
+
```python
# stim.ExplainedError
diff --git a/doc/stim.pyi b/doc/stim.pyi
index d08243a74..217cb7f72 100644
--- a/doc/stim.pyi
+++ b/doc/stim.pyi
@@ -5069,6 +5069,61 @@ class DetectorErrorModel:
>>> contents
'error(0.25) D2 D3\n'
"""
+ def to_simple_error_lists(
+ self,
+ ) -> Tuple['np.ndarray[float]', List[Set[int]], List[Set[int]]]:
+ """Simplifies the model into lists of chances, detection sets, and obs flips sets.
+
+ Note that this summary doesn't include information about loops, coordinates, or
+ suggested decompositions. Also note that this method will merge all errors with
+ identical symptoms, by Bernoulli summing their probabilities, and will sort the
+ errors. For example, the following two detector error models will give identical
+ results:
+
+ # dem 1
+ error(0.01) D1 D2 ^ D3 D4
+ error(0.125) D1
+ error(0.25) D1
+ detector(2, 3, 5) D1
+
+ vs
+
+ # dem 2
+ error(0.3125) D1
+ error(0.01) D1 D2 D3 D4
+
+ Returns:
+ A tuple of sequences (probabilities, dets, obs).
+
+ `probabilities` will be a numpy array with dtype=np.float64 and
+ shape=(num_errors,).
+
+ `dets` will be a list of detection event sets. Each detection
+ event set is a python set containing the indices of detectors
+ flipped by the error.
+
+ `obs` will be a list of observable flip sets. Each observable
+ flip set is a python set containing the indices of detectors
+ flipped by the error.
+
+ These sequences are paired up by index. They will have the same length, and
+ the combination of probabilities[k], dets[k], and obs[k] form a single
+ error.
+
+ Examples:
+ >>> import stim
+ >>> dem = stim.DetectorErrorModel('''
+ ... error(0.25) D0 D1
+ ... error(0.125) D0 D2 L1 L5
+ ... ''')
+ >>> probs, dets, obs = dem.to_simple_error_lists()
+ >>> probs
+ array([0.25 , 0.125])
+ >>> dets
+ [{0, 1}, {0, 2}]
+ >>> obs
+ [set(), {1, 5}]
+ """
class ExplainedError:
"""Describes the location of an error mechanism from a stim circuit.
"""
diff --git a/file_lists/source_files_no_main b/file_lists/source_files_no_main
index 2c508d733..92531b88a 100644
--- a/file_lists/source_files_no_main
+++ b/file_lists/source_files_no_main
@@ -91,6 +91,7 @@ src/stim/util_bot/error_decomp.cc
src/stim/util_top/circuit_inverse_unitary.cc
src/stim/util_top/circuit_to_detecting_regions.cc
src/stim/util_top/circuit_vs_amplitudes.cc
+src/stim/util_top/dem_to_matrix.cc
src/stim/util_top/export_crumble_url.cc
src/stim/util_top/export_qasm.cc
src/stim/util_top/export_quirk_url.cc
diff --git a/file_lists/test_files b/file_lists/test_files
index 5c986278b..ef8d28c8e 100644
--- a/file_lists/test_files
+++ b/file_lists/test_files
@@ -86,6 +86,7 @@ src/stim/util_top/circuit_inverse_unitary.test.cc
src/stim/util_top/circuit_to_detecting_regions.test.cc
src/stim/util_top/circuit_vs_amplitudes.test.cc
src/stim/util_top/circuit_vs_tableau.test.cc
+src/stim/util_top/dem_to_matrix.test.cc
src/stim/util_top/export_crumble_url.test.cc
src/stim/util_top/export_qasm.test.cc
src/stim/util_top/export_quirk_url.test.cc
diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi
index d08243a74..217cb7f72 100644
--- a/glue/python/src/stim/__init__.pyi
+++ b/glue/python/src/stim/__init__.pyi
@@ -5069,6 +5069,61 @@ class DetectorErrorModel:
>>> contents
'error(0.25) D2 D3\n'
"""
+ def to_simple_error_lists(
+ self,
+ ) -> Tuple['np.ndarray[float]', List[Set[int]], List[Set[int]]]:
+ """Simplifies the model into lists of chances, detection sets, and obs flips sets.
+
+ Note that this summary doesn't include information about loops, coordinates, or
+ suggested decompositions. Also note that this method will merge all errors with
+ identical symptoms, by Bernoulli summing their probabilities, and will sort the
+ errors. For example, the following two detector error models will give identical
+ results:
+
+ # dem 1
+ error(0.01) D1 D2 ^ D3 D4
+ error(0.125) D1
+ error(0.25) D1
+ detector(2, 3, 5) D1
+
+ vs
+
+ # dem 2
+ error(0.3125) D1
+ error(0.01) D1 D2 D3 D4
+
+ Returns:
+ A tuple of sequences (probabilities, dets, obs).
+
+ `probabilities` will be a numpy array with dtype=np.float64 and
+ shape=(num_errors,).
+
+ `dets` will be a list of detection event sets. Each detection
+ event set is a python set containing the indices of detectors
+ flipped by the error.
+
+ `obs` will be a list of observable flip sets. Each observable
+ flip set is a python set containing the indices of detectors
+ flipped by the error.
+
+ These sequences are paired up by index. They will have the same length, and
+ the combination of probabilities[k], dets[k], and obs[k] form a single
+ error.
+
+ Examples:
+ >>> import stim
+ >>> dem = stim.DetectorErrorModel('''
+ ... error(0.25) D0 D1
+ ... error(0.125) D0 D2 L1 L5
+ ... ''')
+ >>> probs, dets, obs = dem.to_simple_error_lists()
+ >>> probs
+ array([0.25 , 0.125])
+ >>> dets
+ [{0, 1}, {0, 2}]
+ >>> obs
+ [set(), {1, 5}]
+ """
class ExplainedError:
"""Describes the location of an error mechanism from a stim circuit.
"""
diff --git a/src/stim.h b/src/stim.h
index 090c6d585..bc69d9793 100644
--- a/src/stim.h
+++ b/src/stim.h
@@ -112,6 +112,7 @@
#include "stim/util_top/circuit_to_detecting_regions.h"
#include "stim/util_top/circuit_vs_amplitudes.h"
#include "stim/util_top/circuit_vs_tableau.h"
+#include "stim/util_top/dem_to_matrix.h"
#include "stim/util_top/export_crumble_url.h"
#include "stim/util_top/export_qasm.h"
#include "stim/util_top/export_quirk_url.h"
diff --git a/src/stim/dem/detector_error_model.pybind.cc b/src/stim/dem/detector_error_model.pybind.cc
index 2eb642efe..e57718149 100644
--- a/src/stim/dem/detector_error_model.pybind.cc
+++ b/src/stim/dem/detector_error_model.pybind.cc
@@ -23,10 +23,13 @@
#include "stim/dem/detector_error_model_target.pybind.h"
#include "stim/io/raii_file.h"
#include "stim/py/base.pybind.h"
+#include "stim/py/numpy.pybind.h"
#include "stim/search/search.h"
#include "stim/simulators/dem_sampler.h"
+#include "stim/util_top/dem_to_matrix.h"
using namespace stim;
+using namespace stim_pybind;
std::string stim_pybind::detector_error_model_repr(const DetectorErrorModel &self) {
if (self.instructions.empty()) {
@@ -1196,4 +1199,98 @@ void stim_pybind::pybind_detector_error_model_methods(
... print(diagram, file=f)
)DOC")
.data());
+
+ c.def(
+ "to_simple_error_lists",
+ [](const DetectorErrorModel &self) {
+ auto map = dem_to_map(self);
+ size_t num_errors = map.size();
+
+ pybind11::list dets_list;
+ pybind11::list obs_list;
+ double *probabilities = new double[num_errors];
+
+ size_t row = 0;
+ for (const auto &kv : map) {
+ probabilities[row] = kv.second;
+ pybind11::set dets;
+ pybind11::set obs;
+ for (DemTarget t : kv.first) {
+ if (t.is_relative_detector_id()) {
+ dets.add(t.val());
+ } else {
+ obs.add(t.val());
+ }
+ }
+ dets_list.append(dets);
+ obs_list.append(obs);
+ row++;
+ }
+
+ auto prob_array = pybind11::array_t(
+ {(pybind11::ssize_t)num_errors},
+ {(pybind11::ssize_t)sizeof(double)},
+ probabilities,
+ pybind11::capsule(probabilities, [](void *f) {
+ delete[] reinterpret_cast(f);
+ })
+ );
+
+ return pybind11::make_tuple(prob_array, dets_list, obs_list);
+ },
+ clean_doc_string(R"DOC(
+ @signature def to_simple_error_lists(self) -> Tuple['np.ndarray[float]', List[Set[int]], List[Set[int]]]:
+ Simplifies the model into lists of chances, detection sets, and obs flips sets.
+
+ Note that this summary doesn't include information about loops, coordinates, or
+ suggested decompositions. Also note that this method will merge all errors with
+ identical symptoms, by Bernoulli summing their probabilities, and will sort the
+ errors. For example, the following two detector error models will give identical
+ results:
+
+ # dem 1
+ error(0.01) D1 D2 ^ D3 D4
+ error(0.125) D1
+ error(0.25) D1
+ detector(2, 3, 5) D1
+
+ vs
+
+ # dem 2
+ error(0.3125) D1
+ error(0.01) D1 D2 D3 D4
+
+ Returns:
+ A tuple of sequences (probabilities, dets, obs).
+
+ `probabilities` will be a numpy array with dtype=np.float64 and
+ shape=(num_errors,).
+
+ `dets` will be a list of detection event sets. Each detection
+ event set is a python set containing the indices of detectors
+ flipped by the error.
+
+ `obs` will be a list of observable flip sets. Each observable
+ flip set is a python set containing the indices of detectors
+ flipped by the error.
+
+ These sequences are paired up by index. They will have the same length, and
+ the combination of probabilities[k], dets[k], and obs[k] form a single
+ error.
+
+ Examples:
+ >>> import stim
+ >>> dem = stim.DetectorErrorModel('''
+ ... error(0.25) D0 D1
+ ... error(0.125) D0 D2 L1 L5
+ ... ''')
+ >>> probs, dets, obs = dem.to_simple_error_lists()
+ >>> probs
+ array([0.25 , 0.125])
+ >>> dets
+ [{0, 1}, {0, 2}]
+ >>> obs
+ [set(), {1, 5}]
+ )DOC")
+ .data());
}
diff --git a/src/stim/dem/detector_error_model_pybind_test.py b/src/stim/dem/detector_error_model_pybind_test.py
index cd479aa92..6c8892bd4 100644
--- a/src/stim/dem/detector_error_model_pybind_test.py
+++ b/src/stim/dem/detector_error_model_pybind_test.py
@@ -14,6 +14,7 @@
import pathlib
import tempfile
+import numpy as np
import pytest
import stim
@@ -536,3 +537,41 @@ def test_shortest_graphlike_error_remnant():
assert len(d.shortest_graphlike_error(ignore_ungraphlike_errors=True)) == 8
assert len(c.shortest_graphlike_error()) == 8
assert len(d.shortest_graphlike_error()) == 8
+
+
+def test_to_simple_error_lists():
+ dem = stim.DetectorErrorModel("""
+ error(0.125) D0 D2 L1 L5
+ error(0.25) D0 D1
+ """)
+ probs, dets, obs = dem.to_simple_error_lists()
+ np.testing.assert_array_equal(probs, np.array([0.25, 0.125], dtype=np.float64))
+ assert dets == [
+ {0, 1},
+ {0, 2},
+ ]
+ assert obs == [
+ set(),
+ {1, 5},
+ ]
+
+
+def test_to_simple_error_lists_simplify():
+ dem = stim.DetectorErrorModel("""
+ error(0.125) D0 D2 L1 L5
+ error(0.25) D0 ^ D1
+ error(0) D0 D1
+ error(0) D2
+ """)
+ probs, dets, obs = dem.to_simple_error_lists()
+ np.testing.assert_array_equal(probs, np.array([0.25, 0.125, 0], dtype=np.float64))
+ assert dets == [
+ {0, 1},
+ {0, 2},
+ {2},
+ ]
+ assert obs == [
+ set(),
+ {1, 5},
+ set(),
+ ]
diff --git a/src/stim/stabilizers/pauli_string_ref.inl b/src/stim/stabilizers/pauli_string_ref.inl
index 5d8a95994..943b67633 100644
--- a/src/stim/stabilizers/pauli_string_ref.inl
+++ b/src/stim/stabilizers/pauli_string_ref.inl
@@ -883,9 +883,9 @@ template
void PauliStringRef::do_ZCX(const CircuitInstruction &inst) {
const auto &targets = inst.targets;
assert((targets.size() & 1) == 0);
- for (size_t k = 0; k < inst.targets.size(); k += 2) {
- size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k;
- size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data;
+ for (size_t k = 0; k < targets.size(); k += 2) {
+ size_t k2 = reverse_order ? targets.size() - 2 - k : k;
+ size_t q1 = targets[k2].data, q2 = targets[k2 + 1].data;
do_single_cx(inst, q1, q2);
}
}
@@ -895,9 +895,9 @@ template
void PauliStringRef::do_ZCY(const CircuitInstruction &inst) {
const auto &targets = inst.targets;
assert((targets.size() & 1) == 0);
- for (size_t k = 0; k < inst.targets.size(); k += 2) {
- size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k;
- size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data;
+ for (size_t k = 0; k < targets.size(); k += 2) {
+ size_t k2 = reverse_order ? targets.size() - 2 - k : k;
+ size_t q1 = targets[k2].data, q2 = targets[k2 + 1].data;
do_single_cy(inst, q1, q2);
}
}
@@ -916,9 +916,9 @@ template
void PauliStringRef::do_SWAP(const CircuitInstruction &inst) {
const auto &targets = inst.targets;
assert((targets.size() & 1) == 0);
- for (size_t k = 0; k < inst.targets.size(); k += 2) {
- size_t k2 = reverse_order ? inst.targets.size() - 2 - k : k;
- size_t q1 = inst.targets[k2].data, q2 = inst.targets[k2 + 1].data;
+ for (size_t k = 0; k < targets.size(); k += 2) {
+ size_t k2 = reverse_order ? targets.size() - 2 - k : k;
+ size_t q1 = targets[k2].data, q2 = targets[k2 + 1].data;
zs[q1].swap_with(zs[q2]);
xs[q1].swap_with(xs[q2]);
}
diff --git a/src/stim/util_top/dem_to_matrix.cc b/src/stim/util_top/dem_to_matrix.cc
new file mode 100644
index 000000000..6544968b6
--- /dev/null
+++ b/src/stim/util_top/dem_to_matrix.cc
@@ -0,0 +1,25 @@
+#include "stim/util_top/dem_to_matrix.h"
+
+using namespace stim;
+
+std::map, double> stim::dem_to_map(const DetectorErrorModel &dem) {
+ std::map, double> result;
+ SparseXorVec buf;
+ dem.iter_flatten_error_instructions([&](DemInstruction instruction) {
+ if (instruction.type != DemInstructionType::DEM_ERROR) {
+ return;
+ }
+ buf.sorted_items.clear();
+ for (DemTarget t: instruction.target_data) {
+ if (t.is_observable_id() || t.is_relative_detector_id()) {
+ buf.sorted_items.push_back(t);
+ }
+ }
+ size_t kept = xor_sort(buf.sorted_items);
+ buf.sorted_items.resize(kept);
+ auto q = instruction.arg_data[0];
+ auto &p = result[buf];
+ p = p * (1 - q) + q * (1 - p);
+ });
+ return result;
+}
diff --git a/src/stim/util_top/dem_to_matrix.h b/src/stim/util_top/dem_to_matrix.h
new file mode 100644
index 000000000..581a5a135
--- /dev/null
+++ b/src/stim/util_top/dem_to_matrix.h
@@ -0,0 +1,43 @@
+#ifndef _STIM_UTIL_TOP_DEM_TO_MATRIX_H
+#define _STIM_UTIL_TOP_DEM_TO_MATRIX_H
+
+#include "stim/mem/sparse_xor_vec.h"
+#include "stim/dem/detector_error_model.h"
+
+namespace stim {
+
+std::map, double> dem_to_map(const DetectorErrorModel &dem);
+
+/// Sorts the given items, and cancels out duplicates.
+/// If an item appears an even number of times in the span, it is removed.
+/// If it appears an odd number of times, exactly one instance is kept.
+///
+/// Args:
+/// target: The span of items to xor-sort.
+///
+/// Returns:
+/// The number of kept items. Kept items are moved to the start of the range.
+template
+size_t xor_sort(std::span target) {
+ if (target.empty()) {
+ return 0;
+ }
+ std::sort(target.begin(), target.end());
+ size_t kept = 0;
+ for (size_t k = 0; k < target.size(); k++) {
+ if (k + 1 < target.size() && target[k] == target[k + 1]) {
+ k += 1;
+ continue;
+ }
+ if (kept < k) {
+ target[kept] = std::move(target[k]);
+ }
+ kept++;
+ }
+
+ return kept;
+}
+
+} // namespace stim
+
+#endif
diff --git a/src/stim/util_top/dem_to_matrix.test.cc b/src/stim/util_top/dem_to_matrix.test.cc
new file mode 100644
index 000000000..c0f836795
--- /dev/null
+++ b/src/stim/util_top/dem_to_matrix.test.cc
@@ -0,0 +1,58 @@
+#include "stim/util_top/dem_to_matrix.h"
+
+#include "gtest/gtest.h"
+
+using namespace stim;
+
+TEST(dem_to_matrix, xor_sort) {
+ std::vector vals;
+ size_t v;
+
+ vals = {};
+ v = xor_sort(vals);
+ ASSERT_EQ(v, 0);
+ ASSERT_EQ(vals, (std::vector{}));
+
+ vals = {5};
+ v = xor_sort(vals);
+ ASSERT_EQ(v, 1);
+ ASSERT_EQ(vals, (std::vector{5}));
+
+ vals = {5, 6};
+ v = xor_sort(vals);
+ ASSERT_EQ(v, 2);
+ ASSERT_EQ(vals, (std::vector{5, 6}));
+
+ vals = {6, 5};
+ v = xor_sort(vals);
+ ASSERT_EQ(v, 2);
+ ASSERT_EQ(vals, (std::vector{5, 6}));
+
+ vals = {5, 5};
+ v = xor_sort(vals);
+ ASSERT_EQ(v, 0);
+
+ vals = {5, 5, 5};
+ v = xor_sort(vals);
+ ASSERT_EQ(v, 1);
+ while (vals.size() > v) {
+ vals.pop_back();
+ }
+ ASSERT_EQ(vals, (std::vector{5}));
+
+ vals = {5, 6, 5, 6, 5};
+ v = xor_sort(vals);
+ ASSERT_EQ(v, 1);
+ while (vals.size() > v) {
+ vals.pop_back();
+ }
+ ASSERT_EQ(vals, (std::vector{5}));
+
+ vals = {5, 6, 5, 6, 5, 2, 3, 5};
+ v = xor_sort(vals);
+ ASSERT_EQ(v, 2);
+ while (vals.size() > v) {
+ vals.pop_back();
+ }
+ ASSERT_EQ(vals, (std::vector{2, 3}));
+}