|
| 1 | +import stim |
| 2 | +from functools import reduce |
| 3 | +import itertools |
| 4 | +from collections import defaultdict |
| 5 | +from collections.abc import Callable, Iterable |
| 6 | + |
| 7 | + |
| 8 | +def reduce_symmetric_difference(items: Iterable[int]) -> tuple[int]: |
| 9 | + """ |
| 10 | + Calculates the symmetric difference of a multiset of items. |
| 11 | +
|
| 12 | + Returns items that appear an odd number of times in the input. |
| 13 | + """ |
| 14 | + unpaired_set = reduce(lambda acc, i: acc ^ {i}, items, set()) |
| 15 | + return tuple(sorted(unpaired_set)) |
| 16 | + |
| 17 | + |
| 18 | +def reduce_set_symmetric_difference(sets: Iterable[Iterable[int]]) -> tuple[int]: |
| 19 | + return reduce_symmetric_difference(itertools.chain.from_iterable(sets)) |
| 20 | + |
| 21 | + |
| 22 | +def undecomposed_error_detectors_and_observables( |
| 23 | + instruction: stim.DemInstruction, |
| 24 | +) -> tuple[tuple[int], tuple[int]]: |
| 25 | + """Outputs the indices of the detectors and observables in a stim error, |
| 26 | + undecomposing the error if necessary.""" |
| 27 | + if instruction.type != "error": |
| 28 | + raise ValueError(f"DEM instruction must be an error, not {instruction.type}") |
| 29 | + detectors = reduce_symmetric_difference( |
| 30 | + d.val for d in instruction.targets_copy() if d.is_relative_detector_id() |
| 31 | + ) |
| 32 | + observables = reduce_symmetric_difference( |
| 33 | + o.val for o in instruction.targets_copy() if o.is_logical_observable_id() |
| 34 | + ) |
| 35 | + return detectors, observables |
| 36 | + |
| 37 | + |
| 38 | +def get_component_obs_matching_undecomposed_obs( |
| 39 | + obs_options_by_component: list[set[tuple[int]]], error_obs: tuple[int] |
| 40 | +) -> list[tuple[int]] | None: |
| 41 | + """Given the possible observables that could be a symptom of each component |
| 42 | + of a dem error, find the assignment of observables to components that is |
| 43 | + consistent with the observables associated with the undecomposed error. |
| 44 | + Returns None if there is no assignment that is consistent with the observables |
| 45 | + of the undecomposed error. |
| 46 | +
|
| 47 | + Parameters |
| 48 | + ---------- |
| 49 | + obs_options_by_component : list[set[tuple[int]]] |
| 50 | + The possible observables consistent with each component. Here |
| 51 | + `obs_options_by_component[i]` is a set of tuples, where each tuple |
| 52 | + contains the indices of observables that could have been flipped by |
| 53 | + component i. For example, these could be observables flipped by |
| 54 | + an undecomposable error elsewhere in the dem that has the same detectors |
| 55 | + as the component. Note that if there is more than one choice for a given |
| 56 | + component (i.e. if `len(obs_options_by_component[i]) > 1`) then the dem |
| 57 | + must have distance at most 2. If the distance is more than 2, then this |
| 58 | + function makes the trivial assignment of assigning the only possble |
| 59 | + observables to each component. |
| 60 | + error_obs : tuple[int] |
| 61 | + The observables flipped by the undecomposed error. |
| 62 | +
|
| 63 | + Returns |
| 64 | + ------- |
| 65 | + list[tuple[int]] |
| 66 | + Assignment of observables to each component. |
| 67 | + """ |
| 68 | + error_obs_set = set(reduce_symmetric_difference(error_obs)) |
| 69 | + for obs_combinations in itertools.product(*obs_options_by_component): |
| 70 | + obs_from_combination = reduce_set_symmetric_difference(obs_combinations) |
| 71 | + if set(obs_from_combination) == error_obs_set: |
| 72 | + return list(obs_combinations) |
| 73 | + return None |
| 74 | + |
| 75 | + |
| 76 | +def decompose_errors_using_detector_assignment( |
| 77 | + dem: stim.DetectorErrorModel, detector_component_func: Callable[[int], int] |
| 78 | +) -> stim.DetectorErrorModel: |
| 79 | + """Decomposes errors in the detector error model `dem` based on an assignment of |
| 80 | + detectors to components by the function `detector_component_func`. |
| 81 | +
|
| 82 | + An undecomposed error is an error that flips detectors that are all in the same |
| 83 | + component. A decomposed error is an error that flips detectors from more than one |
| 84 | + component, but is decomposed into components where each component corresponds |
| 85 | + to an undecomposed error elsewhere in the dem. The symmetric difference of the |
| 86 | + detectors and observables in the components of a decomposed error will equal |
| 87 | + the detectors and observables of the original error in the dem. |
| 88 | + See https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md#error-instruction |
| 89 | + for more details on the Stim ERROR instruction format, including decomposition. |
| 90 | + If the dem provided was already decomposed, this decomposition will be ignored |
| 91 | + (each error will be undecomposed before the new decomposition is applied). |
| 92 | +
|
| 93 | + Parameters |
| 94 | + ---------- |
| 95 | + dem : stim.DetectorErrorModel |
| 96 | + The detector error model to decompose. |
| 97 | + detector_component_func : Callable[[int], int] |
| 98 | + A function that maps a detector id to its component. i.e. This could map |
| 99 | + a detector index to 0 if it is X-type or to 1 if it is Z-type. |
| 100 | +
|
| 101 | + Returns |
| 102 | + ------- |
| 103 | + stim.DetectorErrorModel |
| 104 | + The decomposed detector error model |
| 105 | + """ |
| 106 | + dem = dem.flattened() |
| 107 | + |
| 108 | + single_component_dets_to_obs: dict[tuple[int], set[tuple[int]]] = defaultdict(set) |
| 109 | + |
| 110 | + for instruction in dem: |
| 111 | + if instruction.type != "error": |
| 112 | + continue |
| 113 | + |
| 114 | + detectors, observables = undecomposed_error_detectors_and_observables( |
| 115 | + instruction=instruction |
| 116 | + ) |
| 117 | + |
| 118 | + if len(set(detector_component_func(d) for d in detectors)) == 1: |
| 119 | + single_component_dets_to_obs[detectors].add(observables) |
| 120 | + |
| 121 | + output_dem = stim.DetectorErrorModel() |
| 122 | + |
| 123 | + for instruction in dem: |
| 124 | + if instruction.type != "error": |
| 125 | + output_dem.append(instruction) |
| 126 | + continue |
| 127 | + |
| 128 | + detectors, observables = undecomposed_error_detectors_and_observables( |
| 129 | + instruction=instruction |
| 130 | + ) |
| 131 | + det_components = {d: detector_component_func(d) for d in detectors} |
| 132 | + unique_components = sorted(set(det_components.values())) |
| 133 | + num_components = len(unique_components) |
| 134 | + |
| 135 | + dets_by_component = [] |
| 136 | + obs_options_by_component = [] |
| 137 | + |
| 138 | + for c in unique_components: |
| 139 | + component_dets = tuple( |
| 140 | + sorted(d for d in detectors if det_components[d] == c) |
| 141 | + ) |
| 142 | + if component_dets not in single_component_dets_to_obs: |
| 143 | + raise ValueError( |
| 144 | + f"The dem error `{instruction}` needs to be decomposed into components, however " |
| 145 | + f"the component with detectors {component_dets} is not present as its own error " |
| 146 | + "in the dem." |
| 147 | + ) |
| 148 | + dets_by_component.append(component_dets) |
| 149 | + obs_options_by_component.append( |
| 150 | + single_component_dets_to_obs[component_dets] |
| 151 | + ) |
| 152 | + |
| 153 | + # Assign observables to each component, such that they are consistent with the |
| 154 | + # observables of the undecomposed error |
| 155 | + consistent_obs_by_component = get_component_obs_matching_undecomposed_obs( |
| 156 | + obs_options_by_component=obs_options_by_component, error_obs=observables |
| 157 | + ) |
| 158 | + |
| 159 | + if consistent_obs_by_component is None: |
| 160 | + raise ValueError( |
| 161 | + f"The error instruction `{instruction}` could not be decomposed, due to its " |
| 162 | + "observables not being consistent with the observables of any available " |
| 163 | + f"choices of components." |
| 164 | + ) |
| 165 | + |
| 166 | + targets = [] |
| 167 | + for i in range(num_components): |
| 168 | + targets.extend( |
| 169 | + stim.target_relative_detector_id(d) for d in dets_by_component[i] |
| 170 | + ) |
| 171 | + targets.extend( |
| 172 | + stim.target_logical_observable_id(o) |
| 173 | + for o in consistent_obs_by_component[i] |
| 174 | + ) |
| 175 | + if i != num_components - 1: |
| 176 | + targets.append(stim.target_separator()) |
| 177 | + |
| 178 | + decomposed_instruction = stim.DemInstruction( |
| 179 | + type=instruction.type, |
| 180 | + args=instruction.args_copy(), |
| 181 | + targets=targets, |
| 182 | + tag=instruction.tag, |
| 183 | + ) |
| 184 | + output_dem.append(decomposed_instruction) |
| 185 | + |
| 186 | + return output_dem |
| 187 | + |
| 188 | + |
| 189 | +def decompose_errors_using_detector_coordinate_assignment( |
| 190 | + dem: stim.DetectorErrorModel, coord_to_component_func: Callable[[list[float]], int] |
| 191 | +) -> stim.DetectorErrorModel: |
| 192 | + """Decomposes errors in the detector error model `dem` based on an assignment of |
| 193 | + detectors to components using a function of the detector coordinates. |
| 194 | +
|
| 195 | + A detector with coordinates `coords` is assigned to component |
| 196 | + `coord_to_component_func(coords)`. If an error flips detectors that are all |
| 197 | + in component `i` then this error itself is assigned as an error in component `i`. |
| 198 | + This error is said to be undecomposable. If an error flips a set of detectors that |
| 199 | + belong to more than one component, then this function attempts to decompose the |
| 200 | + error into undecomposable errors (i.e. errors with detectors in a single component). |
| 201 | + For a definition of errors and decompositions see: |
| 202 | + https://github.com/quantumlib/Stim/blob/main/doc/file_format_dem_detector_error_model.md#error-instruction. |
| 203 | +
|
| 204 | +
|
| 205 | + Parameters |
| 206 | + ---------- |
| 207 | + dem : stim.DetectorErrorModel |
| 208 | + The detector error model to decompose |
| 209 | + coord_to_component_func : Callable[[list[float]], int] |
| 210 | + A function that coordinates of a detector to an integer corresponding to |
| 211 | + the index of a component, to be used for the decomposition. The coordinates |
| 212 | + are provided as a list of floats. |
| 213 | +
|
| 214 | + Returns |
| 215 | + ------- |
| 216 | + stim.DetectorErrorModel |
| 217 | + The decomposed detector error model. Note that the DEM will also be flattened. |
| 218 | + """ |
| 219 | + detector_coords = dem.get_detector_coordinates() |
| 220 | + |
| 221 | + def component_using_coords(detector_id: int) -> int: |
| 222 | + return coord_to_component_func(detector_coords[detector_id]) |
| 223 | + |
| 224 | + return decompose_errors_using_detector_assignment( |
| 225 | + dem=dem, detector_component_func=component_using_coords |
| 226 | + ) |
| 227 | + |
| 228 | + |
| 229 | +def detector_coord_to_basis_for_stim_surface_code_convention(coord: tuple[int]) -> int: |
| 230 | + """For detector coordinates consistent with the stim.Circuit.generated |
| 231 | + surface code circuits, return the basis from the detector coordinate. |
| 232 | + Returns 0 for X basis and 1 for Z basis detector.""" |
| 233 | + x = coord[0] |
| 234 | + y = coord[1] |
| 235 | + return 1 - ((x // 2 + y // 2) % 2) |
| 236 | + |
| 237 | + |
| 238 | +def decompose_errors_using_last_coordinate_index( |
| 239 | + dem: stim.DetectorErrorModel, |
| 240 | +) -> stim.DetectorErrorModel: |
| 241 | + """Decomposes errors in the detector error model `dem` based on an assignment of |
| 242 | + detectors to components by the last element of each detector coordinate. |
| 243 | +
|
| 244 | + An undecomposed error is an error that flips detectors that are all in the same |
| 245 | + component. A decomposed error is an error that flips detectors from more than one |
| 246 | + component, but is decomposed into components where each component corresponds |
| 247 | + to an undecomposed error elsewhere in the dem. The symmetric difference of the |
| 248 | + detectors and observables in the components of a decomposed error will equal |
| 249 | + the detectors and observables of the original error in the dem. |
| 250 | + If the dem provided was already decomposed, this decomposition will be ignored |
| 251 | + (each error will be undecomposed before the new decomposition is applied). |
| 252 | +
|
| 253 | + Parameters |
| 254 | + ---------- |
| 255 | + dem : stim.DetectorErrorModel |
| 256 | + The detector error model to decompose. |
| 257 | +
|
| 258 | + Returns |
| 259 | + ------- |
| 260 | + stim.DetectorErrorModel |
| 261 | + The decomposed detector error model |
| 262 | + """ |
| 263 | + detector_coords = dem.get_detector_coordinates() |
| 264 | + |
| 265 | + def last_coordinate_component(detector_id: int) -> int: |
| 266 | + return detector_coords[detector_id][-1] |
| 267 | + |
| 268 | + return decompose_errors_using_detector_assignment( |
| 269 | + dem=dem, detector_component_func=last_coordinate_component |
| 270 | + ) |
| 271 | + |
| 272 | + |
| 273 | +def decompose_errors_for_stim_surface_code_coords( |
| 274 | + dem: stim.DetectorErrorModel, |
| 275 | +) -> stim.DetectorErrorModel: |
| 276 | + """Decomposes the errors in the dem, such that each component |
| 277 | + of a decomposed error only triggers detectors of one basis (X or Z) |
| 278 | + based on an assignment of detector coordinates to X or Z basis |
| 279 | + consistent with the convention used in stim.Circuit.generated |
| 280 | + surface code circuits. |
| 281 | +
|
| 282 | + A detector is assumed to be X-type if `(x // 2 + y // 2) % 2 == 1` |
| 283 | + and is assumed to be Z-type if `(x // 2 + y // 2) % 2 == 0` where |
| 284 | + the detector has coordinates (x, y, ...). |
| 285 | +
|
| 286 | + Parameters |
| 287 | + ---------- |
| 288 | + dem : stim.DetectorErrorModel |
| 289 | + The detector error model to decompose |
| 290 | +
|
| 291 | + Returns |
| 292 | + ------- |
| 293 | + stim.DetectorErrorModel |
| 294 | + The decomposed detector error model |
| 295 | + """ |
| 296 | + detector_coords = dem.get_detector_coordinates() |
| 297 | + |
| 298 | + def stim_surface_code_det_component(detector_id: int) -> int: |
| 299 | + return detector_coord_to_basis_for_stim_surface_code_convention( |
| 300 | + detector_coords[detector_id] |
| 301 | + ) |
| 302 | + |
| 303 | + return decompose_errors_using_detector_assignment( |
| 304 | + dem=dem, detector_component_func=stim_surface_code_det_component |
| 305 | + ) |
| 306 | + |
| 307 | + |
| 308 | +def undecompose_errors(dem: stim.DetectorErrorModel) -> stim.DetectorErrorModel: |
| 309 | + """Returns a detector error model with any error decompositions removed. |
| 310 | +
|
| 311 | + If an error is decomposed into components in the dem, it will be replaced with a |
| 312 | + single undecomposed error instruction (of the same probability) with detectors |
| 313 | + equal to the symmetric difference of the detectors of the components, and |
| 314 | + likewise for the observables. Repeat blocks are preserved, rather than flattened. |
| 315 | +
|
| 316 | + Parameters |
| 317 | + ---------- |
| 318 | + dem : stim.DetectorErrorModel |
| 319 | + The detector error model to undecompose |
| 320 | +
|
| 321 | + Returns |
| 322 | + ------- |
| 323 | + stim.DetectorErrorModel |
| 324 | + The undecomposed detector error model |
| 325 | + """ |
| 326 | + undecomposed_dem = stim.DetectorErrorModel() |
| 327 | + for instruction in dem: |
| 328 | + if instruction.type == "repeat": |
| 329 | + undecomposed_dem.append( |
| 330 | + instruction=stim.DemRepeatBlock( |
| 331 | + repeat_count=instruction.repeat_count, |
| 332 | + block=undecompose_errors(instruction.body_copy()), |
| 333 | + ) |
| 334 | + ) |
| 335 | + continue |
| 336 | + |
| 337 | + if instruction.type != "error": |
| 338 | + undecomposed_dem.append(instruction=instruction) |
| 339 | + continue |
| 340 | + |
| 341 | + detectors, observables = undecomposed_error_detectors_and_observables( |
| 342 | + instruction=instruction |
| 343 | + ) |
| 344 | + |
| 345 | + targets = [stim.target_relative_detector_id(d) for d in detectors] + [ |
| 346 | + stim.target_logical_observable_id(o) for o in observables |
| 347 | + ] |
| 348 | + |
| 349 | + undecomposed_dem.append( |
| 350 | + stim.DemInstruction( |
| 351 | + type=instruction.type, |
| 352 | + args=instruction.args_copy(), |
| 353 | + targets=targets, |
| 354 | + tag=instruction.tag, |
| 355 | + ) |
| 356 | + ) |
| 357 | + return undecomposed_dem |
0 commit comments