diff --git a/glue/stimflow/doc/api.md b/glue/stimflow/doc/api.md index 6baa8bfd..c69521f7 100644 --- a/glue/stimflow/doc/api.md +++ b/glue/stimflow/doc/api.md @@ -1381,18 +1381,74 @@ def append( self, appended: Chunk | ChunkLoop | ChunkReflow, ) -> None: - """Appends a chunk to the circuit being built. + """Appends a circuit chunk, or other object, to the circuit being built. - The input flows of the appended chunk must exactly match the open outgoing flows of the - circuit so far. + The input flows of the appended chunk must exactly match the open outgoing flows of + the circuit so far. Args: appended: The object to append to the circuit. + This can be a Chunk, a ChunkReflow, or a ChunkLoop. + Unless `skip_verification_before_append=True` was specified when constructing the compiler, the `verify` method of this object will be called in order to ensure it is well form. If verification is skipped and the object is not well-formed, the compiler may output an invalid Stim circuit (e.g. with non-deterministic detectors). + + Examples: + >>> import stim + >>> import stimflow as sf + >>> zz = sf.PauliMap({0: 'Z', 1 + 1j: 'Z'}) + >>> lz = sf.PauliMap({0: 'Z'}, obs_name='L_REP_CODE_ZZ') + >>> idle_chunk = sf.Chunk( + ... stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... QUBIT_COORDS(0, 1) 1 + ... QUBIT_COORDS(1, 1) 2 + ... R 1 + ... CX 0 1 2 1 + ... M 1 + ... '''), + ... flows=[ + ... sf.Flow(start=zz, measurement_indices=[0]), + ... sf.Flow(end=zz, measurement_indices=[0]), + ... sf.Flow(start=lz, end=lz), + ... ] + ... ) + + >>> compiler = sf.ChunkCompiler() + >>> compiler.append(idle_chunk.start_code().transversal_init_chunk(basis='Z')) + >>> compiler.append(idle_chunk * 100) + >>> compiler.append(idle_chunk.end_code().transversal_measure_chunk(basis='Z')) + >>> compiler.finish_circuit() + stim.Circuit(''' + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 1) 2 + R 0 2 1 + CX 0 1 2 1 + M 1 + DETECTOR(0.5, 0.5, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + REPEAT 98 { + R 1 + CX 0 1 2 1 + M 1 + DETECTOR(0.5, 0.5, 0) rec[-2] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + } + R 1 + CX 0 1 2 1 + M 1 + DETECTOR(0.5, 0.5, 0) rec[-2] rec[-1] + SHIFT_COORDS(0, 0, 1) + M 2 0 + DETECTOR(0.5, 0.5, 0) rec[-3] rec[-2] rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + ''') """ ``` @@ -1624,9 +1680,51 @@ def finish_circuit( ) -> stim.Circuit: """Returns the circuit built by the compiler. - Performs some final translation steps: - - Re-indexing the qubits to be in a sorted order. - - Re-indexing the observables to omit discarded observable flows. + Also performs some final polishing steps on the circuit, such as re-indexing the + qubits to be in a sorted-by-position order and re-indexing the observables to omit + unused indices due to e.g. discarded observable flows. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> zz = sf.PauliMap({0: 'Z', 1 + 1j: 'Z'}) + >>> lz = sf.PauliMap({0: 'Z'}, obs_name='L_ZI') + >>> lx = sf.PauliMap({0: 'X', 1 + 1j: 'X'}, obs_name='L_XX') + >>> idle_chunk = sf.Chunk( + ... stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... QUBIT_COORDS(0, 1) 1 + ... QUBIT_COORDS(1, 1) 2 + ... R 1 + ... CX 0 1 2 1 + ... M 1 + ... '''), + ... flows=[ + ... sf.Flow(start=zz, measurement_indices=[0]), + ... sf.Flow(end=zz, measurement_indices=[0]), + ... sf.Flow(start=lz, end=lz), + ... sf.Flow(start=lx, end=lx), + ... ] + ... ) + + >>> compiler = sf.ChunkCompiler() + >>> compiler.append(idle_chunk.start_code().transversal_init_chunk(basis='Z')) + >>> compiler.append(idle_chunk) # Note: L_XX discarded by transversal chunks. + >>> compiler.append(idle_chunk.end_code().transversal_measure_chunk(basis='Z')) + >>> compiler.finish_circuit() + stim.Circuit(''' + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 1) 2 + R 0 2 1 + CX 0 1 2 1 + M 1 + DETECTOR(0.5, 0.5, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + M 2 0 + DETECTOR(0.5, 0.5, 0) rec[-3] rec[-2] rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + ''') """ ``` @@ -2048,6 +2146,47 @@ def from_auto_rewrite( inputs: Iterable[PauliMap], out2in: "dict[PauliMap, list[PauliMap] | Literal['auto']]", ) -> ChunkReflow: + """Creates a ChunkReflow while allowing for some products to solved automatically. + + In particular, the `out2in` dictionary can map an output to the string "auto" + instead of to an explicit list of PauliMap inputs. The method will then solve for + the product of inputs that produces the output. + + Args: + inputs: The input Pauli products that are available for use when producing an + output Pauli product. + out2in: A dictionary mapping output Pauli products to an input Pauli product, + or list of input Pauli products, or the string "auto" in order to + automatically find a satisfying list of Pauli products that produces the + output. + + Returns: + A stimflow.ChunkReflow instance containing the desired output-to-input mappings. + + Raises: + ValueError: + An output was mapped to "auto", but could not be formed as a product of the + available inputs. + + Examples: + >>> import stimflow as sf + >>> xi = sf.PauliMap({0: "X"}) + >>> ix = sf.PauliMap({1: "X"}) + >>> xx = sf.PauliMap({0: "X", 1: "X"}) + >>> sf.ChunkReflow.from_auto_rewrite( + ... inputs=[xi, xx], + ... out2in={ix: "auto", xi: "auto"}, + ... ) + stimflow.ChunkReflow( + out2in={ + stimflow.PauliMap({(1+0j): 'X'}): [ + stimflow.PauliMap({0j: 'X'}), + stimflow.PauliMap.from_xs([0j, (1+0j)]), + ], + stimflow.PauliMap({0j: 'X'}): [stimflow.PauliMap({0j: 'X'})], + }, + ) + """ ``` @@ -3460,7 +3599,20 @@ def with_obs_name( ) -> PauliMap: """Returns the same PauliMap, but with the given name. - Names are used to identify logical operators. + Names are used to identify logical operators. Other operators use `None` as their + name. + + Args: + name: The new name. + + Examples: + >>> import stimflow as sf + + >>> sf.PauliMap({0: "Z"}).with_obs_name("test") + stimflow.PauliMap({0j: 'Z'}, obs_name='test') + + >>> sf.PauliMap({0: "Z"}, obs_name='do not forget me').with_obs_name(None) + stimflow.PauliMap({0j: 'Z'}) """ ``` @@ -3614,10 +3766,39 @@ def find_logical_error( # (in class stimflow.StabilizerCode) @functools.cached_property def flat_logicals(self) -> tuple[PauliMap, ...]: - """Returns a list of the logical operators defined by the stabilizer code. + """Returns a tuple of the logical operators defined by the stabilizer code. It's "flat" because paired X/Z logicals are returned separately instead of as a tuple. + + Returns: + The tuple of logical operators. + + Examples: + >>> import stimflow as sf + >>> code = sf.StabilizerCode( + ... stabilizers=[], + ... logicals=[ + ... ( + ... sf.PauliMap({"X": [0, 1, 2]}, obs_name="pair_LX"), + ... sf.PauliMap({"Z": [0j, 1j, 2j]}, obs_name="pair_LZ"), + ... ), + ... sf.PauliMap({"X": [3, 4, 5]}, obs_name="commuting_x0"), + ... sf.PauliMap({"X": [6, 7, 8]}, obs_name="commuting_x1"), + ... ], + ... scattered_logicals=[ + ... sf.PauliMap({"X": [10, 11, 12]}, obs_name="scattered_x"), + ... sf.PauliMap({"Y": [10, 11j, 12j]}, obs_name="scattered_y"), + ... ], + ... ) + >>> for logical in code.flat_logicals: + ... print(logical) + (obs_name='pair_LX') X0*X1*X2 + (obs_name='pair_LZ') Z0*Z1j*Z2j + (obs_name='commuting_x0') X3*X4*X5 + (obs_name='commuting_x1') X6*X7*X8 + (obs_name='scattered_x') X10*X11*X12 + (obs_name='scattered_y') Y11j*Y12j*Y10 """ ``` @@ -4450,20 +4631,50 @@ def append_reindexed_content_to_circuit( obs_i2i: "dict[int, int | Literal['discard']]", rewrite_detector_time_coordinates: bool = False, ) -> None: - """Reindexes content and appends it to a circuit. + """Reindexes content from one circuit while appending it to another. - Note that QUBIT_COORDS instructions are skipped. + For example, if two circuits use different qubit-position-to-qubit-index mappings, this + method can be used to account for the difference while appending. + + Note that `QUBIT_COORDS` instructions in the `content` circuit are skipped. They aren't + appended to `out_circuit`. Args: out_circuit: The output circuit. The circuit being edited. content: The circuit to be appended to the output circuit. qubit_i2i: A dictionary specifying how qubit indices are remapped. Indices outside the map are not changed. - obs_i2i: A dictionary specifying how observable indices are remapped. Indices outside the - map are not changed. + obs_i2i: A dictionary specifying how observable indices are remapped. Indices + outside the map are not changed. Indices can be mapped to the string "discard" + in order to discard `OBSERVABLE_INCLUDE` operations from the source that target + that index (rather than rewriting the index and appending it to the destination + circuit). rewrite_detector_time_coordinates: Defaults to False. When set to True, SHIFT_COORD and DETECTOR instructions are automatically rewritten to track the passage of time without using the same detector position twice at the same time. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> out_circuit = stim.Circuit("H 5") + >>> sf.append_reindexed_content_to_circuit( + ... out_circuit=out_circuit, + ... content=stim.Circuit(''' + ... CX 0 1 + ... M 0 1 + ... OBSERVABLE_INCLUDE(0) rec[-2] + ... OBSERVABLE_INCLUDE(1) rec[-1] + ... '''), + ... qubit_i2i={0: 100, 1: 101}, + ... obs_i2i={1: 0, 0: "discard"}, + ... ) + >>> out_circuit + stim.Circuit(''' + H 5 + CX 100 101 + M 100 101 + OBSERVABLE_INCLUDE(0) rec[-1] + ''') """ ``` @@ -4533,6 +4744,60 @@ def gate_counts_for_circuit( Feedback instructions like `CX rec[-1] 0` become the gate "feedback". Sweep instructions like `CX sweep[2] 0` become the gate "sweep". + + + Args: + circuit: The circuit to count gates from. + + Returns: + A `collections.Counter` mapping gate names to gate counts. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> gates = sf.gate_counts_for_circuit(stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... H 0 1 2 3 + ... CX 0 1 + ... TICK + ... CX 2 3 + ... MZZ 2 3 + ... ''')) + >>> for k, v in sorted(gates.items()): + ... print(f'{k}: {v}') + CX: 2 + H: 4 + MZZ: 1 + QUBIT_COORDS: 1 + TICK: 1 + + >>> gates = sf.gate_counts_for_circuit(stim.Circuit(''' + ... MPP X0*X1 X0*Y1*Z2 + ... CX rec[-1] 2 rec[-1] 3 sweep[0] 2 + ... ''')) + >>> for k, v in sorted(gates.items()): + ... print(f'{k}: {v}') + MXX: 1 + MXYZ: 1 + feedback: 2 + sweep: 1 + + >>> gates = sf.gate_counts_for_circuit(stim.Circuit(''' + ... CX 0 1 + ... REPEAT 1000 { + ... H 0 1 + ... MPAD 0 0 0 0 + ... DETECTOR rec[-1] rec[-2] + ... TICK + ... } + ... ''')) + >>> for k, v in sorted(gates.items()): + ... print(f'{k}: {v}') + CX: 1 + DETECTOR: 1000 + H: 2000 + MPAD: 4000 + TICK: 1000 """ ``` @@ -4552,6 +4817,42 @@ def gates_used_by_circuit( Feedback instructions like `CX rec[-1] 0` become the gate "feedback". Sweep instructions like `CX sweep[2] 0` become the gate "sweep". + + Args: + circuit: The circuit to get gates from. + + Returns: + The set of names of gates being used. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> gates = sf.gates_used_by_circuit(stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... H 0 1 + ... CX 0 1 + ... TICK + ... CX 2 3 + ... MZZ 2 3 + ... ''')) + >>> sorted(gates) + ['CX', 'H', 'MZZ', 'QUBIT_COORDS', 'TICK'] + >>> gates = sf.gates_used_by_circuit(stim.Circuit(''' + ... MPP X0*X1 X0*Y1*Z2 + ... ''')) + >>> sorted(gates) + ['MXX', 'MXYZ'] + >>> gates = sf.gates_used_by_circuit(stim.Circuit(''' + ... M 0 + ... CX rec[-1] 2 + ... ''')) + >>> sorted(gates) + ['M', 'feedback'] + >>> gates = sf.gates_used_by_circuit(stim.Circuit(''' + ... CX sweep[0] 2 + ... ''')) + >>> sorted(gates) + ['sweep'] """ ``` @@ -4759,6 +5060,24 @@ def stim_circuit_with_transformed_coords( Returns: The transformed circuit. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> sf.stim_circuit_with_transformed_coords(stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... QUBIT_COORDS(1, 0) 1 + ... CX 0 1 + ... M 1 + ... DETECTOR(2, 3) rec[-1] + ... '''), lambda e: e*2j + 100) + stim.Circuit(''' + QUBIT_COORDS(100, 0) 0 + QUBIT_COORDS(100, 2) 1 + CX 0 1 + M 1 + DETECTOR(94, 4) rec[-1] + ''') """ ``` diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py index 66a8f7a2..5759265d 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_compiler.py @@ -231,9 +231,51 @@ def cur_circuit_html_viewer(self) -> stimflow.str_html: def finish_circuit(self) -> stim.Circuit: """Returns the circuit built by the compiler. - Performs some final translation steps: - - Re-indexing the qubits to be in a sorted order. - - Re-indexing the observables to omit discarded observable flows. + Also performs some final polishing steps on the circuit, such as re-indexing the + qubits to be in a sorted-by-position order and re-indexing the observables to omit + unused indices due to e.g. discarded observable flows. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> zz = sf.PauliMap({0: 'Z', 1 + 1j: 'Z'}) + >>> lz = sf.PauliMap({0: 'Z'}, obs_name='L_ZI') + >>> lx = sf.PauliMap({0: 'X', 1 + 1j: 'X'}, obs_name='L_XX') + >>> idle_chunk = sf.Chunk( + ... stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... QUBIT_COORDS(0, 1) 1 + ... QUBIT_COORDS(1, 1) 2 + ... R 1 + ... CX 0 1 2 1 + ... M 1 + ... '''), + ... flows=[ + ... sf.Flow(start=zz, measurement_indices=[0]), + ... sf.Flow(end=zz, measurement_indices=[0]), + ... sf.Flow(start=lz, end=lz), + ... sf.Flow(start=lx, end=lx), + ... ] + ... ) + + >>> compiler = sf.ChunkCompiler() + >>> compiler.append(idle_chunk.start_code().transversal_init_chunk(basis='Z')) + >>> compiler.append(idle_chunk) # Note: L_XX discarded by transversal chunks. + >>> compiler.append(idle_chunk.end_code().transversal_measure_chunk(basis='Z')) + >>> compiler.finish_circuit() + stim.Circuit(''' + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 1) 2 + R 0 2 1 + CX 0 1 2 1 + M 1 + DETECTOR(0.5, 0.5, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + M 2 0 + DETECTOR(0.5, 0.5, 0) rec[-3] rec[-2] rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + ''') """ if self.open_flows or self.waiting_for_magic_init: @@ -505,18 +547,74 @@ def cur_end_interface(self) -> ChunkInterface: return ChunkInterface(ports, discards=discards) def append(self, appended: Chunk | ChunkLoop | ChunkReflow) -> None: - """Appends a chunk to the circuit being built. + """Appends a circuit chunk, or other object, to the circuit being built. - The input flows of the appended chunk must exactly match the open outgoing flows of the - circuit so far. + The input flows of the appended chunk must exactly match the open outgoing flows of + the circuit so far. Args: appended: The object to append to the circuit. + This can be a Chunk, a ChunkReflow, or a ChunkLoop. + Unless `skip_verification_before_append=True` was specified when constructing the compiler, the `verify` method of this object will be called in order to ensure it is well form. If verification is skipped and the object is not well-formed, the compiler may output an invalid Stim circuit (e.g. with non-deterministic detectors). + + Examples: + >>> import stim + >>> import stimflow as sf + >>> zz = sf.PauliMap({0: 'Z', 1 + 1j: 'Z'}) + >>> lz = sf.PauliMap({0: 'Z'}, obs_name='L_REP_CODE_ZZ') + >>> idle_chunk = sf.Chunk( + ... stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... QUBIT_COORDS(0, 1) 1 + ... QUBIT_COORDS(1, 1) 2 + ... R 1 + ... CX 0 1 2 1 + ... M 1 + ... '''), + ... flows=[ + ... sf.Flow(start=zz, measurement_indices=[0]), + ... sf.Flow(end=zz, measurement_indices=[0]), + ... sf.Flow(start=lz, end=lz), + ... ] + ... ) + + >>> compiler = sf.ChunkCompiler() + >>> compiler.append(idle_chunk.start_code().transversal_init_chunk(basis='Z')) + >>> compiler.append(idle_chunk * 100) + >>> compiler.append(idle_chunk.end_code().transversal_measure_chunk(basis='Z')) + >>> compiler.finish_circuit() + stim.Circuit(''' + QUBIT_COORDS(0, 0) 0 + QUBIT_COORDS(0, 1) 1 + QUBIT_COORDS(1, 1) 2 + R 0 2 1 + CX 0 1 2 1 + M 1 + DETECTOR(0.5, 0.5, 0) rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + REPEAT 98 { + R 1 + CX 0 1 2 1 + M 1 + DETECTOR(0.5, 0.5, 0) rec[-2] rec[-1] + SHIFT_COORDS(0, 0, 1) + TICK + } + R 1 + CX 0 1 2 1 + M 1 + DETECTOR(0.5, 0.5, 0) rec[-2] rec[-1] + SHIFT_COORDS(0, 0, 1) + M 2 0 + DETECTOR(0.5, 0.5, 0) rec[-3] rec[-2] rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + ''') """ __tracebackhide__ = True if not self.skip_verification_before_append: diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py index 71a28198..9d8b060b 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow.py @@ -40,6 +40,47 @@ def __init__(self, out2in: dict[PauliMap, list[PauliMap]], discard_in: Iterable[ def from_auto_rewrite( *, inputs: Iterable[PauliMap], out2in: dict[PauliMap, list[PauliMap] | Literal["auto"]] ) -> ChunkReflow: + """Creates a ChunkReflow while allowing for some products to solved automatically. + + In particular, the `out2in` dictionary can map an output to the string "auto" + instead of to an explicit list of PauliMap inputs. The method will then solve for + the product of inputs that produces the output. + + Args: + inputs: The input Pauli products that are available for use when producing an + output Pauli product. + out2in: A dictionary mapping output Pauli products to an input Pauli product, + or list of input Pauli products, or the string "auto" in order to + automatically find a satisfying list of Pauli products that produces the + output. + + Returns: + A stimflow.ChunkReflow instance containing the desired output-to-input mappings. + + Raises: + ValueError: + An output was mapped to "auto", but could not be formed as a product of the + available inputs. + + Examples: + >>> import stimflow as sf + >>> xi = sf.PauliMap({0: "X"}) + >>> ix = sf.PauliMap({1: "X"}) + >>> xx = sf.PauliMap({0: "X", 1: "X"}) + >>> sf.ChunkReflow.from_auto_rewrite( + ... inputs=[xi, xx], + ... out2in={ix: "auto", xi: "auto"}, + ... ) + stimflow.ChunkReflow( + out2in={ + stimflow.PauliMap({(1+0j): 'X'}): [ + stimflow.PauliMap({0j: 'X'}), + stimflow.PauliMap.from_xs([0j, (1+0j)]), + ], + stimflow.PauliMap({0j: 'X'}): [stimflow.PauliMap({0j: 'X'})], + }, + ) + """ new_out2in: dict[PauliMap, list[PauliMap]] = {} unsolved: list[PauliMap] = [] for pk, pv in out2in.items(): @@ -289,10 +330,12 @@ def __repr__(self) -> str: lines.append(f" {v2!r},") lines.append(" ],") lines.append(" },") - lines.append(" discard_in=(") - for discarded_in in self.discard_in: - lines.append(f" {discarded_in!r},") - lines.append(" ),") + if self.discard_in: + lines.append(" discard_in=(") + for discarded_in in self.discard_in: + lines.append(f" {discarded_in!r},") + lines.append(" ),") + lines.append(")") return "\n".join(lines) def __str__(self) -> str: diff --git a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py index 8d67b562..69897a92 100644 --- a/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py +++ b/glue/stimflow/src/stimflow/_chunk/_chunk_reflow_test.py @@ -1,6 +1,21 @@ import stimflow +def test_repr(): + xi = stimflow.PauliMap({0: "X"}) + ix = stimflow.PauliMap({1: "X"}) + xx = stimflow.PauliMap({0: "X", 1: "X"}) + zz = stimflow.PauliMap({0: "Z", 1: "Z"}) + val = stimflow.ChunkReflow( + out2in={xi: [ix, xx], ix: [ix]}, + discard_in=[zz], + ) + repr_text = repr(val) + reconstructed = eval(repr_text, {"stimflow": stimflow}, {}) + assert reconstructed == val + assert repr(reconstructed) == repr_text + + def test_from_auto_rewrite_xs(): result = stimflow.ChunkReflow.from_auto_rewrite( inputs=[ diff --git a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py index b4034ae6..214c65d9 100644 --- a/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py +++ b/glue/stimflow/src/stimflow/_chunk/_stabilizer_code.py @@ -83,10 +83,39 @@ def patch(self) -> Patch: @functools.cached_property def flat_logicals(self) -> tuple[PauliMap, ...]: - """Returns a list of the logical operators defined by the stabilizer code. + """Returns a tuple of the logical operators defined by the stabilizer code. It's "flat" because paired X/Z logicals are returned separately instead of as a tuple. + + Returns: + The tuple of logical operators. + + Examples: + >>> import stimflow as sf + >>> code = sf.StabilizerCode( + ... stabilizers=[], + ... logicals=[ + ... ( + ... sf.PauliMap({"X": [0, 1, 2]}, obs_name="pair_LX"), + ... sf.PauliMap({"Z": [0j, 1j, 2j]}, obs_name="pair_LZ"), + ... ), + ... sf.PauliMap({"X": [3, 4, 5]}, obs_name="commuting_x0"), + ... sf.PauliMap({"X": [6, 7, 8]}, obs_name="commuting_x1"), + ... ], + ... scattered_logicals=[ + ... sf.PauliMap({"X": [10, 11, 12]}, obs_name="scattered_x"), + ... sf.PauliMap({"Y": [10, 11j, 12j]}, obs_name="scattered_y"), + ... ], + ... ) + >>> for logical in code.flat_logicals: + ... print(logical) + (obs_name='pair_LX') X0*X1*X2 + (obs_name='pair_LZ') Z0*Z1j*Z2j + (obs_name='commuting_x0') X3*X4*X5 + (obs_name='commuting_x1') X6*X7*X8 + (obs_name='scattered_x') X10*X11*X12 + (obs_name='scattered_y') Y11j*Y12j*Y10 """ result: list[PauliMap] = [] for logical in self.logicals: diff --git a/glue/stimflow/src/stimflow/_core/_circuit_util.py b/glue/stimflow/src/stimflow/_core/_circuit_util.py index fa7c84d3..79d714ea 100644 --- a/glue/stimflow/src/stimflow/_core/_circuit_util.py +++ b/glue/stimflow/src/stimflow/_core/_circuit_util.py @@ -77,6 +77,60 @@ def gate_counts_for_circuit(circuit: stim.Circuit) -> collections.Counter[str]: Feedback instructions like `CX rec[-1] 0` become the gate "feedback". Sweep instructions like `CX sweep[2] 0` become the gate "sweep". + + + Args: + circuit: The circuit to count gates from. + + Returns: + A `collections.Counter` mapping gate names to gate counts. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> gates = sf.gate_counts_for_circuit(stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... H 0 1 2 3 + ... CX 0 1 + ... TICK + ... CX 2 3 + ... MZZ 2 3 + ... ''')) + >>> for k, v in sorted(gates.items()): + ... print(f'{k}: {v}') + CX: 2 + H: 4 + MZZ: 1 + QUBIT_COORDS: 1 + TICK: 1 + + >>> gates = sf.gate_counts_for_circuit(stim.Circuit(''' + ... MPP X0*X1 X0*Y1*Z2 + ... CX rec[-1] 2 rec[-1] 3 sweep[0] 2 + ... ''')) + >>> for k, v in sorted(gates.items()): + ... print(f'{k}: {v}') + MXX: 1 + MXYZ: 1 + feedback: 2 + sweep: 1 + + >>> gates = sf.gate_counts_for_circuit(stim.Circuit(''' + ... CX 0 1 + ... REPEAT 1000 { + ... H 0 1 + ... MPAD 0 0 0 0 + ... DETECTOR rec[-1] rec[-2] + ... TICK + ... } + ... ''')) + >>> for k, v in sorted(gates.items()): + ... print(f'{k}: {v}') + CX: 1 + DETECTOR: 1000 + H: 2000 + MPAD: 4000 + TICK: 1000 """ ANNOTATION_OPS = { "DETECTOR", @@ -84,7 +138,6 @@ def gate_counts_for_circuit(circuit: stim.Circuit) -> collections.Counter[str]: "QUBIT_COORDS", "SHIFT_COORDS", "TICK", - "MPAD", } out: collections.Counter[str] = collections.Counter() @@ -155,55 +208,49 @@ def gates_used_by_circuit(circuit: stim.Circuit) -> set[str]: Feedback instructions like `CX rec[-1] 0` become the gate "feedback". Sweep instructions like `CX sweep[2] 0` become the gate "sweep". - """ - out = set() - for instruction in circuit: - if isinstance(instruction, stim.CircuitRepeatBlock): - out |= gates_used_by_circuit(instruction.body_copy()) - - elif instruction.name in ["CX", "CY", "CZ", "XCZ", "YCZ"]: - for a, b in instruction.target_groups(): - if a.is_measurement_record_target or b.is_measurement_record_target: - out.add("feedback") - elif a.is_sweep_bit_target or b.is_sweep_bit_target: - out.add("sweep") - else: - out.add(instruction.name) - elif instruction.name == "MPP": - op = "M" - targets = instruction.targets_copy() - is_continuing = True - for t in targets: - if t.is_combiner: - is_continuing = True - continue - p = ( - "X" - if t.is_x_target - else "Y" if t.is_y_target else "Z" if t.is_z_target else "?" - ) - if is_continuing: - op += p - is_continuing = False - else: - if op == "MZ": - op = "M" - out.add(op) - op = "M" + p - if op: - if op == "MZ": - op = "M" - out.add(op) - - else: - out.add(instruction.name) + Args: + circuit: The circuit to get gates from. - return out + Returns: + The set of names of gates being used. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> gates = sf.gates_used_by_circuit(stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... H 0 1 + ... CX 0 1 + ... TICK + ... CX 2 3 + ... MZZ 2 3 + ... ''')) + >>> sorted(gates) + ['CX', 'H', 'MZZ', 'QUBIT_COORDS', 'TICK'] + >>> gates = sf.gates_used_by_circuit(stim.Circuit(''' + ... MPP X0*X1 X0*Y1*Z2 + ... ''')) + >>> sorted(gates) + ['MXX', 'MXYZ'] + >>> gates = sf.gates_used_by_circuit(stim.Circuit(''' + ... M 0 + ... CX rec[-1] 2 + ... ''')) + >>> sorted(gates) + ['M', 'feedback'] + >>> gates = sf.gates_used_by_circuit(stim.Circuit(''' + ... CX sweep[0] 2 + ... ''')) + >>> sorted(gates) + ['sweep'] + """ + return set(gate_counts_for_circuit(circuit).keys()) def stim_circuit_with_transformed_coords( - circuit: stim.Circuit, transform: Callable[[complex], complex] + circuit: stim.Circuit, + transform: Callable[[complex], complex], ) -> stim.Circuit: """Returns an equivalent circuit, but with the qubit and detector position metadata modified. The "position" is assumed to be the first two coordinates. These are mapped to the real and @@ -221,6 +268,24 @@ def stim_circuit_with_transformed_coords( Returns: The transformed circuit. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> sf.stim_circuit_with_transformed_coords(stim.Circuit(''' + ... QUBIT_COORDS(0, 0) 0 + ... QUBIT_COORDS(1, 0) 1 + ... CX 0 1 + ... M 1 + ... DETECTOR(2, 3) rec[-1] + ... '''), lambda e: e*2j + 100) + stim.Circuit(''' + QUBIT_COORDS(100, 0) 0 + QUBIT_COORDS(100, 2) 1 + CX 0 1 + M 1 + DETECTOR(94, 4) rec[-1] + ''') """ result = stim.Circuit() for instruction in circuit: @@ -253,7 +318,9 @@ def stim_circuit_with_transformed_coords( def stim_circuit_with_transformed_moments( - circuit: stim.Circuit, *, moment_func: Callable[[stim.Circuit], stim.Circuit] + circuit: stim.Circuit, + *, + moment_func: Callable[[stim.Circuit], stim.Circuit], ) -> stim.Circuit: """Applies a transformation to regions of a circuit separated by TICKs and blocks. @@ -330,23 +397,53 @@ def append_reindexed_content_to_circuit( obs_i2i: dict[int, int | Literal["discard"]], rewrite_detector_time_coordinates: bool = False, ) -> None: - """Reindexes content and appends it to a circuit. + """Reindexes content from one circuit while appending it to another. + + For example, if two circuits use different qubit-position-to-qubit-index mappings, this + method can be used to account for the difference while appending. - Note that QUBIT_COORDS instructions are skipped. + Note that `QUBIT_COORDS` instructions in the `content` circuit are skipped. They aren't + appended to `out_circuit`. Args: out_circuit: The output circuit. The circuit being edited. content: The circuit to be appended to the output circuit. qubit_i2i: A dictionary specifying how qubit indices are remapped. Indices outside the map are not changed. - obs_i2i: A dictionary specifying how observable indices are remapped. Indices outside the - map are not changed. + obs_i2i: A dictionary specifying how observable indices are remapped. Indices + outside the map are not changed. Indices can be mapped to the string "discard" + in order to discard `OBSERVABLE_INCLUDE` operations from the source that target + that index (rather than rewriting the index and appending it to the destination + circuit). rewrite_detector_time_coordinates: Defaults to False. When set to True, SHIFT_COORD and DETECTOR instructions are automatically rewritten to track the passage of time without using the same detector position twice at the same time. + + Examples: + >>> import stim + >>> import stimflow as sf + >>> out_circuit = stim.Circuit("H 5") + >>> sf.append_reindexed_content_to_circuit( + ... out_circuit=out_circuit, + ... content=stim.Circuit(''' + ... CX 0 1 + ... M 0 1 + ... OBSERVABLE_INCLUDE(0) rec[-2] + ... OBSERVABLE_INCLUDE(1) rec[-1] + ... '''), + ... qubit_i2i={0: 100, 1: 101}, + ... obs_i2i={1: 0, 0: "discard"}, + ... ) + >>> out_circuit + stim.Circuit(''' + H 5 + CX 100 101 + M 100 101 + OBSERVABLE_INCLUDE(0) rec[-1] + ''') """ - def rewritten_targets(inst: stim.CircuitInstruction) -> list[stim.GateTarget]: + def _rewritten_targets(inst: stim.CircuitInstruction) -> list[stim.GateTarget]: new_targets: list[int | stim.GateTarget] = [] for t in inst.targets_copy(): if t.is_qubit_target: @@ -397,7 +494,7 @@ def rewritten_targets(inst: stim.CircuitInstruction) -> list[stim.GateTarget]: obs_index = obs_i2i.get(obs_index, obs_index) if obs_index != "discard": out_circuit.append( - "OBSERVABLE_INCLUDE", rewritten_targets(inst), obs_index, tag=inst.tag + "OBSERVABLE_INCLUDE", _rewritten_targets(inst), obs_index, tag=inst.tag ) elif inst.name == "MPAD": out_circuit.append(inst) @@ -408,7 +505,7 @@ def rewritten_targets(inst: stim.CircuitInstruction) -> list[stim.GateTarget]: out_circuit.append(inst) else: out_circuit.append( - inst.name, rewritten_targets(inst), inst.gate_args_copy(), tag=inst.tag + inst.name, _rewritten_targets(inst), inst.gate_args_copy(), tag=inst.tag ) if rewrite_detector_time_coordinates and det_offset_needed > 0: diff --git a/glue/stimflow/src/stimflow/_core/_pauli_map.py b/glue/stimflow/src/stimflow/_core/_pauli_map.py index 9874db4e..6e76aa87 100644 --- a/glue/stimflow/src/stimflow/_core/_pauli_map.py +++ b/glue/stimflow/src/stimflow/_core/_pauli_map.py @@ -177,7 +177,20 @@ def __iter__(self) -> Iterator[complex]: def with_obs_name(self, name: Any) -> PauliMap: """Returns the same PauliMap, but with the given name. - Names are used to identify logical operators. + Names are used to identify logical operators. Other operators use `None` as their + name. + + Args: + name: The new name. + + Examples: + >>> import stimflow as sf + + >>> sf.PauliMap({0: "Z"}).with_obs_name("test") + stimflow.PauliMap({0j: 'Z'}, obs_name='test') + + >>> sf.PauliMap({0: "Z"}, obs_name='do not forget me').with_obs_name(None) + stimflow.PauliMap({0j: 'Z'}) """ return PauliMap(self, obs_name=name)