Skip to content

Commit 0834c26

Browse files
authored
Add get_logical_error_and_discard_rate (#449)
* factor out get_logical_error_and_discard_rate * remove get_logical_error_and_discard_rates * move MemoryExperimentParts * fix JSON serializability issue * minor notebook bugfix * keyword args * fix coverage
1 parent 7886637 commit 0834c26

7 files changed

Lines changed: 116 additions & 123 deletions

File tree

examples/logical_error_rates/5_state_preparation.ipynb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -239,15 +239,15 @@
239239
}
240240
],
241241
"source": [
242-
"logical_error_rates, discard_rates = circuits.get_logical_error_and_discard_rates(\n",
243-
" code,\n",
244-
" state_prep_circuit,\n",
245-
" error_rates,\n",
246-
" noise_model_family,\n",
247-
" sinter_decoder=sinter_decoder,\n",
248-
" num_samples=10**6,\n",
249-
" post_select_on_flags=True,\n",
250-
")\n",
242+
"logical_error_rates = np.empty(len(tasks))\n",
243+
"discard_rates = np.empty(len(tasks))\n",
244+
"for tt, task in enumerate(tasks):\n",
245+
" logical_error_rates[tt], discard_rates[tt] = circuits.get_logical_error_and_discard_rate(\n",
246+
" task.circuit,\n",
247+
" sinter_decoder=sinter_decoder,\n",
248+
" num_samples=10**6,\n",
249+
" flags=task.json_metadata[\"flags\"],\n",
250+
" )\n",
251251
"\n",
252252
"# plot simulation results!\n",
253253
"plot_error_and_discard_rates(error_rates, logical_error_rates, discard_rates)\n",

src/qldpc/circuits/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from .alpha_syndrome import AlphaSyndrome
22
from .benchmarking import (
3-
get_logical_error_and_discard_rates,
3+
get_logical_error_and_discard_rate,
44
get_nontrivial_logical_stabilizers,
55
get_state_prep_diagnostic_circuit,
66
get_state_prep_diagnostic_tasks,
77
)
88
from .bookkeeping import (
99
DetectorRecord,
1010
MeasurementRecord,
11-
MemoryExperimentParts,
1211
QubitIDs,
1312
Record,
1413
)
@@ -22,6 +21,7 @@
2221
with_remapped_qubits,
2322
)
2423
from .memory import (
24+
MemoryExperimentParts,
2525
get_logical_bell_prep,
2626
get_memory_experiment,
2727
get_memory_experiment_parts,
@@ -49,13 +49,12 @@
4949

5050
__all__ = [
5151
"AlphaSyndrome",
52-
"get_logical_error_and_discard_rates",
52+
"get_logical_error_and_discard_rate",
5353
"get_nontrivial_logical_stabilizers",
5454
"get_state_prep_diagnostic_circuit",
5555
"get_state_prep_diagnostic_tasks",
5656
"DetectorRecord",
5757
"MeasurementRecord",
58-
"MemoryExperimentParts",
5958
"QubitIDs",
6059
"Record",
6160
"get_encoder_and_decoder",
@@ -65,6 +64,7 @@
6564
"get_pauli_product_measurements",
6665
"restrict_to_qubits",
6766
"with_remapped_qubits",
67+
"MemoryExperimentParts",
6868
"get_logical_bell_prep",
6969
"get_memory_experiment",
7070
"get_memory_experiment_parts",

src/qldpc/circuits/benchmarking.py

Lines changed: 69 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -284,114 +284,92 @@ def get_state_prep_diagnostic_tasks(
284284
sinter.Task(
285285
circuit=noise_model_family(error_rate).noisy_circuit(diagnostic_circuit),
286286
postselection_mask=postselection_mask_bit_packed,
287-
json_metadata={"p": error_rate},
287+
json_metadata={"p": error_rate, "flags": detector_record.get_events("flag")},
288288
)
289289
for error_rate in error_rates
290290
]
291291

292292

293-
def get_logical_error_and_discard_rates(
294-
code: codes.QuditCode,
295-
state_prep_circuit: stim.Circuit,
296-
error_rates: Sequence[float] | npt.NDArray[np.floating],
297-
noise_model_family: Callable[[float], NoiseModel] = DepolarizingNoiseModel,
293+
def get_logical_error_and_discard_rate(
294+
circuit_or_dem: stim.Circuit | stim.DetectorErrorModel,
295+
sinter_decoder: sinter.Decoder,
298296
*,
299-
sinter_decoder: sinter.Decoder | Sequence[sinter.Decoder],
300-
num_samples: int | Sequence[int],
301-
observables: npt.NDArray[np.int_]
302-
| Sequence[Sequence[int]]
303-
| Sequence[stim.PauliString]
304-
| None = None,
305-
post_select_on_flags: bool = False,
306-
skip_validation: bool = False,
307-
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
308-
"""Compute logical error rates of the provided logical state prep circuit for the provided code.
309-
310-
The first len(code) qubits addressed by the circuit must be the data qubits of the code.
297+
num_samples: int,
298+
flags: Sequence[int] | None = None,
299+
) -> tuple[float, float]:
300+
"""Compute a logical error rate and discard rate from samples of the provided cirucit.
311301
312302
Each logical error rate is a fraction of the (possibly post-selected) shots in which observable
313303
flips are predicted incorrectly by the provided decoder.
314304
315-
This method is provided as an alternative to get_state_prep_diagnostic_tasks, which currently
316-
cannot support post-selection due to a sinter bug: https://github.com/quantumlib/Stim/pull/844
317-
Once this bug is fixed, it is recommended to instead use get_state_prep_diagnostic_tasks.
305+
This method is provided as an alternative to sinter, which currently cannot support post
306+
selection due to an outstanding bug: https://github.com/quantumlib/Stim/pull/844
307+
Once the bug is fixed, it is recommended to instead build a sinter.Task and call sinter.collect.
308+
309+
The sinter.Task would use the post-selection flags as follows:
310+
postselection_mask_bits = np.zeros(circuit_or_dem.num_detectors, dtype=int)
311+
postselection_mask_bits[flags] = 1
312+
postselection_mask = np.packbits(postselection_mask, bitorder="little")
313+
task = sinter.Task(
314+
circuit=circuit,
315+
postselection_mask=postselection_mask_bit_packed,
316+
)
317+
Sampling data would then be collected with:
318+
stats = sinter.collect(
319+
tasks=[task], # or more maybe more tasks
320+
decoders=["custom"],
321+
custom_decoders={"custom": sinter_decoder},
322+
num_shots=num_samples,
323+
# other options such as num_workers=os.cpu_count() or max_errors=100,
324+
)
318325
319326
Args:
320-
code: The code whose logical state is prepared by the provided state_prep_circuit.
321-
state_prep_circuit: A circuit that prepares a logical state of the provided code.
322-
error_rates: The error rates at which to evaluate the provided family of noise models.
323-
noise_model_family: A single-parameter family of noise models for adding noise to circuits.
324-
Default: qldpc.circuits.DepolarizingNoiseModel.
327+
circuit_or_dem: The circuit or detector error model we wish to sample.
328+
sinter_decoder: The circuit-level decoder used to predict observable flips.
325329
326330
Keyword args:
327-
sinter_decoder: The circuit-level decoder used to predict observable flips, or a sequence of
328-
circuit-level decoders (one for each error rate).
329-
num_samples: The number of times to sample each noisy circuit, or a sequence of sample
330-
numbers (one for each error rate).
331-
observables: The observables that should stabilize the prepared state, or (by default) None.
332-
If not None, the observables should be either a a matrix of symplectic row vectors, with
333-
shape (num_observables, 2 * len(code)), or a sequence of Pauli strings supported on the
334-
data qubits of the code. If None, observables are determined automatically by finding
335-
all logical Pauli operators of the code that stabilize the state prepared by
336-
state_prep_circuit.
337-
post_select_on_flags: If True, post-select samples on nonzero measurement outcomes in the
338-
provided state_prep_circuit. Default: False.
339-
skip_validation: If True, skip the check to assert that the provided circuit prepares a
340-
logical state fo the provided code.
331+
num_samples: The number of times to the circuit_or_dem.
332+
flags: The detectors in circuit_or_dem to post-select on.
341333
342334
Returns:
343-
An array of estimated logical error rates.
344-
An array of discard rates, or the fraction of shots (for each simulated error rate) that
345-
were discarded due to post-selection on state prep flags. If post_select_on_flags is
346-
False, this array contains only zeros.
335+
A fraction of samples in which at least one observable was decoded incorrectly.
336+
A fraction of samples that were discarded due to post-selection.
347337
"""
348-
diagnostic_circuit, detector_record = get_state_prep_diagnostic_circuit(
349-
code, state_prep_circuit, observables=observables
350-
)
351-
if not isinstance(num_samples, Sequence):
352-
num_samples = [num_samples] * len(error_rates)
353-
if not isinstance(sinter_decoder, Sequence):
354-
sinter_decoder = [sinter_decoder] * len(error_rates)
355-
356-
logical_error_rates = np.zeros(len(error_rates), dtype=float)
357-
discard_rates = np.zeros(len(error_rates), dtype=float)
358-
for pp, error_rate in enumerate(error_rates):
359-
# sample detector and observable flips in the circuit
360-
noise_model = noise_model_family(error_rate)
361-
noisy_circuit = noise_model.noisy_circuit(diagnostic_circuit)
362-
dem_arrays = decoders.DetectorErrorModelArrays(
363-
noisy_circuit.detector_error_model(), simplify=True
364-
)
365-
dem = dem_arrays.to_dem()
366-
sampler = dem.compile_sampler()
367-
det_data, obs_data, err_data = sampler.sample(shots=num_samples[pp])
368-
369-
# if applicable, post-select on flag detectors
370-
if post_select_on_flags:
371-
# identify shots and detectors to remove
372-
flag_dets = detector_record.get_events("flag")
373-
shot_mask = ~np.any(det_data[:, flag_dets], axis=1)
374-
detector_mask = np.ones(dem.num_detectors, dtype=bool)
375-
detector_mask[flag_dets] = False
376-
377-
# post-select simulated data
378-
det_data = det_data[shot_mask][:, detector_mask]
379-
obs_data = obs_data[shot_mask]
380-
dem = dem_arrays.post_selected_on(detector_record.get_events("flag")).to_dem()
381-
382-
# record the fraction of shots that were discarded
383-
discard_rates[pp] = 1 - np.sum(shot_mask) / len(shot_mask)
384-
385-
# compile a decoder for this detector error model
386-
compiled_sinter_decoder = sinter_decoder[pp].compile_decoder_for_dem(dem)
387-
388-
# decode and compute the logical error rate
389-
predicted_flips = compiled_sinter_decoder.decode_shots(det_data)
390-
obs_flips = obs_data ^ predicted_flips
391-
failures = np.any(obs_flips, axis=1)
392-
logical_error_rates[pp] = np.sum(failures) / len(failures)
393-
394-
return logical_error_rates, discard_rates
338+
# build and simplify a detector error model
339+
dem_arrays = decoders.DetectorErrorModelArrays(circuit_or_dem, simplify=True)
340+
dem = dem_arrays.to_dem()
341+
342+
# sample detector and observable flips in the circuit
343+
sampler = dem.compile_sampler()
344+
det_data, obs_data, err_data = sampler.sample(shots=num_samples)
345+
346+
# if applicable, post-select on flag detectors
347+
if flags:
348+
# identify shots and detectors to remove
349+
shot_mask = ~np.any(det_data[:, flags], axis=1)
350+
detector_mask = np.ones(dem.num_detectors, dtype=bool)
351+
detector_mask[flags] = False
352+
353+
# post-select simulated data
354+
det_data = det_data[shot_mask][:, detector_mask]
355+
obs_data = obs_data[shot_mask]
356+
dem = dem_arrays.post_selected_on(flags).to_dem()
357+
358+
# record the fraction of shots that were discarded
359+
discard_rate = 1 - np.sum(shot_mask) / len(shot_mask)
360+
else: # pragma: no cover
361+
discard_rate = 0
362+
363+
# compile a decoder for this detector error model
364+
compiled_sinter_decoder = sinter_decoder.compile_decoder_for_dem(dem)
365+
366+
# decode and compute the logical error rate
367+
predicted_flips = compiled_sinter_decoder.decode_shots(det_data)
368+
obs_flips = obs_data ^ predicted_flips
369+
failures = np.any(obs_flips, axis=1)
370+
logical_error_rate = np.sum(failures) / len(failures)
371+
372+
return logical_error_rate, discard_rate
395373

396374

397375
def _assert_logical_state_preparation(

src/qldpc/circuits/benchmarking_test.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,23 @@ def test_state_prep() -> None:
7878
for error_rate, task in zip(error_rates, tasks):
7979
assert task.json_metadata["p"] == error_rate
8080

81-
# cover alternative method for computing logical error rates
82-
logical_error_rates, discard_rates = circuits.get_logical_error_and_discard_rates(
81+
# find observables automatically
82+
task = circuits.get_state_prep_diagnostic_tasks(
8383
code,
8484
state_prep_circuit,
85-
error_rates=[0],
85+
error_rates[:1],
86+
noise_model_family,
87+
observables=None,
88+
post_select_on_flags=False,
89+
)[0]
90+
assert task == tasks[0]
91+
92+
# bypass sinter to compute logical error rates
93+
logical_error_rate, discard_rate = circuits.get_logical_error_and_discard_rate(
94+
task.circuit,
8695
sinter_decoder=decoders.SinterDecoder(),
8796
num_samples=1,
88-
observables=None, # construct automatically
89-
post_select_on_flags=True,
97+
flags=task.json_metadata["flags"],
9098
)
91-
assert np.array_equal(logical_error_rates, [0])
92-
assert np.array_equal(discard_rates, [0])
99+
assert logical_error_rate == 0
100+
assert discard_rate == 0

src/qldpc/circuits/bookkeeping.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import dataclasses
2323
import itertools
2424
from collections.abc import Hashable, ItemsView, Iterable, Iterator, Mapping, Sequence
25-
from typing import NamedTuple
2625

2726
import numpy as np
2827
import stim
@@ -274,12 +273,3 @@ def after_post_selection(self, key: Hashable) -> DetectorRecord:
274273
if other_key != key
275274
}
276275
)
277-
278-
279-
class MemoryExperimentParts(NamedTuple):
280-
initialization: stim.Circuit
281-
qec_cycle: stim.Circuit
282-
readout: stim.Circuit
283-
measurement_record: MeasurementRecord
284-
detector_record: DetectorRecord
285-
qubit_ids: QubitIDs

src/qldpc/circuits/memory.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@
1616
"""
1717

1818
from collections.abc import Collection, Sequence
19+
from typing import NamedTuple
1920

2021
import numpy as np
2122
import stim
2223

2324
from qldpc import codes
2425
from qldpc.objects import Node, Pauli, PauliXZ
2526

26-
from .bookkeeping import DetectorRecord, MeasurementRecord, MemoryExperimentParts, QubitIDs
27+
from .bookkeeping import DetectorRecord, MeasurementRecord, QubitIDs
2728
from .common import (
2829
get_encoding_circuit,
2930
get_pauli_product_measurements,
@@ -34,6 +35,15 @@
3435
from .syndrome_measurement import EdgeColoring, SyndromeMeasurementStrategy
3536

3637

38+
class MemoryExperimentParts(NamedTuple):
39+
initialization: stim.Circuit
40+
qec_cycle: stim.Circuit
41+
readout: stim.Circuit
42+
measurement_record: MeasurementRecord
43+
detector_record: DetectorRecord
44+
qubit_ids: QubitIDs
45+
46+
3747
def get_memory_experiment(
3848
code: codes.QuditCode | codes.ClassicalCode,
3949
basis: PauliXZ | None = Pauli.X,

src/qldpc/decoders/dems.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,15 @@ class DetectorErrorModelArrays:
4444
observable_flip_matrix: scipy.sparse.csc_matrix # maps errors to observable flips
4545
error_probs: npt.NDArray[np.floating] # probability of occurrence for each error
4646

47-
def __init__(self, dem: stim.DetectorErrorModel, *, simplify: bool = True) -> None:
47+
def __init__(
48+
self, circuit_or_dem: stim.Circuit | stim.DetectorErrorModel, *, simplify: bool = True
49+
) -> None:
4850
"""Initialize from a stim.DetectorErrorModel."""
51+
dem = (
52+
circuit_or_dem.detector_error_model()
53+
if isinstance(circuit_or_dem, stim.Circuit)
54+
else circuit_or_dem
55+
)
4956
errors = DetectorErrorModelArrays.get_circuit_errors(dem)
5057
if simplify:
5158
errors = DetectorErrorModelArrays.get_merged_circuit_errors(errors)

0 commit comments

Comments
 (0)